Matrix Expressions

The Matrix expression module allows users to write down statements like

>>> from sympy import MatrixSymbol, Matrix
>>> X = MatrixSymbol('X', 3, 3)
>>> Y = MatrixSymbol('Y', 3, 3)
>>> (X.T*X).I*Y
X**(-1)*X.T**(-1)*Y
>>> Matrix(X)
Matrix([
[X[0, 0], X[0, 1], X[0, 2]],
[X[1, 0], X[1, 1], X[1, 2]],
[X[2, 0], X[2, 1], X[2, 2]]])
>>> (X*Y)[1, 2]
X[1, 0]*Y[0, 2] + X[1, 1]*Y[1, 2] + X[1, 2]*Y[2, 2]

where X and Y are MatrixSymbol’s rather than scalar symbols.

Matrix expression derivatives are supported. The derivative of a matrix by another matrix is generally a 4-dimensional array, but if some dimensions are trivial or diagonal, the derivation algorithm will try to express the result as a matrix expression:

>>> a = MatrixSymbol("a", 3, 1)
>>> b = MatrixSymbol("b", 3, 1)
>>> (a.T*X**2*b).diff(X)
a*b.T*X.T + X.T*a*b.T
>>> X.diff(X)
PermuteDims(ArrayTensorProduct(I, I), (3)(1 2))

The last output is an array expression, as the returned symbol is 4-dimensional.

Matrix Expressions Core Reference

class sympy.matrices.expressions.MatrixExpr(*args, **kwargs)[source]

Superclass for Matrix Expressions

MatrixExprs represent abstract matrices, linear transformations represented within a particular basis.

Examples

>>> from sympy import MatrixSymbol
>>> A = MatrixSymbol('A', 3, 3)
>>> y = MatrixSymbol('y', 3, 1)
>>> x = (A.T*A).I * A * y
property T

Matrix transposition

as_coeff_Mul(rational=False)[source]

Efficiently extract the coefficient of a product.

as_explicit()[source]

Returns a dense Matrix with elements represented explicitly

Returns an object of type ImmutableDenseMatrix.

Examples

>>> from sympy import Identity
>>> I = Identity(3)
>>> I
I
>>> I.as_explicit()
Matrix([
[1, 0, 0],
[0, 1, 0],
[0, 0, 1]])

See also

as_mutable

returns mutable Matrix type

as_mutable()[source]

Returns a dense, mutable matrix with elements represented explicitly

Examples

>>> from sympy import Identity
>>> I = Identity(3)
>>> I
I
>>> I.shape
(3, 3)
>>> I.as_mutable()
Matrix([
[1, 0, 0],
[0, 1, 0],
[0, 0, 1]])

See also

as_explicit

returns ImmutableDenseMatrix

equals(other)[source]

Test elementwise equality between matrices, potentially of different types

>>> from sympy import Identity, eye
>>> Identity(3).equals(eye(3))
True
static from_index_summation(expr, first_index=None, last_index=None, dimensions=None)[source]

Parse expression of matrices with explicitly summed indices into a matrix expression without indices, if possible.

This transformation expressed in mathematical notation:

\(\sum_{j=0}^{N-1} A_{i,j} B_{j,k} \Longrightarrow \mathbf{A}\cdot \mathbf{B}\)

Optional parameter first_index: specify which free index to use as the index starting the expression.

Examples

>>> from sympy import MatrixSymbol, MatrixExpr, Sum
>>> from sympy.abc import i, j, k, l, N
>>> A = MatrixSymbol("A", N, N)
>>> B = MatrixSymbol("B", N, N)
>>> expr = Sum(A[i, j]*B[j, k], (j, 0, N-1))
>>> MatrixExpr.from_index_summation(expr)
A*B

Transposition is detected:

>>> expr = Sum(A[j, i]*B[j, k], (j, 0, N-1))
>>> MatrixExpr.from_index_summation(expr)
A.T*B

Detect the trace:

>>> expr = Sum(A[i, i], (i, 0, N-1))
>>> MatrixExpr.from_index_summation(expr)
Trace(A)

More complicated expressions:

>>> expr = Sum(A[i, j]*B[k, j]*A[l, k], (j, 0, N-1), (k, 0, N-1))
>>> MatrixExpr.from_index_summation(expr)
A*B.T*A.T
class sympy.matrices.expressions.MatrixSymbol(name, n, m)[source]

