Initial commit of public repository open_pdks.
diff --git a/common/soc_floorplanner.py b/common/soc_floorplanner.py
new file mode 100755
index 0000000..959a623
--- /dev/null
+++ b/common/soc_floorplanner.py
@@ -0,0 +1,2619 @@
+#!/usr/bin/env python3
+#
+#--------------------------------------------------------
+# Padframe Editor and Core Floorplanner
+#
+#--------------------------------------------------------
+# Written by Tim Edwards
+# efabless, inc.
+# April 24, 2019
+# Version 0.5
+# Based on https://github.com/YosysHQ/padring (requirement)
+# Update: May 9, 2019 to add console message window
+# Update: May 10, 2019 to incorporate core floorplanning
+# Update: Jan 31, 2020 to allow batch operation
+#--------------------------------------------------------
+
+import os
+import re
+import sys
+import glob
+import json
+import math
+import shutil
+import signal
+import select
+import subprocess
+import faulthandler
+
+import tkinter
+from tkinter import ttk
+from tkinter import filedialog
+import tksimpledialog
+from consoletext import ConsoleText
+
+# User preferences file (if it exists)
+prefsfile = '~/design/.profile/prefs.json'
+
+#------------------------------------------------------
+# Dialog for entering a pad
+#------------------------------------------------------
+
+class PadNameDialog(tksimpledialog.Dialog):
+    def body(self, master, warning=None, seed=None):
+        if warning:
+            ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns')
+        ttk.Label(master, text="Enter new group name:").grid(row = 1, column = 0, sticky = 'wns')
+        self.nentry = ttk.Entry(master)
+        self.nentry.grid(row = 1, column = 1, sticky = 'ewns')
+        if seed:
+            self.nentry.insert(0, seed)
+        return self.nentry # initial focus
+
+    def apply(self):
+        return self.nentry.get()
+
+#------------------------------------------------------
+# Dialog for entering core dimensions
+#------------------------------------------------------
+
+class CoreSizeDialog(tksimpledialog.Dialog):
+    def body(self, master, warning="Chip core dimensions", seed=None):
+        if warning:
+            ttk.Label(master, text=warning).grid(row = 0, columnspan = 2, sticky = 'wns')
+        ttk.Label(master, text="Enter core width x height (microns):").grid(row = 1, column = 0, sticky = 'wns')
+        self.nentry = ttk.Entry(master)
+        self.nentry.grid(row = 1, column = 1, sticky = 'ewns')
+
+
+        if seed:
+            self.nentry.insert(0, seed)
+        return self.nentry # initial focus
+
+    def apply(self):
+        return self.nentry.get()
+
+#------------------------------------------------
+# SoC Floorplanner and Padframe Generator GUI
+#------------------------------------------------
+
+class SoCFloorplanner(ttk.Frame):
+    """Open Galaxy Pad Frame Generator."""
+
+    def __init__(self, parent = None, *args, **kwargs):
+        '''See the __init__ for Tkinter.Toplevel.'''
+        ttk.Frame.__init__(self, parent, *args[1:], **kwargs)
+        self.root = parent
+        self.init_data()
+        if args[0] == True:
+            self.do_gui = True
+            self.init_gui()
+        else:
+            self.do_gui = False
+            self.use_console = False
+
+    def on_quit(self):
+        """Exits program."""
+        quit()
+
+    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('normal.TButton', font=('Helvetica', fontsize), border = 3, relief = 'raised')
+        s.configure('title.TLabel', font=('Helvetica', fontsize, 'bold italic'),
+                        foreground = 'brown', anchor = 'center')
+        s.configure('blue.TLabel', font=('Helvetica', fontsize), foreground = 'blue')
+        s.configure('normal.TLabel', font=('Helvetica', fontsize))
+        s.configure('normal.TCheckbutton', font=('Helvetica', fontsize))
+        s.configure('normal.TMenubutton', font=('Helvetica', fontsize))
+        s.configure('normal.TEntry', font=('Helvetica', fontsize), background='white')
+        s.configure('pad.TLabel', font=('Helvetica', fontsize), foreground = 'blue', relief = 'flat')
+        s.configure('select.TLabel', font=('Helvetica', fontsize, 'bold'), foreground = 'white',
+			background = 'blue', relief = 'flat')
+ 
+        # parent.withdraw()
+        self.root.title('Padframe Generator and Core Floorplanner')
+        self.root.option_add('*tearOff', 'FALSE')
+        self.pack(side = 'top', fill = 'both', expand = 'true')
+        self.root.protocol("WM_DELETE_WINDOW", self.on_quit)
+
+        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)
+
+        self.toppane.columnconfigure(0, weight = 1)
+        self.toppane.rowconfigure(0, weight = 1)
+        self.botpane.columnconfigure(0, weight = 1)
+        self.botpane.rowconfigure(0, weight = 1)
+
+        # Scrolled frame using canvas widget
+        self.pframe = tkinter.Frame(self.toppane)
+        self.pframe.grid(row = 0, column = 0, sticky = 'news')
+        self.pframe.rowconfigure(0, weight = 1)
+        self.pframe.columnconfigure(0, weight = 1)
+
+        # Add column on the left, listing all groups and the pads they belong to.
+        # This starts as just a frame to be filled.  Use a canvas to create a
+        # scrolled frame.
+
+        # The primary frame holds the canvas
+        self.canvas = tkinter.Canvas(self.pframe, background = "white")
+        self.canvas.grid(row = 0, column = 0, sticky = 'news')
+
+        # Add Y scrollbar to pad list window
+        xscrollbar = ttk.Scrollbar(self.pframe, orient = 'horizontal')
+        xscrollbar.grid(row = 1, column = 0, sticky = 'news')
+        yscrollbar = ttk.Scrollbar(self.pframe, orient = 'vertical')
+        yscrollbar.grid(row = 0, column = 1, sticky = 'news')
+
+        self.canvas.config(xscrollcommand = xscrollbar.set)
+        xscrollbar.config(command = self.canvas.xview)
+        self.canvas.config(yscrollcommand = yscrollbar.set)
+        yscrollbar.config(command = self.canvas.yview)
+
+        self.canvas.bind("<Button-4>", self.on_scrollwheel)
+        self.canvas.bind("<Button-5>", self.on_scrollwheel)
+
+        # Configure callback
+        self.canvas.bind("<Configure>", self.frame_configure)
+
+        # Add a text window to capture output.  Redirect print statements to it.
+        self.console = ttk.Frame(self.botpane)
+        self.console.grid(column = 0, row = 0, sticky = "news")
+        self.text_box = ConsoleText(self.console, wrap='word', height = 4)
+        self.text_box.pack(side='left', fill='both', expand='true')
+        console_scrollbar = ttk.Scrollbar(self.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 the bottom bar with buttons
+        self.bbar = ttk.Frame(self.botpane)
+        self.bbar.grid(column = 0, row = 1, sticky = "news")
+
+        self.bbar.import_button = ttk.Button(self.bbar, text='Import',
+		command=self.vlogimport, style='normal.TButton')
+        self.bbar.import_button.grid(column=0, row=0, padx = 5)
+
+        self.bbar.generate_button = ttk.Button(self.bbar, text='Generate',
+		command=self.generate, style='normal.TButton')
+        self.bbar.generate_button.grid(column=1, row=0, padx = 5)
+
+        self.bbar.save_button = ttk.Button(self.bbar, text='Save',
+		command=self.save, style='normal.TButton')
+        self.bbar.save_button.grid(column=2, row=0, padx = 5)
+
+        self.bbar.cancel_button = ttk.Button(self.bbar, text='Quit',
+		command=self.on_quit, style='normal.TButton')
+        self.bbar.cancel_button.grid(column=3, row=0, padx = 5)
+
+        pane.add(self.toppane)
+        pane.add(self.botpane)
+        pane.paneconfig(self.toppane, stretch='first')
+
+    def init_data(self):
+
+        self.vlogpads = []
+        self.corecells = []
+        self.Npads = []
+        self.Spads = []
+        self.Epads = []
+        self.Wpads = []
+        self.NEpad = []
+        self.NWpad = []
+        self.SEpad = []
+        self.SWpad = []
+        self.coregroup = []
+
+        self.celldefs = []
+        self.coredefs = []
+        self.selected = []
+        self.ioleflibs = []
+        self.llx = 0
+        self.lly = 0
+        self.urx = 0
+        self.ury = 0
+
+        self.event_data = {}
+        self.event_data['x0'] = 0
+        self.event_data['y0'] = 0
+        self.event_data['x'] = 0
+        self.event_data['y'] = 0
+        self.event_data['tag'] = None
+        self.scale = 1.0
+        self.margin = 10
+        self.pad_rotation = 0
+
+        self.init_messages = []
+        self.stdout = None
+        self.stderr = None
+
+        self.keep_cfg = False
+        self.ef_format = False
+        self.use_console = False
+
+    def init_padframe(self):
+        self.set_project()
+        self.vlogimport()
+        self.readplacement(precheck=True)
+        self.resolve()
+        self.generate(0)
+
+    # Local routines for handling printing to the text console
+
+    def print(self, message, file=None, end='\n', flush=True):
+        if not file:
+            if not self.use_console:
+                file = sys.stdout
+            else:
+                file = ConsoleText.StdoutRedirector(self.text_box)
+        if self.stdout:
+            print(message, file=file, end=end)
+            if flush:
+                self.stdout.flush()
+                self.update_idletasks()
+        else:
+            self.init_messages.append(message)
+
+    def text_to_console(self):
+        # 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
+        if self.use_console:
+            sys.stdout = ConsoleText.StdoutRedirector(self.text_box)
+            sys.stderr = ConsoleText.StderrRedirector(self.text_box)
+
+        if len(self.init_messages) > 0:
+            for message in self.init_messages:
+                self.print(message)
+            self.init_messages = []
+
+    # Set the project name(s).  This is the name of the top-level verilog.
+    # The standard protocol is that the project directory contains a file
+    # project.json that defines a name 'ip-name' that is the same as the
+    # layout name, the verilog module name, etc.  
+
+    def set_project(self):
+        # Check pwd
+        pwdname = self.projectpath if self.projectpath else os.getcwd()
+        
+        subdir = os.path.split(pwdname)[1]
+        if subdir == 'mag' or subdir == 'verilog':
+            projectpath = os.path.split(pwdname)[0]
+        else:
+            projectpath = pwdname
+
+        projectroot = os.path.split(projectpath)[0]
+        projectdirname = os.path.split(projectpath)[1]
+
+        # Check for efabless format.  This is probably more complicated than
+        # it deserves to be.  Option -ef_format is the best way to specify
+        # efabless format.  However, if it is not specified, then check the
+        # technology PDK directory and the project directory for the tell-tale
+        # ".ef-config" (efabless format) directory vs. ".config" (not efabless
+        # format).
+
+        if not self.ef_format:
+            if os.path.exists(projectpath + '/.ef-config'):
+                self.ef_format = True
+            elif self.techpath:
+                if os.path.exists(self.techpath + '/.ef-config'):
+                    self.ef_format = True
+        else:
+            # Do a quick consistency check.  Honor the -ef_format option but warn if
+            # there is an apparent inconsistency.
+            if os.path.exists(projectpath + '/.config'):
+                self.print('Warning:  -ef_format used in apparently non-efabless setup.')
+            elif self.techpath:
+                if os.path.exists(self.techpath + '/.config'):
+                    self.print('Warning:  -ef_format used in apparently non-efabless setup.')
+
+        # Check for project.json
+
+        jsonname = None
+        if os.path.isfile(projectpath + '/project.json'):
+            jsonname = projectpath + '/project.json'
+        elif os.path.isfile(projectroot + '/' + projectdirname + '.json'):
+            jsonname = projectroot + '/' + projectdirname + '.json'
+        if os.path.isfile(projectroot + '/project.json'):
+            # Just in case this was started from some other subdirectory
+            projectpath = projectroot
+            jsonname = projectroot + '/project.json'
+
+        if jsonname:
+            self.print('Reading project JSON file ' + jsonname)
+            with open(jsonname, 'r') as ifile:
+                topdata = json.load(ifile)
+                if 'data-sheet' in topdata:
+                    dsheet = topdata['data-sheet']
+                    if 'ip-name' in dsheet:
+                        self.project = dsheet['ip-name']
+                        self.projectpath = projectpath
+        else:
+            self.print('No project JSON file; using directory name as the project name.')
+            self.project = os.path.split(projectpath)[1]
+            self.projectpath = projectpath
+
+        self.print('Project name is ' + self.project + ' (' + self.projectpath + ')')
+
+    # Functions for drag-and-drop capability
+    def add_draggable(self, tag):
+        self.canvas.tag_bind(tag, '<ButtonPress-1>', self.on_button_press)
+        self.canvas.tag_bind(tag, '<ButtonRelease-1>', self.on_button_release)
+        self.canvas.tag_bind(tag, '<B1-Motion>', self.on_button_motion)
+        self.canvas.tag_bind(tag, '<ButtonPress-2>', self.on_button2_press)
+        self.canvas.tag_bind(tag, '<ButtonPress-3>', self.on_button3_press)
+
+    def on_button_press(self, event):
+        '''Begining drag of an object'''
+        # Find the closest item, then record its tag.
+        locx = event.x + self.canvas.canvasx(0)
+        locy = event.y + self.canvas.canvasy(0)
+        item = self.canvas.find_closest(locx, locy)[0]
+        self.event_data['tag'] = self.canvas.gettags(item)[0]
+        self.event_data['x0'] = event.x
+        self.event_data['y0'] = event.y
+        self.event_data['x'] = event.x
+        self.event_data['y'] = event.y
+
+    def on_button2_press(self, event):
+        '''Flip an object (excluding corners)'''
+        locx = event.x + self.canvas.canvasx(0)
+        locy = event.y + self.canvas.canvasy(0)
+        item = self.canvas.find_closest(locx, locy)[0]
+        tag = self.canvas.gettags(item)[0]
+
+        try:
+            corecell = next(item for item in self.coregroup if item['name'] == tag)
+        except:
+            try:
+                pad = next(item for item in self.Npads if item['name'] == tag)
+            except:
+                pad = None
+            if not pad:
+                try:
+                    pad = next(item for item in self.Epads if item['name'] == tag)
+                except:
+                    pad = None
+            if not pad:
+                try:
+                    pad = next(item for item in self.Spads if item['name'] == tag)
+                except:
+                    pad = None
+            if not pad:
+                try:
+                    pad = next(item for item in self.Wpads if item['name'] == tag)
+                except:
+                    pad = None
+            if not pad:
+                self.print('Error: Object cannot be flipped.')
+            else:
+                # Flip the pad (in the only way meaningful for the pad).
+                orient = pad['o']
+                if orient == 'N':
+                    pad['o'] = 'FN'
+                elif orient == 'E':
+                    pad['o'] = 'FW'
+                elif orient == 'S':
+                    pad['o'] = 'FS'
+                elif orient == 'W':
+                    pad['o'] = 'FE'
+                elif orient == 'FN':
+                    pad['o'] = 'N'
+                elif orient == 'FE':
+                    pad['o'] = 'W'
+                elif orient == 'FS':
+                    pad['o'] = 'S'
+                elif orient == 'FW':
+                    pad['o'] = 'E'
+        else:
+            # Flip the cell.  Use the DEF meaning of flip, which is to
+            # add or subtract 'F' from the orientation.
+            orient = corecell['o']
+            if not 'F' in orient:
+                corecell['o'] = 'F' + orient
+            else:
+                corecell['o'] = orient[1:]
+
+        # Redraw
+        self.populate(0)
+
+    def on_button3_press(self, event):
+        '''Rotate a core object (no pads) '''
+        locx = event.x + self.canvas.canvasx(0)
+        locy = event.y + self.canvas.canvasy(0)
+        item = self.canvas.find_closest(locx, locy)[0]
+        tag = self.canvas.gettags(item)[0]
+
+        try:
+            corecell = next(item for item in self.coregroup if item['name'] == tag)
+        except:
+            self.print('Error: Object cannot be rotated.')
+        else:
+            # Modify its orientation
+            orient = corecell['o']
+            if orient == 'N':
+                corecell['o'] = 'E'
+            elif orient == 'E':
+                corecell['o'] = 'S'
+            elif orient == 'S':
+                corecell['o'] = 'W'
+            elif orient == 'W':
+                corecell['o'] = 'N'
+            elif orient == 'FN':
+                corecell['o'] = 'FW'
+            elif orient == 'FW':
+                corecell['o'] = 'FS'
+            elif orient == 'FS':
+                corecell['o'] = 'FE'
+            elif orient == 'FE':
+                corecell['o'] = 'FN'
+
+            # rewrite the core DEF file
+            self.write_core_def()
+
+        # Redraw
+        self.populate(0)
+
+    def on_button_motion(self, event):
+        '''Handle dragging of an object'''
+        # compute how much the mouse has moved
+        delta_x = event.x - self.event_data['x']
+        delta_y = event.y - self.event_data['y']
+        # move the object the appropriate amount
+        self.canvas.move(self.event_data['tag'], delta_x, delta_y)
+        # record the new position
+        self.event_data['x'] = event.x
+        self.event_data['y'] = event.y
+
+    def on_button_release(self, event):
+        '''End drag of an object'''
+
+        # Find the pad associated with the tag and update its position information
+        tag = self.event_data['tag']
+
+        # Collect pads in clockwise order.  Note that E and S rows are not clockwise
+        allpads = []
+        allpads.extend(self.Npads)
+        allpads.extend(self.NEpad)
+        allpads.extend(reversed(self.Epads))
+        allpads.extend(self.SEpad)
+        allpads.extend(reversed(self.Spads))
+        allpads.extend(self.SWpad)
+        allpads.extend(self.Wpads)
+        allpads.extend(self.NWpad)
+
+        # Create a list of row references (also in clockwise order, but no reversing)
+        padrows = [self.Npads, self.NEpad, self.Epads, self.SEpad, self.Spads, self.SWpad, self.Wpads, self.NWpad]
+
+        # Record the row or corner where this pad was located before the move
+        for row in padrows:
+            try:
+                pad = next(item for item in row if item['name'] == tag)
+            except:
+                pass
+            else:
+                padrow = row
+                break
+
+        # Currently there is no procedure to move a pad out of the corner
+        # position;  corners are fixed by definition.
+        if padrow == self.NEpad or padrow == self.SEpad or padrow == self.SWpad or padrow == self.NWpad:
+            # Easier to run generate() than to put the pad back. . .
+            self.generate(0)
+            return
+
+        # Find the original center point of the pad being moved
+
+        padllx = pad['x']
+        padlly = pad['y']
+        if pad['o'] == 'N' or pad['o'] == 'S':
+            padurx = padllx + pad['width']
+            padury = padlly + pad['height']
+        else:
+            padurx = padllx + pad['height']
+            padury = padlly + pad['width']
+        padcx = (padllx + padurx) / 2
+        padcy = (padlly + padury) / 2
+
+        # Add distance from drag information (note that drag position in y
+        # is negative relative to the chip dimensions)
+        padcx += (self.event_data['x'] - self.event_data['x0']) / self.scale
+        padcy -= (self.event_data['y'] - self.event_data['y0']) / self.scale
+
+        # reset the drag information
+        self.event_data['tag'] = None
+        self.event_data['x'] = 0
+        self.event_data['y'] = 0
+        self.event_data['x0'] = 0
+        self.event_data['y0'] = 0
+
+        # Find the distance from the pad to all other pads, and get the two
+        # closest entries.
+
+        wwidth = self.urx - self.llx
+        dist0 = wwidth
+        dist1 = wwidth
+        pad0 = None
+        pad1 = None
+          
+        for npad in allpads:
+            if npad == pad:
+                continue
+
+            npadllx = npad['x']
+            npadlly = npad['y']
+            if npad['o'] == 'N' or npad['o'] == 'S':
+                npadurx = npadllx + npad['width']
+                npadury = npadlly + npad['height']
+            else:
+                npadurx = npadllx + npad['height']
+                npadury = npadlly + npad['width']
+            npadcx = (npadllx + npadurx) / 2
+            npadcy = (npadlly + npadury) / 2
+
+            deltx = npadcx - padcx
+            delty = npadcy - padcy
+            pdist = math.sqrt(deltx * deltx + delty * delty)
+            if pdist < dist0:
+                dist1 = dist0
+                pad1 = pad0
+                dist0 = pdist
+                pad0 = npad
+
+            elif pdist < dist1:
+                dist1 = pdist
+                pad1 = npad
+
+        # Diagnostic
+        # self.print('Two closest pads to pad ' + pad['name'] + ' (' + pad['cell'] + '): ')
+        # self.print(pad0['name'] + ' (' + pad0['cell'] + ') dist = ' + str(dist0))
+        # self.print(pad1['name'] + ' (' + pad1['cell'] + ') dist = ' + str(dist1))
+
+        # Record the row or corner where these pads are
+        for row in padrows:
+            try:
+                testpad = next(item for item in row if item['name'] == pad0['name'])
+            except:
+                pass
+            else:
+                padrow0 = row
+                break
+
+        for row in padrows:
+            try:
+                testpad = next(item for item in row if item['name'] == pad1['name'])
+            except:
+                pass
+            else:
+                padrow1 = row
+                break
+
+        # Remove pad from its own row
+        padrow.remove(pad)
+
+        # Insert pad into new row.  Watch for wraparound from the last entry to the first
+        padidx0 = allpads.index(pad0)
+        padidx1 = allpads.index(pad1)
+        if padidx0 == 0 and padidx1 > 2:
+            padidx1 = -1
+
+        if padidx1 > padidx0:
+            padafter = pad1
+            rowafter = padrow1
+            padbefore = pad0
+            rowbefore = padrow0
+        else:
+            padafter = pad0
+            rowafter = padrow0
+            padbefore = pad1
+            rowbefore = padrow1
+
+        # Do not replace corner positions (? may be necessary ?)
+        if rowafter == self.NWpad:
+            self.Wpads.append(pad)
+        elif rowafter == self.NWpad:
+            self.Npads.append(pad)
+        elif rowafter == self.SEpad:
+            self.Epads.insert(0, pad)
+        elif rowafter == self.SWpad:
+            self.Spads.insert(0, pad)
+        elif rowafter == self.Wpads or rowafter == self.Npads:
+            idx = rowafter.index(padafter)
+            rowafter.insert(idx, pad)
+        elif rowbefore == self.NEpad:
+            self.Epads.append(pad)
+        elif rowbefore == self.SEpad:
+            self.Spads.append(pad)
+        else:
+            # rows E and S are ordered counterclockwise
+            idx = rowbefore.index(padbefore)
+            rowbefore.insert(idx, pad)
+
+        # Re-run padring
+        self.generate(0)
+
+    def on_scrollwheel(self, event):
+        if event.num == 4:
+            zoomval = 1.1;
+        elif event.num == 5:
+            zoomval = 0.9;
+        else:
+            zoomval = 1.0;
+
+        self.scale *= zoomval
+        self.canvas.scale('all', -15 * zoomval, -15 * zoomval, zoomval, zoomval)
+        self.event_data['x'] *= zoomval
+        self.event_data['y'] *= zoomval
+        self.event_data['x0'] *= zoomval
+        self.event_data['y0'] *= zoomval
+        self.frame_configure(event)
+
+    # Callback functions similar to the pad event callbacks above, but for
+    # core cells.  Unlike pad cells, core cells can be rotated and flipped
+    # arbitrarily, and they do not force a recomputation of the padframe
+    # unless their position forces the padframe to expand
+
+    def add_core_draggable(self, tag):
+        self.canvas.tag_bind(tag, '<ButtonPress-1>', self.on_button_press)
+        self.canvas.tag_bind(tag, '<ButtonRelease-1>', self.core_on_button_release)
+        self.canvas.tag_bind(tag, '<B1-Motion>', self.on_button_motion)
+        self.canvas.tag_bind(tag, '<ButtonPress-2>', self.on_button2_press)
+        self.canvas.tag_bind(tag, '<ButtonPress-3>', self.on_button3_press)
+
+    def core_on_button_release(self, event):
+        '''End drag of a core cell'''
+
+        # Find the pad associated with the tag and update its position information
+        tag = self.event_data['tag']
+
+        try:
+            corecell = next(item for item in self.coregroup if item['name'] == tag)
+        except:
+            self.print('Error: cell ' + item['name'] + ' is not in coregroup!')
+        else:  
+            # Modify its position values
+            corex = corecell['x']
+            corey = corecell['y']
+
+            # Add distance from drag information (note that drag position in y
+            # is negative relative to the chip dimensions)
+            deltax = (self.event_data['x'] - self.event_data['x0']) / self.scale
+            deltay = (self.event_data['y'] - self.event_data['y0']) / self.scale
+
+            corecell['x'] = corex + deltax
+            corecell['y'] = corey - deltay
+
+            # rewrite the core DEF file
+            self.write_core_def()
+
+        # reset the drag information
+        self.event_data['tag'] = None
+        self.event_data['x'] = 0
+        self.event_data['y'] = 0
+        self.event_data['x0'] = 0
+        self.event_data['y0'] = 0
+
+    # Critically needed or else frame does not resize to scrollbars!
+    def grid_configure(self, padx, pady):
+        pass
+
+    # Redraw the chip frame view in response to changes in the pad list
+    def redraw_frame(self):
+        self.canvas.coords('boundary', self.llx, self.urx, self.lly, self.ury)
+   
+    # Update the canvas scrollregion to incorporate all the interior windows
+    def frame_configure(self, event):
+        if self.do_gui == False:
+            return
+        self.update_idletasks()
+        bbox = self.canvas.bbox("all")
+        try:
+            newbbox = (-15, -15, bbox[2] + 15, bbox[3] + 15)
+        except:
+            pass
+        else:
+            self.canvas.configure(scrollregion = newbbox)
+
+    # Fill the GUI entries with resident data
+    def populate(self, level):
+        if self.do_gui == False:
+            return
+
+        if level > 1:
+            self.print('Recursion error:  Returning now.')
+            return
+
+        self.print('Populating floorplan view.')
+
+        # Remove all entries from the canvas
+        self.canvas.delete('all')
+
+        allpads = self.Npads + self.NEpad + self.Epads + self.SEpad + self.Spads + self.SWpad + self.Wpads + self.NWpad
+
+        notfoundlist = []
+
+        for pad in allpads:
+            if 'x' not in pad:
+                self.print('Error:  Pad ' + pad['name'] + ' has no placement information.')
+                continue
+            llx = int(pad['x'])
+            lly = int(pad['y'])
+            pado = pad['o']
+            try:
+                padcell = next(item for item in self.celldefs if item['name'] == pad['cell'])
+            except:
+                # This should not happen (failsafe)
+                if pad['cell'] not in notfoundlist:
+                    self.print('Warning:  there is no cell named ' + pad['cell'] + ' in the libraries.')
+                notfoundlist.append(pad['cell'])
+                continue
+            padw = padcell['width']
+            padh = padcell['height']
+            if 'N' in pado or 'S' in pado:
+                urx = int(llx + padw)
+                ury = int(lly + padh)
+            else:
+                urx = int(llx + padh)
+                ury = int(lly + padw)
+            
+            pad['llx'] = llx
+            pad['lly'] = lly
+            pad['urx'] = urx
+            pad['ury'] = ury
+
+        # Note that the DEF coordinate system is reversed in Y from the canvas. . .
+
+        height = self.ury - self.lly
+        for pad in allpads:
+
+            llx = pad['llx']
+            lly = height - pad['lly']
+            urx = pad['urx']
+            ury = height - pad['ury']
+
+            tag_id = pad['name']
+            if 'subclass' in pad:
+                if pad['subclass'] == 'POWER':
+                    pad_color = 'orange2'
+                elif pad['subclass'] == 'INOUT':
+                    pad_color = 'yellow'
+                elif pad['subclass'] == 'OUTPUT':
+                    pad_color = 'powder blue'
+                elif pad['subclass'] == 'INPUT':
+                    pad_color = 'goldenrod1'
+                elif pad['subclass'] == 'SPACER':
+                    pad_color = 'green yellow'
+                elif pad['class'] == 'ENDCAP':
+                    pad_color = 'green yellow'
+                elif pad['subclass'] == '' or pad['class'] == ';':
+                    pad_color = 'khaki1'
+                else:
+                    self.print('Unhandled pad class ' + pad['class'])
+                    pad_color = 'gray'
+            else:
+                 pad_color = 'gray'
+
+            sllx = self.scale * llx
+            slly = self.scale * lly
+            surx = self.scale * urx
+            sury = self.scale * ury
+
+            self.canvas.create_rectangle((sllx, slly), (surx, sury), fill=pad_color, tags=[tag_id])
+            cx = (sllx + surx) / 2
+            cy = (slly + sury) / 2
+
+            s = 10 if pad['width'] >= 10 else pad['width']
+            if pad['height'] < s:
+                s = pad['height']
+
+            # Create an indicator line at the bottom left corner of the cell
+            if pad['o'] == 'N':
+                allx = sllx
+                ally = slly - s
+                aurx = sllx + s
+                aury = slly
+            elif pad['o'] == 'E':
+                allx = sllx
+                ally = sury + s
+                aurx = sllx + s
+                aury = sury
+            elif pad['o'] == 'S':
+                allx = surx
+                ally = sury + s
+                aurx = surx - s
+                aury = sury
+            elif pad['o'] == 'W':
+                allx = surx
+                ally = slly - s
+                aurx = surx - s
+                aury = slly
+            elif pad['o'] == 'FN':
+                allx = surx
+                ally = slly - s
+                aurx = surx - s
+                aury = slly
+            elif pad['o'] == 'FE':
+                allx = surx
+                ally = sury + s
+                aurx = surx - s
+                aury = sury
+            elif pad['o'] == 'FS':
+                allx = sllx
+                ally = sury + s
+                aurx = sllx + s
+                aury = sury
+            elif pad['o'] == 'FW':
+                allx = sllx
+                ally = slly - s
+                aurx = sllx + s
+                aury = slly
+            self.canvas.create_line((allx, ally), (aurx, aury), tags=[tag_id])
+ 
+            # Rotate text on top and bottom rows if the tkinter version allows it.
+            if tkinter.TclVersion >= 8.6:
+                if pad['o'] == 'N' or pad['o'] == 'S':
+                    angle = 90
+                else:
+                    angle = 0
+                self.canvas.create_text((cx, cy), text=pad['name'], angle=angle, tags=[tag_id])
+            else:
+                self.canvas.create_text((cx, cy), text=pad['name'], tags=[tag_id])
+
+            # Make the pad draggable
+            self.add_draggable(tag_id)
+
+        # Now add the core cells
+        for cell in self.coregroup:
+            if 'x' not in cell:
+                self.print('Error:  Core cell ' + cell['name'] + ' has no placement information.')
+                continue
+            # else:
+            #     self.print('Diagnostic:  Creating object for core cell ' + cell['name'])
+            llx = int(cell['x'])
+            lly = int(cell['y'])
+            cello = cell['o']
+            try:
+                corecell = next(item for item in self.coredefs if item['name'] == cell['cell'])
+            except:
+                # This should not happen (failsafe)
+                if cell['cell'] not in notfoundlist:
+                    self.print('Warning:  there is no cell named ' + cell['cell'] + ' in the libraries.')
+                notfoundlist.append(cell['cell'])
+                continue
+            cellw = corecell['width']
+            cellh = corecell['height']
+            if 'N' in cello or 'S' in cello:
+                urx = int(llx + cellw)
+                ury = int(lly + cellh)
+            else:
+                urx = int(llx + cellh)
+                ury = int(lly + cellw)
+                print('NOTE: cell ' + corecell['name'] + ' is rotated, w = ' + str(urx - llx) + '; h = ' + str(ury - lly))
+
+            cell['llx'] = llx
+            cell['lly'] = lly
+            cell['urx'] = urx
+            cell['ury'] = ury
+
+        # Watch for out-of-window position in core cells.
+        corellx = self.llx
+        corelly = self.lly
+        coreurx = self.urx
+        coreury = self.ury
+
+        for cell in self.coregroup:
+
+            if 'llx' not in cell:
+                # Error message for this was handled above
+                continue
+
+            llx = cell['llx']
+            lly = height - cell['lly']
+            urx = cell['urx']
+            ury = height - cell['ury']
+
+            # Check for out-of-window cell
+            if llx < corellx:
+                corellx = llx
+            if lly < corelly:
+                corelly = lly
+            if urx > coreurx:
+                coreurx = urx
+            if ury > coreury:
+                coreury = ury
+
+            tag_id = cell['name']
+            cell_color = 'gray40'
+
+            sllx = self.scale * llx
+            slly = self.scale * lly
+            surx = self.scale * urx
+            sury = self.scale * ury
+
+            self.canvas.create_rectangle((sllx, slly), (surx, sury), fill=cell_color, tags=[tag_id])
+            cx = (sllx + surx) / 2
+            cy = (slly + sury) / 2
+
+            s = 10 if cell['width'] >= 10 else cell['width']
+            if cell['height'] < s:
+                s = cell['height']
+
+            # Create an indicator line at the bottom left corner of the cell
+            if cell['o'] == 'N':
+                allx = sllx
+                ally = slly - s
+                aurx = sllx + s
+                aury = slly
+            elif cell['o'] == 'E':
+                allx = sllx
+                ally = sury + s
+                aurx = sllx + s
+                aury = sury
+            elif cell['o'] == 'S':
+                allx = surx
+                ally = sury + s
+                aurx = surx - s
+                aury = sury
+            elif cell['o'] == 'W':
+                allx = surx
+                ally = slly - s
+                aurx = surx - s
+                aury = slly
+            elif cell['o'] == 'FN':
+                allx = surx
+                ally = slly - s
+                aurx = surx - s
+                aury = slly
+            elif cell['o'] == 'FE':
+                allx = surx
+                ally = sury + s
+                aurx = surx - s
+                aury = sury
+            elif cell['o'] == 'FS':
+                allx = sllx
+                ally = sury + s
+                aurx = sllx + s
+                aury = sury
+            elif cell['o'] == 'FW':
+                allx = sllx
+                ally = slly - s
+                aurx = sllx + s
+                aury = slly
+            self.canvas.create_line((allx, ally), (aurx, aury), tags=[tag_id])
+ 
+            # self.print('Created entry for cell ' + cell['name'] + ' at {0:g}, {1:g}'.format(cx, cy))
+ 
+            # Rotate text on top and bottom rows if the tkinter version allows it.
+            if tkinter.TclVersion >= 8.6:
+                if 'N' in cell['o'] or 'S' in cell['o']:
+                    angle = 90
+                else:
+                    angle = 0
+                self.canvas.create_text((cx, cy), text=cell['name'], angle=angle, tags=[tag_id])
+            else:
+                self.canvas.create_text((cx, cy), text=cell['name'], tags=[tag_id])
+
+            # Make the core cell draggable
+            self.add_core_draggable(tag_id)
+
+        # Is there a boundary size defined?
+        if self.urx > self.llx and self.ury > self.lly:
+            self.create_boundary()
+
+        # Did the core extend into negative X or Y?  If so, adjust all canvas
+        # coordinates to fit in the window, or else objects cannot be reached
+        # even by zooming out (since zooming is pinned on the top corner).
+
+        offsetx = 0
+        offsety = 0
+
+        # NOTE:  Probably want to check if the core exceeds the inner
+        # dimension of the pad ring, not the outer (to check and to do).
+
+        if corellx < self.llx:
+            offsetx = self.llx - corellx 
+        if corelly < self.lly:
+            offsety = self.lly - corelly
+        if offsetx > 0 or offsety > 0:
+            self.canvas.move("all", offsetx, offsety)
+            # An offset implies that the chip is core limited, and the
+            # padframe requires additional space.  This can be accomplished
+            # simply by running "Generate".  NOTE:  Since generate() calls
+            # populate(), be VERY SURE that this does not infinitely recurse!
+            self.generate(level)
+
+    # Generate a DEF file of the core area
+
+    def write_core_def(self):
+        self.print('Writing core placementment information in DEF file "core.def".')
+
+        mag_path = self.projectpath + '/mag'
+
+        # The core cells must always clear the I/O pads on the left and
+        # bottom (with the ad-hoc margin of self.margin).  If core cells have
+        # been moved to the left or down past the padframe edge, then the
+        # entire core needs to be repositioned.
+
+        # To be done:  draw a boundary around the core, let the edges of that
+        # boundary be draggable, and let the difference between the boundary
+        # and the core area define the margin.
+
+        if self.SWpad != []:
+            corellx = self.SWpad[0]['x'] + self.SWpad[0]['width'] + self.margin
+            corelly = self.SWpad[0]['y'] + self.SWpad[0]['height'] + self.margin
+        else:
+            corellx = self.Wpads[0]['x'] + self.Wpads[0]['height'] + self.margin
+            corelly = self.Spads[0]['x'] + self.Spads[0]['height'] + self.margin
+
+        offsetx = 0
+        offsety = 0
+        for corecell in self.coregroup:
+            if corecell['x'] < corellx:
+                if corellx - corecell['x'] > offsetx:
+                    offsetx = corellx - corecell['x']
+            if corecell['y'] < corelly:
+                if corelly - corecell['y'] > offsety:
+                    offsety = corelly - corecell['y']
+        if offsetx > 0 or offsety > 0:
+            for corecell in self.coregroup:
+                corecell['x'] += offsetx
+                corecell['y'] += offsety
+
+        # Now write the core DEF file
+
+        with open(mag_path + '/core.def', 'w') as ofile:
+            print('DESIGN CORE ;', file=ofile)
+            print('UNITS DISTANCE MICRONS 1000 ;', file=ofile)
+            print('COMPONENTS {0:d} ;'.format(len(self.coregroup)), file=ofile)
+            for corecell in self.coregroup:
+                print('  - ' + corecell['name'] + ' ' + corecell['cell'], file=ofile)
+                print('    + PLACED ( {0:d} {1:d} ) {2:s} ;'.format(int(corecell['x'] * 1000), int(corecell['y'] * 1000), corecell['o']), file=ofile)
+            print ('END COMPONENTS', file=ofile)
+            print ('END DESIGN', file=ofile)
+
+    # Create the chip boundary area
+
+    def create_boundary(self):
+        scale = self.scale
+        llx = (self.llx - 10) * scale
+        lly = (self.lly - 10) * scale
+        urx = (self.urx + 10) * scale
+        ury = (self.ury + 10) * scale
+
+        pad_color = 'plum1'
+        tag_id = 'boundary'
+        self.canvas.create_rectangle((llx, lly), (urx, ury), outline=pad_color, width=2, tags=[tag_id])
+        # Add text to the middle representing the chip and core areas
+        cx = ((self.llx + self.urx) / 2) * scale
+        cy = ((self.lly + self.ury) / 2) * scale
+        width = self.urx - self.llx
+        height = self.ury - self.lly
+        areatext = 'Chip dimensions (um): {0:g} x {1:g}'.format(width, height)
+        tag_id = 'chiparea' 
+        self.canvas.create_text((cx, cy), text=areatext, tags=[tag_id])
+
+    # Rotate orientation according to self.pad_rotation. 
+
+    def rotate_orientation(self, orient_in):
+        orient_v = ['N', 'E', 'S', 'W', 'N', 'E', 'S', 'W']
+        idxadd = int(self.pad_rotation / 90)
+        idx = orient_v.index(orient_in)
+        return orient_v[idx + idxadd]
+
+    # Read a list of cell macros (name, size, class) from a LEF library
+
+    def read_lef_macros(self, libpath, libname = None, libtype = 'iolib'):
+        if libtype == 'iolib':
+            libtext = 'I/O '
+        elif libtype == 'celllib':
+            libtext = 'core '
+        else:
+            libtext = ''
+
+        macros = []
+
+        if libname:
+            if os.path.splitext(libname)[1] == '':
+                libname += '.lef'
+            leffiles = glob.glob(libpath + '/' + libname)
+        else:
+            leffiles = glob.glob(libpath + '/*.lef')
+        if leffiles == []:
+            if libname:
+                self.print('WARNING:  No file ' + libpath + '/' + libname + '.lef')
+            else:
+                self.print('WARNING:  No files ' + libpath + '/*.lef')
+        for leffile in leffiles:
+            libpath = os.path.split(leffile)[0]
+            libname = os.path.split(libpath)[1]
+            self.print('Reading LEF ' + libtext + 'library ' + leffile)
+            with open(leffile, 'r') as ifile:
+                ilines = ifile.read().splitlines()
+                in_macro = False
+                for iline in ilines:
+                    iparse = iline.split()
+                    if iparse == []:
+                        continue
+                    elif iparse[0] == 'MACRO':
+                        in_macro = True
+                        newmacro = {}
+                        newmacro['name'] = iparse[1]
+                        newmacro[libtype] = leffile
+                        macros.append(newmacro)
+                    elif in_macro:
+                        if iparse[0] == 'END':
+                            if len(iparse) > 1 and iparse[1] == newmacro['name']:
+                                in_macro = False
+                        elif iparse[0] == 'CLASS':
+                            newmacro['class'] = iparse[1]
+                            if len(iparse) > 2:
+                                newmacro['subclass'] = iparse[2]
+
+                                # Use the 'ENDCAP' class to identify pad rotations
+                                # other than BOTTOMLEFT.  This is somewhat ad-hoc
+                                # depending on the foundry;  may not be generally
+                                # applicable.
+
+                                if newmacro['class'] == 'ENDCAP':
+                                    if newmacro['subclass'] == 'TOPLEFT':
+                                        self.pad_rotation = 90
+                                    elif newmacro['subclass'] == 'TOPRIGHT':
+                                        self.pad_rotation = 180
+                                    elif newmacro['subclass'] == 'BOTTOMRIGHT':
+                                        self.pad_rotation = 270
+                            else:
+                                newmacro['subclass'] = None
+                        elif iparse[0] == 'SIZE':
+                            newmacro['width'] = float(iparse[1])
+                            newmacro['height'] = float(iparse[3])
+                        elif iparse[0] == 'ORIGIN':
+                            newmacro['x'] = float(iparse[1])
+                            newmacro['y'] = float(iparse[2])
+        return macros
+          
+    # Read a list of cell names from a verilog file
+    # If filename is relative, then check in the same directory as the verilog
+    # top-level netlist (vlogpath) and in the subdirectory 'source/' of the top-
+    # level directory.  Also check in the ~/design/ip/ directory.  These are
+    # common include paths for the simulation.
+
+    def read_verilog_lib(self, incpath, vlogpath):
+        iocells = []
+        if not os.path.isfile(incpath) and incpath[0] != '/':
+            locincpath = vlogpath + '/' + incpath
+            if not os.path.isfile(locincpath):
+                locincpath = vlogpath + '/source/' + incpath
+            if not os.path.isfile(locincpath):
+                projectpath = os.path.split(vlogpath)[0]
+                designpath = os.path.split(projectpath)[0]
+                locincpath = designpath + '/ip/' + incpath
+        else:
+            locincpath = incpath
+
+        if not os.path.isfile(locincpath):
+            self.print('File ' + incpath + ' not found (at ' + locincpath + ')!')
+        else:
+            self.print('Reading verilog library ' + locincpath)
+            with open(locincpath, 'r') as ifile:
+                ilines = ifile.read().splitlines()
+                for iline in ilines:
+                    iparse = re.split('[\t ()]', iline)
+                    while '' in iparse:
+                        iparse.remove('')
+                    if iparse == []:
+                        continue
+                    elif iparse[0] == 'module':
+                        iocells.append(iparse[1])
+        return iocells
+
+    # Generate a LEF abstract view from a magic layout.  If "outpath" is not
+    # "None", then write output to outputpath (this is required if the input
+    # file is in a read-only directory).
+
+    def write_lef_file(self, magfile, outpath=None):
+        mag_path = os.path.split(magfile)[0]
+        magfullname = os.path.split(magfile)[1]
+        module = os.path.splitext(magfullname)[0]
+
+        if outpath:
+            write_path = outpath
+        else:
+            write_path = mag_path
+
+        self.print('Generating LEF view from layout for module ' + module)
+
+        with open(write_path + '/pfg_write_lef.tcl', 'w') as ofile:
+            print('drc off', file=ofile)
+            print('box 0 0 0 0', file=ofile)
+            # NOTE:  Using "-force" option in case an IP with a different but
+            # compatible tech is used (e.g., EFHX035A IP inside EFXH035C).  This
+            # is not checked for legality!
+            if outpath:
+                print('load ' + magfile + ' -force', file=ofile)
+            else:
+                print('load ' + module + ' -force', file=ofile)
+            print('lef write -hide', file=ofile)
+            print('quit', file=ofile)
+
+        magicexec = self.magic_path if self.magic_path else 'magic'
+        mproc = subprocess.Popen([magicexec, '-dnull', '-noconsole',
+			'pfg_write_lef.tcl'],
+			stdin = subprocess.PIPE, stdout = subprocess.PIPE,
+			stderr = subprocess.PIPE, cwd = write_path, universal_newlines = True)
+
+        self.watch(mproc)
+        os.remove(write_path + '/pfg_write_lef.tcl')
+
+    # Watch a running process, polling for output and updating the GUI message
+    # window as output arrives.  Return only when the process has exited.
+    # Note that this process cannot handle stdin(), so any input to the process
+    # must be passed from a file.
+
+    def watch(self, process):
+        if process == None:
+            return
+
+        while True:
+            status = process.poll()
+            if status != None:
+                try:
+                    outputpair = process.communicate(timeout=1)
+                except ValueError:
+                    self.print("Process forced stop, status " + str(status))
+                else:
+                    for line in outputpair[0].splitlines():
+                        self.print(line)
+                    for line in outputpair[1].splitlines():
+                        self.print(line, file=sys.stderr)
+                break
+            else:
+                sresult = select.select([process.stdout, process.stderr], [], [], 0)[0]
+                if process.stdout in sresult:
+                    outputline = process.stdout.readline().strip()
+                    self.print(outputline)
+                elif process.stderr in sresult:
+                    outputline = process.stderr.readline().strip()
+                    self.print(outputline, file=sys.stderr)
+                else:
+                    self.update_idletasks()
+
+    # Reimport the pad list by reading the top-level verilog netlist.  Determine
+    # what pads are listed in the file, and check against the existing pad list.
+
+    # The verilog/ directory should have a .v file containing a module of the
+    # same name as self.project (ip-name).  The .v filename should have the
+    # same name as well (but not necessarily).  To do:  Handle import of
+    # projects having a top-level schematic instead of a verilog netlist.
+
+    def vlogimport(self):
+
+        if self.ef_format:
+            config_dir = '/.ef-config'
+        else:
+            config_dir = '/.config'
+
+        # First find the process PDK name for this project.  Read the nodeinfo.json
+        # file and find the list of I/O cell libraries.  
+
+        if self.techpath:
+            pdkpath = self.techpath
+        elif os.path.islink(self.projectpath + config_dir + '/techdir'):
+            pdkpath = os.path.realpath(self.projectpath + config_dir + '/techdir')
+        else:
+            self.print('Error:  Cannot determine path to PDK.  Try using option -tech-path=')
+            return
+
+        self.print('Importing verilog sources.')
+
+        nodeinfopath = pdkpath + config_dir + '/nodeinfo.json'
+        ioleflist = []
+        if os.path.exists(nodeinfopath):
+            self.print('Reading known I/O cell libraries from ' + nodeinfopath)
+            with open(nodeinfopath, 'r') as ifile:
+                itop = json.load(ifile)
+                if 'iocells' in itop:
+                    ioleflist = []
+                    for iolib in itop['iocells']:
+                        if '/' in iolib:
+                            # Entries <lib>/<cell> refer to specific files
+                            if self.ef_format:
+                                iolibpath = pdkpath + '/libs.ref/lef/' + iolib
+                            else:
+                                iolibpath = pdkpath + '/libs.ref/' + iolib
+                            if os.path.splitext(iolib)[1] == '':
+                                if not os.path.exists(iolibpath):
+                                    iolibpath = iolibpath + '.lib'
+                            if not os.path.exists(iolibpath):
+                                self.print('Warning: nodeinfo.json bad I/O library path ' + iolibpath)
+                            ioleflist.append(iolibpath)
+                        else:
+                            # All other entries refer to everything in the directory.
+                            if self.ef_format:
+                                iolibpath = pdkpath + '/libs.ref/lef/' + iolib
+                            else:
+                                iolibpath = pdkpath + '/libs.ref/' + iolib + '/lef/'
+                            iolibfiles = glob.glob(iolibpath + '/*.lef')
+                            if len(iolibfiles) == 0:
+                                self.print('Warning: nodeinfo.json bad I/O library path ' + iolibpath)
+                            ioleflist.extend(iolibfiles)
+        else:
+            # Diagnostic
+            self.print('Cannot read PDK information file ' + nodeinfopath)
+
+        # Fallback behavior:  List everything in libs.ref/lef/ beginning with "IO"
+        if len(ioleflist) == 0:
+            if self.ef_format:
+                ioleflist = glob.glob(pdkpath + '/libs.ref/lef/IO*/*.lef')
+            else:
+                ioleflist = glob.glob(pdkpath + '/libs.ref/IO*/lef/*.lef')
+
+        if len(ioleflist) == 0:
+            self.print('Cannot find any I/O cell libraries for this technology')
+            return
+
+        # Read the LEF libraries to get a list of all available cells.  Keep
+        # this list of cells in "celldefs".
+
+        celldefs = []
+        ioliblist = []
+        ioleflibs = []
+        for iolib in ioleflist:
+            iolibpath = os.path.split(iolib)[0]
+            iolibfile = os.path.split(iolib)[1]
+            ioliblist.append(os.path.split(iolibpath)[1])
+            celldefs.extend(self.read_lef_macros(iolibpath, iolibfile, 'iolib'))
+
+        verilogcells = []
+        newpadlist = []
+        coredefs = []
+        corecells = []
+        corecelllist = []
+        lefprocessed = []
+
+        busrex = re.compile('.*\[[ \t]*([0-9]+)[ \t]*:[ \t]*([0-9]+)[ \t]*\]')
+
+        vlogpath = self.projectpath + '/verilog'
+        vlogfile = vlogpath + '/' + self.project + '.v'
+
+        # Verilog netlists are too difficult to parse from a simple script.
+        # Use qflow tools to convert to SPICE, if they are available.  Parse
+        # the verilog only for "include" statements to find the origin of
+        # the various IP blocks, and then parse the SPICE file to get the
+        # full list of instances.
+        #
+        # (to be done)
+
+        if os.path.isfile(vlogfile):
+            with open(vlogfile, 'r') as ifile:
+                vloglines = ifile.read().splitlines()
+                for vlogline in vloglines:
+                    vlogparse = re.split('[\t ()]', vlogline)
+                    while '' in vlogparse:
+                        vlogparse.remove('')
+                    if vlogparse == []:
+                        continue
+                    elif vlogparse[0] == '//':
+                        continue
+                    elif vlogparse[0] == '`include':
+                        incpath = vlogparse[1].strip('"')
+                        libpath = os.path.split(incpath)[0]
+                        libname = os.path.split(libpath)[1]
+                        libfile = os.path.split(incpath)[1]
+
+                        # Read the verilog library for module names to match
+                        # against macro names in celldefs.
+                        modulelist = self.read_verilog_lib(incpath, vlogpath)
+                        matching = list(item for item in celldefs if item['name'] in modulelist)
+                        for imatch in matching:
+                            verilogcells.append(imatch['name'])
+                            leffile = imatch['iolib']
+                            if leffile not in ioleflibs:
+                                ioleflibs.append(leffile)
+
+                        # Read a corresponding LEF file entry for non-I/O macros, if one
+                        # can be found (this handles files in the PDK).
+                        if len(matching) == 0:
+                            if libname != '':
+                                # (NOTE:  Assumes full path starting with '/')
+                                lefpath = libpath.replace('verilog', 'lef')
+                                lefname = libfile.replace('.v', '.lef')
+                                if not os.path.exists(lefpath + '/' + lefname):
+                                    leffiles = glob.glob(lefpath + '/*.lef')
+                                else:
+                                    leffiles = [lefpath + '/' + lefname]
+
+                                for leffile in leffiles:
+                                    if leffile in ioleflibs:
+                                        continue
+                                    elif leffile in lefprocessed:
+                                        continue
+                                    else:
+                                        lefprocessed.append(leffile)
+
+                                    lefname = os.path.split(leffile)[1]
+
+                                    newcoredefs = self.read_lef_macros(lefpath, lefname, 'celllib')
+                                    coredefs.extend(newcoredefs)
+                                    corecells.extend(list(item['name'] for item in newcoredefs))
+
+                                if leffiles == []:
+                                    maglefname = libfile.replace('.v', '.mag')
+
+                                    # Handle PDK files with a maglef/ view but no LEF file.
+                                    maglefpath = libpath.replace('verilog', 'maglef')
+                                    if not os.path.exists(maglefpath + '/' + maglefname):
+                                        magleffiles = glob.glob(maglefpath + '/*.mag')
+                                    else:
+                                        magleffiles = [maglefpath + '/' + maglefname]
+
+                                    if magleffiles == []:
+                                        # Handle user ip/ files with a maglef/ view but
+                                        # no LEF file.
+                                        maglefpath = libpath.replace('verilog', 'maglef')
+                                        designpath = os.path.split(self.projectpath)[0]
+                                        maglefpath = designpath + '/ip/' + maglefpath
+
+                                        if not os.path.exists(maglefpath + '/' + maglefname):
+                                            magleffiles = glob.glob(maglefpath + '/*.mag')
+                                        else:
+                                            magleffiles = [maglefpath + '/' + maglefname]
+
+                                    for magleffile in magleffiles:
+                                        # Generate LEF file.  Since PDK and ip/ entries
+                                        # are not writeable, write into the project mag/
+                                        # directory.
+                                        magpath = self.projectpath + '/mag'
+                                        magname = os.path.split(magleffile)[1]
+                                        magroot = os.path.splitext(magname)[0]
+                                        leffile = magpath + '/' + magroot + '.lef'
+                                        if not os.path.isfile(leffile):
+                                            self.write_lef_file(magleffile, magpath)
+
+                                        if leffile in ioleflibs:
+                                            continue
+                                        elif leffile in lefprocessed:
+                                            continue
+                                        else:
+                                            lefprocessed.append(leffile)
+
+                                        lefname = os.path.split(leffile)[1]
+
+                                        newcoredefs = self.read_lef_macros(magpath, lefname, 'celllib')
+                                        coredefs.extend(newcoredefs)
+                                        corecells.extend(list(item['name'] for item in newcoredefs))
+                                        # LEF files generated on-the-fly are not needed
+                                        # after they have been parsed.
+                                        # os.remove(leffile)
+
+                            # Check if all modules in modulelist are represented by
+                            # corresponding LEF macros.  If not, then go looking for a LEF
+                            # file in the mag/ or maglef/ directory.  Then, go looking for
+                            # a .mag file in the mag/ or maglef/ directory, and build a
+                            # LEF macro from it.
+
+                            matching = list(item['name'] for item in coredefs if item['name'] in modulelist)
+                            for module in modulelist:
+                                if module not in matching:
+                                    lefpath = self.projectpath + '/lef'
+                                    magpath = self.projectpath + '/mag'
+                                    maglefpath = self.projectpath + '/mag'
+                                    lefname = libfile.replace('.v', '.lef')
+
+                                    # If the verilog file root name is not the same as
+                                    # the module name, then make a quick check for a
+                                    # LEF file with the same root name as the verilog.
+                                    # That indicates that the module does not exist in
+                                    # the LEF file, probably because it is a primary
+                                    # module that does not correspond to any layout.
+
+                                    leffile = lefpath + '/' + lefname
+                                    if os.path.exists(leffile):
+                                        self.print('Diagnostic: module ' + module + ' is not in ' + leffile + ' (probably a primary module)')
+                                        continue
+
+                                    leffile = magpath + '/' + lefname
+                                    istemp = False
+                                    if not os.path.exists(leffile):
+                                        magname = libfile.replace('.v', '.mag')
+                                        magfile = magpath + '/' + magname
+                                        if os.path.exists(magfile):
+                                            self.print('Diagnostic: Found a .mag file for ' + module + ' in ' + magfile)
+                                            self.write_lef_file(magfile)
+                                            istemp = True
+                                        else:
+                                            magleffile = maglefpath + '/' + lefname
+                                            if not os.path.exists(magleffile):
+                                                self.print('Diagnostic: (module ' + module + ') has no LEF file ' + leffile + ' or ' + magleffile)
+                                                magleffile = maglefpath + '/' + magname
+                                                if os.path.exists(magleffile):
+                                                    self.print('Diagnostic: Found a .mag file for ' + module + ' in ' + magleffile)
+                                                    if os.access(maglefpath, os.W_OK):
+                                                        self.write_lef_file(magleffile)
+                                                        leffile = magleffile
+                                                        istemp = True
+                                                    else:
+                                                        self.write_lef_file(magleffile, magpath)
+                                                else:
+                                                    self.print('Did not find a file ' + magfile)
+                                                    # self.print('Warning: module ' + module + ' has no LEF or .mag views')
+                                                    pass
+                                            else:
+                                                self.print('Diagnostic: Found a LEF file for ' + module + ' in ' + magleffile)
+                                                leffile = magleffile
+                                    else:
+                                        self.print('Diagnostic: Found a LEF file for ' + module + ' in ' + leffile)
+
+                                    if os.path.exists(leffile):
+                                        if leffile in lefprocessed:
+                                            continue
+                                        else:
+                                            lefprocessed.append(leffile)
+
+                                        newcoredefs = self.read_lef_macros(magpath, lefname, 'celllib')
+                                        # The LEF file generated on-the-fly is not needed
+                                        # any more after parsing the macro(s).
+                                        # if istemp:
+                                        #     os.remove(leffile)
+                                        coredefs.extend(newcoredefs)
+                                        corecells.extend(list(item['name'] for item in newcoredefs))
+                                    else:
+                                        # self.print('Failed to find a LEF view for module ' + module)
+                                        pass
+
+                    elif vlogparse[0] in verilogcells:
+                        # Check for array of pads
+                        bushigh = buslow = -1
+                        if len(vlogparse) >= 3:
+                            bmatch = busrex.match(vlogline)
+                            if bmatch:
+                                bushigh = int(bmatch.group(1))
+                                buslow = int(bmatch.group(2))
+                                
+                        for i in range(buslow, bushigh + 1):
+                            newpad = {}
+                            if i >= 0:
+                                newpad['name'] = vlogparse[1] + '[' + str(i) + ']'
+                            else:
+                                newpad['name'] = vlogparse[1]
+                            newpad['cell'] = vlogparse[0]
+                            padcell = next(item for item in celldefs if item['name'] == vlogparse[0])
+                            newpad['iolib'] = padcell['iolib']
+                            newpad['class'] = padcell['class']
+                            newpad['subclass'] = padcell['subclass']
+                            newpad['width'] = padcell['width']
+                            newpad['height'] = padcell['height']
+                            newpadlist.append(newpad)
+
+                    elif vlogparse[0] in corecells:
+                        # Check for array of cells
+                        bushigh = buslow = -1
+                        if len(vlogparse) >= 3:
+                            bmatch = busrex.match(vlogline)
+                            if bmatch:
+                                bushigh = int(bmatch.group(1))
+                                buslow = int(bmatch.group(2))
+                                
+                        for i in range(buslow, bushigh + 1):
+                            newcorecell = {}
+                            if i >= 0:
+                                newcorecell['name'] = vlogparse[1] + '[' + str(i) + ']'
+                            else:
+                                newcorecell['name'] = vlogparse[1]
+                            newcorecell['cell'] = vlogparse[0]
+                            corecell = next(item for item in coredefs if item['name'] == vlogparse[0])
+                            newcorecell['celllib'] = corecell['celllib']
+                            newcorecell['class'] = corecell['class']
+                            newcorecell['subclass'] = corecell['subclass']
+                            newcorecell['width'] = corecell['width']
+                            newcorecell['height'] = corecell['height']
+                            corecelllist.append(newcorecell)
+
+        self.print('')
+        self.print('Source file information:')
+        self.print('Source filename: ' + vlogfile)
+        self.print('Number of I/O libraries is ' + str(len(ioleflibs)))
+        self.print('Number of library cells in I/O libraries used: ' + str(len(verilogcells)))
+        self.print('Number of core celldefs is ' + str(len(coredefs)))
+        self.print('')
+        self.print('Number of I/O cells in design: ' + str(len(newpadlist)))
+        self.print('Number of core cells in design: ' + str(len(corecelllist)))
+        self.print('')
+
+        # Save the results
+        self.celldefs = celldefs
+        self.coredefs = coredefs
+        self.vlogpads = newpadlist
+        self.corecells = corecelllist
+        self.ioleflibs = ioleflibs
+
+    # Check self.vlogpads, which was created during import (above) against
+    # self.(N,S,W,E)pads, which was read from the DEF file (if there was one)
+    # Also check self.corecells, which was created during import against
+    # self.coregroup, which was read from the DEF file.
+
+    def resolve(self):
+        self.print('Resolve differences in verilog and LEF views.')
+
+        samepads = []
+        addedpads = []
+        removedpads = []
+
+        # (1) Create list of entries that are in both self.vlogpads and self.()pads
+        # (2) Create list of entries that are in self.vlogpads but not in self.()pads
+
+        allpads = self.Npads + self.NEpad + self.Epads + self.SEpad + self.Spads + self.SWpad + self.Wpads + self.NWpad
+
+        for pad in self.vlogpads:
+            newpadname = pad['name']
+            try:
+                lpad = next(item for item in allpads if item['name'] == newpadname) 
+            except:
+                addedpads.append(pad)
+            else:
+                samepads.append(lpad)
+
+        # (3) Create list of entries that are in allpads but not in self.vlogpads
+        for pad in allpads:
+            newpadname = pad['name']
+            try:
+                lpad = next(item for item in self.vlogpads if item['name'] == newpadname) 
+            except:
+                removedpads.append(pad)
+
+        # Print results
+        if len(addedpads) > 0:
+            self.print('Added pads:')
+            for pad in addedpads:
+                self.print(pad['name'] + ' (' + pad['cell'] + ')')
+
+        if len(removedpads) > 0:
+            plist = []
+            nspacers = 0
+            for pad in removedpads:
+                if 'subclass' in pad:
+                    if pad['subclass'] != 'SPACER':
+                        plist.append(pad)
+                    else:
+                        nspacers += 1
+
+            if nspacers > 0:
+                self.print(str(nspacers) + ' spacer cells ignored.')
+            if len(plist) > 0:
+                self.print('Removed pads:')
+                for pad in removedpads:
+                    self.print(pad['name'] + ' (' + pad['cell'] + ')')
+
+        if len(addedpads) + len(removedpads) == 0:
+            self.print('Pad list has not changed.')
+
+        # Remove all cells from the "removed" list, with comment
+
+        allpads = [self.Npads, self.NEpad, self.Epads, self.SEpad, self.Spads, self.SWpad, self.Wpads, self.NWpad]
+
+        for pad in removedpads:
+            rname = pad['name']
+            for row in allpads:
+                try:
+                    rpad = next(item for item in row if item['name'] == rname)
+                except:
+                    rpad = None
+                else:
+                    row.remove(rpad)
+
+        # Now the verilog file has no placement information, so the old padlist
+        # entries (if they exist) are preferred.  Add to these the new padlist
+        # entries
+
+        # First pass for unassigned pads:  Use of "CLASS ENDCAP" is preferred
+        # for identifying corner pads.  Otherwise, if 'CORNER' or 'corner' is
+        # in the pad name, then make sure there is one per row in the first
+        # position.  This is not foolproof and depends on the cell library
+        # using the text 'corner' in the name of the corner cell.  However,
+        # if the ad hoc methods fail, the user can still manually move the
+        # corner cells to the right place (to be done:  Identify if library
+        # uses ENDCAP designation for corner cells up front;  don't go
+        # looking for 'corner' text if the cells are easily identifiable by
+        # LEF class).
+
+        for pad in addedpads[:]:
+            iscorner = False
+            if 'class' in pad and pad['class'] == 'ENDCAP':
+                iscorner = True
+            elif 'CORNER' in pad['cell'].upper():
+                iscorner = True
+           
+            if iscorner:
+                if self.NWpad == []:
+                    self.NWpad.append(pad)
+                    pad['o'] = 'E'
+                    addedpads.remove(pad)
+                elif self.NEpad == []:
+                    self.NEpad.append(pad)
+                    pad['o'] = 'S'
+                    addedpads.remove(pad)
+                elif self.SEpad == []:
+                    self.SEpad.append(pad)
+                    pad['o'] = 'W'
+                    addedpads.remove(pad)
+                elif self.SWpad == []:
+                    self.SWpad.append(pad)
+                    pad['o'] = 'N'
+                    addedpads.remove(pad)
+
+        numN = len(self.Npads)
+        numS = len(self.Spads)
+        numE = len(self.Epads)
+        numW = len(self.Wpads)
+
+        minnum = min(numN, numS, numE, numW)
+        minnum = max(minnum, int(len(addedpads) / 4))
+
+        # Add pads in clockwise order.  Note that S and E pads are defined counterclockwise
+        for pad in addedpads:
+            if numN < minnum:
+                self.Npads.append(pad)
+                numN += 1
+                pad['o'] = 'S'
+                self.print("Adding pad " + pad['name'] + " to Npads")
+            elif numE < minnum:
+                self.Epads.insert(0, pad)
+                numE += 1
+                pad['o'] = 'W'
+                self.print("Adding pad " + pad['name'] + " to Epads")
+            elif numS < minnum:
+                self.Spads.insert(0, pad)
+                numS += 1
+                pad['o'] = 'N'
+                self.print("Adding pad " + pad['name'] + " to Spads")
+            # elif numW < minnum:
+            else:
+                self.Wpads.append(pad)
+                numW += 1
+                pad['o'] = 'E'
+                self.print("Adding pad " + pad['name'] + " to Wpads")
+
+            minnum = min(numN, numS, numE, numW)
+            minnum = max(minnum, int(len(addedpads) / 4))
+
+        # Make sure all pads have included information from the cell definition
+
+        allpads = self.Npads + self.NEpad + self.Epads + self.SEpad + self.Spads + self.SWpad + self.Wpads + self.NWpad
+
+        for pad in allpads:
+            if 'width' not in pad:
+                try:
+                    celldef = next(item for item in celldefs if item['name'] == pad['cell'])
+                except:
+                    self.print('Cell ' + pad['cell'] + ' not found!')
+                else:
+                    pad['width'] = celldef['width']
+                    pad['height'] = celldef['height']
+                    pad['class'] = celldef['class']
+                    pad['subclass'] = celldef['subclass']
+
+        # Now treat the core cells in the same way (resolve list parsed from verilog
+        # against the list parsed from DEF)	
+
+        # self.print('Diagnostic: ')
+        # self.print('self.corecells = ' + str(self.corecells))
+        # self.print('self.coregroup = ' + str(self.coregroup))
+
+        samecore = []
+        addedcore = []
+        removedcore = []
+
+        # (1) Create list of entries that are in both self.corecells and self.coregroup
+        # (2) Create list of entries that are in self.corecells but not in self.coregroup
+
+        for cell in self.corecells:
+            newcellname = cell['name']
+            try:
+                lcore = next(item for item in self.coregroup if item['name'] == newcellname) 
+            except:
+                addedcore.append(cell)
+            else:
+                samecore.append(lcore)
+
+        # (3) Create list of entries that are in self.coregroup but not in self.corecells
+        for cell in self.coregroup:
+            newcellname = cell['name']
+            try:
+                lcore = next(item for item in self.corecells if item['name'] == newcellname) 
+            except:
+                removedcore.append(cell)
+
+        # Print results
+        if len(addedcore) > 0:
+            self.print('Added core cells:')
+            for cell in addedcore:
+                self.print(cell['name'] + ' (' + cell['cell'] + ')')
+
+        if len(removedcore) > 0:
+            clist = []
+            for cell in removedcore:
+                clist.append(cell)
+
+            if len(clist) > 0:
+                self.print('Removed core cells:')
+                for cell in removedcore:
+                    self.print(cell['name'] + ' (' + cell['cell'] + ')')
+
+        if len(addedcore) + len(removedcore) == 0:
+            self.print('Core cell list has not changed.')
+
+        # Remove all cells from the "removed" list
+
+        coregroup = self.coregroup
+        for cell in removedcore:
+            rname = cell['name']
+            try:
+                rcell = next(item for item in coregroup if item['name'] == rname)
+            except:
+                rcell = None
+            else:
+                coregroup.remove(rcell)
+
+        # Add all cells from the "added" list to coregroup
+
+        for cell in addedcore:
+            rname = cell['name']
+            try:
+                rcell = next(item for item in coregroup if item['name'] == rname)
+            except:
+                coregroup.append(cell)
+                if not 'o' in cell:
+                    cell['o'] = 'N'
+                if not 'x' in cell:
+                    if len(self.Wpads) > 0:
+                        pad = self.Wpads[0]
+                        padx = pad['x'] if 'x' in pad else 0
+                        cell['x'] = padx + pad['height'] + self.margin
+                    else:
+                        cell['x'] = self.margin
+                if not 'y' in cell:
+                    if len(self.Spads) > 0:
+                        pad = self.Spads[0]
+                        pady = pad['y'] if 'y' in pad else 0
+                        cell['y'] = pady + pad['height'] + self.margin
+                    else:
+                        cell['y'] = self.margin
+            else:
+                rcell = None
+
+        # Make sure all core cells have included information from the cell definition
+
+        for cell in coregroup:
+            if 'width' not in cell:
+                try:
+                    coredef = next(item for item in coredefs if item['name'] == cell['cell'])
+                except:
+                    self.print('Cell ' + cell['cell'] + ' not found!')
+                else:
+                    cell['width'] = coredef['width']
+                    cell['height'] = coredef['height']
+                    cell['class'] = coredef['class']
+                    cell['subclass'] = coredef['subclass']
+
+    # Generate a new padframe by writing the configuration file, running
+    # padring, reading back the DEF file, and (re)poplulating the workspace
+
+    def generate(self, level):
+        self.print('Generate legal padframe using padring')
+
+        # Write out the configuration file
+        self.writeconfig()
+        # Run the padring app
+        self.runpadring()
+        # Rotate pads in the output if pad orientations are different from
+        # padring's expectations
+        self.rotate_pads_in_def()
+        # Read the placement information back from the generated DEF file
+        self.readplacement()
+        # Resolve differences (e.g., remove spacers)
+        self.resolve()
+        # Recreate and draw the padframe view on the canvas
+        self.populate(level + 1)
+        self.frame_configure(None)
+
+    # Write a new configuration file
+
+    def writeconfig(self):
+        mag_path = self.projectpath + '/mag'
+        if not os.path.exists(mag_path):
+            self.print('Error:  No project path /mag directory exists.  Cannot write config file.')
+            return
+
+        self.print('Writing padring configuration file.')
+
+        # Determine cell width and height from pad sizes.
+        # NOTE:  This compresses the chip to the minimum dimensions
+        # allowed by the arrangement of pads.  Use a "core" block to
+        # force the area larger than minimum (not yet implemented)
+        
+        topwidth = 0
+        for pad in self.Npads:
+            if 'width' not in pad:
+                self.print('No width: pad = ' + str(pad))
+            topwidth += pad['width']
+
+        # Add in the corner cells
+        if self.NWpad != []:
+            topwidth += self.NWpad[0]['height']
+        if self.NEpad != []:
+            topwidth += self.NEpad[0]['width']
+
+        botwidth = 0
+        for pad in self.Spads:
+            botwidth += pad['width']
+
+        # Add in the corner cells
+        if self.SWpad != []:
+            botwidth += self.SWpad[0]['width']
+        if self.SEpad != []:
+            botwidth += self.SEpad[0]['height']
+
+        width = max(botwidth, topwidth)
+
+        # if width < self.urx - self.llx:
+        #     width = self.urx - self.llx
+
+        leftheight = 0
+        for pad in self.Wpads:
+            leftheight += pad['width']
+
+        # Add in the corner cells
+        if self.NWpad != []:
+            leftheight += self.NWpad[0]['height']
+        if self.SWpad != []:
+            leftheight += self.SWpad[0]['width']
+
+        rightheight = 0
+        for pad in self.Epads:
+            rightheight += pad['width']
+
+        # Add in the corner cells
+        if self.NEpad != []:
+            rightheight += self.NEpad[0]['width']
+        if self.SEpad != []:
+            rightheight += self.SEpad[0]['height']
+
+        height = max(leftheight, rightheight)
+
+        # Check the dimensions of the core cells.  If they exceed the available
+        # padframe area, then expand the padframe to accomodate the core.
+
+        corellx = coreurx = (self.llx + self.urx) / 2
+        corelly = coreury = (self.lly + self.ury) / 2
+
+        for corecell in self.coregroup:
+            corient = corecell['o']
+            if 'S' in corient or 'N' in corient:
+                cwidth = corecell['width']
+                cheight = corecell['height']
+            else:
+                cwidth = corecell['height']
+                cheight = corecell['width']
+
+            if corecell['x'] < corellx:
+                corellx = corecell['x']
+            if corecell['x'] + cwidth > coreurx:
+                coreurx = corecell['x'] + cwidth
+            if corecell['y'] < corelly:
+                corelly = corecell['y']
+            if corecell['y'] + cheight > coreury:
+                coreury = corecell['y'] + cheight
+
+        coreheight = coreury - corelly
+        corewidth = coreurx - corellx
+
+        # Ignoring the possibility of overlaps with nonstandard-sized pads,
+        # assuming that the user has visually separated them.  Only check
+        # the core bounds against the standard padframe inside edge.
+
+        if self.SWpad != [] and self.SEpad != []:
+            if corewidth > width - self.SWpad[0]['width'] - self.SEpad[0]['width']:
+                width = corewidth + self.SWpad[0]['width'] + self.SEpad[0]['width']
+        if self.NWpad != [] and self.SWpad != []:
+            if coreheight > height - self.NWpad[0]['height'] - self.SWpad[0]['height']:
+                height = coreheight + self.NWpad[0]['height'] + self.SWpad[0]['height']
+
+        # Core cells are given a margin of self.margin from the pad inside edge, so the
+        # core area passed to the padring app is 2 * self.margin larger than the
+        # measured size of the core area.
+        width += 2 * self.margin
+        height += 2 * self.margin
+
+        if self.keep_cfg == False or not os.path.exists(mag_path + '/padframe.cfg'):
+
+            if os.path.exists(mag_path + '/padframe.cfg'):
+                # Copy the previous padframe.cfg file to a backup.  In case something
+                # goes badly wrong, this should be the only file overwritten, and can
+                # be recovered from the backup.
+                shutil.copy(mag_path + '/padframe.cfg', mag_path + '/padframe.cfg.bak')
+
+            with open(mag_path + '/padframe.cfg', 'w') as ofile:
+                print('AREA ' + str(int(width)) + ' ' + str(int(height)) + ' ;',
+			file=ofile)
+                print('', file=ofile)
+                for pad in self.NEpad:
+                    print('CORNER ' + pad['name'] + ' SW ' + pad['cell'] + ' ;',
+			    file=ofile)
+                for pad in self.SEpad:
+                    print('CORNER ' + pad['name'] + ' NW ' + pad['cell'] + ' ;',
+			    file=ofile)
+                for pad in self.SWpad:
+                    print('CORNER ' + pad['name'] + ' NE ' + pad['cell'] + ' ;',
+			    file=ofile)
+                for pad in self.NWpad:
+                    print('CORNER ' + pad['name'] + ' SE ' + pad['cell'] + ' ;',
+			    file=ofile)
+                for pad in self.Npads:
+                    flip = 'F ' if 'F' in pad['o'] else ''
+                    print('PAD ' + pad['name'] + ' N ' + flip + pad['cell'] + ' ;',
+			    file=ofile)
+                for pad in self.Epads:
+                    flip = 'F ' if 'F' in pad['o'] else ''
+                    print('PAD ' + pad['name'] + ' E ' + flip + pad['cell'] + ' ;',
+			    file=ofile)
+                for pad in self.Spads:
+                    flip = 'F ' if 'F' in pad['o'] else ''
+                    print('PAD ' + pad['name'] + ' S ' + flip + pad['cell'] + ' ;',
+			    file=ofile)
+                for pad in self.Wpads:
+                    flip = 'F ' if 'F' in pad['o'] else ''
+                    print('PAD ' + pad['name'] + ' W ' + flip + pad['cell'] + ' ;',
+			    file=ofile)
+
+    # Run the padring app.
+
+    def runpadring(self):
+        mag_path = self.projectpath + '/mag'
+        if not os.path.exists(mag_path):
+            self.print('No path /mag exists in project space;  cannot run padring.')
+            return
+
+        self.print('Running padring')
+
+        if self.padring_path:
+            padringopts = [self.padring_path]
+        else:
+            padringopts = ['padring']
+
+        # Diagnostic
+        # self.print('Used libraries (self.ioleflibs) = ' + str(self.ioleflibs))
+
+        for iolib in self.ioleflibs:
+            padringopts.append('-L')
+            padringopts.append(iolib)
+        padringopts.append('--def')
+        padringopts.append('padframe.def')
+        padringopts.append('padframe.cfg')
+
+        self.print('Running ' + str(padringopts))
+   
+        p = subprocess.Popen(padringopts, stdout = subprocess.PIPE,
+		    stderr = subprocess.PIPE, cwd = mag_path)
+        self.watch(p)
+
+    # Read placement information from the DEF file generated by padring.
+
+    def readplacement(self, precheck=False):
+        self.print('Reading placement information from DEF file')
+
+        mag_path = self.projectpath + '/mag'
+        if not os.path.isfile(mag_path + '/padframe.def'):
+            if not precheck:
+                self.print('No file padframe.def:  pad frame was not generated.')
+            return False 
+
+        # Very simple DEF file parsing.  The placement DEF only contains a
+        # COMPONENTS section.  Certain assumptions are made about the syntax
+        # that depends on the way 'padring' writes its output.  This is not
+        # a rigorous DEF parser!
+
+        units = 1000
+        in_components = False
+        Npadlist = []
+        Spadlist = []
+        Epadlist = []
+        Wpadlist = []
+        NEpad = []
+        NWpad = []
+        SEpad = []
+        SWpad = []
+        coregroup = []
+
+        # Reset bounds
+        self.llx = self.lly = self.urx = self.ury = 0
+        corners = 0
+
+        with open(mag_path + '/padframe.def', 'r') as ifile:
+            deflines = ifile.read().splitlines()
+            for line in deflines:
+                if 'UNITS DISTANCE MICRONS' in line:
+                    units = line.split()[3]
+                elif in_components:
+                    lparse = line.split()
+                    if lparse[0] == '-':
+                        instname = lparse[1]
+                        cellname = lparse[2]
+                        
+                    elif lparse[0] == '+':
+                        if lparse[1] == 'PLACED':
+                            placex = lparse[3]
+                            placey = lparse[4]
+                            placeo = lparse[6]
+
+                            newpad = {}
+                            newpad['name'] = instname
+                            newpad['cell'] = cellname
+
+                            try:
+                                celldef = next(item for item in self.celldefs if item['name'] == cellname)
+                            except:
+                                celldef = None
+                            else:
+                                newpad['iolib'] = celldef['iolib']
+                                newpad['width'] = celldef['width']
+                                newpad['height'] = celldef['height']
+                                newpad['class'] = celldef['class']
+                                newpad['subclass'] = celldef['subclass']
+ 
+                            newpad['x'] = float(placex) / float(units)
+                            newpad['y'] = float(placey) / float(units)
+                            newpad['o'] = placeo
+
+                            # Adjust bounds
+                            if celldef:
+                                if newpad['x'] < self.llx:
+                                    self.llx = newpad['x']
+                                if newpad['y'] < self.lly:
+                                    self.lly = newpad['y']
+
+                                if newpad['o'] == 'N' or newpad['o'] == 'S':
+                                    padurx = newpad['x'] + celldef['width']
+                                    padury = newpad['y'] + celldef['height']
+                                else:
+                                    padurx = newpad['x'] + celldef['height']
+                                    padury = newpad['y'] + celldef['width']
+
+                                if padurx > self.urx:
+                                    self.urx = padurx
+                                if padury > self.ury:
+                                    self.ury = padury
+
+                            # First four entries in the DEF file are corners
+                            # padring puts the lower left corner at zero, so
+                            # use the zero coordinates to determine which pads
+                            # are which.  Note that padring assumes the corner
+                            # pad is drawn in the SW corner position!
+
+                            if corners < 4:
+                                if newpad['x'] == 0 and newpad['y'] == 0:
+                                    SWpad.append(newpad)
+                                elif newpad['x'] == 0:
+                                    NWpad.append(newpad)
+                                elif newpad['y'] == 0:
+                                    SEpad.append(newpad)
+                                else:
+                                    NEpad.append(newpad)
+                                corners += 1       
+                            else:
+                                # Place according to orientation.  If orientation
+                                # is not standard, be sure to make it standard!
+                                placeo = self.rotate_orientation(placeo)
+                                if placeo == 'N':
+                                    Spadlist.append(newpad)
+                                elif placeo == 'E':
+                                    Wpadlist.append(newpad)
+                                elif placeo == 'S':
+                                    Npadlist.append(newpad)
+                                else:
+                                    Epadlist.append(newpad)
+
+                    elif 'END COMPONENTS' in line:
+                        in_components = False
+                elif 'COMPONENTS' in line:
+                    in_components = True
+
+            self.Npads = Npadlist
+            self.Wpads = Wpadlist
+            self.Spads = Spadlist
+            self.Epads = Epadlist
+
+            self.NWpad = NWpad
+            self.NEpad = NEpad
+            self.SWpad = SWpad
+            self.SEpad = SEpad
+
+        # The padframe has its own DEF file from the padring app, but the core
+        # does not.  The core needs to be floorplanned in a very similar manner.
+        # This will be done by searching for a DEF file of the project top-level
+        # layout.  If none exists, it is created by generating it from the layout.
+        # If the top-level layout does not exist, then all core cells are placed
+        # at the origin, and the origin placed at the padframe inside corner.
+
+        mag_path = self.projectpath + '/mag'
+        if not os.path.isfile(mag_path + '/' + self.project + '.def'):
+            if os.path.isfile(mag_path + '/' + self.project + '.mag'):
+
+                # Create a DEF file from the layout
+                with open(mag_path + '/pfg_write_def.tcl', 'w') as ofile:
+                    print('drc off', file=ofile)
+                    print('box 0 0 0 0', file=ofile)
+                    print('load ' + self.project, file=ofile)
+                    print('def write', file=ofile)
+                    print('quit', file=ofile)
+
+                magicexec = self.magic_path if self.magic_path else 'magic'
+                mproc = subprocess.Popen([magicexec, '-dnull', '-noconsole',
+			'pfg_write_def.tcl'],
+			stdin = subprocess.PIPE, stdout = subprocess.PIPE,
+			stderr = subprocess.PIPE, cwd = mag_path, universal_newlines = True)
+
+                self.watch(mproc)
+                os.remove(mag_path + '/pfg_write_def.tcl')
+
+            elif not os.path.isfile(mag_path + '/core.def'):
+
+                # With no other information available, copy the corecells
+                # (from the verilog file) into the coregroup list.
+                # Position all core cells starting at the padframe top left
+                # inside corner, and arranging in rows without overlapping.
+                # Note that no attempt is made to organize the cells or
+                # otherwise produce an efficient layout.  Any dimension larger
+                # than the current padframe overruns to the right or bottom.
+
+                if self.SWpad != []:
+                    corellx = SWpad[0]['x'] + SWpad[0]['width'] + self.margin
+                    corelly = SWpad[0]['y'] + SWpad[0]['height'] + self.margin
+                else:
+                    corellx = Wpadlist[0]['x'] + Wpadlist[0]['height'] + self.margin
+                    corelly = Spadlist[0]['x'] + Spadlist[0]['height'] + self.margin
+                if self.NEpad != []:
+                    coreurx = NEpad[0]['x'] - self.margin
+                    coreury = NEpad[0]['y'] - self.margin
+                else:
+                    coreurx = Epadlist[0]['x'] - self.margin
+                    coreury = Npadlist[0]['x'] - self.margin
+                locllx = corellx
+                testllx = corellx
+                loclly = corelly
+                testlly = corelly
+                nextlly = corelly
+              
+                for cell in self.corecells:
+
+                    testllx = locllx + cell['width']
+                    if testllx > coreurx:
+                        locllx = corellx
+                        corelly = nextlly
+                        loclly = nextlly
+
+                    newcore = cell
+                    newcore['x'] = locllx
+                    newcore['y'] = loclly
+                    newcore['o'] = 'N'
+
+                    locllx += cell['width'] + self.margin
+
+                    testlly = corelly + cell['height'] + self.margin
+                    if testlly > nextlly:
+                        nextlly = testlly
+
+                    coregroup.append(newcore)
+
+                self.coregroup = coregroup
+
+        if os.path.isfile(mag_path + '/' + self.project + '.def'):
+            # Read the top-level DEF, and use it to position the core cells.
+            self.print('Reading the top-level cell DEF for core cell placement.')
+
+            units = 1000
+            in_components = False
+            with open(mag_path + '/' + self.project + '.def', 'r') as ifile:
+                deflines = ifile.read().splitlines()
+                for line in deflines:
+                    if 'UNITS DISTANCE MICRONS' in line:
+                        units = line.split()[3]
+                    elif in_components:
+                        lparse = line.split()
+                        if lparse[0] == '-':
+                            instname = lparse[1]
+                            # NOTE: Magic should not drop the entire path to the
+                            # cell for the cellname;  this needs to be fixed!  To
+                            # work around it, remove any path components.
+                            cellpath = lparse[2]
+                            cellname = os.path.split(cellpath)[1]
+                        
+                        elif lparse[0] == '+':
+                            if lparse[1] == 'PLACED':
+                                placex = lparse[3]
+                                placey = lparse[4]
+                                placeo = lparse[6]
+
+                                newcore = {}
+                                newcore['name'] = instname
+                                newcore['cell'] = cellname
+
+                                try:
+                                    celldef = next(item for item in self.coredefs if item['name'] == cellname)
+                                except:
+                                    celldef = None
+                                else:
+                                    newcore['celllib'] = celldef['celllib']
+                                    newcore['width'] = celldef['width']
+                                    newcore['height'] = celldef['height']
+                                    newcore['class'] = celldef['class']
+                                    newcore['subclass'] = celldef['subclass']
+ 
+                                newcore['x'] = float(placex) / float(units)
+                                newcore['y'] = float(placey) / float(units)
+                                newcore['o'] = placeo
+                                coregroup.append(newcore)
+
+                        elif 'END COMPONENTS' in line:
+                            in_components = False
+                    elif 'COMPONENTS' in line:
+                        in_components = True
+
+            self.coregroup = coregroup
+
+        elif os.path.isfile(mag_path + '/core.def'):
+            # No DEF or .mag file, so fallback position is the last core.def
+            # file generated by this script.
+            self.read_core_def(precheck=precheck)
+
+        return True
+
+    # Read placement information from the "padframe.def" file and rotate
+    # all cells according to self.pad_rotation.  This accounts for the
+    # problem that the default orientation of pads is arbitrarily defined
+    # by the foundry, while padring assumes that the corner pad is drawn
+    # in the lower-left position and other pads are drawn with the pad at
+    # the bottom and the buses at the top.
+
+    def rotate_pads_in_def(self):
+        if self.pad_rotation == 0:
+            return
+
+        self.print('Rotating pads in padframe DEF file.')
+        mag_path = self.projectpath + '/mag'
+
+        if not os.path.isfile(mag_path + '/padframe.def'):
+            self.print('No file padframe.def:  Cannot modify pad rotations.')
+            return
+
+        deflines = []
+        with open(mag_path + '/padframe.def', 'r') as ifile:
+            deflines = ifile.read().splitlines()
+
+        outlines = []
+        in_components = False
+        for line in deflines:
+            if in_components:
+                lparse = line.split()
+                if lparse[0] == '+':
+                    if lparse[1] == 'PLACED':
+                        neworient = self.rotate_orientation(lparse[6])
+                        lparse[6] = neworient
+                        line = ' '.join(lparse)
+
+                elif 'END COMPONENTS' in line:
+                    in_components = False
+            elif 'COMPONENTS' in line:
+                in_components = True
+            outlines.append(line)
+
+        with open(mag_path + '/padframe.def', 'w') as ofile:
+            for line in outlines:
+                print(line, file=ofile)
+
+    # Read placement information from the DEF file for the core (created by
+    # a previous run of this script)
+
+    def read_core_def(self, precheck=False):
+        self.print('Reading placement information from core DEF file.')
+
+        mag_path = self.projectpath + '/mag'
+
+        if not os.path.isfile(mag_path + '/core.def'):
+            if not precheck:
+                self.print('No file core.def:  core placement was not generated.')
+            return False 
+
+        # Very simple DEF file parsing, similar to the padframe.def reading
+        # routine above.
+
+        units = 1000
+        in_components = False
+
+        coregroup = []
+
+        with open(mag_path + '/core.def', 'r') as ifile:
+            deflines = ifile.read().splitlines()
+            for line in deflines:
+                if 'UNITS DISTANCE MICRONS' in line:
+                    units = line.split()[3]
+                elif in_components:
+                    lparse = line.split()
+                    if lparse[0] == '-':
+                        instname = lparse[1]
+                        cellname = lparse[2]
+                        
+                    elif lparse[0] == '+':
+                        if lparse[1] == 'PLACED':
+                            placex = lparse[3]
+                            placey = lparse[4]
+                            placeo = lparse[6]
+
+                            newcore = {}
+                            newcore['name'] = instname
+                            newcore['cell'] = cellname
+
+                            try:
+                                celldef = next(item for item in self.coredefs if item['name'] == cellname)
+                            except:
+                                celldef = None
+                            else:
+                                newcore['celllib'] = celldef['celllib']
+                                newcore['width'] = celldef['width']
+                                newcore['height'] = celldef['height']
+                                newcore['class'] = celldef['class']
+                                newcore['subclass'] = celldef['subclass']
+ 
+                            newcore['x'] = float(placex) / float(units)
+                            newcore['y'] = float(placey) / float(units)
+                            newcore['o'] = placeo
+                            coregroup.append(newcore)
+
+                    elif 'END COMPONENTS' in line:
+                        in_components = False
+                elif 'COMPONENTS' in line:
+                    in_components = True
+
+            self.coregroup = coregroup
+
+        return True
+
+    # Save the layout to a Magic database file (to be completed)
+
+    def save(self):
+        self.print('Saving results in a magic layout database.')
+
+        # Generate a list of (unique) LEF libraries for all padframe and core cells
+        leflist = []
+        for pad in self.celldefs:
+            if pad['iolib'] not in leflist:
+                leflist.append(pad['iolib'])
+
+        for core in self.coredefs:
+            if core['celllib'] not in leflist:
+                leflist.append(core['celllib'])
+
+        # Run magic, and generate the padframe with a series of commands
+        mag_path = self.projectpath + '/mag'
+
+        with open(mag_path + '/pfg_write_mag.tcl', 'w') as ofile:
+            print('drc off', file=ofile)
+            print('box 0 0 0 0', file=ofile)
+            for leffile in leflist:
+                print('lef read ' + leffile, file=ofile)
+            print('def read padframe', file=ofile)
+            print('select top cell', file=ofile)
+            print('select area', file=ofile)
+            print('select save padframe', file=ofile)
+            print('delete', file=ofile)
+            print('def read core', file=ofile)
+            print('getcell padframe', file=ofile)
+            print('save ' + self.project, file=ofile)
+            print('writeall force ' + self.project, file=ofile)
+            print('quit', file=ofile)
+
+        magicexec = self.magic_path if self.magic_path else 'magic'
+        mproc = subprocess.Popen([magicexec, '-dnull', '-noconsole',
+			'pfg_write_mag.tcl'],
+			stdin = subprocess.PIPE, stdout = subprocess.PIPE,
+			stderr = subprocess.PIPE, cwd = mag_path, universal_newlines = True)
+        self.watch(mproc)
+        os.remove(mag_path + '/pfg_write_mag.tcl')
+        self.print('Done writing layout ' + self.project + '.mag')
+
+        # Write the core DEF file if it does not exist yet.
+        if not os.path.isfile(mag_path + '/core.def'):
+            self.write_core_def()
+
+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)
+
+    if '-help' in options:
+        print(sys.argv[0] + ' [options]')
+        print('')
+        print('options:')
+        print('   -noc    Print output to terminal, not the gui window')
+        print('   -nog    No graphics, run in batch mode')
+        print('   -cfg    Use existing padframe.cfg, do not regenerate')
+        print('   -padring-path=<path>	path to padring executable')
+        print('   -magic-path=<path>	path to magic executable')
+        print('   -tech-path=<path>	path to tech root folder')
+        print('   -project-path=<path>	path to project root folder')
+        print('   -help   Print this usage information')
+        print('')
+        sys.exit(0)
+
+    root = tkinter.Tk()
+    do_gui = False if ('-nog' in options or '-nogui' in options) else True
+    app = SoCFloorplanner(root, do_gui)
+
+    # Allow option -noc to bypass the text-to-console redirection, so crash
+    # information doesn't disappear with the app.
+
+    app.use_console = False if ('-noc' in options or '-noconsole' in options) else True
+    if do_gui == False:
+        app.use_console = False
+
+    # efabless format can be specified on the command line, but note that it
+    # is otherwise auto-detected by checking for .config vs. .ef-config in
+    # the project space.
+
+    app.ef_format = True if '-ef_format' in options else False
+    app.keep_cfg = True if '-cfg' in options else False
+
+    app.padring_path = None
+    app.magic_path = None
+    app.techpath = None
+    app.projectpath = None
+
+    for option in options:
+        if option.split('=')[0] == '-padring-path':
+            app.padring_path = option.split('=')[1]
+        elif option.split('=')[0] == '-magic-path':
+            app.magic_path = option.split('=')[1]
+        elif option.split('=')[0] == '-tech-path':
+            app.techpath = option.split('=')[1]
+        elif option.split('=')[0] == '-project-path':
+            app.projectpath = option.split('=')[1]
+            app.projectpath = app.projectpath[:-1] if app.projectpath[-1] == '/' else app.projectpath
+
+    app.text_to_console()
+    app.init_padframe()
+    if app.do_gui:
+        root.mainloop()
+    else:
+        # Run 'save' in non-GUI mode
+        app.save()
+        sys.exit(0)
+