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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. resqpy/__init__.py +1 -1
  2. resqpy/fault/_gcs_functions.py +10 -10
  3. resqpy/fault/_grid_connection_set.py +277 -113
  4. resqpy/grid/__init__.py +2 -3
  5. resqpy/grid/_defined_geometry.py +3 -3
  6. resqpy/grid/_extract_functions.py +2 -1
  7. resqpy/grid/_grid.py +95 -12
  8. resqpy/grid/_grid_types.py +22 -7
  9. resqpy/grid/_points_functions.py +1 -1
  10. resqpy/grid/_regular_grid.py +6 -2
  11. resqpy/grid_surface/__init__.py +17 -38
  12. resqpy/grid_surface/_blocked_well_populate.py +5 -5
  13. resqpy/grid_surface/_find_faces.py +1349 -253
  14. resqpy/lines/_polyline.py +24 -33
  15. resqpy/model/_catalogue.py +9 -0
  16. resqpy/model/_forestry.py +18 -14
  17. resqpy/model/_hdf5.py +11 -3
  18. resqpy/model/_model.py +85 -10
  19. resqpy/model/_xml.py +38 -13
  20. resqpy/multi_processing/wrappers/grid_surface_mp.py +92 -37
  21. resqpy/olio/read_nexus_fault.py +8 -2
  22. resqpy/olio/relperm.py +1 -1
  23. resqpy/olio/transmission.py +8 -8
  24. resqpy/olio/triangulation.py +36 -30
  25. resqpy/olio/vector_utilities.py +340 -6
  26. resqpy/olio/volume.py +0 -20
  27. resqpy/olio/wellspec_keywords.py +19 -13
  28. resqpy/olio/write_hdf5.py +1 -1
  29. resqpy/olio/xml_et.py +12 -0
  30. resqpy/property/__init__.py +6 -4
  31. resqpy/property/_collection_add_part.py +4 -3
  32. resqpy/property/_collection_create_xml.py +4 -2
  33. resqpy/property/_collection_get_attributes.py +4 -0
  34. resqpy/property/attribute_property_set.py +311 -0
  35. resqpy/property/grid_property_collection.py +11 -11
  36. resqpy/property/property_collection.py +79 -31
  37. resqpy/property/property_common.py +3 -8
  38. resqpy/rq_import/_add_surfaces.py +34 -14
  39. resqpy/rq_import/_grid_from_cp.py +2 -2
  40. resqpy/rq_import/_import_nexus.py +75 -48
  41. resqpy/rq_import/_import_vdb_all_grids.py +64 -52
  42. resqpy/rq_import/_import_vdb_ensemble.py +12 -13
  43. resqpy/surface/_mesh.py +4 -0
  44. resqpy/surface/_surface.py +593 -118
  45. resqpy/surface/_tri_mesh.py +22 -12
  46. resqpy/surface/_tri_mesh_stencil.py +4 -4
  47. resqpy/surface/_triangulated_patch.py +71 -51
  48. resqpy/time_series/_any_time_series.py +7 -4
  49. resqpy/time_series/_geologic_time_series.py +1 -1
  50. resqpy/unstructured/_hexa_grid.py +6 -2
  51. resqpy/unstructured/_prism_grid.py +13 -5
  52. resqpy/unstructured/_pyramid_grid.py +6 -2
  53. resqpy/unstructured/_tetra_grid.py +6 -2
  54. resqpy/unstructured/_unstructured_grid.py +6 -2
  55. resqpy/well/_blocked_well.py +1986 -1946
  56. resqpy/well/_deviation_survey.py +3 -3
  57. resqpy/well/_md_datum.py +11 -21
  58. resqpy/well/_trajectory.py +10 -5
  59. resqpy/well/_wellbore_frame.py +10 -2
  60. resqpy/well/blocked_well_frame.py +3 -3
  61. resqpy/well/well_object_funcs.py +7 -9
  62. resqpy/well/well_utils.py +33 -0
  63. {resqpy-4.14.1.dist-info → resqpy-5.1.5.dist-info}/METADATA +8 -9
  64. {resqpy-4.14.1.dist-info → resqpy-5.1.5.dist-info}/RECORD +66 -66
  65. {resqpy-4.14.1.dist-info → resqpy-5.1.5.dist-info}/WHEEL +1 -1
  66. resqpy/grid/_moved_functions.py +0 -15
  67. {resqpy-4.14.1.dist-info → resqpy-5.1.5.dist-info}/LICENSE +0 -0
@@ -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