Source code for spice.spectre.spectre_testbench

"""
=================
Spectre Testbench
=================

Simulators sepecific testbench generation class for Spectre.

"""
import os
import sys
import subprocess
import shlex
import fileinput
import re

from thesdk import *
from spice.testbench_common import testbench_common
import pdb

import numpy as np
import pandas as pd
from functools import reduce
import textwrap
from datetime import datetime

[docs] class spectre_testbench(testbench_common): def __init__(self, parent=None, **kwargs): ''' Executes init of testbench_common, thus having the same attributes and parameters. Parameters ---------- **kwargs : See module testbench_common ''' super().__init__(parent=parent,**kwargs) # Generating spice options string @property def options(self): """String Spice options string parsed from self.spiceoptions -dictionary in the parent entity. """ if not hasattr(self,'_options'): self._options = "%s Options\n" % self.parent.spice_simulator.commentchar if self.parent.postlayout and 'savefilter' not in self.parent.spiceoptions: self.print_log(type='I', msg='Consider using option savefilter=rc for post-layout netlists to reduce output file size!') if self.parent.postlayout and 'save' not in self.parent.spiceoptions: self.print_log(type='I', msg='Consider using option save=none and specifiying saves with plotlist for post-layout netlists to reduce output file size!') i=0 for optname,optval in self.parent.spiceoptions.items(): self._options += "Option%d " % i # spectre options need unique names i+=1 if optval != "": self._options += self.parent.spice_simulator.option + ' ' + optname + "=" + optval + "\n" else: self._options += ".option " + optname + "\n" return self._options @options.setter def options(self,value): self._options=value @property def libcmd(self): """str : Library inclusion string. Parsed from self.spicecorner -dictionary in the parent entity, as well as 'ELDOLIBFILE' or 'SPECTRELIBFILE' global variables in TheSDK.config. """ if not hasattr(self,'_libcmd'): libfile = "" corner = "top_tt" temp = "27" for optname,optval in self.parent.spicecorner.items(): if optname == "temp": temp = optval if optname == "corner": corner = optval try: libfile = thesdk.GLOBALS['SPECTRELIBFILE'] if libfile == '': raise ValueError else: self._libcmd = "// Spectre device models\n" files = libfile.split(',') if len(files)>1: if isinstance(corner,list) and len(files) == len(corner): for path,corn in zip(files,corner): if not isinstance(corn, list): corn = [corn] for c in corn: self._libcmd += 'include "%s" section=%s\n' % (path,c) else: self.print_log(type='W',msg='Multiple entries in SPECTRELIBFILE but spicecorner wasn\'t a list or contained different number of elements!') self._libcmd += 'include "%s" section=%s\n' % (files[0], corner) else: self._libcmd += 'include "%s" section=%s\n' % (files[0], corner) except: self.print_log(type='W',msg='Global TheSDK variable SPECTRELIBPATH not set.') self._libcmd = "// Spectre device models (undefined)\n" self._libcmd += "//include " + libfile + " " + corner + "\n" self._libcmd += 'tempOption options temp=%s\n' % str(temp) return self._libcmd @libcmd.setter def libcmd(self,value): self._libcmd=value @libcmd.deleter def libcmd(self,value): self._libcmd=None @property def dcsourcestr(self): """str : DC source definitions parsed from spice_dcsource objects instantiated in the parent entity. """ if not hasattr(self,'_dcsourcestr'): self._dcsourcestr = "%s DC sources\n" % self.parent.spice_simulator.commentchar for name, val in self.dcsources.Members.items(): value = val.value supply = '%s%s' % (val.sourcetype.upper(),val.name.upper()) if val.ramp == 0: self._dcsourcestr += "%s %s %s %s%s\n" % \ (supply,self.esc_bus(val.pos),self.esc_bus(val.neg),\ ('%ssource dc=' % val.sourcetype.lower()),value) else: self._dcsourcestr += "%s %s %s %s type=pulse val0=0 val1=%s rise=%g\n" % \ (supply,self.esc_bus(val.pos),self.esc_bus(val.neg),\ ('%ssource' % val.sourcetype.lower()),value,val.ramp) return self._dcsourcestr @property def inputsignals(self): """str : Input signal definitions parsed from spice_iofile objects instantiated in the parent entity. """ if not hasattr(self,'_inputsignals'): self._inputsignals = "%s Input signals\n" % self.parent.spice_simulator.commentchar for name, val in self.iofiles.Members.items(): # Input file becomes a source if val.dir.lower()=='in' or val.dir.lower()=='input': # Event signals are analog if val.iotype.lower()=='event': for i in range(len(val.ionames)): # Finding the max time instant try: maxtime = val.Data[-1,0] except TypeError: self.print_log(type='F', msg='Input data not assinged to IO %s! Terminating.' % name) if float(self._trantime) < float(maxtime): self._trantime_name = name self._trantime = maxtime # Adding the source if val.pos and val.neg: self._inputsignals += "%s%s %s %s %ssource type=pwl file=\"%s\"\n" % \ (val.sourcetype.upper(),self.esc_bus(val.name.lower()), self.esc_bus(val.pos), self.esc_bus(val.neg),val.sourcetype.lower(),val.file[i]) else: self._inputsignals += "%s%s %s 0 %ssource type=pwl file=\"%s\"\n" % \ (val.sourcetype.upper(),self.esc_bus(val.name.lower()), self.esc_bus(val.ionames[i]),val.sourcetype.lower(),val.file[i]) # Sample signals are digital # Presumably these are already converted to bitstrings elif val.iotype.lower()=='sample': for i in range(len(val.ionames)): # This is a lazy way to handle non-list val.Data try: if float(self._trantime) < len(val.Data)/val.rs: self._trantime = len(val.Data)/val.rs self._trantime_name = name except: pass self._inputsignals += 'vec_include "%s"\n' % val.file[i] else: self.print_log(type='F',msg='Input type \'%s\' undefined.' % val.iotype) if self._trantime == 0: self._trantime = "UNDEFINED" self.print_log(type='I',msg='Transient time could not be inferred from input signals. Make sure to provide tstop argument to spice_simcmd.') return self._inputsignals @inputsignals.setter def inputsignals(self,value): self._inputsignals=value @inputsignals.deleter def inputsignals(self,value): self._inputsignals=None @property def simcmdstr(self): """str : Simulation command definition parsed from spice_simcmd object instantiated in the parent entity. """ if not hasattr(self,'_simcmdstr'): self._simcmdstr = "%s Simulation commands\n" % self.parent.spice_simulator.commentchar for sim, val in self.simcmds.Members.items(): if val.mc: self._simcmdstr += 'mc montecarlo donominal=no variations=all %snumruns=1 {\n' \ % ('' if val.mc_seed is None else 'seed=%d '%val.mc_seed) if str(sim).lower() == 'tran': simtime = val.tstop if val.tstop is not None else self._trantime if val.tstop is None: self.print_log(type='D',msg='Inferred transient duration is %g s from \'%s\'.' % (simtime,self._trantime_name)) #TODO initial conditions self._simcmdstr += 'TRAN_analysis %s pstep=%s stop=%s %s ' % \ (sim,str(val.tprint),str(simtime),'UIC' if val.uic else '') if val.noise: if val.seed==0: self.print_log(type='W',msg='Spectre disables noise if seed=0.') self._simcmdstr += 'trannoisemethod=default noisefmin=%s noisefmax=%s %s ' % \ (str(val.fmin),str(val.fmax),'noiseseed=%d'%(val.seed) if val.seed is not None else '') if val.method is not None: self._simcmdstr += 'method=%s ' % (str(val.method)) if val.cmin is not None: self._simcmdstr += 'cmin=%s ' % (str(val.cmin)) if val.maxstep is not None: self._simcmdstr += 'maxstep=%s ' % (str(val.maxstep)) if val.step is not None: self._simcmdstr += 'step=%s ' % (str(val.step)) if val.strobeperiod is not None: self._simcmdstr += 'strobeperiod=%s strobeoutput=strobeonly ' % (str(val.strobeperiod)) if val.strobedelay is not None: self._simcmdstr += 'strobedelay=%s' % (str(val.strobedelay)) if val.skipstart is not None: self._simcmdstr += 'skipstart=%s' % (str(val.skipstart)) self._simcmdstr += '\n\n' elif str(sim).lower() == 'dc': if len(val.sweep) == 0: # This is not a sweep analysis self._simcmdstr+='oppoint dc\n\n' else: if self.parent.distributed_run: distributestr = 'distribute=lsf numprocesses=%d' % self.parent.num_processes else: distributestr = '' if len(val.subcktname) != 0: # Sweep subckt parameter length=len(val.subcktname) if any(len(lst) != length for lst in [val.sweep, val.swpstart, val.swpstop, val.swpstep]): self.print_log(type='F', msg='Mismatch in length of simulation parameters.\nEnsure that sweep points and subcircuit names have the same number of elements!') for i in range(len(val.subcktname)): self._simcmdstr+='Sweep%d sweep param=%s sub=%s start=%s stop=%s step=%s %s { \n' \ % (i, val.sweep[i], val.subcktname[i], val.swpstart[i], val.swpstop[i], val.swpstep[i], distributestr) elif len(val.devname) != 0: # Sweep device parameter length=len(val.devname) if any(len(lst) != length for lst in [val.sweep, val.swpstart, val.swpstop, val.swpstep]): self.print_log(type='F', msg='Mismatch in length of simulation parameters.\nEnsure that sweep points and device names have the same number of elements!') for i in range(len(val.devname)): self._simcmdstr+='Sweep%d sweep param=%s dev=%s start=%s stop=%s step=%s %s { \n' \ % (i, val.sweep[i], val.devname[i], val.swpstart[i], val.swpstop[i], val.swpstep[i], distributestr) else: # Sweep top-level netlist parameter length=len(val.sweep) if any(len(lst) != length for lst in [val.swpstart, val.swpstop, val.swpstep]): self.print_log(type='F', msg='Mismatch in length of simulation parameters.\nEnsure that sweep points and parameter names have the same number of elements!') for i in range(len(val.sweep)): self._simcmdstr+='Sweep%d sweep param=%s start=%s stop=%s step=%s %s { \n' \ % (i, val.sweep[i], val.swpstart[i], val.swpstop[i], val.swpstep[i], distributestr) self._simcmdstr+='oppoint dc\n' # Closing brackets for j in range(i, -1, -1): self._simcmdstr+='}\n' self._simcmdstr+='\n' elif str(sim).lower() == 'ac': if val.fscale.lower()=='log': if val.fpoints != 0: pts_str='log=%d' % val.fpoints elif val.fstepsize != 0: pts_str='dec=%d' % val.fstepsize else: self.print_log(type='F', msg='Set either fpoints or fstepsize for AC simulation!') elif val.fscale.lower()=='lin': if val.fpoints != 0: pts_str='lin=%d' % val.fpoints elif val.fstepsize != 0: pts_str='step=%d' % val.fstepsize else: self.print_log(type='F', msg='Set either fpoints or fstepsize for AC simulation!') else: self.print_log(type='F', msg='Unsupported frequency scale %s for AC simulation!' % val.fscale) self._simcmdstr += 'AC_analysis %s start=%s stop=%s %s' % \ (sim,str(val.fmin),str(val.fmax),pts_str) self._simcmdstr += '\n\n' else: self.print_log(type='E',msg='Simulation type \'%s\' not yet implemented.' % str(sim)) if val.mc: self._simcmdstr += '}\n\n' if val.model_info: self._simcmdstr += 'element info what=inst where=rawfile \nmodelParameter info what=models where=rawfile\n\n' return self._simcmdstr @simcmdstr.setter def simcmdstr(self,value): self._simcmdstr=value @simcmdstr.deleter def simcmdstr(self,value): self._simcmdstr=None @property def plotcmd(self): """str : All output IOs are mapped to plot or print statements in the testbench. Also manual plot commands through `spice_simcmd.plotlist` are handled here. """ if not hasattr(self,'_plotcmd'): self._plotcmd = "" for name, val in self.simcmds.Members.items(): # Manual probes if len(val.plotlist) > 0 and name.lower() != 'dc': self._plotcmd = "%s Manually probed signals\n" % self.parent.spice_simulator.commentchar self._plotcmd += 'save ' for i in val.plotlist: self._plotcmd += self.esc_bus(i) + " " self._plotcmd += "\n\n" #DC probes if len(val.plotlist) > 0 and name.lower() == 'dc': self._plotcmd = "%s DC operating points to be captured:\n" % self.parent.spice_simulator.commentchar self._plotcmd += 'save ' for i in val.plotlist: self._plotcmd += self.esc_bus(i, esc_colon=False) + " " if val.excludelist != []: self._plotcmd += 'exclude=[ ' for i in val.excludelist: self._plotcmd += i + ' ' self._plotcmd += ']' self._plotcmd += "\n\n" if name.lower() == 'tran' or name.lower() == 'ac' : self._plotcmd += "%s Output signals\n" % self.parent.spice_simulator.commentchar # Parsing output iofiles savestr='' plotstr='' first=True for name, val in self.iofiles.Members.items(): # Output iofile becomes a plot/print command if val.dir.lower()=='out' or val.dir.lower()=='output': if val.iotype=='event': for i in range(len(val.ionames)): signame = self.esc_bus(val.ionames[i]) if first: savestr += 'save %s' % signame if val.datatype.lower() == 'complex': plotstr += '.print %sr(%s) %si(%s)' % \ (val.sourcetype, val.ionames[i], val.sourcetype, val.ionames[i]) else: plotstr += '.print %s(%s)' % (val.sourcetype, val.ionames[i]) first=False else: if val.datatype.lower() == 'complex': if f'{val.sourcetype}({val.ionames[i]})' not in plotstr.split(' '): savestr += ' %s' % signame plotstr += ' %sr(%s) %si(%s)' % \ (val.sourcetype, val.ionames[i], val.sourcetype, val.ionames[i]) else: if f'{val.sourcetype}({val.ionames[i]})' not in plotstr.split(' '): savestr += ' %s' % signame plotstr += ' %s(%s)' % (val.sourcetype, val.ionames[i]) elif val.iotype=='sample': for i in range(len(val.ionames)): # Checking the given trigger(s) if isinstance(val.trigger,list): if len(val.trigger) == len(val.ionames): trig = val.trigger[i] else: trig = val.trigger[0] self.print_log(type='W', msg='%d triggers given for %d ionames. Using the first trigger for all ionames.' % (len(val.trigger),len(val.ionames))) else: trig = val.trigger # Extracting the bus width signame = val.ionames[i] busstart,busstop,buswidth,busrange = self.parent.get_buswidth(signame) signame = signame.replace('<',' ').replace('>',' ').replace('[',' ').replace(']',' ').replace(':',' ').split(' ') # If not already, add the respective trigger signal voltage to iofile_eventdict if trig not in self.parent.iofile_eventdict: self.parent.iofile_eventdict[trig] = None if first: savestr += 'save %s' % self.esc_bus(trig) plotstr += '.print v(%s)' % (trig) first=False else: savestr += ' %s' % self.esc_bus(trig) plotstr += ' v(%s)' % (trig) for j in busrange: if buswidth == 1 and '<' not in val.ionames[i]: bitname = signame[0] else: bitname = '%s<%d>' % (signame[0],j) # If not already, add the bit voltage to iofile_eventdict if bitname not in self.parent.iofile_eventdict: self.parent.iofile_eventdict[bitname] = None if first: savestr += 'save %s' % self.esc_bus(bitname) plotstr += '.print %s(%s)' % (val.sourcetype, bitname) first=False else: savestr += ' %s' % self.esc_bus(bitname) plotstr += ' %s(%s)' % (val.sourcetype, bitname) elif val.iotype=='time': # For time IOs, the node voltage is saved as # event and the time information is later # parsed in Python for i in range(len(val.ionames)): signame = self.esc_bus(val.ionames[i]) # Check if this same node was already saved as event type if val.ionames[i] not in self.parent.iofile_eventdict: # Requested node was not saved as event # -> add to eventdict + save to output database self.parent.iofile_eventdict[val.ionames[i]] = None if first: savestr += 'save %s' % signame plotstr += '.print %s(%s)' % (val.sourcetype, val.ionames[i]) first=False else: savestr += ' %s' % signame plotstr += ' %s(%s)' % (val.sourcetype, val.ionames[i]) elif val.iotype=='vsample': self.print_log(type='O',msg='IO type \'vsample\' is obsolete. Please use type \'sample\' and set ioformat=\'volt\'.') self.print_log(type='F',msg='Please do it now :)') else: self.print_log(type='W',msg='Output filetype incorrectly defined.') # Parsing supply currents here as well (I think ngspice # plots need to be grouped like this) for name, val in self.dcsources.Members.items(): if val.extract: supply = '%s%s' % (val.sourcetype.upper(),val.name.upper()) if supply not in self.parent.iofile_eventdict: self.parent.iofile_eventdict[supply] = None if first: savestr += 'save %s:pwr %s:p' % (supply,supply) plotstr += '.print I(%s)' % (supply) first=False else: savestr += ' %s:pwr %s:p' % (supply,supply) plotstr += ' I(%s)' % (supply) # Output accumulated save and print statement to plotcmd savestr += '\n' plotstr += '\n' self._plotcmd += savestr self._plotcmd += 'simulator lang=spice\n' self._plotcmd += '.option ingold 2\n' # Format the output to same "table", 15 bits per column self._plotcmd += '.option co=%d\n' % (self.num_cols) self._plotcmd += plotstr self._plotcmd += 'simulator lang=spectre\n' return self._plotcmd @plotcmd.setter def plotcmd(self,value): self._plotcmd=value @plotcmd.deleter def plotcmd(self,value): self._plotcmd=None @property def num_cols(self): ''' Number of columns in the output file, when using Spectre. Each signal takes 1 column (unless it is complex, then two). Each column is 15 bit wide, hence number of columns is multiplied by 15. ''' if not hasattr(self, '_num_cols'): self._num_cols=0 # If power is extracted, it adds current line for name, val in self.dcsources.Members.items(): if val.extract: self._num_cols += 1 for name, val in self.iofiles.Members.items(): if val.dir.lower() == 'out': for io_name in val.ionames: num_addition=2 if val.datatype.lower()=='complex' else 1 pattern=re.compile('<[0-9]+:[0-9]+>') if pattern.search(io_name): start=io_name.split('<')[1] start=start.split(':') lower=start[0] higher=start[1].split('>')[0] add=abs(int(higher)-int(lower))+1 self._num_cols += num_addition*add else: self._num_cols += num_addition self._num_cols *= 15 return self._num_cols @num_cols.setter def num_cols(self, val): self._num_cols=val