Source code for pyopus.simulator.hspice

"""
.. inheritance-diagram:: pyopus.simulator.hspice
    :parts: 1

**HSPICE interface (PyOPUS subsystem name: HSSI)**

HSPICE is a batch mode simulator. It is capable of changing the circuit's 
topology and its parameters between consecutive simulations without the need to 
restart the simulator with a new input file. HSPICE is completely input-file 
driven and presents no command prompt to the user. 

Save directives do not apply to the AC analysis because the HSPICE ``.probe`` 
simulator directive works only for real values. 

The ``temperature`` parameter has special meaning and represents the circuit's 
temperature in degrees centigrade. 

Analysis command generators return a tuple with teh following members: 
 
0. analysis type (``'dc'``, ``'ac'``, ``'tran'``, ``'noise'``)
1. analysis results file ending (``'sw'``, ``'ac'``, ``'tr'``)
2. analysis directive text

A job sequence in HSPICE is internally list of lists where the inner lists 
contain the indices of jobs belonging to one job group. The user is, however, 
presented with only one job group containing all job indices ordered in the 
manner the jobs will later be simulated (flat job sequence). 

Job sequence optimization minimizes the number of topology changes between 
consecutive jobs. Internally this is represented in the job sequence by job 
groups where all jobs in a group share the same circuit topology. 

One result group can consist of multiple plots resulting from multiple 
invocations of the same analysis (resulting from a parametric sweep). 
See :mod:`pyopus.simulator.hspicefile` module for the details on the result 
files. 

Repeated analyses with a parameter sweep and the collection of ``.measure`` 
directive results are currently not supported. 
"""

# HSPICE simulator interface

# Benchmark result on opamp, HSPICE, Windows XP 32bit
#	131 iterations, best in 113, final cost -0.0692397737822
#	53.236s/57.737s = 92.2% time spent in simulator

# Benchmark result on opamp, HSPICE, Linux AMD64 2CPU
#	131 iterations, best in 113, final cost -0.0692719531626
#	16.046s/18.014s = 89.1% time spent in simulator

# Benchmark result on opamp, HSPICE, Linux AMD64 1CPU
#	131 iterations, best in 113, final cost -0.0692719531626
#	16.046s/18.014s = 89.1% time spent in simulator

from ..misc.debug import DbgMsgOut, DbgMsg
import subprocess
from .base import Simulator, SimulationResults
from .hspicefile import hspice_read
import os
import platform
import shutil
from sys import exc_info
from traceback import format_exception, format_exception_only
from ..misc.env import environ
from .. import PyOpusError
from pprint import pprint


__all__ = [ 'ipath', 'save_all', 'save_voltage', 'save_current', 'save_property', 
			'an_op', 'an_dc', 'an_ac', 'an_tran', 'an_noise', 'HSpice',
			'HSpiceSimulationResults' ] 


simulatorDescription=("HSpice", "Synopsys HSPICE")
"""
For detecting simulators. 
"""

#
# Hierarchical path handling 
#

