unitsystem.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. """
  2. Unit system for physical quantities; include definition of constants.
  3. """
  4. from typing import Dict as tDict, Set as tSet
  5. from sympy.core.add import Add
  6. from sympy.core.function import (Derivative, Function)
  7. from sympy.core.mul import Mul
  8. from sympy.core.power import Pow
  9. from sympy.core.singleton import S
  10. from sympy.physics.units.dimensions import _QuantityMapper
  11. from sympy.physics.units.quantities import Quantity
  12. from .dimensions import Dimension
  13. class UnitSystem(_QuantityMapper):
  14. """
  15. UnitSystem represents a coherent set of units.
  16. A unit system is basically a dimension system with notions of scales. Many
  17. of the methods are defined in the same way.
  18. It is much better if all base units have a symbol.
  19. """
  20. _unit_systems = {} # type: tDict[str, UnitSystem]
  21. def __init__(self, base_units, units=(), name="", descr="", dimension_system=None, derived_units: tDict[Dimension, Quantity]={}):
  22. UnitSystem._unit_systems[name] = self
  23. self.name = name
  24. self.descr = descr
  25. self._base_units = base_units
  26. self._dimension_system = dimension_system
  27. self._units = tuple(set(base_units) | set(units))
  28. self._base_units = tuple(base_units)
  29. self._derived_units = derived_units
  30. super().__init__()
  31. def __str__(self):
  32. """
  33. Return the name of the system.
  34. If it does not exist, then it makes a list of symbols (or names) of
  35. the base dimensions.
  36. """
  37. if self.name != "":
  38. return self.name
  39. else:
  40. return "UnitSystem((%s))" % ", ".join(
  41. str(d) for d in self._base_units)
  42. def __repr__(self):
  43. return '<UnitSystem: %s>' % repr(self._base_units)
  44. def extend(self, base, units=(), name="", description="", dimension_system=None, derived_units: tDict[Dimension, Quantity]={}):
  45. """Extend the current system into a new one.
  46. Take the base and normal units of the current system to merge
  47. them to the base and normal units given in argument.
  48. If not provided, name and description are overridden by empty strings.
  49. """
  50. base = self._base_units + tuple(base)
  51. units = self._units + tuple(units)
  52. return UnitSystem(base, units, name, description, dimension_system, {**self._derived_units, **derived_units})
  53. def get_dimension_system(self):
  54. return self._dimension_system
  55. def get_quantity_dimension(self, unit):
  56. qdm = self.get_dimension_system()._quantity_dimension_map
  57. if unit in qdm:
  58. return qdm[unit]
  59. return super().get_quantity_dimension(unit)
  60. def get_quantity_scale_factor(self, unit):
  61. qsfm = self.get_dimension_system()._quantity_scale_factors
  62. if unit in qsfm:
  63. return qsfm[unit]
  64. return super().get_quantity_scale_factor(unit)
  65. @staticmethod
  66. def get_unit_system(unit_system):
  67. if isinstance(unit_system, UnitSystem):
  68. return unit_system
  69. if unit_system not in UnitSystem._unit_systems:
  70. raise ValueError(
  71. "Unit system is not supported. Currently"
  72. "supported unit systems are {}".format(
  73. ", ".join(sorted(UnitSystem._unit_systems))
  74. )
  75. )
  76. return UnitSystem._unit_systems[unit_system]
  77. @staticmethod
  78. def get_default_unit_system():
  79. return UnitSystem._unit_systems["SI"]
  80. @property
  81. def dim(self):
  82. """
  83. Give the dimension of the system.
  84. That is return the number of units forming the basis.
  85. """
  86. return len(self._base_units)
  87. @property
  88. def is_consistent(self):
  89. """
  90. Check if the underlying dimension system is consistent.
  91. """
  92. # test is performed in DimensionSystem
  93. return self.get_dimension_system().is_consistent
  94. @property
  95. def derived_units(self) -> tDict[Dimension, Quantity]:
  96. return self._derived_units
  97. def get_dimensional_expr(self, expr):
  98. from sympy.physics.units import Quantity
  99. if isinstance(expr, Mul):
  100. return Mul(*[self.get_dimensional_expr(i) for i in expr.args])
  101. elif isinstance(expr, Pow):
  102. return self.get_dimensional_expr(expr.base) ** expr.exp
  103. elif isinstance(expr, Add):
  104. return self.get_dimensional_expr(expr.args[0])
  105. elif isinstance(expr, Derivative):
  106. dim = self.get_dimensional_expr(expr.expr)
  107. for independent, count in expr.variable_count:
  108. dim /= self.get_dimensional_expr(independent)**count
  109. return dim
  110. elif isinstance(expr, Function):
  111. args = [self.get_dimensional_expr(arg) for arg in expr.args]
  112. if all(i == 1 for i in args):
  113. return S.One
  114. return expr.func(*args)
  115. elif isinstance(expr, Quantity):
  116. return self.get_quantity_dimension(expr).name
  117. return S.One
  118. def _collect_factor_and_dimension(self, expr):
  119. """
  120. Return tuple with scale factor expression and dimension expression.
  121. """
  122. from sympy.physics.units import Quantity
  123. if isinstance(expr, Quantity):
  124. return expr.scale_factor, expr.dimension
  125. elif isinstance(expr, Mul):
  126. factor = 1
  127. dimension = Dimension(1)
  128. for arg in expr.args:
  129. arg_factor, arg_dim = self._collect_factor_and_dimension(arg)
  130. factor *= arg_factor
  131. dimension *= arg_dim
  132. return factor, dimension
  133. elif isinstance(expr, Pow):
  134. factor, dim = self._collect_factor_and_dimension(expr.base)
  135. exp_factor, exp_dim = self._collect_factor_and_dimension(expr.exp)
  136. if self.get_dimension_system().is_dimensionless(exp_dim):
  137. exp_dim = 1
  138. return factor ** exp_factor, dim ** (exp_factor * exp_dim)
  139. elif isinstance(expr, Add):
  140. factor, dim = self._collect_factor_and_dimension(expr.args[0])
  141. for addend in expr.args[1:]:
  142. addend_factor, addend_dim = \
  143. self._collect_factor_and_dimension(addend)
  144. if not self.get_dimension_system().equivalent_dims(dim, addend_dim):
  145. raise ValueError(
  146. 'Dimension of "{}" is {}, '
  147. 'but it should be {}'.format(
  148. addend, addend_dim, dim))
  149. factor += addend_factor
  150. return factor, dim
  151. elif isinstance(expr, Derivative):
  152. factor, dim = self._collect_factor_and_dimension(expr.args[0])
  153. for independent, count in expr.variable_count:
  154. ifactor, idim = self._collect_factor_and_dimension(independent)
  155. factor /= ifactor**count
  156. dim /= idim**count
  157. return factor, dim
  158. elif isinstance(expr, Function):
  159. fds = [self._collect_factor_and_dimension(arg) for arg in expr.args]
  160. dims = [Dimension(1) if self.get_dimension_system().is_dimensionless(d[1]) else d[1] for d in fds]
  161. return (expr.func(*(f[0] for f in fds)), *dims)
  162. elif isinstance(expr, Dimension):
  163. return S.One, expr
  164. else:
  165. return expr, Dimension(1)
  166. def get_units_non_prefixed(self) -> tSet[Quantity]:
  167. """
  168. Return the units of the system that do not have a prefix.
  169. """
  170. return set(filter(lambda u: not u.is_prefixed and not u.is_physical_constant, self._units))