A simulation I'm doing requires me to calculate the partial trace of a large density matrix. I am trying to calculate it using tools from numpy, but my code seems to be having some problems. For background, let me explain the arrays I am interested in a little more, and the way I'm defining the partial trace. Then, I will give the code I have and the errors I am getting.

First, the partial trace. If I have a tensor product of vector spaces

$$ V = \prod_i V_i$$

and a linear operator $T: V \to V$, then given a basis I can store all the information about $T$ as a multidimensional array $T_{out_1,..,out_n,in_1,..,in_n}$ where $T_{out_1,..,out_n,in_1,..,in_n}$ is the $v_{out_1} \otimes v_{out_2}\otimes \dots \otimes v_{out_n}$ component of $T(v_{in_1} \otimes \dots \otimes v_{in_n} )$. (Compare this to a matrix in a basis where $M_{ij}$ is the $i^{th}$ component of $M(v_j)$.)

The partial trace over some of these indices is a new operator given as a multidimensional array defined by $\widetilde{T}_{kept_{out},kept_{in}} = \sum_{traced} T_{keep_{out},traced, keep_{in},traced}$, where $kept_{in}$, $kept_{out}$, and $traced$ are all multi indices referring to a subset of basis states. The sum is taken over all combinations of indices in the traced set.

My code for computing this in numpy is:

