Source code for pydgilib_extra.dgilib_plot

from time import sleep
import csv
import sys

from pydgilib_extra.dgilib_extra_config import *
from pydgilib_extra.dgilib_calculations import StreamingCalculation
#from tests_plot.dgilib_averages import HoldTimes

import matplotlib.pyplot as plt; plt.ion()
import matplotlib
from matplotlib.widgets import Slider, Button, TextBox

from threading import Lock

float_epsilon = sys.float_info.epsilon

[docs]class HoldTimes(StreamingCalculation):
[docs] def identify_toggle_times(self, pin, data_gpio=None, gpio_start_index=0): if data_gpio is None: data_gpio = self.data if gpio_start_index == None: gpio_start_index = self.index if len(data_gpio.timestamps) <= 1: return [] # We can't identify intervals with only one value if gpio_start_index > (len(data_gpio.timestamps) - 1): return [] # We're being asked to do an index that does not exist yet, so just skip toggle_times = [] true_to_false_toggle_times = [] false_to_true_toggle_times = [] #last_toggle_timestamp = data_gpio.timestamps[start_index] last_toggle_value = data_gpio.values[gpio_start_index][pin] #print("New data, starting on pin " + str(pin) + " at timestamp " + str(data_gpio.timestamps[start_index]) + " of value " + str(last_toggle_value) + ". Index is: " + str(start_index)) for i in range(gpio_start_index, len(data_gpio)): if last_toggle_value != data_gpio.values[i][pin]: #print("Detected toggle on pin " + str(pin) + " at timestamp " + str(data_gpio.timestamps[i]) + " of value " + str(data_gpio.values[i][pin]) + ". Index is: " + str(i)) toggle_times.append(data_gpio.timestamps[i]) if last_toggle_value == True: true_to_false_toggle_times.append(data_gpio.timestamps[i]) if last_toggle_value == False: false_to_true_toggle_times.append(data_gpio.timestamps[i]) #last_toggle_timestamp = data_gpio.timestamps[i] last_toggle_value = data_gpio.values[i][pin] # A smart printing for debugging this function # Either leave 'debug = False' or comment it, but don't lose it debug = False if debug: for (t, v) in data_gpio: # print(str((t,v))) if t in toggle_times: print("\t" + str(t) + "\t\t" + str(v) + "\t <-- toggled") else: print("\t" + str(t) + "\t\t" + str(v)) # , last_toggle_index return toggle_times, true_to_false_toggle_times, false_to_true_toggle_times
[docs] def identify_hold_times(self, pin, pin_value, data_gpio=None): if data_gpio is None: data_gpio = self.data if len(data_gpio.timestamps) <= 1: return [] # We can't identify intervals with only one value if self.index > (len(data_gpio.timestamps) - 1): return [] # We're being asked to do an index that does not exist yet, so just skip hold_times = [] (_, true_to_false_times, false_to_true_times) = self.identify_toggle_times( pin, data_gpio, self.index) #print("T2F: " + str(true_to_false_times)) #print("F2T: " + str(false_to_true_times)) if len(false_to_true_times) == 0: return if len(true_to_false_times) == 0: return if (pin_value == True): # A fix if false_to_true_times[0] > true_to_false_times[0]: true_to_false_times.pop(0) hold_times = zip(false_to_true_times, true_to_false_times) elif (pin_value == False): # A fix if true_to_false_times[0] > false_to_true_times[0]: false_to_true_times.pop(0) hold_times = zip(true_to_false_times, false_to_true_times) # A smart printing for debugging this function # Either leave 'debug = False' or comment it, but don't lose it debug = False if debug: ht_zip = list(zip(*hold_times)) for (t, v) in data_gpio: # print(str((t,v))) if t in ht_zip[0]: print("\t" + str(t) + "\t\t" + str(v) + "\t <-- start") elif t in ht_zip[1]: print("\t" + str(t) + "\t\t" + str(v) + "\t <-- stop") else: print("\t" + str(t) + "\t\t" + str(v)) hold_times_list = list(hold_times) try: self.index = data_gpio.timestamps.index( hold_times_list[-1][-1]) + 1 except IndexError: # If you remove this, you get an error pass return hold_times_list
[docs]class DGILibPlot(object): """DGILibPlot The `DGILibPlot` class is responsible with plotting the electrical current (Amperes) data and gpio state data (values of `1`/`0`) obtained from an Atmel board. The X axis represents time in seconds, while the Y axis represents the electrical current in Amperes. There are two ways that the gpio pins state can be shown along with the electrical current. One is the `line` method and one is the `highlight` method. The `line` method shows a square waveform, a typical byproduct of the digital signal that gpio pins usually have. The `highlight` method highlights only particular parts of the plot with semi-transparent coloured areas, where the pins have a value of interest (set using the ``plot_pins_values`` argument of the class). Below are shown some examples of `DGILibPlot` and the two methods of drawing the gpio pins (`line`/`highlight`). **Example plots using "line" method:** .. figure:: images/plot_line_1.png :scale: 60% Figure 1: Example of plot with the 'line' method chosen for the drawing of pins. All of the pins are being plotted, so you can always see their `True`/`False` values. .. figure:: images/plot_line_2.png :scale: 60% Figure 2: The same plot with the same data as figure 1, only zoomed in. .. figure:: images/plot_line_3.png :scale: 60% Figure 3: The same plot with the same data as figure 1 and 2, only even more zoomed in. Here we can clearly see that gpio pins 0, 2 and 3 have defaulted on a single value all along the program's execution on the board. We can however clearly see the toggling of pin 1, represented in orange. **Example plots using "highlight" method:** .. figure:: images/plot_highlight_1.png :scale: 60% Figure 4: Example of plot with the 'highlight' method chosen for the drawing of pins. The time the pins are holding the value of interest (in this case, `True` value) is small every time. This is why we can see the highlighted areas looking more like vertical lines when zoomed out. The only pin being toggled by the program on the board is pin 1, hence it's why we only see one color of highlighted areas. .. figure:: images/plot_highlight_2.png :scale: 60% Figure 5: The same plot with the same data as figure 1, only zoomed in. .. figure:: images/plot_highlight_3.png :scale: 60% Figure 6: The same plot with the same data as figure 1 and 2, only even more zoomed in. Now we can see one of the the highlight area in its proper form. **Parameters** The parameters listed in the section below can be passed as arguments when initializing the `DGILibPlot` object, or as arguments to a `DGILibExtra` object. They can be included in a configuration dictionary (:py:class:`dict` object) and then unwrapped in the initialization function call of either the `DGILibPlot` object or the `DGILibExtra` object (by doing: ``dgilib_plot = DGILibPlot(**config_dict)`` or ``with DGILibExtra(**config_dict) as dgilib: ...``). Parameters ---------- dgilib_extra : DGILibExtra A `DGILibExtra` object can be specified, from where the plot can obtain the electrical current and gpio state data. If a `DGILibExtra` object is not desired to be specified however, then it should be set to `None`. (the default is `None`, meaning that measurements data (in the form of a :class:`DGILibData` should be manually called as function updates)) fig : matplotlib.pyplot.figure, optional If it is wanted so that the data is to be plotted on an already existing `matplotlib` figure, then the object representing the already instantiated figure can be specified for this parameter. For example, the electrical current data and gpio state data can be plotted in a subplot of a figure window that holds other plots as well. (the default is `None`, meaning that a new figure object will be created internally) ax : matplotlib.pyplot.axes, optional If it is wanted so that the data is to be plotted on an already existing `matplotlib` axes, then the object representing the already instantiated axes can be specified for this parameter. (the default is `None`, meaning that a new axes object will be created internally) ln : matplotlib.pyplot.lines2d, optional If it is wanted so that the data is to be plotted on an already existing `matplotlib` `Lines2D` object, then the object representing the already instantiated `Lines2D` object can be specified for this parameter. (the default is `None`, meaning that a new `Lines2D` object will be created internally) window_title : str, optional If another window title than the default is desired to be used, it can be specified here. (the default is ``Plot of current (in amperes) and gpio pins``) plot_xmax : int, optional This *initializes* the figure view to a maximum of `plot_xmax` on the X axis, where the data to be plotted. Later, the user can change the figure view using the bottom sliders of the plot figure. (the default is an arbitrary `10`) plot_ymax : int, optional This *initializes* the figure view to a maximum of `plot_xmax` on the Y axis, where the data to be plotted. Later, the user can change the figure view using the bottom sliders of the plot figure. (the default is `0.005`, meaning 5 mA, so that something as energy consuming as a blinking LED can be shown by a `DGILibPlot` with default settings) plot_pins : list(bool, bool, bool, bool), optional Set the pins to be drawn in the plot, out of the 4 GPIO available pins that the Atmel board gives data about to be sent through the computer through the Atmel Embedded Debugger (EDBG) Data Gateway Interface (DGI). (the default is `[True, True, True, True]`, meaning all pins are drawn) plot_pins_method : str, optional Set the *method* of drawing the pin. The values can be either ``"line"`` or ``"highlight"``. Refer to the above figures to see the differences. (the default is `"highlight"`) plot_pins_colors : list(str, str, str, str), optional Set the colors of the semi-transparent highlight areas drawn when using the `highlight` method, or the lines when using the `lines` method of drawing pins. (the default is `["red", "orange", "blue", "green"]`, meaning that pin 0 will have a `red` semi-transparent highlight area or line, pin 1 will have `orange` ones, and so on) automove_method : str, optional When the plot is receiving data live from the measurements taken in real-time from the board (as opposed to receiving all the data to be plotted at once, say, when reading the data from saved csv files), and `plot_xmax` is smaller than the expected size of the data in the end, then at some point the data will update past the figure view. `DGILibPlot` automatically moves the figure view to the last timestamp of the latest data received, and it can do so in two ways, depending on the value of ``automove_method``. The `page` method changes the figure view in increments of ``plot_ymax``, when the data updates past the figure view, as if the plot is turning one "page" at a time. The `latest_data` method makes the figure view always have the latest data in view, meaning that it moves in small increments so that it always keeps the latest data point `0.15` seconds away from the right edge of the figure view. The `0.15` seconds value is an arbitrary hard-coded value, chosen after some experiments. (the default is 'latest_data', meaning the plot's figure view will follow the latest data in smaller increments, keeping the latest data always on the right side of the plot) verbose : int Specifies verbosity: - 0: Silent - 1: Currently prints if data is missing, either as an object or as \ values, when :func:`update_plot` is being called. (the default is `0`) Attributes ---------- axvspans : list(4 x list(matplotlib.pyplot.axvspan)) The way the semi-transparent coloured areas for the gpio pins are drawn on the plot is by using ``matplotlib.pyplot.axvspan`` objects. The `axvspan` objects are collected in a list for potential use for later. (e.g.: such as to delete them, using the :func:`clear_pins` method). (the default is 4 empty lists, meaning no highlighting of areas of interest has occured yet) annotations : list(4 x list(str)) As we have seen in figures 4, 5, 6, for the `highlight` method of drawing pins, the counting or iteration of the highlighted areas are also showed on the plot. There are 4 pins, so therefore 4 lists of the counting/iteration stored as strings are saved by the `DGILibPlot` for later use by developers (e.g.: to replace from numbers to actual descriptions and then call the redrawing of pins). (the default is 4 empty lists, meaning no annotations for the highlighted areas of interest were placed yet) preprocessed_averages_data : list(4 x list(tuple(int, \ tuple(float, float), int, float))) As the `highlight` method draws the pins on the plot with the help of the :class:`HoldTimes` class, that means the plot knows afterwards the time intervals in which the pins have values of interest. This can be valuable for a subsequent averaging function that wants to calculate faster the average current or energy consumption of the the board activity only where it was highlighted on the plot. As such, `DGILibPlot` prepares a list of 4 lists of tuples, each for every pin, the tuple containing the iteration index of the highlighted area of interest the pins kept the same value consecutively (called a `hold` time), another tuple containing the timestamps with respect to the electrical current data, which says the beginning and end times of that `hold` interval, an index in the list of the electrical current data points where the respective hold time starts and, lastly, a `None` value, which then should be replaced with a :py:class:`float` value for the the average current or charge during that hold time. (the default is 4 empty lists, meaning no gathered preprocessed averages data yet) iterations : list(4 x int) As the plot is being updated live, the `iterations` list holds the number of highlighed areas that have been drawn already for for each pin. As the whole measurement data gets plotted, the iterations list practically holds the number of iterations the of all the areas of interest for each pin (which can be, for example, the number of `for` loops that took place in the program itself running on the board). (the default are 4 ``0`` values, meaning no areas of interest on the gpio data has been identified) last_xpos : float As the plot is being updated live, the figure view moves along with the latest data to be shown on the plot. If the user desires to stop this automatic movement and focus on some specific area of the plot, while the data is still being updated in real-time, the figure needs to detect that it should stop following the data to not disturb the user. It does so by comparing the current x-axis position value of the x axis shown, with the last x-axis position value saved, before doing any automatic movement of the figure view. If they are different, no automatic movement is being done from now on. xylim_mutex : Lock As there are many ways to move, zoom and otherwise manipulate the figure view to different sections of the plot, using the `matplotlib`'s implicit movement and zoom controls, or the freshly built `DGILibPlot` sliders appearing at the bottom (See figures), or the automatic following of the latest data feature, there is a multi-threading aspect involved and therefore a mutex should be involved, in the form of a `Lock` object, to prevent anomalies. hold_times_obj: HoldTimes Only instantiated when `highlight` method is being used for drawing pins information, the :class:`HoldTimes` class holds the algorithm that works on the live data to obtain the timestamps of areas of interest of the gpio data, in order to be highlighted on the plot. As the algorithm works on live updating data, the areas of interst can be cut off between updates of data. As such, the :class:`HoldTimes` keeps information for the algorithm to work with, in order to advert this. """ def __init__(self, dgilib_extra=None, *args, **kwargs): self.dgilib_extra = dgilib_extra # Maybe the user wants to put the power figure along with # other figures he wants self.fig = kwargs.get("fig") if self.fig is None: self.fig = plt.figure(figsize=(8, 6)) # Set window title if supplied, if not set a default self.window_title = kwargs.get("window_title", "Plot of current (in amperes) and" + "gpio pins") self.fig.canvas.set_window_title(self.window_title) self.ax = kwargs.get("ax") if self.ax is None: self.ax = self.fig.add_subplot(1, 1, 1) self.ax.set_xlabel('Time [s]') self.ax.set_ylabel('Current [A]') # We need the Line2D object as well, to update it if (len(self.ax.lines) == 0): self.ln, = self.ax.plot([], [], 'r-', label="Power") else: self.ln = self.ax.lines[0] self.ln.set_xdata([]) self.ln.set_ydata([]) # Initialize xmax, ymax of plot initially self.plot_xmax = kwargs.get("plot_xmax", None) if self.plot_xmax is None: self.plot_xauto = True self.plot_xmax = 10 else: self.plot_xauto = False self.plot_ymax = kwargs.get("plot_ymax", None) if self.plot_ymax is None: self.plot_yauto = True self.plot_ymax = 0.005 else: self.plot_yauto = False self.plot_pins = kwargs.get("plot_pins", [True, True, True, True]) self.plot_pins_values = kwargs.get("plot_pins_values", [True, True, True, True]) self.plot_pins_method = kwargs.get("plot_pins_method", "highlight") # or "line" self.plot_pins_colors = kwargs.get("plot_pins_colors", ["red", "orange", "blue", "green"]) self.automove_method = kwargs.get("automove_method", "latest_data") # or "page" self.axvspans = [[], [], [], []] self.annotations = [[], [], [], []] self.preprocessed_averages_data = [[], [], [], []] #self.total_average = [0,0,0,0] self.iterations = [0,0,0,0] self.last_xpos = 0.0 self.xylim_mutex = Lock() #self.refresh_plot_pause_secs = kwargs.get("refresh_plot_pause_secs", 0.00000001) if self.plot_pins_method == "highlight": self.hold_times_obj = HoldTimes() if self.plot_pins_method == "line": self.ax_pins = self.ax.twinx() self.ax_pins.set_ylabel('Pin Value') self.ax_pins.set_ylim([-0.1, 1.1]) self.ax_pins.set_yticks([0,1]) self.ax_pins.set_yticklabels(["Low", "High"]) self.ln_pins = list(self.plot_pins) # Instantiate as copy of plot_pins for pin, plot_pin in enumerate(self.plot_pins): if plot_pin: self.ln_pins[pin] = self.ax_pins.plot([], [], label=f"gpio{pin}")[0] self.ax_pins.legend(handles=[ln_pin for ln_pin in self.ln_pins if not isinstance( ln_pin, bool)] + [self.ln]) # Should actually check if it is a lines instance # Hardwiring these values to 0 self.plot_xmin = 0 self.plot_ymin = 0 # We need these values from the user (or from the superior class), # hence no default values # TODO: Are they really needed though? self.plot_xdiv = kwargs.get("plot_xdiv", min(5, self.plot_xmax)) self.plot_xstep = kwargs.get("plot_xstep", 0.5) self.plot_xstep_default = self.plot_xstep # self.duration = kwargs.get("duration", max(self.plot_xmax, 5)) # We'll have some sliders to zoom in and out of the plot, as well as a cursor to move around when zoomed in # Leave space for sliders at the bottom plt.subplots_adjust(bottom=0.3) # Show grid self.ax.grid() # Slider color self.axcolor = 'lightgoldenrodyellow' # Title format # Should use this https://matplotlib.org/gallery/recipes/placing_text_boxes.html self.title_avg = "Total avg curr: %1" self.title_vis = "Visible avg curr: %1" self.title_pin = "Avg curr in %1: %2" # "calculate_average(data[INTERFACE_POWER])*1e3:.4} mA" self.title_time = "Time spent in %1: %2" self.ax.set_title("Waiting for data...") # Hold times for pins list, we're going to collect them self.hold_times_sum = 0.00 self.hold_times_next_index = 0 # Will be incremented to 0 later self.hold_times_already_drawn = [] self.verbose = kwargs.get("verbose", 0) self.initialize_sliders()
[docs] def initialize_sliders(self): self.axpos = plt.axes([0.25, 0.1, 0.65, 0.03], facecolor=self.axcolor) self.axwidth = plt.axes([0.25, 0.15, 0.65, 0.03], facecolor=self.axcolor) self.resetax = plt.axes([0.8, 0.025, 0.1, 0.04]) self.spos = Slider(self.axpos, 'x', 0, self.plot_xmax, valinit=0, valstep=self.plot_xstep) self.swidth = Slider(self.axwidth, 'xmax', 0, self.plot_xmax, valinit=self.plot_xdiv, valstep=self.plot_xstep) self.resetbtn = Button(self.resetax, 'Reset', color=self.axcolor, hovercolor='0.975') #TODO: Change to pixel sizes self.xleftax = plt.axes([0.4, 0.025, 0.095, 0.04]) # x_pos, y_pos, width, height self.xrightax = plt.axes([0.5, 0.025, 0.095, 0.04]) self.xmaxleftax = plt.axes([0.6, 0.025, 0.095, 0.04]) self.xmaxrightax = plt.axes([0.7, 0.025, 0.095, 0.04]) self.xstepupax = plt.axes([0.3, 0.025, 0.095, 0.04]) self.xstepdownax = plt.axes([0.2, 0.025, 0.095, 0.04]) self.xleftbtn = Button(self.xleftax, '<x', color=self.axcolor, hovercolor='0.975') self.xrightbtn = Button(self.xrightax, 'x>', color=self.axcolor, hovercolor='0.975') self.xmaxleftbtn = Button(self.xmaxleftax, 'xmax-', color=self.axcolor, hovercolor='0.975') self.xmaxrightbtn = Button(self.xmaxrightax, 'xmax+', color=self.axcolor, hovercolor='0.975') self.xstepupbtn = Button(self.xstepupax, 'xstep+', color=self.axcolor, hovercolor='0.975') self.xstepdownbtn = Button(self.xstepdownax, 'xstep-', color=self.axcolor, hovercolor='0.975') self.xstepax = plt.axes([0.1, 0.025, 0.095, 0.04]) self.xsteptb = TextBox(self.xstepax, 'xstep', initial=str(self.plot_xstep)) def xstep_submit(text): self.plot_xstep = float(text) self.xsteptb.on_submit(xstep_submit) def increase_x(event): #if ((self.spos.val + self.plot_xstep) <= (self.plot_xmax - self.swidth.val)): self.spos.set_val(self.spos.val + self.plot_xstep) update_pos(self.spos.val) def decrease_x(event): #if ((self.spos.val - self.plot_xstep) >= (self.plot_xmin)): self.spos.set_val(self.spos.val - self.plot_xstep) update_pos(self.spos.val) def increase_xmax(event): #if ((self.swidth.val + self.plot_xstep) <= self.plot_xmax): self.swidth.set_val(self.swidth.val + self.plot_xstep) update_width(self.swidth.val) def decrease_xmax(event): if ((self.swidth.val - self.plot_xstep) >= (self.plot_xmin + 0.000001)): self.swidth.set_val(self.swidth.val - self.plot_xstep) update_width(self.swidth.val) def increase_xstep(event): val = float(self.xsteptb.text) if ((val + 0.05) < (self.swidth.val - 0.000001)): self.xsteptb.set_val("{0:.2f}".format(val + 0.05)) xstep_submit(self.xsteptb.text) def decrease_xstep(event): val = float(self.xsteptb.text) if ((val - 0.05) >= (0.05)): self.xsteptb.set_val("{0:.2f}".format(val - 0.05)) xstep_submit(self.xsteptb.text) self.xleftbtn.on_clicked(decrease_x) self.xrightbtn.on_clicked(increase_x) self.xmaxleftbtn.on_clicked(decrease_xmax) self.xmaxrightbtn.on_clicked(increase_xmax) self.xstepupbtn.on_clicked(increase_xstep) self.xstepdownbtn.on_clicked(decrease_xstep) # I'm not sure how to detach these without them forgetting their parents (sliders) def update_pos(val): if self.xylim_mutex.acquire(False): pos = self.spos.val width = self.swidth.val #self.set_axis(pos, pos + width, self.plot_ymin, self.plot_ymax, "update_pos function") self.ax.axis([pos, pos + width, self.plot_ymin, self.plot_ymax]) self.xylim_mutex.release() def update_width(val): if self.xylim_mutex.acquire(False): pos = self.spos.val width = self.swidth.val self.axpos.clear() self.spos.__init__(self.axpos, 'x', 0, width, valinit=pos, valstep=self.plot_xstep) self.spos.on_changed(update_pos) self.spos.set_val(pos) #self.set_axis(pos, pos + width, self.plot_ymin, self.plot_ymax, "update_width function") self.ax.axis([pos, pos + width, self.plot_ymin, self.plot_ymax]) self.xylim_mutex.release() self.spos.on_changed(update_pos) self.swidth.on_changed(update_width) # TODO: This def see_all(event): if self.xylim_mutex.acquire(False): #self.set_axis(0, self.last_timestamp, self.plot_ymin, self.plot_ymax, "see_all function") self.ax.axis([0, self.last_timestamp, self.plot_ymin, self.plot_ymax]) self.last_xpos = -1 self.xylim_mutex.release() def reset(event): if self.xylim_mutex.acquire(False): self.swidth.set_val(self.plot_xmax) self.axpos.clear() self.spos.__init__(self.axpos, 'x', 0, self.plot_xmax, valinit=0, valstep=self.plot_xstep_default) self.spos.on_changed(update_pos) self.xsteptb.set_val(str(self.plot_xstep_default)) #self.set_axis(self.plot_xmin, self.plot_xmax, self.plot_ymin, self.plot_ymax, "reset function") self.ax.axis([self.plot_xmin, self.plot_xmax, self.plot_ymin, self.plot_ymax]) self.last_xpos = -1 self.xylim_mutex.release() self.resetbtn.on_clicked(reset) self.spos.set_val(0) self.swidth.set_val(self.plot_xmax) # Auto-change the sliders/buttons when using plot tools def on_xlims_change(axes): if self.xylim_mutex.acquire(False): # Non-blocking xlim_left = self.ax.get_xlim()[0] xlim_right = self.ax.get_xlim()[1] pos = xlim_left width = xlim_right - xlim_left self.spos.set_val(pos) self.swidth.set_val(width) self.last_xpos = -1 self.xylim_mutex.release() def on_ylims_change(axes): print(self.ax.get_ylim()) self.ax.callbacks.connect('xlim_changed', on_xlims_change)
#self.ax.callbacks.connect('ylim_changed', on_ylims_change)
[docs] def update_plot(self, data): verbose = self.verbose if data is None: data = self.dgilib_extra.data if (not data): if verbose: print("dgilib_plot.update_plot: Expected 'data' containing interfaces. Got 'data' with no interfaces. Returning from call with no work done.") return if (not data.power): if verbose: print("dgilib_plot.update_plot: Expected 'data' containing power data. Got 'data' with interfaces but no power timestamp & value pairs. Returning from call with no work done.") return if (not data.gpio): if verbose: print("dgilib_plot.update_plot: Expected 'data' containing gpio data. Got 'data' with interfaces but no gpio timestamp & value pairs.") return if not plt.fignum_exists(self.fig.number): plt.show() else: plt.draw() self.refresh_plot() #TODO: ln might have an update_callback and then it can listen to the data being updated instead of updating data here self.ln.set_xdata(data.power.timestamps) self.ln.set_ydata(data.power.values) automove = True current_xpos = self.ax.get_xlim()[0] if self.last_xpos != current_xpos: automove = False if automove and self.xylim_mutex.acquire(False): last_timestamp = data.power.timestamps[-1] pos = self.spos.val width = self.swidth.val if (last_timestamp > (pos + width)): if self.automove_method == "page": self.spos.set_val(pos + width) elif self.automove_method == "latest_data": arbitrary_amount = 0.15 if last_timestamp > width: self.spos.set_val(last_timestamp + arbitrary_amount - width) pos = self.spos.val width = self.swidth.val self.ax.axis([pos, pos + width, self.plot_ymin, self.plot_ymax]) self.last_xpos = pos self.xylim_mutex.release() self.refresh_plot() self.draw_pins(data)
[docs] def clear_pins(self): """ Clears the highlighted areas on the plot that represent the state of the gpio pins (as seen in figures 4, 5, 6). Using this method only makes sense if the `highlight` method of drawing pins was used. """ if self.axvspans is None: return for axvsp in self.axvspans: axvsp.remove()
[docs] def draw_pins(self, data): """draw_pins [summary] Raises ------ ValueError Raised when `plot_pins_method` member of class has another string value than the ones available (`highlight`, `line`) Parameters ---------- data : DGILibData :class:`DGILibData` object that contains the GPIO data to be drawn on the plot. """ # Here we set defaults (with 'or' keyword ...) ax = self.ax plot_pins = self.plot_pins plot_pins_values = self.plot_pins_values #plot_pins_method = self.plot_pins_method or "highlight" plot_pins_colors = self.plot_pins_colors # Here we do checks and stop drawing pins if something is unset if ax is None: return if plot_pins is None: return verbose=self.verbose no_of_pins = len(self.plot_pins) if self.plot_pins_method == "highlight": for pin_idx in range(no_of_pins): # For every pin number (0,1,2,3) if plot_pins[pin_idx] == True: # If we want them plotted hold_times = self.hold_times_obj.identify_hold_times(pin_idx, plot_pins_values[pin_idx], data.gpio) if hold_times is not None: for ht in hold_times: axvsp = ax.axvspan(ht[0], ht[1], color=plot_pins_colors[pin_idx], alpha=0.25) self.axvspans[pin_idx].append(axvsp) x_halfway = (ht[1] - ht[0]) / 4 + ht[0] y_halfway = (self.plot_ymax - self.plot_ymin) / 2 + self.plot_ymin annon = ax.annotate(str(self.iterations[pin_idx] + 1), xy=(x_halfway, y_halfway)) self.annotations[pin_idx].append(annon) self.iterations[pin_idx] += 1 # TODO: The start and stop indexes of the data points that are area of interest # might be more useful for an averaging function, but currently the plot uses # the coordinates of the X axis(the start/stop timestamps) in order to highlight # the areas of interest. self.preprocessed_averages_data[pin_idx].append((self.iterations[pin_idx], ht, 0, None)) # This should be in update_plot() self.ax.set_title( f"Logging. Collected {len(data.power)} power samples and {len(data.gpio)} gpio samples.") elif self.plot_pins_method == "line": extend_gpio = data.gpio.timestamps[-1] < data.power.timestamps[-1] for pin, plot_pin in enumerate(self.plot_pins): if plot_pin: self.ln_pins[pin].set_xdata( data.gpio.timestamps + extend_gpio * [data.power.timestamps[-1]]) self.ln_pins[pin].set_ydata( data.gpio.get_select_in_value(pin) + extend_gpio * [data.gpio.values[-1][pin]]) self.ax.set_title(f"Logging. Collected {len(data.power)} power samples and {len(data.gpio)} gpio samples.") self.fig.show() else: raise ValueError(f"Unrecognized plot_pins_method: {self.plot_pins_method}")
[docs] def plot_still_exists(self): """plot_still_exists Can be used in a boolean (e.g.: `if`, `while`) clause to check if the plot is still shown inside a window (e.g.: to unpause program functionality if the plot is closed). Also used in the :func:`keep_plot_alive` member function of the class. Returns ------- bool Returns `True` if the plot still exists and `False` otherwise. """ return plt.fignum_exists(self.fig.number)
[docs] def refresh_plot(self): """refresh_plot Makes a few `matplotlib` specific calls to refresh and redraw the plot (especially when new data is to be drawn). Is used in :func:`update_plot` member function of the class. """ self.ax.relim() # recompute the data limits self.ax.autoscale_view() # automatic axis scaling self.fig.canvas.flush_events()
[docs] def keep_plot_alive(self): while self.plot_still_exists(): self.refresh_plot() """keep_plot_alive Pauses program functionality until the plot is closed. Does so by refreshing the plot using :func:`refresh_plot`. """
[docs] def pause(self, time=0.000001): """pause Calls `matplotlib's` `pause` function for an amount of seconds. Parameters ---------- time : float The number of seconds the plot should have time to refresh itself and pause program functionality. """ plt.pause(time)
# Obsolete # Give it an index to continue from, so it does not go through all the data # def identify_hold_times(data, start_index, true_false, pin, correction_forward=0.00, shrink=0.00): # if len(data.gpio.timestamps) <= 1: return [] # We can't identify intervals with only one value # hold_times = [] # start = data.gpio.timestamps[0] # end = data.gpio.timestamps[0] # in_hold = true_false # not_in_hold = not true_false # search = not true_false # #interval_sizes = [] # #print("Start index: " + str(start_index)) # for i in range(start_index, len(data.gpio.timestamps)): # if search == not_in_hold: # Searching for start of hold time # if data.gpio.values[i][pin] == search: # start = data.gpio.timestamps[i] # else: # end = data.gpio.timestamps[i] # search = not search # if search == in_hold: # Searching for end of hold time # if data.gpio.values[i][pin] == search: # end = data.gpio.timestamps[i] # else: # search = not search # to_add = (start + correction_forward + shrink, end + correction_forward - shrink) # if ((to_add[0] != to_add[1]) and (to_add[0] < to_add[1])): # hold_times.append(to_add) # #interval_sizes.append(to_add[1] - to_add[0]) # start = data.gpio.timestamps[i] # # should_add_last_interval = True # # for ht in hold_times: # # if (ht[0] == start): should_add_last_interval = False # # if should_add_last_interval: # # invented_end_time = data.power.timestamps[-1] + correction_forward - shrink # # # This function ASSUMES that the intervals are about the same in size. # # # ... If the last interval that should be highlighted on the graph is # # # ... abnormally higher than the maximum of the ones that already happened # # # ... correctly then cancel highlighting with the help of 'invented_end_time' # # # ... and highlight using the minimum from the 'interval_sizes' list, to get # # # ... an average that is most likely unaffected by stuff happening at the end # # # ... of the interval, which the power interface from the board failed to # # # ... communicate to us. # # # if ((invented_end_time - start) > max(interval_sizes)): # # # invented_end_time = start + min(interval_sizes) # # to_add = (start + correction_forward + shrink, invented_end_time) # # if ((to_add[0] != to_add[1]) and (to_add[0] < to_add[1])): # # hold_times.append(to_add) # # A smart printing for debugging this function # # Either leave 'debug = False' or comment it, but don't lose it # debug = False # if debug: # ht_zip = list(zip(*hold_times)) # for (t, v) in data.gpio: # #print(str((t,v))) # if t in ht_zip[0]: # print("\t" + str(t) + "\t\t" + str(v) + "\t <-- start") # elif t in ht_zip[1]: # print("\t" + str(t) + "\t\t" + str(v) + "\t <-- stop") # else: # print("\t" + str(t) + "\t\t" + str(v)) # return hold_times # Obsolete # def shift_data(data, shift_by): # new_data = copy.deepcopy(data) # for i in range(len(data[INTERFACE_POWER][0])): # new_data[INTERFACE_POWER][0][i] = shift_by + data[INTERFACE_POWER][0][i] # for i in range(len(data[INTERFACE_GPIO][0])): # new_data[INTERFACE_GPIO][0][i] = shift_by + data[INTERFACE_GPIO][0][i] # return new_data