I have tried to solve Task 1 and Task 2. Please consider Task 1 for my official submission.
This task checks the time required for quantum operations upon increase in the number of computational qubits. For the same purpose I test our results for standard quantum circuit using pennylane's lightning qubit device. this is just for an initial test and has no purpose with the tasks.
Our main solution starts in the next bit where the "simulation" of these quantum operations are carried out using simple arithmetic with the help of numpy library. The vector size increases as O([1,0]
denoting
The technique here is that [1,0,0,0,0,0,0,0]
if you operate it with X(0) it will become [0,0,0,0,1,0,0,0]
. The init state is acquired using np.kron()
looped "n_qubit
" times. Same goes for X & H gates. But the CNOT Gate is not so trivial. So initially I use qiskit's circuit to operator functionality using Operator.from_circuit(QuantumCircuit)
method after sequentially operating the CNOT gate as I wish for value based accuracy. But I thought it could perform better and since our goal is to check the computational load I relaxed the result accuracy a little. Here's what I did : I calculated the kronecker product of CNOT n_qubits//2 times and took the direct sum of an identity matrix with cnot in case of odd num_qubit. This will provide us an operator of appropriate dimensions and I can subsequently calculate the growth rate of runtime with respect to increasing qubits.
Next I use an advanced simulation method by using tensors which is also another "classical" technique. The first idea is to writing the tensors as a tensor of shape (2,2,...)
n times. We treat gates similarly too. We do not need to change single qubit gates as their shape is already ((2,2))
, but we do need to reshape
2 qubit gates since the dimensions they have is np.tensordot
for the target axis in the state and the 1st axis in the single qubit operator (because there are only two axes). Then I rearrange the qubits by using np.transpose
and setting the last qubit in the "target" position and using this list as the transpose method, finally in order to retreieve the correct state. Similarly for double qubit gates we follow the same routine by applying tensordot
as state = np.tensordot(state, gate, axes=([control, target], [0, 1]))
. We then as we did before by bringing the second last qubit in the control position and the last qubit in the target position. We also must pop out the last two entries since then there would be a shape mismatch otherwise. After that we transpose the state in the fashion dictated by the aforementioned axes
list that we have created.
The BONUS section deals with sampling of these simulated quantum states mimicking real measurements using np.random.choice
parameterized by the probability (np.dot(np.conjugate(statevec), np.dot(O, statevec))
.
For each section I have validated my results against traditional pennylane results to show the accuracy of my final product states for each implementation i.e NAIVE matrix as well as ADVANCED tensor.
This tasks deals with the potrayal of increasing noise to the results. For these purposes of noise simulation I was required to build a adder circuit using the Draper algorithm. The drpaer algorithm achieves it using
We use two three bit numbers like 3 ('b'='011') and 4 ('b' = '100') and sum it to get 7 which should be ('b'='111'). The scheme of Draper adder is to first turn b into
Now to simulate noise I have used the add_noise
function which randomly adds X, Y and Z gates into the circuit based on
To potray the effect of noise I sweep alpha and beta for 5 values between [0.5, 1]. And store these circuits to show the count values for each.
To directly address the noise introduced by one-qubit and two-qubit gates in our QPU-transpiled circuit, we can employ both circuit optimization and error mitigation techniques that are compatible with the target QPU’s hardware topology. Our main aim is to minimize noise while preserving the statevector fidelity and ensuring accurate measurement outcomes.
- Trivial methods - Gate Reduction: Qiskit provides optimization levels within the transpiler that minimize the gate count by reducing redundant gate sequences and simplifying the circuit structure. For instance, adjacent gates that cancel each other (like successive Pauli-X gates) or gate pairs that simplify to a lower-cost equivalent (e.g., combining rotations) can be merged, reducing noise.
- More importantly - Native Gate Decomposition: By decomposing the circuit and good transpilation with proper optimization directly into the QPU’s native gate set, we bypass intermediate gates that would require further decomposition, reducing error from accumulated noise.
- Zero-Noise Extrapolation (ZNE): This method simulates low-noise conditions by scaling gate errors in the circuit (like amplifying noise slightly) and then extrapolating the results back to estimate the zero-noise outcome. Practically, this is done by running the circuit at multiple noise levels and fitting the results to “zero” noise—particularly useful for both one-qubit and two-qubit errors.
- Dynamical Decoupling (DD): This method helps tackle the issue of decoherence greatly. Applied at the level of the hardware, DD sequences mitigate noise for one-qubit gates using pulses.
- Qubit Mapping and Routing: Intelligent qubit mapping, where the logical qubits are placed on physically “closer” qubits in the QPU, reduces the need for two-qubit gates across long distances, directly decreasing noise from two-qubit gate errors. This becomes even more effective when combined with the transpiler’s layout optimization, ensuring that the transpilation process respects hardware-specific connectivity.
The counts around the correct answer progressively decreases with increase in the noise probabilities