Symbolic representation of a Matrix object

Creates a SymPy Symbol to represent a Matrix. This matrix has a shape and can be included in Matrix Expressions

Examples

>>> from sympy import MatrixSymbol, Identity
>>> A = MatrixSymbol('A', 3, 4) # A 3 by 4 Matrix
>>> B = MatrixSymbol('B', 4, 3) # A 4 by 3 Matrix
>>> A.shape
(3, 4)
>>> 2*A*B + Identity(3)
I + 2*A*B
class sympy.matrices.expressions.MatAdd(*args, evaluate=False, check=None, _sympify=True)[source]

A Sum of Matrix Expressions

MatAdd inherits from and operates like SymPy Add

Examples

>>> from sympy import MatAdd, MatrixSymbol
>>> A = MatrixSymbol('A', 5, 5)
>>> B = MatrixSymbol('B', 5, 5)
>>> C = MatrixSymbol('C', 5, 5)
>>> MatAdd(A, B, C)
A + B + C
class sympy.matrices.expressions.MatMul(*args, evaluate=False, check=None, _sympify=True)[source]

A product of matrix expressions

Examples

>>> from sympy import MatMul, MatrixSymbol
>>> A = MatrixSymbol('A', 5, 4)
>>> B = MatrixSymbol('B', 4, 3)
>>> C = MatrixSymbol('C', 3, 6)
>>> MatMul(A, B, C)
A*B*C
class sympy.matrices.expressions.MatPow(base, exp, evaluate=False, **options)[source]
sympy.matrices.expressions.hadamard_product(*matrices)[source]

Return the elementwise (aka Hadamard) product of matrices.

Examples

>>> from sympy import hadamard_product, MatrixSymbol
>>> A = MatrixSymbol('A', 2, 3)
>>> B = MatrixSymbol('B', 2, 3)
>>> hadamard_product(A)
A
>>> hadamard_product(A, B)
HadamardProduct(A, B)
>>> hadamard_product(A, B)[0, 1]
A[0, 1]*B[0, 1]
class sympy.matrices.expressions.HadamardProduct(*args, evaluate=False, check=None)[source]

Elementwise product of matrix expressions

Examples

Hadamard product for matrix symbols:

>>> from sympy import hadamard_product, HadamardProduct, MatrixSymbol
>>> A = MatrixSymbol('A', 5, 5)
>>> B = MatrixSymbol('B', 5, 5)
>>> isinstance(hadamard_product(A, B), HadamardProduct)
True

Notes

This is a symbolic object that simply stores its argument without evaluating it. To actually compute the product, use the function hadamard_product() or HadamardProduct.doit

class sympy.matrices.expressions.HadamardPower(base, exp)[source]

Elementwise power of matrix expressions

Parameters:

base : scalar or matrix

exp : scalar or matrix

Notes

There are four definitions for the hadamard power which can be used. Let’s consider \(A, B\) as \((m, n)\) matrices, and \(a, b\) as scalars.

Matrix raised to a scalar exponent:

\[\begin{split}A^{\circ b} = \begin{bmatrix} A_{0, 0}^b & A_{0, 1}^b & \cdots & A_{0, n-1}^b \\ A_{1, 0}^b & A_{1, 1}^b & \cdots & A_{1, n-1}^b \\ \vdots & \vdots & \ddots & \vdots \\ A_{m-1, 0}^b & A_{m-1, 1}^b & \cdots & A_{m-1, n-1}^b \end{bmatrix}\end{split}\]

Scalar raised to a matrix exponent:

\[\begin{split}a^{\circ B} = \begin{bmatrix} a^{B_{0, 0}} & a^{B_{0, 1}} & \cdots & a^{B_{0, n-1}} \\ a^{B_{1, 0}} & a^{B_{1, 1}} & \cdots & a^{B_{1, n-1}} \\ \vdots & \vdots & \ddots & \vdots \\ a^{B_{m-1, 0}} & a^{B_{m-1, 1}} & \cdots & a^{B_{m-1, n-1}} \end{bmatrix}\end{split}\]

Matrix raised to a matrix exponent:

