Source code for PySpice.Spice.Parser_jmgc

####################################################################################################
#
# PySpice - A Spice Package for Python
# Copyright (C) 2020 jmgc / Fabrice Salvaire
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
####################################################################################################

####################################################################################################

"""This module implements a partial SPICE netlist parser.

See the :command:`cir2py` tool for an example of usage of the parser.

It would be difficult to implement a full parser for Ngspice since the syntax is mainly contextual.

SPICE is case insensitive.

"""

####################################################################################################

from collections import OrderedDict
import logging
import os
import regex

####################################################################################################

from .ElementParameter import FlagParameter
from .Netlist import ElementParameterMetaClass, Circuit, SubCircuit

####################################################################################################

_module_logger = logging.getLogger(__name__)

####################################################################################################

[docs]class ParseError(NameError): pass
####################################################################################################
[docs]class PrefixData: """This class represents a device prefix.""" ############################################## def __init__(self, prefix, classes): self.prefix = prefix self.classes = classes number_of_positionals_min = 1000 number_of_positionals_max = 0 has_optionals = False for element_class in classes: number_of_positionals = element_class.number_of_positional_parameters number_of_positionals_min = min(number_of_positionals_min, number_of_positionals) number_of_positionals_max = max(number_of_positionals_max, number_of_positionals) has_optionals = max(has_optionals, bool(element_class.optional_parameters)) self.number_of_positionals_min = number_of_positionals_min self.number_of_positionals_max = number_of_positionals_max self.has_optionals = has_optionals self.multi_devices = len(classes) > 1 self.has_variable_number_of_pins = prefix in ('Q', 'X') # NPinElement, Q has 3 to 4 pins if self.has_variable_number_of_pins: self.number_of_pins = None else: # Q and X are single self.number_of_pins = classes[0].number_of_pins self.has_flag = False for element_class in classes: for parameter in element_class.optional_parameters.values(): if isinstance(parameter, FlagParameter): self.has_flag = True ############################################## def __len__(self): return len(self.classes) ############################################## def __iter__(self): return iter(self.classes) ############################################## @property def single(self): if not self.multi_devices: return self.classes[0] else: raise NameError()
#################################################################################################### _prefix_cache = {} for prefix, classes in ElementParameterMetaClass._classes.items(): prefix_data = PrefixData(prefix, classes) _prefix_cache[prefix] = prefix_data _prefix_cache[prefix.lower()] = prefix_data # for prefix_data in sorted(_prefix_cache.values(), key=lambda x: len(x)): # print(prefix_data.prefix, # len(prefix_data), # prefix_data.number_of_positionals_min, prefix_data.number_of_positionals_max, # prefix_data.has_optionals) # Single: # B 0 True # D 1 True # F 2 False # G 1 False # H 2 False # I 1 False # J 1 True # K 3 False # M 1 True # S 2 False # V 1 False # W 3 False # Z 1 True # Two: # E 0 1 False # L 1 2 True # Three: # C 1 2 True # R 1 2 True # NPinElement: # Q 1 1 True # X 1 1 False ####################################################################################################
[docs]class Statement: """This base class implements a statement, in fact a line in a Spice netlist.""" ############################################## def __init__(self, line, statement=None): self._line = line if statement is not None: self._line.lower_case_statement(statement) ############################################## def __repr__(self): return '{} {}'.format(self.__class__.__name__, repr(self._line)) ##############################################
[docs] def value_to_python(self, x): if x: if str(x)[0].isdigit(): return str(x) else: return "'{}'".format(x) else: return ''
##############################################
[docs] def values_to_python(self, values): return [self.value_to_python(x) for x in values]
##############################################
[docs] def kwargs_to_python(self, kwargs): return ['{}={}'.format(key, self.value_to_python(value)) for key, value in kwargs.items()]
##############################################
[docs] def join_args(self, args): return ', '.join(args)
####################################################################################################
[docs]class Comment(Statement): pass
####################################################################################################
[docs]class Title(Statement): """This class implements a title definition.""" ############################################## def __init__(self, line): super().__init__(line, statement='title') self._title = self._line.right_of('.title') ############################################## def __str__(self): return self._title ############################################## def __repr__(self): return 'Title {}'.format(self._title)
####################################################################################################
[docs]class Include(Statement): """This class implements a include definition.""" ############################################## def __init__(self, line): super().__init__(line, statement='include') self._include = self._line.right_of('.include') ############################################## def __str__(self): return self._include ############################################## def __repr__(self): return 'Include {}'.format(self._include) ##############################################
[docs] def to_python(self, netlist_name): return '{}.include({})'.format(netlist_name, self._include) + os.linesep
####################################################################################################
[docs]class Model(Statement): """This class implements a model definition. Spice syntax:: .model mname type(pname1=pval1 pname2=pval2 ... ) """ ############################################## def __init__(self, line): super().__init__(line, statement='model') base, self._parameters = line.split_keyword('.model') self._name, self._model_type = base self._name = self._name.lower() ############################################## @property def name(self): """Name of the model""" return self._name ############################################## def __repr__(self): return 'Model {} {} {}'.format(self._name, self._model_type, self._parameters) ##############################################
[docs] def to_python(self, netlist_name): args = self.values_to_python((self._name, self._model_type)) kwargs = self.kwargs_to_python(self._parameters) return '{}.model({})'.format(netlist_name, self.join_args(args + kwargs)) + os.linesep
##############################################
[docs] def build(self, circuit): return circuit.model(self._name, self._model_type, **self._parameters)
####################################################################################################
[docs]class Parameter(Statement): """This class implements a parameter definition. Spice syntax:: .param name=expr """ ############################################## def __init__(self, line): super().__init__(line, statement='param') text = line.right_of('.param').strip().lower() # Fixme: lower ??? idx = text.find('=') self._name = text[:idx].strip() self._value = text[idx + 1:].strip() ############################################## @property def name(self): """Name of the model""" return self._name ############################################## def __repr__(self): return 'Param {}={}'.format(self._name, self._value) ##############################################
[docs] def to_python(self, netlist_name): args = self.values_to_python((self._name, self._value)) # Fixme: linesep here ??? return '{}.param({})'.format(netlist_name, self.join_args(args)) + os.linesep
##############################################
[docs] def build(self, circuit): circuit.parameter(self._name, self._value)
#################################################################################################### # Review: HERE
[docs]class CircuitStatement(Statement): # Review: jmgc """This class implements a circuit definition. Spice syntax:: Title ... """ ############################################## def __init__(self, title): super().__init__(title, statement='title') # Review: Title title_statement = '.title ' self._title = str(title) if self._title.startswith(title_statement): self._title = self._title[len(title_statement):] self._statements = [] self._subcircuits = [] self._models = [] self._required_subcircuits = set() self._required_models = set() self._params = [] ############################################## @property def title(self): """Title of the circuit.""" return self._title @property def name(self): """Name of the circuit.""" return self._title @property def models(self): """Models of the circuit.""" return self._models @property def subcircuits(self): """Subcircuits of the circuit.""" return self._subcircuits @property def params(self): """Parameters of the circuit.""" return self._params ############################################## def __repr__(self): text = 'Circuit {}'.format(self._title) + os.linesep text += os.linesep.join([repr(model) for model in self._models]) + os.linesep text += os.linesep.join([repr(subcircuit) for subcircuit in self._subcircuits]) + os.linesep text += os.linesep.join([' ' + repr(statement) for statement in self._statements]) return text ############################################## def __iter__(self): """Return an iterator on the statements.""" return iter(self._models + self._subcircuits + self._statements) ##############################################
[docs] def append(self, statement): """Append a statement to the statement's list.""" self._statements.append(statement)
##############################################
[docs] def append_model(self, statement): """Append a model to the statement's list.""" self._models.append(statement)
##############################################
[docs] def append_param(self, statement): """Append a param to the statement's list.""" self._params.append(statement)
##############################################
[docs] def append_subcircuit(self, statement): """Append a subcircuit to the statement's list.""" self._subcircuits.append(statement)
##############################################
[docs] def to_python(self, ground=0): subcircuit_name = 'subcircuit_' + self._name args = self.values_to_python([subcircuit_name] + self._nodes) source_code = '' source_code += '{} = SubCircuit({})'.format(subcircuit_name, self.join_args(args)) + os.linesep source_code += SpiceParser.netlist_to_python(subcircuit_name, self, ground) return source_code
##############################################
[docs] def build(self, ground=0): circuit = Circuit(self._title) for statement in self._params: statement.build(circuit) for statement in self._models: model = statement.build(circuit) for statement in self._subcircuits: subckt = statement.build(ground) # Fixme: ok ??? circuit.subcircuit(subckt) for statement in self._statements: if isinstance(statement, Element): statement.build(circuit, ground) return circuit
####################################################################################################
[docs]class SubCircuitStatement(Statement): """This class implements a sub-circuit definition. Spice syntax:: .SUBCKT name node1 ... param1=value1 ... """ ############################################## def __init__(self, line): super().__init__(line, statement='subckt') # Fixme parameters, dict_parameters = self._line.split_keyword('.subckt') # Review: syntax ??? if parameters[-1].lower() == 'params:': parameters = parameters[:-1] self._name, self._nodes = parameters[0], parameters[1:] self._name = self._name.lower() self._parameters = dict_parameters self._statements = [] self._subcircuits = [] self._models = [] self._required_subcircuits = set() self._required_models = set() self._params = [] ############################################## @property def name(self): """Name of the sub-circuit.""" return self._name @property def nodes(self): """Nodes of the sub-circuit.""" return self._nodes @property def models(self): """Models of the sub-circuit.""" return self._models @property def params(self): """Params of the sub-circuit.""" return self._params @property def subcircuits(self): """Subcircuits of the sub-circuit.""" return self._subcircuits ############################################## def __repr__(self): if self._parameters: text = 'SubCircuit {} {} Parameters: {}'.format(self._name, self._nodes, self._parameters) + os.linesep else: text = 'SubCircuit {} {}'.format(self._name, self._nodes) + os.linesep text += os.linesep.join([repr(model) for model in self._models]) + os.linesep text += os.linesep.join([repr(subcircuit) for subcircuit in self._subcircuits]) + os.linesep text += os.linesep.join([' ' + repr(statement) for statement in self._statements]) return text ############################################## def __iter__(self): """Return an iterator on the statements.""" return iter(self._models + self._subcircuits + self._statements) ##############################################
[docs] def append(self, statement): """Append a statement to the statement's list.""" self._statements.append(statement)
##############################################
[docs] def append_model(self, statement): """Append a model to the statement's list.""" self._models.append(statement)
##############################################
[docs] def append_param(self, statement): """Append a param to the statement's list.""" self._params.append(statement)
##############################################
[docs] def append_subcircuit(self, statement): """Append a model to the statement's list.""" self._subcircuits.append(statement)
##############################################
[docs] def to_python(self, ground=0): subcircuit_name = 'subcircuit_' + self._name args = self.values_to_python([subcircuit_name] + self._nodes) source_code = '' source_code += '{} = SubCircuit({})'.format(subcircuit_name, self.join_args(args)) + os.linesep source_code += SpiceParser.netlist_to_python(subcircuit_name, self, ground) return source_code
##############################################
[docs] def build(self, ground=0, parent=None): subcircuit = SubCircuit(self._name, *self._nodes, **self._parameters) subcircuit.parent = parent for statement in self._params: statement.build(subcircuit) for statement in self._models: model = statement.build(subcircuit) for statement in self._subcircuits: subckt = statement.build(ground, parent=subcircuit) # Fixme: ok ??? subcircuit.subcircuit(subckt) for statement in self._statements: if isinstance(statement, Element): statement.build(subcircuit, ground) return subcircuit
####################################################################################################
[docs]class Element(Statement): """This class implements an element definition. "{ expression }" are allowed in device line. """ _logger = _module_logger.getChild('Element') ############################################## def __init__(self, line): super().__init__(line) line_str = str(line) # self._logger.debug(os.linesep + line_str) # Retrieve device prefix prefix = line_str[0] if prefix.isalpha(): self._prefix = prefix else: raise ParseError("Not an element prefix: " + prefix) prefix_data = _prefix_cache[self._prefix] # Retrieve device name args, kwargs = line.split_element(prefix) self._name = args.pop(0) self._nodes = [] self._parameters = [] self._dict_parameters = {} # Read nodes if not prefix_data.has_variable_number_of_pins: number_of_pins = prefix_data.number_of_pins if number_of_pins: self._nodes = args[:number_of_pins] args = args[number_of_pins:] else: # Q or X if prefix_data.prefix == 'Q': self._nodes = args[:3] args = args[3:] # Fixme: optional node else: # X if args[-1].lower() == 'params:': args.pop() self._parameters.append(args.pop()) self._nodes = args args = [] # Read positionals number_of_positionals = prefix_data.number_of_positionals_min if number_of_positionals and (len(args) > 0) and (prefix_data.prefix != 'X'): # model is optional self._parameters = args[:number_of_positionals] args = args[number_of_positionals:] if prefix_data.multi_devices and (len(args) > 0): remaining = args args = [] self._parameters.extend(remaining) if prefix_data.prefix in ('V', 'I') and (len(args) > 0): # merge remaining self._parameters[-1] += " " + " ".join(args) self._dict_parameters = kwargs # Read optionals if (prefix_data.has_optionals or (prefix_data.prefix == 'X')) and (len(kwargs) > 0): for key in kwargs: self._dict_parameters[key] = kwargs[key] if prefix_data.multi_devices: for element_class in prefix_data: if len(self._parameters) == element_class.number_of_positional_parameters: break else: element_class = prefix_data.single self.factory = element_class # Move positionals passed as kwarg to_delete = [] for parameter in element_class.positional_parameters.values(): if parameter.key_parameter: idx = parameter.position if idx < len(self._parameters): self._dict_parameters[parameter.attribute_name] = self._parameters[idx] to_delete.append(idx - len(to_delete)) for idx in to_delete: self._parameters.pop(idx) # self._logger.debug(os.linesep + self.__repr__()) ############################################## @property def name(self): """Name of the element""" return self._name ############################################## def __repr__(self): return 'Element {0._prefix} {0._name} {0._nodes} {0._parameters} {0._dict_parameters}'.format(self) ##############################################
[docs] def translate_ground_node(self, ground): nodes = [] for node in self._nodes: if str(node) == str(ground): node = 0 nodes.append(node) return nodes
##############################################
[docs] def to_python(self, netlist_name, ground=0): nodes = self.translate_ground_node(ground) args = [self._name] if self._prefix != 'X': args += nodes + self._parameters else: # != Spice args += self._parameters + nodes args = self.values_to_python(args) kwargs = self.kwargs_to_python(self._dict_parameters) return '{}.{}({})'.format(netlist_name, self._prefix, self.join_args(args + kwargs)) + os.linesep
############################################## def _check_params(self, elements=1): params = [] for param in self._parameters: values = param.replace(',', ' ') if values[0] == '(' and values[-1] == ')': values = values[1: -1].split() if len(values) > elements: raise IndexError('Incorrect number of elements for (%r): %s' % (self, param)) params.extend(values) else: params.extend(values.split()) self._parameters = params ############################################## def _voltage_controlled_nodes(self, poly_arg): result = ['v(%s,%s)' % nodes for nodes in zip(self._parameters[:(2 * poly_arg):2], self._parameters[1:(2 * poly_arg):2])] result += self._parameters[2 * poly_arg:] return ' '.join(result) ############################################## def _current_controlled_nodes(self, poly_arg): result = ['i(%s)' % node for node in self._parameters[:poly_arg]] result += self._parameters[poly_arg:] return ' '.join(result) ############################################## def _manage_controlled_sources(self, nodes): try: idx = self._nodes.index('POLY') if idx == 2: poly_arg = self._nodes[3] if poly_arg[0] == '(' and poly_arg[-1] == ')': poly_arg = poly_arg[1:-1] try: poly_arg = int(poly_arg) except TypeError as te: raise TypeError('Not valid poly argument: %s' % poly_arg, te) self._nodes = self._nodes[:2] nodes = nodes[:2] if self._prefix in 'EG': self._check_params(2) values = self._voltage_controlled_nodes(poly_arg) if self._prefix == 'E': key = 'v' else: key = 'i' else: self._check_params(1) values = self._current_controlled_nodes(poly_arg) if self._prefix == 'F': key = 'v' else: key = 'i' poly_str = '{ POLY (%d) %s }' % (poly_arg, values) self._dict_parameters[key] = poly_str self._parameters.clear() self._name = self._prefix + self._name self._prefix = 'B' prefix_data = _prefix_cache[self._prefix] self.factory = prefix_data.single return nodes raise IndexError('Incorrect position of POLY: %r' % self) except ValueError: pass _correction = [] correction = [] for _node, node in zip(self._nodes, nodes): _values = _node.replace(',', ' ') try: values = node.replace(',', ' ') except AttributeError: values = str(node) if _values[0] == '(' and _values[-1] == ')': _values = _values[1: -1] if values[0] == '(' and values[-1] == ')': values = values[1: -1] _correction.extend(_values.split()) correction.extend(values.split()) self._parameters = correction[len(self._nodes):] + self._parameters self._nodes = _correction[:len(self._nodes)] parameters = self._parameters correction = correction[:len(self._nodes)] if self._prefix in 'EG': if len(correction) + len(parameters) == 5: parameters = correction[2:] + parameters self._nodes = _correction[:2] value = '{v(%s, %s) * %s}' % tuple(parameters) if self._prefix == 'E': key = 'v' else: key = 'i' self._dict_parameters[key] = value self._parameters.clear() self._name = self._prefix + self._name self._prefix = 'B' prefix_data = _prefix_cache[self._prefix] self.factory = prefix_data.single else: if len(correction) + len(parameters) == 4: parameters = correction[2:] + parameters self._nodes = _correction[:2] value = '{i(%s) * %s}' % tuple(parameters) if self._prefix == 'F': key = 'v' else: key = 'i' self._dict_parameters[key] = value self._parameters.clear() self._name = self._prefix + self._name self._prefix = 'B' prefix_data = _prefix_cache[self._prefix] self.factory = prefix_data.single return correction[:len(self._nodes)] ##############################################
[docs] def build(self, circuit, ground=0): nodes = self.translate_ground_node(ground) if self._prefix != 'X': if self._prefix in ('EFGH'): nodes = self._manage_controlled_sources(nodes) args = nodes + self._parameters else: # != Spice args = self._parameters + nodes factory = getattr(circuit, self.factory.__alias__) kwargs = self._dict_parameters message = ' '.join([str(x) for x in (self._prefix, self._name, args, self._dict_parameters)]) self._logger.debug(message) return factory(self._name, *args, **kwargs)
####################################################################################################
[docs]class Line: """This class implements a line in the netlist.""" _logger = _module_logger.getChild('Line') ############################################## def __init__(self, line, line_range, end_of_line_comment): self._end_of_line_comment = end_of_line_comment text, comment, self._is_comment = self._split_comment(line) self._text = text self._comment = comment self._line_range = line_range ############################################## def __repr__(self): return '{0._line_range}: {0._text} // {0._comment}'.format(self) ############################################## def __str__(self): return self._text ############################################## @property def comment(self): return self._comment @property def is_comment(self): return self._is_comment ############################################## def _split_comment(self, line): line = str(line) if line.startswith('*'): is_comment = True text = '' comment = line[1:].strip() else: is_comment = False # remove end of line comment location = -1 for marker in self._end_of_line_comment: _location = line.find(marker) if _location != -1: if location == -1: location = _location else: location = min(_location, location) if location != -1: text = line[:location].strip() comment = line[location:].strip() else: text = line comment = '' return text, comment, is_comment ##############################################
[docs] def append(self, line): text, comment, is_comment = self._split_comment(line) if text: if not self._text.endswith(' ') or text.startswith(' '): self._text += ' ' self._text += text if comment: self._comment += ' // ' + comment _slice = self._line_range self._line_range = slice(_slice.start, _slice.stop + 1)
##############################################
[docs] def lower_case_statement(self, statement): """Lower case the statement""" # statement without . prefix if self._text: lower_statement = statement.lower() _slice = slice(1, len(statement) + 1) _statement = self._text[_slice] if _statement.lower() == lower_statement: self._text = '.' + lower_statement + self._text[_slice.stop:]
##############################################
[docs] def right_of(self, text): return self._text[len(text):].strip()
##############################################
[docs] def read_words(self, start_location, number_of_words): """Read a fixed number of words separated by space.""" words = [] stop_location = None line_str = self._text number_of_words_read = 0 while number_of_words_read < number_of_words: # and start_location < len(line_str) if line_str[start_location] == '{': stop_location = line_str.find('}', start_location) if stop_location > start_location: stop_location += 1 else: stop_location = line_str.find(' ', start_location) if stop_location == -1: stop_location = None # read until end word = line_str[start_location:stop_location].strip() if word: number_of_words_read += 1 words.append(word) if stop_location is None: # we should stop if number_of_words_read != number_of_words: template = 'Bad element line, looking for word {}/{}:' + os.linesep message = (template.format(number_of_words_read, number_of_words) + line_str + os.linesep + ' ' * start_location + '^') self._logger.warning(message) raise ParseError(message) else: if start_location < stop_location: start_location = stop_location else: # we have read a space start_location += 1 return words, stop_location
##############################################
[docs] def split_words(self, start_location, until=None): stop_location = None line_str = self._text if until is not None: location = line_str.find(until, start_location) if location != -1: stop_location = location location = line_str.rfind(' ', start_location, stop_location) if location != -1: stop_location = location else: raise NameError('Bad element line, missing key? ' + line_str) line_str = line_str[start_location:stop_location] words = [x for x in line_str.split(' ') if x] result = [] expression = 0 begin_idx = 0 for idx, word in enumerate(words): if expression == 0: begin_idx = idx expression += word.count('{') - word.count('}') if expression == 0: if begin_idx < idx: result.append(' '.join(words[begin_idx:idx + 1])) else: result.append(word) return result, stop_location
##############################################
[docs] @staticmethod def get_kwarg(text): dict_parameters = {} parts = [] for part in text.split(): if '=' in part and part != '=': left, right = [x for x in part.split('=')] parts.append(left) parts.append('=') if right: parts.append(right) else: parts.append(part) i = 0 i_stop = len(parts) while i < i_stop: if i + 1 < i_stop and parts[i + 1] == '=': key, value = parts[i], parts[i + 2] dict_parameters[key] = value i += 3 else: raise ParseError("Bad kwarg: {}".format(text)) return dict_parameters
############################################## @staticmethod def _partition(text): parts = [] values = text.replace(',', ' ') for part in values.split(): if '=' in part and part != '=': left, right = [x for x in part.split('=')] parts.append(left) parts.append('=') if right: parts.append(right) else: parts.append(part) return parts ############################################## @staticmethod def _partition_parentheses(text): p = regex.compile(r'\(([^\(\)]|(?R))*?\)') parts = [] previous_start = 0 for m in regex.finditer(p, text): parts.extend(Line._partition(text[previous_start:m.start()])) parts.append(m.group()) previous_start = m.end() parts.extend(Line._partition(text[previous_start:])) return parts ############################################## @staticmethod def _partition_braces(text): p = regex.compile(r'\{([^\{\}]|(?R))*?\}') parts = [] previous_start = 0 for m in regex.finditer(p, text): parts.extend(Line._partition_parentheses(text[previous_start:m.start()])) parts.append(m.group()) previous_start = m.end() parts.extend(Line._partition_parentheses(text[previous_start:])) return parts ############################################## @staticmethod def _check_parameters(parts): parameters = [] dict_parameters = {} i = 0 i_stop = len(parts) while i < i_stop: if i + 1 < i_stop and parts[i + 1] == '=': key, value = parts[i], parts[i + 2] dict_parameters[key] = value i += 3 else: parameters.append(parts[i]) i += 1 return parameters, dict_parameters ##############################################
[docs] def split_keyword(self, keyword): """Split the line according to the following pattern:: keyword parameter1 parameter2 ( key1=value1 key2=value2 ) Return the list of parameters and the dictionary. The parenthesis can be omitted. """ text = self.right_of(keyword) p = regex.compile(r'\(([^\(\)]|(?R))*?\)') b = regex.compile(r'\{([^\{\}]|(?R))*?\}') parts = [] mp = regex.search(p, text) mb = regex.search(b, text) if mb is not None: if mp is not None: if (mb.start() > mp.start()) and (mb.end() < mp.end()): parts.extend(Line._partition(text[:mp.start()])) parts.extend(Line._partition_braces(mp.group()[1:-1])) elif (mb.start() < mp.start()) and (mb.end() > mp.end()): parts.extend(Line._partition_braces(text)) else: raise ValueError("Incorrect format {}".format(text)) else: parts.extend(Line._partition_braces(text)) else: if mp is not None: parts.extend(Line._partition(text[:mp.start()])) parts.extend(Line._partition(mp.group()[1:-1])) else: parts.extend(Line._partition(text)) return Line._check_parameters(parts)
##############################################
[docs] def split_element(self, prefix): """Split the line according to the following pattern:: keyword parameter1 parameter2 ... key1=value1 key2=value2 ... Return the list of parameters and the dictionary. """ # Fixme: cf. get_kwarg parameters = [] dict_parameters = {} text = self.right_of(prefix) parts = Line._partition_braces(text) return Line._check_parameters(parts)
####################################################################################################
[docs]class SpiceParser: """This class parse a Spice netlist file and build a syntax tree. Public Attributes: :attr:`circuit` :attr:`models` :attr:`subcircuits` """ _logger = _module_logger.getChild('SpiceParser') ############################################## def __init__(self, path=None, source=None, end_of_line_comment=('$', '//', ';')): # Fixme: empty source if path is not None: with open(str(path), 'r') as fh: raw_lines = fh.readlines() # Fixme: cf. jmgc elif source is not None: raw_lines = source.split(os.linesep) else: raise ValueError self._end_of_line_comment = end_of_line_comment lines = self._merge_lines(raw_lines) self._title = None self._statements = self._parse(lines) ############################################## def _merge_lines(self, raw_lines): """Merge broken lines and return a new list of lines. A line starting with "+" continues the preceding line. """ lines = [] current_line = None for line_index, line_string in enumerate(raw_lines): if line_string.startswith('+'): current_line.append(line_string[1:].strip('\r\n')) else: line_string = line_string.strip(' \t\r\n') if line_string: _slice = slice(line_index, line_index + 1) line = Line(line_string, _slice, self._end_of_line_comment) lines.append(line) # handle case with comment before line continuation if not line_string.startswith('*'): current_line = line return lines ############################################## @staticmethod def _check_models(circuit, available_models=set()): p_available_models = available_models.copy() p_available_models.update([model.name for model in circuit._models]) for subcircuit in circuit._subcircuits: SpiceParser._check_models(subcircuit, p_available_models) for model in circuit._required_models: if model not in p_available_models: raise ValueError("model (%s) not available in (%s)" % (model, circuit.name)) ############################################## @staticmethod def _sort_subcircuits(circuit, available_subcircuits=set()): p_available_subcircuits = available_subcircuits.copy() names = [subcircuit.name for subcircuit in circuit._subcircuits] p_available_subcircuits.update(names) dependencies = dict() for subcircuit in circuit._subcircuits: required = SpiceParser._sort_subcircuits(subcircuit, p_available_subcircuits) dependencies[subcircuit] = required for subcircuit in circuit._required_subcircuits: if subcircuit not in p_available_subcircuits: raise ValueError("subcircuit (%s) not available in (%s)" % (subcircuit, circuit.name)) items = sorted(dependencies.items(), key=lambda item: len(item[1])) result = list() result_names = list() previous = len(items) + 1 while 0 < len(items) < previous: previous = len(items) remove = list() for item in items: subckt, depends = item for name in depends: if name not in result_names: break else: result.append(subckt) result_names.append(subckt.name) remove.append(item) for item in remove: items.remove(item) if len(items) > 0: raise ValueError("Crossed dependencies (%s)" % [(key.name, value) for key, value in items]) circuit._subcircuits = result return circuit._required_subcircuits - set(names) ############################################## def _parse(self, lines): """Parse the lines and return a list of statements.""" # The first line in the input file must be the title, which is the only comment line that does # not need any special character in the first place. # # The last line must be .end if len(lines) <= 1: raise NameError('Netlist is empty') # if lines[-1] != '.end': # raise NameError('".end" is expected at the end of the netlist') circuit = CircuitStatement(lines[0]) stack = [] scope = circuit for line in lines[1:]: # print('>', repr(line)) text = str(line) lower_case_text = text.lower() # ! if line.is_comment: scope.append(Comment(line)) elif lower_case_text.startswith('.'): lower_case_text = lower_case_text[1:] if lower_case_text.startswith('subckt'): stack.append(scope) scope = SubCircuitStatement(line) elif lower_case_text.startswith('ends'): parent = stack.pop() parent.append_subcircuit(scope) scope = parent elif lower_case_text.startswith('title'): # override fist line self._title = Title(line) scope.append(self._title) elif lower_case_text.startswith('end'): pass elif lower_case_text.startswith('model'): model = Model(line) scope.append_model(model) elif lower_case_text.startswith('include'): include = Include(line) scope.append(include) elif lower_case_text.startswith('param'): param = Parameter(line) scope.append_param(param) else: # options param ... # .global # .lib filename libname # .func .csparam .temp .if # { expr } are allowed in .model lines and in device lines. self._logger.warn('Parser ignored: {}'.format(line)) else: try: element = Element(line) scope.append(element) if hasattr(element, '_prefix') and (element._prefix == "X"): name = element._parameters[0].lower() scope._required_subcircuits.add(name) elif hasattr(element, '_dict_parameters') and 'model' in element._dict_parameters: name = element._dict_parameters['model'].lower() scope._required_models.add(name) except ParseError: pass SpiceParser._check_models(circuit) SpiceParser._sort_subcircuits(circuit) return circuit ############################################## @property def circuit(self): """Circuit statements.""" return self._statements @property def models(self): """Models of the sub-circuit.""" return self._statements.models @property def subcircuits(self): """Subcircuits of the sub-circuit.""" return self._statements.subcircuits ##############################################
[docs] def is_only_subcircuit(self): return bool(not self.circuit and self.subcircuits)
##############################################
[docs] def is_only_model(self): return bool(not self.circuit and not self.subcircuits and self.models)
############################################## @staticmethod def _build_circuit(circuit, statements, ground): for statement in statements: if isinstance(statement, Include): circuit.include(str(statement)) for statement in statements: if isinstance(statement, Element): statement.build(circuit, ground) elif isinstance(statement, Model): statement.build(circuit) elif isinstance(statement, SubCircuit): subcircuit = statement.build(ground) # Fixme: ok ??? circuit.subcircuit(subcircuit) ##############################################
[docs] def build_circuit(self, ground=0): """Build a :class:`Circuit` instance. Use the *ground* parameter to specify the node which must be translated to 0 (SPICE ground node). """ # circuit = Circuit(str(self._title)) circuit = self.circuit.build(str(ground)) return circuit
##############################################
[docs] @staticmethod def netlist_to_python(netlist_name, statements, ground=0): source_code = '' for statement in statements: if isinstance(statement, Element): source_code += statement.to_python(netlist_name, ground) elif isinstance(statement, Include): pass elif isinstance(statement, Model): source_code += statement.to_python(netlist_name) elif isinstance(statement, SubCircuitStatement): source_code += statement.to_python(netlist_name) elif isinstance(statement, Include): source_code += statement.to_python(netlist_name) return source_code
##############################################
[docs] def to_python_code(self, ground=0): ground = str(ground) source_code = '' if self.circuit: source_code += "circuit = Circuit('{}')".format(self._title) + os.linesep source_code += self.netlist_to_python('circuit', self._statements, ground) return source_code