Manual
In this section we show the working pipeline of JuliVQC for simulating quantum circuits and variational quantum circuits, as well as their noisy counterparts.
Initialize a quantum state
The first step of using JuliVQC for any quantum circuit simulation is to initialize a quantum state stored as a state vector. JuliVQC provides twos function: StateVector
and DensityMatrix
to initialize a pure state and a mixed state respectively. Mathematically, the data of an $n$-qubit pure state should be understood as a rank-$n$ tensor, and the data of an $n$-qubit mixed state should be understood as a rank-$2n$ tensor, where each dimension has size $2$.
As implementation-wise details, the qubits are internally labeled from $1$ to $n$ for pure state , while for mixed state the ket indices are labeled from $1$ to $n$ and the bra indices are labeled from $n+1$ to $2n$.
Column-major storage is used for the data of both pure and mixed quantum states, e.g., the smaller indices of the tensor are iterated first. These details are not important for the users if they do not want to access the raw data of the quantum states.
using JuliVQC,QuantumCircuits
state = StateVector(2)
n = 2
pure_state = StateVector(n)
mixed_state = DensityMatrix(n)
custom_pure_state= StateVector([.0,.1,.0,.0])
custom_mixed_state = DensityMatrix([.0,.0,.0,.0,.0,.0,.0,.0,.0,.0,.0,.0,.0,.0,.0,.1])
Quantum gates
The second step of using JuliVQC is to build a quantum circuit, for which one needs to define each elementary quantum gate operations (and quantum channels for noisy quantum circuits).
The universal way of defining quantum gates is to use the function QuantumGate(positions, data)
, where the first argument specifies the qubits indices that the gate operates on, for example positions =(1, 3)
, and the second argument is the raw data of the gate operation which should be a unitary matrix.
In the meantime, JuliVQC provides specialized definitions of commonly used quantum gates "X
, Y
, Z
, S
, H
, sqrtX
, sqrtY
, T
, Rx
, Ry
,Rz
, CONTROL
, CZ
, CNOT
, CX
,SWAP
, iSWAP
, XGate
, YGate
, ZGate
, HGate
, SGate
, TGate
, SqrtXGate
, SqrtYGate
, RxGate
, RyGate
, RzGate
, CZGate
, CNOTGate
, SWAPGate
, iSWAPGate
, CRxGate
, CRyGate
, CRzGate
, TOFFOLIGate
". Specific optimizations have been implemented for most of the predefined gate operations by exploring their structures, which will usually be faster than using the QuantumGate}
function.
JuliVQC also provides general two-qubit and three-qubit controlled gate operations: CONTROLGate
and CONTROLCONTROLGate
, which can be used as CONTROLGate(i,j,data)
(i
is the control qubit and j
is the target qubit) and CONTROLCONTROLGate(i,j,k,data)
(i
and j
are control qubits and k
is the target qubit), with data
the raw data for the target single-qubit operation.
The general interface for initializing a parametric quantum gate is G(i..., paras; isparas)
where paras
is a single scalar if G
only has a single parameter or an array of scalars if G
has several parameters.
The illustrative code for initializing non-parametric and parametric quantum gates:
using JuliVQC,QuantumCircuits
n=1
X = XGate(n)
ncontrol = 1
ntarget = 2
CNOT = CNOTGate(ncontrol, ntarget)
theta = pi/2
non_para_Rx = RxGate(n, theta, isparas=false) # a non-parametric Rx gate
para_Rx = RxGate(n, theta, isparas=true) # a parametric Rx gate
Noise channels
In additional to the quantum gate operations, an indispensable ingredient for noisy quantum circuit is the quantum channel, which describes the effects of noises.
Similar to the function QuantumGate
, JuliVQC provides a universal function QuantumMap(positions, kraus)
which allows the user to define arbitrary quantum channels, where the first argument positions
specifies the qubit indices that the quantum channel operates on, similar to the case of a unitary quantum gate, and the second argument kraus
is a list of Kraus operators.
JuliVQC also provides some commonly used single-qubit quantum channels based on the function QuantumMap
, including AmplitudeDamping(pos, p)
,PhaseDamping(pos, p)
,Depolarizing(pos, p)
Manipulating and running quantum circuits
JuliVQC uses a very simple wrapper QCircuit
on top of an array of quantum operations to represent a quantum circuit. Each element of QCircuit
can be either a (parametric) unitary gate operation, a quantum channel, or a QCircuit
.
After manipulating the quantum circuit, one could apply the quantum circuit onto the quantum state using theapply!(circ, state)
function. state
can either be a pure state or a density matrix, which modifies the quantum state in-place. There is also an out-of-place version of this operation, e.g., apply(circ, state)
or equivalently circ * state
, which will return a new quantum state and is useful for running variational quantum algorithms.
using JuliVQC
state = StateVector(2)
circuit = QCircuit([HGate(1), RyGate(1,pi/4,isparas = false) ,CNOTGate(1,2)])
apply!(circuit,state)
outcome, prob = measure!(state,2)
Qubit operators
The qubit operator is represented as a QubitsOperator
object in JuliVQC, which can be built as in following example. Once a qubit operator op
has been initialized, one could apply the function expectation(op, state)
to evaluate the expectation of it on the quantum state state
.
using JuliVQC
function heisenberg_1d(L; hz=1, J=1)
terms = []
# one site terms
for i in 1:L
push!(terms, QubitsTerm(i=>"z", coeff=hz))
end
# nearest-neighbour interactions
for i in 1:L-1
push!(terms, QubitsTerm(i=>"x", i+1=>"x", coeff=J))
push!(terms, QubitsTerm(i=>"y", i+1=>"y", coeff=J))
push!(terms, QubitsTerm(i=>"z", i+1=>"z", coeff=J))
end
return QubitsOperator(terms)
end
Automatic differentiation
JuliVQC has a transparent support for automatic differentiation, one could simply run a variational quantum algorithm in the similar way as a standard quantum algorithm. The major difference from running a standard quantum algorithm is that one wraps the expectation
function into a loss function, and then use the function gradient(loss, circ)
to obtain the gradient of the parameters within the quantum circuit.
using JuliVQC, Zygote
state = StateVector(3)
op = heisenberg_1d(3) #Construct Heisenberg Hamiltonian as a qubit operator
alpha = 0.01
circ = QCircuit()
for depth in 1:4
for i in 1:2
push!(circ,CNOTGate(i,i+1))
end
for i in 1:3
push!(circ,RyGate(i,randn(),isparas=true))
push!(circ,RxGate(i,randn(),isparas=true))
end
end
loss(circ)=real(expectation(op, circ * state))
grad = gradient(loss, circ)[1] # calculate gradient
paras = active_parameters(circ) # extracting the parameters
new_paras = paras - alpha * grad # gradient descent to update parameters
reset_parameters!(circ, new_paras) # reset parameters
Under the hood, the gradient is calculated using the Zygote auto-differentiation framework, by rewriting the backpropagation rules of a few elementary operations (the detailed algorithm we use to implement the classical backpropagation can be found in our paper (https://arxiv.org/abs/2406.19212).