Registers and Observables

Bloqade follows the register interface in Yao. It uses a register to represent a device and its internal quantum state. For Bloqade, the most commonly used register types are ArrayReg and SubspaceArrayReg. They both use a dense array to store the corresponding quantum state. The only difference is that SubspaceArrayReg also stores a subspace object. In this section, we will only cover how to create registers and perform operations on them in the full Hilbert space. For subspace operations, please refer to the subspace page. We will also discuss a few convenient wrappers on commonly used observables for Rydberg systems.

In the analog mode, we use the states $|g\rangle$ (ground state) and $|r\rangle$ (Rydberg state) to encode a qubit. To be consistent with the standard language of qubits, we refer the states $|g\rangle$ and $|r\rangle$ as $|0\rangle$ and $|1\rangle$ here.

Basic Interface

To create a register with all atoms being in the ground state $| 00..00 \rangle$, we can use the function zero_state by specifying the number of qubits:

julia> using Bloqade
julia> zero_state(5) # creates a 5-qubit registerArrayReg{2, ComplexF64, Array...} active qubits: 5/5 nlevel: 2

To create a more general product state in the computational basis, one can use the product_state function by inputting its bitstring:

julia> product_state(bit"10011")ArrayReg{2, ComplexF64, Array...}
    active qubits: 5/5
    nlevel: 2

where bit"10011" is a special Julia string literal defined for bitstrings.

One can also construct the ArrayReg or SubspaceArrayReg directly from arrays, e.g.:

julia> ArrayReg(rand(ComplexF64, 2^5))ArrayReg{2, ComplexF64, Array...}
    active qubits: 5/5
    nlevel: 2

For a subspace register, one can create in the following way:

julia> space = Subspace(5, [0, 2, 3, 7])5-qubits 4-elements Subspace{Int64, Vector{Int64}}:
──┬──
 1│ 0
 2│ 2
 3│ 3
 4│ 7
julia> state = rand(ComplexF64, length(space))4-element Vector{ComplexF64}: 0.2524838006188409 + 0.4174058326514425im 0.6144582045639959 + 0.5294612775164254im 0.8746901844872599 + 0.19122305772153525im 0.15011538435315053 + 0.8533670681342784im
julia> reg = SubspaceArrayReg(state, space)SubspaceArrayReg{2, ComplexF64, Vector{ComplexF64}, Subspace{Int64, Vector{Int64}}}(5, ComplexF64[0.2524838006188409 + 0.4174058326514425im, 0.6144582045639959 + 0.5294612775164254im, 0.8746901844872599 + 0.19122305772153525im, 0.15011538435315053 + 0.8533670681342784im], Subspace{Int64, Vector{Int64}}(5, Dict(0 => 1, 7 => 4, 2 => 2, 3 => 3), [0, 2, 3, 7]))

Here, $[0, 2, 3, 7]$ are base-10 integer representations of the corresponding states in bitstrings. For a more detailed guide on how to work in the subspace, please see subspace.

Operations on Registers

You can perform various operations on registers via the standard Yao register interface. This includes, e.g.,

  • applying operators on quantum states by using apply!,
  • measuring bitstrings with a projection on the quantum state by using measure!,
  • calculating the expectation value of certain observables by using expect,
  • inspecting the internal state of the register by using statevec.
julia> reg = rand_state(3)ArrayReg{2, ComplexF64, Array...}
    active qubits: 3/3
    nlevel: 2
julia> measure(reg; nshots=5)5-element Vector{DitStr{2, 3, Int64}}: 001 ₍₂₎ 100 ₍₂₎ 001 ₍₂₎ 100 ₍₂₎ 100 ₍₂₎
julia> expect(put(3,1=>X), reg)-0.22352751093736956 + 0.0im
julia> statevec(reg)8-element Vector{ComplexF64}: -0.22742162381801925 + 0.16255736045506963im 0.13150074492652392 - 0.44193310614233866im 0.17250489988014245 - 0.09926520822228996im -0.10678867565166161 + 0.334306613808023im -0.3185332998601297 - 0.5082878929952093im 0.2568307649008957 - 0.23551331816795892im -0.04675252207698388 + 0.24937234767119082im 0.02235138062379517 + 0.018983613801665078im
julia> apply!(reg, put(1=>X))ArrayReg{2, ComplexF64, Array...} active qubits: 3/3 nlevel: 2

