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/completion.py +152 -542
- completor/constants.py +223 -150
- completor/create_output.py +559 -431
- completor/exceptions/exceptions.py +6 -6
- completor/get_version.py +8 -0
- completor/hook_implementations/jobs.py +2 -3
- completor/input_validation.py +53 -41
- completor/launch_args_parser.py +7 -12
- completor/logger.py +3 -3
- completor/main.py +102 -360
- completor/parse.py +104 -93
- completor/prepare_outputs.py +593 -457
- completor/read_casefile.py +248 -197
- completor/read_schedule.py +317 -14
- completor/utils.py +256 -25
- completor/visualization.py +1 -14
- completor/visualize_well.py +29 -27
- completor/wells.py +273 -0
- {completor-0.1.2.dist-info → completor-1.0.0.dist-info}/METADATA +10 -11
- completor-1.0.0.dist-info/RECORD +27 -0
- completor/create_wells.py +0 -314
- completor/pvt_model.py +0 -14
- completor-0.1.2.dist-info/RECORD +0 -27
- {completor-0.1.2.dist-info → completor-1.0.0.dist-info}/LICENSE +0 -0
- {completor-0.1.2.dist-info → completor-1.0.0.dist-info}/WHEEL +0 -0
- {completor-0.1.2.dist-info → completor-1.0.0.dist-info}/entry_points.txt +0 -0
completor/read_casefile.py
CHANGED
|
@@ -3,14 +3,14 @@ from __future__ import annotations
|
|
|
3
3
|
import re
|
|
4
4
|
from collections.abc import Mapping
|
|
5
5
|
from io import StringIO
|
|
6
|
+
from typing import Any
|
|
6
7
|
|
|
7
8
|
import numpy as np
|
|
9
|
+
import numpy.typing as npt
|
|
8
10
|
import pandas as pd
|
|
9
11
|
|
|
10
|
-
from completor import input_validation
|
|
11
|
-
from completor import
|
|
12
|
-
from completor.completion import WellSchedule
|
|
13
|
-
from completor.constants import Headers, Keywords
|
|
12
|
+
from completor import input_validation, parse
|
|
13
|
+
from completor.constants import Content, Headers, Keywords, Method, WellData
|
|
14
14
|
from completor.exceptions import CaseReaderFormatError, CompletorError
|
|
15
15
|
from completor.logger import logger
|
|
16
16
|
from completor.utils import clean_file_lines
|
|
@@ -46,8 +46,8 @@ class ReadCasefile:
|
|
|
46
46
|
|
|
47
47
|
This class reads the case/input file of the Completor program.
|
|
48
48
|
It reads the following keywords:
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
SCHEDULE_FILE, OUT_FILE, COMPLETION, SEGMENTLENGTH, JOINTLENGTH AUTONOMOUS_INFLOW_CONTROL_DEVICE, WELL_SEGMENTS_VALVE,
|
|
50
|
+
INFLOW_CONTROL_DEVICE, DENSITY_ACTIVATED_RECOVERY, AUTONOMOUS_INFLOW_CONTROL_VALVE, INFLOW_CONTROL_VALVE, PVTFILE, PVTTABLE.
|
|
51
51
|
In the absence of some keywords, the program uses the default values.
|
|
52
52
|
|
|
53
53
|
Attributes:
|
|
@@ -58,15 +58,15 @@ class ReadCasefile:
|
|
|
58
58
|
pvt_file (str): The pvt file content.
|
|
59
59
|
pvt_file_name (str): The pvt file name.
|
|
60
60
|
completion_table (pd.DataFrame): ....
|
|
61
|
-
wsegaicd_table (pd.DataFrame):
|
|
62
|
-
wsegsicd_table (pd.DataFrame):
|
|
63
|
-
wsegvalv_table (pd.DataFrame):
|
|
64
|
-
wsegicv_table (pd.DataFrame):
|
|
65
|
-
wsegdar_table (pd.DataFrame):
|
|
66
|
-
wsegaicv_table (pd.DataFrame):
|
|
61
|
+
wsegaicd_table (pd.DataFrame): AUTONOMOUS_INFLOW_CONTROL_DEVICE.
|
|
62
|
+
wsegsicd_table (pd.DataFrame): INFLOW_CONTROL_DEVICE.
|
|
63
|
+
wsegvalv_table (pd.DataFrame): WELL_SEGMENTS_VALVE.
|
|
64
|
+
wsegicv_table (pd.DataFrame): INFLOW_CONTROL_VALVE.
|
|
65
|
+
wsegdar_table (pd.DataFrame): DENSITY_ACTIVATED_RECOVERY.
|
|
66
|
+
wsegaicv_table (pd.DataFrame): AUTONOMOUS_INFLOW_CONTROL_VALVE.
|
|
67
67
|
strict (bool): USE_STRICT. If TRUE it will exit if any lateral is not defined in the case-file. Default to TRUE.
|
|
68
68
|
lat2device (pd.DataFrame): LATERAL_TO_DEVICE.
|
|
69
|
-
gp_perf_devicelayer (bool):
|
|
69
|
+
gp_perf_devicelayer (bool): GRAVEL_PACKED_PERFORATED_DEVICELAYER. If TRUE all wells with
|
|
70
70
|
gravel pack and perforation completion are given a device layer.
|
|
71
71
|
If FALSE (default) all wells with this type of completions are untouched by Completor.
|
|
72
72
|
"""
|
|
@@ -86,7 +86,6 @@ class ReadCasefile:
|
|
|
86
86
|
|
|
87
87
|
# assign default values
|
|
88
88
|
self.joint_length = 12.0
|
|
89
|
-
self.segment_length: float | str = 0.0
|
|
90
89
|
self.minimum_segment_length: float = 0.0
|
|
91
90
|
self.strict = True
|
|
92
91
|
self.gp_perf_devicelayer = False
|
|
@@ -108,7 +107,8 @@ class ReadCasefile:
|
|
|
108
107
|
# Run programs
|
|
109
108
|
self.read_completion()
|
|
110
109
|
self.read_joint_length()
|
|
111
|
-
self.read_segment_length()
|
|
110
|
+
self.segment_length = self.read_segment_length()
|
|
111
|
+
self.method = self.segmentation_method(self.segment_length)
|
|
112
112
|
self.read_strictness()
|
|
113
113
|
self.read_gp_perf_devicelayer()
|
|
114
114
|
self.read_mapfile()
|
|
@@ -127,7 +127,7 @@ class ReadCasefile:
|
|
|
127
127
|
Raises:
|
|
128
128
|
ValueError: If COMPLETION keyword is not defined in the case.
|
|
129
129
|
"""
|
|
130
|
-
start_index, end_index =
|
|
130
|
+
start_index, end_index = parse.locate_keyword(self.content, Keywords.COMPLETION)
|
|
131
131
|
if start_index == end_index:
|
|
132
132
|
raise ValueError("No completion is defined in the case file.")
|
|
133
133
|
|
|
@@ -147,15 +147,15 @@ class ReadCasefile:
|
|
|
147
147
|
]
|
|
148
148
|
df_temp = self._create_dataframe_with_columns(header, start_index, end_index)
|
|
149
149
|
# Set default value for packer segment
|
|
150
|
-
df_temp =
|
|
150
|
+
df_temp = input_validation.set_default_packer_section(df_temp)
|
|
151
151
|
# Set default value for PERF segments
|
|
152
|
-
df_temp =
|
|
152
|
+
df_temp = input_validation.set_default_perf_section(df_temp)
|
|
153
153
|
# Give errors if 1* is found for non packer segments
|
|
154
|
-
df_temp =
|
|
154
|
+
df_temp = input_validation.check_default_non_packer(df_temp)
|
|
155
155
|
# Fix the data types format
|
|
156
|
-
df_temp =
|
|
156
|
+
df_temp = input_validation.set_format_completion(df_temp)
|
|
157
157
|
# Check overall user inputs on completion
|
|
158
|
-
|
|
158
|
+
input_validation.assess_completion(df_temp)
|
|
159
159
|
df_temp = self.read_icv_tubing(df_temp)
|
|
160
160
|
self.completion_table = df_temp.copy(deep=True)
|
|
161
161
|
|
|
@@ -170,18 +170,18 @@ class ReadCasefile:
|
|
|
170
170
|
"""
|
|
171
171
|
if not df_temp.loc[
|
|
172
172
|
(df_temp[Headers.START_MEASURED_DEPTH] == df_temp[Headers.END_MEASURED_DEPTH])
|
|
173
|
-
& (df_temp[Headers.DEVICE_TYPE] ==
|
|
173
|
+
& (df_temp[Headers.DEVICE_TYPE] == Content.INFLOW_CONTROL_VALVE)
|
|
174
174
|
].empty:
|
|
175
175
|
# take ICV tubing table
|
|
176
176
|
self.completion_icv_tubing = df_temp.loc[
|
|
177
177
|
(df_temp[Headers.START_MEASURED_DEPTH] == df_temp[Headers.END_MEASURED_DEPTH])
|
|
178
|
-
& (df_temp[Headers.DEVICE_TYPE] ==
|
|
178
|
+
& (df_temp[Headers.DEVICE_TYPE] == Content.INFLOW_CONTROL_VALVE)
|
|
179
179
|
].reset_index(drop=True)
|
|
180
180
|
# drop its line
|
|
181
181
|
df_temp = df_temp.drop(
|
|
182
182
|
df_temp.loc[
|
|
183
183
|
(df_temp[Headers.START_MEASURED_DEPTH] == df_temp[Headers.END_MEASURED_DEPTH])
|
|
184
|
-
& (df_temp[Headers.DEVICE_TYPE] ==
|
|
184
|
+
& (df_temp[Headers.DEVICE_TYPE] == Content.INFLOW_CONTROL_VALVE)
|
|
185
185
|
].index[:]
|
|
186
186
|
).reset_index(drop=True)
|
|
187
187
|
return df_temp
|
|
@@ -203,19 +203,19 @@ class ReadCasefile:
|
|
|
203
203
|
/
|
|
204
204
|
"""
|
|
205
205
|
header = [Headers.WELL, Headers.BRANCH]
|
|
206
|
-
start_index, end_index =
|
|
206
|
+
start_index, end_index = parse.locate_keyword(self.content, Keywords.LATERAL_TO_DEVICE)
|
|
207
207
|
|
|
208
208
|
if start_index == end_index:
|
|
209
209
|
# set default behaviour (if keyword not in case file)
|
|
210
210
|
self.lat2device = pd.DataFrame([], columns=header) # empty df
|
|
211
211
|
return
|
|
212
212
|
self.lat2device = self._create_dataframe_with_columns(header, start_index, end_index)
|
|
213
|
-
|
|
213
|
+
input_validation.validate_lateral_to_device(self.lat2device, self.completion_table)
|
|
214
214
|
self.lat2device[Headers.BRANCH] = self.lat2device[Headers.BRANCH].astype(np.int64)
|
|
215
215
|
|
|
216
216
|
def read_joint_length(self) -> None:
|
|
217
217
|
"""Read the JOINTLENGTH keyword in the case file."""
|
|
218
|
-
start_index, end_index =
|
|
218
|
+
start_index, end_index = parse.locate_keyword(self.content, Keywords.JOINT_LENGTH)
|
|
219
219
|
if end_index == start_index + 2:
|
|
220
220
|
self.joint_length = float(self.content[start_index + 1])
|
|
221
221
|
if self.joint_length <= 0:
|
|
@@ -224,52 +224,75 @@ class ReadCasefile:
|
|
|
224
224
|
else:
|
|
225
225
|
logger.info("No joint length is defined. It is set to default 12.0 m")
|
|
226
226
|
|
|
227
|
-
def read_segment_length(self) ->
|
|
227
|
+
def read_segment_length(self) -> float | str:
|
|
228
228
|
"""Read the SEGMENTLENGTH keyword in the case file.
|
|
229
229
|
|
|
230
230
|
Raises:
|
|
231
231
|
CompletorError: If SEGMENTLENGTH is not float or string.
|
|
232
232
|
"""
|
|
233
|
-
start_index, end_index =
|
|
233
|
+
start_index, end_index = parse.locate_keyword(self.content, Keywords.SEGMENT_LENGTH)
|
|
234
234
|
if end_index == start_index + 2:
|
|
235
235
|
try:
|
|
236
|
-
|
|
237
|
-
# 'Fix' method if value is positive.
|
|
238
|
-
if self.segment_length > 0.0:
|
|
239
|
-
logger.info("Segments are defined per %s meters.", self.segment_length)
|
|
240
|
-
# 'User' method if value is negative.
|
|
241
|
-
elif self.segment_length < 0.0:
|
|
242
|
-
logger.info(
|
|
243
|
-
"Segments are defined based on the COMPLETION keyword. "
|
|
244
|
-
"Attempting to pick segments' measured depth from .case file."
|
|
245
|
-
)
|
|
246
|
-
# 'Cells' method if value is zero.
|
|
247
|
-
elif self.segment_length == 0:
|
|
248
|
-
logger.info("Segments are defined based on the grid dimensions.")
|
|
249
|
-
|
|
236
|
+
return float(self.content[start_index + 1])
|
|
250
237
|
except ValueError:
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
# 'Welsegs' method
|
|
254
|
-
if "welsegs" in self.segment_length.lower() or "infill" in self.segment_length.lower():
|
|
255
|
-
logger.info(
|
|
256
|
-
"Segments are defined based on the WELSEGS keyword. "
|
|
257
|
-
"Retaining the original tubing segment structure."
|
|
258
|
-
)
|
|
259
|
-
# 'User' method if value is negative
|
|
260
|
-
elif "user" in self.segment_length.lower():
|
|
261
|
-
logger.info(
|
|
262
|
-
"Segments are defined based on the COMPLETION keyword. "
|
|
263
|
-
"Attempting to pick segments' measured depth from casefile."
|
|
264
|
-
)
|
|
265
|
-
# 'Cells' method
|
|
266
|
-
elif "cell" in self.segment_length.lower():
|
|
267
|
-
logger.info("Segment lengths are created based on the grid dimensions.")
|
|
268
|
-
except ValueError as err:
|
|
269
|
-
raise CompletorError("SEGMENTLENGTH takes number or string") from err
|
|
238
|
+
return self.content[start_index + 1]
|
|
239
|
+
|
|
270
240
|
else:
|
|
271
|
-
|
|
272
|
-
|
|
241
|
+
logger.info(
|
|
242
|
+
"SEGMENTLENGTH keyword undefined, using default strategy 'cells' "
|
|
243
|
+
"to create segments based on the grid dimensions."
|
|
244
|
+
)
|
|
245
|
+
return 0.0
|
|
246
|
+
|
|
247
|
+
@staticmethod
|
|
248
|
+
def segmentation_method(segment_length: float | str) -> Method:
|
|
249
|
+
"""Determine the method of segmentation, and log the implication to info.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
segment_length: The string or number value from the SEGMENTLENGTH keyword.
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
The method used to create the segments.
|
|
256
|
+
|
|
257
|
+
Raises:
|
|
258
|
+
ValueError: If value of segment_length is invalid.
|
|
259
|
+
"""
|
|
260
|
+
if isinstance(segment_length, float):
|
|
261
|
+
if segment_length > 0.0:
|
|
262
|
+
logger.info("Segments are defined per fixed %s meters.", segment_length)
|
|
263
|
+
return Method.FIX
|
|
264
|
+
if segment_length == 0.0:
|
|
265
|
+
logger.info("Segments are defined based on the grid dimensions.")
|
|
266
|
+
return Method.CELLS
|
|
267
|
+
if segment_length < 0.0:
|
|
268
|
+
logger.info(
|
|
269
|
+
"Segments are defined based on the COMPLETION keyword. "
|
|
270
|
+
"Attempting to pick segments' measured depth from case file."
|
|
271
|
+
)
|
|
272
|
+
return Method.USER
|
|
273
|
+
|
|
274
|
+
if isinstance(segment_length, str):
|
|
275
|
+
if "welsegs" in segment_length.lower() or "infill" in segment_length.lower():
|
|
276
|
+
logger.info(
|
|
277
|
+
"Segments are defined based on the WELL_SEGMENTS%s keyword. "
|
|
278
|
+
"Retaining the original tubing segment structure.",
|
|
279
|
+
Keywords.WELL_SEGMENTS,
|
|
280
|
+
)
|
|
281
|
+
return Method.WELSEGS
|
|
282
|
+
if "cell" in segment_length.lower():
|
|
283
|
+
logger.info("Segment lengths are created based on the grid dimensions.")
|
|
284
|
+
return Method.CELLS
|
|
285
|
+
if "user" in segment_length.lower():
|
|
286
|
+
logger.info(
|
|
287
|
+
"Segments are defined based on the COMPLETION keyword. "
|
|
288
|
+
"Attempting to pick segments' measured depth from casefile."
|
|
289
|
+
)
|
|
290
|
+
return Method.USER
|
|
291
|
+
raise CompletorError(
|
|
292
|
+
f"Unrecognized method for SEGMENTLENGTH keyword '{segment_length}'. The value should be one of: "
|
|
293
|
+
f"'{Keywords.WELL_SEGMENTS}', 'CELLS', 'USER'. "
|
|
294
|
+
"Alternatively a negative number for 'USER', zero for 'CELLS', or positive number for 'FIX'.",
|
|
295
|
+
)
|
|
273
296
|
|
|
274
297
|
def read_strictness(self) -> None:
|
|
275
298
|
"""Read the USE_STRICT keyword in the case file.
|
|
@@ -282,7 +305,7 @@ class ReadCasefile:
|
|
|
282
305
|
|
|
283
306
|
Best practice: All branches in all wells should be defined in the case file.
|
|
284
307
|
"""
|
|
285
|
-
start_index, end_index =
|
|
308
|
+
start_index, end_index = parse.locate_keyword(self.content, Keywords.USE_STRICT)
|
|
286
309
|
if end_index == start_index + 2:
|
|
287
310
|
strict = self.content[start_index + 1]
|
|
288
311
|
if strict.upper() == "FALSE":
|
|
@@ -290,14 +313,14 @@ class ReadCasefile:
|
|
|
290
313
|
logger.info("case-strictness is set to %d", self.strict)
|
|
291
314
|
|
|
292
315
|
def read_gp_perf_devicelayer(self) -> None:
|
|
293
|
-
"""Read the
|
|
316
|
+
"""Read the GRAVEL_PACKED_PERFORATED_DEVICELAYER keyword in the case file.
|
|
294
317
|
|
|
295
|
-
If
|
|
296
|
-
wells with GP PERF type completions. If
|
|
318
|
+
If GRAVEL_PACKED_PERFORATED_DEVICELAYER = True the program assigns a device layer to
|
|
319
|
+
wells with GP PERF type completions. If GRAVEL_PACKED_PERFORATED_DEVICELAYER = False, the
|
|
297
320
|
program does not add a device layer to the well. I.e. the well is
|
|
298
321
|
untouched by the program. The default value is False.
|
|
299
322
|
"""
|
|
300
|
-
start_index, end_index =
|
|
323
|
+
start_index, end_index = parse.locate_keyword(self.content, Keywords.GRAVEL_PACKED_PERFORATED_DEVICELAYER)
|
|
301
324
|
if end_index == start_index + 2:
|
|
302
325
|
gp_perf_devicelayer = self.content[start_index + 1]
|
|
303
326
|
self.gp_perf_devicelayer = gp_perf_devicelayer.upper() == "TRUE"
|
|
@@ -309,97 +332,108 @@ class ReadCasefile:
|
|
|
309
332
|
The default value is 0.0, meaning that no segments are lumped by this keyword.
|
|
310
333
|
The program will continue to coalesce segments until all segments are longer than the given minimum.
|
|
311
334
|
"""
|
|
312
|
-
start_index, end_index =
|
|
335
|
+
start_index, end_index = parse.locate_keyword(self.content, Keywords.MINIMUM_SEGMENT_LENGTH)
|
|
313
336
|
if end_index == start_index + 2:
|
|
314
337
|
min_seg_len = self.content[start_index + 1]
|
|
315
|
-
self.minimum_segment_length =
|
|
338
|
+
self.minimum_segment_length = input_validation.validate_minimum_segment_length(min_seg_len)
|
|
316
339
|
logger.info("minimum_segment_length is set to %s", self.minimum_segment_length)
|
|
317
340
|
|
|
318
341
|
def read_mapfile(self) -> None:
|
|
319
|
-
"""Read the
|
|
320
|
-
start_index, end_index =
|
|
342
|
+
"""Read the MAP_FILE keyword in the case file (if any) into a mapper."""
|
|
343
|
+
start_index, end_index = parse.locate_keyword(self.content, Keywords.MAP_FILE)
|
|
321
344
|
if end_index == start_index + 2:
|
|
322
345
|
# the content is in between the keyword and the /
|
|
323
346
|
self.mapfile = parse.remove_string_characters(self.content[start_index + 1])
|
|
324
347
|
self.mapper = _mapper(self.mapfile)
|
|
325
|
-
else:
|
|
326
|
-
self.mapfile = None
|
|
327
|
-
self.mapper = None
|
|
328
348
|
|
|
329
349
|
def read_wsegvalv(self) -> None:
|
|
330
|
-
"""Read the
|
|
350
|
+
"""Read the WELL_SEGMENTS_VALVE keyword in the case file.
|
|
331
351
|
|
|
332
352
|
Raises:
|
|
333
353
|
CompletorError: If WESEGVALV is not defined and VALVE is used in COMPLETION. If the device number is not found.
|
|
334
354
|
"""
|
|
335
|
-
start_index, end_index =
|
|
355
|
+
start_index, end_index = parse.locate_keyword(self.content, Keywords.WELL_SEGMENTS_VALVE)
|
|
336
356
|
if start_index == end_index:
|
|
337
|
-
if
|
|
338
|
-
raise CompletorError("
|
|
357
|
+
if Content.VALVE in self.completion_table[Headers.DEVICE_TYPE]:
|
|
358
|
+
raise CompletorError("WELL_SEGMENTS_VALVE keyword must be defined, if VALVE is used in the completion.")
|
|
339
359
|
else:
|
|
340
360
|
# Table headers
|
|
341
|
-
header = [
|
|
361
|
+
header = [
|
|
362
|
+
Headers.DEVICE_NUMBER,
|
|
363
|
+
Headers.FLOW_COEFFICIENT,
|
|
364
|
+
Headers.FLOW_CROSS_SECTIONAL_AREA,
|
|
365
|
+
Headers.ADDITIONAL_PIPE_LENGTH_FRICTION_PRESSURE_DROP,
|
|
366
|
+
]
|
|
342
367
|
try:
|
|
343
368
|
df_temp = self._create_dataframe_with_columns(header, start_index, end_index)
|
|
344
|
-
df_temp[Headers.
|
|
369
|
+
df_temp[Headers.MAX_FLOW_CROSS_SECTIONAL_AREA] = np.nan
|
|
345
370
|
except CaseReaderFormatError:
|
|
346
|
-
header += [Headers.
|
|
371
|
+
header += [Headers.MAX_FLOW_CROSS_SECTIONAL_AREA]
|
|
347
372
|
df_temp = self._create_dataframe_with_columns(header, start_index, end_index)
|
|
348
373
|
|
|
349
|
-
self.wsegvalv_table =
|
|
350
|
-
device_checks = self.completion_table[self.completion_table[Headers.DEVICE_TYPE] ==
|
|
374
|
+
self.wsegvalv_table = input_validation.set_format_wsegvalv(df_temp)
|
|
375
|
+
device_checks = self.completion_table[self.completion_table[Headers.DEVICE_TYPE] == Content.VALVE][
|
|
351
376
|
Headers.DEVICE_NUMBER
|
|
352
377
|
].to_numpy()
|
|
353
378
|
if not check_contents(device_checks, self.wsegvalv_table[Headers.DEVICE_NUMBER].to_numpy()):
|
|
354
|
-
raise CompletorError(
|
|
379
|
+
raise CompletorError(
|
|
380
|
+
f"Not all device in {Keywords.COMPLETION} is specified in {Keywords.WELL_SEGMENTS_VALVE}"
|
|
381
|
+
)
|
|
355
382
|
|
|
356
383
|
def read_wsegsicd(self) -> None:
|
|
357
|
-
"""Read the
|
|
384
|
+
"""Read the INFLOW_CONTROL_DEVICE keyword in the case file.
|
|
358
385
|
|
|
359
386
|
Raises:
|
|
360
|
-
CompletorError: If
|
|
361
|
-
|
|
387
|
+
CompletorError: If INFLOW_CONTROL_DEVICE is not defined and ICD is used in COMPLETION,
|
|
388
|
+
or if the device number is not found.
|
|
389
|
+
If not all devices in COMPLETION are specified in INFLOW_CONTROL_DEVICE.
|
|
362
390
|
"""
|
|
363
|
-
start_index, end_index =
|
|
391
|
+
start_index, end_index = parse.locate_keyword(self.content, Keywords.INFLOW_CONTROL_DEVICE)
|
|
364
392
|
if start_index == end_index:
|
|
365
|
-
if
|
|
366
|
-
raise CompletorError(
|
|
393
|
+
if Content.INFLOW_CONTROL_DEVICE in self.completion_table[Headers.DEVICE_TYPE]:
|
|
394
|
+
raise CompletorError(
|
|
395
|
+
f"{Keywords.INFLOW_CONTROL_DEVICE} keyword must be defined, if ICD is used in the completion."
|
|
396
|
+
)
|
|
367
397
|
else:
|
|
368
398
|
# Table headers
|
|
369
399
|
header = [
|
|
370
400
|
Headers.DEVICE_NUMBER,
|
|
371
401
|
Headers.STRENGTH,
|
|
372
|
-
Headers.
|
|
373
|
-
Headers.
|
|
402
|
+
Headers.CALIBRATION_FLUID_DENSITY,
|
|
403
|
+
Headers.CALIBRATION_FLUID_VISCOSITY,
|
|
374
404
|
Headers.WATER_CUT,
|
|
375
405
|
]
|
|
376
|
-
self.wsegsicd_table =
|
|
406
|
+
self.wsegsicd_table = input_validation.set_format_wsegsicd(
|
|
377
407
|
self._create_dataframe_with_columns(header, start_index, end_index)
|
|
378
408
|
)
|
|
379
|
-
# Check if the device in COMPLETION is exist in
|
|
380
|
-
device_checks = self.completion_table[
|
|
381
|
-
Headers.
|
|
382
|
-
].to_numpy()
|
|
409
|
+
# Check if the device in COMPLETION is exist in INFLOW_CONTROL_DEVICE
|
|
410
|
+
device_checks = self.completion_table[
|
|
411
|
+
self.completion_table[Headers.DEVICE_TYPE] == Content.INFLOW_CONTROL_DEVICE
|
|
412
|
+
][Headers.DEVICE_NUMBER].to_numpy()
|
|
383
413
|
if not check_contents(device_checks, self.wsegsicd_table[Headers.DEVICE_NUMBER].to_numpy()):
|
|
384
|
-
raise CompletorError("Not all device in COMPLETION is specified in
|
|
414
|
+
raise CompletorError(f"Not all device in COMPLETION is specified in {Keywords.INFLOW_CONTROL_DEVICE}")
|
|
385
415
|
|
|
386
416
|
def read_wsegaicd(self) -> None:
|
|
387
|
-
"""Read the
|
|
417
|
+
"""Read the AUTONOMOUS_INFLOW_CONTROL_DEVICE keyword in the case file.
|
|
388
418
|
|
|
389
419
|
Raises:
|
|
390
|
-
ValueError: If invalid entries in
|
|
391
|
-
CompletorError: If
|
|
392
|
-
|
|
420
|
+
ValueError: If invalid entries in AUTONOMOUS_INFLOW_CONTROL_DEVICE.
|
|
421
|
+
CompletorError: If AUTONOMOUS_INFLOW_CONTROL_DEVICE is not defined, and AICD is used in COMPLETION,
|
|
422
|
+
or if the device number is not found.
|
|
423
|
+
If all devices in COMPLETION are not specified in AUTONOMOUS_INFLOW_CONTROL_DEVICE.
|
|
393
424
|
"""
|
|
394
|
-
start_index, end_index =
|
|
425
|
+
start_index, end_index = parse.locate_keyword(self.content, Keywords.AUTONOMOUS_INFLOW_CONTROL_DEVICE)
|
|
395
426
|
if start_index == end_index:
|
|
396
|
-
if
|
|
397
|
-
raise CompletorError(
|
|
427
|
+
if Content.AUTONOMOUS_INFLOW_CONTROL_DEVICE in self.completion_table[Headers.DEVICE_TYPE]:
|
|
428
|
+
raise CompletorError(
|
|
429
|
+
f"{Keywords.AUTONOMOUS_INFLOW_CONTROL_DEVICE} keyword must be defined, "
|
|
430
|
+
"if AICD is used in the completion."
|
|
431
|
+
)
|
|
398
432
|
else:
|
|
399
433
|
# Table headers
|
|
400
434
|
header = [
|
|
401
435
|
Headers.DEVICE_NUMBER,
|
|
402
|
-
Headers.
|
|
436
|
+
Headers.STRENGTH,
|
|
403
437
|
Headers.X,
|
|
404
438
|
Headers.Y,
|
|
405
439
|
Headers.A,
|
|
@@ -408,75 +442,84 @@ class ReadCasefile:
|
|
|
408
442
|
Headers.D,
|
|
409
443
|
Headers.E,
|
|
410
444
|
Headers.F,
|
|
411
|
-
Headers.
|
|
412
|
-
Headers.
|
|
445
|
+
Headers.AICD_CALIBRATION_FLUID_DENSITY,
|
|
446
|
+
Headers.AICD_FLUID_VISCOSITY,
|
|
413
447
|
]
|
|
414
|
-
self.wsegaicd_table =
|
|
448
|
+
self.wsegaicd_table = input_validation.set_format_wsegaicd(
|
|
415
449
|
self._create_dataframe_with_columns(header, start_index, end_index)
|
|
416
450
|
)
|
|
417
|
-
device_checks = self.completion_table[
|
|
418
|
-
Headers.
|
|
419
|
-
].to_numpy()
|
|
451
|
+
device_checks = self.completion_table[
|
|
452
|
+
self.completion_table[Headers.DEVICE_TYPE] == Content.AUTONOMOUS_INFLOW_CONTROL_DEVICE
|
|
453
|
+
][Headers.DEVICE_NUMBER].to_numpy()
|
|
420
454
|
if not check_contents(device_checks, self.wsegaicd_table[Headers.DEVICE_NUMBER].to_numpy()):
|
|
421
|
-
raise CompletorError(
|
|
455
|
+
raise CompletorError(
|
|
456
|
+
f"Not all device in COMPLETION is specified in {Keywords.AUTONOMOUS_INFLOW_CONTROL_DEVICE}"
|
|
457
|
+
)
|
|
422
458
|
|
|
423
459
|
def read_wsegdar(self) -> None:
|
|
424
|
-
"""Read the
|
|
460
|
+
"""Read the DENSITY_ACTIVATED_RECOVERY keyword in the case file.
|
|
425
461
|
|
|
426
462
|
Raises:
|
|
427
|
-
ValueError: If there are invalid entries in
|
|
428
|
-
CompletorError: If not all device in COMPLETION is specified in
|
|
429
|
-
|
|
463
|
+
ValueError: If there are invalid entries in DENSITY_ACTIVATED_RECOVERY.
|
|
464
|
+
CompletorError: If not all device in COMPLETION is specified in DENSITY_ACTIVATED_RECOVERY.
|
|
465
|
+
If DENSITY_ACTIVATED_RECOVERY keyword not defined, when DAR is used in the completion.
|
|
430
466
|
"""
|
|
431
|
-
start_index, end_index =
|
|
467
|
+
start_index, end_index = parse.locate_keyword(self.content, Keywords.DENSITY_ACTIVATED_RECOVERY)
|
|
432
468
|
if start_index == end_index:
|
|
433
|
-
if
|
|
434
|
-
raise CompletorError(
|
|
469
|
+
if Content.DENSITY_ACTIVATED_RECOVERY in self.completion_table[Headers.DEVICE_TYPE]:
|
|
470
|
+
raise CompletorError(
|
|
471
|
+
f"{Keywords.DENSITY_ACTIVATED_RECOVERY} keyword must be defined, if DAR is used in the completion"
|
|
472
|
+
)
|
|
435
473
|
else:
|
|
436
474
|
# Table headers
|
|
437
475
|
header = [
|
|
438
476
|
Headers.DEVICE_NUMBER,
|
|
439
|
-
Headers.
|
|
440
|
-
Headers.
|
|
441
|
-
Headers.
|
|
442
|
-
Headers.
|
|
443
|
-
Headers.
|
|
444
|
-
Headers.
|
|
445
|
-
Headers.
|
|
446
|
-
Headers.
|
|
477
|
+
Headers.FLOW_COEFFICIENT,
|
|
478
|
+
Headers.OIL_FLOW_CROSS_SECTIONAL_AREA,
|
|
479
|
+
Headers.GAS_FLOW_CROSS_SECTIONAL_AREA,
|
|
480
|
+
Headers.WATER_FLOW_CROSS_SECTIONAL_AREA,
|
|
481
|
+
Headers.WATER_HOLDUP_FRACTION_LOW_CUTOFF,
|
|
482
|
+
Headers.WATER_HOLDUP_FRACTION_HIGH_CUTOFF,
|
|
483
|
+
Headers.GAS_HOLDUP_FRACTION_LOW_CUTOFF,
|
|
484
|
+
Headers.GAS_HOLDUP_FRACTION_HIGH_CUTOFF,
|
|
447
485
|
]
|
|
448
486
|
|
|
449
487
|
# Fix table format
|
|
450
|
-
if self.completion_table[Headers.DEVICE_TYPE].str.contains(
|
|
451
|
-
self.wsegdar_table =
|
|
488
|
+
if self.completion_table[Headers.DEVICE_TYPE].str.contains(Content.DENSITY_ACTIVATED_RECOVERY).any():
|
|
489
|
+
self.wsegdar_table = input_validation.set_format_wsegdar(
|
|
452
490
|
self._create_dataframe_with_columns(header, start_index, end_index)
|
|
453
491
|
)
|
|
454
|
-
device_checks = self.completion_table[
|
|
455
|
-
Headers.
|
|
456
|
-
].to_numpy()
|
|
492
|
+
device_checks = self.completion_table[
|
|
493
|
+
self.completion_table[Headers.DEVICE_TYPE] == Content.DENSITY_ACTIVATED_RECOVERY
|
|
494
|
+
][Headers.DEVICE_NUMBER].to_numpy()
|
|
457
495
|
if not check_contents(device_checks, self.wsegdar_table[Headers.DEVICE_NUMBER].to_numpy()):
|
|
458
|
-
raise CompletorError(
|
|
496
|
+
raise CompletorError(
|
|
497
|
+
f"Not all device in COMPLETION is specified in {Keywords.DENSITY_ACTIVATED_RECOVERY}"
|
|
498
|
+
)
|
|
459
499
|
|
|
460
500
|
def read_wsegaicv(self) -> None:
|
|
461
|
-
"""Read the
|
|
501
|
+
"""Read the AUTONOMOUS_INFLOW_CONTROL_VALVE keyword in the case file.
|
|
462
502
|
|
|
463
503
|
Raises:
|
|
464
|
-
ValueError: If invalid entries in
|
|
465
|
-
CompletorError:
|
|
466
|
-
If all devices in COMPLETION are not specified in
|
|
504
|
+
ValueError: If invalid entries in AUTONOMOUS_INFLOW_CONTROL_VALVE.
|
|
505
|
+
CompletorError: AUTONOMOUS_INFLOW_CONTROL_VALVE keyword not defined when AICV is used in completion.
|
|
506
|
+
If all devices in COMPLETION are not specified in AUTONOMOUS_INFLOW_CONTROL_VALVE.
|
|
467
507
|
"""
|
|
468
|
-
start_index, end_index =
|
|
508
|
+
start_index, end_index = parse.locate_keyword(self.content, Keywords.AUTONOMOUS_INFLOW_CONTROL_VALVE)
|
|
469
509
|
if start_index == end_index:
|
|
470
|
-
if
|
|
471
|
-
raise CompletorError(
|
|
510
|
+
if Content.AUTONOMOUS_INFLOW_CONTROL_VALVE in self.completion_table[Headers.DEVICE_TYPE]:
|
|
511
|
+
raise CompletorError(
|
|
512
|
+
f"{Keywords.AUTONOMOUS_INFLOW_CONTROL_VALVE} keyword must be defined, "
|
|
513
|
+
"if AICV is used in the completion."
|
|
514
|
+
)
|
|
472
515
|
else:
|
|
473
516
|
# Table headers
|
|
474
517
|
header = [
|
|
475
518
|
Headers.DEVICE_NUMBER,
|
|
476
|
-
Headers.
|
|
477
|
-
Headers.
|
|
478
|
-
Headers.
|
|
479
|
-
Headers.
|
|
519
|
+
Headers.AICV_WATER_CUT,
|
|
520
|
+
Headers.AICV_GAS_HOLDUP_FRACTION,
|
|
521
|
+
Headers.AICV_CALIBRATION_FLUID_DENSITY,
|
|
522
|
+
Headers.AICV_FLUID_VISCOSITY,
|
|
480
523
|
Headers.ALPHA_MAIN,
|
|
481
524
|
Headers.X_MAIN,
|
|
482
525
|
Headers.Y_MAIN,
|
|
@@ -497,45 +540,47 @@ class ReadCasefile:
|
|
|
497
540
|
Headers.F_PILOT,
|
|
498
541
|
]
|
|
499
542
|
# Fix table format
|
|
500
|
-
self.wsegaicv_table =
|
|
543
|
+
self.wsegaicv_table = input_validation.set_format_wsegaicv(
|
|
501
544
|
self._create_dataframe_with_columns(header, start_index, end_index)
|
|
502
545
|
)
|
|
503
|
-
# Check if the device in COMPLETION is exist in
|
|
504
|
-
device_checks = self.completion_table[
|
|
505
|
-
Headers.
|
|
506
|
-
].to_numpy()
|
|
546
|
+
# Check if the device in COMPLETION is exist in AUTONOMOUS_INFLOW_CONTROL_VALVE
|
|
547
|
+
device_checks = self.completion_table[
|
|
548
|
+
self.completion_table[Headers.DEVICE_TYPE] == Content.AUTONOMOUS_INFLOW_CONTROL_VALVE
|
|
549
|
+
][Headers.DEVICE_NUMBER].to_numpy()
|
|
507
550
|
if not check_contents(device_checks, self.wsegaicv_table[Headers.DEVICE_NUMBER].to_numpy()):
|
|
508
|
-
raise CompletorError(
|
|
551
|
+
raise CompletorError(
|
|
552
|
+
f"Not all devices in COMPLETION are specified in {Keywords.AUTONOMOUS_INFLOW_CONTROL_VALVE}"
|
|
553
|
+
)
|
|
509
554
|
|
|
510
555
|
def read_wsegicv(self) -> None:
|
|
511
|
-
"""Read
|
|
556
|
+
"""Read INFLOW_CONTROL_VALVE keyword in the case file.
|
|
512
557
|
|
|
513
558
|
Raises:
|
|
514
|
-
ValueError: If invalid entries in
|
|
515
|
-
CompletorError:
|
|
559
|
+
ValueError: If invalid entries in INFLOW_CONTROL_VALVE.
|
|
560
|
+
CompletorError: INFLOW_CONTROL_VALVE keyword not defined when ICV is used in completion.
|
|
516
561
|
"""
|
|
517
562
|
|
|
518
|
-
start_index, end_index =
|
|
563
|
+
start_index, end_index = parse.locate_keyword(self.content, Keywords.INFLOW_CONTROL_VALVE)
|
|
519
564
|
if start_index == end_index:
|
|
520
|
-
if
|
|
521
|
-
raise CompletorError("
|
|
565
|
+
if Content.INFLOW_CONTROL_VALVE in self.completion_table[Headers.DEVICE_TYPE]:
|
|
566
|
+
raise CompletorError("INFLOW_CONTROL_VALVE keyword must be defined, if ICV is used in the completion")
|
|
522
567
|
else:
|
|
523
568
|
# Table headers
|
|
524
|
-
header = [Headers.DEVICE_NUMBER, Headers.
|
|
569
|
+
header = [Headers.DEVICE_NUMBER, Headers.FLOW_COEFFICIENT, Headers.FLOW_CROSS_SECTIONAL_AREA]
|
|
525
570
|
try:
|
|
526
571
|
df_temp = self._create_dataframe_with_columns(header, start_index, end_index)
|
|
527
|
-
df_temp[Headers.
|
|
572
|
+
df_temp[Headers.MAX_FLOW_CROSS_SECTIONAL_AREA] = np.nan
|
|
528
573
|
except CaseReaderFormatError:
|
|
529
|
-
header += [Headers.
|
|
574
|
+
header += [Headers.MAX_FLOW_CROSS_SECTIONAL_AREA]
|
|
530
575
|
df_temp = self._create_dataframe_with_columns(header, start_index, end_index)
|
|
531
576
|
# Fix format
|
|
532
|
-
self.wsegicv_table =
|
|
533
|
-
# Check if the device in COMPLETION exists in
|
|
534
|
-
device_checks = self.completion_table[
|
|
535
|
-
Headers.
|
|
536
|
-
].to_numpy()
|
|
577
|
+
self.wsegicv_table = input_validation.set_format_wsegicv(df_temp)
|
|
578
|
+
# Check if the device in COMPLETION exists in INFLOW_CONTROL_VALVE
|
|
579
|
+
device_checks = self.completion_table[
|
|
580
|
+
self.completion_table[Headers.DEVICE_TYPE] == Content.INFLOW_CONTROL_VALVE
|
|
581
|
+
][Headers.DEVICE_NUMBER].to_numpy()
|
|
537
582
|
if not check_contents(device_checks, self.wsegicv_table[Headers.DEVICE_NUMBER].to_numpy()):
|
|
538
|
-
raise CompletorError("Not all device in COMPLETION is specified in
|
|
583
|
+
raise CompletorError("Not all device in COMPLETION is specified in INFLOW_CONTROL_VALVE")
|
|
539
584
|
|
|
540
585
|
def get_completion(self, well_name: str | None, branch: int) -> pd.DataFrame:
|
|
541
586
|
"""Create the COMPLETION table for the selected well and branch.
|
|
@@ -551,7 +596,7 @@ class ReadCasefile:
|
|
|
551
596
|
df_temp = df_temp[df_temp[Headers.BRANCH] == branch]
|
|
552
597
|
return df_temp
|
|
553
598
|
|
|
554
|
-
def check_input(self, well_name: str,
|
|
599
|
+
def check_input(self, well_name: str, well_data: WellData) -> None:
|
|
555
600
|
"""Ensure that the completion table (given in the case-file) is complete.
|
|
556
601
|
|
|
557
602
|
If one branch is completed, all branches must be completed, unless not 'strict'.
|
|
@@ -561,35 +606,45 @@ class ReadCasefile:
|
|
|
561
606
|
|
|
562
607
|
Args:
|
|
563
608
|
well_name: Well name.
|
|
564
|
-
|
|
609
|
+
schedule_data: Schedule file data.
|
|
565
610
|
|
|
566
611
|
Returns:
|
|
567
612
|
COMPLETION for that well and branch.
|
|
613
|
+
|
|
614
|
+
Raises:
|
|
615
|
+
CompletorError: If strict is true and there are undefined branches.
|
|
568
616
|
"""
|
|
569
|
-
|
|
570
|
-
|
|
617
|
+
if sorted(list(well_data.keys())) != Keywords.main_keywords:
|
|
618
|
+
found_keys = set(well_data.keys())
|
|
619
|
+
raise CompletorError(
|
|
620
|
+
f"Well '{well_name}' is missing keyword(s): '{', '.join(set(Keywords.main_keywords) - found_keys)}'!"
|
|
621
|
+
)
|
|
622
|
+
df_completion = self.completion_table[self.completion_table.WELL == well_name]
|
|
623
|
+
# Check that all branches are defined in the case-file.
|
|
571
624
|
|
|
572
|
-
#
|
|
573
|
-
branch_nos = set(
|
|
625
|
+
# TODO(#173): Use TypedDict for this, and remove the type: ignore.
|
|
626
|
+
branch_nos = set(well_data[Keywords.COMPLETION_SEGMENTS][Headers.BRANCH]).difference( # type: ignore
|
|
627
|
+
set(df_completion[Headers.BRANCH])
|
|
628
|
+
)
|
|
574
629
|
if len(branch_nos):
|
|
575
630
|
logger.warning("Well %s has branch(es) not defined in case-file", well_name)
|
|
576
631
|
if self.strict:
|
|
577
632
|
raise CompletorError("USE_STRICT True: Define all branches in case file.")
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
633
|
+
|
|
634
|
+
for branch_no in branch_nos:
|
|
635
|
+
logger.warning("Adding branch %s for Well %s", branch_no, well_name)
|
|
636
|
+
# copy first entry
|
|
637
|
+
lateral = pd.DataFrame(
|
|
638
|
+
[self.completion_table.loc[self.completion_table.WELL == well_name].iloc[0]],
|
|
639
|
+
columns=self.completion_table.columns,
|
|
640
|
+
)
|
|
641
|
+
lateral[Headers.START_MEASURED_DEPTH] = 0
|
|
642
|
+
lateral[Headers.END_MEASURED_DEPTH] = 999999
|
|
643
|
+
lateral[Headers.DEVICE_TYPE] = Content.PERFORATED
|
|
644
|
+
lateral[Headers.ANNULUS] = Content.GRAVEL_PACKED
|
|
645
|
+
lateral[Headers.BRANCH] = branch_no
|
|
646
|
+
# add new entry
|
|
647
|
+
self.completion_table = pd.concat([self.completion_table, lateral])
|
|
593
648
|
|
|
594
649
|
def connect_to_tubing(self, well_name: str, lateral: int) -> bool:
|
|
595
650
|
"""Connect a branch to the tubing- or device-layer.
|
|
@@ -607,9 +662,6 @@ class ReadCasefile:
|
|
|
607
662
|
return False
|
|
608
663
|
return True
|
|
609
664
|
|
|
610
|
-
def locate_keyword(self, keyword: str) -> tuple[int, int]:
|
|
611
|
-
return parse.locate_keyword(self.content, keyword)
|
|
612
|
-
|
|
613
665
|
def _create_dataframe_with_columns(
|
|
614
666
|
self, header: list[str], start_index: int, end_index: int, keyword: str | None = None
|
|
615
667
|
) -> pd.DataFrame:
|
|
@@ -629,7 +681,6 @@ class ReadCasefile:
|
|
|
629
681
|
if keyword is None:
|
|
630
682
|
keyword = self.content[start_index]
|
|
631
683
|
table_header = " ".join(header)
|
|
632
|
-
table_content = ""
|
|
633
684
|
# Handle weirdly formed keywords.
|
|
634
685
|
if start_index + 1 == end_index or self.content[start_index + 1].endswith("/"):
|
|
635
686
|
content_str = "\n".join(self.content[start_index + 1 :]) + "\n"
|
|
@@ -664,14 +715,14 @@ class ReadCasefile:
|
|
|
664
715
|
return parse.remove_string_characters(df_temp)
|
|
665
716
|
|
|
666
717
|
|
|
667
|
-
def check_contents(values:
|
|
718
|
+
def check_contents(values: npt.NDArray[Any], reference: npt.NDArray[Any]) -> bool:
|
|
668
719
|
"""Check if all members of a list is in another list.
|
|
669
720
|
|
|
670
721
|
Args:
|
|
671
|
-
|
|
672
|
-
|
|
722
|
+
values: Array to be evaluated.
|
|
723
|
+
reference: Reference array.
|
|
673
724
|
|
|
674
725
|
Returns:
|
|
675
|
-
True if members of
|
|
726
|
+
True if members of values are present in reference, false otherwise.
|
|
676
727
|
"""
|
|
677
728
|
return all(comp in reference for comp in values)
|