Skip to content

Commit a722f70

Browse files
Merge pull request #122 from Stanford-NavLab/v0.1.12
V0.1.12
2 parents b284c02 + 7f5e750 commit a722f70

File tree

8 files changed

+307
-38
lines changed

8 files changed

+307
-38
lines changed

docs/source/conf.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
1212
#
1313
import os
1414
import sys
15+
import inspect
16+
import subprocess
17+
from os.path import relpath, dirname
18+
19+
1520
sys.path.insert(0, os.path.abspath('.'))
1621
sys.path.insert(0, os.path.abspath('../'))
1722
sys.path.insert(0, os.path.abspath('../../'))
@@ -40,6 +45,7 @@
4045
extensions = [
4146
'sphinx.ext.autodoc',
4247
'sphinx.ext.napoleon',
48+
'sphinx.ext.linkcode',
4349
'nbsphinx',
4450
'nbsphinx_link',
4551
'IPython.sphinxext.ipython_console_highlighting',
@@ -104,3 +110,124 @@
104110

105111
# document __init__ methods
106112
autoclass_content = 'both'
113+
114+
# Function to find URLs for the source code on GitHub for built docs
115+
116+
# The original code to find the head tag was taken from:
117+
# https://gist.github.com/nlgranger/55ff2e7ff10c280731348a16d569cb73
118+
# This code was modified to use the current commit when the code differs from
119+
# main or a tag
120+
121+
#Default to the main branch
122+
linkcode_revision = "main"
123+
124+
125+
#Default to the main branch, default to main and tags not existing
126+
linkcode_revision = "main"
127+
in_main = False
128+
tagged = False
129+
130+
131+
# lock to commit number
132+
cmd = "git log -n1 --pretty=%H"
133+
head = subprocess.check_output(cmd.split()).strip().decode('utf-8')
134+
# if we are on main's HEAD, use main as reference irrespective of
135+
# what branch you are on
136+
cmd = "git log --first-parent main -n1 --pretty=%H"
137+
main = subprocess.check_output(cmd.split()).strip().decode('utf-8')
138+
if head == main:
139+
in_main = True
140+
141+
# if we have a tag, use tag as reference, irrespective of what branch
142+
# you are actually on
143+
try:
144+
cmd = "git describe --exact-match --tags " + head
145+
tag = subprocess.check_output(cmd.split(" ")).strip().decode('utf-8')
146+
linkcode_revision = tag
147+
tagged = True
148+
except subprocess.CalledProcessError:
149+
pass
150+
151+
# If the current branch is main, or a tag exists, use the branch name.
152+
# If not, use the commit number
153+
if not tagged and not in_main:
154+
linkcode_revision = head
155+
156+
linkcode_url = "https://github.com/Stanford-NavLab/gnss_lib_py/blob/" \
157+
+ linkcode_revision + "/{filepath}#L{linestart}-L{linestop}"
158+
159+
160+
161+
def linkcode_resolve(domain, info):
162+
"""Return GitHub link to Python file for docs.
163+
164+
This function does not return a link for non-Python objects.
165+
For Python objects, `domain == 'py'`, `info['module']` contains the
166+
name of the module containing the method being documented, and
167+
`info['fullname']` contains the name of the method.
168+
169+
Notes
170+
-----
171+
Based off the numpy implementation of linkcode_resolve:
172+
https://github.com/numpy/numpy/blob/2f375c0f9f19085684c9712d602d22a2b4cb4c8e/doc/source/conf.py#L443
173+
Retrieved on 1 Jul, 2023.
174+
"""
175+
if domain != 'py':
176+
return None
177+
178+
modname = info['module']
179+
fullname = info['fullname']
180+
submod = sys.modules.get(modname)
181+
if submod is None:
182+
return None
183+
184+
obj = submod
185+
for part in fullname.split('.'):
186+
try:
187+
obj = getattr(obj, part)
188+
except Exception:
189+
return None
190+
191+
# strip decorators, which would resolve to the source of the decorator
192+
# possibly an upstream bug in getsourcefile, bpo-1764286
193+
try:
194+
unwrap = inspect.unwrap
195+
except AttributeError:
196+
pass
197+
else:
198+
obj = unwrap(obj)
199+
filepath = None
200+
lineno = None
201+
202+
if filepath is None:
203+
try:
204+
filepath = inspect.getsourcefile(obj)
205+
except Exception:
206+
filepath = None
207+
if not filepath:
208+
return None
209+
#NOTE: Re-export filtering turned off because
210+
# # Ignore re-exports as their source files are not within the gnss_lib_py repo
211+
# module = inspect.getmodule(obj)
212+
# if module is not None and not module.__name__.startswith("gnss_lib_py"):
213+
# return "no_module_not_gnss_lib_py"
214+
215+
try:
216+
source, lineno = inspect.getsourcelines(obj)
217+
except Exception:
218+
lineno = ""
219+
# The following line of code first finds the relative path from
220+
# the location of conf.py and then goes up to the root directory
221+
222+
root_glp_path = os.path.join(dirname(os.path.abspath(__file__)), '../..')
223+
filepath = relpath(filepath, root_glp_path)
224+
225+
if lineno:
226+
linestart = lineno
227+
linestop = lineno + len(source) - 1
228+
else:
229+
linestart = ""
230+
linestop = ""
231+
codelink = linkcode_url.format(
232+
filepath=filepath, linestart=linestart, linestop=linestop)
233+
return codelink

docs/source/reference/reference.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,29 @@ in learning more about GNSS:
2626
a visually-appealing and interactive blog post about some of the
2727
basic principles of GNSS positioning.
2828

29+
Reference Documents for GNSS Standards
30+
--------------------------------------
31+
32+
GNSS constellations and receivers use standardized file formats to transfer
33+
information such as estimated receiver coordinates, broadcast ephemeris
34+
parameters, and precise ephimerides.
35+
The parsers in ``gnss_lib_py`` are based on standard documentation for
36+
the GNSS constellations and file types, which are listed below along with
37+
their use in ``gnss_lib_py``.
38+
39+
* *Rinex v2.11* (`version format document <https://geodesy.noaa.gov/corsdata/RINEX211.txt>`__
40+
retrieved on 2nd July, 2023): for parsing broadcast navigation ephimerides.
41+
* *Rinex v3.05* (`version format document <https://files.igs.org/pub/data/format/rinex305.pdf>`__
42+
retrieved on 2nd July, 2023): for parsing broadcast navigation ephimerides.
43+
* *Rinex v4.00* (`version format document <https://files.igs.org/pub/data/format/rinex_4.00.pdf>`__
44+
retrieved on 2nd July, 2023): currently not supported by ``gnss_lib_py``.
45+
* *NMEA* (`reference manual <https://www.sparkfun.com/datasheets/GPS/NMEA%20Reference%20Manual-Rev2.1-Dec07.pdf>`__
46+
retrieved on 23rd June, 2023): for parsing NMEA files with GGA and RMC messages.
47+
* *SP3*: used to determine SV positions for precise
48+
* *GLONASS ICD* (retrieved from this `link <https://www.unavco.org/help/glossary/docs/ICD_GLONASS_4.0_(1998)_en.pdf>`__
49+
retrieved on 27th June, 2023): for determining GLOASS SV states from
50+
broadcast satellite positions, velocities, and accelerations.
51+
2952
Package Architecture
3053
--------------------
3154

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
},

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "gnss-lib-py"
3-
version = "0.1.11"
3+
version = "0.1.12"
44
description = "Modular Python tool for parsing, analyzing, and visualizing Global Navigation Satellite Systems (GNSS) data and state estimates"
55
authors = ["Derek Knowles <dcknowles@stanford.edu>",
66
"Ashwin Kanhere <akanhere@stanford.edu>",

0 commit comments

Comments
 (0)