For a more detailed introduction of the register interface, please refer to Yao:Registers.

Convenient Wrappers

Bloqade also provides a few convenient wrappers on some commonly used observables for Rydberg systems, including the Rydberg density and two-point correlation functions:

Bloqade.rydberg_densityFunction
rydberg_density(reg, i::Int) -> Real

Calculates the rydberg density at site i.

\[\langle n_i \rangle\]

source
rydberg_density(reg) -> Vector

Return the rydberg density at each site.

source

For example, if we want to measure the Rydberg density at each site or at a specific site, we can use the code below:

julia> reg = rand_state(10)ArrayReg{2, ComplexF64, Array...}
    active qubits: 10/10
    nlevel: 2
julia> n_each = rydberg_density(reg)10-element Vector{Float64}: 0.49628280915012984 0.4993909720161575 0.49393767907981057 0.5146475348934376 0.4863714043948183 0.5088836008112809 0.48239923962781606 0.5128402494029822 0.5374504986264643 0.5244533550755199
julia> n_2 = rydberg_density(reg, 2)0.4993909720161575

To access the two-point correlation functions, we can use the rydberg_corr function below:

Bloqade.rydberg_corrFunction
rydberg_corr([op=Op.n], reg) -> Matrix

Calculates the rydberg correlation matrix.

\[\langle \text{op}_i \text{op}_j \rangle\]

here op can be Op.n, X or Y.

Arguments

  • op: the correlation function, default is Op.n.
  • reg: required, the register object.
source

This function will output a matrix that stores the correlation function for each pair of sites:

julia> nn_corr = rydberg_corr(Op.n, reg)10×10 Matrix{ComplexF64}:
 0.496283+0.0im   0.23888+0.0im  …  0.268538+0.0im  0.247065+0.0im
  0.23888+0.0im  0.499391+0.0im     0.257034+0.0im   0.26708+0.0im
 0.242745+0.0im  0.249532+0.0im     0.264682+0.0im  0.257139+0.0im
 0.248271+0.0im  0.243865+0.0im     0.279917+0.0im  0.274097+0.0im
 0.232924+0.0im  0.244445+0.0im     0.258639+0.0im  0.258575+0.0im
  0.24716+0.0im  0.271766+0.0im  …  0.276086+0.0im  0.264539+0.0im
  0.24196+0.0im  0.241761+0.0im     0.255098+0.0im  0.247519+0.0im
 0.253965+0.0im  0.256637+0.0im     0.282446+0.0im  0.266228+0.0im
 0.268538+0.0im  0.257034+0.0im      0.53745+0.0im  0.283899+0.0im
 0.247065+0.0im   0.26708+0.0im     0.283899+0.0im  0.524453+0.0im

It is worth mentioning that besides Op.n, other single-site operators including the Pauli operators X, Y and Z can also be used.

To directly obtain the time-dependent Rydberg density under Hamiltonian evolution, we can use the highly-wrapped function get_average_rydberg_densities

Bloqade.get_average_rydberg_densitiesFunction
get_average_rydberg_densities(atoms, reg; [C=2π * 862690 * MHz*µm^6], Ω[, ϕ, Δ], [dt=1e-3 * μs])

Return average Rydberg densities throughout an evolution.

Arguments

  • atoms: a collection of atom positions.
  • reg: required, the register object.

Keyword Arguments

  • C: optional, default unit is MHz*µm^6, interation parameter, see also RydInteract.
  • Ω: optional, default unit is MHz, Rabi frequencies, divided by 2, see also SumOfX.
  • ϕ: optional, does not have unit, the phase, see SumOfXPhase.
  • Δ: optional, default unit is MHz, detuning parameter, see SumOfN.
  • dt: optional, default unit is μs, time step for the evolution.
  • solver: optional, default solver is Vern8(), the solver for the SchrodingerProblem, see SchrodingerProblem.
source

Create General Observables using Operator Expressions

Bloqade makes use of Yao's block system to represent operator expressions. For example, one can construct the Rydberg correlation operator as:

julia> corr(n, i, j) = chain(n, put(i=>Op.n), put(j=>Op.n))corr (generic function with 1 method)

You can thus create any kinds of quantum operators in this way and use it with the expect or measure function, e.g.:

