1111
1212from gnss_lib_py .parsers .navdata import NavData
1313from 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
1416from gnss_lib_py .utils .coordinates import ecef_to_geodetic
1517from gnss_lib_py .utils .filters import BaseExtendedKalmanFilter
1618
@@ -49,31 +51,80 @@ def solve_gnss_ekf(measurements, init_dict = None,
4951 init_dict = {}
5052
5153 if "state_0" not in init_dict :
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-
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" ]]
64114 init_dict ["state_0" ] = state_0
65115
66116 if "sigma_0" not in init_dict :
67117 sigma_0 = np .eye (init_dict ["state_0" ].size )
68118 init_dict ["sigma_0" ] = sigma_0
69119
70120 if "Q" not in init_dict :
71- process_noise = np .eye (init_dict ["state_0" ].size )
72- init_dict ["Q" ] = process_noise
121+ raise RuntimeError ("Process noise must be specified in init_dict" )
73122
74123 if "R" not in init_dict :
75- measurement_noise = np .eye (1 ) # gets overwritten
76- init_dict ["R" ] = measurement_noise
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
77128
78129 # initialize parameter dictionary
79130 if params_dict is None :
@@ -161,6 +212,7 @@ def __init__(self, init_dict, params_dict):
161212 self .delta_t = params_dict .get ('dt' ,1.0 )
162213 self .motion_type = params_dict .get ('motion_type' ,'stationary' )
163214 self .measure_type = params_dict .get ('measure_type' ,'pseudorange' )
215+ self .use_tx_time = init_dict .get ('use_tx_time' , False )
164216
165217 def dyn_model (self , u , predict_dict = None ):
166218 """Nonlinear dynamics
@@ -196,11 +248,34 @@ def measure_model(self, update_dict):
196248 of shape [3 x N] with rows of x_sv_m, y_sv_m, and z_sv_m in that
197249 order.
198250
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+
199268 Parameters
200269 ----------
201270 update_dict : dict
202271 Update dictionary containing satellite positions with key
203- ``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.
204279
205280 Returns
206281 -------
@@ -216,6 +291,18 @@ def measure_model(self, update_dict):
216291 """
217292 if self .measure_type == 'pseudorange' :
218293 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 ,:]
219306 pseudo = np .sqrt ((self .state [0 ] - pos_sv_m [0 , :])** 2
220307 + (self .state [1 ] - pos_sv_m [1 , :])** 2
221308 + (self .state [2 ] - pos_sv_m [2 , :])** 2 ) \
0 commit comments