completor 0.1.2__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.
completor/completion.py CHANGED
@@ -2,74 +2,16 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import overload
5
+ from typing import Literal, overload
6
6
 
7
7
  import numpy as np
8
8
  import numpy.typing as npt
9
9
  import pandas as pd
10
10
 
11
- from completor.constants import Headers, Keywords, Method
11
+ from completor.constants import Content, Headers, Method
12
12
  from completor.exceptions import CompletorError
13
13
  from completor.logger import logger
14
- from completor.read_schedule import fix_compsegs, fix_welsegs
15
- from completor.utils import as_data_frame, log_and_raise_exception
16
-
17
- try:
18
- from typing import Literal, TypeAlias # type: ignore
19
- except ImportError:
20
- pass
21
-
22
- # Use more precise type information, if possible
23
- DeviceType: TypeAlias = 'Literal["AICD", "ICD", "DAR", "VALVE", "AICV", "ICV"]'
24
-
25
-
26
- class Information:
27
- """Holds information from `get_completion`."""
28
-
29
- # TODO(#85): Improve the class.
30
-
31
- def __init__(
32
- self,
33
- number_of_devices: float | list[float] | None = None,
34
- device_type: DeviceType | list[DeviceType] | None = None,
35
- device_number: int | list[int] | None = None,
36
- inner_diameter: float | list[float] | None = None,
37
- outer_diameter: float | list[float] | None = None,
38
- roughness: float | list[float] | None = None,
39
- annulus_zone: int | list[int] | None = None,
40
- ):
41
- """Initialize Information class."""
42
- self.number_of_devices = number_of_devices
43
- self.device_type = device_type
44
- self.device_number = device_number
45
- self.inner_diameter = inner_diameter
46
- self.outer_diameter = outer_diameter
47
- self.roughness = roughness
48
- self.annulus_zone = annulus_zone
49
-
50
- def __iadd__(self, other: Information):
51
- """Implement value-wise addition between two Information instances."""
52
- attributes = [
53
- attribute for attribute in dir(self) if not attribute.startswith("__") and not attribute.endswith("__")
54
- ]
55
- for attribute in attributes:
56
- value = getattr(self, attribute)
57
- if not isinstance(value, list):
58
- if value is None:
59
- value = []
60
- else:
61
- value = [value]
62
- setattr(self, attribute, value)
63
-
64
- value = getattr(other, attribute)
65
- attr: list = getattr(self, attribute)
66
- if attr is None:
67
- attr = []
68
- if isinstance(value, list):
69
- attr.extend(value)
70
- else:
71
- attr.append(value)
72
- return self
14
+ from completor.utils import log_and_raise_exception, shift_array
73
15
 
74
16
 
75
17
  def well_trajectory(df_well_segments_header: pd.DataFrame, df_well_segments_content: pd.DataFrame) -> pd.DataFrame:
