reverted sky130/makefile.in, renamed files starting with 'og,' replaced hard-coded path in create_project with PREFIX
diff --git a/common/cace.py b/common/cace.py
new file mode 100755
index 0000000..4443e75
--- /dev/null
+++ b/common/cace.py
@@ -0,0 +1,1781 @@
+#!/usr/bin/env python3 -B
+#
+#--------------------------------------------------------
+# Open Galaxy 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
+
+import config
+
+# User preferences file (if it exists)
+prefsfile = '~/design/.profile/prefs.json'
+
+#------------------------------------------------------
+# 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 OpenGalaxyCharacterize(ttk.Frame):
+ """Open Galaxy 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(config.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('Open Galaxy 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:
+ userid = os.environ['USER']
+ p = subprocess.run(['/ef/apps/bin/withnet',
+ config.apps_path + '/og_uid_service.py', userid],
+ stdout = subprocess.PIPE)
+ if p.stdout:
+ uid_string = p.stdout.splitlines()[0].decode('utf-8')
+ userspec = re.findall(r'[^"\s]\S*|".+?"', uid_string)
+ if len(userspec) > 0:
+ username = userspec[0].strip('"')
+ # Note userspec[1] = UID and userspec[2] = role, useful
+ # for future applications.
+ else:
+ username = userid
+ else:
+ username = userid
+
+ # 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(['/ef/apps/bin/withnet',
+ config.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 spi/ 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(['/ef/apps/bin/withnet',
+ config.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()
+ 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/char'
+ 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 + '/spi'
+
+ 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([config.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]
+ dsdir = dspath + '/ngspice/char'
+ 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/char'
+
+ 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/char'
+
+ 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 .spi 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
+
+ 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 + '/spi'
+ if self.origin.get() == 'Layout Extracted':
+ spifile = dsdir + '/pex/' + dsroot + '.spi'
+ savesuffix = 'lsave'
+ else:
+ spifile = dsdir + '/' + dsroot + '.spi'
+ savesuffix = 'save'
+
+ dsdir = dspath + '/ngspice/char'
+ 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 = OpenGalaxyCharacterize(root)
+ if arguments:
+ print('Calling set_datasheet with argument ' + arguments[0])
+ app.set_datasheet(arguments[0])
+
+ root.mainloop()