Skip to content

Commit 5eb0c3c

Browse files
gast04Kurt Nistelberger
andauthored
combine residual calculation from openpilot (#116)
* combine resiudals and remove scipy * fix usage * update walkthrough Co-authored-by: Kurt Nistelberger <[email protected]>
1 parent 3d8a1ff commit 5eb0c3c

File tree

6 files changed

+204
-110
lines changed

6 files changed

+204
-110
lines changed

examples/Kalman.ipynb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,8 @@
137137
}
138138
],
139139
"source": [
140-
"from laika.raw_gnss import process_measurements, correct_measurements, calc_pos_fix\n",
140+
"from laika.raw_gnss import process_measurements, correct_measurements\n",
141+
"from laika.opt import calc_pos_fix\n",
141142
"from tqdm.auto import tqdm\n",
142143
"\n",
143144
"# We want to group measurements by measurement epoch\n",
@@ -305,4 +306,4 @@
305306
},
306307
"nbformat": 4,
307308
"nbformat_minor": 4
308-
}
309+
}

examples/Walkthrough.ipynb

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@
180180
"outputs": [],
181181
"source": [
182182
"import laika.raw_gnss as raw\n",
183+
"import laika.opt as opt\n",
183184
"import laika.helpers as helpers\n",
184185
"\n",
185186
"# this example data is the from the example segment\n",
@@ -351,12 +352,12 @@
351352
"corrected_measurements_by_epoch = []\n",
352353
"for meas_epoch in measurements_by_epoch[::10]:\n",
353354
" processed = raw.process_measurements(meas_epoch, dog)\n",
354-
" est_pos = raw.calc_pos_fix(processed)[0][:3]\n",
355+
" est_pos = opt.calc_pos_fix(processed)[0][:3]\n",
355356
" corrected = raw.correct_measurements(meas_epoch, est_pos, dog)\n",
356357
" corrected_measurements_by_epoch.append(corrected)\n",
357-
" pos_solutions.append(raw.calc_pos_fix(corrected))\n",
358+
" pos_solutions.append(opt.calc_pos_fix(corrected))\n",
358359
" # you need an estimate position to calculate a velocity fix\n",
359-
" vel_solutions.append(raw.calc_vel_fix(corrected, pos_solutions[-1][0]))"
360+
" vel_solutions.append(opt.calc_vel_fix(corrected, pos_solutions[-1][0]))"
360361
]
361362
},
362363
{

laika/dgps.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from .gps_time import GPSTime
66
from .constants import SECS_IN_YEAR
77
from . import raw_gnss as raw
8+
from . import opt
89
from .rinex_file import RINEXFile
910
from .downloader import download_cors_coords
1011
from .helpers import get_constellation
@@ -107,8 +108,9 @@ def parse_dgps(station_id, station_obs_file_path, dog, max_distance=100000, requ
107108
station_delays[signal] = {}
108109
for i, proc_measurement in enumerate(proc_measurements):
109110
times.append(proc_measurement[0].recv_time)
110-
Fx_pos = raw.pr_residual(proc_measurement, signal=signal)
111-
residual = -np.array(Fx_pos(list(station_pos) + [0, 0]))
111+
Fx_pos = opt.pr_residual(proc_measurement, signal=signal)
112+
residual, _ = Fx_pos(list(station_pos) + [0,0])
113+
residual = -np.array(residual)
112114
for j, m in enumerate(proc_measurement):
113115
prn = m.prn
114116
if prn not in station_delays[signal]:

laika/opt.py

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import sympy
2+
import numpy as np
3+
from typing import List
4+
5+
from .constants import EARTH_ROTATION_RATE, SPEED_OF_LIGHT
6+
from .helpers import ConstellationId
7+
from .raw_gnss import GNSSMeasurement
8+
9+
10+
def gauss_newton(fun, b, xtol=1e-8, max_n=25):
11+
for _ in range(max_n):
12+
# Compute function and jacobian on current estimate
13+
r, J = fun(b)
14+
15+
# Update estimate
16+
delta = np.linalg.pinv(J) @ r
17+
b -= delta
18+
19+
# Check step size for stopping condition
20+
if np.linalg.norm(delta) < xtol:
21+
break
22+
return b
23+
24+
25+
def calc_pos_fix(measurements, posfix_functions=None, x0=None, no_weight=False, signal='C1C', min_measurements=6):
26+
'''
27+
Calculates gps fix using gauss newton method
28+
To solve the problem a minimal of 4 measurements are required.
29+
If Glonass is included 5 are required to solve for the additional free variable.
30+
returns:
31+
0 -> list with positions
32+
1 -> pseudorange errs
33+
'''
34+
if x0 is None:
35+
x0 = [0, 0, 0, 0, 0]
36+
37+
if len(measurements) < min_measurements:
38+
return [],[]
39+
40+
Fx_pos = pr_residual(measurements, posfix_functions, signal=signal, no_weight=no_weight, no_nans=True)
41+
x = gauss_newton(Fx_pos, x0)
42+
residual, _ = Fx_pos(x, no_weight=True)
43+
return x.tolist(), residual.tolist()
44+
45+
46+
def calc_vel_fix(measurements, est_pos, velfix_function=None, v0=None, no_weight=False, signal='D1C', min_measurements=6):
47+
'''
48+
Calculates gps velocity fix using gauss newton method
49+
returns:
50+
0 -> list with velocities
51+
1 -> pseudorange_rate errs
52+
'''
53+
if v0 is None:
54+
v0 = [0, 0, 0, 0]
55+
56+
if len(measurements) < min_measurements:
57+
return [], []
58+
59+
Fx_vel = prr_residual(measurements, est_pos, velfix_function, signal=signal, no_weight=no_weight, no_nans=True)
60+
v = gauss_newton(Fx_vel, v0)
61+
residual, _ = Fx_vel(v, no_weight=True)
62+
return v.tolist(), residual.tolist()
63+
64+
65+
def get_posfix_sympy_fun(constellation):
66+
# Unknowns
67+
x, y, z = sympy.Symbol('x'), sympy.Symbol('y'), sympy.Symbol('z')
68+
bc = sympy.Symbol('bc')
69+
bg = sympy.Symbol('bg')
70+
zero_theta = sympy.Symbol('zero_theta')
71+
var = [x, y, z, bc, bg]
72+
73+
# Knowns
74+
pr = sympy.Symbol('pr')
75+
sat_x, sat_y, sat_z = sympy.Symbol('sat_x'), sympy.Symbol('sat_y'), sympy.Symbol('sat_z')
76+
weight = sympy.Symbol('weight')
77+
78+
theta = (EARTH_ROTATION_RATE * (pr - bc) / SPEED_OF_LIGHT)*zero_theta
79+
val = sympy.sqrt(
80+
(sat_x * sympy.cos(theta) + sat_y * sympy.sin(theta) - x) ** 2 +
81+
(sat_y * sympy.cos(theta) - sat_x * sympy.sin(theta) - y) ** 2 +
82+
(sat_z - z) ** 2
83+
)
84+
85+
if constellation == ConstellationId.GLONASS:
86+
res = weight * (val - (pr - bc - bg))
87+
elif constellation == ConstellationId.GPS:
88+
res = weight * (val - (pr - bc))
89+
else:
90+
raise NotImplementedError(f"Constellation {constellation} not supported")
91+
92+
res = [res] + [sympy.diff(res, v) for v in var]
93+
94+
return sympy.lambdify([x, y, z, bc, bg, pr, zero_theta, sat_x, sat_y, sat_z, weight], res, modules=["numpy"])
95+
96+
97+
def get_velfix_sympy_func():
98+
# implementing this without sympy.Matrix gives a 2x speedup at generation
99+
100+
# knowns, receiver position, satellite position, satellite velocity
101+
ep_x, ep_y, ep_z = sympy.Symbol('ep_x'), sympy.Symbol('ep_y'), sympy.Symbol('ep_z')
102+
est_pos = np.array([ep_x, ep_y, ep_z])
103+
sp_x, sp_y, sp_z = sympy.Symbol('sp_x'), sympy.Symbol('sp_y'), sympy.Symbol('sp_z')
104+
sat_pos = np.array([sp_x, sp_y, sp_z])
105+
sv_x, sv_y, sv_z = sympy.Symbol('sv_x'), sympy.Symbol('sv_y'), sympy.Symbol('sv_z')
106+
sat_vel = np.array([sv_x, sv_y, sv_z])
107+
observables = sympy.Symbol('observables')
108+
weight = sympy.Symbol('weight')
109+
110+
# unknown, receiver velocity
111+
v_x, v_y, v_z = sympy.Symbol('v_x'), sympy.Symbol('v_y'), sympy.Symbol('v_z')
112+
vel = np.array([v_x, v_y, v_z])
113+
vel_o = sympy.Symbol('vel_o')
114+
115+
loss = sat_pos - est_pos
116+
loss /= sympy.sqrt(loss.dot(loss))
117+
118+
nv = loss.dot(sat_vel - vel)
119+
ov = (observables - vel_o)
120+
res = (nv - ov)*weight
121+
122+
res = [res] + [sympy.diff(res, v) for v in [v_x, v_y, v_z, vel_o]]
123+
124+
return sympy.lambdify([
125+
ep_x, ep_y, ep_z, sp_x, sp_y, sp_z,
126+
sv_x, sv_y, sv_z, observables, weight,
127+
v_x, v_y, v_z, vel_o
128+
],
129+
res, modules=["numpy"])
130+
131+
132+
def pr_residual(measurements: List[GNSSMeasurement], posfix_functions=None, signal='C1C', no_weight=False, no_nans=False):
133+
134+
if posfix_functions is None:
135+
posfix_functions = {constellation: get_posfix_sympy_fun(constellation) for constellation in (ConstellationId.GPS, ConstellationId.GLONASS)}
136+
137+
def Fx_pos(inp, no_weight=no_weight):
138+
vals, gradients = [], []
139+
140+
for meas in measurements:
141+
if signal in meas.observables_final and np.isfinite(meas.observables_final[signal]):
142+
pr = meas.observables_final[signal]
143+
sat_pos = meas.sat_pos_final
144+
zero_theta = 0
145+
elif signal in meas.observables and np.isfinite(meas.observables[signal]) and meas.processed:
146+
pr = meas.observables[signal]
147+
pr += meas.sat_clock_err * SPEED_OF_LIGHT
148+
sat_pos = meas.sat_pos
149+
zero_theta = 1
150+
else:
151+
if not no_nans:
152+
vals.append(np.nan)
153+
gradients.append(np.nan)
154+
continue
155+
156+
w = 1.0 if no_weight or meas.observables_std[signal] == 0 else (1 / meas.observables_std[signal])
157+
val, *gradient = posfix_functions[meas.constellation_id](*inp, pr, zero_theta, *sat_pos, w)
158+
vals.append(val)
159+
gradients.append(gradient)
160+
return np.asarray(vals), np.asarray(gradients)
161+
return Fx_pos
162+
163+
164+
def prr_residual(measurements: List[GNSSMeasurement], est_pos, velfix_function=None, signal='D1C', no_weight=False, no_nans=False):
165+
166+
if velfix_function is None:
167+
velfix_function = get_velfix_sympy_func()
168+
169+
def Fx_vel(vel, no_weight=no_weight):
170+
vals, gradients = [], []
171+
172+
for meas in measurements:
173+
if signal not in meas.observables or not np.isfinite(meas.observables[signal]):
174+
if not no_nans:
175+
vals.append(np.nan)
176+
gradients.append(np.nan)
177+
continue
178+
179+
sat_pos = meas.sat_pos_final if meas.corrected else meas.sat_pos
180+
weight = 1.0 if no_weight or meas.observables_std[signal] == 0 else (1 / meas.observables_std[signal])
181+
182+
val, *gradient = velfix_function(est_pos[0], est_pos[1], est_pos[2],
183+
sat_pos[0], sat_pos[1], sat_pos[2],
184+
meas.sat_vel[0], meas.sat_vel[1], meas.sat_vel[2],
185+
meas.observables[signal], weight,
186+
vel[0], vel[1], vel[2], vel[3])
187+
vals.append(val)
188+
gradients.append(gradient)
189+
190+
return np.asarray(vals), np.asarray(gradients)
191+
return Fx_vel

laika/raw_gnss.py

Lines changed: 0 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -292,108 +292,6 @@ def read_rinex_obs(obsdata) -> List[List[GNSSMeasurement]]:
292292
return measurements
293293

294294

295-
def calc_pos_fix(measurements, x0=[0, 0, 0, 0, 0], no_weight=False, signal='C1C', min_measurements=6):
296-
'''
297-
Calculates gps fix with WLS optimizer
298-
299-
returns:
300-
0 -> list with positions
301-
1 -> pseudorange errs
302-
'''
303-
import scipy.optimize as opt # Only use scipy here
304-
305-
n = len(measurements)
306-
if n < min_measurements:
307-
return []
308-
309-
Fx_pos = pr_residual(measurements, signal=signal, no_weight=no_weight, no_nans=True)
310-
opt_pos = opt.least_squares(Fx_pos, x0).x
311-
return opt_pos, Fx_pos(opt_pos, no_weight=True)
312-
313-
314-
def calc_vel_fix(measurements, est_pos, v0=[0, 0, 0, 0], no_weight=False, signal='D1C'):
315-
'''
316-
Calculates gps velocity fix with WLS optimizer
317-
318-
returns:
319-
0 -> list with velocities
320-
1 -> pseudorange_rate errs
321-
'''
322-
import scipy.optimize as opt # Only use scipy here
323-
324-
n = len(measurements)
325-
if n < 6:
326-
return []
327-
328-
Fx_vel = prr_residual(measurements, est_pos, signal=signal, no_weight=no_weight, no_nans=True)
329-
opt_vel = opt.least_squares(Fx_vel, v0).x
330-
return opt_vel, Fx_vel(opt_vel, no_weight=True)
331-
332-
333-
def pr_residual(measurements: List[GNSSMeasurement], signal='C1C', no_weight=False, no_nans=False):
334-
# solve for pos
335-
def Fx_pos(xxx_todo_changeme, no_weight=no_weight):
336-
(x, y, z, bc, bg) = xxx_todo_changeme
337-
rows = []
338-
339-
for meas in measurements:
340-
if signal in meas.observables_final and np.isfinite(meas.observables_final[signal]):
341-
pr = meas.observables_final[signal]
342-
sat_pos = meas.sat_pos_final
343-
theta = 0
344-
elif signal in meas.observables and np.isfinite(meas.observables[signal]) and meas.processed:
345-
pr = meas.observables[signal]
346-
pr += meas.sat_clock_err * constants.SPEED_OF_LIGHT
347-
sat_pos = meas.sat_pos
348-
theta = constants.EARTH_ROTATION_RATE * (pr - bc) / constants.SPEED_OF_LIGHT
349-
else:
350-
if not no_nans:
351-
rows.append(np.nan)
352-
continue
353-
if no_weight:
354-
weight = 1
355-
else:
356-
weight = (1 / meas.observables_std[signal])
357-
358-
val = np.sqrt(
359-
(sat_pos[0] * np.cos(theta) + sat_pos[1] * np.sin(theta) - x) ** 2 +
360-
(sat_pos[1] * np.cos(theta) - sat_pos[0] * np.sin(theta) - y) ** 2 +
361-
(sat_pos[2] - z) ** 2
362-
)
363-
if meas.constellation_id == ConstellationId.GLONASS:
364-
rows.append(weight * (val - (pr - bc - bg)))
365-
elif meas.constellation_id == ConstellationId.GPS:
366-
rows.append(weight * (val - (pr - bc)))
367-
return rows
368-
return Fx_pos
369-
370-
371-
def prr_residual(measurements, est_pos, signal='D1C', no_weight=False, no_nans=False):
372-
# solve for vel
373-
def Fx_vel(vel, no_weight=no_weight):
374-
rows = []
375-
for meas in measurements:
376-
if signal not in meas.observables or not np.isfinite(meas.observables[signal]):
377-
if not no_nans:
378-
rows.append(np.nan)
379-
continue
380-
if meas.corrected:
381-
sat_pos = meas.sat_pos_final
382-
else:
383-
sat_pos = meas.sat_pos
384-
if no_weight:
385-
weight = 1
386-
else:
387-
weight = (1 / meas.observables[signal])
388-
los_vector = (sat_pos - est_pos[0:3]
389-
) / np.linalg.norm(sat_pos - est_pos[0:3])
390-
rows.append(
391-
weight * ((meas.sat_vel - vel[0:3]).dot(los_vector) -
392-
(meas.observables[signal] - vel[3])))
393-
return rows
394-
return Fx_vel
395-
396-
397295
def get_Q(recv_pos, sat_positions):
398296
local = LocalCoord.from_ecef(recv_pos)
399297
sat_positions_rel = local.ecef2ned(sat_positions)

tests/test_positioning.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from laika.rinex_file import RINEXFile
1111
from laika.dgps import get_station_position
1212
import laika.raw_gnss as raw
13+
import laika.opt as opt
1314

1415

1516
class TestPositioning(unittest.TestCase):
@@ -50,7 +51,7 @@ def run_station_position(self, length):
5051
# fixes for every epoch (every 30s) over 24h.
5152
ests = []
5253
for corr in tqdm(rinex_corr_grouped):
53-
ret = raw.calc_pos_fix(corr)
54+
ret = opt.calc_pos_fix(corr)
5455
if len(ret) > 0:
5556
fix, _ = ret
5657
ests.append(fix)

0 commit comments

Comments
 (0)