1#!/usr/bin/env python3 2# 3# Copyright 2020 Google, Inc. 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17import datetime 18import logging 19import math 20import numpy 21import os 22 23from bokeh.models import tools as bokeh_tools 24from bokeh.models import CustomJS, ColumnDataSource 25from bokeh.models.widgets import DataTable, TableColumn 26from bokeh.models.formatters import DatetimeTickFormatter 27from bokeh.plotting import figure, output_file, save 28from bokeh.layouts import layout 29 30 31def current_waveform_plot(samples, voltage, dest_path, plot_title): 32 """Plot the current data using bokeh interactive plotting tool. 33 34 Plotting power measurement data with bokeh to generate interactive plots. 35 You can do interactive data analysis on the plot after generating with the 36 provided widgets, which make the debugging much easier. To realize that, 37 bokeh callback java scripting is used. 38 39 Args: 40 samples: a list of tuples in which the first element is a timestamp and 41 the second element is the sampled current in milli amps at that time. 42 voltage: the voltage that was used during the measurement. 43 dest_path: destination path. 44 plot_title: a filename and title for the plot. 45 Returns: 46 plot: the plotting object of bokeh, optional, will be needed if multiple 47 plots will be combined to one html file. 48 dt: the datatable object of bokeh, optional, will be needed if multiple 49 datatables will be combined to one html file. 50 """ 51 logging.info('Plotting the power measurement data.') 52 53 duration = samples[-1][0] - samples[0][0] 54 current_data = [sample[1] * 1000 for sample in samples] 55 avg_current = sum(current_data) / len(current_data) 56 color = ['navy'] * len(samples) 57 time_realtime = [ 58 datetime.datetime.fromtimestamp(sample[0]) for sample in samples 59 ] 60 61 # Preparing the data and source link for bokehn java callback 62 source = ColumnDataSource( 63 data=dict(x=time_realtime, y=current_data, color=color)) 64 s2 = ColumnDataSource( 65 data=dict(a=[duration], 66 b=[round(avg_current, 2)], 67 c=[round(avg_current * voltage, 2)], 68 d=[round(avg_current * voltage * duration, 2)], 69 e=[round(avg_current * duration, 2)])) 70 # Setting up data table for the output 71 columns = [ 72 TableColumn(field='a', title='Total Duration (s)'), 73 TableColumn(field='b', title='Average Current (mA)'), 74 TableColumn(field='c', title='Average Power (4.2v) (mW)'), 75 TableColumn(field='d', title='Average Energy (mW*s)'), 76 TableColumn(field='e', title='Normalized Average Energy (mA*s)') 77 ] 78 dt = DataTable(source=s2, 79 columns=columns, 80 width=1300, 81 height=60, 82 editable=True) 83 84 output_file(os.path.join(dest_path, plot_title + '.html')) 85 tools = 'box_zoom,box_select,pan,crosshair,redo,undo,reset,hover,save' 86 # Create a new plot with the datatable above 87 plot = figure(x_axis_type='datetime', 88 plot_width=1300, 89 plot_height=700, 90 title=plot_title, 91 tools=tools) 92 plot.add_tools(bokeh_tools.WheelZoomTool(dimensions='width')) 93 plot.add_tools(bokeh_tools.WheelZoomTool(dimensions='height')) 94 plot.line('x', 'y', source=source, line_width=2) 95 plot.circle('x', 'y', source=source, size=0.5, fill_color='color') 96 plot.xaxis.axis_label = 'Time (s)' 97 plot.yaxis.axis_label = 'Current (mA)' 98 plot.xaxis.formatter = DatetimeTickFormatter( 99 seconds=["%H:%M:%S"], 100 milliseconds=["%H:%M:%S:%3Ns"], 101 microseconds=["%H:%M:%S:%fus"], 102 minutes=["%H:%M:%S"], 103 minsec=["%H:%M:%S"], 104 hours=["%H:%M:%S"]) 105 106 # Callback JavaScript 107 source.selected.js_on_change( 108 "indices", 109 CustomJS(args=dict(source=source, mytable=dt), 110 code=""" 111 const inds = source.selected.indices; 112 const d1 = source.data; 113 const d2 = mytable.source.data; 114 var ym = 0 115 var ts = 0 116 var min=d1['x'][inds[0]] 117 var max=d1['x'][inds[0]] 118 d2['a'] = [] 119 d2['b'] = [] 120 d2['c'] = [] 121 d2['d'] = [] 122 d2['e'] = [] 123 if (inds.length==0) {return;} 124 for (var i = 0; i < inds.length; i++) { 125 ym += d1['y'][inds[i]] 126 d1['color'][inds[i]] = "red" 127 if (d1['x'][inds[i]] < min) { 128 min = d1['x'][inds[i]]} 129 if (d1['x'][inds[i]] > max) { 130 max = d1['x'][inds[i]]} 131 } 132 ym /= inds.length 133 ts = max - min 134 d2['a'].push(Math.round(ts*1000.0)/1000000.0) 135 d2['b'].push(Math.round(ym*100.0)/100.0) 136 d2['c'].push(Math.round(ym*4.2*100.0)/100.0) 137 d2['d'].push(Math.round(ym*4.2*ts*100.0)/100.0) 138 d2['e'].push(Math.round(ym*ts*100.0)/100.0) 139 source.change.emit(); 140 mytable.change.emit(); 141 """)) 142 143 # Layout the plot and the datatable bar 144 save(layout([[dt], [plot]])) 145 return plot, dt 146 147 148def monsoon_histogram_plot(samples, dest_path, plot_title): 149 """ Creates a histogram from a monsoon result object. 150 151 Args: 152 samples: a list of tuples in which the first element is a timestamp and 153 the second element is the sampled current in milli amps at that time. 154 dest_path: destination path 155 plot_title: a filename and title for the plot. 156 Returns: 157 a tuple of arrays containing the values of the histogram and the 158 bin edges. 159 """ 160 milli_amps = [sample[1] * 1000 for sample in samples] 161 hist, edges = numpy.histogram(milli_amps, 162 bins=math.ceil(max(milli_amps)), 163 range=(0, max(milli_amps))) 164 165 output_file(os.path.join(dest_path, plot_title + '.html')) 166 167 plot = figure(title=plot_title, 168 y_axis_type='log', 169 background_fill_color='#fafafa') 170 171 plot.quad(top=hist, 172 bottom=0, 173 left=edges[:-1], 174 right=edges[1:], 175 fill_color='navy') 176 177 plot.y_range.start = 0 178 plot.xaxis.axis_label = 'Instantaneous current [mA]' 179 plot.yaxis.axis_label = 'Count' 180 plot.grid.grid_line_color = 'white' 181 182 save(plot) 183 184 return hist, edges 185 186 187def monsoon_tx_power_sweep_plot(dest_path, plot_title, currents, txs): 188 """ Creates average current vs tx power plot 189 190 Args: 191 dest_path: destination path 192 plot_title: a filename and title for the plot. 193 currents: List of average currents measured during power sweep 194 txs: List of uplink input power levels specified for each measurement 195 """ 196 197 output_file(os.path.join(dest_path, plot_title + '.html')) 198 199 plot = figure(title=plot_title, 200 y_axis_label='Average Current [mA]', 201 x_axis_label='Tx Power [dBm]', 202 background_fill_color='#fafafa') 203 204 plot.line(txs, currents) 205 plot.circle(txs, currents, fill_color='white', size=8) 206 plot.y_range.start = 0 207 208 save(plot) 209