\[\begin{split}A^{\circ B} = \begin{bmatrix} A_{0, 0}^{B_{0, 0}} & A_{0, 1}^{B_{0, 1}} & \cdots & A_{0, n-1}^{B_{0, n-1}} \\ A_{1, 0}^{B_{1, 0}} & A_{1, 1}^{B_{1, 1}} & \cdots & A_{1, n-1}^{B_{1, n-1}} \\ \vdots & \vdots & \ddots & \vdots \\ A_{m-1, 0}^{B_{m-1, 0}} & A_{m-1, 1}^{B_{m-1, 1}} & \cdots & A_{m-1, n-1}^{B_{m-1, n-1}} \end{bmatrix}\end{split}\]

Scalar raised to a scalar exponent:

\[a^{\circ b} = a^b\]
class sympy.matrices.expressions.Inverse(mat, exp=-1)[source]

The multiplicative inverse of a matrix expression

This is a symbolic object that simply stores its argument without evaluating it. To actually compute the inverse, use the .inverse() method of matrices.

Examples

>>> from sympy import MatrixSymbol, Inverse
>>> A = MatrixSymbol('A', 3, 3)
>>> B = MatrixSymbol('B', 3, 3)
>>> Inverse(A)
A**(-1)
>>> A.inverse() == Inverse(A)
True
>>> (A*B).inverse()
B**(-1)*A**(-1)
>>> Inverse(A*B)
(A*B)**(-1)
class sympy.matrices.expressions.Transpose(*args, **kwargs)[source]

The transpose of a matrix expression.

This is a symbolic object that simply stores its argument without evaluating it. To actually compute the transpose, use the transpose() function, or the .T attribute of matrices.

Examples

>>> from sympy import MatrixSymbol, Transpose, transpose
>>> A = MatrixSymbol('A', 3, 5)
>>> B = MatrixSymbol('B', 5, 3)
>>> Transpose(A)
A.T
>>> A.T == transpose(A) == Transpose(A)
True
>>> Transpose(A*B)
(A*B).T
>>> transpose(A*B)
B.T*A.T
class sympy.matrices.expressions.Trace(mat)[source]

Matrix Trace

Represents the trace of a matrix expression.

Examples

>>> from sympy import MatrixSymbol, Trace, eye
>>> A = MatrixSymbol('A', 3, 3)
>>> Trace(A)
Trace(A)
>>> Trace(eye(3))
Trace(Matrix([
[1, 0, 0],
[0, 1, 0],
[0, 0, 1]]))
>>> Trace(eye(3)).simplify()
3
class sympy.matrices.expressions.FunctionMatrix(rows, cols, lamda)[source]

Represents a matrix using a function (Lambda) which gives outputs according to the coordinates of each matrix entries.

Parameters:

rows : nonnegative integer. Can be symbolic.

cols : nonnegative integer. Can be symbolic.

lamda : Function, Lambda or str

If it is a SymPy Function or Lambda instance, it should be able to accept two arguments which represents the matrix coordinates.

If it is a pure string containing Python lambda semantics, it is interpreted by the SymPy parser and casted into a SymPy Lambda instance.

Examples

Creating a FunctionMatrix from Lambda:

>>> from sympy import FunctionMatrix, symbols, Lambda, MatPow
>>> i, j, n, m = symbols('i,j,n,m')
>>> FunctionMatrix(n, m, Lambda((i, j), i + j))
FunctionMatrix(n, m, Lambda((i, j), i + j))

Creating a FunctionMatrix from a SymPy function:

>>> from sympy import KroneckerDelta
>>> X = FunctionMatrix(3, 3, KroneckerDelta)
>>> X.as_explicit()
Matrix([
[1, 0, 0],
[0, 1, 0],
[0, 0, 1]])

Creating a FunctionMatrix from a SymPy undefined function:

>>> from sympy import Function
>>> f = Function('f')
>>> X = FunctionMatrix(3, 3, f)
>>> X.as_explicit()
Matrix([
[f(0, 0), f(0, 1), f(0, 2)],
[f(1, 0), f(1, 1), f(1, 2)],
[f(2, 0), f(2, 1), f(2, 2)]])

Creating a FunctionMatrix from Python lambda:

>>> FunctionMatrix(n, m, 'lambda i, j: i + j')
FunctionMatrix(n, m, Lambda((i, j), i + j))

Example of lazy evaluation of matrix product:

>>> Y = FunctionMatrix(1000, 1000, Lambda((i, j), i + j))
>>> isinstance(Y*Y, MatPow) # this is an expression object
True
>>> (Y**2)[10,10] # So this is evaluated lazily
342923500

