resqpy 4.16.11__py3-none-any.whl → 4.17.1__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.
@@ -495,18 +495,18 @@ def find_faces_to_represent_surface_regular(
495
495
  return gcs
496
496
 
497
497
 
498
- def find_faces_to_represent_surface_regular_optimised(grid,
499
- surface,
500
- name,
501
- title = None,
502
- agitate = False,
503
- random_agitation = False,
504
- feature_type = "fault",
505
- is_curtain = False,
506
- progress_fn = None,
507
- return_properties = None,
508
- raw_bisector = False,
509
- n_batches = 20):
498
+ def find_faces_to_represent_surface_regular_dense_optimised(grid,
499
+ surface,
500
+ name,
501
+ title = None,
502
+ agitate = False,
503
+ random_agitation = False,
504
+ feature_type = "fault",
505
+ is_curtain = False,
506
+ progress_fn = None,
507
+ return_properties = None,
508
+ raw_bisector = False,
509
+ n_batches = 20):
510
510
  """Returns a grid connection set containing those cell faces which are deemed to represent the surface.
511
511
 
512
512
  argumants:
@@ -553,7 +553,10 @@ def find_faces_to_represent_surface_regular_optimised(grid,
553
553
  no trimming of the surface is carried out here: for computational efficiency, it is recommended
554
554
  to trim first;
555
555
  organisational objects for the feature are created if needed;
556
- if the offset return property is requested, the implicit units will be the z units of the grid's crs
556
+ if the offset return property is requested, the implicit units will be the z units of the grid's crs;
557
+ this version of the function uses fully explicit boolean arrays to capture the faces before conversion
558
+ to a grid connection set; use the non-dense version of the function for a reduced memory footprint;
559
+ this function is DEPRECATED pending proving of newer find_faces_to_represent_surface_regular_optimised()
557
560
  """
558
561
 
559
562
  assert isinstance(grid, grr.RegularGrid)
@@ -599,8 +602,10 @@ def find_faces_to_represent_surface_regular_optimised(grid,
599
602
  grid.block_dxyz_dkji[0, 2],
600
603
  )
601
604
  triangles, points = surface.triangles_and_points()
605
+ t_dtype = np.int32 if len(triangles) < 2_000_000_000 else np.int64
602
606
  assert (triangles is not None and points is not None), f"surface {surface.title} is empty"
603
607
  if agitate:
608
+ points = points.copy()
604
609
  if random_agitation:
605
610
  points += 1.0e-5 * (np.random.random(points.shape) - 0.5)
606
611
  else:
@@ -622,7 +627,7 @@ def find_faces_to_represent_surface_regular_optimised(grid,
622
627
  if nk > 1:
623
628
  # log.debug("searching for k faces")
624
629
  k_faces = np.zeros((nk - 1, grid.nj, grid.ni), dtype = bool)
625
- k_triangles = np.full((nk - 1, grid.nj, grid.ni), -1, dtype = int)
630
+ k_triangles = np.full((nk - 1, grid.nj, grid.ni), -1, dtype = t_dtype)
626
631
  k_depths = np.full((nk - 1, grid.nj, grid.ni), np.nan)
627
632
  k_offsets = np.full((nk - 1, grid.nj, grid.ni), np.nan)
628
633
  p_xy = np.delete(points, 2, 1)
@@ -666,7 +671,7 @@ def find_faces_to_represent_surface_regular_optimised(grid,
666
671
  if grid.nj > 1:
667
672
  # log.debug("searching for j faces")
668
673
  j_faces = np.zeros((nk, grid.nj - 1, grid.ni), dtype = bool)
669
- j_triangles = np.full((nk, grid.nj - 1, grid.ni), -1, dtype = int)
674
+ j_triangles = np.full((nk, grid.nj - 1, grid.ni), -1, dtype = t_dtype)
670
675
  j_depths = np.full((nk, grid.nj - 1, grid.ni), np.nan)
671
676
  j_offsets = np.full((nk, grid.nj - 1, grid.ni), np.nan)
672
677
  p_xz = np.delete(points, 1, 1)
@@ -715,7 +720,7 @@ def find_faces_to_represent_surface_regular_optimised(grid,
715
720
  if grid.ni > 1:
716
721
  # log.debug("searching for i faces")
717
722
  i_faces = np.zeros((nk, grid.nj, grid.ni - 1), dtype = bool)
718
- i_triangles = np.full((nk, grid.nj, grid.ni - 1), -1, dtype = int)
723
+ i_triangles = np.full((nk, grid.nj, grid.ni - 1), -1, dtype = t_dtype)
719
724
  i_depths = np.full((nk, grid.nj, grid.ni - 1), np.nan)
720
725
  i_offsets = np.full((nk, grid.nj, grid.ni - 1), np.nan)
721
726
  p_yz = np.delete(points, 0, 1)
@@ -827,7 +832,8 @@ def find_faces_to_represent_surface_regular_optimised(grid,
827
832
  # log.debug('finished preparing columns bisector')
828
833
  else:
829
834
  log.debug("preparing cells bisector")
830
- bisector, is_curtain = bisector_from_faces(tuple(grid.extent_kji), k_faces, j_faces, i_faces, raw_bisector)
835
+ bisector, is_curtain = bisector_from_faces(tuple(grid.extent_kji), k_faces, j_faces, i_faces, raw_bisector,
836
+ False)
831
837
  if is_curtain:
832
838
  bisector = bisector[0] # reduce to a columns property
833
839
 
@@ -861,6 +867,386 @@ def find_faces_to_represent_surface_regular_optimised(grid,
861
867
  return gcs
862
868
 
863
869
 
870
+ def find_faces_to_represent_surface_regular_optimised(grid,
871
+ surface,
872
+ name,
873
+ title = None,
874
+ agitate = False,
875
+ random_agitation = False,
876
+ feature_type = "fault",
877
+ is_curtain = False,
878
+ progress_fn = None,
879
+ return_properties = None,
880
+ raw_bisector = False,
881
+ n_batches = 20):
882
+ """Returns a grid connection set containing those cell faces which are deemed to represent the surface.
883
+
884
+ argumants:
885
+ grid (RegularGrid): the grid for which to create a grid connection set representation of the surface;
886
+ must be aligned, ie. I with +x, J with +y, K with +z and local origin of (0.0, 0.0, 0.0)
887
+ surface (Surface): the surface to be intersected with the grid
888
+ name (str): the feature name to use in the grid connection set
889
+ title (str, optional): the citation title to use for the grid connection set; defaults to name
890
+ agitate (bool, default False): if True, the points of the surface are perturbed by a small
891
+ offset, which can help if the surface has been built from a regular mesh with a periodic resonance
892
+ with the grid
893
+ random_agitation (bool, default False): if True, the agitation is by a small random distance; if False,
894
+ a constant positive shift of 5.0e-6 is applied to x, y & z values; ignored if agitate is False
895
+ feature_type (str, default 'fault'): 'fault', 'horizon' or 'geobody boundary'
896
+ is_curtain (bool, default False): if True, only the top layer of the grid is processed and the bisector
897
+ property, if requested, is generated with indexable element columns
898
+ progress_fn (f(x: float), optional): a callback function to be called at intervals by this function;
899
+ the argument will progress from 0.0 to 1.0 in unspecified and uneven increments
900
+ return_properties (List[str]): if present, a list of property arrays to calculate and
901
+ return as a dictionary; recognised values in the list are 'triangle', 'depth', 'offset',
902
+ 'flange bool', 'grid bisector', or 'grid shadow';
903
+ triangle is an index into the surface triangles of the triangle detected for the gcs face; depth is
904
+ the z value of the intersection point of the inter-cell centre vector with a triangle in the surface;
905
+ offset is a measure of the distance between the centre of the cell face and the intersection point;
906
+ grid bisector is a grid cell boolean property holding True for the set of cells on one
907
+ side of the surface, deemed to be shallower;
908
+ grid shadow is a grid cell int8 property holding 0: cell neither above nor below a K face of the
909
+ gridded surface, 1 cell is above K face(s), 2 cell is below K face(s), 3 cell is between K faces;
910
+ the returned dictionary has the passed strings as keys and numpy arrays as values
911
+ raw_bisector (bool, default False): if True and grid bisector is requested then it is left in a raw
912
+ form without assessing which side is shallower (True values indicate same side as origin cell)
913
+ n_batches (int, default 20): the number of batches of triangles to use at the low level (numba multi
914
+ threading allows some parallelism between the batches)
915
+
916
+ returns:
917
+ gcs or (gcs, gcs_props)
918
+ where gcs is a new GridConnectionSet with a single feature, not yet written to hdf5 nor xml created;
919
+ gcs_props is a dictionary mapping from requested return_properties string to numpy array
920
+
921
+ notes:
922
+ this function is designed for aligned regular grids only;
923
+ this function can handle the surface and grid being in different coordinate reference systems, as
924
+ long as the implicit parent crs is shared;
925
+ no trimming of the surface is carried out here: for computational efficiency, it is recommended
926
+ to trim first;
927
+ organisational objects for the feature are created if needed;
928
+ if the offset return property is requested, the implicit units will be the z units of the grid's crs
929
+ """
930
+
931
+ assert isinstance(grid, grr.RegularGrid)
932
+ assert grid.is_aligned
933
+ return_triangles = False
934
+ return_depths = False
935
+ return_offsets = False
936
+ return_bisector = False
937
+ return_shadow = False
938
+ return_flange_bool = False
939
+ if return_properties:
940
+ assert all([
941
+ p in [
942
+ "triangle",
943
+ "depth",
944
+ "offset",
945
+ "grid bisector",
946
+ "grid shadow",
947
+ "flange bool",
948
+ ] for p in return_properties
949
+ ])
950
+ return_triangles = "triangle" in return_properties
951
+ return_depths = "depth" in return_properties
952
+ return_offsets = "offset" in return_properties
953
+ return_bisector = "grid bisector" in return_properties
954
+ return_shadow = "grid shadow" in return_properties
955
+ return_flange_bool = "flange bool" in return_properties
956
+ if return_flange_bool:
957
+ return_triangles = True
958
+
959
+ if title is None:
960
+ title = name
961
+
962
+ if progress_fn is not None:
963
+ progress_fn(0.0)
964
+
965
+ log.debug(f"intersecting surface {surface.title} with regular grid {grid.title}")
966
+ # log.debug(f'grid extent kji: {grid.extent_kji}')
967
+
968
+ triangles, points = surface.triangles_and_points(copy = True)
969
+ surface.decache_triangles_and_points()
970
+
971
+ t_dtype = np.int32 if len(triangles) < 2_147_483_648 else np.int64
972
+
973
+ assert (triangles is not None and points is not None), f"surface {surface.title} is empty"
974
+ if agitate:
975
+ if random_agitation:
976
+ points += 1.0e-5 * (np.random.random(points.shape) - 0.5)
977
+ else:
978
+ points += 5.0e-6
979
+ # log.debug(f'surface: {surface.title}; p0: {points[0]}; crs uuid: {surface.crs_uuid}')
980
+ # log.debug(f'surface min xyz: {np.min(points, axis = 0)}')
981
+ # log.debug(f'surface max xyz: {np.max(points, axis = 0)}')
982
+ if not bu.matching_uuids(grid.crs_uuid, surface.crs_uuid):
983
+ log.debug("converting from surface crs to grid crs")
984
+ s_crs = rqc.Crs(surface.model, uuid = surface.crs_uuid)
985
+ s_crs.convert_array_to(grid.crs, points)
986
+ surface.crs_uuid = grid.crs.uuid
987
+ # log.debug(f'surface: {surface.title}; p0: {points[0]}; crs uuid: {surface.crs_uuid}')
988
+ # log.debug(f'surface min xyz: {np.min(points, axis = 0)}')
989
+ # log.debug(f'surface max xyz: {np.max(points, axis = 0)}')
990
+
991
+ # convert surface points to work with unit cube grid cells
992
+ dx = grid.block_dxyz_dkji[2, 0]
993
+ dy = grid.block_dxyz_dkji[1, 1]
994
+ dz = grid.block_dxyz_dkji[0, 2]
995
+ points[:, 0] /= dx
996
+ points[:, 1] /= dy
997
+ points[:, 2] /= dz
998
+ points[:] -= 0.5
999
+ p = points[triangles]
1000
+
1001
+ nk = 1 if is_curtain else grid.nk
1002
+ # K direction (xy projection)
1003
+ k_faces_kji0 = None
1004
+ k_triangles = None
1005
+ k_depths = None
1006
+ k_offsets = None
1007
+ k_props = None
1008
+ if nk > 1:
1009
+ # log.debug("searching for k faces")
1010
+
1011
+ k_hits, k_depths = vec.points_in_triangles_aligned_unified(grid.ni, grid.nj, 0, 1, 2, p, n_batches)
1012
+
1013
+ k_faces = np.floor(k_depths)
1014
+ mask = np.logical_and(k_faces >= 0, k_faces < nk - 1)
1015
+
1016
+ if np.any(mask):
1017
+ k_hits = k_hits[mask, :]
1018
+ k_faces = k_faces[mask]
1019
+ k_depths = k_depths[mask]
1020
+ k_triangles = k_hits[:, 0]
1021
+ k_faces_kji0 = np.empty((len(k_faces), 3), dtype = np.int32)
1022
+ k_faces_kji0[:, 0] = k_faces
1023
+ k_faces_kji0[:, 1] = k_hits[:, 1]
1024
+ k_faces_kji0[:, 2] = k_hits[:, 2]
1025
+ if return_offsets:
1026
+ k_offsets = (k_depths - k_faces.astype(np.float64) - 0.5) * dz
1027
+ if return_depths:
1028
+ k_depths[:] += 0.5
1029
+ k_depths[:] *= dz
1030
+ k_props = []
1031
+ if return_triangles:
1032
+ k_props.append(k_triangles)
1033
+ if return_depths:
1034
+ k_props.append(k_depths)
1035
+ if return_offsets:
1036
+ k_props.append(k_offsets)
1037
+ log.debug(f"k face count: {len(k_faces_kji0)}")
1038
+
1039
+ del k_hits
1040
+ del k_faces
1041
+
1042
+ if progress_fn is not None:
1043
+ progress_fn(0.3)
1044
+
1045
+ # J direction (xz projection)
1046
+ j_faces_kji0 = None
1047
+ j_triangles = None
1048
+ j_depths = None
1049
+ j_offsets = None
1050
+ j_props = None
1051
+ if grid.nj > 1:
1052
+ # log.debug("searching for J faces")
1053
+
1054
+ j_hits, j_depths = vec.points_in_triangles_aligned_unified(grid.ni, grid.nk, 0, 2, 1, p, n_batches)
1055
+
1056
+ j_faces = np.floor(j_depths)
1057
+ mask = np.logical_and(j_faces >= 0, j_faces < grid.nj - 1)
1058
+
1059
+ if np.any(mask):
1060
+ j_hits = j_hits[mask, :]
1061
+ j_faces = j_faces[mask]
1062
+ j_depths = j_depths[mask]
1063
+ j_triangles = j_hits[:, 0]
1064
+ j_faces_kji0 = np.empty((len(j_faces), 3), dtype = np.int32)
1065
+ j_faces_kji0[:, 0] = j_hits[:, 1]
1066
+ j_faces_kji0[:, 1] = j_faces
1067
+ j_faces_kji0[:, 2] = j_hits[:, 2]
1068
+ if return_offsets:
1069
+ j_offsets = (j_depths - j_faces.astype(np.float64) - 0.5) * dy
1070
+ if return_depths:
1071
+ j_depths[:] += 0.5
1072
+ j_depths[:] *= dy
1073
+ if is_curtain and grid.nk > 1: # expand arrays to all layers
1074
+ j_faces = np.repeat(np.expand_dims(j_faces_kji0, axis = 0), grid.nk, axis = 0)
1075
+ j_faces[:, :, 0] = np.expand_dims(np.arange(grid.nk, dtype = np.int32), axis = -1)
1076
+ j_faces_kji0 = j_faces.reshape((-1, 3))
1077
+ j_triangles = np.repeat(j_triangles, grid.nk, axis = 0)
1078
+ if return_offsets:
1079
+ j_offsets = np.repeat(j_offsets, grid.nk, axis = 0)
1080
+ if return_depths:
1081
+ j_depths = np.repeat(j_depths, grid.nk, axis = 0)
1082
+ j_props = []
1083
+ if return_triangles:
1084
+ j_props.append(j_triangles)
1085
+ if return_depths:
1086
+ j_props.append(j_depths)
1087
+ if return_offsets:
1088
+ j_props.append(j_offsets)
1089
+ log.debug(f"j face count: {len(j_faces_kji0)}")
1090
+
1091
+ del j_hits
1092
+ del j_faces
1093
+
1094
+ if progress_fn is not None:
1095
+ progress_fn(0.6)
1096
+
1097
+ # I direction (yz projection)
1098
+ i_faces_kji0 = None
1099
+ i_triangles = None
1100
+ i_depths = None
1101
+ i_offsets = None
1102
+ i_props = None
1103
+ if grid.ni > 1:
1104
+ # log.debug("searching for I faces")
1105
+
1106
+ i_hits, i_depths = vec.points_in_triangles_aligned_unified(grid.nj, grid.nk, 1, 2, 0, p, n_batches)
1107
+
1108
+ i_faces = np.floor(i_depths)
1109
+ mask = np.logical_and(i_faces >= 0, i_faces < grid.ni - 1)
1110
+
1111
+ if np.any(mask):
1112
+ i_hits = i_hits[mask, :]
1113
+ i_faces = i_faces[mask]
1114
+ i_depths = i_depths[mask]
1115
+ i_triangles = i_hits[:, 0]
1116
+ i_faces_kji0 = np.empty((len(i_faces), 3), dtype = np.int32)
1117
+ i_faces_kji0[:, 0] = i_hits[:, 1]
1118
+ i_faces_kji0[:, 1] = i_hits[:, 2]
1119
+ i_faces_kji0[:, 2] = i_faces
1120
+ if return_offsets:
1121
+ i_offsets = (i_depths - i_faces.astype(np.float64) - 0.5) * dx
1122
+ if return_depths:
1123
+ i_depths[:] += 0.5
1124
+ i_depths[:] *= dx
1125
+ if is_curtain and grid.nk > 1: # expand arrays to all layers
1126
+ i_faces = np.repeat(np.expand_dims(i_faces_kji0, axis = 0), grid.nk, axis = 0)
1127
+ i_faces[:, :, 0] = np.expand_dims(np.arange(grid.nk, dtype = np.int32), axis = -1)
1128
+ i_faces_kji0 = i_faces.reshape((-1, 3))
1129
+ i_triangles = np.repeat(i_triangles, grid.nk, axis = 0)
1130
+ if return_offsets:
1131
+ i_offsets = np.repeat(i_offsets, grid.nk, axis = 0)
1132
+ if return_depths:
1133
+ i_depths = np.repeat(i_depths, grid.nk, axis = 0)
1134
+ i_props = []
1135
+ if return_triangles:
1136
+ i_props.append(i_triangles)
1137
+ if return_depths:
1138
+ i_props.append(i_depths)
1139
+ if return_offsets:
1140
+ i_props.append(i_offsets)
1141
+ log.debug(f"i face count: {len(i_faces_kji0)}")
1142
+
1143
+ del i_hits
1144
+ del i_faces
1145
+
1146
+ if progress_fn is not None:
1147
+ progress_fn(0.9)
1148
+
1149
+ log.debug("converting face sets into grid connection set")
1150
+ # NB: kji0 arrays in internal face protocol: used as cell_kji0 with polarity of 1
1151
+ # property lists have elements replaced with sorted and filtered equivalents
1152
+ gcs = rqf.GridConnectionSet.from_faces_indices(grid = grid,
1153
+ k_faces_kji0 = k_faces_kji0,
1154
+ j_faces_kji0 = j_faces_kji0,
1155
+ i_faces_kji0 = i_faces_kji0,
1156
+ remove_duplicates = True,
1157
+ k_properties = k_props,
1158
+ j_properties = j_props,
1159
+ i_properties = i_props,
1160
+ feature_name = name,
1161
+ feature_type = feature_type,
1162
+ create_organizing_objects_where_needed = True,
1163
+ title = title)
1164
+ # log.debug('finished coversion to gcs')
1165
+
1166
+ # NB. following assumes faces have been added to gcs in a particular order!
1167
+ if return_triangles:
1168
+ # log.debug('preparing triangles array')
1169
+ k_triangles = np.emptry((0,), dtype = np.int32) if k_props is None else k_props.pop(0)
1170
+ j_triangles = np.emptry((0,), dtype = np.int32) if j_props is None else j_props.pop(0)
1171
+ i_triangles = np.emptry((0,), dtype = np.int32) if i_props is None else i_props.pop(0)
1172
+ all_tris = np.concatenate((k_triangles, j_triangles, i_triangles), axis = 0)
1173
+ # log.debug(f'gcs count: {gcs.count}; all triangles shape: {all_tris.shape}')
1174
+ assert all_tris.shape == (gcs.count,)
1175
+
1176
+ # NB. following assumes faces have been added to gcs in a particular order!
1177
+ if return_depths:
1178
+ # log.debug('preparing depths array')
1179
+ k_depths = np.emptry((0,), dtype = np.float64) if k_props is None else k_props.pop(0)
1180
+ j_depths = np.emptry((0,), dtype = np.float64) if j_props is None else j_props.pop(0)
1181
+ i_depths = np.emptry((0,), dtype = np.float64) if i_props is None else i_props.pop(0)
1182
+ all_depths = np.concatenate((k_depths, j_depths, i_depths), axis = 0)
1183
+ # log.debug(f'gcs count: {gcs.count}; all depths shape: {all_depths.shape}')
1184
+ assert all_depths.shape == (gcs.count,)
1185
+
1186
+ # NB. following assumes faces have been added to gcs in a particular order!
1187
+ if return_offsets:
1188
+ # log.debug('preparing offsets array')
1189
+ k_offsets = np.emptry((0,), dtype = np.float64) if k_props is None else k_props[0]
1190
+ j_offsets = np.emptry((0,), dtype = np.float64) if j_props is None else j_props[0]
1191
+ i_offsets = np.emptry((0,), dtype = np.float64) if i_props is None else i_props[0]
1192
+ all_offsets = _all_offsets(grid.crs, k_offsets, j_offsets, i_offsets)
1193
+ # log.debug(f'gcs count: {gcs.count}; all offsets shape: {all_offsets.shape}')
1194
+ assert all_offsets.shape == (gcs.count,)
1195
+
1196
+ if return_flange_bool:
1197
+ # log.debug('preparing flange array')
1198
+ flange_bool_uuid = surface.model.uuid(title = "flange bool",
1199
+ obj_type = "DiscreteProperty",
1200
+ related_uuid = surface.uuid)
1201
+ assert (flange_bool_uuid is not None), f"No flange bool property found for surface: {surface.title}"
1202
+ flange_bool = rqp.Property(surface.model, uuid = flange_bool_uuid)
1203
+ flange_array = flange_bool.array_ref(dtype = bool)
1204
+ all_flange = np.take(flange_array, all_tris)
1205
+ assert all_flange.shape == (gcs.count,)
1206
+
1207
+ # note: following is a grid cells property, not a gcs property
1208
+ if return_bisector:
1209
+ if is_curtain:
1210
+ log.debug("preparing columns bisector")
1211
+ bisector = column_bisector_from_face_indices((grid.nj, grid.ni), j_faces_kji0[:, 1:], i_faces_kji0[:, 1:])
1212
+ # log.debug('finished preparing columns bisector')
1213
+ else:
1214
+ log.debug("preparing cells bisector")
1215
+ bisector, is_curtain = bisector_from_faces(tuple(grid.extent_kji), k_faces_kji0, j_faces_kji0, i_faces_kji0,
1216
+ raw_bisector, True)
1217
+ if is_curtain:
1218
+ bisector = bisector[0] # reduce to a columns property
1219
+
1220
+ # note: following is a grid cells property, not a gcs property
1221
+ if return_shadow:
1222
+ log.debug("preparing cells shadow")
1223
+ shadow = shadow_from_face_indices(tuple(grid.extent_kji), k_faces_kji0)
1224
+
1225
+ if progress_fn is not None:
1226
+ progress_fn(1.0)
1227
+
1228
+ log.debug(f"finishing find_faces_to_represent_surface_regular_optimised for {name}")
1229
+
1230
+ # if returning properties, construct dictionary
1231
+ if return_properties:
1232
+ props_dict = {}
1233
+ if return_triangles:
1234
+ props_dict["triangle"] = all_tris
1235
+ if return_depths:
1236
+ props_dict["depth"] = all_depths
1237
+ if return_offsets:
1238
+ props_dict["offset"] = all_offsets
1239
+ if return_bisector:
1240
+ props_dict["grid bisector"] = (bisector, is_curtain)
1241
+ if return_shadow:
1242
+ props_dict["grid shadow"] = shadow
1243
+ if return_flange_bool:
1244
+ props_dict["flange bool"] = all_flange
1245
+ return (gcs, props_dict)
1246
+
1247
+ return gcs
1248
+
1249
+
864
1250
  def find_faces_to_represent_surface(grid, surface, name, mode = "auto", feature_type = "fault", progress_fn = None):
865
1251
  """Returns a grid connection set containing those cell faces which are deemed to represent the surface.
866
1252
 
@@ -888,19 +1274,7 @@ def find_faces_to_represent_surface(grid, surface, name, mode = "auto", feature_
888
1274
  mode = "regular_optimised"
889
1275
  else:
890
1276
  mode = "staffa"
891
- if mode == "staffa":
892
- return find_faces_to_represent_surface_staffa(grid,
893
- surface,
894
- name,
895
- feature_type = feature_type,
896
- progress_fn = progress_fn)
897
- elif mode == "regular":
898
- return find_faces_to_represent_surface_regular(grid,
899
- surface,
900
- name,
901
- feature_type = feature_type,
902
- progress_fn = progress_fn)
903
- elif mode == "regular_optimised":
1277
+ if mode == "regular_optimised":
904
1278
  return find_faces_to_represent_surface_regular_optimised(grid,
905
1279
  surface,
906
1280
  name,
@@ -914,177 +1288,129 @@ def find_faces_to_represent_surface(grid, surface, name, mode = "auto", feature_
914
1288
  name,
915
1289
  feature_type = feature_type,
916
1290
  progress_fn = progress_fn)
1291
+ elif mode == "staffa":
1292
+ return find_faces_to_represent_surface_staffa(grid,
1293
+ surface,
1294
+ name,
1295
+ feature_type = feature_type,
1296
+ progress_fn = progress_fn)
1297
+ elif mode == "regular_dense":
1298
+ return find_faces_to_represent_surface_regular_dense_optimised(grid,
1299
+ surface,
1300
+ name,
1301
+ feature_type = feature_type,
1302
+ progress_fn = progress_fn)
1303
+ elif mode == "regular":
1304
+ return find_faces_to_represent_surface_regular(grid,
1305
+ surface,
1306
+ name,
1307
+ feature_type = feature_type,
1308
+ progress_fn = progress_fn)
917
1309
  log.critical("unrecognised mode: " + str(mode))
918
1310
  return None
919
1311
 
920
1312
 
921
1313
  def bisector_from_faces( # type: ignore
922
- grid_extent_kji: Tuple[int, int, int],
923
- k_faces: np.ndarray,
924
- j_faces: np.ndarray,
925
- i_faces: np.ndarray,
926
- raw_bisector: bool,
927
- ) -> Tuple[np.ndarray, bool]:
1314
+ grid_extent_kji: Tuple[int, int, int], k_faces: np.ndarray, j_faces: np.ndarray, i_faces: np.ndarray,
1315
+ raw_bisector: bool, using_indices: bool) -> Tuple[np.ndarray, bool]:
928
1316
  """Creates a boolean array denoting the bisection of the grid by the face sets.
929
1317
 
930
1318
  arguments:
931
- grid_extent_kji (Tuple[int, int, int]): the shape of the grid.
932
- k_faces (np.ndarray): a boolean array of which faces represent the surface in the k dimension.
933
- j_faces (np.ndarray): a boolean array of which faces represent the surface in the j dimension.
934
- i_faces (np.ndarray): a boolean array of which faces represent the surface in the i dimension.
1319
+ - grid_extent_kji (Tuple[int, int, int]): the shape of the grid
1320
+ - k_faces (np.ndarray): a boolean array of which faces represent the surface in the k dimension
1321
+ - j_faces (np.ndarray): a boolean array of which faces represent the surface in the j dimension
1322
+ - i_faces (np.ndarray): a boolean array of which faces represent the surface in the i dimension
1323
+ - raw_bisector (bool): if True, the bisector is returned without determining which side is shallower
1324
+ - using_indices (bool): if True, k_faces etc. are list-like arrays of kji indices; if False, they
1325
+ are full boolean arrays covering the internal faces
935
1326
 
936
1327
  returns:
937
1328
  Tuple containing:
938
-
939
1329
  - array (np.ndarray): boolean bisectors array where values are True for cells on the side
940
- of the surface that has a lower mean k index on average and False for cells on the other side.
1330
+ of the surface that has a lower mean k index on average and False for cells on the other side.
941
1331
  - is_curtain (bool): True if the surface is a curtain (vertical), otherwise False.
942
1332
 
943
1333
  notes:
944
- The face sets must form a single 'sealed' cut of the grid (eg. not waving in and out of the grid).
945
- Any 'boxed in' parts of the grid (completely enclosed by bisecting faces) will be consistently
946
- assigned to either the True or False part.
1334
+ - the face sets must form a single 'sealed' cut of the grid (eg. not waving in and out of the grid)
1335
+ - any 'boxed in' parts of the grid (completely enclosed by bisecting faces) will be consistently
1336
+ assigned to either the True or False part
1337
+ - a value of False for using_indices is DEPRECATED, pending proving of newer indices based approach
947
1338
  """
948
1339
  assert len(grid_extent_kji) == 3
949
1340
 
950
- # Finding the surface boundary (includes a buffer slice where surface does not reach edge of grid).
951
- boundary = get_boundary(k_faces, j_faces, i_faces, grid_extent_kji)
952
-
953
- # Setting up the bisector array for the bounding box.
954
- bounding_array = np.zeros(
955
- (
956
- boundary["k_max"] - boundary["k_min"] + 1,
957
- boundary["j_max"] - boundary["j_min"] + 1,
958
- boundary["i_max"] - boundary["i_min"] + 1,
959
- ),
960
- dtype = np.bool_,
961
- )
1341
+ # find the surface boundary (includes a buffer slice where surface does not reach edge of grid)
1342
+ if using_indices:
1343
+ box = get_boundary_from_indices(k_faces, j_faces, i_faces, grid_extent_kji)
1344
+ # set k_faces as bool arrays covering box
1345
+ k_faces, j_faces, i_faces = _box_face_arrays_from_indices(k_faces, j_faces, i_faces, box)
1346
+ else:
1347
+ box = get_boundary(k_faces, j_faces, i_faces, grid_extent_kji)
1348
+ # switch k_faces etc. to box coverage
1349
+ k_faces = k_faces[box[0, 0]:box[1, 0] - 1, box[0, 1]:box[1, 1], box[0, 2]:box[1, 2]]
1350
+ j_faces = j_faces[box[0, 0]:box[1, 0], box[0, 1]:box[1, 1] - 1, box[0, 2]:box[1, 2]]
1351
+ i_faces = i_faces[box[0, 0]:box[1, 0], box[0, 1]:box[1, 1], box[0, 2]:box[1, 2] - 1]
962
1352
 
963
- # Seeding the bisector array from (0, 0, 0) up to the first faces that represent the surface.
964
- boundary_values = tuple(boundary.values())
965
- bounding_array, first_k, first_j, first_i = _seed_array((0, 0, 0), k_faces, j_faces, i_faces, boundary_values,
966
- bounding_array)
967
- points = set()
968
- for dimension, first_true in enumerate([first_k, first_j, first_i]):
969
- for dimension_value in range(1, first_true):
970
- point = [0, 0, 0]
971
- point[dimension] = dimension_value
972
- point = tuple(point) # type: ignore
973
- bounding_array, first_k_sub, first_j_sub, first_i_sub = _seed_array(point, k_faces, j_faces, i_faces,
974
- boundary_values, bounding_array)
975
- for sub_dimension, first_true_sub in enumerate([first_k_sub, first_j_sub, first_i_sub]):
976
- if dimension != sub_dimension:
977
- for sub_dimension_value in range(1, first_true_sub):
978
- point = [0, 0, 0]
979
- point[dimension] = dimension_value
980
- point[sub_dimension] = sub_dimension_value
981
- point = tuple(point) # type: ignore
982
- if point not in points:
983
- points.add(point)
984
- bounding_array, _, _, _ = _seed_array(
985
- point,
986
- k_faces,
987
- j_faces,
988
- i_faces,
989
- boundary_values,
990
- bounding_array,
991
- )
992
-
993
- # Setting up the array for the changing values.
994
- changing_array = np.zeros_like(bounding_array, dtype = np.bool_)
995
-
996
- # Repeatedly spreading True values to neighbouring cells that are not the other side of a face.
997
- # yapf: disable
998
- open_k = np.logical_not(k_faces)[
999
- boundary["k_min"]:boundary["k_max"],
1000
- boundary["j_min"]:boundary["j_max"] + 1,
1001
- boundary["i_min"]:boundary["i_max"] + 1,
1002
- ]
1003
- open_j = np.logical_not(j_faces)[
1004
- boundary["k_min"]:boundary["k_max"] + 1,
1005
- boundary["j_min"]:boundary["j_max"],
1006
- boundary["i_min"]:boundary["i_max"] + 1,
1007
- ]
1008
- open_i = np.logical_not(i_faces)[
1009
- boundary["k_min"]:boundary["k_max"] + 1,
1010
- boundary["j_min"]:boundary["j_max"] + 1,
1011
- boundary["i_min"]:boundary["i_max"],
1012
- ]
1013
- # yapf: enable
1014
- while True:
1015
- changing_array[:] = False
1016
-
1017
- # k faces
1018
- changing_array[1:, :, :] = np.logical_and(bounding_array[:-1, :, :], open_k)
1019
- changing_array[:-1, :, :] = np.logical_or(changing_array[:-1, :, :],
1020
- np.logical_and(bounding_array[1:, :, :], open_k))
1353
+ box_shape = box[1, :] - box[0, :]
1021
1354
 
1022
- # j faces
1023
- changing_array[:, 1:, :] = np.logical_or(changing_array[:, 1:, :],
1024
- np.logical_and(bounding_array[:, :-1, :], open_j))
1025
- changing_array[:, :-1, :] = np.logical_or(changing_array[:, :-1, :],
1026
- np.logical_and(bounding_array[:, 1:, :], open_j))
1355
+ # set up the bisector array for the bounding box
1356
+ box_array = np.zeros(box_shape, dtype = np.bool_)
1027
1357
 
1028
- # i faces
1029
- changing_array[:, :, 1:] = np.logical_or(changing_array[:, :, 1:],
1030
- np.logical_and(bounding_array[:, :, :-1], open_i))
1031
- changing_array[:, :, :-1] = np.logical_or(changing_array[:, :, :-1],
1032
- np.logical_and(bounding_array[:, :, 1:], open_i))
1358
+ # seed the bisector box array at (0, 0, 0)
1359
+ box_array[0, 0, 0] = True
1033
1360
 
1034
- changing_array[:] = np.logical_and(changing_array, np.logical_not(bounding_array))
1035
- if np.count_nonzero(changing_array) == 0:
1036
- break
1037
- bounding_array = np.logical_or(bounding_array, changing_array)
1361
+ # prepare to spread True values to neighbouring cells that are not the other side of a face
1362
+ open_k = np.logical_not(k_faces)
1363
+ open_j = np.logical_not(j_faces)
1364
+ open_i = np.logical_not(i_faces)
1038
1365
 
1039
- # Setting up the full bisectors array and assigning the bounding box values.
1366
+ # populate bisector array for box
1367
+ _fill_bisector(box_array, open_k, open_j, open_i)
1368
+
1369
+ # set up the full bisectors array and assigning the bounding box values
1040
1370
  array = np.zeros(grid_extent_kji, dtype = np.bool_)
1041
- # yapf: disable
1042
- array[
1043
- boundary["k_min"]:boundary["k_max"] + 1,
1044
- boundary["j_min"]:boundary["j_max"] + 1,
1045
- boundary["i_min"]:boundary["i_max"] + 1,
1046
- ] = bounding_array
1047
- # yapf: enable
1048
-
1049
- # Setting values outside of the bounding box.
1050
- if boundary["k_max"] != grid_extent_kji[0] - 1 and np.any(bounding_array[-1, :, :]):
1051
- array[boundary["k_max"] + 1:, :, :] = True
1052
- if boundary["k_min"] != 0:
1053
- array[:boundary["k_min"], :, :] = True
1054
- if boundary["j_max"] != grid_extent_kji[1] - 1 and np.any(bounding_array[:, -1, :]):
1055
- array[:, boundary["j_max"] + 1:, :] = True
1056
- if boundary["j_min"] != 0:
1057
- array[:, :boundary["j_min"], :] = True
1058
- if boundary["i_max"] != grid_extent_kji[2] - 1 and np.any(bounding_array[:, :, -1]):
1059
- array[:, :, boundary["i_max"] + 1:] = True
1060
- if boundary["i_min"] != 0:
1061
- array[:, :, :boundary["i_min"]] = True
1062
-
1063
- # Check all array elements are not the same.
1371
+ array[box[0, 0]:box[1, 0], box[0, 1]:box[1, 1], box[0, 2]:box[1, 2]] = box_array
1372
+
1373
+ # set bisector values outside of the bounding box
1374
+ _set_bisector_outside_box(array, box, box_array)
1375
+
1376
+ # check all array elements are not the same
1064
1377
  true_count = np.count_nonzero(array)
1065
1378
  cell_count = array.size
1066
- assert (0 < true_count < cell_count), "Face set for surface is leaky or empty (surface does not intersect grid)."
1067
-
1068
- # Negate the array if it minimises the mean k and determine if the surface is a curtain.
1069
- layer_cell_count = grid_extent_kji[1] * grid_extent_kji[2]
1070
- array_k_sum = 0
1071
- array_opposite_k_sum = 0
1072
- is_curtain = False
1073
- for k in range(grid_extent_kji[0]):
1074
- array_layer_count = np.count_nonzero(array[k])
1075
- array_k_sum += (k + 1) * array_layer_count
1076
- array_opposite_k_sum += (k + 1) * (layer_cell_count - array_layer_count)
1077
- array_mean_k = float(array_k_sum) / float(true_count)
1078
- array_opposite_mean_k = float(array_opposite_k_sum) / float(cell_count - true_count)
1079
- if array_mean_k > array_opposite_mean_k and not raw_bisector:
1080
- array[:] = np.logical_not(array)
1081
- if abs(array_mean_k - array_opposite_mean_k) <= 0.001:
1082
- # log.warning('unable to determine which side of surface is shallower')
1083
- is_curtain = True
1379
+ assert (0 < true_count < cell_count), "face set for surface is leaky or empty (surface does not intersect grid)"
1380
+
1381
+ # negate the array if it minimises the mean k and determine if the surface is a curtain
1382
+ is_curtain = _shallow_or_curtain(array, true_count, raw_bisector)
1084
1383
 
1085
1384
  return array, is_curtain
1086
1385
 
1087
1386
 
1387
+ def column_bisector_from_face_indices(grid_extent_ji: Tuple[int, int], j_faces_ji0: np.ndarray,
1388
+ i_faces_ji0: np.ndarray) -> np.ndarray:
1389
+ """Returns a numpy bool array denoting the bisection of the top layer of the grid by the curtain face sets.
1390
+
1391
+ arguments:
1392
+ - grid_extent_ji (pair of int): the shape of a layer of the grid
1393
+ - j_faces_ji0, i_faces_ji0 (2D numpy int arrays of shape (N, 2)): indices of faces within a layer
1394
+
1395
+ returns:
1396
+ numpy bool array of shape grid_extent_ji, set True for cells on one side of the face sets;
1397
+ set False for cells on othe side
1398
+
1399
+ notes:
1400
+ - the face sets must form a single 'sealed' cut of the grid (eg. not waving in and out of the grid)
1401
+ - any 'boxed in' parts of the grid (completely enclosed by bisecting faces) will be consistently
1402
+ assigned to the False part
1403
+ - the resulting array is suitable for use as a grid property with indexable element of columns
1404
+ - the array is set True for the side of the curtain that contains cell [0, 0]
1405
+ """
1406
+ assert len(grid_extent_ji) == 2
1407
+ j_faces = np.zeros((grid_extent_ji[0] - 1, grid_extent_ji[1]), dtype = np.bool_)
1408
+ i_faces = np.zeros((grid_extent_ji[0], grid_extent_ji[1] - 1), dtype = np.bool_)
1409
+ j_faces[j_faces_ji0[:, 0], j_faces_ji0[:, 1]] = True
1410
+ i_faces[i_faces_ji0[:, 0], i_faces_ji0[:, 1]] = True
1411
+ return column_bisector_from_faces(grid_extent_ji, j_faces, i_faces)
1412
+
1413
+
1088
1414
  def column_bisector_from_faces(grid_extent_ji: Tuple[int, int], j_faces: np.ndarray, i_faces: np.ndarray) -> np.ndarray:
1089
1415
  """Returns a numpy bool array denoting the bisection of the top layer of the grid by the curtain face sets.
1090
1416
 
@@ -1135,6 +1461,42 @@ def column_bisector_from_faces(grid_extent_ji: Tuple[int, int], j_faces: np.ndar
1135
1461
  return a
1136
1462
 
1137
1463
 
1464
+ def shadow_from_face_indices(extent_kji, kji0):
1465
+ """Returns a numpy int8 array indicating whether cells are above, below or between K faces.
1466
+
1467
+ arguments:
1468
+ extent_kji (triple int): the shape of the grid
1469
+ kji0 (numpy int array of shape (N, 3)): indices where a K face is present
1470
+
1471
+ returns:
1472
+ numpy int8 array of shape extent_kji; values are: 0 neither above nor below a K face;
1473
+ 1: above any K faces in the column; 2 below any K faces in the column;
1474
+ 3: between K faces (one or more above and one or more below)
1475
+ """
1476
+ assert len(extent_kji) == 3
1477
+ limit = extent_kji[0] - 1 # maximum number of iterations needed to spead shadow
1478
+ shadow = np.zeros(extent_kji, dtype = np.int8)
1479
+ shadow[kji0[:, 0], kji0[:, 1], kji0[:, 2]] = 1
1480
+ shadow[kji0[:, 0] + 1, kji0[:, 1], kji0[:, 2]] += 2
1481
+ for _ in range(limit):
1482
+ c = np.logical_and(shadow[:-1] == 0, shadow[1:] == 1)
1483
+ if np.count_nonzero(c) == 0:
1484
+ break
1485
+ shadow[:-1][c] = 1
1486
+ for _ in range(limit):
1487
+ c = np.logical_and(shadow[1:] == 0, shadow[:-1] == 2)
1488
+ if np.count_nonzero(c) == 0:
1489
+ break
1490
+ shadow[1:][c] = 2
1491
+ for _ in range(limit):
1492
+ c = np.logical_and(shadow[:-1] >= 2, shadow[1:] == 1)
1493
+ if np.count_nonzero(c) == 0:
1494
+ break
1495
+ shadow[:-1][c] = 3
1496
+ shadow[1:][c] = 3
1497
+ return shadow
1498
+
1499
+
1138
1500
  def shadow_from_faces(extent_kji, k_faces):
1139
1501
  """Returns a numpy int8 array indicating whether cells are above, below or between K faces.
1140
1502
 
@@ -1176,8 +1538,8 @@ def get_boundary( # type: ignore
1176
1538
  j_faces: np.ndarray,
1177
1539
  i_faces: np.ndarray,
1178
1540
  grid_extent_kji: Tuple[int, int, int],
1179
- ) -> Dict[str, int]:
1180
- """Cretaes a dictionary of the indices that bound the surface (where the faces are True).
1541
+ ) -> np.ndarray:
1542
+ """Cretaes a box of the indices that bound the surface (where the faces are True).
1181
1543
 
1182
1544
  arguments:
1183
1545
  k_faces (np.ndarray): a boolean array of which faces represent the surface in the k dimension
@@ -1186,21 +1548,14 @@ def get_boundary( # type: ignore
1186
1548
  grid_extent_kji (Tuple[int, int, int]): the shape of the grid
1187
1549
 
1188
1550
  returns:
1189
- boundary (Dict[str, int]): a dictionary of the indices that bound the surface
1551
+ int array of shape (2, 3): bounding box in python protocol (max values have been incremented)
1190
1552
 
1191
1553
  note:
1192
1554
  input faces arrays are for internal grid faces (ie. extent reduced by 1 in axis of faces);
1193
1555
  a buffer slice is included where the surface does not reach the edge of the grid
1194
1556
  """
1195
1557
 
1196
- boundary = {
1197
- "k_min": None,
1198
- "k_max": None,
1199
- "j_min": None,
1200
- "j_max": None,
1201
- "i_min": None,
1202
- "i_max": None,
1203
- }
1558
+ boundary = np.zeros((2, 3), dtype = np.int32)
1204
1559
 
1205
1560
  starting = True
1206
1561
 
@@ -1247,26 +1602,72 @@ def get_boundary( # type: ignore
1247
1602
  max_i += 1
1248
1603
 
1249
1604
  if starting:
1250
- boundary["k_min"] = min_k
1251
- boundary["k_max"] = max_k
1252
- boundary["j_min"] = min_j
1253
- boundary["j_max"] = max_j
1254
- boundary["i_min"] = min_i
1255
- boundary["i_max"] = max_i
1605
+ boundary[0, 0] = min_k
1606
+ boundary[1, 0] = max_k
1607
+ boundary[0, 1] = min_j
1608
+ boundary[1, 1] = max_j
1609
+ boundary[0, 2] = min_i
1610
+ boundary[1, 2] = max_i
1256
1611
  starting = False
1257
1612
  else:
1258
- if min_k < boundary["k_min"]:
1259
- boundary["k_min"] = min_k
1260
- if max_k > boundary["k_max"]:
1261
- boundary["k_max"] = max_k
1262
- if min_j < boundary["j_min"]:
1263
- boundary["j_min"] = min_j
1264
- if max_j > boundary["j_max"]:
1265
- boundary["j_max"] = max_j
1266
- if min_i < boundary["i_min"]:
1267
- boundary["i_min"] = min_i
1268
- if max_i > boundary["i_max"]:
1269
- boundary["i_max"] = max_i
1613
+ if min_k < boundary[0, 0]:
1614
+ boundary[0, 0] = min_k
1615
+ if max_k > boundary[1, 0]:
1616
+ boundary[1, 0] = max_k
1617
+ if min_j < boundary[0, 1]:
1618
+ boundary[0, 1] = min_j
1619
+ if max_j > boundary[1, 1]:
1620
+ boundary[1, 1] = max_j
1621
+ if min_i < boundary[0, 2]:
1622
+ boundary[0, 2] = min_i
1623
+ if max_i > boundary[1, 2]:
1624
+ boundary[1, 2] = max_i
1625
+
1626
+ boundary[1, :] += 1 # increment max values to give python protocol box
1627
+
1628
+ return boundary # type: ignore
1629
+
1630
+
1631
+ def get_boundary_dict( # type: ignore
1632
+ k_faces: np.ndarray,
1633
+ j_faces: np.ndarray,
1634
+ i_faces: np.ndarray,
1635
+ grid_extent_kji: Tuple[int, int, int],
1636
+ ) -> Dict[str, int]:
1637
+ """Cretaes a dictionary of the indices that bound the surface (where the faces are True).
1638
+
1639
+ arguments:
1640
+ k_faces (np.ndarray): a boolean array of which faces represent the surface in the k dimension
1641
+ j_faces (np.ndarray): a boolean array of which faces represent the surface in the j dimension
1642
+ i_faces (np.ndarray): a boolean array of which faces represent the surface in the i dimension
1643
+ grid_extent_kji (Tuple[int, int, int]): the shape of the grid
1644
+
1645
+ returns:
1646
+ boundary (Dict[str, int]): a dictionary of the indices that bound the surface
1647
+
1648
+ note:
1649
+ input faces arrays are for internal grid faces (ie. extent reduced by 1 in axis of faces);
1650
+ a buffer slice is included where the surface does not reach the edge of the grid;
1651
+ max values are not increment, ie. need to be incremented to be used as an upper end of a python range
1652
+ """
1653
+
1654
+ boundary = {
1655
+ "k_min": None,
1656
+ "k_max": None,
1657
+ "j_min": None,
1658
+ "j_max": None,
1659
+ "i_min": None,
1660
+ "i_max": None,
1661
+ }
1662
+
1663
+ box = get_boundary(k_faces, j_faces, i_faces, grid_extent_kji)
1664
+
1665
+ boundary["k_min"] = box[0, 0]
1666
+ boundary["k_max"] = box[1, 0] - 1
1667
+ boundary["j_min"] = box[0, 1]
1668
+ boundary["j_max"] = box[1, 1] - 1
1669
+ boundary["i_min"] = box[0, 2]
1670
+ boundary["i_max"] = box[1, 2] - 1
1270
1671
 
1271
1672
  return boundary # type: ignore
1272
1673
 
@@ -1278,7 +1679,7 @@ def _where_true(data: np.ndarray):
1278
1679
 
1279
1680
 
1280
1681
  @njit # pragma: no cover
1281
- def _first_true(array: np.ndarray) -> Optional[int]: # type: ignore
1682
+ def _first_true(array: np.ndarray) -> int: # type: ignore
1282
1683
  """Returns the index + 1 of the first True value in the array."""
1283
1684
  for idx, val in np.ndenumerate(array):
1284
1685
  if val:
@@ -1369,6 +1770,7 @@ def intersect_numba(
1369
1770
  face_idx[index2] = d2
1370
1771
  face_idx[2 - axis] = face
1371
1772
 
1773
+ # dangerous: relies on indivisible read-modify-write of memory word containing multiple faces elements
1372
1774
  faces[face_idx[0], face_idx[1], face_idx[2]] = True
1373
1775
 
1374
1776
  if return_depths:
@@ -1387,19 +1789,19 @@ def _seed_array(
1387
1789
  k_faces: np.ndarray,
1388
1790
  j_faces: np.ndarray,
1389
1791
  i_faces: np.ndarray,
1390
- boundary: Tuple[int, int, int, int, int, int],
1792
+ box: np.ndarray,
1391
1793
  array: np.ndarray,
1392
1794
  ) -> Tuple[np.ndarray, int, int, int]:
1393
1795
  """Sets values of the array True up until a face is hit in each direction.
1394
1796
 
1395
1797
  arguments:
1396
- point (Tuple[int, int, int]): coordinates of the initial seed point.
1397
- k_faces (np.ndarray): a boolean array of which faces represent the surface in the k dimension.
1398
- j_faces (np.ndarray): a boolean array of which faces represent the surface in the j dimension.
1399
- i_faces (np.ndarray): a boolean array of which faces represent the surface in the i dimension.
1400
- boundary (Tuple[int, int, int, int, int, int]): the boundaries of the surface given in the order
1401
- (minimum k, maximum k, minimum j, maximum j, minimum i, maximum i).
1402
- array (np.ndarray): boolean array that will be seeded.
1798
+
1799
+ - point (Tuple[int, int, int]): coordinates of the initial seed point
1800
+ - k_faces (np.ndarray): a boolean array of which faces represent the surface in the k dimension
1801
+ - j_faces (np.ndarray): a boolean array of which faces represent the surface in the j dimension
1802
+ - i_faces (np.ndarray): a boolean array of which faces represent the surface in the i dimension
1803
+ - box (numpy int array of shape (2, 3)): the boundaries of the surface in python protocol
1804
+ - array (np.ndarray): boolean array that will be seeded
1403
1805
 
1404
1806
  returns:
1405
1807
  Tuple containing:
@@ -1411,6 +1813,10 @@ def _seed_array(
1411
1813
  array size in the j direction if there are no j faces.
1412
1814
  - first_i (int): the index of the first i face in the i direction from the seed point or the
1413
1815
  array size in the i direction if there are no i faces.
1816
+
1817
+ note:
1818
+
1819
+ - this function is DEPRECATED as it is no longer in use
1414
1820
  """
1415
1821
  k = point[0]
1416
1822
  j = point[1]
@@ -1418,17 +1824,17 @@ def _seed_array(
1418
1824
 
1419
1825
  first_k = 0
1420
1826
  if k == 0:
1421
- first_k = _first_true(k_faces[boundary[0]:boundary[1], boundary[2] + j, boundary[4] + i])
1827
+ first_k = _first_true(k_faces[box[0, 0]:box[1, 0] - 1, box[0, 1] + j, box[0, 2] + i])
1422
1828
  array[:first_k, j, i] = True
1423
1829
 
1424
1830
  first_j = 0
1425
1831
  if j == 0:
1426
- first_j = _first_true(j_faces[boundary[0] + k, boundary[2]:boundary[3], boundary[4] + i])
1832
+ first_j = _first_true(j_faces[box[0, 0] + k, box[0, 1]:box[1, 1] - 1, box[0, 2] + i])
1427
1833
  array[k, :first_j, i] = True
1428
1834
 
1429
1835
  first_i = 0
1430
1836
  if i == 0:
1431
- first_i = _first_true(i_faces[boundary[0] + k, boundary[2] + j, boundary[4]:boundary[5]])
1837
+ first_i = _first_true(i_faces[box[0, 0] + k, box[0, 1] + j, box[0, 2]:box[1, 2] - 1])
1432
1838
  array[k, j, :first_i] = True
1433
1839
 
1434
1840
  return array, first_k, first_j, first_i
@@ -1440,3 +1846,116 @@ def _all_offsets(crs, k_offsets_list, j_offsets_list, i_offsets_list):
1440
1846
  ji_offsets = np.concatenate((j_offsets_list, i_offsets_list), axis = 0)
1441
1847
  wam.convert_lengths(ji_offsets, crs.xy_units, crs.z_units)
1442
1848
  return np.concatenate((k_offsets_list, ji_offsets), axis = 0)
1849
+
1850
+
1851
+ @njit # pragma: no cover
1852
+ def _fill_bisector(bisect: np.ndarray, open_k: np.ndarray, open_j: np.ndarray, open_i: np.ndarray):
1853
+ change = np.zeros(bisect.shape, dtype = np.bool_)
1854
+ nk: int = bisect.shape[0]
1855
+ nj: int = bisect.shape[1]
1856
+ ni: int = bisect.shape[2]
1857
+ going: bool = True
1858
+ while going:
1859
+ going = False
1860
+ change[:] = False
1861
+ for k in range(nk):
1862
+ for j in range(nj):
1863
+ for i in range(ni):
1864
+ if bisect[k, j, i]:
1865
+ continue
1866
+ if ((k and bisect[k - 1, j, i] and open_k[k - 1, j, i]) or
1867
+ (j and bisect[k, j - 1, i] and open_j[k, j - 1, i]) or
1868
+ (i and bisect[k, j, i - 1] and open_i[k, j, i - 1]) or
1869
+ (k < nk - 1 and bisect[k + 1, j, i] and open_k[k, j, i]) or
1870
+ (j < nj - 1 and bisect[k, j + 1, i] and open_j[k, j, i]) or
1871
+ (i < ni - 1 and bisect[k, j, i + 1] and open_i[k, j, i])):
1872
+ bisect[k, j, i] = True
1873
+ going = True
1874
+ continue
1875
+
1876
+
1877
+ @njit # pragma: no cover
1878
+ def _shallow_or_curtain(a: np.ndarray, true_count: int, raw: bool) -> bool:
1879
+ # negate the bool array if it minimises the mean k and determine if the bisector indicates a curtain
1880
+ assert a.ndim == 3
1881
+ layer_cell_count: int = a.shape[1] * a.shape[2]
1882
+ k_sum: int = 0
1883
+ opposite_k_sum: int = 0
1884
+ is_curtain: bool = False
1885
+ layer_count: int = 0
1886
+ for k in range(a.shape[0]):
1887
+ layer_count = np.count_nonzero(a[k])
1888
+ k_sum += (k + 1) * layer_count
1889
+ opposite_k_sum += (k + 1) * (layer_cell_count - layer_count)
1890
+ mean_k: float = float(k_sum) / float(true_count)
1891
+ opposite_mean_k: float = float(opposite_k_sum) / float(a.size - true_count)
1892
+ if mean_k > opposite_mean_k and not raw:
1893
+ a[:] = np.logical_not(a)
1894
+ if abs(mean_k - opposite_mean_k) <= 0.001:
1895
+ # log.warning('unable to determine which side of surface is shallower')
1896
+ is_curtain = True
1897
+ return is_curtain
1898
+
1899
+
1900
+ def _set_bisector_outside_box(a: np.ndarray, box: np.ndarray, box_array: np.ndarray):
1901
+ # set values outside of the bounding box
1902
+ if box[1, 0] < a.shape[0] and np.any(box_array[-1, :, :]):
1903
+ a[box[1, 0]:, :, :] = True
1904
+ if box[0, 0] != 0:
1905
+ a[:box[0, 0], :, :] = True
1906
+ if box[1, 1] < a.shape[1] and np.any(box_array[:, -1, :]):
1907
+ a[:, box[1, 1]:, :] = True
1908
+ if box[0, 1] != 0:
1909
+ a[:, :box[0, 1], :] = True
1910
+ if box[1, 2] < a.shape[2] and np.any(box_array[:, :, -1]):
1911
+ a[:, :, box[1, 2]:] = True
1912
+ if box[0, 2] != 0:
1913
+ a[:, :, :box[0, 2]] = True
1914
+
1915
+
1916
+ def _box_face_arrays_from_indices(k_faces: np.ndarray, j_faces: np.ndarray, i_faces: np.ndarray,
1917
+ box: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
1918
+ box_shape = box[1, :] - box[0, :]
1919
+ k_a = np.zeros((box_shape[0] - 1, box_shape[1], box_shape[2]), dtype = np.bool_)
1920
+ j_a = np.zeros((box_shape[0], box_shape[1] - 1, box_shape[2]), dtype = np.bool_)
1921
+ i_a = np.zeros((box_shape[0], box_shape[1], box_shape[2] - 1), dtype = np.bool_)
1922
+ ko = box[0, 0]
1923
+ jo = box[0, 1]
1924
+ io = box[0, 2]
1925
+ _set_face_array(k_a, k_faces, ko, jo, io)
1926
+ _set_face_array(j_a, j_faces, ko, jo, io)
1927
+ _set_face_array(i_a, i_faces, ko, jo, io)
1928
+ return k_a, j_a, i_a
1929
+
1930
+
1931
+ @njit # pragma: no cover
1932
+ def _set_face_array(a: np.ndarray, indices: np.ndarray, ko: int, jo: int, io: int):
1933
+ for ind in range(len(indices)):
1934
+ k = indices[ind, 0] - ko
1935
+ j = indices[ind, 1] - jo
1936
+ i = indices[ind, 2] - io
1937
+ a[k, j, i] = True
1938
+
1939
+
1940
+ def get_boundary_from_indices(k_faces: np.ndarray, j_faces: np.ndarray, i_faces: np.ndarray,
1941
+ grid_extent_kji: Tuple[int, int, int]) -> np.ndarray:
1942
+ """Return python protocol box containing indices"""
1943
+ k_min_kji0 = np.min(k_faces, axis = 0)
1944
+ k_max_kji0 = np.max(k_faces, axis = 0)
1945
+ j_min_kji0 = np.min(j_faces, axis = 0)
1946
+ j_max_kji0 = np.max(j_faces, axis = 0)
1947
+ i_min_kji0 = np.min(i_faces, axis = 0)
1948
+ i_max_kji0 = np.max(i_faces, axis = 0)
1949
+ box = np.empty((2, 3), dtype = np.int32)
1950
+ box[0, 0] = min(k_min_kji0[0], j_min_kji0[0], i_min_kji0[0])
1951
+ box[0, 1] = min(k_min_kji0[1], j_min_kji0[1], i_min_kji0[1])
1952
+ box[0, 2] = min(k_min_kji0[2], j_min_kji0[2], i_min_kji0[2])
1953
+ box[1, 0] = max(k_max_kji0[0], j_max_kji0[0], i_max_kji0[0]) + 1
1954
+ box[1, 1] = max(k_max_kji0[1], j_max_kji0[1], i_max_kji0[1]) + 1
1955
+ box[1, 2] = max(k_max_kji0[2], j_max_kji0[2], i_max_kji0[2]) + 1
1956
+ box[0, :] = np.maximum(box[0, :] - 1, 0)
1957
+ # include buffer layer where box does not reach edge of grid
1958
+ extent_kji = np.array(grid_extent_kji, dtype = np.int32)
1959
+ assert np.all(box[1] <= grid_extent_kji)
1960
+ box[1, :] = np.minimum(box[1, :] + 1, extent_kji)
1961
+ return box