Coverage for /usr/lib/python3/dist-packages/sympy/physics/units/dimensions.py: 58%
288 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
1"""
2Definition of physical dimensions.
4Unit systems will be constructed on top of these dimensions.
6Most of the examples in the doc use MKS system and are presented from the
7computer point of view: from a human point, adding length to time is not legal
8in MKS but it is in natural system; for a computer in natural system there is
9no time dimension (but a velocity dimension instead) - in the basis - so the
10question of adding time to length has no meaning.
11"""
13from __future__ import annotations
15import collections
16from functools import reduce
18from sympy.core.basic import Basic
19from sympy.core.containers import (Dict, Tuple)
20from sympy.core.singleton import S
21from sympy.core.sorting import default_sort_key
22from sympy.core.symbol import Symbol
23from sympy.core.sympify import sympify
24from sympy.matrices.dense import Matrix
25from sympy.functions.elementary.trigonometric import TrigonometricFunction
26from sympy.core.expr import Expr
27from sympy.core.power import Pow
30class _QuantityMapper:
32 _quantity_scale_factors_global: dict[Expr, Expr] = {}
33 _quantity_dimensional_equivalence_map_global: dict[Expr, Expr] = {}
34 _quantity_dimension_global: dict[Expr, Expr] = {}
36 def __init__(self, *args, **kwargs):
37 self._quantity_dimension_map = {}
38 self._quantity_scale_factors = {}
40 def set_quantity_dimension(self, quantity, dimension):
41 """
42 Set the dimension for the quantity in a unit system.
44 If this relation is valid in every unit system, use
45 ``quantity.set_global_dimension(dimension)`` instead.
46 """
47 from sympy.physics.units import Quantity
48 dimension = sympify(dimension)
49 if not isinstance(dimension, Dimension):
50 if dimension == 1:
51 dimension = Dimension(1)
52 else:
53 raise ValueError("expected dimension or 1")
54 elif isinstance(dimension, Quantity):
55 dimension = self.get_quantity_dimension(dimension)
56 self._quantity_dimension_map[quantity] = dimension
58 def set_quantity_scale_factor(self, quantity, scale_factor):
59 """
60 Set the scale factor of a quantity relative to another quantity.
62 It should be used only once per quantity to just one other quantity,
63 the algorithm will then be able to compute the scale factors to all
64 other quantities.
66 In case the scale factor is valid in every unit system, please use
67 ``quantity.set_global_relative_scale_factor(scale_factor)`` instead.
68 """
69 from sympy.physics.units import Quantity
70 from sympy.physics.units.prefixes import Prefix
71 scale_factor = sympify(scale_factor)
72 # replace all prefixes by their ratio to canonical units:
73 scale_factor = scale_factor.replace(
74 lambda x: isinstance(x, Prefix),
75 lambda x: x.scale_factor
76 )
77 # replace all quantities by their ratio to canonical units:
78 scale_factor = scale_factor.replace(
79 lambda x: isinstance(x, Quantity),
80 lambda x: self.get_quantity_scale_factor(x)
81 )
82 self._quantity_scale_factors[quantity] = scale_factor
84 def get_quantity_dimension(self, unit):
85 from sympy.physics.units import Quantity
86 # First look-up the local dimension map, then the global one:
87 if unit in self._quantity_dimension_map:
88 return self._quantity_dimension_map[unit]
89 if unit in self._quantity_dimension_global:
90 return self._quantity_dimension_global[unit]
91 if unit in self._quantity_dimensional_equivalence_map_global:
92 dep_unit = self._quantity_dimensional_equivalence_map_global[unit]
93 if isinstance(dep_unit, Quantity):
94 return self.get_quantity_dimension(dep_unit)
95 else:
96 return Dimension(self.get_dimensional_expr(dep_unit))
97 if isinstance(unit, Quantity):
98 return Dimension(unit.name)
99 else:
100 return Dimension(1)
102 def get_quantity_scale_factor(self, unit):
103 if unit in self._quantity_scale_factors:
104 return self._quantity_scale_factors[unit]
105 if unit in self._quantity_scale_factors_global:
106 mul_factor, other_unit = self._quantity_scale_factors_global[unit]
107 return mul_factor*self.get_quantity_scale_factor(other_unit)
108 return S.One
111class Dimension(Expr):
112 """
113 This class represent the dimension of a physical quantities.
115 The ``Dimension`` constructor takes as parameters a name and an optional
116 symbol.
118 For example, in classical mechanics we know that time is different from
119 temperature and dimensions make this difference (but they do not provide
120 any measure of these quantites.
122 >>> from sympy.physics.units import Dimension
123 >>> length = Dimension('length')
124 >>> length
125 Dimension(length)
126 >>> time = Dimension('time')
127 >>> time
128 Dimension(time)
130 Dimensions can be composed using multiplication, division and
131 exponentiation (by a number) to give new dimensions. Addition and
132 subtraction is defined only when the two objects are the same dimension.
134 >>> velocity = length / time
135 >>> velocity
136 Dimension(length/time)
138 It is possible to use a dimension system object to get the dimensionsal
139 dependencies of a dimension, for example the dimension system used by the
140 SI units convention can be used:
142 >>> from sympy.physics.units.systems.si import dimsys_SI
143 >>> dimsys_SI.get_dimensional_dependencies(velocity)
144 {Dimension(length, L): 1, Dimension(time, T): -1}
145 >>> length + length
146 Dimension(length)
147 >>> l2 = length**2
148 >>> l2
149 Dimension(length**2)
150 >>> dimsys_SI.get_dimensional_dependencies(l2)
151 {Dimension(length, L): 2}
153 """
155 _op_priority = 13.0
157 # XXX: This doesn't seem to be used anywhere...
158 _dimensional_dependencies = {} # type: ignore
160 is_commutative = True
161 is_number = False
162 # make sqrt(M**2) --> M
163 is_positive = True
164 is_real = True
166 def __new__(cls, name, symbol=None):
168 if isinstance(name, str):
169 name = Symbol(name)
170 else:
171 name = sympify(name)
173 if not isinstance(name, Expr):
174 raise TypeError("Dimension name needs to be a valid math expression")
176 if isinstance(symbol, str):
177 symbol = Symbol(symbol)
178 elif symbol is not None:
179 assert isinstance(symbol, Symbol)
181 obj = Expr.__new__(cls, name)
183 obj._name = name
184 obj._symbol = symbol
185 return obj
187 @property
188 def name(self):
189 return self._name
191 @property
192 def symbol(self):
193 return self._symbol
195 def __str__(self):
196 """
197 Display the string representation of the dimension.
198 """
199 if self.symbol is None:
200 return "Dimension(%s)" % (self.name)
201 else:
202 return "Dimension(%s, %s)" % (self.name, self.symbol)
204 def __repr__(self):
205 return self.__str__()
207 def __neg__(self):
208 return self
210 def __add__(self, other):
211 from sympy.physics.units.quantities import Quantity
212 other = sympify(other)
213 if isinstance(other, Basic):
214 if other.has(Quantity):
215 raise TypeError("cannot sum dimension and quantity")
216 if isinstance(other, Dimension) and self == other:
217 return self
218 return super().__add__(other)
219 return self
221 def __radd__(self, other):
222 return self.__add__(other)
224 def __sub__(self, other):
225 # there is no notion of ordering (or magnitude) among dimension,
226 # subtraction is equivalent to addition when the operation is legal
227 return self + other
229 def __rsub__(self, other):
230 # there is no notion of ordering (or magnitude) among dimension,
231 # subtraction is equivalent to addition when the operation is legal
232 return self + other
234 def __pow__(self, other):
235 return self._eval_power(other)
237 def _eval_power(self, other):
238 other = sympify(other)
239 return Dimension(self.name**other)
241 def __mul__(self, other):
242 from sympy.physics.units.quantities import Quantity
243 if isinstance(other, Basic):
244 if other.has(Quantity):
245 raise TypeError("cannot sum dimension and quantity")
246 if isinstance(other, Dimension):
247 return Dimension(self.name*other.name)
248 if not other.free_symbols: # other.is_number cannot be used
249 return self
250 return super().__mul__(other)
251 return self
253 def __rmul__(self, other):
254 return self.__mul__(other)
256 def __truediv__(self, other):
257 return self*Pow(other, -1)
259 def __rtruediv__(self, other):
260 return other * pow(self, -1)
262 @classmethod
263 def _from_dimensional_dependencies(cls, dependencies):
264 return reduce(lambda x, y: x * y, (
265 d**e for d, e in dependencies.items()
266 ), 1)
268 def has_integer_powers(self, dim_sys):
269 """
270 Check if the dimension object has only integer powers.
272 All the dimension powers should be integers, but rational powers may
273 appear in intermediate steps. This method may be used to check that the
274 final result is well-defined.
275 """
277 return all(dpow.is_Integer for dpow in dim_sys.get_dimensional_dependencies(self).values())
280# Create dimensions according to the base units in MKSA.
281# For other unit systems, they can be derived by transforming the base
282# dimensional dependency dictionary.
285class DimensionSystem(Basic, _QuantityMapper):
286 r"""
287 DimensionSystem represents a coherent set of dimensions.
289 The constructor takes three parameters:
291 - base dimensions;
292 - derived dimensions: these are defined in terms of the base dimensions
293 (for example velocity is defined from the division of length by time);
294 - dependency of dimensions: how the derived dimensions depend
295 on the base dimensions.
297 Optionally either the ``derived_dims`` or the ``dimensional_dependencies``
298 may be omitted.
299 """
301 def __new__(cls, base_dims, derived_dims=(), dimensional_dependencies={}):
302 dimensional_dependencies = dict(dimensional_dependencies)
304 def parse_dim(dim):
305 if isinstance(dim, str):
306 dim = Dimension(Symbol(dim))
307 elif isinstance(dim, Dimension):
308 pass
309 elif isinstance(dim, Symbol):
310 dim = Dimension(dim)
311 else:
312 raise TypeError("%s wrong type" % dim)
313 return dim
315 base_dims = [parse_dim(i) for i in base_dims]
316 derived_dims = [parse_dim(i) for i in derived_dims]
318 for dim in base_dims:
319 if (dim in dimensional_dependencies
320 and (len(dimensional_dependencies[dim]) != 1 or
321 dimensional_dependencies[dim].get(dim, None) != 1)):
322 raise IndexError("Repeated value in base dimensions")
323 dimensional_dependencies[dim] = Dict({dim: 1})
325 def parse_dim_name(dim):
326 if isinstance(dim, Dimension):
327 return dim
328 elif isinstance(dim, str):
329 return Dimension(Symbol(dim))
330 elif isinstance(dim, Symbol):
331 return Dimension(dim)
332 else:
333 raise TypeError("unrecognized type %s for %s" % (type(dim), dim))
335 for dim in dimensional_dependencies.keys():
336 dim = parse_dim(dim)
337 if (dim not in derived_dims) and (dim not in base_dims):
338 derived_dims.append(dim)
340 def parse_dict(d):
341 return Dict({parse_dim_name(i): j for i, j in d.items()})
343 # Make sure everything is a SymPy type:
344 dimensional_dependencies = {parse_dim_name(i): parse_dict(j) for i, j in
345 dimensional_dependencies.items()}
347 for dim in derived_dims:
348 if dim in base_dims:
349 raise ValueError("Dimension %s both in base and derived" % dim)
350 if dim not in dimensional_dependencies:
351 # TODO: should this raise a warning?
352 dimensional_dependencies[dim] = Dict({dim: 1})
354 base_dims.sort(key=default_sort_key)
355 derived_dims.sort(key=default_sort_key)
357 base_dims = Tuple(*base_dims)
358 derived_dims = Tuple(*derived_dims)
359 dimensional_dependencies = Dict({i: Dict(j) for i, j in dimensional_dependencies.items()})
360 obj = Basic.__new__(cls, base_dims, derived_dims, dimensional_dependencies)
361 return obj
363 @property
364 def base_dims(self):
365 return self.args[0]
367 @property
368 def derived_dims(self):
369 return self.args[1]
371 @property
372 def dimensional_dependencies(self):
373 return self.args[2]
375 def _get_dimensional_dependencies_for_name(self, dimension):
376 if isinstance(dimension, str):
377 dimension = Dimension(Symbol(dimension))
378 elif not isinstance(dimension, Dimension):
379 dimension = Dimension(dimension)
381 if dimension.name.is_Symbol:
382 # Dimensions not included in the dependencies are considered
383 # as base dimensions:
384 return dict(self.dimensional_dependencies.get(dimension, {dimension: 1}))
386 if dimension.name.is_number or dimension.name.is_NumberSymbol:
387 return {}
389 get_for_name = self._get_dimensional_dependencies_for_name
391 if dimension.name.is_Mul:
392 ret = collections.defaultdict(int)
393 dicts = [get_for_name(i) for i in dimension.name.args]
394 for d in dicts:
395 for k, v in d.items():
396 ret[k] += v
397 return {k: v for (k, v) in ret.items() if v != 0}
399 if dimension.name.is_Add:
400 dicts = [get_for_name(i) for i in dimension.name.args]
401 if all(d == dicts[0] for d in dicts[1:]):
402 return dicts[0]
403 raise TypeError("Only equivalent dimensions can be added or subtracted.")
405 if dimension.name.is_Pow:
406 dim_base = get_for_name(dimension.name.base)
407 dim_exp = get_for_name(dimension.name.exp)
408 if dim_exp == {} or dimension.name.exp.is_Symbol:
409 return {k: v * dimension.name.exp for (k, v) in dim_base.items()}
410 else:
411 raise TypeError("The exponent for the power operator must be a Symbol or dimensionless.")
413 if dimension.name.is_Function:
414 args = (Dimension._from_dimensional_dependencies(
415 get_for_name(arg)) for arg in dimension.name.args)
416 result = dimension.name.func(*args)
418 dicts = [get_for_name(i) for i in dimension.name.args]
420 if isinstance(result, Dimension):
421 return self.get_dimensional_dependencies(result)
422 elif result.func == dimension.name.func:
423 if isinstance(dimension.name, TrigonometricFunction):
424 if dicts[0] in ({}, {Dimension('angle'): 1}):
425 return {}
426 else:
427 raise TypeError("The input argument for the function {} must be dimensionless or have dimensions of angle.".format(dimension.func))
428 else:
429 if all(item == {} for item in dicts):
430 return {}
431 else:
432 raise TypeError("The input arguments for the function {} must be dimensionless.".format(dimension.func))
433 else:
434 return get_for_name(result)
436 raise TypeError("Type {} not implemented for get_dimensional_dependencies".format(type(dimension.name)))
438 def get_dimensional_dependencies(self, name, mark_dimensionless=False):
439 dimdep = self._get_dimensional_dependencies_for_name(name)
440 if mark_dimensionless and dimdep == {}:
441 return {Dimension(1): 1}
442 return {k: v for k, v in dimdep.items()}
444 def equivalent_dims(self, dim1, dim2):
445 deps1 = self.get_dimensional_dependencies(dim1)
446 deps2 = self.get_dimensional_dependencies(dim2)
447 return deps1 == deps2
449 def extend(self, new_base_dims, new_derived_dims=(), new_dim_deps=None):
450 deps = dict(self.dimensional_dependencies)
451 if new_dim_deps:
452 deps.update(new_dim_deps)
454 new_dim_sys = DimensionSystem(
455 tuple(self.base_dims) + tuple(new_base_dims),
456 tuple(self.derived_dims) + tuple(new_derived_dims),
457 deps
458 )
459 new_dim_sys._quantity_dimension_map.update(self._quantity_dimension_map)
460 new_dim_sys._quantity_scale_factors.update(self._quantity_scale_factors)
461 return new_dim_sys
463 def is_dimensionless(self, dimension):
464 """
465 Check if the dimension object really has a dimension.
467 A dimension should have at least one component with non-zero power.
468 """
469 if dimension.name == 1:
470 return True
471 return self.get_dimensional_dependencies(dimension) == {}
473 @property
474 def list_can_dims(self):
475 """
476 Useless method, kept for compatibility with previous versions.
478 DO NOT USE.
480 List all canonical dimension names.
481 """
482 dimset = set()
483 for i in self.base_dims:
484 dimset.update(set(self.get_dimensional_dependencies(i).keys()))
485 return tuple(sorted(dimset, key=str))
487 @property
488 def inv_can_transf_matrix(self):
489 """
490 Useless method, kept for compatibility with previous versions.
492 DO NOT USE.
494 Compute the inverse transformation matrix from the base to the
495 canonical dimension basis.
497 It corresponds to the matrix where columns are the vector of base
498 dimensions in canonical basis.
500 This matrix will almost never be used because dimensions are always
501 defined with respect to the canonical basis, so no work has to be done
502 to get them in this basis. Nonetheless if this matrix is not square
503 (or not invertible) it means that we have chosen a bad basis.
504 """
505 matrix = reduce(lambda x, y: x.row_join(y),
506 [self.dim_can_vector(d) for d in self.base_dims])
507 return matrix
509 @property
510 def can_transf_matrix(self):
511 """
512 Useless method, kept for compatibility with previous versions.
514 DO NOT USE.
516 Return the canonical transformation matrix from the canonical to the
517 base dimension basis.
519 It is the inverse of the matrix computed with inv_can_transf_matrix().
520 """
522 #TODO: the inversion will fail if the system is inconsistent, for
523 # example if the matrix is not a square
524 return reduce(lambda x, y: x.row_join(y),
525 [self.dim_can_vector(d) for d in sorted(self.base_dims, key=str)]
526 ).inv()
528 def dim_can_vector(self, dim):
529 """
530 Useless method, kept for compatibility with previous versions.
532 DO NOT USE.
534 Dimensional representation in terms of the canonical base dimensions.
535 """
537 vec = []
538 for d in self.list_can_dims:
539 vec.append(self.get_dimensional_dependencies(dim).get(d, 0))
540 return Matrix(vec)
542 def dim_vector(self, dim):
543 """
544 Useless method, kept for compatibility with previous versions.
546 DO NOT USE.
549 Vector representation in terms of the base dimensions.
550 """
551 return self.can_transf_matrix * Matrix(self.dim_can_vector(dim))
553 def print_dim_base(self, dim):
554 """
555 Give the string expression of a dimension in term of the basis symbols.
556 """
557 dims = self.dim_vector(dim)
558 symbols = [i.symbol if i.symbol is not None else i.name for i in self.base_dims]
559 res = S.One
560 for (s, p) in zip(symbols, dims):
561 res *= s**p
562 return res
564 @property
565 def dim(self):
566 """
567 Useless method, kept for compatibility with previous versions.
569 DO NOT USE.
571 Give the dimension of the system.
573 That is return the number of dimensions forming the basis.
574 """
575 return len(self.base_dims)
577 @property
578 def is_consistent(self):
579 """
580 Useless method, kept for compatibility with previous versions.
582 DO NOT USE.
584 Check if the system is well defined.
585 """
587 # not enough or too many base dimensions compared to independent
588 # dimensions
589 # in vector language: the set of vectors do not form a basis
590 return self.inv_can_transf_matrix.is_square