blob: 603b28af8104799169099da92ed903d6f77ee3ac [file] [log] [blame]
emayecs5656b2b2021-08-04 12:44:13 -04001#!/usr/bin/env python3
emayecs5966a532021-07-29 10:07:02 -04002"""
3cace_gensim.py
4This is the main part of the automatic characterization engine. It takes
5a JSON simulation template file as input and parses it for information on
6how to construct files for the characterization simulations. Output is
7a number of simulation files (for now, at least, in ng-spice format).
8
9Usage:
10
11cace_gensim.py [<root_path>] [<option> ...]
12
13 <root_path> is the root of all the other path names, if the other
14 path names are not full paths. If the other pathnames are all
15 full paths, then <root_path> may be omitted.
16
17options:
18
19 -simdir <path>
20 is the location where simulation files and data should be placed.
21 -datasheetdir <path>
22 is the location of the JSON file describing the characterization
23 -testbenchdir <path>
24 is the location of the netlists for the characterization methods
25 -designdir <path>
26 is the location of the netlist for the device-under-test
27 -layoutdir <path>
28 is the location of the layout netlist for the device-under-test
29 -datasheet <name>
30 is the name of the datasheet JSON file
31 -method <name>, ...
32 is a list of one or more names of methods to simulate. If omitted,
33 all methods are run for a complete characterization.
34 -local
35 indicates that cace_gensim is being run locally, not on the CACE
36 server, simulation conditions should be output along with results;
37 'local' mode implies that results are not posted to the marketplace
38 after simulation, and result files are kept.
39 -bypass
40 acts like remote CACE by running all simulations in one batch and
41 posting to the marketplace. Does not generate status reports.
42 -keep
43 test mode: keep all files after simulation
44 -plot
45 test mode: generate plot (.png) files locally
46 -nopost
47 test mode: do not post results to the marketplace
48 -nosim
49 test mode: set up all files for simulation but do not simulate
50
51Quick local run---Use:
52
53 cace_gensim.py <root_dir> -local -method=<method_name>
54
55e.g.,
56
57 cace_gensim.py ~/design/XBG_1V23LC_V01 -local -method=DCVOLTAGE_VBG.1
58"""
59
60import os
61import sys
62import json
63import re
64import time
65import shutil
66import signal
67import datetime
68import subprocess
69import faulthandler
70from functools import reduce
71from spiceunits import spice_unit_convert
72from fix_libdirs import fix_libdirs
73
emayecsb2487ae2021-08-05 10:30:13 -040074import config
emayecs5966a532021-07-29 10:07:02 -040075
emayecsb2487ae2021-08-05 10:30:13 -040076# Values obtained from config:
emayecs5966a532021-07-29 10:07:02 -040077#
emayecsb2487ae2021-08-05 10:30:13 -040078apps_path = config.apps_path
emayecs5966a532021-07-29 10:07:02 -040079launchproc = []
80
81def construct_dut_from_path(pname, pathname, pinlist, foundry, node):
82 # Read the indicated file, find the .subckt line, and copy out the
83 # pin names and DUT name. Complain if pin names don't match pin names
84 # in the datasheet.
85 # NOTE: There may be more than one subcircuit in the netlist, so
86 # insist upon the actual DUT (pname)
87
88 subrex = re.compile('^[^\*]*[ \t]*.subckt[ \t]+(.*)$', re.IGNORECASE)
89 noderex = re.compile('\*\*\* Layout tech:[ \t]+([^ \t,]+),[ \t]+foundry[ \t]+([^ \t]+)', re.IGNORECASE)
90 outline = ""
91 dutname = ""
92 if not os.path.isfile(pathname):
93 print('Error: No design netlist file ' + pathname + ' found.')
94 return outline
95
96 # First pull in all lines of the file and concatenate all continuation
97 # lines.
98 with open(pathname, 'r') as ifile:
99 duttext = ifile.read()
100
101 dutlines = duttext.replace('\n+', ' ').splitlines()
102 found = 0
103 for line in dutlines:
104 lmatch = noderex.match(line)
105 if lmatch:
106 nlnode = lmatch.group(1)
107 nlfoundry = lmatch.group(2)
108 if nlfoundry != foundry:
109 print('Error: Foundry is ' + foundry + ' in spec sheet, ' + nlfoundry + ' in netlist.')
110 # Not yet fixed in Electric
111 ## return ""
112 if nlnode != node:
113 # Hack for legacy node name
114 if nlnode == 'XH035A' and node == 'XH035':
115 pass
116 else:
117 print('Error: Node is ' + node + ' in spec sheet, ' + nlnode + ' in netlist.')
118 # Not yet fixed in Electric
119 ## return ""
120 lmatch = subrex.match(line)
121 if lmatch:
122 rest = lmatch.group(1)
123 tokens = rest.split()
124 dutname = tokens[0]
125 if dutname == pname:
126 outline = outline + 'X' + dutname + ' '
127 for pin in tokens[1:]:
128 upin = pin.upper()
129 try:
130 pinmatch = next(item for item in pinlist if item['name'].upper() == upin)
131 except StopIteration:
132 # Maybe this is not the DUT?
133 found = 0
134 # Try the next line
135 break
136 else:
137 outline = outline + pin + ' '
138 found += 1
139
140 if found == 0 and dutname == "":
141 print('File ' + pathname + ' does not contain any subcircuits!')
142 raise SyntaxError('File ' + pathname + ' does not contain any subcircuits!')
143 elif found == 0:
144 if dutname != pname:
145 print('File ' + pathname + ' does not have a subcircuit named ' + pname + '!')
146 raise SyntaxError('File ' + pathname + ' does not have a subcircuit named ' + pname + '!')
147 else:
148 print('Pins in schematic: ' + str(tokens[1:]))
149 print('Pins in datasheet: ', end='')
150 for pin in pinlist:
151 print(pin['name'] + ' ', end='')
152 print('')
153 print('File ' + pathname + ' subcircuit ' + pname + ' does not have expected pins!')
154 raise SyntaxError('File ' + pathname + ' subcircuit ' + pname + ' does not have expected pins!')
155 elif found != len(pinlist):
156 print('File ' + pathname + ' does not contain the project DUT ' + pname)
157 print('or not all pins of the DUT were found.')
158 print('Pinlist is : ', end='')
159 for pinrec in pinlist:
160 print(pinrec['name'] + ' ', end='')
161 print('')
162
163 print('Length of pinlist is ' + str(len(pinlist)))
164 print('Number of pins found in subcircuit call is ' + str(found))
165 raise SyntaxError('File ' + pathname + ' does not contain the project DUT!')
166 else:
167 outline = outline + dutname + '\n'
168 return outline
169
170conditiontypes = {
171 "VOLTAGE": 1,
172 "DIGITAL": 2,
173 "CURRENT": 3,
174 "RISETIME": 4,
175 "FALLTIME": 5,
176 "RESISTANCE": 6,
177 "CAPACITANCE": 7,
178 "TEMPERATURE": 8,
179 "FREQUENCY": 9,
180 "CORNER": 10,
181 "SIGMA": 11,
182 "ITERATIONS": 12,
183 "TIME": 13
184}
185
186# floating-point numeric sequence generators, to be used with condition generator
187
188def linseq(condition, unit, start, stop, step):
189 a = float(start)
190 e = float(stop)
191 s = float(step)
192 while (a < e + s):
193 if (a > e):
194 yield (condition, unit, stop)
195 else:
196 yield (condition, unit, str(a))
197 a = a + s
198
199def logseq(condition, unit, start, stop, step):
200 a = float(start)
201 e = float(stop)
202 s = float(step)
203 while (a < e * s):
204 if (a > e):
205 yield (condition, unit, stop)
206 else:
207 yield (condition, unit, str(a))
208 a = a * s
209
210# binary (integer) numeric sequence generators, to be used with condition generator
211
212def bindigits(n, bits):
213 s = bin(n & int("1" * bits, 2))[2:]
214 return ("{0:0>%s}" % (bits)).format(s)
215
216def twos_comp(val, bits):
217 """compute the 2's compliment of int value val"""
218 if (val & (1 << (bits - 1))) != 0: # if sign bit is set e.g., 8bit: 128-255
219 val = val - (1 << bits) # compute negative value
220 return val # return positive value as is
221
222def bcount(condition, unit, start, stop, step):
223 blen = len(start)
224 a = eval('0b' + start)
225 e = eval('0b' + stop)
226 if a > e:
227 a = twos_comp(a, blen)
228 e = twos_comp(e, blen)
229 s = int(step)
230 while (a < e + s):
231 if (a > e):
232 bstr = bindigits(e, blen)
233 else:
234 bstr = bindigits(a, blen)
235 yield (condition, unit, bstr)
236 a = a + s
237
238def bshift(condition, unit, start, stop, step):
239 a = eval('0b' + start)
240 e = eval('0b' + stop)
241 if a > e:
242 a = twos_comp(a, blen)
243 e = twos_comp(e, blen)
244 s = int(step)
245 while (a < e * s):
246 if (a > e):
247 bstr = bindigits(e, blen)
248 else:
249 bstr = bindigits(a, blen)
250 yield (condition, unit, bstr)
251 a = a * s
252
253# define a generator for conditions. Given a condition (dictionary),
254# return (as a yield) each specified condition as a
255# 3-tuple (condition_type, value, unit)
256
257def condition_gen(cond):
258 lcond = cond['condition']
259 if "unit" in cond:
260 unit = cond['unit']
261 else:
262 unit = ''
263
264 if "enum" in cond:
265 for i in cond["enum"]:
266 yield(lcond, unit, i)
267 elif "min" in cond and "max" in cond and "linstep" in cond:
268 if unit == "'b" or lcond.split(':', 1)[0] == 'DIGITAL':
269 yield from bcount(lcond, unit, cond["min"], cond["max"], cond["linstep"])
270 else:
271 yield from linseq(lcond, unit, cond["min"], cond["max"], cond["linstep"])
272 elif "min" in cond and "max" in cond and "logstep" in cond:
273 if unit == "'b" or lcond.split(':', 1)[0] == 'DIGITAL':
274 yield from bshift(lcond, unit, cond["min"], cond["max"], cond["logstep"])
275 else:
276 yield from logseq(lcond, unit, cond["min"], cond["max"], cond["logstep"])
277 elif "min" in cond and "max" in cond and "typ" in cond:
278 yield(lcond, unit, cond["min"])
279 yield(lcond, unit, cond["typ"])
280 yield(lcond, unit, cond["max"])
281 elif "min" in cond and "max" in cond:
282 yield(lcond, unit, cond["min"])
283 yield(lcond, unit, cond["max"])
284 elif "min" in cond and "typ" in cond:
285 yield(lcond, unit, cond["min"])
286 yield(lcond, unit, cond["typ"])
287 elif "max" in cond and "typ" in cond:
288 yield(lcond, unit, cond["typ"])
289 yield(lcond, unit, cond["max"])
290 elif "min" in cond:
291 yield(lcond, unit, cond["min"])
292 elif "max" in cond:
293 yield(lcond, unit, cond["max"])
294 elif "typ" in cond:
295 yield(lcond, unit, cond["typ"])
296
297# Find the maximum time to run a simulation. This is the maximum of:
298# (1) maximum value, if method is RISETIME or FALLTIME, and (2) maximum
299# RISETIME or FALLTIME of any condition.
300#
301# "lcondlist" is the list of local conditions extended by the list of
302# all global conditions that are not overridden by local values.
303#
304# NOTE: This list is limited to rise and fall time values, as they are
305# the only time constraints known to cace_gensim at this time. This list
306# will be extended as more simulation methods are added.
307
308def findmaxtime(param, lcondlist):
309 maxtime = 0.0
310 try:
311 simunit = param['unit']
312 except KeyError:
313 # Plots has no min/max/typ so doesn't require units.
314 if 'plot' in param:
315 return maxtime
316
317 maxval = 0.0
318 found = False
319 if 'max' in param:
320 prec = param['max']
321 if 'target' in prec:
322 pmax = prec['target']
323 try:
324 maxval = float(spice_unit_convert([simunit, pmax], 'time'))
325 found = True
326 except:
327 pass
328 if not found and 'typ' in param:
329 prec = param['typ']
330 if 'target' in prec:
331 ptyp = prec['target']
332 try:
333 maxval = float(spice_unit_convert([simunit, ptyp], 'time'))
334 found = True
335 except:
336 pass
337 if not found and 'min' in param:
338 prec = param['min']
339 if 'target' in prec:
340 pmin = prec['target']
341 try:
342 maxval = float(spice_unit_convert([simunit, pmin], 'time'))
343 found = True
344 except:
345 pass
346 if maxval > maxtime:
347 maxtime = maxval
348 for cond in lcondlist:
349 condtype = cond['condition'].split(':', 1)[0]
350 # print ('condtype ' + condtype)
351 if condtype == 'RISETIME' or condtype == 'FALLTIME':
352 condunit = cond['unit']
353 maxval = 0.0
354 if 'max' in cond:
355 maxval = float(spice_unit_convert([condunit, cond['max']], 'time'))
356 elif 'enum' in cond:
357 maxval = float(spice_unit_convert([condunit, cond['enum'][-1]], 'time'))
358 elif 'typ' in cond:
359 maxval = float(spice_unit_convert([condunit, cond['typ']], 'time'))
360 elif 'min' in cond:
361 maxval = float(spice_unit_convert([condunit, cond['min']], 'time'))
362 if maxval > maxtime:
363 maxtime = maxval
364
365 return maxtime
366
367# Picked up from StackOverflow: Procedure to remove non-unique entries
368# in a list of lists (as always, thanks StackOverflow!).
369
370def uniquify(seq):
371 seen = set()
372 return [x for x in seq if str(x) not in seen and not seen.add(str(x))]
373
374# Insert hints that have been selected in the characterization tool for
375# aid in getting stubborn simulations to converge, or to avoid failures
376# due to floating nodes, etc. The hints are somewhat open-ended and can
377# be extended as needed. NOTE: Hint "method" selects the parameter
378# method and is handled outside this routine, which only adds lines to
379# the simulation netlist.
380
381def insert_hints(param, ofile):
382 if 'hints' in param:
383 phints = param['hints']
384 if 'reltol' in phints:
385 value = phints['reltol']
386 ofile.write('.options reltol = ' + value + '\n')
387 if 'rshunt' in phints:
388 value = phints['rshunt']
389 ofile.write('.options rshunt = ' + value + '\n')
390 if 'itl1' in phints:
391 value = phints['itl1']
392 ofile.write('.options itl1 = ' + value + '\n')
393 if 'nodeset' in phints:
394 value = phints['nodeset']
395 # replace '/' in nodeset with '|' to match character replacement done
396 # on the output of magic.
397 ofile.write('.nodeset ' + value.replace('/', '|') + '\n')
398 if 'include' in phints:
399 value = phints['include']
400 ofile.write('.include ' + value + '\n')
401
402# Replace the substitution token ${INCLUDE_DUT} with the contents of the DUT subcircuit
403# netlist file. "functional" is a list of IP block names that are to be searched for in
404# .include lines in the netlist and replaced with functional view equivalents (if such
405# exist).
406
407def inline_dut(filename, functional, rootpath, ofile):
408 comtrex = re.compile(r'^\*') # SPICE comment
409 inclrex = re.compile(r'[ \t]*\.include[ \t]+["\']?([^"\' \t]+)["\']?', re.IGNORECASE) # SPICE include statement
410 braktrex = re.compile(r'([^ \t]+)\[([^ \t])\]', re.IGNORECASE) # Node name with brackets
411 subcrex = re.compile(r'[ \t]*x([^ \t]+)[ \t]+(.*)$', re.IGNORECASE) # SPICE subcircuit line
412 librex = re.compile(r'(.*)__(.*)', re.IGNORECASE)
413 endrex = re.compile(r'[ \t]*\.end[ \t]*', re.IGNORECASE)
414 endsrex = re.compile(r'[ \t]*\.ends[ \t]*', re.IGNORECASE)
415 # IP names in the ridiculously complicated form
416 # <user_path>/design/ip/<proj_name>/<version>/<spi-type>/<proj_name>/<proj_netlist>
417 ippathrex = re.compile(r'(.+)/design/ip/([^/]+)/([^/]+)/([^/]+)/([^/]+)/([^/ \t]+)')
418 locpathrex = re.compile(r'(.+)/design/([^/]+)/spi/([^/]+)/([^/ \t]+)')
419 # This form does not appear on servers but is used if an IP block is being prepared locally.
420 altpathrex = re.compile(r'(.+)/design/([^/]+)/([^/]+)/([^/]+)/([^/ \t]+)')
421 # Local IP names in the form
422 # <user_path>/design/<project>/spi/<spi-type>/<proj_netlist>
423
424 # To be completed
425 with open(filename, 'r') as ifile:
426 nettext = ifile.read()
427
428 netlines = nettext.replace('\n+', ' ').splitlines()
429 for line in netlines:
430 subsline = line
431 cmatch = comtrex.match(line)
432 if cmatch:
433 print(line, file=ofile)
434 continue
435 # Check for ".end" which should be removed (but not ".ends", which must remain)
436 ematch = endrex.match(line)
437 if ematch:
438 smatch = endsrex.match(line)
439 if not smatch:
440 continue
441 imatch = inclrex.match(line)
442 if imatch:
443 incpath = imatch.group(1)
444 # Substitution behavior is complicated due to the difference between netlist
445 # files from schematic capture vs. layout and read-only vs. read-write IP.
446 incroot = os.path.split(incpath)[1]
447 incname = os.path.splitext(incroot)[0]
448 lmatch = librex.match(incname)
449 if lmatch:
450 ipname = lmatch.group(2)
451 else:
452 ipname = incname
453 if ipname.upper() in functional:
454 # Search for functional view (depends on if this is a read-only IP or
455 # read-write local subcircuit)
456 funcpath = None
457 ippath = ippathrex.match(incpath)
458 if ippath:
459 userpath = ippath.group(1)
460 ipname2 = ippath.group(2)
461 ipversion = ippath.group(3)
462 spitype = ippath.group(4)
463 ipname3 = ippath.group(5)
464 ipnetlist = ippath.group(6)
465 funcpath = userpath + '/design/ip/' + ipname2 + '/' + ipversion + '/spi-func/' + ipname + '.spi'
466 else:
467 locpath = locpathrex.match(incpath)
468 if locpath:
469 userpath = locpath.group(1)
470 ipname2 = locpath.group(2)
471 spitype = locpath.group(3)
472 ipnetlist = locpath.group(4)
473 funcpath = userpath + '/design/' + ipname2 + '/spi/func/' + ipname + '.spi'
474 else:
475 altpath = altpathrex.match(incpath)
476 if altpath:
477 userpath = altpath.group(1)
478 ipname2 = altpath.group(2)
479 spitype = altpath.group(3)
480 ipname3 = altpath.group(4)
481 ipnetlist = altpath.group(5)
482 funcpath = userpath + '/design/' + ipname2 + '/spi/func/' + ipname + '.spi'
483
484 funcpath = os.path.expanduser(funcpath)
485 if funcpath and os.path.exists(funcpath):
486 print('Subsituting functional view for IP block ' + ipname)
487 print('Original netlist is ' + incpath)
488 print('Functional netlist is ' + funcpath)
489 subsline = '.include ' + funcpath
490 elif funcpath:
491 print('Original netlist is ' + incpath)
492 print('Functional view specified but no functional view found.')
493 print('Tried looking for ' + funcpath)
494 print('Retaining original view.')
495 else:
496 print('Original netlist is ' + incpath)
497 print('Cannot make sense of netlist path to find functional view.')
498
499 # If include file name is in <lib>__<cell> format (from electric) and the
500 # functional view is not, then find the subcircuit call and replace the
501 # subcircuit name. At least at the moment, the vice versa case does not
502 # happen.
503 smatch = subcrex.match(line)
504 if smatch:
505 subinst = smatch.group(1)
506 tokens = smatch.group(2).split()
507 # Need to test for parameters passed to subcircuit. The actual subcircuit
508 # name occurs before any parameters.
509 params = []
510 pins = []
511 for token in tokens:
512 if '=' in token:
513 params.append(token)
514 else:
515 pins.append(token)
516
517 subname = pins[-1]
518 pins = pins[0:-1]
519 lmatch = librex.match(subname)
520 if lmatch:
521 testname = lmatch.group(1)
522 if testname.upper() in functional:
523 subsline = 'X' + subinst + ' ' + ' '.join(pins) + ' ' + testname + ' ' + ' '.join(params)
524
525 # Remove any array brackets from node names in the top-level subcircuit, because they
526 # interfere with the array notation used by XSPICE which may be present in functional
527 # views (replace bracket characters with underscores).
528 # subsline = subsline.replace('[', '_').replace(']', '_')
529 #
530 # Do this *only* when there are no spaces inside the brackets, or else any XSPICE
531 # primitives in the netlist containing arrays will get messed up.
532 subsline = braktrex.sub(r'\1_\2_', subsline)
533
534 ofile.write(subsline + '\n')
535
536 ofile.write('\n')
537
538# Define how to write a simulation file by making substitutions into a
539# template schematic.
540
541def substitute(filename, fileinfo, template, simvals, maxtime, schemline,
542 localmode, param):
543 """Simulation derived by substitution into template schematic"""
544
545 # Regular expressions
546 varex = re.compile(r'(\$\{[^ \}\t]+\})') # variable name ${name}
547 defaultex = re.compile(r'\$\{([^=]+)=([^=\}]+)\}') # name in ${name=default} format
548 condpinex = re.compile(r'\$\{([^:]+):([^:\}]+)\}') # name in ${cond:pin} format
549 condex = re.compile(r'\$\{([^\}]+)\}') # name in ${cond} format
550 sweepex = re.compile(r'\$\{([^\}]+):SWEEP([^\}]+)\}') # name in ${cond:[pin:]sweep} format
551 pinex = re.compile(r'PIN:([^:]+):([^:]+)') # name in ${PIN:pin_name:net_name} format
552 funcrex = re.compile(r'FUNCTIONAL:([^:]+)') # name in ${FUNCTIONAL:ip_name} format
553 colonsepex = re.compile(r'^([^:]+):([^:]+)$') # a:b (colon-separated values)
554 vectrex = re.compile(r'([^\[]+)\[([0-9]+)\]') # pin name is a vector signal
555 vect2rex = re.compile(r'([^<]+)<([0-9]+)>') # pin name is a vector signal (alternate style)
556 libdirrex = re.compile(r'.lib[ \t]+(.*)[ \t]+') # pick up library name from .lib
557 vinclrex = re.compile(r'[ \t]*`include[ \t]+"([^"]+)"') # verilog include statement
558
559 # Information about the DUT
560 simfilepath = fileinfo['simulation-path']
561 schempath = fileinfo['design-netlist-path']
562 schemname = fileinfo['design-netlist-name']
563 testbenchpath = fileinfo['testbench-netlist-path']
564 rootpath = fileinfo['root-path']
565 schempins = schemline.upper().split()[1:-1]
566 simpins = [None] * len(schempins)
567
568 suffix = os.path.splitext(template)[1]
569 functional = []
570
571 # Read ifile into a list
572 # Concatenate any continuation lines
573 with open(template, 'r') as ifile:
574 simtext = ifile.read()
575
576 simlines = simtext.replace('\n+', ' ').splitlines()
577
578 # Make initial pass over contents of template file, looking for SWEEP
579 # entries, and collapse simvals accordingly.
580
581 sweeps = []
582 for line in simlines:
583 sublist = sweepex.findall(line)
584 for pattern in sublist:
585 condition = pattern[0]
586 try:
587 entry = next(item for item in sweeps if item['condition'] == condition)
588 except (StopIteration, KeyError):
589 print("Did not find condition " + condition + " in sweeps.")
590 print("Pattern = " + str(pattern))
591 print("Sublist = " + str(sublist))
592 print("Sweeps = " + str(sweeps))
593 entry = {'condition':condition}
594 sweeps.append(entry)
595
596 # Find each entry in simvals with the same condition.
597 # Record the minimum, maximum, and step for substitution, at the same
598 # time removing that item from the entry.
599 lvals = []
600 units = ''
601 for simval in simvals:
602 try:
603 simrec = next(item for item in simval if item[0] == condition)
604 except StopIteration:
605 print('No condition = ' + condition + ' in record:\n')
606 ptext = str(simval) + '\n'
607 sys.stdout.buffer.write(ptext.encode('utf-8'))
608 else:
609 units = simrec[1]
610 lvals.append(float(simrec[2]))
611 simval.remove(simrec)
612
613 # Remove non-unique entries from lvals
614 lvals = list(set(lvals))
615
616 # Now parse lvals for minimum/maximum
617 entry['unit'] = units
618 minval = min(lvals)
619 maxval = max(lvals)
620 entry['START'] = str(minval)
621 entry['END'] = str(maxval)
622 numvals = len(lvals)
623 if numvals > 1:
624 entry['STEPS'] = str(numvals)
625 entry['STEP'] = str((maxval - minval) / (numvals - 1))
626 else:
627 entry['STEPS'] = "1"
628 entry['STEP'] = str(minval)
629
630 # Remove non-unique entries from simvals
631 simvals = uniquify(simvals)
632
633 simnum = 0
634 testbenches = []
635 for simval in simvals:
636 # Create the file
637 simnum += 1
638 simfilename = simfilepath + '/' + filename + '_' + str(simnum) + suffix
639 controlblock = False
640 with open(simfilename, 'w') as ofile:
641 for line in simlines:
642
643 # Check if the parser is in the ngspice control block section
644 if '.control' in line:
645 controlblock = True
646 elif '.endc' in line:
647 controlblock = False
648 elif controlblock == True:
649 ofile.write('set sqrnoise\n')
650 # This will need to be more nuanced if controlblock is used
651 # to do more than just insert the noise sim hack.
652 controlblock = False
653
654 # This will be replaced
655 subsline = line
656
657 # Find all variables to substitute
658 for patmatch in varex.finditer(line):
659 pattern = patmatch.group(1)
660 # If variable is in ${x=y} format, it declares a default value
661 # Remove the =y default part and keep it for later if needed.
662 defmatch = defaultex.match(pattern)
663 if defmatch:
664 default = defmatch.group(2)
665 vpattern = '${' + defmatch.group(1) + '}'
666 else:
667 default = []
668 vpattern = pattern
669
670 repl = []
671 no_repl_ok = False
672 sweeprec = sweepex.match(vpattern)
673 if sweeprec:
674 sweeptype = sweeprec.group(2)
675 condition = sweeprec.group(1)
676
677 entry = next(item for item in sweeps if item['condition'] == condition)
678 uval = spice_unit_convert((entry['unit'], entry[sweeptype]))
679 repl = str(uval)
680 else:
681 cond = condex.match(vpattern)
682 if cond:
683 condition = cond.group(1)
684
685 # Check if the condition contains a pin vector
686 lmatch = vectrex.match(condition)
687 if lmatch:
688 pinidx = int(lmatch.group(2))
689 vcondition = lmatch.group(1)
690 else:
691 lmatch = vect2rex.match(condition)
692 if lmatch:
693 pinidx = int(lmatch.group(2))
694 vcondition = lmatch.group(1)
695
696 try:
697 entry = next((item for item in simval if item[0] == condition))
698 except (StopIteration, KeyError):
699 # check against known names (to-do: change if block to array of procs)
700 if condition == 'N':
701 repl = str(simnum)
702 elif condition == 'MAXTIME':
703 repl = str(maxtime)
704 elif condition == 'STEPTIME':
705 repl = str(maxtime / 100)
706 elif condition == 'DUT_PATH':
707 repl = schempath + '/' + schemname + '\n'
708 # DUT_PATH is required and is a good spot to
709 # insert hints (but deprecated in fafor of INCLUDE_DUT)
710 insert_hints(param, ofile)
711 elif condition == 'INCLUDE_DUT':
712 if len(functional) == 0:
713 repl = '.include ' + schempath + '/' + schemname + '\n'
714 else:
715 inline_dut(schempath + '/' + schemname, functional, rootpath, ofile)
716 repl = '** End of in-line DUT subcircuit'
717 insert_hints(param, ofile)
718 elif condition == 'DUT_CALL':
719 repl = schemline
720 elif condition == 'DUT_NAME':
721 # This verifies pin list of schematic vs. the netlist.
722 repl = schemline.split()[-1]
723 elif condition == 'FILENAME':
724 repl = filename
725 elif condition == 'RANDOM':
726 repl = str(int(time.time() * 1000) & 0x7fffffff)
727 # Stack math operators. Perform specified math
728 # operation on the last two values and replace.
729 #
730 # Note that ngspice is finicky about space around "=" so
731 # handle this in a way that keeps ngspice happy.
732 elif condition == '+':
733 smatch = varex.search(subsline)
734 watchend = smatch.start()
735 ltok = subsline[0:watchend].replace('=', ' = ').split()
736 ntok = ltok[:-2]
737 ntok.append(str(float(ltok[-2]) + float(ltok[-1])))
738 subsline = ' '.join(ntok).replace(' = ', '=') + line[patmatch.end():]
739 repl = ''
740 no_repl_ok = True
741 elif condition == '-':
742 smatch = varex.search(subsline)
743 watchend = smatch.start()
744 ltok = subsline[0:watchend].replace('=', ' = ').split()
745 ntok = ltok[:-2]
746 ntok.append(str(float(ltok[-2]) - float(ltok[-1])))
747 subsline = ' '.join(ntok).replace(' = ', '=') + line[patmatch.end():]
748 repl = ''
749 no_repl_ok = True
750 elif condition == '*':
751 smatch = varex.search(subsline)
752 watchend = smatch.start()
753 ltok = subsline[0:watchend].replace('=', ' = ').split()
754 ntok = ltok[:-2]
755 ntok.append(str(float(ltok[-2]) * float(ltok[-1])))
756 subsline = ' '.join(ntok).replace(' = ', '=') + line[patmatch.end():]
757 repl = ''
758 no_repl_ok = True
759 elif condition == '/':
760 smatch = varex.search(subsline)
761 watchend = smatch.start()
762 ltok = subsline[0:watchend].replace('=', ' = ').split()
763 ntok = ltok[:-2]
764 ntok.append(str(float(ltok[-2]) / float(ltok[-1])))
765 subsline = ' '.join(ntok).replace(' = ', '=') + line[patmatch.end():]
766 repl = ''
767 no_repl_ok = True
768 elif condition == 'MAX':
769 smatch = varex.search(subsline)
770 watchend = smatch.start()
771 ltok = subsline[0:watchend].replace('=', ' = ').split()
772 ntok = ltok[:-2]
773 ntok.append(str(max(float(ltok[-2]), float(ltok[-1]))))
774 subsline = ' '.join(ntok).replace(' = ', '=') + line[patmatch.end():]
775 repl = ''
776 no_repl_ok = True
777 elif condition == 'MIN':
778 smatch = varex.search(subsline)
779 watchend = smatch.start()
780 ltok = subsline[0:watchend].replace('=', ' = ').split()
781 ntok = ltok[:-2]
782 ntok.append(str(min(float(ltok[-2]), float(ltok[-1]))))
783 subsline = ' '.join(ntok).replace(' = ', '=') + line[patmatch.end():]
784 repl = ''
785 no_repl_ok = True
786 # 'NEG' acts on only the previous value in the string.
787 elif condition == 'NEG':
788 smatch = varex.search(subsline)
789 watchend = smatch.start()
790 ltok = subsline[0:watchend].replace('=', ' = ').split()
791 ntok = ltok[:-1]
792 ntok.append(str(-float(ltok[-1])))
793 subsline = ' '.join(ntok).replace(' = ', '=') + line[patmatch.end():]
794 repl = ''
795 no_repl_ok = True
796 elif condition.find('PIN:') == 0:
797 # Parse for ${PIN:<pin_name>:<net_name>}
798 # Replace <pin_name> with index of pin from DUT subcircuit
799 pinrec = pinex.match(condition)
800 pinname = pinrec.group(1).upper()
801 netname = pinrec.group(2).upper()
802 try:
803 idx = schempins.index(pinname)
804 except ValueError:
805 repl = netname
806 else:
807 repl = '${PIN}'
808 simpins[idx] = netname
809 elif condition.find('FUNCTIONAL:') == 0:
810 # Parse for ${FUNCTIONAL:<ip_name>}
811 # Add <ip_name> to "functional" array.
812 # 'FUNCTIONAL' declarations must come before 'INCLUDE_DUT' or else
813 # substitution will not be made. 'INCLUDE_DUT' must be used in place
814 # of 'DUT_PATH' to get the correct behavior.
815 funcrec = funcrex.match(condition)
816 ipname = funcrec.group(1)
817 functional.append(ipname.upper())
818 repl = '** Using functional view for ' + ipname
819 else:
820 if lmatch:
821 try:
822 entry = next((item for item in simval if item[0].split('[')[0].split('<')[0] == vcondition))
823 except:
824 # if no match, subsline remains as-is.
825 pass
826 else:
827 # Handle as vector bit slice (see below)
828 vlen = len(entry[2])
829 uval = entry[2][(vlen - 1) - pinidx]
830 repl = str(uval)
831 # else if no match, subsline remains as-is.
832
833 else:
834 if lmatch:
835 # pull signal at pinidx out of the vector.
836 # Note: DIGITAL assumes binary value. May want to
837 # allow general case of real-valued vectors, which would
838 # require a spice unit conversion routine without indexing.
839 vlen = len(entry[2])
840 uval = entry[2][(vlen - 1) - pinidx]
841 else:
842 uval = spice_unit_convert(entry[1:])
843 repl = str(uval)
844
845 if not repl and default:
846 # Use default if no match was found and default was specified
847 repl = default
848
849 if repl:
850 # Make the variable substitution
851 subsline = subsline.replace(pattern, repl)
852 elif not no_repl_ok:
853 print('Warning: Variable ' + pattern + ' had no substitution')
854
855 # Check if ${PIN} are in line. If so, order by index and
856 # rewrite pins in order
857 for i in range(len(simpins)):
858 if '${PIN}' in subsline:
859 if simpins[i]:
860 subsline = subsline.replace('${PIN}', simpins[i], 1)
861 else:
862 print("Error: simpins is " + str(simpins) + '\n')
863 print(" subsline is " + subsline + '\n')
864 print(" i is " + str(i) + '\n')
865
866 # Check for a verilog include file, and if any is found, copy it
867 # to the target simulation directory. Replace any leading path
868 # with the local current working directory '.'.
869 vmatch = vinclrex.match(subsline)
870 if vmatch:
871 incfile = vmatch.group(1)
872 incroot = os.path.split(incfile)[1]
873 curpath = os.path.split(template)[0]
874 incpath = os.path.abspath(os.path.join(curpath, incfile))
875 shutil.copy(incpath, simfilepath + '/' + incroot)
876 subsline = ' `include "./' + incroot + '"'
877
878 # Write the modified output line (with variable substitutions)
879 ofile.write(subsline + '\n')
880
881 # Add information about testbench file and conditions to datasheet JSON,
882 # which can be parsed by cace_launch.py.
883 testbench = {}
884 testbench['filename'] = simfilename
885 testbench['prefix'] = filename
886 testbench['conditions'] = simval
887 testbenches.append(testbench)
888
889 return testbenches
890
891# Define how to write simulation devices
892
893def generate_simfiles(datatop, fileinfo, arguments, methods, localmode):
894
895 # pull out the relevant part, which is "data-sheet"
896 dsheet = datatop['data-sheet']
897
898 # grab values held in 'fileinfo'
899 testbenchpath = fileinfo['testbench-netlist-path']
900
901 # electrical parameter list comes from "methods" if non-NULL.
902 # Otherwise, each entry in 'methods' is checked against the
903 # electrical parameters.
904
905 if 'electrical-params' in dsheet:
906 eparamlist = dsheet['electrical-params']
907 else:
908 eparamlist = []
909 if 'physical-params' in dsheet:
910 pparamlist = dsheet['physical-params']
911 else:
912 pparamlist = []
913
914 # If specific methods are called out for simulation using option "-method=", then
915 # generate the list of electrical parameters for those methods only.
916
917 if methods:
918 neweparamlist = []
919 newpparamlist = []
920 for method in methods:
921 # If method is <methodname>.<index>, simulate only the <index>th instance of
922 # the method.
923 if '.' in method:
924 (method, index) = method.split('.')
925 else:
926 index = []
927
928 if method == 'physical':
929 usedmethods = list(item for item in pparamlist if item['condition'] == index)
930 if not usedmethods:
931 print('Unknown parameter ' + index + ' requested in options. Ignoring.\n')
932 for used in usedmethods:
933 newpparamlist.append(used)
934
935 else:
936 usedmethods = list(item for item in eparamlist if item['method'] == method)
937 if not usedmethods:
938 print('Unknown method ' + method + ' requested in options. Ignoring.\n')
939 if index:
940 neweparamlist.append(usedmethods[int(index)])
941 else:
942 for used in usedmethods:
943 neweparamlist.append(used)
944
945 if not neweparamlist and not newpparamlist:
946 print('Warning: No valid methods given as options, so no simulations will be done.\n')
947 if neweparamlist:
948 for param in neweparamlist:
949 if 'display' in param:
950 ptext = 'Simulating parameter: ' + param['display'] + ' (' + param['method'] + ')\n'
951 else:
952 ptext = 'Simulating method: ' + param['method'] + '\n'
953 sys.stdout.buffer.write(ptext.encode('utf-8'))
954 eparamlist = neweparamlist
955 if newpparamlist:
956 for param in newpparamlist:
957 if 'display' in param:
958 ptext = 'Checking parameter: ' + param['display'] + ' (' + param['condition'] + ')\n'
959 else:
960 ptext = 'Checking parameter: ' + param['condition'] + '\n'
961 sys.stdout.buffer.write(ptext.encode('utf-8'))
962 pparamlist = newpparamlist
963
964 # Diagnostic
965 # print('pparamlist:')
966 # for param in pparamlist:
967 # ptext = param['condition'] + '\n'
968 # sys.stdout.buffer.write(ptext.encode('utf-8'))
969 # print('eparamlist:')
970 # for param in eparamlist:
971 # ptext = param['method'] + '\n'
972 # sys.stdout.buffer.write(ptext.encode('utf-8'))
973
974 # major subcategories of "data-sheet"
975 gcondlist = dsheet['global-conditions']
976
977 # Make a copy of the pin list in the datasheet, and expand any vectors.
978 pinlist = []
979 vectrex = re.compile(r"([^\[]+)\[([0-9]+):([0-9]+)\]")
980 vect2rex = re.compile(r"([^<]+)\<([0-9]+):([0-9]+)\>")
981 for pinrec in dsheet['pins']:
982 vmatch = vectrex.match(pinrec['name'])
983 if vmatch:
984 pinname = vmatch.group(1)
985 pinmin = vmatch.group(2)
986 pinmax = vmatch.group(3)
987 if int(pinmin) > int(pinmax):
988 pinmin = vmatch.group(3)
989 pinmax = vmatch.group(2)
990 for i in range(int(pinmin), int(pinmax) + 1):
991 newpinrec = pinrec.copy()
992 pinlist.append(newpinrec)
993 newpinrec['name'] = pinname + '[' + str(i) + ']'
994 else:
995 vmatch = vect2rex.match(pinrec['name'])
996 if vmatch:
997 pinname = vmatch.group(1)
998 pinmin = vmatch.group(2)
999 pinmax = vmatch.group(3)
1000 if int(pinmin) > int(pinmax):
1001 pinmin = vmatch.group(3)
1002 pinmax = vmatch.group(2)
1003 for i in range(int(pinmin), int(pinmax) + 1):
1004 newpinrec = pinrec.copy()
1005 pinlist.append(newpinrec)
1006 newpinrec['name'] = pinname + '<' + str(i) + '>'
1007 else:
1008 pinlist.append(pinrec)
1009
1010 # Make sure all local conditions define a pin. Those that are not
1011 # associated with a pin will have a null string for the pin name.
1012
1013 for cond in gcondlist:
1014 # Convert old style (separate condition, pin) to new style
1015 if 'pin' in cond and cond['pin'] != '':
1016 if ':' not in cond['condition']:
1017 cond['condition'] += ':' + cond['pin']
1018 cond.pop('pin', 0)
1019 if 'order' not in cond:
1020 try:
1021 cond['order'] = conditiontypes[cond['condition']]
1022 except:
1023 cond['order'] = 0
1024
1025 # Find DUT netlist file and capture the subcircuit call line
1026 schempath = fileinfo['design-netlist-path']
1027 schemname = fileinfo['design-netlist-name']
1028 pname = fileinfo['project-name']
1029 dutpath = schempath + '/' + schemname
1030 foundry = dsheet['foundry']
1031 node = dsheet['node']
1032 try:
1033 schemline = construct_dut_from_path(pname, dutpath, pinlist, foundry, node)
1034 except SyntaxError:
1035 print("Failure to construct a DUT subcircuit. Does the design have ports?")
1036 schemline = ''
1037
1038 if schemline == '':
1039 # Error finding DUT file. If only physical parameters are requested, this may
1040 # not be a failure (e.g., chip top level)
1041 if len(eparamlist) == 0:
1042 prescore = 'unknown'
1043 else:
1044 prescore = 'fail'
1045 else:
1046 prescore = 'pass'
1047
1048 methodsfound = {}
1049
1050 # electrical parameter types determine the simulation type. Simulation
1051 # types will be broken into individual routines (to be done)
1052
1053 for param in eparamlist:
1054
1055 # Break out name, method, and conditions as variables
1056 simtype = param['method']
1057
1058 # For methods with ":", the filename is the part before the colon.
1059 testbench = simtype.split(":")[0]
1060
1061 # If hint 'method' is applied, append the value to the method name.
1062 # If no such method exists, flag a warning and revert to the original.
1063
1064 testbench_orig = None
1065 if 'hints' in param:
1066 phints = param['hints']
1067 if 'method' in phints:
1068 testbench_orig = testbench
1069 testbench += phints['method']
1070
1071 if testbench == simtype:
1072 if arguments:
1073 if simtype not in arguments:
1074 continue
1075
1076 if simtype in methodsfound:
1077 fnum = methodsfound[simtype]
1078 fsuffix = '_' + str(fnum)
1079 methodsfound[simtype] = fnum + 1
1080 else:
1081 fsuffix = '_0'
1082 methodsfound[simtype] = 1
1083 else:
1084 if arguments:
1085 if testbench not in arguments:
1086 continue
1087
1088 if testbench in methodsfound:
1089 fnum = methodsfound[testbench]
1090 fsuffix = '_' + str(fnum)
1091 methodsfound[testbench] = fnum + 1
1092 else:
1093 fsuffix = '_0'
1094 methodsfound[testbench] = 1
1095
1096 lcondlist = param['conditions']
1097
1098 # Make sure all local conditions which define a pin are in condition:pin form
1099
1100 for cond in lcondlist:
1101 if 'pin' in cond and cond['pin'] != '':
1102 if not ':' in cond['condition']:
1103 cond['condition'] += ':' + cond['pin']
1104 cond.pop('pin', 0)
1105 if "order" not in cond:
1106 if cond["condition"].split(':', 1)[0] in conditiontypes:
1107 cond["order"] = conditiontypes[cond["condition"].split(':', 1)[0]]
1108 else:
1109 cond["order"] = 14
1110
1111 # Append to lcondlist any global conditions that aren't overridden by
1112 # local values for the electrical parameter's set of conditions.
1113
1114 grec = []
1115 for cond in gcondlist:
1116 try:
1117 test = next((item for item in lcondlist if item["condition"] == cond["condition"]))
1118 except StopIteration:
1119 grec.append(cond)
1120
1121 lcondlist.extend(grec) # Note this will permanently alter lcondlist
1122
1123 # Find the maximum simulation time required by this method
1124 # Simulations are ordered so that "risetime" and "falltime" simulations
1125 # on a pin will set the simulation time of any simulation of any other
1126 # electrical parameter on that same pin.
1127
1128 maxtime = findmaxtime(param, lcondlist)
1129 print("maxtime is " + str(maxtime))
1130
1131 # Sort the list for output conditions, ordering according to 'conditiontypes'.
1132
1133 list.sort(lcondlist, key=lambda k: k['order'])
1134
1135 # Find the length of each generator
1136 cgenlen = []
1137 for cond in lcondlist:
1138 cgenlen.append(len(list(condition_gen(cond))))
1139
1140 # The lengths of all generators multiplied together is the number of
1141 # simulations to be run
1142 numsims = reduce(lambda x, y: x * y, cgenlen)
1143 rlen = [x for x in cgenlen] # note floor division operator
1144
1145 # This code repeats each condition as necessary such that the final list
1146 # (transposed) is a complete set of unique condition combinations.
1147 cgensim = []
1148 for i in range(len(rlen)):
1149 mpre = reduce(lambda x, y: x * y, rlen[0:i], 1)
1150 mpost = reduce(lambda x, y: x * y, rlen[i + 1:], 1)
1151 clist = list(condition_gen(lcondlist[i]))
1152 duplist = [item for item in list(condition_gen(lcondlist[i])) for j in range(mpre)]
1153 cgensim.append(duplist * mpost)
1154
1155 # Transpose this list
1156 simvals = list(map(list, zip(*cgensim)))
1157
1158 # Generate filename prefix for this electrical parameter
1159 filename = testbench + fsuffix
1160
1161 # If methodtype is the name of a schematic netlist, then use
1162 # it and make substitutions
1163 # NOTE: Schematic methods are bundled with the DUT schematic
1164
1165 template = testbenchpath + '/' + testbench.lower() + '.spi'
1166
1167 if testbench_orig and not os.path.isfile(template):
1168 print('Warning: Alternate testbench ' + testbench + ' cannot be found.')
1169 print('Reverting to original testbench ' + testbench_orig)
1170 testbench = testbench_orig
1171 filename = testbench + fsuffix
1172 template = testbenchpath + '/' + testbench.lower() + '.spi'
1173
1174 if os.path.isfile(template):
1175 param['testbenches'] = substitute(filename, fileinfo, template,
1176 simvals, maxtime, schemline, localmode, param)
1177
1178 # For cosimulations, if there is a '.tv' file corresponding to the '.spi' file,
1179 # then make substitutions as for the .spi file, and place in characterization
1180 # directory.
1181
1182 vtemplate = testbenchpath + '/' + testbench.lower() + '.tv'
1183 if os.path.isfile(vtemplate):
1184 substitute(filename, fileinfo, vtemplate,
1185 simvals, maxtime, schemline, localmode, param)
1186
1187 else:
1188 print('Error: No testbench file ' + template + '.')
1189
1190 for param in pparamlist:
1191 # Break out name, method, and conditions as variables
1192 cond = param['condition']
1193 simtype = 'physical.' + cond
1194
1195 if arguments:
1196 if simtype not in arguments:
1197 continue
1198
1199 if simtype in methodsfound:
1200 fnum = methodsfound[simtype]
1201 fsuffix = '_' + str(fnum)
1202 methodsfound[simtype] = fnum + 1
1203 else:
1204 fsuffix = '_0'
1205 methodsfound[simtype] = 1
1206
1207 # Mark parameter as needing checking by cace_launch.
1208 param['check'] = 'true'
1209
1210 # Remove "order" keys
1211 for param in eparamlist:
1212 lcondlist = param['conditions']
1213 for cond in lcondlist:
1214 cond.pop('order', 0)
1215 gconds = dsheet['global-conditions']
1216 for cond in gconds:
1217 cond.pop('order', 0)
1218
1219 return prescore
1220
1221def check_layout_out_of_date(spipath, layoutpath):
1222 # Check if a netlist (spipath) is out-of-date relative to the layouts
1223 # (layoutpath). Need to read the netlist and check all of the subcells.
1224 need_capture = False
1225 if not os.path.isfile(spipath):
1226 need_capture = True
1227 elif not os.path.isfile(layoutpath):
1228 need_capture = True
1229 else:
1230 spi_statbuf = os.stat(spipath)
1231 lay_statbuf = os.stat(layoutpath)
1232 if spi_statbuf.st_mtime < lay_statbuf.st_mtime:
1233 # netlist exists but is out-of-date
1234 need_capture = True
1235 else:
1236 # only found that the top-level-layout is older than the
1237 # netlist. Now need to read the netlist, find all subcircuits,
1238 # and check those dates, too.
1239 layoutdir = os.path.split(layoutpath)[0]
1240 subrex = re.compile('^[^\*]*[ \t]*.subckt[ \t]+([^ \t]+).*$', re.IGNORECASE)
1241 with open(spipath, 'r') as ifile:
1242 duttext = ifile.read()
1243 dutlines = duttext.replace('\n+', ' ').splitlines()
1244 for line in dutlines:
1245 lmatch = subrex.match(line)
1246 if lmatch:
1247 subname = lmatch.group(1)
1248 sublayout = layoutdir + '/' + subname + '.mag'
1249 # subcircuits that cannot be found in the current directory are
1250 # assumed to be library components and therefore never out-of-date.
1251 if os.path.exists(sublayout):
1252 sub_statbuf = os.stat(sublayout)
1253 if spi_statbuf.st_mtime < lay_statbuf.st_mtime:
1254 # netlist exists but is out-of-date
1255 need_capture = True
1256 break
1257 return need_capture
1258
1259def check_schematic_out_of_date(spipath, schempath):
1260 # Check if a netlist (spipath) is out-of-date relative to the schematics
1261 # (schempath). Need to read the netlist and check all of the subcells.
1262 need_capture = False
1263 if not os.path.isfile(spipath):
1264 print('Schematic-captured netlist does not exist. Need to regenerate.')
1265 need_capture = True
1266 elif not os.path.isfile(schempath):
1267 need_capture = True
1268 else:
1269 spi_statbuf = os.stat(spipath)
1270 sch_statbuf = os.stat(schempath)
1271 print('DIAGNOSTIC: Comparing ' + spipath + ' to ' + schempath)
1272 if spi_statbuf.st_mtime < sch_statbuf.st_mtime:
1273 # netlist exists but is out-of-date
1274 print('Netlist is older than top-level schematic')
1275 need_capture = True
1276 else:
1277 print('Netlist is newer than top-level schematic, but must check subcircuits')
1278 # only found that the top-level-schematic is older than the
1279 # netlist. Now need to read the netlist, find all subcircuits,
1280 # and check those dates, too.
1281 schemdir = os.path.split(schempath)[0]
1282 subrex = re.compile('^[^\*]*[ \t]*.subckt[ \t]+([^ \t]+).*$', re.IGNORECASE)
1283 with open(spipath, 'r') as ifile:
1284 duttext = ifile.read()
1285
1286 dutlines = duttext.replace('\n+', ' ').splitlines()
1287 for line in dutlines:
1288 lmatch = subrex.match(line)
1289 if lmatch:
1290 subname = lmatch.group(1)
1291 # NOTE: Electric uses library:cell internally to track libraries,
1292 # and maps the ":" to "__" in the netlist. Not entirely certain that
1293 # the double-underscore uniquely identifies the library:cell. . .
1294 librex = re.compile('(.*)__(.*)', re.IGNORECASE)
1295 lmatch = librex.match(subname)
1296 if lmatch:
1297 elecpath = os.path.split(os.path.split(schempath)[0])[0]
1298 libname = lmatch.group(1)
1299 subschem = elecpath + '/' + libname + '.delib/' + lmatch.group(2) + '.sch'
1300 else:
1301 libname = {}
1302 subschem = schemdir + '/' + subname + '.sch'
1303 # subcircuits that cannot be found in the current directory are
1304 # assumed to be library components or read-only IP components and
1305 # therefore never out-of-date.
1306 if os.path.exists(subschem):
1307 sub_statbuf = os.stat(subschem)
1308 if spi_statbuf.st_mtime < sch_statbuf.st_mtime:
1309 # netlist exists but is out-of-date
1310 print('Netlist is older than subcircuit schematic ' + subname)
1311 need_capture = True
1312 break
1313 # mapping of characters to what's allowed in SPICE makes finding
1314 # the associated schematic file a bit difficult. Requires wild-card
1315 # searching.
1316 elif libname:
1317 restr = lmatch.group(2) + '.sch'
1318 restr = restr.replace('.', '\.')
1319 restr = restr.replace('_', '.')
1320 schrex = re.compile(restr, re.IGNORECASE)
1321 try:
1322 liblist = os.listdir(elecpath + '/' + libname + '.delib')
1323 except FileNotFoundError:
1324 # Potentially could look through the paths in LIBDIR. . .
1325 pass
1326 else:
1327 for file in liblist:
1328 lmatch = schrex.match(file)
1329 if lmatch:
1330 subschem = elecpath + '/' + libname + '.delib/' + file
1331 sub_statbuf = os.stat(subschem)
1332 if spi_statbuf.st_mtime < sch_statbuf.st_mtime:
1333 # netlist exists but is out-of-date
1334 need_capture = True
1335 print('Netlist is older than subcircuit schematic ' + file)
1336 print('In library ' + libname)
1337 break
1338 return need_capture
1339
1340def printwarn(output):
1341 # Check output for warning or error
1342 if not output:
1343 return 0
1344
1345 warnrex = re.compile('.*warning', re.IGNORECASE)
1346 errrex = re.compile('.*error', re.IGNORECASE)
1347
1348 errors = 0
1349 outlines = output.splitlines()
1350 for line in outlines:
1351 try:
1352 wmatch = warnrex.match(line)
1353 except TypeError:
1354 line = line.decode('utf-8')
1355 wmatch = warnrex.match(line)
1356 ematch = errrex.match(line)
1357 if ematch:
1358 errors += 1
1359 if ematch or wmatch:
1360 print(line)
1361 return errors
1362
1363def layout_netlist_includes(pexnetlist, dspath):
1364 # Magic does not generate netlist output for LEF-like views unless
1365 # the option "blackbox on" is passed to ext2spice, in which case it
1366 # generates stub entries. When generating a PEX view for simulation,
1367 # these entries need to be generated then replaced with the correct
1368 # include statement to the ip/ directory.
1369
1370 comtrex = re.compile(r'^\*') # SPICE comment
1371 subcrex = re.compile(r'^[ \t]*x([^ \t]+)[ \t]+(.*)$', re.IGNORECASE) # SPICE subcircuit line
1372 subrex = re.compile(r'^[ \t]*.subckt[ \t]+([^ \t]+)[ \t]*([^ \t]+.*)', re.IGNORECASE)
1373 endsrex = re.compile(r'^[ \t]*\.ends[ \t]*', re.IGNORECASE)
1374
1375 # Also convert commas from [X,Y] arrays to vertical bars as something
1376 # that can be converted back as necessary. ngspice treats commas as
1377 # special characters for some reason. ngspice also does not correctly
1378 # handle slash characters in node names (okay as part of the netlist but
1379 # fails if used in, say, ".nodeset"). Should be okay to replace all '/'
1380 # because the layout extracted netlist won't have .include or other
1381 # entries with filename paths.
1382
1383 # Find project tech path
1384 if os.path.exists(dspath + '/.ef-config/techdir'):
1385 techdir = os.path.realpath(dspath + '/.ef-config/techdir')
1386 maglefdir = techdir + '/libs.ref/maglef'
1387 else:
1388 print('Warning: Project ' + dspath + ' does not define a target process!')
1389 techdir = None
1390 maglefdir = None
1391
1392 with open(pexnetlist, 'r') as ifile:
1393 spitext = ifile.read()
1394
1395 # Find project info file (introduced with FFS, 2/2019. Does not exist in earlier
1396 # projects)
1397
1398 depends = {}
1399 ipname = ''
1400 if os.path.exists(dspath + '/.ef-config/info'):
1401 with open(dspath + '/.ef-config/info', 'r') as ifile:
1402 infolines = ifile.read().splitlines
1403 deprec = False
1404 for line in infolines:
1405 if 'dependencies:' in line:
1406 deprec = True
1407 if deprec:
1408 if 'version' in line:
1409 version = line.split()[1].strip("'")
1410 if ipname != '':
1411 depends[ipname] = version
1412 ipname = ''
1413 else:
1414 print('Error: Badly formed info file in .ef-config', file=sys.stderr)
1415 else:
1416 ipname = line.strip(':')
1417
1418 spilines = spitext.replace('\n+', ' ').replace(',', '|').replace('/','|').splitlines()
1419
1420 newspilines = []
1421 extended_names = []
1422 pinsorts = {}
1423 inbox = False
1424 for line in spilines:
1425 cmatch = comtrex.match(line)
1426 smatch = subrex.match(line)
1427 xmatch = subcrex.match(line)
1428 if 'Black-box' in line:
1429 inbox = True
1430 elif not inbox:
1431 if xmatch:
1432 # Pull subcircuit name from an 'X' component and see if it matches any of the
1433 # names that were rewritten in Electric <library>__<cell> style. If so, replace
1434 # the subcircuit name with the modified name while preserving the rest of the
1435 # component line.
1436 rest = xmatch.group(2).split()
1437 r1 = list(i for i in rest if '=' not in i)
1438 r2 = list(i for i in rest if '=' in i)
1439 subname = r1[-1]
1440 r1 = r1[0:-1]
1441
1442 # Re-sort the pins if needed
1443 if subname in pinsorts:
1444 r1 = [r1[i] for i in pinsorts[subname]]
1445
1446 if subname.upper() in extended_names:
1447 newsubname = subname + '__' + subname
1448 newspilines.append('X' + xmatch.group(1) + ' ' + ' '.join(r1) + ' ' + newsubname + ' ' + ' '.join(r2))
1449 else:
1450 newspilines.append(line)
1451 else:
1452 newspilines.append(line)
1453 elif cmatch:
1454 newspilines.append(line)
1455 elif smatch:
1456 subname = smatch.group(1)
1457 pinlist = smatch.group(2).split()
1458 print("Parsing black-box subcircuit " + subname)
1459 ippath = '~/design/ip/' + subname
1460 ipfullpath = os.path.expanduser(ippath)
1461 if os.path.exists(ipfullpath):
1462 # Version control: Use the versions specified in the .ef-config/info
1463 # version list. If it does not exist (legacy behavior), then use the
1464 # method outlined below (finds highest version number available).
1465 if subname in depends:
1466 useversion = str(depends[subname])
1467 else:
1468 versions = os.listdir(ipfullpath)
1469 vf = list(float(i) for i in versions)
1470 vi = vf.index(max(vf))
1471 useversion = versions[vi]
1472
1473 versionpath = ipfullpath + '/' + useversion
1474
1475 # First to do: Check for /spi-stub entry (which is readable), and
1476 # check if pin order is correct. Flag a warning if it is not, and
1477 # save the pin order in a record so that all X records can be pin
1478 # sorted correctly.
1479
1480 if os.path.exists(versionpath + '/spi-stub'):
1481 stubpath = versionpath + '/spi-stub/' + subname + '/' + subname + '__' + subname + '.spi'
1482 # More spice file reading! This should be quick, as these files have
1483 # only a single empty subcircuit in them.
1484 found = False
1485 with open(stubpath, 'r') as sfile:
1486 stubtext = sfile.read()
1487 stublines = stubtext.replace('\n+', ' ').replace(',', '|').splitlines()
1488 for line in stublines:
1489 smatch = subrex.match(line)
1490 if smatch:
1491 found = True
1492 stubname = smatch.group(1)
1493 stublist = smatch.group(2).split()
1494 if stubname != subname + '__' + subname:
1495 print('Error: Looking for subcircuit ' + subname + '__' + subname + ' in file ' + stubpath + ' but found subcircuit ' + stubname + ' instead!')
1496 print("This simulation probably isn't going to go well.")
1497 else:
1498 needsort = False
1499 for stubpin, subpin in zip(stublist, pinlist):
1500 if stubpin.upper() != subpin.upper():
1501 print('Warning: pin mismatch between layout and schematic stub header on subcircuit ' + subname)
1502 print('Will sort layout netlist to match.')
1503 print('Correct pin order is: ' + smatch.group(2))
1504 needsort = True
1505 break
1506 if needsort:
1507 pinorder = [i[0] for i in sorted(enumerate(pinlist), key = lambda x:stublist.index(x[1]))]
1508 pinsorts[subname] = pinorder
1509 break
1510 if not found:
1511 print('Error: Cannot find subcircuit in IP spi-stub entry.')
1512 else:
1513 print('Warning: IP has no spi-stub entry, cannot verify pin order.')
1514
1515 if os.path.exists(versionpath + '/spi-rcx'):
1516 # This path is restricted and can only be seen by ngspice, which is privileged
1517 # to read it. So we can only assume that it matches the spi-stub entry.
1518 # NOTE (10/16/2018): Use unexpanded tilde expression in file.
1519 # rcxpath = versionpath + '/spi-rcx/' + subname + '/' + subname + '__' + subname + '.spi'
1520 rcxpath = ippath + '/' + useversion + '/spi-rcx/' + subname + '/' + subname + '__' + subname + '.spi'
1521 newspilines.append('* Black-box entry replaced by path to RCX netlist')
1522 newspilines.append('.include ' + rcxpath)
1523 extended_names.append(subname.upper())
1524 elif os.path.exists(ipfullpath + '/' + useversion + '/spi'):
1525 # In a pinch, if there is no spi-rcx, try plain spi
1526 # NOTE (10/16/2018): Use unexpanded tilde expression in file.
1527 # spipath = versionpath + '/spi/' + subname + '.spi'
1528 spipath = ippath + '/' + useversion + '/spi/' + subname + '.spi'
1529 newspilines.append('* Black-box entry replaced by path to schematic netlist')
1530 newspilines.append('.include ' + spipath)
1531 else:
1532 # Leave as is, and let it force an error
1533 newspilines.append(line)
1534 inbox = False
1535 elif maglefdir:
1536 # Check tech file paths
1537 found = False
1538 maglefsubdirs = os.listdir(maglefdir)
1539 for techsubdir in maglefsubdirs:
1540 if not os.path.isdir(maglefdir + '/' + techsubdir):
1541 continue
1542 # print('Diagnostic: looking in ' + str(maglefdir) + ' ' + str(techsubdir))
1543 maglefcells = os.listdir(maglefdir + '/' + techsubdir)
1544 if subname + '.mag' in maglefcells:
1545 # print("Diagnostic: Parsing black-box subcircuit " + subname)
1546 # print('from tech path ' + maglefdir + '/' + techsubdir)
1547
1548 # Like the IP directory, can't read spi/ so have to assume it's there.
1549 # Problem---there is no consistency across PDKs for the naming of
1550 # files in spi/!
1551
1552 newspilines.append('* Need include to schematic netlist for ' + subname)
1553 # However, the CDL stub file can be used to check pin order
1554 stubpath = techdir + '/libs.ref/cdlStub/' + techsubdir + '/stub.cdl'
1555 if os.path.exists(stubpath):
1556 # More spice file reading! This should be quick, as these files have
1557 # only a empty subcircuits in them.
1558 with open(stubpath, 'r') as sfile:
1559 stubtext = sfile.read()
1560 stublines = stubtext.replace('\n+', ' ').replace(',', '|').splitlines()
1561 for line in spilines:
1562 smatch = subrex.match(line)
1563 if smatch:
1564 stubname = smatch.group(1)
1565 stublist = smatch.group(2).split()
1566 if stubname == subname:
1567 found = True
1568 needsort = False
1569 for stubpin, subpin in zip(stublist, pinlist):
1570 if stubpin.upper() != subpin.upper():
1571 print('Warning: pin mismatch between layout and schematic stub header on subcircuit ' + subname)
1572 print('Will sort layout netlist to match.')
1573 print('Correct pin order is: ' + smatch.group(2))
1574 needsort = True
1575 break
1576 if needsort:
1577 pinorder = [i[0] for i in sorted(enumerate(pinlist), key = lambda x:stublist.index(x[1]))]
1578 pinsorts[subname] = pinorder
1579 if found:
1580 break
1581
1582 else:
1583 print('No file ' + stubpath + ' found.')
1584 print('Failure to find stub netlist for checking pin order. Good luck.')
1585 break
1586
1587 if not found:
1588 print('Error: Subcell ' + subname + ' not found in IP or tech paths.')
1589 print('This netlist is not going to simulate correctly.')
1590 newspilines.append('* Unknown black-box entry ' + subname)
1591 newspilines.append(line)
1592 elif endsrex.match(line):
1593 inbox = False
1594
1595 with open(pexnetlist, 'w') as ofile:
1596 for line in newspilines:
1597 print(line, file=ofile)
1598
1599def regenerate_netlists(localmode, dspath, dsheet):
1600 # When running locally, 'netlist-source' determines whether to use the
1601 # layout extracted netlist or the schematic captured netlist. Also for
1602 # local running only, regenerate the netlist only if it is out of date,
1603 # or if the user has selected forced regeneration in the settings.
1604
1605 dname = dsheet['ip-name']
1606 magpath = dspath + '/mag/'
1607
1608 spipath = dspath + '/spi/' # Schematic netlist for sim
1609 stubpath = dspath + '/spi/stub/' # Schematic netlist for LVS
1610 pexpath = dspath + '/spi/pex/' # Layout netlist for sim
1611 lvspath = dspath + '/spi/lvs/' # Layout netlist for LVS
1612 vlogpath = dspath + '/verilog/' # Verilog netlist for sim and LVS
1613
1614 netlistname = dname + '.spi'
1615 schnetlist = spipath + netlistname
1616 stubnetlist = stubpath + netlistname
1617 pexnetlist = pexpath + netlistname
1618 laynetlist = lvspath + netlistname
1619
1620 layoutpath = magpath + dname + '.mag'
1621 elecpath = dspath + '/elec/' + dname + '.delib'
1622 schempath = elecpath + '/' + dname + '.sch'
1623 verilogpath = vlogpath + dname + '.v'
1624 pathlast = os.path.split(dspath)[1]
1625 verilogaltpath = vlogpath + pathlast + '/' + dname + '.vgl'
1626 need_sch_capture = True
1627 need_stub_capture = True
1628 need_lay_capture = True
1629 need_pex_capture = True
1630 force_regenerate = False
1631
1632 # Check if datasheet has been marked for forced netlist regeneration
1633 if 'regenerate' in dsheet:
1634 if dsheet['regenerate'] == 'force':
1635 force_regenerate = True
1636
1637 # If schempath does not exist, check if the .sch file is in a different
1638 # library.
1639 if not os.path.exists(schempath):
1640 print('No schematic in path ' + schempath)
1641 print('Checking for other library paths.')
1642 for libname in os.listdir(dspath + '/elec/'):
1643 if os.path.splitext(libname)[1] == '.delib':
1644 elecpath = dspath + '/elec/' + libname
1645 if os.path.exists(elecpath):
1646 for schfile in os.listdir(elecpath):
1647 if schfile == dname + '.sch':
1648 schempath = elecpath + '/' + schfile
1649 print('Schematic found in ' + schempath)
1650 break
1651
1652 # Guess the source based on the file or files in the design directory,
1653 # with preference given to layout. This may be overridden in local mode.
1654
1655 if localmode and ('netlist-source' in dsheet) and (not force_regenerate):
1656 print("Checking for out-of-date netlists.\n")
1657 netlist_source = dsheet['netlist-source']
1658 need_sch_capture = check_schematic_out_of_date(schnetlist, schempath)
1659 need_stub_capture = check_schematic_out_of_date(stubnetlist, schempath)
1660 if netlist_source == 'layout':
1661 netlist_path = pexnetlist
1662 need_pex_capture = check_layout_out_of_date(pexnetlist, layoutpath)
1663 need_lay_capture = check_layout_out_of_date(laynetlist, layoutpath)
1664 else:
1665 netlist_path = schnetlist
1666 need_lay_capture = False
1667 need_pex_capture = False
1668 else:
1669 if not localmode:
1670 print("Remote use, ", end='');
1671 print("forcing regeneration of all netlists.\n")
1672 if 'netlist-source' in dsheet:
1673 netlist_source = dsheet['netlist-source']
1674 if netlist_source == 'layout':
1675 netlist_path = pexnetlist
1676 else:
1677 netlist_path = schnetlist
1678 need_lay_capture = False
1679 need_pex_capture = False
1680 else:
1681 if os.path.exists(layoutpath):
1682 netlist_path = pexnetlist
1683 dsheet['netlist-source'] = 'layout'
1684 elif os.path.exists(schempath):
1685 netlist_path = schnetlist
1686 dsheet['netlist-source'] = 'schematic'
1687 need_lay_capture = False
1688 need_pex_capture = False
1689 elif os.path.exists(verilogpath):
1690 netlist_path = verilogpath
1691 dsheet['netlist-source'] = 'verilog'
1692 need_lay_capture = False
1693 need_pex_capture = False
1694 need_sch_capture = False
1695 need_stub_capture = False
1696 elif os.path.exists(verilogaltpath):
1697 netlist_path = verilogaltpath
1698 dsheet['netlist-source'] = 'verilog'
1699 need_lay_capture = False
1700 need_pex_capture = False
1701 need_sch_capture = False
1702 need_stub_capture = False
1703
1704 if need_lay_capture or need_pex_capture:
1705 # Layout LVS netlist needs regenerating. Check for magic layout.
1706 if not os.path.isfile(layoutpath):
1707 print('Error: No netlist or layout for project ' + dname + '.')
1708 print('(layout master file ' + layoutpath + ' not found.)\n')
1709 return False
1710
1711 # Check for spi/lvs/ directory
1712 if not os.path.exists(lvspath):
1713 os.makedirs(lvspath)
1714
1715 # Check for spi/pex/ directory
1716 if not os.path.exists(pexpath):
1717 os.makedirs(pexpath)
1718
1719 print("Extracting LVS netlist from layout. . .")
1720 mproc = subprocess.Popen(['/ef/apps/bin/magic', '-dnull', '-noconsole',
1721 layoutpath], stdin = subprocess.PIPE, stdout=subprocess.PIPE,
1722 stderr=subprocess.STDOUT, cwd = dspath + '/mag',
1723 universal_newlines = True)
1724 mproc.stdin.write("select top cell\n")
1725 mproc.stdin.write("expand true\n")
1726 mproc.stdin.write("extract all\n")
1727 mproc.stdin.write("ext2spice hierarchy on\n")
1728 mproc.stdin.write("ext2spice format ngspice\n")
1729 mproc.stdin.write("ext2spice scale off\n")
1730 mproc.stdin.write("ext2spice renumber off\n")
1731 mproc.stdin.write("ext2spice subcircuit on\n")
1732 mproc.stdin.write("ext2spice global off\n")
1733 # Don't want black box entries, but create them so that we know which
1734 # subcircuits are in the ip path, then replace them.
1735 mproc.stdin.write("ext2spice blackbox on\n")
1736 if need_lay_capture:
1737 mproc.stdin.write("ext2spice cthresh infinite\n")
1738 mproc.stdin.write("ext2spice rthresh infinite\n")
1739 mproc.stdin.write("ext2spice -o " + laynetlist + "\n")
1740 if need_pex_capture:
1741 mproc.stdin.write("ext2spice cthresh 0.005\n")
1742 mproc.stdin.write("ext2spice rthresh 1\n")
1743 mproc.stdin.write("ext2spice -o " + pexnetlist + "\n")
1744 mproc.stdin.write("quit -noprompt\n")
1745 magout = mproc.communicate()[0]
1746 printwarn(magout)
1747 if mproc.returncode != 0:
1748 print('Magic process returned error code ' + str(mproc.returncode) + '\n')
1749
1750 if need_lay_capture and not os.path.isfile(laynetlist):
1751 print('Error: No LVS netlist extracted from magic.')
1752 if need_pex_capture and not os.path.isfile(pexnetlist):
1753 print('Error: No parasitic extracted netlist extracted from magic.')
1754
1755 if (mproc.returncode != 0) or (need_lay_capture and not os.path.isfile(laynetlist)) or (need_pex_capture and not os.path.isfile(pexnetlist)):
1756 return False
1757
1758 if need_pex_capture and os.path.isfile(pexnetlist):
1759 print('Generating include statements for read-only IP blocks in layout, if needed')
1760 layout_netlist_includes(pexnetlist, dspath)
1761
1762 if need_sch_capture or need_stub_capture:
1763 # Netlist needs regenerating. Check for electric schematic
1764 if not os.path.isfile(schempath):
1765 if os.path.isfile(verilogpath):
1766 print('No schematic for project.')
1767 print('Using verilog netlist ' + verilogpath + ' for simulation and LVS.')
1768 return verilogpath
1769 elif os.path.isfile(verilogaltpath):
1770 print('No schematic for project.')
1771 print('Using verilog netlist ' + verilogaltpath + ' for simulation and LVS.')
1772 return verilogaltpath
1773 else:
1774 print('Error: No netlist or schematic for project ' + dname + '.')
1775 print('(schematic master file ' + schempath + ' not found.)\n')
1776 print('Error: No verilog netlist ' + verilogpath + ' or ' + verilogaltpath + ', either.')
1777 return False
1778
1779 # Check if there is a .java directory, if not (e.g., for remote CACE),
1780 # then copy it from the defaults.
1781 if not os.path.exists(dspath + '/elec/.java'):
1782 shutil.copytree('/ef/efabless/deskel/dotjava', dspath + '/elec/.java',
1783 symlinks = True)
1784
1785 # Fix the LIBDIRS file if needed
1786 if not os.path.isfile(dspath + '/elec/LIBDIRS'):
1787 fix_libdirs(dspath, create = True)
1788 elif need_sch_capture or need_stub_capture:
1789 fix_libdirs(dspath)
1790
1791 if need_sch_capture:
1792 print("Generating simulation netlist from schematic. . .")
1793 # Generate the netlist
1794 print('Calling /ef/efabless/bin/elec2spi -o ')
1795 libpath = os.path.split(schempath)[0]
1796 libname = os.path.split(libpath)[1]
1797 print(schnetlist + ' -TS -NTI ' + libname + ' ' + dname + '.sch\n')
1798
1799 # elec2spi requires that the /spi/ and /spi/stub directory exists
1800 if not os.path.exists(spipath):
1801 os.makedirs(spipath)
1802
1803 eproc = subprocess.Popen(['/ef/efabless/bin/elec2spi',
1804 '-o', schnetlist, '-TS', '-NTI', libname, dname + '.sch'],
1805 stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
1806 cwd = dspath + '/elec')
1807
1808 elecout = eproc.communicate()[0]
1809 if eproc.returncode != 0:
1810 for line in elecout.splitlines():
1811 print(line.decode('utf-8'))
1812
1813 print('Electric process returned error code ' + str(eproc.returncode) + '\n')
1814 else:
1815 printwarn(elecout)
1816
1817 if not os.path.isfile(schnetlist):
1818 print('Error: No netlist found for the circuit!\n')
1819 print('(schematic netlist for simulation ' + schnetlist + ' not found.)\n')
1820
1821 if need_stub_capture:
1822 print("Generating LVS netlist from schematic. . .")
1823 # Generate the netlist
1824 print('Calling /ef/efabless/bin/elec2spi -o ')
1825 libpath = os.path.split(schempath)[0]
1826 libname = os.path.split(libpath)[1]
1827 print(stubnetlist + ' -LP -TS -NTI ' + libname + ' ' + dname + '.sch\n')
1828
1829 # elec2spi requires that the /spi/stub directory exists
1830 if not os.path.exists(stubpath):
1831 os.makedirs(stubpath)
1832
1833 eproc = subprocess.Popen(['/ef/efabless/bin/elec2spi',
1834 '-o', stubnetlist, '-LP', '-TS', '-NTI', libname, dname + '.sch'],
1835 stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
1836 cwd = dspath + '/elec')
1837
1838 elecout = eproc.communicate()[0]
1839 if eproc.returncode != 0:
1840 for line in elecout.splitlines():
1841 print(line.decode('utf-8'))
1842
1843 print('Electric process returned error code ' + str(eproc.returncode) + '\n')
1844 else:
1845 printwarn(elecout)
1846
1847 if not os.path.isfile(stubnetlist):
1848 print('Error: No netlist found for the circuit!\n')
1849 print('(schematic netlist for LVS ' + stubnetlist + ' not found.)\n')
1850
1851 if need_sch_capture or need_stub_capture:
1852 if (not os.path.isfile(schnetlist)) or (not os.path.isfile(stubnetlist)):
1853 return False
1854
1855 return netlist_path
1856
1857def cleanup_exit(signum, frame):
1858 global launchproc
1859 print("CACE gensim: Received termination signal.")
1860 if launchproc:
1861 print("CACE gensim: Stopping simulations now.")
1862 launchproc.terminate()
1863 else:
1864 sys.exit(1)
1865
1866# Main entry point. Read arguments, print usage or load the json file
1867# and call generate_simfiles.
1868
1869if __name__ == '__main__':
1870 faulthandler.register(signal.SIGUSR2)
1871 signal.signal(signal.SIGINT, cleanup_exit)
1872 signal.signal(signal.SIGTERM, cleanup_exit)
1873
1874 # Divide up command line into options and arguments
1875 options = []
1876 arguments = []
1877 localmode = False
1878 for item in sys.argv[1:]:
1879 if item.find('-', 0) == 0:
1880 options.append(item)
1881 else:
1882 arguments.append(item)
1883
1884 # Read the JSON file
1885 root_path = []
1886 if len(arguments) > 0:
1887 root_path = str(sys.argv[1])
1888 arguments = arguments[1:]
1889 elif len(options) == 0:
1890 # Print usage information when arguments don't match
1891 print('Usage:\n')
1892 print(' ' + str(sys.argv[0]) + ' [root_path] [options ...]')
1893 print('Where [options ...] are one or more of the following:')
1894 print(' -simdir <path>')
1895 print(' is the location where simulation files and data should be placed.')
1896 print(' -datasheetdir <path>')
1897 print(' is the location of the JSON file describing the characterization.')
1898 print(' -testbenchdir <path>')
1899 print(' is the location of the netlists for the characterization methods.')
1900 print(' -netlist <path>')
1901 print(' is the location of the netlist for the device-under-test.')
1902 print(' -layoutdir <path>')
1903 print(' is the location of the layout netlist for the device-under-test.')
1904 print(' -datasheet <name>')
1905 print(' is the name of the datasheet JSON file.')
1906 print(' -method <name>, ...')
1907 print(' is a list of one or more names of methods to simulated. If omitted,')
1908 print(' all methods are run for a complete characterization.')
1909 print(' -local')
1910 print(' indicates that cace_gensim is being run locally, not on the CACE')
1911 print(' server, simulation conditions should be output along with results;')
1912 print(' "local" mode implies that results are not posted to the marketplace')
1913 print(' after simulation, and result files are kept.')
1914 print(' -keep')
1915 print(' test mode: keep all files after simulation')
1916 print(' -plot')
1917 print(' test mode: generate plot (.png) files locally')
1918 print(' -nopost')
1919 print(' test mode: do not post results to the marketplace')
1920 print(' -nosim')
1921 print(' test mode: set up all files for simulation but do not simulate')
1922 sys.exit(0)
1923
1924 simulation_path = []
1925 datasheet_path = []
1926 testbench_path = []
1927 design_path = []
1928 layout_path = []
1929 datasheet_name = []
1930 methods = []
1931 for option in options[:]:
1932 result = option.split('=')
1933 if result[0] == '-simdir':
1934 simulation_path = result[1]
1935 options.remove(option)
1936 elif result[0] == '-datasheetdir':
1937 datasheet_path = result[1]
1938 options.remove(option)
1939 elif result[0] == '-testbenchdir':
1940 testbench_path = result[1]
1941 options.remove(option)
1942 elif result[0] == '-designdir':
1943 design_path = result[1]
1944 options.remove(option)
1945 elif result[0] == '-layoutdir':
1946 layout_path = result[1]
1947 options.remove(option)
1948 elif result[0] == '-datasheet':
1949 datasheet_name = result[1]
1950 options.remove(option)
1951 elif result[0] == '-method':
1952 methods.append(result[1])
1953 options.remove(option)
1954 elif result[0] == '-bypass':
1955 bypassmode = True
1956 options.remove(option)
1957 elif result[0] == '-local':
1958 localmode = True
1959
1960 # To be valid, must either have a root path or all other options must have been
1961 # specified with full paths.
1962 if not root_path:
1963 err_result = 1
1964 if not simulation_path:
1965 print('Error: If root_path is not provided, -simdir is required.')
1966 elif simulation_path[0] != '/':
1967 print('Error: If root_path not provided, -simdir must be a full path.')
1968 if not testbench_path:
1969 print('Error: If root_path is not provided, -testbenchdir is required.')
1970 elif testbench_path[0] != '/':
1971 print('Error: If root_path not provided, -testbenchdir must be a full path.')
1972 if not design_path:
1973 print('Error: If root_path is not provided, -designdir is required.')
1974 elif design_path[0] != '/':
1975 print('Error: If root_path not provided, -designdir must be a full path.')
1976 if not layout_path:
1977 print('Error: If root_path is not provided, -layoutdir is required.')
1978 elif layout_path[0] != '/':
1979 print('Error: If root_path not provided, -layoutdir must be a full path.')
1980 if not datasheet_path:
1981 print('Error: If root_path is not provided, -datasheetdir is required.')
1982 elif datasheet_path[0] != '/':
1983 print('Error: If root_path not provided, -datasheetdir must be a full path.')
1984 else:
1985 err_result = 0
1986
1987 if err_result:
1988 sys.exit(1)
1989
1990 # Apply defaults where not provided as command-line options
1991 else:
1992 if not datasheet_path:
1993 datasheet_path = root_path
1994 elif not os.path.isabs(datasheet_path):
1995 datasheet_path = root_path + '/' + datasheet_path
1996 if not datasheet_name:
1997 datasheet_name = 'datasheet.json'
1998 inputfile = datasheet_path + '/' + datasheet_name
1999 # 2nd guess: 'project.json'
2000 if not os.path.isfile(inputfile):
2001 datasheet_name = 'project.json'
2002 inputfile = datasheet_path + '/' + datasheet_name
2003 # 3rd guess (legacy behavior): project directory name + '.json'
2004 if not os.path.isfile(inputfile):
2005 datasheet_name = os.path.split(datasheet_path)[1] + '.json'
2006 inputfile = datasheet_path + '/' + datasheet_name
2007 if not os.path.isfile(inputfile):
2008 # Return to original datasheet name; error will be generated.
2009 datasheet_name = 'datasheet.json'
2010 elif localmode and root_path:
2011 # Use normal path to local simulation workspace
2012 simulation_path = root_path + '/ngspice/char'
2013
2014 # Check that datasheet path exists and that the datasheet is there
2015 if not os.path.isdir(datasheet_path):
2016 print('Error: Path to datasheet ' + datasheet_path + ' does not exist.')
2017 sys.exit(1)
2018 if len(os.path.splitext(datasheet_name)) != 2:
2019 datasheet_name += '.json'
2020 inputfile = datasheet_path + '/' + datasheet_name
2021 if not os.path.isfile(inputfile):
2022 print('Error: No datasheet file ' + inputfile )
2023 sys.exit(1)
2024
2025 with open(inputfile) as ifile:
2026 datatop = json.load(ifile)
2027
2028 # Pick up testbench and design paths from options now, since some of them
2029 # depend on the request-hash value in the JSON file.
2030
2031 if not simulation_path:
2032 if 'request-hash' in datatop:
2033 hashname = datatop['request-hash']
2034 simulation_path = root_path + '/' + hashname
2035 elif os.path.isdir(root_path + '/ngspice/char'):
2036 simulation_path = root_path + '/ngspice/char'
2037 else:
2038 simulation_path = root_path
2039 elif not os.path.isabs(simulation_path):
2040 simulation_path = root_path + '/' + simulation_path
2041 if not testbench_path:
2042 testbench_path = root_path + '/testbench'
2043 elif not os.path.isabs(testbench_path):
2044 testbench_path = root_path + '/' + testbench_path
2045 if not design_path:
2046 design_path = root_path + '/spi'
2047 elif not os.path.isabs(design_path):
2048 design_path = root_path + '/' + design_path
2049 if not layout_path:
2050 layout_path = root_path + '/mag'
2051 elif not os.path.isabs(layout_path):
2052 layout_path = root_path + '/' + layout_path
2053
2054 # Project name should be 'ip-name' in datatop['data-sheet']
2055 try:
2056 dsheet = datatop['data-sheet']
2057 except KeyError:
2058 print('Error: File ' + inputfile + ' is not a datasheet.\n')
2059 sys.exit(1)
2060 try:
2061 name = dsheet['ip-name']
2062 except KeyError:
2063 print('Error: File ' + inputfile + ' is missing ip-name.\n')
2064 sys.exit(1)
2065
2066 if not os.path.isdir(testbench_path):
2067 print('Warning: Path ' + testbench_path + ' does not exist. ' +
2068 'Testbench files are not available.\n')
2069
2070 if not os.path.isdir(design_path):
2071 print('Warning: Path ' + design_path + ' does not exist. ' +
2072 'Netlist files may not be available.\n')
2073
2074 # Simulation path is where the output is dumped. If it doesn't
2075 # exist, then create it.
2076 if not os.path.isdir(simulation_path):
2077 print('Creating simulation path ' + simulation_path)
2078 os.makedirs(simulation_path)
2079
2080 if not os.path.isdir(layout_path):
2081 print('Creating layout path ' + layout_path)
2082 os.makedirs(layout_path)
2083
2084 if not os.path.exists(layout_path + '/.magicrc'):
2085 # Make sure correct .magicrc file exists
2086 configdir = os.path.split(layout_path)[0]
2087 rcpath = configdir + '/.ef-config/techdir/libs.tech/magic/current'
2088 pdkname = os.path.split(os.path.realpath(configdir + '/.ef-config/techdir'))[1]
2089 rcfile = rcpath + '/' + pdkname + '.magicrc'
2090 if os.path.isdir(rcpath):
2091 if os.path.exists(rcfile):
2092 shutil.copy(rcfile, layout_path + '/.magicrc')
2093
2094 # Find the electrical parameter list. If it exists, then the
2095 # template has been loaded. If not, find the template name,
2096 # then load it from known templates. Templates may be local to
2097 # the simulation files. Priority is (1) templates known to CACE
2098 # (for challenges; cannot be overridden by a user; (2) templates
2099 # local to the simulation (user-generated)
2100
2101 if not 'electrical-params' in dsheet and not 'physical-params' in dsheet:
2102 print('Error: Circuit JSON file does not have a valid characterization template!\n')
2103 sys.exit(1)
2104
2105 fullnetlistpath = regenerate_netlists(localmode, root_path, dsheet)
2106 if not fullnetlistpath:
2107 sys.exit(1)
2108
2109 netlistpath, netlistname = os.path.split(fullnetlistpath)
2110
2111 # If there is a 'hints.json' file in the root path, read it and apply to the
2112 # electrical parameters. The file contains exactly one hint record per
2113 # electrical parameter, although the hint record may be empty.
2114 if os.path.exists(root_path + '/hints.json'):
2115 with open(root_path + '/hints.json') as hfile:
2116 hintlist = json.load(hfile)
2117 i = 0
2118 for eparam in dsheet['electrical-params']:
2119 if not 'hints' in eparam:
2120 if hintlist[i]:
2121 eparam['hints'] = hintlist[i]
2122 i += 1
2123
2124 # Construct fileinfo dictionary
2125 fileinfo = {}
2126 fileinfo['project-name'] = name
2127 fileinfo['design-netlist-name'] = netlistname
2128 fileinfo['design-netlist-path'] = netlistpath
2129 fileinfo['testbench-netlist-path'] = testbench_path
2130 fileinfo['simulation-path'] = simulation_path
2131 fileinfo['root-path'] = root_path
2132
2133 # Generate the simulation files
2134 prescore = generate_simfiles(datatop, fileinfo, arguments, methods, localmode)
2135 if prescore == 'fail':
2136 # In case of failure
2137 options.append('-score=fail')
2138
2139 # Remove option keys
2140 if 'keep' in datatop:
2141 options.append('-keep')
2142 datatop.pop('keep')
2143 if 'plot' in datatop:
2144 options.append('-plot')
2145 datatop.pop('plot')
2146 if 'nopost' in datatop:
2147 options.append('-nopost')
2148 datatop.pop('nopost')
2149 if 'nosim' in datatop:
2150 options.append('-nosim')
2151 datatop.pop('nosim')
2152
2153 # Reconstruct the -simdir option for cace_launch
2154 options.append('-simdir=' + simulation_path)
2155
2156 # Reconstruct the -layoutdir option for cace_launch
2157 options.append('-layoutdir=' + layout_path)
2158
2159 # Reconstruct the -netlistdir option for cace_launch
2160 options.append('-netlistdir=' + design_path)
2161
2162 # Reconstruct the -rootdir option for cace_launch
2163 if root_path:
2164 options.append('-rootdir=' + root_path)
2165
2166 # Dump the modified JSON file
2167 basename = os.path.basename(inputfile)
2168 outputfile = simulation_path + '/' + basename
2169 with open(outputfile, 'w') as ofile:
2170 json.dump(datatop, ofile, indent = 4)
2171
2172 # Launch simulator as a subprocess and wait for it to finish
2173 # Waiting is important, as otherwise child processes get detached and it
2174 # becomes very difficult to find them if the simulation needs to be stopped.
2175 launchname = apps_path + '/' + 'cace_launch.py'
2176
2177 # Diagnostic
2178 print("Running: " + launchname + ' ' + outputfile)
2179 for a in arguments:
2180 print(a)
2181 for o in options:
2182 print(o)
2183
2184 with subprocess.Popen([launchname, outputfile, *arguments, *options],
2185 stdout=subprocess.PIPE, bufsize = 1,
2186 universal_newlines=True) as launchproc:
2187 for line in launchproc.stdout:
2188 print(line, end='')
2189 sys.stdout.flush()
2190
2191 launchproc.stdout.close()
2192 return_code = launchproc.wait()
2193 if return_code != 0:
2194 raise subprocess.CalledProcessError(return_code, launchname)
2195
2196 sys.exit(0)