Skip to content

Commit dee7a34

Browse files
committed
Changed default use_tx_time to False
1 parent 1881064 commit dee7a34

4 files changed

Lines changed: 232 additions & 49 deletions

File tree

gnss_lib_py/algorithms/gnss_filters.py

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ def solve_gnss_ekf(measurements, init_dict = None,
5959
"z_rx*_m",
6060
"b_rx*_m"],
6161
max_allow=1)
62-
6362
not_nan_idxs = measurements.argwhere(pos_est_rows['x_rx*_m'],
6463
np.nan, 'neq')
6564
state_0 = np.zeros((7,1))
@@ -83,10 +82,11 @@ def solve_gnss_ekf(measurements, init_dict = None,
8382
state_0[1,0] = measurements[pos_est_rows['y_rx*_m'], not_nan_idxs[0]]
8483
state_0[2,0] = measurements[pos_est_rows['z_rx*_m'], not_nan_idxs[0]]
8584
pos_0 = NavData()
86-
pos_0[pos_est_rows['x_rx*_m']] = state_0[0,0]
87-
pos_0[pos_est_rows['y_rx*_m']] = state_0[1,0]
88-
pos_0[pos_est_rows['z_rx*_m']] = state_0[2,0]
89-
measurement_subset = measurements.copy(col=not_nan_idxs[0])
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])
9090
pos_0 = solve_wls(measurement_subset,
9191
receiver_state=pos_0,
9292
only_bias=True)
@@ -111,7 +111,6 @@ def solve_gnss_ekf(measurements, init_dict = None,
111111
if pos_0 is not None:
112112
state_0[:3,0] = pos_0[["x_rx_wls_m","y_rx_wls_m","z_rx_wls_m"]]
113113
state_0[6,0] = pos_0[["b_rx_wls_m"]]
114-
115114
init_dict["state_0"] = state_0
116115

117116
if "sigma_0" not in init_dict:
@@ -124,6 +123,9 @@ def solve_gnss_ekf(measurements, init_dict = None,
124123
if "R" not in init_dict:
125124
raise RuntimeError("Measurement noise must be specified in init_dict")
126125

126+
if "use_tx_time" not in init_dict:
127+
init_dict["use_tx_time"] = False
128+
127129
# initialize parameter dictionary
128130
if params_dict is None:
129131
params_dict = {}
@@ -210,6 +212,7 @@ def __init__(self, init_dict, params_dict):
210212
self.delta_t = params_dict.get('dt',1.0)
211213
self.motion_type = params_dict.get('motion_type','stationary')
212214
self.measure_type = params_dict.get('measure_type','pseudorange')
215+
self.use_tx_time = init_dict.get('use_tx_time', False)
213216

214217
def dyn_model(self, u, predict_dict=None):
215218
"""Nonlinear dynamics
@@ -266,7 +269,13 @@ def measure_model(self, update_dict):
266269
----------
267270
update_dict : dict
268271
Update dictionary containing satellite positions with key
269-
``pos_sv_m``.
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.
270279
271280
Returns
272281
-------
@@ -282,17 +291,18 @@ def measure_model(self, update_dict):
282291
"""
283292
if self.measure_type=='pseudorange':
284293
pos_sv_m = update_dict['pos_sv_m']
285-
rx_pos_m = np.array([[self.state[0]], [self.state[1]], [self.state[2]]])
286-
num_svs = np.shape(pos_sv_m)[1]
287-
_, true_range = _find_delxyz_range(pos_sv_m.T, rx_pos_m, num_svs)
288-
tx_time = (true_range - self.state[6])/consts.C
289-
dtheta = consts.OMEGA_E_DOT*tx_time
290-
# The following two lines are expanded position updates for a
291-
# rotation by dtheta radians about the z-axis, which updates
292-
# the positions along the x and y axes but the position along
293-
# the z-axis is unchanged.
294-
pos_sv_m[0, :] = np.cos(dtheta)*pos_sv_m[0,:] + np.sin(dtheta)*pos_sv_m[1,:]
295-
pos_sv_m[1, :] = -np.sin(dtheta)*pos_sv_m[0,:] + np.cos(dtheta)*pos_sv_m[1,:]
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,:]
296306
pseudo = np.sqrt((self.state[0] - pos_sv_m[0, :])**2
297307
+ (self.state[1] - pos_sv_m[1, :])**2
298308
+ (self.state[2] - pos_sv_m[2, :])**2) \

gnss_lib_py/algorithms/snapshot.py

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
def solve_wls(measurements, weight_type = None, only_bias = False,
2121
receiver_state=None, tol = 1e-7, max_count = 20,
22-
delta_t_decimals=-2):
22+
use_tx_time=False, delta_t_decimals=-2):
2323
"""Runs weighted least squares across each timestep.
2424
2525
Runs weighted least squares across each timestep and adds a new
@@ -51,6 +51,11 @@ def solve_wls(measurements, weight_type = None, only_bias = False,
5151
max_count : int
5252
Number of maximum iterations before process is aborted and
5353
solution returned.
54+
use_tx_time : bool
55+
Flag that specifies whether the SV positions at transmit time
56+
should be used as is or if they should be transformed to the
57+
corresponding ECEF coordinates when the signal is received. If
58+
True, the SV positions will not be modified.
5459
delta_t_decimals : int
5560
Decimal places after which times are considered equal.
5661
@@ -97,6 +102,7 @@ def solve_wls(measurements, weight_type = None, only_bias = False,
97102
if weight_type is not None:
98103
if isinstance(weight_type,str) and weight_type in measurements.rows:
99104
weights = measurement_subset[weight_type].reshape(-1,1)
105+
weights = weights[not_nan_indexes]
100106
else:
101107
raise TypeError("WLS weights must be None or row"\
102108
+" in NavData")
@@ -113,7 +119,7 @@ def solve_wls(measurements, weight_type = None, only_bias = False,
113119
,0].reshape(-1,1),
114120
position[3])) # clock bias
115121
position = wls(position, pos_sv_m, corr_pr_m, weights,
116-
only_bias, tol, max_count)
122+
only_bias, tol, max_count, use_tx_time)
117123
states.append([timestamp] + np.squeeze(position).tolist())
118124
except RuntimeError as error:
119125
if str(error) not in runtime_error_idxs:
@@ -150,7 +156,7 @@ def solve_wls(measurements, weight_type = None, only_bias = False,
150156
return state_estimate
151157

152158
def wls(rx_est_m, pos_sv_m, corr_pr_m, weights = None,
153-
only_bias = False, tol = 1e-7, max_count = 20):
159+
only_bias = False, tol = 1e-7, max_count = 20, use_tx_time=False):
154160
"""Weighted least squares solver for GNSS measurements.
155161
156162
The option for only_bias allows the user to only calculate the clock
@@ -181,6 +187,14 @@ def wls(rx_est_m, pos_sv_m, corr_pr_m, weights = None,
181187
max_count : int
182188
Number of maximum iterations before process is aborted and
183189
solution returned.
190+
use_tx_time : bool
191+
Flag to indicate if the satellite positions at the time of
192+
transmission should be used as is or if they should be transformed
193+
to the ECEF frame of reference at the time of reception. For real
194+
measurements, use ``use_tx_time=False`` to account for the Earth's
195+
rotation. For simulations, ``use_tx_time=True`` can be used to
196+
simplify the simulation and estimation. By default,
197+
``use_tx_time=True``.
184198
185199
Returns
186200
-------
@@ -264,13 +278,15 @@ def wls(rx_est_m, pos_sv_m, corr_pr_m, weights = None,
264278
else:
265279
rx_est_m += pos_x_delta
266280

267-
268-
# Update the satellite positions based on the time taken for
269-
# the signal to reach the Earth and the satellite clock bias.
270-
tx_time = (corr_pr_m.reshape(-1) - rx_est_m[3,0])/consts.C
271-
dtheta = consts.OMEGA_E_DOT*tx_time
272-
pos_sv_m[:, 0] = np.cos(dtheta)*rx_time_pos_sv_m[:,0] + np.sin(dtheta)*rx_time_pos_sv_m[:,1]
273-
pos_sv_m[:, 1] = -np.sin(dtheta)*rx_time_pos_sv_m[:,0] + np.cos(dtheta)*rx_time_pos_sv_m[:,1]
281+
if not use_tx_time:
282+
# Update the satellite positions based on the time taken for
283+
# the signal to reach the Earth and the satellite clock bias.
284+
tx_time = (corr_pr_m.reshape(-1) - rx_est_m[3,0])/consts.C
285+
dtheta = consts.OMEGA_E_DOT*tx_time
286+
pos_sv_m[:, 0] = np.cos(dtheta)*rx_time_pos_sv_m[:,0] + \
287+
np.sin(dtheta)*rx_time_pos_sv_m[:,1]
288+
pos_sv_m[:, 1] = -np.sin(dtheta)*rx_time_pos_sv_m[:,0] + \
289+
np.cos(dtheta)*rx_time_pos_sv_m[:,1]
274290

275291
count += 1
276292

tests/algorithms/test_gnss_filters.py

Lines changed: 127 additions & 7 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
15+
from gnss_lib_py.parsers.android import AndroidDerived2021, AndroidDerived2022
1616
from gnss_lib_py.algorithms.gnss_filters import GNSSEKF, solve_gnss_ekf
1717

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

4142

@@ -159,17 +160,83 @@ def fixture_load_derived(derived_path):
159160
derived = AndroidDerived2021(derived_path)
160161
return derived
161162

162-
def test_solve_gnss_ekf(derived):
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):
163228
"""Test that solving for GNSS EKF doesn't fail
164229
165230
Parameters
166231
----------
167232
derived : AndroidDerived2021
168233
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.
169237
170238
"""
171-
state_estimate = solve_gnss_ekf(derived)
172-
239+
state_estimate = solve_gnss_ekf(derived, noise_tx_init_dict)
173240
# result should be a NavData Class instance
174241
assert isinstance(state_estimate,type(NavData()))
175242

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

204271

205-
def test_solve_gnss_ekf_fails(derived):
272+
273+
def test_solve_gnss_ekf_fails(derived, noise_tx_init_dict):
206274
"""Test expected fails for the GNSS EKF.
207275
208276
Parameters
209277
----------
210278
derived : AndroidDerived2021
211279
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.
212283
213284
"""
214285

215286
navdata = derived.remove(cols=list(range(len(derived))))
216287

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

220291
# verify RuntimeWarning
221292
assert len(warns) == 1
222293
warn = warns[0]
223294
assert issubclass(warn.category, RuntimeWarning)
224295
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)

0 commit comments

Comments
 (0)