resqpy 4.14.2__py3-none-any.whl → 5.1.6__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 +8 -2
  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 +1413 -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.6.dist-info}/METADATA +8 -9
  64. {resqpy-4.14.2.dist-info → resqpy-5.1.6.dist-info}/RECORD +66 -66
  65. {resqpy-4.14.2.dist-info → resqpy-5.1.6.dist-info}/WHEEL +1 -1
  66. resqpy/grid/_moved_functions.py +0 -15
  67. {resqpy-4.14.2.dist-info → resqpy-5.1.6.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,536 @@ 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
+ # note: following is a grid cells property, not a gcs property
1171
+ bisector = None
1172
+ if return_bisector:
1173
+ if is_curtain and not patchwork:
1174
+ log.debug(f'preparing columns bisector for: {surface.title}')
1175
+ if j_faces_kji0 is None:
1176
+ j_faces_ji0 = np.empty((0, 2), dtype = np.int32)
1177
+ else:
1178
+ j_faces_ji0 = j_faces_kji0[:, 1:]
1179
+ if i_faces_kji0 is None:
1180
+ i_faces_ji0 = np.empty((0, 2), dtype = np.int32)
1181
+ else:
1182
+ i_faces_ji0 = i_faces_kji0[:, 1:]
1183
+ bisector = column_bisector_from_face_indices((grid.nj, grid.ni), j_faces_ji0, i_faces_ji0)
1184
+ # log.debug('finished preparing columns bisector')
1185
+ elif patchwork:
1186
+ # NB. following assumes faces have been added to gcs in a particular order!
1187
+ all_tris = None
1188
+ assert return_triangles
1189
+ # log.debug('preparing triangles array')
1190
+ k_triangles = np.empty((0,), dtype = np.int32) if k_props is None else k_props[0]
1191
+ j_triangles = np.empty((0,), dtype = np.int32) if j_props is None else j_props[0]
1192
+ i_triangles = np.empty((0,), dtype = np.int32) if i_props is None else i_props[0]
1193
+ all_tris = np.concatenate((k_triangles, j_triangles, i_triangles), axis = 0)
1194
+ # log.debug(f'gcs count: {gcs.count}; all triangles shape: {all_tris.shape}')
1195
+ n_patches = surface.number_of_patches()
1196
+ log.info(f'preparing composite cells bisector for surface: {surface.title}; number of patches: {n_patches}')
1197
+ nkf = 0 if k_faces_kji0 is None else len(k_faces_kji0)
1198
+ njf = 0 if j_faces_kji0 is None else len(j_faces_kji0)
1199
+ nif = 0 if i_faces_kji0 is None else len(i_faces_kji0)
1200
+ # fetch patch indices for triangle hits
1201
+ assert all_tris is not None and len(all_tris) == nkf + njf + nif
1202
+ patch_indices_k = surface.patch_indices_for_triangle_indices(all_tris[:nkf])
1203
+ patch_indices_j = surface.patch_indices_for_triangle_indices(all_tris[nkf:nkf + njf])
1204
+ patch_indices_i = surface.patch_indices_for_triangle_indices(all_tris[nkf + njf:])
1205
+ # add extra dimension to bisector array (at axis 0) for patches
1206
+ pb_shape = tuple([n_patches] + list(grid.extent_kji))
1207
+ if packed_bisectors:
1208
+ bisector = np.invert(np.zeros(_shape_packed(grid.extent_kji), dtype = np.uint8), dtype = np.uint8)
1209
+ else:
1210
+ bisector = np.ones(tuple(grid.extent_kji), dtype = np.bool_)
1211
+ # populate composite bisector
1212
+ for patch in range(n_patches):
1213
+ log.debug(f'processing patch {patch} of surface: {surface.title}')
1214
+ mask = (patch_indices == patch)
1215
+ mask_count = np.count_nonzero(mask)
1216
+ if mask_count == 0:
1217
+ log.warning(f'patch {patch} of surface {surface.title} is not applicable to any cells in grid')
1218
+ continue
1219
+ patch_box, box_count = get_box(mask)
1220
+ assert box_count == mask_count
1221
+ assert np.all(patch_box[1] > patch_box[0])
1222
+ patch_box = expanded_box(patch_box, tuple(grid.extent_kji))
1223
+ patch_box[0, 0] = 0
1224
+ patch_box[1, 0] = grid.extent_kji[0]
1225
+ packed_box = shrunk_box_for_packing(patch_box)
1226
+ patch_k_faces_kji0 = None
1227
+ if k_faces_kji0 is not None:
1228
+ patch_k_faces_kji0 = k_faces_kji0[(patch_indices_k == patch).astype(bool)]
1229
+ patch_j_faces_kji0 = None
1230
+ if j_faces_kji0 is not None:
1231
+ patch_j_faces_kji0 = j_faces_kji0[(patch_indices_j == patch).astype(bool)]
1232
+ patch_i_faces_kji0 = None
1233
+ if i_faces_kji0 is not None:
1234
+ patch_i_faces_kji0 = i_faces_kji0[(patch_indices_i == patch).astype(bool)]
1235
+ if packed_bisectors:
1236
+ mask = np.packbits(mask, axis = -1)
1237
+ patch_bisector, is_curtain = \
1238
+ packed_bisector_from_face_indices(tuple(grid.extent_kji),
1239
+ patch_k_faces_kji0,
1240
+ patch_j_faces_kji0,
1241
+ patch_i_faces_kji0,
1242
+ raw_bisector,
1243
+ patch_box)
1244
+ # bisector[:] = np.bitwise_or(np.bitwise_and(mask, patch_bisector),
1245
+ #  np.bitwise_and(np.invert(mask, dtype = np.uint8), bisector))
1246
+ _set_packed_where_mask(bisector, mask, patch_bisector, packed_box)
1247
+ else:
1248
+ patch_bisector, is_curtain = \
1249
+ bisector_from_face_indices(tuple(grid.extent_kji),
1250
+ patch_k_faces_kji0,
1251
+ patch_j_faces_kji0,
1252
+ patch_i_faces_kji0,
1253
+ raw_bisector,
1254
+ patch_box)
1255
+ bisector[mask] = patch_bisector[mask]
1256
+ if is_curtain:
1257
+ # TODO: downgrade following to debug once downstream functionality tested
1258
+ log.warning(f'ignoring curtain nature of bisector for patch {patch} of surface: {surface.title}')
1259
+ is_curtain = False
1260
+ else:
1261
+ log.info(f'preparing singlular cells bisector for surface: {surface.title}') # could downgrade to debug
1262
+ if ((k_faces_kji0 is None or len(k_faces_kji0) == 0) and
1263
+ (j_faces_kji0 is None or len(j_faces_kji0) == 0) and (i_faces_kji0 is None or len(i_faces_kji0) == 0)):
1264
+ bisector = np.ones((grid.nj, grid.ni), dtype = bool)
1265
+ is_curtain = True
1266
+ elif packed_bisectors:
1267
+ bisector, is_curtain = packed_bisector_from_face_indices(tuple(grid.extent_kji), k_faces_kji0,
1268
+ j_faces_kji0, i_faces_kji0, raw_bisector, None)
1269
+ if is_curtain:
1270
+ bisector = np.unpackbits(bisector[0], axis = -1,
1271
+ count = grid.ni).astype(bool) # reduce to a columns property
1272
+ else:
1273
+ bisector, is_curtain = bisector_from_face_indices(tuple(grid.extent_kji), k_faces_kji0, j_faces_kji0,
1274
+ i_faces_kji0, raw_bisector, None)
1275
+ if is_curtain:
1276
+ bisector = bisector[0] # reduce to a columns property
1277
+
1278
+ # if using patchwork, filter all faces (and properties) to those within (or on boundary of) volume corresponding to patch
1279
+ if patchwork:
1280
+ if k_faces_kji0 is not None:
1281
+ selection = filter_faces(k_faces_kji0, patch_indices_k, patch_indices, 0)
1282
+ if np.any(selection):
1283
+ k_faces_kji0 = k_faces_kji0[selection]
1284
+ if k_props is not None:
1285
+ k_props = [prop[selection] for prop in k_props]
1286
+ else:
1287
+ k_faces_kji0 = None
1288
+ k_props = None
1289
+ if j_faces_kji0 is not None:
1290
+ selection = filter_faces(j_faces_kji0, patch_indices_j, patch_indices, 1)
1291
+ if np.any(selection):
1292
+ j_faces_kji0 = j_faces_kji0[selection]
1293
+ if j_props is not None:
1294
+ j_props = [prop[selection] for prop in j_props]
1295
+ else:
1296
+ j_faces_kji0 = None
1297
+ j_props = None
1298
+ if i_faces_kji0 is not None:
1299
+ selection = filter_faces(i_faces_kji0, patch_indices_i, patch_indices, 2)
1300
+ if np.any(selection):
1301
+ i_faces_kji0 = i_faces_kji0[selection]
1302
+ if i_props is not None:
1303
+ i_props = [prop[selection] for prop in i_props]
1304
+ else:
1305
+ i_faces_kji0 = None
1306
+ i_props = None
1307
+
1308
+ log.debug("converting face sets into grid connection set")
1309
+ # NB: kji0 arrays in internal face protocol: used as cell_kji0 with polarity of 1
1310
+ # property lists have elements replaced with sorted and filtered equivalents
1311
+ gcs = rqf.GridConnectionSet.from_faces_indices(grid = grid,
1312
+ k_faces_kji0 = k_faces_kji0,
1313
+ j_faces_kji0 = j_faces_kji0,
1314
+ i_faces_kji0 = i_faces_kji0,
1315
+ remove_duplicates = not patchwork,
1316
+ k_properties = k_props,
1317
+ j_properties = j_props,
1318
+ i_properties = i_props,
1319
+ feature_name = name,
1320
+ feature_type = feature_type,
1321
+ create_organizing_objects_where_needed = True,
1322
+ title = title)
1323
+ # log.debug('finished coversion to gcs')
1324
+
1325
+ # NB. following assumes faces have been added to gcs in a particular order!
1326
+ all_tris = None
1327
+ if return_triangles:
1328
+ # log.debug('preparing triangles array')
1329
+ k_triangles = np.empty((0,), dtype = np.int32) if k_props is None else k_props.pop(0)
1330
+ j_triangles = np.empty((0,), dtype = np.int32) if j_props is None else j_props.pop(0)
1331
+ i_triangles = np.empty((0,), dtype = np.int32) if i_props is None else i_props.pop(0)
1332
+ all_tris = np.concatenate((k_triangles, j_triangles, i_triangles), axis = 0)
1333
+ # log.debug(f'gcs count: {gcs.count}; all triangles shape: {all_tris.shape}')
1334
+ assert all_tris.shape == (gcs.count,)
1335
+
1336
+ # NB. following assumes faces have been added to gcs in a particular order!
1337
+ all_depths = None
1338
+ if return_depths:
1339
+ # log.debug('preparing depths array')
1340
+ k_depths = np.empty((0,), dtype = np.float64) if k_props is None else k_props.pop(0)
1341
+ j_depths = np.empty((0,), dtype = np.float64) if j_props is None else j_props.pop(0)
1342
+ i_depths = np.empty((0,), dtype = np.float64) if i_props is None else i_props.pop(0)
1343
+ all_depths = np.concatenate((k_depths, j_depths, i_depths), axis = 0)
1344
+ # log.debug(f'gcs count: {gcs.count}; all depths shape: {all_depths.shape}')
1345
+ assert all_depths.shape == (gcs.count,)
1346
+
1347
+ # NB. following assumes faces have been added to gcs in a particular order!
1348
+ all_offsets = None
1349
+ if return_offsets:
1350
+ # log.debug('preparing offsets array')
1351
+ k_offsets = np.empty((0,), dtype = np.float64) if k_props is None else k_props[0]
1352
+ j_offsets = np.empty((0,), dtype = np.float64) if j_props is None else j_props[0]
1353
+ i_offsets = np.empty((0,), dtype = np.float64) if i_props is None else i_props[0]
1354
+ all_offsets = _all_offsets(grid.crs, k_offsets, j_offsets, i_offsets)
1355
+ # log.debug(f'gcs count: {gcs.count}; all offsets shape: {all_offsets.shape}')
1356
+ assert all_offsets.shape == (gcs.count,)
1357
+
1358
+ all_flange = None
1359
+ if return_flange_bool:
1360
+ # log.debug('preparing flange array')
1361
+ flange_bool_uuid = surface.model.uuid(title = "flange bool",
1362
+ obj_type = "DiscreteProperty",
1363
+ related_uuid = surface.uuid)
1364
+ assert (flange_bool_uuid is not None), f"No flange bool property found for surface: {surface.title}"
1365
+ flange_bool = rqp.Property(surface.model, uuid = flange_bool_uuid)
1366
+ flange_array = flange_bool.array_ref(dtype = bool)
1367
+ all_flange = np.take(flange_array, all_tris)
1368
+ assert all_flange.shape == (gcs.count,)
1369
+
1370
+ # note: following is a grid cells property, not a gcs property
1371
+ shadow = None
1372
+ if return_shadow:
1373
+ log.debug("preparing cells shadow")
1374
+ shadow = shadow_from_face_indices(tuple(grid.extent_kji), k_faces_kji0)
1375
+
1376
+ if progress_fn is not None:
1377
+ progress_fn(1.0)
1378
+
1379
+ log.debug(f"finishing find_faces_to_represent_surface_regular_optimised for {name}")
1380
+
1381
+ # if returning properties, construct dictionary
1382
+ if return_properties:
1383
+ props_dict = {}
1384
+ if 'triangle' in return_properties:
1385
+ props_dict["triangle"] = all_tris
1386
+ if return_depths:
1387
+ props_dict["depth"] = all_depths
1388
+ if return_offsets:
1389
+ props_dict["offset"] = all_offsets
1390
+ if return_bisector:
1391
+ props_dict["grid bisector"] = (bisector, is_curtain)
1392
+ if return_shadow:
1393
+ props_dict["grid shadow"] = shadow
1394
+ if return_flange_bool:
1395
+ props_dict["flange bool"] = all_flange
1396
+ return (gcs, props_dict)
1397
+
1398
+ return gcs
1399
+
1400
+
861
1401
  def find_faces_to_represent_surface(grid, surface, name, mode = "auto", feature_type = "fault", progress_fn = None):
862
1402
  """Returns a grid connection set containing those cell faces which are deemed to represent the surface.
863
1403
 
@@ -885,19 +1425,7 @@ def find_faces_to_represent_surface(grid, surface, name, mode = "auto", feature_
885
1425
  mode = "regular_optimised"
886
1426
  else:
887
1427
  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":
1428
+ if mode == "regular_optimised":
901
1429
  return find_faces_to_represent_surface_regular_optimised(grid,
902
1430
  surface,
903
1431
  name,
@@ -911,161 +1439,331 @@ def find_faces_to_represent_surface(grid, surface, name, mode = "auto", feature_
911
1439
  name,
912
1440
  feature_type = feature_type,
913
1441
  progress_fn = progress_fn)
1442
+ elif mode == "staffa":
1443
+ return find_faces_to_represent_surface_staffa(grid,
1444
+ surface,
1445
+ name,
1446
+ feature_type = feature_type,
1447
+ progress_fn = progress_fn)
1448
+ elif mode == "regular_dense":
1449
+ return find_faces_to_represent_surface_regular_dense_optimised(grid,
1450
+ surface,
1451
+ name,
1452
+ feature_type = feature_type,
1453
+ progress_fn = progress_fn)
1454
+ elif mode == "regular":
1455
+ return find_faces_to_represent_surface_regular(grid,
1456
+ surface,
1457
+ name,
1458
+ feature_type = feature_type,
1459
+ progress_fn = progress_fn)
914
1460
  log.critical("unrecognised mode: " + str(mode))
915
1461
  return None
916
1462
 
917
1463
 
918
1464
  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]:
1465
+ grid_extent_kji: Tuple[int, int, int], k_faces: Union[np.ndarray, None], j_faces: Union[np.ndarray, None],
1466
+ i_faces: Union[np.ndarray, None], raw_bisector: bool) -> Tuple[np.ndarray, bool]:
925
1467
  """Creates a boolean array denoting the bisection of the grid by the face sets.
926
1468
 
927
1469
  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.
1470
+ - grid_extent_kji (Tuple[int, int, int]): the shape of the grid
1471
+ - k_faces (np.ndarray): a boolean array of which faces represent the surface in the k dimension
1472
+ - j_faces (np.ndarray): a boolean array of which faces represent the surface in the j dimension
1473
+ - i_faces (np.ndarray): a boolean array of which faces represent the surface in the i dimension
1474
+ - raw_bisector (bool): if True, the bisector is returned without determining which side is shallower
932
1475
 
933
1476
  returns:
934
1477
  Tuple containing:
1478
+ - array (np.ndarray): boolean bisectors array where values are True for cells on the side
1479
+ of the surface that has a lower mean k index on average and False for cells on the other side.
1480
+ - is_curtain (bool): True if the surface is a curtain (vertical), otherwise False.
1481
+
1482
+ notes:
1483
+ - the face sets must form a single 'sealed' cut of the grid (eg. not waving in and out of the grid)
1484
+ - any 'boxed in' parts of the grid (completely enclosed by bisecting faces) will be consistently
1485
+ assigned to either the True or False part
1486
+ - this function is DEPRECATED, use newer indices based approach instead: bisector_from_face_indices()
1487
+ """
1488
+ warnings.warn('DEPRECATED: grid_surface.bisector_from_faces() function; use bisector_from_face_indices() instead')
1489
+ assert len(grid_extent_kji) == 3
1490
+
1491
+ # find the surface boundary (includes a buffer slice where surface does not reach edge of grid)
1492
+ box = get_boundary(k_faces, j_faces, i_faces, grid_extent_kji)
1493
+ box_shape = box[1, :] - box[0, :]
1494
+
1495
+ # set up the bisector array for the bounding box
1496
+ box_array = np.zeros(box_shape, dtype = np.bool_)
1497
+
1498
+ # seed the bisector box array at (0, 0, 0)
1499
+ box_array[0, 0, 0] = True
1500
+
1501
+ # prepare to spread True values to neighbouring cells that are not the other side of a face
1502
+ if k_faces is None:
1503
+ open_k = np.ones((box_shape[0] - 1, box_shape[1], box_shape[2]), dtype = bool)
1504
+ else:
1505
+ k_faces = k_faces[box[0, 0]:box[1, 0] - 1, box[0, 1]:box[1, 1], box[0, 2]:box[1, 2]]
1506
+ open_k = np.logical_not(k_faces)
1507
+ if j_faces is None:
1508
+ open_j = np.ones((box_shape[0], box_shape[1] - 1, box_shape[2]), dtype = bool)
1509
+ else:
1510
+ j_faces = j_faces[box[0, 0]:box[1, 0], box[0, 1]:box[1, 1] - 1, box[0, 2]:box[1, 2]]
1511
+ open_j = np.logical_not(j_faces)
1512
+ if i_faces is None:
1513
+ open_i = np.ones((box_shape[0], box_shape[1], box_shape[2] - 1), dtype = bool)
1514
+ else:
1515
+ i_faces = i_faces[box[0, 0]:box[1, 0], box[0, 1]:box[1, 1], box[0, 2]:box[1, 2] - 1]
1516
+ open_i = np.logical_not(i_faces)
1517
+
1518
+ # populate bisector array for box
1519
+ _fill_bisector(box_array, open_k, open_j, open_i)
1520
+
1521
+ # set up the full bisectors array and assigning the bounding box values
1522
+ array = np.zeros(grid_extent_kji, dtype = np.bool_)
1523
+ array[box[0, 0]:box[1, 0], box[0, 1]:box[1, 1], box[0, 2]:box[1, 2]] = box_array
1524
+
1525
+ # set bisector values outside of the bounding box
1526
+ _set_bisector_outside_box(array, box, box_array)
1527
+
1528
+ # check all array elements are not the same
1529
+ true_count = np.count_nonzero(array)
1530
+ cell_count = array.size
1531
+ if 0 < true_count < cell_count:
1532
+ # negate the array if it minimises the mean k and determine if the surface is a curtain
1533
+ is_curtain = _shallow_or_curtain(array, true_count, raw_bisector)
1534
+ else:
1535
+ assert raw_bisector, 'face set for surface is leaky or empty (surface does not intersect grid)'
1536
+ log.error('face set for surface is leaky or empty (surface does not intersect grid)')
1537
+ is_curtain = False
1538
+
1539
+ return array, is_curtain
1540
+
1541
+
1542
+ # yapf: disable
1543
+ def bisector_from_face_indices( # type: ignore
1544
+ grid_extent_kji: Tuple[int, int, int],
1545
+ k_faces_kji0: Union[np.ndarray, None],
1546
+ j_faces_kji0: Union[np.ndarray, None],
1547
+ i_faces_kji0: Union[np.ndarray, None],
1548
+ raw_bisector: bool,
1549
+ p_box: Union[np.ndarray, None]) -> Tuple[np.ndarray, bool]:
1550
+ # yapf: enable
1551
+ """Creates a boolean array denoting the bisection of the grid by the face sets.
935
1552
 
1553
+ arguments:
1554
+ - grid_extent_kji (Tuple[int, int, int]): the shape of the grid
1555
+ - k_faces_kji0 (np.ndarray): an int array of indices of which faces represent the surface in the k dimension
1556
+ - j_faces_kji0 (np.ndarray): an int array of indices of which faces represent the surface in the j dimension
1557
+ - i_faces_kji0 (np.ndarray): an int array of indices of which faces represent the surface in the i dimension
1558
+ - raw_bisector (bool): if True, the bisector is returned without determining which side is shallower
1559
+ - p_box (np.ndarray): a python protocol box to limit the bisector evaluation over
1560
+
1561
+ returns:
1562
+ Tuple containing:
936
1563
  - 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.
1564
+ of the surface that has a lower mean k index on average and False for cells on the other side.
938
1565
  - is_curtain (bool): True if the surface is a curtain (vertical), otherwise False.
939
1566
 
940
1567
  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.
1568
+ - the face sets must form a single 'sealed' cut of the grid (eg. not waving in and out of the grid)
1569
+ - any 'boxed in' parts of the grid (completely enclosed by bisecting faces) will be consistently
1570
+ assigned to either the True or False part
944
1571
  """
945
1572
  assert len(grid_extent_kji) == 3
946
1573
 
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
- )
1574
+ # find the surface boundary (includes a buffer slice where surface does not reach edge of grid)
1575
+ face_box = get_boundary_from_indices(k_faces_kji0, j_faces_kji0, i_faces_kji0, grid_extent_kji)
1576
+ box = np.empty((2, 3), dtype = np.int32)
1577
+ if p_box is None:
1578
+ box[:] = face_box
1579
+ else:
1580
+ box[:] = box_intersection(p_box, face_box)
1581
+ if np.all(box == 0):
1582
+ box[:] = face_box
1583
+ # set k_faces as bool arrays covering box
1584
+ k_faces, j_faces, i_faces = _box_face_arrays_from_indices(k_faces_kji0, j_faces_kji0, i_faces_kji0, box)
959
1585
 
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))
1586
+ box_shape = box[1, :] - box[0, :]
1007
1587
 
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))
1588
+ # set up the bisector array for the bounding box
1589
+ box_array = np.zeros(box_shape, dtype = np.bool_)
1013
1590
 
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))
1591
+ # seed the bisector box array at (0, 0, 0)
1592
+ box_array[0, 0, 0] = True
1019
1593
 
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)
1594
+ # prepare to spread True values to neighbouring cells that are not the other side of a face
1595
+ if k_faces is None:
1596
+ open_k = np.ones((box_shape[0] - 1, box_shape[1], box_shape[2]), dtype = bool)
1597
+ else:
1598
+ open_k = np.logical_not(k_faces)
1599
+ if j_faces is None:
1600
+ open_j = np.ones((box_shape[0], box_shape[1] - 1, box_shape[2]), dtype = bool)
1601
+ else:
1602
+ open_j = np.logical_not(j_faces)
1603
+ if i_faces is None:
1604
+ open_i = np.ones((box_shape[0], box_shape[1], box_shape[2] - 1), dtype = bool)
1605
+ else:
1606
+ open_i = np.logical_not(i_faces)
1607
+
1608
+ # populate bisector array for box
1609
+ _fill_bisector(box_array, open_k, open_j, open_i)
1024
1610
 
1025
- # Setting up the full bisectors array and assigning the bounding box values.
1611
+ # set up the full bisectors array and assigning the bounding box values
1026
1612
  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.
1613
+ array[box[0, 0]:box[1, 0], box[0, 1]:box[1, 1], box[0, 2]:box[1, 2]] = box_array
1614
+
1615
+ # set bisector values outside of the bounding box
1616
+ _set_bisector_outside_box(array, box, box_array)
1617
+
1618
+ # check all array elements are not the same
1045
1619
  true_count = np.count_nonzero(array)
1046
1620
  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
1621
+ if 0 < true_count < cell_count:
1622
+ # negate the array if it minimises the mean k and determine if the surface is a curtain
1623
+ is_curtain = _shallow_or_curtain(array, true_count, raw_bisector)
1624
+ else:
1625
+ assert raw_bisector, 'face set for surface is leaky or empty (surface does not intersect grid)'
1626
+ log.error('face set for surface is leaky or empty (surface does not intersect grid)')
1627
+ is_curtain = False
1065
1628
 
1066
1629
  return array, is_curtain
1067
1630
 
1068
1631
 
1632
+ # yapf: disable
1633
+ def packed_bisector_from_face_indices( # type: ignore
1634
+ grid_extent_kji: Tuple[int, int, int],
1635
+ k_faces_kji0: Union[np.ndarray, None],
1636
+ j_faces_kji0: Union[np.ndarray, None],
1637
+ i_faces_kji0: Union[np.ndarray, None],
1638
+ raw_bisector: bool,
1639
+ p_box: Union[np.ndarray, None]) -> Tuple[np.ndarray, bool]:
1640
+ # yapf: enable
1641
+ """Creates a uint8 (packed bool) array denoting the bisection of the grid by the face sets.
1642
+
1643
+ arguments:
1644
+ - grid_extent_kji (Tuple[int, int, int]): the shape of the grid
1645
+ - k_faces_kji0 (np.ndarray): an int array of indices of which faces represent the surface in the k dimension
1646
+ - j_faces_kji0 (np.ndarray): an int array of indices of which faces represent the surface in the j dimension
1647
+ - i_faces_kji0 (np.ndarray): an int array of indices of which faces represent the surface in the i dimension
1648
+ - raw_bisector (bool): if True, the bisector is returned without determining which side is shallower
1649
+ - p_box (np.ndarray): a python protocol unshrunken box to limit the bisector evaluation over
1650
+
1651
+ returns:
1652
+ Tuple containing:
1653
+ - array (np.uint8 array): packed boolean bisector array where values are 1 for cells on the side
1654
+ of the surface that has a lower mean k index on average and 0 for cells on the other side
1655
+ - is_curtain (bool): True if the surface is a curtain (vertical), otherwise False
1656
+
1657
+ notes:
1658
+ - the face sets must form a single 'sealed' cut of the grid (eg. not waving in and out of the grid)
1659
+ - any 'boxed in' parts of the grid (completely enclosed by bisecting faces) will be consistently
1660
+ assigned to either the True or False part
1661
+ - the returned array is packed in the I axis; use np.unpackbits() to unpack
1662
+ """
1663
+ assert len(grid_extent_kji) == 3
1664
+
1665
+ # find the surface boundary (includes a buffer slice where surface does not reach edge of grid), and shrink the I axis
1666
+ face_box = get_packed_boundary_from_indices(k_faces_kji0, j_faces_kji0, i_faces_kji0, grid_extent_kji)
1667
+ box = np.empty((2, 3), dtype = np.int32)
1668
+ if p_box is None:
1669
+ box[:] = face_box
1670
+ else:
1671
+ box[:] = box_intersection(shrunk_box_for_packing(p_box), face_box)
1672
+ if np.all(box == 0):
1673
+ box[:] = face_box
1674
+
1675
+ # set k_faces, j_faces & i_faces as uint8 packed bool arrays covering box
1676
+ k_faces, j_faces, i_faces = _packed_box_face_arrays_from_indices(k_faces_kji0, j_faces_kji0, i_faces_kji0, box)
1677
+
1678
+ box_shape = box[1, :] - box[0, :]
1679
+
1680
+ # set up the bisector array for the bounding box
1681
+ box_array = np.zeros(box_shape, dtype = np.uint8)
1682
+
1683
+ # seed the bisector box array at (0, 0, 0)
1684
+ box_array[0, 0, 0] = 0x80 # first bit only set
1685
+
1686
+ # prepare to spread True values to neighbouring cells that are not the other side of a face
1687
+ if k_faces is None:
1688
+ open_k = np.invert(np.zeros((box_shape[0] - 1, box_shape[1], box_shape[2]), dtype = np.uint8), dtype = np.uint8)
1689
+ else:
1690
+ open_k = np.invert(k_faces, dtype = np.uint8)
1691
+ if j_faces is None:
1692
+ open_j = np.invert(np.zeros((box_shape[0], box_shape[1] - 1, box_shape[2]), dtype = np.uint8), dtype = np.uint8)
1693
+ else:
1694
+ open_j = np.invert(j_faces, dtype = np.uint8)
1695
+ if i_faces is None:
1696
+ open_i = np.invert(np.zeros(tuple(box_shape), dtype = np.uint8), dtype = np.uint8)
1697
+ else:
1698
+ open_i = np.invert(i_faces, dtype = np.uint8)
1699
+
1700
+ # close off faces in padding bits, if within box
1701
+ if box[1, 2] * 8 > grid_extent_kji[2]:
1702
+ tail = grid_extent_kji[2] % 8 # number of valid bits in padded byte
1703
+ assert tail
1704
+ m = np.uint8((255 << (8 - tail)) & 255)
1705
+ open_k[:, :, -1] &= m
1706
+ open_j[:, :, -1] &= m
1707
+ m = np.uint8((m << 1) & 255)
1708
+ open_i[:, :, -1] &= m
1709
+
1710
+ # populate bisector array for box
1711
+ _fill_packed_bisector(box_array, open_k, open_j, open_i)
1712
+
1713
+ del open_i, open_j, open_k
1714
+
1715
+ # set up the full bisectors array and assigning the bounding box values
1716
+ p_array = np.zeros(_shape_packed(grid_extent_kji), dtype = np.uint8)
1717
+ p_array[box[0, 0]:box[1, 0], box[0, 1]:box[1, 1], box[0, 2]:box[1, 2]] = box_array
1718
+
1719
+ # set bisector values outside of the bounding box
1720
+ _set_packed_bisector_outside_box(p_array, box, box_array, grid_extent_kji[2] % 8)
1721
+
1722
+ # check all array elements are not the same
1723
+ if hasattr(np, 'bitwise_count'):
1724
+ true_count = np.sum(np.bitwise_count(p_array))
1725
+ else:
1726
+ true_count = _bitwise_count_njit(p_array) # note: will usually include some padding bits, so not so true!
1727
+ cell_count = np.prod(grid_extent_kji)
1728
+ if 0 < true_count < cell_count:
1729
+ # negate the array if it minimises the mean k and determine if the surface is a curtain
1730
+ # TODO: switch to _packed_shallow_or_curtain() when numba supports np.bitwise_count()
1731
+ is_curtain = _packed_shallow_or_curtain_temp_bitwise_count(p_array, true_count, raw_bisector)
1732
+ else:
1733
+ assert raw_bisector, 'face set for surface is leaky or empty (surface does not intersect grid)'
1734
+ log.error('face set for surface is leaky or empty (surface does not intersect grid)')
1735
+ is_curtain = False
1736
+
1737
+ return p_array, is_curtain
1738
+
1739
+
1740
+ def column_bisector_from_face_indices(grid_extent_ji: Tuple[int, int], j_faces_ji0: np.ndarray,
1741
+ i_faces_ji0: np.ndarray) -> np.ndarray:
1742
+ """Returns a numpy bool array denoting the bisection of the top layer of the grid by the curtain face sets.
1743
+
1744
+ arguments:
1745
+ - grid_extent_ji (pair of int): the shape of a layer of the grid
1746
+ - j_faces_ji0, i_faces_ji0 (2D numpy int arrays of shape (N, 2)): indices of faces within a layer
1747
+
1748
+ returns:
1749
+ numpy bool array of shape grid_extent_ji, set True for cells on one side of the face sets;
1750
+ set False for cells on othe side
1751
+
1752
+ notes:
1753
+ - the face sets must form a single 'sealed' cut of the grid (eg. not waving in and out of the grid)
1754
+ - any 'boxed in' parts of the grid (completely enclosed by bisecting faces) will be consistently
1755
+ assigned to the False part
1756
+ - the resulting array is suitable for use as a grid property with indexable element of columns
1757
+ - the array is set True for the side of the curtain that contains cell [0, 0]
1758
+ """
1759
+ assert len(grid_extent_ji) == 2
1760
+ j_faces = np.zeros((grid_extent_ji[0] - 1, grid_extent_ji[1]), dtype = np.bool_)
1761
+ i_faces = np.zeros((grid_extent_ji[0], grid_extent_ji[1] - 1), dtype = np.bool_)
1762
+ j_faces[j_faces_ji0[:, 0], j_faces_ji0[:, 1]] = True
1763
+ i_faces[i_faces_ji0[:, 0], i_faces_ji0[:, 1]] = True
1764
+ return column_bisector_from_faces(grid_extent_ji, j_faces, i_faces)
1765
+
1766
+
1069
1767
  def column_bisector_from_faces(grid_extent_ji: Tuple[int, int], j_faces: np.ndarray, i_faces: np.ndarray) -> np.ndarray:
1070
1768
  """Returns a numpy bool array denoting the bisection of the top layer of the grid by the curtain face sets.
1071
1769
 
@@ -1116,6 +1814,42 @@ def column_bisector_from_faces(grid_extent_ji: Tuple[int, int], j_faces: np.ndar
1116
1814
  return a
1117
1815
 
1118
1816
 
1817
+ def shadow_from_face_indices(extent_kji, kji0):
1818
+ """Returns a numpy int8 array indicating whether cells are above, below or between K faces.
1819
+
1820
+ arguments:
1821
+ extent_kji (triple int): the shape of the grid
1822
+ kji0 (numpy int array of shape (N, 3)): indices where a K face is present
1823
+
1824
+ returns:
1825
+ numpy int8 array of shape extent_kji; values are: 0 neither above nor below a K face;
1826
+ 1: above any K faces in the column; 2 below any K faces in the column;
1827
+ 3: between K faces (one or more above and one or more below)
1828
+ """
1829
+ assert len(extent_kji) == 3
1830
+ limit = extent_kji[0] - 1 # maximum number of iterations needed to spead shadow
1831
+ shadow = np.zeros(extent_kji, dtype = np.int8)
1832
+ shadow[kji0[:, 0], kji0[:, 1], kji0[:, 2]] = 1
1833
+ shadow[kji0[:, 0] + 1, kji0[:, 1], kji0[:, 2]] += 2
1834
+ for _ in range(limit):
1835
+ c = np.logical_and(shadow[:-1] == 0, shadow[1:] == 1)
1836
+ if np.count_nonzero(c) == 0:
1837
+ break
1838
+ shadow[:-1][c] = 1
1839
+ for _ in range(limit):
1840
+ c = np.logical_and(shadow[1:] == 0, shadow[:-1] == 2)
1841
+ if np.count_nonzero(c) == 0:
1842
+ break
1843
+ shadow[1:][c] = 2
1844
+ for _ in range(limit):
1845
+ c = np.logical_and(shadow[:-1] >= 2, shadow[1:] == 1)
1846
+ if np.count_nonzero(c) == 0:
1847
+ break
1848
+ shadow[:-1][c] = 3
1849
+ shadow[1:][c] = 3
1850
+ return shadow
1851
+
1852
+
1119
1853
  def shadow_from_faces(extent_kji, k_faces):
1120
1854
  """Returns a numpy int8 array indicating whether cells are above, below or between K faces.
1121
1855
 
@@ -1137,28 +1871,28 @@ def shadow_from_faces(extent_kji, k_faces):
1137
1871
  c = np.logical_and(shadow[:-1] == 0, shadow[1:] == 1)
1138
1872
  if np.count_nonzero(c) == 0:
1139
1873
  break
1140
- shadow[:-1] = np.where(c, 1, shadow[:-1])
1874
+ shadow[:-1][c] = 1
1141
1875
  for _ in range(limit):
1142
1876
  c = np.logical_and(shadow[1:] == 0, shadow[:-1] == 2)
1143
1877
  if np.count_nonzero(c) == 0:
1144
1878
  break
1145
- shadow[1:] = np.where(c, 2, shadow[1:])
1879
+ shadow[1:][c] = 2
1146
1880
  for _ in range(limit):
1147
1881
  c = np.logical_and(shadow[:-1] >= 2, shadow[1:] == 1)
1148
1882
  if np.count_nonzero(c) == 0:
1149
1883
  break
1150
- shadow[:-1] = np.where(c, 3, shadow[:-1])
1151
- shadow[1:] = np.where(c, 3, shadow[1:])
1884
+ shadow[:-1][c] = 3
1885
+ shadow[1:][c] = 3
1152
1886
  return shadow
1153
1887
 
1154
1888
 
1155
1889
  def get_boundary( # type: ignore
1156
- k_faces: np.ndarray,
1157
- j_faces: np.ndarray,
1158
- i_faces: np.ndarray,
1890
+ k_faces: Union[np.ndarray, None],
1891
+ j_faces: Union[np.ndarray, None],
1892
+ i_faces: Union[np.ndarray, None],
1159
1893
  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).
1894
+ ) -> np.ndarray:
1895
+ """Cretaes a box of the indices that bound the surface (where the faces are True).
1162
1896
 
1163
1897
  arguments:
1164
1898
  k_faces (np.ndarray): a boolean array of which faces represent the surface in the k dimension
@@ -1167,26 +1901,22 @@ def get_boundary( # type: ignore
1167
1901
  grid_extent_kji (Tuple[int, int, int]): the shape of the grid
1168
1902
 
1169
1903
  returns:
1170
- boundary (Dict[str, int]): a dictionary of the indices that bound the surface
1904
+ int array of shape (2, 3): bounding box in python protocol (max values have been incremented)
1171
1905
 
1172
1906
  note:
1173
1907
  input faces arrays are for internal grid faces (ie. extent reduced by 1 in axis of faces);
1174
1908
  a buffer slice is included where the surface does not reach the edge of the grid
1175
1909
  """
1176
1910
 
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
- }
1911
+ boundary = np.zeros((2, 3), dtype = np.int32)
1185
1912
 
1186
1913
  starting = True
1187
1914
 
1188
1915
  for f_i, faces in enumerate([k_faces, j_faces, i_faces]):
1189
1916
 
1917
+ if faces is None:
1918
+ continue
1919
+
1190
1920
  # NB. k, j & i for rest of loop refer to indices of faces, regardless of which face set is being processed
1191
1921
 
1192
1922
  where_k, where_j, where_i = _where_true(faces)
@@ -1210,44 +1940,90 @@ def get_boundary( # type: ignore
1210
1940
  else:
1211
1941
  if min_k > 0:
1212
1942
  min_k -= 1
1213
- if max_k < j_faces.shape[0] - 1:
1943
+ if max_k < grid_extent_kji[0] - 1:
1214
1944
  max_k += 1
1215
1945
  if f_i == 1:
1216
1946
  max_j += 1
1217
1947
  else:
1218
1948
  if min_j > 0:
1219
1949
  min_j -= 1
1220
- if max_j < k_faces.shape[1] - 1:
1950
+ if max_j < grid_extent_kji[1] - 1:
1221
1951
  max_j += 1
1222
1952
  if f_i == 2:
1223
1953
  max_i += 1
1224
1954
  else:
1225
1955
  if min_i > 0:
1226
1956
  min_i -= 1
1227
- if max_i < k_faces.shape[2] - 1:
1957
+ if max_i < grid_extent_kji[2] - 1:
1228
1958
  max_i += 1
1229
1959
 
1230
1960
  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
1961
+ boundary[0, 0] = min_k
1962
+ boundary[1, 0] = max_k
1963
+ boundary[0, 1] = min_j
1964
+ boundary[1, 1] = max_j
1965
+ boundary[0, 2] = min_i
1966
+ boundary[1, 2] = max_i
1237
1967
  starting = False
1238
1968
  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
1969
+ if min_k < boundary[0, 0]:
1970
+ boundary[0, 0] = min_k
1971
+ if max_k > boundary[1, 0]:
1972
+ boundary[1, 0] = max_k
1973
+ if min_j < boundary[0, 1]:
1974
+ boundary[0, 1] = min_j
1975
+ if max_j > boundary[1, 1]:
1976
+ boundary[1, 1] = max_j
1977
+ if min_i < boundary[0, 2]:
1978
+ boundary[0, 2] = min_i
1979
+ if max_i > boundary[1, 2]:
1980
+ boundary[1, 2] = max_i
1981
+
1982
+ boundary[1, :] += 1 # increment max values to give python protocol box
1983
+
1984
+ return boundary # type: ignore
1985
+
1986
+
1987
+ def get_boundary_dict( # type: ignore
1988
+ k_faces: Union[np.ndarray, None],
1989
+ j_faces: Union[np.ndarray, None],
1990
+ i_faces: Union[np.ndarray, None],
1991
+ grid_extent_kji: Tuple[int, int, int],
1992
+ ) -> Dict[str, int]:
1993
+ """Cretaes a dictionary of the indices that bound the surface (where the faces are True).
1994
+
1995
+ arguments:
1996
+ k_faces (np.ndarray): a boolean array of which faces represent the surface in the k dimension
1997
+ j_faces (np.ndarray): a boolean array of which faces represent the surface in the j dimension
1998
+ i_faces (np.ndarray): a boolean array of which faces represent the surface in the i dimension
1999
+ grid_extent_kji (Tuple[int, int, int]): the shape of the grid
2000
+
2001
+ returns:
2002
+ boundary (Dict[str, int]): a dictionary of the indices that bound the surface
2003
+
2004
+ note:
2005
+ input faces arrays are for internal grid faces (ie. extent reduced by 1 in axis of faces);
2006
+ a buffer slice is included where the surface does not reach the edge of the grid;
2007
+ max values are not increment, ie. need to be incremented to be used as an upper end of a python range
2008
+ """
2009
+
2010
+ boundary = {
2011
+ "k_min": None,
2012
+ "k_max": None,
2013
+ "j_min": None,
2014
+ "j_max": None,
2015
+ "i_min": None,
2016
+ "i_max": None,
2017
+ }
2018
+
2019
+ box = get_boundary(k_faces, j_faces, i_faces, grid_extent_kji)
2020
+
2021
+ boundary["k_min"] = box[0, 0]
2022
+ boundary["k_max"] = box[1, 0] - 1
2023
+ boundary["j_min"] = box[0, 1]
2024
+ boundary["j_max"] = box[1, 1] - 1
2025
+ boundary["i_min"] = box[0, 2]
2026
+ boundary["i_max"] = box[1, 2] - 1
1251
2027
 
1252
2028
  return boundary # type: ignore
1253
2029
 
@@ -1259,7 +2035,7 @@ def _where_true(data: np.ndarray):
1259
2035
 
1260
2036
 
1261
2037
  @njit # pragma: no cover
1262
- def _first_true(array: np.ndarray) -> Optional[int]: # type: ignore
2038
+ def _first_true(array: np.ndarray) -> int: # type: ignore
1263
2039
  """Returns the index + 1 of the first True value in the array."""
1264
2040
  for idx, val in np.ndenumerate(array):
1265
2041
  if val:
@@ -1267,6 +2043,25 @@ def _first_true(array: np.ndarray) -> Optional[int]: # type: ignore
1267
2043
  return array.size
1268
2044
 
1269
2045
 
2046
+ @njit('(uint8[:,:,:], uint8[:,:,:], uint8[:,:,:], int32[:,:])', parallel = True) # pragma: no cover
2047
+ def _set_packed_where_mask(a: np.ndarray, mask: np.ndarray, v: np.ndarray, box: np.ndarray): # type: ignore
2048
+ """Update 3D packed boolean array a from packed boolean array v where packed boolean array mask is set."""
2049
+ assert a.ndim == 3 and a.shape == mask.shape and a.shape == v.shape and box.shape == (2, 3)
2050
+ sk: int = box[0, 0]
2051
+ sj: int = box[0, 1]
2052
+ si: int = box[0, 2]
2053
+ ek: int = box[1, 0]
2054
+ ej: int = box[1, 1]
2055
+ ei: int = box[1, 2]
2056
+ for k in prange(sk, ek):
2057
+ for j in range(sj, ej):
2058
+ for i in range(si, ei):
2059
+ m: np.uint8 = mask[k, j, i]
2060
+ if m != 0:
2061
+ not_m: np.uint8 = ~m
2062
+ a[k, j, i] = (m & v[k, j, i]) | (not_m & a[k, j, i])
2063
+
2064
+
1270
2065
  @njit # pragma: no cover
1271
2066
  def intersect_numba(
1272
2067
  axis: int,
@@ -1350,6 +2145,7 @@ def intersect_numba(
1350
2145
  face_idx[index2] = d2
1351
2146
  face_idx[2 - axis] = face
1352
2147
 
2148
+ # dangerous: relies on indivisible read-modify-write of memory word containing multiple faces elements
1353
2149
  faces[face_idx[0], face_idx[1], face_idx[2]] = True
1354
2150
 
1355
2151
  if return_depths:
@@ -1362,62 +2158,426 @@ def intersect_numba(
1362
2158
  return faces, offsets, triangle_per_face
1363
2159
 
1364
2160
 
2161
+ def _all_offsets(crs, k_offsets_list, j_offsets_list, i_offsets_list):
2162
+ if crs.xy_units == crs.z_units:
2163
+ return np.concatenate((k_offsets_list, j_offsets_list, i_offsets_list), axis = 0)
2164
+ ji_offsets = np.concatenate((j_offsets_list, i_offsets_list), axis = 0)
2165
+ wam.convert_lengths(ji_offsets, crs.xy_units, crs.z_units)
2166
+ return np.concatenate((k_offsets_list, ji_offsets), axis = 0)
2167
+
2168
+
1365
2169
  @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.
2170
+ def _fill_bisector(bisect: np.ndarray, open_k: np.ndarray, open_j: np.ndarray, open_i: np.ndarray):
2171
+ nk: int = bisect.shape[0]
2172
+ nj: int = bisect.shape[1]
2173
+ ni: int = bisect.shape[2]
2174
+ going: bool = True
2175
+ while going:
2176
+ going = False
2177
+ for k in range(nk):
2178
+ for j in range(nj):
2179
+ for i in range(ni):
2180
+ if bisect[k, j, i]:
2181
+ continue
2182
+ if ((k and bisect[k - 1, j, i] and open_k[k - 1, j, i]) or
2183
+ (j and bisect[k, j - 1, i] and open_j[k, j - 1, i]) or
2184
+ (i and bisect[k, j, i - 1] and open_i[k, j, i - 1]) or
2185
+ (k < nk - 1 and bisect[k + 1, j, i] and open_k[k, j, i]) or
2186
+ (j < nj - 1 and bisect[k, j + 1, i] and open_j[k, j, i]) or
2187
+ (i < ni - 1 and bisect[k, j, i + 1] and open_i[k, j, i])):
2188
+ bisect[k, j, i] = True
2189
+ going = True
1375
2190
 
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
2191
 
1385
- returns:
1386
- Tuple containing:
2192
+ @njit # pragma: no cover
2193
+ def _fill_packed_bisector(bisect: np.ndarray, open_k: np.ndarray, open_j: np.ndarray, open_i: np.ndarray):
2194
+ nk: int = bisect.shape[0]
2195
+ nj: int = bisect.shape[1]
2196
+ ni: int = bisect.shape[2]
2197
+ going: bool = True
2198
+ m: np.uint8 = np.uint8(0)
2199
+ om: np.uint8 = np.uint8(0)
2200
+ oi: np.uint8 = np.uint8(0)
2201
+ while going:
2202
+ going = False
2203
+ for k in range(nk):
2204
+ for j in range(nj):
2205
+ for i in range(ni):
2206
+ m = np.uint8(bisect[k, j, i]) # 8 bools packed into a uint8
2207
+ if bisect[k, j, i] == np.uint8(0xFF): # all 8 values already set
2208
+ continue
2209
+ om = m # copy to check for changes later
2210
+ if k:
2211
+ m |= (bisect[k - 1, j, i] & open_k[k - 1, j, i])
2212
+ if k < nk - 1:
2213
+ m |= (bisect[k + 1, j, i] & open_k[k, j, i])
2214
+ if j:
2215
+ m |= (bisect[k, j - 1, i] & open_j[k, j - 1, i])
2216
+ if j < nj - 1:
2217
+ m |= (bisect[k, j + 1, i] & open_j[k, j, i])
2218
+ oi = np.uint8(open_i[k, j, i]) # type: ignore
2219
+ m |= (m >> 1) & (oi >> 1) # type: ignore
2220
+ m |= (m << 1) & oi # type: ignore
2221
+ # handle rollover bits for I
2222
+ if i and (bisect[k, j, i - 1] & open_i[k, j, i - 1] & np.uint8(0x01)):
2223
+ m |= np.uint8(0x80)
2224
+ if (i < ni - 1) and (oi & 1) and (bisect[k, j, i + 1] & 0x80):
2225
+ m |= np.uint8(0x01)
2226
+ if m != om:
2227
+ bisect[k, j, i] = m
2228
+ going = True
1387
2229
 
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
2230
 
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
2231
+ @njit # pragma: no cover
2232
+ def _shallow_or_curtain(a: np.ndarray, true_count: int, raw: bool) -> bool:
2233
+ # negate the bool array if it minimises the mean k and determine if the bisector indicates a curtain
2234
+ assert a.ndim == 3
2235
+ layer_cell_count: int = a.shape[1] * a.shape[2]
2236
+ k_sum: int = 0
2237
+ opposite_k_sum: int = 0
2238
+ is_curtain: bool = False
2239
+ layer_count: int = 0
2240
+ for k in range(a.shape[0]):
2241
+ layer_count = np.count_nonzero(a[k])
2242
+ k_sum += (k + 1) * layer_count
2243
+ opposite_k_sum += (k + 1) * (layer_cell_count - layer_count)
2244
+ mean_k: float = float(k_sum) / float(true_count)
2245
+ opposite_mean_k: float = float(opposite_k_sum) / float(a.size - true_count)
2246
+ if mean_k > opposite_mean_k and not raw:
2247
+ a[:] = np.logical_not(a)
2248
+ if abs(mean_k - opposite_mean_k) <= 0.001:
2249
+ # log.warning('unable to determine which side of surface is shallower')
2250
+ is_curtain = True
2251
+ return is_curtain
1404
2252
 
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
1409
2253
 
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
2254
+ @njit # pragma: no cover
2255
+ def _packed_shallow_or_curtain(a: np.ndarray, true_count: int, raw: bool) -> bool:
2256
+ # negate the packed bool array if it minimises the mean k and determine if the bisector indicates a curtain
2257
+ assert a.ndim == 3
2258
+ layer_cell_count: int = 8 * a.shape[1] * a.shape[2] # note: includes padding bits
2259
+ k_sum: int = 0
2260
+ opposite_k_sum: int = 0
2261
+ is_curtain: bool = False
2262
+ layer_count: int = 0
2263
+ for k in range(a.shape[0]):
2264
+ # np.bitwise_count() not yet supported by numba
2265
+ layer_count = np.sum(np.bitwise_count(a[k]), dtype = np.int64) # type: ignore
2266
+ k_sum += (k + 1) * layer_count
2267
+ opposite_k_sum += (k + 1) * (layer_cell_count - layer_count)
2268
+ mean_k: float = float(k_sum) / float(true_count)
2269
+ opposite_mean_k: float = float(opposite_k_sum) / float(8 * a.size - true_count)
2270
+ if mean_k > opposite_mean_k and not raw:
2271
+ a[:] = np.invert(a)
2272
+ if abs(mean_k - opposite_mean_k) <= 0.001:
2273
+ # log.warning('unable to determine which side of surface is shallower')
2274
+ is_curtain = True
2275
+ return is_curtain
1414
2276
 
1415
- return array, first_k, first_j, first_i
1416
2277
 
2278
+ @njit # pragma: no cover
2279
+ def _packed_shallow_or_curtain_temp_bitwise_count(a: np.ndarray, true_count: int, raw: bool) -> bool:
2280
+ # negate the packed bool array if it minimises the mean k and determine if the bisector indicates a curtain
2281
+ assert a.ndim == 3
2282
+ # note: following 'cell count' includes padding bits
2283
+ layer_cell_count: np.int64 = 8 * a.shape[1] * a.shape[2] # type: ignore
2284
+ k_sum: np.int64 = 0 # type: ignore
2285
+ opposite_k_sum: np.int64 = 0 # type: ignore
2286
+ is_curtain: bool = False
2287
+ layer_count: np.int64 = 0 # type: ignore
2288
+ for k in range(a.shape[0]):
2289
+ layer_count = _bitwise_count_njit(a[k, :, :])
2290
+ k_sum += (k + 1) * layer_count
2291
+ opposite_k_sum += (k + 1) * (layer_cell_count - layer_count)
2292
+ mean_k: float = float(k_sum) / float(true_count)
2293
+ opposite_mean_k: float = float(opposite_k_sum) / float(8 * a.size - true_count)
2294
+ if mean_k > opposite_mean_k and not raw:
2295
+ a[:] = np.invert(a)
2296
+ if abs(mean_k - opposite_mean_k) <= 0.001:
2297
+ # log.warning('unable to determine which side of surface is shallower')
2298
+ is_curtain = True
2299
+ return is_curtain
2300
+
2301
+
2302
+ def _set_bisector_outside_box(a: np.ndarray, box: np.ndarray, box_array: np.ndarray): # type: ignore
2303
+ # set values outside of the bounding box
2304
+ if box[1, 0] < a.shape[0] and np.any(box_array[-1, :, :]):
2305
+ a[box[1, 0]:, :, :] = True
2306
+ if box[0, 0] != 0:
2307
+ a[:box[0, 0], :, :] = True
2308
+ if box[1, 1] < a.shape[1] and np.any(box_array[:, -1, :]):
2309
+ a[:, box[1, 1]:, :] = True
2310
+ if box[0, 1] != 0:
2311
+ a[:, :box[0, 1], :] = True
2312
+ if box[1, 2] < a.shape[2] and np.any(box_array[:, :, -1]):
2313
+ a[:, :, box[1, 2]:] = True
2314
+ if box[0, 2] != 0:
2315
+ a[:, :, :box[0, 2]] = True
2316
+
2317
+
2318
+ def _set_packed_bisector_outside_box(a: np.ndarray, box: np.ndarray, box_array: np.ndarray, tail: int):
2319
+ # set values outside of the bounding box, working with packed arrays
2320
+ if box[1, 0] < a.shape[0] and np.any(box_array[-1, :, :]):
2321
+ a[box[1, 0]:, :, :] = 255
2322
+ if box[0, 0] != 0:
2323
+ a[:box[0, 0], :, :] = 255
2324
+ if box[1, 1] < a.shape[1] and np.any(box_array[:, -1, :]):
2325
+ a[:, box[1, 1]:, :] = 255
2326
+ if box[0, 1] != 0:
2327
+ a[:, :box[0, 1], :] = 255
2328
+ if box[1, 2] < a.shape[2] and np.any(np.bitwise_and(box_array[:, :, -1], 1)):
2329
+ a[:, :, box[1, 2]:] = 255
2330
+ if box[0, 2] != 0:
2331
+ a[:, :, :box[0, 2]] = 255
2332
+ if tail:
2333
+ m = np.uint8((255 << (8 - tail)) & 255)
2334
+ a[:, :, -1] &= m
2335
+
2336
+
2337
+ def _box_face_arrays_from_indices( # type: ignore
2338
+ k_faces_kji0: Union[np.ndarray, None], j_faces_kji0: Union[np.ndarray, None],
2339
+ i_faces_kji0: Union[np.ndarray, None], box: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
2340
+ box_shape = box[1, :] - box[0, :]
2341
+ k_a = np.zeros((box_shape[0] - 1, box_shape[1], box_shape[2]), dtype = np.bool_)
2342
+ j_a = np.zeros((box_shape[0], box_shape[1] - 1, box_shape[2]), dtype = np.bool_)
2343
+ i_a = np.zeros((box_shape[0], box_shape[1], box_shape[2] - 1), dtype = np.bool_)
2344
+ ko = box[0, 0]
2345
+ jo = box[0, 1]
2346
+ io = box[0, 2]
2347
+ kr = box[1, 0] - ko
2348
+ jr = box[1, 1] - jo
2349
+ ir = box[1, 2] - io
2350
+ if k_faces_kji0 is not None:
2351
+ _set_face_array(k_a, k_faces_kji0, ko, jo, io, kr - 1, jr, ir)
2352
+ if j_faces_kji0 is not None:
2353
+ _set_face_array(j_a, j_faces_kji0, ko, jo, io, kr, jr - 1, ir)
2354
+ if i_faces_kji0 is not None:
2355
+ _set_face_array(i_a, i_faces_kji0, ko, jo, io, kr, jr, ir - 1)
2356
+ return k_a, j_a, i_a
2357
+
2358
+
2359
+ def _packed_box_face_arrays_from_indices( # type: ignore
2360
+ k_faces_kji0: Union[np.ndarray, None], j_faces_kji0: Union[np.ndarray, None],
2361
+ i_faces_kji0: Union[np.ndarray, None], box: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
2362
+ box_shape = box[1, :] - box[0, :] # note: I axis already shrunken
2363
+ k_a = np.zeros((box_shape[0] - 1, box_shape[1], box_shape[2]), dtype = np.uint8)
2364
+ j_a = np.zeros((box_shape[0], box_shape[1] - 1, box_shape[2]), dtype = np.uint8)
2365
+ i_a = np.zeros(tuple(box_shape), dtype = np.uint8)
2366
+ ko = box[0, 0]
2367
+ jo = box[0, 1]
2368
+ io = box[0, 2] * 8
2369
+ kr = box[1, 0] - ko
2370
+ jr = box[1, 1] - jo
2371
+ ir = box[1, 2] * 8 - io
2372
+ if k_faces_kji0 is not None:
2373
+ _set_packed_face_array(k_a, k_faces_kji0, ko, jo, io, kr - 1, jr, ir)
2374
+ if j_faces_kji0 is not None:
2375
+ _set_packed_face_array(j_a, j_faces_kji0, ko, jo, io, kr, jr - 1, ir)
2376
+ if i_faces_kji0 is not None:
2377
+ _set_packed_face_array(i_a, i_faces_kji0, ko, jo, io, kr, jr, ir)
2378
+ return k_a, j_a, i_a
1417
2379
 
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)
2380
+
2381
+ @njit # pragma: no cover
2382
+ def _set_face_array(a: np.ndarray, indices: np.ndarray, ko: int, jo: int, io: int, kr: int, jr: int, ir: int) -> None:
2383
+ k: int = 0
2384
+ j: int = 0
2385
+ i: int = 0
2386
+ for ind in range(len(indices)):
2387
+ k = indices[ind, 0] - ko
2388
+ if k < 0 or k >= kr:
2389
+ continue
2390
+ j = indices[ind, 1] - jo
2391
+ if j < 0 or j >= jr:
2392
+ continue
2393
+ i = indices[ind, 2] - io
2394
+ if i < 0 or i >= ir:
2395
+ continue
2396
+ a[k, j, i] = True
2397
+
2398
+
2399
+ @njit # pragma: no cover
2400
+ def _set_packed_face_array(a: np.ndarray, indices: np.ndarray, ko: int, jo: int, io: int, kr: int, jr: int,
2401
+ ir: int) -> None:
2402
+ k: int = 0
2403
+ j: int = 0
2404
+ i: int = 0
2405
+ for ind in range(len(indices)):
2406
+ k = indices[ind, 0] - ko
2407
+ if k < 0 or k >= kr:
2408
+ continue
2409
+ j = indices[ind, 1] - jo
2410
+ if j < 0 or j >= jr:
2411
+ continue
2412
+ i = indices[ind, 2] - io
2413
+ if i < 0 or i >= ir:
2414
+ continue
2415
+ ii, ib = divmod(i, 8)
2416
+ a[k, j, ii] |= (1 << (7 - ib))
2417
+
2418
+
2419
+ # yapf: disable
2420
+ def get_boundary_from_indices( # type: ignore
2421
+ k_faces_kji0: Union[np.ndarray, None],
2422
+ j_faces_kji0: Union[np.ndarray, None],
2423
+ i_faces_kji0: Union[np.ndarray, None],
2424
+ grid_extent_kji: Tuple[int, int, int]) -> np.ndarray:
2425
+ # yapf: enable
2426
+ """Return python protocol box containing indices"""
2427
+ k_min_kji0 = None if ((k_faces_kji0 is None) or (k_faces_kji0.size == 0)) else np.min(k_faces_kji0, axis = 0)
2428
+ k_max_kji0 = None if ((k_faces_kji0 is None) or (k_faces_kji0.size == 0)) else np.max(k_faces_kji0, axis = 0)
2429
+ j_min_kji0 = None if ((j_faces_kji0 is None) or (j_faces_kji0.size == 0)) else np.min(j_faces_kji0, axis = 0)
2430
+ j_max_kji0 = None if ((j_faces_kji0 is None) or (j_faces_kji0.size == 0)) else np.max(j_faces_kji0, axis = 0)
2431
+ i_min_kji0 = None if ((i_faces_kji0 is None) or (i_faces_kji0.size == 0)) else np.min(i_faces_kji0, axis = 0)
2432
+ i_max_kji0 = None if ((i_faces_kji0 is None) or (i_faces_kji0.size == 0)) else np.max(i_faces_kji0, axis = 0)
2433
+ box = np.empty((2, 3), dtype = np.int32)
2434
+ box[0, :] = grid_extent_kji
2435
+ box[1, :] = -1
2436
+ if k_min_kji0 is not None:
2437
+ box[0, 0] = k_min_kji0[0]
2438
+ box[0, 1] = k_min_kji0[1]
2439
+ box[0, 2] = k_min_kji0[2]
2440
+ box[1, 0] = k_max_kji0[0] # type: ignore
2441
+ box[1, 1] = k_max_kji0[1] # type: ignore
2442
+ box[1, 2] = k_max_kji0[2] # type: ignore
2443
+ if j_min_kji0 is not None:
2444
+ box[0, 0] = min(box[0, 0], j_min_kji0[0])
2445
+ box[0, 1] = min(box[0, 1], j_min_kji0[1])
2446
+ box[0, 2] = min(box[0, 2], j_min_kji0[2])
2447
+ box[1, 0] = max(box[1, 0], j_max_kji0[0]) # type: ignore
2448
+ box[1, 1] = max(box[1, 1], j_max_kji0[1]) # type: ignore
2449
+ box[1, 2] = max(box[1, 2], j_max_kji0[2]) # type: ignore
2450
+ if i_min_kji0 is not None:
2451
+ box[0, 0] = min(box[0, 0], i_min_kji0[0])
2452
+ box[0, 1] = min(box[0, 1], i_min_kji0[1])
2453
+ box[0, 2] = min(box[0, 2], i_min_kji0[2])
2454
+ box[1, 0] = max(box[1, 0], i_max_kji0[0]) # type: ignore
2455
+ box[1, 1] = max(box[1, 1], i_max_kji0[1]) # type: ignore
2456
+ box[1, 2] = max(box[1, 2], i_max_kji0[2]) # type: ignore
2457
+ assert np.all(box[1] >= box[0]), 'attempt to find bounding box when all faces None'
2458
+ # include buffer layer where box does not reach edge of grid
2459
+ box[1, :] += 1 # switch to python protocol
2460
+ return expanded_box(box, grid_extent_kji)
2461
+
2462
+
2463
+ def expanded_box(box: np.ndarray, extent_kji: Tuple[int, int, int]) -> np.ndarray:
2464
+ """Return a python protocol box expanded by a single slice on all six faces, where extent alloas."""
2465
+ # include buffer layer where box does not reach edge of grid
2466
+ np_extent_kji = np.array(extent_kji, dtype = np.int32)
2467
+ e_box = np.zeros((2, 3), dtype = np.int32)
2468
+ e_box[0, :] = np.maximum(box[0, :] - 1, 0)
2469
+ e_box[1, :] = np.minimum(box[1, :] + 1, extent_kji)
2470
+ assert np.all(e_box[0] >= 0)
2471
+ assert np.all(e_box[1] > e_box[0])
2472
+ assert np.all(e_box[1] <= np_extent_kji)
2473
+ return e_box
2474
+
2475
+
2476
+ def get_packed_boundary_from_indices( # type: ignore
2477
+ k_faces_kji0: Union[np.ndarray, None], j_faces_kji0: Union[np.ndarray, None],
2478
+ i_faces_kji0: Union[np.ndarray, None], grid_extent_kji: Tuple[int, int, int]) -> np.ndarray:
2479
+ """Return python protocol box containing indices, with I axis packed"""
2480
+ box = get_boundary_from_indices(k_faces_kji0, j_faces_kji0, i_faces_kji0, grid_extent_kji)
2481
+ return shrunk_box_for_packing(box)
2482
+
2483
+
2484
+ def shrunk_box_for_packing(box: np.ndarray) -> np.ndarray:
2485
+ """Return box with I dimension shrunk for bit packing equivalent."""
2486
+ shrunk_box = box.copy()
2487
+ shrunk_box[0, 2] /= 8
2488
+ shrunk_box[1, 2] = ((box[1, 2] - 1) // 8) + 1
2489
+ return shrunk_box
2490
+
2491
+
2492
+ def _shape_packed(unpacked_shape):
2493
+ """Return the equivalent packed shape for a given unpacked shape, as a tuple."""
2494
+ shrunken = ((unpacked_shape[-1] - 1) // 8) + 1
2495
+ if len(unpacked_shape) == 1:
2496
+ return (shrunken,)
2497
+ head = list(unpacked_shape[:-1])
2498
+ head.append(shrunken)
2499
+ return tuple(head)
2500
+
2501
+
2502
+ @njit # pragma: no cover
2503
+ def _bitwise_count_njit(a: np.ndarray) -> np.int64:
2504
+ """Deprecated: only needed till numpy versions < 2.0.0 are dropped."""
2505
+ c: np.int64 = 0 # type: ignore
2506
+ c += np.count_nonzero(np.bitwise_and(a, 0x01))
2507
+ c += np.count_nonzero(np.bitwise_and(a, 0x02))
2508
+ c += np.count_nonzero(np.bitwise_and(a, 0x04))
2509
+ c += np.count_nonzero(np.bitwise_and(a, 0x08))
2510
+ c += np.count_nonzero(np.bitwise_and(a, 0x10))
2511
+ c += np.count_nonzero(np.bitwise_and(a, 0x20))
2512
+ c += np.count_nonzero(np.bitwise_and(a, 0x40))
2513
+ c += np.count_nonzero(np.bitwise_and(a, 0x80))
2514
+ return c
2515
+
2516
+
2517
+ @njit # pragma: no cover
2518
+ def box_intersection(box_a: np.ndarray, box_b: np.ndarray) -> np.ndarray:
2519
+ """Return a box which is the intersection of two boxes, python protocol; all zeros if no intersection."""
2520
+ box = np.zeros((2, 3), dtype = np.int32)
2521
+ box[0] = np.maximum(box_a[0], box_b[0])
2522
+ box[1] = np.minimum(box_a[1], box_b[1])
2523
+ if np.any(box[1] <= box[0]):
2524
+ box[:] = 0
2525
+ return box
2526
+
2527
+
2528
+ @njit # pragma: no cover
2529
+ def get_box(mask: np.ndarray) -> Tuple[np.ndarray, int]:
2530
+ """Returns a python protocol box enclosing True elements of 3D boolean mask, and count which is zero if all False."""
2531
+ box = np.full((2, 3), -1, dtype = np.int32)
2532
+ count = 0
2533
+ for k in range(mask.shape[0]):
2534
+ for j in range(mask.shape[1]):
2535
+ for i in range(mask.shape[2]):
2536
+ if mask[k, j, i]:
2537
+ if count == 0:
2538
+ box[0, 0] = k
2539
+ box[0, 1] = j
2540
+ box[0, 2] = i
2541
+ box[1, 0] = k + 1
2542
+ box[1, 1] = j + 1
2543
+ box[1, 2] = i + 1
2544
+ else:
2545
+ if k < box[0, 0]:
2546
+ box[0, 0] = k
2547
+ elif k >= box[1, 0]:
2548
+ box[1, 0] = k + 1
2549
+ if j < box[0, 1]:
2550
+ box[0, 1] = j
2551
+ elif j >= box[1, 1]:
2552
+ box[1, 1] = j + 1
2553
+ if i < box[0, 2]:
2554
+ box[0, 2] = i
2555
+ elif i >= box[1, 2]:
2556
+ box[1, 2] = i + 1
2557
+ count += 1
2558
+ return box, count
2559
+
2560
+
2561
+ @njit # pragma: no cover
2562
+ def filter_faces(faces_kji0: np.ndarray, face_patches: np.ndarray, cell_patches: np.ndarray, axis: int) -> np.ndarray:
2563
+ """Return 1D boolean selection array indicating subset of faces that are applicable to cells with matching patch."""
2564
+ n: int = len(faces_kji0)
2565
+ assert len(face_patches) == n
2566
+ selection = np.zeros(n, dtype = np.bool_)
2567
+ for f in range(n):
2568
+ k: int = faces_kji0[f, 0]
2569
+ j: int = faces_kji0[f, 1]
2570
+ i: int = faces_kji0[f, 2]
2571
+ if face_patches[f] == cell_patches[k, j, i]:
2572
+ selection[f] = True
2573
+ else:
2574
+ if axis == 0:
2575
+ if face_patches[f] == cell_patches[k + 1, j, i]:
2576
+ selection[f] = True
2577
+ elif axis == 1:
2578
+ if face_patches[f] == cell_patches[k, j + 1, i]:
2579
+ selection[f] = True
2580
+ else:
2581
+ if face_patches[f] == cell_patches[k, j, i + 1]:
2582
+ selection[f] = True
2583
+ return selection