[docs]def ipath(input, outerHierarchy=None, innerHierarchy=None, objectType='inst'): """ Constructs a hierarchical path for the instance with name given by *input*. The object is located within *outerHierarchy* (a list of instances with innermost instance listed first). *innerHierarchy* a list of names specifying the instance hierarchy inner to the *input* instance. The innermost instance name is listed first. If *outerHierarchy* is not given *input* is assumed to be the outermost element in the hierarchy. Similarly if *innerHierarchy* is not given *input* is assumed to be the innermost element in teh hierarchy. Returns a string representing a hierarchical path. If *input* is a list the return value is also a list representing hierarchical paths corresponding to elements in *input*. *innerHierarchy* and *outerHierarchy* can also be ordinary strings (equivalent to a list with only one string as a member). The *objectType* argument is for compatibility with other simulators. Because HSPICE treats the hierarchical paths of all objects in the same way, the return value does not depend on *objectType*. The available values of *objectType* are ``'inst'``, ``'mod'``, and ``'node'``. HSPICE hierarchical paths begin with the outermost instance followed by its subinstances. A dot (``.``) is used as the separator between instances in the hierarchy. So ``x2.x1.m1`` is an instance named ``m1`` that is a part of ``x1`` (inside ``x1``) which in turn is a part of ``x2`` (inside ``x2``). Some examples: * ``ipath('m1', ['x1', 'x2'])`` - instance named ``m1`` inside ``x1`` inside ``x2``. Returns ``'x2.x1.m1'``. * ``ipath('x1', innerHierarchy=['m0', 'x0'])`` - instance ``m0`` inside ``x0`` inside ``x1``. Returns ``'x1.x0.m0'``. * ``ipath(['m1', 'm2'], ['x1', 'x2']) - instances ``m1`` and ``m2`` inside ``x1`` inside ``x2``. Returns ``['x2.x1.m1', 'x2.x1.m2']``. * ``ipath(['xm1', 'xm2'], ['x1', 'x2'], 'm0')`` - instances named ``m0`` inside paths ``x2.x1.xm1`` and ``x2.x1.xm2``. Returns ``['x2.x1.xm1.m0', 'x2.x1.xm2.m0']``. """ # Create outer and inner path # Outer hierarchy is represented by a prefix if outerHierarchy is None: suffStr='' else: if type(outerHierarchy ) is str: prefStr=outerHierarchy+'.' else: # Reverse outer hierarchy prefStr='.'.join(outerHierarchy[::-1])+'.' # Inner hierarchy is represented by a suffix if innerHierarchy is None: suffStr='' else: if type(innerHierarchy) is str: suffStr='.'+innerHierarchy else: # Reverse inner hierarchy suffStr='.'+'.'.join(innerHierarchy[::-1]) # Build results if type(input) is not list: return prefStr+input+suffStr else: result=[] for inst in input: result.append(prefStr+inst+suffStr) return result
# # Save directive generators #
[docs]def save_all(): """ Returns a save directive that saves all results the simulator normally saves in its output (in HSPICE these are all node voltages and all currents flowing through voltage sources and inductances). """ return [ 'all' ]
[docs]def save_voltage(what): """ If *what* is a string it returns a save directive that instructs the simulator to save the voltage of node named *what* in simulator output. If *what* is a list of strings a multiple save directives are returned instructing the simulator to save the voltages of nodes with names given by the *what* list. """ compiledList=[] if type(what) is list: input=what else: input=[what] for name in input: compiledList.append('v('+name+')') return compiledList
[docs]def save_current(what): """ If *what si a string it returns a save directive that instructs the simulator to save the current flowing through instance names *what* in simulator output. If *what* is a list of strings multiple save diretives are returned instructing the simulator to save the currents flowing through instances with names given by the *what* list. """ compiledList=[] if type(what) is list: input=what else: input=[what] for name in input: compiledList.append('i('+name+')') return compiledList
[docs]def save_property(devices, params, indices=None): """ Saves the properties given by the list of property names in *params* of instances with names given by the *devices* list. Also capable of handling properties that are vectors (although currently SPICE OPUS devices have no such properties). The indices of vector components that need to be saved is given by the *indices* list. If *params*, *devices*, and *indices* have n, m, and o memebrs, n*m*o save directives are are returned describing all combinations of device name, property name, and index. If *indices* is not given, save directives for scalar device properties are returned. Currently HSPICE devices have no vector properties. """ compiledList=[] if type(devices) is list: inputDevices=devices else: inputDevices=[devices] if type(params) is list: inputParams=params else: inputParams=[params] if indices is None: for name in inputDevices: for param in inputParams: compiledList.append(name+':'+param) else: raise PyOpusError("HSPICE does not support properties with indices.") return compiledList
# # Analysis command generators #
[docs]def an_op(): """ Generates the HSPICE simulator directive that invokes the operating point analysis. This is achieved with a trick - performing a DC sweep of a parameter named ``dummy__`` across only one point. The ``dummy__`` parameter is added to the simulator input file automatically. """ # Sweep a dummy parameter dummy___, one point where dummy___=0 return ('dc', 'sw', '.dc dummy___ poi 1 0')
[docs]def an_dc(start, stop, sweep, points, name, parameter, index=None): """ Generates the HSPICE simulator directive that invokes the operating point sweep (DC) analysis. *start* and *stop* give the intial and the final value of the swept parameter. *sweep* can be one of * ``'lin'`` - linear sweep with the number of points given by *points* * ``'dec'`` - logarithmic sweep with points per decade (scale range of 1..10) given by *points* * ``'oct'`` - logarithmic sweep with points per octave (scale range of 1..2) given by *points* *name* gives the name of the instance whose *parameter* is swept. Because HSPICE can sweep only independent voltage and current sources, these two element types are the only ones allowed. Due to this the only allowed value for parameter is ``dc``. Because HSPICE knows no such thing as vector parameters, *index* should never be used. If *name* is not given a sweep of a circuit parameter (defined with ``.param``) is performed. The name of the parameter can be specified with the *parameter* argument. If *parameter* is ``temperature`` a sweep of the circuit's temperature is performed. """ if index is None: if name is None: if parameter=='temperature': devStr='temp' else: devstr=str(parameter) else: if name[0].lower()!='v' and name[0].lower()!='i': raise PyOpusError("HSPICE can't sweep elements other than independent sources") if parameter is not None and parameter.lower()!='dc': raise PyOpusError("HSPICE can sweep only the dc parameter of independent sources") devStr=str(name) else: raise PyOpusError("HSPICE does not support vector parameter sweep") if sweep == 'lin': anstr='.dc '+devStr+' lin '+str(points)+' '+str(start)+' '+str(stop) elif sweep == 'dec': anstr='.dc '+devStr+' dec '+str(points)+' '+str(start)+' '+str(stop) elif sweep == 'oct': anstr='.dc '+devStr+' oct '+str(points)+' '+str(start)+' '+str(stop) else: raise PyOpusError("Bad sweep type.") return ('dc', 'sw', anstr)
[docs]def an_ac(start, stop, sweep, points): """ Generats the HSPICE simulator directive that invokes the small signal (AC) analysis. The range of the frequency sweep is given by *start* and *stop*. *sweep* is one of * ``'lin'`` - linear sweep with the number of points given by *points* * ``'dec'`` - logarithmic sweep with points per decade (scale range of 1..10) given by *points* * ``'oct'`` - logarithmic sweep with points per octave (scale range of 1..2) given by *points* """ if sweep == 'lin': anstr='.ac lin '+str(points)+' '+str(start)+' '+str(stop) elif sweep == 'dec': anstr='.ac dec '+str(points)+' '+str(start)+' '+str(stop) elif sweep == 'oct': anstr='.ac oct '+str(points)+' '+str(start)+' '+str(stop) else: raise PyOpusError("Bad sweep type.") return ('ac', 'ac', anstr)
[docs]def an_tran(step, stop, start=0.0, maxStep=None, uic=False): """ Generats the HSPICE simulator directive that invokes the transient analysis. The range of the time sweep is given by *start* and *stop*. *step* is the intiial time step. HSPICE does not support an upper limit on the time step. Therefore the *maxStep* argument is ignored. If the *uic* flag is set to ``True`` the initial conditions given by ``.ic`` simulator directives are used as the first point of the transient analysis instead of the operating point analysis results. """ if uic: anstr='.tran '+str(step)+' '+str(stop)+' start='+str(start)+' uic' else: anstr='.tran '+str(step)+' '+str(stop)+' start='+str(start) return ('tran', 'tr', anstr)
# Pts per summary is supported in HSPICE, but the results go in the text file. We don't want # to process them anyway so the results are noise spectra only and we ignore ptsSum. # A noise analysis is bundled with an ac analysis.
[docs]def an_noise(start, stop, sweep, points, input, outp, outn=None, ptsSum=1): """ Generats the HSPICE simulator directive that invokes the small signal noise analysis. The range of the frequency sweep is given by *start* and *stop*. *sweep* is one of * ``'lin'`` - linear sweep with the number of points given by *points* * ``'dec'`` - logarithmic sweep with points per decade (scale range of 1..10) given by *points* * ``'oct'`` - logarithmic sweep with points per octave (scale range of 1..2) given by *points* *input* is the name of the independent voltage/current source with ``ac`` parameter set to 1 that is used for calculating the input referred noise. *outp* and *outn* give the voltage that is used as the output voltage. If only *outp* is given the output voltage is the voltage at node *outp*. If *outn* is also given, the output voltage is the voltage between nodes *outp* and *outn*. *ptsSum* is supported by HSPICE but the results go to a text file and are not collected after the analysis. If it wasn't ignored we would specify it as the third argument to ``.noise``. A HSPICE noise analysis is an addition to the AC analysis. """ if outn is None: outspec="v("+str(outp)+")" else: outspec="v("+str(outp)+","+str(outn)+")" if sweep=='lin': anstr='.ac '+' lin '+str(points)+" "+str(start)+" "+str(stop) elif sweep=='dec': anstr='.ac '+' dec '+str(points)+" "+str(start)+" "+str(stop) elif sweep=='oct': anstr='.ac '+' oct '+str(points)+" "+str(start)+" "+str(stop) else: raise PyOpusError("Bad sweep type.") return ('noise', 'ac', anstr+'\n.noise '+outspec+' '+input)
[docs]class HSpice(Simulator): """ A class for interfacing with the HSPICE batch mode simulator. *binary* is the path to the HSPICE simulator binary. If it is not given the ``HSPICE_BINARY`` environmental variable is used as the path to the HSPICE simulator binary. If ``HSPICE_BINARY`` is not defined the binary is assumed to be in the current working directory. *args* apecifies a list of additional arguments passed to the simulator binary at startup. If *debug* is greater than 0 debug messages are printed at the standard output. If it is above 1 a part of the simulator output (.lis file) is also printed. If *debug* is above 2 full simulator output is printed. The save directives from the simulator job description are evaluated in an environment where the following objects are available: * ``all`` - a reference to the :func:`save_all` function * ``v`` - a reference to the :func:`save_voltage` function * ``i`` - a reference to the :func:`save_current` function * ``p`` - a reference to the :func:`save_property` function * ``ipath`` - a reference to the :func:`ipath` function Similarly the environment for evaluating the analysis command given in the job description consists of the following objects: * ``op`` - a reference to the :func:`an_op` function * ``dc`` - a reference to the :func:`an_dc` function * ``ac`` - a reference to the :func:`an_ac` function * ``tran`` - a reference to the :func:`an_tran` function * ``noise`` - a reference to the :func:`an_noise` function * ``ipath`` - a reference to the :func:`ipath` function * ``param`` - a dictionary containing the members of the ``params`` entry in the simulator job description together with the parameters from the dictionary passed at the last call to the :meth:`setInputParameters` method. The parameters values given in the job description take precedence over the values passed to the :meth:`setInputParameters` method. """ def __init__(self, binary=None, args=[], debug=0): Simulator.__init__(self, binary, args, debug) self._compile()
[docs] @classmethod def findSimulator(cls): """ Finds the simulator. Location is defined by the HSPICE_BINARY environmental variable. If the binary is not found there the system path is used. """ if 'HSPICE_BINARY' in environ: hspicebinary=environ['HSPICE_BINARY'] else: if platform.system()=='Windows': hspicebinary=shutil.which("hspice.exe") else: hspicebinary=shutil.which("hspice") # Verify binary if hspicebinary is None: return None elif os.path.isfile(hspicebinary): return hspicebinary else: return None
def _compile(self): """ Prepares internal structures. * dictionaries of functions for evaluating save directives and analysis commands * constructs the binary name for invoking the simulator """ # Local namespace for save directive evaluation self.saveLocals={ 'all': save_all, 'v': save_voltage, 'i': save_current, 'p': save_property, 'ipath': ipath, } # Local namespace for analysis evaluation self.analysisLocals={ 'op': an_op, 'dc': an_dc, 'ac': an_ac, 'tran': an_tran, 'noise': an_noise, 'ipath': ipath, 'param': {}, } # Default binary based on HSPICE_BINARY and platform if self.binary is None: self.binary=HSpice.findSimulator() # For pickling - copy object's dictionary and remove members # with references to member functions so that the object can be pickled. def __getstate__(self): state=self.__dict__.copy() del state['saveLocals'] del state['analysisLocals'] # Force simulator ID update on unpickle state['simulatorID']=None return state # For unpickling - update object's dictionary and rebuild members with references # to member functions. Also rebuild simulator binary name. def __setstate__(self, state): self.__dict__.update(state) # Generate simulator ID if we don't have one if self.simulatorID is None: self.generateSimulatorID() self._compile() def _createSaves(self, saveDirectives, variables): """ Creates a list of save directives by evaluating the members of the *saveDirectives* list. *variables* is a dictionary of extra variables that are available during directive evaluation. In case of a name conflict the variables from *saveDirectives* take precedence. """ # Prepare evaluation environment evalEnv={} evalEnv.update(variables) evalEnv.update(self.saveLocals) compiledList=[] for saveDirective in saveDirectives: # A directive must be a string that evaluates to a list of strings saveList=eval(saveDirective, globals(), evalEnv) if type(saveList) is not list: raise PyOpusError("Save directives must evaluate to a list of strings.") for save in saveList: if type(save) is not str: raise PyOpusError("Save directives must evaluate to a list of strings.") compiledList+=saveList # Make list memebers unique compiledSet=set(compiledList) # Find 'all' in set, make it the first element of the unique saves list if 'all' in compiledSet: compiledSet.remove('all') haveAll=True return ['all']+list(compiledSet) else: return list(compiledSet) # # Batch simulation #
[docs] def writeFile(self, i): """ Prepares the simulator input file for running the *i*-th job group. Because there is only one job group in HSPICE 0 is the only allowed value of *i*. Generates files * ``simulatorID_analysis.lib`` - lists all analyses, one library section per analysis * ``simulatorID.sp`` - the main simulator input file These files must be generated every time new input parameter values are set with the :meth:`setInputParaneters` method. The ``simulatorID_analysis.lib`` file is a library file with sections named ``anFileEndingIndex`` where ``FileEnding`` is the one returned by analysis command generators and ``Index`` is the consecutive index of the analysis of that type. Every section has in ``simulatorID_analysis.lib`` consists of * Simulator options (``.options`` simulator directive). * The value of the ``temperature`` parameter in form of a ``.temp`` simulator directive. * ``.options post=1`` which forces writing the results in binary output files. * The valus of parameters in the form of ``.param`` dirrectives. The parameters specified in the corresponding job description take precedence over input parameter values. * Save directives (``.probe`` simulator directive). If at least one save directive is specified, the ``all()`` directive is not used, and the analysis is not an AC analysis the ``.options probe=1`` directive is added. This instructs the simulator to save only those results that are specified with save directives. The ``simulatorID.sp`` file invokes individual jobs. The first job starts with a ``.title`` simulator directive giving the job name as the title. All other jobs start with an ``.alter`` directive giving their corresponding job names. Every ``.title``/``.alter`` directive is followed by ``.del lib`` and ``.lib`` directives that include a section of the topology file and the section of the the ``simulatorID_analysis.lib`` file that correspond to the job. All output files with simulation results are files with endings comprising * the file ending returned by the analysis command generator and * the consecutive index of the analysis. All output files are in HSPICE binary file format. The function returns the name of the main simulator input file. """ # Because the user is always presented with only one job group, # raise PyOpusError if i is not 0. if i!=0: raise PyOpusError(DbgMsg("HSSI", "Bad job group index (not 0).")) # Write analysis file analysisFileName=self.simulatorID+'_analysis.lib' if self.debug>0: DbgMsgOut("HSSI", "Writing analyses to '"+analysisFileName+"'.") with open(analysisFileName, 'w') as f: f.write('* HSPICE analysis library\n\n') # Prepare file ending counter anCodeCount={} # Prepare file ending list for jobs. self.jobIndex2fileEnding=[""]*len(self.jobList) self.jobIndex2paFileEnding=[""]*len(self.jobList) # Traverse all jobs in flat job sequence. paSeq=0 for jobIndex in self.flatJobSequence: # Get job. job=self.jobList[jobIndex] if self.debug>0: DbgMsgOut("HSSI", " '"+str(job['name'])+"'") # Prepare evaluation environment for analysis command evalEnv={} evalEnv.update(job['variables']) evalEnv.update(self.analysisLocals) # Prepare analysis params - used for evauating analysis expression. # Case: input parameters get overriden by job parameters - default analysisParams={} analysisParams.update(self.inputParameters) if 'params' in job: analysisParams.update(job['params']) # Case: job parameters get overriden by input parameters - unimplemented # Prepare parameters dictionary for local namespace self.analysisLocals['param'].clear() self.analysisLocals['param'].update(analysisParams) # Evaluate analysis statement (anType, anCode, anCommand)=eval(job['command'], globals(), evalEnv) # File ending if anCode not in anCodeCount: anCodeCount[anCode]=0 else: anCodeCount[anCode]+=1 fileEnding=anCode+str(anCodeCount[anCode]) paFileEnding="pa"+str(paSeq) paSeq+=1 # Store file endings self.jobIndex2fileEnding[jobIndex]=fileEnding self.jobIndex2paFileEnding[jobIndex]=paFileEnding f.write('* Analysis: '+job['name']+'\n') f.write('.lib an'+fileEnding+'\n') # Write options if 'options' in job: for (option, value) in job['options'].items(): if value is True: f.write('.options '+str(option)+'=1\n') elif value is False: f.write('.options '+str(option)+'=0\n') else: f.write('.options '+str(option)+'='+str(value)+'\n') # Force writing the results to binary files f.write('.options post=1\n') # Write parameters for (name, value) in analysisParams.items(): if name=='temperature': f.write('.temp '+str(value)+'\n') else: f.write('.param '+name+'='+str(value)+'\n') # Generate saves if 'saves' in job: savesList=self._createSaves(job['saves'], job['variables']) # Write saves # Assume that 'all' is first in compiled saves list if len(savesList)>0: # Force storing only saves if 'all' is missing if savesList[0]!='all' and anType!='ac': # Save only those quantities that are listed under .probe # This can't be done for AC analysis because probe saves only real values f.write('.options probe=1\n') saves=savesList elif anType=='ac': # In case of AC analysis ignore saves, save everything saves=[] else: # Skip 'all' saves=savesList[1:] # Are there any saves left if len(saves)>0: # Write 8 saves at a time count=0 for save in saves: if count == 0: f.write('.probe '+anType) f.write(' '+save) count+=1 if count == 8: count=0 f.write('\n') f.write('\n') # Write analysis f.write(anCommand+'\n') f.write('.endl an'+fileEnding+'\n\n') # Write main file fileName=self.simulatorID+'.sp' if self.debug>0: DbgMsgOut("HSSI", "Writing top level file to '"+fileName+"'.") with open(fileName, 'w') as f: # First line f.write('* HSPICE simulator input file.\n\n') # Add dummy parameter for operating point calculation f.write('.param dummy___=0\n\n') # Add analyses first=True for jobIndex in self.flatJobSequence: # Get job. job=self.jobList[jobIndex] if self.debug>0: DbgMsgOut("HSSI", " '"+str(job['name'])+"'") # Set alter or title f.write('* Analysis: '+job['name']+'\n') if first: f.write('.title '+job['name']+'\n') else: f.write('.alter '+job['name']+'\n') # Write topology if first or self.jobIndex2topologyIndex[jobIndex]!=topologyIndex: if not first: # For any topology change, delete the old topology f.write('.del lib \''+self.simulatorID+'_topology.lib\' top'+str(topologyIndex)+'\n') topologyIndex=self.jobIndex2topologyIndex[jobIndex] f.write('.lib \''+self.simulatorID+'_topology.lib\' top'+str(topologyIndex)+'\n') # Write analysis if not first: f.write('.del lib \''+self.simulatorID+'_analysis.lib\' an'+fileEnding+'\n') fileEnding=self.jobIndex2fileEnding[jobIndex] f.write('.lib \''+self.simulatorID+'_analysis.lib\' an'+fileEnding+'\n\n') # Handled first analysis if first: first=False # Write .end f.write('.end\n') return fileName
[docs] def writeTopology(self): """ Creates the topology file. The file is named ``simulatorID_topology.lib`` and is a library file with one section corresponding to the circuit description of one group of jobs with a common circuit definition. Sections of the library are named ``topIndex`` where ``Index`` is the index of the group of jobs in the job list. Every section consists of ``.include`` and ``.lib`` simulator directives corresponding to system description modules given in the job description. The topology file does not depend on the input parameter values. Therefore it is created only once for every job list that is supplied with the :meth:`setJobList` method. """ # Write the topology file, store topology section names in job fileName=self.simulatorID+'_topology.lib' if self.debug>0: DbgMsgOut("HSSI", "Writing topology file to '"+fileName+"'.") with open(fileName, 'w') as f: f.write('* HSPICE topology library\n\n') # Prepare topology index list for jobs. self.jobIndex2topologyIndex=[0]*len(self.jobList) # Traverse all job groups topologyIndex=0 for jobGroup in self.jobSequence: # Get representative job (first in job group) jobIndex=jobGroup[0] job=self.jobList[jobIndex] f.write('.lib top'+str(topologyIndex)+'\n') # Go through all definitions for definition in job['definitions']: if 'section' in definition: f.write('.lib \''+definition['file']+'\' '+definition['section']+'\n') else: f.write('.include \''+definition['file']+'\'\n') # Store topology index for all jobs in this group. for jobIndex in jobGroup: self.jobIndex2topologyIndex[jobIndex]=topologyIndex f.write('.endl top'+str(topologyIndex)+'\n\n') # Next topology index topologyIndex+=1
[docs] def cleanupResults(self, i): """ Removes all result files that were produced during the simulation of the *i*-th job group. Because the user is always presented with only one job group, 0 is the only allowed value of *i*. Simulator input files are left untouched. """ if i!=0: raise PyOpusError(DbgMsg("HSSI", "Bad job group index (not 0).")) if self.debug>0: DbgMsgOut("HSSI", "Cleaning up.") # Remove old results files for jobIndex in self.flatJobSequence: fileEnding=self.jobIndex2fileEnding[jobIndex] try: os.remove(self.simulatorID+'.'+fileEnding) except KeyboardInterrupt: DbgMsgOut("HSSI", "Keyboard interrupt.") raise except: None
[docs] def runFile(self, fileName): """ Runs the simulator on the main simulator input file given by *fileName*. Returns ``True`` if the simulation finished successfully. This does not mean that any results were produced. It only means that the return code from the simuator was 0 (OK). """ if self.debug>0: DbgMsgOut("HSSI", "Running file '"+fileName+"'.") # Run the file spawnOK=True p=None try: # Start simulator, input file is fileName, output file is fileName less last 3 chars (.sp) if platform.system()=='Windows': # The HSPICE window should not appear info = subprocess.STARTUPINFO() info.dwFlags |= subprocess.STARTF_USESHOWWINDOW info.wShowWindow = subprocess.SW_HIDE p=subprocess.Popen( [self.binary]+self.cmdline+['-i', fileName, '-o', fileName[:-3]], startupinfo=info ) else: p=subprocess.Popen( [self.binary]+self.cmdline+['-i', fileName, '-o', fileName[:-3]], # universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE ) # Collect output self.messages=p.stdout.read().decode("utf-8") # Now wait for the process to finish. If we don't wait p might get garbage-collected before the # actual process finishes which can result in a crash of the interpreter. retcode=p.wait() # Nothing goes to stdout so we must read the .lis file if 1 or platform.system()=='Windows': # We read the .lis file only in debug mode if self.debug>1: with open(fileName[:-3]+'.lis', 'r') as f: self.messages=f.read() # No decoding, file is opened as text by default else: self.messages='' if self.debug>2: DbgMsgOut("HSSI", self.messages) elif self.debug>1: DbgMsgOut("HSSI", self.messages[-400:]) # Check return code. Nonzero return code means that something has gone bad. # At least the simulator says so. if retcode!=0: spawnOK=False if self.debug>0: DbgMsgOut("HSSI", "Spawn process FAILED.") except KeyboardInterrupt: DbgMsgOut("HSSI", "Keyboard interrupt.") # Will raise an exception if process exits before kill() is called. try: p.kill() except: pass raise KeyboardInterrupt except: if self.debug>0: ei=exc_info() if self.debug>1: for line in format_exception(ei[0], ei[1], ei[2]): DbgMsgOut("HSSI", "Exception: "+line) else: for line in format_exception_only(ei[0], ei[1]): DbgMsgOut("HSSI", "Exception: "+line) spawnOK=False if not spawnOK and self.debug>0: DbgMsgOut("HSSI", " run FAILED") return spawnOK
[docs] def runJobGroup(self, i): """ Runs the *i*-th job group. Because the user is always presented with only one job group, 0 is the only allowed value of *i*. If a fresh job list is detected a new topology file is created by invoking the :meth:`writeTopology` method. Next the analysis library file and the main simulator input file are created by the :meth:`writeFile` method. The :meth:`cleanupResults` method removes any old results produced by previous runs of the jobs. Finally the :meth:`runFile` method is invoked. Its return value is stored in the :attr:`lastRunStatus` member. The function returns a tuple (*jobIndices*, *status*) where *jobIndices* is a list of job indices corresponding to the jobs that were simulated. *status* is the status returned by the :meth:`runFile` method. """ # raise PyOpusError if i is not 0. if i!=0: raise PyOpusError(DbgMsg("HSSI", "Bad job group index (not 0).")) # Topology must always be written despite the fact that it does not change # because the user might have cleaned up the files from the last job self.writeTopology() # Write file for job group. filename=self.writeFile(i) # Delete old results. self.cleanupResults(i) # Run file self.lastRunStatus=self.runFile(filename) # Get job indices for jobs in this job group. jobIndices=self.jobGroup(i) return (jobIndices, self.lastRunStatus)
[docs] def readResults(self, jobIndex, runOK=None): """ Read results of a job with given *jobIndex*. *runOK* specifies the status returned by the :meth:`runJobGroup` method which produced the results. If not specified the run status stored by the simulator is used. Returns an object of the class :class:`HSpiceSimulationResults`. If the run failed or the results file cannot be read the ``None`` is returned. """ if runOK is None: runOK=self.lastRunStatus path2num={} job=self.jobList[jobIndex] if runOK: fileEnding=self.jobIndex2fileEnding[jobIndex] fileName=self.simulatorID+'.'+fileEnding if self.debug>1: DbgMsgOut("HSSI", "Reading results from '"+fileName+"'.") try: rawData=hspice_read( fileName ) except: rawData=None paFileName=self.simulatorID+'.'+self.jobIndex2paFileEnding[jobIndex] if self.debug>1: DbgMsgOut("HSSI", "Reading .pa# file '"+paFileName+"'.") try: with open(paFileName, "r") as f: while True: l=f.readline() if not l: break l=l.strip() ntxt, path = l.split(" ") subn=int(ntxt) path2num[path]=subn except: pass else: rawData=None if self.debug>0: if rawData is not None: DbgMsgOut("HSSI", "Job '"+str(job['name'])+"' OK") else: DbgMsgOut("HSSI", "Job '"+str(job['name'])+"' FAILED") if rawData is None: return None else: params={} params.update(self.inputParameters) params.update(job['params']) return HSpiceSimulationResults( rawData, path2num, params=params, variables=job['variables'] )
[docs] def jobGroupCount(self): """ Returns the number of job groups. """ return 1
[docs] def jobGroup(self, i): """ Returns a list of job indices corresponding to the jobs in *i*-th job group. Because the user is always presented with only one job group, only 0 is allowed for the value of *i*. """ # Because the user is always presented with only one job group, # raise PyOpusError if i is not 0. if i!=0: raise PyOpusError(DbgMsg("HSSI", "Bad job group index (not 0).")) return self.flatJobSequence
# # Job optimization #
[docs] def unoptimizedJobSequence(self): """ Returns the unoptimized internal job sequence. If there are n jobs in the job list the following list of lists is returned: ``[[0], [1], ..., [n-1]]``. This means we have n job groups with every one of them holding one job. Also stores the flat job sequence in the :attr:`flatJobSequence` member. A flat job sequence is a list of jobs appearing in the order in which they will be simulated. In this case the flat job list is [0, 1, ..., n-1]. The flast job sequence is the first and only job group which is presented to the user. """ seq=[[0]]*len(self.jobList) for i in range(len(self.jobList)): seq[i]=[i]; # Store flat job sequence, so we don't have to rebuild it every time. self.flatJobSequence=range(len(self.jobList)) return seq
[docs] def optimizedJobSequence(self): """ Returns the optimized internal job sequence. It has as many job groups as there are different circuit topologies (lists of system description modules) in the job list. Jobs in one job group share the same circuit topology. They are ordered by their indices with lowest job index listed as the first in the group. Also stores the flat job sequence in the :attr:`flatJobSequence` member. A flat job sequence is a list of jobs appearing in the order in which they will be simulated. In this case the flat job sequence is actually the flattened version of the optimized internal job sequence. """ # Count jobs jobCount=len(self.jobList) # Construct a list of job indices candidates=set(range(jobCount)) # Empty job sequence seq=[] # Repeat while we have a nonempty indices list. while len(candidates)>0: # Take one job i1=candidates.pop() # Start a new job group jobGroup=[i1] # Compare i1-th job with all other jobs peerCandidates=list(candidates) for i2 in peerCandidates: # Check if i1 and i2 can be joined together # Compare jobs, join them if the definitions are identical. if (self.jobList[i1]['definitions']==self.jobList[i2]['definitions']): # Job i2 can be joined with job i1, add it to jobGroup jobGroup.append(i2) # Remove i2 from candidates candidates.remove(i2) # Sort jobGroup jobGroup.sort() # Append it to job sequence seq.append(jobGroup) # Store flat job sequence, so we don't have to rebuild it every time. self.flatJobSequence=[] for jobGroup in seq: self.flatJobSequence.extend(jobGroup) return seq
[docs]class HSpiceSimulationResults(SimulationResults): """ Objects of this class hold HSPICE simulation results along with the data from the corresponding .pa# file. """ def __init__(self, rawData, path2num={}, params={}, variables={}, results={}): SimulationResults.__init__(self, params, variables, results) self.rawData=rawData self.path2num=path2num
[docs] def shorten(self, path): """ Returns the shortened name of an instance based on the subcircuit number listed in the corresponding .pa# file. If the full instance path is ``x1.xm1.m0`` and ``x1.xm1.`` is subcircuit number 5, then the shortened name is ``5:m0``. Shortened names are used for accessing noise contributions. """ lastDotPos=path.rfind(".") if lastDotPos<0: # Not found, do not translate return path # Path including the last dot p=path[:(lastDotPos+1)] if p in self.path2num: num=self.path2num[p] return str(num)+":"+path[(lastDotPos+1):] # Not found in dictionary, do not translate return path
[docs] def title(self): """ Return the title of the results. """ # First index is always 0 (top level object is list with one entry) # Second index is an index into a tuple with 6 elements, take element 3 (title) return self.rawData[0][3]
[docs] def date(self): """ Return the date of the results. """ # First index is always 0 (top level object is list with one entry) # Second index is an index into a tuple with 6 elements, take element 4 (date) return self.rawData[0][4]
[docs] def sweptParameter(self): """ Return the name of the swept parameter. """ # First index is always 0 (top level object is list with one entry) # Second index is an index into a tuple with 6 elements, take element 0 # Third index is an index into a tuple with 3 elements, take element 0 (variable name) return self.rawData[0][0][0]
[docs] def sweepValues(self): """ Return the values of the swept parameter. """ # First index is always 0 (top level object is list with one entry) # Second index is an index into a tuple with 6 elements, take element 0 # Third index is an index into a tuple with 3 elements, take element 1 (variable values) return self.rawData[0][0][1]
[docs] def vectorNames(self, resIndex=0): """ Returns the names of available vectors. """ if resIndex<0 or resIndex>len(self.rawData[0][0][2]): raise PyOpusError("Result group index out of bounds.") return list(self.rawData[0][0][2][resIndex].keys())
[docs] def vector(self, name, resIndex=0): """ Returns vector named *name* for *resIndex*-th sweep point. """ # First index is always 0 (top level object is list with one entry) # Second index is an index into a tuple with 6 elements, take element 0 # Third index is an index into a tuple with 3 elements, take element 2 (result groups list) # Fourth index is the swept parameter index (resIndex) if resIndex<0 or resIndex>len(self.rawData[0][0][2]): raise PyOpusError("Sweep point index out of bounds.") resGrp=self.rawData[0][0][2][resIndex] if name in resGrp: return resGrp[name] else: raise PyOpusError("Vector '%s' not found." % (name))
[docs] def scaleName(self, vecName=None, resIndex=0): """ If *vecName* is specified returns the name of the scale vector corresponding to the specified vector of the *resIndex*-th sweep point. For HSPICE this is always the default scale. If *vecName* is not specified returns the name of the vector holding the default scale of the *resIndex*-th sweep point. """ if resIndex<0 or resIndex>len(self.rawData[0][0][2]): raise PyOpusError("Sweep point index out of bounds.") # First index is always 0 (top level object is list with one entry) # Second index is an index into a tuple with 6 elements, take element 1 (default scale name) return self.rawData[0][1]
[docs] def scale(self, vecName=None, resIndex=0): """ If *vecName* is specified returns the scale corresponding to the specified vector in the *resIndex*-th sweep point. For HSPICE this is always the default scale. If *vecName* is not specified returns the default scale of the *resIndex*-th sweep point. """ name=self.scaleName(vecName, resIndex) return self.vector(name, resIndex=resIndex)
[docs] def v(self, node1, node2=None, resIndex=0): """ Retrieves the voltage corresponding to *node1* (voltage between nodes *node1* and *node2* if *node2* is also given) from the *resIndex*-th sweep point. """ if node2 is None: return self.vector(node1, resIndex=resIndex) else: return self.vector(node1, resIndex=resIndex)-self.vector(node2, resIndex=resIndex)
[docs] def i(self, name, resIndex=0): """ Retrieves the current flowing through instance *name* from the *resIndex*-th plot. """ return self.vector("i("+name, resIndex=resIndex)
[docs] def p(self, name, parameter, index=None, resIndex=0): """ Retrieves property named *parameter* belonging to instance named *name*. *index* must always be ``None`` because HSPICE does not support vector properties. The property is retrieved for *resIndex*-th sweep point. Note that this works only of the property was saved with a corresponding save directive. """ if index is None: return self.vector(parameter+'('+name, resIndex=resIndex) else: raise PyOpusError("HSPICE does not support properties with indices")
[docs] def ns(self, reference, name=None, contrib=None, resIndex=0): """ Retrieves the noise spectrum density of contribution *contrib* of instance *name* to the input/output noise spectrum density. *reference* can be ``'input'`` or ``'output'``. If *name* and *contrib* are not given the output or the equivalent input noise spectrum density is returned (depending on the value of *reference*). The spectrum is returned for the *resIndex*-th sweep point. """ # TODO: units of total/partial input/output spectra (V**2/Hz or V**2) if name is None: # Input/output noise spectrum if reference=='input': spec=self.vector('innoise', resIndex=resIndex) elif reference=='output': spec=self.vector('outnoise', resIndex=resIndex) else: raise PyOpusError("Bad noise reference.") else: # Partial spectrum if reference=='input': A=( self.vector('outnoise', resIndex=resIndex) / self.vector('innoise', resIndex=resIndex) ) elif reference=='output': A=1.0 else: raise PyOpusError("Bad noise reference.") shortName=self.shorten(name) if contrib is None: spec=self.vector('nt('+str(shortName), resIndex=resIndex)/A else: spec=self.vector(str(contrib)+'('+str(shortName), resIndex=resIndex)/A return spec
[docs] def driverTable(self): """ Returns a dictionary of available driver functions for accessing simulation results. """ return { 'ipath': ipath, 'scaleName': self.scaleName, 'scale': self.scale, 'vectorNames': self.vectorNames, 'vector': self.vector, 'v': self.v, 'i': self.i, 'p': self.p, 'ns': self.ns, }