resqpy 4.5.0__py3-none-any.whl → 4.6.3__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.
@@ -751,9 +751,6 @@ def reorient(points, rough = True, max_dip = None, use_linalg = False):
751
751
  the numpy linear algebra option seems to be memory intensive, not recommended
752
752
  """
753
753
 
754
- def z_range(p):
755
- return np.nanmax(p[..., 2]) - np.nanmin(p[..., 2])
756
-
757
754
  def best_angles(points, mid_x, mid_y, steps, d_theta):
758
755
  best_range = None
759
756
  best_x_rotation = None
@@ -766,7 +763,7 @@ def reorient(points, rough = True, max_dip = None, use_linalg = False):
766
763
  rotation_m = vec.rotation_3d_matrix((x_degrees, 0.0, y_degrees))
767
764
  p = points.copy()
768
765
  rotated_p = vec.rotate_array(rotation_m, p)
769
- z_r = z_range(rotated_p)
766
+ z_r = np.nanmax(rotated_p[..., 2]) - np.nanmin(rotated_p[..., 2])
770
767
  if best_range is None or z_r < best_range:
771
768
  best_range = z_r
772
769
  best_x_rotation = x_degrees
@@ -777,14 +774,13 @@ def reorient(points, rough = True, max_dip = None, use_linalg = False):
777
774
  assert p.ndim >= 2 and p.shape[-1] == 3
778
775
  p = p.reshape((-1, 3))
779
776
  centre = p.sum(axis = 0) / p.shape[0]
780
- u, s, vh = np.linalg.svd(p - centre)
777
+ _, _, vh = np.linalg.svd(p - centre)
781
778
  # unit normal vector
782
779
  return vh[2, :]
783
780
 
784
781
  assert points.ndim >= 2 and points.shape[-1] == 3
785
782
 
786
783
  if use_linalg:
787
-
788
784
  normal_vector = linalg_normal_vector(points)
789
785
  incl = vec.inclination(normal_vector)
790
786
  if incl == 0.0:
@@ -792,10 +788,8 @@ def reorient(points, rough = True, max_dip = None, use_linalg = False):
792
788
  else:
793
789
  azi = vec.azimuth(normal_vector)
794
790
  rotation_m = vec.tilt_3d_matrix(azi, incl)
795
-
796
791
  else:
797
-
798
- # coarse iteration trying a few different angles
792
+ # coarse iteration trying a few different angles
799
793
  best_x_rotation, best_y_rotation = best_angles(points, 0.0, 0.0, 7, 30.0)
800
794
 
801
795
  # finer iteration searching around the best coarse rotation
@@ -837,7 +831,7 @@ def make_all_clockwise_xy(t, p):
837
831
  return t
838
832
 
839
833
 
840
- def surrounding_xy_ring(p, count = 12, radial_factor = 10.0, radial_distance = None):
834
+ def surrounding_xy_ring(p, count = 12, radial_factor = 10.0, radial_distance = None, inner_ring = False):
841
835
  """Creates a set of points surrounding the point set p, in the xy plane.
842
836
 
843
837
  arguments:
@@ -847,12 +841,23 @@ def surrounding_xy_ring(p, count = 12, radial_factor = 10.0, radial_distance = N
847
841
  the 'radius' of the outermost points in p
848
842
  radial_distance (float): if present, the radius of the ring of points, unless radial_factor
849
843
  results in a greater distance in which case that is used
844
+ inner_ring (bool, default False): if True, an inner ring of points, with double count, is created
845
+ at a radius just outside that of the furthest flung original point; this improves triangulation
846
+ of the extended point set when the original has a non-convex hull
850
847
 
851
848
  returns:
852
- numpy float array of shape (count, 3) being xyz points in surrounding ring; z is set constant to
853
- mean value of z in p
849
+ numpy float array of shape (N, 3) being xyz points in surrounding ring(s); z is set constant to
850
+ mean value of z in p; N is count if inner_ring is False, 3 * count if True
854
851
  """
855
852
 
853
+ def make_ring(count, centre, radius):
854
+ delta_theta = 2.0 * maths.pi / float(count)
855
+ ring = np.zeros((count, 3))
856
+ for i in range(count):
857
+ theta = float(i) * delta_theta
858
+ ring[i] = centre + radius * np.array([maths.cos(theta), maths.sin(theta), 0.0])
859
+ return ring
860
+
856
861
  assert p.shape[-1] == 3
857
862
  assert radial_factor >= 1.0
858
863
  centre = np.nanmean(p.reshape((-1, 3)), axis = 0)
@@ -861,11 +866,12 @@ def surrounding_xy_ring(p, count = 12, radial_factor = 10.0, radial_distance = N
861
866
  radius = p_radius * radial_factor
862
867
  if radial_distance is not None and radial_distance > radius:
863
868
  radius = radial_distance
864
- delta_theta = 2.0 * maths.pi / float(count)
865
- ring = np.zeros((count, 3))
866
- for i in range(count):
867
- theta = float(i) * delta_theta
868
- ring[i] = centre + radius * np.array([maths.cos(theta), maths.sin(theta), 0.0])
869
+ ring = make_ring(count, centre, radius)
870
+ if inner_ring:
871
+ inner_radius = p_radius * 1.1
872
+ assert radius > inner_radius
873
+ in_ring = make_ring(2 * count, centre, inner_radius)
874
+ return np.concatenate((in_ring, ring), axis = 0)
869
875
  return ring
870
876
 
871
877
 
@@ -896,6 +902,36 @@ def edges(t):
896
902
  return np.unique(all_edges, axis = 0, return_counts = True)
897
903
 
898
904
 
905
+ def triangles_using_point(t, point_index):
906
+ """Returns list-like 1D int array of indices of triangles using vertex identified by point_index."""
907
+
908
+ assert t.ndim == 2 and t.shape[1] == 3 and isinstance(point_index, int)
909
+ mask = np.any(t == point_index, axis = -1)
910
+ return np.where(mask)[0]
911
+
912
+
913
+ def triangles_using_edge(t, p1, p2):
914
+ """Returns list-like 1D int array of indices of triangles using edge identified by pair of point indices."""
915
+
916
+ assert t.ndim == 2 and t.shape[1] == 3 and p1 != p2
917
+ mask = np.logical_and(np.any(t == p1, axis = -1), np.any(t == p2, axis = -1))
918
+ return np.where(mask)[0]
919
+
920
+
921
+ def triangles_using_edges(t, edges):
922
+ """Returns int array of shape (len(edges), 2) with indices of upto 2 triangles using each edge (-1 for unused)."""
923
+
924
+ assert t.ndim == 2 and t.shape[1] == 3 and edges.ndim == 2 and edges.shape[1] == 2
925
+ ti = np.full((len(edges), 2), -1, dtype = int)
926
+ for i in range(len(edges)):
927
+ te = triangles_using_edge(t, edges[i, 0], edges[i, 1])
928
+ c = len(te)
929
+ assert 0 <= c <= 2
930
+ if c:
931
+ ti[i, :c] = te
932
+ return ti
933
+
934
+
899
935
  def rim_edges(all_edges, edge_counts):
900
936
  """Returns a subset of all edges where the edge count is 1."""
901
937
 
@@ -904,6 +940,14 @@ def rim_edges(all_edges, edge_counts):
904
940
  return all_edges[edge_counts == 1, :]
905
941
 
906
942
 
943
+ def internal_edges(all_edges, edge_counts):
944
+ """Returns a subset of all edges where the edge count is 2."""
945
+
946
+ assert all_edges.ndim == 2 and all_edges.shape[1] == 2
947
+ assert edge_counts.ndim == 1 and edge_counts.size == len(all_edges)
948
+ return all_edges[edge_counts == 2, :]
949
+
950
+
907
951
  def rims(all_rim_edges):
908
952
  """Returns edge index and points index lists of distinct rims.
909
953
 
@@ -949,7 +993,7 @@ def rims(all_rim_edges):
949
993
  return rim_edges_list, rim_points_list
950
994
 
951
995
 
952
- @njit
996
+ @njit # pragma: no cover
953
997
  def _find_unused(ap: np.ndarray, used_mask: np.ndarray, v: int): # type: ignore
954
998
  """Finds the first unused occurence of v in pair list ap, returning index and paired value."""
955
999
  for idx, val in np.ndenumerate(ap[:, 0]):
@@ -961,18 +1005,18 @@ def _find_unused(ap: np.ndarray, used_mask: np.ndarray, v: int): # type: ignore
961
1005
  return ap.shape[0], -1
962
1006
 
963
1007
 
964
- @njit
1008
+ @njit # pragma: no cover
965
1009
  def _first_false(array: np.ndarray) -> Optional[int]: # type: ignore
966
- """Returns the index of the first False value in the array."""
1010
+ """Returns the index of the first False (or zero) value in the 1D array."""
967
1011
  for idx, val in np.ndenumerate(array):
968
1012
  if not val:
969
1013
  return idx[0]
970
1014
  return array.size
971
1015
 
972
1016
 
973
- @njit
1017
+ @njit # pragma: no cover
974
1018
  def _first_match(array: np.ndarray, v: int) -> Optional[int]: # type: ignore
975
- """Returns the index of the first True value in the array."""
1019
+ """Returns the index of the first occurrence of value v in the 1D array."""
976
1020
  for idx, val in np.ndenumerate(array):
977
1021
  if val == v:
978
1022
  return idx[0]
@@ -75,10 +75,22 @@ def amplify(v, scaling): # note: could just use numpy a * scalar facility
75
75
  def unit_vector(v):
76
76
  """Returns vector with same direction as v but with unit length."""
77
77
  assert 2 <= len(v) <= 3
78
- v = np.array(v, dtype = float)
79
- if np.all(v == 0.0):
78
+ v = np.array(v, dtype = np.float64)
79
+ norm = np.linalg.norm(v)
80
+ if norm == 0.0:
80
81
  return v
81
- return v / maths.sqrt(np.sum(v * v))
82
+ v = v / norm
83
+ return v
84
+
85
+
86
+ @njit
87
+ def unit_vector_njit(v): # pragma: no cover
88
+ """Returns vector with same direction as v but with unit length."""
89
+ norm = np.linalg.norm(v)
90
+ if norm == 0.0:
91
+ return v
92
+ v = v / norm
93
+ return v
82
94
 
83
95
 
84
96
  def unit_vectors(v):
@@ -189,6 +201,7 @@ def nan_inclinations(a, already_unit_vectors = False):
189
201
 
190
202
  def points_direction_vector(a, axis):
191
203
  """Returns an average direction vector based on first and last non-NaN points or slices in given axis."""
204
+ # note: as currently coded, might give poor results with some patterns of NaNs
192
205
 
193
206
  assert a.ndim > 1 and 0 <= axis < a.ndim - 1 and a.shape[-1] > 1 and a.shape[axis] > 1
194
207
  if np.all(np.isnan(a)):
@@ -196,17 +209,18 @@ def points_direction_vector(a, axis):
196
209
  start = 0
197
210
  start_slicing = [slice(None)] * a.ndim
198
211
  while True:
199
- start_slicing[axis] = slice(start)
212
+ start_slicing[axis] = slice(start, start + 1)
200
213
  if not np.all(np.isnan(a[tuple(start_slicing)])):
201
214
  break
202
215
  start += 1
216
+ assert start < a.shape[axis]
203
217
  finish = a.shape[axis] - 1
204
218
  finish_slicing = [slice(None)] * a.ndim
205
219
  while True:
206
- finish_slicing[axis] = slice(finish)
220
+ finish_slicing[axis] = slice(finish, finish + 1)
207
221
  if not np.all(np.isnan(a[tuple(finish_slicing)])):
208
222
  break
209
- finish += 1
223
+ finish -= 1
210
224
  if start >= finish:
211
225
  return None
212
226
  if a.ndim > 2:
@@ -220,24 +234,24 @@ def points_direction_vector(a, axis):
220
234
  return finish_p - start_p
221
235
 
222
236
 
223
- def dot_product(a, b):
237
+ def dot_product(a, b): # pragma: no cover
224
238
  """Returns the dot product (scalar product) of the two vectors."""
225
239
  return np.dot(a, b)
226
240
 
227
241
 
228
- def dot_products(a, b):
242
+ def dot_products(a, b): # pragma: no cover
229
243
  """Returns the dot products of pairs of vectors; last axis covers element of a vector."""
230
244
  return np.sum(a * b, axis = -1)
231
245
 
232
246
 
233
- def cross_product(a, b):
247
+ def cross_product(a, b): # pragma: no cover
234
248
  """Returns the cross product (vector product) of the two vectors."""
235
249
  return np.cross(a, b)
236
250
 
237
251
 
238
- def naive_length(v):
252
+ def naive_length(v): # pragma: no cover
239
253
  """Returns the length of the vector assuming consistent units."""
240
- return maths.sqrt(dot_product(v, v))
254
+ return np.linalg.norm(v)
241
255
 
242
256
 
243
257
  def naive_lengths(v):
@@ -245,9 +259,9 @@ def naive_lengths(v):
245
259
  return np.sqrt(np.sum(v * v, axis = -1))
246
260
 
247
261
 
248
- def naive_2d_length(v):
262
+ def naive_2d_length(v): # pragma: no cover
249
263
  """Returns the length of the vector projected onto xy plane, assuming consistent units."""
250
- return maths.sqrt(dot_product(v[0:2], v[0:2]))
264
+ return np.linalg.norm(v[0:2])
251
265
 
252
266
 
253
267
  def naive_2d_lengths(v):
@@ -257,9 +271,20 @@ def naive_2d_lengths(v):
257
271
 
258
272
 
259
273
  def unit_corrected_length(v, unit_conversion):
260
- """Returns the length of the vector v after applying the unit_conversion factors."""
261
- # unit_conversion might be [1.0, 1.0, 0.3048] to convert z from feet to metres, for example
262
- # or [3.28084, 3.28084, 1.0] to convert x and y from metres to feet
274
+ """Returns the length of the vector v after applying the unit_conversion factors.
275
+
276
+ arguments:
277
+ v (1D numpy float array): vector with mixed units of measure
278
+ unit_conversion (1D numpy float array): vector to multiply elements of v by, prior to finding length
279
+
280
+ returns:
281
+ float, being the length of v after adjustment by unit_conversion
282
+
283
+ notes:
284
+ example unit_conversion might be:
285
+ [1.0, 1.0, 0.3048] to convert z from feet to metres, or
286
+ [3.28084, 3.28084, 1.0] to convert x and y from metres to feet
287
+ """
263
288
  converted = elemental_multiply(v, unit_conversion)
264
289
  return naive_length(converted)
265
290
 
@@ -293,14 +318,12 @@ def rotation_matrix_3d_axial(axis, angle):
293
318
  this function follows the mathematical convention: a positive angle results in anti-clockwise rotation
294
319
  when viewed in direction of positive axis
295
320
  """
296
-
297
321
  axis_a = (axis + 1) % 3
298
322
  axis_b = (axis_a + 1) % 3
299
- matrix = np.zeros((3, 3))
300
- matrix[axis, axis] = 1.0
301
- radians = radians_from_degrees(angle)
302
- cosine = maths.cos(radians)
303
- sine = maths.sin(radians)
323
+ matrix = np.eye(3)
324
+ radians = np.radians(angle)
325
+ cosine = np.cos(radians)
326
+ sine = np.sin(radians)
304
327
  matrix[axis_a, axis_a] = cosine
305
328
  matrix[axis_b, axis_b] = cosine
306
329
  matrix[axis_a, axis_b] = -sine # left handed coordinate system, eg. UTM & depth
@@ -308,45 +331,66 @@ def rotation_matrix_3d_axial(axis, angle):
308
331
  return matrix
309
332
 
310
333
 
311
- def no_rotation_matrix():
312
- """Returns a rotation matrix which will not move points."""
313
- matrix = np.zeros((3, 3))
314
- for axis in range(3):
315
- matrix[axis, axis] = 1.0
316
- return matrix
334
+ def no_rotation_matrix(): # pragma: no cover
335
+ """Returns a rotation matrix which will not move points (identity matrix)."""
336
+ return np.eye(3)
317
337
 
318
338
 
319
339
  def rotation_3d_matrix(xzy_axis_angles):
320
- """Returns a rotation matrix which will rotate points about 3 axes by angles in degrees."""
321
-
322
- # matrix = np.zeros((3, 3))
323
- # for axis in range(3):
324
- # matrix[axis, axis] = 1.0
325
- # for axis in range(3):
326
- # matrix = np.dot(matrix, rotation_matrix_3d_axial(axis, xzy_axis_angles[axis]))
327
- matrix = rotation_matrix_3d_axial(1, xzy_axis_angles[2]) # about y axis
328
- matrix = np.dot(matrix, rotation_matrix_3d_axial(2, xzy_axis_angles[1])) # about z axis
329
- matrix = np.dot(matrix, rotation_matrix_3d_axial(0, xzy_axis_angles[0])) # about x axis
330
- return matrix
340
+ """Returns a rotation matrix which will rotate points about the x, z, then y axis by angles in degrees."""
341
+ angles = np.radians(xzy_axis_angles)
342
+ cos_c, cos_a, cos_b = np.cos(angles)
343
+ sin_c, sin_a, sin_b = np.sin(angles)
344
+
345
+ sin_a_sin_c = sin_a * sin_c
346
+ sin_a_cos_c = sin_a * cos_c
347
+
348
+ rotation_matrix = np.array([
349
+ [cos_a * cos_b, -sin_a_cos_c * cos_b + sin_b * sin_c, cos_b * sin_a_sin_c + sin_b * cos_c],
350
+ [sin_a, cos_a * cos_c, -sin_c * cos_a],
351
+ [-sin_b * cos_a, sin_a_cos_c * sin_b + cos_b * sin_c, -sin_b * sin_a_sin_c + cos_b * cos_c],
352
+ ])
353
+ return rotation_matrix
354
+
355
+
356
+ @njit
357
+ def rotation_3d_matrix_njit(xzy_axis_angles): # pragma: no cover
358
+ """Returns a rotation matrix which will rotate points about the x, z, then y axis by angles in degrees."""
359
+ angles = np.radians(xzy_axis_angles)
360
+ cos_c, cos_a, cos_b = np.cos(angles)
361
+ sin_c, sin_a, sin_b = np.sin(angles)
362
+
363
+ sin_a_sin_c = sin_a * sin_c
364
+ sin_a_cos_c = sin_a * cos_c
331
365
 
366
+ rotation_matrix = np.array([
367
+ [cos_a * cos_b, -sin_a_cos_c * cos_b + sin_b * sin_c, cos_b * sin_a_sin_c + sin_b * cos_c],
368
+ [sin_a, cos_a * cos_c, -sin_c * cos_a],
369
+ [-sin_b * cos_a, sin_a_cos_c * sin_b + cos_b * sin_c, -sin_b * sin_a_sin_c + cos_b * cos_c],
370
+ ])
371
+ return rotation_matrix
332
372
 
333
- def reverse_rotation_3d_matrix(xzy_axis_angles):
334
- """Returns a rotation matrix which will rotate back points about 3 axes by angles in degrees."""
373
+
374
+ def reverse_rotation_3d_matrix(xzy_axis_angles): # pragma: no cover
375
+ """Returns a rotation matrix which will rotate points about the y, z, then x axis by angles in degrees."""
335
376
 
336
377
  return rotation_3d_matrix(xzy_axis_angles).T
337
378
 
338
379
 
339
- def rotate_vector(rotation_matrix, vector):
380
+ def rotate_vector(rotation_matrix, vector): # pragma: no cover
340
381
  """Returns the rotated vector."""
341
-
342
382
  return np.dot(rotation_matrix, vector)
343
383
 
344
384
 
345
385
  def rotate_array(rotation_matrix, a):
346
386
  """Returns a copy of array a with each vector rotated by the rotation matrix."""
387
+ return np.matmul(rotation_matrix, a.reshape(-1, 3).T).T.reshape(a.shape)
347
388
 
348
- s = a.shape
349
- return np.matmul(rotation_matrix, a.reshape(-1, 3).T).T.reshape(s)
389
+
390
+ @njit
391
+ def rotate_array_njit(rotation_matrix, a): # pragma: no cover
392
+ """Returns a copy of array a with each vector rotated by the rotation matrix."""
393
+ return np.dot(rotation_matrix, a.reshape(-1, 3).T).T.reshape(a.shape)
350
394
 
351
395
 
352
396
  def rotate_xyz_array_around_z_axis(a, target_xy_vector):
@@ -399,15 +443,32 @@ def tilt_3d_matrix(azimuth, dip):
399
443
 
400
444
 
401
445
  def rotation_matrix_3d_vector(v):
446
+ """Returns a rotation matrix which will rotate vector v to the vertical (z) axis.
447
+
448
+ note:
449
+ the returned matrix will map a positive z axis vector onto v
450
+ """
451
+ v = v / np.linalg.norm(v)
452
+ kmat = np.array([[0, 0, -v[0]], [0, 0, -v[1]], [v[0], v[1], 0]])
453
+ rotation_matrix = np.eye(3) + kmat + kmat.dot(kmat) * (1 / (1 + v[2]))
454
+
455
+ rotation_matrix[:2] = -rotation_matrix[:2]
456
+ return rotation_matrix
457
+
458
+
459
+ @njit
460
+ def rotation_matrix_3d_vector_njit(v): # pragma: no cover
402
461
  """Returns a rotation matrix which will rotate points by inclination and azimuth of vector.
403
462
 
404
463
  note:
405
464
  the returned matrix will map a positive z axis vector onto v
406
465
  """
466
+ v = v / np.linalg.norm(v)
467
+ kmat = np.array([[0, 0, -v[0]], [0, 0, -v[1]], [v[0], v[1], 0]])
468
+ rotation_matrix = np.eye(3) + kmat + kmat.dot(kmat) * (1 / (1 + v[2]))
407
469
 
408
- m = tilt_3d_matrix(azimuth(v), inclination(v))
409
- m[:2, :] = -m[:2, :] # todo: should this change be in the tilt matrix?
410
- return m
470
+ rotation_matrix[:2] = -rotation_matrix[:2]
471
+ return rotation_matrix
411
472
 
412
473
 
413
474
  def tilt_points(pivot_xyz, azimuth, dip, points):
@@ -562,7 +623,7 @@ def points_in_triangles(p, t, da, projection = 'xy', edged = False):
562
623
 
563
624
 
564
625
  @njit
565
- def point_in_polygon(x, y, polygon):
626
+ def point_in_polygon(x, y, polygon): # pragma: no cover
566
627
  """Calculates if a point in within a polygon in 2D.
567
628
 
568
629
  arguments:
@@ -576,14 +637,14 @@ def point_in_polygon(x, y, polygon):
576
637
  note:
577
638
  the polygon is assumed closed, the closing point should not be repeated
578
639
  """
579
- polygon_vertices = len(polygon)
640
+ n = len(polygon)
580
641
  inside = False
581
642
  p2x = 0.0
582
643
  p2y = 0.0
583
644
  xints = 0.0
584
645
  p1x, p1y = polygon[0]
585
- for i in numba.prange(polygon_vertices + 1):
586
- p2x, p2y = polygon[i % polygon_vertices]
646
+ for i in range(n + 1):
647
+ p2x, p2y = polygon[i % n]
587
648
  if y > min(p1y, p2y):
588
649
  if y <= max(p1y, p2y):
589
650
  if x <= max(p1x, p2x):
@@ -592,11 +653,12 @@ def point_in_polygon(x, y, polygon):
592
653
  if p1x == p2x or x <= xints:
593
654
  inside = not inside
594
655
  p1x, p1y = p2x, p2y
656
+
595
657
  return inside
596
658
 
597
659
 
598
660
  @njit
599
- def point_in_triangle(x, y, triangle):
661
+ def point_in_triangle(x, y, triangle): # pragma: no cover
600
662
  """Calculates if a point in within a triangle in 2D.
601
663
 
602
664
  arguments:
@@ -666,7 +728,7 @@ def point_in_triangle(x, y, triangle):
666
728
  return inside
667
729
 
668
730
 
669
- @njit
731
+ @njit(parallel = True) # pragma: no cover
670
732
  def points_in_polygon(points: np.ndarray, polygon: np.ndarray, points_xlen: int, polygon_num: int = 0) -> np.ndarray:
671
733
  """Calculates which points are within a polygon in 2D.
672
734
 
@@ -683,17 +745,18 @@ def points_in_polygon(points: np.ndarray, polygon: np.ndarray, points_xlen: int,
683
745
  note:
684
746
  the polygon is assumed closed, the closing point should not be repeated
685
747
  """
686
- polygon_points = np.empty((0, 3), dtype = numba.int32)
748
+ polygon_points = np.full((len(points), 3), -1, dtype = np.int32)
687
749
  for point_num in numba.prange(len(points)):
688
- p = point_in_polygon(points[point_num, 0], points[point_num, 1], polygon)
750
+ x, y = points[point_num]
751
+ p = point_in_polygon(x, y, polygon)
689
752
  if p is True:
690
753
  j, i = divmod(point_num, points_xlen)
691
- polygon_points = np.append(polygon_points, np.array([[polygon_num, j, i]], dtype = numba.int32), axis = 0)
754
+ polygon_points[point_num] = [polygon_num, j, i]
692
755
 
693
- return polygon_points
756
+ return polygon_points[polygon_points[:, 0] != -1]
694
757
 
695
758
 
696
- @njit
759
+ @njit # pragma: no cover
697
760
  def points_in_triangle(points: np.ndarray, triangle: np.ndarray, points_xlen: int, triangle_num: int = 0) -> np.ndarray:
698
761
  """Calculates which points are within a triangle in 2D.
699
762
 
@@ -707,19 +770,17 @@ def points_in_triangle(points: np.ndarray, triangle: np.ndarray, points_xlen: in
707
770
  triangle_points (np.ndarray): 2D array containing only the points within the triangle,
708
771
  with each row being the triangle number, points y index, and points x index.
709
772
  """
710
- triangle_points = np.empty((0, 3), dtype = numba.int32)
773
+ triangle_points = np.full((len(points), 3), -1, dtype = np.int32)
711
774
  for point_num in numba.prange(len(points)):
712
775
  p = point_in_triangle(points[point_num, 0], points[point_num, 1], triangle)
713
776
  if p is True:
714
777
  yi, xi = divmod(point_num, points_xlen)
715
- triangle_points = np.append(triangle_points,
716
- np.array([[triangle_num, yi, xi]], dtype = numba.int32),
717
- axis = 0)
778
+ triangle_points[point_num] = [triangle_num, yi, xi]
718
779
 
719
- return triangle_points
780
+ return triangle_points[triangle_points[:, 0] != -1]
720
781
 
721
782
 
722
- @njit
783
+ @njit # pragma: no cover
723
784
  def mesh_points_in_triangle(triangle: np.ndarray,
724
785
  points_xlen: int,
725
786
  points_ylen: int,
@@ -751,7 +812,7 @@ def mesh_points_in_triangle(triangle: np.ndarray,
751
812
  return triangle_points
752
813
 
753
814
 
754
- @njit
815
+ @njit # pragma: no cover
755
816
  def points_in_polygons(points: np.ndarray, polygons: np.ndarray, points_xlen: int) -> np.ndarray:
756
817
  """Calculates which points are within which polygons in 2D.
757
818
 
@@ -793,7 +854,7 @@ def points_in_triangles_njit(points: np.ndarray, triangles: np.ndarray, points_x
793
854
  return triangles_points
794
855
 
795
856
 
796
- @njit
857
+ @njit # pragma: no cover
797
858
  def meshgrid(x: np.ndarray, y: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
798
859
  """Returns coordinate matrices from coordinate vectors x and y.
799
860
 
@@ -861,7 +922,7 @@ def points_in_triangles_aligned(nx: int, ny: int, dx: float, dy: float, triangle
861
922
  return triangles_points
862
923
 
863
924
 
864
- @njit
925
+ @njit # pragma: no cover
865
926
  def triangle_box(triangle: np.ndarray) -> Tuple[float, float, float, float]:
866
927
  """Finds the minimum and maximum x and y values of a single traingle.
867
928
 
@@ -907,7 +968,7 @@ def vertical_intercept(x: float, x_values: np.ndarray, y_values: np.ndarray) ->
907
968
  return y
908
969
 
909
970
 
910
- @njit
971
+ @njit # pragma: no cover
911
972
  def points_in_triangles_aligned_optimised(nx: int, ny: int, dx: float, dy: float, triangles: np.ndarray) -> np.ndarray:
912
973
  """Calculates which points are within which triangles in 2D for a regular mesh of aligned points.
913
974
 
@@ -961,7 +1022,7 @@ def triangle_normal_vector(p3):
961
1022
 
962
1023
 
963
1024
  @njit
964
- def triangle_normal_vector_numba(points):
1025
+ def triangle_normal_vector_numba(points): # pragma: no cover
965
1026
  """For a triangle in 3D space, defined by 3 vertex points, returns a unit vector normal to the plane of the triangle.
966
1027
 
967
1028
  note:
@@ -1120,13 +1181,56 @@ def xy_sorted(p, axis = None):
1120
1181
  """
1121
1182
  assert p.ndim >= 2 and p.shape[-1] >= 2
1122
1183
  p = p.reshape((-1, p.shape[-1]))
1123
- xy_range = np.nanmax(p, axis = 0) - np.nanmin(p, axis = 0)
1124
1184
  if axis is None:
1185
+ xy_range = np.nanmax(p, axis = 0) - np.nanmin(p, axis = 0)
1125
1186
  axis = (1 if xy_range[1] > xy_range[0] else 0)
1126
1187
  spi = np.argsort(p[:, axis])
1127
1188
  return p[spi], axis
1128
1189
 
1129
1190
 
1191
+ @njit
1192
+ def xy_sorted_njit(p, axis = -1): # pragma: no cover
1193
+ """Returns copy of points p sorted according to x or y (whichever has greater range)."""
1194
+ assert p.ndim >= 2 and p.shape[-1] >= 2
1195
+ p = p.reshape((-1, p.shape[-1]))
1196
+ if axis == -1:
1197
+ xy_range = _nanmax(p) - _nanmin(p)
1198
+ axis = (1 if xy_range[1] > xy_range[0] else 0)
1199
+ points = p[:, axis]
1200
+ spi = np.argsort(points)
1201
+ return p[spi], axis
1202
+
1203
+
1204
+ @njit
1205
+ def _nanmax(array): # pragma: no cover
1206
+ """Numba implementation of np.nanmax with axis = 0."""
1207
+ len0 = array.shape[1]
1208
+ max_array = np.empty(len0)
1209
+ for col in range(len0):
1210
+ col_array = array[:, col]
1211
+ max_val = col_array[0]
1212
+ for val in col_array:
1213
+ if val > max_val and not np.isnan(val):
1214
+ max_val = val
1215
+ max_array[col] = max_val
1216
+ return max_array
1217
+
1218
+
1219
+ @njit
1220
+ def _nanmin(array): # pragma: no cover
1221
+ """Numba implementation of np.nanmin with axis = 0."""
1222
+ len0 = array.shape[1]
1223
+ min_array = np.empty(len0)
1224
+ for col in range(len0):
1225
+ col_array = array[:, col]
1226
+ min_val = col_array[0]
1227
+ for val in col_array:
1228
+ if val < min_val and not np.isnan(val):
1229
+ min_val = val
1230
+ min_array[col] = min_val
1231
+ return min_array
1232
+
1233
+
1130
1234
  def _projected_xyz_axes(projection):
1131
1235
  assert projection in ['xy', 'xz', 'yz'], f'invalid projection {projection}'
1132
1236
  a0 = 'xyz'.index(projection[0])
@@ -382,6 +382,7 @@ def get_well_pointers(
382
382
  words = line.split()
383
383
  assert len(words) >= 2, "Missing date after TIME keyword."
384
384
  date = words[1]
385
+ date_obj = None
385
386
  if '(' in date:
386
387
  # sometimes user specifies (HH:MM:SS) along with date - can separate time from date with this check
387
388
  date = date.split('(')[0]
@@ -390,13 +391,13 @@ def get_well_pointers(
390
391
  date_obj = datetime.datetime.strptime(date, "%m/%d/%Y").date()
391
392
  else:
392
393
  date_obj = datetime.datetime.strptime(date, "%d/%m/%Y").date()
393
- if no_date_replacement is not None and date_obj < no_date_replacement:
394
- raise ValueError(
395
- f"The Zero Date {no_date_replacement} must be before the first wellspec TIME {date_obj}.")
396
- date = date_obj.isoformat()
397
394
  except ValueError:
398
395
  raise ValueError(f"The date found '{date}' does not match the correct format (usa_date_format "
399
396
  f"is {usa_date_format}).")
397
+ if no_date_replacement is not None and date_obj < no_date_replacement:
398
+ raise ValueError(
399
+ f"The Zero Date {no_date_replacement} must be before the first wellspec TIME {date_obj}.")
400
+ date = date_obj.isoformat()
400
401
  time_pointers[file.tell()] = date
401
402
 
402
403
  current_date = None # Before first TIME