resqpy 4.14.2__py3-none-any.whl → 5.1.5__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.
Files changed (67) hide show
  1. resqpy/__init__.py +1 -1
  2. resqpy/fault/_gcs_functions.py +10 -10
  3. resqpy/fault/_grid_connection_set.py +277 -113
  4. resqpy/grid/__init__.py +2 -3
  5. resqpy/grid/_defined_geometry.py +3 -3
  6. resqpy/grid/_extract_functions.py +2 -1
  7. resqpy/grid/_grid.py +95 -12
  8. resqpy/grid/_grid_types.py +22 -7
  9. resqpy/grid/_points_functions.py +1 -1
  10. resqpy/grid/_regular_grid.py +6 -2
  11. resqpy/grid_surface/__init__.py +17 -38
  12. resqpy/grid_surface/_blocked_well_populate.py +5 -5
  13. resqpy/grid_surface/_find_faces.py +1349 -253
  14. resqpy/lines/_polyline.py +24 -33
  15. resqpy/model/_catalogue.py +9 -0
  16. resqpy/model/_forestry.py +18 -14
  17. resqpy/model/_hdf5.py +11 -3
  18. resqpy/model/_model.py +85 -10
  19. resqpy/model/_xml.py +38 -13
  20. resqpy/multi_processing/wrappers/grid_surface_mp.py +92 -37
  21. resqpy/olio/read_nexus_fault.py +8 -2
  22. resqpy/olio/relperm.py +1 -1
  23. resqpy/olio/transmission.py +8 -8
  24. resqpy/olio/triangulation.py +36 -30
  25. resqpy/olio/vector_utilities.py +340 -6
  26. resqpy/olio/volume.py +0 -20
  27. resqpy/olio/wellspec_keywords.py +19 -13
  28. resqpy/olio/write_hdf5.py +1 -1
  29. resqpy/olio/xml_et.py +12 -0
  30. resqpy/property/__init__.py +6 -4
  31. resqpy/property/_collection_add_part.py +4 -3
  32. resqpy/property/_collection_create_xml.py +4 -2
  33. resqpy/property/_collection_get_attributes.py +4 -0
  34. resqpy/property/attribute_property_set.py +311 -0
  35. resqpy/property/grid_property_collection.py +11 -11
  36. resqpy/property/property_collection.py +79 -31
  37. resqpy/property/property_common.py +3 -8
  38. resqpy/rq_import/_add_surfaces.py +34 -14
  39. resqpy/rq_import/_grid_from_cp.py +2 -2
  40. resqpy/rq_import/_import_nexus.py +75 -48
  41. resqpy/rq_import/_import_vdb_all_grids.py +64 -52
  42. resqpy/rq_import/_import_vdb_ensemble.py +12 -13
  43. resqpy/surface/_mesh.py +4 -0
  44. resqpy/surface/_surface.py +593 -118
  45. resqpy/surface/_tri_mesh.py +13 -10
  46. resqpy/surface/_tri_mesh_stencil.py +4 -4
  47. resqpy/surface/_triangulated_patch.py +71 -51
  48. resqpy/time_series/_any_time_series.py +7 -4
  49. resqpy/time_series/_geologic_time_series.py +1 -1
  50. resqpy/unstructured/_hexa_grid.py +6 -2
  51. resqpy/unstructured/_prism_grid.py +13 -5
  52. resqpy/unstructured/_pyramid_grid.py +6 -2
  53. resqpy/unstructured/_tetra_grid.py +6 -2
  54. resqpy/unstructured/_unstructured_grid.py +6 -2
  55. resqpy/well/_blocked_well.py +1986 -1946
  56. resqpy/well/_deviation_survey.py +3 -3
  57. resqpy/well/_md_datum.py +11 -21
  58. resqpy/well/_trajectory.py +10 -5
  59. resqpy/well/_wellbore_frame.py +10 -2
  60. resqpy/well/blocked_well_frame.py +3 -3
  61. resqpy/well/well_object_funcs.py +7 -9
  62. resqpy/well/well_utils.py +33 -0
  63. {resqpy-4.14.2.dist-info → resqpy-5.1.5.dist-info}/METADATA +8 -9
  64. {resqpy-4.14.2.dist-info → resqpy-5.1.5.dist-info}/RECORD +66 -66
  65. {resqpy-4.14.2.dist-info → resqpy-5.1.5.dist-info}/WHEEL +1 -1
  66. resqpy/grid/_moved_functions.py +0 -15
  67. {resqpy-4.14.2.dist-info → resqpy-5.1.5.dist-info}/LICENSE +0 -0
@@ -10,6 +10,7 @@ log = logging.getLogger(__name__)
10
10
  import math as maths
11
11
  import numpy as np
12
12
  import pandas as pd
13
+ import warnings
13
14
  from functools import partial
14
15
 
15
16
  import resqpy.crs as crs
@@ -136,12 +137,13 @@ class BlockedWell(BaseResqpy):
136
137
  self.wellbore_interpretation = None #: associated wellbore interpretation object
137
138
  self.wellbore_feature = None #: associated wellbore feature object
138
139
  self.well_name = None #: name of well to import from ascii file formats
140
+ self.cell_index_dtype = np.int32 #: set to int64 if any grid has more than 2^31 - 1 cells, otherwise int32
139
141
 
140
142
  self.cell_interval_map = None # maps from cell index to interval (ie. node) index; populated on demand
141
143
 
142
144
  self.property_collection = None
143
145
  #: all logs associated with the blockedwellbore; an instance of :class:`resqpy.property.WellIntervalPropertyCollection`
144
- self.logs = None # due for deprecation
146
+ self._logs = None # use of .logs is deprecated
145
147
  self.cellind_null = -1
146
148
  self.gridind_null = -1
147
149
  self.facepair_null = -1
@@ -150,11 +152,11 @@ class BlockedWell(BaseResqpy):
150
152
  # this is the default as indicated on page 139 (but not p. 180) of the RESQML Usage Gude v2.0.1
151
153
  # also assumes K is generally increasing downwards
152
154
  # see DevOps backlog item 269001 discussion for more information
153
- # self.face_index_map = np.array([[0, 1], [4, 2], [5, 3]], dtype = int)
154
- self.face_index_map = np.array([[0, 1], [2, 4], [5, 3]], dtype = int) # order: top, base, J-, I+, J+, I-
155
+ # self.face_index_map = np.array([[0, 1], [4, 2], [5, 3]], dtype = np.int8)
156
+ self.face_index_map = np.array([[0, 1], [2, 4], [5, 3]], dtype = np.int8) # order: top, base, J-, I+, J+, I-
155
157
  # and the inverse, maps from 0..5 to (axis, p01)
156
- # self.face_index_inverse_map = np.array([[0, 0], [0, 1], [1, 1], [2, 1], [1, 0], [2, 0]], dtype = int)
157
- self.face_index_inverse_map = np.array([[0, 0], [0, 1], [1, 0], [2, 1], [1, 1], [2, 0]], dtype = int)
158
+ # self.face_index_inverse_map = np.array([[0, 0], [0, 1], [1, 1], [2, 1], [1, 0], [2, 0]], dtype = np.int8)
159
+ self.face_index_inverse_map = np.array([[0, 0], [0, 1], [1, 0], [2, 1], [1, 1], [2, 0]], dtype = np.int8)
158
160
  # note: the rework_face_pairs() method, below, overwrites the face indices based on I, J cell indices
159
161
 
