import numpy as np
from typing import *
from qiskit import QuantumCircuit
from qiskit_aer import AerSimulator
from qiskit.quantum_info import Statevector, Operator
from qiskit.visualization import plot_bloch_multivector
sim = AerSimulator()
from util import zero, one, demonstrate_measure, PlotGateOpOnBloch, plot_bloch_vector
Foundations: Quantum Circuits for a Single Qubit System#
In this notebook, we will introduce quantum circuits that act on single qubit system to perform computations in the gate-based model of quantum computation. Later, we will extend our definitions to cover multi-qubit quantum systems and multi-qubit quantum circuits.
References
Quantum Gates on a Single Qubit#
Similar to how we could apply classical logic gates to classical bits to perform computation, we can apply quantum gates to qubits to perform quantum computation. We will introduce a few single qubit quantum gates now, starting with the Pauli gates.
Pauli Gates: Rotations on the Bloch Sphere#
As a reminder, the quantum state of a single qubit system can be encoded on a Bloch sphere. The Pauli gates are a set of 3 gates that encode rotations around the 3 axes of the Bloch sphere: \(X\), \(Y\), and \(Z\). We’ll cover these in turn now.
X Gate#
The X gate performs a rotation around the \(X\)-axis of the Bloch sphere. It is similar to a logical negation.
qc_x = QuantumCircuit(1)  # Create a quantum circuit with a single qubit
qc_x.x(0)                 # Apply an x gate to qubit 0
qc_x.draw(output="mpl", style="iqp")
 
with PlotGateOpOnBloch() as ctx:
    plot_bloch_vector(zero, ax=ctx.ax1, title="Before X")
    plot_bloch_vector(zero.evolve(Operator(qc_x)), ax=ctx.ax2, title="After X")
 
Y Gate#
The Y gate performs a rotation around the \(Y\)-axis of the Bloch sphere. It performs a flip and phase flip.
qc_y = QuantumCircuit(1)
qc_y.y(0)
qc_y.draw(output="mpl", style="iqp")
 
zero.evolve(Operator(qc_y)), one.evolve(Operator(qc_y))
(Statevector([0.+0.j, 0.+1.j],
             dims=(2,)),
 Statevector([0.-1.j, 0.+0.j],
             dims=(2,)))
with PlotGateOpOnBloch() as ctx:
    plot_bloch_vector(zero, ax=ctx.ax1, title="Before Y")
    plot_bloch_vector(zero.evolve(Operator(qc_y)), ax=ctx.ax2, title="After Y")
 
with PlotGateOpOnBloch() as ctx:
    q = 1/np.sqrt(2)*zero + 1/np.sqrt(2)*one
    plot_bloch_vector(q, ax=ctx.ax1, title="Before Y")
    plot_bloch_vector(q.evolve(Operator(qc_y)), ax=ctx.ax2, title="After Y")
 
Z Gate#
The \(Z\) gate performs a rotation around the \(Z\) axis of the Bloch sphere. It performs a phase flip.
qc_z = QuantumCircuit(1)
qc_z.z(0)
qc_z.draw(output="mpl", style="iqp")
 
zero.evolve(Operator(qc_z)), one.evolve(Operator(qc_z))
(Statevector([1.+0.j, 0.+0.j],
             dims=(2,)),
 Statevector([ 0.+0.j, -1.+0.j],
             dims=(2,)))
plot_bloch_multivector(zero.evolve(Operator(qc_z)))
 
Hadamard Gate: Superposition#
The Hadamard gate, written \(H\), takes a \(|0\rangle\) qubit and puts it in superposition. The \(H\) gate is a commonly used gate in many quantum algorithms and protocols.
qc_h = QuantumCircuit(1)
qc_h.h(0)
qc_h.draw(output="mpl", style="iqp")
 
with PlotGateOpOnBloch() as ctx:
    plot_bloch_vector(zero, ax=ctx.ax1, title="Before H")
    plot_bloch_vector(zero.evolve(Operator(qc_h)), ax=ctx.ax2, title="After H")
 
Quantum Circuits#
- As we have seen, a quantum gate translates a point on the Bloch sphere to another point on the Bloch sphere. 
- Since each point on the Bloch sphere encodes a quantum state, a quantum gate can be used to convert input quantum states into output quantum states. We will call a function from an input quantum state to an output quantum state a quantum computation. 
- By sequencing the applications of quantum gates to form a quantum circuit, we can perform more complex transformations. 
Example 1; H-Z#
Here is an example quantum circuit that first applies an \(H\) gate followed by a \(Z\) gate. We read the circuit from left-to-right.
qc = QuantumCircuit(1)
qc.h(0)
qc.z(0)
qc.draw(output="mpl", style="iqp")
 
- This quantum circuit first puts the qubit in superposition with the \(H\) gate (north pole to positive \(X\)). 
- Then, we flip the phase with the \(Z\) gate (positive \(X\) to negative \(X\)). 
with PlotGateOpOnBloch() as ctx:
    plot_bloch_vector(zero, ax=ctx.ax1, title="Before H-Z")
    plot_bloch_vector(zero.evolve(Operator(qc)), ax=ctx.ax2, title="After H-Z")
 
Example 2: Sequencing H Gates#
We define a function that sequentially applies \(H\) gates. The output quantum state from a previous application of a H can be fed as the input to the next \(H\).
def seq_H(n: int) -> QuantumCircuit:
    qc = QuantumCircuit(1)
    for i in range(n):
        qc.h(0)
    return qc
qc2 = seq_H(3)
qc2.draw(output="mpl", style="iqp")
 
with PlotGateOpOnBloch() as ctx:
    plot_bloch_vector(zero, ax=ctx.ax1, title="Before H-H")
    plot_bloch_vector(zero.evolve(Operator(qc2)), ax=ctx.ax2, title="After H-H")
 
Notice that the application of 2 H gates acted like a noop, i.e, it did not affect the quantum state. We’ll check 3 \(H\) gates now.
qc3 = seq_H(3)
qc3.draw(output="mpl", style="iqp")
 
with PlotGateOpOnBloch() as ctx:
    plot_bloch_vector(zero, ax=ctx.ax1, title="Before H-H-H")
    plot_bloch_vector(zero.evolve(Operator(qc3)), ax=ctx.ax2, title="After H-H-H")
 
Observation: Non-Uniqueness#
- We observed that applying 3 \(H\) gates in a row produced the same operator as a single \(H\) gate. 
- Thus there are many quantum circuits that perform the same quantum computation. In other words, a quantum circuit performs a non-unique quantum computation. 
Summary#
- We saw that an operation on a single qubit systems can be implemented by a quantum gate that performs transformations on Bloch spheres. 
- We reviewed important single qubit gates, including the Pauli gates and the Hadamard gate. 
