####################################################################################################
#
# PySpice - A Spice Package for Python
# Copyright (C) 2014 Fabrice Salvaire
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
####################################################################################################
####################################################################################################
"""This 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__)
####################################################################################################
####################################################################################################
[docs]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
##############################################
[docs] def str(self, spice=False):
if spice:
return self.spice_prefix
else:
return self.PREFIX
####################################################################################################
[docs]class ZeroPower(UnitPrefix):
POWER = 0
PREFIX = ''
SPICE_PREFIX = ''
_zero_power = UnitPrefixMetaclass.get(0)
####################################################################################################
[docs]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)
##############################################
[docs] @classmethod
def new_powers(cls):
return {unit: 0 for unit in cls.BASE_UNITS}
##############################################
[docs] @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
##############################################
[docs] @classmethod
def to_hash(cls, powers):
hash_ = ''
for unit in cls.BASE_UNITS:
hash_ += str(powers[unit])
return hash_
##############################################
[docs] @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
[docs] 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
[docs] def is_unit_less(self):
return self._hash == '0'*len(self.BASE_UNITS)
##############################################
def __bool__(self):
return not self.is_unit_less()
##############################################
[docs] 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
##############################################
[docs] def power(self, value):
powers = {unit: self._powers[unit] * value
for unit in self.BASE_UNITS}
return self.__class__(powers=powers)
##############################################
[docs] def reciprocal(self):
return self.power(-1)
##############################################
[docs] def sqrt(self):
return self.power(1/2)
##############################################
[docs] def square(self):
return self.power(2)
##############################################
[docs] def cbrt(self):
return self.power(1/3)
####################################################################################################
####################################################################################################
[docs]class UnitError(ValueError):
pass
####################################################################################################
[docs]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()
##############################################
[docs] @classmethod
def is_default_unit(cls):
return cls.DEFAULT_UNIT
[docs] @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)
##############################################
[docs] 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)
##############################################
[docs] 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)
##############################################
[docs] def power(self, exponent, prefixed_unit=False):
si_unit = self._si_unit.power(exponent)
return self._equivalent_unit_or_power(si_unit, prefixed_unit)
##############################################
[docs] def reciprocal(self, prefixed_unit=False):
si_unit = self._si_unit.reciprocal()
return self._equivalent_unit_or_power(si_unit, prefixed_unit)
##############################################
[docs] def sqrt(self, prefixed_unit=False):
si_unit = self._si_unit.sqrt()
return self._equivalent_unit_or_power(si_unit, prefixed_unit)
##############################################
[docs] def square(self, prefixed_unit=False):
si_unit = self._si_unit.square()
return self._equivalent_unit_or_power(si_unit, prefixed_unit)
##############################################
[docs] 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)
##############################################
[docs] def is_same_unit(self, value):
return value.unit == self
##############################################
[docs] 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)
####################################################################################################
[docs]class SiBaseUnit(Unit):
"""This class implements an SI base unit."""
##############################################
[docs] @classmethod
def is_base_unit(cls):
return True
##############################################
[docs] @classmethod
def is_default_unit(cls):
return True
####################################################################################################
[docs]class PrefixedUnit:
"""This class implements a prefixed unit.
"""
_unit_map = {} # Prefixed unit singletons
_prefixed_unit_map = {}
_value_ctor = None
_values_ctor = None
##############################################
[docs] @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
##############################################
[docs] @classmethod
def from_si_unit(cls, si_unit):
return cls._unit_map.get(si_unit.hash, None)
##############################################
[docs] @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
##############################################
[docs] def clone(self):
return self.__class__(self._unit, self._power)
##############################################
[docs] def is_same_unit(self, other):
return self._unit == other.unit
##############################################
[docs] def check_unit(self, other):
if not self.is_same_unit(other):
raise UnitError('{} versus {}'.format(self, other))
##############################################
[docs] 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)
##############################################
[docs] 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
##############################################
[docs] def str_spice(self):
return self.str(spice=True, unit=True)
##############################################
def __str__(self):
return self.str(spice=False, unit=True)
##############################################
[docs] 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)
####################################################################################################
[docs]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')
##############################################
[docs] @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
##############################################
[docs] def clone(self):
return self.__class__(self._prefixed_unit, self._value)
##############################################
[docs] 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)
##############################################
[docs] 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
##############################################
[docs] 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)
##############################################
[docs] 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
##############################################
[docs] def str_space(self):
return self.str(space=True)
##############################################
[docs] 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))
##############################################
[docs] def reciprocal(self):
equivalent_unit = self.unit.reciprocal(prefixed_unit=True)
reciprocal_value = 1. / float(self)
return equivalent_unit.new_value(reciprocal_value)
##############################################
[docs] 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))
##############################################
[docs] 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)
##############################################
[docs] 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)
##############################################
[docs] 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
####################################################################################################
[docs]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,
}
##############################################
[docs] @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=<ufunc 'multiply'>
# method=__call__
# inputs=(UnitValues(mV, [0 1 2 3 4 5 6 7 8 9]), 2)
# ufunc=<ufunc 'sin'>
# method=__call__
# inputs=(UnitValues(mV, [0 1 2 3 4 5 6 7 8 9]),)
# kwargs={}
# ufunc=<ufunc 'add'>
# 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=<ufunc 'add'>
# 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)
##############################################
[docs] 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__()
# ( <built-in function _reconstruct>,
# (<class 'PySpice.Unit.Unit.UnitValues'>, (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
##############################################
[docs] 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
##############################################
[docs] 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)
##############################################
[docs] 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)
##############################################
[docs] 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))
##############################################
[docs] 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)
##############################################
[docs] 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()
####################################################################################################
[docs]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)
####################################################################################################
[docs]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