julia> reg = rand_state(10)ArrayReg{2, ComplexF64, Array...}
    active qubits: 10/10
    nlevel: 2
julia> corr_XY = chain(10, put(2=>Op.X), put(4=>Op.Y))nqubits: 10 chain ├─ put on (2) │ └─ X └─ put on (4) └─ Y
julia> expect(corr_XY, reg)0.002458711964503575 + 3.0357660829594124e-18im

Because the Hamiltonian is also an operator expression, it can be used as an observable too:

julia> r = rand_state(5)ArrayReg{2, ComplexF64, Array...}
    active qubits: 5/5
    nlevel: 2
julia> pos = [(i, ) for i in 1:5]5-element Vector{Tuple{Int64}}: (1,) (2,) (3,) (4,) (5,)
julia> h = rydberg_h(pos; Ω=2π*0.1)nqubits: 5 + ├─ [+] ∑ 2π ⋅ 8.627e5.0/|r_i-r_j|^6 n_i n_j └─ [+] 2π ⋅ 0.05 ⋅ ∑ σ^x_i
julia> expect(h, r)6.013555818696128e6 + 0.0im

Please refer to the Hamiltonians to see other operators that are supported in building the Hamiltonian.

References

YaoArrayRegister.arrayregFunction
arrayreg(state; nbatch::Union{Integer,NoBatch}=NoBatch(), nlevel::Integer=2)

Create an array register, if nbatch is a integer, it will return a BatchedArrayReg.

arrayreg([T=ComplexF64], bit_str; nbatch=NoBatch())

Construct an array register from bit string literal. For bit string literal please read @bit_str.

Examples

julia> arrayreg(bit"1010")
ArrayReg{2, ComplexF64, Array...}
    active qubits: 4/4
    nlevel: 2

julia> arrayreg(ComplexF32, bit"1010")
ArrayReg{2, ComplexF32, Array...}
    active qubits: 4/4
    nlevel: 2
YaoAPI.apply!Function
apply!(register, block)

Apply a block (of quantum circuit) to a quantum register.

Note

to overload apply! for a new block, please overload the unsafe_apply! function with same interface. Then the apply! interface will do the size checks on inputs automatically.

Examples

julia> r = zero_state(2)
ArrayReg{2, ComplexF64, Array...}
    active qubits: 2/2
    nlevel: 2

julia> apply!(r, put(2, 1=>X))
ArrayReg{2, ComplexF64, Array...}
    active qubits: 2/2
    nlevel: 2

julia> measure(r;nshots=10)
10-element Vector{DitStr{2, 2, Int64}}:
 01 ₍₂₎
 01 ₍₂₎
 01 ₍₂₎
 01 ₍₂₎
 01 ₍₂₎
 01 ₍₂₎
 01 ₍₂₎
 01 ₍₂₎
 01 ₍₂₎
 01 ₍₂₎
YaoAPI.measure!Function
measure!([postprocess,] [operator, ]register[, locs]; rng=Random.GLOBAL_RNG)

Measure current active qudits or qudits at locs. If the operator is not provided, it will measure on the computational basis and collapse to a product state. Otherwise, the quantum state collapse to the subspace corresponds to the resulting eigenvalue of the observable.

Arguments

  • postprocess is the postprocessing method, it can be
    • NoPostProcess() (default).
    • ResetTo(config), reset to result state to config. It can not be used if operator is provided, because measuring an operator in general does not return a product state.
    • RemoveMeasured(), remove the measured qudits from the register. It is also incompatible with the operator argument.
  • operator::AbstractBlock is the operator to measure.
  • register::AbstractRegister is the quantum state.
  • locs is the qubits to performance the measurement. If locs is not provided, all current active qudits are measured (regarding to active qudits,

see focus! and relax!).

Keyword arguments

  • rng is the random number generator.

Examples

The following example measures a random state on the computational basis and reset it to a certain bitstring value.

julia> reg = rand_state(3);

julia> measure!(ResetTo(bit"011"), reg)
110 ₍₂₎

julia> measure(reg; nshots=3)
3-element Vector{DitStr{2, 3, Int64}}:
 011 ₍₂₎
 011 ₍₂₎
 011 ₍₂₎

julia> measure!(RemoveMeasured(), reg, (1,2))
11 ₍₂₎

