798 lines
24 KiB
Python
798 lines
24 KiB
Python
|
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
|