Skip to content

Hamiltonians

The Hamiltonian is the foundation of all quantum system modelling in QruiseML. It encodes the physics of your system — the energy levels of qubits, their interactions, and the effects of external drives.

Everything else in QruiseML builds on the Hamiltonian: the time-evolution equations you solve, the drives you design, the problems you define, and the optimisation you run. A well-defined Hamiltonian is therefore essential for accurate simulation and effective control.

This page walks you through how to define your system Hamiltonian in QruiseML.


In QruiseML, the Hamiltonian, \(H\), is defined as the sum of a stationary (time-independent) term and a time-dependent term:

\[ H(t; p) = H_0 + \sum_k f_k(t; \{p\}_k) H_k. \]

\(H_0\) defines the stationary part, which specifies the intrinsic energy levels of the system. The sum term defines the time-dependent part, where \(H_k\) are Hermitian operators describing the coupling of the system to the \(k^\text{th}\) drive channel, and \(f_k(t; \{p\}_k)\) are the corresponding drive functions, whose parameters \(\{p\}_k\) collectively form the complete parameter set \(p\).

Defining the Hamiltonian

In QruiseML, you specify your Hamiltonian with the Hamiltonian() class:

from qruise.toolset import Hamiltonian

H = Hamiltonian(H0, Ht)
  • H0 is a single time-independent operator, so you can scale it directly with a constant, i.e. prefactor * operator.
  • Ht must be provided as (drive, operator) pairs, for example [(drive1, operator1), (drive2, operator2), ...].

For example, let's assume the following Hamiltonian:

\[ H = \frac{\omega}{2} \sigma_z + c_x(t)\sigma_x, \]

where \(\omega\) is your qubit resonance frequency, \(\sigma_{x,z}\) are the Pauli-X and -Z operators and \(c_x(t)\) is your time-dependent drive term in the \(x\) direction.

Assuming you've already defined your drive as drive_x, your code would look like this:

import qutip as qt
from qruise.toolset import Hamiltonian

omega = 2.0  # qubit frequency

H = qr.Hamiltonian(omega / 2 * qt.sigmaz(), [(drive_x, qt.sigmax())])

You can see that QruiseML uses QuTiP operators, so you can reuse what you already know. Alternatively, you can pass your own operators as NumPy arrays.

If there's no stationary term in your Hamiltonian, you can simply pass None:

H = qr.Hamiltonian(None, [(drive_x, qt.sigmax())])

Tip

You can learn how to define your drives here.

Operators

Hamiltonians are built from quantum operators. In QruiseML, we typically use QuTiP operators.

Ladder operators

The ladder operators, also known as the creation and annihilation operators, raise or lower the excitation number of a quantum state, respectively. They are fundamental in describing oscillators, multi-level systems, and qubits.

They act on Fock states \(|n\rangle\), which are number states with exactly \(n\) excitations (\(n=0,1,2,...\)) with the annihilation, \(a\), and creation, \(a^\dagger\), operators given by

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

and

\[ a^\dagger|n\rangle = \sqrt{n+1}|n+1\rangle. \]

For an \(N\)-level system, you can define the annihilation and creation operators like so:

# annihilation operator
a = qt.destroy(N)

# creation operator
adag = a.dag()

Pauli operators

The Pauli operators are the special case of ladder operators for two-level systems, i.e. \(N=2\). They're denoted as \(\sigma_x\), \(\sigma_y\), and \(\sigma_z\), corresponding to rotations around the \(x\)-, \(y\)-, and \(z\)-axis of the Bloch sphere, respectively:

# Pauli operators
X = qt.sigmax()
Y = qt.sigmay()
Z = qt.sigmaz()

# Two-qubit coupling
XX = qt.tensor(qt.sigmax(), qt.sigmax())

Custom operators

You can also define custom operators using NumPy arrays or QuTiP objects:

import numpy as np
import qutip as qt

# Custom 2x2 operator (NumPy array)
op1 = np.array([[0, 1],
               [0, 0]])

# Custom 2x2 operator (QuTiP object)
op2 = qt.Qobj(np.array([[0, 0],
                        [1, 0]]))