import collections
import re
import logging
from .model import Model
from .subcircuit import Circuit, SubCircuit
from .instance import Instance
from .types import set_context
from .library import Library
from . import constraint
logger = logging.getLogger(__name__)
unit_multipliers = {
'T': 1e12,
'G': 1e9,
'X': 1e6,
'MEG': 1e6,
'K': 1e3,
'M': 1e-3,
'U': 1e-6,
'N': 1e-9,
'P': 1e-12,
'F': 1e-15
}
[docs]def str2float(val):
unit = next((x for x in unit_multipliers if val.endswith(x.upper()) or val.endswith(x.lower())), None)
numstr = val if unit is None else val[:-1*len(unit)]
return float(numstr) * unit_multipliers[unit] if unit is not None else float(numstr)
# Token specification
modifiers = '|'.join(unit_multipliers.keys())
numericval = fr'[+-]?(?:0|[1-9]\d*)(?:[.]\d+)?(?:E[+\-]?\d+)?(?:{modifiers})?'
identifier = r'[^\s{}()=;*]+'
operator = r'\s*[*+-/%]\s*'
exprcontent = fr'(?:{numericval}|{identifier})(?:{operator}(?:{numericval}|{identifier}))*'
commentchars = r'(?:[;$]|//)'
token_re_map = {
'ANNOTATION': fr'(^|\s)*(\*|{commentchars})+\s*\@:\s*[^\n\r]*',
'NLCOMMENT': r'(^|[\n\r])+\*[^\n\r]*',
'COMMENT': fr'(^|\s)*{commentchars}[^\n\r]*',
'CONTINUE': r'(^|[\n\r])+\+',
'CONTINUEBACKSLASH': r'\\\s*[\n\r]',
'NEWL': r'[\n\r]+',
'EQUALS': r'\s*=\s*',
'EXPR': fr"""(?P<quote>['"]){exprcontent}(?P=quote)|({{){exprcontent}(}})""",
'NUMBER': numericval + fr'(?=\s|\Z|{commentchars})',
'DECL': fr'\.{identifier}',
'NAME': identifier,
'WS': r'\s+'}
spice_pat = re.compile('|'.join(fr'(?P<{x}>{y})' for x, y in token_re_map.items()), flags=re.IGNORECASE)
constraint_dict = {x: getattr(constraint, x) for x in dir(constraint) if not x.startswith('_')}
# Tokenizer
Token = collections.namedtuple('Token', ['type', 'value'])
[docs]class SpiceParser:
_context = []
def __init__(self, library=None, mode='Xyce'):
self.mode = mode.lower()
assert self.mode in ('xyce', 'hspice')
self.library = Library(loadbuiltins=True) if library is None else library
with set_context(self.library):
self.circuit = Circuit()
self._scope = [self.circuit]
@staticmethod
def _generate_tokens(text):
scanner = spice_pat.scanner(text)
for m in iter(scanner.match, None):
tok = Token(m.lastgroup, m.group())
if tok.type in ['EXPR', 'NUMBER', 'DECL', 'NAME', 'NEWL', 'EQUALS', 'ANNOTATION']:
yield tok
[docs] def parse(self, text):
cache = []
self._constraints = None
for tok in self._generate_tokens(text):
if tok.type == 'NEWL':
self._dispatch(cache)
cache.clear()
elif tok.type == 'ANNOTATION':
self._dispatch(cache)
cache.clear()
self._queue_constraint(tok.value)
else:
cache.append(tok)
self._dispatch(cache)
def _dispatch(self, cache):
if len(cache) == 0:
return
token = cache.pop(0)
args, kwargs = self._decompose(cache)
if token.type == 'DECL':
self._process_declaration(token.value.upper(), args, kwargs)
elif token.type == 'NAME':
self._process_instance(token.value.upper(), args, kwargs)
else:
assert False
def _queue_constraint(self, annotation):
constraint = annotation.split('@:')[1].strip()
assert self._constraints is not None, \
f'Constraint {constraint} can only be defined within a .SUBCKT \nCurrent scope:{self._scope[-1]}'
self._constraints.append(constraint)
@staticmethod
def _decompose(cache):
assert all(x.type in ('NAME', 'NUMBER', 'EXPR', 'EQUALS') for x in cache), cache
assignments = {i for i, x in enumerate(cache) if x.type == 'EQUALS'}
assert all(cache[i-1].type == 'NAME' for i in assignments)
args = [SpiceParser._cast(x.value.upper(), x.type) for i, x in enumerate(cache) if len(assignments.intersection({i-1, i, i+1})) == 0]
kwargs = {cache[i-1].value.upper(): SpiceParser._cast(cache[i+1].value.upper(), cache[i+1].type) for i in assignments}
return args, kwargs
@staticmethod
def _cast(val, ty='NUMBER'):
if ty == 'EXPR':
return val[1:-1]
elif ty == 'NAME':
return val
# Attempt to cast number to float
try:
val = str2float(val)
except ValueError:
return val
# Cast to int if possible
return int(val) if val.is_integer() else val
def _process_instance(self, name, args, kwargs):
defaults = {'C': 'CAP', 'R': 'RES', 'L': 'IND'}
if any(name.startswith(x) for x in ('C', 'R', 'L')):
model = defaults[name[0]]
if not kwargs:
kwargs['VALUE'] = args.pop()
else: # to allow cap/res parameters
model = args.pop()
else:
model = args.pop()
if self.library.find(model):
model = self.library.find(model)
else:
logger.warning(f"Unknown device found {model} {kwargs}, creating a generic model for this")
with set_context(self.library):
# Use generic pin names
pins = [f"p{i}" for i in range(len(args))]
self.library.append(Model(name=model, pins=pins, parameters={k: '1' for k in kwargs.keys()}, prefix=''))
model = self.library.find(model)
assert model is not None, (model, name, args, kwargs)
assert len(args) == len(model.pins), \
f"Model {model.name} has {len(model.pins)} pins {model.pins}. " \
+ f"{len(args)} nets {args} were passed when instantiating {name}."
pins = {pin: net for pin, net in zip(model.pins, args)}
with set_context(self._scope[-1].elements):
try:
self._scope[-1].elements.append(Instance(name=name, model=model.name,
pins=pins, parameters=kwargs
))
except ValueError:
assert False, f"could not identify device parameters {name} {kwargs} \
allowed parameters of model {model.name} are {model.parameters}"
def _process_constraints(self):
with set_context(self._scope[-1].constraints):
for const in self._constraints:
self._scope[-1].constraints.append(eval(const, {}, constraint_dict))
self._constraints = None
def _process_declaration(self, decl, args, kwargs):
if decl == '.SUBCKT':
self._constraints = []
name = args.pop(0)
assert not isinstance(self.library.find(name), SubCircuit), f"User is attempting to redeclare subcircuit {name}"
with set_context(self.library):
subckt = SubCircuit(name=name, pins=args, parameters=kwargs)
self.library.append(subckt)
self._scope.append(subckt)
elif decl == '.ENDS':
self._process_constraints()
self._scope.pop()
elif decl == '.PARAM':
assert len(args) == 0, f"unsupported arguments {args}, probably missing default values"
self._scope[-1].parameters.update({
k.upper(): str(v).upper() for k, v in kwargs.items()
})
elif decl == '.MODEL':
assert len(args) == 2, args
name, base = args[0], args[1]
assert self.library.find(name) is None, f"User is attempting to redeclare {name}"
assert self.library.find(base) is not None, f"Base model {base} not found for model {name}"
with set_context(self.library):
self.library.append(Model(name=name, base=base, parameters=kwargs))