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/completion.py
CHANGED
|
@@ -2,74 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from typing import overload
|
|
5
|
+
from typing import Literal, overload
|
|
6
6
|
|
|
7
7
|
import numpy as np
|
|
8
8
|
import numpy.typing as npt
|
|
9
9
|
import pandas as pd
|
|
10
10
|
|
|
11
|
-
from completor.constants import
|
|
11
|
+
from completor.constants import Content, Headers, Method
|
|
12
12
|
from completor.exceptions import CompletorError
|
|
13
13
|
from completor.logger import logger
|
|
14
|
-
from completor.
|
|
15
|
-
from completor.utils import as_data_frame, log_and_raise_exception
|
|
16
|
-
|
|
17
|
-
try:
|
|
18
|
-
from typing import Literal, TypeAlias # type: ignore
|
|
19
|
-
except ImportError:
|
|
20
|
-
pass
|
|
21
|
-
|
|
22
|
-
# Use more precise type information, if possible
|
|
23
|
-
DeviceType: TypeAlias = 'Literal["AICD", "ICD", "DAR", "VALVE", "AICV", "ICV"]'
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
class Information:
|
|
27
|
-
"""Holds information from `get_completion`."""
|
|
28
|
-
|
|
29
|
-
# TODO(#85): Improve the class.
|
|
30
|
-
|
|
31
|
-
def __init__(
|
|
32
|
-
self,
|
|
33
|
-
number_of_devices: float | list[float] | None = None,
|
|
34
|
-
device_type: DeviceType | list[DeviceType] | None = None,
|
|
35
|
-
device_number: int | list[int] | None = None,
|
|
36
|
-
inner_diameter: float | list[float] | None = None,
|
|
37
|
-
outer_diameter: float | list[float] | None = None,
|
|
38
|
-
roughness: float | list[float] | None = None,
|
|
39
|
-
annulus_zone: int | list[int] | None = None,
|
|
40
|
-
):
|
|
41
|
-
"""Initialize Information class."""
|
|
42
|
-
self.number_of_devices = number_of_devices
|
|
43
|
-
self.device_type = device_type
|
|
44
|
-
self.device_number = device_number
|
|
45
|
-
self.inner_diameter = inner_diameter
|
|
46
|
-
self.outer_diameter = outer_diameter
|
|
47
|
-
self.roughness = roughness
|
|
48
|
-
self.annulus_zone = annulus_zone
|
|
49
|
-
|
|
50
|
-
def __iadd__(self, other: Information):
|
|
51
|
-
"""Implement value-wise addition between two Information instances."""
|
|
52
|
-
attributes = [
|
|
53
|
-
attribute for attribute in dir(self) if not attribute.startswith("__") and not attribute.endswith("__")
|
|
54
|
-
]
|
|
55
|
-
for attribute in attributes:
|
|
56
|
-
value = getattr(self, attribute)
|
|
57
|
-
if not isinstance(value, list):
|
|
58
|
-
if value is None:
|
|
59
|
-
value = []
|
|
60
|
-
else:
|
|
61
|
-
value = [value]
|
|
62
|
-
setattr(self, attribute, value)
|
|
63
|
-
|
|
64
|
-
value = getattr(other, attribute)
|
|
65
|
-
attr: list = getattr(self, attribute)
|
|
66
|
-
if attr is None:
|
|
67
|
-
attr = []
|
|
68
|
-
if isinstance(value, list):
|
|
69
|
-
attr.extend(value)
|
|
70
|
-
else:
|
|
71
|
-
attr.append(value)
|
|
72
|
-
return self
|
|
14
|
+
from completor.utils import log_and_raise_exception, shift_array
|
|
73
15
|
|
|
74
16
|
|
|
75
17
|
def well_trajectory(df_well_segments_header: pd.DataFrame, df_well_segments_content: pd.DataFrame) -> pd.DataFrame:
|
|
@@ -85,13 +27,19 @@ def well_trajectory(df_well_segments_header: pd.DataFrame, df_well_segments_cont
|
|
|
85
27
|
Measured depth versus true vertical depth.
|
|
86
28
|
|
|
87
29
|
"""
|
|
88
|
-
measured_depth = df_well_segments_content[Headers.
|
|
89
|
-
measured_depth = np.insert(measured_depth, 0, df_well_segments_header[Headers.
|
|
90
|
-
true_vertical_depth = df_well_segments_content[Headers.
|
|
91
|
-
true_vertical_depth = np.insert(
|
|
92
|
-
|
|
30
|
+
measured_depth = df_well_segments_content[Headers.TUBING_MEASURED_DEPTH].to_numpy()
|
|
31
|
+
measured_depth = np.insert(measured_depth, 0, df_well_segments_header[Headers.MEASURED_DEPTH].iloc[0])
|
|
32
|
+
true_vertical_depth = df_well_segments_content[Headers.TRUE_VERTICAL_DEPTH].to_numpy()
|
|
33
|
+
true_vertical_depth = np.insert(
|
|
34
|
+
true_vertical_depth, 0, df_well_segments_header[Headers.TRUE_VERTICAL_DEPTH].iloc[0]
|
|
35
|
+
)
|
|
36
|
+
df_measured_true_vertical_depth = pd.DataFrame(
|
|
37
|
+
{Headers.MEASURED_DEPTH: measured_depth, Headers.TRUE_VERTICAL_DEPTH: true_vertical_depth}
|
|
38
|
+
)
|
|
93
39
|
# sort based on md
|
|
94
|
-
df_measured_true_vertical_depth = df_measured_true_vertical_depth.sort_values(
|
|
40
|
+
df_measured_true_vertical_depth = df_measured_true_vertical_depth.sort_values(
|
|
41
|
+
by=[Headers.MEASURED_DEPTH, Headers.TRUE_VERTICAL_DEPTH]
|
|
42
|
+
)
|
|
95
43
|
# reset index after sorting
|
|
96
44
|
return df_measured_true_vertical_depth.reset_index(drop=True)
|
|
97
45
|
|
|
@@ -114,19 +62,19 @@ def define_annulus_zone(df_completion: pd.DataFrame) -> pd.DataFrame:
|
|
|
114
62
|
"""
|
|
115
63
|
start_measured_depth = df_completion[Headers.START_MEASURED_DEPTH].iloc[0]
|
|
116
64
|
end_measured_depth = df_completion[Headers.END_MEASURED_DEPTH].iloc[-1]
|
|
117
|
-
gravel_pack_location = df_completion[df_completion[Headers.ANNULUS] ==
|
|
65
|
+
gravel_pack_location = df_completion[df_completion[Headers.ANNULUS] == Content.GRAVEL_PACKED][
|
|
118
66
|
[Headers.START_MEASURED_DEPTH, Headers.END_MEASURED_DEPTH]
|
|
119
67
|
].to_numpy()
|
|
120
|
-
packer_location = df_completion[df_completion[Headers.ANNULUS] ==
|
|
68
|
+
packer_location = df_completion[df_completion[Headers.ANNULUS] == Content.PACKER][
|
|
121
69
|
[Headers.START_MEASURED_DEPTH, Headers.END_MEASURED_DEPTH]
|
|
122
70
|
].to_numpy()
|
|
123
71
|
# update df_completion by removing PA rows
|
|
124
|
-
df_completion = df_completion[df_completion[Headers.ANNULUS] !=
|
|
72
|
+
df_completion = df_completion[df_completion[Headers.ANNULUS] != Content.PACKER].copy()
|
|
125
73
|
# reset index after filter
|
|
126
74
|
df_completion.reset_index(drop=True, inplace=True)
|
|
127
75
|
annulus_content = df_completion[Headers.ANNULUS].to_numpy()
|
|
128
76
|
df_completion[Headers.ANNULUS_ZONE] = 0
|
|
129
|
-
if
|
|
77
|
+
if Content.OPEN_ANNULUS in annulus_content:
|
|
130
78
|
# only if there is an open annulus
|
|
131
79
|
boundary = np.concatenate((packer_location.flatten(), gravel_pack_location.flatten()))
|
|
132
80
|
boundary = np.sort(np.append(np.insert(boundary, 0, start_measured_depth), end_measured_depth))
|
|
@@ -145,7 +93,7 @@ def define_annulus_zone(df_completion: pd.DataFrame) -> pd.DataFrame:
|
|
|
145
93
|
if not is_gravel_pack_location:
|
|
146
94
|
annulus_zone[idx] = max(annulus_zone) + 1
|
|
147
95
|
# else it is 0
|
|
148
|
-
df_annulus =
|
|
96
|
+
df_annulus = pd.DataFrame(
|
|
149
97
|
{
|
|
150
98
|
Headers.START_MEASURED_DEPTH: start_bound,
|
|
151
99
|
Headers.END_MEASURED_DEPTH: end_bound,
|
|
@@ -212,7 +160,7 @@ def create_tubing_segments(
|
|
|
212
160
|
cells: Create one segment per cell.
|
|
213
161
|
user: Create segment based on the completion definition.
|
|
214
162
|
fix: Create segment based on a fixed interval.
|
|
215
|
-
|
|
163
|
+
well_data: Create segment based on well segments keyword.
|
|
216
164
|
|
|
217
165
|
Returns:
|
|
218
166
|
DataFrame with start and end measured depth, tubing measured depth, and tubing true vertical depth.
|
|
@@ -230,7 +178,10 @@ def create_tubing_segments(
|
|
|
230
178
|
if not df_reservoir[Headers.SEGMENT].isin(["1*"]).any():
|
|
231
179
|
create_start_measured_depths = []
|
|
232
180
|
create_end_measured_depths = []
|
|
233
|
-
|
|
181
|
+
try:
|
|
182
|
+
create_start_measured_depths.append(df_reservoir[Headers.START_MEASURED_DEPTH].iloc[0])
|
|
183
|
+
except IndexError:
|
|
184
|
+
raise CompletorError("Number of WELSEGS and COMPSEGS is inconsistent.")
|
|
234
185
|
current_segment = df_reservoir[Headers.SEGMENT].iloc[0]
|
|
235
186
|
for i in range(1, len(df_reservoir[Headers.SEGMENT])):
|
|
236
187
|
if df_reservoir[Headers.SEGMENT].iloc[i] != current_segment:
|
|
@@ -290,18 +241,18 @@ def create_tubing_segments(
|
|
|
290
241
|
# Update the end point of the last segment.
|
|
291
242
|
end_measured_depth[-1] = min(float(end_measured_depth[-1]), max_measured_depth)
|
|
292
243
|
elif method == Method.WELSEGS:
|
|
293
|
-
# Create the tubing layer from
|
|
294
|
-
#
|
|
295
|
-
# Completor interprets
|
|
296
|
-
# Obtain the
|
|
297
|
-
well_segments = df_measured_depth_true_vertical_depth[Headers.
|
|
244
|
+
# Create the tubing layer from measured depths in the WELL_SEGMENTS keyword that are missing from COMPLETION_SEGMENTS.
|
|
245
|
+
# WELL_SEGMENTS depths are collected in the `df_measured_depth_true_vertical_depth`, available here.
|
|
246
|
+
# Completor interprets WELL_SEGMENTS depths as segment midpoint depths.
|
|
247
|
+
# Obtain the multisegmented well segments midpoint depth.
|
|
248
|
+
well_segments = df_measured_depth_true_vertical_depth[Headers.MEASURED_DEPTH].to_numpy()
|
|
298
249
|
end_welsegs_depth = 0.5 * (well_segments[:-1] + well_segments[1:])
|
|
299
250
|
# The start of the very first segment in any branch is the actual startMD of the first segment.
|
|
300
251
|
start_welsegs_depth = np.insert(end_welsegs_depth[:-1], 0, well_segments[0], axis=None)
|
|
301
252
|
start_compsegs_depth: npt.NDArray[np.float64] = df_reservoir[Headers.START_MEASURED_DEPTH].to_numpy()
|
|
302
253
|
end_compsegs_depth = df_reservoir[Headers.END_MEASURED_DEPTH].to_numpy()
|
|
303
|
-
# If there are gaps in compsegs and there are
|
|
304
|
-
# insert
|
|
254
|
+
# If there are gaps in compsegs and there are schedule segments that fit in the gaps,
|
|
255
|
+
# insert segments into the compsegs gaps.
|
|
305
256
|
gaps_compsegs = start_compsegs_depth[1:] - end_compsegs_depth[:-1]
|
|
306
257
|
# Indices of gaps in compsegs.
|
|
307
258
|
indices_gaps = np.nonzero(gaps_compsegs)
|
|
@@ -309,7 +260,7 @@ def create_tubing_segments(
|
|
|
309
260
|
start_gaps_depth = end_compsegs_depth[indices_gaps[0]]
|
|
310
261
|
# End of the gaps.
|
|
311
262
|
end_gaps_depth = start_compsegs_depth[indices_gaps[0] + 1]
|
|
312
|
-
# Check the gaps between
|
|
263
|
+
# Check the gaps between COMPLETION_SEGMENTS and fill it out with WELL_SEGMENTS.
|
|
313
264
|
start = np.abs(start_welsegs_depth[:, np.newaxis] - start_gaps_depth).argmin(axis=0)
|
|
314
265
|
end = np.abs(end_welsegs_depth[:, np.newaxis] - end_gaps_depth).argmin(axis=0)
|
|
315
266
|
welsegs_to_add = np.setxor1d(start_welsegs_depth[start], end_welsegs_depth[end])
|
|
@@ -317,7 +268,7 @@ def create_tubing_segments(
|
|
|
317
268
|
end_welsegs_outside = end_welsegs_depth[np.argwhere(end_welsegs_depth > end_compsegs_depth[-1])]
|
|
318
269
|
welsegs_to_add = np.append(welsegs_to_add, start_welsegs_outside)
|
|
319
270
|
welsegs_to_add = np.append(welsegs_to_add, end_welsegs_outside)
|
|
320
|
-
# Find
|
|
271
|
+
# Find schedule segments start and end in gaps.
|
|
321
272
|
start_compsegs_depth = np.append(start_compsegs_depth, welsegs_to_add)
|
|
322
273
|
end_compsegs_depth = np.append(end_compsegs_depth, welsegs_to_add)
|
|
323
274
|
start_measured_depth = np.sort(start_compsegs_depth)
|
|
@@ -339,19 +290,19 @@ def create_tubing_segments(
|
|
|
339
290
|
|
|
340
291
|
# md for tubing segments
|
|
341
292
|
measured_depth_ = 0.5 * (start_measured_depth + end_measured_depth)
|
|
342
|
-
# estimate
|
|
293
|
+
# estimate TRUE_VERTICAL_DEPTH
|
|
343
294
|
true_vertical_depth = np.interp(
|
|
344
295
|
measured_depth_,
|
|
345
|
-
df_measured_depth_true_vertical_depth[Headers.
|
|
346
|
-
df_measured_depth_true_vertical_depth[Headers.
|
|
296
|
+
df_measured_depth_true_vertical_depth[Headers.MEASURED_DEPTH].to_numpy(),
|
|
297
|
+
df_measured_depth_true_vertical_depth[Headers.TRUE_VERTICAL_DEPTH].to_numpy(),
|
|
347
298
|
)
|
|
348
299
|
# create data frame
|
|
349
|
-
return
|
|
300
|
+
return pd.DataFrame(
|
|
350
301
|
{
|
|
351
302
|
Headers.START_MEASURED_DEPTH: start_measured_depth,
|
|
352
303
|
Headers.END_MEASURED_DEPTH: end_measured_depth,
|
|
353
|
-
Headers.
|
|
354
|
-
Headers.
|
|
304
|
+
Headers.TUBING_MEASURED_DEPTH: measured_depth_,
|
|
305
|
+
Headers.TRUE_VERTICAL_DEPTH: true_vertical_depth,
|
|
355
306
|
}
|
|
356
307
|
)
|
|
357
308
|
|
|
@@ -378,32 +329,30 @@ def insert_missing_segments(df_tubing_segments: pd.DataFrame, well_name: str | N
|
|
|
378
329
|
"Schedule file is missing data for one or more branches defined in the case file. "
|
|
379
330
|
f"Please check the data for well {well_name}."
|
|
380
331
|
)
|
|
381
|
-
# sort the data frame based on STARTMD
|
|
382
332
|
df_tubing_segments.sort_values(by=[Headers.START_MEASURED_DEPTH], inplace=True)
|
|
383
|
-
#
|
|
384
|
-
df_tubing_segments[Headers.SEGMENT_DESC] =
|
|
333
|
+
# Add column to indicate original segment.
|
|
334
|
+
df_tubing_segments[Headers.SEGMENT_DESC] = Headers.ORIGINAL_SEGMENT
|
|
385
335
|
end_measured_depth = df_tubing_segments[Headers.END_MEASURED_DEPTH].to_numpy()
|
|
386
|
-
#
|
|
336
|
+
# Get start_measured_depth and start from segment 2 and add the last item to be the last end_measured_depth.
|
|
387
337
|
start_measured_depth = np.append(
|
|
388
338
|
df_tubing_segments[Headers.START_MEASURED_DEPTH].to_numpy()[1:], end_measured_depth[-1]
|
|
389
339
|
)
|
|
390
|
-
#
|
|
340
|
+
# Find rows where start_measured_depth > end_measured_depth.
|
|
391
341
|
missing_index = np.argwhere(start_measured_depth > end_measured_depth).flatten()
|
|
392
|
-
#
|
|
342
|
+
# Proceed only if there are missing indexes.
|
|
393
343
|
if missing_index.size == 0:
|
|
394
344
|
return df_tubing_segments
|
|
395
|
-
#
|
|
345
|
+
# Shift one row down because we move it up one row.
|
|
396
346
|
missing_index += 1
|
|
397
347
|
df_copy = df_tubing_segments.iloc[missing_index, :].copy(deep=True)
|
|
398
|
-
#
|
|
348
|
+
# New start measured depth is the previous segment end measured depth.
|
|
399
349
|
df_copy[Headers.START_MEASURED_DEPTH] = df_tubing_segments[Headers.END_MEASURED_DEPTH].to_numpy()[missing_index - 1]
|
|
400
350
|
df_copy[Headers.END_MEASURED_DEPTH] = df_tubing_segments[Headers.START_MEASURED_DEPTH].to_numpy()[missing_index]
|
|
401
351
|
df_copy[Headers.SEGMENT_DESC] = [Headers.ADDITIONAL_SEGMENT] * df_copy.shape[0]
|
|
402
|
-
#
|
|
352
|
+
# Combine the dataframes.
|
|
403
353
|
df_tubing_segments = pd.concat([df_tubing_segments, df_copy])
|
|
404
|
-
df_tubing_segments.sort_values(by=[Headers.START_MEASURED_DEPTH]
|
|
405
|
-
df_tubing_segments.reset_index(drop=True
|
|
406
|
-
return df_tubing_segments
|
|
354
|
+
df_tubing_segments = df_tubing_segments.sort_values(by=[Headers.START_MEASURED_DEPTH])
|
|
355
|
+
return df_tubing_segments.reset_index(drop=True)
|
|
407
356
|
|
|
408
357
|
|
|
409
358
|
def completion_index(df_completion: pd.DataFrame, start: float, end: float) -> tuple[int, int]:
|
|
@@ -427,18 +376,20 @@ def completion_index(df_completion: pd.DataFrame, start: float, end: float) -> t
|
|
|
427
376
|
return int(_start[0]), int(_end[0])
|
|
428
377
|
|
|
429
378
|
|
|
430
|
-
def get_completion(
|
|
379
|
+
def get_completion(
|
|
380
|
+
start: float, end: float, df_completion: pd.DataFrame, joint_length: float
|
|
381
|
+
) -> tuple[float, float, float, float, float, float, float]:
|
|
431
382
|
"""Get information from the completion.
|
|
432
383
|
|
|
433
384
|
Args:
|
|
434
385
|
start: Start measured depth of the segment.
|
|
435
386
|
end: End measured depth of the segment.
|
|
436
387
|
df_completion: COMPLETION table that must contain columns: `STARTMD`, `ENDMD`, `NVALVEPERJOINT`,
|
|
437
|
-
|
|
388
|
+
`INNER_DIAMETER`, `OUTER_DIAMETER`, `ROUGHNESS`, `DEVICETYPE`, `DEVICENUMBER`, and `ANNULUS_ZONE`.
|
|
438
389
|
joint_length: Length of a joint.
|
|
439
390
|
|
|
440
391
|
Returns:
|
|
441
|
-
|
|
392
|
+
The number of devices, device type, device number, inner diameter, outer diameter, roughness, annulus zone.
|
|
442
393
|
|
|
443
394
|
Raises:
|
|
444
395
|
ValueError:
|
|
@@ -447,13 +398,6 @@ def get_completion(start: float, end: float, df_completion: pd.DataFrame, joint_
|
|
|
447
398
|
If the completion data contains illegal / invalid rows.
|
|
448
399
|
If information class is None.
|
|
449
400
|
"""
|
|
450
|
-
information = None
|
|
451
|
-
device_type = None
|
|
452
|
-
device_number = None
|
|
453
|
-
inner_diameter = None
|
|
454
|
-
outer_diameter = None
|
|
455
|
-
roughness = None
|
|
456
|
-
annulus_zone = None
|
|
457
401
|
|
|
458
402
|
start_completion = df_completion[Headers.START_MEASURED_DEPTH].to_numpy()
|
|
459
403
|
end_completion = df_completion[Headers.END_MEASURED_DEPTH].to_numpy()
|
|
@@ -463,58 +407,39 @@ def get_completion(start: float, end: float, df_completion: pd.DataFrame, joint_
|
|
|
463
407
|
well_name = df_completion[Headers.WELL].iloc[0]
|
|
464
408
|
log_and_raise_exception(f"No completion is defined on well {well_name} from {start} to {end}.")
|
|
465
409
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
if all(
|
|
500
|
-
x is not None for x in [device_type, device_number, inner_diameter, outer_diameter, roughness, annulus_zone]
|
|
501
|
-
):
|
|
502
|
-
information = Information(
|
|
503
|
-
num_device, device_type, device_number, inner_diameter, outer_diameter, roughness, annulus_zone
|
|
504
|
-
)
|
|
505
|
-
else:
|
|
506
|
-
# I.e. if completion_length > prev_length never happens
|
|
507
|
-
raise ValueError(
|
|
508
|
-
f"The completion data for well '{df_completion[Headers.WELL][completion_idx]}' "
|
|
509
|
-
"contains illegal / invalid row(s). "
|
|
510
|
-
"Please check their start mD / end mD columns, and ensure that they start before they end."
|
|
511
|
-
)
|
|
512
|
-
if information is None:
|
|
513
|
-
raise ValueError(
|
|
514
|
-
f"idx0 == idx1 + 1 (idx0={idx0}). For the time being, the reason is unknown. "
|
|
515
|
-
"Please reach out to the Equinor Inflow Control Team if you encounter this."
|
|
516
|
-
)
|
|
517
|
-
return information
|
|
410
|
+
indices = np.arange(idx0, idx1 + 1)
|
|
411
|
+
lengths = np.minimum(end_completion[indices], end) - np.maximum(start_completion[indices], start)
|
|
412
|
+
if (lengths <= 0).any():
|
|
413
|
+
# _ = "equals" if length == 0 else "less than"
|
|
414
|
+
# _ = np.where(lengths == 0, "equals", 0)
|
|
415
|
+
# _2 = np.where(lengths < 0, "less than", 0)
|
|
416
|
+
# logger.warning(
|
|
417
|
+
# f"Start depth less than or equals to stop depth,
|
|
418
|
+
# for {df_completion[Headers.START_MEASURED_DEPTH][indices][warning_mask]}"
|
|
419
|
+
# )
|
|
420
|
+
logger.warning("Depths are incongruent.")
|
|
421
|
+
number_of_devices = np.sum((lengths / joint_length) * df_completion[Headers.VALVES_PER_JOINT].to_numpy()[indices])
|
|
422
|
+
|
|
423
|
+
mask = lengths > shift_array(lengths, 1, fill_value=0)
|
|
424
|
+
inner_diameter = df_completion[Headers.INNER_DIAMETER].to_numpy()[indices][mask]
|
|
425
|
+
outer_diameter = df_completion[Headers.OUTER_DIAMETER].to_numpy()[indices][mask]
|
|
426
|
+
roughness = df_completion[Headers.ROUGHNESS].to_numpy()[indices][mask]
|
|
427
|
+
if (inner_diameter > outer_diameter).any():
|
|
428
|
+
raise ValueError("Check screen/tubing and well/casing ID in case file.")
|
|
429
|
+
outer_diameter = (outer_diameter**2 - inner_diameter**2) ** 0.5
|
|
430
|
+
device_type = df_completion[Headers.DEVICE_TYPE].to_numpy()[indices][mask]
|
|
431
|
+
device_number = df_completion[Headers.DEVICE_NUMBER].to_numpy()[indices][mask]
|
|
432
|
+
annulus_zone = df_completion[Headers.ANNULUS_ZONE].to_numpy()[indices][mask]
|
|
433
|
+
|
|
434
|
+
return (
|
|
435
|
+
number_of_devices,
|
|
436
|
+
device_type[-1],
|
|
437
|
+
device_number[-1],
|
|
438
|
+
inner_diameter[-1],
|
|
439
|
+
outer_diameter[-1],
|
|
440
|
+
roughness[-1],
|
|
441
|
+
annulus_zone[-1],
|
|
442
|
+
)
|
|
518
443
|
|
|
519
444
|
|
|
520
445
|
def complete_the_well(
|
|
@@ -530,27 +455,47 @@ def complete_the_well(
|
|
|
530
455
|
Returns:
|
|
531
456
|
Well information.
|
|
532
457
|
"""
|
|
458
|
+
number_of_devices = []
|
|
459
|
+
device_type = []
|
|
460
|
+
device_number = []
|
|
461
|
+
inner_diameter = []
|
|
462
|
+
outer_diameter = []
|
|
463
|
+
roughness = []
|
|
464
|
+
annulus_zone = []
|
|
465
|
+
|
|
533
466
|
start = df_tubing_segments[Headers.START_MEASURED_DEPTH].to_numpy()
|
|
534
467
|
end = df_tubing_segments[Headers.END_MEASURED_DEPTH].to_numpy()
|
|
535
|
-
|
|
536
|
-
information = Information()
|
|
468
|
+
|
|
537
469
|
# loop through the cells
|
|
538
470
|
for i in range(df_tubing_segments.shape[0]):
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
471
|
+
indices = [np.array(completion_index(df_completion, s, e)) for s, e in zip(start, end)]
|
|
472
|
+
|
|
473
|
+
if any(idx0 == -1 or idx1 == -1 for idx0, idx1 in indices):
|
|
474
|
+
well_name = df_completion[Headers.WELL].iloc[0]
|
|
475
|
+
log_and_raise_exception(f"No completion is defined on well {well_name} from {start} to {end}.")
|
|
476
|
+
|
|
477
|
+
completion_data = get_completion(start[i], end[i], df_completion, joint_length)
|
|
478
|
+
number_of_devices.append(completion_data[0])
|
|
479
|
+
device_type.append(completion_data[1])
|
|
480
|
+
device_number.append(completion_data[2])
|
|
481
|
+
inner_diameter.append(completion_data[3])
|
|
482
|
+
outer_diameter.append(completion_data[4])
|
|
483
|
+
roughness.append(completion_data[5])
|
|
484
|
+
annulus_zone.append(completion_data[6])
|
|
485
|
+
|
|
486
|
+
df_well = pd.DataFrame(
|
|
542
487
|
{
|
|
543
|
-
Headers.
|
|
544
|
-
Headers.
|
|
488
|
+
Headers.TUBING_MEASURED_DEPTH: df_tubing_segments[Headers.TUBING_MEASURED_DEPTH].to_numpy(),
|
|
489
|
+
Headers.TRUE_VERTICAL_DEPTH: df_tubing_segments[Headers.TRUE_VERTICAL_DEPTH].to_numpy(),
|
|
545
490
|
Headers.LENGTH: end - start,
|
|
546
491
|
Headers.SEGMENT_DESC: df_tubing_segments[Headers.SEGMENT_DESC].to_numpy(),
|
|
547
|
-
Headers.NUMBER_OF_DEVICES:
|
|
548
|
-
Headers.DEVICE_NUMBER:
|
|
549
|
-
Headers.DEVICE_TYPE:
|
|
550
|
-
Headers.INNER_DIAMETER:
|
|
551
|
-
Headers.OUTER_DIAMETER:
|
|
552
|
-
Headers.ROUGHNESS:
|
|
553
|
-
Headers.ANNULUS_ZONE:
|
|
492
|
+
Headers.NUMBER_OF_DEVICES: number_of_devices,
|
|
493
|
+
Headers.DEVICE_NUMBER: device_number,
|
|
494
|
+
Headers.DEVICE_TYPE: device_type,
|
|
495
|
+
Headers.INNER_DIAMETER: inner_diameter,
|
|
496
|
+
Headers.OUTER_DIAMETER: outer_diameter,
|
|
497
|
+
Headers.ROUGHNESS: roughness,
|
|
498
|
+
Headers.ANNULUS_ZONE: annulus_zone,
|
|
554
499
|
}
|
|
555
500
|
)
|
|
556
501
|
|
|
@@ -558,7 +503,7 @@ def complete_the_well(
|
|
|
558
503
|
df_well = lumping_segments(df_well)
|
|
559
504
|
|
|
560
505
|
# create scaling factor
|
|
561
|
-
df_well[Headers.
|
|
506
|
+
df_well[Headers.SCALE_FACTOR] = np.where(
|
|
562
507
|
df_well[Headers.NUMBER_OF_DEVICES] > 0.0, -1.0 / df_well[Headers.NUMBER_OF_DEVICES], 0.0
|
|
563
508
|
)
|
|
564
509
|
return df_well
|
|
@@ -606,7 +551,7 @@ def lumping_segments(df_well: pd.DataFrame) -> pd.DataFrame:
|
|
|
606
551
|
return df_well.reset_index(drop=True, inplace=False)
|
|
607
552
|
|
|
608
553
|
|
|
609
|
-
def get_device(df_well: pd.DataFrame, df_device: pd.DataFrame, device_type:
|
|
554
|
+
def get_device(df_well: pd.DataFrame, df_device: pd.DataFrame, device_type: str) -> pd.DataFrame:
|
|
610
555
|
"""Get device characteristics.
|
|
611
556
|
|
|
612
557
|
Args:
|
|
@@ -627,14 +572,14 @@ def get_device(df_well: pd.DataFrame, df_device: pd.DataFrame, device_type: Devi
|
|
|
627
572
|
if f"'{Headers.DEVICE_TYPE}'" in str(err):
|
|
628
573
|
raise ValueError(f"Missing keyword 'DEVICETYPE {device_type}' in input files.") from err
|
|
629
574
|
raise err
|
|
630
|
-
if device_type ==
|
|
575
|
+
if device_type == Content.VALVE:
|
|
631
576
|
# rescale the Cv
|
|
632
|
-
# because no scaling factor in
|
|
633
|
-
df_well[Headers.
|
|
634
|
-
elif device_type ==
|
|
577
|
+
# because no scaling factor in WELL_SEGMENTS_VALVE
|
|
578
|
+
df_well[Headers.FLOW_COEFFICIENT] = -df_well[Headers.FLOW_COEFFICIENT] / df_well[Headers.SCALE_FACTOR]
|
|
579
|
+
elif device_type == Content.DENSITY_ACTIVATED_RECOVERY:
|
|
635
580
|
# rescale the Cv
|
|
636
|
-
# because no scaling factor in
|
|
637
|
-
df_well[Headers.
|
|
581
|
+
# because no scaling factor in WELL_SEGMENTS_VALVE
|
|
582
|
+
df_well[Headers.FLOW_COEFFICIENT] = -df_well[Headers.FLOW_COEFFICIENT] / df_well[Headers.SCALE_FACTOR]
|
|
638
583
|
return df_well
|
|
639
584
|
|
|
640
585
|
|
|
@@ -655,7 +600,8 @@ def correct_annulus_zone(df_well: pd.DataFrame) -> pd.DataFrame:
|
|
|
655
600
|
continue
|
|
656
601
|
df_zone = df_well[df_well[Headers.ANNULUS_ZONE] == zone]
|
|
657
602
|
df_zone_device = df_zone[
|
|
658
|
-
(df_zone[Headers.NUMBER_OF_DEVICES].to_numpy() > 0)
|
|
603
|
+
(df_zone[Headers.NUMBER_OF_DEVICES].to_numpy() > 0)
|
|
604
|
+
| (df_zone[Headers.DEVICE_TYPE].to_numpy() == Content.PERFORATED)
|
|
659
605
|
]
|
|
660
606
|
if df_zone_device.shape[0] == 0:
|
|
661
607
|
df_well[Headers.ANNULUS_ZONE].replace(zone, 0, inplace=True)
|
|
@@ -669,374 +615,38 @@ def connect_cells_to_segments(
|
|
|
669
615
|
|
|
670
616
|
Args:
|
|
671
617
|
df_well: Segment table. Must contain tubing measured depth.
|
|
672
|
-
df_reservoir:
|
|
618
|
+
df_reservoir: COMPLETION_SEGMENTS table. Must contain start and end measured depth.
|
|
673
619
|
df_tubing_segments: Tubing segment dataframe. Must contain start and end measured depth.
|
|
674
620
|
method: Segmentation method indicator. Must be one of 'user', 'fix', 'welsegs', or 'cells'.
|
|
675
621
|
|
|
676
622
|
Returns:
|
|
677
623
|
Merged DataFrame.
|
|
678
624
|
"""
|
|
625
|
+
df_well = df_well.copy()
|
|
679
626
|
# Calculate mid cell measured depth
|
|
680
|
-
df_reservoir[Headers.
|
|
627
|
+
df_reservoir[Headers.MEASURED_DEPTH] = (
|
|
681
628
|
df_reservoir[Headers.START_MEASURED_DEPTH] + df_reservoir[Headers.END_MEASURED_DEPTH]
|
|
682
|
-
)
|
|
629
|
+
) / 2
|
|
683
630
|
if method == Method.USER:
|
|
684
|
-
df_res = df_reservoir.copy(deep=True)
|
|
685
|
-
df_wel = df_well.copy(deep=True)
|
|
686
631
|
# Ensure that tubing segment boundaries as described in the case file are honored.
|
|
687
|
-
# Associate reservoir cells with tubing segment midpoints using markers
|
|
632
|
+
# Associate reservoir cells with tubing segment midpoints using markers.
|
|
633
|
+
df_reservoir[Headers.MARKER] = np.full(df_reservoir.shape[0], 0)
|
|
634
|
+
df_well.loc[:, Headers.MARKER] = np.arange(df_well.shape[0]) + 1
|
|
635
|
+
|
|
636
|
+
start_measured_depths = df_tubing_segments[Headers.START_MEASURED_DEPTH]
|
|
637
|
+
end_measured_depths = df_tubing_segments[Headers.END_MEASURED_DEPTH]
|
|
638
|
+
|
|
688
639
|
marker = 1
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
for idx in df_wel[Headers.TUB_MD].index:
|
|
692
|
-
start_measured_depth = df_tubing_segments[Headers.START_MEASURED_DEPTH].iloc[idx]
|
|
693
|
-
end_measured_depth = df_tubing_segments[Headers.END_MEASURED_DEPTH].iloc[idx]
|
|
694
|
-
df_res.loc[df_res[Headers.MD].between(start_measured_depth, end_measured_depth), Headers.MARKER] = marker
|
|
640
|
+
for start, end in zip(start_measured_depths, end_measured_depths):
|
|
641
|
+
df_reservoir.loc[df_reservoir[Headers.MEASURED_DEPTH].between(start, end), Headers.MARKER] = marker
|
|
695
642
|
marker += 1
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
return tmp.drop([Headers.MARKER], axis=1, inplace=False)
|
|
643
|
+
|
|
644
|
+
return df_reservoir.merge(df_well, on=Headers.MARKER).drop(Headers.MARKER, axis=1)
|
|
699
645
|
|
|
700
646
|
return pd.merge_asof(
|
|
701
|
-
left=df_reservoir,
|
|
647
|
+
left=df_reservoir,
|
|
648
|
+
right=df_well,
|
|
649
|
+
left_on=Headers.MEASURED_DEPTH,
|
|
650
|
+
right_on=Headers.TUBING_MEASURED_DEPTH,
|
|
651
|
+
direction="nearest",
|
|
702
652
|
)
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
class WellSchedule:
|
|
706
|
-
"""A collection of all the active multi-segment wells.
|
|
707
|
-
|
|
708
|
-
Attributes:
|
|
709
|
-
msws: Multisegmented well segments.
|
|
710
|
-
active_wells: The active wells for completor to work on.
|
|
711
|
-
|
|
712
|
-
Args:
|
|
713
|
-
active_wells: Active multi-segment wells defined in a case file.
|
|
714
|
-
"""
|
|
715
|
-
|
|
716
|
-
def __init__(self, active_wells: npt.NDArray[np.unicode_] | list[str]):
|
|
717
|
-
"""Initialize WellSchedule."""
|
|
718
|
-
self.msws: dict[str, dict] = {}
|
|
719
|
-
self.active_wells = np.array(active_wells)
|
|
720
|
-
|
|
721
|
-
def set_welspecs(self, records: list[list[str]]) -> None:
|
|
722
|
-
"""Convert the well specifications (WELSPECS) record to a Pandas DataFrame.
|
|
723
|
-
|
|
724
|
-
* Sets DataFrame column titles.
|
|
725
|
-
* Formats column values.
|
|
726
|
-
* Pads missing columns at the end of the DataFrame with default values (1*).
|
|
727
|
-
|
|
728
|
-
Args:
|
|
729
|
-
records: Raw well specification.
|
|
730
|
-
|
|
731
|
-
Returns:
|
|
732
|
-
Record of inactive wells (in `self.msws`).
|
|
733
|
-
"""
|
|
734
|
-
columns = [
|
|
735
|
-
Headers.WELL,
|
|
736
|
-
Headers.GROUP,
|
|
737
|
-
Headers.I,
|
|
738
|
-
Headers.J,
|
|
739
|
-
Headers.BHP_DEPTH,
|
|
740
|
-
Headers.PHASE,
|
|
741
|
-
Headers.DR,
|
|
742
|
-
Headers.FLAG,
|
|
743
|
-
Headers.SHUT,
|
|
744
|
-
Headers.CROSS,
|
|
745
|
-
Headers.PRESSURE_TABLE,
|
|
746
|
-
Headers.DENSCAL,
|
|
747
|
-
Headers.REGION,
|
|
748
|
-
Headers.ITEM_14,
|
|
749
|
-
Headers.ITEM_15,
|
|
750
|
-
Headers.ITEM_16,
|
|
751
|
-
Headers.ITEM_17,
|
|
752
|
-
]
|
|
753
|
-
_records = records[0] + ["1*"] * (len(columns) - len(records[0])) # pad with default values (1*)
|
|
754
|
-
df = pd.DataFrame(np.array(_records).reshape((1, len(columns))), columns=columns)
|
|
755
|
-
# datatypes
|
|
756
|
-
df[columns[2:4]] = df[columns[2:4]].astype(np.int64)
|
|
757
|
-
try:
|
|
758
|
-
df[columns[4]] = df[columns[4]].astype(np.float64)
|
|
759
|
-
except ValueError:
|
|
760
|
-
pass
|
|
761
|
-
# welspecs could be for multiple wells - split it
|
|
762
|
-
for well_name in df[Headers.WELL].unique():
|
|
763
|
-
if well_name not in self.msws:
|
|
764
|
-
self.msws[well_name] = {}
|
|
765
|
-
self.msws[well_name][Keywords.WELSPECS] = df[df[Headers.WELL] == well_name]
|
|
766
|
-
logger.debug("set_welspecs for %s", well_name)
|
|
767
|
-
|
|
768
|
-
def handle_compdat(self, records: list[list[str]]) -> list[list[str]]:
|
|
769
|
-
"""Convert completion data (COMPDAT) record to a DataFrame.
|
|
770
|
-
|
|
771
|
-
* Sets DataFrame column titles.
|
|
772
|
-
* Pads missing values with default values (1*).
|
|
773
|
-
* Sets column data types.
|
|
774
|
-
|
|
775
|
-
Args:
|
|
776
|
-
records: Record set of COMPDAT data.
|
|
777
|
-
|
|
778
|
-
Returns:
|
|
779
|
-
Records for inactive wells.
|
|
780
|
-
"""
|
|
781
|
-
well_names = set() # the active well-names found in this chunk
|
|
782
|
-
remains = [] # the other wells
|
|
783
|
-
for rec in records:
|
|
784
|
-
well_name = rec[0]
|
|
785
|
-
if well_name in list(self.active_wells):
|
|
786
|
-
well_names.add(well_name)
|
|
787
|
-
else:
|
|
788
|
-
remains.append(rec)
|
|
789
|
-
columns = [
|
|
790
|
-
Headers.WELL,
|
|
791
|
-
Headers.I,
|
|
792
|
-
Headers.J,
|
|
793
|
-
Headers.K,
|
|
794
|
-
Headers.K2,
|
|
795
|
-
Headers.STATUS,
|
|
796
|
-
Headers.SATURATION_FUNCTION_REGION_NUMBERS,
|
|
797
|
-
Headers.CONNECTION_FACTOR,
|
|
798
|
-
Headers.DIAMETER,
|
|
799
|
-
Headers.FORAMTION_PERMEABILITY_THICKNESS,
|
|
800
|
-
Headers.SKIN,
|
|
801
|
-
Headers.DFACT,
|
|
802
|
-
Headers.COMPDAT_DIRECTION,
|
|
803
|
-
Headers.RO,
|
|
804
|
-
]
|
|
805
|
-
df = pd.DataFrame(records, columns=columns[0 : len(records[0])])
|
|
806
|
-
if Headers.RO in df.columns:
|
|
807
|
-
df[Headers.RO] = df[Headers.RO].fillna("1*")
|
|
808
|
-
for i in range(len(records[0]), len(columns)):
|
|
809
|
-
df[columns[i]] = ["1*"] * len(records)
|
|
810
|
-
# data types
|
|
811
|
-
df[columns[1:5]] = df[columns[1:5]].astype(np.int64)
|
|
812
|
-
# Change default value '1*' to equivalent float
|
|
813
|
-
df["SKIN"] = df["SKIN"].replace(["1*"], 0.0)
|
|
814
|
-
df[[Headers.DIAMETER, Headers.SKIN]] = df[[Headers.DIAMETER, Headers.SKIN]].astype(np.float64)
|
|
815
|
-
# check if CONNECTION_FACTOR, FORAMTION_PERMEABILITY_THICKNESS, and RO are defaulted by the users
|
|
816
|
-
try:
|
|
817
|
-
df[[Headers.CONNECTION_FACTOR]] = df[[Headers.CONNECTION_FACTOR]].astype(np.float64)
|
|
818
|
-
except ValueError:
|
|
819
|
-
pass
|
|
820
|
-
try:
|
|
821
|
-
df[[Headers.FORAMTION_PERMEABILITY_THICKNESS]] = df[[Headers.FORAMTION_PERMEABILITY_THICKNESS]].astype(
|
|
822
|
-
np.float64
|
|
823
|
-
)
|
|
824
|
-
except ValueError:
|
|
825
|
-
pass
|
|
826
|
-
try:
|
|
827
|
-
df[[Headers.RO]] = df[[Headers.RO]].astype(np.float64)
|
|
828
|
-
except ValueError:
|
|
829
|
-
pass
|
|
830
|
-
# compdat could be for multiple wells - split it
|
|
831
|
-
for well_name in well_names:
|
|
832
|
-
if well_name not in self.msws:
|
|
833
|
-
self.msws[well_name] = {}
|
|
834
|
-
self.msws[well_name][Keywords.COMPDAT] = df[df[Headers.WELL] == well_name]
|
|
835
|
-
logger.debug("handle_compdat for %s", well_name)
|
|
836
|
-
return remains
|
|
837
|
-
|
|
838
|
-
def set_welsegs(self, recs: list[list[str]]) -> str | None:
|
|
839
|
-
"""Update the well segments (WELSEGS) for a given well if it is an active well.
|
|
840
|
-
|
|
841
|
-
* Pads missing record columns in header and contents with default values.
|
|
842
|
-
* Convert header and column records to DataFrames.
|
|
843
|
-
* Sets proper DataFrame column types and titles.
|
|
844
|
-
* Converts segment depth specified in incremental (INC) to absolute (ABS) values using fix_welsegs.
|
|
845
|
-
|
|
846
|
-
Args:
|
|
847
|
-
recs: Record set of header and contents data.
|
|
848
|
-
|
|
849
|
-
Returns:
|
|
850
|
-
Name of well if it was updated, or None if it is not in the active_wells list.
|
|
851
|
-
"""
|
|
852
|
-
well_name = recs[0][0] # each WELSEGS-chunk is for one well only
|
|
853
|
-
if well_name not in self.active_wells:
|
|
854
|
-
return None
|
|
855
|
-
|
|
856
|
-
# make df for header record
|
|
857
|
-
columns_header = [
|
|
858
|
-
Headers.WELL,
|
|
859
|
-
Headers.SEGMENTTVD,
|
|
860
|
-
Headers.SEGMENTMD,
|
|
861
|
-
Headers.WBVOLUME,
|
|
862
|
-
Headers.INFO_TYPE,
|
|
863
|
-
Headers.PDROPCOMP,
|
|
864
|
-
Headers.MPMODEL,
|
|
865
|
-
Headers.ITEM_8,
|
|
866
|
-
Headers.ITEM_9,
|
|
867
|
-
Headers.ITEM_10,
|
|
868
|
-
Headers.ITEM_11,
|
|
869
|
-
Headers.ITEM_12,
|
|
870
|
-
]
|
|
871
|
-
# pad header with default values (1*)
|
|
872
|
-
header = recs[0] + ["1*"] * (len(columns_header) - len(recs[0]))
|
|
873
|
-
df_header = pd.DataFrame(np.array(header).reshape((1, len(columns_header))), columns=columns_header)
|
|
874
|
-
df_header[columns_header[1:3]] = df_header[columns_header[1:3]].astype(np.float64) # data types
|
|
875
|
-
|
|
876
|
-
# make df for data records
|
|
877
|
-
columns_data = [
|
|
878
|
-
Headers.TUBING_SEGMENT,
|
|
879
|
-
Headers.TUBING_SEGMENT_2,
|
|
880
|
-
Headers.TUBINGBRANCH,
|
|
881
|
-
Headers.TUBING_OUTLET,
|
|
882
|
-
Headers.TUBINGMD,
|
|
883
|
-
Headers.TUBINGTVD,
|
|
884
|
-
Headers.TUBING_INNER_DIAMETER,
|
|
885
|
-
Headers.TUBING_ROUGHNESS,
|
|
886
|
-
Headers.CROSS,
|
|
887
|
-
Headers.VSEG,
|
|
888
|
-
Headers.ITEM_11,
|
|
889
|
-
Headers.ITEM_12,
|
|
890
|
-
Headers.ITEM_13,
|
|
891
|
-
Headers.ITEM_14,
|
|
892
|
-
Headers.ITEM_15,
|
|
893
|
-
]
|
|
894
|
-
# pad with default values (1*)
|
|
895
|
-
recs = [rec + ["1*"] * (len(columns_data) - len(rec)) for rec in recs[1:]]
|
|
896
|
-
df_records = pd.DataFrame(recs, columns=columns_data)
|
|
897
|
-
# data types
|
|
898
|
-
df_records[columns_data[:4]] = df_records[columns_data[:4]].astype(np.int64)
|
|
899
|
-
df_records[columns_data[4:7]] = df_records[columns_data[4:7]].astype(np.float64)
|
|
900
|
-
# fix abs/inc issue with welsegs
|
|
901
|
-
df_header, df_records = fix_welsegs(df_header, df_records)
|
|
902
|
-
|
|
903
|
-
# Warn user if the tubing segments' measured depth for a branch
|
|
904
|
-
# is not sorted in ascending order (monotonic)
|
|
905
|
-
for branch_num in df_records[Headers.TUBINGBRANCH].unique():
|
|
906
|
-
if (
|
|
907
|
-
not df_records[Headers.TUBINGMD]
|
|
908
|
-
.loc[df_records[Headers.TUBINGBRANCH] == branch_num]
|
|
909
|
-
.is_monotonic_increasing
|
|
910
|
-
):
|
|
911
|
-
logger.warning(
|
|
912
|
-
"The branch %s in well %s contains negative length segments. "
|
|
913
|
-
"Check the input schedulefile WELSEGS keyword for inconsistencies "
|
|
914
|
-
"in measured depth (MD) of Tubing layer.",
|
|
915
|
-
branch_num,
|
|
916
|
-
well_name,
|
|
917
|
-
)
|
|
918
|
-
|
|
919
|
-
if well_name not in self.msws:
|
|
920
|
-
self.msws[well_name] = {}
|
|
921
|
-
self.msws[well_name][Keywords.WELSEGS] = df_header, df_records
|
|
922
|
-
return well_name
|
|
923
|
-
|
|
924
|
-
def set_compsegs(self, recs: list[list[str]]) -> str | None:
|
|
925
|
-
"""Update COMPSEGS for a well if it is an active well.
|
|
926
|
-
|
|
927
|
-
* Pads missing record columns in header and contents with default 1*.
|
|
928
|
-
* Convert header and column records to DataFrames.
|
|
929
|
-
* Sets proper DataFrame column types and titles.
|
|
930
|
-
|
|
931
|
-
Args:
|
|
932
|
-
recs: Record set of header and contents data.
|
|
933
|
-
|
|
934
|
-
Returns:
|
|
935
|
-
Name of well if it was updated, or None if it is not in active_wells.
|
|
936
|
-
"""
|
|
937
|
-
well_name = recs[0][0] # each COMPSEGS-chunk is for one well only
|
|
938
|
-
if well_name not in self.active_wells:
|
|
939
|
-
return None
|
|
940
|
-
columns = [
|
|
941
|
-
Headers.I,
|
|
942
|
-
Headers.J,
|
|
943
|
-
Headers.K,
|
|
944
|
-
Headers.BRANCH,
|
|
945
|
-
Headers.START_MEASURED_DEPTH,
|
|
946
|
-
Headers.END_MEASURED_DEPTH,
|
|
947
|
-
Headers.COMPSEGS_DIRECTION,
|
|
948
|
-
Headers.ENDGRID,
|
|
949
|
-
Headers.PERFDEPTH,
|
|
950
|
-
Headers.THERM,
|
|
951
|
-
Headers.SEGMENT,
|
|
952
|
-
]
|
|
953
|
-
recs = [rec + ["1*"] * (len(columns) - len(rec)) for rec in recs[1:]] # pad with default values (1*)
|
|
954
|
-
df = pd.DataFrame(recs, columns=columns)
|
|
955
|
-
df[columns[:4]] = df[columns[:4]].astype(np.int64)
|
|
956
|
-
df[columns[4:6]] = df[columns[4:6]].astype(np.float64)
|
|
957
|
-
if well_name not in self.msws:
|
|
958
|
-
self.msws[well_name] = {}
|
|
959
|
-
self.msws[well_name][Keywords.COMPSEGS] = df
|
|
960
|
-
logger.debug("set_compsegs for %s", well_name)
|
|
961
|
-
return well_name
|
|
962
|
-
|
|
963
|
-
def get_welspecs(self, well_name: str) -> pd.DataFrame:
|
|
964
|
-
"""Get-function for WELSPECS.
|
|
965
|
-
|
|
966
|
-
Args:
|
|
967
|
-
well_name: Well name.
|
|
968
|
-
|
|
969
|
-
Returns:
|
|
970
|
-
Well specifications.
|
|
971
|
-
"""
|
|
972
|
-
return self.msws[well_name][Keywords.WELSPECS]
|
|
973
|
-
|
|
974
|
-
def get_compdat(self, well_name: str) -> pd.DataFrame:
|
|
975
|
-
"""Get-function for COMPDAT.
|
|
976
|
-
|
|
977
|
-
Args:
|
|
978
|
-
well_name: Well name.
|
|
979
|
-
|
|
980
|
-
Returns:
|
|
981
|
-
Completion data.
|
|
982
|
-
|
|
983
|
-
Raises:
|
|
984
|
-
ValueError: If completion data keyword is missing in input schedule file.
|
|
985
|
-
"""
|
|
986
|
-
try:
|
|
987
|
-
return self.msws[well_name][Keywords.COMPDAT]
|
|
988
|
-
except KeyError as err:
|
|
989
|
-
if f"'{Keywords.COMPDAT}'" in str(err):
|
|
990
|
-
raise ValueError("Input schedule file missing COMPDAT keyword.") from err
|
|
991
|
-
raise err
|
|
992
|
-
|
|
993
|
-
def get_compsegs(self, well_name: str, branch: int | None = None) -> pd.DataFrame:
|
|
994
|
-
"""Get-function for COMPSEGS.
|
|
995
|
-
|
|
996
|
-
Args:
|
|
997
|
-
well_name: Well name.
|
|
998
|
-
branch: Branch number.
|
|
999
|
-
|
|
1000
|
-
Returns:
|
|
1001
|
-
Completion segment data.
|
|
1002
|
-
"""
|
|
1003
|
-
df = self.msws[well_name][Keywords.COMPSEGS].copy()
|
|
1004
|
-
if branch is not None:
|
|
1005
|
-
df = df[df[Headers.BRANCH] == branch]
|
|
1006
|
-
df.reset_index(drop=True, inplace=True) # reset index after filtering
|
|
1007
|
-
return fix_compsegs(df, well_name)
|
|
1008
|
-
|
|
1009
|
-
def get_well_segments(self, well_name: str, branch: int | None = None) -> tuple[pd.DataFrame, pd.DataFrame]:
|
|
1010
|
-
"""Get-function for well segments.
|
|
1011
|
-
|
|
1012
|
-
Args:
|
|
1013
|
-
well_name: Well name.
|
|
1014
|
-
branch: Branch number.
|
|
1015
|
-
|
|
1016
|
-
Returns:
|
|
1017
|
-
Well segments headers and content.
|
|
1018
|
-
|
|
1019
|
-
Raises:
|
|
1020
|
-
ValueError: If WELSEGS keyword missing in input schedule file.
|
|
1021
|
-
"""
|
|
1022
|
-
try:
|
|
1023
|
-
columns, content = self.msws[well_name][Keywords.WELSEGS]
|
|
1024
|
-
except KeyError as err:
|
|
1025
|
-
if f"'{Keywords.WELSEGS}'" in str(err):
|
|
1026
|
-
raise ValueError("Input schedule file missing WELSEGS keyword.") from err
|
|
1027
|
-
raise err
|
|
1028
|
-
if branch is not None:
|
|
1029
|
-
content = content[content[Headers.TUBINGBRANCH] == branch]
|
|
1030
|
-
content.reset_index(drop=True, inplace=True)
|
|
1031
|
-
return columns, content
|
|
1032
|
-
|
|
1033
|
-
def get_well_number(self, well_name: str) -> int:
|
|
1034
|
-
"""Well number in the active_wells list.
|
|
1035
|
-
|
|
1036
|
-
Args:
|
|
1037
|
-
well_name: Well name.
|
|
1038
|
-
|
|
1039
|
-
Returns:
|
|
1040
|
-
Well number.
|
|
1041
|
-
"""
|
|
1042
|
-
return int(np.where(self.active_wells == well_name)[0][0])
|