Skip to content

Commit 29ace4c

Browse files
committed
Move unit vector code to coordinates.
1 parent 60efa44 commit 29ace4c

5 files changed

Lines changed: 183 additions & 66 deletions

File tree

gnss_lib_py/utils/coordinates.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,3 +570,34 @@ def wrap_0_to_2pi(angles):
570570
angles = np.mod(angles, 2*np.pi)
571571

572572
return angles
573+
574+
575+
def el_az_to_enu_unit_vector(el_deg, az_deg):
576+
"""
577+
Convert elevation and azimuth to ENU unit vectors.
578+
579+
Parameters
580+
----------
581+
el_deg : np.ndarray
582+
Elevation angle in degrees.
583+
584+
az_deg : np.ndarray
585+
Azimuth angle in degrees.
586+
587+
Returns
588+
-------
589+
unit_dir_mat : np.ndarray
590+
ENU unit vectors.
591+
"""
592+
el_rad = np.deg2rad(el_deg)
593+
az_rad = np.deg2rad(az_deg)
594+
595+
unit_dir_mat = np.vstack(
596+
(np.atleast_2d(np.cos(el_rad) * np.sin(az_rad)),
597+
np.atleast_2d(np.cos(el_rad) * np.cos(az_rad)),
598+
np.atleast_2d(np.sin(el_rad))
599+
)).T
600+
601+
return unit_dir_mat
602+
603+

gnss_lib_py/utils/dop.py

Lines changed: 54 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from gnss_lib_py.navdata.navdata import NavData
1111
from gnss_lib_py.navdata.operations import loop_time
12+
from gnss_lib_py.utils.coordinates import el_az_to_enu_unit_vector
1213

1314

1415
def get_dop(navdata, **which_dop):
@@ -114,60 +115,70 @@ def get_dop(navdata, **which_dop):
114115
return dop_navdata
115116

116117

