####################################################################################################
#
# 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 ..Tools.StringTools import join_lines, join_list, join_dict
from .ElementParameter import (
ParameterDescriptor,
PositionalElementParameter,
FlagParameter, KeyValueParameter,
)
from .Simulation import CircuitSimulator
####################################################################################################
_module_logger = logging.getLogger(__name__)
####################################################################################################
[docs]class DeviceModel:
"""This class implements a device model.
Ngspice model types:
+------+-------------------------------+
| Code + Model Type |
+------+-------------------------------+
| R + Semiconductor resistor model |
+------+-------------------------------+
| C + Semiconductor capacitor model |
+------+-------------------------------+
| L + Inductor model |
+------+-------------------------------+
| SW + Voltage controlled switch |
+------+-------------------------------+
| CSW + Current controlled switch |
+------+-------------------------------+
| URC + Uniform distributed RC model |
+------+-------------------------------+
| LTRA + Lossy transmission line model |
+------+-------------------------------+
| D + Diode model |
+------+-------------------------------+
| NPN + NPN BJT model |
+------+-------------------------------+
| PNP + PNP BJT model |
+------+-------------------------------+
| NJF + N-channel JFET model |
+------+-------------------------------+
| PJF + P-channel JFET model |
+------+-------------------------------+
| NMOS + N-channel MOSFET model |
+------+-------------------------------+
| PMOS + P-channel MOSFET model |
+------+-------------------------------+
| NMF + N-channel MESFET model |
+------+-------------------------------+
| PMF + P-channel MESFET model |
+------+-------------------------------+
"""
##############################################
def __init__(self, name, modele_type, **parameters):
self._name = str(name)
self._model_type = str(modele_type)
self._parameters = {}
for key, value in parameters.items():
if key.endswith('_'):
key = key[:-1]
self._parameters[key] = value
##############################################
[docs] def clone(self):
# Fixme: clone parameters ???
return self.__class__(self._name, self._model_type, self._parameters)
##############################################
@property
def name(self):
return self._name
@property
def model_type(self):
return self._model_type
@property
def parameters(self):
return self._parameters.keys()
##############################################
def __getitem__(self, name):
return self._parameters[name]
##############################################
def __getattr__(self, name):
try:
return self._parameters[name]
except KeyError:
if name.endswith('_'):
return self._parameters[name[:-1]]
##############################################
def __repr__(self):
return str(self.__class__) + ' ' + self.name
##############################################
def __str__(self):
return ".model {0._name} {0._model_type} ({1})".format(self, join_dict(self._parameters))
####################################################################################################
[docs]class PinDefinition:
"""This class defines a pin of an element."""
##############################################
def __init__(self, position, name=None, alias=None, optional=False):
self._position = position
self._name = name
self._alias = alias
self._optional = optional
##############################################
[docs] def clone(self):
# Fixme: self.__class__ ???
return PinDefinition(self._position, self._name, self._alias, self._optional)
##############################################
@property
def position(self):
return self._position
@property
def name(self):
return self._name
@property
def alias(self):
return self._alias
@property
def optional(self):
return self._optional
####################################################################################################
[docs]class OptionalPin:
def __init__(self, name):
self._name = name
@property
def name(self):
return self._name
####################################################################################################
[docs]class Pin(PinDefinition):
"""This class implements a pin of an element. It stores a reference to the element, the name of the
pin and the node.
"""
_logger = _module_logger.getChild('Pin')
##############################################
def __init__(self, element, pin_definition, node):
super().__init__(pin_definition.position, pin_definition.name, pin_definition.alias)
self._element = element
self._node = node
node.connect(self)
##############################################
@property
def element(self):
return self._element
@property
def node(self):
return self._node
##############################################
def __repr__(self):
return "Pin {} of {} on node {}".format(self._name, self._element.name, self._node)
##############################################
[docs] def disconnect(self):
self._node.disconnect(self)
self._node = None
##############################################
[docs] def add_current_probe(self, circuit):
"""Add a current probe between the node and the pin.
The ammeter is named *ElementName_PinName*.
"""
# Fixme: require a reference to circuit
# Fixme: add it to a list
node = self._node
self._node = '_'.join((self._element.name, self._name))
circuit.V(self._node, node, self._node, '0')
####################################################################################################
####################################################################################################
[docs]class Element(metaclass=ElementParameterMetaClass):
"""This class implements a base class for an element.
It use a metaclass machinery for the declaration of the parameters.
"""
# These class attributes are defined in subclasses or via the metaclass.
__pins__ = None
__positional_parameters__ = None
__optional_parameters__ = None
__parameters_from_args__ = None
__spice_to_parameters__ = None
#: SPICE element prefix
__prefix__ = None
##############################################
def __init__(self, netlist, name, *args, **kwargs):
self._netlist = netlist
self._name = str(name)
self.raw_spice = ''
self.enabled = True
# Process remaining args
if len(self.__parameters_from_args__) < len(args):
raise NameError("Number of args mismatch")
for parameter, value in zip(self.__parameters_from_args__, args):
setattr(self, parameter.attribute_name, value)
# Process kwargs
for key, value in kwargs.items():
if key == 'raw_spice':
self.raw_spice = value
elif (key in self.__positional_parameters__ or
key in self.__optional_parameters__ or
key in self.__spice_to_parameters__):
setattr(self, key, value)
else:
raise ValueError('Unknown argument {}={}'.format(key, value))
self._pins = ()
netlist._add_element(self)
##############################################
[docs] def copy_to(self, element):
for parameter_dict in self.__positional_parameters__, self.__optional_parameters__:
for parameter in parameter_dict.values():
if hasattr(self, parameter.attribute_name):
value = getattr(self, parameter.attribute_name)
setattr(element, parameter.attribute_name, value)
if hasattr(self, 'raw_spice'):
element.raw_spice = self.raw_spice
##############################################
@property
def netlist(self):
return self._netlist
@property
def name(self):
return self.__prefix__ + self._name
@property
def pins(self):
return self._pins
##############################################
[docs] def detach(self):
for pin in self._pins:
pin.disconnect()
self._netlist._remove_element(self)
self._netlist = None
return self
##############################################
@property
def nodes(self):
return [pin.node for pin in self._pins]
@property
def node_names(self):
return [str(x) for x in self.nodes]
##############################################
def __repr__(self):
return self.__class__.__name__ + ' ' + self.name
##############################################
def __setattr__(self, name, value):
# Implement alias for parameters
if name in self.__spice_to_parameters__:
parameter = self.__spice_to_parameters__[name]
object.__setattr__(self, parameter.attribute_name, value)
else:
object.__setattr__(self, name, value)
##############################################
def __getattr__(self, name):
# Implement alias for parameters
if name in self.__spice_to_parameters__:
parameter = self.__spice_to_parameters__[name]
return object.__getattribute__(self, parameter.attribute_name)
else:
raise AttributeError(name)
##############################################
##############################################
[docs] def parameter_iterator(self):
""" This iterator returns the parameter in the right order. """
# Fixme: .parameters ???
for parameter_dict in self.__positional_parameters__, self.__optional_parameters__:
for parameter in parameter_dict.values():
if parameter.nonzero(self):
yield parameter
##############################################
# @property
# def parameters(self):
# return self._parameters
##############################################
##############################################
def __str__(self):
""" Return the SPICE element definition. """
return join_list((self.format_node_names(), self.format_spice_parameters(), self.raw_spice))
####################################################################################################
[docs]class AnyPinElement(Element):
__pins__ = ()
##############################################
[docs] def copy_to(self, netlist):
element = self.__class__(netlist, self._name)
super().copy_to(element)
return element
####################################################################################################
[docs]class FixedPinElement(Element):
##############################################
def __init__(self, netlist, name, *args, **kwargs):
# Get nodes
# Usage: if pins are passed using keywords then args must be empty
# optional pins are passed as keyword
pin_definition_nodes = []
number_of_args = len(args)
if number_of_args:
expected_number_of_pins = self.__class__.number_of_pins # Fixme:
if isinstance(expected_number_of_pins, slice):
expected_number_of_pins = expected_number_of_pins.start
if number_of_args < expected_number_of_pins:
raise NameError("Incomplete node list for element {}".format(self.name))
else:
nodes = args[:expected_number_of_pins]
args = args[expected_number_of_pins:]
pin_definition_nodes = zip(self.__pins__, nodes)
else:
for pin_definition in self.__pins__:
if pin_definition.name in kwargs:
node = kwargs[pin_definition.name]
del kwargs[pin_definition.name]
elif pin_definition.alias is not None and pin_definition.alias in kwargs:
node = kwargs[pin_definition.alias]
del kwargs[pin_definition.alias]
elif pin_definition.optional:
continue
else:
raise NameError("Node '{}' is missing for element {}".format(pin_definition.name, self.name))
pin_definition_nodes.append((pin_definition, node))
super().__init__(netlist, name, *args, **kwargs)
self._pins = [Pin(self, pin_definition, netlist.get_node(node, True))
for pin_definition, node in pin_definition_nodes]
##############################################
[docs] def copy_to(self, netlist):
element = self.__class__(netlist, self._name, *self.nodes)
super().copy_to(element)
return element
####################################################################################################
[docs]class NPinElement(Element):
__pins__ = '*'
##############################################
def __init__(self, netlist, name, nodes, *args, **kwargs):
super().__init__(netlist, name, *args, **kwargs)
self._pins = [Pin(self, PinDefinition(position), netlist.get_node(node, True))
for position, node in enumerate(nodes)]
##############################################
[docs] def copy_to(self, netlist):
nodes = [str(x) for x in self.nodes]
element = self.__class__(netlist, self._name, nodes)
super().copy_to(element)
return element
####################################################################################################
[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')
##############################################
def __init__(self, netlist, name):
if keyword.iskeyword(name):
self._logger.warning("Node name '{}' is a Python keyword".format(name))
self._netlist = netlist
self._name = str(name)
self._pins = set()
##############################################
def __repr__(self):
return 'Node {}'.format(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._netlist._update_node_name(self, value) # update nodes dict
self._name = value
@property
def pins(self):
return self._pins
##############################################
@property
def is_ground_node(self):
return self._name in ('0', 'gnd')
##############################################
def __bool__(self):
return bool(self._pins)
##############################################
def __iter__(self):
return iter(self._pins)
##############################################
[docs] def connect(self, pin):
if pin not in self._pins:
self._pins.add(pin)
else:
raise ValueError("Pin {} is already connected to node {}".format(pin, self))
##############################################
[docs] def disconnect(self, pin):
self._pins.remove(pin)
####################################################################################################
[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._ground_name = 0
self._nodes = {}
self._ground_node = self._add_node(self._ground_name)
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()
##############################################
[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):
return self._ground
@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()
##############################################
def __getitem__(self, attribute_name):
if attribute_name in self._elements:
return self._elements[attribute_name]
elif attribute_name in self._models:
return self._models[attribute_name]
# Fixme: subcircuits
elif attribute_name in self._nodes:
return self._nodes[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:
node = Node(self, node_name)
self._nodes[node_name] = node
return node
else:
raise ValueError("Node {} is already defined".format(node_name))
##############################################
def _update_node_name(self, node, new_name):
if node.name not in self._nodes:
# should not happen
raise ValueError("Unknown node")
del self._nodes[node.name]
self._nodes[new_name] = node
##############################################
[docs] def get_node(self, node, create=False):
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("Node {} doesn't exists".format(node))
##############################################
[docs] def has_ground_node(self):
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("Element name {} is already defined".format(element.name))
##############################################
def _remove_element(self, element):
try:
del self._elements[element.name]
except KeyError:
raise NameError("Cannot remove undefined element {}".format(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("Model name {} is already defined".format(name))
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("Duplicated nodes in {}".format(nodes))
super().__init__()
self._name = str(name)
self._external_nodes = nodes
# Fixme: ok ?
self._ground = kwargs.get('ground', 0)
if 'ground' in kwargs:
del kwargs['ground']
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("SubCircuit Nodes {} are not connected".format(not_connected_nodes))
##############################################
def __str__(self):
"""Return the formatted subcircuit definition."""
nodes = join_list(self._external_nodes)
parameters = join_list(['{}={}'.format(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=0, # Fixme: gnd = 0
global_nodes=(),
):
super().__init__()
self.title = str(title)
self._ground = ground
self._global_nodes = set(global_nodes) # .global
self._includes = [] # .include
self._parameters = {} # .param
# Fixme: not implemented
# .csparam
# .func
# .if
# .lib
##############################################
[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 parameter(self, name, expression):
"""Set a parameter."""
self._parameters[str(name)] = str(expression)
##############################################
[docs] def str(self, simulator=None):
"""Return the formatted desk."""
# 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_globals()
netlist += self._str_parameters()
netlist += super().__str__()
return netlist
##############################################
def _str_title(self):
return '.title {}'.format(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_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_lines(self._parameters, prefix='.param ') + os.linesep
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)