{
"cells": [
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "slide"
}
},
"source": [
"# Quantum computing hands-on\n",
"\n",
"This notebook presentation is part of the series of lecutures on the basics of quantum computing of the CERN Openlab Summer Student Programme 2024. It contains a general introduction to the Pennylane software platform with guided excercises.\n",
"\n",
"**Giulio Crognaletti**, PhD student @ University of Trieste"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "slide"
}
},
"source": [
"## Hands-on with Pennylane"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Why Pennylane?\n",
"\n",
"Many of you may be wondering why Pennylane? Isn't Qiskit the most common Quantum SDK? Why bother learning a new one?\n",
"\n",
"While it is true that Qiskit is a very popular and very good quantum software toolkit, there are some peculiar advantages that a platform like Pennylane has over Qiskit. As an example, it is higly optimized for parameterized circuits, as it integrates *automatic differentiation* capabilities to the quantum simulators, making it a lot faster when it comes to Quantum Machine Learning (QML) applications!"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "slide"
}
},
"source": [
"While in this lecture we won't get into QML, it's still useful to know the basics of the framework, as it can be used for a wide variety of quantum applications, including more standard quantum algorithms. The most valuable source of information in this regard is of course the official API guide (https://docs.pennylane.ai/en/stable/code/qml.html), which contains documentation, tutorials and more.\n",
"\n",
"In this lecture, we'll go through the main concepts, necessary to succesfully build and execute a simple circuit. In particular we will learn about:\n",
"\n",
"* quantum functions (i.e. circuits)\n",
"* observables\n",
"* devices and quantum nodes\n",
"\n",
"Let's get started!"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### First steps\n",
"\n",
"The first step is to install and import the pennylane package. The standard import is `qml`.\n",
"\n",
"Since it will be useful to use numpy primitives and functions throught the tutorial, we will import numpy as well. Note however that is **crucial** to import numpy directly *from* pennylane. This is important to make sure that automatic differentiation works properly."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Install Pennylane\n",
"#!pip install pennylane\n",
"\n",
"# Import it\n",
"import pennylane as qml\n",
"from pennylane import numpy as np"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"qml.__version__"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Circuits vs quantum functions\n",
"\n",
"In most quantum SDKs there are dedicated primitives that implement *Quantum circuits*. If you already have some experience with quantum computing, you may recall that indeed, this was the case for IBM's Qiskit, where quantum algorithms are realized as specific circuit objects:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from qiskit import QuantumCircuit, QuantumRegister\n",
"\n",
"theta_value = np.array([0.1, 0.3, -1.6], requires_grad=True)\n",
"\n",
"qr = QuantumRegister(2)\n",
"qiskit_circ = QuantumCircuit(qr)\n",
"\n",
"for t in theta_value:\n",
" qiskit_circ.x(qr[0])\n",
" qiskit_circ.ry(t.item(), qr[1])\n",
" qiskit_circ.cz(qr[0], qr[1])\n",
"\n",
"qiskit_circ.measure_all()\n",
"\n",
"qiskit_circ.draw(\"mpl\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"While this approach is straight-forward to understand, this makes handling some situations more cumbersome than really needed. Take for instance the case of a circuit whose gates, depend on some parameter $\\theta$. What if I want to change the numerical value of $\\theta$? This would either require the creation of a new circuit, or usage of more complicated abstractions, which can make our code hard to develop. "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"At the end of the day, quantum circuits are a kind of function, that map some intitial state to the final state, so why not use this and implement a *quantum function*?"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"This is Pennylane's strategy, where circuits are represented by special kinds of **functions**, which are called quantum functions. In particular, each parameterized circuit $U(\\cdot)$ is represented by a function the acts on parameter space $\\Theta = [-\\pi,\\pi]^{p}$ and returns the circuit output, which in general is a collection of expectation values of observables $\\{O_i\\}_{i=1}^n$.\n",
"\n",
"$$f_{U, \\psi_0} : \\Theta \\to \\mathbb {R}^n$$\n",
"$$ \\theta \\in \\Theta \\to \\left[\\langle \\psi_0 |U^\\dagger (\\theta) O_1 U(\\theta)|\\psi_0\\rangle, ... , \\langle \\psi_0 |U^\\dagger (\\theta) O_n U(\\theta)|\\psi_0\\rangle\\right] \\in \\mathbb {R}^n$$\n",
"\n",
"This framework can be generalized to include also other types of circuit outputs, for instance computational basis measurements, or the quantum statevector itself (for simulations), and of course a **non paramterized** circuit is still a function, but with no inputs. \n",
"\n",
"In pennylane, quantum functions are implemented simply as Python functions, and evaluation of quantum functions are linked to the estimation of the circuit output specified during the definition.\n",
"\n",
"Writing a quantum function is as simple as this:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# The parameters of the circuit are specified as the input of the function\n",
"def circ_fn(theta):\n",
"\n",
" #By default, the initial state is |0>, just as in Qiskit\n",
"\n",
" # Gates, operations, or even other quantum functions called here will be part of circ\n",
" # Cycles and other control flow structures are allowed, as long as they do not depend \n",
" # on the value of the parameters\n",
" for t in theta:\n",
" qml.PauliX(0)\n",
" qml.RY(t, 1)\n",
" qml.CZ([0,1])\n",
"\n",
" # The output type is defined with the return statement. In this case computational basis measurement is specified.\n",
" return qml.probs()\n",
"\n",
"_ = qml.draw_mpl(circ_fn, decimals=2)(theta_value)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Pennylane offers a wide variety of gates, which you can discover at https://docs.pennylane.ai/en/stable/code/qml.html#classes\n",
"\n",
"Moreover, in pennylane, quantum functions are always *differentiable*, and gradients of quantum functions can be computed by a wide variety of methods. "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### E.1\n",
"\n",
"Create and display a quantum fuction for a circuit that includes at least a CNOT, one Hadamard gates and one X gate, and return the marginal probabilities for each qubit.\n",
"\n",
"(*Hint*: The marginal proability of a qubit can be obtain by specifying it to `qml.probs`, i.e. `qml.probs(0)` represents the marginal probability of the zeroth quit)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Live demo\n",
"\n",
"_ = qml.draw_mpl(bell_state)()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Observables\n",
"\n",
"Once we have formulated our very cool quantum algorithm, which elegantly encodes the problem in the quantum system, we might face the dilemma of how *to extract* the information provided by the quantum computation. Of course, we generally **do not** have access to the full state of the system, so we have to be smart about it.\n",
"\n",
"Generally, we need to devise a strategy that will allow us to know only the interesting properties of the state, i.e. measure some **observable**.\n",
"\n",
"In quantum theory, an observable is defined as an operator $O=O^\\dagger$ acting on the system's Hilbert space $\\mathcal{H}$. Due to the exponential dimension of $\\mathcal{H}$ ($\\text{dim}(\\mathcal{H})=2^q$), describing $O$ is in general quite demanding. \n",
"\n",
"However, for simple enough observables, there exist very compact descriptions that can be used to define it:\n",
"\n",
"* **Pauli decomposition**: It is common to define observables as linear combination of tensor produts of Pauli operators $\\sigma_k$ acting on single qubits. This is especially true for Hamiltonians, that often take the form $$ H = \\sum_i c_i P_i \\;\\;\\; \\text{where} \\;\\;\\;\\; P_i = \\bigotimes_k \\sigma^{(k)}_{i_k}$$\n",
"(In fact, every operator acting on a quantum register can be decomposed in this way, but this strategy can incurr in exponential number of terms to keep track.)\n",
"\n",
"* **Hermitian matrix**: Given a basis, each observable is represented by a Hermitian matrix, which fully characterizes $O$. However this type of description is suited only for small sistems, i.e. few qubits.\n",
"\n",
"Pennylane allows to use both descriptions to characterize an observable. As an example, let's implement\n",
"$$O = 1.7 X^{(0)}\\otimes Y^{(1)} - 0.2 Z^{(0)}\\otimes Z^{(1)}$$"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Pauli decomposition\n",
"\n",
"# First we need to define the list of coefficients of the linear combination\n",
"cs = [1.7, -0.2]\n",
"\n",
"# Then we need to implement the pauli strings\n",
"# @ indicates the tensor products\n",
"os = [qml.PauliX(0)@qml.PauliY(1), qml.PauliZ(0)@qml.PauliZ(1)]\n",
"\n",
"O_ham = qml.Hamiltonian(cs, os)\n",
"\n",
"# Hermitian matrix\n",
"\n",
"O_mat = [[0.2,0,0,-1.7j],\n",
" [0,-0.2,1.7j,0],\n",
" [0,-1.7j,-0.2,0],\n",
" [1.7j,0,0,0.2]]\n",
"\n",
"O_her = qml.Hermitian(O_mat, wires=(0,1)) #Note: qubits in Pennylane are called \"quantum wires\""
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Both of them can be used as the output of a quantum function, which means that the information extracted from running the circuit, will be automatically used to compute the expectation values of those observable!"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def obs_circ_fn(theta):\n",
"\n",
" for t in theta:\n",
" qml.PauliX(0)\n",
" qml.RY(t, 1)\n",
" qml.CZ([0,1])\n",
" \n",
" return qml.expval(O_ham)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Note that, computational basis measurements are also measurements of this kind. In that case we need $q$ observables, one per qubit, each of which is represented by the Pauli $Z$ operator."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### E.2\n",
"\n",
"Build the 4 qubit observable:\n",
"\n",
"$$O = 0.25*Z \\otimes 1\\!\\!1 \\otimes 1\\!\\!1 \\otimes 1\\!\\!1 + 0.25*1\\!\\!1 \\otimes Z \\otimes 1\\!\\!1 \\otimes 1\\!\\!1 + 0.25*1\\!\\!1 \\otimes 1\\!\\!1 \\otimes Z \\otimes 1\\!\\!1 + 0.25* 1\\!\\!1 \\otimes 1\\!\\!1 \\otimes 1\\!\\!1 \\otimes Z$$"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Live demo"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Devices and Quantum Nodes\n",
"\n",
"Quantum functions alone, just as quantum circuits, are only an abstract description of the quantum algorithm, and are not enough to acutally perform computations. For that, we need *devices*.\n",
"\n",
"In pennylane, a device represents an entity able to execute quantum functions, be it a simulator or a real quantum device.\n",
"\n",
"Thanks to the large amout of interfaces and plugins that this environment provides (https://pennylane.ai/plugins/) code written in pennylane can run almost on all quantum simulator (ideal, noisy, tensor-network based etc..) and even real devices (including IBM public machines!). Of course, each device has its own peculiarity and must be configured with care. \n",
"\n",
"Today we'll focus on the simplest one, i.e. `deafult.qubit`."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Each device is created by calling qml.device, and specifying its device name, toghether with all required properties\n",
"\n",
"dev = qml.device(\"default.qubit\", wires=2, shots=None) # If no shots are specified, the simulation will be exact "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"In order to run, the device and the quantum function must be joined to form a so called \"Quantum Node\" or \"QNode\". QNodes are the most important objects in Pennylane, since they are responsible of handling *execution* and possibly *differentiation* of quantum functions."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"obs_qnode = qml.QNode(obs_circ_fn, dev)\n",
"\n",
"# Finally execute the function\n",
"obs_qnode(theta_value)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Since the concept of qnode and quantum function are so close to each other, and often one needs to create both at the same time, Pennylane offers a quick way of doing so, using the `qml.qnode` *decorator*."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"@qml.qnode(dev, interface=\"autograd\", diff_method=\"backprop\")\n",
"def decorated_qfun(theta):\n",
"\n",
" for t in theta:\n",
" qml.PauliX(0)\n",
" qml.RY(t, 1)\n",
" qml.CZ([0,1])\n",
" \n",
" return qml.expval(O_ham)\n",
"\n",
"decorated_qfun(theta_value)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Preparing the GHZ state and measure it\n",
"\n",
"As an example of the above, let us prepare the GHZ state and measure the expectation value of the observable in E.2.\n",
"\n",
"What is a GHZ state?\n",
"\n",
"This one:\n",
"$$|GHZ\\rangle = \\frac{|000...0\\rangle + |111...1\\rangle}{\\sqrt{2}}$$\n",
"\n",
"Why the GHZ? \n",
"\n",
"Well, because it is, in a sense, a quantum state that is *very quantum* compared to other classes of states. In fact it exhibits a large degree of entanglement (by many, but crucially not all measures), and has interesting properties related to non-locality. In general it is a good starting point to explore quantum computing.\n",
"\n",
"How can we prepare it? What information can we get out of it?\n",
"\n",
"First, we need to fix the number of qubits. Here we will work with 4. Then we look at the circuit we need to implement:\n",
"\n",
"![image](ghz.ppm)\n",
"\n",
"For the rest, we let the code talk!"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Live demo\n",
"\n",
"#Number of qubits (or \"quantum wires\")\n",
"W = 4 \n",
"\n",
"#Steps:\n",
"# 1) Define the appropriate quantum function\n",
"\n",
"_ = qml.draw_mpl(ghz)()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# 2) Define the observable"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# 3) Build a QNode\n",
"\n",
"#Specify the device\n",
"\n",
"#Build the qnode"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# 4) Execute the quantum function"
]
}
],
"metadata": {
"celltoolbar": "Slideshow",
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.3"
}
},
"nbformat": 4,
"nbformat_minor": 4
}