Notes @nosent pyspice.py << head docstring>> << head >> << copyright >> << release notes >> v0.2 v0.1 structures todo << global imports >> << global vars >> Option processing options classes base classes class SpiceElement __init__ __str__ drop class Passive2NodeElement __init__ __str__ drop class Active2NodeElement __init__ __str__ class Active4NodeElement __init__ __str__ element classes class CommentLine __init__ class ControlElement __init__ class Capacitor __init__ isparallel combine class Inductor __init__ isparallel combine class Mosfet __init__ __str__ isparallel combine class Resistor __init__ class Vsource __init__ class Isource __init__ helpers unit debug info warning drop_2node write_2node read_netlist << docstring >> << imports >> class ElementError __init__ __str__ classify main @ These classes are used to break down the spectrum of SPICE elements into 'classes' of elements. I.e. 2 node passives, 2-node sources, 4-node sources, and so on. The elements found in a real netlist are based on these types. @c @ This is a(n incomplete) definition of the various SPICE elements. @c class Active2NodeElement(SpiceElement): """Base class for active 2-node elements. Assumes SPICE element line: xXXX n1 n2 value p1=val p2=val ... Inherits: None Redefines: None """ @others def __init__(self,line,num): if isinstance(line,str): line=line.split() self.line=line self.type='active2' self.num=num self.n1=_current_scope+line[1] self.n2=_current_scope+line[2] self.value=unit(line[3]) self.param=dict() #store x=y as dictionary for p in line[4:]: k,v=p.split('=') self.param[k]=unit(v) def __str__(self): s=StringIO() print>>s, self.line[0],self.n1,self.n2,self.value, for k,v in self.param.iteritems(): #are there instances when 0 is (in)significant? #if v==0: continue print>>s, k+'='+str(v), return wrapper.fill(s.getvalue()) class Vsource(Active2NodeElement): """Assumes SPICE element line: vXXX n1 n2 value p1=val p2=val ... """ @others def __init__(self,line,num): Active2NodeElement.__init__(self,line,num) self.type='vsource' class Isource(Active2NodeElement): """Assumes SPICE element line: iXXX n1 n2 value p1=val p2=val ... """ @others def __init__(self,line,num): Active2NodeElement.__init__(self,line,num) self.type='isource' class Active4NodeElement(SpiceElement): """Base class for active 4-node elements (xCyS). Assumes SPICE element line: xXXX n1 n2 value p1=val p2=val ... Inherits: None Redefines: None """ @others def __init__(self,line,num): if isinstance(line,str): line=line.split() self.line=line self.type='active4' self.num=num self.n1=_current_scope+line[1] self.n2=_current_scope+line[2] self.n3=_current_scope+line[3] self.n4=_current_scope+line[4] self.value=unit(line[5]) self.param=dict() #store x=y as dictionary for p in line[6:]: k,v=p.split('=') self.param[k]=unit(v) def __str__(self): s=StringIO() print>>s, self.line[0],self.n1,self.n2,self.n3,self.n4,self.value, for k,v in self.param.iteritems(): #are there instances when 0 is (in)significant? #if v==0: continue print>>s, k+'='+str(v), return wrapper.fill(s.getvalue()) Release Notes, changelog, whatever it turns out as... ----------------------------------------------------- @others pyspice v0.1: ------------- Initial release. Only worked for netlist containing MOSFETs and Capacitors. pyspice.py v0.2: ---------------- -At least default (pass through) handling of all element types. NOTE: For combining, this uses a global node name scheme. In other words: subcircuits, libraries, etc. are not in a separate node namespace as they should be, beware. -Changed structure of classes (in LEO), there are base classes that contain common attributes and element classes that define the specific behavior. -This version _should_ work with any netlist and only touch M's and C's, YMMV. -Work is ongoing on the class structure and most important IMO is getting netlist hierarchy implemented. """ << head >> << copyright >> << release notes >> @others """ @nocolor class SpiceElement: """Base class for SPICE elements. Methods: __init__(self,line,num) -> SpiceElement __str__(self) -> string spice line drop() -> False """ @others def __init__(self,line,num=None): """SpiceElement constructor line - netlist expanded line type - first 'word' num - netlist line number (for keeping roughly the same order when printing modified netlist) """ #accept lists of 'words' also; BE CAREFUL with this, though if isinstance(line,list): line=' '.join(line) self.line=line self.type=line[0] self.num=num def __str__(self): return wrapper.fill(self.line) def drop(self,val=0,mode='<'): """Template for dropping elements that defaults to NO if not overidden in the element class""" return False class CommentLine(SpiceElement): """SPICE Comment line (/^\*.*/) """ @others def __init__(self,line,num): SpiceElement.__init__(self,line,num) self.type='comment' class ControlElement(SpiceElement): """Control statement object, no processing for now. Note: this will eventially be a base class for the real control elements Note: currently has no knowledge of blocks (.lib/.endl, .subckt/.ends) has only ONE node namespace, make sure subckt's have unique node names! """ @others def __init__(self,line,num): SpiceElement.__init__(self,line,num) self.type='control' class Passive2NodeElement(SpiceElement): """Base class for 2-node elements. Assumes SPICE element line: xXXX n1 n2 value p1=val p2=val ... Inherits: None Redefines: drop(self,val,mode) -> bool """ @others def __init__(self,line,num): if isinstance(line,str): line=line.split() self.line=line self.type='passive2' self.num=num self.n1=_current_scope+line[1] self.n2=_current_scope+line[2] self.value=unit(line[3]) self.param=dict() #store x=y as dictionary for p in line[4:]: k,v=p.split('=') self.param[k]=unit(v) def __str__(self): s=StringIO() print>>s, self.line[0],self.n1,self.n2,self.value, for k,v in self.param.iteritems(): #are there instances when 0 is (in)significant? #if v==0: continue print>>s, k+'='+str(v), return wrapper.fill(s.getvalue()) def drop(self,val=0.0,mode='<'): """Indicate whether to drop the element from the list. Occurs iff (val 'mode' self.value) Can this be converted to specifying an arbitrary binary function? This may allow a more elegant comparison. e.g. mode=< instead of mode='<' or mode=cap_smaller(x,y) """ if mode=='<': if self.value<val: return True elif mode=='<=': if self.value<=val: return True elif mode=='>': if self.value>val: return True elif mode=='>=': if self.value>=val: return True else: return False return False #shouldn't get here, but... class Capacitor(Passive2NodeElement): """Assumes SPICE element line: cXXX n1 n2 value p1=val p2=val ... Provides: isparallel(other) combine(other) """ @others def __init__(self,line,num): Passive2NodeElement.__init__(self,line,num) self.type='capacitor' def isparallel(self,other): """Returns True if instance is parallel with other instance """ if self.n1==other.n1 and self.n2==other.n2: return True elif self.n1==other.n2 and self.n2==other.n1: return True else: return False def combine(self,other): """Adds values if capacitors are in parallel, returns True if it combined them. NOTE: Does not currently touch param dictionary when combining, just the values. How should this be done? Maybe combine iff params are identical to avoid problems? """ global _ncombine_capacitors if self.isparallel(other): self.value+=other.value _ncombine_capacitors+=1 return True else: return False class Mosfet(SpiceElement): """Mosfet constructor takes an array derived from the netlist line """ @others def __init__(self,line,num): if isinstance(line,str): line=line.split() self.line=line self.type='mosfet' self.num=num self.d=line[1] self.g=line[2] self.s=line[3] self.b=line[4] self.model=line[5] self.param=dict() for p in line[6:]: k,v=p.split('=') self.param[k]=unit(v) self.w=self.param['w'] self.l=self.param['l'] def __str__(self): s=StringIO() print>>s, self.line[0], self.d, self.g, self.s, self.b, self.model, for k,v in self.param.iteritems(): if v==0: continue print>>s, k+'='+str(v), return wrapper.fill(s.getvalue()) def isparallel(self,other): """Returns True if transistors are parallel """ # check gate, substrate, and model first if self.g==other.g and self.b==other.b and self.model==other.model: #source and drain can be reversed if self.d==other.d and self.s==other.s: return True elif self.d==other.s and self.s==other.d: return True else: return False else: return False def combine(self,other): """Combines adds other to self iff the transistors are identical, will NOT combine if W/L is different. Parameter 'M' is incremented on self, other is left alone. Returns True if it combined the transistors. Increments global _ncombine_mosfets for information. NOTE: This currently merely adds the parameters (except w, l, and m) without regard to their meaning. Here is the place to specially handle certain FET parameters. Average certain parameters? """ global _ncombine_mosfets if self.isparallel(other): #combine iff W/L (for original FET) is same also if self.w==other.w and self.l==other.l: for k,v in other.param.iteritems(): if k=='w' or k=='l' or k=='m': continue self.param[k]+=v if ('m' in self.param.keys()) or ('m' in other.param.keys()): #add other's M parameter or increment self.param['m']+=other.param.get('m',1) else: self.param['m']=2 _ncombine_mosfets+=1 return True else: return False class Resistor(Passive2NodeElement): """Assumes SPICE element line: rXXX n1 n2 value p1=val p2=val ... """ @others def __init__(self,line,num): Passive2NodeElement.__init__(self,line,num) self.type='resistor' def info(message): """Print information to stderr.""" print>>stderr,'Info:',message def debug(message): """Print debugging info to stderr.""" print>>stderr,'Debug:',message def warning(message,elm=None,num=None): """Print warning to stderr. If elm and num defined, print different message""" if elm and num: message=_opt.infile+":"+str(num)+" '"+elm+\ "' type not defined yet, passing through..." print>>stderr,'Warning:',message def drop_2node(elm,val,mode='<',type=None, verbose=True): """Drop 2-node elements in elm list according to val. mode = '<' | '>' type - print info on what and how many it dropped Note: only works correctly for capacitors for now """ new_elm=[] val=float(val) for i in elm: if mode=='<' and i.value>=val: new_elm.append(i) elif mode=='>' and i.value<=val: new_elm.append(i) else: continue #Print info about how many it dropped if verbose: infostr='Dropped '+str((len(elm)-len(new_elm)))+' ' if type and type[-1]=='s': info(infostr+type) elif type: info(infostr+type+'s') else: info(infostr+'elements') return new_elm ## # write 2-node SPICE elements to file # node pair is specified by key in dict: # "node0,node1" ## #NOTE: # this function is dying a slow, painful death. Each element is # getting its own custom __str__() method for writing to netlists. # def write_2node(elm, type=None, ofp=sys.stdout, comment=None): """Write 2-node SPICE elements to file elm - dictionary of elements type - SPICE element name ofp - output file pointer comment - comment string at head of elm list Note: "type" must begin with a SPICE element letter, the rest is printed as identifying information, e.g. linductor, capacitor, mosfet. """ if not type: raise SyntaxError('Must define a SPICE element name') i=1 if comment: print>>ofp,'\n**\n* '+comment+'\n**' for k,v in elm.iteritems(): node=k.split(',') print>>ofp, type[0]+str(i).rjust(3,'0'),node[0],node[1],v i+=1 infostr='Wrote '+str(i)+' '+type if type[-1]=='s': info(infostr) else: info(infostr+'s') ## # Read netlist from open file object # -make sure if reading from stdin to read only once and # use this function to make sure you read the entire netlist ## def read_netlist(fname): << docstring >> << imports >> if isinstance(fname,file): ifp=fname else: ifp=open(fname,'rU') # netlist=ifp.readlines() nline=0 lines=[] #raw expanded netlist # re_param=re.compile(r"(\S*)\s*=\s*(\S*)") for line in ifp: line=line.strip('\r\n') #pass through empty lines if not len(line.split()): #convert empty line to comment as a placeholder lines.append('*') nline+=1 continue #next please... #pass through comments, they stay asis elif line[0]=='*': lines.append(line) nline+=1 continue #next please... #case is unimportant in SPICE line=line.lower() #remove whitespace in parameter assignments # to prepare for x.split(' ') that happens later: # 'as = 3e-12' => 'as=3e-12' line=re.sub(re_param,r'\1=\2',line) if line[0]!='+': #beginning of SPICE line lines.append(line) nline+=1 else: #line continuation line=line[1:] lines[-1]=lines[-1]+line return lines class ElementError(LookupError): @others def __init__(self,elm='???'): self.elm=elm def __str__(self): return str('No class defined for this element: '+self.elm) def classify(net): """Reads expanded netlist and classifies each line, calls appropriate function to combine nodes. net - list of unwrapped SPICE netlist lines """ # Is there a better (faster) way of doing this? Maybe generating a dict # of handler functions and calling based on the first character. At any # rate, it would make the classification a constant-time operation. import string elements=dict() global _counts #initialize to all element types for ch in '*.'+string.lowercase: elements[ch]=[] _counts[ch]=0 num=-1 #line counter for line in net: num+=1 arr=line.split() x=arr[0][0] # Comment if x=='*': elements[x].append(CommentLine(line,num)) _counts[x]+=1 # Control line elif x=='.': elements[x].append(ControlElement(line,num)) _counts[x]+=1 elif x=='a': elements[x].append(SpiceElement(line,num)) warning('',elm=arr[0],num=num) _counts[x]+=1 elif x=='b': elements[x].append(SpiceElement(line,num)) warning('',elm=arr[0],num=num) _counts[x]+=1 # Capacitor elif x=='c': elm=Capacitor(line,num) #don't combine if it's the first encountered if _counts[x]==0: elements[x].append(elm) else: #combine if parallel or add if unique if elements[x][-1].combine(elm): pass else: elements[x].append(elm) _counts[x]+=1 elif x=='d': elements[x].append(SpiceElement(line,num)) warning('',elm=arr[0],num=num) _counts[x]+=1 elif x=='e': elements[x].append(SpiceElement(line,num)) warning('',elm=arr[0],num=num) _counts[x]+=1 elif x=='f': elements[x].append(SpiceElement(line,num)) warning('',elm=arr[0],num=num) _counts[x]+=1 elif x=='g': elements[x].append(SpiceElement(line,num)) warning('',elm=arr[0],num=num) _counts[x]+=1 elif x=='h': elements[x].append(SpiceElement(line,num)) warning('',elm=arr[0],num=num) _counts[x]+=1 # Current Source elif x=='i': elements[x].append(SpiceElement(line,num)) warning('',elm=arr[0],num=num) _counts[x]+=1 elif x=='j': elements[x].append(SpiceElement(line,num)) warning('',elm=arr[0],num=num) _counts[x]+=1 elif x=='k': elements[x].append(SpiceElement(line,num)) warning('',elm=arr[0],num=num) _counts[x]+=1 # Inductor elif x=='l': elm=Inductor(line,num) #don't combine if it's the first encountered if _counts[x]==0: elements[x].append(elm) else: #combine if parallel or add if unique if elements[x][-1].combine(elm): pass else: elements[x].append(elm) _counts[x]+=1 # Mosfet elif x=='m': elm=Mosfet(line,num) if _counts[x]==0: #don't combine if it's the first encountered elements[x].append(elm) mosfets.append(elm) else: #search list backwards to find a parallel one #in case they aren't adjacent, faster? i=len(elements[x])-1 while i>=0: if elements[x][i].combine(elm): break else: i-=1 #found unique MOSFET if i==-1: elements[x].append(elm) _counts['m']+=1 elif x=='n': elements[x].append(SpiceElement(line,num)) warning('',elm=arr[0],num=num) _counts[x]+=1 elif x=='o': elements[x].append(SpiceElement(line,num)) warning('',elm=arr[0],num=num) _counts[x]+=1 elif x=='p': elements[x].append(SpiceElement(line,num)) warning('',elm=arr[0],num=num) _counts[x]+=1 elif x=='q': elements[x].append(SpiceElement(line,num)) warning('',elm=arr[0],num=num) _counts[x]+=1 # Resistor elif x=='r': elm=Resistor(line,num) elements[x].append(elm) _counts['r']+=1 elif x=='s': elements[x].append(SpiceElement(line,num)) warning('',elm=arr[0],num=num) _counts[x]+=1 elif x=='t': elements[x].append(SpiceElement(line,num)) warning('',elm=arr[0],num=num) _counts[x]+=1 elif x=='u': elements[x].append(SpiceElement(line,num)) warning('',elm=arr[0],num=num) _counts[x]+=1 # Voltage Source elif x=='v': elements[x].append(SpiceElement(line,num)) warning('',elm=arr[0],num=num) _counts[x]+=1 elif x=='w': elements[x].append(SpiceElement(line,num)) warning('',elm=arr[0],num=num) _counts[x]+=1 # Subcircuit elif x=='x': elements[x].append(SpiceElement(line,num)) warning('',elm=arr[0],num=num) _counts[x]+=1 elif x=='y': elements[x].append(SpiceElement(line,num)) warning('',elm=arr[0],num=num) _counts[x]+=1 elif x=='z': elements[x].append(SpiceElement(line,num)) warning('',elm=arr[0],num=num) _counts[x]+=1 elif x=='+': raise Error('No line continuations (+) allowed.') else: raise Error('Encountered unknown element: '+line) #add 'special' elements to netlist #debugging/information header print>>ofp,'* Input Element counts' for k,v in _counts.iteritems(): if v!=0: print>>ofp, '*',k,'-',v #return classified netlist return elements ## # Fancy option processing ## def options(args=sys.argv): """Define options and parse argument list. """ global ifp, ofp usage="""%prog [options]""" desc="""This script reads a SPICE input files and processes it according to the options given. It is especially useful for processing netlists from the output of layout extractors by combining parallel C's and FET's. More features added upon request.""" parser = OptionParser(usage=usage,description=desc) parser.add_option('-i','--infile',dest='infile',default='stdin', help='Input SPICE file to be processed' ', (default: %default)') parser.add_option('-o','--outifle',dest='outfile',default='stdout', help='Output file for changes' ', (default: %default)') parser.add_option('-d','--dropcap',dest='dropcap', default='10', metavar='X', help='Drop all capacitors smaller than X fF' ', (default: %default)') parser.add_option('-v','--verbose',dest='v',action='store_true', default=True, help='Show info and debugging messages (default)') parser.add_option('-q','--quiet',dest='v',action='store_false', help='Suppress all messages on stderr') parser.add_option('-w','--linewidth',dest='linewidth', default=75, help='Max. line width for netlist (default: %default)') (opt, args) = parser.parse_args() #infile if opt.infile=='stdin': ifp=sys.stdin if opt.v: info('Input: stdin') else: try: ifp=open(opt.infile,'rU') #python's universal line ending mode if opt.v: info('Input: '+opt.infile) except IOError, (errno,strerror): print>>stderr, "IOError(%s): %s '%s'" % (errno,strerror,opt.infile) #outfile if opt.outfile=='stdout': ofp=sys.stdout if opt.v: info('Output: stdout') else: try: ofp=open(opt.outfile,'w') if opt.v: info('Output: '+opt.outfile) except IOError, (errno,strerror): print>>stderr, "IOError(%s): %s '%s'" % (errno,strerror,opt.infile) #dropcap opt.dropcap=float(opt.dropcap) opt.dropcap=opt.dropcap*1e-15 if opt.v: info('Dropping caps < '+str(opt.dropcap)+' F') #return the option object return opt #### # # test code # #### def main(): global _opt opt = options() _opt=opt import textwrap #set line continuation style and max width wrapper=textwrap.TextWrapper(subsequent_indent='+ ',width=opt.linewidth) print>>ofp,"* pyspice.py: by Dan White <etihwnad at gmail dot com>" print>>ofp,"* mail me bug reports, fixes, and comments if you find this useful" print>>ofp,"* ----------------------------------------------------------------" netlist = read_netlist(ifp) nlist_new=classify(netlist) info('Combined %i parallel capacitors' % _ncombine_capacitors) info('Combined %i parallel mosfets' % _ncombine_mosfets) nlist_new['c']=drop_2node(nlist_new['c'], opt.dropcap, type='capacitor') #create a new list of netlist objects #in order of first appearance all=[] print '*\n* Output Element counts' for type in nlist_new.keys(): num=len(nlist_new[type]) if num: print '*',type,'-',num for elm in nlist_new[type]: all.append(elm) #python2.4 specific operation #sort netlist by order of appearance in orig. netlist all.sort(cmp=lambda x,y: cmp(x.num,y.num)) for x in all: print>>ofp,x Copyright Dan White 2006 Licensed by the GPL, see http://www.whiteaudio.com/soft/COPYING or the current GNU GPL license for details. pyspice.py v0.2 SPICE pre-processor that combines parallel elements (e.g. capacitors, mosfets) for GREATLY reduced simulation time. Uses the 'M' parameter of MOSFETS when combining parallel fets. Makes simulating fingered transistors easier and faster. This module is in a constant state of flux because I am using and modifying it for my needs. Combine parallel capacitors Drop combined caps smaller than X fF Usage: pyspice.py [options] [-i infile] [-o outfile] Use pyspice.py -h for all the options. Data structures: (outdated) lines - list of tuples: (original_line, line.split(), line_type) caps - dictionary of node pairs and capacitance between nodes TODO: xpreserve comments in position -element class definitions xinductor xv-source xi-source -e-source (VCVS) -preserve node namespaces (within subckts, libraries, etc.) -find illegal SPICE node name character to prepend namespace e.g. @sub1.node1 -handle control statements specially -.model -.dc, .ac, .tran -.lib/.endl handling -.meas -.print -.probe -.param -.option -.global -.ic -.subckt/.ends -.prot/.unprot -.alter blocks -.end -others? -option to find/evaluate .param statements? -HSPICE takes params, ngspice doesn't -logic to find when an explicit number is specified (e.g. 10k) or when it is a parameter (e.g. 'value') -create dict of .params and use as substitution keys in: k=v model parameters node names element values (Rxx n1 n2 'value'; Cxx n1 n2 'fc/2') others? -option to make a flat output (inline everything): .lib statements .include statements .model statements others? -store node dictionary to keep track of elements connected to that node -allows tracking of R-C nodes to drop C or R based on time constant -allows more sophisticated dropping/combining with series elements specifically R+R+R+R chains -> equivalent R+- (roughly) for faster sims C C C C ## # Global Shorthands ## stderr=sys.stderr #set line continuation style and max width wrapper=textwrap.TextWrapper(subsequent_indent='+ ',width=75) #not used? #element lists _defined_types=['comment', 'control', 'capacitor', 'mosfet', 'resistor'] capacitors=[] resistors=[] mosfets=[] #end not used? #global variables _counts=dict() #keyed by spice element letter _ncombine_capacitors=0 _ncombine_inductors=0 _ncombine_mosfets=0 _ncombine_res=0 #namespace tracking # -not implemented yet _current_scope='' import sys, getopt, textwrap, warnings from optparse import OptionParser import re try: from cStringIO import StringIO as StringIO except: from StringIO import StringIO as StringIO @first #!/usr/bin/env python @language python @tabwidth -4 << head docstring >> << global imports >> << global vars >> @ Debug options: string of debugging elements to print * - comments follow SPICE element names (c-capacitor, l-inductor) @c dbg='' @others #magic script-maker if __name__ == '__main__': main() """Read a SPICE netlist from the open file pointer read_netlist(filename) -> array lines Returns a list of expanded lines (without continuation '+') Keeps case of comments, all other lines are lowercased return: netlist (list) of SPICE netlist lines """ import re def unit(s): """Takes a string and returns the equivalent float. '3.0u' -> 3.0e-6""" mult={'t':1.0e12, 'g':1.0e9, 'meg':1.0e6, 'k':1.0e3, 'mil':25.4e-6, 'm':1.0e-3, 'u':1.0e-6, 'n':1.0e-9, 'p':1.0e-12, 'f':1.0e-15} m=re.search('^([0-9e\+\-\.]+)(t|g|meg|k|mil|m|u|n|p|f)?',s.lower()) if m.group(2): return float(m.group(1))*mult[m.group(2)] else: return float(m.group(1)) class Inductor(Passive2NodeElement): """Assumes SPICE element line: cXXX n1 n2 value p1=val p2=val ... Provides: isparallel(other) combine(other) """ @others def __init__(self,line,num): Passive2NodeElement.__init__(self,line,num) self.type='inductor' def isparallel(self,other): """Returns True if instance is parallel with other instance """ if self.n1==other.n1 and self.n2==other.n2: return True elif self.n1==other.n2 and self.n2==other.n1: return True else: return False def combine(self,other): """Combines values if inductors are in parallel, returns True if it combined them. NOTE: -Does not currently touch param dictionary when combining, just the values. How should this be done? """ global _ncombine_inductors if self.isparallel(other): self.value=(self.value*other.value)/(self.value+other.value) _ncombine_inductors+=1 return True else: return False