completor 1.1.3__tar.gz → 1.3.0__tar.gz

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.
Files changed (26) hide show
  1. {completor-1.1.3 → completor-1.3.0}/PKG-INFO +7 -7
  2. {completor-1.1.3 → completor-1.3.0}/README.md +2 -2
  3. {completor-1.1.3 → completor-1.3.0}/completor/completion.py +5 -1
  4. {completor-1.1.3 → completor-1.3.0}/completor/constants.py +15 -1
  5. {completor-1.1.3 → completor-1.3.0}/completor/create_output.py +84 -15
  6. {completor-1.1.3 → completor-1.3.0}/completor/input_validation.py +20 -1
  7. {completor-1.1.3 → completor-1.3.0}/completor/main.py +8 -2
  8. {completor-1.1.3 → completor-1.3.0}/completor/prepare_outputs.py +363 -10
  9. {completor-1.1.3 → completor-1.3.0}/completor/read_casefile.py +59 -4
  10. {completor-1.1.3 → completor-1.3.0}/completor/utils.py +1 -0
  11. {completor-1.1.3 → completor-1.3.0}/completor/visualize_well.py +2 -0
  12. {completor-1.1.3 → completor-1.3.0}/completor/wells.py +2 -0
  13. {completor-1.1.3 → completor-1.3.0}/pyproject.toml +6 -5
  14. {completor-1.1.3 → completor-1.3.0}/LICENSE +0 -0
  15. {completor-1.1.3 → completor-1.3.0}/completor/__init__.py +0 -0
  16. {completor-1.1.3 → completor-1.3.0}/completor/config_jobs/run_completor +0 -0
  17. {completor-1.1.3 → completor-1.3.0}/completor/exceptions/__init__.py +0 -0
  18. {completor-1.1.3 → completor-1.3.0}/completor/exceptions/clean_exceptions.py +0 -0
  19. {completor-1.1.3 → completor-1.3.0}/completor/exceptions/exceptions.py +0 -0
  20. {completor-1.1.3 → completor-1.3.0}/completor/get_version.py +0 -0
  21. {completor-1.1.3 → completor-1.3.0}/completor/hook_implementations/jobs.py +0 -0
  22. {completor-1.1.3 → completor-1.3.0}/completor/launch_args_parser.py +0 -0
  23. {completor-1.1.3 → completor-1.3.0}/completor/logger.py +0 -0
  24. {completor-1.1.3 → completor-1.3.0}/completor/parse.py +0 -0
  25. {completor-1.1.3 → completor-1.3.0}/completor/read_schedule.py +0 -0
  26. {completor-1.1.3 → completor-1.3.0}/completor/visualization.py +0 -0
@@ -1,29 +1,29 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: completor
3
- Version: 1.1.3
3
+ Version: 1.3.0
4
4
  Summary: Advanced multi-segmented well completion tool.
5
5
  Home-page: https://github.com/equinor/completor
6
6
  License: LGPL-3.0-only
7
7
  Author: Equinor ASA
8
8
  Author-email: opensource@equinor.com
9
- Requires-Python: >=3.11,<4.0
9
+ Requires-Python: >=3.11,<3.14
10
10
  Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)
11
11
  Classifier: Natural Language :: English
12
12
  Classifier: Operating System :: OS Independent
13
13
  Classifier: Programming Language :: Python :: 3
14
14
  Classifier: Programming Language :: Python :: 3.11
15
15
  Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
16
17
  Provides-Extra: ert
17
18
  Provides-Extra: test
18
- Requires-Dist: ert (>=12,<13) ; extra == "ert"
19
+ Requires-Dist: ert (>=14,<15) ; extra == "ert"
19
20
  Requires-Dist: matplotlib (>=3.9,<4.0)
20
- Requires-Dist: numpy (>=1.26,<3.0)
21
+ Requires-Dist: numpy (>=2.0,<3.0)
21
22
  Requires-Dist: pandas (>=2.2,<3.0)
22
23
  Requires-Dist: pytest (>=8.3,<9.0) ; extra == "test"
23
24
  Requires-Dist: pytest-env (>=1,<2) ; extra == "test"
24
25
  Requires-Dist: pytest-xdist (>=3.6,<4.0) ; extra == "test"
25
26
  Requires-Dist: rstcheck-core (>=1.2,<2.0) ; extra == "test"
26
- Requires-Dist: scipy (>=1.14,<2.0)
27
27
  Requires-Dist: tqdm (>=4.66,<5.0)
28
28
  Project-URL: Bug Tracker, https://github.com/equinor/completor/issues
29
29
  Project-URL: Documentation, https://equinor.github.io/completor
@@ -44,7 +44,7 @@ Detailed documentation for usage of completor can be found at https://equinor.gi
44
44
  ## Getting started as a user
45
45
 
46
46
  ### Prerequisites
