emayecs | 5656b2b | 2021-08-04 12:44:13 -0400 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
emayecs | 5966a53 | 2021-07-29 10:07:02 -0400 | [diff] [blame] | 2 | """ |
| 3 | cace_makeplot.py |
| 4 | Plot routines for CACE using matplotlib |
| 5 | """ |
| 6 | |
| 7 | import re |
| 8 | import os |
| 9 | import matplotlib |
| 10 | from matplotlib.figure import Figure |
| 11 | from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg |
| 12 | from matplotlib.backends.backend_agg import FigureCanvasAgg |
| 13 | |
| 14 | def twos_comp(val, bits): |
| 15 | """compute the 2's compliment of int value val""" |
| 16 | if (val & (1 << (bits - 1))) != 0: # if sign bit is set e.g., 8bit: 128-255 |
| 17 | val = val - (1 << bits) # compute negative value |
| 18 | return val # return positive value as is |
| 19 | |
| 20 | def makeplot(plotrec, results, variables, parent = None): |
| 21 | """ |
| 22 | Given a plot record from a spec sheet and a full set of results, generate |
| 23 | a plot. The name of the plot file and the vectors to plot, labels, legends, |
| 24 | and so forth are all contained in the 'plotrec' dictionary. |
| 25 | """ |
| 26 | |
| 27 | binrex = re.compile(r'([0-9]*)\'([bodh])', re.IGNORECASE) |
| 28 | # Organize data into plot lines according to formatting |
| 29 | |
| 30 | if 'type' in plotrec: |
| 31 | plottype = plotrec['type'] |
| 32 | else: |
| 33 | plottype = 'xyplot' |
| 34 | |
| 35 | # Find index of X data in results |
| 36 | if plottype == 'histogram': |
| 37 | xname = 'RESULT' |
| 38 | else: |
| 39 | xname = plotrec['xaxis'] |
| 40 | rlen = len(results[0]) |
| 41 | try: |
| 42 | xidx = next(r for r in range(rlen) if results[0][r] == xname) |
| 43 | except StopIteration: |
| 44 | return None |
| 45 | |
| 46 | # Find unique values of each variable (except results, traces, and iterations) |
| 47 | steps = [[0]] |
| 48 | traces = [0] |
| 49 | bmatch = binrex.match(results[1][0]) |
| 50 | if bmatch: |
| 51 | digits = bmatch.group(1) |
| 52 | if digits == '': |
| 53 | digits = len(results[2][0]) |
| 54 | else: |
| 55 | digits = int(digits) |
| 56 | cbase = bmatch.group(2) |
| 57 | if cbase == 'b': |
| 58 | base = 2 |
| 59 | elif cbase == 'o': |
| 60 | base = 8 |
| 61 | elif cbase == 'd': |
| 62 | base = 10 |
| 63 | else: |
| 64 | base = 16 |
| 65 | binconv = [[base, digits]] |
| 66 | else: |
| 67 | binconv = [[]] |
| 68 | |
| 69 | for i in range(1, rlen): |
| 70 | lsteps = [] |
| 71 | |
| 72 | # results labeled 'ITERATIONS', 'RESULT', 'TRACE', or 'TIME' are treated as plot vectors |
| 73 | isvector = False |
| 74 | if results[0][i] == 'ITERATIONS': |
| 75 | isvector = True |
| 76 | elif results[0][i] == 'RESULT': |
| 77 | isvector = True |
| 78 | elif results[0][i] == 'TIME': |
| 79 | isvector = True |
| 80 | elif results[0][i].split(':')[0] == 'TRACE': |
| 81 | isvector = True |
| 82 | |
| 83 | # results whose labels are in the 'variables' list are treated as plot vectors |
| 84 | if isvector == False: |
| 85 | if variables: |
| 86 | try: |
| 87 | varrec = next(item for item in variables if item['condition'] == results[0][i]) |
| 88 | except StopIteration: |
| 89 | pass |
| 90 | else: |
| 91 | isvector = True |
| 92 | |
| 93 | # those results that are not traces are stepped conditions (unless they are constant) |
| 94 | if isvector == False: |
| 95 | try: |
| 96 | for item in list(a[i] for a in results[2:]): |
| 97 | if item not in lsteps: |
| 98 | lsteps.append(item) |
| 99 | except IndexError: |
| 100 | # Diagnostic |
| 101 | print("Error: Failed to find " + str(i) + " items in result set") |
| 102 | print("Results set has " + len(results[0]) + " entries") |
| 103 | print(str(results[0])) |
| 104 | for x in range(2, len(results)): |
| 105 | if len(results[x]) <= i: |
| 106 | print("Failed at entry " + str(x)) |
| 107 | print(str(results[x])) |
| 108 | break |
| 109 | |
| 110 | # 'ITERATIONS' and 'TIME' are the x-axis variable, so don't add them to traces |
| 111 | # (but maybe just check that xaxis name is not made into a trace?) |
| 112 | elif results[0][i] != 'ITERATIONS' and results[0][i] != 'TIME': |
| 113 | traces.append(i) |
| 114 | steps.append(lsteps) |
| 115 | |
| 116 | # Mark which items need converting from digital. Format is verilog-like. Use |
| 117 | # a format width that is larger than the actual number of digits to force |
| 118 | # unsigned conversion. |
| 119 | bmatch = binrex.match(results[1][i]) |
| 120 | if bmatch: |
| 121 | digits = bmatch.group(1) |
| 122 | if digits == '': |
| 123 | digits = len(results[2][i]) |
| 124 | else: |
| 125 | digits = int(digits) |
| 126 | cbase = bmatch.group(2) |
| 127 | if cbase == 'b': |
| 128 | base = 2 |
| 129 | elif cbase == 'o': |
| 130 | base = 8 |
| 131 | elif cbase == 'd': |
| 132 | base = 10 |
| 133 | else: |
| 134 | base = 16 |
| 135 | binconv.append([base, digits]) |
| 136 | else: |
| 137 | binconv.append([]) |
| 138 | |
| 139 | # Support older method of declaring a digital vector |
| 140 | if xname.split(':')[0] == 'DIGITAL': |
| 141 | binconv[xidx] = [2, len(results[2][0])] |
| 142 | |
| 143 | # Which stepped variables (ignoring X axis variable) have more than one value? |
| 144 | watchsteps = list(i for i in range(1, rlen) if len(steps[i]) > 1 and i != xidx) |
| 145 | |
| 146 | # Diagnostic |
| 147 | # print("Stepped conditions are: ") |
| 148 | # for j in watchsteps: |
| 149 | # print(results[0][j] + ' (' + str(len(steps[j])) + ' steps)') |
| 150 | |
| 151 | # Collect results. Make a separate record for each unique set of stepped conditions |
| 152 | # encountered. Record has (X, Y) vector and a list of conditions. |
| 153 | pdata = {} |
| 154 | for item in results[2:]: |
| 155 | if xname.split(':')[0] == 'DIGITAL' or binconv[xidx] != []: |
| 156 | base = binconv[xidx][0] |
| 157 | digits = binconv[xidx][1] |
| 158 | # Recast binary strings as integers |
| 159 | # Watch for strings that have been cast to floats (need to find the source of this) |
| 160 | if '.' in item[xidx]: |
| 161 | item[xidx] = item[xidx].split('.')[0] |
| 162 | a = int(item[xidx], base) |
| 163 | b = twos_comp(a, digits) |
| 164 | xvalue = b |
| 165 | else: |
| 166 | xvalue = item[xidx] |
| 167 | |
| 168 | slist = [] |
| 169 | for j in watchsteps: |
| 170 | slist.append(item[j]) |
| 171 | istr = ','.join(slist) |
| 172 | if istr not in pdata: |
| 173 | stextlist = [] |
| 174 | for j in watchsteps: |
| 175 | if results[1][j] == '': |
| 176 | stextlist.append(results[0][j] + '=' + item[j]) |
| 177 | else: |
| 178 | stextlist.append(results[0][j] + '=' + item[j] + ' ' + results[1][j]) |
| 179 | pdict = {} |
| 180 | pdata[istr] = pdict |
| 181 | pdict['xdata'] = [] |
| 182 | if stextlist: |
| 183 | tracelegnd = False |
| 184 | else: |
| 185 | tracelegnd = True |
| 186 | |
| 187 | for i in traces: |
| 188 | aname = 'ydata' + str(i) |
| 189 | pdict[aname] = [] |
| 190 | alabel = 'ylabel' + str(i) |
| 191 | tracename = results[0][i] |
| 192 | if ':' in tracename: |
| 193 | tracename = tracename.split(':')[1] |
| 194 | |
| 195 | if results[1][i] != '' and not binrex.match(results[1][i]): |
| 196 | tracename += ' (' + results[1][i] + ')' |
| 197 | |
| 198 | pdict[alabel] = tracename |
| 199 | |
| 200 | pdict['sdata'] = ' '.join(stextlist) |
| 201 | else: |
| 202 | pdict = pdata[istr] |
| 203 | pdict['xdata'].append(xvalue) |
| 204 | |
| 205 | for i in traces: |
| 206 | # For each trace, convert the value from digital to integer if needed |
| 207 | if binconv[i] != []: |
| 208 | base = binconv[i][0] |
| 209 | digits = binconv[i][1] |
| 210 | a = int(item[i], base) |
| 211 | b = twos_comp(a, digits) |
| 212 | yvalue = b |
| 213 | else: |
| 214 | yvalue = item[i] |
| 215 | |
| 216 | aname = 'ydata' + str(i) |
| 217 | pdict[aname].append(yvalue) |
| 218 | |
| 219 | fig = Figure() |
| 220 | if parent == None: |
| 221 | canvas = FigureCanvasAgg(fig) |
| 222 | else: |
| 223 | canvas = FigureCanvasTkAgg(fig, parent) |
| 224 | |
| 225 | # With no parent, just make one plot and put the legend off to the side. The |
| 226 | # 'extra artists' capability of print_figure will take care of the bounding box. |
| 227 | # For display, prepare two subplots so that the legend takes up the space of the |
| 228 | # second one. |
| 229 | if parent == None: |
| 230 | ax = fig.add_subplot(111) |
| 231 | else: |
| 232 | ax = fig.add_subplot(121) |
| 233 | |
| 234 | fig.hold(True) |
| 235 | for record in pdata: |
| 236 | pdict = pdata[record] |
| 237 | |
| 238 | # Check if xdata is numeric |
| 239 | try: |
| 240 | test = float(pdict['xdata'][0]) |
| 241 | except ValueError: |
| 242 | numeric = False |
| 243 | xdata = [i for i in range(len(pdict['xdata']))] |
| 244 | else: |
| 245 | numeric = True |
| 246 | xdata = list(map(float,pdict['xdata'])) |
| 247 | |
| 248 | if plottype == 'histogram': |
| 249 | ax.hist(xdata, histtype='barstacked', label=pdict['sdata'], stacked=True) |
| 250 | else: |
| 251 | for i in traces: |
| 252 | aname = 'ydata' + str(i) |
| 253 | alabl = 'ylabel' + str(i) |
| 254 | ax.plot(xdata, pdict[aname], label=pdict[alabl] + ' ' + pdict['sdata']) |
| 255 | # Diagnostic |
| 256 | # print("Y values for " + aname + ": " + str(pdict[aname])) |
| 257 | |
| 258 | if not numeric: |
| 259 | ax.set_xticks(xdata) |
| 260 | ax.set_xticklabels(pdict['xdata']) |
| 261 | |
| 262 | if 'xlabel' in plotrec: |
| 263 | if results[1][xidx] == '' or binrex.match(results[1][xidx]): |
| 264 | ax.set_xlabel(plotrec['xlabel']) |
| 265 | else: |
| 266 | ax.set_xlabel(plotrec['xlabel'] + ' (' + results[1][xidx] + ')') |
| 267 | else: |
| 268 | # Automatically generate X axis label if not given alternate text |
| 269 | xtext = results[0][xidx] |
| 270 | if results[1][xidx] != '': |
| 271 | xtext += ' (' + results[1][xidx] + ')' |
| 272 | ax.set_xlabel(xtext) |
| 273 | |
| 274 | if 'ylabel' in plotrec: |
| 275 | if results[1][0] == '' or binrex.match(results[1][0]): |
| 276 | ax.set_ylabel(plotrec['ylabel']) |
| 277 | else: |
| 278 | ax.set_ylabel(plotrec['ylabel'] + ' (' + results[1][0] + ')') |
| 279 | else: |
| 280 | # Automatically generate Y axis label if not given alternate text |
| 281 | ytext = results[0][0] |
| 282 | if results[1][0] != '' or binrex.match(results[1][0]): |
| 283 | ytext += ' (' + results[1][0] + ')' |
| 284 | ax.set_ylabel(ytext) |
| 285 | |
| 286 | ax.grid(True) |
| 287 | if watchsteps or tracelegnd: |
| 288 | legnd = ax.legend(loc = 2, bbox_to_anchor = (1.05, 1), borderaxespad=0.) |
| 289 | else: |
| 290 | legnd = None |
| 291 | |
| 292 | if legnd: |
| 293 | legnd.draggable() |
| 294 | |
| 295 | if parent == None: |
| 296 | if not os.path.exists('simulation_files'): |
| 297 | os.makedirs('simulation_files') |
| 298 | |
| 299 | filename = 'simulation_files/' + plotrec['filename'] |
| 300 | # NOTE: print_figure only makes use of bbox_extra_artists if |
| 301 | # bbox_inches is set to 'tight'. This forces a two-pass method |
| 302 | # that calculates the real maximum bounds of the figure. Otherwise |
| 303 | # the legend gets clipped. |
| 304 | if legnd: |
| 305 | canvas.print_figure(filename, bbox_inches = 'tight', |
| 306 | bbox_extra_artists = [legnd]) |
| 307 | else: |
| 308 | canvas.print_figure(filename, bbox_inches = 'tight') |
| 309 | |
| 310 | return canvas |