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.
- resqpy/__init__.py +1 -1
- resqpy/fault/_grid_connection_set.py +224 -62
- resqpy/grid/_grid.py +4 -0
- resqpy/grid_surface/__init__.py +4 -0
- resqpy/grid_surface/_blocked_well_populate.py +5 -5
- resqpy/grid_surface/_find_faces.py +731 -212
- resqpy/model/_hdf5.py +3 -3
- resqpy/olio/triangulation.py +17 -13
- resqpy/olio/vector_utilities.py +175 -1
- resqpy/olio/wellspec_keywords.py +16 -10
- resqpy/property/grid_property_collection.py +10 -10
- resqpy/rq_import/_import_vdb_ensemble.py +12 -13
- resqpy/surface/_mesh.py +4 -0
- resqpy/surface/_surface.py +40 -24
- resqpy/surface/_tri_mesh.py +8 -7
- resqpy/surface/_triangulated_patch.py +71 -51
- resqpy/well/_blocked_well.py +28 -25
- resqpy/well/_trajectory.py +2 -2
- resqpy/well/blocked_well_frame.py +1 -1
- resqpy/well/well_object_funcs.py +5 -5
- {resqpy-4.16.11.dist-info → resqpy-4.17.1.dist-info}/METADATA +1 -1
- {resqpy-4.16.11.dist-info → resqpy-4.17.1.dist-info}/RECORD +24 -24
- {resqpy-4.16.11.dist-info → resqpy-4.17.1.dist-info}/LICENSE +0 -0
- {resqpy-4.16.11.dist-info → resqpy-4.17.1.dist-info}/WHEEL +0 -0
@@ -495,18 +495,18 @@ def find_faces_to_represent_surface_regular(
|
|
495
495
|
return gcs
|
496
496
|
|
497
497
|
|
498
|
-
def
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
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 =
|
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 =
|
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 =
|
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 == "
|
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
|
-
|
923
|
-
|
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
|
-
|
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
|
-
|
945
|
-
|
946
|
-
|
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
|
-
#
|
951
|
-
|
952
|
-
|
953
|
-
|
954
|
-
|
955
|
-
|
956
|
-
|
957
|
-
|
958
|
-
|
959
|
-
|
960
|
-
|
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
|
-
|
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
|
-
|
1023
|
-
|
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
|
-
|
1029
|
-
|
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
|
-
|
1035
|
-
|
1036
|
-
|
1037
|
-
|
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
|
-
#
|
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
|
-
|
1042
|
-
|
1043
|
-
|
1044
|
-
|
1045
|
-
|
1046
|
-
|
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), "
|
1067
|
-
|
1068
|
-
#
|
1069
|
-
|
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
|
-
) ->
|
1180
|
-
"""Cretaes a
|
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
|
-
|
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[
|
1251
|
-
boundary[
|
1252
|
-
boundary[
|
1253
|
-
boundary[
|
1254
|
-
boundary[
|
1255
|
-
boundary[
|
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[
|
1259
|
-
boundary[
|
1260
|
-
if max_k > boundary[
|
1261
|
-
boundary[
|
1262
|
-
if min_j < boundary[
|
1263
|
-
boundary[
|
1264
|
-
if max_j > boundary[
|
1265
|
-
boundary[
|
1266
|
-
if min_i < boundary[
|
1267
|
-
boundary[
|
1268
|
-
if max_i > boundary[
|
1269
|
-
boundary[
|
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) ->
|
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
|
-
|
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
|
-
|
1397
|
-
|
1398
|
-
|
1399
|
-
|
1400
|
-
|
1401
|
-
|
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[
|
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[
|
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[
|
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
|