Working with Subspace

Due to the strong Rydberg interactions, only one Rydberg excitation is allowed if the atoms are close to each other. We typically take this as the blockade radius, $R_b$, which is the distance for which the Rydberg interaction is the same as the Rabi frequency, $\Omega$ (see Rydberg Blockade). This is the so-called blockade constraint.

In Bloqade, we can take advantage of this effect by allowing users to run emulation in a truncated subspace, i.e., by throwing out states that violate the blockade constraint. This can help accelerate the simulation and enables simulation for a larger system size. In this section, we will show how to create a blockade subspace, create registers in the subspace, obtain the Hamiltonian matrix in the subspace, and run emulation in the subspace.

Note

Note that the blockade radius $R_b$ is the distance for which the Rydberg interaction is the same as the Rabi frequency, $\Omega$. For accurate simulation, however, it's not recommended to throw away the states that's close to the blockade radius. In other words, it's safer to set the subspace radius $R_s$ to be smaller than $R_b$, where we throw away the blockade violated states when the atoms are within $R_s$. For example, if we set $R_s = 1/2 * R_b$, we will be throwing away states that have interaction energies at least $2^6*\Omega$, which will be a good approximation. See the Rydberg Blockade page for recommendations on how to set $R_b$, $R_s$, and the atom lattice separation, $a$.

Create the Blockade Subspace

One can create a blockade subspace via the blockade_subspace method if we know the atomic positions:

For example, we can construct a blockade subspace of a square lattice using the code below:

using Bloqade
atoms = generate_sites(SquareLattice(), 3, 3, scale=5.1)
space = blockade_subspace(atoms, 5.2)
9-qubits 63-elements Subspace{Int64, Vector{Int64}}:
───┬────
  1│ 0
  2│ 1
  3│ 2
   ⋮│ ⋮
 61│ 337
 62│ 340
 63│ 341

We first created a $3*3$ square lattice with nearest neighbor atoms separated by $5.1$ μm. Then we have created a blockade subspace with the subspace radius, $R_s$, being $5.2$ μm. This means that if two atoms have a separation distance that is smaller than (or equal to) $5.2$ μm, then the blockade subspace does not contain states where both of them being in the Rydberg states. For the dictionary shown, the left is the new index of the states in the blockade subspace; in this case, there are 63 allowed states, which is much smaller than the full Hilbert space size 512. The vectors on the right correspond to the base-10 representations of the states in bitstrings.

Here space is of the type Subspace:

BloqadeExpr.SubspaceType
Subspace{S <: AbstractVector} <: AbstractSpace

A Dict-like object stores the mapping between subspace and full space.

source

Other than using atomic positions and the subspace radius, we can also use a graph to create a subspace. In this case, the subspace corresponds to the space composed by the independent sets of this graph. Bloqade has an explicit function for this, by using a graph as an input, and produces the subspace as the output. Here is an example code:

using Graphs

g = SimpleGraph(5)
edge_set = [(1,2), (1, 4), (2, 5), (3, 4)]
for (i,j) in edge_set
    add_edge!(g, i, j)
end
space = independent_set_subspace(g)
5-qubits 13-elements Subspace{Int64, Vector{Int64}}:
───┬───
  1│ 0
  2│ 1
  3│ 2
   ⋮│ ⋮
 11│ 20
 12│ 21
 13│ 24

Create Registers in the Subspace

One can create a register in the subspace by feeding the space object instead of an integer for the common register interfaces, e.g.:

julia> zero_state(space)SubspaceArrayReg{2, ComplexF64, Vector{ComplexF64}, Subspace{Int64, Vector{Int64}}}(5, ComplexF64[1.0 + 0.0im, 0.0 + 0.0im, 0.0 + 0.0im, 0.0 + 0.0im, 0.0 + 0.0im, 0.0 + 0.0im, 0.0 + 0.0im, 0.0 + 0.0im, 0.0 + 0.0im, 0.0 + 0.0im, 0.0 + 0.0im, 0.0 + 0.0im, 0.0 + 0.0im], Subspace{Int64, Vector{Int64}}(5, Dict(5 => 5, 16 => 9, 20 => 11, 24 => 13, 8 => 7, 17 => 10, 1 => 2, 0 => 1, 6 => 6, 4 => 4…), [0, 1, 2, 4, 5, 6, 8, 10, 16, 17, 20, 21, 24]))
julia> product_state(bit"000_000_001", space)SubspaceArrayReg{2, ComplexF64, Vector{ComplexF64}, Subspace{Int64, Vector{Int64}}}(5, ComplexF64[0.0 + 0.0im, 1.0 + 0.0im, 0.0 + 0.0im, 0.0 + 0.0im, 0.0 + 0.0im, 0.0 + 0.0im, 0.0 + 0.0im, 0.0 + 0.0im, 0.0 + 0.0im, 0.0 + 0.0im, 0.0 + 0.0im, 0.0 + 0.0im, 0.0 + 0.0im], Subspace{Int64, Vector{Int64}}(5, Dict(5 => 5, 16 => 9, 20 => 11, 24 => 13, 8 => 7, 17 => 10, 1 => 2, 0 => 1, 6 => 6, 4 => 4…), [0, 1, 2, 4, 5, 6, 8, 10, 16, 17, 20, 21, 24]))

