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.
@@ -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,4 @@
1
+ from completor.exceptions.clean_exceptions import CompletorError
2
+ from completor.exceptions.exceptions import CaseReaderFormatError
3
+
4
+ __all__ = ["CompletorError", "CaseReaderFormatError"]
@@ -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}