import numpy as np
from qiskit.visualization import array_to_latex
= [[1], [0]]
zero
=zero, prefix='|0\\rangle = ', max_size=(10,10)) array_to_latex(array
\[ |0\rangle = \begin{bmatrix} 1 \\ 0 \\ \end{bmatrix} \]
Now, do we have to create our own set of gates and operations? Well, it might be a good practice as the formalism can be easily reproduced by setting the basic needs:
We can then create our own set of functions and objects to simulate those computations:
We would easily create basic vector structures for our quantum framework. The minimum unit is the qubit and in order to frame the potential quantum states it may hold we would need to create the computational basis set \(\{|0\rangle, |1\rangle \}\).
import numpy as np
from qiskit.visualization import array_to_latex
= [[1], [0]]
zero
=zero, prefix='|0\\rangle = ', max_size=(10,10)) array_to_latex(array
\[ |0\rangle = \begin{bmatrix} 1 \\ 0 \\ \end{bmatrix} \]
= [[0], [1]]
one
=one, prefix='|1\\rangle = ', max_size=(10,10)) array_to_latex(array
\[ |1\rangle = \begin{bmatrix} 0 \\ 1 \\ \end{bmatrix} \]
Now lets try with some gates.
\[ X = \left[ \begin{array}{cc} 0 & 1 \\ 1 & 0 \end{array} \right] \quad H = \frac{1}{\sqrt{2}}\left[ \begin{array}{cc} 1 & 1 \\ 1 & -1 \end{array} \right] \]
= [[0,1],[1,0]]
X
=X, prefix='X = ', max_size=(10,10)) array_to_latex(array
\[ X = \begin{bmatrix} 0 & 1 \\ 1 & 0 \\ \end{bmatrix} \]
= np.dot((1/(np.sqrt(2))), [[1, 1], [1, -1]])
hadamard
=hadamard, prefix='H = ', max_size=(10,10)) array_to_latex(array
\[ H = \begin{bmatrix} \frac{\sqrt{2}}{2} & \frac{\sqrt{2}}{2} \\ \frac{\sqrt{2}}{2} & - \frac{\sqrt{2}}{2} \\ \end{bmatrix} \]
Well, it is already taking shape. We can test if the outcome matches our expectations.
= np.dot(hadamard, zero)
superposition
=superposition, prefix='H|0\\rangle = |+\\rangle = ', max_size=(10,10)) array_to_latex(array
\[ H|0\rangle = |+\rangle = \begin{bmatrix} \frac{\sqrt{2}}{2} \\ \frac{\sqrt{2}}{2} \\ \end{bmatrix} \]
= np.dot(X, zero)
one
=one, prefix='X|0\\rangle = |1\\rangle = ', max_size=(10,10)) array_to_latex(array
\[ X|0\rangle = |1\rangle = \begin{bmatrix} 0 \\ 1 \\ \end{bmatrix} \]
We can scale it to a couple of qubits to see what we get. Let’s try to create on of the bell states we saw during class.
\[ |\Phi^{+}\rangle = \frac{1}{\sqrt{2}}(|00\rangle + |11\rangle) \quad |\Phi^{-}\rangle = \frac{1}{\sqrt{2}}(|00\rangle - |11\rangle) \] \[ |\Psi^{+}\rangle = \frac{1}{\sqrt{2}}(|01\rangle + |10\rangle) \quad |\Psi^{-}\rangle = \frac{1}{\sqrt{2}}(|10\rangle - |10\rangle) \]
We will need the CNOT gate for this.
= [[1,0,0,0],[0,1,0,0],[0,0,0,1],[0,0,1,0]]
CNOT
=CNOT, prefix='CNOT = ', max_size=(10,10)) array_to_latex(array
\[ CNOT = \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 1 \\ 0 & 0 & 1 & 0 \\ \end{bmatrix} \]
With this we will create a two qubit system, apply the Hadamard gate to the first one and the CNOT gate with the conrol over the first qubit as well.
# Initial state
= np.kron(zero, zero)
init_state
# (I tensor Hadamard)
= np.kron(hadamard, np.eye(2)) HI
With that we can perform the full operation
\[ CNOT (I\otimes H)|00\rangle = |\Phi^{+}\rangle = \frac{1}{\sqrt{2}}(|00\rangle + |11\rangle \]
= np.dot(HI, init_state)
psi_1 = np.dot(CNOT, psi_1)
psi
=psi, prefix='|\\psi\\rangle = ', max_size=(10,10)) array_to_latex(array
\[ |\psi\rangle = \begin{bmatrix} \frac{\sqrt{2}}{2} \\ 0 \\ 0 \\ \frac{\sqrt{2}}{2} \\ \end{bmatrix} \]
There you go. This is our entangled 2-qubit state. Building the whole formalism from scratch might be tedious, but it helps us understand every detail of it.
Of course, in order to continue forward, we will take advantage of the collective effort and use some existing tools to ease our way into quantum computing.
Of course, we are not the first ones with this need, so there do exist some quite useful libraries in this domain. QuTiP might be one of the most used ones (at least for us, Python enthusiasts).
import scipy
import numpy as np
from qutip import Qobj, mesolve
from qutip import basis, tensor
from qutip.qip.operations import cnot
# Basis states
= basis(2,0)
zero = basis(2,1)
one
# |10>
= tensor(one, zero)
one_zero
# CNOT
= cnot().full()
hamiltonian
# e^itH
= Qobj(1j * scipy.linalg.logm(hamiltonian), dims=[[2] * 2] * 2)
u_generator
# Time range 0.0 -> 1.0
= np.arange(0, 1.1, 0.1)
times
# \psi = H\psi_0
= mesolve(u_generator, one_zero, times) evolution
0] evolution.states[
Quantum object: dims=[[2, 2], [1]], shape=(4, 1), type='ket', dtype=Dense
Qobj data =
[[0.]
[0.]
[1.]
[0.]]
1] evolution.states[
Quantum object: dims=[[2, 2], [1]], shape=(4, 1), type='ket', dtype=Dense
Qobj data =
[[0. +0.j ]
[0. +0.j ]
[0.97552824+0.15450855j]
[0.02447175-0.15450855j]]
-1] evolution.states[
Quantum object: dims=[[2, 2], [1]], shape=(4, 1), type='ket', dtype=Dense
Qobj data =
[[0.00000000e+00+0.00000000e+00j]
[0.00000000e+00+0.00000000e+00j]
[1.07316277e-06+9.88099162e-07j]
[1.00000000e+00-9.88099162e-07j]]
Let’s go by steps.
from qutip import basis, tensor
= basis(2,1)
one = basis(2,0)
zero
= tensor(one, zero) # |10>
one_zero one_zero
Quantum object: dims=[[2, 2], [1]], shape=(4, 1), type='ket', dtype=Dense
Qobj data =
[[0.]
[0.]
[1.]
[0.]]
from qutip.qip.operations import cnot
= cnot().full()
cnot_matrix cnot_matrix
array([[1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
[0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j],
[0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j],
[0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j]])
In fact, it allows us to do more than just the simple operations we envisioned. For example, we could generate the whole evolution over a period of time of the \(U\) unitary generated for our Hamiltonian.
from qutip import identity
from qutip.qip.operations import hadamard_transform
# hamiltonian
= cnot() * tensor(hadamard_transform(1), identity(2))
hamiltonian hamiltonian
Quantum object: dims=[[2, 2], [2, 2]], shape=(4, 4), type='oper', dtype=Dense, isherm=False
Qobj data =
[[ 0.70710678 0. 0.70710678 0. ]
[ 0. 0.70710678 0. 0.70710678]
[ 0. 0.70710678 0. -0.70710678]
[ 0.70710678 0. -0.70710678 0. ]]
import numpy as np
import scipy
from qutip import Qobj, mesolve
# Initial state
= tensor(zero, zero) # |00>
init_state
# e^itH
= Qobj(1j * scipy.linalg.logm(hamiltonian.full()), dims=[[2] * 2] * 2)
u_generator
# Time range
= np.arange(0, 1.1, 0.1)
times = mesolve(u_generator, init_state, times) evolution
0] evolution.states[
Quantum object: dims=[[2, 2], [1]], shape=(4, 1), type='ket', dtype=Dense
Qobj data =
[[1.]
[0.]
[0.]
[0.]]
1] evolution.states[
Quantum object: dims=[[2, 2], [1]], shape=(4, 1), type='ket', dtype=Dense
Qobj data =
[[ 0.99487486+0.02262725j]
[-0.00204247+0.02262725j]
[-0.03057749-0.05462701j]
[ 0.04788161-0.05462701j]]
-1] evolution.states[
Quantum object: dims=[[2, 2], [1]], shape=(4, 1), type='ket', dtype=Dense
Qobj data =
[[ 7.07106734e-01-2.77290950e-08j]
[-2.74299654e-08-2.77290950e-08j]
[ 6.61796919e-08+6.69439574e-08j]
[ 7.07106828e-01+6.69439574e-08j]]
= np.round(evolution.states[-1].full(), decimals = 5)
psi
=psi, prefix='|\\psi\\rangle = ', max_size=(10,10)) array_to_latex(array
\[ |\psi\rangle = \begin{bmatrix} 0.70711 \\ 0 \\ 0 \\ 0.70711 \\ \end{bmatrix} \]
Looks like \(\frac{1}{\sqrt{2}}(|00\rangle + |11\rangle)\) but due to numerical imprecisions we wil get this type of ugly structures in between. We will need to familiarize with those artifacts as noise when using hardware will produce similar numerical artifacts.
Well, having a theoretical framework may be a good option for simulating and doing some local experimentation but that will reach soon limitations when trying to scale it up. Our classical device won’t be able to perform the whole system calculations and we might need to switch to actual quantum computers doing those. Therefore, we need to find a way to o so.
This is why some manufacturers have invested time and effort on creating open-source frameworks to be adopted by the community (and position themselves). Companies such as IBM or AWS have leveraged their own version that also allows for these programs to be sent to an end device that will perform the set of operations.
from qiskit import QuantumCircuit
= QuantumCircuit(2, 2)
qc 0)
qc.x(0, 1)
qc.cx(0,1], [0,1])
qc.measure([
'mpl') qc.draw(
Qiskit is IBM’s quantum computing toolkit the enables interaction with their devices. Let’s first start replicating the theoretical basis and then move forward up to device simulation.
Opflow is the framework section dedicated to provide the pieces to perform previous computations.
from qiskit.quantum_info import Statevector
= Statevector.from_label('0')
Zero = Statevector.from_label('1') One
= Zero.probabilities()
probs print('Probability of measuring 0: {}'.format(probs[0]))
Probability of measuring 0: 1.0
print('Probability of measuring 1: {}'.format(probs[1]))
Probability of measuring 1: 0.0
= Statevector.from_label('+')
Plus
print(Plus)
Statevector([0.70710678+0.j, 0.70710678+0.j],
dims=(2,))
Plus.probabilities()
array([0.5, 0.5])
Qiskit tends to understand everything in terms of circuits. But in essence we can request the actual operation being performed there and check that the Hadamard action over a \(|0\rangle\) state (the initial state) provides the \(|+\rangle\) state as expected.
=Plus.data, prefix='|+\\rangle = ', max_size=(10,10)) array_to_latex(array
\[ |+\rangle = \begin{bmatrix} \frac{\sqrt{2}}{2} & \frac{\sqrt{2}}{2} \\ \end{bmatrix} \]
What would be the outcome of it?
= Plus.probabilities()
probs print('Probability of measuring 0: {}'.format(probs[0]))
Probability of measuring 0: 0.4999999999999999
This is the expected outcome for the \(\langle 0 | \psi \rangle\) operation. But we can mimic it as well.
print(abs(np.dot(Zero.data, Plus.data.T)**2))
0.4999999999999999
Similarly gate operations can be used.
from qiskit.quantum_info import Pauli
= Pauli('X') X
And what is the amplitude of \(|1\rangle\) after the \(X|0\rangle\) operation?
== One Zero.evolve(X)
True
Qiskit orders bits in a specific manner so some gates may look different but is just a matter of ordering when applying the operations.
from qiskit.circuit.library import UnitaryGate
= [[1., 0., 0., 0.],
matrix 0., 0., 0., 1.],
[0., 0., 1., 0.],
[0., 1., 0., 0.]]
[= UnitaryGate(matrix) CNOT
Let play around with the Bell state we produced before
\[ CNOT (I\otimes H)|00\rangle = |\Phi^{+}\rangle = \frac{1}{\sqrt{2}}(|00\rangle + |11\rangle \]
print(Zero ^ Zero)
Statevector([1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
dims=(2, 2))
from qiskit import QuantumCircuit
= QuantumCircuit(2)
qc 0)
qc.h(
qc.draw()
┌───┐ q_0: ┤ H ├ └───┘ q_1: ─────
from qiskit.quantum_info import Operator
print(Operator(qc).data)
[[ 0.70710678+0.j 0.70710678+0.j 0. +0.j 0. +0.j]
[ 0.70710678+0.j -0.70710678+0.j 0. +0.j 0. +0.j]
[ 0. +0.j 0. +0.j 0.70710678+0.j 0.70710678+0.j]
[ 0. +0.j 0. +0.j 0.70710678+0.j -0.70710678+0.j]]
^ Zero).evolve(qc).data (Zero
array([0.70710678+0.j, 0.70710678+0.j, 0. +0.j, 0. +0.j])
= (Zero ^ Zero).evolve(qc).evolve(CNOT)
bell_state bell_state.data
array([0.70710678+0.j, 0. +0.j, 0. +0.j, 0.70710678+0.j])
=bell_state.data, prefix='|\\psi\\rangle = ', max_size=(10,10)) array_to_latex(array
\[ |\psi\rangle = \begin{bmatrix} \frac{\sqrt{2}}{2} & 0 & 0 & \frac{\sqrt{2}}{2} \\ \end{bmatrix} \]
Of course the gate-based nature of IBM devices makes it more natural to directly code our approach as a gate based circuit.
from qiskit import QuantumCircuit
= QuantumCircuit(2)
qc 0)
qc.h(0,1)
qc.cx(
'mpl') qc.draw(
Most likelly moving forward, this pictorical approach will ease the abstraction but is good to know that, the formalism is still there.
print('Math:', (Zero ^ Zero).evolve(qc).probabilities())
Math: [0.5 0. 0. 0.5]
Of course qiskit offers some other nice ways to simulate and visualize the results.
from qiskit.quantum_info import Statevector
= Statevector.from_instruction(qc) psi
from qiskit.visualization import plot_state_qsphere
plot_state_qsphere(psi)
from qiskit.visualization import plot_bloch_multivector
plot_bloch_multivector(psi)
In order to simulate the actual action of the circuit we will need to add some classical registers and measurement operations.
= QuantumCircuit(2, 2)
circuit
= circuit.compose(qc)
circuit
0,1],[0,1])
circuit.measure([
'mpl') circuit.draw(
from qiskit_aer import AerSimulator
# execute the quantum circuit
= AerSimulator()
simulator
= simulator.run(circuit, shots=1000).result()
result = result.get_counts(circuit)
counts print(counts)
{'11': 510, '00': 490}
from qiskit.visualization import plot_histogram
plot_histogram(counts)
Could we create a state that superposes all potential basis states for a 2-qubit configuration?
\[ \frac{1}{2}|00\rangle + \frac{1}{2}|01\rangle + \frac{1}{2}|10\rangle + \frac{1}{2}|11\rangle \]
= QuantumCircuit(2)
qc
# HERE GOES YOUR CIRCUIT
= Statevector.from_instruction(qc)
psi
plot_bloch_multivector(psi)
plot_state_qsphere(psi)
= QuantumCircuit(2, 2)
circuit = circuit.compose(qc)
circuit 0,1],[0,1])
circuit.measure([
# execute the quantum circuit
= AerSimulator()
simulator
= simulator.run(circuit, shots=1000).result()
result = result.get_counts(circuit)
counts
plot_histogram(counts)