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