blob: 8ac5c4eac1a26b57e8df8aede6bd98c4202c61d6 [file] [log] [blame]
#!/usr/bin/env python3
"""
cace_gensim.py
This is the main part of the automatic characterization engine. It takes
a JSON simulation template file as input and parses it for information on
how to construct files for the characterization simulations. Output is
a number of simulation files (for now, at least, in ng-spice format).
Usage:
cace_gensim.py [<root_path>] [<option> ...]
<root_path> is the root of all the other path names, if the other
path names are not full paths. If the other pathnames are all
full paths, then <root_path> may be omitted.
options:
-simdir <path>
is the location where simulation files and data should be placed.
-datasheetdir <path>
is the location of the JSON file describing the characterization
-testbenchdir <path>
is the location of the netlists for the characterization methods
-designdir <path>
is the location of the netlist for the device-under-test
-layoutdir <path>
is the location of the layout netlist for the device-under-test
-datasheet <name>
is the name of the datasheet JSON file
-method <name>, ...
is a list of one or more names of methods to simulate. If omitted,
all methods are run for a complete characterization.
-local
indicates that cace_gensim is being run locally, not on the CACE
server, simulation conditions should be output along with results;
'local' mode implies that results are not posted to the marketplace
after simulation, and result files are kept.
-bypass
acts like remote CACE by running all simulations in one batch and
posting to the marketplace. Does not generate status reports.
-keep
test mode: keep all files after simulation
-plot
test mode: generate plot (.png) files locally
-nopost
test mode: do not post results to the marketplace
-nosim
test mode: set up all files for simulation but do not simulate
Quick local run---Use:
cace_gensim.py <root_dir> -local -method=<method_name>
e.g.,
cace_gensim.py ~/design/XBG_1V23LC_V01 -local -method=DCVOLTAGE_VBG.1
"""
import os
import sys
import json
import re
import time
import shutil
import signal
import datetime
import subprocess
import faulthandler
from functools import reduce
from spiceunits import spice_unit_convert
from spiceunits import numeric
# Application path (path where this script is located)
apps_path = os.path.realpath(os.path.dirname(__file__))
launchproc = []
def construct_dut_from_path(pname, pathname, pinlist, foundry, node):
# Read the indicated file, find the .subckt line, and copy out the
# pin names and DUT name. Complain if pin names don't match pin names
# in the datasheet.
# NOTE: There may be more than one subcircuit in the netlist, so
# insist upon the actual DUT (pname)
subrex = re.compile('^[^\*]*[ \t]*.subckt[ \t]+(.*)$', re.IGNORECASE)
noderex = re.compile('\*\*\* Layout tech:[ \t]+([^ \t,]+),[ \t]+foundry[ \t]+([^ \t]+)', re.IGNORECASE)
outline = ""
dutname = ""
if not os.path.isfile(pathname):
print('Error: No design netlist file ' + pathname + ' found.')
return outline
# First pull in all lines of the file and concatenate all continuation
# lines.
with open(pathname, 'r') as ifile:
duttext = ifile.read()
dutlines = duttext.replace('\n+', ' ').splitlines()
found = 0
for line in dutlines:
lmatch = noderex.match(line)
if lmatch:
nlnode = lmatch.group(1)
nlfoundry = lmatch.group(2)
if nlfoundry != foundry:
print('Error: Foundry is ' + foundry + ' in spec sheet, ' + nlfoundry + ' in netlist.')
if nlnode != node:
print('Error: Node is ' + node + ' in spec sheet, ' + nlnode + ' in netlist.')
lmatch = subrex.match(line)
if lmatch:
rest = lmatch.group(1)
tokens = rest.split()
dutname = tokens[0]
if dutname == pname:
outline = outline + 'X' + dutname + ' '
for pin in tokens[1:]:
upin = pin.upper()
try:
pinmatch = next(item for item in pinlist if item['name'].upper() == upin)
except StopIteration:
# Maybe this is not the DUT?
found = 0
# Try the next line (to be done)
break
else:
outline = outline + pin + ' '
found += 1
break
if found == 0 and dutname == "":
print('File ' + pathname + ' does not contain any subcircuits!')
raise SyntaxError('File ' + pathname + ' does not contain any subcircuits!')
elif found == 0:
if dutname != pname:
print('File ' + pathname + ' does not have a subcircuit named ' + pname + '!')
raise SyntaxError('File ' + pathname + ' does not have a subcircuit named ' + pname + '!')
else:
print('Pins in schematic: ' + str(tokens[1:]))
print('Pins in datasheet: ', end='')
for pin in pinlist:
print(pin['name'] + ' ', end='')
print('')
print('File ' + pathname + ' subcircuit ' + pname + ' does not have expected pins!')
raise SyntaxError('File ' + pathname + ' subcircuit ' + pname + ' does not have expected pins!')
elif found != len(pinlist):
print('File ' + pathname + ' does not contain the project DUT ' + pname)
print('or not all pins of the DUT were found.')
print('Pinlist is : ', end='')
for pinrec in pinlist:
print(pinrec['name'] + ' ', end='')
print('')
print('Length of pinlist is ' + str(len(pinlist)))
print('Number of pins found in subcircuit call is ' + str(found))
raise SyntaxError('File ' + pathname + ' does not contain the project DUT!')
else:
outline = outline + dutname + '\n'
return outline
conditiontypes = {
"VOLTAGE": 1,
"DIGITAL": 2,
"CURRENT": 3,
"RISETIME": 4,
"FALLTIME": 5,
"RESISTANCE": 6,
"CAPACITANCE": 7,
"TEMPERATURE": 8,
"FREQUENCY": 9,
"CORNER": 10,
"SIGMA": 11,
"ITERATIONS": 12,
"TIME": 13
}
# floating-point numeric sequence generators, to be used with condition generator
def linseq(condition, unit, start, stop, step):
a = numeric(start)
e = numeric(stop)
s = numeric(step)
while (a < e + s):
if (a > e):
yield (condition, unit, stop)
else:
yield (condition, unit, str(a))
a = a + s
def logseq(condition, unit, start, stop, step):
a = numeric(start)
e = numeric(stop)
s = numeric(step)
while (a < e * s):
if (a > e):
yield (condition, unit, stop)
else:
yield (condition, unit, str(a))
a = a * s
# binary (integer) numeric sequence generators, to be used with condition generator
def bindigits(n, bits):
s = bin(n & int("1" * bits, 2))[2:]
return ("{0:0>%s}" % (bits)).format(s)
def twos_comp(val, bits):
"""compute the 2's compliment of int value val"""
if (val & (1 << (bits - 1))) != 0: # if sign bit is set e.g., 8bit: 128-255
val = val - (1 << bits) # compute negative value
return val # return positive value as is
def bcount(condition, unit, start, stop, step):
blen = len(start)
a = eval('0b' + start)
e = eval('0b' + stop)
if a > e:
a = twos_comp(a, blen)
e = twos_comp(e, blen)
s = int(step)
while (a < e + s):
if (a > e):
bstr = bindigits(e, blen)
else:
bstr = bindigits(a, blen)
yield (condition, unit, bstr)
a = a + s
def bshift(condition, unit, start, stop, step):
a = eval('0b' + start)
e = eval('0b' + stop)
if a > e:
a = twos_comp(a, blen)
e = twos_comp(e, blen)
s = int(step)
while (a < e * s):
if (a > e):
bstr = bindigits(e, blen)
else:
bstr = bindigits(a, blen)
yield (condition, unit, bstr)
a = a * s
# define a generator for conditions. Given a condition (dictionary),
# return (as a yield) each specified condition as a
# 3-tuple (condition_type, value, unit)
def condition_gen(cond):
lcond = cond['condition']
if "unit" in cond:
unit = cond['unit']
else:
unit = ''
if "enum" in cond:
for i in cond["enum"]:
yield(lcond, unit, i)
elif "min" in cond and "max" in cond and "linstep" in cond:
if unit == "'b" or lcond.split(':', 1)[0] == 'DIGITAL':
yield from bcount(lcond, unit, cond["min"], cond["max"], cond["linstep"])
else:
yield from linseq(lcond, unit, cond["min"], cond["max"], cond["linstep"])
elif "min" in cond and "max" in cond and "logstep" in cond:
if unit == "'b" or lcond.split(':', 1)[0] == 'DIGITAL':
yield from bshift(lcond, unit, cond["min"], cond["max"], cond["logstep"])
else:
yield from logseq(lcond, unit, cond["min"], cond["max"], cond["logstep"])
elif "min" in cond and "max" in cond and "typ" in cond:
yield(lcond, unit, cond["min"])
yield(lcond, unit, cond["typ"])
yield(lcond, unit, cond["max"])
elif "min" in cond and "max" in cond:
yield(lcond, unit, cond["min"])
yield(lcond, unit, cond["max"])
elif "min" in cond and "typ" in cond:
yield(lcond, unit, cond["min"])
yield(lcond, unit, cond["typ"])
elif "max" in cond and "typ" in cond:
yield(lcond, unit, cond["typ"])
yield(lcond, unit, cond["max"])
elif "min" in cond:
yield(lcond, unit, cond["min"])
elif "max" in cond:
yield(lcond, unit, cond["max"])
elif "typ" in cond:
yield(lcond, unit, cond["typ"])
# Find the maximum time to run a simulation. This is the maximum of:
# (1) maximum value, if method is RISETIME or FALLTIME, and (2) maximum
# RISETIME or FALLTIME of any condition.
#
# "lcondlist" is the list of local conditions extended by the list of
# all global conditions that are not overridden by local values.
#
# NOTE: This list is limited to rise and fall time values, as they are
# the only time constraints known to cace_gensim at this time. This list
# will be extended as more simulation methods are added.
def findmaxtime(param, lcondlist):
maxtime = 0.0
try:
simunit = param['unit']
except KeyError:
# Plots has no min/max/typ so doesn't require units.
if 'plot' in param:
return maxtime
maxval = 0.0
found = False
if 'max' in param:
prec = param['max']
if 'target' in prec:
pmax = prec['target']
try:
maxval = numeric(spice_unit_convert([simunit, pmax], 'time'))
found = True
except:
pass
if not found and 'typ' in param:
prec = param['typ']
if 'target' in prec:
ptyp = prec['target']
try:
maxval = numeric(spice_unit_convert([simunit, ptyp], 'time'))
found = True
except:
pass
if not found and 'min' in param:
prec = param['min']
if 'target' in prec:
pmin = prec['target']
try:
maxval = numeric(spice_unit_convert([simunit, pmin], 'time'))
found = True
except:
pass
if maxval > maxtime:
maxtime = maxval
for cond in lcondlist:
condtype = cond['condition'].split(':', 1)[0]
# print ('condtype ' + condtype)
if condtype == 'RISETIME' or condtype == 'FALLTIME':
condunit = cond['unit']
maxval = 0.0
if 'max' in cond:
maxval = numeric(spice_unit_convert([condunit, cond['max']], 'time'))
elif 'enum' in cond:
maxval = numeric(spice_unit_convert([condunit, cond['enum'][-1]], 'time'))
elif 'typ' in cond:
maxval = numeric(spice_unit_convert([condunit, cond['typ']], 'time'))
elif 'min' in cond:
maxval = numeric(spice_unit_convert([condunit, cond['min']], 'time'))
if maxval > maxtime:
maxtime = maxval
return maxtime
# Picked up from StackOverflow: Procedure to remove non-unique entries
# in a list of lists (as always, thanks StackOverflow!).
def uniquify(seq):
seen = set()
return [x for x in seq if str(x) not in seen and not seen.add(str(x))]
# Insert hints that have been selected in the characterization tool for
# aid in getting stubborn simulations to converge, or to avoid failures
# due to floating nodes, etc. The hints are somewhat open-ended and can
# be extended as needed. NOTE: Hint "method" selects the parameter
# method and is handled outside this routine, which only adds lines to
# the simulation netlist.
def insert_hints(param, ofile):
if 'hints' in param:
phints = param['hints']
if 'reltol' in phints:
value = phints['reltol']
ofile.write('.options reltol = ' + value + '\n')
if 'rshunt' in phints:
value = phints['rshunt']
ofile.write('.options rshunt = ' + value + '\n')
if 'itl1' in phints:
value = phints['itl1']
ofile.write('.options itl1 = ' + value + '\n')
if 'nodeset' in phints:
value = phints['nodeset']
# replace '/' in nodeset with '|' to match character replacement done
# on the output of magic.
ofile.write('.nodeset ' + value.replace('/', '|') + '\n')
if 'include' in phints:
value = phints['include']
ofile.write('.include ' + value + '\n')
# Replace the substitution token ${INCLUDE_DUT} with the contents of the DUT subcircuit
# netlist file. "functional" is a list of IP block names that are to be searched for in
# .include lines in the netlist and replaced with functional view equivalents (if such
# exist).
def inline_dut(filename, functional, rootpath, ofile):
comtrex = re.compile(r'^\*') # SPICE comment
inclrex = re.compile(r'[ \t]*\.include[ \t]+["\']?([^"\' \t]+)["\']?', re.IGNORECASE) # SPICE include statement
braktrex = re.compile(r'([^ \t]+)\[([^ \t])\]', re.IGNORECASE) # Node name with brackets
subcrex = re.compile(r'[ \t]*x([^ \t]+)[ \t]+(.*)$', re.IGNORECASE) # SPICE subcircuit line
librex = re.compile(r'(.*)__(.*)', re.IGNORECASE)
endrex = re.compile(r'[ \t]*\.end[ \t]*', re.IGNORECASE)
endsrex = re.compile(r'[ \t]*\.ends[ \t]*', re.IGNORECASE)
# IP names in the ridiculously complicated form
# <user_path>/design/ip/<proj_name>/<version>/<spice-type>/<proj_name>/<proj_netlist>
ippathrex = re.compile(r'(.+)/design/ip/([^/]+)/([^/]+)/([^/]+)/([^/]+)/([^/ \t]+)')
locpathrex = re.compile(r'(.+)/design/([^/]+)/spi/([^/]+)/([^/ \t]+)')
# This form does not appear on servers but is used if an IP block is being prepared locally.
altpathrex = re.compile(r'(.+)/design/([^/]+)/([^/]+)/([^/]+)/([^/ \t]+)')
# Local IP names in the form
# <user_path>/design/<project>/spi/<spice-type>/<proj_netlist>
# To be completed
with open(filename, 'r') as ifile:
nettext = ifile.read()
netlines = nettext.replace('\n+', ' ').splitlines()
for line in netlines:
subsline = line
cmatch = comtrex.match(line)
if cmatch:
print(line, file=ofile)
continue
# Check for ".end" which should be removed (but not ".ends", which must remain)
ematch = endrex.match(line)
if ematch:
smatch = endsrex.match(line)
if not smatch:
continue
imatch = inclrex.match(line)
if imatch:
incpath = imatch.group(1)
# Substitution behavior is complicated due to the difference between netlist
# files from schematic capture vs. layout and read-only vs. read-write IP.
incroot = os.path.split(incpath)[1]
incname = os.path.splitext(incroot)[0]
lmatch = librex.match(incname)
if lmatch:
ipname = lmatch.group(2)
else:
ipname = incname
if ipname.upper() in functional:
# Search for functional view (depends on if this is a read-only IP or
# read-write local subcircuit)
funcpath = None
ippath = ippathrex.match(incpath)
if ippath:
userpath = ippath.group(1)
ipname2 = ippath.group(2)
ipversion = ippath.group(3)
spitype = ippath.group(4)
ipname3 = ippath.group(5)
ipnetlist = ippath.group(6)
funcpath = userpath + '/design/ip/' + ipname2 + '/' + ipversion + '/spice-func/' + ipname + '.spice'
else:
locpath = locpathrex.match(incpath)
if locpath:
userpath = locpath.group(1)
ipname2 = locpath.group(2)
spitype = locpath.group(3)
ipnetlist = locpath.group(4)
funcpath = userpath + '/design/' + ipname2 + '/spi/func/' + ipname + '.spice'
else:
altpath = altpathrex.match(incpath)
if altpath:
userpath = altpath.group(1)
ipname2 = altpath.group(2)
spitype = altpath.group(3)
ipname3 = altpath.group(4)
ipnetlist = altpath.group(5)
funcpath = userpath + '/design/' + ipname2 + '/spi/func/' + ipname + '.spice'
funcpath = os.path.expanduser(funcpath)
if funcpath and os.path.exists(funcpath):
print('Subsituting functional view for IP block ' + ipname)
print('Original netlist is ' + incpath)
print('Functional netlist is ' + funcpath)
subsline = '.include ' + funcpath
elif funcpath:
print('Original netlist is ' + incpath)
print('Functional view specified but no functional view found.')
print('Tried looking for ' + funcpath)
print('Retaining original view.')
else:
print('Original netlist is ' + incpath)
print('Cannot make sense of netlist path to find functional view.')
# If include file name is in <lib>__<cell> format (from electric) and the
# functional view is not, then find the subcircuit call and replace the
# subcircuit name. At least at the moment, the vice versa case does not
# happen.
smatch = subcrex.match(line)
if smatch:
subinst = smatch.group(1)
tokens = smatch.group(2).split()
# Need to test for parameters passed to subcircuit. The actual subcircuit
# name occurs before any parameters.
params = []
pins = []
for token in tokens:
if '=' in token:
params.append(token)
else:
pins.append(token)
subname = pins[-1]
pins = pins[0:-1]
lmatch = librex.match(subname)
if lmatch:
testname = lmatch.group(1)
if testname.upper() in functional:
subsline = 'X' + subinst + ' ' + ' '.join(pins) + ' ' + testname + ' ' + ' '.join(params)
# Remove any array brackets from node names in the top-level subcircuit, because they
# interfere with the array notation used by XSPICE which may be present in functional
# views (replace bracket characters with underscores).
# subsline = subsline.replace('[', '_').replace(']', '_')
#
# Do this *only* when there are no spaces inside the brackets, or else any XSPICE
# primitives in the netlist containing arrays will get messed up.
subsline = braktrex.sub(r'\1_\2_', subsline)
ofile.write(subsline + '\n')
ofile.write('\n')
# Define how to write a simulation file by making substitutions into a
# template schematic.
def substitute(filename, fileinfo, template, simvals, maxtime, schemline,
localmode, param):
"""Simulation derived by substitution into template schematic"""
# Regular expressions
varex = re.compile(r'(\$\{[^ \}\t]+\})') # variable name ${name}
defaultex = re.compile(r'\$\{([^=]+)=([^=\}]+)\}') # name in ${name=default} format
condpinex = re.compile(r'\$\{([^:]+):([^:\}]+)\}') # name in ${cond:pin} format
condex = re.compile(r'\$\{([^\}]+)\}') # name in ${cond} format
sweepex = re.compile(r'\$\{([^\}]+):SWEEP([^\}]+)\}') # name in ${cond:[pin:]sweep} format
pinex = re.compile(r'PIN:([^:]+):([^:]+)') # name in ${PIN:pin_name:net_name} format
funcrex = re.compile(r'FUNCTIONAL:([^:]+)') # name in ${FUNCTIONAL:ip_name} format
colonsepex = re.compile(r'^([^:]+):([^:]+)$') # a:b (colon-separated values)
vectrex = re.compile(r'([^\[]+)\[([0-9]+)\]') # pin name is a vector signal
vect2rex = re.compile(r'([^<]+)<([0-9]+)>') # pin name is a vector signal (alternate style)
vect3rex = re.compile(r'([a-zA-Z][^0-9]*)([0-9]+)') # pin name is a vector signal (alternate style)
libdirrex = re.compile(r'.lib[ \t]+(.*)[ \t]+') # pick up library name from .lib
vinclrex = re.compile(r'[ \t]*`include[ \t]+"([^"]+)"') # verilog include statement
# Information about the DUT
simfilepath = fileinfo['simulation-path']
schempath = fileinfo['design-netlist-path']
schemname = fileinfo['design-netlist-name']
testbenchpath = fileinfo['testbench-netlist-path']
rootpath = fileinfo['root-path']
schempins = schemline.upper().split()[1:-1]
simpins = [None] * len(schempins)
suffix = os.path.splitext(template)[1]
functional = []
# Read ifile into a list
# Concatenate any continuation lines
with open(template, 'r') as ifile:
simtext = ifile.read()
simlines = simtext.replace('\n+', ' ').splitlines()
# Make initial pass over contents of template file, looking for SWEEP
# entries, and collapse simvals accordingly.
sweeps = []
for line in simlines:
sublist = sweepex.findall(line)
for pattern in sublist:
condition = pattern[0]
try:
entry = next(item for item in sweeps if item['condition'] == condition)
except (StopIteration, KeyError):
print("Did not find condition " + condition + " in sweeps.")
print("Pattern = " + str(pattern))
print("Sublist = " + str(sublist))
print("Sweeps = " + str(sweeps))
entry = {'condition':condition}
sweeps.append(entry)
# Find each entry in simvals with the same condition.
# Record the minimum, maximum, and step for substitution, at the same
# time removing that item from the entry.
lvals = []
units = ''
for simval in simvals:
try:
simrec = next(item for item in simval if item[0] == condition)
except StopIteration:
print('No condition = ' + condition + ' in record:\n')
ptext = str(simval) + '\n'
sys.stdout.buffer.write(ptext.encode('utf-8'))
else:
units = simrec[1]
lvals.append(numeric(simrec[2]))
simval.remove(simrec)
# Remove non-unique entries from lvals
lvals = list(set(lvals))
# Now parse lvals for minimum/maximum
entry['unit'] = units
minval = min(lvals)
maxval = max(lvals)
entry['START'] = str(minval)
entry['END'] = str(maxval)
numvals = len(lvals)
if numvals > 1:
entry['STEPS'] = str(numvals)
entry['STEP'] = str((maxval - minval) / (numvals - 1))
else:
entry['STEPS'] = "1"
entry['STEP'] = str(minval)
# Remove non-unique entries from simvals
simvals = uniquify(simvals)
simnum = 0
testbenches = []
for simval in simvals:
# Create the file
simnum += 1
simfilename = simfilepath + '/' + filename + '_' + str(simnum) + suffix
controlblock = False
with open(simfilename, 'w') as ofile:
for line in simlines:
# Check if the parser is in the ngspice control block section
if '.control' in line:
controlblock = True
elif '.endc' in line:
controlblock = False
elif controlblock == True:
ofile.write('set sqrnoise\n')
# This will need to be more nuanced if controlblock is used
# to do more than just insert the noise sim hack.
controlblock = False
# This will be replaced
subsline = line
# Find all variables to substitute
for patmatch in varex.finditer(line):
pattern = patmatch.group(1)
# If variable is in ${x=y} format, it declares a default value
# Remove the =y default part and keep it for later if needed.
defmatch = defaultex.match(pattern)
if defmatch:
default = defmatch.group(2)
vpattern = '${' + defmatch.group(1) + '}'
else:
default = []
vpattern = pattern
repl = []
no_repl_ok = False
vtype = -1
sweeprec = sweepex.match(vpattern)
if sweeprec:
sweeptype = sweeprec.group(2)
condition = sweeprec.group(1)
entry = next(item for item in sweeps if item['condition'] == condition)
uval = spice_unit_convert((entry['unit'], entry[sweeptype]))
repl = str(uval)
else:
cond = condex.match(vpattern)
if cond:
condition = cond.group(1)
# Check if the condition contains a pin vector
lmatch = vectrex.match(condition)
if lmatch:
pinidx = int(lmatch.group(2))
vcondition = lmatch.group(1)
vtype = 0
else:
lmatch = vect2rex.match(condition)
if lmatch:
pinidx = int(lmatch.group(2))
vcondition = lmatch.group(1)
vtype = 1
else:
lmatch = vect3rex.match(condition)
if lmatch:
pinidx = int(lmatch.group(2))
vcondition = lmatch.group(1)
vtype = 3
try:
entry = next((item for item in simval if item[0] == condition))
except (StopIteration, KeyError):
# check against known names (to-do: change if block to array of procs)
if condition == 'N':
repl = str(simnum)
elif condition == 'MAXTIME':
repl = str(maxtime)
elif condition == 'STEPTIME':
repl = str(maxtime / 100)
elif condition == 'DUT_PATH':
repl = schempath + '/' + schemname + '\n'
# DUT_PATH is required and is a good spot to
# insert hints (but deprecated in fafor of INCLUDE_DUT)
insert_hints(param, ofile)
elif condition == 'INCLUDE_DUT':
if len(functional) == 0:
repl = '.include ' + schempath + '/' + schemname + '\n'
else:
inline_dut(schempath + '/' + schemname, functional, rootpath, ofile)
repl = '** End of in-line DUT subcircuit'
insert_hints(param, ofile)
elif condition == 'DUT_CALL':
repl = schemline
elif condition == 'DUT_NAME':
# This verifies pin list of schematic vs. the netlist.
repl = schemline.split()[-1]
elif condition == 'FILENAME':
repl = filename
elif condition == 'RANDOM':
repl = str(int(time.time() * 1000) & 0x7fffffff)
# Stack math operators. Perform specified math
# operation on the last two values and replace.
#
# Note that ngspice is finicky about space around "=" so
# handle this in a way that keeps ngspice happy.
elif condition == '+':
smatch = varex.search(subsline)
watchend = smatch.start()
ltok = subsline[0:watchend].replace('=', ' = ').split()
ntok = ltok[:-2]
ntok.append(str(numeric(ltok[-2]) + numeric(ltok[-1])))
subsline = ' '.join(ntok).replace(' = ', '=') + line[patmatch.end():]
repl = ''
no_repl_ok = True
elif condition == '-':
smatch = varex.search(subsline)
watchend = smatch.start()
ltok = subsline[0:watchend].replace('=', ' = ').split()
ntok = ltok[:-2]
ntok.append(str(numeric(ltok[-2]) - numeric(ltok[-1])))
subsline = ' '.join(ntok).replace(' = ', '=') + line[patmatch.end():]
repl = ''
no_repl_ok = True
elif condition == '*':
smatch = varex.search(subsline)
watchend = smatch.start()
ltok = subsline[0:watchend].replace('=', ' = ').split()
ntok = ltok[:-2]
ntok.append(str(numeric(ltok[-2]) * numeric(ltok[-1])))
subsline = ' '.join(ntok).replace(' = ', '=') + line[patmatch.end():]
repl = ''
no_repl_ok = True
elif condition == '/':
smatch = varex.search(subsline)
watchend = smatch.start()
ltok = subsline[0:watchend].replace('=', ' = ').split()
ntok = ltok[:-2]
ntok.append(str(numeric(ltok[-2]) / numeric(ltok[-1])))
subsline = ' '.join(ntok).replace(' = ', '=') + line[patmatch.end():]
repl = ''
no_repl_ok = True
elif condition == 'MAX':
smatch = varex.search(subsline)
watchend = smatch.start()
ltok = subsline[0:watchend].replace('=', ' = ').split()
ntok = ltok[:-2]
ntok.append(str(max(numeric(ltok[-2]), numeric(ltok[-1]))))
subsline = ' '.join(ntok).replace(' = ', '=') + line[patmatch.end():]
repl = ''
no_repl_ok = True
elif condition == 'MIN':
smatch = varex.search(subsline)
watchend = smatch.start()
ltok = subsline[0:watchend].replace('=', ' = ').split()
ntok = ltok[:-2]
ntok.append(str(min(numeric(ltok[-2]), numeric(ltok[-1]))))
subsline = ' '.join(ntok).replace(' = ', '=') + line[patmatch.end():]
repl = ''
no_repl_ok = True
# 'NEG' acts on only the previous value in the string.
elif condition == 'NEG':
smatch = varex.search(subsline)
watchend = smatch.start()
ltok = subsline[0:watchend].replace('=', ' = ').split()
ntok = ltok[:-1]
ntok.append(str(-numeric(ltok[-1])))
subsline = ' '.join(ntok).replace(' = ', '=') + line[patmatch.end():]
repl = ''
no_repl_ok = True
# 'INT' also acts on only the previous value in the string.
elif condition == 'INT':
smatch = varex.search(subsline)
watchend = smatch.start()
ltok = subsline[0:watchend].replace('=', ' = ').split()
ntok = ltok[:-1]
ntok.append(str(int(ltok[-1])))
subsline = ' '.join(ntok).replace(' = ', '=') + line[patmatch.end():]
repl = ''
no_repl_ok = True
elif condition.find('PIN:') == 0:
# Parse for ${PIN:<pin_name>:<net_name>}
# Replace <pin_name> with index of pin from DUT subcircuit
pinrec = pinex.match(condition)
pinname = pinrec.group(1).upper()
netname = pinrec.group(2).upper()
try:
idx = schempins.index(pinname)
except ValueError:
repl = netname
else:
repl = '${PIN}'
simpins[idx] = netname
elif condition.find('FUNCTIONAL:') == 0:
# Parse for ${FUNCTIONAL:<ip_name>}
# Add <ip_name> to "functional" array.
# 'FUNCTIONAL' declarations must come before 'INCLUDE_DUT' or else
# substitution will not be made. 'INCLUDE_DUT' must be used in place
# of 'DUT_PATH' to get the correct behavior.
funcrec = funcrex.match(condition)
ipname = funcrec.group(1)
functional.append(ipname.upper())
repl = '** Using functional view for ' + ipname
else:
if lmatch:
try:
entry = next((item for item in simval if item[0].split('[')[0].split('<')[0] == vcondition))
except:
if vtype == 3:
for entry in simval:
lmatch = vect3rex.match(entry[0])
if lmatch:
if lmatch.group(1) == vcondition:
vlen = len(entry[2])
uval = entry[2][(vlen - 1) - pinidx]
repl = str(uval)
break
else:
# if no match, subsline remains as-is.
pass
else:
# Handle as vector bit slice (see below)
vlen = len(entry[2])
uval = entry[2][(vlen - 1) - pinidx]
repl = str(uval)
# else if no match, subsline remains as-is.
else:
if lmatch:
# pull signal at pinidx out of the vector.
# Note: DIGITAL assumes binary value. May want to
# allow general case of real-valued vectors, which would
# require a spice unit conversion routine without indexing.
vlen = len(entry[2])
uval = entry[2][(vlen - 1) - pinidx]
else:
uval = spice_unit_convert(entry[1:])
repl = str(uval)
if not repl and default:
# Use default if no match was found and default was specified
repl = default
if repl:
# Make the variable substitution
subsline = subsline.replace(pattern, repl)
elif not no_repl_ok:
print('Warning: Variable ' + pattern + ' had no substitution')
# Check if ${PIN} are in line. If so, order by index and
# rewrite pins in order
for i in range(len(simpins)):
if '${PIN}' in subsline:
if simpins[i]:
subsline = subsline.replace('${PIN}', simpins[i], 1)
else:
print("Error: simpins is " + str(simpins) + '\n')
print(" subsline is " + subsline + '\n')
print(" i is " + str(i) + '\n')
# Check for a verilog include file, and if any is found, copy it
# to the target simulation directory. Replace any leading path
# with the local current working directory '.'.
vmatch = vinclrex.match(subsline)
if vmatch:
incfile = vmatch.group(1)
incroot = os.path.split(incfile)[1]
curpath = os.path.split(template)[0]
incpath = os.path.abspath(os.path.join(curpath, incfile))
shutil.copy(incpath, simfilepath + '/' + incroot)
subsline = ' `include "./' + incroot + '"'
# Write the modified output line (with variable substitutions)
ofile.write(subsline + '\n')
# Add information about testbench file and conditions to datasheet JSON,
# which can be parsed by cace_launch.py.
testbench = {}
testbench['filename'] = simfilename
testbench['prefix'] = filename
testbench['conditions'] = simval
testbenches.append(testbench)
return testbenches
# Define how to write simulation devices
def generate_simfiles(datatop, fileinfo, arguments, methods, localmode):
# pull out the relevant part, which is "data-sheet"
dsheet = datatop['data-sheet']
# grab values held in 'fileinfo'
testbenchpath = fileinfo['testbench-netlist-path']
# electrical parameter list comes from "methods" if non-NULL.
# Otherwise, each entry in 'methods' is checked against the
# electrical parameters.
if 'electrical-params' in dsheet:
eparamlist = dsheet['electrical-params']
else:
eparamlist = []
if 'physical-params' in dsheet:
pparamlist = dsheet['physical-params']
else:
pparamlist = []
# If specific methods are called out for simulation using option "-method=", then
# generate the list of electrical parameters for those methods only.
if methods:
neweparamlist = []
newpparamlist = []
for method in methods:
# If method is <methodname>.<index>, simulate only the <index>th instance of
# the method.
if '.' in method:
(method, index) = method.split('.')
else:
index = []
if method == 'physical':
usedmethods = list(item for item in pparamlist if item['condition'] == index)
if not usedmethods:
print('Unknown parameter ' + index + ' requested in options. Ignoring.\n')
for used in usedmethods:
newpparamlist.append(used)
else:
usedmethods = list(item for item in eparamlist if item['method'] == method)
if not usedmethods:
print('Unknown method ' + method + ' requested in options. Ignoring.\n')
if index:
neweparamlist.append(usedmethods[int(index)])
else:
for used in usedmethods:
neweparamlist.append(used)
if not neweparamlist and not newpparamlist:
print('Warning: No valid methods given as options, so no simulations will be done.\n')
if neweparamlist:
for param in neweparamlist:
if 'display' in param:
ptext = 'Simulating parameter: ' + param['display'] + ' (' + param['method'] + ')\n'
else:
ptext = 'Simulating method: ' + param['method'] + '\n'
sys.stdout.buffer.write(ptext.encode('utf-8'))
eparamlist = neweparamlist
if newpparamlist:
for param in newpparamlist:
if 'display' in param:
ptext = 'Checking parameter: ' + param['display'] + ' (' + param['condition'] + ')\n'
else:
ptext = 'Checking parameter: ' + param['condition'] + '\n'
sys.stdout.buffer.write(ptext.encode('utf-8'))
pparamlist = newpparamlist
# Diagnostic
# print('pparamlist:')
# for param in pparamlist:
# ptext = param['condition'] + '\n'
# sys.stdout.buffer.write(ptext.encode('utf-8'))
# print('eparamlist:')
# for param in eparamlist:
# ptext = param['method'] + '\n'
# sys.stdout.buffer.write(ptext.encode('utf-8'))
# major subcategories of "data-sheet"
gcondlist = dsheet['global-conditions']
# Make a copy of the pin list in the datasheet, and expand any vectors.
pinlist = []
vectrex = re.compile(r"([^\[]+)\[([0-9]+):([0-9]+)\]")
vect2rex = re.compile(r"([^<]+)\<([0-9]+):([0-9]+)\>")
vect3rex = re.compile(r"([^0-9]+)([0-9]+):([0-9]+)")
for pinrec in dsheet['pins']:
vmatch = vectrex.match(pinrec['name'])
if vmatch:
pinname = vmatch.group(1)
pinmin = vmatch.group(2)
pinmax = vmatch.group(3)
if int(pinmin) > int(pinmax):
pinmin = vmatch.group(3)
pinmax = vmatch.group(2)
for i in range(int(pinmin), int(pinmax) + 1):
newpinrec = pinrec.copy()
pinlist.append(newpinrec)
newpinrec['name'] = pinname + '[' + str(i) + ']'
else:
vmatch = vect2rex.match(pinrec['name'])
if vmatch:
pinname = vmatch.group(1)
pinmin = vmatch.group(2)
pinmax = vmatch.group(3)
if int(pinmin) > int(pinmax):
pinmin = vmatch.group(3)
pinmax = vmatch.group(2)
for i in range(int(pinmin), int(pinmax) + 1):
newpinrec = pinrec.copy()
pinlist.append(newpinrec)
newpinrec['name'] = pinname + '<' + str(i) + '>'
else:
vmatch = vect3rex.match(pinrec['name'])
if vmatch:
pinname = vmatch.group(1)
pinmin = vmatch.group(2)
pinmax = vmatch.group(3)
if int(pinmin) > int(pinmax):
pinmin = vmatch.group(3)
pinmax = vmatch.group(2)
for i in range(int(pinmin), int(pinmax) + 1):
newpinrec = pinrec.copy()
pinlist.append(newpinrec)
newpinrec['name'] = pinname + str(i)
else:
pinlist.append(pinrec)
# Make sure all local conditions define a pin. Those that are not
# associated with a pin will have a null string for the pin name.
for cond in gcondlist:
# Convert old style (separate condition, pin) to new style
if 'pin' in cond and cond['pin'] != '':
if ':' not in cond['condition']:
cond['condition'] += ':' + cond['pin']
cond.pop('pin', 0)
if 'order' not in cond:
try:
cond['order'] = conditiontypes[cond['condition']]
except:
cond['order'] = 0
# Find DUT netlist file and capture the subcircuit call line
schempath = fileinfo['design-netlist-path']
schemname = fileinfo['design-netlist-name']
pname = fileinfo['project-name']
dutpath = schempath + '/' + schemname
foundry = dsheet['foundry']
node = dsheet['node']
try:
schemline = construct_dut_from_path(pname, dutpath, pinlist, foundry, node)
except SyntaxError:
print("Failure to construct a DUT subcircuit. Does the design have ports?")
schemline = ''
if schemline == '':
# Error finding DUT file. If only physical parameters are requested, this may
# not be a failure (e.g., chip top level)
if len(eparamlist) == 0:
prescore = 'unknown'
else:
prescore = 'fail'
else:
prescore = 'pass'
methodsfound = {}
# electrical parameter types determine the simulation type. Simulation
# types will be broken into individual routines (to be done)
for param in eparamlist:
# Break out name, method, and conditions as variables
simtype = param['method']
# For methods with ":", the filename is the part before the colon.
testbench = simtype.split(":")[0]
# If hint 'method' is applied, append the value to the method name.
# If no such method exists, flag a warning and revert to the original.
testbench_orig = None
if 'hints' in param:
phints = param['hints']
if 'method' in phints:
testbench_orig = testbench
testbench += phints['method']
if testbench == simtype:
if arguments:
if simtype not in arguments:
continue
if simtype in methodsfound:
fnum = methodsfound[simtype]
fsuffix = '_' + str(fnum)
methodsfound[simtype] = fnum + 1
else:
fsuffix = '_0'
methodsfound[simtype] = 1
else:
if arguments:
if testbench not in arguments:
continue
if testbench in methodsfound:
fnum = methodsfound[testbench]
fsuffix = '_' + str(fnum)
methodsfound[testbench] = fnum + 1
else:
fsuffix = '_0'
methodsfound[testbench] = 1
lcondlist = param['conditions']
# Make sure all local conditions which define a pin are in condition:pin form
for cond in lcondlist:
if 'pin' in cond and cond['pin'] != '':
if not ':' in cond['condition']:
cond['condition'] += ':' + cond['pin']
cond.pop('pin', 0)
if "order" not in cond:
if cond["condition"].split(':', 1)[0] in conditiontypes:
cond["order"] = conditiontypes[cond["condition"].split(':', 1)[0]]
else:
cond["order"] = 14
# Append to lcondlist any global conditions that aren't overridden by
# local values for the electrical parameter's set of conditions.
grec = []
for cond in gcondlist:
try:
test = next((item for item in lcondlist if item["condition"] == cond["condition"]))
except StopIteration:
grec.append(cond)
lcondlist.extend(grec) # Note this will permanently alter lcondlist
# Find the maximum simulation time required by this method
# Simulations are ordered so that "risetime" and "falltime" simulations
# on a pin will set the simulation time of any simulation of any other
# electrical parameter on that same pin.
maxtime = findmaxtime(param, lcondlist)
print("maxtime is " + str(maxtime))
# Sort the list for output conditions, ordering according to 'conditiontypes'.
list.sort(lcondlist, key=lambda k: k['order'])
# Find the length of each generator
cgenlen = []
for cond in lcondlist:
cgenlen.append(len(list(condition_gen(cond))))
# The lengths of all generators multiplied together is the number of
# simulations to be run
numsims = reduce(lambda x, y: x * y, cgenlen)
rlen = [x for x in cgenlen] # note floor division operator
# This code repeats each condition as necessary such that the final list
# (transposed) is a complete set of unique condition combinations.
cgensim = []
for i in range(len(rlen)):
mpre = reduce(lambda x, y: x * y, rlen[0:i], 1)
mpost = reduce(lambda x, y: x * y, rlen[i + 1:], 1)
clist = list(condition_gen(lcondlist[i]))
duplist = [item for item in list(condition_gen(lcondlist[i])) for j in range(mpre)]
cgensim.append(duplist * mpost)
# Transpose this list
simvals = list(map(list, zip(*cgensim)))
# Generate filename prefix for this electrical parameter
filename = testbench + fsuffix
# If methodtype is the name of a schematic netlist, then use
# it and make substitutions
# NOTE: Schematic methods are bundled with the DUT schematic
template = testbenchpath + '/' + testbench.lower() + '.spice'
if testbench_orig and not os.path.isfile(template):
print('Warning: Alternate testbench ' + testbench + ' cannot be found.')
print('Reverting to original testbench ' + testbench_orig)
testbench = testbench_orig
filename = testbench + fsuffix
template = testbenchpath + '/' + testbench.lower() + '.spice'
if os.path.isfile(template):
param['testbenches'] = substitute(filename, fileinfo, template,
simvals, maxtime, schemline, localmode, param)
# For cosimulations, if there is a '.tv' file corresponding to the '.spice' file,
# then make substitutions as for the .spice file, and place in characterization
# directory.
vtemplate = testbenchpath + '/' + testbench.lower() + '.tv'
if os.path.isfile(vtemplate):
substitute(filename, fileinfo, vtemplate,
simvals, maxtime, schemline, localmode, param)
else:
print('Error: No testbench file ' + template + '.')
for param in pparamlist:
# Break out name, method, and conditions as variables
cond = param['condition']
simtype = 'physical.' + cond
if arguments:
if simtype not in arguments:
continue
if simtype in methodsfound:
fnum = methodsfound[simtype]
fsuffix = '_' + str(fnum)
methodsfound[simtype] = fnum + 1
else:
fsuffix = '_0'
methodsfound[simtype] = 1
# Mark parameter as needing checking by cace_launch.
param['check'] = 'true'
# Remove "order" keys
for param in eparamlist:
lcondlist = param['conditions']
for cond in lcondlist:
cond.pop('order', 0)
gconds = dsheet['global-conditions']
for cond in gconds:
cond.pop('order', 0)
return prescore
def check_layout_out_of_date(spicepath, layoutpath):
# Check if a netlist (spicepath) is out-of-date relative to the layouts
# (layoutpath). Need to read the netlist and check all of the subcells.
need_capture = False
if not os.path.isfile(spicepath):
need_capture = True
elif not os.path.isfile(layoutpath):
need_capture = True
else:
spi_statbuf = os.stat(spicepath)
lay_statbuf = os.stat(layoutpath)
if spi_statbuf.st_mtime < lay_statbuf.st_mtime:
# netlist exists but is out-of-date
need_capture = True
else:
# only found that the top-level-layout is older than the
# netlist. Now need to read the netlist, find all subcircuits,
# and check those dates, too.
layoutdir = os.path.split(layoutpath)[0]
subrex = re.compile('^[^\*]*[ \t]*.subckt[ \t]+([^ \t]+).*$', re.IGNORECASE)
with open(spicepath, 'r') as ifile:
duttext = ifile.read()
dutlines = duttext.replace('\n+', ' ').splitlines()
for line in dutlines:
lmatch = subrex.match(line)
if lmatch:
subname = lmatch.group(1)
sublayout = layoutdir + '/' + subname + '.mag'
# subcircuits that cannot be found in the current directory are
# assumed to be library components and therefore never out-of-date.
if os.path.exists(sublayout):
sub_statbuf = os.stat(sublayout)
if spi_statbuf.st_mtime < lay_statbuf.st_mtime:
# netlist exists but is out-of-date
need_capture = True
break
return need_capture
def check_schematic_out_of_date(spicepath, schempath):
# Check if a netlist (spicepath) is out-of-date relative to the schematics
# (schempath). Need to read the netlist and check all of the subcells.
need_capture = False
if not os.path.isfile(spicepath):
print('Schematic-captured netlist does not exist. Need to regenerate.')
need_capture = True
elif not os.path.isfile(schempath):
need_capture = True
else:
spi_statbuf = os.stat(spicepath)
sch_statbuf = os.stat(schempath)
print('DIAGNOSTIC: Comparing ' + spicepath + ' to ' + schempath)
if spi_statbuf.st_mtime < sch_statbuf.st_mtime:
# netlist exists but is out-of-date
print('Netlist is older than top-level schematic')
need_capture = True
else:
print('Netlist is newer than top-level schematic, but must check subcircuits')
# only found that the top-level-schematic is older than the
# netlist. Now need to read the netlist, find all subcircuits,
# and check those dates, too.
schemdir = os.path.split(schempath)[0]
schrex = re.compile('\*\*[ \t]*sch_path:[ \t]*([^ \t\n]+)', re.IGNORECASE)
subrex = re.compile('^[^\*]*[ \t]*.subckt[ \t]+([^ \t]+).*$', re.IGNORECASE)
with open(spicepath, 'r') as ifile:
duttext = ifile.read()
dutlines = duttext.replace('\n+', ' ').splitlines()
for line in dutlines:
# xschem helpfully adds a "sch_path" comment line for every subcircuit
# coming from a separate schematic file.
lmatch = schrex.match(line)
if lmatch:
subschem = lmatch.group(1)
subfile = os.path.split(subschem)[1]
subname = os.path.splitext(subfile)[0]
# subcircuits that cannot be found in the current directory are
# assumed to be library components or read-only IP components and
# therefore never out-of-date.
if os.path.exists(subschem):
sub_statbuf = os.stat(subschem)
if spi_statbuf.st_mtime < sch_statbuf.st_mtime:
# netlist exists but is out-of-date
print('Netlist is older than subcircuit schematic ' + subname)
need_capture = True
break
return need_capture
def printwarn(output):
# Check output for warning or error
if not output:
return 0
warnrex = re.compile('.*warning', re.IGNORECASE)
errrex = re.compile('.*error', re.IGNORECASE)
errors = 0
outlines = output.splitlines()
for line in outlines:
try:
wmatch = warnrex.match(line)
except TypeError:
line = line.decode('utf-8')
wmatch = warnrex.match(line)
ematch = errrex.match(line)
if ematch:
errors += 1
if ematch or wmatch:
print(line)
return errors
def layout_netlist_includes(pexnetlist, dspath):
# Magic does not generate netlist output for LEF-like views unless
# the option "blackbox on" is passed to ext2spice, in which case it
# generates stub entries. When generating a PEX view for simulation,
# these entries need to be generated then replaced with the correct
# include statement to the ip/ directory.
comtrex = re.compile(r'^\*') # SPICE comment
subcrex = re.compile(r'^[ \t]*x([^ \t]+)[ \t]+(.*)$', re.IGNORECASE) # SPICE subcircuit line
subrex = re.compile(r'^[ \t]*.subckt[ \t]+([^ \t]+)[ \t]*([^ \t]+.*)', re.IGNORECASE)
endsrex = re.compile(r'^[ \t]*\.ends[ \t]*', re.IGNORECASE)
# Also convert commas from [X,Y] arrays to vertical bars as something
# that can be converted back as necessary. ngspice treats commas as
# special characters for some reason. ngspice also does not correctly
# handle slash characters in node names (okay as part of the netlist but
# fails if used in, say, ".nodeset"). Should be okay to replace all '/'
# because the layout extracted netlist won't have .include or other
# entries with filename paths.
# Find project tech path
if os.path.exists(dspath + '/.config/techdir'):
techdir = os.path.realpath(dspath + '/.config/techdir')
maglefdir = techdir + '/libs.ref/maglef'
else:
print('Warning: Project ' + dspath + ' does not define a target process!')
techdir = None
maglefdir = None
with open(pexnetlist, 'r') as ifile:
spitext = ifile.read()
# Find project info file (introduced with FFS, 2/2019. Does not exist in earlier
# projects)
depends = {}
ipname = ''
if os.path.exists(dspath + '/.config/info'):
with open(dspath + '/.config/info', 'r') as ifile:
infolines = ifile.read().splitlines
deprec = False
for line in infolines:
if 'dependencies:' in line:
deprec = True
if deprec:
if 'version' in line:
version = line.split()[1].strip("'")
if ipname != '':
depends[ipname] = version
ipname = ''
else:
print('Error: Badly formed info file in .config', file=sys.stderr)
else:
ipname = line.strip(':')
spilines = spitext.replace('\n+', ' ').replace(',', '|').replace('/','|').splitlines()
newspilines = []
extended_names = []
pinsorts = {}
inbox = False
for line in spilines:
cmatch = comtrex.match(line)
smatch = subrex.match(line)
xmatch = subcrex.match(line)
if 'Black-box' in line:
inbox = True
elif not inbox:
if xmatch:
# Pull subcircuit name from an 'X' component and see if it matches any of the
# names that were rewritten in Electric <library>__<cell> style. If so, replace
# the subcircuit name with the modified name while preserving the rest of the
# component line.
rest = xmatch.group(2).split()
r1 = list(i for i in rest if '=' not in i)
r2 = list(i for i in rest if '=' in i)
subname = r1[-1]
r1 = r1[0:-1]
# Re-sort the pins if needed
if subname in pinsorts:
r1 = [r1[i] for i in pinsorts[subname]]
if subname.upper() in extended_names:
newsubname = subname + '__' + subname
newspilines.append('X' + xmatch.group(1) + ' ' + ' '.join(r1) + ' ' + newsubname + ' ' + ' '.join(r2))
else:
newspilines.append(line)
else:
newspilines.append(line)
elif cmatch:
newspilines.append(line)
elif smatch:
subname = smatch.group(1)
pinlist = smatch.group(2).split()
print("Parsing black-box subcircuit " + subname)
ippath = '~/design/ip/' + subname
ipfullpath = os.path.expanduser(ippath)
if os.path.exists(ipfullpath):
# Version control: Use the versions specified in the .config/info
# version list. If it does not exist (legacy behavior), then use the
# method outlined below (finds highest version number available).
if subname in depends:
useversion = str(depends[subname])
else:
versions = os.listdir(ipfullpath)
vf = list(float(i) for i in versions)
vi = vf.index(max(vf))
useversion = versions[vi]
versionpath = ipfullpath + '/' + useversion
# First to do: Check for /spice/lvs entry (which is readable), and
# check if pin order is correct. Flag a warning if it is not, and
# save the pin order in a record so that all X records can be pin
# sorted correctly.
if os.path.exists(versionpath + '/spice/lvs'):
lvspath = versionpath + '/spice/lvs/' + subname + '.spice'
# More spice file reading! This should be quick, as these files have
# only a single empty subcircuit in them.
found = False
with open(lvspath, 'r') as sfile:
stubtext = sfile.read()
stublines = stubtext.replace('\n+', ' ').replace(',', '|').splitlines()
for line in stublines:
smatch = subrex.match(line)
if smatch:
found = True
stubname = smatch.group(1)
stublist = smatch.group(2).split()
if stubname != subname + '__' + subname:
print('Error: Looking for subcircuit ' + subname + '__' + subname + ' in file ' + lvspath + ' but found subcircuit ' + stubname + ' instead!')
print("This simulation probably isn't going to go well.")
else:
needsort = False
for stubpin, subpin in zip(stublist, pinlist):
if stubpin.upper() != subpin.upper():
print('Warning: pin mismatch between layout and schematic stub header on subcircuit ' + subname)
print('Will sort layout netlist to match.')
print('Correct pin order is: ' + smatch.group(2))
needsort = True
break
if needsort:
pinorder = [i[0] for i in sorted(enumerate(pinlist), key = lambda x:stublist.index(x[1]))]
pinsorts[subname] = pinorder
break
if not found:
print('Error: Cannot find subcircuit in IP spice-stub entry.')
else:
print('Warning: IP has no spice-stub entry, cannot verify pin order.')
if os.path.exists(versionpath + '/spice/rcx'):
# This path is restricted and can only be seen by ngspice, which is privileged
# to read it. So we can only assume that it matches the spice/stub entry.
# NOTE (10/16/2018): Use unexpanded tilde expression in file.
# rcxpath = versionpath + '/spice/rcx/' + subname + '/' + subname + '__' + subname + '.spice'
rcxpath = ippath + '/' + useversion + '/spice/rcx/' + subname + '/' + subname + '__' + subname + '.spice'
newspilines.append('* Black-box entry replaced by path to RCX netlist')
newspilines.append('.include ' + rcxpath)
extended_names.append(subname.upper())
elif os.path.exists(ipfullpath + '/' + useversion + '/spice'):
# In a pinch, if there is no spice/rcx, try plain spice
# NOTE (10/16/2018): Use unexpanded tilde expression in file.
# spicepath = versionpath + '/spice/' + subname + '.spice'
spicepath = ippath + '/' + useversion + '/spice/' + subname + '.spice'
newspilines.append('* Black-box entry replaced by path to schematic netlist')
newspilines.append('.include ' + spicepath)
else:
# Leave as is, and let it force an error
newspilines.append(line)
inbox = False
elif maglefdir:
# Check tech file paths
found = False
maglefsubdirs = os.listdir(maglefdir)
for techsubdir in maglefsubdirs:
if not os.path.isdir(maglefdir + '/' + techsubdir):
continue
# print('Diagnostic: looking in ' + str(maglefdir) + ' ' + str(techsubdir))
maglefcells = os.listdir(maglefdir + '/' + techsubdir)
if subname + '.mag' in maglefcells:
# print("Diagnostic: Parsing black-box subcircuit " + subname)
# print('from tech path ' + maglefdir + '/' + techsubdir)
# Like the IP directory, can't read spi/ so have to assume it's there.
# Problem---there is no consistency across PDKs for the naming of
# files in spi/!
newspilines.append('* Need include to schematic netlist for ' + subname)
# However, the CDL file can be used to check pin order
cdlpath = techdir + '/libs.ref/' + techsubdir + '/' + techsubdir + '.cdl'
if os.path.exists(cdlpath):
# More spice file reading! This should be quick, as these files have
# only a empty subcircuits in them.
with open(cdlpath, 'r') as sfile:
stubtext = sfile.read()
stublines = stubtext.replace('\n+', ' ').replace(',', '|').splitlines()
for line in spilines:
smatch = subrex.match(line)
if smatch:
stubname = smatch.group(1)
stublist = smatch.group(2).split()
if stubname == subname:
found = True
needsort = False
for stubpin, subpin in zip(stublist, pinlist):
if stubpin.upper() != subpin.upper():
print('Warning: pin mismatch between layout and schematic stub header on subcircuit ' + subname)
print('Will sort layout netlist to match.')
print('Correct pin order is: ' + smatch.group(2))
needsort = True
break
if needsort:
pinorder = [i[0] for i in sorted(enumerate(pinlist), key = lambda x:stublist.index(x[1]))]
pinsorts[subname] = pinorder
if found:
break
else:
print('No file ' + cdlpath + ' found.')
print('Failure to find stub netlist for checking pin order. Good luck.')
break
if not found:
print('Error: Subcell ' + subname + ' not found in IP or tech paths.')
print('This netlist is not going to simulate correctly.')
newspilines.append('* Unknown black-box entry ' + subname)
newspilines.append(line)
elif endsrex.match(line):
inbox = False
with open(pexnetlist, 'w') as ofile:
for line in newspilines:
print(line, file=ofile)
def regenerate_netlists(localmode, dspath, dsheet):
# When running locally, 'netlist-source' determines whether to use the
# layout extracted netlist or the schematic captured netlist. Also for
# local running only, regenerate the netlist only if it is out of date,
# or if the user has selected forced regeneration in the settings.
dname = dsheet['ip-name']
magpath = dspath + '/mag/'
spicepath = dspath + '/spice/' # Schematic netlist for sim
pexpath = dspath + '/spice/pex/' # Layout netlist for sim (C-parasitics)
rcxpath = dspath + '/spice/rcx/' # Layout netlist for sim (R+C-parasitics)
lvspath = dspath + '/spice/lvs/' # Layout netlist for LVS
vlogpath = dspath + '/verilog/' # Verilog netlist for sim and LVS
netlistname = dname + '.spice'
schnetlist = spicepath + netlistname
rcxnetlist = rcxpath + netlistname
pexnetlist = pexpath + netlistname
lvsnetlist = lvspath + netlistname
layoutpath = magpath + dname + '.mag'
schempath = dspath + '/xschem/' + dname + '.sch'
verilogpath = vlogpath + dname + '.v'
pathlast = os.path.split(dspath)[1]
need_sch_capture = True
need_extract = True
need_pex = True
force_regenerate = False
# Check if datasheet has been marked for forced netlist regeneration
if 'regenerate' in dsheet:
if dsheet['regenerate'] == 'force':
force_regenerate = True
if not os.path.exists(schempath):
print('No schematic in path ' + schempath)
# Guess the source based on the file or files in the design directory,
# with preference given to layout. This may be overridden in local mode.
if localmode and ('netlist-source' in dsheet) and (not force_regenerate):
print("Checking for out-of-date netlists.\n")
netlist_source = dsheet['netlist-source']
need_sch_capture = check_schematic_out_of_date(schnetlist, schempath)
if netlist_source == 'layout':
netlist_path = pexnetlist
need_pex_extract = check_layout_out_of_date(pexnetlist, layoutpath)
need_lvs_extract = check_layout_out_of_date(laynetlist, layoutpath)
else:
netlist_path = schnetlist
need_lvs_extract = False
need_pex_extract = False
else:
if not localmode:
print("Remote use, ", end='');
print("forcing regeneration of all netlists.\n")
if 'netlist-source' in dsheet:
netlist_source = dsheet['netlist-source']
if netlist_source == 'layout':
netlist_path = pexnetlist
else:
netlist_path = schnetlist
need_lvs_extract = False
need_pex_extract = False
else:
if os.path.exists(layoutpath):
netlist_path = pexnetlist
dsheet['netlist-source'] = 'layout'
elif os.path.exists(schempath):
netlist_path = schnetlist
dsheet['netlist-source'] = 'schematic'
need_lvs_extract = False
need_pex_extract = False
elif os.path.exists(verilogpath):
netlist_path = verilogpath
dsheet['netlist-source'] = 'verilog'
need_lvs_extract = False
need_pex_extract = False
need_sch_capture = False
elif os.path.exists(verilogaltpath):
netlist_path = verilogaltpath
dsheet['netlist-source'] = 'verilog'
need_lvs_extract = False
need_pex_extract = False
need_sch_capture = False
if need_lvs_extract or need_pex_extract:
# Layout LVS netlist needs regenerating. Check for magic layout.
if not os.path.isfile(layoutpath):
print('Error: No netlist or layout for project ' + dname + '.')
print('(layout master file ' + layoutpath + ' not found.)\n')
return False
# Check for spi/lvs/ directory
if not os.path.exists(lvspath):
os.makedirs(lvspath)
# Check for spi/pex/ directory
if not os.path.exists(pexpath):
os.makedirs(pexpath)
print("Extracting LVS netlist from layout. . .")
mproc = subprocess.Popen(['magic', '-dnull', '-noconsole',
layoutpath], stdin = subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, cwd = dspath + '/mag',
universal_newlines = True)
mproc.stdin.write("select top cell\n")
mproc.stdin.write("expand true\n")
mproc.stdin.write("extract all\n")
mproc.stdin.write("ext2spice hierarchy on\n")
mproc.stdin.write("ext2spice format ngspice\n")
mproc.stdin.write("ext2spice scale off\n")
mproc.stdin.write("ext2spice renumber off\n")
mproc.stdin.write("ext2spice subcircuit on\n")
mproc.stdin.write("ext2spice global off\n")
# Don't want black box entries, but create them so that we know which
# subcircuits are in the ip path, then replace them.
mproc.stdin.write("ext2spice blackbox on\n")
if need_lvs_extract:
mproc.stdin.write("ext2spice cthresh infinite\n")
mproc.stdin.write("ext2spice rthresh infinite\n")
mproc.stdin.write("ext2spice -o " + laynetlist + "\n")
if need_pex_extract:
mproc.stdin.write("ext2spice cthresh 0.005\n")
mproc.stdin.write("ext2spice rthresh 1\n")
mproc.stdin.write("ext2spice -o " + pexnetlist + "\n")
mproc.stdin.write("quit -noprompt\n")
magout = mproc.communicate()[0]
printwarn(magout)
if mproc.returncode != 0:
print('Magic process returned error code ' + str(mproc.returncode) + '\n')
if need_lvs_extract and not os.path.isfile(laynetlist):
print('Error: No LVS netlist extracted from magic.')
if need_pex_extract and not os.path.isfile(pexnetlist):
print('Error: No parasitic extracted netlist extracted from magic.')
if (mproc.returncode != 0) or (need_lvs_extract and not os.path.isfile(laynetlist)) or (need_pex_extract and not os.path.isfile(pexnetlist)):
return False
if need_pex_extract and os.path.isfile(pexnetlist):
print('Generating include statements for read-only IP blocks in layout, if needed')
layout_netlist_includes(pexnetlist, dspath)
if need_sch_capture:
# Netlist needs regenerating. Check for xschem schematic
if not os.path.isfile(schempath):
if os.path.isfile(verilogpath):
print('No schematic for project.')
print('Using verilog netlist ' + verilogpath + ' for simulation and LVS.')
return verilogpath
elif os.path.isfile(verilogaltpath):
print('No schematic for project.')
print('Using verilog netlist ' + verilogaltpath + ' for simulation and LVS.')
return verilogaltpath
else:
print('Error: No netlist or schematic for project ' + dname + '.')
print('(schematic master file ' + schempath + ' not found.)\n')
print('Error: No verilog netlist ' + verilogpath + ' or ' + verilogaltpath + ', either.')
return False
if need_sch_capture:
print("Generating simulation netlist from schematic. . .")
# Generate the netlist
print('Calling xschem to generate netlist')
if not os.path.exists(spicepath):
os.makedirs(spicepath)
xproc = subprocess.Popen(['xschem', '-n', '-r', '-q',
'--tcl "set top_subckt 1',
'-o', schnetlist, dname + '.sch'],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
cwd = dspath + '/xschem')
xout = xproc.communicate()[0]
if xproc.returncode != 0:
for line in xout.splitlines():
print(line.decode('utf-8'))
print('Xschem process returned error code ' + str(xproc.returncode) + '\n')
else:
printwarn(xout)
if not os.path.isfile(schnetlist):
print('Error: No netlist found for the circuit!\n')
print('(schematic netlist for simulation ' + schnetlist + ' not found.)\n')
if need_sch_capture:
if (not os.path.isfile(schnetlist)):
return False
return netlist_path
def cleanup_exit(signum, frame):
global launchproc
print("CACE gensim: Received termination signal.")
if launchproc:
print("CACE gensim: Stopping simulations now.")
launchproc.terminate()
else:
sys.exit(1)
# Main entry point. Read arguments, print usage or load the json file
# and call generate_simfiles.
if __name__ == '__main__':
faulthandler.register(signal.SIGUSR2)
signal.signal(signal.SIGINT, cleanup_exit)
signal.signal(signal.SIGTERM, cleanup_exit)
# Divide up command line into options and arguments
options = []
arguments = []
localmode = False
for item in sys.argv[1:]:
if item.find('-', 0) == 0:
options.append(item)
else:
arguments.append(item)
# Read the JSON file
root_path = []
if len(arguments) > 0:
root_path = str(sys.argv[1])
arguments = arguments[1:]
elif len(options) == 0:
# Print usage information when arguments don't match
print('Usage:\n')
print(' ' + str(sys.argv[0]) + ' [root_path] [options ...]')
print('Where [options ...] are one or more of the following:')
print(' -simdir <path>')
print(' is the location where simulation files and data should be placed.')
print(' -datasheetdir <path>')
print(' is the location of the JSON file describing the characterization.')
print(' -testbenchdir <path>')
print(' is the location of the netlists for the characterization methods.')
print(' -netlist <path>')
print(' is the location of the netlist for the device-under-test.')
print(' -layoutdir <path>')
print(' is the location of the layout netlist for the device-under-test.')
print(' -datasheet <name>')
print(' is the name of the datasheet JSON file.')
print(' -method <name>, ...')
print(' is a list of one or more names of methods to simulated. If omitted,')
print(' all methods are run for a complete characterization.')
print(' -local')
print(' indicates that cace_gensim is being run locally, not on the CACE')
print(' server, simulation conditions should be output along with results;')
print(' "local" mode implies that results are not posted to the marketplace')
print(' after simulation, and result files are kept.')
print(' -keep')
print(' test mode: keep all files after simulation')
print(' -plot')
print(' test mode: generate plot (.png) files locally')
print(' -nopost')
print(' test mode: do not post results to the marketplace')
print(' -nosim')
print(' test mode: set up all files for simulation but do not simulate')
sys.exit(0)
simulation_path = []
datasheet_path = []
testbench_path = []
design_path = []
layout_path = []
datasheet_name = []
methods = []
for option in options[:]:
result = option.split('=')
if result[0] == '-simdir':
simulation_path = result[1]
options.remove(option)
elif result[0] == '-datasheetdir':
datasheet_path = result[1]
options.remove(option)
elif result[0] == '-testbenchdir':
testbench_path = result[1]
options.remove(option)
elif result[0] == '-designdir':
design_path = result[1]
options.remove(option)
elif result[0] == '-layoutdir':
layout_path = result[1]
options.remove(option)
elif result[0] == '-datasheet':
datasheet_name = result[1]
options.remove(option)
elif result[0] == '-method':
methods.append(result[1])
options.remove(option)
elif result[0] == '-bypass':
bypassmode = True
options.remove(option)
elif result[0] == '-local':
localmode = True
# To be valid, must either have a root path or all other options must have been
# specified with full paths.
if not root_path:
err_result = 1
if not simulation_path:
print('Error: If root_path is not provided, -simdir is required.')
elif simulation_path[0] != '/':
print('Error: If root_path not provided, -simdir must be a full path.')
if not testbench_path:
print('Error: If root_path is not provided, -testbenchdir is required.')
elif testbench_path[0] != '/':
print('Error: If root_path not provided, -testbenchdir must be a full path.')
if not design_path:
print('Error: If root_path is not provided, -designdir is required.')
elif design_path[0] != '/':
print('Error: If root_path not provided, -designdir must be a full path.')
if not layout_path:
print('Error: If root_path is not provided, -layoutdir is required.')
elif layout_path[0] != '/':
print('Error: If root_path not provided, -layoutdir must be a full path.')
if not datasheet_path:
print('Error: If root_path is not provided, -datasheetdir is required.')
elif datasheet_path[0] != '/':
print('Error: If root_path not provided, -datasheetdir must be a full path.')
else:
err_result = 0
if err_result:
sys.exit(1)
# Apply defaults where not provided as command-line options
else:
if not datasheet_path:
datasheet_path = root_path
elif not os.path.isabs(datasheet_path):
datasheet_path = root_path + '/' + datasheet_path
if not datasheet_name:
datasheet_name = 'datasheet.json'
inputfile = datasheet_path + '/' + datasheet_name
# 2nd guess: 'project.json'
if not os.path.isfile(inputfile):
datasheet_name = 'project.json'
inputfile = datasheet_path + '/' + datasheet_name
# 3rd guess (legacy behavior): project directory name + '.json'
if not os.path.isfile(inputfile):
datasheet_name = os.path.split(datasheet_path)[1] + '.json'
inputfile = datasheet_path + '/' + datasheet_name
if not os.path.isfile(inputfile):
# Return to original datasheet name; error will be generated.
datasheet_name = 'datasheet.json'
elif localmode and root_path:
# Use normal path to local simulation workspace
simulation_path = root_path + '/ngspice'
# Check that datasheet path exists and that the datasheet is there
if not os.path.isdir(datasheet_path):
print('Error: Path to datasheet ' + datasheet_path + ' does not exist.')
sys.exit(1)
if len(os.path.splitext(datasheet_name)) != 2:
datasheet_name += '.json'
inputfile = datasheet_path + '/' + datasheet_name
if not os.path.isfile(inputfile):
print('Error: No datasheet file ' + inputfile )
sys.exit(1)
with open(inputfile) as ifile:
datatop = json.load(ifile)
# Pick up testbench and design paths from options now, since some of them
# depend on the request-hash value in the JSON file.
if not simulation_path:
if 'request-hash' in datatop:
hashname = datatop['request-hash']
simulation_path = root_path + '/' + hashname
elif os.path.isdir(root_path + '/ngspice'):
simulation_path = root_path + '/ngspice'
else:
simulation_path = root_path
elif not os.path.isabs(simulation_path):
simulation_path = root_path + '/' + simulation_path
if not testbench_path:
testbench_path = root_path + '/testbench'
elif not os.path.isabs(testbench_path):
testbench_path = root_path + '/' + testbench_path
if not design_path:
design_path = root_path + '/spi'
elif not os.path.isabs(design_path):
design_path = root_path + '/' + design_path
if not layout_path:
layout_path = root_path + '/mag'
elif not os.path.isabs(layout_path):
layout_path = root_path + '/' + layout_path
# Project name should be 'ip-name' in datatop['data-sheet']
try:
dsheet = datatop['data-sheet']
except KeyError:
print('Error: File ' + inputfile + ' is not a datasheet.\n')
sys.exit(1)
try:
name = dsheet['ip-name']
except KeyError:
print('Error: File ' + inputfile + ' is missing ip-name.\n')
sys.exit(1)
if not os.path.isdir(testbench_path):
print('Warning: Path ' + testbench_path + ' does not exist. ' +
'Testbench files are not available.\n')
if not os.path.isdir(design_path):
print('Warning: Path ' + design_path + ' does not exist. ' +
'Netlist files may not be available.\n')
# Simulation path is where the output is dumped. If it doesn't
# exist, then create it.
if not os.path.isdir(simulation_path):
print('Creating simulation path ' + simulation_path)
os.makedirs(simulation_path)
if not os.path.isdir(layout_path):
print('Creating layout path ' + layout_path)
os.makedirs(layout_path)
if not os.path.exists(layout_path + '/.magicrc'):
# Make sure correct .magicrc file exists
configdir = os.path.split(layout_path)[0]
rcpath = configdir + '/.config/techdir/libs.tech/magic'
pdkname = os.path.split(os.path.realpath(configdir + '/.config/techdir'))[1]
rcfile = rcpath + '/' + pdkname + '.magicrc'
if os.path.isdir(rcpath):
if os.path.exists(rcfile):
shutil.copy(rcfile, layout_path + '/.magicrc')
# Find the electrical parameter list. If it exists, then the
# template has been loaded. If not, find the template name,
# then load it from known templates. Templates may be local to
# the simulation files. Priority is (1) templates known to CACE
# (for challenges; cannot be overridden by a user; (2) templates
# local to the simulation (user-generated)
if not 'electrical-params' in dsheet and not 'physical-params' in dsheet:
print('Error: Circuit JSON file does not have a valid characterization template!\n')
sys.exit(1)
fullnetlistpath = regenerate_netlists(localmode, root_path, dsheet)
if not fullnetlistpath:
sys.exit(1)
netlistpath, netlistname = os.path.split(fullnetlistpath)
# If there is a 'hints.json' file in the root path, read it and apply to the
# electrical parameters. The file contains exactly one hint record per
# electrical parameter, although the hint record may be empty.
if os.path.exists(root_path + '/hints.json'):
with open(root_path + '/hints.json') as hfile:
hintlist = json.load(hfile)
i = 0
for eparam in dsheet['electrical-params']:
if not 'hints' in eparam:
if hintlist[i]:
eparam['hints'] = hintlist[i]
i += 1
# Construct fileinfo dictionary
fileinfo = {}
fileinfo['project-name'] = name
fileinfo['design-netlist-name'] = netlistname
fileinfo['design-netlist-path'] = netlistpath
fileinfo['testbench-netlist-path'] = testbench_path
fileinfo['simulation-path'] = simulation_path
fileinfo['root-path'] = root_path
# Generate the simulation files
prescore = generate_simfiles(datatop, fileinfo, arguments, methods, localmode)
if prescore == 'fail':
# In case of failure
options.append('-score=fail')
# Remove option keys
if 'keep' in datatop:
options.append('-keep')
datatop.pop('keep')
if 'plot' in datatop:
options.append('-plot')
datatop.pop('plot')
if 'nopost' in datatop:
options.append('-nopost')
datatop.pop('nopost')
if 'nosim' in datatop:
options.append('-nosim')
datatop.pop('nosim')
# Reconstruct the -simdir option for cace_launch
options.append('-simdir=' + simulation_path)
# Reconstruct the -layoutdir option for cace_launch
options.append('-layoutdir=' + layout_path)
# Reconstruct the -netlistdir option for cace_launch
options.append('-netlistdir=' + design_path)
# Reconstruct the -rootdir option for cace_launch
if root_path:
options.append('-rootdir=' + root_path)
# Dump the modified JSON file
basename = os.path.basename(inputfile)
outputfile = simulation_path + '/' + basename
with open(outputfile, 'w') as ofile:
json.dump(datatop, ofile, indent = 4)
# Launch simulator as a subprocess and wait for it to finish
# Waiting is important, as otherwise child processes get detached and it
# becomes very difficult to find them if the simulation needs to be stopped.
launchname = apps_path + '/' + 'cace_launch.py'
# Diagnostic
print("Running: " + launchname + ' ' + outputfile)
for a in arguments:
print(a)
for o in options:
print(o)
with subprocess.Popen([launchname, outputfile, *arguments, *options],
stdout=subprocess.PIPE, bufsize = 1,
universal_newlines=True) as launchproc:
for line in launchproc.stdout:
print(line, end='')
sys.stdout.flush()
launchproc.stdout.close()
return_code = launchproc.wait()
if return_code != 0:
raise subprocess.CalledProcessError(return_code, launchname)
sys.exit(0)