| #!/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('\.param[ \t]+(.*)') |
| subrex = re.compile('\.subckt[ \t]+([^ \t]+)[ \t]+([^ \t]*)') |
| modelrex = re.compile('\.model[ \t]+([^ \t]+)[ \t]+([^ \t]+)[ \t]+(.*)') |
| endsubrex = re.compile('\.ends[ \t]+(.+)') |
| increx = re.compile('\.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 + '[ \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) |