State vectors and gates

This page explains how state vectors are represented in ffsim and how you apply gates to them.

State vectors

In ffsim, state vectors are represented as plain one-dimensional NumPy arrays. The length of a state vector is determined by the number of orbitals in the system and their occupancies. The number of \(\alpha\) (spin up) electrons and the number of \(\beta\) (spin down) electrons are each assumed to be fixed. For a system with \(N\) spatial orbitals, \(N_\alpha\) electrons with spin \(\alpha\), and \(N_\beta\) electrons with spin \(\beta\), the state vector has length

\[{N \choose N_\alpha} \times {N \choose N_\beta}.\]

You can contrast this expression with a generic quantum circuit simulator, for which a state vector would have length \(2^{2N}\).

ffsim includes convenient functions to calculate the full dimension of the vector space as well as the dimensions of the individual spin subsystems.

[1]:
import ffsim

# Let's use 3 spatial orbitals with 2 alpha electrons and 1 beta electron.
norb = 3
nelec = (2, 1)

# Get the dimension of the vector space.
dim = ffsim.dim(norb, nelec)

# We can also get the dimensions of the alpha- and beta- spaces separately.
dim_a, dim_b = ffsim.dims(norb, nelec)

# The full dimension is the product of alpha- and beta- dimensions.
assert dim == dim_a * dim_b

print(f"The dimension of the vector space is {dim}.")
print(f"On the other hand, 2 ** (2 * norb) = {2 ** (2 * norb)}.")
The dimension of the vector space is 9.
On the other hand, 2 ** (2 * norb) = 64.

Each entry of the state vector is associated with an electronic configuration, which can be labeled by the concatenation of two bitstrings, \(\lvert s_\beta s_\alpha \rangle\), where \(s_\alpha\) is a bitstring of length \(N\) with Hamming weight \(N_\alpha\), and \(s_\beta\) is a bitstring of length \(N\) with Hamming weight \(N_\beta\). A full specification of the state vector representation requires a choice of ordering for the bitstrings. ffsim uses the same ordering as PySCF’s FCI module, pyscf.fci. You can use the addresses_to_strings function in ffsim to convert a list of state vector indices to the corresponding bitstrings.

[2]:
strings = ffsim.addresses_to_strings(
    range(dim), norb=norb, nelec=nelec, bitstring_type=ffsim.BitstringType.STRING
)

strings
[2]:
['001011',
 '010011',
 '100011',
 '001101',
 '010101',
 '100101',
 '001110',
 '010110',
 '100110']

The first electronic configuration always has the electrons occupying the lowest-numbered orbitals (note that the bit positions increase from right to left). When using molecular orbitals, this configuration corresponds to the Hartree-Fock state. ffsim includes a convenient function to construct the Hartree-Fock state, which is just a vector with a 1 in its first position and 0 everywhere else:

[3]:
vec = ffsim.hartree_fock_state(norb, nelec)

vec
[3]:
array([1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j,
       0.+0.j])

It is sometimes convenient to represent the state vector as a matrix whose rows are indexed by the spin \(\alpha\) part of the bitstring and whose columns are indexed by the spin \(\beta\) part. To convert the vector into this representation, simply reshape it:

[4]:
mat = vec.reshape((dim_a, dim_b))

mat
[4]:
array([[1.+0.j, 0.+0.j, 0.+0.j],
       [0.+0.j, 0.+0.j, 0.+0.j],
       [0.+0.j, 0.+0.j, 0.+0.j]])

Gates

In ffsim, you apply a unitary gate to a state vector by calling a function whose name begins with apply_. For example, the function for applying an orbital rotation is called apply_orbital_rotation. The first argument to the function is always the state vector itself. The number of orbitals, as well as the number of alpha and beta electrons, are passed as the arguments norb and nelec. See the API reference for the full list of supported gates and their definitions (search for ffsim.apply_).

As an example, the following code cell generates a random orbital rotation (represented by an \(N \times N\) unitary matrix) and applies it to the Hartree-Fock state vector we created previously.

[5]:
# Generate a random orbital rotation.
orbital_rotation = ffsim.random.random_unitary(norb, seed=1234)

# Apply the orbital rotation to the state vector.
rotated_vec = ffsim.apply_orbital_rotation(
    vec, orbital_rotation, norb=norb, nelec=nelec
)

rotated_vec
[5]:
array([ 0.23611476+0.03101213j, -0.06273307+0.1102529j ,
        0.09723851+0.36730125j,  0.13113848+0.17276745j,
       -0.11157654+0.02998708j, -0.17558331+0.29821173j,
       -0.20881506-0.33731417j,  0.20835741-0.03525116j,
        0.3714141 -0.51253171j])

As a further demonstration, let’s apply a few more gates to the rotated state vector.

[6]:
# Apply some more gates
rotated_vec = ffsim.apply_on_site_interaction(
    rotated_vec, 0.1, 2, norb=norb, nelec=nelec
)
rotated_vec = ffsim.apply_tunneling_interaction(
    rotated_vec, 0.1, (0, 1), norb=norb, nelec=nelec
)

rotated_vec
[6]:
array([ 0.22392824+0.02459434j, -0.06551571+0.13327423j,
        0.09723851+0.36730125j,  0.15828306+0.13957088j,
       -0.12204343+0.06677383j, -0.15624569+0.31980058j,
       -0.21928194-0.30052742j,  0.23550198-0.06844774j,
        0.39075171-0.49094286j])

Treating spinless fermions

Many functions in ffsim support spinless fermions, which are not distinguished into spin \(\alpha\) and spin \(\beta\). With spinless fermions, the nelec variable is simply an integer, rather than a pair of integers. The following code cell gives an example of creating a spinless state vector and applying a gate to it.

[7]:
norb = 3
nelec = 2

vec = ffsim.hartree_fock_state(norb, nelec)
orbital_rotation = ffsim.random.random_unitary(norb, seed=1234)
rotated_vec = ffsim.apply_orbital_rotation(
    vec, orbital_rotation, norb=norb, nelec=nelec
)

rotated_vec
[7]:
array([-0.4390672 -0.1561685j , -0.18007105-0.38435478j,
        0.26121865+0.73105542j])