117-
def calculate_dop(derived):
118-
"""Calculate DOP from elevation and azimuth (ENU).
119118

119+
def calculate_enu_dop_matrix(derived):
120+
"""
121+
Calculate the DOP matrix from elevation and azimuth (ENU).
122+
120123
Parameters
121124
----------
122125
derived : gnss_lib_py.navdata.navdata.NavData
123126
NavData instance containing received GNSS measurements for a
124127
particular time instance, contains elevation and azimuth angle
125128
information for an estimated location.
126-
129+
127130
Returns
128131
-------
129-
dop : Dict
130-
Dilution of precision, with DOP type as the keys: "HDOP", "VDOP",
131-
"TDOP", "PDOP", "GDOP".
132+
dop_matrix : np.ndarray (4, 4)
133+
DOP matrix.
132134
"""
133135

134136
# Use the elevation and azimuth angles to get the ENU and Time matrix
135137
# Each row is [d_e, d_n, d_u, 1] for each satellite.
136138
enut_matrix = _calculate_enut_matrix(derived)
137139
enut_gram_matrix = enut_matrix.T @ enut_matrix
138-
139-
try:
140-
dop_matrix = np.linalg.inv(enut_gram_matrix)
141-
142-
# Calculate the DOP
143-
dop = {}
144-
dop["dop_matrix"] = dop_matrix
145-
dop["GDOP"] = np.sqrt(np.trace(dop_matrix))
146-
dop["HDOP"] = np.sqrt(dop_matrix[0, 0] + dop_matrix[1, 1])
147-
dop["VDOP"] = np.sqrt(dop_matrix[2, 2])
148-
dop["PDOP"] = np.sqrt(dop_matrix[0, 0] + \
149-
dop_matrix[1, 1] + \
150-
dop_matrix[2, 2])
151-
dop["TDOP"] = np.sqrt(dop_matrix[3, 3])
152140

141+
# Calculate the DOP matrix
142+
try:
143+
dop_matrix = np.linalg.inv(enut_gram_matrix)
153144
except np.linalg.LinAlgError:
154145
# If the matrix is singular, return NaNs for the DOP
155-
dop = {}
156-
dop["dop_matrix"] = np.nan * np.ones((4, 4))
157-
dop["GDOP"] = np.nan
158-
dop["HDOP"] = np.nan
159-
dop["VDOP"] = np.nan
160-
dop["PDOP"] = np.nan
161-
dop["TDOP"] = np.nan
146+
dop_matrix = np.nan * np.ones((4, 4))
147+
148+
return dop_matrix
149+
150+
151+
def parse_dop(dop_matrix):
152+
"""Calculate DOP types from the DOP matrix.
153+
154+
Parameters
155+
----------
156+
dop_matrix : np.ndarray (4, 4)
157+
DOP matrix in ENU coordinates.
158+
159+
Returns
160+
-------
161+
dop : Dict
162+
Dilution of precision, with DOP type as the keys: "HDOP", "VDOP",
163+
"TDOP", "PDOP", "GDOP".
164+
"""
165+
166+
dop = {}
167+
dop["dop_matrix"] = dop_matrix
168+
dop["GDOP"] = np.sqrt(np.trace(dop_matrix))
169+
dop["HDOP"] = np.sqrt(dop_matrix[0, 0] + dop_matrix[1, 1])
170+
dop["VDOP"] = np.sqrt(dop_matrix[2, 2])
171+
dop["PDOP"] = np.sqrt(dop_matrix[0, 0] + \
172+
dop_matrix[1, 1] + \
173+
dop_matrix[2, 2])
174+
dop["TDOP"] = np.sqrt(dop_matrix[3, 3])
162175

163176
return dop
164177

165178

166-
def calculate_enu_unit_vectors(derived):
179+
def calculate_dop(derived):
167180
"""
168-
Calculate the ENU unit vectors from elevation and azimuth.
169-
Each row is [d_e, d_n, d_u] for each satellite, where d is a unit
170-
line-of-sight vector in the ENU frame.
181+
Calculate the DOP from elevation and azimuth (ENU).
171182
172183
Parameters
173184
----------
@@ -178,21 +189,18 @@ def calculate_enu_unit_vectors(derived):
178189
179190
Returns
180191
-------
181-
unit_dir_mat : np.ndarray (num_satellites, 3)
182-
Matrix of ENU unit vectors.
192+
dop : Dict
193+
Dilution of precision, with DOP type as the keys: "HDOP", "VDOP",
194+
"TDOP", "PDOP", "GDOP".
183195
"""
184196

185-
# Get the elevation and azimuth angles
186-
sv_el_az_rad = np.deg2rad(derived['el_sv_deg', 'az_sv_deg'])
197+
# Calculate the DOP matrix
198+
dop_matrix = calculate_enu_dop_matrix(derived)
187199

188-
# Important: This matrix is in ENU coordinates, not NED.
189-
unit_dir_mat = np.vstack(
190-
(np.atleast_2d(np.cos(sv_el_az_rad[0,:]) * np.sin(sv_el_az_rad[1,:])),
191-
np.atleast_2d(np.cos(sv_el_az_rad[0,:]) * np.cos(sv_el_az_rad[1,:])),
192-
np.atleast_2d(np.sin(sv_el_az_rad[0,:]))
193-
)).T
200+
# Parse the DOP matrix to get the DOP values
201+
dop = parse_dop(dop_matrix)
194202

195-
return unit_dir_mat
203+
return dop
196204

197205

198206
def _calculate_enut_matrix(derived):
@@ -213,8 +221,9 @@ def _calculate_enut_matrix(derived):
213221
Matrix of ENU and Time vectors.
214222
215223
"""
216-
unit_dir_mat = calculate_enu_unit_vectors(derived)
217-
enut_matrix = np.hstack((unit_dir_mat,
218-
np.ones((unit_dir_mat.shape[0], 1))))
224+
enu_unit_dir_mat = el_az_to_enu_unit_vector(derived['el_sv_deg'],
225+
derived['az_sv_deg'])
226+
enut_matrix = np.hstack((enu_unit_dir_mat,
227+
np.ones((enu_unit_dir_mat.shape[0], 1))))
219228

