Source code for ahkab.netlist_parser

# -*- coding: iso-8859-1 -*-
# netlist_parser.py
# Netlist parser module
# Copyright 2006 Giuseppe Venturini

# This file is part of the ahkab simulator.
#
# Ahkab 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, version 2 of the License.
#
# Ahkab 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 v2
# along with ahkab.  If not, see <http://www.gnu.org/licenses/>.

"""Parse spice-like netlist files and generate circuits instances.

The syntax is explained in :doc:`help/Netlist-Syntax` and it's based on [#1]_
whenever possible.

.. [#1] http://newton.ex.ac.uk/teaching/CDHW/Electronics2/userguide/

Introduction
------------

This module has one main circuit that is expected to be useful to the end user:
:func:`parse_circuit`, which encapsulates parsing a netlist file and returns the
circuit, the simulation objects and the post-processing directives (such as
plotting instructions).

Additionally, the module provides utility functions related to parsing, among
which the end user may be interested in the :func:`convert` function, which allows
converting from SPICE-like representations of floats, booleans and strings to
their Python representations.

The last type of functions in the module are utility functions to go through the
netlist files and remove comments.

Except for the aforementioned functions, the rest seem to be more suitable for
developers than end users.

Overview
--------

Function for parsing
====================

.. autosummary::
    parse_circuit
    main_netlist_parser
    parse_elem_resistor
    parse_elem_capacitor
    parse_elem_inductor
    parse_elem_inductor_coupling
    parse_elem_vsource
    parse_elem_isource
    parse_elem_diode
    parse_elem_mos
    parse_elem_vcvs
    parse_elem_vccs
    parse_elem_cccs
    parse_elem_ccvs
    parse_elem_switch
    parse_elem_user_defined
    parse_models
    parse_time_function
    parse_postproc
    parse_ics
    parse_analysis
    parse_single_analysis
    parse_temp_directive
    parse_param_value_from_string
    parse_ic_directive
    parse_sub_declaration
    parse_sub_instance
    parse_include_directive

Utility functions for conversions
=================================

.. autosummary::
    convert
    convert_units
    convert_boolean

Utility functions for file/txt handling
=======================================

.. autosummary::
    join_lines
    is_valid_value_param_string
    get_next_file_and_close_current


Module reference
----------------
"""

from __future__ import (unicode_literals, absolute_import,
                        division, print_function)

import sys
import imp
import math
import copy
import os

from . import circuit
from . import dc_analysis
from . import components
from . import diode
from . import mosq
from . import ekv
from . import switch
from . import printing
from . import utilities
from . import plotting
from . import options

# analyses syntax
from .dc_analysis import specs as dc_spec
from .ac import specs as ac_spec
from .transient import specs as tran_spec
from .pss import specs as pss_spec
from .py3compat import StringIO
from .pz import specs as pz_specs
from .symbolic import specs as symbolic_spec
from .time_functions import time_fun_specs
from .time_functions import sin, pulse, exp, sffm, am

from .fourier import specs as fft_specs

specs = {}
for i in dc_spec, ac_spec, tran_spec, pss_spec, symbolic_spec, pz_specs:
    specs.update(i)

time_functions = {}
for i in sin, pulse, exp, sffm, am:
    time_functions.update({i.__name__:i})