julia> reg  # removed qubits are not usable anymore
ArrayReg{2, ComplexF64, Array...}
    active qubits: 1/1
    nlevel: 2

Measuring an operator will project the state to the subspace associated with the returned eigenvalue.

julia> reg = uniform_state(3)
ArrayReg{2, ComplexF64, Array...}
    active qubits: 3/3
    nlevel: 2

julia> print_table(reg)
000 ₍₂₎   0.35355 + 0.0im
001 ₍₂₎   0.35355 + 0.0im
010 ₍₂₎   0.35355 + 0.0im
011 ₍₂₎   0.35355 + 0.0im
100 ₍₂₎   0.35355 + 0.0im
101 ₍₂₎   0.35355 + 0.0im
110 ₍₂₎   0.35355 + 0.0im
111 ₍₂₎   0.35355 + 0.0im

julia> measure!(repeat(3, Z, 1:3), reg)
-1.0 + 0.0im

julia> print_table(reg)
000 ₍₂₎   0.0 + 0.0im
001 ₍₂₎   0.5 + 0.0im
010 ₍₂₎   0.5 + 0.0im
011 ₍₂₎   0.0 + 0.0im
100 ₍₂₎   0.5 + 0.0im
101 ₍₂₎   0.0 + 0.0im
110 ₍₂₎   0.0 + 0.0im
111 ₍₂₎   0.5 + 0.0im

Here, we measured the parity operator, as a result, the resulting state collapsed to the subspace with either even or odd parity.

YaoAPI.measureFunction
measure([, operator], register[, locs]; nshots=1, rng=Random.GLOBAL_RNG) -> Vector{Int}

Measure a quantum state and return measurement results of qudits. This measurement function a cheating version of measure! that does not collapse the input state. It also does not need to recompute the quantum state for performing multiple shots measurement.

Arguments

  • operator::AbstractBlock is the operator to measure.
  • register::AbstractRegister is the quantum state.
  • locs is the qubits to performance the measurement. If locs is not provided, all current active qudits are measured (regarding to active qudits,

see focus! and relax!).

Keyword arguments

  • nshots::Int is the number of shots.
  • rng is the random number generator.

Examples

julia> reg = product_state(bit"110")
ArrayReg{2, ComplexF64, Array...}
    active qubits: 3/3
    nlevel: 2

julia> measure(reg; nshots=3)
3-element Vector{DitStr{2, 3, Int64}}:
 110 ₍₂₎
 110 ₍₂₎
 110 ₍₂₎

julia> measure(reg, (2,3); nshots=3)
3-element Vector{DitStr{2, 2, Int64}}:
 11 ₍₂₎
 11 ₍₂₎
 11 ₍₂₎

The following example switches to the X basis for measurement.

julia> reg = apply!(product_state(bit"100"), repeat(3, H, 1:3))
ArrayReg{2, ComplexF64, Array...}
    active qubits: 3/3
    nlevel: 2

julia> measure(repeat(3, X, 1:3), reg; nshots=3)
3-element Vector{ComplexF64}:
 -1.0 + 0.0im
 -1.0 + 0.0im
 -1.0 + 0.0im

julia> reg = apply!(product_state(bit"101"), repeat(3, H, 1:3))
ArrayReg{2, ComplexF64, Array...}
    active qubits: 3/3
    nlevel: 2

julia> measure(repeat(3, X, 1:3), reg; nshots=3)
3-element Vector{ComplexF64}:
 1.0 - 0.0im
 1.0 - 0.0im
 1.0 - 0.0im
YaoAPI.expectFunction
expect(op::AbstractBlock, reg) -> Vector
expect(op::AbstractBlock, reg => circuit) -> Vector
expect(op::AbstractBlock, density_matrix) -> Vector

Get the expectation value of an operator, the second parameter can be a register reg or a pair of input register and circuit reg => circuit.

expect'(op::AbstractBlock, reg=>circuit) -> Pair
expect'(op::AbstractBlock, reg) -> AbstracRegister

Obtain the gradient with respect to registers and circuit parameters. For pair input, the second return value is a pair of gψ=>gparams, with the gradient of input state and gparams the gradients of circuit parameters. For register input, the return value is a register.

Note

