resqpy 4.5.0__py3-none-any.whl → 4.6.3__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -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