Skip to content

Commit 57654a7

Browse files
committed
Revert GNSS EKF changes
1 parent 19f5b59 commit 57654a7

3 files changed

Lines changed: 25 additions & 238 deletions

File tree

gnss_lib_py/algorithms/gnss_filters.py

Lines changed: 17 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@
1111

1212
from gnss_lib_py.parsers.navdata import NavData
1313
from gnss_lib_py.algorithms.snapshot import solve_wls
14-
from gnss_lib_py.utils import constants as consts
15-
from gnss_lib_py.utils.sim_gnss import _find_delxyz_range
1614
from gnss_lib_py.utils.coordinates import ecef_to_geodetic
1715
from gnss_lib_py.utils.filters import BaseExtendedKalmanFilter
1816

@@ -51,80 +49,31 @@ def solve_gnss_ekf(measurements, init_dict = None,
5149
init_dict = {}
5250

5351
if "state_0" not in init_dict:
54-
try:
55-
# if the given measurement frame has a state estimate, use
56-
# that, including the clock bias estimate
57-
pos_est_rows = measurements.find_wildcard_indexes(["x_rx*_m",
58-
"y_rx*_m",
59-
"z_rx*_m",
60-
"b_rx*_m"],
61-
max_allow=1)
62-
not_nan_idxs = measurements.argwhere(pos_est_rows['x_rx*_m'],
63-
np.nan, 'neq')
64-
state_0 = np.zeros((7,1))
65-
state_0[0,0] = measurements[pos_est_rows['x_rx*_m'], not_nan_idxs[0]]
66-
state_0[1,0] = measurements[pos_est_rows['y_rx*_m'], not_nan_idxs[0]]
67-
state_0[2,0] = measurements[pos_est_rows['z_rx*_m'], not_nan_idxs[0]]
68-
state_0[6,0] = measurements[pos_est_rows['b_rx*_m'], not_nan_idxs[0]]
69-
except KeyError:
70-
try:
71-
# a key error happened and one of the rows from the last
72-
# try clause is not present. Try again without bias,
73-
# which often missing from datasets
74-
pos_est_rows = measurements.find_wildcard_indexes(["x_rx*_m",
75-
"y_rx*_m",
76-
"z_rx*_m"],
77-
max_allow=1)
78-
not_nan_idxs = measurements.argwhere(pos_est_rows['x_rx*_m'],
79-
np.nan, 'neq')
80-
state_0 = np.zeros((7,1))
81-
state_0[0,0] = measurements[pos_est_rows['x_rx*_m'], not_nan_idxs[0]]
82-
state_0[1,0] = measurements[pos_est_rows['y_rx*_m'], not_nan_idxs[0]]
83-
state_0[2,0] = measurements[pos_est_rows['z_rx*_m'], not_nan_idxs[0]]
84-
pos_0 = NavData()
85-
pos_0['gps_millis'] = measurements['gps_millis', not_nan_idxs[0]]
86-
pos_0['x_rx_m'] = state_0[0,0]
87-
pos_0['y_rx_m'] = state_0[1,0]
88-
pos_0['z_rx_m'] = state_0[2,0]
89-
measurement_subset = measurements.copy(cols=not_nan_idxs[0])
90-
pos_0 = solve_wls(measurement_subset,
91-
receiver_state=pos_0,
92-
only_bias=True)
93-
# if len(pos_0.where('b_rx_wls_m', np.nan, 'eq'))==0:
94-
# break
95-
state_0[6,0] = pos_0['b_rx_wls_m']
96-
except KeyError:
97-
# position rows were not found again, use a WLS estimate
98-
pos_0 = None
99-
for _, _, measurement_subset in measurements.loop_time("gps_millis",
100-
delta_t_decimals=delta_t_decimals):
101-
pos_0 = solve_wls(measurement_subset)
102-
# Assume that if 'x_rx_wls_m' is np.nan, then a state estimate
103-
# has not been found and all x, y, and z are np.nan.
104-
# If the length of elements where 'x_rx_wls_m' is np.nan is
105-
# 0, then a solution has been found and can be used as an
106-
# initialization
107-
if len(pos_0.where('x_rx_wls_m', np.nan, 'eq'))==0:
108-
break
109-
110-
state_0 = np.zeros((7,1))
111-
if pos_0 is not None:
112-
state_0[:3,0] = pos_0[["x_rx_wls_m","y_rx_wls_m","z_rx_wls_m"]]
113-
state_0[6,0] = pos_0[["b_rx_wls_m"]]
52+
pos_0 = None
53+
for _, _, measurement_subset in measurements.loop_time("gps_millis",
54+
delta_t_decimals=delta_t_decimals):
55+
pos_0 = solve_wls(measurement_subset)
56+
if pos_0 is not None:
57+
break
58+
59+
state_0 = np.zeros((7,1))
60+
if pos_0 is not None:
61+
state_0[:3,0] = pos_0[["x_rx_wls_m","y_rx_wls_m","z_rx_wls_m"]]
62+
state_0[6,0] = pos_0[["b_rx_wls_m"]]
63+
11464
init_dict["state_0"] = state_0
11565

11666
if "sigma_0" not in init_dict:
11767
sigma_0 = np.eye(init_dict["state_0"].size)
11868
init_dict["sigma_0"] = sigma_0
11969

12070
if "Q" not in init_dict:
121-
raise RuntimeError("Process noise must be specified in init_dict")
71+
process_noise = np.eye(init_dict["state_0"].size)
72+
init_dict["Q"] = process_noise
12273

12374
if "R" not in init_dict:
124-
raise RuntimeError("Measurement noise must be specified in init_dict")
125-
126-
if "use_tx_time" not in init_dict:
127-
init_dict["use_tx_time"] = False
75+
measurement_noise = np.eye(1) # gets overwritten
76+
init_dict["R"] = measurement_noise
12877

12978
# initialize parameter dictionary
13079
if params_dict is None:
@@ -212,7 +161,6 @@ def __init__(self, init_dict, params_dict):
212161
self.delta_t = params_dict.get('dt',1.0)
213162
self.motion_type = params_dict.get('motion_type','stationary')
214163
self.measure_type = params_dict.get('measure_type','pseudorange')
215-
self.use_tx_time = init_dict.get('use_tx_time', False)
216164

217165
def dyn_model(self, u, predict_dict=None):
218166
"""Nonlinear dynamics
@@ -248,34 +196,11 @@ def measure_model(self, update_dict):
248196
of shape [3 x N] with rows of x_sv_m, y_sv_m, and z_sv_m in that
249197
order.
250198
251-
This measurment model uses the current state estimate to find
252-
the time taken for signals to propagate from the satellites to
253-
the receiver and updates the SV positions to reflect the changed
254-
ECEF reference frame.
255-
256-
Since the ECEF reference frame moves with the Earth, the frame
257-
of reference is different at different times.
258-
SV positions are calculated for the time at which the signal was
259-
transmitted but the receiver position is computed for the ECEF
260-
frame of reference when the signals are received. Consequently,
261-
the SV positions must be updated to account for the change in the
262-
ECEF frame between signal transmission and reception.
263-
However, given the EKF has an initial position guess around the
264-
true position (either through prior knowledge or a prior state
265-
estimation process such as WLS), we can simply correct the SV
266-
positions once and use them as such, without further modification.
267-
268199
Parameters
269200
----------
270201
update_dict : dict
271202
Update dictionary containing satellite positions with key
272-
``pos_sv_m`` and optionally ``tx_time``. ``tx_time`` specifies
273-
if the filter should use the SV positions at time of
274-
transmission (if True). If False, the the time it takes for
275-
the signal to propagate from the satellite to the receiver
276-
is accounted for and the SV positions are propagated forward
277-
to the ECEF coordinate frame at the receiver time. By default,
278-
``tx_time`` is True.
203+
``pos_sv_m``.
279204
280205
Returns
281206
-------
@@ -291,18 +216,6 @@ def measure_model(self, update_dict):
291216
"""
292217
if self.measure_type=='pseudorange':
293218
pos_sv_m = update_dict['pos_sv_m']
294-
if not self.use_tx_time:
295-
rx_pos_m = np.array([[self.state[0]], [self.state[1]], [self.state[2]]])
296-
num_svs = np.shape(pos_sv_m)[1]
297-
_, true_range = _find_delxyz_range(pos_sv_m.T, rx_pos_m, num_svs)
298-
tx_time = (true_range - self.state[6])/consts.C
299-
dtheta = consts.OMEGA_E_DOT*tx_time
300-
# The following two lines are expanded position updates for a
301-
# rotation by dtheta radians about the z-axis, which updates
302-
# the positions along the x and y axes but the position along
303-
# the z-axis is unchanged.
304-
pos_sv_m[0, :] = np.cos(dtheta)*pos_sv_m[0,:] + np.sin(dtheta)*pos_sv_m[1,:]
305-
pos_sv_m[1, :] = -np.sin(dtheta)*pos_sv_m[0,:] + np.cos(dtheta)*pos_sv_m[1,:]
306219
pseudo = np.sqrt((self.state[0] - pos_sv_m[0, :])**2
307220
+ (self.state[1] - pos_sv_m[1, :])**2
308221
+ (self.state[2] - pos_sv_m[2, :])**2) \

tests/algorithms/test_gnss_filters.py

Lines changed: 7 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from numpy.random import default_rng
1313

1414
from gnss_lib_py.parsers.navdata import NavData
15-
from gnss_lib_py.parsers.android import AndroidDerived2021, AndroidDerived2022
15+
from gnss_lib_py.parsers.android import AndroidDerived2021
1616
from gnss_lib_py.algorithms.gnss_filters import GNSSEKF, solve_gnss_ekf
1717

1818
@pytest.fixture(name='init_dict')
@@ -35,8 +35,7 @@ def gnss_init_params():
3535
'state_0': state_0,
3636
'sigma_0': 5*np.eye(state_dim),
3737
'Q': Q,
38-
'R': R,
39-
'use_tx_time': True}
38+
'R': R}
4039
return init_dict
4140

