Source code for brian2.units.fundamentalunits

"""
Defines physical units and quantities

=====================  ========  ======
Quantity               Unit      Symbol
---------------------  --------  ------
Length                 metre     m
Mass                   kilogram  kg
Time                   second    s
Electric current       ampere    A
Temperature            kelvin    K
Quantity of substance  mole      mol
Luminosity             candle    cd
=====================  ========  ======
"""
from __future__ import division

import numbers
import collections
from warnings import warn
import operator
import itertools

import numpy as np
from sympy import latex

__all__ = [
    'DimensionMismatchError', 'get_or_create_dimension',
    'get_dimensions', 'is_dimensionless', 'have_same_dimensions',
    'in_unit', 'in_best_unit', 'Quantity', 'Unit', 'register_new_unit',
    'check_units', 'is_scalar_type', 'get_unit', 'get_unit_fast',
    'unit_checking'
    ]


unit_checking = True

def _flatten(iterable):
    '''
    Flatten a given list `iterable`.
    '''
    for e in iterable:
        if isinstance(e, list):
            for f in _flatten(e):
                yield f
        else:
            yield e


def _short_str(arr):
    '''
    Return a short string representation of an array, suitable for use in
    error messages.
    '''
    arr = np.asanyarray(arr)
    old_printoptions = np.get_printoptions()
    np.set_printoptions(edgeitems=2, threshold=5)
    arr_string = str(arr)
    np.set_printoptions(**old_printoptions)
    return arr_string

#===============================================================================
# Numpy ufuncs
#===============================================================================

# Note: A list of numpy ufuncs can be found here:
# http://docs.scipy.org/doc/numpy/reference/ufuncs.html#available-ufuncs

#: ufuncs that work on all dimensions and preserve the dimensions, e.g. abs
UFUNCS_PRESERVE_DIMENSIONS = ['absolute', 'rint', 'negative', 'conj',
                              'conjugate', 'floor', 'ceil', 'trunc']

#: ufuncs that work on all dimensions but change the dimensions, e.g. square
UFUNCS_CHANGE_DIMENSIONS = ['multiply', 'divide', 'true_divide',
                            'floor_divide', 'sqrt', 'square', 'reciprocal',
                            'dot']

#: ufuncs that work with matching dimensions, e.g. add
UFUNCS_MATCHING_DIMENSIONS = ['add', 'subtract', 'maximum', 'minimum',
                              'remainder', 'mod', 'fmod']

#: ufuncs that compare values, i.e. work only with matching dimensions but do
#: not result in a value with dimensions, e.g. equals
UFUNCS_COMPARISONS = ['less', 'less_equal', 'greater', 'greater_equal',
                      'equal', 'not_equal']

#: Logical operations that work on all quantities and return boolean arrays
UFUNCS_LOGICAL = ['logical_and', 'logical_or', 'logical_xor', 'logical_not',
                  'isreal', 'iscomplex', 'isfinite', 'isinf', 'isnan']

#: ufuncs that only work on dimensionless quantities
UFUNCS_DIMENSIONLESS = ['sin', 'sinh', 'arcsin', 'arcsinh', 'cos', 'cosh',
                        'arccos', 'arccosh', 'tan', 'tanh', 'arctan',
                        'arctanh', 'log', 'log2', 'log10', 'log1p',
                        'exp', 'exp2', 'expm1']

#: ufuncs that only work on two dimensionless quantities
UFUNCS_DIMENSIONLESS_TWOARGS = ['logaddexp', 'logaddexp2', 'arctan2',
                                'hypot']

#: ufuncs that only work on integers and therefore never on quantities
UFUNCS_INTEGERS = ['bitwise_and', 'bitwise_or', 'bitwise_xor', 'invert',
                   'left_shift', 'right_shift']


#==============================================================================
# Utility functions
#==============================================================================

