####################################################################################################
#
# 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