47
- * [Python](https://www.python.org/), version 3.11
47
+ * [Python](https://www.python.org/), version 3.11 or 3.13.
48
48
  * [ERT](https://github.com/equinor/ert) (optional, and only available on Linux.)
49
49
 
50
50
  ### Installation
@@ -85,7 +85,7 @@ and making a pull request.
85
85
  See [Contribution Document](documentation/docs/contribution_guide.mdx) on how to contribute.
86
86
 
87
87
  ### Install completor as dev
88
- In order to run Completor® you need to have versions of [Python 3.11](https://www.python.org/downloads/) installed.
88
+ In order to run Completor® you need to have versions of [Python 3.11, 3.12 or 3.13](https://www.python.org/downloads/) installed.
89
89
  #### Source code
90
90
  Clone the [Completor® repository](https://github.com/equinor/completor) to get the source code.
91
91
  ```bash
@@ -12,7 +12,7 @@ Detailed documentation for usage of completor can be found at https://equinor.gi
12
12
  ## Getting started as a user
13
13
 
14
14
  ### Prerequisites
15
- * [Python](https://www.python.org/), version 3.11
15
+ * [Python](https://www.python.org/), version 3.11 or 3.13.
16
16
  * [ERT](https://github.com/equinor/ert) (optional, and only available on Linux.)
17
17
 
18
18
  ### Installation
@@ -53,7 +53,7 @@ and making a pull request.
53
53
  See [Contribution Document](documentation/docs/contribution_guide.mdx) on how to contribute.
54
54
 
55
55
  ### Install completor as dev
56
- In order to run Completor® you need to have versions of [Python 3.11](https://www.python.org/downloads/) installed.
56
+ In order to run Completor® you need to have versions of [Python 3.11, 3.12 or 3.13](https://www.python.org/downloads/) installed.
57
57
  #### Source code
58
58
  Clone the [Completor® repository](https://github.com/equinor/completor) to get the source code.
59
59
  ```bash
@@ -551,7 +551,7 @@ def get_device(df_well: pd.DataFrame, df_device: pd.DataFrame, device_type: str)
551
551
  Args:
552
552
  df_well: Must contain device type, device number, and the scaling factor.
553
553
  df_device: Device table.
554
- device_type: Device type. `AICD`, `ICD`, `DENSITY`, `VALVE`, `DUALRCP`, `ICV`.
554
+ device_type: Device type. `AICD`, `ICD`, `DENSITY`, `VALVE`, `DUALRCP`, `ICV`, `INJV`.
555
555
 
556
556
  Returns:
557
557
  Updated well information with device characteristics.
@@ -581,6 +581,10 @@ def get_device(df_well: pd.DataFrame, df_device: pd.DataFrame, device_type: str)
581
581
  # rescale the Cv
582
582
  # because no scaling factor in WELL_SEGMENTS_VALVE
583
583
  df_well[Headers.FLOW_COEFFICIENT] = -df_well[Headers.FLOW_COEFFICIENT] / df_well[Headers.SCALE_FACTOR]
584
+ elif device_type == Content.INJECTION_VALVE:
585
+ # rescale the Cv
586
+ # because no scaling factor in WELL_SEGMENTS_VALVE
587
+ df_well[Headers.FLOW_COEFFICIENT] = -df_well[Headers.FLOW_COEFFICIENT] / df_well[Headers.SCALE_FACTOR]
584
588
  return df_well
585
589
 
586
590
 
@@ -78,7 +78,7 @@ class _Headers:
78
78
  DR = "DR"
79
79
  FLAG = "FLAG" # This is actually a header, but OPEN, SHUT, and AUTO are its possible values, see manual on COMPLETION_DATA.
80
80
  SHUT = "SHUT"
81
- # CROSS = "CROSS"
81
+ CROSS = "CROSS"
82
82
  PRESSURE_TABLE = "PRESSURETABLE"
83
83
  DENSITY_CALCULATION_TYPE = "DENSCAL" # Type of density calculation for the wellbore hydrostatic head.
84
84
  REGION = "REGION"
@@ -126,6 +126,7 @@ class _Headers:
126
126
  # This stops making sense from here on out?
127
127
  X = "X"
128
128
  Y = "Y"
129
+ Z = "Z"
129
130
  # FLAG
130
131
  A = "A"
131
132
  B = "B"
@@ -165,6 +166,15 @@ class _Headers:
165
166
  GAS_HOLDUP_FRACTION_LOW_CUTOFF = "GAS_HOLDUP_FRACTION_LOW_CUTOFF"
166
167
  GAS_HOLDUP_FRACTION_HIGH_CUTOFF = "GAS_HOLDUP_FRACTION_HIGH_CUTOFF"
167
168
 
169
+ # Injection Valve Well Segments (WSEGINJV)
170
+ # DEVICE_NUMBER
171
+ TRIGGER_PARAMETER = "TRIGGER_PARAMETER"
172
+ TRIGGER_VALUE = "TRIGGER_VALUE"
173
+ # FLOW_COEFFICIENT / Cv
174
+ # FLOW_CROSS_SECTIONAL_AREA
175
+ PRIMARY_FLOW_CROSS_SECTIONAL_AREA = "PRIMARY_FLOW_CROSS_SECTIONAL_AREA"
176
+ SECONDARY_FLOW_CROSS_SECTIONAL_AREA = "SECONDARY_FLOW_CROSS_SECTIONAL_AREA"
177
+
168
178
  # Miscellaneous
169
179
  DEFAULTS = "DEFAULTS"
170
180
  MEASURED_DEPTH = "MEASURED_DEPTH"
@@ -244,7 +254,9 @@ class _Keywords:
244
254
  INFLOW_CONTROL_VALVE = "WSEGICV"
245
255
  INFLOW_CONTROL_DEVICE = "WSEGSICD"
246
256
  DENSITY = "WSEGDENSITY"
257
+ PYTHON_DEPENDENT = "PYTHON"
247
258
  DENSITY_ACTIVATED_RECOVERY = "WSEGDAR"
259
+ INJECTION_VALVE = "WSEGINJV"
248
260
  LATERAL_TO_DEVICE = "LATERAL_TO_DEVICE"
249
261
  JOINT_LENGTH = "JOINTLENGTH"
250
262
  SEGMENT_LENGTH = "SEGMENTLENGTH"
@@ -290,6 +302,7 @@ class _Content:
290
302
  AUTONOMOUS_INFLOW_CONTROL_DEVICE = "AICD"
291
303
  DENSITY = "DENSITY"
292
304
  DENSITY_ACTIVATED_RECOVERY = "DAR"
305
+ INJECTION_VALVE = "INJV"
293
306
  VALVE = "VALVE"
294
307
  DEVICE_TYPES = [
295
308
  AUTONOMOUS_INFLOW_CONTROL_DEVICE,
@@ -297,6 +310,7 @@ class _Content:
297
310
  AUTONOMOUS_INFLOW_CONTROL_VALVE,
298
311
  DENSITY,
299
312
  DENSITY_ACTIVATED_RECOVERY,
313
+ INJECTION_VALVE,
300
314
  INFLOW_CONTROL_DEVICE,
301
315
  VALVE,
302
316
  INFLOW_CONTROL_VALVE,
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import getpass
6
6
  from datetime import datetime
7
7
 
8
+ import matplotlib.pyplot as plt # type: ignore
8
9
  import numpy as np
9
10
  import numpy.typing as npt
10
11
  import pandas as pd
@@ -20,13 +21,13 @@ from completor.visualize_well import visualize_well
20
21
  from completor.wells import Lateral, Well
21
22
 
22
23
 
23
- def format_output(well: Well, case: ReadCasefile, figure_name: str | None = None) -> tuple[str, str, str, str]:
24
+ def format_output(well: Well, case: ReadCasefile, pdf: PdfPages | None = None) -> tuple[str, str, str, str]:
24
25
  """Formats the finished output string to be written to a file.
25
26
 
26
27
  Args:
27
28
  well: Well data.
28
29
  case: Case data.
29
- figure_name: The name of the figure, if None, no figure is printed. Defaults to None.
30
+ pdf: The name of the figure, if None, no figure is printed. Defaults to None.
30
31
 
31
32
  Returns:
32
33
  Properly formatted output data for completion data, well segments, completion segments, and bonus.
@@ -41,7 +42,10 @@ def format_output(well: Well, case: ReadCasefile, figure_name: str | None = None
41
42
  print_autonomous_inflow_control_device = ""
42
43
  print_inflow_control_device = ""
43
44
  print_density_driven = ""
45
+ print_density_driven_include = ""
46
+ print_injection_valve = ""
44
47
  print_dual_rate_controlled_production = ""
48
+ print_density_driven_pyaction = ""
45
49
 
46
50
  start_segment = 2
47
51
  start_branch = 1
@@ -111,6 +115,7 @@ def format_output(well: Well, case: ReadCasefile, figure_name: str | None = None
111
115
  well.well_name, lateral.df_well, lateral.df_device
112
116
  )
113
117
  df_density_driven = prepare_outputs.prepare_density_driven(well.well_name, lateral.df_well, lateral.df_device)
118
+ df_injection_valve = prepare_outputs.prepare_injection_valve(well.well_name, lateral.df_well, lateral.df_device)
114
119
  df_dual_rate_controlled_production = prepare_outputs.prepare_dual_rate_controlled_production(
115
120
  well.well_name, lateral.df_well, lateral.df_device
116
121
  )
@@ -145,22 +150,35 @@ def format_output(well: Well, case: ReadCasefile, figure_name: str | None = None
145
150
  print_inflow_control_valve += _format_inflow_control_valve(
146
151
  well.well_name, lateral.lateral_number, df_inflow_control_valve, first
147
152
  )
148
- print_density_driven += _format_density_driven(well.well_number, df_density_driven)
153
+ print_injection_valve += _format_injection_valve(well.well_number, df_injection_valve)
149
154
  print_dual_rate_controlled_production += _format_dual_rate_controlled_production(
150
155
  well.well_number, df_dual_rate_controlled_production
151
156
  )
157
+ # output using ACTIONX (if-else) logic is dual RCP, density driven, and injection valve
158
+ if case.python_dependent:
159
+ # print the python file out
160
+ # append all laterals for density driven, dual RCP, and injection valve
161
+ # TODO(#274): Add functionality for dual RCP
162
+ print_density_driven_pyaction = _format_density_driven_pyaction(df_density_driven)
163
+ output_directory = prepare_outputs.print_python_file(
164
+ print_density_driven_pyaction, str(case.output_file), well.well_name, lateral.lateral_number
165
+ )
166
+ print_density_driven_include += prepare_outputs.print_wsegdensity_include(
167
+ output_directory, well.well_name, lateral.lateral_number
168
+ )
169
+ else:
170
+ print_density_driven += _format_density_driven(well.well_number, df_density_driven)
152
171
 
153
- if figure_name is not None:
154
- logger.info(f"Creating figure for lateral {lateral.lateral_number}.")
155
- with PdfPages(figure_name) as figure:
156
- figure.savefig(
157
- visualize_well(
158
- well.well_name, well.df_well_all_laterals, well.df_reservoir_all_laterals, case.segment_length
159
- ),
160
- orientation="landscape",
161
- )
162
- logger.info("Creating schematics: %s.pdf", figure_name)
172
+ if pdf is not None:
173
+ logger.info(f"Creating figure for well {well.well_name}, lateral {lateral.lateral_number}.")
174
+ fig = visualize_well(
175
+ well.well_name, well.df_well_all_laterals, well.df_reservoir_all_laterals, case.segment_length
176
+ )
177
+ pdf.savefig(fig, orientation="landscape")
178
+ plt.close(fig)
179
+ logger.info("Creating schematics: %s", pdf)
163
180
  first = False
181
+
164
182
  print_completion_data = "\n".join(completion_data_list)
165
183
  if print_well_segments:
166
184
  print_well_segments = f"{print_well_segments}\n/\n\n"
@@ -182,7 +200,7 @@ def format_output(well: Well, case: ReadCasefile, figure_name: str | None = None
182
200
  if print_density_driven:
183
201
  metadata = (
184
202
  f"{'-' * 100}\n"
185
- "-- This is how we model DENSITY technology using sets of ACTIONX keywords.\n"
203
+ "-- This is how we model density driven technology using sets of ACTIONX keywords.\n"
186
204
  "-- The segment dP curves changes according to the segment water-\n"
187
205
  "-- and gas volume fractions at downhole condition.\n"
188
206
  "-- The value of Cv is adjusted according to the segment length and the number of\n"
@@ -191,15 +209,37 @@ def format_output(well: Well, case: ReadCasefile, figure_name: str | None = None
191
209
  f"{'-' * 100}\n\n\n"
192
210
  )
193
211
  bonus.append(metadata + print_density_driven + "\n\n\n\n")
212
+ if print_injection_valve:
213
+ metadata = (
214
+ f"{'-' * 100}\n"
215
+ "-- This is how we model autonomous injection valve technology using sets of ACTIONX keywords.\n"
216
+ "-- The DP paramaters changes according to the trigger parameter.-\n"
217
+ "-- The value of Cv is adjusted according to the segment length and the number of\n"
218
+ "-- devices per joint. The constriction area will change if the parameter is triggered.\n"
219
+ f"{'-' * 100}\n\n\n"
220
+ )
221
+ bonus.append(metadata + print_injection_valve + "\n\n\n\n")
194
222
  if print_dual_rate_controlled_production:
195
223
  metadata = (
196
224
  f"{'-' * 100}\n"
197
- "-- This is how we model DUALRCP technology using sets of ACTIONX keyword\n"
225
+ "-- This is how we model dual RCP curves using sets of ACTIONX keyword\n"
198
226
  "-- the DP parameters change according to the segment water cut (at downhole condition )\n"
199
227
  "-- and gas volume fraction (at downhole condition)\n"
200
228
  f"{'-' * 100}\n\n\n"
201
229
  )
202
230
  bonus.append(metadata + print_dual_rate_controlled_production + "\n\n\n\n")
231
+ if print_density_driven_pyaction:
232
+ metadata = (
233
+ f"{'-' * 100}\n"
234
+ "-- This is how we model density driven technology for python dependent keyword.\n"
235
+ "-- The segment dP curves changes according to the segment water-\n"
236
+ "-- and gas volume fractions at downhole condition.\n"
237
+ "-- The value of Cv is adjusted according to the segment length and the number of\n"
238
+ "-- devices per joint. The constriction area varies according to values of\n"
239
+ "-- volume fractions.\n"
240
+ f"{'-' * 100}\n\n\n"
241
+ )
242
+ bonus.append(metadata + print_density_driven_include + "\n\n\n\n")
203
243
 
204
244
  return print_completion_data, print_well_segments, print_completion_segments, "".join(bonus)
205
245
 
@@ -450,6 +490,35 @@ def _format_density_driven(well_number: int, df_wsegdensity: pd.DataFrame) -> st
450
490
  return prepare_outputs.print_wsegdensity(df_wsegdensity, well_number + 1)
451
491
 
452
492
 
493
+ def _format_density_driven_pyaction(df_wsegdensity: pd.DataFrame) -> str:
494
+ """Formats well-segments for density driven valve.
495
+
496
+ Args:
497
+ df_wsegdensity: Data to print.
498
+
499
+ Returns:
500
+ Formatted string.
501
+ """
502
+ if df_wsegdensity.empty:
503
+ return ""
504
+ return prepare_outputs.print_wsegdensity_pyaction(df_wsegdensity)
505
+
506
+
507
+ def _format_injection_valve(well_number: int, df_wseginjv: pd.DataFrame) -> str:
508
+ """Formats well-segments for injection valve.
509
+
510
+ Args:
511
+ well_number: The well's number
512
+ df_wsegdinjv: Data to print.
513
+
514
+ Returns:
515
+ Formatted string.
516
+ """
517
+ if df_wseginjv.empty:
518
+ return ""
519
+ return prepare_outputs.print_wseginjv(df_wseginjv, well_number + 1)
520
+
521
+
453
522
  def _format_dual_rate_controlled_production(well_number: int, df_wsegdualrcp: pd.DataFrame) -> str:
454
523
  """Formats the DUALRCP section.
455
524
 
@@ -173,7 +173,7 @@ def _check_for_errors(df_comp: pd.DataFrame, well_name: str, idx: int) -> None:
173
173
  if df_comp[Headers.DEVICE_TYPE].iloc[idx] not in Content.DEVICE_TYPES:
174
174
  raise CompletorError(
175
175
  f"{df_comp[Headers.DEVICE_TYPE].iloc[idx]} is not a valid device type. "
176
- "Valid types are PERF, AICD, ICD, VALVE, DENSITY, DUALRCP, and ICV."
176
+ "Valid types are PERF, AICD, ICD, VALVE, DENSITY, INJV, DUALRCP, and ICV."
177
177
  )
178
178
  if df_comp[Headers.ANNULUS].iloc[idx] not in Content.ANNULUS_TYPES:
179
179
  raise CompletorError(
@@ -290,6 +290,25 @@ def set_format_wsegdensity(df_temp: pd.DataFrame) -> pd.DataFrame:
290
290
  return df_temp
291
291
 
292
292
 
293
+ def set_format_wseginjv(df_temp: pd.DataFrame) -> pd.DataFrame:
294
+ """Format the well segments Injection Valve (INJV) data.
295
+
296
+ Args:
297
+ df_temp: Well segments INJV device data.
298
+
299
+ Returns:
300
+ Updated data.
301
+ """
302
+ df_temp[Headers.DEVICE_NUMBER] = df_temp[Headers.DEVICE_NUMBER].astype(np.int64)
303
+ # left out devicenumber and trigger parameter because devicenumber has been formatted as integer
304
+ # trigger parameter is a string
305
+ columns = df_temp.columns.to_numpy()[2:]
306
+ df_temp[columns] = df_temp[columns].astype(np.float64)
307
+ # Create ID device column
308
+ df_temp.insert(0, Headers.DEVICE_TYPE, np.full(df_temp.shape[0], Content.INJECTION_VALVE))
309
+ return df_temp
310
+
311
+
293
312
  def set_format_wsegdualrcp(df_temp: pd.DataFrame) -> pd.DataFrame:
294
313
  """Format the well segments Dual RCP (DUALRCP) table.
295
314
 
@@ -5,8 +5,10 @@ from __future__ import annotations
5
5
  import logging
6
6
  import os
7
7
  import re
8
+ import sys
8
9
  import time
9
10
 
11
+ from matplotlib.backends.backend_pdf import PdfPages # type: ignore
10
12
  from tqdm import tqdm
11
13
 
12
14
  from completor import create_output, parse, read_schedule, utils
@@ -89,6 +91,7 @@ def create(
89
91
  """
90
92
  case = ReadCasefile(case_file=case_file, schedule_file=schedule, output_file=new_file)
91
93
  active_wells = utils.get_active_wells(case.completion_table, case.gp_perf_devicelayer)
94
+ pdf = None
92
95
  figure_name = None
93
96
  if show_fig:
94
97
  figure_no = 1
@@ -96,6 +99,7 @@ def create(
96
99
  while os.path.isfile(figure_name):
97
100
  figure_no += 1
98
101
  figure_name = f"Well_schematic_{figure_no:03d}.pdf"
102
+ pdf = PdfPages(figure_name)
99
103
 
100
104
  err: Exception | None = None
101
105
  well = None
@@ -122,13 +126,13 @@ def create(
122
126
  for chunk in find_keyword_data(Keywords.COMPLETION_SEGMENTS, schedule):
123
127
  clean_data = clean_raw_data(chunk, Keywords.COMPLETION_SEGMENTS)
124
128
  meaningful_data = read_schedule.set_compsegs(meaningful_data, clean_data)
125
- for i, well_name in tqdm(enumerate(active_wells.tolist()), total=len(active_wells)):
129
+ for i, well_name in tqdm(enumerate(active_wells.tolist()), total=len(active_wells), file=sys.stdout):
126
130
  try:
127
131
  well = Well(well_name, i, case, meaningful_data[well_name])
128
132
  except KeyError:
129
133
  logger.warning(f"Well '{well_name}' is written in case file but does not exist in schedule file.")
130
134
  continue
131
- compdat, welsegs, compsegs, bonus = create_output.format_output(well, case, figure_name)
135
+ compdat, welsegs, compsegs, bonus = create_output.format_output(well, case, pdf)
132
136
  for keyword in [Keywords.COMPLETION_SEGMENTS, Keywords.WELL_SEGMENTS, Keywords.COMPLETION_DATA]:
133
137
  old_data = find_well_keyword_data(well_name, keyword, schedule)
134
138
  if not old_data:
@@ -156,6 +160,8 @@ def create(
156
160
  schedule = replace_preprocessing_names(schedule, case.mapper)
157
161
  with open(new_file, "w", encoding="utf-8") as file:
158
162
  file.write(schedule)
163
+ if pdf is not None:
164
+ pdf.close()
159
165
 
160
166
  if err is not None:
161
167
  raise err
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import math
4
4
  from collections.abc import MutableMapping
5
+ from pathlib import Path
5
6
  from typing import Any
6
7
 
7
8
  import numpy as np
@@ -120,6 +121,7 @@ def dataframe_tostring(
120
121
  "END_MD": "{:.3f}".format,
121
122
  Headers.FLOW_COEFFICIENT: "{:.10g}".format,
122
123
  "CV": "{:.10g}".format,
124
+ Headers.CROSS: "{:.3e}".format,
123
125
  Headers.FLOW_CROSS_SECTIONAL_AREA: "{:.3e}".format,
124
126
  "FLOW_CROSS_SECTIONAL_AREA": "{:.3e}".format,
125
127
  Headers.OIL_FLOW_CROSS_SECTIONAL_AREA: "{:.3e}".format,
@@ -135,6 +137,13 @@ def dataframe_tostring(
135
137
  Headers.ALPHA_PILOT: "{:.10g}".format,
136
138
  }
137
139
 
140
+ # Cast floats to str befor headers are messed up (pandas formatter does not work reliably with MultiIndex headers).
141
+ for column, formatter in formatters.items():
142
+ try:
143
+ df_temp[column] = df_temp[column].map(formatter)
144
+ except (KeyError, ValueError):
145
+ pass
146
+
138
147
  if header:
139
148
  # Modify headers to reduce width.
140
149
  column_splits = [tuple(column.split("_")) for column in df_temp.columns]
@@ -173,9 +182,7 @@ def dataframe_tostring(
173
182
  df_temp.columns = pd.MultiIndex.from_frame(new_cols)
174
183
 
175
184
  try:
176
- output_string = df_temp.to_string(
177
- index=False, justify="justify", formatters=formatters, header=header, sparsify=False
178
- )
185
+ output_string = df_temp.to_string(index=False, justify="justify", header=header, sparsify=False)
179
186
  except ValueError:
180
187
  if df_temp.isnull().values.any():
181
188
  raise CompletorError("Got NaN values in table, please report if encountered!")
@@ -184,18 +191,14 @@ def dataframe_tostring(
184
191
  df_temp = df_temp.replace("1*", np.nan, inplace=False)
185
192
  # Probably find columns where this is the case and cast to numeric after replacing with nan?
186
193
  df_temp[columns_with_1_star] = df_temp[columns_with_1_star].astype(np.float64, errors="ignore")
187
- output_string = df_temp.to_string(
188
- index=False, justify="justify", formatters=formatters, header=header, sparsify=False, na_rep="1*"
189
- )
194
+ output_string = df_temp.to_string(index=False, justify="justify", header=header, sparsify=False, na_rep="1*")
190
195
 
191
196
  if output_string is None:
192
197
  return ""
193
198
 
194
199
  too_long_lines = check_width_lines(output_string, limit)
195
200
  if too_long_lines:
196
- output_string = df_temp.to_string(
197
- index=False, justify="left", formatters=formatters, header=header, sparsify=False
198
- )
201
+ output_string = df_temp.to_string(index=False, justify="left", header=header, sparsify=False)
199
202
  if output_string is None:
200
203
  return ""
201
204
  too_long_lines2 = check_width_lines(output_string, limit)
@@ -454,7 +457,11 @@ def prepare_device_layer(df_well: pd.DataFrame, df_tubing: pd.DataFrame, device_
454
457
  df_well[Headers.DEVICE_TYPE] == Content.DUAL_RATE_CONTROLLED_PRODUCTION,
455
458
  "/ -- DUALRCP types",
456
459
  np.where(
457
- df_well[Headers.DEVICE_TYPE] == Content.INFLOW_CONTROL_VALVE, "/ -- ICV types", ""
460
+ df_well[Headers.DEVICE_TYPE] == Content.INJECTION_VALVE,
461
+ "/ -- INJV types",
462
+ np.where(
463
+ df_well[Headers.DEVICE_TYPE] == Content.INFLOW_CONTROL_VALVE, "/ -- ICV types", ""
464
+ ),
458
465
  ),
459
466
  ),
460
467
  ),
@@ -1068,6 +1075,8 @@ def prepare_autonomous_inflow_control_device(
1068
1075
  wsegaicd[Headers.D] = df_merge[Headers.D].to_numpy()
1069
1076
  wsegaicd[Headers.E] = df_merge[Headers.E].to_numpy()
1070
1077
  wsegaicd[Headers.F] = df_merge[Headers.F].to_numpy()
1078
+ if Headers.Z in df_merge.columns:
1079
+ wsegaicd[Headers.Z] = df_merge[Headers.Z].to_numpy()
1071
1080
  wsegaicd[Headers.EMPTY] = "/"
1072
1081
  return wsegaicd
1073
1082
 
@@ -1303,6 +1312,48 @@ def prepare_density_driven(well_name: str, df_well: pd.DataFrame, df_device: pd.
1303
1312
  return wsegdensity
1304
1313
 
1305
1314
 
1315
+ def prepare_injection_valve(well_name: str, df_well: pd.DataFrame, df_device: pd.DataFrame) -> pd.DataFrame:
1316
+ """Prepare data frame for INJECTION VALVE.
1317
+
1318
+ Args:
1319
+ well_name: Well name.
1320
+ df_well: Well data.
1321
+ df_device: Device data for this well and lateral.
1322
+
1323
+ Returns:
1324
+ DataFrame for INJECTION VALVE.
1325
+ """
1326
+ df_well = df_well[(df_well[Headers.DEVICE_TYPE] == Content.PERFORATED) | (df_well[Headers.NUMBER_OF_DEVICES] > 0)]
1327
+ if df_well.shape[0] == 0:
1328
+ return pd.DataFrame()
1329
+ df_merge = pd.merge_asof(
1330
+ left=df_device,
1331
+ right=df_well,
1332
+ left_on=[Headers.MEASURED_DEPTH],
1333
+ right_on=[Headers.TUBING_MEASURED_DEPTH],
1334
+ direction="nearest",
1335
+ )
1336
+ df_merge = df_merge[df_merge[Headers.DEVICE_TYPE] == Content.INJECTION_VALVE]
1337
+ wseginjv = pd.DataFrame()
1338
+ if df_merge.shape[0] > 0:
1339
+ wseginjv[Headers.WELL] = [well_name] * df_merge.shape[0]
1340
+ wseginjv[Headers.START_SEGMENT_NUMBER] = df_merge[Headers.START_SEGMENT_NUMBER].to_numpy()
1341
+ # the Cv is already corrected by the scaling factor
1342
+ wseginjv[Headers.FLOW_COEFFICIENT] = df_merge[Headers.FLOW_COEFFICIENT].to_numpy()
1343
+ wseginjv[Headers.PRIMARY_FLOW_CROSS_SECTIONAL_AREA] = df_merge[
1344
+ Headers.PRIMARY_FLOW_CROSS_SECTIONAL_AREA
1345
+ ].to_numpy()
1346
+ wseginjv[Headers.SECONDARY_FLOW_CROSS_SECTIONAL_AREA] = df_merge[
1347
+ Headers.SECONDARY_FLOW_CROSS_SECTIONAL_AREA
1348
+ ].to_numpy()
1349
+ wseginjv[Headers.TRIGGER_PARAMETER] = df_merge[Headers.TRIGGER_PARAMETER].to_numpy()
1350
+ wseginjv[Headers.TRIGGER_VALUE] = df_merge[Headers.TRIGGER_VALUE].to_numpy()
1351
+ wseginjv[Headers.DEFAULTS] = "5*"
1352
+ wseginjv[Headers.MAX_FLOW_CROSS_SECTIONAL_AREA] = wseginjv[Headers.PRIMARY_FLOW_CROSS_SECTIONAL_AREA].to_numpy()
1353
+ wseginjv[Headers.EMPTY] = "/"
1354
+ return wseginjv
1355
+
1356
+
1306
1357
  def prepare_dual_rate_controlled_production(
1307
1358
  well_name: str, df_well: pd.DataFrame, df_device: pd.DataFrame
1308
1359
  ) -> pd.DataFrame:
@@ -1513,6 +1564,168 @@ def print_wsegdensity(df_wsegdensity: pd.DataFrame, well_number: int) -> str:
1513
1564
  return action
1514
1565
 
1515
1566
 
1567
+ def print_wseginjv(df_wseginjv: pd.DataFrame, well_number: int) -> str:
1568
+ """Print INJECTION VALVE devices.
1569
+
1570
+ Args:
1571
+ df_wseginjv: Output from function prepare_wseginjv.
1572
+ well_number: Well number.
1573
+
1574
+ Returns:
1575
+ Formatted actions to be included in the output file.
1576
+
1577
+ Raises:
1578
+ CompletorError: If there are to many wells and/or segments with INJECTION VALVE.
1579
+ """
1580
+ header = [
1581
+ [
1582
+ Headers.WELL,
1583
+ Headers.START_SEGMENT_NUMBER,
1584
+ Headers.FLOW_COEFFICIENT,
1585
+ Headers.SECONDARY_FLOW_CROSS_SECTIONAL_AREA,
1586
+ Headers.DEFAULTS,
1587
+ Headers.MAX_FLOW_CROSS_SECTIONAL_AREA,
1588
+ ],
1589
+ [
1590
+ Headers.WELL,
1591
+ Headers.START_SEGMENT_NUMBER,
1592
+ Headers.FLOW_COEFFICIENT,
1593
+ Headers.PRIMARY_FLOW_CROSS_SECTIONAL_AREA,
1594
+ Headers.DEFAULTS,
1595
+ Headers.MAX_FLOW_CROSS_SECTIONAL_AREA,
1596
+ ],
1597
+ ]
1598
+
1599
+ sign = ["<", ">="]
1600
+ suvtrig = ["0", "1"]
1601
+ action = "UDQ\n"
1602
+ for idx in range(df_wseginjv.shape[0]):
1603
+ segment_number = df_wseginjv[Headers.START_SEGMENT_NUMBER].iloc[idx]
1604
+ well_name = df_wseginjv[Headers.WELL].iloc[idx]
1605
+ action += f" ASSIGN SUVTRIG {well_name} {segment_number} 0 /\n"
1606
+ action += "/\n\n"
1607
+ iaction = 1
1608
+ action += Keywords.WELL_SEGMENTS_VALVE + "\n"
1609
+ header_string = "--"
1610
+ for itm in header[iaction]:
1611
+ header_string += " " + itm
1612
+ action += header_string.rstrip() + "\n"
1613
+ for idx in range(df_wseginjv.shape[0]):
1614
+ segment_number = df_wseginjv[Headers.START_SEGMENT_NUMBER].iloc[idx]
1615
+ print_df = df_wseginjv[df_wseginjv[Headers.START_SEGMENT_NUMBER] == segment_number]
1616
+ print_df = print_df[header[iaction]]
1617
+ print_df = dataframe_tostring(print_df, True, False, False) + "\n"
1618
+ action += print_df
1619
+ action += "/\n\n"
1620
+ for idx in range(df_wseginjv.shape[0]):
1621
+ segment_number = df_wseginjv[Headers.START_SEGMENT_NUMBER].iloc[idx]
1622
+ well_name = df_wseginjv[Headers.WELL].iloc[idx]
1623
+ # Trigger paramater is segment water rate
1624
+ if df_wseginjv[Headers.TRIGGER_PARAMETER].iloc[idx] == "SWFR":
1625
+ water_segment_rate_cutoff = -1 * df_wseginjv[Headers.TRIGGER_VALUE].iloc[idx]
1626
+ iaction = 0
1627
+ act_number = iaction + 1
1628
+ act_name = f"INJVOP{well_number:03d}{segment_number:03d}{act_number:1d}"
1629
+ if len(act_name) > 13:
1630
+ raise CompletorError("Too many wells and/or too many segments with Injection Valve")
1631
+ action += (
1632
+ f"ACTIONX\n{act_name} 1000000 /\n"
1633
+ f"SWFR '{well_name}' {segment_number} "
1634
+ f"{sign[iaction]} {water_segment_rate_cutoff} AND /\n"
1635
+ f"SUVTRIG '{well_name}' {segment_number} "
1636
+ f"= {suvtrig[iaction]} /\n/\n\n"
1637
+ )
1638
+ print_df = df_wseginjv[df_wseginjv[Headers.START_SEGMENT_NUMBER] == segment_number]
1639
+ print_df = print_df[header[iaction]] # type: ignore
1640
+ header_string = Keywords.WELL_SEGMENTS_VALVE + "\n--"
1641
+
1642
+ for item in header[iaction]:
1643
+ header_string += " " + item
1644
+ header_string = header_string.rstrip() + "\n"
1645
+ print_df = header_string + dataframe_tostring(print_df, True, False, False) # type: ignore
1646
+ print_df += "\n/\n"
1647
+ print_df += f"\nUDQ\n ASSIGN SUVTRIG {well_name} {segment_number} 1 /\n/\n"
1648
+ action += print_df + "\nENDACTIO\n\n"
1649
+
1650
+ iaction = 1
1651
+ act_number = iaction + 1
1652
+ act_name = f"INJVCL{well_number:03d}{segment_number:03d}{act_number:1d}"
1653
+ if len(act_name) > 13:
1654
+ raise CompletorError("Too many wells and/or too many segments with Injection Valve")
1655
+ action += (
1656
+ f"ACTIONX\n{act_name} 1000000 /\n"
1657
+ f"SWFR '{well_name}' {segment_number} "
1658
+ f"{sign[iaction]} {water_segment_rate_cutoff} AND /\n"
1659
+ f"SUVTRIG '{well_name}' {segment_number} "
1660
+ f"= {suvtrig[iaction]} /\n/\n\n"
1661
+ )
1662
+ print_df = df_wseginjv[df_wseginjv[Headers.START_SEGMENT_NUMBER] == segment_number]
1663
+ print_df = print_df[header[iaction]] # type: ignore
1664
+ header_string = Keywords.WELL_SEGMENTS_VALVE + "\n--"
1665
+
1666
+ for item in header[iaction]:
1667
+ header_string += " " + item
1668
+ header_string = header_string.rstrip() + "\n"
1669
+ print_df = header_string + dataframe_tostring(print_df, True, False, False) # type: ignore
1670
+ print_df += "\n/\n"
1671
+ print_df += f"\nUDQ\n ASSIGN SUVTRIG {well_name} {segment_number} 0 /\n/\n"
1672
+ action += print_df + "\nENDACTIO\n\n"
1673
+
1674
+ # Trigger parameter is segment pressure drop
1675
+ elif df_wseginjv[Headers.TRIGGER_PARAMETER].iloc[idx] == "SPRD":
1676
+ pressure_drop_cutoff = -1 * df_wseginjv[Headers.TRIGGER_VALUE].iloc[idx]
1677
+ iaction = 0
1678
+ act_number = iaction + 1
1679
+ act_name = f"INJVOP{well_number:03d}{segment_number:03d}{act_number:1d}"
1680
+ if len(act_name) > 13:
1681
+ raise CompletorError("Too many wells and/or too many segments with Injection Valve")
1682
+ action += (
1683
+ f"ACTIONX\n{act_name} 1000000 /\n"
1684
+ f"SPRD '{well_name}' {segment_number} "
1685
+ f"{sign[iaction]} {pressure_drop_cutoff} AND /\n"
1686
+ f"SUVTRIG '{well_name}' {segment_number} "
1687
+ f"= {suvtrig[iaction]} /\n/\n\n"
1688
+ )
1689
+ print_df = df_wseginjv[df_wseginjv[Headers.START_SEGMENT_NUMBER] == segment_number]
1690
+ print_df = print_df[header[iaction]] # type: ignore
1691
+ header_string = Keywords.WELL_SEGMENTS_VALVE + "\n--"
1692
+
1693
+ for item in header[iaction]:
1694
+ header_string += " " + item
1695
+ header_string = header_string.rstrip() + "\n"
1696
+ print_df = header_string + dataframe_tostring(print_df, True, False, False) # type: ignore
1697
+ print_df += "\n/\n"
1698
+ print_df += f"\nUDQ\n ASSIGN SUVTRIG {well_name} {segment_number} 1 /\n/\n"
1699
+ action += print_df + "\nENDACTIO\n\n"
1700
+
1701
+ iaction = 1
1702
+ act_number = iaction + 1
1703
+ act_name = f"INJVCL{well_number:03d}{segment_number:03d}{act_number:1d}"
1704
+ if len(act_name) > 13:
1705
+ raise CompletorError("Too many wells and/or too many segments with Injection Valve")
1706
+ action += (
1707
+ f"ACTIONX\n{act_name} 1000000 /\n"
1708
+ f"SPRD '{well_name}' {segment_number} "
1709
+ f"{sign[iaction]} {pressure_drop_cutoff} AND /\n"
1710
+ f"SUVTRIG '{well_name}' {segment_number} "
1711
+ f"= {suvtrig[iaction]} /\n/\n\n"
1712
+ )
1713
+ print_df = df_wseginjv[df_wseginjv[Headers.START_SEGMENT_NUMBER] == segment_number]
1714
+ print_df = print_df[header[iaction]] # type: ignore
1715
+ header_string = Keywords.WELL_SEGMENTS_VALVE + "\n--"
1716
+
1717
+ for item in header[iaction]:
1718
+ header_string += " " + item
1719
+ header_string = header_string.rstrip() + "\n"
1720
+ print_df = header_string + dataframe_tostring(print_df, True, False, False) # type: ignore
1721
+ print_df += "\n/\n"
1722
+ print_df += f"\nUDQ\n ASSIGN SUVTRIG {well_name} {segment_number} 0 /\n/\n"
1723
+ action += print_df + "\nENDACTIO\n\n"
1724
+ else:
1725
+ raise CompletorError("Trigger paramater given is not supported")
1726
+ return action
1727
+
1728
+
1516
1729
  def print_wsegdualrcp(df_wsegdualrcp: pd.DataFrame, well_number: int) -> str:
1517
1730
  """Print for DUALRCP devices.
1518
1731
 
@@ -1616,3 +1829,143 @@ def print_wsegdualrcp(df_wsegdualrcp: pd.DataFrame, well_number: int) -> str:
1616
1829
  print_df = Keywords.AUTONOMOUS_INFLOW_CONTROL_DEVICE + "\n" + dataframe_tostring(print_df, True)
1617
1830
  action += f"{print_df}\n/\nENDACTIO\n\n"
1618
1831
  return action
1832
+
1833
+
1834
+ def print_wsegdensity_pyaction(df_wsegdensity: pd.DataFrame) -> str:
1835
+ """Create PYACTION code.
1836
+
1837
+ Args:
1838
+ df_wsegdensity: Output from function prepare_wsegdensity.
1839
+
1840
+ Returns:
1841
+ Final code output formatted in PYACTION.
1842
+ """
1843
+ data_dict = df_wsegdensity.to_dict(orient="list")
1844
+ final_code = f"""
1845
+ import opm_embedded
1846
+
1847
+ ecl_state = opm_embedded.current_ecl_state
1848
+ schedule = opm_embedded.current_schedule
1849
+ report_step = opm_embedded.current_report_step
1850
+ summary_state = opm_embedded.current_summary_state
1851
+
1852
+ if 'setup_done' not in locals():
1853
+ execution_counter = dict()
1854
+ executed = False
1855
+ setup_done = True
1856
+
1857
+ data={data_dict}
1858
+
1859
+ for i in range(len(data["WELL"])):
1860
+ well_name = data["WELL"][i]
1861
+ segment_number = data["START_SEGMENT_NUMBER"][i]
1862
+ flow_coefficient = data["FLOW_COEFFICIENT"][i]
1863
+ oil_flow_area = data["OIL_FLOW_CROSS_SECTIONAL_AREA"][i]
1864
+ gas_flow_area = data["GAS_FLOW_CROSS_SECTIONAL_AREA"][i]
1865
+ water_flow_area = data["WATER_FLOW_CROSS_SECTIONAL_AREA"][i]
1866
+ max_flow_area = data["MAX_FLOW_CROSS_SECTIONAL_AREA"][i]
1867
+ water_low = data["WATER_HOLDUP_FRACTION_LOW_CUTOFF"][i]
1868
+ water_high = data["WATER_HOLDUP_FRACTION_HIGH_CUTOFF"][i]
1869
+ gas_low = data["GAS_HOLDUP_FRACTION_LOW_CUTOFF"][i]
1870
+ gas_high = data["GAS_HOLDUP_FRACTION_HIGH_CUTOFF"][i]
1871
+ defaults = data["DEFAULTS"][i]
1872
+
1873
+ swhf = summary_state[f"SWHF:{{well_name}}:{{segment_number}}"]
1874
+ sghf = summary_state[f"SGHF:{{well_name}}:{{segment_number}}"]
1875
+ suvtrig = summary_state[f"SUVTRIG:{{well_name}}:{{segment_number}}"]
1876
+
1877
+ keyword_oil = (
1878
+ f"WSEGVALV\\n"
1879
+ f" '{{well_name}}' {{segment_number}} {{flow_coefficient}} {{oil_flow_area}} {{defaults}} {{max_flow_area}} /\\n/"
1880
+ )
1881
+ keyword_water = (
1882
+ f"WSEGVALV\\n"
1883
+ f" '{{well_name}}' {{segment_number}} {{flow_coefficient}} {{water_flow_area}} {{defaults}} {{max_flow_area}} /\\n/"
1884
+ )
1885
+ keyword_gas = (
1886
+ f"WSEGVALV\\n"
1887
+ f" '{{well_name}}' {{segment_number}} {{flow_coefficient}} {{gas_flow_area}} {{defaults}} {{max_flow_area}} /\\n/"
1888
+ )
1889
+
1890
+ key = (well_name, segment_number)
1891
+ execution_counter.setdefault(key, 0)
1892
+
1893
+ if execution_counter[key] == 0:
1894
+ schedule.insert_keywords(keyword_oil, report_step)
1895
+ summary_state[f"SUVTRIG:{{well_name}}:{{segment_number}}"] = 0
1896
+
1897
+ if execution_counter[key] < 1000000:
1898
+ if swhf is not None and sghf is not None:
1899
+ if swhf <= water_high and sghf > gas_high and suvtrig == 0:
1900
+ schedule.insert_keywords(keyword_gas, report_step)
1901
+ summary_state[f"SUVTRIG:{{well_name}}:{{segment_number}}"] = 1
1902
+ execution_counter[key] += 1
1903
+
1904
+ elif swhf > water_high and sghf <= gas_high and suvtrig == 0:
1905
+ schedule.insert_keywords(keyword_water, report_step)
1906
+ summary_state[f"SUVTRIG:{{well_name}}:{{segment_number}}"] = 2
1907
+ execution_counter[key] += 1
1908
+
1909
+ elif sghf < gas_low and suvtrig == 1:
1910
+ schedule.insert_keywords(keyword_oil, report_step)
1911
+ summary_state[f"SUVTRIG:{{well_name}}:{{segment_number}}"] = 0
1912
+ execution_counter[key] += 1
1913
+
1914
+ elif swhf < water_low and suvtrig == 2:
1915
+ schedule.insert_keywords(keyword_oil, report_step)
1916
+ summary_state[f"SUVTRIG:{{well_name}}:{{segment_number}}"] = 0
1917
+ execution_counter[key] += 1
1918
+ """
1919
+ return final_code
1920
+
1921
+
1922
+ def print_python_file(code: str, dir: str, well_name: str, lateral_number: int) -> str:
1923
+ """Print Python PYACTION file.
1924
+
1925
+ Args:
1926
+ code: Final code output formatted in PYACTION.
1927
+ dir: Output path.
1928
+ well_name: Well name.
1929
+ lateral_number: Lateral number.
1930
+
1931
+ Returns:
1932
+ Python file with PYACTION format, output directory with FMU format.
1933
+ """
1934
+ base_dir = Path.cwd() if Path(dir).parent == Path(".") else Path(dir).parent
1935
+ fmu_path = Path("eclipse/include/")
1936
+ if str(fmu_path) in str(base_dir):
1937
+ base_include_path = Path("../include/schedule")
1938
+ else:
1939
+ base_include_path = Path("")
1940
+ output_directory = f"{base_include_path}/wsegdensity_{well_name}_{lateral_number}.py"
1941
+ python_file = base_dir / f"wsegdensity_{well_name}_{lateral_number}.py"
1942
+ with open(python_file, "w") as file:
1943
+ file.writelines(code)
1944
+ return output_directory
1945
+
1946
+
1947
+ def print_wsegdensity_include(output_directory: str, well_name: str, lateral_number: int) -> str:
1948
+ """Formatted PYACTION include in the output file.
1949
+
1950
+ Args:
1951
+ output_directory: Include file path in FMU relative format.
1952
+ well_name: Well name.
1953
+ lateral_number: Lateral number.
1954
+
1955
+ Returns:
1956
+ Include file output for the output file.
1957
+ """
1958
+ action = f"""
1959
+ -------------------------------------
1960
+ -- START OF PYACTION SECTION
1961
+
1962
+ PYACTION
1963
+ WSEGDENSITY_{well_name}_{lateral_number} UNLIMITED /
1964
+
1965
+ '{output_directory}' /
1966
+
1967
+ -- END OF PYACTION SECTION
1968
+ -------------------------------------
1969
+ """
1970
+
1971
+ return action
@@ -48,7 +48,7 @@ class ReadCasefile:
48
48
  This class reads the case/input file of the Completor program.
49
49
  It reads the following keywords:
50
50
  COMPLETION, SEGMENTLENGTH, JOINTLENGTH, AUTONOMOUS_INFLOW_CONTROL_DEVICE, WELL_SEGMENTS_VALVE,
51
- INFLOW_CONTROL_DEVICE, DENSITY_DRIVEN, DUAL_RATE_CONTROLLED_PRODUCTION, INFLOW_CONTROL_VALVE.
51
+ INFLOW_CONTROL_DEVICE, DENSITY_DRIVEN, INJECTION_VALVE, DUAL_RATE_CONTROLLED_PRODUCTION, INFLOW_CONTROL_VALVE.
52
52
  In the absence of some keywords, the program uses the default values.
53
53
 
54
54
  Attributes:
@@ -64,6 +64,7 @@ class ReadCasefile:
64
64
  wsegvalv_table (pd.DataFrame): WELL_SEGMENTS_VALVE.
65
65
  wsegicv_table (pd.DataFrame): INFLOW_CONTROL_VALVE.
66
66
  wsegdensity_table (pd.DataFrame): DENSITY_DRIVEN.
67
+ wseginjv_table (pd.DataFrame): INJECTION_VALVE.
67
68
  wsegdualrcp_table (pd.DataFrame): DUAL_RATE_CONTROLLED_PRODUCTION.
68
69
  strict (bool): USE_STRICT. If TRUE it will exit if any lateral is not defined in the case-file. Default to TRUE.
69
70
  lat2device (pd.DataFrame): LATERAL_TO_DEVICE.
@@ -90,6 +91,7 @@ class ReadCasefile:
90
91
  self.minimum_segment_length: float = 0.0
91
92
  self.strict = True
92
93
  self.gp_perf_devicelayer = False
94
+ self.python_dependent = False
93
95
  self.schedule_file = schedule_file
94
96
  self.output_file = output_file
95
97
  self.completion_table = pd.DataFrame()
@@ -99,6 +101,7 @@ class ReadCasefile:
99
101
  self.wsegsicd_table = pd.DataFrame()
100
102
  self.wsegvalv_table = pd.DataFrame()
101
103
  self.wsegdensity_table = pd.DataFrame()
104
+ self.wseginjv_table = pd.DataFrame()
102
105
  self.wsegdualrcp_table = pd.DataFrame()
103
106
  self.wsegicv_table = pd.DataFrame()
104
107
  self.lat2device = pd.DataFrame()
@@ -117,6 +120,8 @@ class ReadCasefile:
117
120
  self.read_wsegvalv()
118
121
  self.read_wsegsicd()
119
122
  self.read_wsegdensity()
123
+ self.read_python_dependent()
124
+ self.read_wseginjv()
120
125
  self.read_wsegdualrcp()
121
126
  self.read_wsegicv()
122
127
  self.read_lat2device()
@@ -449,10 +454,14 @@ class ReadCasefile:
449
454
  Headers.F,
450
455
  Headers.AICD_CALIBRATION_FLUID_DENSITY,
451
456
  Headers.AICD_FLUID_VISCOSITY,
457
+ Headers.Z,
452
458
  ]
453
- self.wsegaicd_table = input_validation.set_format_wsegaicd(
454
- self._create_dataframe_with_columns(header, start_index, end_index)
455
- )
459
+ try:
460
+ df_temp = self._create_dataframe_with_columns(header, start_index, end_index)
461
+ except CaseReaderFormatError:
462
+ header.remove(Headers.Z)
463
+ df_temp = self._create_dataframe_with_columns(header, start_index, end_index)
464
+ self.wsegaicd_table = input_validation.set_format_wsegaicd(df_temp)
456
465
  device_checks = self.completion_table[
457
466
  self.completion_table[Headers.DEVICE_TYPE] == Content.AUTONOMOUS_INFLOW_CONTROL_DEVICE
458
467
  ][Headers.DEVICE_NUMBER].to_numpy()
@@ -514,6 +523,52 @@ class ReadCasefile:
514
523
  if not check_contents(device_checks, self.wsegdensity_table[Headers.DEVICE_NUMBER].to_numpy()):
515
524
  raise CompletorError(f"Not all device in COMPLETION is specified in {key}")
516
525
 
526
+ def read_wseginjv(self) -> None:
527
+ """Read the INJECTION_VALVE keyword in the case file.
528
+
529
+ Raises:
530
+ CompletorError: If INJECTION_VALVE is not defined and INJV is used in COMPLETION,
531
+ or if the device number is not found.
532
+ If not all devices in COMPLETION are specified in INJECTION_VALVE.
533
+ """
534
+ start_index, end_index = parse.locate_keyword(self.content, Keywords.INJECTION_VALVE)
535
+ if start_index == end_index:
536
+ if Content.INJECTION_VALVE in self.completion_table[Headers.DEVICE_TYPE]:
537
+ raise CompletorError(
538
+ f"{Keywords.INJECTION_VALVE} keyword must be defined, if INJV is used in the completion."
539
+ )
540
+ else:
541
+ # Table headers
542
+ header = [
543
+ Headers.DEVICE_NUMBER,
544
+ Headers.TRIGGER_PARAMETER,
545
+ Headers.TRIGGER_VALUE,
546
+ Headers.FLOW_COEFFICIENT,
547
+ Headers.PRIMARY_FLOW_CROSS_SECTIONAL_AREA,
548
+ Headers.SECONDARY_FLOW_CROSS_SECTIONAL_AREA,
549
+ ]
550
+ self.wseginjv_table = input_validation.set_format_wseginjv(
551
+ self._create_dataframe_with_columns(header, start_index, end_index)
552
+ )
553
+ # Check if the device in COMPLETION is exist in INJECTION_VALVE
554
+ device_checks = self.completion_table[
555
+ self.completion_table[Headers.DEVICE_TYPE] == Content.INJECTION_VALVE
556
+ ][Headers.DEVICE_NUMBER].to_numpy()
557
+ if not check_contents(device_checks, self.wseginjv_table[Headers.DEVICE_NUMBER].to_numpy()):
558
+ raise CompletorError(f"Not all device in COMPLETION is specified in {Keywords.INJECTION_VALVE}")
559
+
560
+ def read_python_dependent(self) -> None:
561
+ """Read PYTHON keyword. Accepts TRUE or just '/' as True."""
562
+ start_index, end_index = parse.locate_keyword(self.content, Keywords.PYTHON_DEPENDENT)
563
+
564
+ if end_index == start_index + 1:
565
+ # Keyword followed directly by '/', no value = True
566
+ self.python_dependent = True
567
+ elif end_index == start_index + 2:
568
+ val = self.content[start_index + 1]
569
+ if val.upper() == "TRUE":
570
+ self.python_dependent = True
571
+
517
572
  def read_wsegdualrcp(self) -> None:
518
573
  """Read the DUALRCP keyword in the case file.
519
574
 
@@ -193,6 +193,7 @@ def get_active_wells(completion_table: pd.DataFrame, gp_perf_devicelayer: bool)
193
193
  Content.AUTONOMOUS_INFLOW_CONTROL_DEVICE,
194
194
  Content.DUAL_RATE_CONTROLLED_PRODUCTION,
195
195
  Content.DENSITY,
196
+ Content.INJECTION_VALVE,
196
197
  Content.INFLOW_CONTROL_DEVICE,
197
198
  Content.VALVE,
198
199
  Content.INFLOW_CONTROL_VALVE,
@@ -48,6 +48,8 @@ def visualize_device(axs: Axes, df_well: pd.DataFrame) -> Axes:
48
48
  axs.plot(xpar, ypar, "rv-", markevery=[1])
49
49
  elif df_device[Headers.DEVICE_TYPE].iloc[idx] == Content.DENSITY:
50
50
  axs.plot(xpar, ypar, "rP-", markevery=[1])
51
+ elif df_device[Headers.DEVICE_TYPE].iloc[idx] == Content.INJECTION_VALVE:
52
+ axs.plot(xpar, ypar, "rP-", markevery=[1])
51
53
  elif df_device[Headers.DEVICE_TYPE].iloc[idx] == Content.DUAL_RATE_CONTROLLED_PRODUCTION:
52
54
  axs.plot(xpar, ypar, "r*-", markevery=[1])
53
55
  return axs
@@ -220,6 +220,8 @@ class Lateral:
220
220
  df_well = completion.get_device(df_well, case.wsegaicd_table, Content.AUTONOMOUS_INFLOW_CONTROL_DEVICE)
221
221
  if Content.DENSITY in active_devices:
222
222
  df_well = completion.get_device(df_well, case.wsegdensity_table, Content.DENSITY)
223
+ if Content.INJECTION_VALVE in active_devices:
224
+ df_well = completion.get_device(df_well, case.wseginjv_table, Content.INJECTION_VALVE)
223
225
  if Content.DUAL_RATE_CONTROLLED_PRODUCTION in active_devices:
224
226
  df_well = completion.get_device(df_well, case.wsegdualrcp_table, Content.DUAL_RATE_CONTROLLED_PRODUCTION)
225
227
  if Content.INFLOW_CONTROL_VALVE in active_devices:
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "completor"
3
- version = "1.1.3"
3
+ version = "1.3.0"
4
4
  description = "Advanced multi-segmented well completion tool."
5
5
  authors = ["Equinor ASA <opensource@equinor.com>"]
6
6
  repository = "https://github.com/equinor/completor"
@@ -13,21 +13,22 @@ classifiers = [
13
13
  "Operating System :: OS Independent",
14
14
  "Natural Language :: English",
15
15
  "Programming Language :: Python :: 3.11",
16
+ "Programming Language :: Python :: 3.12",
17
+ "Programming Language :: Python :: 3.13",
16
18
  ]
17
19
 
18
20
  [tool.poetry.urls]
19
21
  "Bug Tracker" = "https://github.com/equinor/completor/issues"
20
22
 
21
23
  [tool.poetry.dependencies]
22
- python = "^3.11"
24
+ python = ">=3.11, <3.14"
23
25
  matplotlib = "^3.9"
24
- numpy = ">=1.26, <3.0"
26
+ numpy = "^2.0"
25
27
  pandas = "^2.2"
26
- scipy = "^1.14"
27
28
  tqdm = "^4.66"
28
29
 
29
30
  rstcheck-core = {version="^1.2", optional=true}
30
- ert = {version="^12", optional=true}
31
+ ert = {version="^14", optional=true}
31
32
  pytest = {version="^8.3", optional=true}
32
33
  pytest-xdist = {version="^3.6", optional=true}
33
34
  pytest-env = {version="^1", optional=true}
File without changes
File without changes
File without changes