"""
.. inheritance-diagram:: pyopus.simulator.ngspice
:parts: 1
** Ngspice batch mode interface (PyOPUS subsystem name: NGSI)**
Ngspice is a free Berkeley SPICE3-based simulator. It is capable of
interactive operation but this module uses it in batch mode. This means that
none of the advanced interactive features of Ngspice are used.
Ngspice in batch mode is not capable of changing the circuit's parameters
or its topology (system definition) without restarting the simulator and
loading a new input file.
An exception to this is the ``temperature`` parameter which represents the
circuit's temperature in degrees centigrade (``.option temp=...`` simulator
directive) and can be changed without restarting the simulator. Consequently
the ``temp`` simulator option is not allowed to appear in the simulator
options list.
All simulator options (``.options`` directive) can be changed interactively
without the need to restart the simulator and load a new input file. This
leaves very little space for job list optimization. Nevertheles there are
still some advantages to be gained from an optimized job list.
A job sequence in Ngspice is a list of lists containing the indices of jobs
belonging to individual job groups.
One result group can consist of multiple plots. See
:mod:`pyopus.simulator.rawfile` module for the details on the result files and
plots in Ngspice.
"""
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
__all__ = [ 'ipath', 'save_all', 'save_voltage', 'save_current', 'save_property',
'an_op', 'an_dc', 'an_tf', 'an_ac', 'an_tran', 'an_noise', 'Ngspice',
'NgspiceSimulationResults' ]
simulatorDescription=("Ngspice", "Ngspice")
"""
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
*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 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).
Because Ngspice handles hierarchical paths differently for instances, models,
and nodes the *objectType* argument specifies what kind of an object the path
will refer to. The available values of *objectType* are ``'inst'``, ``'mod'``,
and ``'node'``.
Ngspice hierarchical paths for nodes begin with the outermost instance
followed by its children. Dot (``.``) is used as the separator between
instances in the hierarchy. 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``).
Hierarchical paths of models are similar, except that the last separator
between the innermost subcircuit and the actual model name is a colon
``:``. For instance ``x1.x2:t2n2222`` is a model named ``t2n222`` which
is inside ``x2``, which in turn is inside ``x1``.
Hierarchical paths of instances start with instance type code (letter)
followed by a dot (``.``), which in turn is followed by the path to the
actual instance. E.g. ``r.x1.x2.r3`` is a resistor (note the leading
``r.``) named ``r3`` inside subcircuit ``x2``, which in turn is inside
subcircuit ``x1``.
Some examples:
* ``ipath('m1', ['x1', 'x2'])`` - instance named ``m1`` inside ``x1``
inside ``x2``. Returns ``'m.x2.x1.m1'``.
* ``ipath('x1', innerHierarchy=['m0', 'x0'])`` - instance ``m0`` inside
``x0`` inside ``x1``. Returns ``'m.x1.x0.m0'``.
* ``ipath(['m1', 'm2'], ['x1', 'x2']) - instances ``m1`` and ``m2`` inside
``x1`` inside ``x2``. Returns ``['m.x2.x1.m1', 'm.x2.x1.m2']``.
* ``ipath(['xm1', 'xm2'], ['x1', 'x2'], 'm0')`` - instances named ``m0``
inside paths ``xm1:x1:x2`` and ``xm2:x1:x2``. Returns
``['m.x2.x1.xm1.m0', 'm.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]
# Convert to strings
if objectType=='inst':
outlist=[e[-1][0]+"."+(".".join(e)) for e in outlist]
elif objectType=='mod':
outlist=[(".".join(e[:-1]))+":"+e[-1] for e in outlist]
else:
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 results the simulator normally
saves in its output (in Ngspice these are all node voltages and all
currents flowing through voltage sources and inductances).
Equivalent of Ngspice ``save all`` simulator command.
"""
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 multiple save directives are returned
instructing the simulator to save the voltages of nodes with names given
by the *what* list.
Equivalent of Ngspice ``save v(what)`` simulator command.
"""
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.
Equivalent of Ngspice ``save i(what)`` simulator command.
"""
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.
If *params* and *devices* have n and m memebrs, n*m save
directives are are returned describing all combinations of device name
and property name.
If *indices* is not supported by Ngspice.
Equvalent of Ngspice ``save @device[property]`` simulator command.
"""
compiledList=[]
if type(devices) is list:
inputDevices=devices
else:
inputDevices=[devices]
if type(params) is list:
inputParams=params
else:
inputParams=[params]
if indices is not None:
raise PyOpusError(DbgMsg("NGSI", "Property index is not supported."))
else:
for name in inputDevices:
for param in inputParams:
compiledList.append('@'+name+'['+param+']')
return compiledList
#
# Analysis command generators
#
[docs]def an_op():
"""
Generates the Ngspice simulator command that invokes the operating
point analysis.
Equivalent of Ngspice ``op`` simulator command.
"""
return 'op'
[docs]def an_dc(start, stop, sweep, points, name, parameter=None, index=None):
"""
Generates the Ngspice 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*
* ``'dec'`` - logarithmic sweep with points per decade
Not supported by Ngspice.
* ``'oct'`` - logarithmic sweep with points per octave
Not supported by Ngspice.
*name* gives the name of the instance whose *parameter* is swept. If the
parameter is a vector parameter *index* gives the integer index (zero
based) of the vector's component that will be swept.
Equivalent of Ngspice ``dc @name[param][index] start stop step``
simulator command.
"""
if index is None:
if name is None:
if parameter=='temperature':
devStr='temp'
else:
raise PyOpusError(DbgMsg("NGSI", "Bad sweep parameter."))
elif parameter is not None:
raise PyOpusError(DbgMsg("NGSI", "Property sweep is not supported."))
else:
devStr=str(name)
else:
raise PyOpusError(DbgMsg("NGSI", "Property sweep is not supported."))
if sweep == 'lin':
return 'dc '+devStr+' '+str(start)+' '+str(stop)+' '+str((stop-start)/(points+1))
elif sweep == 'dec':
raise PyOpusError(DbgMsg("NGSI", "Logarithmic DC sweep is not supported."))
elif sweep == 'oct':
raise PyOpusError(DbgMsg("NGSI", "Logarithmic DC sweep is not supported."))
else:
raise PyOpusError(DbgMsg("NGSI", "Bad sweep type."))
[docs]def an_tf(src, outp=None, outn=None, outcur=None):
"""
Generates the Ngspice simulator command that invokes the small signal
(TF) analysis.
*input* is the name of the independent voltage/current source that
generates the input signal.
*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.
"""
if outcur is not None and outp is None and outn is None:
outspec="i("+str(outcur)+")"
elif outcur is None and outp is not None and outn is None:
outspec="v("+str(outp)+")"
elif outcur is None and outp is not None and outn is not None:
outspec="v("+str(outp)+","+str(outn)+")"
else:
raise PyOpusError(DbgMsg("NGSI", "Bad output specification for TF analysis."))
return 'tf '+outspec+' '+src
[docs]def an_ac(start, stop, sweep, points):
"""
Generates the Ngspice 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 Ngspice ``ac sweep points start stop`` simulator command.
"""
if sweep == 'lin':
return 'ac lin '+str(points)+' '+str(start)+' '+str(stop)
elif sweep == 'dec':
return 'ac dec '+str(points)+' '+str(start)+' '+str(stop)
elif sweep == 'oct':
return 'ac oct '+str(points)+' '+str(start)+' '+str(stop)
else:
raise PyOpusError(DbgMsg("NGSI", "Bad sweep type."))
[docs]def an_tran(step, stop, start=0.0, maxStep=None, uic=False):
"""
Generates the Ngspice 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 Ngspice ``tran step stop start maxStep [uic]`` simulator
command.
"""
if uic:
if maxStep is None:
maxStep=step
return 'tran '+str(step)+" "+str(stop)+" "+str(start)+" "+str(maxStep)+" uic"
else:
if maxStep is None:
return 'tran '+str(step)+" "+str(stop)+" "+str(start)
else:
return '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 Ngspice 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* gives the number of points per summary (integrated noise) vector.
Equivalent of Ngspice
``noise outspec input sweep points start stop ptsSum``
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("NGSI", "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("NGSI", "Bad sweep type."))
return anstr+"\nsetplot previous"
[docs]class Ngspice(Simulator):
"""
A class for interfacing with the Ngspice simulator in batch mode.
*binary* is the path to the Ngspice simulator binary. If it is not given
the ``NGSPICEPATH`` environmental variable is used as the path to the
Ngspice binary. If ``NGSPICEPATH`` 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.
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
* ``tf`` - a reference to the :func:`an_tf` 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 NGSPICEPATH
environmental variable. If the binary is not found there
the system path is used.
"""
if 'NGSPICEPATH' in environ:
ngspicebinary=environ['NGSPICEPATH']
else:
if platform.system()=='Windows':
ngspicebinary=shutil.which("ngspice.exe")
else:
ngspicebinary=shutil.which("ngspice")
# Verify binary
if ngspicebinary is None:
return None
elif os.path.isfile(ngspicebinary):
return ngspicebinary
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,
'tf': an_tf,
'ac': an_ac,
'tran': an_tran,
'noise': an_noise,
'ipath': ipath,
'param': {},
}
# Default binary
if self.binary is None:
self.binary=Ngspice.findSimulator()
# Last resort - use local folder
if self.binary is None:
if platform.system()=='Windows':
self.binary=os.path.join(".", 'ngspice.exe')
else:
self.binary=os.path.join(".", 'ngspice')
# 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("NGSI", "Save directives must evaluate to a list of strings."))
for save in saveList:
if type(save) is not str:
raise PyOpusError(DbgMsg("NGSI", "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 ``set`` simulator command preceding its
corresponding analysis command.
Save directives are written as a series of ``save`` simulator commands.
Every analysis command is evaluated in its corresponding environment
taking into account the parameter values passed to the
:meth:`setInputParameters` method.
Every analysis is followed by a ``write`` command that stores the
results 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("NGSI", "Writing job group '"+str(i)+"' to file '"+fileName+"'")
with open(fileName, 'w') as f:
# First line
f.write('* Simulator input file for job group '+str(i)+'\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('.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');
f.write('.control\n')
f.write('unset *\n')
f.write('delete all\n\n')
f.write('set filetype=binary\n\n')
f.write('set sqrnoise\n\n')
# Handle analyses
for j in jobGroup:
# Get job
job=self.jobList[j]
# Get job name
if self.debug>0:
DbgMsgOut("NGSI", " 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')
# Delete old results and save directives.
# Do not unset old options.
# f.write('unset *\n')
f.write('destroy all\n')
f.write('delete all\n')
# Write options for analysis
if 'options' in job:
for (option, value) in job['options'].items():
if value is True:
f.write('set '+option+'\n')
elif value is False:
f.write('unset '+option+'\n')
else:
f.write('option '+option+'='+str(value)+'\n')
# Handle temperature parameter
# Because job parameters
if 'temperature' in job['params']:
f.write('option temp='+str(job['params']['temperature'])+'\n')
# Write saves for analysis
if 'saves' in job:
saves=self._createSaves(job['saves'], job['variables'])
count=0
for save in saves:
if count == 0:
f.write('save ')
f.write(save+' ')
count+=1
if count == 10:
count=0
f.write('\n')
f.write('\n')
# Prepare parameters dictionary for local namespace
self.analysisLocals['param'].clear()
self.analysisLocals['param'].update(analysisParams)
# Write analysis
f.write('echo Running '+str(job['name'])+'\n')
f.write(eval(job['command'], globals(), evalEnv)+'\n')
f.write('if $#plots gt 1\n set filetype=binary\n write '+self.simulatorID+'.job'+str(j)+'.'+job['name']+'.raw\nelse\n echo '+str(job['name'])+' analysis failed.\nend\n\n')
# Write quit - no need for it... it is sent to simulator's stdin
# f.write('set noaskquit\nquit\n')
# End control block
f.write('.endc\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("NGSI", "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')
except KeyboardInterrupt:
DbgMsgOut("NGSI", "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, '-b']+self.cmdline+[fileName]
if self.debug>3:
with open(fileName) as dbgf:
DbgMsgOut("NGSI", dbgf.read())
DbgMsgOut("NGSI", "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(
bytes('quit 0\n', 'utf-8'),
timeout=self.timeout
)
self.messages=msgs.decode("utf-8")
except subprocess.TimeoutExpired:
if self.debug>1:
DbgMsgOut("NGSI", "Simulation timeout")
p.kill()
msgs, _ = p.communicate()
self.messages=msgs.decode("utf-8")
spawnOK = False
#p.stdin.write(bytes('set noaskquit\nquit\n', 'utf-8'))
#p.stdin.flush()
# Collect output
#self.messages=p.stdout.read().decode("utf-8")
if self.debug>2:
DbgMsgOut("NGSI", self.messages)
elif self.debug>1:
DbgMsgOut("NGSI", 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.
# Somehow with ngspice we get retcode=1, ignore it and rely solely on
# the existence of raw file to confirm successful analysis.
# if retcode!=0:
# spawnOK=False
except KeyboardInterrupt:
DbgMsgOut("NGSI", "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("NGSI", " 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:
fileName=self.simulatorID+".job"+str(jobIndex)+'.'+job['name']+'.raw'
if self.debug>1:
DbgMsgOut("NGSI", "Reading results from '"+fileName+"'.")
try:
rawData=raw_read(
fileName
)
except:
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 NgspiceSimulationResults(
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]
#
# 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.
Jobs in a job group have:
* identical circuit definition,
* identical simulator parameter values (excluding temperature which is
actually a simulator option),
* identical simulator option lists, but not neccessarily identical
option values.
In other words: job group members are job indices of jobs that differ
only in simulator option values.
"""
# Move temperature to options. Raise an error if temp option is found.
# This way jobs that have different temperature but are otherwise joinable end up in the same group.
# Also add empty dictionaries for missing entries
for job in self.jobList:
for option in job['options'].keys():
if option.lower()=='temp':
raise PyOpusError(DbgMsg("NGSI", "TEMP option is not allowed. Use temperature parameter."))
if 'temperature' in job['params']:
job['options']['temp']=job['params']['temperature']
del job['params']['temperature']
# 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 all of the following holds
# - definitions are identical
# - parameters are identical
# - the list of options is identical, but not neccessarily the values
if (self.jobList[i1]['definitions']==self.jobList[i2]['definitions'] and
self.jobList[i1]['params']==self.jobList[i2]['params'] and
set(self.jobList[i1]['options'].keys())==set(self.jobList[i1]['options'].keys())):
# 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)
# Move temp option to parameters
for job in self.jobList:
if 'temp' in job['options']:
job['params']['temperature']=job['options']['temp']
del job['options']['temp']
return seq
[docs]class NgspiceSimulationResults(SimulationResults):
"""
Objects of this class hold Ngspice 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, 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.
Equivalent to Ngspice expression ``i(name)`` (also ``name#branch``).
"""
return self.vector("i("+name+")", 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 Ngspice expression ``@name[parameter]``
(or ``@name[parameter][index]``).
"""
if index is not None:
raise PyOpusError("Vector properties are not supported by Ngspice.")
name='@'+name+'['+parameter+']'
vec=self.vector_(name, resIndex)
if vec is not None:
return vec
vec=self.vector_("v("+name+")", resIndex)
if vec is not None:
return vec
vec=self.vector_("i("+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_spectrum', resIndex=resIndex)
elif reference=='output':
spec=self.vector('onoise_spectrum', resIndex=resIndex)
else:
raise PyOpusError("Bad noise reference.")
else:
# Partial spectrum
if reference=='input':
A=(
self.vector('onoise_spectrum', resIndex=resIndex) /
self.vector('inoise_spectrum', 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_("onoise_"+str(name), resIndex=resIndex)
if vec is None:
vec=self.vector_("onoise."+str(name), resIndex=resIndex)
if vec is None:
raise PyOpusError("Output noise contribution from '%s' not found." % str(name))
spec=vec/A
else:
vec=self.vector_("onoise_"+str(name)+"_"+str(contrib), resIndex=resIndex)
if vec is None:
vec=self.vector_("onoise."+str(name)+"."+str(contrib), 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,
}