Source code for PySpice.Spice.Netlist

####################################################################################################
#
# PySpice - A Spice Package for Python
# Copyright (C) 2014 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 modules implements circuit and subcircuit.

The definition of a netlist follows the same conventions as SPICE. For example this SPICE netlist
is translated to Python like this:

.. code-block:: spice

    .title Voltage Divider
    Vinput in 0 10V
    R1 in out 9k
    R2 out 0 1k
    .end

.. code-block:: python3

    circuit = Circuit('Voltage Divider')
   circuit.V('input', 'in', circuit.gnd, 10)
    circuit.R(1, 'in', 'out', kilo(9))
    circuit.R(2, 'out', circuit.gnd, kilo(1))

or as a class definition:

.. code-block:: python3

      class VoltageDivider(Circuit):

          def __init__(self, **kwargs):

              super().__init__(title='Voltage Divider', **kwargs)

              self.V('input', 'in', self.gnd, '10V')
              self.R(1, 'in', 'out', kilo(9))
              self.R(2, 'out', self.gnd, kilo(1))

The circuit attribute :attr:`gnd` represents the ground of the circuit or subcircuit, usually set to
0.

We can get an element or a model using its name using these two possibilities::

    circuit['R1'] # dictionary style
    circuit.R1    # attribute style

The dictionary style always works, but the attribute only works if it complies with the Python
syntax, i.e. the element or model name is a valide attribute name (identifier), i.e. starting by a
letter and not a keyword like 'in', cf. `Python Language Reference
<https://docs.python.org/2/reference/lexical_analysis.html>`_.

We can update an element parameter like this::

    circuit.R1.resistance = kilo(1)

To simulate the circuit, we must create a simulator instance using the :meth:`Circuit.simulator`::

    simulator = circuit.simulator()

