Source code for pyveu


# Copyright (C) 2018-2020 Frank Sauerburger

"""
The python package pyveu (Value Error Unit) handles real-life experimental
data which includes uncertainties and physical units. The packages implements
arithmetic operations and many mathematical functions for physical quantities.
Gaussian error propagation is used to calculate the uncertainty of derived
quantities.

The package is built with the day-to-day requirements of people working a
laboratory kept in mind. The package offers an imperative programming style,
which means that the operations are evaluated when they are typed
interactively in python, giving researcher the freedom and flexibility they
need.

"""

from __future__ import division  # Compatibility with 2.7
from builtins import int, round   # Compatibility with 2.7

import collections
import numpy as np
import re
import math
from decimal import Decimal, ROUND_HALF_UP, ROUND_FLOOR

__version__ = "0.1.0"  # Also change in setup.py

# The content of the file might be move to other module within the same
# package.

def join_or_none(a, b):
    """
    Joins the two strings. If one argument is None, the other argument is
    returned. If both arguments are None, None is returned.
    """
    if a is None and b is None:
        return None
    elif a is None:
        return b
    elif b is None:
        return a
    else:
        return a + b

def _round(value, precision=0):
    """
    Rounds the value to the given precision. The optional parameter precision
    is the number of decimal digits after rounding. The value is rounded to
    the nearest number with the given precision. In case of a tie, the method
    rounds away from zero. The precision can be negative.

    The method returns an integer for negative precision.
    """
    scale = Decimal(10)**precision
    floored = (Decimal(value) * scale).to_integral(ROUND_HALF_UP) / scale

    if precision <= 0:
        return int(floored)
    else:
        return float(floored)

def _floor(value, precision=0):
    """
    Rounds the value down to the given precision. The optional parameter precision
    is the number of decimal digits after rounding. The value is always
    rounding down. This means positive values are rounded towards zero,
    negative values are rounded away from zero. The precision can be negative.

    The method returns an integer for negative precision.
    """
    scale = Decimal(10)**precision
    floored = (Decimal(value) * scale).to_integral(ROUND_FLOOR) / scale

    if precision <= 0:
        return int(floored)
    else:
        return float(floored)

