Spaces:
Sleeping
Sleeping
""" | |
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 __future__ import annotations | |
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: dict[Expr, Expr] = {} | |
_quantity_dimensional_equivalence_map_global: dict[Expr, Expr] = {} | |
_quantity_dimension_global: dict[Expr, Expr] = {} | |
def __init__(self, *args, **kwargs): | |
self._quantity_dimension_map = {} | |
self._quantity_scale_factors = {} | |
def set_quantity_dimension(self, quantity, dimension): | |
""" | |
Set the dimension for the quantity in a unit system. | |
If this relation is valid in every unit system, use | |
``quantity.set_global_dimension(dimension)`` instead. | |
""" | |
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[quantity] = dimension | |
def set_quantity_scale_factor(self, quantity, scale_factor): | |
""" | |
Set the scale factor of a quantity relative to another quantity. | |
It should be used only once per quantity to just one other quantity, | |
the algorithm will then be able to compute the scale factors to all | |
other quantities. | |
In case the scale factor is valid in every unit system, please use | |
``quantity.set_global_relative_scale_factor(scale_factor)`` instead. | |
""" | |
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[quantity] = 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) | |
{Dimension(length, L): 1, Dimension(time, T): -1} | |
>>> length + length | |
Dimension(length) | |
>>> l2 = length**2 | |
>>> l2 | |
Dimension(length**2) | |
>>> dimsys_SI.get_dimensional_dependencies(l2) | |
{Dimension(length, L): 2} | |
""" | |
_op_priority = 13.0 | |
# XXX: This doesn't seem to be used anywhere... | |
_dimensional_dependencies = {} # 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) | |
obj = Expr.__new__(cls, name) | |
obj._name = name | |
obj._symbol = symbol | |
return obj | |
def name(self): | |
return self._name | |
def symbol(self): | |
return self._symbol | |
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) | |
def _from_dimensional_dependencies(cls, dependencies): | |
return reduce(lambda x, y: x * y, ( | |
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: | |
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 | |
elif isinstance(dim, str): | |
return Dimension(Symbol(dim)) | |
elif isinstance(dim, Symbol): | |
return Dimension(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 not in dimensional_dependencies: | |
# TODO: should this raise a warning? | |
dimensional_dependencies[dim] = Dict({dim: 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 | |
def base_dims(self): | |
return self.args[0] | |
def derived_dims(self): | |
return self.args[1] | |
def dimensional_dependencies(self): | |
return self.args[2] | |
def _get_dimensional_dependencies_for_name(self, dimension): | |
if isinstance(dimension, str): | |
dimension = Dimension(Symbol(dimension)) | |
elif not isinstance(dimension, Dimension): | |
dimension = Dimension(dimension) | |
if dimension.name.is_Symbol: | |
# Dimensions not included in the dependencies are considered | |
# as base dimensions: | |
return dict(self.dimensional_dependencies.get(dimension, {dimension: 1})) | |
if dimension.name.is_number or dimension.name.is_NumberSymbol: | |
return {} | |
get_for_name = self._get_dimensional_dependencies_for_name | |
if dimension.name.is_Mul: | |
ret = collections.defaultdict(int) | |
dicts = [get_for_name(i) for i in dimension.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 dimension.name.is_Add: | |
dicts = [get_for_name(i) for i in dimension.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 dimension.name.is_Pow: | |
dim_base = get_for_name(dimension.name.base) | |
dim_exp = get_for_name(dimension.name.exp) | |
if dim_exp == {} or dimension.name.exp.is_Symbol: | |
return {k: v * dimension.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 dimension.name.is_Function: | |
args = (Dimension._from_dimensional_dependencies( | |
get_for_name(arg)) for arg in dimension.name.args) | |
result = dimension.name.func(*args) | |
dicts = [get_for_name(i) for i in dimension.name.args] | |
if isinstance(result, Dimension): | |
return self.get_dimensional_dependencies(result) | |
elif result.func == dimension.name.func: | |
if isinstance(dimension.name, TrigonometricFunction): | |
if dicts[0] in ({}, {Dimension('angle'): 1}): | |
return {} | |
else: | |
raise TypeError("The input argument for the function {} must be dimensionless or have dimensions of angle.".format(dimension.func)) | |
else: | |
if all(item == {} for item in dicts): | |
return {} | |
else: | |
raise TypeError("The input arguments for the function {} must be dimensionless.".format(dimension.func)) | |
else: | |
return get_for_name(result) | |
raise TypeError("Type {} not implemented for get_dimensional_dependencies".format(type(dimension.name))) | |
def get_dimensional_dependencies(self, name, mark_dimensionless=False): | |
dimdep = self._get_dimensional_dependencies_for_name(name) | |
if mark_dimensionless and dimdep == {}: | |
return {Dimension(1): 1} | |
return dict(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) == {} | |
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)) | |
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 | |
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 | |
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) | |
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 | |