For batched register, expect(op, reg=>circuit) returns a vector of size number of batch as output. However, one can not differentiate over a vector loss, so expect'(op, reg=>circuit) accumulates the gradient over batch, rather than returning a batched gradient of parameters.

Examples

julia> r = normalize!(product_state(bit"11") + product_state(bit"00"))
ArrayReg{2, ComplexF64, Array...}
    active qubits: 2/2
    nlevel: 2

julia> op = chain(2, put(1=>H), put(2=>X))
nqubits: 2
chain
├─ put on (1)
│  └─ H
└─ put on (2)
   └─ X


julia> expect(op, r)
0.7071067811865474 + 0.0im
YaoArrayRegister.statevecFunction
statevec(r::ArrayReg) -> array

Return a state matrix/vector by droping the last dimension of size 1 (i.e. nactive(r) = nqudits(r)). See also state.

Warning

statevec is not type stable. It may cause performance slow down.

YaoArrayRegister.zero_stateFunction
zero_state([T=ComplexF64], n::Int, subspace; nlevel=2)

Create a SubspaceArrayReg in zero state in given subspace.

Arguments

  • T: optional, element type, default is ComplexF64.
  • n: required, number of atoms (qubits).
  • subspace: required, the subspace of rydberg state.
source
zero_state([T=ComplexF64], n::Int; nbatch::Int=NoBatch())

Create an AbstractArrayReg that initialized to state $|0\rangle^{\otimes n}$. See also product_state, rand_state, uniform_state and ghz_state.

Examples

julia> zero_state(4)
ArrayReg{2, ComplexF64, Array...}
    active qubits: 4/4
    nlevel: 2

julia> zero_state(ComplexF32, 4)
ArrayReg{2, ComplexF32, Array...}
    active qubits: 4/4
    nlevel: 2

julia> zero_state(ComplexF32, 4; nbatch=3)
BatchedArrayReg{2, ComplexF32, Transpose...}
    active qubits: 4/4
    nlevel: 2
    nbatch: 3
YaoArrayRegister.rand_stateFunction
rand_state([T=ComplexF64], subspace; nlevel=2)

Create a random state in the given subspace.

source
rand_state([T=ComplexF64], n::Int; nbatch=NoBatch(), no_transpose_storage=false)

Create a random AbstractArrayReg with total number of qudits n.

Examples

julia> rand_state(4)
ArrayReg{2, ComplexF64, Array...}
    active qubits: 4/4
    nlevel: 2

julia> rand_state(ComplexF64, 4)
ArrayReg{2, ComplexF64, Array...}
    active qubits: 4/4
    nlevel: 2

julia> rand_state(ComplexF64, 4; nbatch=2)
BatchedArrayReg{2, ComplexF64, Transpose...}
    active qubits: 4/4
    nlevel: 2
    nbatch: 2
YaoArrayRegister.product_stateFunction
product_state([T=ComplexF64], config, subspace)

Create a product state of given config from subspace.

source
product_state([T=ComplexF64], dit_str; nbatch=NoBatch(), no_transpose_storage=false)
product_state([T=ComplexF64], nbits::Int, val::Int; nbatch=NoBatch(), nlevel=2, no_transpose_storage=false)
product_state([T=ComplexF64], vector; nbatch=NoBatch(), nlevel=2, no_transpose_storage=false)

Create an ArrayReg of product state. The configuration can be specified with a dit string, which can be defined with @bit_str or @dit_str. Or equivalently, it can be specified explicitly with nbits, val and nlevel. See also zero_state, rand_state, uniform_state.

Examples

julia> reg = product_state(dit"120;3"; nbatch=2)
BatchedArrayReg{3, ComplexF64, Transpose...}
    active qudits: 3/3
    nlevel: 3
    nbatch: 2

julia> measure(reg)
1×2 Matrix{BitBasis.DitStr64{3, 3}}:
 120 ₍₃₎  120 ₍₃₎

julia> product_state(bit"100"; nbatch=2);

julia> r1 = product_state(ComplexF32, bit"001"; nbatch=2);

julia> r2 = product_state(ComplexF32, [1, 0, 0]; nbatch=2);

julia> r3 = product_state(ComplexF32, 3, 0b001; nbatch=2);

julia> r1 ≈ r2   # because we read bit strings from right to left, vectors from left to right.
true

julia> r1 ≈ r3
true