Notes

This class provides an alternative way to represent an extremely dense matrix with entries in some form of a sequence, in a most sparse way.

class sympy.matrices.expressions.PermutationMatrix(perm)[source]

A Permutation Matrix

Parameters:

perm : Permutation

The permutation the matrix uses.

The size of the permutation determines the matrix size.

See the documentation of sympy.combinatorics.permutations.Permutation for the further information of how to create a permutation object.

Examples

>>> from sympy import Matrix, PermutationMatrix
>>> from sympy.combinatorics import Permutation

Creating a permutation matrix:

>>> p = Permutation(1, 2, 0)
>>> P = PermutationMatrix(p)
>>> P = P.as_explicit()
>>> P
Matrix([
[0, 1, 0],
[0, 0, 1],
[1, 0, 0]])

Permuting a matrix row and column:

>>> M = Matrix([0, 1, 2])
>>> Matrix(P*M)
Matrix([
[1],
[2],
[0]])
>>> Matrix(M.T*P)
Matrix([[2, 0, 1]])
class sympy.matrices.expressions.MatrixPermute(mat, perm, axis=0)[source]

Symbolic representation for permuting matrix rows or columns.

Parameters:

perm : Permutation, PermutationMatrix

The permutation to use for permuting the matrix. The permutation can be resized to the suitable one,

axis : 0 or 1

The axis to permute alongside. If \(0\), it will permute the matrix rows. If \(1\), it will permute the matrix columns.

Notes

This follows the same notation used in sympy.matrices.matrixbase.MatrixBase.permute().

Examples

>>> from sympy import Matrix, MatrixPermute
>>> from sympy.combinatorics import Permutation

Permuting the matrix rows:

>>> p = Permutation(1, 2, 0)
>>> A = Matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
>>> B = MatrixPermute(A, p, axis=0)
>>> B.as_explicit()
Matrix([
[4, 5, 6],
[7, 8, 9],
[1, 2, 3]])

Permuting the matrix columns:

>>> B = MatrixPermute(A, p, axis=1)
>>> B.as_explicit()
Matrix([
[2, 3, 1],
[5, 6, 4],
[8, 9, 7]])
class sympy.matrices.expressions.Identity(n)[source]

The Matrix Identity I - multiplicative identity

Examples

>>> from sympy import Identity, MatrixSymbol
>>> A = MatrixSymbol('A', 3, 5)
>>> I = Identity(3)
>>> I*A
A
class sympy.matrices.expressions.ZeroMatrix(m, n)[source]

The Matrix Zero 0 - additive identity

Examples

>>> from sympy import MatrixSymbol, ZeroMatrix
>>> A = MatrixSymbol('A', 3, 5)
>>> Z = ZeroMatrix(3, 5)
>>> A + Z
A
>>> Z*A.T
0
class sympy.matrices.expressions.CompanionMatrix(poly)[source]

A symbolic companion matrix of a polynomial.

Examples

>>> from sympy import Poly, Symbol, symbols
>>> from sympy.matrices.expressions import CompanionMatrix
>>> x = Symbol('x')
>>> c0, c1, c2, c3, c4 = symbols('c0:5')
>>> p = Poly(c0 + c1*x + c2*x**2 + c3*x**3 + c4*x**4 + x**5, x)
>>> CompanionMatrix(p)
CompanionMatrix(Poly(x**5 + c4*x**4 + c3*x**3 + c2*x**2 + c1*x + c0,
x, domain='ZZ[c0,c1,c2,c3,c4]'))
class sympy.matrices.expressions.MatrixSet(n, m, set)[source]

MatrixSet represents the set of matrices with shape = (n, m) over the given set.

Examples

>>> from sympy.matrices import MatrixSet
>>> from sympy import S, I, Matrix
>>> M = MatrixSet(2, 2, set=S.Reals)
>>> X = Matrix([[1, 2], [3, 4]])
>>> X in M
True
>>> X = Matrix([[1, 2], [I, 4]])
>>> X in M
False

Block Matrices

Block matrices allow you to construct larger matrices out of smaller sub-blocks. They can work with MatrixExpr or ImmutableMatrix objects.

class sympy.matrices.expressions.blockmatrix.BlockMatrix(*args, **kwargs)[source]

A BlockMatrix is a Matrix comprised of other matrices.

