tectonic-utils 0.1.2__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 (51) hide show
  1. tectonic_utils/.DS_Store +0 -0
  2. tectonic_utils/__init__.py +3 -0
  3. tectonic_utils/cover_picture.png +0 -0
  4. tectonic_utils/geodesy/.DS_Store +0 -0
  5. tectonic_utils/geodesy/.ruff_cache/.gitignore +1 -0
  6. tectonic_utils/geodesy/.ruff_cache/0.1.5/15663111236935520357 +0 -0
  7. tectonic_utils/geodesy/.ruff_cache/CACHEDIR.TAG +1 -0
  8. tectonic_utils/geodesy/__init__.py +0 -0
  9. tectonic_utils/geodesy/datums.py +156 -0
  10. tectonic_utils/geodesy/euler_pole.py +170 -0
  11. tectonic_utils/geodesy/fault_vector_functions.py +383 -0
  12. tectonic_utils/geodesy/haversine.py +193 -0
  13. tectonic_utils/geodesy/insar_vector_functions.py +285 -0
  14. tectonic_utils/geodesy/linear_elastic.py +231 -0
  15. tectonic_utils/geodesy/test/.DS_Store +0 -0
  16. tectonic_utils/geodesy/test/__init__.py +0 -0
  17. tectonic_utils/geodesy/test/test_conversion_functions.py +74 -0
  18. tectonic_utils/geodesy/test/test_euler_poles.py +33 -0
  19. tectonic_utils/geodesy/test/test_insar_vector_functions.py +36 -0
  20. tectonic_utils/geodesy/utilities.py +47 -0
  21. tectonic_utils/geodesy/xyz2llh.py +220 -0
  22. tectonic_utils/read_write/.DS_Store +0 -0
  23. tectonic_utils/read_write/.ruff_cache/.gitignore +1 -0
  24. tectonic_utils/read_write/.ruff_cache/0.1.5/680373307893520726 +0 -0
  25. tectonic_utils/read_write/.ruff_cache/CACHEDIR.TAG +1 -0
  26. tectonic_utils/read_write/__init__.py +0 -0
  27. tectonic_utils/read_write/general_io.py +55 -0
  28. tectonic_utils/read_write/netcdf_read_write.py +382 -0
  29. tectonic_utils/read_write/read_kml.py +68 -0
  30. tectonic_utils/read_write/test/.DS_Store +0 -0
  31. tectonic_utils/read_write/test/__init__.py +0 -0
  32. tectonic_utils/read_write/test/example_grd.grd +0 -0
  33. tectonic_utils/read_write/test/test_conversion_functions.py +40 -0
  34. tectonic_utils/read_write/test/written_example.grd +0 -0
  35. tectonic_utils/seismo/.DS_Store +0 -0
  36. tectonic_utils/seismo/.ruff_cache/.gitignore +1 -0
  37. tectonic_utils/seismo/.ruff_cache/0.1.5/12911000862714636977 +0 -0
  38. tectonic_utils/seismo/.ruff_cache/CACHEDIR.TAG +1 -0
  39. tectonic_utils/seismo/MT_calculations.py +132 -0
  40. tectonic_utils/seismo/__init__.py +0 -0
  41. tectonic_utils/seismo/moment_calculations.py +44 -0
  42. tectonic_utils/seismo/second_focal_plane.py +138 -0
  43. tectonic_utils/seismo/test/.DS_Store +0 -0
  44. tectonic_utils/seismo/test/__init__.py +0 -0
  45. tectonic_utils/seismo/test/test_WC.py +19 -0
  46. tectonic_utils/seismo/test/test_second_focal_plane.py +16 -0
  47. tectonic_utils/seismo/wells_and_coppersmith.py +167 -0
  48. tectonic_utils-0.1.2.dist-info/LICENSE.md +21 -0
  49. tectonic_utils-0.1.2.dist-info/METADATA +82 -0
  50. tectonic_utils-0.1.2.dist-info/RECORD +51 -0
  51. tectonic_utils-0.1.2.dist-info/WHEEL +4 -0
