Skip to content

Commit b8abfca

Browse files
Merge pull request #84 from Stanford-NavLab/derek/viz-axes
Derek/viz axes
2 parents 774c387 + 673589a commit b8abfca

4 files changed

Lines changed: 143 additions & 37 deletions

File tree

gnss_lib_py/utils/coordinates.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import numpy as np
3434

3535
import gnss_lib_py.utils.constants as consts
36+
from gnss_lib_py.parsers.navdata import NavData
3637

3738
EPSILON = 1e-7
3839

@@ -467,3 +468,81 @@ def ecef_to_el_az(rx_pos, sv_pos):
467468
el_az[:,1][el_az[:,1] < 0] += 360
468469

469470
return el_az
471+
472+
def add_el_az(navdata, receiver_state, inplace=False):
473+
"""Adds elevation and azimuth to NavData object.
474+
475+
Parameters
476+
----------
477+
navdata : gnss_lib_py.parsers.navdata.NavData
478+
Instance of the NavData class. Must include ``gps_millis`` as
479+
well as satellite ECEF positions as ``x_sv_m``, ``y_sv_m``,
480+
``z_sv_m``, ``gnss_id`` and ``sv_id``.
481+
receiver_state : gnss_lib_py.parsers.navdata.NavData
482+
Either estimated or ground truth receiver position in ECEF frame
483+
in meters as an instance of the NavData class with the
484+
following rows: ``x_*_m``, ``y_*_m``, ``z_*_m``, ``gps_millis``.
485+
inplace : bool
486+
If false (default) will add elevation and azimuth to a new
487+
NavData instance. If true, will add elevation and azimuth to the
488+
existing NavData instance.
489+
490+
Returns
491+
-------
492+
data_el_az : gnss_lib_py.parsers.navdata.NavData
493+
If inplace is True, adds ``el_sv_deg`` and ``az_sv_deg`` to
494+
the input navdata and returns the same object.
495+
If inplace is False, returns ``el_sv_deg`` and ``az_sv_deg``
496+
in a new NavData instance along with ``gps_millis`` and the
497+
corresponding satellite and receiver rows.
498+
499+
"""
500+
501+
# check for missing rows
502+
navdata.in_rows(["gps_millis","x_sv_m","y_sv_m","z_sv_m",
503+
"gnss_id","sv_id"])
504+
receiver_state.in_rows(["gps_millis"])
505+
506+
# check for receiver_state indexes
507+
rx_idxs = receiver_state.find_wildcard_indexes(["x_*_m","y_*_m",
508+
"z_*_m"],max_allow=1)
509+
510+
sv_el_az = None
511+
for timestamp, _, navdata_subset in navdata.loop_time("gps_millis"):
512+
513+
pos_sv_m = navdata_subset[["x_sv_m","y_sv_m","z_sv_m"]].T
514+
515+
# find time index for receiver_state NavData instance
516+
rx_t_idx = np.argmin(np.abs(receiver_state["gps_millis"] - timestamp))
517+
518+
pos_rx_m = receiver_state[[rx_idxs["x_*_m"][0],
519+
rx_idxs["y_*_m"][0],
520+
rx_idxs["z_*_m"][0]],
521+
rx_t_idx].reshape(1,-1)
522+
523+
timestep_el_az = ecef_to_el_az(pos_rx_m, pos_sv_m)
524+
525+
if sv_el_az is None:
526+
sv_el_az = timestep_el_az.T
527+
else:
528+
sv_el_az = np.hstack((sv_el_az,timestep_el_az.T))
529+
530+
if inplace:
531+
navdata["el_sv_deg"] = sv_el_az[0,:]
532+
navdata["az_sv_deg"] = sv_el_az[1,:]
533+
return navdata
534+
535+
data_el_az = NavData()
536+
data_el_az["gps_millis"] = navdata["gps_millis"]
537+
data_el_az["gnss_id"] = navdata["gnss_id"]
538+
data_el_az["sv_id"] = navdata["sv_id"]
539+
data_el_az["x_sv_m"] = navdata["x_sv_m"]
540+
data_el_az["y_sv_m"] = navdata["y_sv_m"]
541+
data_el_az["z_sv_m"] = navdata["z_sv_m"]
542+
data_el_az[rx_idxs["x_*_m"][0]] = receiver_state[rx_idxs["x_*_m"][0]]
543+
data_el_az[rx_idxs["y_*_m"][0]] = receiver_state[rx_idxs["y_*_m"][0]]
544+
data_el_az[rx_idxs["z_*_m"][0]] = receiver_state[rx_idxs["z_*_m"][0]]
545+
data_el_az["el_sv_deg"] = sv_el_az[0,:]
546+
data_el_az["az_sv_deg"] = sv_el_az[1,:]
547+
548+
return data_el_az

