Source code for skidl.tools.spice.spice

# -*- coding: utf-8 -*-

# The MIT License (MIT) - Copyright (c) 2016-2021 Dave Vandenbout.

"""
Handler for reading SPICE libraries.
"""

from __future__ import (  # isort:skip
    absolute_import,
    division,
    print_function,
    unicode_literals,
)

import os.path
from builtins import dict, int, object, range, str, zip

from future import standard_library

from ...common import USING_PYTHON2
from ...logger import active_logger
from ...net import Net
from ...part import Part
from ...pin import Pin, PinList
from ...utilities import *

standard_library.install_aliases()


# PySpice may not be installed, particularly under Python 2.
try:
    from PySpice.Spice.Library import SpiceLibrary
    from PySpice.Spice.Netlist import (
        Circuit as PySpiceCircuit,  # Avoid clash with Circuit class below.
    )
except ImportError:
    pass

# These aren't used here, but they are used in modules
# that include this module.
tool_name = "spice"
lib_suffix = [".lib", ".spice"]


def _gather_statement(file):
    """Return list of words in a complete statement read from a SPICE file."""

    statement = ""  # Holds complete SPICE statement consisting of one or more lines.
    for line in file:
        line = line.strip()

        if not line:
            continue  # Ignore blank lines.
        if line.startswith("*"):
            continue  # Ignore comments.

        if line.startswith("+"):
            # Continuation lines are appended to the statement.
            statement += " " + line[1:]
        else:
            # If the current line is not a continuation, then
            # return the statement accumulated from the previous lines.
            if statement:
                yield statement.lower().split()
            # The current line becomes the start of the next statement.
            statement = line[:]

    # Return any statement that was in-process when the file ended.
    if statement != "":
        yield statement.lower().split()


