resqpy 4.14.2__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 +13 -10
  46. resqpy/surface/_tri_mesh_stencil.py +4 -4
  47. resqpy/surface/_triangulated_patch.py +71 -51
  48. resqpy/time_series/_any_time_series.py +7 -4
  49. resqpy/time_series/_geologic_time_series.py +1 -1
  50. resqpy/unstructured/_hexa_grid.py +6 -2
  51. resqpy/unstructured/_prism_grid.py +13 -5
  52. resqpy/unstructured/_pyramid_grid.py +6 -2
  53. resqpy/unstructured/_tetra_grid.py +6 -2
  54. resqpy/unstructured/_unstructured_grid.py +6 -2
  55. resqpy/well/_blocked_well.py +1986 -1946
  56. resqpy/well/_deviation_survey.py +3 -3
  57. resqpy/well/_md_datum.py +11 -21
  58. resqpy/well/_trajectory.py +10 -5
  59. resqpy/well/_wellbore_frame.py +10 -2
  60. resqpy/well/blocked_well_frame.py +3 -3
  61. resqpy/well/well_object_funcs.py +7 -9
  62. resqpy/well/well_utils.py +33 -0
  63. {resqpy-4.14.2.dist-info → resqpy-5.1.5.dist-info}/METADATA +8 -9
  64. {resqpy-4.14.2.dist-info → resqpy-5.1.5.dist-info}/RECORD +66 -66
  65. {resqpy-4.14.2.dist-info → resqpy-5.1.5.dist-info}/WHEEL +1 -1
  66. resqpy/grid/_moved_functions.py +0 -15
  67. {resqpy-4.14.2.dist-info → resqpy-5.1.5.dist-info}/LICENSE +0 -0
@@ -10,6 +10,7 @@ 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]