completor 0.1.2__py3-none-any.whl → 1.0.0__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/main.py CHANGED
@@ -6,165 +6,28 @@ import logging
6
6
  import os
7
7
  import re
8
8
  import time
9
- from collections.abc import Mapping
10
- from typing import overload
11
9
 
12
- import numpy as np
10
+ import pandas as pd
11
+ from tqdm import tqdm
13
12
 
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
13
+ from completor import create_output, parse, read_schedule, utils
14
+ from completor.constants import Keywords, ScheduleData
20
15
  from completor.exceptions import CompletorError
16
+ from completor.get_version import get_version
21
17
  from completor.launch_args_parser import get_parser
22
18
  from completor.logger import handle_error_messages, logger
23
19
  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.
20
+ from completor.utils import (
21
+ abort,
22
+ clean_file_lines,
23
+ clean_raw_data,
24
+ find_keyword_data,
25
+ find_well_keyword_data,
26
+ replace_preprocessing_names,
27
+ )
28
+ from completor.wells import Well
143
29
 
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
30
+ pd.set_option("future.no_silent_downcasting", True)
168
31
 
169
32
 
170
33
  def get_content_and_path(case_content: str, file_path: str | None, keyword: str) -> tuple[str | None, str | None]:
@@ -181,8 +44,7 @@ def get_content_and_path(case_content: str, file_path: str | None, keyword: str)
181
44
  File content, file path.
182
45
 
183
46
  Raises:
184
- CompletorError: If the keyword cannot be found.
185
- CompletorError: If the file cannot be found.
47
+ CompletorError: If the keyword or file cannot be found.
186
48
  """
187
49
  if file_path is None:
188
50
  # Find the path/name of file from case file
@@ -195,186 +57,112 @@ def get_content_and_path(case_content: str, file_path: str | None, keyword: str)
195
57
  file_path = re.sub("[\"']+", "", file_path)
196
58
 
197
59
  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":
60
+ # OUT_FILE is optional, if it's needed but not supplied the error is caught in ReadCasefile:check_pvt_file()
61
+ if keyword == Keywords.OUT_FILE:
200
62
  return None, None
201
63
  raise CompletorError(f"The keyword {keyword} is not defined correctly in the casefile")
202
- if keyword != "OUTFILE":
64
+ if keyword != Keywords.OUT_FILE:
203
65
  try:
204
66
  with open(file_path, encoding="utf-8") as file:
205
67
  file_content = file.read()
206
68
  except FileNotFoundError as e:
207
69
  raise CompletorError(f"Could not find the file: '{file_path}'!") from e
208
70
  except (PermissionError, IsADirectoryError) as e:
209
- raise CompletorError("Could not read SCHFILE, this is likely because the path is missing quotes.") from e
71
+ raise CompletorError(
72
+ f"Could not read {Keywords.SCHEDULE_FILE}, this is likely because the path is missing quotes."
73
+ ) from e
210
74
  return file_content, file_path
211
75
  return None, file_path
212
76
 
213
77
 
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
78
  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.
79
+ case_file: str, schedule: str, new_file: str, show_fig: bool = False, paths: tuple[str, str] | None = None
80
+ ) -> tuple[ReadCasefile, Well | None]:
81
+ """Create and write the advanced schedule file from input case- and schedule files.
230
82
 
231
83
  Args:
232
- input_file: Input case file.
233
- schedule_file: Input schedule file.
84
+ case_file: Input case file.
85
+ schedule: Input schedule file.
234
86
  new_file: Output schedule file.
235
87
  show_fig: Flag indicating if a figure is to be shown.
236
- percent: ProgressStatus percentage steps to be shown (in percent, %).
237
88
  paths: Optional additional paths.
238
89
 
239
90
  Returns:
