@@ -724,7 +724,8 @@ def plot_solution_type(self):
724724
725725 self ._add_figure (name = "solution_type" , figure = figure , title = "Solution Type" )
726726
727- def _plot_displacement (self , source , time , solution_type , displacement_enu_m , std_enu_m ):
727+ def _plot_displacement (self , source , time , solution_type , displacement_enu_m , std_enu_m ,
728+ title = 'Displacement' ):
728729 """!
729730 @brief Generate a topocentric (top-down) plot of position displacement, as well as plot of displacement over
730731 time.
@@ -734,7 +735,7 @@ def _plot_displacement(self, source, time, solution_type, displacement_enu_m, st
734735
735736 # Setup the figure.
736737 topo_figure = make_subplots (rows = 1 , cols = 1 , print_grid = False , shared_xaxes = False ,
737- subplot_titles = ['Displacement' ])
738+ subplot_titles = [title ])
738739 topo_figure ['layout' ]['xaxis1' ].update (title = "East (m)" )
739740 topo_figure ['layout' ]['yaxis1' ].update (title = "North (m)" )
740741
@@ -743,10 +744,10 @@ def _plot_displacement(self, source, time, solution_type, displacement_enu_m, st
743744 time_figure ['layout' ].update (showlegend = True , modebar_add = ['v1hovermode' ])
744745 for i in range (4 ):
745746 time_figure ['layout' ]['xaxis%d' % (i + 1 )].update (title = self .p1_time_label , showticklabels = True )
746- time_figure ['layout' ]['yaxis1' ].update (title = "Displacement (m)" )
747- time_figure ['layout' ]['yaxis2' ].update (title = "Displacement (m)" )
748- time_figure ['layout' ]['yaxis3' ].update (title = "Displacement (m)" )
749- time_figure ['layout' ]['yaxis4' ].update (title = "Displacement (m)" )
747+ time_figure ['layout' ]['yaxis1' ].update (title = f" { title } (m)" )
748+ time_figure ['layout' ]['yaxis2' ].update (title = f" { title } (m)" )
749+ time_figure ['layout' ]['yaxis3' ].update (title = f" { title } (m)" )
750+ time_figure ['layout' ]['yaxis4' ].update (title = f" { title } (m)" )
750751
751752 # Remove invalid solutions.
752753 valid_idx = np .logical_and (~ np .isnan (time ), solution_type != SolutionType .Invalid )
@@ -815,13 +816,25 @@ def _plot_data(name, idx, marker_style=None):
815816
816817 name = source .replace (' ' , '_' ).lower ()
817818 self ._add_figure (name = f"{ name } _top_down" , figure = topo_figure , title = f"{ source } : Top-Down (Topocentric)" )
818- self ._add_figure (name = f"{ name } _displacement" , figure = time_figure , title = f"{ source } : vs. Time" )
819+ self ._add_figure (name = f"{ name } _vs_time" , figure = time_figure , title = f"{ source } : vs. Time" )
820+
821+ def plot_stationary_position_error (self , truth_lla_deg ):
822+ """!
823+ @brief Plot position error vs. a known stationary location.
824+
825+ @param truth_lla_deg The truth LLA location (in degrees/meters).
826+ """
827+ truth_ecef_m = np .array (geodetic2ecef (* truth_lla_deg , deg = True ))
828+ self ._plot_pose_displacement (title = 'Position Error' , center_ecef_m = truth_ecef_m )
819829
820830 def plot_pose_displacement (self ):
821831 """!
822832 @brief Generate a topocentric (top-down) plot of position displacement, as well as plot of displacement over
823833 time.
824834 """
835+ self ._plot_pose_displacement ()
836+
837+ def _plot_pose_displacement (self , title = 'Pose Displacement' , center_ecef_m = None ):
825838 if self .output_dir is None :
826839 return
827840
@@ -847,12 +860,18 @@ def plot_pose_displacement(self):
847860 # Convert to ENU displacement with respect to the median position (we use median instead of centroid just in
848861 # case there are one or two huge outliers).
849862 position_ecef_m = np .array (geodetic2ecef (lat = lla_deg [0 , :], lon = lla_deg [1 , :], alt = lla_deg [2 , :], deg = True ))
850- center_ecef_m = np .median (position_ecef_m , axis = 1 )
863+
864+ if center_ecef_m is None :
865+ center_ecef_m = np .median (position_ecef_m , axis = 1 )
866+
851867 displacement_ecef_m = position_ecef_m - center_ecef_m .reshape (3 , 1 )
852868 c_enu_ecef = get_enu_rotation_matrix (* lla_deg [0 :2 , 0 ], deg = True )
853869 displacement_enu_m = c_enu_ecef .dot (displacement_ecef_m )
854870
855- self ._plot_displacement ('Pose Displacement' , time , solution_type , displacement_enu_m , std_enu_m )
871+ axis_title = 'Error' if title == 'Position Error' else 'Displacement'
872+
873+ self ._plot_displacement (source = title , title = axis_title , time = time , solution_type = solution_type ,
874+ displacement_enu_m = displacement_enu_m , std_enu_m = std_enu_m )
856875
857876 def plot_relative_position (self ):
858877 """!
@@ -882,7 +901,7 @@ def plot_relative_position(self):
882901 displacement_enu_m = relative_position_data .relative_position_enu_m [:, valid_idx ]
883902 std_enu_m = relative_position_data .position_std_enu_m [:, valid_idx ]
884903
885- self ._plot_displacement ('Relative Position vs.Base Station' , time , solution_type , displacement_enu_m , std_enu_m )
904+ self ._plot_displacement ('Position vs. Base Station' , time , solution_type , displacement_enu_m , std_enu_m )
886905
887906 def plot_map (self , mapbox_token ):
888907 """!
@@ -2787,6 +2806,11 @@ def main():
27872806 "\n "
27882807 "\n Truncation is disabled if --plot is specified." %
27892808 (Analyzer .LONG_LOG_DURATION_SEC / 3600.0 , Analyzer .HIGH_MEASUREMENT_RATE_HZ ))
2809+ plot_group .add_argument (
2810+ '--reference' , '--truth' ,
2811+ help = "Specify a reference data to use as a truth source for position, velocity, and orientation. Supported "
2812+ "formats:"
2813+ "\n - Stationary LLA position: 37.1234, -122.526335, 102.34" )
27902814
27912815 plot_function_names = [n for n in dir (Analyzer ) if n .startswith ('plot_' )]
27922816 plot_group .add_argument (
@@ -2899,6 +2923,16 @@ def main():
28992923 _logger .error ('Source identifiers must be integers. Exiting.' )
29002924 sys .exit (1 )
29012925
2926+ # Parse truth data if specified.
2927+ truth_lla_deg = None
2928+ if options .reference is not None :
2929+ m = re .match (r'^(-?\d+(?:\.\d+)),\s*(-?\d+(?:\.\d+)),\s*(-?\d+(?:\.\d+))$' , options .reference )
2930+ if m :
2931+ truth_lla_deg = np .array ((float (m .group (1 )), float (m .group (2 )), float (m .group (3 ))))
2932+ else :
2933+ _logger .error ('Unrecognized reference data format.' )
2934+ sys .exit (1 )
2935+
29022936 # Read pose data from the file.
29032937 analyzer = Analyzer (file = input_path , output_dir = output_dir , ignore_index = options .ignore_index ,
29042938 prefix = options .prefix + '.' if options .prefix is not None else '' ,
@@ -2927,6 +2961,9 @@ def main():
29272961 # LG69T-AH), separate from other sensor measurements controlled by --measurements.
29282962 analyzer .plot_heading_measurements ()
29292963
2964+ if truth_lla_deg is not None :
2965+ analyzer .plot_stationary_position_error (truth_lla_deg )
2966+
29302967 if options .measurements :
29312968 analyzer .plot_imu ()
29322969 analyzer .plot_wheel_data ()
@@ -2960,6 +2997,11 @@ def main():
29602997 analyzer .plot_map (mapbox_token = options .mapbox_token )
29612998 elif func == 'plot_skyplot' :
29622999 analyzer .plot_gnss_skyplot (decimate = False )
3000+ elif func == 'plot_stationary_position_error' :
3001+ if truth_lla_deg is not None :
3002+ analyzer .plot_stationary_position_error (truth_lla_deg )
3003+ else :
3004+ _logger .warning ('No truth data available. Cannot plot position error.' )
29633005 else :
29643006 getattr (analyzer , func )()
29653007
0 commit comments