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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. resqpy/__init__.py +1 -1
  2. resqpy/fault/_gcs_functions.py +10 -10
  3. resqpy/fault/_grid_connection_set.py +277 -113
  4. resqpy/grid/__init__.py +2 -3
  5. resqpy/grid/_defined_geometry.py +3 -3
  6. resqpy/grid/_extract_functions.py +2 -1
  7. resqpy/grid/_grid.py +95 -12
  8. resqpy/grid/_grid_types.py +22 -7
  9. resqpy/grid/_points_functions.py +1 -1
  10. resqpy/grid/_regular_grid.py +6 -2
  11. resqpy/grid_surface/__init__.py +17 -38
  12. resqpy/grid_surface/_blocked_well_populate.py +5 -5
  13. resqpy/grid_surface/_find_faces.py +1349 -253
  14. resqpy/lines/_polyline.py +24 -33
  15. resqpy/model/_catalogue.py +9 -0
  16. resqpy/model/_forestry.py +18 -14
  17. resqpy/model/_hdf5.py +11 -3
  18. resqpy/model/_model.py +85 -10
  19. resqpy/model/_xml.py +38 -13
  20. resqpy/multi_processing/wrappers/grid_surface_mp.py +92 -37
  21. resqpy/olio/read_nexus_fault.py +8 -2
  22. resqpy/olio/relperm.py +1 -1
  23. resqpy/olio/transmission.py +8 -8
  24. resqpy/olio/triangulation.py +36 -30
  25. resqpy/olio/vector_utilities.py +340 -6
  26. resqpy/olio/volume.py +0 -20
  27. resqpy/olio/wellspec_keywords.py +19 -13
  28. resqpy/olio/write_hdf5.py +1 -1
  29. resqpy/olio/xml_et.py +12 -0
  30. resqpy/property/__init__.py +6 -4
  31. resqpy/property/_collection_add_part.py +4 -3
  32. resqpy/property/_collection_create_xml.py +4 -2
  33. resqpy/property/_collection_get_attributes.py +4 -0
  34. resqpy/property/attribute_property_set.py +311 -0
  35. resqpy/property/grid_property_collection.py +11 -11
  36. resqpy/property/property_collection.py +79 -31
  37. resqpy/property/property_common.py +3 -8
  38. resqpy/rq_import/_add_surfaces.py +34 -14
  39. resqpy/rq_import/_grid_from_cp.py +2 -2
  40. resqpy/rq_import/_import_nexus.py +75 -48
  41. resqpy/rq_import/_import_vdb_all_grids.py +64 -52
  42. resqpy/rq_import/_import_vdb_ensemble.py +12 -13
  43. resqpy/surface/_mesh.py +4 -0
  44. resqpy/surface/_surface.py +593 -118
  45. resqpy/surface/_tri_mesh.py +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 @@ import math as maths
10
10
  import numpy as np
11
11
  import pandas as pd
12
12
 
13
+ import resqpy.grid as grr
13
14
  import resqpy.fault
14
15
  import resqpy.olio.read_nexus_fault as rnf
15
16
  import resqpy.olio.trademark as tm
@@ -118,7 +119,7 @@ class GridConnectionSet(BaseResqpy):
118
119
  self.cell_index_pairs = None #: shape (count, 2); dtype int; index normalized for flattened array
119
120
  self.cell_index_pairs_null_value = -1 #: integer null value for array above
120
121
  self.grid_index_pairs = None #: shape (count, 2); dtype int; optional; used if more than one grid referenced
121
- self.face_index_pairs = None #: shape (count, 2); dtype int32; local to cell, ie. range 0 to 5
122
+ self.face_index_pairs = None #: shape (count, 2); dtype int8; local to cell, ie. range 0 to 5
122
123
  self.face_index_pairs_null_value = -1 #: integer null value for array above
123
124
  # NB face index values 0..5 usually mean [K-, K+, J+, I+, J-, I-] respectively but there is some ambiguity
124
125
  # over I & J in the Energistics RESQML Usage Guide; see comments in DevOps backlog item 269001 for more info
@@ -129,17 +130,18 @@ class GridConnectionSet(BaseResqpy):
129
130
  self.feature_list = None #: ordered list, actually of interpretations, indexed by feature_indices
130
131
  # feature list contains tuples: (content_type, uuid, title) for fault features (or other interpretations)
131
132
  self.property_collection = None #: optional property.PropertyCollection
133
+ self.cell_index_dtype = np.int32 #: set to int64 if any grid is big, otherwise int32
132
134
 
133
135
  # NB: RESQML documentation is not clear which order is correct; should be kept consistent with same data in property.py
134
136
  # face_index_map maps from (axis, p01) to face index value in range 0..5
135
137
  # this is the default as indicated on page 139 (but not p. 180) of the RESQML Usage Gude v2.0.1
136
138
  # also assumes K is generally increasing downwards
137
139
  # see DevOps backlog item 269001 discussion for more information
138
- # self.face_index_map = np.array([[0, 1], [4, 2], [5, 3]], dtype = int)
139
- self.face_index_map = np.array([[0, 1], [2, 4], [5, 3]], dtype = int) # order: top, base, J-, I+, J+, I-
140
+ # self.face_index_map = np.array([[0, 1], [4, 2], [5, 3]], dtype = np.int8)
141
+ self.face_index_map = np.array([[0, 1], [2, 4], [5, 3]], dtype = np.int8) # order: top, base, J-, I+, J+, I-
140
142
  # and the inverse, maps from 0..5 to (axis, p01)