240
- Completor schedule file.
91
+ The case and schedule file, the well and output object.
241
92
  """
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
93
+ case = ReadCasefile(case_file=case_file, schedule_file=schedule, output_file=new_file)
94
+ active_wells = utils.get_active_wells(case.completion_table, case.gp_perf_devicelayer)
95
+
96
+ figure_name = None
262
97
  if show_fig:
263
98
  figure_no = 1
264
- fnm = f"Well_schematic_{figure_no:03d}.pdf"
265
- while os.path.isfile(fnm):
99
+ figure_name = f"Well_schematic_{figure_no:03d}.pdf"
100
+ while os.path.isfile(figure_name):
266
101
  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)
102
+ figure_name = f"Well_schematic_{figure_no:03d}.pdf"
325
103
 
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()
104
+ err: Exception | None = None
105
+ well = None
106
+ # Add banner.
107
+ schedule = create_output.metadata_banner(paths) + schedule
108
+ # Strip trailing whitespace.
109
+ schedule = re.sub(r"[^\S\r\n]+$", "", schedule, flags=re.MULTILINE)
110
+ meaningful_data: ScheduleData = {}
367
111
 
368
112
  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
- )
113
+ # Find the old data for each of the four main keywords.
114
+ for chunk in find_keyword_data(Keywords.WELL_SPECIFICATION, schedule):
115
+ clean_data = clean_raw_data(chunk, Keywords.WELL_SPECIFICATION)
116
+ meaningful_data = read_schedule.set_welspecs(meaningful_data, clean_data)
117
+
118
+ for chunk in find_keyword_data(Keywords.COMPLETION_DATA, schedule):
119
+ clean_data = clean_raw_data(chunk, Keywords.COMPLETION_DATA)
120
+ meaningful_data = read_schedule.set_compdat(meaningful_data, clean_data)
121
+
122
+ for chunk in find_keyword_data(Keywords.WELL_SEGMENTS, schedule):
123
+ clean_data = clean_raw_data(chunk, Keywords.WELL_SEGMENTS)
124
+ meaningful_data = read_schedule.set_welsegs(meaningful_data, clean_data)
125
+
126
+ for chunk in find_keyword_data(Keywords.COMPLETION_SEGMENTS, schedule):
127
+ clean_data = clean_raw_data(chunk, Keywords.COMPLETION_SEGMENTS)
128
+ meaningful_data = read_schedule.set_compsegs(meaningful_data, clean_data)
129
+
130
+ for i, well_name in tqdm(enumerate(active_wells.tolist()), total=len(active_wells)):
131
+ well = Well(well_name, i, case, meaningful_data[well_name])
132
+ compdat, welsegs, compsegs, bonus = create_output.format_output(well, case, figure_name)
133
+
134
+ for keyword in [Keywords.COMPLETION_SEGMENTS, Keywords.WELL_SEGMENTS, Keywords.COMPLETION_DATA]:
135
+ old_data = find_well_keyword_data(well_name, keyword, schedule)
136
+ if not old_data:
137
+ raise CompletorError(
138
+ "Could not find the unmodified data in original schedule file. Please contact the team!"
139
+ )
140
+ try:
141
+ # Check that nothing is lost.
142
+ schedule.index(old_data)
143
+ except ValueError:
144
+ raise CompletorError("Could not match the old data to schedule file. Please contact the team!")
145
+
146
+ match keyword:
147
+ case Keywords.COMPLETION_DATA:
148
+ schedule = schedule.replace(old_data, compdat)
149
+ case Keywords.COMPLETION_SEGMENTS:
150
+ schedule = schedule.replace(old_data, compsegs + bonus)
151
+ case Keywords.WELL_SEGMENTS:
152
+ schedule = schedule.replace(old_data, welsegs)
153
+
154
+ except Exception as e_:
155
+ err = e_
156
+ finally:
157
+ # Make sure the output thus far is written, and figure files are closed.
158
+ schedule = replace_preprocessing_names(schedule, case.mapper)
159
+ with open(new_file, "w", encoding="utf-8") as file:
160
+ file.write(schedule)
161
+
162
+ if err is not None:
163
+ raise err
164
+
165
+ return case, well
378
166
 
379
167
 
380
168
  def main() -> None:
@@ -401,26 +189,27 @@ def main() -> None:
401
189
  if inputs.inputfile is not None:
402
190
  with open(inputs.inputfile, encoding="utf-8") as file:
403
191
  case_file_content = file.read()
404
- else:
405
- raise CompletorError("Need input case file to run Completor")
406
192
 
407
193
  schedule_file_content, inputs.schedulefile = get_content_and_path(
408
- case_file_content, inputs.schedulefile, Keywords.SCHFILE
194
+ case_file_content, inputs.schedulefile, Keywords.SCHEDULE_FILE
409
195
  )
410
196
 
411
197
  if isinstance(schedule_file_content, str):
412
198
  parse.read_schedule_keywords(clean_file_lines(schedule_file_content.splitlines()), Keywords.main_keywords)
413
199
 
414
- _, inputs.outputfile = get_content_and_path(case_file_content, inputs.outputfile, Keywords.OUTFILE)
200
+ _, inputs.outputfile = get_content_and_path(case_file_content, inputs.outputfile, Keywords.OUT_FILE)
415
201
 
416
202
  if inputs.outputfile is None:
417
203
  if inputs.schedulefile is None:
418
- raise ValueError("No schedule provided, or none where found " "in the case file (keyword 'SCHFILE')")
204
+ raise ValueError(
205
+ "Could not find a path to schedule file. "
206
+ f"It must be provided as a input argument or within the case files keyword '{Keywords.SCHEDULE_FILE}'."
207
+ )
419
208
  inputs.outputfile = inputs.schedulefile.split(".")[0] + "_advanced.wells"
420
209
 
421
210
  paths_input_schedule = (inputs.inputfile, inputs.schedulefile)
422
211
 
423
- logger.debug("Running Completor %s. An advanced well modelling tool.", completor.__version__)
212
+ logger.info("Running Completor version %s. An advanced well modelling tool.", get_version())
424
213
  logger.debug("-" * 60)
425
214
  start_a = time.time()
426
215
 
@@ -432,53 +221,6 @@ def main() -> None:
432
221
  logger.debug("-" * 60)
433
222
 
434
223
 
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
224
  if __name__ == "__main__":
483
225
  try:
484
226
  main()