| #!/usr/bin/env python3 | |
| # | |
| # split_one_spice.py -- | |
| # | |
| # Script that reads a SPICE file that contains multiple models and | |
| # subcircuits, and splits it into one file per subcircuit, with each | |
| # file containing any related in-lined models. | |
| # | |
| # The arguments are <path_to_input> and <path_to_output>. | |
| # <path_to_input> should be the path to a single file, while | |
| # <path_to_output> is the path to a directory where the split files will | |
| # be put. | |
| import os | |
| import sys | |
| import re | |
| import glob | |
| def usage(): | |
| print('split_one_spice.py <path_to_input> <path_to_output>') | |
| def convert_file(in_file, out_path): | |
| # Regexp patterns | |
| paramrex = re.compile(r'\.param[ \t]+(.*)') | |
| subrex = re.compile(r'\.subckt[ \t]+([^ \t]+)[ \t]+([^ \t]*)') | |
| modelrex = re.compile(r'\.model[ \t]+([^ \t]+)[ \t]+([^ \t]+)[ \t]+(.*)') | |
| endsubrex = re.compile(r'\.ends[ \t]+(.+)') | |
| increx = re.compile(r'\.include[ \t]+') | |
| with open(in_file, 'r') as ifile: | |
| inplines = ifile.read().splitlines() | |
| insubckt = False | |
| inparam = False | |
| inmodel = False | |
| inpinlist = False | |
| subname = '' | |
| modname = '' | |
| modtype = '' | |
| # Keep track of what the subcircuit names are | |
| subnames = [] | |
| filenos = {} | |
| # Keep track of what parameters are used by what subcircuits | |
| paramlist = {} | |
| # Enumerate which lines go to which files | |
| linedest = [-1]*len(inplines) | |
| fileno = -1; | |
| lineno = -1; | |
| for line in inplines: | |
| lineno += 1 | |
| # Item 1. Handle comment lines | |
| if line.startswith('*'): | |
| linedest[lineno] = fileno | |
| continue | |
| # Item 2. Flag continuation lines | |
| if line.startswith('+'): | |
| contline = True | |
| else: | |
| contline = False | |
| if line.strip() != '': | |
| if inparam: | |
| inparam = False | |
| if inpinlist: | |
| inpinlist = False | |
| # Item 3. Handle blank lines like comment lines | |
| if line.strip() == '': | |
| linedest[lineno] = fileno | |
| continue | |
| # Item 4. Handle continuation lines | |
| if contline: | |
| if inparam: | |
| # Continue handling parameters | |
| linedest[lineno] = fileno | |
| if not insubckt: | |
| # Find (global) parameters and record what line they were found on | |
| ptok = list(item for item in line[1:].strip().split() if item != '=') | |
| for param, value in zip(*[iter(ptok)]*2): | |
| paramlist[param] = lineno | |
| else: | |
| # Find if a global parameter was used. Assign it to this | |
| # subcircuit. If it has already been used, assign it to | |
| # be a common parameter | |
| for param in paramlist: | |
| if param in line[1:]: | |
| checkfile = linedest[paramlist[param]] | |
| if checkfile == -1: | |
| linedest[paramlist[param]] = fileno | |
| elif checkfile != fileno: | |
| linedest[paramlist[param]] = -3 | |
| continue | |
| # Item 5. Regexp matching | |
| # parameters | |
| pmatch = paramrex.match(line) | |
| if pmatch: | |
| inparam = True | |
| linedest[lineno] = fileno | |
| if not insubckt: | |
| # Find (global) parameters and record what line they were found on | |
| ptok = list(item for item in pmatch.group(1).split() if item != '=') | |
| for param, value in zip(*[iter(ptok)]*2): | |
| paramlist[param] = lineno | |
| else: | |
| # Find if a global parameter was used. Assign it to this | |
| # subcircuit. If it has already been used, assign it to | |
| # be a common parameter | |
| for param in paramlist: | |
| if param in pmatch.group(1): | |
| checkfile = linedest[paramlist[param]] | |
| if checkfile == -1: | |
| linedest[paramlist[param]] = fileno | |
| if checkfile != fileno: | |
| linedest[paramlist[param]] = -3 | |
| continue | |
| # model | |
| mmatch = modelrex.match(line) | |
| if mmatch: | |
| modname = mmatch.group(1) | |
| modtype = mmatch.group(2) | |
| linedest[lineno] = fileno | |
| inmodel = 2 | |
| continue | |
| if not insubckt: | |
| # Things to parse if not in a subcircuit | |
| imatch = subrex.match(line) | |
| if imatch: | |
| insubckt = True | |
| subname = imatch.group(1) | |
| fileno = len(subnames) | |
| subnames.append(subname) | |
| filenos[subname] = fileno | |
| if fileno > 0: | |
| # If this is not the first subcircuit, then add all blank | |
| # and comment lines above it to the same file | |
| lastno = -1 | |
| tline = lineno - 1 | |
| while tline >= 0: | |
| tinp = inplines[tline] | |
| # Backup through all comment and blank lines | |
| if not tinp.startswith('*') and not tinp.strip() == '': | |
| lastno = linedest[tline] | |
| tline += 1; | |
| break; | |
| tline -= 1; | |
| while tline < lineno: | |
| # Forward through all blank lines, and assign them to | |
| # the previous subcell. | |
| tinp = inplines[tline] | |
| if tinp.strip() != '': | |
| break; | |
| if linedest[tline] == -1: | |
| linedest[tline] = lastno | |
| tline += 1; | |
| while tline < lineno: | |
| linedest[tline] = fileno | |
| tline += 1; | |
| else: | |
| # If this is the first subcircuit encountered, then assign | |
| # to it the nearest block of comment lines before it. If | |
| # those comment lines include a parameter or statistics | |
| # block, then abandon the effort. | |
| # Backup through blank lines immediately above | |
| abandon = False | |
| tline = lineno - 1 | |
| while tline >= 0: | |
| tinp = inplines[tline] | |
| if not tinp.strip() == '': | |
| break; | |
| tline -= 1; | |
| while tline > 0: | |
| # Backup through the next comment block above | |
| tinp = inplines[tline] | |
| if not tinp.startswith('*'): | |
| tline += 1; | |
| break; | |
| elif tinp.strip('*').strip().startswith('statistics'): | |
| abandon = True | |
| tline -= 1; | |
| if tline == 0: | |
| abandon = True | |
| if not abandon: | |
| while tline < lineno: | |
| linedest[tline] = fileno | |
| tline += 1; | |
| devrex = re.compile(subname + r'[ \t]*([^ \t]+)[ \t]*([^ \t]+)[ \t]*(.*)', re.IGNORECASE) | |
| inpinlist = True | |
| linedest[lineno] = fileno | |
| continue | |
| else: | |
| # Things to parse when inside of a ".subckt" block | |
| if inpinlist: | |
| # Watch for pin list continuation line. | |
| linedest[lineno] = fileno | |
| continue | |
| else: | |
| ematch = endsubrex.match(line) | |
| if ematch: | |
| if ematch.group(1) != subname: | |
| print('Error: "ends" name does not match "subckt" name!') | |
| print('"ends" name = ' + ematch.group(1)) | |
| print('"subckt" name = ' + subname) | |
| linedest[lineno] = fileno | |
| fileno = -1 | |
| insubckt = False | |
| inmodel = False | |
| subname = '' | |
| continue | |
| else: | |
| linedest[lineno] = fileno | |
| continue | |
| # Sort subcircuit names | |
| subnames.sort(reverse=True) | |
| # Look for any lines containing parameters in paramlist. If those lines | |
| # are unassigned (-1), then assign them to the same cell that the parameter | |
| # was assigned to. NOTE: Assumes that there will never be two parameters | |
| # on the same line that were from two different subcircuits that is not | |
| # already marked as a common parameter. | |
| lineno = -1 | |
| for line in inplines: | |
| lineno += 1 | |
| if linedest[lineno] == -1: | |
| for param in paramlist: | |
| if param in line: | |
| linedest[lineno] = linedest[paramlist[param]] | |
| break | |
| # Ad hoc method: Look for any lines containing each cell name, and assign | |
| # that line to the cell. That isolates parameters that belong to only one | |
| # cell. Ignore comment lines from line 1 down to the first non-comment line. | |
| # Since all parameters and comment blocks have been handled, this is not | |
| # likely to change anything. | |
| lineno = -1 | |
| for line in inplines: | |
| lineno = -1 | |
| if not line.startswith('*'): | |
| break | |
| topcomm = True | |
| for line in inplines: | |
| lineno += 1 | |
| if topcomm and not line.startswith('*'): | |
| topcomm = False | |
| if not topcomm: | |
| if linedest[lineno] == -1: | |
| for subname in subnames: | |
| subno = filenos[subname] | |
| if subname in line: | |
| linedest[lineno] = subno | |
| break | |
| # All lines marked -1 except for comment lines should be remarked -3 | |
| # (go into the common file only) | |
| lineno = -1 | |
| for line in inplines: | |
| lineno += 1 | |
| if linedest[lineno] == -1: | |
| if not line.startswith('*'): | |
| linedest[lineno] = -3 | |
| # All comment lines that are surrounded by lines marked -3 should | |
| # also be marked -3. This keeps comments that are completely inside | |
| # blocks that are only in the common file out of the individual files. | |
| # ignore "* statistics" and "* mismatch" lines. | |
| lineno = 0 | |
| for line in inplines[1:]: | |
| lineno += 1 | |
| if line.startswith('*') and ('statistics' in line or 'mismatch' in line): | |
| continue | |
| if linedest[lineno] == -1 and linedest[lineno - 1] == -3: | |
| testline = lineno + 1 | |
| while linedest[testline] == -1: | |
| testline += 1 | |
| if linedest[testline] == -3: | |
| testline = lineno | |
| while linedest[testline] == -1: | |
| linedest[testline] = -3 | |
| testline += 1 | |
| froot = os.path.split(in_file)[1] | |
| for subname in subnames: | |
| subno = filenos[subname] | |
| fext = os.path.splitext(in_file)[1] | |
| # Guard against one of the split files having the same name as | |
| # the original, since we need to keep the original file. | |
| if subname == os.path.splitext(froot)[0]: | |
| fext = '_split' + fext | |
| # Output the result to out_file. | |
| with open(out_path + '/' + subname + fext, 'w') as ofile: | |
| firstline = True | |
| lineno = -1 | |
| for line in inplines: | |
| lineno += 1 | |
| if linedest[lineno] == subno or linedest[lineno] == -1: | |
| if firstline: | |
| print('* File ' + subname + fext + ' split from ' + froot + ' by split_one_spice.py', file=ofile) | |
| firstline = False | |
| print(line, file=ofile) | |
| # Debug: Print one diagnostic file (do this before messing with the | |
| # linedest[] entries in the next step). This debug file shows which | |
| # lines of the file are split into which file, and which lines are | |
| # common. | |
| ffile = os.path.split(in_file)[1] | |
| froot = os.path.splitext(ffile)[0] | |
| fext = os.path.splitext(ffile)[1] | |
| with open(out_path + '/' + froot + '_debug' + fext, 'w') as ofile: | |
| for subname in subnames: | |
| subno = filenos[subname] | |
| print(str(subno) + '\t' + subname, file=ofile) | |
| print('\n', file=ofile) | |
| lineno = -1 | |
| for line in inplines: | |
| lineno += 1 | |
| print(str(linedest[lineno]) + '\t' + line, file=ofile) | |
| # Reset all linedest[] entries except the bottommost entry for each subcircuit. | |
| lineno = len(inplines) | |
| subrefs = [0] * len(subnames) | |
| while lineno > 0: | |
| lineno -= 1 | |
| if linedest[lineno] >= 0: | |
| if subrefs[linedest[lineno]] == 0: | |
| subrefs[linedest[lineno]] = 1 | |
| else: | |
| linedest[lineno] = -2 | |
| # Print the original file, including each of the new files. | |
| # Also print out all lines marked "-1" or "-3" | |
| with open(out_path + '/' + froot + fext, 'w') as ofile: | |
| lineno = -1 | |
| subno = -1 | |
| for line in inplines: | |
| lineno += 1 | |
| if linedest[lineno] == -1 or linedest[lineno] == -3 : | |
| print(line, file=ofile) | |
| elif linedest[lineno] >= 0: | |
| for subname in subnames: | |
| if filenos[subname] == linedest[lineno]: | |
| fext = os.path.splitext(in_file)[1] | |
| if subname == os.path.splitext(froot)[0]: | |
| fext = '_split' + fext | |
| break | |
| print('.include ' + subname + fext, file=ofile) | |
| subno = linedest[lineno] | |
| if __name__ == '__main__': | |
| debug = False | |
| if len(sys.argv) == 1: | |
| print("No options given to split_one_spice.py.") | |
| usage() | |
| sys.exit(0) | |
| optionlist = [] | |
| arguments = [] | |
| for option in sys.argv[1:]: | |
| if option.find('-', 0) == 0: | |
| optionlist.append(option) | |
| else: | |
| arguments.append(option) | |
| if len(arguments) != 2: | |
| print("Wrong number of arguments given to split_one_spice.py.") | |
| usage() | |
| sys.exit(0) | |
| if '-debug' in optionlist: | |
| debug = True | |
| inpath = arguments[0] | |
| outpath = arguments[1] | |
| do_one_file = False | |
| if not os.path.exists(inpath): | |
| print('No such source file ' + inpath) | |
| sys.exit(1) | |
| if not os.path.isfile(inpath): | |
| print('Input path ' + inpath + ' is not a file.') | |
| sys.exit(1) | |
| convert_file(inpath, outpath) | |
| print('Done.') | |
| exit(0) |