blob: df8127c1803c9acac584aca8c5003ea3c3fb1915 [file] [log] [blame]
emayecs5656b2b2021-08-04 12:44:13 -04001#!/usr/bin/env python3
emayecs5966a532021-07-29 10:07:02 -04002#
3#--------------------------------------------------------------------
emayecs14748312021-08-05 14:21:26 -04004# Characterization Report Window for the project manager
emayecs5966a532021-07-29 10:07:02 -04005#
6#--------------------------------------------------------------------
7# Written by Tim Edwards
8# efabless, inc.
9# September 12, 2016
10# Version 0.1
11#----------------------------------------------------------
12
13import os
14import base64
15import subprocess
16
17import tkinter
18from tkinter import ttk
19
20import tooltip
21import cace_makeplot
22
23class FailReport(tkinter.Toplevel):
emayecs14748312021-08-05 14:21:26 -040024 """failure report window."""
emayecs5966a532021-07-29 10:07:02 -040025
26 def __init__(self, parent=None, fontsize=11, *args, **kwargs):
27 '''See the __init__ for Tkinter.Toplevel.'''
28 tkinter.Toplevel.__init__(self, parent, *args, **kwargs)
29
30 s = ttk.Style()
31 s.configure('bg.TFrame', background='gray40')
32 s.configure('italic.TLabel', font=('Helvetica', fontsize, 'italic'), anchor = 'west')
33 s.configure('title.TLabel', font=('Helvetica', fontsize, 'bold italic'),
34 foreground = 'brown', anchor = 'center')
35 s.configure('normal.TLabel', font=('Helvetica', fontsize))
36 s.configure('red.TLabel', font=('Helvetica', fontsize), foreground = 'red')
37 s.configure('green.TLabel', font=('Helvetica', fontsize), foreground = 'green4')
38 s.configure('blue.TLabel', font=('Helvetica', fontsize), foreground = 'blue')
39 s.configure('brown.TLabel', font=('Helvetica', fontsize, 'italic'),
40 foreground = 'brown', anchor = 'center')
41 s.configure('normal.TButton', font=('Helvetica', fontsize), border = 3,
42 relief = 'raised')
43 s.configure('red.TButton', font=('Helvetica', fontsize), foreground = 'red',
44 border = 3, relief = 'raised')
45 s.configure('green.TButton', font=('Helvetica', fontsize), foreground = 'green4',
46 border = 3, relief = 'raised')
47 s.configure('title.TButton', font=('Helvetica', fontsize, 'bold italic'),
48 foreground = 'brown', border = 0, relief = 'groove')
49
50 self.withdraw()
emayecs14748312021-08-05 14:21:26 -040051 self.title('Local Characterization Report')
emayecs5966a532021-07-29 10:07:02 -040052 self.root = parent.root
53 self.rowconfigure(0, weight = 1)
54 self.columnconfigure(0, weight = 1)
55
56 # Scrolled frame: Need frame, then canvas and scrollbars; finally, the
57 # actual grid of results gets placed in the canvas.
58 self.failframe = ttk.Frame(self)
59 self.failframe.grid(column = 0, row = 0, sticky = 'nsew')
60 self.mainarea = tkinter.Canvas(self.failframe)
61 self.mainarea.grid(row = 0, column = 0, sticky = 'nsew')
62
63 self.mainarea.faildisplay = ttk.Frame(self.mainarea)
64 self.mainarea.create_window((0,0), window=self.mainarea.faildisplay,
65 anchor="nw", tags="self.frame")
66
67 # Create a frame for displaying plots, but don't put it in the grid.
68 # Make it resizeable.
69 self.plotframe = ttk.Frame(self)
70 self.plotframe.rowconfigure(0, weight = 1)
71 self.plotframe.columnconfigure(0, weight = 1)
72
73 # Main window resizes, not the scrollbars
74 self.failframe.rowconfigure(0, weight = 1)
75 self.failframe.columnconfigure(0, weight = 1)
76 # Add scrollbars
77 xscrollbar = ttk.Scrollbar(self.failframe, orient = 'horizontal')
78 xscrollbar.grid(row = 1, column = 0, sticky = 'nsew')
79 yscrollbar = ttk.Scrollbar(self.failframe, orient = 'vertical')
80 yscrollbar.grid(row = 0, column = 1, sticky = 'nsew')
81 # Attach viewing area to scrollbars
82 self.mainarea.config(xscrollcommand = xscrollbar.set)
83 xscrollbar.config(command = self.mainarea.xview)
84 self.mainarea.config(yscrollcommand = yscrollbar.set)
85 yscrollbar.config(command = self.mainarea.yview)
86 # Set up configure callback
87 self.mainarea.faildisplay.bind("<Configure>", self.frame_configure)
88
89 self.bbar = ttk.Frame(self)
90 self.bbar.grid(column = 0, row = 1, sticky = "news")
91 self.bbar.close_button = ttk.Button(self.bbar, text='Close',
92 command=self.close, style = 'normal.TButton')
93 self.bbar.close_button.grid(column=0, row=0, padx = 5)
94 # Table button returns to table view but is only displayed for plots.
95 self.bbar.table_button = ttk.Button(self.bbar, text='Table', style = 'normal.TButton')
96
97 self.protocol("WM_DELETE_WINDOW", self.close)
98 tooltip.ToolTip(self.bbar.close_button,
99 text='Close detail view of conditions and results')
100
101 self.sortdir = False
102 self.data = []
103
104 def grid_configure(self, padx, pady):
105 pass
106
107 def frame_configure(self, event):
108 self.update_idletasks()
109 self.mainarea.configure(scrollregion=self.mainarea.bbox("all"))
110
111 def check_failure(self, record, calc, value):
112 if not 'target' in record:
113 return None
114 else:
115 target = record['target']
116
117 if calc == 'min':
118 targval = float(target)
119 if value < targval:
120 return True
121 elif calc == 'max':
122 targval = float(target)
123 if value > targval:
124 return True
125 else:
126 return None
127
128 # Given an electrical parameter 'param' and a condition name 'condname', find
129 # the units of that condition. If the condition isn't found in the local
130 # parameters, then it is searched for in 'globcond'.
131
132 def findunit(self, condname, param, globcond):
133 unit = ''
134 try:
135 loccond = next(item for item in param['conditions'] if item['condition'] == condname)
136 except StopIteration:
137 try:
138 globitem = next(item for item in globcond if item['condition'] == condname)
139 except (TypeError, StopIteration):
140 unit = '' # No units
141 else:
142 if 'unit' in globitem:
143 unit = globitem['unit']
144 else:
145 unit = '' # No units
146 else:
147 if 'unit' in loccond:
148 unit = loccond['unit']
149 else:
150 unit = '' # No units
151 return unit
152
153 def size_plotreport(self):
154 self.update_idletasks()
155 width = self.plotframe.winfo_width()
156 height = self.plotframe.winfo_height()
157 if width < 3 * height:
158 self.plotframe.configure(width=height * 3)
159
160 def size_failreport(self):
161 # Attempt to set the datasheet viewer width to the interior width
162 # but do not set it larger than the available desktop.
163
164 self.update_idletasks()
165 width = self.mainarea.faildisplay.winfo_width()
166 screen_width = self.root.winfo_screenwidth()
167 if width > screen_width - 20:
168 self.mainarea.configure(width=screen_width - 20)
169 else:
170 self.mainarea.configure(width=width)
171
172 # Likewise for the height, up to the desktop height. Note that this
173 # needs to account for both the button bar at the bottom of the GUI
174 # window plus the bar at the bottom of the desktop.
175 height = self.mainarea.faildisplay.winfo_height()
176 screen_height = self.root.winfo_screenheight()
177 if height > screen_height - 120:
178 self.mainarea.configure(height=screen_height - 120)
179 else:
180 self.mainarea.configure(height=height)
181
182 def table_to_histogram(self, globcond, filename):
183 # Switch from a table view to a histogram plot view, using the
184 # result as the X axis variable and count for the Y axis.
185
186 # Destroy existing contents.
187 for widget in self.plotframe.winfo_children():
188 widget.destroy()
189
190 param = self.data
191 plotrec = {}
192 plotrec['xaxis'] = param['method']
193 plotrec['xlabel'] = param['method']
194 plotrec['ylabel'] = 'COUNT'
195 plotrec['type'] = 'histogram'
196 if 'unit' in param:
197 plotrec['xlabel'] += ' (' + param['unit'] + ')'
198
199 results = param['results']
200
201 if 'variables' in param:
202 variables = param['variables']
203 else:
204 variables = []
205 # faild = self.mainarea.faildisplay # definition for convenience
206 self.failframe.grid_forget()
207 self.plotframe.grid(row = 0, column = 0, sticky = 'nsew')
208 canvas = cace_makeplot.makeplot(plotrec, results, variables, parent = self.plotframe)
209 if 'display' in param:
210 ttk.Label(self.plotframe, text=param['display'], style='title.TLabel').grid(row=1, column=0)
211 canvas.show()
212 canvas.get_tk_widget().grid(row=0, column=0, sticky = 'nsew')
213 # Finally, open the window if it was not already open.
214 self.open()
215
216 def table_to_plot(self, condition, globcond, filename):
217 # Switch from a table view to a plot view, using the condname as
218 # the X axis variable.
219
220 # Destroy existing contents.
221 for widget in self.plotframe.winfo_children():
222 widget.destroy()
223
224 param = self.data
225 plotrec = {}
226 plotrec['xaxis'] = condition
227 plotrec['xlabel'] = condition
228 # Note: cace_makeplot adds text for units, if available
229 plotrec['ylabel'] = param['method']
230 plotrec['type'] = 'xyplot'
231
232 results = param['results']
233
234 if 'variables' in param:
235 variables = param['variables']
236 else:
237 variables = []
238
239 # faild = self.mainarea.faildisplay # definition for convenience
240 self.failframe.grid_forget()
241 self.plotframe.grid(row = 0, column = 0, sticky = 'nsew')
242 canvas = cace_makeplot.makeplot(plotrec, results, variables, parent = self.plotframe)
243 if 'display' in param:
244 ttk.Label(self.plotframe, text=param['display'], style='title.TLabel').grid(row=1, column=0)
245 canvas.show()
246 canvas.get_tk_widget().grid(row=0, column=0, sticky = 'nsew')
247 # Display the button to return to the table view
248 # except for transient and Monte Carlo simulations which are too large to tabulate.
249 if not condition == 'TIME':
250 self.bbar.table_button.grid(column=1, row=0, padx = 5)
251 self.bbar.table_button.configure(command=lambda param=param, globcond=globcond,
252 filename=filename: self.display(param, globcond, filename))
253
254 # Finally, open the window if it was not already open.
255 self.open()
256
257 def display(self, param=None, globcond=None, filename=None):
258 # (Diagnostic)
259 # print('failure report: passed parameter ' + str(param))
260
261 # Destroy existing contents.
262 for widget in self.mainarea.faildisplay.winfo_children():
263 widget.destroy()
264
265 if not param:
266 param = self.data
267
268 # 'param' is a dictionary pulled in from the annotate datasheet.
269 # If the failure display was called, then 'param' should contain
270 # record called 'results'. If the parameter has no results, then
271 # there is nothing to do.
272
273 if filename and 'plot' in param:
274 simfiles = os.path.split(filename)[0] + '/ngspice/char/simulation_files/'
275 self.failframe.grid_forget()
276 self.plotframe.grid(row = 0, column = 0, sticky = 'nsew')
277
278 # Clear the plotframe and remake
279 for widget in self.plotframe.winfo_children():
280 widget.destroy()
281
282 plotrec = param['plot']
283 results = param['results']
284 if 'variables' in param:
285 variables = param['variables']
286 else:
287 variables = []
288 canvas = cace_makeplot.makeplot(plotrec, results, variables, parent = self.plotframe)
289 if 'display' in param:
290 ttk.Label(self.plotframe, text=param['display'],
291 style='title.TLabel').grid(row=1, column=0)
292 canvas.show()
293 canvas.get_tk_widget().grid(row=0, column=0, sticky = 'nsew')
294 self.data = param
295 # Display the button to return to the table view
296 self.bbar.table_button.grid(column=1, row=0, padx = 5)
297 self.bbar.table_button.configure(command=lambda param=param, globcond=globcond,
298 filename=filename: self.display(param, globcond, filename))
299
300 elif not 'results' in param:
301 print("No results to build a report with.")
302 return
303
304 else:
305 self.data = param
306 self.plotframe.grid_forget()
307 self.failframe.grid(column = 0, row = 0, sticky = 'nsew')
308 faild = self.mainarea.faildisplay # definition for convenience
309 results = param['results']
310 names = results[0]
311 units = results[1]
312 results = results[2:]
313
314 # Check for transient simulation
315 if 'TIME' in names:
316 # Transient data are (usually) too numerous to tabulate, so go straight to plot
317 self.table_to_plot('TIME', globcond, filename)
318 return
319
320 # Check for Monte Carlo simulation
321 if 'ITERATIONS' in names:
322 # Monte Carlo data are too numerous to tabulate, so go straight to plot
323 self.table_to_histogram(globcond, filename)
324 return
325
326 # Numerically sort by result (to be done: sort according to up/down
327 # criteria, which will be retained per header entry)
328 results.sort(key = lambda row: float(row[0]), reverse = self.sortdir)
329
330 # To get ranges, transpose the results matrix, then make unique
331 ranges = list(map(list, zip(*results)))
332 for r, vrange in enumerate(ranges):
333 try:
334 vmin = min(float(v) for v in vrange)
335 vmax = max(float(v) for v in vrange)
336 if vmin == vmax:
337 ranges[r] = [str(vmin)]
338 else:
339 ranges[r] = [str(vmin), str(vmax)]
340 except ValueError:
341 ranges[r] = list(set(vrange))
342 pass
343
344 faild.titlebar = ttk.Frame(faild)
345 faild.titlebar.grid(row = 0, column = 0, sticky = 'ewns')
346
347 faild.titlebar.label1 = ttk.Label(faild.titlebar, text = 'Electrical Parameter: ',
348 style = 'italic.TLabel')
349 faild.titlebar.label1.pack(side = 'left', padx = 6, ipadx = 3)
350 if 'display' in param:
351 faild.titlebar.label2 = ttk.Label(faild.titlebar, text = param['display'],
352 style = 'normal.TLabel')
353 faild.titlebar.label2.pack(side = 'left', padx = 6, ipadx = 3)
354 faild.titlebar.label3 = ttk.Label(faild.titlebar, text = ' Method: ',
355 style = 'italic.TLabel')
356 faild.titlebar.label3.pack(side = 'left', padx = 6, ipadx = 3)
357 faild.titlebar.label4 = ttk.Label(faild.titlebar, text = param['method'],
358 style = 'normal.TLabel')
359 faild.titlebar.label4.pack(side = 'left', padx = 6, ipadx = 3)
360
361 if 'min' in param:
362 if 'target' in param['min']:
363 faild.titlebar.label7 = ttk.Label(faild.titlebar, text = ' Min Limit: ',
364 style = 'italic.TLabel')
365 faild.titlebar.label7.pack(side = 'left', padx = 3, ipadx = 3)
366 faild.titlebar.label8 = ttk.Label(faild.titlebar, text = param['min']['target'],
367 style = 'normal.TLabel')
368 faild.titlebar.label8.pack(side = 'left', padx = 6, ipadx = 3)
369 if 'unit' in param:
370 faild.titlebar.label9 = ttk.Label(faild.titlebar, text = param['unit'],
371 style = 'italic.TLabel')
372 faild.titlebar.label9.pack(side = 'left', padx = 3, ipadx = 3)
373 if 'max' in param:
374 if 'target' in param['max']:
375 faild.titlebar.label10 = ttk.Label(faild.titlebar, text = ' Max Limit: ',
376 style = 'italic.TLabel')
377 faild.titlebar.label10.pack(side = 'left', padx = 6, ipadx = 3)
378 faild.titlebar.label11 = ttk.Label(faild.titlebar, text = param['max']['target'],
379 style = 'normal.TLabel')
380 faild.titlebar.label11.pack(side = 'left', padx = 6, ipadx = 3)
381 if 'unit' in param:
382 faild.titlebar.label12 = ttk.Label(faild.titlebar, text = param['unit'],
383 style = 'italic.TLabel')
384 faild.titlebar.label12.pack(side = 'left', padx = 3, ipadx = 3)
385
386 # Simplify view by removing constant values from the table and just listing them
387 # on the second line.
388
389 faild.constants = ttk.Frame(faild)
390 faild.constants.grid(row = 1, column = 0, sticky = 'ewns')
391 faild.constants.title = ttk.Label(faild.constants, text = 'Constant Conditions: ',
392 style = 'italic.TLabel')
393 faild.constants.title.grid(row = 0, column = 0, padx = 6, ipadx = 3)
394 j = 0
395 for condname, unit, range in zip(names, units, ranges):
396 if len(range) == 1:
397 labtext = condname
398 # unit = self.findunit(condname, param, globcond)
399 labtext += ' = ' + range[0] + ' ' + unit + ' '
400 row = int(j / 3)
401 col = 1 + (j % 3)
402 ttk.Label(faild.constants, text = labtext,
403 style = 'blue.TLabel').grid(row = row,
404 column = col, padx = 6, sticky = 'nsew')
405 j += 1
406
407 body = ttk.Frame(faild, style = 'bg.TFrame')
408 body.grid(row = 2, column = 0, sticky = 'ewns')
409
410 # Print out names
411 j = 0
412 for condname, unit, range in zip(names, units, ranges):
413 # Now find the range for each entry from the global and local conditions.
414 # Use local conditions if specified, otherwise default to global condition.
415 # Each result is a list of three numbers for min, typ, and max. List
416 # entries may be left unfilled.
417
418 if len(range) == 1:
419 continue
420
421 labtext = condname
422 plottext = condname
423 if j == 0:
424 # Add unicode arrow up/down depending on sort direction
425 labtext += ' \u21e9' if self.sortdir else ' \u21e7'
426 header = ttk.Button(body, text=labtext, style = 'title.TButton',
427 command = self.changesort)
428 tooltip.ToolTip(header, text='Reverse order of results')
429 else:
430 header = ttk.Button(body, text=labtext, style = 'title.TLabel',
431 command = lambda plottext=plottext, globcond=globcond,
432 filename=filename: self.table_to_plot(plottext, globcond, filename))
433 tooltip.ToolTip(header, text='Plot results with this condition on the X axis')
434 header.grid(row = 0, column = j, sticky = 'ewns')
435
436 # Second row is the measurement unit
437 # if j == 0:
438 # # Measurement unit of result in first column
439 # if 'unit' in param:
440 # unit = param['unit']
441 # else:
442 # unit = '' # No units
443 # else:
444 # # Measurement unit of condition in other columns
445 # # Find condition in local conditions else global conditions
446 # unit = self.findunit(condname, param, globcond)
447
448 unitlabel = ttk.Label(body, text=unit, style = 'brown.TLabel')
449 unitlabel.grid(row = 1, column = j, sticky = 'ewns')
450
451 # (Pick up limits when all entries have been processed---see below)
452 j += 1
453
454 # Now list entries for each failure record. These should all be in the
455 # same order.
456 m = 2
457 for result in results:
458 m += 1
459 j = 0
460 condition = result[0]
461 lstyle = 'normal.TLabel'
462 value = float(condition)
463 if 'min' in param:
464 minrec = param['min']
465 if 'calc' in minrec:
466 calc = minrec['calc']
467 else:
468 calc = 'min'
469 if self.check_failure(minrec, calc, value):
470 lstyle = 'red.TLabel'
471 if 'max' in param:
472 maxrec = param['max']
473 if 'calc' in maxrec:
474 calc = maxrec['calc']
475 else:
476 calc = 'max'
477 if self.check_failure(maxrec, calc, value):
478 lstyle = 'red.TLabel'
479
480 for condition, range in zip(result, ranges):
481 if len(range) > 1:
482 pname = ttk.Label(body, text=condition, style = lstyle)
483 pname.grid(row = m, column = j, sticky = 'ewns')
484 j += 1
485
486 # Row 2 contains the ranges of each column
487 j = 1
488 k = 1
489 for vrange in ranges[1:]:
490 if len(vrange) > 1:
491
492 condlimits = '( '
493
494 # This is a bit of a hack; results are assumed floating-point
495 # unless they can't be resolved as a number. So numerical values
496 # that should be treated as integers or strings must be handled
497 # here according to the condition type.
498 if names[k].split(':')[0] == 'DIGITAL':
499 for l in vrange:
500 condlimits += str(int(float(l))) + ' '
501 else:
502 for l in vrange:
503 condlimits += l + ' '
504 condlimits += ')'
505 header = ttk.Label(body, text=condlimits, style = 'blue.TLabel')
506 header.grid(row = 2, column = j, sticky = 'ewns')
507 j += 1
508 k += 1
509
510 # Add padding around widgets in the body of the failure report, so that
511 # the frame background comes through, making a grid.
512 for child in body.winfo_children():
513 child.grid_configure(ipadx = 5, ipady = 1, padx = 2, pady = 2)
514
515 # Resize the window to fit in the display, if necessary.
516 self.size_failreport()
517
518 # Don't put the button at the bottom to return to table view.
519 self.bbar.table_button.grid_forget()
520 # Finally, open the window if it was not already open.
521 self.open()
522
523 def changesort(self):
524 self.sortdir = False if self.sortdir == True else True
525 self.display(param=None)
526
527 def close(self):
528 # pop down failure report window
529 self.withdraw()
530
531 def open(self):
532 # pop up failure report window
533 self.deiconify()
534 self.lift()