@@ -85,13 +27,19 @@ def well_trajectory(df_well_segments_header: pd.DataFrame, df_well_segments_cont
85
27
  Measured depth versus true vertical depth.
86
28
 
87
29
  """
88
- measured_depth = df_well_segments_content[Headers.TUBINGMD].to_numpy()
89
- measured_depth = np.insert(measured_depth, 0, df_well_segments_header[Headers.SEGMENTMD].iloc[0])
90
- true_vertical_depth = df_well_segments_content[Headers.TUBINGTVD].to_numpy()
91
- true_vertical_depth = np.insert(true_vertical_depth, 0, df_well_segments_header[Headers.SEGMENTTVD].iloc[0])
92
- df_measured_true_vertical_depth = as_data_frame({Headers.MD: measured_depth, Headers.TVD: true_vertical_depth})
30
+ measured_depth = df_well_segments_content[Headers.TUBING_MEASURED_DEPTH].to_numpy()
31
+ measured_depth = np.insert(measured_depth, 0, df_well_segments_header[Headers.MEASURED_DEPTH].iloc[0])
32
+ true_vertical_depth = df_well_segments_content[Headers.TRUE_VERTICAL_DEPTH].to_numpy()
33
+ true_vertical_depth = np.insert(
34
+ true_vertical_depth, 0, df_well_segments_header[Headers.TRUE_VERTICAL_DEPTH].iloc[0]
35
+ )
36
+ df_measured_true_vertical_depth = pd.DataFrame(
37
+ {Headers.MEASURED_DEPTH: measured_depth, Headers.TRUE_VERTICAL_DEPTH: true_vertical_depth}
38
+ )
93
39
  # sort based on md
94
- df_measured_true_vertical_depth = df_measured_true_vertical_depth.sort_values(by=[Headers.MD, Headers.TVD])
40
+ df_measured_true_vertical_depth = df_measured_true_vertical_depth.sort_values(
41
+ by=[Headers.MEASURED_DEPTH, Headers.TRUE_VERTICAL_DEPTH]
42
+ )
95
43
  # reset index after sorting
96
44
  return df_measured_true_vertical_depth.reset_index(drop=True)
97
45
 
@@ -114,19 +62,19 @@ def define_annulus_zone(df_completion: pd.DataFrame) -> pd.DataFrame:
114
62
  """
115
63
  start_measured_depth = df_completion[Headers.START_MEASURED_DEPTH].iloc[0]
116
64
  end_measured_depth = df_completion[Headers.END_MEASURED_DEPTH].iloc[-1]
117
- gravel_pack_location = df_completion[df_completion[Headers.ANNULUS] == "GP"][
65
+ gravel_pack_location = df_completion[df_completion[Headers.ANNULUS] == Content.GRAVEL_PACKED][
118
66
  [Headers.START_MEASURED_DEPTH, Headers.END_MEASURED_DEPTH]
119
67
  ].to_numpy()
120
- packer_location = df_completion[df_completion[Headers.ANNULUS] == "PA"][
68
+ packer_location = df_completion[df_completion[Headers.ANNULUS] == Content.PACKER][
121
69
  [Headers.START_MEASURED_DEPTH, Headers.END_MEASURED_DEPTH]
122
70
  ].to_numpy()
123
71
  # update df_completion by removing PA rows
124
- df_completion = df_completion[df_completion[Headers.ANNULUS] != "PA"].copy()
72
+ df_completion = df_completion[df_completion[Headers.ANNULUS] != Content.PACKER].copy()
125
73
  # reset index after filter
126
74
  df_completion.reset_index(drop=True, inplace=True)
127
75
  annulus_content = df_completion[Headers.ANNULUS].to_numpy()
128
76
  df_completion[Headers.ANNULUS_ZONE] = 0
129
- if "OA" in annulus_content:
77
+ if Content.OPEN_ANNULUS in annulus_content:
130
78
  # only if there is an open annulus
131
79
  boundary = np.concatenate((packer_location.flatten(), gravel_pack_location.flatten()))
132
80
  boundary = np.sort(np.append(np.insert(boundary, 0, start_measured_depth), end_measured_depth))
@@ -145,7 +93,7 @@ def define_annulus_zone(df_completion: pd.DataFrame) -> pd.DataFrame:
145
93
  if not is_gravel_pack_location:
146
94
  annulus_zone[idx] = max(annulus_zone) + 1
147
95
  # else it is 0
148
- df_annulus = as_data_frame(
96
+ df_annulus = pd.DataFrame(
149
97
  {
150
98
  Headers.START_MEASURED_DEPTH: start_bound,
151
99
  Headers.END_MEASURED_DEPTH: end_bound,
@@ -212,7 +160,7 @@ def create_tubing_segments(
212
160
  cells: Create one segment per cell.
213
161
  user: Create segment based on the completion definition.
214
162
  fix: Create segment based on a fixed interval.
215
- well_segments: Create segment based on well segments keyword.
163
+ well_data: Create segment based on well segments keyword.
216
164
 
217
165
  Returns:
218
166
  DataFrame with start and end measured depth, tubing measured depth, and tubing true vertical depth.
@@ -230,7 +178,10 @@ def create_tubing_segments(
230
178
  if not df_reservoir[Headers.SEGMENT].isin(["1*"]).any():
231
179
  create_start_measured_depths = []
232
180
  create_end_measured_depths = []
233
- create_start_measured_depths.append(df_reservoir[Headers.START_MEASURED_DEPTH].iloc[0])
181
+ try:
182
+ create_start_measured_depths.append(df_reservoir[Headers.START_MEASURED_DEPTH].iloc[0])
183
+ except IndexError:
184
+ raise CompletorError("Number of WELSEGS and COMPSEGS is inconsistent.")
234
185
  current_segment = df_reservoir[Headers.SEGMENT].iloc[0]
235
186
  for i in range(1, len(df_reservoir[Headers.SEGMENT])):
236
187
  if df_reservoir[Headers.SEGMENT].iloc[i] != current_segment:
@@ -290,18 +241,18 @@ def create_tubing_segments(
290
241
  # Update the end point of the last segment.
291
242
  end_measured_depth[-1] = min(float(end_measured_depth[-1]), max_measured_depth)
292
243
  elif method == Method.WELSEGS:
293
- # Create the tubing layer from segment measured depths in the WELSEGS keyword that are missing from COMPSEGS.
294
- # WELSEGS segment depths are collected in the df_measured_depth_true_vertical_depth dataframe, which is available here.
295
- # Completor interprets WELSEGS depths as segment midpoint depths.
296
- # Obtain the well_segments segment midpoint depth.
297
- well_segments = df_measured_depth_true_vertical_depth[Headers.MD].to_numpy()
244
+ # Create the tubing layer from measured depths in the WELL_SEGMENTS keyword that are missing from COMPLETION_SEGMENTS.
245
+ # WELL_SEGMENTS depths are collected in the `df_measured_depth_true_vertical_depth`, available here.
246
+ # Completor interprets WELL_SEGMENTS depths as segment midpoint depths.
247
+ # Obtain the multisegmented well segments midpoint depth.
248
+ well_segments = df_measured_depth_true_vertical_depth[Headers.MEASURED_DEPTH].to_numpy()
298
249
  end_welsegs_depth = 0.5 * (well_segments[:-1] + well_segments[1:])
299
250
  # The start of the very first segment in any branch is the actual startMD of the first segment.
300
251
  start_welsegs_depth = np.insert(end_welsegs_depth[:-1], 0, well_segments[0], axis=None)
301
252
  start_compsegs_depth: npt.NDArray[np.float64] = df_reservoir[Headers.START_MEASURED_DEPTH].to_numpy()
302
253
  end_compsegs_depth = df_reservoir[Headers.END_MEASURED_DEPTH].to_numpy()
303
- # If there are gaps in compsegs and there are well_segments segments that fit in the gaps,
304
- # insert well_segments segments into the compsegs gaps.
254
+ # If there are gaps in compsegs and there are schedule segments that fit in the gaps,
255
+ # insert segments into the compsegs gaps.
305
256
  gaps_compsegs = start_compsegs_depth[1:] - end_compsegs_depth[:-1]
306
257
  # Indices of gaps in compsegs.
307
258
  indices_gaps = np.nonzero(gaps_compsegs)
@@ -309,7 +260,7 @@ def create_tubing_segments(
309
260
  start_gaps_depth = end_compsegs_depth[indices_gaps[0]]
310
261
  # End of the gaps.
311
262
  end_gaps_depth = start_compsegs_depth[indices_gaps[0] + 1]
312
- # Check the gaps between COMPSEGS and fill it out with WELSEGS.
263
+ # Check the gaps between COMPLETION_SEGMENTS and fill it out with WELL_SEGMENTS.
313
264
  start = np.abs(start_welsegs_depth[:, np.newaxis] - start_gaps_depth).argmin(axis=0)
314
265
  end = np.abs(end_welsegs_depth[:, np.newaxis] - end_gaps_depth).argmin(axis=0)
315
266
  welsegs_to_add = np.setxor1d(start_welsegs_depth[start], end_welsegs_depth[end])
@@ -317,7 +268,7 @@ def create_tubing_segments(
317
268
  end_welsegs_outside = end_welsegs_depth[np.argwhere(end_welsegs_depth > end_compsegs_depth[-1])]
318
269
  welsegs_to_add = np.append(welsegs_to_add, start_welsegs_outside)
319
270
  welsegs_to_add = np.append(welsegs_to_add, end_welsegs_outside)
320
- # Find well_segments start and end in gaps.
271
+ # Find schedule segments start and end in gaps.
321
272
  start_compsegs_depth = np.append(start_compsegs_depth, welsegs_to_add)
322
273
  end_compsegs_depth = np.append(end_compsegs_depth, welsegs_to_add)
323
274
  start_measured_depth = np.sort(start_compsegs_depth)
@@ -339,19 +290,19 @@ def create_tubing_segments(
339
290
 
340
291
  # md for tubing segments
341
292
  measured_depth_ = 0.5 * (start_measured_depth + end_measured_depth)
342
- # estimate TVD
293
+ # estimate TRUE_VERTICAL_DEPTH
343
294
  true_vertical_depth = np.interp(
344
295
  measured_depth_,
345
- df_measured_depth_true_vertical_depth[Headers.MD].to_numpy(),
346
- df_measured_depth_true_vertical_depth[Headers.TVD].to_numpy(),
296
+ df_measured_depth_true_vertical_depth[Headers.MEASURED_DEPTH].to_numpy(),
297
+ df_measured_depth_true_vertical_depth[Headers.TRUE_VERTICAL_DEPTH].to_numpy(),
347
298
  )
348
299
  # create data frame
349
- return as_data_frame(
300
+ return pd.DataFrame(
350
301
  {
351
302
  Headers.START_MEASURED_DEPTH: start_measured_depth,
352
303
  Headers.END_MEASURED_DEPTH: end_measured_depth,
353
- Headers.TUB_MD: measured_depth_,
354
- Headers.TUB_TVD: true_vertical_depth,
304
+ Headers.TUBING_MEASURED_DEPTH: measured_depth_,
305
+ Headers.TRUE_VERTICAL_DEPTH: true_vertical_depth,
355
306
  }
356
307
  )
357
308
 
@@ -378,32 +329,30 @@ def insert_missing_segments(df_tubing_segments: pd.DataFrame, well_name: str | N
378
329
  "Schedule file is missing data for one or more branches defined in the case file. "
379
330
  f"Please check the data for well {well_name}."
380
331
  )
381
- # sort the data frame based on STARTMD
382
332
  df_tubing_segments.sort_values(by=[Headers.START_MEASURED_DEPTH], inplace=True)
383
- # add column to indicate original segment
384
- df_tubing_segments[Headers.SEGMENT_DESC] = [Headers.ORIGINAL_SEGMENT] * df_tubing_segments.shape[0]
333
+ # Add column to indicate original segment.
334
+ df_tubing_segments[Headers.SEGMENT_DESC] = Headers.ORIGINAL_SEGMENT
385
335
  end_measured_depth = df_tubing_segments[Headers.END_MEASURED_DEPTH].to_numpy()
386
- # get start_measured_depth and start from segment 2 and add the last item to be the last end_measured_depth
336
+ # Get start_measured_depth and start from segment 2 and add the last item to be the last end_measured_depth.
387
337
  start_measured_depth = np.append(
388
338
  df_tubing_segments[Headers.START_MEASURED_DEPTH].to_numpy()[1:], end_measured_depth[-1]
389
339
  )
390
- # find rows where start_measured_depth > end_measured_depth
340
+ # Find rows where start_measured_depth > end_measured_depth.
391
341
  missing_index = np.argwhere(start_measured_depth > end_measured_depth).flatten()
392
- # proceed only if there are missing index
342
+ # Proceed only if there are missing indexes.
393
343
  if missing_index.size == 0:
394
344
  return df_tubing_segments
395
- # shift one row down because we move it up one row
345
+ # Shift one row down because we move it up one row.
396
346
  missing_index += 1
397
347
  df_copy = df_tubing_segments.iloc[missing_index, :].copy(deep=True)
398
- # new start measured depth is the previous segment end measured depth
348
+ # New start measured depth is the previous segment end measured depth.
399
349
  df_copy[Headers.START_MEASURED_DEPTH] = df_tubing_segments[Headers.END_MEASURED_DEPTH].to_numpy()[missing_index - 1]
400
350
  df_copy[Headers.END_MEASURED_DEPTH] = df_tubing_segments[Headers.START_MEASURED_DEPTH].to_numpy()[missing_index]
401
351
  df_copy[Headers.SEGMENT_DESC] = [Headers.ADDITIONAL_SEGMENT] * df_copy.shape[0]
402
- # combine the two data frame
352
+ # Combine the dataframes.
403
353
  df_tubing_segments = pd.concat([df_tubing_segments, df_copy])
404
- df_tubing_segments.sort_values(by=[Headers.START_MEASURED_DEPTH], inplace=True)
405
- df_tubing_segments.reset_index(drop=True, inplace=True)
406
- return df_tubing_segments
354
+ df_tubing_segments = df_tubing_segments.sort_values(by=[Headers.START_MEASURED_DEPTH])
355
+ return df_tubing_segments.reset_index(drop=True)
407
356
 
408
357
 
409
358
  def completion_index(df_completion: pd.DataFrame, start: float, end: float) -> tuple[int, int]:
@@ -427,18 +376,20 @@ def completion_index(df_completion: pd.DataFrame, start: float, end: float) -> t
427
376
  return int(_start[0]), int(_end[0])
428
377
 
429
378
 
430
- def get_completion(start: float, end: float, df_completion: pd.DataFrame, joint_length: float) -> Information:
379
+ def get_completion(
380
+ start: float, end: float, df_completion: pd.DataFrame, joint_length: float
381
+ ) -> tuple[float, float, float, float, float, float, float]:
431
382
  """Get information from the completion.
432
383
 
433
384
  Args:
434
385
  start: Start measured depth of the segment.
435
386
  end: End measured depth of the segment.
436
387
  df_completion: COMPLETION table that must contain columns: `STARTMD`, `ENDMD`, `NVALVEPERJOINT`,
437
- `INNER_DIAMETER`, `OUTER_DIAMETER`, `ROUGHNESS`, `DEVICETYPE`, `DEVICENUMBER`, and `ANNULUS_ZONE`.
388
+ `INNER_DIAMETER`, `OUTER_DIAMETER`, `ROUGHNESS`, `DEVICETYPE`, `DEVICENUMBER`, and `ANNULUS_ZONE`.
438
389
  joint_length: Length of a joint.
439
390
 
440
391
  Returns:
441
- Instance of Information.
392
+ The number of devices, device type, device number, inner diameter, outer diameter, roughness, annulus zone.
442
393
 
443
394
  Raises:
444
395
  ValueError:
@@ -447,13 +398,6 @@ def get_completion(start: float, end: float, df_completion: pd.DataFrame, joint_
447
398
  If the completion data contains illegal / invalid rows.
448
399
  If information class is None.
449
400
  """
450
- information = None
451
- device_type = None
452
- device_number = None
453
- inner_diameter = None
454
- outer_diameter = None
455
- roughness = None
456
- annulus_zone = None
457
401
 
458
402
  start_completion = df_completion[Headers.START_MEASURED_DEPTH].to_numpy()
459
403
  end_completion = df_completion[Headers.END_MEASURED_DEPTH].to_numpy()
@@ -463,58 +407,39 @@ def get_completion(start: float, end: float, df_completion: pd.DataFrame, joint_
463
407
  well_name = df_completion[Headers.WELL].iloc[0]
464
408
  log_and_raise_exception(f"No completion is defined on well {well_name} from {start} to {end}.")
465
409
 
466
- # previous length start with 0
467
- prev_length = 0.0
468
- num_device = 0.0
469
-
470
- for completion_idx in range(idx0, idx1 + 1):
471
- completion_length = min(end_completion[completion_idx], end) - max(start_completion[completion_idx], start)
472
- if completion_length <= 0:
473
- _ = "equals" if completion_length == 0 else "less than"
474
- logger.warning(
475
- f"Start depth {_} stop depth, in row {completion_idx}, "
476
- f"for well {df_completion[Headers.WELL][completion_idx]}"
477
- )
478
- # calculate cumulative parameter
479
- num_device += (completion_length / joint_length) * df_completion[Headers.VALVES_PER_JOINT].iloc[completion_idx]
480
-
481
- if completion_length > prev_length:
482
- # get well geometry
483
- inner_diameter = df_completion[Headers.INNER_DIAMETER].iloc[completion_idx]
484
- outer_diameter = df_completion[Headers.OUTER_DIAMETER].iloc[completion_idx]
485
- roughness = df_completion[Headers.ROUGHNESS].iloc[completion_idx]
486
- if outer_diameter > inner_diameter:
487
- outer_diameter = (outer_diameter**2 - inner_diameter**2) ** 0.5
488
- else:
489
- raise ValueError("Check screen/tubing and well/casing ID in case file.")
490
-
491
- # get device information
492
- device_type = df_completion[Headers.DEVICE_TYPE].iloc[completion_idx]
493
- device_number = df_completion[Headers.DEVICE_NUMBER].iloc[completion_idx]
494
- # other information
495
- annulus_zone = df_completion[Headers.ANNULUS_ZONE].iloc[completion_idx]
496
- # set prev_length to this segment
497
- prev_length = completion_length
498
-
499
- if all(
500
- x is not None for x in [device_type, device_number, inner_diameter, outer_diameter, roughness, annulus_zone]
501
- ):
502
- information = Information(
503
- num_device, device_type, device_number, inner_diameter, outer_diameter, roughness, annulus_zone
504
- )
505
- else:
506
- # I.e. if completion_length > prev_length never happens
507
- raise ValueError(
508
- f"The completion data for well '{df_completion[Headers.WELL][completion_idx]}' "
509
- "contains illegal / invalid row(s). "
510
- "Please check their start mD / end mD columns, and ensure that they start before they end."
511
- )
512
- if information is None:
513
- raise ValueError(
514
- f"idx0 == idx1 + 1 (idx0={idx0}). For the time being, the reason is unknown. "
515
- "Please reach out to the Equinor Inflow Control Team if you encounter this."
516
- )
517
- return information
410
+ indices = np.arange(idx0, idx1 + 1)
411
+ lengths = np.minimum(end_completion[indices], end) - np.maximum(start_completion[indices], start)
412
+ if (lengths <= 0).any():
413
+ # _ = "equals" if length == 0 else "less than"
414
+ # _ = np.where(lengths == 0, "equals", 0)
415
+ # _2 = np.where(lengths < 0, "less than", 0)
416
+ # logger.warning(
417
+ # f"Start depth less than or equals to stop depth,
418
+ # for {df_completion[Headers.START_MEASURED_DEPTH][indices][warning_mask]}"
419
+ # )
420
+ logger.warning("Depths are incongruent.")
421
+ number_of_devices = np.sum((lengths / joint_length) * df_completion[Headers.VALVES_PER_JOINT].to_numpy()[indices])
422
+
423
+ mask = lengths > shift_array(lengths, 1, fill_value=0)
424
+ inner_diameter = df_completion[Headers.INNER_DIAMETER].to_numpy()[indices][mask]
425
+ outer_diameter = df_completion[Headers.OUTER_DIAMETER].to_numpy()[indices][mask]
426
+ roughness = df_completion[Headers.ROUGHNESS].to_numpy()[indices][mask]
427
+ if (inner_diameter > outer_diameter).any():
428
+ raise ValueError("Check screen/tubing and well/casing ID in case file.")
429
+ outer_diameter = (outer_diameter**2 - inner_diameter**2) ** 0.5
430
+ device_type = df_completion[Headers.DEVICE_TYPE].to_numpy()[indices][mask]
431
+ device_number = df_completion[Headers.DEVICE_NUMBER].to_numpy()[indices][mask]
432
+ annulus_zone = df_completion[Headers.ANNULUS_ZONE].to_numpy()[indices][mask]
433
+
434
+ return (
435
+ number_of_devices,
436
+ device_type[-1],
437
+ device_number[-1],
438
+ inner_diameter[-1],
439
+ outer_diameter[-1],
440
+ roughness[-1],
441
+ annulus_zone[-1],
442
+ )
518
443
 
519
444
 
520
445
  def complete_the_well(
@@ -530,27 +455,47 @@ def complete_the_well(
530
455
  Returns:
531
456
  Well information.
532
457
  """
458
+ number_of_devices = []
459
+ device_type = []
460
+ device_number = []
461
+ inner_diameter = []
462
+ outer_diameter = []
463
+ roughness = []
464
+ annulus_zone = []
465
+
533
466
  start = df_tubing_segments[Headers.START_MEASURED_DEPTH].to_numpy()
534
467
  end = df_tubing_segments[Headers.END_MEASURED_DEPTH].to_numpy()
535
- # initiate completion
536
- information = Information()
468
+
537
469
  # loop through the cells
538
470
  for i in range(df_tubing_segments.shape[0]):
539
- information += get_completion(start[i], end[i], df_completion, joint_length)
540
-
541
- df_well = as_data_frame(
471
+ indices = [np.array(completion_index(df_completion, s, e)) for s, e in zip(start, end)]
472
+
473
+ if any(idx0 == -1 or idx1 == -1 for idx0, idx1 in indices):
474
+ well_name = df_completion[Headers.WELL].iloc[0]
475
+ log_and_raise_exception(f"No completion is defined on well {well_name} from {start} to {end}.")
476
+
477
+ completion_data = get_completion(start[i], end[i], df_completion, joint_length)
478
+ number_of_devices.append(completion_data[0])
479
+ device_type.append(completion_data[1])
480
+ device_number.append(completion_data[2])
481
+ inner_diameter.append(completion_data[3])
482
+ outer_diameter.append(completion_data[4])
483
+ roughness.append(completion_data[5])
484
+ annulus_zone.append(completion_data[6])
485
+
486
+ df_well = pd.DataFrame(
542
487
  {
543
- Headers.TUB_MD: df_tubing_segments[Headers.TUB_MD].to_numpy(),
544
- Headers.TUB_TVD: df_tubing_segments[Headers.TUB_TVD].to_numpy(),
488
+ Headers.TUBING_MEASURED_DEPTH: df_tubing_segments[Headers.TUBING_MEASURED_DEPTH].to_numpy(),
489
+ Headers.TRUE_VERTICAL_DEPTH: df_tubing_segments[Headers.TRUE_VERTICAL_DEPTH].to_numpy(),
545
490
  Headers.LENGTH: end - start,
546
491
  Headers.SEGMENT_DESC: df_tubing_segments[Headers.SEGMENT_DESC].to_numpy(),
547
- Headers.NUMBER_OF_DEVICES: information.number_of_devices,
548
- Headers.DEVICE_NUMBER: information.device_number,
549
- Headers.DEVICE_TYPE: information.device_type,
550
- Headers.INNER_DIAMETER: information.inner_diameter,
551
- Headers.OUTER_DIAMETER: information.outer_diameter,
552
- Headers.ROUGHNESS: information.roughness,
553
- Headers.ANNULUS_ZONE: information.annulus_zone,
492
+ Headers.NUMBER_OF_DEVICES: number_of_devices,
493
+ Headers.DEVICE_NUMBER: device_number,
494
+ Headers.DEVICE_TYPE: device_type,
495
+ Headers.INNER_DIAMETER: inner_diameter,
496
+ Headers.OUTER_DIAMETER: outer_diameter,
497
+ Headers.ROUGHNESS: roughness,
498
+ Headers.ANNULUS_ZONE: annulus_zone,
554
499
  }
555
500
  )
556
501
 
@@ -558,7 +503,7 @@ def complete_the_well(
558
503
  df_well = lumping_segments(df_well)
559
504
 
560
505
  # create scaling factor
561
- df_well[Headers.SCALING_FACTOR] = np.where(
506
+ df_well[Headers.SCALE_FACTOR] = np.where(
562
507
  df_well[Headers.NUMBER_OF_DEVICES] > 0.0, -1.0 / df_well[Headers.NUMBER_OF_DEVICES], 0.0
563
508
  )
564
509
  return df_well
@@ -606,7 +551,7 @@ def lumping_segments(df_well: pd.DataFrame) -> pd.DataFrame:
606
551
  return df_well.reset_index(drop=True, inplace=False)
607
552
 
608
553
 
609
- def get_device(df_well: pd.DataFrame, df_device: pd.DataFrame, device_type: DeviceType) -> pd.DataFrame:
554
+ def get_device(df_well: pd.DataFrame, df_device: pd.DataFrame, device_type: str) -> pd.DataFrame:
610
555
  """Get device characteristics.
611
556
 
612
557
  Args:
@@ -627,14 +572,14 @@ def get_device(df_well: pd.DataFrame, df_device: pd.DataFrame, device_type: Devi
627
572
  if f"'{Headers.DEVICE_TYPE}'" in str(err):
628
573
  raise ValueError(f"Missing keyword 'DEVICETYPE {device_type}' in input files.") from err
629
574
  raise err
630
- if device_type == "VALVE":
575
+ if device_type == Content.VALVE:
631
576
  # rescale the Cv
632
- # because no scaling factor in WSEGVALV
633
- df_well[Headers.CV] = -df_well[Headers.CV] / df_well[Headers.SCALING_FACTOR]
634
- elif device_type == "DAR":
577
+ # because no scaling factor in WELL_SEGMENTS_VALVE
578
+ df_well[Headers.FLOW_COEFFICIENT] = -df_well[Headers.FLOW_COEFFICIENT] / df_well[Headers.SCALE_FACTOR]
579
+ elif device_type == Content.DENSITY_ACTIVATED_RECOVERY:
635
580
  # rescale the Cv
636
- # because no scaling factor in WSEGVALV
637
- df_well[Headers.CV_DAR] = -df_well[Headers.CV_DAR] / df_well[Headers.SCALING_FACTOR]
581
+ # because no scaling factor in WELL_SEGMENTS_VALVE
582
+ df_well[Headers.FLOW_COEFFICIENT] = -df_well[Headers.FLOW_COEFFICIENT] / df_well[Headers.SCALE_FACTOR]
638
583
  return df_well
639
584
 
640
585
 
@@ -655,7 +600,8 @@ def correct_annulus_zone(df_well: pd.DataFrame) -> pd.DataFrame:
655
600
  continue
656
601
  df_zone = df_well[df_well[Headers.ANNULUS_ZONE] == zone]
657
602
  df_zone_device = df_zone[
658
- (df_zone[Headers.NUMBER_OF_DEVICES].to_numpy() > 0) | (df_zone[Headers.DEVICE_TYPE].to_numpy() == "PERF")
603
+ (df_zone[Headers.NUMBER_OF_DEVICES].to_numpy() > 0)
604
+ | (df_zone[Headers.DEVICE_TYPE].to_numpy() == Content.PERFORATED)
659
605
  ]
660
606
  if df_zone_device.shape[0] == 0:
661
607
  df_well[Headers.ANNULUS_ZONE].replace(zone, 0, inplace=True)
@@ -669,374 +615,38 @@ def connect_cells_to_segments(
669
615
 
670
616
  Args:
671
617
  df_well: Segment table. Must contain tubing measured depth.
672
- df_reservoir: COMPSEGS table. Must contain start and end measured depth.
618
+ df_reservoir: COMPLETION_SEGMENTS table. Must contain start and end measured depth.
673
619
  df_tubing_segments: Tubing segment dataframe. Must contain start and end measured depth.
674
620
  method: Segmentation method indicator. Must be one of 'user', 'fix', 'welsegs', or 'cells'.
675
621
 
676
622
  Returns:
677
623
  Merged DataFrame.
678
624
  """
625
+ df_well = df_well.copy()
679
626
  # Calculate mid cell measured depth
680
- df_reservoir[Headers.MD] = (
627
+ df_reservoir[Headers.MEASURED_DEPTH] = (
681
628
  df_reservoir[Headers.START_MEASURED_DEPTH] + df_reservoir[Headers.END_MEASURED_DEPTH]
682
- ) * 0.5
629
+ ) / 2
683
630
  if method == Method.USER:
684
- df_res = df_reservoir.copy(deep=True)
685
- df_wel = df_well.copy(deep=True)
686
631
  # Ensure that tubing segment boundaries as described in the case file are honored.
687
- # Associate reservoir cells with tubing segment midpoints using markers
632
+ # Associate reservoir cells with tubing segment midpoints using markers.
633
+ df_reservoir[Headers.MARKER] = np.full(df_reservoir.shape[0], 0)
634
+ df_well.loc[:, Headers.MARKER] = np.arange(df_well.shape[0]) + 1
635
+
636
+ start_measured_depths = df_tubing_segments[Headers.START_MEASURED_DEPTH]
637
+ end_measured_depths = df_tubing_segments[Headers.END_MEASURED_DEPTH]
638
+
688
639
  marker = 1
689
- df_res[Headers.MARKER] = np.full(df_reservoir.shape[0], 0)
690
- df_wel[Headers.MARKER] = np.arange(df_well.shape[0]) + 1
691
- for idx in df_wel[Headers.TUB_MD].index:
692
- start_measured_depth = df_tubing_segments[Headers.START_MEASURED_DEPTH].iloc[idx]
693
- end_measured_depth = df_tubing_segments[Headers.END_MEASURED_DEPTH].iloc[idx]
694
- df_res.loc[df_res[Headers.MD].between(start_measured_depth, end_measured_depth), Headers.MARKER] = marker
640
+ for start, end in zip(start_measured_depths, end_measured_depths):
641
+ df_reservoir.loc[df_reservoir[Headers.MEASURED_DEPTH].between(start, end), Headers.MARKER] = marker
695
642
  marker += 1
696
- # Merge
697
- tmp = df_res.merge(df_wel, on=[Headers.MARKER])
698
- return tmp.drop([Headers.MARKER], axis=1, inplace=False)
643
+
644
+ return df_reservoir.merge(df_well, on=Headers.MARKER).drop(Headers.MARKER, axis=1)
699
645
 
700
646
  return pd.merge_asof(
701
- left=df_reservoir, right=df_well, left_on=[Headers.MD], right_on=[Headers.TUB_MD], direction="nearest"
647
+ left=df_reservoir,
648
+ right=df_well,
649
+ left_on=Headers.MEASURED_DEPTH,
650
+ right_on=Headers.TUBING_MEASURED_DEPTH,
651
+ direction="nearest",
702
652
  )
703
-
704
-
705
- class WellSchedule:
706
- """A collection of all the active multi-segment wells.
707
-
708
- Attributes:
709
- msws: Multisegmented well segments.
710
- active_wells: The active wells for completor to work on.
711
-
712
- Args:
713
- active_wells: Active multi-segment wells defined in a case file.
714
- """
715
-
716
- def __init__(self, active_wells: npt.NDArray[np.unicode_] | list[str]):
717
- """Initialize WellSchedule."""
718
- self.msws: dict[str, dict] = {}
719
- self.active_wells = np.array(active_wells)
720
-
721
- def set_welspecs(self, records: list[list[str]]) -> None:
722
- """Convert the well specifications (WELSPECS) record to a Pandas DataFrame.
723
-
724
- * Sets DataFrame column titles.
725
- * Formats column values.
726
- * Pads missing columns at the end of the DataFrame with default values (1*).
727
-
728
- Args:
729
- records: Raw well specification.
730
-
731
- Returns:
732
- Record of inactive wells (in `self.msws`).
733
- """
734
- columns = [
735
- Headers.WELL,
736
- Headers.GROUP,
737
- Headers.I,
738
- Headers.J,
739
- Headers.BHP_DEPTH,
740
- Headers.PHASE,
741
- Headers.DR,
742
- Headers.FLAG,
743
- Headers.SHUT,
744
- Headers.CROSS,
745
- Headers.PRESSURE_TABLE,
746
- Headers.DENSCAL,
747
- Headers.REGION,
748
- Headers.ITEM_14,
749
- Headers.ITEM_15,
750
- Headers.ITEM_16,
751
- Headers.ITEM_17,
752
- ]
753
- _records = records[0] + ["1*"] * (len(columns) - len(records[0])) # pad with default values (1*)
754
- df = pd.DataFrame(np.array(_records).reshape((1, len(columns))), columns=columns)
755
- # datatypes
756
- df[columns[2:4]] = df[columns[2:4]].astype(np.int64)
757
- try:
758
- df[columns[4]] = df[columns[4]].astype(np.float64)
759
- except ValueError:
760
- pass
761
- # welspecs could be for multiple wells - split it
762
- for well_name in df[Headers.WELL].unique():
763
- if well_name not in self.msws:
764
- self.msws[well_name] = {}
765
- self.msws[well_name][Keywords.WELSPECS] = df[df[Headers.WELL] == well_name]
766
- logger.debug("set_welspecs for %s", well_name)
767
-
768
- def handle_compdat(self, records: list[list[str]]) -> list[list[str]]:
769
- """Convert completion data (COMPDAT) record to a DataFrame.
770
-
771
- * Sets DataFrame column titles.
772
- * Pads missing values with default values (1*).
773
- * Sets column data types.
774
-
775
- Args:
776
- records: Record set of COMPDAT data.
777
-
778
- Returns:
779
- Records for inactive wells.
780
- """
781
- well_names = set() # the active well-names found in this chunk
782
- remains = [] # the other wells
783
- for rec in records:
784
- well_name = rec[0]
785
- if well_name in list(self.active_wells):
786
- well_names.add(well_name)
787
- else:
788
- remains.append(rec)
789
- columns = [
790
- Headers.WELL,
791
- Headers.I,
792
- Headers.J,
793
- Headers.K,
794
- Headers.K2,
795
- Headers.STATUS,
796
- Headers.SATURATION_FUNCTION_REGION_NUMBERS,
797
- Headers.CONNECTION_FACTOR,
798
- Headers.DIAMETER,
799
- Headers.FORAMTION_PERMEABILITY_THICKNESS,
800
- Headers.SKIN,
801
- Headers.DFACT,
802
- Headers.COMPDAT_DIRECTION,
803
- Headers.RO,
804
- ]
805
- df = pd.DataFrame(records, columns=columns[0 : len(records[0])])
806
- if Headers.RO in df.columns:
807
- df[Headers.RO] = df[Headers.RO].fillna("1*")
808
- for i in range(len(records[0]), len(columns)):
809
- df[columns[i]] = ["1*"] * len(records)
810
- # data types
811
- df[columns[1:5]] = df[columns[1:5]].astype(np.int64)
812
- # Change default value '1*' to equivalent float
813
- df["SKIN"] = df["SKIN"].replace(["1*"], 0.0)
814
- df[[Headers.DIAMETER, Headers.SKIN]] = df[[Headers.DIAMETER, Headers.SKIN]].astype(np.float64)
815
- # check if CONNECTION_FACTOR, FORAMTION_PERMEABILITY_THICKNESS, and RO are defaulted by the users
816
- try:
817
- df[[Headers.CONNECTION_FACTOR]] = df[[Headers.CONNECTION_FACTOR]].astype(np.float64)
818
- except ValueError:
819
- pass
820
- try:
821
- df[[Headers.FORAMTION_PERMEABILITY_THICKNESS]] = df[[Headers.FORAMTION_PERMEABILITY_THICKNESS]].astype(
822
- np.float64
823
- )
824
- except ValueError:
825
- pass
826
- try:
827
- df[[Headers.RO]] = df[[Headers.RO]].astype(np.float64)
828
- except ValueError:
829
- pass
830
- # compdat could be for multiple wells - split it
831
- for well_name in well_names:
832
- if well_name not in self.msws:
833
- self.msws[well_name] = {}
834
- self.msws[well_name][Keywords.COMPDAT] = df[df[Headers.WELL] == well_name]
835
- logger.debug("handle_compdat for %s", well_name)
836
- return remains
837
-
838
- def set_welsegs(self, recs: list[list[str]]) -> str | None:
839
- """Update the well segments (WELSEGS) for a given well if it is an active well.
840
-
841
- * Pads missing record columns in header and contents with default values.
842
- * Convert header and column records to DataFrames.
843
- * Sets proper DataFrame column types and titles.
844
- * Converts segment depth specified in incremental (INC) to absolute (ABS) values using fix_welsegs.
845
-
846
- Args:
847
- recs: Record set of header and contents data.
848
-
849
- Returns:
850
- Name of well if it was updated, or None if it is not in the active_wells list.
851
- """
852
- well_name = recs[0][0] # each WELSEGS-chunk is for one well only
853
- if well_name not in self.active_wells:
854
- return None
855
-
856
- # make df for header record
857
- columns_header = [
858
- Headers.WELL,
859
- Headers.SEGMENTTVD,
860
- Headers.SEGMENTMD,
861
- Headers.WBVOLUME,
862
- Headers.INFO_TYPE,
863
- Headers.PDROPCOMP,
864
- Headers.MPMODEL,
865
- Headers.ITEM_8,
866
- Headers.ITEM_9,
867
- Headers.ITEM_10,
868
- Headers.ITEM_11,
869
- Headers.ITEM_12,
870
- ]
871
- # pad header with default values (1*)
872
- header = recs[0] + ["1*"] * (len(columns_header) - len(recs[0]))
873
- df_header = pd.DataFrame(np.array(header).reshape((1, len(columns_header))), columns=columns_header)
874
- df_header[columns_header[1:3]] = df_header[columns_header[1:3]].astype(np.float64) # data types
875
-
876
- # make df for data records
877
- columns_data = [
878
- Headers.TUBING_SEGMENT,
879
- Headers.TUBING_SEGMENT_2,
880
- Headers.TUBINGBRANCH,
881
- Headers.TUBING_OUTLET,
882
- Headers.TUBINGMD,
883
- Headers.TUBINGTVD,
884
- Headers.TUBING_INNER_DIAMETER,
885
- Headers.TUBING_ROUGHNESS,
886
- Headers.CROSS,
887
- Headers.VSEG,
888
- Headers.ITEM_11,
889
- Headers.ITEM_12,
890
- Headers.ITEM_13,
891
- Headers.ITEM_14,
892
- Headers.ITEM_15,
893
- ]
894
- # pad with default values (1*)
895
- recs = [rec + ["1*"] * (len(columns_data) - len(rec)) for rec in recs[1:]]
896
- df_records = pd.DataFrame(recs, columns=columns_data)
897
- # data types
898
- df_records[columns_data[:4]] = df_records[columns_data[:4]].astype(np.int64)
899
- df_records[columns_data[4:7]] = df_records[columns_data[4:7]].astype(np.float64)
900
- # fix abs/inc issue with welsegs
901
- df_header, df_records = fix_welsegs(df_header, df_records)
902
-
903
- # Warn user if the tubing segments' measured depth for a branch
904
- # is not sorted in ascending order (monotonic)
905
- for branch_num in df_records[Headers.TUBINGBRANCH].unique():
906
- if (
907
- not df_records[Headers.TUBINGMD]
908
- .loc[df_records[Headers.TUBINGBRANCH] == branch_num]
909
- .is_monotonic_increasing
910
- ):
911
- logger.warning(
912
- "The branch %s in well %s contains negative length segments. "
913
- "Check the input schedulefile WELSEGS keyword for inconsistencies "
914
- "in measured depth (MD) of Tubing layer.",
915
- branch_num,
916
- well_name,
917
- )
918
-
919
- if well_name not in self.msws:
920
- self.msws[well_name] = {}
921
- self.msws[well_name][Keywords.WELSEGS] = df_header, df_records
922
- return well_name
923
-
924
- def set_compsegs(self, recs: list[list[str]]) -> str | None:
925
- """Update COMPSEGS for a well if it is an active well.
926
-
927
- * Pads missing record columns in header and contents with default 1*.
928
- * Convert header and column records to DataFrames.
929
- * Sets proper DataFrame column types and titles.
930
-
931
- Args:
932
- recs: Record set of header and contents data.
933
-
934
- Returns:
935
- Name of well if it was updated, or None if it is not in active_wells.
936
- """
937
- well_name = recs[0][0] # each COMPSEGS-chunk is for one well only
938
- if well_name not in self.active_wells:
939
- return None
940
- columns = [
941
- Headers.I,
942
- Headers.J,
943
- Headers.K,
944
- Headers.BRANCH,
945
- Headers.START_MEASURED_DEPTH,
946
- Headers.END_MEASURED_DEPTH,
947
- Headers.COMPSEGS_DIRECTION,
948
- Headers.ENDGRID,
949
- Headers.PERFDEPTH,
950
- Headers.THERM,
951
- Headers.SEGMENT,
952
- ]
953
- recs = [rec + ["1*"] * (len(columns) - len(rec)) for rec in recs[1:]] # pad with default values (1*)
954
- df = pd.DataFrame(recs, columns=columns)
955
- df[columns[:4]] = df[columns[:4]].astype(np.int64)
956
- df[columns[4:6]] = df[columns[4:6]].astype(np.float64)
957
- if well_name not in self.msws:
958
- self.msws[well_name] = {}
959
- self.msws[well_name][Keywords.COMPSEGS] = df
960
- logger.debug("set_compsegs for %s", well_name)
961
- return well_name
962
-
963
- def get_welspecs(self, well_name: str) -> pd.DataFrame:
964
- """Get-function for WELSPECS.
965
-
966
- Args:
967
- well_name: Well name.
968
-
969
- Returns:
970
- Well specifications.
971
- """
972
- return self.msws[well_name][Keywords.WELSPECS]
973
-
974
- def get_compdat(self, well_name: str) -> pd.DataFrame:
975
- """Get-function for COMPDAT.
976
-
977
- Args:
978
- well_name: Well name.
979
-
980
- Returns:
981
- Completion data.
982
-
983
- Raises:
984
- ValueError: If completion data keyword is missing in input schedule file.
985
- """
986
- try:
987
- return self.msws[well_name][Keywords.COMPDAT]
988
- except KeyError as err:
989
- if f"'{Keywords.COMPDAT}'" in str(err):
990
- raise ValueError("Input schedule file missing COMPDAT keyword.") from err
991
- raise err
992
-
993
- def get_compsegs(self, well_name: str, branch: int | None = None) -> pd.DataFrame:
994
- """Get-function for COMPSEGS.
995
-
996
- Args:
997
- well_name: Well name.
998
- branch: Branch number.
999
-
1000
- Returns:
1001
- Completion segment data.
1002
- """
1003
- df = self.msws[well_name][Keywords.COMPSEGS].copy()
1004
- if branch is not None:
1005
- df = df[df[Headers.BRANCH] == branch]
1006
- df.reset_index(drop=True, inplace=True) # reset index after filtering
1007
- return fix_compsegs(df, well_name)
1008
-
1009
- def get_well_segments(self, well_name: str, branch: int | None = None) -> tuple[pd.DataFrame, pd.DataFrame]:
1010
- """Get-function for well segments.
1011
-
1012
- Args:
1013
- well_name: Well name.
1014
- branch: Branch number.
1015
-
1016
- Returns:
1017
- Well segments headers and content.
1018
-
1019
- Raises:
1020
- ValueError: If WELSEGS keyword missing in input schedule file.
1021
- """
1022
- try:
1023
- columns, content = self.msws[well_name][Keywords.WELSEGS]
1024
- except KeyError as err:
1025
- if f"'{Keywords.WELSEGS}'" in str(err):
1026
- raise ValueError("Input schedule file missing WELSEGS keyword.") from err
1027
- raise err
1028
- if branch is not None:
1029
- content = content[content[Headers.TUBINGBRANCH] == branch]
1030
- content.reset_index(drop=True, inplace=True)
1031
- return columns, content
1032
-
1033
- def get_well_number(self, well_name: str) -> int:
1034
- """Well number in the active_wells list.
1035
-
1036
- Args:
1037
- well_name: Well name.
1038
-
1039
- Returns:
1040
- Well number.
1041
- """
1042
- return int(np.where(self.active_wells == well_name)[0][0])