Source code for pyopus.simulator.xyce

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

** Xyce interface (PyOPUS subsystem name: XYSI)**

Xyce is a free simulator written from scratch at Sandia National Laboratories. 

Xyce can run only one analysis per simulator invocation. The save directive 
philosopy is radically different from that of the other simulators supported 
by PyOpus. We try hard to make this interface similar to thos of other
simulators, but one can only do so much. 

As with other simulators the ``temperature`` parameter which represents the 
circuit's temperature in degrees centigrade (``.option temp=...`` simulator 
directive). Consequently the ``temp`` simulator option is not allowed to 
appear in the simulator options list. 

The Xyce interface does no joblist optimization because there can be none due 
to the way Xyce is designed. 

Furthermore, Xyce noise analysis cannot store results in rawfile format.  
Therefore csv format is used which is less efficient. It is read with the 
datatable Python package. If datatable is not available Pandas is used 
which is somewhat slower. You can check if datatable is used by looking at 
``pyopus.simulator.xyce.datatable``. If it is not ``None`` datatable is 
used for loading csv files. 
"""

import subprocess
from .base import Simulator, SimulationResults
from .rawfile import raw_read
import os
import platform
import shutil
from ..misc.env import environ
from ..misc.debug import DbgMsgOut, DbgMsg
from .. import PyOpusError
from pprint import pprint
from datetime import datetime
try:
	import datatable
except:
	import pandas
	datatable=None


__all__ = [ 'ipath', 'save_all', 'save_voltage', 'save_current', 'save_property', 
			'an_op', 'an_dc', 'an_tf', 'an_ac', 'an_tran', 'an_noise', 'Xyce', 
			'XyceSimulationResults' ] 

simulatorDescription=("Xyce", "Xyce")
"""
For detecting simulators. 
"""

#
# Hierarchical path handling 
#

[docs]def ipath(inputobj, 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 *inputobj* 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 the 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). Xyce handles hierarchical paths identically for instances, models, and nodes. Therefore the *objectType* argument is ignored. All Xyce hierarchical paths begin with the outermost instance followed by its children. Instances along a hierarchical path are separated with a colon (``:``). So ``x2:x1:10`` is a node named ``10`` that is a part of ``x1`` (inside ``x1``) which in turn is a part of ``x2`` (inside ``x2``). The same applies to instance and model names. 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 ``xm1:x1:x2`` and ``xm2:x1:x2``. Returns ``['x2:x1:xm1:m0', 'x2:x1:xm2:m0']``. """ # Create outer path if outerHierarchy is None: outerHierarchy=[] elif type(outerHierarchy ) is str: outerHierarchy=[outerHierarchy] # Create inner path if innerHierarchy is None: innerHierarchy=[] elif type(innerHierarchy ) is str: innerHierarchy=[innerHierarchy] # Create list of objects if type(inputobj) is not list: lst=[inputobj] else: lst=inputobj # Create list of outputs outlist=[innerHierarchy+[obj]+outerHierarchy for obj in lst] # All paths are innermost object first, reverse them # so that innermost object is last outlist=[list(reversed(e)) for e in outlist] # Handle all objects in the same way outlist=[":".join(e) for e in outlist] # Scalarize if inputobj was a scalar if type(inputobj) is not list: return outlist[0] else: return outlist
# # Save directive generators #
[docs]def save_all(): """ Returns a save directive that saves all the voltages the simulator computes. Equivalent of Xyce ``v(*)`` output variable. """ return [ 'v(*)' ]
[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 multiple save directives are returned instructing the simulator to save the voltages of nodes with names given by the *what* list. Equivalent of Xyce ``v(what)`` output variable. """ compiledList=[] if type(what) is list: insts=what else: insts=[what] compiledList=['v('+name+')' for name in insts] return compiledList
[docs]def save_current(what, lead=None): """ If *what* is 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. If *lead* is given it must be a one letter string. It specifies the lead through which the current is measured. *lead* can also be a list in which case currents through all listed leads are saved. Equivalent of Xyce ``save i(what)`` output variable (if *lead* is not given). If *lead* is given it is the equivalent of ``i<lead>(what)`` output variable. """ compiledList=[] if type(what) is list: insts=what else: insts=[what] if lead is None: compiledList=['i('+name+')' for name in insts] else: if type(lead) is list: leads=lead else: leads=[lead] compiledList=['i'+l+'('+name+')' for name in insts for l in leads] return compiledList
[docs]def save_property(devices, params=None, indices=None): """ Saves the internal variables of devices given by *devices* and parameters *params* both arguments can either be a string or a list. If *params* is not given the device names are used as parameter names. *indices* is not supported because Xyce variables are all scalars. If *devices* and/or *params* is a list all combinations of devices and parameters are generated. Run Xyce netlist with ``Xyce -namesfile <filename> <netlist>`` to get the list of available internal variables. Equvalent of Xyce ``n(device:param)`` output variable or ``n(device)`` if *params* is not given. """ compiledList=[] if type(devices) is list: insts=devices else: insts=[devices] if indices is not None: raise PyOpusError(DbgMsg("XYSI", "Property index is not supported.")) if params is not None: if type(params) is list: inputParams=params else: inputParams=[params] compiledList=['n('+inst+':'+param+')' for inst in insts for param in inputParams] else: compiledList=['n('+inst+')' for inst in insts] return compiledList
def save_ns(devices, contributions=None): """ Saves noise contribution of *devices* to output noise. If *contributions* is given the contributions of individual noise sources within *devices* are saved. *devices* and *contributions* can be a list or a string. In case of a list all combinations of device and contribution are generated. Equivalent of Xyce ``n(device,source)`` output variable or ``n(device)`` if *contributions* is not given. """ compiledList=[] if type(devices) is list: devs=devices else: devs=[devices] if contributions is not None: if type(contributions) is list: srcs=contributions else: srcs=[contributions] compiledList=['dno('+dev+','+src+')' for dev in devs for src in srcs] else: compiledList=['dno('+dev+')' for dev in devs] return compiledList # # Analysis command generators #
[docs]def an_op(): """ Generates the Ngspice simulator command that invokes the operating point analysis. Performs a single point dc sweep of a voltage source named ``dummy___`` added by PyOpus. """ return ('dc', '.dc dummy___ list 1')
[docs]def an_dc(start, stop, sweep, points, name, parameter=None, index=None): """ Generates the Xyce simulator command 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* points is an integer specifying the number of points * ``'dec'`` - logarithmic sweep with points per decade points is an integer specifying the number of points per decade * ``'oct'`` - logarithmic sweep with points per octave points is an integer specifying the number of points per octave * ``'list'`` - sweep a list of values points is a list specifying the points *name* gives the name of the instance whose *parameter* is swept. For sweeping voltage and current sources *parameter* does not have to be given. *index* is not supported by Xyce. Equivalent of Xyce ``.dc`` simulator command. """ if index is not None: raise PyOpusError(DbgMsg("XYSI", "Index is not suported.")) if name is None: if parameter=="temperature": devStr='temp' else: devStr=parameter else: if parameter is not None: devStr=str(name)+':'+parameter else: devStr=str(name) if sweep == 'lin': return ('dc', '.dc lin '+devStr+' '+str(start)+' '+str(stop)+' '+str((stop-start)/(points+1))) elif sweep == 'dec': return ('dc', '.dc dec '+devStr+' '+str(start)+' '+str(stop)+' '+str(points)) elif sweep == 'oct': return ('dc', '.dc oct '+devStr+' '+str(start)+' '+str(stop)+' '+str(points)) elif sweep == 'list': return ('dc', '.dc oct '+devStr+' '+str(start)+' '+str(stop)+' '+(" ".join([str(p) for p in points]))) else: raise PyOpusError(DbgMsg("XYSI", "Bad sweep type."))
[docs]def an_ac(start, stop, sweep, points): """ Generates the Xyce simulator command 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* Equivalent of Xyce ``.ac`` simulator command. """ if sweep == 'lin': return ('ac', '.ac lin '+str(points)+' '+str(start)+' '+str(stop)) elif sweep == 'dec': return ('ac', '.ac dec '+str(points)+' '+str(start)+' '+str(stop)) elif sweep == 'oct': return ('ac', '.ac oct '+str(points)+' '+str(start)+' '+str(stop)) else: raise PyOpusError(DbgMsg("XYSI", "Bad sweep type."))
[docs]def an_tran(step, stop, start=0.0, maxStep=None, uic=False): """ Generates the Xyce simulator command that invokes the transient analysis. The range of the time sweep is given by *start* and *stop*. *step* is the intiial time step. The upper limit on the time step is given by *maxStep*. If the *uic* flag is set to ``True`` the initial conditions given by ``.ic`` simulator directives and initial conditions specified as instance parameters (e.g. ``ic`` parameter of capacitor) are used as the first point of the transient analysis instead of the operating point analysis results. If *uic* is ``True`` and *maxStep* is not given, the default value *maxStep* is *step*. Equivalent of Xyce ``.tran`` simulator command. """ if uic: if maxStep is None: maxStep=step return ('tran', '.tran '+str(step)+" "+str(stop)+" "+str(start)+" "+str(maxStep)+" uic") else: if maxStep is None: return ('tran', '.tran '+str(step)+" "+str(stop)+" "+str(start)) else: return ('tran', '.tran '+str(step)+" "+str(stop)+" "+str(start)+" "+str(maxStep))
[docs]def an_noise(start, stop, sweep, points, input, outp=None, outn=None, outcur=None, ptsSum=1): """ Generates the Xyce simulator command 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*. If *outcur* is given the current flowing through the voltage source with that name is considered to be the output. *ptsSum* is not supported by Xyce and is ignored. Equivalent of Xyce ``.noise`` simulator command. """ if outp is not None and outn is None and outcur is None: outspec="v("+str(outp)+")" elif outp is not None and outn is not None and outcur is None: outspec="v("+str(outp)+","+str(outn)+")" elif outp is None and outn is None and outcur is not None: outspec="i("+str(outcur)+")" else: raise PyOpusError(DbgMsg("XYSI", "Bad output specification for NOISE analysis.")) if sweep=='lin': anstr='.noise '+outspec+" "+str(input)+' lin '+str(points)+" "+str(start)+" "+str(stop)+" "+str(ptsSum) elif sweep=='dec': anstr='.noise '+outspec+" "+str(input)+' dec '+str(points)+" "+str(start)+" "+str(stop)+" "+str(ptsSum) elif sweep=='oct': anstr='.noise '+outspec+" "+str(input)+' oct '+str(points)+" "+str(start)+" "+str(stop)+" "+str(ptsSum) else: raise PyOpusError(DbgMsg("XYSI", "Bad sweep type.")) return ('noise', anstr)
[docs]class Xyce(Simulator): """ A class for interfacing with the Ngspice simulator in batch mode. *binary* is the path to the Xyce simulator binary. If it is not given the ``XYCEPATH`` environmental variable is used as the path to the Ngspice binary. If ``XYCEPATH`` 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 is also printed. If *debug* is above 2 full simulator output is printed. By default the Xyce interface is configured to save all voltages. Contrary to other Spice simulators, currents of indipendent voltagfe sources are not saved by the default. Similarly noise analysis saves only the output and the equivalebnt output noise. To save noise contributions one must specify corresponding save directives. 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 * ``ns`` - a reference to the :func:`save_ns` 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, timeout=None): Simulator.__init__(self, binary, args, debug) self.timeout = timeout; self._compile()
[docs] @classmethod def findSimulator(cls): """ Finds the simulator. Location is defined by the XYCEPATH environmental variable. If the binary is not found there the system path is used. """ if 'XYCEPATH' in environ: xycebinary=environ['XYCEPATH'] else: if platform.system()=='Windows': xycebinary=shutil.which("Xyce.exe") else: xycebinary=shutil.which("Xyce") # Verify binary if xycebinary is None: return None elif os.path.isfile(xycebinary): return xycebinary 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, 'ns': save_ns, '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 if self.binary is None: self.binary=Xyce.findSimulator() # Last resort - use local folder if self.binary is None: if platform.system()=='Windows': self.binary=os.path.join(".", 'Xyce.exe') else: self.binary=os.path.join(".", 'Xyce') # 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(DbgMsg("XYSI", "Save directives must evaluate to a list of strings.")) for save in saveList: if type(save) is not str: raise PyOpusError(DbgMsg("XYSI", "Save directives must evaluate to a list of strings.")) compiledList+=saveList # Make list memebers unique return list(set(compiledList)) # # Batch simulation #
[docs] def writeFile(self, i): """ Prepares the simulator input file for running the *i*-th job group. The file is named ``simulatorID.group_i.cir`` where *i* is the index of the job group. All output files with simulation results are .raw files in binary format. System description modules are converted to ``.include`` and ``.lib`` simulator directives. Simulator options are set with the ``set`` simulator command. Integer, real, and string simulator options are converted with the :meth:`__str__` method before they are written to the file. Boolean options are converted to ``set`` or ``unset`` commands depending on whether they are ``True`` or ``False``. The parameters set with the last call to :meth:`setInputParameters` method are joined with the parameters in the job description. The values from the job description take precedence over the values specified with the :meth:`setInputParameters` method. All parameters are written to the input file in form of ``.param`` simulator directives. The ``temperature`` parameter is treated differently. It is written to the input file in form if a ``.options`` simulator command. Save directives are dumped into a ``.print`` simulator command. Every analysis command is evaluated in its corresponding environment taking into account the parameter values passed to the :meth:`setInputParameters` method. The results are stored in a file named ``simulatorID.job_j.jobName.raw`` where *j* denotes the job index from which the analysis was generated. *jobName* is the ``name`` member of the job description. The function returns the name of the simulator input file it generated. """ # Build file name fileName=self.simulatorID+".group"+str(i)+'.cir' if self.debug>0: DbgMsgOut("XYSI", "Writing job group '"+str(i)+"' to file '"+fileName+"'") with open(fileName, 'w') as f: # First line title='* Simulator input file for job group '+str(i) self.jobTitles[i]=title f.write(title+'\n\n') # Add dummy parameter for operating point calculation f.write('.global_param dummy___=0\n\n') # Job group jobGroup=self.jobGroup(i) # Representative job repJob=self.jobList[jobGroup[0]] # Write representative options (as .option directives) if 'options' in repJob: for (option, value) in repJob['options'].items(): if value is True: f.write('.options '+option+'\n') else: f.write('.options '+option+'='+str(value)+'\n') # Prepare representative parameters dictionary. # Case: input parameters get overriden by job parameters - default params={} params.update(self.inputParameters) if 'params' in repJob: params.update(repJob['params']) # Case: job parameters get overriden by input parameters - unimplemented # Write representative parameters, handle temperature as simulator option. for (param, value) in params.items(): if param!="temperature": f.write('.global_param '+param+'={'+str(value)+'}\n') else: f.write('.options temp='+str(value)+'\n') # Include definitions for definition in repJob['definitions']: if 'section' in definition: f.write('.lib \"'+definition['file']+'\" '+definition['section']+'\n') else: f.write('.include \"'+definition['file']+'\"\n') # Control block f.write('\n'); # A job group can contain only one job if len(jobGroup)>1: raise PyOpusError(DbgMsg("XYSI", "Every job group can contain only one job.")) # Handle analyses for j in jobGroup: # Get job job=self.jobList[j] # Get job name if self.debug>0: DbgMsgOut("XYSI", " job '"+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 # Analysis commands start here f.write('* '+job['name']+'\n') # No need to write options for analysis because there is only one analysis # and its options were handled by the representative job before this loop # The same goes for temperature parameter # Prepare parameters dictionary for local namespace self.analysisLocals['param'].clear() self.analysisLocals['param'].update(analysisParams) # Evaluate analysis statement (anType, anCommand)=eval(job['command'], globals(), evalEnv) self.jobAnalysisType[j]=anType # Write analysis f.write(anCommand+'\n') # Prepare saves if 'saves' in job and len(job['saves'])>0: saves=self._createSaves(job['saves'], job['variables']) savesStr=" ".join(saves) else: if anType!='noise': # By default save all voltages savesStr="v(*)" else: # No saves for noise analysis, except for those added later (inoise, onoise) savesStr="" # By default save inoise and onoise in noise analysis if anType=='noise': savesStr='inoise onoise '+savesStr # Write .print directive if anType=='noise': resultsFile = self.simulatorID+'.job'+str(j)+'.'+job['name']+'.csv' f.write('.print '+anType+' file='+resultsFile+' format=csv '+savesStr+'\n') else: resultsFile = self.simulatorID+'.job'+str(j)+'.'+job['name']+'.raw' f.write('.print '+anType+' file='+resultsFile+' format=raw '+savesStr+'\n') # End netlist f.write('.end\n') return fileName
[docs] def cleanupResults(self, i): """ Removes all result files that were produced during the simulation of the *i*-th job group. Simulator input files are left untouched. """ if self.debug>0: DbgMsgOut("XYSI", "Cleaning up result for job group "+str(i)) jobGroup=self.jobGroup(i) # Remove old .raw files for j in jobGroup: job=self.jobList[j] try: os.remove(self.simulatorID+".job"+str(j)+'.'+job['name']+'.raw') os.remove(self.simulatorID+".group"+str(i)+'.cir_noise.dat') except KeyboardInterrupt: DbgMsgOut("XYSI", "Keyboard interrupt") raise except: None
[docs] def runFile(self, fileName): """ Runs the simulator on the 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("NGSI", "Running file '"+fileName+"'") cmdLineList=[self.binary]+self.cmdline+[fileName] if self.debug>3: with open(fileName) as dbgf: DbgMsgOut("XYSI", dbgf.read()) DbgMsgOut("XYSI", "Starting simulator: "+str(cmdLineList)) # Run the file spawnOK=True p=None try: # Start simulator p=subprocess.Popen( cmdLineList, # universal_newlines=True, # Does not work with python3 stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE ) # Send quit command try: msgs, _ = p.communicate(timeout=self.timeout) self.messages=msgs.decode("utf-8") except subprocess.TimeoutExpired: if self.debug>1: DbgMsgOut("XYSI", "Simulation timeout") p.kill() msgs, _ = p.communicate() self.messages=msgs.decode("utf-8") spawnOK = False if self.debug>2: DbgMsgOut("XYSI", self.messages) elif self.debug>1: DbgMsgOut("XYSI", self.messages[-400:]) # 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() # Check return code. Nonzero return code means that something has gone bad. # At least the simulator says so. if retcode!=0: spawnOK=False except KeyboardInterrupt: DbgMsgOut("XYSI", "Keyboard interrupt") # Will raise an exception if process exits before kill() is called. try: p.kill() except: pass raise KeyboardInterrupt except: spawnOK=False if not spawnOK and self.debug>0: DbgMsgOut("XYSI", " run FAILED") return spawnOK
[docs] def runJobGroup(self, i): """ Runs the *i*-th job group. First calls the :meth:`writeFile` method followed by the :meth:`cleanupResults` method that removes any old results produced by previous runs of the jobs in *i*-th job group. 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 *i*-th job group. *status* is the status returned by the :meth:`runFile` method. """ # 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:`NgspiceSimulationResults`. If the run failed or the results file cannot be read the ``None`` is returned. """ if runOK is None: runOK=self.lastRunStatus job=self.jobList[jobIndex] if runOK: if self.jobAnalysisType[jobIndex]=="noise": fileName=self.simulatorID+".job"+str(jobIndex)+'.'+job['name']+'.csv' else: fileName=self.simulatorID+".job"+str(jobIndex)+'.'+job['name']+'.raw' if self.debug>1: DbgMsgOut("NGSI", "Reading results from '"+fileName+"'.") try: if self.jobAnalysisType[jobIndex]=="noise": rawData=loadCsvAsRaw(fileName, self.jobTitles[jobIndex], "NOISE analysis results") else: rawData=raw_read(fileName) except: raise rawData=None else: rawData=None if self.debug>0: if rawData is not None: DbgMsgOut("NGSI", "Job '"+str(job['name'])+"' OK") else: DbgMsgOut("NGSI", "Job '"+str(job['name'])+"' FAILED") if rawData is None: return None else: params={} params.update(self.inputParameters) params.update(job['params']) return XyceSimulationResults( rawData, params=params, variables=job['variables'] )
[docs] def jobGroupCount(self): """ Returns the number of job groups. """ return len(self.jobSequence)
[docs] def jobGroup(self, i): """ Returns a list of job indices corresponding to the jobs in *i*-th job group. """ return self.jobSequence[i]
# # Override setJoblist() #
[docs] def setJobList(self, jobList, optimize=True): # Prepare v list of analysis types corresponding to jobList entries # (needed for loading results) self.jobAnalysisType=[None]*len(jobList) # Prepare list of titles self.jobTitles=[None]*len(jobList) Simulator.setJobList(self, jobList, optimize)
# # Job optimization #
[docs] def unoptimizedJobSequence(self): """ Returns the unoptimized job sequence. If there are n jobs the job list the following list of lists is returned: ``[[0], [1], ..., [n-1]]``. This means we have n job groups with one job per job group. """ seq=[[0]]*len(self.jobList) for i in range(len(self.jobList)): seq[i]=[i] return seq
[docs] def optimizedJobSequence(self): """ Returns the optimized job sequence. Because Xyce does not support multiple analyses in one file there is nothing to optimize. """ return [[i] for i in range(len(self.jobList))]
def loadCsvAsRaw(fileName, title, name): # Bad column names when a column name contains a comma # cnames=data.names # Xyce CSV does not quote column names. This causes a problem when names contain commas. # We should ignore commas in parenthesis. Therefore we get the column names manually. with open(fileName) as f: l=f.readline() paren=0 last=0 cnames=[] for ii in range(len(l)): if l[ii]=='(': paren+=1 elif l[ii]==')': paren-=1 elif (l[ii]==',' and paren==0) or l[ii]=='\n': cnames.append(l[last:ii]) last=ii+1 # Skip header because it is incorrectly formated (names with commas are not quoted) if datatable is not None: # Use datatable data=datatable.fread(fileName, header=False, skip_to_line=2) vectors={cnames[ii]: data.to_numpy(column=ii) for ii in range(len(cnames))} scaleName=cnames[0] else: # Use pandas data=pandas.read_csv(fileName, header=None, skiprows=1) npData=data.to_numpy() vectors={cnames[ii]: npData[:,ii] for ii in range(len(cnames))} scaleName=cnames[0] tstamp=os.path.getmtime(fileName) # Format as 'Mon May 29 07:34:26 2023' date=datetime.utcfromtimestamp(tstamp).strftime('%a %b %d %H:%M:%S %Y') # date=datetime.fromtimestamp(tstamp) return [(vectors, scaleName, {}, title, date, name)]
[docs]class XyceSimulationResults(SimulationResults): """ Objects of this class hold Xyce simulation results. """ def __init__(self, rawData, params={}, variables={}, results={}): SimulationResults.__init__(self, params, variables, results) self.rawData=rawData
[docs] def title(self, resIndex): """ Return the title of the *resIndex*-th plot. """ if self.rawData is None or resIndex<0 or resIndex>len(self.rawData): raise PyOpusError("Result group index out of bounds.") return self.rawData[resIndex][3]
[docs] def date(self, resIndex): """ Return the date of the *resIndex*-th plot. """ if self.rawData is None or resIndex<0 or resIndex>len(self.rawData): raise PyOpusError("Result group index out of bounds.") return self.rawData[resIndex][4]
[docs] def name(self, resIndex): """ Return the name of the *resIndex*-th plot. """ if self.rawData is None or resIndex<0 or resIndex>len(self.rawData): raise PyOpusError("Result group index out of bounds.") return self.rawData[resIndex][5]
[docs] def vectorNames(self, resIndex=0): """ Returns the names of available vectors. """ if self.rawData is None or resIndex<0 or resIndex>len(self.rawData): raise PyOpusError("Result group index out of bounds.") return list(self.rawData[resIndex][0].keys())
[docs] def vector_(self, name, resIndex=0): """ Returns vector named *name* from *resIndex*-th plot. """ if self.rawData is None or resIndex<0 or resIndex>len(self.rawData): raise PyOpusError("Result group index out of bounds.") resGrp=self.rawData[resIndex] if name in resGrp[0]: return resGrp[0][name] else: return None
[docs] def vector(self, name, resIndex=0): """ Returns vector named *name* from *resIndex*-th plot. """ if self.rawData is None or resIndex<0 or resIndex>len(self.rawData): raise PyOpusError("Result group index out of bounds.") vec=self.vector_(name, resIndex) if vec is not None: return vec 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 in the *resIndex*-th plot. Usually this is the default scale. If *vecName* is not specified returns the name of the vector holding the default scale of the *resIndex*-th plot. """ if self.rawData is None or resIndex<0 or resIndex>len(self.rawData): raise PyOpusError("Result group index out of bounds.") resGrp=self.rawData[resIndex] if vecName not in resGrp[2]: return resGrp[1] else: return resGrp[2][vecName]
[docs] def scale(self, vecName=None, resIndex=0): """ If *vecName* is specified returns the scale corresponding to the specified vector in the *resIndex*-th plot. Usually this is the default scale. If *vecName* is not specified returns the default scale of the *resIndex*-th plot. """ 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 plot. Equivalent to Ngspice expression ``v(node1)`` (or ``v(node1,node2)``). Also used for retrieving results of tf analysis. Set *node1* to ``input_impedance``, ``output_impedance``, or ``transfer_function``. """ if node2 is None: return self.vector(node1.upper(), resIndex=resIndex) else: return self.vector(node1.upper(), resIndex=resIndex)-self.vector(node2.upper(), resIndex=resIndex)
[docs] def i(self, name, resIndex=0): """ Retrieves the current flowing through instance *name* from the *resIndex*-th plot. Equivalent to Xyce output variable ``i(name)``. """ return self.vector("I("+name.upper()+")", resIndex=resIndex)
[docs] def il(self, name, lead, resIndex=0): """ Retrieves the current flowing through instance *name* from the *resIndex*-th plot. Equivalent to Xyce output variable ``ilead(name)``. """ return self.vector("I("+name.upper()+")", resIndex=resIndex)
[docs] def p(self, name, parameter, index=None, resIndex=0): """ Retrieves the *index*-th component of property named *parameter* belonging to instance named *name*. If the property is not a vector, *index* can be ommitted. The property is retrieved from *resIndex*-th plot. Note that this works only of the property was saved with a corresponding save directive. Equivalent to Xyce output variable expression ``n(device)`` (or ``n(device:param)``). """ if index is not None: raise PyOpusError("Vector properties are not supported by Ngspice.") name='N('+name.upper()+':'+parameter.upper()+')' vec=self.vector_(name, resIndex) if vec is not None: return vec else: raise PyOpusError("Vector '%s' not found." % (name))
[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*). Partial and total noise spectra are returned as squared noise (in V^2/Hz or A^2/Hz). The spectrum is obtained from the *resIndex*-th plot. """ if name is None: # Input/output noise spectrum if reference=='input': spec=self.vector('INOISE', resIndex=resIndex) elif reference=='output': spec=self.vector('ONOISE', resIndex=resIndex) else: raise PyOpusError("Bad noise reference.") else: # Partial spectrum if reference=='input': A=( self.vector('ONOISE', resIndex=resIndex) / self.vector('INOISE', resIndex=resIndex) ) elif reference=='output': A=1.0 else: raise PyOpusError("Bad noise reference.") # Try both _ and . as separator, Ngspice is inconsistent in vector naming. if contrib is None: vec=self.vector_("DNO("+str(name).upper()+")", resIndex=resIndex) if vec is None: raise PyOpusError("Output noise contribution from '%s' not found." % str(name)) spec=vec/A else: vec=self.vector_("DNO("+str(name).upper()+","+str(contrib).upper()+")", resIndex=resIndex) if vec is None: raise PyOpusError("Output noise contribution from '%s', '%s' not found." % (str(name), str(contrib))) spec=vec/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, }