blob: 5951d35663cbf8792e787b616f45bc52e1c19271 [file] [log] [blame]
Tim Edwardsdae621a2021-09-08 09:28:02 -04001#!/usr/bin/env python3
emayecs5966a532021-07-29 10:07:02 -04002#
3#--------------------------------------------------------
emayecs14748312021-08-05 14:21:26 -04004# Project Manager GUI.
emayecs5966a532021-07-29 10:07:02 -04005#
6# This is a Python tkinter script that handles local
7# project management. Much of this involves the
8# running of ng-spice for characterization, allowing
9# the user to determine where a circuit is failing
10# characterization; and when the design passes local
11# characterization, it may be submitted to the
12# marketplace for official characterization.
13#
14#--------------------------------------------------------
15# Written by Tim Edwards
16# efabless, inc.
17# September 9, 2016
18# Version 1.0
19#--------------------------------------------------------
20
21import io
22import re
23import os
24import sys
25import copy
26import json
27import time
28import signal
29import select
30import datetime
31import contextlib
32import subprocess
33import faulthandler
34
35import tkinter
36from tkinter import ttk
37from tkinter import filedialog
38
39import tksimpledialog
40import tooltip
41from consoletext import ConsoleText
42from helpwindow import HelpWindow
43from failreport import FailReport
44from textreport import TextReport
45from editparam import EditParam
46from settings import Settings
47from simhints import SimHints
48
emayecs5966a532021-07-29 10:07:02 -040049# User preferences file (if it exists)
50prefsfile = '~/design/.profile/prefs.json'
51
Tim Edwards32d012a2023-03-07 10:11:32 -050052# Application path (path where this script is located)
53apps_path = os.path.realpath(os.path.dirname(__file__))
54
emayecs5966a532021-07-29 10:07:02 -040055#------------------------------------------------------
56# Simple dialog for confirming quit or upload
57#------------------------------------------------------
58
59class ConfirmDialog(tksimpledialog.Dialog):
60 def body(self, master, warning, seed):
61 ttk.Label(master, text=warning, wraplength=500).grid(row = 0, columnspan = 2, sticky = 'wns')
62 return self
63
64 def apply(self):
65 return 'okay'
66
67#------------------------------------------------------
68# Simple dialog with no "OK" button (can only cancel)
69#------------------------------------------------------
70
71class PuntDialog(tksimpledialog.Dialog):
72 def body(self, master, warning, seed):
73 if warning:
74 ttk.Label(master, text=warning, wraplength=500).grid(row = 0, columnspan = 2, sticky = 'wns')
75 return self
76
77 def buttonbox(self):
78 # Add button box with "Cancel" only.
79 box = ttk.Frame(self.obox)
80 w = ttk.Button(box, text="Cancel", width=10, command=self.cancel)
81 w.pack(side='left', padx=5, pady=5)
82 self.bind("<Escape>", self.cancel)
83 box.pack(fill='x', expand='true')
84
85 def apply(self):
86 return 'okay'
87
88#------------------------------------------------------
89# Main class for this application
90#------------------------------------------------------
91
Tim Edwards63dbde92023-03-07 20:58:29 -050092class CACECharacterize(ttk.Frame):
emayecs14748312021-08-05 14:21:26 -040093 """local characterization GUI."""
emayecs5966a532021-07-29 10:07:02 -040094
95 def __init__(self, parent, *args, **kwargs):
96 ttk.Frame.__init__(self, parent, *args, **kwargs)
97 self.root = parent
98 self.init_gui()
99 parent.protocol("WM_DELETE_WINDOW", self.on_quit)
100
101 def on_quit(self):
102 """Exits program."""
103 if not self.check_saved():
104 warning = 'Warning: Simulation results have not been saved.'
105 confirm = ConfirmDialog(self, warning).result
106 if not confirm == 'okay':
107 print('Quit canceled.')
108 return
109 if self.logfile:
110 self.logfile.close()
111 quit()
112
113 def on_mousewheel(self, event):
114 if event.num == 5:
115 self.datasheet_viewer.yview_scroll(1, "units")
116 elif event.num == 4:
117 self.datasheet_viewer.yview_scroll(-1, "units")
118
119 def init_gui(self):
120 """Builds GUI."""
121 global prefsfile
122
123 message = []
124 fontsize = 11
125
126 # Read user preferences file, get default font size from it.
127 prefspath = os.path.expanduser(prefsfile)
128 if os.path.exists(prefspath):
129 with open(prefspath, 'r') as f:
130 self.prefs = json.load(f)
131 if 'fontsize' in self.prefs:
132 fontsize = self.prefs['fontsize']
133 else:
134 self.prefs = {}
135
136 s = ttk.Style()
137
138 available_themes = s.theme_names()
139 s.theme_use(available_themes[0])
140
141 s.configure('bg.TFrame', background='gray40')
142 s.configure('italic.TLabel', font=('Helvetica', fontsize, 'italic'))
143 s.configure('title.TLabel', font=('Helvetica', fontsize, 'bold italic'),
144 foreground = 'brown', anchor = 'center')
145 s.configure('normal.TLabel', font=('Helvetica', fontsize))
146 s.configure('red.TLabel', font=('Helvetica', fontsize), foreground = 'red')
147 s.configure('green.TLabel', font=('Helvetica', fontsize), foreground = 'green3')
148 s.configure('blue.TLabel', font=('Helvetica', fontsize), foreground = 'blue')
149 s.configure('hlight.TLabel', font=('Helvetica', fontsize), background='gray93')
150 s.configure('rhlight.TLabel', font=('Helvetica', fontsize), foreground = 'red',
151 background='gray93')
152 s.configure('ghlight.TLabel', font=('Helvetica', fontsize), foreground = 'green3',
153 background='gray93')
154 s.configure('blue.TLabel', font=('Helvetica', fontsize), foreground = 'blue')
155 s.configure('blue.TMenubutton', font=('Helvetica', fontsize), foreground = 'blue',
156 border = 3, relief = 'raised')
157 s.configure('normal.TButton', font=('Helvetica', fontsize),
158 border = 3, relief = 'raised')
159 s.configure('red.TButton', font=('Helvetica', fontsize), foreground = 'red',
160 border = 3, relief = 'raised')
161 s.configure('green.TButton', font=('Helvetica', fontsize), foreground = 'green3',
162 border = 3, relief = 'raised')
163 s.configure('hlight.TButton', font=('Helvetica', fontsize),
164 border = 3, relief = 'raised', background='gray93')
165 s.configure('rhlight.TButton', font=('Helvetica', fontsize), foreground = 'red',
166 border = 3, relief = 'raised', background='gray93')
167 s.configure('ghlight.TButton', font=('Helvetica', fontsize), foreground = 'green3',
168 border = 3, relief = 'raised', background='gray93')
169 s.configure('blue.TButton', font=('Helvetica', fontsize), foreground = 'blue',
170 border = 3, relief = 'raised')
171 s.configure('redtitle.TButton', font=('Helvetica', fontsize, 'bold italic'),
172 foreground = 'red', border = 3, relief = 'raised')
173 s.configure('bluetitle.TButton', font=('Helvetica', fontsize, 'bold italic'),
174 foreground = 'blue', border = 3, relief = 'raised')
175
176 # Create the help window
177 self.help = HelpWindow(self, fontsize = fontsize)
178
179 with io.StringIO() as buf, contextlib.redirect_stdout(buf):
Tim Edwards32d012a2023-03-07 10:11:32 -0500180 self.help.add_pages_from_file(apps_path + '/characterize_help.txt')
emayecs5966a532021-07-29 10:07:02 -0400181 message = buf.getvalue()
182
183 # Set the help display to the first page
184 self.help.page(0)
185
186 # Create the failure report window
187 self.failreport = FailReport(self, fontsize = fontsize)
188
189 # LVS results get a text window of results
190 self.textreport = TextReport(self, fontsize = fontsize)
191
192 # Create the settings window
193 self.settings = Settings(self, fontsize = fontsize, callback = self.callback)
194
195 # Create the simulation hints window
196 self.simhints = SimHints(self, fontsize = fontsize)
197
198 # Create the edit parameter window
199 self.editparam = EditParam(self, fontsize = fontsize)
200
201 # Variables used by option menus and other stuff
202 self.origin = tkinter.StringVar(self)
203 self.cur_project = tkinter.StringVar(self)
204 self.cur_datasheet = "(no selection)"
205 self.datatop = {}
206 self.status = {}
207 self.caceproc = None
208 self.logfile = None
209
210 # Root window title
emayecs14748312021-08-05 14:21:26 -0400211 self.root.title('Characterization')
emayecs5966a532021-07-29 10:07:02 -0400212 self.root.option_add('*tearOff', 'FALSE')
213 self.pack(side = 'top', fill = 'both', expand = 'true')
214
215 pane = tkinter.PanedWindow(self, orient = 'vertical', sashrelief='groove', sashwidth=6)
216 pane.pack(side = 'top', fill = 'both', expand = 'true')
217 self.toppane = ttk.Frame(pane)
218 self.botpane = ttk.Frame(pane)
219
220 # Get username
221 if 'username' in self.prefs:
222 username = self.prefs['username']
223 else:
Tim Edwards32d012a2023-03-07 10:11:32 -0500224 username = os.environ['USER']
emayecs5966a532021-07-29 10:07:02 -0400225
226 # Label with the user
227 self.toppane.title_frame = ttk.Frame(self.toppane)
228 self.toppane.title_frame.grid(column = 0, row=0, sticky = 'nswe')
229
230 self.toppane.title_frame.title = ttk.Label(self.toppane.title_frame, text='User:', style = 'red.TLabel')
231 self.toppane.title_frame.user = ttk.Label(self.toppane.title_frame, text=username, style = 'blue.TLabel')
232
233 self.toppane.title_frame.title.grid(column=0, row=0, ipadx = 5)
234 self.toppane.title_frame.user.grid(column=1, row=0, ipadx = 5)
235
236 #---------------------------------------------
237 ttk.Separator(self.toppane, orient='horizontal').grid(column = 0, row = 1, sticky = 'nswe')
238 #---------------------------------------------
239
240 self.toppane.title2_frame = ttk.Frame(self.toppane)
241 self.toppane.title2_frame.grid(column = 0, row = 2, sticky = 'nswe')
242 self.toppane.title2_frame.datasheet_label = ttk.Label(self.toppane.title2_frame, text="Datasheet:",
243 style = 'normal.TLabel')
244 self.toppane.title2_frame.datasheet_label.grid(column=0, row=0, ipadx = 5)
245
246 # New datasheet select button
247 self.toppane.title2_frame.datasheet_select = ttk.Button(self.toppane.title2_frame,
248 text=self.cur_datasheet, style='normal.TButton', command=self.choose_datasheet)
249 self.toppane.title2_frame.datasheet_select.grid(column=1, row=0, ipadx = 5)
250
251 tooltip.ToolTip(self.toppane.title2_frame.datasheet_select,
252 text = "Select new datasheet file")
253
254 # Show path to datasheet
255 self.toppane.title2_frame.path_label = ttk.Label(self.toppane.title2_frame, text=self.cur_datasheet,
256 style = 'normal.TLabel')
257 self.toppane.title2_frame.path_label.grid(column=2, row=0, ipadx = 5, padx = 10)
258
259 # Spacer in middle moves selection button to right
260 self.toppane.title2_frame.sep_label = ttk.Label(self.toppane.title2_frame, text=' ',
261 style = 'normal.TLabel')
262 self.toppane.title2_frame.sep_label.grid(column=3, row=0, ipadx = 5, padx = 10)
263 self.toppane.title2_frame.columnconfigure(3, weight = 1)
264 self.toppane.title2_frame.rowconfigure(0, weight=0)
265
266 # Selection for origin of netlist
267 self.toppane.title2_frame.origin_label = ttk.Label(self.toppane.title2_frame,
268 text='Netlist from:', style = 'normal.TLabel')
269 self.toppane.title2_frame.origin_label.grid(column=4, row=0, ipadx = 5, padx = 10)
270
271 self.origin.set('Schematic Capture')
272 self.toppane.title2_frame.origin_select = ttk.OptionMenu(self.toppane.title2_frame,
273 self.origin, 'Schematic Capture', 'Schematic Capture', 'Layout Extracted',
274 style='blue.TMenubutton', command=self.load_results)
275 self.toppane.title2_frame.origin_select.grid(column=5, row=0, ipadx = 5)
276
277 #---------------------------------------------
278 ttk.Separator(self.toppane, orient='horizontal').grid(column = 0, row = 3, sticky = 'news')
279 #---------------------------------------------
280
281 # Datasheet information goes here when datasheet is loaded.
282 self.mframe = ttk.Frame(self.toppane)
283 self.mframe.grid(column = 0, row = 4, sticky = 'news')
284
285 # Row 4 (mframe) is expandable, the other rows are not.
286 self.toppane.rowconfigure(0, weight = 0)
287 self.toppane.rowconfigure(1, weight = 0)
288 self.toppane.rowconfigure(2, weight = 0)
289 self.toppane.rowconfigure(3, weight = 0)
290 self.toppane.rowconfigure(4, weight = 1)
291 self.toppane.columnconfigure(0, weight = 1)
292
293 #---------------------------------------------
294 # ttk.Separator(self, orient='horizontal').grid(column=0, row=5, sticky='ew')
295 #---------------------------------------------
296
297 # Add a text window below the datasheet to capture output. Redirect
298 # print statements to it.
299
300 self.botpane.console = ttk.Frame(self.botpane)
301 self.botpane.console.pack(side = 'top', fill = 'both', expand = 'true')
302
303 self.text_box = ConsoleText(self.botpane.console, wrap='word', height = 4)
304 self.text_box.pack(side='left', fill='both', expand='true')
305 console_scrollbar = ttk.Scrollbar(self.botpane.console)
306 console_scrollbar.pack(side='right', fill='y')
307 # attach console to scrollbar
308 self.text_box.config(yscrollcommand = console_scrollbar.set)
309 console_scrollbar.config(command = self.text_box.yview)
310
311 # Add button bar at the bottom of the window
312 self.bbar = ttk.Frame(self.botpane)
313 self.bbar.pack(side = 'top', fill = 'x')
314 # Progress bar expands with the window, buttons don't
315 self.bbar.columnconfigure(6, weight = 1)
316
317 # Define the "quit" button and action
318 self.bbar.quit_button = ttk.Button(self.bbar, text='Close', command=self.on_quit,
319 style = 'normal.TButton')
320 self.bbar.quit_button.grid(column=0, row=0, padx = 5)
321
322 # Define the save button
323 self.bbar.save_button = ttk.Button(self.bbar, text='Save', command=self.save_results,
324 style = 'normal.TButton')
325 self.bbar.save_button.grid(column=1, row=0, padx = 5)
326
327 # Define the save-as button
328 self.bbar.saveas_button = ttk.Button(self.bbar, text='Save As', command=self.save_manual,
329 style = 'normal.TButton')
330
331 # Also a load button
332 self.bbar.load_button = ttk.Button(self.bbar, text='Load', command=self.load_manual,
333 style = 'normal.TButton')
334
335 # Define help button
336 self.bbar.help_button = ttk.Button(self.bbar, text='Help', command=self.help.open,
337 style = 'normal.TButton')
338 self.bbar.help_button.grid(column = 4, row = 0, padx = 5)
339
340 # Define settings button
341 self.bbar.settings_button = ttk.Button(self.bbar, text='Settings',
342 command=self.settings.open, style = 'normal.TButton')
343 self.bbar.settings_button.grid(column = 5, row = 0, padx = 5)
344
345 # Define upload action
346 self.bbar.upload_button = ttk.Button(self.bbar, text='Submit', state = 'enabled',
347 command=self.upload_to_marketplace, style = 'normal.TButton')
348 # "Submit" button remains unplaced; upload may be done from the web side. . .
349 # self.bbar.upload_button.grid(column = 8, row = 0, padx = 5, sticky = 'ens')
350
351 tooltip.ToolTip(self.bbar.quit_button, text = "Exit characterization tool")
352 tooltip.ToolTip(self.bbar.save_button, text = "Save current characterization state")
353 tooltip.ToolTip(self.bbar.saveas_button, text = "Save current characterization state")
354 tooltip.ToolTip(self.bbar.load_button, text = "Load characterization state from file")
355 tooltip.ToolTip(self.bbar.help_button, text = "Start help tool")
356 tooltip.ToolTip(self.bbar.settings_button, text = "Manage characterization tool settings")
357 tooltip.ToolTip(self.bbar.upload_button, text = "Submit completed design to Marketplace")
358
359 # Inside frame with main electrical parameter display and scrollbar
360 # To make the frame scrollable, it must be a frame inside a canvas.
361 self.datasheet_viewer = tkinter.Canvas(self.mframe)
362 self.datasheet_viewer.grid(row = 0, column = 0, sticky = 'nsew')
363 self.datasheet_viewer.dframe = ttk.Frame(self.datasheet_viewer,
364 style='bg.TFrame')
365 # Place the frame in the canvas
366 self.datasheet_viewer.create_window((0,0),
367 window=self.datasheet_viewer.dframe,
368 anchor="nw", tags="self.frame")
369
370 # Make sure the main window resizes, not the scrollbars.
371 self.mframe.rowconfigure(0, weight = 1)
372 self.mframe.columnconfigure(0, weight = 1)
373 # X scrollbar for datasheet viewer
374 main_xscrollbar = ttk.Scrollbar(self.mframe, orient = 'horizontal')
375 main_xscrollbar.grid(row = 1, column = 0, sticky = 'nsew')
376 # Y scrollbar for datasheet viewer
377 main_yscrollbar = ttk.Scrollbar(self.mframe, orient = 'vertical')
378 main_yscrollbar.grid(row = 0, column = 1, sticky = 'nsew')
379 # Attach console to scrollbars
380 self.datasheet_viewer.config(xscrollcommand = main_xscrollbar.set)
381 main_xscrollbar.config(command = self.datasheet_viewer.xview)
382 self.datasheet_viewer.config(yscrollcommand = main_yscrollbar.set)
383 main_yscrollbar.config(command = self.datasheet_viewer.yview)
384
385 # Make sure that scrollwheel pans window
386 self.datasheet_viewer.bind_all("<Button-4>", self.on_mousewheel)
387 self.datasheet_viewer.bind_all("<Button-5>", self.on_mousewheel)
388
389 # Set up configure callback
390 self.datasheet_viewer.dframe.bind("<Configure>", self.frame_configure)
391
392 # Add the panes once the internal geometry is known
393 pane.add(self.toppane)
394 pane.add(self.botpane)
395 pane.paneconfig(self.toppane, stretch='first')
396
397 # Initialize variables
398 self.sims_to_go = []
399
400 # Capture time of start to compare against the annotated
401 # output file timestamp.
402 self.starttime = time.time()
403
404 # Redirect stdout and stderr to the console as the last thing to do. . .
405 # Otherwise errors in the GUI get sucked into the void.
406 self.stdout = sys.stdout
407 self.stderr = sys.stderr
408 sys.stdout = ConsoleText.StdoutRedirector(self.text_box)
409 sys.stderr = ConsoleText.StderrRedirector(self.text_box)
410
411 if message:
412 print(message)
413
414 def frame_configure(self, event):
415 self.update_idletasks()
416 self.datasheet_viewer.configure(scrollregion=self.datasheet_viewer.bbox("all"))
417
418 def logstart(self):
419 # Start a logfile (or append to it, if it already exists)
420 # Disabled by default, as it can get very large.
421 # Can be enabled from Settings.
422 if self.settings.get_log() == True:
423 dataroot = os.path.splitext(self.cur_datasheet)[0]
424 if not self.logfile:
425 self.logfile = open(dataroot + '.log', 'a')
426
427 # Print some initial information to the logfile.
428 self.logprint('-------------------------')
429 self.logprint('Starting new log file ' + datetime.datetime.now().strftime('%c'),
430 doflush=True)
431
432 def logstop(self):
433 if self.logfile:
434 self.logprint('-------------------------', doflush=True)
435 self.logfile.close()
436 self.logfile = []
437
438 def logprint(self, message, doflush=False):
439 if self.logfile:
440 self.logfile.buffer.write(message.encode('utf-8'))
441 self.logfile.buffer.write('\n'.encode('utf-8'))
442 if doflush:
443 self.logfile.flush()
444
445 def set_datasheet(self, datasheet):
446 if self.logfile:
447 self.logprint('end of log.')
448 self.logprint('-------------------------', doflush=True)
449 self.logfile.close()
450 self.logfile = None
451
452 if not os.path.isfile(datasheet):
453 print('Error: File ' + datasheet + ' not found.')
454 return
455
456 [dspath, dsname] = os.path.split(datasheet)
457 # Read the datasheet
458 with open(datasheet) as ifile:
459 try:
460 datatop = json.load(ifile)
461 except json.decoder.JSONDecodeError as e:
462 print("Error: Parse error reading JSON file " + datasheet + ':')
463 print(str(e))
464 return
465 else:
466 # 'request-hash' set to '.' for local simulation
467 datatop['request-hash'] = '.'
468 try:
469 dsheet = datatop['data-sheet']
470 except KeyError:
471 print("Error: JSON file is not a datasheet!\n")
472 else:
473 self.datatop = datatop
474 self.cur_datasheet = datasheet
475 self.create_datasheet_view()
476 self.toppane.title2_frame.datasheet_select.configure(text=dsname)
477 self.toppane.title2_frame.path_label.configure(text=datasheet)
478
479 # Determine if there is a saved, annotated JSON file that is
480 # more recent than the netlist used for simulation.
481 self.load_results()
482
483 # Attempt to set the datasheet viewer width to the interior width
484 # but do not set it larger than the available desktop.
485 self.update_idletasks()
486 widthnow = self.datasheet_viewer.winfo_width()
487 width = self.datasheet_viewer.dframe.winfo_width()
488 screen_width = self.root.winfo_screenwidth()
489 if width > widthnow:
490 if width < screen_width - 10:
491 self.datasheet_viewer.configure(width=width)
492 else:
493 self.datasheet_viewer.configure(width=screen_width - 10)
494 elif widthnow > screen_width:
495 self.datasheet_viewer.configure(width=screen_width - 10)
496 elif widthnow > width:
497 self.datasheet_viewer.configure(width=width)
498
499 # Likewise for the height, up to 3/5 of the desktop height.
500 height = self.datasheet_viewer.dframe.winfo_height()
501 heightnow = self.datasheet_viewer.winfo_height()
502 screen_height = self.root.winfo_screenheight()
503 if height > heightnow:
504 if height < screen_height * 0.6:
505 self.datasheet_viewer.configure(height=height)
506 else:
507 self.datasheet_viewer.configure(height=screen_height * 0.6)
508 elif heightnow > screen_height:
509 self.datasheet_viewer.configure(height=screen_height - 10)
510 elif heightnow > height:
511 self.datasheet_viewer.configure(height=height)
512
513 def choose_datasheet(self):
514 datasheet = filedialog.askopenfilename(multiple = False,
515 initialdir = os.path.expanduser('~/design'),
516 filetypes = (("JSON File", "*.json"),("All Files","*.*")),
517 title = "Find a datasheet.")
518 if datasheet != '':
519 self.set_datasheet(datasheet)
520
521 def cancel_upload(self):
522 # Post a cancelation message to CACE. CACE responds by setting the
523 # status to 'canceled'. The watchprogress procedure is responsible for
524 # returning the button to 'Submit' when the characterization finishes
525 # or is canceled.
526 dspath = os.path.split(self.cur_datasheet)[0]
527 datasheet = os.path.split(self.cur_datasheet)[1]
528 designname = os.path.splitext(datasheet)[0]
529 print('Cancel characterization of ' + designname + ' (' + dspath + ' )')
Tim Edwards32d012a2023-03-07 10:11:32 -0500530 subprocess.run([apps_path + '/cace_design_upload.py', '-cancel',
emayecs5966a532021-07-29 10:07:02 -0400531 dspath])
532 self.removeprogress()
533 self.bbar.upload_button.configure(text='Submit', state = 'enabled',
534 command=self.upload_to_marketplace,
535 style = 'normal.TButton')
536 # Delete the remote status file.
537 dsdir = dspath + '/ngspice/char'
538 statusname = dsdir + '/remote_status.json'
539 if os.path.exists(statusname):
540 os.remove(statusname)
541
542 def progress_bar_setup(self, dspath):
543 # Create the progress bar at the bottom of the window to indicate
544 # the status of a challenge submission.
545
546 # Disable the Submit button
547 self.bbar.upload_button.configure(state='disabled')
548
549 # Start progress bar watchclock
550 dsdir = dspath + '/ngspice/char'
551 statusname = dsdir + '/remote_status.json'
552 if os.path.exists(statusname):
553 statbuf = os.stat(statusname)
554 mtime = statbuf.st_mtime
555 else:
556 if os.path.exists(dsdir):
557 # Write a simple status
558 status = {'message': 'not started', 'total': '0', 'completed': '0'}
559 with open(statusname, 'w') as f:
560 json.dump(status, f)
561 mtime = 0
562 # Create a TTK progress bar widget in the buttonbar.
563 self.bbar.progress_label = ttk.Label(self.bbar, text="Characterization: ",
564 style = 'normal.TLabel')
565 self.bbar.progress_label.grid(column=4, row=0, ipadx = 5)
566
567 self.bbar.progress_message = ttk.Label(self.bbar, text="(not started)",
568 style = 'blue.TLabel')
569 self.bbar.progress_message.grid(column=5, row=0, ipadx = 5)
570 self.bbar.progress = ttk.Progressbar(self.bbar,
571 orient='horizontal', mode='determinate')
572 self.bbar.progress.grid(column = 6, row = 0, padx = 5, sticky = 'nsew')
573 self.bbar.progress_text = ttk.Label(self.bbar, text="0/0",
574 style = 'blue.TLabel')
575 self.bbar.progress_text.grid(column=7, row=0, ipadx = 5)
576
577 # Start the timer to watch the progress
578 self.watchprogress(statusname, mtime, 1)
579
580 def check_ongoing_upload(self):
581 # Determine if an upload is ongoing when the characterization tool is
582 # started. If so, immediately go to the 'characterization running'
583 # state with progress bar.
584 dspath = os.path.split(self.cur_datasheet)[0]
585 datasheet = os.path.split(self.cur_datasheet)[1]
586 designname = os.path.splitext(datasheet)[0]
587 dsdir = dspath + '/ngspice/char'
588 statusname = dsdir + '/remote_status.json'
589 if os.path.exists(statusname):
590 with open(statusname, 'r') as f:
591 status = json.load(f)
592 if 'message' in status:
593 if status['message'] == 'in progress':
594 print('Design characterization in progress for ' + designname + ' (' + dspath + ' )')
595 self.progress_bar_setup(dspath)
596 else:
597 print("No message in status file")
598
599 def upload_to_marketplace(self):
600 dspath = os.path.split(self.cur_datasheet)[0]
601 datasheet = os.path.split(self.cur_datasheet)[1]
602 dsheet = self.datatop['data-sheet']
603 designname = dsheet['ip-name']
604
605 # Make sure a netlist has been generated.
606 if self.sim_param('check') == False:
607 print('No netlist was generated, cannot submit!')
608 return
609
610 # For diagnostic purposes, place all of the characterization tool
611 # settings into datatop['settings'] when uploading to remote CACE.
612 runtime_settings = {}
613 runtime_settings['force-regenerate'] = self.settings.get_force()
614 runtime_settings['edit-all-params'] = self.settings.get_edit()
615 runtime_settings['keep-files'] = self.settings.get_keep()
616 runtime_settings['make-plots'] = self.settings.get_plot()
617 runtime_settings['submit-test-mode'] = self.settings.get_test()
618 runtime_settings['submit-as-schematic'] = self.settings.get_schem()
619 runtime_settings['submit-failing'] = self.settings.get_submitfailed()
620 runtime_settings['log-output'] = self.settings.get_log()
621
622 # Write out runtime settings as a JSON file
623 with open(dspath + '/settings.json', 'w') as file:
624 json.dump(runtime_settings, file, indent = 4)
625
626 warning = ''
627 must_confirm = False
628 if self.settings.get_schem() == True:
629 # If a layout exists but "submit as schematic" was chosen, then
630 # flag a warning and insist on confirmation.
631 if os.path.exists(dspath + '/mag/' + designname + '.mag'):
632 warning += 'Warning: layout exists but only schematic has been selected for submission'
633 must_confirm = True
634 else:
635 print('No layout in ' + dspath + '/mag/' + designname + '.mag')
636 print('Schematic only submission selection is not needed.')
637 else:
638 # Likewise, check if schematic netlist results are showing but a layout
639 # exists, which means that the existing results are not the ones that are
640 # going to be tested.
641 if self.origin.get() == 'Schematic Capture':
642 if os.path.exists(dspath + '/mag/' + designname + '.mag'):
643 warning += 'Warning: schematic results are shown but remote CACE will be run on layout results.'
644 must_confirm = True
645
646
647 # Make a check to see if all simulations have been made and passed. If so,
648 # then just do the upload. If not, then generate a warning dialog and
649 # require the user to respond to force an upload in spite of an incomplete
650 # simulation. Give dire warnings if any simulation has failed.
651
652 failures = 0
653 missed = 0
654 for param in dsheet['electrical-params']:
655 if 'max' in param:
656 pmax = param['max']
657 if not 'value' in pmax:
658 missed += 1
659 elif 'score' in pmax:
660 if pmax['score'] == 'fail':
661 failures += 1
662 if 'min' in param:
663 pmin = param['min']
664 if not 'value' in pmin:
665 missed += 1
666 elif 'score' in pmin:
667 if pmin['score'] == 'fail':
668 failures += 1
669
670 if missed > 0:
671 if must_confirm == True:
672 warning += '\n'
673 warning += 'Warning: Not all critical parameters have been simulated.'
674 if missed > 0 and failures > 0:
675 warning += '\n'
676 if failures > 0:
677 warning += 'Dire Warning: This design has errors on critical parameters!'
678
679 # Require confirmation
680 if missed > 0 or failures > 0:
681 must_confirm = True
682
683 if must_confirm:
684 if self.settings.get_submitfailed() == True:
685 confirm = ConfirmDialog(self, warning).result
686 else:
687 confirm = PuntDialog(self, warning).result
688 if not confirm == 'okay':
689 print('Upload canceled.')
690 return
691 print('Upload selected')
692
Tim Edwards32d012a2023-03-07 10:11:32 -0500693 # Save hints in file in spice/ directory.
emayecs5966a532021-07-29 10:07:02 -0400694 hintlist = []
695 for eparam in dsheet['electrical-params']:
696 if not 'editable' in eparam:
697 if 'hints' in eparam:
698 hintlist.append(eparam['hints'])
699 else:
700 # Must have a placeholder
701 hintlist.append({})
702 if hintlist:
703 hfilename = dspath + '/hints.json'
704 with open(hfilename, 'w') as hfile:
705 json.dump(hintlist, hfile, indent = 4)
706
707 print('Uploading design ' + designname + ' (' + dspath + ' )')
708 print('to marketplace and submitting for characterization.')
709 if not self.settings.get_test():
710 self.progress_bar_setup(dspath)
711 self.update_idletasks()
Tim Edwards32d012a2023-03-07 10:11:32 -0500712 subprocess.run([apps_path + '/cace_design_upload.py', dspath])
emayecs5966a532021-07-29 10:07:02 -0400713
714 # Remove the settings file
715 os.remove(dspath + '/settings.json')
716 os.remove(dspath + '/hints.json')
717
718 def removeprogress(self):
719 # Remove the progress bar. This is left up for a second after
720 # completion or cancelation so that the final message has time
721 # to be seen.
722 try:
723 self.bbar.progress_label.destroy()
724 self.bbar.progress_message.destroy()
725 self.bbar.progress.destroy()
726 self.bbar.progress_text.destroy()
727 except:
728 pass
729
730 def watchprogress(self, filename, filemtime, timeout):
731 new_timeout = timeout + 1 if timeout > 0 else 0
732 # 2 minute timeout for startup (note that all simulation files have to be
733 # made during this period.
734 if new_timeout == 120:
735 self.cancel_upload()
736 return
737
738 # If file does not exist, then keep checking at 2 second intervals.
739 if not os.path.exists(filename):
740 self.after(2000, lambda: self.watchprogress(filename, filemtime, new_timeout))
741 return
742
743 # If filename file is modified, then update progress bar;
744 # otherwise, restart the clock.
745 statbuf = os.stat(filename)
746 if statbuf.st_mtime > filemtime:
747 self.after(250) # Otherwise can catch file while it's incomplete. . .
748 if self.update_progress(filename) == True:
749 self.after(1000, lambda: self.watchprogress(filename, filemtime, 0))
750 else:
751 # Remove the progress bar when done, after letting the final
752 # message display for a second.
753 self.after(1500, self.removeprogress)
754 # And return the button to "Submit" and in an enabled state.
755 self.bbar.upload_button.configure(text='Submit', state = 'enabled',
756 command=self.upload_to_marketplace,
757 style = 'normal.TButton')
758 else:
759 self.after(1000, lambda: self.watchprogress(filename, filemtime, new_timeout))
760
761 def update_progress(self, filename):
762 # On first update, button changes from "Submit" to "Cancel"
763 # This ensures that the 'remote_status.json' file has been sent
764 # from the CACE with the hash value needed for the CACE to identify
765 # the right simulation and cancel it.
766 if self.bbar.upload_button.configure('text')[-1] == 'Submit':
767 self.bbar.upload_button.configure(text='Cancel', state = 'enabled',
768 command=self.cancel_upload, style = 'red.TButton')
769
770 if not os.path.exists(filename):
771 return False
772
773 # Update the progress bar during an CACE simulation run.
774 # Read the status file
775 try:
776 with open(filename, 'r') as f:
777 status = json.load(f)
778 except (PermissionError, FileNotFoundError):
779 # For a very short time the user does not have ownership of
780 # the file and the read will fail. This is a rare case, so
781 # just punt until the next cycle.
782 return True
783
784 if 'message' in status:
785 self.bbar.progress_message.configure(text = status['message'])
786
787 try:
788 total = int(status['total'])
789 except:
790 total = 0
791 else:
792 self.bbar.progress.configure(maximum = total)
793
794 try:
795 completed = int(status['completed'])
796 except:
797 completed = 0
798 else:
799 self.bbar.progress.configure(value = completed)
800
801 self.bbar.progress_text.configure(text = str(completed) + '/' + str(total))
802 if completed > 0 and completed == total:
803 print('Notice: Design completed.')
804 print('The CACE server has finished characterizing the design.')
805 print('Go to the efabless marketplace to view submission.')
806 return False
807 elif status['message'] == 'canceled':
808 print('Notice: Design characterization was canceled.')
809 return False
810 else:
811 return True
812
813 def topfilter(self, line):
814 # Check output for ubiquitous "Reference value" lines and remove them.
815 # This happens before logging both to the file and to the console.
816 refrex = re.compile('Reference value')
817 rmatch = refrex.match(line)
818 if not rmatch:
819 return line
820 else:
821 return None
822
823 def spicefilter(self, line):
824 # Check for the alarmist 'tran simulation interrupted' message and remove it.
825 # Check for error or warning and print as stderr or stdout accordingly.
826 intrex = re.compile('tran simulation interrupted')
827 warnrex = re.compile('.*warning', re.IGNORECASE)
828 errrex = re.compile('.*error', re.IGNORECASE)
829
830 imatch = intrex.match(line)
831 if not imatch:
832 ematch = errrex.match(line)
833 wmatch = warnrex.match(line)
834 if ematch or wmatch:
835 print(line, file=sys.stderr)
836 else:
837 print(line, file=sys.stdout)
838
839 def printwarn(self, output):
840 # Check output for warning or error
841 if not output:
842 return 0
843
844 warnrex = re.compile('.*warning', re.IGNORECASE)
845 errrex = re.compile('.*error', re.IGNORECASE)
846
847 errors = 0
848 outlines = output.splitlines()
849 for line in outlines:
850 try:
851 wmatch = warnrex.match(line)
852 except TypeError:
853 line = line.decode('utf-8')
854 wmatch = warnrex.match(line)
855 ematch = errrex.match(line)
856 if ematch:
857 errors += 1
858 if ematch or wmatch:
859 print(line)
860 return errors
861
862 def sim_all(self):
863 if self.caceproc:
864 # Failsafe
865 if self.caceproc.poll() != None:
866 self.caceproc = None
867 else:
868 print('Simulation in progress must finish first.')
869 return
870
871 # Create netlist if necessary, check for valid result
872 if self.sim_param('check') == False:
873 return
874
875 # Simulate all of the electrical parameters in turn
876 self.sims_to_go = []
877 for puniq in self.status:
878 self.sims_to_go.append(puniq)
879
880 # Start first sim
881 if len(self.sims_to_go) > 0:
882 puniq = self.sims_to_go[0]
883 self.sims_to_go = self.sims_to_go[1:]
884 self.sim_param(puniq)
885
886 # Button now stops the simulations
887 self.allsimbutton.configure(style = 'redtitle.TButton', text='Stop Simulations',
888 command=self.stop_sims)
889
890 def stop_sims(self):
891 # Make sure there will be no more simulations
892 self.sims_to_go = []
893 if not self.caceproc:
894 print("No simulation running.")
895 return
896 self.caceproc.terminate()
897 # Use communicate(), not wait() , on piped processes to avoid deadlock.
898 try:
899 self.caceproc.communicate(timeout=10)
900 except subprocess.TimeoutExpired:
901 self.caceproc.kill()
902 self.caceproc.communicate()
903 print("CACE process killed.")
904 else:
905 print("CACE process exited.")
906 # Let watchdog timer see that caceproc is gone and reset the button.
907
908 def edit_param(self, param):
909 # Edit the conditions under which the parameter is tested.
910 if ('editable' in param and param['editable'] == True) or self.settings.get_edit() == True:
911 self.editparam.populate(param)
912 self.editparam.open()
913 else:
914 print('Parameter is not editable')
915
916 def copy_param(self, param):
917 # Make a copy of the parameter (for editing)
918 newparam = param.copy()
919 # Make the copied parameter editable
920 newparam['editable'] = True
921 # Append this to the electrical parameter list after the item being copied
922 if 'display' in param:
923 newparam['display'] = param['display'] + ' (copy)'
924 datatop = self.datatop
925 dsheet = datatop['data-sheet']
926 eparams = dsheet['electrical-params']
927 eidx = eparams.index(param)
928 eparams.insert(eidx + 1, newparam)
929 self.create_datasheet_view()
930
931 def delete_param(self, param):
932 # Remove an electrical parameter from the datasheet. This is only
933 # allowed if the parameter has been copied from another and so does
934 # not belong to the original set of parameters.
935 datatop = self.datatop
936 dsheet = datatop['data-sheet']
937 eparams = dsheet['electrical-params']
938 eidx = eparams.index(param)
939 eparams.pop(eidx)
940 self.create_datasheet_view()
941
942 def add_hints(self, param, simbutton):
943 # Raise hints window and configure appropriately for the parameter.
944 # Fill in any existing hints.
945 self.simhints.populate(param, simbutton)
946 self.simhints.open()
947
948 def sim_param(self, method):
949 if self.caceproc:
950 # Failsafe
951 if self.caceproc.poll() != None:
952 self.caceproc = None
953 else:
954 print('Simulation in progress, queued for simulation.')
955 if not method in self.sims_to_go:
956 self.sims_to_go.append(method)
957 return False
958
959 # Get basic values for datasheet and ip-name
960
961 dspath = os.path.split(self.cur_datasheet)[0]
962 dsheet = self.datatop['data-sheet']
963 dname = dsheet['ip-name']
964
965 # Open log file, if specified
966 self.logstart()
967
968 # Check for whether the netlist is specified to come from schematic
969 # or layout. Add a record to the datasheet depending on whether
970 # the netlist is from layout or extracted. The settings window has
971 # a checkbox to force submitting as a schematic even if layout exists.
972
973 if self.origin.get() == 'Schematic Capture':
974 dsheet['netlist-source'] = 'schematic'
975 else:
976 dsheet['netlist-source'] = 'layout'
977
978 if self.settings.get_force() == True:
979 dsheet['regenerate'] = 'force'
980
981 basemethod = method.split('.')[0]
982 if basemethod == 'check': # used by submit to ensure netlist exists
983 return True
984
985 if basemethod == 'physical':
986 print('Checking ' + method.split('.')[1])
987 else:
988 print('Simulating method = ' + basemethod)
989 self.stat_label = self.status[method]
990 self.stat_label.configure(text='(in progress)', style='blue.TLabel')
991 # Update status now
992 self.update_idletasks()
Tim Edwards32d012a2023-03-07 10:11:32 -0500993
994 if dspath == '':
995 dspath = '.'
996
emayecs5966a532021-07-29 10:07:02 -0400997 print('Datasheet directory is = ' + dspath + '\n')
998
999 # Instead of using the original datasheet, use the one in memory so that
1000 # it accumulates results. A "save" button will update the original.
1001 if not os.path.isdir(dspath + '/ngspice'):
1002 os.makedirs(dspath + '/ngspice')
Tim Edwards32d012a2023-03-07 10:11:32 -05001003 dsdir = dspath + '/ngspice'
emayecs5966a532021-07-29 10:07:02 -04001004 if not os.path.isdir(dsdir):
1005 os.makedirs(dsdir)
1006 with open(dsdir + '/datasheet.json', 'w') as file:
1007 json.dump(self.datatop, file, indent = 4)
1008 # As soon as we call CACE, we will be watching the status of file
1009 # datasheet_anno. So create it if it does not exist, else attempting
1010 # to stat a nonexistant file will cause the 1st simulation to fail.
1011 if not os.path.exists(dsdir + '/datasheet_anno.json'):
1012 open(dsdir + '/datasheet_anno.json', 'a').close()
1013 # Call cace_gensim with full set of options
1014 # First argument is the root directory
1015 # (Diagnostic)
Tim Edwards32d012a2023-03-07 10:11:32 -05001016 design_path = dspath + '/spice'
emayecs5966a532021-07-29 10:07:02 -04001017
1018 print('Calling cace_gensim.py ' + dspath +
1019 ' -local -method=' + method)
1020
1021 modetext = ['-local']
1022 if self.settings.get_keep() == True:
1023 print(' -keep ')
1024 modetext.append('-keep')
1025
1026 if self.settings.get_plot() == True:
1027 print(' -plot ')
1028 modetext.append('-plot')
1029
1030 print(' -simdir=' + dsdir + ' -datasheetdir=' + dsdir + ' -designdir=' + design_path)
1031 print(' -layoutdir=' + dspath + '/mag' + ' -testbenchdir=' + dspath + '/testbench')
1032 print(' -datasheet=datasheet.json')
1033
Tim Edwards32d012a2023-03-07 10:11:32 -05001034 self.caceproc = subprocess.Popen([apps_path + '/cace_gensim.py', dspath,
emayecs5966a532021-07-29 10:07:02 -04001035 *modetext,
1036 '-method=' + method, # Call local mode w/method
1037 '-simdir=' + dsdir,
1038 '-datasheetdir=' + dsdir,
1039 '-designdir=' + design_path,
1040 '-layoutdir=' + dspath + '/mag',
1041 '-testbenchdir=' + dspath + '/testbench',
1042 '-datasheet=datasheet.json'],
1043 stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0)
1044
1045 # Simulation finishes on its own time. Use watchdog to handle.
1046 # Note that python "watchdog" is threaded, and tkinter is not thread-safe.
1047 # So watchdog is done with a simple timer loop.
1048 statbuf = os.stat(dsdir + '/datasheet.json')
1049 checktime = statbuf.st_mtime
1050
1051 filename = dsdir + '/datasheet_anno.json'
1052 statbuf = os.stat(filename)
1053 self.watchclock(filename, statbuf.st_mtime, checktime)
1054
1055 def watchclock(self, filename, filemtime, checktime):
1056 # In case simulations cleared while watchclock was pending
1057 if self.caceproc == None:
1058 return
1059 # Poll cace_gensim to see if it finished
1060 cace_status = self.caceproc.poll()
1061 if cace_status != None:
1062 try:
1063 output = self.caceproc.communicate(timeout=1)
1064 except ValueError:
1065 print("CACE gensim forced stop, status " + str(cace_status))
1066 else:
1067 outlines = output[0]
1068 errlines = output[1]
1069 for line in outlines.splitlines():
1070 print(line.decode('utf-8'))
1071 for line in errlines.splitlines():
1072 print(line.decode('utf-8'))
1073 print("CACE gensim exited with status " + str(cace_status))
1074 else:
1075 n = 0
1076 while True:
1077 self.update_idletasks()
1078 # Attempt to avoid infinite loop, unsure of the cause.
1079 n += 1
1080 if n > 100:
1081 n = 0
1082 cace_status = self.caceproc.poll()
1083 if cace_status != None:
1084 break
1085 self.logprint("100 lines of output", doflush=True)
1086 # Something went wrong. Kill the process.
1087 # self.stop_sims()
1088 sresult = select.select([self.caceproc.stdout, self.caceproc.stderr], [], [], 0)[0]
1089 if self.caceproc.stdout in sresult:
1090 outstring = self.caceproc.stdout.readline().decode().strip()
1091 self.logprint(outstring, doflush=True)
1092 print(outstring)
1093 elif self.caceproc.stderr in sresult:
1094 # ngspice passes back simulation time on stderr. This ends in \r but no
1095 # newline. '\r' ends the transmission, so return.
1096 # errstring = self.topfilter(self.caceproc.stderr.readline().decode().strip())
1097 # if errstring:
1098 # self.logprint(errstring, doflush=True)
1099 # # Recast everything that isn't an error back into stdout.
1100 # self.spicefilter(errstring)
1101 ochar = str(self.caceproc.stderr.read(1).decode())
1102 if ochar == '\r':
1103 print('')
1104 break
1105 else:
1106 print(ochar, end='')
1107 else:
1108 break
1109
1110 # If filename file is modified, then call annotate; otherwise, restart the clock.
1111 statbuf = os.stat(filename)
1112 if (statbuf.st_mtime > filemtime) or (cace_status != None):
1113 if cace_status != None:
1114 self.caceproc = None
1115 else:
1116 # Re-run to catch last output.
1117 self.after(500, lambda: self.watchclock(filename, statbuf.st_mtime, checktime))
1118 return
1119 if cace_status != 0:
1120 print('Errors encountered in simulation.')
1121 self.logprint('Errors in simulation, CACE status = ' + str(cace_status), doflush=True)
1122 self.annotate('anno', checktime)
1123 if len(self.sims_to_go) > 0:
1124 puniq = self.sims_to_go[0]
1125 self.sims_to_go = self.sims_to_go[1:]
1126 self.sim_param(puniq)
1127 else:
1128 # Button goes back to original text and command
1129 self.allsimbutton.configure(style = 'bluetitle.TButton',
1130 text='Simulate All', command = self.sim_all)
1131 elif not self.caceproc:
1132 # Process terminated by "stop"
1133 # Button goes back to original text and command
1134 self.allsimbutton.configure(style = 'bluetitle.TButton',
1135 text='Simulate All', command = self.sim_all)
1136 # Just redraw everthing so that the "(in progress)" message goes away.
1137 self.annotate('anno', checktime)
1138 else:
1139 self.after(500, lambda: self.watchclock(filename, filemtime, checktime))
1140
1141 def clear_results(self, dsheet):
1142 # Remove results from the window by clearing parameter results
1143 paramstodo = []
1144 if 'electrical-params' in dsheet:
1145 paramstodo.extend(dsheet['electrical-params'])
1146 if 'physical-params' in dsheet:
1147 paramstodo.extend(dsheet['physical-params'])
1148
1149 for param in paramstodo:
1150 # Fill frame with electrical parameter information
1151 if 'max' in param:
1152 maxrec = param['max']
1153 if 'value' in maxrec:
1154 maxrec.pop('value')
1155 if 'score' in maxrec:
1156 maxrec.pop('score')
1157 if 'typ' in param:
1158 typrec = param['typ']
1159 if 'value' in typrec:
1160 typrec.pop('value')
1161 if 'score' in typrec:
1162 typrec.pop('score')
1163 if 'min' in param:
1164 minrec = param['min']
1165 if 'value' in minrec:
1166 minrec.pop('value')
1167 if 'score' in minrec:
1168 minrec.pop('score')
1169 if 'results' in param:
1170 param.pop('results')
1171
1172 if 'plot' in param:
1173 plotrec = param['plot']
1174 if 'status' in plotrec:
1175 plotrec.pop('status')
1176
1177 # Regenerate datasheet view
1178 self.create_datasheet_view()
1179
1180 def annotate(self, suffix, checktime):
1181 # Pull results back from datasheet_anno.json. Do NOT load this
1182 # file if it predates the unannotated datasheet (that indicates
1183 # simulator failure, and no results).
1184 dspath = os.path.split(self.cur_datasheet)[0]
Tim Edwards32d012a2023-03-07 10:11:32 -05001185 if dspath == '':
1186 dspath = '.'
1187 dsdir = dspath + '/ngspice'
emayecs5966a532021-07-29 10:07:02 -04001188 anno = dsdir + '/datasheet_' + suffix + '.json'
1189 unanno = dsdir + '/datasheet.json'
1190
1191 if os.path.exists(anno):
1192 statbuf = os.stat(anno)
1193 mtimea = statbuf.st_mtime
1194 if checktime >= mtimea:
1195 # print('original = ' + str(checktime) + ' annotated = ' + str(mtimea))
1196 print('Error in simulation, no update to results.', file=sys.stderr)
1197 elif statbuf.st_size == 0:
1198 print('Error in simulation, no results.', file=sys.stderr)
1199 else:
1200 with open(anno, 'r') as file:
1201 self.datatop = json.load(file)
1202 else:
1203 print('Error in simulation, no update to results.', file=sys.stderr)
1204
1205 # Regenerate datasheet view
1206 self.create_datasheet_view()
1207
1208 # Close log file, if it was enabled in the settings
1209 self.logstop()
1210
1211 def save_results(self):
1212 # Write datasheet_save with all the locally processed results.
1213 dspath = os.path.split(self.cur_datasheet)[0]
Tim Edwards32d012a2023-03-07 10:11:32 -05001214 dsdir = dspath + '/ngspice'
emayecs5966a532021-07-29 10:07:02 -04001215
1216 if self.origin.get() == 'Layout Extracted':
1217 jsonfile = dsdir + '/datasheet_lsave.json'
1218 else:
1219 jsonfile = dsdir + '/datasheet_save.json'
1220
1221 with open(jsonfile, 'w') as ofile:
1222 json.dump(self.datatop, ofile, indent = 4)
1223 self.last_save = os.path.getmtime(jsonfile)
1224
1225 # Create copy of datasheet without result data. This is
1226 # the file appropriate to insert into the IP catalog
1227 # metadata JSON file.
1228
1229 datacopy = copy.copy(self.datatop)
1230 dsheet = datacopy['data-sheet']
1231 if 'electrical-params' in dsheet:
1232 for eparam in dsheet['electrical-params']:
1233 if 'results' in eparam:
1234 eparam.pop('results')
1235
1236 datacopy.pop('request-hash')
1237 jsonfile = dsdir + '/datasheet_compact.json'
1238 with open(jsonfile, 'w') as ofile:
1239 json.dump(datacopy, ofile, indent = 4)
1240
1241 print('Characterization results saved.')
1242
1243 def check_saved(self):
1244 # Check if there is a file 'datasheet_save' and if it is more
1245 # recent than 'datasheet_anno'. If so, return True, else False.
1246
1247 [dspath, dsname] = os.path.split(self.cur_datasheet)
Tim Edwards32d012a2023-03-07 10:11:32 -05001248 dsdir = dspath + '/ngspice'
emayecs5966a532021-07-29 10:07:02 -04001249
1250 if self.origin.get() == 'Layout Extracted':
1251 savefile = dsdir + '/datasheet_lsave.json'
1252 else:
1253 savefile = dsdir + '/datasheet_save.json'
1254
1255 annofile = dsdir + '/datasheet_anno.json'
1256 if os.path.exists(annofile):
1257 annotime = os.path.getmtime(annofile)
1258
1259 # If nothing has been updated since the characterization
1260 # tool was started, then there is no new information to save.
1261 if annotime < self.starttime:
1262 return True
1263
1264 if os.path.exists(savefile):
1265 savetime = os.path.getmtime(savefile)
1266 # return True if (savetime > annotime) else False
1267 if savetime > annotime:
1268 print("Save is more recent than sim, so no need to save.")
1269 return True
1270 else:
1271 print("Sim is more recent than save, so need to save.")
1272 return False
1273 else:
1274 # There is a datasheet_anno file but no datasheet_save,
1275 # so there are necessarily unsaved results.
1276 print("no datasheet_save, so any results have not been saved.")
1277 return False
1278 else:
1279 # There is no datasheet_anno file, so datasheet_save
1280 # is either current or there have been no simulations.
1281 print("no datasheet_anno, so there are no results to save.")
1282 return True
1283
1284 def callback(self):
1285 # Check for manual load/save-as status from settings window (callback
1286 # when the settings window is closed).
1287 if self.settings.get_loadsave() == True:
1288 self.bbar.saveas_button.grid(column=2, row=0, padx = 5)
1289 self.bbar.load_button.grid(column=3, row=0, padx = 5)
1290 else:
1291 self.bbar.saveas_button.grid_forget()
1292 self.bbar.load_button.grid_forget()
1293
1294 def save_manual(self, value={}):
1295 dspath = self.cur_datasheet
1296 # Set initialdir to the project where cur_datasheet is located
1297 dsparent = os.path.split(dspath)[0]
1298
1299 datasheet = filedialog.asksaveasfilename(multiple = False,
1300 initialdir = dsparent,
1301 confirmoverwrite = True,
1302 defaultextension = ".json",
1303 filetypes = (("JSON File", "*.json"),("All Files","*.*")),
1304 title = "Select filename for saved datasheet.")
1305 with open(datasheet, 'w') as ofile:
1306 json.dump(self.datatop, ofile, indent = 4)
1307
1308 def load_manual(self, value={}):
1309 dspath = self.cur_datasheet
1310 # Set initialdir to the project where cur_datasheet is located
1311 dsparent = os.path.split(dspath)[0]
1312
1313 datasheet = filedialog.askopenfilename(multiple = False,
1314 initialdir = dsparent,
1315 filetypes = (("JSON File", "*.json"),("All Files","*.*")),
1316 title = "Find a datasheet.")
1317 if datasheet != '':
1318 try:
1319 with open(datasheet, 'r') as file:
1320 self.datatop = json.load(file)
1321 except:
1322 print('Error in file, no update to results.', file=sys.stderr)
1323
1324 else:
1325 # Regenerate datasheet view
1326 self.create_datasheet_view()
1327
1328 def load_results(self, value={}):
1329 # Check if datasheet_save exists and is more recent than the
1330 # latest design netlist. If so, load it; otherwise, not.
Tim Edwards32d012a2023-03-07 10:11:32 -05001331 # NOTE: Name of .spice file comes from the project 'ip-name'
emayecs5966a532021-07-29 10:07:02 -04001332 # in the datasheet.
1333
1334 [dspath, dsname] = os.path.split(self.cur_datasheet)
1335 try:
1336 dsheet = self.datatop['data-sheet']
1337 except KeyError:
1338 return
1339
Tim Edwards32d012a2023-03-07 10:11:32 -05001340 if dspath == '':
1341 dspath = '.'
1342
emayecs5966a532021-07-29 10:07:02 -04001343 dsroot = dsheet['ip-name']
1344
1345 # Remove any existing results from the datasheet records
1346 self.clear_results(dsheet)
1347
1348 # Also must be more recent than datasheet
1349 jtime = os.path.getmtime(self.cur_datasheet)
1350
1351 # dsroot = os.path.splitext(dsname)[0]
1352
Tim Edwards32d012a2023-03-07 10:11:32 -05001353 dsdir = dspath + '/spice'
1354
1355 if not os.path.exists(dsdir):
1356 print('Error: Cannot find directory spice/ in path ' + dspath)
1357
emayecs5966a532021-07-29 10:07:02 -04001358 if self.origin.get() == 'Layout Extracted':
Tim Edwards32d012a2023-03-07 10:11:32 -05001359 spifile = dsdir + '/pex/' + dsroot + '.spice'
emayecs5966a532021-07-29 10:07:02 -04001360 savesuffix = 'lsave'
1361 else:
Tim Edwards32d012a2023-03-07 10:11:32 -05001362 spifile = dsdir + '/' + dsroot + '.spice'
emayecs5966a532021-07-29 10:07:02 -04001363 savesuffix = 'save'
1364
Tim Edwards32d012a2023-03-07 10:11:32 -05001365 dsdir = dspath + '/ngspice'
emayecs5966a532021-07-29 10:07:02 -04001366 savefile = dsdir + '/datasheet_' + savesuffix + '.json'
1367
1368 if os.path.exists(savefile):
1369 savetime = os.path.getmtime(savefile)
1370
1371 if os.path.exists(spifile):
1372 spitime = os.path.getmtime(spifile)
1373
1374 if os.path.exists(savefile):
1375 if (savetime > spitime and savetime > jtime):
1376 self.annotate(savesuffix, 0)
1377 print('Characterization results loaded.')
1378 # print('(' + savefile + ' timestamp = ' + str(savetime) + '; ' + self.cur_datasheet + ' timestamp = ' + str(jtime))
1379 else:
1380 print('Saved datasheet is out-of-date, not loading')
1381 else:
1382 print('Datasheet file ' + savefile)
1383 print('No saved datasheet file, nothing to pre-load')
1384 else:
1385 print('No netlist file ' + spifile + '!')
1386
1387 # Remove outdated datasheet.json and datasheet_anno.json to prevent
1388 # them from overwriting characterization document entries
1389
1390 if os.path.exists(savefile):
1391 if savetime < jtime:
1392 print('Removing outdated save file ' + savefile)
1393 os.remove(savefile)
1394
1395 savefile = dsdir + '/datasheet_anno.json'
1396 if os.path.exists(savefile):
1397 savetime = os.path.getmtime(savefile)
1398 if savetime < jtime:
1399 print('Removing outdated results file ' + savefile)
1400 os.remove(savefile)
1401
1402 savefile = dsdir + '/datasheet.json'
1403 if os.path.exists(savefile):
1404 savetime = os.path.getmtime(savefile)
1405 if savetime < jtime:
1406 print('Removing outdated results file ' + savefile)
1407 os.remove(savefile)
1408
1409 def create_datasheet_view(self):
1410 dframe = self.datasheet_viewer.dframe
1411
1412 # Destroy the existing datasheet frame contents (if any)
1413 for widget in dframe.winfo_children():
1414 widget.destroy()
1415 self.status = {} # Clear dictionary
1416
1417 dsheet = self.datatop['data-sheet']
1418 if 'global-conditions' in dsheet:
1419 globcond = dsheet['global-conditions']
1420 else:
1421 globcond = []
1422
1423 # Add basic information at the top
1424
1425 n = 0
1426 dframe.cframe = ttk.Frame(dframe)
1427 dframe.cframe.grid(column = 0, row = n, sticky='ewns', columnspan = 10)
1428
1429 dframe.cframe.plabel = ttk.Label(dframe.cframe, text = 'Project IP name:',
1430 style = 'italic.TLabel')
1431 dframe.cframe.plabel.grid(column = 0, row = n, sticky='ewns', ipadx = 5)
1432 dframe.cframe.pname = ttk.Label(dframe.cframe, text = dsheet['ip-name'],
1433 style = 'normal.TLabel')
1434 dframe.cframe.pname.grid(column = 1, row = n, sticky='ewns', ipadx = 5)
1435 dframe.cframe.fname = ttk.Label(dframe.cframe, text = dsheet['foundry'],
1436 style = 'normal.TLabel')
1437 dframe.cframe.fname.grid(column = 2, row = n, sticky='ewns', ipadx = 5)
1438 dframe.cframe.fname = ttk.Label(dframe.cframe, text = dsheet['node'],
1439 style = 'normal.TLabel')
1440 dframe.cframe.fname.grid(column = 3, row = n, sticky='ewns', ipadx = 5)
1441 if 'decription' in dsheet:
1442 dframe.cframe.pdesc = ttk.Label(dframe.cframe, text = dsheet['description'],
1443 style = 'normal.TLabel')
1444 dframe.cframe.pdesc.grid(column = 4, row = n, sticky='ewns', ipadx = 5)
1445
1446 if 'UID' in self.datatop:
1447 n += 1
1448 dframe.cframe.ulabel = ttk.Label(dframe.cframe, text = 'UID:',
1449 style = 'italic.TLabel')
1450 dframe.cframe.ulabel.grid(column = 0, row = n, sticky='ewns', ipadx = 5)
1451 dframe.cframe.uname = ttk.Label(dframe.cframe, text = self.datatop['UID'],
1452 style = 'normal.TLabel')
1453 dframe.cframe.uname.grid(column = 1, row = n, columnspan = 5, sticky='ewns', ipadx = 5)
1454
1455 n = 1
1456 ttk.Separator(dframe, orient='horizontal').grid(column=0, row=n, sticky='ewns', columnspan=10)
1457
1458 # Title block
1459 n += 1
1460 dframe.desc_title = ttk.Label(dframe, text = 'Parameter', style = 'title.TLabel')
1461 dframe.desc_title.grid(column = 0, row = n, sticky='ewns')
1462 dframe.method_title = ttk.Label(dframe, text = 'Method', style = 'title.TLabel')
1463 dframe.method_title.grid(column = 1, row = n, sticky='ewns')
1464 dframe.min_title = ttk.Label(dframe, text = 'Min', style = 'title.TLabel')
1465 dframe.min_title.grid(column = 2, row = n, sticky='ewns', columnspan = 2)
1466 dframe.typ_title = ttk.Label(dframe, text = 'Typ', style = 'title.TLabel')
1467 dframe.typ_title.grid(column = 4, row = n, sticky='ewns', columnspan = 2)
1468 dframe.max_title = ttk.Label(dframe, text = 'Max', style = 'title.TLabel')
1469 dframe.max_title.grid(column = 6, row = n, sticky='ewns', columnspan = 2)
1470 dframe.stat_title = ttk.Label(dframe, text = 'Status', style = 'title.TLabel')
1471 dframe.stat_title.grid(column = 8, row = n, sticky='ewns')
1472
1473 if not self.sims_to_go:
1474 self.allsimbutton = ttk.Button(dframe, text='Simulate All',
1475 style = 'bluetitle.TButton', command = self.sim_all)
1476 else:
1477 self.allsimbutton = ttk.Button(dframe, text='Stop Simulations',
1478 style = 'redtitle.TButton', command = self.stop_sims)
1479 self.allsimbutton.grid(column = 9, row=n, sticky='ewns')
1480
1481 tooltip.ToolTip(self.allsimbutton, text = "Simulate all electrical parameters")
1482
1483 # Make all columns equally expandable
1484 for i in range(10):
1485 dframe.columnconfigure(i, weight = 1)
1486
1487 # Parse the file for electrical parameters
1488 n += 1
1489 binrex = re.compile(r'([0-9]*)\'([bodh])', re.IGNORECASE)
1490 paramstodo = []
1491 if 'electrical-params' in dsheet:
1492 paramstodo.extend(dsheet['electrical-params'])
1493 if 'physical-params' in dsheet:
1494 paramstodo.extend(dsheet['physical-params'])
1495
1496 if self.origin.get() == 'Schematic Capture':
1497 isschem = True
1498 else:
1499 isschem = False
1500
1501 for param in paramstodo:
1502 # Fill frame with electrical parameter information
1503 if 'method' in param:
1504 p = param['method']
1505 puniq = p + '.0'
1506 if puniq in self.status:
1507 # This method was used before, so give it a unique identifier
1508 j = 1
1509 while True:
1510 puniq = p + '.' + str(j)
1511 if puniq not in self.status:
1512 break
1513 else:
1514 j += 1
1515 else:
1516 j = 0
1517 paramtype = 'electrical'
1518 else:
1519 paramtype = 'physical'
1520 p = param['condition']
1521 puniq = paramtype + '.' + p
1522 j = 0
1523
1524 if 'editable' in param and param['editable'] == True:
1525 normlabel = 'hlight.TLabel'
1526 redlabel = 'rhlight.TLabel'
1527 greenlabel = 'ghlight.TLabel'
1528 normbutton = 'hlight.TButton'
1529 redbutton = 'rhlight.TButton'
1530 greenbutton = 'ghlight.TButton'
1531 else:
1532 normlabel = 'normal.TLabel'
1533 redlabel = 'red.TLabel'
1534 greenlabel = 'green.TLabel'
1535 normbutton = 'normal.TButton'
1536 redbutton = 'red.TButton'
1537 greenbutton = 'green.TButton'
1538
1539 if 'display' in param:
1540 dtext = param['display']
1541 else:
1542 dtext = p
1543
1544 # Special handling: Change LVS_errors to "device check" when using
1545 # schematic netlist.
1546 if paramtype == 'physical':
1547 if isschem:
1548 if p == 'LVS_errors':
1549 dtext = 'Invalid device check'
1550
1551 dframe.description = ttk.Label(dframe, text = dtext, style = normlabel)
1552
1553 dframe.description.grid(column = 0, row=n, sticky='ewns')
1554 dframe.method = ttk.Label(dframe, text = p, style = normlabel)
1555 dframe.method.grid(column = 1, row=n, sticky='ewns')
1556 if 'plot' in param:
1557 status_style = normlabel
1558 dframe.plots = ttk.Frame(dframe)
1559 dframe.plots.grid(column = 2, row=n, columnspan = 6, sticky='ewns')
1560 plotrec = param['plot']
1561 if 'status' in plotrec:
1562 status_value = plotrec['status']
1563 else:
1564 status_value = '(not checked)'
1565 dframe_plot = ttk.Label(dframe.plots, text=plotrec['filename'],
1566 style = normlabel)
1567 dframe_plot.grid(column = j, row = n, sticky='ewns')
1568 else:
1569 # For schematic capture, mark physical parameters that can't and won't be
1570 # checked as "not applicable".
1571 status_value = '(not checked)'
1572 if paramtype == 'physical':
1573 if isschem:
1574 if p == 'area' or p == 'width' or p == 'height' or p == 'DRC_errors':
1575 status_value = '(N/A)'
1576
1577 if 'min' in param:
1578 status_style = normlabel
1579 pmin = param['min']
1580 if 'target' in pmin:
1581 if 'unit' in param and not binrex.match(param['unit']):
1582 targettext = pmin['target'] + ' ' + param['unit']
1583 else:
1584 targettext = pmin['target']
1585 # Hack for use of min to change method of scoring
1586 if not 'penalty' in pmin or pmin['penalty'] != '0':
1587 dframe.min = ttk.Label(dframe, text=targettext, style = normlabel)
1588 else:
1589 dframe.min = ttk.Label(dframe, text='(no limit)', style = normlabel)
1590 else:
1591 dframe.min = ttk.Label(dframe, text='(no limit)', style = normlabel)
1592 if 'score' in pmin:
1593 if pmin['score'] != 'fail':
1594 status_style = greenlabel
1595 if status_value != 'fail':
1596 status_value = 'pass'
1597 else:
1598 status_style = redlabel
1599 status_value = 'fail'
1600 if 'value' in pmin:
1601 if 'unit' in param and not binrex.match(param['unit']):
1602 valuetext = pmin['value'] + ' ' + param['unit']
1603 else:
1604 valuetext = pmin['value']
1605 dframe.value = ttk.Label(dframe, text=valuetext, style=status_style)
1606 dframe.value.grid(column = 3, row=n, sticky='ewns')
1607 else:
1608 dframe.min = ttk.Label(dframe, text='(no limit)', style = normlabel)
1609 dframe.min.grid(column = 2, row=n, sticky='ewns')
1610 if 'typ' in param:
1611 status_style = normlabel
1612 ptyp = param['typ']
1613 if 'target' in ptyp:
1614 if 'unit' in param and not binrex.match(param['unit']):
1615 targettext = ptyp['target'] + ' ' + param['unit']
1616 else:
1617 targettext = ptyp['target']
1618 dframe.typ = ttk.Label(dframe, text=targettext, style = normlabel)
1619 else:
1620 dframe.typ = ttk.Label(dframe, text='(no target)', style = normlabel)
1621 if 'score' in ptyp:
1622 # Note: You can't fail a "typ" score, but there is only one "Status",
1623 # so if it is a "fail", it must remain a "fail".
1624 if ptyp['score'] != 'fail':
1625 status_style = greenlabel
1626 if status_value != 'fail':
1627 status_value = 'pass'
1628 else:
1629 status_style = redlabel
1630 status_value = 'fail'
1631 if 'value' in ptyp:
1632 if 'unit' in param and not binrex.match(param['unit']):
1633 valuetext = ptyp['value'] + ' ' + param['unit']
1634 else:
1635 valuetext = ptyp['value']
1636 dframe.value = ttk.Label(dframe, text=valuetext, style=status_style)
1637 dframe.value.grid(column = 5, row=n, sticky='ewns')
1638 else:
1639 dframe.typ = ttk.Label(dframe, text='(no target)', style = normlabel)
1640 dframe.typ.grid(column = 4, row=n, sticky='ewns')
1641 if 'max' in param:
1642 status_style = normlabel
1643 pmax = param['max']
1644 if 'target' in pmax:
1645 if 'unit' in param and not binrex.match(param['unit']):
1646 targettext = pmax['target'] + ' ' + param['unit']
1647 else:
1648 targettext = pmax['target']
1649 # Hack for use of max to change method of scoring
1650 if not 'penalty' in pmax or pmax['penalty'] != '0':
1651 dframe.max = ttk.Label(dframe, text=targettext, style = normlabel)
1652 else:
1653 dframe.max = ttk.Label(dframe, text='(no limit)', style = normlabel)
1654 else:
1655 dframe.max = ttk.Label(dframe, text='(no limit)', style = normlabel)
1656 if 'score' in pmax:
1657 if pmax['score'] != 'fail':
1658 status_style = greenlabel
1659 if status_value != 'fail':
1660 status_value = 'pass'
1661 else:
1662 status_style = redlabel
1663 status_value = 'fail'
1664 if 'value' in pmax:
1665 if 'unit' in param and not binrex.match(param['unit']):
1666 valuetext = pmax['value'] + ' ' + param['unit']
1667 else:
1668 valuetext = pmax['value']
1669 dframe.value = ttk.Label(dframe, text=valuetext, style=status_style)
1670 dframe.value.grid(column = 7, row=n, sticky='ewns')
1671 else:
1672 dframe.max = ttk.Label(dframe, text='(no limit)', style = normlabel)
1673 dframe.max.grid(column = 6, row=n, sticky='ewns')
1674
1675 if paramtype == 'electrical':
1676 if 'hints' in param:
1677 simtext = '\u2022Simulate'
1678 else:
1679 simtext = 'Simulate'
1680 else:
1681 simtext = 'Check'
1682
1683 simbutton = ttk.Menubutton(dframe, text=simtext, style = normbutton)
1684
1685 # Generate pull-down menu on Simulate button. Most items apply
1686 # only to electrical parameters (at least for now)
1687 simmenu = tkinter.Menu(simbutton)
1688 simmenu.add_command(label='Run',
1689 command = lambda puniq=puniq: self.sim_param(puniq))
1690 simmenu.add_command(label='Stop', command = self.stop_sims)
1691 if paramtype == 'electrical':
1692 simmenu.add_command(label='Hints',
1693 command = lambda param=param, simbutton=simbutton: self.add_hints(param, simbutton))
1694 simmenu.add_command(label='Edit',
1695 command = lambda param=param: self.edit_param(param))
1696 simmenu.add_command(label='Copy',
1697 command = lambda param=param: self.copy_param(param))
1698 if 'editable' in param and param['editable'] == True:
1699 simmenu.add_command(label='Delete',
1700 command = lambda param=param: self.delete_param(param))
1701
1702 # Attach the menu to the button
1703 simbutton.config(menu=simmenu)
1704
1705 # simbutton = ttk.Button(dframe, text=simtext, style = normbutton)
1706 # command = lambda puniq=puniq: self.sim_param(puniq))
1707
1708 simbutton.grid(column = 9, row=n, sticky='ewns')
1709
1710 if paramtype == 'electrical':
1711 tooltip.ToolTip(simbutton, text = "Simulate one electrical parameter")
1712 else:
1713 tooltip.ToolTip(simbutton, text = "Check one physical parameter")
1714
1715 # If 'pass', then just display message. If 'fail', then create a button that
1716 # opens and configures the failure report window.
1717 if status_value == '(not checked)':
1718 bstyle=normbutton
1719 stat_label = ttk.Label(dframe, text=status_value, style=bstyle)
1720 else:
1721 if status_value == 'fail':
1722 bstyle=redbutton
1723 else:
1724 bstyle=greenbutton
1725 if paramtype == 'electrical':
1726 stat_label = ttk.Button(dframe, text=status_value, style=bstyle,
1727 command = lambda param=param, globcond=globcond:
1728 self.failreport.display(param, globcond,
1729 self.cur_datasheet))
1730 elif p == 'LVS_errors':
1731 dspath = os.path.split(self.cur_datasheet)[0]
1732 datasheet = os.path.split(self.cur_datasheet)[1]
1733 dsheet = self.datatop['data-sheet']
1734 designname = dsheet['ip-name']
1735 if self.origin.get() == 'Schematic Capture':
1736 lvs_file = dspath + '/mag/precheck.log'
1737 else:
1738 lvs_file = dspath + '/mag/comp.out'
1739 if not os.path.exists(lvs_file):
1740 if os.path.exists(dspath + '/mag/precheck.log'):
1741 lvs_file = dspath + '/mag/precheck.log'
1742 elif os.path.exists(dspath + '/mag/comp.out'):
1743 lvs_file = dspath + '/mag/comp.out'
1744
1745 stat_label = ttk.Button(dframe, text=status_value, style=bstyle,
1746 command = lambda lvs_file=lvs_file: self.textreport.display(lvs_file))
1747 else:
1748 stat_label = ttk.Label(dframe, text=status_value, style=bstyle)
1749 tooltip.ToolTip(stat_label,
1750 text = "Show detail view of simulation conditions and results")
1751 stat_label.grid(column = 8, row=n, sticky='ewns')
1752 self.status[puniq] = stat_label
1753 n += 1
1754
1755 for child in dframe.winfo_children():
1756 child.grid_configure(ipadx = 5, ipady = 1, padx = 2, pady = 2)
1757
1758 # Check if a design submission and characterization may be in progress.
1759 # If so, add the progress bar at the bottom.
1760 self.check_ongoing_upload()
1761
1762if __name__ == '__main__':
1763 faulthandler.register(signal.SIGUSR2)
1764 options = []
1765 arguments = []
1766 for item in sys.argv[1:]:
1767 if item.find('-', 0) == 0:
1768 options.append(item)
1769 else:
1770 arguments.append(item)
1771
1772 root = tkinter.Tk()
Tim Edwards63dbde92023-03-07 20:58:29 -05001773 app = CACECharacterize(root)
emayecs5966a532021-07-29 10:07:02 -04001774 if arguments:
1775 print('Calling set_datasheet with argument ' + arguments[0])
1776 app.set_datasheet(arguments[0])
1777
1778 root.mainloop()