blob: 86a448a79cb235284a6973629697643b4bf8907b [file] [log] [blame]
#!/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)