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