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