blob: dc2fd04d382ca89e47ab8aa1e7a33cd07f18b7c5 [file] [log] [blame]
Tim Edwards9d3debb2020-10-20 20:52:18 -04001#!/usr/bin/env python3
Tim Edwards55f4d0e2020-07-05 15:41:02 -04002#
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
20import re
21import os
22import sys
23import textwrap
24
25def 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
322if __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)