| #!/usr/bin/env python3 | |
| # | |
| #-------------------------------------------------------- | |
| # Project Manager GUI. | |
| # | |
| # This is a Python tkinter script that handles local | |
| # project management. Much of this involves the | |
| # running of ng-spice for characterization, allowing | |
| # the user to determine where a circuit is failing | |
| # characterization; and when the design passes local | |
| # characterization, it may be submitted to the | |
| # marketplace for official characterization. | |
| # | |
| #-------------------------------------------------------- | |
| # Written by Tim Edwards | |
| # efabless, inc. | |
| # September 9, 2016 | |
| # Version 1.0 | |
| #-------------------------------------------------------- | |
| import io | |
| import re | |
| import os | |
| import sys | |
| import copy | |
| import json | |
| import time | |
| import signal | |
| import select | |
| import datetime | |
| import contextlib | |
| import subprocess | |
| import faulthandler | |
| import tkinter | |
| from tkinter import ttk | |
| from tkinter import filedialog | |
| import tksimpledialog | |
| import tooltip | |
| from consoletext import ConsoleText | |
| from helpwindow import HelpWindow | |
| from failreport import FailReport | |
| from textreport import TextReport | |
| from editparam import EditParam | |
| from settings import Settings | |
| from simhints import SimHints | |
| # User preferences file (if it exists) | |
| prefsfile = '~/design/.profile/prefs.json' | |
| # Application path (path where this script is located) | |
| apps_path = os.path.realpath(os.path.dirname(__file__)) | |
| #------------------------------------------------------ | |
| # Simple dialog for confirming quit or upload | |
| #------------------------------------------------------ | |
| class ConfirmDialog(tksimpledialog.Dialog): | |
| def body(self, master, warning, seed): | |
| ttk.Label(master, text=warning, wraplength=500).grid(row = 0, columnspan = 2, sticky = 'wns') | |
| return self | |
| def apply(self): | |
| return 'okay' | |
| #------------------------------------------------------ | |
| # Simple dialog with no "OK" button (can only cancel) | |
| #------------------------------------------------------ | |
| class PuntDialog(tksimpledialog.Dialog): | |
| def body(self, master, warning, seed): | |
| if warning: | |
| ttk.Label(master, text=warning, wraplength=500).grid(row = 0, columnspan = 2, sticky = 'wns') | |
| return self | |
| def buttonbox(self): | |
| # Add button box with "Cancel" only. | |
| box = ttk.Frame(self.obox) | |
| w = ttk.Button(box, text="Cancel", width=10, command=self.cancel) | |
| w.pack(side='left', padx=5, pady=5) | |
| self.bind("<Escape>", self.cancel) | |
| box.pack(fill='x', expand='true') | |
| def apply(self): | |
| return 'okay' | |
| #------------------------------------------------------ | |
| # Main class for this application | |
| #------------------------------------------------------ | |
| class CACECharacterize(ttk.Frame): | |
| """local characterization GUI.""" | |
| def __init__(self, parent, *args, **kwargs): | |
| ttk.Frame.__init__(self, parent, *args, **kwargs) | |
| self.root = parent | |
| self.init_gui() | |
| parent.protocol("WM_DELETE_WINDOW", self.on_quit) | |
| def on_quit(self): | |
| """Exits program.""" | |
| if not self.check_saved(): | |
| warning = 'Warning: Simulation results have not been saved.' | |
| confirm = ConfirmDialog(self, warning).result | |
| if not confirm == 'okay': | |
| print('Quit canceled.') | |
| return | |
| if self.logfile: | |
| self.logfile.close() | |
| quit() | |
| def on_mousewheel(self, event): | |
| if event.num == 5: | |
| self.datasheet_viewer.yview_scroll(1, "units") | |
| elif event.num == 4: | |
| self.datasheet_viewer.yview_scroll(-1, "units") | |
| def init_gui(self): | |
| """Builds GUI.""" | |
| global prefsfile | |
| message = [] | |
| fontsize = 11 | |
| # Read user preferences file, get default font size from it. | |
| prefspath = os.path.expanduser(prefsfile) | |
| if os.path.exists(prefspath): | |
| with open(prefspath, 'r') as f: | |
| self.prefs = json.load(f) | |
| if 'fontsize' in self.prefs: | |
| fontsize = self.prefs['fontsize'] | |
| else: | |
| self.prefs = {} | |
| s = ttk.Style() | |
| available_themes = s.theme_names() | |
| s.theme_use(available_themes[0]) | |
| s.configure('bg.TFrame', background='gray40') | |
| s.configure('italic.TLabel', font=('Helvetica', fontsize, 'italic')) | |
| s.configure('title.TLabel', font=('Helvetica', fontsize, 'bold italic'), | |
| foreground = 'brown', anchor = 'center') | |
| s.configure('normal.TLabel', font=('Helvetica', fontsize)) | |
| s.configure('red.TLabel', font=('Helvetica', fontsize), foreground = 'red') | |
| s.configure('green.TLabel', font=('Helvetica', fontsize), foreground = 'green3') | |
| s.configure('blue.TLabel', font=('Helvetica', fontsize), foreground = 'blue') | |
| s.configure('hlight.TLabel', font=('Helvetica', fontsize), background='gray93') | |
| s.configure('rhlight.TLabel', font=('Helvetica', fontsize), foreground = 'red', | |
| background='gray93') | |
| s.configure('ghlight.TLabel', font=('Helvetica', fontsize), foreground = 'green3', | |
| background='gray93') | |
| s.configure('blue.TLabel', font=('Helvetica', fontsize), foreground = 'blue') | |
| s.configure('blue.TMenubutton', font=('Helvetica', fontsize), foreground = 'blue', | |
| border = 3, relief = 'raised') | |
| s.configure('normal.TButton', font=('Helvetica', fontsize), | |
| border = 3, relief = 'raised') | |
| s.configure('red.TButton', font=('Helvetica', fontsize), foreground = 'red', | |
| border = 3, relief = 'raised') | |
| s.configure('green.TButton', font=('Helvetica', fontsize), foreground = 'green3', | |
| border = 3, relief = 'raised') | |
| s.configure('hlight.TButton', font=('Helvetica', fontsize), | |
| border = 3, relief = 'raised', background='gray93') | |
| s.configure('rhlight.TButton', font=('Helvetica', fontsize), foreground = 'red', | |
| border = 3, relief = 'raised', background='gray93') | |
| s.configure('ghlight.TButton', font=('Helvetica', fontsize), foreground = 'green3', | |
| border = 3, relief = 'raised', background='gray93') | |
| s.configure('blue.TButton', font=('Helvetica', fontsize), foreground = 'blue', | |
| border = 3, relief = 'raised') | |
| s.configure('redtitle.TButton', font=('Helvetica', fontsize, 'bold italic'), | |
| foreground = 'red', border = 3, relief = 'raised') | |
| s.configure('bluetitle.TButton', font=('Helvetica', fontsize, 'bold italic'), | |
| foreground = 'blue', border = 3, relief = 'raised') | |
| # Create the help window | |
| self.help = HelpWindow(self, fontsize = fontsize) | |
| with io.StringIO() as buf, contextlib.redirect_stdout(buf): | |
| self.help.add_pages_from_file(apps_path + '/characterize_help.txt') | |
| message = buf.getvalue() | |
| # Set the help display to the first page | |
| self.help.page(0) | |
| # Create the failure report window | |
| self.failreport = FailReport(self, fontsize = fontsize) | |
| # LVS results get a text window of results | |
| self.textreport = TextReport(self, fontsize = fontsize) | |
| # Create the settings window | |
| self.settings = Settings(self, fontsize = fontsize, callback = self.callback) | |
| # Create the simulation hints window | |
| self.simhints = SimHints(self, fontsize = fontsize) | |
| # Create the edit parameter window | |
| self.editparam = EditParam(self, fontsize = fontsize) | |
| # Variables used by option menus and other stuff | |
| self.origin = tkinter.StringVar(self) | |
| self.cur_project = tkinter.StringVar(self) | |
| self.cur_datasheet = "(no selection)" | |
| self.datatop = {} | |
| self.status = {} | |
| self.caceproc = None | |
| self.logfile = None | |
| # Root window title | |
| self.root.title('Characterization') | |
| self.root.option_add('*tearOff', 'FALSE') | |
| self.pack(side = 'top', fill = 'both', expand = 'true') | |
| pane = tkinter.PanedWindow(self, orient = 'vertical', sashrelief='groove', sashwidth=6) | |
| pane.pack(side = 'top', fill = 'both', expand = 'true') | |
| self.toppane = ttk.Frame(pane) | |
| self.botpane = ttk.Frame(pane) | |
| # Get username | |
| if 'username' in self.prefs: | |
| username = self.prefs['username'] | |
| else: | |
| username = os.environ['USER'] | |
| # Label with the user | |
| self.toppane.title_frame = ttk.Frame(self.toppane) | |
| self.toppane.title_frame.grid(column = 0, row=0, sticky = 'nswe') | |
| self.toppane.title_frame.title = ttk.Label(self.toppane.title_frame, text='User:', style = 'red.TLabel') | |
| self.toppane.title_frame.user = ttk.Label(self.toppane.title_frame, text=username, style = 'blue.TLabel') | |
| self.toppane.title_frame.title.grid(column=0, row=0, ipadx = 5) | |
| self.toppane.title_frame.user.grid(column=1, row=0, ipadx = 5) | |
| #--------------------------------------------- | |
| ttk.Separator(self.toppane, orient='horizontal').grid(column = 0, row = 1, sticky = 'nswe') | |
| #--------------------------------------------- | |
| self.toppane.title2_frame = ttk.Frame(self.toppane) | |
| self.toppane.title2_frame.grid(column = 0, row = 2, sticky = 'nswe') | |
| self.toppane.title2_frame.datasheet_label = ttk.Label(self.toppane.title2_frame, text="Datasheet:", | |
| style = 'normal.TLabel') | |
| self.toppane.title2_frame.datasheet_label.grid(column=0, row=0, ipadx = 5) | |
| # New datasheet select button | |
| self.toppane.title2_frame.datasheet_select = ttk.Button(self.toppane.title2_frame, | |
| text=self.cur_datasheet, style='normal.TButton', command=self.choose_datasheet) | |
| self.toppane.title2_frame.datasheet_select.grid(column=1, row=0, ipadx = 5) | |
| tooltip.ToolTip(self.toppane.title2_frame.datasheet_select, | |
| text = "Select new datasheet file") | |
| # Show path to datasheet | |
| self.toppane.title2_frame.path_label = ttk.Label(self.toppane.title2_frame, text=self.cur_datasheet, | |
| style = 'normal.TLabel') | |
| self.toppane.title2_frame.path_label.grid(column=2, row=0, ipadx = 5, padx = 10) | |
| # Spacer in middle moves selection button to right | |
| self.toppane.title2_frame.sep_label = ttk.Label(self.toppane.title2_frame, text=' ', | |
| style = 'normal.TLabel') | |
| self.toppane.title2_frame.sep_label.grid(column=3, row=0, ipadx = 5, padx = 10) | |
| self.toppane.title2_frame.columnconfigure(3, weight = 1) | |
| self.toppane.title2_frame.rowconfigure(0, weight=0) | |
| # Selection for origin of netlist | |
| self.toppane.title2_frame.origin_label = ttk.Label(self.toppane.title2_frame, | |
| text='Netlist from:', style = 'normal.TLabel') | |
| self.toppane.title2_frame.origin_label.grid(column=4, row=0, ipadx = 5, padx = 10) | |
| self.origin.set('Schematic Capture') | |
| self.toppane.title2_frame.origin_select = ttk.OptionMenu(self.toppane.title2_frame, | |
| self.origin, 'Schematic Capture', 'Schematic Capture', 'Layout Extracted', | |
| style='blue.TMenubutton', command=self.load_results) | |
| self.toppane.title2_frame.origin_select.grid(column=5, row=0, ipadx = 5) | |
| #--------------------------------------------- | |
| ttk.Separator(self.toppane, orient='horizontal').grid(column = 0, row = 3, sticky = 'news') | |
| #--------------------------------------------- | |
| # Datasheet information goes here when datasheet is loaded. | |
| self.mframe = ttk.Frame(self.toppane) | |
| self.mframe.grid(column = 0, row = 4, sticky = 'news') | |
| # Row 4 (mframe) is expandable, the other rows are not. | |
| self.toppane.rowconfigure(0, weight = 0) | |
| self.toppane.rowconfigure(1, weight = 0) | |
| self.toppane.rowconfigure(2, weight = 0) | |
| self.toppane.rowconfigure(3, weight = 0) | |
| self.toppane.rowconfigure(4, weight = 1) | |
| self.toppane.columnconfigure(0, weight = 1) | |
| #--------------------------------------------- | |
| # ttk.Separator(self, orient='horizontal').grid(column=0, row=5, sticky='ew') | |
| #--------------------------------------------- | |
| # Add a text window below the datasheet to capture output. Redirect | |
| # print statements to it. | |
| self.botpane.console = ttk.Frame(self.botpane) | |
| self.botpane.console.pack(side = 'top', fill = 'both', expand = 'true') | |
| self.text_box = ConsoleText(self.botpane.console, wrap='word', height = 4) | |
| self.text_box.pack(side='left', fill='both', expand='true') | |
| console_scrollbar = ttk.Scrollbar(self.botpane.console) | |
| console_scrollbar.pack(side='right', fill='y') | |
| # attach console to scrollbar | |
| self.text_box.config(yscrollcommand = console_scrollbar.set) | |
| console_scrollbar.config(command = self.text_box.yview) | |
| # Add button bar at the bottom of the window | |
| self.bbar = ttk.Frame(self.botpane) | |
| self.bbar.pack(side = 'top', fill = 'x') | |
| # Progress bar expands with the window, buttons don't | |
| self.bbar.columnconfigure(6, weight = 1) | |
| # Define the "quit" button and action | |
| self.bbar.quit_button = ttk.Button(self.bbar, text='Close', command=self.on_quit, | |
| style = 'normal.TButton') | |
| self.bbar.quit_button.grid(column=0, row=0, padx = 5) | |
| # Define the save button | |
| self.bbar.save_button = ttk.Button(self.bbar, text='Save', command=self.save_results, | |
| style = 'normal.TButton') | |
| self.bbar.save_button.grid(column=1, row=0, padx = 5) | |
| # Define the save-as button | |
| self.bbar.saveas_button = ttk.Button(self.bbar, text='Save As', command=self.save_manual, | |
| style = 'normal.TButton') | |
| # Also a load button | |
| self.bbar.load_button = ttk.Button(self.bbar, text='Load', command=self.load_manual, | |
| style = 'normal.TButton') | |
| # Define help button | |
| self.bbar.help_button = ttk.Button(self.bbar, text='Help', command=self.help.open, | |
| style = 'normal.TButton') | |
| self.bbar.help_button.grid(column = 4, row = 0, padx = 5) | |
| # Define settings button | |
| self.bbar.settings_button = ttk.Button(self.bbar, text='Settings', | |
| command=self.settings.open, style = 'normal.TButton') | |
| self.bbar.settings_button.grid(column = 5, row = 0, padx = 5) | |
| # Define upload action | |
| self.bbar.upload_button = ttk.Button(self.bbar, text='Submit', state = 'enabled', | |
| command=self.upload_to_marketplace, style = 'normal.TButton') | |
| # "Submit" button remains unplaced; upload may be done from the web side. . . | |
| # self.bbar.upload_button.grid(column = 8, row = 0, padx = 5, sticky = 'ens') | |
| tooltip.ToolTip(self.bbar.quit_button, text = "Exit characterization tool") | |
| tooltip.ToolTip(self.bbar.save_button, text = "Save current characterization state") | |
| tooltip.ToolTip(self.bbar.saveas_button, text = "Save current characterization state") | |
| tooltip.ToolTip(self.bbar.load_button, text = "Load characterization state from file") | |
| tooltip.ToolTip(self.bbar.help_button, text = "Start help tool") | |
| tooltip.ToolTip(self.bbar.settings_button, text = "Manage characterization tool settings") | |
| tooltip.ToolTip(self.bbar.upload_button, text = "Submit completed design to Marketplace") | |
| # Inside frame with main electrical parameter display and scrollbar | |
| # To make the frame scrollable, it must be a frame inside a canvas. | |
| self.datasheet_viewer = tkinter.Canvas(self.mframe) | |
| self.datasheet_viewer.grid(row = 0, column = 0, sticky = 'nsew') | |
| self.datasheet_viewer.dframe = ttk.Frame(self.datasheet_viewer, | |
| style='bg.TFrame') | |
| # Place the frame in the canvas | |
| self.datasheet_viewer.create_window((0,0), | |
| window=self.datasheet_viewer.dframe, | |
| anchor="nw", tags="self.frame") | |
| # Make sure the main window resizes, not the scrollbars. | |
| self.mframe.rowconfigure(0, weight = 1) | |
| self.mframe.columnconfigure(0, weight = 1) | |
| # X scrollbar for datasheet viewer | |
| main_xscrollbar = ttk.Scrollbar(self.mframe, orient = 'horizontal') | |
| main_xscrollbar.grid(row = 1, column = 0, sticky = 'nsew') | |
| # Y scrollbar for datasheet viewer | |
| main_yscrollbar = ttk.Scrollbar(self.mframe, orient = 'vertical') | |
| main_yscrollbar.grid(row = 0, column = 1, sticky = 'nsew') | |
| # Attach console to scrollbars | |
| self.datasheet_viewer.config(xscrollcommand = main_xscrollbar.set) | |
| main_xscrollbar.config(command = self.datasheet_viewer.xview) | |
| self.datasheet_viewer.config(yscrollcommand = main_yscrollbar.set) | |
| main_yscrollbar.config(command = self.datasheet_viewer.yview) | |
| # Make sure that scrollwheel pans window | |
| self.datasheet_viewer.bind_all("<Button-4>", self.on_mousewheel) | |
| self.datasheet_viewer.bind_all("<Button-5>", self.on_mousewheel) | |
| # Set up configure callback | |
| self.datasheet_viewer.dframe.bind("<Configure>", self.frame_configure) | |
| # Add the panes once the internal geometry is known | |
| pane.add(self.toppane) | |
| pane.add(self.botpane) | |
| pane.paneconfig(self.toppane, stretch='first') | |
| # Initialize variables | |
| self.sims_to_go = [] | |
| # Capture time of start to compare against the annotated | |
| # output file timestamp. | |
| self.starttime = time.time() | |
| # Redirect stdout and stderr to the console as the last thing to do. . . | |
| # Otherwise errors in the GUI get sucked into the void. | |
| self.stdout = sys.stdout | |
| self.stderr = sys.stderr | |
| sys.stdout = ConsoleText.StdoutRedirector(self.text_box) | |
| sys.stderr = ConsoleText.StderrRedirector(self.text_box) | |
| if message: | |
| print(message) | |
| def frame_configure(self, event): | |
| self.update_idletasks() | |
| self.datasheet_viewer.configure(scrollregion=self.datasheet_viewer.bbox("all")) | |
| def logstart(self): | |
| # Start a logfile (or append to it, if it already exists) | |
| # Disabled by default, as it can get very large. | |
| # Can be enabled from Settings. | |
| if self.settings.get_log() == True: | |
| dataroot = os.path.splitext(self.cur_datasheet)[0] | |
| if not self.logfile: | |
| self.logfile = open(dataroot + '.log', 'a') | |
| # Print some initial information to the logfile. | |
| self.logprint('-------------------------') | |
| self.logprint('Starting new log file ' + datetime.datetime.now().strftime('%c'), | |
| doflush=True) | |
| def logstop(self): | |
| if self.logfile: | |
| self.logprint('-------------------------', doflush=True) | |
| self.logfile.close() | |
| self.logfile = [] | |
| def logprint(self, message, doflush=False): | |
| if self.logfile: | |
| self.logfile.buffer.write(message.encode('utf-8')) | |
| self.logfile.buffer.write('\n'.encode('utf-8')) | |
| if doflush: | |
| self.logfile.flush() | |
| def set_datasheet(self, datasheet): | |
| if self.logfile: | |
| self.logprint('end of log.') | |
| self.logprint('-------------------------', doflush=True) | |
| self.logfile.close() | |
| self.logfile = None | |
| if not os.path.isfile(datasheet): | |
| print('Error: File ' + datasheet + ' not found.') | |
| return | |
| [dspath, dsname] = os.path.split(datasheet) | |
| # Read the datasheet | |
| with open(datasheet) as ifile: | |
| try: | |
| datatop = json.load(ifile) | |
| except json.decoder.JSONDecodeError as e: | |
| print("Error: Parse error reading JSON file " + datasheet + ':') | |
| print(str(e)) | |
| return | |
| else: | |
| # 'request-hash' set to '.' for local simulation | |
| datatop['request-hash'] = '.' | |
| try: | |
| dsheet = datatop['data-sheet'] | |
| except KeyError: | |
| print("Error: JSON file is not a datasheet!\n") | |
| else: | |
| self.datatop = datatop | |
| self.cur_datasheet = datasheet | |
| self.create_datasheet_view() | |
| self.toppane.title2_frame.datasheet_select.configure(text=dsname) | |
| self.toppane.title2_frame.path_label.configure(text=datasheet) | |
| # Determine if there is a saved, annotated JSON file that is | |
| # more recent than the netlist used for simulation. | |
| self.load_results() | |
| # Attempt to set the datasheet viewer width to the interior width | |
| # but do not set it larger than the available desktop. | |
| self.update_idletasks() | |
| widthnow = self.datasheet_viewer.winfo_width() | |
| width = self.datasheet_viewer.dframe.winfo_width() | |
| screen_width = self.root.winfo_screenwidth() | |
| if width > widthnow: | |
| if width < screen_width - 10: | |
| self.datasheet_viewer.configure(width=width) | |
| else: | |
| self.datasheet_viewer.configure(width=screen_width - 10) | |
| elif widthnow > screen_width: | |
| self.datasheet_viewer.configure(width=screen_width - 10) | |
| elif widthnow > width: | |
| self.datasheet_viewer.configure(width=width) | |
| # Likewise for the height, up to 3/5 of the desktop height. | |
| height = self.datasheet_viewer.dframe.winfo_height() | |
| heightnow = self.datasheet_viewer.winfo_height() | |
| screen_height = self.root.winfo_screenheight() | |
| if height > heightnow: | |
| if height < screen_height * 0.6: | |
| self.datasheet_viewer.configure(height=height) | |
| else: | |
| self.datasheet_viewer.configure(height=screen_height * 0.6) | |
| elif heightnow > screen_height: | |
| self.datasheet_viewer.configure(height=screen_height - 10) | |
| elif heightnow > height: | |
| self.datasheet_viewer.configure(height=height) | |
| def choose_datasheet(self): | |
| datasheet = filedialog.askopenfilename(multiple = False, | |
| initialdir = os.path.expanduser('~/design'), | |
| filetypes = (("JSON File", "*.json"),("All Files","*.*")), | |
| title = "Find a datasheet.") | |
| if datasheet != '': | |
| self.set_datasheet(datasheet) | |
| def cancel_upload(self): | |
| # Post a cancelation message to CACE. CACE responds by setting the | |
| # status to 'canceled'. The watchprogress procedure is responsible for | |
| # returning the button to 'Submit' when the characterization finishes | |
| # or is canceled. | |
| dspath = os.path.split(self.cur_datasheet)[0] | |
| datasheet = os.path.split(self.cur_datasheet)[1] | |
| designname = os.path.splitext(datasheet)[0] | |
| print('Cancel characterization of ' + designname + ' (' + dspath + ' )') | |
| subprocess.run([apps_path + '/cace_design_upload.py', '-cancel', | |
| dspath]) | |
| self.removeprogress() | |
| self.bbar.upload_button.configure(text='Submit', state = 'enabled', | |
| command=self.upload_to_marketplace, | |
| style = 'normal.TButton') | |
| # Delete the remote status file. | |
| dsdir = dspath + '/ngspice/char' | |
| statusname = dsdir + '/remote_status.json' | |
| if os.path.exists(statusname): | |
| os.remove(statusname) | |
| def progress_bar_setup(self, dspath): | |
| # Create the progress bar at the bottom of the window to indicate | |
| # the status of a challenge submission. | |
| # Disable the Submit button | |
| self.bbar.upload_button.configure(state='disabled') | |
| # Start progress bar watchclock | |
| dsdir = dspath + '/ngspice/char' | |
| statusname = dsdir + '/remote_status.json' | |
| if os.path.exists(statusname): | |
| statbuf = os.stat(statusname) | |
| mtime = statbuf.st_mtime | |
| else: | |
| if os.path.exists(dsdir): | |
| # Write a simple status | |
| status = {'message': 'not started', 'total': '0', 'completed': '0'} | |
| with open(statusname, 'w') as f: | |
| json.dump(status, f) | |
| mtime = 0 | |
| # Create a TTK progress bar widget in the buttonbar. | |
| self.bbar.progress_label = ttk.Label(self.bbar, text="Characterization: ", | |
| style = 'normal.TLabel') | |
| self.bbar.progress_label.grid(column=4, row=0, ipadx = 5) | |
| self.bbar.progress_message = ttk.Label(self.bbar, text="(not started)", | |
| style = 'blue.TLabel') | |
| self.bbar.progress_message.grid(column=5, row=0, ipadx = 5) | |
| self.bbar.progress = ttk.Progressbar(self.bbar, | |
| orient='horizontal', mode='determinate') | |
| self.bbar.progress.grid(column = 6, row = 0, padx = 5, sticky = 'nsew') | |
| self.bbar.progress_text = ttk.Label(self.bbar, text="0/0", | |
| style = 'blue.TLabel') | |
| self.bbar.progress_text.grid(column=7, row=0, ipadx = 5) | |
| # Start the timer to watch the progress | |
| self.watchprogress(statusname, mtime, 1) | |
| def check_ongoing_upload(self): | |
| # Determine if an upload is ongoing when the characterization tool is | |
| # started. If so, immediately go to the 'characterization running' | |
| # state with progress bar. | |
| dspath = os.path.split(self.cur_datasheet)[0] | |
| datasheet = os.path.split(self.cur_datasheet)[1] | |
| designname = os.path.splitext(datasheet)[0] | |
| dsdir = dspath + '/ngspice/char' | |
| statusname = dsdir + '/remote_status.json' | |
| if os.path.exists(statusname): | |
| with open(statusname, 'r') as f: | |
| status = json.load(f) | |
| if 'message' in status: | |
| if status['message'] == 'in progress': | |
| print('Design characterization in progress for ' + designname + ' (' + dspath + ' )') | |
| self.progress_bar_setup(dspath) | |
| else: | |
| print("No message in status file") | |
| def upload_to_marketplace(self): | |
| dspath = os.path.split(self.cur_datasheet)[0] | |
| datasheet = os.path.split(self.cur_datasheet)[1] | |
| dsheet = self.datatop['data-sheet'] | |
| designname = dsheet['ip-name'] | |
| # Make sure a netlist has been generated. | |
| if self.sim_param('check') == False: | |
| print('No netlist was generated, cannot submit!') | |
| return | |
| # For diagnostic purposes, place all of the characterization tool | |
| # settings into datatop['settings'] when uploading to remote CACE. | |
| runtime_settings = {} | |
| runtime_settings['force-regenerate'] = self.settings.get_force() | |
| runtime_settings['edit-all-params'] = self.settings.get_edit() | |
| runtime_settings['keep-files'] = self.settings.get_keep() | |
| runtime_settings['make-plots'] = self.settings.get_plot() | |
| runtime_settings['submit-test-mode'] = self.settings.get_test() | |
| runtime_settings['submit-as-schematic'] = self.settings.get_schem() | |
| runtime_settings['submit-failing'] = self.settings.get_submitfailed() | |
| runtime_settings['log-output'] = self.settings.get_log() | |
| # Write out runtime settings as a JSON file | |
| with open(dspath + '/settings.json', 'w') as file: | |
| json.dump(runtime_settings, file, indent = 4) | |
| warning = '' | |
| must_confirm = False | |
| if self.settings.get_schem() == True: | |
| # If a layout exists but "submit as schematic" was chosen, then | |
| # flag a warning and insist on confirmation. | |
| if os.path.exists(dspath + '/mag/' + designname + '.mag'): | |
| warning += 'Warning: layout exists but only schematic has been selected for submission' | |
| must_confirm = True | |
| else: | |
| print('No layout in ' + dspath + '/mag/' + designname + '.mag') | |
| print('Schematic only submission selection is not needed.') | |
| else: | |
| # Likewise, check if schematic netlist results are showing but a layout | |
| # exists, which means that the existing results are not the ones that are | |
| # going to be tested. | |
| if self.origin.get() == 'Schematic Capture': | |
| if os.path.exists(dspath + '/mag/' + designname + '.mag'): | |
| warning += 'Warning: schematic results are shown but remote CACE will be run on layout results.' | |
| must_confirm = True | |
| # Make a check to see if all simulations have been made and passed. If so, | |
| # then just do the upload. If not, then generate a warning dialog and | |
| # require the user to respond to force an upload in spite of an incomplete | |
| # simulation. Give dire warnings if any simulation has failed. | |
| failures = 0 | |
| missed = 0 | |
| for param in dsheet['electrical-params']: | |
| if 'max' in param: | |
| pmax = param['max'] | |
| if not 'value' in pmax: | |
| missed += 1 | |
| elif 'score' in pmax: | |
| if pmax['score'] == 'fail': | |
| failures += 1 | |
| if 'min' in param: | |
| pmin = param['min'] | |
| if not 'value' in pmin: | |
| missed += 1 | |
| elif 'score' in pmin: | |
| if pmin['score'] == 'fail': | |
| failures += 1 | |
| if missed > 0: | |
| if must_confirm == True: | |
| warning += '\n' | |
| warning += 'Warning: Not all critical parameters have been simulated.' | |
| if missed > 0 and failures > 0: | |
| warning += '\n' | |
| if failures > 0: | |
| warning += 'Dire Warning: This design has errors on critical parameters!' | |
| # Require confirmation | |
| if missed > 0 or failures > 0: | |
| must_confirm = True | |
| if must_confirm: | |
| if self.settings.get_submitfailed() == True: | |
| confirm = ConfirmDialog(self, warning).result | |
| else: | |
| confirm = PuntDialog(self, warning).result | |
| if not confirm == 'okay': | |
| print('Upload canceled.') | |
| return | |
| print('Upload selected') | |
| # Save hints in file in spice/ directory. | |
| hintlist = [] | |
| for eparam in dsheet['electrical-params']: | |
| if not 'editable' in eparam: | |
| if 'hints' in eparam: | |
| hintlist.append(eparam['hints']) | |
| else: | |
| # Must have a placeholder | |
| hintlist.append({}) | |
| if hintlist: | |
| hfilename = dspath + '/hints.json' | |
| with open(hfilename, 'w') as hfile: | |
| json.dump(hintlist, hfile, indent = 4) | |
| print('Uploading design ' + designname + ' (' + dspath + ' )') | |
| print('to marketplace and submitting for characterization.') | |
| if not self.settings.get_test(): | |
| self.progress_bar_setup(dspath) | |
| self.update_idletasks() | |
| subprocess.run([apps_path + '/cace_design_upload.py', dspath]) | |
| # Remove the settings file | |
| os.remove(dspath + '/settings.json') | |
| os.remove(dspath + '/hints.json') | |
| def removeprogress(self): | |
| # Remove the progress bar. This is left up for a second after | |
| # completion or cancelation so that the final message has time | |
| # to be seen. | |
| try: | |
| self.bbar.progress_label.destroy() | |
| self.bbar.progress_message.destroy() | |
| self.bbar.progress.destroy() | |
| self.bbar.progress_text.destroy() | |
| except: | |
| pass | |
| def watchprogress(self, filename, filemtime, timeout): | |
| new_timeout = timeout + 1 if timeout > 0 else 0 | |
| # 2 minute timeout for startup (note that all simulation files have to be | |
| # made during this period. | |
| if new_timeout == 120: | |
| self.cancel_upload() | |
| return | |
| # If file does not exist, then keep checking at 2 second intervals. | |
| if not os.path.exists(filename): | |
| self.after(2000, lambda: self.watchprogress(filename, filemtime, new_timeout)) | |
| return | |
| # If filename file is modified, then update progress bar; | |
| # otherwise, restart the clock. | |
| statbuf = os.stat(filename) | |
| if statbuf.st_mtime > filemtime: | |
| self.after(250) # Otherwise can catch file while it's incomplete. . . | |
| if self.update_progress(filename) == True: | |
| self.after(1000, lambda: self.watchprogress(filename, filemtime, 0)) | |
| else: | |
| # Remove the progress bar when done, after letting the final | |
| # message display for a second. | |
| self.after(1500, self.removeprogress) | |
| # And return the button to "Submit" and in an enabled state. | |
| self.bbar.upload_button.configure(text='Submit', state = 'enabled', | |
| command=self.upload_to_marketplace, | |
| style = 'normal.TButton') | |
| else: | |
| self.after(1000, lambda: self.watchprogress(filename, filemtime, new_timeout)) | |
| def update_progress(self, filename): | |
| # On first update, button changes from "Submit" to "Cancel" | |
| # This ensures that the 'remote_status.json' file has been sent | |
| # from the CACE with the hash value needed for the CACE to identify | |
| # the right simulation and cancel it. | |
| if self.bbar.upload_button.configure('text')[-1] == 'Submit': | |
| self.bbar.upload_button.configure(text='Cancel', state = 'enabled', | |
| command=self.cancel_upload, style = 'red.TButton') | |
| if not os.path.exists(filename): | |
| return False | |
| # Update the progress bar during an CACE simulation run. | |
| # Read the status file | |
| try: | |
| with open(filename, 'r') as f: | |
| status = json.load(f) | |
| except (PermissionError, FileNotFoundError): | |
| # For a very short time the user does not have ownership of | |
| # the file and the read will fail. This is a rare case, so | |
| # just punt until the next cycle. | |
| return True | |
| if 'message' in status: | |
| self.bbar.progress_message.configure(text = status['message']) | |
| try: | |
| total = int(status['total']) | |
| except: | |
| total = 0 | |
| else: | |
| self.bbar.progress.configure(maximum = total) | |
| try: | |
| completed = int(status['completed']) | |
| except: | |
| completed = 0 | |
| else: | |
| self.bbar.progress.configure(value = completed) | |
| self.bbar.progress_text.configure(text = str(completed) + '/' + str(total)) | |
| if completed > 0 and completed == total: | |
| print('Notice: Design completed.') | |
| print('The CACE server has finished characterizing the design.') | |
| print('Go to the efabless marketplace to view submission.') | |
| return False | |
| elif status['message'] == 'canceled': | |
| print('Notice: Design characterization was canceled.') | |
| return False | |
| else: | |
| return True | |
| def topfilter(self, line): | |
| # Check output for ubiquitous "Reference value" lines and remove them. | |
| # This happens before logging both to the file and to the console. | |
| refrex = re.compile('Reference value') | |
| rmatch = refrex.match(line) | |
| if not rmatch: | |
| return line | |
| else: | |
| return None | |
| def spicefilter(self, line): | |
| # Check for the alarmist 'tran simulation interrupted' message and remove it. | |
| # Check for error or warning and print as stderr or stdout accordingly. | |
| intrex = re.compile('tran simulation interrupted') | |
| warnrex = re.compile('.*warning', re.IGNORECASE) | |
| errrex = re.compile('.*error', re.IGNORECASE) | |
| imatch = intrex.match(line) | |
| if not imatch: | |
| ematch = errrex.match(line) | |
| wmatch = warnrex.match(line) | |
| if ematch or wmatch: | |
| print(line, file=sys.stderr) | |
| else: | |
| print(line, file=sys.stdout) | |
| def printwarn(self, output): | |
| # Check output for warning or error | |
| if not output: | |
| return 0 | |
| warnrex = re.compile('.*warning', re.IGNORECASE) | |
| errrex = re.compile('.*error', re.IGNORECASE) | |
| errors = 0 | |
| outlines = output.splitlines() | |
| for line in outlines: | |
| try: | |
| wmatch = warnrex.match(line) | |
| except TypeError: | |
| line = line.decode('utf-8') | |
| wmatch = warnrex.match(line) | |
| ematch = errrex.match(line) | |
| if ematch: | |
| errors += 1 | |
| if ematch or wmatch: | |
| print(line) | |
| return errors | |
| def sim_all(self): | |
| if self.caceproc: | |
| # Failsafe | |
| if self.caceproc.poll() != None: | |
| self.caceproc = None | |
| else: | |
| print('Simulation in progress must finish first.') | |
| return | |
| # Create netlist if necessary, check for valid result | |
| if self.sim_param('check') == False: | |
| return | |
| # Simulate all of the electrical parameters in turn | |
| self.sims_to_go = [] | |
| for puniq in self.status: | |
| self.sims_to_go.append(puniq) | |
| # Start first sim | |
| if len(self.sims_to_go) > 0: | |
| puniq = self.sims_to_go[0] | |
| self.sims_to_go = self.sims_to_go[1:] | |
| self.sim_param(puniq) | |
| # Button now stops the simulations | |
| self.allsimbutton.configure(style = 'redtitle.TButton', text='Stop Simulations', | |
| command=self.stop_sims) | |
| def stop_sims(self): | |
| # Make sure there will be no more simulations | |
| self.sims_to_go = [] | |
| if not self.caceproc: | |
| print("No simulation running.") | |
| return | |
| self.caceproc.terminate() | |
| # Use communicate(), not wait() , on piped processes to avoid deadlock. | |
| try: | |
| self.caceproc.communicate(timeout=10) | |
| except subprocess.TimeoutExpired: | |
| self.caceproc.kill() | |
| self.caceproc.communicate() | |
| print("CACE process killed.") | |
| else: | |
| print("CACE process exited.") | |
| # Let watchdog timer see that caceproc is gone and reset the button. | |
| def edit_param(self, param): | |
| # Edit the conditions under which the parameter is tested. | |
| if ('editable' in param and param['editable'] == True) or self.settings.get_edit() == True: | |
| self.editparam.populate(param) | |
| self.editparam.open() | |
| else: | |
| print('Parameter is not editable') | |
| def copy_param(self, param): | |
| # Make a copy of the parameter (for editing) | |
| newparam = param.copy() | |
| # Make the copied parameter editable | |
| newparam['editable'] = True | |
| # Append this to the electrical parameter list after the item being copied | |
| if 'display' in param: | |
| newparam['display'] = param['display'] + ' (copy)' | |
| datatop = self.datatop | |
| dsheet = datatop['data-sheet'] | |
| eparams = dsheet['electrical-params'] | |
| eidx = eparams.index(param) | |
| eparams.insert(eidx + 1, newparam) | |
| self.create_datasheet_view() | |
| def delete_param(self, param): | |
| # Remove an electrical parameter from the datasheet. This is only | |
| # allowed if the parameter has been copied from another and so does | |
| # not belong to the original set of parameters. | |
| datatop = self.datatop | |
| dsheet = datatop['data-sheet'] | |
| eparams = dsheet['electrical-params'] | |
| eidx = eparams.index(param) | |
| eparams.pop(eidx) | |
| self.create_datasheet_view() | |
| def add_hints(self, param, simbutton): | |
| # Raise hints window and configure appropriately for the parameter. | |
| # Fill in any existing hints. | |
| self.simhints.populate(param, simbutton) | |
| self.simhints.open() | |
| def sim_param(self, method): | |
| if self.caceproc: | |
| # Failsafe | |
| if self.caceproc.poll() != None: | |
| self.caceproc = None | |
| else: | |
| print('Simulation in progress, queued for simulation.') | |
| if not method in self.sims_to_go: | |
| self.sims_to_go.append(method) | |
| return False | |
| # Get basic values for datasheet and ip-name | |
| dspath = os.path.split(self.cur_datasheet)[0] | |
| dsheet = self.datatop['data-sheet'] | |
| dname = dsheet['ip-name'] | |
| # Open log file, if specified | |
| self.logstart() | |
| # Check for whether the netlist is specified to come from schematic | |
| # or layout. Add a record to the datasheet depending on whether | |
| # the netlist is from layout or extracted. The settings window has | |
| # a checkbox to force submitting as a schematic even if layout exists. | |
| if self.origin.get() == 'Schematic Capture': | |
| dsheet['netlist-source'] = 'schematic' | |
| else: | |
| dsheet['netlist-source'] = 'layout' | |
| if self.settings.get_force() == True: | |
| dsheet['regenerate'] = 'force' | |
| basemethod = method.split('.')[0] | |
| if basemethod == 'check': # used by submit to ensure netlist exists | |
| return True | |
| if basemethod == 'physical': | |
| print('Checking ' + method.split('.')[1]) | |
| else: | |
| print('Simulating method = ' + basemethod) | |
| self.stat_label = self.status[method] | |
| self.stat_label.configure(text='(in progress)', style='blue.TLabel') | |
| # Update status now | |
| self.update_idletasks() | |
| if dspath == '': | |
| dspath = '.' | |
| print('Datasheet directory is = ' + dspath + '\n') | |
| # Instead of using the original datasheet, use the one in memory so that | |
| # it accumulates results. A "save" button will update the original. | |
| if not os.path.isdir(dspath + '/ngspice'): | |
| os.makedirs(dspath + '/ngspice') | |
| dsdir = dspath + '/ngspice' | |
| if not os.path.isdir(dsdir): | |
| os.makedirs(dsdir) | |
| with open(dsdir + '/datasheet.json', 'w') as file: | |
| json.dump(self.datatop, file, indent = 4) | |
| # As soon as we call CACE, we will be watching the status of file | |
| # datasheet_anno. So create it if it does not exist, else attempting | |
| # to stat a nonexistant file will cause the 1st simulation to fail. | |
| if not os.path.exists(dsdir + '/datasheet_anno.json'): | |
| open(dsdir + '/datasheet_anno.json', 'a').close() | |
| # Call cace_gensim with full set of options | |
| # First argument is the root directory | |
| # (Diagnostic) | |
| design_path = dspath + '/spice' | |
| print('Calling cace_gensim.py ' + dspath + | |
| ' -local -method=' + method) | |
| modetext = ['-local'] | |
| if self.settings.get_keep() == True: | |
| print(' -keep ') | |
| modetext.append('-keep') | |
| if self.settings.get_plot() == True: | |
| print(' -plot ') | |
| modetext.append('-plot') | |
| print(' -simdir=' + dsdir + ' -datasheetdir=' + dsdir + ' -designdir=' + design_path) | |
| print(' -layoutdir=' + dspath + '/mag' + ' -testbenchdir=' + dspath + '/testbench') | |
| print(' -datasheet=datasheet.json') | |
| self.caceproc = subprocess.Popen([apps_path + '/cace_gensim.py', dspath, | |
| *modetext, | |
| '-method=' + method, # Call local mode w/method | |
| '-simdir=' + dsdir, | |
| '-datasheetdir=' + dsdir, | |
| '-designdir=' + design_path, | |
| '-layoutdir=' + dspath + '/mag', | |
| '-testbenchdir=' + dspath + '/testbench', | |
| '-datasheet=datasheet.json'], | |
| stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0) | |
| # Simulation finishes on its own time. Use watchdog to handle. | |
| # Note that python "watchdog" is threaded, and tkinter is not thread-safe. | |
| # So watchdog is done with a simple timer loop. | |
| statbuf = os.stat(dsdir + '/datasheet.json') | |
| checktime = statbuf.st_mtime | |
| filename = dsdir + '/datasheet_anno.json' | |
| statbuf = os.stat(filename) | |
| self.watchclock(filename, statbuf.st_mtime, checktime) | |
| def watchclock(self, filename, filemtime, checktime): | |
| # In case simulations cleared while watchclock was pending | |
| if self.caceproc == None: | |
| return | |
| # Poll cace_gensim to see if it finished | |
| cace_status = self.caceproc.poll() | |
| if cace_status != None: | |
| try: | |
| output = self.caceproc.communicate(timeout=1) | |
| except ValueError: | |
| print("CACE gensim forced stop, status " + str(cace_status)) | |
| else: | |
| outlines = output[0] | |
| errlines = output[1] | |
| for line in outlines.splitlines(): | |
| print(line.decode('utf-8')) | |
| for line in errlines.splitlines(): | |
| print(line.decode('utf-8')) | |
| print("CACE gensim exited with status " + str(cace_status)) | |
| else: | |
| n = 0 | |
| while True: | |
| self.update_idletasks() | |
| # Attempt to avoid infinite loop, unsure of the cause. | |
| n += 1 | |
| if n > 100: | |
| n = 0 | |
| cace_status = self.caceproc.poll() | |
| if cace_status != None: | |
| break | |
| self.logprint("100 lines of output", doflush=True) | |
| # Something went wrong. Kill the process. | |
| # self.stop_sims() | |
| sresult = select.select([self.caceproc.stdout, self.caceproc.stderr], [], [], 0)[0] | |
| if self.caceproc.stdout in sresult: | |
| outstring = self.caceproc.stdout.readline().decode().strip() | |
| self.logprint(outstring, doflush=True) | |
| print(outstring) | |
| elif self.caceproc.stderr in sresult: | |
| # ngspice passes back simulation time on stderr. This ends in \r but no | |
| # newline. '\r' ends the transmission, so return. | |
| # errstring = self.topfilter(self.caceproc.stderr.readline().decode().strip()) | |
| # if errstring: | |
| # self.logprint(errstring, doflush=True) | |
| # # Recast everything that isn't an error back into stdout. | |
| # self.spicefilter(errstring) | |
| ochar = str(self.caceproc.stderr.read(1).decode()) | |
| if ochar == '\r': | |
| print('') | |
| break | |
| else: | |
| print(ochar, end='') | |
| else: | |
| break | |
| # If filename file is modified, then call annotate; otherwise, restart the clock. | |
| statbuf = os.stat(filename) | |
| if (statbuf.st_mtime > filemtime) or (cace_status != None): | |
| if cace_status != None: | |
| self.caceproc = None | |
| else: | |
| # Re-run to catch last output. | |
| self.after(500, lambda: self.watchclock(filename, statbuf.st_mtime, checktime)) | |
| return | |
| if cace_status != 0: | |
| print('Errors encountered in simulation.') | |
| self.logprint('Errors in simulation, CACE status = ' + str(cace_status), doflush=True) | |
| self.annotate('anno', checktime) | |
| if len(self.sims_to_go) > 0: | |
| puniq = self.sims_to_go[0] | |
| self.sims_to_go = self.sims_to_go[1:] | |
| self.sim_param(puniq) | |
| else: | |
| # Button goes back to original text and command | |
| self.allsimbutton.configure(style = 'bluetitle.TButton', | |
| text='Simulate All', command = self.sim_all) | |
| elif not self.caceproc: | |
| # Process terminated by "stop" | |
| # Button goes back to original text and command | |
| self.allsimbutton.configure(style = 'bluetitle.TButton', | |
| text='Simulate All', command = self.sim_all) | |
| # Just redraw everthing so that the "(in progress)" message goes away. | |
| self.annotate('anno', checktime) | |
| else: | |
| self.after(500, lambda: self.watchclock(filename, filemtime, checktime)) | |
| def clear_results(self, dsheet): | |
| # Remove results from the window by clearing parameter results | |
| paramstodo = [] | |
| if 'electrical-params' in dsheet: | |
| paramstodo.extend(dsheet['electrical-params']) | |
| if 'physical-params' in dsheet: | |
| paramstodo.extend(dsheet['physical-params']) | |
| for param in paramstodo: | |
| # Fill frame with electrical parameter information | |
| if 'max' in param: | |
| maxrec = param['max'] | |
| if 'value' in maxrec: | |
| maxrec.pop('value') | |
| if 'score' in maxrec: | |
| maxrec.pop('score') | |
| if 'typ' in param: | |
| typrec = param['typ'] | |
| if 'value' in typrec: | |
| typrec.pop('value') | |
| if 'score' in typrec: | |
| typrec.pop('score') | |
| if 'min' in param: | |
| minrec = param['min'] | |
| if 'value' in minrec: | |
| minrec.pop('value') | |
| if 'score' in minrec: | |
| minrec.pop('score') | |
| if 'results' in param: | |
| param.pop('results') | |
| if 'plot' in param: | |
| plotrec = param['plot'] | |
| if 'status' in plotrec: | |
| plotrec.pop('status') | |
| # Regenerate datasheet view | |
| self.create_datasheet_view() | |
| def annotate(self, suffix, checktime): | |
| # Pull results back from datasheet_anno.json. Do NOT load this | |
| # file if it predates the unannotated datasheet (that indicates | |
| # simulator failure, and no results). | |
| dspath = os.path.split(self.cur_datasheet)[0] | |
| if dspath == '': | |
| dspath = '.' | |
| dsdir = dspath + '/ngspice' | |
| anno = dsdir + '/datasheet_' + suffix + '.json' | |
| unanno = dsdir + '/datasheet.json' | |
| if os.path.exists(anno): | |
| statbuf = os.stat(anno) | |
| mtimea = statbuf.st_mtime | |
| if checktime >= mtimea: | |
| # print('original = ' + str(checktime) + ' annotated = ' + str(mtimea)) | |
| print('Error in simulation, no update to results.', file=sys.stderr) | |
| elif statbuf.st_size == 0: | |
| print('Error in simulation, no results.', file=sys.stderr) | |
| else: | |
| with open(anno, 'r') as file: | |
| self.datatop = json.load(file) | |
| else: | |
| print('Error in simulation, no update to results.', file=sys.stderr) | |
| # Regenerate datasheet view | |
| self.create_datasheet_view() | |
| # Close log file, if it was enabled in the settings | |
| self.logstop() | |
| def save_results(self): | |
| # Write datasheet_save with all the locally processed results. | |
| dspath = os.path.split(self.cur_datasheet)[0] | |
| dsdir = dspath + '/ngspice' | |
| if self.origin.get() == 'Layout Extracted': | |
| jsonfile = dsdir + '/datasheet_lsave.json' | |
| else: | |
| jsonfile = dsdir + '/datasheet_save.json' | |
| with open(jsonfile, 'w') as ofile: | |
| json.dump(self.datatop, ofile, indent = 4) | |
| self.last_save = os.path.getmtime(jsonfile) | |
| # Create copy of datasheet without result data. This is | |
| # the file appropriate to insert into the IP catalog | |
| # metadata JSON file. | |
| datacopy = copy.copy(self.datatop) | |
| dsheet = datacopy['data-sheet'] | |
| if 'electrical-params' in dsheet: | |
| for eparam in dsheet['electrical-params']: | |
| if 'results' in eparam: | |
| eparam.pop('results') | |
| datacopy.pop('request-hash') | |
| jsonfile = dsdir + '/datasheet_compact.json' | |
| with open(jsonfile, 'w') as ofile: | |
| json.dump(datacopy, ofile, indent = 4) | |
| print('Characterization results saved.') | |
| def check_saved(self): | |
| # Check if there is a file 'datasheet_save' and if it is more | |
| # recent than 'datasheet_anno'. If so, return True, else False. | |
| [dspath, dsname] = os.path.split(self.cur_datasheet) | |
| dsdir = dspath + '/ngspice' | |
| if self.origin.get() == 'Layout Extracted': | |
| savefile = dsdir + '/datasheet_lsave.json' | |
| else: | |
| savefile = dsdir + '/datasheet_save.json' | |
| annofile = dsdir + '/datasheet_anno.json' | |
| if os.path.exists(annofile): | |
| annotime = os.path.getmtime(annofile) | |
| # If nothing has been updated since the characterization | |
| # tool was started, then there is no new information to save. | |
| if annotime < self.starttime: | |
| return True | |
| if os.path.exists(savefile): | |
| savetime = os.path.getmtime(savefile) | |
| # return True if (savetime > annotime) else False | |
| if savetime > annotime: | |
| print("Save is more recent than sim, so no need to save.") | |
| return True | |
| else: | |
| print("Sim is more recent than save, so need to save.") | |
| return False | |
| else: | |
| # There is a datasheet_anno file but no datasheet_save, | |
| # so there are necessarily unsaved results. | |
| print("no datasheet_save, so any results have not been saved.") | |
| return False | |
| else: | |
| # There is no datasheet_anno file, so datasheet_save | |
| # is either current or there have been no simulations. | |
| print("no datasheet_anno, so there are no results to save.") | |
| return True | |
| def callback(self): | |
| # Check for manual load/save-as status from settings window (callback | |
| # when the settings window is closed). | |
| if self.settings.get_loadsave() == True: | |
| self.bbar.saveas_button.grid(column=2, row=0, padx = 5) | |
| self.bbar.load_button.grid(column=3, row=0, padx = 5) | |
| else: | |
| self.bbar.saveas_button.grid_forget() | |
| self.bbar.load_button.grid_forget() | |
| def save_manual(self, value={}): | |
| dspath = self.cur_datasheet | |
| # Set initialdir to the project where cur_datasheet is located | |
| dsparent = os.path.split(dspath)[0] | |
| datasheet = filedialog.asksaveasfilename(multiple = False, | |
| initialdir = dsparent, | |
| confirmoverwrite = True, | |
| defaultextension = ".json", | |
| filetypes = (("JSON File", "*.json"),("All Files","*.*")), | |
| title = "Select filename for saved datasheet.") | |
| with open(datasheet, 'w') as ofile: | |
| json.dump(self.datatop, ofile, indent = 4) | |
| def load_manual(self, value={}): | |
| dspath = self.cur_datasheet | |
| # Set initialdir to the project where cur_datasheet is located | |
| dsparent = os.path.split(dspath)[0] | |
| datasheet = filedialog.askopenfilename(multiple = False, | |
| initialdir = dsparent, | |
| filetypes = (("JSON File", "*.json"),("All Files","*.*")), | |
| title = "Find a datasheet.") | |
| if datasheet != '': | |
| try: | |
| with open(datasheet, 'r') as file: | |
| self.datatop = json.load(file) | |
| except: | |
| print('Error in file, no update to results.', file=sys.stderr) | |
| else: | |
| # Regenerate datasheet view | |
| self.create_datasheet_view() | |
| def load_results(self, value={}): | |
| # Check if datasheet_save exists and is more recent than the | |
| # latest design netlist. If so, load it; otherwise, not. | |
| # NOTE: Name of .spice file comes from the project 'ip-name' | |
| # in the datasheet. | |
| [dspath, dsname] = os.path.split(self.cur_datasheet) | |
| try: | |
| dsheet = self.datatop['data-sheet'] | |
| except KeyError: | |
| return | |
| if dspath == '': | |
| dspath = '.' | |
| dsroot = dsheet['ip-name'] | |
| # Remove any existing results from the datasheet records | |
| self.clear_results(dsheet) | |
| # Also must be more recent than datasheet | |
| jtime = os.path.getmtime(self.cur_datasheet) | |
| # dsroot = os.path.splitext(dsname)[0] | |
| dsdir = dspath + '/spice' | |
| if not os.path.exists(dsdir): | |
| print('Error: Cannot find directory spice/ in path ' + dspath) | |
| if self.origin.get() == 'Layout Extracted': | |
| spifile = dsdir + '/pex/' + dsroot + '.spice' | |
| savesuffix = 'lsave' | |
| else: | |
| spifile = dsdir + '/' + dsroot + '.spice' | |
| savesuffix = 'save' | |
| dsdir = dspath + '/ngspice' | |
| savefile = dsdir + '/datasheet_' + savesuffix + '.json' | |
| if os.path.exists(savefile): | |
| savetime = os.path.getmtime(savefile) | |
| if os.path.exists(spifile): | |
| spitime = os.path.getmtime(spifile) | |
| if os.path.exists(savefile): | |
| if (savetime > spitime and savetime > jtime): | |
| self.annotate(savesuffix, 0) | |
| print('Characterization results loaded.') | |
| # print('(' + savefile + ' timestamp = ' + str(savetime) + '; ' + self.cur_datasheet + ' timestamp = ' + str(jtime)) | |
| else: | |
| print('Saved datasheet is out-of-date, not loading') | |
| else: | |
| print('Datasheet file ' + savefile) | |
| print('No saved datasheet file, nothing to pre-load') | |
| else: | |
| print('No netlist file ' + spifile + '!') | |
| # Remove outdated datasheet.json and datasheet_anno.json to prevent | |
| # them from overwriting characterization document entries | |
| if os.path.exists(savefile): | |
| if savetime < jtime: | |
| print('Removing outdated save file ' + savefile) | |
| os.remove(savefile) | |
| savefile = dsdir + '/datasheet_anno.json' | |
| if os.path.exists(savefile): | |
| savetime = os.path.getmtime(savefile) | |
| if savetime < jtime: | |
| print('Removing outdated results file ' + savefile) | |
| os.remove(savefile) | |
| savefile = dsdir + '/datasheet.json' | |
| if os.path.exists(savefile): | |
| savetime = os.path.getmtime(savefile) | |
| if savetime < jtime: | |
| print('Removing outdated results file ' + savefile) | |
| os.remove(savefile) | |
| def create_datasheet_view(self): | |
| dframe = self.datasheet_viewer.dframe | |
| # Destroy the existing datasheet frame contents (if any) | |
| for widget in dframe.winfo_children(): | |
| widget.destroy() | |
| self.status = {} # Clear dictionary | |
| dsheet = self.datatop['data-sheet'] | |
| if 'global-conditions' in dsheet: | |
| globcond = dsheet['global-conditions'] | |
| else: | |
| globcond = [] | |
| # Add basic information at the top | |
| n = 0 | |
| dframe.cframe = ttk.Frame(dframe) | |
| dframe.cframe.grid(column = 0, row = n, sticky='ewns', columnspan = 10) | |
| dframe.cframe.plabel = ttk.Label(dframe.cframe, text = 'Project IP name:', | |
| style = 'italic.TLabel') | |
| dframe.cframe.plabel.grid(column = 0, row = n, sticky='ewns', ipadx = 5) | |
| dframe.cframe.pname = ttk.Label(dframe.cframe, text = dsheet['ip-name'], | |
| style = 'normal.TLabel') | |
| dframe.cframe.pname.grid(column = 1, row = n, sticky='ewns', ipadx = 5) | |
| dframe.cframe.fname = ttk.Label(dframe.cframe, text = dsheet['foundry'], | |
| style = 'normal.TLabel') | |
| dframe.cframe.fname.grid(column = 2, row = n, sticky='ewns', ipadx = 5) | |
| dframe.cframe.fname = ttk.Label(dframe.cframe, text = dsheet['node'], | |
| style = 'normal.TLabel') | |
| dframe.cframe.fname.grid(column = 3, row = n, sticky='ewns', ipadx = 5) | |
| if 'decription' in dsheet: | |
| dframe.cframe.pdesc = ttk.Label(dframe.cframe, text = dsheet['description'], | |
| style = 'normal.TLabel') | |
| dframe.cframe.pdesc.grid(column = 4, row = n, sticky='ewns', ipadx = 5) | |
| if 'UID' in self.datatop: | |
| n += 1 | |
| dframe.cframe.ulabel = ttk.Label(dframe.cframe, text = 'UID:', | |
| style = 'italic.TLabel') | |
| dframe.cframe.ulabel.grid(column = 0, row = n, sticky='ewns', ipadx = 5) | |
| dframe.cframe.uname = ttk.Label(dframe.cframe, text = self.datatop['UID'], | |
| style = 'normal.TLabel') | |
| dframe.cframe.uname.grid(column = 1, row = n, columnspan = 5, sticky='ewns', ipadx = 5) | |
| n = 1 | |
| ttk.Separator(dframe, orient='horizontal').grid(column=0, row=n, sticky='ewns', columnspan=10) | |
| # Title block | |
| n += 1 | |
| dframe.desc_title = ttk.Label(dframe, text = 'Parameter', style = 'title.TLabel') | |
| dframe.desc_title.grid(column = 0, row = n, sticky='ewns') | |
| dframe.method_title = ttk.Label(dframe, text = 'Method', style = 'title.TLabel') | |
| dframe.method_title.grid(column = 1, row = n, sticky='ewns') | |
| dframe.min_title = ttk.Label(dframe, text = 'Min', style = 'title.TLabel') | |
| dframe.min_title.grid(column = 2, row = n, sticky='ewns', columnspan = 2) | |
| dframe.typ_title = ttk.Label(dframe, text = 'Typ', style = 'title.TLabel') | |
| dframe.typ_title.grid(column = 4, row = n, sticky='ewns', columnspan = 2) | |
| dframe.max_title = ttk.Label(dframe, text = 'Max', style = 'title.TLabel') | |
| dframe.max_title.grid(column = 6, row = n, sticky='ewns', columnspan = 2) | |
| dframe.stat_title = ttk.Label(dframe, text = 'Status', style = 'title.TLabel') | |
| dframe.stat_title.grid(column = 8, row = n, sticky='ewns') | |
| if not self.sims_to_go: | |
| self.allsimbutton = ttk.Button(dframe, text='Simulate All', | |
| style = 'bluetitle.TButton', command = self.sim_all) | |
| else: | |
| self.allsimbutton = ttk.Button(dframe, text='Stop Simulations', | |
| style = 'redtitle.TButton', command = self.stop_sims) | |
| self.allsimbutton.grid(column = 9, row=n, sticky='ewns') | |
| tooltip.ToolTip(self.allsimbutton, text = "Simulate all electrical parameters") | |
| # Make all columns equally expandable | |
| for i in range(10): | |
| dframe.columnconfigure(i, weight = 1) | |
| # Parse the file for electrical parameters | |
| n += 1 | |
| binrex = re.compile(r'([0-9]*)\'([bodh])', re.IGNORECASE) | |
| paramstodo = [] | |
| if 'electrical-params' in dsheet: | |
| paramstodo.extend(dsheet['electrical-params']) | |
| if 'physical-params' in dsheet: | |
| paramstodo.extend(dsheet['physical-params']) | |
| if self.origin.get() == 'Schematic Capture': | |
| isschem = True | |
| else: | |
| isschem = False | |
| for param in paramstodo: | |
| # Fill frame with electrical parameter information | |
| if 'method' in param: | |
| p = param['method'] | |
| puniq = p + '.0' | |
| if puniq in self.status: | |
| # This method was used before, so give it a unique identifier | |
| j = 1 | |
| while True: | |
| puniq = p + '.' + str(j) | |
| if puniq not in self.status: | |
| break | |
| else: | |
| j += 1 | |
| else: | |
| j = 0 | |
| paramtype = 'electrical' | |
| else: | |
| paramtype = 'physical' | |
| p = param['condition'] | |
| puniq = paramtype + '.' + p | |
| j = 0 | |
| if 'editable' in param and param['editable'] == True: | |
| normlabel = 'hlight.TLabel' | |
| redlabel = 'rhlight.TLabel' | |
| greenlabel = 'ghlight.TLabel' | |
| normbutton = 'hlight.TButton' | |
| redbutton = 'rhlight.TButton' | |
| greenbutton = 'ghlight.TButton' | |
| else: | |
| normlabel = 'normal.TLabel' | |
| redlabel = 'red.TLabel' | |
| greenlabel = 'green.TLabel' | |
| normbutton = 'normal.TButton' | |
| redbutton = 'red.TButton' | |
| greenbutton = 'green.TButton' | |
| if 'display' in param: | |
| dtext = param['display'] | |
| else: | |
| dtext = p | |
| # Special handling: Change LVS_errors to "device check" when using | |
| # schematic netlist. | |
| if paramtype == 'physical': | |
| if isschem: | |
| if p == 'LVS_errors': | |
| dtext = 'Invalid device check' | |
| dframe.description = ttk.Label(dframe, text = dtext, style = normlabel) | |
| dframe.description.grid(column = 0, row=n, sticky='ewns') | |
| dframe.method = ttk.Label(dframe, text = p, style = normlabel) | |
| dframe.method.grid(column = 1, row=n, sticky='ewns') | |
| if 'plot' in param: | |
| status_style = normlabel | |
| dframe.plots = ttk.Frame(dframe) | |
| dframe.plots.grid(column = 2, row=n, columnspan = 6, sticky='ewns') | |
| plotrec = param['plot'] | |
| if 'status' in plotrec: | |
| status_value = plotrec['status'] | |
| else: | |
| status_value = '(not checked)' | |
| dframe_plot = ttk.Label(dframe.plots, text=plotrec['filename'], | |
| style = normlabel) | |
| dframe_plot.grid(column = j, row = n, sticky='ewns') | |
| else: | |
| # For schematic capture, mark physical parameters that can't and won't be | |
| # checked as "not applicable". | |
| status_value = '(not checked)' | |
| if paramtype == 'physical': | |
| if isschem: | |
| if p == 'area' or p == 'width' or p == 'height' or p == 'DRC_errors': | |
| status_value = '(N/A)' | |
| if 'min' in param: | |
| status_style = normlabel | |
| pmin = param['min'] | |
| if 'target' in pmin: | |
| if 'unit' in param and not binrex.match(param['unit']): | |
| targettext = pmin['target'] + ' ' + param['unit'] | |
| else: | |
| targettext = pmin['target'] | |
| # Hack for use of min to change method of scoring | |
| if not 'penalty' in pmin or pmin['penalty'] != '0': | |
| dframe.min = ttk.Label(dframe, text=targettext, style = normlabel) | |
| else: | |
| dframe.min = ttk.Label(dframe, text='(no limit)', style = normlabel) | |
| else: | |
| dframe.min = ttk.Label(dframe, text='(no limit)', style = normlabel) | |
| if 'score' in pmin: | |
| if pmin['score'] != 'fail': | |
| status_style = greenlabel | |
| if status_value != 'fail': | |
| status_value = 'pass' | |
| else: | |
| status_style = redlabel | |
| status_value = 'fail' | |
| if 'value' in pmin: | |
| if 'unit' in param and not binrex.match(param['unit']): | |
| valuetext = pmin['value'] + ' ' + param['unit'] | |
| else: | |
| valuetext = pmin['value'] | |
| dframe.value = ttk.Label(dframe, text=valuetext, style=status_style) | |
| dframe.value.grid(column = 3, row=n, sticky='ewns') | |
| else: | |
| dframe.min = ttk.Label(dframe, text='(no limit)', style = normlabel) | |
| dframe.min.grid(column = 2, row=n, sticky='ewns') | |
| if 'typ' in param: | |
| status_style = normlabel | |
| ptyp = param['typ'] | |
| if 'target' in ptyp: | |
| if 'unit' in param and not binrex.match(param['unit']): | |
| targettext = ptyp['target'] + ' ' + param['unit'] | |
| else: | |
| targettext = ptyp['target'] | |
| dframe.typ = ttk.Label(dframe, text=targettext, style = normlabel) | |
| else: | |
| dframe.typ = ttk.Label(dframe, text='(no target)', style = normlabel) | |
| if 'score' in ptyp: | |
| # Note: You can't fail a "typ" score, but there is only one "Status", | |
| # so if it is a "fail", it must remain a "fail". | |
| if ptyp['score'] != 'fail': | |
| status_style = greenlabel | |
| if status_value != 'fail': | |
| status_value = 'pass' | |
| else: | |
| status_style = redlabel | |
| status_value = 'fail' | |
| if 'value' in ptyp: | |
| if 'unit' in param and not binrex.match(param['unit']): | |
| valuetext = ptyp['value'] + ' ' + param['unit'] | |
| else: | |
| valuetext = ptyp['value'] | |
| dframe.value = ttk.Label(dframe, text=valuetext, style=status_style) | |
| dframe.value.grid(column = 5, row=n, sticky='ewns') | |
| else: | |
| dframe.typ = ttk.Label(dframe, text='(no target)', style = normlabel) | |
| dframe.typ.grid(column = 4, row=n, sticky='ewns') | |
| if 'max' in param: | |
| status_style = normlabel | |
| pmax = param['max'] | |
| if 'target' in pmax: | |
| if 'unit' in param and not binrex.match(param['unit']): | |
| targettext = pmax['target'] + ' ' + param['unit'] | |
| else: | |
| targettext = pmax['target'] | |
| # Hack for use of max to change method of scoring | |
| if not 'penalty' in pmax or pmax['penalty'] != '0': | |
| dframe.max = ttk.Label(dframe, text=targettext, style = normlabel) | |
| else: | |
| dframe.max = ttk.Label(dframe, text='(no limit)', style = normlabel) | |
| else: | |
| dframe.max = ttk.Label(dframe, text='(no limit)', style = normlabel) | |
| if 'score' in pmax: | |
| if pmax['score'] != 'fail': | |
| status_style = greenlabel | |
| if status_value != 'fail': | |
| status_value = 'pass' | |
| else: | |
| status_style = redlabel | |
| status_value = 'fail' | |
| if 'value' in pmax: | |
| if 'unit' in param and not binrex.match(param['unit']): | |
| valuetext = pmax['value'] + ' ' + param['unit'] | |
| else: | |
| valuetext = pmax['value'] | |
| dframe.value = ttk.Label(dframe, text=valuetext, style=status_style) | |
| dframe.value.grid(column = 7, row=n, sticky='ewns') | |
| else: | |
| dframe.max = ttk.Label(dframe, text='(no limit)', style = normlabel) | |
| dframe.max.grid(column = 6, row=n, sticky='ewns') | |
| if paramtype == 'electrical': | |
| if 'hints' in param: | |
| simtext = '\u2022Simulate' | |
| else: | |
| simtext = 'Simulate' | |
| else: | |
| simtext = 'Check' | |
| simbutton = ttk.Menubutton(dframe, text=simtext, style = normbutton) | |
| # Generate pull-down menu on Simulate button. Most items apply | |
| # only to electrical parameters (at least for now) | |
| simmenu = tkinter.Menu(simbutton) | |
| simmenu.add_command(label='Run', | |
| command = lambda puniq=puniq: self.sim_param(puniq)) | |
| simmenu.add_command(label='Stop', command = self.stop_sims) | |
| if paramtype == 'electrical': | |
| simmenu.add_command(label='Hints', | |
| command = lambda param=param, simbutton=simbutton: self.add_hints(param, simbutton)) | |
| simmenu.add_command(label='Edit', | |
| command = lambda param=param: self.edit_param(param)) | |
| simmenu.add_command(label='Copy', | |
| command = lambda param=param: self.copy_param(param)) | |
| if 'editable' in param and param['editable'] == True: | |
| simmenu.add_command(label='Delete', | |
| command = lambda param=param: self.delete_param(param)) | |
| # Attach the menu to the button | |
| simbutton.config(menu=simmenu) | |
| # simbutton = ttk.Button(dframe, text=simtext, style = normbutton) | |
| # command = lambda puniq=puniq: self.sim_param(puniq)) | |
| simbutton.grid(column = 9, row=n, sticky='ewns') | |
| if paramtype == 'electrical': | |
| tooltip.ToolTip(simbutton, text = "Simulate one electrical parameter") | |
| else: | |
| tooltip.ToolTip(simbutton, text = "Check one physical parameter") | |
| # If 'pass', then just display message. If 'fail', then create a button that | |
| # opens and configures the failure report window. | |
| if status_value == '(not checked)': | |
| bstyle=normbutton | |
| stat_label = ttk.Label(dframe, text=status_value, style=bstyle) | |
| else: | |
| if status_value == 'fail': | |
| bstyle=redbutton | |
| else: | |
| bstyle=greenbutton | |
| if paramtype == 'electrical': | |
| stat_label = ttk.Button(dframe, text=status_value, style=bstyle, | |
| command = lambda param=param, globcond=globcond: | |
| self.failreport.display(param, globcond, | |
| self.cur_datasheet)) | |
| elif p == 'LVS_errors': | |
| dspath = os.path.split(self.cur_datasheet)[0] | |
| datasheet = os.path.split(self.cur_datasheet)[1] | |
| dsheet = self.datatop['data-sheet'] | |
| designname = dsheet['ip-name'] | |
| if self.origin.get() == 'Schematic Capture': | |
| lvs_file = dspath + '/mag/precheck.log' | |
| else: | |
| lvs_file = dspath + '/mag/comp.out' | |
| if not os.path.exists(lvs_file): | |
| if os.path.exists(dspath + '/mag/precheck.log'): | |
| lvs_file = dspath + '/mag/precheck.log' | |
| elif os.path.exists(dspath + '/mag/comp.out'): | |
| lvs_file = dspath + '/mag/comp.out' | |
| stat_label = ttk.Button(dframe, text=status_value, style=bstyle, | |
| command = lambda lvs_file=lvs_file: self.textreport.display(lvs_file)) | |
| else: | |
| stat_label = ttk.Label(dframe, text=status_value, style=bstyle) | |
| tooltip.ToolTip(stat_label, | |
| text = "Show detail view of simulation conditions and results") | |
| stat_label.grid(column = 8, row=n, sticky='ewns') | |
| self.status[puniq] = stat_label | |
| n += 1 | |
| for child in dframe.winfo_children(): | |
| child.grid_configure(ipadx = 5, ipady = 1, padx = 2, pady = 2) | |
| # Check if a design submission and characterization may be in progress. | |
| # If so, add the progress bar at the bottom. | |
| self.check_ongoing_upload() | |
| if __name__ == '__main__': | |
| faulthandler.register(signal.SIGUSR2) | |
| options = [] | |
| arguments = [] | |
| for item in sys.argv[1:]: | |
| if item.find('-', 0) == 0: | |
| options.append(item) | |
| else: | |
| arguments.append(item) | |
| root = tkinter.Tk() | |
| app = CACECharacterize(root) | |
| if arguments: | |
| print('Calling set_datasheet with argument ' + arguments[0]) | |
| app.set_datasheet(arguments[0]) | |
| root.mainloop() |