141
- # self.face_index_inverse_map = np.array([[0, 0], [0, 1], [1, 1], [2, 1], [1, 0], [2, 0]], dtype = int)
142
- self.face_index_inverse_map = np.array([[0, 0], [0, 1], [1, 0], [2, 1], [1, 1], [2, 0]], dtype = int)
143
+ # self.face_index_inverse_map = np.array([[0, 0], [0, 1], [1, 1], [2, 1], [1, 0], [2, 0]], dtype = np.int8)
144
+ self.face_index_inverse_map = np.array([[0, 0], [0, 1], [1, 0], [2, 1], [1, 1], [2, 0]], dtype = np.int8)
143
145
  # note: the rework_face_pairs() method, below, overwrites the face indices based on I, J cell indices
144
146
  if not title:
145
147
  title = feature_name
@@ -196,6 +198,110 @@ class GridConnectionSet(BaseResqpy):
196
198
  self.cache_arrays()
197
199
  if find_properties:
198
200
  self.extract_property_collection()
201
+ self._set_cell_index_dtype()
202
+
203
+ @classmethod
204
+ def from_faces_indices(cls,
205
+ grid,
206
+ k_faces_kji0,
207
+ j_faces_kji0,
208
+ i_faces_kji0,
209
+ remove_duplicates = True,
210
+ k_properties = None,
211
+ j_properties = None,
212
+ i_properties = None,
213
+ feature_name = None,
214
+ feature_type = 'fault',
215
+ create_organizing_objects_where_needed = True,
216
+ title = None,
217
+ originator = None,
218
+ extra_metadata = None):
219
+ """Create a GridConnectionSet given a grid and 3 list-like arrays identifying faces by indices.
220
+
221
+ arguments:
222
+ - grid (Grid): the single grid to be referenced by the grid connection set
223
+ - k_faces_kji0 (numpy int array of shape (Nk, 3)): indices of cells on negative side of desired K faces
224
+ - j_faces_kji0 (numpy int array of shape (Nj, 3)): indices of cells on negative side of desired J faces
225
+ - i_faces_kji0 (numpy int array of shape (Ni, 3)): indices of cells on negative side of desired I faces
226
+ - remove_duplicates (bool, default True): if True, indices are sorted and duplicates removed
227
+ - k_properties (list of 1D numpy arrays, optional): if present and remove_duplicates is True, each array
228
+ is sorted and has elements removed to keep them compatible with the indices
229
+ - j_properties (list of 1D numpy arrays, optional): if present and remove_duplicates is True, each array
230
+ is sorted and has elements removed to keep them compatible with the indices
231
+ - i_properties (list of 1D numpy arrays, optional): if present and remove_duplicates is True, each array
232
+ is sorted and has elements removed to keep them compatible with the indices
233
+ - feature_name (string, optional): the feature name to use when setting from faces
234
+ - feature_type (string, default 'fault'): 'fault', 'horizon' or 'geobody boundary'
235
+ - create_organizing_objects_where_needed (boolean, default True): if True, a fault interpretation object
236
+ and tectonic boundary feature object will be created if such objects do not exist for the feature;
237
+ if False, missing organizational objects will cause an error to be logged
238
+ - title (str, optional): the citation title to use for a new grid connection set
239
+ - originator (str, optional): the name of the person creating the new grid connection set, defaults to login id
240
+ - extra_metadata (dict, optional): string key, value pairs to add as extra metadata for the grid connection set
241
+
242
+ returns:
243
+ - a new GridConnectionSet populated based on the faces indices
244
+
245
+ notes:
246
+ - this method only supports creation of single grid connection sets
247
+ - the faces indices are for cells on the negative side of the face
248
+ - the paired cell is implicitly the neighbouring cell in the positive direction of the axis
249
+ - the indices must therefore not include the last cell in the axis, though this is not checked
250
+ - if properties are passed, they should be passed in list variables which have their elements
251
+ replaced; individual property arrays should therefore be extracted from the lists afterwards
252
+ """
253
+ assert isinstance(grid, grr.Grid)
254
+
255
+ gcs = cls(grid.model, title = title, originator = originator, extra_metadata = extra_metadata)
256
+
257
+ gcs._sort_out_organizing_objects(feature_type, feature_name, create_organizing_objects_where_needed)
258
+
259
+ nj_ni = grid.nj * grid.ni
260
+ if k_faces_kji0 is not None and len(k_faces_kji0) > 0:
261
+ ci = grid.natural_cell_indices(k_faces_kji0)
262
+ if remove_duplicates:
263
+ ci = _sort_and_remove_duplicates(ci, k_properties)
264
+ cip = np.empty((ci.size, 2), dtype = gcs.cell_index_dtype)
265
+ cip[:, 0] = ci
266
+ cip[:, 1] = ci + nj_ni
267
+ fip = np.empty(cip.shape, dtype = np.int8)
268
+ fip[:, 0] = gcs.face_index_map[0, 1]
269
+ fip[:, 1] = gcs.face_index_map[0, 0]
270
+ else:
271
+ cip = np.empty((0, 2), dtype = gcs.cell_index_dtype)
272
+ fip = np.empty((0, 2), dtype = np.int8)
273
+ if j_faces_kji0 is not None and len(j_faces_kji0) > 0:
274
+ ci = grid.natural_cell_indices(j_faces_kji0)
275
+ if remove_duplicates:
276
+ ci = _sort_and_remove_duplicates(ci, j_properties)
277
+ j_cip = np.empty((ci.size, 2), dtype = gcs.cell_index_dtype)
278
+ j_cip[:, 0] = ci
279
+ j_cip[:, 1] = ci + grid.ni
280
+ j_fip = np.empty(j_cip.shape, dtype = np.int8)
281
+ j_fip[:, 0] = gcs.face_index_map[1, 1]
282
+ j_fip[:, 1] = gcs.face_index_map[1, 0]
283
+ cip = np.concatenate((cip, j_cip), axis = 0)
284
+ fip = np.concatenate((fip, j_fip), axis = 0)
285
+ del j_cip, j_fip
286
+ if i_faces_kji0 is not None and len(i_faces_kji0) > 0:
287
+ ci = grid.natural_cell_indices(i_faces_kji0)
288
+ if remove_duplicates:
289
+ ci = _sort_and_remove_duplicates(ci, i_properties)
290
+ i_cip = np.empty((ci.size, 2), dtype = gcs.cell_index_dtype)
291
+ i_cip[:, 0] = ci
292
+ i_cip[:, 1] = ci + 1
293
+ i_fip = np.empty(i_cip.shape, dtype = np.int8)
294
+ i_fip[:, 0] = gcs.face_index_map[2, 1]
295
+ i_fip[:, 1] = gcs.face_index_map[2, 0]
296
+ cip = np.concatenate((cip, i_cip), axis = 0)
297
+ fip = np.concatenate((fip, i_fip), axis = 0)
298
+ del i_cip, i_fip
299
+ gcs.cell_index_pairs = cip
300
+ gcs.face_index_pairs = fip
301
+ gcs.count = len(gcs.cell_index_pairs)
302
+ gcs.feature_indices = np.zeros(gcs.count, dtype = np.int8)
303
+ assert len(gcs.face_index_pairs) == gcs.count
304
+ return gcs
199
305
 
