Skip to content

Commit fa4f017

Browse files
pd0wmgijskoning
andauthored
laikad: calc_pos_fix numpy implementation (#24865)
* Replace posfix with gauss newton method * Cleanup * Check if glonass is in the list * Fix * also return residual * Add residuals * Update selfdrive/locationd/laikad.py Co-authored-by: Willem Melching <willem.melching@gmail.com> * Cleanup Co-authored-by: Gijs Koning <gijs-koning@live.nl>
1 parent 4172191 commit fa4f017

File tree

1 file changed

+95
-8
lines changed

1 file changed

+95
-8
lines changed

selfdrive/locationd/laikad.py

+95-8
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,17 @@
77
import numpy as np
88
from collections import defaultdict
99

10+
import sympy
1011
from numpy.linalg import linalg
1112

1213
from cereal import log, messaging
1314
from common.params import Params, put_nonblocking
1415
from laika import AstroDog
15-
from laika.constants import SECS_IN_HR, SECS_IN_MIN
16+
from laika.constants import EARTH_ROTATION_RATE, SECS_IN_HR, SECS_IN_MIN, SPEED_OF_LIGHT
1617
from laika.ephemeris import Ephemeris, EphemerisType, convert_ublox_ephem
1718
from laika.gps_time import GPSTime
1819
from laika.helpers import ConstellationId
19-
from laika.raw_gnss import GNSSMeasurement, calc_pos_fix, correct_measurements, process_measurements, read_raw_ublox
20+
from laika.raw_gnss import GNSSMeasurement, correct_measurements, process_measurements, read_raw_ublox
2021
from selfdrive.locationd.models.constants import GENERATED_DIR, ObservationKind
2122
from selfdrive.locationd.models.gnss_kf import GNSSKalman
2223
from selfdrive.locationd.models.gnss_kf import States as GStates
@@ -38,6 +39,7 @@ def __init__(self, valid_const=("GPS", "GLONASS"), auto_update=False, valid_ephe
3839
self.last_cached_t = None
3940
self.save_ephemeris = save_ephemeris
4041
self.load_cache()
42+
self.posfix_functions = {constellation: get_posfix_sympy_fun(constellation) for constellation in (ConstellationId.GPS, ConstellationId.GLONASS)}
4143

4244
def load_cache(self):
4345
cache = Params().get(EPHEMERIS_CACHE)
@@ -66,7 +68,9 @@ def process_ublox_msg(self, ublox_msg, ublox_mono_time: int, block=False):
6668
self.fetch_orbits(latest_msg_t + SECS_IN_MIN, block)
6769
new_meas = read_raw_ublox(report)
6870
processed_measurements = process_measurements(new_meas, self.astro_dog)
69-
pos_fix = calc_pos_fix(processed_measurements, min_measurements=4)
71+
72+
min_measurements = 5 if any(p.constellation_id == ConstellationId.GLONASS for p in processed_measurements) else 4
73+
pos_fix = calc_pos_fix_gauss_newton(processed_measurements, self.posfix_functions, min_measurements=min_measurements)
7074

7175
t = ublox_mono_time * 1e-9
7276
kf_pos_std = None
@@ -84,7 +88,7 @@ def process_ublox_msg(self, ublox_msg, ublox_mono_time: int, block=False):
8488
if est_pos is not None:
8589
corrected_measurements = correct_measurements(processed_measurements, est_pos, self.astro_dog)
8690

87-
self.update_localizer(pos_fix, t, corrected_measurements)
91+
self.update_localizer(est_pos, t, corrected_measurements)
8892
kf_valid = all(self.kf_valid(t))
8993

9094
ecef_pos = self.gnss_kf.x[GStates.ECEF_POS].tolist()
@@ -110,7 +114,7 @@ def process_ublox_msg(self, ublox_msg, ublox_mono_time: int, block=False):
110114
# elif ublox_msg.which == 'ionoData':
111115
# todo add this. Needed to better correct messages offline. First fix ublox_msg.cc to sent them.
112116

113-
def update_localizer(self, pos_fix, t: float, measurements: List[GNSSMeasurement]):
117+
def update_localizer(self, est_pos, t: float, measurements: List[GNSSMeasurement]):
114118
# Check time and outputs are valid
115119
valid = self.kf_valid(t)
116120
if not all(valid):
@@ -123,11 +127,10 @@ def update_localizer(self, pos_fix, t: float, measurements: List[GNSSMeasurement
123127
else:
124128
cloudlog.error("Gnss kalman std too far")
125129

126-
if len(pos_fix) == 0:
130+
if est_pos is None:
127131
cloudlog.info("Position fix not available when resetting kalman filter")
128132
return
129-
post_est = pos_fix[0][:3].tolist()
130-
self.init_gnss_localizer(post_est)
133+
self.init_gnss_localizer(est_pos.tolist())
131134
if len(measurements) > 0:
132135
kf_add_observations(self.gnss_kf, t, measurements)
133136
else:
@@ -227,6 +230,90 @@ def deserialize_hook(dct):
227230
return dct
228231

229232

233+
def calc_pos_fix_gauss_newton(measurements, posfix_functions, x0=None, signal='C1C', min_measurements=6):
234+
'''
235+
Calculates gps fix using gauss newton method
236+
To solve the problem a minimal of 4 measurements are required.
237+
If Glonass is included 5 are required to solve for the additional free variable.
238+
returns:
239+
0 -> list with positions
240+
'''
241+
if x0 is None:
242+
x0 = [0, 0, 0, 0, 0]
243+
n = len(measurements)
244+
if n < min_measurements:
245+
return []
246+
247+
Fx_pos = pr_residual(measurements, posfix_functions, signal=signal)
248+
x = gauss_newton(Fx_pos, x0)
249+
residual, _ = Fx_pos(x, weight=1.0)
250+
return x, residual
251+
252+
253+
def pr_residual(measurements, posfix_functions, signal='C1C'):
254+
def Fx_pos(inp, weight=None):
255+
vals, gradients = [], []
256+
257+
for meas in measurements:
258+
pr = meas.observables[signal]
259+
pr += meas.sat_clock_err * SPEED_OF_LIGHT
260+
261+
w = (1 / meas.observables_std[signal]) if weight is None else weight
262+
263+
val, *gradient = posfix_functions[meas.constellation_id](*inp, pr, *meas.sat_pos, w)
264+
vals.append(val)
265+
gradients.append(gradient)
266+
return np.asarray(vals), np.asarray(gradients)
267+
268+
return Fx_pos
269+
270+
271+
def gauss_newton(fun, b, xtol=1e-8, max_n=25):
272+
for _ in range(max_n):
273+
# Compute function and jacobian on current estimate
274+
r, J = fun(b)
275+
276+
# Update estimate
277+
delta = np.linalg.pinv(J) @ r
278+
b -= delta
279+
280+
# Check step size for stopping condition
281+
if np.linalg.norm(delta) < xtol:
282+
break
283+
return b
284+
285+
286+
def get_posfix_sympy_fun(constellation):
287+
# Unknowns
288+
x, y, z = sympy.Symbol('x'), sympy.Symbol('y'), sympy.Symbol('z')
289+
bc = sympy.Symbol('bc')
290+
bg = sympy.Symbol('bg')
291+
var = [x, y, z, bc, bg]
292+
293+
# Knowns
294+
pr = sympy.Symbol('pr')
295+
sat_x, sat_y, sat_z = sympy.Symbol('sat_x'), sympy.Symbol('sat_y'), sympy.Symbol('sat_z')
296+
weight = sympy.Symbol('weight')
297+
298+
theta = EARTH_ROTATION_RATE * (pr - bc) / SPEED_OF_LIGHT
299+
val = sympy.sqrt(
300+
(sat_x * sympy.cos(theta) + sat_y * sympy.sin(theta) - x) ** 2 +
301+
(sat_y * sympy.cos(theta) - sat_x * sympy.sin(theta) - y) ** 2 +
302+
(sat_z - z) ** 2
303+
)
304+
305+
if constellation == ConstellationId.GLONASS:
306+
res = weight * (val - (pr - bc - bg))
307+
elif constellation == ConstellationId.GPS:
308+
res = weight * (val - (pr - bc))
309+
else:
310+
raise NotImplementedError(f"Constellation {constellation} not supported")
311+
312+
res = [res] + [sympy.diff(res, v) for v in var]
313+
314+
return sympy.lambdify([x, y, z, bc, bg, pr, sat_x, sat_y, sat_z, weight], res)
315+
316+
230317
def main():
231318
sm = messaging.SubMaster(['ubloxGnss'])
232319
pm = messaging.PubMaster(['gnssMeasurements'])

0 commit comments

Comments
 (0)