First pass at incorporating the efabless LVS manager GUI into the

netgen distribution.  It sort of works, but not quite there yet.
This commit is contained in:
Tim Edwards
2018-05-01 14:07:16 -04:00
parent 02a77b61f9
commit 25ac57d103
21 changed files with 3240 additions and 283 deletions
+54
View File
@@ -0,0 +1,54 @@
MODULE = python
NETGENDIR = ..
SRCS =
SCRIPTS = consoletext.py helpwindow.py lvs_manager.py treeviewsplit.py
SCRIPTS += tksimpledialog.py tooltip.py lvs_help.txt
SCRIPTINSTALL = $(DESTDIR)${PYDIR}
include ${NETGENDIR}/defs.mak
main: lvs_manager.py
tcl-main: lvs_manager.py
$(DESTDIR)${PYDIR}/consoletext.py:
${RM} $@
${CP} consoletext.py $@
$(DESTDIR)${PYDIR}/helpwindow.py:
${RM} $@
${CP} helpwindow.py $@
lvs_manager.py: lvs_manager.py.in
sed -e '/SUBST_SCRIPT_DIR/s#SUBST_SCRIPT_DIR#$(SCRIPTINSTALL)#' \
lvs_manager.py.in > lvs_manager.py
$(DESTDIR)${PYDIR}/lvs_manager.py: lvs_manager.py
${RM} $@
${CP} lvs_manager.py $@
$(DESTDIR)${PYDIR}/treeviewsplit.py: treeviewsplit.py
${RM} $@
${CP} treeviewsplit.py $@
$(DESTDIR)${PYDIR}/tksimpledialog.py: tksimpledialog.py
${RM} $@
${CP} tksimpledialog.py $@
$(DESTDIR)${PYDIR}/tooltip.py: tooltip.py
${RM} $@
${CP} tooltip.py $@
$(DESTDIR)${PYDIR}/lvs_help.txt: lvs_help.txt
${RM} $@
${CP} lvs_help.txt $@
install: install-tcl
install-tcl: $(DESTDIR)${PYDIR} $(DESTDIR)${PYDIR}/consoletext.py \
$(DESTDIR)${PYDIR}/helpwindow.py $(DESTDIR)${PYDIR}/lvs_manager.py \
$(DESTDIR)${PYDIR}/treeviewsplit.py $(DESTDIR)${PYDIR}/tksimpledialog.py \
$(DESTDIR)${PYDIR}/tooltip.py $(DESTDIR)${PYDIR}/lvs_help.txt
include ${NETGENDIR}/rules.mak
+54
View File
@@ -0,0 +1,54 @@
#!/bin/env python3
#
#--------------------------------------------------------
"""
consoletext --- extends tkinter class Text
with stdout and stderr redirection.
"""
#--------------------------------------------------------
# Written by Tim Edwards
# efabless, inc.
# September 11, 2016
# Version 0.1
#--------------------------------------------------------
import sys
import tkinter
class ConsoleText(tkinter.Text):
linelimit = 500
class IORedirector(object):
'''A general class for redirecting I/O to this Text widget.'''
def __init__(self,text_area):
self.text_area = text_area
class StdoutRedirector(IORedirector):
'''A class for redirecting stdout to this Text widget.'''
def write(self,str):
self.text_area.write(str,False)
class StderrRedirector(IORedirector):
'''A class for redirecting stderr to this Text widget.'''
def write(self,str):
self.text_area.write(str,True)
def __init__(self, master=None, cnf={}, **kw):
'''See the __init__ for Tkinter.Text.'''
tkinter.Text.__init__(self, master, cnf, **kw)
self.tag_configure('stdout',background='white',foreground='black')
self.tag_configure('stderr',background='white',foreground='red')
# None of these works! Cannot change selected text background!
self.config(selectbackground='blue', selectforeground='white')
self.tag_configure('sel',background='blue',foreground='white')
def write(self, val, is_stderr=False):
lines = int(self.index('end-1c').split('.')[0])
if lines > self.linelimit:
self.delete('1.0', str(lines - self.linelimit) + '.0')
self.insert('end',val,'stderr' if is_stderr else 'stdout')
self.see('end')
def limit(self, val):
self.linelimit = val
+229
View File
@@ -0,0 +1,229 @@
#!/bin/env python3
#
#--------------------------------------------------------
# Help Window for the Open Galaxy project manager
#
#--------------------------------------------------------
# Written by Tim Edwards
# efabless, inc.
# September 12, 2016
# Version 0.1
#--------------------------------------------------------
import re
import tkinter
from tkinter import ttk
class HelpWindow(tkinter.Toplevel):
"""Open Galaxy help window."""
def __init__(self, parent=None, fontsize = 11, *args, **kwargs):
'''See the __init__ for Tkinter.Toplevel.'''
tkinter.Toplevel.__init__(self, parent, *args, **kwargs)
s = ttk.Style()
s.configure('normal.TButton', font=('Helvetica', fontsize), border = 3, relief = 'raised')
self.withdraw()
self.title('Open Galaxy Help')
self.helptitle = ttk.Label(self, style='title.TLabel', text = '(no text)')
self.helptitle.grid(column = 0, row = 0, sticky = "news")
self.helpbar = ttk.Separator(self, orient='horizontal')
self.helpbar.grid(column = 0, row = 1, sticky = "news")
self.hframe = tkinter.Frame(self)
self.hframe.grid(column = 0, row = 2, sticky = "news")
self.hframe.helpdisplay = ttk.Frame(self.hframe)
self.hframe.helpdisplay.pack(side = 'left', fill = 'both', expand = 'true')
self.hframe.helpdisplay.helptext = tkinter.Text(self.hframe.helpdisplay, wrap='word')
self.hframe.helpdisplay.helptext.pack(side = 'top', fill = 'both', expand = 'true')
# Add scrollbar to help window
self.hframe.scrollbar = ttk.Scrollbar(self.hframe)
self.hframe.scrollbar.pack(side='right', fill='y')
# attach help window to scrollbar
self.hframe.helpdisplay.helptext.config(yscrollcommand = self.hframe.scrollbar.set)
self.hframe.scrollbar.config(command = self.hframe.helpdisplay.helptext.yview)
self.hframe.toc = ttk.Treeview(self.hframe, selectmode='browse')
self.hframe.toc.bind('<<TreeviewSelect>>', self.toc_to_page)
self.hframe.toc.bind('<<TreeviewOpen>>', self.toc_toggle)
self.hframe.toc.bind('<<TreeviewClose>>', self.toc_toggle)
self.hframe.toc.tag_configure('title', font=('Helvetica', fontsize, 'bold italic'),
foreground = 'brown', anchor = 'center')
self.hframe.toc.heading('#0', text = "Table of Contents")
self.bbar = ttk.Frame(self)
self.bbar.grid(column = 0, row = 3, sticky = "news")
self.bbar.close_button = ttk.Button(self.bbar, text='Close',
command=self.close, style = 'normal.TButton')
self.bbar.close_button.grid(column=0, row=0, padx = 5)
self.bbar.prev_button = ttk.Button(self.bbar, text='Prev',
command=self.prevpage, style = 'normal.TButton')
self.bbar.prev_button.grid(column=1, row=0, padx = 5)
self.bbar.next_button = ttk.Button(self.bbar, text='Next',
command=self.nextpage, style = 'normal.TButton')
self.bbar.next_button.grid(column=2, row=0, padx = 5)
self.bbar.contents_button = ttk.Button(self.bbar, text='Table of Contents',
command=self.page_to_toc, style = 'normal.TButton')
self.bbar.contents_button.grid(column=3, row=0, padx = 5)
self.rowconfigure(0, weight=0)
self.rowconfigure(1, weight=0)
self.rowconfigure(2, weight=1)
self.rowconfigure(3, weight=0)
self.columnconfigure(0, weight=1)
# Help pages
self.pages = []
self.pageno = -1 # No page
self.toggle = False
def grid_configure(self, padx, pady):
pass
def redisplay(self):
# remove contents
if self.pageno >= 0 and self.pageno < len(self.pages):
self.hframe.helpdisplay.helptext.delete('1.0', 'end')
self.hframe.helpdisplay.helptext.insert('end', self.pages[self.pageno]['text'])
self.helptitle.configure(text = self.pages[self.pageno]['title'])
def toc_toggle(self, event):
self.toggle = True
def toc_to_page(self, event):
treeview = event.widget
selection = treeview.item(treeview.selection())
# Make sure any open/close callback is handled first!
self.update_idletasks()
if self.toggle:
# Item was opened or closed, so consider this a 'false select' and
# do not go to the page.
self.toggle = False
return
if 'values' in selection:
pagenum = selection['values'][0]
else:
print('Unknown page selected.')
pagenum = 0
# Display a page after displaying the table of contents
self.hframe.toc.pack_forget()
self.hframe.scrollbar.pack_forget()
self.hframe.helpdisplay.pack(side='left', fill='both', expand = 'true')
self.hframe.scrollbar.pack(side='right', fill='y')
self.hframe.scrollbar.config(command = self.hframe.helpdisplay.helptext.yview)
# Enable Prev and Next buttons
self.bbar.prev_button.configure(state='enabled')
self.bbar.next_button.configure(state='enabled')
# Redisplay
self.page(pagenum)
def page_to_toc(self):
# Display the table of contents after displaying a page
self.hframe.scrollbar.pack_forget()
self.hframe.helpdisplay.pack_forget()
self.hframe.toc.pack(side='left', fill='both', expand = 'true')
self.hframe.scrollbar.pack(side='right', fill='y')
self.hframe.scrollbar.config(command = self.hframe.toc.yview)
# Disable Prev and Next buttons
self.bbar.prev_button.configure(state='disabled')
self.bbar.next_button.configure(state='disabled')
# Simple add page with a single block of plain text
def add_page(self, toc_text, text_block):
newdict = {}
newdict['text'] = text_block
newdict['title'] = toc_text
self.pages.append(newdict)
newpageno = len(self.pages)
self.hframe.toc.insert('', 'end', text=str(newpageno) + '. ' + toc_text,
tag='title', value = newpageno - 1)
if self.pageno < 0:
self.pageno = 0 # First page
# Fill the help text from a file. The format of the file is:
# <page_num>
# <title>
# <text>
# '.'
# Text is multi-line and ends when '.' is encountered by itself
def add_pages_from_file(self, filename):
endpagerex = re.compile('^\.$')
newpagerex = re.compile('^[0-9\.]+$')
commentrex = re.compile('^[\-]+$')
hierarchy = ''
print('Loading help text from file ' + filename)
with open(filename, 'r') as f:
toc_text = []
page_text = []
for line in f:
if newpagerex.match(line) or endpagerex.match(line):
if toc_text and page_text:
newdict = {}
self.pages.append(newdict)
newpageno = len(self.pages)
if '.' in hierarchy:
pageinfo = hierarchy.rsplit('.', 1)
if pageinfo[1] == '':
parentid = ''
pageid = pageinfo[0]
else:
parentid = pageinfo[0]
pageid = pageinfo[1]
else:
parentid = ''
pageid = hierarchy
if parentid:
pageid = parentid + '.' + pageid
newdict['text'] = page_text
newdict['title'] = pageid + '. ' + toc_text
self.hframe.toc.insert(parentid, 'end',
text=newdict['title'], tag='title',
value = newpageno - 1, iid = pageid)
if newpagerex.match(line):
hierarchy = line.rstrip()
toc_text = []
elif not toc_text:
toc_text = line.rstrip()
page_text = []
elif not commentrex.match(line):
if not page_text:
page_text = line
else:
page_text += line
def nextpage(self):
# Go to next page
if self.pageno < len(self.pages) - 1:
self.pageno += 1
self.redisplay()
def prevpage(self):
# Go to previous page
if self.pageno > 0:
self.pageno -= 1
self.redisplay()
def page(self, pagenum):
# Go to indicated page
if pagenum >= 0 and pagenum < len(self.pages):
self.pageno = pagenum
self.redisplay()
def close(self):
# pop down help window
self.withdraw()
def open(self):
# pop up help window
self.deiconify()
self.lift()
+5
View File
@@ -0,0 +1,5 @@
1.
Selecting a project
-------
Start by selecting a project. This step is only necessary if the application is started from the command line. If started from the project manager, then it can only be run if a project was selected.
.
+819
View File
@@ -0,0 +1,819 @@
#!/bin/env python3
#
#--------------------------------------------------------
# LVS Manager GUI.
#
# This is a Python tkinter script that handles the
# process of running LVS and interpreting results.
#
#--------------------------------------------------------
# Written by Tim Edwards
# efabless, inc.
# Version 1. November 30, 2016
# Version 2. March 6, 2017. Reads JSON format output
# Version 3. April 25, 2018. Handles layout vs. verilog
#--------------------------------------------------------
import io
import os
import re
import sys
import json
import shutil
import signal
import socket
import select
import datetime
import contextlib
import subprocess
import tkinter
from tkinter import ttk
from tkinter import filedialog
import tksimpledialog
import tooltip
from consoletext import ConsoleText
from helpwindow import HelpWindow
from treeviewsplit import TreeViewSplit
# User preferences file (if it exists)
prefsfile = '~/.profile/prefs.json'
netgen_script_dir = '/usr/local/lib/netgen/python'
#------------------------------------------------------
# Simple dialog for confirming quit
#------------------------------------------------------
class ConfirmDialog(tksimpledialog.Dialog):
def body(self, master, warning, seed):
if warning:
ttk.Label(master, text=warning, wraplength=500).grid(row = 0, columnspan = 2, sticky = 'wns')
return self
def apply(self):
return 'okay'
#------------------------------------------------------
# Main class for this application
#------------------------------------------------------
class LVSManager(ttk.Frame):
"""LVS Manager GUI."""
def __init__(self, parent, *args, **kwargs):
ttk.Frame.__init__(self, parent, *args, **kwargs)
self.root = parent
self.init_gui()
parent.protocol("WM_DELETE_WINDOW", self.on_quit)
def on_quit(self):
"""Exits program."""
if self.msock:
self.msock.close()
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('bg.TFrame', background='gray40')
s.configure('italic.TLabel', font=('Helvetica', fontsize, 'italic'))
s.configure('title.TLabel', font=('Helvetica', fontsize, 'bold italic'),
foreground = 'brown', anchor = 'center')
s.configure('normal.TLabel', font=('Helvetica', fontsize))
s.configure('red.TLabel', font=('Helvetica', fontsize), foreground = 'red')
s.configure('green.TLabel', font=('Helvetica', fontsize), foreground = 'green3')
s.configure('blue.TLabel', font=('Helvetica', fontsize), foreground = 'blue')
s.configure('normal.TButton', font=('Helvetica', fontsize),
border = 3, relief = 'raised')
s.configure('red.TButton', font=('Helvetica', fontsize), foreground = 'red',
border = 3, relief = 'raised')
s.configure('green.TButton', font=('Helvetica', fontsize), foreground = 'green3',
border = 3, relief = 'raised')
s.configure('blue.TButton', font=('Helvetica', fontsize), foreground = 'blue',
border = 3, relief = 'raised')
s.configure('redtitle.TButton', font=('Helvetica', fontsize, 'bold italic'),
foreground = 'red', border = 3, relief = 'raised')
s.configure('bluetitle.TButton', font=('Helvetica', fontsize, 'bold italic'),
foreground = 'blue', border = 3, relief = 'raised')
# These values to be overridden from arguments
self.rootpath = None
self.project = None
self.logfile = None
self.msock = None
self.help = None
# Create the help window
if os.path.exists(netgen_script_dir + '/netgen_help.txt'):
self.help = HelpWindow(self, fontsize = fontsize)
with io.StringIO() as buf, contextlib.redirect_stdout(buf):
self.help.add_pages_from_file('lvs_help.txt')
message = buf.getvalue()
# Set the help display to the first page
self.help.page(0)
# Variables used by option menus and other stuff
self.project = "(no selection)"
self.layout = "(default)"
self.schematic = "(default)"
self.tech = "(none)"
self.lvs_setup = ''
self.lvsdata = {}
# Root window title
self.root.title('LVS Manager')
self.root.option_add('*tearOff', 'FALSE')
self.pack(side = 'top', fill = 'both', expand = 'true')
pane = tkinter.PanedWindow(self, orient = 'vertical', sashrelief='groove', sashwidth=6)
pane.pack(side = 'top', fill = 'both', expand = 'true')
self.toppane = ttk.Frame(pane)
self.botpane = ttk.Frame(pane)
# Get username
if 'username' in self.prefs:
username = self.prefs['username']
else:
username = os.environ['USER']
# Label with the user
self.toppane.title_frame = ttk.Frame(self.toppane)
self.toppane.title_frame.pack(side = 'top', fill = 'x')
self.toppane.title_frame.title = ttk.Label(self.toppane.title_frame, text='User:', style = 'red.TLabel')
self.toppane.title_frame.user = ttk.Label(self.toppane.title_frame, text=username, style = 'blue.TLabel')
self.toppane.title_frame.title.grid(column=0, row=0, ipadx = 5)
self.toppane.title_frame.user.grid(column=1, row=0, ipadx = 5)
self.toppane.title2_frame = ttk.Frame(self.toppane)
self.toppane.title2_frame.pack(side = 'top', fill = 'x')
self.toppane.title2_frame.project_label = ttk.Label(self.toppane.title2_frame, text="Project:",
style = 'title.TLabel')
self.toppane.title2_frame.project_label.grid(column=0, row=0, ipadx = 5)
# New project select button
self.toppane.title2_frame.project_select = ttk.Button(self.toppane.title2_frame,
text=self.project, style='normal.TButton', command=self.choose_project)
self.toppane.title2_frame.project_select.grid(column=1, row=0, ipadx = 5)
tooltip.ToolTip(self.toppane.title2_frame.project_select,
text = "Select new project")
# Show path to project
self.toppane.title2_frame.path_label = ttk.Label(self.toppane.title2_frame, text=self.project,
style = 'normal.TLabel')
self.toppane.title2_frame.path_label.grid(column=2, row=0, ipadx = 5, padx = 10)
# Show top-level layout cellname with select button. Initial cell name is the top-level cell.
self.toppane.title2_frame.tech_label = ttk.Label(self.toppane.title2_frame, text="Technology setup:",
style = 'title.TLabel')
self.toppane.title2_frame.tech_label.grid(column=3, row=0, ipadx = 5)
self.toppane.title2_frame.tech_select = ttk.Button(self.toppane.title2_frame,
text=self.tech, style='normal.TButton', command=self.choose_tech)
self.toppane.title2_frame.tech_select.grid(column=4, row=0, ipadx = 3, padx = 3)
self.toppane.title2_frame.layout_label = ttk.Label(self.toppane.title2_frame, text="Layout:",
style = 'title.TLabel')
self.toppane.title2_frame.layout_label.grid(column=0, row=1, ipadx = 5)
self.toppane.title2_frame.layout_select = ttk.Button(self.toppane.title2_frame,
text=self.layout, style='normal.TButton', command=self.choose_layout)
self.toppane.title2_frame.layout_select.grid(column=1, row=1, ipadx = 3, padx = 3)
# Show top-level schematic cellname with select button. Initial cell name is the top-level cell.
self.toppane.title2_frame.schem_label = ttk.Label(self.toppane.title2_frame, text="Schematic:",
style = 'title.TLabel')
self.toppane.title2_frame.schem_label.grid(column=3, row=1, ipadx = 5)
self.toppane.title2_frame.schem_select = ttk.Button(self.toppane.title2_frame,
text=self.schematic, style='normal.TButton', command=self.choose_netlist)
self.toppane.title2_frame.schem_select.grid(column=4, row=1, ipadx = 3, padx = 3)
tooltip.ToolTip(self.toppane.title2_frame.project_select,
text = "Select new project")
tooltip.ToolTip(self.toppane.title2_frame.layout_select,
text = "Select a layout subcirucit to compare")
tooltip.ToolTip(self.toppane.title2_frame.schem_select,
text = "Select a schematic subcirucit to compare")
#---------------------------------------------
ttk.Separator(self.toppane, orient='horizontal').pack(side = 'top', fill = 'x')
#---------------------------------------------
# Create listbox of Circuit1 vs. Circuit2 results
height = 10
self.toppane.lvsreport = TreeViewSplit(self.toppane, fontsize = fontsize)
self.toppane.lvsreport.populate("Layout:", [], "Schematic:", [],
[["Run", True, self.run_lvs],
# ["Find", True, self.findrecord]
], height = height)
self.toppane.lvsreport.set_title("Line")
self.toppane.lvsreport.pack(side = 'top', fill = 'both', expand = 'true')
tooltip.ToolTip(self.toppane.lvsreport.get_button(0), text="Run LVS")
#---------------------------------------------
# ttk.Separator(self, orient='horizontal').grid(column=0, row=3, sticky='ew')
#---------------------------------------------
# Add a text window below the project name to capture output. Redirect
# print statements to it.
self.botpane.console = ttk.Frame(self.botpane)
self.botpane.console.pack(side = 'top', fill = 'both', expand = 'true')
self.text_box = ConsoleText(self.botpane.console, wrap='word', height = 4)
self.text_box.pack(side='left', fill='both', expand='true')
console_scrollbar = ttk.Scrollbar(self.botpane.console)
console_scrollbar.pack(side='right', fill='y')
# attach console to scrollbar
self.text_box.config(yscrollcommand = console_scrollbar.set)
console_scrollbar.config(command = self.text_box.yview)
# Add button bar at the bottom of the window
self.botpane.bbar = ttk.Frame(self.botpane)
self.botpane.bbar.pack(side = 'top', fill = 'x')
# Define the "quit" button and action
self.botpane.bbar.quit_button = ttk.Button(self.botpane.bbar, text='Quit', command=self.on_quit,
style = 'normal.TButton')
self.botpane.bbar.quit_button.grid(column=0, row=0, padx = 5)
# Define help button
if self.help:
self.botpane.bbar.help_button = ttk.Button(self.botpane.bbar, text='Help',
command=self.help.open, style = 'normal.TButton')
self.botpane.bbar.help_button.grid(column = 2, row = 0, padx = 5)
tooltip.ToolTip(self.botpane.bbar.help_button, text = "Show help window")
# Add the panes once the internal geometry is known.
pane.add(self.toppane)
pane.add(self.botpane)
pane.paneconfig(self.toppane, stretch='first')
# Redirect stdout and stderr to the console as the last thing to do. . .
# Otherwise errors in the GUI get sucked into the void.
self.stdout = sys.stdout
self.stderr = sys.stderr
sys.stdout = ConsoleText.StdoutRedirector(self.text_box)
sys.stderr = ConsoleText.StderrRedirector(self.text_box)
if message:
print(message)
def logprint(self, message, doflush=False):
if self.logfile:
self.logfile.buffer.write(message.encode('utf-8'))
self.logfile.buffer.write('\n'.encode('utf-8'))
if doflush:
self.logfile.flush()
def printout(self, output):
# Generate output
if not output:
return
outlines = output.splitlines()
for line in outlines:
try:
print(line)
except TypeError:
line = line.decode('utf-8')
pritn(line)
def printwarn(self, output):
# Check output for warning or error
if not output:
return 0
warnrex = re.compile('.*warning', re.IGNORECASE)
errrex = re.compile('.*error', re.IGNORECASE)
errors = 0
outlines = output.splitlines()
for line in outlines:
try:
wmatch = warnrex.match(line)
except TypeError:
line = line.decode('utf-8')
wmatch = warnrex.match(line)
ematch = errrex.match(line)
if ematch:
errors += 1
if ematch or wmatch:
print(line)
return errors
def choose_tech(self):
try:
project_path = self.rootpath
initdirname = self.rootpath + '/tech',
except:
print('Must choose a project first.')
return
techname = filedialog.askopenfilename(multiple=False,
initialdir = initdirname,
filetypes = (("Tcl script", "*.tcl"),("All Files","*.*")),
title = "Choose a netgen technology setup script.")
if techname != '':
print("Selected technology setup script " + techname)
techbase = os.path.split(techname)[1]
self.tech = os.path.splitext(techbase)[0]
self.lvs_setup = techname
self.toppane.title2_frame.layout_select.config(text = self.tech)
def choose_layout(self):
try:
project_path = self.rootpath
initdirname = self.rootpath + '/layout',
except:
print('Must choose a project first.')
return
cellname = filedialog.askopenfilename(multiple=False,
initialdir = initdirname,
filetypes = (("Magic layout", "*.mag"),("All Files","*.*")),
title = "Choose a layout cell to compare.")
if cellname != '':
print("Selected compare cell " + cellname)
cellbase = os.path.split(cellname)[1]
self.layout = os.path.splitext(cellbase)[0]
self.toppane.title2_frame.layout_select.config(text = self.layout)
def choose_netlist(self):
try:
project_path = self.rootpath
initdirname = self.rootpath + '/netlist/' + self.project + '.spi'
except:
print('Must choose a project first.')
return
cellname = filedialog.askopenfilename(multiple=False,
initialdir = initdirname,
filetypes = (("Spice netlist", "*.spi"),("Verilog netlist", "*.v")),
title = "Choose a netlist to compare.")
if cellname != '':
print("Selected compare cell " + cellname)
cellbase = os.path.split(cellname)[1]
self.schematic = os.path.splitext(cellbase)[0]
self.toppane.title2_frame.schem_select.config(text = self.schematic)
fileext = os.path.splitext(cellbase)[1]
if fileext == '.v':
self.toppane.title2_frame.schem_label.config(text = 'Verilog netlist:')
elif fileext == '.sp' or fileext == '.spi' or fileext == '.spice' or fileext == '.spc' or fileext == '.ckt':
self.toppane.title2_frame.schem_label.config(text = 'SPICE netlist:')
elif fileext == '.cdl':
self.toppane.title2_frame.schem_label.config(text = 'CDL netlist:')
else:
self.toppane.title2_frame.schem_label.config(text = 'Unknown netlist:')
def choose_project(self):
project = filedialog.askdirectory(initialdir = os.getcwd(),
title = "Find a project.")
if project != '':
print("Selected project " + str(project))
result = self.set_project(project)
def set_project(self, rootpath, project_name=None):
# Check if rootpath is valid. For LVS, there should be subdirectories
# "layout/" and "netlist/" or "verilog/".
haslay = os.path.isdir(rootpath + '/layout')
hasvlog = os.path.isdir(rootpath + '/verilog')
hasnet = os.path.isdir(rootpath + '/netlist')
if not haslay or not (hasvlog or hasnet):
if not haslay:
print("Project path has no layout (/layout) subdirectory.")
if not (hasvlog or hasnet):
print("Project path has no verilog (/verilog), or netlist (/netlist) subdirectory.")
# Continue anyway; assume that netlists will be selected manually
if self.logfile:
self.logfile.close()
self.logfile = None
if not project_name:
project = os.path.split(rootpath)[1]
else:
project = project_name
if self.project != project:
self.rootpath = rootpath
self.project = project
# Clear out old project data
self.toppane.lvsreport.repopulate([], [])
# Close any open logfile.
if self.logfile:
self.logfile.close()
self.logfile = None
# Put new log file called 'lvs.log' in the mag/ subdirectory
self.logfile = open(rootpath + '/layout/lvs.log', 'w')
# Print some initial information to the logfile.
self.logprint('Starting new log file ' + datetime.datetime.now().strftime('%c'),
doflush=True)
# Update project button
self.toppane.title2_frame.project_select.config(text = self.project)
self.toppane.title2_frame.path_label.config(text = self.rootpath)
# Cell name is the same as project name initially
self.layout = self.project
self.schematic = self.project
self.toppane.title2_frame.layout_select.config(text = self.layout)
self.toppane.title2_frame.schem_select.config(text = self.schematic)
# Update schematic button
if os.path.isfile(rootpath + '/verilog/' + self.schematic + '.v'):
self.toppane.title2_frame.schem_label.config(text = 'Verilog netlist:')
else:
self.toppane.title2_frame.schem_label.config(text = 'Schematic netlist:')
# If there is a comparison file that post-dates both netlists, load it.
self.check_lvs()
return True
def check_layout_out_of_date(self, spipath, layoutpath):
# Check if a netlist (spipath) is out-of-date relative to the layouts
# (layoutpath). Need to read the netlist and check all of the subcells.
need_capture = False
if not os.path.isfile(spipath):
return True
if os.path.isfile(layoutpath):
spi_statbuf = os.stat(spipath)
lay_statbuf = os.stat(layoutpath)
if spi_statbuf.st_mtime < lay_statbuf.st_mtime:
# netlist exists but is out-of-date
need_capture = True
else:
# only found that the top-level-layout is older than the
# netlist. Now need to read the netlist, find all subcircuits,
# and check those dates, too.
layoutdir = os.path.split(layoutpath)[0]
subrex = re.compile('^[^\*]*[ \t]*.subckt[ \t]+([^ \t]+).*$', re.IGNORECASE)
with open(spipath, 'r') as ifile:
duttext = ifile.read()
dutlines = duttext.replace('\n+', ' ').splitlines()
for line in dutlines:
lmatch = subrex.match(line)
if lmatch:
subname = lmatch.group(1)
sublayout = layoutdir + '/' + subname + '.mag'
# subcircuits that cannot be found in the current directory are
# assumed to be library components and therefore never out-of-date.
if os.path.exists(sublayout):
sub_statbuf = os.stat(sublayout)
if spi_statbuf.st_mtime < lay_statbuf.st_mtime:
# netlist exists but is out-of-date
need_capture = True
break
return need_capture
def check_schematic_out_of_date(self, spipath, schempath):
# Check if a netlist (spipath) is out-of-date relative to the schematics
# (schempath). Need to read the netlist and check all of the subcells.
need_capture = False
if not os.path.isfile(spipath):
return True
if os.path.isfile(schempath):
spi_statbuf = os.stat(spipath)
sch_statbuf = os.stat(schempath)
if spi_statbuf.st_mtime < sch_statbuf.st_mtime:
# netlist exists but is out-of-date
need_capture = True
else:
# only found that the top-level-schematic is older than the
# netlist. Now need to read the netlist, find all subcircuits,
# and check those dates, too.
schemdir = os.path.split(schempath)[0]
subrex = re.compile('^[^\*]*[ \t]*.subckt[ \t]+([^ \t]+).*$', re.IGNORECASE)
with open(spipath, 'r') as ifile:
duttext = ifile.read()
dutlines = duttext.replace('\n+', ' ').splitlines()
for line in dutlines:
lmatch = subrex.match(line)
if lmatch:
subname = lmatch.group(1)
# NOTE: Electric uses library:cell internally to track libraries,
# and maps the ":" to "__" in the netlist. Not entirely certain that
# the double-underscore uniquely identifies the library:cell. . .
librex = re.compile('(.*)__(.*)', re.IGNORECASE)
lmatch = librex.match(subname)
if lmatch:
elecpath = os.path.split(os.path.split(schempath)[0])[0]
libname = lmatch.group(1)
subschem = elecpath + '/' + libname + '.delib/' + lmatch.group(2) + '.sch'
else:
libname = {}
subschem = schemdir + '/' + subname + '.sch'
# subcircuits that cannot be found in the current directory are
# assumed to be library components and therefore never out-of-date.
if os.path.exists(subschem):
sub_statbuf = os.stat(subschem)
if spi_statbuf.st_mtime < sub_statbuf.st_mtime:
# netlist exists but is out-of-date
need_capture = True
break
# mapping of characters to what's allowed in SPICE makes finding
# the associated schematic file a bit difficult. Requires wild-card
# searching.
elif libname:
restr = lmatch.group(2) + '.sch'
restr = restr.replace('.', '\.')
restr = restr.replace('_', '.')
schrex = re.compile(restr, re.IGNORECASE)
libpath = elecpath + '/' + libname + '.delib'
if os.path.exists(libpath):
liblist = os.listdir(libpath)
for file in liblist:
lmatch = schrex.match(file)
if lmatch:
subschem = libpath + '/' + file
sub_statbuf = os.stat(subschem)
if spi_statbuf.st_mtime < sch_statbuf.st_mtime:
# netlist exists but is out-of-date
need_capture = True
break
return need_capture
def check_lvs(self):
# If both netlists exist, and comp.json is more recent than both, then
# load LVS results from comp.json
project_path = self.rootpath
project_name = self.project
layout_path = project_path + '/layout/' + project_name + '.spc'
net_path = project_path + '/netlist/' + project_name + '.spi'
comp_path = project_path + '/layout/comp.json'
if os.path.exists(layout_path) and os.path.exists(net_path) and os.path.exists(comp_path):
magtime = os.stat(layout_path).st_mtime
schemtime = os.stat(net_path).st_mtime
comptime = os.stat(comp_path).st_mtime
if comptime > magtime and comptime > schemtime:
print("Loading LVS results from file.")
self.generate(comp_path)
def generate_layout_netlist(self, layout_path, layout_src, project_path):
# Does layout netlist exist and is it current?
if self.check_layout_out_of_date(layout_path, layout_src):
print('Generating layout netlist.')
self.update_idletasks()
mproc = subprocess.Popen(['magic', '-dnull', '-noconsole',
self.layout], stdin = subprocess.PIPE, stdout = subprocess.PIPE,
stderr = subprocess.PIPE, cwd = project_path + '/layout',
universal_newlines = True)
mproc.stdin.write("select top cell\n")
mproc.stdin.write("expand\n")
mproc.stdin.write("extract all\n")
mproc.stdin.write("ext2spice hierarchy on\n")
mproc.stdin.write("ext2spice format ngspice\n")
mproc.stdin.write("ext2spice scale off\n")
mproc.stdin.write("ext2spice renumber off\n")
mproc.stdin.write("ext2spice subcircuit top auto\n")
mproc.stdin.write("ext2spice cthresh infinite\n")
mproc.stdin.write("ext2spice rthresh infinite\n")
mproc.stdin.write("ext2spice blackbox on\n")
mproc.stdin.write("ext2spice -o " + self.layout + ".spi\n")
mproc.stdin.write("quit -noprompt\n")
magicout = mproc.communicate()[0]
self.printwarn(magicout)
if mproc.returncode != 0:
print('Failure to generate new layout netlist.')
return False
# Move .spi netlist to project_dir/netlist/lvs/
shutil.move(project_path + '/layout/' + self.layout + '.spi', layout_path)
# Remove extraction files
for file in os.listdir(project_path + '/layout'):
if os.path.splitext(file)[1] == '.ext':
os.remove(project_path + '/layout/' + file)
else:
print('Layout netlist is up-to-date, not regenerating.')
return True
def run_lvs(self, value):
# "value" is ignored (?)
# Check if netlists exist and are current; otherwise create them.
# Then run LVS.
project_path = self.rootpath
project_name = self.project
comp_path = project_path + '/layout/comp.json'
has_vlog = False
vlog_path = project_path + '/verilog/' + project_name + '.v'
layout_path = project_path + '/netlist/lvs/' + self.layout + '.spi'
net_path = project_path + '/netlist/schem/' + self.schematic + '.spi'
layout_src = project_path + '/layout/' + self.layout + '.mag'
# Does the setup file exist (this is optional)?
if self.lvs_setup != '' and not os.path.isfile('setup.tcl'):
print('No technology setup file selected.')
# Does schematic netlist exist?
if not os.path.isfile(vlog_path) and not os.path.isfile(net_path):
print('Error: No schematic netlist or verilog netlist.')
return
# Does LVS netlist subdirectory exist?
if not os.path.exists(project_path + '/netlist/lvs'):
os.makedirs(project_path + '/netlist/lvs')
# Does layout netlist exist and is it current?
if not self.generate_layout_netlist(layout_path, layout_src, project_path):
return False
# Final check
if not os.path.isfile(layout_path):
print('Error: No netlist generated from magic.')
return
else:
# Read in netlist and convert commas from [X,Y] arrays to vertical bars
# as something that can be converted back as necessary. ngspice treats
# commas as special characters for some reason.
with open(layout_path) as ifile:
spitext = ifile.read()
# spilines = spitext.replace(',', '|')
# with open(layout_path, 'w') as ofile:
# ofile.write(spilines)
# Check the netlist to see if the cell to match is a subcircuit. If
# not, then assume it is the top level.
is_subckt = False
subrex = re.compile('^[^\*]*[ \t]*.subckt[ \t]+([^ \t]+).*$', re.IGNORECASE)
dutlines = spitext.replace('\n+', ' ').splitlines()
for line in dutlines:
lmatch = subrex.match(line)
if lmatch:
subname = lmatch.group(1)
if subname == self.layout:
is_subckt = True
break
if is_subckt:
layout_arg = layout_path + ' ' + self.layout
layout_text = '"' + layout_arg + '"'
else:
layout_arg = layout_path
layout_text = layout_arg
if has_vlog:
schem_arg = vlog_path + ' ' + self.schematic
else:
schem_arg = net_path + ' ' + self.schematic
schem_text = '"' + schem_arg + '"'
# Remove any previous comparison output file
comp_out_path = os.path.splitext(comp_path)[0] + '.out'
if os.path.exists(comp_out_path):
os.remove(comp_out_path)
# Run netgen as subprocess
print('Running: netgen -batch lvs ' + layout_text +
' ' + schem_text + ' ' + self.lvs_setup + ' ' + comp_out_path +
' -json -blackbox')
# Note: Because arguments to subprocess are list items, the {filename cell}
# pair does *not* have to be quoted or braced. Doing so causes a parse
# error.
self.lvsproc = subprocess.Popen(['netgen', '-batch', 'lvs',
layout_arg, schem_arg,
self.lvs_setup, comp_out_path, '-json', '-blackbox'],
cwd=project_path + '/layout',
stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0)
# This is largely unnecessary as netgen usually runs to completion very quickly.
self.watchclock(comp_path)
def watchclock(self, filename):
if self.lvsproc == None:
return
lvs_status = self.lvsproc.poll()
sresult = select.select([self.lvsproc.stdout, self.lvsproc.stderr], [], [], 0)[0]
if self.lvsproc.stdout in sresult:
outstring = self.lvsproc.stdout.readline().decode().strip()
self.logprint(outstring, doflush=True)
print(outstring)
elif self.lvsproc.stderr in sresult:
errstring = self.lvsproc.stderr.readline().decode().strip()
self.logprint(errstring, doflush = True)
print(errstring, file=sys.stderr)
if lvs_status != None:
print("netgen LVS exited with status " + str(lvs_status))
self.lvsproc = None
if lvs_status != 0:
print('Errors encountered in LVS.')
self.logprint('Errors in LVS, lvs status = ' + str(lvs_status), doflush=True)
# Done; now read comp.json and fill the treeview listbox.
self.generate(filename)
else:
self.after(500, lambda: self.watchclock(filename))
# Generate display from "comp.out" file (json file now preferred)
def generate_orig(self, lvspath):
lefttext = []
righttext = []
print("Reading LVS output file " + lvspath)
if os.path.exists(lvspath):
with open(lvspath, 'r') as ifile:
lvslines = ifile.read().splitlines()
for line in lvslines:
if '|' in line:
# parts = line.split('|')
# lefttext.append(parts[0])
# righttext.append(parts[1])
lefttext.append(line[0:42].strip())
righttext.append(line[44:].strip())
else:
lefttext.append(line)
righttext.append('')
# Populate treeview with text
self.toppane.lvsreport.repopulate(lefttext, righttext)
else:
print("Error: No output file generated from LVS.")
# Generate output from LVS report JSON file comp.json
def generate(self, lvspath):
lefttext = []
righttext = []
print("Reading LVS output file " + lvspath)
if os.path.exists(lvspath):
with open(lvspath, 'r') as ifile:
self.lvsdata = json.load(ifile)
# Populate treeview with text
self.toppane.lvsreport.json_repopulate(self.lvsdata)
else:
print("Error: No output file generated from LVS.")
def findrecord(self, value):
print("Unimplemented function")
def findrecord_test(self, value):
# Check if socket is defined; if not, attempt to open one
if not self.msock:
try:
self.msock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
except:
print("No response from layout tool.")
if self.msock:
self.msock.connect(("0.0.0.0", 12946))
self.msock.setblocking(False)
if self.msock:
# Pull name of net or device from 'value'
# This is a test:
self.msock.sendall(b'box 0 0 100 100\r\n')
if __name__ == '__main__':
options = []
arguments = []
for item in sys.argv[1:]:
if item.find('-', 0) == 0:
options.append(item)
else:
arguments.append(item)
root = tkinter.Tk()
app = LVSManager(root)
if arguments:
if len(arguments) >= 2:
app.set_project(arguments[0], project_name=arguments[1])
else:
app.set_project(arguments[0])
root.mainloop()
+819
View File
@@ -0,0 +1,819 @@
#!/bin/env python3
#
#--------------------------------------------------------
# LVS Manager GUI.
#
# This is a Python tkinter script that handles the
# process of running LVS and interpreting results.
#
#--------------------------------------------------------
# Written by Tim Edwards
# efabless, inc.
# Version 1. November 30, 2016
# Version 2. March 6, 2017. Reads JSON format output
# Version 3. April 25, 2018. Handles layout vs. verilog
#--------------------------------------------------------
import io
import os
import re
import sys
import json
import shutil
import signal
import socket
import select
import datetime
import contextlib
import subprocess
import tkinter
from tkinter import ttk
from tkinter import filedialog
import tksimpledialog
import tooltip
from consoletext import ConsoleText
from helpwindow import HelpWindow
from treeviewsplit import TreeViewSplit
# User preferences file (if it exists)
prefsfile = '~/.profile/prefs.json'
netgen_script_dir = 'SUBST_SCRIPT_DIR'
#------------------------------------------------------
# Simple dialog for confirming quit
#------------------------------------------------------
class ConfirmDialog(tksimpledialog.Dialog):
def body(self, master, warning, seed):
if warning:
ttk.Label(master, text=warning, wraplength=500).grid(row = 0, columnspan = 2, sticky = 'wns')
return self
def apply(self):
return 'okay'
#------------------------------------------------------
# Main class for this application
#------------------------------------------------------
class LVSManager(ttk.Frame):
"""LVS Manager GUI."""
def __init__(self, parent, *args, **kwargs):
ttk.Frame.__init__(self, parent, *args, **kwargs)
self.root = parent
self.init_gui()
parent.protocol("WM_DELETE_WINDOW", self.on_quit)
def on_quit(self):
"""Exits program."""
if self.msock:
self.msock.close()
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('bg.TFrame', background='gray40')
s.configure('italic.TLabel', font=('Helvetica', fontsize, 'italic'))
s.configure('title.TLabel', font=('Helvetica', fontsize, 'bold italic'),
foreground = 'brown', anchor = 'center')
s.configure('normal.TLabel', font=('Helvetica', fontsize))
s.configure('red.TLabel', font=('Helvetica', fontsize), foreground = 'red')
s.configure('green.TLabel', font=('Helvetica', fontsize), foreground = 'green3')
s.configure('blue.TLabel', font=('Helvetica', fontsize), foreground = 'blue')
s.configure('normal.TButton', font=('Helvetica', fontsize),
border = 3, relief = 'raised')
s.configure('red.TButton', font=('Helvetica', fontsize), foreground = 'red',
border = 3, relief = 'raised')
s.configure('green.TButton', font=('Helvetica', fontsize), foreground = 'green3',
border = 3, relief = 'raised')
s.configure('blue.TButton', font=('Helvetica', fontsize), foreground = 'blue',
border = 3, relief = 'raised')
s.configure('redtitle.TButton', font=('Helvetica', fontsize, 'bold italic'),
foreground = 'red', border = 3, relief = 'raised')
s.configure('bluetitle.TButton', font=('Helvetica', fontsize, 'bold italic'),
foreground = 'blue', border = 3, relief = 'raised')
# These values to be overridden from arguments
self.rootpath = None
self.project = None
self.logfile = None
self.msock = None
self.help = None
# Create the help window
if os.path.exists(netgen_script_dir + '/netgen_help.txt'):
self.help = HelpWindow(self, fontsize = fontsize)
with io.StringIO() as buf, contextlib.redirect_stdout(buf):
self.help.add_pages_from_file('lvs_help.txt')
message = buf.getvalue()
# Set the help display to the first page
self.help.page(0)
# Variables used by option menus and other stuff
self.project = "(no selection)"
self.layout = "(default)"
self.schematic = "(default)"
self.tech = "(none)"
self.lvs_setup = ''
self.lvsdata = {}
# Root window title
self.root.title('LVS Manager')
self.root.option_add('*tearOff', 'FALSE')
self.pack(side = 'top', fill = 'both', expand = 'true')
pane = tkinter.PanedWindow(self, orient = 'vertical', sashrelief='groove', sashwidth=6)
pane.pack(side = 'top', fill = 'both', expand = 'true')
self.toppane = ttk.Frame(pane)
self.botpane = ttk.Frame(pane)
# Get username
if 'username' in self.prefs:
username = self.prefs['username']
else:
username = os.environ['USER']
# Label with the user
self.toppane.title_frame = ttk.Frame(self.toppane)
self.toppane.title_frame.pack(side = 'top', fill = 'x')
self.toppane.title_frame.title = ttk.Label(self.toppane.title_frame, text='User:', style = 'red.TLabel')
self.toppane.title_frame.user = ttk.Label(self.toppane.title_frame, text=username, style = 'blue.TLabel')
self.toppane.title_frame.title.grid(column=0, row=0, ipadx = 5)
self.toppane.title_frame.user.grid(column=1, row=0, ipadx = 5)
self.toppane.title2_frame = ttk.Frame(self.toppane)
self.toppane.title2_frame.pack(side = 'top', fill = 'x')
self.toppane.title2_frame.project_label = ttk.Label(self.toppane.title2_frame, text="Project:",
style = 'title.TLabel')
self.toppane.title2_frame.project_label.grid(column=0, row=0, ipadx = 5)
# New project select button
self.toppane.title2_frame.project_select = ttk.Button(self.toppane.title2_frame,
text=self.project, style='normal.TButton', command=self.choose_project)
self.toppane.title2_frame.project_select.grid(column=1, row=0, ipadx = 5)
tooltip.ToolTip(self.toppane.title2_frame.project_select,
text = "Select new project")
# Show path to project
self.toppane.title2_frame.path_label = ttk.Label(self.toppane.title2_frame, text=self.project,
style = 'normal.TLabel')
self.toppane.title2_frame.path_label.grid(column=2, row=0, ipadx = 5, padx = 10)
# Show top-level layout cellname with select button. Initial cell name is the top-level cell.
self.toppane.title2_frame.tech_label = ttk.Label(self.toppane.title2_frame, text="Technology setup:",
style = 'title.TLabel')
self.toppane.title2_frame.tech_label.grid(column=3, row=0, ipadx = 5)
self.toppane.title2_frame.tech_select = ttk.Button(self.toppane.title2_frame,
text=self.tech, style='normal.TButton', command=self.choose_tech)
self.toppane.title2_frame.tech_select.grid(column=4, row=0, ipadx = 3, padx = 3)
self.toppane.title2_frame.layout_label = ttk.Label(self.toppane.title2_frame, text="Layout:",
style = 'title.TLabel')
self.toppane.title2_frame.layout_label.grid(column=0, row=1, ipadx = 5)
self.toppane.title2_frame.layout_select = ttk.Button(self.toppane.title2_frame,
text=self.layout, style='normal.TButton', command=self.choose_layout)
self.toppane.title2_frame.layout_select.grid(column=1, row=1, ipadx = 3, padx = 3)
# Show top-level schematic cellname with select button. Initial cell name is the top-level cell.
self.toppane.title2_frame.schem_label = ttk.Label(self.toppane.title2_frame, text="Schematic:",
style = 'title.TLabel')
self.toppane.title2_frame.schem_label.grid(column=3, row=1, ipadx = 5)
self.toppane.title2_frame.schem_select = ttk.Button(self.toppane.title2_frame,
text=self.schematic, style='normal.TButton', command=self.choose_netlist)
self.toppane.title2_frame.schem_select.grid(column=4, row=1, ipadx = 3, padx = 3)
tooltip.ToolTip(self.toppane.title2_frame.project_select,
text = "Select new project")
tooltip.ToolTip(self.toppane.title2_frame.layout_select,
text = "Select a layout subcirucit to compare")
tooltip.ToolTip(self.toppane.title2_frame.schem_select,
text = "Select a schematic subcirucit to compare")
#---------------------------------------------
ttk.Separator(self.toppane, orient='horizontal').pack(side = 'top', fill = 'x')
#---------------------------------------------
# Create listbox of Circuit1 vs. Circuit2 results
height = 10
self.toppane.lvsreport = TreeViewSplit(self.toppane, fontsize = fontsize)
self.toppane.lvsreport.populate("Layout:", [], "Schematic:", [],
[["Run", True, self.run_lvs],
# ["Find", True, self.findrecord]
], height = height)
self.toppane.lvsreport.set_title("Line")
self.toppane.lvsreport.pack(side = 'top', fill = 'both', expand = 'true')
tooltip.ToolTip(self.toppane.lvsreport.get_button(0), text="Run LVS")
#---------------------------------------------
# ttk.Separator(self, orient='horizontal').grid(column=0, row=3, sticky='ew')
#---------------------------------------------
# Add a text window below the project name to capture output. Redirect
# print statements to it.
self.botpane.console = ttk.Frame(self.botpane)
self.botpane.console.pack(side = 'top', fill = 'both', expand = 'true')
self.text_box = ConsoleText(self.botpane.console, wrap='word', height = 4)
self.text_box.pack(side='left', fill='both', expand='true')
console_scrollbar = ttk.Scrollbar(self.botpane.console)
console_scrollbar.pack(side='right', fill='y')
# attach console to scrollbar
self.text_box.config(yscrollcommand = console_scrollbar.set)
console_scrollbar.config(command = self.text_box.yview)
# Add button bar at the bottom of the window
self.botpane.bbar = ttk.Frame(self.botpane)
self.botpane.bbar.pack(side = 'top', fill = 'x')
# Define the "quit" button and action
self.botpane.bbar.quit_button = ttk.Button(self.botpane.bbar, text='Quit', command=self.on_quit,
style = 'normal.TButton')
self.botpane.bbar.quit_button.grid(column=0, row=0, padx = 5)
# Define help button
if self.help:
self.botpane.bbar.help_button = ttk.Button(self.botpane.bbar, text='Help',
command=self.help.open, style = 'normal.TButton')
self.botpane.bbar.help_button.grid(column = 2, row = 0, padx = 5)
tooltip.ToolTip(self.botpane.bbar.help_button, text = "Show help window")
# Add the panes once the internal geometry is known.
pane.add(self.toppane)
pane.add(self.botpane)
pane.paneconfig(self.toppane, stretch='first')
# Redirect stdout and stderr to the console as the last thing to do. . .
# Otherwise errors in the GUI get sucked into the void.
self.stdout = sys.stdout
self.stderr = sys.stderr
sys.stdout = ConsoleText.StdoutRedirector(self.text_box)
sys.stderr = ConsoleText.StderrRedirector(self.text_box)
if message:
print(message)
def logprint(self, message, doflush=False):
if self.logfile:
self.logfile.buffer.write(message.encode('utf-8'))
self.logfile.buffer.write('\n'.encode('utf-8'))
if doflush:
self.logfile.flush()
def printout(self, output):
# Generate output
if not output:
return
outlines = output.splitlines()
for line in outlines:
try:
print(line)
except TypeError:
line = line.decode('utf-8')
pritn(line)
def printwarn(self, output):
# Check output for warning or error
if not output:
return 0
warnrex = re.compile('.*warning', re.IGNORECASE)
errrex = re.compile('.*error', re.IGNORECASE)
errors = 0
outlines = output.splitlines()
for line in outlines:
try:
wmatch = warnrex.match(line)
except TypeError:
line = line.decode('utf-8')
wmatch = warnrex.match(line)
ematch = errrex.match(line)
if ematch:
errors += 1
if ematch or wmatch:
print(line)
return errors
def choose_tech(self):
try:
project_path = self.rootpath
initdirname = self.rootpath + '/tech',
except:
print('Must choose a project first.')
return
techname = filedialog.askopenfilename(multiple=False,
initialdir = initdirname,
filetypes = (("Tcl script", "*.tcl"),("All Files","*.*")),
title = "Choose a netgen technology setup script.")
if techname != '':
print("Selected technology setup script " + techname)
techbase = os.path.split(techname)[1]
self.tech = os.path.splitext(techbase)[0]
self.lvs_setup = techname
self.toppane.title2_frame.layout_select.config(text = self.tech)
def choose_layout(self):
try:
project_path = self.rootpath
initdirname = self.rootpath + '/layout',
except:
print('Must choose a project first.')
return
cellname = filedialog.askopenfilename(multiple=False,
initialdir = initdirname,
filetypes = (("Magic layout", "*.mag"),("All Files","*.*")),
title = "Choose a layout cell to compare.")
if cellname != '':
print("Selected compare cell " + cellname)
cellbase = os.path.split(cellname)[1]
self.layout = os.path.splitext(cellbase)[0]
self.toppane.title2_frame.layout_select.config(text = self.layout)
def choose_netlist(self):
try:
project_path = self.rootpath
initdirname = self.rootpath + '/netlist/' + self.project + '.spi'
except:
print('Must choose a project first.')
return
cellname = filedialog.askopenfilename(multiple=False,
initialdir = initdirname,
filetypes = (("Spice netlist", "*.spi"),("Verilog netlist", "*.v")),
title = "Choose a netlist to compare.")
if cellname != '':
print("Selected compare cell " + cellname)
cellbase = os.path.split(cellname)[1]
self.schematic = os.path.splitext(cellbase)[0]
self.toppane.title2_frame.schem_select.config(text = self.schematic)
fileext = os.path.splitext(cellbase)[1]
if fileext == '.v':
self.toppane.title2_frame.schem_label.config(text = 'Verilog netlist:')
elif fileext == '.sp' or fileext == '.spi' or fileext == '.spice' or fileext == '.spc' or fileext == '.ckt':
self.toppane.title2_frame.schem_label.config(text = 'SPICE netlist:')
elif fileext == '.cdl':
self.toppane.title2_frame.schem_label.config(text = 'CDL netlist:')
else:
self.toppane.title2_frame.schem_label.config(text = 'Unknown netlist:')
def choose_project(self):
project = filedialog.askdirectory(initialdir = os.getcwd(),
title = "Find a project.")
if project != '':
print("Selected project " + str(project))
result = self.set_project(project)
def set_project(self, rootpath, project_name=None):
# Check if rootpath is valid. For LVS, there should be subdirectories
# "layout/" and "netlist/" or "verilog/".
haslay = os.path.isdir(rootpath + '/layout')
hasvlog = os.path.isdir(rootpath + '/verilog')
hasnet = os.path.isdir(rootpath + '/netlist')
if not haslay or not (hasvlog or hasnet):
if not haslay:
print("Project path has no layout (/layout) subdirectory.")
if not (hasvlog or hasnet):
print("Project path has no verilog (/verilog), or netlist (/netlist) subdirectory.")
# Continue anyway; assume that netlists will be selected manually
if self.logfile:
self.logfile.close()
self.logfile = None
if not project_name:
project = os.path.split(rootpath)[1]
else:
project = project_name
if self.project != project:
self.rootpath = rootpath
self.project = project
# Clear out old project data
self.toppane.lvsreport.repopulate([], [])
# Close any open logfile.
if self.logfile:
self.logfile.close()
self.logfile = None
# Put new log file called 'lvs.log' in the mag/ subdirectory
self.logfile = open(rootpath + '/layout/lvs.log', 'w')
# Print some initial information to the logfile.
self.logprint('Starting new log file ' + datetime.datetime.now().strftime('%c'),
doflush=True)
# Update project button
self.toppane.title2_frame.project_select.config(text = self.project)
self.toppane.title2_frame.path_label.config(text = self.rootpath)
# Cell name is the same as project name initially
self.layout = self.project
self.schematic = self.project
self.toppane.title2_frame.layout_select.config(text = self.layout)
self.toppane.title2_frame.schem_select.config(text = self.schematic)
# Update schematic button
if os.path.isfile(rootpath + '/verilog/' + self.schematic + '.v'):
self.toppane.title2_frame.schem_label.config(text = 'Verilog netlist:')
else:
self.toppane.title2_frame.schem_label.config(text = 'Schematic netlist:')
# If there is a comparison file that post-dates both netlists, load it.
self.check_lvs()
return True
def check_layout_out_of_date(self, spipath, layoutpath):
# Check if a netlist (spipath) is out-of-date relative to the layouts
# (layoutpath). Need to read the netlist and check all of the subcells.
need_capture = False
if not os.path.isfile(spipath):
return True
if os.path.isfile(layoutpath):
spi_statbuf = os.stat(spipath)
lay_statbuf = os.stat(layoutpath)
if spi_statbuf.st_mtime < lay_statbuf.st_mtime:
# netlist exists but is out-of-date
need_capture = True
else:
# only found that the top-level-layout is older than the
# netlist. Now need to read the netlist, find all subcircuits,
# and check those dates, too.
layoutdir = os.path.split(layoutpath)[0]
subrex = re.compile('^[^\*]*[ \t]*.subckt[ \t]+([^ \t]+).*$', re.IGNORECASE)
with open(spipath, 'r') as ifile:
duttext = ifile.read()
dutlines = duttext.replace('\n+', ' ').splitlines()
for line in dutlines:
lmatch = subrex.match(line)
if lmatch:
subname = lmatch.group(1)
sublayout = layoutdir + '/' + subname + '.mag'
# subcircuits that cannot be found in the current directory are
# assumed to be library components and therefore never out-of-date.
if os.path.exists(sublayout):
sub_statbuf = os.stat(sublayout)
if spi_statbuf.st_mtime < lay_statbuf.st_mtime:
# netlist exists but is out-of-date
need_capture = True
break
return need_capture
def check_schematic_out_of_date(self, spipath, schempath):
# Check if a netlist (spipath) is out-of-date relative to the schematics
# (schempath). Need to read the netlist and check all of the subcells.
need_capture = False
if not os.path.isfile(spipath):
return True
if os.path.isfile(schempath):
spi_statbuf = os.stat(spipath)
sch_statbuf = os.stat(schempath)
if spi_statbuf.st_mtime < sch_statbuf.st_mtime:
# netlist exists but is out-of-date
need_capture = True
else:
# only found that the top-level-schematic is older than the
# netlist. Now need to read the netlist, find all subcircuits,
# and check those dates, too.
schemdir = os.path.split(schempath)[0]
subrex = re.compile('^[^\*]*[ \t]*.subckt[ \t]+([^ \t]+).*$', re.IGNORECASE)
with open(spipath, 'r') as ifile:
duttext = ifile.read()
dutlines = duttext.replace('\n+', ' ').splitlines()
for line in dutlines:
lmatch = subrex.match(line)
if lmatch:
subname = lmatch.group(1)
# NOTE: Electric uses library:cell internally to track libraries,
# and maps the ":" to "__" in the netlist. Not entirely certain that
# the double-underscore uniquely identifies the library:cell. . .
librex = re.compile('(.*)__(.*)', re.IGNORECASE)
lmatch = librex.match(subname)
if lmatch:
elecpath = os.path.split(os.path.split(schempath)[0])[0]
libname = lmatch.group(1)
subschem = elecpath + '/' + libname + '.delib/' + lmatch.group(2) + '.sch'
else:
libname = {}
subschem = schemdir + '/' + subname + '.sch'
# subcircuits that cannot be found in the current directory are
# assumed to be library components and therefore never out-of-date.
if os.path.exists(subschem):
sub_statbuf = os.stat(subschem)
if spi_statbuf.st_mtime < sub_statbuf.st_mtime:
# netlist exists but is out-of-date
need_capture = True
break
# mapping of characters to what's allowed in SPICE makes finding
# the associated schematic file a bit difficult. Requires wild-card
# searching.
elif libname:
restr = lmatch.group(2) + '.sch'
restr = restr.replace('.', '\.')
restr = restr.replace('_', '.')
schrex = re.compile(restr, re.IGNORECASE)
libpath = elecpath + '/' + libname + '.delib'
if os.path.exists(libpath):
liblist = os.listdir(libpath)
for file in liblist:
lmatch = schrex.match(file)
if lmatch:
subschem = libpath + '/' + file
sub_statbuf = os.stat(subschem)
if spi_statbuf.st_mtime < sch_statbuf.st_mtime:
# netlist exists but is out-of-date
need_capture = True
break
return need_capture
def check_lvs(self):
# If both netlists exist, and comp.json is more recent than both, then
# load LVS results from comp.json
project_path = self.rootpath
project_name = self.project
layout_path = project_path + '/layout/' + project_name + '.spc'
net_path = project_path + '/netlist/' + project_name + '.spi'
comp_path = project_path + '/layout/comp.json'
if os.path.exists(layout_path) and os.path.exists(net_path) and os.path.exists(comp_path):
magtime = os.stat(layout_path).st_mtime
schemtime = os.stat(net_path).st_mtime
comptime = os.stat(comp_path).st_mtime
if comptime > magtime and comptime > schemtime:
print("Loading LVS results from file.")
self.generate(comp_path)
def generate_layout_netlist(self, layout_path, layout_src, project_path):
# Does layout netlist exist and is it current?
if self.check_layout_out_of_date(layout_path, layout_src):
print('Generating layout netlist.')
self.update_idletasks()
mproc = subprocess.Popen(['magic', '-dnull', '-noconsole',
self.layout], stdin = subprocess.PIPE, stdout = subprocess.PIPE,
stderr = subprocess.PIPE, cwd = project_path + '/layout',
universal_newlines = True)
mproc.stdin.write("select top cell\n")
mproc.stdin.write("expand\n")
mproc.stdin.write("extract all\n")
mproc.stdin.write("ext2spice hierarchy on\n")
mproc.stdin.write("ext2spice format ngspice\n")
mproc.stdin.write("ext2spice scale off\n")
mproc.stdin.write("ext2spice renumber off\n")
mproc.stdin.write("ext2spice subcircuit top auto\n")
mproc.stdin.write("ext2spice cthresh infinite\n")
mproc.stdin.write("ext2spice rthresh infinite\n")
mproc.stdin.write("ext2spice blackbox on\n")
mproc.stdin.write("ext2spice -o " + self.layout + ".spi\n")
mproc.stdin.write("quit -noprompt\n")
magicout = mproc.communicate()[0]
self.printwarn(magicout)
if mproc.returncode != 0:
print('Failure to generate new layout netlist.')
return False
# Move .spi netlist to project_dir/netlist/lvs/
shutil.move(project_path + '/layout/' + self.layout + '.spi', layout_path)
# Remove extraction files
for file in os.listdir(project_path + '/layout'):
if os.path.splitext(file)[1] == '.ext':
os.remove(project_path + '/layout/' + file)
else:
print('Layout netlist is up-to-date, not regenerating.')
return True
def run_lvs(self, value):
# "value" is ignored (?)
# Check if netlists exist and are current; otherwise create them.
# Then run LVS.
project_path = self.rootpath
project_name = self.project
comp_path = project_path + '/layout/comp.json'
has_vlog = False
vlog_path = project_path + '/verilog/' + project_name + '.v'
layout_path = project_path + '/netlist/lvs/' + self.layout + '.spi'
net_path = project_path + '/netlist/schem/' + self.schematic + '.spi'
layout_src = project_path + '/layout/' + self.layout + '.mag'
# Does the setup file exist (this is optional)?
if self.lvs_setup != '' and not os.path.isfile('setup.tcl'):
print('No technology setup file selected.')
# Does schematic netlist exist?
if not os.path.isfile(vlog_path) and not os.path.isfile(net_path):
print('Error: No schematic netlist or verilog netlist.')
return
# Does LVS netlist subdirectory exist?
if not os.path.exists(project_path + '/netlist/lvs'):
os.makedirs(project_path + '/netlist/lvs')
# Does layout netlist exist and is it current?
if not self.generate_layout_netlist(layout_path, layout_src, project_path):
return False
# Final check
if not os.path.isfile(layout_path):
print('Error: No netlist generated from magic.')
return
else:
# Read in netlist and convert commas from [X,Y] arrays to vertical bars
# as something that can be converted back as necessary. ngspice treats
# commas as special characters for some reason.
with open(layout_path) as ifile:
spitext = ifile.read()
# spilines = spitext.replace(',', '|')
# with open(layout_path, 'w') as ofile:
# ofile.write(spilines)
# Check the netlist to see if the cell to match is a subcircuit. If
# not, then assume it is the top level.
is_subckt = False
subrex = re.compile('^[^\*]*[ \t]*.subckt[ \t]+([^ \t]+).*$', re.IGNORECASE)
dutlines = spitext.replace('\n+', ' ').splitlines()
for line in dutlines:
lmatch = subrex.match(line)
if lmatch:
subname = lmatch.group(1)
if subname == self.layout:
is_subckt = True
break
if is_subckt:
layout_arg = layout_path + ' ' + self.layout
layout_text = '"' + layout_arg + '"'
else:
layout_arg = layout_path
layout_text = layout_arg
if has_vlog:
schem_arg = vlog_path + ' ' + self.schematic
else:
schem_arg = net_path + ' ' + self.schematic
schem_text = '"' + schem_arg + '"'
# Remove any previous comparison output file
comp_out_path = os.path.splitext(comp_path)[0] + '.out'
if os.path.exists(comp_out_path):
os.remove(comp_out_path)
# Run netgen as subprocess
print('Running: netgen -batch lvs ' + layout_text +
' ' + schem_text + ' ' + self.lvs_setup + ' ' + comp_out_path +
' -json -blackbox')
# Note: Because arguments to subprocess are list items, the {filename cell}
# pair does *not* have to be quoted or braced. Doing so causes a parse
# error.
self.lvsproc = subprocess.Popen(['netgen', '-batch', 'lvs',
layout_arg, schem_arg,
self.lvs_setup, comp_out_path, '-json', '-blackbox'],
cwd=project_path + '/layout',
stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0)
# This is largely unnecessary as netgen usually runs to completion very quickly.
self.watchclock(comp_path)
def watchclock(self, filename):
if self.lvsproc == None:
return
lvs_status = self.lvsproc.poll()
sresult = select.select([self.lvsproc.stdout, self.lvsproc.stderr], [], [], 0)[0]
if self.lvsproc.stdout in sresult:
outstring = self.lvsproc.stdout.readline().decode().strip()
self.logprint(outstring, doflush=True)
print(outstring)
elif self.lvsproc.stderr in sresult:
errstring = self.lvsproc.stderr.readline().decode().strip()
self.logprint(errstring, doflush = True)
print(errstring, file=sys.stderr)
if lvs_status != None:
print("netgen LVS exited with status " + str(lvs_status))
self.lvsproc = None
if lvs_status != 0:
print('Errors encountered in LVS.')
self.logprint('Errors in LVS, lvs status = ' + str(lvs_status), doflush=True)
# Done; now read comp.json and fill the treeview listbox.
self.generate(filename)
else:
self.after(500, lambda: self.watchclock(filename))
# Generate display from "comp.out" file (json file now preferred)
def generate_orig(self, lvspath):
lefttext = []
righttext = []
print("Reading LVS output file " + lvspath)
if os.path.exists(lvspath):
with open(lvspath, 'r') as ifile:
lvslines = ifile.read().splitlines()
for line in lvslines:
if '|' in line:
# parts = line.split('|')
# lefttext.append(parts[0])
# righttext.append(parts[1])
lefttext.append(line[0:42].strip())
righttext.append(line[44:].strip())
else:
lefttext.append(line)
righttext.append('')
# Populate treeview with text
self.toppane.lvsreport.repopulate(lefttext, righttext)
else:
print("Error: No output file generated from LVS.")
# Generate output from LVS report JSON file comp.json
def generate(self, lvspath):
lefttext = []
righttext = []
print("Reading LVS output file " + lvspath)
if os.path.exists(lvspath):
with open(lvspath, 'r') as ifile:
self.lvsdata = json.load(ifile)
# Populate treeview with text
self.toppane.lvsreport.json_repopulate(self.lvsdata)
else:
print("Error: No output file generated from LVS.")
def findrecord(self, value):
print("Unimplemented function")
def findrecord_test(self, value):
# Check if socket is defined; if not, attempt to open one
if not self.msock:
try:
self.msock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
except:
print("No response from layout tool.")
if self.msock:
self.msock.connect(("0.0.0.0", 12946))
self.msock.setblocking(False)
if self.msock:
# Pull name of net or device from 'value'
# This is a test:
self.msock.sendall(b'box 0 0 100 100\r\n')
if __name__ == '__main__':
options = []
arguments = []
for item in sys.argv[1:]:
if item.find('-', 0) == 0:
options.append(item)
else:
arguments.append(item)
root = tkinter.Tk()
app = LVSManager(root)
if arguments:
if len(arguments) >= 2:
app.set_project(arguments[0], project_name=arguments[1])
else:
app.set_project(arguments[0])
root.mainloop()
+86
View File
@@ -0,0 +1,86 @@
#!/bin/env python3
#
# Dialog class for tkinter
import os
import tkinter
from tkinter import ttk
class Dialog(tkinter.Toplevel):
def __init__(self, parent, message = None, title = None, seed = None, border = 'blue', **kwargs):
tkinter.Toplevel.__init__(self, parent)
self.transient(parent)
if title:
self.title(title)
self.configure(background=border, padx=2, pady=2)
self.obox = ttk.Frame(self)
self.obox.pack(side = 'left', fill = 'both', expand = 'true')
self.parent = parent
self.result = None
body = ttk.Frame(self.obox)
self.initial_focus = self.body(body, message, seed, **kwargs)
body.pack(padx = 5, pady = 5)
self.buttonbox()
self.grab_set()
if not self.initial_focus:
self.initial_focus = self
self.protocol("WM_DELETE_WINDOW", self.cancel)
self.geometry("+%d+%d" % (parent.winfo_rootx() + 50,
parent.winfo_rooty() + 50))
self.initial_focus.focus_set()
self.wait_window(self)
# Construction hooks
def body(self, master, **kwargs):
# Create dialog body. Return widget that should have
# initial focus. This method should be overridden
pass
def buttonbox(self):
# Add standard button box. Override if you don't want the
# standard buttons
box = ttk.Frame(self.obox)
self.okb = ttk.Button(box, text="OK", width=10, command=self.ok, default='active')
self.okb.pack(side='left', padx=5, pady=5)
w = ttk.Button(box, text="Cancel", width=10, command=self.cancel)
w.pack(side='left', padx=5, pady=5)
self.bind("<Return>", self.ok)
self.bind("<Escape>", self.cancel)
box.pack(fill='x', expand='true')
# Standard button semantics
def ok(self, event=None):
if not self.validate():
self.initial_focus.focus_set() # put focus back
return
self.withdraw()
self.update_idletasks()
self.result = self.apply()
self.cancel()
def cancel(self, event=None):
# Put focus back to the parent window
self.parent.focus_set()
self.destroy()
def validate(self):
return 1 # Override this
def apply(self):
return None # Override this
+158
View File
@@ -0,0 +1,158 @@
#!/bin/env python3
'''Michael Lange <klappnase (at) freakmail (dot) de>
The ToolTip class provides a flexible tooltip widget for tkinter; it is based on IDLE's ToolTip
module which unfortunately seems to be broken (at least the version I saw).
INITIALIZATION OPTIONS:
anchor : where the text should be positioned inside the widget, must be on of "n", "s", "e", "w", "nw" and so on;
default is "center"
bd : borderwidth of the widget; default is 1 (NOTE: don't use "borderwidth" here)
bg : background color to use for the widget; default is "lightyellow" (NOTE: don't use "background")
delay : time in ms that it takes for the widget to appear on the screen when the mouse pointer has
entered the parent widget; default is 1500
fg : foreground (i.e. text) color to use; default is "black" (NOTE: don't use "foreground")
follow_mouse : if set to 1 the tooltip will follow the mouse pointer instead of being displayed
outside of the parent widget; this may be useful if you want to use tooltips for
large widgets like listboxes or canvases; default is 0
font : font to use for the widget; default is system specific
justify : how multiple lines of text will be aligned, must be "left", "right" or "center"; default is "left"
padx : extra space added to the left and right within the widget; default is 4
pady : extra space above and below the text; default is 2
relief : one of "flat", "ridge", "groove", "raised", "sunken" or "solid"; default is "solid"
state : must be "normal" or "disabled"; if set to "disabled" the tooltip will not appear; default is "normal"
text : the text that is displayed inside the widget
textvariable : if set to an instance of tkinter.StringVar() the variable's value will be used as text for the widget
width : width of the widget; the default is 0, which means that "wraplength" will be used to limit the widgets width
wraplength : limits the number of characters in each line; default is 150
WIDGET METHODS:
configure(**opts) : change one or more of the widget's options as described above; the changes will take effect the
next time the tooltip shows up; NOTE: follow_mouse cannot be changed after widget initialization
Other widget methods that might be useful if you want to subclass ToolTip:
enter() : callback when the mouse pointer enters the parent widget
leave() : called when the mouse pointer leaves the parent widget
motion() : is called when the mouse pointer moves inside the parent widget if follow_mouse is set to 1 and the
tooltip has shown up to continually update the coordinates of the tooltip window
coords() : calculates the screen coordinates of the tooltip window
create_contents() : creates the contents of the tooltip window (by default a tkinter.Label)
'''
# Ideas gleaned from PySol
import tkinter
class ToolTip:
def __init__(self, master, text='Your text here', delay=1500, **opts):
self.master = master
self._opts = {'anchor':'center', 'bd':1, 'bg':'lightyellow', 'delay':delay, 'fg':'black',\
'follow_mouse':0, 'font':None, 'justify':'left', 'padx':4, 'pady':2,\
'relief':'solid', 'state':'normal', 'text':text, 'textvariable':None,\
'width':0, 'wraplength':150}
self.configure(**opts)
self._tipwindow = None
self._id = None
self._id1 = self.master.bind("<Enter>", self.enter, '+')
self._id2 = self.master.bind("<Leave>", self.leave, '+')
self._id3 = self.master.bind("<ButtonPress>", self.leave, '+')
self._follow_mouse = 0
if self._opts['follow_mouse']:
self._id4 = self.master.bind("<Motion>", self.motion, '+')
self._follow_mouse = 1
def configure(self, **opts):
for key in opts:
if key in self._opts:
self._opts[key] = opts[key]
else:
KeyError = 'KeyError: Unknown option: "%s"' %key
raise KeyError
##----these methods handle the callbacks on "<Enter>", "<Leave>" and "<Motion>"---------------##
##----events on the parent widget; override them if you want to change the widget's behavior--##
def enter(self, event=None):
self._schedule()
def leave(self, event=None):
self._unschedule()
self._hide()
def motion(self, event=None):
if self._tipwindow and self._follow_mouse:
x, y = self.coords()
self._tipwindow.wm_geometry("+%d+%d" % (x, y))
##------the methods that do the work:---------------------------------------------------------##
def _schedule(self):
self._unschedule()
if self._opts['state'] == 'disabled':
return
self._id = self.master.after(self._opts['delay'], self._show)
def _unschedule(self):
id = self._id
self._id = None
if id:
self.master.after_cancel(id)
def _show(self):
if self._opts['state'] == 'disabled':
self._unschedule()
return
if not self._tipwindow:
self._tipwindow = tw = tkinter.Toplevel(self.master)
# hide the window until we know the geometry
tw.withdraw()
tw.wm_overrideredirect(1)
if tw.tk.call("tk", "windowingsystem") == 'aqua':
tw.tk.call("::tk::unsupported::MacWindowStyle", "style", tw._w, "help", "none")
self.create_contents()
tw.update_idletasks()
x, y = self.coords()
tw.wm_geometry("+%d+%d" % (x, y))
tw.deiconify()
tw.lift()
def _hide(self):
tw = self._tipwindow
self._tipwindow = None
if tw:
tw.destroy()
##----these methods might be overridden in derived classes:----------------------------------##
def coords(self):
# The tip window must be completely outside the master widget;
# otherwise when the mouse enters the tip window we get
# a leave event and it disappears, and then we get an enter
# event and it reappears, and so on forever :-(
# or we take care that the mouse pointer is always outside the tipwindow :-)
tw = self._tipwindow
twx, twy = tw.winfo_reqwidth(), tw.winfo_reqheight()
w, h = tw.winfo_screenwidth(), tw.winfo_screenheight()
# calculate the y coordinate:
if self._follow_mouse:
y = tw.winfo_pointery() + 20
# make sure the tipwindow is never outside the screen:
if y + twy > h:
y = y - twy - 30
else:
y = self.master.winfo_rooty() + self.master.winfo_height() + 3
if y + twy > h:
y = self.master.winfo_rooty() - twy - 3
# we can use the same x coord in both cases:
x = tw.winfo_pointerx() - twx / 2
if x < 0:
x = 0
elif x + twx > w:
x = w - twx
return x, y
def create_contents(self):
opts = self._opts.copy()
for opt in ('delay', 'follow_mouse', 'state'):
del opts[opt]
label = tkinter.Label(self._tipwindow, **opts)
label.pack()
+653
View File
@@ -0,0 +1,653 @@
#!/bin/env python3
#
# Simple ttk treeview with split view, scrollbar, and
# row of callback buttons
import os
import re
import itertools
import tkinter
from tkinter import ttk
#------------------------------------------------------
# Tree view used as a multi-column list box
#------------------------------------------------------
class TreeViewSplit(ttk.Frame):
def __init__(self, parent, fontsize=11, *args, **kwargs):
ttk.Frame.__init__(self, parent, *args, **kwargs)
s = ttk.Style()
s.configure('normal.TLabel', font=('Helvetica', fontsize))
s.configure('title.TLabel', font=('Helvetica', fontsize, 'bold'))
s.configure('normal.TButton', font=('Helvetica', fontsize),
border = 3, relief = 'raised')
s.configure('Treeview.Heading', font=('Helvetica', fontsize, 'bold'))
s.configure('Treeview.Column', font=('Helvetica', fontsize))
self.fontsize = fontsize
# Last item is a list of 2-item lists, each containing the name of a button
# to place along the button bar at the bottom, and a callback function to
# run when the button is pressed.
def populate(self, title1="", item1list=[], title2="", item2list=[], buttons=[], height=10):
self.item1list = item1list[:]
self.item2list = item2list[:]
columns = [0, 1]
treeFrame = ttk.Frame(self)
treeFrame.pack(side='top', padx=5, pady=5, fill='both', expand='true')
scrollBar = ttk.Scrollbar(treeFrame)
scrollBar.pack(side='right', fill='y')
self.treeView = ttk.Treeview(treeFrame, selectmode='browse', columns=columns, height=height)
self.treeView.pack(side='left', fill='both', expand='true')
scrollBar.config(command=self.treeView.yview)
self.treeView.config(yscrollcommand=scrollBar.set)
self.treeView.column('#0', width=120, stretch='false')
self.treeView.heading(0, text=title1, anchor='w')
self.treeView.heading(1, text=title2, anchor='w')
buttonFrame = ttk.Frame(self)
buttonFrame.pack(side='bottom', fill='x')
self.treeView.tag_configure('select',background='darkslategray',foreground='white')
# Test type tags
self.treeView.tag_configure('error', font=('Helvetica', self.fontsize - 1), foreground = 'red')
self.treeView.tag_configure('clean', font=('Helvetica', self.fontsize - 1), foreground = 'green3')
self.treeView.tag_configure('normal', font=('Helvetica', self.fontsize - 1), foreground = 'black')
self.treeView.tag_configure('prep', font=('Helvetica', self.fontsize, 'bold italic'),
foreground = 'black', anchor = 'center')
self.treeView.tag_configure('header1', font=('Helvetica', self.fontsize, 'bold italic'),
foreground = 'brown', anchor = 'center')
self.treeView.tag_configure('header2', font=('Helvetica', self.fontsize - 1, 'bold'),
foreground = 'blue', anchor = 'center')
self.treeView.tag_configure('header3', font=('Helvetica', self.fontsize - 1, 'italic'),
foreground = 'green4', anchor = 'center')
self.treeView.tag_configure('header4', font=('Helvetica', self.fontsize - 1),
foreground = 'purple', anchor = 'center')
self.func_buttons = []
for button in buttons:
func = button[2]
# Each func_buttons entry is a list of two items; first is the
# button widget, and the second is a boolean that is True if the
# button is to be present always, False if the button is only
# present when there are entries in the itemlists.
self.func_buttons.append([ttk.Button(buttonFrame, text=button[0],
style = 'normal.TButton',
command = lambda func=func: self.func_callback(func)),
button[1]])
self.selectcallback = None
self.lastselected = None
self.lasttag = None
self.repopulate(item1list, item2list)
def get_button(self, index):
if index >= 0 and index < len(self.func_buttons):
return self.func_buttons[index][0]
else:
return None
def set_title(self, title):
self.treeView.heading('#0', text=title, anchor='w')
def repopulate(self, item1list=[], item2list=[]):
# Remove all children of treeview
self.treeView.delete(*self.treeView.get_children())
self.item1list = item1list[:]
self.item2list = item2list[:]
lines = max(len(self.item1list), len(self.item2list))
# Parse the information coming from comp.out. This is preferably
# handled from inside netgen, but that requires development in netgen.
# Note: A top-level group is denoted by an empty string.
nested = ['']
if lines > 0:
# print("Create item ID 0 parent = ''")
self.treeView.insert(nested[-1], 'end', text='-', iid='0',
value=['Initialize', 'Initialize'], tags=['prep'])
nested.append('0')
tagstyle = 'header1'
emptyrec = re.compile('^[\t ]*$')
subrex = re.compile('Subcircuit summary')
cktrex = re.compile('Circuit[\t ]+[12]:[\t ]+([^ \t]+)')
netrex = re.compile('NET mismatches')
devrex = re.compile('DEVICE mismatches')
seprex = re.compile('-----')
sumrex = re.compile('Netlists ')
matchrex = re.compile('.*\*\*Mismatch\*\*')
incircuit = False
watchgroup = False
groupnum = 0
for item1, item2, index in zip(self.item1list, self.item2list, range(lines)):
cname1 = 'Layout' # Placeholder
cname2 = 'Schematic' # Placeholder
# Remove blank lines from the display
lmatch = emptyrec.match(item1)
if lmatch:
lmatch = emptyrec.match(item2)
if lmatch:
continue
index = str(index + 1)
# Parse text to determine how to structure and display it.
tagstyle = 'normal'
nextnest = None
lmatch = subrex.match(item1)
if lmatch:
nested = [''] # pop back to topmost level
nextnest = index
tagstyle = 'header1'
incircuit = False
watchgroup = False
groupnum = 0
item1 = 'Layout compare'
item2 = 'Schematic compare'
else:
lmatch = cktrex.match(item1)
if lmatch and not incircuit:
# Pick up circuit names and replace them in the title, then use them
# for all following titles.
cname1 = lmatch.group(1)
lmatch = cktrex.match(item2)
cname2 = lmatch.group(1)
print("Circuit names " + cname1 + " " + cname2)
# Rewrite title
cktitem = self.treeView.item(nested[-1], values=[cname1 + ' compare',
cname2 + ' compare'])
nextnest = index
tagstyle = 'header2'
incircuit = True
item1 = cname1 + ' Summary'
item2 = cname2 + ' Summary'
elif lmatch:
continue
else:
lmatch = netrex.match(item1)
if lmatch:
if watchgroup:
nested = nested[0:-1]
nested = nested[0:-1]
nextnest = index
tagstyle = 'header2'
groupnum = 1
watchgroup = True
item1 = cname1 + ' Net mismatches'
item2 = cname2 + ' Net mismatches'
else:
lmatch = devrex.match(item1)
if lmatch:
if watchgroup:
nested = nested[0:-1]
nested = nested[0:-1]
nextnest = index
tagstyle = 'header2'
groupnum = 1
watchgroup = True
item1 = cname1 + ' Device mismatches'
item2 = cname2 + ' Device mismatches'
else:
lmatch = seprex.match(item1)
if lmatch:
if watchgroup:
tagstyle = 'header3'
item1 = 'Group ' + str(groupnum)
item2 = 'Group ' + str(groupnum)
if groupnum > 1:
nested = nested[0:-1]
groupnum += 1
nextnest = index
watchgroup = False
else:
if groupnum > 0:
watchgroup = True
continue
else:
lmatch = sumrex.match(item1)
if lmatch:
if watchgroup:
nested = nested[0:-1]
nested = nested[0:-1]
watchgroup = False
tagstyle = 'header2'
groupnum = 0
lmatch1 = matchrex.match(item1)
lmatch2 = matchrex.match(item2)
if lmatch1 or lmatch2:
tagstyle='error'
# print("Create item ID " + str(index) + " parent = " + str(nested[-1]))
self.treeView.insert(nested[-1], 'end', text=index, iid=index, value=[item1, item2],
tags=[tagstyle])
if nextnest:
nested.append(nextnest)
for button in self.func_buttons:
button[0].pack_forget()
if lines == 0:
self.treeView.insert('', 'end', text='-', value=['(no items)', '(no items)'])
for button in self.func_buttons:
if button[1]:
button[0].pack(side='left', padx = 5)
else:
for button in self.func_buttons:
button[0].pack(side='left', padx = 5)
# Special routine to pull in the JSON file data produced by netgen-1.5.72
def json_repopulate(self, lvsdata):
# Remove all children of treeview
self.treeView.delete(*self.treeView.get_children())
# Parse the information coming from comp.out. This is preferably
# handled from inside netgen, but that requires development in netgen.
# Note: A top-level group is denoted by an empty string.
index = 0
errtotal = {}
errtotal['net'] = 0
errtotal['netmatch'] = 0
errtotal['device'] = 0
errtotal['devmatch'] = 0
errtotal['property'] = 0
errtotal['pin'] = 0
ncells = len(lvsdata)
for c in range(0, ncells):
cellrec = lvsdata[c]
if c == ncells - 1:
topcell = True
else:
topcell = False
errcell = {}
errcell['net'] = 0
errcell['netmatch'] = 0
errcell['device'] = 0
errcell['devmatch'] = 0
errcell['property'] = 0
errcell['pin'] = 0;
cname1 = 'Layout'
cname2 = 'Schematic'
# cellrec is a dictionary. Parse the cell summary, then failing nets,
# devices, and properties, and finally pins.
if 'name' in cellrec:
names = cellrec['name']
cname1 = names[0]
cname2 = names[1]
else:
# This is an error but don't halt the program because of it.
cname1 = '(unknown layout)'
cname2 = '(unknown schematic)'
item1 = cname1
item2 = cname2
tagstyle = 'header1'
index += 1
nest0 = index
self.treeView.insert('', 'end', text=index, iid=index, value=[item1, item2],
tags=[tagstyle])
if 'devices' in cellrec or 'nets' in cellrec:
item1 = cname1 + " Summary"
item2 = cname2 + " Summary"
tagstyle = 'header2'
index += 1
nest1 = index
self.treeView.insert(nest0, 'end', text=index, iid=index, value=[item1, item2],
tags=[tagstyle])
if 'devices' in cellrec:
item1 = cname1 + " Devices"
item2 = cname2 + " Devices"
tagstyle = 'header3'
index += 1
nest2 = index
self.treeView.insert(nest1, 'end', text=index, iid=index, value=[item1, item2],
tags=[tagstyle])
devices = cellrec['devices']
devlist = [val for pair in zip(devices[0], devices[1]) for val in pair]
devpair = list(devlist[p:p + 2] for p in range(0, len(devlist), 2))
for dev in devpair:
c1dev = dev[0]
c2dev = dev[1]
item1 = c1dev[0] + "(" + str(c1dev[1]) + ")"
item2 = c2dev[0] + "(" + str(c2dev[1]) + ")"
diffdevs = abs(c1dev[1] - c2dev[1])
if diffdevs == 0:
tagstyle = 'normal'
else:
tagstyle = 'error'
errcell['device'] += diffdevs
if topcell:
errtotal['device'] += diffdevs
index += 1
nest2 = index
self.treeView.insert(nest1, 'end', text=index, iid=index,
value=[item1, item2], tags=[tagstyle])
if 'nets' in cellrec:
item1 = cname1 + " Nets"
item2 = cname2 + " Nets"
tagstyle = 'header3'
index += 1
nest2 = index
self.treeView.insert(nest1, 'end', text=index, iid=index, value=[item1, item2],
tags=[tagstyle])
nets = cellrec['nets']
item1 = nets[0]
item2 = nets[1]
diffnets = abs(nets[0] - nets[1])
if diffnets == 0:
tagstyle = 'normal'
else:
tagstyle = 'error'
errcell['net'] = diffnets
if topcell:
errtotal['net'] += diffnets
index += 1
nest2 = index
self.treeView.insert(nest1, 'end', text=index, iid=index,
value=[item1, item2], tags=[tagstyle])
if 'badnets' in cellrec:
badnets = cellrec['badnets']
if len(badnets) > 0:
item1 = cname1 + " Net Mismatches"
item2 = cname2 + " Net Mismatches"
tagstyle = 'header2'
index += 1
nest1 = index
self.treeView.insert(nest0, 'end', text=index, iid=index,
value=[item1, item2], tags=[tagstyle])
groupnum = 0
for group in badnets:
groupc1 = group[0]
groupc2 = group[1]
nnets = len(groupc1)
groupnum += 1
tagstyle = 'header3'
index += 1
nest2 = index
item1 = "Group " + str(groupnum) + ' (' + str(nnets) + ' nets)'
self.treeView.insert(nest1, 'end', text=index, iid=index,
value=[item1, item1], tags=[tagstyle])
tagstyle = 'error'
errcell['netmatch'] += nnets
if topcell:
errtotal['netmatch'] += nnets
for netnum in range(0, nnets):
if netnum > 0:
item1 = ""
index += 1
nest3 = index
self.treeView.insert(nest2, 'end', text=index, iid=index,
value=[item1, item1], tags=[tagstyle])
net1 = groupc1[netnum]
net2 = groupc2[netnum]
tagstyle = 'header4'
item1 = net1[0]
item2 = net2[0]
index += 1
nest3 = index
self.treeView.insert(nest2, 'end', text=index, iid=index,
value=[item1, item2], tags=[tagstyle])
# Pad shorter device list to the length of the longer one
netdevs = list(itertools.zip_longest(net1[1], net2[1]))
for devpair in netdevs:
devc1 = devpair[0]
devc2 = devpair[1]
tagstyle = 'normal'
if devc1 and devc1[0] != "":
item1 = devc1[0] + '/' + devc1[1] + ' = ' + str(devc1[2])
else:
item1 = ""
if devc2 and devc2[0] != "":
item2 = devc2[0] + '/' + devc2[1] + ' = ' + str(devc2[2])
else:
item2 = ""
index += 1
nest3 = index
self.treeView.insert(nest2, 'end', text=index, iid=index,
value=[item1, item2], tags=[tagstyle])
if 'badelements' in cellrec:
badelements = cellrec['badelements']
if len(badelements) > 0:
item1 = cname1 + " Device Mismatches"
item2 = cname2 + " Device Mismatches"
tagstyle = 'header2'
index += 1
nest1 = index
self.treeView.insert(nest0, 'end', text=index, iid=index,
value=[item1, item2], tags=[tagstyle])
groupnum = 0
for group in badelements:
groupc1 = group[0]
groupc2 = group[1]
ndevs = len(groupc1)
groupnum += 1
tagstyle = 'header3'
index += 1
nest2 = index
item1 = "Group " + str(groupnum) + ' (' + str(ndevs) + ' devices)'
self.treeView.insert(nest1, 'end', text=index, iid=index,
value=[item1, item1], tags=[tagstyle])
tagstyle = 'error'
errcell['devmatch'] += ndevs
if topcell:
errtotal['devmatch'] += ndevs
for elemnum in range(0, ndevs):
if elemnum > 0:
item1 = ""
index += 1
nest3 = index
self.treeView.insert(nest2, 'end', text=index, iid=index,
value=[item1, item1], tags=[tagstyle])
elem1 = groupc1[elemnum]
elem2 = groupc2[elemnum]
tagstyle = 'header4'
item1 = elem1[0]
item2 = elem2[0]
index += 1
nest3 = index
self.treeView.insert(nest2, 'end', text=index, iid=index,
value=[item1, item2], tags=[tagstyle])
# Pad shorter pin list to the length of the longer one
elempins = list(itertools.zip_longest(elem1[1], elem2[1]))
for pinpair in elempins:
pinc1 = pinpair[0]
pinc2 = pinpair[1]
tagstyle = 'normal'
if pinc1 and pinc1[0] != "":
item1 = pinc1[0] + ' = ' + str(pinc1[1])
else:
item1 = ""
if pinc2 and pinc2[0] != "":
item2 = pinc2[0] + ' = ' + str(pinc2[1])
else:
item2 = ""
index += 1
nest3 = index
self.treeView.insert(nest2, 'end', text=index, iid=index,
value=[item1, item2], tags=[tagstyle])
if 'properties' in cellrec:
properties = cellrec['properties']
numproperr = len(properties)
if numproperr > 0:
item1 = cname1 + " Properties"
item2 = cname2 + " Properties"
tagstyle = 'header2'
index += 1
nest1 = index
self.treeView.insert(nest0, 'end', text=index, iid=index, value=[item1, item2],
tags=[tagstyle])
errcell['property'] = numproperr
errtotal['property'] += numproperr
for prop in properties:
if prop != properties[0]:
item1 = ""
index += 1
nest2 = index
self.treeView.insert(nest1, 'end', text=index, iid=index,
value=[item1, item1], tags=[tagstyle])
propc1 = prop[0]
propc2 = prop[1]
tagstyle = 'header3'
item1 = propc1[0]
item2 = propc2[0]
index += 1
nest2 = index
self.treeView.insert(nest1, 'end', text=index, iid=index,
value=[item1, item2], tags=[tagstyle])
# Pad shorter property list to the length of the longer one
elemprops = list(itertools.zip_longest(propc1[1], propc2[1]))
for proppair in elemprops:
perrc1 = proppair[0]
perrc2 = proppair[1]
tagstyle = 'normal'
if perrc1 and perrc1[0] != "":
item1 = perrc1[0] + ' = ' + str(perrc1[1])
else:
item1 = ""
if perrc2 and perrc2[0] != "":
item2 = perrc2[0] + ' = ' + str(perrc2[1])
else:
item2 = ""
index += 1
nest2 = index
self.treeView.insert(nest1, 'end', text=index, iid=index,
value=[item1, item2], tags=[tagstyle])
if 'pins' in cellrec:
item1 = cname1 + " Pins"
item2 = cname2 + " Pins"
tagstyle = 'header2'
index += 1
nest1 = index
self.treeView.insert(nest0, 'end', text=index, iid=index, value=[item1, item2],
tags=[tagstyle])
pins = cellrec['pins']
pinlist = [val for pair in zip(pins[0], pins[1]) for val in pair]
pinpair = list(pinlist[p:p + 2] for p in range(0, len(pinlist), 2))
for pin in pinpair:
item1 = pin[0]
item2 = pin[1]
if item1.lower() == item2.lower():
tagstyle = 'header4'
else:
tagstyle = 'error'
errcell['pin'] += 1
if topcell:
errtotal['pin'] += 1
index += 1
nest2 = index
self.treeView.insert(nest1, 'end', text=index, iid=index,
value=[item1, item2], tags=[tagstyle])
allcellerror = errcell['net'] + errcell['device'] + errcell['property'] + errcell['pin'] + errcell['netmatch'] + errcell['devmatch']
if allcellerror > 0:
item1 = 'Errors: Net = ' + str(errcell['net']) + ', Device = ' + str(errcell['device']) + ', Property = ' + str(errcell['property']) + ', Pin = ' + str(errcell['pin']) + ', Net match = ' + str(errcell['netmatch']) + ', Device match = ' + str(errcell['devmatch'])
tagstyle = 'error'
else:
item1 = 'LVS Clean'
tagstyle = 'clean'
item2 = ""
index += 1
nest0 = index
self.treeView.insert('', 'end', text=index, iid=index, value=[item1, item2],
tags=[tagstyle])
item1 = "Final LVS result:"
item2 = ""
tagstyle = 'header1'
index += 1
nest0 = index
self.treeView.insert('', 'end', text=index, iid=index, value=[item1, item2],
tags=[tagstyle])
allerror = errtotal['net'] + errtotal['device'] + errtotal['property'] + errtotal['pin'] + errtotal['netmatch'] + errtotal['devmatch']
if allerror > 0:
item1 = 'Errors: Net = ' + str(errtotal['net']) + ', Device = ' + str(errtotal['device']) + ', Property = ' + str(errtotal['property']) + ', Pin = ' + str(errtotal['pin']) + ', Net match = ' + str(errtotal['netmatch']) + ', Device match = ' + str(errtotal['devmatch'])
tagstyle = 'error'
else:
item1 = 'LVS Clean'
tagstyle = 'clean'
item2 = ""
index += 1
nest0 = index
self.treeView.insert('', 'end', text=index, iid=index, value=[item1, item2],
tags=[tagstyle])
for button in self.func_buttons:
button[0].pack_forget()
if index == 0:
self.treeView.insert('', 'end', text='-', value=['(no items)', '(no items)'])
for button in self.func_buttons:
if button[1]:
button[0].pack(side='left', padx = 5)
else:
for button in self.func_buttons:
button[0].pack(side='left', padx = 5)
# Return values from the treeview
def getlist(self):
return self.treeView.get_children()
def func_callback(self, callback, event=None):
callback(self.treeView.item(self.treeView.selection()))
def bindselect(self, callback):
self.selectcallback = callback
def setselect(self, value):
self.treeView.selection_set(value)
def selected(self):
value = self.treeView.item(self.treeView.selection())
if value['values']:
return value
else:
return None