@@ -0,0 +1,383 @@
1
+ """
2
+ Useful utilities for defining fault planes and coordinate systems.
3
+ """
4
+
5
+ import numpy as np
6
+ import math
7
+ from collections.abc import Iterable
8
+ from Tectonic_Utils.geodesy import haversine, utilities
9
+
10
+
11
+ def xy2lonlat_single(xi, yi, reflon, reflat):
12
+ """
13
+ Convert cartesian x/y coordinate into lon/lat coordinate using reference point.
14
+
15
+ :param xi: x coordinate of target point, in km
16
+ :type xi: float
17
+ :param yi: y coordinate of target point, in km
18
+ :type yi: float
19
+ :param reflon: longitude of reference point
20
+ :type reflon: float
21
+ :param reflat: latitude of reference point
22
+ :type reflat: float
23
+ :returns: lon, lat of target point
24
+ :rtype: float, float
25
+ """
26
+ lat = reflat + (yi * 1 / 111.000)
27
+ lon = reflon + (xi * 1 / (111.000 * abs(np.cos(np.deg2rad(reflat)))))
28
+ return lon, lat
29
+
30
+
31
+ def latlon2xy_single(loni, lati, lon0, lat0):
32
+ """
33
+ Convert lon/lat coordinate into cartesian x/y coordinate using reference point.
34
+
35
+ :param loni: longitude of target point
36
+ :type loni: float
37
+ :param lati: latitude of target point
38
+ :type lati: float
39
+ :param lon0: longitude of reference point
40
+ :type lon0: float
41
+ :param lat0: latitude of reference point
42
+ :type lat0: float
43
+ :returns: x, y of target point, in km
44
+ :rtype: float, float
45
+ """
46
+ radius = haversine.distance([lat0, lon0], [lati, loni])
47
+ bearing = haversine.calculate_initial_compass_bearing((lat0, lon0), (lati, loni))
48
+ azimuth = 90 - bearing
49
+ x = radius * np.cos(np.deg2rad(azimuth))
50
+ y = radius * np.sin(np.deg2rad(azimuth))
51
+ return x, y
52
+
53
+
54
+ def xy2lonlat(xi, yi, reflon, reflat):
55
+ """
56
+ Convert cartesian x/y coordinates into lon/lat coordinates using reference point.
57
+
58
+ :param xi: x coordinate of target point(s), in km
59
+ :type xi: float or list
60
+ :param yi: y coordinate of target point(s), in km
61
+ :type yi: float or list
62
+ :param reflon: longitude of reference point
63
+ :type reflon: float
64
+ :param reflat: latitude of reference point
65
+ :type reflat: float
66
+ :returns: lon, lat of target point(s)
67
+ :rtype: list, list (float, float in case of single inputs)
68
+ """
69
+
70
+ if not isinstance(xi, Iterable):
71
+ if not isinstance(yi, Iterable): # if single value, return single value
72
+ return xy2lonlat_single(xi, yi, reflon, reflat)
73
+ else:
74
+ raise ValueError(f"Error! Dimension of x and y does not agree: {xi} {yi}" )
75
+ if len(list(xi)) != len(list(yi)):
76
+ raise ValueError(f'Error! Length of x and y does not agree: {len(list(xi))} {len(list(yi))}')
77
+
78
+ # if we are getting a list of values, we return a list of the same dimensions
79
+ lat, lon = [], []
80
+ for a, b in zip(xi, yi):
81
+ loni, lati = xy2lonlat_single(a, b, reflon, reflat)
82
+ lon.append(loni)
83
+ lat.append(lati)
84
+ return lon, lat
85
+
86
+
87
+ def latlon2xy(loni, lati, lon0, lat0):
88
+ """
89
+ Convert lon/lat coordinates into cartesian x/y coordinates using reference point.
90
+
91
+ :param loni: longitude of target point(s)
92
+ :type loni: float or list
93
+ :param lati: latitude of target point(s)
94
+ :type lati: float or list
95
+ :param lon0: longitude of reference point
96
+ :type lon0: float
97
+ :param lat0: latitude of reference point
98
+ :type lat0: float
99
+ :returns: x, y of target point(s), in km
100
+ :rtype: list, list (float, float in case of single inputs)
101
+ """
102
+ if not isinstance(loni, Iterable):
103
+ if not isinstance(lati, Iterable): # if single value, return single value
104
+ return latlon2xy_single(loni, lati, lon0, lat0)
105
+ else:
106
+ raise ValueError(f"Error! Dimension of x and y does not agree: {loni} {lati}" )
107
+ if len(list(loni)) != len(list(lati)):
108
+ raise ValueError(f'Error! Length of x and y does not agree: {len(list(loni))} {len(list(lati))}')
109
+
110
+ # If we are getting a list, return a list of the same dimensions
111
+ x, y = [], []
112
+ for a, b in zip(loni, lati):
113
+ xi, yi = latlon2xy_single(a, b, lon0, lat0)
114
+ x.append(xi)
115
+ y.append(yi)
116
+ return x, y
117
+
118
+
119
+ def get_plane_normal(strike, dip):
120
+ """
121
+ Get the outward-facing unit normal to a plane of specified strike and dip (dip-cross-strike).
122
+
123
+ :param strike: strike, degrees
124
+ :type strike: float
125
+ :param dip: dip, degrees
126
+ :type dip: float
127
+ :return: 3-component vector of outward facing unit normal vector, [x, y, z]
128
+ :rtype: np.ndarray
129
+ """
130
+ # Inside, we first find the orthogonal unit vectors
131
+ # aligned with strike and dip directions that sit within the plane. The plane normal is their cross product,
132
+ # i.e. the outward facing unit normal vector, dip-cross-strike, in x-y-z coordinates.
133
+ strike_vector = get_strike_vector(strike) # unit vector
134
+ dip_vector = get_dip_vector(strike, dip) # unit vector
135
+ plane_normal = simple_cross_product(dip_vector, strike_vector) # dip x strike
136
+ # for outward facing normal, by right-hand rule.
137
+ return plane_normal
138
+
139
+
140
+ def simple_cross_product(a, b):
141
+ """
142
+ Implement the simple cross product for 2 vectors in 3-D space.
143
+
144
+ :param a: array-like, 3-component vector
145
+ :param b: array-like, 3-component vector
146
+ :returns: 3-component array, [x, y, z]
147
+ :rtype: [float, float, float]
148
+ """
149
+ s1 = a[1]*b[2] - a[2]*b[1]
150
+ s2 = a[2]*b[0] - a[0]*b[2]
151
+ s3 = a[0]*b[1] - a[1]*b[0]
152
+ return [s1, s2, s3]
153
+
154
+
155
+ def get_dip_degrees(x0, y0, z0, x1, y1, z1):
156
+ """
157
+ Get the dip of the line that connects two points.
158
+
159
+ :param x0: x coordinate of shallower point
160
+ :type x0: float
161
+ :param y0: y coordinate of shallower point
162
+ :type y0: float
163
+ :param z0: z coordinate of shallower point
164
+ :type z0: float
165
+ :param x1: x coordinate of deeper point
166
+ :type x1: float
167
+ :param y1: y coordinate of deeper point
168
+ :type y1: float
169
+ :param z1: z coordinate of deeper point
170
+ :type z1: float
171
+ :returns: dip, in degrees
172
+ :rtype: float
173
+ """
174
+ horizontal_length = get_strike_length(x0, x1, y0, y1)
175
+ vertical_distance = abs(z1 - z0)
176
+ dip = np.rad2deg(math.atan2(vertical_distance, horizontal_length))
177
+ return dip
178
+
179
+
180
+ def get_strike_vector(strike):
181
+ """
182
+ Get a unit vector along the strike direction of a plane.
183
+
184
+ :param strike: strike, in degrees CW from N
185
+ :type strike: float
186
+ :returns: 3-component unit vector, in x, y, z coordinates
187
+ :rtype: [float, float, float]
188
+ """
189
+ theta = np.deg2rad(90 - strike)
190
+ strike_vector = [np.cos(theta), np.sin(theta), 0]
191
+ return strike_vector
192
+
193
+
194
+ def get_dip_vector(strike, dip):
195
+ """
196
+ Get a unit vector along the dip direction of a plane.
197
+
198
+ :param strike: strike, in degrees CW from N
199
+ :type strike: float
200
+ :param dip: dip, in degrees
201
+ :type dip: float
202
+ :returns: 3-component unit vector, in x, y, z coordinates
203
+ :rtype: [float, float, float]
204
+ """
205
+ downdip_direction_theta = np.deg2rad(-strike) # theta(strike+90)
206
+ dip_unit_vector_z = np.sin(np.deg2rad(dip)) # the vertical component of the downdip unit vector
207
+ dip_unit_vector_xy = np.sqrt(1-dip_unit_vector_z*dip_unit_vector_z) # horizontal component of downdip unit vector
208
+ dip_vector = [dip_unit_vector_xy * np.cos(downdip_direction_theta),
209
+ dip_unit_vector_xy * np.sin(downdip_direction_theta), -dip_unit_vector_z]
210
+ return dip_vector
211
+
212
+
213
+ def get_rtlat_dip_slip(slip, rake):
214
+ """
215
+ Decompose slip into right lateral and reverse dip slip components.
216
+
217
+ :param slip: slip, in any length unit
218
+ :type slip: float
219
+ :param rake: rake, in degrees
220
+ :type rake: float
221
+ :returns: rt-lat strike slip and reverse dip slip, in the same length units as `slip`
222
+ :rtype: float, float
223
+ """
224
+ rt_strike_slip = -slip * np.cos(np.deg2rad(rake)) # negative sign for convention of right lateral slip
225
+ dip_slip = slip * np.sin(np.deg2rad(rake))
226
+ return rt_strike_slip, dip_slip
227
+
228
+
229
+ def get_leftlat_reverse_slip(slip, rake):
230
+ """
231
+ Decompose slip into left lateral and reverse slip components.
232
+
233
+ :param slip: slip, in any length unit
234
+ :type slip: float
235
+ :param rake: rake, in degrees
236
+ :type rake: float
237
+ :returns: left-lat strike slip and reverse dip slip, in the same length units as `slip`
238
+ :rtype: float, float
239
+ """
240
+ ll_strike_slip = slip * np.cos(np.deg2rad(rake)) # convention of left lateral slip
241
+ dip_slip = slip * np.sin(np.deg2rad(rake))
242
+ return ll_strike_slip, dip_slip
243
+
244
+
245
+ def get_strike(deltax, deltay):
246
+ """
247
+ Compute the strike of a vector x,y.
248
+
249
+ :param deltax: displacement in x direction, in any length unit
250
+ :type deltax: float
251
+ :param deltay: displacement in y direction, in any length unit
252
+ :type deltay: float
253
+ :returns: strike of vector, in CW degrees from north
254
+ :rtype: float
255
+ """
256
+ slope = math.atan2(deltay, deltax)
257
+ strike = 90 - np.rad2deg(slope)
258
+ if strike < 0:
259
+ strike = strike + 360
260
+ return strike
261
+
262
+
263
+ def get_downdip_width(top, bottom, dip):
264
+ """
265
+ Get total downdip-width of a rectangular fault plane given top depth, bottom depth, and dip.
266
+
267
+ :param top: depth of top of fault plane, in km (positive down)
268
+ :type top: float
269
+ :param bottom: depth of top of fault plane, in km (positive down)
270
+ :type bottom: float
271
+ :param dip: dip of fault plane, in degrees (range 0 to 90)
272
+ :type dip: float
273
+ :returns: total down-dip width of the rectangle, in km
274
+ :rtype: float
275
+ """
276
+ W = abs(top - bottom) / np.sin(np.deg2rad(dip)) # guaranteed to be between 0 and 90
277
+ return W
278
+
279
+
280
+ def get_total_slip(strike_slip, dip_slip):
281
+ """Just the pythagorean theorem."""
282
+ return np.sqrt(strike_slip * strike_slip + dip_slip * dip_slip)
283
+
284
+
285
+ def get_strike_length(x0, x1, y0, y1):
286
+ """Just the pythagorean theorem."""
287
+ length = np.sqrt((x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0))
288
+ return length
289
+
290
+
291
+ def get_top_bottom_from_center(center_depth, width, dip):
292
+ """
293
+ Get the top and bottom depth of a rectangular fault from its width, center, and dip.
294
+
295
+ :param center_depth: depth of center of fault plane, in km (positive down)
296
+ :type center_depth: float
297
+ :param width: total downdip width of rectangular fault plane, in km
298
+ :type width: float
299
+ :param dip: dip of fault plane, in degrees (range 0 to 90)
300
+ :type dip: float
301
+ :returns: top and bottom of fault plane, in km
302
+ :rtype: float, float
303
+ """
304
+ top = center_depth - (width / 2.0 * np.sin(np.deg2rad(dip)))
305
+ bottom = center_depth + (width / 2.0 * np.sin(np.deg2rad(dip)))
306
+ return top, bottom
307
+
308
+
309
+ def get_top_bottom_from_top(top_depth, width, dip):
310
+ """
311
+ Get the top and bottom depth of a rectangular fault from its width, top-edge depth, and dip.
312
+
313
+ :param top_depth: depth of top edge of fault plane, in km (positive down)
314
+ :type top_depth: float
315
+ :param width: total downdip width of rectangular fault plane, in km
316
+ :type width: float
317
+ :param dip: dip of fault plane, in degrees (range 0 to 90)
318
+ :type dip: float
319
+ :returns: top and bottom of fault plane, in km
320
+ :rtype: float, float
321
+ """
322
+ bottom = top_depth + (width * np.sin(np.deg2rad(dip)))
323
+ return top_depth, bottom
324
+
325
+
326
+ def get_vector_magnitude(vector):
327
+ """
328
+ Get magnitude of a vector.
329
+
330
+ :param vector: n-component vector, any units
331
+ :type vector: array_like
332
+ :return: magnitude
333
+ :rtype: float
334
+ """
335
+ return utilities.get_vector_magnitude(vector)
336
+
337
+
338
+ def get_unit_vector(vec):
339
+ """
340
+ Get unit vector.
341
+
342
+ :param vec: 3-component vector, any units
343
+ :type vec: array_like
344
+ :return: unit vector
345
+ :rtype: array_like
346
+ """
347
+ return utilities.get_unit_vector(vec)
348
+
349
+
350
+ def add_vector_to_point(x0, y0, vector_mag, vector_heading):
351
+ """
352
+ :param x0: starting x-coordinate for vector
353
+ :type x0: float
354
+ :param y0: starting y-coordinate for vector
355
+ :type y0: float
356
+ :param vector_mag: magnitude of vector to be added to point
357
+ :type vector_mag: float
358
+ :param vector_heading: direction of vector, in degrees CW from north
359
+ :type vector_heading: float
360
+ :returns: x1, y1 coordinates of ending point
361
+ :rtype: float, float
362
+ """
363
+ theta = np.deg2rad(90 - vector_heading)
364
+ x1 = x0 + vector_mag * np.cos(theta)
365
+ y1 = y0 + vector_mag * np.sin(theta)
366
+ return x1, y1
367
+
368
+
369
+ def get_rake(rtlat_strike_slip, dip_slip):
370
+ """
371
+ Return the rake of a given slip vector.
372
+ Positive strike-slip is right lateral, and positive dip-slip is reverse.
373
+ Will return 0 if dipslip,strikeslip == 0,0.
374
+
375
+ :param rtlat_strike_slip: quantity of right lateral slip, any length units
376
+ :type rtlat_strike_slip: float
377
+ :param dip_slip: quantity of reverse slip, any length units
378
+ :type dip_slip: float
379
+ :return: rake in range -180 to 180 degrees
380
+ :rtype: float
381
+ """
382
+ rake = np.rad2deg(math.atan2(dip_slip, -rtlat_strike_slip)) # Aki and Richards definition: positive ll
383
+ return rake
@@ -0,0 +1,193 @@
1
+ """
2
+ Haversine formula example in Python. Original author Wayne Dyck.
3
+ """
4
+
5
+ import numpy as np
6
+ import math
7
+
8
+
9
+ def distance(origin, destination, radius=6371):
10
+ """
11
+ Computes the distance between origin [lat1, lon1] and destination [lat2, lon2].
12
+
13
+ :param origin: Tuple representing (latitude, longitude) of first point, in decimal degrees
14
+ :type origin: array_like
15
+ :param destination: Tuple representing (latitude, longitude) of second point, in decimal degrees
16
+ :type destination: array_like
17
+ :param radius: float, radius of Earth in km, default 6371
18
+ :return: distance, in km
19
+ :rtype: float
20
+ """
21
+ lat1, lon1 = origin
22
+ lat2, lon2 = destination
23
+
24
+ dlat = math.radians(lat2-lat1)
25
+ dlon = math.radians(lon2-lon1)
26
+ a = math.sin(dlat/2) * math.sin(dlat/2) + math.cos(math.radians(lat1)) \
27
+ * math.cos(math.radians(lat2)) * math.sin(dlon/2) * math.sin(dlon/2)
28
+ c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a))
29
+ d = radius * c
30
+
31
+ return d
32
+
33
+
34
+ def distance_vectorized(origin, destination, radius=6371.0):
35
+ """
36
+ Calculate the great-circle distance between pairs of points using numpy vectorized operations.
37
+
38
+ Parameters:
39
+ - origin: ndarray of shape (N, 2) with columns [lat, lon] in degrees
40
+ - destination: ndarray of shape (N, 2) with columns [lat, lon] in degrees
41
+ - radius: radius of the Earth in kilometers (default: 6371)
42
+
43
+ Returns:
44
+ - distances: ndarray of shape (N,) with distances in kilometers
45
+ """
46
+ lat1 = np.radians(origin[:, 0])
47
+ lon1 = np.radians(origin[:, 1])
48
+ lat2 = np.radians(destination[:, 0])
49
+ lon2 = np.radians(destination[:, 1])
50
+
51
+ dlat = lat2 - lat1
52
+ dlon = lon2 - lon1
53
+
54
+ a = np.sin(dlat / 2.0) ** 2 + np.cos(lat1) * np.cos(lat2) * np.sin(dlon / 2.0) ** 2
55
+ c = 2 * np.arctan2(np.sqrt(a), np.sqrt(1 - a))
56
+
57
+ dist = radius * c
58
+ return dist
59
+
60
+
61
+ def calculate_initial_compass_bearing(pointA, pointB):
62
+ r"""
63
+ Calculate the bearing between two points.
64
+ By the formula
65
+
66
+ .. math:: \theta = atan2(\sin(\Delta_{long})*\cos(lat_2), \cos(lat_1)*\sin(lat_2) -
67
+ \sin(lat_1)*\cos(lat_2)*\cos(\Delta_{long})).
68
+
69
+ :param pointA: Tuple representing (latitude, longitude) of first point, in decimal degrees
70
+ :type pointA: array_like
71
+ :param pointB: Tuple representing (latitude, longitude) of second point, in decimal degrees
72
+ :type pointB: array_like
73
+ :return: bearing, in degrees CW from north
74
+ :rtype: float
75
+ """
76
+ if not isinstance(pointA, tuple) or not isinstance(pointB, tuple):
77
+ raise TypeError("Only tuples are supported as arguments")
78
+
79
+ lat1 = math.radians(pointA[0])
80
+ lat2 = math.radians(pointB[0])
81
+
82
+ diffLong = math.radians(pointB[1] - pointA[1])
83
+
84
+ x = math.sin(diffLong) * math.cos(lat2)
85
+ y = math.cos(lat1) * math.sin(lat2) - (math.sin(lat1) * math.cos(lat2) * math.cos(diffLong))
86
+
87
+ initial_bearing = math.atan2(x, y)
88
+
89
+ # Now we have the initial bearing but math.atan2 return values
90
+ # from -180 to + 180 which is not what we want for a compass bearing
91
+ # The solution is to normalize the initial bearing as shown below
92
+ initial_bearing = math.degrees(initial_bearing)
93
+ compass_bearing = (initial_bearing + 360) % 360
94
+
95
+ return compass_bearing
96
+
97
+
98
+ def calculate_initial_compass_bearing_vectorized(pointA, pointB):
99
+ """
100
+ Calculate initial compass bearing from pointA to pointB using numpy vectorized operations.
101
+
102
+ Parameters:
103
+ - pointA: ndarray of shape (N, 2) with [lat, lon] in degrees
104
+ - pointB: ndarray of shape (N, 2) with [lat, lon] in degrees
105
+
106
+ Returns:
107
+ - bearings: ndarray of shape (N,) with compass bearings in degrees [0, 360)
108
+ """
109
+ lat1 = np.radians(pointA[:, 0])
110
+ lat2 = np.radians(pointB[:, 0])
111
+ diff_long = np.radians(pointB[:, 1] - pointA[:, 1])
112
+
113
+ x = np.sin(diff_long) * np.cos(lat2)
114
+ y = np.cos(lat1) * np.sin(lat2) - (
115
+ np.sin(lat1) * np.cos(lat2) * np.cos(diff_long)
116
+ )
117
+
118
+ initial_bearing = np.arctan2(x, y)
119
+ initial_bearing = np.degrees(initial_bearing)
120
+ compass_bearing = (initial_bearing + 360) % 360
121
+
122
+ return compass_bearing
123
+
124
+
125
+ def calculate_endpoint_given_bearing(origin, bearing, angular_distance_degrees):
126
+ """
127
+ Travel a certain angular distance (degrees) along a bearing starting from a coordinate.
128
+ Source: https://www.movable-type.co.uk/scripts/latlong.html
129
+
130
+ :param origin: Tuple representing (latitude, longitude) of first point, in decimal degrees
131
+ :type origin: array_like
132
+ :param bearing: angle clockwise from north, in decimal degrees
133
+ :type bearing: float
134
+ :param angular_distance_degrees: angular distance, in decimal degrees
135
+ :type angular_distance_degrees: float
136
+ :return: [lat, lon], in degrees
137
+ :rtype: [float, float]
138
+ """
139
+ # Internally, phi2, lambda2 are the latitude and longitude of the destination point.
140
+ # theta is bearing in radians.
141
+ # delta is the angular distance in radians, d/R (d = distance, R = radius of Earth)
142
+
143
+ lat0 = origin[0]
144
+ lon0 = origin[1]
145
+ phi1 = np.deg2rad(lat0)
146
+ lambda1 = np.deg2rad(lon0)
147
+ delta = np.deg2rad(angular_distance_degrees)
148
+ theta = np.deg2rad(bearing)
149
+ phi2 = np.arcsin((np.sin(phi1)*np.cos(delta)) + (np.cos(phi1)*np.sin(delta)*np.cos(theta)))
150
+ lambda2 = lambda1 + np.arctan2(np.sin(theta)*np.sin(delta)*np.cos(phi1), np.cos(delta)-np.sin(phi1)*np.sin(phi2))
151
+ lat2 = np.rad2deg(phi2)
152
+ lon2 = np.rad2deg(lambda2)
153
+ destination = [lat2, lon2]
154
+ return destination
155
+
156
+
157
+ def xy_distance(ref_loc, sta_loc):
158
+ """
159
+ Pythagorean distance between two latitude/longitude pairs, assuming flat surface between the points.
160
+ Returns x and y in meters.
161
+
162
+ :param ref_loc: Tuple representing (latitude, longitude) of first point, in decimal degrees
163
+ :type ref_loc: array_like
164
+ :param sta_loc: Tuple representing (latitude, longitude) of second point, in decimal degrees
165
+ :type sta_loc: array_like
166
+ :return: [distance_x, distance_y], in m
167
+ :rtype: [float, float]
168
+ """
169
+ radius = distance(ref_loc, sta_loc) # in km
170
+ bearing = calculate_initial_compass_bearing((ref_loc[0], ref_loc[1]), (sta_loc[0], sta_loc[1]))
171
+ azimuth = 90 - bearing
172
+ x = radius * np.cos(np.deg2rad(azimuth)) * 1000 # in m
173
+ y = radius * np.sin(np.deg2rad(azimuth)) * 1000 # in m
174
+ return [x, y]
175
+
176
+
177
+ def add_vector_to_coords(lon0, lat0, dx, dy):
178
+ """Add a vector of km to a set of latitude/longitude points.
179
+
180
+ :param lon0: Longitude of initial point, in degrees
181
+ :type lon0: float
182
+ :param lat0: Latitude of initial point, in degrees
183
+ :type lat0: float
184
+ :param dx: x component of vector added to the point, in km
185
+ :type dx: float
186
+ :param dy: y component of vector added to the point, in km
187
+ :type dy: float
188
+ :return: [lon1, lat1], in degrees
189
+ :rtype: [float, float]
190
+ """
191
+ lat1 = lat0+dy/111.000
192
+ lon1 = lon0+dx/(111.000*np.cos(np.deg2rad(lat0)))
193
+ return [lon1, lat1]