Alternatively, if you have an existing state stored as a subtype of AbstractVector, you can also create the register using the constructor:

julia> state = rand(ComplexF64, length(space))13-element Vector{ComplexF64}:
 0.9311427370186367 + 0.6313726025480704im
 0.7152884504041968 + 0.24159636886983704im
 0.8956421665391127 + 0.6933887314800372im
 0.5163243344949098 + 0.7323159758803979im
 0.9823598183432223 + 0.5609870737870479im
 0.5969909964101042 + 0.9699929718487893im
  0.829027837952347 + 0.23134335992209998im
 0.3149980900486393 + 0.5763901458560168im
 0.7509677465609392 + 0.24922717660333793im
 0.3955529171029867 + 0.2222449127295565im
 0.3256120877388522 + 0.7434749007538914im
 0.9741634984231073 + 0.8393529313768457im
  0.198092351316961 + 0.14938803219124652im
julia> reg = SubspaceArrayReg(state, space)SubspaceArrayReg{2, ComplexF64, Vector{ComplexF64}, Subspace{Int64, Vector{Int64}}}(5, ComplexF64[0.9311427370186367 + 0.6313726025480704im, 0.7152884504041968 + 0.24159636886983704im, 0.8956421665391127 + 0.6933887314800372im, 0.5163243344949098 + 0.7323159758803979im, 0.9823598183432223 + 0.5609870737870479im, 0.5969909964101042 + 0.9699929718487893im, 0.829027837952347 + 0.23134335992209998im, 0.3149980900486393 + 0.5763901458560168im, 0.7509677465609392 + 0.24922717660333793im, 0.3955529171029867 + 0.2222449127295565im, 0.3256120877388522 + 0.7434749007538914im, 0.9741634984231073 + 0.8393529313768457im, 0.198092351316961 + 0.14938803219124652im], Subspace{Int64, Vector{Int64}}(5, Dict(5 => 5, 16 => 9, 20 => 11, 24 => 13, 8 => 7, 17 => 10, 1 => 2, 0 => 1, 6 => 6, 4 => 4…), [0, 1, 2, 4, 5, 6, 8, 10, 16, 17, 20, 21, 24]))

Obtain the Hamiltonian Matrix in the Subspace

The matrix projected in the subspace of a given Hamiltonian can be obtained via mat as well, e.g.:

julia> atoms = generate_sites(SquareLattice(), 3, 3, scale=5.1);
julia> space = blockade_subspace(atoms, 5.2);
julia> h1 = rydberg_h(atoms; Δ=2.0*2π, Ω=1.0*2π)nqubits: 9 + ├─ [+] ∑ 2π ⋅ 8.627e5.0/|r_i-r_j|^6 n_i n_j ├─ [+] 2π ⋅ 0.5 ⋅ ∑ σ^x_i └─ [-] 2π ⋅ 2.0 ⋅ ∑ n_i
julia> mat(h1, space)63×63 SparseMatrixCSC{ComplexF64, Int64} with 366 stored entries: ⡞⢍⡊⢄⠑⡀⠑⠄⠈⠢⡀⠀⠀⠀⠈⠢⡀⠀⠀⠀⠀⠑⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⠊⢌⢱⢖⠀⠈⠀⠐⠄⠀⠈⠀⠀⠀⠀⠀⠈⠢⡀⠀⠀⠀⠀⠑⢄⠀⠀⠀⠀⠀⠀⠀ ⠑⠠⡀⠀⢟⣵⠀⠀⠀⠀⠀⠑⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠑⢄⠀⠀⠀⠀⠀ ⠑⠄⢀⠀⠀⠀⡟⢍⡄⠀⠀⠀⠀⠑⠄⠀⠀⠀⠈⠢⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⠢⡀⠀⠁⠀⠀⠀⠉⢱⢖⠢⠢⠀⠢⡀⠀⠀⠀⠀⠀⠈⠀⠀⠀⠀⠀⠀⠢⡀⠀⠀⠀ ⠀⠈⠂⠀⢄⠀⠀⠀⠨⡂⠛⣤⢕⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠢⡀⠀ ⠀⠀⠀⠀⠀⠑⢄⠀⠠⡀⠑⠑⠛⣤⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠂ ⠢⡀⠀⠀⠀⠀⠀⠁⠀⠈⠀⠀⠀⠁⢱⢖⠢⢂⠐⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⠈⠢⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠨⢂⠛⣤⠅⠀⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠈⠀⠀⠢⡀⠀⠀⠀⠀⠀⠀⠐⢄⠁⠁⢱⢖⢂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⢄⠀⠀⠀⠀⠀⠀⠈⠂⠀⠀⠀⠀⠀⠀⠀⠀⠑⠈⠐⠛⣤⠤⡠⠀⢄⠀⢄⠀⠀⠀⠀ ⠀⠑⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡣⠱⢆⣑⠀⠢⠀⠑⠄⠀⠀ ⠀⠀⠀⠑⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢄⠑⠘⠑⣤⢄⠀⠀⠠⡀⠀ ⠀⠀⠀⠀⠀⠑⠀⠀⠠⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢄⠈⠂⠀⠑⠛⣤⠤⡠⡈⠂ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠢⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠑⠄⠀⡀⠀⡣⠱⢆⣐⠄ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠢⠈⠐⠜⠵⠇

