|
""" |
|
Unit system for physical quantities; include definition of constants. |
|
""" |
|
from __future__ import annotations |
|
|
|
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: dict[str, UnitSystem] = {} |
|
|
|
def __init__(self, base_units, units=(), name="", descr="", dimension_system=None, derived_units: dict[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 '<UnitSystem: %s>' % repr(self._base_units) |
|
|
|
def extend(self, base, units=(), name="", description="", dimension_system=None, derived_units: dict[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. |
|
""" |
|
|
|
return self.get_dimension_system().is_consistent |
|
|
|
@property |
|
def derived_units(self) -> dict[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) -> set[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)) |
|
|