4241

@@ -160,83 +159,17 @@ def fixture_load_derived(derived_path):
160159
derived = AndroidDerived2021(derived_path)
161160
return derived
162161

163-
164-
@pytest.fixture(name="derived_2022_path")
165-
def fixture_derived_2022_path(root_path):
166-
"""Filepath of Android Derived 2022 measurements
167-
168-
Returns
169-
-------
170-
derived_2022_path : string
171-
Location for the unit_test Android 2022 derived measurements
172-
173-
Notes
174-
-----
175-
Test data is a subset of the Android Raw Measurement Dataset [3]_,
176-
from the 2022 Decimeter Challenge. Particularly, the
177-
train/2021-04-29-MTV-2/SamsungGalaxyS20Ultra trace. The dataset
178-
was retrieved from
179-
https://www.kaggle.com/competitions/smartphone-decimeter-2022/data
180-
181-
References
182-
----------
183-
.. [3] Fu, Guoyu Michael, Mohammed Khider, and Frank van Diggelen.
184-
"Android Raw GNSS Measurement Datasets for Precise Positioning."
185-
Proceedings of the 33rd International Technical Meeting of the
186-
Satellite Division of The Institute of Navigation (ION GNSS+
187-
2020). 2020.
188-
"""
189-
derived_2022_path = os.path.join(root_path, '../android_2022/device_gnss.csv')
190-
return derived_2022_path
191-
192-
193-
@pytest.fixture(name="derived_2022")
194-
def fixture_load_derived_2022(derived_2022_path):
195-
"""Load instance of AndroidDerived2021
196-
197-
Parameters
198-
----------
199-
derived_2022_path : pytest.fixture
200-
String with location of Android derived 2022 measurement file
201-
202-
Returns
203-
-------
204-
derived_2022 : AndroidDerived2021
205-
Instance of AndroidDerived2022 for testing
206-
"""
207-
derived_2022 = AndroidDerived2022(derived_2022_path)
208-
return derived_2022
209-
210-
211-
@pytest.fixture(name="noise_tx_init_dict")
212-
def fixture_android_init_dict():
213-
"""Define dictionary containing identity process and measure noises.
214-
215-
Returns
216-
-------
217-
init_dict : dict
218-
Dictionary of initialization parameters, in this case, containing
219-
just the process and measurement noise covariance matrices.
220-
"""
221-
init_dict = {}
222-
init_dict['Q'] = np.eye(7)
223-
init_dict['R'] = np.eye(1)
224-
init_dict['use_tx_time'] = False
225-
return init_dict
226-
227-
def test_solve_gnss_ekf(derived, noise_tx_init_dict):
162+
def test_solve_gnss_ekf(derived):
228163
"""Test that solving for GNSS EKF doesn't fail
229164
230165
Parameters
231166
----------
232167
derived : AndroidDerived2021
233168
Instance of AndroidDerived2021 for testing.
234-
init_dict : dict
235-
Dictionary of initialization parameters, in this case, containing
236-
just the process and measurement noise covariance matrices.
237169
238170
"""
239-
state_estimate = solve_gnss_ekf(derived, noise_tx_init_dict)
171+
state_estimate = solve_gnss_ekf(derived)
172+
240173
# result should be a NavData Class instance
241174
assert isinstance(state_estimate,type(NavData()))
242175

