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,462 @@
1
+ """Defines a class for generating output files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import getpass
6
+ from datetime import datetime
7
+
8
+ import matplotlib # type: ignore
9
+
10
+ from completor import prepare_outputs as po
11
+ from completor.completion import WellSchedule
12
+ from completor.constants import Headers, Keywords
13
+ from completor.create_wells import CreateWells
14
+ from completor.logger import logger
15
+ from completor.pvt_model import CORRELATION_UDQ
16
+ from completor.read_casefile import ReadCasefile
17
+ from completor.visualize_well import visualize_well
18
+
19
+
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.
26
+
27
+ 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.
37
+ """
38
+
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,
183
+ )
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,
198
+ )
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),
214
+ orientation="landscape",
215
+ )
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
+ )
278
+
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
+ )
302
+
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
+
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
+ )
322
+
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
+ )
332
+
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
+ )
342
+
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
+ )
352
+
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
+
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
445
+ )
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