commit 46a30708e1be726087fa7e4a043733853d37ec89 Author: Daniel Knüttel Date: Mon Jan 28 17:59:23 2019 +0100 initial work is basically finished diff --git a/test.py b/test.py new file mode 100644 index 0000000..1db61fa --- /dev/null +++ b/test.py @@ -0,0 +1,7 @@ +# coding: utf-8 +from units.units import BaseUnit +m = BaseUnit("m", "meter", "length", True) +s = BaseUnit("s", "second", "time", True) +print(m * s) +print(m / s) +print((m / s)**-1) diff --git a/units/SI.py b/units/SI.py new file mode 100644 index 0000000..6ffa54c --- /dev/null +++ b/units/SI.py @@ -0,0 +1,36 @@ +from .backend import base_unit, compound_unit +from .backend.dimensional_values import Dimension, Scaling + +A = base_unit("A", "Ampere", "Current", True) +m = base_unit("m", "meter", "Length", True) +K = base_unit("K", "Kelvin", "Temperature", True) +s = base_unit("s", "second", "Time", True) +kg = base_unit("kg", "kilo grams", "Mass", True) +cd = base_unit("cd", "candela", "Luminous intensity", True) +mol = base_unit("mol", "mole", "Amount of substance", True) + +cm = Dimension(Scaling(10, -2), m) +km = Dimension(Scaling(10, 3), m) + +g = Dimension(Scaling(10, -3), kg) + + +Hz = compound_unit("Hz", "Hertz", "Frequency", [(s, -1)]) +N = compound_unit("N", "Newton", "Force", [(m, 1), (kg, 1), (s, -2)]) +Pa = compound_unit("Pa", "Pascal", "Pressure", [(N, 1), (m, -2)]) +J = compound_unit("J", "Joule", "Energy", [(kg, 1), (m, 2), (s, -2)]) +W = compound_unit("W", "Watt", "Power", [(J, 1), (s, -1)]) +C = compound_unit("C", "Coulomb", "Electric charge", [(A, 1), (s, 1)]) +V = compound_unit("V", "Volt", "Voltage", [(W, 1), (A, -1)]) +F = compound_unit("F", "Farad", "Capacity", [(C, 1), (V, -1)]) +ohm = compound_unit("omega", "Ohm", "Resistance is futile", [(V, 1), (A, -1)]) +omega = ohm +S = compound_unit("S", "Siemens", "Conductance", [(ohm, -1)]) +Wb = compound_unit("Wb", "Weber", "Magnetic flux", [(V, 1), (s, 1)]) +T = compound_unit("T", "Tesla", "Magnetic flux density", [(Wb, 1), (m, -2)]) +H = compound_unit("H", "Henry", "Inductance", [(Wb, 1), (A, -1)]) +lx = compound_unit("lx", "Lux", "Illuminance", [(cd, 1), (m, -2)]) +Bq = compound_unit("Bq", "Becquerel", "Radioactivity", [(s, -1)]) +Gy = compound_unit("Gy", "Gray", "Absorbed dose (of ionizing radiation)", [(J, 1), (kg, -1)]) +Sv = compound_unit("Sv", "Sievert", "Equivalent dose (of ionizing radiation)", [(J, 1), (kg, -1)]) +kat = compound_unit("kat", "Katal", "Catalytic activity", [(mol, 1), (s, -1)]) diff --git a/units/__init__.py b/units/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/units/backend/__init__.py b/units/backend/__init__.py new file mode 100644 index 0000000..05d3e42 --- /dev/null +++ b/units/backend/__init__.py @@ -0,0 +1,24 @@ + +""" +backend package for units +""" + +from .units import BaseUnit, NamedCompoundUnit + +REGISTRY = dict() + +def base_unit(symbol, name, description, is_SI): + if(symbol in REGISTRY): + return REGISTRY[symbol] + unit = BaseUnit(symbol, name, description, is_SI) + REGISTRY[symbol] = unit + return unit + +def compound_unit(symbol, name, description, bases): + if(symbol in REGISTRY): + return REGISTRY[symbol] + unit = NamedCompoundUnit(symbol, name, description, bases) + REGISTRY[symbol] = unit + return unit + + diff --git a/units/backend/dimensional_values.py b/units/backend/dimensional_values.py new file mode 100644 index 0000000..49d45b2 --- /dev/null +++ b/units/backend/dimensional_values.py @@ -0,0 +1,230 @@ +""" +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 + + + diff --git a/units/backend/errors.py b/units/backend/errors.py new file mode 100644 index 0000000..e604fe8 --- /dev/null +++ b/units/backend/errors.py @@ -0,0 +1,6 @@ +class DimensionalError(Exception): + pass +class RescalingError(Exception): + pass +class UnitError(Exception): + pass diff --git a/units/backend/units.py b/units/backend/units.py new file mode 100644 index 0000000..08b00de --- /dev/null +++ b/units/backend/units.py @@ -0,0 +1,290 @@ +from collections import defaultdict +from .dimensional_values import Dimension, Scaling, DimensionalValue +from .errors import UnitError + +class BaseUnit(object): + """ + The atomic unit type that cannot be expressed by other units + (well of course they can be expressed, but they should not) + + #FIXME: + Should I compare symbols here? + """ + __slots__ = "symbol", "name", "description", "is_SI" + def __init__(self, symbol, name, description, is_SI): + self.symbol = symbol + self.name = name + self.description = description + self.is_SI = is_SI + + def __mul__(self, other): + if(isinstance(other, CompoundUnit)): + return other.__rmul__(self) + if(isinstance(other, OutputUnit)): + return other.to_compound().__rmul__(self) + if(not isinstance(other, BaseUnit)): + raise TypeError("Cannot __mul__ BaseUnit and {}".format(type(other))) + return CompoundUnit([(self, 1), (other, 1)]) + def __truediv__(self, other): + if(isinstance(other, CompoundUnit)): + return other.__rtruediv__(self) + if(isinstance(other, OutputUnit)): + return other.to_compound().__rtruediv__(self) + if(not isinstance(other, BaseUnit)): + raise TypeError("Cannot __mul__ BaseUnit and {}".format(type(other))) + return CompoundUnit([(self, 1), (other, -1)]) + + def __pow__(self, other): + if(not isinstance(other, (int, float, complex))): + raise TypeError("Cannot raise BaseUnit to non-scalar") + return CompoundUnit([(self, other)]) + + def __repr__(self): + return self.symbol + def __str__(self): + return self.symbol + + def __matmul__(self, other): + return other @ Dimension(Scaling(1, 1), self) + def __rmatmul__(self, other): + return other @ Dimension(Scaling(1, 1), self) + + def __rrshift__(self, other): + if(isinstance(other, DimensionalValue)): + return other >> Dimension(Scaling(1, 1), self) + if(isinstance(other, BaseUnit)): + if(other == self): + return self + raise UnitError("Cannot shift {} to {}".format(other, self)) + if(isinstance(other, CompoundUnit)): + if(self in [base for base, exponent in other.bases]): + return OutputUnit([other / self, self]) + raise UnitError("Cannot shift {} to {}".format(other, self)) + + if(isinstance(other, OutputUnit)): + try_here = other.components[0] + rest = other.components[1:] + return OutputUnit((try_here >> self).components.extend(rest)) + raise TypeError("Cannot rightshift BaseUnit and {}".format(type(other))) + + def __rshift__(self, other): + if(isinstance(other, (CompoundUnit, BaseUnit))): + return other.__rrshift__(self) + if(isinstance(other, OutputUnit)): + return other.to_compound().__rrshift__(self) + raise TypeError("Cannot rightshift BaseUnit and {}".format(type(other))) + + def is_scalar(self): + return False + + + + + + +class CompoundUnit(object): + """ + A unit that can (and should) be expressed using other units. + This unit can be used as-is and can be broke down to BaseUnits. + + Also it should be possible to automatically gather BaseUnits and + create CompoundUnits while multiplying. + + ``bases`` is a list of tuples ``[(BaseUnit, exponent)]`` where + exponent is an integer + """ + __slots__ = "is_SI", "bases" + def __check_and_split_bases(bases): + for base, exponent in bases: + if(isinstance(exponent, float) and exponent.is_integer()): + exponent = int(exponent) + if(not isinstance(exponent, int)): + raise ValueError("Unit Exponent must be integer") + if(isinstance(base, BaseUnit)): + yield base, exponent + continue + if(isinstance(base, CompoundUnit)): + yield from CompoundUnit.__check_and_split_bases(base.bases) + continue + raise TypeError("Unit bases must be BaseUnit or CompoundUnit") + + def __cleanup_bases(bases): + dct = defaultdict(int) + for base, exponent in bases: + dct[base] += exponent + for k,v in dct.items(): + # do not yield powers of 0 + if(v): + yield (k, v) + def __init__(self, bases): + self.bases = list(CompoundUnit.__cleanup_bases(CompoundUnit.__check_and_split_bases(bases))) + self.is_SI = all([base.is_SI for base,_ in self.bases]) + + def split(self): + return self.bases + + def __mul__(self, other): + if(isinstance(other, BaseUnit)): + return CompoundUnit(self.bases + [(other, 1)]) + if(isinstance(other, CompoundUnit)): + return CompoundUnit(self.bases + other.bases) + raise TypeError("Cannot __mul__ CompoundUnit and {}".format(type(other))) + def __rmul__(self, other): + if(isinstance(other, BaseUnit)): + return CompoundUnit(self.bases + [(other, 1)]) + if(isinstance(other, CompoundUnit)): + return CompoundUnit(self.bases + other.bases) + raise TypeError("Cannot __rmul__ CompoundUnit and {}".format(type(other))) + def __pow__(self, other): + if(not isinstance(other, (float, int, complex))): + raise TypeError("Cannot raise CompoundUnit to non-scalar") + return CompoundUnit([(base, other * exponent) for base, exponent in self.bases]) + + def __truediv__(self, other): + if(isinstance(other, BaseUnit)): + return CompoundUnit(self.bases + [(other, -1)]) + if(isinstance(other, CompoundUnit)): + return CompoundUnit(self.bases + [(base, -exponent) for base, exponent in other.bases]) + + raise TypeError("Cannot __truediv__ CompoundUnit and {}".format(type(other))) + + def __rtruediv__(self, other): + if(isinstance(other, BaseUnit)): + return CompoundUnit([(other, 1)] + [(base, -exponent) for base, exponent in self.bases]) + if(isinstance(other, CompoundUnit)): + return CompoundUnit(other.bases + [(base, -exponent) for base, exponent in self.bases]) + raise TypeError("Cannot __rtruediv__ CompoundUnit and {}".format(type(other))) + + def __repr__(self): + return "*".join(["{}**{}".format(base, exponent) for base, exponent in self.bases]) + + def __matmul__(self, other): + return other @ Dimension(Scaling(1, 1), self) + def __rmatmul__(self, other): + return other @ Dimension(Scaling(1, 1), self) + + def __eq__(self, other): + if(isinstance(other, BaseUnit)): + return self.bases == [(other, 1)] + if(isinstance(other, CompoundUnit)): + return self.bases == other.bases + raise TypeError("Cannot compare CompoundUnit and {}".format(type(other))) + def ungroup(self): + """ + This is just used to implement the rightshift stuff. + """ + return CompoundUnit(self.bases) + + def __rrshift__(self, other): + if(isinstance(other, DimensionalValue)): + return other >> Dimension(Scaling(1, 1), self) + + if(isinstance(other, BaseUnit)): + chk = [base for base, exponent in other.bases] + if(other in chk): + return self.ungroup() + raise UnitError("Cannot shift {} to {}".format(other, self)) + if(isinstance(other, CompoundUnit)): + chk = [base for base, exponent in other.bases] + for base, exponent in self.bases: + if(not base in chk): + raise UnitError("Cannot shift {} to {}".format(other, self)) + + return OutputUnit([(other.ungroup() / self.ungroup()), self]) + if(isinstance(other, OutputUnit)): + try_here = other.components[0] + rest = other.components[1:] + return OutputUnit((try_here >> self).components.extend(rest)) + + raise TypeError("Cannot rightshift BaseUnit and {}".format(type(other))) + + def __rshift__(self, other): + if(isinstance(other, (CompoundUnit, BaseUnit))): + return other.__rrshift__(self) + if(isinstance(other, OutputUnit)): + return other.to_compound().__rrshift__(self) + raise TypeError("Cannot rightshift CompoundUnit and {}".format(type(other))) + + def is_scalar(self): + return len(self.bases) == 0 + + + +class NamedCompoundUnit(CompoundUnit): + """ + A unit that can (and should) be expressed using other units. + This unit can be used as-is and can be broke down to BaseUnits. + + Also it should be possible to automatically gather BaseUnits and + create CompoundUnits while multiplying. + + ``bases`` is a list of tuples ``[(BaseUnit, exponent)]`` where + exponent is an integer + """ + __slots__ = "symbol", "name", "description", "is_SI", "bases" + def __init__(self, symbol, name, description, bases): + CompoundUnit.__init__(self, bases) + self.symbol = symbol + self.name = name + self.description = description + + def __repr__(self): + return self.symbol + def __str__(self): + return self.symbol + + + +class OutputUnit(object): + """ + Unit type for pretty output. + + Will be created by __rrshift__ and others. + + It has several compound units that are listed explicitly + for prettier output, if any operation is performed it will automatically + cast to a CompoundUnit. + """ + __slots__ = "components", + def __init__(self, components): + self.components = components + + def to_compound(self): + bases = [] + for compound in self.components: + if(isinstance(compound, BaseUnit)): + bases.append((compound, 1)) + if(isinstance(compound, CompoundUnit)): + bases.extend(compound.bases) + + return CompoundUnit(bases) + + def __repr__(self): + return "*".join([repr(c) for c in self.components if c]) + + def __mul__(self, other): + return self.to_compound().__mul__(other) + def __rmul__(self, other): + return self.to_compound().__rmul__(other) + def __truediv__(self, other): + return self.to_compound().__truediv__(other) + def __rtruediv__(self, other): + return self.to_compound().__rtruediv__(other) + def __pow__(self, other): + return self.to_compound().__pow__(other) + def __matmul__(self, other): + return self.to_compound().__matmul__(other) + def __rmatmul__(self, other): + return self.to_compound().__rmatmul__(other) + def __eq__(self, other): + return self.to_compound().__eq__(other) + + def split(self): + return self.to_compound().split() + def ungroup(self): + return self.to_compound().ungroup() + def __rrshift__(self, other): + return self.to_compound().__rrshift__(other) + + def is_scalar(self): + return self.to_compound().is_scalar()