# Intro

In this example, we will show a simple example of parametric quantum program (PQC).

We will take 1 input from the user, and consider 1 weight, while utilizing 1 qubit in the PQC. During this example, the goal of the learning process is to assess the right angle for a Rx gate for performing a "NOT" operation (spoiler, the correct answer is $$\pi$$).

# General flow

In section 1 we will see the code required for defining a quantum layer. This will include:

• section 1.1: defining the quantum model and synthesizing it to a quantum program
• section 1.2: defining the execute and post-process callables
• section 1.3: defining a torch.nn.Module network

In section 2 we will choose our dataset, loss function, and optimizer. Section 3 will demostrate how to handle the learning process, and section 4 will test our network's performance.

If you're not familiar with PyTorch, it is highly recommended that you'll check out the following pages from their documentation:

# Step 1 - Create our torch.nn.Module

## Step 1.1 - Create our parametric quantum program

Our quantum model will be defined and synthesized as follows:

from classiq import synthesize, qfunc, QArray, QBit, RX, Output, allocate, CReal
from classiq.qmod.quantum_function import create_model

@qfunc
def encoding(theta: CReal, q: QArray[QBit]) -> None:
RX(theta=theta, target=q[0])

@qfunc
def mixing(theta: CReal, q: QArray[QBit]) -> None:
RX(theta=theta, target=q[0])

@qfunc
def main(input_0: CReal, weight_0: CReal, res: Output[QArray[QBit]]) -> None:
allocate(1, res)
encoding(theta=input_0, q=res)
mixing(theta=weight_0, q=res)

model = create_model(main)

quantum_program = synthesize(model)


The input (input_0), logically indicating the state |0> or |1>, is transformed into an angle, either 0 or pi.

## Step 1.2 - Create the Execution and Post-processing

The following example defines a function that takes in a parametric quantum program plus parameters, executes the program, and returns the result. Notes:

1. The code can be executed on a physical computer or on a simulator. In any case, implement the execution using execute_qnn.
2. Post-process the result of the execution to obtain a single number (float) and a single dimension Tensor.
import torch

from classiq.applications.qnn.types import (
MultipleArguments,
SavedResult,
ResultsCollection,
)

from classiq.execution import execute_qnn
from classiq.synthesis import SerializedQuantumProgram

def execute(
quantum_program: SerializedQuantumProgram, arguments: MultipleArguments
) -> ResultsCollection:
return execute_qnn(quantum_program, arguments)

# Post-process the result, returning a dict:
# Note: this function assumes that we only care about
#   differentiating a single state (|0>)
#   from all the rest of the states.
#   In case of a different differentiation, this function should change.
def post_process(result: SavedResult) -> torch.Tensor:
"""
Take in a SavedResult with ExecutionDetails value type, and return the
probability of measuring |0> which equals the amount of |0> measurements
divided by the total amount of measurements.
"""
counts: dict = result.value.counts
# The probability of measuring |0>
p_zero: float = counts.get("0", 0.0) / sum(counts.values())


## Step 1.3 - Create a network

Now we're going to define a network, just like any other PyTorch network, only that this time, we will have only 1 layer, and it will be a quantum layer.

import torch

from classiq.applications.qnn import QLayer

class Net(torch.nn.Module):
def __init__(self, *args, **kwargs) -> None:
super().__init__()
self.qlayer = QLayer(
quantum_program,  # the quantum program, the result of synthesize()
execute,  # a callable that takes
# - a quantum program
# - parameters to that program (a tuple of dictionaries)
# and returns a ResultsCollection
post_process,  # a callable that takes
# - a single SavedResult
# and returns a torch.Tensor
*args,
**kwargs
)

def forward(self, x: torch.Tensor) -> torch.Tensor:
x = self.qlayer(x)
return x

model = Net()


## Step 2 - Choose a dataset, loss function, and optimizer

We will use the DATALOADER_NOT dataset, defined here, as well as L1Loss and SGD

from classiq.applications.qnn.datasets import DATALOADER_NOT
import torch.nn as nn
import torch.optim as optim

_LEARNING_RATE = 1.0

# choosing our data
# choosing our loss function
loss_func = nn.L1Loss()
# choosing our optimizer
optimizer = optim.SGD(model.parameters(), lr=_LEARNING_RATE)


## Step 3 - Train

For the training process, we will use a loop similar to the one recommended by PyTorch

import torch.nn as nn
import torch.optim as optim

def train(
model: nn.Module,
loss_func: nn.modules.loss._Loss,
optimizer: optim.Optimizer,
epoch: int = 20,
) -> None:
for index in range(epoch):
print(index, model.qlayer.weight)

output = model(data)

loss = loss_func(output, label)
loss.backward()

optimizer.step()



## Step 4 - Test

Lastly, we will test our network accuracy, using the following answer

def check_accuracy(model: nn.Module, data_loader: DataLoader, atol=1e-4) -> float:
num_correct = 0
total = 0
model.eval()

# Let the model predict
predictions = model(data)

# Get a tensor of booleans, indicating if each label is close to the real label
is_prediction_correct = predictions.isclose(labels, atol=atol)

# Count the amount of True predictions
num_correct += is_prediction_correct.sum().item()
# Count the total evaluations
#   the first dimension of labels is batch_size
total += labels.size(0)

accuracy = float(num_correct) / float(total)
print(f"Test Accuracy of the model: {accuracy*100:.2f}")
return accuracy


The results show that the accuracy is $$1$$, meaning a 100% success rate at performing the required transformation (i.e. the network learned to perform a X-gate). We may further test it by printing the value of model.qlayer.weight, which is a tensor of shape (1,1), which should, after training, be close to $$\pi$$.
In section 1 we defined our parametric quantum program, as well as our execution function. Together, these two are sent as arguments to the QLayer object. In section 2 we set some hyperparameters, and in section 3 we trained our model. Section 4 helped us verify that our network is working as intended.