160
162
  super().__init__(model = parent_model,
@@ -190,160 +192,17 @@ class BlockedWell(BaseResqpy):
190
192
  self.title = well_name
191
193
  # else an empty object is returned
192
194
 
193
- def __set_grid(self, grid, wellspec_file, cellio_file, column_ji0):
194
- """Set the grid to which the blocked well belongs."""
195
-
196
- if grid is None and (self.trajectory is not None or wellspec_file is not None or cellio_file is not None or
197
- column_ji0 is not None):
198
- grid_final = self.model.grid()
199
- else:
200
- grid_final = grid
201
- return grid_final
202
-
203
- def __check_cellio_init_okay(self, cellio_file, well_name, grid):
204
- """Checks if BlockedWell object initialization from a cellio file is okay."""
205
-
206
- okay = self.import_from_rms_cellio(cellio_file, well_name, grid)
207
- if not okay:
208
- self.node_count = 0
209
-
210
- def _load_from_xml(self):
211
- """Loads the blocked wellbore object from an xml node (and associated hdf5 data)."""
212
-
213
- node = self.root
214
- assert node is not None
215
-
216
- self.__find_trajectory_uuid(node = node)
217
-
218
- self.node_count = rqet.find_tag_int(node, 'NodeCount')
219
- assert self.node_count is not None and self.node_count >= 2, 'problem with blocked well node count'
220
-
221
- mds_node = rqet.find_tag(node, 'NodeMd')
222
- assert mds_node is not None, 'blocked well node measured depths hdf5 reference not found in xml'
223
- rqwu.load_hdf5_array(self, mds_node, 'node_mds')
224
-
225
- # Statement below has no effect, is this a bug?
226
- self.node_mds is not None and self.node_mds.ndim == 1 and self.node_mds.size == self.node_count
227
-
228
- self.cell_count = rqet.find_tag_int(node, 'CellCount')
229
- assert self.cell_count is not None and self.cell_count > 0
230
-
231
- # todo: remove this if block once RMS export issue resolved
232
- if self.cell_count == self.node_count:
233
- extended_mds = np.empty((self.node_mds.size + 1,))
234
- extended_mds[:-1] = self.node_mds
235
- extended_mds[-1] = self.node_mds[-1] + 1.0
236
- self.node_mds = extended_mds
237
- self.node_count += 1
238
-
239
- assert self.cell_count < self.node_count
240
-
241
- self.__find_ci_node_and_load_hdf5_array(node = node)
242
-
243
- self.__find_fi_node_and_load_hdf5_array(node)
244
-
245
- unique_grid_indices = self.__find_gi_node_and_load_hdf5_array(node = node)
246
-
247
- self.__find_grid_node(node = node, unique_grid_indices = unique_grid_indices)
248
-
249
- interp_uuid = rqet.find_nested_tags_text(node, ['RepresentedInterpretation', 'UUID'])
250
- if interp_uuid is None:
251
- self.wellbore_interpretation = None
252
- else:
253
- self.wellbore_interpretation = rqo.WellboreInterpretation(self.model, uuid = interp_uuid)
254
-
255
- # Create blocked well log collection of all log data
256
- self.logs = rqp.WellIntervalPropertyCollection(frame = self)
257
-
258
- # Set up matches between cell_indices and grid_indices
259
- self.cell_grid_link = self.map_cell_and_grid_indices()
260
-
261
- def __find_trajectory_uuid(self, node):
262
- """Find and verify the uuid of the trajectory associated with the BlockedWell object."""
263
-
264
- trajectory_uuid = bu.uuid_from_string(rqet.find_nested_tags_text(node, ['Trajectory', 'UUID']))
265
- assert trajectory_uuid is not None, 'blocked well trajectory reference not found in xml'
266
- if self.trajectory is None:
267
- self.trajectory = rqw.Trajectory(self.model, uuid = trajectory_uuid)
268
- else:
269
- assert bu.matching_uuids(self.trajectory.uuid, trajectory_uuid), 'blocked well trajectory uuid mismatch'
270
-
271
- def __find_ci_node_and_load_hdf5_array(self, node):
272
- """Find the BlockedWell object's cell indices hdf5 reference node and load the array."""
273
-
274
- ci_node = rqet.find_tag(node, 'CellIndices')
275
- assert ci_node is not None, 'blocked well cell indices hdf5 reference not found in xml'
276
- rqwu.load_hdf5_array(self, ci_node, 'cell_indices', dtype = int)
277
- assert (self.cell_indices is not None and self.cell_indices.ndim == 1 and
278
- self.cell_indices.size == self.cell_count), 'mismatch in number of cell indices for blocked well'
279
- self.cellind_null = rqet.find_tag_int(ci_node, 'NullValue')
280
- if self.cellind_null is None:
281
- self.cellind_null = -1 # if no Null found assume -1 default
282
-
283
- def __find_fi_node_and_load_hdf5_array(self, node):
284
- """Find the BlockedWell object's face indices hdf5 reference node and load the array."""
285
-
286
- fi_node = rqet.find_tag(node, 'LocalFacePairPerCellIndices')
287
- assert fi_node is not None, 'blocked well face indices hdf5 reference not found in xml'
288
- rqwu.load_hdf5_array(self, fi_node, 'raw_face_indices', dtype = 'int')
289
- assert self.raw_face_indices is not None, 'failed to load face indices for blocked well'
290
- assert self.raw_face_indices.size == 2 * self.cell_count, 'mismatch in number of cell faces for blocked well'
291
- if self.raw_face_indices.ndim > 1:
292
- self.raw_face_indices = self.raw_face_indices.reshape((self.raw_face_indices.size,))
293
- mask = np.where(self.raw_face_indices == -1)
294
- self.raw_face_indices[mask] = 0
295
- self.face_pair_indices = self.face_index_inverse_map[self.raw_face_indices]
296
- self.face_pair_indices[mask] = (-1, -1)
297
- self.face_pair_indices = self.face_pair_indices.reshape((-1, 2, 2))
298
- del self.raw_face_indices
299
- self.facepair_null = rqet.find_tag_int(fi_node, 'NullValue')
300
- if self.facepair_null is None:
301
- self.facepair_null = -1
302
-
303
- def __find_gi_node_and_load_hdf5_array(self, node):
304
- """Find the BlockedWell object's grid indices hdf5 reference node and load the array."""
305
-
306
- gi_node = rqet.find_tag(node, 'GridIndices')
307
- assert gi_node is not None, 'blocked well grid indices hdf5 reference not found in xml'
308
- rqwu.load_hdf5_array(self, gi_node, 'grid_indices', dtype = 'int')
309
- # assert self.grid_indices is not None and self.grid_indices.ndim == 1 and self.grid_indices.size == self.node_count - 1
310
- # temporary code to handle blocked wells with incorrectly shaped grid indices wrt. nodes
311
- assert self.grid_indices is not None and self.grid_indices.ndim == 1
312
- if self.grid_indices.size != self.node_count - 1:
313
- if self.grid_indices.size == self.cell_count and self.node_count == 2 * self.cell_count:
314
- log.warning(f'handling node duplication or missing unblocked intervals in blocked well: {self.title}')
315
-
316
- expanded_grid_indices = np.full(self.node_count - 1, -1, dtype = int)
317
- expanded_grid_indices[::2] = self.grid_indices
318
- self.grid_indices = expanded_grid_indices
319
- else:
320
- raise ValueError(
321
- f'incorrect grid indices size with respect to node count in blocked well: {self.title}')
322
- # end of temporary code
323
- unique_grid_indices = np.unique(self.grid_indices) # sorted list of unique values
324
- self.gridind_null = rqet.find_tag_int(gi_node, 'NullValue')
325
- if self.gridind_null is None:
326
- self.gridind_null = -1 # if no Null found assume -1 default
327
- return unique_grid_indices
328
-
329
- def __find_grid_node(self, node, unique_grid_indices):
330
- """Find the BlockedWell object's grid reference node(s)."""
331
- grid_node_list = rqet.list_of_tag(node, 'Grid')
332
- assert len(grid_node_list) > 0, 'blocked well grid reference(s) not found in xml'
333
- assert unique_grid_indices[0] >= -1 and unique_grid_indices[-1] < len(
334
- grid_node_list), 'blocked well grid index out of range'
335
- assert np.count_nonzero(
336
- self.grid_indices >= 0) == self.cell_count, 'mismatch in number of blocked well intervals'
337
- self.grid_list = []
338
- for grid_ref_node in grid_node_list:
339
- grid_node = self.model.referenced_node(grid_ref_node)
340
- assert grid_node is not None, 'grid referenced in blocked well xml is not present in model'
341
- grid_uuid = rqet.uuid_for_part_root(grid_node)
342
- grid_obj = self.model.grid(uuid = grid_uuid, find_properties = False)
343
- self.grid_list.append(grid_obj)
195
+ @property
196
+ def logs(self):
197
+ """Returns blocked well property collection of all well log properties."""
198
+ warnings.warn('DEPRECATED: use of BlockedWell.logs attribute is deprecated, use property collection instead')
199
+ if self._logs is None:
200
+ self._logs = rqp.WellIntervalPropertyCollection(frame = self)
201
+ return self._logs
344
202
 
345
203
  def extract_property_collection(self, refresh = False):
346
204
  """Returns a property collection for the blocked well."""
205
+
347
206
  if self.property_collection is None or refresh:
348
207
  self.property_collection = rqp.PropertyCollection(support = self)
349
208
  return self.property_collection
@@ -407,6 +266,7 @@ class BlockedWell(BaseResqpy):
407
266
 
408
267
  def interval_for_cell(self, cell_index):
409
268
  """Returns the interval index for a given cell index (identical if there are no unblocked intervals)."""
269
+
410
270
  assert 0 <= cell_index < self.cell_count
411
271
  if self.node_count == self.cell_count + 1:
412
272
  return cell_index
@@ -424,20 +284,10 @@ class BlockedWell(BaseResqpy):
424
284
  (float, float) being the entry and exit measured depths for the cell, along the trajectory;
425
285
  uom is held in trajectory object
426
286
  """
287
+
427
288
  interval = self.interval_for_cell(cell_index)
428
289
  return (self.node_mds[interval], self.node_mds[interval + 1])
429
290
 
430
- def _set_cell_interval_map(self):
431
- """Sets up an index mapping from blocked cell index to interval index, accounting for unblocked intervals."""
432
- self.cell_interval_map = np.zeros(self.cell_count, dtype = int)
433
- ci = 0
434
- for ii in range(self.node_count - 1):
435
- if self.grid_indices[ii] < 0:
436
- continue
437
- self.cell_interval_map[ci] = ii
438
- ci += 1
439
- assert ci == self.cell_count
440
-
441
291
  def cell_indices_kji0(self):
442
292
  """Returns a numpy int array of shape (N, 3) of cells visited by well, for a single grid situation.
443
293
 
@@ -456,7 +306,7 @@ class BlockedWell(BaseResqpy):
456
306
  grid_for_cell_list = []
457
307
  grid_indices = self.compressed_grid_indices()
458
308
  assert len(grid_indices) == self.cell_count
459
- cell_indices = np.empty((self.cell_count, 3), dtype = int)
309
+ cell_indices = np.empty((self.cell_count, 3), dtype = np.int32)
460
310
  for cell_number in range(self.cell_count):
461
311
  grid = self.grid_list[grid_indices[cell_number]]
462
312
  grid_for_cell_list.append(grid)
@@ -488,7 +338,7 @@ class BlockedWell(BaseResqpy):
488
338
 
489
339
  if cells_kji0 is None or len(cells_kji0) == 0:
490
340
  return None
491
- well_box = np.empty((2, 3), dtype = int)
341
+ well_box = np.empty((2, 3), dtype = np.int32)
492
342
  well_box[0] = np.min(cells_kji0, axis = 0)
493
343
  well_box[1] = np.max(cells_kji0, axis = 0)
494
344
  return well_box
@@ -716,16 +566,6 @@ class BlockedWell(BaseResqpy):
716
566
 
717
567
  return self
718
568
 
719
- def __derive_from_wellspec_check_well_name(self, well_name):
720
- """Set the well name to be used in the wellspec file."""
721
- if well_name:
722
- self.well_name = well_name
723
- else:
724
- well_name = self.well_name
725
- if not self.title:
726
- self.title = well_name
727
- return well_name
728
-
729
569
  def derive_from_cell_list(self, cell_kji0_list, well_name, grid, length_uom = None):
730
570
  """Populate empty blocked well from numpy int array of shape (N, 3) being list of cells."""
731
571
 
@@ -847,9 +687,9 @@ class BlockedWell(BaseResqpy):
847
687
  self.node_count = len(trajectory_mds)
848
688
  self.node_mds = np.array(trajectory_mds)
849
689
  self.cell_count = len(blocked_cells_kji0)
850
- self.grid_indices = np.array(blocked_intervals, dtype = int) # NB. only supporting one grid at the moment
851
- self.cell_indices = grid.natural_cell_indices(np.array(blocked_cells_kji0))
852
- self.face_pair_indices = np.array(blocked_face_pairs, dtype = int)
690
+ self.grid_indices = np.array(blocked_intervals, dtype = np.int32) # NB. only supporting one grid at the moment
691
+ self.cell_indices = grid.natural_cell_indices(np.array(blocked_cells_kji0)).astype(self.cell_index_dtype)
692
+ self.face_pair_indices = np.array(blocked_face_pairs, dtype = np.int8)
853
693
  self.grid_list = [grid]
854
694
 
855
695
  trajectory_points, trajectory_mds = BlockedWell.__add_tail_to_trajectory_if_necessary(
@@ -866,258 +706,38 @@ class BlockedWell(BaseResqpy):
866
706
 
867
707
  return self
868
708
 
869
- @staticmethod
870
- def __cell_kji0_from_df(df, df_row):
871
- row = df.iloc[df_row]
872
- if pd.isna(row[0]) or pd.isna(row[1]) or pd.isna(row[2]):
873
- return None
874
- cell_kji0 = np.empty((3,), dtype = int)
875
- cell_kji0[:] = row[2], row[1], row[0]
876
- cell_kji0[:] -= 1
877
- return cell_kji0
878
-
879
- @staticmethod
880
- def __verify_grid_name(grid_name_to_check, row, skipped_warning_grid, well_name):
881
- """Check whether the grid associated with a row of the dataframe matches the expected grid name."""
882
- skip_row = False
883
- if grid_name_to_check and pd.notna(row['GRID']) and grid_name_to_check != str(row['GRID']).upper():
884
- other_grid = str(row['GRID'])
885
- if skipped_warning_grid != other_grid:
886
- log.warning('skipping perforation(s) in grid ' + other_grid + ' for well ' + str(well_name))
887
- skipped_warning_grid = other_grid
888
- skip_row = True
889
- return skipped_warning_grid, skip_row
890
-
891
- @staticmethod
892
- def __calculate_entry_and_exit_axes_polarities_and_points(angles_present, row, cp, well_name, df, i, cell_kji0,
893
- blocked_cells_kji0, use_face_centres, xy_units, z_units):
894
- if angles_present:
895
- entry_axis, entry_polarity, entry_xyz, exit_axis, exit_polarity, exit_xyz = \
896
- BlockedWell.__calculate_entry_and_exit_axes_polarities_and_points_using_angles(
897
- row = row, cp = cp, well_name = well_name, xy_units = xy_units, z_units = z_units)
898
- else:
899
- # fabricate entry and exit axes and polarities based on indices alone
900
- # note: could use geometry but here a cheap rough-and-ready approach is used
901
- log.debug('row ' + str(i) + ': using cell moves')
902
- entry_axis, entry_polarity, exit_axis, exit_polarity = BlockedWell.__calculate_entry_and_exit_axes_polarities_and_points_using_indices(
903
- df = df, i = i, cell_kji0 = cell_kji0, blocked_cells_kji0 = blocked_cells_kji0)
904
-
905
- entry_xyz, exit_xyz = BlockedWell.__override_vector_based_xyz_entry_and_exit_points_if_necessary(
906
- use_face_centres = use_face_centres,
907
- entry_axis = entry_axis,
908
- exit_axis = exit_axis,
909
- entry_polarity = entry_polarity,
910
- exit_polarity = exit_polarity,
911
- cp = cp)
709
+ def import_from_rms_cellio(self,
710
+ cellio_file,
711
+ well_name,
712
+ grid,
713
+ include_overburden_unblocked_interval = False,
714
+ set_tangent_vectors = False):
715
+ """Populates empty blocked well from RMS cell I/O data; creates simulation trajectory and md datum.
912
716
 
913
- return entry_axis, entry_polarity, entry_xyz, exit_axis, exit_polarity, exit_xyz
717
+ arguments:
718
+ cellio_file (string): path of RMS ascii export file holding blocked well cell I/O data; cell entry and
719
+ exit points are expected
720
+ well_name (string): the name of the well as used in the cell I/O file
721
+ grid (grid.Grid object): the grid object which the cell indices in the cell I/O data relate to
722
+ set_tangent_vectors (boolean, default False): if True, tangent vectors will be computed from the well
723
+ trajectory's control points
914
724
 
915
- @staticmethod
916
- def __calculate_entry_and_exit_axes_polarities_and_points_using_angles(row, cp, well_name, xy_units, z_units):
917
- """Calculate entry and exit axes, polarities and points using azimuth and inclination angles."""
725
+ returns:
726
+ self if successful; None otherwise
727
+ """
918
728
 
919
- angla = row['ANGLA']
920
- inclination = row['ANGLV']
921
- if inclination < 0.001:
922
- azimuth = 0.0
729
+ if well_name:
730
+ self.well_name = well_name
923
731
  else:
924
- i_vector = np.sum(cp[:, :, 1] - cp[:, :, 0], axis = (0, 1))
925
- azimuth = vec.azimuth(i_vector) - angla # see Nexus keyword reference doc
926
- well_vector = vec.unit_vector_from_azimuth_and_inclination(azimuth, inclination) * 10000.0
927
- if xy_units != z_units:
928
- well_vector[2] = wam.convert_lengths(well_vector[2], xy_units, z_units)
929
- well_vector = vec.unit_vector(well_vector) * 10000.0
930
- # todo: the following might be producing NaN's when vector passes precisely through an edge
931
- (entry_axis, entry_polarity, entry_xyz, exit_axis, exit_polarity, exit_xyz) = \
932
- rqwu.find_entry_and_exit(cp, -well_vector, well_vector, well_name)
933
- return entry_axis, entry_polarity, entry_xyz, exit_axis, exit_polarity, exit_xyz
732
+ well_name = self.well_name
733
+ if not self.title:
734
+ self.title = well_name
934
735
 
935
- def __calculate_entry_and_exit_axes_polarities_and_points_using_indices(df, i, cell_kji0, blocked_cells_kji0):
936
-
937
- entry_axis, entry_polarity = BlockedWell.__fabricate_entry_axis_and_polarity_using_indices(
938
- i, cell_kji0, blocked_cells_kji0)
939
- exit_axis, exit_polarity = BlockedWell.__fabricate_exit_axis_and_polarity_using_indices(
940
- i, cell_kji0, entry_axis, entry_polarity, df)
941
-
942
- return entry_axis, entry_polarity, exit_axis, exit_polarity
943
-
944
- @staticmethod
945
- def __fabricate_entry_axis_and_polarity_using_indices(i, cell_kji0, blocked_cells_kji0):
946
- """Fabricate entry and exit axes and polarities based on indices alone.
947
-
948
- note:
949
- could use geometry but here a cheap rough-and-ready approach is used
950
- """
951
-
952
- if i == 0:
953
- entry_axis, entry_polarity = 0, 0 # K-
954
- else:
955
- entry_move = cell_kji0 - blocked_cells_kji0[-1]
956
- log.debug(f'entry move: {entry_move}')
957
- if entry_move[1] == 0 and entry_move[2] == 0: # K move
958
- entry_axis = 0
959
- entry_polarity = 0 if entry_move[0] >= 0 else 1
960
- elif abs(entry_move[1]) > abs(entry_move[2]): # J dominant move
961
- entry_axis = 1
962
- entry_polarity = 0 if entry_move[1] >= 0 else 1
963
- else: # I dominant move
964
- entry_axis = 2
965
- entry_polarity = 0 if entry_move[2] >= 0 else 1
966
- return entry_axis, entry_polarity
967
-
968
- @staticmethod
969
- def __fabricate_exit_axis_and_polarity_using_indices(i, cell_kji0, entry_axis, entry_polarity, df):
970
- if i == len(df) - 1:
971
- exit_axis, exit_polarity = entry_axis, 1 - entry_polarity
972
- else:
973
- next_cell_kji0 = BlockedWell.__cell_kji0_from_df(df, i + 1)
974
- if next_cell_kji0 is None:
975
- exit_axis, exit_polarity = entry_axis, 1 - entry_polarity
976
- else:
977
- exit_move = next_cell_kji0 - cell_kji0
978
- log.debug(f'exit move: {exit_move}')
979
- if exit_move[1] == 0 and exit_move[2] == 0: # K move
980
- exit_axis = 0
981
- exit_polarity = 1 if exit_move[0] >= 0 else 0
982
- elif abs(exit_move[1]) > abs(exit_move[2]): # J dominant move
983
- exit_axis = 1
984
- exit_polarity = 1 if exit_move[1] >= 0 else 0
985
- else: # I dominant move
986
- exit_axis = 2
987
- exit_polarity = 1 if exit_move[2] >= 0 else 0
988
- return exit_axis, exit_polarity
989
-
990
- @staticmethod
991
- def __override_vector_based_xyz_entry_and_exit_points_if_necessary(use_face_centres, entry_axis, exit_axis,
992
- entry_polarity, exit_polarity, cp):
993
- """Override the vector based xyz entry and exit with face centres."""
994
-
995
- if use_face_centres: # override the vector based xyz entry and exit points with face centres
996
- if entry_axis == 0:
997
- entry_xyz = np.mean(cp[entry_polarity, :, :], axis = (0, 1))
998
- elif entry_axis == 1:
999
- entry_xyz = np.mean(cp[:, entry_polarity, :], axis = (0, 1))
1000
- else:
1001
- entry_xyz = np.mean(cp[:, :, entry_polarity], axis = (0, 1)) # entry_axis == 2, ie. I
1002
- if exit_axis == 0:
1003
- exit_xyz = np.mean(cp[exit_polarity, :, :], axis = (0, 1))
1004
- elif exit_axis == 1:
1005
- exit_xyz = np.mean(cp[:, exit_polarity, :], axis = (0, 1))
1006
- else:
1007
- exit_xyz = np.mean(cp[:, :, exit_polarity], axis = (0, 1)) # exit_axis == 2, ie. I
1008
- return entry_xyz, exit_xyz
1009
-
1010
- @staticmethod
1011
- def __add_interval(previous_xyz, entry_axis, entry_polarity, entry_xyz, exit_axis, exit_polarity, exit_xyz,
1012
- cell_kji0, trajectory_mds, trajectory_points, blocked_intervals, blocked_cells_kji0,
1013
- blocked_face_pairs, xy_units, z_units, length_uom):
1014
- if previous_xyz is None: # first entry
1015
- log.debug('adding mean sea level trajectory start')
1016
- previous_xyz = entry_xyz.copy()
1017
- previous_xyz[2] = 0.0 # use depth zero as md datum
1018
- trajectory_mds.append(0.0)
1019
- trajectory_points.append(previous_xyz)
1020
- if not vec.isclose(previous_xyz, entry_xyz, tolerance = 0.05): # add an unblocked interval
1021
- log.debug('adding unblocked interval')
1022
- trajectory_points.append(entry_xyz)
1023
- new_md = trajectory_mds[-1] + BlockedWell._md_length(entry_xyz - previous_xyz, xy_units, z_units,
1024
- length_uom)
1025
- trajectory_mds.append(new_md)
1026
- blocked_intervals.append(-1) # unblocked interval
1027
- previous_xyz = entry_xyz
1028
- log.debug('adding blocked interval for cell kji0: ' + str(cell_kji0))
1029
- trajectory_points.append(exit_xyz)
1030
- new_md = trajectory_mds[-1] + BlockedWell._md_length(exit_xyz - previous_xyz, xy_units, z_units, length_uom)
1031
- trajectory_mds.append(new_md)
1032
- blocked_intervals.append(0) # blocked interval
1033
- previous_xyz = exit_xyz
1034
- blocked_cells_kji0.append(cell_kji0)
1035
- blocked_face_pairs.append(((entry_axis, entry_polarity), (exit_axis, exit_polarity)))
1036
-
1037
- return previous_xyz, trajectory_mds, trajectory_points, blocked_intervals, blocked_cells_kji0, blocked_face_pairs
1038
-
1039
- @staticmethod
1040
- def _md_length(xyz_vector, xy_units, z_units, length_uom):
1041
- if length_uom == xy_units and length_uom == z_units:
1042
- return vec.naive_length(xyz_vector)
1043
- x = wam.convert_lengths(xyz_vector[0], xy_units, length_uom)
1044
- y = wam.convert_lengths(xyz_vector[1], xy_units, length_uom)
1045
- z = wam.convert_lengths(xyz_vector[2], z_units, length_uom)
1046
- return vec.naive_length((x, y, z))
1047
-
1048
- @staticmethod
1049
- def __add_tail_to_trajectory_if_necessary(blocked_count, exit_axis, exit_polarity, cell_kji0, grid,
1050
- trajectory_points, trajectory_mds):
1051
- """Add tail to trajcetory if last segment terminates at bottom face in bottom layer."""
1052
-
1053
- if blocked_count > 0 and exit_axis == 0 and exit_polarity == 1 and cell_kji0[
1054
- 0] == grid.nk - 1 and grid.k_direction_is_down:
1055
- tail_length = 10.0 # metres or feet
1056
- tail_xyz = trajectory_points[-1].copy()
1057
- tail_xyz[2] += tail_length * (1.0 if grid.z_inc_down() else -1.0)
1058
- trajectory_points.append(tail_xyz)
1059
- new_md = trajectory_mds[-1] + tail_length
1060
- trajectory_mds.append(new_md)
1061
-
1062
- return trajectory_points, trajectory_mds
1063
-
1064
- def __add_as_properties_if_specified(self,
1065
- add_as_properties,
1066
- df,
1067
- length_uom,
1068
- time_index = None,
1069
- time_series_uuid = None):
1070
- # if add_as_properties is True and present as a list of wellspec column names, both the blocked well and
1071
- # the properties will have their hdf5 data written, xml created and be added as parts to the model
1072
-
1073
- if add_as_properties and len(df.columns) > 3:
1074
- # NB: atypical writing of hdf5 data and xml creation in order to support related properties
1075
- self.write_hdf5()
1076
- self.create_xml()
1077
- if isinstance(add_as_properties, list):
1078
- for col in add_as_properties:
1079
- assert col in df.columns[3:] # could just skip missing columns
1080
- property_columns = add_as_properties
1081
- else:
1082
- property_columns = df.columns[3:]
1083
- self.add_df_properties(df,
1084
- property_columns,
1085
- length_uom = length_uom,
1086
- time_index = time_index,
1087
- time_series_uuid = time_series_uuid)
1088
-
1089
- def import_from_rms_cellio(self,
1090
- cellio_file,
1091
- well_name,
1092
- grid,
1093
- include_overburden_unblocked_interval = False,
1094
- set_tangent_vectors = False):
1095
- """Populates empty blocked well from RMS cell I/O data; creates simulation trajectory and md datum.
1096
-
1097
- arguments:
1098
- cellio_file (string): path of RMS ascii export file holding blocked well cell I/O data; cell entry and
1099
- exit points are expected
1100
- well_name (string): the name of the well as used in the cell I/O file
1101
- grid (grid.Grid object): the grid object which the cell indices in the cell I/O data relate to
1102
- set_tangent_vectors (boolean, default False): if True, tangent vectors will be computed from the well
1103
- trajectory's control points
1104
-
1105
- returns:
1106
- self if successful; None otherwise
1107
- """
1108
-
1109
- if well_name:
1110
- self.well_name = well_name
1111
- else:
1112
- well_name = self.well_name
1113
- if not self.title:
1114
- self.title = well_name
1115
-
1116
- grid_name = rqet.citation_title_for_node(grid.root)
1117
- length_uom = grid.z_units()
1118
- grid_z_inc_down = crs.Crs(grid.model, uuid = grid.crs_uuid).z_inc_down
1119
- log.debug('grid z increasing downwards: ' + str(grid_z_inc_down) + '(type: ' + str(type(grid_z_inc_down)) + ')')
1120
- cellio_z_inc_down = None
736
+ grid_name = rqet.citation_title_for_node(grid.root)
737
+ length_uom = grid.z_units()
738
+ grid_z_inc_down = crs.Crs(grid.model, uuid = grid.crs_uuid).z_inc_down
739
+ log.debug('grid z increasing downwards: ' + str(grid_z_inc_down) + '(type: ' + str(type(grid_z_inc_down)) + ')')
740
+ cellio_z_inc_down = None
1121
741
 
1122
742
  try:
1123
743
  assert ' ' not in well_name, 'cannot import for well name containing spaces'
@@ -1195,9 +815,10 @@ class BlockedWell(BaseResqpy):
1195
815
  self.node_count = len(trajectory_mds)
1196
816
  self.node_mds = np.array(trajectory_mds)
1197
817
  self.cell_count = len(blocked_cells_kji0)
1198
- self.grid_indices = np.array(blocked_intervals,
1199
- dtype = int) # NB. only supporting one grid at the moment
1200
- self.cell_indices = grid.natural_cell_indices(np.array(blocked_cells_kji0))
818
+ # NB. only supporting one grid at the moment
819
+ self.grid_indices = np.array(blocked_intervals, dtype = np.int32)
820
+ self.cell_indices = grid.natural_cell_indices(np.array(blocked_cells_kji0)).astype(
821
+ self.cell_index_dtype)
1201
822
  self.face_pair_indices = np.array(blocked_face_pairs)
1202
823
  self.grid_list = [grid]
1203
824
 
@@ -1208,70 +829,6 @@ class BlockedWell(BaseResqpy):
1208
829
 
1209
830
  return self
1210
831
 
1211
- @staticmethod
1212
- def __verify_header_lines_in_cellio_file(fp, well_name, cellio_file):
1213
- """Find and verify the information in the header lines for the specified well in the RMS cellio file."""
1214
- while True:
1215
- kf.skip_blank_lines_and_comments(fp)
1216
- line = fp.readline() # file format version number?
1217
- assert line, 'well ' + str(well_name) + ' not found in file ' + str(cellio_file)
1218
- fp.readline() # 'Undefined'
1219
- words = fp.readline().split()
1220
- assert len(words), 'missing header info in cell I/O file'
1221
- if words[0].upper() == well_name.upper():
1222
- break
1223
- while not kf.blank_line(fp):
1224
- fp.readline() # skip to block of data for next well
1225
- header_lines = int(fp.readline().strip())
1226
- for _ in range(header_lines):
1227
- fp.readline()
1228
-
1229
- @staticmethod
1230
- def __parse_non_blank_line_in_cellio_file(line, grid, cellio_z_inc_down, grid_z_inc_down):
1231
- """Parse each non-blank line in the RMS cellio file for the relevant parameters."""
1232
-
1233
- words = line.split()
1234
- assert len(words) >= 9, 'not enough items on data line in cell I/O file, minimum 9 expected'
1235
- i1, j1, k1 = int(words[0]), int(words[1]), int(words[2])
1236
- cell_kji0 = np.array((k1 - 1, j1 - 1, i1 - 1), dtype = int)
1237
- assert np.all(0 <= cell_kji0) and np.all(
1238
- cell_kji0 < grid.extent_kji), 'cell I/O cell index not within grid extent'
1239
- entry_xyz = np.array((float(words[3]), float(words[4]), float(words[5])))
1240
- exit_xyz = np.array((float(words[6]), float(words[7]), float(words[8])))
1241
- if cellio_z_inc_down is None:
1242
- cellio_z_inc_down = bool(entry_xyz[2] + exit_xyz[2] > 0.0)
1243
- if cellio_z_inc_down != grid_z_inc_down:
1244
- entry_xyz[2] = -entry_xyz[2]
1245
- exit_xyz[2] = -exit_xyz[2]
1246
- return cell_kji0, entry_xyz, exit_xyz
1247
-
1248
- @staticmethod
1249
- def __calculate_cell_cp_center_and_vectors(grid, cell_kji0, entry_xyz, exit_xyz, well_name):
1250
- # calculate the i,j,k coordinates that represent the corner points and center of a perforation cell
1251
- # calculate the entry and exit vectors for the perforation cell
1252
-
1253
- cp = grid.corner_points(cell_kji0 = cell_kji0, cache_resqml_array = False)
1254
- assert not np.any(np.isnan(
1255
- cp)), 'missing geometry for perforation cell(kji0) ' + str(cell_kji0) + ' for well ' + str(well_name)
1256
- cell_centre = np.mean(cp, axis = (0, 1, 2))
1257
- # let's hope everything is in the same coordinate reference system!
1258
- entry_vector = 100.0 * (entry_xyz - cell_centre)
1259
- exit_vector = 100.0 * (exit_xyz - cell_centre)
1260
- return cp, cell_centre, entry_vector, exit_vector
1261
-
1262
- @staticmethod
1263
- def __check_number_of_blocked_well_intervals(blocked_cells_kji0, well_name, grid_name):
1264
- """Check that at least one interval is blocked for the specified well."""
1265
-
1266
- blocked_count = len(blocked_cells_kji0)
1267
- if blocked_count == 0:
1268
- log.warning(f"No intervals blocked for well {well_name} in grid"
1269
- f"{f' {grid_name}' if grid_name is not None else ''}.")
1270
- return None
1271
- else:
1272
- log.info(f"{blocked_count} interval{rqwu._pl(blocked_count)} blocked for well {well_name} in"
1273
- f" grid{f' {grid_name}' if grid_name is not None else ''}.")
1274
-
1275
832
  def dataframe(self,
1276
833
  i_col = 'IW',
1277
834
  j_col = 'JW',
@@ -1410,20 +967,21 @@ class BlockedWell(BaseResqpy):
1410
967
  time_series_uuid (UUID, optional): the uuid of the time series for time dependent properties being added
1411
968
 
1412
969
  notes:
1413
- units of length along wellbore will be those of the trajectory's length_uom (also applies to K.H values) unless
1414
- the length_uom argument is used;
1415
- the constraints are applied independently for each row and a row is excluded if it fails any constraint;
1416
- the min_k0 and max_k0 arguments do not stop later rows within the layer range from being included;
1417
- the min_length and min_kh limits apply to individual cell intervals and thus depend on cell size;
1418
- the water and oil saturation limits are for saturations at a single time and affect whether the interval
1419
- is included in the dataframe – there is no functionality to support turning perforations off and on over time;
1420
- the saturation limits do not stop deeper intervals with qualifying saturations from being included;
1421
- the k0_list, perforation_list and region_list arguments should be set to None to disable the corresponding functionality,
1422
- if set to an empty list, no rows will be included in the dataframe;
1423
- if add_as_properties is True, the blocked well must already have been added as a part to the model;
1424
- add_as_properties and use_properties cannot both be True;
1425
- add_as_properties and use_properties are only currently functional for single grid blocked wells;
1426
- at present, unit conversion is not handled when using properties
970
+ - units of length along wellbore will be those of the trajectory's length_uom (also applies to K.H values) unless
971
+ the length_uom argument is used;
972
+ - the constraints are applied independently for each row and a row is excluded if it fails any constraint;
973
+ - the min_k0 and max_k0 arguments do not stop later rows within the layer range from being included;
974
+ - the min_length and min_kh limits apply to individual cell intervals and thus depend on cell size;
975
+ - the water and oil saturation limits are for saturations at a single time and affect whether the interval
976
+ is included in the dataframe
977
+ to turn perforations off and on over time create a time series dependent bunch of boolean properties on
978
+ the blocked well, with title 'STAT' or local property kind 'well connection open';
979
+ - the saturation limits do not stop deeper intervals with qualifying saturations from being included;
980
+ - the k0_list, perforation_list and region_list arguments should be set to None to disable the
981
+ corresponding functionality, if set to an empty list, no rows will be included in the dataframe;
982
+ - if add_as_properties is True, the blocked well must already have been added as a part to the model;
983
+ - add_as_properties and use_properties cannot both be True;
984
+ - add_as_properties and use_properties are only currently functional for single grid blocked wells;
1427
985
 
1428
986
  :meta common:
1429
987
  """
@@ -1503,7 +1061,7 @@ class BlockedWell(BaseResqpy):
1503
1061
  for grid in self.grid_list:
1504
1062
  grid.cache_all_geometry_arrays()
1505
1063
 
1506
- k_face_check = np.zeros((2, 2), dtype = int)
1064
+ k_face_check = np.zeros((2, 2), dtype = np.int8)
1507
1065
  k_face_check[1, 1] = 1 # now represents entry, exit of K-, K+
1508
1066
  k_face_check_end = k_face_check.copy()
1509
1067
  k_face_check_end[1] = -1 # entry through K-, terminating (TD) within cell
@@ -1637,41 +1195,46 @@ class BlockedWell(BaseResqpy):
1637
1195
  if skip_interval_due_to_min_kh:
1638
1196
  continue
1639
1197
 
1640
- length, radw_i, skin_i, radb, wi, wbc = BlockedWell.__get_pc_arrays_for_interval(pc = pc,
1641
- pc_timeless = pc_timeless,
1642
- pc_titles = pc_titles,
1643
- ci = ci,
1644
- length = length,
1645
- radw = radw,
1646
- skin = skin,
1647
- length_uom = length_uom,
1648
- grid = grid,
1649
- traj_crs = traj_crs)
1198
+ length, radw_i, skin_i, radb, wi, wbc, stat_i = \
1199
+ BlockedWell.__get_pc_arrays_for_interval(pc = pc,
1200
+ pc_timeless = pc_timeless,
1201
+ pc_titles = pc_titles,
1202
+ ci = ci,
1203
+ length = length,
1204
+ radw = radw,
1205
+ skin = skin,
1206
+ stat = stat,
1207
+ length_uom = length_uom,
1208
+ grid = grid,
1209
+ traj_crs = traj_crs)
1650
1210
  if skin_i is None:
1651
1211
  skin_i = 0.0
1652
1212
  if radw_i is None:
1653
1213
  radw_i = (0.33 if length_uom == 'ft' else 0.1)
1654
-
1655
- radb, wi, wbc = BlockedWell.__get_well_inflow_parameters_for_interval(do_well_inflow = do_well_inflow,
1656
- isotropic_perm = isotropic_perm,
1657
- ntg_is_one = ntg_is_one,
1658
- k_i = k_i,
1659
- k_j = k_j,
1660
- k_k = k_k,
1661
- sine_anglv = sine_anglv,
1662
- cosine_anglv = cosine_anglv,
1663
- sine_angla = sine_angla,
1664
- cosine_angla = cosine_angla,
1665
- grid = grid,
1666
- cell_kji0 = cell_kji0,
1667
- radw = radw_i,
1668
- radb = radb,
1669
- wi = wi,
1670
- wbc = wbc,
1671
- skin = skin_i,
1672
- kh = kh,
1673
- length_uom = length_uom,
1674
- column_list = column_list)
1214
+ if stat_i is None:
1215
+ stat_i = stat
1216
+
1217
+ radb, wi, wbc = \
1218
+ BlockedWell.__get_well_inflow_parameters_for_interval(do_well_inflow = do_well_inflow,
1219
+ isotropic_perm = isotropic_perm,
1220
+ ntg_is_one = ntg_is_one,
1221
+ k_i = k_i,
1222
+ k_j = k_j,
1223
+ k_k = k_k,
1224
+ sine_anglv = sine_anglv,
1225
+ cosine_anglv = cosine_anglv,
1226
+ sine_angla = sine_angla,
1227
+ cosine_angla = cosine_angla,
1228
+ grid = grid,
1229
+ cell_kji0 = cell_kji0,
1230
+ radw = radw_i,
1231
+ radb = radb,
1232
+ wi = wi,
1233
+ wbc = wbc,
1234
+ skin = skin_i,
1235
+ kh = kh,
1236
+ length_uom = length_uom,
1237
+ column_list = column_list)
1675
1238
 
1676
1239
  xyz = self.__get_xyz_for_interval(doing_xyz = doing_xyz,
1677
1240
  length_mode = length_mode,
@@ -1703,7 +1266,7 @@ class BlockedWell(BaseResqpy):
1703
1266
  kh = kh,
1704
1267
  xyz = xyz,
1705
1268
  md = md,
1706
- stat = stat,
1269
+ stat = stat_i,
1707
1270
  part_perf_fraction = part_perf_fraction,
1708
1271
  radb = radb,
1709
1272
  wi = wi,
@@ -1751,6 +1314,7 @@ class BlockedWell(BaseResqpy):
1751
1314
  fraction of wellbore frame interval in cell,
1752
1315
  fraction of cell's wellbore interval in wellbore frame interval)
1753
1316
  """
1317
+
1754
1318
  return bwf.blocked_well_frame_contributions_list(self, wbf)
1755
1319
 
1756
1320
  def add_properties_from_wellbore_frame(self,
@@ -1815,1469 +1379,2055 @@ class BlockedWell(BaseResqpy):
1815
1379
  set_perforation_fraction = set_perforation_fraction,
1816
1380
  set_frame_interval = set_frame_interval)
1817
1381
 
1818
- def __get_interval_count(self):
1819
- """Get the number of intervals to be added to the dataframe."""
1820
-
1821
- if self.node_count is None or self.node_count < 2:
1822
- interval_count = 0
1823
- else:
1824
- interval_count = self.node_count - 1
1825
-
1826
- return interval_count
1827
-
1828
- @staticmethod
1829
- def __prop_array(uuid_or_dict, grid):
1830
- assert uuid_or_dict is not None and grid is not None
1831
- if isinstance(uuid_or_dict, dict):
1832
- prop_uuid = uuid_or_dict[grid.uuid]
1833
- else:
1834
- prop_uuid = uuid_or_dict # uuid either in form of string or uuid.UUID
1835
- return grid.property_collection.single_array_ref(uuid = prop_uuid)
1836
-
1837
- @staticmethod
1838
- def __get_ref_vector(grid, grid_crs, cell_kji0, mode):
1839
- # returns unit vector with true direction, ie. accounts for differing xy & z units in grid's crs
1840
- # gravity = np.array((0.0, 0.0, 1.0))
1841
- if mode == 'normal well i+':
1842
- return None # ANGLA only: option for no projection onto a plane
1843
- ref_vector = None
1844
- # options for anglv or angla reference: 'z down', 'z+', 'k down', 'k+', 'normal ij', 'normal ij down'
1845
- if mode == 'z+':
1846
- ref_vector = np.array((0.0, 0.0, 1.0))
1847
- elif mode == 'z down':
1848
- if grid_crs.z_inc_down:
1849
- ref_vector = np.array((0.0, 0.0, 1.0))
1850
- else:
1851
- ref_vector = np.array((0.0, 0.0, -1.0))
1852
- else:
1853
- cell_axial_vectors = grid.interface_vectors_kji(cell_kji0)
1854
- if grid_crs.xy_units != grid_crs.z_units:
1855
- wam.convert_lengths(cell_axial_vectors[..., 2], grid_crs.z_units, grid_crs.xy_units)
1856
- if mode in ['k+', 'k down']:
1857
- ref_vector = vec.unit_vector(cell_axial_vectors[0])
1858
- if mode == 'k down' and not grid.k_direction_is_down:
1859
- ref_vector = -ref_vector
1860
- else: # normal to plane of ij axes
1861
- ref_vector = vec.unit_vector(vec.cross_product(cell_axial_vectors[1], cell_axial_vectors[2]))
1862
- if mode == 'normal ij down':
1863
- if grid_crs.z_inc_down:
1864
- if ref_vector[2] < 0.0:
1865
- ref_vector = -ref_vector
1866
- else:
1867
- if ref_vector[2] > 0.0:
1868
- ref_vector = -ref_vector
1869
- if ref_vector is None or ref_vector[2] == 0.0:
1870
- if grid_crs.z_inc_down:
1871
- ref_vector = np.array((0.0, 0.0, 1.0))
1872
- else:
1873
- ref_vector = np.array((0.0, 0.0, -1.0))
1874
- return ref_vector
1875
-
1876
- @staticmethod
1877
- def __verify_angle_references(anglv_ref, angla_plane_ref):
1878
- """Verify that the references for anglv and angla are one of the acceptable options."""
1879
-
1880
- assert anglv_ref in ['gravity', 'z down', 'z+', 'k down', 'k+', 'normal ij', 'normal ij down']
1881
- if anglv_ref == 'gravity':
1882
- anglv_ref = 'z down'
1883
- if angla_plane_ref is None:
1884
- angla_plane_ref = anglv_ref
1885
- assert angla_plane_ref in [
1886
- 'gravity', 'z down', 'z+', 'k down', 'k+', 'normal ij', 'normal ij down', 'normal well i+'
1887
- ]
1888
- if angla_plane_ref == 'gravity':
1889
- angla_plane_ref = 'z down'
1890
- return anglv_ref, angla_plane_ref
1891
-
1892
- @staticmethod
1893
- def __verify_saturation_ranges_and_property_uuids(max_satw, min_sato, max_satg, satw_uuid, sato_uuid, satg_uuid):
1894
- # verify that the fluid saturation limits fall within 0.0 to 1.0 and that the uuid of the required
1895
- # saturation property array has been specified.
1896
-
1897
- if max_satw is not None and max_satw >= 1.0:
1898
- max_satw = None
1899
- if min_sato is not None and min_sato <= 0.0:
1900
- min_sato = None
1901
- if max_satg is not None and max_satg >= 1.0:
1902
- max_satg = None
1903
-
1904
- phase_list = ['water', 'oil', 'gas']
1905
- phase_saturation_limits_list = [max_satw, min_sato, max_satg]
1906
- uuids_list = [satw_uuid, sato_uuid, satg_uuid]
1907
-
1908
- for phase, phase_limit, uuid in zip(phase_list, phase_saturation_limits_list, uuids_list):
1909
- if phase_limit is not None:
1910
- assert uuid is not None, f'{phase} saturation limit specified without saturation property array'
1382
+ def add_df_properties(self,
1383
+ df,
1384
+ columns,
1385
+ length_uom = None,
1386
+ time_index = None,
1387
+ time_series_uuid = None,
1388
+ realization = None):
1389
+ """Creates a property part for each column in the dataframe, based on the dataframe values.
1911
1390
 
1912
- return max_satw, min_sato, max_satg
1391
+ arguments:
1392
+ df (pd.DataFrame): dataframe containing the columns that will be converted to properties
1393
+ columns (List[str]): list of the column names that will be converted to properties
1394
+ length_uom (str, optional): the length unit of measure
1395
+ time_index (int, optional): if adding a timestamp to the property, this is the timestamp
1396
+ index of the TimeSeries timestamps attribute
1397
+ time_series_uuid (uuid.UUID, optional): if adding a timestamp to the property, this is
1398
+ the uuid of the TimeSeries object
1399
+ realization (int, optional): if present, is used as the realization number for all the
1400
+ properties
1913
1401
 
1914
- @staticmethod
1915
- def __verify_extra_properties_to_be_added_to_dataframe(extra_columns_list, column_list, add_as_properties,
1916
- use_properties, skin, stat, radw):
1917
- """Determine which extra columns, if any, should be added as properties to the dataframe.
1402
+ returns:
1403
+ None
1918
1404
 
1919
- note:
1920
- if skin, stat or radw are None, default values are specified
1405
+ notes:
1406
+ the column name is used as the property citation title;
1407
+ the blocked well must already exist as a part in the model;
1408
+ this method currently only handles single grid situations;
1409
+ dataframe rows must be in the same order as the cells in the blocked well
1921
1410
  """
1922
1411
 
1923
- if extra_columns_list:
1924
- for extra in extra_columns_list:
1925
- assert extra.upper() in [
1926
- 'GRID', 'ANGLA', 'ANGLV', 'LENGTH', 'KH', 'DEPTH', 'MD', 'X', 'Y', 'SKIN', 'RADW', 'PPERF', 'RADB',
1927
- 'WI', 'WBC', 'STAT'
1928
- ]
1929
- column_list.append(extra.upper())
1930
- else:
1931
- add_as_properties = use_properties = False
1932
- assert not (add_as_properties and use_properties)
1412
+ # todo: enhance to handle multiple grids
1413
+ assert len(self.grid_list) == 1
1414
+ if columns is None or len(columns) == 0 or len(df) == 0:
1415
+ return
1416
+ if length_uom is None:
1417
+ length_uom = self.trajectory.md_uom
1418
+ extra_pc = rqp.PropertyCollection()
1419
+ extra_pc.set_support(support = self)
1420
+ assert len(df) == self.cell_count
1933
1421
 
1934
- column_list, skin, stat, radw = BlockedWell.__check_skin_stat_radw_to_be_added_as_properties(
1935
- skin = skin, stat = stat, radw = radw, column_list = column_list)
1422
+ for column in columns:
1423
+ extra = column.upper()
1424
+ uom, pk, discrete = self._get_uom_pk_discrete_for_df_properties(extra = extra, length_uom = length_uom)
1425
+ if discrete:
1426
+ null_value = -1
1427
+ na_value = -1
1428
+ dtype = np.int32
1429
+ else:
1430
+ null_value = None
1431
+ na_value = np.nan
1432
+ dtype = float
1433
+ # 'SKIN': use defaults for now; todo: create local property kind for skin
1434
+ if column == 'STAT':
1435
+ col_as_list = list(df[column])
1436
+ expanded = np.array([(0 if (str(st).upper() in ['OFF', '0', 'FALSE']) else 1) for st in col_as_list],
1437
+ dtype = np.int8)
1438
+ dtype = np.int8
1439
+ else:
1440
+ expanded = df[column].to_numpy(dtype = dtype, copy = True, na_value = na_value)
1441
+ extra_pc.add_cached_array_to_imported_list(
1442
+ expanded,
1443
+ 'blocked well dataframe',
1444
+ extra,
1445
+ discrete = discrete,
1446
+ uom = uom,
1447
+ property_kind = pk,
1448
+ local_property_kind_uuid = None,
1449
+ facet_type = None,
1450
+ facet = None,
1451
+ realization = realization,
1452
+ indexable_element = 'cells',
1453
+ count = 1,
1454
+ time_index = time_index,
1455
+ null_value = null_value,
1456
+ )
1457
+ extra_pc.write_hdf5_for_imported_list()
1458
+ extra_pc.create_xml_for_imported_list_and_add_parts_to_model(time_series_uuid = time_series_uuid,
1459
+ find_local_property_kinds = True)
1936
1460
 
1937
- return column_list, add_as_properties, use_properties, skin, stat, radw
1461
+ def static_kh(self,
1462
+ ntg_uuid = None,
1463
+ perm_i_uuid = None,
1464
+ perm_j_uuid = None,
1465
+ perm_k_uuid = None,
1466
+ satw_uuid = None,
1467
+ sato_uuid = None,
1468
+ satg_uuid = None,
1469
+ region_uuid = None,
1470
+ active_only = False,
1471
+ min_k0 = None,
1472
+ max_k0 = None,
1473
+ k0_list = None,
1474
+ min_length = None,
1475
+ min_kh = None,
1476
+ max_depth = None,
1477
+ max_satw = None,
1478
+ min_sato = None,
1479
+ max_satg = None,
1480
+ perforation_list = None,
1481
+ region_list = None,
1482
+ set_k_face_intervals_vertical = False,
1483
+ anglv_ref = 'gravity',
1484
+ angla_plane_ref = None,
1485
+ length_mode = 'MD',
1486
+ length_uom = None,
1487
+ use_face_centres = False,
1488
+ preferential_perforation = True):
1489
+ """Returns the total static K.H (permeability x height).
1938
1490
 
1939
- @staticmethod
1940
- def __check_perforation_properties_to_be_added(column_list, perforation_list):
1491
+ notes:
1492
+ length units are those of trajectory md_uom unless length_upm is set;
1493
+ see doc string for dataframe() method for argument descriptions; perm_i_uuid required
1494
+ """
1941
1495
 
1942
- if all(['LENGTH' in column_list, 'PPERF' in column_list, 'KH' not in column_list, perforation_list
1943
- is not None]):
1944
- log.warning(
1945
- 'both LENGTH and PPERF will include effects of partial perforation; only one should be used in WELLSPEC'
1946
- )
1947
- elif all([
1948
- perforation_list is not None, 'LENGTH' not in column_list, 'PPERF' not in column_list, 'KH'
1949
- not in column_list, 'WBC' not in column_list
1950
- ]):
1951
- log.warning('perforation list supplied but no use of LENGTH, KH, PPERF nor WBC')
1496
+ df = self.dataframe(i_col = 'I',
1497
+ j_col = 'J',
1498
+ k_col = 'K',
1499
+ one_based = False,
1500
+ extra_columns_list = ['KH'],
1501
+ ntg_uuid = ntg_uuid,
1502
+ perm_i_uuid = perm_i_uuid,
1503
+ perm_j_uuid = perm_j_uuid,
1504
+ perm_k_uuid = perm_k_uuid,
1505
+ satw_uuid = satw_uuid,
1506
+ sato_uuid = sato_uuid,
1507
+ satg_uuid = satg_uuid,
1508
+ region_uuid = region_uuid,
1509
+ active_only = active_only,
1510
+ min_k0 = min_k0,
1511
+ max_k0 = max_k0,
1512
+ k0_list = k0_list,
1513
+ min_length = min_length,
1514
+ min_kh = min_kh,
1515
+ max_depth = max_depth,
1516
+ max_satw = max_satw,
1517
+ min_sato = min_sato,
1518
+ max_satg = max_satg,
1519
+ perforation_list = perforation_list,
1520
+ region_list = region_list,
1521
+ set_k_face_intervals_vertical = set_k_face_intervals_vertical,
1522
+ anglv_ref = anglv_ref,
1523
+ angla_plane_ref = angla_plane_ref,
1524
+ length_mode = length_mode,
1525
+ length_uom = length_uom,
1526
+ use_face_centres = use_face_centres,
1527
+ preferential_perforation = preferential_perforation)
1952
1528
 
1953
- if perforation_list is not None and len(perforation_list) == 0:
1954
- log.warning('empty perforation list specified for blocked well dataframe: no rows will be included')
1529
+ return sum(df['KH'])
1955
1530
 
1956
- @staticmethod
1957
- def __check_skin_stat_radw_to_be_added_as_properties(skin, stat, radw, column_list):
1958
- """Verify whether skin should be added as a property in the dataframe."""
1959
-
1960
- if skin is not None and 'SKIN' not in column_list:
1961
- column_list.append('SKIN')
1962
-
1963
- if stat is not None:
1964
- assert str(stat).upper() in ['ON', 'OFF']
1965
- stat = str(stat).upper()
1966
- if 'STAT' not in column_list:
1967
- column_list.append('STAT')
1968
- else:
1969
- stat = 'ON'
1970
-
1971
- if radw is not None and 'RADW' not in column_list:
1972
- column_list.append('RADW')
1973
-
1974
- return column_list, skin, stat, radw
1975
-
1976
- @staticmethod
1977
- def __verify_perm_i_uuid_for_well_inflow(column_list, perm_i_uuid, pc_titles):
1978
- # Verify that the I direction permeability has been specified if well inflow properties are to be added
1979
- # to the dataframe.
1980
-
1981
- do_well_inflow = (('WI' in column_list and 'WI' not in pc_titles) or
1982
- ('WBC' in column_list and 'WBC' not in pc_titles) or
1983
- ('RADB' in column_list and 'RADB' not in pc_titles))
1984
- if do_well_inflow:
1985
- assert perm_i_uuid is not None, 'WI, RADB or WBC requested without I direction permeabilty being specified'
1531
+ def write_wellspec(self,
1532
+ wellspec_file,
1533
+ well_name = None,
1534
+ mode = 'a',
1535
+ extra_columns_list = [],
1536
+ ntg_uuid = None,
1537
+ perm_i_uuid = None,
1538
+ perm_j_uuid = None,
1539
+ perm_k_uuid = None,
1540
+ satw_uuid = None,
1541
+ sato_uuid = None,
1542
+ satg_uuid = None,
1543
+ region_uuid = None,
1544
+ radw = None,
1545
+ skin = None,
1546
+ stat = None,
1547
+ active_only = False,
1548
+ min_k0 = None,
1549
+ max_k0 = None,
1550
+ k0_list = None,
1551
+ min_length = None,
1552
+ min_kh = None,
1553
+ max_depth = None,
1554
+ max_satw = None,
1555
+ min_sato = None,
1556
+ max_satg = None,
1557
+ perforation_list = None,
1558
+ region_list = None,
1559
+ set_k_face_intervals_vertical = False,
1560
+ depth_inc_down = True,
1561
+ anglv_ref = 'gravity',
1562
+ angla_plane_ref = None,
1563
+ length_mode = 'MD',
1564
+ length_uom = None,
1565
+ preferential_perforation = True,
1566
+ space_instead_of_tab_separator = True,
1567
+ align_columns = True,
1568
+ preceeding_blank_lines = 0,
1569
+ trailing_blank_lines = 0,
1570
+ length_uom_comment = False,
1571
+ write_nexus_units = True,
1572
+ float_format = '5.3',
1573
+ use_properties = False,
1574
+ property_time_index = None):
1575
+ """Writes Nexus WELLSPEC keyword to an ascii file.
1986
1576
 
1987
- return do_well_inflow
1577
+ returns:
1578
+ pandas DataFrame containing data that has been written to the wellspec file
1988
1579
 
1989
- @staticmethod
1990
- def __verify_perm_i_uuid_for_kh(min_kh, column_list, perm_i_uuid, pc_titles):
1991
- # verify that the I direction permeability has been specified if permeability thickness and
1992
- # wellbore constant properties are to be added to the dataframe.
1580
+ note:
1581
+ see doc string for dataframe() method for most of the argument descriptions;
1582
+ align_columns and float_format arguments are deprecated and no longer used
1583
+ """
1993
1584
 
1994
- if min_kh is not None and min_kh <= 0.0:
1995
- min_kh = None
1996
- doing_kh = False
1997
- if ('KH' in column_list or min_kh is not None) and 'KH' not in pc_titles:
1998
- assert perm_i_uuid is not None, 'KH requested (or minimum specified) without I direction permeabilty being specified'
1999
- doing_kh = True
2000
- if 'WBC' in column_list and 'WBC' not in pc_titles:
2001
- assert perm_i_uuid is not None, 'WBC requested without I direction permeabilty being specified'
2002
- doing_kh = True
1585
+ assert wellspec_file, 'no output file specified to write WELLSPEC to'
2003
1586
 
2004
- return min_kh, doing_kh
1587
+ col_width_dict = {
1588
+ 'IW': 4,
1589
+ 'JW': 4,
1590
+ 'L': 4,
1591
+ 'ANGLA': 8,
1592
+ 'ANGLV': 8,
1593
+ 'LENGTH': 8,
1594
+ 'KH': 10,
1595
+ 'DEPTH': 10,
1596
+ 'MD': 10,
1597
+ 'X': 8,
1598
+ 'Y': 12,
1599
+ 'SKIN': 7,
1600
+ 'RADW': 5,
1601
+ 'RADB': 8,
1602
+ 'PPERF': 5
1603
+ }
2005
1604
 
2006
- @staticmethod
2007
- def __verify_perm_j_k_uuids_for_kh_and_well_inflow(doing_kh, do_well_inflow, perm_i_uuid, perm_j_uuid, perm_k_uuid):
2008
- # verify that the J and K direction permeabilities have been specified if well inflow properties or
2009
- # permeability thickness properties are to be added to the dataframe.
1605
+ well_name = self.__get_well_name(well_name = well_name)
2010
1606
 
2011
- isotropic_perm = None
2012
- if doing_kh or do_well_inflow:
2013
- if perm_j_uuid is None and perm_k_uuid is None:
2014
- isotropic_perm = True
2015
- else:
2016
- if perm_j_uuid is None:
2017
- perm_j_uuid = perm_i_uuid
2018
- if perm_k_uuid is None:
2019
- perm_k_uuid = perm_i_uuid
2020
- # following line assumes arguments are passed in same form; if not, some unnecessary maths might be done
2021
- isotropic_perm = (bu.matching_uuids(perm_i_uuid, perm_j_uuid) and
2022
- bu.matching_uuids(perm_i_uuid, perm_k_uuid))
1607
+ df = self.dataframe(one_based = True,
1608
+ extra_columns_list = extra_columns_list,
1609
+ ntg_uuid = ntg_uuid,
1610
+ perm_i_uuid = perm_i_uuid,
1611
+ perm_j_uuid = perm_j_uuid,
1612
+ perm_k_uuid = perm_k_uuid,
1613
+ satw_uuid = satw_uuid,
1614
+ sato_uuid = sato_uuid,
1615
+ satg_uuid = satg_uuid,
1616
+ region_uuid = region_uuid,
1617
+ radw = radw,
1618
+ skin = skin,
1619
+ stat = stat,
1620
+ active_only = active_only,
1621
+ min_k0 = min_k0,
1622
+ max_k0 = max_k0,
1623
+ k0_list = k0_list,
1624
+ min_length = min_length,
1625
+ min_kh = min_kh,
1626
+ max_depth = max_depth,
1627
+ max_satw = max_satw,
1628
+ min_sato = min_sato,
1629
+ max_satg = max_satg,
1630
+ perforation_list = perforation_list,
1631
+ region_list = region_list,
1632
+ depth_inc_down = depth_inc_down,
1633
+ set_k_face_intervals_vertical = set_k_face_intervals_vertical,
1634
+ anglv_ref = anglv_ref,
1635
+ angla_plane_ref = angla_plane_ref,
1636
+ length_mode = length_mode,
1637
+ length_uom = length_uom,
1638
+ preferential_perforation = preferential_perforation,
1639
+ use_properties = use_properties,
1640
+ property_time_index = property_time_index)
2023
1641
 
2024
- return perm_j_uuid, perm_k_uuid, isotropic_perm
1642
+ sep = ' ' if space_instead_of_tab_separator else '\t'
2025
1643
 
2026
- @staticmethod
2027
- def __verify_k_layers_to_be_included(min_k0, max_k0, k0_list):
2028
- # verify that the k layers to be included in the dataframe exist within the appropriate range
1644
+ with open(wellspec_file, mode = mode) as fp:
1645
+ for _ in range(preceeding_blank_lines):
1646
+ fp.write('\n')
2029
1647
 
2030
- if min_k0 is None:
2031
- min_k0 = 0
2032
- else:
2033
- assert min_k0 >= 0
2034
- if max_k0 is not None:
2035
- assert min_k0 <= max_k0
2036
- if k0_list is not None and len(k0_list) == 0:
2037
- log.warning('no layers included for blocked well dataframe: no rows will be included')
1648
+ self.__write_wellspec_file_units_metadata(write_nexus_units = write_nexus_units,
1649
+ fp = fp,
1650
+ length_uom = length_uom,
1651
+ length_uom_comment = length_uom_comment,
1652
+ extra_columns_list = extra_columns_list,
1653
+ well_name = well_name)
2038
1654
 
2039
- @staticmethod
2040
- def __verify_if_angles_xyz_and_length_to_be_added(column_list, pc_titles, doing_kh, do_well_inflow, length_mode):
2041
- # determine if angla, anglv, x, y, z and length data are to be added as properties to the dataframe
1655
+ BlockedWell.__write_wellspec_file_columns(df = df, fp = fp, col_width_dict = col_width_dict, sep = sep)
2042
1656
 
2043
- doing_angles = any([('ANGLA' in column_list and 'ANGLA' not in pc_titles),
2044
- ('ANGLV' in column_list and 'ANGLV' not in pc_titles), (doing_kh), (do_well_inflow)])
2045
- doing_xyz = any([('X' in column_list and 'X' not in pc_titles), ('Y' in column_list and 'Y' not in pc_titles),
2046
- ('DEPTH' in column_list and 'DEPTH' not in pc_titles)])
2047
- doing_entry_exit = any([(doing_angles),
2048
- ('LENGTH' in column_list and 'LENGTH' not in pc_titles and length_mode == 'straight')])
1657
+ fp.write('\n')
2049
1658
 
2050
- # doing_angles = (('ANGLA' in column_list and 'ANGLA' not in pc_titles) or
2051
- # ('ANGLV' in column_list and 'ANGLV' not in pc_titles) or doing_kh or do_well_inflow)
2052
- # doing_xyz = (('X' in column_list and 'X' not in pc_titles) or (
2053
- # 'Y' in column_list and 'Y' not in pc_titles) or
2054
- # ('DEPTH' in column_list and 'DEPTH' not in pc_titles))
2055
- # doing_entry_exit = doing_angles or ('LENGTH' in column_list and 'LENGTH' not in pc_titles and
2056
- # length_mode == 'straight')
1659
+ BlockedWell.__write_wellspec_file_rows_from_dataframe(df = df,
1660
+ fp = fp,
1661
+ col_width_dict = col_width_dict,
1662
+ sep = sep)
1663
+ for _ in range(trailing_blank_lines):
1664
+ fp.write('\n')
2057
1665
 
2058
- return doing_angles, doing_xyz, doing_entry_exit
1666
+ return df
2059
1667
 
2060
- def __verify_number_of_grids_and_crs_units(self, column_list):
2061
- # verify that a GRID column is included in the dataframe if the well intersects more than one grid
2062
- # verify that each grid's crs units are consistent in all directions
1668
+ def kji0_marker(self, active_only = True):
1669
+ """Convenience method returning (k0, j0, i0), grid_uuid of first blocked interval."""
2063
1670
 
2064
- if 'GRID' not in column_list and self.number_of_grids() > 1:
2065
- log.error('creating blocked well dataframe without GRID column for well that intersects more than one grid')
2066
- grid_crs_list = []
2067
- for grid in self.grid_list:
2068
- grid_crs = crs.Crs(self.model, uuid = grid.crs_uuid)
2069
- grid_crs_list.append(grid_crs)
2070
- return grid_crs_list
1671
+ cells, grids = self.cell_indices_and_grid_list()
1672
+ if cells is None or grids is None or len(grids) == 0:
1673
+ return None, None, None, None
1674
+ return cells[0], grids[0].uuid
2071
1675
 
2072
- def __get_trajectory_crs_and_z_inc_down(self):
1676
+ def xyz_marker(self, active_only = True):
1677
+ """Convenience method returning (x, y, z), crs_uuid of perforation in first blocked interval.
2073
1678
 
2074
- if self.trajectory is None or self.trajectory.crs_uuid is None:
2075
- traj_crs = None
2076
- traj_z_inc_down = None
2077
- else:
2078
- traj_crs = crs.Crs(self.trajectory.model, uuid = self.trajectory.crs_uuid)
2079
- traj_z_inc_down = traj_crs.z_inc_down
1679
+ notes:
1680
+ active_only argument not yet in use;
1681
+ returns None, None if no blocked interval found
1682
+ """
2080
1683
 
2081
- return traj_crs, traj_z_inc_down
1684
+ cells, grids = self.cell_indices_and_grid_list()
1685
+ if cells is None or grids is None or len(grids) == 0:
1686
+ return None, None
1687
+ node_index = 0
1688
+ while node_index < self.node_count - 1 and self.grid_indices[node_index] == -1:
1689
+ node_index += 1
1690
+ if node_index >= self.node_count - 1:
1691
+ return None, None
1692
+ md = 0.5 * (self.node_mds[node_index] + self.node_mds[node_index + 1])
1693
+ xyz = self.trajectory.xyz_for_md(md)
1694
+ return xyz, self.trajectory.crs_uuid
2082
1695
 
2083
- @staticmethod
2084
- def __check_cell_depth(max_depth, grid, cell_kji0, grid_crs):
2085
- """Check whether the maximum depth specified has been exceeded with the current interval."""
1696
+ def create_feature_and_interpretation(self, shared_interpretation = True):
1697
+ """Instantiate new empty WellboreFeature and WellboreInterpretation objects.
2086
1698
 
2087
- max_depth_exceeded = False
2088
- if max_depth is not None:
2089
- cell_depth = grid.centre_point(cell_kji0)[2]
2090
- if not grid_crs.z_inc_down:
2091
- cell_depth = -cell_depth
2092
- if cell_depth > max_depth:
2093
- max_depth_exceeded = True
2094
- return max_depth_exceeded
2095
-
2096
- @staticmethod
2097
- def __skip_interval_check(max_depth, grid, cell_kji0, grid_crs, active_only, tuple_kji0, min_k0, max_k0, k0_list,
2098
- region_list, region_uuid, max_satw, satw_uuid, min_sato, sato_uuid, max_satg, satg_uuid):
2099
- """Check whether any conditions are met that mean the interval should be skipped."""
1699
+ note:
1700
+ uses the Blocked well citation title or other related object title as the well name
1701
+ """
1702
+ title = self.well_name
1703
+ if not title:
1704
+ title = self.title
1705
+ if not title and self.trajectory is not None:
1706
+ title = rqw.well_name(self.trajectory)
1707
+ if not title:
1708
+ title = 'WELL'
1709
+ if self.trajectory is not None:
1710
+ traj_interp_uuid = self.model.uuid(obj_type = 'WellboreInterpretation', related_uuid = self.trajectory.uuid)
1711
+ if traj_interp_uuid is not None:
1712
+ if shared_interpretation:
1713
+ self.wellbore_interpretation = rqo.WellboreInterpretation(parent_model = self.model,
1714
+ uuid = traj_interp_uuid)
1715
+ traj_feature_uuid = self.model.uuid(obj_type = 'WellboreFeature', related_uuid = traj_interp_uuid)
1716
+ if traj_feature_uuid is not None:
1717
+ self.wellbore_feature = rqo.WellboreFeature(parent_model = self.model, uuid = traj_feature_uuid)
1718
+ if self.wellbore_feature is None:
1719
+ self.wellbore_feature = rqo.WellboreFeature(parent_model = self.model, feature_name = title)
1720
+ self.feature_to_be_written = True
1721
+ if self.wellbore_interpretation is None:
1722
+ title = title if not self.wellbore_feature.title else self.wellbore_feature.title
1723
+ self.wellbore_interpretation = rqo.WellboreInterpretation(parent_model = self.model,
1724
+ title = title,
1725
+ wellbore_feature = self.wellbore_feature)
1726
+ if self.trajectory.wellbore_interpretation is None and shared_interpretation:
1727
+ self.trajectory.wellbore_interpretation = self.wellbore_interpretation
1728
+ self.interpretation_to_be_written = True
2100
1729
 
2101
- max_depth_exceeded = BlockedWell.__check_cell_depth(max_depth = max_depth,
2102
- grid = grid,
2103
- cell_kji0 = cell_kji0,
2104
- grid_crs = grid_crs)
2105
- inactive_grid = active_only and grid.inactive is not None and grid.inactive[tuple_kji0]
2106
- out_of_bounds_layer_1 = (min_k0 is not None and cell_kji0[0] < min_k0) or (max_k0 is not None and
2107
- cell_kji0[0] > max_k0)
2108
- out_of_bounds_layer_2 = k0_list is not None and cell_kji0[0] not in k0_list
2109
- out_of_bounds_region = (region_list is not None and
2110
- BlockedWell.__prop_array(region_uuid, grid)[tuple_kji0] not in region_list)
2111
- saturation_limit_exceeded_1 = (max_satw is not None and
2112
- BlockedWell.__prop_array(satw_uuid, grid)[tuple_kji0] > max_satw)
2113
- saturation_limit_exceeded_2 = (min_sato is not None and
2114
- BlockedWell.__prop_array(sato_uuid, grid)[tuple_kji0] < min_sato)
2115
- saturation_limit_exceeded_3 = (max_satg is not None and
2116
- BlockedWell.__prop_array(satg_uuid, grid)[tuple_kji0] > max_satg)
2117
- skip_interval = any([
2118
- max_depth_exceeded, inactive_grid, out_of_bounds_layer_1, out_of_bounds_layer_2, out_of_bounds_region,
2119
- saturation_limit_exceeded_1, saturation_limit_exceeded_2, saturation_limit_exceeded_3
2120
- ])
1730
+ def create_md_datum_and_trajectory(self,
1731
+ grid,
1732
+ trajectory_mds,
1733
+ trajectory_points,
1734
+ length_uom,
1735
+ well_name,
1736
+ set_depth_zero = False,
1737
+ set_tangent_vectors = False,
1738
+ create_feature_and_interp = True):
1739
+ """Creates an Md Datum object and a (simulation) Trajectory object for this blocked well.
2121
1740
 
2122
- return skip_interval
1741
+ note:
1742
+ not usually called directly; used by import methods
1743
+ """
2123
1744
 
2124
- def __get_part_perf_fraction_for_interval(self, pc, pc_titles, ci, perforation_list, interval, length_tol = 0.01):
2125
- """Get the partial perforation fraction for the interval."""
1745
+ if not well_name:
1746
+ well_name = self.title
2126
1747
 
2127
- skip_interval = False
2128
- if 'PPERF' in pc_titles:
2129
- part_perf_fraction = pc.single_array_ref(citation_title = 'PPERF')[ci]
2130
- else:
2131
- part_perf_fraction = 1.0
2132
- if perforation_list is not None:
2133
- perf_length = 0.0
2134
- for perf_start, perf_end in perforation_list:
2135
- if perf_end <= self.node_mds[interval] or perf_start >= self.node_mds[interval + 1]:
2136
- continue
2137
- if perf_start <= self.node_mds[interval]:
2138
- if perf_end >= self.node_mds[interval + 1]:
2139
- perf_length += self.node_mds[interval + 1] - self.node_mds[interval]
2140
- break
2141
- else:
2142
- perf_length += perf_end - self.node_mds[interval]
2143
- else:
2144
- if perf_end >= self.node_mds[interval + 1]:
2145
- perf_length += self.node_mds[interval + 1] - perf_start
2146
- else:
2147
- perf_length += perf_end - perf_start
2148
- if perf_length < length_tol:
2149
- skip_interval = True
2150
- perf_length = 0.0
2151
- part_perf_fraction = min(1.0, perf_length / (self.node_mds[interval + 1] - self.node_mds[interval]))
1748
+ # create md datum node for synthetic trajectory, using crs for grid
1749
+ datum_location = trajectory_points[0].copy()
1750
+ if set_depth_zero:
1751
+ datum_location[2] = 0.0
1752
+ datum = rqw.MdDatum(self.model,
1753
+ crs_uuid = grid.crs_uuid,
1754
+ location = datum_location,
1755
+ md_reference = 'mean sea level')
2152
1756
 
2153
- return skip_interval, part_perf_fraction
1757
+ # create synthetic trajectory object, using crs for grid
1758
+ trajectory_mds_array = np.array(trajectory_mds)
1759
+ trajectory_xyz_array = np.array(trajectory_points)
1760
+ trajectory_df = pd.DataFrame({
1761
+ 'MD': trajectory_mds_array,
1762
+ 'X': trajectory_xyz_array[..., 0],
1763
+ 'Y': trajectory_xyz_array[..., 1],
1764
+ 'Z': trajectory_xyz_array[..., 2]
1765
+ })
1766
+ self.trajectory = rqw.Trajectory(self.model,
1767
+ md_datum = datum,
1768
+ data_frame = trajectory_df,
1769
+ length_uom = length_uom,
1770
+ well_name = well_name,
1771
+ set_tangent_vectors = set_tangent_vectors)
1772
+ self.trajectory_to_be_written = True
2154
1773
 
2155
- def __get_entry_exit_xyz_and_crs_for_interval(self, doing_entry_exit, use_face_centres, grid, cell_kji0, interval,
2156
- ci, grid_crs, traj_crs):
2157
- # calculate the entry and exit points for the interval and set the entry and exit coordinate reference system
1774
+ if create_feature_and_interp:
1775
+ self.create_feature_and_interpretation()
2158
1776
 
2159
- entry_xyz = None
2160
- exit_xyz = None
2161
- ee_crs = None
2162
- if doing_entry_exit:
2163
- assert self.trajectory is not None
2164
- if use_face_centres:
2165
- entry_xyz = grid.face_centre(cell_kji0, self.face_pair_indices[ci, 0, 0], self.face_pair_indices[ci, 0,
2166
- 1])
2167
- if self.face_pair_indices[ci, 1, 0] >= 0:
2168
- exit_xyz = grid.face_centre(cell_kji0, self.face_pair_indices[ci, 1, 0],
2169
- self.face_pair_indices[ci, 1, 1])
2170
- else:
2171
- exit_xyz = grid.face_centre(cell_kji0, self.face_pair_indices[ci, 0, 0],
2172
- 1 - self.face_pair_indices[ci, 0, 1])
2173
- ee_crs = grid_crs
2174
- else:
2175
- entry_xyz = self.trajectory.xyz_for_md(self.node_mds[interval])
2176
- exit_xyz = self.trajectory.xyz_for_md(self.node_mds[interval + 1])
2177
- ee_crs = traj_crs
1777
+ def create_xml(self,
1778
+ ext_uuid = None,
1779
+ create_for_trajectory_if_needed = True,
1780
+ add_as_part = True,
1781
+ add_relationships = True,
1782
+ title = None,
1783
+ originator = None):
1784
+ """Create a blocked wellbore representation node from this BlockedWell object, optionally add as part.
2178
1785
 
2179
- return entry_xyz, exit_xyz, ee_crs
1786
+ note:
1787
+ trajectory xml node must be in place before calling this function;
1788
+ witsml log reference, interval stratigraphic units, and cell fluid phase units not yet supported
2180
1789
 
2181
- def __get_length_of_interval(self, length_mode, interval, length_uom, entry_xyz, exit_xyz, ee_crs, perforation_list,
2182
- part_perf_fraction, min_length):
2183
- """Calculate the length of the interval."""
1790
+ :meta common:
1791
+ """
2184
1792
 
2185
- skip_interval = False
2186
- if length_mode == 'MD':
2187
- length = self.node_mds[interval + 1] - self.node_mds[interval]
2188
- if length_uom is not None and self.trajectory is not None and length_uom != self.trajectory.md_uom:
2189
- length = wam.convert_lengths(length, self.trajectory.md_uom, length_uom)
2190
- else: # use straight line length between entry and exit
2191
- entry_xyz, exit_xyz = BlockedWell._single_uom_entry_exit_xyz(entry_xyz, exit_xyz, ee_crs)
2192
- length = vec.naive_length(exit_xyz - entry_xyz)
2193
- if length_uom is not None:
2194
- length = wam.convert_lengths(length, ee_crs.z_units, length_uom)
2195
- elif self.trajectory is not None:
2196
- length = wam.convert_lengths(length, ee_crs.z_units, self.trajectory.md_uom)
2197
- if perforation_list is not None:
2198
- length *= part_perf_fraction
2199
- if min_length is not None and length < min_length:
2200
- skip_interval = True
1793
+ assert self.trajectory is not None, 'trajectory object missing'
2201
1794
 
2202
- return skip_interval, length
1795
+ if ext_uuid is None:
1796
+ ext_uuid = self.model.h5_uuid()
2203
1797
 
2204
- @staticmethod
2205
- def _single_uom_xyz(xyz, crs, required_uom):
2206
- if xyz is None:
2207
- return None
2208
- xyz = np.array(xyz, dtype = float)
2209
- if crs.xy_units != required_uom:
2210
- xyz[0] = wam.convert_lengths(xyz[0], crs.xy_units, required_uom)
2211
- xyz[1] = wam.convert_lengths(xyz[1], crs.xy_units, required_uom)
2212
- if crs.z_units != required_uom:
2213
- xyz[2] = wam.convert_lengths(xyz[2], crs.z_units, required_uom)
2214
- return xyz
1798
+ if title:
1799
+ self.title = title
1800
+ if not self.title:
1801
+ self.title = self.well_name
1802
+ title = self.title
2215
1803
 
2216
- @staticmethod
2217
- def _single_uom_entry_exit_xyz(entry_xyz, exit_xyz, ee_crs):
2218
- return (BlockedWell._single_uom_xyz(entry_xyz, ee_crs, ee_crs.z_units),
2219
- BlockedWell._single_uom_xyz(exit_xyz, ee_crs, ee_crs.z_units))
1804
+ self.__create_wellbore_feature_and_interpretation_xml_if_needed(add_as_part = add_as_part,
1805
+ add_relationships = add_relationships,
1806
+ originator = originator)
2220
1807
 
2221
- def __get_angles_for_interval(self, pc, pc_titles, doing_angles, set_k_face_intervals_vertical, ci, k_face_check,
2222
- k_face_check_end, entry_xyz, exit_xyz, ee_crs, traj_z_inc_down, grid, grid_crs,
2223
- cell_kji0, anglv_ref, angla_plane_ref):
2224
- """Calculate angla, anglv and related trigonometirc transforms for the interval."""
1808
+ self.__create_trajectory_xml_if_needed(create_for_trajectory_if_needed = create_for_trajectory_if_needed,
1809
+ add_as_part = add_as_part,
1810
+ add_relationships = add_relationships,
1811
+ originator = originator,
1812
+ ext_uuid = ext_uuid,
1813
+ title = title)
2225
1814
 
2226
- sine_anglv = sine_angla = 0.0
2227
- cosine_anglv = cosine_angla = 1.0
2228
- anglv = pc.single_array_ref(citation_title = 'ANGLV')[ci] if 'ANGLV' in pc_titles else None
2229
- angla = pc.single_array_ref(citation_title = 'ANGLA')[ci] if 'ANGLA' in pc_titles else None
1815
+ assert self.trajectory.root is not None, 'trajectory xml not established'
2230
1816
 
2231
- if doing_angles and not (set_k_face_intervals_vertical and
2232
- (np.all(self.face_pair_indices[ci] == k_face_check) or
2233
- np.all(self.face_pair_indices[ci] == k_face_check_end))):
2234
- anglv, sine_anglv, cosine_anglv, vector, a_ref_vector = BlockedWell.__get_anglv_for_interval(
2235
- anglv = anglv,
2236
- entry_xyz = entry_xyz,
2237
- exit_xyz = exit_xyz,
2238
- ee_crs = ee_crs,
2239
- traj_z_inc_down = traj_z_inc_down,
2240
- grid = grid,
2241
- grid_crs = grid_crs,
2242
- cell_kji0 = cell_kji0,
2243
- anglv_ref = anglv_ref,
2244
- angla_plane_ref = angla_plane_ref)
2245
- if anglv != 0.0:
2246
- angla, sine_angla, cosine_angla = BlockedWell.__get_angla_for_interval(angla = angla,
2247
- grid = grid,
2248
- cell_kji0 = cell_kji0,
2249
- vector = vector,
2250
- a_ref_vector = a_ref_vector)
2251
- if angla is None:
2252
- angla = 0.0
2253
- if anglv is None:
2254
- anglv = 0.0
1817
+ bw_node = super().create_xml(title = title, originator = originator, add_as_part = False)
2255
1818
 
2256
- return anglv, sine_anglv, cosine_anglv, angla, sine_angla, cosine_angla
1819
+ # wellbore frame elements
2257
1820
 
2258
- @staticmethod
2259
- def __get_angla_for_interval(angla, grid, cell_kji0, vector, a_ref_vector):
2260
- """Calculate angla and related trigonometric transforms for the interval."""
1821
+ nc_node, mds_node, mds_values_node, cc_node, cis_node, cnull_node, cis_values_node, gis_node, gnull_node, \
1822
+ gis_values_node, fis_node, fnull_node, fis_values_node = \
1823
+ self.__create_bw_node_sub_elements(bw_node = bw_node)
2261
1824
 
2262
- if vector is None:
2263
- return None, None, None
1825
+ self.__create_hdf5_dataset_references(ext_uuid = ext_uuid,
1826
+ mds_values_node = mds_values_node,
1827
+ cis_values_node = cis_values_node,
1828
+ gis_values_node = gis_values_node,
1829
+ fis_values_node = fis_values_node)
2264
1830
 
2265
- # project well vector and i-axis vector onto plane defined by normal vector a_ref_vector
2266
- i_axis = grid.interface_vector(cell_kji0, 2)
2267
- if grid.crs.xy_units != grid.crs.z_units:
2268
- i_axis[2] = wam.convert_lengths(i_axis[2], grid.crs.z_units, grid.crs.xy_units)
2269
- i_axis = vec.unit_vector(i_axis)
2270
- if a_ref_vector is not None: # project vector and i axis onto a plane
2271
- vector -= vec.dot_product(vector, a_ref_vector) * a_ref_vector
2272
- vector = vec.unit_vector(vector)
2273
- # log.debug('i axis unit vector: ' + str(i_axis))
2274
- i_axis -= vec.dot_product(i_axis, a_ref_vector) * a_ref_vector
2275
- i_axis = vec.unit_vector(i_axis)
2276
- # log.debug('i axis unit vector in reference plane: ' + str(i_axis))
2277
- if angla is not None:
2278
- angla_rad = vec.radians_from_degrees(angla)
2279
- cosine_angla = maths.cos(angla_rad)
2280
- sine_angla = maths.sin(angla_rad)
2281
- else:
2282
- cosine_angla = min(max(vec.dot_product(vector, i_axis), -1.0), 1.0)
2283
- angla_rad = maths.acos(cosine_angla)
2284
- # negate angla if vector is 'clockwise from' i_axis when viewed from above, projected in the xy plane
2285
- # todo: have discussion around angla sign under different ijk handedness (and z inc direction?)
2286
- sine_angla = maths.sin(angla_rad)
2287
- angla = vec.degrees_from_radians(angla_rad)
2288
- if vec.clockwise((0.0, 0.0), i_axis, vector) > 0.0:
2289
- angla = -angla
2290
- angla_rad = -angla_rad ## as angle_rad before --> typo?
2291
- sine_angla = -sine_angla
1831
+ traj_root, grid_root, interp_root = self.__create_trajectory_grid_wellbore_interpretation_reference_nodes(
1832
+ bw_node = bw_node)
2292
1833
 
2293
- # log.debug('angla: ' + str(angla))
1834
+ self.__add_as_part_and_add_relationships_if_required(add_as_part = add_as_part,
1835
+ add_relationships = add_relationships,
1836
+ bw_node = bw_node,
1837
+ interp_root = interp_root,
1838
+ ext_uuid = ext_uuid)
2294
1839
 
2295
- return angla, sine_angla, cosine_angla
1840
+ return bw_node
2296
1841
 
2297
- @staticmethod
2298
- def __get_anglv_for_interval(anglv, entry_xyz, exit_xyz, ee_crs, traj_z_inc_down, grid, grid_crs, cell_kji0,
2299
- anglv_ref, angla_plane_ref):
2300
- """Get anglv and related trigonometric transforms for the interval."""
1842
+ def write_hdf5(self, file_name = None, mode = 'a', create_for_trajectory_if_needed = True):
1843
+ """Create or append to an hdf5 file, writing datasets for the measured depths, grid, cell & face indices.
2301
1844
 
2302
- if entry_xyz is None or exit_xyz is None:
2303
- return None, None, None, None, None
1845
+ :meta common:
1846
+ """
2304
1847
 
2305
- entry_xyz, exit_xyz = BlockedWell._single_uom_entry_exit_xyz(entry_xyz, exit_xyz, ee_crs)
2306
- vector = vec.unit_vector(np.array(exit_xyz) - np.array(entry_xyz)) # nominal wellbore vector for interval
2307
- if traj_z_inc_down is not None and traj_z_inc_down != grid_crs.z_inc_down:
2308
- vector[2] = -vector[2]
2309
- if grid.crs.xy_units == grid.crs.z_units:
2310
- unit_adjusted_vector = vector
2311
- else:
2312
- unit_adjusted_vector = vector.copy()
2313
- unit_adjusted_vector[2] = wam.convert_lengths(unit_adjusted_vector[2], grid.crs.z_units, grid.crs.xy_units)
2314
- v_ref_vector = BlockedWell.__get_ref_vector(grid, grid_crs, cell_kji0, anglv_ref)
2315
- # log.debug('v ref vector: ' + str(v_ref_vector))
2316
- if angla_plane_ref == anglv_ref:
2317
- a_ref_vector = v_ref_vector
2318
- else:
2319
- a_ref_vector = BlockedWell.__get_ref_vector(grid, grid_crs, cell_kji0, angla_plane_ref)
2320
- # log.debug('a ref vector: ' + str(a_ref_vector))
2321
- if anglv is not None:
2322
- anglv_rad = vec.radians_from_degrees(anglv)
2323
- cosine_anglv = maths.cos(anglv_rad)
2324
- sine_anglv = maths.sin(anglv_rad)
2325
- else:
2326
- cosine_anglv = min(max(vec.dot_product(unit_adjusted_vector, v_ref_vector), -1.0), 1.0)
2327
- anglv_rad = maths.acos(cosine_anglv)
2328
- sine_anglv = maths.sin(anglv_rad)
2329
- anglv = vec.degrees_from_radians(anglv_rad)
2330
- # log.debug('anglv: ' + str(anglv))
1848
+ # NB: array data must all have been set up prior to calling this function
2331
1849
 
2332
- return anglv, sine_anglv, cosine_anglv, vector, a_ref_vector
1850
+ if self.uuid is None:
1851
+ self.uuid = bu.new_uuid()
2333
1852
 
2334
- @staticmethod
2335
- def __get_ntg_and_directional_perm_for_interval(doing_kh, do_well_inflow, ntg_uuid, grid, tuple_kji0,
2336
- isotropic_perm, preferential_perforation, part_perf_fraction,
2337
- perm_i_uuid, perm_j_uuid, perm_k_uuid):
2338
- """Get the net-to-gross and directional permeability arrays for the interval."""
1853
+ h5_reg = rwh5.H5Register(self.model)
2339
1854
 
2340
- ntg_is_one = False
2341
- k_i = k_j = k_k = None
2342
- if doing_kh or do_well_inflow:
2343
- if ntg_uuid is None:
2344
- ntg = 1.0
2345
- ntg_is_one = True
2346
- else:
2347
- ntg = BlockedWell.__prop_array(ntg_uuid, grid)[tuple_kji0]
2348
- ntg_is_one = maths.isclose(ntg, 1.0, rel_tol = 0.001)
2349
- if isotropic_perm and ntg_is_one:
2350
- k_i = k_j = k_k = BlockedWell.__prop_array(perm_i_uuid, grid)[tuple_kji0]
2351
- else:
2352
- if preferential_perforation and not ntg_is_one:
2353
- if part_perf_fraction <= ntg:
2354
- ntg = 1.0 # effective ntg when perforated intervals are in pay
2355
- else:
2356
- ntg /= part_perf_fraction # adjusted ntg when some perforations in non-pay
2357
- # todo: check netgross facet type in property perm i & j parts: if set to gross then don't multiply by ntg below
2358
- k_i = BlockedWell.__prop_array(perm_i_uuid, grid)[tuple_kji0] * ntg
2359
- k_j = BlockedWell.__prop_array(perm_j_uuid, grid)[tuple_kji0] * ntg
2360
- k_k = BlockedWell.__prop_array(perm_k_uuid, grid)[tuple_kji0]
1855
+ if create_for_trajectory_if_needed and self.trajectory_to_be_written:
1856
+ self.trajectory.write_hdf5(file_name, mode = mode)
1857
+ mode = 'a'
2361
1858
 
2362
- return ntg_is_one, k_i, k_j, k_k
1859
+ h5_reg.register_dataset(self.uuid, 'NodeMd', self.node_mds)
1860
+ h5_reg.register_dataset(self.uuid, 'CellIndices', self.cell_indices) # could use int32?
1861
+ h5_reg.register_dataset(self.uuid, 'GridIndices', self.grid_indices) # could use int32?
1862
+ # convert face index pairs from [axis, polarity] back to strange local face numbering
1863
+ mask = (self.face_pair_indices.flatten() == -1).reshape((-1, 2)) # 2nd axis is (axis, polarity)
1864
+ masked_face_indices = np.where(mask, 0, self.face_pair_indices.reshape((-1, 2))) # 2nd axis is (axis, polarity)
1865
+ # using flat array for raw_face_indices array
1866
+ # other resqml writing code might use an array with one int per entry point and one per exit point, with 2nd axis as (entry, exit)
1867
+ raw_face_indices = np.where(mask[:, 0], -1, self.face_index_map[masked_face_indices[:, 0],
1868
+ masked_face_indices[:,
1869
+ 1]].flatten()).reshape(-1)
2363
1870
 
2364
- @staticmethod
2365
- def __get_kh_for_interval(doing_kh, isotropic_perm, ntg_is_one, length, perm_i_uuid, grid, tuple_kji0, k_i, k_j,
2366
- k_k, anglv, sine_anglv, cosine_anglv, sine_angla, cosine_angla, min_kh, pc, pc_titles,
2367
- ci):
2368
- """Get the permeability-thickness value for the interval."""
1871
+ h5_reg.register_dataset(self.uuid, 'LocalFacePairPerCellIndices', raw_face_indices) # could use uint8?
2369
1872
 
2370
- skip_interval = False
2371
- if doing_kh:
2372
- kh = BlockedWell.__get_kh_if_doing_kh(isotropic_perm = isotropic_perm,
2373
- ntg_is_one = ntg_is_one,
2374
- length = length,
2375
- perm_i_uuid = perm_i_uuid,
2376
- grid = grid,
2377
- tuple_kji0 = tuple_kji0,
2378
- k_i = k_i,
2379
- k_j = k_j,
2380
- k_k = k_k,
2381
- anglv = anglv,
2382
- sine_anglv = sine_anglv,
2383
- cosine_anglv = cosine_anglv,
2384
- sine_angla = sine_angla,
2385
- cosine_angla = cosine_angla)
2386
- if min_kh is not None and kh < min_kh:
2387
- skip_interval = True
2388
- elif 'KH' in pc_titles:
2389
- kh = pc.single_array_ref(citation_title = 'KH')[ci]
2390
- else:
2391
- kh = None
2392
- return skip_interval, kh
1873
+ h5_reg.write(file = file_name, mode = mode)
2393
1874
 
2394
- @staticmethod
2395
- def __get_kh_if_doing_kh(isotropic_perm, ntg_is_one, length, perm_i_uuid, grid, tuple_kji0, k_i, k_j, k_k, anglv,
2396
- sine_anglv, cosine_anglv, sine_angla, cosine_angla):
2397
- # note: this is believed to return required value even when grid crs has mixed xy & z units;
2398
- # angles are true angles accounting for any mixed units
2399
- if isotropic_perm and ntg_is_one:
2400
- kh = length * BlockedWell.__prop_array(perm_i_uuid, grid)[tuple_kji0]
2401
- else:
2402
- if np.isnan(k_i) or np.isnan(k_j):
2403
- kh = 0.0
2404
- elif anglv == 0.0:
2405
- kh = length * maths.sqrt(k_i * k_j)
2406
- elif np.isnan(k_k):
2407
- kh = 0.0
2408
- else:
2409
- k_e = maths.pow(k_i * k_j * k_k, 1.0 / 3.0)
2410
- if k_e == 0.0:
2411
- kh = 0.0
2412
- else:
2413
- l_i = length * maths.sqrt(k_e / k_i) * sine_anglv * cosine_angla
2414
- l_j = length * maths.sqrt(k_e / k_j) * sine_anglv * sine_angla
2415
- l_k = length * maths.sqrt(k_e / k_k) * cosine_anglv
2416
- l_p = maths.sqrt(l_i * l_i + l_j * l_j + l_k * l_k)
2417
- kh = k_e * l_p
2418
- return kh
1875
+ def add_grid_property_to_blocked_well(self, uuid_list):
1876
+ """Add properties to blocked wells from a list of uuids for properties on the supporting grid."""
2419
1877
 
2420
- @staticmethod
2421
- def __get_pc_arrays_for_interval(pc, pc_timeless, pc_titles, ci, length, radw, skin, length_uom, grid, traj_crs):
2422
- """Get the property collection arrays for the interval."""
1878
+ part_list = [self.model.part_for_uuid(uuid) for uuid in uuid_list]
2423
1879
 
2424
- def get_item(v, title, pc_titles, pc, pc_timeless, ci, uom):
1880
+ assert len(self.grid_list) == 1, "only blocked wells with a single grid can be handled currently"
1881
+ grid = self.grid_list[0]
1882
+ # filter to only those properties on the grid
1883
+ parts = self.model.parts_list_filtered_by_supporting_uuid(part_list, grid.uuid)
1884
+ if len(parts) < len(uuid_list):
1885
+ log.warning(
1886
+ f"{len(uuid_list)-len(parts)} uuids ignored as they do not belong to the same grid as the blocked well")
2425
1887
 
2426
- def pk_for_title(title):
2427
- d = {'RADW': 'wellbore radius', 'RADB': 'block equivalent radius', 'SKIN': 'skin'}
2428
- return d.get(title)
1888
+ gridpc = grid.extract_property_collection()
1889
+ # only 'cell' properties are handled
1890
+ cell_parts = [part for part in parts if gridpc.indexable_for_part(part) == 'cells']
1891
+ if len(cell_parts) < len(parts):
1892
+ log.warning(f"{len(parts)-len(cell_parts)} uuids ignored as they do not have indexable element of cells")
2429
1893
 
2430
- pc_uom = None
2431
- if title in pc_titles:
2432
- p = pc.singleton(citation_title = title)
2433
- v = pc.cached_part_array_ref(p)[ci]
2434
- pc_uom = pc.uom_for_part(p)
2435
- elif pc_timeless is not None:
2436
- p = pc_timeless.singleton(citation_title = title)
2437
- if p is None:
2438
- pk = pk_for_title(title)
2439
- if pk is not None:
2440
- p = pc_timeless.singleton(property_kind = pk)
2441
- if p is not None:
2442
- v = pc_timeless.cached_part_array_ref(p)[ci]
2443
- pc_uom = pc.uom_for_part(p)
2444
- if pc_uom is not None and uom is not None and pc_uom != uom:
2445
- v = wam.convert_lengths(v, pc_uom, uom)
2446
- return v
1894
+ if len(cell_parts) > 0:
1895
+ bwpc = rqp.PropertyCollection(support = self)
1896
+ if len(gridpc.string_lookup_uuid_list()) > 0:
1897
+ sl_dict = {}
1898
+ for part in cell_parts:
1899
+ if gridpc.string_lookup_uuid_for_part(part) in sl_dict.keys():
1900
+ sl_dict[gridpc.string_lookup_uuid_for_part(part)] = \
1901
+ sl_dict[gridpc.string_lookup_uuid_for_part(part)] + [part]
1902
+ else:
1903
+ sl_dict[gridpc.string_lookup_uuid_for_part(part)] = [part]
1904
+ else:
1905
+ sl_dict = {None: cell_parts}
2447
1906
 
2448
- if length_uom is None:
2449
- l_uom = traj_crs.z_units
2450
- r_uom = grid.crs.xy_units
2451
- else:
2452
- l_uom = length_uom
2453
- r_uom = length_uom
2454
- length = get_item(length, 'LENGTH', pc_titles, pc, pc_timeless, ci, l_uom)
2455
- radw = get_item(radw, 'RADW', pc_titles, pc, pc_timeless, ci, r_uom)
2456
- assert radw is None or radw > 0.0 # todo: allow zero for inactive intervals?
2457
- skin = get_item(skin, 'SKIN', pc_titles, pc, pc_timeless, ci, None)
2458
- if skin is None:
2459
- skin = get_item(None, 'skin', pc_titles, pc, pc_timeless, ci, None)
2460
- radb = get_item(None, 'RADB', pc_titles, pc, pc_timeless, ci, r_uom)
2461
- if radb is None:
2462
- radb = get_item(None, 'block equivalent radius', pc_titles, pc, pc_timeless, ci, r_uom)
2463
- wi = get_item(None, 'WI', pc_titles, pc, pc_timeless, ci, None)
2464
- wbc = get_item(None, 'WBC', pc_titles, pc, pc_timeless, ci, None)
1907
+ sl_ts_dict = {}
1908
+ for sl_uuid in sl_dict.keys():
1909
+ if len(gridpc.time_series_uuid_list()) > 0:
1910
+ # dictionary with keys for string_lookup uuids and None where missing
1911
+ # values for each key are a list of property parts associated with that lookup uuid, or None
1912
+ time_dict = {}
1913
+ for part in sl_dict[sl_uuid]:
1914
+ if gridpc.time_series_uuid_for_part(part) in time_dict.keys():
1915
+ time_dict[gridpc.time_series_uuid_for_part(part)] = \
1916
+ time_dict[gridpc.time_series_uuid_for_part(part)] + [part]
1917
+ else:
1918
+ time_dict[gridpc.time_series_uuid_for_part(part)] = [part]
1919
+ else:
1920
+ time_dict = {None: sl_dict[sl_uuid]}
1921
+ sl_ts_dict[sl_uuid] = time_dict
2465
1922
 
2466
- return length, radw, skin, radb, wi, wbc
2467
-
2468
- @staticmethod
2469
- def __get_well_inflow_parameters_for_interval(do_well_inflow, isotropic_perm, ntg_is_one, k_i, k_j, k_k, sine_anglv,
2470
- cosine_anglv, sine_angla, cosine_angla, grid, cell_kji0, radw, radb,
2471
- wi, wbc, skin, kh, length_uom, column_list):
2472
-
2473
- if do_well_inflow:
2474
- if not length_uom:
2475
- length_uom = grid.crs.z_units
2476
- k_ei, k_ej, k_ek, radw_e = BlockedWell.__calculate_ke_and_radw_e(isotropic_perm = isotropic_perm,
2477
- ntg_is_one = ntg_is_one,
2478
- radw = radw,
2479
- k_i = k_i,
2480
- k_j = k_j,
2481
- k_k = k_k,
2482
- sine_anglv = sine_anglv,
2483
- cosine_anglv = cosine_anglv,
2484
- sine_angla = sine_angla,
2485
- cosine_angla = cosine_angla)
1923
+ for sl_uuid in sl_ts_dict.keys():
1924
+ time_dict = sl_ts_dict[sl_uuid]
1925
+ for time_uuid in time_dict.keys():
1926
+ parts = time_dict[time_uuid]
1927
+ for part in parts:
1928
+ array = gridpc.cached_part_array_ref(part)
1929
+ indices = self.cell_indices_for_grid_uuid(grid.uuid)
1930
+ bwarray = np.empty(shape = (indices.shape[0],), dtype = array.dtype)
1931
+ for i, ind in enumerate(indices):
1932
+ bwarray[i] = array[tuple(ind)]
1933
+ bwpc.add_cached_array_to_imported_list(
1934
+ bwarray,
1935
+ source_info = f'property from grid {grid.title}',
1936
+ keyword = gridpc.citation_title_for_part(part),
1937
+ discrete = (not gridpc.continuous_for_part(part)),
1938
+ uom = gridpc.uom_for_part(part),
1939
+ time_index = gridpc.time_index_for_part(part),
1940
+ null_value = gridpc.null_value_for_part(part),
1941
+ property_kind = gridpc.property_kind_for_part(part),
1942
+ local_property_kind_uuid = gridpc.local_property_kind_uuid(part),
1943
+ facet_type = gridpc.facet_type_for_part(part),
1944
+ facet = gridpc.facet_for_part(part),
1945
+ realization = gridpc.realization_for_part(part),
1946
+ indexable_element = 'cells')
1947
+ bwpc.write_hdf5_for_imported_list(use_int32 = False)
1948
+ bwpc.create_xml_for_imported_list_and_add_parts_to_model(time_series_uuid = time_uuid,
1949
+ string_lookup_uuid = sl_uuid)
1950
+ else:
1951
+ log.debug(
1952
+ "no properties added - uuids either not 'cell' properties or blocked well is associated with multiple grids"
1953
+ )
2486
1954
 
2487
- cell_axial_vectors = grid.interface_vectors_kji(cell_kji0)
2488
- wam.convert_lengths(cell_axial_vectors[..., :2], grid.crs.xy_units, length_uom)
2489
- wam.convert_lengths(cell_axial_vectors[..., 2], grid.crs.z_units, length_uom)
2490
- d2 = np.empty(3)
2491
- for axis in range(3):
2492
- d2[axis] = np.sum(cell_axial_vectors[axis] * cell_axial_vectors[axis])
2493
- if radb is None:
2494
- radb_e = BlockedWell.__calculate_radb_e(k_ei = k_ei,
2495
- k_ej = k_ej,
2496
- k_ek = k_ek,
2497
- k_i = k_i,
2498
- k_j = k_j,
2499
- k_k = k_k,
2500
- d2 = d2,
2501
- sine_anglv = sine_anglv,
2502
- cosine_anglv = cosine_anglv,
2503
- sine_angla = sine_angla,
2504
- cosine_angla = cosine_angla)
2505
- radb = radw * radb_e / radw_e
2506
- log.debug(f'RADB value calculated in BlockedWell dataframe method as: {radb}')
2507
- if wi is None:
2508
- wi = 0.0 if radb <= 0.0 else 2.0 * maths.pi / (maths.log(radb / radw) + skin)
2509
- if 'WBC' in column_list and wbc is None:
2510
- assert length_uom == 'm' or length_uom.startswith('ft'), \
2511
- 'WBC only calculable for length uom of m or ft*'
2512
- conversion_constant = 8.5270171e-5 if length_uom == 'm' else 0.006328286
2513
- wbc = conversion_constant * kh * wi # note: pperf aleady accounted for in kh
1955
+ def _set_cell_interval_map(self):
1956
+ """Sets up an index mapping from blocked cell index to interval index, accounting for unblocked intervals."""
2514
1957
 
2515
- return radb, wi, wbc
1958
+ self.cell_interval_map = np.zeros(self.cell_count, dtype = np.int32)
1959
+ ci = 0
1960
+ for ii in range(self.node_count - 1):
1961
+ if self.grid_indices[ii] < 0:
1962
+ continue
1963
+ self.cell_interval_map[ci] = ii
1964
+ ci += 1
1965
+ assert ci == self.cell_count
2516
1966
 
2517
- @staticmethod
2518
- def __calculate_ke_and_radw_e(isotropic_perm, ntg_is_one, radw, k_i, k_j, k_k, sine_anglv, cosine_anglv, sine_angla,
2519
- cosine_angla):
1967
+ def __derive_from_wellspec_check_well_name(self, well_name):
1968
+ """Set the well name to be used in the wellspec file."""
2520
1969
 
2521
- if isotropic_perm and ntg_is_one:
2522
- k_ei = k_ej = k_ek = k_i
2523
- radw_e = radw
1970
+ if well_name:
1971
+ self.well_name = well_name
2524
1972
  else:
2525
- k_ei = maths.sqrt(k_j * k_k)
2526
- k_ej = maths.sqrt(k_i * k_k)
2527
- k_ek = maths.sqrt(k_i * k_j)
2528
- r_wi = 0.0 if k_ei == 0.0 else 0.5 * radw * (maths.sqrt(k_ei / k_j) + maths.sqrt(k_ei / k_k))
2529
- r_wj = 0.0 if k_ej == 0.0 else 0.5 * radw * (maths.sqrt(k_ej / k_i) + maths.sqrt(k_ej / k_k))
2530
- r_wk = 0.0 if k_ek == 0.0 else 0.5 * radw * (maths.sqrt(k_ek / k_i) + maths.sqrt(k_ek / k_j))
2531
- rwi = r_wi * sine_anglv * cosine_angla
2532
- rwj = r_wj * sine_anglv * sine_angla
2533
- rwk = r_wk * cosine_anglv
2534
- radw_e = maths.sqrt(rwi * rwi + rwj * rwj + rwk * rwk)
2535
- if radw_e == 0.0:
2536
- radw_e = radw # no permeability in this situation anyway
2537
-
2538
- return k_ei, k_ej, k_ek, radw_e
1973
+ well_name = self.well_name
1974
+ if not self.title:
1975
+ self.title = well_name
1976
+ return well_name
2539
1977
 
2540
1978
  @staticmethod
2541
- def __calculate_radb_e(k_ei, k_ej, k_ek, k_i, k_j, k_k, d2, sine_anglv, cosine_anglv, sine_angla, cosine_angla):
2542
-
2543
- r_bi = 0.0 if k_ei == 0.0 else 0.14 * maths.sqrt(k_ei * (d2[1] / k_j + d2[0] / k_k))
2544
- r_bj = 0.0 if k_ej == 0.0 else 0.14 * maths.sqrt(k_ej * (d2[2] / k_i + d2[0] / k_k))
2545
- r_bk = 0.0 if k_ek == 0.0 else 0.14 * maths.sqrt(k_ek * (d2[2] / k_i + d2[1] / k_j))
2546
- rbi = r_bi * sine_anglv * cosine_angla
2547
- rbj = r_bj * sine_anglv * sine_angla
2548
- rbk = r_bk * cosine_anglv
2549
- radb_e = maths.sqrt(rbi * rbi + rbj * rbj + rbk * rbk)
1979
+ def __cell_kji0_from_df(df, df_row):
1980
+ row = df.iloc[df_row]
1981
+ if pd.isna(row.iloc[0]) or pd.isna(row.iloc[1]) or pd.isna(row.iloc[2]):
1982
+ return None
1983
+ cell_kji0 = np.empty((3,), dtype = np.int32)
1984
+ cell_kji0[:] = row.iloc[2], row.iloc[1], row.iloc[0]
1985
+ cell_kji0[:] -= 1
1986
+ return cell_kji0
2550
1987
 
2551
- return radb_e
1988
+ @staticmethod
1989
+ def __verify_grid_name(grid_name_to_check, row, skipped_warning_grid, well_name):
1990
+ """Check whether the grid associated with a row of the dataframe matches the expected grid name."""
1991
+ skip_row = False
1992
+ if grid_name_to_check and pd.notna(row['GRID']) and grid_name_to_check != str(row['GRID']).upper():
1993
+ other_grid = str(row['GRID'])
1994
+ if skipped_warning_grid != other_grid:
1995
+ log.warning('skipping perforation(s) in grid ' + other_grid + ' for well ' + str(well_name))
1996
+ skipped_warning_grid = other_grid
1997
+ skip_row = True
1998
+ return skipped_warning_grid, skip_row
2552
1999
 
2553
- def __get_xyz_for_interval(self, doing_xyz, length_mode, length_uom, md, traj_crs, depth_inc_down, traj_z_inc_down,
2554
- entry_xyz, exit_xyz, ee_crs, pc, pc_titles, ci):
2555
- """Get the x, y and z location of the midpoint of the interval."""
2000
+ @staticmethod
2001
+ def __calculate_entry_and_exit_axes_polarities_and_points(angles_present, row, cp, well_name, df, i, cell_kji0,
2002
+ blocked_cells_kji0, use_face_centres, xy_units, z_units):
2003
+ if angles_present:
2004
+ entry_axis, entry_polarity, entry_xyz, exit_axis, exit_polarity, exit_xyz = \
2005
+ BlockedWell.__calculate_entry_and_exit_axes_polarities_and_points_using_angles(
2006
+ row = row, cp = cp, well_name = well_name, xy_units = xy_units, z_units = z_units)
2007
+ else:
2008
+ # fabricate entry and exit axes and polarities based on indices alone
2009
+ # note: could use geometry but here a cheap rough-and-ready approach is used
2010
+ log.debug('row ' + str(i) + ': using cell moves')
2011
+ entry_axis, entry_polarity, exit_axis, exit_polarity = BlockedWell.__calculate_entry_and_exit_axes_polarities_and_points_using_indices(
2012
+ df = df, i = i, cell_kji0 = cell_kji0, blocked_cells_kji0 = blocked_cells_kji0)
2556
2013
 
2557
- xyz = (np.NaN, np.NaN, np.NaN)
2558
- if doing_xyz:
2559
- xyz = self.__get_xyz_if_doing_xyz(length_mode = length_mode,
2560
- md = md,
2561
- length_uom = length_uom,
2562
- traj_crs = traj_crs,
2563
- depth_inc_down = depth_inc_down,
2564
- traj_z_inc_down = traj_z_inc_down,
2565
- entry_xyz = entry_xyz,
2566
- exit_xyz = exit_xyz,
2567
- ee_crs = ee_crs)
2568
- xyz = np.array(xyz)
2569
- for i, col_header in enumerate(['X', 'Y', 'DEPTH']):
2570
- if col_header in pc_titles:
2571
- xyz[i] = pc.single_array_ref(citation_title = col_header)[ci]
2014
+ entry_xyz, exit_xyz = BlockedWell.__override_vector_based_xyz_entry_and_exit_points_if_necessary(
2015
+ use_face_centres = use_face_centres,
2016
+ entry_axis = entry_axis,
2017
+ exit_axis = exit_axis,
2018
+ entry_polarity = entry_polarity,
2019
+ exit_polarity = exit_polarity,
2020
+ cp = cp)
2572
2021
 
2573
- return xyz
2022
+ return entry_axis, entry_polarity, entry_xyz, exit_axis, exit_polarity, exit_xyz
2574
2023
 
2575
- def __get_xyz_if_doing_xyz(self, length_mode, md, length_uom, traj_crs, depth_inc_down, traj_z_inc_down, exit_xyz,
2576
- entry_xyz, ee_crs):
2024
+ @staticmethod
2025
+ def __calculate_entry_and_exit_axes_polarities_and_points_using_angles(row, cp, well_name, xy_units, z_units):
2026
+ """Calculate entry and exit axes, polarities and points using azimuth and inclination angles."""
2577
2027
 
2578
- if length_mode == 'MD' and self.trajectory is not None:
2579
- xyz = self.trajectory.xyz_for_md(md)
2580
- if length_uom is not None and length_uom != self.trajectory.md_uom:
2581
- wam.convert_lengths(xyz, traj_crs.z_units, length_uom)
2582
- if depth_inc_down and traj_z_inc_down is False:
2583
- xyz[2] = -xyz[2]
2028
+ angla = row['ANGLA']
2029
+ inclination = row['ANGLV']
2030
+ if inclination < 0.001:
2031
+ azimuth = 0.0
2584
2032
  else:
2585
- xyz = 0.5 * (np.array(exit_xyz) + np.array(entry_xyz))
2586
- if length_uom is not None and length_uom != ee_crs.z_units:
2587
- xyz[2] = wam.convert_lengths(xyz[2], ee_crs.z_units, length_uom)
2588
- if depth_inc_down != ee_crs.z_inc_down:
2589
- xyz[2] = -xyz[2]
2590
-
2591
- return xyz
2033
+ i_vector = np.sum(cp[:, :, 1] - cp[:, :, 0], axis = (0, 1))
2034
+ azimuth = vec.azimuth(i_vector) - angla # see Nexus keyword reference doc
2035
+ well_vector = vec.unit_vector_from_azimuth_and_inclination(azimuth, inclination) * 10000.0
2036
+ if xy_units != z_units:
2037
+ well_vector[2] = wam.convert_lengths(well_vector[2], xy_units, z_units)
2038
+ well_vector = vec.unit_vector(well_vector) * 10000.0
2039
+ # todo: the following might be producing NaN's when vector passes precisely through an edge
2040
+ (entry_axis, entry_polarity, entry_xyz, exit_axis, exit_polarity, exit_xyz) = \
2041
+ rqwu.find_entry_and_exit(cp, -well_vector, well_vector, well_name)
2042
+ return entry_axis, entry_polarity, entry_xyz, exit_axis, exit_polarity, exit_xyz
2592
2043
 
2593
- def __get_md_array_in_correct_units_for_interval(self, md, length_uom, pc, pc_titles, ci):
2594
- """Convert the measured depth to the correct units or get the measured depth from the property collection."""
2044
+ def __calculate_entry_and_exit_axes_polarities_and_points_using_indices(df, i, cell_kji0, blocked_cells_kji0):
2595
2045
 
2596
- if 'MD' in pc_titles:
2597
- md = pc.single_array_ref(citation_title = 'MD')[ci]
2598
- elif length_uom is not None and self.trajectory is not None and length_uom != self.trajectory.md_uom:
2599
- md = wam.convert_lengths(md, self.trajectory.md_uom, length_uom)
2046
+ entry_axis, entry_polarity = BlockedWell.__fabricate_entry_axis_and_polarity_using_indices(
2047
+ i, cell_kji0, blocked_cells_kji0)
2048
+ exit_axis, exit_polarity = BlockedWell.__fabricate_exit_axis_and_polarity_using_indices(
2049
+ i, cell_kji0, entry_axis, entry_polarity, df)
2600
2050
 
2601
- return md
2051
+ return entry_axis, entry_polarity, exit_axis, exit_polarity
2602
2052
 
2603
2053
  @staticmethod
2604
- def __append_interval_data_to_dataframe(df, grid_name, radw, skin, angla, anglv, length, kh, xyz, md, stat,
2605
- part_perf_fraction, radb, wi, wbc, column_list, one_based, row_dict,
2606
- cell_kji0, row_ci_list, ci):
2607
- """Append the row of data corresponding to the interval to the dataframe."""
2608
-
2609
- column_names = [
2610
- 'GRID', 'RADW', 'SKIN', 'ANGLA', 'ANGLV', 'LENGTH', 'KH', 'DEPTH', 'MD', 'X', 'Y', 'STAT', 'PPERF', 'RADB',
2611
- 'WI', 'WBC'
2612
- ]
2613
- column_values = [
2614
- grid_name, radw, skin, angla, anglv, length, kh, xyz[2], md, xyz[0], xyz[1], stat, part_perf_fraction, radb,
2615
- wi, wbc
2616
- ]
2617
- column_values_dict = dict(zip(column_names, column_values))
2054
+ def __fabricate_entry_axis_and_polarity_using_indices(i, cell_kji0, blocked_cells_kji0):
2055
+ """Fabricate entry and exit axes and polarities based on indices alone.
2618
2056
 
2619
- data = df.to_dict()
2620
- data = {k: list(v.values()) for k, v in data.items()}
2621
- for col_index, col in enumerate(column_list):
2622
- if col_index < 3:
2623
- if one_based:
2624
- row_dict[col] = [cell_kji0[2 - col_index] + 1]
2625
- else:
2626
- row_dict[col] = [cell_kji0[2 - col_index]]
2627
- else:
2628
- row_dict[col] = [column_values_dict[col]]
2057
+ note:
2058
+ could use geometry but here a cheap rough-and-ready approach is used
2059
+ """
2629
2060
 
2630
- for col, vals in row_dict.items():
2631
- if col in data:
2632
- data[col].extend(vals)
2061
+ if i == 0:
2062
+ entry_axis, entry_polarity = 0, 0 # K-
2063
+ else:
2064
+ entry_move = cell_kji0 - blocked_cells_kji0[-1]
2065
+ log.debug(f'entry move: {entry_move}')
2066
+ if entry_move[1] == 0 and entry_move[2] == 0: # K move
2067
+ entry_axis = 0
2068
+ entry_polarity = 0 if entry_move[0] >= 0 else 1
2069
+ elif abs(entry_move[1]) > abs(entry_move[2]): # J dominant move
2070
+ entry_axis = 1
2071
+ entry_polarity = 0 if entry_move[1] >= 0 else 1
2072
+ else: # I dominant move
2073
+ entry_axis = 2
2074
+ entry_polarity = 0 if entry_move[2] >= 0 else 1
2075
+ return entry_axis, entry_polarity
2076
+
2077
+ @staticmethod
2078
+ def __fabricate_exit_axis_and_polarity_using_indices(i, cell_kji0, entry_axis, entry_polarity, df):
2079
+ if i == len(df) - 1:
2080
+ exit_axis, exit_polarity = entry_axis, 1 - entry_polarity
2081
+ else:
2082
+ next_cell_kji0 = BlockedWell.__cell_kji0_from_df(df, i + 1)
2083
+ if next_cell_kji0 is None:
2084
+ exit_axis, exit_polarity = entry_axis, 1 - entry_polarity
2633
2085
  else:
2634
- data[col] = vals
2635
- df = pd.DataFrame(data)
2086
+ exit_move = next_cell_kji0 - cell_kji0
2087
+ log.debug(f'exit move: {exit_move}')
2088
+ if exit_move[1] == 0 and exit_move[2] == 0: # K move
2089
+ exit_axis = 0
2090
+ exit_polarity = 1 if exit_move[0] >= 0 else 0
2091
+ elif abs(exit_move[1]) > abs(exit_move[2]): # J dominant move
2092
+ exit_axis = 1
2093
+ exit_polarity = 1 if exit_move[1] >= 0 else 0
2094
+ else: # I dominant move
2095
+ exit_axis = 2
2096
+ exit_polarity = 1 if exit_move[2] >= 0 else 0
2097
+ return exit_axis, exit_polarity
2636
2098
 
2637
- row_ci_list.append(ci)
2099
+ @staticmethod
2100
+ def __override_vector_based_xyz_entry_and_exit_points_if_necessary(use_face_centres, entry_axis, exit_axis,
2101
+ entry_polarity, exit_polarity, cp):
2102
+ """Override the vector based xyz entry and exit with face centres."""
2638
2103
 
2639
- return df
2104
+ if use_face_centres: # override the vector based xyz entry and exit points with face centres
2105
+ if entry_axis == 0:
2106
+ entry_xyz = np.mean(cp[entry_polarity, :, :], axis = (0, 1))
2107
+ elif entry_axis == 1:
2108
+ entry_xyz = np.mean(cp[:, entry_polarity, :], axis = (0, 1))
2109
+ else:
2110
+ entry_xyz = np.mean(cp[:, :, entry_polarity], axis = (0, 1)) # entry_axis == 2, ie. I
2111
+ if exit_axis == 0:
2112
+ exit_xyz = np.mean(cp[exit_polarity, :, :], axis = (0, 1))
2113
+ elif exit_axis == 1:
2114
+ exit_xyz = np.mean(cp[:, exit_polarity, :], axis = (0, 1))
2115
+ else:
2116
+ exit_xyz = np.mean(cp[:, :, exit_polarity], axis = (0, 1)) # exit_axis == 2, ie. I
2117
+ return entry_xyz, exit_xyz
2640
2118
 
2641
- def __add_as_properties(self,
2642
- df,
2643
- add_as_properties,
2644
- extra_columns_list,
2645
- length_uom,
2646
- time_index = None,
2647
- time_series_uuid = None):
2648
- """Adds property parts from df with columns listed in add_as_properties or extra_columns_list."""
2119
+ @staticmethod
2120
+ def __add_interval(previous_xyz, entry_axis, entry_polarity, entry_xyz, exit_axis, exit_polarity, exit_xyz,
2121
+ cell_kji0, trajectory_mds, trajectory_points, blocked_intervals, blocked_cells_kji0,
2122
+ blocked_face_pairs, xy_units, z_units, length_uom):
2123
+ if previous_xyz is None: # first entry
2124
+ log.debug('adding mean sea level trajectory start')
2125
+ previous_xyz = entry_xyz.copy()
2126
+ previous_xyz[2] = 0.0 # use depth zero as md datum
2127
+ trajectory_mds.append(0.0)
2128
+ trajectory_points.append(previous_xyz)
2129
+ if not vec.isclose(previous_xyz, entry_xyz, tolerance = 0.05): # add an unblocked interval
2130
+ log.debug('adding unblocked interval')
2131
+ trajectory_points.append(entry_xyz)
2132
+ new_md = trajectory_mds[-1] + BlockedWell._md_length(entry_xyz - previous_xyz, xy_units, z_units,
2133
+ length_uom)
2134
+ trajectory_mds.append(new_md)
2135
+ blocked_intervals.append(-1) # unblocked interval
2136
+ previous_xyz = entry_xyz
2137
+ log.debug('adding blocked interval for cell kji0: ' + str(cell_kji0))
2138
+ trajectory_points.append(exit_xyz)
2139
+ new_md = trajectory_mds[-1] + BlockedWell._md_length(exit_xyz - previous_xyz, xy_units, z_units, length_uom)
2140
+ trajectory_mds.append(new_md)
2141
+ blocked_intervals.append(0) # blocked interval
2142
+ previous_xyz = exit_xyz
2143
+ blocked_cells_kji0.append(cell_kji0)
2144
+ blocked_face_pairs.append(((entry_axis, entry_polarity), (exit_axis, exit_polarity)))
2649
2145
 
2650
- if add_as_properties:
2146
+ return previous_xyz, trajectory_mds, trajectory_points, blocked_intervals, blocked_cells_kji0, blocked_face_pairs
2147
+
2148
+ @staticmethod
2149
+ def _md_length(xyz_vector, xy_units, z_units, length_uom):
2150
+ if length_uom == xy_units and length_uom == z_units:
2151
+ return vec.naive_length(xyz_vector)
2152
+ x = wam.convert_lengths(xyz_vector[0], xy_units, length_uom)
2153
+ y = wam.convert_lengths(xyz_vector[1], xy_units, length_uom)
2154
+ z = wam.convert_lengths(xyz_vector[2], z_units, length_uom)
2155
+ return vec.naive_length((x, y, z))
2156
+
2157
+ @staticmethod
2158
+ def __add_tail_to_trajectory_if_necessary(blocked_count, exit_axis, exit_polarity, cell_kji0, grid,
2159
+ trajectory_points, trajectory_mds):
2160
+ """Add tail to trajcetory if last segment terminates at bottom face in bottom layer."""
2161
+
2162
+ if blocked_count > 0 and exit_axis == 0 and exit_polarity == 1 and cell_kji0[
2163
+ 0] == grid.nk - 1 and grid.k_direction_is_down:
2164
+ tail_length = 10.0 # metres or feet
2165
+ tail_xyz = trajectory_points[-1].copy()
2166
+ tail_xyz[2] += tail_length * (1.0 if grid.z_inc_down() else -1.0)
2167
+ trajectory_points.append(tail_xyz)
2168
+ new_md = trajectory_mds[-1] + tail_length
2169
+ trajectory_mds.append(new_md)
2170
+
2171
+ return trajectory_points, trajectory_mds
2172
+
2173
+ def __add_as_properties_if_specified(self,
2174
+ add_as_properties,
2175
+ df,
2176
+ length_uom,
2177
+ time_index = None,
2178
+ time_series_uuid = None):
2179
+ # if add_as_properties is True and present as a list of wellspec column names, both the blocked well and
2180
+ # the properties will have their hdf5 data written, xml created and be added as parts to the model
2181
+
2182
+ if add_as_properties and len(df.columns) > 3:
2183
+ # NB: atypical writing of hdf5 data and xml creation in order to support related properties
2184
+ self.write_hdf5()
2185
+ self.create_xml()
2651
2186
  if isinstance(add_as_properties, list):
2652
2187
  for col in add_as_properties:
2653
- assert col in extra_columns_list
2188
+ assert col in df.columns[3:] # could just skip missing columns
2654
2189
  property_columns = add_as_properties
2655
2190
  else:
2656
- property_columns = extra_columns_list
2191
+ property_columns = df.columns[3:]
2657
2192
  self.add_df_properties(df,
2658
2193
  property_columns,
2659
2194
  length_uom = length_uom,
2660
2195
  time_index = time_index,
2661
2196
  time_series_uuid = time_series_uuid)
2662
2197
 
2663
- def add_df_properties(self,
2664
- df,
2665
- columns,
2666
- length_uom = None,
2667
- time_index = None,
2668
- time_series_uuid = None,
2669
- realization = None):
2670
- """Creates a property part for each column in the dataframe, based on the dataframe values.
2198
+ def __set_grid(self, grid, wellspec_file, cellio_file, column_ji0):
2199
+ """Set the grid to which the blocked well belongs."""
2671
2200
 
2672
- arguments:
2673
- df (pd.DataFrame): dataframe containing the columns that will be converted to properties
2674
- columns (List[str]): list of the column names that will be converted to properties
2675
- length_uom (str, optional): the length unit of measure
2676
- time_index (int, optional): if adding a timestamp to the property, this is the timestamp
2677
- index of the TimeSeries timestamps attribute
2678
- time_series_uuid (uuid.UUID, optional): if adding a timestamp to the property, this is
2679
- the uuid of the TimeSeries object
2680
- realization (int, optional): if present, is used as the realization number for all the
2681
- properties
2201
+ if grid is None and (self.trajectory is not None or wellspec_file is not None or cellio_file is not None or
2202
+ column_ji0 is not None):
2203
+ grid_final = self.model.grid()
2204
+ else:
2205
+ grid_final = grid
2206
+ return grid_final
2682
2207
 
2683
- returns:
2684
- None
2208
+ def __check_cellio_init_okay(self, cellio_file, well_name, grid):
2209
+ """Checks if BlockedWell object initialization from a cellio file is okay."""
2685
2210
 
2686
- notes:
2687
- the column name is used as the property citation title;
2688
- the blocked well must already exist as a part in the model;
2689
- this method currently only handles single grid situations;
2690
- dataframe rows must be in the same order as the cells in the blocked well
2691
- """
2692
- # todo: enhance to handle multiple grids
2693
- assert len(self.grid_list) == 1
2694
- if columns is None or len(columns) == 0 or len(df) == 0:
2695
- return
2696
- if length_uom is None:
2697
- length_uom = self.trajectory.md_uom
2698
- extra_pc = rqp.PropertyCollection()
2699
- extra_pc.set_support(support = self)
2700
- assert len(df) == self.cell_count
2211
+ okay = self.import_from_rms_cellio(cellio_file, well_name, grid)
2212
+ if not okay:
2213
+ self.node_count = 0
2701
2214
 
2702
- for column in columns:
2703
- extra = column.upper()
2704
- uom, pk, discrete = self._get_uom_pk_discrete_for_df_properties(extra = extra, length_uom = length_uom)
2705
- if discrete:
2706
- null_value = -1
2707
- na_value = -1
2708
- dtype = np.int32
2709
- else:
2710
- null_value = None
2711
- na_value = np.NaN
2712
- dtype = float
2713
- # 'SKIN': use defaults for now; todo: create local property kind for skin
2714
- if column == 'STAT':
2715
- col_as_list = list(df[column])
2716
- expanded = np.array([(0 if (str(st).upper() in ['OFF', '0']) else 1) for st in col_as_list],
2717
- dtype = int)
2718
- else:
2719
- expanded = df[column].to_numpy(dtype = dtype, copy = True, na_value = na_value)
2720
- extra_pc.add_cached_array_to_imported_list(
2721
- expanded,
2722
- 'blocked well dataframe',
2723
- extra,
2724
- discrete = discrete,
2725
- uom = uom,
2726
- property_kind = pk,
2727
- local_property_kind_uuid = None,
2728
- facet_type = None,
2729
- facet = None,
2730
- realization = realization,
2731
- indexable_element = 'cells',
2732
- count = 1,
2733
- time_index = time_index,
2734
- null_value = null_value,
2735
- )
2736
- extra_pc.write_hdf5_for_imported_list()
2737
- extra_pc.create_xml_for_imported_list_and_add_parts_to_model(time_series_uuid = time_series_uuid,
2738
- find_local_property_kinds = True)
2215
+ def _load_from_xml(self):
2216
+ """Loads the blocked wellbore object from an xml node (and associated hdf5 data)."""
2739
2217
 
2740
- def _get_uom_pk_discrete_for_df_properties(self, extra, length_uom, temperature_uom = None):
2741
- """Set the property kind and unit of measure for all properties in the dataframe."""
2742
- # todo: this is horribly inefficient, building a whole dictionary for every call but only using one entry
2743
- if length_uom not in ['m', 'ft']:
2744
- raise ValueError(f"The length_uom {length_uom} must be either 'm' or 'ft'.")
2745
- if extra == 'TEMP' and (temperature_uom is None or
2746
- temperature_uom not in wam.valid_uoms('thermodynamic temperature')):
2747
- raise ValueError(f"The temperature_uom must be in {wam.valid_uoms('thermodynamic temperature')}.")
2218
+ node = self.root
2219
+ assert node is not None
2748
2220
 
2749
- length_uom_pk_discrete = self._get_uom_pk_discrete_for_length_based_properties(length_uom = length_uom,
2750
- extra = extra)
2751
- uom_pk_discrete_dict = {
2752
- 'ANGLA': ('dega', 'azimuth', False),
2753
- 'ANGLV': ('dega', 'inclination', False),
2754
- 'KH': (f'mD.{length_uom}', 'permeability length', False),
2755
- 'PPERF': (f'{length_uom}/{length_uom}', 'perforation fraction', False),
2756
- 'STAT': (None, 'well connection open', True),
2757
- 'LENGTH': length_uom_pk_discrete,
2758
- 'MD': (length_uom, 'measured depth', False),
2759
- 'X': length_uom_pk_discrete,
2760
- 'Y': length_uom_pk_discrete,
2761
- 'DEPTH': (length_uom, 'depth', False),
2762
- 'RADW': (length_uom, 'wellbore radius', False),
2763
- 'RADB': (length_uom, 'block equivalent radius', False),
2764
- 'RADBP': length_uom_pk_discrete,
2765
- 'RADWP': length_uom_pk_discrete,
2766
- 'FM': (f'{length_uom}/{length_uom}', 'matrix fraction', False),
2767
- 'IRELPM': (None, 'relative permeability index', True), # TODO: change to 'region initialization' with facet
2768
- 'SECT': (None, 'wellbore section index', True),
2769
- 'LAYER': (None, 'layer index', True),
2770
- 'ANGLE': ('dega', 'plane angle', False),
2771
- 'TEMP': (temperature_uom, 'thermodynamic temperature', False),
2772
- 'MDCON': length_uom_pk_discrete,
2773
- 'K': ('mD', 'rock permeability', False),
2774
- 'DZ': (length_uom, 'cell length', False), # TODO: add direction facet
2775
- 'DTOP': (length_uom, 'depth', False),
2776
- 'DBOT': (length_uom, 'depth', False),
2777
- 'SKIN': ('Euc', 'skin', False),
2778
- 'WI': ('Euc', 'well connection index', False),
2779
- }
2780
- return uom_pk_discrete_dict.get(extra, ('Euc', 'generic continuous', False))
2221
+ self.__find_trajectory_uuid(node = node)
2781
2222
 
2782
- def _get_uom_pk_discrete_for_length_based_properties(self, length_uom, extra):
2783
- if length_uom is None or length_uom == 'Euc':
2784
- if extra in ['LENGTH', 'MD', 'MDCON']:
2785
- uom = self.trajectory.md_uom
2786
- elif extra in ['X', 'Y', 'RADW', 'RADB', 'RADBP', 'RADWP']:
2787
- uom = self.grid_list[0].xy_units()
2788
- else:
2789
- uom = self.grid_list[0].z_units()
2223
+ self.node_count = rqet.find_tag_int(node, 'NodeCount')
2224
+ assert self.node_count is not None and self.node_count >= 2, 'problem with blocked well node count'
2225
+
2226
+ mds_node = rqet.find_tag(node, 'NodeMd')
2227
+ assert mds_node is not None, 'blocked well node measured depths hdf5 reference not found in xml'
2228
+ rqwu.load_hdf5_array(self, mds_node, 'node_mds')
2229
+
2230
+ # Statement below has no effect, is this a bug?
2231
+ self.node_mds is not None and self.node_mds.ndim == 1 and self.node_mds.size == self.node_count
2232
+
2233
+ self.cell_count = rqet.find_tag_int(node, 'CellCount')
2234
+ assert self.cell_count is not None and self.cell_count > 0
2235
+
2236
+ # todo: remove this if block once RMS export issue resolved
2237
+ if self.cell_count == self.node_count:
2238
+ extended_mds = np.empty((self.node_mds.size + 1,))
2239
+ extended_mds[:-1] = self.node_mds
2240
+ extended_mds[-1] = self.node_mds[-1] + 1.0
2241
+ self.node_mds = extended_mds
2242
+ self.node_count += 1
2243
+
2244
+ assert self.cell_count < self.node_count
2245
+
2246
+ unique_grid_indices = self.__find_gi_node_and_load_hdf5_array(node = node)
2247
+
2248
+ self.__find_grid_node(node = node, unique_grid_indices = unique_grid_indices)
2249
+
2250
+ self.__find_ci_node_and_load_hdf5_array(node = node)
2251
+
2252
+ self.__find_fi_node_and_load_hdf5_array(node)
2253
+
2254
+ interp_uuid = rqet.find_nested_tags_text(node, ['RepresentedInterpretation', 'UUID'])
2255
+ if interp_uuid is None:
2256
+ self.wellbore_interpretation = None
2790
2257
  else:
2791
- uom = length_uom
2792
- if extra == 'DEPTH':
2793
- pk = 'depth'
2258
+ self.wellbore_interpretation = rqo.WellboreInterpretation(self.model, uuid = interp_uuid)
2259
+
2260
+ # Set up matches between cell_indices and grid_indices
2261
+ self.cell_grid_link = self.map_cell_and_grid_indices()
2262
+
2263
+ def __find_trajectory_uuid(self, node):
2264
+ """Find and verify the uuid of the trajectory associated with the BlockedWell object."""
2265
+
2266
+ trajectory_uuid = bu.uuid_from_string(rqet.find_nested_tags_text(node, ['Trajectory', 'UUID']))
2267
+ assert trajectory_uuid is not None, 'blocked well trajectory reference not found in xml'
2268
+ if self.trajectory is None:
2269
+ self.trajectory = rqw.Trajectory(self.model, uuid = trajectory_uuid)
2794
2270
  else:
2795
- pk = 'length'
2796
- return uom, pk, False
2271
+ assert bu.matching_uuids(self.trajectory.uuid, trajectory_uuid), 'blocked well trajectory uuid mismatch'
2797
2272
 
2798
- def static_kh(self,
2799
- ntg_uuid = None,
2800
- perm_i_uuid = None,
2801
- perm_j_uuid = None,
2802
- perm_k_uuid = None,
2803
- satw_uuid = None,
2804
- sato_uuid = None,
2805
- satg_uuid = None,
2806
- region_uuid = None,
2807
- active_only = False,
2808
- min_k0 = None,
2809
- max_k0 = None,
2810
- k0_list = None,
2811
- min_length = None,
2812
- min_kh = None,
2813
- max_depth = None,
2814
- max_satw = None,
2815
- min_sato = None,
2816
- max_satg = None,
2817
- perforation_list = None,
2818
- region_list = None,
2819
- set_k_face_intervals_vertical = False,
2820
- anglv_ref = 'gravity',
2821
- angla_plane_ref = None,
2822
- length_mode = 'MD',
2823
- length_uom = None,
2824
- use_face_centres = False,
2825
- preferential_perforation = True):
2826
- """Returns the total static K.H (permeability x height).
2273
+ def __find_ci_node_and_load_hdf5_array(self, node):
2274
+ """Find the BlockedWell object's cell indices hdf5 reference node and load the array."""
2827
2275
 
2828
- notes:
2829
- length units are those of trajectory md_uom unless length_upm is set;
2830
- see doc string for dataframe() method for argument descriptions; perm_i_uuid required
2831
- """
2276
+ ci_node = rqet.find_tag(node, 'CellIndices')
2277
+ assert ci_node is not None, 'blocked well cell indices hdf5 reference not found in xml'
2278
+ rqwu.load_hdf5_array(self, ci_node, 'cell_indices', dtype = self.cell_index_dtype)
2279
+ assert (self.cell_indices is not None and self.cell_indices.ndim == 1 and
2280
+ self.cell_indices.size == self.cell_count), 'mismatch in number of cell indices for blocked well'
2281
+ self.cellind_null = rqet.find_tag_int(ci_node, 'NullValue')
2282
+ if self.cellind_null is None:
2283
+ self.cellind_null = -1 # if no Null found assume -1 default
2832
2284
 
2833
- df = self.dataframe(i_col = 'I',
2834
- j_col = 'J',
2835
- k_col = 'K',
2836
- one_based = False,
2837
- extra_columns_list = ['KH'],
2838
- ntg_uuid = ntg_uuid,
2839
- perm_i_uuid = perm_i_uuid,
2840
- perm_j_uuid = perm_j_uuid,
2841
- perm_k_uuid = perm_k_uuid,
2842
- satw_uuid = satw_uuid,
2843
- sato_uuid = sato_uuid,
2844
- satg_uuid = satg_uuid,
2845
- region_uuid = region_uuid,
2846
- active_only = active_only,
2847
- min_k0 = min_k0,
2848
- max_k0 = max_k0,
2849
- k0_list = k0_list,
2850
- min_length = min_length,
2851
- min_kh = min_kh,
2852
- max_depth = max_depth,
2853
- max_satw = max_satw,
2854
- min_sato = min_sato,
2855
- max_satg = max_satg,
2856
- perforation_list = perforation_list,
2857
- region_list = region_list,
2858
- set_k_face_intervals_vertical = set_k_face_intervals_vertical,
2859
- anglv_ref = anglv_ref,
2860
- angla_plane_ref = angla_plane_ref,
2861
- length_mode = length_mode,
2862
- length_uom = length_uom,
2863
- use_face_centres = use_face_centres,
2864
- preferential_perforation = preferential_perforation)
2285
+ def __find_fi_node_and_load_hdf5_array(self, node):
2286
+ """Find the BlockedWell object's face indices hdf5 reference node and load the array."""
2865
2287
 
2866
- return sum(df['KH'])
2288
+ fi_node = rqet.find_tag(node, 'LocalFacePairPerCellIndices')
2289
+ assert fi_node is not None, 'blocked well face indices hdf5 reference not found in xml'
2290
+ rqwu.load_hdf5_array(self, fi_node, 'raw_face_indices', dtype = np.int8)
2291
+ assert self.raw_face_indices is not None, 'failed to load face indices for blocked well'
2292
+ assert self.raw_face_indices.size == 2 * self.cell_count, 'mismatch in number of cell faces for blocked well'
2293
+ if self.raw_face_indices.ndim > 1:
2294
+ self.raw_face_indices = self.raw_face_indices.reshape((self.raw_face_indices.size,))
2295
+ mask = np.where(self.raw_face_indices == -1)
2296
+ self.raw_face_indices[mask] = 0
2297
+ self.face_pair_indices = self.face_index_inverse_map[self.raw_face_indices]
2298
+ self.face_pair_indices[mask] = (-1, -1)
2299
+ self.face_pair_indices = self.face_pair_indices.reshape((-1, 2, 2))
2300
+ del self.raw_face_indices
2301
+ self.facepair_null = rqet.find_tag_int(fi_node, 'NullValue')
2302
+ if self.facepair_null is None:
2303
+ self.facepair_null = -1
2867
2304
 
2868
- def write_wellspec(self,
2869
- wellspec_file,
2870
- well_name = None,
2871
- mode = 'a',
2872
- extra_columns_list = [],
2873
- ntg_uuid = None,
2874
- perm_i_uuid = None,
2875
- perm_j_uuid = None,
2876
- perm_k_uuid = None,
2877
- satw_uuid = None,
2878
- sato_uuid = None,
2879
- satg_uuid = None,
2880
- region_uuid = None,
2881
- radw = None,
2882
- skin = None,
2883
- stat = None,
2884
- active_only = False,
2885
- min_k0 = None,
2886
- max_k0 = None,
2887
- k0_list = None,
2888
- min_length = None,
2889
- min_kh = None,
2890
- max_depth = None,
2891
- max_satw = None,
2892
- min_sato = None,
2893
- max_satg = None,
2894
- perforation_list = None,
2895
- region_list = None,
2896
- set_k_face_intervals_vertical = False,
2897
- depth_inc_down = True,
2898
- anglv_ref = 'gravity',
2899
- angla_plane_ref = None,
2900
- length_mode = 'MD',
2901
- length_uom = None,
2902
- preferential_perforation = True,
2903
- space_instead_of_tab_separator = True,
2904
- align_columns = True,
2905
- preceeding_blank_lines = 0,
2906
- trailing_blank_lines = 0,
2907
- length_uom_comment = False,
2908
- write_nexus_units = True,
2909
- float_format = '5.3',
2910
- use_properties = False,
2911
- property_time_index = None):
2912
- """Writes Nexus WELLSPEC keyword to an ascii file.
2305
+ def __find_gi_node_and_load_hdf5_array(self, node):
2306
+ """Find the BlockedWell object's grid indices hdf5 reference node and load the array."""
2913
2307
 
2914
- returns:
2915
- pandas DataFrame containing data that has been written to the wellspec file
2308
+ gi_node = rqet.find_tag(node, 'GridIndices')
2309
+ assert gi_node is not None, 'blocked well grid indices hdf5 reference not found in xml'
2310
+ rqwu.load_hdf5_array(self, gi_node, 'grid_indices', dtype = np.int32)
2311
+ # assert self.grid_indices is not None and self.grid_indices.ndim == 1 and self.grid_indices.size == self.node_count - 1
2312
+ # temporary code to handle blocked wells with incorrectly shaped grid indices wrt. nodes
2313
+ assert self.grid_indices is not None and self.grid_indices.ndim == 1
2314
+ if self.grid_indices.size != self.node_count - 1:
2315
+ if self.grid_indices.size == self.cell_count and self.node_count == 2 * self.cell_count:
2316
+ log.warning(f'handling node duplication or missing unblocked intervals in blocked well: {self.title}')
2317
+ expanded_grid_indices = np.full(self.node_count - 1, -1, dtype = np.int32)
2318
+ expanded_grid_indices[::2] = self.grid_indices
2319
+ self.grid_indices = expanded_grid_indices
2320
+ else:
2321
+ raise ValueError(
2322
+ f'incorrect grid indices size with respect to node count in blocked well: {self.title}')
2323
+ # end of temporary code
2324
+ unique_grid_indices = np.unique(self.grid_indices) # sorted list of unique values
2325
+ self.gridind_null = rqet.find_tag_int(gi_node, 'NullValue')
2326
+ if self.gridind_null is None:
2327
+ self.gridind_null = -1 # if no Null found assume -1 default
2328
+ return unique_grid_indices
2916
2329
 
2917
- note:
2918
- see doc string for dataframe() method for most of the argument descriptions;
2919
- align_columns and float_format arguments are deprecated and no longer used
2920
- """
2330
+ def __find_grid_node(self, node, unique_grid_indices):
2331
+ """Find the BlockedWell object's grid reference node(s)."""
2921
2332
 
2922
- assert wellspec_file, 'no output file specified to write WELLSPEC to'
2333
+ grid_node_list = rqet.list_of_tag(node, 'Grid')
2334
+ assert len(grid_node_list) > 0, 'blocked well grid reference(s) not found in xml'
2335
+ assert unique_grid_indices[0] >= -1 and unique_grid_indices[-1] < len(
2336
+ grid_node_list), 'blocked well grid index out of range'
2337
+ assert np.count_nonzero(
2338
+ self.grid_indices >= 0) == self.cell_count, 'mismatch in number of blocked well intervals'
2339
+ self.grid_list = []
2340
+ for grid_ref_node in grid_node_list:
2341
+ grid_node = self.model.referenced_node(grid_ref_node)
2342
+ assert grid_node is not None, 'grid referenced in blocked well xml is not present in model'
2343
+ grid_uuid = rqet.uuid_for_part_root(grid_node)
2344
+ grid_obj = self.model.grid(uuid = grid_uuid, find_properties = False)
2345
+ self.grid_list.append(grid_obj)
2346
+ if grid_obj.is_big():
2347
+ self.cell_index_dtype = np.int64
2923
2348
 
2924
- col_width_dict = {
2925
- 'IW': 4,
2926
- 'JW': 4,
2927
- 'L': 4,
2928
- 'ANGLA': 8,
2929
- 'ANGLV': 8,
2930
- 'LENGTH': 8,
2931
- 'KH': 10,
2932
- 'DEPTH': 10,
2933
- 'MD': 10,
2934
- 'X': 8,
2935
- 'Y': 12,
2936
- 'SKIN': 7,
2937
- 'RADW': 5,
2938
- 'RADB': 8,
2939
- 'PPERF': 5
2940
- }
2349
+ @staticmethod
2350
+ def __verify_header_lines_in_cellio_file(fp, well_name, cellio_file):
2351
+ """Find and verify the information in the header lines for the specified well in the RMS cellio file."""
2352
+
2353
+ while True:
2354
+ kf.skip_blank_lines_and_comments(fp)
2355
+ line = fp.readline() # file format version number?
2356
+ assert line, 'well ' + str(well_name) + ' not found in file ' + str(cellio_file)
2357
+ fp.readline() # 'Undefined'
2358
+ words = fp.readline().split()
2359
+ assert len(words), 'missing header info in cell I/O file'
2360
+ if words[0].upper() == well_name.upper():
2361
+ break
2362
+ while not kf.blank_line(fp):
2363
+ fp.readline() # skip to block of data for next well
2364
+ header_lines = int(fp.readline().strip())
2365
+ for _ in range(header_lines):
2366
+ fp.readline()
2367
+
2368
+ @staticmethod
2369
+ def __parse_non_blank_line_in_cellio_file(line, grid, cellio_z_inc_down, grid_z_inc_down):
2370
+ """Parse each non-blank line in the RMS cellio file for the relevant parameters."""
2371
+
2372
+ words = line.split()
2373
+ assert len(words) >= 9, 'not enough items on data line in cell I/O file, minimum 9 expected'
2374
+ i1, j1, k1 = int(words[0]), int(words[1]), int(words[2])
2375
+ cell_kji0 = np.array((k1 - 1, j1 - 1, i1 - 1), dtype = np.int32)
2376
+ assert np.all(0 <= cell_kji0) and np.all(
2377
+ cell_kji0 < grid.extent_kji), 'cell I/O cell index not within grid extent'
2378
+ entry_xyz = np.array((float(words[3]), float(words[4]), float(words[5])))
2379
+ exit_xyz = np.array((float(words[6]), float(words[7]), float(words[8])))
2380
+ if cellio_z_inc_down is None:
2381
+ cellio_z_inc_down = bool(entry_xyz[2] + exit_xyz[2] > 0.0)
2382
+ if cellio_z_inc_down != grid_z_inc_down:
2383
+ entry_xyz[2] = -entry_xyz[2]
2384
+ exit_xyz[2] = -exit_xyz[2]
2385
+ return cell_kji0, entry_xyz, exit_xyz
2386
+
2387
+ @staticmethod
2388
+ def __calculate_cell_cp_center_and_vectors(grid, cell_kji0, entry_xyz, exit_xyz, well_name):
2389
+ # calculate the i,j,k coordinates that represent the corner points and center of a perforation cell
2390
+ # calculate the entry and exit vectors for the perforation cell
2391
+
2392
+ cp = grid.corner_points(cell_kji0 = cell_kji0, cache_resqml_array = False)
2393
+ assert not np.any(np.isnan(
2394
+ cp)), 'missing geometry for perforation cell(kji0) ' + str(cell_kji0) + ' for well ' + str(well_name)
2395
+ cell_centre = np.mean(cp, axis = (0, 1, 2))
2396
+ # let's hope everything is in the same coordinate reference system!
2397
+ entry_vector = 100.0 * (entry_xyz - cell_centre)
2398
+ exit_vector = 100.0 * (exit_xyz - cell_centre)
2399
+ return cp, cell_centre, entry_vector, exit_vector
2400
+
2401
+ @staticmethod
2402
+ def __check_number_of_blocked_well_intervals(blocked_cells_kji0, well_name, grid_name):
2403
+ """Check that at least one interval is blocked for the specified well."""
2404
+
2405
+ blocked_count = len(blocked_cells_kji0)
2406
+ if blocked_count == 0:
2407
+ log.warning(f"No intervals blocked for well {well_name} in grid"
2408
+ f"{f' {grid_name}' if grid_name is not None else ''}.")
2409
+ return None
2410
+ else:
2411
+ log.info(f"{blocked_count} interval{rqwu._pl(blocked_count)} blocked for well {well_name} in"
2412
+ f" grid{f' {grid_name}' if grid_name is not None else ''}.")
2413
+
2414
+ def __get_interval_count(self):
2415
+ """Get the number of intervals to be added to the dataframe."""
2416
+
2417
+ if self.node_count is None or self.node_count < 2:
2418
+ interval_count = 0
2419
+ else:
2420
+ interval_count = self.node_count - 1
2421
+
2422
+ return interval_count
2423
+
2424
+ @staticmethod
2425
+ def __prop_array(uuid_or_dict, grid):
2426
+ assert uuid_or_dict is not None and grid is not None
2427
+ if isinstance(uuid_or_dict, dict):
2428
+ prop_uuid = uuid_or_dict[grid.uuid]
2429
+ else:
2430
+ prop_uuid = uuid_or_dict # uuid either in form of string or uuid.UUID
2431
+ return grid.property_collection.single_array_ref(uuid = prop_uuid)
2432
+
2433
+ @staticmethod
2434
+ def __get_ref_vector(grid, grid_crs, cell_kji0, mode):
2435
+ # returns unit vector with true direction, ie. accounts for differing xy & z units in grid's crs
2436
+ # gravity = np.array((0.0, 0.0, 1.0))
2437
+ if mode == 'normal well i+':
2438
+ return None # ANGLA only: option for no projection onto a plane
2439
+ ref_vector = None
2440
+ # options for anglv or angla reference: 'z down', 'z+', 'k down', 'k+', 'normal ij', 'normal ij down'
2441
+ if mode == 'z+':
2442
+ ref_vector = np.array((0.0, 0.0, 1.0))
2443
+ elif mode == 'z down':
2444
+ if grid_crs.z_inc_down:
2445
+ ref_vector = np.array((0.0, 0.0, 1.0))
2446
+ else:
2447
+ ref_vector = np.array((0.0, 0.0, -1.0))
2448
+ else:
2449
+ cell_axial_vectors = grid.interface_vectors_kji(cell_kji0)
2450
+ if grid_crs.xy_units != grid_crs.z_units:
2451
+ wam.convert_lengths(cell_axial_vectors[..., 2], grid_crs.z_units, grid_crs.xy_units)
2452
+ if mode in ['k+', 'k down']:
2453
+ ref_vector = vec.unit_vector(cell_axial_vectors[0])
2454
+ if mode == 'k down' and not grid.k_direction_is_down:
2455
+ ref_vector = -ref_vector
2456
+ else: # normal to plane of ij axes
2457
+ ref_vector = vec.unit_vector(vec.cross_product(cell_axial_vectors[1], cell_axial_vectors[2]))
2458
+ if mode == 'normal ij down':
2459
+ if grid_crs.z_inc_down:
2460
+ if ref_vector[2] < 0.0:
2461
+ ref_vector = -ref_vector
2462
+ else:
2463
+ if ref_vector[2] > 0.0:
2464
+ ref_vector = -ref_vector
2465
+ if ref_vector is None or ref_vector[2] == 0.0:
2466
+ if grid_crs.z_inc_down:
2467
+ ref_vector = np.array((0.0, 0.0, 1.0))
2468
+ else:
2469
+ ref_vector = np.array((0.0, 0.0, -1.0))
2470
+ return ref_vector
2471
+
2472
+ @staticmethod
2473
+ def __verify_angle_references(anglv_ref, angla_plane_ref):
2474
+ """Verify that the references for anglv and angla are one of the acceptable options."""
2475
+
2476
+ assert anglv_ref in ['gravity', 'z down', 'z+', 'k down', 'k+', 'normal ij', 'normal ij down']
2477
+ if anglv_ref == 'gravity':
2478
+ anglv_ref = 'z down'
2479
+ if angla_plane_ref is None:
2480
+ angla_plane_ref = anglv_ref
2481
+ assert angla_plane_ref in [
2482
+ 'gravity', 'z down', 'z+', 'k down', 'k+', 'normal ij', 'normal ij down', 'normal well i+'
2483
+ ]
2484
+ if angla_plane_ref == 'gravity':
2485
+ angla_plane_ref = 'z down'
2486
+ return anglv_ref, angla_plane_ref
2487
+
2488
+ @staticmethod
2489
+ def __verify_saturation_ranges_and_property_uuids(max_satw, min_sato, max_satg, satw_uuid, sato_uuid, satg_uuid):
2490
+ # verify that the fluid saturation limits fall within 0.0 to 1.0 and that the uuid of the required
2491
+ # saturation property array has been specified.
2492
+
2493
+ if max_satw is not None and max_satw >= 1.0:
2494
+ max_satw = None
2495
+ if min_sato is not None and min_sato <= 0.0:
2496
+ min_sato = None
2497
+ if max_satg is not None and max_satg >= 1.0:
2498
+ max_satg = None
2499
+
2500
+ phase_list = ['water', 'oil', 'gas']
2501
+ phase_saturation_limits_list = [max_satw, min_sato, max_satg]
2502
+ uuids_list = [satw_uuid, sato_uuid, satg_uuid]
2503
+
2504
+ for phase, phase_limit, uuid in zip(phase_list, phase_saturation_limits_list, uuids_list):
2505
+ if phase_limit is not None:
2506
+ assert uuid is not None, f'{phase} saturation limit specified without saturation property array'
2507
+
2508
+ return max_satw, min_sato, max_satg
2509
+
2510
+ @staticmethod
2511
+ def __verify_extra_properties_to_be_added_to_dataframe(extra_columns_list, column_list, add_as_properties,
2512
+ use_properties, skin, stat, radw):
2513
+ """Determine which extra columns, if any, should be added as properties to the dataframe.
2514
+
2515
+ note:
2516
+ if skin, stat or radw are None, default values are specified
2517
+ """
2518
+
2519
+ if extra_columns_list:
2520
+ for extra in extra_columns_list:
2521
+ assert extra.upper() in [
2522
+ 'GRID', 'ANGLA', 'ANGLV', 'LENGTH', 'KH', 'DEPTH', 'MD', 'X', 'Y', 'SKIN', 'RADW', 'PPERF', 'RADB',
2523
+ 'WI', 'WBC', 'STAT'
2524
+ ]
2525
+ column_list.append(extra.upper())
2526
+ else:
2527
+ add_as_properties = use_properties = False
2528
+ assert not (add_as_properties and use_properties)
2529
+
2530
+ column_list, skin, stat, radw = BlockedWell.__check_skin_stat_radw_to_be_added_as_properties(
2531
+ skin = skin, stat = stat, radw = radw, column_list = column_list)
2532
+
2533
+ return column_list, add_as_properties, use_properties, skin, stat, radw
2534
+
2535
+ @staticmethod
2536
+ def __check_perforation_properties_to_be_added(column_list, perforation_list):
2537
+
2538
+ if all(['LENGTH' in column_list, 'PPERF' in column_list, 'KH' not in column_list, perforation_list
2539
+ is not None]):
2540
+ log.warning(
2541
+ 'both LENGTH and PPERF will include effects of partial perforation; only one should be used in WELLSPEC'
2542
+ )
2543
+ elif all([
2544
+ perforation_list is not None, 'LENGTH' not in column_list, 'PPERF' not in column_list, 'KH'
2545
+ not in column_list, 'WBC' not in column_list
2546
+ ]):
2547
+ log.warning('perforation list supplied but no use of LENGTH, KH, PPERF nor WBC')
2548
+
2549
+ if perforation_list is not None and len(perforation_list) == 0:
2550
+ log.warning('empty perforation list specified for blocked well dataframe: no rows will be included')
2551
+
2552
+ @staticmethod
2553
+ def __check_skin_stat_radw_to_be_added_as_properties(skin, stat, radw, column_list):
2554
+ """Verify whether skin should be added as a property in the dataframe."""
2555
+
2556
+ if skin is not None and 'SKIN' not in column_list:
2557
+ column_list.append('SKIN')
2558
+
2559
+ if stat is not None:
2560
+ assert str(stat).upper() in ['ON', 'OFF']
2561
+ stat = str(stat).upper()
2562
+ if 'STAT' not in column_list:
2563
+ column_list.append('STAT')
2564
+
2565
+
2566
+ # else:
2567
+ #  stat = 'ON'
2568
+
2569
+ if radw is not None and 'RADW' not in column_list:
2570
+ column_list.append('RADW')
2571
+
2572
+ return column_list, skin, stat, radw
2573
+
2574
+ @staticmethod
2575
+ def __verify_perm_i_uuid_for_well_inflow(column_list, perm_i_uuid, pc_titles):
2576
+ # Verify that the I direction permeability has been specified if well inflow properties are to be added
2577
+ # to the dataframe.
2578
+
2579
+ do_well_inflow = (('WI' in column_list and 'WI' not in pc_titles) or
2580
+ ('WBC' in column_list and 'WBC' not in pc_titles) or
2581
+ ('RADB' in column_list and 'RADB' not in pc_titles))
2582
+ if do_well_inflow:
2583
+ assert perm_i_uuid is not None, 'WI, RADB or WBC requested without I direction permeabilty being specified'
2584
+
2585
+ return do_well_inflow
2586
+
2587
+ @staticmethod
2588
+ def __verify_perm_i_uuid_for_kh(min_kh, column_list, perm_i_uuid, pc_titles):
2589
+ # verify that the I direction permeability has been specified if permeability thickness and
2590
+ # wellbore constant properties are to be added to the dataframe.
2591
+
2592
+ if min_kh is not None and min_kh <= 0.0:
2593
+ min_kh = None
2594
+ doing_kh = False
2595
+ if ('KH' in column_list or min_kh is not None) and 'KH' not in pc_titles:
2596
+ assert perm_i_uuid is not None, 'KH requested (or minimum specified) without I direction permeabilty being specified'
2597
+ doing_kh = True
2598
+ if 'WBC' in column_list and 'WBC' not in pc_titles:
2599
+ assert perm_i_uuid is not None, 'WBC requested without I direction permeabilty being specified'
2600
+ doing_kh = True
2601
+
2602
+ return min_kh, doing_kh
2603
+
2604
+ @staticmethod
2605
+ def __verify_perm_j_k_uuids_for_kh_and_well_inflow(doing_kh, do_well_inflow, perm_i_uuid, perm_j_uuid, perm_k_uuid):
2606
+ # verify that the J and K direction permeabilities have been specified if well inflow properties or
2607
+ # permeability thickness properties are to be added to the dataframe.
2608
+
2609
+ isotropic_perm = None
2610
+ if doing_kh or do_well_inflow:
2611
+ if perm_j_uuid is None and perm_k_uuid is None:
2612
+ isotropic_perm = True
2613
+ else:
2614
+ if perm_j_uuid is None:
2615
+ perm_j_uuid = perm_i_uuid
2616
+ if perm_k_uuid is None:
2617
+ perm_k_uuid = perm_i_uuid
2618
+ # following line assumes arguments are passed in same form; if not, some unnecessary maths might be done
2619
+ isotropic_perm = (bu.matching_uuids(perm_i_uuid, perm_j_uuid) and
2620
+ bu.matching_uuids(perm_i_uuid, perm_k_uuid))
2621
+
2622
+ return perm_j_uuid, perm_k_uuid, isotropic_perm
2623
+
2624
+ @staticmethod
2625
+ def __verify_k_layers_to_be_included(min_k0, max_k0, k0_list):
2626
+ # verify that the k layers to be included in the dataframe exist within the appropriate range
2627
+
2628
+ if min_k0 is None:
2629
+ min_k0 = 0
2630
+ else:
2631
+ assert min_k0 >= 0
2632
+ if max_k0 is not None:
2633
+ assert min_k0 <= max_k0
2634
+ if k0_list is not None and len(k0_list) == 0:
2635
+ log.warning('no layers included for blocked well dataframe: no rows will be included')
2636
+
2637
+ @staticmethod
2638
+ def __verify_if_angles_xyz_and_length_to_be_added(column_list, pc_titles, doing_kh, do_well_inflow, length_mode):
2639
+ # determine if angla, anglv, x, y, z and length data are to be added as properties to the dataframe
2640
+
2641
+ doing_angles = any([('ANGLA' in column_list and 'ANGLA' not in pc_titles),
2642
+ ('ANGLV' in column_list and 'ANGLV' not in pc_titles), (doing_kh), (do_well_inflow)])
2643
+ doing_xyz = any([('X' in column_list and 'X' not in pc_titles), ('Y' in column_list and 'Y' not in pc_titles),
2644
+ ('DEPTH' in column_list and 'DEPTH' not in pc_titles)])
2645
+ doing_entry_exit = any([(doing_angles),
2646
+ ('LENGTH' in column_list and 'LENGTH' not in pc_titles and length_mode == 'straight')])
2647
+
2648
+ # doing_angles = (('ANGLA' in column_list and 'ANGLA' not in pc_titles) or
2649
+ # ('ANGLV' in column_list and 'ANGLV' not in pc_titles) or doing_kh or do_well_inflow)
2650
+ # doing_xyz = (('X' in column_list and 'X' not in pc_titles) or (
2651
+ # 'Y' in column_list and 'Y' not in pc_titles) or
2652
+ # ('DEPTH' in column_list and 'DEPTH' not in pc_titles))
2653
+ # doing_entry_exit = doing_angles or ('LENGTH' in column_list and 'LENGTH' not in pc_titles and
2654
+ # length_mode == 'straight')
2655
+
2656
+ return doing_angles, doing_xyz, doing_entry_exit
2657
+
2658
+ def __verify_number_of_grids_and_crs_units(self, column_list):
2659
+ # verify that a GRID column is included in the dataframe if the well intersects more than one grid
2660
+ # verify that each grid's crs units are consistent in all directions
2661
+
2662
+ if 'GRID' not in column_list and self.number_of_grids() > 1:
2663
+ log.error('creating blocked well dataframe without GRID column for well that intersects more than one grid')
2664
+ grid_crs_list = []
2665
+ for grid in self.grid_list:
2666
+ grid_crs = crs.Crs(self.model, uuid = grid.crs_uuid)
2667
+ grid_crs_list.append(grid_crs)
2668
+ return grid_crs_list
2669
+
2670
+ def __get_trajectory_crs_and_z_inc_down(self):
2671
+
2672
+ if self.trajectory is None or self.trajectory.crs_uuid is None:
2673
+ traj_crs = None
2674
+ traj_z_inc_down = None
2675
+ else:
2676
+ traj_crs = crs.Crs(self.trajectory.model, uuid = self.trajectory.crs_uuid)
2677
+ traj_z_inc_down = traj_crs.z_inc_down
2678
+
2679
+ return traj_crs, traj_z_inc_down
2680
+
2681
+ @staticmethod
2682
+ def __check_cell_depth(max_depth, grid, cell_kji0, grid_crs):
2683
+ """Check whether the maximum depth specified has been exceeded with the current interval."""
2684
+
2685
+ max_depth_exceeded = False
2686
+ if max_depth is not None:
2687
+ cell_depth = grid.centre_point(cell_kji0)[2]
2688
+ if not grid_crs.z_inc_down:
2689
+ cell_depth = -cell_depth
2690
+ if cell_depth > max_depth:
2691
+ max_depth_exceeded = True
2692
+ return max_depth_exceeded
2693
+
2694
+ @staticmethod
2695
+ def __skip_interval_check(max_depth, grid, cell_kji0, grid_crs, active_only, tuple_kji0, min_k0, max_k0, k0_list,
2696
+ region_list, region_uuid, max_satw, satw_uuid, min_sato, sato_uuid, max_satg, satg_uuid):
2697
+ """Check whether any conditions are met that mean the interval should be skipped."""
2698
+
2699
+ max_depth_exceeded = BlockedWell.__check_cell_depth(max_depth = max_depth,
2700
+ grid = grid,
2701
+ cell_kji0 = cell_kji0,
2702
+ grid_crs = grid_crs)
2703
+ inactive_grid = active_only and grid.inactive is not None and grid.inactive[tuple_kji0]
2704
+ out_of_bounds_layer_1 = (min_k0 is not None and cell_kji0[0] < min_k0) or (max_k0 is not None and
2705
+ cell_kji0[0] > max_k0)
2706
+ out_of_bounds_layer_2 = k0_list is not None and cell_kji0[0] not in k0_list
2707
+ out_of_bounds_region = (region_list is not None and
2708
+ BlockedWell.__prop_array(region_uuid, grid)[tuple_kji0] not in region_list)
2709
+ saturation_limit_exceeded_1 = (max_satw is not None and
2710
+ BlockedWell.__prop_array(satw_uuid, grid)[tuple_kji0] > max_satw)
2711
+ saturation_limit_exceeded_2 = (min_sato is not None and
2712
+ BlockedWell.__prop_array(sato_uuid, grid)[tuple_kji0] < min_sato)
2713
+ saturation_limit_exceeded_3 = (max_satg is not None and
2714
+ BlockedWell.__prop_array(satg_uuid, grid)[tuple_kji0] > max_satg)
2715
+ skip_interval = any([
2716
+ max_depth_exceeded, inactive_grid, out_of_bounds_layer_1, out_of_bounds_layer_2, out_of_bounds_region,
2717
+ saturation_limit_exceeded_1, saturation_limit_exceeded_2, saturation_limit_exceeded_3
2718
+ ])
2719
+
2720
+ return skip_interval
2721
+
2722
+ def __get_part_perf_fraction_for_interval(self, pc, pc_titles, ci, perforation_list, interval, length_tol = 0.01):
2723
+ """Get the partial perforation fraction for the interval."""
2724
+
2725
+ skip_interval = False
2726
+ if 'PPERF' in pc_titles:
2727
+ part_perf_fraction = pc.single_array_ref(citation_title = 'PPERF')[ci]
2728
+ else:
2729
+ part_perf_fraction = 1.0
2730
+ if perforation_list is not None:
2731
+ perf_length = 0.0
2732
+ for perf_start, perf_end in perforation_list:
2733
+ if perf_end <= self.node_mds[interval] or perf_start >= self.node_mds[interval + 1]:
2734
+ continue
2735
+ if perf_start <= self.node_mds[interval]:
2736
+ if perf_end >= self.node_mds[interval + 1]:
2737
+ perf_length += self.node_mds[interval + 1] - self.node_mds[interval]
2738
+ break
2739
+ else:
2740
+ perf_length += perf_end - self.node_mds[interval]
2741
+ else:
2742
+ if perf_end >= self.node_mds[interval + 1]:
2743
+ perf_length += self.node_mds[interval + 1] - perf_start
2744
+ else:
2745
+ perf_length += perf_end - perf_start
2746
+ if perf_length < length_tol:
2747
+ skip_interval = True
2748
+ perf_length = 0.0
2749
+ part_perf_fraction = min(1.0, perf_length / (self.node_mds[interval + 1] - self.node_mds[interval]))
2750
+
2751
+ return skip_interval, part_perf_fraction
2752
+
2753
+ def __get_entry_exit_xyz_and_crs_for_interval(self, doing_entry_exit, use_face_centres, grid, cell_kji0, interval,
2754
+ ci, grid_crs, traj_crs):
2755
+ # calculate the entry and exit points for the interval and set the entry and exit coordinate reference system
2756
+
2757
+ entry_xyz = None
2758
+ exit_xyz = None
2759
+ ee_crs = None
2760
+ if doing_entry_exit:
2761
+ assert self.trajectory is not None
2762
+ if use_face_centres:
2763
+ entry_xyz = grid.face_centre(cell_kji0, self.face_pair_indices[ci, 0, 0], self.face_pair_indices[ci, 0,
2764
+ 1])
2765
+ if self.face_pair_indices[ci, 1, 0] >= 0:
2766
+ exit_xyz = grid.face_centre(cell_kji0, self.face_pair_indices[ci, 1, 0],
2767
+ self.face_pair_indices[ci, 1, 1])
2768
+ else:
2769
+ exit_xyz = grid.face_centre(cell_kji0, self.face_pair_indices[ci, 0, 0],
2770
+ 1 - self.face_pair_indices[ci, 0, 1])
2771
+ ee_crs = grid_crs
2772
+ else:
2773
+ entry_xyz = self.trajectory.xyz_for_md(self.node_mds[interval])
2774
+ exit_xyz = self.trajectory.xyz_for_md(self.node_mds[interval + 1])
2775
+ ee_crs = traj_crs
2776
+
2777
+ return entry_xyz, exit_xyz, ee_crs
2778
+
2779
+ def __get_length_of_interval(self, length_mode, interval, length_uom, entry_xyz, exit_xyz, ee_crs, perforation_list,
2780
+ part_perf_fraction, min_length):
2781
+ """Calculate the length of the interval."""
2782
+
2783
+ skip_interval = False
2784
+ if length_mode == 'MD':
2785
+ length = self.node_mds[interval + 1] - self.node_mds[interval]
2786
+ if length_uom is not None and self.trajectory is not None and length_uom != self.trajectory.md_uom:
2787
+ length = wam.convert_lengths(length, self.trajectory.md_uom, length_uom)
2788
+ else: # use straight line length between entry and exit
2789
+ entry_xyz, exit_xyz = BlockedWell._single_uom_entry_exit_xyz(entry_xyz, exit_xyz, ee_crs)
2790
+ length = vec.naive_length(exit_xyz - entry_xyz)
2791
+ if length_uom is not None:
2792
+ length = wam.convert_lengths(length, ee_crs.z_units, length_uom)
2793
+ elif self.trajectory is not None:
2794
+ length = wam.convert_lengths(length, ee_crs.z_units, self.trajectory.md_uom)
2795
+ if perforation_list is not None:
2796
+ length *= part_perf_fraction
2797
+ if min_length is not None and length < min_length:
2798
+ skip_interval = True
2799
+
2800
+ return skip_interval, length
2801
+
2802
+ @staticmethod
2803
+ def _single_uom_xyz(xyz, crs, required_uom):
2804
+ if xyz is None:
2805
+ return None
2806
+ xyz = np.array(xyz, dtype = float)
2807
+ if crs.xy_units != required_uom:
2808
+ xyz[0] = wam.convert_lengths(xyz[0], crs.xy_units, required_uom)
2809
+ xyz[1] = wam.convert_lengths(xyz[1], crs.xy_units, required_uom)
2810
+ if crs.z_units != required_uom:
2811
+ xyz[2] = wam.convert_lengths(xyz[2], crs.z_units, required_uom)
2812
+ return xyz
2813
+
2814
+ @staticmethod
2815
+ def _single_uom_entry_exit_xyz(entry_xyz, exit_xyz, ee_crs):
2816
+ return (BlockedWell._single_uom_xyz(entry_xyz, ee_crs, ee_crs.z_units),
2817
+ BlockedWell._single_uom_xyz(exit_xyz, ee_crs, ee_crs.z_units))
2818
+
2819
+ def __get_angles_for_interval(self, pc, pc_titles, doing_angles, set_k_face_intervals_vertical, ci, k_face_check,
2820
+ k_face_check_end, entry_xyz, exit_xyz, ee_crs, traj_z_inc_down, grid, grid_crs,
2821
+ cell_kji0, anglv_ref, angla_plane_ref):
2822
+ """Calculate angla, anglv and related trigonometirc transforms for the interval."""
2823
+
2824
+ sine_anglv = sine_angla = 0.0
2825
+ cosine_anglv = cosine_angla = 1.0
2826
+ anglv = pc.single_array_ref(citation_title = 'ANGLV')[ci] if 'ANGLV' in pc_titles else None
2827
+ angla = pc.single_array_ref(citation_title = 'ANGLA')[ci] if 'ANGLA' in pc_titles else None
2828
+
2829
+ if doing_angles and not (set_k_face_intervals_vertical and
2830
+ (np.all(self.face_pair_indices[ci] == k_face_check) or
2831
+ np.all(self.face_pair_indices[ci] == k_face_check_end))):
2832
+ anglv, sine_anglv, cosine_anglv, vector, a_ref_vector = BlockedWell.__get_anglv_for_interval(
2833
+ anglv = anglv,
2834
+ entry_xyz = entry_xyz,
2835
+ exit_xyz = exit_xyz,
2836
+ ee_crs = ee_crs,
2837
+ traj_z_inc_down = traj_z_inc_down,
2838
+ grid = grid,
2839
+ grid_crs = grid_crs,
2840
+ cell_kji0 = cell_kji0,
2841
+ anglv_ref = anglv_ref,
2842
+ angla_plane_ref = angla_plane_ref)
2843
+ if anglv != 0.0:
2844
+ angla, sine_angla, cosine_angla = BlockedWell.__get_angla_for_interval(angla = angla,
2845
+ grid = grid,
2846
+ cell_kji0 = cell_kji0,
2847
+ vector = vector,
2848
+ a_ref_vector = a_ref_vector)
2849
+ if angla is None:
2850
+ angla = 0.0
2851
+ if anglv is None:
2852
+ anglv = 0.0
2853
+
2854
+ return anglv, sine_anglv, cosine_anglv, angla, sine_angla, cosine_angla
2855
+
2856
+ @staticmethod
2857
+ def __get_angla_for_interval(angla, grid, cell_kji0, vector, a_ref_vector):
2858
+ """Calculate angla and related trigonometric transforms for the interval."""
2859
+
2860
+ if vector is None:
2861
+ return None, None, None
2862
+
2863
+ # project well vector and i-axis vector onto plane defined by normal vector a_ref_vector
2864
+ i_axis = grid.interface_vector(cell_kji0, 2)
2865
+ if grid.crs.xy_units != grid.crs.z_units:
2866
+ i_axis[2] = wam.convert_lengths(i_axis[2], grid.crs.z_units, grid.crs.xy_units)
2867
+ i_axis = vec.unit_vector(i_axis)
2868
+ if a_ref_vector is not None: # project vector and i axis onto a plane
2869
+ vector -= vec.dot_product(vector, a_ref_vector) * a_ref_vector
2870
+ vector = vec.unit_vector(vector)
2871
+ # log.debug('i axis unit vector: ' + str(i_axis))
2872
+ i_axis -= vec.dot_product(i_axis, a_ref_vector) * a_ref_vector
2873
+ i_axis = vec.unit_vector(i_axis)
2874
+ # log.debug('i axis unit vector in reference plane: ' + str(i_axis))
2875
+ if angla is not None:
2876
+ angla_rad = vec.radians_from_degrees(angla)
2877
+ cosine_angla = maths.cos(angla_rad)
2878
+ sine_angla = maths.sin(angla_rad)
2879
+ else:
2880
+ cosine_angla = min(max(vec.dot_product(vector, i_axis), -1.0), 1.0)
2881
+ angla_rad = maths.acos(cosine_angla)
2882
+ # negate angla if vector is 'clockwise from' i_axis when viewed from above, projected in the xy plane
2883
+ # todo: have discussion around angla sign under different ijk handedness (and z inc direction?)
2884
+ sine_angla = maths.sin(angla_rad)
2885
+ angla = vec.degrees_from_radians(angla_rad)
2886
+ if vec.clockwise((0.0, 0.0), i_axis, vector) > 0.0:
2887
+ angla = -angla
2888
+ angla_rad = -angla_rad ## as angle_rad before --> typo?
2889
+ sine_angla = -sine_angla
2890
+
2891
+ # log.debug('angla: ' + str(angla))
2892
+
2893
+ return angla, sine_angla, cosine_angla
2894
+
2895
+ @staticmethod
2896
+ def __get_anglv_for_interval(anglv, entry_xyz, exit_xyz, ee_crs, traj_z_inc_down, grid, grid_crs, cell_kji0,
2897
+ anglv_ref, angla_plane_ref):
2898
+ """Get anglv and related trigonometric transforms for the interval."""
2899
+
2900
+ if entry_xyz is None or exit_xyz is None:
2901
+ return None, None, None, None, None
2902
+
2903
+ entry_xyz, exit_xyz = BlockedWell._single_uom_entry_exit_xyz(entry_xyz, exit_xyz, ee_crs)
2904
+ vector = vec.unit_vector(np.array(exit_xyz) - np.array(entry_xyz)) # nominal wellbore vector for interval
2905
+ if traj_z_inc_down is not None and traj_z_inc_down != grid_crs.z_inc_down:
2906
+ vector[2] = -vector[2]
2907
+ if grid.crs.xy_units == grid.crs.z_units:
2908
+ unit_adjusted_vector = vector
2909
+ else:
2910
+ unit_adjusted_vector = vector.copy()
2911
+ unit_adjusted_vector[2] = wam.convert_lengths(unit_adjusted_vector[2], grid.crs.z_units, grid.crs.xy_units)
2912
+ v_ref_vector = BlockedWell.__get_ref_vector(grid, grid_crs, cell_kji0, anglv_ref)
2913
+ # log.debug('v ref vector: ' + str(v_ref_vector))
2914
+ if angla_plane_ref == anglv_ref:
2915
+ a_ref_vector = v_ref_vector
2916
+ else:
2917
+ a_ref_vector = BlockedWell.__get_ref_vector(grid, grid_crs, cell_kji0, angla_plane_ref)
2918
+ # log.debug('a ref vector: ' + str(a_ref_vector))
2919
+ if anglv is not None:
2920
+ anglv_rad = vec.radians_from_degrees(anglv)
2921
+ cosine_anglv = maths.cos(anglv_rad)
2922
+ sine_anglv = maths.sin(anglv_rad)
2923
+ else:
2924
+ cosine_anglv = min(max(vec.dot_product(unit_adjusted_vector, v_ref_vector), -1.0), 1.0)
2925
+ anglv_rad = maths.acos(cosine_anglv)
2926
+ sine_anglv = maths.sin(anglv_rad)
2927
+ anglv = vec.degrees_from_radians(anglv_rad)
2928
+ # log.debug('anglv: ' + str(anglv))
2929
+
2930
+ return anglv, sine_anglv, cosine_anglv, vector, a_ref_vector
2931
+
2932
+ @staticmethod
2933
+ def __get_ntg_and_directional_perm_for_interval(doing_kh, do_well_inflow, ntg_uuid, grid, tuple_kji0,
2934
+ isotropic_perm, preferential_perforation, part_perf_fraction,
2935
+ perm_i_uuid, perm_j_uuid, perm_k_uuid):
2936
+ """Get the net-to-gross and directional permeability arrays for the interval."""
2937
+
2938
+ ntg_is_one = False
2939
+ k_i = k_j = k_k = None
2940
+ if doing_kh or do_well_inflow:
2941
+ if ntg_uuid is None:
2942
+ ntg = 1.0
2943
+ ntg_is_one = True
2944
+ else:
2945
+ ntg = BlockedWell.__prop_array(ntg_uuid, grid)[tuple_kji0]
2946
+ ntg_is_one = maths.isclose(ntg, 1.0, rel_tol = 0.001)
2947
+ if isotropic_perm and ntg_is_one:
2948
+ k_i = k_j = k_k = BlockedWell.__prop_array(perm_i_uuid, grid)[tuple_kji0]
2949
+ else:
2950
+ if preferential_perforation and not ntg_is_one:
2951
+ if part_perf_fraction <= ntg:
2952
+ ntg = 1.0 # effective ntg when perforated intervals are in pay
2953
+ else:
2954
+ ntg /= part_perf_fraction # adjusted ntg when some perforations in non-pay
2955
+ # todo: check netgross facet type in property perm i & j parts: if set to gross then don't multiply by ntg below
2956
+ k_i = BlockedWell.__prop_array(perm_i_uuid, grid)[tuple_kji0] * ntg
2957
+ k_j = BlockedWell.__prop_array(perm_j_uuid, grid)[tuple_kji0] * ntg
2958
+ k_k = BlockedWell.__prop_array(perm_k_uuid, grid)[tuple_kji0]
2959
+
2960
+ return ntg_is_one, k_i, k_j, k_k
2961
+
2962
+ @staticmethod
2963
+ def __get_kh_for_interval(doing_kh, isotropic_perm, ntg_is_one, length, perm_i_uuid, grid, tuple_kji0, k_i, k_j,
2964
+ k_k, anglv, sine_anglv, cosine_anglv, sine_angla, cosine_angla, min_kh, pc, pc_titles,
2965
+ ci):
2966
+ """Get the permeability-thickness value for the interval."""
2967
+
2968
+ skip_interval = False
2969
+ if doing_kh:
2970
+ kh = BlockedWell.__get_kh_if_doing_kh(isotropic_perm = isotropic_perm,
2971
+ ntg_is_one = ntg_is_one,
2972
+ length = length,
2973
+ perm_i_uuid = perm_i_uuid,
2974
+ grid = grid,
2975
+ tuple_kji0 = tuple_kji0,
2976
+ k_i = k_i,
2977
+ k_j = k_j,
2978
+ k_k = k_k,
2979
+ anglv = anglv,
2980
+ sine_anglv = sine_anglv,
2981
+ cosine_anglv = cosine_anglv,
2982
+ sine_angla = sine_angla,
2983
+ cosine_angla = cosine_angla)
2984
+ if min_kh is not None and kh < min_kh:
2985
+ skip_interval = True
2986
+ elif 'KH' in pc_titles:
2987
+ kh = pc.single_array_ref(citation_title = 'KH')[ci]
2988
+ else:
2989
+ kh = None
2990
+ return skip_interval, kh
2941
2991
 
2942
- well_name = self.__get_well_name(well_name = well_name)
2992
+ @staticmethod
2993
+ def __get_kh_if_doing_kh(isotropic_perm, ntg_is_one, length, perm_i_uuid, grid, tuple_kji0, k_i, k_j, k_k, anglv,
2994
+ sine_anglv, cosine_anglv, sine_angla, cosine_angla):
2995
+ # note: this is believed to return required value even when grid crs has mixed xy & z units;
2996
+ # angles are true angles accounting for any mixed units
2997
+ if isotropic_perm and ntg_is_one:
2998
+ kh = length * BlockedWell.__prop_array(perm_i_uuid, grid)[tuple_kji0]
2999
+ else:
3000
+ if np.isnan(k_i) or np.isnan(k_j):
3001
+ kh = 0.0
3002
+ elif anglv == 0.0:
3003
+ kh = length * maths.sqrt(k_i * k_j)
3004
+ elif np.isnan(k_k):
3005
+ kh = 0.0
3006
+ else:
3007
+ k_e = maths.pow(k_i * k_j * k_k, 1.0 / 3.0)
3008
+ if k_e == 0.0:
3009
+ kh = 0.0
3010
+ else:
3011
+ l_i = length * maths.sqrt(k_e / k_i) * sine_anglv * cosine_angla
3012
+ l_j = length * maths.sqrt(k_e / k_j) * sine_anglv * sine_angla
3013
+ l_k = length * maths.sqrt(k_e / k_k) * cosine_anglv
3014
+ l_p = maths.sqrt(l_i * l_i + l_j * l_j + l_k * l_k)
3015
+ kh = k_e * l_p
3016
+ return kh
2943
3017
 
2944
- df = self.dataframe(one_based = True,
2945
- extra_columns_list = extra_columns_list,
2946
- ntg_uuid = ntg_uuid,
2947
- perm_i_uuid = perm_i_uuid,
2948
- perm_j_uuid = perm_j_uuid,
2949
- perm_k_uuid = perm_k_uuid,
2950
- satw_uuid = satw_uuid,
2951
- sato_uuid = sato_uuid,
2952
- satg_uuid = satg_uuid,
2953
- region_uuid = region_uuid,
2954
- radw = radw,
2955
- skin = skin,
2956
- stat = stat,
2957
- active_only = active_only,
2958
- min_k0 = min_k0,
2959
- max_k0 = max_k0,
2960
- k0_list = k0_list,
2961
- min_length = min_length,
2962
- min_kh = min_kh,
2963
- max_depth = max_depth,
2964
- max_satw = max_satw,
2965
- min_sato = min_sato,
2966
- max_satg = max_satg,
2967
- perforation_list = perforation_list,
2968
- region_list = region_list,
2969
- depth_inc_down = depth_inc_down,
2970
- set_k_face_intervals_vertical = set_k_face_intervals_vertical,
2971
- anglv_ref = anglv_ref,
2972
- angla_plane_ref = angla_plane_ref,
2973
- length_mode = length_mode,
2974
- length_uom = length_uom,
2975
- preferential_perforation = preferential_perforation,
2976
- use_properties = use_properties,
2977
- property_time_index = property_time_index)
3018
+ @staticmethod
3019
+ def __get_pc_arrays_for_interval(pc, pc_timeless, pc_titles, ci, length, radw, skin, stat, length_uom, grid,
3020
+ traj_crs):
3021
+ """Get the property collection arrays for the interval."""
2978
3022
 
2979
- sep = ' ' if space_instead_of_tab_separator else '\t'
3023
+ def get_item(v, title, pc_titles, pc, pc_timeless, ci, uom):
2980
3024
 
2981
- with open(wellspec_file, mode = mode) as fp:
2982
- for _ in range(preceeding_blank_lines):
2983
- fp.write('\n')
3025
+ def pk_for_title(title):
3026
+ d = {
3027
+ 'RADW': 'wellbore radius',
3028
+ 'RADB': 'block equivalent radius',
3029
+ 'SKIN': 'skin',
3030
+ 'STAT': 'well connection open'
3031
+ }
3032
+ return d.get(title)
2984
3033
 
2985
- self.__write_wellspec_file_units_metadata(write_nexus_units = write_nexus_units,
2986
- fp = fp,
2987
- length_uom = length_uom,
2988
- length_uom_comment = length_uom_comment,
2989
- extra_columns_list = extra_columns_list,
2990
- well_name = well_name)
3034
+ p = None
3035
+ pk = pk_for_title(title)
3036
+ pc_uom = None
3037
+ for try_pc in [pc, pc_timeless]:
3038
+ if try_pc is None:
3039
+ continue
3040
+ if title in pc_titles:
3041
+ p = try_pc.singleton(citation_title = title)
3042
+ if p is None and pk is not None:
3043
+ p = try_pc.singleton(property_kind = pk)
3044
+ if p is not None:
3045
+ v = try_pc.cached_part_array_ref(p)[ci]
3046
+ pc_uom = try_pc.uom_for_part(p)
3047
+ break
3048
+ if (title == 'STAT' or pk == 'well connection open') and v is not None and not isinstance(v, str):
3049
+ v = 'ON' if v else 'OFF'
3050
+ if pc_uom is not None and uom is not None and pc_uom != uom:
3051
+ v = wam.convert_lengths(v, pc_uom, uom)
3052
+ return v
2991
3053
 
2992
- BlockedWell.__write_wellspec_file_columns(df = df, fp = fp, col_width_dict = col_width_dict, sep = sep)
3054
+ if length_uom is None:
3055
+ l_uom = traj_crs.z_units
3056
+ r_uom = grid.crs.xy_units
3057
+ else:
3058
+ l_uom = length_uom
3059
+ r_uom = length_uom
3060
+ length = get_item(length, 'LENGTH', pc_titles, pc, pc_timeless, ci, l_uom)
3061
+ radw = get_item(radw, 'RADW', pc_titles, pc, pc_timeless, ci, r_uom)
3062
+ stat = get_item(stat, 'STAT', pc_titles, pc, pc_timeless, ci, None)
3063
+ assert radw is None or radw > 0.0 # todo: allow zero for inactive intervals?
3064
+ skin = get_item(skin, 'SKIN', pc_titles, pc, pc_timeless, ci, None)
3065
+ if skin is None:
3066
+ skin = get_item(None, 'skin', pc_titles, pc, pc_timeless, ci, None)
3067
+ radb = get_item(None, 'RADB', pc_titles, pc, pc_timeless, ci, r_uom)
3068
+ if radb is None:
3069
+ radb = get_item(None, 'block equivalent radius', pc_titles, pc, pc_timeless, ci, r_uom)
3070
+ wi = get_item(None, 'WI', pc_titles, pc, pc_timeless, ci, None)
3071
+ wbc = get_item(None, 'WBC', pc_titles, pc, pc_timeless, ci, None)
2993
3072
 
2994
- fp.write('\n')
3073
+ return length, radw, skin, radb, wi, wbc, stat
2995
3074
 
2996
- BlockedWell.__write_wellspec_file_rows_from_dataframe(df = df,
2997
- fp = fp,
2998
- col_width_dict = col_width_dict,
2999
- sep = sep)
3000
- for _ in range(trailing_blank_lines):
3001
- fp.write('\n')
3075
+ @staticmethod
3076
+ def __get_well_inflow_parameters_for_interval(do_well_inflow, isotropic_perm, ntg_is_one, k_i, k_j, k_k, sine_anglv,
3077
+ cosine_anglv, sine_angla, cosine_angla, grid, cell_kji0, radw, radb,
3078
+ wi, wbc, skin, kh, length_uom, column_list):
3002
3079
 
3003
- return df
3080
+ if do_well_inflow:
3081
+ if not length_uom:
3082
+ length_uom = grid.crs.z_units
3083
+ k_ei, k_ej, k_ek, radw_e = BlockedWell.__calculate_ke_and_radw_e(isotropic_perm = isotropic_perm,
3084
+ ntg_is_one = ntg_is_one,
3085
+ radw = radw,
3086
+ k_i = k_i,
3087
+ k_j = k_j,
3088
+ k_k = k_k,
3089
+ sine_anglv = sine_anglv,
3090
+ cosine_anglv = cosine_anglv,
3091
+ sine_angla = sine_angla,
3092
+ cosine_angla = cosine_angla)
3004
3093
 
3005
- @staticmethod
3006
- def __tidy_well_name(well_name):
3007
- nexus_friendly = ''
3008
- previous_underscore = False
3009
- for ch in well_name:
3010
- if not 32 <= ord(ch) < 128 or ch in ' ,!*#':
3011
- ch = '_'
3012
- if not (previous_underscore and ch == '_'):
3013
- nexus_friendly += ch
3014
- previous_underscore = (ch == '_')
3015
- if not nexus_friendly:
3016
- well_name = 'WELL_X'
3017
- return nexus_friendly
3094
+ cell_axial_vectors = grid.interface_vectors_kji(cell_kji0)
3095
+ wam.convert_lengths(cell_axial_vectors[..., :2], grid.crs.xy_units, length_uom)
3096
+ wam.convert_lengths(cell_axial_vectors[..., 2], grid.crs.z_units, length_uom)
3097
+ d2 = np.empty(3)
3098
+ for axis in range(3):
3099
+ d2[axis] = np.sum(cell_axial_vectors[axis] * cell_axial_vectors[axis])
3100
+ if radb is None:
3101
+ radb_e = BlockedWell.__calculate_radb_e(k_ei = k_ei,
3102
+ k_ej = k_ej,
3103
+ k_ek = k_ek,
3104
+ k_i = k_i,
3105
+ k_j = k_j,
3106
+ k_k = k_k,
3107
+ d2 = d2,
3108
+ sine_anglv = sine_anglv,
3109
+ cosine_anglv = cosine_anglv,
3110
+ sine_angla = sine_angla,
3111
+ cosine_angla = cosine_angla)
3112
+ radb = radw * radb_e / radw_e
3113
+ log.debug(f'RADB value calculated in BlockedWell dataframe method as: {radb}')
3114
+ if wi is None:
3115
+ wi = 0.0 if radb <= 0.0 else 2.0 * maths.pi / (maths.log(radb / radw) + skin)
3116
+ if 'WBC' in column_list and wbc is None:
3117
+ assert length_uom == 'm' or length_uom.startswith('ft'), \
3118
+ 'WBC only calculable for length uom of m or ft*'
3119
+ conversion_constant = 8.5270171e-5 if length_uom == 'm' else 0.006328286
3120
+ wbc = conversion_constant * kh * wi # note: pperf aleady accounted for in kh
3018
3121
 
3019
- @staticmethod
3020
- def __is_float_column(col_name):
3021
- if col_name.upper() in [
3022
- 'ANGLA', 'ANGLV', 'LENGTH', 'KH', 'DEPTH', 'MD', 'X', 'Y', 'SKIN', 'RADW', 'RADB', 'PPERF'
3023
- ]:
3024
- return True
3025
- return False
3122
+ return radb, wi, wbc
3026
3123
 
3027
3124
  @staticmethod
3028
- def __is_int_column(col_name):
3029
- if col_name.upper() in ['IW', 'JW', 'L']:
3030
- return True
3031
- return False
3125
+ def __calculate_ke_and_radw_e(isotropic_perm, ntg_is_one, radw, k_i, k_j, k_k, sine_anglv, cosine_anglv, sine_angla,
3126
+ cosine_angla):
3032
3127
 
3033
- def __get_well_name(self, well_name):
3034
- """Get the name of the well whose data is to be written to the Nexus WELLSPEC file."""
3128
+ if isotropic_perm and ntg_is_one:
3129
+ k_ei = k_ej = k_ek = k_i
3130
+ radw_e = radw
3131
+ else:
3132
+ k_ei = maths.sqrt(k_j * k_k)
3133
+ k_ej = maths.sqrt(k_i * k_k)
3134
+ k_ek = maths.sqrt(k_i * k_j)
3135
+ r_wi = 0.0 if k_ei == 0.0 else 0.5 * radw * (maths.sqrt(k_ei / k_j) + maths.sqrt(k_ei / k_k))
3136
+ r_wj = 0.0 if k_ej == 0.0 else 0.5 * radw * (maths.sqrt(k_ej / k_i) + maths.sqrt(k_ej / k_k))
3137
+ r_wk = 0.0 if k_ek == 0.0 else 0.5 * radw * (maths.sqrt(k_ek / k_i) + maths.sqrt(k_ek / k_j))
3138
+ rwi = r_wi * sine_anglv * cosine_angla
3139
+ rwj = r_wj * sine_anglv * sine_angla
3140
+ rwk = r_wk * cosine_anglv
3141
+ radw_e = maths.sqrt(rwi * rwi + rwj * rwj + rwk * rwk)
3142
+ if radw_e == 0.0:
3143
+ radw_e = radw # no permeability in this situation anyway
3035
3144
 
3036
- if not well_name:
3037
- if self.well_name:
3038
- well_name = self.well_name
3039
- elif self.root is not None:
3040
- well_name = rqet.citation_title_for_node(self.root)
3041
- elif self.wellbore_interpretation is not None:
3042
- well_name = self.wellbore_interpretation.title
3043
- elif self.trajectory is not None:
3044
- well_name = self.trajectory.title
3045
- if not well_name:
3046
- log.warning('no well name identified for use in WELLSPEC')
3047
- well_name = 'WELLNAME'
3048
- well_name = BlockedWell.__tidy_well_name(well_name)
3145
+ return k_ei, k_ej, k_ek, radw_e
3049
3146
 
3050
- return well_name
3147
+ @staticmethod
3148
+ def __calculate_radb_e(k_ei, k_ej, k_ek, k_i, k_j, k_k, d2, sine_anglv, cosine_anglv, sine_angla, cosine_angla):
3051
3149
 
3052
- def __write_wellspec_file_units_metadata(self, write_nexus_units, fp, length_uom, length_uom_comment,
3053
- extra_columns_list, well_name):
3054
- # write the units of measure (uom) and system of measure for length in the WELLSPEC file
3055
- # also write a comment on the length uom if necessary
3150
+ r_bi = 0.0 if k_ei == 0.0 else 0.14 * maths.sqrt(k_ei * (d2[1] / k_j + d2[0] / k_k))
3151
+ r_bj = 0.0 if k_ej == 0.0 else 0.14 * maths.sqrt(k_ej * (d2[2] / k_i + d2[0] / k_k))
3152
+ r_bk = 0.0 if k_ek == 0.0 else 0.14 * maths.sqrt(k_ek * (d2[2] / k_i + d2[1] / k_j))
3153
+ rbi = r_bi * sine_anglv * cosine_angla
3154
+ rbj = r_bj * sine_anglv * sine_angla
3155
+ rbk = r_bk * cosine_anglv
3156
+ radb_e = maths.sqrt(rbi * rbi + rbj * rbj + rbk * rbk)
3056
3157
 
3057
- if write_nexus_units:
3058
- length_uom_system_list = ['METRIC', 'ENGLISH']
3059
- length_uom_index = ['m', 'ft'].index(length_uom)
3060
- fp.write(f'{length_uom_system_list[length_uom_index]}\n\n')
3158
+ return radb_e
3061
3159
 
3062
- if length_uom_comment and self.trajectory is not None and ('LENGTH' in extra_columns_list or 'MD'
3063
- in extra_columns_list or 'KH' in extra_columns_list):
3064
- fp.write(f'! Length units along wellbore: {self.trajectory.md_uom if length_uom is None else length_uom}\n')
3065
- fp.write('WELLSPEC ' + str(well_name) + '\n')
3160
+ def __get_xyz_for_interval(self, doing_xyz, length_mode, length_uom, md, traj_crs, depth_inc_down, traj_z_inc_down,
3161
+ entry_xyz, exit_xyz, ee_crs, pc, pc_titles, ci):
3162
+ """Get the x, y and z location of the midpoint of the interval."""
3066
3163
 
3067
- @staticmethod
3068
- def __write_wellspec_file_columns(df, fp, col_width_dict, sep):
3069
- """Write the column names to the WELLSPEC file."""
3070
- for col_name in df.columns:
3071
- if col_name in col_width_dict:
3072
- width = col_width_dict[col_name]
3073
- else:
3074
- width = 10
3075
- form = '{0:>' + str(width) + '}'
3076
- fp.write(sep + form.format(col_name))
3164
+ xyz = (np.nan, np.nan, np.nan)
3165
+ if doing_xyz:
3166
+ xyz = self.__get_xyz_if_doing_xyz(length_mode = length_mode,
3167
+ md = md,
3168
+ length_uom = length_uom,
3169
+ traj_crs = traj_crs,
3170
+ depth_inc_down = depth_inc_down,
3171
+ traj_z_inc_down = traj_z_inc_down,
3172
+ entry_xyz = entry_xyz,
3173
+ exit_xyz = exit_xyz,
3174
+ ee_crs = ee_crs)
3175
+ xyz = np.array(xyz)
3176
+ for i, col_header in enumerate(['X', 'Y', 'DEPTH']):
3177
+ if col_header in pc_titles:
3178
+ xyz[i] = pc.single_array_ref(citation_title = col_header)[ci]
3077
3179
 
3078
- @staticmethod
3079
- def __write_wellspec_file_rows_from_dataframe(df, fp, col_width_dict, sep):
3080
- """Writes the non-blank lines of a Nexus WELLSPEC file from a BlockedWell dataframe."""
3180
+ return xyz
3081
3181
 
3082
- for row_info in df.iterrows():
3083
- row = row_info[1]
3084
- for col_name in df.columns:
3085
- try:
3086
- if col_name in col_width_dict:
3087
- width = col_width_dict[col_name]
3088
- else:
3089
- width = 10
3090
- if BlockedWell.__is_float_column(col_name):
3091
- form = '{0:>' + str(width) + '.3f}'
3092
- value = row[col_name]
3093
- if col_name == 'ANGLA' and (pd.isna(row[col_name]) or value is None or np.isnan(value)):
3094
- value = 0.0
3095
- fp.write(sep + form.format(float(value)))
3096
- else:
3097
- form = '{0:>' + str(width) + '}'
3098
- if BlockedWell.__is_int_column(col_name):
3099
- fp.write(sep + form.format(int(row[col_name])))
3100
- elif col_name == 'STAT':
3101
- fp.write(sep + form.format('OFF' if str(row['STAT']).upper() in ['0', 'OFF'] else 'ON'))
3102
- else:
3103
- fp.write(sep + form.format(str(row[col_name])))
3104
- except Exception:
3105
- fp.write(sep + str(row[col_name]))
3106
- fp.write('\n')
3182
+ def __get_xyz_if_doing_xyz(self, length_mode, md, length_uom, traj_crs, depth_inc_down, traj_z_inc_down, exit_xyz,
3183
+ entry_xyz, ee_crs):
3107
3184
 
3108
- def kji0_marker(self, active_only = True):
3109
- """Convenience method returning (k0, j0, i0), grid_uuid of first blocked interval."""
3185
+ if length_mode == 'MD' and self.trajectory is not None:
3186
+ xyz = self.trajectory.xyz_for_md(md)
3187
+ if length_uom is not None and length_uom != self.trajectory.md_uom:
3188
+ wam.convert_lengths(xyz, traj_crs.z_units, length_uom)
3189
+ if depth_inc_down and traj_z_inc_down is False:
3190
+ xyz[2] = -xyz[2]
3191
+ else:
3192
+ xyz = 0.5 * (np.array(exit_xyz) + np.array(entry_xyz))
3193
+ if length_uom is not None and length_uom != ee_crs.z_units:
3194
+ xyz[2] = wam.convert_lengths(xyz[2], ee_crs.z_units, length_uom)
3195
+ if depth_inc_down != ee_crs.z_inc_down:
3196
+ xyz[2] = -xyz[2]
3110
3197
 
3111
- cells, grids = self.cell_indices_and_grid_list()
3112
- if cells is None or grids is None or len(grids) == 0:
3113
- return None, None, None, None
3114
- return cells[0], grids[0].uuid
3198
+ return xyz
3115
3199
 
3116
- def xyz_marker(self, active_only = True):
3117
- """Convenience method returning (x, y, z), crs_uuid of perforation in first blocked interval.
3200
+ def __get_md_array_in_correct_units_for_interval(self, md, length_uom, pc, pc_titles, ci):
3201
+ """Convert the measured depth to the correct units or get the measured depth from the property collection."""
3118
3202
 
3119
- notes:
3120
- active_only argument not yet in use;
3121
- returns None, None if no blocked interval found
3122
- """
3203
+ if 'MD' in pc_titles:
3204
+ md = pc.single_array_ref(citation_title = 'MD')[ci]
3205
+ elif length_uom is not None and self.trajectory is not None and length_uom != self.trajectory.md_uom:
3206
+ md = wam.convert_lengths(md, self.trajectory.md_uom, length_uom)
3123
3207
 
3124
- cells, grids = self.cell_indices_and_grid_list()
3125
- if cells is None or grids is None or len(grids) == 0:
3126
- return None, None
3127
- node_index = 0
3128
- while node_index < self.node_count - 1 and self.grid_indices[node_index] == -1:
3129
- node_index += 1
3130
- if node_index >= self.node_count - 1:
3131
- return None, None
3132
- md = 0.5 * (self.node_mds[node_index] + self.node_mds[node_index + 1])
3133
- xyz = self.trajectory.xyz_for_md(md)
3134
- return xyz, self.trajectory.crs_uuid
3208
+ return md
3135
3209
 
3136
- def create_feature_and_interpretation(self, shared_interpretation = True):
3137
- """Instantiate new empty WellboreFeature and WellboreInterpretation objects.
3210
+ @staticmethod
3211
+ def __append_interval_data_to_dataframe(df, grid_name, radw, skin, angla, anglv, length, kh, xyz, md, stat,
3212
+ part_perf_fraction, radb, wi, wbc, column_list, one_based, row_dict,
3213
+ cell_kji0, row_ci_list, ci):
3214
+ """Append the row of data corresponding to the interval to the dataframe."""
3138
3215
 
3139
- note:
3140
- uses the Blocked well citation title or other related object title as the well name
3141
- """
3142
- title = self.well_name
3143
- if not title:
3144
- title = self.title
3145
- if not title and self.trajectory is not None:
3146
- title = rqw.well_name(self.trajectory)
3147
- if not title:
3148
- title = 'WELL'
3149
- if self.trajectory is not None:
3150
- traj_interp_uuid = self.model.uuid(obj_type = 'WellboreInterpretation', related_uuid = self.trajectory.uuid)
3151
- if traj_interp_uuid is not None:
3152
- if shared_interpretation:
3153
- self.wellbore_interpretation = rqo.WellboreInterpretation(parent_model = self.model,
3154
- uuid = traj_interp_uuid)
3155
- traj_feature_uuid = self.model.uuid(obj_type = 'WellboreFeature', related_uuid = traj_interp_uuid)
3156
- if traj_feature_uuid is not None:
3157
- self.wellbore_feature = rqo.WellboreFeature(parent_model = self.model, uuid = traj_feature_uuid)
3158
- if self.wellbore_feature is None:
3159
- self.wellbore_feature = rqo.WellboreFeature(parent_model = self.model, feature_name = title)
3160
- self.feature_to_be_written = True
3161
- if self.wellbore_interpretation is None:
3162
- title = title if not self.wellbore_feature.title else self.wellbore_feature.title
3163
- self.wellbore_interpretation = rqo.WellboreInterpretation(parent_model = self.model,
3164
- title = title,
3165
- wellbore_feature = self.wellbore_feature)
3166
- if self.trajectory.wellbore_interpretation is None and shared_interpretation:
3167
- self.trajectory.wellbore_interpretation = self.wellbore_interpretation
3168
- self.interpretation_to_be_written = True
3216
+ column_names = [
3217
+ 'GRID', 'RADW', 'SKIN', 'ANGLA', 'ANGLV', 'LENGTH', 'KH', 'DEPTH', 'MD', 'X', 'Y', 'STAT', 'PPERF', 'RADB',
3218
+ 'WI', 'WBC'
3219
+ ]
3220
+ column_values = [
3221
+ grid_name, radw, skin, angla, anglv, length, kh, xyz[2], md, xyz[0], xyz[1], stat, part_perf_fraction, radb,
3222
+ wi, wbc
3223
+ ]
3224
+ column_values_dict = dict(zip(column_names, column_values))
3169
3225
 
3170
- def create_md_datum_and_trajectory(self,
3171
- grid,
3172
- trajectory_mds,
3173
- trajectory_points,
3174
- length_uom,
3175
- well_name,
3176
- set_depth_zero = False,
3177
- set_tangent_vectors = False,
3178
- create_feature_and_interp = True):
3179
- """Creates an Md Datum object and a (simulation) Trajectory object for this blocked well.
3226
+ data = df.to_dict()
3227
+ data = {k: list(v.values()) for k, v in data.items()}
3228
+ for col_index, col in enumerate(column_list):
3229
+ if col_index < 3:
3230
+ if one_based:
3231
+ row_dict[col] = [cell_kji0[2 - col_index] + 1]
3232
+ else:
3233
+ row_dict[col] = [cell_kji0[2 - col_index]]
3234
+ else:
3235
+ row_dict[col] = [column_values_dict[col]]
3180
3236
 
3181
- note:
3182
- not usually called directly; used by import methods
3183
- """
3237
+ for col, vals in row_dict.items():
3238
+ if col in data:
3239
+ data[col].extend(vals)
3240
+ else:
3241
+ data[col] = vals
3242
+ df = pd.DataFrame(data)
3184
3243
 
3185
- if not well_name:
3186
- well_name = self.title
3244
+ row_ci_list.append(ci)
3187
3245
 
3188
- # create md datum node for synthetic trajectory, using crs for grid
3189
- datum_location = trajectory_points[0].copy()
3190
- if set_depth_zero:
3191
- datum_location[2] = 0.0
3192
- datum = rqw.MdDatum(self.model,
3193
- crs_uuid = grid.crs_uuid,
3194
- location = datum_location,
3195
- md_reference = 'mean sea level')
3246
+ return df
3196
3247
 
3197
- # create synthetic trajectory object, using crs for grid
3198
- trajectory_mds_array = np.array(trajectory_mds)
3199
- trajectory_xyz_array = np.array(trajectory_points)
3200
- trajectory_df = pd.DataFrame({
3201
- 'MD': trajectory_mds_array,
3202
- 'X': trajectory_xyz_array[..., 0],
3203
- 'Y': trajectory_xyz_array[..., 1],
3204
- 'Z': trajectory_xyz_array[..., 2]
3205
- })
3206
- self.trajectory = rqw.Trajectory(self.model,
3207
- md_datum = datum,
3208
- data_frame = trajectory_df,
3209
- length_uom = length_uom,
3210
- well_name = well_name,
3211
- set_tangent_vectors = set_tangent_vectors)
3212
- self.trajectory_to_be_written = True
3248
+ def __add_as_properties(self,
3249
+ df,
3250
+ add_as_properties,
3251
+ extra_columns_list,
3252
+ length_uom,
3253
+ time_index = None,
3254
+ time_series_uuid = None):
3255
+ """Adds property parts from df with columns listed in add_as_properties or extra_columns_list."""
3213
3256
 
3214
- if create_feature_and_interp:
3215
- self.create_feature_and_interpretation()
3257
+ if add_as_properties:
3258
+ if isinstance(add_as_properties, list):
3259
+ for col in add_as_properties:
3260
+ assert col in extra_columns_list
3261
+ property_columns = add_as_properties
3262
+ else:
3263
+ property_columns = extra_columns_list
3264
+ self.add_df_properties(df,
3265
+ property_columns,
3266
+ length_uom = length_uom,
3267
+ time_index = time_index,
3268
+ time_series_uuid = time_series_uuid)
3216
3269
 
3217
- def create_xml(self,
3218
- ext_uuid = None,
3219
- create_for_trajectory_if_needed = True,
3220
- add_as_part = True,
3221
- add_relationships = True,
3222
- title = None,
3223
- originator = None):
3224
- """Create a blocked wellbore representation node from this BlockedWell object, optionally add as part.
3270
+ def _get_uom_pk_discrete_for_df_properties(self, extra, length_uom, temperature_uom = None):
3271
+ """Set the property kind and unit of measure for all properties in the dataframe."""
3225
3272
 
3226
- note:
3227
- trajectory xml node must be in place before calling this function;
3228
- witsml log reference, interval stratigraphic units, and cell fluid phase units not yet supported
3273
+ # todo: this is horribly inefficient, building a whole dictionary for every call but only using one entry
3274
+ if length_uom not in ['m', 'ft']:
3275
+ raise ValueError(f"The length_uom {length_uom} must be either 'm' or 'ft'.")
3276
+ if extra == 'TEMP' and (temperature_uom is None or
3277
+ temperature_uom not in wam.valid_uoms('thermodynamic temperature')):
3278
+ raise ValueError(f"The temperature_uom must be in {wam.valid_uoms('thermodynamic temperature')}.")
3229
3279
 
3230
- :meta common:
3231
- """
3280
+ length_uom_pk_discrete = self._get_uom_pk_discrete_for_length_based_properties(length_uom = length_uom,
3281
+ extra = extra)
3282
+ uom_pk_discrete_dict = {
3283
+ 'ANGLA': ('dega', 'azimuth', False),
3284
+ 'ANGLV': ('dega', 'inclination', False),
3285
+ 'KH': (f'mD.{length_uom}', 'permeability length', False),
3286
+ 'PPERF': (f'{length_uom}/{length_uom}', 'perforation fraction', False),
3287
+ 'STAT': (None, 'well connection open', True),
3288
+ 'LENGTH': length_uom_pk_discrete,
3289
+ 'MD': (length_uom, 'measured depth', False),
3290
+ 'X': length_uom_pk_discrete,
3291
+ 'Y': length_uom_pk_discrete,
3292
+ 'DEPTH': (length_uom, 'depth', False),
3293
+ 'RADW': (length_uom, 'wellbore radius', False),
3294
+ 'RADB': (length_uom, 'block equivalent radius', False),
3295
+ 'RADBP': length_uom_pk_discrete,
3296
+ 'RADWP': length_uom_pk_discrete,
3297
+ 'FM': (f'{length_uom}/{length_uom}', 'matrix fraction', False),
3298
+ 'IRELPM': (None, 'relative permeability index', True), # TODO: change to 'region initialization' with facet
3299
+ 'SECT': (None, 'wellbore section index', True),
3300
+ 'LAYER': (None, 'layer index', True),
3301
+ 'ANGLE': ('dega', 'plane angle', False),
3302
+ 'TEMP': (temperature_uom, 'thermodynamic temperature', False),
3303
+ 'MDCON': length_uom_pk_discrete,
3304
+ 'K': ('mD', 'rock permeability', False),
3305
+ 'DZ': (length_uom, 'cell length', False), # TODO: add direction facet
3306
+ 'DTOP': (length_uom, 'depth', False),
3307
+ 'DBOT': (length_uom, 'depth', False),
3308
+ 'SKIN': ('Euc', 'skin', False),
3309
+ 'WI': ('Euc', 'well connection index', False),
3310
+ }
3311
+ return uom_pk_discrete_dict.get(extra, ('Euc', 'generic continuous', False))
3232
3312
 
3233
- assert self.trajectory is not None, 'trajectory object missing'
3313
+ def _get_uom_pk_discrete_for_length_based_properties(self, length_uom, extra):
3314
+ if length_uom is None or length_uom == 'Euc':
3315
+ if extra in ['LENGTH', 'MD', 'MDCON']:
3316
+ uom = self.trajectory.md_uom
3317
+ elif extra in ['X', 'Y', 'RADW', 'RADB', 'RADBP', 'RADWP']:
3318
+ uom = self.grid_list[0].xy_units()
3319
+ else:
3320
+ uom = self.grid_list[0].z_units()
3321
+ else:
3322
+ uom = length_uom
3323
+ if extra == 'DEPTH':
3324
+ pk = 'depth'
3325
+ else:
3326
+ pk = 'length'
3327
+ return uom, pk, False
3234
3328
 
3235
- if ext_uuid is None:
3236
- ext_uuid = self.model.h5_uuid()
3329
+ @staticmethod
3330
+ def __tidy_well_name(well_name):
3331
+ nexus_friendly = ''
3332
+ previous_underscore = False
3333
+ for ch in well_name:
3334
+ if not 32 <= ord(ch) < 128 or ch in ' ,!*#':
3335
+ ch = '_'
3336
+ if not (previous_underscore and ch == '_'):
3337
+ nexus_friendly += ch
3338
+ previous_underscore = (ch == '_')
3339
+ if not nexus_friendly:
3340
+ well_name = 'WELL_X'
3341
+ return nexus_friendly
3237
3342
 
3238
- if title:
3239
- self.title = title
3240
- if not self.title:
3241
- self.title = self.well_name
3242
- title = self.title
3343
+ @staticmethod
3344
+ def __is_float_column(col_name):
3345
+ if col_name.upper() in [
3346
+ 'ANGLA', 'ANGLV', 'LENGTH', 'KH', 'DEPTH', 'MD', 'X', 'Y', 'SKIN', 'RADW', 'RADB', 'PPERF'
3347
+ ]:
3348
+ return True
3349
+ return False
3243
3350
 
3244
- self.__create_wellbore_feature_and_interpretation_xml_if_needed(add_as_part = add_as_part,
3245
- add_relationships = add_relationships,
3246
- originator = originator)
3351
+ @staticmethod
3352
+ def __is_int_column(col_name):
3353
+ if col_name.upper() in ['IW', 'JW', 'L']:
3354
+ return True
3355
+ return False
3247
3356
 
3248
- self.__create_trajectory_xml_if_needed(create_for_trajectory_if_needed = create_for_trajectory_if_needed,
3249
- add_as_part = add_as_part,
3250
- add_relationships = add_relationships,
3251
- originator = originator,
3252
- ext_uuid = ext_uuid,
3253
- title = title)
3357
+ def __get_well_name(self, well_name):
3358
+ """Get the name of the well whose data is to be written to the Nexus WELLSPEC file."""
3254
3359
 
3255
- assert self.trajectory.root is not None, 'trajectory xml not established'
3360
+ if not well_name:
3361
+ if self.well_name:
3362
+ well_name = self.well_name
3363
+ elif self.root is not None:
3364
+ well_name = rqet.citation_title_for_node(self.root)
3365
+ elif self.wellbore_interpretation is not None:
3366
+ well_name = self.wellbore_interpretation.title
3367
+ elif self.trajectory is not None:
3368
+ well_name = self.trajectory.title
3369
+ if not well_name:
3370
+ log.warning('no well name identified for use in WELLSPEC')
3371
+ well_name = 'WELLNAME'
3372
+ well_name = BlockedWell.__tidy_well_name(well_name)
3256
3373
 
3257
- bw_node = super().create_xml(title = title, originator = originator, add_as_part = False)
3374
+ return well_name
3258
3375
 
3259
- # wellbore frame elements
3376
+ def __write_wellspec_file_units_metadata(self, write_nexus_units, fp, length_uom, length_uom_comment,
3377
+ extra_columns_list, well_name):
3378
+ # write the units of measure (uom) and system of measure for length in the WELLSPEC file
3379
+ # also write a comment on the length uom if necessary
3260
3380
 
3261
- nc_node, mds_node, mds_values_node, cc_node, cis_node, cnull_node, cis_values_node, gis_node, gnull_node, \
3262
- gis_values_node, fis_node, fnull_node, fis_values_node = \
3263
- self.__create_bw_node_sub_elements(bw_node = bw_node)
3381
+ if write_nexus_units:
3382
+ length_uom_system_list = ['METRIC', 'ENGLISH']
3383
+ length_uom_index = ['m', 'ft'].index(length_uom)
3384
+ fp.write(f'{length_uom_system_list[length_uom_index]}\n\n')
3264
3385
 
3265
- self.__create_hdf5_dataset_references(ext_uuid = ext_uuid,
3266
- mds_values_node = mds_values_node,
3267
- cis_values_node = cis_values_node,
3268
- gis_values_node = gis_values_node,
3269
- fis_values_node = fis_values_node)
3386
+ if length_uom_comment and self.trajectory is not None and ('LENGTH' in extra_columns_list or 'MD'
3387
+ in extra_columns_list or 'KH' in extra_columns_list):
3388
+ fp.write(f'! Length units along wellbore: {self.trajectory.md_uom if length_uom is None else length_uom}\n')
3389
+ fp.write('WELLSPEC ' + str(well_name) + '\n')
3270
3390
 
3271
- traj_root, grid_root, interp_root = self.__create_trajectory_grid_wellbore_interpretation_reference_nodes(
3272
- bw_node = bw_node)
3391
+ @staticmethod
3392
+ def __write_wellspec_file_columns(df, fp, col_width_dict, sep):
3393
+ """Write the column names to the WELLSPEC file."""
3394
+ for col_name in df.columns:
3395
+ if col_name in col_width_dict:
3396
+ width = col_width_dict[col_name]
3397
+ else:
3398
+ width = 10
3399
+ form = '{0:>' + str(width) + '}'
3400
+ fp.write(sep + form.format(col_name))
3273
3401
 
3274
- self.__add_as_part_and_add_relationships_if_required(add_as_part = add_as_part,
3275
- add_relationships = add_relationships,
3276
- bw_node = bw_node,
3277
- interp_root = interp_root,
3278
- ext_uuid = ext_uuid)
3402
+ @staticmethod
3403
+ def __write_wellspec_file_rows_from_dataframe(df, fp, col_width_dict, sep):
3404
+ """Writes the non-blank lines of a Nexus WELLSPEC file from a BlockedWell dataframe."""
3279
3405
 
3280
- return bw_node
3406
+ for row_info in df.iterrows():
3407
+ row = row_info[1]
3408
+ for col_name in df.columns:
3409
+ try:
3410
+ if col_name in col_width_dict:
3411
+ width = col_width_dict[col_name]
3412
+ else:
3413
+ width = 10
3414
+ if BlockedWell.__is_float_column(col_name):
3415
+ form = '{0:>' + str(width) + '.3f}'
3416
+ value = row[col_name]
3417
+ if col_name == 'ANGLA' and (pd.isna(row[col_name]) or value is None or np.isnan(value)):
3418
+ value = 0.0
3419
+ fp.write(sep + form.format(float(value)))
3420
+ else:
3421
+ form = '{0:>' + str(width) + '}'
3422
+ if BlockedWell.__is_int_column(col_name):
3423
+ fp.write(sep + form.format(int(row[col_name])))
3424
+ elif col_name == 'STAT':
3425
+ fp.write(sep + form.format('OFF' if str(row['STAT']).upper() in ['0', 'OFF'] else 'ON'))
3426
+ else:
3427
+ fp.write(sep + form.format(str(row[col_name])))
3428
+ except Exception:
3429
+ fp.write(sep + str(row[col_name]))
3430
+ fp.write('\n')
3281
3431
 
3282
3432
  def __create_wellbore_feature_and_interpretation_xml_if_needed(self, add_as_part, add_relationships, originator):
3283
3433
  """Create root node for WellboreFeature and WellboreInterpretation objects if necessary."""
@@ -3297,6 +3447,7 @@ class BlockedWell(BaseResqpy):
3297
3447
  def __create_trajectory_xml_if_needed(self, create_for_trajectory_if_needed, add_as_part, add_relationships,
3298
3448
  originator, ext_uuid, title):
3299
3449
  """Create root node for associated Trajectory object if necessary."""
3450
+
3300
3451
  if create_for_trajectory_if_needed and self.trajectory_to_be_written and self.trajectory.root is None:
3301
3452
  md_datum_root = self.trajectory.md_datum.create_xml(add_as_part = add_as_part,
3302
3453
  add_relationships = add_relationships,
@@ -3311,6 +3462,7 @@ class BlockedWell(BaseResqpy):
3311
3462
 
3312
3463
  def __create_bw_node_sub_elements(self, bw_node):
3313
3464
  """Append sub-elements to the BlockedWell object's root node."""
3465
+
3314
3466
  nc_node = rqet.SubElement(bw_node, ns['resqml2'] + 'NodeCount')
3315
3467
  nc_node.set(ns['xsi'] + 'type', ns['xsd'] + 'positiveInteger')
3316
3468
  nc_node.text = str(self.node_count)
@@ -3369,7 +3521,8 @@ class BlockedWell(BaseResqpy):
3369
3521
  fis_values_node.set(ns['xsi'] + 'type', ns['eml'] + 'Hdf5Dataset')
3370
3522
  fis_values_node.text = rqet.null_xml_text
3371
3523
 
3372
- return nc_node, mds_node, mds_values_node, cc_node, cis_node, cnull_node, cis_values_node, gis_node, gnull_node, gis_values_node, fis_node, fnull_node, fis_values_node
3524
+ return (nc_node, mds_node, mds_values_node, cc_node, cis_node, cnull_node, cis_values_node, gis_node,
3525
+ gnull_node, gis_values_node, fis_node, fnull_node, fis_values_node)
3373
3526
 
3374
3527
  def __create_trajectory_grid_wellbore_interpretation_reference_nodes(self, bw_node):
3375
3528
  """Create nodes and add to BlockedWell object root node."""
@@ -3430,116 +3583,3 @@ class BlockedWell(BaseResqpy):
3430
3583
  ext_node = self.model.root_for_part(ext_part)
3431
3584
  self.model.create_reciprocal_relationship(bw_node, 'mlToExternalPartProxy', ext_node,
3432
3585
  'externalPartProxyToMl')
3433
-
3434
- def write_hdf5(self, file_name = None, mode = 'a', create_for_trajectory_if_needed = True):
3435
- """Create or append to an hdf5 file, writing datasets for the measured depths, grid, cell & face indices.
3436
-
3437
- :meta common:
3438
- """
3439
-
3440
- # NB: array data must all have been set up prior to calling this function
3441
-
3442
- if self.uuid is None:
3443
- self.uuid = bu.new_uuid()
3444
-
3445
- h5_reg = rwh5.H5Register(self.model)
3446
-
3447
- if create_for_trajectory_if_needed and self.trajectory_to_be_written:
3448
- self.trajectory.write_hdf5(file_name, mode = mode)
3449
- mode = 'a'
3450
-
3451
- h5_reg.register_dataset(self.uuid, 'NodeMd', self.node_mds)
3452
- h5_reg.register_dataset(self.uuid, 'CellIndices', self.cell_indices) # could use int32?
3453
- h5_reg.register_dataset(self.uuid, 'GridIndices', self.grid_indices) # could use int32?
3454
- # convert face index pairs from [axis, polarity] back to strange local face numbering
3455
- mask = (self.face_pair_indices.flatten() == -1).reshape((-1, 2)) # 2nd axis is (axis, polarity)
3456
- masked_face_indices = np.where(mask, 0, self.face_pair_indices.reshape((-1, 2))) # 2nd axis is (axis, polarity)
3457
- # using flat array for raw_face_indices array
3458
- # other resqml writing code might use an array with one int per entry point and one per exit point, with 2nd axis as (entry, exit)
3459
- raw_face_indices = np.where(mask[:, 0], -1, self.face_index_map[masked_face_indices[:, 0],
3460
- masked_face_indices[:,
3461
- 1]].flatten()).reshape(-1)
3462
-
3463
- h5_reg.register_dataset(self.uuid, 'LocalFacePairPerCellIndices', raw_face_indices) # could use uint8?
3464
-
3465
- h5_reg.write(file = file_name, mode = mode)
3466
-
3467
- def add_grid_property_to_blocked_well(self, uuid_list):
3468
- """Add properties to blocked wells from a list of uuids for properties on the supporting grid."""
3469
-
3470
- part_list = [self.model.part_for_uuid(uuid) for uuid in uuid_list]
3471
-
3472
- assert len(self.grid_list) == 1, "only blocked wells with a single grid can be handled currently"
3473
- grid = self.grid_list[0]
3474
- # filter to only those properties on the grid
3475
- parts = self.model.parts_list_filtered_by_supporting_uuid(part_list, grid.uuid)
3476
- if len(parts) < len(uuid_list):
3477
- log.warning(
3478
- f"{len(uuid_list)-len(parts)} uuids ignored as they do not belong to the same grid as the blocked well")
3479
-
3480
- gridpc = grid.extract_property_collection()
3481
- # only 'cell' properties are handled
3482
- cell_parts = [part for part in parts if gridpc.indexable_for_part(part) == 'cells']
3483
- if len(cell_parts) < len(parts):
3484
- log.warning(f"{len(parts)-len(cell_parts)} uuids ignored as they do not have indexable element of cells")
3485
-
3486
- if len(cell_parts) > 0:
3487
- bwpc = rqp.PropertyCollection(support = self)
3488
- if len(gridpc.string_lookup_uuid_list()) > 0:
3489
- sl_dict = {}
3490
- for part in cell_parts:
3491
- if gridpc.string_lookup_uuid_for_part(part) in sl_dict.keys():
3492
- sl_dict[gridpc.string_lookup_uuid_for_part(part)] = \
3493
- sl_dict[gridpc.string_lookup_uuid_for_part(part)] + [part]
3494
- else:
3495
- sl_dict[gridpc.string_lookup_uuid_for_part(part)] = [part]
3496
- else:
3497
- sl_dict = {None: cell_parts}
3498
-
3499
- sl_ts_dict = {}
3500
- for sl_uuid in sl_dict.keys():
3501
- if len(gridpc.time_series_uuid_list()) > 0:
3502
- # dictionary with keys for string_lookup uuids and None where missing
3503
- # values for each key are a list of property parts associated with that lookup uuid, or None
3504
- time_dict = {}
3505
- for part in sl_dict[sl_uuid]:
3506
- if gridpc.time_series_uuid_for_part(part) in time_dict.keys():
3507
- time_dict[gridpc.time_series_uuid_for_part(part)] = \
3508
- time_dict[gridpc.time_series_uuid_for_part(part)] + [part]
3509
- else:
3510
- time_dict[gridpc.time_series_uuid_for_part(part)] = [part]
3511
- else:
3512
- time_dict = {None: sl_dict[sl_uuid]}
3513
- sl_ts_dict[sl_uuid] = time_dict
3514
-
3515
- for sl_uuid in sl_ts_dict.keys():
3516
- time_dict = sl_ts_dict[sl_uuid]
3517
- for time_uuid in time_dict.keys():
3518
- parts = time_dict[time_uuid]
3519
- for part in parts:
3520
- array = gridpc.cached_part_array_ref(part)
3521
- indices = self.cell_indices_for_grid_uuid(grid.uuid)
3522
- bwarray = np.empty(shape = (indices.shape[0],), dtype = array.dtype)
3523
- for i, ind in enumerate(indices):
3524
- bwarray[i] = array[tuple(ind)]
3525
- bwpc.add_cached_array_to_imported_list(
3526
- bwarray,
3527
- source_info = f'property from grid {grid.title}',
3528
- keyword = gridpc.citation_title_for_part(part),
3529
- discrete = (not gridpc.continuous_for_part(part)),
3530
- uom = gridpc.uom_for_part(part),
3531
- time_index = gridpc.time_index_for_part(part),
3532
- null_value = gridpc.null_value_for_part(part),
3533
- property_kind = gridpc.property_kind_for_part(part),
3534
- local_property_kind_uuid = gridpc.local_property_kind_uuid(part),
3535
- facet_type = gridpc.facet_type_for_part(part),
3536
- facet = gridpc.facet_for_part(part),
3537
- realization = gridpc.realization_for_part(part),
3538
- indexable_element = 'cells')
3539
- bwpc.write_hdf5_for_imported_list(use_int32 = False)
3540
- bwpc.create_xml_for_imported_list_and_add_parts_to_model(time_series_uuid = time_uuid,
3541
- string_lookup_uuid = sl_uuid)
3542
- else:
3543
- log.debug(
3544
- "no properties added - uuids either not 'cell' properties or blocked well is associated with multiple grids"
3545
- )