220229
return enut_matrix

notebooks/tutorials/utils/dop.ipynb

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
},
2020
{
2121
"cell_type": "code",
22-
"execution_count": null,
22+
"execution_count": 1,
2323
"metadata": {},
2424
"outputs": [],
2525
"source": [
@@ -38,7 +38,7 @@
3838
},
3939
{
4040
"cell_type": "code",
41-
"execution_count": null,
41+
"execution_count": 2,
4242
"metadata": {},
4343
"outputs": [],
4444
"source": [
@@ -60,9 +60,23 @@
6060
},
6161
{
6262
"cell_type": "code",
63-
"execution_count": null,
63+
"execution_count": 3,
6464
"metadata": {},
65-
"outputs": [],
65+
"outputs": [
66+
{
67+
"name": "stdout",
68+
"output_type": "stream",
69+
"text": [
70+
" gps_millis HDOP VDOP\n",
71+
"0 1.303771e+12 0.558578 0.830517\n",
72+
"1 1.303771e+12 0.549160 0.829362\n",
73+
"2 1.303771e+12 0.558593 0.830452\n",
74+
"3 1.303771e+12 0.549176 0.829296\n",
75+
"4 1.303771e+12 0.549185 0.829263\n",
76+
"5 1.303771e+12 0.549193 0.829230\n"
77+
]
78+
}
79+
],
6680
"source": [
6781
"dop_navdata = glp.get_dop(navdata)\n",
6882
"print(dop_navdata)"

tests/utils/test_coordinates.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@
1212
from pytest_lazyfixture import lazy_fixture
1313

1414
import gnss_lib_py.utils.constants as consts
15+
from gnss_lib_py.navdata.navdata import NavData
1516
from gnss_lib_py.parsers.google_decimeter import AndroidDerived2022
1617
from gnss_lib_py.utils.coordinates import ecef_to_el_az, add_el_az
1718
from gnss_lib_py.utils.coordinates import geodetic_to_ecef
1819
from gnss_lib_py.utils.coordinates import ecef_to_geodetic, LocalCoord
1920
from gnss_lib_py.utils.coordinates import wrap_0_to_2pi
21+
from gnss_lib_py.utils.coordinates import el_az_to_enu_unit_vector
2022
from gnss_lib_py.navdata.operations import loop_time
2123

2224
@pytest.fixture(name="local_ecef")
@@ -457,3 +459,75 @@ def test_wrap_0_to_2pi():
457459
angles_out = np.concatenate((np.linspace(np.pi,7*np.pi/4.,4),
458460
np.linspace(0,3*np.pi/4.,4)))
459461
np.testing.assert_array_almost_equal(wrap_0_to_2pi(angles_in),angles_out)
462+
463+
464+
#######################################
465+
# Unit Vector Tests
466+
467+
@pytest.fixture(name="simple_sat_scenario")
468+
def fixture_simple_sat_scenario():
469+
"""
470+
A simple set of satellites for DOP calculation.
471+
472+
"""
473+
# Create a simple NavData instance
474+
navdata = NavData()
475+
476+
# Add a few satellites
477+
navdata['gps_millis'] = np.array([0, 0, 0, 0, 0], dtype=int)
478+
navdata['el_sv_deg'] = np.array([0, 0, 45, 45, 90], dtype=float)
479+
navdata['az_sv_deg'] = np.array([0, 90, 180, 270, 360], dtype=float)
480+
481+
return navdata
482+
483+
484+
@pytest.fixture(name="simple_sat_expected_enu_unit_vectors")
485+
def fixture_simple_sat_expected_enu_unit_vectors():
486+
"""
487+
The expected ENU unit vectors for the simple satellite scenario.
488+
489+
"""
490+
# Expected ENU unit vectors
491+
divsqrt2 = 1 / np.sqrt(2)
492+
expected_enu_unit_vectors = np.array([[0, 1, 0],
493+
[1, 0, 0],
494+
[0, -divsqrt2, divsqrt2],
495+
[-divsqrt2, 0, divsqrt2],
496+
[0, 0, 1]])
497+
498+
return expected_enu_unit_vectors
499+
500+
@pytest.mark.parametrize('navdata, expected_los_vectors',
501+
[
502+
(lazy_fixture('simple_sat_scenario'),
503+
lazy_fixture('simple_sat_expected_enu_unit_vectors'))
504+
])
505+
def test_el_az_to_enu_unit_vector(navdata, expected_los_vectors):
506+
"""
507+
Test the conversion from elevation and azimuth to ENU unit vectors.
508+
509+
Parameters
510+
----------
511+
navdata : NavData
512+
The input NavData instance.
513+
expected_los_vectors : np.ndarray
514+
The expected ENU unit vectors.
515+
516+
"""
517+
518+
# Construct the ENU unit vectors
519+
enu_unit_vectors = el_az_to_enu_unit_vector(navdata['el_sv_deg'],
520+
navdata['az_sv_deg'])
521+
522+
# First check the shape
523+
assert enu_unit_vectors.shape == expected_los_vectors.shape
524+
assert enu_unit_vectors.shape[0] > enu_unit_vectors.shape[1]
525+
526+
# Check the ENU unit vectors are as expected
527+
np.testing.assert_array_almost_equal(enu_unit_vectors, expected_los_vectors)
528+
529+
# Check the ENU unit vectors are normalized
530+
np.testing.assert_array_almost_equal(
531+
np.linalg.norm(enu_unit_vectors, axis=1),
532+
np.ones(expected_los_vectors.shape[0]))
533+

tests/utils/test_dop.py

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@
1616

1717
from gnss_lib_py.navdata.navdata import NavData
1818
from gnss_lib_py.utils.dop import \
19-
get_dop, calculate_dop, calculate_enu_unit_vectors, _calculate_enut_matrix
20-
from gnss_lib_py.parsers.google_decimeter import AndroidDerived2022
19+
get_dop, calculate_dop, _calculate_enut_matrix
2120

2221

2322
#####################################################################
@@ -94,21 +93,6 @@ def test_simple_enu_unit_vectors(navdata, expected_los_vectors):
9493
A simple set of satellites for ENU unit vector calculation.
9594
9695
"""
97-
# Construct the ENU unit vectors
98-
enu_unit_vectors = calculate_enu_unit_vectors(navdata)
99-
100-
# First check the shape
101-
assert enu_unit_vectors.shape == expected_los_vectors.shape
102-
assert enu_unit_vectors.shape[0] > enu_unit_vectors.shape[1]
103-
104-
# Check the ENU unit vectors are as expected
105-
np.testing.assert_array_almost_equal(enu_unit_vectors, expected_los_vectors)
106-
107-
# Check the ENU unit vectors are normalized
108-
np.testing.assert_array_almost_equal(
109-
np.linalg.norm(enu_unit_vectors, axis=1),
110-
np.ones(expected_los_vectors.shape[0]))
111-
11296
# Check the we get the expected ENUT matrix
11397
enut_matrix = _calculate_enut_matrix(navdata)
11498

@@ -376,6 +360,11 @@ def test_dop_across_time_with_selection(navdata, which_dop):
376360
if which_dop[base_row]:
377361
assert base_row in dop_navdata.rows
378362

363+
# Check no nans
364+
assert all(np.isfinite(dop_navdata[base_row]))
365+
# Check no negative values
366+
assert all(dop_navdata[base_row] >= 0)
367+
379368
# Check that the DOP NavData is the expected length
380369
unique_gps_millis = np.unique(navdata['gps_millis'])
381370
assert len(dop_navdata['gps_millis']) == len(unique_gps_millis)

0 commit comments

Comments
 (0)