completor 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- completor/__init__.py +3 -0
- completor/completion.py +1042 -0
- completor/config_jobs/run_completor +18 -0
- completor/constants.py +256 -0
- completor/create_output.py +462 -0
- completor/create_wells.py +314 -0
- completor/exceptions/__init__.py +4 -0
- completor/exceptions/clean_exceptions.py +10 -0
- completor/exceptions/exceptions.py +134 -0
- completor/hook_implementations/jobs.py +63 -0
- completor/input_validation.py +340 -0
- completor/launch_args_parser.py +41 -0
- completor/logger.py +138 -0
- completor/main.py +486 -0
- completor/parse.py +581 -0
- completor/prepare_outputs.py +1473 -0
- completor/pvt_model.py +14 -0
- completor/read_casefile.py +677 -0
- completor/read_schedule.py +160 -0
- completor/utils.py +185 -0
- completor/visualization.py +164 -0
- completor/visualize_well.py +217 -0
- completor-0.1.2.dist-info/LICENSE +165 -0
- completor-0.1.2.dist-info/METADATA +205 -0
- completor-0.1.2.dist-info/RECORD +27 -0
- completor-0.1.2.dist-info/WHEEL +4 -0
- completor-0.1.2.dist-info/entry_points.txt +6 -0
completor/main.py
ADDED
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
"""Main module of Completor."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import time
|
|
9
|
+
from collections.abc import Mapping
|
|
10
|
+
from typing import overload
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
import completor
|
|
15
|
+
from completor import parse
|
|
16
|
+
from completor.completion import WellSchedule
|
|
17
|
+
from completor.constants import Keywords
|
|
18
|
+
from completor.create_output import CreateOutput
|
|
19
|
+
from completor.create_wells import CreateWells
|
|
20
|
+
from completor.exceptions import CompletorError
|
|
21
|
+
from completor.launch_args_parser import get_parser
|
|
22
|
+
from completor.logger import handle_error_messages, logger
|
|
23
|
+
from completor.read_casefile import ReadCasefile
|
|
24
|
+
from completor.utils import abort, clean_file_line, clean_file_lines
|
|
25
|
+
from completor.visualization import close_figure, create_pdfpages
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
from typing import Literal
|
|
29
|
+
except ImportError:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class FileWriter:
|
|
34
|
+
"""Functionality for writing a new schedule file."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, file: str, mapper: Mapping[str, str] | None):
|
|
37
|
+
"""Initialize the FileWriter.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
file: Name of file to be written. Does not check if it already exists.
|
|
41
|
+
mapper: A dictionary for mapping strings.
|
|
42
|
+
Typically used for mapping pre-processor reservoir modelling tools to reservoir simulator well names.
|
|
43
|
+
"""
|
|
44
|
+
self.fh = open(file, "w", encoding="utf-8") # create new output file
|
|
45
|
+
self.mapper = mapper
|
|
46
|
+
|
|
47
|
+
@overload
|
|
48
|
+
def write(self, keyword: Literal[None], content: str, chunk: bool = True, end_of_record: bool = False) -> None: ...
|
|
49
|
+
|
|
50
|
+
@overload
|
|
51
|
+
def write(
|
|
52
|
+
self, keyword: str, content: list[list[str]], chunk: Literal[True] = True, end_of_record: bool = False
|
|
53
|
+
) -> None: ...
|
|
54
|
+
|
|
55
|
+
@overload
|
|
56
|
+
def write(
|
|
57
|
+
self, keyword: str, content: list[str] | str, chunk: Literal[False] = False, end_of_record: bool = False
|
|
58
|
+
) -> None: ...
|
|
59
|
+
|
|
60
|
+
@overload
|
|
61
|
+
def write(
|
|
62
|
+
self, keyword: str, content: list[list[str]] | list[str] | str, chunk: bool = True, end_of_record: bool = False
|
|
63
|
+
) -> None: ...
|
|
64
|
+
|
|
65
|
+
def write(
|
|
66
|
+
self,
|
|
67
|
+
keyword: str | None,
|
|
68
|
+
content: list[list[str]] | list[str] | str,
|
|
69
|
+
chunk: bool = True,
|
|
70
|
+
end_of_record: bool = False,
|
|
71
|
+
) -> None:
|
|
72
|
+
"""Write the content of a keyword to the output file.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
keyword: Reservoir simulator keyword.
|
|
76
|
+
content: Text to be written.
|
|
77
|
+
chunk: Flag for indicating this is a list of records.
|
|
78
|
+
end_of_record: Flag for adding end-of-record ('/').
|
|
79
|
+
"""
|
|
80
|
+
txt = ""
|
|
81
|
+
|
|
82
|
+
if keyword is None:
|
|
83
|
+
txt = content # type: ignore # it's really a formatted string
|
|
84
|
+
else:
|
|
85
|
+
self.fh.write(f"{keyword:s}\n")
|
|
86
|
+
if chunk:
|
|
87
|
+
for recs in content:
|
|
88
|
+
txt += " " + " ".join(recs) + " /\n"
|
|
89
|
+
else:
|
|
90
|
+
for line in content:
|
|
91
|
+
if isinstance(line, list):
|
|
92
|
+
logger.warning(
|
|
93
|
+
"Chunk is False, but content contains lists of lists, "
|
|
94
|
+
"instead of a list of strings the lines will be concatenated."
|
|
95
|
+
)
|
|
96
|
+
line = " ".join(line)
|
|
97
|
+
txt += line + "\n"
|
|
98
|
+
if self.mapper:
|
|
99
|
+
txt = self._replace_preprocessing_names(txt)
|
|
100
|
+
if end_of_record:
|
|
101
|
+
txt += "/\n"
|
|
102
|
+
self.fh.write(txt)
|
|
103
|
+
|
|
104
|
+
def _replace_preprocessing_names(self, text: str) -> str:
|
|
105
|
+
"""Expand start and end marker pairs for well pattern recognition as needed.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
text: Text with pre-processor reservoir modelling well names.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Text with reservoir simulator well names.
|
|
112
|
+
"""
|
|
113
|
+
if self.mapper is None:
|
|
114
|
+
raise ValueError(
|
|
115
|
+
f"{self._replace_preprocessing_names.__name__} requires a file containing two "
|
|
116
|
+
"columns with input and output names given by the MAPFILE keyword in "
|
|
117
|
+
f"case file to be set when creating {self.__class__.__name__}."
|
|
118
|
+
)
|
|
119
|
+
start_marks = ["'", " ", "\n", "\t"]
|
|
120
|
+
end_marks = ["'", " ", " ", " "]
|
|
121
|
+
for key, value in self.mapper.items():
|
|
122
|
+
for start, end in zip(start_marks, end_marks):
|
|
123
|
+
my_key = start + str(key) + start
|
|
124
|
+
if my_key in text:
|
|
125
|
+
my_value = start + str(value) + end
|
|
126
|
+
text = text.replace(my_key, my_value)
|
|
127
|
+
return text
|
|
128
|
+
|
|
129
|
+
def close(self) -> None:
|
|
130
|
+
"""Close FileWriter."""
|
|
131
|
+
self.fh.close()
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class ProgressStatus:
|
|
135
|
+
"""Bookmark the reading progress of a schedule file.
|
|
136
|
+
|
|
137
|
+
See https://stackoverflow.com/questions/3173320/text-progress-bar-in-the-console
|
|
138
|
+
for improved functionality.
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
def __init__(self, num_lines: int, percent: float):
|
|
142
|
+
"""Initialize ProgressStatus.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
num_lines: Number of lines in schedule file.
|
|
146
|
+
percent: Indicates schedule file processing progress (in percent).
|
|
147
|
+
"""
|
|
148
|
+
self.percent = percent
|
|
149
|
+
self.nlines = num_lines
|
|
150
|
+
self.prev_n = 0
|
|
151
|
+
|
|
152
|
+
def update(self, line_number: int) -> None:
|
|
153
|
+
"""Update logger information.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
line_number: Input schedule file line number.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
Logger info message.
|
|
160
|
+
"""
|
|
161
|
+
# If the divisor, or numerator is a float, the integer division gives a float
|
|
162
|
+
n = int((line_number / self.nlines * 100) // self.percent)
|
|
163
|
+
if n > self.prev_n:
|
|
164
|
+
logger.info("=" * 80)
|
|
165
|
+
logger.info("Done processing %i %% of schedule/data file", n * self.percent)
|
|
166
|
+
logger.info("=" * 80)
|
|
167
|
+
self.prev_n = n
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def get_content_and_path(case_content: str, file_path: str | None, keyword: str) -> tuple[str | None, str | None]:
|
|
171
|
+
"""Get the contents of a file from a path defined by user or case file.
|
|
172
|
+
|
|
173
|
+
The method prioritizes paths given as input argument over the paths found in the case file.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
case_content: The case file content.
|
|
177
|
+
file_path: Path to file if given.
|
|
178
|
+
keyword: Reservoir simulator keyword.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
File content, file path.
|
|
182
|
+
|
|
183
|
+
Raises:
|
|
184
|
+
CompletorError: If the keyword cannot be found.
|
|
185
|
+
CompletorError: If the file cannot be found.
|
|
186
|
+
"""
|
|
187
|
+
if file_path is None:
|
|
188
|
+
# Find the path/name of file from case file
|
|
189
|
+
case_file_lines = clean_file_lines(case_content.splitlines())
|
|
190
|
+
start_idx, end_idx = parse.locate_keyword(case_file_lines, keyword)
|
|
191
|
+
# If the keyword is defined correctly
|
|
192
|
+
if end_idx == start_idx + 2:
|
|
193
|
+
# preprocess the text, remove leading/trailing whitespace and quotes
|
|
194
|
+
file_path = " ".join(case_file_lines[start_idx + 1].strip("'").strip(" ").split())
|
|
195
|
+
file_path = re.sub("[\"']+", "", file_path)
|
|
196
|
+
|
|
197
|
+
else:
|
|
198
|
+
# OUTFILE is optional, if it's needed but not supplied the error is caught in ReadCasefile:check_pvt_file()
|
|
199
|
+
if keyword == "OUTFILE":
|
|
200
|
+
return None, None
|
|
201
|
+
raise CompletorError(f"The keyword {keyword} is not defined correctly in the casefile")
|
|
202
|
+
if keyword != "OUTFILE":
|
|
203
|
+
try:
|
|
204
|
+
with open(file_path, encoding="utf-8") as file:
|
|
205
|
+
file_content = file.read()
|
|
206
|
+
except FileNotFoundError as e:
|
|
207
|
+
raise CompletorError(f"Could not find the file: '{file_path}'!") from e
|
|
208
|
+
except (PermissionError, IsADirectoryError) as e:
|
|
209
|
+
raise CompletorError("Could not read SCHFILE, this is likely because the path is missing quotes.") from e
|
|
210
|
+
return file_content, file_path
|
|
211
|
+
return None, file_path
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# noinspection TimingAttack
|
|
215
|
+
# caused by `if token == '...'` and token is interpreted as a security token / JWT
|
|
216
|
+
# or otherwise sensitive, but in this context, `token` refers to a token of parsed
|
|
217
|
+
# text / semantic token
|
|
218
|
+
def create(
|
|
219
|
+
input_file: str,
|
|
220
|
+
schedule_file: str,
|
|
221
|
+
new_file: str,
|
|
222
|
+
show_fig: bool = False,
|
|
223
|
+
percent: float = 5.0,
|
|
224
|
+
paths: tuple[str, str] | None = None,
|
|
225
|
+
) -> (
|
|
226
|
+
tuple[list[tuple[str, list[list[str]]]], ReadCasefile, WellSchedule, CreateWells, CreateOutput]
|
|
227
|
+
| tuple[list[tuple[str, list[list[str]]]], ReadCasefile, WellSchedule, CreateWells]
|
|
228
|
+
):
|
|
229
|
+
"""Create a new Completor schedule file from input case- and schedule files.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
input_file: Input case file.
|
|
233
|
+
schedule_file: Input schedule file.
|
|
234
|
+
new_file: Output schedule file.
|
|
235
|
+
show_fig: Flag indicating if a figure is to be shown.
|
|
236
|
+
percent: ProgressStatus percentage steps to be shown (in percent, %).
|
|
237
|
+
paths: Optional additional paths.
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Completor schedule file.
|
|
241
|
+
"""
|
|
242
|
+
case = ReadCasefile(case_file=input_file, schedule_file=schedule_file, output_file=new_file)
|
|
243
|
+
wells = CreateWells(case)
|
|
244
|
+
schedule = WellSchedule(wells.active_wells) # container for MSW-data
|
|
245
|
+
|
|
246
|
+
lines = schedule_file.splitlines()
|
|
247
|
+
|
|
248
|
+
clean_lines_map = {}
|
|
249
|
+
for line_number, line in enumerate(lines):
|
|
250
|
+
line = clean_file_line(line, remove_quotation_marks=True)
|
|
251
|
+
if line:
|
|
252
|
+
clean_lines_map[line_number] = line
|
|
253
|
+
|
|
254
|
+
outfile = FileWriter(new_file, case.mapper)
|
|
255
|
+
chunks = [] # for debug..
|
|
256
|
+
figno = 0
|
|
257
|
+
written = set() # Keep track of which MSW's has been written
|
|
258
|
+
line_number = 0
|
|
259
|
+
progress_status = ProgressStatus(len(lines), percent)
|
|
260
|
+
|
|
261
|
+
pdf_file = None
|
|
262
|
+
if show_fig:
|
|
263
|
+
figure_no = 1
|
|
264
|
+
fnm = f"Well_schematic_{figure_no:03d}.pdf"
|
|
265
|
+
while os.path.isfile(fnm):
|
|
266
|
+
figure_no += 1
|
|
267
|
+
fnm = f"Well_schematic_{figure_no:03d}.pdf"
|
|
268
|
+
pdf_file = create_pdfpages(fnm)
|
|
269
|
+
# loop lines
|
|
270
|
+
while line_number < len(lines):
|
|
271
|
+
progress_status.update(line_number)
|
|
272
|
+
line = lines[line_number]
|
|
273
|
+
keyword = line[:8].rstrip() # look for keywords
|
|
274
|
+
|
|
275
|
+
# most lines will just be duplicated
|
|
276
|
+
if keyword not in Keywords.main_keywords:
|
|
277
|
+
outfile.write(None, f"{line}\n")
|
|
278
|
+
else:
|
|
279
|
+
# This is a (potential) MSW keyword.
|
|
280
|
+
logger.debug(keyword)
|
|
281
|
+
|
|
282
|
+
well_name = _get_well_name(clean_lines_map, line_number)
|
|
283
|
+
if keyword in Keywords.segments: # check if it is an active well
|
|
284
|
+
logger.debug(well_name)
|
|
285
|
+
if well_name not in list(schedule.active_wells):
|
|
286
|
+
outfile.write(keyword, "")
|
|
287
|
+
line_number += 1
|
|
288
|
+
continue # not an active well
|
|
289
|
+
|
|
290
|
+
# first, collect data for this keyword into a 'chunk'
|
|
291
|
+
chunk_str = ""
|
|
292
|
+
raw = [] # only used for WELSPECS which we dont modify
|
|
293
|
+
# concatenate and look for 'end of records' => //
|
|
294
|
+
while not re.search(r"/\s*/$", chunk_str):
|
|
295
|
+
line_number += 1
|
|
296
|
+
raw.append(lines[line_number])
|
|
297
|
+
if line_number in clean_lines_map:
|
|
298
|
+
chunk_str += clean_lines_map[line_number]
|
|
299
|
+
chunk = _format_chunk(chunk_str)
|
|
300
|
+
chunks.append((keyword, chunk)) # for debug ...
|
|
301
|
+
|
|
302
|
+
# use data to update our schedule
|
|
303
|
+
if keyword == Keywords.WELSPECS:
|
|
304
|
+
schedule.set_welspecs(chunk) # update with new data
|
|
305
|
+
outfile.write(keyword, raw, chunk=False) # but write it back 'untouched'
|
|
306
|
+
line_number += 1 # ready for next line
|
|
307
|
+
continue
|
|
308
|
+
|
|
309
|
+
elif keyword == Keywords.COMPDAT:
|
|
310
|
+
remains = schedule.handle_compdat(chunk) # update with new data
|
|
311
|
+
if remains:
|
|
312
|
+
# Add single quotes to non-active well names
|
|
313
|
+
for remain in remains:
|
|
314
|
+
remain[0] = "'" + remain[0] + "'"
|
|
315
|
+
outfile.write(keyword, remains, end_of_record=True) # write any 'none-active' wells here
|
|
316
|
+
line_number += 1 # ready for next line
|
|
317
|
+
continue
|
|
318
|
+
|
|
319
|
+
elif keyword == Keywords.WELSEGS:
|
|
320
|
+
schedule.set_welsegs(chunk) # update with new data
|
|
321
|
+
|
|
322
|
+
elif keyword == Keywords.COMPSEGS:
|
|
323
|
+
# this is COMPSEGS'. will now update and write out new data
|
|
324
|
+
schedule.set_compsegs(chunk)
|
|
325
|
+
|
|
326
|
+
try:
|
|
327
|
+
case.check_input(well_name, schedule)
|
|
328
|
+
except NameError as err:
|
|
329
|
+
# This might mean that `Keywords.segments` has changed to
|
|
330
|
+
# not include `Keywords.COMPSEGS`
|
|
331
|
+
raise SystemError(
|
|
332
|
+
"Well name not defined, even though it should be defined when "
|
|
333
|
+
f"token ({keyword} is one of "
|
|
334
|
+
f"{', '.join(Keywords.segments)})"
|
|
335
|
+
) from err
|
|
336
|
+
|
|
337
|
+
if well_name not in written:
|
|
338
|
+
write_welsegs = True # will only write WELSEGS once
|
|
339
|
+
written.add(well_name)
|
|
340
|
+
else:
|
|
341
|
+
write_welsegs = False
|
|
342
|
+
figno += 1
|
|
343
|
+
logger.debug("Writing new MSW info for well %s", well_name)
|
|
344
|
+
wells.update(well_name, schedule)
|
|
345
|
+
output = CreateOutput(
|
|
346
|
+
case,
|
|
347
|
+
schedule,
|
|
348
|
+
wells,
|
|
349
|
+
well_name,
|
|
350
|
+
schedule.get_well_number(well_name),
|
|
351
|
+
completor.__version__,
|
|
352
|
+
show_fig,
|
|
353
|
+
pdf_file,
|
|
354
|
+
write_welsegs,
|
|
355
|
+
paths,
|
|
356
|
+
)
|
|
357
|
+
outfile.write(None, output.finalprint)
|
|
358
|
+
else:
|
|
359
|
+
raise ValueError(f"The keyword '{keyword}' has not been implemented in Completor, but should have been")
|
|
360
|
+
|
|
361
|
+
line_number += 1 # ready for next line
|
|
362
|
+
logger.debug(line_number)
|
|
363
|
+
outfile.close()
|
|
364
|
+
close_figure()
|
|
365
|
+
if pdf_file is not None:
|
|
366
|
+
pdf_file.close()
|
|
367
|
+
|
|
368
|
+
try:
|
|
369
|
+
return chunks, case, schedule, wells, output # for debug ...
|
|
370
|
+
except NameError:
|
|
371
|
+
if len(schedule.active_wells) == 0:
|
|
372
|
+
return chunks, case, schedule, wells
|
|
373
|
+
else:
|
|
374
|
+
raise ValueError(
|
|
375
|
+
"Inconsistent case and input schedule files. "
|
|
376
|
+
"Check well names and WELSPECS, COMPDAT, WELSEGS and COMPSEGS."
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def main() -> None:
|
|
381
|
+
"""Generate a Completor output schedule file from the input given from user.
|
|
382
|
+
|
|
383
|
+
Also set the correct loglevel based on user input. Defaults to WARNING if not set.
|
|
384
|
+
|
|
385
|
+
Raises:
|
|
386
|
+
CompletorError: If input schedule file is not defined as input or in case file.
|
|
387
|
+
"""
|
|
388
|
+
parser = get_parser()
|
|
389
|
+
inputs = parser.parse_args()
|
|
390
|
+
|
|
391
|
+
if inputs.loglevel is not None:
|
|
392
|
+
loglevel = inputs.loglevel
|
|
393
|
+
else:
|
|
394
|
+
loglevel = logging.WARNING
|
|
395
|
+
# Loglevel NOTSET (0) gets overwritten by higher up loggers to WARNING, setting loglevel to 1 is a lazy workaround.
|
|
396
|
+
loglevel = 1 if loglevel == 0 else loglevel
|
|
397
|
+
|
|
398
|
+
logger.setLevel(loglevel)
|
|
399
|
+
|
|
400
|
+
# Open the case file
|
|
401
|
+
if inputs.inputfile is not None:
|
|
402
|
+
with open(inputs.inputfile, encoding="utf-8") as file:
|
|
403
|
+
case_file_content = file.read()
|
|
404
|
+
else:
|
|
405
|
+
raise CompletorError("Need input case file to run Completor")
|
|
406
|
+
|
|
407
|
+
schedule_file_content, inputs.schedulefile = get_content_and_path(
|
|
408
|
+
case_file_content, inputs.schedulefile, Keywords.SCHFILE
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
if isinstance(schedule_file_content, str):
|
|
412
|
+
parse.read_schedule_keywords(clean_file_lines(schedule_file_content.splitlines()), Keywords.main_keywords)
|
|
413
|
+
|
|
414
|
+
_, inputs.outputfile = get_content_and_path(case_file_content, inputs.outputfile, Keywords.OUTFILE)
|
|
415
|
+
|
|
416
|
+
if inputs.outputfile is None:
|
|
417
|
+
if inputs.schedulefile is None:
|
|
418
|
+
raise ValueError("No schedule provided, or none where found " "in the case file (keyword 'SCHFILE')")
|
|
419
|
+
inputs.outputfile = inputs.schedulefile.split(".")[0] + "_advanced.wells"
|
|
420
|
+
|
|
421
|
+
paths_input_schedule = (inputs.inputfile, inputs.schedulefile)
|
|
422
|
+
|
|
423
|
+
logger.debug("Running Completor %s. An advanced well modelling tool.", completor.__version__)
|
|
424
|
+
logger.debug("-" * 60)
|
|
425
|
+
start_a = time.time()
|
|
426
|
+
|
|
427
|
+
handle_error_messages(create)(
|
|
428
|
+
case_file_content, schedule_file_content, inputs.outputfile, inputs.figure, paths=paths_input_schedule
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
logger.debug("Total runtime: %d", (time.time() - start_a))
|
|
432
|
+
logger.debug("-" * 60)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _get_well_name(schedule_lines: dict[int, str], i: int) -> str:
|
|
436
|
+
"""Get the well name from line number
|
|
437
|
+
|
|
438
|
+
Args:
|
|
439
|
+
schedule_lines: Dictionary of lines in schedule file.
|
|
440
|
+
i: Line index.
|
|
441
|
+
|
|
442
|
+
Returns:
|
|
443
|
+
Well name.
|
|
444
|
+
"""
|
|
445
|
+
keys = np.array(sorted(list(schedule_lines.keys())))
|
|
446
|
+
j = np.where(keys == i)[0][0]
|
|
447
|
+
next_line = schedule_lines[int(keys[j + 1])]
|
|
448
|
+
return next_line.split()[0]
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _format_chunk(chunk_str: str) -> list[list[str]]:
|
|
452
|
+
"""Format the data-records and resolve the repeat-mechanism.
|
|
453
|
+
|
|
454
|
+
E.g. 3* == 1* 1* 1*, 3*250 == 250 250 250.
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
chunk_str: A chunk data-record.
|
|
458
|
+
|
|
459
|
+
Returns:
|
|
460
|
+
Expanded values.
|
|
461
|
+
"""
|
|
462
|
+
chunk = re.split(r"\s+/", chunk_str)[:-1]
|
|
463
|
+
expanded_data = []
|
|
464
|
+
for line in chunk:
|
|
465
|
+
new_record = ""
|
|
466
|
+
for record in line.split():
|
|
467
|
+
if not record[0].isdigit():
|
|
468
|
+
new_record += record + " "
|
|
469
|
+
continue
|
|
470
|
+
if "*" not in record:
|
|
471
|
+
new_record += record + " "
|
|
472
|
+
continue
|
|
473
|
+
|
|
474
|
+
# need to handle things like 3* or 3*250
|
|
475
|
+
multiplier, number = record.split("*")
|
|
476
|
+
new_record += f"{number if number else '1*'} " * int(multiplier)
|
|
477
|
+
if new_record:
|
|
478
|
+
expanded_data.append(new_record.split())
|
|
479
|
+
return expanded_data
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
if __name__ == "__main__":
|
|
483
|
+
try:
|
|
484
|
+
main()
|
|
485
|
+
except CompletorError as e:
|
|
486
|
+
raise abort(str(e)) from e
|