200
306
  @classmethod
201
307
  def from_gcs_uuid_list(cls,
@@ -249,6 +355,7 @@ class GridConnectionSet(BaseResqpy):
249
355
  gcs.model = parent_model
250
356
  gcs.uuid = bu.new_uuid() # not strictly necessary as append will cause a new uuid as well
251
357
  gcs.title = title
358
+ gcs.property_collection = None
252
359
  # append data from the remaining grid connection sets
253
360
  for another_uuid in gcs_uuid_list[1:]:
254
361
  another_gcs = GridConnectionSet(source_model, uuid = another_uuid)
@@ -356,6 +463,14 @@ class GridConnectionSet(BaseResqpy):
356
463
  self.property_collection = rqp.PropertyCollection(support = self)
357
464
  return self.property_collection
358
465
 
466
+ def _set_cell_index_dtype(self):
467
+ """Determines whether to use int32 or int64 for normalised cell indices."""
468
+ self.cell_index_dtype = np.int32
469
+ for g in self.grid_list:
470
+ if g.is_big():
471
+ self.cell_index_dtype = np.int64
472
+ break
473
+
359
474
  def set_pairs_from_kelp(self,
360
475
  kelp_0,
361
476
  kelp_1,
@@ -393,7 +508,7 @@ class GridConnectionSet(BaseResqpy):
393
508
  k_layer = np.zeros((grid.nk - 1, grid.ni), dtype = bool)
394
509
  else:
395
510
  k_layer = np.zeros((grid.nk - 1, grid.nj), dtype = bool)
396
- kelp_a = np.array(kelp_k, dtype = int).T
511
+ kelp_a = np.array(kelp_k, dtype = np.int32).T
397
512
  k_layer[kelp_a[0], kelp_a[1]] = True
398
513
  k_faces = np.zeros((grid.nk - 1, grid.nj, grid.ni), dtype = bool)
399
514
  if axis == 'J':
@@ -407,7 +522,7 @@ class GridConnectionSet(BaseResqpy):
407
522
  j_layer = np.zeros((grid.nj - 1, grid.ni), dtype = bool)
408
523
  else:
409
524
  j_layer = np.zeros((grid.nk, grid.nj - 1), dtype = bool)
410
- kelp_a = np.array(kelp_j, dtype = int).T
525
+ kelp_a = np.array(kelp_j, dtype = np.int32).T
411
526
  j_layer[kelp_a[0], kelp_a[1]] = True
412
527
  j_faces = np.zeros((grid.nk, grid.nj - 1, grid.ni), dtype = bool)
413
528
  if axis == 'K':
@@ -421,7 +536,7 @@ class GridConnectionSet(BaseResqpy):
421
536
  i_layer = np.zeros((grid.nj, grid.ni - 1), dtype = bool)
422
537
  else:
423
538
  i_layer = np.zeros((grid.nk, grid.ni - 1), dtype = bool)
424
- kelp_a = np.array(kelp_i, dtype = int).T
539
+ kelp_a = np.array(kelp_i, dtype = np.int32).T
425
540
  i_layer[kelp_a[0], kelp_a[1]] = True
426
541
  i_faces = np.zeros((grid.nk, grid.nj, grid.ni - 1), dtype = bool)
427
542
  if axis == 'K':
@@ -437,24 +552,13 @@ class GridConnectionSet(BaseResqpy):
437
552
  create_organizing_objects_where_needed,
438
553
  feature_type = feature_type)
439
554
 
440
- def set_pairs_from_face_masks(
441
- self,
442
- k_faces,
443
- j_faces,
444
- i_faces,
445
- feature_name,
446
- create_organizing_objects_where_needed,
447
- feature_type = 'fault', # other feature_type values: 'horizon', 'geobody boundary'
448
- k_sides = None,
449
- j_sides = None,
450
- i_sides = None):
451
- """Sets cell_index_pairs and face_index_pairs based on triple face masks, using simple no throw pairing."""
452
-
555
+ def _sort_out_organizing_objects(self, feature_type, feature_name, create_organizing_objects_where_needed):
556
+ """Finds or creates interpretation and feature objects."""
453
557
  assert feature_type in ['fault', 'horizon', 'geobody boundary']
454
558
  if feature_name is None:
455
- feature_name = 'feature from face masks' # not sure this default is wise
559
+ feature_name = 'feature from faces' # not sure this default is wise
456
560
  if len(self.grid_list) > 1:
457
- log.warning('setting grid connection set pairs from face masks for first grid in list only')
561
+ log.warning('setting grid connection set pairs from faces for first grid in list only')
458
562
  grid = self.grid_list[0]
459
563
  if feature_type == 'fault':
460
564
  feature_flavour = 'TectonicBoundaryFeature'
@@ -511,6 +615,23 @@ class GridConnectionSet(BaseResqpy):
511
615
  log.error('no interpretation found for feature: ' + feature_name)
512
616
  return
513
617
  self.feature_list = [('obj_' + interpretation_flavour, fi_uuid, str(feature_name))]
618
+
619
+ def set_pairs_from_face_masks(
620
+ self,
621
+ k_faces,
622
+ j_faces,
623
+ i_faces,
624
+ feature_name,
625
+ create_organizing_objects_where_needed,
626
+ feature_type = 'fault', # other feature_type values: 'horizon', 'geobody boundary'
627
+ k_sides = None,
628
+ j_sides = None,
629
+ i_sides = None):
630
+ """Sets cell_index_pairs and face_index_pairs based on triple face masks, using simple no throw pairing."""
631
+
632
+ self._sort_out_organizing_objects(feature_type, feature_name, create_organizing_objects_where_needed)
633
+
634
+ grid = self.grid_list[0]
514
635
  cell_pair_list = []
515
636
  face_pair_list = []
516
637
  nj_ni = grid.nj * grid.ni
@@ -547,10 +668,10 @@ class GridConnectionSet(BaseResqpy):
547
668
  else:
548
669
  cell_pair_list.append((cell, cell + 1))
549
670
  face_pair_list.append((self.face_index_map[2, 1], self.face_index_map[2, 0]))
550
- self.cell_index_pairs = np.array(cell_pair_list, dtype = int)
551
- self.face_index_pairs = np.array(face_pair_list, dtype = int)
671
+ self.cell_index_pairs = np.array(cell_pair_list, dtype = self.cell_index_dtype)
672
+ self.face_index_pairs = np.array(face_pair_list, dtype = np.int8)
552
673
  self.count = len(self.cell_index_pairs)
553
- self.feature_indices = np.zeros(self.count, dtype = int)
674
+ self.feature_indices = np.zeros(self.count, dtype = np.int8)
554
675
  assert len(self.face_index_pairs) == self.count
555
676
 
556
677
  def set_pairs_from_faces_df(self,
@@ -608,9 +729,9 @@ class GridConnectionSet(BaseResqpy):
608
729
  if success:
609
730
  feature_index += 1
610
731
 
611
- self.feature_indices = np.array(fi_list, dtype = int)
612
- self.cell_index_pairs = np.array(cell_pair_list, dtype = int)
613
- self.face_index_pairs = np.array(face_pair_list, dtype = int)
732
+ self.feature_indices = np.array(fi_list, dtype = np.int32)
733
+ self.cell_index_pairs = np.array(cell_pair_list, dtype = self.cell_index_dtype)
734
+ self.face_index_pairs = np.array(face_pair_list, dtype = np.int8)
614
735
  self.count = len(self.cell_index_pairs)
615
736
  assert len(self.face_index_pairs) == self.count
616
737
  if create_mult_prop and self.count > 0:
@@ -695,7 +816,7 @@ class GridConnectionSet(BaseResqpy):
695
816
  singleton.cell_index_pairs, singleton.face_index_pairs = \
696
817
  self.raw_list_of_cell_face_pairs_for_feature_index(feature_index)
697
818
  singleton.count = singleton.cell_index_pairs.shape[0]
698
- singleton.feature_indices = np.zeros((singleton.count,), dtype = int)
819
+ singleton.feature_indices = np.zeros((singleton.count,), dtype = np.int32)
699
820
  singleton.feature_list = [self.feature_list[feature_index]]
700
821
  return singleton
701
822
 
@@ -822,7 +943,7 @@ class GridConnectionSet(BaseResqpy):
822
943
  cache_array = True,
823
944
  object = self,
824
945
  array_attribute = 'cell_index_pairs',
825
- dtype = 'int')
946
+ dtype = 'int64' if self.cell_index_dtype is np.int64 else 'int32')
826
947
 