The submatrices are stored in a SymPy Matrix object but accessed as part of a Matrix Expression

>>> from sympy import (MatrixSymbol, BlockMatrix, symbols,
...     Identity, ZeroMatrix, block_collapse)
>>> n,m,l = symbols('n m l')
>>> X = MatrixSymbol('X', n, n)
>>> Y = MatrixSymbol('Y', m, m)
>>> Z = MatrixSymbol('Z', n, m)
>>> B = BlockMatrix([[X, Z], [ZeroMatrix(m,n), Y]])
>>> print(B)
Matrix([
[X, Z],
[0, Y]])
>>> C = BlockMatrix([[Identity(n), Z]])
>>> print(C)
Matrix([[I, Z]])
>>> print(block_collapse(C*B))
Matrix([[X, Z + Z*Y]])

Some matrices might be comprised of rows of blocks with the matrices in each row having the same height and the rows all having the same total number of columns but not having the same number of columns for each matrix in each row. In this case, the matrix is not a block matrix and should be instantiated by Matrix.

>>> from sympy import ones, Matrix
>>> dat = [
... [ones(3,2), ones(3,3)*2],
... [ones(2,3)*3, ones(2,2)*4]]
...
>>> BlockMatrix(dat)
Traceback (most recent call last):
...
ValueError:
Although this matrix is comprised of blocks, the blocks do not fill
the matrix in a size-symmetric fashion. To create a full matrix from
these arguments, pass them directly to Matrix.
>>> Matrix(dat)
Matrix([
[1, 1, 2, 2, 2],
[1, 1, 2, 2, 2],
[1, 1, 2, 2, 2],
[3, 3, 3, 4, 4],
[3, 3, 3, 4, 4]])
LDUdecomposition()[source]

Returns the Block LDU decomposition of a 2x2 Block Matrix

Returns:

(L, D, U) : Matrices

L : Lower Diagonal Matrix D : Diagonal Matrix U : Upper Diagonal Matrix

Raises:

ShapeError

If the block matrix is not a 2x2 matrix

NonInvertibleMatrixError

If the matrix “A” is non-invertible

Examples

>>> from sympy import symbols, MatrixSymbol, BlockMatrix, block_collapse
>>> m, n = symbols('m n')
>>> A = MatrixSymbol('A', n, n)
>>> B = MatrixSymbol('B', n, m)
>>> C = MatrixSymbol('C', m, n)
>>> D = MatrixSymbol('D', m, m)
>>> X = BlockMatrix([[A, B], [C, D]])
>>> L, D, U = X.LDUdecomposition()
>>> block_collapse(L*D*U)
Matrix([
[A, B],
[C, D]])
LUdecomposition()[source]

Returns the Block LU decomposition of a 2x2 Block Matrix

Returns:

(L, U) : Matrices

L : Lower Diagonal Matrix U : Upper Diagonal Matrix

Raises:

ShapeError

If the block matrix is not a 2x2 matrix

NonInvertibleMatrixError

If the matrix “A” is non-invertible

Examples

>>> from sympy import symbols, MatrixSymbol, BlockMatrix, block_collapse
>>> m, n = symbols('m n')
>>> A = MatrixSymbol('A', n, n)
>>> B = MatrixSymbol('B', n, m)
>>> C = MatrixSymbol('C', m, n)
>>> D = MatrixSymbol('D', m, m)
>>> X = BlockMatrix([[A, B], [C, D]])
>>> L, U = X.LUdecomposition()
>>> block_collapse(L*U)
Matrix([
[A, B],
[C, D]])
UDLdecomposition()[source]

Returns the Block UDL decomposition of a 2x2 Block Matrix

Returns:

(U, D, L) : Matrices

U : Upper Diagonal Matrix D : Diagonal Matrix L : Lower Diagonal Matrix

Raises:

ShapeError

If the block matrix is not a 2x2 matrix

NonInvertibleMatrixError

If the matrix “D” is non-invertible

Examples

>>> from sympy import symbols, MatrixSymbol, BlockMatrix, block_collapse
>>> m, n = symbols('m n')
>>> A = MatrixSymbol('A', n, n)
>>> B = MatrixSymbol('B', n, m)
>>> C = MatrixSymbol('C', m, n)
>>> D = MatrixSymbol('D', m, m)
>>> X = BlockMatrix([[A, B], [C, D]])
>>> U, D, L = X.UDLdecomposition()
>>> block_collapse(U*D*L)
Matrix([
[A, B],
[C, D]])
schur(mat='A', generalized=False)[source]

