Tim Edwards | 7eb7ab1 | 2020-08-17 22:21:10 -0400 | [diff] [blame] | 1 | #!/bin/env python3 |
| 2 | # |
| 3 | # split_one_spice.py -- |
| 4 | # |
| 5 | # Script that reads a SPICE file that contains multiple models and |
| 6 | # subcircuits, and splits it into one file per subcircuit, with each |
| 7 | # file containing any related in-lined models. |
| 8 | # |
| 9 | # The arguments are <path_to_input> and <path_to_output>. |
| 10 | # <path_to_input> should be the path to a single file, while |
| 11 | # <path_to_output> is the path to a directory where the split files will |
| 12 | # be put. |
| 13 | |
| 14 | import os |
| 15 | import sys |
| 16 | import re |
| 17 | import glob |
| 18 | |
| 19 | def usage(): |
| 20 | print('split_one_spice.py <path_to_input> <path_to_output>') |
| 21 | |
| 22 | def convert_file(in_file, out_path): |
| 23 | |
| 24 | # Regexp patterns |
| 25 | paramrex = re.compile('\.param[ \t]+(.*)') |
| 26 | subrex = re.compile('\.subckt[ \t]+([^ \t]+)[ \t]+([^ \t]*)') |
| 27 | modelrex = re.compile('\.model[ \t]+([^ \t]+)[ \t]+([^ \t]+)[ \t]+(.*)') |
| 28 | endsubrex = re.compile('\.ends[ \t]+(.+)') |
| 29 | increx = re.compile('\.include[ \t]+') |
| 30 | |
| 31 | with open(in_file, 'r') as ifile: |
| 32 | inplines = ifile.read().splitlines() |
| 33 | |
| 34 | insubckt = False |
| 35 | inparam = False |
| 36 | inmodel = False |
| 37 | inpinlist = False |
| 38 | subname = '' |
| 39 | modname = '' |
| 40 | modtype = '' |
| 41 | |
| 42 | # Keep track of what the subcircuit names are |
| 43 | subnames = [] |
| 44 | filenos = {} |
| 45 | |
| 46 | # Keep track of what parameters are used by what subcircuits |
| 47 | paramlist = {} |
| 48 | |
| 49 | # Enumerate which lines go to which files |
| 50 | linedest = [-1]*len(inplines) |
| 51 | fileno = -1; |
| 52 | lineno = -1; |
| 53 | |
| 54 | for line in inplines: |
| 55 | lineno += 1 |
| 56 | |
| 57 | # Item 1. Handle comment lines |
| 58 | if line.startswith('*'): |
| 59 | linedest[lineno] = fileno |
| 60 | continue |
| 61 | |
| 62 | # Item 2. Flag continuation lines |
| 63 | if line.startswith('+'): |
| 64 | contline = True |
| 65 | else: |
| 66 | contline = False |
| 67 | if line.strip() != '': |
| 68 | if inparam: |
| 69 | inparam = False |
| 70 | if inpinlist: |
| 71 | inpinlist = False |
| 72 | |
| 73 | # Item 3. Handle blank lines like comment lines |
| 74 | if line.strip() == '': |
| 75 | linedest[lineno] = fileno |
| 76 | continue |
| 77 | |
| 78 | # Item 4. Handle continuation lines |
| 79 | if contline: |
| 80 | if inparam: |
| 81 | # Continue handling parameters |
| 82 | linedest[lineno] = fileno |
| 83 | if not insubckt: |
| 84 | # Find (global) parameters and record what line they were found on |
| 85 | ptok = list(item for item in line[1:].strip().split() if item != '=') |
| 86 | for param, value in zip(*[iter(ptok)]*2): |
| 87 | paramlist[param] = lineno |
| 88 | else: |
| 89 | # Find if a global parameter was used. Assign it to this |
| 90 | # subcircuit. If it has already been used, assign it to |
| 91 | # be a common parameter |
| 92 | for param in paramlist: |
| 93 | if param in line[1:]: |
| 94 | checkfile = linedest[paramlist[param]] |
| 95 | if checkfile == -1: |
| 96 | linedest[paramlist[param]] = fileno |
| 97 | elif checkfile != fileno: |
| 98 | linedest[paramlist[param]] = -3 |
| 99 | continue |
| 100 | |
| 101 | # Item 5. Regexp matching |
| 102 | |
| 103 | # parameters |
| 104 | pmatch = paramrex.match(line) |
| 105 | if pmatch: |
| 106 | inparam = True |
| 107 | linedest[lineno] = fileno |
| 108 | if not insubckt: |
| 109 | # Find (global) parameters and record what line they were found on |
| 110 | ptok = list(item for item in pmatch.group(1).split() if item != '=') |
| 111 | for param, value in zip(*[iter(ptok)]*2): |
| 112 | paramlist[param] = lineno |
| 113 | else: |
| 114 | # Find if a global parameter was used. Assign it to this |
| 115 | # subcircuit. If it has already been used, assign it to |
| 116 | # be a common parameter |
| 117 | for param in paramlist: |
| 118 | if param in pmatch.group(1): |
| 119 | checkfile = linedest[paramlist[param]] |
| 120 | if checkfile == -1: |
| 121 | linedest[paramlist[param]] = fileno |
| 122 | if checkfile != fileno: |
| 123 | linedest[paramlist[param]] = -3 |
| 124 | continue |
| 125 | |
| 126 | # model |
| 127 | mmatch = modelrex.match(line) |
| 128 | if mmatch: |
| 129 | modname = mmatch.group(1) |
| 130 | modtype = mmatch.group(2) |
| 131 | |
| 132 | linedest[lineno] = fileno |
| 133 | inmodel = 2 |
| 134 | continue |
| 135 | |
| 136 | if not insubckt: |
| 137 | # Things to parse if not in a subcircuit |
| 138 | |
| 139 | imatch = subrex.match(line) |
| 140 | if imatch: |
| 141 | insubckt = True |
| 142 | subname = imatch.group(1) |
| 143 | fileno = len(subnames) |
| 144 | subnames.append(subname) |
| 145 | filenos[subname] = fileno |
| 146 | |
| 147 | if fileno > 0: |
| 148 | # If this is not the first subcircuit, then add all blank |
| 149 | # and comment lines above it to the same file |
| 150 | |
| 151 | lastno = -1 |
| 152 | tline = lineno - 1 |
| 153 | while tline >= 0: |
| 154 | tinp = inplines[tline] |
| 155 | # Backup through all comment and blank lines |
| 156 | if not tinp.startswith('*') and not tinp.strip() == '': |
| 157 | lastno = linedest[tline] |
| 158 | tline += 1; |
| 159 | break; |
| 160 | tline -= 1; |
| 161 | |
| 162 | while tline < lineno: |
| 163 | # Forward through all blank lines, and assign them to |
| 164 | # the previous subcell. |
| 165 | tinp = inplines[tline] |
| 166 | if tinp.strip() != '': |
| 167 | break; |
| 168 | if linedest[tline] == -1: |
| 169 | linedest[tline] = lastno |
| 170 | tline += 1; |
| 171 | |
| 172 | while tline < lineno: |
| 173 | linedest[tline] = fileno |
| 174 | tline += 1; |
| 175 | else: |
| 176 | # If this is the first subcircuit encountered, then assign |
| 177 | # to it the nearest block of comment lines before it. If |
| 178 | # those comment lines include a parameter or statistics |
| 179 | # block, then abandon the effort. |
| 180 | |
| 181 | # Backup through blank lines immediately above |
| 182 | abandon = False |
| 183 | tline = lineno - 1 |
| 184 | while tline >= 0: |
| 185 | tinp = inplines[tline] |
| 186 | if not tinp.strip() == '': |
| 187 | break; |
| 188 | tline -= 1; |
| 189 | |
| 190 | while tline > 0: |
| 191 | # Backup through the next comment block above |
| 192 | tinp = inplines[tline] |
| 193 | if not tinp.startswith('*'): |
| 194 | tline += 1; |
| 195 | break; |
| 196 | elif tinp.strip('*').strip().startswith('statistics'): |
| 197 | abandon = True |
| 198 | tline -= 1; |
| 199 | |
| 200 | if tline == 0: |
| 201 | abandon = True |
| 202 | |
| 203 | if not abandon: |
| 204 | while tline < lineno: |
| 205 | linedest[tline] = fileno |
| 206 | tline += 1; |
| 207 | |
| 208 | devrex = re.compile(subname + '[ \t]*([^ \t]+)[ \t]*([^ \t]+)[ \t]*(.*)', re.IGNORECASE) |
| 209 | inpinlist = True |
| 210 | linedest[lineno] = fileno |
| 211 | continue |
| 212 | |
| 213 | else: |
| 214 | # Things to parse when inside of a ".subckt" block |
| 215 | |
| 216 | if inpinlist: |
| 217 | # Watch for pin list continuation line. |
| 218 | linedest[lineno] = fileno |
| 219 | continue |
| 220 | |
| 221 | else: |
| 222 | ematch = endsubrex.match(line) |
| 223 | if ematch: |
| 224 | if ematch.group(1) != subname: |
| 225 | print('Error: "ends" name does not match "subckt" name!') |
| 226 | print('"ends" name = ' + ematch.group(1)) |
| 227 | print('"subckt" name = ' + subname) |
| 228 | |
| 229 | linedest[lineno] = fileno |
| 230 | fileno = -1 |
| 231 | |
| 232 | insubckt = False |
| 233 | inmodel = False |
| 234 | subname = '' |
| 235 | continue |
| 236 | else: |
| 237 | linedest[lineno] = fileno |
| 238 | continue |
| 239 | |
| 240 | # Sort subcircuit names |
| 241 | subnames.sort(reverse=True) |
| 242 | |
| 243 | # Look for any lines containing parameters in paramlist. If those lines |
| 244 | # are unassigned (-1), then assign them to the same cell that the parameter |
| 245 | # was assigned to. NOTE: Assumes that there will never be two parameters |
| 246 | # on the same line that were from two different subcircuits that is not |
| 247 | # already marked as a common parameter. |
| 248 | |
| 249 | lineno = -1 |
| 250 | for line in inplines: |
| 251 | lineno += 1 |
| 252 | if linedest[lineno] == -1: |
| 253 | for param in paramlist: |
| 254 | if param in line: |
| 255 | linedest[lineno] = linedest[paramlist[param]] |
| 256 | break |
| 257 | |
| 258 | # Ad hoc method: Look for any lines containing each cell name, and assign |
| 259 | # that line to the cell. That isolates parameters that belong to only one |
| 260 | # cell. Ignore comment lines from line 1 down to the first non-comment line. |
| 261 | # Since all parameters and comment blocks have been handled, this is not |
| 262 | # likely to change anything. |
| 263 | |
| 264 | lineno = -1 |
| 265 | for line in inplines: |
| 266 | lineno = -1 |
| 267 | if not line.startswith('*'): |
| 268 | break |
| 269 | |
| 270 | topcomm = True |
| 271 | for line in inplines: |
| 272 | lineno += 1 |
| 273 | if topcomm and not line.startswith('*'): |
| 274 | topcomm = False |
| 275 | |
| 276 | if not topcomm: |
| 277 | if linedest[lineno] == -1: |
| 278 | for subname in subnames: |
| 279 | subno = filenos[subname] |
| 280 | if subname in line: |
| 281 | linedest[lineno] = subno |
| 282 | break |
| 283 | |
| 284 | # All lines marked -1 except for comment lines should be remarked -3 |
| 285 | # (go into the common file only) |
| 286 | |
| 287 | lineno = -1 |
| 288 | for line in inplines: |
| 289 | lineno += 1 |
| 290 | if linedest[lineno] == -1: |
| 291 | if not line.startswith('*'): |
| 292 | linedest[lineno] = -3 |
| 293 | |
| 294 | # All comment lines that are surrounded by lines marked -3 should |
| 295 | # also be marked -3. This keeps comments that are completely inside |
| 296 | # blocks that are only in the common file out of the individual files. |
Tim Edwards | 475b527 | 2020-08-25 14:05:50 -0400 | [diff] [blame] | 297 | # ignore "* statistics" and "* mismatch" lines. |
Tim Edwards | 7eb7ab1 | 2020-08-17 22:21:10 -0400 | [diff] [blame] | 298 | |
| 299 | lineno = 0 |
| 300 | for line in inplines[1:]: |
| 301 | lineno += 1 |
Tim Edwards | 475b527 | 2020-08-25 14:05:50 -0400 | [diff] [blame] | 302 | if line.startswith('*') and ('statistics' in line or 'mismatch' in line): |
| 303 | continue |
Tim Edwards | 7eb7ab1 | 2020-08-17 22:21:10 -0400 | [diff] [blame] | 304 | if linedest[lineno] == -1 and linedest[lineno - 1] == -3: |
| 305 | testline = lineno + 1 |
| 306 | while linedest[testline] == -1: |
| 307 | testline += 1 |
| 308 | if linedest[testline] == -3: |
| 309 | testline = lineno |
| 310 | while linedest[testline] == -1: |
| 311 | linedest[testline] = -3 |
| 312 | testline += 1 |
| 313 | |
| 314 | froot = os.path.split(in_file)[1] |
| 315 | for subname in subnames: |
| 316 | subno = filenos[subname] |
| 317 | fext = os.path.splitext(in_file)[1] |
| 318 | |
| 319 | # Guard against one of the split files having the same name as |
| 320 | # the original, since we need to keep the original file. |
| 321 | if subname == os.path.splitext(froot)[0]: |
| 322 | fext = '_split' + fext |
| 323 | |
| 324 | # Output the result to out_file. |
| 325 | with open(out_path + '/' + subname + fext, 'w') as ofile: |
| 326 | firstline = True |
| 327 | lineno = -1 |
| 328 | for line in inplines: |
| 329 | lineno += 1 |
| 330 | if linedest[lineno] == subno or linedest[lineno] == -1: |
| 331 | if firstline: |
| 332 | print('* File ' + subname + fext + ' split from ' + froot + ' by split_one_spice.py', file=ofile) |
| 333 | firstline = False |
| 334 | print(line, file=ofile) |
| 335 | |
| 336 | # Debug: Print one diagnostic file (do this before messing with the |
| 337 | # linedest[] entries in the next step). This debug file shows which |
| 338 | # lines of the file are split into which file, and which lines are |
| 339 | # common. |
| 340 | |
| 341 | ffile = os.path.split(in_file)[1] |
| 342 | froot = os.path.splitext(ffile)[0] |
| 343 | fext = os.path.splitext(ffile)[1] |
| 344 | |
| 345 | with open(out_path + '/' + froot + '_debug' + fext, 'w') as ofile: |
| 346 | for subname in subnames: |
| 347 | subno = filenos[subname] |
| 348 | print(str(subno) + '\t' + subname, file=ofile) |
| 349 | |
| 350 | print('\n', file=ofile) |
| 351 | |
| 352 | lineno = -1 |
| 353 | for line in inplines: |
| 354 | lineno += 1 |
| 355 | print(str(linedest[lineno]) + '\t' + line, file=ofile) |
| 356 | |
| 357 | # Reset all linedest[] entries except the bottommost entry for each subcircuit. |
| 358 | lineno = len(inplines) |
| 359 | subrefs = [0] * len(subnames) |
| 360 | while lineno > 0: |
| 361 | lineno -= 1 |
| 362 | if linedest[lineno] >= 0: |
| 363 | if subrefs[linedest[lineno]] == 0: |
| 364 | subrefs[linedest[lineno]] = 1 |
| 365 | else: |
| 366 | linedest[lineno] = -2 |
| 367 | |
| 368 | # Print the original file, including each of the new files. |
| 369 | # Also print out all lines marked "-1" or "-3" |
| 370 | |
| 371 | with open(out_path + '/' + froot + fext, 'w') as ofile: |
| 372 | lineno = -1 |
| 373 | subno = -1 |
| 374 | for line in inplines: |
| 375 | lineno += 1 |
| 376 | if linedest[lineno] == -1 or linedest[lineno] == -3 : |
| 377 | print(line, file=ofile) |
| 378 | elif linedest[lineno] >= 0: |
| 379 | for subname in subnames: |
| 380 | if filenos[subname] == linedest[lineno]: |
| 381 | fext = os.path.splitext(in_file)[1] |
| 382 | if subname == os.path.splitext(froot)[0]: |
| 383 | fext = '_split' + fext |
| 384 | break |
| 385 | print('.include ' + subname + fext, file=ofile) |
| 386 | subno = linedest[lineno] |
| 387 | |
| 388 | if __name__ == '__main__': |
| 389 | debug = False |
| 390 | |
| 391 | if len(sys.argv) == 1: |
| 392 | print("No options given to split_one_spice.py.") |
| 393 | usage() |
| 394 | sys.exit(0) |
| 395 | |
| 396 | optionlist = [] |
| 397 | arguments = [] |
| 398 | |
| 399 | for option in sys.argv[1:]: |
| 400 | if option.find('-', 0) == 0: |
| 401 | optionlist.append(option) |
| 402 | else: |
| 403 | arguments.append(option) |
| 404 | |
| 405 | if len(arguments) != 2: |
| 406 | print("Wrong number of arguments given to split_one_spice.py.") |
| 407 | usage() |
| 408 | sys.exit(0) |
| 409 | |
| 410 | if '-debug' in optionlist: |
| 411 | debug = True |
| 412 | |
| 413 | inpath = arguments[0] |
| 414 | outpath = arguments[1] |
| 415 | do_one_file = False |
| 416 | |
| 417 | if not os.path.exists(inpath): |
| 418 | print('No such source file ' + inpath) |
| 419 | sys.exit(1) |
| 420 | |
| 421 | if not os.path.isfile(inpath): |
| 422 | print('Input path ' + inpath + ' is not a file.') |
| 423 | sys.exit(1) |
| 424 | |
| 425 | convert_file(inpath, outpath) |
| 426 | |
| 427 | print('Done.') |
| 428 | exit(0) |