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

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. resqpy/__init__.py +1 -1
  2. resqpy/fault/_gcs_functions.py +10 -10
  3. resqpy/fault/_grid_connection_set.py +277 -113
  4. resqpy/grid/__init__.py +2 -3
  5. resqpy/grid/_defined_geometry.py +3 -3
  6. resqpy/grid/_extract_functions.py +2 -1
  7. resqpy/grid/_grid.py +95 -12
  8. resqpy/grid/_grid_types.py +22 -7
  9. resqpy/grid/_points_functions.py +1 -1
  10. resqpy/grid/_regular_grid.py +6 -2
  11. resqpy/grid_surface/__init__.py +17 -38
  12. resqpy/grid_surface/_blocked_well_populate.py +5 -5
  13. resqpy/grid_surface/_find_faces.py +1349 -253
  14. resqpy/lines/_polyline.py +24 -33
  15. resqpy/model/_catalogue.py +9 -0
  16. resqpy/model/_forestry.py +18 -14
  17. resqpy/model/_hdf5.py +11 -3
  18. resqpy/model/_model.py +85 -10
  19. resqpy/model/_xml.py +38 -13
  20. resqpy/multi_processing/wrappers/grid_surface_mp.py +92 -37
  21. resqpy/olio/read_nexus_fault.py +8 -2
  22. resqpy/olio/relperm.py +1 -1
  23. resqpy/olio/transmission.py +8 -8
  24. resqpy/olio/triangulation.py +36 -30
  25. resqpy/olio/vector_utilities.py +340 -6
  26. resqpy/olio/volume.py +0 -20
  27. resqpy/olio/wellspec_keywords.py +19 -13
  28. resqpy/olio/write_hdf5.py +1 -1
  29. resqpy/olio/xml_et.py +12 -0
  30. resqpy/property/__init__.py +6 -4
  31. resqpy/property/_collection_add_part.py +4 -3
  32. resqpy/property/_collection_create_xml.py +4 -2
  33. resqpy/property/_collection_get_attributes.py +4 -0
  34. resqpy/property/attribute_property_set.py +311 -0
  35. resqpy/property/grid_property_collection.py +11 -11
  36. resqpy/property/property_collection.py +79 -31
  37. resqpy/property/property_common.py +3 -8
  38. resqpy/rq_import/_add_surfaces.py +34 -14
  39. resqpy/rq_import/_grid_from_cp.py +2 -2
  40. resqpy/rq_import/_import_nexus.py +75 -48
  41. resqpy/rq_import/_import_vdb_all_grids.py +64 -52
  42. resqpy/rq_import/_import_vdb_ensemble.py +12 -13
  43. resqpy/surface/_mesh.py +4 -0
  44. resqpy/surface/_surface.py +593 -118
  45. resqpy/surface/_tri_mesh.py +13 -10
  46. resqpy/surface/_tri_mesh_stencil.py +4 -4
  47. resqpy/surface/_triangulated_patch.py +71 -51
  48. resqpy/time_series/_any_time_series.py +7 -4
  49. resqpy/time_series/_geologic_time_series.py +1 -1
  50. resqpy/unstructured/_hexa_grid.py +6 -2
  51. resqpy/unstructured/_prism_grid.py +13 -5
  52. resqpy/unstructured/_pyramid_grid.py +6 -2
  53. resqpy/unstructured/_tetra_grid.py +6 -2
  54. resqpy/unstructured/_unstructured_grid.py +6 -2
  55. resqpy/well/_blocked_well.py +1986 -1946
  56. resqpy/well/_deviation_survey.py +3 -3
  57. resqpy/well/_md_datum.py +11 -21
  58. resqpy/well/_trajectory.py +10 -5
  59. resqpy/well/_wellbore_frame.py +10 -2
  60. resqpy/well/blocked_well_frame.py +3 -3
  61. resqpy/well/well_object_funcs.py +7 -9
  62. resqpy/well/well_utils.py +33 -0
  63. {resqpy-4.14.2.dist-info → resqpy-5.1.5.dist-info}/METADATA +8 -9
  64. {resqpy-4.14.2.dist-info → resqpy-5.1.5.dist-info}/RECORD +66 -66
  65. {resqpy-4.14.2.dist-info → resqpy-5.1.5.dist-info}/WHEEL +1 -1
  66. resqpy/grid/_moved_functions.py +0 -15
  67. {resqpy-4.14.2.dist-info → resqpy-5.1.5.dist-info}/LICENSE +0 -0
@@ -8,9 +8,9 @@ import numpy as np
8
8
  import warnings
9
9
  import numba # type: ignore
10
10
  from numba import njit, prange # type: ignore
11
- from typing import Tuple, Optional, Dict
11
+ from typing import Tuple, Union, Dict
12
12
 
13
- import resqpy as rq
13
+ import resqpy.model as rq
14
14
  import resqpy.crs as rqc
15
15
  import resqpy.grid as grr
16
16
  import resqpy.fault as rqf
@@ -495,19 +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(
499
- grid,
500
- surface,
501
- name,
502
- title = None,
503
- agitate = False,
504
- random_agitation = False,
505
- feature_type = "fault",
506
- is_curtain = False,
507
- progress_fn = None,
508
- return_properties = None,
509
- raw_bisector = False,
510
- ):
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):
511
510
  """Returns a grid connection set containing those cell faces which are deemed to represent the surface.
512
511
 
513
512
  argumants:
@@ -539,6 +538,8 @@ def find_faces_to_represent_surface_regular_optimised(
539
538
  the returned dictionary has the passed strings as keys and numpy arrays as values
540
539
  raw_bisector (bool, default False): if True and grid bisector is requested then it is left in a raw
541
540
  form without assessing which side is shallower (True values indicate same side as origin cell)
541
+ n_batches (int, default 20): the number of batches of triangles to use at the low level (numba multi
542
+ threading allows some parallelism between the batches)
542
543
 
543
544
  returns:
544
545
  gcs or (gcs, gcs_props)
@@ -552,8 +553,13 @@ def find_faces_to_represent_surface_regular_optimised(
552
553
  no trimming of the surface is carried out here: for computational efficiency, it is recommended
553
554
  to trim first;
554
555
  organisational objects for the feature are created if needed;
555
- 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()
556
560
  """
561
+ warnings.warn('DEPRECATED: grid_surface.find_faces_to_represent_surface_regular_dense_optimised() function; ' +
562
+ 'use find_faces_to_represent_surface_regular_optimised() instead')
557
563
 
558
564
  assert isinstance(grid, grr.RegularGrid)
559
565
  assert grid.is_aligned
@@ -598,8 +604,10 @@ def find_faces_to_represent_surface_regular_optimised(
598
604
  grid.block_dxyz_dkji[0, 2],
599
605
  )
600
606
  triangles, points = surface.triangles_and_points()
607
+ t_dtype = np.int32 if len(triangles) < 2_000_000_000 else np.int64
601
608
  assert (triangles is not None and points is not None), f"surface {surface.title} is empty"
602
609
  if agitate:
610
+ points = points.copy()
603
611
  if random_agitation:
604
612
  points += 1.0e-5 * (np.random.random(points.shape) - 0.5)
605
613
  else:
@@ -621,14 +629,15 @@ def find_faces_to_represent_surface_regular_optimised(
621
629
  if nk > 1:
622
630
  # log.debug("searching for k faces")
623
631
  k_faces = np.zeros((nk - 1, grid.nj, grid.ni), dtype = bool)
624
- k_triangles = np.full((nk - 1, grid.nj, grid.ni), -1, dtype = int)
632
+ k_triangles = np.full((nk - 1, grid.nj, grid.ni), -1, dtype = t_dtype)
625
633
  k_depths = np.full((nk - 1, grid.nj, grid.ni), np.nan)
626
634
  k_offsets = np.full((nk - 1, grid.nj, grid.ni), np.nan)
627
635
  p_xy = np.delete(points, 2, 1)
628
636
 
629
637
  k_hits = vec.points_in_triangles_aligned_optimised(grid.ni, grid.nj, grid_dxyz[0], grid_dxyz[1],
630
- p_xy[triangles])
638
+ p_xy[triangles], n_batches)
631
639
 
640
+ del p_xy
632
641
  axis = 2
633
642
  index1 = 1
634
643
  index2 = 2
@@ -650,7 +659,6 @@ def find_faces_to_represent_surface_regular_optimised(
650
659
  k_triangles,
651
660
  )
652
661
  del k_hits
653
- del p_xy
654
662
  log.debug(f"k face count: {np.count_nonzero(k_faces)}")
655
663
  else:
656
664
  k_faces = None
