pyunits/units/backend/dimensional_values.py

231 lines
7.9 KiB
Python

"""
Dimensional Values
==================
Dimensional values are values of a python type ``int`` or ``float`` (or ``complex``).
The dimension is always of type ``units.dimensional_values.Dimension``.
The dimension can be assigned using the ``@`` operator.
"""
from .errors import *
class DimensionalValue(object):
__slots__ = "value", "dimension"
def __init__(self, value, dimension):
self.value = value
self.dimension = dimension
@staticmethod
def make(value, dimension):
if(dimension.is_scalar()):
return value * dimension.scale.to_scalar()
return DimensionalValue(value, dimension)
def _typecheck_add(self, other):
if(isinstance(other, (float, int, complex))):
raise DimensionalError("cannot add dimensional value {} to value without dimension {}".format(repr(self),
repr(other)))
if(isinstance(other, DimensionalValue)):
if(not self.dimension.can_rescale_to(other.dimension)):
raise DimensionalError("incompatible dimension: {} and {}".format(repr(self.dimension),
repr(other.dimension)))
else:
return
raise ValueError("cannot add DimensionalValue and {}".format(type(other)))
def _typecheck_mul(self, other):
if(not isinstance(other, (float, int, complex, DimensionalValue))):
raise ValueError("cannot mul DimensionalValue and {}".format(type(other)))
def __add__(self, other):
self._typecheck_add(other)
return DimensionalValue.make(self.value + other.value, self.dimension)
def __mul__(self, other):
self._typecheck_mul(other)
if(isinstance(other, DimensionalValue)):
value, dimension = self.dimension.auto_rescale_and_mul(self.value, other.value, other.dimension)
return DimensionalValue.make(value, dimension)
return DimensionalValue.make(self.value * other, self.dimension)
def __pow__(self, other):
if(not isinstance(other, (float, int, complex))):
raise ValueError("cannot raise DimensionalValue to non-scalar power")
if(isinstance(other, int)):
# scalings can be raisen to integers
return DimensionalValue.make(self.value ** other, self.dimension ** other)
if(isinstance(other, (float, complex))):
# scalings cannot be raisen to non-integers
value, dimension = self.dimension.rescale_to_base(self.value)
return DimensionalValue.make(value ** other, dimension ** other)
def __radd__(self, other):
self._typecheck_add(other)
return DimensionalValue.make(other.value + self.value , self.dimension)
def __rmul__(self, other):
self._typecheck_mul(other)
if(isinstance(other, DimensionalValue)):
value, dimension = self.dimension.auto_rescale_and_mul(self.value, other.value, other.dimension)
return DimensionalValue.make(value, dimension)
return DimensionalValue.make(self.value * other, self.dimension)
def __truediv__(self, other):
self._typecheck_mul(other)
if(isinstance(other, (float, int, complex))):
return DimensionalValue.make(self.value / other, self.dimension)
value, dimension = self.dimension.auto_rescale_and_mul(self.value, 1 / other.value, other.dimension ** -1)
return DimensionalValue.make(value, dimension)
def __rtruediv__(self, other):
self._typecheck_mul(other)
if(isinstance(other, (float, int, complex))):
return DimensionalValue.make(other / self.value, self.dimension ** -1)
value, dimension = other.dimension.auto_rescale_and_mul(other.value, 1 / self.value, self.dimension ** -1)
return DimensionalValue.make(value, dimension)
def __sub__(self, other):
self._typecheck_add(other)
return DimensionalValue.make(self.value - other.value, self.dimension)
def __sub__(self, other):
self._typecheck_add(other)
return DimensionalValue.make(other.value - self.value , self.dimension)
def __repr__(self):
return "{} * {}".format(repr(self.value), repr(self.dimension))
class Dimension(object):
__slots__ = "scale", "unit"
"""
class for representing dimensions
"""
def __init__(self, scale, unit):
self.scale = scale
self.unit = unit
def can_rescale_to(self, other):
"""
check if the dimensions are compatible
"""
if(not self.unit == other.unit):
return False
return True
def rescale(self, other):
"""
Rescale the ``DimensionalValue`` ``other`` to this
dimension.
"""
rescale = other.dimension.scale / self.scale
return DimensionalValue.make(other.value * rescale, other.dimension)
def auto_rescale_and_mul(self, v1, v2, other):
dummy = Dimension(Scaling(1, 1), self.unit)
v2 = dummy.rescale(DimensionalValue.make(v2, other)).value
v1 = dummy.rescale(DimensionalValue.make(v1, self)).value
return v1 * v2, Dimension(Scaling(1, 1), self.unit * other.unit)
def __mul__(self, other):
if(not isinstance(other, Dimension)):
raise TypeError("Cannot __mul__ Dimension and {}".format(type(other)))
return Dimension(self.scale * other.scale, self.unit * other.unit)
def __matmul__(self, other):
# FIXME:
# maybe turn this off.
# I don't like it :-(
if(isinstance(other, DimensionalValue)):
if(not self.can_rescale_to(other.dimension)):
raise DimensionalError("Cannot rescale {} to {}".format(repr(other), repr(self)))
return self.rescale(other).value
if(isinstance(other, (float, int, complex))):
return DimensionalValue.make(other, self)
raise ValueError("Cannot __matmul__ Dimensional and {}".format(type(other)))
def __rmatmul__(self, other):
if(isinstance(other, DimensionalValue)):
if(not self.can_rescale_to(other.dimension)):
raise DimensionalError("Cannot rescale {} to {}".format(repr(other), repr(self)))
return self.rescale(other).value
if(isinstance(other, (float, int, complex))):
return DimensionalValue.make(other, self)
raise ValueError("Cannot __matmul__ Dimension and {}".format(type(other)))
def __pow__(self, other):
if(not isinstance(other, (float, int, complex))):
raise ValueError("cannot raise Dimensional to non-scalar power")
return Dimension(self.scale ** other, self.unit ** other)
def __repr__(self):
return "{}@({})".format(repr(self.scale), repr(self.unit))
def __rrshift__(self, other):
print(__file__, ":", type(self), ":", "__rrshift__({}, {})".format(self, other))
if(not isinstance(other, DimensionalValue)):
raise TypeError("cannot shift non-DimensionalValue to Dimension")
other = self.rescale(other)
print(other.dimension.unit, self.unit)
unit = other.dimension.unit >> self.unit
return DimensionalValue.make(other.value, Dimension(other.dimension.scale, unit))
def is_scalar(self):
return self.unit.is_scalar()
# def __eq__(self, other):
# if(not isinstance(other, Dimension)):
# raise TypeError("cannot compare Dimension and {}".format(type(other)))
#
class Scaling(object):
__slots__ = "value", "exponent"
def __init__(self, value, exponent):
self.value = value
self.exponent = exponent
def __repr__(self):
if(self.value == 1 or self.exponent == 0):
return "1"
return "{}**{}".format(self.value, self.exponent)
def __pow__(self, other):
# this is actually kind a dirty:
# Scalings must not be raisen to non-integers,
# so just rescale the total dimensional value to the
# base dimension (scale = 1), so now we can ignore powers.
if(self.value == 1):
return Scaling(1, 1)
return Scaling(self.value , self.exponent * other)
def __truediv__(self, other):
"""
Return the scalar value that is required
to rescale the other dimension to this dimension.
"""
if(not isinstance(other, Scaling)):
raise TypeError("Cannot truedivide Scaling by {}".format(type(other)))
# Try to do this as elegant as possible:
# use as much integer math as possible.
if(other.value == self.value):
return self.value ** (self.exponent - other.exponent)
return self.value ** self.exponent / other.value ** other.exponent
def __mul__(self, other):
if(not isinstance(other, Scaling)):
raise TypeError("Cannot __mul__ Scaling and {}".format(type(other)))
if(self.value == other.value):
return Scaling(self.value, self.exponent + other.exponent)
return Scaling(self.value ** self.exponent + other.value ** other.exponent, 1)
def to_scalar(self):
return self.value ** self.exponent