9  Introduction to QuTiP

The QuTiP package can be imported with

import qutip
import numpy as np

It can also be imported with the command from qutip import *, that automatically imports all the QuTiP functions. However, here we use the first method, in order to explicitly see the QuTiP functions.

qutip.about()

QuTiP: Quantum Toolbox in Python
================================
Copyright (c) QuTiP team 2011 and later.
Current admin team: Alexander Pitchford, Nathan Shammah, Shahnawaz Ahmed, Neill Lambert, Eric Giguère, Boxi Li, Simon Cross, Asier Galicia, Paul Menczel, and Patrick Hopf.
Board members: Daniel Burgarth, Robert Johansson, Anton F. Kockum, Franco Nori and Will Zeng.
Original developers: R. J. Johansson & P. D. Nation.
Previous lead developers: Chris Granade & A. Grimsmo.
Currently developed through wide collaboration. See https://github.com/qutip for details.

QuTiP Version:      5.1.1
Numpy Version:      2.2.6
Scipy Version:      1.15.3
Cython Version:     3.1.1
Matplotlib Version: 3.10.3
Python Version:     3.13.4
Number of CPUs:     4
BLAS Info:          Generic
INTEL MKL Ext:      None
Platform Info:      Linux (x86_64)
Installation path:  /opt/hostedtoolcache/Python/3.13.4/x64/lib/python3.13/site-packages/qutip

Installed QuTiP family packages
-------------------------------

No QuTiP family packages installed.

================================================================================
Please cite QuTiP in your publication.
================================================================================
For your convenience a bibtex reference can be easily generated using `qutip.cite()`

9.1 Quantum Operators

Quantum operators play a crucial role in the formulation of quantum mechanics, representing physical observables and operations on quantum states. In QuTiP, operators are represented as Qobj instances, just like quantum states. This section introduces the creation and manipulation of quantum operators.

9.1.1 Creating Operators

Operators in quantum mechanics can represent measurements, such as position or momentum, and transformations, such as rotation. Let’s see how we can define some common operators in QuTiP.

The Annihilation Operator of the Quantum Harmonic oscillator

The harmonic oscillator is a fundamental model in quantum mechanics for understanding various physical systems. Its quantization leads to the concept of creation and annihilation operators, which respectively increase and decrease the energy of the system by one quantum of energy.

The annihilation operator, often denoted by \(\hat{a}\), acts on a quantum state to reduce its quantum number. The action of \(\hat{a}\) on a state \(|n\rangle\) is defined as:

\[ \hat{a} |n\rangle = \sqrt{n} |n-1\rangle \]

Here, \(|n\rangle\) represents a quantum state with \(n\) quanta of energy (also known as a Fock state), and \(\sqrt{n}\) is the normalization factor. The matrix representation of the annihilation operator in an \(d\)-dimensional Hilbert space is given by an upper triangular matrix with the square roots of natural numbers as its off-diagonal elements.

# Define the annihilation operator for d-dimensional Hilbert space
d = 7

a = qutip.destroy(d)

print("Annihilation operator (a) for d=7:")
a
Annihilation operator (a) for d=7:
Quantum object: dims=[[7], [7]], shape=(7, 7), type='oper', dtype=Dia, isherm=False
Qobj data =
[[0.         1.         0.         0.         0.         0.
  0.        ]
 [0.         0.         1.41421356 0.         0.         0.
  0.        ]
 [0.         0.         0.         1.73205081 0.         0.
  0.        ]
 [0.         0.         0.         0.         2.         0.
  0.        ]
 [0.         0.         0.         0.         0.         2.23606798
  0.        ]
 [0.         0.         0.         0.         0.         0.
  2.44948974]
 [0.         0.         0.         0.         0.         0.
  0.        ]]

Pauli Matrices

The Pauli matrices are fundamental in the study of quantum mechanics, representing the spin operators for a spin-1/2 particle and quantum two-level systems.

\[ \sigma_x = \begin{pmatrix} 0 & 1 \\ 1 & 0 \end{pmatrix}, \ \sigma_y = \begin{pmatrix} 0 & -i \\ i & 0 \end{pmatrix}, \ \sigma_z = \begin{pmatrix} 1 & 0 \\ 0 & -1 \end{pmatrix} \]

We can define these matrices in QuTiP as follows:

sigma_x = qutip.sigmax()
sigma_y = qutip.sigmay()
sigma_z = qutip.sigmaz()

print("Sigma X:")
display(sigma_x)
print("\n")
print("Sigma Y:")
display(sigma_y)
print("\n")
print("Sigma Z:")
sigma_z
Sigma X:
Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=CSR, isherm=True
Qobj data =
[[0. 1.]
 [1. 0.]]


