"""
.. 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,
}