1""" 2Definition of physical dimensions. 3 4Unit systems will be constructed on top of these dimensions. 5 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""" 12 13from typing import Dict as tDict 14 15import collections 16from functools import reduce 17 18from sympy import (Integer, Matrix, S, Symbol, sympify, Basic, Tuple, Dict, 19 default_sort_key) 20from sympy.functions.elementary.trigonometric import TrigonometricFunction 21from sympy.core.expr import Expr 22from sympy.core.power import Pow 23from sympy.utilities.exceptions import SymPyDeprecationWarning 24 25 26class _QuantityMapper: 27 28 _quantity_scale_factors_global = {} # type: tDict[Expr, Expr] 29 _quantity_dimensional_equivalence_map_global = {} # type: tDict[Expr, Expr] 30 _quantity_dimension_global = {} # type: tDict[Expr, Expr] 31 32 def __init__(self, *args, **kwargs): 33 self._quantity_dimension_map = {} 34 self._quantity_scale_factors = {} 35 36 def set_quantity_dimension(self, unit, dimension): 37 from sympy.physics.units import Quantity 38 dimension = sympify(dimension) 39 if not isinstance(dimension, Dimension): 40 if dimension == 1: 41 dimension = Dimension(1) 42 else: 43 raise ValueError("expected dimension or 1") 44 elif isinstance(dimension, Quantity): 45 dimension = self.get_quantity_dimension(dimension) 46 self._quantity_dimension_map[unit] = dimension 47 48 def set_quantity_scale_factor(self, unit, scale_factor): 49 from sympy.physics.units import Quantity 50 from sympy.physics.units.prefixes import Prefix 51 scale_factor = sympify(scale_factor) 52 # replace all prefixes by their ratio to canonical units: 53 scale_factor = scale_factor.replace( 54 lambda x: isinstance(x, Prefix), 55 lambda x: x.scale_factor 56 ) 57 # replace all quantities by their ratio to canonical units: 58 scale_factor = scale_factor.replace( 59 lambda x: isinstance(x, Quantity), 60 lambda x: self.get_quantity_scale_factor(x) 61 ) 62 self._quantity_scale_factors[unit] = scale_factor 63 64 def get_quantity_dimension(self, unit): 65 from sympy.physics.units import Quantity 66 # First look-up the local dimension map, then the global one: 67 if unit in self._quantity_dimension_map: 68 return self._quantity_dimension_map[unit] 69 if unit in self._quantity_dimension_global: 70 return self._quantity_dimension_global[unit] 71 if unit in self._quantity_dimensional_equivalence_map_global: 72 dep_unit = self._quantity_dimensional_equivalence_map_global[unit] 73 if isinstance(dep_unit, Quantity): 74 return self.get_quantity_dimension(dep_unit) 75 else: 76 return Dimension(self.get_dimensional_expr(dep_unit)) 77 if isinstance(unit, Quantity): 78 return Dimension(unit.name) 79 else: 80 return Dimension(1) 81 82 def get_quantity_scale_factor(self, unit): 83 if unit in self._quantity_scale_factors: 84 return self._quantity_scale_factors[unit] 85 if unit in self._quantity_scale_factors_global: 86 mul_factor, other_unit = self._quantity_scale_factors_global[unit] 87 return mul_factor*self.get_quantity_scale_factor(other_unit) 88 return S.One 89 90 91class Dimension(Expr): 92 """ 93 This class represent the dimension of a physical quantities. 94 95 The ``Dimension`` constructor takes as parameters a name and an optional 96 symbol. 97 98 For example, in classical mechanics we know that time is different from 99 temperature and dimensions make this difference (but they do not provide 100 any measure of these quantites. 101 102 >>> from sympy.physics.units import Dimension 103 >>> length = Dimension('length') 104 >>> length 105 Dimension(length) 106 >>> time = Dimension('time') 107 >>> time 108 Dimension(time) 109 110 Dimensions can be composed using multiplication, division and 111 exponentiation (by a number) to give new dimensions. Addition and 112 subtraction is defined only when the two objects are the same dimension. 113 114 >>> velocity = length / time 115 >>> velocity 116 Dimension(length/time) 117 118 It is possible to use a dimension system object to get the dimensionsal 119 dependencies of a dimension, for example the dimension system used by the 120 SI units convention can be used: 121 122 >>> from sympy.physics.units.systems.si import dimsys_SI 123 >>> dimsys_SI.get_dimensional_dependencies(velocity) 124 {'length': 1, 'time': -1} 125 >>> length + length 126 Dimension(length) 127 >>> l2 = length**2 128 >>> l2 129 Dimension(length**2) 130 >>> dimsys_SI.get_dimensional_dependencies(l2) 131 {'length': 2} 132 133 """ 134 135 _op_priority = 13.0 136 137 # XXX: This doesn't seem to be used anywhere... 138 _dimensional_dependencies = dict() # type: ignore 139 140 is_commutative = True 141 is_number = False 142 # make sqrt(M**2) --> M 143 is_positive = True 144 is_real = True 145 146 def __new__(cls, name, symbol=None): 147 148 if isinstance(name, str): 149 name = Symbol(name) 150 else: 151 name = sympify(name) 152 153 if not isinstance(name, Expr): 154 raise TypeError("Dimension name needs to be a valid math expression") 155 156 if isinstance(symbol, str): 157 symbol = Symbol(symbol) 158 elif symbol is not None: 159 assert isinstance(symbol, Symbol) 160 161 if symbol is not None: 162 obj = Expr.__new__(cls, name, symbol) 163 else: 164 obj = Expr.__new__(cls, name) 165 166 obj._name = name 167 obj._symbol = symbol 168 return obj 169 170 @property 171 def name(self): 172 return self._name 173 174 @property 175 def symbol(self): 176 return self._symbol 177 178 def __hash__(self): 179 return Expr.__hash__(self) 180 181 def __eq__(self, other): 182 if isinstance(other, Dimension): 183 return self.name == other.name 184 return False 185 186 def __str__(self): 187 """ 188 Display the string representation of the dimension. 189 """ 190 if self.symbol is None: 191 return "Dimension(%s)" % (self.name) 192 else: 193 return "Dimension(%s, %s)" % (self.name, self.symbol) 194 195 def __repr__(self): 196 return self.__str__() 197 198 def __neg__(self): 199 return self 200 201 def __add__(self, other): 202 from sympy.physics.units.quantities import Quantity 203 other = sympify(other) 204 if isinstance(other, Basic): 205 if other.has(Quantity): 206 raise TypeError("cannot sum dimension and quantity") 207 if isinstance(other, Dimension) and self == other: 208 return self 209 return super().__add__(other) 210 return self 211 212 def __radd__(self, other): 213 return self.__add__(other) 214 215 def __sub__(self, other): 216 # there is no notion of ordering (or magnitude) among dimension, 217 # subtraction is equivalent to addition when the operation is legal 218 return self + other 219 220 def __rsub__(self, other): 221 # there is no notion of ordering (or magnitude) among dimension, 222 # subtraction is equivalent to addition when the operation is legal 223 return self + other 224 225 def __pow__(self, other): 226 return self._eval_power(other) 227 228 def _eval_power(self, other): 229 other = sympify(other) 230 return Dimension(self.name**other) 231 232 def __mul__(self, other): 233 from sympy.physics.units.quantities import Quantity 234 if isinstance(other, Basic): 235 if other.has(Quantity): 236 raise TypeError("cannot sum dimension and quantity") 237 if isinstance(other, Dimension): 238 return Dimension(self.name*other.name) 239 if not other.free_symbols: # other.is_number cannot be used 240 return self 241 return super().__mul__(other) 242 return self 243 244 def __rmul__(self, other): 245 return self.__mul__(other) 246 247 def __truediv__(self, other): 248 return self*Pow(other, -1) 249 250 def __rtruediv__(self, other): 251 return other * pow(self, -1) 252 253 @classmethod 254 def _from_dimensional_dependencies(cls, dependencies): 255 return reduce(lambda x, y: x * y, ( 256 Dimension(d)**e for d, e in dependencies.items() 257 ), 1) 258 259 @classmethod 260 def _get_dimensional_dependencies_for_name(cls, name): 261 from sympy.physics.units.systems.si import dimsys_default 262 SymPyDeprecationWarning( 263 deprecated_since_version="1.2", 264 issue=13336, 265 feature="do not call from `Dimension` objects.", 266 useinstead="DimensionSystem" 267 ).warn() 268 return dimsys_default.get_dimensional_dependencies(name) 269 270 @property 271 def is_dimensionless(self): 272 """ 273 Check if the dimension object really has a dimension. 274 275 A dimension should have at least one component with non-zero power. 276 """ 277 if self.name == 1: 278 return True 279 280 from sympy.physics.units.systems.si import dimsys_default 281 SymPyDeprecationWarning( 282 deprecated_since_version="1.2", 283 issue=13336, 284 feature="wrong class", 285 ).warn() 286 dimensional_dependencies=dimsys_default 287 288 return dimensional_dependencies.get_dimensional_dependencies(self) == {} 289 290 def has_integer_powers(self, dim_sys): 291 """ 292 Check if the dimension object has only integer powers. 293 294 All the dimension powers should be integers, but rational powers may 295 appear in intermediate steps. This method may be used to check that the 296 final result is well-defined. 297 """ 298 299 for dpow in dim_sys.get_dimensional_dependencies(self).values(): 300 if not isinstance(dpow, (int, Integer)): 301 return False 302 303 return True 304 305 306# Create dimensions according the the base units in MKSA. 307# For other unit systems, they can be derived by transforming the base 308# dimensional dependency dictionary. 309 310 311class DimensionSystem(Basic, _QuantityMapper): 312 r""" 313 DimensionSystem represents a coherent set of dimensions. 314 315 The constructor takes three parameters: 316 317 - base dimensions; 318 - derived dimensions: these are defined in terms of the base dimensions 319 (for example velocity is defined from the division of length by time); 320 - dependency of dimensions: how the derived dimensions depend 321 on the base dimensions. 322 323 Optionally either the ``derived_dims`` or the ``dimensional_dependencies`` 324 may be omitted. 325 """ 326 327 def __new__(cls, base_dims, derived_dims=[], dimensional_dependencies={}, name=None, descr=None): 328 dimensional_dependencies = dict(dimensional_dependencies) 329 330 if (name is not None) or (descr is not None): 331 SymPyDeprecationWarning( 332 deprecated_since_version="1.2", 333 issue=13336, 334 useinstead="do not define a `name` or `descr`", 335 ).warn() 336 337 def parse_dim(dim): 338 if isinstance(dim, str): 339 dim = Dimension(Symbol(dim)) 340 elif isinstance(dim, Dimension): 341 pass 342 elif isinstance(dim, Symbol): 343 dim = Dimension(dim) 344 else: 345 raise TypeError("%s wrong type" % dim) 346 return dim 347 348 base_dims = [parse_dim(i) for i in base_dims] 349 derived_dims = [parse_dim(i) for i in derived_dims] 350 351 for dim in base_dims: 352 dim = dim.name 353 if (dim in dimensional_dependencies 354 and (len(dimensional_dependencies[dim]) != 1 or 355 dimensional_dependencies[dim].get(dim, None) != 1)): 356 raise IndexError("Repeated value in base dimensions") 357 dimensional_dependencies[dim] = Dict({dim: 1}) 358 359 def parse_dim_name(dim): 360 if isinstance(dim, Dimension): 361 return dim.name 362 elif isinstance(dim, str): 363 return Symbol(dim) 364 elif isinstance(dim, Symbol): 365 return dim 366 else: 367 raise TypeError("unrecognized type %s for %s" % (type(dim), dim)) 368 369 for dim in dimensional_dependencies.keys(): 370 dim = parse_dim(dim) 371 if (dim not in derived_dims) and (dim not in base_dims): 372 derived_dims.append(dim) 373 374 def parse_dict(d): 375 return Dict({parse_dim_name(i): j for i, j in d.items()}) 376 377 # Make sure everything is a SymPy type: 378 dimensional_dependencies = {parse_dim_name(i): parse_dict(j) for i, j in 379 dimensional_dependencies.items()} 380 381 for dim in derived_dims: 382 if dim in base_dims: 383 raise ValueError("Dimension %s both in base and derived" % dim) 384 if dim.name not in dimensional_dependencies: 385 # TODO: should this raise a warning? 386 dimensional_dependencies[dim.name] = Dict({dim.name: 1}) 387 388 base_dims.sort(key=default_sort_key) 389 derived_dims.sort(key=default_sort_key) 390 391 base_dims = Tuple(*base_dims) 392 derived_dims = Tuple(*derived_dims) 393 dimensional_dependencies = Dict({i: Dict(j) for i, j in dimensional_dependencies.items()}) 394 obj = Basic.__new__(cls, base_dims, derived_dims, dimensional_dependencies) 395 return obj 396 397 @property 398 def base_dims(self): 399 return self.args[0] 400 401 @property 402 def derived_dims(self): 403 return self.args[1] 404 405 @property 406 def dimensional_dependencies(self): 407 return self.args[2] 408 409 def _get_dimensional_dependencies_for_name(self, name): 410 if isinstance(name, Dimension): 411 name = name.name 412 413 if isinstance(name, str): 414 name = Symbol(name) 415 416 if name.is_Symbol: 417 # Dimensions not included in the dependencies are considered 418 # as base dimensions: 419 return dict(self.dimensional_dependencies.get(name, {name: 1})) 420 421 if name.is_number or name.is_NumberSymbol: 422 return {} 423 424 get_for_name = self._get_dimensional_dependencies_for_name 425 426 if name.is_Mul: 427 ret = collections.defaultdict(int) 428 dicts = [get_for_name(i) for i in name.args] 429 for d in dicts: 430 for k, v in d.items(): 431 ret[k] += v 432 return {k: v for (k, v) in ret.items() if v != 0} 433 434 if name.is_Add: 435 dicts = [get_for_name(i) for i in name.args] 436 if all([d == dicts[0] for d in dicts[1:]]): 437 return dicts[0] 438 raise TypeError("Only equivalent dimensions can be added or subtracted.") 439 440 if name.is_Pow: 441 dim_base = get_for_name(name.base) 442 dim_exp = get_for_name(name.exp) 443 if dim_exp == {} or name.exp.is_Symbol: 444 return {k: v*name.exp for (k, v) in dim_base.items()} 445 else: 446 raise TypeError("The exponent for the power operator must be a Symbol or dimensionless.") 447 448 if name.is_Function: 449 args = (Dimension._from_dimensional_dependencies( 450 get_for_name(arg)) for arg in name.args) 451 result = name.func(*args) 452 453 dicts = [get_for_name(i) for i in name.args] 454 455 if isinstance(result, Dimension): 456 return self.get_dimensional_dependencies(result) 457 elif result.func == name.func: 458 if isinstance(name, TrigonometricFunction): 459 if dicts[0] == {} or dicts[0] == {Symbol('angle'): 1}: 460 return {} 461 else: 462 raise TypeError("The input argument for the function {} must be dimensionless or have dimensions of angle.".format(name.func)) 463 else: 464 if all( (item == {} for item in dicts) ): 465 return {} 466 else: 467 raise TypeError("The input arguments for the function {} must be dimensionless.".format(name.func)) 468 else: 469 return get_for_name(result) 470 471 raise TypeError("Type {} not implemented for get_dimensional_dependencies".format(type(name))) 472 473 def get_dimensional_dependencies(self, name, mark_dimensionless=False): 474 dimdep = self._get_dimensional_dependencies_for_name(name) 475 if mark_dimensionless and dimdep == {}: 476 return {'dimensionless': 1} 477 return {str(i): j for i, j in dimdep.items()} 478 479 def equivalent_dims(self, dim1, dim2): 480 deps1 = self.get_dimensional_dependencies(dim1) 481 deps2 = self.get_dimensional_dependencies(dim2) 482 return deps1 == deps2 483 484 def extend(self, new_base_dims, new_derived_dims=[], new_dim_deps={}, name=None, description=None): 485 if (name is not None) or (description is not None): 486 SymPyDeprecationWarning( 487 deprecated_since_version="1.2", 488 issue=13336, 489 feature="name and descriptions of DimensionSystem", 490 useinstead="do not specify `name` or `description`", 491 ).warn() 492 493 deps = dict(self.dimensional_dependencies) 494 deps.update(new_dim_deps) 495 496 new_dim_sys = DimensionSystem( 497 tuple(self.base_dims) + tuple(new_base_dims), 498 tuple(self.derived_dims) + tuple(new_derived_dims), 499 deps 500 ) 501 new_dim_sys._quantity_dimension_map.update(self._quantity_dimension_map) 502 new_dim_sys._quantity_scale_factors.update(self._quantity_scale_factors) 503 return new_dim_sys 504 505 @staticmethod 506 def sort_dims(dims): 507 """ 508 Useless method, kept for compatibility with previous versions. 509 510 DO NOT USE. 511 512 Sort dimensions given in argument using their str function. 513 514 This function will ensure that we get always the same tuple for a given 515 set of dimensions. 516 """ 517 SymPyDeprecationWarning( 518 deprecated_since_version="1.2", 519 issue=13336, 520 feature="sort_dims", 521 useinstead="sorted(..., key=default_sort_key)", 522 ).warn() 523 return tuple(sorted(dims, key=str)) 524 525 def __getitem__(self, key): 526 """ 527 Useless method, kept for compatibility with previous versions. 528 529 DO NOT USE. 530 531 Shortcut to the get_dim method, using key access. 532 """ 533 SymPyDeprecationWarning( 534 deprecated_since_version="1.2", 535 issue=13336, 536 feature="the get [ ] operator", 537 useinstead="the dimension definition", 538 ).warn() 539 d = self.get_dim(key) 540 #TODO: really want to raise an error? 541 if d is None: 542 raise KeyError(key) 543 return d 544 545 def __call__(self, unit): 546 """ 547 Useless method, kept for compatibility with previous versions. 548 549 DO NOT USE. 550 551 Wrapper to the method print_dim_base 552 """ 553 SymPyDeprecationWarning( 554 deprecated_since_version="1.2", 555 issue=13336, 556 feature="call DimensionSystem", 557 useinstead="the dimension definition", 558 ).warn() 559 return self.print_dim_base(unit) 560 561 def is_dimensionless(self, dimension): 562 """ 563 Check if the dimension object really has a dimension. 564 565 A dimension should have at least one component with non-zero power. 566 """ 567 if dimension.name == 1: 568 return True 569 return self.get_dimensional_dependencies(dimension) == {} 570 571 @property 572 def list_can_dims(self): 573 """ 574 Useless method, kept for compatibility with previous versions. 575 576 DO NOT USE. 577 578 List all canonical dimension names. 579 """ 580 dimset = set() 581 for i in self.base_dims: 582 dimset.update(set(self.get_dimensional_dependencies(i).keys())) 583 return tuple(sorted(dimset, key=str)) 584 585 @property 586 def inv_can_transf_matrix(self): 587 """ 588 Useless method, kept for compatibility with previous versions. 589 590 DO NOT USE. 591 592 Compute the inverse transformation matrix from the base to the 593 canonical dimension basis. 594 595 It corresponds to the matrix where columns are the vector of base 596 dimensions in canonical basis. 597 598 This matrix will almost never be used because dimensions are always 599 defined with respect to the canonical basis, so no work has to be done 600 to get them in this basis. Nonetheless if this matrix is not square 601 (or not invertible) it means that we have chosen a bad basis. 602 """ 603 matrix = reduce(lambda x, y: x.row_join(y), 604 [self.dim_can_vector(d) for d in self.base_dims]) 605 return matrix 606 607 @property 608 def can_transf_matrix(self): 609 """ 610 Useless method, kept for compatibility with previous versions. 611 612 DO NOT USE. 613 614 Return the canonical transformation matrix from the canonical to the 615 base dimension basis. 616 617 It is the inverse of the matrix computed with inv_can_transf_matrix(). 618 """ 619 620 #TODO: the inversion will fail if the system is inconsistent, for 621 # example if the matrix is not a square 622 return reduce(lambda x, y: x.row_join(y), 623 [self.dim_can_vector(d) for d in sorted(self.base_dims, key=str)] 624 ).inv() 625 626 def dim_can_vector(self, dim): 627 """ 628 Useless method, kept for compatibility with previous versions. 629 630 DO NOT USE. 631 632 Dimensional representation in terms of the canonical base dimensions. 633 """ 634 635 vec = [] 636 for d in self.list_can_dims: 637 vec.append(self.get_dimensional_dependencies(dim).get(d, 0)) 638 return Matrix(vec) 639 640 def dim_vector(self, dim): 641 """ 642 Useless method, kept for compatibility with previous versions. 643 644 DO NOT USE. 645 646 647 Vector representation in terms of the base dimensions. 648 """ 649 return self.can_transf_matrix * Matrix(self.dim_can_vector(dim)) 650 651 def print_dim_base(self, dim): 652 """ 653 Give the string expression of a dimension in term of the basis symbols. 654 """ 655 dims = self.dim_vector(dim) 656 symbols = [i.symbol if i.symbol is not None else i.name for i in self.base_dims] 657 res = S.One 658 for (s, p) in zip(symbols, dims): 659 res *= s**p 660 return res 661 662 @property 663 def dim(self): 664 """ 665 Useless method, kept for compatibility with previous versions. 666 667 DO NOT USE. 668 669 Give the dimension of the system. 670 671 That is return the number of dimensions forming the basis. 672 """ 673 return len(self.base_dims) 674 675 @property 676 def is_consistent(self): 677 """ 678 Useless method, kept for compatibility with previous versions. 679 680 DO NOT USE. 681 682 Check if the system is well defined. 683 """ 684 685 # not enough or too many base dimensions compared to independent 686 # dimensions 687 # in vector language: the set of vectors do not form a basis 688 return self.inv_can_transf_matrix.is_square 689