gnss_lib_py/utils/visualizations.py

Lines changed: 27 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
import gnss_lib_py.utils.file_operations as fo
2323
from gnss_lib_py.parsers.navdata import NavData
24-
from gnss_lib_py.utils.coordinates import ecef_to_el_az
24+
from gnss_lib_py.utils.coordinates import add_el_az
2525

2626
STANFORD_COLORS = [
2727
"#8C1515", # cardinal red
@@ -274,39 +274,9 @@ def plot_skyplot(navdata, receiver_state, save=False, prefix="",
274274

275275
if not isinstance(prefix, str):
276276
raise TypeError("Prefix must be a string.")
277-
# check for missing rows
278-
navdata.in_rows(["gps_millis","x_sv_m","y_sv_m","z_sv_m",
279-
"gnss_id","sv_id"])
280-
receiver_state.in_rows(["gps_millis"])
281-
282-
# check for receiver_state indexes
283-
rx_idxs = receiver_state.find_wildcard_indexes(["x_*_m","y_*_m",
284-
"z_*_m"],max_allow=1)
285277

286278
if "el_sv_deg" not in navdata.rows or "az_sv_deg" not in navdata.rows:
287-
sv_el_az = None
288-
289-
for timestamp, _, navdata_subset in navdata.loop_time("gps_millis"):
290-
291-
pos_sv_m = navdata_subset[["x_sv_m","y_sv_m","z_sv_m"]].T
292-
293-
# find time index for receiver_state NavData instance
294-
rx_t_idx = np.argmin(np.abs(receiver_state["gps_millis"] - timestamp))
295-
296-
pos_rx_m = receiver_state[[rx_idxs["x_*_m"][0],
297-
rx_idxs["y_*_m"][0],
298-
rx_idxs["z_*_m"][0]],
299-
rx_t_idx].reshape(1,-1)
300-
301-
timestep_el_az = ecef_to_el_az(pos_rx_m, pos_sv_m)
302-
303-
if sv_el_az is None:
304-
sv_el_az = timestep_el_az.T
305-
else:
306-
sv_el_az = np.hstack((sv_el_az,timestep_el_az.T))
307-
308-
navdata["el_sv_deg"] = sv_el_az[0,:]
309-
navdata["az_sv_deg"] = sv_el_az[1,:]
279+
add_el_az(navdata, receiver_state, inplace=True)
310280

311281
# create new figure
312282
fig = plt.figure(figsize=(6,4.5))
@@ -579,6 +549,23 @@ def _get_label(inputs):
579549
if not isinstance(inputs,dict):
580550
raise TypeError("_get_label input must be dictionary.")
581551

552+
# handle units specially.
553+
units = {"m","km",
554+
"deg","rad",
555+
"sec","s","hr","min",
556+
"mps","kmph","mph",
557+
"dgps","radps",
558+
"mps2",
559+
}
560+
unit_replacements = {
561+
"mps" : "m/s",
562+
"kmph" : "km/hr",
563+
"mph" : "miles/hr",
564+
"degps" : "deg/s",
565+
"radps" : "rad/s",
566+
"mps2" : "m/s^2",
567+
}
568+
582569
label = ""
583570
for key, value in inputs.items():
584571

@@ -593,8 +580,14 @@ def _get_label(inputs):
593580
except ValueError:
594581
pass
595582

596-
value = value.upper()
597-
value = value.replace("_"," ")
583+
value = value.split("_")
584+
if value[-1] in units:
585+
# make units lowercase and bracketed.
586+
if value[-1] in unit_replacements:
587+
value[-1] = unit_replacements[value[-1]]
588+
value = " ".join(value[:-1]).upper() + " [" + value[-1] + "]"
589+
else:
590+
value = " ".join(value).upper()
598591

599592
if key == "gnss_id": # use GNSS specific capitalization
600593
constellation_map = {"GALILEO" : "Galileo",

tests/utils/test_coordinates.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
import gnss_lib_py.utils.constants as consts
1515
from gnss_lib_py.parsers.android import AndroidDerived2022
16-
from gnss_lib_py.utils.coordinates import ecef_to_el_az
16+
from gnss_lib_py.utils.coordinates import ecef_to_el_az, add_el_az
1717
from gnss_lib_py.utils.coordinates import geodetic_to_ecef
1818
from gnss_lib_py.utils.coordinates import ecef_to_geodetic, LocalCoord
1919

@@ -409,3 +409,33 @@ def test_ecef_to_el_az_fails(set_sv_pos, set_rx_pos):
409409
with pytest.raises(RuntimeError) as excinfo:
410410
ecef_to_el_az(set_rx_pos,set_sv_pos.T)
411411
assert "Nx3" in str(excinfo.value)
412+
413+
414+
@pytest.mark.parametrize('navdata',[
415+
lazy_fixture('derived_2022'),
416+
])
417+
def test_add_el_az(navdata):
418+
"""Test for plotting skyplot.
419+
420+
Parameters
421+
----------
422+
navdata : AndroidDerived
423+
Instance of AndroidDerived for testing.
424+
425+
"""
426+
427+
receiver_state = navdata.copy(rows=["gps_millis",
428+
"WlsPositionXEcefMeters",
429+
"WlsPositionYEcefMeters",
430+
"WlsPositionZEcefMeters"])
431+
row_map = {
432+
"WlsPositionXEcefMeters" : "x_rx_m",
433+
"WlsPositionYEcefMeters" : "y_rx_m",
434+
"WlsPositionZEcefMeters" : "z_rx_m",
435+
}
436+
receiver_state.rename(row_map,inplace=True)
437+
data_el_az = add_el_az(navdata, receiver_state)
438+
np.testing.assert_array_almost_equal(data_el_az["el_sv_deg"],
439+
navdata["el_sv_deg"])
440+
np.testing.assert_array_almost_equal(data_el_az["az_sv_deg"],
441+
navdata["az_sv_deg"])

tests/utils/test_visualizations.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -369,11 +369,11 @@ def test_plot_skyplot(navdata, state_estimate):
369369
navdata["x_sv_m",col_idx] = np.nan
370370

371371
# don't save figures
372-
fig = viz.plot_skyplot(navdata, state_estimate, save=True)
372+
fig = viz.plot_skyplot(navdata.copy(), state_estimate, save=False)
373373
viz.close_figures(fig)
374374

375375
with pytest.raises(TypeError) as excinfo:
376-
viz.plot_skyplot(navdata, state_estimate, save=True, prefix=1)
376+
viz.plot_skyplot(navdata.copy(), state_estimate, save=True, prefix=1)
377377
assert "Prefix" in str(excinfo.value)
378378

379379
for row in ["x_sv_m","y_sv_m","z_sv_m","gps_millis"]:
@@ -415,6 +415,10 @@ def test_get_label():
415415
assert viz._get_label({"gnss_id" : "galileo",
416416
"signal_type" : "b1i"}) == "Galileo B1i"
417417

418+
assert viz._get_label({"row" : "x_rx_m"}) == "X RX [m]"
419+
assert viz._get_label({"row" : "lat_rx_deg"}) == "LAT RX [deg]"
420+
assert viz._get_label({"row" : "vx_sv_mps"}) == "VX SV [m/s]"
421+
418422
with pytest.raises(TypeError) as excinfo:
419423
viz._get_label(["should","fail"])
420424
assert "dictionary" in str(excinfo.value)

0 commit comments

Comments
 (0)