blob: 86a448a79cb235284a6973629697643b4bf8907b [file] [log] [blame]
Tim Edwards7eb7ab12020-08-17 22:21:10 -04001#!/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
14import os
15import sys
16import re
17import glob
18
19def usage():
20 print('split_one_spice.py <path_to_input> <path_to_output>')
21
22def 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 Edwards475b5272020-08-25 14:05:50 -0400297 # ignore "* statistics" and "* mismatch" lines.
Tim Edwards7eb7ab12020-08-17 22:21:10 -0400298
299 lineno = 0
300 for line in inplines[1:]:
301 lineno += 1
Tim Edwards475b5272020-08-25 14:05:50 -0400302 if line.startswith('*') and ('statistics' in line or 'mismatch' in line):
303 continue
Tim Edwards7eb7ab12020-08-17 22:21:10 -0400304 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
388if __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)