`!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)
plt.show()
```

`<Figure size 640x480 with 1 Axes>`

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)`

`print(M0)`

```
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)
plt.show()
```

`<Figure size 640x480 with 1 Axes>`

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)
plt.show()
```

`<Figure size 640x480 with 1 Axes>`

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

`print(a)`

```
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