completor 0.1.3__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.
@@ -5,458 +5,586 @@ from __future__ import annotations
5
5
  import getpass
6
6
  from datetime import datetime
7
7
 
8
- import matplotlib # type: ignore
8
+ import numpy as np
9
+ import numpy.typing as npt
10
+ import pandas as pd
11
+ from matplotlib.backends.backend_pdf import PdfPages # type: ignore
9
12
 
10
- from completor import prepare_outputs as po
11
- from completor.completion import WellSchedule
13
+ from completor import prepare_outputs
12
14
  from completor.constants import Headers, Keywords
13
- from completor.create_wells import CreateWells
15
+ from completor.exceptions import CompletorError
16
+ from completor.get_version import get_version
14
17
  from completor.logger import logger
15
- from completor.pvt_model import CORRELATION_UDQ
16
18
  from completor.read_casefile import ReadCasefile
17
19
  from completor.visualize_well import visualize_well
20
+ from completor.wells import Lateral, Well
18
21
 
19
22
 
20
- class CreateOutput:
21
- """Create output files from completor.
22
-
23
- There are two output files from completor:
24
- 1. Well schedule file (text file) for input to reservoir simulator.
25
- 2. Well diagram (pdf file), i.e. a well completion schematic.
23
+ def format_output(well: Well, case: ReadCasefile, figure_name: str | None = None) -> tuple[str, str, str, str]:
24
+ """Formats the finished output string to be written to a file.
26
25
 
27
26
  Args:
28
- case: ReadCasefile object.
29
- schedule: ReadSchedule object.
30
- wells: CreateWells object.
31
- well_name: Well name.
32
- iwell: Well number used in creating WSEGAICV and WSEGDAR output.
33
- version: Completor version information.
34
- show_figure: Flag for pdf export of well completion schematic.
35
- figure_no: Figure number.
36
- write_welsegs: Flag to write WELSEGS.
27
+ well: Well data.
28
+ case: Case data.
29
+ figure_name: The name of the figure, if None, no figure is printed. Defaults to None.
30
+
31
+ Returns:
32
+ Properly formatted output data for completion data, well segments, completion segments, and bonus.
37
33
  """
38
34
 
39
- def __init__(
40
- self,
41
- case: ReadCasefile,
42
- schedule: WellSchedule,
43
- wells: CreateWells,
44
- well_name: str,
45
- iwell: int,
46
- completor_version: str,
47
- show_figure: bool = False,
48
- figure_name: matplotlib.backends.backend_pdf.PdfPages | None = None, # type: ignore
49
- write_welsegs: bool = True,
50
- paths: tuple[str, str] | None = None,
51
- ):
52
- """Initialize CreateOutput class.
53
-
54
- Args:
55
- case: ReadCasefile object.
56
- schedule: ReadSchedule object.
57
- wells: CreateWells object.
58
- completor_version: Completor version information.
59
- figure_no: Must be set if show_figure.
60
- show_figure: True if the user wants to create well diagram file.
61
- """
62
- self.case = case
63
- self.schedule = schedule
64
- self.case_path: str | None
65
- self.schedule_path: str | None
66
- if paths:
67
- self.case_path, self.schedule_path = paths
68
- else:
69
- self.case_path = None
70
- self.schedule_path = None
71
- self.wells = wells
72
- self.well_name = well_name
73
- self.iwell = iwell
74
- self.version = completor_version
75
- self.write_welsegs = write_welsegs
76
- self.show_figure = show_figure
77
-
78
- # different line connection
79
- self.newline1 = "\n\n\n"
80
- self.newline2 = "\n/" + self.newline1
81
- self.newline3 = "/" + self.newline1
82
-
83
- # create final print
84
- self.header = self.make_completor_header()
85
-
86
- # Prints the UDQ statement if a PVT file and
87
- # PVT table are specified in the case file.
88
- self.print_udq = False
89
- self.udq_correlation = ""
90
- self.udq_parameter: dict[str, str] = {}
91
- if self.case.completion_table[Headers.DEVICE_TYPE].isin(["AICV"]).any():
92
- self.print_udq = True
93
- self.udq_correlation = CORRELATION_UDQ
94
-
95
- # Start printing all wells
96
- self.finalprint = self.header
97
- # print udq equation if relevant
98
- if self.print_udq:
99
- self.finalprint += self.udq_correlation
100
-
101
- self.df_reservoir = wells.df_reservoir_all[wells.df_reservoir_all[Headers.WELL] == self.well_name]
102
- self.df_well = wells.df_well_all[wells.df_well_all[Headers.WELL] == self.well_name]
103
- self.laterals = self.df_well[self.df_well[Headers.WELL] == self.well_name][Headers.LATERAL].unique()
104
-
105
- # Start printing per well.
106
- self.welsegs_header, _ = self.schedule.get_well_segments(self.well_name, branch=1)
107
- self.check_welsegs1()
108
- self.print_welsegs = f"{Keywords.WELSEGS}\n{po.dataframe_tostring(self.welsegs_header, True)}\n"
109
- self.print_welsegsinit = self.print_welsegs
110
- self.print_wseglink = f"{Keywords.WSEGLINK}\n"
111
- self.print_wseglinkinit = self.print_wseglink
112
- self.print_compsegs = f"{Keywords.COMPSEGS}\n'{self.well_name}' /\n"
113
- self.print_compsegsinit = self.print_compsegs
114
- self.print_compdat = f"{Keywords.COMPDAT}\n"
115
- self.print_compdatinit = self.print_compdat
116
- self.print_wsegvalv = f"{Keywords.WSEGVALV}\n"
117
- self.print_wsegvalvinit = self.print_wsegvalv
118
- self.print_wsegicv = f"{Keywords.WSEGVALV}\n"
119
- self.print_wsegicvinit = self.print_wsegicv
120
- self.print_wsegaicd = f"{Keywords.WSEGAICD}\n"
121
- self.print_wsegaicdinit = self.print_wsegaicd
122
- self.print_wsegsicd = f"{Keywords.WSEGSICD}\n"
123
- self.print_wsegsicdinit = self.print_wsegsicd
124
- self.print_wsegdar = f"""\
125
- {'-' * 100}
126
- -- This is how we model DAR technology using sets of ACTIONX keywords.
127
- -- The segment dP curves changes according to the segment water-
128
- -- and gas volume fractions at downhole condition.
129
- -- The value of Cv is adjusted according to the segment length and the number of
130
- -- devices per joint. The constriction area varies according to values of
131
- -- volume fractions.
132
- {"-" * 100}{self.newline1}"""
133
-
134
- self.print_wsegdarinit = self.print_wsegdar
135
- self.print_wsegaicv = f"""\
136
- {"-" * 100}
137
- -- This is how we model AICV technology using sets of ACTIONX keyword
138
- -- the DP parameters change according to the segment water cut (at downhole condition )
139
- -- and gas volume fraction (at downhole condition)
140
- {"-" * 100}{self.newline1}"""
141
-
142
- self.print_wsegaicvinit = self.print_wsegaicv
143
- self.start_segment = 2
144
- self.start_branch = 1
145
- # pre-preparations
146
- data = {} # just a container. need to to loop twice to make connect_lateral work
147
- for lateral in self.laterals:
148
- self.df_tubing, top = po.prepare_tubing_layer(
149
- self.schedule,
150
- self.well_name,
151
- lateral,
152
- self.df_well,
153
- self.start_segment,
154
- self.start_branch,
155
- self.case.completion_table,
156
- )
157
- self.df_device = po.prepare_device_layer(self.well_name, lateral, self.df_well, self.df_tubing)
158
- self.df_annulus, self.df_wseglink = po.prepare_annulus_layer(
159
- self.well_name, lateral, self.df_well, self.df_device
160
- )
161
- self.update_segmentbranch()
162
- self.check_segments(lateral)
163
- data[lateral] = (self.df_tubing, self.df_device, self.df_annulus, self.df_wseglink, top)
164
- # attach lateral to their proper segments (in overburden, potentially)
165
- for lateral in data:
166
- po.connect_lateral(self.well_name, lateral, data, self.case)
167
- # main preparations
168
- for lateral in self.laterals:
169
- self.df_tubing, self.df_device, self.df_annulus, self.df_wseglink = data[lateral][:4]
170
-
171
- self.branch_revision(lateral)
172
-
173
- completion_table_well = case.completion_table[case.completion_table[Headers.WELL] == self.well_name]
174
- completion_table_lateral = completion_table_well[completion_table_well[Headers.BRANCH] == lateral]
175
- self.df_compsegs = po.prepare_compsegs(
176
- self.well_name,
177
- lateral,
178
- self.df_reservoir,
179
- self.df_device,
180
- self.df_annulus,
181
- completion_table_lateral,
182
- self.case.segment_length,
35
+ completion_data_list = []
36
+ print_well_segments = ""
37
+ print_well_segments_link = ""
38
+ print_completion_segments = ""
39
+ print_valve = ""
40
+ print_inflow_control_valve = ""
41
+ print_autonomous_inflow_control_device = ""
42
+ print_inflow_control_device = ""
43
+ print_density_activated_recovery = ""
44
+ print_autonomous_inflow_control_valve = ""
45
+
46
+ start_segment = 2
47
+ start_branch = 1
48
+
49
+ header_written = False
50
+ first = True
51
+ for lateral in well.active_laterals:
52
+ _check_well_segments_header(
53
+ lateral.df_welsegs_header, well.df_reservoir_all_laterals[Headers.START_MEASURED_DEPTH].iloc[0]
54
+ )
55
+
56
+ if not header_written:
57
+ print_well_segments += (
58
+ f"{Keywords.WELL_SEGMENTS}\n{prepare_outputs.dataframe_tostring(lateral.df_welsegs_header, True)}"
183
59
  )
184
- self.df_compdat = po.prepare_compdat(self.well_name, lateral, self.df_reservoir, completion_table_lateral)
185
- self.df_wsegvalv = po.prepare_wsegvalv(self.well_name, lateral, self.df_well, self.df_device)
186
- self.df_wsegsicd = po.prepare_wsegsicd(self.well_name, lateral, self.df_well, self.df_device)
187
- self.df_wsegaicd = po.prepare_wsegaicd(self.well_name, lateral, self.df_well, self.df_device)
188
- self.df_wsegdar = po.prepare_wsegdar(self.well_name, lateral, self.df_well, self.df_device)
189
- self.df_wsegaicv = po.prepare_wsegaicv(self.well_name, lateral, self.df_well, self.df_device)
190
- self.df_wsegicv = po.prepare_wsegicv(
191
- self.well_name,
192
- lateral,
193
- self.df_well,
194
- self.df_device,
195
- self.df_tubing,
196
- self.case.completion_icv_tubing,
197
- self.case.wsegicv_table,
60
+ header_written = True
61
+
62
+ lateral.df_tubing, top = prepare_outputs.prepare_tubing_layer(
63
+ well, lateral, start_segment, start_branch, case.completion_table
64
+ )
65
+ lateral.df_device = prepare_outputs.prepare_device_layer(lateral.df_well, lateral.df_tubing)
66
+
67
+ if lateral.df_device.empty:
68
+ logger.warning(
69
+ "No connection from reservoir to tubing in Well : %s Lateral : %d",
70
+ well.well_name,
71
+ lateral.lateral_number,
198
72
  )
199
- self.make_compdat(lateral)
200
- self.make_welsegs(lateral)
201
- self.make_wseglink(lateral)
202
- self.make_compsegs(lateral)
203
- self.make_wsegvalv(lateral)
204
- self.make_wsegsicd(lateral)
205
- self.make_wsegaicd(lateral)
206
- self.make_wsegicv(lateral)
207
- self.make_wsegdar()
208
- self.make_wsegaicv()
209
-
210
- if show_figure and figure_name is not None:
211
- logger.info(f"Creating figure for lateral {lateral}.")
212
- figure_name.savefig(
213
- visualize_well(self.well_name, self.df_well, self.df_reservoir, self.case.segment_length),
73
+ df_annulus, df_well_segments_link = prepare_outputs.prepare_annulus_layer(
74
+ well.well_name, lateral.df_well, lateral.df_device
75
+ )
76
+ if df_annulus.empty:
77
+ logger.info("No annular flow in Well : %s Lateral : %d", well.well_name, lateral.lateral_number)
78
+
79
+ start_segment, start_branch = _update_segmentbranch(lateral.df_device, df_annulus)
80
+
81
+ lateral.df_tubing = _connect_lateral(well.well_name, lateral, top, well, case)
82
+
83
+ lateral.df_tubing[Headers.BRANCH] = lateral.lateral_number
84
+ active_laterals = [lateral.lateral_number for lateral in well.active_laterals]
85
+ lateral.df_device, df_annulus = _branch_revision(
86
+ lateral.lateral_number, active_laterals, lateral.df_device, df_annulus
87
+ )
88
+
89
+ completion_table_well = case.completion_table[case.completion_table[Headers.WELL] == well.well_name]
90
+ completion_table_lateral = completion_table_well[
91
+ completion_table_well[Headers.BRANCH] == lateral.lateral_number
92
+ ]
93
+ df_completion_segments = prepare_outputs.prepare_completion_segments(
94
+ well.well_name,
95
+ lateral.lateral_number,
96
+ well.df_reservoir_all_laterals,
97
+ lateral.df_device,
98
+ df_annulus,
99
+ completion_table_lateral,
100
+ case.segment_length,
101
+ )
102
+ df_completion_data = prepare_outputs.prepare_completion_data(
103
+ well.well_name, lateral.lateral_number, well.df_reservoir_all_laterals, completion_table_lateral
104
+ )
105
+ df_valve = prepare_outputs.prepare_valve(well.well_name, lateral.df_well, lateral.df_device)
106
+ df_inflow_control_device = prepare_outputs.prepare_inflow_control_device(
107
+ well.well_name, lateral.df_well, lateral.df_device
108
+ )
109
+ df_autonomous_inflow_control_device = prepare_outputs.prepare_autonomous_inflow_control_device(
110
+ well.well_name, lateral.df_well, lateral.df_device
111
+ )
112
+ df_density_activated_recovery = prepare_outputs.prepare_density_activated_recovery(
113
+ well.well_name, lateral.df_well, lateral.df_device
114
+ )
115
+ df_autonomous_inflow_control_valve = prepare_outputs.prepare_autonomous_inflow_control_valve(
116
+ well.well_name, lateral.df_well, lateral.df_device
117
+ )
118
+ df_inflow_control_valve = prepare_outputs.prepare_inflow_control_valve(
119
+ well.well_name,
120
+ lateral.lateral_number,
121
+ well.df_well_all_laterals,
122
+ lateral.df_device,
123
+ lateral.df_tubing,
124
+ case.completion_icv_tubing,
125
+ case.wsegicv_table,
126
+ )
127
+ completion_data_list.append(
128
+ _format_completion_data(well.well_name, lateral.lateral_number, df_completion_data, first)
129
+ )
130
+ print_well_segments += _format_well_segments(
131
+ well.well_name, lateral.lateral_number, lateral.df_tubing, lateral.df_device, df_annulus, first
132
+ )
133
+ print_well_segments_link += _format_well_segments_link(
134
+ well.well_name, lateral.lateral_number, df_well_segments_link, first
135
+ )
136
+ print_completion_segments += _format_completion_segments(
137
+ well.well_name, lateral.lateral_number, df_completion_segments, first
138
+ )
139
+ print_valve += _format_valve(well.well_name, lateral.lateral_number, df_valve, first)
140
+ print_inflow_control_device += _format_inflow_control_device(
141
+ well.well_name, lateral.lateral_number, df_inflow_control_device, first
142
+ )
143
+ print_autonomous_inflow_control_device += _format_autonomous_inflow_control_device(
144
+ well.well_name, lateral.lateral_number, df_autonomous_inflow_control_device, first
145
+ )
146
+ print_inflow_control_valve += _format_inflow_control_valve(
147
+ well.well_name, lateral.lateral_number, df_inflow_control_valve, first
148
+ )
149
+ print_density_activated_recovery += _format_density_activated_recovery(
150
+ well.well_number, df_density_activated_recovery
151
+ )
152
+ print_autonomous_inflow_control_valve += _format_autonomous_inflow_control_valve(
153
+ well.well_number, df_autonomous_inflow_control_valve
154
+ )
155
+
156
+ if figure_name is not None:
157
+ logger.info(f"Creating figure for lateral {lateral.lateral_number}.")
158
+ with PdfPages(figure_name) as figure:
159
+ figure.savefig(
160
+ visualize_well(
161
+ well.well_name, well.df_well_all_laterals, well.df_reservoir_all_laterals, case.segment_length
162
+ ),
214
163
  orientation="landscape",
215
164
  )
216
- logger.info("creating schematics: %s.pdf", figure_name)
217
- elif show_figure and figure_name is None:
218
- raise ValueError("Cannot show figure without filename supplied.")
219
- self.fix_printing()
220
- self.print_per_well()
221
-
222
- def make_completor_header(self) -> str:
223
- """Print header note."""
224
- header = f"{'-' * 100}\n"
225
- header += f"-- Output from completor {self.version}\n"
226
- try:
227
- header += f"-- Case file : {self.case_path}\n"
228
- except AttributeError:
229
- header += "-- Case file : No path found \n"
230
- logger.warning("Could not resolve case-file path to output file")
231
- header += f"-- Schedule file : {self.schedule_path}\n"
232
-
233
- header += f"""\
234
- -- Created by : {(getpass.getuser()).upper()}
235
- -- Created at : {datetime.now().strftime('%Y %B %d %H:%M')}
236
- {'-' * 100}{self.newline1}
237
- """
238
- return header
239
-
240
- def check_welsegs1(self) -> None:
241
- """Check whether the measured depth of the first segment is deeper than the first cells start measured depth.
242
-
243
- In this case, adjust segments measured depth to be 1 meter shallower.
244
- """
245
- start_md = self.df_reservoir[Headers.START_MEASURED_DEPTH].iloc[0]
246
- if self.welsegs_header[Headers.SEGMENTMD].iloc[0] > start_md:
247
- self.welsegs_header[Headers.SEGMENTMD] = start_md - 1.0
248
-
249
- def check_segments(self, lateral: int) -> None:
250
- """Check whether there is annular flow in the well.
251
-
252
- Also check if there are any connections from the reservoir to the tubing in a well.
253
- """
254
- if self.df_annulus.shape[0] == 0:
255
- logger.info("No annular flow in Well : %s Lateral : %d", self.well_name, lateral)
256
- if self.df_device.shape[0] == 0:
257
- logger.warning("No connection from reservoir to tubing in Well : %s Lateral : %d", self.well_name, lateral)
258
-
259
- def update_segmentbranch(self) -> None:
260
- """Update the numbering of the tubing segment and branch."""
261
-
262
- if self.df_annulus.shape[0] == 0 and self.df_device.shape[0] > 0:
263
- self.start_segment = max(self.df_device[Headers.SEG].to_numpy()) + 1
264
- self.start_branch = max(self.df_device[Headers.BRANCH].to_numpy()) + 1
265
- elif self.df_annulus.shape[0] > 0:
266
- self.start_segment = max(self.df_annulus[Headers.SEG].to_numpy()) + 1
267
- self.start_branch = max(self.df_annulus[Headers.BRANCH].to_numpy()) + 1
268
-
269
- def make_compdat(self, lateral: int) -> None:
270
- """Print completion data to file."""
271
- nchar = po.get_number_of_characters(self.df_compdat)
272
- if self.df_compdat.shape[0] > 0:
273
- self.print_compdat += (
274
- po.get_header(self.well_name, Keywords.COMPDAT, lateral, "", nchar)
275
- + po.dataframe_tostring(self.df_compdat, True)
276
- + "\n"
277
- )
165
+ logger.info("Creating schematics: %s.pdf", figure_name)
166
+ first = False
167
+ print_completion_data = "\n".join(completion_data_list)
168
+ if print_well_segments:
169
+ print_well_segments = f"{print_well_segments}\n/\n\n"
170
+ if print_completion_segments:
171
+ print_completion_segments = (
172
+ f"{Keywords.COMPLETION_SEGMENTS}\n'{well.well_name}' /{print_completion_segments}\n/\n\n\n"
173
+ )
174
+ bonus = []
175
+ if print_well_segments_link:
176
+ bonus.append(f"{Keywords.WELL_SEGMENTS_LINK}{print_well_segments_link}\n/\n\n\n")
177
+ if print_valve:
178
+ bonus.append(f"{Keywords.WELL_SEGMENTS_VALVE}{print_valve}\n/\n\n\n")
179
+ if print_inflow_control_device:
180
+ bonus.append(f"{Keywords.INFLOW_CONTROL_DEVICE}{print_inflow_control_device}\n/\n\n\n")
181
+ if print_autonomous_inflow_control_device:
182
+ bonus.append(f"{Keywords.AUTONOMOUS_INFLOW_CONTROL_DEVICE}{print_autonomous_inflow_control_device}\n/\n\n\n")
183
+ if print_inflow_control_valve:
184
+ bonus.append(f"{Keywords.WELL_SEGMENTS_VALVE}{print_inflow_control_valve}\n/\n\n\n")
185
+ if print_density_activated_recovery:
186
+ metadata = (
187
+ f"{'-' * 100}\n"
188
+ "-- This is how we model DAR technology using sets of ACTIONX keywords.\n"
189
+ "-- The segment dP curves changes according to the segment water-\n"
190
+ "-- and gas volume fractions at downhole condition.\n"
191
+ "-- The value of Cv is adjusted according to the segment length and the number of\n"
192
+ "-- devices per joint. The constriction area varies according to values of\n"
193
+ "-- volume fractions.\n"
194
+ f"{'-' * 100}\n\n\n"
195
+ )
196
+ bonus.append(metadata + print_density_activated_recovery + "\n\n\n\n")
197
+ if print_autonomous_inflow_control_valve:
198
+ metadata = (
199
+ f"{'-' * 100}\n"
200
+ "-- This is how we model AICV technology using sets of ACTIONX keyword\n"
201
+ "-- the DP parameters change according to the segment water cut (at downhole condition )\n"
202
+ "-- and gas volume fraction (at downhole condition)\n"
203
+ f"{'-' * 100}\n\n\n"
204
+ )
205
+ bonus.append(metadata + print_autonomous_inflow_control_valve + "\n\n\n\n")
278
206
 
279
- def make_welsegs(self, lateral: int) -> None:
280
- """Print well segments to file."""
281
- nchar = po.get_number_of_characters(self.df_tubing)
282
- if self.df_device.shape[0] > 0:
283
- self.print_welsegs += (
284
- po.get_header(self.well_name, Keywords.WELSEGS, lateral, "Tubing", nchar)
285
- + po.dataframe_tostring(self.df_tubing, True)
286
- + "\n"
287
- )
288
- if self.df_device.shape[0] > 0:
289
- nchar = po.get_number_of_characters(self.df_tubing)
290
- self.print_welsegs += (
291
- po.get_header(self.well_name, Keywords.WELSEGS, lateral, "Device", nchar)
292
- + po.dataframe_tostring(self.df_device, True)
293
- + "\n"
294
- )
295
- if self.df_annulus.shape[0] > 0:
296
- nchar = po.get_number_of_characters(self.df_tubing)
297
- self.print_welsegs += (
298
- po.get_header(self.well_name, Keywords.WELSEGS, lateral, "Annulus", nchar)
299
- + po.dataframe_tostring(self.df_annulus, True)
300
- + "\n"
301
- )
207
+ return print_completion_data, print_well_segments, print_completion_segments, "".join(bonus)
302
208
 
303
- def make_wseglink(self, lateral: int) -> None:
304
- """Print WSEGLINK to file."""
305
- if self.df_wseglink.shape[0] > 0:
306
- nchar = po.get_number_of_characters(self.df_wseglink)
307
- self.print_wseglink += (
308
- po.get_header(self.well_name, Keywords.WSEGLINK, lateral, "", nchar)
309
- + po.dataframe_tostring(self.df_wseglink, True)
310
- + "\n"
311
- )
312
209
 
313
- def make_compsegs(self, lateral: int) -> None:
314
- """Print completion segments to file."""
315
- nchar = po.get_number_of_characters(self.df_compsegs)
316
- if self.df_compsegs.shape[0] > 0:
317
- self.print_compsegs += (
318
- po.get_header(self.well_name, Keywords.COMPSEGS, lateral, "", nchar)
319
- + po.dataframe_tostring(self.df_compsegs, True)
320
- + "\n"
321
- )
210
+ def _check_well_segments_header(welsegs_header: pd.DataFrame, start_measured_depths: pd.Series) -> pd.DataFrame:
211
+ """Check whether the measured depth of the first segment is deeper than the first cells start measured depth.
322
212
 