[docs]def fail_for_dimension_mismatch(obj1, obj2=None, error_message=None, **error_quantities): ''' Compare the dimensions of two objects. Parameters ---------- obj1, obj2 : {array-like, `Quantity`} The object to compare. If `obj2` is ``None``, assume it to be dimensionless error_message : str, optional An error message that is used in the DimensionMismatchError error_quantities : dict mapping str to `Quantity`, optional Quantities in this dictionary will be converted using the `_short_str` helper method and inserted into the ``error_message`` (which should have placeholders with the corresponding names). The reason for doing this in a somewhat complicated way instead of directly including all the details in ``error_messsage`` is that converting large quantity arrays to strings can be rather costly and we don't want to do it if no error occured. Returns ------- dim1, dim2 : `Dimension`, `Dimension` The dimensions of the two arguments (so that later code does not need to get the dimensions again). Raises ------ DimensionMismatchError If the dimensions of `obj1` and `obj2` do not match (or, if `obj2` is ``None``, in case `obj1` is not dimensionsless). Notes ----- Implements special checking for ``0``, treating it as having "any dimensions". ''' if not unit_checking: return None, None dim1 = get_dimensions(obj1) if obj2 is None: dim2 = DIMENSIONLESS else: dim2 = get_dimensions(obj2) if dim1 is not dim2: # Special treatment for "0": # if it is not a Quantity, it has "any dimension". # This allows expressions like 3*mV + 0 to pass (useful in cases where # zero is treated as the neutral element, e.g. in the Python sum # builtin) or comparisons like 3 * mV == 0 to return False instead of # failing # with a DimensionMismatchError. Note that 3*mV == 0*second # is not allowed, though. if ((dim1 is DIMENSIONLESS and np.all(obj1 == 0)) or (dim2 is DIMENSIONLESS and np.all(obj2 == 0))): return dim1, dim2 # We do another check here, this should allow Brian1 units to pass as # having the same dimensions as a Brian2 unit if dim1 == dim2: return dim1, dim2 if error_message is None: error_message = 'Dimension mismatch' else: error_quantities = {name: _short_str(q) for name, q in error_quantities.iteritems()} error_message = error_message.format(**error_quantities) # If we are comparing an object to a specific unit, we don't want to # restate this unit (it is probably mentioned in the text already) if obj2 is None or isinstance(obj2, Unit): raise DimensionMismatchError(error_message, dim1) else: raise DimensionMismatchError(error_message, dim1, dim2) else: return dim1, dim2
[docs]def wrap_function_dimensionless(func): ''' Returns a new function that wraps the given function `func` so that it raises a DimensionMismatchError if the function is called on a quantity with dimensions (excluding dimensionless quantitities). Quantities are transformed to unitless numpy arrays before calling `func`. These checks/transformations apply only to the very first argument, all other arguments are ignored/untouched. ''' def f(x, *args, **kwds): # pylint: disable=C0111 fail_for_dimension_mismatch(x, error_message=('%s expects a dimensionless ' 'argument but got ' '{value}' % func.__name__), value=x) return func(np.array(x, copy=False), *args, **kwds) f._arg_units = [1] f._return_unit = 1 f.__name__ = func.__name__ f.__doc__ = func.__doc__ return f
[docs]def wrap_function_keep_dimensions(func): ''' Returns a new function that wraps the given function `func` so that it keeps the dimensions of its input. Quantities are transformed to unitless numpy arrays before calling `func`, the output is a quantity with the original dimensions re-attached. These transformations apply only to the very first argument, all other arguments are ignored/untouched, allowing to work functions like ``sum`` to work as expected with additional ``axis`` etc. arguments. ''' def f(x, *args, **kwds): # pylint: disable=C0111 return Quantity(func(np.array(x, copy=False), *args, **kwds), dim=x.dim) f._arg_units = [None] f._return_unit = lambda u : u f.__name__ = func.__name__ f.__doc__ = func.__doc__ return f
[docs]def wrap_function_change_dimensions(func, change_dim_func): ''' Returns a new function that wraps the given function `func` so that it changes the dimensions of its input. Quantities are transformed to unitless numpy arrays before calling `func`, the output is a quantity with the original dimensions passed through the function `change_dim_func`. A typical use would be a ``sqrt`` function that uses ``lambda d: d ** 0.5`` as ``change_dim_func``. These transformations apply only to the very first argument, all other arguments are ignored/untouched. ''' def f(x, *args, **kwds): # pylint: disable=C0111 ar = np.array(x, copy=False) return Quantity(func(ar, *args, **kwds), dim=change_dim_func(ar, x.dim)) f._arg_units = [None] f._return_unit = change_dim_func f.__name__ = func.__name__ f.__doc__ = func.__doc__ return f
[docs]def wrap_function_remove_dimensions(func): ''' Returns a new function that wraps the given function `func` so that it removes any dimensions from its input. Useful for functions that are returning integers (indices) or booleans, irrespective of the datatype contained in the array. These transformations apply only to the very first argument, all other arguments are ignored/untouched. ''' def f(x, *args, **kwds): # pylint: disable=C0111 return func(np.array(x, copy=False), *args, **kwds) f._arg_units = [None] f._return_unit = 1 f.__name__ = func.__name__ f.__doc__ = func.__doc__ return f
# SI dimensions (see table at the top of the file) and various descriptions, # each description maps to an index i, and the power of each dimension # is stored in the variable dims[i] _di = {"Length": 0, "length": 0, "metre": 0, "metres": 0, "meter": 0, "meters": 0, "m": 0, "Mass": 1, "mass": 1, "kilogram": 1, "kilograms": 1, "kg": 1, "Time": 2, "time": 2, "second": 2, "seconds": 2, "s": 2, "Electric Current":3, "electric current": 3, "Current": 3, "current": 3, "ampere": 3, "amperes": 3, "A": 3, "Temperature": 4, "temperature": 4, "kelvin": 4, "kelvins": 4, "K": 4, "Quantity of Substance": 5, "Quantity of substance": 5, "quantity of substance": 5, "Substance": 5, "substance": 5, "mole": 5, "moles": 5, "mol": 5, "Luminosity": 6, "luminosity": 6, "candle": 6, "candles": 6, "cd": 6} _ilabel = ["m", "kg", "s", "A", "K", "mol", "cd"] # The same labels with the names used for constructing them in Python code _iclass_label = ["metre", "kilogram", "second", "amp", "kelvin", "mole", "candle"] # SI unit _prefixes, see table at end of file _siprefixes = {"y": 1e-24, "z": 1e-21, "a": 1e-18, "f": 1e-15, "p": 1e-12, "n": 1e-9, "u": 1e-6, "m": 1e-3, "c": 1e-2, "d": 1e-1, "": 1, "da": 1e1, "h": 1e2, "k": 1e3, "M": 1e6, "G": 1e9, "T": 1e12, "P": 1e15, "E": 1e18, "Z": 1e21, "Y": 1e24}
[docs]class Dimension(object): ''' Stores the indices of the 7 basic SI unit dimension (length, mass, etc.). Provides a subset of arithmetic operations appropriate to dimensions: multiplication, division and powers, and equality testing. Parameters ---------- dims : sequence of `float` The dimension indices of the 7 basic SI unit dimensions. Notes ----- Users shouldn't use this class directly, it is used internally in Quantity and Unit. Even internally, never use ``Dimension(...)`` to create a new instance, use `get_or_create_dimension` instead. This function makes sure that only one Dimension instance exists for every combination of indices, allowing for a very fast dimensionality check with ``is``. ''' __slots__ = ["_dims"] __array_priority__ = 1000 #### INITIALISATION #### def __init__(self, dims): self._dims = dims #### METHODS ####
[docs] def get_dimension(self, d): """ Return a specific dimension. Parameters ---------- d : `str` A string identifying the SI basic unit dimension. Can be either a description like "length" or a basic unit like "m" or "metre". Returns ------- dim : `float` The dimensionality of the dimension `d`. """ return self._dims[_di[d]]
@property def is_dimensionless(self): ''' Whether this Dimension is dimensionless. Notes ----- Normally, instead one should check dimension for being identical to `DIMENSIONLESS`. ''' return all([x == 0 for x in self._dims]) @property def dim(self): ''' Returns the `Dimension` object itself. This can be useful, because it allows to check for the dimension of an object by checking its ``dim`` attribute -- this will return a `Dimension` object for `Quantity`, `Unit` and `Dimension`. ''' return self #### REPRESENTATION #### def _str_representation(self, python_code=False): """ String representation in basic SI units, or ``"1"`` for dimensionless. Use ``python_code=False`` for display purposes and ``True`` for valid Python code. """ if python_code: power_operator = " ** " else: power_operator = "^" parts = [] for i in range(len(self._dims)): if self._dims[i]: if python_code: s = _iclass_label[i] else: s = _ilabel[i] if self._dims[i] != 1: s += power_operator + str(self._dims[i]) parts.append(s) if python_code: s = " * ".join(parts) if not len(s): return "%s()" % self.__class__.__name__ else: s = " ".join(parts) if not len(s): return "1" return s.strip() def _latex(self, *args): parts = [] for i in xrange(len(self._dims)): if self._dims[i]: s = _ilabel[i] if self._dims[i] != 1: s += '^{%s}' % str(self._dims[i]) parts.append(s) s = "\,".join(parts) if not len(s): return "1" return s.strip() def _repr_latex(self): return '$%s$' % latex(self) def __repr__(self): return self._str_representation(python_code=True) def __str__(self): return self._str_representation(python_code=False) #### ARITHMETIC #### # Note that none of the dimension arithmetic objects do sanity checking # on their inputs, although most will throw an exception if you pass the # wrong sort of input def __mul__(self, value): return get_or_create_dimension([x + y for x, y in itertools.izip(self._dims, value._dims)]) def __div__(self, value): return get_or_create_dimension([x - y for x, y in itertools.izip(self._dims, value._dims)]) def __truediv__(self, value): return self.__div__(value) def __pow__(self, value): value = np.array(value, copy=False) if value.size > 1: raise TypeError('Too many exponents') return get_or_create_dimension([x * value for x in self._dims]) def __imul__(self, value): raise TypeError('Dimension object is immutable') def __idiv__(self, value): raise TypeError('Dimension object is immutable') def __itruediv__(self, value): raise TypeError('Dimension object is immutable') def __ipow__(self, value): raise TypeError('Dimension object is immutable') #### COMPARISON #### def __eq__(self, value): return np.allclose(self._dims, value._dims) def __ne__(self, value): return not self.__eq__(value) def __hash__(self): return hash(self._dims) #### MAKE DIMENSION PICKABLE #### def __getstate__(self): return self._dims def __setstate__(self, state): self._dims = state
#: The singleton object for dimensionless Dimensions. DIMENSIONLESS = Dimension((0, 0, 0, 0, 0, 0, 0)) _dimensions = {(0, 0, 0, 0, 0, 0, 0): DIMENSIONLESS}
[docs]def get_or_create_dimension(*args, **kwds): """ Create a new Dimension object or get a reference to an existing one. This function takes care of only creating new objects if they were not created before and otherwise returning a reference to an existing object. This allows to compare dimensions very efficiently using ``is``. Parameters ---------- args : sequence of `float` A sequence with the indices of the 7 elements of an SI dimension. kwds : keyword arguments a sequence of ``keyword=value`` pairs where the keywords are the names of the SI dimensions, or the standard unit. Examples -------- The following are all definitions of the dimensions of force >>> from brian2 import * >>> get_or_create_dimension(length=1, mass=1, time=-2) metre * kilogram * second ** -2 >>> get_or_create_dimension(m=1, kg=1, s=-2) metre * kilogram * second ** -2 >>> get_or_create_dimension([1, 1, -2, 0, 0, 0, 0]) metre * kilogram * second ** -2 Notes ----- The 7 units are (in order): Length, Mass, Time, Electric Current, Temperature, Quantity of Substance, Luminosity and can be referred to either by these names or their SI unit names, e.g. length, metre, and m all refer to the same thing here. """ if len(args): # initialisation by list dims = args[0] try: if len(dims) != 7: raise TypeError() except TypeError: raise TypeError('Need a sequence of exactly 7 items') else: # initialisation by keywords dims = [0, 0, 0, 0, 0, 0, 0] for k in kwds.keys(): # _di stores the index of the dimension with name 'k' dims[_di[k]] = kwds[k] dims = tuple(dims) # check whether this Dimension object has already been created if dims in _dimensions: return _dimensions[dims] else: new_dim = Dimension(dims) _dimensions[dims] = new_dim return new_dim
[docs]class DimensionMismatchError(Exception): """ Exception class for attempted operations with inconsistent dimensions. For example, ``3*mvolt + 2*amp`` raises this exception. The purpose of this class is to help catch errors based on incorrect units. The exception will print a representation of the dimensions of the two inconsistent objects that were operated on. Parameters ---------- description : ``str`` A description of the type of operation being performed, e.g. Addition, Multiplication, etc. dims : ``Dimension`` The dimensions of the objects involved in the operation, any number of them is possible """ def __init__(self, description, *dims): # Call the base class constructor to make Exception pickable, see: # http://bugs.python.org/issue1692335 Exception.__init__(self, description, *dims) self.dims = dims self.desc = description def __repr__(self): dims_repr = [repr(dim) for dim in self.dims] return '%s(%r, %s)' % (self.__class__.__name__, self.desc, ', '.join(dims_repr)) def __str__(self): s = self.desc if len(self.dims)==0: pass elif len(self.dims) == 1: s += ' (unit is ' + get_unit_for_display(self.dims[0]) elif len(self.dims) == 2: d1, d2 = self.dims s += ' (units are %s and %s' % (get_unit_for_display(d1), get_unit_for_display(d2)) else: s += (' (units are ' + ' '.join(['(' + get_unit_for_display(d) + ')' for d in self.dims])) if len(self.dims): s += ').' return s
[docs]def is_scalar_type(obj): """ Tells you if the object is a 1d number type. Parameters ---------- obj : `object` The object to check. Returns ------- scalar : `bool` ``True`` if `obj` is a scalar that can be interpreted as a dimensionless `Quantity`. """ try: return obj.ndim == 0 except AttributeError: return np.isscalar(obj) and not isinstance(obj, basestring)
[docs]def get_dimensions(obj): """ Return the dimensions of any object that has them. Slightly more general than `Quantity.dimensions` because it will return `DIMENSIONLESS` if the object is of number type but not a `Quantity` (e.g. a `float` or `int`). Parameters ---------- obj : `object` The object to check. Returns ------- dim: `Dimension` The dimensions of the `obj`. """ try: return obj.dim except AttributeError: # The following is not very pretty, but it will avoid the costly # isinstance check for the common types if (type(obj) in [int, long, float, np.int32, np.int64, np.float32, np.float64, np.ndarray] or isinstance(obj, (numbers.Number, np.number, np.ndarray))): return DIMENSIONLESS try: return Quantity(obj).dim except TypeError: raise TypeError('Object of type %s does not have dimensions' % type(obj))
[docs]def is_dimensionless(obj): """ Test if a value is dimensionless or not. Parameters ---------- obj : `object` The object to check. Returns ------- dimensionless : `bool` ``True`` if `obj` is dimensionless. """ return get_dimensions(obj) is DIMENSIONLESS
[docs]def have_same_dimensions(obj1, obj2): """Test if two values have the same dimensions. Parameters ---------- obj1, obj2 : {`Quantity`, array-like, number} The values of which to compare the dimensions. Returns ------- same : `bool` ``True`` if `obj1` and `obj2` have the same dimensions. """ if not unit_checking: return True # ignore units when unit checking is disabled # If dimensions are consistently created using get_or_create_dimensions, # the fast "is" comparison should always return the correct result. # To be safe, we also do an equals comparison in case it fails. This # should only add a small amount of unnecessary computation for cases in # which this function returns False which very likely leads to a # DimensionMismatchError anyway. dim1 = get_dimensions(obj1) dim2 = get_dimensions(obj2) return (dim1 is dim2) or (dim1 == dim2)
[docs]def in_unit(x, u, precision=None): """ Display a value in a certain unit with a given precision. Parameters ---------- x : {`Quantity`, array-like, number} The value to display u : {`Quantity`, `Unit`} The unit to display the value `x` in. precision : `int`, optional The number of digits of precision (in the given unit, see Examples). If no value is given, numpy's `get_printoptions` value is used. Returns ------- s : `str` A string representation of `x` in units of `u`. Examples -------- >>> from brian2 import * >>> in_unit(3 * volt, mvolt) '3000. mV' >>> in_unit(123123 * msecond, second, 2) '123.12 s' >>> in_unit(10 * uA/cm**2, nA/um**2) '1.00000000e-04 nA/um^2' >>> in_unit(10 * mV, ohm * amp) '0.01 ohm A' >>> in_unit(10 * nS, ohm) # doctest: +NORMALIZE_WHITESPACE ... # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... DimensionMismatchError: Non-matching unit for method "in_unit", dimensions were (m^-2 kg^-1 s^3 A^2) (m^2 kg s^-3 A^-2) See Also -------- Quantity.in_unit """ if is_dimensionless(x): fail_for_dimension_mismatch(x, u, 'Non-matching unit for function ' '"in_unit"') return str(np.array(x / u, copy=False)) else: return x.in_unit(u, precision=precision)
[docs]def in_best_unit(x, precision=None): """ Represent the value in the "best" unit. Parameters ---------- x : {`Quantity`, array-like, number} The value to display precision : `int`, optional The number of digits of precision (in the best unit, see Examples). If no value is given, numpy's `get_printoptions` value is used. Returns ------- representation : `str` A string representation of this `Quantity`. Examples -------- >>> from brian2.units import * >>> in_best_unit(0.00123456 * volt) '1.23456 mV' >>> in_best_unit(0.00123456 * volt, 2) '1.23 mV' >>> in_best_unit(0.123456) '0.123456' >>> in_best_unit(0.123456, 2) '0.12' See Also -------- Quantity.in_best_unit """ if is_dimensionless(x): if precision is None: precision = np.get_printoptions()['precision'] return str(np.round(x, precision)) u = x._get_best_unit() return x.in_unit(u, precision=precision)
[docs]def quantity_with_dimensions(floatval, dims): ''' Create a new `Quantity` with the given dimensions. Calls `get_or_create_dimensions` with the dimension tuple of the `dims` argument to make sure that unpickling (which calls this function) does not accidentally create new Dimension objects which should instead refer to existing ones. Parameters ---------- floatval : `float` The floating point value of the quantity. dims : `Dimension` The dimensions of the quantity. Returns ------- q : `Quantity` A quantity with the given dimensions. Examples -------- >>> from brian2 import * >>> quantity_with_dimensions(0.001, volt.dim) 1. * mvolt See Also -------- get_or_create_dimensions ''' return Quantity(floatval, get_or_create_dimension(dims._dims))
[docs]class Quantity(np.ndarray, object): """ A number with an associated physical dimension. In most cases, it is not necessary to create a Quantity object by hand, instead use multiplication and division of numbers with the constant unit names ``second``, ``kilogram``, etc. Notes ----- The `Quantity` class defines arithmetic operations which check for consistency of dimensions and raise the DimensionMismatchError exception if they are inconsistent. It also defines default and other representations for a number for printing purposes. See the documentation on the Unit class for more details about the available unit names like mvolt, etc. *Casting rules* The rules that define the casting operations for Quantity object are: 1. Quantity op Quantity = Quantity Performs dimension checking if appropriate 2. (Scalar or Array) op Quantity = Quantity Assumes that the scalar or array is dimensionless There is one exception to the above rule, the number ``0`` is interpreted as having "any dimension". Examples -------- >>> from brian2 import * >>> I = 3 * amp # I is a Quantity object >>> R = 2 * ohm # same for R >>> I * R 6. * volt >>> (I * R).in_unit(mvolt) '6000. mV' >>> (I * R) / mvolt 6000.0 >>> X = I + R # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... DimensionMismatchError: Addition, dimensions were (A) (m^2 kg s^-3 A^-2) >>> Is = np.array([1, 2, 3]) * amp >>> Is * R array([ 2., 4., 6.]) * volt >>> np.asarray(Is * R) # gets rid of units array([ 2., 4., 6.]) See also -------- Unit Attributes ---------- dimensions is_dimensionless dim : Dimensions The dimensions of this quantity. Methods ------- with_dimensions has_same_dimensions in_unit in_best_unit """ __slots__ = ["dim"] __array_priority__ = 1000 #========================================================================== # Construction and handling of numpy ufuncs #========================================================================== def __new__(cls, arr, dim=None, dtype=None, copy=False, force_quantity=False): # Do not create dimensionless quantities, use pure numpy arrays instead if dim is DIMENSIONLESS and not force_quantity: arr = np.array(arr, dtype=dtype, copy=copy) if arr.shape == (): # For scalar values, return a simple Python object instead of # a numpy scalar return arr.item() return arr # All np.ndarray subclasses need something like this, see # http://www.scipy.org/Subclasses subarr = np.array(arr, dtype=dtype, copy=copy).view(cls) # We only want numerical datatypes if not np.issubclass_(np.dtype(subarr.dtype).type, (np.number, np.bool_)): raise TypeError('Quantities can only be created from numerical data.') # If a dimension is given, force this dimension if dim is not None: subarr.dim = dim return subarr # Use the given dimension or the dimension of the given array (if any) try: subarr.dim = arr.dim except AttributeError: if not isinstance(arr, (np.ndarray, np.number, numbers.Number)): # check whether it is an iterable containing Quantity objects try: is_quantity = [isinstance(x, Quantity) for x in _flatten(arr)] except TypeError: # Not iterable is_quantity = [False] if len(is_quantity) == 0: # Empty list subarr.dim = DIMENSIONLESS elif all(is_quantity): dims = [x.dim for x in _flatten(arr)] one_dim = dims[0] for d in dims: if d != one_dim: raise DimensionMismatchError('Mixing quantities ' 'with different ' 'dimensions is not ' 'allowed', d, one_dim) subarr.dim = dims[0] elif any(is_quantity): raise TypeError('Mixing quantities and non-quantities is ' 'not allowed.') return subarr def __array_finalize__(self, orig): self.dim = getattr(orig, 'dim', DIMENSIONLESS) def __array_prepare__(self, array, context=None): if context is None: return array uf, args, _ = context if uf.__name__ in (UFUNCS_PRESERVE_DIMENSIONS + UFUNCS_CHANGE_DIMENSIONS + UFUNCS_LOGICAL): # always allowed pass elif uf.__name__ in UFUNCS_INTEGERS: # Numpy should already raise a TypeError by itself raise TypeError('%s cannot be used on quantities.' % uf.__name__) elif uf.__name__ in UFUNCS_MATCHING_DIMENSIONS + UFUNCS_COMPARISONS: # Ok if dimension of arguments match fail_for_dimension_mismatch(args[0], args[1], error_message=('Cannot calculate ' '{val1} %s {val2}, the ' 'units do not ' 'match') % uf.__name__, val1=args[0], val2=args[1]) elif uf.__name__ in UFUNCS_DIMENSIONLESS: # Ok if argument is dimensionless fail_for_dimension_mismatch(args[0], error_message=('%s expects a ' 'dimensionless argument ' 'but got ' '{value}') % uf.__name__, value=args[0]) elif uf.__name__ in UFUNCS_DIMENSIONLESS_TWOARGS: # Ok if both arguments are dimensionless fail_for_dimension_mismatch(args[0], error_message=('Both arguments for ' '"%s" should be ' 'dimensionless but ' 'first argument was ' '{value}') % uf.__name__, value=args[0]) fail_for_dimension_mismatch(args[1], error_message=('Both arguments for ' '"%s" should be ' 'dimensionless but ' 'second argument was ' '{value}') % uf.__name__, value=args[1]) elif uf.__name__ == 'power': fail_for_dimension_mismatch(args[1], error_message='The exponent for a ' 'power operation has to ' 'be dimensionless but ' 'was {value}', value=args[1]) if np.array(args[1], copy=False).size != 1: raise TypeError('Only length-1 arrays can be used as an ' 'exponent for quantities.') elif uf.__name__ in ('sign', 'ones_like'): return np.array(array, copy=False) else: warn("Unknown ufunc '%s' in __array_prepare__" % uf.__name__) return array def __array_wrap__(self, array, context=None): dim = DIMENSIONLESS if not context is None: uf, args, _ = context if uf.__name__ in (UFUNCS_PRESERVE_DIMENSIONS + UFUNCS_MATCHING_DIMENSIONS): dim = self.dim elif uf.__name__ in (UFUNCS_DIMENSIONLESS + UFUNCS_DIMENSIONLESS_TWOARGS): # We should have been arrived here only for dimensionless # quantities dim = DIMENSIONLESS elif uf.__name__ in (UFUNCS_COMPARISONS + UFUNCS_LOGICAL + ['sign', 'ones_like']): # Do not touch the return value (boolean or integer array) return array elif uf.__name__ == 'sqrt': dim = self.dim ** 0.5 elif uf.__name__ == 'power': dim = get_dimensions(args[0]) ** np.array(args[1], copy=False) elif uf.__name__ == 'square': dim = self.dim ** 2 elif uf.__name__ in ('divide', 'true_divide', 'floor_divide'): dim = get_dimensions(args[0]) / get_dimensions(args[1]) elif uf.__name__ == 'reciprocal': dim = get_dimensions(args[0]) ** -1 elif uf.__name__ in ('multiply', 'dot'): dim = get_dimensions(args[0]) * get_dimensions(args[1]) else: warn("Unknown ufunc '%s' in __array_wrap__" % uf.__name__) #TODO: Remove units in this case? # This seems to be better than using type(self) instead of quantity # This may convert units to Quantities, e.g. np.square(volt) leads to # a 1 * volt ** 2 quantitiy instead of volt ** 2. But this should # rarely be an issue. The alternative leads to more confusing # behaviour: np.float64(3) * mV would result in a dimensionless float64 result = array.view(Quantity) result.dim = dim return result def __deepcopy__(self, memo): return Quantity(self, copy=True) #============================================================================== # Quantity-specific functions (not existing in ndarray) #============================================================================== @staticmethod
[docs] def with_dimensions(value, *args, **keywords): """ Create a `Quantity` object with dimensions. Parameters ---------- value : {array_like, number} The value of the dimension args : {`Dimension`, sequence of float} Either a single argument (a `Dimension`) or a sequence of 7 values. kwds Keywords defining the dimensions, see `Dimension` for details. Returns ------- q : `Quantity` A `Quantity` object with the given dimensions Examples -------- All of these define an equivalent `Quantity` object: >>> from brian2 import * >>> Quantity.with_dimensions(2, get_or_create_dimension(length=1)) 2. * metre >>> Quantity.with_dimensions(2, length=1) 2. * metre >>> 2 * metre 2. * metre """ if len(args) and isinstance(args[0], Dimension): dimensions = args[0] else: dimensions = get_or_create_dimension(*args, **keywords) return Quantity(value, dim=dimensions)
### ATTRIBUTES ### is_dimensionless = property(lambda self: self.dim.is_dimensionless, doc='Whether this is a dimensionless quantity.') @property def dimensions(self): ''' The dimensions of this quantity. ''' return self.dim @dimensions.setter def dimensions(self, dim): self.dim = dim #### METHODS ####
[docs] def has_same_dimensions(self, other): """ Return whether this object has the same dimensions as another. Parameters ---------- other : {`Quantity`, array-like, number} The object to compare the dimensions against. Returns ------- same : `bool` ``True`` if `other` has the same dimensions. """ if not unit_checking: return True # ignore units if unit checking is disabled other_dim = get_dimensions(other) return (self.dim is other_dim) or (self.dim == other_dim)
[docs] def in_unit(self, u, precision=None, python_code=False): """ Represent the quantity in a given unit. If `python_code` is ``True``, this will return valid python code, i.e. a string like ``5.0 * um ** 2`` instead of ``5.0 um^2`` Parameters ---------- u : {`Quantity`, `Unit`} The unit in which to show the quantity. precision : `int`, optional The number of digits of precision (in the given unit, see Examples). If no value is given, numpy's `get_printoptions` value is used. python_code : `bool`, optional Whether to return valid python code (``True``) or a human readable string (``False``, the default). Returns ------- s : `str` String representation of the object in unit `u`. Examples -------- >>> from brian2.units import * >>> from brian2.units.stdunits import * >>> x = 25.123456 * mV >>> x.in_unit(volt) '0.02512346 V' >>> x.in_unit(volt, 3) '0.025 V' >>> x.in_unit(mV, 3) '25.123 mV' See Also -------- in_unit """ fail_for_dimension_mismatch(self, u, 'Non-matching unit for method "in_unit"') value = np.array(self / u, copy=False) # numpy uses the printoptions setting only in arrays, not in array # scalars, so we use this hackish way of turning the scalar first into # an array, then removing the square brackets from the output if value.shape == (): s = np.array_str(np.array([value]), precision=precision) s = s.replace('[', '').replace(']', '').strip() else: if python_code: s = np.array_repr(value, precision=precision) else: s = np.array_str(value, precision=precision) if not u.is_dimensionless: if isinstance(u, Unit): if python_code: s += ' * ' + repr(u) else: s += ' ' + str(u) else: if python_code: s += ' * ' + repr(u.dim) else: s += ' ' + str(u.dim) elif python_code == True: # Make a quantity without unit recognisable return '%s(%s)' % (self.__class__.__name__, s.strip()) return s.strip()
def _get_best_unit(self, *regs): """ Return the best unit for this `Quantity`. Parameters ---------- regs : any number of `UnitRegistry` objects The registries that are searched for units. If none are provided, it will check the standard, user and additional unit registers in turn. Returns ------- u : `Quantity` or `Unit` The best-fitting unit for the quantity `x`. """ if self.is_dimensionless: return Unit(1) if len(regs): for r in regs: try: return r[self] except KeyError: pass return Quantity(1, self.dim) else: return self._get_best_unit(standard_unit_register, user_unit_register, additional_unit_register)
[docs] def in_best_unit(self, precision=None, python_code=False, *regs): """ Represent the quantity in the "best" unit. Parameters ---------- python_code : `bool`, optional If set to ``False`` (the default), will return a string like ``5.0 um^2`` which is not a valid Python expression. If set to ``True``, it will return ``5.0 * um ** 2`` instead. precision : `int`, optional The number of digits of precision (in the best unit, see Examples). If no value is given, numpy's `get_printoptions` value is used. regs : `UnitRegistry` objects The registries where to search for units. If none are given, the standard, user-defined and additional registries are searched in that order. Returns ------- representation : `str` A string representation of this `Quantity`. Examples -------- >>> from brian2.units import * >>> x = 0.00123456 * volt >>> x.in_best_unit() '1.23456 mV' >>> x.in_best_unit(3) '1.235 mV' See Also -------- in_best_unit """ u = self._get_best_unit(*regs) return self.in_unit(u, precision=precision, python_code=python_code)
#============================================================================== # Overwritten ndarray methods #============================================================================== #### Setting/getting items #### def __getitem__(self, key): ''' Overwritten to assure that single elements (i.e., indexed with a single integer or a tuple of integers) retain their unit. ''' return Quantity(np.ndarray.__getitem__(self, key), self.dim) def __getslice__(self, start, end): return self.__getitem__(slice(start, end)) def __setitem__(self, key, value): fail_for_dimension_mismatch(self, value, 'Inconsistent units in assignment') return super(Quantity, self).__setitem__(key, value) def __setslice__(self, start, end, value): return self.__setitem__(slice(start, end), value) #### ARITHMETIC #### def _binary_operation(self, other, operation, dim_operation=lambda a, b: a, fail_for_mismatch=False, operator_str=None, inplace=False): ''' General implementation for binary operations. Parameters ---------- other : {`Quantity`, `ndarray`, scalar} The object with which the operation should be performed. operation : function of two variables The function with which the two objects are combined. For example, `operator.mul` for a multiplication. dim_operation : function of two variables, optional The function with which the dimension of the resulting object is calculated (as a function of the dimensions of the two involved objects). For example, `operator.mul` for a multiplication. If not specified, the dimensions of `self` are used for the resulting object. fail_for_mismatch : bool, optional Whether to fail for a dimension mismatch between `self` and `other` (defaults to ``False``) operator_str : str, optional The string to use for the operator in an error message. inplace: bool, optional Whether to do the operation in-place (defaults to ``False``). Notes ----- For in-place operations on scalar values, a copy of the original object is returned, i.e. it rather works like a fundamental Python type and not like a numpy array scalar, preventing weird effects when a reference to the same value was stored in another variable. See github issue #469. ''' other_dim = None if fail_for_mismatch: if inplace: message = ('Cannot calculate ... %s {value}, units do not ' 'match') % operator_str _, other_dim = fail_for_dimension_mismatch(self, other, message, value=other) else: message = ('Cannot calculate {value1} %s {value2}, units do not ' 'match') % operator_str _, other_dim = fail_for_dimension_mismatch(self, other, message, value1=self, value2=other) if other_dim is None: other_dim = get_dimensions(other) if inplace: if self.shape == (): self_value = Quantity(self, copy=True) else: self_value = self operation(self_value, other) self_value.dim = dim_operation(self.dim, other_dim) return self_value else: newdims = dim_operation(self.dim, other_dim) self_arr = np.array(self, copy=False) other_arr = np.array(other, copy=False) result = operation(self_arr, other_arr) return Quantity(result, newdims) def __mul__(self, other): return self._binary_operation(other, operator.mul, operator.mul) def __rmul__(self, other): return self.__mul__(other) def __imul__(self, other): return self._binary_operation(other, np.ndarray.__imul__, operator.mul, inplace=True) def __div__(self, other): return self._binary_operation(other, operator.truediv, operator.truediv) def __truediv__(self, other): return self.__div__(other) def __rdiv__(self, other): # division with swapped arguments rdiv = lambda a, b: operator.truediv(b, a) return self._binary_operation(other, rdiv, rdiv) def __rtruediv__(self, other): return self.__rdiv__(other) def __idiv__(self, other): return self._binary_operation(other, np.ndarray.__itruediv__, operator.truediv, inplace=True) def __itruediv__(self, other): return self._binary_operation(other, np.ndarray.__itruediv__, operator.truediv, inplace=True) def __mod__(self, other): return self._binary_operation(other, operator.mod, fail_for_mismatch=True, operator_str=r'%') def __add__(self, other): return self._binary_operation(other, operator.add, fail_for_mismatch=True, operator_str='+') def __radd__(self, other): return self.__add__(other) def __iadd__(self, other): return self._binary_operation(other, np.ndarray.__iadd__, fail_for_mismatch=True, operator_str='+=', inplace=True) def __sub__(self, other): return self._binary_operation(other, operator.sub, fail_for_mismatch=True, operator_str='-') def __rsub__(self, other): # We allow operations with 0 even for dimension mismatches, e.g. # 0 - 3*mV is allowed. In this case, the 0 is not represented by a # Quantity object so we cannot simply call Quantity.__sub__ if ((not isinstance(other, Quantity) or other.dim is DIMENSIONLESS) and np.all(other == 0)): return self.__neg__() else: return Quantity(other, copy=False, force_quantity=True).__sub__(self) def __isub__(self, other): return self._binary_operation(other, np.ndarray.__isub__, fail_for_mismatch=True, operator_str='-=', inplace=True) def __pow__(self, other): if isinstance(other, np.ndarray) or is_scalar_type(other): fail_for_dimension_mismatch(other, error_message='Cannot calculate ' '{base} ** {exponent}, ' 'the exponent has to be ' 'dimensionless', base=self, exponent=other) other = np.array(other, copy=False) return Quantity(np.array(self, copy=False)**other, self.dim**other) else: return NotImplemented def __rpow__(self, other): if self.is_dimensionless: if isinstance(other, np.ndarray) or isinstance(other, np.ndarray): new_array = np.array(other, copy=False)**np.array(self, copy=False) return Quantity(new_array, DIMENSIONLESS) else: return NotImplemented else: raise DimensionMismatchError(('Cannot calculate ' '{base} ** {exponent}, the ' 'exponent has to be ' 'dimensionless').format(base=_short_str(other), exponent=_short_str(self)), self.dim) def __ipow__(self, other): if isinstance(other, np.ndarray) or is_scalar_type(other): fail_for_dimension_mismatch(other, error_message='Cannot calculate ' '... **= {exponent}, ' 'the exponent has to be ' 'dimensionless', exponent=other) other = np.array(other, copy=False) super(Quantity, self).__ipow__(other) self.dim = self.dim ** other return self else: return NotImplemented def __neg__(self): return Quantity(-np.array(self, copy=False), self.dim) def __pos__(self): return self def __abs__(self): return Quantity(abs(np.array(self, copy=False)), self.dim) def tolist(self): ''' Convert the array into a list. Returns ------- l : list of `Quantity` A (possibly nested) list equivalent to the original array. ''' def replace_with_quantity(seq, dim): ''' Replace all the elements in the list with an equivalent `Quantity` with the given `dim`. ''' # No recursion needed for single values if not isinstance(seq, list): return Quantity(seq, dim) def top_replace(s): ''' Recursivley descend into the list. ''' for i in s: if not isinstance(i, list): yield Quantity(i, dim) else: yield type(i)(top_replace(i)) return type(seq)(top_replace(seq)) return replace_with_quantity(np.array(self, copy=False).tolist(), self.dim) #### COMPARISONS #### def _comparison(self, other, operator_str, operation): is_scalar = is_scalar_type(other) if not is_scalar and not isinstance(other, np.ndarray): return NotImplemented if not is_scalar or not np.isinf(other): message = ('Cannot perform comparison {value1} %s {value2}, ' 'units do not match') % operator_str fail_for_dimension_mismatch(self, other, message, value1=self, value2=other) return operation(np.array(self, copy=False), np.array(other, copy=False)) def __lt__(self, other): return self._comparison(other, '<', operator.lt) def __le__(self, other): return self._comparison(other, '<=', operator.le) def __gt__(self, other): return self._comparison(other, '>', operator.gt) def __ge__(self, other): return self._comparison(other, '>=', operator.ge) def __eq__(self, other): return self._comparison(other, '==', operator.eq) def __ne__(self, other): return self._comparison(other, '!=', operator.ne) #### MAKE QUANTITY PICKABLE #### def __reduce__(self): return quantity_with_dimensions, (np.array(self, copy=False), self.dim) #### REPRESENTATION #### def __repr__(self): return self.in_best_unit(python_code=True) # TODO: Use sympy's _latex method, then latex(unit) should work def _latex(self, expr): from sympy import Matrix best_unit = self._get_best_unit() if isinstance(best_unit, Unit): best_unit_latex = latex(best_unit) else: # A quantity best_unit_latex = latex(best_unit.dimensions) unitless = np.array(self / best_unit, copy=False) if unitless.ndim == 0: sympy_quantity = np.float(unitless) else: sympy_quantity = Matrix(unitless) return latex(sympy_quantity) + '\,' + best_unit_latex def _repr_latex_(self): return '$' + latex(self) + '$' def __str__(self): return self.in_best_unit() #### Mathematic methods #### cumsum = wrap_function_keep_dimensions(np.ndarray.cumsum) diagonal = wrap_function_keep_dimensions(np.ndarray.diagonal) max = wrap_function_keep_dimensions(np.ndarray.max) mean = wrap_function_keep_dimensions(np.ndarray.mean) min = wrap_function_keep_dimensions(np.ndarray.min) ptp = wrap_function_keep_dimensions(np.ndarray.ptp) # To work around an issue in matplotlib 1.3.1 (see # https://github.com/matplotlib/matplotlib/pull/2591), we make `ravel` # return a unitless array and emit a warning explaining the issue. use_matplotlib_units_fix = False try: import matplotlib if matplotlib.__version__ == '1.3.1': use_matplotlib_units_fix = True except ImportError: pass if use_matplotlib_units_fix: def ravel(self, *args, **kwds): # Note that we don't use Brian's logging system here as we don't want # the unit system to depend on other parts of Brian warn(('As a workaround for a bug in matplotlib 1.3.1, calling ' '"ravel()" on a quantity will return unit-less values. If you ' 'get this warning during plotting, consider removing the units ' 'before plotting, e.g. by dividing by the unit. If you are ' 'explicitly calling "ravel()", consider using "flatten()" ' 'instead.')) return np.array(self, copy=False).ravel(*args, **kwds) ravel._arg_units = [None] ravel._return_unit = 1 ravel.__name__ = np.ndarray.ravel.__name__ ravel.__doc__ = np.ndarray.ravel.__doc__ else: ravel = wrap_function_keep_dimensions(np.ndarray.ravel) round = wrap_function_keep_dimensions(np.ndarray.round) std = wrap_function_keep_dimensions(np.ndarray.std) sum = wrap_function_keep_dimensions(np.ndarray.sum) trace = wrap_function_keep_dimensions(np.ndarray.trace) var = wrap_function_change_dimensions(np.ndarray.var, lambda ar, d: d ** 2) all = wrap_function_remove_dimensions(np.ndarray.all) any = wrap_function_remove_dimensions(np.ndarray.any) nonzero = wrap_function_remove_dimensions(np.ndarray.nonzero) argmax = wrap_function_remove_dimensions(np.ndarray.argmax) argmin = wrap_function_remove_dimensions(np.ndarray.argmin) argsort = wrap_function_remove_dimensions(np.ndarray.argsort) def fill(self, values): # pylint: disable=C0111 fail_for_dimension_mismatch(self, values, 'fill') super(Quantity, self).fill(values) fill.__doc__ = np.ndarray.fill.__doc__ def put(self, indices, values, *args, **kwds): # pylint: disable=C0111 fail_for_dimension_mismatch(self, values, 'fill') super(Quantity, self).put(indices, values, *args, **kwds) put.__doc__ = np.ndarray.put.__doc__ def clip(self, a_min, a_max, *args, **kwds): # pylint: disable=C0111 fail_for_dimension_mismatch(self, a_min, 'clip') fail_for_dimension_mismatch(self, a_max, 'clip') return Quantity(np.clip(np.array(self, copy=False), np.array(a_min, copy=False), np.array(a_max, copy=False), *args, **kwds), self.dim) clip.__doc__ = np.ndarray.clip.__doc__ def dot(self, other, **kwds): # pylint: disable=C0111 return Quantity(np.array(self).dot(np.array(other), **kwds), self.dim*get_dimensions(other)) dot.__doc__ = np.ndarray.dot.__doc__ def searchsorted(self, v, **kwds): # pylint: disable=C0111 fail_for_dimension_mismatch(self, v, 'searchsorted') return super(Quantity, self).searchsorted(np.array(v, copy=False), **kwds) searchsorted.__doc__ = np.ndarray.searchsorted.__doc__ def prod(self, *args, **kwds): # pylint: disable=C0111 prod_result = super(Quantity, self).prod(*args, **kwds) # Calculating the correct dimensions is not completly trivial (e.g. # like doing self.dim**self.size) because prod can be called on # multidimensional arrays along a certain axis. # Our solution: Use a "dummy matrix" containing a 1 (without units) at # each entry and sum it, using the same keyword arguments as provided. # The result gives the exponent for the dimensions. # This relies on sum and prod having the same arguments, which is true # now and probably remains like this in the future dim_exponent = np.ones_like(self).sum(*args, **kwds) # The result is possibly multidimensional but all entries should be # identical if dim_exponent.size > 1: dim_exponent = dim_exponent[0] return Quantity(np.array(prod_result, copy=False), self.dim ** dim_exponent) prod.__doc__ = np.ndarray.prod.__doc__ def cumprod(self, *args, **kwds): # pylint: disable=C0111 if not self.is_dimensionless: raise TypeError('cumprod over array elements on quantities ' 'with dimensions is not possible.') return Quantity(np.array(self, copy=False).cumprod(*args, **kwds)) cumprod.__doc__ = np.ndarray.cumprod.__doc__
[docs]class Unit(Quantity): r''' A physical unit. Normally, you do not need to worry about the implementation of units. They are derived from the `Quantity` object with some additional information (name and string representation). Basically, a unit is just a number with given dimensions, e.g. mvolt = 0.001 with the dimensions of voltage. The units module defines a large number of standard units, and you can also define your own (see below). The unit class also keeps track of various things that were used to define it so as to generate a nice string representation of it. See below. When creating scaled units, you can use the following prefixes: ====== ====== ============== Factor Name Prefix ====== ====== ============== 10^24 yotta Y 10^21 zetta Z 10^18 exa E 10^15 peta P 10^12 tera T 10^9 giga G 10^6 mega M 10^3 kilo k 10^2 hecto h 10^1 deka da 1 10^-1 deci d 10^-2 centi c 10^-3 milli m 10^-6 micro u (\mu in SI) 10^-9 nano n 10^-12 pico p 10^-15 femto f 10^-18 atto a 10^-21 zepto z 10^-24 yocto y ====== ====== ============== **Defining your own** It can be useful to define your own units for printing purposes. So for example, to define the newton metre, you write >>> from brian2.units.allunits import metre, newton >>> Nm = newton * metre You can then do >>> (1*Nm).in_unit(Nm) '1. N m' which returns ``"1 N m"`` because the `Unit` class generates a new display name of ``"N m"`` from the display names ``"N"`` and ``"m"`` for newtons and metres automatically. To register this unit for use in the automatic printing of the `Quantity.in_best_unit` method, see the documentation for the `~brian2.units.fundamentalunits.UnitRegistry` class. **Construction** The best way to construct a new unit is to use standard units already defined and arithmetic operations, e.g. ``newton*metre``. See the documentation for the static methods `Unit.create` and `Unit.create_scaled_units` for more details. If you don't like the automatically generated display name for the unit, use the `Unit.set_display_name` method. **Representation** A new unit defined by multiplication, division or taking powers generates a name for the unit automatically, so that for example the name for ``pfarad/mmetre**2`` is ``"pF/mm^2"``, etc. If you don't like the automatically generated name, use the `Unit.set_display_name` method. ''' __slots__ = ["dim", "scale", "scalefactor", "_dispname", "_name", "_latexname", "iscompound"] __array_priority__ = 100 automatically_register_units = True #### CONSTRUCTION #### def __new__(cls, arr, dim=None, scale=None, dtype=None, copy=False): if dim is None: dim = DIMENSIONLESS obj = super(Unit, cls).__new__(cls, arr, dim=dim, dtype=dtype, copy=copy, force_quantity=True) if Unit.automatically_register_units: register_new_unit(obj) return obj def __array_finalize__(self, orig): self.dim = getattr(orig, 'dim', DIMENSIONLESS) self.scale = getattr(orig, 'scale', ("", "", "", "", "", "", "")) self.scalefactor = getattr(orig, 'scalefactor', '') self._name = getattr(orig, '_name', '') self._dispname = getattr(orig, '_dispname', '') self._latexname = getattr(orig, '_latexname', '') self.iscompound = getattr(orig, '_iscompound', False) return self def __init__(self, value, dim=None, scale=None): if dim is None: dim = DIMENSIONLESS self.dim = dim #: The Dimensions of this unit if scale is None: scale = ("", "", "", "", "", "", "") if not len(scale) == 7: raise ValueError('scale needs seven entries') #: The scale for this unit (a 7-tuple) self.scale = scale #: The scalefactor for this unit, e.g. 'm' for milli self.scalefactor = "" #: The full name of this unit. self._name = "" #: The display name of this unit. self._dispname = "" #: A LaTeX expression for the name of this unit. self._latexname = "" #: Whether this unit is a combination of other units. self.iscompound = False @staticmethod
[docs] def create(dim, name="", dispname="", latexname=None, scalefactor="", **keywords): """ Create a new named unit. Parameters ---------- dim : `Dimension` The dimensions of the unit. name : `str`, optional The full name of the unit, e.g. ``'volt'`` dispname : `str`, optional The display name, e.g. ``'V'`` latexname : str, optional The name as a LaTeX expression (math mode is assumed, do not add $ signs or similar), e.g. ``'\omega'``. If no `latexname` is specified, `dispname` will be used. scalefactor : str, optional The scaling factor, e.g. ``'m'`` for mvolt keywords The scaling for each SI dimension, e.g. ``length="m"``, ``mass="-1"``, etc. Returns ------- u : `Unit` The new unit. """ scale = [ "", "", "", "", "", "", "" ] for k in keywords: scale[_di[k]] = keywords[k] v = 1.0 for s, i in itertools.izip(scale, dim._dims): if i: v *= _siprefixes[s] ** i u = Unit(v * _siprefixes[scalefactor], dim=dim, scale=tuple(scale)) u.scalefactor = scalefactor + "" u._name = str(name) u._dispname = str(dispname) if latexname is None: latexname = u._dispname u._latexname = r'\mathrm{' + latexname + '}' u.iscompound = False return u
@staticmethod
[docs] def create_scaled_unit(baseunit, scalefactor): """ Create a scaled unit from a base unit. Parameters ---------- baseunit : `Unit` The unit of which to create a scaled version, e.g. ``volt``, ``amp``. scalefactor : `str` The scaling factor, e.g. ``"m"`` for mvolt, mamp Returns ------- u : `Unit` The new unit. """ u = Unit(np.array(baseunit, copy=False) * _siprefixes[scalefactor], dim=baseunit.dim, scale=baseunit.scale) u.scalefactor = scalefactor u._name = scalefactor + baseunit._name u._dispname = scalefactor + baseunit._dispname # As u --> \mu is the only transformation we have, I think it # makes sense to just special-case it here instead of coming # up with a general system for scale factors # TODO: Unfortunately, \mu gives the typographically incorrect symbol, #it should be an upright letter :-/ if scalefactor == 'u': scalefactor = r'\mu' u._latexname = r'\mathrm{' + scalefactor + '}' + r'\,' + baseunit.latexname u.iscompound = False return u
#### METHODS ####
[docs] def get_name(self): if self._name == "": if self.scalefactor: parts = [repr(_siprefixes[self.scalefactor])] else: parts = [] for i in range(7): if self.dim._dims[i]: s = self.scale[i] + _iclass_label[i] if self.dim._dims[i] != 1: s += ' ** ' + str(self.dim._dims[i]) parts.append(s) s = " * ".join(parts) s = s.strip() if not len(s): return "%s(1)" % self.__class__.__name__ else: return s else: return self._name
[docs] def set_name(self, name): """Sets the name for the unit """ self._name = name
[docs] def get_display_name(self): if self._dispname == "": s = self.scalefactor + " " for i in range(7): if self.dim._dims[i]: s += self.scale[i] + _ilabel[i] if self.dim._dims[i] != 1: s += "^" + str(self.dim._dims[i]) s += " " s = s.strip() if not len(s): return "1" else: return s else: return self._dispname
[docs] def set_display_name(self, name): """Sets the display name for the unit """ self._dispname = name
[docs] def get_latex_name(self): if self._latexname == "": if len(self.scalefactor): if self.scalefactor == 'u': scalefactor = r'\mu' else: scalefactor = self.scalefactor s = r'\mathrm{' + scalefactor + "} " else: s = '' for i in range(7): if self.dim._dims[i]: s += self.scale[i] + _ilabel[i] if self.dim._dims[i] != 1: s += "^{" + str(self.dim._dims[i]) + '}' s += " " s = s.strip() if not len(s): return "1" else: return s else: return self._latexname
[docs] def set_latex_name(self, name): self._latexname = name
name = property(fget=get_name, fset=set_name, doc='The name of the unit') dispname = property(fget=get_display_name, fset=set_display_name, doc='The display name of the unit') latexname = property(fget=get_latex_name, fset=set_latex_name, doc='The LaTeX name of the unit') #### REPRESENTATION #### def __repr__(self): return self.name def __str__(self): return self.dispname def _latex(self, *args): return self.latexname def _repr_latex_(self): return '$' + latex(self) + '$' #### ARITHMETIC #### def __mul__(self, other): if isinstance(other, Unit): u = Unit(np.array(self, copy=False) * np.array(other, copy=False)) u.name = self.name + " * " + other.name u.dispname = self.dispname + ' ' + other.dispname u.latexname = self.latexname + r'\,' + other.latexname u.dim = self.dim * other.dim u.iscompound = True return u else: return super(Unit, self).__mul__(other) def __rmul__(self, other): return self.__mul__(other) def __div__(self, other): if isinstance(other, Unit): u = Unit(np.array(self, copy=False) / np.array(other, copy=False)) if other.iscompound: u.dispname = '(' + self.dispname + ')' u.name = '(' + self.name + ')' else: u.dispname = self.dispname u.name = self.name u.dispname += '/' u.name += ' / ' if other.iscompound: u.dispname += '(' + other.dispname + ')' u.name += '(' + other.name + ')' else: u.dispname += other.dispname u.name += other.name u.dim = self.dim / other.dim u.iscompound = True u.latexname = r'\frac{%s}{%s}' % (self.latexname, other.latexname) return u else: return super(Unit, self).__div__(other) def __rdiv__(self, other): if isinstance(other, Unit): return other.__div__(self) else: try: if other == 1: return self**-1 except (ValueError, TypeError, DimensionMismatchError): pass return super(Unit, self).__rdiv__(other) def __pow__(self, other): if is_scalar_type(other): u = Unit(np.array(self, copy=False) ** other) if self.iscompound: u.dispname = '(' + self.dispname + ')' u.name = '(' + self.name + ')' u.latexname = r'\left(%s\right)' % self.latexname else: u.dispname = self.dispname u.name = self.name u.latexname = self.latexname u.dispname += '^' + str(other) u.name += ' ** ' + repr(other) u.latexname += '^{%s}' % latex(other) u.dim = self.dim ** other return u else: return super(Unit, self).__pow__(other) def __iadd__(self, other): raise TypeError('Units cannot be modified in-place') def __isub__(self, other): raise TypeError('Units cannot be modified in-place') def __imul__(self, other): raise TypeError('Units cannot be modified in-place') def __idiv__(self, other): raise TypeError('Units cannot be modified in-place') def __itruediv__(self, other): raise TypeError('Units cannot be modified in-place') def __ifloordiv__(self, other): raise TypeError('Units cannot be modified in-place') def __imod__(self, other): raise TypeError('Units cannot be modified in-place') def __ipow__(self, other, modulo=None): raise TypeError('Units cannot be modified in-place') def __eq__(self, other): if isinstance(other, Unit): return (other.dim == self.dim and other.scalefactor == self.scalefactor and other.scale == self.scale) else: return Quantity.__eq__(self, other) def __neq__(self, other): return not self.__eq__(other) def __hash__(self): return hash((self.dim, self.scalefactor, self.scale, self.name, self.dispname))
[docs]class UnitRegistry(object): """ Stores known units for printing in best units. All a user needs to do is to use the `register_new_unit` function. Default registries: The units module defines three registries, the standard units, user units, and additional units. Finding best units is done by first checking standard, then user, then additional. New user units are added by using the `register_new_unit` function. Standard units includes all the basic non-compound unit names built in to the module, including volt, amp, etc. Additional units defines some compound units like newton metre (Nm) etc. Methods ------- add __getitem__ """ def __init__(self): self.units = set() self.units_for_dimensions = collections.defaultdict(list)
[docs] def add(self, u): """Add a unit to the registry """ if u in self.units: return self.units.add(u) self.units_for_dimensions[u.dim].append(u)
def remove(self, u): """Remove a unit from the registry """ self.units.remove(u) dim = u.dim self.units_for_dimensions[dim].remove(u)
[docs] def __getitem__(self, x): """Returns the best unit for quantity x The algorithm is to consider the value: m=abs(x/u) for all matching units u. We select the unit where this ratio is the closest to 10 (if it is an array with several values, we select the unit where the deviations from that are the smallest. More precisely, the unit that minimizes the sum of (log10(m)-1)**2 over all entries). """ matching = self.units_for_dimensions.get(x.dim, []) if len(matching) == 0: raise KeyError("Unit not found in registry.") # determine how well this unit represents the value matching_values = np.array(matching, copy=False) x_flat = np.array(x, copy=False).flatten() floatreps = np.tile(np.abs(x_flat), (len(matching), 1)).T / matching_values # ignore zeros, they are well represented in any unit floatreps[floatreps == 0] = np.nan if np.all(np.isnan(floatreps)): return matching[0] # all zeros, use the base unit deviations = np.nansum((np.log10(floatreps) - 1)**2, axis=0) return matching[deviations.argmin()]
[docs]def register_new_unit(u): """Register a new unit for automatic displaying of quantities Parameters ---------- u : `Unit` The unit that should be registered. Examples -------- >>> from brian2 import * >>> 2.0*farad/metre**2 2. * metre ** -4 * kilogram ** -1 * second ** 4 * amp ** 2 >>> register_new_unit(pfarad / mmetre**2) >>> 2.0*farad/metre**2 2000000. * pfarad / mmetre ** 2 >>> unregister_unit(pfarad / mmetre**2) >>> 2.0*farad/metre**2 2. * metre ** -4 * kilogram ** -1 * second ** 4 * amp ** 2 """ user_unit_register.add(u)
[docs]def unregister_unit(u): """Remove a previously registered unit for automatic displaying of quantities Parameters ---------- u : `Unit` The unit that should be unregistered. """ user_unit_register.remove(u)
#: `UnitRegistry` containing all the standard units (metre, kilogram, um2...) standard_unit_register = UnitRegistry() #: `UnitRegistry` containing additional units (newton*metre, farad / metre, ...) additional_unit_register = UnitRegistry() #: `UnitRegistry` containing all units defined by the user user_unit_register = UnitRegistry()
[docs]def all_registered_units(*regs): """ Generator returning all registered units. Parameters ---------- regs : any number of `UnitRegistry` objects. If given, units from the given registries are returned. If none are given, units are returned from the standard units, the user-registered units and the "additional units" (e.g. ``newton * metre``) in that order. Returns ------- u : `Unit` A single unit from the registry. """ if not len(regs): regs = [standard_unit_register, user_unit_register, additional_unit_register] for r in regs: for u in r.units: yield u
[docs]def get_unit(x, *regs): ''' Find the most appropriate consistent unit from the unit registries. Parameters ---------- x : {`Quantity`, `Dimension`, array-like, number} The value to find a unit for. Returns ------- q : `Unit` The equivalent `Unit` for the quantity `x`. ''' for u in all_registered_units(*regs): if np.array(u, copy=False) == 1 and have_same_dimensions(u, x): return u dim = getattr(x, 'dim', DIMENSIONLESS) # For units, get dimensions return Unit(1.0, dim=dim)
[docs]def get_unit_for_display(x): ''' Return a string representation of the most appropriate unit or ``'1'`` for a dimensionless quantity ''' if x is 1 or x is DIMENSIONLESS or have_same_dimensions(x, 1): return '1' else: return repr(get_unit(x))
[docs]def get_unit_fast(x): ''' Return a `Quantity` with value 1 and the same dimensions. ''' return Quantity.with_dimensions(1, get_dimensions(x))
#### DECORATORS
[docs]def check_units(**au): """Decorator to check units of arguments passed to a function Examples -------- >>> from brian2.units import * >>> @check_units(I=amp, R=ohm, wibble=metre, result=volt) ... def getvoltage(I, R, **k): ... return I*R You don't have to check the units of every variable in the function, and you can define what the units should be for variables that aren't explicitly named in the definition of the function. For example, the code above checks that the variable wibble should be a length, so writing >>> getvoltage(1*amp, 1*ohm, wibble=1) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... DimensionMismatchError: Function "getvoltage" variable "wibble" has wrong dimensions, dimensions were (1) (m) fails, but >>> getvoltage(1*amp, 1*ohm, wibble=1*metre) 1. * volt passes. String arguments or ``None`` are not checked >>> getvoltage(1*amp, 1*ohm, wibble='hello') 1. * volt By using the special name ``result``, you can check the return value of the function. You can also use ``1`` or ``bool`` as a special value to check for a unitless number or a boolean value, respectively: >>> @check_units(value=1, absolute=bool, result=bool) ... def is_high(value, absolute=False): ... if absolute: ... return abs(value) >= 5 ... else: ... return value >= 5 This will then again raise an error if the argument if not of the expected type: >>> is_high(7) True >>> is_high(-7, True) True >>> is_high(3, 4) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... TypeError: Function "is_high" expected a boolean value for argument "absolute" but got 4. Raises ------ DimensionMismatchError In case the input arguments or the return value do not have the expected dimensions. TypeError If an input argument or return value was expected to be a boolean but is not. Notes ----- This decorator will destroy the signature of the original function, and replace it with the signature ``(*args, **kwds)``. Other decorators will do the same thing, and this decorator critically needs to know the signature of the function it is acting on, so it is important that it is the first decorator to act on a function. It cannot be used in combination with another decorator that also needs to know the signature of the function. Note that the ``bool`` type is "strict", i.e. it expects a proper boolean value and does not accept 0 or 1. This is not the case the other way round, declaring an argument or return value as "1" *does* allow for a ``True`` or ``False`` value. """ def do_check_units(f): def new_f(*args, **kwds): newkeyset = kwds.copy() arg_names = f.func_code.co_varnames[0:f.func_code.co_argcount] for (n, v) in zip(arg_names, args[0:f.func_code.co_argcount]): if (not isinstance(v, (Quantity, basestring, bool)) and v is not None and n in au): try: # allow e.g. to pass a Python list of values v = Quantity(v) except TypeError: if have_same_dimensions(au[n], 1): raise TypeError(('Argument %s is not a unitless ' 'value/array.') % n) else: raise TypeError(('Argument %s is not a quantity, ' 'expected a quantity with dimensions ' '%s') % (n, au[n])) newkeyset[n] = v for k in newkeyset.iterkeys(): # string variables are allowed to pass, the presumption is they # name another variable. None is also allowed, useful for # default parameters if (k in au.keys() and not isinstance(newkeyset[k], str) and not newkeyset[k] is None): if au[k] == bool: if not isinstance(newkeyset[k], bool): error_message = ('Function "{f.__name__}" ' 'expected a boolean value ' 'for argument "{k}" but got ' '{value}').format(f=f, k=k, value=newkeyset[k]) raise TypeError(error_message) elif not have_same_dimensions(newkeyset[k], au[k]): error_message = ('Function "{f.__name__}" ' 'expected a quantitity with unit ' '{unit} for argument "{k}" but got ' '{value}').format(f=f, k=k, unit=repr(au[k]), value=newkeyset[k]) raise DimensionMismatchError(error_message, newkeyset[k]) result = f(*args, **kwds) if 'result' in au: if au['result'] == bool: if not isinstance(result, bool): error_message = ('The return value of function ' '"{f.__name__}" was expected to be ' 'a boolean value, but was of type ' '{type}').format(f=f, type=type(result)) raise TypeError(error_message) elif not have_same_dimensions(result, au['result']): error_message = ('The return value of function ' '"{f.__name__}" was expected to have ' 'unit {unit} but was ' '{value}').format(f=f, unit=repr(au['result']), value=result) raise DimensionMismatchError(error_message, get_dimensions(result)) return result new_f._orig_func = f new_f.__doc__ = f.__doc__ new_f.__name__ = f.__name__ # store the information in the function, necessary when using the # function in expressions or equations arg_units = [] all_specified = True if hasattr(f, '_orig_arg_names'): arg_names = f._orig_arg_names else: arg_names = f.func_code.co_varnames[:f.func_code.co_argcount] for name in arg_names: unit = au.get(name, None) if unit is None: all_specified = False break else: arg_units.append(unit) if all_specified: new_f._arg_units = arg_units else: new_f._arg_units = None return_unit = au.get('result', None) if return_unit is None: new_f._return_unit = None else: new_f._return_unit = return_unit if return_unit == bool: new_f._returns_bool = True else: new_f._returns_bool = False new_f._orig_arg_names = arg_names # copy any annotation attributes if hasattr(f, '_annotation_attributes'): for attrname in f._annotation_attributes: setattr(new_f, attrname, getattr(f, attrname)) new_f._annotation_attributes = getattr(f, '_annotation_attributes', [])+['_arg_units', '_return_unit', '_orig_func', '_returns_bool'] return new_f return do_check_units