[docs]class Named(object): """ The class Named defines the base class for all classes in this package, which have a label, a mathematical symbol and optionally and alternative latex representation. The label should be used to describe an object with a verbose, human readable string. In case of a physical quantity, the label can be 'Current', 'Voltage of the photo diode' or similar strings. The label can be retrieved with the label() method. The symbol should be used to decorate object with a mathematical symbol. If the named object stores time information, an intuitively understandable symbol is `t`. The symbol is used when the named object is printed on the console. The symbol should not use latex syntax. The latex property stores an alternative representation of the symbol with latex support. Dollar signs should not be included in the string. For example, the latex symbol of the dielectric constant in matter is commonly set to `\\epsilon_r`. The base class Named does not define methods to modify the properties. This means, unless a derived class uses the ModifiableNamed mix-in, the name properties are read-only. """ def __init__(self, label, symbol, latex): """ Creates a new named object. This method is intended to be called by the constructors of derived classes. """ self._label = label self._symbol = symbol self._latex = latex
[docs] def symbol(self): """ Returns the non-latex symbol. The symbol property is read-only. """ return self._symbol
[docs] def latex(self): """ Returns the latex symbol. The latex symbol property is read-only. """ return self._latex
[docs] def lors(self): """ Returns the latex symbol if it is not None. Otherwise the symbol is returned. This method is read-only. The name of the method stands for latex-or-symbol. """ return self.symbol() if self.latex() is None else self.latex()
[docs] def label(self): """ Returns the verbose label of the named object. The label is read-only. """ return self._label
class ModifiableNamedMixin(object): """ Overrides symbol(), latex() and label(). The new methods support modification of these properties, by passing a value to the method. """ def label(self, *args): """ Set or get the label. If the arguments are omitted, the method returns the current label. If an argument is given, the argument is used as the new label. """ if len(args) == 0: return self._label elif len(args) == 1: self._label = args[0] else: raise TypeError("label() takes 0 or 1 arguments (%d given)" \ % len(args)) def symbol(self, *args): """ Set or get the symbol. If the arguments are omitted, the method returns the current symbol. If an argument is given, the argument is used as the new symbol. """ if len(args) == 0: return self._symbol elif len(args) == 1: self._symbol = args[0] else: raise TypeError("symbol() takes 0 or 1 arguments (%d given)" \ % len(args)) def latex(self, *args): """ Set or get the latex symbol. If the arguments are omitted, the method returns the current latex symbol. If an argument is given, the argument is used as the new latex symbol. """ if len(args) == 0: return self._latex elif len(args) == 1: self._latex = args[0] else: raise TypeError("latex() takes 0 or 1 arguments (%d given)" \ % len(args)) class SystemAffiliatedMixin: """ The SystemAffiliatedMixin class can be used as a mix-in to indicate that a class is affiliated with a particular unit system. The unit system can be set in the constructor. Later modifications of the unit system are not possible. The current unit system object can be retrieved via the unit_system() method. """ def __init__(self, unit_system): """ Constructor of a SystemAffiliated mix-in. This method should be called in constructors of other classes which use the mix-in. The first argument is expected to be a unit system object. """ self._unit_system = unit_system def unit_system(self): """ Returns the unit system to which the object is affiliated. This method is read-only. """ return self._unit_system class _Arithmetic: """ The arithmetic class is the base class of arithmetic operations which are used to record the history how anonymous units and prefixes were constructed. Each arithmetic operation performed with prefixes or units are reflected in object dependencies of sub-classes. The main purpose of this class an sub-classes is to store all the operands used in the arithmetic operations. """ pass class _Product(_Arithmetic): """ The product class represents a multiplication of two or more factors. The main purpose of this class is to store the factors. """ def __init__(self, *factors): """ Create a new product object. All argument of the constructor are treated as factor of the product. If there are less than two factors, an Exception is raised. """ if len(factors) < 2: raise TypeError("_Product expects at least do factors.") self.factors = list(factors) def str(self, latex=False): """ Create a string representation by calling str() on all factors. The method adds parenthesis when needed. Factors are joined by an asterisk. If the optional parameter latex is True, then a latex version of the string is constructed. """ if len(self.factors) < 2: raise ValueError("_Product expects at least two factors.") str_factors = [] for factor in self.factors: if isinstance(factor, _Arithmetic): str_factors.append(factor.str(latex)) elif isinstance(factor, Unit) and latex: str_factors.append(factor.lors()) elif isinstance(factor, Unit) and not latex: str_factors.append(factor.symbol()) elif isinstance(factor, Prefix): str_factors.append("%g" % factor.factor()) elif isinstance(factor, (int, float)): str_factors.append("%g" % factor) else: str_factors.append(str(factor)) if latex: return " ".join(str_factors) else: return " * ".join(str_factors) def __str__(self): """ Calls _Product.str(). """ return self.str() def __repr__(self): """ Create a string representation by calling str() on all factors. The method adds parenthesis around all sub-arithmetical objects. """ if len(self.factors) < 2: raise ValueError("_Product expects at least do factors.") str_factors = ["(%s)" % repr(f) if isinstance(f, _Arithmetic) \ else repr(f) for f in self.factors] return " * ".join(str_factors) class _Fraction(_Arithmetic): """ The fraction class represents a division of two objects. The main purpose of this class is to store the numerator an denominator. """ def __init__(self, numerator, denominator): """ Create a new fraction object. The constructor expects and stores an numerator and denominator object. """ self.numerator = numerator self.denominator = denominator def str(self, latex=False): """ Create a string representation by calling str() on the numerator and denominator. The method adds parenthesis when needed. Factors are joined by an asterisk. If the optional parameter latex is True, then a latex version of the string is constructed. """ if isinstance(self.numerator, _Arithmetic): str_numerator = self.numerator.str(latex) elif isinstance(self.numerator, Unit) and latex: str_numerator = self.numerator.lors() elif isinstance(self.numerator, Unit) and not latex: str_numerator = self.numerator.symbol() elif isinstance(self.numerator, Prefix): str_numerator = "%g" % factor.factor() elif isinstance(self.numerator, (int, float)): str_numerator = "%g" % self.numerator else: str_numerator = str(self.numerator) if isinstance(self.denominator, _Power): str_denominator = self.denominator.str(latex) elif isinstance(self.denominator, _Arithmetic) and latex: str_denominator = self.denominator.str(latex) elif isinstance(self.denominator, _Arithmetic) and not latex: str_denominator = "(%s)" % self.denominator.str(latex) elif isinstance(self.denominator, Unit) and latex: str_denominator = self.denominator.lors() elif isinstance(self.denominator, Unit) and not latex: str_denominator = self.denominator.symbol() elif isinstance(self.denominator, Prefix): str_denominator = "%g" % factor.factor() elif isinstance(self.denominator, (int, float)): str_denominator = "%g" % self.denominator else: str_denominator = str(self.denominator) if latex: return r"\frac{%s}{%s}" % (str_numerator, str_denominator) else: return "%s / %s" % (str_numerator, str_denominator) def __str__(self): """ Calls _Fraction.str(). """ return self.str() def __repr__(self): """ Create a string representation by calling str() on the numerator and denominator. The method adds parenthesis around all sub-arithmetical objects. """ def escape(item): if isinstance(item, _Arithmetic): return "(%s)" % repr(item) else: return repr(item) return "%s / %s" % (escape(self.numerator), escape(self.denominator)) class _Power(_Arithmetic): """ The power class represents a power of two objects. The main purpose of this class is to store the base and the exponent. """ def __init__(self, base, exponent): """ Create a new power object. The constructor expects and stores a base and exponent object. """ self.base = base self.exponent = exponent def str(self, latex=False): """ Create a string representation by calling str() on the base and exponent. The method adds parenthesis when needed. If the optional parameter latex is True, then a latex version of the string is constructed. """ if isinstance(self.base, _Arithmetic) and latex: base = r"\left(%s\right)" % self.base.str(latex) elif isinstance(self.base, _Arithmetic) and not latex: base = "(%s)" % self.base.str(latex) elif isinstance(self.base, Unit) and latex: base = self.base.lors() elif isinstance(self.base, Unit) and not latex: base = self.base.symbol() elif isinstance(self.base, (int, float)): base = "%g" % self.base else: base = str(self.base) if isinstance(self.exponent, _Arithmetic) and latex: exponent = self.exponent.str(latex) elif isinstance(self.exponent, (_Fraction, _Product)) and not latex: exponent = "(%s)" % self.exponent.str(latex) elif isinstance(self.exponent, _Arithmetic) and not latex: exponent = self.exponent.str(latex) elif isinstance(self.exponent, Unit) and latex: exponent = self.exponent.lors() elif isinstance(self.exponent, Unit) and not latex: exponent = self.exponent.symbol() elif isinstance(self.exponent, (int, float)): exponent = "%g" % self.exponent else: exponent = str(self.exponent) if latex: return "%s^{%s}" % (base, exponent) else: return "%s^%s" % (base, exponent) def __str__(self): """ Calls _Power.str() """ return self.str() def __repr__(self): """ Create a string representation by calling repr() on the base and exponent. The method adds parenthesis around sub-arithmetical objects. """ def escape(item): if isinstance(item, _Arithmetic): return "(%s)" % repr(item) else: return repr(item) return "%s^%s" % (escape(self.base), escape(self.exponent))
[docs]class Prefix(Named, SystemAffiliatedMixin): """ The Prefix class represents a string with which units can be prepended in order to scale them. A popular example are the SI prefixes such as Kilo, Mega or Giga. A prefix can be created via a unit system. The unit system adds all prefixes to its internal registry. Registered prefixes are considered when parsing unit strings. The prefix class inherits the label, symbol and latex property of the Named class. Furthermore, the class inherits the properties of the SystemAffiliated class and is therefore tied to a particular unit system. In addition to the inherited properties, this class stores a factor which is used to scale a unit. For example, the factor of the prefix Kilo is 1000. Multiplications and divisions are overloaded for units. Multiplications and divisions of a prefix and a number return a number in most of the cases. The only exception is the case when prefix is multiplied by a number from the left, e.g., 10 * kilo. In that case, the result is an anonymous prefix. Its history is a product with the two factors 10 and kilo. These derived prefixes are not automatically added to the registry. If you want to add an anonymous prefix to the registry, use the register_prefix() method of the unit system. """ def __init__(self, factor, label, symbol, latex, unit_system): """ This method is for internal usage only. Creates a new Prefix. Prefixes should be created via a unit system. """ Named.__init__(self, label, symbol, latex) SystemAffiliatedMixin.__init__(self, unit_system) self._factor = factor self._history = None
[docs] def factor(self): """ Returns the factor of the prefix. This method is read-only. """ return self._factor
def __repr__(self): """ Returns a string which can be used to recreate the object. The string has the following pattern: <Prefix <label>: <symbol> = <factor>> If the label of symbol is None, they are excluded from the representation. """ if self._label is None and self._symbol is None: id = ": " elif self._label is None: id = ": %s = " % self._symbol elif self._symbol is None: id = " %s: " % self._label else: id = " %s: %s = " % (self._label, self._symbol) return "<Prefix%s%g>" % (id, self._factor) def __mul__(self, other): """ Multiplies the factor with the given operand. The operand must be integer, float or Prefix, otherwise NotImplemented is returned. This allows Unit and Quantity to implement custom behavior. The method returns a simple number (float or int). """ if not isinstance(other, (int, float, Prefix)): return NotImplemented if isinstance(other, SystemAffiliatedMixin): if self.unit_system() is not other.unit_system(): raise DifferentUnitSystem() if isinstance(other, Prefix): other = other.factor() # Return simple number return self.factor() * other def __rmul__(self, other): """ Multiplies the factor with the given operand. The operand must be integer, float or Prefix, otherwise NotImplemented is returned. This allows Unit and Quantity to implement custom behavior. The method returns a simple number (float or int). The only exception is when a prefix is multiplied by a number form the left, e.g., 10 * kilo. In that case, the method returns a new object. The prefix on which this method is called is not modified. The new prefix is anonymous and not registered. To register it, use the register_prefix() method of the unit system. The new Prefix's history is a Product of the scalar and the original prefix. """ if not isinstance(other, (int, float, Prefix)): return NotImplemented if isinstance(other, SystemAffiliatedMixin): if self.unit_system() is not other.unit_system(): raise DifferentUnitSystem() if isinstance(other, Prefix): # Return simple number return self.factor() * other.factor() # Return a new, unnamed prefix new_prefix = Prefix(self.factor() * other, None, None, None, self.unit_system()) # Calculate new history if self._history is None: if self.symbol() is None: raise Exception("History of the other unnamed prefix shouldn't " "be None. How was the unit created? " "This is probably a bug.") new_prefix._history = _Product(other, self) else: new_prefix._history = _Product(other, *self._history.factors) return new_prefix def __truediv__(self, other): """ Divides the factor by the given operand. The operand must be integer, float or Prefix, otherwise NotImplemented is returned. This allows Unit and Quantity to implement custom behavior. The method returns a simple number (int, float). """ if not isinstance(other, (int, float, Prefix)): return NotImplemented if isinstance(other, SystemAffiliatedMixin): if self.unit_system() is not other.unit_system(): raise DifferentUnitSystem() if isinstance(other, Prefix): other = other.factor() # Create new anonymous Prefix return self.factor() / other def __rtruediv__(self, other): """ See __truediv__(). Similar, but with reversed roles. """ if not isinstance(other, (int, float, Prefix)): return NotImplemented if isinstance(other, SystemAffiliatedMixin): if self.unit_system() is not other.unit_system(): raise DifferentUnitSystem() if isinstance(other, Prefix): other = other.factor() # Create new anonymous Prefix return other / self.factor() def __div__(self, other): """ Compatibility with Python 2.7. The standard division (/) in Python 2.7 calls this method. Dividing a Prefix by an integer should not floor the factor. """ return self.__truediv__(other) def __rdiv__(self, other): """ Compatibility with Python 2.7. The standard division (/) in Python 2.7 calls this method. Dividing a Prefix by an integer should not floor the factor. """ return self.__rtruediv__(other)
[docs] def history_str(self, latex=False): """ Returns a string representation of the history. If the optional parameter latex=True, a latex version of the string is created. If the history is None, returns None. This method completely includes scalar factors in the history. """ if self._history is None: return None else: return self._history.str(latex=latex)
[docs]class Unit(Named, SystemAffiliatedMixin): """ The unit class represents physical units, such as Ampere or Newton. A unit is created by a unit system. A unit is permanently tied to the creating unit system. It is not necessary to create units with all possible prefixes. Prefixes are automatically handled once they are registered with a unit system. A unit inherits all the properties from Named and SystemAffiliated. Additionally, a factor and a unit vector is stored. The unit vector stores the exponents of the base units. Assume a unit system with three base units A, B and C. A unit with a unit vector of [0, 2, 1] corresponds to A^0 * B^2 * C^1. The factor can be used to generated arbitrarily scaled derived units. For example, if you set the factor to 60, the unit represent minutes, if it has the same unit vector as seconds. Please note that it is not necessary to register units with prefixes. Prefixes are handles by the unit system. Multiplications, divisions and powers are overloaded for units. These operations create a new unit object. The resulting objects are anonymous, i.e. their label and symbol properties are None. Furthermore, these derived units are not automatically added to the registry. If you want to add a anonymous unit to the registry, use the register_unit() method of the unit system. The properties of a unit can not be changed. """ def __init__(self, factor, unit_vector, label, symbol, latex, unit_system): """ For internal usage only. Creates a new Unit. Units should be created via a unit system. """ Named.__init__(self, label, symbol, latex) SystemAffiliatedMixin.__init__(self, unit_system) # Mathematical operations consider only the factor and the # unit_vector. self._factor = factor self._unit_vector = np.array(unit_vector) # copy external vector # The history of a unit is not used to perform actual calculations. # The history is maintained inorder to be able to print the unit in a # user friendly way. self._history = None
[docs] @staticmethod def create_with_history(factor, unit_vector, unit_system): """ For internal usage only. Alternative method to create a unit. Instead of assembling a new Unit from scratch, it is created by multiplying, dividing and exponentiating base units. The return value is an anonymous unit with a minimal history of base units. If the final unit is dimensionless (i.e. unit_vector = [0, 0, ...]), the factor is multiplied by the first base unit to the 0-th power. """ # Create initial history numerator_factors= [] denominator_factors= [] if factor != 1: numerator_factors.append(factor) for unit, exp in zip(unit_system._reg_base, unit_vector): if exp == 1: numerator_factors.append(unit) elif exp > 0: numerator_factors.append(unit**exp) elif exp == -1: denominator_factors.append(unit) elif exp < 0: denominator_factors.append(unit**(-exp)) if len(numerator_factors): unit = numerator_factors[0] for factor in numerator_factors[1:]: unit *= factor else: unit = 1 if len(denominator_factors): denominator = denominator_factors[0] for factor in denominator_factors[1:]: denominator *= factor unit /= denominator if isinstance(unit, (int, float)): unit *= unit_system._reg_base[0]**0 return unit
def __pow__(self, power): """ Returns the power of the unit. The power of the unit is calculated by exponentiating the factor and multiplying the unit vector with the given exponent. The exponent argument must be an integer or float. The method returns a new object, the object on which this method is called remains unchanged. The returned object is anonymous and not registered. """ ###################################################### # Type checks if not isinstance(power, (int, float)): return NotImplemented ###################################################### # Determine history if self.symbol() is None: history = _Power(self._history, power) else: history = _Power(self, power) ###################################################### # Determine factor factor = self.factor()**power unit_vector = self.unit_vector() * power ###################################################### # Assemble unit unit = Unit(factor, unit_vector, None, None, None, self.unit_system()) unit._history = history return unit def __mul__(self, other, reverse=False): """ Returns the product of the unit with the other operand. If the other operand is an integer, float or Prefix, only the factor is multiplied by the given number (or factor of the prefix). The unit vector is not affected. If the other operand is a unit object, the product of the two factors is calculated and the unit vectors are added. If the other operand is neither integer, float, Prefix nor unit object, NotImplemented is returned. This makes it possible to implement custom behavior in the Quantity class. The method returns a new object, the object on which this method is called remains unchanged. The returned object is anonymous and not registered. if reverse is True, the multiplication order is reversed. """ ###################################################### # Type checks if not isinstance(other, (int, float, Prefix, Unit)): return NotImplemented if isinstance(other, SystemAffiliatedMixin): if other.unit_system() is not self.unit_system(): raise DifferentUnitSystem() ###################################################### # Determine history # Recording of the history has been changed, see #9 and #17. factors = [] ####################### # Self history # 1. Named unit: If a named unit is encountered, use the unit # directly and do not copy any of its history. # 2. Unnamed unit without history: error # 3. History is _Product: If a participant is a product, extract the # factors and assemble joint product. # 4. History is _Arithmetic: If an other _Arithmetic is encountered, # it should be a factor of the resulting product. if self.symbol() is not None: factors.append(self) # Named elif self._history is None: # History is none raise Exception("History of this unnamed unit shouldn't be None. " "How was the unit created? " "This is probably a bug.") elif isinstance(self._history, _Product): factors.extend(self._history.factors) # Product elif isinstance(self._history, _Arithmetic): factors.append(self._history) # Arithmetic else: raise Exception("This is a bug. This unit has an unexpected " "history. How was it created?") if reverse: factors_self = factors factors = [] ####################### # Other history # 1. Named unit/prefix: If a named unit/prefix is encountered, use # the unit/prefix directly and do not copy any of its history. # 2. Unnamed unit/prefix without history: error # 3. History is _Product: If a participant is a product, extract the # factors and assemble joint product. # 4. History is _Arithmetic: If an other _Arithmetic is encountered, # it should be a factor of the resulting product. # 5. If other is neither Unit nor Prefix, use as factor. if isinstance(other, (Unit, Prefix)): if other.symbol() is not None: factors.append(other) # Named elif other._history is None: # History is none raise Exception("History of this unnamed unit/prefix shouldn't be None. " "How was the unit created? " "This is probably a bug.") elif isinstance(other._history, _Product): factors.extend(other._history.factors) # Product elif isinstance(other._history, _Arithmetic): factors.append(other._history) # Arithmetic else: # Other is number factors.append(other) if reverse: factors.extend(factors_self) ####################### # Build history object history = _Product(*factors) ###################################################### # Determine unit vector and factor unit_vector = self.unit_vector() if isinstance(other, Unit): unit_vector += other.unit_vector() other = other.factor() if isinstance(other, Prefix): other = other.factor() factor = self.factor() * other ###################################################### # Assemble new unit result = Unit(factor, unit_vector, None, None, None, self.unit_system()) result._history = history return result def __rmul__(self, other): """ See __mul__(). This operation is equivalent to __mul__ since multiplication is commutative. """ return self.__mul__(other, reverse=True) def __truediv__(self, other, reverse=False): """ Returns the division of the unit with the other operand. If the other operand is an integer, float or Prefix, only the factor is divided by the given number (or factor of the prefix). The unit vector is not affected. If the other operand is a unit object, the fraction of the two factors is calculated and the unit vectors are subtracted. If the other operand is neither integer, float, Prefix nor unit object, NotImplemented is returned. This makes it possible to implement custom behavior in the Quantity class. The method returns a new object, the object on which this method is called remains unchanged. The returned object is anonymous and not registered. If reverse is True, exchanges the order of the division. """ ###################################################### # Type checks if not isinstance(other, (int, float, Prefix, Unit)): return NotImplemented if isinstance(other, SystemAffiliatedMixin): if other.unit_system() is not self.unit_system(): raise DifferentUnitSystem() ###################################################### # Determine history # Recording of the history has been changed, see #9 and #17. numerator = None denominator = None ####################### # Self history # 1. Named unit: If a named unit is encountered, use the unit # directly and do not copy any of its history. # 2. Unnamed unit without history: error # 3. History is _Arithmetic: If an other _Arithmetic is encountered, # it should be a numerator of the resulting fraction. if self.symbol() is not None: numerator = self # Named elif self._history is None: # History is none raise Exception("History of this unnamed unit shouldn't be None. " "How was the unit created? " "This is probably a bug.") elif isinstance(self._history, _Arithmetic): numerator = self._history # Arithmetic else: raise Exception("This is a bug. This unit has an unexpected " "history. How was it created?") ####################### # Other history # 1. Named unit/prefix: If a named unit/prefix is encountered, use # the unit/prefix directly and do not copy any of its history. # 2. Unnamed unit/prefix without history: error # 3. History is _Product: If a participant is a product, extract the # factors and assemble joint product. # 4. History is _Arithmetic: If an other _Arithmetic is encountered, # it should be a factor of the resulting product. # 5. If other is neither Unit nor Prefix, use as factor. if isinstance(other, (Unit, Prefix)): if other.symbol() is not None: denominator = other # Named elif other._history is None: # History is none raise Exception("History of the other unnamed unit shouldn't " "be None. How was the unit created? " "This is probably a bug.") elif isinstance(other._history, _Arithmetic): denominator = other._history # Arithmetic else: raise Exception("This is a bug. The other unit has an unexpected " "history. How was it created?") else: # Other is number denominator = other ####################### # Build history object if reverse: history = _Fraction(denominator, numerator) else: history = _Fraction(numerator, denominator) ###################################################### # Determine unit vector and factor unit_vector = self.unit_vector() if isinstance(other, Unit): unit_vector -= other.unit_vector() other = other.factor() if reverse: unit_vector *= -1 if isinstance(other, Prefix): other = other.factor() if reverse: # Might avoid rounding errors factor = other / self.factor() else: factor = self.factor() / other ###################################################### # Assemble unit vector unit = Unit(factor, unit_vector, None, None, None, self.unit_system()) unit._history = history return unit def __rtruediv__(self, other): """ See __div__(). Similar, but with reversed roles. """ return self.__truediv__(other, reverse=True) def __div__(self, other): """ Compatibility with Python 2.7. The standard division (/) in Python 2.7 calls this method. Dividing a Unit by an integer should not floor the factor. """ return self.__truediv__(other) def __rdiv__(self, other): """ Compatibility with Python 2.7. The standard division (/) in Python 2.7 calls this method. Dividing a Unit by an integer should not floor the factor. """ return self.__rtruediv__(other)
[docs] def unit_vector(self): """ Returns the unit vector of the unit. The stores the exponents of the base units. The unit (neglecting the factor of the unit) is the product of all base units raised to the powers stored in the unit vector. The i-th value in the unit vector specifies the power of the i-th base unit. Unit vectors are stored as numpy arrays. This method returns a copy of the numpy array. This method provides read-only access. """ return np.array(self._unit_vector)
[docs] def dimensionless(self): """ Consults the unit system to see, whether this unit is dimensionless, and turns the outcome. If this is true, the unit can be used in mathematical functions such as Sine or the exponential function. """ return self.unit_system().dimensionless(self)
[docs] def factor(self): """ Returns the factor of the units. This method provides read-only access to the factor. """ return self._factor
def __repr__(self): """ Returns a string which shows the label and the symbol of the unit and its relation to base units. The returned string has the following pattern: <Unit <label>: 1 <symbol> = <factor> <A>^<a> <B>^<b> ... > where a and b are non-zero elements of the unit vector and A and B are base units with non-zero powers. If a symbol or a label is None, it is removed from the string representation. """ pre_colon = ["Unit"] post_colon = [] if self._label is not None: pre_colon.append(self._label) if self._symbol is not None: post_colon.append("1") post_colon.append(self._symbol) post_colon.append("=") post_colon.append("%g" % self._factor) base_repr = self._unit_system.base_representation(self._unit_vector) post_colon.append(base_repr) return "<%s: %s>" % (" ".join(pre_colon), " ".join(post_colon))
[docs] def history_str(self, latex=False): """ Returns a string representation of the history. If the optional parameter latex=True, a latex version of the string is created. If the history is None, returns None. This method completely ignores the factor of the unit. """ if self._history is None: return None else: return self._history.str(latex=latex)
[docs] def base_representation(self, latex=False, suppress_factor=False): """ Returns the unit using only base units. If the optional argument latex is True, this method returns a latex version. By default the returned string includes the factor of the unit. If suppress_factor is True, the returned string excludes the factor. """ base_repr = self.unit_system().base_representation(self.unit_vector(), latex) if suppress_factor: return base_repr else: return "%g %s" % (self.factor(), base_repr)
[docs] def str(self, latex=False): """ If the unit is unnamed returns the factor and the history of the unit. If the unit is named, returns the symbol. If the optional parameter latex=True, latex version of the string is created. """ if self.symbol() is None: return self.history_str(latex) else: if latex: return self.lors() else: return self.symbol()
def __str__(self): """ Calls Unit.str(). """ return self.str()
[docs]class UnitSystem: """ This class represents a unit system. This means it holds the definition of all base units. The set of base units spans a vector space (multiplication of base units is the vector space addition). All derived units are vectors in the vector space, this means they can be represented as a linear combination of the base vectors. The class also functions as a factory for units belonging to this unit system. The unit system keeps a registry of all units created within this system. The registry can be used to parse unit strings. Similarly, the unit system creates and registers prefixes which can be prepended to all units. A unit is considered dimensionless, if it is a linear combination of dimensionless base units. This means there can not be a linear combination of base vectors considered as dimensionless, which is not pure linear combination of dimensionless vector. """ def __init__(self, name, n_base, n_dimensionless=0): """ Creates a new unit system. The first argument specifies the number of base units. The second argument defines how many of them are considered to be dimensionless (n_base >= n_dimensionless). By convention the base units are numbered 0, 1, ..., n_base - 1. The last n_dimensionless base units are dimensionless. A freshly created unit system is agnostic of its base units names. To add names (labels, symbols and latex symbols) use the create_base_unit() method. Please note that this merely generated the unit objects and defines the names of the base units. This does not register the base units (unless register=True in create_base_unit()). Until all base units have been created, the placeholder [base#i] is use in string representations for the i-th base unit. """ self._name = name self._n_base = n_base self._n_dimensionless = n_dimensionless self._reg_base = [None] * n_base self._dimensionless_mask = np.zeros(n_base) self._dimensionless_mask[-n_dimensionless:] = 1 self._reg_prefixes_all = [] self._reg_prefixes_latex = {} self._reg_prefixes_label = {} self._reg_prefixes_symbol = {} self._reg_units_all = [] self._reg_units_latex = {} self._reg_units_label = {} self._reg_units_symbol = {}
[docs] def create_base_unit(self, index, label, symbol, latex=None, register=False): """ Creates a unit for the base unit identified with the given index. The created unit is returned. The created unit is not registered unless the argument 'register' is set to True. Please note that base units can not be scaled by a factor. Unlike create_unit, this method does not register the unit. Therefore, unregistered base units do not take part in the unit string parsing. This mechanism is useful if a base unit has a prefix, such as kilogram. The base class is kilogram. However, in order that prefixes work as expected (i.e. not create a double prefix Mkg, Mega-kilogram), one can register a non-base unit gram. Only gram is be considered during string parsing with all possible prefixes (e.g. kg, mg, etc.). For all other base units, one can set 'register' to True, or manually register them with the 'register_unit()' method. Since the index argument is used as a list index, it is possible to give negative values. This makes adding dimensionless units more convenient. The first dimensionless base unit can be created with index=-1, the second with index=-2, etc. Creating a base unit is final. Once a base unit has been created, create_base_unit() will raise an exception, if the same index is used again. """ if self._reg_base[index] is not None: raise BaseUnitExists() unit_vector = np.zeros(self._n_base) unit_vector[index] = 1 base = Unit(1, unit_vector, label, symbol, latex, self) self._reg_base[index] = base if register: self.register_unit(base, label, symbol, latex) return base
[docs] def create_unit(self, factor, unit_vector, label=None, symbol=None, latex=None): """ Creates and registers a new unit constructed from the given unit_vector and the factor. The newly created unit is returned. Please note that unlike create_base_unit(), all units created with this method are registered. If registering this unit causes ambiguities, an exception is raised. An example of an ambiguity is adding a unit with symbol 'h' (hour) if Planck's quantum with symbol 'h' is already registered. An example of a more subtle ambiguity is, if one tries to add a unit with the symbol 'min' (minutes) and there is already a unit 'in' (inch) and the prefix 'm' (Milli). """ unit = Unit(factor, unit_vector, label, symbol, latex, self) return self.register_unit(unit, label, symbol, latex)
[docs] def register_unit(self, unit, label, symbol, latex=None): """ Registers the given unit. Registering a unit means, that it is added to internal registry and will be considered when parsing a unit string. This method can be used to register anonymous units. The method returns the created unit. The same note about ambiguities for create_prefix() applies here. """ if unit.unit_system() is not self: raise DifferentUnitSystem() for other_unit in self._reg_units_symbol.keys(): for other_prefix in [""] + list(self._reg_prefixes_symbol.keys()): for this_prefix in [""] + \ list(self._reg_prefixes_symbol.keys()): if other_prefix + other_unit == this_prefix + symbol: raise SymbolCollision("Existing %s+%s conflicts with" " requested %s+%s." % \ (repr(other_prefix), repr(other_unit), repr(this_prefix), repr(symbol))) copy = Unit(unit.factor(), unit.unit_vector(), label, symbol, latex, self) copy._history = unit._history self._reg_units_all.append(copy) self._reg_units_label[label] = copy self._reg_units_symbol[symbol] = copy if latex is not None: self._reg_units_latex[latex] = copy return copy
[docs] def create_prefix(self, factor, label=None, symbol=None, latex=None): """ Creates and registers a new prefix built from the given factor. The newly created prefix is returned. If registering this prefix causes ambiguities, an exception is raised. An example of a ambiguity is, if one tries to add the 'm' (Milli) prefix, and there are already units with the symbols 'min' (minutes) and 'in' (inch). """ prefix = Prefix(factor, label, symbol, latex, self) return self.register_prefix(prefix, label, symbol, latex)
[docs] def register_prefix(self, prefix, label, symbol, latex=None): """ Registers the given prefix. Registering a prefix means, that it is added to internal registry and will be considered when parsing a unit string. This method can be used to register anonymous prefixes. The same note about ambiguities for create_prefix() applies here. """ if prefix.unit_system() is not self: raise DifferentUnitSystem() if symbol in self._reg_prefixes_symbol: raise SymbolCollision("Conflict with existing prefix %s" % \ repr(symbol)) for other_unit in self._reg_units_symbol.keys(): for other_prefix in [""] + list(self._reg_prefixes_symbol.keys()): for this_unit in self._reg_units_symbol.keys(): if other_prefix + other_unit == symbol + this_unit: raise SymbolCollision("Existing %s+%s conflicts with" " requested %s+%s." % \ (repr(other_prefix), repr(other_unit), repr(symbol), repr(this_unit))) copy = Prefix(prefix.factor(), label, symbol, latex, self) self._reg_prefixes_all.append(copy) self._reg_prefixes_label[label] = copy self._reg_prefixes_symbol[symbol] = copy if latex is not None: self._reg_prefixes_latex[latex] = copy return copy
[docs] def dimensionless(self, unit): """ Check whether the unit vector of the given unit is a pure linear combination of dimensionless base units. If so, return True, otherwise False. """ return not (unit.unit_vector() * (1 - self._dimensionless_mask)).any()
[docs] def parse_unit(self, expression): """ Try to parser the given string to construct a unit from the string. The new unit is returned on success. In case of an error, an exception is raised. While parsing the string, the method searches all combinations of registered prefixes and registered units to identify the individual tokens. The method only tries to parse a single unit, optionally combined with a prefix. """ if expression in self._reg_units_symbol.keys(): return self._reg_units_symbol[expression] for prefix in self._reg_prefixes_symbol.keys(): if expression.startswith(prefix): remainder = expression[len(prefix):] if remainder in self._reg_units_symbol.keys(): unit = self._reg_units_symbol[remainder] prefix = self._reg_prefixes_symbol[prefix] break else: raise UnitNotFound(expression) return Unit(prefix.factor() * unit.factor(), unit.unit_vector(), join_or_none(prefix.label(), unit.label()), join_or_none(prefix.symbol(), unit.symbol()), join_or_none(prefix.latex(), unit.latex()), self)
[docs] def base_representation(self, unit_vector, lors=False): """ Creates a string representation of the given unit vector using a product of powers of the base units. If the lors arguemnt is set to true, the method uses lors() of the base units. Otherwise symbol() is used. If there are undefined base units at the time of execution, these base units are represented by [base#i] where i is the index of the base unit. """ assert len(unit_vector) == self._n_base factors = [] for i, (base, power) in enumerate(zip(self._reg_base, unit_vector)): if base is None: base_str = "[base#%d]" % i elif lors: base_str = base.lors() else: base_str = base.symbol() if power == 1: factors.append(base_str) elif power > 0: if lors: pattern = "%s^{%g}" else: pattern = "%s^%g" factors.append(pattern % (base_str, power)) elif power < 0: if lors: pattern = "%s^{%g}" else: pattern = "%s^(%g)" factors.append(pattern % (base_str, power)) return " ".join(factors)
[docs]class Quantity(ModifiableNamedMixin, Named, SystemAffiliatedMixin): """ The Quantity stores a single value annotated with an uncertainty and a unit. The Quantity class is the working horse of the pyveu package. Quantity objects support arithmetic operations with other Quantities, Units and prefixes. Each operation generates a new object. Quantities keep track of its dependencies and how it has been constructed. This information is used to keep track of correlations between quantities and to propagate uncertainties. Arithmetic operations involving quantities consider the units of all participants. This means, that for example an addition of two quantities is only possible, if they have the same unit. Multiplications yield a new quantity object with the product of both units. The error propagation is calculated when the arithmetic operation is executed. Modifying one of the participating quantities does not modify the resulting quantity. Internally, quantity objects store the value in base units. The unit is stored as a unit vector which specifies the exponents of the base units. In order to propagates the uncertainty each quantity needs to keep a list of all quantities it was initially derived from. Additionally, quantities store the uncertainties of these initial quantities and the partial derivatives of the quantity with respect to the initial quantities. Initial quantities are creates with the Quantity() constructor. All quantities creates in this way are considered to be statistically independent. Quantities created by arithmetic operations are called dependent quantities, since their uncertainty depends on the initial independent quantities. Dependent quantities store the uncertainty of all the independent quantities it depends on, and the partial derivatives with respect to all independent quantities it depends on. The id() method is used to identify independent quantities. In-place arithmetic operations generate a new object, such that id() returns a different value. The returned object is a dependent quantity. This distinction is important in the following example >>> a = Quantity("10 += 1") >>> b = a + 3 # dependent quantity >>> a *= 2 # A has a different id() >>> c = a + 6 Quantity b depends on quantity a. However, when the quantity a is multiplied in-place, a new quantity a is created, which depends on the initial quantity a. The third quantity c, which is derived from the new a, also becomes a quantity depending on the initial quantity a. When one combines b and c in an arithmetic operations, the framework knows that the both variables depend on the same initial quantity. >>> 2 * a - c 0 +- 0 If the values had been modified in-place, leaving the id() unchanged, the resulting object would have an ambiguous value of the initial uncertainty of a. Quantity objects inherit all the properties from Named and SystemAffiliatedMixin. This means, Quantity object can be annotated with a label, symbol and latex alternative symbol. It is possible to modify the names using the label(), symbol() and latex() method. Furthermore, Quantity objects are tied to a unit system. The constructor has an optional argument unit_system. If it is omitted or set to None, the new Quantity object is tied to the unit system returned by pyveu.get_default_unit_system(). Various methods accept the 'to' argument. This argument changes the unit on which the return value is based. For example, consider a quantity for the speed of a tennis ball. >>> v = Quantity("60 m/s") By default, the value() method returns the value in base units. >>> v.value() 60 If the 'to' argument is passed to the value method, one can retrieve the value in different units. >>> v.value(to="km/h") 216.0 """ qid_counter = 1 def __init__(self, value, error=None, unit=None, label=None, symbol=None, latex=None, unit_system=None): """ Creates a new, statistically independent quantity object. If the value argument is a string and error and unit are None, the value argument is parsed to obtain the value, the uncertainty and the unit of the quantity. Otherwise, value must be a number. The error argument defines the uncertainty of the quantity. If the value is a string, error must be None. The unit argument can be a unit object or a unit_vector. In any case value and error are assumed to be given in this unit. If the unit_system parameter is omitted, the quantity is tied to the unit system returned by pyveu.get_default_unit_system(). Otherwise, the given unit system is used. If the parameter is omitted but a unit object is given, the unit system of the unit object is used. """ Named.__init__(self, label, symbol, latex) self._qid = Quantity.qid_counter Quantity.qid_counter += 1 # resolve unit system if unit_system is None: if isinstance(unit, Unit): unit_system = unit.unit_system() else: unit_system = get_default_unit_system() else: if isinstance(unit, Unit): if unit.unit_system() is not unit_system: raise DifferentUnitSystem() SystemAffiliatedMixin.__init__(self, unit_system) self._derivatives = {} self._variances = {} if isinstance(value, str): # parse quantity string if error is not None: raise ValueError("When value is a string, error must be None.") if unit is not None: raise ValueError("When value is a string, unit must be None.") value, error, unit_vector = Quantity.parse(value, unit_system) self._variance = error**2 self._value = value self._unit_vector = unit_vector else: factor = 1 # resolve unit if unit is None: self._unit_vector = np.zeros(unit_system._n_base) elif isinstance(unit, collections.Iterable): self._unit_vector = np.array(unit) if len(self._unit_vector) != unit_system._n_base: raise ValueError("Unit vector length mismatch.") else: self._unit_vector = unit.unit_vector() factor = unit.factor() # resolve value self._value = value * factor # resolve error if error is None: error = 0 if error < 0: raise ValueError("Error must be positive.") error *= factor self._variance = error**2
[docs] def qid(self): """ Returns an identifier of the quantity. The identifier is unique for the lifetime of the python process. The identifier is an integer. The qid is used to track the dependencies between quantities. """ return self._qid
[docs] def value(self, to=None): """ Returns the value of the quantity object. If the optional argument 'to' is omitted, the returned value is in base units. If a string or a unit object is given, the unit is returned in this unit. If the given unit is not a scalar multiple of this quantity, an exception is raised. """ if to is None: return self._value elif isinstance(to, str): to = Quantity.parse_unit(to, self.unit_system()) if to.unit_system() is not self.unit_system(): raise DifferentUnitSystem() if (to.unit_vector() != self.unit_vector()).any(): raise ValueError("Unit is not a scalar multiple.") return self._value / to.factor()
[docs] def error(self, to=None): """ Returns the uncertainty of the quantity object. If the optional argument 'to' is omitted, the returned error is in base units. If a string or a unit object is given, the unit is returned in this unit. If the given unit is not a scalar multiple of this quantity, an exception is raised. The return value is the square root of variance(). """ return math.sqrt(self.variance(to))
[docs] def variance(self, to=None): """ Returns the variance of the quantity object. If the optional argument 'to' is omitted, the returned error is in base units. If a string or a unit object is given, the unit is returned in this unit. If the given unit is not a scalar multiple of this quantity, an exception is raised. """ if to is None: return self._variance elif isinstance(to, str): to = Quantity.parse_unit(to, self.unit_system()) if to.unit_system() is not self.unit_system(): raise DifferentUnitSystem() if (to.unit_vector() != self.unit_vector()).any(): raise ValueError("Unit is not a scalar multiple.") return self._variance / to.factor()**2
[docs] def round(self, to=None, significant_digits=1.2, exponent=None): """ Returns the triple: rounded value as string, rounded error as string, common exponent as integer. This information can be used to construct string representations of the quantities. The value string is rounded to the last significant digit of the rounded error. The optional keywords significant_digits determines how the error should be rounded. If significant_digits is set to an integer, this corresponds to the number of significant digits of the error. If significant_digits is a float, the error is rounded to ceil(significant_digits) or floor(significant_digits), depending on the value of the error. For example, if significant_digits is 1.2, errors between 0.10 and 0.20 are rounded to two significant_digits, errors between 0.2 and 1.0 are rounded to one significant digit. The border between these two ranges is determined by the decimal part of significant_digits. Both, the value and the error, need to be multiplied with 10^(common_exponent). This means the returned information can be used to construct a sting of the form (rounded_value +- rounded_error) * 10^(common_exponent). If the optional argument 'to' is omitted, the returned error is in base units. If a string or a unit object is given, the unit is returned in this unit. If the given unit is not a scalar multiple of this quantity, an exception is raised. If the optional argument 'exponent' is given, the common exponent is fixed to the given value. """ value = self.value(to) error = self.error(to) if significant_digits < 1: raise ValueError("Argument significant_digits must be >= 1.") if error == 0: if value == 0: return "0", "0", 0 return str(value), "0", 0 # Get error MSD error_msd_pos = Quantity._msd_position(error) # Determine (int) number of significant digits if not isinstance(significant_digits, int): is_int = False threshold = significant_digits - _floor(significant_digits) # 10**int form build ins doesn't work for negative values error_msd = error * math.pow(10, -error_msd_pos - 1) significant_digits = _floor(significant_digits) if error_msd < threshold: significant_digits += 1 else: is_int = True # Determine round position (considering error after rounding) round_pos = error_msd_pos - significant_digits + 1 last_digit = Quantity._get_digit(error, round_pos) rounded_error = _round(error, -round_pos) rounded_last_digit = Quantity._get_digit(rounded_error, round_pos) if last_digit == 9 and rounded_last_digit == 0 and is_int: round_pos += 1 # Extract common factor if exponent is None: value_msd_pos = Quantity._msd_position(value) if value_msd_pos > 3 and error_msd_pos > 3: min_msd_pos = min(value_msd_pos, error_msd_pos) common_factor = int(min_msd_pos / 3) * 3 elif value_msd_pos < -3 and error_msd_pos < -3: max_msd_pos = max(value_msd_pos, error_msd_pos) common_factor = int(max_msd_pos / 3) * 3 else: common_factor = 0 else: common_factor = exponent value /= math.pow(10, common_factor) error /= math.pow(10, common_factor) round_pos -= common_factor # Round error and value value = _round(value, -round_pos) error = _round(error, -round_pos) # Format string if round_pos < 0: value = "%.{}f".format(-round_pos) % value error = "%.{}f".format(-round_pos) % error else: value = "%d" % value error = "%d" % error return value, error, common_factor
@staticmethod def _get_digit(number, position): """ Return the digit at the given position. Consider 3352.429, the digit at position 1 is 5, the digit at position -3 is 9. """ upper = _floor(simple_abs(number) * 10**(-position)) lower = _floor(upper / 10) * 10 return upper - lower @staticmethod def _msd_position(number): """ Returns the position of the most significant digit of the given number. The most significant digit of 1 is at position zero, the most significant digit of 314 is 2. """ return int(_floor(math.log10(simple_abs(number))))
[docs] def str(self, to=1, significant_digits=1.2): """ Returns a string containing the value, error and the unit of the quantity. The error is rounded to 1 significant digit (two between 1.0 and 2.0), the value is rounded to the last significant digit of the error. By default, the value and error are returned in base units. If the optional argument 'to' is given, it is used to determine the desired unit. 'to' can be a string or a unit object. If the given unit is not a scalar multiple of this quantity, all the remaining terms are added to the unit string. Any factor included in 'to' is ignored. See round() for more information about significant_digits. """ if isinstance(to, (int, float)): round_to = Unit.create_with_history(to, self.unit_vector(), self.unit_system()) remainder = round_to elif isinstance(to, str): to = Quantity.parse_unit(to, self.unit_system()) if isinstance(to, Unit): remainder_vector = self.unit_vector() - to.unit_vector() remainder = Unit.create_with_history(1, remainder_vector, self.unit_system()) round_to = to * remainder value, error, common_factor = self.round(round_to, significant_digits) tokens = [] if self.symbol(): tokens.append(self.symbol()) tokens.append("=") if error != "0": value_error = "%s +- %s" % (value, error) else: value_error = value if error != "0" and (self.unit_vector() != 0).any(): value_error = "(%s)" % value_error tokens.append(value_error) if common_factor != 0: tokens.append("*") tokens.append("10^%d" % common_factor) if to != 1: tokens.append(to.str()) has_remainder = remainder.factor() != 1 or remainder.unit_vector().any() if to != 1 and has_remainder: tokens.append("*") if has_remainder: tokens.append(remainder.str()) return " ".join(tokens)
[docs] def unit_vector(self): """ Returns the unit vector. """ return np.array(self._unit_vector)
[docs] def dimensionless(self): """ Check whether the unit vector of this quantity is dimensionless(). Dimensionless quantities can be used in mathematical function such as Sine of the exponential function. """ return self.unit_system().dimensionless(self)
def __str__(self): """ Same as Quantity.str() with default arguments. """ return self.str() def __repr__(self): """ Returns a string containing the label and symbol if present, the value error and unit in base units. Furthermore, the string contains the list of id() of all the quantities it depends on. An example of the string is <Quantity Velocity: v = (219.2 +- 10.0) m s^-1 | depends=[124323432]> """ pre_colon = ["Quantity"] post_colon = [] if self._label is not None: pre_colon.append(self._label) if self._symbol is not None: post_colon.append(self._symbol) post_colon.append("=") if self.error() != 0: value_error = "%g +- %g" % (self._value, self.error()) else: value_error = "%g" % self._value if self.error() != 0 and (self._unit_vector != 0).any(): post_colon.append("(%s)" % value_error) else: post_colon.append(value_error) base_repr = self._unit_system.base_representation(self._unit_vector) if len(base_repr): post_colon.append(base_repr) if len(self._derivatives) > 0: ids = [str(x) for x in sorted(self._derivatives)] post_colon.append("|") post_colon.append("depends=[%s]" % ", ".join(ids)) return "<%s: %s>" % (" ".join(pre_colon), " ".join(post_colon))
[docs] @staticmethod def parse(expression, unit_system=None): """ Parses the expression and returns the triple: value, error and unit vector. The optional parameter determines the unit system. If it is omitted (or set to None), the default unit system is used. This is a static method. It should be called using the class. >>> Quantity.parse("125.09 +- 0.24 GeV") (125.09, 0.24, np.array(...)) The syntax of the expression is intended to follow mathematical intuition. An expression consists of three parts. The first part specifies the value of a quantity. This part is mandatory. The value must be specified as an integer or a float. Syntax of the value is similar to the float syntax in python. The second part is optional and specifies the uncertainty of the value. If this part is included it must start with the string '+-' followed by another integer of float. The error must be positive. The final part specifies the unit. The method returns a unit vector of (0, 0, ..., 0) if this part is omitted. The unit part consists of an arbitrary number of unit specifiers. A unit specifier is a string consisting a registered unit and optionally a registered prefix. unit specifies can be raised to a power by appending '^' and the exponent. The exponent must be an integer or a float. If unit specifies are separated by spaces, they are multiplied. Unlike the regular mathematical notation, a slash '/' begins a denominator sequence. All space-separated unit specifiers following the slash are part of the denominator. Another slash '/' continues the denominator. To switch back to the numerator, use the star '*'. The following examples illustrate the denominator/numerator convention. The right hand side in the following examples follow the regular mathematical conventions. a / b c = a / (b * c) a / b / c = a / (b * c) a / b * c = (a * c) / b a c / b = (a * c) / b a * c / b = (a * c) / b To summarize: Everything to the right of a slash '/' before a star '*' is in the denominator, independently of the number of slashes. Everything to the right of a star '/' before a slash '/' is in the numerator, independently of the number of stars. Full expression strings look like this. "10.1 +- 0.3 mm / s" "210 +- 2 m s^-1" "3e8 kg / m^2" "-42" "1e-3 +- 43e-5" "312 +- 21.1 kg m / s^2" Please note that the use of parentheses is not possible. """ # default value if unit_system is None: unit_system = get_default_unit_system() # coarse regular expression number_re = r"[+-]?[0-9]+(\.[0-9]+)?(e[+-]?[0-9]+)?" unit_re = r"[a-zA-Z*/\s][a-zA-Z0-9\s/*^+.-]*" total_re = r"^\s*(?P<value>%s)(\s*\+-\s*(?P<error>%s))?(?P<unit>%s)?$" total_re %= (number_re, number_re, unit_re) match = re.match(total_re, expression) if not match: raise ValueError("Syntax error in '%s'." % expression) groups = match.groupdict() # handle value value_part = groups["value"] value = float(value_part) # handle error error_part = groups["error"] error_part = error_part if error_part is not None else 0 error = float(error_part) if error < 0: raise ValueError("Error %g < 0, must be non-negative." % error) # handle unit unit_part = groups["unit"] if unit_part is not None: unit = Quantity.parse_unit(unit_part, unit_system) value *= unit.factor() error *= unit.factor() unit_vector = unit.unit_vector() else: unit_vector = np.zeros(unit_system._n_base) return value, error, unit_vector
[docs] @staticmethod def parse_unit(unit_part, unit_system=None): """ Parse the unit part of Quantity expression. See Quantity.parse() for more information about the syntax. The method returns a unit object. The history of the unit object reflects the structure of given string. If the optional argument unit_system is omitted, the default unit system is used. """ # Default value if unit_system is None: unit_system = get_default_unit_system() unit_token_re = r"\s*((?P<op>[*/])|(?P<name>[a-zA-Z][a-zA-Z0-9]*)" \ r"(\^(?P<exp>[-+]?[0-9]+(\.[0-9]+)?))?)\s*" unit_re = r"[a-zA-Z*/\s][a-zA-Z0-9\s/*^+.-]*" if not re.match("^(%s)*$" % unit_re, unit_part): raise ValueError("Error while parsing Unit string '%s'." % unit_part) # loop over all unit specifier and operators unit_vector = np.zeros(unit_system._n_base) mode = +1 # -1 for denominator, +1 for numerator previous_op = False # whether the previous token was a operator numerator_factors = [] denominator_factors = [] for unit_token in re.finditer(unit_token_re, unit_part): groups = unit_token.groupdict() # Found a '*' if groups["op"] == '*': if previous_op: raise ValueError("Unexpected '*', expected unit.") previous_op = True mode = +1 continue # Found a '/' if groups["op"] == '/': if previous_op: raise ValueError("Unexpected '/', expected unit.") previous_op = True mode = -1 continue # Found a unit if groups["name"] is not None: previous_op = False unit = unit_system.parse_unit(groups["name"]) # calculate effective exponent exponent = groups["exp"] exponent = exponent if exponent is not None else 1 exponent = float(exponent) exponent *= mode # merge with current result if exponent == 1: numerator_factors.append(unit) elif exponent > 0: numerator_factors.append(unit**exponent) elif exponent == -1: denominator_factors.append(unit) elif exponent < 0: denominator_factors.append(unit**(-exponent)) if previous_op: raise ValueError("Trailing '*' or '/'.") if len(numerator_factors): result = numerator_factors[0] for factor in numerator_factors[1:]: result *= factor else: result = 1 if len(denominator_factors): denominator = denominator_factors[0] for factor in denominator_factors[1:]: denominator *= factor result /= denominator if isinstance(result, (int, float)): if result == 1: result = unit_system._reg_base[0]**0 else: result *= unit_system._reg_base[0]**0 return result
def __neg__(self): """ Calculates the negative of this quantity and returns the result as a new dependent quantity. """ return self * (-1) def _generic_operation(self, other, value_handler, d_dx_handler, unit_handler): """ Generic multiplication operator. By passing appropriate handlers this method can be used for (right and left) multiplications and (right and left) divisions. Consider the generic operator #, and the generic operation c = a # b. The handler signatures are as follows: value_handler(a) -> Returns the value of the result: c d_dx_handler(a, da_dx) -> Returns the derivative dc_dx unit_handler(unit_vector_a, a, unit_system) -> Returns the unit_vector of c """ if isinstance(other, (int, float)): other = Quantity(other, unit_system=self.unit_system()) elif isinstance(other, Prefix): other = Quantity(other.factor(), unit_system=other.unit_system()) elif isinstance(other, Unit): other = Quantity(other.factor(), 0, other.unit_vector(), unit_system=other.unit_system()) if other.unit_system() is not self.unit_system(): raise DifferentUnitSystem() value = value_handler(self.value(), other.value()) unit = unit_handler(self.unit_vector(), other.unit_vector(), self.value(), other.value(), self.unit_system()) result = Quantity(value, 0, # Will operate on variance and derivatives unit, unit_system=self.unit_system()) if self._variances: variances_self = self._variances derivatives_self = self._derivatives elif self.variance() > 0: variances_self = {self.qid(): self.variance()} derivatives_self = {self.qid(): 1} else: variances_self = {} derivatives_self = {} if other._variances: variances_other = other._variances derivatives_other = other._derivatives elif other.variance() > 0: variances_other = {other.qid(): other.variance()} derivatives_other = {other.qid(): 1} else: variances_other = {} derivatives_other = {} dependencies = set(variances_self.keys()).union(variances_other.keys()) result._variance = 0 for dependency in dependencies: if dependency in variances_self and dependency in variances_other: if variances_self[dependency] != variances_other[dependency]: raise ValueError("This is a bug. Two quantities stored " "different variances for the same qid.") # Implementation of the chain rule derivative = d_dx_handler(self.value(), other.value(), derivatives_self.get(dependency, 0), derivatives_other.get(dependency, 0)) if dependency in variances_self: variance = variances_self[dependency] else: variance = variances_other[dependency] if variance == 0 or derivative == 0: continue result._variance += variance * derivative**2 result._variances[dependency] = variance result._derivatives[dependency] = derivative return result @staticmethod def _mul_value_handler(a, b): """ Helper method for multiplcations. Combines the two values. """ return a * b @staticmethod def _mul_d_dx_handler(a, b, da_dx, db_dx): """ Helper method for multiplcations. Computes a combined derication. """ return a * db_dx + da_dx * b @staticmethod def _mul_unit_handler(unit_vector_a, unit_vector_b, a, b, unit_system): """ Helper method for multiplcations. Computes a combined derication. """ return unit_vector_a + unit_vector_b def __mul__(self, other): """ Multiplies the Quantity with an other Quantity, Unit or prefix. This multiplies the values, propagates the errors and multiplies the unit. The method returns a new Quantity. """ return self._generic_operation(other, self._mul_value_handler, self._mul_d_dx_handler, self._mul_unit_handler) def __rmul__(self, other): """ Multiplication where this object is one the right. There is no difference between left- and right-multiplication. """ return self * other @staticmethod def _div_value_handler(a, b): """ Helper method for divisions. Combines the two values. """ return a / b @staticmethod def _div_d_dx_handler(a, b, da_dx, db_dx): """ Helper method for divisions. Computes a combined derication. """ return da_dx / b - a / b**2 * db_dx @staticmethod def _div_unit_handler(unit_vector_a, unit_vector_b, a, b, unit_system): """ Helper method for divisions. Computes a combined derication. """ return unit_vector_a - unit_vector_b def __truediv__(self, other): """ Divides the Quantity with an other Quantity, Unit or prefix. This divides the values, propagates the errors and divides the unit. The method returns a new Quantity. """ return self._generic_operation(other, self._div_value_handler, self._div_d_dx_handler, self._div_unit_handler) @staticmethod def _rdiv_value_handler(a, b): """ Helper method for divisions. Combines the two values. """ return Quantity._div_value_handler(b, a) @staticmethod def _rdiv_d_dx_handler(a, b, da_dx, db_dx): """ Helper method for divisions. Computes a combined derication. """ return Quantity._div_d_dx_handler(b, a, db_dx, da_dx) @staticmethod def _rdiv_unit_handler(unit_vector_a, unit_vector_b, a, b, unit_system): """ Helper method for divisions. Computes a combined derication. """ return Quantity._div_unit_handler(unit_vector_b, unit_vector_a, b, a, unit_system) def __rtruediv__(self, other): """ Divides the Quantity with an other Quantity, Unit or prefix. This divides the values, propagates the errors and divides the unit. The method returns a new Quantity. """ return self._generic_operation(other, self._rdiv_value_handler, self._rdiv_d_dx_handler, self._rdiv_unit_handler) def __div__(self, other): """ Compatibility with Python 2.7. The standard division (/) in Python 2.7 calls this method. Dividing a Quantity by an integer should not floor the value. """ return self.__truediv__(other) def __rdiv__(self, other): """ Compatibility with Python 2.7. The standard division (/) in Python 2.7 calls this method. Dividing a Quantity by an integer should not floor the value. """ return self.__rtruediv__(other) @staticmethod def _add_value_handler(a, b): """ Helper method for additions. Combines the two values. """ return a + b @staticmethod def _add_d_dx_handler(a, b, da_dx, db_dx): """ Helper method for additions. Computes a combined derivations. """ return da_dx + db_dx @staticmethod def _add_unit_handler(unit_vector_a, unit_vector_b, a, b, unit_system): """ Helper method for additions. Computes a combined unit vectors. """ unit_a = Unit(1, unit_vector_a, None, None, None, unit_system) unit_b = Unit(1, unit_vector_b, None, None, None, unit_system) if unit_a.dimensionless() and (unit_vector_b == 0).all(): # Adding integer to dimensionless, this is fine. return unit_vector_a elif unit_b.dimensionless() and (unit_vector_a == 0).all(): # Same here, roles swapped return unit_vector_b elif (unit_vector_a != unit_vector_b).any(): # Otherwise unit vectors need to be identical raise ValueError("Cannot add %s and %s." % (unit_a.base_representation(), unit_b.base_representation())) else: return unit_vector_a def __add__(self, other): """ Adds a Quantity and another Quantity, Unit or prefix. This adds the values, propagates the errors. Units need to identical or dimensionless. The method returns a new Quantity. """ return self._generic_operation(other, self._add_value_handler, self._add_d_dx_handler, self._add_unit_handler) def __radd__(self, other): """ Addition where this object is one the right. There is no difference between left- and right-addition. """ return self + other @staticmethod def _sub_value_handler(a, b): """ Helper method for subtractions. Combines the two values. """ return a - b @staticmethod def _sub_d_dx_handler(a, b, da_dx, db_dx): """ Helper method for subtractions. Computes a combined derivations. """ return da_dx - db_dx def __sub__(self, other): """ Adds a Quantity and another Quantity, Unit or prefix. This adds the values, propagates the errors. Units need to identical or dimensionless. The method returns a new Quantity. """ return self._generic_operation(other, self._sub_value_handler, self._sub_d_dx_handler, self._add_unit_handler) @staticmethod def _rsub_value_handler(a, b): """ Helper method for subtractions. Combines the two values. """ return Quantity._sub_value_handler(b, a) @staticmethod def _rsub_d_dx_handler(a, b, da_dx, db_dx): """ Helper method for subtractions. Computes a combined derivations. """ return Quantity._sub_d_dx_handler(b, a, db_dx, da_dx) def __rsub__(self, other): """ Subtraction where this object is one the right. """ return self._generic_operation(other, self._rsub_value_handler, self._rsub_d_dx_handler, self._add_unit_handler) @staticmethod def _pow_value_handler(a, b): """ Helper method for exponentiations. Combines the two values. """ if a <= 0 and _floor(b) != b: raise ValueError("Cannot raise negative number to " "a fractional power.") return a**b @staticmethod def _pow_d_dx_handler(a, b, da_dx, db_dx): """ Helper method for exponentiations. Computes a combined derivations. """ if a <= 0 and db_dx != 0: raise UncertaintyIllDefined("Base cannot be negative when " "exponent has uncertainty.") result = 0 if da_dx != 0: if a == 0 and b < 1: raise UncertaintyIllDefined("Base cannot be zero with " "uncertainty when " "exponent is less than one.") result += b * a**(b - 1) * da_dx if db_dx != 0: result += a**b * np.log(a) * db_dx return result @staticmethod def _pow_unit_handler(unit_vector_a, unit_vector_b, a, b, unit_system): """ Helper method for exponentiations. Computes a combined unit vectors. """ unit_b = Unit(1, unit_vector_b, None, None, None, unit_system) if not unit_b.dimensionless(): raise ValueError("Exponent '%s' is not dimensionless." % unit_b.base_representation()) else: return unit_vector_a * b def __pow__(self, power): """ Calculates a power of this quantity and returns the result as a new dependent quantity. The other operand can be a number, a prefix, a or a quantity. If it is a unit or a quantity, it must be dimensionless(). """ return self._generic_operation(power, self._pow_value_handler, self._pow_d_dx_handler, self._pow_unit_handler) @staticmethod def _rpow_value_handler(a, b): """ Helper method for exponentiations. Combines the two values. """ return Quantity._pow_value_handler(b, a) @staticmethod def _rpow_d_dx_handler(a, b, da_dx, db_dx): """ Helper method for exponentiations. Computes a combined derivations. """ return Quantity._pow_d_dx_handler(b, a, db_dx, da_dx) @staticmethod def _rpow_unit_handler(unit_vector_a, unit_vector_b, a, b, unit_system): """ Helper method for exponentiations. Computes a combined unit vectors. """ return Quantity._pow_unit_handler(unit_vector_b, unit_vector_a, b, a, unit_system) def __rpow__(self, power): """ Calculates a power of this quantity and returns the result as a new dependent quantity. The other operand can be a number, a prefix, a or a quantity. If it is a unit or a quantity, it must be dimensionless(). """ return self._generic_operation(power, self._rpow_value_handler, self._rpow_d_dx_handler, self._rpow_unit_handler)
def _generic_function(argument, value_handler, d_dx_handler, unit_handler): """ Generic function. By passing appropriate handlers, this method can be used for generic mathematical functions. Consider the generic function f, and the generic calculation c = f(a) The handler signatures are as follows: value_handler(a) -> Returns the value of the result: c d_dx_handler(a, da_dx) -> Returns the derivative dc_dx unit_handler(unit_vector_a, a, unit_system) -> Returns the unit_vector of c """ if isinstance(argument, (int, float)): argument = Quantity(argument) elif isinstance(argument, Prefix): argument = Quantity(argument, unit_system=argument.unit_system()) elif isinstance(argument, Unit): argument = Quantity(argument.factor(), 0, argument.unit_vector(), unit_system=argument.unit_system()) value = value_handler(argument.value()) unit = unit_handler(argument.unit_vector(), argument.value(), argument.unit_system()) result = Quantity(value, 0, # Will operate on variance and derivatives unit, unit_system=argument.unit_system()) if argument._variances: variances = argument._variances derivatives = argument._derivatives elif argument.variance() > 0: variances = {argument.qid(): argument.variance()} derivatives = {argument.qid(): 1} else: variances = {} derivatives = {} result._variance = 0 for dependency in variances: variance = variances[dependency] # Implementation of the chain rule derivative = d_dx_handler(argument.value(), derivatives[dependency]) if derivative == 0: continue result._variance += variance * derivative**2 result._variances[dependency] = variance result._derivatives[dependency] = derivative return result ################################################################################ # Exponential function def _exp_value_handler(a): """ Calculates value of expoential function. """ return math.exp(a) def _exp_d_dx_handler(a, da_dx): """ Calculates the derivative d/dx exp(a(x)). """ return math.exp(a) * da_dx def _no_unit_handler(unit_vector, a, unit_system): """ Check that the unit vector is dimensionless and return a zero unit vector. """ unit = Unit(1, unit_vector, None, None, None, unit_system) if not unit.dimensionless(): raise ValueError("Operand '%s' is not dimensionless." % unit.base_representation()) return unit_vector * 0
[docs]def exp(quantity): """ Calculates the exponential of the given quantity and propagates the uncertainty. The quantity has to be dimensionless. """ return _generic_function(quantity, _exp_value_handler, _exp_d_dx_handler, _no_unit_handler)
################################################################################ # Natural logarithm def _log_value_handler(a): """ Calculates value of natural logaritm. """ if a <= 0: raise ValueError("Argument of log must be positive.") return math.log(a) def _log_d_dx_handler(a, da_dx): """ Calculates the derivative d/dx log(a(x)). """ return da_dx / a
[docs]def log(quantity): """ Calculates the natural logarithm of the given quantity and propagates the uncertainty. The quantity has to be dimensionless. """ return _generic_function(quantity, _log_value_handler, _log_d_dx_handler, _no_unit_handler)
################################################################################ # Logaritm to the base 2
[docs]def log2(quantity): """ Calculates the logarithm to the base 2 of the given quantity and propagates the uncertainty. The quantity has to be dimensionless. """ return 1 / math.log(2) * _generic_function(quantity, _log_value_handler, _log_d_dx_handler, _no_unit_handler)
################################################################################ # Logaritm to the base 10
[docs]def log10(quantity): """ Calculates the logarithm to the base 10 of the given quantity and propagates the uncertainty. The quantity has to be dimensionless. """ return 1 / math.log(10) * _generic_function(quantity, _log_value_handler, _log_d_dx_handler, _no_unit_handler)
################################################################################ # Power
[docs]def pow(base, exponent): """ Calculates the power the given quantity and propagates the uncertainty. The exponent has to be dimensionless. See Quantity's power operator. """ return base**exponent
################################################################################ # Square root
[docs]def sqrt(quantity): """ Calculates the square root of the given quantity and propagates the uncertainty. The quantity has to be dimensionless. """ return quantity**0.5
################################################################################ # Sine def _sin_value_handler(a): """ Calculates the sine of a. """ return math.sin(a) def _sin_d_dx_handler(a, da_dx): """ Calculates the derivative d/dx sin(a(x)). """ return da_dx * math.cos(a)
[docs]def sin(quantity): """ Calculates the sine of the given quantity and propagates the uncertainty. The quantity has to be dimensionless. """ return _generic_function(quantity, _sin_value_handler, _sin_d_dx_handler, _no_unit_handler)
################################################################################ # Cosine def _cos_value_handler(a): """ Calculates the cosine of a. """ return math.cos(a) def _cos_d_dx_handler(a, da_dx): """ Calculates the derivative d/dx cos(a(x)). """ return -da_dx * math.sin(a)
[docs]def cos(quantity): """ Calculates the cosine of the given quantity and propagates the uncertainty. The quantity has to be dimensionless. """ return _generic_function(quantity, _cos_value_handler, _cos_d_dx_handler, _no_unit_handler)
################################################################################ # Tangent
[docs]def tan(quantity): """ Calculates the tangent of the given quantity and propagates the uncertainty. The quantity has to be dimensionless. """ return sin(quantity) / cos(quantity)
################################################################################ # Hyperbolic sine def _sinh_value_handler(a): """ Calculates the hyperbolic sine of a. """ return math.sinh(a) def _sinh_d_dx_handler(a, da_dx): """ Calculates the derivative d/dx sinh(a(x)). """ return da_dx * math.cosh(a)
[docs]def sinh(quantity): """ Calculates the hyperblic sine of the given quantity and propagates the uncertainty. The quantity has to be dimensionless. """ return _generic_function(quantity, _sinh_value_handler, _sinh_d_dx_handler, _no_unit_handler)
################################################################################ def _cosh_value_handler(a): """ Calculates the hyperbolic sine of a. """ return math.cosh(a) def _cosh_d_dx_handler(a, da_dx): """ Calculates the derivative d/dx sinh(a(x)). """ return da_dx * math.sinh(a)
[docs]def cosh(quantity): """ Calculates the hyperbolic cosine of the given quantity and propagates the uncertainty. The quantity has to be dimensionless. """ return _generic_function(quantity, _cosh_value_handler, _cosh_d_dx_handler, _no_unit_handler)
################################################################################ # Hyperbolic tangent
[docs]def tanh(quantity): """ Calculates the hyperbolic tangent of the given quantity and propagates the uncertainty. The quantity has to be dimensionless. """ return sinh(quantity) / cosh(quantity)
################################################################################ # Inverse sine def _asin_value_handler(a): """ Calculates the inverse sine of a. """ return math.asin(a) def _asin_d_dx_handler(a, da_dx): """ Calculates the derivative d/dx asin(a(x)). """ return da_dx / math.sqrt(1 - a**2)
[docs]def asin(quantity): """ Calculates the inverse sine of the given quantity and propagates the uncertainty. The quantity has to be dimensionless. """ return _generic_function(quantity, _asin_value_handler, _asin_d_dx_handler, _no_unit_handler)
################################################################################ # Inverse cosine def _acos_value_handler(a): """ Calculates the inverse cosine of a. """ return math.acos(a) def _acos_d_dx_handler(a, da_dx): """ Calculates the derivative d/dx acos(a(x)). """ return -da_dx / math.sqrt(1 - a**2)
[docs]def acos(quantity): """ Calculates the inverse cosine of the given quantity and propagates the uncertainty. The quantity has to be dimensionless. """ return _generic_function(quantity, _acos_value_handler, _acos_d_dx_handler, _no_unit_handler)
################################################################################ # Inverse tangent def _atan_value_handler(a): """ Calculates the inverse tangent of a. """ return math.atan(a) def _atan_d_dx_handler(a, da_dx): """ Calculates the derivative d/dx atan(a(x)). """ return da_dx / (1 + a**2)
[docs]def atan(quantity): """ Calculates the inverse tangent of the given quantity and propagates the uncertainty. The quantity has to be dimensionless. """ return _generic_function(quantity, _atan_value_handler, _atan_d_dx_handler, _no_unit_handler)
################################################################################ # Inverse hyperbolic sine
[docs]def asinh(quantity): """ Calculates the inverse hyperbolic sine of the given quantity and propagates the uncertainty. The quantity has to be dimensionless. """ return log(quantity + sqrt(quantity**2 + 1))
[docs]def acosh(quantity): """ Calculates the inverse hyperbolic cosine of the given quantity and propagates the uncertainty. The quantity has to be dimensionless. """ return log(quantity + sqrt(quantity**2 - 1))
[docs]def atanh(quantity): """ Calculates the inverse hyperbolic tangent of the given quantity and propagates the uncertainty. The quantity has to be dimensionless. """ return 0.5 * log((1 + quantity) / (1 - quantity))
################################################################################ # Absolute value # Backup builtin absolute value function simple_abs = abs def _abs_value_handler(a): """ Calculates value of expoential function. """ return simple_abs(a) def _abs_d_dx_handler(a, da_dx): """ Calculates the derivative d/dx exp(a(x)). """ if a == 0: raise UncertaintyIllDefined("Argument of abs() cannot be zero.") elif a < 0: return -da_dx else: return da_dx def _abs_unit_handler(unit_vector, a, unit_system): """ Check that the unit vector is dimensionless and return a zero unit vector. """ return unit_vector def abs(quantity): """ Calculates the absolute value of the given quantity and propagates the uncertainty. The quantity has to be dimensionless. """ return _generic_function(quantity, _abs_value_handler, _abs_d_dx_handler, _abs_unit_handler) class QuantityArray(Quantity): """ QuantityArrays are similar to regular Quantities, however they can store numpy arrays as values and/or error. """ def from_list(self): """ Builds a QuantityArray based on a list of Quantity objects. This method makes QuantityBuffers obsolete. """ pass # Stores the default unit system used by Quantity if the unit system argument # is omitted. The variable is returned by get_default_unit_system(). This can # be overwritten after importing the package. default_unit_system = None def get_default_unit_system(): """ Returns the variable default_unit_system. If the variable is None, it is initialized with the SI unit system. """ global default_unit_system if default_unit_system is None: import pyveu.si default_unit_system = si.unit_system return default_unit_system ################################################################################ # Exceptions
[docs]class PyVeuException(Exception): """ Base class from which all package-specific exceptions will be derived. """ pass
[docs]class DifferentUnitSystem(PyVeuException): """ This exception will be raises if an arithmetic operation is requested between two objects tied to different unit systems. """ def __init__(self): super(DifferentUnitSystem, self).__init__( "Operations with two objects assigned to different unit systems" " are not permitted.")
[docs]class BaseUnitExists(PyVeuException): """ This exception will be raises if one tries to overwrite a base unit. """ def __init__(self): super(BaseUnitExists, self).__init__( "Base unit with this index exists. Base units can not be " "overwritten.")
[docs]class SymbolCollision(PyVeuException): """ This exception is raised when one tried to add a unit/prefix which causes ambiguities in the unit system. """ pass
[docs]class UnitNotFound(PyVeuException): """ This exception is raised when parse_unit() is not able to find a unit (and prefix) to match the given expression. """ pass
[docs]class UncertaintyIllDefined(PyVeuException): """ Raises when the error propagation cannot be performed for the given values. """ pass