completor 0.1.3__py3-none-any.whl → 1.0.1__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 +149 -545
- completor/constants.py +223 -150
- completor/create_output.py +559 -431
- completor/exceptions/__init__.py +0 -4
- completor/exceptions/exceptions.py +6 -6
- completor/get_version.py +8 -0
- completor/hook_implementations/jobs.py +21 -13
- completor/input_validation.py +54 -42
- completor/launch_args_parser.py +7 -12
- completor/logger.py +3 -3
- completor/main.py +105 -363
- completor/parse.py +104 -93
- completor/prepare_outputs.py +594 -458
- completor/read_casefile.py +250 -198
- completor/read_schedule.py +317 -14
- completor/utils.py +256 -54
- completor/visualization.py +1 -14
- completor/visualize_well.py +29 -27
- completor/wells.py +271 -0
- {completor-0.1.3.dist-info → completor-1.0.1.dist-info}/METADATA +11 -12
- completor-1.0.1.dist-info/RECORD +27 -0
- completor/create_wells.py +0 -314
- completor/pvt_model.py +0 -14
- completor-0.1.3.dist-info/RECORD +0 -27
- {completor-0.1.3.dist-info → completor-1.0.1.dist-info}/LICENSE +0 -0
- {completor-0.1.3.dist-info → completor-1.0.1.dist-info}/WHEEL +0 -0
- {completor-0.1.3.dist-info → completor-1.0.1.dist-info}/entry_points.txt +0 -0
completor/wells.py
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""Classes to keep track of Well objects."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import numpy.typing as npt
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
from completor import completion, read_schedule
|
|
10
|
+
from completor.constants import Content, Headers, Method, WellData
|
|
11
|
+
from completor.read_casefile import ReadCasefile
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Well:
|
|
15
|
+
"""A well containing one or more laterals.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
well_name: The name of the well.
|
|
19
|
+
well_number: The number of the well.
|
|
20
|
+
lateral_numbers: Numbers for each lateral.
|
|
21
|
+
active_laterals: List of laterals.
|
|
22
|
+
df_well_all_laterals: DataFrame containing all the laterals' well-layer data.
|
|
23
|
+
df_reservoir_all_laterals: DataFrame containing all the laterals' reservoir-layer data.
|
|
24
|
+
df_welsegs_header_all_laterals: DataFrame containing all the laterals' well-segments header data.
|
|
25
|
+
df_welsegs_content_all_laterals: DataFrame containing all the laterals' well-segments content.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
well_name: str
|
|
29
|
+
well_number: int
|
|
30
|
+
df_well_all_laterals: pd.DataFrame
|
|
31
|
+
df_reservoir_all_laterals: pd.DataFrame
|
|
32
|
+
lateral_numbers: npt.NDArray[np.int64]
|
|
33
|
+
active_laterals: list[Lateral]
|
|
34
|
+
df_welsegs_header_all_laterals: pd.DataFrame
|
|
35
|
+
df_welsegs_content_all_laterals: pd.DataFrame
|
|
36
|
+
|
|
37
|
+
def __init__(self, well_name: str, well_number: int, case: ReadCasefile, well_data: WellData):
|
|
38
|
+
"""Create well.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
well_name: Well name.
|
|
42
|
+
case: Data from the case file.
|
|
43
|
+
well_data: Data from the schedule file.
|
|
44
|
+
"""
|
|
45
|
+
# Note: Important to run this check before creating wells as it fixes potential problems with cases.
|
|
46
|
+
case.check_input(well_name, well_data)
|
|
47
|
+
self.well_name = well_name
|
|
48
|
+
self.well_number = well_number
|
|
49
|
+
|
|
50
|
+
lateral_numbers = self._get_active_laterals(well_name, case.completion_table)
|
|
51
|
+
self.active_laterals = [Lateral(num, well_name, case, well_data) for num in lateral_numbers]
|
|
52
|
+
|
|
53
|
+
self.df_well_all_laterals = pd.DataFrame()
|
|
54
|
+
self.df_reservoir_all_laterals = pd.DataFrame()
|
|
55
|
+
self.df_well_all_laterals = pd.concat([lateral.df_well for lateral in self.active_laterals], sort=False)
|
|
56
|
+
self.df_reservoir_all_laterals = pd.concat(
|
|
57
|
+
[lateral.df_reservoir for lateral in self.active_laterals], sort=False
|
|
58
|
+
)
|
|
59
|
+
self.df_welsegs_header_all_laterals = pd.concat(
|
|
60
|
+
[lateral.df_welsegs_header for lateral in self.active_laterals], sort=False
|
|
61
|
+
)
|
|
62
|
+
self.df_welsegs_content_all_laterals = pd.concat(
|
|
63
|
+
[lateral.df_welsegs_content for lateral in self.active_laterals], sort=False
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def _get_active_laterals(well_name: str, df_completion: pd.DataFrame) -> npt.NDArray[np.int_]:
|
|
68
|
+
"""Get a list of lateral numbers for the well.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
well_name: The well name.
|
|
72
|
+
df_completion: The completion information from case data.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
The active laterals.
|
|
76
|
+
"""
|
|
77
|
+
return np.array(df_completion[df_completion[Headers.WELL] == well_name][Headers.BRANCH].unique())
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class Lateral:
|
|
81
|
+
"""Lateral containing data related to a specific well's branch.
|
|
82
|
+
|
|
83
|
+
Attributes:
|
|
84
|
+
lateral_number: Current lateral number.
|
|
85
|
+
df_completion: Completion data.
|
|
86
|
+
df_welsegs_header: Header for welsegs.
|
|
87
|
+
df_welsegs_content: Content for welsegs.
|
|
88
|
+
df_reservoir_header: Reservoir header data.
|
|
89
|
+
df_measured_true_vertical_depth: Data for measured and true vertical depths.
|
|
90
|
+
df_tubing: Data for tubing segments.
|
|
91
|
+
df_well: Data for well-layer.
|
|
92
|
+
df_reservoir: Data for reservoir-layer.
|
|
93
|
+
df_tubing: Tubing data.
|
|
94
|
+
df_device: Device data.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
lateral_number: int
|
|
98
|
+
df_completion: pd.DataFrame
|
|
99
|
+
df_welsegs_header: pd.DataFrame
|
|
100
|
+
df_welsegs_content: pd.DataFrame
|
|
101
|
+
df_reservoir_header: pd.DataFrame
|
|
102
|
+
df_measured_true_vertical_depth: pd.DataFrame
|
|
103
|
+
df_well: pd.DataFrame
|
|
104
|
+
df_reservoir: pd.DataFrame
|
|
105
|
+
df_tubing: pd.DataFrame
|
|
106
|
+
df_device: pd.DataFrame
|
|
107
|
+
|
|
108
|
+
def __init__(self, lateral_number: int, well_name: str, case: ReadCasefile, well_data: WellData):
|
|
109
|
+
"""Create Lateral.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
lateral_number: Number of the current lateral/branch.
|
|
113
|
+
well_name: The well's name.
|
|
114
|
+
case: The case data.
|
|
115
|
+
well_data: This wells' schedule data.
|
|
116
|
+
"""
|
|
117
|
+
self.lateral_number = lateral_number
|
|
118
|
+
self.df_completion = case.get_completion(well_name, lateral_number)
|
|
119
|
+
self.df_welsegs_header, self.df_welsegs_content = read_schedule.get_well_segments(well_data, lateral_number)
|
|
120
|
+
|
|
121
|
+
self.df_device = pd.DataFrame()
|
|
122
|
+
|
|
123
|
+
self.df_reservoir = self._select_well(well_name, well_data, lateral_number)
|
|
124
|
+
self.df_measured_true_vertical_depth = completion.well_trajectory(
|
|
125
|
+
self.df_welsegs_header, self.df_welsegs_content
|
|
126
|
+
)
|
|
127
|
+
self.df_completion = completion.define_annulus_zone(self.df_completion)
|
|
128
|
+
self.df_tubing = self._create_tubing_segments(
|
|
129
|
+
self.df_reservoir, self.df_completion, self.df_measured_true_vertical_depth, case
|
|
130
|
+
)
|
|
131
|
+
self.df_tubing = completion.insert_missing_segments(self.df_tubing, well_name)
|
|
132
|
+
self.df_well = completion.complete_the_well(self.df_tubing, self.df_completion, case.joint_length)
|
|
133
|
+
self.df_well = self._get_devices(self.df_completion, self.df_well, case)
|
|
134
|
+
self.df_well = completion.correct_annulus_zone(self.df_well)
|
|
135
|
+
self.df_reservoir = self._connect_cells_to_segments(
|
|
136
|
+
self.df_reservoir, self.df_well, self.df_tubing, case.method
|
|
137
|
+
)
|
|
138
|
+
self.df_well[Headers.WELL] = well_name
|
|
139
|
+
self.df_reservoir[Headers.WELL] = well_name
|
|
140
|
+
self.df_well[Headers.LATERAL] = lateral_number
|
|
141
|
+
self.df_reservoir[Headers.LATERAL] = lateral_number
|
|
142
|
+
|
|
143
|
+
@staticmethod
|
|
144
|
+
def _select_well(well_name: str, well_data: WellData, lateral: int) -> pd.DataFrame:
|
|
145
|
+
"""Filter the reservoir data for this well and its laterals.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
well_name: The name of the well.
|
|
149
|
+
well_data: Multisegmented well segment data.
|
|
150
|
+
lateral: The lateral number.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Filtered reservoir data.
|
|
154
|
+
"""
|
|
155
|
+
df_compsegs = read_schedule.get_completion_segments(well_data, well_name, lateral)
|
|
156
|
+
df_compdat = read_schedule.get_completion_data(well_data)
|
|
157
|
+
df_reservoir = pd.merge(df_compsegs, df_compdat, how="inner", on=[Headers.I, Headers.J, Headers.K])
|
|
158
|
+
|
|
159
|
+
# Remove WELL column in the df_reservoir.
|
|
160
|
+
df_reservoir = df_reservoir.drop([Headers.WELL], axis=1)
|
|
161
|
+
# If multiple occurrences of same IJK in compdat/compsegs --> keep the last one.
|
|
162
|
+
df_reservoir = df_reservoir.drop_duplicates(subset=Headers.START_MEASURED_DEPTH, keep="last")
|
|
163
|
+
return df_reservoir.reset_index()
|
|
164
|
+
|
|
165
|
+
@staticmethod
|
|
166
|
+
def _connect_cells_to_segments(
|
|
167
|
+
df_reservoir: pd.DataFrame, df_well: pd.DataFrame, df_tubing_segments: pd.DataFrame, method: Method
|
|
168
|
+
) -> pd.DataFrame:
|
|
169
|
+
"""Connect cells to the well.
|
|
170
|
+
|
|
171
|
+
Notes:
|
|
172
|
+
Only some columns from well DataFrame are needed: MEASURED_DEPTH, NDEVICES, DEVICETYPE, and ANNULUS_ZONE.
|
|
173
|
+
ICV placement forces different methods in segment creation as USER defined.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
df_reservoir: Reservoir data.
|
|
177
|
+
df_well: Well data.
|
|
178
|
+
df_tubing_segments: Tubing information.
|
|
179
|
+
method: The method to use for creating segments.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Reservoir data with additional info on connected cells.
|
|
183
|
+
"""
|
|
184
|
+
# drop BRANCH column, not needed
|
|
185
|
+
df_reservoir = df_reservoir.drop([Headers.BRANCH], axis=1)
|
|
186
|
+
icv_device = (
|
|
187
|
+
df_well[Headers.DEVICE_TYPE].nunique() > 1
|
|
188
|
+
and (df_well[Headers.DEVICE_TYPE] == Content.INFLOW_CONTROL_VALVE).any()
|
|
189
|
+
and not df_well[Headers.NUMBER_OF_DEVICES].empty
|
|
190
|
+
)
|
|
191
|
+
method = Method.USER if icv_device else method
|
|
192
|
+
df_well = df_well[
|
|
193
|
+
[Headers.TUBING_MEASURED_DEPTH, Headers.NUMBER_OF_DEVICES, Headers.DEVICE_TYPE, Headers.ANNULUS_ZONE]
|
|
194
|
+
]
|
|
195
|
+
return completion.connect_cells_to_segments(df_well, df_reservoir, df_tubing_segments, method)
|
|
196
|
+
|
|
197
|
+
@staticmethod
|
|
198
|
+
def _get_devices(df_completion: pd.DataFrame, df_well: pd.DataFrame, case: ReadCasefile) -> pd.DataFrame:
|
|
199
|
+
"""Complete the well with the device information.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
df_completion: Completion information.
|
|
203
|
+
df_well: Well data.
|
|
204
|
+
case: Case data.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Well data with device information.
|
|
208
|
+
"""
|
|
209
|
+
if not case.completion_icv_tubing.empty:
|
|
210
|
+
active_devices = pd.concat(
|
|
211
|
+
[df_completion[Headers.DEVICE_TYPE], case.completion_icv_tubing[Headers.DEVICE_TYPE]]
|
|
212
|
+
).unique()
|
|
213
|
+
else:
|
|
214
|
+
active_devices = df_completion[Headers.DEVICE_TYPE].unique()
|
|
215
|
+
if Content.VALVE in active_devices:
|
|
216
|
+
df_well = completion.get_device(df_well, case.wsegvalv_table, Content.VALVE)
|
|
217
|
+
if Content.INFLOW_CONTROL_DEVICE in active_devices:
|
|
218
|
+
df_well = completion.get_device(df_well, case.wsegsicd_table, Content.INFLOW_CONTROL_DEVICE)
|
|
219
|
+
if Content.AUTONOMOUS_INFLOW_CONTROL_DEVICE in active_devices:
|
|
220
|
+
df_well = completion.get_device(df_well, case.wsegaicd_table, Content.AUTONOMOUS_INFLOW_CONTROL_DEVICE)
|
|
221
|
+
if Content.DENSITY_ACTIVATED_RECOVERY in active_devices:
|
|
222
|
+
df_well = completion.get_device(df_well, case.wsegdar_table, Content.DENSITY_ACTIVATED_RECOVERY)
|
|
223
|
+
if Content.AUTONOMOUS_INFLOW_CONTROL_VALVE in active_devices:
|
|
224
|
+
df_well = completion.get_device(df_well, case.wsegaicv_table, Content.AUTONOMOUS_INFLOW_CONTROL_VALVE)
|
|
225
|
+
if Content.INFLOW_CONTROL_VALVE in active_devices:
|
|
226
|
+
df_well = completion.get_device(df_well, case.wsegicv_table, Content.INFLOW_CONTROL_VALVE)
|
|
227
|
+
return df_well
|
|
228
|
+
|
|
229
|
+
@staticmethod
|
|
230
|
+
def _create_tubing_segments(
|
|
231
|
+
df_reservoir: pd.DataFrame, df_completion: pd.DataFrame, df_mdtvd: pd.DataFrame, case: ReadCasefile
|
|
232
|
+
) -> pd.DataFrame:
|
|
233
|
+
"""Create tubing segments based on the method and presence of Inflow Control Valves (ICVs).
|
|
234
|
+
|
|
235
|
+
The behavior of the df_tubing_segments will vary depending on the existence of the ICV keyword.
|
|
236
|
+
When the ICV keyword is present, it always creates a lumped tubing segment on its interval,
|
|
237
|
+
whereas other types of devices follow the default input.
|
|
238
|
+
If there is a combination of ICV and other devices (with devicetype > 1),
|
|
239
|
+
it results in a combination of ICV segment length with segment lumping,
|
|
240
|
+
and default segment length on other devices.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
df_reservoir: Reservoir data.
|
|
244
|
+
df_completion: Completion information.
|
|
245
|
+
df_mdtvd: Measured and true vertical depths.
|
|
246
|
+
case: Case data, including the Method used to create segments.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
Tubing data.
|
|
250
|
+
"""
|
|
251
|
+
df_tubing_segments_cells = completion.create_tubing_segments(
|
|
252
|
+
df_reservoir, df_completion, df_mdtvd, case.method, case.segment_length, case.minimum_segment_length
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
df_tubing_segments_user = completion.create_tubing_segments(
|
|
256
|
+
df_reservoir, df_completion, df_mdtvd, Method.USER, case.segment_length, case.minimum_segment_length
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
if (pd.unique(df_completion[Headers.DEVICE_TYPE]).size > 1) & (
|
|
260
|
+
(df_completion[Headers.DEVICE_TYPE] == Content.INFLOW_CONTROL_VALVE)
|
|
261
|
+
& (df_completion[Headers.VALVES_PER_JOINT] > 0)
|
|
262
|
+
).any():
|
|
263
|
+
return read_schedule.fix_compsegs_by_priority(
|
|
264
|
+
df_completion, df_tubing_segments_cells, df_tubing_segments_user
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
# If all the devices are ICVs, lump the segments.
|
|
268
|
+
if (df_completion[Headers.DEVICE_TYPE] == Content.INFLOW_CONTROL_VALVE).all():
|
|
269
|
+
return df_tubing_segments_user
|
|
270
|
+
# If none of the devices are ICVs use defined method.
|
|
271
|
+
return df_tubing_segments_cells
|
|
@@ -1,29 +1,28 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: completor
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: Advanced mulit-segmented well completion tool.
|
|
5
5
|
Home-page: https://github.com/equinor/completor
|
|
6
6
|
License: LGPL-3.0-only
|
|
7
7
|
Author: Equinor ASA
|
|
8
8
|
Author-email: opensource@equinor.com
|
|
9
|
-
Requires-Python: >=3.
|
|
9
|
+
Requires-Python: >=3.11,<3.12
|
|
10
10
|
Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)
|
|
11
11
|
Classifier: Natural Language :: English
|
|
12
12
|
Classifier: Operating System :: OS Independent
|
|
13
13
|
Classifier: Programming Language :: Python :: 3
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
16
14
|
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
18
15
|
Provides-Extra: ert
|
|
19
16
|
Requires-Dist: docutils (==0.20.1) ; extra == "ert"
|
|
20
|
-
Requires-Dist: ert (>=
|
|
21
|
-
Requires-Dist: matplotlib (>=3.
|
|
22
|
-
Requires-Dist: numpy (
|
|
23
|
-
Requires-Dist: pandas (
|
|
17
|
+
Requires-Dist: ert (>=11.1.0,<12.0.0) ; extra == "ert"
|
|
18
|
+
Requires-Dist: matplotlib (>=3.9.2,<4.0.0)
|
|
19
|
+
Requires-Dist: numpy (<2.0.0)
|
|
20
|
+
Requires-Dist: pandas (>=2.2.3,<3.0.0)
|
|
24
21
|
Requires-Dist: pyqt5-qt5 (==5.15.2) ; extra == "ert"
|
|
22
|
+
Requires-Dist: pytest-env (>=1.1.4,<2.0.0)
|
|
25
23
|
Requires-Dist: rstcheck-core (>=1.2.1,<2.0.0) ; extra == "ert"
|
|
26
|
-
Requires-Dist: scipy (>=1.
|
|
24
|
+
Requires-Dist: scipy (>=1.14.1,<2.0.0)
|
|
25
|
+
Requires-Dist: tqdm (>=4.66.5,<5.0.0)
|
|
27
26
|
Project-URL: Bug Tracker, https://github.com/equinor/completor/issues
|
|
28
27
|
Project-URL: Documentation, https://equinor.github.io/completor
|
|
29
28
|
Project-URL: Repository, https://github.com/equinor/completor
|
|
@@ -43,7 +42,7 @@ Detailed documentation for usage of completor can be found at https://equinor.gi
|
|
|
43
42
|
## Getting started as a user
|
|
44
43
|
|
|
45
44
|
### Prerequisites
|
|
46
|
-
* [Python](https://www.python.org/), version 3.
|
|
45
|
+
* [Python](https://www.python.org/), version 3.11
|
|
47
46
|
* [ERT](https://github.com/equinor/ert) (optional, and only available on Linux.)
|
|
48
47
|
|
|
49
48
|
### Installation
|
|
@@ -84,7 +83,7 @@ and making a pull request.
|
|
|
84
83
|
See [Contribution Document](documentation/docs/contribution_guide.mdx) on how to contribute.
|
|
85
84
|
|
|
86
85
|
### Install completor as dev
|
|
87
|
-
In order to run Completor® you need to have versions of [Python 3.
|
|
86
|
+
In order to run Completor® you need to have versions of [Python 3.11](https://www.python.org/downloads/) installed.
|
|
88
87
|
#### Source code
|
|
89
88
|
Clone the [Completor® repository](https://github.com/equinor/completor) to get the source code.
|
|
90
89
|
```bash
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
completor/__init__.py,sha256=k6amf7jhp7KkBIlaw93-NZITxyZjPSzA5McFAZh8yv8,76
|
|
2
|
+
completor/completion.py,sha256=COtRZpEdmuf3X_uOvEYYLz428nQSn1Z8QH-7TTREyus,30485
|
|
3
|
+
completor/config_jobs/run_completor,sha256=XePKj2xocfGF0XFRqr7sqfpZGrjgWcfaZLHIhVvGFCQ,600
|
|
4
|
+
completor/constants.py,sha256=Th7fzID5NPUhyV2RRc5h9qRQ963cKGT3TVCX_bsAtHA,12802
|
|
5
|
+
completor/create_output.py,sha256=8DGrrUKHLMUz3k9hrcEMnyAWzKR04KrT4HkAR3tVZKA,23191
|
|
6
|
+
completor/exceptions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
completor/exceptions/clean_exceptions.py,sha256=uQ2jgGfvUDAoS56JBWh-8ZCPIMB4hvYxLF-snA-_-pg,373
|
|
8
|
+
completor/exceptions/exceptions.py,sha256=eS00mVzmC4gQ7MUyGSknvKotmo2XzNGyp007mhjLMy0,5081
|
|
9
|
+
completor/get_version.py,sha256=LMzybu7m-O203lUSZvyu86J4bMobbjE9A23fWLDlxaM,168
|
|
10
|
+
completor/hook_implementations/jobs.py,sha256=8Ao8UadfJzEY7jWzj6C_OXm18fHmfXrbCB2_jS7KHiU,2180
|
|
11
|
+
completor/input_validation.py,sha256=OGY5oCdctlqzeg5Z2l4cmRb3ITtSjLp1Ufp0Joj7aKA,13495
|
|
12
|
+
completor/launch_args_parser.py,sha256=gb3FcyufZlRnKS3BZkFmgVH1VoSxMD0MbCLsHZKmz4c,1413
|
|
13
|
+
completor/logger.py,sha256=gYDbPL8ca5qT_MqYlDKEMKcSfIXW_59QklX8Gss5b2U,4784
|
|
14
|
+
completor/main.py,sha256=_McIv2fSVROBV1JarPLxoOX9OA6ILIh72zwqedHwosQ,9037
|
|
15
|
+
completor/parse.py,sha256=EGlt9CgkrXPqa7woyWQ5t_nh6OWsFrc2SJr8aFr_KsQ,20133
|
|
16
|
+
completor/prepare_outputs.py,sha256=fenEo3UfDzXF4jaxi9vDI-g-BkYSti0LxILJf7lO84A,72081
|
|
17
|
+
completor/read_casefile.py,sha256=ZJj-xrpcTeagJmqqi72UIOIcq9uQJaEZClbCA1lW4yw,34081
|
|
18
|
+
completor/read_schedule.py,sha256=IYyCubOggFGg664h1flTl7MUJhJWyibr6JsptnURjUA,18101
|
|
19
|
+
completor/utils.py,sha256=6dvclzQ6yAvnqal6bzlE7C1iKWiT-sGjFCCu2tuMdG4,13542
|
|
20
|
+
completor/visualization.py,sha256=ObxThqIyW3fsvVIupxVsgySFI_54n_Di-HTzFtDMSgo,4580
|
|
21
|
+
completor/visualize_well.py,sha256=I2kp2rnM2eFV4RUWnTTysPTqQKNEBicF2S9Z3rrAsNA,8672
|
|
22
|
+
completor/wells.py,sha256=oj3e65tSIDFJEpfOKkr_j4H3RyFKpdjZBAcef7sUoaY,12124
|
|
23
|
+
completor-1.0.1.dist-info/LICENSE,sha256=46mU2C5kSwOnkqkw9XQAJlhBL2JAf1_uCD8lVcXyMRg,7652
|
|
24
|
+
completor-1.0.1.dist-info/METADATA,sha256=z4bPc8-m3xisqxBAAkvD30zuLRZCTDo1pDBk8sTSIrQ,7913
|
|
25
|
+
completor-1.0.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
26
|
+
completor-1.0.1.dist-info/entry_points.txt,sha256=co5L2_CC2QQWVdEALeMp-NIC4mx4nRpcLcvpVXMYdeI,106
|
|
27
|
+
completor-1.0.1.dist-info/RECORD,,
|
completor/create_wells.py
DELETED
|
@@ -1,314 +0,0 @@
|
|
|
1
|
-
"""Module for creating completion structure."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import numpy as np
|
|
6
|
-
import pandas as pd
|
|
7
|
-
|
|
8
|
-
from completor import completion
|
|
9
|
-
from completor.constants import Headers, Method
|
|
10
|
-
from completor.logger import logger
|
|
11
|
-
from completor.read_casefile import ReadCasefile
|
|
12
|
-
from completor.read_schedule import fix_compsegs_by_priority
|
|
13
|
-
|
|
14
|
-
try:
|
|
15
|
-
import numpy.typing as npt
|
|
16
|
-
except ImportError:
|
|
17
|
-
pass
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class CreateWells:
|
|
21
|
-
"""Class for creating well completion structure.
|
|
22
|
-
|
|
23
|
-
Args:
|
|
24
|
-
case: ReadCasefile class.
|
|
25
|
-
|
|
26
|
-
Attributes:
|
|
27
|
-
active_wells: Active wells defined in the case file.
|
|
28
|
-
method: Method for segment creation.
|
|
29
|
-
well_name: Well name (in loop).
|
|
30
|
-
laterals: List of lateral number of the well in loop.
|
|
31
|
-
df_completion: Completion data frame in loop.
|
|
32
|
-
df_reservoir: COMPDAT and COMPSEGS data frame fusion in loop.
|
|
33
|
-
df_welsegs_header: WELSEGS first record.
|
|
34
|
-
df_welsegs_content: WELSEGS second record.
|
|
35
|
-
df_mdtvd: Data frame of MD and TVD relationship.
|
|
36
|
-
df_tubing_segments: Tubing segment data frame.
|
|
37
|
-
df_well: Data frame after completion.
|
|
38
|
-
df_well_all: Data frame (df_well) for all laterals after completion.
|
|
39
|
-
df_reservoir_all: df_reservoir for all laterals.
|
|
40
|
-
"""
|
|
41
|
-
|
|
42
|
-
def __init__(self, case: ReadCasefile):
|
|
43
|
-
"""Initialize CreateWells."""
|
|
44
|
-
self.well_name: str | None = None
|
|
45
|
-
self.df_reservoir = pd.DataFrame()
|
|
46
|
-
self.df_mdtvd = pd.DataFrame()
|
|
47
|
-
self.df_completion = pd.DataFrame()
|
|
48
|
-
self.df_tubing_segments = pd.DataFrame()
|
|
49
|
-
self.df_well = pd.DataFrame()
|
|
50
|
-
self.df_compdat = pd.DataFrame()
|
|
51
|
-
self.df_well_all = pd.DataFrame()
|
|
52
|
-
self.df_reservoir_all = pd.DataFrame()
|
|
53
|
-
self.df_welsegs_header = pd.DataFrame()
|
|
54
|
-
self.df_welsegs_content = pd.DataFrame()
|
|
55
|
-
self.laterals: list[int] = []
|
|
56
|
-
|
|
57
|
-
self.case: ReadCasefile = case
|
|
58
|
-
self.active_wells = self._active_wells()
|
|
59
|
-
self.method = self._method()
|
|
60
|
-
|
|
61
|
-
def update(self, well_name: str, schedule: completion.WellSchedule) -> None:
|
|
62
|
-
"""Update class variables in CreateWells.
|
|
63
|
-
|
|
64
|
-
Args:
|
|
65
|
-
well_name: Well name.
|
|
66
|
-
schedule: ReadSchedule object.
|
|
67
|
-
"""
|
|
68
|
-
self.well_name = well_name
|
|
69
|
-
self._active_laterals()
|
|
70
|
-
for lateral in self.laterals:
|
|
71
|
-
self.select_well(schedule, lateral)
|
|
72
|
-
self.well_trajectory()
|
|
73
|
-
self.define_annulus_zone()
|
|
74
|
-
self.create_tubing_segments()
|
|
75
|
-
self.insert_missing_segments()
|
|
76
|
-
self.complete_the_well()
|
|
77
|
-
self.get_devices()
|
|
78
|
-
self.correct_annulus_zone()
|
|
79
|
-
self.connect_cells_to_segments()
|
|
80
|
-
self.add_well_lateral_column(lateral)
|
|
81
|
-
self.combine_df(lateral)
|
|
82
|
-
|
|
83
|
-
def _active_wells(self) -> npt.NDArray[np.unicode_]:
|
|
84
|
-
"""Get a list of active wells specified by users.
|
|
85
|
-
|
|
86
|
-
If the well has annulus content with gravel pack and the well is perforated,
|
|
87
|
-
Completor will not add a device layer.
|
|
88
|
-
Completor does nothing to gravel-packed perforated wells by default.
|
|
89
|
-
This behavior can be changed by setting the GP_PERF_DEVICELAYER keyword in the case file to true.
|
|
90
|
-
|
|
91
|
-
Returns:
|
|
92
|
-
Active wells.
|
|
93
|
-
"""
|
|
94
|
-
# Need to check completion of all wells in completion table to remove GP-PERF type wells
|
|
95
|
-
active_wells = list(set(self.case.completion_table[Headers.WELL]))
|
|
96
|
-
# We cannot update a list while iterating of it
|
|
97
|
-
for well_name in active_wells.copy():
|
|
98
|
-
# Annulus content of each well
|
|
99
|
-
ann_series = self.case.completion_table[self.case.completion_table[Headers.WELL] == well_name][
|
|
100
|
-
Headers.ANNULUS
|
|
101
|
-
]
|
|
102
|
-
type_series = self.case.completion_table[self.case.completion_table[Headers.WELL] == well_name][
|
|
103
|
-
Headers.DEVICE_TYPE
|
|
104
|
-
]
|
|
105
|
-
gp_check = not ann_series.isin(["OA"]).any()
|
|
106
|
-
perf_check = not type_series.isin(["AICD", "AICV", "DAR", "ICD", "VALVE", "ICV"]).any()
|
|
107
|
-
if gp_check and perf_check and not self.case.gp_perf_devicelayer:
|
|
108
|
-
# De-activate wells with GP_PERF if instructed to do so:
|
|
109
|
-
active_wells.remove(well_name)
|
|
110
|
-
if not active_wells:
|
|
111
|
-
logger.warning(
|
|
112
|
-
"There are no active wells for Completor to work on. E.g. all wells are defined with Gravel Pack "
|
|
113
|
-
"(GP) and valve type PERF. If you want these wells to be active set GP_PERF_DEVICELAYER to TRUE."
|
|
114
|
-
)
|
|
115
|
-
return np.array([])
|
|
116
|
-
return np.array(active_wells)
|
|
117
|
-
|
|
118
|
-
def _method(self) -> Method:
|
|
119
|
-
"""Define how the user wants to create segments.
|
|
120
|
-
|
|
121
|
-
Returns:
|
|
122
|
-
Creation method enum.
|
|
123
|
-
|
|
124
|
-
Raises:
|
|
125
|
-
ValueError: If method is not one of the defined methods.
|
|
126
|
-
"""
|
|
127
|
-
if isinstance(self.case.segment_length, float):
|
|
128
|
-
if float(self.case.segment_length) > 0.0:
|
|
129
|
-
return Method.FIX
|
|
130
|
-
if float(self.case.segment_length) == 0.0:
|
|
131
|
-
return Method.CELLS
|
|
132
|
-
if self.case.segment_length < 0.0:
|
|
133
|
-
return Method.USER
|
|
134
|
-
else:
|
|
135
|
-
raise ValueError(
|
|
136
|
-
f"Unrecognized method '{self.case.segment_length}' in SEGMENTLENGTH keyword. "
|
|
137
|
-
"The value should be one of: 'WELSEGS', 'CELLS', 'USER', or a number: -1 for 'USER', "
|
|
138
|
-
"0 for 'CELLS', or a positive number for 'FIX'."
|
|
139
|
-
)
|
|
140
|
-
|
|
141
|
-
elif isinstance(self.case.segment_length, str):
|
|
142
|
-
if "welsegs" in self.case.segment_length.lower() or "infill" in self.case.segment_length.lower():
|
|
143
|
-
return Method.WELSEGS
|
|
144
|
-
if "cell" in self.case.segment_length.lower():
|
|
145
|
-
return Method.CELLS
|
|
146
|
-
if "user" in self.case.segment_length.lower():
|
|
147
|
-
return Method.USER
|
|
148
|
-
else:
|
|
149
|
-
raise ValueError(
|
|
150
|
-
f"Unrecognized method '{self.case.segment_length}' in SEGMENTLENGTH keyword. "
|
|
151
|
-
"The value should be one of: "
|
|
152
|
-
"'WELSEGS', 'CELLS', 'USER', or a number: -1 for 'USER', 0 for 'CELLS', positive number for 'FIX'."
|
|
153
|
-
)
|
|
154
|
-
else:
|
|
155
|
-
raise ValueError(
|
|
156
|
-
f"Unrecognized type of '{self.case.segment_length}' in SEGMENTLENGTH keyword. "
|
|
157
|
-
"The keyword must either be float or string."
|
|
158
|
-
)
|
|
159
|
-
|
|
160
|
-
def _active_laterals(self) -> None:
|
|
161
|
-
"""Get a list of lateral numbers for the well.
|
|
162
|
-
|
|
163
|
-
``get_active_laterals`` uses the case class DataFrame property
|
|
164
|
-
``completion_table`` with a format as shown in the function
|
|
165
|
-
``read_casefile.ReadCasefile.read_completion``.
|
|
166
|
-
"""
|
|
167
|
-
self.laterals = list(
|
|
168
|
-
self.case.completion_table[self.case.completion_table[Headers.WELL] == self.well_name][
|
|
169
|
-
Headers.BRANCH
|
|
170
|
-
].unique()
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
def select_well(self, schedule: completion.WellSchedule, lateral: int) -> None:
|
|
174
|
-
"""Filter all the required DataFrames for this well and its laterals."""
|
|
175
|
-
if self.well_name is None:
|
|
176
|
-
raise ValueError("No well name given")
|
|
177
|
-
|
|
178
|
-
self.df_completion = self.case.get_completion(self.well_name, lateral)
|
|
179
|
-
self.df_welsegs_header, self.df_welsegs_content = schedule.get_well_segments(self.well_name, lateral)
|
|
180
|
-
df_compsegs = schedule.get_compsegs(self.well_name, lateral)
|
|
181
|
-
df_compdat = schedule.get_compdat(self.well_name)
|
|
182
|
-
self.df_reservoir = pd.merge(df_compsegs, df_compdat, how="inner", on=[Headers.I, Headers.J, Headers.K])
|
|
183
|
-
|
|
184
|
-
# Remove WELL column in the df_reservoir.
|
|
185
|
-
self.df_reservoir.drop([Headers.WELL], inplace=True, axis=1)
|
|
186
|
-
# If multiple occurrences of same IJK in compdat/compsegs --> keep the last one.
|
|
187
|
-
self.df_reservoir.drop_duplicates(subset=Headers.START_MEASURED_DEPTH, keep="last", inplace=True)
|
|
188
|
-
self.df_reservoir.reset_index(inplace=True)
|
|
189
|
-
|
|
190
|
-
def well_trajectory(self) -> None:
|
|
191
|
-
"""Create trajectory DataFrame relations between measured depth and true vertical depth."""
|
|
192
|
-
self.df_mdtvd = completion.well_trajectory(self.df_welsegs_header, self.df_welsegs_content)
|
|
193
|
-
|
|
194
|
-
def define_annulus_zone(self) -> None:
|
|
195
|
-
"""Define an annulus zone if specified."""
|
|
196
|
-
self.df_completion = completion.define_annulus_zone(self.df_completion)
|
|
197
|
-
|
|
198
|
-
def create_tubing_segments(self) -> None:
|
|
199
|
-
"""Create tubing segments as the basis.
|
|
200
|
-
|
|
201
|
-
The function creates a class property DataFrame df_tubing_segments
|
|
202
|
-
from the class property DataFrames df_reservoir, df_completion, and df_mdtvd.
|
|
203
|
-
|
|
204
|
-
The behavior of the df_tubing_segments will vary depending on the existence of the ICV keyword.
|
|
205
|
-
When the ICV keyword is present, it always creates a lumped tubing segment on its interval,
|
|
206
|
-
whereas other types of devices follow the default input.
|
|
207
|
-
If there is a combination of an ICV and other devices (with devicetype >1),
|
|
208
|
-
this results in a combination of ICV segment length with segment lumping,
|
|
209
|
-
and default segment length on other devices.
|
|
210
|
-
"""
|
|
211
|
-
df_tubing_cells = completion.create_tubing_segments(
|
|
212
|
-
self.df_reservoir,
|
|
213
|
-
self.df_completion,
|
|
214
|
-
self.df_mdtvd,
|
|
215
|
-
self.method,
|
|
216
|
-
self.case.segment_length,
|
|
217
|
-
self.case.minimum_segment_length,
|
|
218
|
-
)
|
|
219
|
-
|
|
220
|
-
df_tubing_user = completion.create_tubing_segments(
|
|
221
|
-
self.df_reservoir,
|
|
222
|
-
self.df_completion,
|
|
223
|
-
self.df_mdtvd,
|
|
224
|
-
Method.USER,
|
|
225
|
-
self.case.segment_length,
|
|
226
|
-
self.case.minimum_segment_length,
|
|
227
|
-
)
|
|
228
|
-
|
|
229
|
-
if (len(self.df_completion[Headers.DEVICE_TYPE].unique()) > 1) & (
|
|
230
|
-
(self.df_completion[Headers.DEVICE_TYPE] == "ICV") & (self.df_completion[Headers.VALVES_PER_JOINT] > 0)
|
|
231
|
-
).any():
|
|
232
|
-
self.df_tubing_segments = fix_compsegs_by_priority(self.df_completion, df_tubing_cells, df_tubing_user)
|
|
233
|
-
|
|
234
|
-
# If all the devices are ICVs, lump the segments.
|
|
235
|
-
elif (self.df_completion[Headers.DEVICE_TYPE] == "ICV").all():
|
|
236
|
-
self.df_tubing_segments = df_tubing_user
|
|
237
|
-
# If none of the devices are ICVs use defined method.
|
|
238
|
-
else:
|
|
239
|
-
self.df_tubing_segments = df_tubing_cells
|
|
240
|
-
|
|
241
|
-
def insert_missing_segments(self) -> None:
|
|
242
|
-
"""Create a placeholder segment for inactive cells."""
|
|
243
|
-
self.df_tubing_segments = completion.insert_missing_segments(self.df_tubing_segments, self.well_name)
|
|
244
|
-
|
|
245
|
-
def complete_the_well(self) -> None:
|
|
246
|
-
"""Complete the well with users' completion design."""
|
|
247
|
-
self.df_well = completion.complete_the_well(self.df_tubing_segments, self.df_completion, self.case.joint_length)
|
|
248
|
-
self.df_well[Headers.ROUGHNESS] = self.df_well[Headers.ROUGHNESS].apply(lambda x: f"{x:.3E}")
|
|
249
|
-
|
|
250
|
-
def get_devices(self) -> None:
|
|
251
|
-
"""Complete the well with the device information.
|
|
252
|
-
|
|
253
|
-
Updates the class property DataFrame df_well described in ``complete_the_well``.
|
|
254
|
-
"""
|
|
255
|
-
if not self.case.completion_icv_tubing.empty:
|
|
256
|
-
active_devices = pd.concat(
|
|
257
|
-
[self.df_completion[Headers.DEVICE_TYPE], self.case.completion_icv_tubing[Headers.DEVICE_TYPE]]
|
|
258
|
-
).unique()
|
|
259
|
-
else:
|
|
260
|
-
active_devices = self.df_completion[Headers.DEVICE_TYPE].unique()
|
|
261
|
-
if "VALVE" in active_devices:
|
|
262
|
-
self.df_well = completion.get_device(self.df_well, self.case.wsegvalv_table, "VALVE")
|
|
263
|
-
if "ICD" in active_devices:
|
|
264
|
-
self.df_well = completion.get_device(self.df_well, self.case.wsegsicd_table, "ICD")
|
|
265
|
-
if "AICD" in active_devices:
|
|
266
|
-
self.df_well = completion.get_device(self.df_well, self.case.wsegaicd_table, "AICD")
|
|
267
|
-
if "DAR" in active_devices:
|
|
268
|
-
self.df_well = completion.get_device(self.df_well, self.case.wsegdar_table, "DAR")
|
|
269
|
-
if "AICV" in active_devices:
|
|
270
|
-
self.df_well = completion.get_device(self.df_well, self.case.wsegaicv_table, "AICV")
|
|
271
|
-
if "ICV" in active_devices:
|
|
272
|
-
self.df_well = completion.get_device(self.df_well, self.case.wsegicv_table, "ICV")
|
|
273
|
-
|
|
274
|
-
def correct_annulus_zone(self) -> None:
|
|
275
|
-
"""Remove the annulus zone if there is no connection to the tubing."""
|
|
276
|
-
self.df_well = completion.correct_annulus_zone(self.df_well)
|
|
277
|
-
|
|
278
|
-
def connect_cells_to_segments(self) -> None:
|
|
279
|
-
"""Connect cells to the well.
|
|
280
|
-
|
|
281
|
-
We only need the following columns from the well DataFrame: MD, NDEVICES, DEVICETYPE, and ANNULUS_ZONE.
|
|
282
|
-
|
|
283
|
-
ICV placement forces different methods in segment creation as USER defined.
|
|
284
|
-
"""
|
|
285
|
-
# drop BRANCH column, not needed
|
|
286
|
-
self.df_reservoir.drop([Headers.BRANCH], axis=1, inplace=True)
|
|
287
|
-
icv_device = (
|
|
288
|
-
self.df_well[Headers.DEVICE_TYPE].nunique() > 1
|
|
289
|
-
and (self.df_well[Headers.DEVICE_TYPE] == "ICV").any()
|
|
290
|
-
and not self.df_well[Headers.NUMBER_OF_DEVICES].empty
|
|
291
|
-
)
|
|
292
|
-
method = Method.USER if icv_device else self.method
|
|
293
|
-
self.df_reservoir = completion.connect_cells_to_segments(
|
|
294
|
-
self.df_well[[Headers.TUB_MD, Headers.NUMBER_OF_DEVICES, Headers.DEVICE_TYPE, Headers.ANNULUS_ZONE]],
|
|
295
|
-
self.df_reservoir,
|
|
296
|
-
self.df_tubing_segments,
|
|
297
|
-
method,
|
|
298
|
-
)
|
|
299
|
-
|
|
300
|
-
def add_well_lateral_column(self, lateral: int) -> None:
|
|
301
|
-
"""Add well and lateral column in df_well and df_compsegs."""
|
|
302
|
-
self.df_well[Headers.WELL] = self.well_name
|
|
303
|
-
self.df_reservoir[Headers.WELL] = self.well_name
|
|
304
|
-
self.df_well[Headers.LATERAL] = lateral
|
|
305
|
-
self.df_reservoir[Headers.LATERAL] = lateral
|
|
306
|
-
|
|
307
|
-
def combine_df(self, lateral: int) -> None:
|
|
308
|
-
"""Combine all DataFrames for this well."""
|
|
309
|
-
if lateral == self.laterals[0]:
|
|
310
|
-
self.df_well_all = self.df_well.copy(deep=True)
|
|
311
|
-
self.df_reservoir_all = self.df_reservoir.copy(deep=True)
|
|
312
|
-
else:
|
|
313
|
-
self.df_well_all = pd.concat([self.df_well_all, self.df_well], sort=False)
|
|
314
|
-
self.df_reservoir_all = pd.concat([self.df_reservoir_all, self.df_reservoir], sort=False)
|