+
+ Keyword Parameters:
+
+ :attr:`model`
+
+ :attr:`multiplier`
+ alias `m`
+
+ :attr:`length`
+ alias `l`
+
+ :attr:`width`
+ alias `w`
+
+ :attr:`nfin`
+ only for Xyce
+
+ :attr:`drain_area`
+ alias `ad`
+
+ :attr:`source_area`
+ alias `as`
+
+ :attr:`drain_perimeter`
+ alias `pd`
+
+ :attr:`source_perimeter`
+ alias `ps`
+
+ :attr:`drain_number_square`
+ alias `nrd`
+
+ :attr:`source_number_square`
+ alias `nrs`
+
+ :attr:`off`
+
+ :attr:`ic`
+
+ :attr:`temperature`
+ alias `temp`
+
+ Attributes:
+
+ :attr:`model`
+
+ :attr:`multiplier`
+
+ :attr:`length`
+
+ :attr:`width`
+
+ :attr:`nfin`
+ only for Xyce
+
+ :attr:`drain_area`
+
+ :attr:`source_area`
+
+ :attr:`drain_perimeter`
+
+ :attr:`source_perimeter`
+
+ :attr:`drain_number_square`
+
+ :attr:`source_number_square`
+
+ :attr:`off`
+
+ :attr:`ic`
+
+ :attr:`temperature`
+
+ """
+
+ # Fixme: off doesn't fit in kwargs !
+
+ ALIAS = 'M'
+ LONG_ALIAS = 'MOSFET'
+ PREFIX = 'M'
+ PINS = ('drain', 'gate', 'source', ('bulk', 'substrate'))
+
+ model = ModelPositionalParameter(position=0, key_parameter=True)
+ multiplier = IntKeyParameter('m')
+ length = FloatKeyParameter('l', unit=U_m)
+ width = FloatKeyParameter('w', unit=U_m)
+ drain_area = FloatKeyParameter('ad')
+ source_area = FloatKeyParameter('as')
+ drain_perimeter = FloatKeyParameter('pd')
+ source_perimeter = FloatKeyParameter('ps')
+ drain_number_square = FloatKeyParameter('nrd')
+ source_number_square = FloatKeyParameter('nrs')
+ off = FlagParameter('off')
+ ic = FloatTripletKeyParameter('ic')
+ temperature = FloatKeyParameter('temp', unit=U_Degree)
+
+ # only for Xyce
+ nfin = IntKeyParameter('nfin')
+
+####################################################################################################
+#
+# Transmission Lines
+#
+####################################################################################################
+
+class LosslessTransmissionLine(TwoPortElement):
+
+ """This class implements a lossless transmission line.
+
+ Spice syntax:
+
+ .. code-block:: none
+
+ TXXXXXXX N1 N2 N3 N4 Z0=VALUE >
+
+ where TD or F, NL must be specified.
+
+ Keyword Parameters:
+
+ :attr:`impedance`
+ alias:`Z0`
+ is the characteristic impedance
+
+ :attr:`time_delay`
+ alias:`TD`
+ is the transmission delay
+
+ :attr:`frequency`
+ alias:`F`
+
+ :attr:`normalized_length`
+ alias:`NL`
+
+ Attributes:
+
+ :attr:`impedance`
+
+ :attr:`time_delay`
+
+ :attr:`frequency`
+
+ :attr:`normalized_length`
+
+ The transmission delay, `td`, may be specified directly (as `td=10ns`, for example).
+ Alternatively, a frequency `f` may be given, together with `nl`, the normalized electrical
+ length of the transmission line with respect to the wavelength in the line at the frequency
+ `f`. If a frequency is specified but `nl` is omitted, 0.25 is assumed (that is, the frequency is
+ assumed to be the quarter-wave frequency). Note that although both forms for expressing the line
+ length are indicated as optional, one of the two must be specified.
+
+ Note: Either time_delay or frequency must be given.
+
+ """
+
+ ALIAS = 'TransmissionLine'
+ PREFIX = 'T'
+
+ impedance = FloatKeyParameter('Z0', default=50, unit=U_Ω)
+ time_delay = FloatKeyParameter('TD', unit=U_s)
+ frequency = FloatKeyParameter('F', unit=U_Hz)
+ normalized_length = FloatKeyParameter('NL')
+
+ ##############################################
+
+ def __init__(self, name, *args, **kwargs):
+
+ super().__init__(name, *args, **kwargs)
+
+ if not (self.has_parameter('time_delay') or
+ (self.has_parameter('frequency') and self.has_parameter('normalized_length'))):
+ raise NameError('Either TD or F, NL must be specified')
+
+####################################################################################################
+
+class LossyTransmission(TwoPortElement):
+
+ """This class implements lossy transmission lines.
+
+ Spice syntax:
+
+ .. code-block:: none
+
+ OXXXXXXX n1 n2 n3 n4 model
+
+ Attributes:
+
+ :attr:`model`
+
+ .. note:: As opposite to Spice, the model is specified before the nodes so as to act as `*args`.
+
+ """
+
+ ALIAS = 'O'
+ PREFIX = 'O'
+
+ model = ModelPositionalParameter(position=0, key_parameter=True)
+
+####################################################################################################
+
+class CoupledMulticonductorLine(NPinElement):
+
+ """This class implements coupled multiconductor lines.
+
+ Spice syntax:
+
+ .. code-block:: none
+
+ PXXXXXXX NI1 NI2 ... NIX GND1 NO1 NO2 ... NOX GND2 model
+
+ Attributes:
+
+ :attr:`model`
+
+ :attr:`length`
+ alias `len`
+ length of the line in meters
+
+ .. note:: As opposite to Spice, the model is specified before the nodes so as to act as `*args`.
+
+ """
+
+ ALIAS = 'P'
+ PREFIX = 'P'
+
+ model = ModelPositionalParameter(position=0, key_parameter=True)
+ length = FloatKeyParameter('len', unit=U_m)
+
+ ##############################################
+
+ def __init__(self, netlist, name, *nodes, **parameters):
+
+ super().__init__(netlist, name, nodes, **parameters)
+
+####################################################################################################
+
+class UniformDistributedRCLine(FixedPinElement):
+
+ """This class implements uniform distributed RC lines.
+
+ Spice syntax:
+
+ .. code-block:: none
+
+ UXXXXXXX n1 n2 n3 model l=length
+
+ Attributes:
+
+ :attr:`model`
+
+ :attr:`length`
+ alias `l`
+ length of the RC line in meters
+
+ :attr:`number_of_lumps`
+ alias `n`
+
+ .. note:: As opposite to Spice, the model is specified before the nodes so as to act as `*args`.
+
+ """
+
+ ALIAS = 'U'
+ PREFIX = 'U'
+ PINS = ('output', 'input', 'capacitance_node')
+
+ model = ModelPositionalParameter(position=0, key_parameter=True)
+ length = FloatKeyParameter('l', unit=U_m)
+ number_of_lumps = IntKeyParameter('n')
+
+####################################################################################################
+
+class SingleLossyTransmissionLine(TwoPortElement):
+
+ # Fixme: special TwoPortElement
+
+ """This class implements single lossy transmission lines.
+
+ Spice syntax:
+
+ .. code-block:: none
+
+ YXXXXXXX N1 0 N2 0 model
+
+ Attributes:
+
+ :attr:`model`
+
+ :attr:`length`
+ alias `len`
+ length of the line in meters
+
+ .. note:: As opposite to Spice, the model is specified before the nodes so as to act as `*args`.
+
+ """
+
+ ALIAS = 'Y'
+ PREFIX = 'Y'
+
+ model = ModelPositionalParameter(position=0, key_parameter=True)
+ length = FloatKeyParameter('len', unit=U_m)
+
+####################################################################################################
+#
+# XSPICE
+#
+####################################################################################################
+
+class XSpiceElement(NPinElement):
+
+ """This class implements a sub-circuit.
+
+ Spice syntax:
+
+ .. code-block:: none
+
+ AXXXXXXX <%v ,%i ,%vd ,%id ,%g,%gd ,%h,%hd , or %d>
+ + <[> <~><%v ,%i ,%vd ,%id ,%g,%gd ,%h,%hd , or %d>
+ +
+ + <~>...< NIN2 .. <]> >
+ + <%v ,%i ,%vd ,%id ,%g,%gd ,%h,%hd ,%d or %vnam >
+ + <[> <~><%v ,%i ,%vd ,%id ,%g,%gd ,%h,%hd ,
+ or %d>< NOUT1 or +NOUT1 -NOUT1 >
+ + <~>...< NOUT2 .. <]>>
+ + MODELNAME
+
+ . MODEL MODELNAME MODELTYPE
+ + <( PARAMNAME1 = <[> VAL1 > PARAMNAME2 ..>)>
+
+ Attributes:
+
+ :attr:`model`
+
+ .. note:: As opposite to Spice, the model is specified before the nodes so as to act as `*args`.
+
+ .. warning:: Partially implemented.
+ """
+
+ ALIAS = 'A'
+ PREFIX = 'A'
+
+ model = ModelPositionalParameter(position=0, key_parameter=True)
+
+ ##############################################
+
+ def __init__(self, netlist, name, *nodes, **parameters):
+
+ # Fixme: ok ???
+
+ super().__init__(netlist, name, nodes, **parameters)
+
+####################################################################################################
+#
+# GSS
+#
+####################################################################################################
+
+class GSSElement(NPinElement):
+
+ """This class implements GSS device.
+
+ .. warning:: Not implemented
+ """
+
+ ALIAS = 'N'
+ PREFIX = 'N'
+
+ ##############################################
+
+ def __init__(self):
+
+ raise NotImplementedError
diff --git a/third_party/PySpice-org/PySpice/PySpice/Spice/ElementParameter.py b/third_party/PySpice-org/PySpice/PySpice/Spice/ElementParameter.py
new file mode 100644
index 00000000..19e70d57
--- /dev/null
+++ b/third_party/PySpice-org/PySpice/PySpice/Spice/ElementParameter.py
@@ -0,0 +1,395 @@
+####################################################################################################
+#
+# 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 .
+#
+####################################################################################################
+
+"""This modules implements the machinery to define element's parameters as descriptors.
+
+"""
+
+####################################################################################################
+
+from ..Unit import Unit
+from ..Tools.StringTools import str_spice
+
+####################################################################################################
+
+class ParameterDescriptor:
+
+ """This base class implements a descriptor for element parameters.
+
+ Public Attributes:
+
+ :attr:`attribute_name`
+ Name of the attribute in the element's class
+
+ :attr:`default_value`
+ The default value
+
+ """
+
+ ##############################################
+
+ def __init__(self, default=None):
+
+ self._default_value = default
+ self._attribute_name = None
+
+ ##############################################
+
+ @property
+ def default_value(self):
+ return self._default_value
+
+ @property
+ def attribute_name(self):
+ return self._attribute_name
+
+ @attribute_name.setter
+ def attribute_name(self, name):
+ self._attribute_name = name
+
+ ##############################################
+
+ def __get__(self, instance, owner=None):
+
+ try:
+ return getattr(instance, '_' + self._attribute_name)
+ except AttributeError:
+ return self.default_value
+
+ ##############################################
+
+ def __set__(self, instance, value):
+ setattr(instance, '_' + self._attribute_name, value)
+
+ ##############################################
+
+ def __repr__(self):
+ return self.__class__.__name__
+
+ ##############################################
+
+ def validate(self, value):
+
+ """Validate the parameter's value."""
+
+ return value
+
+ ##############################################
+
+ def nonzero(self, instance):
+ return self.__get__(instance) is not None
+
+ ##############################################
+
+ def to_str(self, instance):
+
+ """Convert the parameter's value to SPICE syntax."""
+
+ raise NotImplementedError
+
+ ##############################################
+
+ def __lt__(self, other):
+ return self._attribute_name < other.attribute_name
+
+####################################################################################################
+
+class PositionalElementParameter(ParameterDescriptor):
+
+ """This class implements a descriptor for positional element parameters.
+
+ Public Attributes:
+
+ :attr:`key_parameter`
+ Flag to specify if the parameter is passed as key parameter in Python
+
+ :attr:`position`
+ Position of the parameter in the element definition
+
+ """
+
+ ##############################################
+
+ def __init__(self, position, default=None, key_parameter=False):
+
+ super().__init__(default)
+
+ self._position = position
+ self._key_parameter = key_parameter
+
+ ##############################################
+
+ @property
+ def position(self):
+ return self._position
+
+ @property
+ def key_parameter(self):
+ return self._key_parameter
+
+ ##############################################
+
+ def to_str(self, instance):
+ return str_spice(self.__get__(instance))
+
+ ##############################################
+
+ def __lt__(self, other):
+ return self._position < other.position
+
+####################################################################################################
+
+class ElementNamePositionalParameter(PositionalElementParameter):
+
+ """This class implements an element name positional parameter."""
+
+ ##############################################
+
+ def validate(self, value):
+ return str(value)
+
+####################################################################################################
+
+class ExpressionPositionalParameter(PositionalElementParameter):
+
+ """This class implements an expression positional parameter. """
+
+ ##############################################
+
+ def validate(self, value):
+ return str(value)
+
+####################################################################################################
+
+class FloatPositionalParameter(PositionalElementParameter):
+
+ """This class implements a float positional parameter."""
+
+ ##############################################
+
+ def __init__(self, position, unit=None, **kwargs):
+
+ super().__init__(position, **kwargs)
+ self._unit = unit
+
+ ##############################################
+
+ def validate(self, value):
+
+ if isinstance(value, Unit):
+ return value
+ else:
+ return Unit(value)
+
+####################################################################################################
+
+class InitialStatePositionalParameter(PositionalElementParameter):
+
+ """This class implements an initial state (on, off) positional parameter."""
+
+ ##############################################
+
+ def validate(self, value):
+ return bool(value) # Fixme: check KeyParameter
+
+ ##############################################
+
+ def to_str(self, instance):
+
+ if self.__get__(instance):
+ return 'on'
+ else:
+ return 'off'
+
+####################################################################################################
+
+class ModelPositionalParameter(PositionalElementParameter):
+
+ """This class implements a model positional parameter. """
+
+ ##############################################
+
+ def validate(self, value):
+ return str(value)
+
+####################################################################################################
+
+class FlagParameter(ParameterDescriptor):
+
+ """This class implements a flag parameter.
+
+ Public Attributes:
+
+ :attr:`spice_name`
+ Name of the parameter
+
+ """
+
+ ##############################################
+
+ def __init__(self, spice_name, default=False):
+
+ super().__init__(default)
+
+ self.spice_name = spice_name
+
+ ##############################################
+
+ def nonzero(self, instance):
+ return bool(self.__get__(instance))
+
+ ##############################################
+
+ def to_str(self, instance):
+
+ if self.nonzero(instance):
+ return 'off'
+ else:
+ return ''
+
+####################################################################################################
+
+class KeyValueParameter(ParameterDescriptor):
+
+ """This class implements a key value pair parameter.
+
+ Public Attributes:
+
+ :attr:`spice_name`
+ Name of the parameter
+
+ """
+
+ ##############################################
+
+ def __init__(self, spice_name, default=None):
+
+ super().__init__(default)
+
+ self.spice_name = spice_name
+
+ ##############################################
+
+ def str_value(self, instance):
+ return str_spice(self.__get__(instance))
+
+ ##############################################
+
+ def to_str(self, instance):
+
+ if bool(self):
+ return '{}={}'.format(self.spice_name, self.str_value(instance))
+ else:
+ return ''
+
+####################################################################################################
+
+class BoolKeyParameter(KeyValueParameter):
+
+ """This class implements a boolean key parameter."""
+
+ ##############################################
+
+ def nonzero(self, instance):
+ return bool(self.__get__(instance))
+
+ ##############################################
+
+ def str_value(self, instance):
+
+ if self.nonzero(instance):
+ return '1'
+ else:
+ return '0'
+
+####################################################################################################
+
+class ExpressionKeyParameter(KeyValueParameter):
+
+ """This class implements an expression key parameter."""
+
+ ##############################################
+
+ def validate(self, value):
+ return str(value)
+
+####################################################################################################
+
+class FloatKeyParameter(KeyValueParameter):
+
+ """This class implements a float key parameter."""
+
+ ##############################################
+
+ def __init__(self, spice_name, unit=None, **kwargs):
+
+ super().__init__(spice_name, **kwargs)
+ self._unit = unit
+
+ ##############################################
+
+ def validate(self, value):
+ return float(value)
+
+####################################################################################################
+
+class FloatPairKeyParameter(KeyValueParameter):
+
+ """This class implements a float pair key parameter. """
+
+ ##############################################
+
+ def validate(self, pair):
+
+ if len(pair) == 2:
+ return (float(pair[0]), float(pair[1]))
+ else:
+ raise ValueError()
+
+ ##############################################
+
+ def str_value(self, instance):
+ return ','.join([str(value) for value in self.__get__(instance)])
+
+####################################################################################################
+
+class FloatTripletKeyParameter(FloatPairKeyParameter):
+
+ """This class implements a triplet key parameter."""
+
+ ##############################################
+
+ def validate(self, uplet):
+
+ if len(uplet) == 3:
+ return (float(uplet[0]), float(uplet[1]), float(uplet[2]))
+ else:
+ raise ValueError()
+
+####################################################################################################
+
+class IntKeyParameter(KeyValueParameter):
+
+ """This class implements an integer key parameter."""
+
+ ##############################################
+
+ def validate(self, value):
+ return int(value)
diff --git a/third_party/PySpice-org/PySpice/PySpice/Spice/Expression/Ast.py b/third_party/PySpice-org/PySpice/PySpice/Spice/Expression/Ast.py
new file mode 100644
index 00000000..548815d8
--- /dev/null
+++ b/third_party/PySpice-org/PySpice/PySpice/Spice/Expression/Ast.py
@@ -0,0 +1,407 @@
+####################################################################################################
+#
+# PySpice - A Spice Package for Python
+# Copyright (C) 2017 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 .
+#
+####################################################################################################
+
+"""This module implements Abstract Syntactic Tree for Spice expressions.
+"""
+
+####################################################################################################
+
+import logging
+import os
+
+####################################################################################################
+
+_module_logger = logging.getLogger(__name__)
+
+####################################################################################################
+
+class StatementList:
+
+ ##############################################
+
+ def __init__(self, *statements):
+
+ self._statements = list(statements)
+
+ ##############################################
+
+ def __nonzero__(self):
+
+ return bool(self._statements)
+
+ ##############################################
+
+ def __iter__(self):
+
+ return iter(self._statements)
+
+ ##############################################
+
+ def add(self, statement):
+
+ self._statements.append(statement)
+
+ ##############################################
+
+ def __str__(self):
+
+ return os.linesep.join([str(statement) for statement in self])
+
+####################################################################################################
+
+class Program(StatementList):
+ pass
+
+####################################################################################################
+
+class Variable:
+
+ ##############################################
+
+ def __init__(self, name):
+ self._name = name
+
+ ##############################################
+
+ @property
+ def name(self):
+ return self._name
+
+ ##############################################
+
+ def __str__(self):
+ return self._name
+
+####################################################################################################
+
+class Constant:
+
+ ##############################################
+
+ def __init__(self, value):
+ self._value = value
+
+ ##############################################
+
+ def __str__(self):
+ return str(self._value)
+
+####################################################################################################
+
+class IntConstant(Constant):
+
+ ##############################################
+
+ def __int__(self):
+ return self._value
+
+####################################################################################################
+
+class FloatConstant(Constant):
+
+ ##############################################
+
+ def __float__(self):
+ return self._value
+
+####################################################################################################
+
+class Expression:
+
+ NUMBER_OF_OPERANDS = None
+
+ ##############################################
+
+ def __init__(self, *args, **kwargs):
+
+ if (self.NUMBER_OF_OPERANDS is not None
+ and len(args) != self.NUMBER_OF_OPERANDS):
+ raise ValueError("Wrong number of operands")
+
+ self._operands = args
+
+ ##############################################
+
+ def iter_on_operands(self):
+ return iter(self._operands)
+
+ ##############################################
+
+ @property
+ def operand(self):
+ return self._operands[0]
+
+ @property
+ def operand1(self):
+ return self._operands[0]
+
+ @property
+ def operand2(self):
+ return self._operands[1]
+
+ @property
+ def operand3(self):
+ return self._operands[2]
+
+class UnaryExpression(Expression):
+ NUMBER_OF_OPERANDS = 1
+
+class BinaryExpression(Expression):
+ NUMBER_OF_OPERANDS = 2
+
+class TernaryExpression(Expression):
+ NUMBER_OF_OPERANDS = 3
+
+####################################################################################################
+
+class OperatorMetaclass(type):
+
+ """Metaclass to register operators"""
+
+ _declaration_order = 0
+ _operators = []
+ _unary_operator_map = {}
+ _binary_operator_map = {}
+
+ ##############################################
+
+ def __new__(meta, class_name, base_classes, attributes):
+
+ cls = type.__new__(meta, class_name, base_classes, attributes)
+ if cls.OPERATOR is not None:
+ meta.register_prefix(cls)
+ return cls
+
+ ##############################################
+
+ @classmethod
+ def register_prefix(meta, cls):
+
+ cls._declaration_order = meta._declaration_order
+ meta._declaration_order += 1
+ meta._operators.append(cls)
+ if issubclass(cls, UnaryOperator):
+ d = meta._unary_operator_map
+ elif issubclass(cls, BinaryOperator):
+ d = meta._binary_operator_map
+ d[cls.OPERATOR] = cls
+
+ ##############################################
+
+ @classmethod
+ def operator_iter(cls):
+ return iter(cls._operators)
+
+ ##############################################
+
+ @classmethod
+ def get_unary(cls, operator):
+ return cls._unary_operator_map[operator]
+
+ ##############################################
+
+ @classmethod
+ def get_binary(cls, operator):
+ return cls._binary_operator_map[operator]
+
+####################################################################################################
+
+class OperatorMixin(metaclass=OperatorMetaclass):
+ OPERATOR = None
+ _declaration_order = 0
+
+####################################################################################################
+
+class UnaryOperator(UnaryExpression, OperatorMixin):
+
+ def __str__(self):
+ return ' '.join((self.OPERATOR, str(self.operand1)))
+
+####################################################################################################
+
+class BinaryOperator(BinaryExpression, OperatorMixin):
+
+ def __str__(self):
+ return ' '.join((str(self.operand1), self.OPERATOR, str(self.operand2)))
+
+####################################################################################################
+
+class Assignation(BinaryExpression):
+
+ @property
+ def variable(self):
+ return self._operands[1]
+
+ @property
+ def value(self):
+ return self._operands[0]
+
+ def __str__(self):
+ return ' '.join((str(self.destination), '=', str(self.value)))
+
+####################################################################################################
+
+class Negation(UnaryOperator):
+ OPERATOR = '-'
+ PRECEDENCE = 1
+
+class Not(UnaryOperator):
+ OPERATOR = '!'
+ PRECEDENCE = 1
+
+####################################################################################################
+
+class power(BinaryOperator):
+ OPERATOR = '**'
+ PRECEDENCE = 2
+
+class Multiplication(BinaryOperator):
+ OPERATOR = '*'
+ PRECEDENCE = 3
+
+class Division(BinaryOperator):
+ OPERATOR = '/'
+ PRECEDENCE = 3
+
+class Modulo(BinaryOperator):
+ OPERATOR = '%'
+ PRECEDENCE = 3
+
+class IntegerDivision(BinaryOperator):
+ OPERATOR = '\\'
+ PRECEDENCE = 3
+
+class Addition(BinaryOperator):
+ OPERATOR = '+'
+ PRECEDENCE = 4
+
+class Subtraction(BinaryOperator):
+ OPERATOR = '-'
+ PRECEDENCE = 4
+
+####################################################################################################
+
+class Equal(BinaryOperator):
+ OPERATOR = '=='
+ PRECEDENCE = 5
+
+class NotEqual(BinaryOperator):
+ OPERATOR = '!='
+ PRECEDENCE = 5
+
+class LessEqual(BinaryOperator):
+ OPERATOR = '<='
+
+class GreaterEqual(BinaryOperator):
+ OPERATOR = '>='
+ PRECEDENCE = 5
+
+class Less(BinaryOperator):
+ OPERATOR = '<'
+ PRECEDENCE = 5
+
+class Greater(BinaryOperator):
+ OPERATOR = '>'
+ PRECEDENCE = 5
+
+####################################################################################################
+
+class And(BinaryOperator):
+ OPERATOR = '&&'
+ PRECEDENCE = 6
+
+class Or(BinaryOperator):
+ OPERATOR = '||'
+ PRECEDENCE = 7
+
+####################################################################################################
+
+class If: #(TernaryExpression)
+
+ # c ? x : y
+
+ PRECEDENCE = 8
+
+ ##############################################
+
+ def __init__(self, condition, then_expression, else_expression):
+
+ self._condition = condition
+ self._then_expression = then_expression
+ self._else_expression = else_expression
+
+ ##############################################
+
+ @property
+ def condition(self):
+ return self._condition
+
+ ##############################################
+
+ @property
+ def then_expression(self):
+ return self._then_expression
+
+ ##############################################
+
+ @property
+ def else_expression(self):
+ return self._else_expression
+
+ ##############################################
+
+ # def _str_compound_expression(self, expressions):
+
+ # string = '(' + os.linesep
+ # if expressions:
+ # string += str(expressions) + os.linesep
+ # string += ')'
+
+ # return string
+
+ ##############################################
+
+ def __str__(self):
+
+ return '{} ? {} : {}'.format(self._condition, self._then_expression, self._else_expression)
+
+####################################################################################################
+
+class Function(Expression):
+
+ ##############################################
+
+ def __init__(self, name, *args):
+
+ super(Function, self).__init__(*args)
+ self._name = name
+
+ ##############################################
+
+ @property
+ def name(self):
+ return self._name
+
+ ##############################################
+
+ def __str__(self):
+
+ parameters = ', '.join([str(operand) for operand in self.iter_on_operands()])
+ return self._name + ' (' + parameters + ')'
diff --git a/third_party/PySpice-org/PySpice/PySpice/Spice/Expression/Parser.py b/third_party/PySpice-org/PySpice/PySpice/Spice/Expression/Parser.py
new file mode 100644
index 00000000..3e141c38
--- /dev/null
+++ b/third_party/PySpice-org/PySpice/PySpice/Spice/Expression/Parser.py
@@ -0,0 +1,346 @@
+####################################################################################################
+#
+# PySpice - A Spice Package for Python
+# Copyright (C) 2017 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 .
+#
+####################################################################################################
+
+"""This module implements a parser for Spice expressions.
+"""
+
+####################################################################################################
+
+import logging
+
+####################################################################################################
+
+import ply.lex as lex
+import ply.yacc as yacc
+
+####################################################################################################
+
+from .Ast import *
+
+####################################################################################################
+
+_module_logger = logging.getLogger(__name__)
+
+####################################################################################################
+
+# def ensure_statement_list(x):
+# if isinstance(x, StatementList):
+# return x
+# else:
+# return StatementList(x)
+
+####################################################################################################
+
+class Parser:
+
+ _logger = _module_logger.getChild('Parser')
+
+ ##############################################
+
+ reserved = {
+ }
+
+ tokens = [
+ 'NAME',
+ # 'INT', 'FLOAT',
+ 'NUMBER',
+ 'SEMICOLON',
+ 'LEFT_PARENTHESIS', 'RIGHT_PARENTHESIS',
+ 'SET',
+ 'NOT',
+ 'POWER',
+ 'MULTIPLY',
+ 'DIVIDE', 'INT_DIVIDE', 'MODULO',
+ 'PLUS', 'MINUS',
+ 'EQUAL', 'NOT_EQUAL', 'LESS', 'GREATER', 'LESS_EQUAL', 'GREATER_EQUAL',
+ 'AND', 'OR',
+ 'IF', 'COLON',
+ ] + list(reserved.values())
+
+ ##############################################
+
+ def t_error(self, token):
+ self._logger.error("Illegal character '%s' at line %u and position %u" %
+ (token.value[0],
+ token.lexer.lineno,
+ token.lexer.lexpos - self._previous_newline_position))
+ # token.lexer.skip(1)
+ raise NameError('Lexer error')
+
+ ##############################################
+
+ t_ignore = ' \t'
+
+ def t_newline(self, t):
+ r'\r?\n+'
+ # Track newline
+ t.lexer.lineno += len(t.value)
+ self._previous_newline_position = t.lexer.lexpos
+ # t.type = 'SEMICOLON'
+ # return t
+
+ t_ignore_COMMENT = r'\#[^\n]*'
+
+ ##############################################
+
+ t_SEMICOLON = r';'
+
+ t_LEFT_PARENTHESIS = r'\('
+ t_RIGHT_PARENTHESIS = r'\)'
+
+ t_SET = r'='
+
+ t_NOT = r'!'
+
+ t_POWER = r'\*\*'
+
+ t_MULTIPLY = r'\*'
+ t_DIVIDE = r'/'
+ t_MODULO = r'%'
+ t_INT_DIVIDE = r'\/'
+
+ t_PLUS = r'\+'
+ t_MINUS = r'-'
+
+ t_EQUAL = r'=='
+ t_NOT_EQUAL = r'!='
+ t_LESS = r'<'
+ t_GREATER = r'>'
+ t_LESS_EQUAL = r'<='
+ t_GREATER_EQUAL = r'>='
+
+ t_AND = r'&&'
+ t_OR = r'\|\|'
+
+ t_IF = r'\?'
+ t_COLON = r':'
+
+ def t_NAME(self, t):
+ r'[a-zA-Z_][a-zA-Z_0-9]*'
+ # Check for reserved words
+ t.type = self.reserved.get(t.value, 'NAME') # Fixme: ???
+ return t
+
+ # def t_INT(self, t):
+ # r'\d+'
+ # t.value = int(t.value)
+ # return t
+
+ # exponent_part = r"""([eE][-+]?[0-9]+)"""
+ # fractional_constant = r"""([0-9]*\.[0-9]+)|([0-9]+\.)"""
+ # floating_constant = '(((('+fractional_constant+')'+exponent_part+'?)|([0-9]+'+exponent_part+'))[FfLl]?)'
+
+ def t_NUMBER(self, t):
+ # 1 1. 1.23 .1
+ # r'\d*\.?\d+([eE][-+]?\d+)?'
+ # r'(\d+)(\.\d+)(e(\+|-)?(\d+))? | (\d+)e(\+|-)?(\d+)'
+ r'\d+\.\d+(e(\+|-)?(\d+))? | \d+\.(e(\+|-)?(\d+))? | \.\d+(e(\+|-)?(\d+))? | \d+'
+ t.value = t.value
+ return t
+
+ ##############################################
+ #
+ # Grammar
+ #
+
+ # from lowest
+ precedence = (
+ ('left', 'IF'),
+ ('left', 'OR'),
+ ('left', 'AND'),
+ ('left', 'GREATER', 'LESS', 'GREATER_EQUAL', 'LESS_EQUAL', 'NOT_EQUAL', 'EQUAL'),
+ ('left', 'MINUS', 'PLUS'),
+ ('left', 'INT_DIVIDE', 'MODULO', 'DIVIDE', 'MULTIPLY'),
+ ('left', 'POWER'),
+ ('left', 'NOT'), # , 'NEGATION'
+ )
+
+ def p_error(self, p):
+ if p:
+ self._logger.error("Syntax error at '%s'", p.value)
+ raise NameError('Syntax Error')
+ else:
+ self._logger.error("Syntax Error at End Of File")
+ raise NameError("Syntax Error at End Of File" )
+
+ # start = 'program'
+ start = 'statement'
+
+ # def p_empty(self, p):
+ # 'empty :'
+ # pass
+
+ def p_statement(self, t):
+ 'statement : expression'
+ print('statement', t[1])
+
+ # def p_program(self, p):
+ # '''program : statement
+ # | program statement
+ # | empty
+ # '''
+ # if len(p) == 3:
+ # statement = p[2]
+ # else:
+ # statement = p[1]
+ # if statement is not None:
+ # self._program.add(statement)
+
+ # def p_statement(self, p):
+ # '''statement : expression_statement
+ # '''
+ # p[0] = p[1]
+
+ # def p_expression_statement(self, p):
+ # '''expression_statement : assignation SEMICOLON
+ # | function SEMICOLON
+ # | SEMICOLON
+ # '''
+ # if len(p) == 3:
+ # p[0] = p[1]
+
+ # def p_statement_list(self, p):
+ # '''statement_list : statement
+ # | statement_list statement
+ # '''
+ # if len(p) == 3:
+ # p[1].add(p[2])
+ # p[0] = p[1]
+ # else:
+ # p[0] = StatementList(p[1])
+
+ # def p_expression_list(self, p):
+ # '''expression_list : expression
+ # | expression_list COMMA expression
+ # '''
+ # if len(p) == 3:
+ # p[1].add(p[2])
+ # p[0] = p[1]
+ # else:
+ # p[0] = StatementList(p[1])
+
+ # def p_function(self, p):
+ # '''function : NAME LEFT_PARENTHESIS expression_list RIGHT_PARENTHESIS
+ # | NAME LEFT_PARENTHESIS RIGHT_PARENTHESIS
+ # '''
+ # if len(p) == 5:
+ # p[0] = Function(p[1], p[3])
+ # else:
+ # p[0] = Function(p[1])
+
+ def p_variable(self, p):
+ '''variable : NAME
+ '''
+ p[0] = Variable(p[1])
+
+ # def p_assignation(self, p):
+ # 'assignation : variable SET expression'
+ # p[0] = Assignation(p[3], p[1]) # eval value first
+
+ # def p_interger(self, p):
+ # '''constant : INT
+ # '''
+ # p[0] = IntConstant(p[1])
+
+ def p_float(self, p):
+ '''constant : NUMBER
+ '''
+ if '.' in p[1]:
+ p[0] = FloatConstant(p[1])
+ else:
+ p[0] = IntConstant(p[1])
+
+ def p_value(self, p):
+ '''expression : variable
+ | constant
+ '''
+ p[0] = p[1]
+
+ def p_unnary_operation(self, p):
+ # OP ...
+ '''expression : MINUS expression
+ | NOT expression
+ '''
+ p[0] = OperatorMetaclass.get_unary(p[1])(p[2])
+
+ def p_binary_operation(self, p):
+ # ... OP ...
+ '''expression : expression POWER expression
+ | expression MULTIPLY expression
+ | expression DIVIDE expression
+ | expression MODULO expression
+ | expression INT_DIVIDE expression
+ | expression PLUS expression
+ | expression MINUS expression
+ | expression EQUAL expression
+ | expression NOT_EQUAL expression
+ | expression LESS expression
+ | expression GREATER expression
+ | expression LESS_EQUAL expression
+ | expression GREATER_EQUAL expression
+ | expression AND expression
+ | expression OR expression
+ '''
+ p[0] = OperatorMetaclass.get_binary(p[2])(p[1], p[3])
+
+ def p_if(self, p):
+ '''expression : expression IF expression COLON expression
+ '''
+ p[0] = If(p[1], p[3], p[5])
+
+ ##############################################
+
+ def __init__(self):
+
+ self._build()
+
+ ##############################################
+
+ def _build(self, **kwargs):
+
+ self._lexer = lex.lex(module=self, **kwargs)
+ self._parser = yacc.yacc(module=self, **kwargs)
+
+ ##############################################
+
+ def _reset(self):
+
+ self._previous_newline_position = 0
+ # self._program = Program()
+
+ ##############################################
+
+ def parse(self, text):
+
+ self._reset() # Fixme: after ?
+ self._parser.parse(text, lexer=self._lexer)
+ # return self._program
+
+ ##############################################
+
+ def test_lexer(self, text):
+
+ self._reset()
+ self._lexer.input(text)
+ while True:
+ token = self._lexer.token()
+ if not token:
+ break
+ print(token)
diff --git a/third_party/PySpice-org/PySpice/PySpice/Spice/Expression/__init__.py b/third_party/PySpice-org/PySpice/PySpice/Spice/Expression/__init__.py
new file mode 100644
index 00000000..3930a3f5
--- /dev/null
+++ b/third_party/PySpice-org/PySpice/PySpice/Spice/Expression/__init__.py
@@ -0,0 +1,19 @@
+####################################################################################################
+#
+# PySpice - A Spice Package for Python
+# Copyright (C) 2017 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 .
+#
+####################################################################################################
diff --git a/third_party/PySpice-org/PySpice/PySpice/Spice/HighLevelElement.py b/third_party/PySpice-org/PySpice/PySpice/Spice/HighLevelElement.py
new file mode 100644
index 00000000..dbce6238
--- /dev/null
+++ b/third_party/PySpice-org/PySpice/PySpice/Spice/HighLevelElement.py
@@ -0,0 +1,865 @@
+# -*- coding: utf-8 -*-
+
+####################################################################################################
+#
+# 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 .
+#
+####################################################################################################
+
+"""This module implements high level elements built on top of Spice elements."""
+
+# Fixme: check NgSpice for discrepancies
+
+####################################################################################################
+
+from ..Math import rms_to_amplitude, amplitude_to_rms
+from ..Tools.StringTools import join_list, join_dict, str_spice, str_spice_list
+from ..Unit import as_s, as_V, as_A, as_Hz
+from .BasicElement import VoltageSource, CurrentSource
+
+####################################################################################################
+
+class SourceMixinAbc:
+ AS_UNIT = None
+
+####################################################################################################
+
+class VoltageSourceMixinAbc:
+ AS_UNIT = as_V
+
+####################################################################################################
+
+class CurrentSourceMixinAbc:
+ AS_UNIT = as_A
+
+####################################################################################################
+
+class SinusoidalMixin(SourceMixinAbc):
+
+ r"""This class implements a sinusoidal waveform.
+
+ +------+----------------+---------------+-------+
+ | Name + Parameter + Default Value + Units |
+ +------+----------------+---------------+-------+
+ | Vo + offset + + V, A |
+ +------+----------------+---------------+-------+
+ | Va + amplitude + + V, A |
+ +------+----------------+---------------+-------+
+ | f + frequency + 1 / TStop + Hz |
+ +------+----------------+---------------+-------+
+ | Td + delay + 0.0 + sec |
+ +------+----------------+---------------+-------+
+ | Df + damping factor + 0.01 + 1/sec |
+ +------+----------------+---------------+-------+
+
+ The shape of the waveform is described by the following formula:
+
+ .. math::
+
+ V(t) = \begin{cases}
+ V_o & \text{if}\ 0 \leq t < T_d, \\
+ V_o + V_a e^{-D_f(t-T_d)} \sin\left(2\pi f (t-T_d)\right) & \text{if}\ T_d \leq t < T_{stop}.
+ \end{cases}
+
+ Spice syntax::
+
+ SIN ( Voffset Vamplitude Freq Tdelay DampingFactor )
+
+ Public Attributes:
+
+ :attr:`ac_magnitude`
+
+ :attr:`amplitude`
+
+ :attr:`damping_factor`
+
+ :attr:`dc_offset`
+
+ :attr:`delay`
+
+ :attr:`frequency`
+
+ :attr:`offset`
+
+ """
+
+ ##############################################
+
+ def __init__(self,
+ dc_offset=0,
+ ac_magnitude=1,
+ offset=0, amplitude=1, frequency=50,
+ delay=0, damping_factor=0):
+
+ self.dc_offset = self.AS_UNIT(dc_offset)
+ self.ac_magnitude = self.AS_UNIT(ac_magnitude)
+ self.offset = self.AS_UNIT(offset)
+ self.amplitude = self.AS_UNIT(amplitude)
+ self.frequency = as_Hz(frequency) # Fixme: protect by setter?
+ self.delay = as_s(delay)
+ self.damping_factor = as_Hz(damping_factor)
+
+ ##############################################
+
+ @property
+ def rms_voltage(self):
+ # Fixme: ok ???
+ return amplitude_to_rms(self.amplitude * self.ac_magnitude)
+
+ ##############################################
+
+ @property
+ def period(self):
+ return self.frequency.period
+
+ ##############################################
+
+ def format_spice_parameters(self):
+ sin_part = join_list((self.offset, self.amplitude, self.frequency, self.delay, self.damping_factor))
+ return join_list((
+ 'DC {} AC {}'.format(*str_spice_list(self.dc_offset, self.ac_magnitude)),
+ 'SIN({})'.format(sin_part),
+ ))
+
+####################################################################################################
+
+class PulseMixin(SourceMixinAbc):
+
+ """This class implements a pulse waveform.
+
+ Nomenclature:
+
+ +--------+---------------+---------------+-------+
+ | Name + Parameter + Default Value + Units |
+ +--------+---------------+---------------+-------+
+ | V1 + initial value + + V, A |
+ +--------+---------------+---------------+-------+
+ | V2 + pulsed value + + V, A |
+ +--------+---------------+---------------+-------+
+ | Td + delay time + 0.0 + sec |
+ +--------+---------------+---------------+-------+
+ | Tr + rise time + Tstep + sec |
+ +--------+---------------+---------------+-------+
+ | Tf + fall time + Tstep + sec |
+ +--------+---------------+---------------+-------+
+ | Pw + pulse width + Tstop + sec |
+ +--------+---------------+---------------+-------+
+ | Period + period + Tstop + sec |
+ +--------+---------------+---------------+-------+
+ | Phase + phase + 0.0 + sec |
+ +--------+---------------+---------------+-------+
+
+ Phase is only possible when XSPICE is enabled
+
+ Spice Syntax::
+
+ PULSE ( V1 V2 Td Tr Tf Pw Period Phase )
+
+ A single pulse so specified is described by the following table:
+
+ +-------------+-------+
+ | Time | Value |
+ +-------------+-------+
+ | 0 | V1 |
+ +-------------+-------+
+ | Td | V1 |
+ +-------------+-------+
+ | Td+Tr | V2 |
+ +-------------+-------+
+ | Td+Tr+Pw | V2 |
+ +-------------+-------+
+ | Td+Tr+Pw+Tf | V1 |
+ +-------------+-------+
+ | Tstop | V1 |
+ +-------------+-------+
+
+ Note: default value in Spice for rise and fall time is the simulation transient step, pulse
+ width and period is the simulation stop time.
+
+ Public Attributes:
+
+ :attr:`delay_time`
+
+ :attr:`fall_time`
+
+ :attr:`initial_value`
+
+ :attr:`period`
+
+ :attr:`phase`
+
+ :attr:`pulse_width`
+
+ :attr:`pulsed_value`
+
+ :attr:`rise_time`
+
+ """
+
+ ##############################################
+
+ def __init__(self,
+ initial_value, pulsed_value,
+ pulse_width, period,
+ delay_time=0, rise_time=0, fall_time=0,
+ phase=None,
+ dc_offset=0):
+
+ # Fixme: default
+ # rise_time, fall_time = Tstep
+ # pulse_width, period = Tstop
+
+ self.dc_offset = self.AS_UNIT(dc_offset) # Fixme: -> SourceMixinAbc
+ self.initial_value = self.AS_UNIT(initial_value)
+ self.pulsed_value = self.AS_UNIT(pulsed_value)
+ self.delay_time = as_s(delay_time)
+ self.rise_time = as_s(rise_time)
+ self.fall_time = as_s(fall_time)
+ self.pulse_width = as_s(pulse_width)
+ self.period = as_s(period) # Fixme: protect by setter?
+
+ # XSPICE
+ if phase is not None:
+ self.phase = as_s(phase)
+ else:
+ self.phase = None
+
+ # # Fixme: to func?
+ # # Check parameters
+ # found_none = False
+ # for parameter in ('rise_time', 'fall_time', 'pulse_width', 'period'):
+ # parameter_value = getattr(self, parameter)
+ # if found_none:
+ # if parameter_value is not None:
+ # raise ValueError("Parameter {} is set but some previous parameters was not set".format(parameter))
+ # else:
+ # found_none = parameter_value is None
+
+ ##############################################
+
+ @property
+ def frequency(self):
+ return self.period.frequency
+
+ ##############################################
+
+ def format_spice_parameters(self):
+
+ # if DC is not provided, ngspice complains
+ # Warning: vpulse: no DC value, transient time 0 value used
+
+ # Fixme: to func?
+ return join_list((
+ 'DC {}'.format(str_spice(self.dc_offset)),
+ 'PULSE(' +
+ join_list((self.initial_value, self.pulsed_value, self.delay_time,
+ self.rise_time, self.fall_time, self.pulse_width, self.period,
+ self.phase)) +
+ ')'))
+
+####################################################################################################
+
+class ExponentialMixin(SourceMixinAbc):
+
+ r"""This class implements a Exponential waveform.
+
+ Nomenclature:
+
+ +------+--------------------+---------------+-------+
+ | Name + Parameter + Default Value + Units |
+ +------+--------------------+---------------+-------+
+ | V1 + Initial value + + V, A |
+ +------+--------------------+---------------+-------+
+ | V2 + pulsed value + + V, A |
+ +------+--------------------+---------------+-------+
+ | Td1 + rise delay time + 0.0 + sec |
+ +------+--------------------+---------------+-------+
+ | tau1 + rise time constant + Tstep + sec |
+ +------+--------------------+---------------+-------+
+ | Td2 + fall delay time + Td1+Tstep + sec |
+ +------+--------------------+---------------+-------+
+ | tau2 + fall time constant + Tstep + sec |
+ +------+--------------------+---------------+-------+
+
+ Spice Syntax::
+
+ EXP ( V1 V2 TD1 TAU1 TD2 TAU2 )
+
+ The shape of the waveform is described by the following formula:
+
+ Let V21 = V2 - V1 and V12 = V1 - V2.
+
+ .. math::
+
+ V(t) = \begin{cases}
+ V_1 & \text{if}\ 0 \leq t < T_{d1}, \\
+ V_1 + V_{21} ( 1 − e^{-\frac{t-T_{d1}}{\tau_1}} )
+ & \text{if}\ T_{d1} \leq t < T_{d2}, \\
+ V_1 + V_{21} ( 1 − e^{-\frac{t-T_{d1}}{\tau_1}} ) + V_{12} ( 1 − e^{-\frac{t-T_{d2}}{\tau_2}} )
+ & \text{if}\ T_{d2} \leq t < T_{stop}
+ \end{cases}
+
+ """
+
+ ##############################################
+
+ def __init__(self,
+ initial_value, pulsed_value,
+ rise_delay_time=.0, rise_time_constant=None,
+ fall_delay_time=None, fall_time_constant=None):
+
+ # Fixme: default
+
+ self.initial_value = self.AS_UNIT(initial_value)
+ self.pulsed_value = self.AS_UNIT(pulsed_value)
+ self.rise_delay_time = as_s(rise_delay_time)
+ self.rise_time_constant = as_s(rise_time_constant)
+ self.fall_delay_time = as_s(fall_delay_time)
+ self.fall_time_constant = as_s(fall_time_constant)
+
+ ##############################################
+
+ def format_spice_parameters(self):
+ # Fixme: to func?
+ return ('EXP(' +
+ join_list((self.initial_value, self.pulsed_value,
+ self.rise_delay_time, self.rise_time_constant,
+ self.fall_delay_time, self.fall_time_constant,
+ )) +
+ ')')
+
+####################################################################################################
+
+class PieceWiseLinearMixin(SourceMixinAbc):
+
+ r"""This class implements a Piece-Wise Linear waveform.
+
+ Spice Syntax::
+
+ PWL( T1 V1 ) |
+
+ Each pair of values (Ti , Vi) specifies that the value of the source is Vi (in Volts or Amps) at
+ time = Ti . The value of the source at intermediate values of time is determined by using linear
+ interpolation on the input values. The parameter r determines a repeat time point. If r is not
+ given, the whole sequence of values (Ti , Vi ) is issued once, then the output stays at its
+ final value. If r = 0, the whole sequence from time = 0 to time = Tn is repeated forever. If r =
+ 10ns, the sequence between 10ns and 50ns is repeated forever. the r value has to be one of the
+ time points T1 to Tn of the PWL sequence. If td is given, the whole PWL sequence is delayed by a
+ delay time time = td. The current source still needs to be patched, td and r are not yet
+ available.
+
+ `values` should be given as a list of (`Time`, `Value`)-tuples, e.g.::
+
+ PieceWiseLinearVoltageSource(
+ circuit,
+ 'pwl1', '1', '0',
+ values=[(0, 0), (10@u_ms, 0), (11@u_ms, 5@u_V), (20@u_ms, 5@u_V)],
+ )
+
+ """
+
+ ##############################################
+
+ def __init__(self, values, repeat_time=None, delay_time=None, dc=None):
+ self.values = sum(([as_s(t), self.AS_UNIT(x)] for (t, x) in values), [])
+ self.repeat_time = as_s(repeat_time, none=True)
+ self.delay_time = as_s(delay_time, none=True)
+ self.dc = self.AS_UNIT(dc, none=True)
+
+ ##############################################
+
+ def format_spice_parameters(self):
+
+ # Fixme: to func?
+
+ d = {}
+ if self.repeat_time is not None:
+ d["r"] = self.repeat_time
+ if self.delay_time is not None:
+ d["td"] = self.delay_time
+
+ _ = ""
+ if self.dc is not None:
+ _ += "DC {} ".format(str_spice(self.dc))
+ _ += "PWL(" + join_list(self.values)
+ if d:
+ _ += " " + join_dict(d) # OrderedDict(
+ _ += ")"
+
+ return _
+
+####################################################################################################
+
+class SingleFrequencyFMMixin(SourceMixinAbc):
+
+ r"""This class implements a Single-Frequency FM waveform.
+
+ Spice Syntax::
+
+ SFFM (VO VA FC MDI FS )
+
+ +------+-------------------+---------------+-------+
+ | Name + Parameter + Default Value + Units |
+ +------+-------------------+---------------+-------+
+ | Vo + offset + + V, A |
+ +------+-------------------+---------------+-------+
+ | Va + amplitude + + V, A |
+ +------+-------------------+---------------+-------+
+ | Fc + carrier frequency + 1 / Tstop + Hz |
+ +------+-------------------+---------------+-------+
+ | Mdi + modulation index + + |
+ +------+-------------------+---------------+-------+
+ | Fs + signal frequency + 1 / Tstop + Hz |
+ +------+-------------------+---------------+-------+
+
+ The shape of the waveform is described by the following equation:
+
+ .. math::
+
+ V(t) = V_o + V_a \sin (2\pi F_c\, t + M_{di} \sin (2\pi F_s\,t))
+
+ """
+
+ ##############################################
+
+ def __init__(self, offset, amplitude, carrier_frequency, modulation_index, signal_frequency):
+ self.offset = self.AS_UNIT(offset)
+ self.amplitude = self.AS_UNIT(amplitude)
+ self.carrier_frequency = as_Hz(carrier_frequency)
+ self.modulation_index = modulation_index
+ self.signal_frequency = as_Hz(signal_frequency)
+
+ ##############################################
+
+ def format_spice_parameters(self):
+ # Fixme: to func?
+ return ('SFFM(' +
+ join_list((self.offset, self.amplitude, self.carrier_frequency,
+ self.modulation_index, self.signal_frequency)) +
+ ')')
+
+####################################################################################################
+
+class AmplitudeModulatedMixin(SourceMixinAbc):
+
+ r"""This class implements a Amplitude Modulated source.
+
+ +------+----------------------+---------------+-------+
+ | Name + Parameter + Default Value + Units |
+ +------+----------------------+---------------+-------+
+ | Vo + offset + + V, A |
+ +------+----------------------+---------------+-------+
+ | Va + amplitude + + V, A |
+ +------+----------------------+---------------+-------+
+ | Mf + modulating frequency + + Hz |
+ +------+----------------------+---------------+-------+
+ | Fc + carrier frequency + 1 / Tstop + Hz |
+ +------+----------------------+---------------+-------+
+ | Td + signal delay + + s |
+ +------+----------------------+---------------+-------+
+
+ Spice Syntax::
+
+ AM(VA VO MF FC TD)
+
+ The shape of the waveform is described by the following equation:
+
+ .. math::
+
+ V(t) = V_a (V_o + \sin (2\pi M_f\,t)) \sin (2\pi F_c\,t)
+
+ """
+
+ ##############################################
+
+ def __init__(self, offset, amplitude, modulating_frequency, carrier_frequency, signal_delay):
+
+ # Fixme: default
+
+ self.offset = self.AS_UNIT(offset)
+ self.amplitude = self.AS_UNIT(amplitude)
+ self.carrier_frequency = as_Hz(carrier_frequency)
+ self.modulating_frequency = as_Hz(modulating_frequency)
+ self.signal_delay = as_s(signal_delay)
+
+ ##############################################
+
+ def format_spice_parameters(self):
+ # Fixme: to func?
+ return ('AM(' +
+ join_list((self.offset, self.amplitude, self.carrier_frequency,
+ self.modulating_frequency, self.signal_delay)) +
+ ')')
+
+####################################################################################################
+
+class RandomMixin(SourceMixinAbc):
+
+ r"""This class implements a Random Voltage source.
+
+ The TRRANDOM option yields statistically distributed voltage values, derived from the ngspice
+ random number generator. These values may be used in the transient simulation directly within a
+ circuit, e.g. for generating a specific noise voltage, but especially they may be used in the
+ control of behavioral sources (B, E, G sources, voltage controllable A sources, capacitors,
+ inductors, or resistors) to simulate the circuit dependence on statistically varying device
+ parameters. A Monte-Carlo simulation may thus be handled in a single simulation run.
+
+ Spice Syntax::
+
+ TRRANDOM( TYPE TS | > >)
+
+ TYPE determines the random variates generated: 1 is uniformly distributed, 2 Gaussian, 3
+ exponential, 4 Poisson. TS is the duration of an individual voltage value. TD is a time delay
+ with 0 V output before the random voltage values start up. PARAM1 and PARAM2 depend on the type
+ selected.
+
+ +-------------+---------------+---------+-------------+---------+
+ | Type + Parameter 1 + Default + Parameter 2 + Default |
+ +-------------+---------------+---------+-------------+---------+
+ | uniform + range + 1 + offset + 0 |
+ +-------------+---------------+---------+-------------+---------+
+ | gaussian + standard dev. + 1 + mean + 0 |
+ +-------------+---------------+---------+-------------+---------+
+ | exponential + mean + 1 + offset + 0 |
+ +-------------+---------------+---------+-------------+---------+
+ | poisson + lambda + 1 + offset + 0 |
+ +-------------+---------------+---------+-------------+---------+
+
+ """
+
+ ##############################################
+
+ def __init__(self, random_type, duration=0, time_delay=0, parameter1=1, parameter2=0):
+ # Fixme: random_type and parameters
+ self.random_type = random_type
+ self.duration = as_s(duration)
+ self.time_delay = as_s(time_delay)
+ self.parameter1 = parameter1
+ self.parameter2 = parameter2
+
+ ##############################################
+
+ def format_spice_parameters(self):
+
+ if self.random_type == 'uniform':
+ random_type = 1
+ elif self.random_type == 'exponential':
+ random_type = 2
+ elif self.random_type == 'gaussian':
+ random_type = 3
+ elif self.random_type == 'poisson':
+ random_type = 4
+ else:
+ raise ValueError("Wrong random type {}".format(self.random_type))
+
+ # Fixme: to func?
+ return ('TRRANDOM(' +
+ join_list((random_type, self.duration, self.time_delay,
+ self.parameter1, self.parameter2)) +
+ ')')
+
+####################################################################################################
+
+class SinusoidalVoltageSource(VoltageSource, VoltageSourceMixinAbc, SinusoidalMixin):
+
+ r"""This class implements a sinusoidal waveform voltage source.
+
+ See :class:`SinusoidalMixin` for documentation.
+
+ """
+
+ ##############################################
+
+ def __init__(self, netlist, name, node_plus, node_minus, *args, **kwargs):
+ VoltageSource.__init__(self, netlist, name, node_plus, node_minus)
+ SinusoidalMixin.__init__(self, *args, **kwargs)
+
+ ##############################################
+
+ format_spice_parameters = SinusoidalMixin.format_spice_parameters
+
+####################################################################################################
+
+class SinusoidalCurrentSource(CurrentSource, CurrentSourceMixinAbc, SinusoidalMixin):
+
+ r"""This class implements a sinusoidal waveform current source.
+
+ See :class:`SinusoidalMixin` for documentation.
+
+ """
+
+ ##############################################
+
+ def __init__(self, netlist, name, node_plus, node_minus, *args, **kwargs):
+ CurrentSource.__init__(self, netlist, name, node_plus, node_minus)
+ SinusoidalMixin.__init__(self, *args, **kwargs)
+
+ ##############################################
+
+ format_spice_parameters = SinusoidalMixin.format_spice_parameters
+
+####################################################################################################
+
+class AcLine(SinusoidalVoltageSource):
+
+ ##############################################
+
+ def __init__(self, netlist, name, node_plus, node_minus, rms_voltage=230, frequency=50):
+ super().__init__(netlist, name, node_plus, node_minus,
+ amplitude=rms_to_amplitude(rms_voltage),
+ frequency=frequency)
+
+####################################################################################################
+
+class PulseVoltageSource(VoltageSource, VoltageSourceMixinAbc, PulseMixin):
+
+ r"""This class implements a pulse waveform voltage source.
+
+ See :class:`PulseMixin` for documentation.
+
+ """
+
+ ##############################################
+
+ def __init__(self, netlist, name, node_plus, node_minus, *args, **kwargs):
+ VoltageSource.__init__(self, netlist, name, node_plus, node_minus)
+ PulseMixin.__init__(self, *args, **kwargs)
+
+ ##############################################
+
+ format_spice_parameters = PulseMixin.format_spice_parameters
+
+####################################################################################################
+
+class PulseCurrentSource(CurrentSource, CurrentSourceMixinAbc, PulseMixin):
+
+ r"""This class implements a pulse waveform current source.
+
+ See :class:`PulseMixin` for documentation.
+
+ """
+
+ ##############################################
+
+ def __init__(self, netlist, name, node_plus, node_minus, *args, **kwargs):
+ CurrentSource.__init__(self, netlist, name, node_plus, node_minus)
+ PulseMixin.__init__(self, *args, **kwargs)
+
+ ##############################################
+
+ format_spice_parameters = PulseMixin.format_spice_parameters
+
+####################################################################################################
+
+class ExponentialVoltageSource(VoltageSource, VoltageSourceMixinAbc, ExponentialMixin):
+
+ r"""This class implements a exponential waveform voltage source.
+
+ See :class:`ExponentialMixin` for documentation.
+
+ """
+
+ ##############################################
+
+ def __init__(self, netlist, name, node_plus, node_minus, *args, **kwargs):
+ VoltageSource.__init__(self, netlist, name, node_plus, node_minus)
+ ExponentialMixin.__init__(self, *args, **kwargs)
+
+ ##############################################
+
+ format_spice_parameters = ExponentialMixin.format_spice_parameters
+
+####################################################################################################
+
+class ExponentialCurrentSource(CurrentSource, CurrentSourceMixinAbc, ExponentialMixin):
+
+ r"""This class implements a exponential waveform current source.
+
+ See :class:`ExponentialMixin` for documentation.
+
+ """
+
+ ##############################################
+
+ def __init__(self, netlist, name, node_plus, node_minus, *args, **kwargs):
+ CurrentSource.__init__(self, netlist, name, node_plus, node_minus)
+ ExponentialMixin.__init__(self, *args, **kwargs)
+
+ ##############################################
+
+ format_spice_parameters = ExponentialMixin.format_spice_parameters
+
+####################################################################################################
+
+class PieceWiseLinearVoltageSource(VoltageSource, VoltageSourceMixinAbc, PieceWiseLinearMixin):
+
+ r"""This class implements a piece wise linear waveform voltage source.
+
+ See :class:`PieceWiseLinearMixin` for documentation.
+
+ """
+
+ ##############################################
+
+ def __init__(self, netlist, name, node_plus, node_minus, *args, **kwargs):
+ VoltageSource.__init__(self, netlist, name, node_plus, node_minus)
+ PieceWiseLinearMixin.__init__(self, *args, **kwargs)
+
+ ##############################################
+
+ format_spice_parameters = PieceWiseLinearMixin.format_spice_parameters
+
+####################################################################################################
+
+class PieceWiseLinearCurrentSource(CurrentSource, CurrentSourceMixinAbc, PieceWiseLinearMixin):
+
+ r"""This class implements a piece wise linear waveform current source.
+
+ See :class:`PieceWiseLinearMixin` for documentation.
+
+ """
+
+ ##############################################
+
+ def __init__(self, netlist, name, node_plus, node_minus, *args, **kwargs):
+ CurrentSource.__init__(self, netlist, name, node_plus, node_minus)
+ PieceWiseLinearMixin.__init__(self, *args, **kwargs)
+
+ ##############################################
+
+ format_spice_parameters = PieceWiseLinearMixin.format_spice_parameters
+
+####################################################################################################
+
+class SingleFrequencyFMVoltageSource(VoltageSource, VoltageSourceMixinAbc, SingleFrequencyFMMixin):
+
+ r"""This class implements a single frequency FM waveform voltage source.
+
+ See :class:`SingleFrequencyFMMixin` for documentation.
+
+ """
+
+ ##############################################
+
+ def __init__(self, netlist, name, node_plus, node_minus, *args, **kwargs):
+
+ VoltageSource.__init__(self, netlist, name, node_plus, node_minus)
+ SingleFrequencyFMMixin.__init__(self, *args, **kwargs)
+
+ ##############################################
+
+ format_spice_parameters = SingleFrequencyFMMixin.format_spice_parameters
+
+####################################################################################################
+
+class SingleFrequencyFMCurrentSource(CurrentSource, CurrentSourceMixinAbc, SingleFrequencyFMMixin):
+
+ r"""This class implements a single frequency FM waveform current source.
+
+ See :class:`SingleFrequencyFMMixin` for documentation.
+
+ """
+
+ ##############################################
+
+ def __init__(self, netlist, name, node_plus, node_minus, *args, **kwargs):
+ CurrentSource.__init__(self, netlist, name, node_plus, node_minus)
+ SingleFrequencyFMMixin.__init__(self, *args, **kwargs)
+
+ ##############################################
+
+ format_spice_parameters = SingleFrequencyFMMixin.format_spice_parameters
+
+####################################################################################################
+
+class AmplitudeModulatedVoltageSource(VoltageSource, VoltageSourceMixinAbc, AmplitudeModulatedMixin):
+
+ r"""This class implements a amplitude modulated waveform voltage source.
+
+ See :class:`AmplitudeModulatedMixin` for documentation.
+
+ """
+
+ ##############################################
+
+ def __init__(self, netlist, name, node_plus, node_minus, *args, **kwargs):
+ VoltageSource.__init__(self, netlist, name, node_plus, node_minus)
+ AmplitudeModulatedMixin.__init__(self, *args, **kwargs)
+
+ ##############################################
+
+ format_spice_parameters = AmplitudeModulatedMixin.format_spice_parameters
+
+####################################################################################################
+
+class AmplitudeModulatedCurrentSource(CurrentSource, CurrentSourceMixinAbc, AmplitudeModulatedMixin):
+
+ r"""This class implements a amplitude modulated waveform current source.
+
+ See :class:`AmplitudeModulatedMixin` for documentation.
+
+ """
+
+ ##############################################
+
+ def __init__(self, netlist, name, node_plus, node_minus, *args, **kwargs):
+ CurrentSource.__init__(self, netlist, name, node_plus, node_minus)
+ AmplitudeModulatedMixin.__init__(self, *args, **kwargs)
+
+ ##############################################
+
+ format_spice_parameters = AmplitudeModulatedMixin.format_spice_parameters
+
+####################################################################################################
+
+class RandomVoltageSource(VoltageSource, VoltageSourceMixinAbc, RandomMixin):
+
+ r"""This class implements a random waveform voltage source.
+
+ See :class:`RandomMixin` for documentation.
+
+ """
+
+ ##############################################
+
+ def __init__(self, netlist, name, node_plus, node_minus, *args, **kwargs):
+ VoltageSource.__init__(self, netlist, name, node_plus, node_minus)
+ RandomMixin.__init__(self, *args, **kwargs)
+
+ ##############################################
+
+ format_spice_parameters = RandomMixin.format_spice_parameters
+
+####################################################################################################
+
+class RandomCurrentSource(CurrentSource, CurrentSourceMixinAbc, RandomMixin):
+
+ r"""This class implements a random waveform current source.
+
+ See :class:`RandomMixin` for documentation.
+
+ """
+
+ ##############################################
+
+ def __init__(self, netlist, name, node_plus, node_minus, *args, **kwargs):
+ CurrentSource.__init__(self, netlist, name, node_plus, node_minus)
+ RandomMixin.__init__(self, *args, **kwargs)
+
+ ##############################################
+
+ format_spice_parameters = RandomMixin.format_spice_parameters
diff --git a/third_party/PySpice-org/PySpice/PySpice/Spice/Library.py b/third_party/PySpice-org/PySpice/PySpice/Spice/Library.py
new file mode 100644
index 00000000..4518d30b
--- /dev/null
+++ b/third_party/PySpice-org/PySpice/PySpice/Spice/Library.py
@@ -0,0 +1,150 @@
+####################################################################################################
+#
+# 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 .
+#
+####################################################################################################
+
+####################################################################################################
+
+import logging
+import re
+
+####################################################################################################
+
+from ..Tools.File import Directory
+from .Parser import SpiceParser
+
+####################################################################################################
+
+_module_logger = logging.getLogger(__name__)
+
+####################################################################################################
+
+class SpiceLibrary:
+
+ """This class implements a Spice sub-circuits and models library.
+
+ A library is a directory which is recursively scanned for '.lib' file and parsed for sub-circuit
+ and models definitions.
+
+ Example of usage::
+
+ spice_library = SpiceLibrary('/some/path/')
+
+ If the directory hierarchy contains a file that define a 1N4148 sub-circuit then we can retrieve
+ the file path using::
+
+ spice_library['1N4148']
+
+ """
+
+ _logger = _module_logger.getChild('Library')
+
+ EXTENSIONS = (
+ '.spice',
+ '.lib',
+ '.mod',
+ '.lib@xyce',
+ '.mod@xyce',
+ )
+
+ ##############################################
+
+ def __init__(self, root_path, recurse=False, section=None):
+
+ self._directory = Directory(root_path).expand_vars_and_user()
+
+ self._subcircuits = {}
+ self._models = {}
+
+ for path in self._directory.iter_file():
+ extension = path.extension.lower()
+ if extension in self.EXTENSIONS:
+ self._logger.debug("Parse {}".format(path))
+ try:
+ spice_parser = SpiceParser(path=path, recurse=recurse, section=section)
+ for lib in spice_parser.incl_libs:
+ self._subcircuits.update(lib._subcircuits)
+ self._models.update(lib._models)
+ except Exception as e:
+ # Parse problem with this file, so skip it and keep going.
+ self._logger.warn("Problem parsing {path} - {e}".format(**locals()))
+ continue
+ if spice_parser.is_only_subcircuit():
+ for subcircuit in spice_parser.subcircuits:
+ name = self._suffix_name(subcircuit.name, extension)
+ self._subcircuits[name] = path
+ elif spice_parser.is_only_model():
+ for model in spice_parser.models:
+ name = self._suffix_name(model.name, extension)
+ self._models[name] = path
+
+ ##############################################
+
+ @staticmethod
+ def _suffix_name(name, extension):
+
+ if extension.endswith('@xyce'):
+ name += '@xyce'
+
+ return name
+
+ ##############################################
+
+ def __getitem__(self, name):
+
+ if name in self._subcircuits:
+ return self._subcircuits[name]
+ elif name in self._models:
+ return self._models[name]
+ else:
+ # print('Library {} not found in {}'.format(name, self._directory))
+ # self._logger.warn('Library {} not found in {}'.format(name, self._directory))
+ raise KeyError(name)
+
+ ##############################################
+
+ @property
+ def subcircuits(self):
+ """ Dictionary of sub-circuits """
+ return iter(self._subcircuits)
+
+ @property
+ def models(self):
+ """ Dictionary of models """
+ return iter(self._models)
+
+ # ##############################################
+
+ # def iter_on_subcircuits(self):
+ # return self._subcircuits.itervalues()
+
+ # ##############################################
+
+ # def iter_on_models(self):
+ # return self._models.itervalues()
+
+ # ##############################################
+
+ def search(self, s):
+ """ Return dict of all models/subcircuits with names matching regex s. """
+ matches = {}
+ models_subcircuits = {**self._models, **self._subcircuits}
+ for name, mdl_subckt in models_subcircuits.items():
+ if re.search(s, name):
+ matches[name] = mdl_subckt
+ return matches
diff --git a/third_party/PySpice-org/PySpice/PySpice/Spice/Netlist.py b/third_party/PySpice-org/PySpice/PySpice/Spice/Netlist.py
new file mode 100644
index 00000000..96fbc733
--- /dev/null
+++ b/third_party/PySpice-org/PySpice/PySpice/Spice/Netlist.py
@@ -0,0 +1,1292 @@
+####################################################################################################
+#
+# 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 .
+#
+####################################################################################################
+
+"""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
+`_.
+
+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__)
+
+####################################################################################################
+
+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
+
+ ##############################################
+
+ 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]]
+ # Fixme: else
+
+ ##############################################
+
+ 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))
+
+####################################################################################################
+
+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
+
+ ##############################################
+
+ 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
+
+####################################################################################################
+
+class OptionalPin:
+
+ def __init__(self, name):
+ self._name = name
+
+ @property
+ def name(self):
+ return self._name
+
+####################################################################################################
+
+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)
+
+ ##############################################
+
+ def disconnect(self):
+ self._node.disconnect(self)
+ self._node = None
+
+ ##############################################
+
+ 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')
+
+####################################################################################################
+
+class ElementParameterMetaClass(type):
+
+ # Metaclass to implements the element node and parameter machinery.
+
+ """Metaclass to customise the element classes when they are created and to register SPICE prefix.
+
+ Element classes are of type :class:`ElementParameterMetaClass` instead of :class:`type`
+
+ .. code-block:: none
+
+ class Resistor(metaclass=ElementParameterMetaClass):
+
+ <=>
+
+ Resistor = ElementParameterMetaClass('Foo', ...)
+
+ """
+
+ #: Dictionary for SPICE prefix -> [cls,]
+ _classes = {}
+
+ _logger = _module_logger.getChild('ElementParameterMetaClass')
+
+ ##############################################
+
+ def __new__(meta_cls, class_name, base_classes, namespace):
+
+ # __new__ is called for the creation of a class depending of this metaclass, i.e. at module loading
+ # It customises the namespace of the new class
+
+ # Collect positional and optional parameters from class attribute dict
+ positional_parameters = {}
+ parameters = {}
+ for attribute_name, obj in namespace.items():
+ if isinstance(obj, ParameterDescriptor):
+ obj.attribute_name = attribute_name
+ if isinstance(obj, PositionalElementParameter):
+ d = positional_parameters
+ elif isinstance(obj, (FlagParameter, KeyValueParameter)):
+ d = parameters
+ # else:
+ # raise NotImplementedError
+ d[attribute_name] = obj
+
+ # Dictionary for positional parameters : attribute_name -> parameter
+ namespace['_positional_parameters'] = OrderedDict(
+ sorted(list(positional_parameters.items()), key=lambda t: t[1]))
+
+ # Dictionary for optional parameters
+ # order is not required for SPICE, but for unit test
+ namespace['_optional_parameters'] = OrderedDict(
+ sorted(list(parameters.items()), key=lambda t: t[0]))
+
+ # Positional parameter array
+ namespace['_parameters_from_args'] = [
+ parameter
+ for parameter in sorted(positional_parameters.values())
+ if not parameter.key_parameter]
+
+ # Implement alias for parameters: spice name -> parameter
+ namespace['_spice_to_parameters'] = {
+ parameter.spice_name:parameter
+ for parameter in namespace['_optional_parameters'].values()}
+ for parameter in namespace['_spice_to_parameters'].values():
+ if (parameter.spice_name in namespace
+ and parameter.spice_name != parameter.attribute_name):
+ _module_logger.error("Spice parameter '{}' clash with namespace".format(parameter.spice_name))
+
+ # Initialise pins
+
+ def make_pin_getter(position):
+ def getter(self):
+ return self._pins[position]
+ return getter
+
+ def make_optional_pin_getter(position):
+ def getter(self):
+ return self._pins[position] if position < len(self._pins) else None
+ return getter
+
+ if 'PINS' in namespace and namespace['PINS'] is not None:
+ number_of_optional_pins = 0
+ pins = []
+ for position, pin_definition in enumerate(namespace['PINS']):
+ # ensure pin_definition is a tuple
+ if isinstance(pin_definition, OptionalPin):
+ optional = True
+ number_of_optional_pins += 1
+ pin_definition = (pin_definition.name,)
+ pin_getter = make_optional_pin_getter(position)
+ else:
+ optional = False
+ pin_getter = make_pin_getter(position)
+ if not isinstance(pin_definition, tuple):
+ pin_definition = (pin_definition,)
+ for name in pin_definition:
+ # Check for name clash
+ if name in namespace:
+ raise NameError("Pin {} of element {} clashes with another attribute".format(name, class_name))
+ # Add a pin getter in element class
+ namespace[name] = property(pin_getter)
+ # Add pin
+ pin = PinDefinition(position, *pin_definition, optional=optional)
+ pins.append(pin)
+ namespace['PINS'] = pins
+ namespace['__number_of_optional_pins__'] = number_of_optional_pins
+ else:
+ _module_logger.debug("{} don't define a PINS attribute".format(class_name))
+
+ return type.__new__(meta_cls, class_name, base_classes, namespace)
+
+ ##############################################
+
+ def __init__(meta_cls, class_name, base_classes, namespace):
+
+ # __init__ is called after the class is created (__new__)
+
+ type.__init__(meta_cls, class_name, base_classes, namespace)
+
+ # Collect basic element classes
+ if 'PREFIX' in namespace:
+ prefix = namespace['PREFIX']
+ if prefix is not None:
+ classes = ElementParameterMetaClass._classes
+ if prefix in classes:
+ classes[prefix].append(meta_cls)
+ else:
+ classes[prefix] = [meta_cls]
+
+ ##############################################
+
+ # Note: These properties are only available from the class object
+ # e.g. Resistor.number_of_pins or Resistor.__class__.number_of_pins
+
+ @property
+ def number_of_pins(cls):
+ #! Fixme: many pins ???
+ number_of_pins = len(cls.PINS)
+ if cls.__number_of_optional_pins__:
+ return slice(number_of_pins - cls.__number_of_optional_pins__, number_of_pins +1)
+ else:
+ return number_of_pins
+
+ @property
+ def number_of_positional_parameters(cls):
+ return len(cls._positional_parameters)
+
+ @property
+ def positional_parameters(cls):
+ return cls._positional_parameters
+
+ @property
+ def optional_parameters(cls):
+ return cls._optional_parameters
+
+ @property
+ def parameters_from_args(cls):
+ return cls._parameters_from_args
+
+ @property
+ def spice_to_parameters(cls):
+ return cls._spice_to_parameters
+
+####################################################################################################
+
+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)
+ elif hasattr(self, 'VALID_KWARGS') and key in self.VALID_KWARGS:
+ pass # cf. NonLinearVoltageSource
+ else:
+ raise ValueError('Unknown argument {}={}'.format(key, value))
+
+ self._pins = ()
+ netlist._add_element(self)
+
+ ##############################################
+
+ def has_parameter(self, name):
+ return hasattr(self, '_' + name)
+
+ ##############################################
+
+ 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
+
+ ##############################################
+
+ 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)
+
+ ##############################################
+
+ def format_node_names(self):
+ """ Return the formatted list of nodes. """
+ return join_list((self.name, join_list(self.nodes)))
+
+ ##############################################
+
+ 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 format_spice_parameters(self):
+ """ Return the formatted list of parameters. """
+ return join_list([parameter.to_str(self) for parameter in self.parameter_iterator()])
+
+ ##############################################
+
+ def __str__(self):
+ """ Return the SPICE element definition. """
+ return join_list((self.format_node_names(), self.format_spice_parameters(), self.raw_spice))
+
+####################################################################################################
+
+class AnyPinElement(Element):
+
+ PINS = ()
+
+ ##############################################
+
+ def copy_to(self, netlist):
+ element = self.__class__(netlist, self._name)
+ super().copy_to(element)
+ return element
+
+####################################################################################################
+
+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]
+
+ ##############################################
+
+ def copy_to(self, netlist):
+ element = self.__class__(netlist, self._name, *self.nodes)
+ super().copy_to(element)
+ return element
+
+####################################################################################################
+
+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)]
+
+ ##############################################
+
+ 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
+
+####################################################################################################
+
+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)
+
+ ##############################################
+
+ 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))
+
+ ##############################################
+
+ def disconnect(self, pin):
+ self._pins.remove(pin)
+
+####################################################################################################
+
+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()
+
+ ##############################################
+
+ 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 element(self, name):
+ return self._elements[name]
+
+ def model(self, name):
+ return self._models[name]
+
+ def node(self, name):
+ return self._nodes[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:
+ 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
+
+ ##############################################
+
+ 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))
+
+ ##############################################
+
+ 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))
+
+ ##############################################
+
+ 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
+
+ ##############################################
+
+ 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
+
+####################################################################################################
+
+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
+
+ ##############################################
+
+ 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
+
+ ##############################################
+
+ 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
+
+####################################################################################################
+
+class SubCircuitFactory(SubCircuit):
+
+ NAME = None
+ NODES = None
+
+ ##############################################
+
+ def __init__(self, **kwargs):
+ super().__init__(self.NAME, *self.NODES, **kwargs)
+
+####################################################################################################
+
+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._libs = [] # .lib, contains a (name, section) tuple
+ self._parameters = {} # .param
+
+ # Fixme: not implemented
+ # .csparam
+ # .func
+ # .if
+
+ ##############################################
+
+ 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
+
+ ##############################################
+
+ def include(self, path):
+ """Include a file."""
+ if path not in self._includes:
+ self._includes.append(path)
+ else:
+ self._logger.warn("Duplicated include")
+
+ ##############################################
+
+ 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}")
+
+ ##############################################
+
+ def parameter(self, name, expression):
+ """Set a parameter."""
+ self._parameters[str(name)] = str(expression)
+
+ ##############################################
+
+ 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_libs(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_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)
+
+ ##############################################
+
+ def str_end(self):
+ return str(self) + '.end' + os.linesep
+
+ ##############################################
+
+ def simulator(self, *args, **kwargs):
+ return CircuitSimulator.factory(self, *args, **kwargs)
diff --git a/third_party/PySpice-org/PySpice/PySpice/Spice/NgSpice/RawFile.py b/third_party/PySpice-org/PySpice/PySpice/Spice/NgSpice/RawFile.py
new file mode 100644
index 00000000..90ae7f6b
--- /dev/null
+++ b/third_party/PySpice-org/PySpice/PySpice/Spice/NgSpice/RawFile.py
@@ -0,0 +1,234 @@
+####################################################################################################
+#
+# PySpice - A Spice Package for Python
+# Copyright (C) 2017 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 .
+#
+####################################################################################################
+
+####################################################################################################
+
+import os
+
+from ..RawFile import VariableAbc, RawFileAbc
+
+####################################################################################################
+
+"""This module provide tools to read the output of Ngspice.
+
+Header
+
+.. code::
+
+ Circuit: 230V Rectifier
+
+ Doing analysis at TEMP = 25.000000 and TNOM = 25.000000
+
+ Title: 230V Rectifier
+ Date: Thu Jun 4 23:40:58 2015
+ Plotname: Transient Analysis
+ Flags: real
+ No. Variables: 6
+ No. Points: 0
+ Variables:
+ No. of Data Columns : 6
+ 0 time time
+ 1 v(in) voltage
+ ...
+ 5 i(vinput) current
+ Binary:
+
+Operating Point
+Node voltages and source branch currents:
+
+ * v(node_name)
+ * i(vname)
+
+Sensitivity Analysis
+
+ * v({element})
+ * v({element}_{parameter})
+ * v(v{source})
+
+DC
+
+ * v(v-sweep)
+ * v({node})
+ * i(v{source})
+
+AC
+Frequency, node voltages and source branch currents:
+
+ * frequency
+ * v({node})
+ * i(v{name})
+
+Transient Analysis
+Time, node voltages and source branch currents:
+
+ * time
+ * v({node})
+ * i(v{source})
+
+"""
+
+# * v({element}:bv_max)
+# * i(e.xdz1.ev1)
+
+####################################################################################################
+
+import logging
+
+####################################################################################################
+
+_module_logger = logging.getLogger(__name__)
+
+####################################################################################################
+
+# Fixme: self._
+
+class Variable(VariableAbc):
+
+ ##############################################
+
+ def is_voltage_node(self):
+ return self.name.startswith('v(')
+
+ ##############################################
+
+ def is_branch_current(self):
+ # source branch current
+ return self.name.startswith('i(')
+
+ ##############################################
+
+ @property
+ def simplified_name(self):
+
+ if self.is_voltage_node() or self.is_branch_current():
+ return self.name[2:-1]
+ else:
+ return self.name
+
+####################################################################################################
+
+class RawFile(RawFileAbc):
+
+ """ This class parse the stdout of ngspice and the raw data output.
+
+ Public Attributes:
+
+ :attr:`circuit`
+ same as title
+
+ :attr:`data`
+
+ :attr:`date`
+
+ :attr:`flags`
+ 'real' or 'complex'
+
+ :attr:`number_of_points`
+
+ :attr:`number_of_variables`
+
+ :attr:`plot_name`
+ AC Analysis, Operating Point, Sensitivity Analysis, DC transfer characteristic
+
+ :attr:`temperature`
+
+ :attr:`title`
+
+ :attr:`variables`
+
+ :attr:`warnings`
+
+ """
+
+ _logger = _module_logger.getChild('RawFile')
+
+ _variable_cls = Variable
+
+ ##############################################
+
+ def __init__(self, stdout, number_of_points):
+
+ self.number_of_points = number_of_points
+
+ raw_data = self._read_header(stdout)
+ self._read_variable_data(raw_data)
+ # self._to_analysis()
+
+ self._simulation = None
+
+ ##############################################
+
+ def _read_header(self, stdout):
+
+ """ Parse the header """
+
+ binary_line = b'Binary:' + os.linesep.encode('ascii')
+ binary_location = stdout.find(binary_line)
+ if binary_location < 0:
+ raise NameError('Cannot locate binary data')
+ raw_data_start = binary_location + len(binary_line)
+ # self._logger.debug(os.linesep + stdout[:raw_data_start].decode('utf-8'))
+ header_lines = stdout[:binary_location].splitlines()
+ raw_data = stdout[raw_data_start:]
+ header_line_iterator = iter(header_lines)
+
+ self.circuit_name = self._read_header_field_line(header_line_iterator, 'Circuit')
+ self.temperature, self.nominal_temperature = self._read_temperature_line(header_line_iterator)
+ self.warnings = [self._read_header_field_line(header_line_iterator, 'Warning')
+ for i in range(stdout.count(b'Warning'))]
+ for warning in self.warnings:
+ self._logger.warn(warning)
+ self.title = self._read_header_field_line(header_line_iterator, 'Title')
+ self.date = self._read_header_field_line(header_line_iterator, 'Date')
+ self.plot_name = self._read_header_field_line(header_line_iterator, 'Plotname')
+ self.flags = self._read_header_field_line(header_line_iterator, 'Flags')
+ self.number_of_variables = int(self._read_header_field_line(header_line_iterator, 'No. Variables'))
+ self._read_header_field_line(header_line_iterator, 'No. Points')
+ self._read_header_field_line(header_line_iterator, 'Variables', has_value=False)
+ self._read_header_field_line(header_line_iterator, 'No. of Data Columns ')
+ self._read_header_variables(header_line_iterator)
+
+ return raw_data
+
+ ##############################################
+
+ def fix_case(self):
+
+ """ Ngspice return lower case names. This method fixes the case of the variable names. """
+
+ circuit = self.circuit
+ element_translation = {element.lower():element for element in circuit.element_names}
+ node_translation = {node.lower():node for node in circuit.node_names}
+ for variable in self.variables.values():
+ variable.fix_case(element_translation, node_translation)
+
+ ##############################################
+
+ def _to_dc_analysis(self):
+
+ if 'v(v-sweep)' in self.variables:
+ sweep_variable = self.variables['v(v-sweep)']
+ elif 'v(i-sweep)' in self.variables:
+ sweep_variable = self.variables['v(i-sweep)']
+ else:
+ #
+ raise NotImplementedError
+
+ return super()._to_dc_analysis(sweep_variable)
diff --git a/third_party/PySpice-org/PySpice/PySpice/Spice/NgSpice/Server.py b/third_party/PySpice-org/PySpice/PySpice/Spice/NgSpice/Server.py
new file mode 100644
index 00000000..83a1f493
--- /dev/null
+++ b/third_party/PySpice-org/PySpice/PySpice/Spice/NgSpice/Server.py
@@ -0,0 +1,162 @@
+####################################################################################################
+#
+# 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 .
+#
+####################################################################################################
+
+"""This module provides an interface to run ngspice in server mode and get back the simulation
+output.
+
+When ngspice runs in server mode, it writes on the standard output an header and then the simulation
+output in binary format. At the end of the simulation, it writes on the standard error a line of
+the form:
+
+ .. code::
+
+ @@@ \d+ \d+
+
+where the second number is the number of points of the simulation. Due to the iterative and
+adaptive nature of a transient simulation, the number of points is only known at the end.
+
+Any line starting with "Error" in the standard output indicates an error in the simulation process.
+The line "run simulation(s) aborted" in the standard error indicates the simulation aborted.
+
+Any line starting with *Warning* in the standard error indicates non critical error in the
+simulation process.
+
+"""
+
+####################################################################################################
+
+import logging
+import os
+import re
+import subprocess
+
+####################################################################################################
+
+from .RawFile import RawFile
+
+####################################################################################################
+
+_module_logger = logging.getLogger(__name__)
+
+####################################################################################################
+
+class SpiceServer:
+
+ """This class wraps the execution of ngspice in server mode and convert the output to a Python data
+ structure.
+
+ Example of usage::
+
+ spice_server = SpiceServer(spice_command='/path/to/ngspice')
+ raw_file = spice_server(spice_input)
+
+ It returns a :obj:`PySpice.Spice.RawFile` instance.
+
+ """
+
+ _logger = _module_logger.getChild('SpiceServer')
+
+ SPICE_COMMAND = 'ngspice'
+
+ ##############################################
+
+ def __init__(self, **kwargs):
+
+ self._spice_command = kwargs.get('spice_command') or self.SPICE_COMMAND
+
+ ##############################################
+
+ def _decode_number_of_points(self, line):
+
+ """Decode the number of points in the given line."""
+
+ match = re.match(r'@@@ (\d+) (\d+)', line)
+ if match is not None:
+ return int(match.group(2))
+ else:
+ raise NameError("Cannot decode the number of points")
+
+ ##############################################
+
+ def _parse_stdout(self, stdout):
+
+ """Parse stdout for errors."""
+
+ # self._logger.debug(os.linesep + stdout)
+
+ error_found = False
+ # UnicodeDecodeError: 'utf-8' codec can't decode byte 0xc0 in position 870: invalid start byte
+ # lines = stdout.decode('utf-8').splitlines()
+ lines = stdout.splitlines()
+ for line_index, line in enumerate(lines):
+ if line.startswith(b'Error '):
+ error_found = True
+ self._logger.error(os.linesep + line.decode('utf-8') + os.linesep + lines[line_index+1].decode('utf-8'))
+ if error_found:
+ raise NameError("Errors was found by Spice")
+
+ ##############################################
+
+ def _parse_stderr(self, stderr):
+
+ """Parse stderr for warnings and return the number of points."""
+
+ self._logger.debug(os.linesep + stderr)
+
+ stderr_lines = stderr.splitlines()
+ number_of_points = None
+ for line in stderr_lines:
+ if line.startswith('Warning:'):
+ self._logger.warning(line[len('Warning :'):])
+ elif line == 'run simulation(s) aborted':
+ raise NameError('Simulation aborted' + os.linesep + stderr)
+ elif line.startswith('@@@'):
+ number_of_points = self._decode_number_of_points(line)
+
+ return number_of_points
+
+ ##############################################
+
+ def __call__(self, spice_input):
+
+ """Run SPICE in server mode as a subprocess for the given input and return a
+ :obj:`PySpice.RawFile.RawFile` instance.
+
+ """
+
+ self._logger.info("Start the spice subprocess")
+
+ process = subprocess.Popen((self._spice_command, '-s'),
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ input_ = str(spice_input).encode('utf-8')
+ stdout, stderr = process.communicate(input_)
+ # stdout = stdout.decode('utf-8')
+ stderr = stderr.decode('utf-8')
+
+ self._parse_stdout(stdout)
+ number_of_points = self._parse_stderr(stderr)
+ if number_of_points is None:
+ raise NameError('The number of points was not found in the standard error buffer,'
+ ' ngspice returned:' + os.linesep +
+ stderr)
+
+ return RawFile(stdout, number_of_points)
diff --git a/third_party/PySpice-org/PySpice/PySpice/Spice/NgSpice/Shared.py b/third_party/PySpice-org/PySpice/PySpice/Spice/NgSpice/Shared.py
new file mode 100644
index 00000000..183c08a4
--- /dev/null
+++ b/third_party/PySpice-org/PySpice/PySpice/Spice/NgSpice/Shared.py
@@ -0,0 +1,1324 @@
+####################################################################################################
+#
+# 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 .
+#
+####################################################################################################
+
+"""This module provides a Python interface to the Ngspice shared library described in the *ngspice
+as shared library or dynamic link library* section of the Ngspice user manual.
+
+In comparison to the subprocess interface, it provides an interaction with the simulator through
+commands and callbacks and it enables the usage of external voltage and current source in the
+circuit.
+
+.. This approach corresponds to the *standard way* to make an interface to a simulator code.
+
+.. warning:: Since we don't simulate a circuit in a fresh environment on demand, this approach is
+ less safe than the subprocess approach. In case of bugs within Ngspice, we can expect some side
+ effects like memory leaks or worse unexpected things.
+
+This interface use the CFFI module to interface with the shared library. It is thus suited to run
+within the Pypy interpreter which implements JIT optimisation for Python.
+
+It can also be used to experiment parallel simulation as explained in the Ngspice user manual. But
+it seems the Ngspice source code was designed with global variables which imply to use one copy of
+the shared library by worker as explained in the manual.
+
+.. warning:: This interface can strongly slow down the simulation if the input or output callbacks
+ is used. If the simulation time goes wrong for you then you need to implement the callbacks at a
+ lower level than Python. You can have look to Pypy, Cython or a C extension module.
+
+"""
+
+####################################################################################################
+
+# 16.7 Environmental variables
+# 16.7.1 Ngspice specific variables
+#
+# SPICE_LIB_DIR
+# default: /usr/local/share/ngspice (Linux, CYGWIN), C:\Spice\share\ngspice (Windows)
+# SPICE_EXEC_DIR
+# default: /usr/local/bin (Linux, CYGWIN), C:\Spice\bin (Windows)
+# SPICE_ASCIIRAWFILE
+# default: 0
+# Format of the rawfile. 0 for binary, and 1 for ascii.
+# SPICE_SCRIPTS
+# default: $SPICE_LIB_DIR/scripts
+# In this directory the spinit file will be searched.
+# NGSPICE_MEAS_PRECISION
+# default: 5
+# Sets the number of digits if output values are printed by the meas(ure) command.
+# SPICE_NO_DATASEG_CHECK
+# default: undefined
+# If defined, will suppress memory resource info (probably obsolete, not used on Windows
+# or where the /proc information system is available.)
+# NGSPICE_INPUT_DIR
+# default: undefined
+# If defined, using a valid directory name, will add the given directory to the search path
+# when looking for input files (*.cir, *.inc, *.lib).
+
+####################################################################################################
+
+__all__ = [
+ 'NgSpiceCircuitError',
+ 'NgSpiceCommandError',
+ 'NgSpiceShared',
+]
+
+####################################################################################################
+
+from pathlib import Path
+import logging
+import os
+import platform
+import re
+
+import numpy as np
+
+from cffi import FFI
+
+####################################################################################################
+
+from PySpice.Config import ConfigInstall
+from PySpice.Probe.WaveForm import (
+ OperatingPoint, SensitivityAnalysis,
+ DcAnalysis, AcAnalysis, TransientAnalysis,
+ PoleZeroAnalysis, NoiseAnalysis, DistortionAnalysis, TransferFunctionAnalysis,
+ WaveForm,
+)
+from PySpice.Tools.EnumFactory import EnumFactory
+from PySpice.Unit import u_V, u_A, u_s, u_Hz, u_F, u_Degree
+
+from .SimulationType import SIMULATION_TYPE
+
+####################################################################################################
+
+ffi = FFI()
+
+####################################################################################################
+
+_module_logger = logging.getLogger(__name__)
+
+####################################################################################################
+
+class NgSpiceCircuitError(NameError):
+ pass
+
+class NgSpiceCommandError(NameError):
+ pass
+
+####################################################################################################
+
+def ffi_string_utf8(_):
+ _ = ffi.string(_)
+ try:
+ return _.decode('utf8')
+ except UnicodeDecodeError:
+ return _
+
+####################################################################################################
+
+class Vector:
+
+ """ This class implements a vector in a simulation output.
+
+ Public Attributes:
+
+ :attr:`data`
+
+ :attr:`name`
+
+ """
+
+ _logger = _module_logger.getChild('Vector')
+
+ ##############################################
+
+ def __init__(self, ngspice_shared, name, type_, data):
+
+ self._ngspice_shared = ngspice_shared
+ self._name = str(name)
+ self._type = type_
+ self._data = data
+ self._unit = ngspice_shared.type_to_unit(type_)
+ if self._unit is None:
+ self._logger.warning('Unit is None for {0._name} {0._type}'.format(self))
+
+ ##############################################
+
+ def __repr__(self):
+ return 'variable: {0._name} {0._type}'.format(self)
+
+ ##############################################
+
+ @property
+ def is_interval_parameter(self):
+ return self._name.startswith('@')
+
+ ##############################################
+
+ @property
+ def is_voltage_node(self):
+ return self._type == self._ngspice_shared.simulation_type.voltage and not self.is_interval_parameter
+
+ ##############################################
+
+ @property
+ def is_branch_current(self):
+ return self._type == self._ngspice_shared.simulation_type.current and not self.is_interval_parameter
+
+ ##############################################
+
+ @property
+ def simplified_name(self):
+
+ if self.is_voltage_node and self._name.startswith('V('):
+ return self._name[2:-1]
+ elif self.is_branch_current:
+ # return self._name.replace('#branch', '')
+ return self._name[:-7]
+ else:
+ return self._name
+
+ ##############################################
+
+ def to_waveform(self, abscissa=None, to_real=False, to_float=False):
+
+ """ Return a :obj:`PySpice.Probe.WaveForm` instance. """
+
+ data = self._data
+ if to_real:
+ data = data.real
+ # Fixme: else UnitValue instead of UnitValues
+ # if to_float:
+ # data = float(data[0])
+
+ if self._unit is not None:
+ return WaveForm.from_unit_values(self.simplified_name, self._unit(data), abscissa=abscissa)
+ else:
+ return WaveForm.from_array(self.simplified_name, data, abscissa=abscissa)
+
+####################################################################################################
+
+class Plot(dict):
+
+ """ This class implements a plot in a simulation output.
+
+ Public Attributes:
+
+ :attr:`plot_name`
+
+ """
+
+ ##############################################
+
+ def __init__(self, simulation, plot_name):
+
+ super().__init__()
+
+ self._simulation = simulation
+ self.plot_name = plot_name
+
+ ##############################################
+
+ def nodes(self, to_float=False, abscissa=None):
+ return [variable.to_waveform(abscissa, to_float=to_float)
+ for variable in self.values()
+ if variable.is_voltage_node]
+
+ ##############################################
+
+ def branches(self, to_float=False, abscissa=None):
+ return [variable.to_waveform(abscissa, to_float=to_float)
+ for variable in self.values()
+ if variable.is_branch_current]
+
+ ##############################################
+
+ def internal_parameters(self, to_float=False, abscissa=None):
+ return [variable.to_waveform(abscissa, to_float=to_float)
+ for variable in self.values()
+ if variable.is_interval_parameter]
+
+ ##############################################
+
+ def elements(self, abscissa=None):
+ return [variable.to_waveform(abscissa, to_float=True)
+ for variable in self.values()]
+
+ ##############################################
+
+ def to_analysis(self):
+
+ if self.plot_name.startswith('op'):
+ return self._to_operating_point_analysis()
+ elif self.plot_name.startswith('sens'):
+ return self._to_sensitivity_analysis()
+ elif self.plot_name.startswith('dc'):
+ return self._to_dc_analysis()
+ elif self.plot_name.startswith('ac'):
+ return self._to_ac_analysis()
+ elif self.plot_name.startswith('tran'):
+ return self._to_transient_analysis()
+ elif self.plot_name.startswith('disto'):
+ return self._to_distortion_analysis()
+ elif self.plot_name.startswith('noise'):
+ return self._to_noise_analysis()
+ elif self.plot_name.startswith('pz'):
+ return self._to_polezero_analysis()
+ elif self.plot_name.startswith('tf'):
+ return self._to_transfer_function_analysis()
+ else:
+ raise NotImplementedError("Unsupported plot name {}".format(self.plot_name))
+
+ ##############################################
+
+ def _to_operating_point_analysis(self):
+ return OperatingPoint(
+ simulation=self._simulation,
+ nodes=self.nodes(to_float=True),
+ branches=self.branches(to_float=True),
+ internal_parameters=self.internal_parameters(),
+ )
+
+ ##############################################
+
+ def _to_sensitivity_analysis(self):
+ # Fixme: separate v(vinput), analysis.R2.m
+ return SensitivityAnalysis(
+ simulation=self._simulation,
+ elements=self.elements(), # Fixme: internal parameters ???
+ internal_parameters=self.internal_parameters(),
+ )
+
+ ##############################################
+
+ def _to_dc_analysis(self):
+ for name in ('v-sweep', 'i-sweep', 'temp-sweep'):
+ if name in self:
+ sweep_variable = self[name]
+ break
+ else:
+ raise NotImplementedError(str(self))
+ sweep = sweep_variable.to_waveform()
+ return DcAnalysis(
+ simulation=self._simulation,
+ sweep=sweep,
+ nodes=self.nodes(),
+ branches=self.branches(),
+ internal_parameters=self.internal_parameters(),
+ )
+
+ ##############################################
+
+ def _to_ac_analysis(self):
+ frequency = self['frequency'].to_waveform(to_real=True)
+ return AcAnalysis(
+ simulation=self._simulation,
+ frequency=frequency,
+ nodes=self.nodes(),
+ branches=self.branches(),
+ internal_parameters=self.internal_parameters(),
+ )
+
+ ##############################################
+
+ def _to_transient_analysis(self):
+
+ time = self['time'].to_waveform(to_real=True)
+ return TransientAnalysis(
+ simulation=self._simulation,
+ time=time,
+ nodes=self.nodes(abscissa=time),
+ branches=self.branches(abscissa=time),
+ internal_parameters=self.internal_parameters(abscissa=time),
+ )
+
+ ##############################################
+
+ def _to_polezero_analysis(self):
+ return PoleZeroAnalysis(
+ simulation=self._simulation,
+ nodes=self.nodes(),
+ branches=self.branches(),
+ internal_parameters=self.internal_parameters(),
+ )
+
+ ##############################################
+
+ def _to_noise_analysis(self):
+ return NoiseAnalysis(
+ simulation=self._simulation,
+ nodes=self.nodes(),
+ branches=self.branches(),
+ internal_parameters=self.internal_parameters(),
+ )
+
+ ##############################################
+
+ def _to_distortion_analysis(self):
+ frequency = self['frequency'].to_waveform(to_real=True)
+ return DistortionAnalysis(
+ simulation=self._simulation,
+ frequency=frequency,
+ nodes=self.nodes(),
+ branches=self.branches(),
+ internal_parameters=self.internal_parameters(),
+ )
+
+ ##############################################
+
+ def _to_transfer_function_analysis(self):
+ return TransferFunctionAnalysis(
+ simulation=self._simulation,
+ nodes=self.nodes(),
+ branches=self.branches(),
+ internal_parameters=self.internal_parameters(),
+ )
+
+####################################################################################################
+
+class NgSpiceShared:
+
+ _logger = _module_logger.getChild('NgSpiceShared')
+
+ NGSPICE_PATH = None
+ LIBRARY_PATH = None
+
+ MAX_COMMAND_LENGTH = 1023
+ NUMBER_OF_EXEC_CALLS_TO_RELEASE_MEMORY = 10_000
+
+ ##############################################
+
+ @classmethod
+ def setup_platform(cls):
+
+ if ConfigInstall.OS.on_windows:
+ if platform.architecture()[0] != '64bit':
+ raise NameError('Windows 32bit is no longer supported by NgSpice')
+
+ _ = os.environ.get('NGSPICE_LIBRARY_PATH', None)
+ if _ is not None:
+ cls.LIBRARY_PATH = _
+ else:
+ if ConfigInstall.OS.on_windows:
+ ngspice_path = Path(__file__).parent.joinpath('Spice64_dll')
+ cls.NGSPICE_PATH = ngspice_path
+ # path = ngspice_path.joinpath('dll-vs', 'ngspice-{version}{id}.dll')
+ path = ngspice_path.joinpath('dll-vs', 'ngspice{}.dll')
+
+ elif ConfigInstall.OS.on_osx:
+ path = 'libngspice{}.dylib'
+
+ elif ConfigInstall.OS.on_linux:
+ path = 'libngspice{}.so'
+
+ else:
+ raise NotImplementedError
+
+ cls.LIBRARY_PATH = str(path)
+
+ ##############################################
+
+ _instances = {}
+
+ @classmethod
+ def new_instance(cls, ngspice_id=0, send_data=False, verbose=False):
+ """Create a NgSpiceShared instance"""
+
+ # Fixme: send_data
+
+ if ngspice_id in cls._instances:
+ return cls._instances[ngspice_id]
+ else:
+ cls._logger.debug("New instance for id {}".format(ngspice_id))
+ instance = cls(ngspice_id=ngspice_id, send_data=send_data, verbose=verbose)
+ cls._instances[ngspice_id] = instance
+ return instance
+
+ ##############################################
+
+ def __init__(self, ngspice_id=0, send_data=False, verbose=False):
+
+ """ Set the *send_data* flag if you want to enable the output callback.
+
+ Set the *ngspice_id* to an integer value if you want to run NgSpice in parallel.
+ """
+
+ self._ngspice_id = ngspice_id
+
+ self._spinit_not_found = False
+
+ self._number_of_exec_calls = 0
+
+ self._stdout = []
+ self._stderr = []
+ self._error_in_stdout = None
+ self._error_in_stderr = None
+
+ self._has_cider = None
+ self._has_xspice = None
+ self._ngspice_version = None
+ self._extensions = []
+
+ self._library_path = None
+ self._load_library(verbose)
+ self._init_ngspice(send_data)
+
+ self._is_running = False
+
+ ##############################################
+
+ @property
+ def spinit_not_found(self):
+ return self._spinit_not_found
+
+ ##############################################
+
+ @property
+ def library_path(self):
+ if self._library_path is None:
+ if not self._ngspice_id:
+ library_prefix = ''
+ else:
+ library_prefix = '{}'.format(self._ngspice_id) # id =
+ library_path = self.LIBRARY_PATH.format(library_prefix)
+ self._library_path = library_path
+ return self._library_path
+
+ ##############################################
+
+ def _load_library(self, verbose):
+
+ if ConfigInstall.OS.on_windows:
+ # https://sourceforge.net/p/ngspice/discussion/133842/thread/1cece652/#4e32/5ab8/9027
+ # When environment variable SPICE_LIB_DIR is empty, ngspice looks in C:\Spice64\share\ngspice\scripts
+ # Else it tries %SPICE_LIB_DIR%\scripts\spinit
+ if 'SPICE_LIB_DIR' not in os.environ:
+ _ = str(Path(self.NGSPICE_PATH).joinpath('share', 'ngspice'))
+ os.environ['SPICE_LIB_DIR'] = _
+ # self._logger.warning('Set SPICE_LIB_DIR = %s', _)
+
+ # Fixme: not compatible with supra
+ # if 'CONDA_PREFIX' in os.environ:
+ # _ = str(Path(os.environ['CONDA_PREFIX']).joinpath('share', 'ngspice'))
+ # os.environ['SPICE_LIB_DIR'] = _
+ # self._logger.warning('Set SPICE_LIB_DIR = %s', _)
+
+ # https://sourceforge.net/p/ngspice/bugs/490
+ # ngspice and Kicad do setlocale(LC_NUMERIC, "C");
+ if ConfigInstall.OS.on_windows:
+ self._logger.debug('locale LC_NUMERIC is not forced to C')
+ elif ConfigInstall.OS.on_linux or ConfigInstall.OS.on_osx:
+ self._logger.debug('Set locale LC_NUMERIC to C')
+ import locale
+ locale.setlocale(locale.LC_NUMERIC, 'C')
+
+ api_path = Path(__file__).parent.joinpath('api.h')
+ with open(api_path) as fh:
+ ffi.cdef(fh.read())
+
+ message = 'Load library {}'.format(self.library_path)
+ self._logger.debug(message)
+ if verbose:
+ print(message)
+ self._ngspice_shared = ffi.dlopen(self.library_path)
+
+ # Note: cannot yet execute command
+
+ ##############################################
+
+ def _init_ngspice(self, send_data):
+
+ # Ngspice API: ngSpice_Init ngSpice_Init_Sync
+
+ self._send_char_c = ffi.callback('int (char *, int, void *)', self._send_char)
+ self._send_stat_c = ffi.callback('int (char *, int, void *)', self._send_stat)
+ self._exit_c = ffi.callback('int (int, bool, bool, int, void *)', self._exit)
+ self._send_init_data_c = ffi.callback('int (pvecinfoall, int, void *)', self._send_init_data)
+ self._background_thread_running_c = ffi.callback('int (bool, int, void *)', self._background_thread_running)
+
+ if send_data:
+ self._send_data_c = ffi.callback('int (pvecvaluesall, int, int, void *)', self._send_data)
+ else:
+ self._send_data_c = FFI.NULL
+
+ self._get_vsrc_data_c = ffi.callback('int (double *, double, char *, int, void *)', self._get_vsrc_data)
+ self._get_isrc_data_c = ffi.callback('int (double *, double, char *, int, void *)', self._get_isrc_data)
+
+ self_c = ffi.new_handle(self)
+ self._self_c = self_c # To prevent garbage collection
+
+ rc = self._ngspice_shared.ngSpice_Init(self._send_char_c,
+ self._send_stat_c,
+ self._exit_c,
+ self._send_data_c,
+ self._send_init_data_c,
+ self._background_thread_running_c,
+ self_c)
+ if rc:
+ raise NameError("Ngspice_Init returned {}".format(rc))
+
+ ngspice_id_c = ffi.new('int *', self._ngspice_id)
+ self._ngspice_id = ngspice_id_c # To prevent garbage collection
+ rc = self._ngspice_shared.ngSpice_Init_Sync(self._get_vsrc_data_c,
+ self._get_isrc_data_c,
+ FFI.NULL, # GetSyncData
+ ngspice_id_c,
+ self_c)
+ if rc:
+ raise NameError("Ngspice_Init_Sync returned {}".format(rc))
+
+ self._get_version()
+
+ try:
+ self._simulation_type = EnumFactory('SimulationType', SIMULATION_TYPE[self._ngspice_version])
+ except KeyError:
+ # See SimulationType.py
+ self._simulation_type = EnumFactory('SimulationType', SIMULATION_TYPE['last'])
+ self._logger.warning("Unsupported Ngspice version {}".format(self._ngspice_version))
+ self._type_to_unit = {
+ self._simulation_type.time: u_s,
+ self._simulation_type.voltage: u_V,
+ self._simulation_type.current: u_A,
+ self._simulation_type.frequency: u_Hz,
+ self._simulation_type.capacitance: u_F,
+ self._simulation_type.temperature: u_Degree,
+ }
+
+ # Prevent paging output of commands (hangs)
+ self.set('nomoremode')
+
+ ##############################################
+
+ @staticmethod
+ def _send_char(message_c, ngspice_id, user_data):
+
+ """Callback for sending output from stdout, stderr to caller"""
+
+ self = ffi.from_handle(user_data)
+ _module_logger.debug(str(ffi.string(message_c)))
+ message = ffi_string_utf8(message_c)
+
+ # split message in ""
+ prefix, _, content = message.partition(' ')
+ if prefix == 'stderr':
+ self._stderr.append(content)
+ if content.startswith('Warning:'):
+ func = self._logger.warning
+ # elif content.startswith('Warning:'):
+ else:
+ self._error_in_stderr = True
+ func = self._logger.error
+ if content.strip() == "Note: can't find init file.":
+ self._spinit_not_found = True
+ self._logger.warning('spinit was not found')
+ func(content)
+ else:
+ self._stdout.append(content)
+ # Fixme: Ngspice writes error on stdout and stderr ...
+ if 'error' in content.lower():
+ self._error_in_stdout = True
+ # if self._error_in_stdout:
+ # self._logger.warning(content)
+
+ # Fixme: ???
+ return self.send_char(message, ngspice_id)
+
+ ##############################################
+
+ @staticmethod
+ def _send_stat(message, ngspice_id, user_data):
+ """Callback for simulation status to caller"""
+ self = ffi.from_handle(user_data)
+ return self.send_stat(ffi_string_utf8(message), ngspice_id)
+
+ ##############################################
+
+ @staticmethod
+ def _exit(exit_status, immediate_unloding, quit_exit, ngspice_id, user_data):
+ """Callback for asking for a reaction after controlled exit"""
+ self = ffi.from_handle(user_data)
+ self._logger.debug('ngspice_id-{} exit status={} immediate_unloding={} quit_exit={}'.format(
+ ngspice_id,
+ exit_status,
+ immediate_unloding,
+ quit_exit))
+ return exit_status
+
+ ##############################################
+
+ @staticmethod
+ def _send_data(data, number_of_vectors, ngspice_id, user_data):
+ """Callback to send back actual vector data"""
+ self = ffi.from_handle(user_data)
+ # self._logger.debug('ngspice_id-{} send_data [{}]'.format(ngspice_id, data.vecindex))
+ actual_vector_values = {}
+ for i in range(int(number_of_vectors)):
+ actual_vector_value = data.vecsa[i]
+ vector_name = ffi_string_utf8(actual_vector_value.name)
+ value = complex(actual_vector_value.creal, actual_vector_value.cimag)
+ actual_vector_values[vector_name] = value
+ # self._logger.debug(' Vector: {} {}'.format(vector_name, value))
+ return self.send_data(actual_vector_values, number_of_vectors, ngspice_id)
+
+ ##############################################
+
+ @staticmethod
+ def _send_init_data(data, ngspice_id, user_data):
+ """Callback to send back initialization vector data"""
+ self = ffi.from_handle(user_data)
+ # if self._logger.isEnabledFor(logging.DEBUG):
+ # self._logger.debug('ngspice_id-{} send_init_data'.format(ngspice_id))
+ # number_of_vectors = data.veccount
+ # for i in range(number_of_vectors):
+ # self._logger.debug(' Vector: ' + ffi_string_utf8(data.vecs[i].vecname))
+ return self.send_init_data(data, ngspice_id) # Fixme: should be a Python object
+
+ ##############################################
+
+ @staticmethod
+ def _background_thread_running(is_running, ngspice_id, user_data):
+ """Callback to indicate if background thread is runnin"""
+ self = ffi.from_handle(user_data)
+ self._logger.debug('ngspice_id-{} background_thread_running {}'.format(ngspice_id, is_running))
+ self._is_running = is_running
+
+ ##############################################
+
+ @staticmethod
+ def _get_vsrc_data(voltage, time, node, ngspice_id, user_data):
+ """FFI Callback"""
+ self = ffi.from_handle(user_data)
+ return self.get_vsrc_data(voltage, time, ffi_string_utf8(node), ngspice_id)
+
+ ##############################################
+
+ @staticmethod
+ def _get_isrc_data(current, time, node, ngspice_id, user_data):
+ """FFI Callback"""
+ self = ffi.from_handle(user_data)
+ return self.get_isrc_data(current, time, ffi_string_utf8(node), ngspice_id)
+
+ ##############################################
+
+ def send_char(self, message, ngspice_id):
+ """ Reimplement this callback in a subclass to process logging messages from the simulator. """
+ # self._logger.debug('ngspice-{} send_char {}'.format(ngspice_id, message))
+ return 0
+
+ ##############################################
+
+ def send_stat(self, message, ngspice_id):
+ """ Reimplement this callback in a subclass to process statistic messages from the simulator. """
+ # self._logger.debug('ngspice-{} send_stat {}'.format(ngspice_id, message))
+ return 0
+
+ ##############################################
+
+ def send_data(self, actual_vector_values, number_of_vectors, ngspice_id):
+ """ Reimplement this callback in a subclass to process the vector actual values. """
+ return 0
+
+ ##############################################
+
+ def send_init_data(self, data, ngspice_id):
+ """ Reimplement this callback in a subclass to process the initial data. """
+ return 0
+
+ ##############################################
+
+ def get_vsrc_data(self, voltage, time, node, ngspice_id):
+ """ Reimplement this callback in a subclass to provide external voltage source. """
+ self._logger.debug('ngspice_id-{} get_vsrc_data @{} node {}'.format(ngspice_id, time, node))
+ return 0
+
+ ##############################################
+
+ def get_isrc_data(self, current, time, node, ngspice_id):
+ """ Reimplement this callback in a subclass to provide external current source. """
+ self._logger.debug('ngspice_id-{} get_isrc_data @{} node {}'.format(ngspice_id, time, node))
+ return 0
+
+ ##############################################
+
+ @staticmethod
+ def _convert_string_array(array):
+ strings = []
+ i = 0
+ while True:
+ if array[i] == FFI.NULL:
+ break
+ strings.append(ffi_string_utf8(array[i]))
+ i += 1
+ return strings
+
+ ##############################################
+
+ @staticmethod
+ def _to_python(value):
+ try:
+ return int(value)
+ except ValueError:
+ try:
+ # Fixme: return float(value.replace(',', '.'))
+ return float(value)
+ except ValueError:
+ return str(value)
+
+ ##############################################
+
+ @staticmethod
+ def _lines_to_dicts(lines):
+ if lines:
+ values = dict(description=lines[0])
+ values.update({
+ parts[0]: NgSpiceShared._to_python(parts[1])
+ for parts in map(str.split, lines)
+ })
+ return values
+ else:
+ raise ValueError
+
+ ##############################################
+
+ @property
+ def is_running(self):
+ return self._is_running
+
+ ##############################################
+
+ def clear_output(self):
+ self._stdout = []
+ self._stderr = []
+ self._error_in_stdout = False
+ self._error_in_stderr = False
+
+ ##############################################
+
+ @property
+ def stdout(self):
+ return os.linesep.join(self._stdout)
+
+ @property
+ def stderr(self):
+ return os.linesep.join(self._stderr)
+
+ ##############################################
+
+ def exec_command(self, command, join_lines=True):
+
+ """ Execute a command and return the output. """
+
+ # Ngspice API: ngSpice_Command
+
+ # Prevent memory leaks by periodically freeing ngspice history of past commands
+ # Each command sent to ngspice is stored in the control structures
+ if self._number_of_exec_calls > self.NUMBER_OF_EXEC_CALLS_TO_RELEASE_MEMORY:
+ # Clear the internal control structures
+ self._ngspice_shared.ngSpice_Command(FFI.NULL)
+ self._number_of_exec_calls = 0
+ self._number_of_exec_calls += 1
+
+ if len(command) > self.MAX_COMMAND_LENGTH:
+ raise ValueError('Command must not exceed {} characters'.format(self.MAX_COMMAND_LENGTH))
+
+ self._logger.debug('Execute command: {}'.format(command))
+
+ self.clear_output()
+
+ encoded_command = command.encode('ascii')
+ rc = self._ngspice_shared.ngSpice_Command(encoded_command)
+
+ if rc: # Fixme: when not 0 ???
+ raise NameError("ngSpice_Command '{}' returned {}".format(command, rc))
+
+ if self._error_in_stdout or self._error_in_stderr:
+ raise NgSpiceCommandError("Command '{}' failed".format(command))
+
+ if join_lines:
+ return self.stdout
+ else:
+ return self._stdout
+
+ ##############################################
+
+ def _get_version(self):
+
+ self._ngspice_version = None
+ self._has_xspice = False
+ self._has_cider = False
+ self._extensions = []
+
+ output = self.exec_command('version -f')
+ for line in output.split('\n'):
+ match = re.match(r'\*\* ngspice\-(\d+)', line)
+ if match is not None:
+ self._ngspice_version = int(match.group(1))
+ # if '** XSPICE extensions included' in line:
+ if '** XSPICE' in line:
+ self._has_xspice = True
+ self._extensions.append('XSPICE')
+ # if '** CIDER 1.b1 (CODECS simulator) included' in line:
+ if 'CIDER' in line:
+ self._has_cider = True
+ self._extensions.append('CIDER')
+
+ self._logger.debug(
+ 'Ngspice version %s with extensions: %s',
+ self._ngspice_version,
+ ', '.join(self._extensions),
+ )
+
+ ##############################################
+
+ @property
+ def ngspice_version(self):
+ return self._ngspice_version
+
+ @property
+ def has_xspice(self):
+ """Return True if libngspice was compiled with XSpice support."""
+ return self._has_xspice
+
+ @property
+ def has_cider(self):
+ """Return True if libngspice was compiled with CIDER support."""
+ return self._has_cider
+
+ ##############################################
+
+ @property
+ def simulation_type(self):
+ return self._simulation_type
+
+ def type_to_unit(self, vector_type):
+ return self._type_to_unit.get(vector_type, None)
+
+ ##############################################
+
+ def _alter(self, command, device, kwargs):
+ # Performance optimization: dispatch multiple alter commands jointly
+ device_name = device.lower()
+ commands = []
+ commands_str_len = 0
+ for key, value in kwargs.items():
+ if isinstance(value, (list, tuple)):
+ value = '[ ' + ' '.join(value) + ' ]'
+ cmd = '{} {} {} = {}'.format(command, device_name, key, value)
+ # performance optimization: collect multiple alter commands and
+ # dispatch them jointly
+ commands.append(cmd)
+ commands_str_len += len(cmd)
+ if commands_str_len + len(commands) > self.MAX_COMMAND_LENGTH:
+ self.exec_command(';'.join(commands[:-1]))
+ commands = commands[-1:]
+ commands_str_len = len(commands[0])
+ if commands:
+ self.exec_command(';'.join(commands))
+
+ ##############################################
+
+ def alter_device(self, device, **kwargs):
+ """Alter device parameters"""
+ self._alter('alter', device, kwargs)
+
+ ##############################################
+
+ def alter_model(self, model, **kwargs):
+ """Alter model parameters"""
+ self._alter('altermod', model, kwargs)
+
+ ##############################################
+
+ def delete(self, debug_number):
+ """Remove a trace or breakpoint"""
+ self.exec_command('delete {}'.format(debug_number))
+
+ ##############################################
+
+ def destroy(self, plot_name='all'):
+ """Release the memory holding the output data (the given plot or all plots) for the specified runs."""
+ self.exec_command('destroy ' + plot_name)
+
+ ##############################################
+
+ def device_help(self, device):
+ """Shows the user information about the devices available in the simulator. """
+ return self.exec_command('devhelp ' + device.lower())
+
+ ##############################################
+
+ def save(self, vector):
+ self.exec_command('save ' + vector)
+
+ ##############################################
+
+ def _show(self, command):
+ lines = self.exec_command(command, join_lines=False)
+ if lines:
+ values = self._lines_to_dicts(lines)
+ return values
+ else:
+ return ''
+
+ ##############################################
+
+ def show(self, device):
+ return self._show('show ' + device.lower())
+
+ ##############################################
+
+ def showmod(self, device):
+ return self._show('showmod ' + device.lower())
+
+ ##############################################
+
+ def source(self, file_path):
+ """Read a ngspice input file"""
+ self.exec_command('source ' + file_path)
+
+ ##############################################
+
+ def option(self, **kwargs):
+ """Set any of the simulator variables."""
+ for key, value in kwargs.items():
+ self.exec_command('option {} = {}'.format(key, value))
+
+ ##############################################
+
+ def quit(self):
+ self.set('noaskquit')
+ return self.exec_command('quit')
+
+ ##############################################
+
+ def remove_circuit(self):
+ """Removes the current circuit from the list of circuits sourced into ngspice."""
+ self.exec_command('remcirc')
+
+ ##############################################
+
+ def reset(self):
+ """Throw out any intermediate data in the circuit (e.g, after a breakpoint or after one or more
+ analyses have been done already), and re-parse the input file. The circuit can then be
+ re-run from it’s initial state, overriding the affect of any set or alter commands.
+
+ """
+ self.exec_command('reset')
+
+ ##############################################
+
+ def ressource_usage(self, *ressources):
+
+ """Print resource usage statistics. If any resources are given, just print the usage of that resource.
+
+ Most resources require that a circuit be loaded. Currently valid resources are:
+
+ * decklineno Number of lines in deck
+ * netloadtime Nelist loading time
+ * netparsetime Netlist parsing time
+ * elapsed The amount of time elapsed since the last rusage elapsed call.
+ * faults Number of page faults and context switches (BSD only).
+ * space Data space used.
+ * time CPU time used so far.
+ * temp Operating temperature.
+ * tnom Temperature at which device parameters were measured.
+ * equations Circuit Equations
+ * time Total Analysis Time
+ * totiter Total iterations
+ * accept Accepted time-points
+ * rejected Rejected time-points
+ * loadtime Time spent loading the circuit matrix and RHS.
+ * reordertime Matrix reordering time
+ * lutime L-U decomposition time
+ * solvetime Matrix solve time
+ * trantime Transient analysis time
+ * tranpoints Transient time-points
+ * traniter Transient iterations
+ * trancuriters Transient iterations for the last time point*
+ * tranlutime Transient L-U decomposition time
+ * transolvetime Transient matrix solve time
+ * everything All of the above.
+ """
+
+ if not ressources:
+ ressources = ['everything']
+
+ command = 'rusage ' + ' '.join(ressources)
+ lines = self.exec_command(command, join_lines=False)
+ values = {}
+ for line in lines:
+ if '=' in line:
+ parts = line.split(' = ')
+ else:
+ parts = line.split(': ')
+ values[parts[0]] = NgSpiceShared._to_python(parts[1])
+ return values
+
+ ##############################################
+
+ def set(self, *args, **kwargs):
+ """Set the value of variables"""
+ for key in args:
+ self.exec_command('set {}'.format(key))
+ for key, value in kwargs.items():
+ self.exec_command('option {} = {}'.format(key, value))
+
+ ##############################################
+
+ def set_circuit(self, name):
+ """Change the current circuit"""
+ self.exec_command('setcirc {}'.format(name))
+
+ ##############################################
+
+ def status(self):
+ """Display breakpoint information"""
+ return self.exec_command('status')
+
+ ##############################################
+
+ def step(self, number_of_steps=None):
+ """Run a fixed number of time-points"""
+ if number_of_steps is not None:
+ self.exec_command('step {}'.format(number_of_steps))
+ else:
+ self.exec_command('step')
+
+ ##############################################
+
+ def stop(self, *args, **kwargs):
+
+ """Set a breakpoint.
+
+ Examples::
+
+ ngspice.stop('v(out) > 1', 'v(1) > 10', after=10)
+
+ A when condition can use theses symbols: = <> > < >= <=.
+
+ """
+
+ command = 'stop'
+ if 'after' in kwargs:
+ command += ' after {}'.format(kwargs['after'])
+ for condition in args:
+ command += ' when {}'.format(condition)
+ self.exec_command(command)
+
+ ##############################################
+
+ def trace(self, *args):
+ """Trace nodes"""
+ self.exec_command('trace ' + ' '.join(args))
+
+ ##############################################
+
+ def unset(self, *args):
+ """Unset variables"""
+ for key in args:
+ self.exec_command('unset {}'.format(key))
+
+ ##############################################
+
+ def where(self):
+ """Identify troublesome node or device"""
+ return self.exec_command('where')
+
+ ##############################################
+
+ def load_circuit(self, circuit):
+
+ """Load the given circuit string."""
+
+ # Ngspice API: ngSpice_Circ
+ circuit_lines = [line for line in str(circuit).splitlines() if line]
+ self._logger.debug('ngSpice_Circ\n' + str(circuit))
+
+ # ngspice 33 requires an empty line at the end
+ circuit_lines.append("")
+
+ circuit_lines_keepalive = [ffi.new("char[]", line.encode('utf8'))
+ for line in circuit_lines]
+ circuit_lines_keepalive += [FFI.NULL]
+ circuit_array = ffi.new("char *[]", circuit_lines_keepalive)
+ self.clear_output()
+ rc = self._ngspice_shared.ngSpice_Circ(circuit_array)
+
+ if rc: # Fixme: when not 0 ???
+ raise NameError("ngSpice_Circ returned {}".format(rc))
+
+ # Fixme: when Ngspice found an error in the circuit, it reports the error in stdout
+ # Fixme: https://sourceforge.net/p/ngspice/bugs/496/
+ if self._error_in_stdout:
+ self._logger.error('\n' + self.stdout)
+ raise NgSpiceCircuitError('')
+
+ # for line in circuit_lines:
+ # rc = self._ngspice_shared.ngSpice_Command(('circbyline ' + line).encode('utf8'))
+ # if rc:
+ # raise NameError("ngSpice_Command circbyline returned {}".format(rc))
+
+ ##############################################
+
+ def listing(self):
+ command = 'listing'
+ return self.exec_command(command)
+
+ ##############################################
+
+ def run(self, background=False):
+
+ """ Run the simulation. """
+
+ # in the background thread and wait until the simulation is done
+
+ command = 'bg_run' if background else 'run'
+ self.exec_command(command)
+
+ if background:
+ self._is_running = True
+ else:
+ self._logger.debug("Simulation is done")
+
+ # time.sleep(.1) # required before to test if the simulation is running
+ # while (self._ngspice_shared.ngSpice_running()):
+ # time.sleep(.1)
+ # self._logger.debug("Simulation is done")
+
+ ##############################################
+
+ def halt(self):
+ """ Halt the simulation in the background thread. """
+ self.exec_command('bg_halt')
+
+ ##############################################
+
+ def resume(self, background=True):
+ """ Halt the simulation in the background thread. """
+ command = 'bg_resume' if background else 'resume'
+ self.exec_command(command)
+
+ ##############################################
+
+ @property
+ def plot_names(self):
+ """ Return the list of plot names. """
+ # Ngspice API: ngSpice_AllPlots
+ return self._convert_string_array(self._ngspice_shared.ngSpice_AllPlots())
+
+ ##############################################
+
+ @property
+ def last_plot(self):
+ """ Return the last plot name. """
+ return self.plot_names[0]
+
+ ##############################################
+
+ @staticmethod
+ def _flags_to_str(flags):
+
+ # enum dvec_flags {
+ # VF_REAL = (1 << 0), // The data is real.
+ # VF_COMPLEX = (1 << 1), // The data is complex.
+ # VF_ACCUM = (1 << 2), // writedata should save this vector.
+ # VF_PLOT = (1 << 3), // writedata should incrementally plot it.
+ # VF_PRINT = (1 << 4), // writedata should print this vector.
+ # VF_MINGIVEN = (1 << 5), // The v_minsignal value is valid.
+ # VF_MAXGIVEN = (1 << 6), // The v_maxsignal value is valid.
+ # VF_PERMANENT = (1 << 7) // Don't garbage collect this vector.
+ # };
+
+ if flags & 1:
+ return 'real'
+ elif flags & 2:
+ return 'complex'
+ else:
+ raise NotImplementedError
+
+ ##############################################
+
+ @staticmethod
+ def _vector_is_real(flags):
+ return flags & 1
+
+ ##############################################
+
+ @staticmethod
+ def _vector_is_complex(flags):
+ return flags & 2
+
+ ##############################################
+
+ def plot(self, simulation, plot_name):
+
+ """ Return the corresponding plot. """
+
+ # Ngspice API: ngSpice_AllVecs ngGet_Vec_Info
+
+ # plot_name is for example dc with an integer suffix which is increment for each run
+
+ plot = Plot(simulation, plot_name)
+ all_vectors_c = self._ngspice_shared.ngSpice_AllVecs(plot_name.encode('utf8'))
+ i = 0
+ while True:
+ if all_vectors_c[i] == FFI.NULL:
+ break
+
+ vector_name = ffi_string_utf8(all_vectors_c[i])
+ name = '.'.join((plot_name, vector_name))
+ vector_info = self._ngspice_shared.ngGet_Vec_Info(name.encode('utf8'))
+ vector_type = self._simulation_type[vector_info.v_type]
+ length = vector_info.v_length
+ # template = 'vector[{}] {} type {} flags {} length {}'
+ # self._logger.debug(template.format(
+ # i,
+ # vector_name,
+ # vector_type,
+ # self._flags_to_str(vector_info.v_flags),
+ # length,
+ # ))
+ if vector_info.v_compdata == FFI.NULL:
+ # for k in range(length):
+ # print(" [{}] {}".format(k, vector_info.v_realdata[k]))
+ tmp_array = np.frombuffer(ffi.buffer(vector_info.v_realdata, length*8), dtype=np.float64)
+ array = np.array(tmp_array, dtype=tmp_array.dtype) # copy data
+ # import json
+ # with open(name + '.json', 'w') as fh:
+ # json.dump(list(array), fh)
+ else:
+ # for k in range(length):
+ # value = vector_info.v_compdata[k]
+ # print(ffi.addressof(value, field='cx_real'), ffi.addressof(value, field='cx_imag'))
+ # print(" [{}] {} + i {}".format(k, value.cx_real, value.cx_imag))
+ tmp_array = np.frombuffer(ffi.buffer(vector_info.v_compdata, length*8*2), dtype=np.float64)
+ array = np.array(tmp_array[0::2], dtype=np.complex128)
+ array.imag = tmp_array[1::2]
+ plot[vector_name] = Vector(self, vector_name, vector_type, array)
+
+ i += 1
+
+ return plot
+
+####################################################################################################
+#
+# Platform setup
+#
+
+NgSpiceShared.setup_platform()
diff --git a/third_party/PySpice-org/PySpice/PySpice/Spice/NgSpice/Simulation.py b/third_party/PySpice-org/PySpice/PySpice/Spice/NgSpice/Simulation.py
new file mode 100644
index 00000000..92b62ec2
--- /dev/null
+++ b/third_party/PySpice-org/PySpice/PySpice/Spice/NgSpice/Simulation.py
@@ -0,0 +1,127 @@
+###################################################################################################
+#
+# 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 .
+#
+####################################################################################################
+
+"""This modules implements classes to perform simulations.
+"""
+
+####################################################################################################
+
+import logging
+
+####################################################################################################
+
+from ..Simulation import CircuitSimulator
+from .Server import SpiceServer
+from .Shared import NgSpiceShared
+
+####################################################################################################
+
+_module_logger = logging.getLogger(__name__)
+
+####################################################################################################
+
+class NgSpiceCircuitSimulator(CircuitSimulator):
+
+ SIMULATOR = 'ngspice'
+
+ ##############################################
+
+ def __init__(self, circuit, **kwargs):
+
+ super().__init__(circuit, **kwargs)
+
+ if kwargs.get('pipe', True):
+ self.options('NOINIT')
+ self.options(filetype='binary')
+
+####################################################################################################
+
+class NgSpiceSubprocessCircuitSimulator(NgSpiceCircuitSimulator):
+
+ _logger = _module_logger.getChild('NgSpiceSubprocessCircuitSimulator')
+
+ ##############################################
+
+ def __init__(self, circuit, **kwargs):
+
+ super().__init__(circuit, pipe=True, **kwargs)
+
+ # Fixme: to func ?
+ server_kwargs = {x:kwargs[x] for x in ('spice_command',) if x in kwargs}
+ self._spice_server = SpiceServer(**server_kwargs)
+
+ ##############################################
+
+ def _run(self, analysis_method, *args, **kwargs):
+
+ super()._run(analysis_method, *args, **kwargs)
+
+ raw_file = self._spice_server(spice_input=str(self))
+ self.reset_analysis()
+ raw_file.simulation = self
+
+ # for field in raw_file.variables:
+ # print field
+
+ return raw_file.to_analysis()
+
+####################################################################################################
+
+class NgSpiceSharedCircuitSimulator(NgSpiceCircuitSimulator):
+
+ _logger = _module_logger.getChild('NgSpiceSharedCircuitSimulator')
+
+ ##############################################
+
+ def __init__(self, circuit, **kwargs):
+
+ super().__init__(circuit, pipe=False, **kwargs)
+
+ ngspice_shared = kwargs.get('ngspice_shared', None)
+ if ngspice_shared is None:
+ self._ngspice_shared = NgSpiceShared.new_instance()
+ else:
+ self._ngspice_shared = ngspice_shared
+
+ ##############################################
+
+ @property
+ def ngspice(self):
+ return self._ngspice_shared
+
+ ##############################################
+
+ def _run(self, analysis_method, *args, **kwargs):
+
+ super()._run(analysis_method, *args, **kwargs)
+
+ self._ngspice_shared.destroy()
+ # load circuit and simulation
+ # Fixme: Error: circuit not parsed.
+ self._ngspice_shared.load_circuit(str(self))
+ self._ngspice_shared.run()
+ self._logger.debug(str(self._ngspice_shared.plot_names))
+ self.reset_analysis()
+
+ plot_name = self._ngspice_shared.last_plot
+ if plot_name == 'const':
+ raise NameError('Simulation failed')
+
+ return self._ngspice_shared.plot(self, plot_name).to_analysis()
diff --git a/third_party/PySpice-org/PySpice/PySpice/Spice/NgSpice/SimulationType.py b/third_party/PySpice-org/PySpice/PySpice/Spice/NgSpice/SimulationType.py
new file mode 100644
index 00000000..fe07f41a
--- /dev/null
+++ b/third_party/PySpice-org/PySpice/PySpice/Spice/NgSpice/SimulationType.py
@@ -0,0 +1,90 @@
+####################################################################################################
+#
+# 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 .
+#
+####################################################################################################
+
+####################################################################################################
+
+__all__ = [
+ 'LAST_VERSION',
+ 'SIMULATION_TYPE',
+]
+
+####################################################################################################
+
+# For a new ngspice relase, we just have to check this file hasn't changed
+# ngspice-xx/src/include/ngspice/sim.h
+
+SIMULATION_TYPE = {}
+
+SIMULATION_TYPE[26] = (
+ 'no_type',
+ 'time',
+ 'frequency',
+ 'voltage',
+ 'current',
+ 'output_n_dens',
+ 'output_noise',
+ 'input_n_dens',
+ 'input_noise',
+ 'pole',
+ 'zero',
+ 's_parameter',
+ 'temperature',
+ 'res',
+ 'impedance',
+ 'admittance',
+ 'power',
+ 'phase',
+ 'db',
+ 'capacitance',
+ 'charge',
+)
+
+SIMULATION_TYPE[27] = (
+ 'no_type',
+ 'time',
+ 'frequency',
+ 'voltage',
+ 'current',
+ 'voltage_density',
+ 'current_density',
+ 'sqr_voltage_density',
+ 'sqr_current_density',
+ 'sqr_voltage',
+ 'sqr_current',
+ 'pole',
+ 'zero',
+ 's_parameter',
+ 'temperature',
+ 'res',
+ 'impedance',
+ 'admittance',
+ 'power',
+ 'phase',
+ 'db',
+ 'capacitance',
+ 'charge',
+)
+
+LAST_VERSION = 34 # released on January 31st, 2021
+
+for version in range(28, LAST_VERSION +1):
+ SIMULATION_TYPE[version] = SIMULATION_TYPE[27]
+
+SIMULATION_TYPE['last'] = SIMULATION_TYPE[LAST_VERSION]
diff --git a/third_party/PySpice-org/PySpice/PySpice/Spice/NgSpice/__init__.py b/third_party/PySpice-org/PySpice/PySpice/Spice/NgSpice/__init__.py
new file mode 100644
index 00000000..80519135
--- /dev/null
+++ b/third_party/PySpice-org/PySpice/PySpice/Spice/NgSpice/__init__.py
@@ -0,0 +1,23 @@
+####################################################################################################
+#
+# PySpice - A Spice Package for Python
+# Copyright (C) 2020 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 .
+#
+####################################################################################################
+
+__all__ = ['NGSPICE_SUPPORTED_VERSION']
+
+from .SimulationType import LAST_VERSION as NGSPICE_SUPPORTED_VERSION
diff --git a/third_party/PySpice-org/PySpice/PySpice/Spice/NgSpice/api.h b/third_party/PySpice-org/PySpice/PySpice/Spice/NgSpice/api.h
new file mode 100644
index 00000000..4a02e3af
--- /dev/null
+++ b/third_party/PySpice-org/PySpice/PySpice/Spice/NgSpice/api.h
@@ -0,0 +1,76 @@
+/* Simplified Ngspice API for CFFI parser */
+
+typedef struct ngcomplex
+{
+ double cx_real;
+ double cx_imag;
+} ngcomplex_t;
+
+typedef struct vector_info
+{
+ char *v_name;
+ int v_type;
+ short v_flags;
+ double *v_realdata;
+ ngcomplex_t *v_compdata;
+ int v_length;
+} vector_info, *pvector_info;
+
+typedef struct vecvalues
+{
+ char *name;
+ double creal;
+ double cimag;
+ bool is_scale;
+ bool is_complex;
+} vecvalues, *pvecvalues;
+
+typedef struct vecvaluesall
+{
+ int veccount;
+ int vecindex;
+ pvecvalues *vecsa;
+} vecvaluesall, *pvecvaluesall;
+
+typedef struct vecinfo
+{
+ int number;
+ char *vecname;
+ bool is_real;
+ void *pdvec;
+ void *pdvecscale;
+} vecinfo, *pvecinfo;
+
+typedef struct vecinfoall
+{
+ char *name;
+ char *title;
+ char *date;
+ char *type;
+ int veccount;
+ pvecinfo *vecs;
+} vecinfoall, *pvecinfoall;
+
+typedef int (SendChar) (char *, int, void *);
+typedef int (SendStat) (char *, int, void *);
+typedef int (ControlledExit) (int, bool, bool, int, void *);
+typedef int (SendData) (pvecvaluesall, int, int, void *);
+typedef int (SendInitData) (pvecinfoall, int, void *);
+typedef int (BGThreadRunning) (bool, int, void *);
+typedef int (GetVSRCData) (double *, double, char *, int, void *);
+typedef int (GetISRCData) (double *, double, char *, int, void *);
+typedef int (GetSyncData) (double, double *, double, int, int, int, void *);
+
+int ngSpice_Init (SendChar *, SendStat *, ControlledExit *, SendData *, SendInitData *, BGThreadRunning *, void *);
+int ngSpice_Init_Sync (GetVSRCData *, GetISRCData *, GetSyncData *, int *, void *);
+
+int ngSpice_Command (char *);
+pvector_info ngGet_Vec_Info (char *);
+int ngSpice_Circ (char **);
+char *ngSpice_CurPlot (void);
+char **ngSpice_AllPlots (void);
+char **ngSpice_AllVecs (char *);
+bool ngSpice_running (void);
+bool ngSpice_SetBkpt (double);
+
+/* End */
diff --git a/third_party/PySpice-org/PySpice/PySpice/Spice/Parser.py b/third_party/PySpice-org/PySpice/PySpice/Spice/Parser.py
new file mode 100644
index 00000000..3fcfda71
--- /dev/null
+++ b/third_party/PySpice-org/PySpice/PySpice/Spice/Parser.py
@@ -0,0 +1,1057 @@
+####################################################################################################
+#
+# 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 .
+#
+####################################################################################################
+
+####################################################################################################
+
+"""This module implements a partial SPICE netlist parser.
+
+See the :command:`cir2py` tool for an example of usage of the parser.
+
+It would be difficult to implement a full parser for Ngspice since the syntax is mainly contextual.
+
+"""
+
+####################################################################################################
+
+import logging
+import os
+
+####################################################################################################
+
+from .BasicElement import SubCircuitElement
+from .ElementParameter import FlagParameter
+from .Netlist import ElementParameterMetaClass, Circuit, SubCircuit
+
+####################################################################################################
+
+_module_logger = logging.getLogger(__name__)
+
+####################################################################################################
+
+class ParseError(NameError):
+ pass
+
+####################################################################################################
+
+class PrefixData:
+
+ """This class represents a device prefix."""
+
+ ##############################################
+
+ def __init__(self, prefix, classes):
+
+ self.prefix = prefix
+ self.classes = classes
+
+ number_of_positionals_min = 1000
+ number_of_positionals_max = 0
+ has_optionals = False
+ for element_class in classes:
+ number_of_positionals = element_class.number_of_positional_parameters
+ number_of_positionals_min = min(number_of_positionals_min, number_of_positionals)
+ number_of_positionals_max = max(number_of_positionals_max, number_of_positionals)
+ has_optionals = max(has_optionals, bool(element_class.optional_parameters))
+
+ self.number_of_positionals_min = number_of_positionals_min
+ self.number_of_positionals_max = number_of_positionals_max
+ self.has_optionals = has_optionals
+
+ self.multi_devices = len(classes) > 1
+ self.has_variable_number_of_pins = prefix in ('Q', 'X') # NPinElement, Q has 3 to 4 pins
+ if self.has_variable_number_of_pins:
+ self.number_of_pins = None
+ else:
+ # Q and X are single
+ self.number_of_pins = classes[0].number_of_pins
+
+ self.has_flag = False
+ for element_class in classes:
+ for parameter in element_class.optional_parameters.values():
+ if isinstance(parameter, FlagParameter):
+ self.has_flag = True
+
+ ##############################################
+
+ def __len__(self):
+ return len(self.classes)
+
+ ##############################################
+
+ def __iter__(self):
+ return iter(self.classes)
+
+ ##############################################
+
+ @property
+ def single(self):
+ if not self.multi_devices:
+ return self.classes[0]
+ else:
+ raise NameError()
+
+####################################################################################################
+
+_prefix_cache = {}
+for prefix, classes in ElementParameterMetaClass._classes.items():
+ prefix_data = PrefixData(prefix, classes)
+ _prefix_cache[prefix] = prefix_data
+ _prefix_cache[prefix.lower()] = prefix_data
+
+# for prefix_data in sorted(_prefix_cache.values(), key=lambda x: len(x)):
+# print(prefix_data.prefix,
+# len(prefix_data),
+# prefix_data.number_of_positionals_min, prefix_data.number_of_positionals_max,
+# prefix_data.has_optionals)
+
+# Single:
+# B 0 True
+# D 1 True
+# F 2 False
+# G 1 False
+# H 2 False
+# I 1 False
+# J 1 True
+# K 3 False
+# M 1 True
+# S 2 False
+# V 1 False
+# W 3 False
+# Z 1 True
+
+# Two:
+# E 0 1 False
+# L 1 2 True
+
+# Three:
+# C 1 2 True
+# R 1 2 True
+
+# NPinElement:
+# Q 1 1 True
+# X 1 1 False
+
+####################################################################################################
+
+class Statement:
+
+ """ This class implements a statement, in fact a line in a Spice netlist. """
+
+ ##############################################
+
+ def __init__(self, line, statement=None):
+
+ self._line = line
+
+ if statement is not None:
+ self._line.lower_case_statement(statement)
+
+ ##############################################
+
+ def __repr__(self):
+ return '{} {}'.format(self.__class__.__name__, repr(self._line))
+
+ ##############################################
+
+ def value_to_python(self, x):
+
+ if x:
+ if str(x)[0].isdigit():
+ return str(x)
+ else:
+ return "'{}'".format(x)
+ else:
+ return ''
+
+ ##############################################
+
+ def values_to_python(self, values):
+
+ return [self.value_to_python(x) for x in values]
+
+ ##############################################
+
+ def kwargs_to_python(self, kwargs):
+ return ['{}={}'.format(key, self.value_to_python(value))
+ for key, value in kwargs.items()]
+
+ ##############################################
+
+ def join_args(self, args):
+ return ', '.join(args)
+
+####################################################################################################
+
+class Comment(Statement):
+ pass
+
+####################################################################################################
+
+class Title(Statement):
+
+ """ This class implements a title definition. """
+
+ ##############################################
+
+ def __init__(self, line):
+
+ super().__init__(line, statement='title')
+ self._title = self._line.right_of('.title')
+
+ ##############################################
+
+ def __str__(self):
+ return self._title
+
+ ##############################################
+
+ def __repr__(self):
+ return 'Title {}'.format(self._title)
+
+####################################################################################################
+
+class Lib(Statement):
+
+ """ This class implements a library definition. """
+
+ ##############################################
+
+ def __init__(self, line):
+
+ super().__init__(line, statement='lib')
+ self._lib = self._line.right_of('.lib')
+
+ ##############################################
+
+ def __str__(self):
+ return self._lib
+
+ ##############################################
+
+ def __repr__(self):
+ return 'Lib {}'.format(self._lib)
+
+ ##############################################
+
+ def to_python(self, netlist_name):
+
+ return '{}.lib({})'.format(netlist_name, self._lib) + os.linesep
+
+####################################################################################################
+
+class Include(Statement):
+
+ """ This class implements a include definition. """
+
+ ##############################################
+
+ def __init__(self, line):
+
+ super().__init__(line, statement='include')
+ self._include = self._line.right_of('.include').strip('"')
+
+ ##############################################
+
+ def __str__(self):
+ return self._include
+
+ ##############################################
+
+ def __repr__(self):
+ return 'Include {}'.format(self._include)
+
+ ##############################################
+
+ def to_python(self, netlist_name):
+
+ return '{}.include({})'.format(netlist_name, self._include) + os.linesep
+
+####################################################################################################
+
+class Model(Statement):
+
+ """ This class implements a model definition.
+
+ Spice syntax::
+
+ .model mname type (pname1=pval1 pname2=pval2)
+
+ """
+
+ ##############################################
+
+ def __init__(self, line):
+
+ super().__init__(line, statement='model')
+
+ text = line.right_of('.model').strip()
+ import re
+ mtch = re.match('\s*([^ \t]+)\s*([^ \t(]+)(.*)', text)
+ self._name = mtch[1]
+ self._model_type = mtch[2]
+ params = mtch[3]
+ params = params.strip('() ')
+ self._parameters = Line.get_kwarg(params)
+
+ ##############################################
+
+ @property
+ def name(self):
+ """ Name of the model """
+ return self._name
+
+ ##############################################
+
+ def __repr__(self):
+ return 'Model {} {} {}'.format(self._name, self._model_type, self._parameters)
+
+ ##############################################
+
+ def to_python(self, netlist_name):
+ args = self.values_to_python((self._name, self._model_type))
+ kwargs = self.kwargs_to_python(self._parameters)
+ return '{}.model({})'.format(netlist_name, self.join_args(args + kwargs)) + os.linesep
+
+ ##############################################
+
+ def build(self, circuit):
+ circuit.model(self._name, self._model_type, **self._parameters)
+
+####################################################################################################
+
+class SubCircuitStatement(Statement):
+
+ """ This class implements a sub-circuit definition.
+
+ Spice syntax::
+
+ .SUBCKT name node1 ... param1=value1 ...
+
+ """
+
+ ##############################################
+
+ def __init__(self, line):
+
+ super().__init__(line, statement='subckt')
+
+ # Fixme
+ parameters, dict_parameters = self._line.split_line('.subckt')
+ self._name, self._nodes = parameters[0], parameters[1:]
+
+ self._statements = []
+
+ ##############################################
+
+ @property
+ def name(self):
+ """ Name of the sub-circuit. """
+ return self._name
+
+ @property
+ def nodes(self):
+ """ Nodes of the sub-circuit. """
+ return self._nodes
+
+ ##############################################
+
+ def __repr__(self):
+ text = 'SubCircuit {} {}'.format(self._name, self._nodes) + os.linesep
+ text += os.linesep.join([' ' + repr(statement) for statement in self._statements])
+ return text
+
+ ##############################################
+
+ def __iter__(self):
+ """ Return an iterator on the statements. """
+ return iter(self._statements)
+
+ ##############################################
+
+ def append(self, statement):
+ """ Append a statement to the statement's list. """
+ self._statements.append(statement)
+
+ ##############################################
+
+ def to_python(self, ground=0):
+
+ subcircuit_name = 'subcircuit_' + self._name
+ args = self.values_to_python([subcircuit_name] + self._nodes)
+ source_code = ''
+ source_code += '{} = SubCircuit({})'.format(subcircuit_name, self.join_args(args)) + os.linesep
+ source_code += SpiceParser.netlist_to_python(subcircuit_name, self, ground)
+ return source_code
+
+ ##############################################
+
+ def build(self, ground=0):
+ subcircuit = SubCircuit(self._name, *self._nodes)
+ SpiceParser._build_circuit(subcircuit, self._statements, ground)
+ return subcircuit
+
+####################################################################################################
+
+class Element(Statement):
+
+ """ This class implements an element definition.
+
+ "{ expression }" are allowed in device line.
+
+ """
+
+ _logger = _module_logger.getChild('Element')
+
+ ##############################################
+
+ def __init__(self, line):
+
+ super().__init__(line)
+
+ line_str = str(line)
+ # self._logger.debug(os.linesep + line_str)
+
+ # Retrieve device prefix
+ self._prefix = line_str[0]
+ prefix_data = _prefix_cache[self._prefix]
+
+ # Retrieve device name
+ start_location = 1
+ stop_location = line_str.find(' ')
+ # Fixme: if stop_location == -1:
+ self._name = line_str[start_location:stop_location]
+
+ self._nodes = []
+ self._parameters = []
+ self._dict_parameters = {}
+
+ # Read nodes
+ if not prefix_data.has_variable_number_of_pins:
+ number_of_pins = prefix_data.number_of_pins
+ if number_of_pins:
+ self._nodes, stop_location = self._line.read_words(stop_location, number_of_pins)
+ else: # Q or X
+ if prefix_data.prefix == 'Q':
+ self._nodes, stop_location = self._line.read_words(stop_location, 3)
+ # Fixme: optional node
+ else: # X
+ args, stop_location = self._line.split_words(stop_location, until='=')
+ self._nodes = args[:-1]
+ self._parameters.append(args[-1]) # model name
+
+ # Read positionals
+ number_of_positionals = prefix_data.number_of_positionals_min
+ if number_of_positionals and stop_location is not None: # model is optional
+ self._parameters, stop_location = self._line.read_words(stop_location, number_of_positionals)
+ if prefix_data.multi_devices and stop_location is not None:
+ remaining, stop_location = self._line.split_words(stop_location, until='=')
+ self._parameters.extend(remaining)
+
+ if prefix_data.prefix in ('V', 'I') and stop_location is not None:
+ # merge remaining
+ self._parameters[-1] += line_str[stop_location:]
+
+ # Read optionals
+ if prefix_data.has_optionals and stop_location is not None:
+ kwargs, stop_location = self._line.split_words(stop_location)
+ for kwarg in kwargs:
+ try:
+ key, value = kwarg.split('=')
+ self._dict_parameters[key.lower()] = value
+ except ValueError:
+ if kwarg in ('off',) and prefix_data.has_flag:
+ self._dict_parameters['off'] = True
+ else:
+ # Fixme: warning -> debug due to spam ...
+ self._logger.debug(line_str)
+ # raise NameError('Bad element line:', line_str)
+
+ if prefix_data.multi_devices:
+ for element_class in prefix_data:
+ if len(self._parameters) == element_class.number_of_positional_parameters:
+ break
+ else:
+ element_class = prefix_data.single
+ self.factory = element_class
+
+ # Move positionals passed as kwarg
+ to_delete = []
+ for parameter in element_class.positional_parameters.values():
+ if parameter.key_parameter:
+ i = parameter.position
+ self._dict_parameters[parameter.attribute_name] = self._parameters[i]
+ to_delete.append(i)
+ for i in to_delete:
+ del self._parameters[i]
+
+ # self._logger.debug(os.linesep + self.__repr__())
+
+ ##############################################
+
+ @property
+ def name(self):
+ """ Name of the element """
+ return self._name
+
+ ##############################################
+
+ def __repr__(self):
+ return 'Element {0._prefix} {0._name} {0._nodes} {0._parameters} {0._dict_parameters}'.format(self)
+
+ ##############################################
+
+ def translate_ground_node(self, ground):
+
+ nodes = []
+ for node in self._nodes:
+ if str(node) == str(ground):
+ node = 0
+ nodes.append(node)
+
+ return nodes
+
+ ##############################################
+
+ def to_python(self, netlist_name, ground=0):
+
+ nodes = self.translate_ground_node(ground)
+ args = [self._name]
+ if self._prefix != 'X':
+ args += nodes + self._parameters
+ else: # != Spice
+ args += self._parameters + nodes
+ args = self.values_to_python(args)
+ kwargs = self.kwargs_to_python(self._dict_parameters)
+ return '{}.{}({})'.format(netlist_name, self._prefix, self.join_args(args + kwargs)) + os.linesep
+
+ ##############################################
+
+ def build(self, circuit, ground=0):
+
+ factory = getattr(circuit, self.factory.ALIAS)
+ nodes = self.translate_ground_node(ground)
+ if self._prefix != 'X':
+ args = nodes + self._parameters
+ else: # != Spice
+ args = self._parameters + nodes
+ kwargs = self._dict_parameters
+ if self._logger.isEnabledFor(logging.DEBUG):
+ message = ' '.join([str(x) for x in (self._prefix, self._name, nodes,
+ self._parameters, self._dict_parameters)])
+ self._logger.debug(message)
+ factory(self._name, *args, **kwargs)
+
+####################################################################################################
+
+class Line:
+
+ """ This class implements a line in the netlist. """
+
+ _logger = _module_logger.getChild('Element')
+
+ ##############################################
+
+ def __init__(self, line, line_range, end_of_line_comment):
+
+ self._end_of_line_comment = end_of_line_comment
+
+ text, comment, self._is_comment = self._split_comment(line)
+
+ self._text = text
+ self._comment = comment
+ self._line_range = line_range
+
+ ##############################################
+
+ def __repr__(self):
+ return '{0._line_range}: {0._text} // {0._comment}'.format(self)
+
+ ##############################################
+
+ def __str__(self):
+ return self._text
+
+ ##############################################
+
+ @property
+ def comment(self):
+ return self._comment
+
+ @property
+ def is_comment(self):
+ return self._is_comment
+
+ ##############################################
+
+ def _split_comment(self, line):
+
+ line = str(line)
+
+ if line.startswith('*'):
+ is_comment = True
+ text = ''
+ comment = line[1:].strip()
+ else:
+ is_comment = False
+ # remove end of line comment
+ location = -1
+ for marker in self._end_of_line_comment:
+ _location = line.find(marker)
+ if _location != -1:
+ if location == -1:
+ location = _location
+ else:
+ location = min(_location, location)
+ if location != -1:
+ text = line[:location].strip()
+ comment = line[location:].strip()
+ else:
+ text = line
+ comment = ''
+
+ return text, comment, is_comment
+
+ ##############################################
+
+ def append(self, line):
+
+ text, comment, is_comment = self._split_comment(line)
+
+ if text:
+ if not self._text.endswith(' ') or text.startswith(' '):
+ self._text += ' '
+ self._text += text
+ if comment:
+ self._comment += ' // ' + comment
+
+ _slice = self._line_range
+ self._line_range = slice(_slice.start, _slice.stop + 1)
+
+ ##############################################
+
+ def lower_case_statement(self, statement):
+
+ """Lower case the statement"""
+
+ # statement without . prefix
+
+ if self._text:
+ lower_statement = statement.lower()
+ _slice = slice(1, len(statement) + 1)
+ _statement = self._text[_slice]
+ if _statement.lower() == lower_statement:
+ self._text = '.' + lower_statement + self._text[_slice.stop:]
+
+ ##############################################
+
+ def right_of(self, text):
+ return self._text[len(text):].strip()
+
+ ##############################################
+
+ def read_words(self, start_location, number_of_words):
+
+ """Read a fixed number of words separated by space."""
+
+ words = []
+ stop_location = None
+
+ line_str = self._text
+ number_of_words_read = 0
+ while number_of_words_read < number_of_words: # and start_location < len(line_str)
+ stop_location = line_str.find(' ', start_location)
+ if stop_location == -1:
+ stop_location = None # read until end
+ word = line_str[start_location:stop_location].strip()
+ if word:
+ number_of_words_read += 1
+ words.append(word)
+ if stop_location is None: # we should stop
+ if number_of_words_read != number_of_words:
+ template = 'Bad element line, looking for word {}/{}:' + os.linesep
+ message = (template.format(number_of_words_read, number_of_words) +
+ line_str + os.linesep +
+ ' '*start_location + '^')
+ self._logger.warning(message)
+ raise ParseError(message)
+ else:
+ if start_location < stop_location:
+ start_location = stop_location
+ else: # we have read a space
+ start_location += 1
+
+ return words, stop_location
+
+ ##############################################
+
+ def split_words(self, start_location, until=None):
+
+ stop_location = None
+
+ line_str = self._text
+ if until is not None:
+ location = line_str.find(until, start_location)
+ if location != -1:
+ stop_location = location
+ location = line_str.rfind(' ', start_location, stop_location)
+ if location != -1:
+ stop_location = location
+ else:
+ raise NameError('Bad element line, missing key? ' + line_str)
+
+ line_str = line_str[start_location:stop_location]
+ words = [x for x in line_str.split(' ') if x]
+
+ return words, stop_location
+
+ ##############################################
+
+ @staticmethod
+ def get_kwarg(text):
+
+ dict_parameters = {}
+
+ parts = []
+ for part in text.split():
+ if '=' in part and part != '=':
+ left, right = [x for x in part.split('=')]
+ parts.append(left)
+ parts.append('=')
+ if right:
+ parts.append(right)
+ else:
+ parts.append(part)
+
+ i = 0
+ i_stop = len(parts)
+ while i < i_stop:
+ if i + 1 < i_stop and parts[i + 1] == '=':
+ key, value = parts[i], parts[i + 2]
+ dict_parameters[key] = value
+ i += 3
+ else:
+ raise ParseError("Bad kwarg: {}".format(text))
+
+ return dict_parameters
+
+ ##############################################
+
+ def split_line(self, keyword):
+
+ """Split the line according to the following pattern::
+
+ keyword parameter1 parameter2 ... key1=value1 key2=value2 ...
+
+ Return the list of parameters and the dictionary.
+
+ """
+
+ # Fixme: cf. get_kwarg
+
+ parameters = []
+ dict_parameters = {}
+
+ text = self.right_of(keyword)
+
+ parts = []
+ for part in text.split():
+ if '=' in part and part != '=':
+ left, right = [x for x in part.split('=')]
+ parts.append(left)
+ parts.append('=')
+ if right:
+ parts.append(right)
+ else:
+ parts.append(part)
+
+ i = 0
+ i_stop = len(parts)
+ while i < i_stop:
+ if i + 1 < i_stop and parts[i + 1] == '=':
+ key, value = parts[i], parts[i + 2]
+ dict_parameters[key] = value
+ i += 3
+ else:
+ parameters.append(parts[i])
+ i += 1
+
+ return parameters, dict_parameters
+
+####################################################################################################
+
+class SpiceParser:
+
+ """ This class parse a Spice netlist file and build a syntax tree.
+
+ Public Attributes:
+
+ :attr:`circuit`
+
+ :attr:`models`
+
+ :attr:`subcircuits`
+
+ :attr:`incl_libs`
+
+ """
+
+ _logger = _module_logger.getChild('SpiceParser')
+
+ ##############################################
+
+ def __init__(self, path=None, source=None, end_of_line_comment=('$', '//', ';'), recurse=False, section=None):
+
+ # Fixme: empty source
+
+ self._path = path # For use by _parse() when recursing through files.
+
+ if path is not None:
+ with open(str(path), 'r') as f:
+ raw_lines = f.readlines()
+ elif source is not None:
+ raw_lines = source.split(os.linesep)
+ else:
+ raise ValueError
+
+ self._end_of_line_comment = end_of_line_comment
+
+ lines = self._merge_lines(raw_lines)
+ self._title = None
+ self._statements = self._parse(lines=lines, recurse=recurse, section=section)
+ self._find_sections()
+
+ ##############################################
+
+ def _merge_lines(self, raw_lines):
+
+ """Merge broken lines and return a new list of lines.
+
+ A line starting with "+" continues the preceding line.
+ """
+
+ lines = []
+ current_line = None
+ for line_index, line_string in enumerate(raw_lines):
+ line_string = line_string.lstrip(' ')
+ if line_string.startswith('+'):
+ current_line.append(line_string[1:].strip('\r\n'))
+ else:
+ line_string = line_string.strip('\r\n')
+ if line_string:
+ _slice = slice(line_index, line_index +1)
+ line = Line(line_string, _slice, self._end_of_line_comment)
+ lines.append(line)
+ # handle case with comment before line continuation
+ if not line_string.startswith('*'):
+ current_line = line
+
+ return lines
+
+ ##############################################
+
+ def _parse(self, lines, recurse=False, section=None):
+
+ """ Parse the lines and return a list of statements. """
+
+ # The first line in the input file must be the title, which is the only comment line that does
+ # not need any special character in the first place.
+ #
+ # The last line must be .end
+
+ if len(lines) <= 1:
+ self._logger.warning('Empty Spice file: {self._path}'.format(**locals()))
+ # raise NameError('Netlist is empty')
+ # if lines[-1] != '.end':
+ # raise NameError('".end" is expected at the end of the netlist')
+
+ title_statement = '.title '
+ self._title = str(lines[0])
+ if self._title.startswith(title_statement):
+ self._title = self._title[len(title_statement):]
+
+ # SUBCKT and MODEL files often start with their commands as the
+ # first line so they'll parse incorrectly if that line is removed.
+ # For everything else, assume the first line is a TITLE line and
+ # remove it.
+ if str(lines[0]).startswith(('.model', '.subckt')):
+ start_index = 0
+ else:
+ start_index = 1
+
+ statements = []
+ skip_lines = [False] # True on top of stack means skip lines.
+ sub_circuit = None
+ scope = statements
+ self.incl_libs = [] # Libraries found during recursive descent into includes.
+ for line in lines[start_index:]:
+ # print('>', repr(line))
+ text = str(line)
+ lower_case_text = text.lower() # !
+ if skip_lines[-1]:
+ if lower_case_text.startswith('.endl'):
+ skip_lines.pop()
+ elif line.is_comment:
+ scope.append(Comment(line))
+ elif lower_case_text.startswith('.'):
+ lower_case_text = lower_case_text[1:]
+ if lower_case_text.startswith('subckt'):
+ sub_circuit = SubCircuitStatement(line)
+ statements.append(sub_circuit)
+ scope = sub_circuit
+ elif lower_case_text.startswith('ends'):
+ sub_circuit = None
+ scope = statements
+ elif lower_case_text.startswith('title'):
+ # override first line
+ self._title = Title(line)
+ scope.append(self._title)
+ elif lower_case_text.startswith('end'):
+ pass
+ elif lower_case_text.startswith('model'):
+ model = Model(line)
+ scope.append(model)
+ elif lower_case_text.startswith('include'):
+ incl = Include(line)
+ scope.append(incl)
+ if recurse:
+ from .Library import SpiceLibrary
+ incl_path = os.path.join(str(self._path.directory_part()), str(incl))
+ self.incl_libs.append(SpiceLibrary(root_path=incl_path, recurse=recurse))
+ elif lower_case_text.startswith('lib'):
+ lib = Lib(line)
+ if section and str(lib) != section.lower():
+ # If the .lib statement is only followed by the name of a section,
+ # then skip any lines in a library section whose name does not match
+ # the library section argument.
+ skip_lines.append(True)
+ else:
+ scope.append(lib)
+ else:
+ # options param ...
+ # .global
+ # .lib filename libname
+ # .param
+ # .func .csparam .temp .if
+ # { expr } are allowed in .model lines and in device lines.
+ # self._logger.warning('Parser ignored: {}'.format(line))
+ pass
+ else:
+ try:
+ element = Element(line)
+ scope.append(element)
+ except ParseError:
+ self._logger.warning('Parse error on:\n{}'.format(line))
+
+ return statements
+
+ ##############################################
+
+ def _find_sections(self):
+
+ """ Look for model, sub-circuit and circuit definitions in the statement list. """
+
+ self.circuit = None
+ self.subcircuits = []
+ self.models = []
+ for statement in self._statements:
+ if isinstance(statement, Title):
+ if self.circuit is None:
+ self.circuit = statement
+ else:
+ raise NameError('More than one title')
+ elif isinstance(statement, SubCircuitStatement):
+ self.subcircuits.append(statement)
+ elif isinstance(statement, Model):
+ self.models.append(statement)
+
+ ##############################################
+
+ def is_only_subcircuit(self):
+ return bool(not self.circuit and self.subcircuits)
+
+ ##############################################
+
+ def is_only_model(self):
+ return bool(not self.circuit and not self.subcircuits and self.models)
+
+ ##############################################
+
+ @staticmethod
+ def _build_circuit(circuit, statements, ground):
+
+ for statement in statements:
+ if isinstance(statement, Include):
+ circuit.include(str(statement))
+
+ for statement in statements:
+ if isinstance(statement, Element):
+ statement.build(circuit, ground)
+ elif isinstance(statement, Model):
+ statement.build(circuit)
+ elif isinstance(statement, SubCircuit):
+ subcircuit = statement.build(ground) # Fixme: ok ???
+ circuit.subcircuit(subcircuit)
+
+ ##############################################
+
+ def build_circuit(self, ground=0):
+
+ """Build a :class:`Circuit` instance.
+
+ Use the *ground* parameter to specify the node which must be translated to 0 (SPICE ground node).
+
+ """
+
+ circuit = Circuit(str(self._title))
+ self._build_circuit(circuit, self._statements, ground)
+ return circuit
+
+ ##############################################
+
+ @staticmethod
+ def netlist_to_python(netlist_name, statements, ground=0):
+
+ source_code = ''
+ for statement in statements:
+ if isinstance(statement, Element):
+ source_code += statement.to_python(netlist_name, ground)
+ elif isinstance(statement, Include):
+ pass
+ elif isinstance(statement, Model):
+ source_code += statement.to_python(netlist_name)
+ elif isinstance(statement, SubCircuitStatement):
+ source_code += statement.to_python(netlist_name)
+ elif isinstance(statement, Include):
+ source_code += statement.to_python(netlist_name)
+ return source_code
+
+ ##############################################
+
+ def to_python_code(self, ground=0):
+
+ ground = str(ground)
+
+ source_code = ''
+
+ if self.circuit:
+ source_code += "circuit = Circuit('{}')".format(self._title) + os.linesep
+ source_code += self.netlist_to_python('circuit', self._statements, ground)
+
+ return source_code
diff --git a/third_party/PySpice-org/PySpice/PySpice/Spice/Parser_jmgc.py b/third_party/PySpice-org/PySpice/PySpice/Spice/Parser_jmgc.py
new file mode 100644
index 00000000..c28e04d7
--- /dev/null
+++ b/third_party/PySpice-org/PySpice/PySpice/Spice/Parser_jmgc.py
@@ -0,0 +1,1486 @@
+####################################################################################################
+#
+# PySpice - A Spice Package for Python
+# Copyright (C) 2020 jmgc / Fabrice Salvaire
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+####################################################################################################
+
+####################################################################################################
+
+"""This module implements a partial SPICE netlist parser.
+
+See the :command:`cir2py` tool for an example of usage of the parser.
+
+It would be difficult to implement a full parser for Ngspice since the syntax is mainly contextual.
+
+SPICE is case insensitive.
+
+"""
+
+####################################################################################################
+
+from collections import OrderedDict
+import logging
+import os
+import regex
+
+####################################################################################################
+
+from .ElementParameter import FlagParameter
+from .Netlist import ElementParameterMetaClass, Circuit, SubCircuit
+
+####################################################################################################
+
+_module_logger = logging.getLogger(__name__)
+
+####################################################################################################
+
+class ParseError(NameError):
+ pass
+
+####################################################################################################
+
+class PrefixData:
+
+ """This class represents a device prefix."""
+
+ ##############################################
+
+ def __init__(self, prefix, classes):
+
+ self.prefix = prefix
+ self.classes = classes
+
+ number_of_positionals_min = 1000
+ number_of_positionals_max = 0
+ has_optionals = False
+ for element_class in classes:
+ number_of_positionals = element_class.number_of_positional_parameters
+ number_of_positionals_min = min(number_of_positionals_min, number_of_positionals)
+ number_of_positionals_max = max(number_of_positionals_max, number_of_positionals)
+ has_optionals = max(has_optionals, bool(element_class.optional_parameters))
+
+ self.number_of_positionals_min = number_of_positionals_min
+ self.number_of_positionals_max = number_of_positionals_max
+ self.has_optionals = has_optionals
+
+ self.multi_devices = len(classes) > 1
+ self.has_variable_number_of_pins = prefix in ('Q', 'X') # NPinElement, Q has 3 to 4 pins
+ if self.has_variable_number_of_pins:
+ self.number_of_pins = None
+ else:
+ # Q and X are single
+ self.number_of_pins = classes[0].number_of_pins
+
+ self.has_flag = False
+ for element_class in classes:
+ for parameter in element_class.optional_parameters.values():
+ if isinstance(parameter, FlagParameter):
+ self.has_flag = True
+
+ ##############################################
+
+ def __len__(self):
+ return len(self.classes)
+
+ ##############################################
+
+ def __iter__(self):
+ return iter(self.classes)
+
+ ##############################################
+
+ @property
+ def single(self):
+ if not self.multi_devices:
+ return self.classes[0]
+ else:
+ raise NameError()
+
+####################################################################################################
+
+_prefix_cache = {}
+for prefix, classes in ElementParameterMetaClass._classes.items():
+ prefix_data = PrefixData(prefix, classes)
+ _prefix_cache[prefix] = prefix_data
+ _prefix_cache[prefix.lower()] = prefix_data
+
+# for prefix_data in sorted(_prefix_cache.values(), key=lambda x: len(x)):
+# print(prefix_data.prefix,
+# len(prefix_data),
+# prefix_data.number_of_positionals_min, prefix_data.number_of_positionals_max,
+# prefix_data.has_optionals)
+
+# Single:
+# B 0 True
+# D 1 True
+# F 2 False
+# G 1 False
+# H 2 False
+# I 1 False
+# J 1 True
+# K 3 False
+# M 1 True
+# S 2 False
+# V 1 False
+# W 3 False
+# Z 1 True
+
+# Two:
+# E 0 1 False
+# L 1 2 True
+
+# Three:
+# C 1 2 True
+# R 1 2 True
+
+# NPinElement:
+# Q 1 1 True
+# X 1 1 False
+
+####################################################################################################
+
+class Statement:
+
+ """This base class implements a statement, in fact a line in a Spice netlist."""
+
+ ##############################################
+
+ def __init__(self, line, statement=None):
+ self._line = line
+ if statement is not None:
+ self._line.lower_case_statement(statement)
+
+ ##############################################
+
+ def __repr__(self):
+ return '{} {}'.format(self.__class__.__name__, repr(self._line))
+
+ ##############################################
+
+ def value_to_python(self, x):
+ if x:
+ if str(x)[0].isdigit():
+ return str(x)
+ else:
+ return "'{}'".format(x)
+ else:
+ return ''
+
+ ##############################################
+
+ def values_to_python(self, values):
+ return [self.value_to_python(x) for x in values]
+
+ ##############################################
+
+ def kwargs_to_python(self, kwargs):
+ return ['{}={}'.format(key, self.value_to_python(value))
+ for key, value in kwargs.items()]
+
+ ##############################################
+
+ def join_args(self, args):
+ return ', '.join(args)
+
+####################################################################################################
+
+class Comment(Statement):
+ pass
+
+####################################################################################################
+
+class Title(Statement):
+
+ """This class implements a title definition."""
+
+ ##############################################
+
+ def __init__(self, line):
+ super().__init__(line, statement='title')
+ self._title = self._line.right_of('.title')
+
+ ##############################################
+
+ def __str__(self):
+ return self._title
+
+ ##############################################
+
+ def __repr__(self):
+ return 'Title {}'.format(self._title)
+
+####################################################################################################
+
+class Include(Statement):
+
+ """This class implements a include definition."""
+
+ ##############################################
+
+ def __init__(self, line):
+ super().__init__(line, statement='include')
+ self._include = self._line.right_of('.include')
+
+ ##############################################
+
+ def __str__(self):
+ return self._include
+
+ ##############################################
+
+ def __repr__(self):
+ return 'Include {}'.format(self._include)
+
+ ##############################################
+
+ def to_python(self, netlist_name):
+ return '{}.include({})'.format(netlist_name, self._include) + os.linesep
+
+####################################################################################################
+
+class Model(Statement):
+
+ """This class implements a model definition.
+
+ Spice syntax::
+
+ .model mname type(pname1=pval1 pname2=pval2 ... )
+
+ """
+
+ ##############################################
+
+ def __init__(self, line):
+ super().__init__(line, statement='model')
+
+ base, self._parameters = line.split_keyword('.model')
+ self._name, self._model_type = base
+ self._name = self._name.lower()
+
+ ##############################################
+
+ @property
+ def name(self):
+ """Name of the model"""
+ return self._name
+
+ ##############################################
+
+ def __repr__(self):
+ return 'Model {} {} {}'.format(self._name, self._model_type, self._parameters)
+
+ ##############################################
+
+ def to_python(self, netlist_name):
+ args = self.values_to_python((self._name, self._model_type))
+ kwargs = self.kwargs_to_python(self._parameters)
+ return '{}.model({})'.format(netlist_name, self.join_args(args + kwargs)) + os.linesep
+
+ ##############################################
+
+ def build(self, circuit):
+ return circuit.model(self._name, self._model_type, **self._parameters)
+
+####################################################################################################
+
+class Parameter(Statement):
+
+ """This class implements a parameter definition.
+
+ Spice syntax::
+
+ .param name=expr
+
+ """
+
+ ##############################################
+
+ def __init__(self, line):
+ super().__init__(line, statement='param')
+
+ text = line.right_of('.param').strip().lower() # Fixme: lower ???
+ idx = text.find('=')
+ self._name = text[:idx].strip()
+ self._value = text[idx + 1:].strip()
+
+ ##############################################
+
+ @property
+ def name(self):
+ """Name of the model"""
+ return self._name
+
+ ##############################################
+
+ def __repr__(self):
+ return 'Param {}={}'.format(self._name, self._value)
+
+ ##############################################
+
+ def to_python(self, netlist_name):
+ args = self.values_to_python((self._name, self._value))
+ # Fixme: linesep here ???
+ return '{}.param({})'.format(netlist_name, self.join_args(args)) + os.linesep
+
+ ##############################################
+
+ def build(self, circuit):
+ circuit.parameter(self._name, self._value)
+
+####################################################################################################
+
+# Review: HERE
+
+class CircuitStatement(Statement):
+
+ # Review: jmgc
+
+ """This class implements a circuit definition.
+
+ Spice syntax::
+
+ Title ...
+
+ """
+
+ ##############################################
+
+ def __init__(self, title):
+
+ super().__init__(title, statement='title')
+
+ # Review: Title
+ title_statement = '.title '
+ self._title = str(title)
+ if self._title.startswith(title_statement):
+ self._title = self._title[len(title_statement):]
+
+ self._statements = []
+ self._subcircuits = []
+ self._models = []
+ self._required_subcircuits = set()
+ self._required_models = set()
+ self._params = []
+
+ ##############################################
+
+ @property
+ def title(self):
+ """Title of the circuit."""
+ return self._title
+
+ @property
+ def name(self):
+ """Name of the circuit."""
+ return self._title
+
+ @property
+ def models(self):
+ """Models of the circuit."""
+ return self._models
+
+ @property
+ def subcircuits(self):
+ """Subcircuits of the circuit."""
+ return self._subcircuits
+
+ @property
+ def params(self):
+ """Parameters of the circuit."""
+ return self._params
+
+ ##############################################
+
+ def __repr__(self):
+ text = 'Circuit {}'.format(self._title) + os.linesep
+ text += os.linesep.join([repr(model) for model in self._models]) + os.linesep
+ text += os.linesep.join([repr(subcircuit) for subcircuit in self._subcircuits]) + os.linesep
+ text += os.linesep.join([' ' + repr(statement) for statement in self._statements])
+ return text
+
+ ##############################################
+
+ def __iter__(self):
+ """Return an iterator on the statements."""
+ return iter(self._models + self._subcircuits + self._statements)
+
+ ##############################################
+
+ def append(self, statement):
+ """Append a statement to the statement's list."""
+ self._statements.append(statement)
+
+ ##############################################
+
+ def append_model(self, statement):
+ """Append a model to the statement's list."""
+ self._models.append(statement)
+
+ ##############################################
+
+ def append_param(self, statement):
+ """Append a param to the statement's list."""
+ self._params.append(statement)
+
+ ##############################################
+
+ def append_subcircuit(self, statement):
+ """Append a subcircuit to the statement's list."""
+ self._subcircuits.append(statement)
+
+ ##############################################
+
+ def to_python(self, ground=0):
+ subcircuit_name = 'subcircuit_' + self._name
+ args = self.values_to_python([subcircuit_name] + self._nodes)
+ source_code = ''
+ source_code += '{} = SubCircuit({})'.format(subcircuit_name, self.join_args(args)) + os.linesep
+ source_code += SpiceParser.netlist_to_python(subcircuit_name, self, ground)
+ return source_code
+
+ ##############################################
+
+ def build(self, ground=0):
+ circuit = Circuit(self._title)
+ for statement in self._params:
+ statement.build(circuit)
+ for statement in self._models:
+ model = statement.build(circuit)
+ for statement in self._subcircuits:
+ subckt = statement.build(ground) # Fixme: ok ???
+ circuit.subcircuit(subckt)
+ for statement in self._statements:
+ if isinstance(statement, Element):
+ statement.build(circuit, ground)
+ return circuit
+
+####################################################################################################
+
+class SubCircuitStatement(Statement):
+
+ """This class implements a sub-circuit definition.
+
+ Spice syntax::
+
+ .SUBCKT name node1 ... param1=value1 ...
+
+ """
+
+ ##############################################
+
+ def __init__(self, line):
+
+ super().__init__(line, statement='subckt')
+
+ # Fixme
+ parameters, dict_parameters = self._line.split_keyword('.subckt')
+ # Review: syntax ???
+ if parameters[-1].lower() == 'params:':
+ parameters = parameters[:-1]
+ self._name, self._nodes = parameters[0], parameters[1:]
+ self._name = self._name.lower()
+ self._parameters = dict_parameters
+
+ self._statements = []
+ self._subcircuits = []
+ self._models = []
+ self._required_subcircuits = set()
+ self._required_models = set()
+ self._params = []
+
+ ##############################################
+
+ @property
+ def name(self):
+ """Name of the sub-circuit."""
+ return self._name
+
+ @property
+ def nodes(self):
+ """Nodes of the sub-circuit."""
+ return self._nodes
+
+ @property
+ def models(self):
+ """Models of the sub-circuit."""
+ return self._models
+
+ @property
+ def params(self):
+ """Params of the sub-circuit."""
+ return self._params
+
+ @property
+ def subcircuits(self):
+ """Subcircuits of the sub-circuit."""
+ return self._subcircuits
+
+ ##############################################
+
+ def __repr__(self):
+ if self._parameters:
+ text = 'SubCircuit {} {} Parameters: {}'.format(self._name, self._nodes, self._parameters) + os.linesep
+ else:
+ text = 'SubCircuit {} {}'.format(self._name, self._nodes) + os.linesep
+ text += os.linesep.join([repr(model) for model in self._models]) + os.linesep
+ text += os.linesep.join([repr(subcircuit) for subcircuit in self._subcircuits]) + os.linesep
+ text += os.linesep.join([' ' + repr(statement) for statement in self._statements])
+ return text
+
+ ##############################################
+
+ def __iter__(self):
+ """Return an iterator on the statements."""
+ return iter(self._models + self._subcircuits + self._statements)
+
+ ##############################################
+
+ def append(self, statement):
+ """Append a statement to the statement's list."""
+ self._statements.append(statement)
+
+ ##############################################
+
+ def append_model(self, statement):
+ """Append a model to the statement's list."""
+ self._models.append(statement)
+
+ ##############################################
+
+ def append_param(self, statement):
+ """Append a param to the statement's list."""
+ self._params.append(statement)
+
+ ##############################################
+
+ def append_subcircuit(self, statement):
+ """Append a model to the statement's list."""
+ self._subcircuits.append(statement)
+
+ ##############################################
+
+ def to_python(self, ground=0):
+ subcircuit_name = 'subcircuit_' + self._name
+ args = self.values_to_python([subcircuit_name] + self._nodes)
+ source_code = ''
+ source_code += '{} = SubCircuit({})'.format(subcircuit_name, self.join_args(args)) + os.linesep
+ source_code += SpiceParser.netlist_to_python(subcircuit_name, self, ground)
+ return source_code
+
+ ##############################################
+
+ def build(self, ground=0, parent=None):
+ subcircuit = SubCircuit(self._name, *self._nodes, **self._parameters)
+ subcircuit.parent = parent
+ for statement in self._params:
+ statement.build(subcircuit)
+ for statement in self._models:
+ model = statement.build(subcircuit)
+ for statement in self._subcircuits:
+ subckt = statement.build(ground, parent=subcircuit) # Fixme: ok ???
+ subcircuit.subcircuit(subckt)
+ for statement in self._statements:
+ if isinstance(statement, Element):
+ statement.build(subcircuit, ground)
+ return subcircuit
+
+####################################################################################################
+
+class Element(Statement):
+
+ """This class implements an element definition.
+
+ "{ expression }" are allowed in device line.
+
+ """
+
+ _logger = _module_logger.getChild('Element')
+
+ ##############################################
+
+ def __init__(self, line):
+
+ super().__init__(line)
+
+ line_str = str(line)
+ # self._logger.debug(os.linesep + line_str)
+
+ # Retrieve device prefix
+ prefix = line_str[0]
+ if prefix.isalpha():
+ self._prefix = prefix
+ else:
+ raise ParseError("Not an element prefix: " + prefix)
+ prefix_data = _prefix_cache[self._prefix]
+
+ # Retrieve device name
+ args, kwargs = line.split_element(prefix)
+ self._name = args.pop(0)
+
+ self._nodes = []
+ self._parameters = []
+ self._dict_parameters = {}
+
+ # Read nodes
+ if not prefix_data.has_variable_number_of_pins:
+ number_of_pins = prefix_data.number_of_pins
+ if number_of_pins:
+ self._nodes = args[:number_of_pins]
+ args = args[number_of_pins:]
+ else: # Q or X
+ if prefix_data.prefix == 'Q':
+ self._nodes = args[:3]
+ args = args[3:]
+ # Fixme: optional node
+ else: # X
+ if args[-1].lower() == 'params:':
+ args.pop()
+ self._parameters.append(args.pop())
+ self._nodes = args
+ args = []
+
+ # Read positionals
+ number_of_positionals = prefix_data.number_of_positionals_min
+ if number_of_positionals and (len(args) > 0) and (prefix_data.prefix != 'X'): # model is optional
+ self._parameters = args[:number_of_positionals]
+ args = args[number_of_positionals:]
+ if prefix_data.multi_devices and (len(args) > 0):
+ remaining = args
+ args = []
+ self._parameters.extend(remaining)
+
+ if prefix_data.prefix in ('V', 'I') and (len(args) > 0):
+ # merge remaining
+ self._parameters[-1] += " " + " ".join(args)
+ self._dict_parameters = kwargs
+
+ # Read optionals
+ if (prefix_data.has_optionals or (prefix_data.prefix == 'X')) and (len(kwargs) > 0):
+ for key in kwargs:
+ self._dict_parameters[key] = kwargs[key]
+
+ if prefix_data.multi_devices:
+ for element_class in prefix_data:
+ if len(self._parameters) == element_class.number_of_positional_parameters:
+ break
+ else:
+ element_class = prefix_data.single
+ self.factory = element_class
+
+ # Move positionals passed as kwarg
+ to_delete = []
+ for parameter in element_class.positional_parameters.values():
+ if parameter.key_parameter:
+ idx = parameter.position
+ if idx < len(self._parameters):
+ self._dict_parameters[parameter.attribute_name] = self._parameters[idx]
+ to_delete.append(idx - len(to_delete))
+ for idx in to_delete:
+ self._parameters.pop(idx)
+
+ # self._logger.debug(os.linesep + self.__repr__())
+
+ ##############################################
+
+ @property
+ def name(self):
+ """Name of the element"""
+ return self._name
+
+ ##############################################
+
+ def __repr__(self):
+ return 'Element {0._prefix} {0._name} {0._nodes} {0._parameters} {0._dict_parameters}'.format(self)
+
+ ##############################################
+
+ def translate_ground_node(self, ground):
+ nodes = []
+ for node in self._nodes:
+ if str(node) == str(ground):
+ node = 0
+ nodes.append(node)
+ return nodes
+
+ ##############################################
+
+ def to_python(self, netlist_name, ground=0):
+
+ nodes = self.translate_ground_node(ground)
+ args = [self._name]
+ if self._prefix != 'X':
+ args += nodes + self._parameters
+ else: # != Spice
+ args += self._parameters + nodes
+ args = self.values_to_python(args)
+ kwargs = self.kwargs_to_python(self._dict_parameters)
+ return '{}.{}({})'.format(netlist_name, self._prefix, self.join_args(args + kwargs)) + os.linesep
+
+ ##############################################
+
+ def _check_params(self, elements=1):
+ params = []
+ for param in self._parameters:
+ values = param.replace(',', ' ')
+ if values[0] == '(' and values[-1] == ')':
+ values = values[1: -1].split()
+ if len(values) > elements:
+ raise IndexError('Incorrect number of elements for (%r): %s' % (self, param))
+ params.extend(values)
+ else:
+ params.extend(values.split())
+ self._parameters = params
+
+ ##############################################
+
+ def _voltage_controlled_nodes(self, poly_arg):
+ result = ['v(%s,%s)' % nodes
+ for nodes in zip(self._parameters[:(2 * poly_arg):2],
+ self._parameters[1:(2 * poly_arg):2])]
+ result += self._parameters[2 * poly_arg:]
+ return ' '.join(result)
+
+ ##############################################
+
+ def _current_controlled_nodes(self, poly_arg):
+ result = ['i(%s)' % node
+ for node in self._parameters[:poly_arg]]
+ result += self._parameters[poly_arg:]
+ return ' '.join(result)
+
+ ##############################################
+
+ def _manage_controlled_sources(self, nodes):
+ try:
+ idx = self._nodes.index('POLY')
+ if idx == 2:
+ poly_arg = self._nodes[3]
+ if poly_arg[0] == '(' and poly_arg[-1] == ')':
+ poly_arg = poly_arg[1:-1]
+ try:
+ poly_arg = int(poly_arg)
+ except TypeError as te:
+ raise TypeError('Not valid poly argument: %s' % poly_arg, te)
+ self._nodes = self._nodes[:2]
+ nodes = nodes[:2]
+ if self._prefix in 'EG':
+ self._check_params(2)
+ values = self._voltage_controlled_nodes(poly_arg)
+ if self._prefix == 'E':
+ key = 'v'
+ else:
+ key = 'i'
+ else:
+ self._check_params(1)
+ values = self._current_controlled_nodes(poly_arg)
+ if self._prefix == 'F':
+ key = 'v'
+ else:
+ key = 'i'
+ poly_str = '{ POLY (%d) %s }' % (poly_arg, values)
+
+ self._dict_parameters[key] = poly_str
+ self._parameters.clear()
+ self._name = self._prefix + self._name
+ self._prefix = 'B'
+ prefix_data = _prefix_cache[self._prefix]
+ self.factory = prefix_data.single
+ return nodes
+ raise IndexError('Incorrect position of POLY: %r' % self)
+ except ValueError:
+ pass
+ _correction = []
+ correction = []
+ for _node, node in zip(self._nodes, nodes):
+ _values = _node.replace(',', ' ')
+ try:
+ values = node.replace(',', ' ')
+ except AttributeError:
+ values = str(node)
+ if _values[0] == '(' and _values[-1] == ')':
+ _values = _values[1: -1]
+ if values[0] == '(' and values[-1] == ')':
+ values = values[1: -1]
+ _correction.extend(_values.split())
+ correction.extend(values.split())
+ self._parameters = correction[len(self._nodes):] + self._parameters
+ self._nodes = _correction[:len(self._nodes)]
+ parameters = self._parameters
+ correction = correction[:len(self._nodes)]
+ if self._prefix in 'EG':
+ if len(correction) + len(parameters) == 5:
+ parameters = correction[2:] + parameters
+ self._nodes = _correction[:2]
+ value = '{v(%s, %s) * %s}' % tuple(parameters)
+ if self._prefix == 'E':
+ key = 'v'
+ else:
+ key = 'i'
+ self._dict_parameters[key] = value
+ self._parameters.clear()
+ self._name = self._prefix + self._name
+ self._prefix = 'B'
+ prefix_data = _prefix_cache[self._prefix]
+ self.factory = prefix_data.single
+ else:
+ if len(correction) + len(parameters) == 4:
+ parameters = correction[2:] + parameters
+ self._nodes = _correction[:2]
+ value = '{i(%s) * %s}' % tuple(parameters)
+ if self._prefix == 'F':
+ key = 'v'
+ else:
+ key = 'i'
+ self._dict_parameters[key] = value
+ self._parameters.clear()
+ self._name = self._prefix + self._name
+ self._prefix = 'B'
+ prefix_data = _prefix_cache[self._prefix]
+ self.factory = prefix_data.single
+ return correction[:len(self._nodes)]
+
+ ##############################################
+
+ def build(self, circuit, ground=0):
+
+ nodes = self.translate_ground_node(ground)
+ if self._prefix != 'X':
+ if self._prefix in ('EFGH'):
+ nodes = self._manage_controlled_sources(nodes)
+ args = nodes + self._parameters
+ else: # != Spice
+ args = self._parameters + nodes
+ factory = getattr(circuit, self.factory.__alias__)
+ kwargs = self._dict_parameters
+ message = ' '.join([str(x) for x in (self._prefix, self._name, args,
+ self._dict_parameters)])
+ self._logger.debug(message)
+ return factory(self._name, *args, **kwargs)
+
+
+####################################################################################################
+
+class Line:
+
+ """This class implements a line in the netlist."""
+
+ _logger = _module_logger.getChild('Line')
+
+ ##############################################
+
+ def __init__(self, line, line_range, end_of_line_comment):
+
+ self._end_of_line_comment = end_of_line_comment
+
+ text, comment, self._is_comment = self._split_comment(line)
+
+ self._text = text
+ self._comment = comment
+ self._line_range = line_range
+
+ ##############################################
+
+ def __repr__(self):
+ return '{0._line_range}: {0._text} // {0._comment}'.format(self)
+
+ ##############################################
+
+ def __str__(self):
+ return self._text
+
+ ##############################################
+
+ @property
+ def comment(self):
+ return self._comment
+
+ @property
+ def is_comment(self):
+ return self._is_comment
+
+ ##############################################
+
+ def _split_comment(self, line):
+
+ line = str(line)
+
+ if line.startswith('*'):
+ is_comment = True
+ text = ''
+ comment = line[1:].strip()
+ else:
+ is_comment = False
+ # remove end of line comment
+ location = -1
+ for marker in self._end_of_line_comment:
+ _location = line.find(marker)
+ if _location != -1:
+ if location == -1:
+ location = _location
+ else:
+ location = min(_location, location)
+ if location != -1:
+ text = line[:location].strip()
+ comment = line[location:].strip()
+ else:
+ text = line
+ comment = ''
+
+ return text, comment, is_comment
+
+ ##############################################
+
+ def append(self, line):
+
+ text, comment, is_comment = self._split_comment(line)
+
+ if text:
+ if not self._text.endswith(' ') or text.startswith(' '):
+ self._text += ' '
+ self._text += text
+ if comment:
+ self._comment += ' // ' + comment
+
+ _slice = self._line_range
+ self._line_range = slice(_slice.start, _slice.stop + 1)
+
+ ##############################################
+
+ def lower_case_statement(self, statement):
+
+ """Lower case the statement"""
+
+ # statement without . prefix
+
+ if self._text:
+ lower_statement = statement.lower()
+ _slice = slice(1, len(statement) + 1)
+ _statement = self._text[_slice]
+ if _statement.lower() == lower_statement:
+ self._text = '.' + lower_statement + self._text[_slice.stop:]
+
+ ##############################################
+
+ def right_of(self, text):
+ return self._text[len(text):].strip()
+
+ ##############################################
+
+ def read_words(self, start_location, number_of_words):
+
+ """Read a fixed number of words separated by space."""
+
+ words = []
+ stop_location = None
+
+ line_str = self._text
+ number_of_words_read = 0
+ while number_of_words_read < number_of_words: # and start_location < len(line_str)
+ if line_str[start_location] == '{':
+ stop_location = line_str.find('}', start_location)
+ if stop_location > start_location:
+ stop_location += 1
+ else:
+ stop_location = line_str.find(' ', start_location)
+ if stop_location == -1:
+ stop_location = None # read until end
+ word = line_str[start_location:stop_location].strip()
+ if word:
+ number_of_words_read += 1
+ words.append(word)
+ if stop_location is None: # we should stop
+ if number_of_words_read != number_of_words:
+ template = 'Bad element line, looking for word {}/{}:' + os.linesep
+ message = (template.format(number_of_words_read, number_of_words) +
+ line_str + os.linesep +
+ ' ' * start_location + '^')
+ self._logger.warning(message)
+ raise ParseError(message)
+ else:
+ if start_location < stop_location:
+ start_location = stop_location
+ else: # we have read a space
+ start_location += 1
+
+ return words, stop_location
+
+ ##############################################
+
+ def split_words(self, start_location, until=None):
+
+ stop_location = None
+
+ line_str = self._text
+ if until is not None:
+ location = line_str.find(until, start_location)
+ if location != -1:
+ stop_location = location
+ location = line_str.rfind(' ', start_location, stop_location)
+ if location != -1:
+ stop_location = location
+ else:
+ raise NameError('Bad element line, missing key? ' + line_str)
+
+ line_str = line_str[start_location:stop_location]
+ words = [x for x in line_str.split(' ') if x]
+ result = []
+ expression = 0
+ begin_idx = 0
+ for idx, word in enumerate(words):
+ if expression == 0:
+ begin_idx = idx
+ expression += word.count('{') - word.count('}')
+ if expression == 0:
+ if begin_idx < idx:
+ result.append(' '.join(words[begin_idx:idx + 1]))
+ else:
+ result.append(word)
+ return result, stop_location
+
+ ##############################################
+
+ @staticmethod
+ def get_kwarg(text):
+
+ dict_parameters = {}
+
+ parts = []
+ for part in text.split():
+ if '=' in part and part != '=':
+ left, right = [x for x in part.split('=')]
+ parts.append(left)
+ parts.append('=')
+ if right:
+ parts.append(right)
+ else:
+ parts.append(part)
+
+ i = 0
+ i_stop = len(parts)
+ while i < i_stop:
+ if i + 1 < i_stop and parts[i + 1] == '=':
+ key, value = parts[i], parts[i + 2]
+ dict_parameters[key] = value
+ i += 3
+ else:
+ raise ParseError("Bad kwarg: {}".format(text))
+
+ return dict_parameters
+
+ ##############################################
+
+ @staticmethod
+ def _partition(text):
+ parts = []
+ values = text.replace(',', ' ')
+ for part in values.split():
+ if '=' in part and part != '=':
+ left, right = [x for x in part.split('=')]
+ parts.append(left)
+ parts.append('=')
+ if right:
+ parts.append(right)
+ else:
+ parts.append(part)
+ return parts
+
+ ##############################################
+
+ @staticmethod
+ def _partition_parentheses(text):
+ p = regex.compile(r'\(([^\(\)]|(?R))*?\)')
+ parts = []
+ previous_start = 0
+ for m in regex.finditer(p, text):
+ parts.extend(Line._partition(text[previous_start:m.start()]))
+ parts.append(m.group())
+ previous_start = m.end()
+ parts.extend(Line._partition(text[previous_start:]))
+ return parts
+
+ ##############################################
+
+ @staticmethod
+ def _partition_braces(text):
+ p = regex.compile(r'\{([^\{\}]|(?R))*?\}')
+ parts = []
+ previous_start = 0
+ for m in regex.finditer(p, text):
+ parts.extend(Line._partition_parentheses(text[previous_start:m.start()]))
+ parts.append(m.group())
+ previous_start = m.end()
+ parts.extend(Line._partition_parentheses(text[previous_start:]))
+ return parts
+
+ ##############################################
+
+ @staticmethod
+ def _check_parameters(parts):
+ parameters = []
+ dict_parameters = {}
+
+ i = 0
+ i_stop = len(parts)
+ while i < i_stop:
+ if i + 1 < i_stop and parts[i + 1] == '=':
+ key, value = parts[i], parts[i + 2]
+ dict_parameters[key] = value
+ i += 3
+ else:
+ parameters.append(parts[i])
+ i += 1
+
+ return parameters, dict_parameters
+
+ ##############################################
+
+ def split_keyword(self, keyword):
+
+ """Split the line according to the following pattern::
+
+ keyword parameter1 parameter2 ( key1=value1 key2=value2 )
+
+ Return the list of parameters and the dictionary.
+ The parenthesis can be omitted.
+
+ """
+
+ text = self.right_of(keyword)
+
+ p = regex.compile(r'\(([^\(\)]|(?R))*?\)')
+ b = regex.compile(r'\{([^\{\}]|(?R))*?\}')
+ parts = []
+
+ mp = regex.search(p, text)
+ mb = regex.search(b, text)
+ if mb is not None:
+ if mp is not None:
+ if (mb.start() > mp.start()) and (mb.end() < mp.end()):
+ parts.extend(Line._partition(text[:mp.start()]))
+ parts.extend(Line._partition_braces(mp.group()[1:-1]))
+ elif (mb.start() < mp.start()) and (mb.end() > mp.end()):
+ parts.extend(Line._partition_braces(text))
+ else:
+ raise ValueError("Incorrect format {}".format(text))
+ else:
+ parts.extend(Line._partition_braces(text))
+ else:
+ if mp is not None:
+ parts.extend(Line._partition(text[:mp.start()]))
+ parts.extend(Line._partition(mp.group()[1:-1]))
+ else:
+ parts.extend(Line._partition(text))
+ return Line._check_parameters(parts)
+
+ ##############################################
+
+ def split_element(self, prefix):
+
+ """Split the line according to the following pattern::
+
+ keyword parameter1 parameter2 ... key1=value1 key2=value2 ...
+
+ Return the list of parameters and the dictionary.
+
+ """
+
+ # Fixme: cf. get_kwarg
+
+ parameters = []
+ dict_parameters = {}
+
+ text = self.right_of(prefix)
+
+ parts = Line._partition_braces(text)
+
+ return Line._check_parameters(parts)
+
+####################################################################################################
+
+class SpiceParser:
+
+ """This class parse a Spice netlist file and build a syntax tree.
+
+ Public Attributes:
+
+ :attr:`circuit`
+
+ :attr:`models`
+
+ :attr:`subcircuits`
+
+ """
+
+ _logger = _module_logger.getChild('SpiceParser')
+
+ ##############################################
+
+ def __init__(self, path=None, source=None, end_of_line_comment=('$', '//', ';')):
+
+ # Fixme: empty source
+
+ if path is not None:
+ with open(str(path), 'r') as fh:
+ raw_lines = fh.readlines() # Fixme: cf. jmgc
+ elif source is not None:
+ raw_lines = source.split(os.linesep)
+ else:
+ raise ValueError
+
+ self._end_of_line_comment = end_of_line_comment
+
+ lines = self._merge_lines(raw_lines)
+ self._title = None
+ self._statements = self._parse(lines)
+
+ ##############################################
+
+ def _merge_lines(self, raw_lines):
+
+ """Merge broken lines and return a new list of lines.
+
+ A line starting with "+" continues the preceding line.
+
+ """
+
+ lines = []
+ current_line = None
+ for line_index, line_string in enumerate(raw_lines):
+ if line_string.startswith('+'):
+ current_line.append(line_string[1:].strip('\r\n'))
+ else:
+ line_string = line_string.strip(' \t\r\n')
+ if line_string:
+ _slice = slice(line_index, line_index + 1)
+ line = Line(line_string, _slice, self._end_of_line_comment)
+ lines.append(line)
+ # handle case with comment before line continuation
+ if not line_string.startswith('*'):
+ current_line = line
+
+ return lines
+
+ ##############################################
+
+ @staticmethod
+ def _check_models(circuit, available_models=set()):
+ p_available_models = available_models.copy()
+ p_available_models.update([model.name for model in circuit._models])
+ for subcircuit in circuit._subcircuits:
+ SpiceParser._check_models(subcircuit, p_available_models)
+ for model in circuit._required_models:
+ if model not in p_available_models:
+ raise ValueError("model (%s) not available in (%s)" % (model, circuit.name))
+
+ ##############################################
+
+ @staticmethod
+ def _sort_subcircuits(circuit, available_subcircuits=set()):
+ p_available_subcircuits = available_subcircuits.copy()
+ names = [subcircuit.name for subcircuit in circuit._subcircuits]
+ p_available_subcircuits.update(names)
+ dependencies = dict()
+ for subcircuit in circuit._subcircuits:
+ required = SpiceParser._sort_subcircuits(subcircuit, p_available_subcircuits)
+ dependencies[subcircuit] = required
+ for subcircuit in circuit._required_subcircuits:
+ if subcircuit not in p_available_subcircuits:
+ raise ValueError("subcircuit (%s) not available in (%s)" % (subcircuit, circuit.name))
+ items = sorted(dependencies.items(), key=lambda item: len(item[1]))
+ result = list()
+ result_names = list()
+ previous = len(items) + 1
+ while 0 < len(items) < previous:
+ previous = len(items)
+ remove = list()
+ for item in items:
+ subckt, depends = item
+ for name in depends:
+ if name not in result_names:
+ break
+ else:
+ result.append(subckt)
+ result_names.append(subckt.name)
+ remove.append(item)
+ for item in remove:
+ items.remove(item)
+ if len(items) > 0:
+ raise ValueError("Crossed dependencies (%s)" % [(key.name, value) for key, value in items])
+ circuit._subcircuits = result
+ return circuit._required_subcircuits - set(names)
+
+ ##############################################
+
+ def _parse(self, lines):
+
+ """Parse the lines and return a list of statements."""
+
+ # The first line in the input file must be the title, which is the only comment line that does
+ # not need any special character in the first place.
+ #
+ # The last line must be .end
+
+ if len(lines) <= 1:
+ raise NameError('Netlist is empty')
+ # if lines[-1] != '.end':
+ # raise NameError('".end" is expected at the end of the netlist')
+
+ circuit = CircuitStatement(lines[0])
+ stack = []
+ scope = circuit
+ for line in lines[1:]:
+ # print('>', repr(line))
+ text = str(line)
+ lower_case_text = text.lower() # !
+ if line.is_comment:
+ scope.append(Comment(line))
+ elif lower_case_text.startswith('.'):
+ lower_case_text = lower_case_text[1:]
+ if lower_case_text.startswith('subckt'):
+ stack.append(scope)
+ scope = SubCircuitStatement(line)
+ elif lower_case_text.startswith('ends'):
+ parent = stack.pop()
+ parent.append_subcircuit(scope)
+ scope = parent
+ elif lower_case_text.startswith('title'):
+ # override fist line
+ self._title = Title(line)
+ scope.append(self._title)
+ elif lower_case_text.startswith('end'):
+ pass
+ elif lower_case_text.startswith('model'):
+ model = Model(line)
+ scope.append_model(model)
+ elif lower_case_text.startswith('include'):
+ include = Include(line)
+ scope.append(include)
+ elif lower_case_text.startswith('param'):
+ param = Parameter(line)
+ scope.append_param(param)
+ else:
+ # options param ...
+ # .global
+ # .lib filename libname
+ # .func .csparam .temp .if
+ # { expr } are allowed in .model lines and in device lines.
+ self._logger.warn('Parser ignored: {}'.format(line))
+ else:
+ try:
+ element = Element(line)
+ scope.append(element)
+ if hasattr(element, '_prefix') and (element._prefix == "X"):
+ name = element._parameters[0].lower()
+ scope._required_subcircuits.add(name)
+ elif hasattr(element, '_dict_parameters') and 'model' in element._dict_parameters:
+ name = element._dict_parameters['model'].lower()
+ scope._required_models.add(name)
+ except ParseError:
+ pass
+ SpiceParser._check_models(circuit)
+ SpiceParser._sort_subcircuits(circuit)
+ return circuit
+
+ ##############################################
+
+ @property
+ def circuit(self):
+ """Circuit statements."""
+ return self._statements
+
+ @property
+ def models(self):
+ """Models of the sub-circuit."""
+ return self._statements.models
+
+ @property
+ def subcircuits(self):
+ """Subcircuits of the sub-circuit."""
+ return self._statements.subcircuits
+
+ ##############################################
+
+ def is_only_subcircuit(self):
+ return bool(not self.circuit and self.subcircuits)
+
+ ##############################################
+
+ def is_only_model(self):
+ return bool(not self.circuit and not self.subcircuits and self.models)
+
+ ##############################################
+
+ @staticmethod
+ def _build_circuit(circuit, statements, ground):
+
+ for statement in statements:
+ if isinstance(statement, Include):
+ circuit.include(str(statement))
+
+ for statement in statements:
+ if isinstance(statement, Element):
+ statement.build(circuit, ground)
+ elif isinstance(statement, Model):
+ statement.build(circuit)
+ elif isinstance(statement, SubCircuit):
+ subcircuit = statement.build(ground) # Fixme: ok ???
+ circuit.subcircuit(subcircuit)
+
+ ##############################################
+
+ def build_circuit(self, ground=0):
+ """Build a :class:`Circuit` instance.
+
+ Use the *ground* parameter to specify the node which must be translated to 0 (SPICE ground node).
+
+ """
+ # circuit = Circuit(str(self._title))
+ circuit = self.circuit.build(str(ground))
+ return circuit
+
+ ##############################################
+
+ @staticmethod
+ def netlist_to_python(netlist_name, statements, ground=0):
+
+ source_code = ''
+ for statement in statements:
+ if isinstance(statement, Element):
+ source_code += statement.to_python(netlist_name, ground)
+ elif isinstance(statement, Include):
+ pass
+ elif isinstance(statement, Model):
+ source_code += statement.to_python(netlist_name)
+ elif isinstance(statement, SubCircuitStatement):
+ source_code += statement.to_python(netlist_name)
+ elif isinstance(statement, Include):
+ source_code += statement.to_python(netlist_name)
+ return source_code
+
+ ##############################################
+
+ def to_python_code(self, ground=0):
+
+ ground = str(ground)
+
+ source_code = ''
+
+ if self.circuit:
+ source_code += "circuit = Circuit('{}')".format(self._title) + os.linesep
+ source_code += self.netlist_to_python('circuit', self._statements, ground)
+
+ return source_code
diff --git a/third_party/PySpice-org/PySpice/PySpice/Spice/RawFile.py b/third_party/PySpice-org/PySpice/PySpice/Spice/RawFile.py
new file mode 100644
index 00000000..eb7d04c6
--- /dev/null
+++ b/third_party/PySpice-org/PySpice/PySpice/Spice/RawFile.py
@@ -0,0 +1,417 @@
+####################################################################################################
+#
+# PySpice - A Spice Package for Python
+# Copyright (C) 2017 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 .
+#
+####################################################################################################
+
+####################################################################################################
+
+import os
+
+####################################################################################################
+
+"""This module provide tools to read raw output.
+"""
+
+####################################################################################################
+
+from PySpice.Unit import u_Degree, u_V, u_A, u_s, u_Hz
+
+####################################################################################################
+
+import logging
+import numpy as np
+
+####################################################################################################
+
+from PySpice.Probe.WaveForm import (OperatingPoint, SensitivityAnalysis,
+ DcAnalysis, AcAnalysis, TransientAnalysis,
+ WaveForm)
+
+####################################################################################################
+
+_module_logger = logging.getLogger(__name__)
+
+####################################################################################################
+
+class VariableAbc:
+
+ """This class implements a variable or probe in a SPICE simulation output.
+
+ Public Attributes:
+
+ :attr:`index`
+ index in the array
+
+ :attr:`name`
+
+ :attr:`unit`
+
+ """
+
+ ##############################################
+
+ def __init__(self, index, name, unit):
+
+ # Fixme: self._ ?
+
+ self._index = int(index)
+ self.name = str(name)
+ self._unit = unit # could be guessed from name also for voltage node and branch current
+ self.data = None
+
+ ##############################################
+
+ @property
+ def index(self):
+ return self._index
+
+ # @property
+ # def name(self):
+ # return self._name
+
+ # @name.setter
+ # def name(self, value):
+ # self._name = value
+
+ ##############################################
+
+ def __repr__(self):
+ return 'variable[{0._index}]: {0.name} [{0._unit}]'.format(self)
+
+ ##############################################
+
+ def is_voltage_node(self):
+ raise NotImplementedError
+
+ ##############################################
+
+ def is_branch_current(self):
+ raise NotImplementedError
+
+ ##############################################
+
+ @property
+ def is_interval_parameter(self):
+ return self.name.startswith('@') # Fixme: Xyce ???
+
+ ##############################################
+
+ @staticmethod
+ def to_voltage_name(node):
+ return 'v({})'.format(node)
+
+ ##############################################
+
+ @staticmethod
+ def to_branch_name(element):
+ return 'i({})'.format(element)
+
+ ##############################################
+
+ def fix_case(self, element_translation, node_translation):
+
+ """ Update the name to the right case. """
+
+ if self.is_branch_current():
+ if self.simplified_name in element_translation:
+ self.name = self.to_branch_name(element_translation[self.simplified_name])
+ elif self.is_voltage_node():
+ if self.simplified_name in node_translation:
+ self.name = self.to_voltage_name(node_translation[self.simplified_name])
+
+ ##############################################
+
+ @property
+ def simplified_name(self):
+ raise NotImplementedError
+
+ ##############################################
+
+ def to_waveform(self, abscissa=None, to_real=False, to_float=False):
+
+ """ Return a :obj:`PySpice.Probe.WaveForm` instance. """
+
+ data = self.data
+ if to_real:
+ data = data.real
+ # Fixme: else UnitValue instead of UnitValues
+ # if to_float:
+ # data = float(data[0])
+
+ if self._unit is not None:
+ return WaveForm.from_unit_values(self.simplified_name, self._unit(data), abscissa=abscissa)
+ else:
+ return WaveForm.from_array(self.simplified_name, data, abscissa=abscissa)
+
+####################################################################################################
+
+class RawFileAbc:
+
+ """ This class parse the stdout of ngspice and the raw data output.
+ """
+
+ _logger = _module_logger.getChild('RawFileAbc')
+
+ ##############################################
+
+ @property
+ def simulation(self):
+
+ if self._simulation is not None:
+ return self._simulation
+ else:
+ raise NameError('Simulation is undefined')
+
+ @simulation.setter
+ def simulation(self, value):
+ self._simulation = value
+
+ ##############################################
+
+ @property
+ def circuit(self):
+ return self._simulation.circuit
+
+ ##############################################
+
+ _name_to_unit = {
+ 'time': u_s,
+ 'voltage': u_V,
+ 'current': u_A,
+ 'frequency': u_Hz,
+ }
+
+ ##############################################
+
+ def _read_line(self, header_line_iterator):
+
+ """ Return the next line """
+
+ # Fixme: self._header_line_iterator, etc.
+
+ line = None
+ while not line:
+ line = next(header_line_iterator)
+ return line.decode('utf-8')
+
+ ##############################################
+
+ def _read_header_line(self, header_line_iterator, head_line):
+
+ """ Read an header line and check it starts with *head_line*. """
+
+ line = self._read_line(header_line_iterator)
+ self._logger.debug(line)
+ if line.startswith(head_line):
+ return line
+ else:
+ raise NameError("Unexpected line: %s" % (line))
+
+ ##############################################
+
+ def _read_header_field_line(self, header_line_iterator, expected_label, has_value=True):
+
+ """ Read an header line and check it starts with *expected_label*.
+
+ Return the values next to the label if the flag *has_value* is set.
+ """
+
+ line = self._read_line(header_line_iterator)
+ self._logger.debug(line)
+ if has_value:
+ # a title can have ': ' after 'title: '
+ location = line.find(': ') # first occurence
+ label, value = line[:location], line[location+2:]
+ else:
+ label = line[:-1]
+ if label != expected_label:
+ raise NameError("Expected label %s instead of %s" % (expected_label, label))
+ if has_value:
+ return value.strip()
+
+ ##############################################
+
+ def _read_temperature_line(self, header_line_iterator):
+
+ # Doing analysis at TEMP = 25.000000 and TNOM = 25.000000
+
+ line = self._read_header_line(header_line_iterator, 'Doing analysis at TEMP')
+ pattern1 = 'TEMP = '
+ pattern2 = ' and TNOM = '
+ pos1 = line.find(pattern1)
+ pos2 = line.find(pattern2)
+ if pos1 != -1 and pos2 != -1:
+ part1 = line[pos1+len(pattern1):pos2]
+ part2 = line[pos2+len(pattern2):].strip()
+ temperature = u_Degree(float(part1))
+ nominal_temperature = u_Degree(float(part2))
+ else:
+ temperature = None
+ nominal_temperature = None
+ return temperature, nominal_temperature
+
+ ##############################################
+
+ def _read_header_variables(self, header_line_iterator):
+
+ self.variables = {}
+ for i in range(self.number_of_variables):
+ line = (next(header_line_iterator)).decode('utf-8')
+ self._logger.debug(line)
+ items = [x.strip() for x in line.split('\t') if x]
+ # 0 frequency frequency grid=3
+ index, name, unit = items[:3]
+ # unit = time, voltage, current
+ unit = self._name_to_unit[unit] # convert to Unit
+ self.variables[name] = self._variable_cls(index, name, unit)
+ # self._read_header_field_line(header_line_iterator, 'Binary', has_value=False)
+
+ ##############################################
+
+ def _read_variable_data(self, raw_data):
+
+ """ Read the raw data and set the variable values. """
+
+ if self.flags == 'real':
+ number_of_columns = self.number_of_variables
+ elif self.flags == 'complex':
+ number_of_columns = 2*self.number_of_variables
+ else:
+ raise NotImplementedError
+
+ input_data = np.fromstring(raw_data, count=number_of_columns*self.number_of_points, dtype='f8')
+ input_data = input_data.reshape((self.number_of_points, number_of_columns))
+ input_data = input_data.transpose()
+ # np.savetxt('raw.txt', input_data)
+ if self.flags == 'complex':
+ raw_data = input_data
+ input_data = np.array(raw_data[0::2], dtype='complex128')
+ input_data.imag = raw_data[1::2]
+ for variable in self.variables.values():
+ variable.data = input_data[variable.index]
+
+ ##############################################
+
+ def nodes(self, to_float=False, abscissa=None):
+
+ return [variable.to_waveform(abscissa, to_float=to_float)
+ for variable in self.variables.values()
+ if variable.is_voltage_node()]
+
+ ##############################################
+
+ def branches(self, to_float=False, abscissa=None):
+
+ return [variable.to_waveform(abscissa, to_float=to_float)
+ for variable in self.variables.values()
+ if variable.is_branch_current()]
+
+ ##############################################
+
+ def internal_parameters(self, to_float=False, abscissa=None):
+
+ return [variable.to_waveform(abscissa, to_float=to_float)
+ for variable in self.variables.values()
+ if variable.is_interval_parameter]
+
+ ##############################################
+
+ def elements(self, abscissa=None):
+
+ return [variable.to_waveform(abscissa, to_float=True)
+ for variable in self.variables.values()]
+
+ ##############################################
+
+ def to_analysis(self):
+
+ self.fix_case()
+
+ if self.plot_name == 'Operating Point':
+ return self._to_operating_point_analysis()
+ elif self.plot_name == 'Sensitivity Analysis':
+ return self._to_sensitivity_analysis()
+ elif self.plot_name == 'DC transfer characteristic':
+ return self._to_dc_analysis()
+ elif self.plot_name == 'AC Analysis':
+ return self._to_ac_analysis()
+ elif self.plot_name == 'Transient Analysis':
+ return self._to_transient_analysis()
+ else:
+
+ raise NotImplementedError("Unsupported plot name {}".format(self.plot_name))
+
+ ##############################################
+
+ def _to_operating_point_analysis(self):
+
+ return OperatingPoint(
+ simulation=self.simulation,
+ nodes=self.nodes(to_float=True),
+ branches=self.branches(to_float=True),
+ )
+
+ ##############################################
+
+ def _to_sensitivity_analysis(self):
+
+ # Fixme: test .SENS I (VTEST)
+ # Fixme: separate v(vinput), analysis.R2.m
+ return SensitivityAnalysis(
+ simulation=self.simulation,
+ elements=self.elements(),
+ )
+
+ ##############################################
+
+ def _to_dc_analysis(self, sweep_variable):
+
+ sweep = sweep_variable.to_waveform()
+ return DcAnalysis(
+ simulation=self.simulation,
+ sweep=sweep,
+ nodes=self.nodes(),
+ branches=self.branches(),
+ internal_parameters=self.internal_parameters(),
+ )
+
+ ##############################################
+
+ def _to_ac_analysis(self):
+
+ frequency = self.variables['frequency'].to_waveform(to_real=True)
+ return AcAnalysis(
+ simulation=self.simulation,
+ frequency=frequency,
+ nodes=self.nodes(),
+ branches=self.branches(),
+ internal_parameters=self.internal_parameters(),
+ )
+
+ ##############################################
+
+ def _to_transient_analysis(self):
+
+ time = self.variables['time'].to_waveform(to_real=True)
+ return TransientAnalysis(
+ simulation=self.simulation,
+ time=time,
+ nodes=self.nodes(abscissa=time),
+ branches=self.branches(abscissa=time),
+ internal_parameters=self.internal_parameters(),
+ )
diff --git a/third_party/PySpice-org/PySpice/PySpice/Spice/Simulation.py b/third_party/PySpice-org/PySpice/PySpice/Spice/Simulation.py
new file mode 100644
index 00000000..54488bc5
--- /dev/null
+++ b/third_party/PySpice-org/PySpice/PySpice/Spice/Simulation.py
@@ -0,0 +1,1236 @@
+###################################################################################################
+#
+# 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 .
+#
+####################################################################################################
+
+"""This modules implements classes to perform simulations.
+"""
+
+####################################################################################################
+
+import logging
+import os
+
+####################################################################################################
+
+from ..Config import ConfigInstall
+from ..Tools.StringTools import join_list, join_dict, str_spice
+from ..Unit import Unit, as_V, as_A, as_s, as_Hz, as_Degree, u_Degree
+
+####################################################################################################
+
+_module_logger = logging.getLogger(__name__)
+
+####################################################################################################
+
+class AnalysisParameters:
+
+ """Base class for analysis parameters"""
+
+ ANALYSIS_NAME = None
+
+ ##############################################
+
+ @property
+ def analysis_name(self):
+ return self.ANALYSIS_NAME
+
+ ##############################################
+
+ def to_list(self):
+ return ()
+
+ ##############################################
+
+ def __str__(self):
+ return '.{0.analysis_name} {1}'.format(self, join_list(self.to_list()))
+
+####################################################################################################
+
+class OperatingPointAnalysisParameters(AnalysisParameters):
+
+ """This class defines analysis parameters for operating point analysis."""
+
+ ANALYSIS_NAME = 'op'
+
+####################################################################################################
+
+class DcSensitivityAnalysisParameters(AnalysisParameters):
+
+ """This class defines analysis parameters for DC sensitivity analysis."""
+
+ ANALYSIS_NAME = 'sens'
+
+ ##############################################
+
+ def __init__(self, output_variable):
+ self._output_variable = output_variable
+
+ ##############################################
+
+ @property
+ def output_variable(self):
+ return self._output_variable
+
+ ##############################################
+
+ def to_list(self):
+ return (self._output_variable,)
+
+####################################################################################################
+
+class AcSensitivityAnalysisParameters(AnalysisParameters):
+
+ """This class defines analysis parameters for AC sensitivity analysis."""
+
+ ANALYSIS_NAME = 'sens'
+
+ ##############################################
+
+ def __init__(self, output_variable, variation, number_of_points, start_frequency, stop_frequency):
+
+ if variation not in ('dec', 'oct', 'lin'):
+ raise ValueError("Incorrect variation type")
+
+ self._output_variable = output_variable
+ self._variation = variation
+ self._number_of_points = number_of_points
+ self._start_frequency = as_Hz(start_frequency)
+ self._stop_frequency = as_Hz(stop_frequency)
+
+ ##############################################
+
+ @property
+ def output_variable(self):
+ return self._output_variable
+
+ @property
+ def variation(self):
+ return self._variation
+
+ @property
+ def number_of_points(self):
+ return self._number_of_points
+
+ @property
+ def start_frequency(self):
+ return self._start_frequency
+
+ @property
+ def stop_frequency(self):
+ return self._stop_frequency
+
+ ##############################################
+
+ def to_list(self):
+ return (
+ self._output_variable,
+ self._variation,
+ self._number_of_points,
+ self._start_frequency,
+ self._stop_frequency
+ )
+
+####################################################################################################
+
+class DCAnalysisParameters(AnalysisParameters):
+
+ """This class defines analysis parameters for DC analysis."""
+
+ ANALYSIS_NAME = 'dc'
+
+ ##############################################
+
+ def __init__(self, **kwargs):
+
+ self._parameters = []
+ for variable, value_slice in kwargs.items():
+ variable_lower = variable.lower()
+ if variable_lower[0] in ('v', 'i', 'r') or variable_lower == 'temp':
+ self._parameters += [variable, value_slice.start, value_slice.stop, value_slice.step]
+ else:
+ raise NameError('Sweep variable must be a voltage/current source, '
+ 'a resistor or the circuit temperature')
+
+ ##############################################
+
+ @property
+ def parameters(self):
+ return self._parameters
+
+ ##############################################
+
+ def to_list(self):
+ return self._parameters
+
+####################################################################################################
+
+class ACAnalysisParameters(AnalysisParameters):
+
+ """This class defines analysis parameters for AC analysis."""
+
+ ANALYSIS_NAME = 'ac'
+
+ ##############################################
+
+ def __init__(self, variation, number_of_points, start_frequency, stop_frequency):
+
+ # Fixme: use mixin
+
+ if variation not in ('dec', 'oct', 'lin'):
+ raise ValueError("Incorrect variation type")
+
+ self._variation = variation
+ self._number_of_points = number_of_points
+ self._start_frequency = as_Hz(start_frequency)
+ self._stop_frequency = as_Hz(stop_frequency)
+
+ ##############################################
+
+ @property
+ def variation(self):
+ return self._variation
+
+ @property
+ def number_of_points(self):
+ return self._number_of_points
+
+ @property
+ def start_frequency(self):
+ return self._start_frequency
+
+ @property
+ def stop_frequency(self):
+ return self._stop_frequency
+
+ ##############################################
+
+ def to_list(self):
+ return (
+ self._variation,
+ self._number_of_points,
+ self._start_frequency,
+ self._stop_frequency
+ )
+
+####################################################################################################
+
+class TransientAnalysisParameters(AnalysisParameters):
+
+ """This class defines analysis parameters for transient analysis."""
+
+ ANALYSIS_NAME = 'tran'
+
+ ##############################################
+
+ def __init__(self, step_time, end_time, start_time=0, max_time=None, use_initial_condition=False):
+
+ self._step_time = as_s(step_time)
+ self._end_time = as_s(end_time)
+ self._start_time = as_s(start_time)
+ self._max_time = as_s(max_time, none=True)
+ self._use_initial_condition = use_initial_condition
+
+ ##############################################
+
+ @property
+ def step_time(self):
+ return self._step_time
+
+ @property
+ def end_time(self):
+ return self._end_time
+
+ @property
+ def start_time(self):
+ return self._start_time
+
+ @property
+ def max_time(self):
+ return self._max_time
+
+ @property
+ def use_initial_condition(self):
+ return self._use_initial_condition
+
+ ##############################################
+
+ def to_list(self):
+ return (
+ self._step_time,
+ self._end_time,
+ self._start_time,
+ self._max_time,
+ 'uic' if self._use_initial_condition else None,
+ )
+
+####################################################################################################
+
+class MeasureParameters(AnalysisParameters):
+
+ """This class defines measurements on analysis.
+
+ """
+
+ ANALYSIS_NAME = 'meas'
+
+ ##############################################
+
+ def __init__(self, analysis_type, name, *args):
+
+ _analysis_type = str(analysis_type).upper()
+ if _analysis_type not in ('AC', 'DC', 'OP', 'TRAN', 'TF', 'NOISE'):
+ raise ValueError('Incorrect analysis type {}'.format(analysis_type))
+
+ self._parameters = [_analysis_type, name, *args]
+
+ ##############################################
+
+ @property
+ def parameters(self):
+ return self._parameters
+
+ ##############################################
+
+ def to_list(self):
+ return self._parameters
+
+####################################################################################################
+
+class PoleZeroAnalysisParameters(AnalysisParameters):
+
+ """This class defines analysis parameters for pole-zero analysis."""
+
+ ANALYSIS_NAME = 'pz'
+
+ ##############################################
+
+ def __init__(self, node1, node2, node3, node4, tf_type, pz_type):
+
+ self._nodes = (node1, node2, node3, node4)
+ self._tf_type = tf_type # transfert_function
+ self._pz_type = pz_type # pole_zero
+
+ ##############################################
+
+ @property
+ def node1(self):
+ return self._nodes[0]
+
+ @property
+ def node2(self):
+ return self._nodes[1]
+
+ def node3(self):
+ return self._nodes[2]
+
+ @property
+ def node4(self):
+ return self._nodes[3]
+
+ @property
+ def tf_type(self):
+ return self._tf_type
+
+ @property
+ def pz_type(self):
+ return self._pz_type
+
+ ##############################################
+
+ def to_list(self):
+ return list(self._nodes) + [self._tf_type, self._pz_type]
+
+####################################################################################################
+
+class NoiseAnalysisParameters(AnalysisParameters):
+
+ """This class defines analysis parameters for noise analysis."""
+
+ ANALYSIS_NAME = 'noise'
+
+ ##############################################
+
+ def __init__(self, output, src, variation, points, start_frequency, stop_frequency, points_per_summary):
+
+ self._output = output
+ self._src = src
+ self._variation = variation
+ self._points = points
+ self._start_frequency = start_frequency
+ self._stop_frequency = stop_frequency
+ self._points_per_summary = points_per_summary
+
+ ##############################################
+
+ @property
+ def output(self):
+ return self._output
+
+ @property
+ def src(self):
+ return self._src
+
+ @property
+ def variation(self):
+ return self._variation
+
+ @property
+ def points(self):
+ return self._points
+
+ # Fixme: mixin
+ @property
+ def start_frequency(self):
+ return self._start_frequency
+
+ @property
+ def stop_frequency(self):
+ return self._stop_frequency
+
+ @property
+ def points_per_summary(self):
+ return self._points_per_summary
+
+ ##############################################
+
+ def to_list(self):
+
+ parameters = [
+ self._output,
+ self._src,
+ self._variation,
+ self._points,
+ self._start_frequency,
+ self._stop_frequency,
+ ]
+
+ if self._points_per_summary:
+ parameters.append(self._points_per_summary)
+
+ return parameters
+
+####################################################################################################
+
+class DistortionAnalysisParameters(AnalysisParameters):
+
+ """This class defines analysis parameters for distortion analysis."""
+
+ ANALYSIS_NAME = 'disto'
+
+ ##############################################
+
+ def __init__(self, variation, points, start_frequency, stop_frequency, f2overf1):
+
+ self._variation = variation
+ self._points = points
+ self._start_frequency = start_frequency
+ self._stop_frequency = stop_frequency
+ self._f2overf1 = f2overf1
+
+ ##############################################
+
+ @property
+ def variation(self):
+ return self._variation
+
+ @property
+ def points(self):
+ return self._points
+
+ @property
+ def start_frequency(self):
+ return self._start_frequency
+
+ @property
+ def stop_frequency(self):
+ return self._stop_frequency
+
+ @property
+ def f2overf1(self):
+ return self._f2overf1
+
+ ##############################################
+
+ def to_list(self):
+
+ parameters = [
+ self._variation,
+ self._points,
+ self._start_frequency,
+ self._stop_frequency,
+ ]
+
+ if self._f2overf1:
+ parameters.append(self._f2overf1)
+
+ return parameters
+
+####################################################################################################
+
+class TransferFunctionAnalysisParameters(AnalysisParameters):
+
+ """This class defines analysis parameters for transfer function (.tf) analysis."""
+
+ ANALYSIS_NAME = 'tf'
+
+ ##############################################
+
+ def __init__(self, outvar, insrc):
+ self._outvar = outvar
+ self._insrc = insrc
+
+ ##############################################
+
+ @property
+ def outvar(self):
+ return self._outvar
+
+ @property
+ def insrc(self):
+ return self._insrc
+
+ ##############################################
+
+ def to_list(self):
+ return (self._outvar, self._insrc)
+
+####################################################################################################
+
+class CircuitSimulation:
+
+ """Define and generate the spice instruction to perform a circuit simulation.
+
+ .. warning:: In some cases NgSpice can perform several analyses one after the other. This case
+ is partially supported.
+
+ """
+
+ _logger = _module_logger.getChild('CircuitSimulation')
+
+ ##############################################
+
+ def __init__(self, circuit, **kwargs):
+
+ self._circuit = circuit
+
+ self._options = {} # .options
+ self._measures = [] # .measure
+ self._initial_condition = {} # .ic
+ self._node_set = {} # .nodeset
+ self._saved_nodes = set()
+ self._analyses = {}
+
+ self.temperature = kwargs.get('temperature', u_Degree(27))
+ self.nominal_temperature = kwargs.get('nominal_temperature', u_Degree(27))
+
+ ##############################################
+
+ @property
+ def circuit(self):
+ return self._circuit
+
+ ##############################################
+
+ def options(self, *args, **kwargs):
+ for item in args:
+ self._options[str(item)] = None
+ for key, value in kwargs.items():
+ self._options[str(key)] = str_spice(value)
+
+ ##############################################
+
+ @property
+ def temperature(self):
+ return self._options['TEMP']
+
+ @temperature.setter
+ def temperature(self, value):
+ self._options['TEMP'] = as_Degree(value)
+
+ ##############################################
+
+ @property
+ def nominal_temperature(self):
+ return self._options['TNOM']
+
+ @nominal_temperature.setter
+ def nominal_temperature(self, value):
+ self._options['TNOM'] = as_Degree(value)
+
+ ##############################################
+
+ @staticmethod
+ def _make_initial_condition_dict(kwargs):
+ return {f"V({key})": str_spice(value) for key, value in kwargs.items()}
+
+ ##############################################
+
+ def initial_condition(self, **kwargs):
+ """Set initial condition for voltage nodes.
+
+ Usage::
+
+ simulator.initial_condition(node_name=value, ...)
+
+ General form::
+
+ .ic v(node_name)=value ...
+
+ The `.ic` line is for setting transient initial conditions. It has two different
+ interpretations, depending on whether the uic parameter is specified on the `.tran` control
+ line, or not. One should not confuse this line with the `.nodeset` line. The `.nodeset`
+ line is only to help DC convergence, and does not affect the final bias solution (except for
+ multi-stable circuits). The two indicated interpretations of this line are as follows:
+
+ 1. When the uic parameter is specified on the `.tran` line, the node voltages specified on
+ the `.ic` control line are used to compute the capacitor, diode, BJT, JFET, and MOSFET
+ initial conditions. This is equivalent to specifying the `ic=...` parameter on each
+ device line, but is much more convenient. The `ic=...` parameter can still be specified
+ and takes precedence over the `.ic` values. Since no dc bias (initial transient)
+ solution is computed before the transient analysis, one should take care to specify all
+ dc source voltages on the `.ic` control line if they are to be used to compute device
+ initial conditions.
+
+ 2. When the uic parameter is not specified on the `.tran` control line, the DC bias (initial
+ transient) solution is computed before the transient analysis. In this case, the node
+ voltages specified on the `.ic` control lines are forced to the desired initial values
+ during the bias solution. During transient analysis, the constraint on these node
+ voltages is removed. This is the preferred method since it allows Ngspice to compute a
+ consistent dc solution.
+
+ """
+ d = self._make_initial_condition_dict(kwargs)
+ self._initial_condition.update(d)
+
+ ##############################################
+
+ def node_set(self, **kwargs):
+ """Specify initial node voltage guesses.
+
+ Usage::
+
+ simulator.node_set(node_name=value, ...)
+
+ General form::
+
+ .nodeset v(node_name)=value ...
+ .nodeset all=val
+
+ The `.nodeset` line helps the program find the DC or initial transient solution by making a
+ preliminary pass with the specified nodes held to the given voltages. The restrictions are
+ then released and the iteration continues to the true solution. The `.nodeset` line may be
+ necessary for convergence on bistable or astable circuits. `.nodeset all=val` sets all
+ starting node voltages (except for the ground node) to the same value. In general, the
+ `.nodeset` line should not be necessary.
+
+ """
+ d = self._make_initial_condition_dict(kwargs)
+ self._node_set.update(d)
+
+ ##############################################
+
+ def save(self, *args):
+
+ # Fixme: pass Node for voltage node, Element for source branch current, ...
+
+ """Set the list of saved vectors.
+
+ If no *.save* line is given, then the default set of vectors is saved (node voltages and
+ voltage source branch currents). If *.save* lines are given, only those vectors specified
+ are saved.
+
+ Node voltages may be saved by giving the node_name or *v(node_name)*. Currents through an
+ independent voltage source (including inductor) are given by *i(source_name)* or
+ *source_name#branch*. Internal device data are accepted as *@dev[param]*.
+
+ If you want to save internal data in addition to the default vector set, add the parameter
+ *all* to the additional vectors to be saved.
+
+ """
+
+ self._saved_nodes |= set(*args)
+
+ ##############################################
+
+ def save_internal_parameters(self, *args):
+ """This method is similar to`save` but assume *all*."""
+ # Fixme: ok ???
+ self.save(list(args) + ['all'])
+
+ ##############################################
+
+ @property
+ def save_currents(self):
+ """ Save all currents. """
+ return self._options.get('SAVECURRENTS', False)
+
+ @save_currents.setter
+ def save_currents(self, value):
+ if value:
+ self._options['SAVECURRENTS'] = True
+ else:
+ del self._options['SAVECURRENTS']
+
+ ##############################################
+
+ def reset_analysis(self):
+ self._analyses.clear()
+
+ ##############################################
+
+ def analysis_iter(self):
+ return self._analyses.values()
+
+ ##############################################
+
+ def _add_analysis(self, analysis_parameters):
+ self._analyses[analysis_parameters.analysis_name] = analysis_parameters
+
+ ##############################################
+
+ def _add_measure(self, measure_parameters):
+ self._measures.append(measure_parameters)
+
+ ##############################################
+
+ def operating_point(self):
+ """Compute the operating point of the circuit with capacitors open and inductors shorted."""
+ self._add_analysis(OperatingPointAnalysisParameters())
+
+ ##############################################
+
+ def dc_sensitivity(self, output_variable):
+
+ """Compute the sensitivity of the DC operating point of a node voltage or voltage-source
+ branch current to all non-zero device parameters.
+
+ Examples of usage::
+
+ analysis = simulator.dc_sensitivity('v(out)')
+
+ Spice syntax:
+
+ .. code:: spice
+
+ .sens outvar
+
+ Examples:
+
+ .. code:: spice
+
+ .sens V(1, OUT)
+ .sens I(VTEST)
+
+ """
+
+ self._add_analysis(DcSensitivityAnalysisParameters(output_variable))
+
+ ##############################################
+
+ def ac_sensitivity(self, output_variable, variation, number_of_points, start_frequency, stop_frequency):
+
+ """Compute the sensitivity of the AC values of a node voltage or voltage-source branch
+ current to all non-zero device parameters.
+
+ Examples of usage::
+
+ analysis = simulator.ac_sensitivity(...)
+
+ Spice syntax:
+
+ .. code::
+
+ .sens outvar ac dec nd fstart fstop
+ .sens outvar ac oct no fstart fstop
+ .sens outvar ac lin np fstart fstop
+
+ Spice examples:
+
+ .. code::
+
+ .sens V(OUT) AC DEC 10 100 100 k
+
+ """
+
+ self._add_analysis(
+ AcSensitivityAnalysisParameters(
+ output_variable,
+ variation, number_of_points, start_frequency, stop_frequency
+ ))
+
+ ##############################################
+
+ def dc(self, **kwargs):
+
+ """Compute the DC transfer fonction of the circuit with capacitors open and inductors shorted.
+
+ Examples of usage::
+
+ analysis = simulator.dc(Vinput=slice(-2, 5, .01))
+ analysis = simulator.dc(Ibase=slice(0, 100e-6, 10e-6))
+ analysis = simulator.dc(Vcollector=slice(0, 5, .1), Ibase=slice(micro(10), micro(100), micro(10))) # broken ???
+
+ Spice syntax:
+
+ .. code:: spice
+
+ .dc src_name vstart vstop vincr [ src2 start2 stop2 incr2 ]
+
+ *src_name* is the name of an independent voltage or a current source, a resistor or the
+ circuit temperature.
+
+ *vstart*, *vstop*, and *vincr* are the starting, final, and incrementing values respectively.
+
+ A second source (*src2*) may optionally be specified with associated sweep parameters. In
+ this case, the first source is swept over its range for each value of the second source.
+
+ Spice examples:
+
+ .. code:: spice
+
+ .dc VIN 0 .2 5 5.0 0.25
+ .dc VDS 0 10 .5 VGS 0 5 1
+ .dc VCE 0 10 .2 5 IB 0 10U 1U
+ .dc RLoad 1k 2k 100
+ .dc TEMP -15 75 5
+
+ """
+
+ self._add_analysis(DCAnalysisParameters(**kwargs))
+
+ ##############################################
+
+ def ac(self, variation, number_of_points, start_frequency, stop_frequency):
+
+ # fixme: concise keyword ?
+
+ """Perform a small-signal AC analysis of the circuit where all non-linear devices are linearized
+ around their actual DC operating point.
+
+ Examples of usage::
+
+ analysis = simulator.ac(start_frequency=10@u_kHz, stop_frequency=1@u_GHz, number_of_points=10, variation='dec')
+
+ Note that in order for this analysis to be meaningful, at least one independent source must
+ have been specified with an AC value. Typically it does not make much sense to specify more
+ than one AC source. If you do, the result will be a superposition of all sources, thus
+ difficult to interpret.
+
+ Spice examples:
+
+ .. code::
+
+ .ac dec nd fstart fstop
+ .ac oct no fstart fstop
+ .ac lin np fstart fstop
+
+ The parameter *variation* must be either `dec`, `oct` or `lin`.
+
+ """
+
+ self._add_analysis(
+ ACAnalysisParameters(
+ variation, number_of_points, start_frequency, stop_frequency
+ ))
+
+ ##############################################
+
+ def measure(self, analysis_type, name, *args):
+
+ """Add a measure in the circuit.
+
+ Examples of usage::
+
+ simulator.measure('TRAN', 'tdiff', 'TRIG AT=10m', 'TARG v(n1) VAL=75.0 CROSS=1')
+ simulator.measure('tran', 'tdiff', 'TRIG AT=0m', f"TARG par('v(n1)-v(derate)') VAL=0 CROSS=1")
+
+ Note: can be used with the .options AUTOSTOP to stop the simulation at Trigger.
+
+ Spice syntax:
+
+ .. code:: spice
+
+ .meas tran tdiff TRIG AT=0m TARG v(n1) VAL=75.0 CROSS=1
+
+ """
+
+ self._add_measure(MeasureParameters(analysis_type, name, *args))
+
+ ##############################################
+
+ def transient(self, step_time, end_time, start_time=0, max_time=None, use_initial_condition=False):
+
+ """Perform a transient analysis of the circuit.
+
+ Examples of usage::
+
+ analysis = simulator.transient(step_time=1@u_us, end_time=500@u_us)
+ analysis = simulator.transient(step_time=source.period/200, end_time=source.period*2)
+
+ Spice syntax:
+
+ .. code:: spice
+
+ .tran tstep tstop >
+
+ """
+
+ self._add_analysis(
+ TransientAnalysisParameters(
+ step_time, end_time, start_time, max_time,
+ use_initial_condition
+ ))
+
+ ##############################################
+
+ def polezero(self, node1, node2, node3, node4, tf_type, pz_type):
+
+ """Perform a Pole-Zero analysis of the circuit.
+
+ node1, node2 - Input node pair.
+ node3, node4 - Output node pair
+ tf_type - should be `cur` for current or `vol` for voltage
+ pz_type - should be `pol` for pole, `zer` for zero, or `pz` for combined pole zero analysis.
+
+ See section 15.3.6 of ngspice manual.
+
+ Spice syntax:
+
+ .. code:: spice
+
+ .tran tstep tstop >
+ .pz node1 node2 node3 node4 cur pol
+ .pz node1 node2 node3 node4 cur zer
+ .pz node1 node2 node3 node4 cur pz
+ .pz node1 node2 node3 node4 vol pol
+ .pz node1 node2 NODE3 node4 vol zer
+ .pz node1 node2 node3 node4 vol pz
+
+ Examples:
+
+ .. code:: spice
+
+ .pz 1 0 3 0 cur pol
+ .pz 2 3 5 0 vol zer
+ .pz 4 1 4 1 cur pz
+
+ """
+
+ # do some rudimentary parameter checking.
+ if tf_type not in ('cur', 'vol'):
+ raise NameError("polezero type must be 'cur' or 'vol'")
+ if pz_type not in ('pol', 'zer', 'pz'):
+ raise NameError("pz_type must be 'pol' or 'zer' or 'pz'")
+
+ self._add_analysis(
+ PoleZeroAnalysisParameters(node1, node2, node3, node4, tf_type, pz_type)
+ )
+
+ ##############################################
+
+ def noise(self, output_node, ref_node, src, variation, points, start_frequency, stop_frequency, points_per_summary=None):
+
+ """Perform a Pole-Zero analysis of the circuit.
+
+ output_node, ref_node - output node pair.
+ src - signal source, typically an ac voltage input.
+ variation - must be 'dec' or 'lin' or 'oct' for decade, linear, or octave.
+ points, start_frequency, stop_frequency - number of points, start and stop frequencies.
+ points_per_summary - if specified, the noise contributions of each noise generator is produced every points_per_summary frequency points.
+
+ See section 15.3.4 of ngspice manual.
+
+ Spice syntax:
+
+ General form:
+
+ .. code:: spice
+
+ .noise v(output <,ref >) src ( dec | lin | oct ) pts fstart fstop
+
+ Examples:
+
+ .. code:: spice
+
+ .noise v(5) VIN dec 10 1kHz 100 MEG
+ .noise v(5 ,3) V1 oct 8 1.0 1.0 e6 1
+
+ """
+
+ # do some rudimentary parameter checking.
+ # Fixme: mixin
+ if variation not in ('dec', 'lin', 'oct'):
+ raise NameError("variation must be 'dec' or 'lin' or 'oct'")
+
+ output = 'V({},{})'.format(output_node, ref_node)
+
+ self._add_analysis(
+ NoiseAnalysisParameters(output, src, variation, points, start_frequency, stop_frequency, points_per_summary)
+ )
+
+ ##############################################
+
+ def transfer_function(self, outvar, insrc):
+
+ """The python arguments to this function should be two strings, outvar and insrc.
+
+ ngspice documentation as follows:
+
+ General form:
+
+ .. code:: spice
+
+ .tf outvar insrc
+
+ Examples:
+
+ .. code:: spice
+
+ .tf v(5, 3) VIN
+ .tf i(VLOAD) VIN
+
+ The .tf line defines the small-signal output and input for the dc small-signal
+ analysis. outvar is the small signal output variable and insrc is the small-signal input
+ source. If this line is included, ngspice computes the dc small-signal value of the transfer
+ function (output/input), input resistance, and output resistance. For the first example,
+ ngspice would compute the ratio of V(5, 3) to VIN, the small-signal input resistance at VIN,
+ and the small signal output resistance measured across nodes 5 and 3
+
+ """
+
+ self._add_analysis(
+ TransferFunctionAnalysisParameters(outvar, insrc)
+ )
+
+ ##############################################
+
+ def distortion(self, variation, points, start_frequency, stop_frequency, f2overf1=None):
+
+ """Perform a distortion analysis of the circuit.
+
+ variation, points, start_frequency, stop_frequency - typical ac range parameters.
+ if f2overf1 is specified, perform a spectral analysis, else perform a harmonic analysis.
+
+ See section 15.3.3 of ngspice manual.
+
+ - harmonic analysis,
+ The distof1 parameter of the AC input to the circuit must be specified.
+ Second harmonic magnitude and phase are calculated at each circuit node.
+
+ - Spectral analysis,
+ The distof2 parameter of the AC input to the circuit must be specified as well as distof1.
+ See the ngspice manual.
+
+ Spice syntax:
+
+ General form:
+
+ .. code:: spice
+
+ .disto dec nd fstart fstop
+ .disto oct no fstart fstop
+ .disto lin np fstart fstop
+
+ Examples:
+
+ .. code:: spice
+
+ .disto dec 10 1kHz 100 MEG
+ .disto dec 10 1kHz 100 MEG 0.9
+
+ """
+
+ # do some rudimentary parameter checking.
+ if variation not in ('dec', 'lin', 'oct'):
+ raise NameError("variation must be 'dec' or 'lin' or 'oct'")
+
+ self._add_analysis(
+ DistortionAnalysisParameters(variation, points, start_frequency, stop_frequency, f2overf1)
+ )
+
+ ##############################################
+
+ def str_options(self, unit=True):
+
+ # Fixme: use cls settings ???
+ if unit:
+ _str = str_spice
+ else:
+ _str = lambda x: str_spice(x, unit)
+
+ netlist = ''
+ if self.options:
+ for key, value in self._options.items():
+ if value is not None:
+ netlist += '.options {} = {}'.format(key, _str(value)) + os.linesep
+ else:
+ netlist += '.options {}'.format(key) + os.linesep
+ return netlist
+
+ ##############################################
+
+ def __str__(self):
+
+ netlist = self._circuit.str(simulator=self.SIMULATOR)
+ netlist += self.str_options()
+ if self._initial_condition:
+ netlist += '.ic ' + join_dict(self._initial_condition) + os.linesep
+ if self._node_set:
+ netlist += '.nodeset ' + join_dict(self._node_set) + os.linesep
+
+ if self._saved_nodes:
+ # Place 'all' first
+ saved_nodes = self._saved_nodes
+ if 'all' in saved_nodes:
+ all_str = 'all '
+ saved_nodes.remove('all')
+ else:
+ all_str = ''
+ netlist += '.save ' + all_str + join_list(saved_nodes) + os.linesep
+ for measure_parameters in self._measures:
+ netlist += str(measure_parameters) + os.linesep
+ for analysis_parameters in self._analyses.values():
+ netlist += str(analysis_parameters) + os.linesep
+ netlist += '.end' + os.linesep
+ return netlist
+
+####################################################################################################
+
+class CircuitSimulator(CircuitSimulation):
+
+ """ This class implements a circuit simulator. Each analysis mode is performed by a method that
+ return the measured probes.
+
+ For *ac* and *transient* analyses, the user must specify a list of nodes using the *probes* key
+ argument.
+ """
+
+ _logger = _module_logger.getChild('CircuitSimulator')
+
+ if ConfigInstall.OS.on_windows:
+ DEFAULT_SIMULATOR = 'ngspice-shared'
+ else:
+ # DEFAULT_SIMULATOR = 'ngspice-subprocess'
+ DEFAULT_SIMULATOR = 'ngspice-shared'
+ # DEFAULT_SIMULATOR = 'xyce-serial'
+ # DEFAULT_SIMULATOR = 'xyce-parallel'
+
+ ##############################################
+
+ @classmethod
+ def factory(cls, circuit, *args, **kwargs):
+
+ """Return a :obj:`PySpice.Spice.Simulation.SubprocessCircuitSimulator` or
+ :obj:`PySpice.Spice.Simulation.NgSpiceSharedCircuitSimulator` instance depending of the
+ value of the *simulator* parameter: ``subprocess`` or ``shared``, respectively. If this
+ parameter is not specified then a subprocess simulator is returned.
+
+ """
+
+ if 'simulator' in kwargs:
+ simulator = kwargs['simulator']
+ del kwargs['simulator']
+ else:
+ simulator = cls.DEFAULT_SIMULATOR
+
+ sub_cls = None
+ if simulator in ('ngspice-subprocess', 'ngspice-shared'):
+ if simulator == 'ngspice-subprocess':
+ from .NgSpice.Simulation import NgSpiceSubprocessCircuitSimulator
+ sub_cls = NgSpiceSubprocessCircuitSimulator
+ elif simulator == 'ngspice-shared':
+ from .NgSpice.Simulation import NgSpiceSharedCircuitSimulator
+ sub_cls = NgSpiceSharedCircuitSimulator
+ elif simulator in ('xyce-serial', 'xyce-parallel'):
+ from .Xyce.Simulation import XyceCircuitSimulator
+ sub_cls = XyceCircuitSimulator
+ if simulator == 'xyce-parallel':
+ kwargs['parallel'] = True
+
+ if sub_cls is not None:
+ return sub_cls(circuit, *args, **kwargs)
+ else:
+ raise ValueError('Unknown simulator type')
+
+ ##############################################
+
+ def _run(self, analysis_method, *args, **kwargs):
+
+ self.reset_analysis()
+ if 'probes' in kwargs:
+ self.save(* kwargs.pop('probes'))
+
+ _kwargs = dict(kwargs)
+ _kwargs.pop('log_desk', None)
+
+ method = getattr(CircuitSimulation, analysis_method)
+ method(self, *args, **_kwargs)
+
+ message = 'desk' + os.linesep + str(self)
+ if kwargs.get('log_desk', False):
+ self._logger.info(message)
+ else:
+ self._logger.debug(message)
+
+ ##############################################
+
+ def operating_point(self, *args, **kwargs):
+ return self._run('operating_point', *args, **kwargs)
+
+ ##############################################
+
+ def dc(self, *args, **kwargs):
+ return self._run('dc', *args, **kwargs)
+
+ ##############################################
+
+ def dc_sensitivity(self, *args, **kwargs):
+ return self._run('dc_sensitivity', *args, **kwargs)
+
+ ##############################################
+
+ def ac(self, *args, **kwargs):
+ return self._run('ac', *args, **kwargs)
+
+ ##############################################
+
+ def transient(self, *args, **kwargs):
+ return self._run('transient', *args, **kwargs)
+
+ ##############################################
+
+ def polezero(self, *args, **kwargs):
+ return self._run('polezero', *args, **kwargs)
+
+ ##############################################
+
+ def noise(self, *args, **kwargs):
+ return self._run('noise', *args, **kwargs)
+
+ ##############################################
+
+ def distortion(self, *args, **kwargs):
+ return self._run('distortion', *args, **kwargs)
+
+ ##############################################
+
+ def transfer_function(self, *args, **kwargs):
+ return self._run('transfer_function', *args, **kwargs)
+
+ tf = transfer_function # shorcut
diff --git a/third_party/PySpice-org/PySpice/PySpice/Spice/Xyce/RawFile.py b/third_party/PySpice-org/PySpice/PySpice/Spice/Xyce/RawFile.py
new file mode 100644
index 00000000..8abf5ffb
--- /dev/null
+++ b/third_party/PySpice-org/PySpice/PySpice/Spice/Xyce/RawFile.py
@@ -0,0 +1,175 @@
+####################################################################################################
+#
+# PySpice - A Spice Package for Python
+# Copyright (C) 2017 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 .
+#
+####################################################################################################
+
+####################################################################################################
+
+import os
+
+from ..RawFile import VariableAbc, RawFileAbc
+
+####################################################################################################
+
+"""This module provide tools to read the output of Xyce.
+
+Header
+
+"""
+
+####################################################################################################
+
+import logging
+
+####################################################################################################
+
+_module_logger = logging.getLogger(__name__)
+
+####################################################################################################
+
+class Variable(VariableAbc):
+
+ ##############################################
+
+ def is_voltage_node(self):
+
+ name = self.name.lower()
+ return name.startswith('v(') or not self.is_branch_current()
+
+ ##############################################
+
+ def is_branch_current(self):
+ return self.name.endswith('#branch')
+
+ ##############################################
+
+ @staticmethod
+ def to_voltage_name(node):
+ return 'v({})'.format(node)
+
+ ##############################################
+
+ @property
+ def simplified_name(self):
+
+ name = self.name
+ if len(name) > 1 and name[1] == '(':
+ return name[2:-1]
+ elif name.endswith('#branch'):
+ return name[:-7]
+ elif '#' in name:
+ # Xyce change name of type "output_plus" to "OUTPUT#PLUS"
+ return name.replace('#', '_')
+ else:
+ return self.name
+
+####################################################################################################
+
+class RawFile(RawFileAbc):
+
+ """ This class parse the stdout of ngspice and the raw data output.
+
+ Public Attributes:
+
+ :attr:`data`
+
+ :attr:`date`
+
+ :attr:`flags`
+ 'real' or 'complex'
+
+ :attr:`number_of_points`
+
+ :attr:`number_of_variables`
+
+ :attr:`plot_name`
+ AC Analysis, Operating Point, Sensitivity Analysis, DC transfer characteristic
+
+ :attr:`title`
+
+ :attr:`variables`
+
+ """
+
+ _logger = _module_logger.getChild('RawFile')
+
+ _variable_cls = Variable
+
+ ##############################################
+
+ def __init__(self, output):
+
+ raw_data = self._read_header(output)
+ self._read_variable_data(raw_data)
+ # self._to_analysis()
+
+ self._simulation = None
+
+
+ ##############################################
+
+ def _read_header(self, output):
+
+ """ Parse the header """
+
+ # see https://github.com/FabriceSalvaire/PySpice/issues/132
+ # Xyce open the file in binary mode and print using: os << "Binary:" << std::endl;
+ # endl is thus \n
+ binary_line = b'Binary:\n'
+ binary_location = output.find(binary_line)
+ if binary_location < 0:
+ raise NameError('Cannot locate binary data')
+ raw_data_start = binary_location + len(binary_line)
+ self._logger.debug(os.linesep + output[:raw_data_start].decode('utf-8'))
+ header_lines = output[:binary_location].splitlines()
+ raw_data = output[raw_data_start:]
+ header_line_iterator = iter(header_lines)
+
+ self.title = self._read_header_field_line(header_line_iterator, 'Title')
+ self.date = self._read_header_field_line(header_line_iterator, 'Date')
+ self.plot_name = self._read_header_field_line(header_line_iterator, 'Plotname')
+ self.flags = self._read_header_field_line(header_line_iterator, 'Flags')
+ self.number_of_variables = int(self._read_header_field_line(header_line_iterator, 'No. Variables'))
+ self.number_of_points = int(self._read_header_field_line(header_line_iterator, 'No. Points'))
+ self._read_header_field_line(header_line_iterator, 'Variables')
+ self._read_header_variables(header_line_iterator)
+
+ return raw_data
+
+ ##############################################
+
+ def fix_case(self):
+
+ """ Ngspice return lower case names. This method fixes the case of the variable names. """
+
+ circuit = self.circuit
+ element_translation = {element.upper():element for element in circuit.element_names}
+ node_translation = {node.upper():node for node in circuit.node_names}
+ for variable in self.variables.values():
+ variable.fix_case(element_translation, node_translation)
+
+ ##############################################
+
+ def _to_dc_analysis(self):
+
+ if 'sweep' in self.variables:
+ sweep_variable = self.variables['sweep']
+ else:
+ raise NotImplementedError
+
+ return super()._to_dc_analysis(sweep_variable)
diff --git a/third_party/PySpice-org/PySpice/PySpice/Spice/Xyce/Server.py b/third_party/PySpice-org/PySpice/PySpice/Spice/Xyce/Server.py
new file mode 100644
index 00000000..c05325c4
--- /dev/null
+++ b/third_party/PySpice-org/PySpice/PySpice/Spice/Xyce/Server.py
@@ -0,0 +1,140 @@
+####################################################################################################
+#
+# PySpice - A Spice Package for Python
+# Copyright (C) 2017 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 .
+#
+####################################################################################################
+
+"""This module provides an interface to run xyce and get back the simulation
+output.
+
+"""
+
+####################################################################################################
+
+import logging
+import os
+import shutil
+import subprocess
+import tempfile
+
+from PySpice.Config import ConfigInstall
+from .RawFile import RawFile
+
+####################################################################################################
+
+_module_logger = logging.getLogger(__name__)
+
+####################################################################################################
+
+class XyceServer:
+
+ """This class wraps the execution of Xyce and convert the output to a Python data structure.
+
+ Example of usage::
+
+ spice_server = XyceServer(xyce_command='/path/to/Xyce')
+ raw_file = spice_server(spice_input)
+
+ It returns a :obj:`PySpice.Spice.RawFile` instance.
+
+ Default Xyce path is set in `XyceServer.XYCE_COMMAND`.
+
+ """
+
+ if ConfigInstall.OS.on_linux:
+ XYCE_COMMAND = 'Xyce'
+ elif ConfigInstall.OS.on_osx:
+ XYCE_COMMAND = 'Xyce'
+ elif ConfigInstall.OS.on_windows:
+ XYCE_COMMAND = 'C:\\Program Files\\Xyce 6.10 OPENSOURCE\\bin\\Xyce.exe'
+ else:
+ raise NotImplementedError
+
+ _logger = _module_logger.getChild('XyceServer')
+
+ ##############################################
+
+ def __init__(self, **kwargs):
+
+ self._xyce_command = kwargs.get('xyce_command') or self.XYCE_COMMAND
+
+ ##############################################
+
+ def _parse_stdout(self, stdout):
+
+ """Parse stdout for errors."""
+
+ # log Spice output
+ self._logger.info(os.linesep + stdout.decode('utf-8'))
+
+ error_found = False
+ simulation_failed = False
+ warning_found = False
+ lines = stdout.splitlines()
+ for line_index, line in enumerate(lines):
+ if line.startswith(b'Netlist warning'):
+ warning_found = True
+ # Fixme: highlight warnings
+ self._logger.warning(os.linesep + line.decode('utf-8'))
+ elif line.startswith(b'Netlist error'):
+ error_found = True
+ self._logger.error(os.linesep + line.decode('utf-8'))
+ elif b'Transient failure history' in line:
+ simulation_failed = True
+ self._logger.error(os.linesep + line.decode('utf-8'))
+ if error_found:
+ raise NameError("Errors was found by Xyce")
+ elif simulation_failed:
+ raise NameError("Xyce simulation failed")
+
+ ##############################################
+
+ def __call__(self, spice_input):
+
+ """Run SPICE in server mode as a subprocess for the given input and return a
+ :obj:`PySpice.RawFile.RawFile` instance.
+
+ """
+
+ self._logger.debug('Start the xyce subprocess')
+
+ tmp_dir = tempfile.mkdtemp()
+ input_filename = os.path.join(tmp_dir, 'input.cir')
+ output_filename = os.path.join(tmp_dir, 'output.raw')
+ with open(input_filename, 'w') as f:
+ f.write(str(spice_input))
+
+ command = (self._xyce_command, '-r', output_filename, input_filename)
+ self._logger.info('Run {}'.format(' '.join(command)))
+ process = subprocess.Popen(
+ command,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ )
+ stdout, stderr = process.communicate()
+
+ self._parse_stdout(stdout)
+
+ with open(output_filename, 'rb') as f:
+ output = f.read()
+ # self._logger.debug(output)
+
+ raw_file = RawFile(output)
+ shutil.rmtree(tmp_dir)
+
+ return raw_file
diff --git a/third_party/PySpice-org/PySpice/PySpice/Spice/Xyce/Simulation.py b/third_party/PySpice-org/PySpice/PySpice/Spice/Xyce/Simulation.py
new file mode 100644
index 00000000..70058ce7
--- /dev/null
+++ b/third_party/PySpice-org/PySpice/PySpice/Spice/Xyce/Simulation.py
@@ -0,0 +1,73 @@
+###################################################################################################
+#
+# PySpice - A Spice Package for Python
+# Copyright (C) 2017 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 .
+#
+####################################################################################################
+
+"""This modules implements classes to perform simulations.
+"""
+
+####################################################################################################
+
+import logging
+
+####################################################################################################
+
+from ..Simulation import CircuitSimulator
+from .Server import XyceServer
+
+####################################################################################################
+
+_module_logger = logging.getLogger(__name__)
+
+####################################################################################################
+
+class XyceCircuitSimulator(CircuitSimulator):
+
+ _logger = _module_logger.getChild('XyceCircuitSimulator')
+
+ SIMULATOR = 'xyce'
+
+ ##############################################
+
+ def __init__(self, circuit, **kwargs):
+
+ super().__init__(circuit, **kwargs)
+
+ xyce_command = kwargs.get('xyce_command', None)
+ self._xyce_server = XyceServer(xyce_command=xyce_command)
+
+ ##############################################
+
+ def str_options(self):
+
+ return super().str_options(unit=False)
+
+ ##############################################
+
+ def _run(self, analysis_method, *args, **kwargs):
+
+ super()._run(analysis_method, *args, **kwargs)
+
+ raw_file = self._xyce_server(spice_input=str(self))
+ self.reset_analysis()
+ raw_file.simulation = self
+
+ # for field in raw_file.variables:
+ # print field
+
+ return raw_file.to_analysis()
diff --git a/third_party/PySpice-org/PySpice/PySpice/Spice/Xyce/__init__.py b/third_party/PySpice-org/PySpice/PySpice/Spice/Xyce/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/third_party/PySpice-org/PySpice/PySpice/Spice/__init__.py b/third_party/PySpice-org/PySpice/PySpice/Spice/__init__.py
new file mode 100644
index 00000000..81e67035
--- /dev/null
+++ b/third_party/PySpice-org/PySpice/PySpice/Spice/__init__.py
@@ -0,0 +1,75 @@
+####################################################################################################
+#
+# 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 .
+#
+####################################################################################################
+
+####################################################################################################
+
+import logging
+
+from . import BasicElement
+from . import HighLevelElement
+from .Netlist import Netlist, ElementParameterMetaClass
+
+####################################################################################################
+
+_module_logger = logging.getLogger(__name__)
+
+####################################################################################################
+
+def _get_elements(module):
+ element_classes = []
+ for item in module.__dict__.values():
+ if (type(item) is ElementParameterMetaClass
+ and item.PREFIX is not None
+ ):
+ element_classes.append(item)
+ return element_classes
+
+####################################################################################################
+#
+# Add a method to create elements to the Netlist class
+#
+
+spice_elements = _get_elements(BasicElement)
+high_level_elements = _get_elements(HighLevelElement)
+
+for element_class in spice_elements + high_level_elements:
+
+ def _make_function(element_class):
+ def function(self, *args, **kwargs):
+ return element_class(self, *args, **kwargs)
+ # Preserve docstrings for element shortcuts
+ function.__doc__ = element_class.__doc__
+ return function
+
+ func = _make_function(element_class)
+
+ def _set(name):
+ # _module_logger.debug("Add device shortcut {} for class {}".format(name, element_class))
+ setattr(Netlist, name, func)
+
+ _set(element_class.__name__)
+
+ if element_class in spice_elements:
+ if hasattr(element_class, 'ALIAS'):
+ _set(element_class.ALIAS)
+ if hasattr(element_class, 'LONG_ALIAS'):
+ _set(element_class.LONG_ALIAS)
+
+
diff --git a/third_party/PySpice-org/PySpice/PySpice/Tools/EnumFactory.py b/third_party/PySpice-org/PySpice/PySpice/Tools/EnumFactory.py
new file mode 100644
index 00000000..658d11ce
--- /dev/null
+++ b/third_party/PySpice-org/PySpice/PySpice/Tools/EnumFactory.py
@@ -0,0 +1,176 @@
+####################################################################################################
+#
+# 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 .
+#
+####################################################################################################
+
+""" This module provides an implementation for enumerate.
+
+The enumerate factory :func:`EnumFactory` builds a enumerate from a list of names and assigns to
+these constants a value from 0 to N-1, where N is the number of constants. For example::
+
+ enum = EnumFactory('Enum1', ('cst1', 'cst2'))
+
+builds a enumerate with *cst1* set to 0 and *cst2* set to 1.
+
+We can get a constant's value using an integer context like::
+
+ int(enum.cst1)
+
+and the constant's name using::
+
+ repr(enum.cst1)
+
+We can test constant equality using::
+
+ enum1.cst == enum2.cst
+
+or with something that understand the *int* protocol::
+
+ enum1.cst == obj
+ # equivalent to
+ int(enum1.cst) == int(obj)
+
+The number of constants could be retrieved with::
+
+ len(enum)
+
+The enumerate factory :func:`ExplicitEnumFactory` is a variant that permits to specify the values of
+the constants::
+
+ enum2 = ExplicitEnumFactory('Enum2', {'cst1':1, 'cst2':3})
+
+We can test if a value is in the enumerate using::
+
+ constant_value in enum2
+
+"""
+
+####################################################################################################
+
+# __all__ = ['EnumFactory', 'ExplicitEnumFactory']
+
+####################################################################################################
+
+class ReadOnlyMetaClass(type):
+
+ """ This meta class implements a class where attributes are read only. """
+
+ ##############################################
+
+ def __setattr__(self, name, value):
+
+ raise NotImplementedError
+
+####################################################################################################
+
+class EnumMetaClass(ReadOnlyMetaClass):
+
+ """ This meta class implements the :func:`len` protocol. """
+
+ ##############################################
+
+ def __len__(self):
+
+ return self._size
+
+ ##############################################
+
+ def __getitem__(self, i):
+
+ return self._index[i]
+
+####################################################################################################
+
+class ExplicitEnumMetaClass(ReadOnlyMetaClass):
+
+ """ This meta class implements the operator ``in``. """
+
+ ##############################################
+
+ def __contains__(self, item):
+
+ return item in self.constants
+
+####################################################################################################
+
+class EnumConstant:
+
+ """ Define an Enum Constant """
+
+ ##############################################
+
+ def __init__(self, name, value):
+
+ self._name = name
+ self._value = value
+
+ ##############################################
+
+ def __eq__(self, other):
+
+ return self._value == int(other)
+
+ ##############################################
+
+ def __int__(self):
+
+ return self._value
+
+ ##############################################
+
+ def __hash__(self):
+
+ return self._value
+
+ ##############################################
+
+ def __repr__(self):
+
+ return self._name
+
+####################################################################################################
+
+def EnumFactory(enum_name, enum_tuple):
+
+ """ Return an :class:`EnumMetaClass` instance, where *enum_name* is the class name and
+ *enum_tuple* is an iterable of constant's names.
+ """
+
+ index = [EnumConstant(name, value) for value, name in enumerate(enum_tuple)]
+
+ obj_dict = {}
+ obj_dict['_size'] = len(enum_tuple)
+ obj_dict['_index'] = index
+ obj_dict.update({str(enum):enum for enum in index})
+
+ return EnumMetaClass(enum_name, (), obj_dict)
+
+####################################################################################################
+
+def ExplicitEnumFactory(enum_name, enum_dict):
+
+ """ Return an :class:`ExplicitEnumMetaClass` instance, where *enum_name* is the class name and
+ *enum_dict* is a dict of constant's names and their values.
+ """
+
+ obj_dict = {}
+ obj_dict['constants'] = list(enum_dict.values())
+ for name, value in list(enum_dict.items()):
+ obj_dict[name] = EnumConstant(name, value)
+
+ return ExplicitEnumMetaClass(enum_name, (), obj_dict)
diff --git a/third_party/PySpice-org/PySpice/PySpice/Tools/File.py b/third_party/PySpice-org/PySpice/PySpice/Tools/File.py
new file mode 100644
index 00000000..ee693263
--- /dev/null
+++ b/third_party/PySpice-org/PySpice/PySpice/Tools/File.py
@@ -0,0 +1,292 @@
+####################################################################################################
+#
+# 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 .
+#
+####################################################################################################
+
+# Used by Spice/Library.py
+
+####################################################################################################
+
+import os
+import subprocess
+
+####################################################################################################
+
+def file_name_has_extension(file_name, extension):
+
+ return file_name.endswith(extension)
+
+####################################################################################################
+
+def file_extension(filename):
+
+ # index = filename.rfind(os.path.extsep)
+ # if index == -1:
+ # return None
+ # else:
+ # return filename[index:]
+
+ return os.path.splitext(filename)[1]
+
+####################################################################################################
+
+def run_shasum(filename, algorithm=1, text=False, binary=False, portable=False):
+
+ if algorithm not in (1, 224, 256, 384, 512, 512224, 512256):
+ raise ValueError
+
+ args = ['shasum', '--algorithm=' + str(algorithm)]
+ if text:
+ args.append('--text')
+ elif binary:
+ args.append('--binary')
+ elif portable:
+ args.append('--portable')
+ args.append(filename)
+ output = subprocess.check_output(args)
+ shasum = output[:output.find(' ')]
+
+ return shasum
+
+####################################################################################################
+
+class Path:
+
+ ##############################################
+
+ def __init__(self, path):
+
+ self._path = str(path)
+
+ ##############################################
+
+ def __bool__(self):
+
+ return os.path.exists(self._path)
+
+ ##############################################
+
+ def __str__(self):
+
+ return self._path
+
+ ##############################################
+
+ @property
+ def path(self):
+
+ return self._path
+
+ ##############################################
+
+ def is_absolut(self):
+
+ return os.path.isabs(self._path)
+
+ ##############################################
+
+ def absolut(self):
+
+ return self.clone_for_path(os.path.abspath(self._path))
+
+ ##############################################
+
+ def normalise(self):
+
+ return self.clone_for_path(os.path.normpath(self._path))
+
+ ##############################################
+
+ def normalise_case(self):
+
+ return self.clone_for_path(os.path.normcase(self._path))
+
+ ##############################################
+
+ def expand_vars_and_user(self):
+
+ return self.clone_for_path(os.path.expandvars(os.path.expanduser(self._path)))
+
+ ##############################################
+
+ def real_path(self):
+
+ return self.clone_for_path(os.path.realpath(self._path))
+
+ ##############################################
+
+ def relative_to(self, directory):
+
+ return self.clone_for_path(os.path.relpath(self._path, str(directory)))
+
+ ##############################################
+
+ def clone_for_path(self, path):
+
+ return self.__class__(path)
+
+ ##############################################
+
+ def split(self):
+
+ return self._path.split(os.path.sep)
+
+ ##############################################
+
+ def directory_part(self):
+
+ return Directory(os.path.dirname(self._path))
+
+ ##############################################
+
+ def filename_part(self):
+
+ return os.path.basename(self._path)
+
+ ##############################################
+
+ def is_directory(self):
+
+ return os.path.isdir(self._path)
+
+ ##############################################
+
+ def is_file(self):
+
+ return os.path.isfile(self._path)
+
+ ##############################################
+
+ @property
+ def inode(self):
+
+ return os.stat(self._path).st_ino
+
+ ##############################################
+
+ @property
+ def creation_time(self):
+
+ return os.stat(self._path).st_ctime
+
+####################################################################################################
+
+class Directory(Path):
+
+ ##############################################
+
+ def __bool__(self):
+
+ return super().__nonzero__() and self.is_directory()
+
+ ##############################################
+
+ def join_directory(self, directory):
+
+ return self.__class__(os.path.join(self._path, str(directory)))
+
+ ##############################################
+
+ def join_filename(self, filename):
+
+ return File(filename, self._path)
+
+ ##############################################
+
+ def iter_file(self, followlinks=False):
+
+ if self.is_file():
+ yield File(self.filename_part(), self.directory_part())
+ else:
+ for root, directories, files in os.walk(self._path, followlinks=followlinks):
+ for filename in files:
+ yield File(filename, root)
+
+ ##############################################
+
+ def iter_directories(self, followlinks=False):
+
+ for root, directories, files in os.walk(self._path, followlinks=followlinks):
+ for directory in directories:
+ yield Path(os.path.join(root, directory))
+
+####################################################################################################
+
+class File(Path):
+
+ default_shasum_algorithm = 256
+
+ ##############################################
+
+ def __init__(self, filename, path=''):
+
+ super().__init__(os.path.join(str(path), str(filename)))
+
+ self._filename = self.filename_part()
+ if not self._filename:
+ raise ValueError
+ self._directory = self.directory_part()
+
+ self._shasum = None # lazy computation
+
+ ##############################################
+
+ def __bool__(self):
+
+ return super().__nonzero__() and os.path.isfile(self._path)
+
+ ##############################################
+
+ @property
+ def directory(self):
+
+ return self._directory
+
+ ##############################################
+
+ @property
+ def filename(self):
+
+ return self._filename
+
+ ##############################################
+
+ @property
+ def extension(self):
+
+ return file_extension(self._filename)
+
+ ##############################################
+
+ @property
+ def shasum(self):
+
+ if self._shasum is None:
+ return self.compute_shasum()
+ else:
+ return self._shasum
+
+ ##############################################
+
+ def compute_shasum(self, algorithm=None):
+
+ if algorithm is None:
+ algorithm = self.default_shasum_algorithm
+ self._shasum = run_shasum(self._path, algorithm, portable=True)
+
+ return self._shasum
diff --git a/third_party/PySpice-org/PySpice/PySpice/Tools/Path.py b/third_party/PySpice-org/PySpice/PySpice/Tools/Path.py
new file mode 100644
index 00000000..6038a948
--- /dev/null
+++ b/third_party/PySpice-org/PySpice/PySpice/Tools/Path.py
@@ -0,0 +1,53 @@
+####################################################################################################
+#
+# 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 .
+#
+####################################################################################################
+
+####################################################################################################
+
+import os
+
+####################################################################################################
+
+def to_absolute_path(path):
+
+ # Expand ~ . and Remove trailing '/'
+
+ return os.path.abspath(os.path.expanduser(path))
+
+####################################################################################################
+
+def parent_directory_of(file_name, step=1):
+
+ directory = file_name
+ for i in range(step):
+ directory = os.path.dirname(directory)
+ return directory
+
+####################################################################################################
+
+def find(file_name, directories):
+
+ if isinstance(directories, bytes):
+ directories = (directories,)
+ for directory in directories:
+ for directory_path, sub_directories, file_names in os.walk(directory):
+ if file_name in file_names:
+ return os.path.join(directory_path, file_name)
+
+ raise NameError("File %s not found in directories %s" % (file_name, str(directories)))
diff --git a/third_party/PySpice-org/PySpice/PySpice/Tools/StringTools.py b/third_party/PySpice-org/PySpice/PySpice/Tools/StringTools.py
new file mode 100644
index 00000000..05a5c992
--- /dev/null
+++ b/third_party/PySpice-org/PySpice/PySpice/Tools/StringTools.py
@@ -0,0 +1,84 @@
+####################################################################################################
+#
+# 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 .
+#
+####################################################################################################
+
+__all__ = [
+ 'join_dict',
+ 'join_lines'
+ 'join_list',
+ 'str_spice',
+ 'str_spice_list',
+]
+
+####################################################################################################
+
+import os
+
+####################################################################################################
+
+from PySpice.Unit.Unit import UnitValue
+
+####################################################################################################
+
+def str_spice(obj, unit=True):
+
+ # Fixme: right place ???
+
+ """Convert an object to a Spice compatible string."""
+
+ if isinstance(obj, UnitValue):
+ if unit:
+ return obj.str_spice()
+ else: # Fixme: ok ???
+ return obj.str(spice=False, space=False, unit=False)
+ else:
+ return str(obj)
+
+####################################################################################################
+
+def str_spice_list(*args):
+ return [str_spice(x) for x in args]
+
+####################################################################################################
+
+def join_lines(items, prefix=''):
+ return os.linesep.join([prefix + str(item)
+ for item in items
+ if item is not None]) # Fixme: and item
+
+####################################################################################################
+
+def join_list(items):
+ # return ' '.join([str_spice(item)
+ # for item in items
+ # if item is not None and str_spice(item)])
+ values = []
+ for item in items:
+ if item is not None:
+ str_value = str_spice(item)
+ if str_value:
+ values.append(str_value)
+ return ' '.join(values)
+
+####################################################################################################
+
+def join_dict(d):
+ return ' '.join(["{}={}".format(key, str_spice(value))
+ for key, value in sorted(d.items())
+ if value is not None])
diff --git a/third_party/PySpice-org/PySpice/PySpice/Tools/__init__.py b/third_party/PySpice-org/PySpice/PySpice/Tools/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/third_party/PySpice-org/PySpice/PySpice/Unit/SiUnits.py b/third_party/PySpice-org/PySpice/PySpice/Unit/SiUnits.py
new file mode 100644
index 00000000..34296151
--- /dev/null
+++ b/third_party/PySpice-org/PySpice/PySpice/Unit/SiUnits.py
@@ -0,0 +1,330 @@
+####################################################################################################
+#
+# PySpice - A Spice Package for Python
+# Copyright (C) 2017 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 .
+#
+####################################################################################################
+
+####################################################################################################
+
+"""This module defines SI prefixes and units.
+"""
+
+####################################################################################################
+
+from .Unit import UnitPrefix, SiBaseUnit, Unit
+
+####################################################################################################
+
+# Define SI unit prefixes
+
+class Yotta(UnitPrefix):
+ POWER = 24
+ PREFIX = 'Y'
+ SPICE_PREFIX = None
+
+class Zetta(UnitPrefix):
+ POWER = 21
+ PREFIX = 'Z'
+ SPICE_PREFIX = None
+
+class Exa(UnitPrefix):
+ POWER = 18
+ PREFIX = 'E'
+ SPICE_PREFIX = None
+
+class Peta(UnitPrefix):
+ POWER = 15
+ PREFIX = 'P'
+ SPICE_PREFIX = None
+
+class Tera(UnitPrefix):
+ POWER = 12
+ PREFIX = 'T'
+
+class Giga(UnitPrefix):
+ POWER = 9
+ PREFIX = 'G'
+
+class Mega(UnitPrefix):
+ POWER = 6
+ PREFIX = 'M'
+ SPICE_PREFIX = 'Meg'
+
+class Kilo(UnitPrefix):
+ POWER = 3
+ PREFIX = 'k'
+
+class Hecto(UnitPrefix):
+ POWER = 2
+ PREFIX = 'h'
+ SPICE_PREFIX = None
+
+class Deca(UnitPrefix):
+ POWER = 1
+ PREFIX = 'da'
+ SPICE_PREFIX = None
+
+class Milli(UnitPrefix):
+ POWER = -3
+ PREFIX = 'm'
+
+class Micro(UnitPrefix):
+ POWER = -6
+ PREFIX = 'μ'
+ SPICE_PREFIX = 'u'
+
+class Nano(UnitPrefix):
+ POWER = -9
+ PREFIX = 'n'
+
+class Pico(UnitPrefix):
+ POWER = -12
+ PREFIX = 'p'
+
+class Femto(UnitPrefix):
+ POWER = -15
+ PREFIX = 'f'
+ SPICE_PREFIX = None
+
+class Atto(UnitPrefix):
+ POWER = -18
+ PREFIX = 'a'
+ SPICE_PREFIX = None
+
+class Zepto(UnitPrefix):
+ POWER = -21
+ PREFIX = 'z'
+ SPICE_PREFIX = None
+
+class Yocto(UnitPrefix):
+ POWER = -24
+ PREFIX = 'y'
+ SPICE_PREFIX = None
+
+# Fixme: ngspice defines mil
+
+####################################################################################################
+
+# Define SI units
+
+class Metre(SiBaseUnit):
+ UNIT_NAME = 'metre'
+ UNIT_SUFFIX = 'm'
+ QUANTITY = 'length'
+
+class Kilogram(SiBaseUnit):
+ UNIT_NAME = 'kilogram'
+ UNIT_SUFFIX = 'kg'
+ QUANTITY = 'mass'
+
+class Second(SiBaseUnit):
+ UNIT_NAME = 'second'
+ UNIT_SUFFIX = 's'
+ QUANTITY = 'time'
+ IS_SI = True
+
+class Ampere(SiBaseUnit):
+ UNIT_NAME = 'ampere'
+ UNIT_SUFFIX = 'A'
+ QUANTITY = 'electric current'
+
+class Kelvin(SiBaseUnit):
+ UNIT_NAME = 'kelvin'
+ UNIT_SUFFIX = 'K'
+ QUANTITY = 'thermodynamic temperature'
+
+class Mole(SiBaseUnit):
+ UNIT_NAME = 'mole'
+ UNIT_SUFFIX = 'mol'
+ QUANTITY = 'amount of substance'
+
+class Candela(SiBaseUnit):
+ UNIT_NAME = 'candela'
+ UNIT_SUFFIX = 'cd'
+ QUANTITY = 'luminosity intensity'
+
+####################################################################################################
+
+# Define Derived units
+
+class Radian(Unit):
+ UNIT_NAME = 'radian'
+ UNIT_SUFFIX = 'rad'
+ QUANTITY = 'angle'
+ SI_UNIT = 'm*m^-1'
+ DEFAULT_UNIT = True
+
+class Steradian(Unit):
+ UNIT_NAME = 'steradian'
+ UNIT_SUFFIX = 'sr'
+ QUANTITY = 'solid angle'
+ SI_UNIT = 'm^2*m^-2'
+ DEFAULT_UNIT = True
+
+class Hertz(Unit):
+ UNIT_NAME = 'frequency'
+ UNIT_SUFFIX = 'Hz'
+ QUANTITY = 'frequency'
+ SI_UNIT = 's^-1'
+ DEFAULT_UNIT = True
+
+class Newton(Unit):
+ UNIT_NAME = 'newton'
+ UNIT_SUFFIX = 'N'
+ QUANTITY = 'force'
+ SI_UNIT = 'kg*m*s^-2'
+ DEFAULT_UNIT = True
+
+class Pascal(Unit):
+ UNIT_NAME = 'pascal'
+ UNIT_SUFFIX = 'Pa'
+ QUANTITY = 'pressure'
+ SI_UNIT = 'kg*m^-1*s^-2'
+ DEFAULT_UNIT = True
+ # N/m^2
+
+class Joule(Unit):
+ UNIT_NAME = 'joule'
+ UNIT_SUFFIX = 'J'
+ QUANTITY = 'energy'
+ SI_UNIT = 'kg*m^2*s^-2'
+ DEFAULT_UNIT = True
+ # N*m
+
+class Watt(Unit):
+ UNIT_NAME = 'watt'
+ UNIT_SUFFIX = 'W'
+ QUANTITY = 'power'
+ SI_UNIT = 'kg*m^2*s^-3'
+ DEFAULT_UNIT = True
+ # J/s
+
+class Coulomb(Unit):
+ UNIT_NAME = 'coulomb'
+ UNIT_SUFFIX = 'C'
+ QUANTITY = 'electric charge'
+ SI_UNIT = 's*A'
+ DEFAULT_UNIT = True
+
+class Volt(Unit):
+ UNIT_NAME = 'volt'
+ UNIT_SUFFIX = 'V'
+ QUANTITY = 'voltage'
+ SI_UNIT = 'kg*m^2*s^-3*A^-1'
+ DEFAULT_UNIT = True
+ # W/A
+
+class Farad(Unit):
+ UNIT_NAME = 'farad'
+ UNIT_SUFFIX = 'F'
+ QUANTITY = 'capacitance'
+ SI_UNIT = 'kg^-1*m^-2*s^4*A^2'
+ DEFAULT_UNIT = True
+ # C/V
+
+class Ohm(Unit):
+ UNIT_NAME = 'ohm'
+ UNIT_SUFFIX = 'Ω'
+ QUANTITY = 'electric resistance, impedance, reactance'
+ SI_UNIT = 'kg*m^2*s^-3*A^-2'
+ DEFAULT_UNIT = True
+ # V/A
+
+class Siemens(Unit):
+ UNIT_NAME = 'siemens'
+ UNIT_SUFFIX = 'S'
+ QUANTITY = 'electrical conductance'
+ SI_UNIT = 'kg^-1*m^-2*s^3*A^2'
+ DEFAULT_UNIT = True
+ # A/V
+
+class Weber(Unit):
+ UNIT_NAME = 'weber'
+ UNIT_SUFFIX = 'Wb'
+ QUANTITY = 'magnetic flux'
+ SI_UNIT = 'kg*m^2*s^-2*A^-1'
+ DEFAULT_UNIT = True
+ # V*s
+
+class Tesla(Unit):
+ UNIT_NAME = 'tesla'
+ UNIT_SUFFIX = ''
+ QUANTITY = 'T'
+ SI_UNIT = 'kg*s^-2*A^-1'
+ DEFAULT_UNIT = True
+ # Wb/m2
+
+class Henry(Unit):
+ UNIT_NAME = 'henry'
+ UNIT_SUFFIX = 'H'
+ QUANTITY = 'inductance'
+ SI_UNIT = 'kg*m^2*s^-2*A^-2'
+ DEFAULT_UNIT = True
+ # Wb/A
+
+class DegreeCelcius(Unit):
+ UNIT_NAME = 'degree celcuis'
+ UNIT_SUFFIX = '°C'
+ QUANTITY = 'temperature relative to 273.15 K'
+ SI_UNIT = 'K'
+
+class Lumen(Unit):
+ UNIT_NAME = 'lumen'
+ UNIT_SUFFIX = 'lm'
+ QUANTITY = 'luminous flux'
+ SI_UNIT = 'cd'
+ # cd*sr
+
+class Lux(Unit):
+ UNIT_NAME = 'lux'
+ UNIT_SUFFIX = 'lx'
+ QUANTITY = 'illuminance'
+ SI_UNIT = 'm^-2*cd'
+ DEFAULT_UNIT = True
+ # lm/m2
+
+class Becquerel(Unit):
+ UNIT_NAME = 'becquerel'
+ UNIT_SUFFIX = 'Bq'
+ QUANTITY = 'radioactivity (decays per unit time)'
+ SI_UNIT = 's^-1' # same as Hertz
+
+class Gray(Unit):
+ UNIT_NAME = 'gray'
+ UNIT_SUFFIX = 'Gy'
+ QUANTITY = 'absorbed dose (of ionizing radiation)'
+ SI_UNIT = 'm^2*s^-2'
+ # J/kg
+
+class Sievert(Unit):
+ UNIT_NAME = 'sievert'
+ UNIT_SUFFIX = 'Sv'
+ QUANTITY = ' equivalent dose (of ionizing radiation)'
+ SI_UNIT = 'm^2*s^-2'
+
+class Katal(Unit):
+ UNIT_NAME = 'katal'
+ UNIT_SUFFIX = 'kat'
+ QUANTITY = 'catalytic activity'
+ SI_UNIT = 'mol*s^-1'
+ DEFAULT_UNIT = True
+
+####################################################################################################
+
+# class Mil(Unit):
+# SCALE = 25.4e-6 # mm
+# SPICE_SUFFIX = 'mil'
diff --git a/third_party/PySpice-org/PySpice/PySpice/Unit/Unit.py b/third_party/PySpice-org/PySpice/PySpice/Unit/Unit.py
new file mode 100644
index 00000000..8eae5175
--- /dev/null
+++ b/third_party/PySpice-org/PySpice/PySpice/Unit/Unit.py
@@ -0,0 +1,1944 @@
+####################################################################################################
+#
+# 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 .
+#
+####################################################################################################
+
+####################################################################################################
+
+"""This module implements units.
+
+A shortcut is defined for each unit prefix, e.g. :class:`pico`, :class:`nano`, :class:`micro`,
+:class:`milli`, :class:`kilo`, :class:`mega`, :class:`tera`.
+
+"""
+
+# https://numpy.org/doc/stable/user/basics.subclassing.html#basics-subclassing
+
+####################################################################################################
+
+import logging
+
+import collections.abc as collections
+import math
+# import numbers
+
+import numpy as np
+
+####################################################################################################
+
+from PySpice.Tools.EnumFactory import EnumFactory
+
+####################################################################################################
+
+_module_logger = logging.getLogger(__name__)
+
+####################################################################################################
+
+class UnitPrefixMetaclass(type):
+
+ """Metaclass to register unit prefixes"""
+
+ _prefixes = {} # singletons
+
+ ##############################################
+
+ def __new__(meta, class_name, base_classes, attributes):
+ cls = type.__new__(meta, class_name, base_classes, attributes)
+ if class_name != 'UnitPrefix':
+ meta.register_prefix(cls)
+ return cls
+
+ ##############################################
+
+ @classmethod
+ def register_prefix(meta, cls):
+ power = cls.POWER
+ if power is None:
+ raise ValueError('Power is None for {}'.format(cls.__name__))
+ meta._prefixes[power] = cls()
+
+ ##############################################
+
+ @classmethod
+ def prefix_iter(cls):
+ return cls._prefixes.values()
+
+ ##############################################
+
+ @classmethod
+ def get(cls, power):
+ return cls._prefixes[power]
+
+####################################################################################################
+
+class UnitPrefix(metaclass=UnitPrefixMetaclass):
+
+ """This class implements a unit prefix like kilo"""
+
+ POWER = None
+ PREFIX = ''
+
+ ##############################################
+
+ def __repr__(self):
+ return '{}({}, {})'.format(self.__class__.__name__, self.POWER, self.PREFIX)
+
+ ##############################################
+
+ def __int__(self):
+ return self.POWER
+
+ ##############################################
+
+ def __str__(self):
+ return self.PREFIX
+
+ ##############################################
+
+ @property
+ def power(self):
+ return self.POWER
+
+ @property
+ def prefix(self):
+ return self.PREFIX
+
+ @property
+ def is_unit(self):
+ return self.POWER == 0
+
+ @property
+ def scale(self):
+ return 10**self.POWER
+
+ ##############################################
+
+ @property
+ def spice_prefix(self):
+ if hasattr(self, 'SPICE_PREFIX'):
+ return self.SPICE_PREFIX
+ else:
+ return self.PREFIX
+
+ ##############################################
+
+ @property
+ def is_defined_in_spice(self):
+ return self.spice_prefix is not None
+
+ ##############################################
+
+ def __eq__(self, other):
+ return self.POWER == other.POWER
+
+ ##############################################
+
+ def __ne__(self, other):
+ return self.POWER != other.POWER
+
+ ##############################################
+
+ def __lt__(self, other):
+ return self.POWER < other.POWER
+
+ ##############################################
+
+ def __gt__(self, other):
+ return self.POWER > other.POWER
+
+ ##############################################
+
+ def str(self, spice=False):
+ if spice:
+ return self.spice_prefix
+ else:
+ return self.PREFIX
+
+####################################################################################################
+
+class ZeroPower(UnitPrefix):
+ POWER = 0
+ PREFIX = ''
+ SPICE_PREFIX = ''
+
+_zero_power = UnitPrefixMetaclass.get(0)
+
+####################################################################################################
+
+class SiDerivedUnit:
+
+ """This class implements a unit defined as powers of SI base units.
+ """
+
+ # SI base units
+ BASE_UNITS = (
+ 'm',
+ 'kg',
+ 's',
+ 'A',
+ 'K',
+ 'mol',
+ 'cd',
+ )
+
+ ##############################################
+
+ def __init__(self, string=None, powers=None):
+
+ if powers is not None:
+ self._powers = self.new_powers()
+ self._powers.update(powers)
+ elif string is not None:
+ self._powers = self.parse_si(string)
+ else:
+ self._powers = self.new_powers()
+
+ self._hash = self.to_hash(self._powers)
+ self._string = self.to_string(self._powers)
+
+ ##############################################
+
+ @property
+ def powers(self):
+ return self._powers
+
+ @property
+ def hash(self):
+ return self._hash
+
+ @property
+ def string(self):
+ return self._string
+
+ def __str__(self):
+ return self._string
+
+ def __repr__(self):
+ return '{}({})'.format(self.__class__.__name__, self._string)
+
+ ##############################################
+
+ @classmethod
+ def new_powers(cls):
+ return {unit: 0 for unit in cls.BASE_UNITS}
+
+ ##############################################
+
+ @classmethod
+ def parse_si(cls, string):
+ si_powers = cls.new_powers()
+ if string:
+ for prefixed_units in string.split('*'):
+ parts = prefixed_units.split('^')
+ unit = parts[0]
+ if len(parts) == 1:
+ powers = 1
+ else:
+ powers = int(parts[1])
+ si_powers[unit] += powers
+ return si_powers
+
+ ##############################################
+
+ @classmethod
+ def to_hash(cls, powers):
+ hash_ = ''
+ for unit in cls.BASE_UNITS:
+ hash_ += str(powers[unit])
+ return hash_
+
+ ##############################################
+
+ @classmethod
+ def to_string(cls, si_powers):
+ units = []
+ for unit in cls.BASE_UNITS:
+ powers = si_powers[unit]
+ if powers == 1:
+ units.append(unit)
+ elif powers > 1 or powers < 0:
+ units.append('{}^{}'.format(unit, powers))
+ return '*'.join(units)
+
+ ##############################################
+
+ # @property
+ def is_base_unit(self):
+ count = 0
+ for powers in self._powers.values():
+ if powers == 1:
+ count += 1
+ elif powers != 0:
+ return False
+ return count == 1
+
+ ##############################################
+
+ # @property
+ def is_unit_less(self):
+ return self._hash == '0'*len(self.BASE_UNITS)
+
+ ##############################################
+
+ def __bool__(self):
+ return not self.is_unit_less()
+
+ ##############################################
+
+ def clone(self):
+ return self.__class__(powers=self._powers)
+
+ ##############################################
+
+ def __eq__(self, other):
+ return self._hash == other.hash
+
+ ##############################################
+
+ def __ne__(self, other):
+ return self._hash != other.hash
+
+ ##############################################
+
+ def __mul__(self, other):
+ powers = {unit: self._powers[unit] + other._powers[unit]
+ for unit in self.BASE_UNITS}
+ return self.__class__(powers=powers)
+
+ ##############################################
+
+ def __imul__(self, other):
+ for unit in self.BASE_UNITS:
+ self._powers[unit] += other.powers[unit]
+ self._hash = self.to_hash(self._powers)
+ self._string = self.to_string(self._powers)
+ return self
+
+ ##############################################
+
+ def __truediv__(self, other):
+ powers = {unit: self._powers[unit] - other._powers[unit]
+ for unit in self.BASE_UNITS}
+ return self.__class__(powers=powers)
+
+ ##############################################
+
+ def __itruediv__(self, other):
+ for unit in self.BASE_UNITS:
+ self._powers[unit] -= other.powers[unit]
+ self._hash = self.to_hash(self._powers)
+ self._string = self.to_string(self._powers)
+ return self
+
+ ##############################################
+
+ def power(self, value):
+ powers = {unit: self._powers[unit] * value
+ for unit in self.BASE_UNITS}
+ return self.__class__(powers=powers)
+
+ ##############################################
+
+ def reciprocal(self):
+ return self.power(-1)
+
+ ##############################################
+
+ def sqrt(self):
+ return self.power(1/2)
+
+ ##############################################
+
+ def square(self):
+ return self.power(2)
+
+ ##############################################
+
+ def cbrt(self):
+ return self.power(1/3)
+
+####################################################################################################
+
+class UnitMetaclass(type):
+
+ """Metaclass to register units"""
+
+ _units = {}
+ _hash_map = {}
+
+ ##############################################
+
+ def __new__(meta, class_name, base_classes, attributes):
+ cls = type.__new__(meta, class_name, base_classes, attributes)
+ meta.init_unit(cls)
+ meta.register_unit(cls)
+ return cls
+
+ ##############################################
+
+ @classmethod
+ def init_unit(meta, cls):
+
+ si_unit = cls.SI_UNIT
+ if not (isinstance(si_unit, SiDerivedUnit) and si_unit):
+ # si_unit is not defined
+ if cls.is_base_unit():
+ si_unit = SiDerivedUnit(cls.UNIT_SUFFIX)
+ else: # str
+ si_unit = SiDerivedUnit(si_unit)
+ cls.SI_UNIT = si_unit
+
+ ##############################################
+
+ @classmethod
+ def register_unit(meta, cls):
+
+ obj = cls()
+ meta._units[obj.unit_suffix] = obj
+
+ if obj.si_unit:
+ hash_ = obj.si_unit.hash
+ if hash_ in meta._hash_map:
+ meta._hash_map[hash_].append(obj)
+ else:
+ meta._hash_map[hash_] = [obj]
+
+ ##############################################
+
+ @classmethod
+ def unit_iter(meta):
+ return meta._units.values()
+
+ ##############################################
+
+ @classmethod
+ def from_prefix(meta, prefix):
+ return meta._units__.get(prefix, None)
+
+ ##############################################
+
+ @classmethod
+ def from_hash(meta, hash_):
+ return meta._hash_map.get(hash_, None)
+
+ ##############################################
+
+ @classmethod
+ def from_si_unit(meta, si_unit, unique=True):
+
+ # Fixme:
+ # - handle power of units
+ # unit -> numpy vector, divide and test for identical factor
+ # define unit, format as V^2
+ # - complex unit
+
+ units = meta._hash_map.get(si_unit.hash, None)
+ if unique and units is not None:
+ if len(units) > 1:
+ units = [unit for unit in units if unit.is_default_unit()]
+ if len(units) == 1:
+ return units[0]
+ else:
+ raise NameError("Unit clash", units)
+ else:
+ return units[0]
+ else:
+ return units
+
+####################################################################################################
+
+class UnitError(ValueError):
+ pass
+
+####################################################################################################
+
+class Unit(metaclass=UnitMetaclass):
+
+ """This class implements a unit.
+ """
+
+ UNIT_NAME = ''
+ UNIT_SUFFIX = ''
+ QUANTITY = ''
+ SI_UNIT = SiDerivedUnit()
+ DEFAULT_UNIT = False
+ # SPICE_SUFFIX = ''
+
+ _logger = _module_logger.getChild('Unit')
+
+ ##############################################
+
+ def __init__(self, si_unit=None):
+
+ self._unit_name = self.UNIT_NAME
+ self._unit_suffix = self.UNIT_SUFFIX
+ self._quantity = self.QUANTITY
+
+ if si_unit is None:
+ self._si_unit = self.SI_UNIT
+ else:
+ self._si_unit = si_unit
+
+ ##############################################
+
+ def __repr__(self):
+ return '{0}({1})'.format(self.__class__.__name__, str(self))
+
+ ##############################################
+
+ @property
+ def unit_name(self):
+ return self._unit_name
+
+ @property
+ def unit_suffix(self):
+ return self._unit_suffix
+
+ @property
+ def quantity(self):
+ return self._quantity
+
+ @property
+ def si_unit(self):
+ return self._si_unit
+
+ ##############################################
+
+ @property
+ def is_unit_less(self):
+ return self._si_unit.is_unit_less()
+
+ ##############################################
+
+ @classmethod
+ def is_default_unit(cls):
+ return cls.DEFAULT_UNIT
+
+ @classmethod
+ def is_base_unit(cls):
+ return False
+
+ ##############################################
+
+ def __eq__(self, other):
+ """self == other"""
+ return self._si_unit == other.si_unit
+
+ ##############################################
+
+ def __ne__(self, other):
+ """self != other"""
+ # The default __ne__ doesn't negate __eq__ until 3.0.
+ return not (self == other)
+
+ ##############################################
+
+ def _equivalent_prefixed_unit(self, si_unit):
+
+ equivalent_unit = PrefixedUnit.from_si_unit(si_unit)
+ if equivalent_unit is not None:
+ return equivalent_unit
+ else:
+ return PrefixedUnit(Unit(si_unit))
+
+ ##############################################
+
+ def _equivalent_unit(self, si_unit):
+
+ equivalent_unit = UnitMetaclass.from_si_unit(si_unit)
+ if equivalent_unit is not None:
+ return equivalent_unit
+ else:
+ return Unit(si_unit)
+
+ ##############################################
+
+ def _equivalent_unit_or_power(self, si_unit, prefixed_unit):
+ if prefixed_unit:
+ return self._equivalent_prefixed_unit(si_unit)
+ else:
+ return self._equivalent_unit(si_unit)
+
+ ##############################################
+
+ def multiply(self, other, prefixed_unit=False):
+ si_unit = self._si_unit * other.si_unit
+ return self._equivalent_unit_or_power(si_unit, prefixed_unit)
+
+ ##############################################
+
+ def divide(self, other, prefixed_unit=False):
+ si_unit = self._si_unit / other.si_unit
+ return self._equivalent_unit_or_power(si_unit, prefixed_unit)
+
+ ##############################################
+
+ def power(self, exponent, prefixed_unit=False):
+ si_unit = self._si_unit.power(exponent)
+ return self._equivalent_unit_or_power(si_unit, prefixed_unit)
+
+ ##############################################
+
+ def reciprocal(self, prefixed_unit=False):
+ si_unit = self._si_unit.reciprocal()
+ return self._equivalent_unit_or_power(si_unit, prefixed_unit)
+
+ ##############################################
+
+ def sqrt(self, prefixed_unit=False):
+ si_unit = self._si_unit.sqrt()
+ return self._equivalent_unit_or_power(si_unit, prefixed_unit)
+
+ ##############################################
+
+ def square(self, prefixed_unit=False):
+ si_unit = self._si_unit.square()
+ return self._equivalent_unit_or_power(si_unit, prefixed_unit)
+
+ ##############################################
+
+ def cbrt(self, prefixed_unit=False):
+ si_unit = self._si_unit.cbrt()
+ return self._equivalent_unit_or_power(si_unit, prefixed_unit)
+
+ ##############################################
+
+ def __str__(self):
+ if self._unit_suffix:
+ return self._unit_suffix
+ else:
+ return str(self._si_unit)
+
+ ##############################################
+
+ def is_same_unit(self, value):
+ return value.unit == self
+
+ ##############################################
+
+ def validate(self, value, none=False):
+
+ if none and value is None:
+ return None
+ if isinstance(value, UnitValue):
+ if self.is_same_unit(value):
+ return value
+ else:
+ raise UnitError
+ else:
+ prefixed_unit = PrefixedUnit.from_prefixed_unit(self)
+ return prefixed_unit.new_value(value)
+
+####################################################################################################
+
+class SiBaseUnit(Unit):
+
+ """This class implements an SI base unit."""
+
+ ##############################################
+
+ @classmethod
+ def is_base_unit(cls):
+ return True
+
+ ##############################################
+
+ @classmethod
+ def is_default_unit(cls):
+ return True
+
+####################################################################################################
+
+class PrefixedUnit:
+
+ """This class implements a prefixed unit.
+ """
+
+ _unit_map = {} # Prefixed unit singletons
+ _prefixed_unit_map = {}
+
+ _value_ctor = None
+ _values_ctor = None
+
+ ##############################################
+
+ @classmethod
+ def register(cls, prefixed_unit):
+ unit = prefixed_unit.unit
+ unit_prefix = prefixed_unit.power
+ if unit_prefix.is_unit and unit.is_default_unit():
+ key = unit.si_unit.hash
+ # print('Register', key, prefixed_unit)
+ cls._unit_map[key] = prefixed_unit
+ if unit.unit_suffix:
+ unit_key = str(unit)
+ else:
+ unit_key = '_'
+ power_key = unit_prefix.power
+ # print('Register', unit_key, power_key, prefixed_unit)
+ if unit_key not in cls._prefixed_unit_map:
+ cls._prefixed_unit_map[unit_key] = {}
+ cls._prefixed_unit_map[unit_key][power_key] = prefixed_unit
+
+ ##############################################
+
+ @classmethod
+ def from_si_unit(cls, si_unit):
+ return cls._unit_map.get(si_unit.hash, None)
+
+ ##############################################
+
+ @classmethod
+ def from_prefixed_unit(cls, unit, power=0):
+
+ if unit.unit_suffix:
+ unit_key = str(unit)
+ else:
+ if power == 0:
+ return _simple_prefixed_unit
+ unit_key = '_'
+ try:
+ return cls._prefixed_unit_map[unit_key][power]
+ except KeyError:
+ return None
+
+ ##############################################
+
+ def __init__(self, unit=None, power=None, value_ctor=None, values_ctor=None):
+
+ if unit is None:
+ self._unit = Unit()
+ else:
+ self._unit = unit
+ if power is None:
+ self._power = _zero_power
+ else:
+ self._power = power
+
+ if value_ctor is not None:
+ self._value_ctor = value_ctor
+
+ if values_ctor is not None:
+ self._values_ctor = values_ctor
+
+ ##############################################
+
+ def __repr__(self):
+ return '{0}({1})'.format(self.__class__.__name__, str(self))
+
+ ##############################################
+
+ @property
+ def unit(self):
+ return self._unit
+
+ @property
+ def power(self):
+ return self._power
+
+ @property
+ def scale(self):
+ return self._power.scale
+
+ ##############################################
+
+ @property
+ def is_unit_less(self):
+ return self._unit.is_unit_less
+
+ ##############################################
+
+ def clone(self):
+ return self.__class__(self._unit, self._power)
+
+ ##############################################
+
+ def is_same_unit(self, other):
+ return self._unit == other.unit
+
+ ##############################################
+
+ def check_unit(self, other):
+ if not self.is_same_unit(other):
+ raise UnitError('{} versus {}'.format(self, other))
+
+ ##############################################
+
+ def is_same_power(self, other):
+ return self._power == other.power
+
+ ##############################################
+
+ def __eq__(self, other):
+ """self == other"""
+ return self.is_same_unit(other) and self.is_same_power(other)
+
+ ##############################################
+
+ def __ne__(self, other):
+ """self != other"""
+ # The default __ne__ doesn't negate __eq__ until 3.0.
+ return not (self == other)
+
+ ##############################################
+
+ def str(self, spice=False, unit=True):
+
+ # Ngspice User Manual Section 2.3.1 Some naming conventions
+ #
+ # Letters immediately following a number that are not scale factors are ignored, and
+ # letters immediately following a scale factor are ignored.
+ #
+ # Hence, 10, 10V, 10Volts, and 10Hz all represent the same number, and
+ # M, MA, MSec, and MMhos all represent the same scale factor.
+ #
+ # Note that 1000, 1000.0, 1000Hz, 1e3, 1.0e3, 1kHz, and 1k all represent the same number.
+
+ # >>> WARNING <<<
+ # Note that M or m denote ’milli’, i.e. 10−3 . Suffix meg has to be used for 106.
+ # see SPICE_PREFIX in SiUnits
+
+ # Fixme: unit clash, e.g. mm ???
+
+ string = self._power.str(spice)
+
+ if unit:
+ string += str(self._unit)
+
+ if spice:
+ # F is interpreted as f = femto
+ if string == 'F':
+ string = ''
+ else:
+ # Ngspice don't support utf-8
+ # degree symbole can be encoded str(176) in Extended ASCII
+ string = string.replace('°', '') # U+00B0
+ string = string.replace('℃', '') # U+2103
+ # U+2109 ℉
+ string = string.replace('Ω', 'Ohm') # U+CEA0
+ string = string.replace('μ', 'u') # U+CEBC
+
+ return string
+
+ ##############################################
+
+ def str_spice(self):
+ return self.str(spice=True, unit=True)
+
+ ##############################################
+
+ def __str__(self):
+ return self.str(spice=False, unit=True)
+
+ ##############################################
+
+ def new_value(self, value):
+ if isinstance(value, np.ndarray):
+ return self._values_ctor.from_ndarray(value, self)
+ elif isinstance(value, collections.Iterable):
+ return [self._value_ctor(self, x) for x in value]
+ else:
+ return self._value_ctor(self, value)
+
+####################################################################################################
+
+class UnitValue: # numbers.Real
+
+ """This class implements a value with a unit and a power (prefix).
+
+ The value is not converted to float if the value is an int.
+ """
+
+ _logger = _module_logger.getChild('UnitValue')
+
+ ##############################################
+
+ @classmethod
+ def simple_value(cls, value):
+ return cls(_simple_prefixed_unit, value)
+
+ ##############################################
+
+ def __init__(self, prefixed_unit, value):
+
+ self._prefixed_unit = prefixed_unit
+
+ if isinstance(value, UnitValue):
+ # Fixme: anonymous ???
+ if not self.is_same_unit(value):
+ raise UnitError
+ if self.is_same_power(value):
+ self._value = value.value
+ else:
+ self._value = self._convert_scalar_value(value)
+ elif isinstance(value, int):
+ self._value = value # to keep as int
+ else:
+ self._value = float(value)
+
+ ##############################################
+
+ def __repr__(self):
+ return '{0}({1})'.format(self.__class__.__name__, str(self))
+
+ ##############################################
+
+ @property
+ def prefixed_unit(self):
+ return self._prefixed_unit
+
+ @property
+ def unit(self):
+ return self._prefixed_unit.unit
+
+ @property
+ def power(self):
+ return self._prefixed_unit.power
+
+ @property
+ def scale(self):
+ return self._prefixed_unit.power.scale
+
+ @property
+ def value(self):
+ return self._value
+
+ ##############################################
+
+ def clone(self):
+ return self.__class__(self._prefixed_unit, self._value)
+
+ ##############################################
+
+ def clone_prefixed_unit(self, value):
+ return self.__class__(self._prefixed_unit, value)
+
+ ##############################################
+
+ # def to_unit_values(self):
+ # return self._prefixed_unit.new_value(self._value)
+
+ ##############################################
+
+ # def clone_unit(self, value, power):
+ # return self.__class__(PrefixedUnit(self.unit, power), value)
+
+ ##############################################
+
+ def is_same_unit(self, other):
+ return self._prefixed_unit.is_same_unit(other.prefixed_unit)
+
+ ##############################################
+
+ def _check_unit(self, other):
+ if not self.is_same_unit(other):
+ raise UnitError
+
+ ##############################################
+
+ def is_same_power(self, other):
+ return self._prefixed_unit.is_same_power(other.prefixed_unit)
+
+ ##############################################
+
+ def __eq__(self, other):
+ """self == other"""
+ if isinstance(other, UnitValue):
+ return self.is_same_unit(other) and float(self) == float(other)
+ else:
+ return float(self) == float(other)
+
+ ##############################################
+
+ def __ne__(self, other):
+ """self != other"""
+ # The default __ne__ doesn't negate __eq__ until 3.0.
+ return not (self == other)
+
+ ##############################################
+
+ def _convert_value(self, other):
+ """Convert the value of other to the power of self."""
+ self._check_unit(other)
+ if self.is_same_power(other):
+ return other.value
+ else:
+ return other.value * (other.scale / self.scale) # for numerical precision
+
+ ##############################################
+
+ def _convert_scalar_value(self, value):
+ return float(value) / self.scale
+
+ ##############################################
+
+ def __int__(self):
+ return int(self._value * self.scale)
+
+ ##############################################
+
+ def __float__(self):
+ return float(self._value * self.scale)
+
+ ##############################################
+
+ def str(self, spice=False, space=False, unit=True):
+ string = str(self._value)
+ if space:
+ string += ' '
+ string += self._prefixed_unit.str(spice, unit)
+ return string
+
+ ##############################################
+
+ def str_space(self):
+ return self.str(space=True)
+
+ ##############################################
+
+ def str_spice(self):
+ return self.str(spice=True, space=False, unit=True)
+
+ ##############################################
+
+ def __str__(self):
+ return self.str(spice=False, space=True, unit=True)
+
+ ##############################################
+
+ def __bool__(self):
+ """True if self != 0. Called for bool(self)."""
+ return self._value != 0
+
+ ##############################################
+
+ def __add__(self, other):
+ """self + other"""
+ if (isinstance(other, UnitValue)):
+ self._check_unit(other)
+ new_obj = self.clone()
+ new_obj._value += self._convert_value(other)
+ return new_obj
+ else:
+ return float(self) + other
+
+ ##############################################
+
+ def __iadd__(self, other):
+ """self += other"""
+ self._check_unit(other)
+ self._value += self._convert_value(other)
+ return self
+
+ ##############################################
+
+ def __radd__(self, other):
+ """other + self"""
+ return float(self) + other
+
+ ##############################################
+
+ def __neg__(self):
+ """-self"""
+ return self.clone_prefixed_unit(-self._value)
+
+ ##############################################
+
+ def __pos__(self):
+ """+self"""
+ return self.clone()
+
+ ##############################################
+
+ def __sub__(self, other):
+ """self - other"""
+ if (isinstance(other, UnitValue)):
+ self._check_unit(other)
+ new_obj = self.clone()
+ new_obj._value -= self._convert_value(other)
+ return new_obj
+ else:
+ return float(self) - other
+
+ ##############################################
+
+ def __isub__(self, other):
+ """self -= other"""
+ self._check_unit(other)
+ self._value -= self._convert_value(other)
+ return self
+
+ ##############################################
+
+ def __rsub__(self, other):
+ """other - self"""
+ return other - float(self)
+
+ ##############################################
+
+ def __mul__(self, other):
+ """self * other"""
+ if (isinstance(other, UnitValue)):
+ equivalent_unit = self.unit.multiply(other.unit, True)
+ value = float(self) * float(other)
+ return equivalent_unit.new_value(value)
+ else:
+ try: # scale value
+ scalar = float(other)
+ new_obj = self.clone()
+ new_obj._value *= scalar
+ return new_obj
+ except (ValueError, TypeError): # Numpy raises TypeError
+ return float(self) * other
+
+ ##############################################
+
+ def __imul__(self, other):
+ """self *= other"""
+ if (isinstance(other, UnitValue)):
+ raise UnitError
+ else: # scale value
+ # Fixme: right ?
+ self._value *= self._convert_value(other)
+ return self
+
+ ##############################################
+
+ def __rmul__(self, other):
+ """other * self"""
+ if (isinstance(other, UnitValue)):
+ raise NotImplementedError # Fixme: when ???
+ else: # scale value
+ return self.__mul__(other)
+
+ ##############################################
+
+ def __floordiv__(self, other):
+
+ """self // other """
+
+ if (isinstance(other, UnitValue)):
+ equivalent_unit = self.unit.divide(other.unit, True)
+ value = float(self) // float(other)
+ return equivalent_unit.new_value(value)
+ else:
+ try: # scale value
+ scalar = float(other)
+ new_obj = self.clone()
+ new_obj._value //= scalar
+ return new_obj
+ except (ValueError, TypeError): # Numpy raises TypeError
+ return float(self) // other
+
+ ##############################################
+
+ def __ifloordiv__(self, other):
+ """self //= other """
+ if (isinstance(other, UnitValue)):
+ raise NotImplementedError
+ else: # scale value
+ self._value //= float(other)
+ return self
+
+ ##############################################
+
+ def __rfloordiv__(self, other):
+ """other // self"""
+ if (isinstance(other, UnitValue)):
+ raise NotImplementedError # Fixme: when ???
+ else: # scale value
+ return other // float(self)
+
+ ##############################################
+
+ def __truediv__(self, other):
+
+ """self / other"""
+
+ if (isinstance(other, UnitValue)):
+ equivalent_unit = self.unit.divide(other.unit, True)
+ value = float(self) / float(other)
+ return equivalent_unit.new_value(value)
+ else:
+ try: # scale value
+ scalar = float(other)
+ new_obj = self.clone()
+ new_obj._value /= scalar
+ return new_obj
+ except (ValueError, TypeError): # Numpy raises TypeError
+ return float(self) / other
+
+ ##############################################
+
+ def __itruediv__(self, other):
+ """self /= other"""
+ if (isinstance(other, UnitValue)):
+ raise NotImplementedError
+ else: # scale value
+ self._value /= float(other)
+ return self
+
+ ##############################################
+
+ def __rtruediv__(self, other):
+ """other / self"""
+ if (isinstance(other, UnitValue)):
+ raise NotImplementedError # Fixme: when ???
+ else: # scale value
+ return other / float(self)
+
+ ##############################################
+
+ def __pow__(self, exponent):
+ """self**exponent; should promote to float or complex when necessary."""
+ new_obj = self.clone()
+ new_obj._value **= float(exponent)
+ return new_obj
+
+ ##############################################
+
+ def __ipow__(self, exponent):
+ self._value **= float(exponent)
+ return self
+
+ ##############################################
+
+ def __rpow__(self, base):
+ """base ** self"""
+ raise NotImplementedError
+
+ ##############################################
+
+ def __abs__(self):
+ """Returns the Real distance from 0. Called for abs(self)."""
+ return self.clone_prefixed_unit(abs(self._value))
+
+ ##############################################
+
+ def __trunc__(self):
+ """trunc(self): Truncates self to an Integral.
+
+ Returns an Integral i such that:
+ * i>0 iff self>0;
+ * abs(i) <= abs(self);
+ * for any Integral j satisfying the first two conditions,
+ abs(i) >= abs(j) [i.e. i has "maximal" abs among those].
+ i.e. "truncate towards 0".
+ """
+ raise NotImplementedError
+
+ ##############################################
+
+ def __divmod__(self, other):
+ """divmod(self, other): The pair (self // other, self % other).
+
+ Sometimes this can be computed faster than the pair of
+ operations.
+ """
+ return (self // other, self % other)
+
+ ##############################################
+
+ def __rdivmod__(self, other):
+ """divmod(other, self): The pair (self // other, self % other).
+
+ Sometimes this can be computed faster than the pair of
+ operations.
+ """
+ return (other // self, other % self)
+
+ ##############################################
+
+ def __mod__(self, other):
+ """self % other"""
+ raise NotImplementedError
+
+ ##############################################
+
+ def __rmod__(self, other):
+ """other % self"""
+ raise NotImplementedError
+
+ ##############################################
+
+ def __lt__(self, other):
+ """self < other
+
+ < on Reals defines a total ordering, except perhaps for NaN.
+
+ """
+ return float(self) < float(other)
+
+ ##############################################
+
+ def __le__(self, other):
+ """self <= other"""
+ return float(self) <= float(other)
+
+ ##############################################
+
+ def __ceil__(self):
+ return math.ceil(float(self))
+
+ ##############################################
+
+ def __floor__(self):
+ return math.floor(float(self))
+
+ ##############################################
+
+ def __round__(self):
+ return round(float(self))
+
+ ##############################################
+
+ def reciprocal(self):
+ equivalent_unit = self.unit.reciprocal(prefixed_unit=True)
+ reciprocal_value = 1. / float(self)
+ return equivalent_unit.new_value(reciprocal_value)
+
+ ##############################################
+
+ def get_prefixed_unit(self, power=0):
+ prefixed_unit = PrefixedUnit.from_prefixed_unit(self.unit, power)
+ if prefixed_unit is not None:
+ return prefixed_unit
+ else:
+ raise NameError("Prefixed unit not found for {} and power {}".format(self, power))
+
+ ##############################################
+
+ def convert(self, prefixed_unit):
+ """Convert the value to another power."""
+ self._prefixed_unit.check_unit(prefixed_unit)
+ if self._prefixed_unit.is_same_power(prefixed_unit):
+ return self
+ else:
+ value = float(self) / prefixed_unit.scale
+ return prefixed_unit.new_value(value)
+
+ ##############################################
+
+ def convert_to_power(self, power=0):
+ """Convert the value to another power."""
+ if power == 0:
+ value = float(self)
+ else:
+ value = float(self) / 10**power
+ return self.get_prefixed_unit(power).new_value(value)
+
+ ##############################################
+
+ def canonise(self):
+
+ # log10(10**n) = n log10(1) = 0 log10(10**-n) = -n log10(0) = -oo
+
+ try:
+ abs_value = abs(float(self))
+ log = math.log(abs_value)/math.log(1000)
+ # if abs_value >= 1:
+ # power = 3 * int(log)
+ # else:
+ # if log - int(log): # frac
+ # power = 3 * (int(log) -1)
+ # else:
+ # power = 3 * int(log)
+ power = int(log)
+ if abs_value < 1 and (log - int(log)):
+ power -= 1
+ power *= 3
+ # print('Unit.canonise', self, self._value, int(self._power), '->', float(self), power)
+ if power == int(self.power):
+ # print('Unit.canonise noting to do for', self)
+ return self
+ else:
+ # print('Unit.canonise convert', self, 'to', power)
+ # print('Unit.canonise convert', self, 'to', Unit)
+ return self.convert_to_power(power)
+ except Exception as e: # Fixme: fallback
+ self._logger.warning(e)
+ return self
+
+####################################################################################################
+
+class UnitValues(np.ndarray):
+
+ """This class implements a Numpy array with a unit and a power (prefix).
+
+ """
+
+ _logger = _module_logger.getChild('UnitValues')
+
+ CONVERSION = EnumFactory('ConversionType', (
+ 'NOT_IMPLEMENTED',
+ 'NO_CONVERSION',
+ 'FLOAT',
+ 'UNIT_MATCH',
+ 'UNIT_MATCH_NO_OUT_CAST',
+ 'NEW_UNIT'
+ ))
+
+ # Reference_documentation:
+ # https://docs.scipy.org/doc/numpy-1.13.0/reference/arrays.ndarray.html
+ # https://docs.scipy.org/doc/numpy-1.13.0/user/basics.subclassing.html
+ # https://docs.scipy.org/doc/numpy-1.13.0/reference/ufuncs.html
+ UFUNC_MAP = {
+ # Math operations
+ # --------------------------------------------------
+ np.add: CONVERSION.UNIT_MATCH,
+ np.subtract: CONVERSION.UNIT_MATCH,
+ np.multiply: CONVERSION.NEW_UNIT,
+ np.divide: CONVERSION.NEW_UNIT,
+ np.logaddexp: CONVERSION.FLOAT,
+ np.logaddexp2: CONVERSION.FLOAT,
+ np.true_divide: CONVERSION.NEW_UNIT,
+ np.floor_divide: CONVERSION.NEW_UNIT,
+ np.negative: CONVERSION.NO_CONVERSION,
+ np.positive: CONVERSION.NO_CONVERSION,
+ np.power: CONVERSION.NEW_UNIT,
+ np.remainder: CONVERSION.UNIT_MATCH,
+ np.mod: CONVERSION.UNIT_MATCH,
+ np.fmod: CONVERSION.UNIT_MATCH,
+ np.divmod: CONVERSION.UNIT_MATCH,
+ np.absolute: CONVERSION.NO_CONVERSION,
+ np.fabs: CONVERSION.NO_CONVERSION,
+ np.rint: CONVERSION.NO_CONVERSION,
+ np.sign: CONVERSION.NO_CONVERSION,
+ np.heaviside: CONVERSION.NOT_IMPLEMENTED, # !
+ np.conj: CONVERSION.NOT_IMPLEMENTED, # !
+ np.exp: CONVERSION.FLOAT,
+ np.exp2: CONVERSION.FLOAT,
+ np.log: CONVERSION.FLOAT,
+ np.log2: CONVERSION.FLOAT,
+ np.log10: CONVERSION.FLOAT,
+ np.expm1: CONVERSION.FLOAT,
+ np.log1p: CONVERSION.FLOAT,
+ np.sqrt: CONVERSION.NEW_UNIT,
+ np.square: CONVERSION.NEW_UNIT,
+ np.cbrt: CONVERSION.NEW_UNIT,
+ np.reciprocal: CONVERSION.NEW_UNIT,
+
+ # Trigonometric functions
+ # --------------------------------------------------
+ np.sin: CONVERSION.FLOAT,
+ np.cos: CONVERSION.FLOAT,
+ np.tan: CONVERSION.FLOAT,
+ np.arcsin: CONVERSION.FLOAT,
+ np.arccos: CONVERSION.FLOAT,
+ np.arctan: CONVERSION.FLOAT,
+ np.arctan2: CONVERSION.FLOAT,
+ np.hypot: CONVERSION.FLOAT,
+ np.sinh: CONVERSION.FLOAT,
+ np.cosh: CONVERSION.FLOAT,
+ np.tanh: CONVERSION.FLOAT,
+ np.arcsinh: CONVERSION.FLOAT,
+ np.arccosh: CONVERSION.FLOAT,
+ np.arctanh: CONVERSION.FLOAT,
+ np.deg2rad: CONVERSION.FLOAT,
+ np.rad2deg: CONVERSION.FLOAT,
+
+ # Bit-twiddling functions
+ # --------------------------------------------------
+ np.bitwise_and: CONVERSION.NOT_IMPLEMENTED, # Nonsense
+ np.bitwise_or: CONVERSION.NOT_IMPLEMENTED, # Nonsense
+ np.bitwise_xor: CONVERSION.NOT_IMPLEMENTED, # Nonsense
+ np.invert: CONVERSION.NOT_IMPLEMENTED, # Nonsense
+ np.left_shift: CONVERSION.NOT_IMPLEMENTED, # Nonsense
+ np.right_shift: CONVERSION.NOT_IMPLEMENTED, # Nonsense
+
+ # Comparison functions
+ # --------------------------------------------------
+ np.greater: CONVERSION.UNIT_MATCH_NO_OUT_CAST,
+ np.greater_equal: CONVERSION.UNIT_MATCH_NO_OUT_CAST,
+ np.less: CONVERSION.UNIT_MATCH_NO_OUT_CAST,
+ np.less_equal: CONVERSION.UNIT_MATCH_NO_OUT_CAST,
+ np.not_equal: CONVERSION.UNIT_MATCH_NO_OUT_CAST,
+ np.equal: CONVERSION.UNIT_MATCH_NO_OUT_CAST,
+
+ np.logical_and: CONVERSION.UNIT_MATCH,
+ np.logical_or: CONVERSION.UNIT_MATCH,
+ np.logical_xor: CONVERSION.UNIT_MATCH,
+ np.logical_not: CONVERSION.UNIT_MATCH,
+
+ np.maximum: CONVERSION.UNIT_MATCH,
+ np.minimum: CONVERSION.UNIT_MATCH,
+ np.fmax: CONVERSION.UNIT_MATCH,
+ np.fmin: CONVERSION.UNIT_MATCH,
+
+ # Floating functions
+ # --------------------------------------------------
+ np.isfinite: CONVERSION.NOT_IMPLEMENTED, # ! _T
+ np.isinf: CONVERSION.NOT_IMPLEMENTED, # ! _T
+ np.isnan: CONVERSION.NOT_IMPLEMENTED, # ! _T
+ np.fabs: CONVERSION.NOT_IMPLEMENTED, # ! _
+ np.signbit: CONVERSION.NOT_IMPLEMENTED, # ! _T
+ np.copysign: CONVERSION.NOT_IMPLEMENTED, # !
+ np.nextafter: CONVERSION.NOT_IMPLEMENTED, # !
+ np.spacing: CONVERSION.NOT_IMPLEMENTED, # !
+ np.modf: CONVERSION.NOT_IMPLEMENTED, # !
+ np.ldexp: CONVERSION.NOT_IMPLEMENTED, # !
+ np.frexp: CONVERSION.NOT_IMPLEMENTED, # !
+ np.fmod: CONVERSION.NOT_IMPLEMENTED, # !
+ np.floor: CONVERSION.NOT_IMPLEMENTED, # !
+ np.ceil: CONVERSION.NO_CONVERSION,
+ np.trunc: CONVERSION.NO_CONVERSION,
+
+ # Statistic functions
+ # --------------------------------------------------
+ np.mean: CONVERSION.NO_CONVERSION,
+ }
+
+ ##############################################
+
+ @classmethod
+ def from_ndarray(cls, array, prefixed_unit):
+
+ # cls._logger.debug('UnitValues.__new__ ' + str((cls, array, prefixed_unit)))
+
+ # obj = cls(prefixed_unit, array.shape, array.dtype) # Fixme: buffer ???
+ # obj[...] = array[...]
+
+ obj = array.view(UnitValues)
+ obj._prefixed_unit = prefixed_unit
+
+ if isinstance(array, UnitValues):
+ return array.convert(prefixed_unit)
+
+ return obj
+
+ ##############################################
+
+ def __new__(cls,
+ prefixed_unit,
+ shape, dtype=float, buffer=None, offset=0, strides=None, order=None):
+
+ # Called for explicit constructor
+ # obj = UnitValues(prefixed_unit, shape)
+
+ # cls._logger.debug('UnitValues.__new__ ' + str((cls, prefixed_unit, shape, dtype, buffer, offset, strides, order)))
+
+ obj = super(UnitValues, cls).__new__(cls, shape, dtype, buffer, offset, strides, order)
+ # obj = np.asarray(input_array).view(cls)
+
+ obj._prefixed_unit = prefixed_unit
+
+ return obj
+
+ ##############################################
+
+ def __array_finalize__(self, obj):
+
+ # self._logger.debug('UnitValues.__new__ ' + '\n {}'.format(obj))
+
+ # self is a new object resulting from ndarray.__new__(UnitValues, ...)
+ # therefore it only has attributes that the ndarray.__new__ constructor gave it
+ # i.e. those of a standard ndarray.
+
+ # We could have got to the ndarray.__new__ call in 3 ways:
+
+ # From an explicit constructor - e.g. UnitValues():
+ # obj is None
+ # we are in the middle of the UnitValues.__new__ constructor
+
+ if obj is None:
+ return
+
+ # From view casting - e.g arr.view(UnitValues):
+ # obj is arr
+ # type(obj) can be UnitValues
+
+ # From new-from-template - e.g infoarr[:3]
+ # type(obj) is UnitValues
+
+ self._prefixed_unit = getattr(obj, '_prefixed_unit', None) # Fixme: None
+
+ ##############################################
+
+ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
+
+ # - "ufunc" is the ufunc object that was called
+ # - "method" is a string indicating how the ufunc was called, either
+ # "__call__" to indicate it was called directly,
+ # or one of its "ufuncs.methods": "reduce", "accumulate", "reduceat", "outer", or "at".
+ # - "inputs" is a tuple of the input arguments to the ufunc
+ # - "kwargs" contains any optional or keyword arguments passed to the function.
+ # This includes any *out* arguments, which are always contained in a tuple.
+
+ # ufunc.reduce(a[, axis, dtype, out, keepdims]) Reduces a‘s dimension by one, by applying ufunc along one axis.
+ # ufunc.accumulate(array[, axis, dtype, out, ...]) Accumulate the result of applying the operator to all elements.
+ # ufunc.reduceat(a, indices[, axis, dtype, out]) Performs a (local) reduce with specified slices over a single axis.
+ # ufunc.outer(A, B, **kwargs) Apply the ufunc op to all pairs (a, b) with a in A and b in B.
+ # ufunc.at(a, indices[, b]) Performs unbuffered in place operation on operand ‘a’ for elements specified by ‘indices’.
+
+ # self._logger.debug(
+ # '\n self={}\n ufunc={}\n method={}\n inputs={}\n kwargs={}'
+ # .format(self, ufunc, method, inputs, kwargs))
+
+ # ufunc=
+ # method=__call__
+ # inputs=(UnitValues(mV, [0 1 2 3 4 5 6 7 8 9]), 2)
+
+ # ufunc=
+ # method=__call__
+ # inputs=(UnitValues(mV, [0 1 2 3 4 5 6 7 8 9]),)
+ # kwargs={}
+
+ # ufunc=
+ # method=__call__
+ # inputs=(UnitValues(mV, [0 1 2 3 4 5 6 7 8 9]), UnitValues(mV, [0 1 2 3 4 5 6 7 8 9]))
+
+ # ufunc=
+ # method=reduce
+ # inputs=(WaveForm [10 12 14 16 18 20 22 24 26 28]@mV,)
+
+ prefixed_unit = self._prefixed_unit
+
+ conversion = self.UFUNC_MAP[ufunc]
+ self._logger.debug("Conversion for {} is {}".format(ufunc, conversion))
+
+ # e.g. np.mean do an internal call to reduce
+ if method != '__call__':
+ conversion = self.CONVERSION.NO_CONVERSION
+
+ # Cast inputs to ndarray
+ args = []
+ if conversion == self.CONVERSION.NO_CONVERSION:
+ # should be 1 arg
+ args = [( input_.as_ndarray(False) if isinstance(input_, UnitValues) else input_ )
+ for input_ in inputs]
+ #
+ elif conversion == self.CONVERSION.FLOAT:
+ if not prefixed_unit.is_unit_less:
+ # raise ValueError("Must be unit less")
+ self._logger.warning("Should be unit less")
+ args = [( input_.as_ndarray(True) if isinstance(input_, UnitValues) else input_ )
+ for input_ in inputs]
+ #
+ elif conversion in (self.CONVERSION.UNIT_MATCH, self.CONVERSION.UNIT_MATCH_NO_OUT_CAST):
+ # len(inputs) == 2
+ other = inputs[1]
+ if isinstance(other, (UnitValues, UnitValue)):
+ self._check_unit(other)
+ args.append(self.as_ndarray())
+ nd_other = self._convert_value(other)
+ if isinstance(other, UnitValues):
+ nd_other = nd_other.as_ndarray()
+ elif isinstance(other, UnitValue):
+ nd_other = float(nd_other)
+ args.append(nd_other)
+ else:
+ raise ValueError
+ #
+ elif conversion == self.CONVERSION.NEW_UNIT:
+ if len(inputs) == 1:
+ #! Fixme: power
+ if ufunc == np.sqrt:
+ prefixed_unit = self.unit.sqrt(True)
+ elif ufunc == np.square:
+ prefixed_unit = self.unit.square(True)
+ elif ufunc == np.cbrt:
+ prefixed_unit = self.unit.cbrt(True)
+ elif ufunc == np.reciprocal:
+ prefixed_unit = self.unit.reciprocal(True)
+ else:
+ raise NotImplementedError
+ args.append(self.as_ndarray(True))
+ elif len(inputs) == 2:
+ other = inputs[1]
+ if isinstance(other, (UnitValues, UnitValue)):
+ if ufunc == np.multiply:
+ prefixed_unit = self.unit.multiply(other.unit, True)
+ elif ufunc in (np.divide, np.true_divide, np.floor_divide):
+ prefixed_unit = self.unit.divide(other.unit, True)
+ else:
+ raise NotImplementedError
+ args.append(self.as_ndarray(True))
+ if isinstance(other, UnitValue):
+ args.append(float(other))
+ else:
+ args.append(other.as_ndarray(True))
+ elif ufunc in (np.multiply, np.divide, np.true_divide, np.floor_divide, np.power):
+ if ufunc == np.power:
+ prefixed_unit = self.unit.power(other, True)
+ args.append(self.as_ndarray())
+ args.append(other)
+ else:
+ raise NotImplementedError
+ else:
+ raise NotImplementedError
+ #
+ else: # self.CONVERSION.NOT_IMPLEMENTED
+ raise NotImplementedError
+
+ # self._logger.debug("Output unit is {}".format(prefixed_unit))
+
+ # Cast outputs to ndarray
+ outputs = kwargs.pop('out', None)
+ if outputs:
+ out_args = []
+ for output in outputs:
+ if isinstance(output, UnitValues):
+ out_args.append(output.as_ndarray())
+ else:
+ out_args.append(output)
+ kwargs['out'] = tuple(out_args)
+ else:
+ outputs = (None,) * ufunc.nout
+
+ # Call ufunc
+ results = super(UnitValues, self).__array_ufunc__(ufunc, method, *args, **kwargs)
+ if results is NotImplemented:
+ return NotImplemented
+
+ # ensure results is a tuple
+ if ufunc.nout == 1:
+ results = (results,)
+
+ # Cast results
+ if conversion in (self.CONVERSION.FLOAT, self.CONVERSION.UNIT_MATCH_NO_OUT_CAST):
+ # Fixme: ok ???
+ results = tuple(( result if output is None else output )
+ for result, output in zip(results, outputs))
+ else:
+ results = tuple(( UnitValues.from_ndarray(np.asarray(result), prefixed_unit) if output is None else output )
+ for result, output in zip(results, outputs))
+
+ # list or scalar
+ return results[0] if len(results) == 1 else results
+
+ ##############################################
+
+# def __array_wrap__(self, out_array, context=None):
+#
+# self._logger.debug('\n self={}\n out_array={}\n context={}'.format(self, out_array, context))
+#
+# return super(UnitValues, self).__array_wrap__(out_array, context)
+
+ ##############################################
+
+ def as_ndarray(self, scale=False):
+ array = self.view(np.ndarray)
+ if scale:
+ return array * self.scale
+ else:
+ return array
+
+ ##############################################
+
+ def __getitem__(self, slice_):
+
+ value = super(UnitValues, self).__getitem__(slice_)
+
+ if isinstance(value, UnitValue): # slice
+ return value
+ else:
+ return self._prefixed_unit.new_value(value)
+
+ ##############################################
+
+ def __setitem__(self, slice_, value):
+
+ if isinstance(value, UnitValue):
+ self._check_unit(value)
+ value = self._convert_value(value).value
+ elif isinstance(value, UnitValues):
+ self._check_unit(value)
+ value = self._convert_value(value)
+
+ super(UnitValues, self).__setitem__(slice_, value)
+
+ ##############################################
+
+ # def __getstate__(self):
+ # # https://docs.python.org/3/library/pickle.html#object.__getstate__
+ # return {
+ # 'data': super(UnitValues, self).__getstate__(),
+ # 'prefixed_unit': self._prefixed_unit,
+ # }
+
+ ##############################################
+
+ def __reduce__(self):
+ # https://docs.python.org/3/library/pickle.html#object.__reduce__
+ np_state = super(UnitValues, self).__reduce__()
+ # ( ,
+ # (, (0,), b'b'),
+ # (1, (1, 1), dtype('float64'), False, b'\x00\x00\x80?\x00\x00\x80?') )
+ obj_state = (self._prefixed_unit,) + np_state[2]
+ return np_state[:2] + (obj_state,) + np_state[3:]
+
+ ##############################################
+
+ def __setstate__(self, state):
+ # https://docs.python.org/3/library/pickle.html#object.__setstate__
+ super(UnitValues, self).__setstate__(state[1:])
+ self._prefixed_unit = state[0]
+
+ ##############################################
+
+ def __contains__(self, value):
+ raise NotImplementedError
+
+ ##############################################
+
+ def __repr__(self):
+ # return repr(self.as_ndarray())
+ return '{}({})'.format(self.__class__.__name__, str(self))
+
+ ##############################################
+
+ @property
+ def prefixed_unit(self):
+ return self._prefixed_unit
+
+ @property
+ def unit(self):
+ return self._prefixed_unit.unit
+
+ @property
+ def power(self):
+ return self._prefixed_unit.power
+
+ @property
+ def scale(self):
+ return self._prefixed_unit.power.scale
+
+ ##############################################
+
+ def is_same_unit(self, other):
+ return self._prefixed_unit.is_same_unit(other.prefixed_unit)
+
+ ##############################################
+
+ def _check_unit(self, other):
+ if not self.is_same_unit(other):
+ raise UnitError
+
+ ##############################################
+
+ def is_same_power(self, other):
+ return self._prefixed_unit.is_same_power(other.prefixed_unit)
+
+ ##############################################
+
+ def __eq__(self, other):
+ """self == other"""
+ if isinstance(other, UnitValues):
+ return self.is_same_unit(other) and self.as_ndarray() == other.as_ndarray()
+ else:
+ raise ValueError
+
+ ##############################################
+
+ def _convert_value(self, other):
+ """Convert the value of other to the power of self."""
+ self._check_unit(other)
+ if self.is_same_power(other):
+ return other
+ else:
+ return other * (other.scale / self.scale) # for numerical precision
+
+ ##############################################
+
+ def __str__(self):
+ return str(self.as_ndarray()) + '@' + str(self._prefixed_unit)
+
+ ##############################################
+
+ def reciprocal(self):
+ equivalent_unit = self.unit.reciprocal(prefixed_unit=True)
+ reciprocal_value = 1. / np.as_ndarray(True)
+ return self.from_ndarray(reciprocal_value, equivalent_unit)
+
+ ##############################################
+
+ def get_prefixed_unit(self, power=0):
+ prefixed_unit = PrefixedUnit.from_prefixed_unit(self.unit, power)
+ if prefixed_unit is not None:
+ return prefixed_unit
+ else:
+ raise NameError("Prefixed unit not found for {} and power {}".format(self, power))
+
+ ##############################################
+
+ def convert(self, prefixed_unit):
+ """Convert the value to another power."""
+ self._prefixed_unit.check_unit(prefixed_unit)
+ if self._prefixed_unit.is_same_power(prefixed_unit):
+ return self
+ else:
+ value = self.as_ndarray(True) / prefixed_unit.scale
+ return prefixed_unit.new_value(value)
+
+ ##############################################
+
+ def convert_to_power(self, power=0):
+ """Convert the value to another power."""
+ value = self.as_ndarray(True)
+ if power != 0:
+ value /= 10**power
+ return self.get_prefixed_unit(power).new_value(value)
+
+####################################################################################################
+
+# Reset
+PrefixedUnit._value_ctor = UnitValue
+PrefixedUnit._values_ctor = UnitValues
+
+_simple_prefixed_unit = PrefixedUnit()
+
+####################################################################################################
+
+class FrequencyMixin:
+
+ """ This class implements a frequency mixin. """
+
+ ##############################################
+
+ @property
+ def period(self):
+ r""" Return the period :math:`T = \frac{1}{f}`. """
+ return self.reciprocal()
+
+ ##############################################
+
+ @property
+ def pulsation(self):
+ r""" Return the pulsation :math:`\omega = 2\pi f`. """
+ # Fixme: UnitValues
+ return float(self * 2 * math.pi)
+
+####################################################################################################
+
+class PeriodMixin:
+
+ """ This class implements a period mixin. """
+
+ ##############################################
+
+ @property
+ def frequency(self):
+ r""" Return the period :math:`f = \frac{1}{T}`. """
+ return self.reciprocal()
+
+ ##############################################
+
+ @property
+ def pulsation(self):
+ r""" Return the pulsation :math:`\omega = \frac{2\pi}{T}`. """
+ return self.frequency.pulsation
diff --git a/third_party/PySpice-org/PySpice/PySpice/Unit/__init__.py b/third_party/PySpice-org/PySpice/PySpice/Unit/__init__.py
new file mode 100644
index 00000000..b714c0c9
--- /dev/null
+++ b/third_party/PySpice-org/PySpice/PySpice/Unit/__init__.py
@@ -0,0 +1,244 @@
+####################################################################################################
+#
+# PySpice - A Spice Package for Python
+# Copyright (C) 2017 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 .
+#
+####################################################################################################
+
+# Note: This module should be outsourced, only code specific to SPICE must remain.
+
+"""This module implements units.
+
+Shortcuts are defined to build unit values easily :
+
+ * for each unit prefix, e.g. :func:`pico`, :func:`nano`, :func:`micro`, :func:`milli`, :func:`kilo`,
+ :func:`mega`, :func:`tera`. These shortcuts return unit less values.
+
+ * for each unit and prefix as the concatenation of *u_*, the unit prefix and the
+ unit suffix, e.g. :func:`u_pV`, :func:`u_nV`, :func:`u_uV` :func:`u_mV`, :func:`u_V`,
+ :func:`u_kV`, :func:`u_MV`, :func:`u_TV`.
+
+Theses unit value constructors accept int, float, object that can be converted to float,
+:class:`UnitValue` instance and an iterable on these types.
+
+A shortcut is defined to check an unit value match a particular unit, e.g. :func:`as_V`. Theses
+shortcuts return the value if the unit match else it raises the exception *UnitError*.
+
+A shortcut is defined to access each unit, e.g. :func:`U_V`, :func:`U_A`, :func:`U_s`, :func:`U_Hz`,
+:func:`U_Ω`, :func:`U_F`, :func:`U_H.`, as well as for prefixes e.g. :func:`U_mV`.
+
+Some shortcuts have Unicode and ASCII variants:
+
+ * For micro, we have the prefix *μ* and *u*.
+ * For Ohm, we have :func:`u_Ω` and :func:`u_Ohm`.
+
+Some examples of usage:
+
+.. code-block:: python3
+
+ foo = kilo(1) # unit less
+
+ resistance_unit = U_Ω
+
+ resistance1 = u_kΩ(1)
+ resistance1 = u_kOhm(1) # ASCII variant
+
+ resistance1 = 1@u_kΩ # using Python 3.5 syntax
+ resistance1 = 1 @u_kΩ # space doesn't matter
+ resistance1 = 1 @ u_kΩ #
+
+ resistance2 = as_Ω(resistance1) # check unit
+
+ resistances = u_kΩ(range(1, 11)) # same as [u_kΩ(x) for x in range(1, 11)]
+ resistances = range(1, 11)@u_kΩ # using Python 3.5 syntax
+
+ capacitance = u_uF(200)
+ inductance = u_mH(1)
+ temperature = u_Degree(25)
+
+ voltage = resistance1 * u_mA(1) # compute unit
+
+ frequency = u_ms(20).frequency
+ period = u_Hz(50).period
+ pulsation = frequency.pulsation
+ pulsation = period.pulsation
+
+.. warning::
+
+ According to the Python `operator precedence
+ `_, division operators
+ have a higher priority than the matrix multiplication operator. In consequence you must had
+ parenthesis to perform something like :code:`(10@u_s) / (2@_us)`.
+
+"""
+
+####################################################################################################
+
+import logging
+import sys
+
+from . import Unit as _Unit
+from . import SiUnits as _SiUnits
+
+####################################################################################################
+
+_module_logger = logging.getLogger(__name__)
+
+####################################################################################################
+
+_version_info = sys.version_info
+_has_matmul = _version_info.major * 10 + _version_info.minor >= 35
+if not _has_matmul:
+ _module_logger.warning("Your Python version doesn't implement @ operator")
+
+####################################################################################################
+
+class UnitValueShorcut:
+
+ ##############################################
+
+ def __init__(self, prefixed_unit):
+
+ self._prefixed_unit = prefixed_unit
+
+ ##############################################
+
+ def _new_value(self, other):
+
+ return self._prefixed_unit.new_value(other)
+
+ ##############################################
+
+ def __call__(self, other):
+
+ """self(other)"""
+
+ return self._new_value(other)
+
+ ##############################################
+
+ def __rmatmul__(self, other):
+
+ """other @ self"""
+
+ return self._new_value(other)
+
+####################################################################################################
+
+def _to_ascii(name):
+ ascii_name = name
+ for args in (
+ ('μ', 'u'),
+ ('Ω', 'Ohm'),
+ ('°C', 'Degree'),
+ ):
+ ascii_name = ascii_name.replace(*args)
+ return ascii_name
+
+def define_shortcut(name, shortcut) :
+ # ° is illegal in Python 3.5
+ # see https://docs.python.org/3/reference/lexical_analysis.html#identifiers
+ # https://www.python.org/dev/peps/pep-3131/
+ if '°' not in name:
+ globals()[name] = shortcut
+ ascii_name = _to_ascii(name)
+ if ascii_name != name:
+ globals()[ascii_name] = shortcut
+
+####################################################################################################
+
+# Define shortcuts for unit prefixes : ..., micro, milli, kilo, mega, ...
+
+def _build_prefix_shortcut(unit_prefix):
+ unit_cls_name = unit_prefix.__class__.__name__
+ name = unit_cls_name.lower()
+ prefixed_unit = _Unit.PrefixedUnit(power=unit_prefix)
+ _Unit.PrefixedUnit.register(prefixed_unit)
+ shortcut = lambda value: _Unit.UnitValue(prefixed_unit, value)
+ define_shortcut(name, shortcut)
+
+for unit_prefix in _Unit.UnitPrefixMetaclass.prefix_iter():
+ if unit_prefix.__class__ != _Unit.ZeroPower:
+ _build_prefix_shortcut(unit_prefix) # capture unit_prefix
+
+####################################################################################################
+
+# Fixme: better ???
+
+class FrequencyValue(_Unit.UnitValue, _Unit.FrequencyMixin):
+ pass
+
+# Fixme:
+class FrequencyValues(_Unit.UnitValues): # , _Unit.FrequencyMixin
+ pass
+
+class PeriodValue(_Unit.UnitValue, _Unit.PeriodMixin):
+ pass
+
+class PeriodValues(_Unit.UnitValues): # , _Unit.PeriodMixin
+ pass
+
+####################################################################################################
+
+# Define unit shortcuts
+
+def _build_unit_type_shortcut(unit):
+ name = 'U_' + unit.unit_suffix
+ define_shortcut(name, unit)
+
+def _build_as_unit_shortcut(unit):
+ name = 'as_' + unit.unit_suffix
+ shortcut = unit.validate
+ define_shortcut(name, shortcut)
+
+def _exec_body(ns, unit_prefix):
+ ns['POWER'] = unit_prefix
+
+def _build_unit_prefix_shortcut(unit, unit_prefix):
+ name = 'u_' + str(unit_prefix) + unit.unit_suffix
+ if unit.__class__ == _SiUnits.Hertz:
+ value_ctor = FrequencyValue
+ values_ctor = FrequencyValues
+ elif unit.__class__ == _SiUnits.Second:
+ value_ctor = PeriodValue
+ values_ctor = PeriodValues
+ else:
+ value_ctor = _Unit.UnitValue
+ values_ctor = _Unit.UnitValues
+ prefixed_unit = _Unit.PrefixedUnit(unit, unit_prefix, value_ctor, values_ctor)
+ _Unit.PrefixedUnit.register(prefixed_unit)
+ define_shortcut('U' + name[1:], prefixed_unit)
+ shortcut = UnitValueShorcut(prefixed_unit)
+ define_shortcut(name, shortcut)
+
+def _build_unit_shortcut(unit):
+ _build_as_unit_shortcut(unit)
+ _build_unit_type_shortcut(unit)
+ for unit_prefix in _Unit.UnitPrefixMetaclass.prefix_iter():
+ if unit_prefix.is_defined_in_spice:
+ _build_unit_prefix_shortcut(unit, unit_prefix)
+
+for unit in _Unit.UnitMetaclass.unit_iter():
+ if unit.unit_suffix and unit.__class__ not in (_SiUnits.Kilogram,):
+ # Fixme: kilogram
+ _build_unit_shortcut(unit)
+
+####################################################################################################
+
+unit_value = _Unit.UnitValue.simple_value
+
+Frequency = u_Hz
+Period = u_s
diff --git a/third_party/PySpice-org/PySpice/PySpice/__init__.py b/third_party/PySpice-org/PySpice/PySpice/__init__.py
new file mode 100644
index 00000000..f1b3b5b3
--- /dev/null
+++ b/third_party/PySpice-org/PySpice/PySpice/__init__.py
@@ -0,0 +1,27 @@
+####################################################################################################
+#
+# 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 .
+#
+####################################################################################################
+
+####################################################################################################
+
+__version__ = '1.5'
+GIT_TAG = 'v1.5'
+
+def show_version():
+ print('PySpice Version {}'.format(__version__))
diff --git a/third_party/PySpice-org/PySpice/README.html b/third_party/PySpice-org/PySpice/README.html
new file mode 100644
index 00000000..4f53b4c8
--- /dev/null
+++ b/third_party/PySpice-org/PySpice/README.html
@@ -0,0 +1,896 @@
+
+
+
+
+
+
+PySpice : Simulate Electronic Circuit using Python and the Ngspice / Xyce Simulators
+
+
+
+
+
+PySpice : Simulate Electronic Circuit using Python and the Ngspice / Xyce Simulators
+
+
+
+
+
+
+
+
+
+
+Quick Links
+
+
+2024 Update
+Disclaimer: PySpice is developed on my free time actually, so I could be busy with other tasks and less reactive.
+The free Discourse forum was closed some time ago due to a lack of activity.
+A HTML backup is stored in the directory pyspice-discourse-backup.
+On Devel HEAD
+
+fixed the ngspice library loading for recent cffi
+fixed simulation aborting due to a message from newer ngspice
+fixes for Spice parser
+added support for Pint unit library
+implemented SpiceLibrary
+code cleanup but must check for typo...
+
+
+An issue was found with NgSpice Shared, we must setlocale(LC_NUMERIC, "C"); see https://sourceforge.net/p/ngspice/bugs/490/
+
+
+Overview
+
+What is PySpice ?
+PySpice is a Python module which interface Python to the Ngspice and Xyce circuit simulators.
+
+
+Where is the Documentation ?
+The documentation is available on the PySpice Home Page.
+Note: This site is hosted on my own infrastructure, if the site seems done, please create an issue to notify me.
+
+
+
+What are the main features ?
+
+support Ngspice and Xyce circuit simulators
+support Linux, Windows and Mac OS X platforms
+licensed under GPLv3 therms
+implement an Ngspice shared library binding using CFFI which support external sources
+implement (partial) SPICE netlist parser
+implement an Oriented Object API to define circuit
+export simulation output to Numpy arrays
+plot using Matplotlib
+handle units
+work with Kicad schematic editor
+implement a documentation generator
+provides many examples
+
+
+
+How to install it ?
+Look at the installation section in the documentation.
+
+
+
+Pull Request Recommendation
+To make it easier to merge your pull request, you should divide your PR into smaller and easier-to-verify units.
+Please do not make a pull requests with a lot of modifications which are difficult to check. If I merge
+pull requests blindly then there is a high risk this software will become a mess quickly for everybody.
+
+
+
+News
+
+
+
+V1.6.0 (development release)
+
+KiCadTools a proof of concept module to read KiCad 6
+.kicad_sch schema file and compute the netlist. This module can
+be used to perform any kind of processings on a KiCad schema. It is
+actually hosted in the source but could become a standalone
+project. For PySpice, it provides a very flexible way to draft a
+circuit with the help of KiCad and then generate the netlist without
+using the netlist export feature of KiCad. And thus leverage the
+writing of fastidious cicruit.
+
+
+
+V1.5.0 (production release) 2021-05-15
+
+Support Ngspice up to version 34
+Renamed custom dunders "__dunder__" to "CONSTANT" or "_private" class attributes
+Fixed typo in documentation (thanks to endolith and brollb)
+Add DC temperature sweep support #272 (thanks to Fatsie)
+PWL support improvements #271 (thanks to Fatsie)
+Assign units on creation of temperature-sweep vectors #263 (thanks to ARF1)
+Prevent memory leaks by freeing ngspice command log #260 thanks to ARF1)
+Performance optimization: dispatch multiple alter commands jointly #259 (thanks to ARF1)
+Added spice library support #258 (thanks to Fatsie)
+Allow to specify DC value for PWL #257 (thanks to Fatsie)
+Support for .nodeset type initial condition #256 (thanks to Fatsie)
+Fix accuracy problems #254 (thanks to sotw1957)
+Changes to make it easier to use PySpice with a large archive of SPICE models medium diff #249 (thanks to xesscorp)
+Netlist.py: Fix wrong method when joining parameters during netlist parse #245 (thanks to cyber-g)
+Unit: add Pickle support
+Add Parser code from #136 (thanks to jmgc) but not yet merged
+Unit: add np.mean
+
+
+
+V1.4.3 2020-07-04
+A huge effort, thanks to @stuarteberg Stuart Berg, has been made to make Ngspice and PySpice
+available on Anaconda (conda-forge) for the Window, OSX and Linux platforms. Thanks to the
+conda-forge continuous integration platform, we can now run unit tests and the examples on theses
+platforms automatically. Hope this will make the software more robust and easier to run !
+
+PySpice is now available on Anaconda(conda-forge) as well as a wheel on PyPI
+Added a post installation tool to download the Ngspice DLL on Windows and to check the installation.
+It should now simplify considerably the PySpice installation on Windows.
+This tool can also download the examples and the Ngspice PDF manual.
+On Linux and OSX, a Ngspice package is now available on Anaconda(conda-forge).
+Note that theses two platforms do not download a binary from Ngspice since a compiler can easily be installed on theses platforms.
+Updated installation documentation for Linux, the main distributions now provide a ngspice shared package.
+Added a front-end web site so as to keep older releases documentation available on the web.
+fixed and rebuilt all examples (but mistakes could happen ...)
+examples are now available as Python files and Jupyter notebooks
+(but some issues must be fixed, e.g. due to the way Jupyter handles Matplotlib plots)
+support NgSpice 32 API (no change)
+removed @substitution@ in PySpice/__init__.py, beacause it breaks pip install from git
+fixed some logging spams
+fixed NonLinearVoltageSource
+fixed Unicode issue with °C (° is Extended ASCII)
+fixed ffi_string_utf8 for UnicodeDecodeError
+fixed logging formater for OSX (removed ANSI codes)
+reworded "Invalid plot name" exception
+removed diacritics in example filenames
+cir2py has been converted to an entry point so as to work on all platforms
+updated Matplotlib subplots in examples
+added a unit example
+added a NMOS example (thanks to cyber-g) cf. #221
+
+
+
+V1.4.0 2020-05-05
+This release is yanked due to broken Windows support.
+
+fixed nasty issue with NgSpice shared for setlocale(LC_NUMERIC, "C"); cf. #172
+fixed AC AC_MAG AC_PASAE SIN for new NgSpice syntax
+fixed initial_state for VoltageControlledSwitch
+fixed LosslessTransmissionLine #169
+fixed docstrings for element shortcut methods (thanks to Kyle Dunn) #178
+fixed parser for leading whitespace (thanks to Matt Huszagh) #182
+fix for PyYAML newer API
+support NgSpice 31 API (no change)
+added check for CoupledInductor #157
+added check-installation tool to help to fix broken installation
+added pole-zero, noise, distorsion, transfer-function analyses (thanks to Peter Garrone) #191
+added .measure support (thanks to ceprio) #160
+added log_desk parameter to CircuitSimulator
+added listing command method to NgSpiceShared
+added Xyce Mosfet nfin #177
+
+
+
+
+V1.2.0 2018-06-07
+
+Initial support of the Xyce simulator. Xyce is an open source, SPICE-compatible,
+high-performance analog circuit simulator, capable of solving extremely large circuit problems
+developed at Sandia National Laboratories. Xyce will make PySpice suitable for industry and
+research use.
+Fixed OSX support
+Splitted G device
+Implemented partially A XSPICE device
+Implemented missing transmission line devices
+Implemented high level current sources
+Notice: Some classes were renamed !
+Implemented node kwarg e.g. circuit.Q(1, base=1, collector=2, emitter=3, model='npn')
+Implemented raw spice pass through (see User FAQ)
+Implemented access to internal parameters (cf. save @device[parameter])
+Implemented check for missing ground node
+Implemented a way to disable an element and clone netlist
+Improved SPICE parser
+Improved unit support:
+
+Implemented unit prefix cast U_μV(U_mV(1)) to easily convert values
+Added U_mV, ... shortcuts
+Added Numpy array support to unit, see UnitValues Notice: this new feature could be buggy !!!
+Rebased WaveForm to UnitValues
+
+
+Fixed node order so as to not confuse users Now PySpice matches SPICE order for two ports elements !
+Fixed device shortcuts in Netlist class
+Fixed model kwarg for BJT Notice: it must be passed exclusively as kwarg !
+Fixed subcircuit nesting
+Outsourced documentation generator to Pyterate
+Updated setup.py for wheel
+
+
+
+
+
+V1.0.0 2017-09-06
+
+Bump version to v1.0.0 since it just works!
+Support Windows platform using Ngspice shared mode
+Fixed shared mode
+Fixed and completed Spice parser : tested on example's libraries
+
+
+
+
+V0.4.0 2017-07-31
+
+Git repository cleanup: filtered generated doc and useless files so as to shrink the repository size.
+Improved documentation generator: Implemented format for RST content and Tikz figure.
+Improved unit support: It implements now the International System of Units.
+And we can now use unit helper like u_mV or compute the value of 1.2@u_kΩ / 2@u_mA.
+The relevant documentation is on this page.
+Added the Simulation instance to the Analysis class.
+Refactored simulation parameters as classes.
+
+
+
+
+
+V0.3.0 2015-12-08
+
+Added an example to show how to use the NgSpice Shared Simulation Mode.
+Completed the Spice netlist parser and added examples, we could now use a schematic editor
+to define the circuit. The program cir2py translates a circuit file to Python.
+
+
+
+V0 2014-03-21
+Started project
+
+
+
+
+
+
+
diff --git a/third_party/PySpice-org/PySpice/README.rst b/third_party/PySpice-org/PySpice/README.rst
new file mode 100644
index 00000000..d3a3102f
--- /dev/null
+++ b/third_party/PySpice-org/PySpice/README.rst
@@ -0,0 +1,374 @@
+.. -*- Mode: rst -*-
+
+.. -*- Mode: rst -*-
+
+.. |PySpiceUrl| replace:: https://pyspice.fabrice-salvaire.fr
+
+.. |PySpiceHomePage| replace:: PySpice Home Page
+.. _PySpiceHomePage: https://pyspice.fabrice-salvaire.fr
+
+
+.. |PySpice@github| replace:: https://github.com/FabriceSalvaire/PySpice
+
+
+.. |PySpice@pypi| replace:: https://pypi.python.org/pypi/PySpice
+
+
+.. |PySpice@anaconda| replace:: https://anaconda.org/conda-forge/pyspice
+
+.. |PySpice@fs-anaconda| replace:: https://anaconda.org/fabricesalvaire/pyspice
+
+.. |Anaconda Version| image:: https://anaconda.org/conda-forge/pyspice/badges/version.svg
+ :target: https://anaconda.org/conda-forge/pyspice/badges/version.svg
+ :alt: Anaconda last version
+
+.. |Anaconda Downloads| image:: https://anaconda.org/conda-forge/pyspice/badges/downloads.svg
+ :target: https://anaconda.org/conda-forge/pyspice/badges/downloads.svg
+ :alt: Anaconda donwloads
+
+
+.. |Pypi Version| image:: https://img.shields.io/pypi/v/PySpice.svg
+ :target: https://pypi.python.org/pypi/PySpice
+ :alt: PySpice last version
+
+.. |Pypi License| image:: https://img.shields.io/pypi/l/PySpice.svg
+ :target: https://pypi.python.org/pypi/PySpice
+ :alt: PySpice license
+
+.. |Pypi Python Version| image:: https://img.shields.io/pypi/pyversions/PySpice.svg
+ :target: https://pypi.python.org/pypi/PySpice
+ :alt: PySpice python version
+
+
+.. |Tavis CI master| image:: https://travis-ci.com/FabriceSalvaire/PySpice.svg?branch=master
+ :target: https://travis-ci.com/FabriceSalvaire/PySpice
+ :alt: PySpice build status @travis-ci.org
+.. -*- Mode: rst -*-
+
+.. _CFFI: http://cffi.readthedocs.org/en/latest/
+.. _Circuit_macros: http://ece.uwaterloo.ca/~aplevich/Circuit_macros
+.. _IPython: http://ipython.org
+.. _Kicad: http://www.kicad-pcb.org
+.. _Matplotlib: http://matplotlib.org
+.. _Modelica: http://www.modelica.org
+.. _Ngspice: http://ngspice.sourceforge.net
+.. _Numpy: http://www.numpy.org
+.. _PyPI: https://pypi.python.org/pypi
+.. _Pyterate: https://github.com/FabriceSalvaire/Pyterate
+.. _Python: http://python.org
+.. _Sphinx: http://sphinx-doc.org
+.. _Tikz: http://www.texample.net/tikz
+.. _Xyce: https://xyce.sandia.gov
+
+.. |CFFI| replace:: CFFI
+.. |Circuit_macros| replace:: Circuit_macros
+.. |IPython| replace:: IPython
+.. |Kicad| replace:: Kicad
+.. |Matplotlib| replace:: Matplotlib
+.. |Modelica| replace:: Modelica
+.. |Ngspice| replace:: Ngspice
+.. |Numpy| replace:: Numpy
+.. |PyPI| replace:: PyPI
+.. |Pyterate| replace:: Pyterate
+.. |Python| replace:: Python
+.. |Sphinx| replace:: Sphinx
+.. |Tikz| replace:: Tikz
+.. |Xyce| replace:: Xyce
+
+=====================================================================================
+ PySpice : Simulate Electronic Circuit using Python and the Ngspice / Xyce Simulators
+=====================================================================================
+
+|Pypi License|
+|Pypi Python Version|
+
+|Pypi Version|
+
+|Anaconda Version|
+|Anaconda Downloads|
+
+|Tavis CI master|
+
+**Quick Links**
+
+* `Production Branch `_
+* `Devel Branch `_
+* `Travis CI `_
+* `pyspice@conda-forge `_
+* `conda-forge/pyspice `_
+* `ngspice@conda-forge `_
+* `Ngspice `_
+* `Ngspice Bug Tracker `_
+* `Xyce of Sandia National Laboratories `_
+
+2024 Update
+===========
+
+**Disclaimer: PySpice is developed on my free time actually, so I could be busy with other tasks and less reactive.**
+
+The free Discourse forum was closed some time ago due to a lack of activity.
+A HTML backup is stored in the directory `pyspice-discourse-backup`.
+
+**On Devel HEAD**
+
+* fixed the ngspice library loading for recent cffi
+* fixed simulation aborting due to a message from newer ngspice
+* fixes for Spice parser
+* added support for Pint unit library
+* implemented SpiceLibrary
+* code cleanup but must check for typo...
+
+..
+ Brief Notes
+ ===========
+
+An issue was found with NgSpice Shared, we must `setlocale(LC_NUMERIC, "C");` see https://sourceforge.net/p/ngspice/bugs/490/
+
+Overview
+========
+
+What is PySpice ?
+-----------------
+
+PySpice is a Python module which interface |Python|_ to the |Ngspice|_ and |Xyce|_ circuit simulators.
+
+Where is the Documentation ?
+----------------------------
+
+The documentation is available on the |PySpiceHomePage|_.
+
+*Note: This site is hosted on my own infrastructure, if the site seems done, please create an issue to notify me.*
+
+Where to get help or talk about PySpice ?
+-----------------------------------------
+
+Thanks to `Discourse `_, PySpice now has a **Forum** hosted at https://pyspice.discourse.group
+
+What are the main features ?
+----------------------------
+
+* support Ngspice and Xyce circuit simulators
+* support **Linux**, **Windows** and Mac **OS X** platforms
+* licensed under **GPLv3** therms
+* implement an **Ngspice shared library binding** using CFFI which support external sources
+* implement (partial) **SPICE netlist parser**
+* implement an **Oriented Object API** to define circuit
+* export simulation output to |Numpy|_ arrays
+* plot using |Matplotlib|_
+* handle **units**
+* work with **Kicad schematic editor**
+* implement a **documentation generator**
+* provides many **examples**
+
+How to install it ?
+-------------------
+
+Look at the `installation `_ section in the documentation.
+
+Pull Request Recommendation
+===========================
+
+To make it easier to merge your pull request, you should divide your PR into smaller and easier-to-verify units.
+
+Please do not make a pull requests with a lot of modifications which are difficult to check. **If I merge
+pull requests blindly then there is a high risk this software will become a mess quickly for everybody.**
+
+Credits
+=======
+
+Authors: `Fabrice Salvaire `_ and `contributors `_
+
+News
+====
+
+.. -*- Mode: rst -*-
+
+
+.. no title here
+
+V1.6.0 (development release)
+----------------------------
+
+* **KiCadTools** a proof of concept module to read KiCad 6
+ `.kicad_sch` schema file and compute the netlist. *This module can
+ be used to perform any kind of processings on a KiCad schema. It is
+ actually hosted in the source but could become a standalone
+ project.* For PySpice, it provides a very flexible way to draft a
+ circuit with the help of KiCad and then generate the netlist without
+ using the netlist export feature of KiCad. And thus leverage the
+ writing of fastidious cicruit.
+
+V1.5.0 (production release) 2021-05-15
+--------------------------------------
+
+* Support Ngspice up to version 34
+* Renamed custom dunders "__dunder__" to "CONSTANT" or "_private" class attributes
+* Fixed typo in documentation (thanks to endolith and brollb)
+* Add DC temperature sweep support #272 (thanks to Fatsie)
+* PWL support improvements #271 (thanks to Fatsie)
+* Assign units on creation of temperature-sweep vectors #263 (thanks to ARF1)
+* Prevent memory leaks by freeing ngspice command log #260 thanks to ARF1)
+* Performance optimization: dispatch multiple alter commands jointly #259 (thanks to ARF1)
+* Added spice library support #258 (thanks to Fatsie)
+* Allow to specify DC value for PWL #257 (thanks to Fatsie)
+* Support for `.nodeset` type initial condition #256 (thanks to Fatsie)
+* Fix accuracy problems #254 (thanks to sotw1957)
+* Changes to make it easier to use PySpice with a large archive of SPICE models medium diff #249 (thanks to xesscorp)
+* `Netlist.py`: Fix wrong method when joining parameters during netlist parse #245 (thanks to cyber-g)
+* Unit: add Pickle support
+* Add Parser code from #136 (thanks to jmgc) but not yet merged
+* Unit: add np.mean
+
+V1.4.3 2020-07-04
+-----------------
+
+A huge effort, thanks to @stuarteberg Stuart Berg, has been made to make Ngspice and PySpice
+available on Anaconda (conda-forge) for the Window, OSX and Linux platforms. Thanks to the
+conda-forge continuous integration platform, we can now run unit tests and the examples on theses
+platforms automatically. Hope this will make the software more robust and easier to run !
+
+* PySpice is now available on Anaconda(conda-forge) as well as a wheel on PyPI
+* Added a post installation tool to download the Ngspice DLL on Windows and to check the installation.
+ It should now simplify considerably the PySpice installation on Windows.
+* This tool can also download the examples and the Ngspice PDF manual.
+* On Linux and OSX, a Ngspice package is now available on Anaconda(conda-forge).
+ Note that theses two platforms do not download a binary from Ngspice since a compiler can easily be installed on theses platforms.
+* Updated installation documentation for Linux, the main distributions now provide a ngspice shared package.
+
+* Added a front-end web site so as to keep older releases documentation available on the web.
+* fixed and rebuilt all examples (but mistakes could happen ...)
+* examples are now available as Python files and Jupyter notebooks
+ (but some issues must be fixed, e.g. due to the way Jupyter handles Matplotlib plots)
+
+* support NgSpice 32 API (no change)
+* removed @substitution@ in PySpice/__init__.py, beacause it breaks pip install from git
+* fixed some logging spams
+* fixed NonLinearVoltageSource
+* fixed Unicode issue with °C (° is Extended ASCII)
+* fixed ffi_string_utf8 for UnicodeDecodeError
+* fixed logging formater for OSX (removed ANSI codes)
+* reworded "Invalid plot name" exception
+* removed diacritics in example filenames
+* cir2py has been converted to an entry point so as to work on all platforms
+* updated Matplotlib subplots in examples
+* added a unit example
+* added a NMOS example (thanks to cyber-g) cf. #221
+
+V1.4.0 2020-05-05
+-----------------
+
+This release is yanked due to broken Windows support.
+
+* fixed nasty issue with NgSpice shared for `setlocale(LC_NUMERIC, "C");` cf. #172
+* fixed `AC AC_MAG AC_PASAE SIN` for new NgSpice syntax
+* fixed `initial_state` for `VoltageControlledSwitch`
+* fixed `LosslessTransmissionLine` #169
+* fixed docstrings for element shortcut methods (thanks to Kyle Dunn) #178
+* fixed parser for leading whitespace (thanks to Matt Huszagh) #182
+* fix for PyYAML newer API
+* support NgSpice 31 API (no change)
+* added check for `CoupledInductor` #157
+* added `check-installation` tool to help to fix broken installation
+* added pole-zero, noise, distorsion, transfer-function analyses (thanks to Peter Garrone) #191
+* added `.measure` support (thanks to ceprio) #160
+* added `log_desk` parameter to `CircuitSimulator`
+* added `listing` command method to `NgSpiceShared`
+* added Xyce Mosfet nfin #177
+
+V1.3.2 2019-03-11
+------------------
+
+* support Ngspice 30 and Xyce 6.10
+* fixed NgSpice and Xyce support on Windows 10
+* bug fixes
+
+V1.2.0 2018-06-07
+-----------------
+
+* Initial support of the |Xyce|_ simulator. Xyce is an open source, SPICE-compatible,
+ high-performance analog circuit simulator, capable of solving extremely large circuit problems
+ developed at Sandia National Laboratories. Xyce will make PySpice suitable for industry and
+ research use.
+* Fixed OSX support
+* Splitted G device
+* Implemented partially `A` XSPICE device
+* Implemented missing transmission line devices
+* Implemented high level current sources
+ **Notice: Some classes were renamed !**
+* Implemented node kwarg e.g. :code:`circuit.Q(1, base=1, collector=2, emitter=3, model='npn')`
+* Implemented raw spice pass through (see `User FAQ `_)
+* Implemented access to internal parameters (cf. :code:`save @device[parameter]`)
+* Implemented check for missing ground node
+* Implemented a way to disable an element and clone netlist
+* Improved SPICE parser
+* Improved unit support:
+
+ * Implemented unit prefix cast `U_μV(U_mV(1))` to easily convert values
+ * Added `U_mV`, ... shortcuts
+ * Added Numpy array support to unit, see `UnitValues` **Notice: this new feature could be buggy !!!**
+ * Rebased `WaveForm` to `UnitValues`
+
+* Fixed node order so as to not confuse users **Now PySpice matches SPICE order for two ports elements !**
+* Fixed device shortcuts in `Netlist` class
+* Fixed model kwarg for BJT **Notice: it must be passed exclusively as kwarg !**
+* Fixed subcircuit nesting
+* Outsourced documentation generator to |Pyterate|_
+* Updated `setup.py` for wheel
+
+.. :ref:`user-faq-page`
+
+V1.1.0 2017-09-06
+-----------------
+
+* Enhanced shared mode
+* Shared mode is now set as default on Linux
+
+V1.0.0 2017-09-06
+-----------------
+
+* Bump version to v1.0.0 since it just works!
+* Support Windows platform using Ngspice shared mode
+* Fixed shared mode
+* Fixed and completed Spice parser : tested on example's libraries
+
+V0.4.2
+------
+
+* Fixed Spice parser for lower case device prefix.
+
+V0.4.0 2017-07-31
+-----------------
+
+* Git repository cleanup: filtered generated doc and useless files so as to shrink the repository size.
+* Improved documentation generator: Implemented :code:`format` for RST content and Tikz figure.
+* Improved unit support: It implements now the International System of Units.
+ And we can now use unit helper like :code:`u_mV` or compute the value of :code:`1.2@u_kΩ / 2@u_mA`.
+ The relevant documentation is on this `page `_.
+* Added the Simulation instance to the Analysis class.
+* Refactored simulation parameters as classes.
+
+V0.3.2 2017-02-22
+-----------------
+
+* fixed CCCS and CCVS
+
+V0.3.1 2017-02-22
+-----------------
+
+* fixed ngspice shared
+
+V0.3.0 2015-12-08
+-----------------
+
+* Added an example to show how to use the NgSpice Shared Simulation Mode.
+* Completed the Spice netlist parser and added examples, we could now use a schematic editor
+ to define the circuit. The program *cir2py* translates a circuit file to Python.
+
+V0 2014-03-21
+-------------
+
+Started project
+
+.. End
+
+.. End
diff --git a/third_party/PySpice-org/PySpice/README.txt b/third_party/PySpice-org/PySpice/README.txt
new file mode 100644
index 00000000..82305f15
--- /dev/null
+++ b/third_party/PySpice-org/PySpice/README.txt
@@ -0,0 +1,114 @@
+.. -*- Mode: rst -*-
+
+.. include:: project-links.txt
+.. include:: abbreviation.txt
+
+=====================================================================================
+ PySpice : Simulate Electronic Circuit using Python and the Ngspice / Xyce Simulators
+=====================================================================================
+
+|Pypi License|
+|Pypi Python Version|
+
+|Pypi Version|
+
+|Anaconda Version|
+|Anaconda Downloads|
+
+|Tavis CI master|
+
+**Quick Links**
+
+* `Production Branch `_
+* `Devel Branch `_
+* `Travis CI `_
+* `pyspice@conda-forge `_
+* `conda-forge/pyspice `_
+* `ngspice@conda-forge `_
+* `Ngspice `_
+* `Ngspice Bug Tracker `_
+* `Xyce of Sandia National Laboratories `_
+
+2024 Update
+===========
+
+**Disclaimer: PySpice is developed on my free time actually, so I could be busy with other tasks and less reactive.**
+
+The free Discourse forum was closed some time ago due to a lack of activity.
+A HTML backup is stored in the directory `pyspice-discourse-backup`.
+
+**On Devel HEAD**
+
+* fixed the ngspice library loading for recent cffi
+* fixed simulation aborting due to a message from newer ngspice
+* fixes for Spice parser
+* added support for Pint unit library
+* implemented SpiceLibrary
+* code cleanup but must check for typo...
+
+..
+ Brief Notes
+ ===========
+
+An issue was found with NgSpice Shared, we must `setlocale(LC_NUMERIC, "C");` see https://sourceforge.net/p/ngspice/bugs/490/
+
+Overview
+========
+
+What is PySpice ?
+-----------------
+
+PySpice is a Python module which interface |Python|_ to the |Ngspice|_ and |Xyce|_ circuit simulators.
+
+Where is the Documentation ?
+----------------------------
+
+The documentation is available on the |PySpiceHomePage|_.
+
+*Note: This site is hosted on my own infrastructure, if the site seems done, please create an issue to notify me.*
+
+Where to get help or talk about PySpice ?
+-----------------------------------------
+
+Thanks to `Discourse `_, PySpice now has a **Forum** hosted at https://pyspice.discourse.group
+
+What are the main features ?
+----------------------------
+
+* support Ngspice and Xyce circuit simulators
+* support **Linux**, **Windows** and Mac **OS X** platforms
+* licensed under **GPLv3** therms
+* implement an **Ngspice shared library binding** using CFFI which support external sources
+* implement (partial) **SPICE netlist parser**
+* implement an **Oriented Object API** to define circuit
+* export simulation output to |Numpy|_ arrays
+* plot using |Matplotlib|_
+* handle **units**
+* work with **Kicad schematic editor**
+* implement a **documentation generator**
+* provides many **examples**
+
+How to install it ?
+-------------------
+
+Look at the `installation `_ section in the documentation.
+
+Pull Request Recommendation
+===========================
+
+To make it easier to merge your pull request, you should divide your PR into smaller and easier-to-verify units.
+
+Please do not make a pull requests with a lot of modifications which are difficult to check. **If I merge
+pull requests blindly then there is a high risk this software will become a mess quickly for everybody.**
+
+Credits
+=======
+
+Authors: `Fabrice Salvaire `_ and `contributors `_
+
+News
+====
+
+.. include:: news.txt
+
+.. End
diff --git a/third_party/PySpice-org/PySpice/anaconda-recipe/README.md b/third_party/PySpice-org/PySpice/anaconda-recipe/README.md
new file mode 100644
index 00000000..7be999dd
--- /dev/null
+++ b/third_party/PySpice-org/PySpice/anaconda-recipe/README.md
@@ -0,0 +1,3 @@
+# Anaconda Recipe
+
+This recipe is used for test.
diff --git a/third_party/PySpice-org/PySpice/anaconda-recipe/bld.bat b/third_party/PySpice-org/PySpice/anaconda-recipe/bld.bat
new file mode 100644
index 00000000..e69de29b
diff --git a/third_party/PySpice-org/PySpice/anaconda-recipe/build.sh b/third_party/PySpice-org/PySpice/anaconda-recipe/build.sh
new file mode 100644
index 00000000..7c9dcf9b
--- /dev/null
+++ b/third_party/PySpice-org/PySpice/anaconda-recipe/build.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+# https://github.com/AnacondaRecipes/conda-feedstock/blob/master/recipe/build.sh
+
+# --record filename in which to record list of
+# installed files
+# --single-version-externally-managed used by system package builders to
+# create 'flat' eggs
+
+invoke release.update-git-sha
+$PYTHON setup.py install --single-version-externally-managed --record record.txt
diff --git a/third_party/PySpice-org/PySpice/anaconda-recipe/meta.yaml b/third_party/PySpice-org/PySpice/anaconda-recipe/meta.yaml
new file mode 100644
index 00000000..a65a73ff
--- /dev/null
+++ b/third_party/PySpice-org/PySpice/anaconda-recipe/meta.yaml
@@ -0,0 +1,80 @@
+# https://jakevdp.github.io/blog/2016/08/25/conda-myths-and-misconceptions
+
+# https://docs.anaconda.com/anaconda-cloud/user-guide/tasks/work-with-packages/#working-with-packages
+# https://docs.conda.io/projects/conda-build/en/latest/resources/define-metadata.html
+# https://github.com/AnacondaRecipes/conda-feedstock
+
+{% set name = "PySpice" %}
+{% set version = "1.4.2" %}
+
+package:
+ name: {{ name|lower }}
+ version: {{ version }}
+
+source:
+ # path: ../..
+ #
+ # url: "https://pypi.io/packages/source/{{ name[0] }}/{{ name }}/{{ name }}-{{ version }}.tar.gz"
+ # sha256: 18f39249072958de6ead18ab54a14442e9e9e32b5579185b907ad7a794908be9
+ #
+ git_url: https://github.com/FabriceSalvaire/PySpice
+ git_rev: "v{{ version }}"
+ git_depth: 1 # (Defaults to -1/not shallow)
+
+build:
+ noarch: python
+ number: 0
+ # script: python -m pip install --no-deps --ignore-installed .
+ # script: "{{ PYTHON }} -m pip install . -vv"
+ # entry_points:
+ # - foo = foo.bar:main_foo
+ has_prefix_files:
+ always_include_files:
+ # - bin/cir2py
+ # - bin/ngspice-check-installation
+ # - bin/ngspice-post-installation
+ # - PySpice/Spice/NgSpice/api.h
+ # - PySpice/Config/logging.yml
+
+requirements:
+ host:
+ - pip
+ - python
+ - invoke >=1.3
+ - numpy >=1.18
+ run:
+ - python
+ - PyYAML >=5.3
+ - cffi >=1.14
+ - matplotlib >=3.1
+ - numpy >=1.18
+ - ply >=3.11
+ - scipy >=1.4
+ - requests >=2.23
+# build:
+# - wheel
+
+# test:
+# imports:
+
+# outputs:
+# - type: wheel
+
+about:
+ home: https://github.com/FabriceSalvaire/PySpice
+ license: "GNU General Public License v3.0"
+ license_family: GPL
+ license_file: LICENSE.txt
+ # https://www.gnu.org/licenses/gpl-3.0.en.html
+ summary: "Simulate electronic circuit using Python and the Ngspice / Xyce simulators"
+ # description: >
+ # ...
+ dev_url: https://github.com/FabriceSalvaire/PySpice
+ doc_url: https://pyspice.fabrice-salvaire.fr
+ # doc_source_url:
+
+extra:
+ maintainers:
+ - FabriceSalvaire
+ recipe-maintainers:
+ - FabriceSalvaire
diff --git a/third_party/PySpice-org/PySpice/bin/cir2py b/third_party/PySpice-org/PySpice/bin/cir2py
new file mode 100644
index 00000000..a30c30b7
--- /dev/null
+++ b/third_party/PySpice-org/PySpice/bin/cir2py
@@ -0,0 +1,24 @@
+#! /usr/bin/env python3
+
+####################################################################################################
+#
+# PySpice - A Spice Package for Python
+# Copyright (C) 2020 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 .
+#
+####################################################################################################
+
+from PySpice.Scripts.cir2py import main
+main()
diff --git a/third_party/PySpice-org/PySpice/bin/pyspice-post-installation b/third_party/PySpice-org/PySpice/bin/pyspice-post-installation
new file mode 100644
index 00000000..2e248aab
--- /dev/null
+++ b/third_party/PySpice-org/PySpice/bin/pyspice-post-installation
@@ -0,0 +1,24 @@
+#! /usr/bin/env python3
+
+####################################################################################################
+#
+# PySpice - A Spice Package for Python
+# Copyright (C) 2020 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 .
+#
+####################################################################################################
+
+from PySpice.Scripts.pyspice_post_installation import main
+main()
\ No newline at end of file
diff --git a/third_party/PySpice-org/PySpice/doc/datasheets/1N4148_1N4448.pdf b/third_party/PySpice-org/PySpice/doc/datasheets/1N4148_1N4448.pdf
new file mode 100644
index 0000000000000000000000000000000000000000..36c7c908cf4e8b1e6839cc0e3ebcd82b0fd8d20a
GIT binary patch
literal 124555
zcmb@u1zc2L(>P8@h=d?WOA9El>@K_PlF}X0(%s!5AxKFpAl)EcA|VnY-I7X5hae~*
z4gX!#N5AUxywCrAf6M1`&z*Z>X3m^BGjq;8Op2o7>_84M9@G57z%U*T5Cr9<<)pPU
zw4~+Z1ISv~nix1*c$yf~0s*qLKoA!M0s$x>5>O~Wj1~mp1aSf6Y2g4BS~wg8g99XK
zwP=B0E)Xq*8=@m1fM;TBjHn8a_LmGzf(oOn2#-+w?T?L~^#_6H1vXz&jh
z2m<>9hQQ-b7#Kka^7l)92(12qL4kkfg8~0&1I!8hqYW?w^hbSQ2o(H#8(=6G@DCXG
zU)q5Gi3=AL{6|>`2*KgsXoJ8w;lIP6T%3Q<0flpO|DKN<2>b(QZV2Shd~om|Fc|R9
z{(y18|L98?H|UQxU~tGEyoW=$e&;EWlN0j08~_oL_eVY;C-nDrfj}7acfKHH{CEC=
zI3e)gX#;V>{*WOMqK)5i27$T%=pzIS{(HZHzz8P%#(NML4*eq^7bo{0ZEykq=r@EE
zaR1)FATBWMPZ)&r58Ak(z~A`?;^GGVnGg0C82rz134{ZGFUxtM3H~Lg&W;8a)+Ua4
zI0(g}jL<0vMFHdlC_0+BB9#X>6riDGVyf(HLyN2mpk!y~jMOs-hS4ek#B7ZmJ?t+4
z2yP(&Qnsdcv_K?T$Olm^K4CF93<%@~a)Ci0QEr&1FgI943@8SLa|sE-fiSoLA190(
zE)D`ifnp+Dh##kr2nYre6$c_z8mBM}EGB?dSU67329D0(G!#NNF)@iLh~we>qP~7f
zB6JuSIp~nu5~(arY|Wg_X}O>@*exv*DBPyJ9ZbSuL}W`XZ|qR>9)
zt}zPDP}P+RG_KFS!g=)GwsEb<&+v9j>3Ey+I*9kZ3i^y+<2#0qWYXFlp>DJ+`6sw=
zZP%BDB@-#1H*?EA;etQp$zD3bVt0`&Z&Yu+m3E%BvFhh$0T!3t_&WO4!)|^!2Lr?@
zVOiuC5l4B8G;WA}`}76(8i{=*z!I0+>iO2n6&se%IQ`c;tjZp4)P-<}Ji0MbeP+=_
zUszIzHb9LJPrJ<-TtVW&z;_JyGZwuP+@~gOTTxcQF2wb^Q(rk4Gtg6&yMEwpqqu|*
zsKl3OL55Qo=0U%N^V|>X>5<*LQJYxVN7RCJoVYR;V#*4*MTvJx_4;q`;%c?lYPFX(
zn+wrnlNedo+E1kwp|-Q8kG`u|Z!37e+A(H~>Ck)+CVgnHfH9P}wq|7R|$dKVz1WFt^zAq;(#=}8OhTl8*`?QDEg&u8PX{6Q#wPAsKtwqp5CYNxe3K|e14kPtgb0e*S-aTSI?;lW!^+sg$l1;j
zF@^*LzK=JA-UFz3*qdC8w4c)EKC$r@h>F6A`=*?$x!qY?!cc;M`lkL1W;t=Kq>rcs
zpFhyu;A;&CcHh1lmClcfi2^z%_Wc_m>?LU
zXkca{ha8KlP9_R2&ejM+#|a>BU}NHhR5}RFh)^p(;0Or(8$`~;*}&Mq*#Mz=5EBg|
z3$hwS3C`~Y>`iQijGQg(Y_$-Te9NJ6Pm|Wx#oGGkKOKOIIbxU_U$lgn6A{%YBd{`Z
zR&g{j`KIrW+!xjTBMXwUZ&^ef?d*l^+_gA4
z5EB;Sg1{k&|1d5P2d54KV@DHPgwFoX9T7WQXT-A;0!8i%{1JTpRyi_p!9ir=f^CQd
z><0unpV2BIAxK?|Ou!M1At*%55zYvuO3Mk5LkPRU1s;eO2po_YAYPj|*||6xee2S1
zGsT7UBRlOM^dR4Tr^n9P&QaOkz{mtSnw+4B#*pR%tr9|H#1Z%-*`@+QK#`L%qT^Il
zz{rOS3=t0yDkkpE
zIsh?uX9>h$aYmvbp^Pkvv<483Nhn{md@%w3)@eW0h_Z{JGjfO{DFt5OjOa!qM+^IJ
zx&Uc_AgYozaB>zgH*iE$4g{!M7(1IgX(7n~gTcs2ixvWbUR*E)&$&S$T7>Cx@ecVO
zjDSI4T(m#(Bk#!TN16ZWeo+Ua9%LOqE+`O43rENRvaMe)D3qHP`HmY={{pNd`0Kx`NCP-ldNSau?nmAh+8L$i6SsNo1rQ`Qr0fUeh
zkDalFtr?x1
z`v)_=>tDo}{cZH7()MP{ZI_A^KAg5u(9i~E
zw_>~&c}iAWX4ZW6fn9V!O`Cf|rRAY~-tdLAY+(V76#7igCBey;=~*)~Zpk=m
z!YM?8OA(bf6PpZa+>uY`T*iwu=!xbLnfqjhxs6~PR~sMv;0;i+5%
z2h4HtxIH?&hxw&j*VdjaDu8XV^NpcwHqN_u_BT4KQ66OSkuL5?HQ!j;X|qgw%yFZ~
zmBdlv?Q*P`NPO_(
z_5XNdBo#N=cCn_(I`PXPAslPTBBaAH)oe2pv+BBP@Ai}Uehi}QRf(9@d$&&OnP$!J
zW}vYD)s(xKnZFIl?*a|^Y0LdppvOi9DVG_peX6~uk#w)zhKl4J_jE7M8=2VZa3J5M
zT4Gdq%DXJmPSiJYj#9#wASoNlhM@E%sTP{s#j(VUbj%IeDqh)C|C3ZR_C)ppI`Y7zt8w$
z#G(=!?v?9IQ6&E{SmR6n7b0)J0n^w}qddIx&GYlx`}?GP{SKb*B42qOL`$nLwqLT%{v6zGi
zIA9C{9@@f@Ct^zpjYzCJEPZ8+{qhHpRh2A2>6lklpP7Usde%@
zqFL{o!DC2lBvZFZtK*s4;J2==nTBI#jtf6c$VSbV*5tqY5mE-+HkJ&e8Aw>o)>A`~oZ7HYL
z#h3R5LI~~>eWJSYe4*t*uc3Qyl1a@??eZ4{r>kJK7BN3p#aV$%4p*+fBMTc`1!ZDu
z5qVYz2n<-Vn7zKeQQ6o0^s#JD3zzC7#bV&cV@XUCMdi0ng)ygOv3b!vNgM?_pJs9mEYivpVT9+}n{YlIB(rgR4kUrA=m3N}?q7pcOR`QVf4
z#vZFC_C*Y;x9f2?@g_ya&2$qorIB2m#+2cYuiy1uQ=h!NJJ-AZ{uA&`TAjC@?>?T#
z6R-Y>IR|Yxdc_(S=-wWBd;&YSstc3t<3O}Wd>Y11!>iHQ(k$i)+w$$;n?=qk*yI)L
zXcou8boxGA8J8VkBP>QCi8(I~s>hk&Hztk=42IC3Uk?(Ashr=xWS(@}<~at35;_K_
z2nw(5EO?8JMEy18oxt@y+H_kN&WUWn>_drJLkklr)4VO?#KDz4#m*11rAliDfo8jx
zIdLwp`nzw(<^_M{5vpfc8bTWrvY`olFXpd7@%o&|0`!G8lq_ZNX2-~8kDz0-FOKN&
ztLvT=TxB(O^Tm10_b;Pu=BM`X$lcC+c|*$`KZz8fY!+
z+0A~QN4*UMK5vV?C*dceu2X=!mieai&aev!;ffi7bE%!mj0J~!{;n5(+uCsl|E0KS
z>)2&%brn#$^?4DI&R)PdsRX9%Z9)Z|aUJ(?M(82Cds6nZ4}o*iiwRANxQ|!4$Wt7S
z8zofHg^V7UG~yqdD3eXSgiyE(i9UlcXS-R?M2-2mUXtUA7QY5pB0
z^g0$ZYivC{7ABw=pT&go#p^|ozlvI3OwhU9r1SIKr(`tZ9#U51F^uLZZ#~tf3O9t>
zMu!cQm{f=OYaO2$THM_4(%AFOlXJP6nH|KK+La6P`&@~!p@cLwS9e0Q6ExHmuCAY>T`Vg&>VN4g*Xf8C{@G<-{a
zcVRB-{E@zB15r2f8HB(Cc_G_G-jVJ7$oJ!Z;btM<|8R*AX(ay0GC!{0+x^ea$d>+d
zj(@)826O-7gb6uX7+C*bd@lqY|8iY^alSdfdnU#Pw$4aL1{wE&U&K9rX7@L_1^Mm+B3zLFLVzzY
z`L}U^e=_Xn6SDJu5ky3nBi|vr_FJBBsh4f@}D_TK~n=`;Vgg8EIgk>dRm#~&E|
z@V$TBkys(q$O}osH@uOl-@SOwZ!r!8RX@rg>H515e<40>|5yM0B2M(({rw?B|6ciA
z`1QZ^9b&oUml)1JIq|=H`cqh8|!DYvM=b;k$M~JR)MWCeB9Y0C_t{8>GTO
zfYiT*5V$xIp8iEh0uev>8IAyc3%`8}M?euVWp*%Pxfq08kw(NMp#O?Tz&RlowTTR5*_0pZ=n~NF6LZ@FF(l)0J)}iGSI3dvBXOB)drxagpJ5JPFQ(!K&gWUxt
zPKH|6jPdi0!&phmzdXgSYPn~mw#$W=A9szB8*BGLgKgSh
z?F87*NY9T*6=IR^AM)~D9sEb3;r?%g1`)nu2O}cl--QN<2V%V*MsGsX@)^sQ>ST
z2B~0v9TA9CRSv|W{@=*_G5-EbVi6ABzZoGA#Jc(gv518&MCcqbO#U6QXxln&Ex!}{
z7d!1AaXWS(V%4y6W
zvo>Pi74nvJqvVao3Oy(XE>b8(B)bjOUp+dpBPRJ|AcU6W-Qv_(a`dbqOK-Q3D(}!$
z1%S~vfA5x%TBnXLv-D!NPTv`B#PS;LVfg+Gs+U3^?lZHK9RV`mnOZoCZC9-H$L9c=
zU>e6wNiN!$BUVCtg2M?vk9vDCBFK{iA*cJt*5D-W9nMd8n=KOi>?RmG;u#=FQ~Q
ztJU;btiSY`$aq|<7<
z7tONg=+ki73J9h3!+198KcZxYFb|HD!5yPH*Dtj);5QPYf&aByy#@Q
z190}g`N_}+lCImFLOZd-+L-j2_l>KmBnO-L1eE!%CBYlRqFBub=F?1svcj6+_=wom
zy7L3|cd%QZ^I9T0Z;H#|#yhMOmV1DSydfGpP^-1HUFWD1eZG0pQmb+EjZLvT=cjeC
zo_TLM1qC+|>jaFZ?ugZE5zy|E?H*`lbJ}zy7`m1N*jk^wWC){=<7f
zPPJeN
zzJ39N{X-r8^kKfoRJi|#tCkSu3%^*i6e}FF^q!jNN#}c;
z`{;CZY2oKtni#a7tDSpuQf>WpUKudX)9>MlT0PMbx|@8WDOM8iyN~R
zn^&qh_pZxgyk;a9yh|Ohrf{k*oLpB)HFgDm!SyU+?q$UFkcX~*k!v_84)|buMJu`P
zn*ae9PrT_Nk<6W!)P#I{a#|n6BIpN5Gj6^QH`n(+(-moXs8yRaf--*p`fb0veHQ-5
z^L{}D-K<*`%GDz^Du9FB$DiIw2%6D{U1o4e+8)Z6gC&gJxAQW(H#Mq)Gm~k2M2)gc
zlc4{upZ}JBm&&;E4Ynl65w^%yjU%EY0cQj9ye|l&?(t7?J
zV2`OdN*J@V)tn{B3CbO6-xGUOp5|go
zMT~gOT9Pv}zvFAIG^b4}P!`@au16g*C?}oG{tk;#G=07jVv9FTOc>*Jq^rnd!~!!i
z_WO#j&W>z+6b_}2)J?MWU1l~%8>ckc6)g{f{kdcNB5gBxl2y@
z?kO18Bq^mWw5=wGTwk(Ze873ol(Q>O6v0|e)_P~#RoX~uxp~EMcfx$~^%`kvA`>y&
z+}9}T_QBeZIo+Y8a&P=_8<|%gGR_=!e=x<9J!V=YdnH@F=@Ihg_6?`Q4&Vh1`Wai_)oSqQ@y{?L0M24sT238p+Ds)RaP!kZ9(hHV4LB6@$l7D*-}**=kx)6
z|J_{SBR9bHLDZqf7f+O1OB~c8mUmwmz6ZKkFVxQ@4+m
zb4_=iJiR<}M%6iVDKh&xmV9N@sX*cB&Jy_YJRQft6Lj;~>9`;&d
z<7tV%9_zD%snNEl)kYiY8K}+;`c07}SDyAI=4v_2;p5a#r~5
ze9V8uOqZW_Z|QPx(?*Ko3v^iaZY-sRdnu}7d~jKz^$;4Hg`nhi!I)<9(sj|7RMQ{z
zPrh8P%d4X#I`j9>qIFV0Y{&j8W2fTo?n6Qrn
zG_Lawx0E*8Cz?^c%EJ;RX7#K#M#ug-L@svf{KNf(LjUs+As1`mKtGXgw0=R0!3ZIs
z$Hl}tfnQx?zTq}oIJX2bE+{ftc#g)J=);hgf@?;J*6%Yp@O8S&DhwJ}+htoVLhb&r
ze2azo5@=f%uP)*xz^J}UO3p=yUGT$`9r@Jx+RWSXOS)Hg-a6t1xf-LOR}bm1@^veSUnK-Zh-W0vo``AF
zRDT*FXSWUPy4IcSd|O|))y<?1X{xr@I~nrp-N{Bd>tpZ{M_%R)o89pj7JtuwK(*sj1U7V|LVze&juO
zgYjexSC{ErlB$>WsAR&V9Sb{P6KU{5;WdQETl`(Pe{O*Jq5t8W2q*cE;L7(cA;_&<
z2*37cs0IJM1UXR!=0ldJ*sq_FS~|#iDa7e88gQeU6*@({8gpY4n^H!p&%3oOGTUGP
zsWroCj4i!+l9^17a>{A0U`r909+hJ#|Eyc@XM+jq%`
zha+H})TzxrYxbgkhGVbDY58V-*Op{+mdMo(ybdaK`gP^t73X|1f@22ZZ#~zT?fH}s
zzP!J2{XzAcY69ye@oC1M%IW-v?^gxA3-0PC;*Ng$Sd&D!+GI)EEx>(+7vBB#{6HgV
z?&_3bkg-TBtMguWrnsbc+Jk37pJMV{KV6peuc3TMq#G#M7vawN%GNlCS)o>&b2LHx
zx+hb)@sp9Q^{=%aGu~})hFaIy;8lWoQtuxPkIU{b)D$qi0zHBGH;Uc;Qc_XeFC);$
zT3l@|;vy#L_VC5RoDSNy(j@4SG}kCiPrJ-SH0TlG=Y%WHE~TPTm79kW>`eizpw+ms
zt09ujpq|1`D~I7{*YEg~tkDxMbABAamsI{(r#i=;VRDiSg->64XO%E1r|e1npgBtp
zjDH0`BJPDVi;9d01X!F^tCY)6K7b-{a@|<$I-yf{dvS}X=aOou1tnpvmQJ|k>P>t9
zXR$g8D0eS&(K6JKGUl%`3rk~XDT(>$YYFF^x-;?R^jzw`o3=uGfNz6)Wp?li847`<
zE{)V)Z(ZXN=CO7!Q@ixjHgnprBq?+n5XQ~46PSbdPTk8Evnc*hvzG<25&OYB8jth&
z#jLeN1slBKM&}~k3)QqD0}$NM=b_E4%i2qebUxU3&6!%=bCftZ*0HPddVvz&j?y=T
zEjWK;m!jADOX3#|+j8p3^y~DX-kJ}HrL#>725)IYmDe8{$@4?*R8TeB8?4uG_TfxX
zJ;!Tk(P$HR8Uw%!atI5HAWcnTZ>In$M@ocra4c2jnDR`Kln9`5)ID-t}w%PJa~U+^!>noDzHz?I9#ZU~nIBV)HQYe|
z=z#5ttZ;fbUr1r&m#>3&&DI?qPhnh&7E3{Zf!1R~U3#XYcxPcN^`wuLoA)KrSuz8c
zDP#*<%7GtKD>`Y6>#xX~ggtIbYJ5IqzW#xorXa~|{zWg03Y`!|GGIlawXG~1-nSez
z&>pcBL};~i2i1#&M{9&y(s`DfCBYo>(33;G2Jp!fl}K*9=G2mAI@5fqF`%1=w07mh
zO3*u>dFI^Rv!7Iqtop80d}{rEbAS9u#DjK`yMB+&<}#Sk&oOM9w`;dmmR*X33LAOT
z9oWdk9QldZU(PidYkr+aRT{m$~06keW;9Q+Y4t
z9WOaOe;|4aH;4_mpKbUhm||*(;4`89L|vCK78vL)XTN(n({&{`9EbLriHKcU_OWan
z{;h{=&t6LcU5R!|@ZV-y*VgX#agx0=Qm0E7Zbgx17RmUMiCXGZ#%o9wX&;|#)Tw`z
zCD!)B)GSIdu42nkYlzF9cSR4#0>5nOO0MltuZmCjM$&v0+QcYTply(qNR9yn_bH$+
z3zbS}%EU`I2D@k7%J-J_u!tKp%QUpr*Ccm`nZA&oAt~i$BqmUOeQnxULioWUs#-=B
z`5`NrvUn*^FGbL%adnZ0E3cvHcv3WL<)IigZI
zi%##-q8CM8rH&7h!k-s#x!9!&yKzb^Ax@OdyVsJ*CThqk(p%|9>g%%*$!
zIqw2tGOzDtTyns>uU8Ddy24V^?&rT)4KwPER#Cf&=c560_MeWNQ`(3LOBH=7;Pspa
z>lxD*Y*C>)e};nK&ULr9g@y3X8!5^!)Y#o6aauhyh3U>P45!-8G>l@v=+1?tI>UGi
znUl0HdRG
z^Gb-5X8HKL7fSV3fVA}(&(@;ls}(nv07sN)dDtELskcANkS&B$ni~NY{5)
zxl(clmcKj~{ls3#PHq6~RE_k^+~gNkeZ#mxZ8P$4y%P0>d#8|#0GEBQYoI0lfih=g
zgMLH3sV7}K*B0puq4h5dR5m_bOnxX+as|iQ`li>Nx0^MW)vHigMs`VT%=qQdo@7X~
zTDcJAU+<5T5n05dE$TyoUCS|C2AWt{Dz?FnM=gfB-doE~QZQI|s4ZO|mh5=y4(%HG
z;+-?TwbI0pBA_P`_2L!k$n_grp@oYpoqLM*__a&h-3*us9}W9S!L^BjmoZ{*4>swZ
z+*%M6zYhZGg*(t_Nr#ENS;O|VKdWw;Jte{}vB)D-Vw=LGo;@KBMS+}R
zNekBN`T6W|&eAomhB7pUCUX&&?%x^Msqc_IiZgD{bK1{xIAppS2ulockmpG@P6fPs
zv3%LM<4F>_rMIP%XM)pHZQ|R$8YM{Flx3P0yLsE;m%aHJ`WOC-iln0AYh6O9e;bVUWkP^NAXM(c)vevZ6Jp&DL
zD+;hQan@?x>61)M?G9eH04J+6-I!uQB~M28hpsuJCs)0mqbgEP@?QDCv-hFkb4fwe
z3_9U!RaMKpsMa)(rJ(|S^p1{ApDC>yMWN7&J1%0`47@ajy^YMb|Z}7kYNPv
zzSa?|?2VnbI=#K9*N9sNInY|zB79$&seH{6DqeQqp4|i-hKdtAP?2>g{Mp*2@zn{@
zD1BnI`dLKI%c?fVJ|TLuA@T9UBTlC;0!LAHTxPrOeO?4R0Jc^P_VhlqWR|%5mj*Dw
zuR%7x)+Mbw8fhq|%`sEYn2WY|npRzf3Bxf(`9UsL%>fx`EH7o6dfJlEv4GTTJ?}
zf%tX=iJ6fOEIK5g
zvT}pszStyJ4=@`CPrW~NM7s3d9?5ONn}wKB+0L>=@@Q1sY+KFBZYCC5i!Fc;LQw(n
zs|F9^%yTSmII!^bD8-0pGh=zwj1$%tn0OCtA20ac14+hCfCm|*j^(fHZTVTt5=CHr
zB?wN_NJ?VUy~Dc3Jk1pv*wp5ZKG!_uyuDrXC{nyyo731%Sd~93nf28z(ipuf9H?(T
zYEdOR;G-42QW_AH3=P0Ih-Wh8Wi>y&TtLK!B~bHlP76SPVxe~WYL!|F{uXD(<_3CT
z(doFIfZ=|AUXsfg46<5yPCsJZqg@HhDsQ`I(O^5{h;!7t-!x>n0BoV
zEs4cixN*LQ(R*};057|3*uBqH_6etV(w*mH(w|)k{TvsY{6qv-uFS_|dk!Cqay?Kv
zDp{cLCk)z|d3ZCQkLC;&yTtY-O9fl;bY_f)ij!d}@%l%F%}?*G6;B6<~|8o
z$U~dnpAdfu6O-WY5przN0NL-#YreI60cM3YYo-{AD9Pox+Y22Fr{ukqK1xt$ob-_s
zZ_>vZOwu(R?4d{LZ
ztOLtr`|iQ-CHWt>MbDimWwZ+%92=FqAZKA&x|Yf3C!)}aaYHGxyDjvrGvx4vv}sBv
z>}oxMEM|q$WfS$1w<;lPW2F+W82v*qXWk*!hfIlvK25#I=GK8jCtAM>&qT-yCT4W_
zW)8z8hTJkk32B$6h-G_W8>Lw^I(V&Y&0rG4iPE#4+0sXcpWb2*hQT-mvxA<{il23|
zI~g)oSi1To53e*Ckvj?7Bix}Y30OsV8XhL~>=9Sci3Y$*goF%~^jhIFCQ|7K?>ZkE
z-MyRXW;2-!*Eb~(RqxyKC{JSNCBx%C0sCHE4*$Ag_~b?44Xt%2{vOuW-p{nEnClTc
zoS3eIR+nRy@IL6XTRuczlOgHmlXwF!PDgL(d(ydFh|QKSs+V9;lDdk)P{%Q^kc^Yv
z8pg@eU_2ssEAzwa+dDX!(tW%#v7ZlI_|DammKfB?dS(@jOG8X^YW$#rKAM6F)wkQe
zQm3{|EkJOtnI^xTxS`;`9}C8PnqBgOFnkC^ih^QC>6Shlx*BKkn0H;HU-W*!?c`Zd
zTeLKhbj|D(uc>-Tw6`})OVK5r>Ly=aQz^i1mi1P6$>Tc}B`7&RzE_1HN*Kpi-j05{O9*%DXBX8A;{%RFwI|C|Y~0
z1nc#war)`7`{q>l8%VOMkKU>n5VcHNLn1ph;BE_vcg%CZ6^`c7H+jrUJOUrQ=2sYS
z+TwD=grh!W%0#p*=$Q8GQah9=L9hz2fQq11ph`7#e#x
zr)|m=HmaK(5&KLoHrC*dYWl&+LBPiS8BaEufyd|7MZ4QIX*UxD3!-rbD`Pn<_U!6N
z42@j&Dg`rlgQJb!@ZD^|G>nwit@h&ZGmjkj(5?PHp_eLKM6rOH_^tZ>aJFQA>5RBx
zyd@7IO(dKSK56wuc{%-tliGQ^39GEcF`v)-=h9jJWceDqZ?>f
z=w;tfp=rhM;Vj%kds=Vj&LyC;6>@hhzHo#eHC1HPvWz23nuNgfog{lg;le;q#X(Z&wYe3Py>?k0<~L)*R#`TjX>-(fjCEN5I%(9-d)Rk6M#P&RZgZRqTKV
zNtd{9+Kfp)eqVY#865$~I1b)(@mRex73<{xaKV=QwfNMI-!SVN)^&b54U`2Uk7-`h
zydJf!CzrraGc{bDrdRd$4dhpN4&}xg#&|6NT|v`Hj|Zcr2VYaP+p(Cw4)AKuDiPA+
z!MQP$K@3C
zE|K`i)^iuC#SBdoN*7_DO4}kgnxjVqIWzXoZ1edYF?%7}?yQnb)%==4vL;_`$h?!v
zL)Y6O*rg4_s%j(fJvjK>QQ1~a38f*OXIsFDhANhmnNo@>fU0YLirT?TM~sP!Ae+NN
zY+y%PZ~C&DX8Ww$^>j>Zi|D1Y>_zqBU>oTh^VzRg8@*;$b&w-=X*MM8^faeq2WYfjXfcDZ+DIAch_npQ*D{!jxwL}tdp6nX
z8b6DXG-|xt9POw$!&G>E$M@fk`rpD^c6p|D=P;%n(+Wy&LO-arE8tiMVU0ZSd
zS~c-Wh9B59=bph+)i+mT@dIw#rO}D^4GP}5t2;`m0}+@?t+{gm2ic_Z)`nm8#eQ3>
zB3?qV;PzVE$>li}voKB{1tz|Enq2$Vh!5VEjGOt>EHG@CUVrP7?Vr
zc`moSnw?m=yJ*E)3oH@Gg8p~?82VUC%c?Za&%MSe1>z20q~cRIlyk1San5L*nFXOg
z)|=R}VSzbv~xXRA|Vn | |