Author: Boxi Li (etamin1201@gmail.com)
In this example, we demonstrate how to simulate simple quantum algorithms on a qauntum hardware with QuTiP. The simulators are defined in the class Processor
(and its sub-classes). Processor
represents a general quantum device. The interaction of the quantum systems such as qubits is defined by the control Hamiltonian. For a general introduction of pulse-level simulation, please refer to the user guide.
In the following, we compile a simple three-qubit quantum circuit into control pulses on different Hamiltonian model.
The Deutsch–Jozsa algorithm is the simplest quantum algorithm that offers an exponential speed-up compared to the classical one. It assumes that we have a function $f:\{0,1\}^n \rightarrow \{0,1\}$ which is either balanced or constant. Constant means that $f(x)$ is either 1 or 0 for all inputs while balanced means that $f(x)$ is 1 for half of the input domain and 0 for the other half. A more rigorous definition can be found at https://en.wikipedia.org/wiki/Deutsch-Jozsa_algorithm.
The implementation of the Deutsch–Jozsa algorithm includes $n$ input qubits and 1 ancilla initialised in state $1$. At the end of the algorithm, the first $n$ qubits are measured on the computational basis. If the function is constant, the result will be $0$ for all $n$ qubits. If balanced, $\left|00...0\right\rangle$ will never be measured. The following example is implemented for the balanced function $f:\{00,01,10,11\} \rightarrow \{0,1\}$, where $f(00)=f(11)=0$ and $f(01)=f(10)=1$. This function is balanced, so the probability of measuring state $\left|00\right\rangle$ should be 0.
import numpy as np
from qutip import basis, ptrace
from qutip_qip.circuit import QubitCircuit
from qutip_qip.device import (LinearSpinChain, OptPulseProcessor, SCQubits,
SpinChainModel)
from qutip.ipynbtools import version_table
import qutip_qip
qc = QubitCircuit(N=3)
qc.add_gate("X", targets=2)
qc.add_gate("SNOT", targets=0)
qc.add_gate("SNOT", targets=1)
qc.add_gate("SNOT", targets=2)
# function f(x)
qc.add_gate("CNOT", controls=0, targets=2)
qc.add_gate("CNOT", controls=1, targets=2)
qc.add_gate("SNOT", targets=0)
qc.add_gate("SNOT", targets=1)
qc
/usr/share/miniconda3/envs/test-environment/lib/python3.10/site-packages/qutip_qip/circuit/circuit_latex.py:95: UserWarning: Could not locate system 'pdfcrop': image output may have additional margins. warnings.warn(
First, we simulate the quantum circuit using the Hamiltonian model LinearSpinChain
. The control Hamiltonians are defined in SpinChainModel
.
processor = LinearSpinChain(3)
processor.load_circuit(qc);
To quickly visualize the pulse, Processor
has a method called plot_pulses
. In the figure bellow, each colour represents the pulse sequence of one control Hamiltonian in the system as a function of time. In each time interval, the pulse remains constant.
processor.plot_pulses(title="Control pulse of Spin chain",
figsize=(8, 4), dpi=100);
Because for the spin chain model interaction only exists between neighbouring qubits, SWAP gates are added between and after the first CNOT gate, swapping the first two qubits. The SWAP gate is decomposed into three iSWAP gates, while the CNOT is decomposed into two iSWAP gates plus additional single-qubit corrections. Both the Hadamard gate and the two-qubit gates need to be decomposed to native gates (iSWAP and rotation on the $x$ and $z$ axes). The compiled coefficients are square pulses and the control coefficients on $\sigma_z$ and $\sigma_x$ are also different, resulting in different gate times.
basis00 = basis([2, 2], [0, 0])
psi0 = basis([2, 2, 2], [0, 0, 0])
result = processor.run_state(init_state=psi0)
print("Probability of measuring state 00:")
print(np.real((basis00.dag() * ptrace(result.states[-1], [0, 1]) * basis00)))
Probability of measuring state 00: 3.924274736491396e-09
/usr/share/miniconda3/envs/test-environment/lib/python3.10/site-packages/qutip/solver/options.py:16: FutureWarning: Dedicated options class are no longer needed, options should be passed as dict to solvers. warnings.warn(
processor.t1 = 100
processor.t2 = 30
psi0 = basis([2, 2, 2], [0, 0, 0])
result = processor.run_state(init_state=psi0)
print("Probability of measuring state 00:")
print(np.real((basis00.dag() * ptrace(result.states[-1], [0, 1]) * basis00)))
Probability of measuring state 00: 0.13730432003950477
This feature integrated into the sub-class OptPulseProcessor
which use methods in the optimal control module to find the optimal pulse sequence for the desired gates. It can find the optimal pulse either for the whole unitary evolution or for each gate. Here we choose the second option.
setting_args = {
"SNOT": {"num_tslots": 6, "evo_time": 2},
"X": {"num_tslots": 1, "evo_time": 0.5},
"CNOT": {"num_tslots": 12, "evo_time": 5},
}
# Use the control Hamiltonians of the spin chain model.
processor = OptPulseProcessor(
num_qubits=3, model=SpinChainModel(3, setup="linear")
)
processor.load_circuit( # Provide parameters for the algorithm
qc,
setting_args=setting_args,
merge_gates=False,
verbose=True,
amp_ubound=5,
amp_lbound=0,
);
********** Gate 0 ********** Final fidelity error 1.0 Final gradient normal 6.661338147750939e-16 Terminated due to Gradient normal minimum reached Number of iterations 0 ********** Gate 1 ********** Final fidelity error 0.0198800563106325 Final gradient normal 0.028305477899259017 Terminated due to Iteration or fidelity function call limit reached Number of iterations 500 ********** Gate 2 ********** Final fidelity error 1.2892485273807708e-06 Final gradient normal 0.000284850989843532 Terminated due to function converged Number of iterations 494 ********** Gate 3 ********** Final fidelity error 0.005103141800351874 Final gradient normal 0.008912183821224473 Terminated due to function converged Number of iterations 483 ********** Gate 4 ********** Final fidelity error 2.314803315695002e-08 Final gradient normal 0.00011738771979468758 Terminated due to function converged Number of iterations 135 ********** Gate 5 ********** Final fidelity error 2.1669624983289282e-08 Final gradient normal 0.0004642543315871194 Terminated due to function converged Number of iterations 136 ********** Gate 6 ********** Final fidelity error 0.018256512758583088 Final gradient normal 0.0767154247823341 Terminated due to function converged Number of iterations 230 ********** Gate 7 ********** Final fidelity error 0.04211023983573614 Final gradient normal 0.06695526790269639 Terminated due to function converged Number of iterations 182
processor.plot_pulses(
title="Control pulse of OptPulseProcessor", figsize=(8, 4), dpi=100
);
For the optimal control model, we use the GRAPE algorithm, where control pulses are piece-wise constant functions. We provide the algorithm with the same control Hamiltonian model used for the spin chain model. In the compiled optimal signals, all controls are active (non-zero pulse amplitude) during most of the execution time. We note that for identical gates on different qubits (e.g., Hadamard), each optimized pulse is different, demonstrating that the optimized solution is not unique, and there are further constraints one could apply, such as adaptions for the specific hardware.
basis00 = basis([2, 2], [0, 0])
psi0 = basis([2, 2, 2], [0, 0, 0])
result = processor.run_state(init_state=psi0)
print("Probability of measuring state 00:")
print(np.real((basis00.dag() * ptrace(result.states[-1], [0, 1]) * basis00)))
Probability of measuring state 00: 0.7772095028184329
processor.t1 = 100
processor.t2 = 30
psi0 = basis([2, 2, 2], [0, 0, 0])
result = processor.run_state(init_state=psi0)
print("Probability of measuring state 00:")
print(np.real((basis00.dag() * ptrace(result.states[-1], [0, 1]) * basis00)))
Probability of measuring state 00: 0.44237345783039905
We can see that under noisy evolution their is a none zero probability of measuring state 00.
Below, we simulate the same quantum circuit using one sub-class LinearSpinChain
. It will find the pulse based on the Hamiltonian available on a quantum computer of the linear spin chain system.
Please refer to the notebook of the spin chain model for more details.
processor = SCQubits(num_qubits=3)
processor.load_circuit(qc);
processor.plot_pulses(title="Control pulse of SCQubits",
figsize=(8, 4), dpi=100);
For the superconducting-qubit processor, the compiled pulses have a Gaussian shape. This is crucial for superconducting qubits because the second excited level is only slightly detuned from the qubit transition energy. A smooth pulse usually prevents leakage to the non-computational subspace. Similar to the spin chain, SWAP gates are added to switch the zeroth and first qubit and one SWAP gate is compiled to three CNOT gates. The control $ZX^{21}$ is not used because there is no CNOT gate that is controlled by the second qubit and acts on the first one.
basis00 = basis([3, 3], [0, 0])
psi0 = basis([3, 3, 3], [0, 0, 0])
result = processor.run_state(init_state=psi0)
print("Probability of measuring state 00:")
print(np.real((basis00.dag() * ptrace(result.states[-1], [0, 1]) * basis00)))
Probability of measuring state 00: 2.877629909429341e-07
processor.t1 = 50.0e3
processor.t2 = 20.0e3
psi0 = basis([3, 3, 3], [0, 0, 0])
result = processor.run_state(init_state=psi0)
print("Probability of measuring state 00:")
print(np.real((basis00.dag() * ptrace(result.states[-1], [0, 1]) * basis00)))
Probability of measuring state 00: 0.060375187227154174
print("qutip-qip version:", qutip_qip.version.version)
version_table()
qutip-qip version: 0.4.0.dev0+2aca4e0
Software | Version |
---|---|
QuTiP | 5.1.0.dev0+50fc9b7 |
Numpy | 1.22.4 |
SciPy | 1.13.0 |
matplotlib | 3.5.2 |
Number of CPUs | 4 |
BLAS Info | Generic |
IPython | 8.22.2 |
Python | 3.10.4 | packaged by conda-forge | (main, Mar 24 2022, 17:39:04) [GCC 10.3.0] |
OS | posix [linux] |
Cython | 3.0.10 |
Mon Apr 15 14:00:50 2024 UTC |