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()