resqpy 4.14.1__py3-none-any.whl → 5.1.5__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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 +22 -12
  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.1.dist-info → resqpy-5.1.5.dist-info}/METADATA +8 -9
  64. {resqpy-4.14.1.dist-info → resqpy-5.1.5.dist-info}/RECORD +66 -66
  65. {resqpy-4.14.1.dist-info → resqpy-5.1.5.dist-info}/WHEEL +1 -1
  66. resqpy/grid/_moved_functions.py +0 -15
  67. {resqpy-4.14.1.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
- )