completor 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- completor/__init__.py +3 -0
- completor/completion.py +1042 -0
- completor/config_jobs/run_completor +18 -0
- completor/constants.py +256 -0
- completor/create_output.py +462 -0
- completor/create_wells.py +314 -0
- completor/exceptions/__init__.py +4 -0
- completor/exceptions/clean_exceptions.py +10 -0
- completor/exceptions/exceptions.py +134 -0
- completor/hook_implementations/jobs.py +63 -0
- completor/input_validation.py +340 -0
- completor/launch_args_parser.py +41 -0
- completor/logger.py +138 -0
- completor/main.py +486 -0
- completor/parse.py +581 -0
- completor/prepare_outputs.py +1473 -0
- completor/pvt_model.py +14 -0
- completor/read_casefile.py +677 -0
- completor/read_schedule.py +160 -0
- completor/utils.py +185 -0
- completor/visualization.py +164 -0
- completor/visualize_well.py +217 -0
- completor-0.1.2.dist-info/LICENSE +165 -0
- completor-0.1.2.dist-info/METADATA +205 -0
- completor-0.1.2.dist-info/RECORD +27 -0
- completor-0.1.2.dist-info/WHEEL +4 -0
- completor-0.1.2.dist-info/entry_points.txt +6 -0
|
@@ -0,0 +1,314 @@
|
|
|
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)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Separate file for exceptions that don't need parser, this is needed to avoid circular imports from parser.py"""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class CompletorError(Exception):
|
|
5
|
+
"""Custom error for completor, if anything goes critically wrong in completor this error will be raised.
|
|
6
|
+
|
|
7
|
+
This error is caught in main initiating the abort function and will terminate the program.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
pass
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from completor import parse
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class _BaseCaseException(Exception):
|
|
7
|
+
"""Base for custom case exceptions."""
|
|
8
|
+
|
|
9
|
+
def __init__(
|
|
10
|
+
self, message: str, lines: list[str] | None = None, error_line: int | None = None, window_size: int = 5
|
|
11
|
+
):
|
|
12
|
+
"""Initialize the error message."""
|
|
13
|
+
if lines is None or error_line is None:
|
|
14
|
+
super().__init__(message)
|
|
15
|
+
else:
|
|
16
|
+
message = self._format_error_message(message, lines, error_line, window_size)
|
|
17
|
+
super().__init__(message)
|
|
18
|
+
|
|
19
|
+
def _format_error_message(self, message: str, original_lines: list[str], error_line: int, windows_size: int) -> str:
|
|
20
|
+
"""Format error message to show lines where the error occurred.
|
|
21
|
+
|
|
22
|
+
The error is displayed with line numbers and ">" to indicate
|
|
23
|
+
the line the error happened at. Then add the error message.
|
|
24
|
+
|
|
25
|
+
Format example:
|
|
26
|
+
Error at line 7:
|
|
27
|
+
Could not parse "KEYWORD".
|
|
28
|
+
5: KEYWORD
|
|
29
|
+
6: -- Some comment
|
|
30
|
+
> 7: data data something wrong /
|
|
31
|
+
8: data data /
|
|
32
|
+
9: /
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
message: Error message appended to the formatted error.
|
|
36
|
+
original_lines: Lines in the original file the error happened at.
|
|
37
|
+
error_line: What line the error occurred at.
|
|
38
|
+
windows_size: How many lines from the file around the error should be
|
|
39
|
+
displayed. (3=error_line-3:error_line+3).
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Error message formatted with lines from file where error occurred,
|
|
43
|
+
arrow pointing at error line and error message.
|
|
44
|
+
"""
|
|
45
|
+
# From 0 indexed to 1 indexed (like the actual file)
|
|
46
|
+
start = self._clamp((error_line - windows_size), 0, len(original_lines))
|
|
47
|
+
end = self._clamp((error_line + windows_size), 0, len(original_lines))
|
|
48
|
+
|
|
49
|
+
lines = original_lines[start:end].copy()
|
|
50
|
+
new_lines = []
|
|
51
|
+
error_line += 1
|
|
52
|
+
for i, line in enumerate(lines):
|
|
53
|
+
shifted_index = i + start + 1
|
|
54
|
+
if shifted_index == error_line:
|
|
55
|
+
new_lines.append(f"> {str(shifted_index).rjust(len(str(end)))}: {line}")
|
|
56
|
+
else:
|
|
57
|
+
new_lines.append(f" {str(shifted_index).rjust(len(str(end)))}: {line}")
|
|
58
|
+
|
|
59
|
+
string = "\n".join(new_lines)
|
|
60
|
+
return f"\n{string}\n\nError at line {error_line} in case file:\n{message}"
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def _clamp(n, minn, maxn):
|
|
64
|
+
"""Method for preventing index out of bounds."""
|
|
65
|
+
return max(min(maxn, n), minn)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class CaseReaderFormatError(_BaseCaseException):
|
|
69
|
+
"""Used for keywords with faulty data/format."""
|
|
70
|
+
|
|
71
|
+
error_index: int | None = None
|
|
72
|
+
|
|
73
|
+
def __init__(
|
|
74
|
+
self,
|
|
75
|
+
message: str | None = None,
|
|
76
|
+
lines: list[str] | None = None,
|
|
77
|
+
header: list[str] | None = None,
|
|
78
|
+
keyword: str | None = None,
|
|
79
|
+
error_index: int | None = None,
|
|
80
|
+
window_size: int = 5,
|
|
81
|
+
):
|
|
82
|
+
if message is None:
|
|
83
|
+
message = "Something went wrong while reading the casefile! "
|
|
84
|
+
|
|
85
|
+
if lines is None or header is None:
|
|
86
|
+
super().__init__(message)
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
extra_info = "few/many"
|
|
90
|
+
if error_index is None:
|
|
91
|
+
if keyword is None:
|
|
92
|
+
super().__init__(message)
|
|
93
|
+
return
|
|
94
|
+
try:
|
|
95
|
+
error_index, is_larger = CaseReaderFormatError.find_error_line(keyword, lines, header)
|
|
96
|
+
extra_info = "many" if is_larger else "few"
|
|
97
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
98
|
+
super().__init__(message)
|
|
99
|
+
|
|
100
|
+
message += (
|
|
101
|
+
f"Too {extra_info} entries in data for keyword '{keyword}', "
|
|
102
|
+
f"expected {len(header)} entries: {header}!\n"
|
|
103
|
+
"Please check the documentation for this keyword or "
|
|
104
|
+
"contact the team directly if you believe this to be a mistake."
|
|
105
|
+
)
|
|
106
|
+
super().__init__(message, lines, error_index, window_size)
|
|
107
|
+
|
|
108
|
+
@staticmethod
|
|
109
|
+
def find_error_line(keyword: str, lines: list[str], header: list[str]):
|
|
110
|
+
"""Find line where error occurs.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
keyword: Current keyword in case-file.
|
|
114
|
+
lines: The (preferably) original case-file lines.
|
|
115
|
+
header: The expected headers for this keyword.
|
|
116
|
+
|
|
117
|
+
Raises:
|
|
118
|
+
ValueError: If the line could not be found.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Line number and whether there are too many/few data entries vs header.
|
|
122
|
+
"""
|
|
123
|
+
stripped_content = [x.strip() for x in lines]
|
|
124
|
+
start, end = parse.locate_keyword(stripped_content, keyword)
|
|
125
|
+
|
|
126
|
+
line_content = {
|
|
127
|
+
i + start + 1: line for i, line in enumerate(lines[start + 1 : end]) if line and not line.startswith("--")
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
for line, content in line_content.items():
|
|
131
|
+
if len(content.strip().split()) != len(header):
|
|
132
|
+
return line, len(header) < len(content.strip().split())
|
|
133
|
+
|
|
134
|
+
raise ValueError("Could not find the erroneous line.")
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from pkg_resources import resource_filename
|
|
4
|
+
|
|
5
|
+
from completor.logger import logger
|
|
6
|
+
|
|
7
|
+
SKIP_TESTS = False
|
|
8
|
+
try:
|
|
9
|
+
from ert.shared.plugins.plugin_manager import hook_implementation # type: ignore
|
|
10
|
+
from ert.shared.plugins.plugin_response import plugin_response # type: ignore
|
|
11
|
+
|
|
12
|
+
except ModuleNotFoundError:
|
|
13
|
+
logger.warning("Cannot import ERT, did you install Completor with ert option enabled?")
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@hook_implementation
|
|
18
|
+
@plugin_response(plugin_name="completor") # type: ignore
|
|
19
|
+
def installable_jobs():
|
|
20
|
+
config_file = Path(resource_filename("completor", "config_jobs/run_completor"))
|
|
21
|
+
return {config_file.name: config_file}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@hook_implementation
|
|
25
|
+
@plugin_response(plugin_name="completor") # type: ignore # pylint: disable=no-value-for-parameter
|
|
26
|
+
def job_documentation(job_name):
|
|
27
|
+
if job_name != "run_completor":
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
description = """Completor is a script for modelling
|
|
31
|
+
wells with advanced completion.
|
|
32
|
+
It generates a well schedule to be included in reservoir simulator,
|
|
33
|
+
by combining the multi-segment tubing definition (from pre-processor reservoir modelling tools)
|
|
34
|
+
with a user defined file specifying the completion design.
|
|
35
|
+
The resulting well schedule comprises all keywords and parameters required by
|
|
36
|
+
reservoir simulator. See the Completor documentation for details.
|
|
37
|
+
|
|
38
|
+
Required:
|
|
39
|
+
---------
|
|
40
|
+
-i : followed by name of file specifying completion design (e.g. completion.case).
|
|
41
|
+
-s : followed by name of schedule file with multi-segment tubing definition,
|
|
42
|
+
including COMPDAT, COMPSEGS and WELSEGS (required if not specified in case file).
|
|
43
|
+
|
|
44
|
+
Optional:
|
|
45
|
+
---------
|
|
46
|
+
--help : how to run completor.
|
|
47
|
+
--about : about completor.
|
|
48
|
+
-o : followed by name of completor output file.
|
|
49
|
+
--figure : generates a pdf file with a schematics of the well segment structure.
|
|
50
|
+
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
examples = """.. code-block:: console
|
|
54
|
+
FORWARD_MODEL run_completor(
|
|
55
|
+
<CASE>=path/to/completion.case,
|
|
56
|
+
<INPUT_SCH>=path/to/input.sch,
|
|
57
|
+
<OUTPUT_SCH>path/to/output.sch
|
|
58
|
+
)
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
category = "modelling.reservoir"
|
|
62
|
+
|
|
63
|
+
return {"description": description, "examples": examples, "category": category}
|