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)