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 register
ArrayReg{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.9909763576732966 + 0.8167070141105562im 0.5268045800539806 + 0.24439773300296963im 0.29826002628104553 + 0.7937325814949338im 0.8005675538678771 + 0.6155620246875098im
julia> reg = SubspaceArrayReg(state, space)
SubspaceArrayReg{2, ComplexF64, Vector{ComplexF64}, Subspace{Int64, Vector{Int64}}}(5, ComplexF64[0.9909763576732966 + 0.8167070141105562im, 0.5268045800539806 + 0.24439773300296963im, 0.29826002628104553 + 0.7937325814949338im, 0.8005675538678771 + 0.6155620246875098im], 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}}: 111 ₍₂₎ 011 ₍₂₎ 011 ₍₂₎ 000 ₍₂₎ 001 ₍₂₎
julia> expect(put(3,1=>X), reg)
0.20896826361401255
julia> statevec(reg)
8-element Vector{ComplexF64}: 0.32098148612016664 - 0.3027045597687297im 0.3445379916471652 - 0.15129648720914116im 0.002213617734381416 - 0.20342225295360214im -0.4962941032625858 - 0.1885608372176945im 0.005937890315291475 - 0.24969864091332164im -0.06270521214148751 - 0.11623277813588731im -0.2483880006928933 + 0.1135710210377388im 0.39247145435242276 - 0.1789965100778524im
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_density
— Functionrydberg_density(reg, i::Int) -> Real
Calculates the rydberg density at site i
.
\[\langle n_i \rangle\]
rydberg_density(reg) -> Vector
Return the rydberg density at each site.
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.5204135561548235 0.5019361395982921 0.4957988747739367 0.49887100388549654 0.5080758657743747 0.5097974246375189 0.49934234531183036 0.505010040231814 0.5006433972157174 0.5216142964221241
julia> n_2 = rydberg_density(reg, 2)
0.5019361395982921
To access the two-point correlation functions, we can use the rydberg_corr
function below:
Bloqade.rydberg_corr
— Functionrydberg_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 isOp.n
.reg
: required, the register object.
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{Float64}: 0.520414 0.26908 0.270902 0.272877 … 0.257089 0.259587 0.294448 0.26908 0.501936 0.245363 0.246707 0.253681 0.27051 0.256149 0.270902 0.245363 0.495799 0.249206 0.248892 0.244435 0.25763 0.272877 0.246707 0.249206 0.498871 0.26278 0.250968 0.275641 0.275657 0.24277 0.256346 0.251869 0.248194 0.257018 0.272093 0.268486 0.267363 0.258183 0.246428 … 0.260518 0.252995 0.25418 0.25256 0.249129 0.247471 0.235026 0.262312 0.256507 0.261592 0.257089 0.253681 0.248892 0.26278 0.50501 0.260067 0.265529 0.259587 0.27051 0.244435 0.250968 0.260067 0.500643 0.26669 0.294448 0.256149 0.25763 0.275641 0.265529 0.26669 0.521614
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_densities
— Functionget_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 isMHz*µm^6
, interation parameter, see alsoRydInteract
.Ω
: optional, default unit isMHz
, Rabi frequencies, divided by 2, see alsoSumOfX
.ϕ
: optional, does not have unit, the phase, seeSumOfXPhase
.Δ
: optional, default unit isMHz
, detuning parameter, seeSumOfN
.dt
: optional, default unit isμs
, time step for the evolution.solver
: optional, default solver isVern8()
, the solver for the SchrodingerProblem, seeSchrodingerProblem
.
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.05348040767512797
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/|x_i-x_j|^6 n_i n_j └─ [+] 2π ⋅ 0.05 ⋅ ∑ σ^x_i
julia> expect(h, r)
4.472483045657386e6
Please refer to the Hamiltonians to see other operators that are supported in building the Hamiltonian.
References
YaoArrayRegister.arrayreg
— Functionarrayreg(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!
— Functionapply!(register, block)
Apply a block (of quantum circuit) to a quantum register.
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!
— Functionmeasure!([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 beNoPostProcess()
(default).ResetTo(config)
, reset to result state toconfig
. It can not be used ifoperator
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 theoperator
argument.
operator::AbstractBlock
is the operator to measure.register::AbstractRegister
is the quantum state.locs
is the qubits to performance the measurement. Iflocs
is not provided, all current active qudits are measured (regarding to active qudits,
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.measure
— Functionmeasure([, 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. Iflocs
is not provided, all current active qudits are measured (regarding to active qudits,
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.expect
— Functionexpect(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 gψ
the gradient of input state and gparams
the gradients of circuit parameters. For register input, the return value is a register.
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
YaoArrayRegister.statevec
— Functionstatevec(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
.
statevec
is not type stable. It may cause performance slow down.
YaoArrayRegister.zero_state
— Functionzero_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
zero_state([T=ComplexF64], n::Int, subspace; nlevel=2)
Create a SubspaceArrayReg
in zero state in given subspace.
Arguments
T
: optional, element type, default isComplexF64
.n
: required, number of atoms (qubits).subspace
: required, the subspace of rydberg state.
YaoArrayRegister.rand_state
— Functionrand_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
rand_state([T=ComplexF64], subspace; nlevel=2)
Create a random state in the given subspace.
YaoArrayRegister.product_state
— Functionproduct_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
product_state([T=ComplexF64], config, subspace)
Create a product state of given config from subspace
.
YaoSubspaceArrayReg.SubspaceArrayReg
— TypeSubspaceArrayReg{D, T, State, Space} <: AbstractArrayReg{D}
SubspaceArrayReg{D}(state, subspace)
SubspaceArrayReg(state, subspace)
Type for registers in a subspace. The subspace must be a Subspace
.
YaoSubspaceArrayReg.set_zero_state!
— Functionset_zero_state!(register)
Set the given register to |00...00⟩.
YaoBlocks.ConstGate.X
— ConstantX
XGate <: ConstantGate{1,2}
Pauli X gate. X
is the instance of XGate
.
YaoBlocks.ConstGate.Y
— ConstantY
YGate <: ConstantGate{1,2}
Pauli Y gate. Y
is the instance of YGate
.
YaoBlocks.ConstGate.Z
— ConstantZ
ZGate <: ConstantGate{1,2}
Pauli Z gate. Z
is the instance of YGate
.