Orbital rotations

The orbital rotation is a fundamental operation in the simulation of fermions. This page discusses orbital rotations and their properties.

Definitions of orbital rotation

There are two related ways to specify an orbital rotation, by providing either a unitary matrix or an antihermitian matrix. Generally, ffsim represents orbital rotations using unitary matrices.

Unitary matrix definition

An orbital rotation is described by an N×N unitary matrix U (here N is the number spatial orbitals), and we denote the corresponding operator as U. This operator has the following action on the fermionic creation operators {aσ,i}:

Uaσ,iU=jUjiaσ,j.

That is, aσ,i is mapped to a new operator bσ,i where bσ,i is a linear combination of the operators {aσ,i} with coefficients given by the i-th column of U. The {bσ,i} also satisfy the fermionic anticommutation relations, so they are creation operators in a rotated basis.

The mapping UU satisfies the important homomorphism property:

UU,U1U2U1U2

for any unitary matrices U, U1, and U2.

Antihermitian matrix definition

Instead of the unitary matrix U, an alternative description of an orbital rotation can be given by providing an antihermitian matrix K. In this case, the orbital rotation is defined as the unitary

U=σexp(ijKijaσ,iaσ,j)

It is a theorem that if K=log(U), then the two definitions coincide.

Independent orbital rotations for spin-up and spin-down

The definitions given above assume that the same orbital rotation applies to both the spin-up and spin-down orbitals. However, it is possible for each spin sector to have an independent orbital rotation, in which case a pair of unitary matrices (Uα,Uβ) needs to be specified. Letting Kσ=log(Uσ), the definitions above then become

Uaσ,iU=jUjiσaσ,j,U=σexp(ijKijσaσ,iaσ,j).

Application to time evolution by a quadratic Hamiltonian

A quadratic Hamiltonian is an operator of the form (here we restrict the definition to Hamiltonians with particle number and spin Z symmetry)

M=σ,ijMijaσ,iaσ,j

where M is a Hermitian matrix. Time evolution by this Hamiltonian is given by the operator eiMt, where t is the evolution time. Orbital rotations can be used to implement time evolution by a quadratic Hamiltonian using two different methods.

Method 1: Diagonalize the quadratic Hamiltonian

A quadratic Hamiltonian can always be rewritten as

M=U(σ,iλinσ,i)U

where the {λi} are real numbers called orbital energies, U is an orbital rotation, and nσ,i=aσ,iaσ,i is the occupation number operator. The {λi} and the unitary matrix U describing the orbital rotation are obtained from an eigendecomposition of M:

Mij=kλkUikUjk.

Time evolution by M can be implemented with the following steps:

  • Compute the orbital energies {λi} and the orbital rotation matrix U by performing an eigendecomposition of M.

  • Perform the orbital rotation U, which corresponds to the matrix U.

  • Perform time evolution by the operator σ,iλinσ,i.

  • Perform the orbital rotation U, which corresponds to the matrix U.

Method 2: Implement directly as an orbital rotation

The antihermitian matrix definition of an orbital rotation shows that the time evolution operator eiMt is itself an orbital rotation defined by K=iMt, or U=eiMt. Thus, the time evolution operator can be implemented directly as an orbital rotation.

The following code cell demonstrates the equivalence of the two methods by applying them to a randomly generated state vector and checking that the result is the same in both cases.

[1]:
import numpy as np
import scipy.linalg

import ffsim

rng = np.random.default_rng(12345)
norb = 6
nelec = (3, 3)

# Generate a random Hermitian matrix M to represent the quadratic Hamiltonian
mat = ffsim.random.random_hermitian(norb, seed=rng)

# Generate a random state vector and set the evolution time
vec = ffsim.random.random_state_vector(ffsim.dim(norb, nelec), seed=rng)
time = 1.0

# Apply time evolution via diagonalization
eigs, orbital_rotation = scipy.linalg.eigh(mat)
result1 = ffsim.apply_num_op_sum_evolution(
    vec, eigs, time=time, norb=norb, nelec=nelec, orbital_rotation=orbital_rotation
)

# Apply time evolution directly as an orbital rotation
evolution_mat = scipy.linalg.expm(-1j * time * mat)
result2 = ffsim.apply_orbital_rotation(vec, evolution_mat, norb=norb, nelec=nelec)

# Check that the results match
np.testing.assert_allclose(result1, result2)

Quantum circuit implementation

An orbital rotation can be implemented as a quantum circuit by decomposing it into Givens rotations. Under the Jordan-Wigner transformation, the circuit can be implemented as a dense brickwork pattern of two-qubit rotations, followed by a single layer of single-qubit phase gates. The following code cell constructs and displays a Qiskit circuit for implementing an orbital rotation. Notice that the circuit consists of two independent sub-circuits acting on each spin sector.

[2]:
from qiskit.circuit import QuantumCircuit, QuantumRegister

norb = 6
nelec = (3, 3)

# Generate a random orbital rotation
orbital_rotation = ffsim.random.random_unitary(norb, seed=rng)

# Create a quantum circuit that implements this orbital rotation
qubits = QuantumRegister(2 * norb, name="q")
circuit = QuantumCircuit(qubits)
circuit.append(ffsim.qiskit.OrbitalRotationJW(norb, orbital_rotation), qubits)

# Decompose the orbital rotation into basic gates before drawing
circuit.decompose().draw("mpl", scale=0.6)
[2]:
../_images/explanations_orbital-rotation_3_0.png

When an orbital rotation is applied to a single electronic configuration (in the qubit picture, a computational basis state), the quantum circuit implementation can be optimized to use fewer gates, resulting in a “diamond” pattern of gates rather than a dense “brickwork” pattern. For additional information, see this paper.

The following code cell constructs and displays an example of the optimized circuit.

[3]:
# Create a quantum circuit that applies an orbital rotation to the Hartree-Fock state
circuit = QuantumCircuit(qubits)
circuit.append(ffsim.qiskit.PrepareHartreeFockJW(norb, nelec), qubits)
circuit.append(ffsim.qiskit.OrbitalRotationJW(norb, orbital_rotation), qubits)

# Optimize the orbital rotation circuit
circuit = ffsim.qiskit.PRE_INIT.run(circuit)

# Decompose the orbital rotation into basic gates before drawing
circuit.decompose().draw("mpl", scale=0.6)
[3]:
../_images/explanations_orbital-rotation_5_0.png