323
- def make_wsegaicd(self, lateral: int) -> None:
324
- """Print WSEGAICD to file."""
325
- if self.df_wsegaicd.shape[0] > 0:
326
- nchar = po.get_number_of_characters(self.df_wsegaicd)
327
- self.print_wsegaicd += (
328
- po.get_header(self.well_name, Keywords.WSEGAICD, lateral, "", nchar)
329
- + po.dataframe_tostring(self.df_wsegaicd, True)
330
- + "\n"
331
- )
213
+ In this case, adjust segments measured depth to be 1 meter shallower.
332
214
 
333
- def make_wsegsicd(self, lateral: int) -> None:
334
- """Print WSEGSICD to file."""
335
- if self.df_wsegsicd.shape[0] > 0:
336
- nchar = po.get_number_of_characters(self.df_wsegsicd)
337
- self.print_wsegsicd += (
338
- po.get_header(self.well_name, Keywords.WSEGSICD, lateral, "", nchar)
339
- + po.dataframe_tostring(self.df_wsegsicd, True)
340
- + "\n"
341
- )
215
+ Args:
216
+ welsegs_header: The header for well segments.
217
+ start_measured_depths: The measured depths of the first cells from the reservoir.
342
218
 
343
- def make_wsegvalv(self, lateral: int) -> None:
344
- """Print WSEGVALV to file."""
345
- if self.df_wsegvalv.shape[0] > 0:
346
- nchar = po.get_number_of_characters(self.df_wsegvalv)
347
- self.print_wsegvalv += (
348
- po.get_header(self.well_name, Keywords.WSEGVALV, lateral, "", nchar)
349
- + po.dataframe_tostring(self.df_wsegvalv, True)
350
- + "\n"
351
- )
219
+ Returns:
220
+ Corrected measured depths if well segments header.
221
+ """
222
+ if welsegs_header[Headers.MEASURED_DEPTH].iloc[0] > start_measured_depths:
223
+ welsegs_header[Headers.MEASURED_DEPTH] = start_measured_depths - 1.0
224
+ return welsegs_header
352
225
 
353
- def make_wsegicv(self, lateral: int) -> None:
354
- """Print WSEGICV to file."""
355
- if self.df_wsegicv.shape[0] > 0:
356
- nchar = po.get_number_of_characters(self.df_wsegicv)
357
- self.print_wsegicv += (
358
- po.get_header(self.well_name, Keywords.WSEGVALV, lateral, "", nchar)
359
- + po.dataframe_tostring(self.df_wsegicv, True)
360
- + "\n"
361
- )
362
226
 
363
- def make_wsegdar(self) -> None:
364
- """Print WSEGDAR to file."""
365
- if self.df_wsegdar.shape[0] > 0:
366
- self.print_wsegdar += po.print_wsegdar(self.df_wsegdar, self.iwell + 1) + "\n"
367
-
368
- def make_wsegaicv(self) -> None:
369
- """Print WSEGAICV to file."""
370
- if self.df_wsegaicv.shape[0] > 0:
371
- self.print_wsegaicv += po.print_wsegaicv(self.df_wsegaicv, self.iwell + 1) + "\n"
372
-
373
- def fix_printing(self) -> None:
374
- """Avoid printing non-existing keywords."""
375
- # if no compdat then dont print it
376
- if self.print_compdat == self.print_compdatinit:
377
- self.print_compdat = ""
378
- else:
379
- self.print_compdat += self.newline3
380
- # if no welsegs then dont print it
381
- if self.print_welsegs == self.print_welsegsinit:
382
- self.print_welsegs = ""
383
- else:
384
- self.print_welsegs += self.newline3
385
- # if no compsegs then dont print it
386
- if self.print_compsegs == self.print_compsegsinit:
387
- self.print_compsegs = ""
388
- else:
389
- self.print_compsegs += self.newline3
390
- # if no weseglink then dont print it
391
- if self.print_wseglink == Keywords.WSEGLINK + "\n":
392
- self.print_wseglink = ""
393
- else:
394
- self.print_wseglink += self.newline3
395
- # if no VALVE then dont print
396
- if self.print_wsegvalv == Keywords.WSEGVALV + "\n":
397
- self.print_wsegvalv = ""
398
- else:
399
- self.print_wsegvalv += self.newline3
400
- # if no ICD then dont print
401
- if self.print_wsegsicd == Keywords.WSEGSICD + "\n":
402
- self.print_wsegsicd = ""
403
- else:
404
- self.print_wsegsicd += self.newline3
405
- # if no AICD then dont print
406
- if self.print_wsegaicd == Keywords.WSEGAICD + "\n":
407
- self.print_wsegaicd = ""
408
- else:
409
- self.print_wsegaicd += self.newline3
410
- # if no DAR then dont print
411
- if self.print_wsegdar == self.print_wsegdarinit:
412
- self.print_wsegdar = ""
413
- else:
414
- self.print_wsegdar += self.newline1
415
- # if no DAR then dont print
416
- if self.print_wsegaicv == self.print_wsegaicvinit:
417
- self.print_wsegaicv = ""
418
- else:
419
- self.print_wsegaicv += self.newline1
420
- # if no ICV then dont print
421
- if self.print_wsegicv == Keywords.WSEGVALV + "\n":
422
- self.print_wsegicv = ""
423
- else:
424
- self.print_wsegicv += self.newline3
425
-
426
- def print_per_well(self) -> None:
427
- """Collect final printing for all wells."""
428
- # here starts active wells
429
- finalprint = self.finalprint + self.print_compdat
430
- if self.write_welsegs:
431
- finalprint += self.print_welsegs
432
- finalprint += self.print_wseglink
433
- finalprint += self.print_compsegs
434
- # print udq parameter if relevant
435
- if self.well_name in self.udq_parameter and self.print_udq:
436
- finalprint += self.udq_parameter[self.well_name]
437
-
438
- finalprint += (
439
- self.print_wsegvalv
440
- + self.print_wsegsicd
441
- + self.print_wsegaicd
442
- + self.print_wsegdar
443
- + self.print_wsegaicv
444
- + self.print_wsegicv
227
+ def _update_segmentbranch(df_device: pd.DataFrame, df_annulus: pd.DataFrame) -> tuple[int, int]:
228
+ """Update the numbering of the tubing segment and branch.
229
+
230
+ Args:
231
+ df_device: Device data.
232
+ df_annulus: Annulus data.
233
+
234
+ Returns:
235
+ The numbers for starting segment and branch.
236
+
237
+ Raises:
238
+ ValueError: If there is neither device nor annulus data.
239
+ """
240
+ if df_annulus.empty and df_device.empty:
241
+ raise ValueError("Cannot determine starting segment and branch without device and annulus data.")
242
+ if df_annulus.empty and not df_device.empty:
243
+ start_segment = max(df_device[Headers.START_SEGMENT_NUMBER].to_numpy()) + 1
244
+ start_branch = max(df_device[Headers.BRANCH].to_numpy()) + 1
245
+ else:
246
+ start_segment = max(df_annulus[Headers.START_SEGMENT_NUMBER].to_numpy()) + 1
247
+ start_branch = max(df_annulus[Headers.BRANCH].to_numpy()) + 1
248
+ return start_segment, start_branch
249
+
250
+
251
+ def _format_completion_data(well_name: str, lateral_number: int, df_compdat: pd.DataFrame, first: bool) -> str:
252
+ """Print completion data to file.
253
+
254
+ Args:
255
+ well_name: Name of well.
256
+ lateral_number: Current laterals number.
257
+ df_compdat: Completion data.
258
+
259
+ Returns:
260
+ Formatted string.
261
+ """
262
+ if df_compdat.empty:
263
+ return ""
264
+ nchar = prepare_outputs.get_number_of_characters(df_compdat)
265
+ result = prepare_outputs.get_header(well_name, Keywords.COMPLETION_DATA, lateral_number, "", nchar)
266
+ result += prepare_outputs.dataframe_tostring(df_compdat, True, keep_header=first)
267
+ return result
268
+
269
+
270
+ def _format_well_segments(
271
+ well_name: str,
272
+ lateral_number: int,
273
+ df_tubing: pd.DataFrame,
274
+ df_device: pd.DataFrame,
275
+ df_annulus: pd.DataFrame,
276
+ header: bool,
277
+ ) -> str:
278
+ """Print well segments to file.
279
+
280
+ Args:
281
+ well_name: Name of well.
282
+ lateral_number: Current lateral number.
283
+ df_tubing: Tubing data.
284
+ df_device: Device data.
285
+ df_annulus: Annulus data.
286
+
287
+ Returns:
288
+ Formatted string.
289
+ """
290
+ print_welsegs = ""
291
+ nchar = prepare_outputs.get_number_of_characters(df_tubing)
292
+ if not df_device.empty:
293
+ # Tubing layer.
294
+ print_welsegs += (
295
+ "\n"
296
+ + prepare_outputs.get_header(well_name, Keywords.WELL_SEGMENTS, lateral_number, "Tubing", nchar)
297
+ + prepare_outputs.dataframe_tostring(df_tubing, True, keep_header=header)
298
+ )
299
+ # Device layer.
300
+ print_welsegs += (
301
+ "\n"
302
+ + prepare_outputs.get_header(well_name, Keywords.WELL_SEGMENTS, lateral_number, "Device", nchar)
303
+ + prepare_outputs.dataframe_tostring(df_device, True, keep_header=False)
445
304
  )
446
- self.finalprint = finalprint
447
-
448
- def branch_revision(self, lateral: int) -> None:
449
- """Revises the order of branch numbers to be in agreement with common practice.
450
-
451
- This means that tubing layers will get branch numbers from 1 to the number of laterals.
452
- Device and lateral branch numbers are changed accordingly if they exist.
453
-
454
- Args:
455
- lateral: The lateral number being worked on.
456
- """
457
- correction = max(self.laterals) - lateral
458
- self.df_tubing[Headers.BRANCH] = lateral
459
- if self.df_device.shape[0] > 0:
460
- self.df_device[Headers.BRANCH] += correction
461
- if self.df_annulus.shape[0] > 0:
462
- self.df_annulus[Headers.BRANCH] += correction
305
+ if not df_annulus.empty:
306
+ # Annulus layer.
307
+ print_welsegs += (
308
+ "\n"
309
+ + prepare_outputs.get_header(well_name, Keywords.WELL_SEGMENTS, lateral_number, "Annulus", nchar)
310
+ + prepare_outputs.dataframe_tostring(df_annulus, True, keep_header=False)
311
+ )
312
+ return print_welsegs
313
+
314
+
315
+ def _format_well_segments_link(
316
+ well_name: str, lateral_number: int, df_well_segments_link: pd.DataFrame, header: bool
317
+ ) -> str:
318
+ """Formats well-segments for links.
319
+
320
+ Args:
321
+ well_name: Name of well.
322
+ lateral_number: Current lateral number.
323
+ df_well_segments_link: Well-segmentation data with links.
324
+
325
+ Returns:
326
+ Formatted string.
327
+ """
328
+ if df_well_segments_link.empty:
329
+ return ""
330
+ nchar = prepare_outputs.get_number_of_characters(df_well_segments_link)
331
+ return (
332
+ "\n"
333
+ + prepare_outputs.get_header(well_name, Keywords.WELL_SEGMENTS_LINK, lateral_number, "", nchar)
334
+ + prepare_outputs.dataframe_tostring(df_well_segments_link, True, keep_header=header)
335
+ )
336
+
337
+
338
+ def _format_completion_segments(well_name: str, lateral_number: int, df_compsegs: pd.DataFrame, header: bool) -> str:
339
+ """Formats completion segments.
340
+
341
+ Args:
342
+ well_name: Name of well.
343
+ lateral_number: Current lateral number.
344
+ df_compsegs: Completion data.
345
+
346
+ Returns:
347
+ Formatted string.
348
+ """
349
+ if df_compsegs.empty:
350
+ return ""
351
+ nchar = prepare_outputs.get_number_of_characters(df_compsegs)
352
+ return (
353
+ "\n"
354
+ + prepare_outputs.get_header(well_name, Keywords.COMPLETION_SEGMENTS, lateral_number, "", nchar)
355
+ + prepare_outputs.dataframe_tostring(df_compsegs, True, keep_header=header)
356
+ )
357
+
358
+
359
+ def _format_autonomous_inflow_control_device(
360
+ well_name: str, lateral_number: int, df_wsegaicd: pd.DataFrame, header: bool
361
+ ) -> str:
362
+ """Formats well-segments for autonomous inflow control devices.
363
+
364
+ Args:
365
+ well_name: Name of well.
366
+ lateral_number: Current lateral number.
367
+ df_wsegaicd: Well-segments data for autonomous inflow control devices.
368
+
369
+ Returns:
370
+ Formatted string.
371
+ """
372
+ if df_wsegaicd.empty:
373
+ return ""
374
+ nchar = prepare_outputs.get_number_of_characters(df_wsegaicd)
375
+ return (
376
+ "\n"
377
+ + prepare_outputs.get_header(well_name, Keywords.INFLOW_CONTROL_DEVICE, lateral_number, "", nchar)
378
+ + prepare_outputs.dataframe_tostring(df_wsegaicd, True, keep_header=header)
379
+ )
380
+
381
+
382
+ def _format_inflow_control_device(well_name: str, lateral_number: int, df_wsegsicd: pd.DataFrame, header: bool) -> str:
383
+ """Formats well-segments for inflow control devices.
384
+
385
+ Args:
386
+ well_name: Name of well.
387
+ lateral_number: Current lateral number.
388
+ df_wsegsicd: Well-segment data for inflow control devices.
389
+
390
+ Returns:
391
+ Formatted string.
392
+ """
393
+ if df_wsegsicd.empty:
394
+ return ""
395
+ nchar = prepare_outputs.get_number_of_characters(df_wsegsicd)
396
+ return (
397
+ "\n"
398
+ + prepare_outputs.get_header(well_name, Keywords.INFLOW_CONTROL_DEVICE, lateral_number, "", nchar)
399
+ + prepare_outputs.dataframe_tostring(df_wsegsicd, True, keep_header=header)
400
+ )
401
+
402
+
403
+ def _format_valve(well_name: str, lateral_number: int, df_wsegvalv, header: bool) -> str:
404
+ """Formats well-segments for valves.
405
+
406
+ Args:
407
+ well_name: Name of well.
408
+ lateral_number: Current lateral number.
409
+ df_wsegvalv: Well-segment data for valves.
410
+
411
+ Returns:
412
+ Formatted string.
413
+ """
414
+ if df_wsegvalv.empty:
415
+ return ""
416
+ nchar = prepare_outputs.get_number_of_characters(df_wsegvalv)
417
+ return (
418
+ "\n"
419
+ + prepare_outputs.get_header(well_name, Keywords.WELL_SEGMENTS_VALVE, lateral_number, "", nchar)
420
+ + prepare_outputs.dataframe_tostring(df_wsegvalv, True, keep_header=header)
421
+ )
422
+
423
+
424
+ def _format_inflow_control_valve(well_name: str, lateral_number: int, df_wsegicv: pd.DataFrame, header: bool) -> str:
425
+ """Formats well-segments for inflow control valve.
426
+
427
+ Args:
428
+ well_name: Name of well.
429
+ lateral_number: Current lateral number.
430
+ df_wsegicv: Well-segment data for inflow control valves.
431
+
432
+ Returns:
433
+ Formatted string.
434
+ """
435
+ if df_wsegicv.empty:
436
+ return ""
437
+ nchar = prepare_outputs.get_number_of_characters(df_wsegicv)
438
+ return (
439
+ "\n"
440
+ + prepare_outputs.get_header(well_name, Keywords.WELL_SEGMENTS_VALVE, lateral_number, "", nchar)
441
+ + prepare_outputs.dataframe_tostring(df_wsegicv, True, keep_header=header)
442
+ )
443
+
444
+
445
+ def _format_density_activated_recovery(well_number: int, df_wsegdar: pd.DataFrame) -> str:
446
+ """Formats well-segments for density activated recovery valve.
447
+
448
+ Args:
449
+ well_number: The well's number
450
+ df_wsegdar: Data to print.
451
+
452
+ Returns:
453
+ Formatted string.
454
+ """
455
+ if df_wsegdar.empty:
456
+ return ""
457
+ return prepare_outputs.print_wsegdar(df_wsegdar, well_number + 1)
458
+
459
+
460
+ def _format_autonomous_inflow_control_valve(well_number: int, df_wsegaicv: pd.DataFrame) -> str:
461
+ """Formats the AICV section.
462
+
463
+ Args:
464
+ well_number: The well's number
465
+ df_wsegaicv: Data to print.
466
+
467
+ Returns:
468
+ Formatted string.
469
+ """
470
+ if df_wsegaicv.empty:
471
+ return ""
472
+ return prepare_outputs.print_wsegaicv(df_wsegaicv, well_number + 1)
473
+
474
+
475
+ def _branch_revision(
476
+ lateral_number: int,
477
+ active_laterals: list[int] | npt.NDArray[np.int64],
478
+ df_device: pd.DataFrame,
479
+ df_annulus: pd.DataFrame,
480
+ ) -> tuple[pd.DataFrame, pd.DataFrame]:
481
+ """Revises the order of branch numbers to be in agreement with common practice.
482
+
483
+ This means that tubing layers will get branch numbers from 1 to the number of laterals.
484
+ Device and lateral branch numbers are changed accordingly if they exist.
485
+
486
+ Args:
487
+ lateral_number: The lateral number being worked on.
488
+ active_laterals: List of active lateral numbers.
489
+ df_device: Dataframe containing device data.
490
+ df_annulus: Dataframe containing annular data.
491
+
492
+ Returns:
493
+ Corrected device.
494
+ """
495
+ correction = max(active_laterals) - lateral_number
496
+ if df_device.get(Headers.BRANCH) is not None:
497
+ df_device[Headers.BRANCH] += correction
498
+ if df_annulus.get(Headers.BRANCH) is not None:
499
+ df_annulus[Headers.BRANCH] += correction
500
+ return df_device, df_annulus
501
+
502
+
503
+ def _connect_lateral(
504
+ well_name: str, lateral: Lateral, top: pd.DataFrame, well: Well, case: ReadCasefile
505
+ ) -> pd.DataFrame:
506
+ """Connect lateral to main wellbore/branch.
507
+
508
+ The main branch can either have a tubing- or device-layer connected.
509
+ By default, the lateral will be connected to tubing-layer, but if connect_to_tubing is False,
510
+ it will be connected to device-layer.
511
+ Abort if it cannot find device layer at junction depth.
512
+
513
+ Args:
514
+ well_name: Well name.
515
+ lateral: Current lateral to connect.
516
+ top: DataFrame of first connection.
517
+ well: Well object containing data from whole well.
518
+
519
+ Returns:
520
+ Tubing data with modified outsegment.
521
+
522
+ Raises:
523
+ CompletorError: If there is no device layer at junction of lateral.
524
+ """
525
+ if top.empty:
526
+ lateral.df_tubing.at[0, Headers.OUT] = 1 # Default out segment.
527
+ return lateral.df_tubing
528
+
529
+ first_lateral_in_top = top[Headers.TUBING_BRANCH].to_numpy()[0]
530
+ top_lateral = [lateral for lateral in well.active_laterals if lateral.lateral_number == first_lateral_in_top][0]
531
+ junction_measured_depth = float(top[Headers.TUBING_MEASURED_DEPTH].to_numpy()[0])
532
+ if junction_measured_depth > lateral.df_tubing[Headers.MEASURED_DEPTH][0]:
533
+ logger.warning(
534
+ "Found a junction above the start of the tubing layer, well %s, branch %s. "
535
+ "Check the depth of segments pointing at the main stem in schedulefile.",
536
+ well_name,
537
+ lateral.lateral_number,
538
+ )
539
+ if case.connect_to_tubing(well_name, lateral.lateral_number):
540
+ layer_to_connect = top_lateral.df_tubing
541
+ measured_depths = top_lateral.df_tubing[Headers.MEASURED_DEPTH]
542
+ else:
543
+ layer_to_connect = top_lateral.df_device
544
+ measured_depths = top_lateral.df_device[Headers.MEASURED_DEPTH]
545
+ try:
546
+ if case.connect_to_tubing(well_name, lateral.lateral_number):
547
+ # Since the junction_measured_depth has segment tops and layer_to_connect has grid block midpoints,
548
+ # a junction at the top of the well may not be found. Therefore, we try the following:
549
+ if (np.array(~(measured_depths <= junction_measured_depth))).all():
550
+ junction_measured_depth = measured_depths.iloc[0]
551
+ idx = np.where(measured_depths <= junction_measured_depth)[0][-1]
552
+
553
+ else:
554
+ idx = np.where(measured_depths <= junction_measured_depth)[0][-1]
555
+ else:
556
+ # Add 0.1 to junction measured depth since it refers to the tubing layer junction measured depth,
557
+ # and the device layer measured depth is shifted 0.1 m to the tubing layer.
558
+ idx = np.where(measured_depths <= junction_measured_depth + 0.1)[0][-1]
559
+ except IndexError as err:
560
+ raise CompletorError(
561
+ f"Cannot find a device layer at junction of lateral {lateral.lateral_number} in {well_name}"
562
+ ) from err
563
+ out_segment = layer_to_connect.at[idx, Headers.START_SEGMENT_NUMBER]
564
+ lateral.df_tubing.at[0, Headers.OUT] = out_segment
565
+ return lateral.df_tubing
566
+
567
+
568
+ def metadata_banner(paths: tuple[str, str] | None) -> str:
569
+ """Formats the header banner, with metadata.
570
+
571
+ Args:
572
+ paths: The paths to case and schedule files.
573
+
574
+ Returns:
575
+ Formatted header.
576
+ """
577
+ header = f"{'-' * 100}\n-- Output from Completor {get_version()}\n"
578
+
579
+ if paths is not None:
580
+ header += f"-- Case file: {paths[0]}\n-- Schedule file: {paths[1]}\n"
581
+ else:
582
+ logger.warning("Could not resolve case-file path to output file.")
583
+ header += "-- Case file: No path found\n-- Schedule file: No path found\n"
584
+
585
+ header += (
586
+ f"-- Created by : {(getpass.getuser()).upper()}\n"
587
+ f"-- Created at : {datetime.now().strftime('%Y %B %d %H:%M')}\n"
588
+ f"{'-' * 100}\n\n"
589
+ )
590
+ return header