Tim Edwards | 9d3debb | 2020-10-20 20:52:18 -0400 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
Tim Edwards | 55f4d0e | 2020-07-05 15:41:02 -0400 | [diff] [blame] | 2 | # |
| 3 | # fixspice --- |
| 4 | # |
| 5 | # This script fixes problems in SPICE models to make them ngspice-compatible. |
| 6 | # The methods searched and corrected in this file correspond to ngspice |
| 7 | # version 30. |
| 8 | # |
| 9 | # This script is a filter to be run by setting the name of this script as |
| 10 | # the value to "filter=" for the model install in the PDK Makefile in |
| 11 | # open_pdks. |
| 12 | # |
| 13 | # This script converted from the bash script by Risto Bell, with improvements. |
| 14 | # |
| 15 | # This script is minimally invasive to the original SPICE file, making changes |
| 16 | # while preserving comments and line continuations. In order to properly parse |
| 17 | # the file, comments and line continuations are recorded and removed from the |
| 18 | # file contents, then inserted again before the modified file is written. |
| 19 | |
| 20 | import re |
| 21 | import os |
| 22 | import sys |
| 23 | import textwrap |
| 24 | |
| 25 | def filter(inname, outname, debug=False): |
| 26 | notparsed = [] |
| 27 | |
| 28 | # Read input. Note that splitlines() performs the additional fix of |
| 29 | # correcting carriage-return linefeed (CRLF) line endings. |
| 30 | try: |
| 31 | with open(inname, 'r') as inFile: |
| 32 | spitext = inFile.read() |
| 33 | except: |
| 34 | print('fixspice.py: failed to open ' + inname + ' for reading.', file=sys.stderr) |
| 35 | return 1 |
| 36 | else: |
| 37 | if debug: |
| 38 | print('Fixing ngspice incompatibilities in file ' + inname + '.') |
| 39 | |
| 40 | # Due to the complexity of comment lines embedded within continuation lines, |
| 41 | # the result needs to be processed line by line. Blank lines and comment |
| 42 | # lines are removed from the text, replaced with tab characters, and collected |
| 43 | # in a separate array. Then the continuation lines are unfolded, and each |
| 44 | # line processed. Then it is all put back together at the end. |
| 45 | |
| 46 | # First replace all tabs with spaces so we can use tabs as markers. |
| 47 | spitext = spitext.replace('\t', ' ') |
| 48 | |
| 49 | # Now do an initial line split |
| 50 | spilines = spitext.splitlines() |
| 51 | |
| 52 | # Search lines for comments and blank lines and replace them with tabs |
| 53 | # Replace continuation lines with tabs and preserve the position. |
| 54 | spitext = '' |
| 55 | for line in spilines: |
| 56 | if len(line) == 0: |
| 57 | notparsed.append('\n') |
| 58 | spitext += '\t ' |
| 59 | elif line[0] == '*': |
| 60 | notparsed.append('\n' + line) |
| 61 | spitext += '\t ' |
| 62 | elif line[0] == '+': |
| 63 | notparsed.append('\n+') |
| 64 | spitext += '\t ' + line[1:] |
| 65 | else: |
| 66 | spitext += '\n' + line |
| 67 | |
| 68 | # Now split back into an array of lines |
| 69 | spilines = spitext.splitlines() |
| 70 | |
| 71 | # Process input with regexp |
| 72 | |
| 73 | fixedlines = [] |
| 74 | modified = False |
| 75 | |
| 76 | # Regular expression to find 'agauss(a,b,c)' lines and record a, b, and c |
| 77 | grex = re.compile('[^{]agauss\(([^,]*),([^,]*),([^)]*)\)', re.IGNORECASE) |
| 78 | |
| 79 | # Regular expression to determine if the line is a .PARAM card |
| 80 | paramrex = re.compile('^\.param', re.IGNORECASE) |
| 81 | # Regular expression to determine if the line is a .MODEL card |
| 82 | modelrex = re.compile('^\.model', re.IGNORECASE) |
| 83 | # Regular expression to detect a .SUBCKT card |
| 84 | subcktrex = re.compile('^\.subckt', re.IGNORECASE) |
| 85 | |
| 86 | for line in spilines: |
| 87 | devtype = line[0].upper() if len(line) > 0 else 0 |
| 88 | |
| 89 | # NOTE: All filter functions below take variable fixedline, alter it, then |
| 90 | # set fixedline to the altered text for the next filter function. |
| 91 | |
| 92 | fixedline = line |
| 93 | |
| 94 | # Fix: Wrap "agauss(...)" in brackets and remove single quotes around expressions |
| 95 | # Example: |
| 96 | # before: + SD_DN_CJ=agauss(7.900e-04,'1.580e-05*__LOT__',1) dn_cj=SD_DN_CJ" |
| 97 | # after: + SD_DN_CJ={agauss(7.900e-04,1.580e-05*__LOT__,1)} dn_cj=SD_DN_CJ" |
| 98 | |
| 99 | # for gmatch in grex.finditer(fixedline): |
| 100 | while True: |
| 101 | gmatch = grex.search(fixedline) |
| 102 | if gmatch: |
| 103 | fixpart1 = gmatch.group(1).strip("'") |
| 104 | fixpart2 = gmatch.group(2).strip("'") |
| 105 | fixpart3 = gmatch.group(3).strip("'") |
| 106 | fixedline = fixedline[0:gmatch.span(0)[0] + 1] + '{agauss(' + fixpart1 + ',' + fixpart2 + ',' + fixpart3 + ')}' + fixedline[gmatch.span(0)[1]:] |
| 107 | if debug: |
| 108 | print('Fixed agauss() call.') |
| 109 | else: |
| 110 | break |
| 111 | |
| 112 | # Fix: Check for "dtemp=dtemp" and remove unless in a .param line |
| 113 | pmatch = paramrex.search(fixedline) |
| 114 | if not pmatch: |
| 115 | altered = re.sub(' dtemp=dtemp', ' ', fixedline, flags=re.IGNORECASE) |
| 116 | if altered != fixedline: |
| 117 | fixedline = altered |
| 118 | if debug: |
| 119 | print('Removed dtemp=dtemp from instance call') |
| 120 | |
| 121 | # Fixes related to .MODEL cards: |
| 122 | |
| 123 | mmatch = modelrex.search(fixedline) |
| 124 | if mmatch: |
| 125 | |
| 126 | modeltype = fixedline.split()[2].lower() |
| 127 | |
| 128 | if modeltype == 'nmos' or modeltype == 'pmos': |
| 129 | |
| 130 | # Fixes related specifically to MOS models: |
| 131 | |
| 132 | # Fix: Look for hspver=98.2 in FET model |
| 133 | altered = re.sub(' hspver[ ]*=[ ]*98\.2', ' ', fixedline, flags=re.IGNORECASE) |
| 134 | if altered != fixedline: |
| 135 | fixedline = altered |
| 136 | if debug: |
| 137 | print('Removed hspver=98.2 from ' + modeltype + ' model') |
| 138 | |
| 139 | # Fix: Change level 53 FETs to level 49 |
| 140 | altered = re.sub(' (level[ ]*=[ ]*)53', ' \g<1>49', fixedline, flags=re.IGNORECASE) |
| 141 | if altered != fixedline: |
| 142 | fixedline = altered |
| 143 | if debug: |
| 144 | print('Changed level 53 ' + modeltype + ' to level 49') |
| 145 | |
| 146 | # Fix: Look for version=4.3 or 4.5 FETs, change to 4.8.0 per recommendations |
| 147 | altered = re.sub(' (version[ ]*=[ ]*)4\.[35]', ' \g<1>4.8.0', |
| 148 | fixedline, flags=re.IGNORECASE) |
| 149 | if altered != fixedline: |
| 150 | fixedline = altered |
| 151 | if debug: |
| 152 | print('Changed version 4.3/4.5 ' + modeltype + ' to version 4.8.0') |
| 153 | |
| 154 | # Fix: Look for mulu0= (NOTE: Might be supported for bsim4?) |
| 155 | altered = re.sub('mulu0[ ]*=[ ]*[0-9.e+-]*', '', fixedline, flags=re.IGNORECASE) |
| 156 | if altered != fixedline: |
| 157 | fixedline = altered |
| 158 | if debug: |
| 159 | print('Removed mulu0= from ' + modeltype + ' model') |
| 160 | |
| 161 | # Fix: Look for apwarn= |
| 162 | altered = re.sub(' apwarn[ ]*=[ ]*[0-9.e+-]*', ' ', fixedline, flags=re.IGNORECASE) |
| 163 | if altered != fixedline: |
| 164 | fixedline = altered |
| 165 | if debug: |
| 166 | print('Removed apwarn= from ' + modeltype + ' model') |
| 167 | |
| 168 | # Fix: Look for lmlt= |
| 169 | altered = re.sub(' lmlt[ ]*=[ ]*[0-9.e+-]*', ' ', fixedline, flags=re.IGNORECASE) |
| 170 | if altered != fixedline: |
| 171 | fixedline = altered |
| 172 | if debug: |
| 173 | print('Removed lmlt= from ' + modeltype + ' model') |
| 174 | |
| 175 | # Fix: Look for nf= |
| 176 | altered = re.sub(' nf[ ]*=[ ]*[0-9.e+-]*', ' ', fixedline, flags=re.IGNORECASE) |
| 177 | if altered != fixedline: |
| 178 | fixedline = altered |
| 179 | if debug: |
| 180 | print('Removed nf= from ' + modeltype + ' model') |
| 181 | |
| 182 | # Fix: Look for sa/b/c/d/= |
| 183 | altered = re.sub(' s[abcd][ ]*=[ ]*[0-9.e+-]*', ' ', fixedline, flags=re.IGNORECASE) |
| 184 | if altered != fixedline: |
| 185 | fixedline = altered |
| 186 | if debug: |
| 187 | print('Removed s[abcd]= from ' + modeltype + ' model') |
| 188 | |
| 189 | # Fix: Look for binflag= in MOS .MODEL |
| 190 | altered = re.sub(' binflag[ ]*=[ ]*[0-9.e+-]*', ' ', fixedline, flags=re.IGNORECASE) |
| 191 | if altered != fixedline: |
| 192 | fixedline = altered |
| 193 | if debug: |
| 194 | print('Removed binflag= from ' + modeltype + ' model') |
| 195 | |
| 196 | # Fix: Look for wref, lref= in MOS .MODEL (note: could be found in other models?) |
| 197 | altered = re.sub(' [wl]ref[ ]*=[ ]*[0-9.e+-]*', ' ', fixedline, flags=re.IGNORECASE) |
| 198 | if altered != fixedline: |
| 199 | fixedline = altered |
| 200 | if debug: |
| 201 | print('Removed lref= from MOS .MODEL') |
| 202 | |
| 203 | # TREF is a known issue for (apparently?) all device types |
| 204 | # Fix: Look for tref= in .MODEL |
| 205 | altered = re.sub(' tref[ ]*=[ ]*[0-9.e+-]*', ' ', fixedline, flags=re.IGNORECASE) |
| 206 | if altered != fixedline: |
| 207 | fixedline = altered |
| 208 | if debug: |
| 209 | print('Removed tref= from ' + modeltype + ' model') |
| 210 | |
| 211 | # Fix: Look for double-dot model binning and replace with single dot |
| 212 | altered = re.sub('\.\.([0-9]+)', '.\g<1>', fixedline, flags=re.IGNORECASE) |
| 213 | if altered != fixedline: |
| 214 | fixedline = altered |
| 215 | if debug: |
| 216 | print('Collapsed double-dot model binning.') |
| 217 | |
| 218 | # Various deleted parameters above may appear in instances, so those must be |
| 219 | # caught as well. Need to catch expressions and variables in addition to the |
| 220 | # usual numeric assignments. |
| 221 | |
| 222 | if devtype == 'M': |
| 223 | altered = re.sub(' nf=[^ \'\t]+', ' ', fixedline, flags=re.IGNORECASE) |
| 224 | altered = re.sub(' nf=\'[^\'\t]+\'', ' ', altered, flags=re.IGNORECASE) |
| 225 | if altered != fixedline: |
| 226 | fixedline = altered |
| 227 | if debug: |
| 228 | print('Removed nf= from MOSFET device instance') |
| 229 | |
| 230 | altered = re.sub(' mulu0=[^ \'\t]+', ' ', fixedline, flags=re.IGNORECASE) |
| 231 | altered = re.sub(' mulu0=\'[^\'\t]+\'', ' ', altered, flags=re.IGNORECASE) |
| 232 | if altered != fixedline: |
| 233 | fixedline = altered |
| 234 | if debug: |
| 235 | print('Removed mulu0= from MOSFET device instance') |
| 236 | |
| 237 | altered = re.sub(' s[abcd]=[^ \'\t]+', ' ', fixedline, flags=re.IGNORECASE) |
| 238 | altered = re.sub(' s[abcd]=\'[^\'\t]+\'', ' ', altered, flags=re.IGNORECASE) |
| 239 | if altered != fixedline: |
| 240 | fixedline = altered |
| 241 | if debug: |
| 242 | print('Removed s[abcd]= from MOSFET device instance') |
| 243 | |
| 244 | # Remove tref= from all device type instances |
| 245 | altered = re.sub(' tref=[^ \'\t]+', ' ', fixedline, flags=re.IGNORECASE) |
| 246 | altered = re.sub(' tref=\'[^\'\t]+\'', ' ', altered, flags=re.IGNORECASE) |
| 247 | if altered != fixedline: |
| 248 | fixedline = altered |
| 249 | if debug: |
| 250 | print('Removed tref= from device instance') |
| 251 | |
| 252 | # Check for use of ".subckt ... <name>=l" (or <name>=w) with no antecedent |
| 253 | # for 'w' or 'l'. It is the responsibility of the technology file for extraction |
| 254 | # to produce the correct name to pass to the subcircuit for length or width. |
| 255 | |
| 256 | smatch = subcktrex.match(fixedline) |
| 257 | if smatch: |
| 258 | altered = fixedline |
| 259 | if fixedline.lower().endswith('=l'): |
| 260 | if ' l=' not in fixedline.lower(): |
| 261 | altered=re.sub( '=l$', '=0', fixedline, flags=re.IGNORECASE) |
| 262 | elif '=l ' in fixedline.lower(): |
| 263 | if ' l=' not in fixedline.lower(): |
| 264 | altered=re.sub( '=l ', '=0 ', altered, flags=re.IGNORECASE) |
| 265 | if altered != fixedline: |
| 266 | fixedline = altered |
| 267 | if debug: |
| 268 | print('Replaced use of "l" with no definition in .subckt line') |
| 269 | |
| 270 | altered = fixedline |
| 271 | if fixedline.lower().endswith('=w'): |
| 272 | if ' w=' not in fixedline.lower(): |
| 273 | altered=re.sub( '=w$', '=0', fixedline, flags=re.IGNORECASE) |
| 274 | elif '=w ' in fixedline.lower(): |
| 275 | if ' w=' not in fixedline.lower(): |
| 276 | altered=re.sub( '=w ', '=0 ', altered, flags=re.IGNORECASE) |
| 277 | if altered != fixedline: |
| 278 | fixedline = altered |
| 279 | if debug: |
| 280 | print('Replaced use of "w" with no definition in .subckt line') |
| 281 | |
| 282 | fixedlines.append(fixedline) |
| 283 | if fixedline != line: |
| 284 | modified = True |
| 285 | |
| 286 | # Reinsert embedded comments and continuation lines |
| 287 | if debug: |
| 288 | print('Reconstructing output') |
| 289 | olines = [] |
| 290 | for line in fixedlines: |
| 291 | while '\t ' in line: |
| 292 | line = line.replace('\t ', notparsed.pop(0), 1) |
| 293 | olines.append(line) |
| 294 | |
| 295 | fixedlines = '\n'.join(olines).strip() |
| 296 | olines = fixedlines.splitlines() |
| 297 | |
| 298 | # Write output |
| 299 | if debug: |
| 300 | print('Writing output') |
| 301 | if outname == None: |
| 302 | for line in olines: |
| 303 | print(line) |
| 304 | else: |
| 305 | # If the output is a symbolic link but no modifications have been made, |
| 306 | # then leave it alone. If it was modified, then remove the symbolic |
| 307 | # link before writing. |
| 308 | if os.path.islink(outname): |
| 309 | if not modified: |
| 310 | return 0 |
| 311 | else: |
| 312 | os.unlink(outname) |
| 313 | try: |
| 314 | with open(outname, 'w') as outFile: |
| 315 | for line in olines: |
| 316 | print(line, file=outFile) |
| 317 | except: |
| 318 | print('fixspice.py: failed to open ' + outname + ' for writing.', file=sys.stderr) |
| 319 | return 1 |
| 320 | |
| 321 | |
| 322 | if __name__ == '__main__': |
| 323 | |
| 324 | # This script expects to get one or two arguments. One argument is |
| 325 | # mandatory and is the input file. The other argument is optional and |
| 326 | # is the output file. The output file and input file may be the same |
| 327 | # name, in which case the original input is overwritten. |
| 328 | |
| 329 | options = [] |
| 330 | arguments = [] |
| 331 | for item in sys.argv[1:]: |
| 332 | if item.find('-', 0) == 0: |
| 333 | options.append(item[1:]) |
| 334 | else: |
| 335 | arguments.append(item) |
| 336 | |
| 337 | if len(arguments) > 0: |
| 338 | infilename = arguments[0] |
| 339 | |
| 340 | if len(arguments) > 1: |
| 341 | outfilename = arguments[1] |
| 342 | else: |
| 343 | outfilename = None |
| 344 | |
| 345 | debug = True if 'debug' in options else False |
| 346 | |
| 347 | result = filter(infilename, outfilename, debug) |
| 348 | sys.exit(result) |