827
948
  if self.face_index_pairs is None:
828
949
  log.debug('caching face index pairs from hdf5')
@@ -833,7 +954,7 @@ class GridConnectionSet(BaseResqpy):
833
954
  cache_array = True,
834
955
  object = self,
835
956
  array_attribute = 'face_index_pairs',
836
- dtype = 'int32')
957
+ dtype = 'int8')
837
958
 
838
959
  if len(self.grid_list) > 1 and self.grid_index_pairs is None:
839
960
  grid_index_node = rqet.find_tag(self.root, 'GridIndexPairs')
@@ -844,7 +965,7 @@ class GridConnectionSet(BaseResqpy):
844
965
  cache_array = True,
845
966
  object = self,
846
967
  array_attribute = 'grid_index_pairs',
847
- dtype = 'int')
968
+ dtype = 'int32' if len(self.grid_list) > 127 else 'int8')
848
969
 
849
970
  if self.feature_list is None:
850
971
  return
@@ -853,18 +974,18 @@ class GridConnectionSet(BaseResqpy):
853
974
  if self.feature_indices is None:
854
975
  log.debug('caching feature indices')
855
976
  elements_node = rqet.find_nested_tags(interp_root, ['InterpretationIndices', 'Elements'])
856
- # elements_node = rqet.find_nested_tags(interp_root, ['FaultIndices', 'Elements'])
977
+ # elements_node = rqet.find_nested_tags(interp_root, ['FaultIndices', 'Elements'])
857
978
  h5_key_pair = self.model.h5_uuid_and_path_for_node(elements_node, tag = 'Values')
