Service overview
Terms of service

Privacy policy


Sign in
Sign up

Quantum computing with PyTorch

Yuichiro Minato

2024/01/08 14:00

!pip --no-cache-dir install -U torch
Requirement already satisfied: torch in /opt/conda/lib/python3.10/site-packages (2.1.2)

Requirement already satisfied: filelock in /opt/conda/lib/python3.10/site-packages (from torch) (3.13.1)

Requirement already satisfied: typing-extensions in /opt/conda/lib/python3.10/site-packages (from torch) (4.5.0)

Requirement already satisfied: sympy in /opt/conda/lib/python3.10/site-packages (from torch) (1.12)

Requirement already satisfied: networkx in /opt/conda/lib/python3.10/site-packages (from torch) (3.2.1)

Requirement already satisfied: jinja2 in /opt/conda/lib/python3.10/site-packages (from torch) (3.1.2)

Requirement already satisfied: fsspec in /opt/conda/lib/python3.10/site-packages (from torch) (2023.5.0)

Requirement already satisfied: nvidia-cuda-nvrtc-cu12==12.1.105 in /opt/conda/lib/python3.10/site-packages (from torch) (12.1.105)

Requirement already satisfied: nvidia-cuda-runtime-cu12==12.1.105 in /opt/conda/lib/python3.10/site-packages (from torch) (12.1.105)

Requirement already satisfied: nvidia-cuda-cupti-cu12==12.1.105 in /opt/conda/lib/python3.10/site-packages (from torch) (12.1.105)

I'll be using PyTorch to advance quantum computing and machine learning. To start, I'll be writing a simple quantum circuit, primarily involving:

    • Quantum Bits (Qubits)
    • Quantum Gates
    • Measurement
    • Hamiltonian for calculating expectations

The representation for these elements may differ somewhat from traditional methods. For explanatory purposes, I'll be using a simpler tool like NetworkX, rather than conventional quantum circuit visualization tools."

import matplotlib.pyplot as plt import networkx as nx import numpy as np G = nx.Graph() nx.add_path(G, ["q0", "H", "C", "M0"]) nx.add_path(G, ["q1", "X", "M1"]) nx.add_path(G, ["C", "X"]) pos = {"q0":[0, 0], "H":[1, 0], "C":[2, 0], "M0":[3, 0], "q1":[0, -1], "X":[2, -1], "M1":[3, -1]} nx.draw(G, pos, with_labels = True)
<Figure size 640x480 with 1 Axes>output

q0 and q1 correspond to quantum bits, and traditionally, the state vector is given separately as |0> = [1, 0] for each.

The H gate is a quantum gate, so it corresponds to a unitary matrix. Regarding the CX gate, in NetworkX, it appears as separate nodes for C and X, but typically, it should be represented as a single matrix with two arms and one node. However, in this case, there are two nodes, each with three arms. The measurements are also labeled as M0 and M1, each assigned to a specific node.

I have a vague memory of having written an article about this before, but I'll start from scratch again.

import torch.optim as optim import torch import numpy as np

q0 is set to |0>, and similarly, q1 is also configured.

q0 = torch.tensor([1,0]) q1 = torch.tensor([1,0])

Next is the H gate. Since it involves matrices, we'll implement it straightforwardly using matrices.

H = torch.tensor([[1.,1],[1,-1]])/np.sqrt(2)

To calculate q0 and H as a test, you simply need to multiply q0 by H.

M0= torch.matmul(q0,H)
tensor([0.7071, 0.7071])

Great, you've successfully obtained the state vector.

Next, I'd like to try the calculation using "einsum." Fundamentally, it's the same approach, but it worked well.

M0 = torch.einsum("a,ab->b", (q0, H)) print(M0)
tensor([0.7071, 0.7071])

It seems that using "einsum" for calculations might be more straightforward.

Later on, I'd like to explore calculations with variational circuits, so I'll investigate whether it's possible to perform quantum computations with gradient information.

To incorporate gradients, it was necessary to rewrite the state vector and quantum gates as floats.

q0 = torch.tensor([1.,0.]) H = torch.tensor([[1.,1],[1,-1]], requires_grad=True)/np.sqrt(2)

Let's try

M0 = torch.einsum("a,ab->b", (q0, H)) print(M0)
tensor([0.7071, 0.7071], grad_fn=<ViewBackward0>)