[docs]def parse_circuit(filename, read_netlist_from_stdin=False): """Parse a SPICE-like netlist Directives are collected in lists and returned too, except for subcircuits, those are added to circuit.subckts_dict. **Returns:** (circuit_instance, analyses, plotting directives) """ # Lots of differences with spice's syntax: # Support for alphanumeric node names, but the ref has to be 0. always # .end is not required, but if is used anything following it is ignored # many others, see doc. circ = circuit.Circuit(title="", filename=filename) if not read_netlist_from_stdin: ffile = open(filename, "r") else: buf = "" for aline in sys.stdin: buf += aline + "\n" ffile = StringIO(buf) file_list = [(ffile, "unknown", not read_netlist_from_stdin)] netlist_wd = os.path.split(filename)[0] file_index = 0 directives = [] model_directives = [] postproc = [] subckts_list_temp = [] netlist_lines = [] current_subckt_temp = [] within_subckt = False line_n = 0 try: while ffile is not None: while True: line = ffile.readline() if len(line) == 0: break # check for EOF line_n = line_n + 1 line = line.strip().lower() if line_n == 1: # the first line is always the title circ.title = line continue elif len(line) == 0: continue # empty line is really empty after strip() line = join_lines(ffile, line) if line[0] == "*": # comments start with * continue # directives are grouped together and evaluated after # we have the whole circuit. # subcircuits are grouped too, but processed first if line[0] == ".": line_elements = line.split() if line_elements[0] == '.subckt': if within_subckt: raise NetlistParseError("nested subcircuit declaration detected") current_subckt_temp = current_subckt_temp + \ [(line, line_n)] within_subckt = True elif line_elements[0] == '.ends': if not within_subckt: raise NetlistParseError(".ENDS outside of .subckt") within_subckt = False subckts_list_temp.append(current_subckt_temp) current_subckt_temp = [] elif line_elements[0] == '.include': file_list.append( parse_include_directive(line, netlist_wd)) elif line_elements[0] == ".end": break elif line_elements[0] == ".plot": postproc.append((line, line_n)) elif line_elements[0] == '.four': postproc.append((line, line_n)) elif line_elements[0] == '.fft': postproc.append((line, line_n)) elif line_elements[0] == ".model": model_directives.append((line, line_n)) else: directives.append((line, line_n)) continue if within_subckt: current_subckt_temp = current_subckt_temp + \ [(line, line_n)] else: netlist_lines = netlist_lines + [(line, line_n)] if within_subckt: raise NetlistParseError(".ends not found") file_index = file_index + 1 ffile = get_next_file_and_close_current(file_list, file_index) # print file_list except NetlistParseError as npe: (msg,) = npe.args if len(msg): printing.print_general_error(msg) printing.print_parse_error(line_n, line) # if not read_netlist_from_stdin: # ffile.close() raise NetlistParseError(msg) # if not read_netlist_from_stdin: # ffile.close() models = parse_models(model_directives) # now we parse the subcircuits, we want a circuit.subckt object that holds the netlist code, # the nodes and the subckt name in a handy way. # We will create all the elements in the subckt every time it is # instantiated in the netlist file. subckts_dict = {} for subckt_temp in subckts_list_temp: subckt_obj = parse_sub_declaration(subckt_temp) if subckt_obj.name not in subckts_dict: subckts_dict.update({subckt_obj.name: subckt_obj}) else: raise NetlistParseError("subckt " + \ subckt_obj.name + " has been redefined") circ += main_netlist_parser(circ, netlist_lines, subckts_dict, models) circ.models = models return (circ, directives, postproc)
[docs]def main_netlist_parser(circ, netlist_lines, subckts_dict, models): elements = [] parse_function = { 'c': lambda line: parse_elem_capacitor(line, circ), 'd': lambda line: parse_elem_diode(line, circ, models), 'e': lambda line: parse_elem_vcvs(line, circ), 'f': lambda line: parse_elem_cccs(line, circ), 'g': lambda line: parse_elem_vccs(line, circ), 'h': lambda line: parse_elem_ccvs(line, circ), 'i': lambda line: parse_elem_isource(line, circ), 'k': lambda line: parse_elem_inductor_coupling(line, circ, elements), 'l': lambda line: parse_elem_inductor(line, circ), 'm': lambda line: parse_elem_mos(line, circ, models), 'r': lambda line: parse_elem_resistor(line, circ), 's': lambda line: parse_elem_switch(line, circ, models), 'v': lambda line: parse_elem_vsource(line, circ), 'x': lambda line: parse_sub_instance(line, circ, subckts_dict, models), 'y': lambda line: parse_elem_user_defined(line, circ) } try: for line, line_n in netlist_lines: # elements: detect the element type and call the # appropriate parsing function # we always use normal convention V opposite to I # n1 is +, n2 is -, current flows from + to - try: elements += parse_function[line[0]](line) except KeyError: raise NetlistParseError("Parser: do not know how to parse" + " '%s' elements." % line[0]) # Handle errors from individual parse functions except NetlistParseError as npe: (msg,) = npe.args if len(msg): printing.print_general_error(msg) printing.print_parse_error(line_n, line) raise NetlistParseError(msg) return elements
[docs]def get_next_file_and_close_current(file_list, file_index): if file_list[file_index - 1][2]: file_list[file_index - 1][0].close() if file_index == len(file_list): ffile = None else: ffile = open(file_list[file_index][1], "r") file_list[file_index][0] = ffile return ffile
[docs]def parse_models(models_lines): models = {} for line, line_n in models_lines: tokens = line.replace("(", "").replace(")", "").split() if len(tokens) < 3: raise NetlistParseError("parse_models(): syntax error in model" + " declaration on line " + str(line_n) + ".\n\t" + line) model_label = tokens[2] model_type = tokens[1] model_parameters = {} for index in range(3, len(tokens)): if tokens[index][0] == "*": break (label, value) = parse_param_value_from_string(tokens[index]) model_parameters.update({label.upper(): value}) if model_type == "ekv": model_iter = ekv.ekv_mos_model(**model_parameters) model_iter.name = model_label elif model_type == "mosq": model_iter = mosq.mosq_mos_model(**model_parameters) model_iter.name = model_label elif model_type == "diode" or model_type == 'd': model_parameters.update({'name': model_label}) model_iter = diode.diode_model(**model_parameters) elif model_type == "sw": model_parameters.update({'name': model_label}) model_iter = switch.vswitch_model(**model_parameters) # elif model_type == "csw": # model_parameters.update({'name':model_label}) # model_iter = switch.iswitch_model(**model_parameters) else: raise NetlistParseError("parse_models(): Unknown model (" + model_type + ") on line " + str(line_n) + ".\n\t" + line,) models.update({model_label: model_iter}) return models
[docs]def parse_elem_resistor(line, circ): """Parses a resistor from the line supplied, adds its nodes to the circuit instance circ and returns a list holding the resistor element. **Parameters:** line : string The netlist line. circ : circuit instance The circuit instance to which the resistor is to be connected. **Returns:** elements_list : list A list containing a :class:`ahkab.components.Resistor` element. """ line_elements = line.split() if len(line_elements) < 4 or (len(line_elements) > 4 and not line_elements[4][0] == "*"): raise NetlistParseError("parse_elem_resistor(): malformed line") ext_n1 = line_elements[1] ext_n2 = line_elements[2] n1 = circ.add_node(ext_n1) n2 = circ.add_node(ext_n2) value = convert_units(line_elements[3]) if value == 0: raise NetlistParseError("parse_elem_resistor(): ZERO-valued resistors are not allowed.") elem = components.Resistor(part_id=line_elements[0], n1=n1, n2=n2, value=value) return [elem]
[docs]def parse_elem_capacitor(line, circ): """Parses a capacitor from the line supplied, adds its nodes to the circuit instance circ and returns a list holding the capacitor element. **Parameters:** line : string The netlist line. circ : circuit instance The circuit to which the capacitor is to be connected. **Returns:** elements_list : list A list containing a :class:`ahkab.components.Capacitor` element. """ line_elements = line.split() if len(line_elements) < 4 or \ (len(line_elements) > 5 and not line_elements[5][0] == "*" and not line_elements[4][0] == "*"): raise NetlistParseError("parse_elem_capacitor(): malformed line") ic = None if len(line_elements) == 5 and not line_elements[4][0] == '*': (label, value) = parse_param_value_from_string(line_elements[4]) if label == "ic": ic = convert_units(value) else: raise NetlistParseError("parse_elem_capacitor(): unknown parameter " + label) ext_n1 = line_elements[1] ext_n2 = line_elements[2] n1 = circ.add_node(ext_n1) n2 = circ.add_node(ext_n2) elem = components.Capacitor(part_id=line_elements[0], n1=n1, n2=n2, value=convert_units(line_elements[3]), ic=ic) return [elem]
[docs]def parse_elem_inductor(line, circ): """Parses a inductor from the line supplied, adds its nodes to the circuit instance circ and returns a list holding the inductor element. **Parameters:** line : string The netlist line. circ : circuit instance The circuit to which the inductor is to be connected. **Returns:** elements_list : list A list containing a :class:`ahkab.components.Inductor` element. """ line_elements = line.split() if len(line_elements) < 4 or (len(line_elements) > 5 and not line_elements[6][0] == "*"): raise NetlistParseError("parse_elem_inductor(): malformed line") ic = None if len(line_elements) == 5 and not line_elements[4][0] == '*': (label, value) = parse_param_value_from_string(line_elements[4]) if label == "ic": ic = convert_units(value) else: raise NetlistParseError("parse_elem_inductor(): unknown parameter " + label) ext_n1 = line_elements[1] ext_n2 = line_elements[2] n1 = circ.add_node(ext_n1) n2 = circ.add_node(ext_n2) elem = components.Inductor(part_id=line_elements[0], n1=n1, n2=n2, value=convert_units(line_elements[3]), ic=ic) return [elem]
[docs]def parse_elem_inductor_coupling(line, circ, elements=[]): """Parses a inductor coupling from the line supplied, returns a list holding the inductor coupling element. **Parameters:** line : string The netlist line. circ : circuit instance The circuit to which the inductor coupling is to be connected. **Returns:** elements_list : list A list containing a :class:`ahkab.components.InductorCoupling` element. """ line_elements = line.split() if len(line_elements) < 4 or (len(line_elements) > 4 and not line_elements[5][0] == "*"): raise NetlistParseError("parse_elem_inductor_coupling(): malformed line") part_id = line_elements[0] L1 = line_elements[1] L2 = line_elements[2] try: Kvalue = convert_units(line_elements[3]) except ValueError: (label, value) = parse_param_value_from_string(line_elements[3]) if not label == "k": raise NetlistParseError("parse_elem_inductor_coupling(): unknown parameter " + label) Kvalue = convert_units(value) L1elem, L2elem = None, None for e in elements: if isinstance(e, components.Inductor) and L1 == e.part_id: L1elem = e elif isinstance(e, components.Inductor) and L2 == e.part_id: L2elem = e if L1elem is None or L2elem is None: error_msg = "parse_elem_inductor_coupling(): One or more coupled" + \ " inductors for %s were not found: %s (found: %s), %s (found: %s)." % \ (part_id, L1, L1elem is not None, L2, L2elem is not None) raise NetlistParseError(error_msg) M = math.sqrt(L1elem.value * L2elem.value) * Kvalue elem = components.InductorCoupling(part_id=part_id, L1=L1, L2=L2, K=Kvalue, M=M) L1elem.coupling_devices.append(elem) L2elem.coupling_devices.append(elem) return [elem]
[docs]def parse_elem_vsource(line, circ): """Parses a voltage source from the line supplied, adds its nodes to the circuit instance and returns a list holding the element. **Parameters:** line : string The netlist line. circ : circuit instance The circuit in which the voltage source is to be inserted. **Returns:** elements_list : list A list containing a :class:`ahkab.components.sources.VSource` element. """ line_elements = line.split() if len(line_elements) < 3: raise NetlistParseError("parse_elem_vsource(): malformed line") dc_value = None vac = None function = None index = 3 while True: # for index in range(3, len(line_elements)): if index >= len(line_elements): break if line_elements[index][0] == '*': break label, value = parse_param_value_from_string(line_elements[index]) if label == 'type': if value == 'vdc': param_number = 0 elif value == 'vac': param_number = 0 elif value == 'pulse': param_number = 7 elif value == 'exp': param_number = 6 elif value == 'sin': param_number = 5 elif value == 'sffm': param_number = 5 elif value == 'am': param_number = 5 else: raise NetlistParseError("parse_elem_vsource(): unknown signal" + "type %s" % value) if param_number and function is None: function = parse_time_function(value, line_elements[index + 1: index + param_number + 1], "voltage") index = index + param_number # continue elif function is not None: raise NetlistParseError("parse_elem_vsource(): only a time function can be defined.") elif label == 'vdc': dc_value = convert_units(value) elif label == 'vac': vac = convert_units(value) else: raise NetlistParseError("parse_elem_vsource(): unknown type %s" % label) index = index + 1 if dc_value == None and function == None: raise NetlistParseError("parse_elem_vsource(): neither vdc nor a time function are defined.") # usual ext_n1 = line_elements[1] ext_n2 = line_elements[2] n1 = circ.add_node(ext_n1) n2 = circ.add_node(ext_n2) elem = components.sources.VSource(part_id=line_elements[0], n1=n1, n2=n2, dc_value=dc_value, ac_value=vac) if function is not None: elem.is_timedependent = True elem._time_function = function return [elem]
[docs]def parse_elem_isource(line, circ): """Parses a current source from the line supplied, adds its nodes to the circuit instance and returns a list holding the current source element. **Parameters:** line : string The netlist line. circ : circuit instance The circuit in which the current source is to be inserted. **Returns:** elements_list : list A list containing a :class:`ahkab.components.sources.ISource` element. """ line_elements = line.split() if len(line_elements) < 3: raise NetlistParseError("parse_elem_isource(): malformed line") dc_value = None iac = None function = None index = 3 while True: # for index in range(3, len(line_elements)): if index == len(line_elements): break if line_elements[index][0] == '*': break (label, value) = parse_param_value_from_string(line_elements[index]) if label == 'type': if value == 'idc': param_number = 0 elif value == 'iac': param_number = 0 elif value == 'pulse': param_number = 7 elif value == 'exp': param_number = 6 elif value == 'sin': param_number = 5 elif value == 'sffm': param_number = 5 elif value == 'am': param_number = 5 else: raise NetlistParseError("parse_elem_isource(): unknown signal type.") if param_number and function is None: function = parse_time_function(value, line_elements[index + 1: index + param_number + 1], "current") index = index + param_number elif function is not None: raise NetlistParseError("parse_elem_isource(): only a time function can be defined.") elif label == 'idc': dc_value = convert_units(value) elif label == 'iac': iac = convert_units(value) else: raise NetlistParseError("parse_elem_isource(): unknown type "+label) index = index + 1 if dc_value == None and function == None: raise NetlistParseError("parse_elem_isource(): neither idc nor a time function are defined.") ext_n1 = line_elements[1] ext_n2 = line_elements[2] n1 = circ.add_node(ext_n1) n2 = circ.add_node(ext_n2) elem = components.sources.ISource(part_id=line_elements[0], n1=n1, n2=n2, dc_value=dc_value, ac_value=iac) if function is not None: elem.is_timedependent = True elem._time_function = function return [elem]
[docs]def parse_elem_diode(line, circ, models=None): """Parses a diode from the line supplied, adds its nodes to the circuit instance and returns a list holding the diode element. Diode syntax: :: DX N+ N- <MODEL_LABEL> <AREA=xxx> **Parameters:** line : string The netlist line. circ : circuit instance The circuit in which the diode will be inserted. **Returns:** elements_list : list A list containing a :class:`ahkab.diode.Diode` element. """ # sarebbe bello implementare anche: <IC=VD> <TEMP=T> Area = None T = None ic = None off = False line_elements = line.split() if len(line_elements) < 4: raise NetlistParseError("") model_label = line_elements[3] for index in range(4, len(line_elements)): if line_elements[index][0] == '*': break param, value = parse_param_value_from_string(line_elements[index]) value = convert_units(value) if param == "area": Area = value elif param == "t": T = value elif param == "ic": ic = value elif param == "off": if not len(value): off = True else: off = convert_boolean(value) else: raise NetlistParseError("parse_elem_diode(): unknown parameter " + param) ext_n1 = line_elements[1] ext_n2 = line_elements[2] n1 = circ.add_node(ext_n1) n2 = circ.add_node(ext_n2) if model_label not in models: raise NetlistParseError("parse_elem_diode(): Unknown model id: " + model_label) elem = diode.diode(part_id=line_elements[0], n1=n1, n2=n2, model=models[ model_label], AREA=Area, ic=ic, off=off) return [elem]
[docs]def parse_elem_mos(line, circ, models): """Parses a MOS transistor from the line supplied, adds its nodes to the circuit instance and returns a list holding the element. MOS syntax: :: MX ND NG NS KP=xxx Vt=xxx W=xxx L=xxx type=n/p <LAMBDA=xxx> **Parameters:** line : string The netlist line. circ : circuit instance The circuit to which the element will be added. **Returns:** elements_list : list A list containing a MOS element. """ #(self, nd, ng, ns, kp=1.0e-14, w, l, mos_type='n', lambd=0, type_of_elem="mosq") line_elements = line.split() if len(line_elements) < 6: raise NetlistParseError("parse_elem_mos(): required parameters are missing.") # print "MX ND NG NS model_id W=xxx L=xxx" model_label = line_elements[5] # kp = None w = None l = None # mos_type = None # vt = None m = 1 n = 1 # lambd = 0 # va is supposed infinite if not specified for index in range(6, len(line_elements)): if line_elements[index][0] == '*': break param, value = parse_param_value_from_string(line_elements[index]) if param == "w": w = convert_units(value) elif param == "l": l = convert_units(value) elif param == "m": m = convert_units(value) elif param == "n": n = convert_units(value) else: raise NetlistParseError("parse_elem_mos(): unknown parameter " + param) if (w is None) or (l is None): raise NetlistParseError('parse_elem_mos(): required parameter ' + 'w'*(w is None) + ' and '* (w is None and l is None) + 'l'*(l is None)+ 'missing.') # print "MX ND NG NS W=xxx L=xxx <M=xxx> <N=xxx>" ext_nd = line_elements[1] ext_ng = line_elements[2] ext_ns = line_elements[3] ext_nb = line_elements[4] nd = circ.add_node(ext_nd) ng = circ.add_node(ext_ng) ns = circ.add_node(ext_ns) nb = circ.add_node(ext_nb) if model_label not in models: raise NetlistParseError("parse_elem_mos(): Unknown model ID: " + model_label) elem = None if isinstance(models[model_label], ekv.ekv_mos_model): elem = ekv.ekv_device(line_elements[0], nd, ng, ns, nb, w, l, models[model_label], m, n) elif isinstance(models[model_label], mosq.mosq_mos_model): elem = mosq.mosq_device(line_elements[0], nd, ng, ns, nb, w, l, models[model_label], m, n) else: raise NetlistParseError("parse_elem_mos(): Unknown MOS model type: " + model_label) return [elem]
[docs]def parse_elem_vcvs(line, circ): """Parses a voltage-controlled voltage source (VCVS) from the line supplied, adds its nodes to the circuit instance circ and returns a list holding the VCVS element. **Parameters:** line : string The netlist line. circ : circuit instance The circuit in which the VCVS is to be inserted. **Returns:** elements_list : list A list containing a :class:`ahkab.components.sources.EVSource` element. """ line_elements = line.split() if len(line_elements) < 6 or (len(line_elements) > 6 and not line_elements[6][0] == "*"): raise NetlistParseError("") ext_n1 = line_elements[1] ext_n2 = line_elements[2] ext_sn1 = line_elements[3] ext_sn2 = line_elements[4] n1 = circ.add_node(ext_n1) n2 = circ.add_node(ext_n2) sn1 = circ.add_node(ext_sn1) sn2 = circ.add_node(ext_sn2) elem = components.sources.EVSource(part_id=line_elements[0], n1=n1, n2=n2, sn1=sn1, sn2=sn2, value=convert_units(line_elements[5])) return [elem]
[docs]def parse_elem_ccvs(line, circ): """Parses a current-controlled voltage source (CCVS) from the line supplied, adds its nodes to the circuit instance and returns a list holding the CCVS element. CCVS syntax: :: HXXX N1 N2 VNAME VALUE **Parameters:** line : string The netlist line. circ : circuit instance The circuit in which the CCVS is to be inserted. **Returns:** elements_list : list A list containing a :class:`ahkab.components.sources.HVSource` element. """ # 0 1 2 3 4 line_elements = line.split() if len(line_elements) < 5 or (len(line_elements) > 5 and not line_elements[5][0] == "*"): raise NetlistParseError("") ext_n1 = line_elements[1] ext_n2 = line_elements[2] n1 = circ.add_node(ext_n1) n2 = circ.add_node(ext_n2) elem = components.sources.HVSource(part_id=line_elements[0], n1=n1, n2=n2, source_id=line_elements[3], value=convert_units(line_elements[4])) return [elem]
[docs]def parse_elem_vccs(line, circ): """Parses a voltage-controlled current source (VCCS) from the line supplied, adds its nodes to the circuit instance and returns a list holding the VCCS element. Syntax: :: GX N+ N- NC+ NC- VALUE **Parameters:** line : string The netlist line. circ : circuit instance The circuit in which the VCCS is to be inserted. **Returns:** elements_list : list A list containing a :class:`ahkab.components.sources.GISource` element. """ line_elements = line.split() if len(line_elements) < 6 or (len(line_elements) > 6 and not line_elements[6][0] == "*"): raise NetlistParseError("") ext_n1 = line_elements[1] ext_n2 = line_elements[2] ext_sn1 = line_elements[3] ext_sn2 = line_elements[4] n1 = circ.add_node(ext_n1) n2 = circ.add_node(ext_n2) sn1 = circ.add_node(ext_sn1) sn2 = circ.add_node(ext_sn2) elem = components.sources.GISource(part_id=line_elements[0], n1=n1, n2=n2, sn1=sn1, sn2=sn2, value=convert_units(line_elements[5])) return [elem]
[docs]def parse_elem_cccs(line, circ): """Parses a current-controlled current source (CCCS) from the line supplied, adds its nodes to the circuit instance and returns a list holding the CCCS element. Syntax:: FX N+ N- VNAME VALUE **Parameters:** line : string The netlist line. circ : circuit instance The circuit in which the CCCS is to be inserted. **Returns:** elements_list : list A list containing a :class:`ahkab.components.sources.FISource` element. """ line_elements = line.split() if len(line_elements) < 5 or (len(line_elements) > 5 and not line_elements[5][0] == "*"): raise NetlistParseError("") ext_n1 = line_elements[1] ext_n2 = line_elements[2] source_id = line_elements[3] n1 = circ.add_node(ext_n1) n2 = circ.add_node(ext_n2) elem = components.sources.FISource(part_id=line_elements[0], n1=n1, n2=n2, source_id=source_id, value=convert_units(line_elements[4])) return [elem]
[docs]def parse_elem_switch(line, circ, models=None): """Parses a switch device from the line supplied, adds its nodes to the circuit instance and returns a list holding the switch element. General syntax:: SW1 n1 n2 ns1 ns2 model_label **Parameters:** line : string The netlist line. circ : circuit instance The circuit in which the switch is to be connected. models : dict, optional The currently defined models. **Returns:** elements_list : list A list containing a :class:`ahkab.switch.switch_device` element. """ line_elements = line.split() if len(line_elements) < 6 or (len(line_elements) > 6 and not line_elements[6][0] == "*"): raise NetlistParseError("") ext_n1 = line_elements[1] ext_n2 = line_elements[2] ext_sn1 = line_elements[3] ext_sn2 = line_elements[4] n1 = circ.add_node(ext_n1) n2 = circ.add_node(ext_n2) sn1 = circ.add_node(ext_sn1) sn2 = circ.add_node(ext_sn2) model_label = line_elements[5] elem = None if model_label not in models: raise NetlistParseError("Unknown model id: " + model_label) if isinstance(models[model_label], switch.vswitch_model): elem = switch.switch_device( n1, n2, sn1, sn2, models[model_label], part_id=line_elements[0]) else: raise NetlistParseError("Unknown MOS model type: " + model_label) return [elem]
[docs]def parse_elem_user_defined(line, circ): """Parses a user defined element. In order for this to work, you should write a module that supplies the elem class. Syntax: Y<X> <n1> <n2> module=<module_name> type=<type> [<param1>=<value1> ...] This method will attempt to load the module <module_name> and it will then look for a class named <type>. An object will be instatiated with the following arguments: n1, n2, param_dict, get_int_id_func, convert_units_func Where: n1: is the anode of the element n2: is the cathode param_dict: is a dictionary, its elements are {param1:value1, ...} get_int_id_func, convert_units_func are two function that may be used in the __init__ method, if needed. get_int_id_func: a function that gives back the internal name of a node convert_units_func: utility function to convert eg 1p -> 1e-12 See ideal_oscillators.py for a reference implementation. **Parameters:** line : string The netlist line. circ : circuit instance. The circuit to which the element will be added. **Returns:** elements_list : list A list containing a :class:`ahkab.components.sources.HVSource` element. Parameters: line: the line circ: the circuit instance. Returns: [userdef_elem] """ line_elements = line.split() if len(line_elements) < 4: raise NetlistParseError("") param_dict = {} for index in range(3, len(line_elements)): if line_elements[index][0] == '*': break param, value = parse_param_value_from_string(line_elements[index]) if param not in param_dict: param_dict.update({param: value}) else: raise NetlistParseError(param + " already defined.") if "module" in param_dict: module_name = param_dict.pop("module", None) else: raise NetlistParseError("module name is missing.") if module_name in circuit.user_defined_modules_dict: module = circuit.user_defined_modules_dict[module_name] else: try: fp, pathname, description = imp.find_module(module_name) module = imp.load_module(module_name, fp, pathname, description) except ImportError: raise NetlistParseError("module " + module_name + " not found.") circuit.user_defined_modules_dict.update({module_name: module}) if "type" in param_dict: elem_type_name = param_dict.pop("type", None) else: raise NetlistParseError("type of element is missing.") try: elem_class = getattr(module, elem_type_name) except AttributeError: raise NetlistParseError("module doesn't have elem type: " + \ elem_type_name) ext_n1 = line_elements[1] ext_n2 = line_elements[2] n1 = circ.add_node(ext_n1) n2 = circ.add_node(ext_n2) elem = elem_class(n1, n2, param_dict, circ.add_node, convert_units, part_id=line_elements[0]) selfcheck_result, error_msg = elem.check() if not selfcheck_result: raise NetlistParseError("module: " + module_name + " elem type: " + elem_type_name + " error: " +\ error_msg) # TODO fixme non so sicuro che sia una buona idea return [elem]
[docs]def parse_time_function(ftype, line_elements, stype): """Parses a time function of type ftype from the line_elements supplied. **Parameters:** ftype : str One among ``"pulse"``, ``"exp"``, ``"sin"``, ``"sffm"`` or ``"am"``. line_elements : list of strings The tokens describing the time function. The list mustn't hold the ``"type=<ftype>"`` element stype : str Set this to "current" for current sources, "voltage" for voltage sources See :class:`ahkab.time_functions.pulse`, :class:`ahkab.time_functions.sin`, :class:`ahkab.time_functions.exp`, :class:`ahkab.time_functions.sffm` and :class:`ahkab.time_functions.am` for more. **Returns:** time_function : object A time-function instance """ if not ftype in time_fun_specs: raise NetlistParseError("Unknown time function: %s" % ftype) prot_params = list(copy.deepcopy(time_fun_specs[ftype]['tokens'])) fun_params = {} for i in range(len(line_elements)): token = line_elements[i] if token[0] == "*": break if is_valid_value_param_string(token): (label, value) = token.split('=') else: label, value = None, token assigned = False for t in prot_params: if (label is None and t['pos'] == i) or label == t['label']: fun_params.update({t['dest']: convert(value, t['type'])}) assigned = True break if assigned: prot_params.pop(prot_params.index(t)) continue else: raise NetlistParseError("Unknown .%s parameter: pos %d (%s=)%s" % \ (ftype.upper(), i, label, value)) missing = [] for t in prot_params: if t['needed']: missing.append(t['label']) if len(missing): raise NetlistParseError("%s: required parameters are missing: %s" % (ftype, " ".join(line_elements))) # load defaults for unsupplied parameters for t in prot_params: fun_params.update({t['dest']: t['default']}) fun = time_functions[ftype](**fun_params) fun._type = "V" * \ (stype.lower() == "voltage") + "I" * (stype.lower() == "current") return fun
[docs]def convert_units(string_value): """Converts a value conforming to SPICE's syntax to ``float``. Quote from the SPICE3 manual: A number field may be an integer field (eg 12, -44), a floating point field (3.14159), either an integer or a floating point number followed by an integer exponent (1e-14, 2.65e3), or either an integer or a floating point number followed by one of the following scale factors: T = 1e12, G = 1e9, Meg = 1e6, K = 1e3, mil = 25.4x1e-6, m = 1e-3, u = 1e-6, n = 1e-9, p = 1e-12, f = 1e-15 :raises ValueError: if the supplied string can't be interpreted according to the above. **Returns:** num : float A float representation of ``string_value``. """ if type(string_value) is float: return string_value # not actually a string! if not len(string_value): raise NetlistParseError("") index = 0 string_value = string_value.strip().upper() while(True): if len(string_value) == index: break if not (string_value[index].isdigit() or string_value[index] == "." or string_value[index] == "+" or string_value[index] == "-" or string_value[index] == "E"): break index = index + 1 if index == 0: # print string_value raise ValueError("Unable to parse value: %s" % string_value) # return 0 numeric_value = float(string_value[:index]) multiplier = string_value[index:] if len(multiplier) == 0: pass # return numeric_value elif multiplier == "T": numeric_value = numeric_value * 1e12 elif multiplier == "G": numeric_value = numeric_value * 1e9 elif multiplier == "K": numeric_value = numeric_value * 1e3 elif multiplier == "M": numeric_value = numeric_value * 1e-3 elif multiplier == "U": numeric_value = numeric_value * 1e-6 elif multiplier == "N": numeric_value = numeric_value * 1e-9 elif multiplier == "P": numeric_value = numeric_value * 1e-12 elif multiplier == "F": numeric_value = numeric_value * 1e-15 elif multiplier == "MEG": numeric_value = numeric_value * 1e6 elif multiplier == "MIL": numeric_value = numeric_value * 25.4e-6 else: raise ValueError("Unknown multiplier %s" % multiplier) return numeric_value
[docs]def parse_postproc(circ, postproc_direc): postproc_list = [] for line, line_n in postproc_direc: if not line[0] == ".": continue try: line_elements = line.split() # plot if line_elements[0] == ".plot": plot_postproc = {} plot_postproc["type"] = "plot" plot_postproc["analysis"] = line_elements[1] if not (plot_postproc["analysis"] == "tran" or plot_postproc["analysis"] == "pss" or plot_postproc["analysis"] == "ac" or plot_postproc["analysis"] == "dc" ): printing.print_general_error("Plotting is unsupported for" + "analysis type " + plot_postproc["analysis"]) graph_labels = "" for glabel in line_elements[2:]: graph_labels = graph_labels + " " + glabel l2l1 = plotting._split_netlist_label(graph_labels) if plot_postproc["analysis"] == "ac": l2l1ac = [] for l2, l1 in l2l1: if l1 is not None: l1 = "|%s|" % (l1, ) else: l1 = None if l2 is not None: l2 = "|%s|" % (l2, ) else: l2 = None l2l1ac.append((l2, l1)) l2l1 = l2l1ac plot_postproc["l2l1"] = l2l1 postproc_list.append(plot_postproc) # fourier elif line_elements[0] == ".four": if type(convert_units(line_elements[1])) is not float: raise NetlistParseError('postprocessing(): fourier' + ' fundamental \'%s\'?' % line_elements[1]) fpv = {'type':'four', 'fund':convert_units(line_elements[1])} variables = [] for le in line_elements[2:]: if le[0] == '*': break variables += [plotting._split_netlist_label(le)[0]] fpv.update({'variables':tuple(variables)}) postproc_list.append(fpv) elif line_elements[0] == '.fft': fpv = {'type': 'fft'} params = list(copy.deepcopy(fft_specs['fft']['tokens'])) for i in range(len(line_elements[1:])): token = line_elements[i + 1] if token[0] == "*": break if is_valid_value_param_string(token): label, value = token.split('=') else: label, value = None, token assigned = False for t in params: if (label is None and t['pos'] == i) or label == t['label']: fpv.update({t['dest']: convert(value, t['type'])}) assigned = True break if assigned: params.pop(params.index(t)) continue else: raise NetlistParseError("Unknown .%s parameter: pos %d (%s=)%s" % \ (fpv[type].upper(), i, label, value)) # is there anything required which is missing? missing = [] for t in params: if t['needed']: missing.append(t['label']) if len(missing): raise NetlistParseError("Required parameters are missing: %s" % (" ".join(line_elements))) # load defaults for unsupplied parameters for t in params: fpv.update({t['dest']: t['default']}) fpv.update({'label': tuple(plotting._split_netlist_label(fpv['label'])[0])}) postproc_list.append(fpv) else: raise NetlistParseError("Unknown postproc directive %s." % line_elements[0]) except NetlistParseError as npe: (msg,) = npe.args if len(msg): printing.print_general_error(msg) printing.print_parse_error(line_n, line) raise NetlistParseError(msg) return postproc_list
[docs]def parse_ics(directives): ics = [] for line, line_n in directives: if line[0] != '.': continue if line[:3] == '.ic': ics += [parse_ic_directive(line)] return ics
[docs]def parse_analysis(circ, directives): """Parses the analyses. **Parameters:** circ: circuit class instance The circuit description directives: list of tuples The list should be assembled as ``(line, line_number)``. Both of them are returned by ``parse_circuit()`` **Returns:** a list of the analyses """ an = [] for line, line_n in directives: if line[0] != '.' or line[:3] == '.ic': continue line_elements = line.split() an += [parse_single_analysis(line)] return an
[docs]def parse_temp_directive(line): """Parses a TEMP directive: The syntax is:: .TEMP <VALUE> """ line_elements = line.split() for token in line_elements[1:]: if token[0] == "*": break value = convert_units(token) return {"type": "temp", "temp": value}
[docs]def parse_single_analysis(line): """Parses an analysis **Parameters:** line : str The netlist line from which an analysis statement is to be parsed. **Returns:** an : dict A dictionary with its parameters as keys. :raises NetlistParseError: if the analysis is not parsed correctly. """ line_elements = line.split() an_type = line_elements[0].replace(".", "").lower() if not an_type in specs: raise NetlistParseError("Unknown directive: %s" % an_type) params = list(copy.deepcopy(specs[an_type]['tokens'])) an = {'type': an_type} for i in range(len(line_elements[1:])): token = line_elements[i + 1] if token[0] == "*": break if is_valid_value_param_string(token): (label, value) = token.split('=') else: label, value = None, token assigned = False for t in params: if (label is None and t['pos'] == i) or label == t['label']: an.update({t['dest']: convert(value, t['type'])}) assigned = True break if assigned: params.pop(params.index(t)) continue else: raise NetlistParseError("Unknown .%s parameter: pos %d (%s=)%s" % \ (an_type.upper(), i, label, value)) missing = [] for t in params: if t['needed']: missing.append(t['label']) if len(missing): raise NetlistParseError("Required parameters are missing: %s" % (" ".join(line_elements))) # load defaults for unsupplied parameters for t in params: an.update({t['dest']: t['default']}) # ad-hoc code for tran ... :( if an['type'] == 'tran': uic = int(an.pop('uic')) if uic == 0: an['x0'] = None elif uic == 1: an['x0'] = 'op' elif uic == 2: an['x0'] = 'op+ic' elif uic == 3: pass # already set by ic_label else: raise NetlistParseError("Unknown UIC value: %d" % uic) # ... and pz :( if an['type'] == 'pz': an.update({'x0':'op'}) return an
[docs]def is_valid_value_param_string(astr): """Has the string a form like ``<param_name>=<value>``? .. note:: No spaces. **Returns:** ans : a boolean The answer to the above question. """ work_astr = astr.strip() if work_astr.count("=") == 1: ret_value = True else: ret_value = False return ret_value
[docs]def convert(astr, rtype, raise_exception=False): """Convert a string to a different representation **Parameters:** astr : str The string to be converted. rtype : type One among ``float``, if a ``float`` sould be parsed from ``astr``, ``bool``, for parsing a boolean or ``str`` to get back a string (no parsing). raise_exception : boolean, optional Set this flag to ``True`` if you wish for this function to raise ``ValueError`` if parsing fails. **Returns:** ret : object The parsed data. """ if rtype == float: try: ret = convert_units(astr) except ValueError as msg: if raise_exception: raise ValueError(msg) else: ret = astr elif rtype == str: ret = astr elif rtype == bool: ret = convert_boolean(astr) elif raise_exception: raise ValueError("Unknown type %s" % rtype) else: ret = astr return ret
[docs]def parse_param_value_from_string(astr, rtype=float, raise_exception=False): """Search the string for a ``<param>=<value>`` couple and returns a list. **Parameters:** astr : str The string to be converted. rtype : type One among ``float``, if a ``float`` sould be parsed from ``astr``, ``bool``, for parsing a boolean or ``str`` to get back a string (no parsing). raise_exception : boolean, optional Set this flag to ``True`` if you wish for this function to raise ``ValueError`` if parsing fails. **Returns:** ret : object The parsed data. If the conversion fails and ``raise_exception`` is not set, a ``string`` is returned. * If ``rtype`` is ``float`` (the type), its default value, the method will attempt converting ``astr`` to a float. If the conversion fails, a string is returned. * If set ``rtype`` to ``str`` (again, the type), a string will always be returned, as if the conversion failed. This prevents ``'0'`` (str) being detected as ``float`` and converted to 0.0, ending up being a new node instead of the reference. Notice that in ``<param>=<value>`` there is no space before or after the equal sign. **Returns:** alist : ``[param, value]`` where ``param`` is a string and ``value`` is parsed as described. """ if not is_valid_value_param_string(astr): return (astr, "") p, v = astr.strip().split("=") v = convert(v, rtype, raise_exception=False) return p, v
[docs]class NetlistParseError(Exception): """Netlist parsing exception.""" pass
[docs]def convert_boolean(value): """Converts the following strings to a boolean: yes, 1, true to True no, false, 0 to False raises NetlistParserException Returns: boolean """ if value == 'no' or value == 'false' or value == '0' or value == 0: return_value = False elif value == 'yes' or value == 'true' or value == '1' or value == 1: return_value = True else: raise NetlistParseError("invalid boolean: " + value) return return_value
[docs]def parse_ic_directive(line): """Parses an ic directive and assembles a dictionary accordingly. """ line_elements = line.split() ic_dict = {} name = None for token in line_elements[1:]: if token[0] == "*": break (label, value) = parse_param_value_from_string(token) if label == "name" and name is None: name = value continue # the user should have specified either something like: # V(node)=10u # or something like: # I(Vtest)=100e-6 ic_dict.update({label: convert_units(value)}) # We may decide to check if the node exists and/or if the syntax # is correct and raise NetlistParseError if needed. if name is None: raise NetlistParseError("The 'name' parameter is missing") return {name: ic_dict}
[docs]def parse_sub_declaration(subckt_lines): """Returns a circuit.subckt instance that holds the subckt information, ready to be instantiated/called. """ index = 0 netlist_lines = [] connected_nodes_list = [] for line, line_n in subckt_lines: if index == 0: line_elements = line.split() if line_elements[0] != '.subckt': raise RuntimeError("BUG? parse_sub_declaration() \ called on non-subckt text. (line" + str(line_n) + ")") name = line_elements[1] for node_name in line_elements[2:]: if node_name[0] == '0': raise NetlistParseError("subckt " + name + \ " has a connection node named '0' (line" + str( line_n) + ")") if node_name[0] == '*': break else: connected_nodes_list = connected_nodes_list + [node_name] else: netlist_lines = netlist_lines + [(line, "")] index = index + 1 subck_inst = circuit.subckt(name, netlist_lines, connected_nodes_list) return subck_inst
[docs]def parse_sub_instance(line, circ, subckts_dict, models=None): """Parses a subckt call/instance. 1. Gets name and nodes connections 2. Looks in subckts_dict for a matching subckts_dict[name] 3. Builds a circuit wrapper 4. Calls main_netlist_parser() on the subcircuit code (with the wrapped circuit) Returns: a elements list """ line_elements = line.split() if len(line_elements) < 2: raise NetlistParseError("") param_value_dict = {} name = None for index in range(1, len(line_elements)): if line_elements[index][0] == '*': break param, value = parse_param_value_from_string(line_elements[index], rtype=str) param_value_dict.update({param:value}) if "name" not in param_value_dict: raise NetlistParseError("missing 'name' in subckt call") if param_value_dict['name'] not in subckts_dict: raise NetlistParseError("subckt " + \ param_value_dict['name'] + " is unknown") name = param_value_dict['name'] subckt = subckts_dict[name] connection_nodes_dict = {} for param, value in param_value_dict.items(): if param == 'name': continue if param in subckt.connected_nodes_list: connection_nodes_dict.update({param: value}) else: raise NetlistParseError("unknown node " + param) # check all nodes are connected for node in subckt.connected_nodes_list: if node not in connection_nodes_dict: raise NetlistParseError("unconnected subckt node " + node) wrapped_circ = circuit._circuit_wrapper(circ, connection_nodes_dict, subckt.name, line_elements[0]) elements_list = main_netlist_parser(wrapped_circ, subckt.code, subckts_dict, models) # Every subckt adds elements with the _same description_ (elem.part_id[1:]) # We modify it so that each description is unique for every instance for element in elements_list: element.part_id = element.part_id[0] + \ "-" + wrapped_circ.prefix + element.part_id[1:] return elements_list
[docs]def parse_include_directive(line, netlist_wd): """.include <filename> [*comments] """ line_elements = line.split() if not len(line_elements) > 1 or \ (len(line_elements) > 2 and not line_elements[2][0] == '*'): raise NetlistParseError("") path = line_elements[1] if not os.path.isabs(path): # the user did not specify the full path. # the path is then assumed to be relative to the netlist location path = os.path.join(netlist_wd, path) if not utilities.check_file(path): raise RuntimeError("") fnew = open(path, "r") return [None, path, True]
[docs]def join_lines(fp, line): """Read the lines coming up in the file. Each line that starts with '+' is added to the previous line (line continuation rule). When a line not starting with '+' is found, the file is rolled back and the line is returned. """ while True: last_pos = fp.tell() next = fp.readline() next = next.strip().lower() if not next: break elif next[0] == '+': line += ' ' + next[1:] else: fp.seek(last_pos) break return line