from qat.lang.AQASM import Program, H, PH, SWAP, CNOT
prog = Program()
reg = prog.qalloc(3)
prog.apply(H, reg[2])
prog.apply(H.ctrl(), reg[0:2])
prog.apply(PH(0.4).ctrl(), reg[0:2])
prog.apply(CNOT, reg[0:2])
prog.apply(SWAP, reg[1:3])
prog.apply(CNOT, reg[1:3])
circ = prog.to_circ()
circ.display()
For more information about how to write a quantum circuit in Python, check out this tutorial.
Instantiating a noisy quantum processor¶
A noisy quantum processor is characterized by
- a hardware model: this object describes the quantum gates (their duration, the quantum channel that defines them, ...) and the environment (also described by quantum channels)
- a simulation method: the user can choose between a deterministic and a stochastic method to compute the final result.
- The deterministic method is based on a density-matrix representation of the system and is hence limited to small numbers of qubits.
- The stochastic method is based a statevector representation, and can thus be used for larger numbers of qubits. It may necessitate a large number of samples to converge to a reliable result
In the following, we choose a predefined hardware model, DefaultHardware, which describes an ideal set of gates: each gate is represented by a unitary matrix. The gate times and environmental noise can be specified by the user at the creation of the hardware model.
To learn more about quantum channels in the QLM, check out this tutorial. To learn how to define your own hardware model (with e.g gates defined by quantum channels, and custom environmental noise), check out this tutorial.
Python usage¶
Basic usage: instantaneous gates, no noise¶
This code snippet illustrates the different steps to
- specify a (oversimplified) noisy quantum processor and
- carry out a computation.
from qat.hardware import DefaultHardwareModel
# import one Quantum Processor Unit Factory
from qat.qpus import NoisyQProc
# here, we define a DefaultHardwareModel without arguments: the gates are supposed to be ideal, instantaneous
# and there is no environmental noise: this is a perfect quantum hardware
hardware_model = DefaultHardwareModel()
# we choose the deterministic method to simulate the circuit
# this method is costly in terms of memory as it is based on the density matrix
noisy_qpu = NoisyQProc(hardware_model=hardware_model, sim_method="deterministic")
# create a task for this circuit + qpu
job = circ.to_job(nbshots=5)
# we submit the job to the noisy QPU
# this simulates the measured output state of a quantum computer
result = noisy_qpu.submit(job)
for sample in result:
print("State %s, probability %s, err %s"%(sample.state, sample.probability, sample.err))
State |000>, probability 0.6, err 0.2449489742783178 State |011>, probability 0.4, err 0.2449489742783178
In res, the err field corresponds to the error bar on the probability. It is set to None for deterministic simulation methods.
We can also print out all the computational basis states with nonzero probability. Note that contrary to ideal circuit simulators, we do not have access to the quantum amplitudes, but merely the probabilities:
job = circ.to_job()
result = noisy_qpu.submit(job)
for sample in result:
print("State %s, probability %s, err %s"%(sample.state, sample.probability, sample.err))
State |000>, probability 0.4999999999999999, err None State |011>, probability 0.4999999999999999, err None
We can compare the results with those of an ideal circuit simulated with linalg, for instance. Because our current hardware model corresponds to a perfect quantum processor, we should get the same results:
# import one Quantum Processor Unit Factory
from qat.qpus import LinAlg
# Create a Quantum Processor Unit
result = LinAlg().submit(circ.to_job())
for res in result:
print("State %s, probability %s, err %s"%(res.state, res.probability, res.err))
State |000>, probability 0.4999999999999999, err None State |011>, probability 0.4999999999999999, err None
Basic usage: user-defined gate times, amplitude-damping and pure-dephasing noises¶
Definition of two noisy quantum processors¶
Here, we construct a hardware model with defects.
We choose to model environmental noise with two simple kinds of noise: amplitude damping and pure dephasing noise with their respective characteristic decay times, $T_1$ and $T_\varphi$ (in the Lindblad approximation). (see here for more details.)
With this model, we define to noisy quantum processing units (QPUs), one which uses the deterministic simulation method, one which uses the stochastic one.
from qat.hardware import DefaultHardwareModel
from qat.quops import ParametricAmplitudeDamping, ParametricPureDephasing
# Here, we instantiate the DefaultHardware with gate times and a description of environmental noise
hardware_model = DefaultHardwareModel(
gate_times = {"H": 2, "PH": lambda angle : 5*angle, "CNOT": 30, "SWAP": 40,
"C-H": 40, "C-PH": lambda angle : 40},
idle_noise = [ParametricAmplitudeDamping(T_1=200), ParametricPureDephasing(T_phi=100)]
)
# We define a first noisy quantum processor with this hardware model,
# and a deterministic method to simulate the computation of the circuit
noisy_qpu_1 = NoisyQProc(hardware_model = hardware_model, sim_method = "deterministic")
# We define a second noisy quantum processor with the same hardware model,
# but this time with a stochastic method to simulate the computation of the circuit
from qat.qpus import LinAlg
noisy_qpu_2 = NoisyQProc(hardware_model=hardware_model,
sim_method="stochastic",
backend_simulator=LinAlg(),
n_samples=10000)
# We display the circuit in the form of a time sequence. The horizontal dimension of the boxes is proportional to the gate duration
%matplotlib inline
circ.display(hardware_model=hardware_model)
/tmp/ipykernel_42486/2421439955.py:25: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator. circ.display(hardware_model=hardware_model)
We made a particular assumption on the timing of each gate: we assume that gates applied on different qubits can be applied simultaneously. For instance, in the plot above, the first Hadamard gate (qubit Q2) is applied at the same time as the controlled Hadamard gate (qubits Q0 and Q1).
If the sim_method is stochastic, one can specify the backend statevector simulator through the option backend_simulator. Here, we used qat.linalg.LinAlg(), but could also have used e.g qat.mps.MPS() or qat.feynman.Feynman().
The n_samples option is also specific to the stochastic method. The error bar on the output probabilities depends on n_samples as $\propto 1/\sqrt{n_\mathrm{samples}}$.
Finite number of shots¶
Here, run the same circuit 10 times on the first noisy QPU.
job = circ.to_job(nbshots=10)
# we submit the job to the noisy QPU
# this simulates the measured output state of a quantum computer
result = noisy_qpu_1.submit(job)
for sample in result:
print("State %s, probability %s, err %s"%(sample.state, sample.probability, sample.err))
State |011>, probability 0.2, err 0.13333333333333336 State |000>, probability 0.8, err 0.13333333333333333
We run the same task with the second QPU, which uses a stochastic method to simulate the noisy circuit evolution:
job = circ.to_job(nbshots=10)
# we submit the job to the noisy QPU
# this simulates the measured output state of a quantum computer
result = noisy_qpu_2.submit(job)
for sample in result:
print("State %s, probability %s, err %s"%(sample.state, sample.probability, sample.err))
gtot= 1.0000000000000009 State |000>, probability 0.5, err 0.16666666666666666 State |011>, probability 0.5, err 0.16666666666666666
The err field in Result is the error bar on the probability, for the number of samples (here 10000) specified by the user.
Infinite number of shots¶
With a simulator, one can compute the probabilities corresponding to an infinite number of shots or repetitions. The default value of the nbshots parameter, 0, corresponds to this situation:
# deterministic simulation
result = noisy_qpu_1.submit(circ.to_job())
for sample in result:
print("State %s, probability %s, err %s"%(sample.state, sample.probability, sample.err))
# stochastic simulation
result = noisy_qpu_2.submit(circ.to_job())
for sample in result:
print("State %s, probability %s, err %s"%(sample.state, sample.probability, sample.err))
State |000>, probability 0.708625873813005, err None State |011>, probability 0.29137412618699465, err None gtot= 1.0000000000000009
State |000>, probability 0.701336687831433, err 0.0047712540620501835 State |011>, probability 0.297171698295714, err 0.0036365314716408294
Note that the probabilities obtained via the stochastic simulation are not exactly equal to those obtained via the deterministic one, but will converge as $\propto 1/\sqrt{n_\mathrm{samples}}$ towards the deterministic values.