Quantum Registers
Constructing quantum states
A quantum register is a quantum state or a batch of quantum states. Qubits in a Yao register can be active or inactive. Only active qubits are visible to quantum operators, which enables applying quantum operators on a subset of qubits. For example, Suppose we want to run a quantum Fourier transformation circuit of size 4 on qubits (1, 3, 5, 7)
with the focus!
function, we first set these qubits to active qubits the rest to inactive, then we apply the circuit on the active qubits, and finally we switch back to the original configuration with the relax!
function.
Yao
provides two types of quantum registers ArrayReg
and BatchedArrayReg
. Both use matrices as the storage. For example, for a quantum register with $a$ active qubits, $r$ remaining qubits and batch size $b$, the storage is as follows.
The first dimension of size $2^a$ is for active qubits, only this subset of qubits are allowed to interact with quantum operators. Since we reshaped the state vector into a matrix, applying a quantum operator can be conceptually represented as a matrix-matrix multiplication.
Various quantum states can be created with the following functions.
julia> using Yao
julia> reg = ArrayReg([0, 1, -1+0.0im, 0]) # a unnormalized Bell state |01⟩ - |10⟩
ArrayReg{2, ComplexF64, Array...} active qubits: 2/2 nlevel: 2
julia> statevec(reg) # a quantum state is represented as a vector
4-element Vector{ComplexF64}: 0.0 + 0.0im 1.0 + 0.0im -1.0 + 0.0im 0.0 + 0.0im
julia> print_table(reg)
00 ₍₂₎ 0.0 + 0.0im 01 ₍₂₎ 1.0 + 0.0im 10 ₍₂₎ -1.0 + 0.0im 11 ₍₂₎ 0.0 + 0.0im
julia> reg_zero = zero_state(3) # create a zero state |000⟩
ArrayReg{2, ComplexF64, Array...} active qubits: 3/3 nlevel: 2
julia> print_table(reg_zero)
000 ₍₂₎ 1.0 + 0.0im 001 ₍₂₎ 0.0 + 0.0im 010 ₍₂₎ 0.0 + 0.0im 011 ₍₂₎ 0.0 + 0.0im 100 ₍₂₎ 0.0 + 0.0im 101 ₍₂₎ 0.0 + 0.0im 110 ₍₂₎ 0.0 + 0.0im 111 ₍₂₎ 0.0 + 0.0im
julia> reg_rand = rand_state(ComplexF32, 3) # a random state
ArrayReg{2, ComplexF32, Array...} active qubits: 3/3 nlevel: 2
julia> reg_uniform = uniform_state(ComplexF32, 3) # a uniform state
ArrayReg{2, ComplexF32, Array...} active qubits: 3/3 nlevel: 2
julia> print_table(reg_uniform)
000 ₍₂₎ 0.35355f0 + 0.0f0im 001 ₍₂₎ 0.35355f0 + 0.0f0im 010 ₍₂₎ 0.35355f0 + 0.0f0im 011 ₍₂₎ 0.35355f0 + 0.0f0im 100 ₍₂₎ 0.35355f0 + 0.0f0im 101 ₍₂₎ 0.35355f0 + 0.0f0im 110 ₍₂₎ 0.35355f0 + 0.0f0im 111 ₍₂₎ 0.35355f0 + 0.0f0im
julia> reg_prod = product_state(bit"110") # a product state
ArrayReg{2, ComplexF64, Array...} active qubits: 3/3 nlevel: 2
julia> bit"110"[3] # the bit string is in little-endian format
1
julia> print_table(reg_prod)
000 ₍₂₎ 0.0 + 0.0im 001 ₍₂₎ 0.0 + 0.0im 010 ₍₂₎ 0.0 + 0.0im 011 ₍₂₎ 0.0 + 0.0im 100 ₍₂₎ 0.0 + 0.0im 101 ₍₂₎ 0.0 + 0.0im 110 ₍₂₎ 1.0 + 0.0im 111 ₍₂₎ 0.0 + 0.0im
julia> reg_ghz = ghz_state(3) # a GHZ state
ArrayReg{2, ComplexF64, Array...} active qubits: 3/3 nlevel: 2
julia> print_table(reg_ghz)
000 ₍₂₎ 0.70711 - 0.0im 001 ₍₂₎ 0.0 + 0.0im 010 ₍₂₎ 0.0 + 0.0im 011 ₍₂₎ 0.0 + 0.0im 100 ₍₂₎ 0.0 + 0.0im 101 ₍₂₎ 0.0 + 0.0im 110 ₍₂₎ 0.0 + 0.0im 111 ₍₂₎ 0.70711 - 0.0im
julia> von_neumann_entropy(reg_ghz, (1, 3)) / log(2) # entanglement entropy between qubits (1, 3) and (2,)
1.0000000000000229
julia> reg_rand3 = rand_state(3, nlevel=3) # a random qutrit state
ArrayReg{3, ComplexF64, Array...} active qudits: 3/3 nlevel: 3
julia> reg_prod3 = product_state(dit"120;3") # a qudit product state, what follows ";" symbol denotes the number of levels
ArrayReg{3, ComplexF64, Array...} active qudits: 3/3 nlevel: 3
julia> print_table(reg_prod3)
000 ₍₃₎ 0.0 + 0.0im 001 ₍₃₎ 0.0 + 0.0im 002 ₍₃₎ 0.0 + 0.0im 010 ₍₃₎ 0.0 + 0.0im 011 ₍₃₎ 0.0 + 0.0im 012 ₍₃₎ 0.0 + 0.0im 020 ₍₃₎ 0.0 + 0.0im 021 ₍₃₎ 0.0 + 0.0im 022 ₍₃₎ 0.0 + 0.0im 100 ₍₃₎ 0.0 + 0.0im 101 ₍₃₎ 0.0 + 0.0im 102 ₍₃₎ 0.0 + 0.0im 110 ₍₃₎ 0.0 + 0.0im 111 ₍₃₎ 0.0 + 0.0im 112 ₍₃₎ 0.0 + 0.0im 120 ₍₃₎ 1.0 + 0.0im 121 ₍₃₎ 0.0 + 0.0im 122 ₍₃₎ 0.0 + 0.0im 200 ₍₃₎ 0.0 + 0.0im 201 ₍₃₎ 0.0 + 0.0im 202 ₍₃₎ 0.0 + 0.0im 210 ₍₃₎ 0.0 + 0.0im 211 ₍₃₎ 0.0 + 0.0im 212 ₍₃₎ 0.0 + 0.0im 220 ₍₃₎ 0.0 + 0.0im 221 ₍₃₎ 0.0 + 0.0im 222 ₍₃₎ 0.0 + 0.0im
julia> reg_batch = rand_state(3; nbatch=2) # a batch of 2 random qubit states
BatchedArrayReg{2, ComplexF64, Transpose...} active qubits: 3/3 nlevel: 2 nbatch: 2
julia> print_table(reg_batch)
000 ₍₂₎ -0.00131 - 0.2684im; 0.16813 + 0.14023im 001 ₍₂₎ 0.30648 - 0.29432im; 0.03173 - 0.23842im 010 ₍₂₎ 0.12485 - 0.09021im; -0.64855 + 0.18629im 011 ₍₂₎ 0.13148 - 0.35264im; 0.16267 - 0.47im 100 ₍₂₎ 0.16153 + 0.35565im; 0.02691 + 0.33801im 101 ₍₂₎ -0.20673 - 0.41949im; 0.06515 + 0.05127im 110 ₍₂₎ 0.2543 - 0.1087im; 0.16238 + 0.15649im 111 ₍₂₎ -0.17558 + 0.3216im; 0.05642 - 0.12514im
julia> reg_view = viewbatch(reg_batch, 1) # view the first state in the batch
ArrayReg{2, ComplexF64, SubArray...} active qubits: 3/3 nlevel: 2
julia> print_table(reg_view)
000 ₍₂₎ -0.00131 - 0.2684im 001 ₍₂₎ 0.30648 - 0.29432im 010 ₍₂₎ 0.12485 - 0.09021im 011 ₍₂₎ 0.13148 - 0.35264im 100 ₍₂₎ 0.16153 + 0.35565im 101 ₍₂₎ -0.20673 - 0.41949im 110 ₍₂₎ 0.2543 - 0.1087im 111 ₍₂₎ -0.17558 + 0.3216im
julia> reg = rand_state(3; nlevel=4, nbatch=2)
BatchedArrayReg{4, ComplexF64, Transpose...} active qudits: 3/3 nlevel: 4 nbatch: 2
julia> nqudits(reg) # the total number of qudits
3
julia> nactive(reg) # the number of active qubits
3
julia> nremain(reg) # the number of remaining qubits
0
julia> nbatch(reg) # the batch size
2
julia> nlevel(reg) # the number of levels of each qudit
4
julia> basis(reg) # the basis of the register
000 ₍₄₎:333 ₍₄₎
julia> focus!(reg, 1:2) # set on the first two qubits as active
BatchedArrayReg{4, ComplexF64, Transpose...} active qudits: 2/3 nlevel: 4 nbatch: 2
julia> nactive(reg) # the number of active qubits
2
julia> basis(reg) # the basis of the register
00 ₍₄₎:33 ₍₄₎
julia> relax!(reg) # set all qubits as active
BatchedArrayReg{4, ComplexF64, Transpose...} active qudits: 3/3 nlevel: 4 nbatch: 2
julia> nactive(reg) # the number of active qubits
3
julia> reorder!(reg, (3,1,2)) # reorder the qubits
BatchedArrayReg{4, ComplexF64, Transpose...} active qudits: 3/3 nlevel: 4 nbatch: 2
julia> reg1 = product_state(bit"111");
julia> reg2 = ghz_state(3);
julia> fidelity(reg1, reg2) # the fidelity between two states
0.7071067811865476
julia> tracedist(reg1, reg2) # the trace distance between two states
0.7071067811865477
Arithmetic operations
The list of arithmetic operations for ArrayReg
include
+
-
*
/
(scalar)adjoint
julia> reg1 = rand_state(3)
ArrayReg{2, ComplexF64, Array...} active qubits: 3/3 nlevel: 2
julia> reg2 = rand_state(3)
ArrayReg{2, ComplexF64, Array...} active qubits: 3/3 nlevel: 2
julia> reg3 = reg1 + reg2 # addition
ArrayReg{2, ComplexF64, Array...} active qubits: 3/3 nlevel: 2
julia> normalize!(reg3) # normalize the state
ArrayReg{2, ComplexF64, Array...} active qubits: 3/3 nlevel: 2
julia> isnormalized(reg3) # check if the state is normalized
true
julia> reg1 - reg2 # subtraction
ArrayReg{2, ComplexF64, Array...} active qubits: 3/3 nlevel: 2
julia> reg1 * 2 # scalar multiplication
ArrayReg{2, ComplexF64, Array...} active qubits: 3/3 nlevel: 2
julia> reg1 / 2 # scalar division
ArrayReg{2, ComplexF64, Array...} active qubits: 3/3 nlevel: 2
julia> reg1' # adjoint
AdjointRegister{2, ArrayReg{2, ComplexF64, Matrix{ComplexF64}}}(ArrayReg{2, ComplexF64, Array...} active qubits: 3/3 nlevel: 2)
julia> reg1' * reg1 # inner product
1.0 + 0.0im
Register operations
julia> reg0 = rand_state(3)
ArrayReg{2, ComplexF64, Array...} active qubits: 3/3 nlevel: 2
julia> append_qudits!(reg0, 2) # append 2 qubits
ArrayReg{2, ComplexF64, Array...} active qubits: 5/5 nlevel: 2
julia> insert_qudits!(reg0, 2, 2) # insert 2 qubits at the 2nd position
ArrayReg{2, ComplexF64, Array...} active qubits: 7/7 nlevel: 2
Comparing with using matrix multiplication for quantum simulation, using specialized instructions are much faster and memory efficient. These instructions are specified with the instruct!
function.
julia> reg = zero_state(2)
ArrayReg{2, ComplexF64, Array...} active qubits: 2/2 nlevel: 2
julia> instruct!(reg, Val(:H), (1,)) # apply a Hadamard gate on the first qubit
ArrayReg{2, ComplexF64, Array...} active qubits: 2/2 nlevel: 2
julia> print_table(reg)
00 ₍₂₎ 0.70711 + 0.0im 01 ₍₂₎ 0.70711 + 0.0im 10 ₍₂₎ 0.0 + 0.0im 11 ₍₂₎ 0.0 + 0.0im
Measurement
We use the measure!
function returns the measurement outcome and collapses the state after the measurement. We also have some "cheating" version measure
that does not collapse states to facilitate classical simulation.
julia> measure!(reg0, 1) # measure the qubit, the state collapses
0 ₍₂₎
julia> measure!(reg0) # measure all qubits
0010000 ₍₂₎
julia> measure(reg0, 3) # measure the qubit at location 3, the state does not collapse (hacky)
1-element Vector{DitStr{2, 1, Int64}}: 0 ₍₂₎
julia> reorder!(reg0, 7:-1:1) # reorder the qubits
ArrayReg{2, ComplexF64, Array...} active qubits: 7/7 nlevel: 2
julia> measure!(reg0)
0000100 ₍₂₎
julia> invorder!(reg0) # reverse the order of qubits
ArrayReg{2, ComplexF64, Array...} active qubits: 7/7 nlevel: 2
julia> measure!(reg0)
0010000 ₍₂₎
julia> measure!(RemoveMeasured(), reg0, 2:4) # remove the measured qubits
000 ₍₂₎
julia> reg0
ArrayReg{2, ComplexF64, Array...} active qubits: 4/4 nlevel: 2
julia> reg1 = ghz_state(3)
ArrayReg{2, ComplexF64, Array...} active qubits: 3/3 nlevel: 2
julia> select!(reg1, bit"111") # post-select the |111⟩ state
ArrayReg{2, ComplexF64, Array...} active qubits: 0/0 nlevel: 2
julia> isnormalized(reg1) # check if the state is normalized
false
Density matrices
julia> reg = rand_state(3)
ArrayReg{2, ComplexF64, Array...} active qubits: 3/3 nlevel: 2
julia> rho = density_matrix(reg) # the density matrix of the state
DensityMatrix{2, ComplexF64, Array...} active qubits: 3/3 nlevel: 2
julia> rand_density_matrix(3) # a random density matrix
DensityMatrix{2, ComplexF64, Array...} active qubits: 3/3 nlevel: 2
julia> completely_mixed_state(3) # a completely mixed state
DensityMatrix{2, ComplexF64, Array...} active qubits: 3/3 nlevel: 2
julia> partial_tr(rho, 1) # partial trace on the first qubit
DensityMatrix{2, ComplexF64, Array...} active qubits: 2/2 nlevel: 2
julia> purify(rho) # purify the state
ArrayReg{2, ComplexF64, Array...} active qubits: 3/6 nlevel: 2
julia> von_neumann_entropy(rho) # von Neumann entropy
5.624513604281401e-14
julia> mutual_information(rho, 1, 2) # mutual information between qubits 1 and 2
0.2659095304711605
API
The constructors and functions for quantum registers are listed below.
YaoAPI.AbstractRegister
— TypeAbstractRegister{D}
Abstract type for quantum registers. Type parameter D
is the number of levels in each qudit. For qubits, D = 2
.
Required methods
Optional methods
YaoArrayRegister.AbstractArrayReg
— TypeAbstractArrayReg
Abstract type for quantum registers that are represented by an array.
YaoArrayRegister.ArrayReg
— TypeArrayReg{D,T,MT<:AbstractMatrix{T}} <: AbstractArrayRegister{D}
ArrayReg{D}(raw)
ArrayReg(raw::AbstractVecOrMat; nlevel=2)
ArrayReg(r::ArrayReg)
Simulated full amplitude register type, it uses an array to represent corresponding one or a batch of quantum states. T
is the numerical type for each amplitude, it is ComplexF64
by default.
ArrayReg
constructor will not normalize the quantum state. If you need a normalized quantum state remember to use normalize!(register)
on the register.
YaoArrayRegister.BatchedArrayReg
— TypeBatchedArrayReg{D,T,MT<:AbstractMatrix{T}} <: AbstractArrayReg{D}
BatchedArrayReg(raw, nbatch; nlevel=2)
BatchedArrayReg{D}(raw, nbatch)
Simulated batched full amplitude register type, it uses an array to represent corresponding one or a batch of quantum states. T
is the numerical type for each amplitude, it is ComplexF64
by default.
BatchedArrayReg
constructor will not normalize the quantum state. If you need a normalized quantum state remember to use normalize!(register)
on the register.
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
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
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
YaoArrayRegister.zero_state_like
— Functionzero_state_like(register, n) -> AbstractRegister
Create a register initialized to zero from an existing one.
Examples
julia> reg = rand_state(3; nbatch=2);
julia> zero_state_like(reg, 2)
BatchedArrayReg{2, ComplexF64, Array...}
active qubits: 2/2
nlevel: 2
nbatch: 2
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
YaoArrayRegister.uniform_state
— Functionuniform_state([T=ComplexF64], n; nbatch=NoBatch(), no_transpose_storage=false)
Create a uniform state:
\[\frac{1}{\sqrt{2^n}} \sum_{k=0}^{2^{n}-1} |k\rangle.\]
This state can also be created by applying H
(Hadmard gate) on $|00⋯00⟩$ state.
Example
julia> uniform_state(4; nbatch=2)
BatchedArrayReg{2, ComplexF64, Transpose...}
active qubits: 4/4
nlevel: 2
nbatch: 2
julia> uniform_state(ComplexF32, 4; nbatch=2)
BatchedArrayReg{2, ComplexF32, Transpose...}
active qubits: 4/4
nlevel: 2
nbatch: 2
YaoArrayRegister.ghz_state
— Functionghz_state([T=ComplexF64], n::Int; nbatch::Int=NoBatch())
Create a GHZ state (or a cat state) that defined as
\[\frac{|0\rangle^{\otimes n} + |1\rangle^{\otimes n}}{\sqrt{2}}.\]
Examples
julia> ghz_state(4)
ArrayReg{2, ComplexF64, Array...}
active qubits: 4/4
nlevel: 2
YaoAPI.clone
— Functionclone(register, n)
Create an ArrayReg
by cloning the original register
for n
times on batch dimension. This function is only for emulation.
Example
julia> clone(arrayreg(bit"101"; nbatch=3), 4)
BatchedArrayReg{2, ComplexF64, Array...}
active qubits: 3/3
nlevel: 2
nbatch: 12
The following functions are for querying the properties of a quantum register.
YaoAPI.nqudits
— Functionnqudits(register) -> Int
Returns the total number of qudits in register
.
YaoAPI.nqubits
— Functionnqubits(register) -> Int
Returns the (total) number of qubits. See nactive
, nremain
for more details.
YaoAPI.nactive
— Functionnactive(register) -> Int
Returns the number of active qudits in register
. Here, active qudits means the system qubits that operators can be applied on.
YaoAPI.nremain
— Functionnremain(register) -> Int
Returns the number of inactive qudits in register
. It equals to subtracting nqudits
and nactive
.
YaoArrayRegister.nbatch
— Functionnbatch(register) -> Union{Int,NoBatch()}
Returns the number of batches.
YaoAPI.nlevel
— Functionnlevel(x)
Number of levels in each qudit.
Examples
julia> nlevel(X)
2
YaoAPI.focus!
— Functionfocus!(register, locs) -> register
focus!(locs...) -> f(register) -> register
Set the active qubits to focused locations, usually used to execute a subroutine. If register
is not provided, returns a lambda that takes a register as input.
Examples
julia> reg = product_state(bit"01101")
ArrayReg{2, ComplexF64, Array...}
active qubits: 5/5
nlevel: 2
julia> focus!(reg, (1,3,4))
ArrayReg{2, ComplexF64, Array...}
active qubits: 3/5
nlevel: 2
julia> measure(reg; nshots=3)
3-element Vector{DitStr{2, 3, Int64}}:
111 ₍₂₎
111 ₍₂₎
111 ₍₂₎
julia> measure(apply(reg, put(3, 2=>X)); nshots=3)
3-element Vector{DitStr{2, 3, Int64}}:
101 ₍₂₎
101 ₍₂₎
101 ₍₂₎
Here, we prepare a product state and only look at the qubits 1, 3 and 4. The measurement results are all ones. With the focued register, we can apply a block of size 3 on it, even though the number of qubits is 5.
YaoAPI.focus
— Functionfocus(f, register, locs)
Call a callable f
under the context of focus
. See also focus!
.
Examples
To print the focused register
julia> r = arrayreg(bit"101100")
ArrayReg{2, ComplexF64, Array...}
active qubits: 6/6
nlevel: 2
julia> focus(x->(println(x);x), r, (1, 2));
ArrayReg{2, ComplexF64, Array...}
active qubits: 2/6
nlevel: 2
YaoAPI.relax!
— Functionrelax!(register[, locs]; to_nactive=nqudits(register)) -> register
relax!(locs::Int...; to_nactive=nqudits(register)) -> f(register) -> register
Inverse transformation of focus!
, where to_nactive
is the number of active bits for target register. If the register is not provided, returns a lambda function that takes a register as input.
Examples
julia> reg = product_state(bit"01101")
ArrayReg{2, ComplexF64, Array...}
active qubits: 5/5
nlevel: 2
julia> focus!(reg, (1,3,4))
ArrayReg{2, ComplexF64, Array...}
active qubits: 3/5
nlevel: 2
julia> relax!(reg, (1,3,4))
ArrayReg{2, ComplexF64, Array...}
active qubits: 5/5
nlevel: 2
YaoArrayRegister.exchange_sysenv
— Functionexchange_sysenv(reg::AbstractArrayReg) -> AbstractRegister
Exchange system (focused qubits) and environment (remaining qubits).
julia> reg = rand_state(5)
ArrayReg{2, ComplexF64, Array...}
active qubits: 5/5
nlevel: 2
julia> focus!(reg, (2,4))
ArrayReg{2, ComplexF64, Array...}
active qubits: 2/5
nlevel: 2
julia> exchange_sysenv(reg)
ArrayReg{2, ComplexF64, Adjoint...}
active qubits: 3/5
nlevel: 2
The following functions are for querying the state of a quantum register.
YaoArrayRegister.state
— Functionstate(register::AbstractArrayReg) -> Matrix
Returns the raw array storage of register
. See also statevec
.
state(ρ::DensityMatrix) -> Matrix
Return the raw state of density matrix ρ
.
BitBasis.basis
— Functionbasis(ditstr) -> UnitRange{DitStr{D,N,T}}
basis(DitStr{D,N,T}) -> UnitRange{DitStr{D,N,T}}
Returns the UnitRange
for basis in Hilbert Space of qudits.
basis(register) -> UnitRange
Returns an UnitRange
of the all the bits in the Hilbert space of given register.
julia> collect(basis(rand_state(3)))
8-element Vector{DitStr{2, 3, Int64}}:
000 ₍₂₎
001 ₍₂₎
010 ₍₂₎
011 ₍₂₎
100 ₍₂₎
101 ₍₂₎
110 ₍₂₎
111 ₍₂₎
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.relaxedvec
— Functionrelaxedvec(r::AbstractArrayReg) -> AbstractArray
Return a vector representation of state, with all qudits activated. See also state
, statevec
.
BitBasis.hypercubic
— Functionhypercubic(A::Array) -> Array
get the hypercubic representation for an array.
hypercubic(r::ArrayReg) -> AbstractArray
Return the hypercubic representation (high dimensional tensor) of this register, only active qudits are considered. See also rank3
and state
.
YaoArrayRegister.rank3
— Functionrank3(r::ArrayReg)
Return the rank 3 tensor representation of state, the 3 dimensions are (activated space, remaining space, batch dimension). See also hypercubic
and state
.
YaoAPI.viewbatch
— Functionviewbatch(register, i::Int) -> AbstractRegister
Returns the i
-th single register of a batched register. The returned instance is a view of the original register, i.e. inplace operation changes the original register directly.
Examples
julia> reg = zero_state(5; nbatch=2);
julia> apply!(viewbatch(reg, 2), put(5, 2=>X));
julia> measure(reg; nshots=3)
3×2 Matrix{DitStr{2, 5, Int64}}:
00000 ₍₂₎ 00010 ₍₂₎
00000 ₍₂₎ 00010 ₍₂₎
00000 ₍₂₎ 00010 ₍₂₎
YaoArrayRegister.transpose_storage
— Functiontranspose_storage(register) -> register
Transpose the register storage. Sometimes transposed storage provides better performance for batched simulation.
The following functions are for arithmetic operations on quantum registers.
YaoArrayRegister.AdjointArrayReg
— TypeAdjointArrayReg{D,T,MT} = AdjointRegister{D,<:AbstractArrayReg{D,T,MT}}
Adjoint array register type, it is used to represent the bra in the Dirac notation.
We also have some faster inplace versions of arithematic operations
YaoArrayRegister.regadd!
— Functionregadd!(target, source)
Inplace version of +
that accumulates source
to target
.
YaoArrayRegister.regsub!
— Functionregsub!(target, source)
Inplace version of -
that subtract source
from target
.
YaoArrayRegister.regscale!
— Functionregscale!(target, x)
Inplace version of multiplying a scalar x
to target.
We also define the following functions for state normalization, and distance measurement.
LinearAlgebra.normalize!
— Functionnormalize!(r::AbstractArrayReg)
Normalize the register r
by its 2-norm. It changes the register directly.
Examples
The following code creates a normalized GHZ state.
julia> reg = product_state(bit"000") + product_state(bit"111");
julia> norm(reg)
1.4142135623730951
julia> isnormalized(reg)
false
julia> normalize!(reg);
julia> isnormalized(reg)
true
YaoArrayRegister.isnormalized
— Functionisnormalized(r::ArrayReg) -> Bool
Returns true if the register r
is normalized.
YaoAPI.fidelity
— Functionfidelity(register1, register2) -> Real/Vector{<:Real}
fidelity'(pair_or_reg1, pair_or_reg2) -> (g1, g2)
Return the fidelity between two states. Calcuate the fidelity between r1
and r2
, if r1
or r2
is not pure state (nactive(r) != nqudits(r)
), the fidelity is calcuated by purification. See also: http://iopscience.iop.org/article/10.1088/1367-2630/aa6a4b/meta
Obtain the gradient with respect to registers and circuit parameters. For pair input ψ=>circuit
, the returned gradient 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.
Definition
The fidelity of two quantum state for qudits is defined as:
\[F(ρ, σ) = tr(\sqrt{\sqrt{ρ}σ\sqrt{ρ}})\]
This definition is different from the one in Wiki by a square.
Examples
julia> reg1 = uniform_state(3);
julia> reg2 = zero_state(3);
julia> fidelity(reg1, reg2)
0.35355339059327373
References
- Jozsa R. Fidelity for mixed quantum states[J]. Journal of modern optics, 1994, 41(12): 2315-2323.
- Nielsen M A, Chuang I. Quantum computation and quantum information[J]. 2002.
The original definition of fidelity $F$ was from "transition probability", defined by Jozsa in 1994, it is the square of what we use here.
YaoAPI.tracedist
— Functiontracedist(register1, register2)
Return the trace distance of register1
and register2
.
Definition
Trace distance is defined as following:
\[\frac{1}{2} || A - B ||_{\rm tr}\]
It takes values between 0 and 1.
Examples
julia> reg1 = uniform_state(3);
julia> reg2 = zero_state(3);
julia> tracedist(reg1, reg2)
0.9354143466934852
References
- https://en.wikipedia.org/wiki/Trace_distance
The following functions are for adding and reordering qubits in a quantum register.
YaoAPI.insert_qudits!
— Functioninsert_qudits!(register, loc::Int, nqudits::Int) -> register
insert_qudits!(loc::Int, nqudits::Int) -> λ(register)
Insert qudits to given register in state |0>. i.e. |psi> -> join(|psi>, |0...>, |psi>), increased bits have higher indices.
Examples
julia> reg = product_state(bit"01101")
ArrayReg{2, ComplexF64, Array...}
active qubits: 5/5
nlevel: 2
julia> insert_qudits!(reg, 2, 2)
ArrayReg{2, ComplexF64, Array...}
active qubits: 7/7
nlevel: 2
julia> measure(reg; nshots=3)
3-element Vector{DitStr{2, 7, Int64}}:
0110001 ₍₂₎
0110001 ₍₂₎
0110001 ₍₂₎
YaoAPI.insert_qubits!
— Functioninsert_qubits!(register, loc::Int, nqubits::Int=1) -> register
insert_qubits!(loc::Int, nqubits::Int=1) -> λ(register)
Insert n
qubits to given register in state |0>. It is an alias of insert_qudits!
function.
YaoAPI.append_qudits!
— Functionappend_qudits!(register, n::Int) -> register
append_qudits!(n::Int) -> λ(register)
Add n
qudits to given register in state |0>. i.e. |psi> -> |000> ⊗ |psi>, increased bits have higher indices.
If only an integer is provided, then returns a lambda function.
Examples
julia> reg = product_state(bit"01101")
ArrayReg{2, ComplexF64, Array...}
active qubits: 5/5
nlevel: 2
julia> append_qudits!(reg, 2)
ArrayReg{2, ComplexF64, Array...}
active qubits: 7/7
nlevel: 2
julia> measure(reg; nshots=3)
3-element Vector{DitStr{2, 7, Int64}}:
0001101 ₍₂₎
0001101 ₍₂₎
0001101 ₍₂₎
Note here, we read the bit string from right to left.
YaoAPI.append_qubits!
— Functionappend_qubits!(register, n::Int) -> register
append_qubits!(n::Int) -> λ(register)
Add n
qudits to given register in state |0>. It is an alias of append_qudits!
function.
YaoAPI.reorder!
— Functionreorder!(reigster, orders)
Reorder the locations of register by input orders. For a 3-qubit register, an order (i, j, k)
specifies the following reordering of qubits
- move the first qubit go to
i
, - move the second qubit go to
j
, - move the third qubit go to
k
.
The convention of reorder!
is different from the permutedims
function, one can use the sortperm
function to relate the permutation order and the order in this function.
Examples
julia> reg = product_state(bit"010101");
julia> reorder!(reg, (1,4,2,5,3,6));
julia> measure(reg)
1-element Vector{DitStr{2, 6, Int64}}:
000111 ₍₂₎
YaoAPI.invorder!
— Functioninvorder!(register)
Inverse the locations of the register.
Examples
julia> reg = product_state(bit"010101")
ArrayReg{2, ComplexF64, Array...}
active qubits: 6/6
nlevel: 2
julia> measure(invorder!(reg); nshots=3)
3-element Vector{DitStr{2, 6, Int64}}:
101010 ₍₂₎
101010 ₍₂₎
101010 ₍₂₎
The instruct!
function is for applying quantum operators on a quantum register.
YaoAPI.instruct!
— Functioninstruct!([nlevel=Val(2), ]state, operator, locs[, control_locs, control_configs, theta])
Unified interface for applying an operator to a quantum state. It modifies the state
directly.
Arguments
nlevel
is the number of levels in each qudit,state
is a vector or matrix representing the quantum state, where the first dimension is the active qubit dimension, the second is the batch dimension.operator
is a quantum operator, which can beVal(GATE_SYMBOL)
or a matrix.locs::Tuple
is a tuple for specifying the locations this gate applied.control_locs::Tuple
andcontrol_configs
are tuples for specifying the control locations and control values.theta::Real
is the parameter for the gate, e.g.Val(:Rx)
gate takes a real number of its parameter.
The following functions are for measurement and post-selection.
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.select!
— Functionselect!(dest::AbstractRegister, src::AbstractRegister, bits::Integer...) -> AbstractRegister
select!(register::AbstractRegister, bits::Integer...) -> register
select!(b::Integer) -> f(register)
select a subspace of given quantum state based on input eigen state bits
. See also select
for the non-inplace version. If the register is not provided, it returns a lambda expression that takes a register as the input.
Examples
julia> reg = ghz_state(3)
ArrayReg{2, ComplexF64, Array...}
active qubits: 3/3
nlevel: 2
julia> select!(reg, bit"111")
ArrayReg{2, ComplexF64, Array...}
active qubits: 0/0
nlevel: 2
julia> norm(reg)
0.7071067811865476
The selection only works on the activated qubits, for example
julia> reg = focus!(ghz_state(3), (1, 2))
ArrayReg{2, ComplexF64, Array...}
active qubits: 2/3
nlevel: 2
julia> select!(reg, bit"11")
ArrayReg{2, ComplexF64, Array...}
active qubits: 0/1
nlevel: 2
julia> statevec(reg)
1×2 Matrix{ComplexF64}:
0.0+0.0im 0.707107+0.0im
Developers should overload select!(r::RegisterType, bits::NTuple{N, <:Integer})
and do not assume bits
has specific number of bits (e.g Int64
), or it will restrict the its maximum available number of qudits.
YaoAPI.select
— Functionselect(register, bits) -> AbstractRegister
The non-inplace version of select!
.
YaoAPI.collapseto!
— Functioncollapseto!(register, config)
Set the register
to bit string literal bit_str
(or an equivalent integer). About bit string literal, see more in @bit_str
. This interface is only for emulation.
Examples
The following code collapse a random state to a certain state.
julia> measure(collapseto!(rand_state(3), bit"001"); nshots=3)
3-element Vector{DitStr{2, 3, Int64}}:
001 ₍₂₎
001 ₍₂₎
001 ₍₂₎
YaoAPI.probs
— Functionprobs(register) -> Vector
Returns the probability distribution of computation basis, aka $|<x|ψ>|^2$.
Examples
julia> reg = product_state(bit"101");
julia> reg |> probs
8-element Vector{Float64}:
0.0
0.0
0.0
0.0
0.0
1.0
0.0
0.0
YaoArrayRegister.most_probable
— Functionmost_probable(reg::ArrayReg{D, T} where T, n::Int64) -> Any
Find n
most probable qubit configurations in a quantum register and return these configurations as a vector of DitStr
instances.
Example
julia> most_probable(ghz_state(3), 2)
2-element Vector{DitStr{2, 3, Int64}}:
000 ₍₂₎
111 ₍₂₎
The following functions are for density matrices.
YaoAPI.DensityMatrix
— TypeDensityMatrix{D,T,MT<:AbstractMatrix{T}} <: AbstractRegister{D}
DensityMatrix{D}(state::AbstractMatrix)
DensityMatrix(state::AbstractMatrix; nlevel=2)
Density matrix type, where state
is a matrix. Type parameter D
is the number of levels, it can also be specified by a keyword argument nlevel
.
YaoAPI.density_matrix
— Functiondensity_matrix(register_or_rho[, locations])
Returns the reduced density matrix for qubits at locations
(default: all qubits).
Examples
The following code gets the single site reduce density matrix for the GHZ state.
julia> reg = ghz_state(3)
ArrayReg{2, ComplexF64, Array...}
active qubits: 3/3
nlevel: 2
julia> density_matrix(reg, (2,)).state
2×2 Matrix{ComplexF64}:
0.5+0.0im 0.0+0.0im
0.0-0.0im 0.5+0.0im
YaoArrayRegister.rand_density_matrix
— Functionrand_density_matrix([T=ComplexF64], n::Int; nlevel::Int=2, pure::Bool=false)
Generate a random density matrix by partial tracing half of the pure state.
The generated density matrix is not strict hermitian due to rounding error. If you need to check hermicity, do not use ishermitian
consider using isapprox(dm.state, dm.state')
or explicit mark it as Hermitian
.
YaoArrayRegister.completely_mixed_state
— Functioncompletely_mixed_state([T=ComplexF64], n::Int; nlevel::Int=2)
Generate the completely mixed state with density matrix I(n) ./ nlevel^n
.
YaoAPI.partial_tr
— Functionpartial_tr(ρ, locs) -> DensityMatrix
Return a density matrix which is the partial traced on locs
.
YaoAPI.purify
— Functionpurify(r::DensityMatrix; nbit_env::Int=nactive(r)) -> ArrayReg
Get a purification of target density matrix.
Examples
The following example shows how to measure a local operator on the register, reduced density matrix and the purified register. Their results should be consistent.
julia> reg = ghz_state(3)
ArrayReg{2, ComplexF64, Array...}
active qubits: 3/3
nlevel: 2
julia> r = density_matrix(reg, (2,));
julia> preg = purify(r)
ArrayReg{2, ComplexF64, Array...}
active qubits: 1/2
nlevel: 2
julia> isapprox(expect(Z + Y, preg), 0.0; atol=1e-10)
true
julia> isapprox(expect(Z + Y, r), 0.0; atol=1e-10)
true
julia> isapprox(expect(put(3, 2=>(Z + Y)), reg), 0.0; atol=1e-10)
true
YaoArrayRegister.von_neumann_entropy
— Functionvon_neumann_entropy(reg::AbstractArrayReg, part)
von_neumann_entropy(ρ::DensityMatrix)
The entanglement entropy between part
and the rest part in quantum state reg
. If the input is a density matrix, it returns the entropy of a mixed state.
Example
The Von Neumann entropy of any segment of GHZ state is $\log 2$.
julia> von_neumann_entropy(ghz_state(3), (1,2))
0.6931471805599612
von_neumann_entropy(rho) -> Real
Return the von-Neumann entropy for the input density matrix:
\[-{\rm Tr}(\rho\ln\rho)\]
YaoArrayRegister.mutual_information
— Functionmutual_information(register_or_rho, part1, part2)
Returns the mutual information between subsystems part1
and part2
of the input quantum register or density matrix:
\[S(\rho_A) + S(\rho_B) - S(\rho_{AB})\]
Example
The mutual information of a GHZ state of any two disjoint parts is always equal to $\log 2$.
julia> mutual_information(ghz_state(4), (1,), (3,4))
0.6931471805599132