Sigma Y:
Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=CSR, isherm=True
Qobj data =
[[0.+0.j 0.-1.j]
 [0.+1.j 0.+0.j]]


Sigma Z:
Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=CSR, isherm=True
Qobj data =
[[ 1.  0.]
 [ 0. -1.]]

9.1.2 Operator Functions and Operations

QuTiP supports various operations on operators, including addition, multiplication (both scalar and matrix), and the commutator. These operations are essential for constructing Hamiltonians, calculating observables, and more.

Example: Commutator of Pauli Matrices

The commutator of two operators \(A\) and \(B\) is defined as \([A, B] = AB - BA\). Let’s calculate the commutator of \(\sigma_x\) and \(\sigma_y\).

commutator_xy = qutip.commutator(sigma_x, sigma_y)
print("Commutator of Sigma X and Sigma Y:")
display(commutator_xy)
commutator_xy == 2j * sigma_z
Commutator of Sigma X and Sigma Y:
Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=CSR, isherm=False
Qobj data =
[[0.+2.j 0.+0.j]
 [0.+0.j 0.-2.j]]
True

9.2 Quantum States

Quantum states describe the state of a quantum system. In QuTiP, states are represented again as Qobj instances. This section focuses on the representation and manipulation of quantum states.

9.2.1 Fock States

The most basic quantum states are the fock states, often denoted as \(|n\rangle\) (with \(n \in \mathbb{N}\)). Let’s see how we can create these in QuTiP.

9.2.2 Superposition States

Quantum mechanics allows particles to be in a superposition of states. Let’s create a superposition state.

\[ \vert \psi \rangle = \frac{1}{\sqrt{2}} \left( \vert 0 \rangle + \vert 1 \rangle \right) \]

fock_0 = qutip.fock(d, 0)  # Fock state |0>
fock_1 = qutip.fock(d, 1)  # Fock state |1>

# Creating a superposition state
superposition_state = (fock_0 + fock_1).unit()  # Normalize the state

print("Superposition state:")
superposition_state
Superposition state:
Quantum object: dims=[[7], [1]], shape=(7, 1), type='ket', dtype=Dense
Qobj data =
[[0.70710678]
 [0.70710678]
 [0.        ]
 [0.        ]
 [0.        ]
 [0.        ]
 [0.        ]]

9.2.3 Coherent States

Coherent states in QuTiP represent quantum states closest to classical waves, defined as \[ |\alpha\rangle = e^{-|\alpha|^2/2} \sum_{n=0}^{\infty} \frac{\alpha^n}{\sqrt{n!}} |n\rangle \, , \]

with minimal uncertainty.

The coherent state is an eigenstate of the annihilation operator \[ \hat{a} \vert \alpha \rangle = \alpha \vert \alpha \rangle \]

Warning!

Remember that every Qobj lives in a truncated Hilbert space. If the \(\alpha\) value is too large, the state will become a non-physical state because it will touch the high energy levels of the truncated Hilbert space.

alpha = 0.8
coherent_state = qutip.coherent(d, alpha)

coherent_state
Quantum object: dims=[[7], [1]], shape=(7, 1), type='ket', dtype=Dense
Qobj data =
[[0.72614904]
 [0.58091919]
 [0.32861796]
 [0.1517784 ]
 [0.06073653]
 [0.02159193]
 [0.00767346]]

Let’s compute the fidelity between \(\vert \alpha \rangle\) and \(\hat{a} \vert \alpha \rangle / \alpha\).

qutip.fidelity(a * coherent_state / alpha, coherent_state)
np.float64(0.9999661194274998)

9.2.4 Spin States

qutip.spin_state(0.5, -1)
Quantum object: dims=[[2], [1]], shape=(2, 1), type='ket', dtype=Dense
Qobj data =
[[0.]
 [1.]]

9.2.5 Density Matrices

Quantum states can also be represented using density matrices, which are useful for describing mixed states.

Creating a Density Matrix

Let’s convert our superposition state into a density matrix.

# Creating a density matrix from a state
density_matrix = superposition_state * superposition_state.dag()  # Outer product

print("Density matrix of the superposition state:")
density_matrix
Density matrix of the superposition state:
Quantum object: dims=[[7], [7]], shape=(7, 7), type='oper', dtype=Dense, isherm=True
Qobj data =
[[0.5 0.5 0.  0.  0.  0.  0. ]
 [0.5 0.5 0.  0.  0.  0.  0. ]
 [0.  0.  0.  0.  0.  0.  0. ]
 [0.  0.  0.  0.  0.  0.  0. ]
 [0.  0.  0.  0.  0.  0.  0. ]
 [0.  0.  0.  0.  0.  0.  0. ]
 [0.  0.  0.  0.  0.  0.  0. ]]

9.2.6 Partial Trace

