blob: d4fbad924f091aed6e2f0ac1ca2318781cc1a5cd [file] [log] [blame]
#!/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