Other Operations in the Subspace

All other operations in the subspace are the same as the fullspace case. For example, to run an emulation in the subspace, one just need to use the subspace register SubspaceArrayReg instead of the fullspace register ArrayReg. The rest of the code are the same:

reg = zero_state(space)
prob = SchrodingerProblem(reg, 1.0, h1)
emulate!(prob)
statevec(reg)
63-element Vector{ComplexF64}:
     0.1358530778240141 + 0.7346213875354803im
  -0.002545005997308238 + 0.03271936126364101im
   -0.09136564445428674 + 0.14709932261128347im
  -0.002545005997308252 + 0.03271936126364104im
    0.03640229360225794 - 0.047858838084333846im
   -0.09136564445428669 + 0.1470993226112836im
  -0.003520344699080374 - 0.13351188317217075im
  -0.009182823579441042 + 0.03774190634765454im
     0.2157281682094962 - 0.10378379632220018im
 -0.0010523784340717255 - 0.10411131382514599im
                        ⋮
    0.03640229360225794 - 0.04785883808433379im
    0.07737130708914187 - 0.03276442239812472im
 -0.0071961909523518336 - 0.022872905951169347im
    0.07737130708914186 - 0.0327644223981247im
     0.1336182222015095 - 0.056576079965067305im
  -0.002309159406646404 + 0.021944693000750554im
 -0.0018900334904653423 - 0.0019784910805732683im
 -0.0018900334904653534 - 0.001978491080573264im
  -0.002966284704680798 + 0.001604412976872172im

Measurements on the subspace register is the same as that in the full space.

Create Constrained Local Hamiltonians in the Subspace

Although we are able to emulate our Hamiltonian problem in the projected subspace, the long-range tail of the Rydberg interactions will be present in the subspace Hamiltonian. In certain cases, you may not want the long-range tail by only simulating a constrained short-range Hamiltonian, e.g. the PXP model. In this case, we can use Bloqade to easily deal with such problems for an arbitrary graph in an arbitrary dimension.

Let us take the PXP model in 1D as an example. We first create a 1D chain and then generate a subspace by projecting out states that have nearest-neighbor interactions.

atoms = generate_sites(ChainLattice(), 10, scale=5.1)
space = blockade_subspace(atoms, 5.2)
register = product_state(bit"0101010101", space)
h = 2π * 4.0 * SumOfX(length(atoms)) - 2π * 1.0 * SumOfN(length(atoms))
prob = SchrodingerProblem(register, 0.2, h)
emulate!(prob)
SchrodingerProblem:
  register info:
    type: SubspaceArrayReg{2, ComplexF64, Vector{ComplexF64}, Subspace{Int64, Vector{Int64}}}
    storage size: 40 bytes

  time span (μs): (0.0, 0.2)

  equation: 
    storage size: 16.492 KiB
    expression:
nqubits: 10
+
├─ [scale: 25.132741228718345] ∑ σ^x_i
└─ [scale: -6.283185307179586] ∑ n_i


    algorithm: CompositeAlgorithm{Tuple{Vern9, Rodas5{0, false, Nothing, typeof(OrdinaryDiffEq.DEFAULT_PRECS), Val{:forward}, true, nothing}}, AutoSwitch{Vern9, Rodas5{0, false, Nothing, typeof(OrdinaryDiffEq.DEFAULT_PRECS), Val{:forward}, true, nothing}, Rational{Int64}, Int64}}((Vern9(true), Rodas5{0, false, Nothing, typeof(OrdinaryDiffEq.DEFAULT_PRECS), Val{:forward}, true, nothing}(nothing, OrdinaryDiffEq.DEFAULT_PRECS)), AutoSwitch{Vern9, Rodas5{0, false, Nothing, typeof(OrdinaryDiffEq.DEFAULT_PRECS), Val{:forward}, true, nothing}, Rational{Int64}, Int64}(Vern9(true), Rodas5{0, false, Nothing, typeof(OrdinaryDiffEq.DEFAULT_PRECS), Val{:forward}, true, nothing}(nothing, OrdinaryDiffEq.DEFAULT_PRECS), 10, 3, 9//10, 9//10, 2, false, 5))
  options:
    save_everystep: false
    save_start: false
    save_on: false
    dense: false

After creating the subspace, we have built a Hamiltonian by explicitly summing up the Rabi frequency term and the detuning term by using SumOfX and SumOfN respectively. In this way, we have created a local constraint Hamiltonian (without the long-range interaction tail). Futhermore, if we want to emulate quantum dynamics under this Hamiltonian, we just need to create a subspace register and emulate the system under the created Hamiltonian.