858
979
  assert h5_key_pair is not None
859
980
  self.model.h5_array_element(h5_key_pair,
860
981
  cache_array = True,
861
982
  object = self,
862
983
  array_attribute = 'feature_indices',
863
- dtype = 'uint32')
984
+ dtype = 'int32')
864
985
  assert self.feature_indices.shape == (self.count,)
865
986
 
866
987
  cl_node = rqet.find_nested_tags(interp_root, ['InterpretationIndices', 'CumulativeLength'])
867
- # cl_node = rqet.find_nested_tags(interp_root, ['FaultIndices', 'CumulativeLength'])
988
+ # cl_node = rqet.find_nested_tags(interp_root, ['FaultIndices', 'CumulativeLength'])
868
989
  h5_key_pair = self.model.h5_uuid_and_path_for_node(cl_node, tag = 'Values')
869
990
  assert h5_key_pair is not None
870
991
  self.model.h5_array_element(h5_key_pair,
@@ -872,8 +993,8 @@ class GridConnectionSet(BaseResqpy):
872
993
  object = self,
873
994
  array_attribute = 'fi_cl',
874
995
  dtype = 'uint32')
875
- assert self.fi_cl.shape == (
876
- self.count,), 'connection set face pair(s) not assigned to exactly one feature' # rough check
996
+ assert self.fi_cl.shape == (self.count,), \
997
+ 'connection set face pair(s) not assigned to exactly one feature' # rough check
877
998
 
878
999
  # delattr(self, 'fi_cl') # assumed to be one-to-one mapping, so cumulative length is discarded
879
1000
 
@@ -1195,7 +1316,7 @@ class GridConnectionSet(BaseResqpy):
1195
1316
  # uuid/InterpretationIndices/elements (N,) uint32
1196
1317
  h5_reg.register_dataset(self.uuid, 'InterpretationIndices/elements', self.feature_indices)
1197
1318
  # uuid/InterpretationIndices/cumulativeLength (N,) uint32
1198
- one_to_one = np.arange(1, self.count + 1, dtype = int)
1319
+ one_to_one = np.arange(1, self.count + 1, dtype = np.uint32)
1199
1320
  h5_reg.register_dataset(self.uuid, 'InterpretationIndices/cumulativeLength', one_to_one)
1200
1321
 
1201
1322
  h5_reg.write(file_name, mode = mode)
@@ -1370,11 +1491,16 @@ class GridConnectionSet(BaseResqpy):
1370
1491
  include_both_sides = False,
1371
1492
  use_minus = False,
1372
1493
  trans_mult_uuid = None):
1373
- """Creates a Nexus include file holding MULT keywords and data. trans_mult_uuid (optional) is the uuid of a property on the gcs containing transmissibility multiplier values. If not provided values of 1.0 will be used."""
1494
+ """Creates a Nexus include file holding MULT keywords and data.
1495
+
1496
+ note:
1497
+ trans_mult_uuid (optional) is the uuid of a property on the gcs containing transmissibility multiplier values;
1498
+ If not provided values of 1.0 will be used
1499
+ """
1374
1500
  if trans_mult_uuid is not None:
1375
1501
  self.extract_property_collection()
1376
- assert self.property_collection.part_in_collection(self.model.part_for_uuid(
1377
- trans_mult_uuid)), f'trans_mult_uuid provided is not part of collection {trans_mult_uuid}'
1502
+ assert self.property_collection.part_in_collection(self.model.part_for_uuid(trans_mult_uuid)), \
1503
+ f'trans_mult_uuid provided is not part of collection {trans_mult_uuid}'
1378
1504
  tmult_array = self.property_collection.cached_part_array_ref(self.model.part_for_uuid(trans_mult_uuid))
1379
1505
  assert tmult_array is not None
1380
1506
  else:
@@ -1420,26 +1546,39 @@ class GridConnectionSet(BaseResqpy):
1420
1546
  feature_name = self.feature_list[feature_index][2].split()[0].upper()
1421
1547
  cell_index_pairs, face_index_pairs = self.list_of_cell_face_pairs_for_feature_index(feature_index)
1422
1548
  if tmult_array is not None:
1423
- feature_mask = np.where(self.feature_indices == feature_index, 1, 0)
1549
+ feature_mask = (self.feature_indices == feature_index)
1424
1550
  feat_mult_array = np.extract(feature_mask, tmult_array)
1425
1551
  else:
1426
1552
  feat_mult_array = np.ones(shape = (cell_index_pairs.shape[0],), dtype = float)
1427
1553
  for side in sides:
