""" Unit system for physical quantities; include definition of constants. """ from typing import Dict as tDict, Set as tSet from sympy.core.add import Add from sympy.core.function import (Derivative, Function) from sympy.core.mul import Mul from sympy.core.power import Pow from sympy.core.singleton import S from sympy.physics.units.dimensions import _QuantityMapper from sympy.physics.units.quantities import Quantity from .dimensions import Dimension class UnitSystem(_QuantityMapper): """ UnitSystem represents a coherent set of units. A unit system is basically a dimension system with notions of scales. Many of the methods are defined in the same way. It is much better if all base units have a symbol. """ _unit_systems = {} # type: tDict[str, UnitSystem] def __init__(self, base_units, units=(), name="", descr="", dimension_system=None, derived_units: tDict[Dimension, Quantity]={}): UnitSystem._unit_systems[name] = self self.name = name self.descr = descr self._base_units = base_units self._dimension_system = dimension_system self._units = tuple(set(base_units) | set(units)) self._base_units = tuple(base_units) self._derived_units = derived_units super().__init__() def __str__(self): """ Return the name of the system. If it does not exist, then it makes a list of symbols (or names) of the base dimensions. """ if self.name != "": return self.name else: return "UnitSystem((%s))" % ", ".join( str(d) for d in self._base_units) def __repr__(self): return '' % repr(self._base_units) def extend(self, base, units=(), name="", description="", dimension_system=None, derived_units: tDict[Dimension, Quantity]={}): """Extend the current system into a new one. Take the base and normal units of the current system to merge them to the base and normal units given in argument. If not provided, name and description are overridden by empty strings. """ base = self._base_units + tuple(base) units = self._units + tuple(units) return UnitSystem(base, units, name, description, dimension_system, {**self._derived_units, **derived_units}) def get_dimension_system(self): return self._dimension_system def get_quantity_dimension(self, unit): qdm = self.get_dimension_system()._quantity_dimension_map if unit in qdm: return qdm[unit] return super().get_quantity_dimension(unit) def get_quantity_scale_factor(self, unit): qsfm = self.get_dimension_system()._quantity_scale_factors if unit in qsfm: return qsfm[unit] return super().get_quantity_scale_factor(unit) @staticmethod def get_unit_system(unit_system): if isinstance(unit_system, UnitSystem): return unit_system if unit_system not in UnitSystem._unit_systems: raise ValueError( "Unit system is not supported. Currently" "supported unit systems are {}".format( ", ".join(sorted(UnitSystem._unit_systems)) ) ) return UnitSystem._unit_systems[unit_system] @staticmethod def get_default_unit_system(): return UnitSystem._unit_systems["SI"] @property def dim(self): """ Give the dimension of the system. That is return the number of units forming the basis. """ return len(self._base_units) @property def is_consistent(self): """ Check if the underlying dimension system is consistent. """ # test is performed in DimensionSystem return self.get_dimension_system().is_consistent @property def derived_units(self) -> tDict[Dimension, Quantity]: return self._derived_units def get_dimensional_expr(self, expr): from sympy.physics.units import Quantity if isinstance(expr, Mul): return Mul(*[self.get_dimensional_expr(i) for i in expr.args]) elif isinstance(expr, Pow): return self.get_dimensional_expr(expr.base) ** expr.exp elif isinstance(expr, Add): return self.get_dimensional_expr(expr.args[0]) elif isinstance(expr, Derivative): dim = self.get_dimensional_expr(expr.expr) for independent, count in expr.variable_count: dim /= self.get_dimensional_expr(independent)**count return dim elif isinstance(expr, Function): args = [self.get_dimensional_expr(arg) for arg in expr.args] if all(i == 1 for i in args): return S.One return expr.func(*args) elif isinstance(expr, Quantity): return self.get_quantity_dimension(expr).name return S.One def _collect_factor_and_dimension(self, expr): """ Return tuple with scale factor expression and dimension expression. """ from sympy.physics.units import Quantity if isinstance(expr, Quantity): return expr.scale_factor, expr.dimension elif isinstance(expr, Mul): factor = 1 dimension = Dimension(1) for arg in expr.args: arg_factor, arg_dim = self._collect_factor_and_dimension(arg) factor *= arg_factor dimension *= arg_dim return factor, dimension elif isinstance(expr, Pow): factor, dim = self._collect_factor_and_dimension(expr.base) exp_factor, exp_dim = self._collect_factor_and_dimension(expr.exp) if self.get_dimension_system().is_dimensionless(exp_dim): exp_dim = 1 return factor ** exp_factor, dim ** (exp_factor * exp_dim) elif isinstance(expr, Add): factor, dim = self._collect_factor_and_dimension(expr.args[0]) for addend in expr.args[1:]: addend_factor, addend_dim = \ self._collect_factor_and_dimension(addend) if not self.get_dimension_system().equivalent_dims(dim, addend_dim): raise ValueError( 'Dimension of "{}" is {}, ' 'but it should be {}'.format( addend, addend_dim, dim)) factor += addend_factor return factor, dim elif isinstance(expr, Derivative): factor, dim = self._collect_factor_and_dimension(expr.args[0]) for independent, count in expr.variable_count: ifactor, idim = self._collect_factor_and_dimension(independent) factor /= ifactor**count dim /= idim**count return factor, dim elif isinstance(expr, Function): fds = [self._collect_factor_and_dimension(arg) for arg in expr.args] dims = [Dimension(1) if self.get_dimension_system().is_dimensionless(d[1]) else d[1] for d in fds] return (expr.func(*(f[0] for f in fds)), *dims) elif isinstance(expr, Dimension): return S.One, expr else: return expr, Dimension(1) def get_units_non_prefixed(self) -> tSet[Quantity]: """ Return the units of the system that do not have a prefix. """ return set(filter(lambda u: not u.is_prefixed and not u.is_physical_constant, self._units))