initial work is basically finished

This commit is contained in:
Daniel Knüttel 2019-01-28 17:59:23 +01:00
commit 46a30708e1
7 changed files with 593 additions and 0 deletions

7
test.py Normal file
View File

@ -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)

36
units/SI.py Normal file
View File

@ -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)])

0
units/__init__.py Normal file
View File

24
units/backend/__init__.py Normal file
View File

@ -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

View File

@ -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

6
units/backend/errors.py Normal file
View File

@ -0,0 +1,6 @@
class DimensionalError(Exception):
pass
class RescalingError(Exception):
pass
class UnitError(Exception):
pass

290
units/backend/units.py Normal file
View File

@ -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()