"""

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

from collections import OrderedDict
from pathlib import Path
import keyword
import logging
import os

# import networkx

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

from PySpice.Tools.StringTools import join_lines, join_list
from .DeviceModel import DeviceModel
from .Element import Pin

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

_module_logger = logging.getLogger(__name__)

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

[docs]class Node: """This class implements a node in the circuit. It stores a reference to the pins connected to the node. """ _logger = _module_logger.getChild('Node') SPICE_GROUND_NUMBER = 0 SPICE_GROUND_NAME = str(SPICE_GROUND_NUMBER) ############################################## @classmethod def _warn_iskeyword(cls, name): if keyword.iskeyword(name): cls._logger.warning(f"Node name '{name}' is a Python keyword") ############################################## def __init__(self, netlist, name): self._warn_iskeyword(name) self._netlist = netlist self._name = str(name) self._pins = set() ############################################## def __repr__(self): return f'Node {self._name}' def __str__(self): return self._name ############################################## @property def netlist(self): return self._netlist @property def name(self): return self._name @name.setter def name(self, value): self._warn_iskeyword(value) self._name = value # update nodes dict self._netlist._update_node_name(self, value) @property def is_ground_node(self): return self._name in (Node.SPICE_GROUND_NAME, 'gnd') ############################################## def __bool__(self): return bool(self._pins) def __len__(self): return len(self._pins) def __iter__(self): return iter(self._pins) @property def pins(self): # Fixme: iter ? return iter(self._pins) def __contains__(self, pin): return pin in self._pins ##############################################
[docs] def connect(self, pin): self._logger.info(f"Connect {pin} => {self}") if pin not in self: self._pins.add(pin) else: # Fixme: could just warn ??? raise ValueError(f"Pin {pin} is already connected to node {self}")
##############################################
[docs] def disconnect(self, pin): self._logger.info(f"Disconnect {pin}") self._pins.remove(pin)
##############################################
[docs] def merge(self, node): self._logger.info(f"Merge {self} and {node}") for pin in list(node.pins): pin.disconnect() pin.connect(self) self._netlist._del_node(node)
############################################## def __iadd__(self, args): """Connect a node, a pin or a list of them to the node.""" if isinstance(args, (Node, Pin)): args = (args,) for obj in args: if isinstance(obj, Node): # node <=> node self.merge(obj) elif isinstance(obj, Pin): # node <= pin if obj.connected: self.merge(obj.node) else: obj.connect(self) else: raise ValueError(f"Invalid object {type(obj)}") return self
####################################################################################################
[docs]class Netlist: """This class implements a base class for a netlist. .. note:: This class is completed with element shortcuts when the module is loaded. """ _logger = _module_logger.getChild('Netlist') ############################################## def __init__(self): self._nodes = {} self._ground_name = Node.SPICE_GROUND_NAME # Fixme: just here self._ground_node = self._add_node(self._ground_name) self._ground = None # Fixme: purpose ??? self._subcircuits = OrderedDict() # to keep the declaration order self._elements = OrderedDict() # to keep the declaration order self._models = {} self.raw_spice = '' # self._graph = networkx.Graph() ############################################## def __setstate__(self, state): self.__dict__.update(state) ##############################################
[docs] def copy_to(self, netlist): for subcircuit in self.subcircuits: netlist.subcircuit(subcircuit) for element in self.elements: element.copy_to(netlist) for name, model in self._models.items(): netlist._models[name] = model.clone() netlist.raw_spice = str(self.raw_spice) return netlist
############################################## @property def gnd(self): # Fixme: purpose ??? # return self._ground return self._ground_node # Note: # circuit.gnd += ... # call a setter... @property def nodes(self): return self._nodes.values() @property def node_names(self): return self._nodes.keys() @property def elements(self): return self._elements.values() @property def element_names(self): return self._elements.keys() @property def models(self): return self._models.values() @property def model_names(self): return self._models.keys() @property def subcircuits(self): return self._subcircuits.values() @property def subcircuit_names(self): return self._subcircuits.keys() ##############################################
[docs] def element(self, name): return self._elements[name]
# Fixme: clash with # def model(self, name, modele_type, **parameters): # def model(self, name): # return self._models[name] # Fixme: versus get node ???
[docs] def node(self, name): return self._nodes[str(name)]
############################################## def __getitem__(self, attribute_name): if attribute_name in self._elements: return self.element(attribute_name) elif attribute_name in self._models: return self.model(attribute_name) # Fixme: subcircuits elif attribute_name in self._nodes: return self.node(attribute_name) else: raise IndexError(attribute_name) # KeyError ############################################## def __getattr__(self, attribute_name): try: return self.__getitem__(attribute_name) except IndexError: raise AttributeError(attribute_name) ############################################## def _add_node(self, node_name): node_name = str(node_name) if node_name not in self._nodes: self._logger.info(f'Create node "{node_name}"') node = Node(self, node_name) self._nodes[node_name] = node return node else: raise ValueError(f"Node {node_name} is already defined") ############################################## def _del_node(self, node): del self._nodes[node.name] ############################################## def _update_node_name(self, node, new_name): """Update the node's map for the new node's name""" # Fixme: check node is None ??? if node.name not in self._nodes: # should not happen raise ValueError(f"Unknown node {node}") self._nodes[new_name] = self._nodes.pop(node.name) ##############################################
[docs] def get_node(self, node, create=False): """Return a node. `node` can be a node instance or node name. A node is created if `create` is set and the node don't yet exist. """ # Fixme: dangling... if node is None: return None # Fixme: always ok ??? if isinstance(node, Node): return node else: str_node = str(node) if str_node in self._nodes: return self._nodes[str_node] elif create: return self._add_node(str_node) else: raise KeyError(f"Node {node} doesn't exists")
##############################################
[docs] def has_ground_node(self): """Test if ground node is connected""" return bool(self._ground_node)
############################################## def _add_element(self, element): """Add an element.""" if element.name not in self._elements: self._elements[element.name] = element else: raise NameError(f"Element name {element.name} is already defined") ############################################## def _remove_element(self, element): try: del self._elements[element.name] except KeyError: raise NameError(f"Cannot remove undefined element {element}") ##############################################
[docs] def model(self, name, modele_type, **parameters): """Add a model.""" model = DeviceModel(name, modele_type, **parameters) if model.name not in self._models: self._models[model.name] = model else: raise NameError(f"Model name {name} is already defined") return model
##############################################
[docs] def subcircuit(self, subcircuit): """Add a sub-circuit.""" # Fixme: subcircuit is a class self._subcircuits[str(subcircuit.name)] = subcircuit
############################################## def __str__(self): """ Return the formatted list of element and model definitions. """ # Fixme: order ??? netlist = self._str_raw_spice() netlist += self._str_subcircuits() # before elements netlist += self._str_elements() netlist += self._str_models() return netlist ############################################## def _str_elements(self): elements = [element for element in self.elements if element.enabled] return join_lines(elements) + os.linesep ############################################## def _str_models(self): if self._models: return join_lines(self.models) + os.linesep else: return '' ############################################## def _str_subcircuits(self): if self._subcircuits: return join_lines(self.subcircuits) else: return '' ############################################## def _str_raw_spice(self): netlist = self.raw_spice if netlist and not netlist.endswith(os.linesep): netlist += os.linesep return netlist
####################################################################################################
[docs]class SubCircuit(Netlist): """This class implements a sub-cicuit netlist.""" ############################################## def __init__(self, name, *nodes, **kwargs): if len(set(nodes)) != len(nodes): raise ValueError(f"Duplicated nodes in {nodes}") super().__init__() self._name = str(name) self._external_nodes = nodes # Fixme: ok ? self._ground = kwargs.pop('ground', Node.SPICE_GROUND_NUMBER) self._parameters = kwargs ##############################################
[docs] def clone(self, name=None): if name is None: name = self._name # Fixme: clone parameters ??? kwargs = dict(self._parameters) kwargs['ground'] = self._ground subcircuit = self.__class__(name, list(self._external_nodes), **kwargs) self.copy_to(subcircuit)
############################################## @property def name(self): return self._name @property def external_nodes(self): return self._external_nodes @property def parameters(self): """Parameters""" return self._parameters ##############################################
[docs] def check_nodes(self): """Check for dangling nodes in the subcircuit.""" nodes = self._external_nodes connected_nodes = set() for element in self.elements: connected_nodes.add(nodes & element.nodes) not_connected_nodes = nodes - connected_nodes if not_connected_nodes: raise NameError(f"SubCircuit Nodes {not_connected_nodes} are not connected")
############################################## def __str__(self): """Return the formatted subcircuit definition.""" nodes = join_list(self._external_nodes) parameters = join_list(['f{key}={value}' for key, value in self._parameters.items()]) netlist = '.subckt ' + join_list((self._name, nodes, parameters)) + os.linesep netlist += super().__str__() netlist += '.ends ' + self._name + os.linesep return netlist
####################################################################################################
[docs]class SubCircuitFactory(SubCircuit): NAME = None NODES = None ############################################## def __init__(self, **kwargs): super().__init__(self.NAME, *self.NODES, **kwargs)
####################################################################################################
[docs]class Circuit(Netlist): """This class implements a cicuit netlist. To get the corresponding Spice netlist use:: circuit = Circuit() ... str(circuit) """ _logger = _module_logger.getChild('Circuit') ############################################## def __init__(self, title, ground=Node.SPICE_GROUND_NUMBER, # Fixme: gnd = Node.SPICE_GROUND_NUMBER global_nodes=(), ): super().__init__() self.title = str(title) self._ground = ground self._global_nodes = set(global_nodes) # .global self._includes = [] # .include self._libs = [] # .lib, contains a (name, section) tuple self._parameters = {} # .param # Fixme: not implemented # .csparam # .func # .if ##############################################
[docs] def clone(self, title=None): if title is None: title = self.title circuit = self.__class__(title, self._ground, set(self._global_nodes)) self.copy_to(circuit) for include in self._includes: circuit.include(include) for name, value in self._parameters.items(): self.parameter(name, value) return circuit
##############################################
[docs] def include(self, path): """Include a file.""" if path not in self._includes: self._includes.append(path) else: self._logger.warn("Duplicated include")
##############################################
[docs] def lib(self, name, section=None): """Load a library.""" v = (name, section) if v not in self._libs: self._libs.append(v) else: self._logger.warn(f"Duplicated lib {v}")
##############################################
[docs] def parameter(self, name, expression): """Set a parameter.""" self._parameters[str(name)] = str(expression)
##############################################
[docs] def str(self, simulator=None): """Return the formatted desk. :param simulator: simulator instance to select the flavour of a Spice library """ # if not self.has_ground_node(): # raise NameError("Circuit don't have ground node") netlist = self._str_title() netlist += self._str_includes(simulator) netlist += self._str_libs(simulator) netlist += self._str_globals() netlist += self._str_parameters() netlist += super().__str__() return netlist
############################################## def _str_title(self): return f'.title {self.title}' + os.linesep ############################################## def _str_includes(self, simulator=None): if self._includes: # ngspice don't like // in path, thus ensure we write real paths real_paths = [] for path in self._includes: path = Path(str(path)).resolve() if simulator: path_flavour = Path(str(path) + '@' + simulator) if path_flavour.exists(): path = path_flavour real_paths.append(path) return join_lines(real_paths, prefix='.include ') + os.linesep else: return '' ############################################## def _str_libs(self, simulator=None): if self._libs: libs = [] for lib, section in self._libs: lib = Path(str(lib)).resolve() if simulator: lib_flavour = Path(f"{lib}@{simulator}") if lib_flavour.exists(): lib = lib_flavour s = f".lib {lib}" if section: s += f" {section}" libs.append(s) return os.linesep.join(libs) + os.linesep else: return '' ############################################## def _str_globals(self): if self._global_nodes: return '.global ' + join_list(self._global_nodes) + os.linesep else: return '' ############################################## def _str_parameters(self): if self._parameters: return ''.join([f'.param {key}={value}' + os.linesep for key, value in self._parameters.items()]) else: return '' ############################################## def __str__(self): return self.str(simulator=None) ##############################################
[docs] def str_end(self): return str(self) + '.end' + os.linesep
##############################################
[docs] def simulator(self, *args, **kwargs): # return CircuitSimulator.factory(self, *args, **kwargs) raise NameError("Deprecated API")