blob: 40feea3b3da49e34c5d01696f7d9610aa0ebcb1a [file] [log] [blame]
James Kuszmaul61a971f2020-01-01 15:06:18 -08001#!/usr/bin/python3
2# Sample usage:
3# bazel run -c opt //frc971/analysis:plot -- --logfile /tmp/log.fbs --config gyro.pb
4import argparse
5import json
6import os.path
7from pathlib import Path
8import sys
9
10from frc971.analysis.py_log_reader import LogReader
11from frc971.analysis.plot_config_pb2 import PlotConfig, Signal
12from google.protobuf import text_format
13
14import matplotlib
15from matplotlib import pyplot as plt
16
17
18class Plotter:
19 def __init__(self, plot_config: PlotConfig, reader: LogReader):
20 self.config = plot_config
21 self.reader = reader
22 # Data streams, indexed by alias.
23 self.data = {}
24
25 def process_logfile(self):
26 aliases = set()
27 for channel in self.config.channel:
28 if channel.alias in aliases:
29 raise ValueError("Duplicate alias " + channel.alias)
30 aliases.add(channel.alias)
31 if not self.reader.subscribe(channel.name, channel.type):
32 raise ValueError("No such channel with name " + channel.name +
33 " and type " + channel.type)
34
35 self.reader.process()
36
37 for channel in self.config.channel:
38 self.data[channel.alias] = []
39 for message in self.reader.get_data_for_channel(
40 channel.name, channel.type):
41 valid_json = message[2].replace('nan', '"nan"')
42 parsed_json = json.loads(valid_json)
43 self.data[channel.alias].append((message[0], message[1],
44 parsed_json))
45
46 def plot_signal(self, axes: matplotlib.axes.Axes, signal: Signal):
47 if not signal.channel in self.data:
48 raise ValueError("No channel alias " + signal.channel)
49 field_path = signal.field.split('.')
50 monotonic_time = []
51 signal_data = []
52 for entry in self.data[signal.channel]:
53 monotonic_time.append(entry[0] * 1e-9)
54 value = entry[2]
55 for name in field_path:
James Kuszmaul9560e902020-01-11 14:13:08 -080056 # If the value wasn't populated in a given message, fill in
57 # NaN rather than crashing.
58 if name in value:
59 value = value[name]
60 else:
61 value = float("nan")
62 break
James Kuszmaul61a971f2020-01-01 15:06:18 -080063 # Catch NaNs and convert them to floats.
64 value = float(value)
65 signal_data.append(value)
66 label_name = signal.channel + "." + signal.field
James Kuszmaul440ee252020-01-03 20:03:52 -080067 axes.plot(monotonic_time, signal_data, marker='o', label=label_name)
James Kuszmaul61a971f2020-01-01 15:06:18 -080068
69 def plot(self):
James Kuszmaul440ee252020-01-03 20:03:52 -080070 shared_axis = None
James Kuszmaul61a971f2020-01-01 15:06:18 -080071 for figure_config in self.config.figure:
72 fig = plt.figure()
73 num_subplots = len(figure_config.axes)
74 for ii in range(num_subplots):
James Kuszmaul440ee252020-01-03 20:03:52 -080075 axes = fig.add_subplot(num_subplots, 1, ii + 1, sharex=shared_axis)
76 shared_axis = shared_axis or axes
James Kuszmaul61a971f2020-01-01 15:06:18 -080077 axes_config = figure_config.axes[ii]
78 for signal in axes_config.signal:
79 self.plot_signal(axes, signal)
James Kuszmaul440ee252020-01-03 20:03:52 -080080 # Make the legend transparent so we can see behind it.
81 legend = axes.legend(framealpha=0.5)
James Kuszmaul61a971f2020-01-01 15:06:18 -080082 axes.set_xlabel("Monotonic Time (sec)")
James Kuszmaul440ee252020-01-03 20:03:52 -080083 axes.grid(True)
James Kuszmaul61a971f2020-01-01 15:06:18 -080084 if axes_config.HasField("ylabel"):
85 axes.set_ylabel(axes_config.ylabel)
86
87
88def main(argv):
89 parser = argparse.ArgumentParser(
90 description="Plot data from an aos logfile.")
91 parser.add_argument(
92 "--logfile",
93 type=str,
94 required=True,
95 help="Path to the logfile to parse.")
96 parser.add_argument(
97 "--config",
98 type=str,
99 required=True,
100 help="Name of the plot config to use.")
101 parser.add_argument(
102 "--config_dir",
103 type=str,
104 default="frc971/analysis/plot_configs",
105 help="Directory to look for plot configs in.")
106 args = parser.parse_args(argv[1:])
107
108 if not os.path.isdir(args.config_dir):
109 print(args.config_dir + " is not a directory.")
110 return 1
111 config_path = os.path.join(args.config_dir, args.config)
112 if not os.path.isfile(config_path):
113 print(config_path +
114 " does not exist or is not a file--available configs are")
115 for file_name in os.listdir(args.config_dir):
116 print(os.path.basename(file_name))
117 return 1
118
119 config = PlotConfig()
120 with open(config_path) as config_file:
121 text_format.Merge(config_file.read(), config)
122
123 if not os.path.isfile(args.logfile):
124 print(args.logfile + " is not a file.")
125 return 1
126
127 reader = LogReader(args.logfile)
128
129 plotter = Plotter(config, reader)
130 plotter.process_logfile()
131 plotter.plot()
132 plt.show()
133
134 return 0
135
136
137if __name__ == '__main__':
138 sys.exit(main(sys.argv))