blob: dc2fd04d382ca89e47ab8aa1e7a33cd07f18b7c5 [file] [log] [blame]
#!/usr/bin/env python3
#
# fixspice ---
#
# This script fixes problems in SPICE models to make them ngspice-compatible.
# The methods searched and corrected in this file correspond to ngspice
# version 30.
#
# This script is a filter to be run by setting the name of this script as
# the value to "filter=" for the model install in the PDK Makefile in
# open_pdks.
#
# This script converted from the bash script by Risto Bell, with improvements.
#
# This script is minimally invasive to the original SPICE file, making changes
# while preserving comments and line continuations. In order to properly parse
# the file, comments and line continuations are recorded and removed from the
# file contents, then inserted again before the modified file is written.
import re
import os
import sys
import textwrap
def filter(inname, outname, debug=False):
notparsed = []
# Read input. Note that splitlines() performs the additional fix of
# correcting carriage-return linefeed (CRLF) line endings.
try:
with open(inname, 'r') as inFile:
spitext = inFile.read()
except:
print('fixspice.py: failed to open ' + inname + ' for reading.', file=sys.stderr)
return 1
else:
if debug:
print('Fixing ngspice incompatibilities in file ' + inname + '.')
# Due to the complexity of comment lines embedded within continuation lines,
# the result needs to be processed line by line. Blank lines and comment
# lines are removed from the text, replaced with tab characters, and collected
# in a separate array. Then the continuation lines are unfolded, and each
# line processed. Then it is all put back together at the end.
# First replace all tabs with spaces so we can use tabs as markers.
spitext = spitext.replace('\t', ' ')
# Now do an initial line split
spilines = spitext.splitlines()
# Search lines for comments and blank lines and replace them with tabs
# Replace continuation lines with tabs and preserve the position.
spitext = ''
for line in spilines:
if len(line) == 0:
notparsed.append('\n')
spitext += '\t '
elif line[0] == '*':
notparsed.append('\n' + line)
spitext += '\t '
elif line[0] == '+':
notparsed.append('\n+')
spitext += '\t ' + line[1:]
else:
spitext += '\n' + line
# Now split back into an array of lines
spilines = spitext.splitlines()
# Process input with regexp
fixedlines = []
modified = False
# Regular expression to find 'agauss(a,b,c)' lines and record a, b, and c
grex = re.compile('[^{]agauss\(([^,]*),([^,]*),([^)]*)\)', re.IGNORECASE)
# Regular expression to determine if the line is a .PARAM card
paramrex = re.compile('^\.param', re.IGNORECASE)
# Regular expression to determine if the line is a .MODEL card
modelrex = re.compile('^\.model', re.IGNORECASE)
# Regular expression to detect a .SUBCKT card
subcktrex = re.compile('^\.subckt', re.IGNORECASE)
for line in spilines:
devtype = line[0].upper() if len(line) > 0 else 0
# NOTE: All filter functions below take variable fixedline, alter it, then
# set fixedline to the altered text for the next filter function.
fixedline = line
# Fix: Wrap "agauss(...)" in brackets and remove single quotes around expressions
# Example:
# before: + SD_DN_CJ=agauss(7.900e-04,'1.580e-05*__LOT__',1) dn_cj=SD_DN_CJ"
# after: + SD_DN_CJ={agauss(7.900e-04,1.580e-05*__LOT__,1)} dn_cj=SD_DN_CJ"
# for gmatch in grex.finditer(fixedline):
while True:
gmatch = grex.search(fixedline)
if gmatch:
fixpart1 = gmatch.group(1).strip("'")
fixpart2 = gmatch.group(2).strip("'")
fixpart3 = gmatch.group(3).strip("'")
fixedline = fixedline[0:gmatch.span(0)[0] + 1] + '{agauss(' + fixpart1 + ',' + fixpart2 + ',' + fixpart3 + ')}' + fixedline[gmatch.span(0)[1]:]
if debug:
print('Fixed agauss() call.')
else:
break
# Fix: Check for "dtemp=dtemp" and remove unless in a .param line
pmatch = paramrex.search(fixedline)
if not pmatch:
altered = re.sub(' dtemp=dtemp', ' ', fixedline, flags=re.IGNORECASE)
if altered != fixedline:
fixedline = altered
if debug:
print('Removed dtemp=dtemp from instance call')
# Fixes related to .MODEL cards:
mmatch = modelrex.search(fixedline)
if mmatch:
modeltype = fixedline.split()[2].lower()
if modeltype == 'nmos' or modeltype == 'pmos':
# Fixes related specifically to MOS models:
# Fix: Look for hspver=98.2 in FET model
altered = re.sub(' hspver[ ]*=[ ]*98\.2', ' ', fixedline, flags=re.IGNORECASE)
if altered != fixedline:
fixedline = altered
if debug:
print('Removed hspver=98.2 from ' + modeltype + ' model')
# Fix: Change level 53 FETs to level 49
altered = re.sub(' (level[ ]*=[ ]*)53', ' \g<1>49', fixedline, flags=re.IGNORECASE)
if altered != fixedline:
fixedline = altered
if debug:
print('Changed level 53 ' + modeltype + ' to level 49')
# Fix: Look for version=4.3 or 4.5 FETs, change to 4.8.0 per recommendations
altered = re.sub(' (version[ ]*=[ ]*)4\.[35]', ' \g<1>4.8.0',
fixedline, flags=re.IGNORECASE)
if altered != fixedline:
fixedline = altered
if debug:
print('Changed version 4.3/4.5 ' + modeltype + ' to version 4.8.0')
# Fix: Look for mulu0= (NOTE: Might be supported for bsim4?)
altered = re.sub('mulu0[ ]*=[ ]*[0-9.e+-]*', '', fixedline, flags=re.IGNORECASE)
if altered != fixedline:
fixedline = altered
if debug:
print('Removed mulu0= from ' + modeltype + ' model')
# Fix: Look for apwarn=
altered = re.sub(' apwarn[ ]*=[ ]*[0-9.e+-]*', ' ', fixedline, flags=re.IGNORECASE)
if altered != fixedline:
fixedline = altered
if debug:
print('Removed apwarn= from ' + modeltype + ' model')
# Fix: Look for lmlt=
altered = re.sub(' lmlt[ ]*=[ ]*[0-9.e+-]*', ' ', fixedline, flags=re.IGNORECASE)
if altered != fixedline:
fixedline = altered
if debug:
print('Removed lmlt= from ' + modeltype + ' model')
# Fix: Look for nf=
altered = re.sub(' nf[ ]*=[ ]*[0-9.e+-]*', ' ', fixedline, flags=re.IGNORECASE)
if altered != fixedline:
fixedline = altered
if debug:
print('Removed nf= from ' + modeltype + ' model')
# Fix: Look for sa/b/c/d/=
altered = re.sub(' s[abcd][ ]*=[ ]*[0-9.e+-]*', ' ', fixedline, flags=re.IGNORECASE)
if altered != fixedline:
fixedline = altered
if debug:
print('Removed s[abcd]= from ' + modeltype + ' model')
# Fix: Look for binflag= in MOS .MODEL
altered = re.sub(' binflag[ ]*=[ ]*[0-9.e+-]*', ' ', fixedline, flags=re.IGNORECASE)
if altered != fixedline:
fixedline = altered
if debug:
print('Removed binflag= from ' + modeltype + ' model')
# Fix: Look for wref, lref= in MOS .MODEL (note: could be found in other models?)
altered = re.sub(' [wl]ref[ ]*=[ ]*[0-9.e+-]*', ' ', fixedline, flags=re.IGNORECASE)
if altered != fixedline:
fixedline = altered
if debug:
print('Removed lref= from MOS .MODEL')
# TREF is a known issue for (apparently?) all device types
# Fix: Look for tref= in .MODEL
altered = re.sub(' tref[ ]*=[ ]*[0-9.e+-]*', ' ', fixedline, flags=re.IGNORECASE)
if altered != fixedline:
fixedline = altered
if debug:
print('Removed tref= from ' + modeltype + ' model')
# Fix: Look for double-dot model binning and replace with single dot
altered = re.sub('\.\.([0-9]+)', '.\g<1>', fixedline, flags=re.IGNORECASE)
if altered != fixedline:
fixedline = altered
if debug:
print('Collapsed double-dot model binning.')
# Various deleted parameters above may appear in instances, so those must be
# caught as well. Need to catch expressions and variables in addition to the
# usual numeric assignments.
if devtype == 'M':
altered = re.sub(' nf=[^ \'\t]+', ' ', fixedline, flags=re.IGNORECASE)
altered = re.sub(' nf=\'[^\'\t]+\'', ' ', altered, flags=re.IGNORECASE)
if altered != fixedline:
fixedline = altered
if debug:
print('Removed nf= from MOSFET device instance')
altered = re.sub(' mulu0=[^ \'\t]+', ' ', fixedline, flags=re.IGNORECASE)
altered = re.sub(' mulu0=\'[^\'\t]+\'', ' ', altered, flags=re.IGNORECASE)
if altered != fixedline:
fixedline = altered
if debug:
print('Removed mulu0= from MOSFET device instance')
altered = re.sub(' s[abcd]=[^ \'\t]+', ' ', fixedline, flags=re.IGNORECASE)
altered = re.sub(' s[abcd]=\'[^\'\t]+\'', ' ', altered, flags=re.IGNORECASE)
if altered != fixedline:
fixedline = altered
if debug:
print('Removed s[abcd]= from MOSFET device instance')
# Remove tref= from all device type instances
altered = re.sub(' tref=[^ \'\t]+', ' ', fixedline, flags=re.IGNORECASE)
altered = re.sub(' tref=\'[^\'\t]+\'', ' ', altered, flags=re.IGNORECASE)
if altered != fixedline:
fixedline = altered
if debug:
print('Removed tref= from device instance')
# Check for use of ".subckt ... <name>=l" (or <name>=w) with no antecedent
# for 'w' or 'l'. It is the responsibility of the technology file for extraction
# to produce the correct name to pass to the subcircuit for length or width.
smatch = subcktrex.match(fixedline)
if smatch:
altered = fixedline
if fixedline.lower().endswith('=l'):
if ' l=' not in fixedline.lower():
altered=re.sub( '=l$', '=0', fixedline, flags=re.IGNORECASE)
elif '=l ' in fixedline.lower():
if ' l=' not in fixedline.lower():
altered=re.sub( '=l ', '=0 ', altered, flags=re.IGNORECASE)
if altered != fixedline:
fixedline = altered
if debug:
print('Replaced use of "l" with no definition in .subckt line')
altered = fixedline
if fixedline.lower().endswith('=w'):
if ' w=' not in fixedline.lower():
altered=re.sub( '=w$', '=0', fixedline, flags=re.IGNORECASE)
elif '=w ' in fixedline.lower():
if ' w=' not in fixedline.lower():
altered=re.sub( '=w ', '=0 ', altered, flags=re.IGNORECASE)
if altered != fixedline:
fixedline = altered
if debug:
print('Replaced use of "w" with no definition in .subckt line')
fixedlines.append(fixedline)
if fixedline != line:
modified = True
# Reinsert embedded comments and continuation lines
if debug:
print('Reconstructing output')
olines = []
for line in fixedlines:
while '\t ' in line:
line = line.replace('\t ', notparsed.pop(0), 1)
olines.append(line)
fixedlines = '\n'.join(olines).strip()
olines = fixedlines.splitlines()
# Write output
if debug:
print('Writing output')
if outname == None:
for line in olines:
print(line)
else:
# If the output is a symbolic link but no modifications have been made,
# then leave it alone. If it was modified, then remove the symbolic
# link before writing.
if os.path.islink(outname):
if not modified:
return 0
else:
os.unlink(outname)
try:
with open(outname, 'w') as outFile:
for line in olines:
print(line, file=outFile)
except:
print('fixspice.py: failed to open ' + outname + ' for writing.', file=sys.stderr)
return 1
if __name__ == '__main__':
# This script expects to get one or two arguments. One argument is
# mandatory and is the input file. The other argument is optional and
# is the output file. The output file and input file may be the same
# name, in which case the original input is overwritten.
options = []
arguments = []
for item in sys.argv[1:]:
if item.find('-', 0) == 0:
options.append(item[1:])
else:
arguments.append(item)
if len(arguments) > 0:
infilename = arguments[0]
if len(arguments) > 1:
outfilename = arguments[1]
else:
outfilename = None
debug = True if 'debug' in options else False
result = filter(infilename, outfilename, debug)
sys.exit(result)