[docs]def load_sch_lib(self, filename=None, lib_search_paths_=None, lib_section=None): """ Load the .subckt I/O from a SPICE library file. Args: filename: The name of the SPICE library file. lib_search_paths_ : List of directories to search for the file. """ from ...part import Part from ...pin import Pin from ...skidl import lib_suffixes from .. import SPICE if os.path.isdir(filename): # A directory was given, so just use that. spice_lib_path = os.path.abspath(filename) else: # A file name was given, so find the absolute file path in the search paths. fp, spice_lib_path = find_and_open_file( filename=filename, paths=lib_search_paths_, ext=lib_suffixes[SPICE], exclude_binary=True, descend=-1, ) fp.close() # Close the file pointer. We just need the path to the file. # Read the Spice library from the given path. spice_lib = SpiceLibrary( root_path=spice_lib_path, recurse=True, section=lib_section ) # Get the unique set of files referenced by the subcircuits in the Spice library. lib_files = set([str(spice_lib[subcirc]) for subcirc in spice_lib.subcircuits]) # Go through the files and create a SKiDL Part for each subcircuit. for lib_file in lib_files: with open(lib_file) as f: # Read the definition of each part line-by-line and then create # a Part object that gets stored in the part list. for statement in _gather_statement(f): # Look for the start of a part definition. if statement[0] == ".subckt": # Create an un-filled part template. part = Part(part_defn="don't care", tool=SPICE, dest=LIBRARY) part.fplist = [] part.aliases = [] part.num_units = 1 part.ref_prefix = "X" part._ref = None part.filename = "" part.name = "" part.pins = [] part.pyspice = { "name": "X", "add": add_subcircuit_to_circuit, "lib": spice_lib, "lib_path": spice_lib_path, "lib_section": lib_section, } # Flesh-out the part. # Parse the part definition. pieces = statement try: # part defn: .subckt part_name pin1, pin2, ... pinN. part.name = pieces[1] part.pins = [Pin(num=p, name=p) for p in pieces[2:]] part.associate_pins() except IndexError: active_logger.warning( "Misformatted SPICE subcircuit: {}".format(part.part_defn) ) else: # Now find a symbol file for the part to assign names to the pins. # First, check for LTSpice symbol file. sym_file, sym_file_path = find_and_open_file( part.name, lib_search_paths_, ".asy", allow_failure=True, exclude_binary=True, descend=-1, ) if sym_file: pin_names = [] pin_indices = [] for sym_line in sym_file: if not sym_line: continue if sym_line.lower().startswith("pinattr pinname"): pin_names.append(sym_line.split()[2]) elif sym_line.lower().startswith("pinattr spiceorder"): pin_indices.append(sym_line.split()[2]) elif sym_line.lower().startswith("symattr description"): part.description = " ".join(sym_line.split()[2:]) sym_file.close() # Pin names and indices should be matched by the order they # appeared in the symbol file. Each index should match the # order of the pins in the .subckt file. for index, name in zip(pin_indices, pin_names): part.pins[int(index) - 1].name = name else: # No LTSpice symbol file, so check for PSPICE symbol file. sym_file, sym_file_path = find_and_open_file( filename, lib_search_paths_, ".slb", allow_failure=True, exclude_binary=True, descend=-1, ) if sym_file: pin_names = [] active = False for sym_line in sym_file: sym_line = sym_line.strip() if not sym_line: continue line_parts = sym_line.lower().split() if line_parts[0] == "*symbol": active = line_parts[1] == part.name.lower() if active: if line_parts[0] == "p": pin_names.append(line_parts[6]) elif line_parts[0] == "d": part.description = " ".join(line_parts[1:]) sym_file.close() pin_indices = list(range(len(pin_names))) for pin, name in zip(part.pins, pin_names): pin.name = name # Add subcircuit part to the library. self.add_parts(part)
[docs]def parse_lib_part(self, get_name_only=False): # pylint: disable=unused-argument """ Create a Part using a part definition from a SPICE library. """ # Parts in a SPICE library are already parsed and ready for use, # so just return the part. return self
# Classes for device and xspice models.
[docs]class XspiceModel(object): """ Object to hold the parameters for an XSPICE model. """ def __init__(self, *args, **kwargs): self.name = args[0] # The name to reference the model by. self.args = args self.kwargs = kwargs
# DeviceModel and XspiceModel are the same. # WARNING: DeviceModel overlaps a class in PySpice! DeviceModel = XspiceModel
[docs]def gen_netlist(self, **kwargs): """ Return a PySpice Circuit generated from a SKiDL circuit. Args: title: String containing the title for the PySpice circuit. libs: String or list of strings containing the paths to directories containing SPICE models. """ from ...skidl import lib_search_paths if USING_PYTHON2: return None # Replace any special chars in all net names because Spice won't like them. # Don't use self.get_nets() because that only returns a single net from a # group of attached nets so the other nets won't get renamed. for net in self.nets: net.replace_spec_chars_in_name() # Create an empty PySpice circuit. title = kwargs.pop("title", "") # Get title and remove it from kwargs. circuit = PySpiceCircuit(title) # Default SPICE libraries will be read-in down below if needed. default_libs = [] # Initialize set of libraries to include in the PySpice circuit. model_paths = set() # Paths to the model files that have been used. lib_paths = set() # Paths to the library files that have been used. lib_ids = set() # A lib_id is a tuple of the path to the lib file and a section. for part in self.parts: try: pyspice = part.pyspice except AttributeError: continue model = getattr(part, "model", None) if model: if isinstance(model, (XspiceModel, DeviceModel)): circuit.model(*model.args, **model.kwargs) else: try: path = pyspice["lib"][model] except KeyError: # The part doesn't contain the library with the model, so look elsewhere. if not default_libs: # Read the default SPICE libraries. for path in lib_search_paths[SPICE]: default_libs.append( SpiceLibrary(root_path=path, recurse=True) ) # Search for the model in the default libraries. path = None for lib in default_libs: try: path = lib[model] break except KeyError: pass if path == None: active_logger.error( "Unable to find model {} for part {}".format( model, part.ref ) ) # Include the model file if it hasn't been included yet. if path != None and path not in model_paths: circuit.include(path) model_paths.add(path) try: path, section = pyspice["lib_path"], pyspice["lib_section"] except KeyError: continue if not section: # Libraries without a section are added as include files. if path not in lib_paths: circuit.include(path) lib_paths.add(path) else: lib_id = (path, section) if lib_id not in lib_ids: circuit.lib(*lib_id) lib_ids.add(lib_id) # Add each part in the SKiDL circuit to the PySpice circuit. # TODO: Make sure self.parts is processed in order that parts were created so ngspice doesn't get references to parts before they exist. for part in self.parts: # Add each part using its add function which will be either # add_part_to_circuit() or add_subcircuit_to_circuit(). try: add_func = part.pyspice["add"] except (AttributeError, KeyError): active_logger.error("Part has no SPICE model: {}".format(part)) else: add_func(part, circuit) return circuit
[docs]def node(net_pin_part): if isinstance(net_pin_part, Net): return net_pin_part.name if isinstance(net_pin_part, Pin): return net_pin_part.net.name if isinstance(net_pin_part, Part): return net_pin_part.ref
def _xspice_node(net_or_pin): if isinstance(net_or_pin, Net): return net_or_pin.name if isinstance(net_or_pin, Pin): if net_or_pin.is_connected(): return net_or_pin.net.name else: # For XSPICE parts, unconnected pins are connected to NULL node. return "NULL" def _get_spice_ref(part): """Return a SPICE reference ID for the part.""" if part.ref.startswith(part.ref_prefix): return part.ref[len(part.ref_prefix) :] return part.ref def _get_kwargs(part, kw): """Return a dict of keyword arguments to PySpice element constructor.""" kwargs = {} for key, param_name in kw.items(): try: # The key indicates some attribute of the part. part_attr = getattr(part, key) except AttributeError: pass else: # If the keyword argument is a Part, then substitute the part # reference because it's probably a control current for something # like a current-controlled source or switch. if isinstance(part_attr, Part): kwargs.update({param_name: part_attr.ref}) # If the keyword argument is a Net, substitute the net name. elif isinstance(part_attr, Net): kwargs.update({param_name: node(part_attr)}) # If the keyword argument is a Pin, skip it. It gets handled below. elif isinstance(part_attr, Pin): continue else: kwargs.update({param_name: part_attr}) for pin in part.pins: if pin.is_connected(): try: param_name = kw[pin.name] kwargs.update({param_name: node(pin)}) except KeyError: active_logger.error( "Part {}-{} has no {} pin: {}".format( part.ref, part.name, pin.name, part ) ) return kwargs
[docs]def not_implemented(part, circuit): """Unable to add a particular SPICE part to a circuit.""" active_logger.error( "Function not implemented for {} - {}.".format(part.name, part.ref) )
[docs]def add_part_to_circuit(part, circuit): """ Add a part to a PySpice Circuit object. Args: part: SKiDL Part object. circuit: PySpice Circuit object. """ # The device reference is always the first positional argument. args = [_get_spice_ref(part)] # Get keyword arguments. kwargs = _get_kwargs(part, part.pyspice["kw"]) # Convert model argument if it exists and it's not a string. try: kwargs["model"] = part.model.name except (KeyError, AttributeError): # Don't change model kw param if it doesn't exist or is a string. pass # Add the part to the PySpice circuit. getattr(circuit, part.pyspice["name"])(*args, **kwargs)
def _get_net_names(part): """Return a list of net names attached to the pins of a part.""" return [node(pin) for pin in part.pins if pin.is_connected()]
[docs]class Parameters(dict): """Class for holding Spice subcircuit parameters.""" def __init__(self, **params): super().__init__(**params) def __copy__(self): return {k: copy(v) for k, v in self}
[docs]def add_subcircuit_to_circuit(part, circuit): """ Add a .SUBCKT part to a PySpice Circuit object. Args: part: SKiDL Part object. circuit: PySpice Circuit object. """ # The device reference is always the first positional argument. args = [_get_spice_ref(part)] args.append(part.name) args.extend(_get_net_names(part)) # Add the part to the PySpice circuit. from ...pyspice import Parameters params = {} for k, v in part.__dict__.items(): if isinstance(v, Parameters): params = v getattr(circuit, part.pyspice["name"])(*args, **params)
[docs]def add_xspice_to_circuit(part, circuit): """ Add an XSPICE part to a PySpice Circuit object. Args: part: SKiDL Part object. circuit: PySpice Circuit object. """ # The device reference is always the first positional argument. args = [_get_spice_ref(part)] # Add the pins to the argument list. for pin in part.pins: if isinstance(pin, Pin): # Add a non-vector pin. Use _xspice_node() in case pin is unconnected. args.append(_xspice_node(pin)) elif isinstance(pin, PinList): # Add pins from a pin vector. args.append("[" + " ".join([node(p) for p in pin]) + "]") else: active_logger.error("Illegal XSPICE argument: {}".format(pin)) # The XSPICE model name should be the only keyword argument. kwargs = {"model": part.model.name} # Add the part to the PySpice circuit. getattr(circuit, part.pyspice["name"])(*args, **kwargs)