This time, let's create an entangled state. Prepare two quantum bits, an H gate, and a CX gate.

q0 = torch.tensor([1.,0]) q1 = torch.tensor([1.,0]) H = torch.tensor([[1.,1],[1,-1]])/np.sqrt(2) CX = torch.tensor([[1.,0,0,0],[0,1,0,0],[0,0,0,1],[0,0,1,0]])

Let's try calculating step by step. First, we'll apply the H gate to q0.

res1 = torch.einsum("a,ab->b", (q0,H)) print(res1)
tensor([0.7071, 0.7071])

Next, we'll put this quantum state and q1 into the CX gate. However, instead of using CX as is, we'll transform it into a tensor. We'll convert it from a matrix to a tensor.

CX_four = CX.reshape([2,2,2,2]) print(CX_four)
tensor([[[[1., 0.],

          [0., 0.]],

         [[0., 1.],

          [0., 0.]]],

        [[[0., 0.],

          [0., 1.]],

Now it's become a fourth-order tensor. Let's proceed with the calculation.

res2 = torch.einsum("a,b,abcd->cd", (res1, q1, CX_four)).reshape(4) print(res2)
tensor([0.7071, 0.0000, 0.0000, 0.7071])

How about that? We've successfully created the Bell state. It's quite straightforward. Of course, you can also do it all at once.

res2 = torch.einsum("a,ab,c,bcde->de", (q0, H, q1, CX_four)).reshape(4) print(res2)
tensor([0.7071, 0.0000, 0.0000, 0.7071])

Since we're at it, I'd like to try quantum-classical variational computation. This time, I'll replace H with RY (to minimize the use of complex numbers) and use a variational algorithm to create |11>.

As a criterion for detecting |11>, I'd like to use the expectation value of the Hamiltonian. I aim to train in a way that makes the value of Z0 + Z1 equal to -2. Since we want to compute the expectation value this time, we'll prepare a tensor with Z operators. When visualizing the expectation value of Z0, it looks like...

G = nx.Graph() nx.add_path(G, [0,1,2,3,4,5,6]) nx.add_path(G, [7,8,10,11]) nx.add_path(G, [2,8]) nx.add_path(G, [4,10]) pos = {0:[0, 0], 1:[1, 0], 2:[2, 0], 3:[3, 0], 4:[4, 0], 5:[5, 0], 6:[6, 0], 7:[0, -1], 8:[2, -1], 10:[4, -1], 11:[6, -1], } nx.draw(G, pos)
<Figure size 640x480 with 1 Axes>output

You just need to calculate <psi|Z0+Z1|psi>.

#initial random parameter a = torch.tensor([np.random.rand()],requires_grad=True) #optimize the parameter with adam op = optim.Adam([a],lr=0.05) #qubits q0 = torch.tensor([1.,0]) q1 = torch.tensor([1.,0]) #Z operator for expectation value Z = torch.tensor([[1.,0],[0,-1]]) #list for result arr = [] #make a loop for optimization for _ in range(100): #making RY gate using pytorch function to keep the grad RY = torch.tensor([[1,0],[0,1]])*torch.cos(a/2) + torch.tensor([[0,-1],[-1,0]])*torch.sin(a/2) #let's prepare the tensor net1 = torch.einsum("a,ab,c,bcde->de", (q0, RY, q1, CX_four)) net2 = torch.einsum("a,ab,c,bcde->de", (q0, RY, q1, CX_four)) #add Z0 and Z1 expectation expt = torch.einsum("ab,cb,ac->", (net1, net2, Z)) + torch.einsum("ab,ad,bd->", (net1, net2, Z)) #append expectation value to the list arr.append(expt.detach().numpy()) #optimization step op.zero_grad() expt.backward() op.step() #draw plt.plot(arr)
<Figure size 640x480 with 1 Axes>output

You've successfully implemented VQE with PyTorch. Now, let's check whether the parameter 'a' has been optimized.

tensor([3.1298], requires_grad=True)

It's almost 3.14, and the quantum bits are set to 1 with RY, followed by CX making both 1. We've successfully achieved |11>.

Quantum computations have been carried out using PyTorch's functionalities. Not only can we leverage GPUs, but we can also seamlessly integrate classical neural networks. That's all for now!

© 2024, blueqat Inc. All rights reserved