123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587 |
- """
- Definition of physical dimensions.
- Unit systems will be constructed on top of these dimensions.
- Most of the examples in the doc use MKS system and are presented from the
- computer point of view: from a human point, adding length to time is not legal
- in MKS but it is in natural system; for a computer in natural system there is
- no time dimension (but a velocity dimension instead) - in the basis - so the
- question of adding time to length has no meaning.
- """
- from typing import Dict as tDict
- import collections
- from functools import reduce
- from sympy.core.basic import Basic
- from sympy.core.containers import (Dict, Tuple)
- from sympy.core.singleton import S
- from sympy.core.sorting import default_sort_key
- from sympy.core.symbol import Symbol
- from sympy.core.sympify import sympify
- from sympy.matrices.dense import Matrix
- from sympy.functions.elementary.trigonometric import TrigonometricFunction
- from sympy.core.expr import Expr
- from sympy.core.power import Pow
- class _QuantityMapper:
- _quantity_scale_factors_global = {} # type: tDict[Expr, Expr]
- _quantity_dimensional_equivalence_map_global = {} # type: tDict[Expr, Expr]
- _quantity_dimension_global = {} # type: tDict[Expr, Expr]
- def __init__(self, *args, **kwargs):
- self._quantity_dimension_map = {}
- self._quantity_scale_factors = {}
- def set_quantity_dimension(self, unit, dimension):
- from sympy.physics.units import Quantity
- dimension = sympify(dimension)
- if not isinstance(dimension, Dimension):
- if dimension == 1:
- dimension = Dimension(1)
- else:
- raise ValueError("expected dimension or 1")
- elif isinstance(dimension, Quantity):
- dimension = self.get_quantity_dimension(dimension)
- self._quantity_dimension_map[unit] = dimension
- def set_quantity_scale_factor(self, unit, scale_factor):
- from sympy.physics.units import Quantity
- from sympy.physics.units.prefixes import Prefix
- scale_factor = sympify(scale_factor)
- # replace all prefixes by their ratio to canonical units:
- scale_factor = scale_factor.replace(
- lambda x: isinstance(x, Prefix),
- lambda x: x.scale_factor
- )
- # replace all quantities by their ratio to canonical units:
- scale_factor = scale_factor.replace(
- lambda x: isinstance(x, Quantity),
- lambda x: self.get_quantity_scale_factor(x)
- )
- self._quantity_scale_factors[unit] = scale_factor
- def get_quantity_dimension(self, unit):
- from sympy.physics.units import Quantity
- # First look-up the local dimension map, then the global one:
- if unit in self._quantity_dimension_map:
- return self._quantity_dimension_map[unit]
- if unit in self._quantity_dimension_global:
- return self._quantity_dimension_global[unit]
- if unit in self._quantity_dimensional_equivalence_map_global:
- dep_unit = self._quantity_dimensional_equivalence_map_global[unit]
- if isinstance(dep_unit, Quantity):
- return self.get_quantity_dimension(dep_unit)
- else:
- return Dimension(self.get_dimensional_expr(dep_unit))
- if isinstance(unit, Quantity):
- return Dimension(unit.name)
- else:
- return Dimension(1)
- def get_quantity_scale_factor(self, unit):
- if unit in self._quantity_scale_factors:
- return self._quantity_scale_factors[unit]
- if unit in self._quantity_scale_factors_global:
- mul_factor, other_unit = self._quantity_scale_factors_global[unit]
- return mul_factor*self.get_quantity_scale_factor(other_unit)
- return S.One
- class Dimension(Expr):
- """
- This class represent the dimension of a physical quantities.
- The ``Dimension`` constructor takes as parameters a name and an optional
- symbol.
- For example, in classical mechanics we know that time is different from
- temperature and dimensions make this difference (but they do not provide
- any measure of these quantites.
- >>> from sympy.physics.units import Dimension
- >>> length = Dimension('length')
- >>> length
- Dimension(length)
- >>> time = Dimension('time')
- >>> time
- Dimension(time)
- Dimensions can be composed using multiplication, division and
- exponentiation (by a number) to give new dimensions. Addition and
- subtraction is defined only when the two objects are the same dimension.
- >>> velocity = length / time
- >>> velocity
- Dimension(length/time)
- It is possible to use a dimension system object to get the dimensionsal
- dependencies of a dimension, for example the dimension system used by the
- SI units convention can be used:
- >>> from sympy.physics.units.systems.si import dimsys_SI
- >>> dimsys_SI.get_dimensional_dependencies(velocity)
- {'length': 1, 'time': -1}
- >>> length + length
- Dimension(length)
- >>> l2 = length**2
- >>> l2
- Dimension(length**2)
- >>> dimsys_SI.get_dimensional_dependencies(l2)
- {'length': 2}
- """
- _op_priority = 13.0
- # XXX: This doesn't seem to be used anywhere...
- _dimensional_dependencies = dict() # type: ignore
- is_commutative = True
- is_number = False
- # make sqrt(M**2) --> M
- is_positive = True
- is_real = True
- def __new__(cls, name, symbol=None):
- if isinstance(name, str):
- name = Symbol(name)
- else:
- name = sympify(name)
- if not isinstance(name, Expr):
- raise TypeError("Dimension name needs to be a valid math expression")
- if isinstance(symbol, str):
- symbol = Symbol(symbol)
- elif symbol is not None:
- assert isinstance(symbol, Symbol)
- if symbol is not None:
- obj = Expr.__new__(cls, name, symbol)
- else:
- obj = Expr.__new__(cls, name)
- obj._name = name
- obj._symbol = symbol
- return obj
- @property
- def name(self):
- return self._name
- @property
- def symbol(self):
- return self._symbol
- def __hash__(self):
- return Expr.__hash__(self)
- def __eq__(self, other):
- if isinstance(other, Dimension):
- return self.name == other.name
- return False
- def __str__(self):
- """
- Display the string representation of the dimension.
- """
- if self.symbol is None:
- return "Dimension(%s)" % (self.name)
- else:
- return "Dimension(%s, %s)" % (self.name, self.symbol)
- def __repr__(self):
- return self.__str__()
- def __neg__(self):
- return self
- def __add__(self, other):
- from sympy.physics.units.quantities import Quantity
- other = sympify(other)
- if isinstance(other, Basic):
- if other.has(Quantity):
- raise TypeError("cannot sum dimension and quantity")
- if isinstance(other, Dimension) and self == other:
- return self
- return super().__add__(other)
- return self
- def __radd__(self, other):
- return self.__add__(other)
- def __sub__(self, other):
- # there is no notion of ordering (or magnitude) among dimension,
- # subtraction is equivalent to addition when the operation is legal
- return self + other
- def __rsub__(self, other):
- # there is no notion of ordering (or magnitude) among dimension,
- # subtraction is equivalent to addition when the operation is legal
- return self + other
- def __pow__(self, other):
- return self._eval_power(other)
- def _eval_power(self, other):
- other = sympify(other)
- return Dimension(self.name**other)
- def __mul__(self, other):
- from sympy.physics.units.quantities import Quantity
- if isinstance(other, Basic):
- if other.has(Quantity):
- raise TypeError("cannot sum dimension and quantity")
- if isinstance(other, Dimension):
- return Dimension(self.name*other.name)
- if not other.free_symbols: # other.is_number cannot be used
- return self
- return super().__mul__(other)
- return self
- def __rmul__(self, other):
- return self.__mul__(other)
- def __truediv__(self, other):
- return self*Pow(other, -1)
- def __rtruediv__(self, other):
- return other * pow(self, -1)
- @classmethod
- def _from_dimensional_dependencies(cls, dependencies):
- return reduce(lambda x, y: x * y, (
- Dimension(d)**e for d, e in dependencies.items()
- ), 1)
- def has_integer_powers(self, dim_sys):
- """
- Check if the dimension object has only integer powers.
- All the dimension powers should be integers, but rational powers may
- appear in intermediate steps. This method may be used to check that the
- final result is well-defined.
- """
- return all(dpow.is_Integer for dpow in dim_sys.get_dimensional_dependencies(self).values())
- # Create dimensions according to the base units in MKSA.
- # For other unit systems, they can be derived by transforming the base
- # dimensional dependency dictionary.
- class DimensionSystem(Basic, _QuantityMapper):
- r"""
- DimensionSystem represents a coherent set of dimensions.
- The constructor takes three parameters:
- - base dimensions;
- - derived dimensions: these are defined in terms of the base dimensions
- (for example velocity is defined from the division of length by time);
- - dependency of dimensions: how the derived dimensions depend
- on the base dimensions.
- Optionally either the ``derived_dims`` or the ``dimensional_dependencies``
- may be omitted.
- """
- def __new__(cls, base_dims, derived_dims=(), dimensional_dependencies={}):
- dimensional_dependencies = dict(dimensional_dependencies)
- def parse_dim(dim):
- if isinstance(dim, str):
- dim = Dimension(Symbol(dim))
- elif isinstance(dim, Dimension):
- pass
- elif isinstance(dim, Symbol):
- dim = Dimension(dim)
- else:
- raise TypeError("%s wrong type" % dim)
- return dim
- base_dims = [parse_dim(i) for i in base_dims]
- derived_dims = [parse_dim(i) for i in derived_dims]
- for dim in base_dims:
- dim = dim.name
- if (dim in dimensional_dependencies
- and (len(dimensional_dependencies[dim]) != 1 or
- dimensional_dependencies[dim].get(dim, None) != 1)):
- raise IndexError("Repeated value in base dimensions")
- dimensional_dependencies[dim] = Dict({dim: 1})
- def parse_dim_name(dim):
- if isinstance(dim, Dimension):
- return dim.name
- elif isinstance(dim, str):
- return Symbol(dim)
- elif isinstance(dim, Symbol):
- return dim
- else:
- raise TypeError("unrecognized type %s for %s" % (type(dim), dim))
- for dim in dimensional_dependencies.keys():
- dim = parse_dim(dim)
- if (dim not in derived_dims) and (dim not in base_dims):
- derived_dims.append(dim)
- def parse_dict(d):
- return Dict({parse_dim_name(i): j for i, j in d.items()})
- # Make sure everything is a SymPy type:
- dimensional_dependencies = {parse_dim_name(i): parse_dict(j) for i, j in
- dimensional_dependencies.items()}
- for dim in derived_dims:
- if dim in base_dims:
- raise ValueError("Dimension %s both in base and derived" % dim)
- if dim.name not in dimensional_dependencies:
- # TODO: should this raise a warning?
- dimensional_dependencies[dim.name] = Dict({dim.name: 1})
- base_dims.sort(key=default_sort_key)
- derived_dims.sort(key=default_sort_key)
- base_dims = Tuple(*base_dims)
- derived_dims = Tuple(*derived_dims)
- dimensional_dependencies = Dict({i: Dict(j) for i, j in dimensional_dependencies.items()})
- obj = Basic.__new__(cls, base_dims, derived_dims, dimensional_dependencies)
- return obj
- @property
- def base_dims(self):
- return self.args[0]
- @property
- def derived_dims(self):
- return self.args[1]
- @property
- def dimensional_dependencies(self):
- return self.args[2]
- def _get_dimensional_dependencies_for_name(self, name):
- if isinstance(name, Dimension):
- name = name.name
- if isinstance(name, str):
- name = Symbol(name)
- if name.is_Symbol:
- # Dimensions not included in the dependencies are considered
- # as base dimensions:
- return dict(self.dimensional_dependencies.get(name, {name: 1}))
- if name.is_number or name.is_NumberSymbol:
- return {}
- get_for_name = self._get_dimensional_dependencies_for_name
- if name.is_Mul:
- ret = collections.defaultdict(int)
- dicts = [get_for_name(i) for i in name.args]
- for d in dicts:
- for k, v in d.items():
- ret[k] += v
- return {k: v for (k, v) in ret.items() if v != 0}
- if name.is_Add:
- dicts = [get_for_name(i) for i in name.args]
- if all(d == dicts[0] for d in dicts[1:]):
- return dicts[0]
- raise TypeError("Only equivalent dimensions can be added or subtracted.")
- if name.is_Pow:
- dim_base = get_for_name(name.base)
- dim_exp = get_for_name(name.exp)
- if dim_exp == {} or name.exp.is_Symbol:
- return {k: v*name.exp for (k, v) in dim_base.items()}
- else:
- raise TypeError("The exponent for the power operator must be a Symbol or dimensionless.")
- if name.is_Function:
- args = (Dimension._from_dimensional_dependencies(
- get_for_name(arg)) for arg in name.args)
- result = name.func(*args)
- dicts = [get_for_name(i) for i in name.args]
- if isinstance(result, Dimension):
- return self.get_dimensional_dependencies(result)
- elif result.func == name.func:
- if isinstance(name, TrigonometricFunction):
- if dicts[0] in ({}, {Symbol('angle'): 1}):
- return {}
- else:
- raise TypeError("The input argument for the function {} must be dimensionless or have dimensions of angle.".format(name.func))
- else:
- if all( (item == {} for item in dicts) ):
- return {}
- else:
- raise TypeError("The input arguments for the function {} must be dimensionless.".format(name.func))
- else:
- return get_for_name(result)
- raise TypeError("Type {} not implemented for get_dimensional_dependencies".format(type(name)))
- def get_dimensional_dependencies(self, name, mark_dimensionless=False):
- dimdep = self._get_dimensional_dependencies_for_name(name)
- if mark_dimensionless and dimdep == {}:
- return {'dimensionless': 1}
- return {str(i): j for i, j in dimdep.items()}
- def equivalent_dims(self, dim1, dim2):
- deps1 = self.get_dimensional_dependencies(dim1)
- deps2 = self.get_dimensional_dependencies(dim2)
- return deps1 == deps2
- def extend(self, new_base_dims, new_derived_dims=(), new_dim_deps=None):
- deps = dict(self.dimensional_dependencies)
- if new_dim_deps:
- deps.update(new_dim_deps)
- new_dim_sys = DimensionSystem(
- tuple(self.base_dims) + tuple(new_base_dims),
- tuple(self.derived_dims) + tuple(new_derived_dims),
- deps
- )
- new_dim_sys._quantity_dimension_map.update(self._quantity_dimension_map)
- new_dim_sys._quantity_scale_factors.update(self._quantity_scale_factors)
- return new_dim_sys
- def is_dimensionless(self, dimension):
- """
- Check if the dimension object really has a dimension.
- A dimension should have at least one component with non-zero power.
- """
- if dimension.name == 1:
- return True
- return self.get_dimensional_dependencies(dimension) == {}
- @property
- def list_can_dims(self):
- """
- Useless method, kept for compatibility with previous versions.
- DO NOT USE.
- List all canonical dimension names.
- """
- dimset = set()
- for i in self.base_dims:
- dimset.update(set(self.get_dimensional_dependencies(i).keys()))
- return tuple(sorted(dimset, key=str))
- @property
- def inv_can_transf_matrix(self):
- """
- Useless method, kept for compatibility with previous versions.
- DO NOT USE.
- Compute the inverse transformation matrix from the base to the
- canonical dimension basis.
- It corresponds to the matrix where columns are the vector of base
- dimensions in canonical basis.
- This matrix will almost never be used because dimensions are always
- defined with respect to the canonical basis, so no work has to be done
- to get them in this basis. Nonetheless if this matrix is not square
- (or not invertible) it means that we have chosen a bad basis.
- """
- matrix = reduce(lambda x, y: x.row_join(y),
- [self.dim_can_vector(d) for d in self.base_dims])
- return matrix
- @property
- def can_transf_matrix(self):
- """
- Useless method, kept for compatibility with previous versions.
- DO NOT USE.
- Return the canonical transformation matrix from the canonical to the
- base dimension basis.
- It is the inverse of the matrix computed with inv_can_transf_matrix().
- """
- #TODO: the inversion will fail if the system is inconsistent, for
- # example if the matrix is not a square
- return reduce(lambda x, y: x.row_join(y),
- [self.dim_can_vector(d) for d in sorted(self.base_dims, key=str)]
- ).inv()
- def dim_can_vector(self, dim):
- """
- Useless method, kept for compatibility with previous versions.
- DO NOT USE.
- Dimensional representation in terms of the canonical base dimensions.
- """
- vec = []
- for d in self.list_can_dims:
- vec.append(self.get_dimensional_dependencies(dim).get(d, 0))
- return Matrix(vec)
- def dim_vector(self, dim):
- """
- Useless method, kept for compatibility with previous versions.
- DO NOT USE.
- Vector representation in terms of the base dimensions.
- """
- return self.can_transf_matrix * Matrix(self.dim_can_vector(dim))
- def print_dim_base(self, dim):
- """
- Give the string expression of a dimension in term of the basis symbols.
- """
- dims = self.dim_vector(dim)
- symbols = [i.symbol if i.symbol is not None else i.name for i in self.base_dims]
- res = S.One
- for (s, p) in zip(symbols, dims):
- res *= s**p
- return res
- @property
- def dim(self):
- """
- Useless method, kept for compatibility with previous versions.
- DO NOT USE.
- Give the dimension of the system.
- That is return the number of dimensions forming the basis.
- """
- return len(self.base_dims)
- @property
- def is_consistent(self):
- """
- Useless method, kept for compatibility with previous versions.
- DO NOT USE.
- Check if the system is well defined.
- """
- # not enough or too many base dimensions compared to independent
- # dimensions
- # in vector language: the set of vectors do not form a basis
- return self.inv_can_transf_matrix.is_square
|