pygnss 2.1.2__cp314-cp314t-macosx_11_0_arm64.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.
- pygnss/__init__.py +1 -0
- pygnss/_c_ext/src/constants.c +36 -0
- pygnss/_c_ext/src/hatanaka.c +94 -0
- pygnss/_c_ext/src/helpers.c +17 -0
- pygnss/_c_ext/src/klobuchar.c +313 -0
- pygnss/_c_ext/src/mtable_init.c +50 -0
- pygnss/_c_ext.cpython-314t-darwin.so +0 -0
- pygnss/cl.py +148 -0
- pygnss/constants.py +4 -0
- pygnss/decorator.py +47 -0
- pygnss/file.py +36 -0
- pygnss/filter/__init__.py +77 -0
- pygnss/filter/ekf.py +80 -0
- pygnss/filter/models.py +74 -0
- pygnss/filter/particle.py +484 -0
- pygnss/filter/ukf.py +322 -0
- pygnss/geodetic.py +1177 -0
- pygnss/gnss/__init__.py +0 -0
- pygnss/gnss/edit.py +66 -0
- pygnss/gnss/observables.py +43 -0
- pygnss/gnss/residuals.py +43 -0
- pygnss/gnss/types.py +359 -0
- pygnss/hatanaka.py +70 -0
- pygnss/ionex.py +410 -0
- pygnss/iono/__init__.py +47 -0
- pygnss/iono/chapman.py +35 -0
- pygnss/iono/gim.py +131 -0
- pygnss/logger.py +70 -0
- pygnss/nequick.py +57 -0
- pygnss/orbit/__init__.py +0 -0
- pygnss/orbit/kepler.py +63 -0
- pygnss/orbit/tle.py +186 -0
- pygnss/parsers/rtklib/stats.py +166 -0
- pygnss/rinex.py +2161 -0
- pygnss/sinex.py +121 -0
- pygnss/stats.py +75 -0
- pygnss/tensorial.py +50 -0
- pygnss/time.py +350 -0
- pygnss-2.1.2.dist-info/METADATA +129 -0
- pygnss-2.1.2.dist-info/RECORD +44 -0
- pygnss-2.1.2.dist-info/WHEEL +6 -0
- pygnss-2.1.2.dist-info/entry_points.txt +8 -0
- pygnss-2.1.2.dist-info/licenses/LICENSE +21 -0
- pygnss-2.1.2.dist-info/top_level.txt +1 -0
pygnss/geodetic.py
ADDED
|
@@ -0,0 +1,1177 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""
|
|
3
|
+
Module with several geodetic related methods
|
|
4
|
+
"""
|
|
5
|
+
from dataclasses import astuple, dataclass
|
|
6
|
+
import math
|
|
7
|
+
from typing import List, Union, Iterable
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
import pygnss.logger
|
|
12
|
+
|
|
13
|
+
# WGS84 constants
|
|
14
|
+
WGS84_A = 6378137.0
|
|
15
|
+
WGS84_E = 8.1819190842622e-2
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class ElAz:
|
|
20
|
+
elevation: float
|
|
21
|
+
azimuth: float
|
|
22
|
+
|
|
23
|
+
def __iter__(self):
|
|
24
|
+
return iter(astuple(self))
|
|
25
|
+
|
|
26
|
+
def __repr__(self):
|
|
27
|
+
return f'{self.elevation:.3f} {self.azimuth:.3f}'
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class DMS:
|
|
32
|
+
degrees: int
|
|
33
|
+
minutes: int
|
|
34
|
+
seconds: float
|
|
35
|
+
|
|
36
|
+
def __iter__(self):
|
|
37
|
+
return iter(astuple(self))
|
|
38
|
+
|
|
39
|
+
def __repr__(self):
|
|
40
|
+
return f"{self.degrees}º {self.minutes}' {self.seconds:.3f}\""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class XYZ:
|
|
45
|
+
x: float
|
|
46
|
+
y: float
|
|
47
|
+
z: float
|
|
48
|
+
|
|
49
|
+
def is_valid(self):
|
|
50
|
+
EPS_THRESHOLD = 1.0e-3
|
|
51
|
+
return abs(self) > EPS_THRESHOLD
|
|
52
|
+
|
|
53
|
+
def __iter__(self):
|
|
54
|
+
return iter(astuple(self))
|
|
55
|
+
|
|
56
|
+
def __repr__(self):
|
|
57
|
+
return f'{self.x:.3f} {self.y:.3f} {self.z:.3f}'
|
|
58
|
+
|
|
59
|
+
def __sub__(self, ref: 'XYZ') -> 'XYZ':
|
|
60
|
+
return XYZ(self.x - ref.x, self.y - ref.y, self.z - ref.z)
|
|
61
|
+
|
|
62
|
+
def __add__(self, ref: 'XYZ') -> 'XYZ':
|
|
63
|
+
return XYZ(self.x + ref.x, self.y + ref.y, self.z + ref.z)
|
|
64
|
+
|
|
65
|
+
def __abs__(self):
|
|
66
|
+
return math.sqrt(self.x * self.x + self.y * self.y + self.z * self.z)
|
|
67
|
+
|
|
68
|
+
def __truediv__(self, scale: float) -> 'XYZ':
|
|
69
|
+
return XYZ(self.x / scale, self.y / scale, self.z / scale)
|
|
70
|
+
|
|
71
|
+
def __mul__(self, scale: float) -> 'XYZ':
|
|
72
|
+
return XYZ(self.x * scale, self.y * scale, self.z * scale)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class LLA:
|
|
77
|
+
longitude: float # In degrees
|
|
78
|
+
latitude: float # In degrees
|
|
79
|
+
altitude: float # In meters
|
|
80
|
+
|
|
81
|
+
def is_valid(self):
|
|
82
|
+
ALT_THRESHOLD_M = 10.0e3 # meters above/under surface level
|
|
83
|
+
return self.altitude < ALT_THRESHOLD_M and self.altitude > -ALT_THRESHOLD_M
|
|
84
|
+
|
|
85
|
+
def __iter__(self):
|
|
86
|
+
return iter(astuple(self))
|
|
87
|
+
|
|
88
|
+
def __repr__(self):
|
|
89
|
+
return f'{self.longitude:.3f} {self.latitude:.3f} {self.altitude:.3f}'
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass
|
|
93
|
+
class LLA_DMS:
|
|
94
|
+
longitude: DMS
|
|
95
|
+
latitude: DMS
|
|
96
|
+
altitude: float
|
|
97
|
+
|
|
98
|
+
def is_valid(self):
|
|
99
|
+
ALT_THRESHOLD_M = 10.0e3 # meters above/under surface level
|
|
100
|
+
return self.altitude < ALT_THRESHOLD_M and self.altitude > -ALT_THRESHOLD_M
|
|
101
|
+
|
|
102
|
+
def __iter__(self):
|
|
103
|
+
return iter(astuple(self))
|
|
104
|
+
|
|
105
|
+
def __repr__(self):
|
|
106
|
+
return f'{self.longitude} {self.latitude} {self.altitude}'
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass
|
|
110
|
+
class ENU:
|
|
111
|
+
east: float
|
|
112
|
+
north: float
|
|
113
|
+
up: float
|
|
114
|
+
|
|
115
|
+
def __iter__(self):
|
|
116
|
+
return iter(astuple(self))
|
|
117
|
+
|
|
118
|
+
def __repr__(self):
|
|
119
|
+
return f'{self.east:.3f} {self.north:.3f} {self.up:.3f}'
|
|
120
|
+
|
|
121
|
+
def get_horizontal_error(self):
|
|
122
|
+
return get_horizontal_error(self.east, self.north)
|
|
123
|
+
|
|
124
|
+
def get_vertical_error(self):
|
|
125
|
+
return get_vertical_error(self.east, self.north, self.up)
|
|
126
|
+
|
|
127
|
+
def __mul__(self, scale: float) -> 'ENU':
|
|
128
|
+
return ENU(self.east * scale, self.north * scale, self.up * scale)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@dataclass
|
|
132
|
+
class AXU:
|
|
133
|
+
along: float
|
|
134
|
+
cross: float
|
|
135
|
+
up: float
|
|
136
|
+
|
|
137
|
+
def __iter__(self):
|
|
138
|
+
return iter(astuple(self))
|
|
139
|
+
|
|
140
|
+
def __repr__(self):
|
|
141
|
+
return f'{self.along:.3f} {self.cross:.3f} {self.up:.3f}'
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class Coordinates(object):
|
|
145
|
+
"""
|
|
146
|
+
Class to store coordinates and transform among various coordinate systems
|
|
147
|
+
(cartesian and geodetic)
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
def __init__(self, x: float, y: float, z: float) -> None:
|
|
151
|
+
self.point = XYZ(x, y, z)
|
|
152
|
+
|
|
153
|
+
def __eq__(self, other) -> bool:
|
|
154
|
+
"""
|
|
155
|
+
>>> a = Coordinates(0, 0, 0)
|
|
156
|
+
>>> a == a
|
|
157
|
+
True
|
|
158
|
+
|
|
159
|
+
>>> b = Coordinates(100, 0, 0)
|
|
160
|
+
>>> a == b
|
|
161
|
+
False
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
if isinstance(other, Coordinates):
|
|
165
|
+
equal_x = math.isclose(self.point.x, other.point.x)
|
|
166
|
+
equal_y = math.isclose(self.point.y, other.point.y)
|
|
167
|
+
equal_z = math.isclose(self.point.z, other.point.z)
|
|
168
|
+
return (equal_x and equal_y and equal_z)
|
|
169
|
+
|
|
170
|
+
return False
|
|
171
|
+
|
|
172
|
+
def is_valid(self) -> bool:
|
|
173
|
+
"""
|
|
174
|
+
>>> coordinates = Coordinates(0, 0, 0)
|
|
175
|
+
>>> coordinates.is_valid()
|
|
176
|
+
False
|
|
177
|
+
"""
|
|
178
|
+
return self.xyz().is_valid()
|
|
179
|
+
|
|
180
|
+
def xyz(self) -> XYZ:
|
|
181
|
+
"""
|
|
182
|
+
>>> coordinates = Coordinates(4889803.653, 170755.706, 4078049.851)
|
|
183
|
+
>>> coordinates.xyz()
|
|
184
|
+
4889803.653 170755.706 4078049.851
|
|
185
|
+
"""
|
|
186
|
+
return self.point
|
|
187
|
+
|
|
188
|
+
def lla(self, a: float = WGS84_A, e: float = WGS84_E) -> LLA:
|
|
189
|
+
"""
|
|
190
|
+
>>> coordinates = Coordinates(4889803.653, 170755.706, 4078049.851)
|
|
191
|
+
>>> "lon: {:.0f} lat: {:.0f} height: {:.0f}".format(*coordinates.lla())
|
|
192
|
+
'lon: 2 lat: 40 height: 100'
|
|
193
|
+
"""
|
|
194
|
+
return LLA(*xyz_to_lla(*self.point, a=a, e=e))
|
|
195
|
+
|
|
196
|
+
def lla_dms(self, a: float = WGS84_A, e: float = WGS84_E) -> LLA_DMS:
|
|
197
|
+
"""
|
|
198
|
+
>>> coordinates = Coordinates(4889803.653, 170755.706, 4078049.851)
|
|
199
|
+
>>> coord = coordinates.lla_dms()
|
|
200
|
+
>>> coord.longitude.degrees
|
|
201
|
+
1
|
|
202
|
+
>>> coord.longitude.minutes
|
|
203
|
+
59
|
|
204
|
+
"""
|
|
205
|
+
lla = self.lla(a=a, e=e)
|
|
206
|
+
|
|
207
|
+
longitude = convert_decimal_units_to_dms(lla.longitude)
|
|
208
|
+
latitude = convert_decimal_units_to_dms(lla.latitude)
|
|
209
|
+
|
|
210
|
+
return LLA_DMS(longitude, latitude, lla.altitude)
|
|
211
|
+
|
|
212
|
+
def apply_displacement(self, enu: ENU) -> 'Coordinates':
|
|
213
|
+
"""
|
|
214
|
+
>>> coordinates = Coordinates(4787369.149, 183275.989, 4196362.621)
|
|
215
|
+
>>> enu = ENU(0.096,0.010,0.144)
|
|
216
|
+
>>> coordinates.apply_displacement(enu).xyz()
|
|
217
|
+
4787369.247 183276.089 4196362.724
|
|
218
|
+
"""
|
|
219
|
+
displacement = CoordinatesDisplacement(enu=enu)
|
|
220
|
+
displacement.set_reference_position(self)
|
|
221
|
+
xyz = displacement.get_xyz()
|
|
222
|
+
return Coordinates(self.xyz().x + xyz.x, self.xyz().y + xyz.y, self.xyz().z + xyz.z)
|
|
223
|
+
|
|
224
|
+
def __repr__(self) -> str:
|
|
225
|
+
"""
|
|
226
|
+
>>> Coordinates(4889803.653, 170755.706, 4078049.851)
|
|
227
|
+
4889803.653 170755.706 4078049.851
|
|
228
|
+
"""
|
|
229
|
+
return f'{self.point.x:.3f} {self.point.y:.3f} {self.point.z:.3f}'
|
|
230
|
+
|
|
231
|
+
def print_lla(self, a=WGS84_A, e=WGS84_E) -> str:
|
|
232
|
+
"""
|
|
233
|
+
>>> coordinates = Coordinates(4889803.653, 170755.706, 4078049.851)
|
|
234
|
+
>>> coordinates.print_lla()
|
|
235
|
+
'40.0000000N 2.0000000E 100.000m'
|
|
236
|
+
"""
|
|
237
|
+
lla = LLA(*xyz_to_lla(*self.point, a=a, e=e))
|
|
238
|
+
lat_letter = 'N' if lla.latitude else 'S'
|
|
239
|
+
latitude = abs(lla.latitude)
|
|
240
|
+
lon_letter = 'E' if lla.longitude else 'W'
|
|
241
|
+
longitude = abs(lla.longitude)
|
|
242
|
+
return f"{latitude:.7f}{lat_letter} {longitude:.7f}{lon_letter} {lla.altitude:.3f}m"
|
|
243
|
+
|
|
244
|
+
def __sub__(self: 'Coordinates', ref: 'Coordinates') -> 'CoordinatesDisplacement':
|
|
245
|
+
"""
|
|
246
|
+
>>> dest_position = Coordinates(4889804, 170755, 4078049)
|
|
247
|
+
>>> orig_position = Coordinates(4889803, 170757, 4078049)
|
|
248
|
+
>>> (dest_position - orig_position).get_xyz()
|
|
249
|
+
1.000 -2.000 0.000
|
|
250
|
+
"""
|
|
251
|
+
|
|
252
|
+
xyz_self = self.xyz()
|
|
253
|
+
xyz_ref = ref.xyz()
|
|
254
|
+
|
|
255
|
+
xyz = XYZ(x=xyz_self.x - xyz_ref.x, y=xyz_self.y - xyz_ref.y, z=xyz_self.z - xyz_ref.z)
|
|
256
|
+
return CoordinatesDisplacement(xyz=xyz, ref_position=ref)
|
|
257
|
+
|
|
258
|
+
@staticmethod
|
|
259
|
+
def from_lla(longitude_deg: float, latitude_deg: float, height_m: float,
|
|
260
|
+
a: float = WGS84_A, e: float = WGS84_E) -> 'Coordinates':
|
|
261
|
+
"""
|
|
262
|
+
>>> coordinates = Coordinates.from_lla(2, 40, 100)
|
|
263
|
+
>>> coordinates.is_valid()
|
|
264
|
+
True
|
|
265
|
+
"""
|
|
266
|
+
|
|
267
|
+
xyz = lla_to_xyz(longitude_deg, latitude_deg, height_m, a=a, e=e)
|
|
268
|
+
|
|
269
|
+
return Coordinates(*xyz)
|
|
270
|
+
|
|
271
|
+
@staticmethod
|
|
272
|
+
def from_xyz(x: float, y: float, z: float) -> 'Coordinates':
|
|
273
|
+
"""
|
|
274
|
+
>>> coordinates = Coordinates.from_lla(4889803.653, 170755.706, 4078049.851)
|
|
275
|
+
>>> coordinates.is_valid()
|
|
276
|
+
True
|
|
277
|
+
"""
|
|
278
|
+
|
|
279
|
+
return Coordinates(x, y, z)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class CoordinatesDisplacement(object):
|
|
283
|
+
"""
|
|
284
|
+
Representation of a displacement relative to a certain point (Coordinates).
|
|
285
|
+
Use this class to represent deviations such as error vector, lever arms, ...
|
|
286
|
+
"""
|
|
287
|
+
|
|
288
|
+
def __init__(self, xyz: XYZ = None, enu: ENU = None, ref_position: Coordinates = None):
|
|
289
|
+
"""
|
|
290
|
+
>>> c = CoordinatesDisplacement(xyz=XYZ(0.1, 0.1, 0.1))
|
|
291
|
+
>>> c.is_valid()
|
|
292
|
+
True
|
|
293
|
+
"""
|
|
294
|
+
self.xyz = xyz
|
|
295
|
+
self.enu = enu
|
|
296
|
+
self.ref_lla = None
|
|
297
|
+
if ref_position:
|
|
298
|
+
self.set_reference_position(ref_position)
|
|
299
|
+
|
|
300
|
+
def __repr__(self) -> str:
|
|
301
|
+
if self.xyz:
|
|
302
|
+
return f'XYZ: {str(self.xyz)}'
|
|
303
|
+
elif self.enu:
|
|
304
|
+
return f'ENU: {str(self.enu)}'
|
|
305
|
+
else:
|
|
306
|
+
raise Exception('Format not defined')
|
|
307
|
+
|
|
308
|
+
def set_reference_position(self, position: Coordinates) -> None:
|
|
309
|
+
|
|
310
|
+
if not position:
|
|
311
|
+
raise ValueError("Invalid reference position")
|
|
312
|
+
|
|
313
|
+
self.ref_lla = position.lla()
|
|
314
|
+
|
|
315
|
+
if not self.enu:
|
|
316
|
+
self.enu = self.get_enu()
|
|
317
|
+
|
|
318
|
+
def is_valid(self) -> bool:
|
|
319
|
+
"""
|
|
320
|
+
>>> c = CoordinatesDisplacement()
|
|
321
|
+
>>> c.is_valid()
|
|
322
|
+
False
|
|
323
|
+
"""
|
|
324
|
+
return self.xyz is not None or self.enu is not None
|
|
325
|
+
|
|
326
|
+
def check_validity(self) -> None:
|
|
327
|
+
if not self.is_valid():
|
|
328
|
+
raise ValueError("CoordinatesDisplacement object is not valid")
|
|
329
|
+
|
|
330
|
+
def get_xyz(self) -> XYZ:
|
|
331
|
+
"""
|
|
332
|
+
>>> enu = ENU(-0.416, 5.664, -0.081)
|
|
333
|
+
>>> reference_position = Coordinates.from_lla(1.52160871, 41.26531742, 127.0)
|
|
334
|
+
>>> c = CoordinatesDisplacement(enu=enu)
|
|
335
|
+
>>> c.set_reference_position(reference_position)
|
|
336
|
+
>>> xyz = c.get_xyz()
|
|
337
|
+
>>> '{:.03f} {:.03f} {:.03f}'.format(*xyz)
|
|
338
|
+
'-3.784 -0.517 4.204'
|
|
339
|
+
"""
|
|
340
|
+
|
|
341
|
+
self.check_validity()
|
|
342
|
+
|
|
343
|
+
if not self.xyz:
|
|
344
|
+
if not self.ref_lla:
|
|
345
|
+
raise ValueError("Missing reference position in CoordinatesDisplacement to convert frame (ENU to XYZ)")
|
|
346
|
+
xyz_list = enu_to_ecef(self.ref_lla.longitude, self.ref_lla.latitude, *self.enu)
|
|
347
|
+
self.xyz = XYZ(*xyz_list)
|
|
348
|
+
|
|
349
|
+
return self.xyz
|
|
350
|
+
|
|
351
|
+
def get_enu(self) -> ENU:
|
|
352
|
+
"""
|
|
353
|
+
>>> xyz = XYZ(0.1, 0.1, 0.1)
|
|
354
|
+
>>> ref_position = Coordinates(4889803.653, 170755.706, 4078049.851)
|
|
355
|
+
>>> c = CoordinatesDisplacement(xyz=xyz, ref_position=ref_position)
|
|
356
|
+
>>> enu = c.get_enu()
|
|
357
|
+
>>> '{:.03f} {:.03f} {:.03f}'.format(*enu)
|
|
358
|
+
'0.096 0.010 0.144'
|
|
359
|
+
"""
|
|
360
|
+
|
|
361
|
+
self.check_validity()
|
|
362
|
+
|
|
363
|
+
if not self.enu:
|
|
364
|
+
if not self.ref_lla:
|
|
365
|
+
raise ValueError("Missing reference position in CoordinatesDisplacement to convert frame (XYZ to ENU)")
|
|
366
|
+
enu_list = ecef_to_enu(self.ref_lla.longitude, self.ref_lla.latitude, *self.xyz)
|
|
367
|
+
self.enu = ENU(*enu_list)
|
|
368
|
+
|
|
369
|
+
return self.enu
|
|
370
|
+
|
|
371
|
+
def get_absolute_position(self) -> Coordinates:
|
|
372
|
+
"""
|
|
373
|
+
>>> enu = ENU(0.096,0.010,0.144)
|
|
374
|
+
>>> ref_position = Coordinates(4889803.653, 170755.706, 4078049.851)
|
|
375
|
+
>>> c = CoordinatesDisplacement(enu=enu, ref_position=ref_position)
|
|
376
|
+
>>> position = c.get_absolute_position()
|
|
377
|
+
>>> '{:.3f} {:.3f} {:.3f}'.format(*position.xyz())
|
|
378
|
+
'4889803.753 170755.806 4078049.951'
|
|
379
|
+
>>> c_displacement = CoordinatesDisplacement.from_positions(ref_position, position)
|
|
380
|
+
>>> '{:.1f} {:.1f} {:.1f}'.format(*c_displacement.get_xyz())
|
|
381
|
+
'0.1 0.1 0.1'
|
|
382
|
+
"""
|
|
383
|
+
|
|
384
|
+
xyz = lla_to_xyz(self.ref_lla.longitude, self.ref_lla.latitude, self.ref_lla.altitude)
|
|
385
|
+
|
|
386
|
+
dxyz = self.get_xyz()
|
|
387
|
+
|
|
388
|
+
return Coordinates(*(xyz + dxyz))
|
|
389
|
+
|
|
390
|
+
def get_axu(self, direction: ENU) -> AXU:
|
|
391
|
+
"""
|
|
392
|
+
Convert to along-track, cross-track and up for a given direction
|
|
393
|
+
|
|
394
|
+
>>> velocity = ENU(1.0, 1.0, 1)
|
|
395
|
+
>>> error = ENU(1.0, 1.0, 0)
|
|
396
|
+
>>> error = CoordinatesDisplacement(enu=error)
|
|
397
|
+
>>> error.get_axu(velocity)
|
|
398
|
+
1.414 0.000 0.000
|
|
399
|
+
|
|
400
|
+
>>> error = ENU(1.0, 0.0, 0)
|
|
401
|
+
>>> error = CoordinatesDisplacement(enu=error)
|
|
402
|
+
>>> error.get_axu(velocity)
|
|
403
|
+
0.707 -0.707 0.000
|
|
404
|
+
|
|
405
|
+
>>> error = ENU(1.0, -1.0, 0)
|
|
406
|
+
>>> error = CoordinatesDisplacement(enu=error)
|
|
407
|
+
>>> error.get_axu(velocity)
|
|
408
|
+
0.000 -1.414 0.000
|
|
409
|
+
|
|
410
|
+
>>> error = ENU(-1.0, -1.0, 0)
|
|
411
|
+
>>> error = CoordinatesDisplacement(enu=error)
|
|
412
|
+
>>> error.get_axu(velocity)
|
|
413
|
+
-1.414 0.000 0.000
|
|
414
|
+
|
|
415
|
+
>>> error = ENU(0.0, -1.0, 0)
|
|
416
|
+
>>> error = CoordinatesDisplacement(enu=error)
|
|
417
|
+
>>> error.get_axu(velocity)
|
|
418
|
+
-0.707 -0.707 0.000
|
|
419
|
+
"""
|
|
420
|
+
|
|
421
|
+
position = self.get_enu()
|
|
422
|
+
|
|
423
|
+
# Compute angle between the horizontal coordinates vector and the horizontal direction
|
|
424
|
+
v_hor = [direction.east, direction.north, 0]
|
|
425
|
+
v_hor_norm = np.linalg.norm(v_hor)
|
|
426
|
+
|
|
427
|
+
u_v = np.array(v_hor) / v_hor_norm
|
|
428
|
+
|
|
429
|
+
p_hor = [position.east, position.north, 0]
|
|
430
|
+
p_hor_norm = np.linalg.norm(p_hor)
|
|
431
|
+
|
|
432
|
+
theta_rad = math.acos(np.dot(v_hor, p_hor) / v_hor_norm / p_hor_norm)
|
|
433
|
+
|
|
434
|
+
signum = +1 if np.cross(u_v, p_hor)[2] >= 0 else -1
|
|
435
|
+
cross_track = p_hor_norm * math.sin(theta_rad) * signum
|
|
436
|
+
|
|
437
|
+
along_track = p_hor_norm * math.cos(theta_rad)
|
|
438
|
+
|
|
439
|
+
return AXU(along_track, cross_track, position.up)
|
|
440
|
+
|
|
441
|
+
def __abs__(self) -> float:
|
|
442
|
+
"""
|
|
443
|
+
>>> c = CoordinatesDisplacement.from_xyz(0.1, 0.1, 0.1)
|
|
444
|
+
>>> f'{abs(c):.4f}'
|
|
445
|
+
'0.1732'
|
|
446
|
+
"""
|
|
447
|
+
|
|
448
|
+
if not self.is_valid():
|
|
449
|
+
raise ValueError("Cannot compute the magnitude of an empty array")
|
|
450
|
+
|
|
451
|
+
vector = self.xyz or self.enu
|
|
452
|
+
|
|
453
|
+
return math.sqrt(sum([v * v for v in vector]))
|
|
454
|
+
|
|
455
|
+
@staticmethod
|
|
456
|
+
def from_xyz(x: float, y: float, z: float):
|
|
457
|
+
"""
|
|
458
|
+
>>> c = CoordinatesDisplacement.from_xyz(0.1, 0.1, 0.1)
|
|
459
|
+
>>> c.is_valid()
|
|
460
|
+
True
|
|
461
|
+
"""
|
|
462
|
+
return CoordinatesDisplacement(xyz=XYZ(x, y, z))
|
|
463
|
+
|
|
464
|
+
@staticmethod
|
|
465
|
+
def from_positions(from_position: Coordinates, to_positions: Coordinates) -> 'CoordinatesDisplacement':
|
|
466
|
+
"""
|
|
467
|
+
Compute the displacement vector between two positions
|
|
468
|
+
|
|
469
|
+
>>> from_position = Coordinates(0, 0, 0.1)
|
|
470
|
+
>>> to_position = Coordinates(0, 0, 0)
|
|
471
|
+
>>> c = CoordinatesDisplacement.from_positions(from_position, to_position)
|
|
472
|
+
>>> c.is_valid()
|
|
473
|
+
True
|
|
474
|
+
>>> abs(c)
|
|
475
|
+
0.1
|
|
476
|
+
"""
|
|
477
|
+
|
|
478
|
+
from_xyz = from_position.xyz()
|
|
479
|
+
to_xyz = to_positions.xyz()
|
|
480
|
+
dxyz = XYZ(to_xyz.x - from_xyz.x, to_xyz.y - from_xyz.y, to_xyz.z - from_xyz.z)
|
|
481
|
+
|
|
482
|
+
return CoordinatesDisplacement(xyz=dxyz, ref_position=from_position)
|
|
483
|
+
|
|
484
|
+
@staticmethod
|
|
485
|
+
def from_enu(e: float, n: float, u: float):
|
|
486
|
+
"""
|
|
487
|
+
>>> c = CoordinatesDisplacement.from_enu(0.1, 0.1, 0.1)
|
|
488
|
+
>>> c.is_valid()
|
|
489
|
+
True
|
|
490
|
+
"""
|
|
491
|
+
return CoordinatesDisplacement(enu=ENU(e, n, u))
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def get_horizontal_error(x: float, y: float) -> float:
|
|
495
|
+
return math.sqrt(x**2 + y**2)
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def get_vertical_error(x: float, y: float, z: float):
|
|
499
|
+
return math.sqrt(x**2 + y**2 + z**2)
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def lla_to_xyz(longitude_deg: Union[float, Iterable], latitude_deg: Union[float, Iterable],
|
|
503
|
+
height_m: Union[float, Iterable], a: float = WGS84_A, e: float = WGS84_E) -> XYZ:
|
|
504
|
+
"""
|
|
505
|
+
Convert from geodetic coordinates (relative to a reference ellipsoid which
|
|
506
|
+
defaults to WGS84) to Cartesian XYZ-ECEF coordinates.
|
|
507
|
+
|
|
508
|
+
The longitude, latitude and height values can be vectors
|
|
509
|
+
|
|
510
|
+
>>> xyz = lla_to_xyz(9.323302567, 48.685064919, 373.2428)
|
|
511
|
+
>>> [float('{0:.3f}'.format(v)) for v in xyz]
|
|
512
|
+
[4163316.145, 683507.935, 4767789.479]
|
|
513
|
+
|
|
514
|
+
>>> lons = np.array([9.323302567, 9.323335545, 9.323368065])
|
|
515
|
+
>>> lats = np.array([48.685064919, 48.685050295, 48.685036011])
|
|
516
|
+
>>> hgts = np.array([373.2428, 373.2277, 373.2078])
|
|
517
|
+
>>> x, y, z = lla_to_xyz(lons, lats, hgts)
|
|
518
|
+
>>> "{0:.6f}".format(x[0])
|
|
519
|
+
'4163316.144693'
|
|
520
|
+
>>> "{0:.6f}".format(x[1])
|
|
521
|
+
'4163316.946837'
|
|
522
|
+
>>> "{0:.6f}".format(x[2])
|
|
523
|
+
'4163317.723291'
|
|
524
|
+
>>> "{0:.6f}".format(y[0])
|
|
525
|
+
'683507.934738'
|
|
526
|
+
>>> "{0:.6f}".format(y[1])
|
|
527
|
+
'683510.527317'
|
|
528
|
+
>>> "{0:.6f}".format(y[2])
|
|
529
|
+
'683513.081502'
|
|
530
|
+
>>> "{0:.6f}".format(z[0])
|
|
531
|
+
'4767789.478699'
|
|
532
|
+
>>> "{0:.6f}".format(z[1])
|
|
533
|
+
'4767788.393654'
|
|
534
|
+
>>> "{0:.6f}".format(z[2])
|
|
535
|
+
'4767787.329966'
|
|
536
|
+
"""
|
|
537
|
+
|
|
538
|
+
# Convert from degrees to radians if necessary
|
|
539
|
+
lon = np.deg2rad(longitude_deg)
|
|
540
|
+
lat = np.deg2rad(latitude_deg)
|
|
541
|
+
|
|
542
|
+
sinlat = np.sin(lat)
|
|
543
|
+
coslat = np.cos(lat)
|
|
544
|
+
sinlon = np.sin(lon)
|
|
545
|
+
coslon = np.cos(lon)
|
|
546
|
+
e2 = e * e
|
|
547
|
+
|
|
548
|
+
# Intermediate calculation (prime vertical radius of curvature)
|
|
549
|
+
N = a / np.sqrt(1.0 - e2 * sinlat * sinlat)
|
|
550
|
+
nalt = N + height_m
|
|
551
|
+
|
|
552
|
+
x = nalt * coslat * coslon
|
|
553
|
+
y = nalt * coslat * sinlon
|
|
554
|
+
z = ((1.0 - e2) * N + height_m) * sinlat
|
|
555
|
+
|
|
556
|
+
return XYZ(x, y, z)
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def convert_decimal_units_to_dms(value: float) -> DMS:
|
|
560
|
+
"""
|
|
561
|
+
Convert from longitude/latitude to Degrees, minutes and seconds
|
|
562
|
+
>>> latitude_deg = 41.528442316
|
|
563
|
+
>>> dms = convert_decimal_units_to_dms(latitude_deg)
|
|
564
|
+
>>> "{:.0f} {:.0f} {:.7f}".format(*dms)
|
|
565
|
+
'41 31 42.3923376'
|
|
566
|
+
>>> longitude_deg = 2.434318900
|
|
567
|
+
>>> dms = convert_decimal_units_to_dms(longitude_deg)
|
|
568
|
+
>>> "{:.0f} {:.0f} {:.7f}".format(*dms)
|
|
569
|
+
'2 26 3.5480400'
|
|
570
|
+
"""
|
|
571
|
+
|
|
572
|
+
degrees = int(value)
|
|
573
|
+
minutes_float = (value - degrees) * 60
|
|
574
|
+
minutes = int(minutes_float)
|
|
575
|
+
seconds = (minutes_float - minutes) * 60
|
|
576
|
+
|
|
577
|
+
return DMS(degrees, minutes, seconds)
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def xyz_to_lla(x, y, z, a=WGS84_A, e=WGS84_E) -> LLA:
|
|
581
|
+
"""
|
|
582
|
+
Convert from Cartesian XYZ-ECEF coordinates to geodetic coordinates
|
|
583
|
+
(relative to a reference ellipsoid which defaults to WGS84).
|
|
584
|
+
|
|
585
|
+
Output longitude and latitude are expressed in degrees
|
|
586
|
+
|
|
587
|
+
The x, y and z values can be vectors
|
|
588
|
+
|
|
589
|
+
>>> x, y, z = xyz_to_lla(4807314.3520, 98057.0330, 4176767.6160)
|
|
590
|
+
>>> assert math.isclose(x, 1.1685266980613551)
|
|
591
|
+
>>> assert math.isclose(y, 41.170001652314625)
|
|
592
|
+
>>> assert math.isclose(z, 173.4421242615208)
|
|
593
|
+
|
|
594
|
+
>>> xs = np.array([4807314.3520, 4807315.3520, 4807316.3520])
|
|
595
|
+
>>> ys = np.array([98057.0330, 98058.0330, 98059.0330])
|
|
596
|
+
>>> zs = np.array([4176767.6160, 4176768.6160, 4176769.6160])
|
|
597
|
+
>>> lon, lat, height = xyz_to_lla(xs, ys, zs)
|
|
598
|
+
>>> "{0:.7f}".format(lon[0])
|
|
599
|
+
'1.1685267'
|
|
600
|
+
>>> "{0:.7f}".format(lon[1])
|
|
601
|
+
'1.1685384'
|
|
602
|
+
>>> "{0:.7f}".format(lon[2])
|
|
603
|
+
'1.1685500'
|
|
604
|
+
>>> "{0:.7f}".format(lat[0])
|
|
605
|
+
'41.1700017'
|
|
606
|
+
>>> "{0:.7f}".format(lat[1])
|
|
607
|
+
'41.1700024'
|
|
608
|
+
>>> "{0:.7f}".format(lat[2])
|
|
609
|
+
'41.1700031'
|
|
610
|
+
>>> "{0:.7f}".format(height[0])
|
|
611
|
+
'173.4421243'
|
|
612
|
+
>>> "{0:.7f}".format(height[1])
|
|
613
|
+
'174.8683741'
|
|
614
|
+
>>> "{0:.7f}".format(height[2])
|
|
615
|
+
'176.2946241'
|
|
616
|
+
"""
|
|
617
|
+
|
|
618
|
+
a2 = a ** 2 # Squared of radius, for convenience
|
|
619
|
+
e2 = e ** 2 # Squared of eccentricity, for convenience
|
|
620
|
+
|
|
621
|
+
b = np.sqrt(a2 * (1 - e2))
|
|
622
|
+
b2 = b ** 2
|
|
623
|
+
|
|
624
|
+
ep = np.sqrt((a2 - b2) / b2)
|
|
625
|
+
|
|
626
|
+
p = np.sqrt(x ** 2 + y ** 2)
|
|
627
|
+
th = np.arctan2(a * z, b * p)
|
|
628
|
+
|
|
629
|
+
lon = np.arctan2(y, x)
|
|
630
|
+
lon = np.fmod(lon, 2 * np.pi)
|
|
631
|
+
|
|
632
|
+
sinth = np.sin(th)
|
|
633
|
+
costh = np.cos(th)
|
|
634
|
+
|
|
635
|
+
lat = np.arctan2((z + b * (ep ** 2) * (sinth ** 3)), (p - e2 * a * costh ** 3))
|
|
636
|
+
sinlat = np.sin(lat)
|
|
637
|
+
|
|
638
|
+
N = a / np.sqrt(1 - e2 * (sinlat ** 2))
|
|
639
|
+
alt = p / np.cos(lat) - N
|
|
640
|
+
|
|
641
|
+
return LLA(np.rad2deg(lon), np.rad2deg(lat), alt)
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
def body_to_enu_matrix(yaw_deg, pitch_deg, roll_deg):
|
|
645
|
+
"""
|
|
646
|
+
Computes the matrix that transforms from Body frame to ENU
|
|
647
|
+
|
|
648
|
+
This accepts scalars or vector of scalars.
|
|
649
|
+
|
|
650
|
+
Also, the yaw, pitch and roll can be expressed as radians (radians=True)
|
|
651
|
+
or degrees (radians=False)
|
|
652
|
+
"""
|
|
653
|
+
|
|
654
|
+
# Convert from degrees to radians if necessary
|
|
655
|
+
ya = np.deg2rad(yaw_deg)
|
|
656
|
+
pi = np.deg2rad(pitch_deg)
|
|
657
|
+
ro = np.deg2rad(roll_deg)
|
|
658
|
+
|
|
659
|
+
# Compute the sines and cosines, used later to compute the elements
|
|
660
|
+
# of the transformation matrix
|
|
661
|
+
siny = np.sin(ya)
|
|
662
|
+
cosy = np.cos(ya)
|
|
663
|
+
sinp = np.sin(pi)
|
|
664
|
+
cosp = np.cos(pi)
|
|
665
|
+
sinr = np.sin(ro)
|
|
666
|
+
cosr = np.cos(ro)
|
|
667
|
+
|
|
668
|
+
cosy_cosr = cosy * cosr
|
|
669
|
+
siny_cosr = siny * cosr
|
|
670
|
+
sinp_sinr = sinp * sinr
|
|
671
|
+
sinr_siny = sinr * siny
|
|
672
|
+
|
|
673
|
+
# Compute the matrix coefficients
|
|
674
|
+
m = np.zeros((3, 3), dtype=object)
|
|
675
|
+
|
|
676
|
+
m[0, 0] = cosp * cosy
|
|
677
|
+
m[0, 1] = sinp_sinr * cosy - siny_cosr
|
|
678
|
+
m[0, 2] = cosy_cosr * sinp + sinr_siny
|
|
679
|
+
|
|
680
|
+
m[1, 0] = cosp * siny
|
|
681
|
+
m[1, 1] = sinp_sinr * siny + cosr * cosy
|
|
682
|
+
m[1, 2] = siny_cosr * sinp - sinr * cosy
|
|
683
|
+
|
|
684
|
+
m[2, 0] = - sinp
|
|
685
|
+
m[2, 1] = sinr * cosp
|
|
686
|
+
m[2, 2] = cosr * cosp
|
|
687
|
+
|
|
688
|
+
return m
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def body_to_enu(yaw_deg, pitch_deg, roll_deg, x, y, z, matrix=None):
|
|
692
|
+
"""
|
|
693
|
+
Converts from Body reference frame to ENU reference point.
|
|
694
|
+
|
|
695
|
+
This accepts scalars or vector of scalars.
|
|
696
|
+
|
|
697
|
+
Also, the yaw, pitch and roll can be expressed as radians (radians=True)
|
|
698
|
+
or degrees (radians=False)
|
|
699
|
+
|
|
700
|
+
TEST RATIONALE HAS TO BE CHANGED ACCORDING TO THE CHANGE IN THE BODY_TO_ENU_MATRIX RESULT INTERPRETATION:
|
|
701
|
+
BODY_TO_ENU_MATRIX IS ACTUALLY IMPLEMENTING BODY2NED_MATRIX!!!!!!
|
|
702
|
+
|
|
703
|
+
>>> body_to_enu(0, 0, 0, 1, 1, 1)
|
|
704
|
+
(1.0, 1.0, -1.0)
|
|
705
|
+
|
|
706
|
+
>>> enu = body_to_enu(90, 0, 0, 1, 1, 1)
|
|
707
|
+
>>> np.round(enu)
|
|
708
|
+
array([ 1., -1., -1.])
|
|
709
|
+
|
|
710
|
+
>>> enu = body_to_enu(0, 90, 0, 1, 1, 1)
|
|
711
|
+
>>> np.round(enu)
|
|
712
|
+
array([1., 1., 1.])
|
|
713
|
+
|
|
714
|
+
>>> enu = body_to_enu(0, 0, 90, 1, 1, 1)
|
|
715
|
+
>>> np.round(enu)
|
|
716
|
+
array([-1., 1., -1.])
|
|
717
|
+
|
|
718
|
+
>>> yaws = [0] * 3; pitches = [0] * 3; rolls = [0] * 3
|
|
719
|
+
>>> xs = [1] * 3; ys = [1] * 3; zs = [1]*3
|
|
720
|
+
>>> es, ns, us = body_to_enu(yaws, pitches, rolls, xs, ys, zs)
|
|
721
|
+
>>> es
|
|
722
|
+
array([1., 1., 1.])
|
|
723
|
+
>>> ns
|
|
724
|
+
array([1., 1., 1.])
|
|
725
|
+
>>> us
|
|
726
|
+
array([-1., -1., -1.])
|
|
727
|
+
"""
|
|
728
|
+
|
|
729
|
+
if matrix is None:
|
|
730
|
+
m = body_to_enu_matrix(yaw_deg, pitch_deg, roll_deg)
|
|
731
|
+
|
|
732
|
+
n = m[0, 0] * x + m[0, 1] * y + m[0, 2] * z
|
|
733
|
+
e = m[1, 0] * x + m[1, 1] * y + m[1, 2] * z
|
|
734
|
+
d = m[2, 0] * x + m[2, 1] * y + m[2, 2] * z
|
|
735
|
+
|
|
736
|
+
return e, n, -d
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
def enu_to_body(yaw_deg, pitch_deg, roll_deg, e, n, u, matrix=None):
|
|
740
|
+
"""
|
|
741
|
+
Converts from ENU to XYZ Body reference frame
|
|
742
|
+
|
|
743
|
+
This accepts scalars or vector of scalars.
|
|
744
|
+
|
|
745
|
+
Also, the yaw, pitch and roll can be expressed as radians (radians=True)
|
|
746
|
+
or degrees (radians=False)
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
>>> np.round(enu_to_body(0, 0, 0, *body_to_enu(0, 0, 0, 0.2, 0.2, 1.5)), decimals=1)
|
|
750
|
+
array([0.2, 0.2, 1.5])
|
|
751
|
+
"""
|
|
752
|
+
|
|
753
|
+
if matrix is None:
|
|
754
|
+
m = body_to_enu_matrix(yaw_deg, pitch_deg, roll_deg)
|
|
755
|
+
|
|
756
|
+
x = m[0, 0] * n + m[1, 0] * e + m[2, 0] * -u
|
|
757
|
+
y = m[0, 1] * n + m[1, 1] * e + m[2, 1] * -u
|
|
758
|
+
z = m[0, 2] * n + m[1, 2] * e + m[2, 2] * -u
|
|
759
|
+
|
|
760
|
+
return x, y, z
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
def enu_to_ecef_matrix(longitude_deg, latitude_deg):
|
|
764
|
+
"""
|
|
765
|
+
Computes the transformation matrix from ENU to XYZ (ECEF) and returns
|
|
766
|
+
the matrix coefficients stored as a a numpy two-dimensional array
|
|
767
|
+
|
|
768
|
+
This accepts scalars or vector of scalars.
|
|
769
|
+
|
|
770
|
+
The latitude and longitude can be expressed either in radians (radians=True)
|
|
771
|
+
or degrees (radians = True)
|
|
772
|
+
"""
|
|
773
|
+
|
|
774
|
+
n_samples = np.size(longitude_deg)
|
|
775
|
+
|
|
776
|
+
# Compute the sine and cos to avoid recalculations
|
|
777
|
+
sinlon = np.sin(np.deg2rad(longitude_deg))
|
|
778
|
+
coslon = np.cos(np.deg2rad(longitude_deg))
|
|
779
|
+
sinlat = np.sin(np.deg2rad(latitude_deg))
|
|
780
|
+
coslat = np.cos(np.deg2rad(latitude_deg))
|
|
781
|
+
|
|
782
|
+
m = np.zeros((3, 3), dtype=object)
|
|
783
|
+
|
|
784
|
+
# Compute the elemnts of the matrix
|
|
785
|
+
|
|
786
|
+
m[0, 0] = -sinlon
|
|
787
|
+
m[1, 0] = coslon
|
|
788
|
+
m[2, 0] = 0 if n_samples == 1 else np.zeros(n_samples)
|
|
789
|
+
|
|
790
|
+
m[0, 1] = -sinlat * coslon
|
|
791
|
+
m[1, 1] = -sinlat * sinlon
|
|
792
|
+
m[2, 1] = coslat
|
|
793
|
+
|
|
794
|
+
m[0, 2] = coslat * coslon
|
|
795
|
+
m[1, 2] = coslat * sinlon
|
|
796
|
+
m[2, 2] = sinlat
|
|
797
|
+
|
|
798
|
+
return m
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
def enu_to_ecef(longitude_deg, latitude_deg, e, n, u, matrix=None):
|
|
802
|
+
"""
|
|
803
|
+
Converts from East North and Up to Cartesian XYZ ECEF
|
|
804
|
+
|
|
805
|
+
This accepts scalars or vector of scalars.
|
|
806
|
+
|
|
807
|
+
The latitude and longitude can be expressed either in radians (radians=True)
|
|
808
|
+
or degrees (radians = True)
|
|
809
|
+
|
|
810
|
+
>>> enu = (0.5, 0.5, 1.0)
|
|
811
|
+
>>> enu_to_ecef(0, 0, *enu)
|
|
812
|
+
(1.0, 0.5, 0.5)
|
|
813
|
+
|
|
814
|
+
>>> enu = (0.0, 0.0, 1.0)
|
|
815
|
+
>>> enu_to_ecef(0, 0, *enu)
|
|
816
|
+
(1.0, 0.0, 0.0)
|
|
817
|
+
|
|
818
|
+
>>> lons = [90, 180, 270]
|
|
819
|
+
>>> lats = [0, 0, 0]
|
|
820
|
+
>>> es = [-1, -1, +1]
|
|
821
|
+
>>> ns = [1, 1, 1]
|
|
822
|
+
>>> us = [1, -1, -1]
|
|
823
|
+
>>> dxyz = enu_to_ecef(lons, lats, es, ns, us)
|
|
824
|
+
>>> round(dxyz[0][0], 8)
|
|
825
|
+
1.0
|
|
826
|
+
>>> round(dxyz[0][1], 8)
|
|
827
|
+
1.0
|
|
828
|
+
>>> round(dxyz[0][2], 8)
|
|
829
|
+
1.0
|
|
830
|
+
>>> round(dxyz[1][0], 8)
|
|
831
|
+
1.0
|
|
832
|
+
>>> round(dxyz[1][1], 8)
|
|
833
|
+
1.0
|
|
834
|
+
>>> round(dxyz[1][2], 8)
|
|
835
|
+
1.0
|
|
836
|
+
>>> round(dxyz[2][0], 8)
|
|
837
|
+
1.0
|
|
838
|
+
>>> round(dxyz[2][1], 8)
|
|
839
|
+
1.0
|
|
840
|
+
>>> round(dxyz[2][2], 8)
|
|
841
|
+
1.0
|
|
842
|
+
"""
|
|
843
|
+
|
|
844
|
+
if matrix is None:
|
|
845
|
+
m = enu_to_ecef_matrix(longitude_deg, latitude_deg)
|
|
846
|
+
|
|
847
|
+
x = m[0, 0] * e + m[0, 1] * n + m[0, 2] * u
|
|
848
|
+
y = m[1, 0] * e + m[1, 1] * n + m[1, 2] * u
|
|
849
|
+
z = m[2, 0] * e + m[2, 1] * n + m[2, 2] * u
|
|
850
|
+
|
|
851
|
+
return x, y, z
|
|
852
|
+
|
|
853
|
+
|
|
854
|
+
def xyz_to_enu(ref_pos, x, y, z, a=WGS84_A, e=WGS84_E):
|
|
855
|
+
"""
|
|
856
|
+
Converts from Cartesian XYZ ECEF to East North and Up given a reference
|
|
857
|
+
position in Cartesian absolute XYZ ECEF.
|
|
858
|
+
|
|
859
|
+
>>> xyz = [WGS84_A, 0.0, 0.0] # Reference position
|
|
860
|
+
>>> dxyz = (1.0, 0.0, 0.0) # Deviation relative to reference position
|
|
861
|
+
>>> enu = xyz_to_enu(xyz, *dxyz) # Conversion to ENU at reference position
|
|
862
|
+
>>> enu
|
|
863
|
+
(0.0, 0.0, 1.0)
|
|
864
|
+
|
|
865
|
+
>>> enu = (0.5, 0.5, 1.0)
|
|
866
|
+
>>> dxyz = enu_to_ecef(0, 0, *enu)
|
|
867
|
+
>>> dxyz
|
|
868
|
+
(1.0, 0.5, 0.5)
|
|
869
|
+
>>> xyz_to_enu(xyz, *dxyz) == enu
|
|
870
|
+
True
|
|
871
|
+
"""
|
|
872
|
+
|
|
873
|
+
lla = xyz_to_lla(*ref_pos, a=a, e=e)
|
|
874
|
+
|
|
875
|
+
return ecef_to_enu(lla.longitude, lla.latitude, x, y, z)
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
def ecef_to_enu(longitude_deg, latitude_deg, x, y, z, matrix=None):
|
|
879
|
+
"""
|
|
880
|
+
Converts from Cartesian XYZ ECEF to East North and Up
|
|
881
|
+
|
|
882
|
+
This accepts scalars or vector of scalars.
|
|
883
|
+
|
|
884
|
+
The latitude and longitude can be expressed either in radians (radians=True)
|
|
885
|
+
or degrees (radians = True)
|
|
886
|
+
|
|
887
|
+
>>> dxyz = (1, 0, 0)
|
|
888
|
+
>>> ecef_to_enu(0, 0, *dxyz)
|
|
889
|
+
(0.0, 0.0, 1.0)
|
|
890
|
+
|
|
891
|
+
>>> dxyz = (1, 0.5, 0.5)
|
|
892
|
+
>>> ecef_to_enu(0, 0, *dxyz)
|
|
893
|
+
(0.5, 0.5, 1.0)
|
|
894
|
+
|
|
895
|
+
>>> enu = (0.5, 0.5, 1)
|
|
896
|
+
>>> dxyz = enu_to_ecef(0, 0, *enu)
|
|
897
|
+
>>> dxyz
|
|
898
|
+
(1.0, 0.5, 0.5)
|
|
899
|
+
>>> ecef_to_enu(0, 0, *dxyz) == enu
|
|
900
|
+
True
|
|
901
|
+
"""
|
|
902
|
+
|
|
903
|
+
if matrix is None:
|
|
904
|
+
m = enu_to_ecef_matrix(longitude_deg, latitude_deg)
|
|
905
|
+
|
|
906
|
+
e = m[0, 0] * x + m[1, 0] * y + m[2, 0] * z
|
|
907
|
+
n = m[0, 1] * x + m[1, 1] * y + m[2, 1] * z
|
|
908
|
+
u = m[0, 2] * x + m[1, 2] * y + m[2, 2] * z
|
|
909
|
+
|
|
910
|
+
return e, n, u
|
|
911
|
+
|
|
912
|
+
|
|
913
|
+
def body_to_ecef(longitude, latitude, yaw, pitch, roll, x, y, z):
|
|
914
|
+
"""
|
|
915
|
+
Transform from body fixed reference frame to Cartesian XYZ-ECEF
|
|
916
|
+
"""
|
|
917
|
+
|
|
918
|
+
enu = body_to_enu(yaw, pitch, roll, x, y, z)
|
|
919
|
+
|
|
920
|
+
return enu_to_ecef(longitude, latitude, *enu)
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
def transpose_pos_geodetic(longitude, latitude, height, yaw, pitch, roll, lx, ly, lz):
|
|
924
|
+
"""
|
|
925
|
+
Apply lever arm (l=[lx,ly,lz]) to position (longitude,latitude, height) according to rover attitude (yaw,pitch,roll)
|
|
926
|
+
"""
|
|
927
|
+
|
|
928
|
+
leverArmEnu = body_to_enu(yaw, pitch, roll, lx, ly, lz)
|
|
929
|
+
leverArmNed = (leverArmEnu[1], leverArmEnu[0], -1 * leverArmEnu[2])
|
|
930
|
+
|
|
931
|
+
deltaPos = small_perturbation_cart_to_geodetic(leverArmNed[0], leverArmNed[1], leverArmNed[2], latitude, height)
|
|
932
|
+
|
|
933
|
+
return latitude + deltaPos[0], longitude + deltaPos[1], height + deltaPos[2]
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
def small_perturbation_cart_to_geodetic(n, e, d, latitude_deg, h):
|
|
937
|
+
"""
|
|
938
|
+
Transform small perturbations from cartesian (in local navigation frame NED) to geodetic, small angle
|
|
939
|
+
aproximation has to apply, which means norm(n,e,d) <<< Earth Radius
|
|
940
|
+
"""
|
|
941
|
+
|
|
942
|
+
lat = np.deg2rad(latitude_deg)
|
|
943
|
+
|
|
944
|
+
sinlat = np.sin(lat)
|
|
945
|
+
|
|
946
|
+
RE = WGS84_A / np.sqrt((1 - (sinlat ** 2) * (WGS84_E ** 2)) ** 3)
|
|
947
|
+
RN = WGS84_A * (1 - WGS84_E ** 2) / np.sqrt(1 - (sinlat ** 2) * (WGS84_E ** 2))
|
|
948
|
+
|
|
949
|
+
deltaLat = n / (RN + h)
|
|
950
|
+
deltaLon = e / ((RE + h) * np.cos(lat))
|
|
951
|
+
deltaH = -1 * d
|
|
952
|
+
|
|
953
|
+
return np.rad2deg(deltaLat), np.rad2deg(deltaLon), deltaH
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
def haversine(lon1_deg, lat1_deg, lon2_deg, lat2_deg, r=6371):
|
|
957
|
+
"""
|
|
958
|
+
Calculate the great circle distance between two points
|
|
959
|
+
on the earth (specified in decimal degrees) using the Haversine formula
|
|
960
|
+
|
|
961
|
+
Return units: kilometers
|
|
962
|
+
|
|
963
|
+
Extracted from http://stackoverflow.com/questions/4913349
|
|
964
|
+
|
|
965
|
+
>>> haversine(0, 0, 0, 0)
|
|
966
|
+
0.0
|
|
967
|
+
|
|
968
|
+
Example extracted from https://rosettacode.org/wiki/Haversine_formula
|
|
969
|
+
|
|
970
|
+
>>> haversine(-86.67, 36.12, -118.40, 33.94, r=6371.8)
|
|
971
|
+
2886.8068907353736
|
|
972
|
+
|
|
973
|
+
Arrays can also be used (for a vectorized computation)
|
|
974
|
+
>>> haversine([2, 2, 2], [89, 40, 0], [3, 3, 3], [89, 40, 0])
|
|
975
|
+
array([ 1.94059443, 85.17980895, 111.19492664])
|
|
976
|
+
"""
|
|
977
|
+
# convert decimal degrees to radians
|
|
978
|
+
lon1, lat1, lon2, lat2 = map(np.radians, [lon1_deg, lat1_deg, lon2_deg, lat2_deg])
|
|
979
|
+
|
|
980
|
+
# haversine formula
|
|
981
|
+
dlon = lon2 - lon1
|
|
982
|
+
dlat = lat2 - lat1
|
|
983
|
+
a = np.sin(dlat / 2) ** 2 + np.cos(lat1) * np.cos(lat2) * np.sin(dlon / 2) ** 2
|
|
984
|
+
c = 2 * np.arcsin(np.sqrt(a))
|
|
985
|
+
|
|
986
|
+
# Return a float if input is a single float (to avoid test failures due to numpy float64)
|
|
987
|
+
if np.ndim(lon1_deg) == 0:
|
|
988
|
+
c = float(c)
|
|
989
|
+
|
|
990
|
+
return c * r
|
|
991
|
+
|
|
992
|
+
|
|
993
|
+
def area_triangle_in_sphere(coords, r=6371):
|
|
994
|
+
"""
|
|
995
|
+
|
|
996
|
+
Area = pi*R^2*E/180
|
|
997
|
+
|
|
998
|
+
R = radius of sphere
|
|
999
|
+
E = spherical excess of triangle, E = A + B + C - 180
|
|
1000
|
+
A, B, C = angles of spherical triangle in degrees
|
|
1001
|
+
|
|
1002
|
+
tan(E/4) = sqrt(tan(s/2)*tan((s-a)/2)*tan((s-b)/2)*tan((s-c)/2))
|
|
1003
|
+
|
|
1004
|
+
where
|
|
1005
|
+
|
|
1006
|
+
a, b, c = sides of spherical triangle
|
|
1007
|
+
s = (a + b + c)/2
|
|
1008
|
+
|
|
1009
|
+
http://mathforum.org/library/drmath/view/65316.html
|
|
1010
|
+
|
|
1011
|
+
# Compute approximate area of India (3287263 km2)
|
|
1012
|
+
>>> pWest = (68.752441, 23.483401)
|
|
1013
|
+
>>> pEast = (90.153809, 22.87744)
|
|
1014
|
+
>>> pNorth = (76.723022, 32.87036)
|
|
1015
|
+
>>> pSouth = (77.45636, 8.119053)
|
|
1016
|
+
>>> triangle1 = (pWest,pEast,pNorth)
|
|
1017
|
+
>>> triangle2 = (pWest,pEast,pSouth)
|
|
1018
|
+
>>> area_triangle_in_sphere(triangle1) + area_triangle_in_sphere(triangle2)
|
|
1019
|
+
3028215.293314756
|
|
1020
|
+
"""
|
|
1021
|
+
|
|
1022
|
+
# Compute the spherical areas using the haversine method
|
|
1023
|
+
a = haversine(coords[0][0], coords[0][1], coords[1][0], coords[1][1], r=1)
|
|
1024
|
+
b = haversine(coords[1][0], coords[1][1], coords[2][0], coords[2][1], r=1)
|
|
1025
|
+
c = haversine(coords[0][0], coords[0][1], coords[2][0], coords[2][1], r=1)
|
|
1026
|
+
|
|
1027
|
+
s = (a + b + c) / 2.0
|
|
1028
|
+
|
|
1029
|
+
tanE = math.sqrt(math.tan(s / 2) * math.tan((s - a) / 2) * math.tan((s - b) / 2) * math.tan((s - c) / 2))
|
|
1030
|
+
|
|
1031
|
+
E = math.atan(tanE * 4.0)
|
|
1032
|
+
|
|
1033
|
+
area = r * r * E
|
|
1034
|
+
|
|
1035
|
+
return area
|
|
1036
|
+
|
|
1037
|
+
|
|
1038
|
+
class Dop(object):
|
|
1039
|
+
"""
|
|
1040
|
+
Class that implements computation of Dilution of Precision (DOP)
|
|
1041
|
+
|
|
1042
|
+
|
|
1043
|
+
Check that dop from NMEA GPGSV is the same as the one in NMEA GPGSA message
|
|
1044
|
+
(further investigations need to be conducted if NMEA generated by u-blox NEO 6P,
|
|
1045
|
+
discrepancies have been found)
|
|
1046
|
+
|
|
1047
|
+
$GPGSA,A,3,10,07,05,02,29,04,08,13,,,,,1.72,1.03,1.38*0A
|
|
1048
|
+
$GPGSV,3,1,11,10,63,137,17,07,61,098,15,05,59,290,20,08,54,157,30*70
|
|
1049
|
+
$GPGSV,3,2,11,02,39,223,19,13,28,070,17,26,23,252,,04,14,186,14*79
|
|
1050
|
+
$GPGSV,3,3,11,29,09,301,24,16,09,020,,36,,,*76
|
|
1051
|
+
|
|
1052
|
+
>>> elAz = [ElAz(63,137),
|
|
1053
|
+
... ElAz(61, 98),
|
|
1054
|
+
... ElAz(59,290),
|
|
1055
|
+
... ElAz(39,223),
|
|
1056
|
+
... ElAz( 9,301),
|
|
1057
|
+
... ElAz(14,186),
|
|
1058
|
+
... ElAz(54,157),
|
|
1059
|
+
... ElAz(28, 70)]
|
|
1060
|
+
>>> dop = Dop(elAz)
|
|
1061
|
+
>>> f'{dop.pdop():.2f}'
|
|
1062
|
+
'1.72'
|
|
1063
|
+
>>> f'{dop.vdop():.2f}'
|
|
1064
|
+
'1.38'
|
|
1065
|
+
>>> f'{dop.hdop():.2f}'
|
|
1066
|
+
'1.04'
|
|
1067
|
+
|
|
1068
|
+
|
|
1069
|
+
Verification through the example of DOP computation proposed
|
|
1070
|
+
in Exercise 6-5 (page 228) of:
|
|
1071
|
+
|
|
1072
|
+
Misra, P., Enge, P., "Global Positioning System: Signals,
|
|
1073
|
+
Measurements and Performance", 2nd Edition. 2006
|
|
1074
|
+
|
|
1075
|
+
Increasing 30 degrees the azimuth position of 3 sats (+ one in zenith), doubles the vdop value
|
|
1076
|
+
leaving pdop approximately constant
|
|
1077
|
+
|
|
1078
|
+
>>> el_az_list = [ElAz( 0, 0),
|
|
1079
|
+
... ElAz( 0, 120),
|
|
1080
|
+
... ElAz( 0, 240),
|
|
1081
|
+
... ElAz(90, 0)]
|
|
1082
|
+
>>> dop = Dop(el_az_list)
|
|
1083
|
+
>>> f'{dop.vdop():.2f}'
|
|
1084
|
+
'1.15'
|
|
1085
|
+
>>> f'{dop.hdop():.2f}'
|
|
1086
|
+
'1.15'
|
|
1087
|
+
|
|
1088
|
+
>>> el_az_list = [ElAz(30, 0),
|
|
1089
|
+
... ElAz(30, 120),
|
|
1090
|
+
... ElAz(30, 240),
|
|
1091
|
+
... ElAz(90, 0)]
|
|
1092
|
+
>>> dop = Dop(el_az_list)
|
|
1093
|
+
>>> f'{dop.vdop():.2f}'
|
|
1094
|
+
'2.31'
|
|
1095
|
+
>>> f'{dop.hdop():.2f}'
|
|
1096
|
+
'1.33'
|
|
1097
|
+
"""
|
|
1098
|
+
|
|
1099
|
+
def __init__(self, el_az_list: List[ElAz]) -> None:
|
|
1100
|
+
|
|
1101
|
+
self.el_az_list = el_az_list
|
|
1102
|
+
self.__G() # generate G matrix
|
|
1103
|
+
self.__Q() # generate Q matrix
|
|
1104
|
+
|
|
1105
|
+
def __G(self) -> None:
|
|
1106
|
+
"""
|
|
1107
|
+
Construct geometry matrix G
|
|
1108
|
+
"""
|
|
1109
|
+
|
|
1110
|
+
elevations = np.radians([v.elevation for v in self.el_az_list])
|
|
1111
|
+
azimuths = np.radians([v.azimuth for v in self.el_az_list])
|
|
1112
|
+
|
|
1113
|
+
n_satellites = len(self.el_az_list)
|
|
1114
|
+
|
|
1115
|
+
cosel = np.cos(elevations)
|
|
1116
|
+
gx = cosel * np.sin(azimuths)
|
|
1117
|
+
gy = cosel * np.cos(azimuths)
|
|
1118
|
+
gz = np.sin(elevations)
|
|
1119
|
+
|
|
1120
|
+
rows = list(zip(gx, gy, gz, [1] * n_satellites))
|
|
1121
|
+
|
|
1122
|
+
self.G = np.array(rows)
|
|
1123
|
+
|
|
1124
|
+
def __Q(self) -> None:
|
|
1125
|
+
"""
|
|
1126
|
+
Construct co-factor matrix
|
|
1127
|
+
"""
|
|
1128
|
+
try:
|
|
1129
|
+
self.Q = np.linalg.inv(np.dot(np.matrix.transpose(self.G), self.G))
|
|
1130
|
+
except np.linalg.linalg.LinAlgError as e:
|
|
1131
|
+
self.Q = np.full([4, 4], np.nan)
|
|
1132
|
+
pygnss.logger.warning("Could not compute DOP. Reason {}".format(e))
|
|
1133
|
+
|
|
1134
|
+
def gdop(self) -> float:
|
|
1135
|
+
"""
|
|
1136
|
+
Geometric DOP
|
|
1137
|
+
"""
|
|
1138
|
+
|
|
1139
|
+
return np.sqrt(self.Q[0, 0] + self.Q[1, 1] + self.Q[2, 2] + self.Q[3, 3])
|
|
1140
|
+
|
|
1141
|
+
def pdop(self) -> float:
|
|
1142
|
+
"""
|
|
1143
|
+
Position DOP
|
|
1144
|
+
"""
|
|
1145
|
+
|
|
1146
|
+
return np.sqrt(self.Q[0, 0] + self.Q[1, 1] + self.Q[2, 2])
|
|
1147
|
+
|
|
1148
|
+
def tdop(self) -> float:
|
|
1149
|
+
"""
|
|
1150
|
+
Time DOP
|
|
1151
|
+
"""
|
|
1152
|
+
|
|
1153
|
+
return np.sqrt(self.Q[3, 3])
|
|
1154
|
+
|
|
1155
|
+
def vdop(self) -> float:
|
|
1156
|
+
"""
|
|
1157
|
+
Vertical DOP
|
|
1158
|
+
"""
|
|
1159
|
+
|
|
1160
|
+
return np.sqrt(self.Q[2, 2])
|
|
1161
|
+
|
|
1162
|
+
def hdop(self) -> float:
|
|
1163
|
+
"""
|
|
1164
|
+
Horizontal DOP
|
|
1165
|
+
"""
|
|
1166
|
+
|
|
1167
|
+
return np.sqrt(self.Q[0, 0] + self.Q[1, 1])
|
|
1168
|
+
|
|
1169
|
+
def __repr__(self) -> str:
|
|
1170
|
+
|
|
1171
|
+
return f'DOP - position: {self.pdop():.3f} horizontal: {self.hdop():.3f} vertical: {self.vdop():.3f}'
|
|
1172
|
+
|
|
1173
|
+
|
|
1174
|
+
if __name__ == "__main__":
|
|
1175
|
+
import doctest
|
|
1176
|
+
|
|
1177
|
+
doctest.testmod(raise_on_error=True)
|