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.
- tectonic_utils/.DS_Store +0 -0
- tectonic_utils/__init__.py +3 -0
- tectonic_utils/cover_picture.png +0 -0
- tectonic_utils/geodesy/.DS_Store +0 -0
- tectonic_utils/geodesy/.ruff_cache/.gitignore +1 -0
- tectonic_utils/geodesy/.ruff_cache/0.1.5/15663111236935520357 +0 -0
- tectonic_utils/geodesy/.ruff_cache/CACHEDIR.TAG +1 -0
- tectonic_utils/geodesy/__init__.py +0 -0
- tectonic_utils/geodesy/datums.py +156 -0
- tectonic_utils/geodesy/euler_pole.py +170 -0
- tectonic_utils/geodesy/fault_vector_functions.py +383 -0
- tectonic_utils/geodesy/haversine.py +193 -0
- tectonic_utils/geodesy/insar_vector_functions.py +285 -0
- tectonic_utils/geodesy/linear_elastic.py +231 -0
- tectonic_utils/geodesy/test/.DS_Store +0 -0
- tectonic_utils/geodesy/test/__init__.py +0 -0
- tectonic_utils/geodesy/test/test_conversion_functions.py +74 -0
- tectonic_utils/geodesy/test/test_euler_poles.py +33 -0
- tectonic_utils/geodesy/test/test_insar_vector_functions.py +36 -0
- tectonic_utils/geodesy/utilities.py +47 -0
- tectonic_utils/geodesy/xyz2llh.py +220 -0
- tectonic_utils/read_write/.DS_Store +0 -0
- tectonic_utils/read_write/.ruff_cache/.gitignore +1 -0
- tectonic_utils/read_write/.ruff_cache/0.1.5/680373307893520726 +0 -0
- tectonic_utils/read_write/.ruff_cache/CACHEDIR.TAG +1 -0
- tectonic_utils/read_write/__init__.py +0 -0
- tectonic_utils/read_write/general_io.py +55 -0
- tectonic_utils/read_write/netcdf_read_write.py +382 -0
- tectonic_utils/read_write/read_kml.py +68 -0
- tectonic_utils/read_write/test/.DS_Store +0 -0
- tectonic_utils/read_write/test/__init__.py +0 -0
- tectonic_utils/read_write/test/example_grd.grd +0 -0
- tectonic_utils/read_write/test/test_conversion_functions.py +40 -0
- tectonic_utils/read_write/test/written_example.grd +0 -0
- tectonic_utils/seismo/.DS_Store +0 -0
- tectonic_utils/seismo/.ruff_cache/.gitignore +1 -0
- tectonic_utils/seismo/.ruff_cache/0.1.5/12911000862714636977 +0 -0
- tectonic_utils/seismo/.ruff_cache/CACHEDIR.TAG +1 -0
- tectonic_utils/seismo/MT_calculations.py +132 -0
- tectonic_utils/seismo/__init__.py +0 -0
- tectonic_utils/seismo/moment_calculations.py +44 -0
- tectonic_utils/seismo/second_focal_plane.py +138 -0
- tectonic_utils/seismo/test/.DS_Store +0 -0
- tectonic_utils/seismo/test/__init__.py +0 -0
- tectonic_utils/seismo/test/test_WC.py +19 -0
- tectonic_utils/seismo/test/test_second_focal_plane.py +16 -0
- tectonic_utils/seismo/wells_and_coppersmith.py +167 -0
- tectonic_utils-0.1.2.dist-info/LICENSE.md +21 -0
- tectonic_utils-0.1.2.dist-info/METADATA +82 -0
- tectonic_utils-0.1.2.dist-info/RECORD +51 -0
- 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]
|