@@ -269,76 +202,23 @@ def test_solve_gnss_ekf(derived, noise_tx_init_dict):
269202
assert row_index in str(excinfo.value)
270203

271204

272-
273-
def test_solve_gnss_ekf_fails(derived, noise_tx_init_dict):
205+
def test_solve_gnss_ekf_fails(derived):
274206
"""Test expected fails for the GNSS EKF.
275207
276208
Parameters
277209
----------
278210
derived : AndroidDerived2021
279211
Instance of AndroidDerived2021 for testing
280-
init_dict : dict
281-
Dictionary of initialization parameters, in this case, containing
282-
just the process and measurement noise covariance matrices.
283212
284213
"""
285214

286215
navdata = derived.remove(cols=list(range(len(derived))))
287216

288217
with pytest.warns(RuntimeWarning) as warns:
289-
solve_gnss_ekf(navdata, noise_tx_init_dict)
218+
solve_gnss_ekf(navdata)
290219

291220
# verify RuntimeWarning
292221
assert len(warns) == 1
293222
warn = warns[0]
294223
assert issubclass(warn.category, RuntimeWarning)
295224
assert "No valid state" in str(warn.message)
296-
297-
298-
# Test that RuntimeError is raised if no measurment noise is provided
299-
with pytest.raises(RuntimeError):
300-
del(noise_tx_init_dict['R'])
301-
solve_gnss_ekf(derived, noise_tx_init_dict)
302-
# Test that RuntimeError is raised if no process noise is provided
303-
with pytest.raises(RuntimeError):
304-
del(noise_tx_init_dict['Q'])
305-
solve_gnss_ekf(derived, noise_tx_init_dict)
306-
# Test that RuntimeError is raised if no initial dictionary is provided
307-
with pytest.raises(RuntimeError):
308-
solve_gnss_ekf(derived)
309-
310-
311-
def test_solve_gnss_ekf_initializations(derived_2022):
312-
"""Tests that different initial state cases run without error.
313-
314-
Parameters
315-
----------
316-
derived_2022 : AndroidDerived2022
317-
Instance of AndroidDerived2022 for testing
318-
init_dict : dict
319-
Dictionary of initialization parameters, in this case, containing
320-
just the process and measurement noise covariance matrices.
321-
"""
322-
# GNSS EKF solution when initial states and biases are given
323-
derived_2022['b_rx_m'] = 0
324-
# Reinitializing the initial dictionary because other functions might
325-
# have added to this.
326-
reset_init_dict = {}
327-
reset_init_dict['Q'] = np.eye(7)
328-
reset_init_dict['R'] = np.eye(1)
329-
reset_init_dict['use_tx_time'] = True
330-
_ = solve_gnss_ekf(derived_2022, reset_init_dict)
331-
# GNSS EKF solution when initial positions are given
332-
derived_2022.remove(rows=['b_rx_m'], inplace=True)
333-
reset_init_dict = {}
334-
reset_init_dict['Q'] = np.eye(7)
335-
reset_init_dict['R'] = np.eye(1)
336-
reset_init_dict['use_tx_time'] = True
337-
_ = solve_gnss_ekf(derived_2022, reset_init_dict)
338-
# GNSS EKF solution when no initial states are given
339-
derived_no_rx_rows = derived_2022.remove(rows=['x_rx_m', 'y_rx_m', 'z_rx_m'])
340-
reset_init_dict = {}
341-
reset_init_dict['Q'] = np.eye(7)
342-
reset_init_dict['R'] = np.eye(1)
343-
reset_init_dict['use_tx_time'] = True
344-
_ = solve_gnss_ekf(derived_no_rx_rows, reset_init_dict)

tests/parsers/test_android.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -750,13 +750,7 @@ def test_solve_kaggle_dataset(root_path):
750750
solve_gnss_ekf,
751751
]:
752752
for verbose in [True, False]:
753-
if solver == solve_gnss_ekf:
754-
init_dict = {'Q': np.eye(7), 'R': np.eye(1)}
755-
solution = android.solve_kaggle_dataset(folder_path, solver,
756-
verbose, init_dict=init_dict)
757-
758-
else:
759-
solution = android.solve_kaggle_dataset(folder_path, solver,
753+
solution = android.solve_kaggle_dataset(folder_path, solver,
760754
verbose)
761755

762756
solution.in_rows(["tripId","UnixTimeMillis",

0 commit comments

Comments
 (0)