blob: 30e98f5984e69fb65176d5541019d1b088439e7c [file] [log] [blame]
#!/usr/bin/env python3
"""
cace_launch.py
A simple script that pulls in a JSON file and uses the hash key to find the
directory of spice simulation netlists associated with that file, and runs
them. The output of all files in a category <SKU>_<METHOD>_<PIN>_* is
analyzed, and the result written back to the data structure. Then the
annotated structure is passed back to the marketplace.
"""
# NOTE: This file is only a local stand-in for the script that launches
# and manages jobs in parallel and that communicates job status with the
# front-end. This stand-alone version should not be used for any significant
# project, as all simulations are run sequentially and will tie up resources.
import os
import sys
import shutil
import tarfile
import json
import re
import math
import signal
import datetime
import requests
import subprocess
import faulthandler
from spiceunits import spice_unit_unconvert
from spiceunits import spice_unit_convert
import file_compressor
import cace_makeplot
# Fix this. . .
simulation_path = ""
og_server_url = ""
mktp_server_url = ""
# Variables needing to be global until this file is properly made into a class
simfiles_path = []
layout_path = []
netlist_path = []
root_path = []
hashname = ""
spiceproc = None
localmode = False
bypassmode = False
statdoc = {}
# Send the simulation status to the remote Open Galaxy host
def send_status(doc):
result = requests.post(og_server_url + '/opengalaxy/send_status_cace', json=doc)
print('send_status_cace ' + str(result.status_code))
# Make request to server sending annotated json back
def send_doc(doc):
result = requests.post(mktp_server_url + '/cace/save_result', json=doc)
print('send_doc ' + str(result.status_code))
# Pure HTTP post here. Add the file to files object and the hash/filename
# to the data params.
def send_file(hash, file, file_name):
files = {'file': file.getvalue()}
data = {'request-hash': hash, 'file-name': file_name}
result = requests.post(mktp_server_url + '/cace/save_result_files', files=files, data=data)
print('send_file ' + str(result.status_code))
# Clean up and exit on termination signal
def cleanup_exit(signum, frame):
global root_path
global simfiles_path
global simulation_path
global spiceproc
global statdoc
global localmode
if spiceproc:
print("CACE launch: Termination signal received.")
spiceproc.terminate()
spiceproc.wait()
# Remove simulation files
print("CACE launch: Simulations have been terminated.")
if localmode == False:
test = os.path.split(root_path)[0]
if test != simulation_path:
print('Error: Root path is not in the system simulation path. Not deleting.')
print('Root path is ' + root_path + '; simulation path is ' + simulation_path)
else:
subprocess.run(['rm', '-r', root_path])
else:
# Remove all .spice files, .data files, .raw files and copy of datasheet
os.chdir(simfiles_path)
if os.path.exists('datasheet.json'):
os.remove('datasheet.json')
files = os.listdir(simfiles_path)
for filename in files:
try:
fileext = os.path.splitext(filename)[1]
except:
pass
else:
if fileext == '.spice' or fileext == '.data' or fileext == '.raw':
os.remove(filename)
elif fileext == '.tv' or fileext == '.tvo' or fileext == '.lxt' or fileext == '.vcd':
os.remove(filename)
# Post exit status back to Open Galaxy
if statdoc and not localmode:
status['message'] = 'canceled'
send_status(statdoc)
# Exit
sys.exit(0)
# Handling of 2s complement values in calculations (e.g., "1000" is -8, not +8)
# If a value should be unsigned, then the units for the value should be one bit
# larger than represented. e.g., if unit = "4'b" and value = "1000" then value
# is -8, but if unit = "5'b" and value = "1000" then value is +8.
def twos_complement(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
# Calculation of results from collected data for an output record,
# given the type of calculation to perform in 'calctype'. Known
# calculations are minimum, maximum, and average (others can be
# added as needed).
def calculate(record, rawdata, conditions, calcrec, score, units, param):
# Calculate result from rawdata based on calctype; place
# result in record['value'].
# "calcrec" is parsed as "calctype"-"limittype", where:
# "calctype" is one of: avg, min, max
# "limittype" is one of: above, below, exact
# "min" alone implies "min-above"
# "max" alone implies "max-below"
# "avg" alone implies "avg-exact"
# Future development:
# Add "minimax", "maximin", and "typ" to calctypes (needs extra record(s))
# Add "range" to limittypes (needs extra record or tuple for target)
binrex = re.compile(r'([0-9]*)\'([bodh])', re.IGNORECASE)
data = rawdata
if 'filter' in record:
# Filter data by condition range.
filtspec = record['filter'].split('=')
if len(filtspec) == 2:
condition = filtspec[0].upper()
valuerange = filtspec[1].split(':')
# Pick data according to filter, which specifies a condition and value, or condition
# and range of values in the form "a:b". Syntax is limited and needs to be expanded.
if condition in conditions:
condvec = conditions[condition]
if len(valuerange) == 2:
valuemin = int(valuerange[0])
valuemax = int(valuerange[1])
data = list(i for i, j in zip(rawdata, condvec) if j >= valuemin and j <= valuemax)
else:
try:
valueonly = float(valuerange[0])
except ValueError:
valueonly = valuerange[0]
vtype = type(valueonly)
if vtype == type('str') or vtype == type('int'):
data = list(i for i, j in zip(rawdata, condvec) if j == valueonly)
if not data:
print('Error: no data match ' + condition + ' = ' + str(valueonly))
data = rawdata
else:
# Avoid round-off problems from floating-point values
d = valueonly * 0.001
data = list(i for i, j in zip(rawdata, condvec) if j - d < valueonly and j + d > valueonly)
if not data:
print('Error: no data match ' + condition + ' ~= ' + str(valueonly))
data = rawdata
# For "filter: typical", limit data to those taken for any condition value
# which is marked as typical for that condition.
elif record['filter'] == 'typ' or record['filter'] == 'typical':
# Create a boolean vector to track which results are under typical conditions
typvec = [True] * len(rawdata)
for condition in conditions:
# Pull record of the condition (this must exist by definition)
condrec = next(item for item in param['conditions'] if item['condition'] == condition)
if 'typ' not in condrec:
continue
try:
valueonly = float(condrec['typ'])
except ValueError:
valueonly = condrec['typ']
condvec = conditions[condition]
typloc = list(i == valueonly for i in condvec)
typvec = list(i and j for i, j in zip(typloc, typvec))
# Limit data to marked entries
data = list(i for i, j in zip(rawdata, typvec) if j)
try:
calctype, limittype = calcrec.split('-')
except ValueError:
calctype = calcrec
if calctype == 'min':
limittype = 'above'
elif calctype == 'max':
limittype = 'below'
elif calctype == 'avg':
limittype = 'exact'
elif calctype == 'diffmin':
limittype = 'above'
elif calctype == 'diffmax':
limittype = 'below'
else:
return 0
# Quick format sanity check---may need binary or hex conversion
# using the new method of letting units be 'b or 'h, etc.
# (to be done: signed conversion, see cace_makeplot.py)
if type(data[0]) == type('str'):
bmatch = binrex.match(units)
if (bmatch):
digits = bmatch.group(1)
if digits == '':
digits = len(data[0])
else:
digits = int(digits)
base = bmatch.group(2)
if base == 'b':
a = list(int(x, 2) for x in data)
elif base == 'o':
a = list(int(x, 8) for x in data)
elif base == 'd':
a = list(int(x, 10) for x in data)
else:
a = list(int(x, 16) for x in data)
data = list(twos_complement(x, digits) for x in a)
else:
print("Warning: result data do not correspond to specified units.")
print("Data = " + str(data))
return 0
# The target and result should both match the specified units, so convert
# the target if it is a binary, hex, etc., value.
if 'target' in record:
targval = record['target']
bmatch = binrex.match(units)
if (bmatch):
digits = bmatch.group(1)
base = bmatch.group(2)
if digits == '':
digits = len(targval)
else:
digits = int(digits)
try:
if base == 'b':
a = int(targval, 2)
elif base == 'o':
a = int(targval, 8)
elif base == 'd':
a = int(targval, 10)
else:
a = int(targval, 16)
targval = twos_complement(a, digits)
except:
print("Warning: target data do not correspond to units; assuming integer.")
# First run the calculation to get the single result value
if calctype == 'min':
# Result is the minimum of the data
value = min(data)
elif calctype == 'max':
# Result is the maximum of the data
value = max(data)
elif calctype == 'avg':
# Result is the average of the data
value = sum(data) / len(data)
elif calctype[0:3] == 'std':
# Result is the standard deviation of the data
mean = sum(data) / len(data)
value = pow(sum([((i - mean) * (i - mean)) for i in data]) / len(data), 0.5)
# For "stdX", where "X" is an integer, multiply the standard deviation by X
if len(calctype) > 3:
value *= int(calctype[3])
if len(calctype) > 4:
# For "stdXn", subtract X times the standard deviation from the mean
if calctype[4] == 'n':
value = mean - value
# For "stdXp", add X times the standard deviation to the mean
elif calctype[4] == 'p':
value = mean + value
elif calctype == 'diffmax':
value = max(data) - min(data)
elif calctype == 'diffmin':
value = min(data) - max(data)
else:
return 0
try:
record['value'] = '{0:.4g}'.format(value)
except ValueError:
print('Warning: Min/Typ/Max value is not not numeric; value is ' + value)
return 0
# Next calculate the score based on the limit type
if limittype == 'above':
# Score a penalty if value is below the target
if 'target' in record:
targval = float(targval)
dopassfail = False
if 'penalty' in record:
if record['penalty'] == 'fail':
dopassfail = True
else:
penalty = float(record['penalty'])
else:
penalty = 0
print('min = ' + str(value))
# NOTE: 0.0005 value corresponds to formatting above, so the
# value is not marked in error unless it would show a different
# value in the display.
if value < targval - 0.0005:
if dopassfail:
locscore = 'fail'
score = 'fail'
print('fail: target = ' + str(record['target']) + '\n')
else:
locscore = (targval - value) * penalty
print('fail: target = ' + str(record['target'])
+ ' penalty = ' + str(locscore))
if score != 'fail':
score += locscore
elif math.isnan(value):
locscore = 'fail'
score = 'fail'
else:
if dopassfail:
locscore = 'pass'
else:
locscore = 0
print('pass')
if dopassfail:
record['score'] = locscore
else:
record['score'] = '{0:.4g}'.format(locscore)
elif limittype == 'below':
# Score a penalty if value is above the target
if 'target' in record:
targval = float(targval)
dopassfail = False
if 'penalty' in record:
if record['penalty'] == 'fail':
dopassfail = True
else:
penalty = float(record['penalty'])
else:
penalty = 0
print('max = ' + str(value))
# NOTE: 0.0005 value corresponds to formatting above, so the
# value is not marked in error unless it would show a different
# value in the display.
if value > targval + 0.0005:
if dopassfail:
locscore = 'fail'
score = 'fail'
print('fail: target = ' + str(record['target']) + '\n')
else:
locscore = (value - targval) * penalty
print('fail: target = ' + str(record['target'])
+ ' penalty = ' + str(locscore))
if score != 'fail':
score += locscore
elif math.isnan(value):
locscore = 'fail'
score = 'fail'
else:
if dopassfail:
locscore = 'pass'
else:
locscore = 0
print('pass')
if dopassfail:
record['score'] = locscore
else:
record['score'] = '{0:.4g}'.format(locscore)
elif limittype == 'exact':
# Score a penalty if value is not equal to the target
if 'target' in record:
targval = float(targval)
dopassfail = False
if 'penalty' in record:
if record['penalty'] == 'fail':
dopassfail = True
else:
penalty = float(record['penalty'])
else:
penalty = 0
if value != targval:
if dopassfail:
locscore = 'fail'
score = 'fail'
print('off-target failure')
else:
locscore = abs(targval - value) * penalty
print('off-target: target = ' + str(record['target'])
+ ' penalty = ' + str(locscore))
if score != 'fail':
score += locscore
elif math.isnan(value):
locscore = 'fail'
score = 'fail'
else:
print('on-target')
if dopassfail:
locscore = 'pass'
else:
locscore = 0
if dopassfail:
record['score'] = locscore
else:
record['score'] = '{0:.4g}'.format(locscore)
elif limittype == 'legacy':
# Score a penalty if the value is not equal to the target, except
# that a lack of a minimum record implies no penalty below the
# target, and lack of a maximum record implies no penalty above
# the target. This is legacy behavior for "typ" records, and is
# used if no "calc" key appears in the "typ" record. "legacy" may
# also be explicitly stated, although it is considered deprecated
# in favor of "avg-max" and "avg-min".
if 'target' in record:
targval = float(targval)
if record['penalty'] == 'fail':
# "typical" should never be pass-fail
penalty = 0
else:
penalty = float(record['penalty'])
print('typ = ' + str(value))
if value != targval:
if 'max' in param and value > targval:
# max specified, so values below 'typ' are not costed
# this method deprecated, use 'calc' = 'avg-max' instead.
locscore = (value - targval) * penalty
print('above-target: target = ' + str(record['target'])
+ ' penalty = ' + str(locscore))
elif 'min' in param and value < targval:
# min specified, so values above 'typ' are not costed
# this method deprecated, use 'calc' = 'avg-min' instead.
locscore = (targval - value) * penalty
print('below-target: target = ' + str(record['target'])
+ ' penalty = ' + str(locscore))
elif 'max' not in param and 'min' not in param:
# Neither min and max specified, so value is costed on
# both sides of the target.
locscore = abs(targval - value) * penalty
print('off-target: target = ' + str(record['target'])
+ ' penalty = ' + str(locscore))
else:
locscore = 0
if score != 'fail':
score += locscore
else:
locscore = 0
print('on-target')
record['score'] = '{0:.4g}'.format(locscore)
# Note: Calctype 'none' performs no calculation. Record is unchanged,
# and "score" is returned unchanged.
return score
def run_and_analyze_lvs(dsheet):
ipname = dsheet['ip-name']
node = dsheet['node']
# Hack---node XH035 should have been specified as EFXH035A; allow
# the original one for backwards compatibility.
if node == 'XH035':
node = 'EFXH035A'
mag_path = netlist_path + '/lvs/' + ipname + '.spice'
schem_path = netlist_path + ipname + '.spice'
if not os.path.exists(schem_path):
schem_path = netlist_path + '/' + ipname + '.spice'
if not os.path.exists(schem_path):
if os.path.exists(root_path + '/verilog'):
schem_path = root_path + '/verilog/' + ipname + '.v'
# Check the netlist to see if the cell to match is a subcircuit. If
# not, then assume it is the top level.
is_subckt = False
subrex = re.compile('^[^\*]*[ \t]*.subckt[ \t]+([^ \t]+).*$', re.IGNORECASE)
with open(mag_path) as ifile:
spitext = ifile.read()
dutlines = spitext.replace('\n+', ' ').splitlines()
for line in dutlines:
lmatch = subrex.match(line)
if lmatch:
subname = lmatch.group(1)
if subname.lower() == ipname.lower():
is_subckt = True
break
if is_subckt:
layout_arg = mag_path + ' ' + ipname
else:
layout_arg = mag_path
if os.getenv('PDK_ROOT'):
PDK_ROOT = os.getenv('PDK_ROOT')
else:
PDK_ROOT = 'PREFIX/share/pdk'
# Get PDK name for finding the netgen setup file
if os.path.exists(root_path + '/.config'):
pdkdir = os.path.realpath(root_path + '/.config/techdir')
else:
foundry = dsheet['foundry']
pdkdir = PDK_ROOT + node
lvs_setup = pdkdir + '/libs.tech/netgen/' + node + '_setup.tcl'
# Run LVS as a subprocess and wait for it to finish. Use the -json
# switch to get a file that is easy to parse.
print('cace_launch.py: running netgen -batch lvs ')
print(layout_arg + ' ' + schem_path + ' ' + ipname + ' ' + lvs_setup + ' comp.out -json -blackbox')
lvsproc = subprocess.run(['netgen', '-batch', 'lvs',
layout_arg, schem_path + ' ' + ipname,
lvs_setup, 'comp.out', '-json', '-blackbox'], cwd=layout_path,
stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0)
with open(layout_path + '/comp.json', 'r') as cfile:
lvsdata = json.load(cfile)
# Count errors in the JSON file
failures = 0
ncells = len(lvsdata)
for c in range(0, ncells):
cellrec = lvsdata[c]
if c == ncells - 1:
topcell = True
else:
topcell = False
# Most errors must only be counted for the top cell, because individual
# failing cells are flattened and the matching attempted again on the
# flattened netlist.
if topcell:
if 'devices' in cellrec:
devices = cellrec['devices']
devlist = [val for pair in zip(devices[0], devices[1]) for val in pair]
devpair = list(devlist[p:p + 2] for p in range(0, len(devlist), 2))
for dev in devpair:
c1dev = dev[0]
c2dev = dev[1]
diffdevs = abs(c1dev[1] - c2dev[1])
failures += diffdevs
if 'nets' in cellrec:
nets = cellrec['nets']
diffnets = abs(nets[0] - nets[1])
failures += diffnets
if 'badnets' in cellrec:
badnets = cellrec['badnets']
failures += len(badnets)
if 'badelements' in cellrec:
badelements = cellrec['badelements']
failures += len(badelements)
if 'pins' in cellrec:
pins = cellrec['pins']
pinlist = [val for pair in zip(pins[0], pins[1]) for val in pair]
pinpair = list(pinlist[p:p + 2] for p in range(0, len(pinlist), 2))
for pin in pinpair:
if pin[0].lower() != pin[1].lower():
failures += 1
# Property errors must be counted for every cell
if 'properties' in cellrec:
properties = cellrec['properties']
failures += len(properties)
return failures
def apply_measure(varresult, measure, variables):
# Apply a measurement (record "measure") using vectors found in
# "varresult" and produce new vectors which overwrite the original
# ones. Operations may reduce "varresult" vectors to a single value.
# 'condition' defaults to TIME; but this only applies to transient analysis data!
if 'condition' in measure:
condition = measure['condition']
if condition == 'RESULT':
# 'RESULT' can either be a specified name (not recommended), or else it is
# taken to be the variable set as the result variable.
try:
activevar = next(item for item in variables if item['condition'] == condition)
except StopIteration:
try:
activevar = next(item for item in variables if 'result' in item)
except StopIteration:
print('Error: measurement condition ' + condition + ' does not exist!')
return 0
else:
condition = activevar['condition']
else:
condition = 'TIME'
# Convert old-style separate condition, pin to new style combined
if 'pin' in measure:
if ':' not in measure['condition']:
measure['condition'] += ':' + measure['pin']
measure.pop('pin', 0)
try:
activevar = next(item for item in variables if item['condition'] == condition)
except:
activeunit = ''
else:
if 'unit' in activevar:
activeunit = activevar['unit']
else:
activeunit = ''
try:
activetrace = varresult[condition]
except KeyError:
print("Measurement error: Condition " + condition + " does not exist in results.")
# No active trace; cannot continue.
return
rsize = len(activetrace)
if 'TIME' in varresult:
timevector = varresult['TIME']
try:
timevar = next(item for item in variables if item['condition'] == 'TIME')
except:
timeunit = 's'
else:
if 'unit' in timevar:
timeunit = timevar['unit']
else:
timeunit = 's'
else:
timevector = []
timeunit = ''
calctype = measure['calc']
# Diagnostic
# print("Measure calctype = " + calctype)
if calctype == 'RESULT':
# Change the 'result' marker to the indicated condition.
for var in variables:
if 'result' in var:
var.pop('result')
activevar['result'] = True
elif calctype == 'REMOVE':
# Remove the indicated condition vector.
varresult.pop(condition)
elif calctype == 'REBASE':
# Rebase specified vector (subtract minimum value from all components)
base = min(activetrace)
varresult[condition] = [i - base for i in activetrace]
elif calctype == 'ABS':
# Take absolute value of activetrace.
varresult[condition] = [abs(i) for i in activetrace]
elif calctype == 'NEGATE':
# Negate the specified vector
varresult[condition] = [-i for i in activetrace]
elif calctype == 'ADD':
if 'value' in measure:
v = float(measure['value'])
varresult[condition] = [i + v for i in activetrace]
else:
# Add the specified vector to the result and replace the result
varresult[condition] = [i + j for i, j in zip(activetrace, paramresult)]
elif calctype == 'SUBTRACT':
if 'value' in measure:
v = float(measure['value'])
varresult[condition] = [i - v for i in activetrace]
else:
# Subtract the specified vector from the result
varresult[condition] = [j - i for i, j in zip(activetrace, paramresult)]
elif calctype == 'MULTIPLY':
if 'value' in measure:
v = float(measure['value'])
varresult[condition] = [i * v for i in activetrace]
else:
# Multiply the specified vector by the result (e.g., to get power)
varresult[condition] = [j * i for i, j in zip(activetrace, paramresult)]
elif calctype == 'CLIP':
if timevector == []:
return
# Clip specified vector to the indicated times
if 'from' in measure:
fromtime = float(spice_unit_convert([timeunit, measure['from'], 'time']))
else:
fromtime = timevector[0]
if 'to' in measure:
totime = float(spice_unit_convert([timeunit, measure['to'], 'time']))
else:
totime = timevector[-1]
try:
fromidx = next(i for i, j in enumerate(timevector) if j >= fromtime)
except StopIteration:
fromidx = len(timevector) - 1
try:
toidx = next(i for i, j in enumerate(timevector) if j >= totime)
toidx += 1
except StopIteration:
toidx = len(timevector)
for key in varresult:
vector = varresult[key]
varresult[key] = vector[fromidx:toidx]
rsize = toidx - fromidx
elif calctype == 'MEAN':
if timevector == []:
return
# Get the mean value of all traces in the indicated range. Results are
# collapsed to the single mean value.
if 'from' in measure:
fromtime = float(spice_unit_convert([timeunit, measure['from'], 'time']))
else:
fromtime = timevector[0]
if 'to' in measure:
totime = float(spice_unit_convert([timeunit, measure['to'], 'time']))
else:
totime = timevector[-1]
try:
fromidx = next(i for i, j in enumerate(timevector) if j >= fromtime)
except StopIteration:
fromidx = len(timevector) - 1
try:
toidx = next(i for i, j in enumerate(timevector) if j >= totime)
toidx += 1
except StopIteration:
toidx = len(timevector)
# Correct time average requires weighting according to the size of the
# time slice.
tsum = timevector[toidx - 1] - timevector[fromidx]
for key in varresult:
vector = varresult[key]
try:
# Test if condition is a numeric value
varresult[key] = vector[fromidx] + 1
except TypeError:
# Some conditions like 'corner' cannot be averaged, so just take the
# first entry (may want to consider different handling)
varresult[key] = [vector[fromidx]]
else:
vtot = 0.0
for i in range(fromidx + 1, toidx):
# Note: This expression can and should be optimized!
vtot += ((vector[i] + vector[i - 1]) / 2) * (timevector[i] - timevector[i - 1])
varresult[key] = [vtot / tsum]
rsize = 1
elif calctype == 'RISINGEDGE':
if timevector == []:
return
# RISINGEDGE finds the time of a signal rising edge.
# parameters used are:
# 'from': start time of search (default zero)
# 'to': end time of search (default end)
# 'number': edge number (default first edge, or zero) (to be done)
# 'cross': measure time when signal crosses this value
# 'keep': determines what part of the vectors to keep
if 'from' in measure:
fromtime = float(spice_unit_convert([timeunit, measure['from'], 'time']))
else:
fromtime = timevector[0]
if 'to' in measure:
totime = float(spice_unit_convert([timeunit, measure['to'], 'time']))
else:
totime = timevector[-1]
if 'cross' in measure:
crossval = float(measure['cross'])
else:
crossval = (max(activetrace) + min(activetrace)) / 2;
try:
fromidx = next(i for i, j in enumerate(timevector) if j >= fromtime)
except StopIteration:
fromidx = len(timevector) - 1
try:
toidx = next(i for i, j in enumerate(timevector) if j >= totime)
toidx += 1
except StopIteration:
toidx = len(timevector)
try:
startidx = next(i for i, j in enumerate(activetrace[fromidx:toidx]) if j < crossval)
except StopIteration:
startidx = 0
startidx += fromidx
try:
riseidx = next(i for i, j in enumerate(activetrace[startidx:toidx]) if j >= crossval)
except StopIteration:
riseidx = toidx - startidx - 1
riseidx += startidx
# If not specified, 'keep' defaults to 'INSTANT'.
if 'keep' in measure:
keeptype = measure['keep']
if keeptype == 'BEFORE':
istart = 0
istop = riseidx
elif keeptype == 'AFTER':
istart = riseidx
istop = len(timevector)
else:
istart = riseidx
istop = riseidx + 1
else:
istart = riseidx
istop = riseidx + 1
for key in varresult:
vector = varresult[key]
varresult[key] = vector[istart:istop]
rsize = istop - istart
elif calctype == 'FALLINGEDGE':
if timevector == []:
return
# FALLINGEDGE finds the time of a signal rising edge.
# parameters used are:
# 'from': start time of search (default zero)
# 'to': end time of search (default end)
# 'number': edge number (default first edge, or zero) (to be done)
# 'cross': measure time when signal crosses this value
# 'keep': determines what part of the vectors to keep
if 'from' in measure:
fromtime = float(spice_unit_convert([timeunit, measure['from'], 'time']))
else:
fromtime = timevector[0]
if 'to' in measure:
totime = float(spice_unit_convert([timeunit, measure['to'], 'time']))
else:
totime = timevector[-1]
if 'cross' in measure:
crossval = measure['cross']
else:
crossval = (max(activetrace) + min(activetrace)) / 2;
try:
fromidx = next(i for i, j in enumerate(timevector) if j >= fromtime)
except StopIteration:
fromidx = len(timevector) - 1
try:
toidx = next(i for i, j in enumerate(timevector) if j >= totime)
toidx += 1
except StopIteration:
toidx = len(timevector)
try:
startidx = next(i for i, j in enumerate(activetrace[fromidx:toidx]) if j > crossval)
except StopIteration:
startidx = 0
startidx += fromidx
try:
fallidx = next(i for i, j in enumerate(activetrace[startidx:toidx]) if j <= crossval)
except StopIteration:
fallidx = toidx - startidx - 1
fallidx += startidx
# If not specified, 'keep' defaults to 'INSTANT'.
if 'keep' in measure:
keeptype = measure['keep']
if keeptype == 'BEFORE':
istart = 0
istop = fallidx
elif keeptype == 'AFTER':
istart = fallidx
istop = len(timevector)
else:
istart = fallidx
istop = fallidx + 1
else:
istart = fallidx
istop = fallidx + 1
for key in varresult:
vector = varresult[key]
varresult[key] = vector[istart:istop]
rsize = istop - istart
elif calctype == 'STABLETIME':
if timevector == []:
return
# STABLETIME finds the time at which the signal stabilizes
# parameters used are:
# 'from': start time of search (default zero)
# 'to': end time of search (works backwards from here) (default end)
# 'slope': measure time when signal rate of change equals this slope
# 'keep': determines what part of the vectors to keep
if 'from' in measure:
fromtime = float(spice_unit_convert([timeunit, measure['from'], 'time']))
else:
fromtime = timevector[0]
if 'to' in measure:
totime = float(spice_unit_convert([timeunit, measure['to'], 'time']))
else:
totime = timevector[-1]
if 'limit' in measure:
limit = float(measure['limit'])
else:
# Default is 5% higher or lower than final value
limit = 0.05
try:
fromidx = next(i for i, j in enumerate(timevector) if j >= fromtime)
except StopIteration:
fromidx = len(timevector) - 1
try:
toidx = next(i for i, j in enumerate(timevector) if j >= totime)
except StopIteration:
toidx = len(timevector) - 1
finalval = activetrace[toidx]
toidx += 1
highval = finalval * (1.0 + limit)
lowval = finalval * (1.0 - limit)
try:
breakidx = next(i for i, j in reversed(list(enumerate(activetrace[fromidx:toidx]))) if j >= highval or j <= lowval)
except StopIteration:
breakidx = 0
breakidx += fromidx
# If not specified, 'keep' defaults to 'INSTANT'.
if 'keep' in measure:
keeptype = measure['keep']
if keeptype == 'BEFORE':
istart = 0
istop = breakidx
elif keeptype == 'AFTER':
istart = breakidx
istop = len(timevector)
else:
istart = breakidx
istop = breakidx + 1
else:
istart = breakidx
istop = breakidx + 1
for key in varresult:
vector = varresult[key]
varresult[key] = vector[istart:istop]
rsize = istop - istart
elif calctype == 'INSIDE':
if timevector == []:
return
# INSIDE retains only values which are inside the indicated limits
# 'min': minimum value limit to keep results
# 'max': maximum value limit to keep results
if 'from' in measure:
fromtime = float(spice_unit_convert([timeunit, measure['from'], 'time']))
else:
fromtime = timevector[0]
if 'to' in measure:
totime = float(spice_unit_convert([timeunit, measure['to'], 'time']))
else:
totime = timevector[-1]
if 'min' in measure:
minval = float(spice_unit_convert([activeunit, measure['min']]))
else:
minval = min(activetrace)
if 'max' in measure:
maxval = float(spice_unit_convert([activeunit, measure['max']]))
else:
maxval = max(activetrace)
try:
fromidx = next(i for i, j in enumerate(timevector) if j >= fromtime)
except StopIteration:
fromidx = len(timevector) - 1
try:
toidx = next(i for i, j in enumerate(timevector) if j >= totime)
toidx += 1
except StopIteration:
toidx = len(timevector)
goodidx = list(i for i, j in enumerate(activetrace[fromidx:toidx]) if j >= minval and j <= maxval)
# Diagnostic
if goodidx == []:
print('All vector components failed bounds test. max = ' + str(max(activetrace[fromidx:toidx])) + '; min = ' + str(min(activetrace[fromidx:toidx])))
goodidx = [i + fromidx for i in goodidx]
for key in varresult:
vector = varresult[key]
varresult[key] = [vector[i] for i in goodidx]
rsize = len(goodidx)
return rsize
def read_ascii_datafile(file, *args):
# Read a file of data produced by the 'wrdata' command in ngspice
# (simple ASCII data in columnar format)
# No unit conversions occur at this time.
#
# Arguments always include the analysis variable vector. If additional
# arguments are present in "args", they are value vectors representing
# additional columns in the data file, and should be treated similarly
# to the analysis variable. Note, however, that the wrdata format
# redundantly puts the analysis variable in every other column.
if not args:
print('Error: testbench does not specify contents of data file!')
return
dmatrix = []
filepath = simfiles_path + '/' + file
if not os.path.isfile(filepath):
# Handle ngspice's stupid handling of file extensions for the argument
# passed to the 'wrdata' command, which sometimes adds the .data
# extension and sometimes doesn't, regardless of whether the argument
# has an extension or not. Method here is to always include the
# extension in the argument, then look for possible ".data.data" files.
if os.path.isfile(filepath + '.data'):
filepath = filepath + '.data'
else:
print('Failed to find data file at path ' + filepath)
return 0
if not os.path.isfile(filepath):
print('Failed to find data file at path ' + filepath)
return 0
with open(filepath, 'r') as afile:
for line in afile.readlines():
ldata = line.split()
if ldata:
# Note: dependent variable (e.g., TIME) is repeated
# every other column, so record this only once, then
# read the remainder while skipping every other column.
dvec = []
dvec.append(float(ldata[0]))
dvec.extend(list(map(float, ldata[1::2])))
dmatrix.append(dvec)
# Transpose dmatrix
try:
dmatrix = list(map(list, zip(*dmatrix)))
except TypeError:
print("last line data are " + str(ldata))
print("dmatrix is " + str(dmatrix))
for dvalues, dvec in zip(dmatrix, args):
dvec.extend(dvalues)
try:
rval = len(ldata[0])
except TypeError:
rval = 1
return rval
if __name__ == '__main__':
# Exit in response to terminate signal by terminating ngspice processes
faulthandler.register(signal.SIGUSR2)
signal.signal(signal.SIGINT, cleanup_exit)
signal.signal(signal.SIGTERM, cleanup_exit)
options = []
arguments = []
for item in sys.argv[1:]:
if item.find('-', 0) == 0:
options.append(item)
else:
arguments.append(item)
# track the circuit score (for simulation; layout handled separately)
# (initial score may be overridden by passing -score=value to cace_launch.py)
score = 0.0
# read the JSON file
keepmode = False
plotmode = False
postmode = True
if len(arguments) > 0:
inputfile = arguments[0]
else:
raise SyntaxError('Usage: ' + sys.argv[0] + ' json_file [-options]\n')
if os.path.splitext(inputfile)[1] != '.json':
raise SyntaxError('Usage: ' + sys.argv[0] + ' json_file [-options]\n')
for item in options:
result = item.split('=')
if result[0] == '-keep':
keepmode = True
elif result[0] == '-plot':
plotmode = True
elif result[0] == '-nosim':
# Diagnostic
print('No simulations specified. . . cace_launch exiting.\n')
sys.exit(0)
elif result[0] == '-nopost':
postmode = False
keepmode = True
elif result[0] == '-simdir':
simfiles_path = result[1]
elif result[0] == '-layoutdir':
layout_path = result[1]
elif result[0] == '-netlistdir':
netlist_path = result[1]
elif result[0] == '-rootdir':
root_path = result[1]
elif result[0] == '-local':
localmode = True
bypassmode = False
postmode = False
keepmode = False
elif result[0] == '-bypass':
bypassmode = True
localmode = True
postmode = True
keepmode = False
elif result[0] == '-score':
score = result[1]
else:
raise SyntaxError('Bad option ' + item + ', options are -keep, -nosim, -nopost, -local, and -simdir=\n')
# Various information could be obtained from the input JSON file
# name, but it will be assumed that all information should be
# obtained from the contents of the JSON file itself.
with open(inputfile) as ifile:
datatop = json.load(ifile)
# Option passing through the JSON: use "nopost" or "keep" defined at the top level.
if 'nopost' in datatop:
postmode = False
datatop.pop('nopost')
if 'keep' in datatop:
keepmode = True
datatop.pop('keep')
if 'local' in datatop:
localmode = True
datatop.pop('local')
if 'request-hash' in datatop:
hashname = datatop['request-hash']
else:
print("Document JSON missing request-hash.")
sys.exit(1)
# Simfiles should be put in path specified by -simdir, or else
# put them in the working directory. Normally "-simdir" will
# be given on the command line.
if not simfiles_path:
if root_path:
simfiles_path = root_path + '/' + hashname
else:
simfiles_path = simulation_path + '/' + hashname
if not os.path.isdir(simfiles_path):
print('Error: Simulation folder ' + simfiles_path + ' does not exist.')
sys.exit(1)
if not layout_path:
if root_path:
layout_path = root_path + '/mag'
if not netlist_path:
if root_path:
netlist_path = root_path + '/spice'
# Change location to the simulation directory
# os.chdir(simfiles_path)
# pull out the relevant part of the JSON file, which is "data-sheet"
dsheet = datatop['data-sheet']
# Prepare a dictionary for the status and pass critical values from datatop.
try:
statdoc['UID'] = datatop['UID']
statdoc['request-hash'] = datatop['request-hash']
if 'project-folder' in datatop:
statdoc['project'] = datatop['project-folder']
else:
statdoc['project'] = dsheet['ip-name']
status = {}
status['message'] = 'initializing'
status['completed'] = '0'
status['total'] = 'unknown'
status['hash'] = datatop['request-hash']
statdoc['status'] = status
if not localmode:
send_status(statdoc)
except KeyError:
if not localmode:
print("Failed to generate status record.")
else:
pass
# find the eparamlist. If it exists, then the template has been
# loaded. If not, find the template name, then load it from known
# templates.
if 'electrical-params' in dsheet:
eparamlist = dsheet['electrical-params']
else:
eparamlist = []
if 'physical-params' in dsheet:
pparamlist = dsheet['physical-params']
else:
pparamlist = []
if eparamlist == [] and pparamlist == []:
print('Circuit JSON file does not have a characterization template!')
sys.exit(0)
simulations = 0
has_aux_files = False
# Diagnostic: find and print the number of files to be simulated
# Names are methodname, pinname, and simulation number.
totalsims = 0
filessimmed = []
for param in eparamlist:
if 'testbenches' in param:
totalsims += len(param['testbenches'])
print('Total files to simulate: ' + str(totalsims))
# Status
if statdoc and not localmode:
status['message'] = 'starting'
status['completed'] = '0'
status['total'] = str(totalsims)
send_status(statdoc)
for param in eparamlist:
# Process only entries in JSON that have 'testbenches' record
if 'testbenches' not in param:
continue
# Information needed to construct the filenames
simtype = param['method']
# For methods with ":", the filename is the part before the colon.
methodname = simtype.split(":")
if len(methodname) > 1:
testbench = methodname[0]
submethod = ":" + methodname[1]
else:
testbench = simtype
submethod = ""
# Simple outputs are followed by a single value
outrex = re.compile("[ \t]*\"?([^ \t\"]+)\"?(.*)$", re.IGNORECASE)
# conditions always follow as key=value pairs
dictrex = re.compile("[ \t]*([^ \t=]+)=([^ \t=]+)(.*)$", re.IGNORECASE)
# conditions specified as min:step:max match a result vector.
steprex = re.compile("[ \t]*([^:]+):([^:]+):([^:]+)$", re.IGNORECASE)
# specification of units as a binary, hex, etc., string in verilog format
binrex = re.compile(r'([0-9]*)\'([bodh])', re.IGNORECASE)
paramresult = [] # List of results
paramname = 'RESULT' # Name of the result parameter (default 'RESULT')
condresult = {} # Dictionary of condition names and values for each result
simfailures = 0 # Track simulations that don't generate results
# Run ngspice on each prepared simulation file
# FYI; ngspice generates output directly to the TTY, bypassing stdout
# and stdin, so that it can update the simulation time at the bottom
# of the screen without scrolling. Subvert this in ngspice, if possible.
# It is a bad practice of ngspice to output to the TTY in batch mode. . .
testbenches = param['testbenches']
print('Files to simulate method ' + testbenches[0]['prefix'] + ': ' + str(len(testbenches)))
for testbench in testbenches:
filename = testbench['filename']
filessimmed.append(filename)
fileprefix = testbench['prefix']
# All output lines start with prefix
outrexall = re.compile(fileprefix + submethod + "[ \t]+=?[ \t]*(.+)$", re.IGNORECASE)
# "measure" statements act on results of individual simulations,
# so keep the results separate until after measurements have been made
locparamresult = []
loccondresult = {}
locvarresult = {}
# Cosimulation: If there is a '.tv' file in the simulation directory
# with the same root name as the netlist file, then run iverilog and
# vvp. vvp will call ngspice from the verilog.
verilog = os.path.splitext(filename)[0] + '.tv'
my_env = os.environ.copy()
if os.path.exists(verilog):
cosim = True
simulator = 'vvp'
simargs = ['-M.', '-md_hdl_vpi']
filename = verilog + 'o'
# Copy the d_hdl object file into the simulation directory
shutil.copy('d_hdl_vpi.vpi', simfiles_path)
# Generate the output executable (.tvo) file for vvp.
subprocess.call(['iverilog', '-o' + filename, verilog])
# Specific version of ngspice must be used for cosimulation
# (Deprecated; default version of ngspice now supports cosimulation)
# my_env['NGSPICE_VERSION'] = 'cosim1'
# There must not be a file 'simulator_pipe' in the directory or vvp will fail.
if os.path.exists('simulator_pipe'):
os.remove('simulator_pipe')
else:
cosim = False
simulator = 'ngspice'
simargs = ['-b']
# Do not generate LXT files, as CACE does not have any methods to handle
# the data in them anyway.
my_env['NGSPICE_LXT2NO'] = '1'
# Is there a .spiceinit file in the simulation directory, and is
# one needed?
if not os.path.exists('.spiceinit'):
if os.path.exists(PDK_ROOT + node + '/libs.tech/ngspice/spinit'):
print('Copying ngspice configuration file from PDK.')
shutil.copy(PDK_ROOT + node + '/libs.tech/ngspice/spinit', '.spiceinit')
# ngspice writes to both stdout and stderr; capture all
# output equally. Print each line in real-time, flush the
# output buffer, and then accumulate the lines for processing.
# Note: bufsize = 1 and universal_newlines = True sets line-buffered output
print('Running: ' + simulator + ' ' + ' '.join(simargs) + ' ' + filename)
with subprocess.Popen([simulator, *simargs, filename],
stdout=subprocess.PIPE,
bufsize=1, universal_newlines=True, env=my_env) as spiceproc:
for line in spiceproc.stdout:
print(line, end='')
sys.stdout.flush()
# Each netlist can have as many results as there are in
# the "measurements" list for the electrical parameter,
# grouped according to common testbench netlist file and
# common set of conditions.
matchline = outrexall.match(line)
if matchline:
# Divide result into tokens. Space-separated values in quotes
# become a result vector; all other entries should be in the
# form <key>=<value>. Result value becomes "result":[<vector>]
# dictionary entry.
rest = matchline.group(1)
# ASCII file format handling: Data are in the indicated
# file in pairs of analysis variable (e.g., TIME for transients)
# and 'result'. Note that the analysis variable is
# always the first and every other column of the data file.
# The primary result is implicit. All other columns
# must be explicitly called out on the echo line.
if '.data' in rest:
print('Reading data from ASCII file.')
# "variables" are similar to conditions but describe what is
# being output from ngspice. There should be one entry for
# each (unique) column in the data file, matching the names
# given in the testbench file.
if 'variables' in param:
pvars = param['variables']
# Convert any old-style condition, pin
for var in pvars:
if 'pin' in var:
if not ':' in var['condition']:
var['condition'] += ':' + var['pin']
var.pop('pin')
else:
pvars = []
# Parse all additional variables. At least one (the
# analysis variable) must be specified.
data_args = []
extra = rest.split()
if len(extra) == 1:
# If the testbench specifies no vectors, then they
# must all be specified in order in 'variables' in
# the datasheet entry for the electrical parameters.
for var in pvars:
extra.append(var['condition'])
if not pvars:
print('Error: No variables specified in testbench or datasheet.')
rest = ''
if len(extra) > 1:
for varname in extra[1:]:
if varname not in locvarresult:
locvarresult[varname] = []
data_args.append(locvarresult[varname])
rsize = read_ascii_datafile(extra[0], *data_args)
# print('Read data file, rsize = ' + str(rsize))
# All values in extra[1:] should be param['variables']. If not, add
# an entry and flag a warning because information may be incomplete.
for varname in extra[1:]:
try:
var = next(item for item in pvars if item['condition'] == varname)
except StopIteration:
print('Variable ' + varname + ' not specified; ', end='')
print('information may be incomplete.')
var = {}
var['condition'] = varname
pvars.append(var)
# By default, the 2nd result is the result
if len(extra) > 2:
varname = extra[2]
varrec = next(item for item in pvars if item['condition'] == varname)
varrec['result'] = True
print('Setting condition ' + varname + ' as the result vector.')
# "measure" records are applied to individual simulation outputs,
# usually to reduce a time-based vector to a single value by
# measuring a steady-state value, peak-peak, frequency, etc.
if 'measure' in param:
# Diagnostic
# print('Applying measurements.')
for measure in param['measure']:
# Convert any old-style condition, pin
if 'pin' in measure:
if not ':' in measure['condition']:
measure['condition'] += ':' + measure['pin']
measure.pop('pin')
rsize = apply_measure(locvarresult, measure, pvars)
# Diagnostic
# print("after measure, rsize = " + str(rsize))
# print("locvarresult = " + str(locvarresult))
# Now recast locvarresult back into loccondresult.
for varname in locvarresult:
varrec = next(item for item in pvars if item['condition'] == varname)
if 'result' in varrec:
# print('Result for ' + varname + ' = ' + str(locvarresult[varname]))
locparamresult = locvarresult[varname]
paramname = varname
else:
# print('Condition ' + varname + ' = ' + str(locvarresult[varname]))
loccondresult[varname] = locvarresult[varname]
# Diagnostic
# print("Variable " + varname + " length = " + str(len(locvarresult[varname])))
rest = ''
else:
# For plots, there is not necessarily any measurements. Just
# copy values into locparamresult and loccondresult.
if len(locvarresult) == 0:
print('Warning: No result data for plot!')
for varname in locvarresult:
varrec = next(item for item in pvars if item['condition'] == varname)
if 'result' in varrec:
# print('Result for ' + varname + ' = ' + str(locvarresult[varname]))
locparamresult = locvarresult[varname]
rsize = len(locparamresult)
paramname = varname
else:
# print('Condition ' + varname + ' = ' + str(locvarresult[varname]))
loccondresult[varname] = locvarresult[varname]
rest = ''
else:
rsize = 0
# To-do: Handle raw files in similar manner to ASCII files.
while rest:
# This code depends on values coming first, followed by conditions.
matchtext = dictrex.match(rest)
if matchtext:
# Diagnostic!
condname = matchtext.group(1)
# Append to the condition list
if condname not in loccondresult:
loccondresult[condname] = []
# Find the condition name in the condition list, so values can
# be converted back to the expected units.
try:
condrec = next(item for item in param['conditions'] if item['condition'] == condname)
except StopIteration:
condunit = ''
else:
condunit = condrec['unit']
rest = matchtext.group(3)
matchstep = steprex.match(matchtext.group(2))
if matchstep:
# condition is in form min:step:max, and the
# number of values must match rsize.
cmin = float(matchstep.group(1))
cstep = float(matchstep.group(2))
cmax = float(matchstep.group(3))
cnum = int(round((cmax + cstep - cmin) / cstep))
if cnum != rsize:
print("Warning: Number of conditions (" + str(cnum) + ") is not")
print("equal to the number of results (" + str(rsize) + ")")
# Back-calculate the correct step size. Usually this
# means that the testbench did not add margin to the
# DC or AC stop condition, and the steps fell 1 short of
# the max.
if rsize > 1:
cstep = (float(cmax) - float(cmin)) / float(rsize - 1)
condvec = []
for r in range(rsize):
condvec.append(cmin)
cmin += cstep
cresult = spice_unit_unconvert([condunit, condvec])
condval = loccondresult[condname]
for cr in cresult:
condval.append(str(cr))
else:
# If there is a vector of results but only one condition, copy the
# condition for each result. Note that value may not be numeric.
# (To do: Apply 'measure' records here)
condval = loccondresult[condname]
try:
test = float(matchtext.group(2))
except ValueError:
cval = matchtext.group(2)
else:
cval = str(spice_unit_unconvert([condunit, test]))
for r in range(rsize):
condval.append(cval)
else:
# Not a key=value pair, so must be a result value
matchtext = outrex.match(rest)
if matchtext:
rest = matchtext.group(2)
rsize += 1
# Result value units come directly from the param record.
if 'unit' in param:
condunit = param['unit']
else:
condunit = ''
if binrex.match(condunit):
# Digital result with units 'b, 'h, etc. are kept as strings.
locparamresult.append(matchtext.group(1))
else:
locparamresult.append(float(matchtext.group(1)))
else:
print('Error: Result line cannot be parsed.')
print('Bad part of line is: ' + rest)
print('Full line is: ' + line)
break
# Values passed in testbench['conditions'] are common to each result
# value. From one line there are rsize values, so append each known
# condition to loccondresult rsize times.
for condrec in testbench['conditions']:
condname = condrec[0]
if condname in locvarresult:
print('Error: name ' + condname + ' is both a variable and a condition!')
print('Ignoring the condition.')
continue
if condname not in loccondresult:
loccondresult[condname] = []
condval = loccondresult[condname]
if 'unit' in condrec:
condunit = condrec['unit']
else:
condunit = ''
for r in range(rsize):
if condname.split(':')[0] == 'DIGITAL' or condname == 'CORNER':
# Values that are known to be strings
condval.append(condrec[2])
elif binrex.match(condunit):
# Alternate digital specification using units 'b, 'h, etc.
condval.append(condrec[2])
elif condname == 'ITERATIONS':
# Values that are known to be integers
condval.append(int(float(condrec[2])))
else:
# All other values to be treated as floats unless
# they are non-numeric, in which case they are
# treated as strings and copied as-is.
try:
condval.append(float(condrec[2]))
except ValueError:
# Values that are not numeric just get copied
condval.append(condrec[2])
spiceproc.stdout.close()
return_code = spiceproc.wait()
if return_code != 0:
raise subprocess.CalledProcessError(return_code, 'ngspice')
if len(locparamresult) > 0:
# Fold local results into total results
paramresult.extend(locparamresult)
for key in loccondresult:
if not key in condresult:
condresult[key] = loccondresult[key]
else:
condresult[key].extend(loccondresult[key])
else:
# Catch simulation failures
simfailures += 1
simulations += 1
# Clean up pipe file after cosimulation, also the .lxt file and .tvo files
if cosim:
if os.path.exists('simulator_pipe'):
os.remove('simulator_pipe')
# Remove all '.tvo', '.lxt', and '.vcd' files from the work area.
if keepmode == False:
files = os.listdir(simfiles_path)
for filename in files:
try:
fileext = os.path.splitext(filename)[1]
except:
pass
else:
if fileext == '.lxt' or fileext == '.vcd' or fileext == '.tvo' or fileext == '.vpi':
os.remove(filename)
# Other files to clean up
if os.path.exists('b3v32check.log'):
os.remove('b3v32check.log')
# Status
if statdoc and not localmode:
if simulations < totalsims:
status['message'] = 'in progress'
else:
status['message'] = 'completed'
status['completed'] = str(simulations)
status['total'] = str(totalsims)
send_status(statdoc)
# Evaluate concatentated results after all files for this electrical parameter
# have been run through simulation.
if paramresult:
print(simtype + ':')
# Diagnostic
# print("paramresult length " + str(len(paramresult)))
# for key in condresult:
# print("condresult length " + str(len(condresult[key])))
# Write out all results into the JSON file.
# Results are a list of lists; the first list is a list of
# methods, and the rest are sets of values corresponding to unique
# conditions. The first item in each lists is the result value
# for that set of conditions.
# Always keep results, even for remote CACE.
outnames = [paramname]
outunits = []
if 'unit' in param:
outunits.append(param['unit'])
else:
outunits.append('')
for key in condresult:
outnames.append(key)
try:
condrec = next(item for item in param['conditions'] if item['condition'] == key)
except:
try:
condrec = next(item for item in param['variables'] if item['condition'] == key)
except:
outunits.append('')
else:
if 'unit' in condrec:
outunits.append(condrec['unit'])
# 'variable' entries need to be unconverted
cconv = spice_unit_unconvert([condrec['unit'], condresult[key]])
condresult[key] = cconv
else:
outunits.append('')
else:
if 'unit' in condrec:
outunits.append(condrec['unit'])
else:
outunits.append('')
# Evaluate a script to transform the output, if there is an 'evaluate'
# record in the electrical parameter.
if 'evaluate' in param:
evalrec = param['evaluate']
try:
tool = evalrec['tool']
except:
print("Error: Evaluate record does not indicate a tool to run.")
break
else:
if tool != 'octave' and tool != 'matlab':
print("Error: CASE does not know how to use tool '" + tool + "'")
break
try:
script = evalrec['script']
except:
print("Error: Evaluate record does not indicate a script to run.")
break
else:
if os.path.isdir(root_path + '/testbench'):
tb_path = root_path + '/testbench/' + script
if not os.path.exists(tb_path):
if os.path.exists(tb_path + '.m'):
tb_path += '.m'
else:
print("Error: No script '" + script + "' found in testbench path.")
break
else:
print("Error: testbench directory not found in root path.")
break
# General purpose tool-based evaluation. For complex operations of
# any kind, dump the simulation results to a file "results.json" and
# invoke the specified tool, which should read the results and
# generate an output in the form of modified 'paramresult'.
# e.g., input is an array of transient vectors, output is an FFT
# analysis. Input is a voltage, output is an INL value. Note that
# 'unit' is the unit produced by the script. The script is supposed
# to know what units it gets as input and what it produces as output.
# Create octave-compatible output with structures for the condition
# names, units, and data.
with open('results.dat', 'w') as ofile:
print('# Created by cace_gensim.py', file=ofile)
print('# name: results', file=ofile)
print('# type: scalar struct', file=ofile)
print('# ndims: 2', file=ofile)
print('# 1 1', file=ofile)
numentries = len(outnames)
print('# length: ' + str(2 + numentries), file=ofile)
print('# name: NAMES', file=ofile)
print('# type: cell', file=ofile)
print('# rows: ' + str(numentries), file=ofile)
print('# columns: 1', file=ofile)
for name in outnames:
print('# name: <cell-element>', file=ofile)
print('# type: sq_string', file=ofile)
print('# elements: 1', file=ofile)
print('# length: ' + str(len(name)), file=ofile)
print(name, file=ofile)
print('', file=ofile)
print('', file=ofile)
print('', file=ofile)
print('', file=ofile)
print('# name: UNITS', file=ofile)
print('# type: cell', file=ofile)
print('# rows: ' + str(len(outunits)), file=ofile)
print('# columns: 1', file=ofile)
for unit in outunits:
print('# name: <cell-element>', file=ofile)
print('# type: sq_string', file=ofile)
print('# elements: 1', file=ofile)
print('# length: ' + str(len(unit)), file=ofile)
print(unit, file=ofile)
print('', file=ofile)
print('', file=ofile)
print('', file=ofile)
print('', file=ofile)
# Each condition is output as a 1D array with structure
# entry name equal to the condition name. If the units
# is empty then the array is a string. Otherwise, the
# array is numeric (as far as octave is concerned).
# First entry is the result (paramresult). This should never
# be a string (at least not in this version of CACE)
idx = 0
print('# name: ' + outnames[idx], file=ofile)
units = outunits[idx]
print('# type: matrix', file=ofile)
print('# rows: ' + str(len(paramresult)), file=ofile)
print('# columns: 1', file=ofile)
for value in paramresult:
print(' ' + str(value), file=ofile)
print('', file=ofile)
print('', file=ofile)
idx += 1
# The rest of the entries are the conditions. Note that the
# name must be a valid octave variable (letters, numbers,
# underscores) and so cannot use the condition name. However,
# each condition name is held in the names list, so it can be
# recovered. Each condition is called CONDITION2, CONDITION3,
# etc.
for key, entry in condresult.items():
print('# name: CONDITION' + str(idx + 1), file=ofile)
units = outunits[idx]
if units == '':
# Use cell array for strings
print('# type: cell', file=ofile)
print('# rows: ' + str(len(entry)), file=ofile)
print('# columns: 1', file=ofile)
for value in entry:
print('# name: <cell-element>', file=ofile)
print('# type: sq_string', file=ofile)
print('# elements: 1', file=ofile)
print('# length: ' + str(len(str(value))), file=ofile)
print(str(value), file=ofile)
print('', file=ofile)
print('', file=ofile)
else:
print('# type: matrix', file=ofile)
print('# rows: ' + str(len(entry)), file=ofile)
print('# columns: 1', file=ofile)
for value in entry:
print(' ' + str(value), file=ofile)
print('', file=ofile)
print('', file=ofile)
idx += 1
# Now run the specified octave script on the result. Script
# generates an output file. stdout/stderr can be ignored.
# May want to watch stderr for error messages and/or handle
# exit status.
postproc = subprocess.Popen(['octave-cli', tb_path],
stdout = subprocess.PIPE)
rvalues = postproc.communicate()[0].decode('ascii').splitlines()
# Replace paramresult with the numeric result
paramresult = list(float(item) for item in rvalues)
# pconv is paramresult scaled to the units used by param.
if 'unit' in param:
pconv = spice_unit_unconvert([param['unit'], paramresult])
else:
pconv = paramresult
outresult = []
outresult.append(outnames)
outresult.append(outunits)
for p in range(len(pconv)):
outvalues = []
outvalues.append(str(pconv[p]))
for key, value in condresult.items():
try:
outvalues.append(str(value[p]))
except IndexError:
# Note: This should not happen. . .
print("Error: number of values in result and conditions do not match!")
print("Result: " + str(len(pconv)))
print("Conditions: " + str(len(condresult)))
break
outresult.append(outvalues)
param['results'] = outresult
if 'unit' in param:
units = param['unit']
else:
units = ''
# Catch simulation failures.
if simfailures > 0:
print('Simulation failures: ' + str(simfailures))
score = 'fail'
if 'min' in param:
minrec = param['min']
if 'calc' in minrec:
calc = minrec['calc']
else:
calc = 'min-above'
minscore = calculate(minrec, pconv, condresult, calc, score, units, param)
if score != 'fail':
score = minscore
if 'max' in param:
maxrec = param['max']
if 'calc' in maxrec:
calc = maxrec['calc']
else:
calc = 'max-below'
maxscore = calculate(maxrec, pconv, condresult, calc, score, units, param)
if score != 'fail':
score = maxscore
if 'typ' in param:
typrec = param['typ']
if 'calc' in typrec:
calc = typrec['calc']
else:
calc = 'avg-legacy'
typscore = calculate(typrec, pconv, condresult, calc, score, units, param)
if score != 'fail':
score = typscore
if 'plot' in param:
# If not in localmode, or if in plotmode then create a plot and
# save it to a file.
plotrec = param['plot']
if localmode == False or bypassmode == True or plotmode == True:
if 'variables' in param:
variables = param['variables']
else:
variables = []
result = cace_makeplot.makeplot(plotrec, param['results'], variables)
# New behavior implemented 3/28/2017: Always keep results.
# param.pop('results')
if result:
plotrec['status'] = 'done'
has_aux_files = True
else:
print('Failure: No plot from file ' + filename + '\n')
else:
plotrec['status'] = 'done'
else:
try:
print('Failure: No output from file ' + filename + '\n')
except NameError:
print('Failure: No simulation file, so no output\n')
continue
# Handle errors where simulation generated no output.
# This is the one case where 'typ' can be treated as pass-fail.
# "score" will be set to "fail" for any of "min", "max", and
# "typ" that exists in the electrical parameters record and
# which specifies a target value. "value" is set to "failure"
# for display.
score = 'fail'
if 'typ' in param:
typrec = param['typ']
if 'target' in typrec:
typrec['score'] = 'fail'
typrec['value'] = 'failure'
if 'max' in param:
maxrec = param['max']
if 'target' in maxrec:
maxrec['score'] = 'fail'
maxrec['value'] = 'failure'
if 'min' in param:
minrec = param['min']
if 'target' in minrec:
minrec['score'] = 'fail'
minrec['value'] = 'failure'
# Pop the testbenches record, which has been replaced by the 'results' record.
param.pop('testbenches')
# Final cleanup step: Remove any remaining '.tv' files from the work area.
if keepmode == False:
files = os.listdir(simfiles_path)
for filename in files:
try:
fileext = os.path.splitext(filename)[1]
except:
pass
else:
if fileext == '.tv':
os.remove(filename)
# Report the final score, and save it to the JSON data
print('Completed ' + str(simulations) + ' of ' + str(totalsims) + ' simulations');
print('Circuit pre-extraction simulation total score (lower is better) = '
+ str(score))
if score == 'fail':
dsheet['score'] = 'fail'
else:
dsheet['score'] = '{0:.4g}'.format(score)
# Now handle physical parameters
netlist_source = dsheet['netlist-source']
areaval = 0.0
totalchecks = 0
for param in pparamlist:
if 'check' in param:
totalchecks += 1
print('Total physical parameters to check: ' + str(totalchecks))
for param in pparamlist:
# Process only entries in JSON that have the 'check' record
if 'check' not in param:
continue
if param['check'] != 'true':
continue
cond = param['condition']
if cond == 'device_area':
areaest = 0
ipname = dsheet['ip-name']
foundry = dsheet['foundry']
node = dsheet['node']
# Hack---node XH035 should have been specified as EFXH035A; allow
# the original one for backwards compatibility.
if node == 'XH035':
node = 'EFXH035A'
if layout_path and netlist_path:
# Run the device area (area estimation) script
if os.path.exists(netlist_path + '/' + ipname + '.spice'):
estproc = subprocess.Popen(['layout_estimate.py',
netlist_path + '/' + ipname + '.spice', node.lower()],
stdout=subprocess.PIPE,
cwd = layout_path, universal_newlines = True)
outlines = estproc.communicate()[0]
arealine = re.compile('.*=[ \t]*([0-9]+)[ \t]*um\^2')
for line in outlines.splitlines():
lmatch = arealine.match(line)
if lmatch:
areaum2 = lmatch.group(1)
areaest = int(areaum2)
if areaest > 0:
score = 'pass'
maxrec = param['max']
targarea = float(maxrec['target'])
maxrec['value'] = str(areaest)
if 'penalty' in maxrec:
if maxrec['penalty'] == 'fail':
if areaest > targarea:
score = 'fail'
else:
score = 'pass'
else:
try:
if areaest > targarea:
score = str((areaest - targarea) * float(maxrec['penalty']))
else:
score = 'pass'
except:
if areaest > targarea:
score = maxrec['penalty']
else:
score = 'pass'
else:
score = 'pass'
maxrec['score'] = score
if cond == 'area' or cond == 'height' or cond == 'width':
# First time for any of these, run the check and get values
if areaval == 0 and not netlist_source == 'schematic':
ipname = dsheet['ip-name']
foundry = dsheet['foundry']
node = dsheet['node']
# Hack---node XH035 should have been specified as EFXH035A; allow
# the original one for backwards compatibility.
if node == 'XH035':
node = 'EFXH035A'
if layout_path:
# Find the layout directory and check if there is a layout
# for the cell there. If not, use the layout estimation
# script. Result is either an actual area or an area estimate.
if os.path.exists(layout_path + '/' + ipname + '.mag'):
areaproc = subprocess.Popen(['magic',
'-dnull', '-noconsole', layout_path + '/' + ipname + '.mag'],
stdin = subprocess.PIPE, stdout = subprocess.PIPE,
cwd = layout_path, universal_newlines = True)
areaproc.stdin.write("select top cell\n")
areaproc.stdin.write("box\n")
areaproc.stdin.write("quit -noprompt\n")
outlines = areaproc.communicate()[0]
magrex = re.compile('microns:[ \t]+([0-9.]+)[ \t]*x[ \t]*([0-9.]+)[ \t]+.*[ \t]+([0-9.]+)[ \t]*$')
for line in outlines.splitlines():
lmatch = magrex.match(line)
if lmatch:
widthval = float(lmatch.group(1))
heightval = float(lmatch.group(2))
areaval = float(lmatch.group(3))
if areaval > 0:
# Now work through the physical parameters --- pass 1
# If area was estimated, then find target width and height
# for estimating actual width and height.
for checkparam in dsheet['physical-params']:
checkcond = checkparam['condition']
maxrec = checkparam['max']
if checkcond == 'area':
targarea = float(maxrec['target'])
elif checkcond == 'width':
targwidth = float(maxrec['target'])
elif checkcond == 'height':
targheight = float(maxrec['target'])
maxrec = param['max']
unit = param['unit']
if cond == 'area':
if areaval > 0:
maxrec['value'] = str(areaval)
if areaval > targarea:
score = 'fail'
maxrec['score'] = 'fail'
else:
maxrec['score'] = 'pass'
elif cond == 'width':
if areaval > 0:
maxrec['value'] = str(widthval)
if widthval > targwidth:
score = 'fail'
maxrec['score'] = 'fail'
else:
maxrec['score'] = 'pass'
elif cond == 'height':
if areaval > 0:
maxrec['value'] = str(heightval)
if heightval > targheight:
score = 'fail'
maxrec['score'] = 'fail'
else:
maxrec['score'] = 'pass'
elif cond == 'DRC_errors':
ipname = dsheet['ip-name']
if layout_path and not netlist_source == 'schematic':
if os.path.exists(layout_path + '/' + ipname + '.mag'):
# Find the layout directory and check if there is a layout
# for the cell there.
areaproc = subprocess.Popen(['magic',
'-dnull', '-noconsole', layout_path + '/' + ipname + '.mag'],
stdin = subprocess.PIPE, stdout = subprocess.PIPE,
cwd = layout_path, universal_newlines = True)
areaproc.stdin.write("drc on\n")
areaproc.stdin.write("select top cell\n")
areaproc.stdin.write("drc check\n")
areaproc.stdin.write("drc catchup\n")
areaproc.stdin.write("set dcount [drc list count total]\n")
areaproc.stdin.write("puts stdout \"drc = $dcount\"\n")
outlines = areaproc.communicate()[0]
magrex = re.compile('drc[ \t]+=[ \t]+([0-9.]+)[ \t]*$')
for line in outlines.splitlines():
# Diagnostic
print(line)
lmatch = magrex.match(line)
if lmatch:
drccount = int(lmatch.group(1))
maxrec = param['max']
maxrec['value'] = str(drccount)
if drccount > 0:
maxrec['score'] = 'fail'
else:
maxrec['score'] = 'pass'
# Check on LVS from comp.out file (must be more recent than both netlists)
elif cond == 'LVS_errors':
ipname = dsheet['ip-name']
foundry = dsheet['foundry']
node = dsheet['node']
# Hack---node XH035 should have been specified as EFXH035A; allow
# the original one for backwards compatibility.
if node == 'XH035':
node = 'EFXH035A'
# To do even a precheck, the layout path must exist and must be populated
# with the .magicrc file.
if not os.path.exists(layout_path):
os.makedirs(layout_path)
if not os.path.exists(layout_path + '/.magicrc'):
pdkdir = PDK_ROOT + node + '/libs.tech/magic'
if os.path.exists(pdkdir + '/' + node + '.magicrc'):
shutil.copy(pdkdir + '/' + node + '.magicrc', layout_path + '/.magicrc')
# Netlists should have been generated by cace_gensim.py
has_layout_nl = os.path.exists(netlist_path + '/lvs/' + ipname + '.spice')
has_schem_nl = os.path.exists(netlist_path + '/' + ipname + '.spice')
has_vlog_nl = os.path.exists(root_path + '/verilog/' + ipname + '.v')
has_stub_nl = os.path.exists(netlist_path + '/stub/' + ipname + '.spice')
if has_layout_nl and has_stub_nl and not netlist_source == 'schematic':
failures = run_and_analyze_lvs(dsheet)
elif has_layout_nl and has_vlog_nl and not netlist_source == 'schematic':
failures = run_and_analyze_lvs(dsheet)
elif netlist_path and has_schem_nl:
if not has_layout_nl or not has_stub_nl:
if not has_layout_nl:
print("Did not find layout LVS netlist " + netlist_path + '/lvs/' + ipname + '.spice')
if not has_stub_nl:
print("Did not find schematic LVS netlist " + netlist_path + '/' + ipname + '.spice')
print("Running layout device pre-check.")
if localmode == True:
if keepmode == True:
precheck_opts = ['-log', '-debug']
else:
precheck_opts = ['-log']
print('layout_precheck.py ' + netlist_path + '/' + ipname + '.spice ' + node.lower() + ' ' + ' '.join(precheck_opts))
chkproc = subprocess.Popen(['layout_precheck.py',
netlist_path + '/' + ipname + '.spice', node.lower(), *precheck_opts],
stdout=subprocess.PIPE,
cwd = layout_path, universal_newlines = True)
else:
chkproc = subprocess.Popen(['layout_precheck.py',
netlist_path + '/' + ipname + '.spice', node.lower()],
stdout=subprocess.PIPE,
cwd = layout_path, universal_newlines = True)
outlines = chkproc.communicate()[0]
failline = re.compile('.*=[ \t]*([0-9]+)[ \t]*')
for line in outlines.splitlines():
lmatch = failline.match(line)
if lmatch:
failures = int(lmatch.group(1))
else:
failures = -1
if failures >= 0:
maxrec = param['max']
maxrec['value'] = str(failures)
if failures > int(maxrec['target']):
score = 'fail'
maxrec['score'] = 'fail'
else:
maxrec['score'] = 'pass'
# Pop the 'check' record, which has been replaced by the 'value' record.
param.pop('check')
# Remove 'project-folder' from document if it exists, as this document
# is no longer related to an Open Galaxy account.
if 'project-folder' in datatop:
datatop.pop('project-folder')
# Write the annotated JSON file (NOTE: In the absence of further
# processing on the CACE side, this file is just getting deleted
# right after it's made. But the file appears to be correctly
# pushed back to the marketplace server, so this can be removed.
filem = os.path.splitext(inputfile)
if filem[1]:
outputfile = filem[0] + '_anno' + filem[1]
else:
outputfile = inputfile + '_anno.json'
with open(outputfile, 'w') as ofile:
json.dump(datatop, ofile, indent = 4)
# Create tarball of auxiliary files and send them as well.
# Note that the files themselves are tarballed, not the directory
if has_aux_files:
tar = file_compressor.tar_directory_contents(simfiles_path + '/simulation_files')
if 'ip-name' in dsheet:
tarballname = dsheet['ip-name'] + '_result_files.tar.gz'
else:
tarballname = 'result_files.tar.gz'
# In addition to dumping the file locally, also send back to the
# marketplace, along with the tarball of simulation-generated files.
if postmode == True:
send_doc(datatop)
if has_aux_files:
send_file(hashname, tar, tarballname)
else:
print('Posting to marketplace was disabled by -nopost\n')
# Clean up by removing simulation directory
if keepmode == False:
if localmode == True:
print('Simulation results retained per -local option\n')
# If cace_gensim and cace_launch are run locally, keep the results
# since they won't be posted, but remove all other generated files.
# os.chdir(simfiles_path)
if os.path.exists('datasheet.json'):
os.remove('datasheet.json')
for filename in filessimmed:
os.remove(filename)
# Remove any generated ASCII data files
dfile = os.path.splitext(filename)[0] + '.data'
if os.path.exists(dfile):
os.remove(dfile)
# Stupid ngspice handling of wrdata command. . .
dfile = os.path.splitext(filename)[0] + '.data.data'
if os.path.exists(dfile):
os.remove(dfile)
# Remove any generated raw files
dfile = os.path.splitext(filename)[0] + '.raw'
if os.path.exists(dfile):
os.remove(dfile)
# Remove any cosim verilog files
verilog = os.path.splitext(filename)[0] + '.tv'
if os.path.exists(verilog):
os.remove(verilog)
else:
# Remove the entire simulation directory. To avoid horrible
# consequences of, e.g., "-rootdir /" insist that the last path
# component of root_path must be the hashname.
test = os.path.split(root_path)[0]
if test != simulation_path:
print('Error: Root path is not in the system simulation path. Not deleting.')
print('Root path is ' + root_path + '; simulation path is ' + simulation_path)
else:
subprocess.run(['rm', '-rf', root_path])
else:
print('Simulation directory retained per -keep option\n')
sys.exit(0)