1428
- both = np.empty((cell_index_pairs.shape[0], 6), dtype = int) # axis, polarity, k, j, i, tmult
1554
+ both = np.empty((cell_index_pairs.shape[0], 5), dtype = np.int32) # axis, polarity, k, j, i
1429
1555
  both[:, :2] = face_index_pairs[:, side, :] # axis, polarity
1430
- both[:, 2:-1] = cell_index_pairs[:, side, :] # k, j, i
1431
- both[:, -1] = feat_mult_array.flatten()
1432
- df = pd.DataFrame(both, columns = ['axis', 'polarity', 'k', 'j', 'i', 'tmult'])
1433
- df = df.sort_values(by = ['axis', 'polarity', 'j', 'i', 'k', 'tmult'])
1434
- both_sorted = np.empty(both.shape, dtype = int)
1435
- both_sorted[:] = df
1436
- cell_indices = both_sorted[:, 2:-1]
1437
- face_indices = np.empty((both_sorted.shape[0], 2), dtype = int)
1438
- face_indices[:, :] = both_sorted[:, :2]
1439
- tmult_values = both_sorted[:, -1]
1440
- del both_sorted
1556
+ both[:, 2:] = cell_index_pairs[:, side, :] # k, j, i
1557
+ # both[:, -1] = feat_mult_array.flatten()
1558
+ # df = pd.DataFrame(both, columns = ['axis', 'polarity', 'k', 'j', 'i', 'tmult'])
1559
+ # df = df.sort_values(by = ['axis', 'polarity', 'j', 'i', 'k', 'tmult'])
1560
+ # both_sorted = np.empty(both.shape, dtype = np.int32)
1561
+ # both_sorted[:] = df
1562
+ si = np.argsort(both[:, 2]) # k
1563
+ msi = si
1564
+ both = both[si]
1565
+ si = np.argsort(both[:, 4], kind = 'stable') # i
1566
+ msi = msi[si]
1567
+ both = both[si]
1568
+ si = np.argsort(both[:, 3], kind = 'stable') # j
1569
+ msi = msi[si]
1570
+ both = both[si]
1571
+ si = np.argsort(both[:, 1], kind = 'stable') # polarity
1572
+ msi = msi[si]
1573
+ both = both[si]
1574
+ si = np.argsort(both[:, 0], kind = 'stable') # axis
1575
+ msi = msi[si]
1576
+ both = both[si]
1577
+ cell_indices = both[:, 2:]
1578
+ face_indices = np.empty((both.shape[0], 2), dtype = np.int8)
1579
+ face_indices[:, :] = both[:, :2]
1580
+ tmult_values = feat_mult_array[msi]
1441
1581
  del both
1442
- del df
1443
1582
  k = None
1444
1583
  i = j = k2 = axis = polarity = None # only needed to placate flake8 which whinges incorrectly otherwise
1445
1584
  for row in range(cell_indices.shape[0]):
@@ -1557,7 +1696,7 @@ class GridConnectionSet(BaseResqpy):
1557
1696
  log.info(
1558
1697
  f'Property name {property_name} not found in extra_metadata for {self.model.citation_title_for_part(self.model.part_for_uuid(feature_uuid))}'
1559
1698
  )
1560
- value_list.append(np.NaN)
1699
+ value_list.append(np.nan)
1561
1700
  else:
1562
1701
  value_list.append(float(feat.extra_metadata[property_name]))
1563
1702
  return value_list
@@ -1691,7 +1830,7 @@ class GridConnectionSet(BaseResqpy):
1691
1830
  combined_values = property_value_by_column_edge.copy()
1692
1831
  else:
1693
1832
  combined_values = None
1694
- combined_index = np.full((fault_by_column_edge_mask.shape), -1, dtype = int)
1833
+ combined_index = np.full((fault_by_column_edge_mask.shape), -1, dtype = np.int32)
1695
1834
  combined_index = np.where(fault_by_column_edge_mask, feature, combined_index)
1696
1835
  sum_unmasked = np.sum(fault_by_column_edge_mask)
1697
1836
  else:
@@ -1801,7 +1940,7 @@ class GridConnectionSet(BaseResqpy):
1801
1940
  def sorted_paired_cell_face_index_position(cell_face_index, a_or_b):
1802
1941
  # pair one side (a_or_b) of cell_face_index with its position, then sort
1803
1942
  count = len(cell_face_index)
1804
- sp = np.empty((count, 2), dtype = int)
1943
+ sp = np.empty((count, 2), dtype = np.int32)
1805
1944
  sp[:, 0] = cell_face_index[:, a_or_b]
1806
1945
  sp[:, 1] = np.arange(count)
1807
1946
  t = [tuple(r) for r in sp] # could use numpy fields based sort instead of tuple list?
@@ -1984,19 +2123,19 @@ class GridConnectionSet(BaseResqpy):
1984
2123
  for k0 in range(entry['k1'], entry['k2'] + 1):
1985
2124
  for j0 in range(entry['j1'], entry['j2'] + 1):
1986
2125
  for i0 in range(entry['i1'], entry['i2'] + 1):
1987
- neighbour = np.array([k0, j0, i0], dtype = int)
2126
+ neighbour = np.array([k0, j0, i0], dtype = np.int32)
1988
2127
  if fp:
1989
2128
  neighbour[axis] += 1
1990
2129
  else:
1991
2130
  neighbour[axis] -= 1
1992
2131
  fi_list.append(feature_index)
1993
- cell_pair_list.append((grid.natural_cell_index(
1994
- (k0, j0, i0)), grid.natural_cell_index(neighbour)))
2132
+ cell_pair_list.append((grid.natural_cell_index((k0, j0, i0)), \
2133
+ grid.natural_cell_index(neighbour)))
1995
2134
  face_pair_list.append((self.face_index_map[axis, fp], self.face_index_map[axis, 1 - fp]))
