Source code for fuzz.values

"""Contains the Value class."""

from math import sqrt

[docs]class Value: """A Value represents a numerical measurement of some kind, with its associated uncertainty/error. For example a value of 23 ± 0.2 would be ``Value(23, 0.2)``. Values mostly support the same operators that numbers do - you can add them, divide them, raise them to powers, compare them etc. There are a few important differences however. Firstly, the error values of the resultant operations will be derived from the standard guidelines for combining uncertainties. Specifically, values will be assumed to be independent, and so errors will be summed etc. in quadrature. Secondly, comparing two values with ``==``, ``<`` etc. will compare the values only - the error values will not be taken into account. I thought it would be too confusing otherwise. However, all values have a :py:meth:`.consistent_with` method which `will` look at the error values. If two values are consistent, then one should not be considered larger than the other, regardless of what ``>`` says. The intention behind the Value class was that if you wanted to, you could forget that it was anything other than an ``int`` or ``float``, and only access the error associated with it if you need it. You can create a Value `from` a value, and the argument will be treated exactly like an ``int`` or ``float``. That is, unless you also supply an error value, the resultant Value will have an error of 0. I considered having the error of the Value passed in become the new error, but decided it would become too easy to lose track of the errors. So, ``Value(Value(23, 0.2))`` would produce a Value with an error of 0 (and a value of 23). One final note on terminology - I know it is confusing that the Value class has a property called :py:meth:`value`, but that is the terminology in use as of this version. :param value: The value. :param error: The uncertainty associated with the value. By default this is\ zero. :raises TypeError: if either the value or its error is not numeric. :raises ValueError: if the error is negative.""" def __init__(self, value, error=0): if isinstance(value, Value): value = value._value if not isinstance(value, (int, float)) or isinstance(value, bool): raise TypeError("value {} is not an int or a float".format(value)) if not isinstance(error, (int, float)) or isinstance(error, bool): raise TypeError("error {} is not an int or a float".format(error)) if error < 0: raise ValueError("error {} is negative".format(error)) self._value = value self._error = error
[docs] @staticmethod def create(value, error=0): """This is a static method, and serves as an alternate constructor for Values. It tries to convert some value to an actual Value, and if it can't because it is the wrong type, it just sends the object back unaltered. :param value: The value to convert. :param error: The error associated with the value. :returns: Either the converted :py:class:`.Value` or the original\ object.""" try: return Value(value, error) except TypeError: return value
def __repr__(self): if self._error: return "{} ± {}".format(self._value, self._error) return str(self._value) def __add__(self, other): value = self._value + (other._value if isinstance(other, Value) else other) error = self._error ** 2 other_error = (other._error if isinstance(other, Value) else 0) ** 2 error = sqrt(error + other_error) return Value(value, error) def __radd__(self, other): return self + other def __sub__(self, other): value = self._value - (other._value if isinstance(other, Value) else other) error = self._error ** 2 other_error = (other._error if isinstance(other, Value) else 0) ** 2 error = sqrt(error + other_error) return Value(value, error) def __rsub__(self, other): value = (other._value if isinstance(other, Value) else other) - self._value error = self._error ** 2 other_error = (other._error if isinstance(other, Value) else 0) ** 2 error = sqrt(error + other_error) return Value(value, error) def __mul__(self, other): value = self._value other_value = (other._value if isinstance(other, Value) else other) value *= other_value error = self.relative_error() ** 2 other_error = other.relative_error() if isinstance(other, Value) else 0 other_error = other_error ** 2 error = sqrt(error + other_error) return Value(value, error * abs(value)) def __rmul__(self, other): return self * other def __truediv__(self, other): value = self._value other_value = (other._value if isinstance(other, Value) else other) value /= other_value error = self.relative_error() ** 2 other_error = other.relative_error() if isinstance(other, Value) else 0 other_error = other_error ** 2 error = sqrt(error + other_error) return Value(value, error * abs(value)) def __rtruediv__(self, other): value = (other._value if isinstance(other, Value) else other) / self._value error = self.relative_error() + ( other.relative_error() if isinstance(other, Value) else 0 ) return Value(value, error * abs(value)) def __pow__(self, other): value = self._value ** other error = self.relative_error() * abs(other) return Value(value, error * abs(value)) def __eq__(self, other): return self._value == (other._value if isinstance(other, Value) else other) def __gt__(self, other): return self._value > (other._value if isinstance(other, Value) else other) def __lt__(self, other): return self._value < (other._value if isinstance(other, Value) else other) def __ge__(self, other): return self._value >= (other._value if isinstance(other, Value) else other) def __le__(self, other): return self._value <= (other._value if isinstance(other, Value) else other)
[docs] def value(self): """Returns the value's... value. That is, the measurement itself, without its associated error. :rtype: ``int`` or ``float``""" return self._value
[docs] def error(self): """Returns the value's associated error. :rtype: ``int`` or ``float``""" return self._error
[docs] def relative_error(self): """Returns the value's associated error as a proportion of the value. If the value is 0, the relative error will be 0 too. :rtype: ``float``""" if not self._value: return 0 return self._error / abs(self._value)
[docs] def error_range(self): """Returns the range of possible values implied by the uncertainty. :rtype: ``tuple``""" return (self._value - self._error, self._value + self._error)
[docs] def consistent_with(self, other): """Checks if the value is `consistent` with another value. Two values are considered consistent if the difference between them is less than or equal to the sum of their uncertainties/errors. If two values are consistent, there cannot be said to be a difference between them, whereas if they are not consistent, there is a meaningful difference between them. You can also provide an ``int`` or ``float``, which will be assumed to have an error of zero. :param Value other: The other value to check against. :raises TypeError: if the other value given is not a ``Value``, ``int``\ or ``float``. :rtype: ``bool``""" if isinstance(other, (int, float)): return abs(self.value() - other) <= self.error() elif not isinstance(other, Value): raise TypeError( "Cannot get consistency with non-number {}".format(other) ) return abs(self.value() - other.value()) <= self.error() + other.error()