123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797 |
- r"""Module that defines indexed objects
- The classes ``IndexedBase``, ``Indexed``, and ``Idx`` represent a
- matrix element ``M[i, j]`` as in the following diagram::
- 1) The Indexed class represents the entire indexed object.
- |
- ___|___
- ' '
- M[i, j]
- / \__\______
- | |
- | |
- | 2) The Idx class represents indices; each Idx can
- | optionally contain information about its range.
- |
- 3) IndexedBase represents the 'stem' of an indexed object, here `M`.
- The stem used by itself is usually taken to represent the entire
- array.
- There can be any number of indices on an Indexed object. No
- transformation properties are implemented in these Base objects, but
- implicit contraction of repeated indices is supported.
- Note that the support for complicated (i.e. non-atomic) integer
- expressions as indices is limited. (This should be improved in
- future releases.)
- Examples
- ========
- To express the above matrix element example you would write:
- >>> from sympy import symbols, IndexedBase, Idx
- >>> M = IndexedBase('M')
- >>> i, j = symbols('i j', cls=Idx)
- >>> M[i, j]
- M[i, j]
- Repeated indices in a product implies a summation, so to express a
- matrix-vector product in terms of Indexed objects:
- >>> x = IndexedBase('x')
- >>> M[i, j]*x[j]
- M[i, j]*x[j]
- If the indexed objects will be converted to component based arrays, e.g.
- with the code printers or the autowrap framework, you also need to provide
- (symbolic or numerical) dimensions. This can be done by passing an
- optional shape parameter to IndexedBase upon construction:
- >>> dim1, dim2 = symbols('dim1 dim2', integer=True)
- >>> A = IndexedBase('A', shape=(dim1, 2*dim1, dim2))
- >>> A.shape
- (dim1, 2*dim1, dim2)
- >>> A[i, j, 3].shape
- (dim1, 2*dim1, dim2)
- If an IndexedBase object has no shape information, it is assumed that the
- array is as large as the ranges of its indices:
- >>> n, m = symbols('n m', integer=True)
- >>> i = Idx('i', m)
- >>> j = Idx('j', n)
- >>> M[i, j].shape
- (m, n)
- >>> M[i, j].ranges
- [(0, m - 1), (0, n - 1)]
- The above can be compared with the following:
- >>> A[i, 2, j].shape
- (dim1, 2*dim1, dim2)
- >>> A[i, 2, j].ranges
- [(0, m - 1), None, (0, n - 1)]
- To analyze the structure of indexed expressions, you can use the methods
- get_indices() and get_contraction_structure():
- >>> from sympy.tensor import get_indices, get_contraction_structure
- >>> get_indices(A[i, j, j])
- ({i}, {})
- >>> get_contraction_structure(A[i, j, j])
- {(j,): {A[i, j, j]}}
- See the appropriate docstrings for a detailed explanation of the output.
- """
- # TODO: (some ideas for improvement)
- #
- # o test and guarantee numpy compatibility
- # - implement full support for broadcasting
- # - strided arrays
- #
- # o more functions to analyze indexed expressions
- # - identify standard constructs, e.g matrix-vector product in a subexpression
- #
- # o functions to generate component based arrays (numpy and sympy.Matrix)
- # - generate a single array directly from Indexed
- # - convert simple sub-expressions
- #
- # o sophisticated indexing (possibly in subclasses to preserve simplicity)
- # - Idx with range smaller than dimension of Indexed
- # - Idx with stepsize != 1
- # - Idx with step determined by function call
- from collections.abc import Iterable
- from sympy.core.numbers import Number
- from sympy.core.assumptions import StdFactKB
- from sympy.core import Expr, Tuple, sympify, S
- from sympy.core.symbol import _filter_assumptions, Symbol
- from sympy.core.logic import fuzzy_bool, fuzzy_not
- from sympy.core.sympify import _sympify
- from sympy.functions.special.tensor_functions import KroneckerDelta
- from sympy.multipledispatch import dispatch
- from sympy.utilities.iterables import is_sequence, NotIterable
- from sympy.utilities.misc import filldedent
- class IndexException(Exception):
- pass
- class Indexed(Expr):
- """Represents a mathematical object with indices.
- >>> from sympy import Indexed, IndexedBase, Idx, symbols
- >>> i, j = symbols('i j', cls=Idx)
- >>> Indexed('A', i, j)
- A[i, j]
- It is recommended that ``Indexed`` objects be created by indexing ``IndexedBase``:
- ``IndexedBase('A')[i, j]`` instead of ``Indexed(IndexedBase('A'), i, j)``.
- >>> A = IndexedBase('A')
- >>> a_ij = A[i, j] # Prefer this,
- >>> b_ij = Indexed(A, i, j) # over this.
- >>> a_ij == b_ij
- True
- """
- is_commutative = True
- is_Indexed = True
- is_symbol = True
- is_Atom = True
- def __new__(cls, base, *args, **kw_args):
- from sympy.tensor.array.ndim_array import NDimArray
- from sympy.matrices.matrices import MatrixBase
- if not args:
- raise IndexException("Indexed needs at least one index.")
- if isinstance(base, (str, Symbol)):
- base = IndexedBase(base)
- elif not hasattr(base, '__getitem__') and not isinstance(base, IndexedBase):
- raise TypeError(filldedent("""
- The base can only be replaced with a string, Symbol,
- IndexedBase or an object with a method for getting
- items (i.e. an object with a `__getitem__` method).
- """))
- args = list(map(sympify, args))
- if isinstance(base, (NDimArray, Iterable, Tuple, MatrixBase)) and all(i.is_number for i in args):
- if len(args) == 1:
- return base[args[0]]
- else:
- return base[args]
- base = _sympify(base)
- obj = Expr.__new__(cls, base, *args, **kw_args)
- try:
- IndexedBase._set_assumptions(obj, base.assumptions0)
- except AttributeError:
- IndexedBase._set_assumptions(obj, {})
- return obj
- def _hashable_content(self):
- return super()._hashable_content() + tuple(sorted(self.assumptions0.items()))
- @property
- def name(self):
- return str(self)
- @property
- def _diff_wrt(self):
- """Allow derivatives with respect to an ``Indexed`` object."""
- return True
- def _eval_derivative(self, wrt):
- from sympy.tensor.array.ndim_array import NDimArray
- if isinstance(wrt, Indexed) and wrt.base == self.base:
- if len(self.indices) != len(wrt.indices):
- msg = "Different # of indices: d({!s})/d({!s})".format(self,
- wrt)
- raise IndexException(msg)
- result = S.One
- for index1, index2 in zip(self.indices, wrt.indices):
- result *= KroneckerDelta(index1, index2)
- return result
- elif isinstance(self.base, NDimArray):
- from sympy.tensor.array import derive_by_array
- return Indexed(derive_by_array(self.base, wrt), *self.args[1:])
- else:
- if Tuple(self.indices).has(wrt):
- return S.NaN
- return S.Zero
- @property
- def assumptions0(self):
- return {k: v for k, v in self._assumptions.items() if v is not None}
- @property
- def base(self):
- """Returns the ``IndexedBase`` of the ``Indexed`` object.
- Examples
- ========
- >>> from sympy import Indexed, IndexedBase, Idx, symbols
- >>> i, j = symbols('i j', cls=Idx)
- >>> Indexed('A', i, j).base
- A
- >>> B = IndexedBase('B')
- >>> B == B[i, j].base
- True
- """
- return self.args[0]
- @property
- def indices(self):
- """
- Returns the indices of the ``Indexed`` object.
- Examples
- ========
- >>> from sympy import Indexed, Idx, symbols
- >>> i, j = symbols('i j', cls=Idx)
- >>> Indexed('A', i, j).indices
- (i, j)
- """
- return self.args[1:]
- @property
- def rank(self):
- """
- Returns the rank of the ``Indexed`` object.
- Examples
- ========
- >>> from sympy import Indexed, Idx, symbols
- >>> i, j, k, l, m = symbols('i:m', cls=Idx)
- >>> Indexed('A', i, j).rank
- 2
- >>> q = Indexed('A', i, j, k, l, m)
- >>> q.rank
- 5
- >>> q.rank == len(q.indices)
- True
- """
- return len(self.args) - 1
- @property
- def shape(self):
- """Returns a list with dimensions of each index.
- Dimensions is a property of the array, not of the indices. Still, if
- the ``IndexedBase`` does not define a shape attribute, it is assumed
- that the ranges of the indices correspond to the shape of the array.
- >>> from sympy import IndexedBase, Idx, symbols
- >>> n, m = symbols('n m', integer=True)
- >>> i = Idx('i', m)
- >>> j = Idx('j', m)
- >>> A = IndexedBase('A', shape=(n, n))
- >>> B = IndexedBase('B')
- >>> A[i, j].shape
- (n, n)
- >>> B[i, j].shape
- (m, m)
- """
- if self.base.shape:
- return self.base.shape
- sizes = []
- for i in self.indices:
- upper = getattr(i, 'upper', None)
- lower = getattr(i, 'lower', None)
- if None in (upper, lower):
- raise IndexException(filldedent("""
- Range is not defined for all indices in: %s""" % self))
- try:
- size = upper - lower + 1
- except TypeError:
- raise IndexException(filldedent("""
- Shape cannot be inferred from Idx with
- undefined range: %s""" % self))
- sizes.append(size)
- return Tuple(*sizes)
- @property
- def ranges(self):
- """Returns a list of tuples with lower and upper range of each index.
- If an index does not define the data members upper and lower, the
- corresponding slot in the list contains ``None`` instead of a tuple.
- Examples
- ========
- >>> from sympy import Indexed,Idx, symbols
- >>> Indexed('A', Idx('i', 2), Idx('j', 4), Idx('k', 8)).ranges
- [(0, 1), (0, 3), (0, 7)]
- >>> Indexed('A', Idx('i', 3), Idx('j', 3), Idx('k', 3)).ranges
- [(0, 2), (0, 2), (0, 2)]
- >>> x, y, z = symbols('x y z', integer=True)
- >>> Indexed('A', x, y, z).ranges
- [None, None, None]
- """
- ranges = []
- sentinel = object()
- for i in self.indices:
- upper = getattr(i, 'upper', sentinel)
- lower = getattr(i, 'lower', sentinel)
- if sentinel not in (upper, lower):
- ranges.append((lower, upper))
- else:
- ranges.append(None)
- return ranges
- def _sympystr(self, p):
- indices = list(map(p.doprint, self.indices))
- return "%s[%s]" % (p.doprint(self.base), ", ".join(indices))
- @property
- def free_symbols(self):
- base_free_symbols = self.base.free_symbols
- indices_free_symbols = {
- fs for i in self.indices for fs in i.free_symbols}
- if base_free_symbols:
- return {self} | base_free_symbols | indices_free_symbols
- else:
- return indices_free_symbols
- @property
- def expr_free_symbols(self):
- from sympy.utilities.exceptions import sympy_deprecation_warning
- sympy_deprecation_warning("""
- The expr_free_symbols property is deprecated. Use free_symbols to get
- the free symbols of an expression.
- """,
- deprecated_since_version="1.9",
- active_deprecations_target="deprecated-expr-free-symbols")
- return {self}
- class IndexedBase(Expr, NotIterable):
- """Represent the base or stem of an indexed object
- The IndexedBase class represent an array that contains elements. The main purpose
- of this class is to allow the convenient creation of objects of the Indexed
- class. The __getitem__ method of IndexedBase returns an instance of
- Indexed. Alone, without indices, the IndexedBase class can be used as a
- notation for e.g. matrix equations, resembling what you could do with the
- Symbol class. But, the IndexedBase class adds functionality that is not
- available for Symbol instances:
- - An IndexedBase object can optionally store shape information. This can
- be used in to check array conformance and conditions for numpy
- broadcasting. (TODO)
- - An IndexedBase object implements syntactic sugar that allows easy symbolic
- representation of array operations, using implicit summation of
- repeated indices.
- - The IndexedBase object symbolizes a mathematical structure equivalent
- to arrays, and is recognized as such for code generation and automatic
- compilation and wrapping.
- >>> from sympy.tensor import IndexedBase, Idx
- >>> from sympy import symbols
- >>> A = IndexedBase('A'); A
- A
- >>> type(A)
- <class 'sympy.tensor.indexed.IndexedBase'>
- When an IndexedBase object receives indices, it returns an array with named
- axes, represented by an Indexed object:
- >>> i, j = symbols('i j', integer=True)
- >>> A[i, j, 2]
- A[i, j, 2]
- >>> type(A[i, j, 2])
- <class 'sympy.tensor.indexed.Indexed'>
- The IndexedBase constructor takes an optional shape argument. If given,
- it overrides any shape information in the indices. (But not the index
- ranges!)
- >>> m, n, o, p = symbols('m n o p', integer=True)
- >>> i = Idx('i', m)
- >>> j = Idx('j', n)
- >>> A[i, j].shape
- (m, n)
- >>> B = IndexedBase('B', shape=(o, p))
- >>> B[i, j].shape
- (o, p)
- Assumptions can be specified with keyword arguments the same way as for Symbol:
- >>> A_real = IndexedBase('A', real=True)
- >>> A_real.is_real
- True
- >>> A != A_real
- True
- Assumptions can also be inherited if a Symbol is used to initialize the IndexedBase:
- >>> I = symbols('I', integer=True)
- >>> C_inherit = IndexedBase(I)
- >>> C_explicit = IndexedBase('I', integer=True)
- >>> C_inherit == C_explicit
- True
- """
- is_commutative = True
- is_symbol = True
- is_Atom = True
- @staticmethod
- def _set_assumptions(obj, assumptions):
- """Set assumptions on obj, making sure to apply consistent values."""
- tmp_asm_copy = assumptions.copy()
- is_commutative = fuzzy_bool(assumptions.get('commutative', True))
- assumptions['commutative'] = is_commutative
- obj._assumptions = StdFactKB(assumptions)
- obj._assumptions._generator = tmp_asm_copy # Issue #8873
- def __new__(cls, label, shape=None, *, offset=S.Zero, strides=None, **kw_args):
- from sympy.matrices.matrices import MatrixBase
- from sympy.tensor.array.ndim_array import NDimArray
- assumptions, kw_args = _filter_assumptions(kw_args)
- if isinstance(label, str):
- label = Symbol(label, **assumptions)
- elif isinstance(label, Symbol):
- assumptions = label._merge(assumptions)
- elif isinstance(label, (MatrixBase, NDimArray)):
- return label
- elif isinstance(label, Iterable):
- return _sympify(label)
- else:
- label = _sympify(label)
- if is_sequence(shape):
- shape = Tuple(*shape)
- elif shape is not None:
- shape = Tuple(shape)
- if shape is not None:
- obj = Expr.__new__(cls, label, shape)
- else:
- obj = Expr.__new__(cls, label)
- obj._shape = shape
- obj._offset = offset
- obj._strides = strides
- obj._name = str(label)
- IndexedBase._set_assumptions(obj, assumptions)
- return obj
- @property
- def name(self):
- return self._name
- def _hashable_content(self):
- return super()._hashable_content() + tuple(sorted(self.assumptions0.items()))
- @property
- def assumptions0(self):
- return {k: v for k, v in self._assumptions.items() if v is not None}
- def __getitem__(self, indices, **kw_args):
- if is_sequence(indices):
- # Special case needed because M[*my_tuple] is a syntax error.
- if self.shape and len(self.shape) != len(indices):
- raise IndexException("Rank mismatch.")
- return Indexed(self, *indices, **kw_args)
- else:
- if self.shape and len(self.shape) != 1:
- raise IndexException("Rank mismatch.")
- return Indexed(self, indices, **kw_args)
- @property
- def shape(self):
- """Returns the shape of the ``IndexedBase`` object.
- Examples
- ========
- >>> from sympy import IndexedBase, Idx
- >>> from sympy.abc import x, y
- >>> IndexedBase('A', shape=(x, y)).shape
- (x, y)
- Note: If the shape of the ``IndexedBase`` is specified, it will override
- any shape information given by the indices.
- >>> A = IndexedBase('A', shape=(x, y))
- >>> B = IndexedBase('B')
- >>> i = Idx('i', 2)
- >>> j = Idx('j', 1)
- >>> A[i, j].shape
- (x, y)
- >>> B[i, j].shape
- (2, 1)
- """
- return self._shape
- @property
- def strides(self):
- """Returns the strided scheme for the ``IndexedBase`` object.
- Normally this is a tuple denoting the number of
- steps to take in the respective dimension when traversing
- an array. For code generation purposes strides='C' and
- strides='F' can also be used.
- strides='C' would mean that code printer would unroll
- in row-major order and 'F' means unroll in column major
- order.
- """
- return self._strides
- @property
- def offset(self):
- """Returns the offset for the ``IndexedBase`` object.
- This is the value added to the resulting index when the
- 2D Indexed object is unrolled to a 1D form. Used in code
- generation.
- Examples
- ==========
- >>> from sympy.printing import ccode
- >>> from sympy.tensor import IndexedBase, Idx
- >>> from sympy import symbols
- >>> l, m, n, o = symbols('l m n o', integer=True)
- >>> A = IndexedBase('A', strides=(l, m, n), offset=o)
- >>> i, j, k = map(Idx, 'ijk')
- >>> ccode(A[i, j, k])
- 'A[l*i + m*j + n*k + o]'
- """
- return self._offset
- @property
- def label(self):
- """Returns the label of the ``IndexedBase`` object.
- Examples
- ========
- >>> from sympy import IndexedBase
- >>> from sympy.abc import x, y
- >>> IndexedBase('A', shape=(x, y)).label
- A
- """
- return self.args[0]
- def _sympystr(self, p):
- return p.doprint(self.label)
- class Idx(Expr):
- """Represents an integer index as an ``Integer`` or integer expression.
- There are a number of ways to create an ``Idx`` object. The constructor
- takes two arguments:
- ``label``
- An integer or a symbol that labels the index.
- ``range``
- Optionally you can specify a range as either
- * ``Symbol`` or integer: This is interpreted as a dimension. Lower and
- upper bounds are set to ``0`` and ``range - 1``, respectively.
- * ``tuple``: The two elements are interpreted as the lower and upper
- bounds of the range, respectively.
- Note: bounds of the range are assumed to be either integer or infinite (oo
- and -oo are allowed to specify an unbounded range). If ``n`` is given as a
- bound, then ``n.is_integer`` must not return false.
- For convenience, if the label is given as a string it is automatically
- converted to an integer symbol. (Note: this conversion is not done for
- range or dimension arguments.)
- Examples
- ========
- >>> from sympy import Idx, symbols, oo
- >>> n, i, L, U = symbols('n i L U', integer=True)
- If a string is given for the label an integer ``Symbol`` is created and the
- bounds are both ``None``:
- >>> idx = Idx('qwerty'); idx
- qwerty
- >>> idx.lower, idx.upper
- (None, None)
- Both upper and lower bounds can be specified:
- >>> idx = Idx(i, (L, U)); idx
- i
- >>> idx.lower, idx.upper
- (L, U)
- When only a single bound is given it is interpreted as the dimension
- and the lower bound defaults to 0:
- >>> idx = Idx(i, n); idx.lower, idx.upper
- (0, n - 1)
- >>> idx = Idx(i, 4); idx.lower, idx.upper
- (0, 3)
- >>> idx = Idx(i, oo); idx.lower, idx.upper
- (0, oo)
- """
- is_integer = True
- is_finite = True
- is_real = True
- is_symbol = True
- is_Atom = True
- _diff_wrt = True
- def __new__(cls, label, range=None, **kw_args):
- if isinstance(label, str):
- label = Symbol(label, integer=True)
- label, range = list(map(sympify, (label, range)))
- if label.is_Number:
- if not label.is_integer:
- raise TypeError("Index is not an integer number.")
- return label
- if not label.is_integer:
- raise TypeError("Idx object requires an integer label.")
- elif is_sequence(range):
- if len(range) != 2:
- raise ValueError(filldedent("""
- Idx range tuple must have length 2, but got %s""" % len(range)))
- for bound in range:
- if (bound.is_integer is False and bound is not S.Infinity
- and bound is not S.NegativeInfinity):
- raise TypeError("Idx object requires integer bounds.")
- args = label, Tuple(*range)
- elif isinstance(range, Expr):
- if range is not S.Infinity and fuzzy_not(range.is_integer):
- raise TypeError("Idx object requires an integer dimension.")
- args = label, Tuple(0, range - 1)
- elif range:
- raise TypeError(filldedent("""
- The range must be an ordered iterable or
- integer SymPy expression."""))
- else:
- args = label,
- obj = Expr.__new__(cls, *args, **kw_args)
- obj._assumptions["finite"] = True
- obj._assumptions["real"] = True
- return obj
- @property
- def label(self):
- """Returns the label (Integer or integer expression) of the Idx object.
- Examples
- ========
- >>> from sympy import Idx, Symbol
- >>> x = Symbol('x', integer=True)
- >>> Idx(x).label
- x
- >>> j = Symbol('j', integer=True)
- >>> Idx(j).label
- j
- >>> Idx(j + 1).label
- j + 1
- """
- return self.args[0]
- @property
- def lower(self):
- """Returns the lower bound of the ``Idx``.
- Examples
- ========
- >>> from sympy import Idx
- >>> Idx('j', 2).lower
- 0
- >>> Idx('j', 5).lower
- 0
- >>> Idx('j').lower is None
- True
- """
- try:
- return self.args[1][0]
- except IndexError:
- return
- @property
- def upper(self):
- """Returns the upper bound of the ``Idx``.
- Examples
- ========
- >>> from sympy import Idx
- >>> Idx('j', 2).upper
- 1
- >>> Idx('j', 5).upper
- 4
- >>> Idx('j').upper is None
- True
- """
- try:
- return self.args[1][1]
- except IndexError:
- return
- def _sympystr(self, p):
- return p.doprint(self.label)
- @property
- def name(self):
- return self.label.name if self.label.is_Symbol else str(self.label)
- @property
- def free_symbols(self):
- return {self}
- @dispatch(Idx, Idx)
- def _eval_is_ge(lhs, rhs): # noqa:F811
- other_upper = rhs if rhs.upper is None else rhs.upper
- other_lower = rhs if rhs.lower is None else rhs.lower
- if lhs.lower is not None and (lhs.lower >= other_upper) == True:
- return True
- if lhs.upper is not None and (lhs.upper < other_lower) == True:
- return False
- return None
- @dispatch(Idx, Number) # type:ignore
- def _eval_is_ge(lhs, rhs): # noqa:F811
- other_upper = rhs
- other_lower = rhs
- if lhs.lower is not None and (lhs.lower >= other_upper) == True:
- return True
- if lhs.upper is not None and (lhs.upper < other_lower) == True:
- return False
- return None
- @dispatch(Number, Idx) # type:ignore
- def _eval_is_ge(lhs, rhs): # noqa:F811
- other_upper = lhs
- other_lower = lhs
- if rhs.upper is not None and (rhs.upper <= other_lower) == True:
- return True
- if rhs.lower is not None and (rhs.lower > other_upper) == True:
- return False
- return None
|