@@ -665,13 +673,15 @@ def find_faces_to_represent_surface_regular_optimised(
665
673
  if grid.nj > 1:
666
674
  # log.debug("searching for j faces")
667
675
  j_faces = np.zeros((nk, grid.nj - 1, grid.ni), dtype = bool)
668
- j_triangles = np.full((nk, grid.nj - 1, grid.ni), -1, dtype = int)
676
+ j_triangles = np.full((nk, grid.nj - 1, grid.ni), -1, dtype = t_dtype)
669
677
  j_depths = np.full((nk, grid.nj - 1, grid.ni), np.nan)
670
678
  j_offsets = np.full((nk, grid.nj - 1, grid.ni), np.nan)
671
679
  p_xz = np.delete(points, 1, 1)
672
680
 
673
- j_hits = vec.points_in_triangles_aligned_optimised(grid.ni, nk, grid_dxyz[0], grid_dxyz[2], p_xz[triangles])
681
+ j_hits = vec.points_in_triangles_aligned_optimised(grid.ni, nk, grid_dxyz[0], grid_dxyz[2], p_xz[triangles],
682
+ n_batches)
674
683
 
684
+ del p_xz
675
685
  axis = 1
676
686
  index1 = 0
677
687
  index2 = 2
@@ -693,7 +703,6 @@ def find_faces_to_represent_surface_regular_optimised(
693
703
  j_triangles,
694
704
  )
695
705
  del j_hits
696
- del p_xz
697
706
  if is_curtain and grid.nk > 1: # expand arrays to all layers
698
707
  j_faces = np.repeat(j_faces, grid.nk, axis = 0)
699
708
  j_triangles = np.repeat(j_triangles, grid.nk, axis = 0)
@@ -713,13 +722,15 @@ def find_faces_to_represent_surface_regular_optimised(
713
722
  if grid.ni > 1:
714
723
  # log.debug("searching for i faces")
715
724
  i_faces = np.zeros((nk, grid.nj, grid.ni - 1), dtype = bool)
716
- i_triangles = np.full((nk, grid.nj, grid.ni - 1), -1, dtype = int)
725
+ i_triangles = np.full((nk, grid.nj, grid.ni - 1), -1, dtype = t_dtype)
717
726
  i_depths = np.full((nk, grid.nj, grid.ni - 1), np.nan)
718
727
  i_offsets = np.full((nk, grid.nj, grid.ni - 1), np.nan)
719
728
  p_yz = np.delete(points, 0, 1)
720
729
 
721
- i_hits = vec.points_in_triangles_aligned_optimised(grid.nj, nk, grid_dxyz[1], grid_dxyz[2], p_yz[triangles])
730
+ i_hits = vec.points_in_triangles_aligned_optimised(grid.nj, nk, grid_dxyz[1], grid_dxyz[2], p_yz[triangles],
731
+ n_batches)
722
732
 
733
+ del p_yz
723
734
  axis = 0
724
735
  index1 = 0
725
736
  index2 = 1
@@ -741,7 +752,6 @@ def find_faces_to_represent_surface_regular_optimised(
741
752
  i_triangles,
742
753
  )
743
754
  del i_hits
744
- del p_yz
745
755
  if is_curtain and grid.nk > 1: # expand arrays to all layers
746
756
  # log.debug('expanding curtain faces')
747
757
  i_faces = np.repeat(i_faces, grid.nk, axis = 0)
@@ -812,7 +822,7 @@ def find_faces_to_represent_surface_regular_optimised(
812
822
  related_uuid = surface.uuid)
813
823
  assert (flange_bool_uuid is not None), f"No flange bool property found for surface: {surface.title}"
814
824
  flange_bool = rqp.Property(surface.model, uuid = flange_bool_uuid)
815
- flange_array = flange_bool.array_ref()
825
+ flange_array = flange_bool.array_ref(dtype = bool)
816
826
  all_flange = np.take(flange_array, all_tris)
817
827
  assert all_flange.shape == (gcs.count,)
818
828
 
@@ -836,7 +846,7 @@ def find_faces_to_represent_surface_regular_optimised(
836
846
  if progress_fn is not None:
837
847
  progress_fn(1.0)
838
848
 
839
- log.debug(f"finishing find_faces_to_represent_surface_regular_optimised for {name}")
849
+ log.debug(f"finishing find_faces_to_represent_surface_regular_dense_optimised for {name}")
840
850
 
841
851
  # if returning properties, construct dictionary
842
852
  if return_properties:
@@ -858,6 +868,497 @@ def find_faces_to_represent_surface_regular_optimised(
858
868
  return gcs
859
869
 
860
870
 
871
+ def find_faces_to_represent_surface_regular_optimised(grid,
872
+ surface,
873
+ name,
874
+ title = None,
875
+ agitate = False,
876
+ random_agitation = False,
877
+ feature_type = "fault",
878
+ is_curtain = False,
879
+ progress_fn = None,
880
+ return_properties = None,
881
+ raw_bisector = False,
882
+ n_batches = 20,
883
+ packed_bisectors = False,
884
+ patch_indices = None):
885
+ """Returns a grid connection set containing those cell faces which are deemed to represent the surface.
886
+
887
+ argumants:
888
+ grid (RegularGrid): the grid for which to create a grid connection set representation of the surface;
889
+ must be aligned, ie. I with +x, J with +y, K with +z and local origin of (0.0, 0.0, 0.0)
890
+ surface (Surface): the surface to be intersected with the grid
891
+ name (str): the feature name to use in the grid connection set
892
+ title (str, optional): the citation title to use for the grid connection set; defaults to name
893
+ agitate (bool, default False): if True, the points of the surface are perturbed by a small
894
+ offset, which can help if the surface has been built from a regular mesh with a periodic resonance
895
+ with the grid
896
+ random_agitation (bool, default False): if True, the agitation is by a small random distance; if False,
897
+ a constant positive shift of 5.0e-6 is applied to x, y & z values; ignored if agitate is False
898
+ feature_type (str, default 'fault'): 'fault', 'horizon' or 'geobody boundary'
899
+ is_curtain (bool, default False): if True, only the top layer of the grid is processed and the bisector
900
+ property, if requested, is generated with indexable element columns
901
+ progress_fn (f(x: float), optional): a callback function to be called at intervals by this function;
902
+ the argument will progress from 0.0 to 1.0 in unspecified and uneven increments
903
+ return_properties (List[str]): if present, a list of property arrays to calculate and
904
+ return as a dictionary; recognised values in the list are 'triangle', 'depth', 'offset',
905
+ 'flange bool', 'grid bisector', or 'grid shadow';
906
+ triangle is an index into the surface triangles of the triangle detected for the gcs face; depth is
907
+ the z value of the intersection point of the inter-cell centre vector with a triangle in the surface;
908
+ offset is a measure of the distance between the centre of the cell face and the intersection point;
909
+ grid bisector is a grid cell boolean property holding True for the set of cells on one
910
+ side of the surface, deemed to be shallower;
911
+ grid shadow is a grid cell int8 property holding 0: cell neither above nor below a K face of the
912
+ gridded surface, 1 cell is above K face(s), 2 cell is below K face(s), 3 cell is between K faces;
913
+ the returned dictionary has the passed strings as keys and numpy arrays as values
914
+ raw_bisector (bool, default False): if True and grid bisector is requested then it is left in a raw
915
+ form without assessing which side is shallower (True values indicate same side as origin cell)
916
+ n_batches (int, default 20): the number of batches of triangles to use at the low level (numba multi
917
+ threading allows some parallelism between the batches)
918
+ packed_bisectors (bool, default False): if True and return properties include 'grid bisector' then
919
+ non curtain bisectors are returned in packed form
920
+ patch_indices (numpy int array, optional): if present, an array over grid cells indicating which
921
+ patch of surface is applicable in terms of a bisector, for each cell
922
+
923
+ returns:
924
+ gcs or (gcs, gcs_props)
925
+ where gcs is a new GridConnectionSet with a single feature, not yet written to hdf5 nor xml created;
926
+ gcs_props is a dictionary mapping from requested return_properties string to numpy array (or tuple
927
+ of numpy array and curtain bool in the case of grid bisector)
928
+
929
+ notes:
930
+ this function is designed for aligned regular grids only;
931
+ this function can handle the surface and grid being in different coordinate reference systems, as
932
+ long as the implicit parent crs is shared;
933
+ no trimming of the surface is carried out here: for computational efficiency, it is recommended
934
+ to trim first;
935
+ organisational objects for the feature are created if needed;
936
+ if the offset return property is requested, the implicit units will be the z units of the grid's crs;
937
+ if patch_indices is present and grid bisectors are being returned, a composite bisector array is returned
938
+ with elements set from individual bisectors for each patch of surface
939
+ """
940
+
941
+ assert isinstance(grid, grr.RegularGrid)
942
+ assert grid.is_aligned
943
+ return_triangles = False
944
+ return_depths = False
945
+ return_offsets = False
946
+ return_bisector = False
947
+ return_shadow = False
948
+ return_flange_bool = False
949
+ if return_properties:
950
+ assert all([
951
+ p in [
952
+ "triangle",
953
+ "depth",
954
+ "offset",
955
+ "grid bisector",
956
+ "grid shadow",
957
+ "flange bool",
958
+ ] for p in return_properties
959
+ ])
960
+ return_triangles = "triangle" in return_properties
961
+ return_depths = "depth" in return_properties
962
+ return_offsets = "offset" in return_properties
963
+ return_bisector = "grid bisector" in return_properties
964
+ return_shadow = "grid shadow" in return_properties
965
+ return_flange_bool = "flange bool" in return_properties
966
+ if return_flange_bool:
967
+ return_triangles = True
968
+ patchwork = return_bisector and patch_indices is not None
969
+ if patchwork:
970
+ return_triangles = True # triangle numbers are used to infer patch index
971
+ assert patch_indices.shape == tuple(grid.extent_kji)
972
+ if title is None:
973
+ title = name
974
+
975
+ if progress_fn is not None:
976
+ progress_fn(0.0)
977
+
978
+ log.debug(f"intersecting surface {surface.title} with regular grid {grid.title}")
979
+ # log.debug(f'grid extent kji: {grid.extent_kji}')
980
+
981
+ triangles, points = surface.triangles_and_points(copy = True)
982
+ surface.decache_triangles_and_points()
983
+
984
+ t_dtype = np.int32 if len(triangles) < 2_147_483_648 else np.int64
985
+
986
+ assert (triangles is not None and points is not None), f"surface {surface.title} is empty"
987
+ if agitate:
988
+ if random_agitation:
989
+ points += 1.0e-5 * (np.random.random(points.shape) - 0.5)
990
+ else:
991
+ points += 5.0e-6
992
+ # log.debug(f'surface: {surface.title}; p0: {points[0]}; crs uuid: {surface.crs_uuid}')
993
+ # log.debug(f'surface min xyz: {np.min(points, axis = 0)}')
994
+ # log.debug(f'surface max xyz: {np.max(points, axis = 0)}')
995
+ if not bu.matching_uuids(grid.crs_uuid, surface.crs_uuid):
996
+ log.debug("converting from surface crs to grid crs")
997
+ s_crs = rqc.Crs(surface.model, uuid = surface.crs_uuid)
998
+ s_crs.convert_array_to(grid.crs, points)
999
+ surface.crs_uuid = grid.crs.uuid
1000
+ # log.debug(f'surface: {surface.title}; p0: {points[0]}; crs uuid: {surface.crs_uuid}')
1001
+ # log.debug(f'surface min xyz: {np.min(points, axis = 0)}')
1002
+ # log.debug(f'surface max xyz: {np.max(points, axis = 0)}')
1003
+
1004
+ # convert surface points to work with unit cube grid cells
1005
+ dx = grid.block_dxyz_dkji[2, 0]
1006
+ dy = grid.block_dxyz_dkji[1, 1]
1007
+ dz = grid.block_dxyz_dkji[0, 2]
1008
+ points[:, 0] /= dx
1009
+ points[:, 1] /= dy
1010
+ points[:, 2] /= dz
1011
+ points[:] -= 0.5
1012
+ p = points[triangles]
1013
+
1014
+ nk = 1 if is_curtain else grid.nk
1015
+ # K direction (xy projection)
1016
+ k_faces_kji0 = None
1017
+ k_triangles = None
1018
+ k_depths = None
1019
+ k_offsets = None
1020
+ k_props = None
1021
+ if nk > 1:
1022
+ # log.debug("searching for k faces")
1023
+
1024
+ k_hits, k_depths = vec.points_in_triangles_aligned_unified(grid.ni, grid.nj, 0, 1, 2, p, n_batches)
1025
+
1026
+ k_faces = np.floor(k_depths)
1027
+ mask = np.logical_and(k_faces >= 0, k_faces < nk - 1)
1028
+
1029
+ if np.any(mask):
1030
+ k_hits = k_hits[mask, :]
1031
+ k_faces = k_faces[mask]
1032
+ k_depths = k_depths[mask]
1033
+ k_triangles = k_hits[:, 0]
1034
+ k_faces_kji0 = np.empty((len(k_faces), 3), dtype = np.int32)
1035
+ k_faces_kji0[:, 0] = k_faces
1036
+ k_faces_kji0[:, 1] = k_hits[:, 1]
1037
+ k_faces_kji0[:, 2] = k_hits[:, 2]
1038
+ if return_offsets:
1039
+ k_offsets = (k_depths - k_faces.astype(np.float64) - 0.5) * dz
1040
+ if return_depths:
1041
+ k_depths[:] += 0.5
1042
+ k_depths[:] *= dz
1043
+ k_props = []
1044
+ if return_triangles:
1045
+ k_props.append(k_triangles)
1046
+ if return_depths:
1047
+ k_props.append(k_depths)
1048
+ if return_offsets:
1049
+ k_props.append(k_offsets)
1050
+ log.debug(f"k face count: {len(k_faces_kji0)}")
1051
+
1052
+ del k_hits
1053
+ del k_faces
1054
+
1055
+ if progress_fn is not None:
1056
+ progress_fn(0.3)
1057
+
1058
+ # J direction (xz projection)
1059
+ j_faces_kji0 = None
1060
+ j_triangles = None
1061
+ j_depths = None
1062
+ j_offsets = None
1063
+ j_props = None
1064
+ if grid.nj > 1:
1065
+ # log.debug("searching for J faces")
1066
+
1067
+ j_hits, j_depths = vec.points_in_triangles_aligned_unified(grid.ni, nk, 0, 2, 1, p, n_batches)
1068
+
1069
+ j_faces = np.floor(j_depths)
1070
+ mask = np.logical_and(j_faces >= 0, j_faces < grid.nj - 1)
1071
+
1072
+ if np.any(mask):
1073
+ j_hits = j_hits[mask, :]
1074
+ j_faces = j_faces[mask]
1075
+ j_depths = j_depths[mask]
1076
+ j_triangles = j_hits[:, 0]
1077
+ j_faces_kji0 = np.empty((len(j_faces), 3), dtype = np.int32)
1078
+ j_faces_kji0[:, 0] = j_hits[:, 1]
1079
+ j_faces_kji0[:, 1] = j_faces
1080
+ j_faces_kji0[:, 2] = j_hits[:, 2]
1081
+ if return_offsets:
1082
+ j_offsets = (j_depths - j_faces.astype(np.float64) - 0.5) * dy
1083
+ if return_depths:
1084
+ j_depths[:] += 0.5
1085
+ j_depths[:] *= dy
1086
+ if is_curtain and grid.nk > 1: # expand arrays to all layers
1087
+ j_faces = np.repeat(np.expand_dims(j_faces_kji0, axis = 0), grid.nk, axis = 0)
1088
+ j_faces[:, :, 0] = np.expand_dims(np.arange(grid.nk, dtype = np.int32), axis = -1)
1089
+ j_faces_kji0 = j_faces.reshape((-1, 3))
1090
+ j_triangles = np.repeat(j_triangles, grid.nk, axis = 0)
1091
+ if return_offsets:
1092
+ j_offsets = np.repeat(j_offsets, grid.nk, axis = 0)
1093
+ if return_depths:
1094
+ j_depths = np.repeat(j_depths, grid.nk, axis = 0)
1095
+ j_props = []
1096
+ if return_triangles:
1097
+ j_props.append(j_triangles)
1098
+ if return_depths:
1099
+ j_props.append(j_depths)
1100
+ if return_offsets:
1101
+ j_props.append(j_offsets)
1102
+ log.debug(f"j face count: {len(j_faces_kji0)}")
1103
+
1104
+ del j_hits
1105
+ del j_faces
1106
+
1107
+ if progress_fn is not None:
1108
+ progress_fn(0.6)
1109
+
1110
+ # I direction (yz projection)
1111
+ i_faces_kji0 = None
1112
+ i_triangles = None
1113
+ i_depths = None
1114
+ i_offsets = None
1115
+ i_props = None
1116
+ if grid.ni > 1:
1117
+ # log.debug("searching for I faces")
1118
+
1119
+ i_hits, i_depths = vec.points_in_triangles_aligned_unified(grid.nj, nk, 1, 2, 0, p, n_batches)
1120
+
1121
+ i_faces = np.floor(i_depths)
1122
+ mask = np.logical_and(i_faces >= 0, i_faces < grid.ni - 1)
1123
+
1124
+ if np.any(mask):
1125
+ i_hits = i_hits[mask, :]
1126
+ i_faces = i_faces[mask]
1127
+ i_depths = i_depths[mask]
1128
+ i_triangles = i_hits[:, 0]
1129
+ i_faces_kji0 = np.empty((len(i_faces), 3), dtype = np.int32)
1130
+ i_faces_kji0[:, 0] = i_hits[:, 1]
1131
+ i_faces_kji0[:, 1] = i_hits[:, 2]
1132
+ i_faces_kji0[:, 2] = i_faces
1133
+ if return_offsets:
1134
+ i_offsets = (i_depths - i_faces.astype(np.float64) - 0.5) * dx
1135
+ if return_depths:
1136
+ i_depths[:] += 0.5
1137
+ i_depths[:] *= dx
1138
+ if is_curtain and grid.nk > 1: # expand arrays to all layers
1139
+ i_faces = np.repeat(np.expand_dims(i_faces_kji0, axis = 0), grid.nk, axis = 0)
1140
+ i_faces[:, :, 0] = np.expand_dims(np.arange(grid.nk, dtype = np.int32), axis = -1)
1141
+ i_faces_kji0 = i_faces.reshape((-1, 3))
1142
+ i_triangles = np.repeat(i_triangles, grid.nk, axis = 0)
1143
+ if return_offsets:
1144
+ i_offsets = np.repeat(i_offsets, grid.nk, axis = 0)
1145
+ if return_depths:
1146
+ i_depths = np.repeat(i_depths, grid.nk, axis = 0)
1147
+ i_props = []
1148
+ if return_triangles:
1149
+ i_props.append(i_triangles)
1150
+ if return_depths:
1151
+ i_props.append(i_depths)
1152
+ if return_offsets:
1153
+ i_props.append(i_offsets)
1154
+ log.debug(f"i face count: {len(i_faces_kji0)}")
1155
+
1156
+ del i_hits
1157
+ del i_faces
1158
+
1159
+ if progress_fn is not None:
1160
+ progress_fn(0.9)
1161
+
1162
+ if ((k_faces_kji0 is None or len(k_faces_kji0) == 0) and (j_faces_kji0 is None or len(j_faces_kji0) == 0) and
1163
+ (i_faces_kji0 is None or len(i_faces_kji0) == 0)):
1164
+ log.error(f'did not find any faces to represent {name}: surface does not intersect grid?')
1165
+ if return_properties:
1166
+ return (None, {})
1167
+ else:
1168
+ return None
1169
+
1170
+ log.debug("converting face sets into grid connection set")
1171
+ # NB: kji0 arrays in internal face protocol: used as cell_kji0 with polarity of 1
1172
+ # property lists have elements replaced with sorted and filtered equivalents
1173
+ gcs = rqf.GridConnectionSet.from_faces_indices(grid = grid,
1174
+ k_faces_kji0 = k_faces_kji0,
1175
+ j_faces_kji0 = j_faces_kji0,
1176
+ i_faces_kji0 = i_faces_kji0,
1177
+ remove_duplicates = not patchwork,
1178
+ k_properties = k_props,
1179
+ j_properties = j_props,
1180
+ i_properties = i_props,
1181
+ feature_name = name,
1182
+ feature_type = feature_type,
1183
+ create_organizing_objects_where_needed = True,
1184
+ title = title)
1185
+ # log.debug('finished coversion to gcs')
1186
+
1187
+ # NB. following assumes faces have been added to gcs in a particular order!
1188
+ all_tris = None
1189
+ if return_triangles:
1190
+ # log.debug('preparing triangles array')
1191
+ k_triangles = np.empty((0,), dtype = np.int32) if k_props is None else k_props.pop(0)
1192
+ j_triangles = np.empty((0,), dtype = np.int32) if j_props is None else j_props.pop(0)
1193
+ i_triangles = np.empty((0,), dtype = np.int32) if i_props is None else i_props.pop(0)
1194
+ all_tris = np.concatenate((k_triangles, j_triangles, i_triangles), axis = 0)
1195
+ # log.debug(f'gcs count: {gcs.count}; all triangles shape: {all_tris.shape}')
1196
+ assert all_tris.shape == (gcs.count,)
1197
+
1198
+ # NB. following assumes faces have been added to gcs in a particular order!
1199
+ all_depths = None
1200
+ if return_depths:
1201
+ # log.debug('preparing depths array')
1202
+ k_depths = np.empty((0,), dtype = np.float64) if k_props is None else k_props.pop(0)
1203
+ j_depths = np.empty((0,), dtype = np.float64) if j_props is None else j_props.pop(0)
1204
+ i_depths = np.empty((0,), dtype = np.float64) if i_props is None else i_props.pop(0)
1205
+ all_depths = np.concatenate((k_depths, j_depths, i_depths), axis = 0)
1206
+ # log.debug(f'gcs count: {gcs.count}; all depths shape: {all_depths.shape}')
1207
+ assert all_depths.shape == (gcs.count,)
1208
+
1209
+ # NB. following assumes faces have been added to gcs in a particular order!
1210
+ all_offsets = None
1211
+ if return_offsets:
1212
+ # log.debug('preparing offsets array')
1213
+ k_offsets = np.empty((0,), dtype = np.float64) if k_props is None else k_props[0]
1214
+ j_offsets = np.empty((0,), dtype = np.float64) if j_props is None else j_props[0]
1215
+ i_offsets = np.empty((0,), dtype = np.float64) if i_props is None else i_props[0]
1216
+ all_offsets = _all_offsets(grid.crs, k_offsets, j_offsets, i_offsets)
1217
+ # log.debug(f'gcs count: {gcs.count}; all offsets shape: {all_offsets.shape}')
1218
+ assert all_offsets.shape == (gcs.count,)
1219
+
1220
+ all_flange = None
1221
+ if return_flange_bool:
1222
+ # log.debug('preparing flange array')
1223
+ flange_bool_uuid = surface.model.uuid(title = "flange bool",
1224
+ obj_type = "DiscreteProperty",
1225
+ related_uuid = surface.uuid)
1226
+ assert (flange_bool_uuid is not None), f"No flange bool property found for surface: {surface.title}"
1227
+ flange_bool = rqp.Property(surface.model, uuid = flange_bool_uuid)
1228
+ flange_array = flange_bool.array_ref(dtype = bool)
1229
+ all_flange = np.take(flange_array, all_tris)
1230
+ assert all_flange.shape == (gcs.count,)
1231
+
1232
+ # note: following is a grid cells property, not a gcs property
1233
+ bisector = None
1234
+ if return_bisector:
1235
+ if is_curtain and not patchwork:
1236
+ log.debug(f'preparing columns bisector for: {surface.title}')
1237
+ if j_faces_kji0 is None:
1238
+ j_faces_ji0 = np.empty((0, 2), dtype = np.int32)
1239
+ else:
1240
+ j_faces_ji0 = j_faces_kji0[:, 1:]
1241
+ if i_faces_kji0 is None:
1242
+ i_faces_ji0 = np.empty((0, 2), dtype = np.int32)
1243
+ else:
1244
+ i_faces_ji0 = i_faces_kji0[:, 1:]
1245
+ bisector = column_bisector_from_face_indices((grid.nj, grid.ni), j_faces_ji0, i_faces_ji0)
1246
+ # log.debug('finished preparing columns bisector')
1247
+ elif patchwork:
1248
+ n_patches = surface.number_of_patches()
1249
+ log.info(f'preparing composite cells bisector for surface: {surface.title}; number of patches: {n_patches}')
1250
+ nkf = 0 if k_faces_kji0 is None else len(k_faces_kji0)
1251
+ njf = 0 if j_faces_kji0 is None else len(j_faces_kji0)
1252
+ nif = 0 if i_faces_kji0 is None else len(i_faces_kji0)
1253
+ # fetch patch indices for triangle hits
1254
+ assert all_tris is not None and len(all_tris) == nkf + njf + nif
1255
+ patch_indices_k = surface.patch_indices_for_triangle_indices(all_tris[:nkf])
1256
+ patch_indices_j = surface.patch_indices_for_triangle_indices(all_tris[nkf:nkf + njf])
1257
+ patch_indices_i = surface.patch_indices_for_triangle_indices(all_tris[nkf + njf:])
1258
+ # add extra dimension to bisector array (at axis 0) for patches
1259
+ pb_shape = tuple([n_patches] + list(grid.extent_kji))
1260
+ if packed_bisectors:
1261
+ bisector = np.invert(np.zeros(_shape_packed(grid.extent_kji), dtype = np.uint8), dtype = np.uint8)
1262
+ else:
1263
+ bisector = np.ones(tuple(grid.extent_kji), dtype = np.bool_)
1264
+ # populate composite bisector
1265
+ for patch in range(n_patches):
1266
+ log.debug(f'processing patch {patch} of surface: {surface.title}')
1267
+ mask = (patch_indices == patch)
1268
+ mask_count = np.count_nonzero(mask)
1269
+ if mask_count == 0:
1270
+ log.warning(f'patch {patch} of surface {surface.title} is not applicable to any cells in grid')
1271
+ continue
1272
+ patch_box, box_count = get_box(mask)
1273
+ assert box_count == mask_count
1274
+ assert np.all(patch_box[1] > patch_box[0])
1275
+ patch_box = expanded_box(patch_box, tuple(grid.extent_kji))
1276
+ patch_box[0, 0] = 0
1277
+ patch_box[1, 0] = grid.extent_kji[0]
1278
+ packed_box = shrunk_box_for_packing(patch_box)
1279
+ patch_k_faces_kji0 = None
1280
+ if k_faces_kji0 is not None:
1281
+ patch_k_faces_kji0 = k_faces_kji0[(patch_indices_k == patch).astype(bool)]
1282
+ patch_j_faces_kji0 = None
1283
+ if j_faces_kji0 is not None:
1284
+ patch_j_faces_kji0 = j_faces_kji0[(patch_indices_j == patch).astype(bool)]
1285
+ patch_i_faces_kji0 = None
1286
+ if i_faces_kji0 is not None:
1287
+ patch_i_faces_kji0 = i_faces_kji0[(patch_indices_i == patch).astype(bool)]
1288
+ if packed_bisectors:
1289
+ mask = np.packbits(mask, axis = -1)
1290
+ patch_bisector, is_curtain = \
1291
+ packed_bisector_from_face_indices(tuple(grid.extent_kji),
1292
+ patch_k_faces_kji0,
1293
+ patch_j_faces_kji0,
1294
+ patch_i_faces_kji0,
1295
+ raw_bisector,
1296
+ patch_box)
1297
+ # bisector[:] = np.bitwise_or(np.bitwise_and(mask, patch_bisector),
1298
+ #  np.bitwise_and(np.invert(mask, dtype = np.uint8), bisector))
1299
+ _set_packed_where_mask(bisector, mask, patch_bisector, packed_box)
1300
+ else:
1301
+ patch_bisector, is_curtain = \
1302
+ bisector_from_face_indices(tuple(grid.extent_kji),
1303
+ patch_k_faces_kji0,
1304
+ patch_j_faces_kji0,
1305
+ patch_i_faces_kji0,
1306
+ raw_bisector,
1307
+ patch_box)
1308
+ bisector[mask] = patch_bisector[mask]
1309
+ if is_curtain:
1310
+ # TODO: downgrade following to debug once downstream functionality tested
1311
+ log.warning(f'ignoring curtain nature of bisector for patch {patch} of surface: {surface.title}')
1312
+ is_curtain = False
1313
+ else:
1314
+ log.info(f'preparing singlular cells bisector for surface: {surface.title}') # could downgrade to debug
1315
+ if ((k_faces_kji0 is None or len(k_faces_kji0) == 0) and
1316
+ (j_faces_kji0 is None or len(j_faces_kji0) == 0) and (i_faces_kji0 is None or len(i_faces_kji0) == 0)):
1317
+ bisector = np.ones((grid.nj, grid.ni), dtype = bool)
1318
+ is_curtain = True
1319
+ elif packed_bisectors:
1320
+ bisector, is_curtain = packed_bisector_from_face_indices(tuple(grid.extent_kji), k_faces_kji0,
1321
+ j_faces_kji0, i_faces_kji0, raw_bisector, None)
1322
+ if is_curtain:
1323
+ bisector = np.unpackbits(bisector[0], axis = -1,
1324
+ count = grid.ni).astype(bool) # reduce to a columns property
1325
+ else:
1326
+ bisector, is_curtain = bisector_from_face_indices(tuple(grid.extent_kji), k_faces_kji0, j_faces_kji0,
1327
+ i_faces_kji0, raw_bisector, None)
1328
+ if is_curtain:
1329
+ bisector = bisector[0] # reduce to a columns property
1330
+
1331
+ # note: following is a grid cells property, not a gcs property
1332
+ shadow = None
1333
+ if return_shadow:
1334
+ log.debug("preparing cells shadow")
1335
+ shadow = shadow_from_face_indices(tuple(grid.extent_kji), k_faces_kji0)
1336
+
1337
+ if progress_fn is not None:
1338
+ progress_fn(1.0)
1339
+
1340
+ log.debug(f"finishing find_faces_to_represent_surface_regular_optimised for {name}")
1341
+
1342
+ # if returning properties, construct dictionary
1343
+ if return_properties:
1344
+ props_dict = {}
1345
+ if 'triangle' in return_properties:
1346
+ props_dict["triangle"] = all_tris
1347
+ if return_depths:
1348
+ props_dict["depth"] = all_depths
1349
+ if return_offsets:
1350
+ props_dict["offset"] = all_offsets
1351
+ if return_bisector:
1352
+ props_dict["grid bisector"] = (bisector, is_curtain)
1353
+ if return_shadow:
1354
+ props_dict["grid shadow"] = shadow
1355
+ if return_flange_bool:
1356
+ props_dict["flange bool"] = all_flange
1357
+ return (gcs, props_dict)
1358
+
1359
+ return gcs
1360
+
1361
+
861
1362
  def find_faces_to_represent_surface(grid, surface, name, mode = "auto", feature_type = "fault", progress_fn = None):
862
1363
  """Returns a grid connection set containing those cell faces which are deemed to represent the surface.
863
1364
 
@@ -885,19 +1386,7 @@ def find_faces_to_represent_surface(grid, surface, name, mode = "auto", feature_
885
1386
  mode = "regular_optimised"
886
1387
  else:
887
1388
  mode = "staffa"
888
- if mode == "staffa":
889
- return find_faces_to_represent_surface_staffa(grid,
890
- surface,
891
- name,
892
- feature_type = feature_type,
893
- progress_fn = progress_fn)
894
- elif mode == "regular":
895
- return find_faces_to_represent_surface_regular(grid,
896
- surface,
897
- name,
898
- feature_type = feature_type,
899
- progress_fn = progress_fn)
900
- elif mode == "regular_optimised":
1389
+ if mode == "regular_optimised":
901
1390
  return find_faces_to_represent_surface_regular_optimised(grid,
902
1391
  surface,
903
1392
  name,
@@ -911,161 +1400,331 @@ def find_faces_to_represent_surface(grid, surface, name, mode = "auto", feature_
911
1400
  name,
912
1401
  feature_type = feature_type,
913
1402
  progress_fn = progress_fn)
1403
+ elif mode == "staffa":
1404
+ return find_faces_to_represent_surface_staffa(grid,
1405
+ surface,
1406
+ name,
1407
+ feature_type = feature_type,
1408
+ progress_fn = progress_fn)
1409
+ elif mode == "regular_dense":
1410
+ return find_faces_to_represent_surface_regular_dense_optimised(grid,
1411
+ surface,
1412
+ name,
1413
+ feature_type = feature_type,
1414
+ progress_fn = progress_fn)
1415
+ elif mode == "regular":
1416
+ return find_faces_to_represent_surface_regular(grid,
1417
+ surface,
1418
+ name,
1419
+ feature_type = feature_type,
1420
+ progress_fn = progress_fn)
914
1421
  log.critical("unrecognised mode: " + str(mode))
915
1422
  return None
916
1423
 
917
1424
 
918
1425
  def bisector_from_faces( # type: ignore
919
- grid_extent_kji: Tuple[int, int, int],
920
- k_faces: np.ndarray,
921
- j_faces: np.ndarray,
922
- i_faces: np.ndarray,
923
- raw_bisector: bool,
924
- ) -> Tuple[np.ndarray, bool]:
1426
+ grid_extent_kji: Tuple[int, int, int], k_faces: Union[np.ndarray, None], j_faces: Union[np.ndarray, None],
1427
+ i_faces: Union[np.ndarray, None], raw_bisector: bool) -> Tuple[np.ndarray, bool]:
925
1428
  """Creates a boolean array denoting the bisection of the grid by the face sets.
926
1429
 
927
1430
  arguments:
928
- grid_extent_kji (Tuple[int, int, int]): the shape of the grid.
929
- k_faces (np.ndarray): a boolean array of which faces represent the surface in the k dimension.
930
- j_faces (np.ndarray): a boolean array of which faces represent the surface in the j dimension.
931
- i_faces (np.ndarray): a boolean array of which faces represent the surface in the i dimension.
1431
+ - grid_extent_kji (Tuple[int, int, int]): the shape of the grid
1432
+ - k_faces (np.ndarray): a boolean array of which faces represent the surface in the k dimension
1433
+ - j_faces (np.ndarray): a boolean array of which faces represent the surface in the j dimension
1434
+ - i_faces (np.ndarray): a boolean array of which faces represent the surface in the i dimension
1435
+ - raw_bisector (bool): if True, the bisector is returned without determining which side is shallower
932
1436
 
933
1437
  returns:
934
1438
  Tuple containing:
1439
+ - array (np.ndarray): boolean bisectors array where values are True for cells on the side
1440
+ of the surface that has a lower mean k index on average and False for cells on the other side.
1441
+ - is_curtain (bool): True if the surface is a curtain (vertical), otherwise False.
1442
+
1443
+ notes:
1444
+ - the face sets must form a single 'sealed' cut of the grid (eg. not waving in and out of the grid)
1445
+ - any 'boxed in' parts of the grid (completely enclosed by bisecting faces) will be consistently
1446
+ assigned to either the True or False part
1447
+ - this function is DEPRECATED, use newer indices based approach instead: bisector_from_face_indices()
1448
+ """
1449
+ warnings.warn('DEPRECATED: grid_surface.bisector_from_faces() function; use bisector_from_face_indices() instead')
1450
+ assert len(grid_extent_kji) == 3
935
1451
 
1452
+ # find the surface boundary (includes a buffer slice where surface does not reach edge of grid)
1453
+ box = get_boundary(k_faces, j_faces, i_faces, grid_extent_kji)
1454
+ box_shape = box[1, :] - box[0, :]
1455
+
1456
+ # set up the bisector array for the bounding box
1457
+ box_array = np.zeros(box_shape, dtype = np.bool_)
1458
+
1459
+ # seed the bisector box array at (0, 0, 0)
1460
+ box_array[0, 0, 0] = True
1461
+
1462
+ # prepare to spread True values to neighbouring cells that are not the other side of a face
1463
+ if k_faces is None:
1464
+ open_k = np.ones((box_shape[0] - 1, box_shape[1], box_shape[2]), dtype = bool)
1465
+ else:
1466
+ k_faces = k_faces[box[0, 0]:box[1, 0] - 1, box[0, 1]:box[1, 1], box[0, 2]:box[1, 2]]
1467
+ open_k = np.logical_not(k_faces)
1468
+ if j_faces is None:
1469
+ open_j = np.ones((box_shape[0], box_shape[1] - 1, box_shape[2]), dtype = bool)
1470
+ else:
1471
+ j_faces = j_faces[box[0, 0]:box[1, 0], box[0, 1]:box[1, 1] - 1, box[0, 2]:box[1, 2]]
1472
+ open_j = np.logical_not(j_faces)
1473
+ if i_faces is None:
1474
+ open_i = np.ones((box_shape[0], box_shape[1], box_shape[2] - 1), dtype = bool)
1475
+ else:
1476
+ i_faces = i_faces[box[0, 0]:box[1, 0], box[0, 1]:box[1, 1], box[0, 2]:box[1, 2] - 1]
1477
+ open_i = np.logical_not(i_faces)
1478
+
1479
+ # populate bisector array for box
1480
+ _fill_bisector(box_array, open_k, open_j, open_i)
1481
+
1482
+ # set up the full bisectors array and assigning the bounding box values
1483
+ array = np.zeros(grid_extent_kji, dtype = np.bool_)
1484
+ array[box[0, 0]:box[1, 0], box[0, 1]:box[1, 1], box[0, 2]:box[1, 2]] = box_array
1485
+
1486
+ # set bisector values outside of the bounding box
1487
+ _set_bisector_outside_box(array, box, box_array)
1488
+
1489
+ # check all array elements are not the same
1490
+ true_count = np.count_nonzero(array)
1491
+ cell_count = array.size
1492
+ if 0 < true_count < cell_count:
1493
+ # negate the array if it minimises the mean k and determine if the surface is a curtain
1494
+ is_curtain = _shallow_or_curtain(array, true_count, raw_bisector)
1495
+ else:
1496
+ assert raw_bisector, 'face set for surface is leaky or empty (surface does not intersect grid)'
1497
+ log.error('face set for surface is leaky or empty (surface does not intersect grid)')
1498
+ is_curtain = False
1499
+
1500
+ return array, is_curtain
1501
+
1502
+
1503
+ # yapf: disable
1504
+ def bisector_from_face_indices( # type: ignore
1505
+ grid_extent_kji: Tuple[int, int, int],
1506
+ k_faces_kji0: Union[np.ndarray, None],
1507
+ j_faces_kji0: Union[np.ndarray, None],
1508
+ i_faces_kji0: Union[np.ndarray, None],
1509
+ raw_bisector: bool,
1510
+ p_box: Union[np.ndarray, None]) -> Tuple[np.ndarray, bool]:
1511
+ # yapf: enable
1512
+ """Creates a boolean array denoting the bisection of the grid by the face sets.
1513
+
1514
+ arguments:
1515
+ - grid_extent_kji (Tuple[int, int, int]): the shape of the grid
1516
+ - k_faces_kji0 (np.ndarray): an int array of indices of which faces represent the surface in the k dimension
1517
+ - j_faces_kji0 (np.ndarray): an int array of indices of which faces represent the surface in the j dimension
1518
+ - i_faces_kji0 (np.ndarray): an int array of indices of which faces represent the surface in the i dimension
1519
+ - raw_bisector (bool): if True, the bisector is returned without determining which side is shallower
1520
+ - p_box (np.ndarray): a python protocol box to limit the bisector evaluation over
1521
+
1522
+ returns:
1523
+ Tuple containing:
936
1524
  - array (np.ndarray): boolean bisectors array where values are True for cells on the side
937
- of the surface that has a lower mean k index on average and False for cells on the other side.
1525
+ of the surface that has a lower mean k index on average and False for cells on the other side.
938
1526
  - is_curtain (bool): True if the surface is a curtain (vertical), otherwise False.
939
1527
 
940
1528
  notes:
941
- The face sets must form a single 'sealed' cut of the grid (eg. not waving in and out of the grid).
942
- Any 'boxed in' parts of the grid (completely enclosed by bisecting faces) will be consistently
943
- assigned to either the True or False part.
1529
+ - the face sets must form a single 'sealed' cut of the grid (eg. not waving in and out of the grid)
1530
+ - any 'boxed in' parts of the grid (completely enclosed by bisecting faces) will be consistently
1531
+ assigned to either the True or False part
944
1532
  """
945
1533
  assert len(grid_extent_kji) == 3
946
1534
 
947
- # Finding the surface boundary (includes a buffer slice where surface does not reach edge of grid).
948
- boundary = get_boundary(k_faces, j_faces, i_faces, grid_extent_kji)
949
-
950
- # Setting up the bisector array for the bounding box.
951
- bounding_array = np.zeros(
952
- (
953
- boundary["k_max"] - boundary["k_min"] + 1,
954
- boundary["j_max"] - boundary["j_min"] + 1,
955
- boundary["i_max"] - boundary["i_min"] + 1,
956
- ),
957
- dtype = np.bool_,
958
- )
1535
+ # find the surface boundary (includes a buffer slice where surface does not reach edge of grid)
1536
+ face_box = get_boundary_from_indices(k_faces_kji0, j_faces_kji0, i_faces_kji0, grid_extent_kji)
1537
+ box = np.empty((2, 3), dtype = np.int32)
1538
+ if p_box is None:
1539
+ box[:] = face_box
1540
+ else:
1541
+ box[:] = box_intersection(p_box, face_box)
1542
+ if np.all(box == 0):
1543
+ box[:] = face_box
1544
+ # set k_faces as bool arrays covering box
1545
+ k_faces, j_faces, i_faces = _box_face_arrays_from_indices(k_faces_kji0, j_faces_kji0, i_faces_kji0, box)
959
1546
 
960
- # Seeding the bisector array from (0, 0, 0) up to the first faces that represent the surface.
961
- boundary_values = tuple(boundary.values())
962
- bounding_array, first_k, first_j, first_i = _seed_array((0, 0, 0), k_faces, j_faces, i_faces, boundary_values,
963
- bounding_array)
964
- points = set()
965
- for dimension, first_true in enumerate([first_k, first_j, first_i]):
966
- for dimension_value in range(1, first_true):
967
- point = [0, 0, 0]
968
- point[dimension] = dimension_value
969
- point = tuple(point) # type: ignore
970
- bounding_array, first_k_sub, first_j_sub, first_i_sub = _seed_array(point, k_faces, j_faces, i_faces,
971
- boundary_values, bounding_array)
972
- for sub_dimension, first_true_sub in enumerate([first_k_sub, first_j_sub, first_i_sub]):
973
- if dimension != sub_dimension:
974
- for sub_dimension_value in range(1, first_true_sub):
975
- point = [0, 0, 0]
976
- point[dimension] = dimension_value
977
- point[sub_dimension] = sub_dimension_value
978
- point = tuple(point) # type: ignore
979
- if point not in points:
980
- points.add(point)
981
- bounding_array, _, _, _ = _seed_array(
982
- point,
983
- k_faces,
984
- j_faces,
985
- i_faces,
986
- boundary_values,
987
- bounding_array,
988
- )
989
-
990
- # Setting up the array for the changing values.
991
- changing_array = np.zeros_like(bounding_array, dtype = np.bool_)
992
-
993
- # Repeatedly spreading True values to neighbouring cells that are not the other side of a face.
994
- open_k = np.logical_not(k_faces)[boundary["k_min"]:boundary["k_max"], boundary["j_min"]:boundary["j_max"] + 1,
995
- boundary["i_min"]:boundary["i_max"] + 1,]
996
- open_j = np.logical_not(j_faces)[boundary["k_min"]:boundary["k_max"] + 1, boundary["j_min"]:boundary["j_max"],
997
- boundary["i_min"]:boundary["i_max"] + 1,]
998
- open_i = np.logical_not(i_faces)[boundary["k_min"]:boundary["k_max"] + 1, boundary["j_min"]:boundary["j_max"] + 1,
999
- boundary["i_min"]:boundary["i_max"],]
1000
- while True:
1001
- changing_array[:] = False
1002
-
1003
- # k faces
1004
- changing_array[1:, :, :] = np.logical_and(bounding_array[:-1, :, :], open_k)
1005
- changing_array[:-1, :, :] = np.logical_or(changing_array[:-1, :, :],
1006
- np.logical_and(bounding_array[1:, :, :], open_k))
1547
+ box_shape = box[1, :] - box[0, :]
1007
1548
 
1008
- # j faces
1009
- changing_array[:, 1:, :] = np.logical_or(changing_array[:, 1:, :],
1010
- np.logical_and(bounding_array[:, :-1, :], open_j))
1011
- changing_array[:, :-1, :] = np.logical_or(changing_array[:, :-1, :],
1012
- np.logical_and(bounding_array[:, 1:, :], open_j))
1549
+ # set up the bisector array for the bounding box
1550
+ box_array = np.zeros(box_shape, dtype = np.bool_)
1013
1551
 
1014
- # i faces
1015
- changing_array[:, :, 1:] = np.logical_or(changing_array[:, :, 1:],
1016
- np.logical_and(bounding_array[:, :, :-1], open_i))
1017
- changing_array[:, :, :-1] = np.logical_or(changing_array[:, :, :-1],
1018
- np.logical_and(bounding_array[:, :, 1:], open_i))
1552
+ # seed the bisector box array at (0, 0, 0)
1553
+ box_array[0, 0, 0] = True
1019
1554
 
1020
- changing_array[:] = np.logical_and(changing_array, np.logical_not(bounding_array))
1021
- if np.count_nonzero(changing_array) == 0:
1022
- break
1023
- bounding_array = np.logical_or(bounding_array, changing_array)
1555
+ # prepare to spread True values to neighbouring cells that are not the other side of a face
1556
+ if k_faces is None:
1557
+ open_k = np.ones((box_shape[0] - 1, box_shape[1], box_shape[2]), dtype = bool)
1558
+ else:
1559
+ open_k = np.logical_not(k_faces)
1560
+ if j_faces is None:
1561
+ open_j = np.ones((box_shape[0], box_shape[1] - 1, box_shape[2]), dtype = bool)
1562
+ else:
1563
+ open_j = np.logical_not(j_faces)
1564
+ if i_faces is None:
1565
+ open_i = np.ones((box_shape[0], box_shape[1], box_shape[2] - 1), dtype = bool)
1566
+ else:
1567
+ open_i = np.logical_not(i_faces)
1024
1568
 
1025
- # Setting up the full bisectors array and assigning the bounding box values.
1569
+ # populate bisector array for box
1570
+ _fill_bisector(box_array, open_k, open_j, open_i)
1571
+
1572
+ # set up the full bisectors array and assigning the bounding box values
1026
1573
  array = np.zeros(grid_extent_kji, dtype = np.bool_)
1027
- array[boundary["k_min"]:boundary["k_max"] + 1, boundary["j_min"]:boundary["j_max"] + 1,
1028
- boundary["i_min"]:boundary["i_max"] + 1,] = bounding_array
1029
-
1030
- # Setting values outside of the bounding box.
1031
- if boundary["k_max"] != grid_extent_kji[0] - 1 and np.any(bounding_array[-1, :, :]):
1032
- array[boundary["k_max"] + 1:, :, :] = True
1033
- if boundary["k_min"] != 0:
1034
- array[:boundary["k_min"], :, :] = True
1035
- if boundary["j_max"] != grid_extent_kji[1] - 1 and np.any(bounding_array[:, -1, :]):
1036
- array[:, boundary["j_max"] + 1:, :] = True
1037
- if boundary["j_min"] != 0:
1038
- array[:, :boundary["j_min"], :] = True
1039
- if boundary["i_max"] != grid_extent_kji[2] - 1 and np.any(bounding_array[:, :, -1]):
1040
- array[:, :, boundary["i_max"] + 1:] = True
1041
- if boundary["i_min"] != 0:
1042
- array[:, :, :boundary["i_min"]] = True
1043
-
1044
- # Check all array elements are not the same.
1574
+ array[box[0, 0]:box[1, 0], box[0, 1]:box[1, 1], box[0, 2]:box[1, 2]] = box_array
1575
+
1576
+ # set bisector values outside of the bounding box
1577
+ _set_bisector_outside_box(array, box, box_array)
1578
+
1579
+ # check all array elements are not the same
1045
1580
  true_count = np.count_nonzero(array)
1046
1581
  cell_count = array.size
1047
- assert (0 < true_count < cell_count), "Face set for surface is leaky or empty (surface does not intersect grid)."
1048
-
1049
- # Negate the array if it minimises the mean k and determine if the surface is a curtain.
1050
- layer_cell_count = grid_extent_kji[1] * grid_extent_kji[2]
1051
- array_k_sum = 0
1052
- array_opposite_k_sum = 0
1053
- is_curtain = False
1054
- for k in range(grid_extent_kji[0]):
1055
- array_layer_count = np.count_nonzero(array[k])
1056
- array_k_sum += (k + 1) * array_layer_count
1057
- array_opposite_k_sum += (k + 1) * (layer_cell_count - array_layer_count)
1058
- array_mean_k = float(array_k_sum) / float(true_count)
1059
- array_opposite_mean_k = float(array_opposite_k_sum) / float(cell_count - true_count)
1060
- if array_mean_k > array_opposite_mean_k and not raw_bisector:
1061
- array[:] = np.logical_not(array)
1062
- if abs(array_mean_k - array_opposite_mean_k) <= 0.001:
1063
- # log.warning('unable to determine which side of surface is shallower')
1064
- is_curtain = True
1582
+ if 0 < true_count < cell_count:
1583
+ # negate the array if it minimises the mean k and determine if the surface is a curtain
1584
+ is_curtain = _shallow_or_curtain(array, true_count, raw_bisector)
1585
+ else:
1586
+ assert raw_bisector, 'face set for surface is leaky or empty (surface does not intersect grid)'
1587
+ log.error('face set for surface is leaky or empty (surface does not intersect grid)')
1588
+ is_curtain = False
1065
1589
 
1066
1590
  return array, is_curtain
1067
1591
 
1068
1592
 
1593
+ # yapf: disable
1594
+ def packed_bisector_from_face_indices( # type: ignore
1595
+ grid_extent_kji: Tuple[int, int, int],
1596
+ k_faces_kji0: Union[np.ndarray, None],
1597
+ j_faces_kji0: Union[np.ndarray, None],
1598
+ i_faces_kji0: Union[np.ndarray, None],
1599
+ raw_bisector: bool,
1600
+ p_box: Union[np.ndarray, None]) -> Tuple[np.ndarray, bool]:
1601
+ # yapf: enable
1602
+ """Creates a uint8 (packed bool) array denoting the bisection of the grid by the face sets.
1603
+
1604
+ arguments:
1605
+ - grid_extent_kji (Tuple[int, int, int]): the shape of the grid
1606
+ - k_faces_kji0 (np.ndarray): an int array of indices of which faces represent the surface in the k dimension
1607
+ - j_faces_kji0 (np.ndarray): an int array of indices of which faces represent the surface in the j dimension
1608
+ - i_faces_kji0 (np.ndarray): an int array of indices of which faces represent the surface in the i dimension
1609
+ - raw_bisector (bool): if True, the bisector is returned without determining which side is shallower
1610
+ - p_box (np.ndarray): a python protocol unshrunken box to limit the bisector evaluation over
1611
+
1612
+ returns:
1613
+ Tuple containing:
1614
+ - array (np.uint8 array): packed boolean bisector array where values are 1 for cells on the side
1615
+ of the surface that has a lower mean k index on average and 0 for cells on the other side
1616
+ - is_curtain (bool): True if the surface is a curtain (vertical), otherwise False
1617
+
1618
+ notes:
1619
+ - the face sets must form a single 'sealed' cut of the grid (eg. not waving in and out of the grid)
1620
+ - any 'boxed in' parts of the grid (completely enclosed by bisecting faces) will be consistently
1621
+ assigned to either the True or False part
1622
+ - the returned array is packed in the I axis; use np.unpackbits() to unpack
1623
+ """
1624
+ assert len(grid_extent_kji) == 3
1625
+
1626
+ # find the surface boundary (includes a buffer slice where surface does not reach edge of grid), and shrink the I axis
1627
+ face_box = get_packed_boundary_from_indices(k_faces_kji0, j_faces_kji0, i_faces_kji0, grid_extent_kji)
1628
+ box = np.empty((2, 3), dtype = np.int32)
1629
+ if p_box is None:
1630
+ box[:] = face_box
1631
+ else:
1632
+ box[:] = box_intersection(shrunk_box_for_packing(p_box), face_box)
1633
+ if np.all(box == 0):
1634
+ box[:] = face_box
1635
+
1636
+ # set k_faces, j_faces & i_faces as uint8 packed bool arrays covering box
1637
+ k_faces, j_faces, i_faces = _packed_box_face_arrays_from_indices(k_faces_kji0, j_faces_kji0, i_faces_kji0, box)
1638
+
1639
+ box_shape = box[1, :] - box[0, :]
1640
+
1641
+ # set up the bisector array for the bounding box
1642
+ box_array = np.zeros(box_shape, dtype = np.uint8)
1643
+
1644
+ # seed the bisector box array at (0, 0, 0)
1645
+ box_array[0, 0, 0] = 0x80 # first bit only set
1646
+
1647
+ # prepare to spread True values to neighbouring cells that are not the other side of a face
1648
+ if k_faces is None:
1649
+ open_k = np.invert(np.zeros((box_shape[0] - 1, box_shape[1], box_shape[2]), dtype = np.uint8), dtype = np.uint8)
1650
+ else:
1651
+ open_k = np.invert(k_faces, dtype = np.uint8)
1652
+ if j_faces is None:
1653
+ open_j = np.invert(np.zeros((box_shape[0], box_shape[1] - 1, box_shape[2]), dtype = np.uint8), dtype = np.uint8)
1654
+ else:
1655
+ open_j = np.invert(j_faces, dtype = np.uint8)
1656
+ if i_faces is None:
1657
+ open_i = np.invert(np.zeros(tuple(box_shape), dtype = np.uint8), dtype = np.uint8)
1658
+ else:
1659
+ open_i = np.invert(i_faces, dtype = np.uint8)
1660
+
1661
+ # close off faces in padding bits, if within box
1662
+ if box[1, 2] * 8 > grid_extent_kji[2]:
1663
+ tail = grid_extent_kji[2] % 8 # number of valid bits in padded byte
1664
+ assert tail
1665
+ m = np.uint8((255 << (8 - tail)) & 255)
1666
+ open_k[:, :, -1] &= m
1667
+ open_j[:, :, -1] &= m
1668
+ m = np.uint8((m << 1) & 255)
1669
+ open_i[:, :, -1] &= m
1670
+
1671
+ # populate bisector array for box
1672
+ _fill_packed_bisector(box_array, open_k, open_j, open_i)
1673
+
1674
+ del open_i, open_j, open_k
1675
+
1676
+ # set up the full bisectors array and assigning the bounding box values
1677
+ p_array = np.zeros(_shape_packed(grid_extent_kji), dtype = np.uint8)
1678
+ p_array[box[0, 0]:box[1, 0], box[0, 1]:box[1, 1], box[0, 2]:box[1, 2]] = box_array
1679
+
1680
+ # set bisector values outside of the bounding box
1681
+ _set_packed_bisector_outside_box(p_array, box, box_array, grid_extent_kji[2] % 8)
1682
+
1683
+ # check all array elements are not the same
1684
+ if hasattr(np, 'bitwise_count'):
1685
+ true_count = np.sum(np.bitwise_count(p_array))
1686
+ else:
1687
+ true_count = _bitwise_count_njit(p_array) # note: will usually include some padding bits, so not so true!
1688
+ cell_count = np.prod(grid_extent_kji)
1689
+ if 0 < true_count < cell_count:
1690
+ # negate the array if it minimises the mean k and determine if the surface is a curtain
1691
+ # TODO: switch to _packed_shallow_or_curtain() when numba supports np.bitwise_count()
1692
+ is_curtain = _packed_shallow_or_curtain_temp_bitwise_count(p_array, true_count, raw_bisector)
1693
+ else:
1694
+ assert raw_bisector, 'face set for surface is leaky or empty (surface does not intersect grid)'
1695
+ log.error('face set for surface is leaky or empty (surface does not intersect grid)')
1696
+ is_curtain = False
1697
+
1698
+ return p_array, is_curtain
1699
+
1700
+
1701
+ def column_bisector_from_face_indices(grid_extent_ji: Tuple[int, int], j_faces_ji0: np.ndarray,
1702
+ i_faces_ji0: np.ndarray) -> np.ndarray:
1703
+ """Returns a numpy bool array denoting the bisection of the top layer of the grid by the curtain face sets.
1704
+
1705
+ arguments:
1706
+ - grid_extent_ji (pair of int): the shape of a layer of the grid
1707
+ - j_faces_ji0, i_faces_ji0 (2D numpy int arrays of shape (N, 2)): indices of faces within a layer
1708
+
1709
+ returns:
1710
+ numpy bool array of shape grid_extent_ji, set True for cells on one side of the face sets;
1711
+ set False for cells on othe side
1712
+
1713
+ notes:
1714
+ - the face sets must form a single 'sealed' cut of the grid (eg. not waving in and out of the grid)
1715
+ - any 'boxed in' parts of the grid (completely enclosed by bisecting faces) will be consistently
1716
+ assigned to the False part
1717
+ - the resulting array is suitable for use as a grid property with indexable element of columns
1718
+ - the array is set True for the side of the curtain that contains cell [0, 0]
1719
+ """
1720
+ assert len(grid_extent_ji) == 2
1721
+ j_faces = np.zeros((grid_extent_ji[0] - 1, grid_extent_ji[1]), dtype = np.bool_)
1722
+ i_faces = np.zeros((grid_extent_ji[0], grid_extent_ji[1] - 1), dtype = np.bool_)
1723
+ j_faces[j_faces_ji0[:, 0], j_faces_ji0[:, 1]] = True
1724
+ i_faces[i_faces_ji0[:, 0], i_faces_ji0[:, 1]] = True
1725
+ return column_bisector_from_faces(grid_extent_ji, j_faces, i_faces)
1726
+
1727
+
1069
1728
  def column_bisector_from_faces(grid_extent_ji: Tuple[int, int], j_faces: np.ndarray, i_faces: np.ndarray) -> np.ndarray:
1070
1729
  """Returns a numpy bool array denoting the bisection of the top layer of the grid by the curtain face sets.
1071
1730
 
@@ -1116,6 +1775,42 @@ def column_bisector_from_faces(grid_extent_ji: Tuple[int, int], j_faces: np.ndar
1116
1775
  return a
1117
1776
 
1118
1777
 
1778
+ def shadow_from_face_indices(extent_kji, kji0):
1779
+ """Returns a numpy int8 array indicating whether cells are above, below or between K faces.
1780
+
1781
+ arguments:
1782
+ extent_kji (triple int): the shape of the grid
1783
+ kji0 (numpy int array of shape (N, 3)): indices where a K face is present
1784
+
1785
+ returns:
1786
+ numpy int8 array of shape extent_kji; values are: 0 neither above nor below a K face;
1787
+ 1: above any K faces in the column; 2 below any K faces in the column;
1788
+ 3: between K faces (one or more above and one or more below)
1789
+ """
1790
+ assert len(extent_kji) == 3
1791
+ limit = extent_kji[0] - 1 # maximum number of iterations needed to spead shadow
1792
+ shadow = np.zeros(extent_kji, dtype = np.int8)
1793
+ shadow[kji0[:, 0], kji0[:, 1], kji0[:, 2]] = 1
1794
+ shadow[kji0[:, 0] + 1, kji0[:, 1], kji0[:, 2]] += 2
1795
+ for _ in range(limit):
1796
+ c = np.logical_and(shadow[:-1] == 0, shadow[1:] == 1)
1797
+ if np.count_nonzero(c) == 0:
1798
+ break
1799
+ shadow[:-1][c] = 1
1800
+ for _ in range(limit):
1801
+ c = np.logical_and(shadow[1:] == 0, shadow[:-1] == 2)
1802
+ if np.count_nonzero(c) == 0:
1803
+ break
1804
+ shadow[1:][c] = 2
1805
+ for _ in range(limit):
1806
+ c = np.logical_and(shadow[:-1] >= 2, shadow[1:] == 1)
1807
+ if np.count_nonzero(c) == 0:
1808
+ break
1809
+ shadow[:-1][c] = 3
1810
+ shadow[1:][c] = 3
1811
+ return shadow
1812
+
1813
+
1119
1814
  def shadow_from_faces(extent_kji, k_faces):
1120
1815
  """Returns a numpy int8 array indicating whether cells are above, below or between K faces.
1121
1816
 
@@ -1137,28 +1832,28 @@ def shadow_from_faces(extent_kji, k_faces):
1137
1832
  c = np.logical_and(shadow[:-1] == 0, shadow[1:] == 1)
1138
1833
  if np.count_nonzero(c) == 0:
1139
1834
  break
1140
- shadow[:-1] = np.where(c, 1, shadow[:-1])
1835
+ shadow[:-1][c] = 1
1141
1836
  for _ in range(limit):
1142
1837
  c = np.logical_and(shadow[1:] == 0, shadow[:-1] == 2)
1143
1838
  if np.count_nonzero(c) == 0:
1144
1839
  break
1145
- shadow[1:] = np.where(c, 2, shadow[1:])
1840
+ shadow[1:][c] = 2
1146
1841
  for _ in range(limit):
1147
1842
  c = np.logical_and(shadow[:-1] >= 2, shadow[1:] == 1)
1148
1843
  if np.count_nonzero(c) == 0:
1149
1844
  break
1150
- shadow[:-1] = np.where(c, 3, shadow[:-1])
1151
- shadow[1:] = np.where(c, 3, shadow[1:])
1845
+ shadow[:-1][c] = 3
1846
+ shadow[1:][c] = 3
1152
1847
  return shadow
1153
1848
 
1154
1849
 
1155
1850
  def get_boundary( # type: ignore
1156
- k_faces: np.ndarray,
1157
- j_faces: np.ndarray,
1158
- i_faces: np.ndarray,
1851
+ k_faces: Union[np.ndarray, None],
1852
+ j_faces: Union[np.ndarray, None],
1853
+ i_faces: Union[np.ndarray, None],
1159
1854
  grid_extent_kji: Tuple[int, int, int],
1160
- ) -> Dict[str, int]:
1161
- """Cretaes a dictionary of the indices that bound the surface (where the faces are True).
1855
+ ) -> np.ndarray:
1856
+ """Cretaes a box of the indices that bound the surface (where the faces are True).
1162
1857
 
1163
1858
  arguments:
1164
1859
  k_faces (np.ndarray): a boolean array of which faces represent the surface in the k dimension
@@ -1167,26 +1862,22 @@ def get_boundary( # type: ignore
1167
1862
  grid_extent_kji (Tuple[int, int, int]): the shape of the grid
1168
1863
 
1169
1864
  returns:
1170
- boundary (Dict[str, int]): a dictionary of the indices that bound the surface
1865
+ int array of shape (2, 3): bounding box in python protocol (max values have been incremented)
1171
1866
 
1172
1867
  note:
1173
1868
  input faces arrays are for internal grid faces (ie. extent reduced by 1 in axis of faces);
1174
1869
  a buffer slice is included where the surface does not reach the edge of the grid
1175
1870
  """
1176
1871
 
1177
- boundary = {
1178
- "k_min": None,
1179
- "k_max": None,
1180
- "j_min": None,
1181
- "j_max": None,
1182
- "i_min": None,
1183
- "i_max": None,
1184
- }
1872
+ boundary = np.zeros((2, 3), dtype = np.int32)
1185
1873
 
1186
1874
  starting = True
1187
1875
 
1188
1876
  for f_i, faces in enumerate([k_faces, j_faces, i_faces]):
1189
1877
 
1878
+ if faces is None:
1879
+ continue
1880
+
1190
1881
  # NB. k, j & i for rest of loop refer to indices of faces, regardless of which face set is being processed
1191
1882
 
1192
1883
  where_k, where_j, where_i = _where_true(faces)
@@ -1210,44 +1901,90 @@ def get_boundary( # type: ignore
1210
1901
  else:
1211
1902
  if min_k > 0:
1212
1903
  min_k -= 1
1213
- if max_k < j_faces.shape[0] - 1:
1904
+ if max_k < grid_extent_kji[0] - 1:
1214
1905
  max_k += 1
1215
1906
  if f_i == 1:
1216
1907
  max_j += 1
1217
1908
  else:
1218
1909
  if min_j > 0:
1219
1910
  min_j -= 1
1220
- if max_j < k_faces.shape[1] - 1:
1911
+ if max_j < grid_extent_kji[1] - 1:
1221
1912
  max_j += 1
1222
1913
  if f_i == 2:
1223
1914
  max_i += 1
1224
1915
  else:
1225
1916
  if min_i > 0:
1226
1917
  min_i -= 1
1227
- if max_i < k_faces.shape[2] - 1:
1918
+ if max_i < grid_extent_kji[2] - 1:
1228
1919
  max_i += 1
1229
1920
 
1230
1921
  if starting:
1231
- boundary["k_min"] = min_k
1232
- boundary["k_max"] = max_k
1233
- boundary["j_min"] = min_j
1234
- boundary["j_max"] = max_j
1235
- boundary["i_min"] = min_i
1236
- boundary["i_max"] = max_i
1922
+ boundary[0, 0] = min_k
1923
+ boundary[1, 0] = max_k
1924
+ boundary[0, 1] = min_j
1925
+ boundary[1, 1] = max_j
1926
+ boundary[0, 2] = min_i
1927
+ boundary[1, 2] = max_i
1237
1928
  starting = False
1238
1929
  else:
1239
- if min_k < boundary["k_min"]:
1240
- boundary["k_min"] = min_k
1241
- if max_k > boundary["k_max"]:
1242
- boundary["k_max"] = max_k
1243
- if min_j < boundary["j_min"]:
1244
- boundary["j_min"] = min_j
1245
- if max_j > boundary["j_max"]:
1246
- boundary["j_max"] = max_j
1247
- if min_i < boundary["i_min"]:
1248
- boundary["i_min"] = min_i
1249
- if max_i > boundary["i_max"]:
1250
- boundary["i_max"] = max_i
1930
+ if min_k < boundary[0, 0]:
1931
+ boundary[0, 0] = min_k
1932
+ if max_k > boundary[1, 0]:
1933
+ boundary[1, 0] = max_k
1934
+ if min_j < boundary[0, 1]:
1935
+ boundary[0, 1] = min_j
1936
+ if max_j > boundary[1, 1]:
1937
+ boundary[1, 1] = max_j
1938
+ if min_i < boundary[0, 2]:
1939
+ boundary[0, 2] = min_i
1940
+ if max_i > boundary[1, 2]:
1941
+ boundary[1, 2] = max_i
1942
+
1943
+ boundary[1, :] += 1 # increment max values to give python protocol box
1944
+
1945
+ return boundary # type: ignore
1946
+
1947
+
1948
+ def get_boundary_dict( # type: ignore
1949
+ k_faces: Union[np.ndarray, None],
1950
+ j_faces: Union[np.ndarray, None],
1951
+ i_faces: Union[np.ndarray, None],
1952
+ grid_extent_kji: Tuple[int, int, int],
1953
+ ) -> Dict[str, int]:
1954
+ """Cretaes a dictionary of the indices that bound the surface (where the faces are True).
1955
+
1956
+ arguments:
1957
+ k_faces (np.ndarray): a boolean array of which faces represent the surface in the k dimension
1958
+ j_faces (np.ndarray): a boolean array of which faces represent the surface in the j dimension
1959
+ i_faces (np.ndarray): a boolean array of which faces represent the surface in the i dimension
1960
+ grid_extent_kji (Tuple[int, int, int]): the shape of the grid
1961
+
1962
+ returns:
1963
+ boundary (Dict[str, int]): a dictionary of the indices that bound the surface
1964
+
1965
+ note:
1966
+ input faces arrays are for internal grid faces (ie. extent reduced by 1 in axis of faces);
1967
+ a buffer slice is included where the surface does not reach the edge of the grid;
1968
+ max values are not increment, ie. need to be incremented to be used as an upper end of a python range
1969
+ """
1970
+
1971
+ boundary = {
1972
+ "k_min": None,
1973
+ "k_max": None,
1974
+ "j_min": None,
1975
+ "j_max": None,
1976
+ "i_min": None,
1977
+ "i_max": None,
1978
+ }
1979
+
1980
+ box = get_boundary(k_faces, j_faces, i_faces, grid_extent_kji)
1981
+
1982
+ boundary["k_min"] = box[0, 0]
1983
+ boundary["k_max"] = box[1, 0] - 1
1984
+ boundary["j_min"] = box[0, 1]
1985
+ boundary["j_max"] = box[1, 1] - 1
1986
+ boundary["i_min"] = box[0, 2]
1987
+ boundary["i_max"] = box[1, 2] - 1
1251
1988
 
1252
1989
  return boundary # type: ignore
1253
1990
 
@@ -1259,7 +1996,7 @@ def _where_true(data: np.ndarray):
1259
1996
 
1260
1997
 
1261
1998
  @njit # pragma: no cover
1262
- def _first_true(array: np.ndarray) -> Optional[int]: # type: ignore
1999
+ def _first_true(array: np.ndarray) -> int: # type: ignore
1263
2000
  """Returns the index + 1 of the first True value in the array."""
1264
2001
  for idx, val in np.ndenumerate(array):
1265
2002
  if val:
@@ -1267,6 +2004,25 @@ def _first_true(array: np.ndarray) -> Optional[int]: # type: ignore
1267
2004
  return array.size
1268
2005
 
1269
2006
 
2007
+ @njit('(uint8[:,:,:], uint8[:,:,:], uint8[:,:,:], int32[:,:])', parallel = True) # pragma: no cover
2008
+ def _set_packed_where_mask(a: np.ndarray, mask: np.ndarray, v: np.ndarray, box: np.ndarray): # type: ignore
2009
+ """Update 3D packed boolean array a from packed boolean array v where packed boolean array mask is set."""
2010
+ assert a.ndim == 3 and a.shape == mask.shape and a.shape == v.shape and box.shape == (2, 3)
2011
+ sk: int = box[0, 0]
2012
+ sj: int = box[0, 1]
2013
+ si: int = box[0, 2]
2014
+ ek: int = box[1, 0]
2015
+ ej: int = box[1, 1]
2016
+ ei: int = box[1, 2]
2017
+ for k in prange(sk, ek):
2018
+ for j in range(sj, ej):
2019
+ for i in range(si, ei):
2020
+ m: np.uint8 = mask[k, j, i]
2021
+ if m != 0:
2022
+ not_m: np.uint8 = ~m
2023
+ a[k, j, i] = (m & v[k, j, i]) | (not_m & a[k, j, i])
2024
+
2025
+
1270
2026
  @njit # pragma: no cover
1271
2027
  def intersect_numba(
1272
2028
  axis: int,
@@ -1350,6 +2106,7 @@ def intersect_numba(
1350
2106
  face_idx[index2] = d2
1351
2107
  face_idx[2 - axis] = face
1352
2108
 
2109
+ # dangerous: relies on indivisible read-modify-write of memory word containing multiple faces elements
1353
2110
  faces[face_idx[0], face_idx[1], face_idx[2]] = True
1354
2111
 
1355
2112
  if return_depths:
@@ -1362,62 +2119,401 @@ def intersect_numba(
1362
2119
  return faces, offsets, triangle_per_face
1363
2120
 
1364
2121
 
2122
+ def _all_offsets(crs, k_offsets_list, j_offsets_list, i_offsets_list):
2123
+ if crs.xy_units == crs.z_units:
2124
+ return np.concatenate((k_offsets_list, j_offsets_list, i_offsets_list), axis = 0)
2125
+ ji_offsets = np.concatenate((j_offsets_list, i_offsets_list), axis = 0)
2126
+ wam.convert_lengths(ji_offsets, crs.xy_units, crs.z_units)
2127
+ return np.concatenate((k_offsets_list, ji_offsets), axis = 0)
2128
+
2129
+
1365
2130
  @njit # pragma: no cover
1366
- def _seed_array(
1367
- point: Tuple[int, int, int],
1368
- k_faces: np.ndarray,
1369
- j_faces: np.ndarray,
1370
- i_faces: np.ndarray,
1371
- boundary: Tuple[int, int, int, int, int, int],
1372
- array: np.ndarray,
1373
- ) -> Tuple[np.ndarray, int, int, int]:
1374
- """Sets values of the array True up until a face is hit in each direction.
2131
+ def _fill_bisector(bisect: np.ndarray, open_k: np.ndarray, open_j: np.ndarray, open_i: np.ndarray):
2132
+ nk: int = bisect.shape[0]
2133
+ nj: int = bisect.shape[1]
2134
+ ni: int = bisect.shape[2]
2135
+ going: bool = True
2136
+ while going:
2137
+ going = False
2138
+ for k in range(nk):
2139
+ for j in range(nj):
2140
+ for i in range(ni):
2141
+ if bisect[k, j, i]:
2142
+ continue
2143
+ if ((k and bisect[k - 1, j, i] and open_k[k - 1, j, i]) or
2144
+ (j and bisect[k, j - 1, i] and open_j[k, j - 1, i]) or
2145
+ (i and bisect[k, j, i - 1] and open_i[k, j, i - 1]) or
2146
+ (k < nk - 1 and bisect[k + 1, j, i] and open_k[k, j, i]) or
2147
+ (j < nj - 1 and bisect[k, j + 1, i] and open_j[k, j, i]) or
2148
+ (i < ni - 1 and bisect[k, j, i + 1] and open_i[k, j, i])):
2149
+ bisect[k, j, i] = True
2150
+ going = True
1375
2151
 
1376
- arguments:
1377
- point (Tuple[int, int, int]): coordinates of the initial seed point.
1378
- k_faces (np.ndarray): a boolean array of which faces represent the surface in the k dimension.
1379
- j_faces (np.ndarray): a boolean array of which faces represent the surface in the j dimension.
1380
- i_faces (np.ndarray): a boolean array of which faces represent the surface in the i dimension.
1381
- boundary (Tuple[int, int, int, int, int, int]): the boundaries of the surface given in the order
1382
- (minimum k, maximum k, minimum j, maximum j, minimum i, maximum i).
1383
- array (np.ndarray): boolean array that will be seeded.
1384
2152
 
1385
- returns:
1386
- Tuple containing:
2153
+ @njit # pragma: no cover
2154
+ def _fill_packed_bisector(bisect: np.ndarray, open_k: np.ndarray, open_j: np.ndarray, open_i: np.ndarray):
2155
+ nk: int = bisect.shape[0]
2156
+ nj: int = bisect.shape[1]
2157
+ ni: int = bisect.shape[2]
2158
+ going: bool = True
2159
+ m: np.uint8 = np.uint8(0)
2160
+ om: np.uint8 = np.uint8(0)
2161
+ oi: np.uint8 = np.uint8(0)
2162
+ while going:
2163
+ going = False
2164
+ for k in range(nk):
2165
+ for j in range(nj):
2166
+ for i in range(ni):
2167
+ m = np.uint8(bisect[k, j, i]) # 8 bools packed into a uint8
2168
+ if bisect[k, j, i] == np.uint8(0xFF): # all 8 values already set
2169
+ continue
2170
+ om = m # copy to check for changes later
2171
+ if k:
2172
+ m |= (bisect[k - 1, j, i] & open_k[k - 1, j, i])
2173
+ if k < nk - 1:
2174
+ m |= (bisect[k + 1, j, i] & open_k[k, j, i])
2175
+ if j:
2176
+ m |= (bisect[k, j - 1, i] & open_j[k, j - 1, i])
2177
+ if j < nj - 1:
2178
+ m |= (bisect[k, j + 1, i] & open_j[k, j, i])
2179
+ oi = np.uint8(open_i[k, j, i]) # type: ignore
2180
+ m |= (m >> 1) & (oi >> 1) # type: ignore
2181
+ m |= (m << 1) & oi # type: ignore
2182
+ # handle rollover bits for I
2183
+ if i and (bisect[k, j, i - 1] & open_i[k, j, i - 1] & np.uint8(0x01)):
2184
+ m |= np.uint8(0x80)
2185
+ if (i < ni - 1) and (oi & 1) and (bisect[k, j, i + 1] & 0x80):
2186
+ m |= np.uint8(0x01)
2187
+ if m != om:
2188
+ bisect[k, j, i] = m
2189
+ going = True
1387
2190
 
1388
- - array (np.ndarray): boolean array that has been seeded.
1389
- - first_k (int): the index of the first k face in the k direction from the seed point or the
1390
- array size in the k direction if there are no k faces.
1391
- - first_j (int): the index of the first j face in the j direction from the seed point or the
1392
- array size in the j direction if there are no j faces.
1393
- - first_i (int): the index of the first i face in the i direction from the seed point or the
1394
- array size in the i direction if there are no i faces.
1395
- """
1396
- k = point[0]
1397
- j = point[1]
1398
- i = point[2]
1399
2191
 
1400
- first_k = 0
1401
- if k == 0:
1402
- first_k = _first_true(k_faces[boundary[0]:boundary[1], boundary[2] + j, boundary[4] + i])
1403
- array[:first_k, j, i] = True
2192
+ @njit # pragma: no cover
2193
+ def _shallow_or_curtain(a: np.ndarray, true_count: int, raw: bool) -> bool:
2194
+ # negate the bool array if it minimises the mean k and determine if the bisector indicates a curtain
2195
+ assert a.ndim == 3
2196
+ layer_cell_count: int = a.shape[1] * a.shape[2]
2197
+ k_sum: int = 0
2198
+ opposite_k_sum: int = 0
2199
+ is_curtain: bool = False
2200
+ layer_count: int = 0
2201
+ for k in range(a.shape[0]):
2202
+ layer_count = np.count_nonzero(a[k])
2203
+ k_sum += (k + 1) * layer_count
2204
+ opposite_k_sum += (k + 1) * (layer_cell_count - layer_count)
2205
+ mean_k: float = float(k_sum) / float(true_count)
2206
+ opposite_mean_k: float = float(opposite_k_sum) / float(a.size - true_count)
2207
+ if mean_k > opposite_mean_k and not raw:
2208
+ a[:] = np.logical_not(a)
2209
+ if abs(mean_k - opposite_mean_k) <= 0.001:
2210
+ # log.warning('unable to determine which side of surface is shallower')
2211
+ is_curtain = True
2212
+ return is_curtain
2213
+
1404
2214
 
1405
- first_j = 0
1406
- if j == 0:
1407
- first_j = _first_true(j_faces[boundary[0] + k, boundary[2]:boundary[3], boundary[4] + i])
1408
- array[k, :first_j, i] = True
2215
+ @njit # pragma: no cover
2216
+ def _packed_shallow_or_curtain(a: np.ndarray, true_count: int, raw: bool) -> bool:
2217
+ # negate the packed bool array if it minimises the mean k and determine if the bisector indicates a curtain
2218
+ assert a.ndim == 3
2219
+ layer_cell_count: int = 8 * a.shape[1] * a.shape[2] # note: includes padding bits
2220
+ k_sum: int = 0
2221
+ opposite_k_sum: int = 0
2222
+ is_curtain: bool = False
2223
+ layer_count: int = 0
2224
+ for k in range(a.shape[0]):
2225
+ # np.bitwise_count() not yet supported by numba
2226
+ layer_count = np.sum(np.bitwise_count(a[k]), dtype = np.int64) # type: ignore
2227
+ k_sum += (k + 1) * layer_count
2228
+ opposite_k_sum += (k + 1) * (layer_cell_count - layer_count)
2229
+ mean_k: float = float(k_sum) / float(true_count)
2230
+ opposite_mean_k: float = float(opposite_k_sum) / float(8 * a.size - true_count)
2231
+ if mean_k > opposite_mean_k and not raw:
2232
+ a[:] = np.invert(a)
2233
+ if abs(mean_k - opposite_mean_k) <= 0.001:
2234
+ # log.warning('unable to determine which side of surface is shallower')
2235
+ is_curtain = True
2236
+ return is_curtain
1409
2237
 
1410
- first_i = 0
1411
- if i == 0:
1412
- first_i = _first_true(i_faces[boundary[0] + k, boundary[2] + j, boundary[4]:boundary[5]])
1413
- array[k, j, :first_i] = True
1414
2238
 
1415
- return array, first_k, first_j, first_i
2239
+ @njit # pragma: no cover
2240
+ def _packed_shallow_or_curtain_temp_bitwise_count(a: np.ndarray, true_count: int, raw: bool) -> bool:
2241
+ # negate the packed bool array if it minimises the mean k and determine if the bisector indicates a curtain
2242
+ assert a.ndim == 3
2243
+ # note: following 'cell count' includes padding bits
2244
+ layer_cell_count: np.int64 = 8 * a.shape[1] * a.shape[2] # type: ignore
2245
+ k_sum: np.int64 = 0 # type: ignore
2246
+ opposite_k_sum: np.int64 = 0 # type: ignore
2247
+ is_curtain: bool = False
2248
+ layer_count: np.int64 = 0 # type: ignore
2249
+ for k in range(a.shape[0]):
2250
+ layer_count = _bitwise_count_njit(a[k, :, :])
2251
+ k_sum += (k + 1) * layer_count
2252
+ opposite_k_sum += (k + 1) * (layer_cell_count - layer_count)
2253
+ mean_k: float = float(k_sum) / float(true_count)
2254
+ opposite_mean_k: float = float(opposite_k_sum) / float(8 * a.size - true_count)
2255
+ if mean_k > opposite_mean_k and not raw:
2256
+ a[:] = np.invert(a)
2257
+ if abs(mean_k - opposite_mean_k) <= 0.001:
2258
+ # log.warning('unable to determine which side of surface is shallower')
2259
+ is_curtain = True
2260
+ return is_curtain
2261
+
2262
+
2263
+ def _set_bisector_outside_box(a: np.ndarray, box: np.ndarray, box_array: np.ndarray): # type: ignore
2264
+ # set values outside of the bounding box
2265
+ if box[1, 0] < a.shape[0] and np.any(box_array[-1, :, :]):
2266
+ a[box[1, 0]:, :, :] = True
2267
+ if box[0, 0] != 0:
2268
+ a[:box[0, 0], :, :] = True
2269
+ if box[1, 1] < a.shape[1] and np.any(box_array[:, -1, :]):
2270
+ a[:, box[1, 1]:, :] = True
2271
+ if box[0, 1] != 0:
2272
+ a[:, :box[0, 1], :] = True
2273
+ if box[1, 2] < a.shape[2] and np.any(box_array[:, :, -1]):
2274
+ a[:, :, box[1, 2]:] = True
2275
+ if box[0, 2] != 0:
2276
+ a[:, :, :box[0, 2]] = True
2277
+
2278
+
2279
+ def _set_packed_bisector_outside_box(a: np.ndarray, box: np.ndarray, box_array: np.ndarray, tail: int):
2280
+ # set values outside of the bounding box, working with packed arrays
2281
+ if box[1, 0] < a.shape[0] and np.any(box_array[-1, :, :]):
2282
+ a[box[1, 0]:, :, :] = 255
2283
+ if box[0, 0] != 0:
2284
+ a[:box[0, 0], :, :] = 255
2285
+ if box[1, 1] < a.shape[1] and np.any(box_array[:, -1, :]):
2286
+ a[:, box[1, 1]:, :] = 255
2287
+ if box[0, 1] != 0:
2288
+ a[:, :box[0, 1], :] = 255
2289
+ if box[1, 2] < a.shape[2] and np.any(np.bitwise_and(box_array[:, :, -1], 1)):
2290
+ a[:, :, box[1, 2]:] = 255
2291
+ if box[0, 2] != 0:
2292
+ a[:, :, :box[0, 2]] = 255
2293
+ if tail:
2294
+ m = np.uint8((255 << (8 - tail)) & 255)
2295
+ a[:, :, -1] &= m
2296
+
2297
+
2298
+ def _box_face_arrays_from_indices( # type: ignore
2299
+ k_faces_kji0: Union[np.ndarray, None], j_faces_kji0: Union[np.ndarray, None],
2300
+ i_faces_kji0: Union[np.ndarray, None], box: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
2301
+ box_shape = box[1, :] - box[0, :]
2302
+ k_a = np.zeros((box_shape[0] - 1, box_shape[1], box_shape[2]), dtype = np.bool_)
2303
+ j_a = np.zeros((box_shape[0], box_shape[1] - 1, box_shape[2]), dtype = np.bool_)
2304
+ i_a = np.zeros((box_shape[0], box_shape[1], box_shape[2] - 1), dtype = np.bool_)
2305
+ ko = box[0, 0]
2306
+ jo = box[0, 1]
2307
+ io = box[0, 2]
2308
+ kr = box[1, 0] - ko
2309
+ jr = box[1, 1] - jo
2310
+ ir = box[1, 2] - io
2311
+ if k_faces_kji0 is not None:
2312
+ _set_face_array(k_a, k_faces_kji0, ko, jo, io, kr - 1, jr, ir)
2313
+ if j_faces_kji0 is not None:
2314
+ _set_face_array(j_a, j_faces_kji0, ko, jo, io, kr, jr - 1, ir)
2315
+ if i_faces_kji0 is not None:
2316
+ _set_face_array(i_a, i_faces_kji0, ko, jo, io, kr, jr, ir - 1)
2317
+ return k_a, j_a, i_a
2318
+
2319
+
2320
+ def _packed_box_face_arrays_from_indices( # type: ignore
2321
+ k_faces_kji0: Union[np.ndarray, None], j_faces_kji0: Union[np.ndarray, None],
2322
+ i_faces_kji0: Union[np.ndarray, None], box: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
2323
+ box_shape = box[1, :] - box[0, :] # note: I axis already shrunken
2324
+ k_a = np.zeros((box_shape[0] - 1, box_shape[1], box_shape[2]), dtype = np.uint8)
2325
+ j_a = np.zeros((box_shape[0], box_shape[1] - 1, box_shape[2]), dtype = np.uint8)
2326
+ i_a = np.zeros(tuple(box_shape), dtype = np.uint8)
2327
+ ko = box[0, 0]
2328
+ jo = box[0, 1]
2329
+ io = box[0, 2] * 8
2330
+ kr = box[1, 0] - ko
2331
+ jr = box[1, 1] - jo
2332
+ ir = box[1, 2] * 8 - io
2333
+ if k_faces_kji0 is not None:
2334
+ _set_packed_face_array(k_a, k_faces_kji0, ko, jo, io, kr - 1, jr, ir)
2335
+ if j_faces_kji0 is not None:
2336
+ _set_packed_face_array(j_a, j_faces_kji0, ko, jo, io, kr, jr - 1, ir)
2337
+ if i_faces_kji0 is not None:
2338
+ _set_packed_face_array(i_a, i_faces_kji0, ko, jo, io, kr, jr, ir)
2339
+ return k_a, j_a, i_a
1416
2340
 
1417
2341
 
1418
- def _all_offsets(crs, k_offsets_list, j_offsets_list, i_offsets_list):
1419
- if crs.xy_units == crs.z_units:
1420
- return np.concatenate((k_offsets_list, j_offsets_list, i_offsets_list), axis = 0)
1421
- ji_offsets = np.concatenate((j_offsets_list, i_offsets_list), axis = 0)
1422
- wam.convert_lengths(ji_offsets, crs.xy_units, crs.z_units)
1423
- return np.concatenate((k_offsets_list, ji_offsets), axis = 0)
2342
+ @njit # pragma: no cover
2343
+ def _set_face_array(a: np.ndarray, indices: np.ndarray, ko: int, jo: int, io: int, kr: int, jr: int, ir: int) -> None:
2344
+ k: int = 0
2345
+ j: int = 0
2346
+ i: int = 0
2347
+ for ind in range(len(indices)):
2348
+ k = indices[ind, 0] - ko
2349
+ if k < 0 or k >= kr:
2350
+ continue
2351
+ j = indices[ind, 1] - jo
2352
+ if j < 0 or j >= jr:
2353
+ continue
2354
+ i = indices[ind, 2] - io
2355
+ if i < 0 or i >= ir:
2356
+ continue
2357
+ a[k, j, i] = True
2358
+
2359
+
2360
+ @njit # pragma: no cover
2361
+ def _set_packed_face_array(a: np.ndarray, indices: np.ndarray, ko: int, jo: int, io: int, kr: int, jr: int,
2362
+ ir: int) -> None:
2363
+ k: int = 0
2364
+ j: int = 0
2365
+ i: int = 0
2366
+ for ind in range(len(indices)):
2367
+ k = indices[ind, 0] - ko
2368
+ if k < 0 or k >= kr:
2369
+ continue
2370
+ j = indices[ind, 1] - jo
2371
+ if j < 0 or j >= jr:
2372
+ continue
2373
+ i = indices[ind, 2] - io
2374
+ if i < 0 or i >= ir:
2375
+ continue
2376
+ ii, ib = divmod(i, 8)
2377
+ a[k, j, ii] |= (1 << (7 - ib))
2378
+
2379
+
2380
+ # yapf: disable
2381
+ def get_boundary_from_indices( # type: ignore
2382
+ k_faces_kji0: Union[np.ndarray, None],
2383
+ j_faces_kji0: Union[np.ndarray, None],
2384
+ i_faces_kji0: Union[np.ndarray, None],
2385
+ grid_extent_kji: Tuple[int, int, int]) -> np.ndarray:
2386
+ # yapf: enable
2387
+ """Return python protocol box containing indices"""
2388
+ k_min_kji0 = None if ((k_faces_kji0 is None) or (k_faces_kji0.size == 0)) else np.min(k_faces_kji0, axis = 0)
2389
+ k_max_kji0 = None if ((k_faces_kji0 is None) or (k_faces_kji0.size == 0)) else np.max(k_faces_kji0, axis = 0)
2390
+ j_min_kji0 = None if ((j_faces_kji0 is None) or (j_faces_kji0.size == 0)) else np.min(j_faces_kji0, axis = 0)
2391
+ j_max_kji0 = None if ((j_faces_kji0 is None) or (j_faces_kji0.size == 0)) else np.max(j_faces_kji0, axis = 0)
2392
+ i_min_kji0 = None if ((i_faces_kji0 is None) or (i_faces_kji0.size == 0)) else np.min(i_faces_kji0, axis = 0)
2393
+ i_max_kji0 = None if ((i_faces_kji0 is None) or (i_faces_kji0.size == 0)) else np.max(i_faces_kji0, axis = 0)
2394
+ box = np.empty((2, 3), dtype = np.int32)
2395
+ box[0, :] = grid_extent_kji
2396
+ box[1, :] = -1
2397
+ if k_min_kji0 is not None:
2398
+ box[0, 0] = k_min_kji0[0]
2399
+ box[0, 1] = k_min_kji0[1]
2400
+ box[0, 2] = k_min_kji0[2]
2401
+ box[1, 0] = k_max_kji0[0] # type: ignore
2402
+ box[1, 1] = k_max_kji0[1] # type: ignore
2403
+ box[1, 2] = k_max_kji0[2] # type: ignore
2404
+ if j_min_kji0 is not None:
2405
+ box[0, 0] = min(box[0, 0], j_min_kji0[0])
2406
+ box[0, 1] = min(box[0, 1], j_min_kji0[1])
2407
+ box[0, 2] = min(box[0, 2], j_min_kji0[2])
2408
+ box[1, 0] = max(box[1, 0], j_max_kji0[0]) # type: ignore
2409
+ box[1, 1] = max(box[1, 1], j_max_kji0[1]) # type: ignore
2410
+ box[1, 2] = max(box[1, 2], j_max_kji0[2]) # type: ignore
2411
+ if i_min_kji0 is not None:
2412
+ box[0, 0] = min(box[0, 0], i_min_kji0[0])
2413
+ box[0, 1] = min(box[0, 1], i_min_kji0[1])
2414
+ box[0, 2] = min(box[0, 2], i_min_kji0[2])
2415
+ box[1, 0] = max(box[1, 0], i_max_kji0[0]) # type: ignore
2416
+ box[1, 1] = max(box[1, 1], i_max_kji0[1]) # type: ignore
2417
+ box[1, 2] = max(box[1, 2], i_max_kji0[2]) # type: ignore
2418
+ assert np.all(box[1] >= box[0]), 'attempt to find bounding box when all faces None'
2419
+ # include buffer layer where box does not reach edge of grid
2420
+ box[1, :] += 1 # switch to python protocol
2421
+ return expanded_box(box, grid_extent_kji)
2422
+
2423
+
2424
+ def expanded_box(box: np.ndarray, extent_kji: Tuple[int, int, int]) -> np.ndarray:
2425
+ """Return a python protocol box expanded by a single slice on all six faces, where extent alloas."""
2426
+ # include buffer layer where box does not reach edge of grid
2427
+ np_extent_kji = np.array(extent_kji, dtype = np.int32)
2428
+ e_box = np.zeros((2, 3), dtype = np.int32)
2429
+ e_box[0, :] = np.maximum(box[0, :] - 1, 0)
2430
+ e_box[1, :] = np.minimum(box[1, :] + 1, extent_kji)
2431
+ assert np.all(e_box[0] >= 0)
2432
+ assert np.all(e_box[1] > e_box[0])
2433
+ assert np.all(e_box[1] <= np_extent_kji)
2434
+ return e_box
2435
+
2436
+
2437
+ def get_packed_boundary_from_indices( # type: ignore
2438
+ k_faces_kji0: Union[np.ndarray, None], j_faces_kji0: Union[np.ndarray, None],
2439
+ i_faces_kji0: Union[np.ndarray, None], grid_extent_kji: Tuple[int, int, int]) -> np.ndarray:
2440
+ """Return python protocol box containing indices, with I axis packed"""
2441
+ box = get_boundary_from_indices(k_faces_kji0, j_faces_kji0, i_faces_kji0, grid_extent_kji)
2442
+ return shrunk_box_for_packing(box)
2443
+
2444
+
2445
+ def shrunk_box_for_packing(box: np.ndarray) -> np.ndarray:
2446
+ """Return box with I dimension shrunk for bit packing equivalent."""
2447
+ shrunk_box = box.copy()
2448
+ shrunk_box[0, 2] /= 8
2449
+ shrunk_box[1, 2] = ((box[1, 2] - 1) // 8) + 1
2450
+ return shrunk_box
2451
+
2452
+
2453
+ def _shape_packed(unpacked_shape):
2454
+ """Return the equivalent packed shape for a given unpacked shape, as a tuple."""
2455
+ shrunken = ((unpacked_shape[-1] - 1) // 8) + 1
2456
+ if len(unpacked_shape) == 1:
2457
+ return (shrunken,)
2458
+ head = list(unpacked_shape[:-1])
2459
+ head.append(shrunken)
2460
+ return tuple(head)
2461
+
2462
+
2463
+ @njit # pragma: no cover
2464
+ def _bitwise_count_njit(a: np.ndarray) -> np.int64:
2465
+ """Deprecated: only needed till numpy versions < 2.0.0 are dropped."""
2466
+ c: np.int64 = 0 # type: ignore
2467
+ c += np.count_nonzero(np.bitwise_and(a, 0x01))
2468
+ c += np.count_nonzero(np.bitwise_and(a, 0x02))
2469
+ c += np.count_nonzero(np.bitwise_and(a, 0x04))
2470
+ c += np.count_nonzero(np.bitwise_and(a, 0x08))
2471
+ c += np.count_nonzero(np.bitwise_and(a, 0x10))
2472
+ c += np.count_nonzero(np.bitwise_and(a, 0x20))
2473
+ c += np.count_nonzero(np.bitwise_and(a, 0x40))
2474
+ c += np.count_nonzero(np.bitwise_and(a, 0x80))
2475
+ return c
2476
+
2477
+
2478
+ @njit
2479
+ def box_intersection(box_a: np.ndarray, box_b: np.ndarray) -> np.ndarray:
2480
+ """Return a box which is the intersection of two boxes, python protocol; all zeros if no intersection."""
2481
+ box = np.zeros((2, 3), dtype = np.int32)
2482
+ box[0] = np.maximum(box_a[0], box_b[0])
2483
+ box[1] = np.minimum(box_a[1], box_b[1])
2484
+ if np.any(box[1] <= box[0]):
2485
+ box[:] = 0
2486
+ return box
2487
+
2488
+
2489
+ @njit
2490
+ def get_box(mask: np.ndarray) -> Tuple[np.ndarray, int]: # pragma: no cover
2491
+ """Returns a python protocol box enclosing True elements of 3D boolean mask, and count which is zero if all False."""
2492
+ box = np.full((2, 3), -1, dtype = np.int32)
2493
+ count = 0
2494
+ for k in range(mask.shape[0]):
2495
+ for j in range(mask.shape[1]):
2496
+ for i in range(mask.shape[2]):
2497
+ if mask[k, j, i]:
2498
+ if count == 0:
2499
+ box[0, 0] = k
2500
+ box[0, 1] = j
2501
+ box[0, 2] = i
2502
+ box[1, 0] = k + 1
2503
+ box[1, 1] = j + 1
2504
+ box[1, 2] = i + 1
2505
+ else:
2506
+ if k < box[0, 0]:
2507
+ box[0, 0] = k
2508
+ elif k >= box[1, 0]:
2509
+ box[1, 0] = k + 1
2510
+ if j < box[0, 1]:
2511
+ box[0, 1] = j
2512
+ elif j >= box[1, 1]:
2513
+ box[1, 1] = j + 1
2514
+ if i < box[0, 2]:
2515
+ box[0, 2] = i
2516
+ elif i >= box[1, 2]:
2517
+ box[1, 2] = i + 1
2518
+ count += 1
2519
+ return box, count