resqpy 4.14.2__py3-none-any.whl → 5.1.6__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 +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