Coverage for /usr/lib/python3/dist-packages/sympy/tensor/array/arrayop.py: 12%
196 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
1import itertools
2from collections.abc import Iterable
4from sympy.core._print_helpers import Printable
5from sympy.core.containers import Tuple
6from sympy.core.function import diff
7from sympy.core.singleton import S
8from sympy.core.sympify import _sympify
10from sympy.tensor.array.ndim_array import NDimArray
11from sympy.tensor.array.dense_ndim_array import DenseNDimArray, ImmutableDenseNDimArray
12from sympy.tensor.array.sparse_ndim_array import SparseNDimArray
15def _arrayfy(a):
16 from sympy.matrices import MatrixBase
18 if isinstance(a, NDimArray):
19 return a
20 if isinstance(a, (MatrixBase, list, tuple, Tuple)):
21 return ImmutableDenseNDimArray(a)
22 return a
25def tensorproduct(*args):
26 """
27 Tensor product among scalars or array-like objects.
29 The equivalent operator for array expressions is ``ArrayTensorProduct``,
30 which can be used to keep the expression unevaluated.
32 Examples
33 ========
35 >>> from sympy.tensor.array import tensorproduct, Array
36 >>> from sympy.abc import x, y, z, t
37 >>> A = Array([[1, 2], [3, 4]])
38 >>> B = Array([x, y])
39 >>> tensorproduct(A, B)
40 [[[x, y], [2*x, 2*y]], [[3*x, 3*y], [4*x, 4*y]]]
41 >>> tensorproduct(A, x)
42 [[x, 2*x], [3*x, 4*x]]
43 >>> tensorproduct(A, B, B)
44 [[[[x**2, x*y], [x*y, y**2]], [[2*x**2, 2*x*y], [2*x*y, 2*y**2]]], [[[3*x**2, 3*x*y], [3*x*y, 3*y**2]], [[4*x**2, 4*x*y], [4*x*y, 4*y**2]]]]
46 Applying this function on two matrices will result in a rank 4 array.
48 >>> from sympy import Matrix, eye
49 >>> m = Matrix([[x, y], [z, t]])
50 >>> p = tensorproduct(eye(3), m)
51 >>> p
52 [[[[x, y], [z, t]], [[0, 0], [0, 0]], [[0, 0], [0, 0]]], [[[0, 0], [0, 0]], [[x, y], [z, t]], [[0, 0], [0, 0]]], [[[0, 0], [0, 0]], [[0, 0], [0, 0]], [[x, y], [z, t]]]]
54 See Also
55 ========
57 sympy.tensor.array.expressions.array_expressions.ArrayTensorProduct
59 """
60 from sympy.tensor.array import SparseNDimArray, ImmutableSparseNDimArray
62 if len(args) == 0:
63 return S.One
64 if len(args) == 1:
65 return _arrayfy(args[0])
66 from sympy.tensor.array.expressions.array_expressions import _CodegenArrayAbstract
67 from sympy.tensor.array.expressions.array_expressions import ArrayTensorProduct
68 from sympy.tensor.array.expressions.array_expressions import _ArrayExpr
69 from sympy.matrices.expressions.matexpr import MatrixSymbol
70 if any(isinstance(arg, (_ArrayExpr, _CodegenArrayAbstract, MatrixSymbol)) for arg in args):
71 return ArrayTensorProduct(*args)
72 if len(args) > 2:
73 return tensorproduct(tensorproduct(args[0], args[1]), *args[2:])
75 # length of args is 2:
76 a, b = map(_arrayfy, args)
78 if not isinstance(a, NDimArray) or not isinstance(b, NDimArray):
79 return a*b
81 if isinstance(a, SparseNDimArray) and isinstance(b, SparseNDimArray):
82 lp = len(b)
83 new_array = {k1*lp + k2: v1*v2 for k1, v1 in a._sparse_array.items() for k2, v2 in b._sparse_array.items()}
84 return ImmutableSparseNDimArray(new_array, a.shape + b.shape)
86 product_list = [i*j for i in Flatten(a) for j in Flatten(b)]
87 return ImmutableDenseNDimArray(product_list, a.shape + b.shape)
90def _util_contraction_diagonal(array, *contraction_or_diagonal_axes):
91 array = _arrayfy(array)
93 # Verify contraction_axes:
94 taken_dims = set()
95 for axes_group in contraction_or_diagonal_axes:
96 if not isinstance(axes_group, Iterable):
97 raise ValueError("collections of contraction/diagonal axes expected")
99 dim = array.shape[axes_group[0]]
101 for d in axes_group:
102 if d in taken_dims:
103 raise ValueError("dimension specified more than once")
104 if dim != array.shape[d]:
105 raise ValueError("cannot contract or diagonalize between axes of different dimension")
106 taken_dims.add(d)
108 rank = array.rank()
110 remaining_shape = [dim for i, dim in enumerate(array.shape) if i not in taken_dims]
111 cum_shape = [0]*rank
112 _cumul = 1
113 for i in range(rank):
114 cum_shape[rank - i - 1] = _cumul
115 _cumul *= int(array.shape[rank - i - 1])
117 # DEFINITION: by absolute position it is meant the position along the one
118 # dimensional array containing all the tensor components.
120 # Possible future work on this module: move computation of absolute
121 # positions to a class method.
123 # Determine absolute positions of the uncontracted indices:
124 remaining_indices = [[cum_shape[i]*j for j in range(array.shape[i])]
125 for i in range(rank) if i not in taken_dims]
127 # Determine absolute positions of the contracted indices:
128 summed_deltas = []
129 for axes_group in contraction_or_diagonal_axes:
130 lidx = []
131 for js in range(array.shape[axes_group[0]]):
132 lidx.append(sum([cum_shape[ig] * js for ig in axes_group]))
133 summed_deltas.append(lidx)
135 return array, remaining_indices, remaining_shape, summed_deltas
138def tensorcontraction(array, *contraction_axes):
139 """
140 Contraction of an array-like object on the specified axes.
142 The equivalent operator for array expressions is ``ArrayContraction``,
143 which can be used to keep the expression unevaluated.
145 Examples
146 ========
148 >>> from sympy import Array, tensorcontraction
149 >>> from sympy import Matrix, eye
150 >>> tensorcontraction(eye(3), (0, 1))
151 3
152 >>> A = Array(range(18), (3, 2, 3))
153 >>> A
154 [[[0, 1, 2], [3, 4, 5]], [[6, 7, 8], [9, 10, 11]], [[12, 13, 14], [15, 16, 17]]]
155 >>> tensorcontraction(A, (0, 2))
156 [21, 30]
158 Matrix multiplication may be emulated with a proper combination of
159 ``tensorcontraction`` and ``tensorproduct``
161 >>> from sympy import tensorproduct
162 >>> from sympy.abc import a,b,c,d,e,f,g,h
163 >>> m1 = Matrix([[a, b], [c, d]])
164 >>> m2 = Matrix([[e, f], [g, h]])
165 >>> p = tensorproduct(m1, m2)
166 >>> p
167 [[[[a*e, a*f], [a*g, a*h]], [[b*e, b*f], [b*g, b*h]]], [[[c*e, c*f], [c*g, c*h]], [[d*e, d*f], [d*g, d*h]]]]
168 >>> tensorcontraction(p, (1, 2))
169 [[a*e + b*g, a*f + b*h], [c*e + d*g, c*f + d*h]]
170 >>> m1*m2
171 Matrix([
172 [a*e + b*g, a*f + b*h],
173 [c*e + d*g, c*f + d*h]])
175 See Also
176 ========
178 sympy.tensor.array.expressions.array_expressions.ArrayContraction
180 """
181 from sympy.tensor.array.expressions.array_expressions import _array_contraction
182 from sympy.tensor.array.expressions.array_expressions import _CodegenArrayAbstract
183 from sympy.tensor.array.expressions.array_expressions import _ArrayExpr
184 from sympy.matrices.expressions.matexpr import MatrixSymbol
185 if isinstance(array, (_ArrayExpr, _CodegenArrayAbstract, MatrixSymbol)):
186 return _array_contraction(array, *contraction_axes)
188 array, remaining_indices, remaining_shape, summed_deltas = _util_contraction_diagonal(array, *contraction_axes)
190 # Compute the contracted array:
191 #
192 # 1. external for loops on all uncontracted indices.
193 # Uncontracted indices are determined by the combinatorial product of
194 # the absolute positions of the remaining indices.
195 # 2. internal loop on all contracted indices.
196 # It sums the values of the absolute contracted index and the absolute
197 # uncontracted index for the external loop.
198 contracted_array = []
199 for icontrib in itertools.product(*remaining_indices):
200 index_base_position = sum(icontrib)
201 isum = S.Zero
202 for sum_to_index in itertools.product(*summed_deltas):
203 idx = array._get_tuple_index(index_base_position + sum(sum_to_index))
204 isum += array[idx]
206 contracted_array.append(isum)
208 if len(remaining_indices) == 0:
209 assert len(contracted_array) == 1
210 return contracted_array[0]
212 return type(array)(contracted_array, remaining_shape)
215def tensordiagonal(array, *diagonal_axes):
216 """
217 Diagonalization of an array-like object on the specified axes.
219 This is equivalent to multiplying the expression by Kronecker deltas
220 uniting the axes.
222 The diagonal indices are put at the end of the axes.
224 The equivalent operator for array expressions is ``ArrayDiagonal``, which
225 can be used to keep the expression unevaluated.
227 Examples
228 ========
230 ``tensordiagonal`` acting on a 2-dimensional array by axes 0 and 1 is
231 equivalent to the diagonal of the matrix:
233 >>> from sympy import Array, tensordiagonal
234 >>> from sympy import Matrix, eye
235 >>> tensordiagonal(eye(3), (0, 1))
236 [1, 1, 1]
238 >>> from sympy.abc import a,b,c,d
239 >>> m1 = Matrix([[a, b], [c, d]])
240 >>> tensordiagonal(m1, [0, 1])
241 [a, d]
243 In case of higher dimensional arrays, the diagonalized out dimensions
244 are appended removed and appended as a single dimension at the end:
246 >>> A = Array(range(18), (3, 2, 3))
247 >>> A
248 [[[0, 1, 2], [3, 4, 5]], [[6, 7, 8], [9, 10, 11]], [[12, 13, 14], [15, 16, 17]]]
249 >>> tensordiagonal(A, (0, 2))
250 [[0, 7, 14], [3, 10, 17]]
251 >>> from sympy import permutedims
252 >>> tensordiagonal(A, (0, 2)) == permutedims(Array([A[0, :, 0], A[1, :, 1], A[2, :, 2]]), [1, 0])
253 True
255 See Also
256 ========
258 sympy.tensor.array.expressions.array_expressions.ArrayDiagonal
260 """
261 if any(len(i) <= 1 for i in diagonal_axes):
262 raise ValueError("need at least two axes to diagonalize")
264 from sympy.tensor.array.expressions.array_expressions import _ArrayExpr
265 from sympy.tensor.array.expressions.array_expressions import _CodegenArrayAbstract
266 from sympy.tensor.array.expressions.array_expressions import ArrayDiagonal, _array_diagonal
267 from sympy.matrices.expressions.matexpr import MatrixSymbol
268 if isinstance(array, (_ArrayExpr, _CodegenArrayAbstract, MatrixSymbol)):
269 return _array_diagonal(array, *diagonal_axes)
271 ArrayDiagonal._validate(array, *diagonal_axes)
273 array, remaining_indices, remaining_shape, diagonal_deltas = _util_contraction_diagonal(array, *diagonal_axes)
275 # Compute the diagonalized array:
276 #
277 # 1. external for loops on all undiagonalized indices.
278 # Undiagonalized indices are determined by the combinatorial product of
279 # the absolute positions of the remaining indices.
280 # 2. internal loop on all diagonal indices.
281 # It appends the values of the absolute diagonalized index and the absolute
282 # undiagonalized index for the external loop.
283 diagonalized_array = []
284 diagonal_shape = [len(i) for i in diagonal_deltas]
285 for icontrib in itertools.product(*remaining_indices):
286 index_base_position = sum(icontrib)
287 isum = []
288 for sum_to_index in itertools.product(*diagonal_deltas):
289 idx = array._get_tuple_index(index_base_position + sum(sum_to_index))
290 isum.append(array[idx])
292 isum = type(array)(isum).reshape(*diagonal_shape)
293 diagonalized_array.append(isum)
295 return type(array)(diagonalized_array, remaining_shape + diagonal_shape)
298def derive_by_array(expr, dx):
299 r"""
300 Derivative by arrays. Supports both arrays and scalars.
302 The equivalent operator for array expressions is ``array_derive``.
304 Explanation
305 ===========
307 Given the array `A_{i_1, \ldots, i_N}` and the array `X_{j_1, \ldots, j_M}`
308 this function will return a new array `B` defined by
310 `B_{j_1,\ldots,j_M,i_1,\ldots,i_N} := \frac{\partial A_{i_1,\ldots,i_N}}{\partial X_{j_1,\ldots,j_M}}`
312 Examples
313 ========
315 >>> from sympy import derive_by_array
316 >>> from sympy.abc import x, y, z, t
317 >>> from sympy import cos
318 >>> derive_by_array(cos(x*t), x)
319 -t*sin(t*x)
320 >>> derive_by_array(cos(x*t), [x, y, z, t])
321 [-t*sin(t*x), 0, 0, -x*sin(t*x)]
322 >>> derive_by_array([x, y**2*z], [[x, y], [z, t]])
323 [[[1, 0], [0, 2*y*z]], [[0, y**2], [0, 0]]]
325 """
326 from sympy.matrices import MatrixBase
327 from sympy.tensor.array import SparseNDimArray
328 array_types = (Iterable, MatrixBase, NDimArray)
330 if isinstance(dx, array_types):
331 dx = ImmutableDenseNDimArray(dx)
332 for i in dx:
333 if not i._diff_wrt:
334 raise ValueError("cannot derive by this array")
336 if isinstance(expr, array_types):
337 if isinstance(expr, NDimArray):
338 expr = expr.as_immutable()
339 else:
340 expr = ImmutableDenseNDimArray(expr)
342 if isinstance(dx, array_types):
343 if isinstance(expr, SparseNDimArray):
344 lp = len(expr)
345 new_array = {k + i*lp: v
346 for i, x in enumerate(Flatten(dx))
347 for k, v in expr.diff(x)._sparse_array.items()}
348 else:
349 new_array = [[y.diff(x) for y in Flatten(expr)] for x in Flatten(dx)]
350 return type(expr)(new_array, dx.shape + expr.shape)
351 else:
352 return expr.diff(dx)
353 else:
354 expr = _sympify(expr)
355 if isinstance(dx, array_types):
356 return ImmutableDenseNDimArray([expr.diff(i) for i in Flatten(dx)], dx.shape)
357 else:
358 dx = _sympify(dx)
359 return diff(expr, dx)
362def permutedims(expr, perm=None, index_order_old=None, index_order_new=None):
363 """
364 Permutes the indices of an array.
366 Parameter specifies the permutation of the indices.
368 The equivalent operator for array expressions is ``PermuteDims``, which can
369 be used to keep the expression unevaluated.
371 Examples
372 ========
374 >>> from sympy.abc import x, y, z, t
375 >>> from sympy import sin
376 >>> from sympy import Array, permutedims
377 >>> a = Array([[x, y, z], [t, sin(x), 0]])
378 >>> a
379 [[x, y, z], [t, sin(x), 0]]
380 >>> permutedims(a, (1, 0))
381 [[x, t], [y, sin(x)], [z, 0]]
383 If the array is of second order, ``transpose`` can be used:
385 >>> from sympy import transpose
386 >>> transpose(a)
387 [[x, t], [y, sin(x)], [z, 0]]
389 Examples on higher dimensions:
391 >>> b = Array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])
392 >>> permutedims(b, (2, 1, 0))
393 [[[1, 5], [3, 7]], [[2, 6], [4, 8]]]
394 >>> permutedims(b, (1, 2, 0))
395 [[[1, 5], [2, 6]], [[3, 7], [4, 8]]]
397 An alternative way to specify the same permutations as in the previous
398 lines involves passing the *old* and *new* indices, either as a list or as
399 a string:
401 >>> permutedims(b, index_order_old="cba", index_order_new="abc")
402 [[[1, 5], [3, 7]], [[2, 6], [4, 8]]]
403 >>> permutedims(b, index_order_old="cab", index_order_new="abc")
404 [[[1, 5], [2, 6]], [[3, 7], [4, 8]]]
406 ``Permutation`` objects are also allowed:
408 >>> from sympy.combinatorics import Permutation
409 >>> permutedims(b, Permutation([1, 2, 0]))
410 [[[1, 5], [2, 6]], [[3, 7], [4, 8]]]
412 See Also
413 ========
415 sympy.tensor.array.expressions.array_expressions.PermuteDims
417 """
418 from sympy.tensor.array import SparseNDimArray
420 from sympy.tensor.array.expressions.array_expressions import _ArrayExpr
421 from sympy.tensor.array.expressions.array_expressions import _CodegenArrayAbstract
422 from sympy.tensor.array.expressions.array_expressions import _permute_dims
423 from sympy.matrices.expressions.matexpr import MatrixSymbol
424 from sympy.tensor.array.expressions import PermuteDims
425 from sympy.tensor.array.expressions.array_expressions import get_rank
426 perm = PermuteDims._get_permutation_from_arguments(perm, index_order_old, index_order_new, get_rank(expr))
427 if isinstance(expr, (_ArrayExpr, _CodegenArrayAbstract, MatrixSymbol)):
428 return _permute_dims(expr, perm)
430 if not isinstance(expr, NDimArray):
431 expr = ImmutableDenseNDimArray(expr)
433 from sympy.combinatorics import Permutation
434 if not isinstance(perm, Permutation):
435 perm = Permutation(list(perm))
437 if perm.size != expr.rank():
438 raise ValueError("wrong permutation size")
440 # Get the inverse permutation:
441 iperm = ~perm
442 new_shape = perm(expr.shape)
444 if isinstance(expr, SparseNDimArray):
445 return type(expr)({tuple(perm(expr._get_tuple_index(k))): v
446 for k, v in expr._sparse_array.items()}, new_shape)
448 indices_span = perm([range(i) for i in expr.shape])
450 new_array = [None]*len(expr)
451 for i, idx in enumerate(itertools.product(*indices_span)):
452 t = iperm(idx)
453 new_array[i] = expr[t]
455 return type(expr)(new_array, new_shape)
458class Flatten(Printable):
459 """
460 Flatten an iterable object to a list in a lazy-evaluation way.
462 Notes
463 =====
465 This class is an iterator with which the memory cost can be economised.
466 Optimisation has been considered to ameliorate the performance for some
467 specific data types like DenseNDimArray and SparseNDimArray.
469 Examples
470 ========
472 >>> from sympy.tensor.array.arrayop import Flatten
473 >>> from sympy.tensor.array import Array
474 >>> A = Array(range(6)).reshape(2, 3)
475 >>> Flatten(A)
476 Flatten([[0, 1, 2], [3, 4, 5]])
477 >>> [i for i in Flatten(A)]
478 [0, 1, 2, 3, 4, 5]
479 """
480 def __init__(self, iterable):
481 from sympy.matrices.matrices import MatrixBase
482 from sympy.tensor.array import NDimArray
484 if not isinstance(iterable, (Iterable, MatrixBase)):
485 raise NotImplementedError("Data type not yet supported")
487 if isinstance(iterable, list):
488 iterable = NDimArray(iterable)
490 self._iter = iterable
491 self._idx = 0
493 def __iter__(self):
494 return self
496 def __next__(self):
497 from sympy.matrices.matrices import MatrixBase
499 if len(self._iter) > self._idx:
500 if isinstance(self._iter, DenseNDimArray):
501 result = self._iter._array[self._idx]
503 elif isinstance(self._iter, SparseNDimArray):
504 if self._idx in self._iter._sparse_array:
505 result = self._iter._sparse_array[self._idx]
506 else:
507 result = 0
509 elif isinstance(self._iter, MatrixBase):
510 result = self._iter[self._idx]
512 elif hasattr(self._iter, '__next__'):
513 result = next(self._iter)
515 else:
516 result = self._iter[self._idx]
518 else:
519 raise StopIteration
521 self._idx += 1
522 return result
524 def next(self):
525 return self.__next__()
527 def _sympystr(self, printer):
528 return type(self).__name__ + '(' + printer._print(self._iter) + ')'