Return the Schur Complement of the 2x2 BlockMatrix

Parameters:

mat : String, optional

The matrix with respect to which the Schur Complement is calculated. ‘A’ is used by default

generalized : bool, optional

If True, returns the generalized Schur Component which uses Moore-Penrose Inverse

Returns:

M : Matrix

The Schur Complement Matrix

Raises:

ShapeError

If the block matrix is not a 2x2 matrix

NonInvertibleMatrixError

If given matrix is non-invertible

Examples

>>> from sympy import symbols, MatrixSymbol, BlockMatrix
>>> m, n = symbols('m n')
>>> A = MatrixSymbol('A', n, n)
>>> B = MatrixSymbol('B', n, m)
>>> C = MatrixSymbol('C', m, n)
>>> D = MatrixSymbol('D', m, m)
>>> X = BlockMatrix([[A, B], [C, D]])

The default Schur Complement is evaluated with “A”

>>> X.schur()
-C*A**(-1)*B + D
>>> X.schur('D')
A - B*D**(-1)*C

Schur complement with non-invertible matrices is not defined. Instead, the generalized Schur complement can be calculated which uses the Moore-Penrose Inverse. To achieve this, \(generalized\) must be set to \(True\)

>>> X.schur('B', generalized=True)
C - D*(B.T*B)**(-1)*B.T*A
>>> X.schur('C', generalized=True)
-A*(C.T*C)**(-1)*C.T*D + B

References

[R608]

Wikipedia Article on Schur Component : https://en.wikipedia.org/wiki/Schur_complement

transpose()[source]

Return transpose of matrix.

Examples

>>> from sympy import MatrixSymbol, BlockMatrix, ZeroMatrix
>>> from sympy.abc import m, n
>>> X = MatrixSymbol('X', n, n)
>>> Y = MatrixSymbol('Y', m, m)
>>> Z = MatrixSymbol('Z', n, m)
>>> B = BlockMatrix([[X, Z], [ZeroMatrix(m,n), Y]])
>>> B.transpose()
Matrix([
[X.T,  0],
[Z.T, Y.T]])
>>> _.transpose()
Matrix([
[X, Z],
[0, Y]])
class sympy.matrices.expressions.blockmatrix.BlockDiagMatrix(*mats)[source]

A sparse matrix with block matrices along its diagonals

Examples

>>> from sympy import MatrixSymbol, BlockDiagMatrix, symbols
>>> n, m, l = symbols('n m l')
>>> X = MatrixSymbol('X', n, n)
>>> Y = MatrixSymbol('Y', m, m)
>>> BlockDiagMatrix(X, Y)
Matrix([
[X, 0],
[0, Y]])

Notes

If you want to get the individual diagonal blocks, use get_diag_blocks().

get_diag_blocks()[source]

Return the list of diagonal blocks of the matrix.

Examples

>>> from sympy import BlockDiagMatrix, Matrix
>>> A = Matrix([[1, 2], [3, 4]])
>>> B = Matrix([[5, 6], [7, 8]])
>>> M = BlockDiagMatrix(A, B)

How to get diagonal blocks from the block diagonal matrix:

>>> diag_blocks = M.get_diag_blocks()
>>> diag_blocks[0]
Matrix([
[1, 2],
[3, 4]])
>>> diag_blocks[1]
Matrix([
[5, 6],
[7, 8]])
sympy.matrices.expressions.blockmatrix.block_collapse(expr)[source]

Evaluates a block matrix expression

>>> from sympy import MatrixSymbol, BlockMatrix, symbols, Identity, ZeroMatrix, block_collapse
>>> n,m,l = symbols('n m l')
>>> X = MatrixSymbol('X', n, n)
>>> Y = MatrixSymbol('Y', m, m)
>>> Z = MatrixSymbol('Z', n, m)
>>> B = BlockMatrix([[X, Z], [ZeroMatrix(m, n), Y]])
>>> print(B)
Matrix([
[X, Z],
[0, Y]])
>>> C = BlockMatrix([[Identity(n), Z]])
>>> print(C)
Matrix([[I, Z]])
>>> print(block_collapse(C*B))
Matrix([[X, Z + Z*Y]])