| #!/usr/bin/env python3 |
| """ |
| cace_makeplot.py |
| Plot routines for CACE using matplotlib |
| """ |
| |
| import re |
| import os |
| import matplotlib |
| from matplotlib.figure import Figure |
| |
| # Warning: PIL Tk required, may not be in default install of python3. |
| # For Fedora, for example, need "yum install python-pillow-tk" |
| |
| from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg |
| from matplotlib.backends.backend_agg import FigureCanvasAgg |
| |
| def twos_comp(val, bits): |
| """compute the 2's compliment of int value val""" |
| if (val & (1 << (bits - 1))) != 0: # if sign bit is set e.g., 8bit: 128-255 |
| val = val - (1 << bits) # compute negative value |
| return val # return positive value as is |
| |
| def makeplot(plotrec, results, variables, parent = None): |
| """ |
| Given a plot record from a spec sheet and a full set of results, generate |
| a plot. The name of the plot file and the vectors to plot, labels, legends, |
| and so forth are all contained in the 'plotrec' dictionary. |
| """ |
| |
| binrex = re.compile(r'([0-9]*)\'([bodh])', re.IGNORECASE) |
| # Organize data into plot lines according to formatting |
| |
| if 'type' in plotrec: |
| plottype = plotrec['type'] |
| else: |
| plottype = 'xyplot' |
| |
| # Find index of X data in results |
| if plottype == 'histogram': |
| xname = 'RESULT' |
| else: |
| xname = plotrec['xaxis'] |
| rlen = len(results[0]) |
| try: |
| xidx = next(r for r in range(rlen) if results[0][r] == xname) |
| except StopIteration: |
| return None |
| |
| # Find unique values of each variable (except results, traces, and iterations) |
| steps = [[0]] |
| traces = [0] |
| bmatch = binrex.match(results[1][0]) |
| if bmatch: |
| digits = bmatch.group(1) |
| if digits == '': |
| digits = len(results[2][0]) |
| else: |
| digits = int(digits) |
| cbase = bmatch.group(2) |
| if cbase == 'b': |
| base = 2 |
| elif cbase == 'o': |
| base = 8 |
| elif cbase == 'd': |
| base = 10 |
| else: |
| base = 16 |
| binconv = [[base, digits]] |
| else: |
| binconv = [[]] |
| |
| for i in range(1, rlen): |
| lsteps = [] |
| |
| # results labeled 'ITERATIONS', 'RESULT', 'TRACE', or 'TIME' are treated as plot vectors |
| isvector = False |
| if results[0][i] == 'ITERATIONS': |
| isvector = True |
| elif results[0][i] == 'RESULT': |
| isvector = True |
| elif results[0][i] == 'TIME': |
| isvector = True |
| elif results[0][i].split(':')[0] == 'TRACE': |
| isvector = True |
| |
| # results whose labels are in the 'variables' list are treated as plot vectors |
| if isvector == False: |
| if variables: |
| try: |
| varrec = next(item for item in variables if item['condition'] == results[0][i]) |
| except StopIteration: |
| pass |
| else: |
| isvector = True |
| |
| # those results that are not traces are stepped conditions (unless they are constant) |
| if isvector == False: |
| try: |
| for item in list(a[i] for a in results[2:]): |
| if item not in lsteps: |
| lsteps.append(item) |
| except IndexError: |
| # Diagnostic |
| print("Error: Failed to find " + str(i) + " items in result set") |
| print("Results set has " + len(results[0]) + " entries") |
| print(str(results[0])) |
| for x in range(2, len(results)): |
| if len(results[x]) <= i: |
| print("Failed at entry " + str(x)) |
| print(str(results[x])) |
| break |
| |
| # 'ITERATIONS' and 'TIME' are the x-axis variable, so don't add them to traces |
| # (but maybe just check that xaxis name is not made into a trace?) |
| elif results[0][i] != 'ITERATIONS' and results[0][i] != 'TIME': |
| traces.append(i) |
| steps.append(lsteps) |
| |
| # Mark which items need converting from digital. Format is verilog-like. Use |
| # a format width that is larger than the actual number of digits to force |
| # unsigned conversion. |
| bmatch = binrex.match(results[1][i]) |
| if bmatch: |
| digits = bmatch.group(1) |
| if digits == '': |
| digits = len(results[2][i]) |
| else: |
| digits = int(digits) |
| cbase = bmatch.group(2) |
| if cbase == 'b': |
| base = 2 |
| elif cbase == 'o': |
| base = 8 |
| elif cbase == 'd': |
| base = 10 |
| else: |
| base = 16 |
| binconv.append([base, digits]) |
| else: |
| binconv.append([]) |
| |
| # Support older method of declaring a digital vector |
| if xname.split(':')[0] == 'DIGITAL': |
| binconv[xidx] = [2, len(results[2][0])] |
| |
| # Which stepped variables (ignoring X axis variable) have more than one value? |
| watchsteps = list(i for i in range(1, rlen) if len(steps[i]) > 1 and i != xidx) |
| |
| # Diagnostic |
| # print("Stepped conditions are: ") |
| # for j in watchsteps: |
| # print(results[0][j] + ' (' + str(len(steps[j])) + ' steps)') |
| |
| # Collect results. Make a separate record for each unique set of stepped conditions |
| # encountered. Record has (X, Y) vector and a list of conditions. |
| pdata = {} |
| for item in results[2:]: |
| if xname.split(':')[0] == 'DIGITAL' or binconv[xidx] != []: |
| base = binconv[xidx][0] |
| digits = binconv[xidx][1] |
| # Recast binary strings as integers |
| # Watch for strings that have been cast to floats (need to find the source of this) |
| if '.' in item[xidx]: |
| item[xidx] = item[xidx].split('.')[0] |
| a = int(item[xidx], base) |
| b = twos_comp(a, digits) |
| xvalue = b |
| else: |
| xvalue = item[xidx] |
| |
| slist = [] |
| for j in watchsteps: |
| slist.append(item[j]) |
| istr = ','.join(slist) |
| if istr not in pdata: |
| stextlist = [] |
| for j in watchsteps: |
| if results[1][j] == '': |
| stextlist.append(results[0][j] + '=' + item[j]) |
| else: |
| stextlist.append(results[0][j] + '=' + item[j] + ' ' + results[1][j]) |
| pdict = {} |
| pdata[istr] = pdict |
| pdict['xdata'] = [] |
| if stextlist: |
| tracelegnd = False |
| else: |
| tracelegnd = True |
| |
| for i in traces: |
| aname = 'ydata' + str(i) |
| pdict[aname] = [] |
| alabel = 'ylabel' + str(i) |
| tracename = results[0][i] |
| if ':' in tracename: |
| tracename = tracename.split(':')[1] |
| |
| if results[1][i] != '' and not binrex.match(results[1][i]): |
| tracename += ' (' + results[1][i] + ')' |
| |
| pdict[alabel] = tracename |
| |
| pdict['sdata'] = ' '.join(stextlist) |
| else: |
| pdict = pdata[istr] |
| pdict['xdata'].append(xvalue) |
| |
| for i in traces: |
| # For each trace, convert the value from digital to integer if needed |
| if binconv[i] != []: |
| base = binconv[i][0] |
| digits = binconv[i][1] |
| a = int(item[i], base) |
| b = twos_comp(a, digits) |
| yvalue = b |
| else: |
| yvalue = item[i] |
| |
| aname = 'ydata' + str(i) |
| pdict[aname].append(yvalue) |
| |
| fig = Figure() |
| if parent == None: |
| canvas = FigureCanvasAgg(fig) |
| else: |
| canvas = FigureCanvasTkAgg(fig, parent) |
| |
| # With no parent, just make one plot and put the legend off to the side. The |
| # 'extra artists' capability of print_figure will take care of the bounding box. |
| # For display, prepare two subplots so that the legend takes up the space of the |
| # second one. |
| if parent == None: |
| ax = fig.add_subplot(111) |
| else: |
| ax = fig.add_subplot(121) |
| |
| # fig.hold(True) |
| for record in pdata: |
| pdict = pdata[record] |
| |
| # Check if xdata is numeric |
| try: |
| test = float(pdict['xdata'][0]) |
| except ValueError: |
| numeric = False |
| xdata = [i for i in range(len(pdict['xdata']))] |
| else: |
| numeric = True |
| xdata = list(map(float,pdict['xdata'])) |
| |
| if plottype == 'histogram': |
| ax.hist(xdata, histtype='barstacked', label=pdict['sdata'], stacked=True) |
| else: |
| for i in traces: |
| aname = 'ydata' + str(i) |
| alabl = 'ylabel' + str(i) |
| ax.plot(xdata, pdict[aname], label=pdict[alabl] + ' ' + pdict['sdata']) |
| # Diagnostic |
| # print("Y values for " + aname + ": " + str(pdict[aname])) |
| |
| if not numeric: |
| ax.set_xticks(xdata) |
| ax.set_xticklabels(pdict['xdata']) |
| |
| if 'xlabel' in plotrec: |
| if results[1][xidx] == '' or binrex.match(results[1][xidx]): |
| ax.set_xlabel(plotrec['xlabel']) |
| else: |
| ax.set_xlabel(plotrec['xlabel'] + ' (' + results[1][xidx] + ')') |
| else: |
| # Automatically generate X axis label if not given alternate text |
| xtext = results[0][xidx] |
| if results[1][xidx] != '': |
| xtext += ' (' + results[1][xidx] + ')' |
| ax.set_xlabel(xtext) |
| |
| if 'ylabel' in plotrec: |
| if results[1][0] == '' or binrex.match(results[1][0]): |
| ax.set_ylabel(plotrec['ylabel']) |
| else: |
| ax.set_ylabel(plotrec['ylabel'] + ' (' + results[1][0] + ')') |
| else: |
| # Automatically generate Y axis label if not given alternate text |
| ytext = results[0][0] |
| if results[1][0] != '' or binrex.match(results[1][0]): |
| ytext += ' (' + results[1][0] + ')' |
| ax.set_ylabel(ytext) |
| |
| ax.grid(True) |
| if watchsteps or tracelegnd: |
| legnd = ax.legend(loc = 2, bbox_to_anchor = (1.05, 1), borderaxespad=0.) |
| else: |
| legnd = None |
| |
| if legnd: |
| legnd.set_draggable(True) |
| |
| if parent == None: |
| if not os.path.exists('ngspice/simulation_files'): |
| os.makedirs('ngspice/simulation_files') |
| |
| filename = 'ngspice/simulation_files/' + plotrec['filename'] |
| # NOTE: print_figure only makes use of bbox_extra_artists if |
| # bbox_inches is set to 'tight'. This forces a two-pass method |
| # that calculates the real maximum bounds of the figure. Otherwise |
| # the legend gets clipped. |
| if legnd: |
| canvas.print_figure(filename, bbox_inches = 'tight', |
| bbox_extra_artists = [legnd]) |
| else: |
| canvas.print_figure(filename, bbox_inches = 'tight') |
| |
| return canvas |