The partial trace over a subsystem, say \(B\), of a composite system \(AB\), mathematically expresses as “tracing out” \(B\), leaving the reduced state of \(A\). For a bipartite state \(\rho_{AB}\), the partial trace over \(B\) is:

\[ \text{Tr}_B(\hat{\rho}_{AB}) = \sum_{i \in \mathcal{H}_B} \langle i| \hat{\rho}_{AB} |i\rangle \]

where \(\{|i\rangle\}\) forms a complete basis for subsystem \(B\).

Let’s try it with an entangled Bell’s state between two qubits:

\[ \vert \phi^+ \rangle = \frac{1}{\sqrt{2}} \left( \vert 0, 0 \rangle + \vert 1, 1 \rangle \right) \]

# Bell state between two qubits
phi_plus = ( qutip.tensor(qutip.spin_state(1/2, -1), qutip.spin_state(1/2, -1)) + qutip.tensor(qutip.spin_state(1/2, 1), qutip.spin_state(1/2, 1)) ).unit()

# Reduced density matrix of the first qubit
rho_1 = qutip.ptrace(phi_plus, 1)
rho_1
Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=True
Qobj data =
[[0.5 0. ]
 [0.  0.5]]

We now apply the partial trace to a more complicated state, that is composed by two bosonic modes and two spins \(\vert j_1, m_1 \rangle\) and \(\vert j_2, m_2 \rangle\), with \(j_1 = 1\) and \(j_2 = \frac{1}{2}\), \(m_1 = 0\), and \(m_2 = 1\).

j1 = 1
j2 = 1/2
m1 = 0
m2 = 1


psi = qutip.tensor(qutip.fock(d, 3), qutip.fock(d, 1), qutip.spin_state(j1, 0), qutip.spin_state(j2, 1))

# Trace only the second spin state
rho_0 = qutip.ptrace(psi, [0, 1, 2])
display(rho_0)

# Trace only the first bosonic mode and the second spin state
rho_1 = qutip.ptrace(psi, [1, 2])
display(rho_1)

# Trace all except the second bosonic mode
rho_2 = qutip.ptrace(psi, [1])
rho_2
Quantum object: dims=[[7, 7, 3], [7, 7, 3]], shape=(147, 147), type='oper', dtype=Dense, isherm=True
Qobj data =
[[0. 0. 0. ... 0. 0. 0.]
 [0. 0. 0. ... 0. 0. 0.]
 [0. 0. 0. ... 0. 0. 0.]
 ...
 [0. 0. 0. ... 0. 0. 0.]
 [0. 0. 0. ... 0. 0. 0.]
 [0. 0. 0. ... 0. 0. 0.]]
Quantum object: dims=[[7, 3], [7, 3]], shape=(21, 21), type='oper', dtype=Dense, isherm=True
Qobj data =
[[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]]
Quantum object: dims=[[7], [7]], shape=(7, 7), type='oper', dtype=Dense, isherm=True
Qobj data =
[[0. 0. 0. 0. 0. 0. 0.]
 [0. 1. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0.]]

9.3 Eigenstates and Eigenvalues

The eigenstates and eigenvalues of a system or an operator provide crucial insights into its properties. Let’s explore how to calculate these in QuTiP.

# Example: Eigenstates and eigenvalues of Pauli Z
eigenvalues, eigenstates = sigma_z.eigenstates()

print("Eigenvalues of Sigma Z:")
display(eigenvalues)
print("\n")
print("Eigenstates of Sigma Z:")
display(eigenstates)
Eigenvalues of Sigma Z:
array([-1.,  1.])


Eigenstates of Sigma Z:
array([Quantum object: dims=[[2], [1]], shape=(2, 1), type='ket', dtype=Dense
       Qobj data =
       [[ 0.]
        [-1.]]                                                               ,
       Quantum object: dims=[[2], [1]], shape=(2, 1), type='ket', dtype=Dense
       Qobj data =
       [[-1.]
        [-0.]]                                                               ],
      dtype=object)

9.4 Computing Expectation Values

The expectation value of an operator provides insight into the average outcome of a quantum measurement. For a quantum state \(|\psi\rangle\) and an operator \(\hat{O}\), the expectation value is given by:

\[ \langle \hat{O} \rangle = \langle\psi| \hat{O} |\psi\rangle \]

Expectation values are crucial for predicting measurable quantities in quantum mechanics. Let’s compute the expectation value of the number operator \(\hat{n} = \hat{a}^\dagger \hat{a}\) for a coherent state, which represents a quantum state closest to a classical harmonic oscillator.

# Define the coherent state |psi> with alpha=2
alpha = 0.8
psi = qutip.coherent(d, alpha)

# Define the number operator n = a.dag() * a
n = a.dag() * a

# Compute the expectation value of n for the state |psi>
expectation_value_n = qutip.expect(n, psi)

