Added find_all_devices.py from the Google/SkyWater work, as it is a useful script for determining information about where a subcircuit can be found in a model directory hierarchy, although it is somewhat tied to the Google/SkyWater specific file hierarchy.
diff --git a/common/find_all_devices.py b/common/find_all_devices.py new file mode 100755 index 0000000..71c27c5 --- /dev/null +++ b/common/find_all_devices.py
@@ -0,0 +1,559 @@ +#!/usr/bin/env python3 +#----------------------------------------------------------- +# +# Find all devices which have subcircuit definitions in the path +# skywater-pdk/libraries/sky130_fd_pr/VERSION/cells/. List all of +# these devices. Then find all paths from the directory models/ +# that will read the subcircuit definition through a hierarchical +# series of includes. +#----------------------------------------------------------- + +import re +import os +import sys + +#----------------------------------------------------------- +# Find all models in the model directory path, recursively +#----------------------------------------------------------- + +def addmodels(modelpath): + modelfmts = os.listdir(modelpath) + files_to_parse = [] + for modelfmt in modelfmts: + if os.path.isdir(modelpath + '/' + modelfmt): + files_to_parse.extend(addmodels(modelpath + '/' + modelfmt)) + else: + fmtext = os.path.splitext(modelfmt)[1] + if fmtext == '.spice': + files_to_parse.append(modelpath + '/' + modelfmt) + + return files_to_parse + +#----------------------------------------------------------- +# Find the device name in a SPICE "X" line +#----------------------------------------------------------- + +def get_device_name(line): + # The instance name has already been parsed out of the line, and + # all continuation lines have been added, so this routine finds + # the last keyword that is not a parameter (i.e., does not contain + # '='). + + tokens = line.split() + for token in tokens: + if '=' in token: + break + else: + devname = token + + return devname + +#----------------------------------------------------------- +# Pick the file from the list that is appropriate for the +# choice of either FEOL or BEOL corner. Mostly ad-hoc rules +# based on the known file names. +#----------------------------------------------------------- + +def choose_preferred(inclist, feol, beol, notop, debug): + + try: + # The top-level file 'sky130.lib.spice' is always preferred + incfile = next(item for item in inclist if os.path.split(item)[1] == 'sky130.lib.spice') + except: + # (1) Sort list by the length of the root name of the file + inclist = sorted(inclist, key=lambda x: len(os.path.splitext(os.path.split(x)[1])[0])) + + # (2) Sort list by depth of directory hierarchy + inclist = sorted(inclist, key=lambda x: len(x.split('/'))) + + for incfile in inclist: + incname = os.path.split(incfile)[1] + if debug: + print(' choose preferred, checking: "' + incname + '"') + + elif incname == 'custom.spice': + # Ignore "custom.spice" (example file, unused) + continue + elif notop and incname.startswith('correl'): + # Ignore "correl1.spice", etc., if "-notop" option chosen + continue + + elif feol in incname: + break + elif 't' in beol and 'typical' in incname: + break + elif 'h' in beol and 'high' in incname: + break; + elif 'l' in beol and 'low' in incname: + break; + + if debug: + incname = os.path.split(incfile)[1] + print(' choose_preferred: chose ' + incfile + ' (' + incname + ')') + return incfile + +#--------------------------------------------------------------------- +# Sort files with the subcircuit by relevance. Files are considered +# in the order ".model.spice", ".pm3.spice", and ".spice". +#--------------------------------------------------------------------- + +def preferred_order(subfiles, feol): + ordfiles = [] + feolstr = '__' + feol + + # Sort by length first, so shorter ones, e.g., without "leak" or + # "discrete", end up at the front of the list. + ordlist = sorted(subfiles, key=len) + + for file in ordlist[:]: + if file.endswith('.corner.spice') and feolstr in file: + ordfiles.append(file) + ordlist.remove(file) + + for file in ordlist[:]: + if file.endswith('.model.spice'): + ordfiles.append(file) + ordlist.remove(file) + + for file in ordlist[:]: + if file.endswith('.pm3.spice') and feolstr in file: + ordfiles.append(file) + ordlist.remove(file) + + for file in ordlist[:]: + if file.endswith('.pm3.spice'): + ordfiles.append(file) + ordlist.remove(file) + + ordfiles.extend(ordlist) + return ordfiles + +#----------------------------------------------------------- +# Find the appropriate file to include to handle this device +#----------------------------------------------------------- + +def check_device(subfiles, includedict, modfilesdict, feol, beol, notop, debug): + # subfiles = list of files that define this subcircuit. + # includedict = files that include the dictionary keyword file + # modfilesdict = files that are included by the dictionary keyword file + # feol = FEOL corner (fs, tt, ss, etc.) for transistors, diodes + # beol = BEOL corner (hl, tt, ll, etc.) for capacitors, resistors, inductors + + ordfiles = preferred_order(subfiles, feol) + if debug: + print('') + print('Check_device: Search order:') + for ordfile in ordfiles: + print(' ' + os.path.split(ordfile)[1]) + + # Find the proper include file to include this device. Attempt on all + # entries in ordfiles, and stop at the first one that returns a result. + # Assume that all files have unique names and point to the proper + # location, so that is is only necessary to look at the last path + # component. + + if debug: + print('\nClimb hierarchy of includes to the top:') + + for ordfile in ordfiles: + ordname = os.path.split(ordfile)[1] + if debug: + print(' (1) Search for "' + ordname + '"') + try: + inclist = includedict[ordname][1:] + except: + if debug: + print(' No include file found for "' + ordfile + '"') + print(' Sample entry:') + for key in includedict: + print(' ' + key + ': "' + str(includedict[key][1:]) + '"') + break + continue + else: + if debug: + print(' Starting list = ') + for item in inclist: + print(' ' + item) + + while True: + incfile = choose_preferred(inclist, feol, beol, notop, debug) + incname = os.path.split(incfile)[1] + if debug: + print(' (2) Search for "' + incname + '"') + try: + inclist = includedict[incname][1:] + except: + break + else: + if debug: + print(' Continuing list = ') + for item in inclist: + print(' ' + item) + + if debug: + print('Final top level include file is: "' + incfile + '"') + return incfile + + # Should only happen if subfiles is empty list + return None + +#----------------------------------------------------------- +# Find all cells and all models +#----------------------------------------------------------- + +def find_everything(pathtop): + cellspath = pathtop + '/cells' + modelspath = pathtop + '/models' + + allcells = os.listdir(cellspath) + + subcktrex = re.compile('\.subckt[ \t]+([^ \t]+)[ \t]+', re.IGNORECASE) + includerex = re.compile('\.include[ \t]+([^ \t]+)', re.IGNORECASE) + + filesdict = {} + subcktdict = {} + includedict = {} + modfilesdict = {} + + for cellfile in allcells: + cellpath = cellspath + '/' + cellfile + cellfmts = os.listdir(cellpath) + files_to_parse = [] + for cellfmt in cellfmts: + fmtext = os.path.splitext(cellfmt)[1] + if fmtext == '.spice': + files_to_parse.append(cellpath + '/' + cellfmt) + + for file in files_to_parse: + with open(file, 'r') as ifile: + spicelines = ifile.read().splitlines() + for line in spicelines: + smatch = subcktrex.match(line) + if smatch: + subname = smatch.group(1) + try: + subcktdict[subname].append(file) + except: + subcktdict[subname] = [file] + filetail = os.path.split(file)[1] + try: + filesdict[filetail].append(subname) + except: + filesdict[filetail] = [subname] + + files_to_parse = addmodels(modelspath) + files_to_parse.extend(addmodels(cellspath)) + + for file in files_to_parse: + # NOTE: Avoid problems with sonos directories using + # "tt.spice", which causes the include chain recursive + # loop to fail to exit. This is a one-off exception + # (hack alert) + if '_of_life' in file: + continue + + with open(file, 'r') as ifile: + spicelines = ifile.read().splitlines() + for line in spicelines: + imatch = includerex.match(line) + if imatch: + incname = imatch.group(1).strip('"') + inckey = os.path.split(incname)[1] + + try: + inclist = includedict[inckey] + except: + includedict[inckey] = [incname, file] + else: + if file not in inclist[1:]: + includedict[inckey].append(file) + filetail = os.path.split(file)[1] + try: + modlist = modfilesdict[filetail] + except: + modfilesdict[filetail] = [incname] + else: + if incname not in modlist: + modfilesdict[filetail].append(incname) + + return filesdict, subcktdict, includedict, modfilesdict + +#----------------------------------------------------------- +# Main application +#----------------------------------------------------------- + +def do_find_all_devices(pathtop, sourcefile, cellname=None, feol='tt', beol='tt', doall=False, notop=False, debug=False): + + (filesdict, subcktdict, includedict, modfilesdict) = find_everything(pathtop) + + if sourcefile: + # Parse the source file and find all 'X' records, and collect a list + # of all primitive devices used in the file by cross-checking against + # the dictionary of subcircuits. + + devrex = re.compile('x([^ \t]+)[ \t]+(.*)', re.IGNORECASE) + incfiles = [] + + with open(sourcefile, 'r') as ifile: + spicelines = ifile.read().splitlines() + + if debug: + print('Netlist file first line is "' + spicelines[0] + '"') + + isdev = False + for line in spicelines: + if line.startswith('*'): + continue + if line.strip() == '': + continue + elif line.startswith('+'): + if isdev: + rest += line[1:] + elif isdev: + devname = get_device_name(rest) + try: + subfiles = subcktdict[devname] + except: + pass + else: + incfile = check_device(subfiles, includedict, modfilesdict, feol, beol, notop, debug) + if not incfile: + incfile = preferred_order(subfiles, feol)[0] + + if incfile: + if debug: + print('Device ' + devname + ': Include ' + incfile) + if incfile not in incfiles: + incfiles.append(incfile) + else: + print('Something went dreadfully wrong with device "' + devname + '"') + + isdev = False + + smatch = devrex.match(line) + if smatch: + instname = smatch.group(1) + rest = smatch.group(2) + isdev = True + elif isdev: + devname = get_device_name(rest) + try: + subfiles = subcktdict[devname] + except: + pass + else: + incfile = check_device(subfiles, includedict, modfilesdict, feol, beol, notop, debug) + if not incfile: + incfile = preferred_order(subfiles, feol)[0] + + if incfile: + if debug: + print('Device "' + devname + '": Include "' + incfile + '"') + if incfile not in incfiles: + incfiles.append(incfile) + else: + print('Something went dreadfully wrong with device "' + devname + '"') + isdev = False + + # Return the .include lines needed + return incfiles + + elif cellname: + # Diagnostic: Given a cell name on the command line (with -cell=<name>), + # Run check_device() on the cell and report. + try: + subfiles = subcktdict[cellname] + except: + print('No cell "' + cellname + '" was found in the PDK files.') + sys.exit(1) + + incfile = check_device(subfiles, includedict, modfilesdict, feol, beol, notop, debug) + if debug: + print('') + print('Report:') + print('') + print('Cell = "' + cellname + '"') + print('') + + bestfilepath = preferred_order(subfiles, feol)[0] + if bestfilepath.startswith(pathtop): + bestfile = bestfilepath[len(pathtop) + 1:] + print('Subcircuit defined in (from ' + pathtop + '/): "' + bestfile + '"') + + if debug: + print('') + print('Top level include: ') + + if incfile: + return [incfile] + else: + return [bestfilepath] + + elif doall: + allincludes = [] + for cellname in subcktdict: + + # Diagnostic: Given a cell name on the command line (with -cell=<name>), + # Run check_device() on the cell and report. + try: + subfiles = subcktdict[cellname] + except: + print('No cell "' + cellname + '" was found in the PDK files.') + continue + + incfile = check_device(subfiles, includedict, modfilesdict, feol, beol, notop, debug) + print('Cell = "' + cellname + '"') + bestfilepath = preferred_order(subfiles, feol)[0] + if bestfilepath.startswith(pathtop): + bestfile = bestfilepath[len(pathtop) + 1:] + print(' Subcircuit: "' + os.path.split(bestfile)[1] + '"') + print(' Include: ', end='') + if incfile: + if incfile not in allincludes: + allincludes.append(incfile) + print('"' + incfile + '"') + else: + if bestfilepath not in allincludes: + allincludes.append(bestfilepath) + print('"' + bestfilepath + '"') + + print('') + print('Summary: All files to include:\n') + return allincludes + + else: + # No source file given, so just dump the lists of subcircuits, models, + # and files into four different output files. + + nsubs = 0 + with open('sublist.txt', 'w') as ofile: + for key in subcktdict: + nsubs += 1 + value = subcktdict[key] + print(key + ': ' + ', '.join(value), file=ofile) + + nfiles = 0 + with open('filelist.txt', 'w') as ofile: + for key in filesdict: + nfiles += 1 + value = filesdict[key] + print(key + ': ' + ', '.join(value), file=ofile) + + with open('inclist.txt', 'w') as ofile: + for key in includedict: + value = includedict[key] + print(key + '(' + value[0] + '): ' + ', '.join(value[1:]), file=ofile) + + with open('modfilelist.txt', 'w') as ofile: + for key in modfilesdict: + value = modfilesdict[key] + print(key + ': ' + ', '.join(value), file=ofile) + + print('Found ' + str(nsubs) + ' subcircuit definitions in ' + str(nfiles) + ' files.') + return [] + +#----------------------------------------------------------- +# Command-line entry point +#----------------------------------------------------------- + +if __name__ == "__main__": + + optionlist = [] + arguments = [] + + for option in sys.argv[1:]: + if option.find('-', 0) == 0: + optionlist.append(option) + else: + arguments.append(option) + + # Defaults: Set up for the most recent PDK version. + version = 'v0.20.1' + pathtop = '../../libraries/sky130_fd_pr/' + version + + # Default FEOL corner is "tt", and default BEOL corner is "tt" + feol = 'tt' + beol = 'tt' + cellname = None + debug = False + doall = False + notop = False + + # Override defaults from options + + for option in optionlist: + if option.startswith('-version'): + try: + version = option.split('=')[1] + except: + print('Option usage: -version=<versionstring>') + sys.exit(1) + elif option.startswith('-corner') or option.startswith('-feol'): + try: + feol = option.split('=')[1] + except: + print('Option usage: -feol=<corner_name>') + sys.exit(1) + elif option.startswith('-beol'): + try: + beol = option.split('=')[1] + except: + print('Option usage: -beol=<corner_name>') + sys.exit(1) + elif option.startswith('-cell'): + try: + cellname = option.split('=')[1] + except: + print('Option usage: -cell=<cell_name>') + sys.exit(1) + elif option == '-notop': + notop = True + elif option == '-all': + doall = True + elif option == '-debug': + debug = True + + # Parse "-pdkpath" after the others because it is dependent on any option + # "-version" passed on the command line. + + for option in optionlist: + if option.startswith('-pdkpath'): + try: + pathroot = option.split('=')[1] + except: + print('Option usage: -pdkpath=<pathname>') + sys.exit(1) + if not os.path.isdir(pathroot): + print('Cannot find PDK directory ' + pathroot) + sys.exit(1) + pathtop = pathroot + '/libraries/sky130_fd_pr/' + version + if not os.path.isdir(pathtop): + print('Cannot find primitive device directory ' + pathtop) + sys.exit(1) + + # To be done: Make this a useful routine that can insert one or more + # .include statements into a SPICE netlist. Should take any number of + # files on the arguments line and modify the files in place. + + if len(arguments) > 0: + sourcefile = arguments[0] + if not os.path.isfile(sourcefile): + print('Cannot read SPICE source file ' + sourcefile) + sys.exit(1) + else: + sourcefile = None + + if not os.path.isdir(pathtop): + print('Cannot find PDK path top level directory ' + pathtop) + sys.exit(1) + elif debug: + print('\nFinding everything in ' + pathtop + '.') + + incfiles = do_find_all_devices(pathtop, sourcefile, cellname, feol, beol, doall, notop, debug) + for incfile in incfiles: + if incfile.endswith('.lib.spice'): + print('.lib ' + incfile + ' ' + feol) + else: + print('.include "' + incfile + '"') + + sys.exit(0)