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/parse.py ADDED
@@ -0,0 +1,581 @@
1
+ """Functions for reading files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from copy import deepcopy
7
+ from typing import Literal, overload
8
+
9
+ import numpy as np
10
+ import numpy.typing as npt
11
+ import pandas as pd
12
+
13
+ from completor.constants import Headers, Keywords
14
+ from completor.exceptions.clean_exceptions import CompletorError
15
+
16
+
17
+ class ContentCollection(list):
18
+ """A subclass of list that can accept additional attributes. To be used like a regular list."""
19
+
20
+ def __new__(cls, *args, **kwargs):
21
+ """Override new method of list."""
22
+ return super().__new__(cls, args, kwargs)
23
+
24
+ def __init__(self, *args, name: str, well: pd.DataFrame | str | None = None):
25
+ """Override init method of list."""
26
+ if len(args) == 1 and hasattr(args[0], "__iter__"):
27
+ list.__init__(self, args[0])
28
+ else:
29
+ list.__init__(self, args)
30
+ self.name = name
31
+ self.well = well
32
+
33
+ def __call__(self, **kwargs):
34
+ """Override call method of list."""
35
+ self.__dict__.update(kwargs)
36
+ return self
37
+
38
+
39
+ @overload
40
+ def locate_keyword(
41
+ content: list[str], keyword: str, end_char: str = ..., take_first: Literal[True] = ...
42
+ ) -> tuple[int, int]: ...
43
+
44
+
45
+ @overload
46
+ def locate_keyword(
47
+ content: list[str], keyword: str, end_char: str = ..., *, take_first: Literal[False]
48
+ ) -> tuple[npt.NDArray[np.int64], npt.NDArray[np.int64]]: ...
49
+
50
+
51
+ @overload
52
+ def locate_keyword(
53
+ content: list[str], keyword: str, end_char: str, take_first: Literal[False]
54
+ ) -> tuple[npt.NDArray[np.int64], npt.NDArray[np.int64]]: ...
55
+
56
+
57
+ def locate_keyword(
58
+ content: list[str], keyword: str, end_char: str = "/", take_first: bool = True
59
+ ) -> tuple[int, int] | tuple[npt.NDArray[np.int64], npt.NDArray[np.int64]]:
60
+ """Find the start and end of a keyword.
61
+
62
+ The start of the keyword is the keyword itself.
63
+ The end of the keyword is end_char if specified.
64
+
65
+ Args:
66
+ content: List of strings.
67
+ keyword: Keyword name.
68
+ end_char: String which ends the keyword. Defaults to '/' Because it's the most used in this code base.
69
+ take_first: Flag to toggle whether to return the first elements of the arrays.
70
+
71
+ Returns:
72
+ start_index - array that located the keyword (or its first element).
73
+ end_index - array that locates the end of the keyword (or its first element).
74
+
75
+ Raises:
76
+ CompletorError: If keyword had no end record.
77
+ ValueError: If keyword cannot be found in case file.
78
+ """
79
+ content_length = len(content)
80
+ start_index: npt.NDArray[np.int64] = np.where(np.asarray(content) == keyword)[0]
81
+ if start_index.size == 0:
82
+ # the keyword is not found
83
+ return np.asarray([-1]), np.asarray([-1])
84
+
85
+ end_index: npt.NDArray[np.int64] = np.array([], dtype="int64")
86
+ idx = 0
87
+ for istart in start_index:
88
+ if end_char != "":
89
+ idx = istart + 1
90
+ for idx in range(istart + 1, content_length):
91
+ if content[idx] == end_char:
92
+ break
93
+ if (idx == content_length - 1) and content[idx] != end_char:
94
+ # error if until the last line the end char is not found
95
+ raise CompletorError(f"Keyword {keyword} has no end record")
96
+ else:
97
+ # if there is no end character is specified, then the end of a record is the next keyword or end of line
98
+ for idx in range(istart + 1, content_length):
99
+ first_char = content[idx][0]
100
+ if first_char.isalpha():
101
+ # end is before the new keyword
102
+ idx -= 1
103
+ break
104
+
105
+ try:
106
+ end_index = np.append(end_index, idx)
107
+ except NameError as err:
108
+ raise ValueError(f"Cannot find keyword {keyword} in file") from err
109
+ # return all in a numpy array format
110
+ end_index = np.asarray(end_index)
111
+ if take_first:
112
+ return start_index[0], end_index[0]
113
+ return start_index, end_index
114
+
115
+
116
+ def take_first_record(
117
+ start_index: list[float] | npt.NDArray[np.float64], end_index: list[float] | npt.NDArray[np.float64]
118
+ ) -> tuple[float | int, float | int]:
119
+ """Take the first record of a list.
120
+
121
+ Args:
122
+ start_index:
123
+ end_index:
124
+
125
+ Returns:
126
+ Tuple of floats.
127
+ """
128
+ return start_index[0], end_index[0]
129
+
130
+
131
+ def unpack_records(record: list[str]) -> list[str]:
132
+ """Unpack the keyword content.
133
+
134
+ E.g. 3* --> 1* 1* 1*
135
+
136
+ Args:
137
+ record: List of strings.
138
+
139
+ Returns:
140
+ Updated record of strings.
141
+ """
142
+ record = deepcopy(record)
143
+ record_length = len(record)
144
+ i = -1
145
+ while i < record_length - 1:
146
+ # Loop and find if default records are found
147
+ i += 1
148
+ if "*" in str(record[i]):
149
+ # default is found and get the number before the star *
150
+ ndefaults = re.search(r"\d+", record[i])
151
+ record[i] = "1*"
152
+ if ndefaults:
153
+ _ndefaults = int(ndefaults.group())
154
+ idef = 0
155
+ while idef < _ndefaults - 1:
156
+ record.insert(i, "1*")
157
+ idef += 1
158
+ record_length = len(record)
159
+ return record
160
+
161
+
162
+ def complete_records(record: list[str], keyword: str) -> list[str]:
163
+ """Complete the record.
164
+
165
+ Args:
166
+ record: List of strings.
167
+ keyword: Keyword name.
168
+
169
+ Returns:
170
+ Completed list of strings.
171
+ """
172
+ if keyword == Keywords.WSEGVALV:
173
+ return complete_wsegvalv_record(record)
174
+
175
+ dict_ncolumns = {
176
+ Keywords.WELSPECS: 17,
177
+ Keywords.COMPDAT: 14,
178
+ Keywords.WELSEGS_H: 12,
179
+ Keywords.WELSEGS: 15,
180
+ Keywords.COMPSEGS: 11,
181
+ }
182
+ max_column = dict_ncolumns[keyword]
183
+ ncolumn = len(record)
184
+ if ncolumn < max_column:
185
+ extension = ["1*"] * (max_column - ncolumn)
186
+ record.extend(extension)
187
+ elif ncolumn > max_column:
188
+ record = record[:max_column]
189
+ return record
190
+
191
+
192
+ def complete_wsegvalv_record(record: list[str]) -> list[str]:
193
+ """Complete the WSEGVALV record.
194
+
195
+ Columns DEFAULT_1 - DEFAULT_4, STATE and AC_MAX might not be provided and need to be filled in with default values.
196
+
197
+ Args:
198
+ record: List of strings.
199
+
200
+ Returns:
201
+ Completed list of strings.
202
+ """
203
+ WSEGVALV_COLUMNS = 10
204
+ AC_INDEX = 3
205
+ DEFAULT_STATE = "OPEN"
206
+
207
+ if len(record) < 8:
208
+ # add defaults
209
+ record.extend(["1*"] * (8 - len(record)))
210
+
211
+ if len(record) < 9:
212
+ # append default state
213
+ record.append(DEFAULT_STATE)
214
+
215
+ if len(record) < WSEGVALV_COLUMNS:
216
+ # append default ac_max
217
+ record.append(record[AC_INDEX])
218
+
219
+ if len(record) > WSEGVALV_COLUMNS:
220
+ record = record[:WSEGVALV_COLUMNS]
221
+
222
+ return record
223
+
224
+
225
+ def read_schedule_keywords(
226
+ content: list[str], keywords: list[str], optional_keywords: list[str] = []
227
+ ) -> tuple[list[ContentCollection], npt.NDArray[np.str_]]:
228
+ """Read schedule keywords or all keywords in table format.
229
+
230
+ E.g. WELSPECS, COMPDAT, WELSEGS, COMPSEGS, WSEGVALV.
231
+
232
+ Args:
233
+ content: List of strings. Lines from the schedule file.
234
+ keywords: List of keywords to find data for.
235
+ optional_keywords: List of optional keywords. Will not raise error if not found.
236
+
237
+ Returns:
238
+ df_collection - Object collection (pd.DataFrame).
239
+ remaining_content - List of strings of un-listed keywords.
240
+
241
+ Raises:
242
+ CompletorError: If keyword is not found.
243
+ """
244
+ content = deepcopy(content)
245
+ used_index = np.asarray([-1])
246
+ collections = []
247
+ # get the contents correspond to the list_keywords
248
+ for keyword in keywords + optional_keywords:
249
+ start_index, end_index = locate_keyword(content, keyword, take_first=False)
250
+ if start_index[0] == end_index[0] and keyword not in optional_keywords:
251
+ raise CompletorError(f"Keyword {keyword} is not found")
252
+ for idx, start in enumerate(start_index):
253
+ end = end_index[idx]
254
+ used_index = np.append(used_index, np.arange(start, end + 1))
255
+ keyword_content = [_create_record(content, keyword, irec, start) for irec in range(start + 1, end)]
256
+ collection = ContentCollection(keyword_content, name=keyword)
257
+ if keyword in [Keywords.WELSEGS, Keywords.COMPSEGS]:
258
+ # remove string characters
259
+ collection.well = remove_string_characters(keyword_content[0][0])
260
+ collections.append(collection)
261
+ # get anything that is not listed in the keywords
262
+ # ignore the first record -1
263
+ used_index = used_index[1:]
264
+ mask = np.full(len(content), True, dtype=bool)
265
+ mask[used_index] = False
266
+ return collections, np.asarray(content)[mask]
267
+
268
+
269
+ def _create_record(content: list[str], keyword: str, irec: int, start: int) -> list[str]:
270
+ _record = content[irec]
271
+ # remove / sign at the end
272
+ _record = list(filter(None, _record.rsplit("/", 1)))[0]
273
+ # split each column
274
+ record = list(filter(None, _record.split(" ")))
275
+ # unpack records
276
+ record = unpack_records(record)
277
+ # complete records
278
+ record = complete_records(
279
+ record, Keywords.WELSEGS_H if keyword == Keywords.WELSEGS and irec == start + 1 else keyword
280
+ )
281
+ return record
282
+
283
+
284
+ def get_welsegs_table(collections: list[ContentCollection]) -> tuple[pd.DataFrame, pd.DataFrame]:
285
+ """Return dataframe table of WELSEGS.
286
+
287
+ Args:
288
+ collections: ContentCollection class.
289
+
290
+ Returns:
291
+ header_table - The header of WELSEGS.
292
+ record_table - The record of WELSEGS.
293
+
294
+ Raises:
295
+ ValueError: If collection does not contain the 'WELSEGS' keyword.
296
+ """
297
+ header_columns = [
298
+ Headers.WELL,
299
+ Headers.SEGMENTTVD,
300
+ Headers.SEGMENTMD,
301
+ Headers.WBVOLUME,
302
+ Headers.INFO_TYPE,
303
+ Headers.PDROPCOMP,
304
+ Headers.MPMODEL,
305
+ Headers.ITEM_8,
306
+ Headers.ITEM_9,
307
+ Headers.ITEM_10,
308
+ Headers.ITEM_11,
309
+ Headers.ITEM_12,
310
+ ]
311
+ content_columns = [
312
+ Headers.WELL,
313
+ Headers.TUBING_SEGMENT,
314
+ Headers.TUBING_SEGMENT_2,
315
+ Headers.TUBINGBRANCH,
316
+ Headers.TUBING_OUTLET,
317
+ Headers.TUBINGMD,
318
+ Headers.TUBINGTVD,
319
+ Headers.TUBING_INNER_DIAMETER,
320
+ Headers.TUBING_ROUGHNESS,
321
+ Headers.CROSS,
322
+ Headers.VSEG,
323
+ Headers.ITEM_11,
324
+ Headers.ITEM_12,
325
+ Headers.ITEM_13,
326
+ Headers.ITEM_14,
327
+ Headers.ITEM_15,
328
+ ]
329
+ for collection in collections:
330
+ if collection.name == Keywords.WELSEGS:
331
+ header_collection = np.asarray(collection[:1])
332
+ record_collection = np.asarray(collection[1:])
333
+ # add additional well column on the second collection
334
+ well_column = np.full(record_collection.shape[0], collection.well)
335
+ record_collection = np.column_stack((well_column, record_collection))
336
+ try:
337
+ header_table: npt.NDArray[np.unicode_] | pd.DataFrame
338
+ record_table: npt.NDArray[np.unicode_] | pd.DataFrame
339
+ header_table = np.row_stack((header_table, header_collection))
340
+ record_table = np.row_stack((record_table, record_collection))
341
+ except NameError:
342
+ # First iteration
343
+ header_table = np.asarray(header_collection)
344
+ record_table = np.asarray(record_collection)
345
+ try:
346
+ header_table = pd.DataFrame(header_table, columns=header_columns)
347
+ record_table = pd.DataFrame(record_table, columns=content_columns)
348
+ except NameError as err:
349
+ raise ValueError("Collection does not contain the 'WELSEGS' keyword") from err
350
+
351
+ # replace string component " or ' in the columns
352
+ header_table = remove_string_characters(header_table)
353
+ record_table = remove_string_characters(record_table)
354
+ return header_table, record_table
355
+
356
+
357
+ def get_welspecs_table(collections: list[ContentCollection]) -> pd.DataFrame:
358
+ """Return dataframe table of WELSPECS.
359
+
360
+ Args:
361
+ collections: ContentCollection class.
362
+
363
+ Returns:
364
+ WELSPECS table.
365
+
366
+ Raises:
367
+ ValueError: If collection does not contain the 'WELSPECS' keyword.
368
+ """
369
+ columns = [
370
+ Headers.WELL,
371
+ Headers.GROUP,
372
+ Headers.I,
373
+ Headers.J,
374
+ Headers.BHP_DEPTH,
375
+ Headers.PHASE,
376
+ Headers.DR,
377
+ Headers.FLAG,
378
+ Headers.SHUT,
379
+ Headers.CROSS,
380
+ Headers.PRESSURE_TABLE,
381
+ Headers.DENSCAL,
382
+ Headers.REGION,
383
+ Headers.ITEM_14,
384
+ Headers.ITEM_15,
385
+ Headers.ITEM_16,
386
+ Headers.ITEM_17,
387
+ ]
388
+ welspecs_table = None
389
+ for collection in collections:
390
+ if collection.name == Keywords.WELSPECS:
391
+ the_collection = np.asarray(collection)
392
+ if welspecs_table is None:
393
+ welspecs_table = np.copy(the_collection)
394
+ else:
395
+ welspecs_table = np.row_stack((welspecs_table, the_collection))
396
+
397
+ if welspecs_table is None:
398
+ raise ValueError("Collection does not contain the 'WELSPECS' keyword")
399
+
400
+ welspecs_table = pd.DataFrame(welspecs_table, columns=columns)
401
+ # replace string component " or ' in the columns
402
+ welspecs_table = remove_string_characters(welspecs_table)
403
+ return welspecs_table
404
+
405
+
406
+ def get_compdat_table(collections: list[ContentCollection]) -> pd.DataFrame:
407
+ """Return dataframe table of COMPDAT.
408
+
409
+ Args:
410
+ collections: ContentCollection class.
411
+
412
+ Returns:
413
+ COMPDAT table.
414
+
415
+ Raises:
416
+ ValueError: If a collection does not contain the 'COMPDAT' keyword.
417
+ """
418
+ compdat_table = None
419
+ for collection in collections:
420
+ if collection.name == Keywords.COMPDAT:
421
+ the_collection = np.asarray(collection)
422
+ if compdat_table is None:
423
+ compdat_table = np.copy(the_collection)
424
+ else:
425
+ compdat_table = np.row_stack((compdat_table, the_collection))
426
+ if compdat_table is None:
427
+ raise ValueError("Collection does not contain the 'COMPDAT' keyword")
428
+ compdat_table = pd.DataFrame(
429
+ compdat_table,
430
+ columns=[
431
+ Headers.WELL,
432
+ Headers.I,
433
+ Headers.J,
434
+ Headers.K,
435
+ Headers.K2,
436
+ Headers.STATUS,
437
+ Headers.SATURATION_FUNCTION_REGION_NUMBERS,
438
+ Headers.CONNECTION_FACTOR,
439
+ Headers.DIAMETER,
440
+ Headers.FORAMTION_PERMEABILITY_THICKNESS,
441
+ Headers.SKIN,
442
+ Headers.DFACT,
443
+ Headers.COMPDAT_DIRECTION,
444
+ Headers.RO,
445
+ ],
446
+ )
447
+ # replace string component " or ' in the columns
448
+ compdat_table = remove_string_characters(compdat_table)
449
+ return compdat_table
450
+
451
+
452
+ def get_compsegs_table(collections: list[ContentCollection]) -> pd.DataFrame:
453
+ """Return data frame table of COMPSEGS.
454
+
455
+ Args:
456
+ collections: ContentCollection class.
457
+
458
+ Returns:
459
+ COMPSEGS table.
460
+
461
+ Raises:
462
+ ValueError: If collection does not contain the 'COMPSEGS' keyword.
463
+
464
+ """
465
+ compsegs_table = None
466
+ for collection in collections:
467
+ if collection.name == Keywords.COMPSEGS:
468
+ the_collection = np.asarray(collection[1:])
469
+ # add additional well column
470
+ well_column = np.full(the_collection.shape[0], collection.well)
471
+ the_collection = np.column_stack((well_column, the_collection))
472
+ if compsegs_table is None:
473
+ compsegs_table = np.copy(the_collection)
474
+ else:
475
+ compsegs_table = np.row_stack((compsegs_table, the_collection))
476
+
477
+ if compsegs_table is None:
478
+ raise ValueError("Collection does not contain the 'COMPSEGS' keyword")
479
+
480
+ compsegs_table = pd.DataFrame(
481
+ compsegs_table,
482
+ columns=[
483
+ Headers.WELL,
484
+ Headers.I,
485
+ Headers.J,
486
+ Headers.K,
487
+ Headers.BRANCH,
488
+ Headers.START_MEASURED_DEPTH,
489
+ Headers.END_MEASURED_DEPTH,
490
+ Headers.COMPSEGS_DIRECTION,
491
+ Headers.ENDGRID,
492
+ Headers.PERFDEPTH,
493
+ Headers.THERM,
494
+ Headers.SEGMENT,
495
+ ],
496
+ )
497
+ # replace string component " or ' in the columns
498
+ compsegs_table = remove_string_characters(compsegs_table)
499
+ return compsegs_table
500
+
501
+
502
+ def get_wsegvalv_table(collections: list[ContentCollection]) -> pd.DataFrame:
503
+ """Return a dataframe of WSEGVALV.
504
+
505
+ Args:
506
+ collections: ContentCollection class.
507
+
508
+ Returns:
509
+ WSEGVALV table.
510
+ """
511
+ columns = ["WELL", "SEGMENT", "CD", "AC", "DEFAULT_1", "DEFAULT_2", "DEFAULT_3", "DEFAULT_4", "STATE", "AC_MAX"]
512
+
513
+ wsegvalv_collections = [np.asarray(collection) for collection in collections if collection.name == "WSEGVALV"]
514
+ wsegvalv_table = np.vstack(wsegvalv_collections)
515
+
516
+ if wsegvalv_table.size == 0:
517
+ return pd.DataFrame(columns=columns)
518
+
519
+ wsegvalv_table = pd.DataFrame(wsegvalv_table, columns=columns)
520
+ wsegvalv_table = wsegvalv_table.astype(
521
+ {
522
+ "WELL": "string",
523
+ "SEGMENT": "int",
524
+ "CD": "float",
525
+ "AC": "float",
526
+ "DEFAULT_1": "string",
527
+ "DEFAULT_2": "string",
528
+ "DEFAULT_3": "string",
529
+ "DEFAULT_4": "string",
530
+ "STATE": "string",
531
+ "AC_MAX": "float",
532
+ }
533
+ )
534
+ return remove_string_characters(wsegvalv_table)
535
+
536
+
537
+ @overload
538
+ def remove_string_characters(df: pd.DataFrame, columns: list[str] | None = ...) -> pd.DataFrame: ...
539
+
540
+
541
+ @overload
542
+ def remove_string_characters(df: str, columns: list[str] | None = ...) -> str: ...
543
+
544
+
545
+ def remove_string_characters(df: pd.DataFrame | str, columns: list[str] | None = None) -> pd.DataFrame | str:
546
+ """Remove string characters `"` and `'`.
547
+
548
+ Args:
549
+ df: DataFrame or string.
550
+ columns: List of column names to be checked.
551
+
552
+ Returns:
553
+ DataFrame without string characters.
554
+
555
+ Raises:
556
+ Exception: If an unexpected error occurred.
557
+ """
558
+ if columns is None:
559
+ columns = []
560
+
561
+ def remove_quotes(item: str):
562
+ return item.replace("'", "").replace('"', "")
563
+
564
+ if isinstance(df, str):
565
+ df = remove_quotes(df)
566
+ elif isinstance(df, pd.DataFrame):
567
+ if len(columns) == 0:
568
+ iterator: range | list[str] = range(df.shape[1])
569
+ else:
570
+ iterator = [] if columns is None else columns
571
+ for column in iterator:
572
+ try:
573
+ df.iloc[:, column] = remove_quotes(df.iloc[:, column].str)
574
+ except ValueError:
575
+ df[column] = remove_quotes(df[column].str)
576
+ except AttributeError:
577
+ # Some dataframes contains numeric data, which we ignore
578
+ pass
579
+ except Exception as err:
580
+ raise err
581
+ return df