From 1653e62279ed069cefe88934e820d977668cfb08 Mon Sep 17 00:00:00 2001 From: Alif Be <11570927+alifbe@users.noreply.github.com> Date: Fri, 16 Aug 2024 09:56:16 +0000 Subject: [PATCH] Use sunsch updated time_vector.py --- src/subscript/sunsch/sunsch.py | 2 +- src/subscript/sunsch/time_vector.py | 425 ++++++++++++++++++++++++++++ 2 files changed, 426 insertions(+), 1 deletion(-) create mode 100644 src/subscript/sunsch/time_vector.py diff --git a/src/subscript/sunsch/sunsch.py b/src/subscript/sunsch/sunsch.py index b6c60f8d6..20f3b1cdd 100755 --- a/src/subscript/sunsch/sunsch.py +++ b/src/subscript/sunsch/sunsch.py @@ -16,10 +16,10 @@ import dateutil.parser import yaml -from opm.tools.time_vector import TimeVector from pydantic import BaseModel, FilePath from subscript import __version__, getLogger +from subscript.sunsch.time_vector import TimeVector # type: ignore logger = getLogger(__name__) diff --git a/src/subscript/sunsch/time_vector.py b/src/subscript/sunsch/time_vector.py new file mode 100644 index 000000000..163c6108b --- /dev/null +++ b/src/subscript/sunsch/time_vector.py @@ -0,0 +1,425 @@ +# To be removed once the OPM version of this file is updated + +import datetime +from operator import attrgetter + +try: + from StringIO import StringIO +except ImportError: + from io import StringIO + +import opm.io +from opm.io.parser import ParseContext, Parser + +error_actions = [("PARSE_INVALID_KEYWORD_COMBINATION", opm.io.action.ignore)] + +# This is from the TimeMap.cpp implementation in opm +ecl_month = { + "JAN": 1, + "FEB": 2, + "MAR": 3, + "APR": 4, + "MAI": 5, + "MAY": 5, + "JUN": 6, + "JLY": 7, + "JUL": 7, + "AUG": 8, + "SEP": 9, + "OCT": 10, + "OKT": 10, + "NOV": 11, + "DEC": 12, + "DES": 12, +} + +inv_ecl_month = { + 1: "JAN", + 2: "FEB", + 3: "MAR", + 4: "APR", + 5: "MAY", + 6: "JUN", + 7: "JUL", + 8: "AUG", + 9: "SEP", + 10: "OCT", + 11: "NOV", + 12: "DEC", +} + + +def _make_datetime(dates_record): + day = dates_record[0].get_int(0) + month = dates_record[1].get_str(0) + year = dates_record[2].get_int(0) + + date_dt = datetime.datetime(year, ecl_month[month], day) + if len(dates_record) < 4: + return date_dt + time_str = dates_record[3].get_str(0) + time_list = time_str.split(":") + hour = minute = second = microsecond = 0 + hour = int(time_list[0]) + if len(time_list) > 1: + minute = int(time_list[1]) + if len(time_list) > 2: + sec_list = time_list[2].split(".") + second = int(sec_list[0]) + if len(sec_list) > 1: + ms_str = sec_list[1].strip() + npad = 6 - len(ms_str) + ms_str += "".join(["0" for i in range(npad)]) + microsecond = int(ms_str) + + return datetime.datetime( + year, ecl_month[month], day, hour, minute, second, microsecond + ) + + +class TimeStep(object): + def __init__(self, dt, keywords): + """The TimeStep class consist of a list of keywords and a corresponding date. + + Observe that the date value corresponds to a DATES / TSTEP keyword + following *after* the keywords; i.e. if the TimeStep instance contains + a WCONHIST keyword the settings in that keyword should apply *until* + the date specified is reached. See the documentation of the TimeVector + class for more details of the relationship between TimeVector and + TimeStep. + + """ + self.dt = dt + self.keywords = keywords + self.tstep = None + self.is_start = False + + @classmethod + def create_first(cls, dt): + ts = cls(dt, []) + ts.is_start = True + return ts + + def add_keyword(self, kw): + self.keywords.append(kw) + + def __len__(self): + return len(self.keywords) + + def __contains__(self, arg): + return any(arg == kw.name for kw in self.keywords) + + def __str__(self): + string = StringIO() + + if not self.is_start: + day = self.dt.day + month = self.dt.month + year = self.dt.year + if not self.dt.time(): + string.write( + "DATES\n {day} '{month}' {year}/\n/\n\n".format( + day=day, month=inv_ecl_month[month], year=year + ) + ) + else: + hour = self.dt.hour + minute = self.dt.minute + second = self.dt.second + self.dt.microsecond * 1.0e-6 + if self.dt.microsecond > 0: + string.write( + "DATES\n {day} '{month}' {year}" + " {hour:02d}:{minute:02d}:{second:07.4f} /\n/\n\n".format( + day=day, + month=inv_ecl_month[month], + year=year, + hour=hour, + minute=minute, + second=second, + ) + ) + else: + string.write( + "DATES\n {day} '{month}' {year}" + " {hour:02d}:{minute:02d}:{second:02d} /\n/\n\n".format( + day=day, + month=inv_ecl_month[month], + year=year, + hour=hour, + minute=minute, + second=int(second), + ) + ) + + for kw in self.keywords: + string.write(str(kw)) + string.write("\n") + + return string.getvalue() + + +class TimeVector(object): + def __init__(self, start_date, base_string=None, base_file=None): + """The TimeVector class is a simple vector class with DATES/TSTEP blocks. + + The TimeVector class is a basic building block for tools designed to + update schedule files. A schedule file consists of a list of keywords + related to the dynamic properties of the field, like opening and + closing wells, specifiying rates and so on. The temporal advancement of + the simulator is controlled by DATES and TSTEP keywords. A typical + schedule section can look like this: + + --- Step 1 ----------------------- + + WELSPECS + 'C1' 'G1' 10 10 10 'OIL' / + / + + COMPDAT + 'C1' 15 20 10 16 'OPEN' / + 'C1' 15 21 16 16 'OPEN' / + / + + WCONHIST + 'C1' 'OPEN' 'ORAT' 1000 / + / + + --- Step 2 ---------------------- + + DATES + 10 'MAY' 2016 / + / + + WCONHIST + 'C1' 'OPEN' 'ORAT' 2000 / + / + + --- Step 3 ---------------------- + + TSTEP + 10 / + + WELSPECS + 'W2' 'G1' 5 5 5 'OIL' / + / + + COMPDAT + 'W2' 10 10 7 10 'OPEN' / + / + + WCONHIST + 'C1' 'OPEN' 'ORAT' 3000 / + 'W2' 'OPEN' 'ORAT' 1500 / + / + + --- Step 4 ---------------------- + + DATES + 30 'MAY' 2016 / + / + + As indicated above the DATES and TSTEP keywords act as delimiters in + the schedule file. In the TimeVector class the fundamental unit is + TimeStep instance which consists of a list of keywords, and a + terminating DATES or TSTEP keyword, the example above would correspond + to a TimeVector with three TimeStep instances. + + Basic usage example: + + #!/usr/bin/env python + from opm.tools import TimeVector + + # Create vector and load history. + tv = TimeVector( start ) + tv.load("history.sch") + + + # Load predictions from another file + tv.load("prediction.sch") + + + # Insert the definition of one particular well at + # a specifed date. + tv.load("extra_wll.sch", date = datetime.datetime(2018,10,1)) + + + # Check if we have a certain timestep: + if datetime.datetime(2017,1,1) in tv: + print("We have it!") + else: + print("No such date") + + + # Dump the updated schedule content to a file: + with open("schedule","w") as f: + f.write(str(tv)) + + + """ + if base_string and base_file: + raise ValueError( + "Can only supply one of base_string and base_file arguments" + ) + + self.start_date = datetime.datetime( + start_date.year, start_date.month, start_date.day + ) + self.time_steps_dict = {} + self.time_steps_list = [] + + ts = TimeStep.create_first(self.start_date) + + self._add_dates_block(ts) + start_dt = datetime.datetime(start_date.year, start_date.month, start_date.day) + parse_context = ParseContext(error_actions) + if base_file: + deck = Parser().parse(base_file, parse_context) + self._add_deck(deck, start_dt) + + if base_string: + deck = Parser().parse_string(base_string, parse_context) + self._add_deck(deck, start_dt) + + def __len__(self): + """ + The number of timesteps in the vector. + """ + return len(self.time_steps_dict) + + def __contains__(self, dt): + """ + Will return true if the vector contains a timestep at date dt. + """ + if isinstance(dt, datetime.date): + dt = datetime.datetime(dt.year, dt.month, dt.day) + return dt in self.time_steps_dict + + def __getitem__(self, index): + """Will look up a timestep in the vector. + + The index argument can either be an integer or a datetime.date/datetime + instance. + + """ + if isinstance(index, int): + return self.time_steps_list[index] + if not isinstance(index, datetime.datetime) and isinstance( + index, datetime.date + ): + index = datetime.datetime(index.year, index.month, index.day) + + return self.time_steps_dict[index] + + def _add_dates_block(self, ts): + self.time_steps_dict[ts.dt] = ts + self.time_steps_list.append(ts) + + def delete(self, dt): + del self.time_steps_dict[dt] + for index, ts in enumerate(self.time_steps_list): + if ts.dt == dt: + del self.time_steps_list[index] + break + + def add_keywords(self, dt, keywords): + if dt < self.start_date: + raise ValueError("Invalid datetime argument: {}".format(dt)) + + if dt in self.time_steps_dict: + ts = self[dt] + for kw in keywords: + ts.add_keyword(kw) + else: + ts = TimeStep(dt, keywords) + self._add_dates_block(ts) + self.time_steps_list.sort(key=attrgetter("dt")) + + def _add_deck(self, deck, start_date): + first_kw = deck[0] + if start_date is None: + if first_kw.name != "DATES": + raise ValueError( + "When loading you must *either* specify date - or file must start" + " with DATES keyword" + ) + dt = _make_datetime(first_kw[len(first_kw) - 1]) + else: + if first_kw.name == "DATES": + raise ValueError( + "When loading you must *either* specify date - or file must start" + " with DATES keyword" + ) + dt = start_date + + keywords = [] + for kw in deck: + if kw.name == "DATES": + self.add_keywords(dt, keywords) + + for index in range(len(kw) - 1): + dt = _make_datetime(kw[index]) + self.add_keywords(dt, []) + + dt = _make_datetime(kw[len(kw) - 1]) + + keywords = [] + continue + + # if kw.name == "TSTEP": + # raise ValueError("Must block the ranges with active TSTEP - + # getting a DATES in there is ERROR") + + keywords.append(kw) + + self.add_keywords(dt, keywords) + + def load(self, filename, date=None): + """Will parse a Schedule file and add the keywords to the current TimeVector. + + You can call the load() method repeatedly, the different timesteps will + be ordered chronologically. If a timestep is already present the + keywords will be appended. + + The optional date argument can be used to insert schedule file + fragments which do not have any DATES / TSTEP keywords. Assuming you + have a base file 'base.sch' and a small fragment 'well.sch' with the + WELSPECS and COMPDAT keywords to create one well, then the new well can + be added 1.st of April 2017 as this: + + tv = TimeVector( start ) + tv.load("base.sch") + tv.load("well.sch", date = datetime.datetime(2017, 4, 1)) + + """ + parse_context = ParseContext(error_actions) + deck = Parser().parse(filename, parse_context) + self._add_deck(deck, date) + + def load_string(self, deck_string, date=None): + """ + Like load() - but load from a string literal instead of file. + """ + parse_context = ParseContext(error_actions) + deck = Parser().parse_string(deck_string, parse_context) + self._add_deck(deck, date) + + def __str__(self): + """Will return a string representation of the vector. + + The output from this method should be valid Schedule input which can be + passed to a simulator. + + """ + + string = StringIO() + for ts in self: + string.write(str(ts)) + + return string.getvalue() + + @property + def dates(self): + """ + Will return a list of all the dates in the vector. + """ + return [x.dt for x in self.time_steps_list]