emayecs | 5656b2b | 2021-08-04 12:44:13 -0400 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
emayecs | 5966a53 | 2021-07-29 10:07:02 -0400 | [diff] [blame] | 2 | """ |
| 3 | cace_gensim.py |
| 4 | This is the main part of the automatic characterization engine. It takes |
| 5 | a JSON simulation template file as input and parses it for information on |
| 6 | how to construct files for the characterization simulations. Output is |
| 7 | a number of simulation files (for now, at least, in ng-spice format). |
| 8 | |
| 9 | Usage: |
| 10 | |
| 11 | cace_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 | |
| 17 | options: |
| 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 | |
| 51 | Quick local run---Use: |
| 52 | |
| 53 | cace_gensim.py <root_dir> -local -method=<method_name> |
| 54 | |
| 55 | e.g., |
| 56 | |
| 57 | cace_gensim.py ~/design/XBG_1V23LC_V01 -local -method=DCVOLTAGE_VBG.1 |
| 58 | """ |
| 59 | |
| 60 | import os |
| 61 | import sys |
| 62 | import json |
| 63 | import re |
| 64 | import time |
| 65 | import shutil |
| 66 | import signal |
| 67 | import datetime |
| 68 | import subprocess |
| 69 | import faulthandler |
| 70 | from functools import reduce |
| 71 | from spiceunits import spice_unit_convert |
| 72 | from fix_libdirs import fix_libdirs |
| 73 | |
emayecs | b2487ae | 2021-08-05 10:30:13 -0400 | [diff] [blame] | 74 | import config |
emayecs | 5966a53 | 2021-07-29 10:07:02 -0400 | [diff] [blame] | 75 | |
emayecs | b2487ae | 2021-08-05 10:30:13 -0400 | [diff] [blame] | 76 | # Values obtained from config: |
emayecs | 5966a53 | 2021-07-29 10:07:02 -0400 | [diff] [blame] | 77 | # |
emayecs | b2487ae | 2021-08-05 10:30:13 -0400 | [diff] [blame] | 78 | apps_path = config.apps_path |
emayecs | 5966a53 | 2021-07-29 10:07:02 -0400 | [diff] [blame] | 79 | launchproc = [] |
| 80 | |
| 81 | def 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 | |
| 170 | conditiontypes = { |
| 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 | |
| 188 | def 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 | |
| 199 | def 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 | |
| 212 | def bindigits(n, bits): |
| 213 | s = bin(n & int("1" * bits, 2))[2:] |
| 214 | return ("{0:0>%s}" % (bits)).format(s) |
| 215 | |
| 216 | def 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 | |
| 222 | def 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 | |
| 238 | def 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 | |
| 257 | def 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 | |
| 308 | def 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 | |
| 370 | def 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 | |
| 381 | def 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 | |
| 407 | def 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 | |
| 541 | def 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 | |
| 893 | def 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 | |
| 1221 | def 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 | |
| 1259 | def 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 | |
| 1340 | def 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 | |
| 1363 | def 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 | |
| 1599 | def 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 | |
| 1857 | def 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 | |
| 1869 | if __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) |