1996
2135
  if create_mult_prop:
1997
2136
  mult_list.append(multiplier)
1998
2137
  if fi_root is not None and fault_const_mult and fault_mult_value is not None:
1999
- #patch extra_metadata into xml for new fault interpretation object
2138
+ # patch extra_metadata into xml for new fault interpretation object
2000
2139
  rqet.create_metadata_xml(fi_root, {"Transmissibility multiplier": str(fault_mult_value)})
2001
2140
  return True, const_mult
2002
2141
 
@@ -2042,14 +2181,15 @@ class GridConnectionSet(BaseResqpy):
2042
2181
  feature_index = None,
2043
2182
  active_only = True,
2044
2183
  lazy = False,
2045
- baffle_uuid = None):
2184
+ baffle_uuid = None,
2185
+ dtype = None):
2046
2186
  """Creates a triplet of grid face numpy arrays populated from a property for this gcs.
2047
2187
 
2048
2188
  arguments:
2049
2189
  property_uuid (UUID): the uuid of the gcs property
2050
2190
  default_value (float or int, optional): the value to use in the grid property
2051
2191
  on faces that do not appear in the grid connection set; will default to
2052
- np.NaN for continuous properties, -1 for categorical or discrete
2192
+ np.nan for continuous properties, -1 for categorical or discrete
2053
2193
  feature_index (int, optional): if present, only faces for this feature are used
2054
2194
  active_only (bool, default True): if True and an active property exists for the
2055
2195
  grid connection set, then only active faces are used when populating the
@@ -2060,6 +2200,8 @@ class GridConnectionSet(BaseResqpy):
2060
2200
  baffle_uuid (uuid, optional): if present, the uuid of a discrete (bool) property
2061
2201
  of the gcs holding baffle flags; where True the output face value is set
2062
2202
  to zero regardless of the main property value
2203
+ dtype (type or str, optional): the element type for the returned arrays; defaults
2204
+ to float for continuous properties or int for discrete properties; see notes
2063
2205
 
2064
2206
  returns:
2065
2207
  triple numpy arrays: identifying the K, J & I direction grid face property values;
@@ -2069,7 +2211,9 @@ class GridConnectionSet(BaseResqpy):
2069
2211
  can only be used on single grid gcs; gcs property must have indexable of faces;
2070
2212
  at present generates grid properties with indexable 'faces' not 'faces per cell',
2071
2213
  which might not be appropriate for grids with split pillars (structural faults);
2072
- points properties not currently supported; count must be 1
2214
+ points properties not currently supported; count must be 1;
2215
+ if the property is a boolean array and may have been written to hdf5 using packing,
2216
+ then the dtype argument must be set to bool or np.uint8 to ensure unpacking
2073
2217
  """
2074
2218
 
2075
2219
  assert self.number_of_grids() == 1
@@ -2077,7 +2221,7 @@ class GridConnectionSet(BaseResqpy):
2077
2221
  active_mask = None
2078
2222
  if active_only:
2079
2223
  pc = self.extract_property_collection()
2080
- active_mask = pc.single_array_ref(property_kind = 'active')
2224
+ active_mask = pc.single_array_ref(property_kind = 'active', dtype = bool)
2081
2225
  if active_mask is not None:
2082
2226
  assert active_mask.shape == (self.count,)
2083
2227
  gcs_prop = rqp.Property(self.model, uuid = property_uuid)
@@ -2085,10 +2229,12 @@ class GridConnectionSet(BaseResqpy):
2085
2229
  assert bu.matching_uuids(gcs_prop.collection.support_uuid, self.uuid)
2086
2230
  assert gcs_prop.count() == 1
2087
2231
  assert not gcs_prop.is_points()
2088
- dtype = float if gcs_prop.is_continuous() else int
2232
+ if dtype is None:
2233
+ dtype = float if gcs_prop.is_continuous() else int
2089
2234
  if default_value is None:
2090
- default_value = -1 if dtype is int else np.NaN
2091
- gcs_prop_array = gcs_prop.array_ref()
2235
+ default_value = -1 if dtype is int else np.nan
2236
+ gcs_prop_array = gcs_prop.array_ref(dtype = dtype)
2237
+ assert gcs_prop_array.shape == (self.count,)
2092
2238
  log.debug(f'preparing grid face arrays from gcs property: {gcs_prop.title}; from gcs:{self.title}')
2093
2239
 
2094
2240
  baffle_mask = None
@@ -2100,55 +2246,49 @@ class GridConnectionSet(BaseResqpy):
2100
2246
  ak = np.full((nk + 1, nj, ni), default_value, dtype = dtype)
2101
2247
  aj = np.full((nk, nj + 1, ni), default_value, dtype = dtype)
2102
2248
  ai = np.full((nk, nj, ni + 1), default_value, dtype = dtype)
2103
- # mk = np.zeros((nk + 1, nj, ni), dtype = bool)
2104
- # mj = np.zeros((nk, nj + 1, ni), dtype = bool)
2105
- # mi = np.zeros((nk, nj, ni + 1), dtype = bool)
2106
2249
 
2107
2250
  # populate arrays from faces of gcs, optionally filtered by feature index