print("Expectation value of the number operator for |psi>:")
display(expectation_value_n)
print("\n")
print("The squared modulus of alpha is:")
display(abs(alpha) ** 2)
Expectation value of the number operator for |psi>:
0.639996733025295


The squared modulus of alpha is:
0.6400000000000001

9.5 A complete example: the Quantum Harmonic Oscillator

In Appendix A we have already defined the quantum harmonic oscillator, which is a fundamental model in quantum mechanics. In the energy eigenbasis, the quantum harmonic oscillator is described by the Hamiltonian

\[ \hat{H} = \omega \hat{a}^\dagger \hat{a} \, , \]

where \(\omega\) is the resonance frequency and \(\hat{a}\) is the bosonic annihilation operator. In this basis, the Hamiltonian is indeed diagonal, with eigenvalues \(E_n = \omega n\), where \(n \in \mathbb{N}\).

N = 120 # Hilbert space cutoff
w = 1 # Resonance frequency of the harmonic oscillator

a = qutip.destroy(N)

H = w * a.dag() * a

H
Quantum object: dims=[[120], [120]], shape=(120, 120), type='oper', dtype=Dia, isherm=True
Qobj data =
[[  0.   0.   0. ...   0.   0.   0.]
 [  0.   1.   0. ...   0.   0.   0.]
 [  0.   0.   2. ...   0.   0.   0.]
 ...
 [  0.   0.   0. ... 117.   0.   0.]
 [  0.   0.   0. ...   0. 118.   0.]
 [  0.   0.   0. ...   0.   0. 119.]]

9.6 Passing in the position basis

From the classical point of view, we are used to describe the harmonic oscillator in terms of position and momentum. In quantum mechanics, we can also express the system in terms of the position and momentum operators, which are related to the annihilation and creation operators as follows (\(\hbar = 1\)):

\[\begin{align*} \hat{x} &= \frac{1}{\sqrt{2 m \omega}} \left( \hat{a} + \hat{a}^\dagger \right) \, , \\ \hat{p} &= i \sqrt{\frac{m \omega}{2}} \left( \hat{a}^\dagger - \hat{a} \right) \, . \end{align*}\]

We first check that

\[ \left[ \hat{x}, \hat{p} \right] = i \]

m = 0.5

x = (a + a.dag()) / np.sqrt(2 * m * w)
p = - 1j * (a - a.dag()) * np.sqrt(m * w / 2)

qutip.commutator(x, p)
Quantum object: dims=[[120], [120]], shape=(120, 120), type='oper', dtype=Dia, isherm=False
Qobj data =
[[0.  +1.j 0.  +0.j 0.  +0.j ... 0.  +0.j 0.  +0.j 0.  +0.j]
 [0.  +0.j 0.  +1.j 0.  +0.j ... 0.  +0.j 0.  +0.j 0.  +0.j]
 [0.  +0.j 0.  +0.j 0.  +1.j ... 0.  +0.j 0.  +0.j 0.  +0.j]
 ...
 [0.  +0.j 0.  +0.j 0.  +0.j ... 0.  +1.j 0.  +0.j 0.  +0.j]
 [0.  +0.j 0.  +0.j 0.  +0.j ... 0.  +0.j 0.  +1.j 0.  +0.j]
 [0.  +0.j 0.  +0.j 0.  +0.j ... 0.  +0.j 0.  +0.j 0.-119.j]]

We now numerically diagonalize the position operator \(\hat{x}\), such that \(\hat{U}^\dagger \hat{x} \hat{U}\) is diagonal. Then we plot the eigenstates of the quantum harmonic oscillator in the new basis, obtained with

\[ \vert \phi_n (x) \rangle = \hat{U}^\dagger \vert \psi_n \rangle \]

import matplotlib.pyplot as plt

E, T = x.eigenstates()

U = np.zeros((N, N)).astype(np.complex128)
for i in range(N):
    U[:,i] = T[i].full().flatten()

U = qutip.Qobj(U)

xlist = ( U.dag() * x * U ).diag()
dx = xlist[1] - xlist[0]

# Harmonic potential
V = w**2 / 2 * xlist**2

fig, ax = plt.subplots()

ax.plot(xlist, V, color="black", ls="--", lw=2)
for i in range(5):
    factor = 5 # The purpose of this factor is to only make more visible the states
    ax.plot(xlist, factor * np.abs( (U.dag() * qutip.fock(N, i)).full() )**2 + i * w + w/2, lw=2)

ax.set_xlabel(r"$x$")
ax.set_xlim(-5, 5)
ax.set_ylim(0, 5)

# Show in Quarto
plt.savefig("_tmp_fig.svg")
plt.close(fig)
SVG("_tmp_fig.svg")