def trace_index(array,i):
Given an array and an index i, traces out the index.
    n = len(array.shape)
    tot = list(range(n))
    tot[n//2+i-1] = i-1
    return np.einsum(array,list(tot))

def partial_trace(array,indices):
Given an array and a list of indices to trace out, traces out the indices

    in_sorted = sorted(indices,reverse=True) 
    cur_trace = array
    for i in in_sorted:
        cur_trace = trace_index(cur_trace,i)
    return cur_trace

I trace them in the descending order eg. 5,4,... because then I can apply trace_index to the indices one at a time. If I trace index 5 and then index 4, index 4 is still index 4 after tracing index 5. If I do it the other way, after I traced index 4, there is no index 5.

This code seems to work well for small cases, but for larger ones I

ValueError: invalid subscript '}' in einstein sum subscripts string, subscripts must be letters

So, my question is this: Is there a better way to do what I am trying to do than what I am doing?

  • $\begingroup$ I am just commenting because 1. I am bad at maths (I don't understand very much your explanation) 2. I am too lazy to work out what the functions you wrote are doing. However, you might want to have a look at numpy.trace(). Additionally, if you are doing something like open quantum systems and your system modes are labeled by adjacent indexes in the density matrix, you can use array slicing to trace only the states of the system and leave out the rest. $\endgroup$
    – Anon
    Commented Jul 26, 2017 at 7:45

3 Answers 3


I didn't follow exactly your notation so I can't say for sure what this would look like for your example, but two suggestions:

  • if you really want your life made simple, check out qutip, the Quantum Toolbox in Python. It has classes specifically for quantum operators and state vectors, supports tensor product of operators in different spaces and keeps track of the original shape; as a result, it can quite easily carry out partial traces as well.
  • if you want to go on with numpy (and you may have reason to: in at least one occasion in the past I have found out that qutip mysteriously killed performance in a computation when changing basis to an operator), then look into .reshape. Specifically, if your tensor space is of size $n\otimes m$, you'll want to use A.reshape((n, m, n, m)) for an operator A. This should preserve order and split the different parts along different axes, which then you can sum or trace along as you need.
  • 4
    $\begingroup$ More specifically, with np, the partial trace with respect to th first subsystem (of dimension $n$) is np.trace(A.reshape(n,m,n,m), axis1=0, axis2=2), and the trace with respect to the second subsystem (of dimension $m$) is np.trace(A.reshape(n,m,n,m), axis1=1, axis2=3) $\endgroup$ Commented May 22, 2019 at 8:35

I don't have enough reputation to comment, but I wanted to give a basic complement of gIS's answer.

Simply put, if we have the decomposition over $V=V_1\otimes V_2$, which we represent with, say, the reshaping [2,4,2,4] why do we einsum over the first and third indices for trace over $V_1$ and second and fourth for trace over $V_2$? Maybe this is obvious for most of you, but it wasn't for me so here is how I reasoned about it, in case someone else also has doubts about how to connect reshape with the tensor product.

Take a matrix $M$ which is as a linear function over $V$, $M:V\to V$. Then given a basis $\{\vert a \rangle \}$ we get the coefficients of the matrix as $\langle a \vert M \vert b \rangle=M_{ab}$. The full matrix can be written as

$$ M = \sum_{ab} M_{ab} \vert a \rangle \otimes \langle b \vert = \sum_{ab} M_{ab} \vert a \rangle \langle b \vert$$

where $\vert a \rangle$ is a vector on $V$ (basically a column vector) and $\langle b \vert$ is a member of the dual $V^*$, basically a row vector. We omit the tensor product where it's ovbious.

Now we insert the decomposition $V=V_1 \otimes V_2$ as ``splitting'' the basis vectors $\vert a \rangle \to \vert i \rangle \otimes \vert j \rangle$, $\langle b \vert \to \langle k \vert \otimes \langle l \vert$ where $0< i,k<\text{dim}V_1$,$0<j,l<\text{dim}V_2$. We also substitute the index a with the pair i,j and b with k,l This gives

$$ M = \sum_{ijkl} M_{i,j,k,l} \vert i \rangle_{V_1} \otimes \vert j \rangle_{V_2} \otimes \langle k \vert_{V_1} \otimes \langle l \vert_{V_2} = \sum_{ijkl} M_{i,j,k,l} \vert i \rangle \langle k \vert \otimes \vert j \rangle \langle l \vert $$

It's obvious from this that if we want to trace over $V_1$ we need to sum over the first and third, and if we want to trace out $V_2$ then we sum over the second and fourth index.


If you use qutip, partial trace operations are already built-in. For example, here is how you can compute the partial trace of a random density matrix over three qubits (that is, an hermitian, trace-1 matrix living in a tensor product space of dimensions $(2, 2, 2)$), and then trace out the last space:

import qutip
dm = qutip.rand_dm_hs(8, dims=[[2] * 3] * 2)
dm.ptrace([0, 1])

On the other hand, there are circumstances in which you may not want to use qutip. Say therefore that you have a matrix stored in a numpy array, which represents a tensor in a tensor product space width dimensions $(d_1,...,d_n)$. The matrix has therefore shape $(d_1\cdots d_n, d_1\cdots d_n)$.

A convenient way to compute the partial trace is, how the other answer suggests, to first reshape the matrix and then to sum the appropriate axes.

For example, suppose you have a matrix representing an object in a space $V= V_1\otimes V_2$, with $V_1$ of dimension $2$ and $V_2$ of dimension $4$. Here is how partial trace the first space:

import numpy as np
import qutip
# generate test matrix (using qutip for convenience)
dm = qutip.rand_dm_hs(8, dims=[[2, 4]] * 2).full()
# reshape to do the partial trace easily using np.einsum
reshaped_dm = dm.reshape([2, 4, 2, 4])
# compute the partial trace
reduced_dm = np.einsum('ijik->jk', reshaped_dm)

while to partial trace with respect to the second space you just change the last line to reduced_dm = np.einsum('jiki->jk', reshaped_dm).

Here is the same thing as above, including also a consistency check with the partial trace given by qutip:

import numpy as np
import qutip
# generate test matrix (using qutip for convenience)
dm = qutip.rand_dm_hs(8, dims=[[2, 4]] * 2).full()
# reshape to do the partial trace easily using np.einsum
reshaped_dm = dm.reshape([2, 4, 2, 4])
# partial trace the second space
reduced_dm = np.einsum('jiki->jk', reshaped_dm)
# check results with qutip
qutip_dm = qutip.Qobj(dm, dims=[[2, 4]] * 2)
reduced_dm_via_qutip = qutip_dm.ptrace([0]).full()
# check consistency of results
np.allclose(reduced_dm, reduced_dm_via_qutip)
  • $\begingroup$ I installed qutip per the instructions, but when Python tries to import the module, it errors out with a message "module 'numpy.__config__' has no attribute 'blas_opt_info'" $\endgroup$
    – Anna Naden
    Commented Jan 3, 2022 at 13:58
  • $\begingroup$ To get qutip to work, I had to install numpy version 1.18,1 "pip install numpy==1.18.1" $\endgroup$
    – Anna Naden
    Commented Jan 3, 2022 at 14:10

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.