From 97dcacf0f4283e5add22985f6d74707f8f0dc323 Mon Sep 17 00:00:00 2001 From: Eivind Jahren Date: Thu, 31 Aug 2023 17:01:11 +0200 Subject: [PATCH 1/3] Replace EclSum with resfo --- src/clib/lib/CMakeLists.txt | 1 - src/clib/lib/enkf/read_summary.cpp | 86 ---- src/ert/config/_read_summary.py | 425 ++++++++++++++++++ src/ert/config/summary_config.py | 25 +- .../status/test_tracking_integration.py | 12 +- tests/unit_tests/analysis/test_es_update.py | 4 +- .../config/config_dict_generator.py | 11 +- tests/unit_tests/config/summary_generator.py | 293 ++++++++++-- tests/unit_tests/config/test_read_summary.py | 290 ++++++++++++ .../0/summary_collector_1.csv | 8 +- .../0/summary_collector_2.csv | 6 +- .../0/summary_collector_3.csv | 8 +- .../0/summary_head.csv | 10 +- .../0/summary_tail.csv | 10 +- .../summary_restart | 12 +- tests/unit_tests/test_load_forward_model.py | 2 +- 16 files changed, 1008 insertions(+), 195 deletions(-) delete mode 100644 src/clib/lib/enkf/read_summary.cpp create mode 100644 src/ert/config/_read_summary.py create mode 100644 tests/unit_tests/config/test_read_summary.py diff --git a/src/clib/lib/CMakeLists.txt b/src/clib/lib/CMakeLists.txt index 749202c654f..919600d03bc 100644 --- a/src/clib/lib/CMakeLists.txt +++ b/src/clib/lib/CMakeLists.txt @@ -13,7 +13,6 @@ pybind11_add_module( job_queue/torque_driver.cpp job_queue/spawn.cpp enkf/enkf_obs.cpp - enkf/read_summary.cpp enkf/row_scaling.cpp) # ----------------------------------------------------------------- diff --git a/src/clib/lib/enkf/read_summary.cpp b/src/clib/lib/enkf/read_summary.cpp deleted file mode 100644 index 877dee3948a..00000000000 --- a/src/clib/lib/enkf/read_summary.cpp +++ /dev/null @@ -1,86 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include // must be included after pybind11.h - -static bool matches(const std::vector &patterns, const char *key) { - return std::any_of(patterns.cbegin(), patterns.cend(), - [key](const std::string &pattern) { - return fnmatch(pattern.c_str(), key, 0) == 0; - }); -} - -ERT_CLIB_SUBMODULE("_read_summary", m) { - m.def("read_dates", [](Cwrap summary) { - if (!PyDateTimeAPI) - PyDateTime_IMPORT; - - time_t_vector_type *tvec = rd_sum_alloc_time_vector(summary, true); - int size = time_t_vector_size(tvec); - pybind11::list result(size); - auto t = tm{}; - for (int i = 0; i < size; i++) { - auto timestamp = time_t_vector_iget(tvec, i); - auto success = ::gmtime_r(×tamp, &t); - if (success == nullptr) - throw std::runtime_error("Unable to parse unix timestamp: " + - std::to_string(timestamp)); - - if (!PyDateTimeAPI) // this is here to silence the linters. it will always be set. - throw std::runtime_error("Python DateTime API not loaded"); - - auto py_time = PyDateTime_FromDateAndTime( - t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, - t.tm_sec, 0); - if (py_time == nullptr) - throw std::runtime_error("Unable to create DateTime object " - "from unix timestamp: " + - std::to_string(timestamp)); - - result[i] = pybind11::reinterpret_steal(py_time); - } - - time_t_vector_free(tvec); - return result; - }); - m.def("read_summary", [](Cwrap summary, - std::vector keys) { - const rd_smspec_type *smspec = rd_sum_get_smspec(summary); - std::vector>> - summary_vectors{}; - std::vector seen_keys{}; - for (int i = 0; i < rd_smspec_num_nodes(smspec); i++) { - const rd::smspec_node &smspec_node = - rd_smspec_iget_node_w_node_index(smspec, i); - const char *key = smspec_node.get_gen_key1(); - if ((matches(keys, key)) && - !(std::find(seen_keys.begin(), seen_keys.end(), key) != - seen_keys.end())) { - seen_keys.push_back(key); - int start = rd_sum_get_first_report_step(summary); - int end = rd_sum_get_last_report_step(summary); - std::vector data{}; - int key_index = - rd_sum_get_general_var_params_index(summary, key); - for (int tstep = start; tstep <= end; tstep++) { - if (rd_sum_has_report_step(summary, tstep)) { - int time_index = rd_sum_iget_report_end(summary, tstep); - data.push_back( - rd_sum_iget(summary, time_index, key_index)); - } - } - summary_vectors.emplace_back(key, data); - } - } - return summary_vectors; - }); -} diff --git a/src/ert/config/_read_summary.py b/src/ert/config/_read_summary.py new file mode 100644 index 00000000000..a07be9ff112 --- /dev/null +++ b/src/ert/config/_read_summary.py @@ -0,0 +1,425 @@ +from __future__ import annotations + +import os +import os.path +import re +from datetime import datetime, timedelta +from enum import Enum, auto +from fnmatch import fnmatch +from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union + +import numpy as np +import numpy.typing as npt +import resfo +from pydantic import PositiveInt + +SPECIAL_KEYWORDS = [ + "NEWTON", + "NAIMFRAC", + "NLINEARS", + "NLINSMIN", + "NLINSMAX", + "ELAPSED", + "MAXDPR", + "MAXDSO", + "MAXDSG", + "MAXDSW", + "STEPTYPE", + "WNEWTON", +] + + +class _SummaryType(Enum): + AQUIFER = auto() + BLOCK = auto() + COMPLETION = auto() + FIELD = auto() + GROUP = auto() + LOCAL_BLOCK = auto() + LOCAL_COMPLETION = auto() + LOCAL_WELL = auto() + NETWORK = auto() + SEGMENT = auto() + WELL = auto() + REGION = auto() + INTER_REGION = auto() + OTHER = auto() + + @classmethod + def from_keyword(cls, summary_keyword: str) -> _SummaryType: + KEYWORD_TYPE_MAPPING = { + "A": cls.AQUIFER, + "B": cls.BLOCK, + "C": cls.COMPLETION, + "F": cls.FIELD, + "G": cls.GROUP, + "LB": cls.LOCAL_BLOCK, + "LC": cls.LOCAL_COMPLETION, + "LW": cls.LOCAL_WELL, + "N": cls.NETWORK, + "S": cls.SEGMENT, + "W": cls.WELL, + } + if summary_keyword == "": + raise ValueError("Got empty summary_keyword") + if any(special in summary_keyword for special in SPECIAL_KEYWORDS): + return cls.OTHER + if summary_keyword[0] in KEYWORD_TYPE_MAPPING: + return KEYWORD_TYPE_MAPPING[summary_keyword[0]] + if summary_keyword[0:2] in KEYWORD_TYPE_MAPPING: + return KEYWORD_TYPE_MAPPING[summary_keyword[0:2]] + if summary_keyword == "RORFR": + return cls.REGION + + if any( + re.match(pattern, summary_keyword) + for pattern in [r"R.FT.*", r"R..FT.*", r"R.FR.*", r"R..FR.*", r"R.F"] + ): + return cls.INTER_REGION + if summary_keyword[0] == "R": + return cls.REGION + + return cls.OTHER + + +def _cell_index( + array_index: int, nx: PositiveInt, ny: PositiveInt +) -> Tuple[int, int, int]: + k = array_index // (nx * ny) + array_index -= k * (nx * ny) + j = array_index // nx + array_index -= j * nx + + return array_index + 1, j + 1, k + 1 + + +def _check_if_missing( + keyword_name: str, missing_key: str, *test_vars: Optional[int] +) -> List[int]: + if any(v is None for v in test_vars): + raise ValueError( + f"Found {keyword_name} keyword in summary " + f"specification without {missing_key} keyword" + ) + return test_vars # type: ignore + + +def make_summary_key( + keyword: str, + number: Optional[int] = None, + name: Optional[str] = None, + nx: Optional[int] = None, + ny: Optional[int] = None, + lgr_name: Optional[str] = None, + li: Optional[int] = None, + lj: Optional[int] = None, + lk: Optional[int] = None, +) -> Optional[str]: + sum_type = _SummaryType.from_keyword(keyword) + if sum_type in [ + _SummaryType.FIELD, + _SummaryType.OTHER, + ]: + return keyword + if sum_type in [ + _SummaryType.REGION, + _SummaryType.AQUIFER, + ]: + return f"{keyword}:{number}" + if sum_type == _SummaryType.BLOCK: + nx, ny = _check_if_missing("block", "dimens", nx, ny) + (number,) = _check_if_missing("block", "nums", number) + i, j, k = _cell_index(number - 1, nx, ny) + return f"{keyword}:{i},{j},{k}" + if sum_type in [ + _SummaryType.GROUP, + _SummaryType.WELL, + ]: + return f"{keyword}:{name}" + if sum_type == _SummaryType.SEGMENT: + return f"{keyword}:{name}:{number}" + if sum_type == _SummaryType.COMPLETION: + nx, ny = _check_if_missing("completion", "dimens", nx, ny) + (number,) = _check_if_missing("completion", "nums", number) + i, j, k = _cell_index(number - 1, nx, ny) + return f"{keyword}:{name}:{i},{j},{k}" + if sum_type == _SummaryType.INTER_REGION: + (number,) = _check_if_missing("inter region", "nums", number) + r1 = number % 32768 + r2 = ((number - r1) // 32768) - 10 + return f"{keyword}:{r1}-{r2}" + if sum_type == _SummaryType.LOCAL_WELL: + return f"{keyword}:{lgr_name}:{name}" + if sum_type == _SummaryType.LOCAL_BLOCK: + return f"{keyword}:{lgr_name}:{li},{lj},{lk}" + if sum_type == _SummaryType.LOCAL_COMPLETION: + nx, ny = _check_if_missing("local completion", "dimens", nx, ny) + (number,) = _check_if_missing("local completion", "nums", number) + i, j, k = _cell_index(number - 1, nx, ny) + return f"{keyword}:{lgr_name}:{name}:{li},{lj},{lk}" + if sum_type == _SummaryType.NETWORK: + # This is consistent with resinsight but + # has a bug in resdata + # https://github.com/equinor/resdata/issues/943 + return keyword + raise ValueError(f"Unexpected keyword type: {sum_type}") + + +class DateUnit(Enum): + HOURS = auto() + DAYS = auto() + + def make_delta(self, val: float) -> timedelta: + if self == DateUnit.HOURS: + return timedelta(hours=val) + if self == DateUnit.DAYS: + return timedelta(days=val) + raise ValueError(f"Unknown date unit {val}") + + +def _is_unsmry(base: str, path: str) -> bool: + if "." not in path: + return False + splitted = path.split(".") + return splitted[-2].endswith(base) and splitted[-1].lower() in ["unsmry", "funsmry"] + + +def _is_smspec(base: str, path: str) -> bool: + if "." not in path: + return False + splitted = path.split(".") + return splitted[-2].endswith(base) and splitted[-1].lower() in ["smspec", "fsmspec"] + + +def _find_file_matching( + kind: str, case: str, predicate: Callable[[str, str], bool] +) -> str: + dir, base = os.path.split(case) + candidates = list(filter(lambda x: predicate(base, x), os.listdir(dir))) + if not candidates: + raise ValueError(f"Could not find any {kind} matching case path {case}") + if len(candidates) > 1: + raise ValueError( + f"Ambigous reference to {kind} in {case}, could be any of {candidates}" + ) + return os.path.join(dir, candidates[0]) + + +def _get_summary_filenames(filepath: str) -> Tuple[str, str]: + summary = _find_file_matching("unified summary file", filepath, _is_unsmry) + spec = _find_file_matching("smspec file", filepath, _is_smspec) + return summary, spec + + +def read_summary( + filepath: str, fetch_keys: Sequence[str] +) -> Tuple[List[str], Sequence[datetime], Any]: + summary, spec = _get_summary_filenames(filepath) + date_index, start_date, date_units, keys, indices = _read_spec(spec, fetch_keys) + fetched, time_map = _read_summary( + summary, start_date, date_units, indices, date_index + ) + return (keys, time_map, fetched) + + +def _key2str(key: Union[bytes, str]) -> str: + ret = key.decode() if isinstance(key, bytes) else key + assert isinstance(ret, str) + return ret.strip() + + +def _read_spec( + spec: str, fetch_keys: Sequence[str] +) -> Tuple[int, datetime, DateUnit, List[str], npt.NDArray[np.int32]]: + date = None + n = None + nx = None + ny = None + + arrays: Dict[str, Optional[npt.NDArray[Any]]] = { + kw: None + for kw in [ + "WGNAMES ", + "NUMS ", + "KEYWORDS", + "NUMLX ", + "NUMLY ", + "NUMLZ ", + "LGRNAMES", + "UNITS ", + ] + } + + if spec.lower().endswith("fsmspec"): + mode = "rt" + format = resfo.Format.FORMATTED + else: + mode = "rb" + format = resfo.Format.UNFORMATTED + + with open(spec, mode) as fp: + for entry in resfo.lazy_read(fp, format): + if all( + p is not None + for p in ( + [ + date, + n, + nx, + ny, + ] + + list(arrays.values()) + ) + ): + break + kw = entry.read_keyword() + if kw in arrays: + vals = entry.read_array() + if vals is resfo.MESS or isinstance(vals, resfo.MESS): + raise ValueError(f"{kw} in {spec} was MESS") + arrays[kw] = vals + if kw == "DIMENS ": + vals = entry.read_array() + if vals is resfo.MESS or isinstance(vals, resfo.MESS): + raise ValueError(f"DIMENS in {spec} was MESS") + size = len(vals) + n = vals[0] if size > 0 else None + nx = vals[1] if size > 1 else None + ny = vals[2] if size > 2 else None + if kw == "STARTDAT": + vals = entry.read_array() + if vals is resfo.MESS or isinstance(vals, resfo.MESS): + raise ValueError(f"Startdate in {spec} was MESS") + size = len(vals) + day = vals[0] if size > 0 else 0 + month = vals[1] if size > 1 else 0 + year = vals[2] if size > 2 else 0 + hour = vals[3] if size > 3 else 0 + minute = vals[4] if size > 4 else 0 + microsecond = vals[5] if size > 5 else 0 + date = datetime( + day=day, + month=month, + year=year, + hour=hour, + minute=minute, + second=microsecond // 10**6, + microsecond=microsecond % 10**6, + ) + keywords = arrays["KEYWORDS"] + wgnames = arrays["WGNAMES "] + nums = arrays["NUMS "] + numlx = arrays["NUMLX "] + numly = arrays["NUMLY "] + numlz = arrays["NUMLZ "] + lgr_names = arrays["LGRNAMES"] + + if date is None: + raise ValueError(f"keyword startdat missing in {spec}") + if keywords is None: + raise ValueError(f"keywords missing in {spec}") + if n is None: + n = len(keywords) + + indices: List[int] = [] + keys: List[str] = [] + date_index = None + + def optional_get(arr: Optional[npt.NDArray[Any]], idx: int) -> Any: + if arr is None: + return None + if len(arr) <= idx: + return None + return arr[idx] + + for i in range(n): + keyword = _key2str(keywords[i]) + if keyword == "TIME": + date_index = i + + name = optional_get(wgnames, i) + if name is not None: + name = _key2str(name) + num = optional_get(nums, i) + lgr_name = optional_get(lgr_names, i) + if lgr_name is not None: + lgr_name = _key2str(lgr_name) + li = optional_get(numlx, i) + lj = optional_get(numly, i) + lk = optional_get(numlz, i) + + key = make_summary_key(keyword, num, name, nx, ny, lgr_name, li, lj, lk) + if key is not None and _should_load_summary_key(key, fetch_keys): + if key in keys: + # only keep the index of the last occurrence of a key + # this is done for backwards compatability + # and to have unique keys + indices[keys.index(key)] = i + else: + indices.append(i) + keys.append(key) + + keys_array = np.array(keys) + rearranged = keys_array.argsort() + keys_array = keys_array[rearranged] + + indices_array = np.array(indices)[rearranged] + + units = arrays["UNITS "] + if units is None: + raise ValueError(f"keyword units missing in {spec}") + if date_index is None: + raise ValueError(f"KEYWORDS did not contain TIME in {spec}") + if date_index >= len(units): + raise ValueError(f"Unit missing for TIME in {spec}") + + return ( + date_index, + date, + DateUnit[_key2str(units[date_index])], + list(keys_array), + indices_array, + ) + + +def _read_summary( + summary: str, + start_date: datetime, + unit: DateUnit, + indices: npt.NDArray[np.int32], + date_index: int, +) -> Tuple[npt.NDArray[np.float32], List[datetime]]: + if summary.lower().endswith("funsmry"): + mode = "rt" + format = resfo.Format.FORMATTED + else: + mode = "rb" + format = resfo.Format.UNFORMATTED + + last_params = None + values: List[npt.NDArray[np.float32]] = [] + dates: List[datetime] = [] + + def read_params() -> None: + nonlocal last_params, values + if last_params is not None: + vals = last_params.read_array() + if vals is resfo.MESS or isinstance(vals, resfo.MESS): + raise ValueError(f"PARAMS in {summary} was MESS") + values.append(vals[indices]) + dates.append(start_date + unit.make_delta(float(vals[date_index]))) + last_params = None + + with open(summary, mode) as fp: + for entry in resfo.lazy_read(fp, format): + kw = entry.read_keyword() + if kw == "PARAMS ": + last_params = entry + if kw == "SEQHDR ": + read_params() + read_params() + return np.array(values).T, dates + + +def _should_load_summary_key(data_key: Any, user_set_keys: Sequence[str]) -> bool: + return any(fnmatch(data_key, key) for key in user_set_keys) diff --git a/src/ert/config/summary_config.py b/src/ert/config/summary_config.py index 1ec0fd1baae..a6760d02c21 100644 --- a/src/ert/config/summary_config.py +++ b/src/ert/config/summary_config.py @@ -6,13 +6,8 @@ from typing import TYPE_CHECKING, Set, Union import xarray as xr -from resdata.summary import Summary - -from ert._clib._read_summary import ( # pylint: disable=import-error - read_dates, - read_summary, -) +from ._read_summary import read_summary from .response_config import ResponseConfig if TYPE_CHECKING: @@ -34,18 +29,8 @@ def __post_init__(self) -> None: def read_from_file(self, run_path: str, iens: int) -> xr.Dataset: filename = self.input_file.replace("", str(iens)) - try: - summary = Summary( - f"{run_path}/{filename}", - include_restart=False, - lazy_load=False, - ) - except IOError as e: - raise ValueError( - "Could not find SUMMARY file or using non unified SUMMARY " - f"file from: {run_path}/{filename}.UNSMRY", - ) from e - time_map = read_dates(summary) + keys, time_map, data = read_summary(f"{run_path}/{filename}", self.keys) + if self.refcase: assert isinstance(self.refcase, set) missing = self.refcase.difference(time_map) @@ -58,10 +43,6 @@ def read_from_file(self, run_path: str, iens: int) -> xr.Dataset: f"{last} from: {run_path}/{filename}.UNSMRY" ) - summary_data = read_summary(summary, list(set(self.keys))) - summary_data.sort(key=lambda x: x[0]) - data = [d for _, d in summary_data] - keys = [k for k, _ in summary_data] ds = xr.Dataset( {"values": (["name", "time"], data)}, coords={"time": time_map, "name": keys}, diff --git a/tests/integration_tests/status/test_tracking_integration.py b/tests/integration_tests/status/test_tracking_integration.py index 11d92fd5ccb..6dbbcb32633 100644 --- a/tests/integration_tests/status/test_tracking_integration.py +++ b/tests/integration_tests/status/test_tracking_integration.py @@ -438,10 +438,10 @@ def test_tracking_missing_ecl( assert ( f"Realization: 0 failed after reaching max submit (1):\n\t\n" "status from done callback: " - "Could not find " - f"SUMMARY file or using non unified SUMMARY file from: " + "Could not find any unified " + f"summary file matching case path " f"{Path().absolute()}/simulations/realization-0/" - "iter-0/ECLIPSE_CASE.UNSMRY" + "iter-0/ECLIPSE_CASE" ) in caplog.messages # Just also check that it failed for the expected reason @@ -449,10 +449,10 @@ def test_tracking_missing_ecl( assert ( f"Realization: 0 failed after reaching max submit (1):\n\t\n" "status from done callback: " - "Could not find " - f"SUMMARY file or using non unified SUMMARY file from: " + "Could not find any unified " + f"summary file matching case path " f"{Path().absolute()}/simulations/realization-0/" - "iter-0/ECLIPSE_CASE.UNSMRY" + "iter-0/ECLIPSE_CASE" ) in failures[0].failed_msg thread.join() diff --git a/tests/unit_tests/analysis/test_es_update.py b/tests/unit_tests/analysis/test_es_update.py index f3f6ed81101..e08c40e84ce 100644 --- a/tests/unit_tests/analysis/test_es_update.py +++ b/tests/unit_tests/analysis/test_es_update.py @@ -274,7 +274,7 @@ def test_update_snapshot( ( [ 0.5895781800838542, - -0.4369786388277017, + -0.4369791317440733, -1.370782409107295, 0.7564469588868706, 0.21572672272162152, @@ -295,7 +295,7 @@ def test_update_snapshot( ( [ -4.47905516481858, - -0.4369786388277017, + -0.4369791317440733, 1.1932696713609265, 0.7564469588868706, 0.21572672272162152, diff --git a/tests/unit_tests/config/config_dict_generator.py b/tests/unit_tests/config/config_dict_generator.py index d57cd991cad..c6026767dda 100644 --- a/tests/unit_tests/config/config_dict_generator.py +++ b/tests/unit_tests/config/config_dict_generator.py @@ -393,16 +393,7 @@ def ert_config_values(draw, use_eclbase=booleans): smspec = draw( smspecs( sum_keys=st.just(sum_keys), - start_date=st.just( - Date( - year=first_date.year, - month=first_date.month, - day=first_date.day, - hour=first_date.hour, - minutes=first_date.minute, - micro_seconds=first_date.second * 10**6 + first_date.microsecond, - ) - ), + start_date=st.just(Date.from_datetime(first_date)), ) ) std_cutoff = draw(small_floats) diff --git a/tests/unit_tests/config/summary_generator.py b/tests/unit_tests/config/summary_generator.py index d4fc227cee1..bf796fc35c4 100644 --- a/tests/unit_tests/config/summary_generator.py +++ b/tests/unit_tests/config/summary_generator.py @@ -4,56 +4,146 @@ See https://opm-project.org/?page_id=955 """ from dataclasses import astuple, dataclass +from datetime import datetime, timedelta from enum import Enum, unique -from typing import Any, List, Tuple +from typing import Any, List, Optional, Tuple import hypothesis.strategies as st import numpy as np import resfo +from hypothesis import assume from hypothesis.extra.numpy import from_dtype from pydantic import PositiveInt, conint +from typing_extensions import Self + +from ert.config._read_summary import SPECIAL_KEYWORDS from .egrid_generator import GrdeclKeyword +""" +See section 11.2 in opm flow reference manual 2022-10 +for definition of summary variable names. +""" + +inter_region_summary_variables = [ + "RGFR", + "RGFR+", + "RGFR-", + "RGFT", + "RGFT+", + "RGFT-", + "RGFTG", + "RGFTL", + "ROFR", + "ROFR+", + "ROFR-", + "ROFT", + "ROFT+", + "ROFT-", + "ROFTG", + "ROFTL", + "RWFR", + "RWFR+", + "RWFR-", + "RWFT", + "RWFT+", + "RWFT-", + "RCFT", + "RSFT", + "RNFT", +] + @st.composite -def summary_variables(draw): - """ - Generator for summary variable mnemonic, See - section 11.2.1 in opm flow reference manual 2022-10. - """ +def root_memnonic(draw): first_character = draw(st.sampled_from("ABFGRWCS")) if first_character == "A": second_character = draw(st.sampled_from("ALN")) third_character = draw(st.sampled_from("QL")) fourth_character = draw(st.sampled_from("RT")) return first_character + second_character + third_character + fourth_character + else: + second_character = draw(st.sampled_from("OWGVLPT")) + third_character = draw(st.sampled_from("PIF")) + fourth_character = draw(st.sampled_from("RT")) + # local = draw(st.sampled_from(["", "L"])) if first_character in "BCW" else "" + return first_character + second_character + third_character + fourth_character + - kind = draw(st.sampled_from([1, 2, 3, 4])) - if kind == 1: +@st.composite +def summary_variables(draw): + kind = draw( + st.sampled_from( + [ + "special", + "network", + "exceptions", + "directional", + "up_or_down", + "mnemonic", + "segment", + "well", + "region2region", + "memnonic", + "region", + ] + ) + ) + if kind == "special": + return draw(st.sampled_from(SPECIAL_KEYWORDS)) + if kind == "exceptions": return draw( st.sampled_from( - ["BAPI", "BOSAT", "BPR", "FAQR", "FPR", "FWCT", "WBHP", "WWCT"] + ["BAPI", "BOSAT", "BPR", "FAQR", "FPR", "FWCT", "WBHP", "WWCT", "ROFR"] ) ) - elif kind == 2: + elif kind == "directional": direction = draw(st.sampled_from("IJK")) return ( draw(st.sampled_from(["FLOO", "VELG", "VELO", "FLOW", "VELW"])) + direction ) - elif kind == 3: + elif kind == "up_or_down": dimension = draw(st.sampled_from("XYZRT")) direction = draw(st.sampled_from(["", "-"])) return draw(st.sampled_from(["GKR", "OKR", "WKR"])) + dimension + direction + elif kind == "network": + root = draw(root_memnonic()) + return "N" + root + elif kind == "segment": + return draw( + st.sampled_from(["SALQ", "SFR", "SGFR", "SGFRF", "SGFRS", "SGFTA", "SGFT"]) + ) + elif kind == "well": + return draw( + st.one_of( + st.builds(lambda r: "W" + r, root_memnonic()), + st.sampled_from( + [ + "WBHP", + "WBP5", + "WBP4", + "WBP9", + "WBP", + "WBHPH", + "WBHPT", + "WPIG", + "WPIL", + "WPIO", + "WPI5", + ] + ), + ) + ) + elif kind == "region2region": + return draw(st.sampled_from(inter_region_summary_variables)) + elif kind == "region": + return draw(st.builds(lambda r: "R" + r, root_memnonic())) else: - second_character = draw(st.sampled_from("OWGVLPT")) - third_character = draw(st.sampled_from("PIF")) - fourth_character = draw(st.sampled_from("RT")) - return first_character + second_character + third_character + fourth_character + return draw(root_memnonic()) unit_names = st.sampled_from( - ["SM3/DAY", "BARSA", "SM3/SM3", "FRACTION", "DAYS", "YEARS", "SM3", "SECONDS"] + ["SM3/DAY", "BARSA", "SM3/SM3", "FRACTION", "DAYS", "HOURS", "SM3"] ) names = st.text( @@ -101,6 +191,28 @@ class Date: def to_ecl(self): return astuple(self) + def to_datetime(self) -> datetime: + return datetime( + year=self.year, + month=self.month, + day=self.day, + hour=self.hour, + minute=self.minutes, + second=self.micro_seconds // 10**6, + microsecond=self.micro_seconds % 10**6, + ) + + @classmethod + def from_datetime(cls, dt: datetime) -> Self: + return cls( + year=dt.year, + month=dt.month, + day=dt.day, + hour=dt.hour, + minutes=dt.minute, + micro_seconds=dt.second * 10**6 + dt.microsecond, + ) + @dataclass class Smspec: @@ -113,9 +225,14 @@ class Smspec: restarted_from_step: PositiveInt keywords: List[str] well_names: List[str] - region_numbers: List[str] + region_numbers: List[int] units: List[str] start_date: Date + lgr_names: Optional[List[str]] = None + lgrs: Optional[List[str]] = None + numlx: Optional[List[PositiveInt]] = None + numly: Optional[List[PositiveInt]] = None + numlz: Optional[List[PositiveInt]] = None def to_ecl(self) -> List[Tuple[str, Any]]: # The restart field contains 9 strings of length 8 which @@ -124,29 +241,36 @@ def to_ecl(self) -> List[Tuple[str, Any]]: # are spaces. (opm manual table F.44, keyword name RESTART) restart = self.restart.ljust(72, " ") restart_list = [restart[i * 8 : i * 8 + 8] for i in range(9)] - return [ - ("INTEHEAD", np.array(self.intehead.to_ecl(), dtype=np.int32)), - ("RESTART ", restart_list), - ( - "DIMENS ", - np.array( - [ - self.num_keywords, - self.nx, - self.ny, - self.nz, - 0, - self.restarted_from_step, - ], - dtype=np.int32, + return ( + [ + ("INTEHEAD", np.array(self.intehead.to_ecl(), dtype=np.int32)), + ("RESTART ", restart_list), + ( + "DIMENS ", + np.array( + [ + self.num_keywords, + self.nx, + self.ny, + self.nz, + 0, + self.restarted_from_step, + ], + dtype=np.int32, + ), ), - ), - ("KEYWORDS", [kw.ljust(8) for kw in self.keywords]), - ("WGNAMES ", self.well_names), - ("NUMS ", np.array(self.region_numbers, dtype=np.int32)), - ("UNITS ", self.units), - ("STARTDAT", np.array(self.start_date.to_ecl(), dtype=np.int32)), - ] + ("KEYWORDS", [kw.ljust(8) for kw in self.keywords]), + ("WGNAMES ", self.well_names), + ("NUMS ", np.array(self.region_numbers, dtype=np.int32)), + ("UNITS ", self.units), + ("STARTDAT", np.array(self.start_date.to_ecl(), dtype=np.int32)), + ] + + ([("LGRS ", self.lgrs)] if self.lgrs is not None else []) + + ([("LGRNAMES", self.lgr_names)] if self.lgr_names is not None else []) + + ([("NUMLX ", self.numlx)] if self.numlx is not None else []) + + ([("NUMLY ", self.numly)] if self.numly is not None else []) + + ([("NUMLZ ", self.numlz)] if self.numlz is not None else []) + ) def to_file(self, filelike, file_format: resfo.Format = resfo.Format.UNFORMATTED): resfo.write(filelike, self.to_ecl(), file_format) @@ -166,6 +290,7 @@ def smspecs( Strategy for smspec that ensures that the TIME parameter, as required by ert, is in the parameters list. """ + use_locals = draw(st.booleans()) sum_keys = draw(sum_keys) n = len(sum_keys) + 1 nx = draw(small_ints) @@ -174,9 +299,21 @@ def smspecs( keywords = ["TIME "] + sum_keys units = ["DAYS "] + draw(st.lists(unit_names, min_size=n - 1, max_size=n - 1)) well_names = [":+:+:+:+"] + draw(st.lists(names, min_size=n - 1, max_size=n - 1)) + if use_locals: # use local + lgrs = draw(st.lists(names, min_size=n, max_size=n)) + numlx = draw(st.lists(small_ints, min_size=n, max_size=n)) + numly = draw(st.lists(small_ints, min_size=n, max_size=n)) + numlz = draw(st.lists(small_ints, min_size=n, max_size=n)) + lgr_names = list(set(lgrs)) + else: + lgrs = None + numlx = None + numly = None + numlz = None + lgr_names = None region_numbers = [-32676] + draw( st.lists( - from_dtype(np.dtype(np.int32), min_value=0, max_value=10), + from_dtype(np.dtype(np.int32), min_value=1, max_value=nx * ny * nz), min_size=len(sum_keys), max_size=len(sum_keys), ) @@ -195,6 +332,11 @@ def smspecs( restart=names, keywords=st.just(keywords), well_names=st.just(well_names), + lgrs=st.just(lgrs), + numlx=st.just(numlx), + numly=st.just(numly), + numlz=st.just(numlz), + lgr_names=st.just(lgr_names), region_numbers=st.just(region_numbers), units=st.just(units), start_date=start_date, @@ -271,3 +413,74 @@ def unsmrys( ] steps.append(SummaryStep(r, minis)) return Unsmry(steps) + + +@st.composite +def summaries(draw): + sum_keys = draw(st.lists(summary_variables(), min_size=1)) + first_date = draw( + st.datetimes( + min_value=datetime.strptime("1999-1-1", "%Y-%m-%d"), + max_value=datetime.strptime("3000-1-1", "%Y-%m-%d"), + ) + ) + smspec = draw( + smspecs( + sum_keys=st.just(sum_keys), + start_date=st.just( + Date( + year=first_date.year, + month=first_date.month, + day=first_date.day, + hour=first_date.hour, + minutes=first_date.minute, + micro_seconds=first_date.second * 10**6 + first_date.microsecond, + ) + ), + ) + ) + assume( + len(set(zip(smspec.keywords, smspec.region_numbers, smspec.well_names))) + == len(smspec.keywords) + ) + dates = [0.0] + draw( + st.lists( + st.floats( + min_value=0.1, + max_value=250_000, # in days ~= 685 years + allow_nan=False, + allow_infinity=False, + ), + min_size=2, + max_size=100, + ) + ) + try: + _ = first_date + timedelta(days=max(dates)) + except (ValueError, OverflowError): # datetime has a max year + assume(False) + + ds = sorted(dates, reverse=True) + steps = [] + i = 0 + j = 0 + while len(ds) > 0: + minis = [] + for _ in range(draw(st.integers(min_value=1, max_value=len(ds)))): + minis.append( + SummaryMiniStep( + i, + [ds.pop()] + + draw( + st.lists( + positive_floats, + min_size=len(sum_keys), + max_size=len(sum_keys), + ) + ), + ) + ) + i += 1 + steps.append(SummaryStep(j, minis)) + j += 1 + return smspec, Unsmry(steps) diff --git a/tests/unit_tests/config/test_read_summary.py b/tests/unit_tests/config/test_read_summary.py new file mode 100644 index 00000000000..7b99c03d950 --- /dev/null +++ b/tests/unit_tests/config/test_read_summary.py @@ -0,0 +1,290 @@ +from datetime import datetime, timedelta +from itertools import zip_longest + +import hypothesis.strategies as st +import pytest +from hypothesis import given +from resdata.summary import Summary, SummaryVarType +from resfo import Format + +from ert.config._read_summary import _SummaryType, make_summary_key, read_summary + +from .summary_generator import ( + inter_region_summary_variables, + summaries, + summary_variables, +) + + +def to_ecl(st: _SummaryType) -> SummaryVarType: + if st == _SummaryType.AQUIFER: + return SummaryVarType.RD_SMSPEC_AQUIFER_VAR + if st == _SummaryType.BLOCK: + return SummaryVarType.RD_SMSPEC_BLOCK_VAR + if st == _SummaryType.COMPLETION: + return SummaryVarType.RD_SMSPEC_COMPLETION_VAR + if st == _SummaryType.FIELD: + return SummaryVarType.RD_SMSPEC_FIELD_VAR + if st == _SummaryType.GROUP: + return SummaryVarType.RD_SMSPEC_GROUP_VAR + if st == _SummaryType.LOCAL_BLOCK: + return SummaryVarType.RD_SMSPEC_LOCAL_BLOCK_VAR + if st == _SummaryType.LOCAL_COMPLETION: + return SummaryVarType.RD_SMSPEC_LOCAL_COMPLETION_VAR + if st == _SummaryType.LOCAL_WELL: + return SummaryVarType.RD_SMSPEC_LOCAL_WELL_VAR + if st == _SummaryType.NETWORK: + return SummaryVarType.RD_SMSPEC_NETWORK_VAR + if st == _SummaryType.SEGMENT: + return SummaryVarType.RD_SMSPEC_SEGMENT_VAR + if st == _SummaryType.WELL: + return SummaryVarType.RD_SMSPEC_WELL_VAR + if st == _SummaryType.REGION: + return SummaryVarType.RD_SMSPEC_REGION_VAR + if st == _SummaryType.INTER_REGION: + return SummaryVarType.RD_SMSPEC_REGION_2_REGION_VAR + if st == _SummaryType.OTHER: + return SummaryVarType.RD_SMSPEC_MISC_VAR + + +@pytest.mark.parametrize("keyword", ["AAQR", "AAQT"]) +def test_aquifer_variables_are_recognized(keyword): + assert Summary.var_type(keyword) == SummaryVarType.RD_SMSPEC_AQUIFER_VAR + assert _SummaryType.from_keyword(keyword) == _SummaryType.AQUIFER + + +@pytest.mark.parametrize("keyword", ["BOSAT"]) +def test_block_variables_are_recognized(keyword): + assert Summary.var_type(keyword) == SummaryVarType.RD_SMSPEC_BLOCK_VAR + assert _SummaryType.from_keyword(keyword) == _SummaryType.BLOCK + + +@pytest.mark.parametrize("keyword", ["LBOSAT"]) +def test_local_block_variables_are_recognized(keyword): + assert Summary.var_type(keyword) == SummaryVarType.RD_SMSPEC_LOCAL_BLOCK_VAR + assert _SummaryType.from_keyword(keyword) == _SummaryType.LOCAL_BLOCK + + +@pytest.mark.parametrize("keyword", ["CGORL"]) +def test_completion_variables_are_recognized(keyword): + assert Summary.var_type(keyword) == SummaryVarType.RD_SMSPEC_COMPLETION_VAR + assert _SummaryType.from_keyword(keyword) == _SummaryType.COMPLETION + + +@pytest.mark.parametrize("keyword", ["LCGORL"]) +def test_local_completion_variables_are_recognized(keyword): + assert Summary.var_type(keyword) == SummaryVarType.RD_SMSPEC_LOCAL_COMPLETION_VAR + assert _SummaryType.from_keyword(keyword) == _SummaryType.LOCAL_COMPLETION + + +@pytest.mark.parametrize("keyword", ["FGOR", "FOPR"]) +def test_field_variables_are_recognized(keyword): + assert Summary.var_type(keyword) == SummaryVarType.RD_SMSPEC_FIELD_VAR + assert _SummaryType.from_keyword(keyword) == _SummaryType.FIELD + + +@pytest.mark.parametrize("keyword", ["GGFT", "GOPR"]) +def test_group_variables_are_recognized(keyword): + assert Summary.var_type(keyword) == SummaryVarType.RD_SMSPEC_GROUP_VAR + assert _SummaryType.from_keyword(keyword) == _SummaryType.GROUP + + +@pytest.mark.parametrize("keyword", ["NOPR", "NGPR"]) +def test_network_variables_are_recognized(keyword): + assert Summary.var_type(keyword) == SummaryVarType.RD_SMSPEC_NETWORK_VAR + assert _SummaryType.from_keyword(keyword) == _SummaryType.NETWORK + + +@pytest.mark.parametrize("keyword", inter_region_summary_variables) +def test_inter_region_summary_variables_are_recognized(keyword): + assert Summary.var_type(keyword) == SummaryVarType.RD_SMSPEC_REGION_2_REGION_VAR + assert _SummaryType.from_keyword(keyword) == _SummaryType.INTER_REGION + + +@pytest.mark.parametrize("keyword", ["RORFR", "RPR", "ROPT"]) +def test_region_variables_are_recognized(keyword): + assert Summary.var_type(keyword) == SummaryVarType.RD_SMSPEC_REGION_VAR + assert _SummaryType.from_keyword(keyword) == _SummaryType.REGION + + +@pytest.mark.parametrize("keyword", ["SOPR"]) +def test_segment_variables_are_recognized(keyword): + assert Summary.var_type(keyword) == SummaryVarType.RD_SMSPEC_SEGMENT_VAR + assert _SummaryType.from_keyword(keyword) == _SummaryType.SEGMENT + + +@pytest.mark.parametrize("keyword", ["WOPR"]) +def test_well_variables_are_recognized(keyword): + assert Summary.var_type(keyword) == SummaryVarType.RD_SMSPEC_WELL_VAR + assert _SummaryType.from_keyword(keyword) == _SummaryType.WELL + + +@pytest.mark.parametrize("keyword", ["LWOPR"]) +def test_local_well_variables_are_recognized(keyword): + assert Summary.var_type(keyword) == SummaryVarType.RD_SMSPEC_LOCAL_WELL_VAR + assert _SummaryType.from_keyword(keyword) == _SummaryType.LOCAL_WELL + + +@given(summary_variables()) +def test_that_identify_var_type_is_same_as_ecl(variable): + assert Summary.var_type(variable) == to_ecl(_SummaryType.from_keyword(variable)) + + +@given(st.integers(), st.text(), st.integers(), st.integers()) +@pytest.mark.parametrize("keyword", ["FOPR", "NEWTON"]) +def test_summary_key_format_of_field_and_misc_is_identity( + keyword, number, name, nx, ny +): + assert make_summary_key(keyword, number, name, nx, ny) == keyword + + +@given(st.integers(), st.text(), st.integers(), st.integers()) +def test_network_variable_keys_has_keyword_as_summary_key(number, name, nx, ny): + assert make_summary_key("NOPR", number, name, nx, ny) == "NOPR" + + +@given(st.integers(), st.text(), st.integers(), st.integers()) +@pytest.mark.parametrize("keyword", ["GOPR", "WOPR"]) +def test_group_and_well_have_named_format(keyword, number, name, nx, ny): + assert make_summary_key(keyword, number, name, nx, ny) == f"{keyword}:{name}" + + +@given(st.text(), st.integers(), st.integers()) +@pytest.mark.parametrize("keyword", inter_region_summary_variables) +def test_inter_region_summary_format_contains_in_and_out_regions(keyword, name, nx, ny): + number = 3014660 + assert make_summary_key(keyword, number, name, nx, ny) == f"{keyword}:4-82" + + +@given(name=st.text()) +@pytest.mark.parametrize("keyword", ["BOPR", "BOSAT"]) +@pytest.mark.parametrize( + "nx,ny,number,indices", + [ + (1, 1, 1, "1,1,1"), + (2, 1, 2, "2,1,1"), + (1, 2, 2, "1,2,1"), + (3, 2, 3, "3,1,1"), + (3, 2, 9, "3,1,2"), + ], +) +def test_block_summary_format_have_cell_index(keyword, number, indices, name, nx, ny): + assert make_summary_key(keyword, number, name, nx, ny) == f"{keyword}:{indices}" + + +@given(name=st.text()) +@pytest.mark.parametrize("keyword", ["COPR"]) +@pytest.mark.parametrize( + "nx,ny,number,indices", + [ + (1, 1, 1, "1,1,1"), + (2, 1, 2, "2,1,1"), + (1, 2, 2, "1,2,1"), + (3, 2, 3, "3,1,1"), + (3, 2, 9, "3,1,2"), + ], +) +def test_completion_summary_format_have_cell_index_and_name( + keyword, number, indices, name, nx, ny +): + assert ( + make_summary_key(keyword, number, name, nx, ny) == f"{keyword}:{name}:{indices}" + ) + + +@given(summaries(), st.sampled_from(Format)) +def test_that_reading_summaries_returns_the_contents_of_the_file( + tmp_path_factory, summary, format +): + tmp_path = tmp_path_factory.mktemp("summary") + format_specifier = "F" if format == Format.FORMATTED else "" + smspec, unsmry = summary + unsmry.to_file(tmp_path / f"TEST.{format_specifier}UNSMRY", format) + smspec.to_file(tmp_path / f"TEST.{format_specifier}SMSPEC", format) + (keys, time_map, data) = read_summary(str(tmp_path / "TEST"), ["*"]) + + local_name = smspec.lgrs if smspec.lgrs else [] + lis = smspec.numlx if smspec.numlx else [] + ljs = smspec.numly if smspec.numly else [] + lks = smspec.numlz if smspec.numlz else [] + keys_in_smspec = [ + x + for x in map( + lambda x: make_summary_key(*x[:3], smspec.nx, smspec.ny, *x[3:]), + zip_longest( + [k.rstrip() for k in smspec.keywords], + smspec.region_numbers, + smspec.well_names, + local_name, + lis, + ljs, + lks, + fillvalue=None, + ), + ) + ] + assert set(keys) == set((k for k in keys_in_smspec if k)) + + def to_date(start_date: datetime, offset: float, unit: str) -> datetime: + if unit == "DAYS": + return start_date + timedelta(days=offset) + if unit == "HOURS": + return start_date + timedelta(hours=offset) + raise ValueError(f"Unknown time unit {unit}") + + assert all( + abs(actual - expected) <= timedelta(minutes=15) + for actual, expected in zip_longest( + time_map, + [ + to_date( + smspec.start_date.to_datetime(), + s.ministeps[-1].params[0], + smspec.units[0].strip(), + ) + for s in unsmry.steps + ], + ) + ) + for key, d in zip_longest(keys, data): + index = [i for i, k in enumerate(keys_in_smspec) if k == key][-1] + assert [s.ministeps[-1].params[index] for s in unsmry.steps] == pytest.approx(d) + + +@pytest.mark.parametrize( + "spec_contents, smry_contents", + [ + (b"", b""), + (b"1", b"1"), + (b"\x00\x00\x00\x10", b"1"), + (b"\x00\x00\x00\x10UNEXPECTED", b"\x00\x00\x00\x10UNEXPECTED"), + ( + b"\x00\x00\x00\x10UNEXPECTED", + b"\x00\x00\x00\x10KEYWORD1" + (2200).to_bytes(4, byteorder="big"), + ), + ( + b"\x00\x00\x00\x10FOOOOOOO\x00", + b"\x00\x00\x00\x10FOOOOOOO" + + (2300).to_bytes(4, byteorder="big") + + b"INTE\x00\x00\x00\x10" + + b"\x00" * (4 * 2300 + 4 * 6), + ), + ( + b"\x00\x00\x00\x10FOOOOOOO\x00\x00\x00\x01" + + b"INTE" + + b"\x00\x00\x00\x10" + + (4).to_bytes(4, signed=True, byteorder="big") + + b"\x00" * 4 + + (4).to_bytes(4, signed=True, byteorder="big"), + b"\x00\x00\x00\x10FOOOOOOO\x00", + ), + ], +) +def test_that_incorrect_summary_files_raises_informative_errors( + smry_contents, spec_contents, tmp_path +): + (tmp_path / "test.UNSMRY").write_bytes(smry_contents) + (tmp_path / "test.SMSPEC").write_bytes(spec_contents) + + with pytest.raises(ValueError): + read_summary(str(tmp_path / "test"), ["*"]) diff --git a/tests/unit_tests/snapshots/test_libres_facade/test_summary_collector/0/summary_collector_1.csv b/tests/unit_tests/snapshots/test_libres_facade/test_summary_collector/0/summary_collector_1.csv index 14ef7548276..aee5554b0e1 100644 --- a/tests/unit_tests/snapshots/test_libres_facade/test_summary_collector/0/summary_collector_1.csv +++ b/tests/unit_tests/snapshots/test_libres_facade/test_summary_collector/0/summary_collector_1.csv @@ -1,5 +1,5 @@ Realization,Date,"BPR:1,3,8","BPR:5,5,5",FGIP,FGIPH,FGOR,FGORH,FGPR,FGPRH,FGPT,FGPTH,FOIP,FOIPH,FOPR,FOPRH,FOPT,FOPTH,FWCT,FWCTH,FWIP,FWIPH,FWPR,FWPRH,FWPT,FWPTH,WGOR:OP1,WGOR:OP2,WGORH:OP1,WGORH:OP2,WGPR:OP1,WGPR:OP2,WGPRH:OP1,WGPRH:OP2,WOPR:OP1,WOPR:OP2,WOPRH:OP1,WOPRH:OP2,WWCT:OP1,WWCT:OP2,WWCTH:OP1,WWCTH:OP2,WWPR:OP1,WWPR:OP2,WWPRH:OP1,WWPRH:OP2 -0,2010-01-10,0.9996,0.9996,2499.4473,2499.9956,1.0,1.0,0.0557,0.0012,0.5528,0.0044,1999.4462,1999.994,0.056,0.0017,0.5538,0.0059,0.1776,0.0002,2249.4492,2249.9998,0.0551,0.0,0.5507,0.0001,1.0,1.0,1.0,1.0,0.0557,0.0,0.0006,0.0006,0.056,0.0,0.0008,0.0008,0.3552,0.0,0.0001,0.0002,0.0551,0.0,0.0,0.0 -1,2010-01-10,0.9996,0.9996,2499.8467,2499.9956,1.0,1.0,0.0157,0.0012,0.1533,0.0044,1999.8458,1999.994,0.016,0.0017,0.1542,0.0059,0.0657,0.0002,2249.8489,2249.9998,0.0151,0.0,0.1512,0.0001,1.0,1.0,1.0,1.0,0.0,0.0157,0.0006,0.0006,0.0,0.016,0.0008,0.0008,0.0,0.1314,0.0001,0.0002,0.0,0.0151,0.0,0.0 -2,2010-01-10,0.9996,0.9996,2500.0,2499.9956,1.0,1.0,0.0,0.0012,0.0,0.0044,2000.0,1999.994,0.0,0.0017,0.0,0.0059,0.0,0.0002,2250.0,2249.9998,0.0,0.0,0.0,0.0001,1.0,1.0,1.0,1.0,0.0,0.0,0.0006,0.0006,0.0,0.0,0.0008,0.0008,0.0,0.0,0.0001,0.0002,0.0,0.0,0.0,0.0 -3,2010-01-10,0.9996,0.9996,2497.1733,2499.9956,0.9994,1.0,0.2835,0.0012,2.8267,0.0044,1997.1715,1999.994,0.284,0.0017,2.8285,0.0059,0.4825,0.0002,2247.1775,2249.9998,0.2823,0.0,2.8224,0.0001,1.0,0.9987,1.0,1.0,0.0879,0.1956,0.0006,0.0006,0.0882,0.1958,0.0008,0.0008,0.4661,0.4989,0.0001,0.0002,0.0873,0.195,0.0,0.0 +0,2010-01-10,0.9996,0.9996,2499.4473,2499.9956,1.0,1.0,0.0557,0.0012,0.5528,0.0044,1999.4462,1999.994,0.056,0.0017,0.5538,0.0059,0.1776,0.0002,2249.4492,2249.9998,0.0551,0.0,0.5507,1e-04,1.0,1.0,1.0,1.0,0.0557,0.0,0.0006,0.0006,0.056,0.0,0.0008,0.0008,0.3552,0.0,1e-04,0.0002,0.0551,0.0,0.0,0.0 +1,2010-01-10,0.9996,0.9996,2499.8467,2499.9956,1.0,1.0,0.0157,0.0012,0.1533,0.0044,1999.8458,1999.994,0.016,0.0017,0.1542,0.0059,0.0657,0.0002,2249.8489,2249.9998,0.0151,0.0,0.1512,1e-04,1.0,1.0,1.0,1.0,0.0,0.0157,0.0006,0.0006,0.0,0.016,0.0008,0.0008,0.0,0.1314,1e-04,0.0002,0.0,0.0151,0.0,0.0 +2,2010-01-10,0.9996,0.9996,2500.0,2499.9956,1.0,1.0,0.0,0.0012,0.0,0.0044,2000.0,1999.994,0.0,0.0017,0.0,0.0059,0.0,0.0002,2250.0,2249.9998,0.0,0.0,0.0,1e-04,1.0,1.0,1.0,1.0,0.0,0.0,0.0006,0.0006,0.0,0.0,0.0008,0.0008,0.0,0.0,1e-04,0.0002,0.0,0.0,0.0,0.0 +3,2010-01-10,0.9996,0.9996,2497.1733,2499.9956,0.9994,1.0,0.2835,0.0012,2.8267,0.0044,1997.1716,1999.994,0.284,0.0017,2.8285,0.0059,0.4825,0.0002,2247.1775,2249.9998,0.2823,0.0,2.8224,1e-04,1.0,0.9987,1.0,1.0,0.0879,0.1956,0.0006,0.0006,0.0882,0.1958,0.0008,0.0008,0.4661,0.4989,1e-04,0.0002,0.0873,0.195,0.0,0.0 diff --git a/tests/unit_tests/snapshots/test_libres_facade/test_summary_collector/0/summary_collector_2.csv b/tests/unit_tests/snapshots/test_libres_facade/test_summary_collector/0/summary_collector_2.csv index 736dbb2194a..972fb720302 100644 --- a/tests/unit_tests/snapshots/test_libres_facade/test_summary_collector/0/summary_collector_2.csv +++ b/tests/unit_tests/snapshots/test_libres_facade/test_summary_collector/0/summary_collector_2.csv @@ -1,5 +1,5 @@ Realization,Date,WWCT:OP1,WWCT:OP2 -0,2010-01-10,0.3551691770553589,0.0 -1,2010-01-10,0.0,0.13138170540332794 +0,2010-01-10,0.35516918,0.0 +1,2010-01-10,0.0,0.1313817 2,2010-01-10,0.0,0.0 -3,2010-01-10,0.46612998843193054,0.49889394640922546 +3,2010-01-10,0.46613,0.49889395 diff --git a/tests/unit_tests/snapshots/test_libres_facade/test_summary_collector/0/summary_collector_3.csv b/tests/unit_tests/snapshots/test_libres_facade/test_summary_collector/0/summary_collector_3.csv index 4b65eff5905..f6289d47497 100644 --- a/tests/unit_tests/snapshots/test_libres_facade/test_summary_collector/0/summary_collector_3.csv +++ b/tests/unit_tests/snapshots/test_libres_facade/test_summary_collector/0/summary_collector_3.csv @@ -1,5 +1,5 @@ Realization,Date,WWCT:OP1,WWCT:OP2 -4,2010-01-10,0.19479288160800934,0.0 -4,2010-01-20,0.19516849517822266,0.0 -4,2010-01-30,0.19581304490566254,0.0 -4,2010-02-09,0.19672726094722748,0.0 +4,2010-01-10,0.19479288,0.0 +4,2010-01-20,0.1951685,0.0 +4,2010-01-30,0.19581304,0.0 +4,2010-02-09,0.19672726,0.0 diff --git a/tests/unit_tests/snapshots/test_libres_facade/test_summary_data_verify_indices_and_values/0/summary_head.csv b/tests/unit_tests/snapshots/test_libres_facade/test_summary_data_verify_indices_and_values/0/summary_head.csv index 7d94f6ba7b7..06558871da3 100644 --- a/tests/unit_tests/snapshots/test_libres_facade/test_summary_data_verify_indices_and_values/0/summary_head.csv +++ b/tests/unit_tests/snapshots/test_libres_facade/test_summary_data_verify_indices_and_values/0/summary_head.csv @@ -1,8 +1,8 @@ ,FOPR,FOPR,FOPR,FOPR,FOPR Realization,0,1,2,3,4 Date,,,,, -2010-01-10,0.05596115440130234,0.015982799232006073,0.0,0.28399229049682617,0.02509653940796852 -2010-01-20,0.05905991047620773,0.018984779715538025,0.0,0.2900899052619934,0.02827547676861286 -2010-01-30,0.06433762609958649,0.024110613390803337,0.0,0.30051347613334656,0.03370004892349243 -2010-02-09,0.07174286246299744,0.03132806345820427,0.0009306804859079421,0.31520578265190125,0.041331253945827484 -2010-02-19,0.08120841532945633,0.040592338889837265,0.009585856460034847,0.3340320289134979,0.051114898175001144 +2010-01-10,0.055961154,0.0159828,0.0,0.2839923,0.02509654 +2010-01-20,0.05905991,0.01898478,0.0,0.2900899,0.028275477 +2010-01-30,0.064337626,0.024110613,0.0,0.30051348,0.03370005 +2010-02-09,0.07174286,0.031328063,0.0009306805,0.31520578,0.041331254 +2010-02-19,0.081208415,0.04059234,0.009585856,0.33403203,0.0511149 diff --git a/tests/unit_tests/snapshots/test_libres_facade/test_summary_data_verify_indices_and_values/0/summary_tail.csv b/tests/unit_tests/snapshots/test_libres_facade/test_summary_data_verify_indices_and_values/0/summary_tail.csv index c2be1d7f9a9..04ecf554393 100644 --- a/tests/unit_tests/snapshots/test_libres_facade/test_summary_data_verify_indices_and_values/0/summary_tail.csv +++ b/tests/unit_tests/snapshots/test_libres_facade/test_summary_data_verify_indices_and_values/0/summary_tail.csv @@ -1,8 +1,8 @@ ,FOPR,FOPR,FOPR,FOPR,FOPR Realization,0,1,2,3,4 Date,,,,, -2015-05-14,0.05978194996714592,0.02822822891175747,0.0,0.2988854944705963,0.031847789883613586 -2015-05-24,0.06022725999355316,0.02732691913843155,0.0,0.29930371046066284,0.03208010271191597 -2015-06-03,0.060678575187921524,0.026679888367652893,0.0,0.29959961771965027,0.032258305698633194 -2015-06-13,0.06101493909955025,0.026284758001565933,0.0,0.29972729086875916,0.032372280955314636 -2015-06-23,0.06115317344665527,0.026137633249163628,0.0,0.2997667193412781,0.03241586685180664 +2015-05-14,0.05978195,0.028228229,0.0,0.2988855,0.03184779 +2015-05-24,0.06022726,0.02732692,0.0,0.2993037,0.032080103 +2015-06-03,0.060678575,0.026679888,0.0,0.29959962,0.032258306 +2015-06-13,0.06101494,0.026284758,0.0,0.2997273,0.03237228 +2015-06-23,0.061153173,0.026137633,0.0,0.29976672,0.032415867 diff --git a/tests/unit_tests/snapshots/test_summary_response/test_load_summary_response_restart_not_zero/summary_restart b/tests/unit_tests/snapshots/test_summary_response/test_load_summary_response_restart_not_zero/summary_restart index c953c196311..3cfe2809703 100644 --- a/tests/unit_tests/snapshots/test_summary_response/test_load_summary_response_restart_not_zero/summary_restart +++ b/tests/unit_tests/snapshots/test_summary_response/test_load_summary_response_restart_not_zero/summary_restart @@ -1,7 +1,7 @@ Realization,Date,FGIP,FGIR,FGIRH,FGIT,FGITH,FGLIR,FGLR,FGOR,FGORH,FGPP,FGPR,FGPRF,FGPRH,FGPRS,FGPT -0,2020-07-02,5737580544.0,0.0,0.0,0.0,0.0,0.0,68.79610443115234,146.78567504882812,146.78567504882812,2777220.0,853880.5625,165187.046875,853880.5625,688693.5625,1168540544.0 -0,2020-08-01,5711617536.0,0.0,0.0,0.0,0.0,0.0,68.93971252441406,151.33901977539062,151.33901977539062,2757425.0,866019.9375,192419.359375,866019.9375,673600.5625,1194503808.0 -0,2020-09-01,5684979200.0,0.0,0.0,0.0,0.0,0.0,68.25880432128906,154.72657775878906,154.72657775878906,2712226.0,856132.875,208552.921875,856132.875,647579.9375,1221141888.0 -0,2020-10-01,5659591680.0,0.0,0.0,0.0,0.0,0.0,66.95653533935547,156.4752960205078,156.4752960205078,2654457.75,837970.5,214351.671875,837970.5,623618.875,1246529280.0 -0,2020-11-01,5634260480.0,0.0,0.0,0.0,0.0,0.0,64.8019790649414,155.9357147216797,155.9357147216797,2580463.0,809597.9375,207660.796875,809597.9375,601937.125,1271860352.0 -0,2020-12-01,5610946048.0,0.0,0.0,0.0,0.0,0.0,62.26524353027344,153.91632080078125,153.91632080078125,2505382.5,777148.4375,193741.40625,777148.4375,583407.0625,1295174912.0 +0,2020-07-02,5737580500.0,0.0,0.0,0.0,0.0,0.0,68.796104,146.78568,146.78568,2777220.0,853880.56,165187.05,853880.56,688693.56,1168540500.0 +0,2020-08-01,5711617500.0,0.0,0.0,0.0,0.0,0.0,68.93971,151.33902,151.33902,2757425.0,866019.94,192419.36,866019.94,673600.56,1194503800.0 +0,2020-09-01,5684979000.0,0.0,0.0,0.0,0.0,0.0,68.258804,154.72658,154.72658,2712226.0,856132.9,208552.92,856132.9,647579.94,1221141900.0 +0,2020-10-01,5659591700.0,0.0,0.0,0.0,0.0,0.0,66.956535,156.4753,156.4753,2654457.8,837970.5,214351.67,837970.5,623618.9,1246529300.0 +0,2020-11-01,5634260500.0,0.0,0.0,0.0,0.0,0.0,64.80198,155.93571,155.93571,2580463.0,809597.94,207660.8,809597.94,601937.1,1271860400.0 +0,2020-12-01,5610946000.0,0.0,0.0,0.0,0.0,0.0,62.265244,153.91632,153.91632,2505382.5,777148.44,193741.4,777148.44,583407.06,1295174900.0 diff --git a/tests/unit_tests/test_load_forward_model.py b/tests/unit_tests/test_load_forward_model.py index 380d88d2c0b..d00c99bdbc8 100644 --- a/tests/unit_tests/test_load_forward_model.py +++ b/tests/unit_tests/test_load_forward_model.py @@ -149,7 +149,7 @@ def test_load_forward_model(snake_oil_default_storage): ), pytest.param( "SUMMARY *", - (0, "Could not find SUMMARY file"), + (0, "Could not find any unified summary file"), id=( "Check that loading fails if we have configured" "SUMMARY but no summary is available in the run path" From 68fc26df7eb6c6e0d38e0bcd96ccb5e99b0e35a2 Mon Sep 17 00:00:00 2001 From: Eivind Jahren Date: Mon, 8 Jan 2024 13:01:16 +0100 Subject: [PATCH 2/3] Add more tests of error messages --- src/ert/config/_read_summary.py | 100 ++++++---- .../config/config_dict_generator.py | 1 + tests/unit_tests/config/summary_generator.py | 33 ++-- tests/unit_tests/config/test_read_summary.py | 178 +++++++++++++++--- 4 files changed, 238 insertions(+), 74 deletions(-) diff --git a/src/ert/config/_read_summary.py b/src/ert/config/_read_summary.py index a07be9ff112..c3ea7cf5ab1 100644 --- a/src/ert/config/_read_summary.py +++ b/src/ert/config/_read_summary.py @@ -6,7 +6,7 @@ from datetime import datetime, timedelta from enum import Enum, auto from fnmatch import fnmatch -from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union +from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, TypeVar, Union import numpy as np import numpy.typing as npt @@ -61,7 +61,7 @@ def from_keyword(cls, summary_keyword: str) -> _SummaryType: "W": cls.WELL, } if summary_keyword == "": - raise ValueError("Got empty summary_keyword") + raise ValueError("Got empty summary keyword") if any(special in summary_keyword for special in SPECIAL_KEYWORDS): return cls.OTHER if summary_keyword[0] in KEYWORD_TYPE_MAPPING: @@ -93,9 +93,12 @@ def _cell_index( return array_index + 1, j + 1, k + 1 +T = TypeVar("T") + + def _check_if_missing( - keyword_name: str, missing_key: str, *test_vars: Optional[int] -) -> List[int]: + keyword_name: str, missing_key: str, *test_vars: Optional[T] +) -> List[T]: if any(v is None for v in test_vars): raise ValueError( f"Found {keyword_name} keyword in summary " @@ -149,13 +152,17 @@ def make_summary_key( r2 = ((number - r1) // 32768) - 10 return f"{keyword}:{r1}-{r2}" if sum_type == _SummaryType.LOCAL_WELL: + (name,) = _check_if_missing("local well", "WGNAMES", name) + (lgr_name,) = _check_if_missing("local well", "LGRS", lgr_name) return f"{keyword}:{lgr_name}:{name}" if sum_type == _SummaryType.LOCAL_BLOCK: + li, lj, lk = _check_if_missing("local block", "NUMLX", li, lj, lk) + (lgr_name,) = _check_if_missing("local block", "LGRS", lgr_name) return f"{keyword}:{lgr_name}:{li},{lj},{lk}" if sum_type == _SummaryType.LOCAL_COMPLETION: - nx, ny = _check_if_missing("local completion", "dimens", nx, ny) - (number,) = _check_if_missing("local completion", "nums", number) - i, j, k = _cell_index(number - 1, nx, ny) + li, lj, lk = _check_if_missing("local completion", "NUMLX", li, lj, lk) + (name,) = _check_if_missing("local completion", "WGNAMES", name) + (lgr_name,) = _check_if_missing("local completion", "LGRS", lgr_name) return f"{keyword}:{lgr_name}:{name}:{li},{lj},{lk}" if sum_type == _SummaryType.NETWORK: # This is consistent with resinsight but @@ -200,7 +207,7 @@ def _find_file_matching( raise ValueError(f"Could not find any {kind} matching case path {case}") if len(candidates) > 1: raise ValueError( - f"Ambigous reference to {kind} in {case}, could be any of {candidates}" + f"Ambiguous reference to {kind} in {case}, could be any of {candidates}" ) return os.path.join(dir, candidates[0]) @@ -215,10 +222,13 @@ def read_summary( filepath: str, fetch_keys: Sequence[str] ) -> Tuple[List[str], Sequence[datetime], Any]: summary, spec = _get_summary_filenames(filepath) - date_index, start_date, date_units, keys, indices = _read_spec(spec, fetch_keys) - fetched, time_map = _read_summary( - summary, start_date, date_units, indices, date_index - ) + try: + date_index, start_date, date_units, keys, indices = _read_spec(spec, fetch_keys) + fetched, time_map = _read_summary( + summary, start_date, date_units, indices, date_index + ) + except resfo.ResfoParsingError as err: + raise ValueError(f"Failed to read summary file {filepath}: {err}") from err return (keys, time_map, fetched) @@ -228,6 +238,14 @@ def _key2str(key: Union[bytes, str]) -> str: return ret.strip() +def _check_vals( + kw: str, spec: str, vals: Union[npt.NDArray[Any], resfo.MESS] +) -> npt.NDArray[Any]: + if vals is resfo.MESS or isinstance(vals, resfo.MESS): + raise ValueError(f"{kw.strip()} in {spec} has incorrect type MESS") + return vals + + def _read_spec( spec: str, fetch_keys: Sequence[str] ) -> Tuple[int, datetime, DateUnit, List[str], npt.NDArray[np.int32]]: @@ -245,7 +263,7 @@ def _read_spec( "NUMLX ", "NUMLY ", "NUMLZ ", - "LGRNAMES", + "LGRS ", "UNITS ", ] } @@ -274,22 +292,15 @@ def _read_spec( break kw = entry.read_keyword() if kw in arrays: - vals = entry.read_array() - if vals is resfo.MESS or isinstance(vals, resfo.MESS): - raise ValueError(f"{kw} in {spec} was MESS") - arrays[kw] = vals + arrays[kw] = _check_vals(kw, spec, entry.read_array()) if kw == "DIMENS ": - vals = entry.read_array() - if vals is resfo.MESS or isinstance(vals, resfo.MESS): - raise ValueError(f"DIMENS in {spec} was MESS") + vals = _check_vals(kw, spec, entry.read_array()) size = len(vals) n = vals[0] if size > 0 else None nx = vals[1] if size > 1 else None ny = vals[2] if size > 2 else None if kw == "STARTDAT": - vals = entry.read_array() - if vals is resfo.MESS or isinstance(vals, resfo.MESS): - raise ValueError(f"Startdate in {spec} was MESS") + vals = _check_vals(kw, spec, entry.read_array()) size = len(vals) day = vals[0] if size > 0 else 0 month = vals[1] if size > 1 else 0 @@ -297,27 +308,32 @@ def _read_spec( hour = vals[3] if size > 3 else 0 minute = vals[4] if size > 4 else 0 microsecond = vals[5] if size > 5 else 0 - date = datetime( - day=day, - month=month, - year=year, - hour=hour, - minute=minute, - second=microsecond // 10**6, - microsecond=microsecond % 10**6, - ) + try: + date = datetime( + day=day, + month=month, + year=year, + hour=hour, + minute=minute, + second=microsecond // 10**6, + microsecond=microsecond % 10**6, + ) + except Exception as err: + raise ValueError( + f"SMSPEC {spec} contains invalid STARTDAT: {err}" + ) from err keywords = arrays["KEYWORDS"] wgnames = arrays["WGNAMES "] nums = arrays["NUMS "] numlx = arrays["NUMLX "] numly = arrays["NUMLY "] numlz = arrays["NUMLZ "] - lgr_names = arrays["LGRNAMES"] + lgr_names = arrays["LGRS "] if date is None: - raise ValueError(f"keyword startdat missing in {spec}") + raise ValueError(f"Keyword startdat missing in {spec}") if keywords is None: - raise ValueError(f"keywords missing in {spec}") + raise ValueError(f"Keywords missing in {spec}") if n is None: n = len(keywords) @@ -367,16 +383,22 @@ def optional_get(arr: Optional[npt.NDArray[Any]], idx: int) -> Any: units = arrays["UNITS "] if units is None: - raise ValueError(f"keyword units missing in {spec}") + raise ValueError(f"Keyword units missing in {spec}") if date_index is None: raise ValueError(f"KEYWORDS did not contain TIME in {spec}") if date_index >= len(units): raise ValueError(f"Unit missing for TIME in {spec}") + unit_key = _key2str(units[date_index]) + try: + date_unit = DateUnit[unit_key] + except KeyError: + raise ValueError(f"Unknown date unit in {spec}: {unit_key}") from None + return ( date_index, date, - DateUnit[_key2str(units[date_index])], + date_unit, list(keys_array), indices_array, ) @@ -403,9 +425,7 @@ def _read_summary( def read_params() -> None: nonlocal last_params, values if last_params is not None: - vals = last_params.read_array() - if vals is resfo.MESS or isinstance(vals, resfo.MESS): - raise ValueError(f"PARAMS in {summary} was MESS") + vals = _check_vals("PARAMS", summary, last_params.read_array()) values.append(vals[indices]) dates.append(start_date + unit.make_delta(float(vals[date_index]))) last_params = None diff --git a/tests/unit_tests/config/config_dict_generator.py b/tests/unit_tests/config/config_dict_generator.py index c6026767dda..5a824a6a647 100644 --- a/tests/unit_tests/config/config_dict_generator.py +++ b/tests/unit_tests/config/config_dict_generator.py @@ -394,6 +394,7 @@ def ert_config_values(draw, use_eclbase=booleans): smspecs( sum_keys=st.just(sum_keys), start_date=st.just(Date.from_datetime(first_date)), + use_days=st.just(True), ) ) std_cutoff = draw(small_floats) diff --git a/tests/unit_tests/config/summary_generator.py b/tests/unit_tests/config/summary_generator.py index bf796fc35c4..6e06c309810 100644 --- a/tests/unit_tests/config/summary_generator.py +++ b/tests/unit_tests/config/summary_generator.py @@ -66,8 +66,14 @@ def root_memnonic(draw): second_character = draw(st.sampled_from("OWGVLPT")) third_character = draw(st.sampled_from("PIF")) fourth_character = draw(st.sampled_from("RT")) - # local = draw(st.sampled_from(["", "L"])) if first_character in "BCW" else "" - return first_character + second_character + third_character + fourth_character + local = draw(st.sampled_from(["", "L"])) if first_character in "BCW" else "" + return ( + local + + first_character + + second_character + + third_character + + fourth_character + ) @st.composite @@ -228,7 +234,6 @@ class Smspec: region_numbers: List[int] units: List[str] start_date: Date - lgr_names: Optional[List[str]] = None lgrs: Optional[List[str]] = None numlx: Optional[List[PositiveInt]] = None numly: Optional[List[PositiveInt]] = None @@ -266,7 +271,6 @@ def to_ecl(self) -> List[Tuple[str, Any]]: ("STARTDAT", np.array(self.start_date.to_ecl(), dtype=np.int32)), ] + ([("LGRS ", self.lgrs)] if self.lgrs is not None else []) - + ([("LGRNAMES", self.lgr_names)] if self.lgr_names is not None else []) + ([("NUMLX ", self.numlx)] if self.numlx is not None else []) + ([("NUMLY ", self.numly)] if self.numly is not None else []) + ([("NUMLZ ", self.numlz)] if self.numlz is not None else []) @@ -281,36 +285,40 @@ def to_file(self, filelike, file_format: resfo.Format = resfo.Format.UNFORMATTED @st.composite -def smspecs( - draw, - sum_keys, - start_date, -): +def smspecs(draw, sum_keys, start_date, use_days=None): """ Strategy for smspec that ensures that the TIME parameter, as required by ert, is in the parameters list. """ + use_days = st.booleans() if use_days is None else use_days use_locals = draw(st.booleans()) sum_keys = draw(sum_keys) + if any(sk.startswith("L") for sk in sum_keys): + use_locals = True n = len(sum_keys) + 1 nx = draw(small_ints) ny = draw(small_ints) nz = draw(small_ints) keywords = ["TIME "] + sum_keys - units = ["DAYS "] + draw(st.lists(unit_names, min_size=n - 1, max_size=n - 1)) + if draw(use_days): + units = ["DAYS "] + draw( + st.lists(unit_names, min_size=n - 1, max_size=n - 1) + ) + else: + units = ["HOURS "] + draw( + st.lists(unit_names, min_size=n - 1, max_size=n - 1) + ) well_names = [":+:+:+:+"] + draw(st.lists(names, min_size=n - 1, max_size=n - 1)) if use_locals: # use local lgrs = draw(st.lists(names, min_size=n, max_size=n)) numlx = draw(st.lists(small_ints, min_size=n, max_size=n)) numly = draw(st.lists(small_ints, min_size=n, max_size=n)) numlz = draw(st.lists(small_ints, min_size=n, max_size=n)) - lgr_names = list(set(lgrs)) else: lgrs = None numlx = None numly = None numlz = None - lgr_names = None region_numbers = [-32676] + draw( st.lists( from_dtype(np.dtype(np.int32), min_value=1, max_value=nx * ny * nz), @@ -336,7 +344,6 @@ def smspecs( numlx=st.just(numlx), numly=st.just(numly), numlz=st.just(numlz), - lgr_names=st.just(lgr_names), region_numbers=st.just(region_numbers), units=st.just(units), start_date=start_date, diff --git a/tests/unit_tests/config/test_read_summary.py b/tests/unit_tests/config/test_read_summary.py index 7b99c03d950..d582cf82376 100644 --- a/tests/unit_tests/config/test_read_summary.py +++ b/tests/unit_tests/config/test_read_summary.py @@ -3,9 +3,9 @@ import hypothesis.strategies as st import pytest +import resfo from hypothesis import given from resdata.summary import Summary, SummaryVarType -from resfo import Format from ert.config._read_summary import _SummaryType, make_summary_key, read_summary @@ -192,12 +192,62 @@ def test_completion_summary_format_have_cell_index_and_name( ) -@given(summaries(), st.sampled_from(Format)) +@pytest.mark.parametrize("keyword", ["LBWPR"]) +@pytest.mark.parametrize( + "li,lj,lk,lgr_name,indices", + [ + (1, 1, 1, "LGRNAME", "1,1,1"), + (2, 1, 1, "LGRNAME", "2,1,1"), + (1, 2, 1, "LGRNAME", "1,2,1"), + (3, 1, 1, "LGRNAME", "3,1,1"), + (3, 1, 2, "LGRNAME", "3,1,2"), + ], +) +def test_local_block_summary_format_have_cell_index_and_name( + keyword, lgr_name, indices, li, lj, lk +): + assert ( + make_summary_key(keyword, li=li, lj=lj, lk=lk, lgr_name=lgr_name) + == f"{keyword}:{lgr_name}:{indices}" + ) + + +@given(name=st.text(), lgr_name=st.text()) +@pytest.mark.parametrize("keyword", ["LCOPR"]) +@pytest.mark.parametrize( + "li,lj,lk,indices", + [ + (1, 1, 1, "1,1,1"), + (2, 1, 1, "2,1,1"), + (1, 2, 1, "1,2,1"), + (3, 1, 1, "3,1,1"), + (3, 1, 2, "3,1,2"), + ], +) +def test_local_completion_summary_format_have_cell_index_and_name( + keyword, name, lgr_name, indices, li, lj, lk +): + assert ( + make_summary_key(keyword, name=name, li=li, lj=lj, lk=lk, lgr_name=lgr_name) + == f"{keyword}:{lgr_name}:{name}:{indices}" + ) + + +@given(name=st.text(), lgr_name=st.text()) +@pytest.mark.parametrize("keyword", ["LWWPR"]) +def test_local_well_summary_format_have_cell_index_and_name(keyword, name, lgr_name): + assert ( + make_summary_key(keyword, name=name, lgr_name=lgr_name) + == f"{keyword}:{lgr_name}:{name}" + ) + + +@given(summaries(), st.sampled_from(resfo.Format)) def test_that_reading_summaries_returns_the_contents_of_the_file( tmp_path_factory, summary, format ): tmp_path = tmp_path_factory.mktemp("summary") - format_specifier = "F" if format == Format.FORMATTED else "" + format_specifier = "F" if format == resfo.Format.FORMATTED else "" smspec, unsmry = summary unsmry.to_file(tmp_path / f"TEST.{format_specifier}UNSMRY", format) smspec.to_file(tmp_path / f"TEST.{format_specifier}SMSPEC", format) @@ -252,39 +302,125 @@ def to_date(start_date: datetime, offset: float, unit: str) -> datetime: @pytest.mark.parametrize( - "spec_contents, smry_contents", + "spec_contents, smry_contents, error_message", [ - (b"", b""), - (b"1", b"1"), - (b"\x00\x00\x00\x10", b"1"), - (b"\x00\x00\x00\x10UNEXPECTED", b"\x00\x00\x00\x10UNEXPECTED"), + (b"", b"", "Keyword startdat missing"), + (b"1", b"1", "Failed to read summary file"), ( - b"\x00\x00\x00\x10UNEXPECTED", - b"\x00\x00\x00\x10KEYWORD1" + (2200).to_bytes(4, byteorder="big"), - ), - ( - b"\x00\x00\x00\x10FOOOOOOO\x00", - b"\x00\x00\x00\x10FOOOOOOO" - + (2300).to_bytes(4, byteorder="big") - + b"INTE\x00\x00\x00\x10" - + b"\x00" * (4 * 2300 + 4 * 6), + b"\x00\x00\x00\x10FOOOOOOO\x00\x00\x00\x01" + + b"INTE" + + b"\x00\x00\x00\x10" + + (4).to_bytes(4, signed=True, byteorder="big") + + b"\x00" * 4, + b"", + "Keyword startdat missing", ), ( - b"\x00\x00\x00\x10FOOOOOOO\x00\x00\x00\x01" + b"\x00\x00\x00\x10STARTDAT\x00\x00\x00\x01" + b"INTE" + b"\x00\x00\x00\x10" + (4).to_bytes(4, signed=True, byteorder="big") + b"\x00" * 4 + (4).to_bytes(4, signed=True, byteorder="big"), - b"\x00\x00\x00\x10FOOOOOOO\x00", + b"", + "contains invalid STARTDAT", ), ], ) def test_that_incorrect_summary_files_raises_informative_errors( - smry_contents, spec_contents, tmp_path + smry_contents, spec_contents, error_message, tmp_path ): (tmp_path / "test.UNSMRY").write_bytes(smry_contents) (tmp_path / "test.SMSPEC").write_bytes(spec_contents) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=error_message): + read_summary(str(tmp_path / "test"), ["*"]) + + +def test_mess_values_in_summary_files_raises_informative_errors(tmp_path): + resfo.write(tmp_path / "test.SMSPEC", [("KEYWORDS", resfo.MESS)]) + (tmp_path / "test.UNSMRY").write_bytes(b"") + + with pytest.raises(ValueError, match="has incorrect type MESS"): + read_summary(str(tmp_path / "test"), ["*"]) + + +def test_empty_keywords_in_summary_files_raises_informative_errors(tmp_path): + resfo.write( + tmp_path / "test.SMSPEC", + [("STARTDAT", [31, 12, 2012, 00]), ("KEYWORDS", [" "])], + ) + (tmp_path / "test.UNSMRY").write_bytes(b"") + + with pytest.raises(ValueError, match="Got empty summary keyword"): + read_summary(str(tmp_path / "test"), ["*"]) + + +def test_missing_names_keywords_in_summary_files_raises_informative_errors( + tmp_path, +): + resfo.write( + tmp_path / "test.SMSPEC", + [("STARTDAT", [31, 12, 2012, 00]), ("KEYWORDS", ["BART "])], + ) + (tmp_path / "test.UNSMRY").write_bytes(b"") + + with pytest.raises( + ValueError, + match="Found block keyword in summary specification without dimens keyword", + ): + read_summary(str(tmp_path / "test"), ["*"]) + + +def test_unknown_date_unit_in_summary_files_raises_informative_errors( + tmp_path, +): + resfo.write( + tmp_path / "test.SMSPEC", + [ + ("STARTDAT", [31, 12, 2012, 00]), + ("KEYWORDS", ["TIME "]), + ("UNITS ", ["ANNUAL "]), + ], + ) + (tmp_path / "test.UNSMRY").write_bytes(b"") + + with pytest.raises( + ValueError, + match="Unknown date unit .* ANNUAL", + ): + read_summary(str(tmp_path / "test"), ["*"]) + + +def test_missing_keywords_in_smspec_raises_informative_error( + tmp_path, +): + resfo.write( + tmp_path / "test.SMSPEC", + [ + ("STARTDAT", [31, 12, 2012, 00]), + ("UNITS ", ["ANNUAL "]), + ], + ) + (tmp_path / "test.UNSMRY").write_bytes(b"") + + with pytest.raises( + ValueError, + match="Keywords missing", + ): + read_summary(str(tmp_path / "test"), ["*"]) + + +def test_that_ambiguous_case_restart_raises_an_informative_error( + tmp_path, +): + (tmp_path / "test.UNSMRY").write_bytes(b"") + (tmp_path / "test.FUNSMRY").write_bytes(b"") + (tmp_path / "test.smspec").write_bytes(b"") + (tmp_path / "test.Smspec").write_bytes(b"") + + with pytest.raises( + ValueError, + match="Ambiguous reference to unified summary", + ): read_summary(str(tmp_path / "test"), ["*"]) From 99a08eba8b3fbbeee403c1268d74e3ccd245183a Mon Sep 17 00:00:00 2001 From: Eivind Jahren Date: Mon, 8 Jan 2024 15:33:38 +0100 Subject: [PATCH 3/3] Added tests for missing keywords --- tests/unit_tests/config/test_read_summary.py | 59 ++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/tests/unit_tests/config/test_read_summary.py b/tests/unit_tests/config/test_read_summary.py index d582cf82376..8bde07e2580 100644 --- a/tests/unit_tests/config/test_read_summary.py +++ b/tests/unit_tests/config/test_read_summary.py @@ -392,6 +392,65 @@ def test_unknown_date_unit_in_summary_files_raises_informative_errors( read_summary(str(tmp_path / "test"), ["*"]) +def test_missing_units_in_summary_files_raises_an_informative_error( + tmp_path, +): + resfo.write( + tmp_path / "test.SMSPEC", + [ + ("STARTDAT", [31, 12, 2012, 00]), + ("KEYWORDS", ["TIME "]), + ], + ) + (tmp_path / "test.UNSMRY").write_bytes(b"") + + with pytest.raises( + ValueError, + match="Keyword units", + ): + read_summary(str(tmp_path / "test"), ["*"]) + + +def test_missing_date_units_in_summary_files_raises_an_informative_error( + tmp_path, +): + resfo.write( + tmp_path / "test.SMSPEC", + [ + ("STARTDAT", [31, 12, 2012, 00]), + ("KEYWORDS", ["FOPR ", "TIME "]), + ("UNITS ", ["SM3 "]), + ], + ) + (tmp_path / "test.UNSMRY").write_bytes(b"") + + with pytest.raises( + ValueError, + match="Unit missing for TIME", + ): + read_summary(str(tmp_path / "test"), ["*"]) + + +def test_missing_time_keyword_in_summary_files_raises_an_informative_error( + tmp_path, +): + resfo.write( + tmp_path / "test.SMSPEC", + [ + ("STARTDAT", [31, 12, 2012, 00]), + ("KEYWORDS", ["FOPR "]), + ("UNITS ", ["SM3 "]), + ], + ) + (tmp_path / "test.UNSMRY").write_bytes(b"") + + with pytest.raises( + ValueError, + match="KEYWORDS did not contain TIME", + ): + read_summary(str(tmp_path / "test"), ["*"]) + + def test_missing_keywords_in_smspec_raises_informative_error( tmp_path, ):