2108
- cip, fip = self.list_of_cell_face_pairs_for_feature_index(None)
2109
- assert len(cip) == self.count and len(fip) == self.count
2110
- assert gcs_prop_array.shape == (self.count,)
2111
- if feature_index is None:
2112
- indices = np.arange(self.count, dtype = int)
2113
- else:
2251
+ cip, fip = self.list_of_cell_face_pairs_for_feature_index(feature_index)
2252
+
2253
+ value_array = gcs_prop_array.copy()
2254
+
2255
+ if baffle_mask is not None:
2256
+ value_array[baffle_mask] = 0 # will be cast to float (or bool) if needed
2257
+
2258
+ if feature_index is not None:
2114
2259
  indices = self.indices_for_feature_index(feature_index)
2260
+ value_array = value_array[indices]
2261
+ if active_mask is not None:
2262
+ active_mask = active_mask[indices]
2263
+
2264
+ if active_mask is not None:
2265
+ cip = cip[active_mask, :, :]
2266
+ fip = fip[active_mask, :, :]
2267
+ value_array = value_array[active_mask]
2115
2268
 
2116
- # opposing_count = 0
2117
2269
  side_list = ([0] if lazy else [0, 1])
2118
- for fi in indices:
2119
- # fi = int(i)
2120
- if active_mask is not None and not active_mask[fi]:
2121
- continue
2122
- value = gcs_prop_array[fi]
2123
- if baffle_mask is not None and baffle_mask[fi]:
2124
- value = 0 # will be cast to float (or bool) if needed when assigned below
2125
- for side in side_list:
2126
- cell_kji0 = cip[fi, side].copy()
2127
- # opposing = cell_kji0.copy()
2128
- axis, polarity = fip[fi, side]
2129
- assert 0 <= axis <= 2 and 0 <= polarity <= 1
2130
- cell_kji0[axis] += polarity
2131
- # opposing[axis] += (1 - polarity)
2132
- if axis == 0:
2133
- ak[tuple(cell_kji0)] = value
2134
- # mk[tuple(cell_kji0)] = True
2135
- # if mk[tuple(opposing)]:
2136
- # opposing_count += 1
2137
- elif axis == 1:
2138
- aj[tuple(cell_kji0)] = value
2139
- # mj[tuple(cell_kji0)] = True
2140
- # if mj[tuple(opposing)]:
2141
- # opposing_count += 1
2142
- else:
2143
- ai[tuple(cell_kji0)] = value
2144
- # mi[tuple(cell_kji0)] = True
2145
- # if mi[tuple(opposing)]:
2146
- # opposing_count += 1
2147
2270
 
2148
- # if opposing_count:
2149
- # log.warning(f'{opposing_count} suspicious opposing faces of {len(indices)} detected in gcs: {self.title}')
2150
- # else:
2151
- # log.debug(f'no suspicious opposing faces detected in gcs: {self.title}')
2271
+ for side in side_list:
2272
+ cell_kji0 = cip[:, side].copy() # shape (N, 3)
2273
+ axis = fip[:, side, 0] # shape (N,)
2274
+ polarity = fip[:, side, 1] # shape (N,)
2275
+ # assert 0 <= np.min(axis) and np.max(axis) <= 2
2276
+ # assert 0 <= np.min(polarity) and np.max(polarity) <= 1
2277
+
2278
+ axis_mask = (axis == 0).astype(bool)
2279
+ ak_kji0 = cell_kji0[axis_mask, :]
2280
+ ak_kji0[:, 0] += polarity[axis_mask]
2281
+ ak[ak_kji0[:, 0], ak_kji0[:, 1], ak_kji0[:, 2]] = value_array[axis_mask]
2282
+
2283
+ axis_mask = (axis == 1).astype(bool)
2284
+ aj_kji0 = cell_kji0[axis_mask, :]
2285
+ aj_kji0[:, 1] += polarity[axis_mask]
2286
+ aj[aj_kji0[:, 0], aj_kji0[:, 1], aj_kji0[:, 2]] = value_array[axis_mask]
2287
+
2288
+ axis_mask = (axis == 2).astype(bool)
2289
+ ai_kji0 = cell_kji0[axis_mask, :]
2290
+ ai_kji0[:, 2] += polarity[axis_mask]
2291
+ ai[ai_kji0[:, 0], ai_kji0[:, 1], ai_kji0[:, 2]] = value_array[axis_mask]
2152
2292
 
2153
2293
  return (ak, aj, ai)
2154
2294
 
@@ -2160,3 +2300,27 @@ def _copy_organisation_objects(target_model, source_model, gcs):
2160
2300
  for _, uuid, _ in gcs.feature_list:
2161
2301
  target_model.copy_uuid_from_other_model(source_model,
2162
2302
  uuid) # will copy related features as well as interpretations
2303
+
2304
+
2305
+ def _sort_and_remove_duplicates(a, props = None):
2306
+ """Return copy of 1D array a, sorted and with duplicates removed; secondary arrays can be kept in alignment."""
2307
+ if a is None or a.size <= 1:
2308
+ return a
2309
+ assert a.ndim == 1
2310
+ si = None
2311
+ no_props = (props is None or len(props) == 0)
2312
+ if no_props:
2313
+ a = np.sort(a)
2314
+ else:
2315
+ si = np.argsort(a)
2316
+ a = a[si]
2317
+ m = np.empty(a.size, dtype = bool)
2318
+ m[0] = True
2319
+ m[1:] = (a[1:] != a[:-1])
2320
+ if np.all(m):
2321
+ return a
2322
+ if not no_props:
2323
+ for i in range(len(props)):
2324
+ p = props[i][si]
2325
+ props[i] = p[m]
2326
+ return a[m]