Skip to content

Commit 7f5e750

Browse files
authored
Merge pull request #121 from Stanford-NavLab/ashwin/wls-fixes-2
Ashwin/wls fixes 2
2 parents 9a4e467 + 64e8bc7 commit 7f5e750

File tree

4 files changed

+134
-30
lines changed

4 files changed

+134
-30
lines changed

gnss_lib_py/algorithms/snapshot.py

Lines changed: 60 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@
1414
import numpy as np
1515

1616
from gnss_lib_py.parsers.navdata import NavData
17+
from gnss_lib_py.utils import constants as consts
1718
from gnss_lib_py.utils.coordinates import ecef_to_geodetic
1819

1920
def solve_wls(measurements, weight_type = None, only_bias = False,
2021
receiver_state=None, tol = 1e-7, max_count = 20,
21-
delta_t_decimals=-2):
22+
sv_rx_time=False, delta_t_decimals=-2):
2223
"""Runs weighted least squares across each timestep.
2324
2425
Runs weighted least squares across each timestep and adds a new
@@ -29,13 +30,7 @@ def solve_wls(measurements, weight_type = None, only_bias = False,
2930
in rx_est_m will be updated if only_bias is set to True.
3031
3132
If only_bias is set to True, then the receiver position must also
32-
be passed in as the receiver_state
33-
34-
receiver_state : gnss_lib_py.parsers.navdata.NavData
35-
Either estimated or ground truth receiver position in ECEF frame
36-
in meters as an instance of the NavData class with the
37-
following rows: ``x_rx*_m``, `y_rx*_m``, ``z_rx*_m``,
38-
``gps_millis``.
33+
be passed in as the receiver_state.
3934
4035
Parameters
4136
----------
@@ -56,6 +51,14 @@ def solve_wls(measurements, weight_type = None, only_bias = False,
5651
max_count : int
5752
Number of maximum iterations before process is aborted and
5853
solution returned.
54+
sv_rx_time : bool
55+
Flag that specifies whether the input SV positions are in the ECEF
56+
frame of reference corresponding to when the measurements were
57+
received. If set to `True`, the satellite positions are used as
58+
is. The default value is `False`, in which case the ECEF positions
59+
are assumed to in the ECEF frame at the time of signal transmission
60+
and are converted to the ECEF frame at the time of signal reception,
61+
while solving the WLS problem.
5962
delta_t_decimals : int
6063
Decimal places after which times are considered equal.
6164
@@ -102,6 +105,7 @@ def solve_wls(measurements, weight_type = None, only_bias = False,
102105
if weight_type is not None:
103106
if isinstance(weight_type,str) and weight_type in measurements.rows:
104107
weights = measurement_subset[weight_type].reshape(-1,1)
108+
weights = weights[not_nan_indexes]
105109
else:
106110
raise TypeError("WLS weights must be None or row"\
107111
+" in NavData")
@@ -118,7 +122,7 @@ def solve_wls(measurements, weight_type = None, only_bias = False,
118122
,0].reshape(-1,1),
119123
position[3])) # clock bias
120124
position = wls(position, pos_sv_m, corr_pr_m, weights,
121-
only_bias, tol, max_count)
125+
only_bias, tol, max_count, sv_rx_time=sv_rx_time)
122126
states.append([timestamp] + np.squeeze(position).tolist())
123127
except RuntimeError as error:
124128
if str(error) not in runtime_error_idxs:
@@ -155,7 +159,7 @@ def solve_wls(measurements, weight_type = None, only_bias = False,
155159
return state_estimate
156160

157161
def wls(rx_est_m, pos_sv_m, corr_pr_m, weights = None,
158-
only_bias = False, tol = 1e-7, max_count = 20):
162+
only_bias = False, tol = 1e-7, max_count = 20, sv_rx_time=False):
159163
"""Weighted least squares solver for GNSS measurements.
160164
161165
The option for only_bias allows the user to only calculate the clock
@@ -170,7 +174,7 @@ def wls(rx_est_m, pos_sv_m, corr_pr_m, weights = None,
170174
array with shape (4 x 1) and the following order:
171175
x_rx_m, y_rx_m, z_rx_m, b_rx_m.
172176
pos_sv_m : np.ndarray
173-
Satellite positions as an array of shape [# svs x 3] where
177+
Satellite ECEF positions as an array of shape [# svs x 3] where
174178
the columns contain in order x_sv_m, y_sv_m, and z_sv_m.
175179
corr_pr_m : np.ndarray
176180
Corrected pseudoranges for all satellites with shape of
@@ -186,6 +190,17 @@ def wls(rx_est_m, pos_sv_m, corr_pr_m, weights = None,
186190
max_count : int
187191
Number of maximum iterations before process is aborted and
188192
solution returned.
193+
sv_rx_time : bool
194+
Flag to indicate if the satellite positions at the time of
195+
transmission should be used as is or if they should be transformed
196+
to the ECEF frame of reference at the time of reception. For real
197+
measurements, use ``sv_rx_time=False`` to account for the Earth's
198+
rotation and convert SV positions from the ECEF frame at the time
199+
of signal transmission to the ECEF frame at the time of signal
200+
reception. If the SV positions should be used as is, set
201+
``sv_rx_time=True`` to indicate that the given positions are in
202+
the ECEF frame of reference for when the signals are received.
203+
By default, ``sv_rx_time=False``.
189204
190205
Returns
191206
-------
@@ -195,11 +210,35 @@ def wls(rx_est_m, pos_sv_m, corr_pr_m, weights = None,
195210
array with shape (4 x 1) and the following order:
196211
x_rx_m, y_rx_m, z_rx_m, b_rx_m.
197212
213+
Notes
214+
-----
215+
This function internally updates the used SV position to account for
216+
the time taken for the signal to travel to the Earth from the GNSS
217+
satellites.
218+
Since the SV and receiver positions are calculated in an ECEF frame
219+
of reference, which is moving with the Earth's rotation, the reference
220+
frame is slightly (about 30 m along longitude) different when the
221+
signals are received than when the signals were transmitted. Given
222+
the receiver's position is estimated when the signal is received,
223+
the SV positions need to be updated to reflect the change in the
224+
frame of reference in which their position is calculated.
225+
226+
This update happens after every Gauss-Newton update step and is
227+
adapted from [1]_.
228+
229+
References
230+
----------
231+
.. [1] https://github.com/google/gps-measurement-tools/blob/master/opensource/FlightTimeCorrection.m
232+
198233
"""
199234

200235
rx_est_m = rx_est_m.copy() # don't change referenced value
201236

202237
count = 0
238+
# Store the SV position at the original receiver time.
239+
# This position will be modified by the time taken by the signal to
240+
# travel to the receiver.
241+
rx_time_pos_sv_m = pos_sv_m.copy()
203242
num_svs = pos_sv_m.shape[0]
204243
if num_svs < 4 and not only_bias:
205244
raise RuntimeError("Need at least four satellites for WLS.")
@@ -245,6 +284,16 @@ def wls(rx_est_m, pos_sv_m, corr_pr_m, weights = None,
245284
else:
246285
rx_est_m += pos_x_delta
247286

287+
if not sv_rx_time:
288+
# Update the satellite positions based on the time taken for
289+
# the signal to reach the Earth and the satellite clock bias.
290+
delta_t = (corr_pr_m.reshape(-1) - rx_est_m[3,0])/consts.C
291+
dtheta = consts.OMEGA_E_DOT*delta_t
292+
pos_sv_m[:, 0] = np.cos(dtheta)*rx_time_pos_sv_m[:,0] + \
293+
np.sin(dtheta)*rx_time_pos_sv_m[:,1]
294+
pos_sv_m[:, 1] = -np.sin(dtheta)*rx_time_pos_sv_m[:,0] + \
295+
np.cos(dtheta)*rx_time_pos_sv_m[:,1]
296+
248297
count += 1
249298

250299
if count >= max_count:

gnss_lib_py/utils/sim_gnss.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,10 @@ def _find_delxyz_range(sv_posvel, pos, satellites):
316316
pos = np.reshape(pos, [1, 3])
317317
if np.size(pos)!=3:
318318
raise ValueError('Position is not in XYZ')
319-
_, sv_pos, _ = _extract_pos_vel_arr(sv_posvel)
319+
if isinstance(sv_posvel, np.ndarray):
320+
sv_pos = sv_posvel[:, :3]
321+
else:
322+
_, sv_pos, _ = _extract_pos_vel_arr(sv_posvel)
320323
del_pos = sv_pos - np.tile(np.reshape(pos, [-1, 3]), (satellites, 1))
321324
true_range = np.linalg.norm(del_pos, axis=1)
322325
return del_pos, true_range

notebooks/tutorials/algorithms.ipynb

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,12 @@
3535
"id": "3bc6b5dd",
3636
"metadata": {},
3737
"source": [
38-
"Solve for the Weighted Least Squares position estimate simply by passing the measurement data."
38+
"Solve for the Weighted Least Squares position estimate simply by passing the measurement data.\n",
39+
"\n",
40+
"When obtaining WLS estimates for real measurements, the rotation of the Earth between the signal transmission and reception has to be accounted for.\n",
41+
"`solve_wls` accounts for this by default and rotates the given SV positions into the ECEF frame of reference when the signals were received rather using the ECEF frame of reference of when the signals were transmitted.\n",
42+
"\n",
43+
"If you assume that the satellite positions are given in the ECEF frame of reference when the signals were received (and not transmitted), set the parameter `sv_rx_time = True` in the function call."
3944
]
4045
},
4146
{
@@ -45,7 +50,9 @@
4550
"metadata": {},
4651
"outputs": [],
4752
"source": [
48-
"state_wls = glp.solve_wls(derived_data)"
53+
"state_wls = glp.solve_wls(derived_data)\n",
54+
"# When assuming that SV positions are given in the ECEF frame when signals are received use\n",
55+
"# state_wls = glp.solve_wls(derived_data, sv_rx_time=True)"
4956
]
5057
},
5158
{
@@ -101,7 +108,7 @@
101108
"id": "0387e03e",
102109
"metadata": {},
103110
"source": [
104-
"Solve for the Weighted Least Squares position estimate simply by passing the measurement data."
111+
"Solve for the extended Kalman filter position estimate simply by passing the measurement data."
105112
]
106113
},
107114
{
@@ -209,11 +216,19 @@
209216
"source": [
210217
"figs = glp.plot_metric_by_constellation(galileo_data, \"gps_millis\", \"residuals_m\")"
211218
]
219+
},
220+
{
221+
"cell_type": "code",
222+
"execution_count": null,
223+
"id": "fdc6d9cb",
224+
"metadata": {},
225+
"outputs": [],
226+
"source": []
212227
}
213228
],
214229
"metadata": {
215230
"kernelspec": {
216-
"display_name": "Python 3",
231+
"display_name": "Python 3 (ipykernel)",
217232
"language": "python",
218233
"name": "python3"
219234
},

tests/algorithms/test_snapshot.py

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ def test_wls(set_user_states, set_sv_states, tolerance):
101101
keepdims = True) + rx_truth_m[3,0]
102102

103103
rx_est_m = np.zeros((4,1))
104-
user_fix = wls(rx_est_m, pos_sv_m, gt_pr_m)
104+
user_fix = wls(rx_est_m, pos_sv_m, gt_pr_m, sv_rx_time=True)
105105
truth_fix = rx_truth_m
106106

107107
np.testing.assert_array_almost_equal(user_fix, truth_fix,
@@ -229,15 +229,15 @@ def test_wls_only_bias(set_user_states, set_sv_states, tolerance):
229229

230230
# check that position doesn't change but clock bias does change
231231
rx_est_m = np.zeros((4,1))
232-
user_fix = wls(rx_est_m, pos_sv_m, gt_pr_m, None, True)
232+
user_fix = wls(rx_est_m, pos_sv_m, gt_pr_m, None, True, sv_rx_time=True)
233233
np.testing.assert_array_almost_equal(user_fix[:3], np.zeros((3,1)),
234234
decimal=tolerance)
235235
assert abs(user_fix[3]) >= 1E5
236236

237237
# check that correct clock bias is calculated
238238
rx_est_m = np.zeros((4,1))
239239
rx_est_m[:3] = rx_truth_m[:3]
240-
user_fix = wls(rx_est_m, pos_sv_m, gt_pr_m, None, True)
240+
user_fix = wls(rx_est_m, pos_sv_m, gt_pr_m, None, True, sv_rx_time=True)
241241
truth_fix = rx_truth_m
242242

243243
np.testing.assert_array_almost_equal(user_fix, truth_fix,
@@ -362,7 +362,7 @@ def test_solve_wls(derived):
362362
Instance of AndroidDerived2021 for testing.
363363
364364
"""
365-
state_estimate = solve_wls(derived)
365+
state_estimate = solve_wls(derived, sv_rx_time=False)
366366

367367
# result should be a NavData Class instance
368368
assert isinstance(state_estimate,type(NavData()))
@@ -403,12 +403,12 @@ def test_solve_wls_weights(derived, tolerance):
403403
Error threshold for test pass/fail
404404
"""
405405

406-
state_estimate_1 = solve_wls(derived)
407-
state_estimate_2 = solve_wls(derived, None)
406+
state_estimate_1 = solve_wls(derived, sv_rx_time=False)
407+
state_estimate_2 = solve_wls(derived, None, sv_rx_time=False)
408408

409409
# create new column of unity weights
410410
derived["unity_weights"] = 1
411-
state_estimate_3 = solve_wls(derived, "unity_weights")
411+
state_estimate_3 = solve_wls(derived, "unity_weights", sv_rx_time=False)
412412

413413
# all of the above should be the same
414414
np.testing.assert_array_almost_equal(state_estimate_1.array,
@@ -418,7 +418,7 @@ def test_solve_wls_weights(derived, tolerance):
418418
state_estimate_3.array,
419419
decimal=tolerance)
420420

421-
state_estimate_4 = solve_wls(derived, "raw_pr_sigma_m")
421+
state_estimate_4 = solve_wls(derived, "raw_pr_sigma_m", sv_rx_time=False)
422422
with np.testing.assert_raises(AssertionError):
423423
np.testing.assert_array_almost_equal(state_estimate_1.array,
424424
state_estimate_4.array,
@@ -463,17 +463,20 @@ def test_wls_weights(set_user_states, set_sv_states, tolerance,
463463
rx_est_m = np.zeros((4,1))
464464

465465
# should work if None is passed
466-
user_fix_1 = wls(rx_est_m, pos_sv_m, noisy_pr_m, weights=None)
466+
user_fix_1 = wls(rx_est_m, pos_sv_m, noisy_pr_m, weights=None,
467+
sv_rx_time=True)
467468
user_fix_2 = wls(rx_est_m, pos_sv_m, noisy_pr_m,
468-
weights=np.ones((pos_sv_m.shape[0],1)))
469+
weights=np.ones((pos_sv_m.shape[0],1)),
470+
sv_rx_time=True)
469471
# both should be unity weights and return the same result
470472
np.testing.assert_array_almost_equal(user_fix_1,
471473
user_fix_2,
472474
decimal=tolerance)
473475

474476
# result should be different if different weights are used
475477
user_fix_3 = wls(rx_est_m, pos_sv_m, noisy_pr_m,
476-
weights=np.arange(pos_sv_m.shape[0]).reshape(-1,1))
478+
weights=np.arange(pos_sv_m.shape[0]).reshape(-1,1),
479+
sv_rx_time=True)
477480
with np.testing.assert_raises(AssertionError):
478481
np.testing.assert_array_almost_equal(user_fix_1,
479482
user_fix_3,
@@ -528,7 +531,7 @@ def test_solve_wls_bias_only(derived_2022):
528531
input_position[row, col] = measure_frame[row, 0]
529532
col += 1
530533
state_estimate = solve_wls(derived_2022, only_bias=True,
531-
receiver_state=derived_2022)
534+
receiver_state=derived_2022, sv_rx_time=False)
532535
# Verify that both structures have the same length
533536
assert len(input_position) == len(state_estimate)
534537
# Verify that solved positions are the same as input positions
@@ -557,11 +560,11 @@ def test_solve_wls_bias_only(derived_2022):
557560
derived_2022.remove(ecef_rows, inplace=True)
558561
with pytest.raises(KeyError):
559562
solve_wls(derived_2022, only_bias=True,
560-
receiver_state=derived_2022)
563+
receiver_state=derived_2022, sv_rx_time=False)
561564

562565
# check error raised when receiver_state is not present in only_bias
563566
with pytest.raises(RuntimeError):
564-
solve_wls(derived_2022, only_bias=True)
567+
solve_wls(derived_2022, only_bias=True, sv_rx_time=False)
565568

566569

567570
def test_wls_fails(capsys):
@@ -613,3 +616,37 @@ def test_solve_wls_fails(derived):
613616

614617
assert caught_four_sats
615618
assert caught_empty_state
619+
620+
621+
def test_rotation_of_earth_fix(derived_2022):
622+
"""Tests that accounting for Earth's rotation reduces WLS errors.
623+
624+
Parameters
625+
----------
626+
derived_2022 : AndroidDerived2022
627+
Instance of AndroidDerived2022 for testing
628+
"""
629+
google_wls = derived_2022[['x_rx_m', 'y_rx_m', 'z_rx_m']]
630+
google_wls = np.empty([3, len(np.unique(np.round(derived_2022['gps_millis'], decimals=-2)))])
631+
for idx, (_, _, frame) in enumerate(derived_2022.loop_time('gps_millis')):
632+
google_wls[:, idx] = frame[['x_rx_m', 'y_rx_m', 'z_rx_m'], 0]
633+
derived_2022['wls_weights'] = 1/derived_2022['raw_pr_sigma_m']
634+
state_with_rotn = solve_wls(derived_2022, weight_type='wls_weights',
635+
sv_rx_time=False)
636+
glp_wls = state_with_rotn[['x_rx_wls_m', 'y_rx_wls_m', 'z_rx_wls_m']]
637+
# Verify that the mean error between both estimates is less than 10m
638+
for idx in range(3):
639+
assert np.mean(np.abs(google_wls[idx, :] - glp_wls[idx, :])) < 30
640+
state_without_rotn = solve_wls(derived_2022, weight_type='wls_weights',
641+
sv_rx_time=True)
642+
glp_wls_no_rotn = state_without_rotn[['x_rx_wls_m',
643+
'y_rx_wls_m',
644+
'z_rx_wls_m']]
645+
for idx in range(3):
646+
error_rotn = np.mean(np.abs(google_wls[idx, :] - glp_wls[idx, :]))
647+
error_no_rotn = np.mean(np.abs(google_wls[idx, :] - glp_wls_no_rotn[idx, :]))
648+
assert error_rotn < error_no_rotn
649+
650+
651+
652+

0 commit comments

Comments
 (0)