# Hybrid Classical-Quantum Neural Network Source: https://docs.classiq.io/explore/algorithms/QML/hybrid_qnn/hybrid_qnn_for_subset_majority Open this notebook in GitHub to run it yourself ## Classical Neural Networks Neural networks is one of the major branches in machine learning, with wide use in applications and research. A neural network - or, more generally, a deep neural network - is a parametric function of a specific structure (inspired by neural networks in biology), which is trained to capture specific functionality. In its most basic form, a neural network for learning a function $\vec{f}: \mathbb{R}^N\rightarrow \mathbb{R}^M$ looks as follows: 1. There is an input vector of size $N$ (red circles in Fig. 1). 2. Each entry of the input goes into a hidden layer of size $K$, where each neuron (blue circles in Fig. 1) is defined with an "activation function" $y^{k}(\vec{w}^{(1)}; \vec{x})$ for $k=1,\dots,K$, and $\vec{w}^{(1)}$ are parameters. 3. The output of the hidden layer is sent to the output layer (green circles in Fig. 1) $\tilde{f}^{m}(\vec{w}^{(2)};\vec{y})$ for $m=1,\dots,M$, and $\vec{w}^{(2)}$ are parameters. The output $\vec{\tilde{f}}$ is thus a parametric function (in $\vec{w}^{(1)},\,\vec{w}^{(2)}$), which can be trained to capture the target function $\vec{f}$. png **Deep neural networks** are similar to the description above, having more than one hidden layer. This provides a more complex structure that can capture more complex functionalities. ## Quantum Neural Networks The idea of a quantum neural network refers to combining parametric circuits as a replacement for all or some of the classical layers in classical neural networks. The basic object in QNN is thus a **quantum layer**, which has a classical input and returns a classical output. The output is obtained by running a quantum program. A quantum layer is thus composed of three parts: 1. A quantum part that encodes the input: This is a parametric quantum function for representing the entries of a single data point. There are three canonical ways to encode a data vector of size $N$: [angle-encoding](https://github.com/Classiq/classiq-library/blob/main/functions/qmod_library_reference/classiq_open_library/variational_data_encoding/variational_data_encoding.ipynb) using $N$ qubits, [dense angle-encoding](https://github.com/Classiq/classiq-library/blob/main/functions/qmod_library_reference/classiq_open_library/variational_data_encoding/variational_data_encoding.ipynb) using $\lceil N/2\rceil$ qubits, and amplitude-encoding using $\lceil\log_2N\rceil$ qubits. 1. A quantum ansatz part: This is a parametric quantum function, whose parameters are trained as the weights in classical layers. 2. A postprocess classical part, for returning an output classical vector. The integration of quantum layers in classical neural networks may offer reduction in resources for a given functionality, as the network (or part of it) is expressed via the Hilbert space, providing different expressibility compared to classical networks. This notebook demonstrates QNN by treating a specific function - the subset majority - for which we construct, train, and verify a hybrid classical-quantum neural network. The notebook assumes familiarity with Classiq and NN with PyTorch. See the [QML guide with Classiq](https://github.com/Classiq/classiq-library/blob/main/tutorials/basic_tutorials/qml_with_classiq_guide/qml_with_classiq_guide.ipynb). ## Example: Hybrid Neural Network for the Subset Majority Function For an integer $N$ and a given subset of indices $S \subset \{0,1,\dots,N\}$ we define the subset majority function, $M_{S}:\{0,1\}^{\times N}\rightarrow \{0,1\}$ that acts on binary strings of size $N$ as follows: it returns 1 if the number of ones within the substring according to $S$ is larger than $|S|//2$, and 0 otherwise, $$ M_S(\vec{b}) = \left\{ \begin{array}{l l } 1 & \text{if } \sum_{j\in S} b_{j}>|S|//2, \\ 0 & \text{otherwise} \end{array} \right . $$ For example, we consider $N=7$ and $S=\{0,1,4\}$: * The string 0101110 corresponds to the substring 011, for which the number of ones is 2(>1). Therefore, $M_S(0101110)=1$. * The string 0011111 corresponds to the substring 001, for which the number of ones is 1(=1). Therefore, $M_S(0101110)=0$. # ## Generating Data for a Specific Example Let us consider a specific example for our demonstration. We choose $N=10$ and generate all possible data of $2^N$ bit strings. We also take a specific subset $S=\{1, 3, 4, 6, 7, 9\}$. ```python theme={null} !pip install -qq -U "classiq[qml]" ``` ```python theme={null} import random import numpy as np np.random.seed(0) random.seed(1) STRING_LEN = 10 majority_data = [ [int(d) for d in np.binary_repr(k, STRING_LEN)] for k in range(2**STRING_LEN) ] random.shuffle(majority_data) # shuffling the data ``` ```python theme={null} SUBSET_INDICES = [1, 3, 4, 6, 7, 9] subset_indicator = np.zeros(STRING_LEN) subset_indicator[SUBSET_INDICES] = 1 ``` ```python theme={null} majority = (majority_data @ subset_indicator > len(SUBSET_INDICES) // 2) * 1 labels = [[l] for l in majority] ``` We choose data for training and data for verification, and define the batch size for the corresponding data loaders: ```python theme={null} TRAINING_SIZE = 340 TEST_SIZE = 512 training_data = majority_data[0:TRAINING_SIZE] training_labels = labels[0:TRAINING_SIZE] test_data = majority_data[TRAINING_SIZE : TRAINING_SIZE + TEST_SIZE] test_labels = labels[TRAINING_SIZE : TRAINING_SIZE + TEST_SIZE] ``` ```python theme={null} BATCH_SIZE = 64 ``` ```python theme={null} import numpy as np import torch from torch.utils.data import DataLoader, TensorDataset training_dataset = TensorDataset( torch.Tensor(training_data), torch.Tensor(training_labels) ) # create dataset training_dataloader = DataLoader( training_dataset, batch_size=BATCH_SIZE, shuffle=True, drop_last=False ) # create dataloader test_dataset = TensorDataset( torch.Tensor(test_data), torch.Tensor(test_labels) ) # create your dataset test_dataloader = DataLoader( test_dataset, batch_size=BATCH_SIZE, shuffle=True, drop_last=False ) # create your dataloader ``` # ## Constructing a Hybrid Network We build the following hybrid neural network: **Data flattening $\rightarrow$ A classical linear layer of size 10 to 4 with `Tanh` activation $\rightarrow$ A qlayer of size 4 to 2 $\rightarrow$ a classical linear layer of size 2 to 1 with `ReLU` activation.** The classical layers can be defined with PyTorch built-in functions. The quantum layer is constructed with (1) a [dense angle-encoding](https://github.com/Classiq/classiq-library/blob/main/functions/qmod_library_reference/classiq_open_library/variational_data_encoding/variational_data_encoding.ipynb) function (2) a simple ansatz with RY and RZZ rotations (3) a postprocess that is based on a measurement per qubit # ### The Quantum Layer ```python theme={null} from classiq import * from classiq.applications.qnn.types import SavedResult from classiq.execution import ExecutionPreferences, execute_qnn @qfunc def my_ansatz(weights: CArray[CReal], qbv: QArray) -> None: """ Gets a quantum variable of $m$ qubits, and applies RY gate on each qubit and RZZ gate on each pair of qubits in a linear connectivity. The classical array weights represents the $2m-1$ parametric rotations. """ repeat( count=qbv.len, iteration=lambda index: RY(weights[index], qbv[index]), ) repeat( count=qbv.len - 1, iteration=lambda index: RZZ(weights[qbv.len + index], qbv[index : index + 2]), ) ``` ```python theme={null} QLAYER_SIZE = 4 num_qubits = int(np.ceil(QLAYER_SIZE / 2)) num_weights = 2 * num_qubits - 1 NUM_SHOTS = 4096 @qfunc def main( input_vec: CArray[CReal, QLAYER_SIZE], weight: CArray[CReal, num_weights], result: Output[QArray], ) -> None: """ The quantum part of the quantum layer. The prefix for the data loading parameters must be set to `input_` or `i_`. The prefix for the ansatz parameters must be set to `weights_` or `weight` """ encode_on_bloch(input_vec, result) my_ansatz(weights=weight, qbv=result) qmod = create_model( main, execution_preferences=ExecutionPreferences(num_shots=NUM_SHOTS) ) qprog = synthesize(qmod) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/32pQqOSulTv1Qvssd71mPgf4btU ``` ```python theme={null} def my_post_process(result: SavedResult, num_qubits, num_shots) -> torch.Tensor: """ Classical postprocess function. Gets the histogram after execution and returns a vector $\vec{y}$, where $y_i$ is the probability of measuring 1 on the $i$-th qubit. """ res = result.value yvec = [ (res.counts_of_qubits(k)["1"] if "1" in res.counts_of_qubits(k) else 0) / num_shots for k in range(num_qubits) ] return torch.tensor(yvec) ``` # ### The Full Hybrid Network Now, we can define the full network. ```python theme={null} import torch.nn as nn from classiq.applications.qnn import QLayer def create_net(*args, **kwargs) -> nn.Module: class Net(nn.Module): def __init__(self, *args, **kwargs): super().__init__() self.flatten = nn.Flatten() self.linear_1 = nn.Linear(STRING_LEN, 4) self.activation_1 = nn.Tanh() self.linear_2 = nn.Linear(2, 1) self.activation_2 = nn.ReLU() self.qlayer = QLayer( qprog, execute_qnn, post_process=lambda res: my_post_process( res, num_qubits=num_qubits, num_shots=NUM_SHOTS ), *args, **kwargs, ) def forward(self, x): x = self.flatten(x) x = self.linear_1(x) x = self.activation_1(x) x = self.qlayer(x) # 4 to 2 x = self.linear_2(x) # 2 to 1 x = self.activation_2(x) return x return Net(*args, **kwargs) my_network = create_net() ``` # ## Training and Verifying the Networks We define some hyperparameters such as loss function and optimization method, and a training function: ```python theme={null} import torch.nn as nn import torch.optim as optim LEARNING_RATE = 0.05 # choosing our loss function loss_func = nn.MSELoss() # choosing our optimizer optimizer = optim.SGD(my_network.parameters(), lr=LEARNING_RATE) ``` Next, we define a `train` function: ```python theme={null} import time as time from torch.utils.data import DataLoader def train( network: nn.Module, data_loader: DataLoader, loss_func: nn.modules.loss._Loss, optimizer: optim.Optimizer, epoch: int = 20, ) -> None: for index in range(epoch): start = time.time() for data, label in data_loader: optimizer.zero_grad() output = network(data) loss = loss_func(output, label.type(output.dtype)) loss.backward() optimizer.step() print(index, f"\tloss = {loss.item()}", "time", time.time() - start) ``` We also define a validation function, `check_accuracy`, which tests a trained network on new data: ```python theme={null} from torch import Tensor def get_correctly_guessed_labels_function( model: nn.Module, data: Tensor, labels: Tensor ) -> int: predictions = model(data) list_of_predictions = [ round(prediction.type(torch.float).item()) for prediction in predictions ] correct = sum( [ list_of_predictions[k] == labels.flatten().tolist()[k] for k in range(len(predictions)) ] ) return correct def _get_amount_of_labels(labels: Tensor) -> int: # the first dimension of `labels` is `batch_size` return labels.size(0) def check_accuracy( network: nn.Module, data_loader: DataLoader, should_print: bool = True, ) -> float: num_correct = 0 total = 0 network.eval() with torch.no_grad(): for data, labels in data_loader: num_correct += get_correctly_guessed_labels_function(network, data, labels) total += _get_amount_of_labels(labels) accuracy = float(num_correct) / float(total) if should_print: print(f"Test accuracy of the model: {accuracy*100:.2f}%") print(f"num correct: {num_correct}, total: {total}") return accuracy ``` # ### Training and Verifying the Network For convenience, we load a pre-trained model and set the epoch size to 1. Training a network takes around 30 epochs. ```python theme={null} # comment out for training my_network.load_state_dict(torch.load("trained_model.pth")) num_epoch = 1 # uncomment out for training # epoch=30 ``` ```python theme={null} data_loader = training_dataloader train(my_network, training_dataloader, loss_func, optimizer, epoch=num_epoch) ``` **Output:** ``` 0 loss = 0.09676678478717804 time 169.12708473205566 ``` ```python theme={null} accuracy = check_accuracy(my_network, test_dataloader) ``` **Output:** ``` Test accuracy of the model: 97.07% num correct: 497, total: 512 ``` # Quantum Generative Adversarial Networks (QGANs) Source: https://docs.classiq.io/explore/algorithms/QML/qgan/qgan_bars_and_strips Open this notebook in GitHub to run it yourself *** Generative AI, especially through Generative Adversarial Networks (GANs), revolutionizes content creation across domains by producing highly realistic output. Quantum GANs further elevate this potential by leveraging quantum computing, promising unprecedented advancements in complex data simulation and analysis. *** In this notebook, we explore the concept of Quantum Generative Adversarial Networks (QGANs) and implement a simple QGAN model using the Classiq SDK. We study a simple use case of a Bars and Stripes dataset. We begin with a classical implementation of a GAN, and then move to a hybrid quantum-classical GAN model. ## 1 Data Preparation We generate the Bars and Stripes dataset, a simple binary dataset consisting of 2x2 images with either a horizontal or vertical stripe pattern: ```python theme={null} !pip install -qq -U "classiq[qml]" ``` ```python theme={null} import time import numpy as np # Function to create Bars and Stripes dataset def create_bars_and_stripes_dataset(num_samples): samples = [] for _ in range(num_samples): horizontal = np.random.randint(0, 2) == 0 if horizontal: stripe = np.random.randint(0, 2, size=(2, 1)) sample = np.tile(stripe, (1, 2)) else: stripe = np.random.randint(0, 2, size=(1, 2)) sample = np.tile(stripe, (2, 1)) samples.append(sample) return np.array(samples, dtype=np.uint8) ``` ```python theme={null} # Generate Bars and Stripes dataset dataset = create_bars_and_stripes_dataset(num_samples=1000) ``` # ## 1.1 Visualizing the Generated Data Let's plot a few samples from the dataset to visualize the bars and stripes patterns: ```python theme={null} import matplotlib.pyplot as plt from matplotlib.colors import LinearSegmentedColormap # Plot images in a 3 by 3 grid def plot_nine_images(generated_images): # Define custom colormap classiq_cmap = LinearSegmentedColormap.from_list( "teal_white", ["#00FF00", "black"] # Green to black ) fig, axes = plt.subplots(3, 3, figsize=(6, 6)) for i, ax in enumerate(axes.flat): ax.imshow(generated_images[i].reshape(2, 2), cmap=classiq_cmap, vmin=0, vmax=1) ax.axis("off") ax.set_title(f"Image {i+1}") for j in range(2): for k in range(2): label = int(generated_images[i].reshape(2, 2)[j, k]) ax.text( k, j, f"{label}", ha="center", va="center", color="white" if label == 1 else "black", fontsize=16, ) plt.tight_layout() plt.show() ``` ```python theme={null} # Generate images generated_images = create_bars_and_stripes_dataset(9) plot_nine_images(generated_images) ``` output We create a PyTorch DataLoader to feed the dataset to the GAN model during training: ```python theme={null} import torch from torch.utils.data import DataLoader, TensorDataset # Create DataLoader for training tensor_dataset = TensorDataset(torch.tensor(dataset, dtype=torch.float)) dataloader = DataLoader(tensor_dataset, batch_size=64, shuffle=True) ``` ## 2 Classical Network # ## 2.1 Defining a Classical GAN We begin by defining the generator and discriminator models (architecture) for the classical GAN. We work with `tensorboard` to save our logs (uncomment the following line to install the package): ```python theme={null} # ! pip install tensorboard ``` ```python theme={null} import torch.nn as nn class Generator(nn.Module): def __init__(self, input_size=2, output_size=4, hidden_size=32): super(Generator, self).__init__() self.model = nn.Sequential( nn.Linear(input_size, hidden_size // 2), # Adjusted hidden layer size nn.ReLU(), nn.Linear(hidden_size // 2, hidden_size), # Adjusted hidden layer size nn.ReLU(), nn.Linear(hidden_size, output_size), nn.Sigmoid(), # Sigmoid activation to output probabilities ) def forward(self, x): return torch.round(self.model(x)) class Discriminator(nn.Module): def __init__(self, input_size=4, hidden_size=16): super(Discriminator, self).__init__() self.model = nn.Sequential( nn.Linear(input_size, hidden_size // 2), # Adjusted hidden layer size nn.LeakyReLU(0.2), nn.Linear(hidden_size // 2, hidden_size), # Adjusted hidden layer size nn.LeakyReLU(0.25), nn.Dropout(0.3), nn.Linear(hidden_size, 1), nn.Sigmoid(), # Sigmoid activation to output probabilities ) def forward(self, x): x = x.view(-1, 4) # Flatten input for fully connected layers return self.model(x) ``` # ## 2.2 Training a Classical GAN We define the training loop for the classical GAN: ```python theme={null} import os from datetime import datetime import torch import torch.nn as nn import torchvision.utils as vutils from torch.utils.tensorboard import SummaryWriter def train_gan( generator, discriminator, dataloader, log_dir_name, fixed_noise, random_fake_data_generator, num_epochs=100, device="cpu", ): # Initialize TensorBoard writer run_id = datetime.now().strftime("%Y%m%d_%H%M%S") log_dir = os.path.join(log_dir_name, run_id) writer = SummaryWriter(log_dir=log_dir) # Define loss function and optimizer criterion = nn.BCELoss() g_optimizer = torch.optim.Adam(generator.parameters(), lr=0.0002) d_optimizer = torch.optim.Adam(discriminator.parameters(), lr=0.0002) generator.to(device) discriminator.to(device) for epoch in range(num_epochs): for i, batch in enumerate(dataloader): real_data = batch[0].to(device) batch_size = real_data.size(0) # Train Discriminator with real data d_optimizer.zero_grad() real_output = discriminator(real_data) d_real_loss = criterion(real_output, torch.ones_like(real_output)) d_real_loss.backward() # Train Discriminator with fake data z = random_fake_data_generator(batch_size) fake_data = generator(z) fake_output = discriminator(fake_data.detach()) d_fake_loss = criterion(fake_output, torch.zeros_like(fake_output)) d_fake_loss.backward() d_optimizer.step() # Train Generator g_optimizer.zero_grad() z = random_fake_data_generator(batch_size) fake_data = generator(z) fake_output = discriminator(fake_data) g_loss = criterion(fake_output, torch.ones_like(fake_output)) g_loss.backward() g_optimizer.step() # Log losses to TensorBoard step = epoch * len(dataloader) + i writer.add_scalar("Generator Loss", g_loss.item(), step) writer.add_scalar("Discriminator Real Loss", d_real_loss.item(), step) writer.add_scalar("Discriminator Fake Loss", d_fake_loss.item(), step) if i % 100 == 0: print( f"Epoch [{epoch+1}/{num_epochs}], Step [{i+1}/{len(dataloader)}], " f"Generator Loss: {g_loss.item():.4f}, " f"Discriminator Real Loss: {d_real_loss.item():.4f}, " f"Discriminator Fake Loss: {d_fake_loss.item():.4f}" ) # Generate and log sample images for visualization # if (epoch+1) % (num_epochs // 10) == 0: # with torch.no_grad(): # generated_images = generator(fixed_noise).detach().cpu() # img_grid = vutils.make_grid(generated_images, nrow=3, normalize=True) # writer.add_image('Generated Images', img_grid, epoch+1) # Close TensorBoard writer writer.close() ``` We train our model and save the trained generator in `'generator_model.pth'`: ```python theme={null} # Fixed noise for visualizing generated samples fixed_noise = torch.randn(9, 2) def random_fake_data_for_gan(batch_size, input_size): return torch.randn(batch_size, input_size) ``` ```python theme={null} generator = Generator(input_size=2, output_size=4, hidden_size=32) discriminator = Discriminator(input_size=4, hidden_size=16) # For simplicitly we load a pretrained model checkpoint = torch.load("resources/generator_trained_model.pth") generator.load_state_dict(checkpoint) train_gan( generator=generator, discriminator=discriminator, dataloader=dataloader, log_dir_name="logs", fixed_noise=fixed_noise, random_fake_data_generator=lambda b_size: random_fake_data_for_gan(b_size, 2), num_epochs=10, device="cpu", ) # Save trained generator model torch.save(generator.state_dict(), "resources/generator_model.pth") ``` **Output:** ``` Epoch [1/10], Step [1/16], Generator Loss: 0.8823, Discriminator Real Loss: 0.8992, Discriminator Fake Loss: 0.5271 Epoch [2/10], Step [1/16], Generator Loss: 0.8759, Discriminator Real Loss: 0.8870, Discriminator Fake Loss: 0.5399 Epoch [3/10], Step [1/16], Generator Loss: 0.8639, Discriminator Real Loss: 0.8805, Discriminator Fake Loss: 0.5475 Epoch [4/10], Step [1/16], Generator Loss: 0.8575, Discriminator Real Loss: 0.8665, Discriminator Fake Loss: 0.5581 Epoch [5/10], Step [1/16], Generator Loss: 0.8437, Discriminator Real Loss: 0.8473, Discriminator Fake Loss: 0.5668 Epoch [6/10], Step [1/16], Generator Loss: 0.8406, Discriminator Real Loss: 0.8405, Discriminator Fake Loss: 0.5634 Epoch [7/10], Step [1/16], Generator Loss: 0.8327, Discriminator Real Loss: 0.8389, Discriminator Fake Loss: 0.5701 Epoch [8/10], Step [1/16], Generator Loss: 0.8203, Discriminator Real Loss: 0.8191, Discriminator Fake Loss: 0.5787 Epoch [9/10], Step [1/16], Generator Loss: 0.8226, Discriminator Real Loss: 0.8198, Discriminator Fake Loss: 0.5721 Epoch [10/10], Step [1/16], Generator Loss: 0.8109, Discriminator Real Loss: 0.8159, Discriminator Fake Loss: 0.5799 ``` # ## 2.3 Evaluating the Performance ```python theme={null} # Load state dictionary with mismatched sizes generator = Generator() checkpoint = torch.load("resources/generator_model.pth") generator.load_state_dict(checkpoint) num_samples = 100 z = random_fake_data_for_gan(num_samples, 2) gen_data = generator(z) def evaluate_generator(samples): count_err = 0 for img in samples: img = img.reshape(2, 2) diag1 = int(img[0, 0]) * int(img[1, 1]) diag2 = int(img[0, 1]) * (int(img[1, 0])) if (diag1 == 1 or diag2 == 1) and diag1 * diag2 != 1: count_err += 1 return (samples.shape[0] - count_err) / samples.shape[0] accuracy_classical = evaluate_generator(samples=gen_data) print(f"Classically trained generator accuracy: {accuracy_classical:.2%}%") ``` **Output:** ``` Classically trained generator accuracy: 70.00%% ``` Visualizing generator examples: ```python theme={null} # Initialize generator for evaluation generator_for_evaluation = Generator(input_size=2, output_size=4) generator_for_evaluation.load_state_dict( torch.load("resources/generator_model.pth") ) # Load trained model generator_for_evaluation.eval() # Generate images with torch.no_grad(): noise = torch.randn(9, 2) generated_images = generator_for_evaluation(noise).detach().cpu().numpy() # Plot images in a 3 by 3 grid generated_images = create_bars_and_stripes_dataset(9) plot_nine_images(generated_images) ``` output ## 3 Quantum Hybrid Network Implementation In this section we define a quantum generator circuit and integrate it into a hybrid quantum-classical GAN model. We then train the QGAN model and evaluate its performance. # ## 3.1 Defining the Quantum GAN # ### 3.1.1 Defining the Quantum Generator We define the three components of the quantum layer. This is where the quantum network architect's creativity comes into play! The design we choose: 1. Data encoding - we take a `datum_angle_encoding` that encodes $n$ data points on $n$ qubits. 2. A variational ansatz - we combine RY and RZZ gates. 3. Classical postprocess - we take the vector $(p_1, p_2, \dots, p_n)$, with $p_i$ being the probability to measure 1 on the $i$-th qubit. ```python theme={null} from typing import List from classiq import * from classiq.applications.qnn.types import SavedResult from classiq.qmod.symbolic import floor, pi @qfunc def datum_angle_encoding(data_in: CArray[CReal], qbv: QArray) -> None: repeat( count=data_in.len, iteration=lambda index: RX(pi * data_in[index], qbv[index]), ) repeat( count=data_in.len, iteration=lambda index: RZ(pi * data_in[index], qbv[index]), ) @qfunc def my_ansatz(weights: CArray[CReal], qbv: QArray) -> None: repeat( count=qbv.len, iteration=lambda index: RY(weights[index], qbv[index]), ) repeat( count=qbv.len - 1, iteration=lambda index: RZZ(weights[qbv.len + index], qbv[index : index + 2]), ) if_( condition=qbv.len > 2, then=lambda: RZZ(weights[weights.len - 1], qbv[0:2]), ) def my_post_process(result: SavedResult, num_qubits, num_shots) -> torch.Tensor: res = result.value yvec = [ (res.counts_of_qubits(k).get("1", 0)) / num_shots for k in range(num_qubits) ] return torch.tensor(yvec) ``` Finally, we define the quantum model with its hyperparameters as our `main` quantum function, and synthesize it into a quantum program. ```python theme={null} import numpy as np from classiq.execution import ( ExecutionPreferences, execute_qnn, set_quantum_program_execution_preferences, ) NUM_SHOTS = 4096 QLAYER_SIZE = 4 num_qubits = int(np.ceil(QLAYER_SIZE)) num_weights = 2 * num_qubits @qfunc def main( input_vec: CArray[CReal, QLAYER_SIZE], weight: CArray[CReal, num_weights], result: Output[QArray[num_qubits]], ) -> None: allocate(result) datum_angle_encoding(data_in=input_vec, qbv=result) my_ansatz(weights=weight, qbv=result) qmod = create_model(main) qmod = update_execution_preferences(qmod, num_shots=NUM_SHOTS) qprog = synthesize(qmod) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/32pR64kcelBdvZPQxyWfy06v25R ``` **The resulting circuit**: Screenshot 2025-07-02 at 13.53.17.png Screenshot 2025-07-02 at 13.54.53.png Screenshot 2025-07-02 at 13.55.05.png # ### 3.1.2 Defining the Hybrid Network We define the network building blocks: the generator and discriminator in a hybrid network configuration with a quantum layer, ```python theme={null} import torch.nn as nn from classiq.applications.qnn import QLayer def create_net(*args, **kwargs) -> nn.Module: class QGenerator(nn.Module): def __init__(self, *args, **kwargs): super().__init__() self.flatten = nn.Flatten() self.linear_1 = nn.Linear(4, 16) self.linear_2 = nn.Linear(16, 32) self.linear_3 = nn.Linear(32, 16) self.linear_4 = nn.Linear(16, 4) self.linear_5 = nn.Linear(2, 4) self.activation_1 = nn.ReLU() self.activation_2 = nn.Sigmoid() self.qlayer = QLayer( qprog, execute_qnn, post_process=lambda res: my_post_process( res, num_qubits=num_qubits, num_shots=NUM_SHOTS ), *args, **kwargs, ) def forward(self, x): x = self.flatten(x) x = self.linear_1(x) x = self.activation_2(x) x = self.linear_2(x) x = self.activation_1(x) x = self.linear_3(x) x = self.activation_1(x) x = self.linear_4(x) x = self.activation_2(x) x = self.qlayer(x) x = torch.round(self.activation_2(x)) return x return QGenerator(*args, **kwargs) class Discriminator(nn.Module): def __init__(self, input_size=4, hidden_size=16): super(Discriminator, self).__init__() self.model = nn.Sequential( nn.Linear(input_size, hidden_size // 2), # Adjusted hidden layer size nn.LeakyReLU(0.2), nn.Linear(hidden_size // 2, hidden_size), # Adjusted hidden layer size nn.LeakyReLU(0.25), nn.Dropout(0.3), nn.Linear(hidden_size, 1), nn.Sigmoid(), # Sigmoid activation to output probabilities ) def forward(self, x): x = x.view(-1, 4) # Flatten input for fully connected layers return self.model(x) ``` ```python theme={null} q_gen = create_net() ``` # ## 3.3 Training the QGAN We can use the training loops defined above for the classical GAN: ```python theme={null} # Fixed noise for visualizing generated samples fixed_noise = torch.randn(4) def random_fake_data_for_qgan(batch_size, input_size): return torch.bernoulli(torch.rand(batch_size, input_size)) ``` The following cell generates an archive of the training process in the `q_logs` directory. We also use Tensorboard to monitor the training in real time. It is possible to use an online version - which is more convenient - but for the purpose of this notebook we use the local version. An example of a vizualization output that can be obtained from `tensorboard` is shown in the next figure. ```python theme={null} # ## generate tensorboard log directory # # # # log_dir = 'MY_LOG_DIR' # # if not os.path.exists(log_dir): # # os.makedirs(log_dir) # # Launch tensorboard and generate the containing folder internally # %load_ext tensorboard # # %reload_ext tensorboard # %tensorboard --logdir='MY_LOG_DIR/q_logs' ``` *** Since training can take long time to run, we take a pre-trained model, whose parameters are stored in `q_generator_trained_model.pth`. In addition, we take a smaller sample size of 250. (The pre-trained model was trained on 1000 samples.) To train a randomly initialized QGAN, change `num_samples` from 250 to 1000 for the data creation, and `num_epochs` from 1 to 10 in the training call `train_gan`. *** ```python theme={null} # Create training dataset for qgan qgan_training_dataset = create_bars_and_stripes_dataset( num_samples=250 # num_samples=1000 ) # Convert to PyTorch tensor qgan_tensor_dataset = torch.tensor(qgan_training_dataset, dtype=torch.float32) # Create a TensorDataset object qgan_tensor_dataset = TensorDataset(qgan_tensor_dataset) # Create a DataLoader object qgan_dataloader = DataLoader(qgan_tensor_dataset, batch_size=64, shuffle=True) q_generator = q_gen discriminator = Discriminator(input_size=4, hidden_size=16) ``` ```python theme={null} checkpoint = torch.load("resources/q_generator_trained_model.pth") q_generator.load_state_dict(checkpoint) train_gan( generator=q_generator, discriminator=discriminator, dataloader=qgan_dataloader, log_dir_name="q_logs", fixed_noise=fixed_noise, random_fake_data_generator=lambda b_size: random_fake_data_for_qgan(b_size, 4), num_epochs=1, # num_epochs=10, device="cpu", ) # Save trained generator model torch.save(q_generator.state_dict(), "resources/q_generator_model_bs64.pth") ``` **Output:** ``` Epoch [1/1], Step [1/4], Generator Loss: 0.7105, Discriminator Real Loss: 0.7182, Discriminator Fake Loss: 0.6834 ``` # ## 3.3 Evaluating the Performance Finally, we can evaluate the performance of the QGAN, similar to the classical counterpart: ```python theme={null} generator = q_gen checkpoint = torch.load("resources/q_generator_model_bs64.pth") generator.load_state_dict(checkpoint) num_samples = 10 z = torch.bernoulli(torch.rand(num_samples, 4)) gen_data = generator(z) accuracy = evaluate_generator(samples=gen_data) print(f"Quantum-classical hybrid trained generator accuracy: {accuracy:.2%}%") ``` **Output:** ``` Quantum-classical hybrid trained generator accuracy: 100.00%% ``` ```python theme={null} z = random_fake_data_for_qgan(10, 4) gen_data = generator(z) print(gen_data) ``` **Output:** ``` tensor([[1., 1., 1., 1.], [1., 1., 1., 1.], [1., 1., 1., 1.], [1., 1., 1., 1.], [1., 1., 1., 1.], [1., 1., 1., 1.], [1., 1., 1., 1.], [1., 1., 1., 1.], [1., 1., 1., 1.], [1., 1., 1., 1.]], grad_fn=) ``` *** Why do you think the accuracy is so high? Answer: The system chose a metastable pathway where no violation of the rules occurs! Try longer training, different sets of hyperparameters, different architectures, etc. # Quantum Support Vector Machines (QSVM) Source: https://docs.classiq.io/explore/algorithms/QML/qsvm/qsvm Open this notebook in GitHub to run it yourself > **Quantum Support Vector Machines** is the quantum version of classical Support Vector Machines (SVM); i.e., a data classification method that separates the data by performing a mapping to a high-dimensional space, in which the data is separated by a hyperplane \[[1](#learning)]. QSVM is a hybrid quantum-classical classification algorithm in which classical data are embedded into a high-dimensional quantum Hilbert space using a parameterized quantum feature map. A quantum processor is then used to evaluate inner products between these quantum states, producing a kernel matrix that captures similarities between data points in this quantum feature space. This kernel is passed to a classical support vector machine optimizer, which learns the optimal separating hyperplane by identifying support vectors and model parameters. For prediction, the trained model classifies new data points using quantum-evaluated kernel values and a classical decision rule. For some problem instances, a quantum feature map may enable improved classification performance while using fewer computational resources than a classical algorithm \[[2](#speedup)]. > > The algorithm treats the following problem: > > * **Input:** Classical data points ${\mathbf{x}_i}$, where $\mathbf{x}_i\in \mathbb{R}^d$ are d-dimensional vectors, corresponding labels $y_i \in\{-1, 1\}$, where $i=1,\dots,m$, as well as feature map $U_\phi(\mathbf{x}_i)$, encoding the classical data in a quantum state. > * **Output:** A kernel matrix evaluated using quantum measurements. The matrix is then fed into a classical SVM optimizer, producing a full characterization of the separating hyperplane. > > *** > > **Keywords:** Quantum Machine Learning (QML), hybrid quantum-classical algorithm, supervised learning, binary classification. ## Background Our goal is to find a hyperplane in $\mathbb{R}^d$ which separates the points $\{\mathbf{x}_i\}$ into ones for which the corresponding labels are $y_i = +1$ and $y_i = -1$. The hyperplane is conveniently defined by a vector normal to it, $\mathbf{w} \in \mathbb{R}^d$ and an offset $b \in \mathbb{R}$. The classification of a point $\mathbf{x}$ can be determined by $h_{\mathbf{w}}(\mathbf{x}) = \text{sign}(\langle \mathbf{w},\mathbf{x} \rangle + b)$, which decides on which side of the hyperplane the point lies on. Here $\langle \mathbf{w},\mathbf{x} \rangle$ is the inner product between the two vectors. To describe the goal explicitly, we introduce the **geometric margin** as the distance from the hyperplane to the closest training point $\mathbf{x}_i$: $\min_{\mathbf{x}}((\langle \mathbf{w},\mathbf{x} \rangle/||\mathbf{w}||)$. The optimal classification corresponds to the hyperplane and offset that maximizes the geometric margin. This goal can be stated as a naive optimization problem: find $\mathbf{w}$ satisfying $$ \text{max}_{\mathbf{w}}\text{min}_{\mathbf{i}}(\text{sign} (\langle \mathbf{w},\mathbf{x}_i \rangle + b )) $$ $$ \text{subject to: }~~\text{sign}(\langle \mathbf{w},\mathbf{x}_i \rangle + b) = \text{sign}(y_i)~. $$ However, this formulation of the problem is a nonlinear optimization problem due to the sign comparison. Alternatively, we can express the objective as a linear function with linear constraints. A separating hyperplane satisfies $(\langle \mathbf{w},\mathbf{x}_i \rangle + b)y_i \geq 0 ~~, $ moreover, we set the length of $\mathbf{w}$ by enforcing that the inner product with respect to the nearest point (in fact, there will always be two data points on each side of the hyperplane with the same minimal distance to the hyperplane) to be $\langle \mathbf{w},\mathbf{x}_{\min} \rangle = 1$. As a result, all data points satisfy $(\langle \mathbf{w},\mathbf{x}_i \rangle + b)y_i \geq 1$. The condition enables defining the optimization problem $$ {\text{minimize}~ \frac{1}{2}} || \mathbf{w}||^2 $$ $$ \text{subject to }~~ (\langle \mathbf{w},\mathbf{x}_i \rangle + b)y_i \geq 1~~~~\text{for}~~~~i=1,\dots,m~. $$ In general, it would not be possible to separate the bare data points by a hyperplane; therefore, a transformation of the data to a higher-dimensional space using a feature map $\phi(\mathbf{x})$ is performed. Following the transformation, the hyperplane and offset are evaluated (similar problem, obtained by transforming $\mathbf{x}_i \rightarrow \phi(\mathbf{x}_i)$). The main disadvantage of the present (primal) formulation of the problem is that explicitly computing $\phi(\mathbf{x})$ may require infeasible computational resources, or even involve a mapping to an infinite-dimensional space. An alternative approach utilizes the dual formulation of the problem. This approach relies on the **Karush-Kuhn-Tucker theorem** \[[3](#kkt)], which implies that one can formulate a dual optimization problem, whose solution (under certain conditions which are satisfied for the present case) coincides with the solution of the present (primal) problem. The dual problem of the original primal optimization problem is given by $$ \text{maximize}~~ {\cal{L}}_D(\alpha_1,\dots,\alpha_m) = \sum_{i=1}^m \alpha_i - \frac{1}{2} \sum_{i,j=1}^m y_i y_j \alpha_i \alpha_j K(\mathbf{x}_i, \mathbf{x}_j) \tag{2} $$ $$ \text{subject to:}~~~~\alpha_i \geq 0~~~~\text{for}~~~~i=1,\dots,m~~ $$ $$ \sum_{i=1}^m y_i \alpha_i = 0~~, $$ where $K(\mathbf{x}_i, \mathbf{x}_j) = \langle \mathbf{x}_i,\mathbf{x}_j \rangle$ is the $(i,j)$ component of the matrix, called the kernel matrix. The important advantage of the dual formulation is that for specific feature maps, evaluation of the kernel matrix components does not require explicit assessment of the inner product of two feature vectors (which might be infinite-dimensional after the feature map transformation). The quantum version of SVM is based on the dual optimization problem, where the main innovation is that a quantum computer can perform unitary feature transformations by applying quantum circuits and evaluate the inner product between transformed states by specified measurements. ## QSVM Algorithms The QSVM training algorithm includes three steps. 1. Data loading of the classical data and feature map transformation. 2. Evaluation of the overlap between two feature states. 3. Classical optimization procedure, optimizing the circuit control parameters and modification of the feature map transformation. **Train:** **Step 1:** The mapping of a classical data point $\mathbf{x}$ into a quantum feature state involves loading or encoding the data into a quantum state $$ |{0}^n\rangle \xrightarrow{U_{DE}(\mathbf{x})} |\mathbf{x}\rangle ~~. $$ Various popular transformations exist, for example: basis, amplitude, angle, and dense encoding. The possible approaches showcase a general tradeoff between the number of qubits required to encode the classical data and the circuit depth. Generally, highly entangled states allow encoding more classical data utilizing fewer qubit, while requiring deeper circuits. Following, a unitary feature operation maps the encoded state to a quantum feature state $$ |{\mathbf{x}}\rangle \xrightarrow{U_{\phi}(\mathbf{x})} |\phi(\mathbf{x})\rangle ~~. $$ The two transformations can be combined to a single unitary transformation $U(\mathbf{x}) = U_{\phi}(\mathbf{x})U_{DE}(\mathbf{x})$, dependent on the classical data point $\mathbf{x}$. **Step 2:** The overlap between two feature vectors $\phi(\mathbf{x}_i)$ and $\phi(\mathbf{x}_j)$ is performed by applying the circuit $$ U_{\text{QSVM}}(\mathbf{x_i},\mathbf{x_j}) = U^{\dagger}(\mathbf{x}_i)U(\mathbf{x}_j) $$ to the initial state $|0^n\rangle$ and measuring the probability of measuring $0^n$. The expected probability, $P_{|0^n\rangle}=| \langle \phi (\mathbf{x}_i)| \phi(\mathbf{x}_j) \rangle|^2$ provides the elements of the kernel matrix $$ K(\mathbf{x}_i,\mathbf{x}_j) = \langle \phi (\mathbf{x}_i)| \phi(\mathbf{x}_j) \rangle~~. $$ **Step 3:** Optimize the dual problem using a classical optimization algorithm. The SVM optimization problem, Eq. (2), is a quadratic programming problem (a specific case of a convex optimization problem), for which several exact or approximate solution methods exist, such as active-set, interior point, and gradient/projection-based methods. The algorithm is given the kernel matrix and constraints as input, and produces the optimized $\{\alpha_i\}$ coefficients. **Prediction:** For a new data point $\mathbf{s}$, the kernel matrix of the new datum is evaluated with respect to the optimized $\{\alpha_i\}$ $$ \text{Predicted Labels}(\mathbf{s}) = \text{sign}(\sum_{i=1}^m y_i \alpha_i K(\mathbf{x}_i,\mathbf{s})+b)~~. \tag{1} $$ In practice, only a number of data points contribute to optimization (the support vectors). Only for these data points the corresponding coefficients, $\alpha_i$, do not vanish. As a consequence, most of the terms in the sum of Eq. (1) vanish, and we can limit the calculation of the kernel matrix elements for $i$'s for which $\alpha_i\neq 0$. ## QSVM with Classiq We consider two data sets, each paired with an appropriate quantum feature map: * A **simple** data set, constructed by randomly distributing points around two source data points. This data enables straightforward linear classification, and a single-qubit Bloch sphere encoding is sufficient to achieve perfect accuracy. * An **Iris** data set (*versicolor* vs *virginica*, petal features), where the two classes partially overlap and the decision boundary depends on cross-feature correlations. Here the Pauli ZZ feature map - which encodes pairwise feature products via ZZ Hamiltonian terms - outperforms the Bloch sphere encoding, which encodes features independently per qubit. The two examples together illustrate a key principle: **use appropriate encoding that matches your data structure**. The Bloch sphere encoding is efficient and sufficient for linearly separable data; the Pauli ZZ encoding provides the expressiveness needed for correlated, nonlinearly-structured data. ## Example 1: Bloch Sphere Feature Map Applied to Linearly Classifiable Data We start coding with the relevant imports: ```python theme={null} !pip install -qq -U "classiq[qml]" ``` ```python theme={null} import matplotlib.pyplot as plt import numpy as np from classiq import * from classiq.applications.qsvm.qsvm import QSVM, QuantumKernelEvaluator from classiq.applications.qsvm.quantum_feature_maps import pauli_feature_map from classiq.open_library.functions.variational import inplace_encode_on_bloch ``` Next, we generate data. Three data sets are generated: * Training data: labelled data utilized to train and optimize the algorithm parameters * Test data: labelled data employed to evaluate the optimization process * Prediction data: unlabelled data that the optimized algorithm predicts the corresponding classification labels. This example takes a 2D input space and a binary classification (i.e., only two groups of data points): ```python theme={null} import random seed = 0 random.seed(seed) np.random.seed(seed) ``` In the data generation we utilize a number of utility functions: * `generate_data`: takes two `sources` points and outputs a python dictionary with the training data points (random points within the vicinity of the sources). * `data_dict_to_data_and_labels`: given a generated data dictionary, outputs the input data and associated labels. ```python theme={null} # Importing functions used for this demo, to generate random linearly separable data from classiq.applications.qsvm.qsvm_data_generation import ( data_dict_to_data_and_labels, generate_data, ) # Generate sample data: sources = np.array([[1.23016026, 1.72327701], [3.20331931, 5.32365722]]) training_input: dict = generate_data(sources=sources) test_input: dict = generate_data(sources=sources) predict_input, predict_real_labels = data_dict_to_data_and_labels( generate_data(sources=sources) ) ``` # ## Defining the Data In addition to the feature map, we need to prepare our data. The `training_input` and `test_input` datasets consist of data and its labels. The labels are a 1D array where the value of the label corresponds to each data point and can be basically anything, such as (0, 1), (3, 5), or ('A', 'B'). The `predict_input` consists only of data points (without labels). We normalize the data to be in the range $[-1, 1)$. ```python theme={null} # Prepare and define `train_input` and `test_input` datasets consisting of data and labels TRAIN_DATA_1, TRAIN_LABELS_1 = data_dict_to_data_and_labels(training_input) TEST_DATA_1, TEST_LABELS_1 = data_dict_to_data_and_labels(test_input) # Prepare and define `predict_input` PREDICT_DATA_1 = predict_input def normalize(data: np.ndarray, range: tuple) -> np.ndarray: return (2 * data - range[1] - range[0]) / (range[1] - range[0]) RANGE = (0, 6 * np.pi) TRAIN_DATA_1 = normalize(TRAIN_DATA_1, RANGE) TEST_DATA_1 = normalize(TEST_DATA_1, RANGE) PREDICT_DATA_1 = normalize(PREDICT_DATA_1, RANGE) ``` To get a better understanding of the classification task at hand, we plot the data. ```python theme={null} plot_range = (-1, -0.4) colors = {0: "blue", 1: "orange"} for i in range(TRAIN_DATA_1.shape[0]): plt.scatter(*TRAIN_DATA_1[i, :].T, c=colors[TRAIN_LABELS_1[i]]) plt.title("Training Data") plt.xlim(plot_range) plt.ylim((-1, 0)) plt.show() for i in range(TRAIN_DATA_1.shape[0]): plt.scatter(*TEST_DATA_1[i, :].T, c=colors[TEST_LABELS_1[i]]) plt.title("Test Data") plt.xlim(plot_range) plt.ylim((-1, 0)) plt.show() plt.scatter(*PREDICT_DATA_1.T) plt.title("Prediction Data (unlabeled)") plt.xlim((-1, -0.4)) plt.ylim((-1, -0.2)) plt.show() ``` output output output Here the dark blue and orange dots correspond to the labels $0$ and $1$, and the prediction data is unlabelled. # ## Defining the Feature Map When constructing a QSVM model, we must supply the feature map, encoding the classical data into quantum states in Hilbert space (the feature space of the problem). Here, we choose to encode the data onto the surface of the Bloch sphere. This can be defined in terms of the following transformation on the 2D data point $$ \mathbf{x} = [x_0,x_1]^T\rightarrow R_Z(\pi x_1) R_X(\pi x_0)|0\rangle = \cos(\pi x_0/2)|0\rangle + i e^{i \pi x_1}\sin(\pi x_0/2)|1\rangle~~, $$ where the circuit takes a single qubit per data point and the last equality is up to a global phase. We define a quantum function that generalizes the Bloch sphere mapping to an input vector of any dimension (also known as "dense angle encoding" in the field of quantum neural networks): $$ RX(\pi x_{2i})RZ(\pi x_{2i+1})|i\rangle = \cos(\pi x_{2i}/2)|0\rangle + i e^{i \pi x_{2i+1}}\sin(\pi x_{2i}/2)|1\rangle~~, $$ where $x_i$ are the elements of the vector $\mathbf{x}$ and $d$ is the dimension of the vector. Each pair of entries in the vector is mapped to a Bloch sphere. If there is an odd size, we apply a single RX gate on an extra qubit. Since a single qubit stores the data of a single data point, for such a feature mapping the number of qubits required is $n=\lceil d/2 \rceil$. The feature map is uploaded from the Classiq's open library. # ## Constructing a Model We begin by building Classiq's `QSVM` class, consisting of the QML model. The model is given a feature map, and possibly `ExecutionPreferences`. The feature map will be employed in the evaluation of the kernel in the training step. ```python theme={null} # Build a quantum support vector machine model bloch_num_qubits = int(np.ceil(np.log2(TRAIN_DATA_1.shape[1]))) qsvm_model = QSVM(feature_map=inplace_encode_on_bloch, num_qubits=bloch_num_qubits) ``` # ## Viewing the Generated Quantum Circuit Before training, the quantum circuit used for kernel evaluation is accessible via `get_qprog`. ```python theme={null} qprog_bloch = qsvm_model.get_qprog(data_dim=TRAIN_DATA_1.shape[1]) show(qprog_bloch) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3EEHzpInwJWGPXz9Nlequ7tatL1 ``` # ## Executing QSVM The execution involves the following steps: 1. Training 2. Testing the training process, and outputting a test score. 3. Predicting, by taking unlabeled data and returning its predicted labels. This may be applied multiple times on different datasets. These steps are performed utilizing the `train`, `test` and `predict` methods of the `QSVM` class. In the training stage, the quantum kernel is constructed element by element, by repeated execution of quantum circuits. The execution employs Classiq's `sample_batch` to evaluate the overlap between the states encoding the two data points of each pair. The kernel matrix is then fed into scikit-learn's `SVC` (Support Vector Classifier) function to optimize the quadratic program (the dual optimization problem). The knowledge of the optimized coefficients enables the model to predict the classification of a new data point, utilizing Eq. (1). ```python theme={null} # Train the model qsvm_model.train(TRAIN_DATA_1, TRAIN_LABELS_1) ``` ```python theme={null} # Check the test score test_score, test_predicted_labels = qsvm_model.test(TEST_DATA_1, TEST_LABELS_1) # Predict labels predicted_labels = qsvm_model.predict(PREDICT_DATA_1) ``` # ## Results We can view the classification accuracy through `test_score`, moreover, since this data was previously generated, we also know the real labels and can print them for comparison. ```python theme={null} # Printing tests result print(f"Testing success ratio: {test_score}") print() # Printing predictions print("Prediction from datapoints set:") print(f" ground truth: {predict_real_labels}") print(f" prediction: {predicted_labels}") print( f" success rate: {100 * np.count_nonzero(predicted_labels == predict_real_labels) / len(predicted_labels)}%" ) ``` **Output:** ``` Testing success ratio: 1.0 Prediction from datapoints set: ground truth: [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1] prediction: [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1] success rate: 100.0% ``` We can even visualize the predicted results: ```python theme={null} plt.figure() for i in range(PREDICT_DATA_1.shape[0]): plt.scatter(*PREDICT_DATA_1[i, :].T, c=colors[predicted_labels[i]]) plt.title("Prediction Data") plt.xlim(plot_range) plt.ylim((-1, 1 + 0.3)) plt.show() ``` output ## Example 2: Pauli and Bloch Sphere Feature Map on a Complex Data Set We begin by generating the dataset for this example. ```python theme={null} from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split from sklearn.preprocessing import MinMaxScaler, StandardScaler ``` We use the Iris dataset, classifying *versicolor* vs *virginica* using the two petal features (petal length and petal width). This binary task is non-trivial since the two classes partially overlap in the original feature space. The data is scaled to $[0, 2\pi]$ to match the range expected by the Pauli ZZ feature map. ```python theme={null} # Load Iris dataset - classify versicolor vs virginica using petal features iris = load_iris() mask = iris.target != 0 # exclude setosa X = iris.data[mask, 2:] # petal length and width (columns 2 and 3) y = iris.target[mask] - 1 # remap labels to {0, 1} FEATURE_SIZE = X.shape[1] # = 2 # Scale to [0, 2π] to match the range expected by the Pauli ZZ feature map std_scaler = StandardScaler().fit(X) X_std = std_scaler.transform(X) mm_scaler = MinMaxScaler(feature_range=(0, 2 * np.pi)).fit(X_std) X_scaled = mm_scaler.transform(X_std) ``` Split into train, test, and prediction sets. ```python theme={null} X_train, X_temp, y_train, y_temp = train_test_split( X_scaled, y, test_size=0.2, random_state=42, stratify=y ) X_test, X_predict, y_test, y_predict = train_test_split( X_temp, y_temp, test_size=0.5, random_state=42, stratify=y_temp ) TRAIN_DATA_2, TRAIN_LABELS_2 = X_train, y_train TEST_DATA_2, TEST_LABELS_2 = X_test, y_test PREDICT_DATA_2 = X_predict predict_real_labels_2 = y_predict ``` The data can be visualized by a color coded plot ```python theme={null} import matplotlib.pyplot as plt plt.scatter( TRAIN_DATA_2[TRAIN_LABELS_2 == 0, 0], TRAIN_DATA_2[TRAIN_LABELS_2 == 0, 1], marker="s", facecolors="w", edgecolors="b", label="A train", ) plt.scatter( TRAIN_DATA_2[TRAIN_LABELS_2 == 1, 0], TRAIN_DATA_2[TRAIN_LABELS_2 == 1, 1], marker="o", facecolors="w", edgecolors="r", label="B train", ) plt.scatter( TEST_DATA_2[TEST_LABELS_2 == 0, 0], TEST_DATA_2[TEST_LABELS_2 == 0, 1], marker="s", facecolors="b", edgecolors="w", label="A test", ) plt.scatter( TEST_DATA_2[TEST_LABELS_2 == 1, 0], TEST_DATA_2[TEST_LABELS_2 == 1, 1], marker="o", facecolors="r", edgecolors="w", label="B test", ) plt.legend(bbox_to_anchor=(1.05, 1), loc="upper left", borderaxespad=0.0) plt.title("Dataset for classification (first 2 features)") plt.show() ``` output # ## Pauli Feature Map We build a Pauli feature map. This feature map is of size $N$ qubits for data $\mathbf{x}$ of size $N$, and it corresponds to the following unitary: $$ U = \exp\left(\sum f^{(1)}_k(\mathbf{x})H^{(1)}_k + \sum f^{(2)}_k(\mathbf{x})H^{(2)}_k+\dots \right) H^{\otimes n}~~, $$ where $H^{\otimes n}$ designates the Hadamard transform, and $H^{(i)}$ is a Hamiltonian acting on $i$ qubits according to some connectivity map, and $f^{(i)}$ is some classical function, typically taken as the polynomial of degree $i$. For example, if our data is of size $3$ and we assume circular connectivity, taking Hamiltonians depending only on $Z$, the Hamiltonian reads as $$ \sum f^{(1)}_k(\mathbf{x})H^{(1)}_k = \alpha(x_0+\beta)ZII+\alpha(x_1+\beta)IZI+\alpha(x_2+\beta)IIZ, $$ $$ \sum f^{(2)}_k(\mathbf{x})H^{(2)}_k = \gamma^2(x_0+\zeta)(x_1+\zeta)ZZI+\gamma^2(x_1+\zeta)(x_2+\zeta)IZZ + \gamma^2(x_0+\zeta)(x_3+\zeta)ZIZ~~, $$ where $(\alpha,\beta)$ and $(\gamma,\zeta)$ define some affine transformation on the data and correspond to the functions $f^{(1,2)}$. We start by defining classical functions for creating a connectivity map for the Hamiltonians and for generating the full Hamiltonian: # ## Model Construction We first define the hyperparameters of the Pauli feature map and construct an appropriate wrapper feature map, utilizing `pauli_feature_map`. ```python theme={null} # Define the parameters for the Pauli feature map N_DIM = FEATURE_SIZE PAULIS = [[Pauli.Z], [Pauli.Z, Pauli.Z]] CONNECTIVITY = 2 # ConnectivityType.FULL — all qubit pairs interact AFFINES = [[1, 0], [1, np.pi]] REPS = 2 # Build the wrapper function for the Pauli feature map feature_map = lambda data, qba: pauli_feature_map( data, PAULIS, AFFINES, CONNECTIVITY, REPS, qba ) ``` Next, the model is constructed ```python theme={null} pauli_num_qubits = N_DIM # Build a quantum support vector machine model qsvm_model_pauli = QSVM(feature_map=feature_map, num_qubits=pauli_num_qubits) ``` # ## Viewing the Generated Quantum Circuit Before training, the quantum circuit used for kernel evaluation is accessible via `get_qprog`. ```python theme={null} qprog_pauli = qsvm_model_pauli.get_qprog(data_dim=N_DIM) show(qprog_pauli) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3EEI4f9X9d9UPMNnqyXnpDHd30k ``` # ## Train, Test and Prediction of the Pauli Model ```python theme={null} # Train the model qsvm_model_pauli.train(TRAIN_DATA_2, TRAIN_LABELS_2) # Check the test score test_score_pauli, test_predicted_labels_pauli = qsvm_model_pauli.test( TEST_DATA_2, TEST_LABELS_2 ) # Predict labels predicted_labels_pauli = qsvm_model_pauli.predict(PREDICT_DATA_2) ``` # ## Prediction Utilizing the Bloch Feature Map We compare the Pauli feature map to the Bloch feature map results. For that end, we construct a new model, using the `inplace_encode_on_bloch`, as in the first example above. ```python theme={null} bloch_num_qubits_2 = int(np.ceil(np.log2(N_DIM))) # Build a quantum support vector machine model qsvm_model_bloch = QSVM( feature_map=inplace_encode_on_bloch, num_qubits=bloch_num_qubits_2 ) qprog_bloch_2 = qsvm_model_bloch.get_qprog(data_dim=N_DIM) show(qprog_bloch_2) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3EEICE0EPmJFCHR7WHBqYyRht2m ``` ```python theme={null} # Train the model qsvm_model_bloch.train(TRAIN_DATA_2, TRAIN_LABELS_2) # Check the test score test_score_bloch, test_predicted_labels_bloch = qsvm_model_bloch.test( TEST_DATA_2, TEST_LABELS_2 ) # Predict labels predicted_labels_bloch = qsvm_model_bloch.predict(PREDICT_DATA_2) ``` # ## Results The Pauli feature map accurately classifies the test and prediction data sets. ```python theme={null} # Printing tests result print(f"Testing success ratio for the Pauli feature map: {test_score_pauli}") print() # Printing predictions print("Prediction from datapoints set:") print(f"ground truth: {predict_real_labels_2}") print(f"prediction: {predicted_labels_pauli}") print( f"success rate: {100 * np.count_nonzero(predicted_labels_pauli == predict_real_labels_2) / len(predicted_labels_pauli)}%" ) ``` **Output:** ``` Testing success ratio for the Pauli feature map: 1.0 Prediction from datapoints set: ground truth: [0 1 1 0 1 0 0 0 1 1] prediction: [0 1 1 1 0 1 0 0 1 1] success rate: 70.0% ``` In comparison, the Bloch sphere feature map achieves lower accuracy in the classification task. ```python theme={null} # Printing tests result print(f"Testing success ratio for the Bloch sphere feature map: {test_score_bloch}") print() # Printing predictions print("Prediction from datapoints set:") print(f"ground truth: {predict_real_labels_2}") print(f"prediction: {predicted_labels_bloch}") print( f"success rate: {100 * np.count_nonzero(predicted_labels_bloch == predict_real_labels_2) / len(predicted_labels_bloch)}%" ) ``` **Output:** ``` Testing success ratio for the Bloch sphere feature map: 0.8 Prediction from datapoints set: ground truth: [0 1 1 0 1 0 0 0 1 1] prediction: [0 0 1 1 0 0 0 0 0 1] success rate: 60.0% ``` # ## Classical Baseline: RBF Kernel SVM To put the quantum results in context, we compare against a classical SVM with a radial basis function (RBF) kernel - the standard non-linear classical baseline. The RBF kernel measures Euclidean similarity between data points and can be expressed as an infinite Taylor series of polynomial kernels, making it a strong and flexible classifier. ```python theme={null} from sklearn.svm import SVC # Train classical SVM with RBF kernel svc_rbf = SVC(kernel="rbf", random_state=42) svc_rbf.fit(TRAIN_DATA_2, TRAIN_LABELS_2) test_score_rbf = svc_rbf.score(TEST_DATA_2, TEST_LABELS_2) predicted_labels_rbf = svc_rbf.predict(PREDICT_DATA_2) success_rate_rbf = ( 100 * np.count_nonzero(predicted_labels_rbf == predict_real_labels_2) / len(predict_real_labels_2) ) print(f"Testing success ratio for the classical RBF kernel SVM: {test_score_rbf}") print() print("Prediction from datapoints set:") print(f"ground truth: {predict_real_labels_2}") print(f"prediction: {predicted_labels_rbf}") print(f"success rate: {success_rate_rbf}%") ``` **Output:** ``` Testing success ratio for the classical RBF kernel SVM: 0.9 Prediction from datapoints set: ground truth: [0 1 1 0 1 0 0 0 1 1] prediction: [0 1 0 0 1 1 0 0 1 1] success rate: 80.0% ``` ## Summary and Discussion The notebook demonstrated the application of the Quantum Support Vector Machine (QSVM) algorithm through two examples, each paired with a quantum feature map suited to the data structure. In the first example, a linearly separable dataset was classified using the Bloch sphere feature map. The single-qubit Bloch encoding is efficient - it encodes two features into one qubit via local $R_X$ and $R_Z$ rotations with no entanglement - and fully sufficient for this task, achieving perfect classification accuracy. In the second example, the Iris *versicolor* vs *virginica* task was classified using both quantum and classical methods. On this partially-overlapping dataset, the Pauli ZZ feature map achieved \~90% test accuracy, while the Bloch sphere feature map reached \~80%, and the classical RBF kernel SVM matched the Pauli result at \~90%. The accuracy gap between the two quantum maps reflects a fundamental difference in their circuit structure and expressiveness: * **Bloch sphere feature map:** uses $\lceil d/2 \rceil$ qubits and applies only single-qubit $R_X / R_Z$ rotations. Each feature is encoded independently - there is no entanglement and no cross-feature interaction. This makes it shallow and qubit-efficient, but unable to model decision boundaries that depend on products of features. * **Pauli ZZ feature map:** uses one qubit per feature and adds two-qubit ZZ interaction gates, encoding pairwise feature products $x_i \cdot x_j$ as entangled quantum phases. The additional circuit depth and entanglement allow it to represent correlated structures that the Bloch map cannot capture. The Pauli ZZ map's ability to match the classical RBF kernel on the Iris task illustrates that the right quantum feature map can reach competitive performance with established non-linear classical methods. The two examples together reinforce a central principle in QSVM design: **choose the simplest feature map whose expressiveness matches the structure of the data**. ## References \[1] [Havlíček, V., Córcoles, A. D., Temme, K., Harrow, A. W., Kandala, A., Chow, J. M., & Gambetta, J. M. (2019). Supervised learning with quantum-enhanced feature spaces. Nature, 567(7747), 209-212.](https://arxiv.org/abs/1804.11326) \[2] [Liu, Y., Arunachalam, S., & Temme, K. (2021). A rigorous and robust quantum speed-up in supervised machine learning. Nature Physics, 17(9), 1013-1017.](https://arxiv.org/abs/2010.02174) \[3] [Karush-Kuhn-Tucker conditions](https://en.wikipedia.org/wiki/Karush%E2%80%93Kuhn%E2%80%93Tucker_conditions) # Quantum Autoencoder Source: https://docs.classiq.io/explore/algorithms/QML/quantum_autoencoder/quantum_autoencoder Open this notebook in GitHub to run it yourself ## Encoder Types # ## Classical Encoders Encode or compress classical data into smaller-sized data via a deterministic algorithm. For example, JPEG is essentially an algorithm that compresses images into smaller-sized images. # ## Classical Autoencoders Use machine-learning techniques and train a variational network for compressing data. In general, an autoencoder network looks as follows: The network has three main parts: 1. The encoder compresses the data into a smaller, coded layer. 2. The latter is the input to a decoder part. 3. Typically, training is done against the comparison between the input and the output of this network. **Classical autoencoders can also be used for anomaly detection (see below).** # ## Quantum Autoencoders In a similar fashion to the classical counterpart, a quantum autoencoder compresses quantum data stored initially on $n$ qubits into a smaller quantum register of $m ## Predefined Functions That Construct the Quantum Layer In the first step we build user-defined functions that allow flexible modeling: 1. `angle_encoding`: This function loads data of size num\_qubits on num\_qubits qubits via RY gates. 2. `encoder_ansatz` : A simple variational ansatz for encoding num\_qubits qubits on num\_encoding\_qubits qubits (see the description in the code block). ```python theme={null} !pip install -qq -U "classiq[qml]" ``` ```python theme={null} from classiq import * from classiq.qmod.symbolic import pi ``` ```python theme={null} @qfunc def angle_encoding(exe_params: CArray[CReal], qbv: QArray) -> None: repeat( count=exe_params.len, iteration=lambda index: RY(pi * exe_params[index], qbv[index]), ) ``` ```python theme={null} @qfunc def encoder_ansatz( exe_params: CArray[CReal], coded: QArray, trash: QArray, ) -> None: """ This is a parametric model that acts on num_qubits=trash.len+coded.len qubits. It contains trash.len layers, each composed of RY gates and CX gates with a linear connectivity, and a final layer with RY gate on each of the trash qubits is applied. """ num_qubits = trash.len + coded.len x = QArray() within_apply( lambda: bind([coded, trash], x), lambda: repeat( trash.len, lambda r: ( repeat(num_qubits, lambda i: RY(exe_params[r * num_qubits + i], x[i])), repeat(num_qubits - 1, lambda i: CX(x[i], x[i + 1])), ), ), ) repeat(trash.len, lambda i: RY(exe_params[(trash.len) * num_qubits + i], trash[i])) ``` ## Example: Autoencoder for Domain Wall Data In the following example we try to encode data which has a domain wall structure. Let us define the relevant data for strings of size 4. # ## The Data ```python theme={null} import numpy as np domain_wall_data = np.array([[0, 0, 1, 1], [0, 0, 0, 1], [0, 1, 1, 1]]) print("domain wall data:\n", domain_wall_data) ``` **Output:** ``` domain wall data: [[0 0 1 1] [0 0 0 1] [0 1 1 1]] ``` # ## The Quantum Program We encode this data of size 4 on 2 qubits. Let us build the corresponding quantum layer based on the predefined functions above: ```python theme={null} NUM_QUBITS = 4 NUM_ENCODING_QUBITS = 2 num_trash_qubits = NUM_QUBITS - NUM_ENCODING_QUBITS num_weights_in_encoder = NUM_QUBITS * num_trash_qubits + num_trash_qubits ``` We construct the model: ```python theme={null} @qfunc def main( w: CArray[CReal, num_weights_in_encoder], input_data: CArray[CReal, NUM_QUBITS], coded: Output[QArray[NUM_ENCODING_QUBITS]], trash: Output[QArray[num_trash_qubits]], test: Output[QBit], ) -> None: psi2 = QArray() allocate(num_trash_qubits, psi2) allocate(coded) allocate(trash) angle_encoding(input_data, [coded, trash]) encoder_ansatz( exe_params=w, coded=coded, trash=trash, ) swap_test(state1=trash, state2=psi2, test=test) drop(psi2) ``` We synthesize and visualize the quantum layer: ```python theme={null} qprog_ae_network = synthesize(main) show(qprog_ae_network) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3561eTc4YsmOErGa4GoTroqL0Yz ``` # ## The Network The network for training contains only a quantum layer. The corresponding quantum program was already defined above, so what remains is to define the execution preferences and the classical postprocess. The classical output is defined as $1-\alpha^2$, with $\alpha$ being the probability of the test qubit being at state 0. ```python theme={null} import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader from classiq.applications.qnn import QLayer from classiq.applications.qnn.types import ( MultipleArguments, ResultsCollection, SavedResult, ) from classiq.execution import ( ExecutionPreferences, execute_qnn, set_quantum_program_execution_preferences, ) ``` ```python theme={null} num_shots = 4096 def execute( quantum_program: QuantumProgram, arguments: MultipleArguments ) -> ResultsCollection: quantum_program = set_quantum_program_execution_preferences( quantum_program, preferences=ExecutionPreferences(num_shots=num_shots) ) return execute_qnn(quantum_program, arguments) def post_process(result: SavedResult) -> torch.Tensor: alpha_sqaured = result.value.counts_of_output("test").get("0", 0) / num_shots out = 1 - alpha_sqaured return torch.tensor(out) ``` ```python theme={null} def create_net(*args, **kwargs) -> nn.Module: class Net(nn.Module): def __init__(self, *args, **kwargs): super().__init__() self.qlayer = QLayer( qprog_ae_network, execute, post_process, *args, **kwargs, ) def forward(self, x): x = self.qlayer(x) return x return Net(*args, **kwargs) encoder_train_network = create_net() ``` # ## Creating the Dataset The cost function to minimize is $|1-\alpha^2|$ for all our training data. Looking at the Qlayer output, this means that we should define the corresponding labels as $0$: ```python theme={null} class MyDWDataset: def __init__(self, data, labels) -> None: self.data = torch.from_numpy(data).float() self.labels = torch.unsqueeze(torch.from_numpy(labels), dim=-1).float() def __len__(self): return self.data.shape[0] def __getitem__(self, idx): return self.data[idx], self.labels[idx] ``` ```python theme={null} labels = np.array([0, 0, 0]) train_dataset = MyDWDataset(domain_wall_data, labels) train_data_loader = DataLoader( train_dataset, batch_size=2, shuffle=True, drop_last=False ) ``` # ## Defining the Training ```python theme={null} import time as time def train( model: nn.Module, data_loader: DataLoader, loss_func: nn.modules.loss._Loss, optimizer: optim.Optimizer, epoch: int = 40, ) -> None: for index in range(epoch): start = time.time() for data, label in data_loader: optimizer.zero_grad() output = model(data) loss = loss_func(torch.squeeze(output), torch.squeeze(label)) loss.backward() optimizer.step() print(time.time() - start) print(index, f"\tloss = {loss.item()}") ``` # ## Setting Hyperparameters The L1 loss function fits the intended cost function we aim to minimize: ```python theme={null} _LEARNING_RATE = 0.3 loss_func = nn.L1Loss() optimizer = optim.SGD(encoder_train_network.parameters(), lr=_LEARNING_RATE) ``` # ## Training In this demo we initialize the network with trained parameters and run only one epoch for demonstration purposes. Reasonable training with the above hyperparameters can be achieved with $\sim 40$ epochs. To train the network from the beginning, uncomment the following code line: ```python theme={null} trained_weights = torch.nn.Parameter( torch.Tensor( [1.5227, 0.3588, 0.6905, 1.4777, 1.5718, 1.5615, 1.5414, 0.6021, 0.1254, 0.9903] ) ) encoder_train_network.qlayer.weight = trained_weights ``` ```python theme={null} data_loader = train_data_loader train(encoder_train_network, data_loader, loss_func, optimizer, epoch=1) ``` **Output:** ``` 8.214931964874268 0 loss = 0.000732421875 ``` # ## Verifying Once we have trained the network, we can build a new network with the trained variables. We verify our encoder by taking only the encoding block, changing the postprocess, etc. Below, we verify our quantum autoencoder by comparing the input with the output of an encoder-decoder network. We create the following network containing three quantum blocks: * The first two blocks of the previous network: a block for loading the inputs followed by our quantum encoder. * We reset the trash qubits, assigning them to be at the zero state explicitly. * The inverse of the quantum encoder. **The network weights are allocated with the trained ones.** Screenshot 2025-07-03 at 0.00.11.png # ## Building the Quantum Validator ```python theme={null} @qfunc def main( w: CArray[CReal, num_weights_in_encoder], input_data: CArray[CReal, NUM_QUBITS], decoded: Output[QArray[NUM_QUBITS]], ) -> None: allocate(decoded) angle_encoding(input_data, decoded) encoder_ansatz( exe_params=w, coded=decoded[0:NUM_ENCODING_QUBITS], trash=decoded[NUM_ENCODING_QUBITS:NUM_QUBITS], ) repeat(num_trash_qubits, lambda i: RESET(decoded[NUM_ENCODING_QUBITS + i])) invert( lambda: encoder_ansatz( exe_params=w, coded=decoded[0:NUM_ENCODING_QUBITS], trash=decoded[NUM_ENCODING_QUBITS:NUM_QUBITS], ) ) ``` ```python theme={null} qprog_validator = synthesize(main) show(qprog_validator) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3561fzFZysryxH7UfubDyHnPtTc ``` # ## Validating the Result For the validator postprocessing, we take the output with the maximum counts. We run the validator quantum program with the trained weights, and compare every input data with its output. ```python theme={null} trained_w = encoder_train_network.qlayer.weight.tolist() input_data = train_dataset.data.tolist() batch_data = [{"w": trained_w, "input_data": data} for data in input_data] results_validator = sample(qprog_validator, parameters=batch_data) ``` Now we can compare the input with the output of the validator for different data: ```python theme={null} for data, res in zip(input_data, results_validator): df = res output = df.loc[df["probability"].idxmax(), "decoded"] print("input =", data, ", output =", output) ``` **Output:** ``` input = [0.0, 0.0, 1.0, 1.0] , output = [0, 0, 1, 1] input = [0.0, 0.0, 0.0, 1.0] , output = [0, 0, 0, 1] input = [0.0, 1.0, 1.0, 1.0] , output = [0, 1, 1, 1] ``` # ## Detecting Anomalies We can use our trained network for anomaly detection. Let's see what happens to the trash qubits when we insert an anomaly; namely, non-domain-wall data: ```python theme={null} import random input_anomaly_data = [ [0, 0, 1, 1], [0, 0, 0, 1], [0, 1, 1, 1], [1, 0, 1, 0], [1, 1, 1, 1], ] random.shuffle(input_anomaly_data) batch_data = [{"w": trained_w, "input_data": data} for data in input_anomaly_data] results_anomaly = sample(qprog_ae_network, parameters=batch_data) ``` We print all the anomaly data based on predefined accuracy for the cost function: ```python theme={null} tolerance = 1e-2 for data, res in zip(input_anomaly_data, results_anomaly): # The probabiliy of the test qubit alpha_sqaured = res.loc[res["test"] == 0, "counts"].sum() / num_shots output = 1 - alpha_sqaured if abs(output) > tolerance: print(f"input= {data}, loss= {output} ----> ANOMALY DETECTED") else: print(f"input= {data}, loss= {output}") ``` **Output:** ``` input= [0, 0, 0, 1], loss= 0.00146484375 input= [0, 0, 1, 1], loss= 0.002197265625 input= [1, 0, 1, 0], loss= 0.4873046875 ----> ANOMALY DETECTED input= [1, 1, 1, 1], loss= 0.49560546875 ----> ANOMALY DETECTED input= [0, 1, 1, 1], loss= 0.00048828125 ``` ## Alternative Network for Training a Quantum Autoencoder Another way to introduce a cost function is by estimating Hamiltonians. Measuring the Pauli $Z$ matrix on a qubit at the general state $|q\rangle=a|0\rangle+b|1\rangle$ is $\langle q |Z|q \rangle=a^2-b^2$. Therefore, a cost function can be defined by taking expectation values on the trash output (without a swap test) as follows: $$ \text{Cost} = \frac{1}{2}\sum^{\text{num of trash qubits}}_{k=1} 1 - \langle Z_k \rangle. $$ Below we show how to define the corresponding Qlayer: the quantum program and postprocessing. # ## The Quantum Program ```python theme={null} @qfunc def main( w: CArray[CReal, num_weights_in_encoder], input_data: CArray[CReal, NUM_QUBITS], trash: Output[QArray[num_trash_qubits]], ) -> None: coded = QArray() allocate(NUM_ENCODING_QUBITS, coded) allocate(trash) angle_encoding(input_data, [coded, trash]) encoder_ansatz( exe_params=w, coded=coded, trash=trash, ) drop(coded) qprog_ae_alt = synthesize(main) show(qprog_ae_alt) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3561gzWYfgvEGJZtxmxerFKeo1a ``` # ## Executing and Postprocessing The size of the trash register is 2. We measure the Pauli $Z$ matrix on each of its qubits: ```python theme={null} from classiq.applications.chemistry import PauliOperator, PauliOperators def execute( quantum_program: QuantumProgram, arguments: MultipleArguments ) -> ResultsCollection: return execute_qnn( quantum_program, arguments, observable=PauliOperator(pauli_list=[("IZ", 1), ("ZI", 1)]), ) def post_process(result: SavedResult) -> torch.Tensor: out = 1 / 2 * (2 - np.real(result.value.value)) return torch.tensor(out) ``` ```python theme={null} def create_net(*args, **kwargs) -> nn.Module: class Net(nn.Module): def __init__(self, *args, **kwargs): super().__init__() self.qlayer = QLayer( qprog_ae_alt, execute, post_process, *args, **kwargs, ) def forward(self, x): x = self.qlayer(x) return x return Net(*args, **kwargs) encoder_train_network = create_net() ``` ```python theme={null} trained_weights = torch.nn.Parameter( torch.Tensor( [1.5227, 0.3588, 0.6905, 1.4777, 1.5718, 1.5615, 1.5414, 0.6021, 0.1254, 0.9903] ) ) encoder_train_network.qlayer.weight = trained_weights ``` ```python theme={null} data_loader = train_data_loader train(encoder_train_network, data_loader, loss_func, optimizer, epoch=1) ``` **Output:** ``` 7.373134136199951 0 loss = 0.00390625 ``` # Oblivious Amplitude Amplification Source: https://docs.classiq.io/explore/algorithms/amplitude_amplification_and_estimation/oblivious_amplitude_amplification/oblivious_amplitude_amplification Open this notebook in GitHub to run it yourself This demo explains the Oblivious Amplitude Amplification algorithm (OAA) \[[2](#oaa)], which can be used as a building block in algorithms such as Hamiltonian simulations. We start with a short recap, then show how to use it in conjunction with the Linear Combination of Unitaries (LCU) algorithm. ## Problem Formulation * **Input:** A $(s, l, 0)$-block-encoding $W$ of a matrix $U$. * **Promise:** $U$ is unitary (or close to unitary). * **Output:** A $(1, l, 0)$-block-encoding of $U$. ## Background # ## Recap: Amplitude Amplification Ref. \[[1](#aa)] shows how to use the Grover operator to amplify a specific state. In detail, given a unitary $A$ to prepare a state $|\psi\rangle$: $$ A|0\rangle = |\psi\rangle = a|\psi_0\rangle + \sqrt{1-a^2}|\psi_1\rangle $$ and a unitary $R_{\psi_1}$ to implement a reflection over the state $\psi_1$: $$ R_{\psi_1}(a|\psi_0\rangle + \sqrt{1-a^2}|\psi_1\rangle) = a|\psi_0\rangle - \sqrt{1-a^2}|\psi_1\rangle $$ We want to decrease the amplitude of $\psi_1$ to be 0. $|\psi_0\rangle$, $|\psi_1\rangle$ define a two-dimensional subspace where the applications of the Grover operator are effectively rotations. To do that we also need to do a reflection about the initial state $|\psi\rangle$: $$ S_\psi = AR_0A^{\dagger} $$ where $R_0$ is a reflection about the $|0\rangle$ state. The rotations can be used to take the initial vector $|\psi\rangle$ closer to $|\psi_0\rangle$. image.png # ## Why Another Version? As you might have noticed, the amplification requires the "recipe" $A$ for the preparation of $\psi$, and uses it to perform the reflection around the initial state. In certain scenarios, such as in Hamiltonian simulations, we might not be able to create the initial state or it is very inefficient. Specifically, we look at a $(s, l, 0)$-block-encoding $W$ of a matrix $U$: $$ W|0^l\rangle|\psi\rangle = \frac{1}{s}|0^l\rangle U|\psi\rangle + \sqrt{1-\frac{1}{s^2}}|\phi\rangle $$ (For detailed explanations of block-encodings and Hamiltonian simulations, see [this demo](https://github.com/Classiq/classiq-library/tree/main/algorithms/hamiltonian_simulation/hamiltonian_simulation_with_block_encoding/).) We want to amplify the amplitude of the state that is in the block, i.e., $|0^l\rangle U|\psi\rangle$. This sometimes can be done in post-selection, but if we use $W$ more than once in the algorithm, then the amplitude to sample the "good" states exponentially decreases. It turns out, however, that if $U$ is unitary, then we can reflect about the initial state $W|0^l\rangle|\psi\rangle$ for any $|\psi\rangle$ using the operator $WRW^\dagger$ where $R=(I-2|0^l\rangle\langle0^l)|\otimes I$, which is analogous to the reflection about the zero state. Then we get the same effective 2D picture! So, to get $|0^l\rangle U |\psi\rangle$ with probability $\thicksim 1$, roughly $\thicksim s$ Grover iterations are needed. image.png ## Building the Algorithm with Classiq image.png # ## Example: Unitary LCU Amplification Here we take the matrix $A = H_0\otimes H_1 = \frac{X_0 + Z_0} {\sqrt{2}} \otimes \frac{X_1 + Z_1} {\sqrt{2}}$ and block-encode it using LCU, creating the matrix $U_A$. In general, though LCU is a combination of unitaries, the result is not necessarily unitary. In this specific example, the matrix $A$ is unitary, and so the encoding can be amplified using oblivious amplitude amplification. # ### Block-Encoding the Hamiltonian First show that the sampled state after the application of $U_A$ is in the wanted block only in $\frac {1}{4}$ of the cases, meaning that in this case $s=2$. The input $|\psi\rangle$ to $U_A$ is a randomly sampled vector of normalized amplitudes. ```python theme={null} import numpy as np from classiq import * HAMILTONIAN = 0.5 * ( Pauli.Z(0) * Pauli.X(1) + Pauli.X(0) * Pauli.Z(1) + Pauli.X(0) * Pauli.X(1) + Pauli.Z(0) * Pauli.Z(1) ) @qfunc def block_encode(hamiltonian: SparsePauliOp, data: QArray, block: QNum): lcu_pauli(hamiltonian, data, block) @qfunc def main(data: Output[QNum], block: Output[QNum]): allocate(2, block) # initialize a random vector np.random.seed(1) amps = np.random.rand(4) amps = (amps / np.linalg.norm(amps)).tolist() prepare_amplitudes(amps, 0, data) block_encode(HAMILTONIAN, data, block) qprog = synthesize(main) result = execute(qprog).result_value() ``` Print the "good" states: ```python theme={null} df = result.dataframe df[df.block == 0] ``` | | data | block | count | probability | bitstring | | - | ---- | ----- | ----- | ----------- | --------- | | 3 | 0 | 0 | 323 | 0.157715 | 0000 | | 4 | 2 | 0 | 122 | 0.059570 | 0010 | | 8 | 1 | 0 | 73 | 0.035645 | 0001 | ```python theme={null} print("Fraction of `good` states:", df[df.block == 0].probability.sum()) ``` **Output:** ``` Fraction of `good` states: 0.2529296875 ``` # ### Block-Encoding Amplification Now we wrap $U_A$ with the oblivious amplitude amplification scheme. It is almost like regular Grover, except that it does not take the initial state preparation to the Grover operator. Also, the Grover diffuser only works on the block qubits. (We take advantage of the language [capturing](https://docs.classiq.io/latest/qmod-reference/language-reference/operators/?h=opera#capturing-context-variables-and-parameters) mechanism.) The `block_oracle` ($R$ in the used notation) operates on the block qubits and checks that they are in the wanted block (i.e., equal to the state $|00\rangle$). The $W$ operator is represented by the function `block_encoding`. As the original total amplitude of the "good states" is $\frac{1}{2}$, exactly one Grover iteration will amplify the amplitude to 1. ```python theme={null} @qfunc def oblivious_amplitude_amplification( reps: CInt, block_encoding: QCallable[QNum, QArray], block: QNum, data: QArray, ): @qperm def block_oracle(b: Const[QNum], res: QBit): res ^= b == 0 block_encoding(data, block) repeat( reps, lambda index: grover_operator( lambda b: phase_oracle(lambda x, res: block_oracle(x, res), b), lambda b: block_encoding(data, b), block, ), ) @qfunc def main(data: Output[QNum], block: Output[QNum]): allocate(2, block) # initialize a random vector np.random.seed(1) amps = np.random.rand(4) amps = (amps / np.linalg.norm(amps)).tolist() prepare_amplitudes(amps, 0, data) oblivious_amplitude_amplification( 1, lambda _block, _data: block_encode(HAMILTONIAN, _block, _data), block, data ) qprog_2 = synthesize(main) show(qprog_2) result_2 = execute(qprog_2).result_value() ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/36hZZ6QNxpJsFRG0gj4NNO5sPFr ``` Print the "good" states: ```python theme={null} df = result_2.dataframe df[df.block == 0] ``` | | data | block | count | probability | bitstring | | - | ---- | ----- | ----- | ----------- | --------- | | 0 | 0 | 0 | 1353 | 0.660645 | 0000 | | 1 | 2 | 0 | 456 | 0.222656 | 0010 | | 2 | 1 | 0 | 239 | 0.116699 | 0001 | ```python theme={null} print("Fraction of `good` states:", df.probability.sum()) assert np.isclose(df.probability.sum(), 1) ``` **Output:** ``` Fraction of `good` states: 1.0 ``` # ## Extending Where $U$ Is Non-Unitary It was proven that when $U$ is close to unitary, it is still possible to amplify (using the same operators) and get a good approximation. This is termed "Robust Oblivious Amplitude Amplification" \[[3](#roaa)]. This is usually the case in Hamiltonian simulation when the LCU reprsents a truncated series which is only an approximation of the unitary $e^{-iHt}$. ## References \[1]: [Brassard, Gilles, et al. "Quantum Amplitude Amplification and Estimation." arXiv preprint quant-ph/0005055 (2000).](https://arxiv.org/abs/quant-ph/0005055) \[2]: [Berry, Dominic W., et al. "Exponential improvement in precision for simulating sparse Hamiltonians." Proceedings of the forty-sixth annual ACM symposium on Theory of Computing (2014).](https://dl.acm.org/doi/abs/10.1145/2591796.2591854) \[3]: [Berry, Dominic W., et al. "Simulating Hamiltonian dynamics with a truncated Taylor series." Physical Review Letters 114.9 (2015): 090502.](https://journals.aps.org/prl/abstract/10.1103/PhysRevLett.114.090502) # Quantum Monte Carlo Integration (QMCI) Source: https://docs.classiq.io/explore/algorithms/amplitude_amplification_and_estimation/qmc_user_defined/qmc_user_defined Open this notebook in GitHub to run it yourself Monte Carlo integration refers to estimating expectation values of a function $f(x)$, where $x$ is a random variable drawn from some known distribution $p$: $$ \tag{1} E_{p}(x) = \int f(x)p(x) dx. $$ Such evaluations appear in the context of option pricing or credit risk analysis. The basic idea of QMCI assumes that we have a quantum function $A$, which, for a given $f$ and $p$, loads the following state of $n+1$ qubits: $$ \begin{aligned} \tag{2} A|0\rangle_n|0\rangle = \sum^{2^n-1}_{i=0} \sqrt{f_i} \sqrt{p_i}|i\rangle_n|1\rangle + \sum^{2^n-1}_{i=0} \sqrt{1-f_i} \sqrt{p_i}|i\rangle_n|0\rangle = \sqrt{a}|\psi_1\rangle+\sqrt{1-a}|\psi_0\rangle, \end{aligned} $$ where it is understood that the first $2^n$ states represent a discretized space of $x$, and that $0\leq f(x)\leq 1$. Then, by applying the amplitude estimation (AE) algorithm for the "good-state" $|\psi_1 \rangle$, we can estimate its amplitude: $$ a = \sum^{2^n-1}_{i=0} f_i p_i. $$ The QMCI algorithm can be separated into two parts: 1. Constructing a Grover operator for the specific problem. This is done here almost from scratch. 1. Applying the AE algorithm based on the Grover operator \[[1](#ae)]. This is done by calling the Classiq Quantum Phase Estimation (QPE) function. ## Specific Use Case for the Tutorial For simplicity we consider a simple use case. We take a probability distribution on the integers $$ \tag{3} p_i = \frac{i}{\mathcal{N}} \text{ for } i\in \{0,\dots 2^3-1\}, $$ where $\mathcal{N}$ is a normalization constant, and we would like to evaluate the expectation value of the function $$ \tag{4} f(x) = \sin^2(0.25x+0.2). $$ Therefore, the value we want to evaluate is $$ a= \frac{1}{\mathcal{N}} \sum^7_{k=0} \sin^2(0.25k+0.2) k \approx 0.834. $$ *** \*This tutorial illustrats how to construct a Quantum Monte Carlo Integration (QMCI), defining all its building blocks in Qmod (rather than using open-library functions). The example below demonstrates how we can exploit various concepts of modeling quantum algorithms with Classiq when building our own functions\*. *** ## 1. Building the Corresponding Grover Operator ```python theme={null} import matplotlib.pyplot as plt from classiq import * ``` # ## Grover Operator for QMCI The Grover operator suitable for QMCI is defined as follows: $$ Q\equiv - S_{\psi_1} A^{\dagger} S_0 A, $$ with $S_0$ and $S_{\psi_1}$ being reflection operators around the zero state $|0\rangle_n|0\rangle$ and the good-state $|\psi_1\rangle$, respectively, and the function $A$ is defined in Eq. ([2](#mjx-eqn-2)). In subsections (1.1)-(1.3) below we build each of the quantum sub-functions, and then in subsection (1.4) we combine them to define a complete Grover operator. On the way we introduce several concepts of functional modeling, which allow the Classiq synthesis engine to reach better optimized circuits. # ### 1.1) The State Loading $A$ Function We start with constructing the $A$ operator in Eq. ([2](#mjx-eqn-2)). We define a quantum function and give it the name `state_loading`. The function's signature declares two arguments: 1. A quantum register `x` declared as `QArray` (an array of qubits with an unspecified size) that is used to represent the discretization of space. 2. A quantum register `ind` of size 1 declared as `QBit` to indicate the good state. Next, we construct the logic flow of the `state_loading` function. The function body consists of two quantum function calls: 1. As can be seen from Eq. ([2](#mjx-eqn-2)), the `load_probabilities` function is constructed using the Classiq `inplace_prepare_state` function call on $n=3$ qubits with probabilities $p_i$. 2. The `amplitude_loading` body calls the Classiq `linear_pauli_rotations` function. The `linear_pauli_rotations` loads the amplitude of the function $f(x) = sin^2(0.25 x + 0.2)$. *Note: The amplitude should be $sin$ so the probability is $sin^2$.* The function uses an auxiliary qubit that is utilized so that the desired probability reflects on the auxiliary qubit if it is in the `|1>` state. We use the function with the Pauli Y matrix and enter the appropriate slope and offset to achieve the right parameters. We define the probabilities according to the specific problem described by Eqs. ([3](#mjx-eqn-3)-[4](#mjx-eqn-4)). ```python theme={null} import numpy as np sp_num_qubits = 3 probabilities = np.linspace(0, 1, 2**sp_num_qubits) / sum( np.linspace(0, 1, 2**sp_num_qubits) ) slope = 0.5 offset = 0.4 @qfunc def load_probabilities(state: QArray): inplace_prepare_state(probabilities.tolist(), 0, state) @qfunc def amplitude_loading(x: QArray, ind: QBit): linear_pauli_rotations( bases=[Pauli.Y.value], slopes=[slope], offsets=[offset], x=x, q=ind ) @qfunc def state_loading(x: QArray, ind: QBit): load_probabilities(x) amplitude_loading(x=x, ind=ind) ``` To examine our function we define a quantum `main` function from which we can build a model, synthesize, and view the quantum program created: ```python theme={null} @qfunc def main(res: Output[QArray[QBit, sp_num_qubits]], ind: Output[QBit]): allocate(res) allocate(ind) state_loading(res, ind) model = create_model(main) qprog = synthesize(model) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3FmVHJIZID8qTQgNcvnJNRc0iL1 ``` # ### 1.2) $S_{\psi_1}$ Function * The Good State Oracle The next quantum function we define is the one that reflects around the good state: any $n+1$ state in which the `ind` register is at state $|1\rangle$. This function can be constructed with a ZGate on the `ind` register. ```python theme={null} @qfunc def good_state_oracle(ind: QBit): Z(ind) ``` # ### 1.3) $S_{0}$ Function * The Grover Diffuser To implement the Grover Diffuser we aim to perform a controlled-Z operation on the $|0>^n$ state. We can define a `zero_oracle` quantum function with the `x` and `ind` registers as its arguments. The `within_apply` operator takes two function arguments - compute and action - and invokes the sequence `compute()`, `action()`, and `invert(compute())`. Quantum objects that are allocated and prepared by compute are subsequently uncomputed and released. ```python theme={null} @qfunc def prepare_minus(q: QBit): X(q) H(q) @qfunc def zero_oracle(x: QNum, ind: QBit): within_apply(lambda: prepare_minus(ind), lambda: inplace_xor(x == 0, ind)) ``` The inplace xor operation, `ind ^= x==0`, is equivalent to `control(x==0, X(ind))`. We can verify that $$ \begin{aligned} |00\dots0\rangle \xrightarrow[{\rm ctrl(-Z)(target=q_0, ctrl=q_1\dots q_n)}]{} -|00\dots0\rangle, \\ |10\dots0\rangle \xrightarrow[{\rm ctrl(-Z)(target=q_0, ctrl=q_1\dots q_n)}]{} |10\dots0\rangle, \\ |11\dots0\rangle \xrightarrow[{\rm ctrl(-Z)(target=q_0, ctrl=q_1\dots q_n)}]{} |11\dots0\rangle,\\ |11\dots1\rangle \xrightarrow[{\rm ctrl(-Z)(target=q_0, ctrl=q_1\dots q_n)}]{} |11\dots1\rangle, \end{aligned} $$ which is exactly the functionality we want. # ### 1.4) $Q$ Function * The Grover Operator We can now define a complete Grover operator $Q\equiv -S_{\psi_1} A^{\dagger} S_0 A$. We do this in a single code block that calls the following: 1. The good state oracle (`good_state_oracle`) 2. THe inverse of the state preparation (`state_loading`) 3. The diffuser (`zero_oracle`) 4. The state preparation (`state_loading`) *Note:* * *Stages 2-4 are implemented by utilizing the `within_apply` operator* * *We add a global phase of -1 to the full operator by using the atomic gate level function `U`* ```python theme={null} from classiq.qmod.symbolic import pi @qfunc def my_grover_operator(state: QArray): good_state_oracle(ind=state[0]) within_apply( lambda: invert(lambda: state_loading(x=state[1 : state.len], ind=state[0])), lambda: zero_oracle(state[1 : state.len], state[0]), ) phase(pi) ``` **Let us look at the `my_grover_operator` function we created:** ```python theme={null} @qfunc def main(state: Output[QArray[QBit, sp_num_qubits + 1]]): allocate(state) my_grover_operator(state) model_2 = create_model(main) qprog_2 = synthesize(model_2) show(qprog_2) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3FmVIQ01BmW45Xd5DGPZ5RlFz26 ``` ## 2. Applying Amplitude Estimation (AE) with Quantum Phase Estimation (QPE) Here we apply a basic AE algorithm that is based on QPE. The idea behind this algorithm is the following: The state $A|0\rangle_n|0\rangle$ is spanned by two eigenvectors of our Grover operator $Q$, with the two corresponding eigenvalues $$ \tag{5} \lambda_{\pm}=\exp\left(\pm i2\pi \theta \right), \qquad \sin^2 \left(\pi \theta\right)\equiv a. $$ Therefore, if we apply a QPE on $A|0\rangle_n|0\rangle$, we have these two eigenvalues encoded in the QPE register. However, both give the value of $a$, so there is no ambiguity. To find $a$ we build a simple quantum model, applying $A$ on a quantum register of size $n+1$ initialized to zero, and then applying the Classiq QPE with the `my_grover_operator` we defined. Below is the `main` function from which we can build our model and synthesize it. In particular, we define the output register `phase` as `QNum` to hold the phase register output of the QPE. We choose a QPE with phase register of size 3, governing the accuracy of our phase-, and thus amplitude-, estimation. ```python theme={null} n_qpe = 3 @qfunc def main(phase: Output[QNum[n_qpe, SIGNED, n_qpe]]): state = QArray() allocate(sp_num_qubits + 1, state) state_loading(state[1 : state.len], state[0]) allocate(phase) qpe(unitary=lambda: my_grover_operator(state=state), phase=phase) drop(state) model_3 = create_model(main) model_3 = set_constraints(model_3, Constraints(max_width=n_qpe + sp_num_qubits + 1)) qprog_3 = synthesize(model_3) show(qprog_3) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3FmVKNkbDC48tJKUIwwuznFFgJq ``` We can export our model to a `.qmod` file: Screenshot 2025-06-20 at 18.29.06.png Screenshot 2025-06-20 at 18.29.19.png # ## Executing the Circuit and Measuring the Approximated Amplitude We execute on a simulator: ```python theme={null} result = execute(qprog_3).result_value() ``` ```python theme={null} ## mapping between register string to phases phases_counts = dict( (sampled_state.state["phase"], sampled_state.shots) for sampled_state in result.parsed_counts ) ``` Upon plotting the resulting histogram we see two phase values with high probability (however, both correspond to the same amplitude $a$): ```python theme={null} plt.bar(phases_counts.keys(), phases_counts.values(), width=0.1) plt.xticks(rotation=90) print("phase with max probability: ", max(phases_counts, key=phases_counts.get)) ``` **Output:** ``` phase with max probability: -0.375 ``` output Recalling the relation in Eq. ([5](#mjx-eqn-5)), we can read the amplitude $a$ from the phase with maximum probability and compare to the expected amplitude: ```python theme={null} measured_amplitude = np.sin(np.pi * max(phases_counts, key=phases_counts.get)) ** 2 exact_amplitude = sum( np.sin(0.5 * n / 2 + 0.4 / 2) ** 2 * probabilities[n] for n in range(2**3) ) print(f"measured amplitude: {measured_amplitude}") print(f"exact amplitude: {exact_amplitude}") ``` **Output:** ``` measured amplitude: 0.8535533905932737 exact amplitude: 0.8338393824876795 ``` ```python theme={null} assert np.abs(measured_amplitude - exact_amplitude) < 1e-1 ``` ## References \[1]: [Brassard, G., Hoyer, P., Mosca, M., & Tapp, A. (2002). Quantum Amplitude Amplification and Estimation. Contemporary Mathematics, 305, 53-74.](https://arxiv.org/abs/quant-ph/0005055) # Using QSVT for Fixed-Point Amplitude Amplification Source: https://docs.classiq.io/explore/algorithms/amplitude_amplification_and_estimation/qsvt_fixed_point_amplitude_amplification/qsvt_fixed_point_amplitude_amplification Open this notebook in GitHub to run it yourself This demo shows how to use the QSVT framework for search problems; specifically, implementing fixed-point amplitude amplification (FPAA). With FPAA, we do not know in advance the concentration of solutions for the search problem, but we want to sample a solution with high probability. In contrast, for the original Grover search algorithm, too many iterations might 'overshoot' the mark. The demo is based on the paper [Grand unification of quantum algorithms](#grand). Given $|s\rangle$ the initial state and $|t\rangle$ the 'good' states, we get an effective block encoding of a one-dimensional matrix $A=|t\rangle\langle s|$. Given that $a = \langle s|t\rangle\gt0$, we want to amplify $a$. The signal operator $U$ here is $I$ (and also $U^\dagger$). Now we implement two projector-rotations: one in the '$|s\rangle$' space and one in the '$|t\rangle$' space; each one around the given state, giving phase to the specific state. ```python theme={null} !pip install -qq "classiq[qsp]" -U ``` ## Defining the QSVT Circuit for the Problem ```python theme={null} import numpy as np from classiq import * ``` image.png We start with the general QSVT framework definition. It accepts a unitary that block-encodes a matrix together with projector-controlled phase functions, which rotate the state around each of the subspaces where the matrix is encoded. The `qsvt` function accepts 3 different functions: 1. **Block Encoding function**: the one for which the singular values are transformed. In this case, the block encoding is trivial, and is just the Identity. 2. **Domain projector-controlled-CNot**: a function that identifies subspace in the domain of the block encoding (i.e the columns of the block encoding). In our case it is an identifier for the initial state $|s\rangle$. 3. **Image projector-controlled-CNot**: a function that identifies subspace in the image of the block encoding (i.e the rows of the block encoding). In our case it is an identifier for the target state $|t\rangle$. So we get a 1x1 block encoded matrix, which will be transformed using a polynomial that approximates the sign function, so that all inputs will be amplified to approximately 1. We need an even degree polynomial since we need to "finish" our sequence in the image space of our unitary. ## Domain State Projector: Identify the $|s\rangle$ State ```python theme={null} @qfunc def initial_state_projector(state: QNum, aux: QBit): within_apply( lambda: hadamard_transform(state), lambda: inplace_xor(state == 0, aux) ) ``` ## Image States Projector: Identify the $|t\rangle$ State ```python theme={null} @qfunc def target_state_projector( arith_oracle: QCallable[QArray, QBit], state: QArray, aux: QBit, ): arith_oracle(state, aux) ``` ## Defining the Arithmetic Oracle We implement the following equation: `(a + b) == 3 and (c - a) == 2` with `a, b, c` in sizes 2, 1, 3: ```python theme={null} from classiq.qmod.symbolic import logical_and class OracleVars(QStruct): a: QNum[2] b: QNum[1] c: QNum[3] @qperm def arith_equation(state: OracleVars, res: QBit): res ^= logical_and((state.a + state.b) == 3, (state.c - state.a) == 2) ``` ## Wrapping Everything for the FPAA Case In the FPAA case, the provided unitary is just the Identity matrix! In addition, we provide defined projector functions: ```python theme={null} @qfunc def qsvt_fpaa( phase_seq: list[float], arith_oracle: QCallable[Const[OracleVars], QBit], state: OracleVars, aux: Output[QBit], ): allocate(aux) qsvt( phase_seq, proj_cnot_1=lambda _aux: initial_state_projector(state, _aux), proj_cnot_2=lambda _aux: target_state_projector(arith_oracle, state, _aux), u=lambda: IDENTITY(state), aux=aux, ) ``` ## Getting the Phase Sequence for the Sign Function Here for demonstration purpose we assume that the initial state has at least `MIN_OVERLAP=0.1` with the target state (in probability). So we approximate the constant function (a scale of 0.95 is used for stability), and we only care about the interval \[`MIN_OVERLAP`, 1]. ```python theme={null} from classiq.applications.qsp import qsp_approximate, qsvt_phases DEGREE = 25 SCALE = 0.95 MIN_OVERLAP = 0.1 def target_function(x): return SCALE * np.sign(x) pcoefs, max_err = qsp_approximate( target_function, degree=DEGREE, parity=1, interval=[MIN_OVERLAP, 1], plot=True ) print(max_err) phases = qsvt_phases(pcoefs) ``` output **Output:** ``` 0.03252868313167301 ``` ## Creating the Full QSVT Model ```python theme={null} @qfunc def main(state: Output[OracleVars], aux: Output[QBit]): allocate(state) hadamard_transform(state) qsvt_fpaa( phase_seq=phases, arith_oracle=arith_equation, state=state, aux=aux, ) ``` ## Synthesizing and Executing on a Simulator We use the Classiq synthesis engine to translate the model to a quantum circuit, and execute on the Classiq simulator: ```python theme={null} qprog = synthesize( main, constraints=Constraints(optimization_parameter="width"), ) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3FmYG5K1jLoHl7FZBmMzplXsQyl ``` Execute the circuit: ```python theme={null} result = execute(qprog).result_value() ``` ```python theme={null} def equation(a, b, c): return ((a + b) == 3) and ((c - a) == 2) measured_good_shots = 0 for r in result.parsed_counts: a, b, c, aux = ( r.state["state"]["a"], r.state["state"]["b"], r.state["state"]["c"], r.state["aux"], ) if equation(a, b, c) and (aux == 0): print( f"a: {a}, b: {b}, c: {c}, aux: {aux}, equation_result: {equation(a, b, c)}, counts={r.shots}" ) measured_good_shots += r.shots print("Measured good shots:", measured_good_shots) ``` **Output:** ``` a: 2, b: 1, c: 4, aux: 0, equation_result: True, counts=953 a: 3, b: 0, c: 5, aux: 0, equation_result: True, counts=925 Measured good shots: 1878 ``` What do we expect? We need to substitute the amplitude of $|s\rangle\langle t|$ in $P(x)$: ```python theme={null} poly_cheb = np.polynomial.Chebyshev(pcoefs) p_good_shot = poly_cheb(np.sqrt(2 / 2**6)) ** 2 print("Expected good shots:", result.num_shots * p_good_shot) ``` **Output:** ``` Expected good shots: 1878.189404991538 ``` Indeed, we received the expected result according to the polynomial we created with the QSVT sequence: ```python theme={null} import scipy assert np.isclose( measured_good_shots, result.num_shots * p_good_shot, atol=5 * scipy.stats.binom.std(result.num_shots, p_good_shot), ) ``` ## References \[1]: [Martyn JM, Rossi ZM, Tan AK, Chuang IL. Grand unification of quantum algorithms. PRX Quantum. 2021 Dec 3;2(4):040203.](https://journals.aps.org/prxquantum/abstract/10.1103/PRXQuantum.2.040203) # Quantum Counting Using the Iterative Quantum Amplitude Estimation Algorithm Source: https://docs.classiq.io/explore/algorithms/amplitude_amplification_and_estimation/quantum_counting/quantum_counting Open this notebook in GitHub to run it yourself The quantum counting algorithm \[[1](#qcwiki)] efficiently estimates the number of valid solutions to a search problem, based on the amplitude estimation algorithm. It demonstrates a quadratic improvement with regard to a classical algorithm with black box oracle access to the function $f$. More precisely, given a Boolean function $f :\{0, 1\}^n\rightarrow\{0,1\}$, the counting problem estimates the number of inputs $x$ to $f$ such that $f(x)=1$. This tutorial demonstrates how to estimate the counting problem using a specific variant of the amplitude estimation algorithm: the Iterative Quantum Amplitude Estimation (IQAE) \[[2](#iqae)]. The IQAE does not rely on the Quantum Phase Estimation algorithm \[[3](#ae)], but purely on applications of the grover operator: $$ Q\equiv - A S_0 A^{\dagger} S_{\psi_1}, $$ thereby reducing the required number of qubits and gates of the circuit, at the expense of additional multiplicative factor polylogarithmic in the error $\epsilon$. ## Setting Up the Problem We choose this equation: $$ (a + b) <= 2 $$ where $a$, $b$ are 2-bit unsigned integers. This equation has six solutions. The goal is to estimate the number of valid solutions out of the 16 possible inputs, with precision $0.5$. ## Amplitude Estimation Using Phase Estimation We first show how to use quantum phase estimation algorithm for quantum counting \[[3](#ae)], then solve it using the IQAE method. Given a state $|\psi\rangle$ such that $|\psi\rangle=\sqrt{a}|\psi_1\rangle+\sqrt{1-a}|\psi_0\rangle$ we can measure $a$ up to arbitrary precision, given the following building blocks: 1. State preparation: A unitary $A$ such that: $A|0\rangle = |\psi\rangle = \sqrt{a}|\psi_1\rangle+\sqrt{1-a}|\psi_0\rangle$. 2. Oracle: A unitary $S_{\psi_1}$ such that $S_{\psi_1}=I-2|\psi_1\rangle\langle\psi_1|$, which adds a $(-1)$ phase to $|\psi_1|\psi\rangle\rangle$ and does nothing to any orthognal states to $|\psi_1\rangle$. This is effectively a reflection around the "good" state $|\psi_1\rangle$. Given these two functions, we can construct the Grover operator: $$ Q\equiv - A S_0 A^{\dagger} S_{\psi_1} , $$ which is exactly the same operator as for the Grover's search algorithm. In the subspace spanned by $|\psi_1\rangle$ and $|\psi_0\rangle$, $Q$ has two eigenvalues: $$ \lambda_{\pm}=\exp\left(\pm i2\pi \theta \right), \qquad \sin^2 \left(\pi \theta\right)\equiv a. $$ Therefore, if we apply a QPE on $A|0\rangle$ we have these two eigenvalues encoded in the QPE register; however, both give the value of $a$, so there is no ambiguity. # ## Arithmetic Oracle We define the $S_{\psi_1}$ oracle: $$ S_{\psi_1}|a\rangle|b\rangle= (-1)^{f(a,b)}|a\rangle|b\rangle. $$ ```python theme={null} from classiq import * A_SIZE = 2 B_SIZE = 2 DOMAIN_SIZE = A_SIZE + B_SIZE class OracleVars(QStruct): a: QNum[A_SIZE] b: QNum[B_SIZE] @qperm def arith_equation(state: Const[OracleVars], res: QBit): res ^= state.a + state.b <= 2 # use phase kickback for turning the arith_equation to an oracle @qfunc def arith_oracle(state: OracleVars): phase_oracle(arith_equation, state) ``` # ## State Preparation Oracle The state preparation function $A$ reflects knowledge about the solution space and can be used to eliminate invalid assignments. Here we assume no knowledge of the solution space; hence, we use the uniform superposition state preparation. ```python theme={null} sp_oracle = hadamard_transform ``` # ## Wrapping All to the Phase Estimation We will achieve the desired precision only in the IQAE phase. Here, we compute the worst-case precision for five phase qubits: ```python theme={null} import numpy as np NUM_PHASE_QUBITS = 5 x = np.linspace(0, 1, 100) (2**DOMAIN_SIZE) * max( np.abs( np.sin(np.pi * x) ** 2 - np.sin(np.pi * (x - 1 / (2**NUM_PHASE_QUBITS))) ** 2 ) ) ``` **Output:** ``` 1.5681439279637486 ``` We use the `grover_operator` library function, where we plug in the defined oracles. We wrap it by the `qpe` function, that accepts the grover operator as a unitary, and additional `phase` register. ```python theme={null} @qfunc def main( phase_reg: Output[QNum[NUM_PHASE_QUBITS, UNSIGNED, NUM_PHASE_QUBITS]], ) -> None: state_reg = OracleVars() allocate(state_reg) allocate(phase_reg) sp_oracle(state_reg) qpe( unitary=lambda: grover_operator( arith_oracle, sp_oracle, state_reg, ), phase=phase_reg, ) drop(state_reg) ``` # ## Synthesizing the Model to a Quantum Program ```python theme={null} qprog_qpe = synthesize( main, constraints=Constraints(optimization_parameter="width"), preferences=Preferences(optimization_level=1), ) show(qprog_qpe) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3FmYyPKeLGOG9ycw32asB3VNNTl ``` # ## Executing the Quantum Program ```python theme={null} result = sample(qprog_qpe) ``` Upon plotting the resulting histogram, we see two phase values with high probability (however, both correspond to the same amplitude). Note that `phase_reg` is already coded as fixed QNum in the range \[0,1]. ```python theme={null} import matplotlib.pyplot as plt phases_counts = dict(zip(result["phase_reg"], result["counts"])) plt.bar(phases_counts.keys(), phases_counts.values(), width=0.1) plt.xticks(rotation=90) print("phase with max probability: ", max(phases_counts, key=phases_counts.get)) ``` **Output:** ``` phase with max probability: 0.21875 ``` output From the phase, we can extract the number of solutions: ```python theme={null} expected_num_solutions = 6 solutions_ratio_qpe = np.sin(np.pi * max(phases_counts, key=phases_counts.get)) ** 2 print( "Number of solutions: ", (2**DOMAIN_SIZE) * solutions_ratio_qpe, ) assert np.isclose( (2**DOMAIN_SIZE) * solutions_ratio_qpe, expected_num_solutions, atol=1.5 ) ``` **Output:** ``` Number of solutions: 6.439277423870974 ``` ## Amplitude Estimation Using Iterative Quantum Amplitude Estimation Now we are ready for the iterative method. Instead of QPE, the algorithm applies the unitary $$ (Q)^mA $$ where $m$, the number of repetitions, changes between iterations of the algorithm. There is one subtlety that changes the way we work with the Grover operator. The classical algorithm expects an additional indicator qubit that marks the "good" states, i.e.: $$ |a\rangle|b\rangle|f(a,b)\rangle $$ So now, most of our logic goes into the state preparation oracle ($A$). It combines the loading of the solution space with setting the indicator qubit. ```python theme={null} @qfunc def iqae_state_preparation(vars: OracleVars, ind: QBit): hadamard_transform(vars) arith_equation(vars, ind) ``` # ## Wrapping All to the Iterative Quantum Amplitude Estimation Algorithm We use the built-in `IQAE` class that the quantum code for the algorithm as well as the classical execution code. The circuit starts with the state $A|0\rangle$, then applies iterations of the Grover operator. Note that the algorithm applies a varied number of Grover iterations on each execution. The number of iterations is chosen dynamically based on previous execution results, using statistical inference methods. It expects a state preparation function that creates the following state: $$ |\Psi\rangle = a|\Psi_1\rangle|1\rangle_{ind} + \sqrt{1-a^2}|\Psi_0\rangle|0\rangle_{ind} $$ Where the indicator qubit is marking the wanted state. ```python theme={null} from classiq.applications.iqae.iqae import IQAE iqae = IQAE( state_prep_op=iqae_state_preparation, problem_vars_size=DOMAIN_SIZE, constraints=Constraints(optimization_parameter="width"), preferences=Preferences(optimization_level=1), ) ``` # ## Synthesizing the Model to a Quantum Program ```python theme={null} qprog_iqae = iqae.get_qprog() show(qprog_iqae) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3FmZ0wHQhG9riz1i4xNcdBMiw8U ``` # ## Executing the Quantum Program ```python theme={null} iqae_result = iqae.run( epsilon=1 / (2**DOMAIN_SIZE * 2), # desired error alpha=0.01, # desired probability for error ) ``` We set $\epsilon = 1/{2^4} \cdot 0.5 = 1/32$. `alpha` is the tail probability of estimating the result with accuracy $\epsilon$. ```python theme={null} print( f"Number of solutions: {(2**DOMAIN_SIZE) * iqae_result.estimation}, accuracy: " f"{(2**DOMAIN_SIZE)*(iqae_result.confidence_interval[1]-iqae_result.confidence_interval[0])}" ) ``` **Output:** ``` Number of solutions: 5.974828015756142, accuracy: 0.12016928519471914 ``` ```python theme={null} assert np.isclose( (2**DOMAIN_SIZE) * iqae_result.estimation, 6, atol=1 / (2**DOMAIN_SIZE - 1) ) ``` We can also see the statistics of the IQAE execution: ```python theme={null} for i, iteration in enumerate(iqae_result.iterations_data): print( f"iteration_id: {i}, num grover iterations: {iteration.grover_iterations}, counts: {iteration.sample_results.counts}" ) ``` **Output:** ``` iteration_id: 0, num grover iterations: 0, counts: {'0': 1325, '1': 723} iteration_id: 1, num grover iterations: 5, counts: {'0': 697, '1': 1351} ``` ## References \[1]: [Quantum Counting Algorithm, Wikipedia](https://en.wikipedia.org/wiki/Quantum_counting_algorithm). \[2]: [Grinko, D., Gacon, J., Zoufal, C. et al. Iterative quantum amplitude estimation. npj Quantum Inf 7, 52 (2021)](https://doi.org/10.1038/s41534-021-00379-1). \[3]: [Brassard, G., Hoyer, P., Mosca, M., & Tapp, A. (2002). Quantum Amplitude Amplification and Estimation. Contemporary Mathematics, 305, 53-74.](https://arxiv.org/abs/quant-ph/0005055) # Bernstein-Vazirani Algorithm Source: https://docs.classiq.io/explore/algorithms/foundational/bernstein_vazirani/bernstein_vazirani Open this notebook in GitHub to run it yourself > **The Bernstein-Vazirani (BV) algorithm** [\[1\]](#original-paper), [\[2\]](#bvwiki), introduced by Ethan Bernstein and Umesh Vazirani, is a fundamental quantum algorithm that addresses a special case of the hidden-shift problem. It employs the same functional circuit structure as the Deutsch-Jozsa algorithm and achieves a linear speedup over its classical counterpart in the oracle query model. > > The algorithm treats the following problem: > > * **Input:** A Boolean function $f: \{0,1\}^n \rightarrow \{0,1\}$ defined as $$ f(x)\equiv (x\cdot a) \,\,\mod 2~~, $$ where $ \cdot$ refers to a bitwise dot operation, and $a$ is a binary string of length $n$. > * **Promise:** > * **Output:** Returns the secret string $a$ with minimum inquiries of the function. > > **Complexity:** The quantum approach requires a single query call, therefore, the quantum complexity query is $O(1)$. In contrast, Classically, the minimum inquiries of the function $f$ for determining the secret string is $n$: $f$ is called with these strings: $$ \begin{aligned} f(100\dots0) &= a_0~, \\ f(010\dots0) &= a_1~, \\ \vdots\\ f(00\dots01) &= a_{n-1}~, \end{aligned} $$ which reveals the secret string, one bit at a time. If one allows for randomness, the expected number of queries remains linear in $n$. > > *** > > **Keywords:** Hidden string problem, Foundational quantum algorithms, Oracle/Query complexity. bv_qprog.png ## How to Build the Algorithm with Classiq The BV algorithm contains three function blocks: an oracle for the predicate $f$, "sandwiched" between two Hadamard transforms. The resulting state corresponds to the secret string. The full [mathematical derivation](#technical-notes) is at the end of this notebook. # ## Implementing the BV Predicate A simple quantum implementation of the binary function $f(x)$ applies a series of controlled-X operations: starting with the state $|f\rangle=|0\rangle$, we apply an X gate, controlled on the $|x_i\rangle$ state, for all $i$ such that $a_i=1$: $$ |x_0\dots x_{n-1}\rangle |0\rangle_f \rightarrow \Pi_{i: a_i=1} {\rm CX}(x_i,f) |x_0\dots x_{n-1}\rangle |0\rangle_f = |x_0\dots x_{n-1}\rangle X^{a\cdot x}|0\rangle_f=|x_0\dots x_{n-1}\rangle |a\cdot x \left(\text{ mod } 2\right)\rangle_f. $$ ```python theme={null} from classiq import * from classiq.qmod.symbolic import floor @qperm def bv_predicate(a: CInt, x: Const[QArray], res: QBit): repeat( x.len, lambda i: if_(floor(a / 2**i) % 2 == 1, lambda: CX(x[i], res)), ) ``` Figure 2 shows an example of such implementation, for $a=01101$ and $n=5$. Screenshot 2025-06-19 at 22.14.13.png # ## Implementing the BV Quantum Function The quantum part of the BV algorithm is essentially identical to the `deutsch_jozsa` function in the [Deutsch-Jozsa notebook](https://github.com/Classiq/classiq-library/blob/main/algorithms/foundational/deutsch_jozsa/deutsch_jozsa.ipynb). However, in contrast to the latter, the predicate function implementation is fixed, depending solely on the secret string $a$. Hereafter, we refer to the secret string as a secret integer, defined as an integer argument for the `bv_function`: ```python theme={null} @qfunc def bv_function(a: CInt, x: QArray): aux = QBit() allocate(aux) within_apply( lambda: hadamard_transform(x), lambda: within_apply(lambda: (X(aux), H(aux)), lambda: bv_predicate(a, x, aux)), ) free(aux) ``` ## An Example on Five Qubits We construct a model for a specific example, setting the secret integer to $a=13$ and $n=5$. The algorithm requires only a single application of the quantum circuit, as in an ideal noiseless execution, the resulting quantum state corresponds exactly to the hidden integer (see the last Eq. ([1](#mjx-eqn-1)) below). Even in the presence of noise, the idea of using only a single query call of $f$ has demonstrated algorithmic speedup in Ref. \[[3](#ssbv)], where the authors considered a modified version of the BV algorithm in which the secret integer changes after every inquiry. In this example, we take `num_shots=1000` to highlight the fact that the resulting state is purely the secret string. ```python theme={null} import numpy as np SECRET_INT = 13 STRING_LENGTH = 5 NUM_SHOTS = 1000 assert ( np.floor(np.log2(SECRET_INT) + 1) <= STRING_LENGTH ), "The STRING_LENGTH cannot be smaller than secret string length" @qfunc def main(x: Output[QNum[STRING_LENGTH]]): allocate(x) bv_function(SECRET_INT, x) qprog = synthesize(main) ``` We can now visualize the circuit: ```python theme={null} show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3HrO3YKB2Lowf1psVSvybVV8JfY ``` **Output:** ``` https://platform.classiq.io/circuit/3HrO3YKB2Lowf1psVSvybVV8JfY?login=True&version=20 ``` We execute and extract the result: ```python theme={null} df = sample(qprog, num_shots=NUM_SHOTS) ``` **Output:** ``` Submitting job to simulator ``` **Output:** ``` Job: https://platform.classiq.io/jobs/6fe183aa-8066-47a6-a8be-208421bcb63d ``` ```python theme={null} secret_integer_q = df["x"][0] print("The secret integer is:", secret_integer_q) print( "The probability for measuring the secret integer is:", df["probability"][0], ) assert int(secret_integer_q) == SECRET_INT ``` **Output:** ``` The secret integer is: 13 The probability for measuring the secret integer is: 1.0 ``` ## Technical Notes Here is a brief summary of the linear algebra behind the Bernstein-Vazirani algorithm. The first Hadamard transformation generates an equal superposition over all the standard basis elements: $$ |0\rangle_n \xrightarrow[H^{\otimes n}]{} \frac{1}{2^{n/2}}\sum^{2^n-1}_{j=0}|j\rangle_n. $$ The oracle gets the Boolean Bernstein-Vazirani predicate and adds an $e^{\pi i}=-1$ phase to all states for which the function returns true: $$ \frac{1}{2^{n/2}}\sum^{2^n-1}_{j=0}|j\rangle_n \xrightarrow[\text{Oracle}(f(j))]{}\frac{1}{2^{n/2}}\sum^{2^n-1}_{j=0}(-1)^{a\cdot j}|j\rangle_n. $$ Finally, application of the Hadamard transform, which can be written as $H^{\otimes n}\equiv \frac{1}{2^{n/2}}\sum^{2^n-1}_{k,l=0}(-1)^{k\cdot l} |k\rangle \langle l| $, gives $$ \frac{1}{2^{n/2}}\sum^{2^n-1}_{j=0}(-1)^{a\cdot j}|j\rangle \xrightarrow[H^{\otimes n}]{} \sum^{2^n-1}_{k=0} \left(\frac{1}{2^{n}}\sum^{2^n-1}_{j=0}(-1)^{j\cdot \left(k\oplus a \right)} \right) |k\rangle. $$ The final expression represents a superposition over all basis states $|k\rangle$; however, we can verify that the amplitude of the state $|k\rangle=|a\rangle$ is simply one, as $a\oplus a =0$: $$ \left(\frac{1}{2^{n}}\sum^{2^n-1}_{j=0}1 \right) |a\rangle + \sum^{2^n-1}_{k\neq a} 0 |k\rangle = |a\rangle. \tag{1} $$ Therefore, the final state is the secret string. ## References \[1]: [Vazirani, Umesh, and E. Bernstein. Quantum complexity theory. Special issue on Quantum Computation of the Siam Journal of Computing 10 (1997)](https://epubs.siam.org/doi/10.1137/S0097539796300921) \[2]: [Bernstein-Vazirani (Wikipedia)](https://en.wikipedia.org/wiki/Bernstein%E2%80%93Vazirani_algorithm) \[3]: [Pokharel B., and Daniel A. L. Demonstration of algorithmic quantum speedup. Physical Review Letters 130, 210602 (2023)](https://arxiv.org/abs/2207.07647) # Deutsch-Jozsa Algorithm Source: https://docs.classiq.io/explore/algorithms/foundational/deutsch_jozsa/deutsch_jozsa Open this notebook in GitHub to run it yourself > **The Deutsch-Jozsa algorithm** [\[1\]](#original-paper), [\[2\]](#djwiki), named after David Deutsch and Richard Jozsa, is one of the first fundamental quantum algorithms showing exponential speedup over its classical counterpart$^*$. While it has no practical applicative use, it serves as a toy model for quantum computing, demonstrating how the concepts of superposition and interference enable quantum algorithms to outperform classical ones. > > The algorithm treats the following problem: > > * **Input:** A black box Boolean function $f(x)$ that acts on the integers in the range $[0, 2^{n}-1]$. > * **Promise:** The function is either constant or balanced (for half of the values it is 1 and for the other half it is 0). > * **Output:** Whether the function is constant or balanced. > > **Complexity:** The quantum approach requires a single query. If we require a deterministic answer to the problem, classically, we must inquire of the oracle $2^{n-1}+1$ times in the worst case. Therefore, the quantum algorithm exhibits a clear exponential speedup. However, without requiring deterministic determination - namely, allowing application of the classical probabilistic algorithm to get the result up to some error - then the exponential speedup is lost: taking $k$ classical evaluations of the function $f$ determines whether the function is constant or balanced, with a probability of $1 - 1/2^k$. > > $^*$ The exponential speedup is in the oracle complexity setting. It only refers to deterministic classical machines. > > *** > > **Keywords:** Foundational quantum algorithms, Phase oracle, Function evaluation, Oracle problem, Oracle/Query complexity. We define the Deutsch-Jozsa algorithm, which has a [quantum part](#the-quantum-part) and a [classical postprocess part](#the-classical-postprocess). Then, we run the algorithm on two different examples, one with a [simple](#example-simple-arithmetic-oracle) $f(x)$ and another that is [more complex](#example-complex-arithmetic-oracle). A [mathematical explanation](#technical-notes) of the algorithm is provided at the end of this notebook. Screenshot 2025-06-19 at 22.35.03.png ## How to Build the Algorithm with Classiq We define a `deutsch_jozsa` quantum function whose arguments are a quantum function for the black box $f(x)$, and a quantum variable on which it acts, $x$. The Deutsch-Jozsa algorithm is composed of three quantum blocks (see Figure 1): a Hadamard transform, an arithmetic oracle for the black box function, and another Hadamard transform. # ## The Quantum Part ```python theme={null} from classiq import * @qfunc def deutsch_jozsa(predicate: QCallable[QNum, QBit], x: QNum) -> None: within_apply( lambda: hadamard_transform(x), lambda: phase_oracle(predicate=lambda x, y: predicate(x, y), target=x), ) ``` # ## The Classical Postprocess The classical part of the algorithm reads: The probability of measuring the $|0\rangle_n$ state is 1 if the function is constant and 0 if it is balanced. We define a classical function that gets the execution results from running the quantum part and returns whether the function is constant or balanced: ```python theme={null} def post_process_deutsch_jozsa(parsed_results): if len(parsed_results) == 1: if 0 not in parsed_results: print("The function is balanced") else: print("The function is constant") else: print( "cannot decide as more than one output was measured, the distribution is:", parsed_results, ) ``` ## Example: Simple Arithmetic Oracle We start with a simple example on $n=4$ qubits, and $f(x)= x >7$. Classically, in the worst case, the function should be evaluated $2^{n-1}+1=9$ times. However, with the Deutsch-Jozsa algorithm, this function is evaluated only once. We build a predicate for this specific use case: ```python theme={null} @qperm def simple_predicate(x: Const[QNum], res: QBit) -> None: res ^= x > 7 ``` Next, we define a model by inserting the predicate into the `deutsch_jozsa` function: ```python theme={null} NUM_QUBITS = 4 @qfunc def main(x: Output[QNum[NUM_QUBITS]]): allocate(x) deutsch_jozsa(lambda x, y: simple_predicate(x, y), x) qprog_1 = synthesize(main) ``` Finally, we execute and call the classical postprocess: ```python theme={null} result_1 = execute(qprog_1).result_value() results_list_1 = [sample.state["x"] for sample in result_1.parsed_counts] post_process_deutsch_jozsa(results_list_1) ``` **Output:** ``` The function is balanced ``` ```python theme={null} show(qprog_1) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3G9AAyCGdMoNHKcC6gPWo0dhrov ``` ## Example: Complex Arithmetic Oracle *Generalizing to more complex scenarios makes no difference for modeling*. Let us take a complicated function, working with $n=3$: a function $f(x)$ that first takes the maximum between the input bitwise-xor with 4 and the input bitwise-and with 3, then checks whether the result is greater or equal to 4. Can you tell whether the function is balanced or constant? *This time we provide a width bound to the synthesis engine.* We follow the three steps as before: ```python theme={null} from classiq.qmod.symbolic import max NUM_QUBITS = 3 MAX_WIDTH = 11 @qperm def complex_predicate(x: Const[QNum], res: QBit) -> None: res ^= max(x ^ 4, x & 3) >= 4 @qfunc def main(x: Output[QNum[NUM_QUBITS]]): allocate(x) deutsch_jozsa(lambda x, y: complex_predicate(x, y), x) qprog_2 = synthesize( model=main, constraints=Constraints(max_width=MAX_WIDTH), ) result_2 = execute(qprog_2).result_value() results_list_2 = [sample.state["x"] for sample in result_2.parsed_counts] post_process_deutsch_jozsa(results_list_2) ``` **Output:** ``` The function is balanced ``` Screenshot 2025-06-19 at 22.38.57.png We can visualize the circuit obtained from the synthesis engine. Figure 2 presents the complex structure of the oracle, generated automatically by the synthesis engine. ```python theme={null} show(qprog_2) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3G9AD6mwVpLv35cEFT485NAvloR ``` ## Technical Notes A brief summary of the linear algebra behind the Deutsch-Jozsa algorithm. The first Hadamard transformation generates an equal superposition over all the standard basis elements: $$ |0\rangle_n \xrightarrow[H^{\otimes n}]{} \frac{1}{2^{n/2}}\sum^{2^n-1}_{j=0}|j\rangle_n. $$ The arithmetic oracle gets a Boolean function and adds an $e^{\pi i}=-1$ phase to all states for which the function returns true: $$ \frac{1}{2^{n/2}}\sum^{2^n-1}_{j=0}|j\rangle_n \xrightarrow[\text{Oracle}(f(j))]{}\frac{1}{2^{n/2}}\sum^{2^n-1}_{j=0}(-1)^{f(j)}|j\rangle_n. $$ Finally, applying the Hadamard transform, which can be written as $H^{\otimes n}\equiv \frac{1}{2^{n/2}}\sum^{2^n-1}_{k,l=0}(-1)^{k\cdot l} |k\rangle \langle l| $, gives $$ \frac{1}{2^{n/2}}\sum^{2^n-1}_{j=0}(-1)^{f(j)}|j\rangle \xrightarrow[H^{\otimes n}]{} \sum^{2^n-1}_{k=0} \left(\frac{1}{2^{n}}\sum^{2^n-1}_{j=0}(-1)^{f(j)+j\cdot k} \right) |k\rangle. $$ The probability of getting the state $|k\rangle = |0\rangle$ is $$ P(0)=\left|\frac{1}{2^{n}}\sum^{2^n-1}_{j=0}(-1)^{f(j)} \right|^2 = \left\{ \begin{array}{l l} 1 & \text{if } f(x) \text{ is constant} \\ 0 & \text{if } f(x) \text{ is balanced.} \end{array} \right. $$ ## References \[1]: [David Deutsch & Richard Jozsa. Rapid solutions of problems by quantum computation. Proceedings of the Royal Society of London A. 439 (1907): 553-558. (1992).](https://royalsocietypublishing.org/doi/10.1098/rspa.1992.0167) \[2]: [Deutsch Jozsa (Wikipedia)](https://en.wikipedia.org/wiki/Deutsch%E2%80%93Jozsa_algorithm) # Quantum Teleportation Algorithm (Protocol) Source: https://docs.classiq.io/explore/algorithms/foundational/quantum_teleportation/quantum_teleportation Open this notebook in GitHub to run it yourself Quantum Teleportation Protocol, first proposed by Bennett et al. in 1993 [\[1\]](#ref-bbc93), is a foundational quantum communication method that enables the transfer of an arbitrary qubit state from one location (Alice) to another (Bob) using a combination of quantum entanglement and classical communication. It does not involve physically moving the qubit but rather transmitting its quantum information through shared entanglement. Input: An arbitrary single-qubit state $\lvert \psi \rangle = \alpha \lvert 0 \rangle + \beta \lvert 1 \rangle$ to be teleported. Resources: One shared Bell pair between Alice and Bob, and two bits of classical communication. Output: Bob's qubit reproduces the original state $\lvert \psi \rangle$ that was initially held by Alice. Process: The protocol begins with the creation of an entangled Bell pair shared between Alice and Bob. Alice entangles her qubit to be sent with her half of the Bell pair and performs mid-circuit measurements on her qubits. The resulting classical bits are sent to Bob, who applies classically controlled corrections to reconstruct the original quantum state on his qubit. Keywords: Quantum communication, Entanglement, Mid-circuit measurement, Hybrid quantum-classical systems, Information transfer. ## Algorithm Description At the start Alice holds two qubits, one that she wishes to teleport (**`alice_qubit`**) and one that will be used during the protocol (**`bell_pair_qubit`**). $$ \lvert a \rangle = \alpha \lvert 0 \rangle + \beta \lvert 1 \rangle $$ $$ \lvert p \rangle =\lvert 0 \rangle $$ Bob holds one qubit (**`bob_qubit`**). $$ \lvert b \rangle =\lvert 0 \rangle $$ # ## 1. Bell State Creation * Apply a **Hadamard (`H`)** gate to **`bell_pair_qubit`**. * Apply a **CNOT** gate with **`bell_pair_qubit`** as control and **`bob_qubit`** as target.\ These two qubits now form a Bell state. $$ \lvert \psi \rangle_{pb} = \tfrac{1}{\sqrt{2}}(\lvert 00 \rangle_{pb} + \lvert 11 \rangle_{pb}) $$ # ## 2. Entangling Alice's Qubit with the Bell Pair * Apply a **CNOT** gate from **`alice_qubit`** to **`bell_pair_qubit`**. * Apply a **Hadamard (`H`)** gate on **`alice_qubit`**.\ This step entangles Alice's qubit with the Bell pair. $$ \begin{split} \lvert \psi \rangle_{apb} = \tfrac{1}{2} \big[ &\lvert 00 \rangle_{ap} \otimes (\alpha \lvert 0 \rangle_{b} + \beta \lvert 1 \rangle_{b}) \\ &+ \lvert 01 \rangle_{ap} \otimes (\alpha \lvert 1 \rangle_{b} + \beta \lvert 0 \rangle_{b}) \\ &+ \lvert 10 \rangle_{ap} \otimes (\alpha \lvert 0 \rangle_{b} - \beta \lvert 1 \rangle_{b}) \\ &+ \lvert 11 \rangle_{ap} \otimes (\alpha \lvert 1 \rangle_{b} - \beta \lvert 0 \rangle_{b}) \big] \end{split} $$ # ## 3. Mid-Circuit Measurement Measure **`alice_qubit`** and **`bell_pair_qubit`**.\ The measurement results will be later used to apply conditional corrections. # ## 4. Classically Controlled Corrections After measuring **`alice_qubit`** and **`bell_pair_qubit`**, the system collapses to one of the four possible states shown above. Each collapsed state differs from Alice's original state by a known unitary transformation. To recover the exact state that Alice initially held, appropriate correction operations must be applied to bob\_qubit based on the measurement results. Depending on the measurement outcomes: * If **`alice_qubit`** measures `0` and **`bell_pair_qubit`** measures `0` - do nothing. * If **`alice_qubit`** measures `0` and **`bell_pair_qubit`** measures `1`, apply **`X`** on **`bob_qubit`**. * If **`alice_qubit`** measures `1` and **`bell_pair_qubit`** measures `0`, apply **`Z`** on **`bob_qubit`**. * If **`alice_qubit`** measures `1` and **`bell_pair_qubit`** measures `1`, apply **`X`** on **`bob_qubit`** and then **`Z`** on **`bob_qubit`**. $$ \begin{array}{ccl} 00 &\rightarrow& I(b) \\ 01 &\rightarrow& X(b) \\ 10 &\rightarrow& Z(b) \\ 11 &\rightarrow& Z(b)X(b) \end{array} $$ These classically controlled gates complete the teleportation process, reconstructing Alice's original quantum state on **`bob_qubit`**. $$ \lvert b \rangle = \alpha \lvert 0 \rangle + \beta \lvert 1 \rangle $$ A complete derivation is provided in the Technical Notes section at the end of this notebook. image.png ## Mid-Circuit Measurement Mid-circuit measurement is a key feature in quantum computation that allows measuring a qubit **while the circuit is still running**, rather than only at the end of execution.\ This capability enables **classical feedback** - where measurement results influence subsequent quantum operations within the same program. In Qmod, mid-circuit measurement is implemented through the `measure()` function.\ Unlike a final measurement, which collapses the qubit state at the end of a circuit, `measure()` can be invoked **within** a quantum function to extract partial information and conditionally control later operations. The `measure()` function returns a Qmod classical variable (symbolic), representing the outcome of the measurement (either `0` or `1`).\ This symbolic value can be used to define conditional logic at circuit construction time, and is determined only during the actual quantum run of the program. ## How to Build the Algorithm with Classiq We define a qunatum function `quantum_teleportation` that takes three qubits: one for Alice (`alice_qubit`), one for Bob (`bob_qubit`), and one for their shared Bell pair (`bell_pair_qubit`). # ## Classiq's Implematation ```python theme={null} from classiq import * @qfunc def quantum_teleportation( alice_qubit: QBit, bob_qubit: QBit, bell_pair_qubit: QBit ) -> None: # Step 1: Create Bell pair between bell_pair_qubit and Bob's qubit H(bell_pair_qubit) CX(bell_pair_qubit, bob_qubit) # Step 2: Alice performs Bell measurement on her qubits CX(alice_qubit, bell_pair_qubit) H(alice_qubit) # Step 3: Measure Alice's qubits to get classical bits alice_bit1 = measure(alice_qubit) alice_bit2 = measure(bell_pair_qubit) # Step 4: Apply corrections to Bob's qubit based on Alice's measurements if_(alice_bit2, lambda: X(bob_qubit)) # Apply X if bell_pair_qubit measurement is 1 if_(alice_bit1, lambda: Z(bob_qubit)) # Apply Z if alice_qubit measurement is 1 ``` **Alice state preperation:** This function defines Alice's input state for teleportation. It can be replaced with any other state-preparation routine as needed. We chose a simple superposition. ```python theme={null} @qfunc def prepare_test_state(q: QBit) -> None: H(q) ``` **Main function:** allocates the necessary qubits, prepares Alice's qubit state, and then calls the quantum teleportation function to perform the protocol. ```python theme={null} @qfunc def main(bob_qubit: Output[QBit]) -> None: alice_qubit = QBit() bell_pair_qubit = QBit() # Allocate qubits allocate(1, alice_qubit) allocate(1, bell_pair_qubit) allocate(1, bob_qubit) # Prepare the state to be teleported on Alice's qubit prepare_test_state(alice_qubit) # Perform teleportation quantum_teleportation(alice_qubit, bob_qubit, bell_pair_qubit) # Drop qubits to avoid uncomputation drop(alice_qubit) drop(bell_pair_qubit) ``` **Create model and synthesize:** We create a quantum model using the defined main function, synthesize it into a circuit, and visualize it. ```python theme={null} qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/34v9mcR9fWGjnK4pGsgLjDyMltJ ``` **Execute** the synthesized quantum program to observe Bob's qubit after teleportation. ```python theme={null} job = execute(qprog) job.get_sample_result().dataframe ``` | | bob\_qubit | count | probability | bitstring | | - | ---------- | ----- | ----------- | --------- | | 0 | 0 | 1065 | 0.52002 | 0 | | 1 | 1 | 983 | 0.47998 | 1 | The measured probabilities of Bob's qubit closely match those of Alice's initial state, indicating successful quantum teleportation of the superposition. ## Technical Notes Start with Alice's qubit in the state: $$ \lvert a \rangle = \alpha \lvert 0 \rangle + \beta \lvert 1 \rangle $$ Bob's and the Bell-pair qubits start in the state: $$ \lvert b \rangle = \lvert p \rangle=\lvert 0 \rangle $$ After creating a bell state between $\lvert p \rangle$ and $\lvert b \rangle$ $$ \lvert \psi \rangle_{pb} = \tfrac{1}{\sqrt{2}}(\lvert 00 \rangle_{pb} + \lvert 11 \rangle_{pb}) $$ The total state is now: $$ \lvert \psi \rangle_{apb} = (\alpha \lvert 0 \rangle_{a} + \beta \lvert 1 \rangle_{a} )\otimes(\tfrac{1}{\sqrt{2}}(\lvert 00 \rangle_{pb} + \lvert 11 \rangle_{pb}))=\tfrac{\alpha}{\sqrt{2}}\lvert 000 \rangle_{apb}+\tfrac{\alpha}{\sqrt{2}}\lvert 011 \rangle_{apb}+\tfrac{\beta}{\sqrt{2}}\lvert 100 \rangle_{apb}+\tfrac{\beta}{\sqrt{2}}\lvert 111 \rangle_{apb} $$ After applying CX(a,p): $$ \lvert \psi \rangle_{apb} = \tfrac{\alpha}{\sqrt{2}}\lvert 000 \rangle_{apb}+\tfrac{\alpha}{\sqrt{2}}\lvert 011 \rangle_{apb}+\tfrac{\beta}{\sqrt{2}}\lvert 110 \rangle_{apb}+\tfrac{\beta}{\sqrt{2}}\lvert 101 \rangle_{apb} $$ Applying H(a): $$ \lvert \psi \rangle_{apb} = \tfrac{\alpha}{2}\lvert 000 \rangle_{apb}+\tfrac{\alpha}{2}\lvert 100 \rangle_{apb}+\tfrac{\alpha}{2}\lvert 011 \rangle_{apb}+\tfrac{\alpha}{2}\lvert 111 \rangle_{apb}+\tfrac{\beta}{2}\lvert 010 \rangle_{apb}-\tfrac{\beta}{2}\lvert 110 \rangle_{apb}+\tfrac{\beta}{2}\lvert 001 \rangle_{apb}-\tfrac{\beta}{2}\lvert 101 \rangle_{apb} $$ Simplify: $$ \begin{split} \lvert \psi \rangle_{apb} = \tfrac{1}{2} \big[ &\lvert 00 \rangle_{ap} \otimes (\alpha \lvert 0 \rangle_{b} + \beta \lvert 1 \rangle_{b}) \\ &+ \lvert 01 \rangle_{ap} \otimes (\alpha \lvert 1 \rangle_{b} + \beta \lvert 0 \rangle_{b}) \\ &+ \lvert 10 \rangle_{ap} \otimes (\alpha \lvert 0 \rangle_{b} - \beta \lvert 1 \rangle_{b}) \\ &+ \lvert 11 \rangle_{ap} \otimes (\alpha \lvert 1 \rangle_{b} - \beta \lvert 0 \rangle_{b}) \big] \end{split} $$ The classical correction operations based on Alice's and the Bell-pair measurement results are: $$ \begin{array}{ccl} 00 &\rightarrow& I(b) \\ 01 &\rightarrow& X(b) \\ 10 &\rightarrow& Z(b) \\ 11 &\rightarrow& Z(b)X(b) \end{array} $$ ## References \[1]: C. H. Bennett, G. Brassard, C. Crépeau, R. Jozsa, A. Peres, and W. K. Wootters, "Teleporting an unknown quantum state via dual classical and Einstein-Podolsky-Rosen channels," Physical Review Letters, 70, 1895 (1993). DOI:10.1103/PhysRevLett.70.1895 \[2]: [Qunatum Teleportation (Wikipedia)](https://en.wikipedia.org/wiki/Quantum_teleportation) # Simon's Algorithm Source: https://docs.classiq.io/explore/algorithms/foundational/simon/simon Open this notebook in GitHub to run it yourself > **Simon's algorithm** [\[1\]](#original-paper), [\[2\]](#simonswiki) is a basic quantum algorithm that demonstrates an exponential speed-up$^*$ over its classical counterpart in the oracle complexity setting. The algorithm solves the so-called Simon's problem: > > The algorithm treats the following problem: > > * **Input:** A function $f: [0,1]^n \rightarrow [0,1]^n$. > * **Promise:** There is a secret binary string $s$ such that $f(x) = f(y) \iff y = x\oplus s\tag{1}~~,$ where $\oplus$ is a bitwise xor operation. > * **Output:** The secret string $s$, using a minimal number of queries of $f$. > > **Complexity:** The Simon's problem is hard to solve with classical deterministic or probabalistic approaches. This can be understood as follows: determining $s$ requires finding a collision $f(x)=f(y)$, as $s = x\oplus y$. What is the minimum number of calls for measuring a collision? If we take the deterministic approach, in the worst case we need $2^{n-1}$ calls. A probablistic approach, in the spirit of the one that solves the birthday problem \[3], has slightly better scaling of $O(2^{n/2})$ queries. The quantum approach requires $O(n)$ queries, thus introducing an exponential speedup. > > $^*$ The exponential speedup is in the oracle complexity setting. It only refers to deterministic classical machines. > > *** > > **Keywords:** Foundational quantum algorithm, Exponential speedup, Function evaluation, Oracle problem, Oracle/Query complexity. Screenshot 2025-06-19 at 22.54.03.png In the following notebook we define the Simon's algorithm, which has a [quantum part](#the-quantum-part) and a [classical postprocess part](#the-classical-postprocess). Then, we run the algorithm on two different examples of a Simon's function: one that can be defined with [simple arithmetic](#example-arithmetic-simons-function) and another that has a [shallow implementation](#example-shallow-simons-function). A [mathematical explanation](#technical-notes) of the algorithm is provided at the end of this notebook. Note that the function $f$ is $2$-to-$1$ if $s\neq 0^n$, and $1$-to-$1$ otherwise. Hereafter, we refer to a function that satisfies the condition in Eq. (1) as a "Simon's function". ## Building the Algorithm with Classiq # ## Quantum Part The quantum part of the algorithm is rather simple, calling the quantum implementation of $f(x)$, between two calls of the hadamard transform. The call of $f$ is done out-of-place, onto a quantum variable $res$, whereas only the final state of $x$ is relevant to the classical postprocess to follow. ```python theme={null} from classiq import * @qfunc def simon_qfunc(f_qfunc: QCallable[QNum, Output[QNum]], x: QNum, res: Output[QNum]): within_apply(lambda: hadamard_transform(x), lambda: f_qfunc(x, res)) ``` # ## Classical Postprocess The classical part of the algorithm includes the following postprocessing steps: 1. Finding $n-1$ samples of $x$ that are linearly independent, $\{y_k\}^{n-1}_{1}$. It is guaranteed that this can be achieved with high probability (see the [technical details](#the-classical-part) below). 2. Finding the string $s$ such that $s \cdot y_k=0 \,\,\, \forall k$, where $\cdot$ refers to a dot-product $\text{mod}~ 2$ (polynomial complexity in $n$). For these steps we use the *Galois* package, which extends *NumPy* to finite field operations. ```python theme={null} # !pip install galois ``` ```python theme={null} import galois import numpy as np # here we work over Boolean arithmetics - F(2) GF = galois.GF(2) ``` We define two classical functions for the first step: ```python theme={null} # The following function checks whether a set contains linearly independent vectors def is_independent_set(vectors): matrix = GF(vectors) rank = np.linalg.matrix_rank(matrix) if rank == len(vectors): return True else: return False def get_independent_set(samples): """ The following function gets samples of n-sized strings from running the quantum part and returns an n-1 x n matrix, whose rows form a set if independent. """ ind_v = [] for v in samples: if is_independent_set(ind_v + [v]): ind_v.append(v) if len(ind_v) == len(v) - 1: # reached max set of N-1 break return ind_v ``` For the second step we need to solve a linear set of equations. We have $n-1$ equations on a binary vector of size $n$. It has two solutions, one of which is the trivial solution $0^n$, while the other gives us the secret string $s$. The *Galois* package handles this task as follows: ```python theme={null} def get_secret_integer(matrix): """ Finds a binary vector that “solves” the equation Ax=0 (mod 2) — and then turns that vector into a single integer, which serves as the “secret”. """ gf_v = GF(matrix) # converting to a matrix over Z_2 null_space = gf_v.T.left_null_space() # finding the right-null space of the matrix return int( "".join(np.array(null_space)[0][::-1].astype(str)), 2 ) # converting from binary to integer ``` \-- * Next, we provide two different examples of Simon's function and run the Simon's algorithm to find their secret string. *** ## Example: Arithmetic Simon's Function An example of a valid $f(x)$ function that satisfies the condition in Eq. ([1](#mjx-eqn-1)): $$ f(x) = \min(x, x\oplus s). $$ Clearly, we have that $f(x\oplus s) = \min(x\oplus s, (x\oplus s)\oplus s)=\min(x\oplus s, x)=f(x)$. # ## Implementing the Simon's Function We define the function, as well as a model that applies it on all computational basis states to illustrate that it is a two-to-one function. ```python theme={null} from classiq.qmod.symbolic import min @qperm def simon_qfunc_simple(s: CInt, x: Const[QNum], res: Output[QNum]): res |= min(x, x ^ s) ``` Let us run it with $n=5$ and $s={'}00110{'} (\equiv 6)$, starting with a uniform distribution of $|x\rangle$ over all possible states: ```python theme={null} NUM_QUBITS = 5 S_SECRET = 6 @qfunc def main(x: Output[QNum[NUM_QUBITS]], res: Output[QNum]): allocate(x) hadamard_transform(x) simon_qfunc_simple(S_SECRET, x, res) qmod_1 = create_model(main) # synthesize qprog_1 = synthesize( qmod_1, constraints=Constraints(optimization_parameter=OptimizationParameter.WIDTH) ) # vizualize show(qprog_1) # execute df_1 = sample(qprog_1) ``` **Output:** ``` Submitting job to simulator ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3HrVycwdtuyNFzvnxaSAWswVaKZ https://platform.classiq.io/circuit/3HrVycwdtuyNFzvnxaSAWswVaKZ?login=True&version=20 ``` **Output:** ``` Job: https://platform.classiq.io/jobs/e0f01ef6-00e5-4c1e-bf50-73593b9f6c3c ``` By plotting the results we can see that this is a two-to-one function: ```python theme={null} import matplotlib.pyplot as plt my_result = {row["x"]: row["res"] for _, row in df_1.iterrows()} fig, ax = plt.subplots() ax.plot(my_result.keys(), my_result.values(), "o") ax.grid(axis="y", which="minor") ax.grid(axis="y", which="major") ax.grid(axis="x", which="minor") ax.grid(axis="x", which="major") plt.xlabel("$x$", fontsize=16) plt.ylabel("$f(x)$", fontsize=16) plt.yticks(fontsize=16) plt.xticks(fontsize=16) ax.minorticks_on() ``` output # ## Running the Simon's Algorithm Taking $n$ number of shots guarantees getting a set of $n-1$ independent strings with high probability (assuming a noiseless quantum computer) (see [technical explanation](#the-quantum-part) below). Moreover, increasing the number of shots by a constant factor provides an exponential improvement. Here we take $50*n$ shots. ```python theme={null} @qfunc def main(x: Output[QNum[NUM_QUBITS]], res: Output[QNum]): allocate(x) simon_qfunc(lambda x, res: simon_qfunc_simple(S_SECRET, x, res), x, res) qmod_2 = create_model( main, constraints=Constraints(optimization_parameter=OptimizationParameter.WIDTH), out_file="simon", ) ``` We synthesize and execute to obtain the results: ```python theme={null} NUM_SHOTS = 50 * NUM_QUBITS qprog_2 = synthesize(qmod_2) df_2 = sample(qprog_2, num_shots=NUM_SHOTS) bitstrings = [f"{x:0{NUM_QUBITS}b}" for x in df_2["x"].tolist()] reversed_bitstrings = [b[::-1] for b in bitstrings] samples = [[int(bit) for bit in b] for b in reversed_bitstrings] ``` **Output:** ``` Submitting job to simulator ``` **Output:** ``` Job: https://platform.classiq.io/jobs/37df35b7-bf07-4f7a-9291-db14abcc8175 ``` ```python theme={null} matrix_of_ind_v = get_independent_set(samples) assert ( len(matrix_of_ind_v) == NUM_QUBITS - 1 ), "Failed to find an independent set, try to increase the number of shots" quantum_secret_integer = get_secret_integer(matrix_of_ind_v) ``` ```python theme={null} print("The secret binary string (integer) of f(x):", S_SECRET) print("The result of the Simon's Algorithm:", quantum_secret_integer) assert ( S_SECRET == quantum_secret_integer ), "The Simon's algorithm failed to find the secret key." ``` **Output:** ``` The secret binary string (integer) of f(x): 6 The result of the Simon's Algorithm: 6 ``` ## Example: Shallow Simon's Function In the second example we take a Simon's function that was presented in a recent paper \[[4](#simonspaper2024)]: Take a secret string of the form $s=0^{n-l}1^l = \underbrace{00\dots0}_{n-l}\underbrace{1\dots111}_{l}~,$ and define the 2-to-1 function: $$ f_{s}(|x\rangle_n) = \underbrace{|x_0\rangle |x_1\rangle \dots |x_{n-l-1}\rangle}_{n-l} |0\rangle \underbrace{|x_{n-l+1}\oplus x_{n-l}\rangle \dots |x_{n-1}\oplus x_{n-l}\rangle}_{l-1}. $$ The function $f$ operates as follows: for the first $n-l$ elements, we simply "copy" the data, whereas for the last $l$ elements we apply a xor with the $n-l$ element. A simple proof that this is indeed a 2-to-1 function is given in Ref. \[[4](#simonspaper2024)]. *Comment:* Ref. \[[4](#simonspaper2024)] employed further reduction of the function implementation (reducing the $n$-sized Simon's problem to an $(n-l)$-sized problem), added a classical postprocess of randomly permutating over the result of $f(x)$ to increase the hardness of the problem, and also included some NISQ analysis. These steps were taken to show an algorithmic speedup on real quantum hardware. # ## Implementing the Simon's Function The first $n-l$ "classical copies", $|x_k,0\rangle\rightarrow |x_k x_k\rangle$, can be implemented by $CX$ gates. The xor operations, $|x_k,0\rangle\rightarrow |x_k, x_k \oplus x_{n-l}\rangle$, can be implemented by two $CX$ operations, one to form a "classical copy" of $x_k$, followed by another $CX$ operation, implementing a xor with $x_{n-l}$. ```python theme={null} @qperm def simon_qfunc_with_bipartite_s( partition_index: CInt, x: Const[QArray], res: Output[QArray] ): allocate(x.len, res) repeat(x.len - partition_index, lambda i: CX(x[i], res[i])) repeat( partition_index - 1, lambda i: ( CX( x[x.len - partition_index + 1 + i], res[x.len - partition_index + 1 + i], ), CX(x[x.len - partition_index], res[x.len - partition_index + 1 + i]), ), ) ``` Here we take a specific example and plot $f(x)$ for all possible $x$ values: ```python theme={null} NUM_QUBITS = 6 PARTITION_INDEX = 4 @qfunc def main(x: Output[QNum[NUM_QUBITS]], res: Output[QNum]): allocate(x) hadamard_transform(x) simon_qfunc_with_bipartite_s(PARTITION_INDEX, x, res) # create model qmod_3 = create_model(main) # synthesize qprog_3 = synthesize( qmod_3, constraints=Constraints(optimization_parameter=OptimizationParameter.DEPTH) ) # vizualize show(qprog_3) # execute df_3 = sample(qprog_3) # plot the f(x) my_result = {row["x"]: row["res"] for _, row in df_3.iterrows()} fig, ax = plt.subplots() ax.plot(my_result.keys(), my_result.values(), "o") ax.grid(axis="y", which="minor") ax.grid(axis="y", which="major") ax.grid(axis="x", which="minor") ax.grid(axis="x", which="major") plt.xlabel("$x$", fontsize=16) plt.ylabel("$f(x)$", fontsize=16) plt.yticks(fontsize=16) plt.xticks(fontsize=16) ax.minorticks_on() ``` **Output:** ``` Submitting job to simulator ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3HrW2uxzodLtmtyx4KGvAMdtBzK https://platform.classiq.io/circuit/3HrW2uxzodLtmtyx4KGvAMdtBzK?login=True&version=20 ``` **Output:** ``` Job: https://platform.classiq.io/jobs/914d5beb-925d-414d-969a-cbf12c178ffd ``` output # ## Running the Simon's Algorithm As in the first example, we take $50*n$ shots. ```python theme={null} @qfunc def main(x: Output[QNum[NUM_QUBITS]], res: Output[QNum]): allocate(x) simon_qfunc( lambda x, res: simon_qfunc_with_bipartite_s(PARTITION_INDEX, x, res), x, res ) qmod_4 = create_model(main) # qmod_4 = update_execution_preferences(qmod_4, num_shots=50 * NUM_QUBITS) ``` We synthesize and execute to obtain the results: ```python theme={null} NUM_SHOTS = 50 * NUM_QUBITS qprog_4 = synthesize(qmod_4) show(qprog_4) df_4 = sample(qprog_4, num_shots=NUM_SHOTS) bitstrings = [f"{x:0{NUM_QUBITS}b}" for x in df_4["x"].tolist()] reversed_bitstrings = [b[::-1] for b in bitstrings] samples = [[int(bit) for bit in b] for b in reversed_bitstrings] ``` **Output:** ``` Submitting job to simulator ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3HrW4U8ymxb4NOIJN73CS22AQyT https://platform.classiq.io/circuit/3HrW4U8ymxb4NOIJN73CS22AQyT?login=True&version=20 ``` **Output:** ``` Job: https://platform.classiq.io/jobs/ff024858-adf5-4e84-8d8b-e3cb42e70990 ``` ```python theme={null} matrix_of_ind_v = get_independent_set(samples) assert ( len(matrix_of_ind_v) == NUM_QUBITS - 1 ), "Failed to find an independent set, try to increase the number of shots" quantum_secret_integer = get_secret_integer(matrix_of_ind_v) ``` ```python theme={null} s_secret = int("1" * PARTITION_INDEX + "0" * (NUM_QUBITS - PARTITION_INDEX), 2) print("The secret binary string (integer) of f(x):", s_secret) print("The result of the Simon's Algorithm:", quantum_secret_integer) assert ( s_secret == quantum_secret_integer ), "The Simon's algorithm failed to find the secret key." ``` **Output:** ``` The secret binary string (integer) of f(x): 60 The result of the Simon's Algorithm: 60 ``` ## Technical Notes This section provides some technical details about the quantum and classical parts of the Simon's algorithm. # ## Quantum Part Following the three blocks of the algorithm: $$ |0\rangle_n |0\rangle_n \xrightarrow[H^{\otimes n}]{} \frac{1}{2^{n/2}}\sum^{2^n-1}_{x=0}|x\rangle_n |0\rangle_n \xrightarrow[\text{Oracle(f(x))}]{} \frac{1}{2^{n/2}}\sum^{2^n-1}_{x=0}|x\rangle_n |f(x)\rangle_n \xrightarrow[H^{\otimes n}]{} \frac{1}{2^{n}} \sum^{2^n-1}_{y=0} |y\rangle_n \left( \sum^{2^n-1}_{x=0}(-1)^{x\cdot y} |f(x)\rangle_n \right)~~. $$ We next measure the first register $|y\rangle$. First, we treat the case where $s\neq 0$ that $f(x)$ is a 2-to-1 function. The claim is that for any measured state $y$, we must have that $y\cdot s=0$. To see this, calculate the probability of measuring some state $|y\rangle$: $$ P(|y\rangle) \propto \left| \sum^{2^n-1}_{x=0}(-1)^{x\cdot y} |f(x)\rangle_n \right|^2. $$ Now, change the sum to run over the image of $f(x)$ instead of over all $x\in [0,2^{n}-1]$. Since for any $f(x)$ there are two sources in the domain, $x$ and $x\oplus s$, we can write $$ P(|y\rangle) \propto \left| \sum_{f(x) \in imag(f)} \left[ (-1)^{x\cdot y} + (-1)^{(x\oplus s )\cdot y} \right]|f(x)\rangle_n \right|^2. $$ Since $$ (-1)^{x\cdot y} + (-1)^{(x\oplus s )\cdot y} = (-1)^{x\cdot y}( 1+ (-1)^{s\cdot y}) $$ vanishes when $s\cdot y \neq 0 ~(\text{mod}~ 2)$, for any measured $y$ we have $y\cdot s = 0$. Therefore, every measurement $y$ gives a random vector orthogonal to $s$. Repeating $O(n)$ measurements collects $n-1$ linear modular equations which span the orthogonal space to $s$. # ## Classical Part We have a set of possible $y$ values that can be measured, each with a probability of $1/M$, where $M$ is the set size. If $s=0^n$, we have $M=2^n$, whereas for $s\neq 0^n$ the set size is $M=2^{n-1}$. The probability of measuring a set of $n-1$ linearly independent binary strings $y$ can be calculated as follows (see also the birthday problem \[[2](#bdwiki)]): For the first string, $y_0$, we just require that we do not pick $y=0^n$, so $P(y_0)=1-1/M$. Then, for the next string, we require that it is not in $\left\{a_0 y_0\,\,\,| a_0=0,1\right\}$, thus $P(y_1)=(1-2/M)$. The following string must not be picked out of $\left\{a_0 y_0+a_1y_1\,\,\,| a_0, a_1=0,1\right\}$, thus $P(y_2)=(1-2^2/M)$. We can continue with this procedure up to $y_{n-2}$ (in total $n-1$ vectors) to get $$ P_{\rm independent} = \left\{\begin{array}{l l} \Pi^{n-2}_{k=0} \left(1-2^k/2^{n}\right) & \text{, if } f(x) \text{ is 1-to-1},\\ \Pi^{n-2}_{k=0} \left(1-2^k/2^{n-1}\right) & \text{, if } f(x) \text{ is 2-to-1} \end{array} \,\,\,\,\, \geq \Pi^{\infty}_{k=1}\left(1-\frac{1}{2^k}\right) \approx 0.2887 \geq 1/4. \right. $$ If we repeat the experiment, the probability of measuring an independent set improves exponentially. Solving the linear system recovers $s$. ## References \[1] [ Simon, D. R. (1997). On the power of quantum computation. SIAM journal on computing, 26(5), 1474-1483.](https://epubs.siam.org/doi/abs/10.1137/S0097539796298637) \[2] [ Simon's Algorithm (Wikipedia).](https://en.wikipedia.org/wiki/Simon%27s_problem) \[3] [Birthday problem. ](https://en.wikipedia.org/wiki/Birthday_problem) \[4] [Singkanipa P. et al. Demonstration of Algorithmic Quantum Speedup for an Abelian Hidden Subgroup Problem. ](https://arxiv.org/abs/2401.07934) # Hamiltonian Simulation Source: https://docs.classiq.io/explore/algorithms/hamiltonian_simulation/hamiltonian_simulation_guide/hamiltonian_simulation_guide Open this notebook in GitHub to run it yourself Quantum Hamiltonian simulation is one of the most important problems in quantum computing. It consists of modeling the time evolution of a complex quantum system using more controllable devices; the quantum computers. Simulations of this type are essential to comprehend quantum systems that are described by complex dynamics, such as molecules in chemistry or materials in condensed matter physics, where classical computers fail due to the exponential scale up required by larger quantum systems. In this guide, you will be using Classiq to work with simple problems of Hamiltonian simulation using two different methods: * **Suzuki-Trotter decomposition** * **qDRIFT** Apart from these methods, known as **product formulas**, this guide also briefly discusses block-encoding methods and links to a specific guide on this topic. ## Table of Contents 1. [Intoduction](#intoduction) 2. [Suzuki-Trotter decomposition](#suzuki-trotter-decomposition) * [Simple Exponentiation of Suzuki Trotter formula of order 1 with 1 repetition](#exponentiate) 3. [qDRIFT](#qdrift) 4. [Hamiltonian simulation with block encoding](#hamiltonian-simulation-with-block-encoding) 5. [Measuring the expected magnetization as a function of time](#measuring-the-expected-magnetization-as-a-function-of-time) 6. [Summary and Exercises](#summary-and-exercises) ## Introduction Quantum Hamiltonian simulation has several significant applications. Some of them are directly related to physics and chemistry, while others extend to fields such as optimization and machine learning: * Quantum Chemistry: It helps predict the properties of molecules using quantum methods such as the Quantum Variational Eigensolver (VQE), which can be more efficient than classical methods in certain conditions. * Materials Science: It aids in designing new materials with desired properties by simulating their quantum mechanical behavior. * Optimization and Machine Learning: The use of quantum Hamiltonian simulation algorithms in optimization and machine learning is an active area of research. These algorithms are often incorporated as subroutines within broader quantum algorithms, combined with other quantum techniques tailored to optimization and machine learning tasks. Within the framework of **product formulas**, we can mathematically approach the Hamiltonian Simulation problem as follows: Suppose we have a quantum computer that can run the evolution operators of the set of Hamiltonians $\{H_j\}$ natively. Now, suppose you want to understand the dynamics of the following, more complex, Hamiltonian: If this quantum computer can only execute the evolution operators of the set of Hamiltonians $\{H_j\}$, then you could only have operations in the form: $$ O = \prod_{j} e^{-i \,\tau_j H_j}. $$ $$ H_{full} = \sum_{j} h_j H_j, $$ For some real values $h_j$. Therefore, a decomposition of the operator into the set of operations is needed, formed by $\{e^{-itH_j}\}$, where $e^{-itH_j}$ is the evolution operator of the Hamiltonian $H$. ## Suzuki-Trotter Decomposition Now that the problem is stated, a possible solution is the Suzuki-Trotter decomposition, also known as Trotterization. Simply put, this method involves 'breaking' the evolution operator into the evolution operators of its components. In the **first order**, it looks like this: $$ e^{-itH}=\exp\left\{-it\sum_{j=1}^N h_j H_j\right\} \approx \left(\prod_{j}^N e^{-it h_j H_j/r}\right)^r + \mathcal{O}(t^2/r). $$ Details about this decomposition are in [\[1\]](#ts-paper). This formula is important and has applications in several scenarios. Additionally, if you need an error that scales better than $t^2$, it is possible to achieve higher-order Suzuki-Trotter formulas. For example, the **second-order** Suzuki-Trotter would look like this: $$ e^{-itH}=\exp\left\{-i\left(\sum_{j=1}^N \frac{t_j}{2}\,H_j + \sum_{j=N}^1 \frac{t_j}{2}\,H_j\right)\right\} \approx \left(\prod_{j=1}^N e^{-it_j\, H_j/(2r)}\prod_{j=N}^1 e^{-it_j\, H_j/(2r)}\right)^r + \mathcal{O}(t^3/r). $$ Higher orders for the Suzuki-Trotter formula are in \[[1](#ts-paper)]. However, as the number of terms in the Hamiltonian and the approximation order increases, it gets more complicated to construct the Suzuki-Trotter formulas. To this end, use the Classiq `suzuki_trotter()` function to execute an operator's Trotter decomposition, specifying the order and number of repetitions. The function's inputs are: * `pauli_operator`: `SparsePauliOp`: The Pauli operator to be exponentiated, representing the term $H_j$ and its respective coefficients $h_j$; * `evolution_coefficient`: `CArray`: A global evolution coefficient multiplying the Pauli operator, representing the value $t$ for the coefficient in the time evolution operator; * `order`: `CInt`: The order of the Suzuki-Trotter decomposition; * `repetitions`: `CInt`: The number of repetitions of the Suzuki-Trotter decomposition; * `qbv`: `QArray`: The target quantum variable of the exponentiation, representing the qubits on which the operation will be applied. The following example demonstrates how to use it: Now that the inputs for the Suzuki-Trotter method are defined, apply it to the example of approximating the dynamics of the following two-qubits Hamiltonian: $$ H = 0.3 \, Z\otimes Z + 0.7 \,X\otimes I + 0.2\, I\otimes X, $$ which represents two spins interacting in a transversal field. Identify the operators $\{H_j\}$ and their respective coefficients $\{h_j\}$: $$ \begin{split} H_1 = Z \otimes Z,\text{ coefficient: }h_1 = 0.3; \\ H_2 = X \otimes I,\text{ coefficient: }h_2 = 0.7; \\ H_3 = I \otimes X,\text{ coefficient: }h_3 = 0.2. \\ \end{split} $$ Define the number of $r$ repetitions into which to "break" the evolution, in this case, $r=10$. The order of the Suzuki-Trotter decomposition can be altered in the parameter `order`. In the current case, apply it to the first order. Define the evolution coefficient $t$, stating how much time the quantum system will be evolving under this Hamiltonian, according to the specific application you want. Set the evolution coefficient in the `evolution_coefficient` variable, considering it unitary in this example. In [this part of the guide](#measuring-the-expected-magnetization-as-a-function-of-time), you can follow an example where the evolution coefficient varies. ```python theme={null} from classiq import * # Defining the Hamiltonian for product formulas: HAMILTONIAN = 0.3 * Pauli.Z(0) * Pauli.Z(1) + 0.7 * Pauli.X(0) + 0.2 * Pauli.X(1) @qfunc def main(qba: Output[QArray]): allocate(2, qba) suzuki_trotter( HAMILTONIAN, evolution_coefficient=1.0, order=1, repetitions=10, qbv=qba, ) qprog_trotter = synthesize(main) show(qprog_trotter) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3DtdllWxDeJxISWzBmUSbC06a9G ``` # ## Exponentiate It is also possible to avoid choosing the Suzuki Trotter parameters, and simply use the default ones of `order=1` and `repetitions=1`. For this we can call Classiq `exponentiate` function. ```python theme={null} @qfunc def main(qba: Output[QArray]): allocate(2, qba) exponentiate( HAMILTONIAN, evolution_coefficient=1, qbv=qba, ) qprog_exponentiation = synthesize(main) show(qprog_exponentiation) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3Dtdm975OAGJk01tNb5xQL3MbKm ``` The number of repetitions can also be understood from the IDE, it is the number of repetitive blocks you see. In the current case, it applies the first-order decomposition, denoted in the IDE by the name `single_trotter_suzuki_layer_xxxx`. In the case of higher orders, the output would identify which order is being used. ![Trotter\_gif](https://docs.classiq.io/resources/Trotter_order.gif) ## qDRIFT The quantum stochastic drift protocol (qDRIFT) [\[2\]](#qdrift-paper) is similar to the first-order Suzuki-Trotter decomposition; however, it relies on a stochastic distribution of the evolution operators. The algorithm operates by sampling unitaries from the set $\{e^{-itH_j}\}$ according to a probability distribution defined by the weights $\{h_j\}$, normalized by a factor $\lambda = \sum_j h_j$. In a more structured manner, the qDRIFT protocol functions as follows: **Input**: A list of Hamiltonian terms $\{H_j\}$, a classical oracle function SAMPLE() that returns a value $j$ according to the probability distribution $p_j = h_j/\lambda$, and a target precision $\epsilon$. **Output**: An ordered list of evolution operators from the set $\{e^{-itH_j}\}$ that approximates the unitary $e^{-it\,H}$ with a bound error $\epsilon$. * Define a normalization to transform $\{h_j\}$ into a probability distribution. For this, define $\sum_j h_j = \lambda$. * The program depth is defined according to the required precision. For this, set $N = \lceil 2\lambda^2t^2/\epsilon \rceil$. * Now generate the ordered list of $N$ evolution operators according to the probability distribution. **The qDRIFT has proved to be a good alternative for the Suzuki-Trotter decomposition when the number of terms in the Hamiltonian is not Pauli sparse**, i.e., the number of terms on the expansion $H = \sum_j h_j H_j$ is not small when compared to all possible terms in it. This interesting method can be applied using the Classiq `qdrift()` function. Its inputs are: * `pauli_operator`: `CArray[PauliTerm]`: A list of Pauli operators that represent the term $H_j$ and its respective coefficients $h_j$; * `evolution_coefficient `: `CArray` The value $t$ for the coefficient in the time evolution operator; * `num_qdrift`:`CInt`: The number $N$ of unitary operators in the list of gates given by the qDRIFT; * `qbv`:`QArray`: The target qubits. In the current example, fix $N=288$, which corresponds to error $\epsilon = 0.01$ in the approximation. ```python theme={null} @qfunc def main(qba: Output[QArray]): allocate(2, qba) qdrift( HAMILTONIAN, evolution_coefficient=1, num_qdrift=288, qbv=qba, ) qprog_qdrift = synthesize(main) show(qprog_qdrift) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3DtdmurE56VJL70WHs0ycPSPMMu ``` ## Hamiltonian Simulation with Block Encoding Other methods of Hamiltonian simulation outside of the **product formulas** leverage the idea of Block Encoding and eigenvalue transformations. These approaches allow for the efficient simulation of quantum systems by encoding operators into unitary matrices and applying transformations to the eigenvalues of these matrices. # ## Qubitization The qubitization [\[3\]](#qubitization-paper) method enables the efficient simulation of quantum Hamiltonians. The key idea is to embed the Hamiltonian into a larger unitary operator, a method known as block encoding, where the block-encoded Hamiltonian is simulated using a quantum walk operator. This allows precise control over the evolution of quantum states and can significantly reduce the resources required for Hamiltonian simulation. For details on implementing the Qubitization method, visit the [Hamiltonian simulation with qubitization](https://github.com/Classiq/classiq-library/tree/main/algorithms/hamiltonian_simulation/hamiltonian_simulation_with_block_encoding/hamiltonian_simulation_qubitization.ipynb). # ## QSVT The general Quantum Singular Value Transformation (QSVT) [\[4\]](#qsvt-paper) method transforms the singular values of matrices encoded in quantum states. In Hamiltonian simulation, we need to combine two QSVT blocks, one for a sine operation and another for the cosine operation, for constructing the exponential of the matrix. For details on implementing the QSVT method, visit the [Hamiltonian simulation with QSVT](https://github.com/Classiq/classiq-library/blob/main/algorithms/hamiltonian_simulation/hamiltonian_simulation_with_block_encoding/hamiltonian_simulation_qsvt.ipynb). # ## GQSP Generalized Quantum Signal Processing (GQSP) [\[5\]](#gqsp-paper) achieves Hamiltonian simulation by applying a Laurent polynomial to the Szegedy walk operator of the block-encoded Hamiltonian. The polynomial coefficients are derived from the Jacobi-Anger expansion and approximate the time-evolution phase $e^{-i\lambda t}$ on each eigenvalue of the Hamiltonian, requiring only **one auxiliary qubit** and no amplitude amplification. For details on implementing the GQSP method, visit the [Hamiltonian simulation with GQSP](https://github.com/Classiq/classiq-library/blob/main/algorithms/hamiltonian_simulation/hamiltonian_simulation_with_block_encoding/hamiltonian_simulation_gqsp.ipynb). # ## Comparison of Block-Encoding Methods | Method | Extra block qubits | Controlled $U_H$? | Amplitude amplification? | Classical preprocessing | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | ----------------- | --------------------------------------- | ----------------------- | | [GQSP](https://github.com/Classiq/classiq-library/blob/main/algorithms/hamiltonian_simulation/hamiltonian_simulation_with_block_encoding/hamiltonian_simulation_gqsp.ipynb) | 1 | Yes | No | Angle computation | | [QSVT](https://github.com/Classiq/classiq-library/blob/main/algorithms/hamiltonian_simulation/hamiltonian_simulation_with_block_encoding/hamiltonian_simulation_qsvt.ipynb) | 2 | No | Yes (for a factor of 2) | Angle computation | | [Qubitization](https://github.com/Classiq/classiq-library/blob/main/algorithms/hamiltonian_simulation/hamiltonian_simulation_with_block_encoding/hamiltonian_simulation_qubitization.ipynb) | $O(\log d)$ | Yes | Yes (for the sum of Cheb. coefficients) | None | All three methods share the same asymptotic query complexity. ## Measuring the Expected Magnetization as a Function of Time Learn how to apply these different methods and evaluate their performance in comparison with the exact evolution. You will see the evolution of the expected value of a specific operator and compare it to the exact solution. This section uses the same Hamiltonian from the examples: $$ H = 0.3 \cdot \, Z\otimes Z + 0.7 \cdot \,X\otimes I + 0.2 \cdot \, I\otimes X $$ Now, analyze a quantity that varies with different values of time. For this, consider the behavior of the magnetization of the system, $M = (\langle I\otimes Z \rangle + \langle Z \otimes I\rangle)/2$ [\[6\]](#magnetization-paper), as a function of time. Precisely evaluate the result by exponentiating the Hamiltonian $H$ directly, and then comparing the results: ```python theme={null} import numpy as np from scipy.linalg import expm time_list = np.linspace(0, 2, 100).tolist() magnetization_hamiltonian = [ PauliTerm(pauli=[Pauli.Z, Pauli.I], coefficient=0.5), PauliTerm(pauli=[Pauli.I, Pauli.Z], coefficient=0.5), ] # Magnetization observable for observe: magnetization_HAMILTONIAN = 0.5 * Pauli.Z(0) + 0.5 * Pauli.Z(1) # Hamiltonians to matrices: magnetization_matrix = hamiltonian_to_matrix(magnetization_hamiltonian) Hamiltonian_matrix = hamiltonian_to_matrix(HAMILTONIAN) initial_state = np.zeros(4) initial_state[0] = 1.0 def expected_value(state, operator): state_H = np.conj(state.T) return state_H @ operator @ state ideal_magnetization = [] for t in time_list: state = expm(-1j * t * Hamiltonian_matrix) @ initial_state ideal_magnetization.append(expected_value(state, magnetization_matrix)) ``` To evaluate the expectation values of magnetization for all three methods, use the observe function: ```python theme={null} @qfunc def main(t: CReal, qba: Output[QArray]): allocate(2, qba) suzuki_trotter( HAMILTONIAN, evolution_coefficient=t, order=1, repetitions=30, qbv=qba, ) qprog_magnetization_trotter = synthesize(main) t_values = [{"t": times} for times in time_list] magnetization_ST_results = observe( qprog_magnetization_trotter, magnetization_HAMILTONIAN, parameters=t_values ) ``` **Output:** ``` Submitting job to simulator Job: https://platform.classiq.io/jobs/04251f67-6b2c-4e3f-b4ae-38ff7e7e0c1d ``` ```python theme={null} @qfunc def main(t: CReal, qba: Output[QArray]): allocate(2, qba) qdrift( HAMILTONIAN, evolution_coefficient=t, num_qdrift=288, qbv=qba, ) qprog_magnetization_qdrift = synthesize(main) magnetization_qdrift_results = observe( qprog_magnetization_qdrift, magnetization_HAMILTONIAN, parameters=t_values ) ``` **Output:** ``` Submitting job to simulator Job: https://platform.classiq.io/jobs/33a215be-ff75-4d3f-a3b3-2487dff359ff ``` ```python theme={null} @qfunc def main(t: CReal, qba: Output[QArray]): allocate(2, qba) exponentiate( HAMILTONIAN, evolution_coefficient=t, qbv=qba, ) qprog_magnetization_exponentiate = synthesize(main) magnetization_exponentiate_results = observe( qprog_magnetization_exponentiate, magnetization_HAMILTONIAN, parameters=t_values ) ``` **Output:** ``` Submitting job to simulator Job: https://platform.classiq.io/jobs/1b73b90f-9638-4013-af04-a768535dbd47 ``` You can plot the magnetization values of the three different methods with the exact values: ```python theme={null} import matplotlib.pyplot as plt fig, ax = plt.subplots() ax.set_xlabel("Evolution coefficient") ax.set_ylabel("Magnetization") magnetization_ST = magnetization_ST_results magnetization_qdrift = magnetization_qdrift_results magnetization_exponentiate = magnetization_exponentiate_results plt.plot( time_list, magnetization_ST, label="Suzuki-Trotter 1st order", ) plt.plot(time_list, magnetization_exponentiate, label="Exponentiate") plt.plot(time_list, magnetization_qdrift, label="qDRIFT") plt.plot(time_list, np.real(ideal_magnetization), label="Ideal Simulation") plt.legend() plt.show() ``` output It's possible to observe that the larger `t` is, the worse the approximation gets for the same set of arguments. To get a better approximation, there is a need for a deeper quantum program with more layers and or with a higher order of approximation. ## Summary and Exercises This guide introduced the Suzuki-Trotter and qDRIFT methods for Hamiltonian simulation, highlighting the simplicity of Classiq's high level functional design in tackling complex problems. To become more familiar with the methods and see how the synthesis engine is able to optimize quantum models for Hamiltonian simulation, try to apply the `qDrift` and `Suzuki-Trotter` methods to this tutorial as well as different values of the `evolution_coefficient`: * [Hamiltonian Evolution for a Water Molecule](https://docs.classiq.io/latest/explore/tutorials/technology_demonstrations/hamiltonian_evolution/hamiltonian_evolution/) Another good exercise is solving molecules other than the water molecule. With Classiq, you can generate the Hamiltonian of any valid molecule structure, following the same process for the water molecule. ## Read More Besides the methods presented in this guide, there are also mixed approaches. One of them, proposed by Matthew Hagan and Nathan Wiebe [\[7\]](#quantum-sim-paper), offers an interesting combination of the Suzuki-Trotter and qDRIFT methods. ## References \[1]: [Finding exponential product formulas of higher orders (Naomichi Hatano and Masuo Suzuki)](https://arxiv.org/abs/math-ph/0506007) \[2]: [A random compiler for fast Hamiltonian simulation (Earl Campbell)](https://arxiv.org/abs/1811.08017) \[3]: [Hamiltonian simulation by qubitization (Guang Hao Low and Isaac L. Chuang)](https://arxiv.org/abs/1610.06546) \[4]: [Quantum singular value transformation and beyond: Exponential improvements for quantum matrix arithmetics (András Gilyén, Yuan Su, Guang Hao Low, and Nathan Wiebe)](https://arxiv.org/abs/1806.01838) \[5]: [Motlagh, D., and Wiebe, N. *Generalized quantum signal processing.* PRX Quantum **5**, 020368 (2024).](https://journals.aps.org/prxquantum/abstract/10.1103/PRXQuantum.5.020368) \[6]: [The microscopic magnetization: Concept and application (L. L. Hirst)](https://journals.aps.org/rmp/abstract/10.1103/RevModPhys.69.607) \[7]: [Composite quantum simulations (Matthew Hagan and Nathan Wiebe)](https://quantum-journal.org/papers/q-2023-11-14-1181/) # Hamiltonian Simulation with Generalized Quantum Signal Processing (GQSP) Source: https://docs.classiq.io/explore/algorithms/hamiltonian_simulation/hamiltonian_simulation_with_block_encoding/hamiltonian_simulation_gqsp Open this notebook in GitHub to run it yourself > **Simulating physical and chemical systems** was among the original motivations for quantum computing, as first envisioned by Richard Feynman in 1982, and remains one of its most impactful applications. Time-independent Hamiltonian simulation refers to the task of approximately implementing the unitary evolution operator $e^{-iHt}$ for a Hermitian matrix $H$. When access to the Hamiltonian is provided via block-encoding, this can be realized by applying an appropriate polynomial transformation within a desired precision $\epsilon$. > > **Generalized Quantum Signal Processing (GQSP)** \[1] achieves this by applying a Laurent polynomial $P(W)$ to the walk operator $W$ of the block-encoded Hamiltonian. The polynomial coefficients are derived from the [Jacobi-Anger expansion](https://github.com/Classiq/classiq-library/blob/main/algorithms/hamiltonian_simulation/hamiltonian_simulation_with_block_encoding/jacobi_anger_expansion.ipynb) (Eq. (1): $e^{it\cos(x)} = \sum_k i^k J_k(t)\,e^{ikx}$). GQSP requires only **one auxiliary qubit** and eliminates the need for amplitude amplification. > > * **Input:** A Hermitian operator $H$ given through a block-encoding unitary $U_H$ with scaling factor $\alpha \ge \|H\|$, evolution time $t$, and target error $\epsilon$. > * **Output:** A unitary $U$ approximating $e^{-iHt}$, with $\|U - e^{-iHt}\| < \epsilon$. > > **Complexity:** $O\!\left(\alpha t + \frac{\log \epsilon^{-1}}{\log\!\left(e + \log(\epsilon^{-1}) / \alpha t\right)}\right)$ calls to the block-encoding, using one auxiliary qubit and a classical preprocessing step to compute the GQSP rotation angles. > > *** > > **Keywords:** Hamiltonian Simulation, Block Encoding, Generalized Quantum Signal Processing, GQSP, Walk Operator, Oracle/Query complexity. A block-encoded Hamiltonian refers to its embedding within a larger unitary matrix. **Definition**: A $(s, m, \epsilon)$-encoding of a $2^n\times 2^n$ matrix $A$ refers to completing it into a $2^{n+m}\times 2^{n+m}$ unitary matrix $U_{(s,m,\epsilon)-A}$: $$ U_{(s,m,\epsilon)-A} = \begin{pmatrix} A/s & * \\ * & * \end{pmatrix}, $$ with functional error $\left|\left|\left(U_{(s,m,\epsilon)-A}\right)_{0:2^n-1,0:2^n-1}-A/s \right|\right|\leq \epsilon$. Here $s$ is a scaling factor that ensures the overall operator is unitary, $m$ is the number of auxiliary (block) qubits, and $\epsilon$ is the encoding error. This notebook assumes basic knowledge of Linear Combination of Unitaries (LCU); see the [LCU tutorial](https://github.com/Classiq/classiq-library/blob/main/tutorials/basic_tutorials/quantum_primitives/linear_combination_of_unitaries/linear_combination_of_unitaries.ipynb) for background. The [GQSP](https://github.com/Classiq/classiq-library/blob/main/algorithms/quantum_primitives/gqsp/gqsp.ipynb) approach implements polynomials on unitary operators. For Hamiltonian evolution, given an exact $(s, m, 0)$-encoding of the Hamiltonian (denoting $U_H \equiv U_{(s,m,0)-H}$), we define the Szegedy quantum walk operator \[2] $W \equiv \Pi_{|0\rangle_m}\, U_H$, where $\Pi_{|0\rangle_m}$ reflects about the $|0\rangle$ state of the block variable. The walk operator $W$ is unitary, with eigenvalues $z = e^{\pm i\arccos(\lambda/s)}$, where $\lambda$ are the Hamiltonian eigenvalues. By the [Jacobi-Anger expansion](https://github.com/Classiq/classiq-library/blob/main/algorithms/hamiltonian_simulation/hamiltonian_simulation_with_block_encoding/jacobi_anger_expansion.ipynb) ([Eq. (1)](https://github.com/Classiq/classiq-library/blob/main/algorithms/hamiltonian_simulation/hamiltonian_simulation_with_block_encoding/jacobi_anger_expansion.ipynb)), the polynomial approximating $e^{-ist\cos(x)}$ as a Laurent series in $z = e^{ix}$ gives $P(z) \approx e^{-i\lambda t}$ on the eigenvalue $z = e^{i\arccos(\lambda/s)}$ (since $\cos(x) = \lambda/s$) - the desired time-evolution phase. Applying $P(W)$ via GQSP therefore gives approximately $e^{-iHt}$ in the block. GQSP computes a set of single-qubit rotation angles $\{\phi_k\}$ such that the resulting circuit block-encodes the desired polynomial: $$ \mathrm{GQSP}(W) \approx U_{(1,\,m+1,\,\epsilon)-e^{-iHt}} = \begin{pmatrix} e^{-iHt} & * \\ * & * \end{pmatrix}. $$ (In practice, our prefactor is slightly different, $\beta^{-1}$ with $\beta = 0.9999$ which ensures numerical stability of the classical angle computation). Compared the the QSVT approach for Hamiltonian simulation, the GQSP method uses only one extra block qubit, and requires no amplitude amplification. However, it includes controlled operations on the block-encoding unitary. This notebook demonstrates Hamiltonian simulation using the GQSP method. For the other approaches, see the companion notebooks on QSVT and Qubitization. For a side-by-side comparison of all three methods, see the table at the end of this notebook. ## Preliminaries # ## Setting a Specific Hamiltonian to Evolve We set some specific hyperparameters for our problem. We use a simple Hamiltonian given as a sum of Pauli strings, and the `lcu_pauli` function to block-encode it via the Linear Combination of Unitaries (LCU) technique: $$ H = \sum_{i} \alpha_i U_i, \qquad U_{(\bar{\alpha},m,0)-H} = \begin{pmatrix} H/\bar{\alpha} & * \\ * & * \end{pmatrix}, \qquad \bar{\alpha} = \sum_i |\alpha_i|. $$ *To treat different problems with the same algorithm, simply change theses hyperparameters*. ```python theme={null} import time import matplotlib.pyplot as plt import numpy as np import scipy from classiq import * ``` ```python theme={null} EVOLUTION_TIME = 22 EPS = 1e-7 HAMILTONIAN = ( 0.4 * Pauli.I(0) + 0.1 * Pauli.Z(1) + 0.05 * Pauli.X(0) * Pauli.X(1) + 0.2 * Pauli.Z(0) * Pauli.Z(1) ) print(f"The Hamiltonian to evolve: {HAMILTONIAN}") ``` **Output:** ``` The Hamiltonian to evolve: 0.4 + 0.05*Pauli.X(0)*Pauli.X(1) + 0.2*Pauli.Z(0)*Pauli.Z(1) + 0.1*Pauli.Z(1) ``` Next, we define the block-encoding quantum function, and a Quantum Struct for its variable. ```python theme={null} data_size = HAMILTONIAN.num_qubits block_size = ( (len(HAMILTONIAN.terms) - 1).bit_length() if len(HAMILTONIAN.terms) != 1 else 1 ) BE_SCALING = np.sum( np.abs([term.coefficient for term in HAMILTONIAN.terms]) ) # scaling for LCU of Paulis print(f"Block size: {block_size}") print(f"Block-encoding scaling factor: {BE_SCALING}") class BlockEncodedState(QStruct): data: QNum[data_size] block: QNum[block_size] @qfunc def be_hamiltonian(state: BlockEncodedState): lcu_pauli(HAMILTONIAN * (1 / BE_SCALING), state.data, state.block) ``` **Output:** ``` Block size: 2 Block-encoding scaling factor: 0.75 ``` Finally, we set the initial state to evolve and calculate classically the expected evolved state for verifying the quantum methods. ```python theme={null} state_to_evolve = np.random.rand(2**data_size) state_to_evolve = (state_to_evolve / np.linalg.norm(state_to_evolve)).tolist() matrix = pauli_operator_to_matrix(HAMILTONIAN) expected_state = scipy.linalg.expm(-1j * matrix * EVOLUTION_TIME) @ state_to_evolve ``` # ## Setting Up a Statevector Simulator Working with block-encoding typically requires post-selection of the block variable being at state $|0\rangle$. The success of this process can be amplified via Oblivious Amplitude Amplification. In this notebook, instead, we use a statevector simulator and project the result. We import two utility functions from `hamiltonian_simulation_utils`: * `get_projected_state_vector`: extracts the post-selected statevector from the execution results. * `compare_quantum_classical_states`: aligns the global phase and computes the overlap with the classically computed reference. ```python theme={null} from hamiltonian_simulation_utils import ( compare_quantum_classical_states, get_projected_state_vector, ) execution_preferences = ExecutionPreferences( num_shots=1, backend_preferences=ClassiqBackendPreferences( backend_name=ClassiqSimulatorBackendNames.SIMULATOR_STATEVECTOR ), ) ``` # ## The Jacobi-Anger Expansion The polynomial approximation of the time evolution relies on the [Jacobi-Anger expansion](https://github.com/Classiq/classiq-library/blob/main/algorithms/hamiltonian_simulation/hamiltonian_simulation_with_block_encoding/jacobi_anger_expansion.ipynb). For GQSP, we use the complex exponential form $e^{it\cos(x)} = \sum_k i^k J_k(t)\,e^{ikx}$, implemented via `poly_jacobi_anger_exp_cos`. We then compute the GQSP rotation angles from this polynomial using `gqsp_phases`. ```python theme={null} from classiq.applications.qsp.qsp import ( gqsp_phases, poly_jacobi_anger_degree, poly_jacobi_anger_exp_cos, ) GQSP_SCALE = 0.9999 t0 = time.perf_counter() gqsp_degree = poly_jacobi_anger_degree(EPS, EVOLUTION_TIME * BE_SCALING) jacobi_anger_poly_expcos = GQSP_SCALE * poly_jacobi_anger_exp_cos( gqsp_degree, -EVOLUTION_TIME * BE_SCALING ) jacobi_anger_phases_expcos = gqsp_phases(jacobi_anger_poly_expcos) classical_preprocess_time_gqsp = time.perf_counter() - t0 print(f"GQSP polynomial degree: {gqsp_degree}") print(f"Classical preprocessing time: {classical_preprocess_time_gqsp:.3f} s") ``` **Output:** ``` GQSP polynomial degree: 33 Classical preprocessing time: 2.932 s ``` # ## The Walk Operator > **Note:** The current implementation assumes that the block-encoding unitary $U_H$ is also Hermitian. For the non-Hermitian generalization, see the Technical Notes. Given the block-encoding $U_{(s,m,0)-H}$, we define the Szegedy quantum walk operator \[2]: $$ W \equiv \Pi_{|0\rangle_m}\, U_{(s,m,0)-H}, $$ where $\Pi_{|0\rangle_m}$ is a reflection about the $|0\rangle$ state of the block variable. As mentioned above, it has the key property that its spectrum is directly tied to the Hamiltonian's eigenvalues: $$ \text{eigenvalues: } e^{\pm i \arccos(\lambda/s)}, \quad \text{eigenvectors: } |\varphi^{\pm}_{\lambda}\rangle \equiv \frac{1}{\sqrt{2}}\left(|v_{\lambda}\rangle|0\rangle_m \pm i|\perp_{\lambda}\rangle\right), $$ where $|v_\lambda\rangle$ is an eigenstate of $H$ with eigenvalue $\lambda$. ```python theme={null} from classiq.qmod.symbolic import pi @qfunc def my_reflect_about_zero(qba: QNum): control(qba == 0, lambda: phase(pi)) phase(pi) @qfunc def walk_operator( be_qfunc: QCallable[BlockEncodedState], state: BlockEncodedState ) -> None: be_qfunc(state) my_reflect_about_zero(state.block) ``` # ## Verifying the Block-Encoding As a sanity check before the main algorithm, we verify the Hamiltonian block-encoding: we apply $U_H$ on the initial state and check that the post-selected result matches $(H/\bar{\alpha})|\psi\rangle$ as expected. ```python theme={null} @qfunc def main(data: Output[QNum[data_size]], block: Output[QNum[block_size]]): state = BlockEncodedState() allocate(state) inplace_prepare_amplitudes(state_to_evolve, 0.0, state.data) be_hamiltonian(state) bind(state, [data, block]) qprog_be = synthesize(main) show(qprog_be) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3D33qBTW7qLuxJGlgJOoBiBBhwH ``` Screenshot 2025-10-16 at 15.16.18.png ```python theme={null} TOLERANCE = 1e-9 with ExecutionSession(qprog_be, execution_preferences=execution_preferences) as es: res_be = es.sample() state_result_be = get_projected_state_vector(res_be) expected_state_be = matrix @ state_to_evolve renormalized_be, overlap_be = compare_quantum_classical_states( expected_state_be, state_result_be, BE_SCALING ) print("Expected state:", expected_state_be) print("Resulting state after rescaling:", renormalized_be) assert np.isclose(overlap_be, 1, TOLERANCE) print("=" * 40) print(f"Fidelity is 1 (with {TOLERANCE} tolerance)") ``` **Output:** ``` Expected state: [0.62080558+0.j 0.09074939+0.j 0.04500028+0.j 0.18596617+0.j] Resulting state after rescaling: [0.62080558+2.44087822e-18j 0.09074939-9.63604777e-17j 0.04500028-2.02041677e-16j 0.18596617-1.14021062e-16j] ======================================== Fidelity is 1 (with 1e-09 tolerance) ``` ## Implementation We define a Quantum Struct for the GQSP block-encoding. The GQSP method adds one block qubit (`block_gqsp`) to the Hamiltonian block variable (`block_ham`). The data variable follows that of the Hamiltonian encoding. Applying GQSP requires a *negative power* of the walk operator, since the approximating polynomial $P(z)$ is a Laurent polynomial that includes negative powers of $z$ (corresponding to $W^{-1}$). This is handled by the `negative_power` parameter. ```python theme={null} class GQSPBlock(QStruct): block_ham: QNum[block_size] block_gqsp: QBit class GQSPState(QStruct): data: QNum[data_size] block: GQSPBlock @qfunc def gqsp_hamiltonian_evolution( be_qfunc: QCallable[BlockEncodedState], state: GQSPState, ): gqsp( u=lambda: walk_operator(be_qfunc, [state.data, state.block.block_ham]), aux=state.block.block_gqsp, phases=jacobi_anger_phases_expcos, negative_power=gqsp_degree, ) ``` The code in the rest of this section builds a model that applies the `gqsp_hamiltonian_evolution` function on the randomly prepared vector, synthesizes it, executes the resulting quantum program, and verifies the results. ```python theme={null} @qfunc def main(data: Output[QNum[data_size]], block: Output[QNum[block_size + 1]]): state = GQSPState() allocate(state) inplace_prepare_amplitudes(state_to_evolve, 0.0, state.data) gqsp_hamiltonian_evolution(be_hamiltonian, state) bind(state, [data, block]) qprog_gqsp = synthesize(main) show(qprog_gqsp) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3D33t06StqkU0O0rBRxsTEFVZF4 ``` Screenshot 2025-10-16 at 15.18.21.png ```python theme={null} with ExecutionSession(qprog_gqsp, execution_preferences) as es: result_gqsp = es.sample() state_result_gqsp = get_projected_state_vector(result_gqsp) exp_scaling_factor_gqsp = 1 / GQSP_SCALE ``` ```python theme={null} renormalized_state_gqsp, overlap_gqsp = compare_quantum_classical_states( expected_state, state_result_gqsp, exp_scaling_factor_gqsp ) print("Expected state:", expected_state) print("Resulting state after rescaling:", renormalized_state_gqsp) assert np.linalg.norm(renormalized_state_gqsp - expected_state) < EPS print("=" * 40) print("Overlap between expected and resulting state:", overlap_gqsp) ``` **Output:** ``` Expected state: [-0.87895119-0.0601642j 0.2797734 -0.11212048j -0.03050629-0.27574903j -0.22787636+0.06391499j] Resulting state after rescaling: [-0.87895119-0.0601642j 0.27977341-0.11212048j -0.03050629-0.27574903j -0.22787636+0.06391499j] ======================================== Overlap between expected and resulting state: 1.0000000000000002 ``` ## References \[1]: [Motlagh, D., and Wiebe, N. *Generalized quantum signal processing.* PRX Quantum **5**, 020368 (2024).](https://journals.aps.org/prxquantum/abstract/10.1103/PRXQuantum.5.020368) \[2]: [Szegedy, M. *Quantum speed-up of Markov chain based algorithms.* In *45th Annual IEEE Symposium on Foundations of Computer Science*, pp. 32-41 (2004).](https://ieeexplore.ieee.org/abstract/document/1366222) \[3]: [Lin, L. *Lecture notes on quantum algorithms for scientific computation.* arXiv:2201.08309 \[quant-ph\] (2022).](https://arxiv.org/abs/2201.08309) ## Technical Notes # ## Generalizing to Non-Hermitian Block-Encoding Unitaries The current implementation assumes that the block-encoding unitary $U_H$ is also Hermitian. This assumption underlies the walk operator's spectral properties. For a non-Hermitian block-encoding unitary, an analogous walk operator can be defined as $\tilde{W} \equiv U_H^T \Pi_{|0\rangle_m} U_H \Pi_{|0\rangle_m}$, which satisfies equivalent spectral properties. See Section 7.4 in Ref. \[3] for details. # ## Comparison with Other Methods | Method | extra block qubits | Controlled $U_H$? | Amplitude amplification? | Classical preprocessing | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | ----------------- | --------------------------------------- | ----------------------- | | [GQSP](https://github.com/Classiq/classiq-library/blob/main/algorithms/hamiltonian_simulation/hamiltonian_simulation_with_block_encoding/hamiltonian_simulation_gqsp.ipynb) | 1 | Yes | No | Angle computation | | [QSVT](https://github.com/Classiq/classiq-library/blob/main/algorithms/hamiltonian_simulation/hamiltonian_simulation_with_block_encoding/hamiltonian_simulation_qsvt.ipynb) | 2 | No | Yes (for a factor of 2) | Angle computation | | [Qubitization](https://github.com/Classiq/classiq-library/blob/main/algorithms/hamiltonian_simulation/hamiltonian_simulation_with_block_encoding/hamiltonian_simulation_qubitization.ipynb) | $O(\log d)$ | Yes | Yes (for the sum of Cheb. coefficients) | None | All three methods share the same asymptotic query complexity. Differences in the table reflect the detailed implementation of this specific example. # Hamiltonian Simulation with Quantum Singular Value Transformation (QSVT) Source: https://docs.classiq.io/explore/algorithms/hamiltonian_simulation/hamiltonian_simulation_with_block_encoding/hamiltonian_simulation_qsvt Open this notebook in GitHub to run it yourself > **Simulating physical and chemical systems** was among the original motivations for quantum computing, as first envisioned by Richard Feynman in 1982, and remains one of its most impactful applications. Time-independent Hamiltonian simulation refers to the task of approximately implementing the unitary evolution operator $e^{-iHt}$ for a Hermitian matrix $H$. When access to the Hamiltonian is provided via block-encoding, this can be realized by applying an appropriate polynomial transformation within a desired precision $\epsilon$. > > **Quantum Singular Value Transformation (QSVT)** \[1] achieves Hamiltonian simulation by exploiting the decomposition $e^{-iHt} = \cos(Ht) - i\sin(Ht)$. Two QSVT blocks, one for the even polynomial $\cos(xt)$ and one for the odd polynomial $\sin(xt)$ (both approximated via the [Jacobi-Anger expansion](https://github.com/Classiq/classiq-library/blob/main/algorithms/hamiltonian_simulation/hamiltonian_simulation_with_block_encoding/jacobi_anger_expansion.ipynb), Eqs. (3)-(4)), are combined via LCU. Unlike GQSP and Qubitization, QSVT operates **directly on the block-encoding** without requiring a walk operator or controlled block-encoding calls, using only **two auxiliary qubits**. > > * **Input:** A Hermitian operator $H$ given through a block-encoding unitary $U_H$ with scaling factor $\alpha \ge \|H\|$, evolution time $t$, and target error $\epsilon$. > * **Output:** A unitary $U$ approximating $e^{-iHt}$, with $\|U - e^{-iHt}\| < \epsilon$. > > **Complexity:** $O\!\left(\alpha t + \frac{\log \epsilon^{-1}}{\log\!\left(e + \log(\epsilon^{-1}) / \alpha t\right)}\right)$ calls to the block-encoding, using two auxiliary qubits. No controlled block-encoding calls required. Requires classical preprocessing to compute QSVT rotation angles. > > *** > > **Keywords:** Hamiltonian Simulation, Block Encoding, Quantum Singular Value Transformation, QSVT, Quantum Signal Processing, LCU, Oracle/Query complexity. A block-encoded Hamiltonian refers to its embedding within a larger unitary matrix. **Definition**: A $(s, m, \epsilon)$-encoding of a $2^n\times 2^n$ matrix $A$ refers to completing it into a $2^{n+m}\times 2^{n+m}$ unitary matrix $U_{(s,m,\epsilon)-A}$: $$ U_{(s,m,\epsilon)-A} = \begin{pmatrix} A/s & * \\ * & * \end{pmatrix}, $$ with functional error $\left|\left|\left(U_{(s,m,\epsilon)-A}\right)_{0:2^n-1,0:2^n-1}-A/s \right|\right|\leq \epsilon$. Here $s$ is a scaling factor that ensures the overall operator is unitary, $m$ is the number of auxiliary (block) qubits, and $\epsilon$ is the encoding error. This notebook assumes basic knowledge of Linear Combination of Unitaries (LCU) and the PREPARE-SELECT implementation; see the [LCU tutorial](https://github.com/Classiq/classiq-library/blob/main/tutorials/basic_tutorials/quantum_primitives/linear_combination_of_unitaries/linear_combination_of_unitaries.ipynb) for background. We assume an exact $(s, m, 0)$-encoding of the Hamiltonian as input. The QSVT method outputs an approximated block-encoding of its time evolution: $$ U_{(s,m,0)-H} \;\rightarrow\; U_{(2,\,m+2,\,\epsilon)-\exp(-iHt)} = \begin{pmatrix} \exp(-iHt)/2 & * \\ * & * \end{pmatrix}. $$ This is achieved by implementing a block-encoding for the polynomial approximation $P(H)\approx \frac{1}{\tilde{s}}e^{-iHt}$. To recover the exact unitary $e^{-iHt}$ one would apply amplitude amplification to drive the prefactor $1/2\rightarrow 1$; here we instead employ projected statevector simulation. QSVT is a general method for implementing polynomial singular value transformation of block-encoded matrices, where each polynomial must have a well-defined parity (even or odd). The transformation is based on Quantum Signal Processing (QSP), achieved by a series of qubit rotations. In the case of general polynomial transformation, as in Hamiltonian simulation $e^{-iHt} = \cos(Ht) - i\sin(Ht)$, one can apply two polynomial transformations, one for the even polynomial $\cos(xt)$ and one for the odd polynomial $\sin(xt)$, combined via Linear Combination of Unitaries (LCU). The two QSVT circuits are combined using the `qsvt_lcu` function, which implements an optimized `select` operation that interleaves the rotations of both polynomials within a single QSVT traversal. The overall result is a $(2,\,m+2,\,\epsilon)$-block-encoding of $e^{-iHt}$, where one block qubit comes from the QSVT and the second one, as well as the $1/2$ prefactor, originates from the LCU. (In practice, our prefactor is slightly different, $2\beta^{-1}$ with $\beta = 0.9999$ which ensures numerical stability of the classical angle computation). This notebook demonstrates Hamiltonian simulation using the QSVT method. For the other approaches, see the companion notebooks on GQSP and Qubitization. For a side-by-side comparison of all three methods, see the table at the end of this notebook. ## Preliminaries # ## Setting a Specific Hamiltonian to Evolve We set some specific hyperparameters for our problem. We use a simple Hamiltonian given as a sum of Pauli strings, and the `lcu_pauli` function to block-encode it via the Linear Combination of Unitaries (LCU) technique: $$ H = \sum_{i} \alpha_i U_i, \qquad U_{(\bar{\alpha},m,0)-H} = \begin{pmatrix} H/\bar{\alpha} & * \\ * & * \end{pmatrix}, \qquad \bar{\alpha} = \sum_i |\alpha_i|. $$ *To treat different problems with the same algorithm, simply change theses hyperparameters*. ```python theme={null} import time import matplotlib.pyplot as plt import numpy as np import scipy from classiq import * ``` ```python theme={null} EVOLUTION_TIME = 22 EPS = 1e-7 HAMILTONIAN = ( 0.4 * Pauli.I(0) + 0.1 * Pauli.Z(1) + 0.05 * Pauli.X(0) * Pauli.X(1) + 0.2 * Pauli.Z(0) * Pauli.Z(1) ) print(f"The Hamiltonian to evolve: {HAMILTONIAN}") ``` **Output:** ``` The Hamiltonian to evolve: 0.4 + 0.05*Pauli.X(0)*Pauli.X(1) + 0.2*Pauli.Z(0)*Pauli.Z(1) + 0.1*Pauli.Z(1) ``` Next, we define the block-encoding quantum function, and a Quantum Struct for its variable. ```python theme={null} data_size = HAMILTONIAN.num_qubits block_size = ( (len(HAMILTONIAN.terms) - 1).bit_length() if len(HAMILTONIAN.terms) != 1 else 1 ) BE_SCALING = np.sum( np.abs([term.coefficient for term in HAMILTONIAN.terms]) ) # scaling for LCU of Paulis print(f"Block size: {block_size}") print(f"Block-encoding scaling factor: {BE_SCALING}") class BlockEncodedState(QStruct): data: QNum[data_size] block: QNum[block_size] @qfunc def be_hamiltonian(state: BlockEncodedState): lcu_pauli(HAMILTONIAN * (1 / BE_SCALING), state.data, state.block) ``` **Output:** ``` Block size: 2 Block-encoding scaling factor: 0.75 ``` Finally, we set the initial state to evolve and calculate classically the expected evolved state for verifying the quantum method. ```python theme={null} state_to_evolve = np.random.rand(2**data_size) state_to_evolve = (state_to_evolve / np.linalg.norm(state_to_evolve)).tolist() matrix = pauli_operator_to_matrix(HAMILTONIAN) expected_state = scipy.linalg.expm(-1j * matrix * EVOLUTION_TIME) @ state_to_evolve ``` # ## Setting Up a Statevector Simulator Working with block-encoding typically requires post-selection of the block variable being at state $|0\rangle$. The success of this process can be amplified via Oblivious Amplitude Amplification. In this notebook, instead, we use a statevector simulator and project the result. We import two utility functions from `hamiltonian_simulation_utils`: * `get_projected_state_vector`: extracts the post-selected statevector from the execution results. * `compare_quantum_classical_states`: aligns the global phase and computes the overlap with the classically computed reference. ```python theme={null} from hamiltonian_simulation_utils import ( compare_quantum_classical_states, get_projected_state_vector, ) execution_preferences = ExecutionPreferences( num_shots=1, backend_preferences=ClassiqBackendPreferences( backend_name=ClassiqSimulatorBackendNames.SIMULATOR_STATEVECTOR ), ) ``` # ## The Jacobi-Anger Expansion The polynomial approximation of the time evolution relies on the [Jacobi-Anger expansion](https://github.com/Classiq/classiq-library/blob/main/algorithms/hamiltonian_simulation/hamiltonian_simulation_with_block_encoding/jacobi_anger_expansion.ipynb). For QSVT, we use the real Chebyshev forms $\cos(xt)$ and $\sin(xt)$ (Eqs. (3)-(4)), computed via `poly_jacobi_anger_cos` and `poly_jacobi_anger_sin`. From these, we derive the QSVT rotation angles using `qsvt_phases`. ```python theme={null} from classiq.applications.qsp import qsvt_phases from classiq.applications.qsp.qsp import ( poly_jacobi_anger_cos, poly_jacobi_anger_degree, poly_jacobi_anger_sin, ) t0 = time.perf_counter() qsvt_degree = poly_jacobi_anger_degree(EPS, EVOLUTION_TIME * BE_SCALING) COS_SCALE = SIN_SCALE = 0.9999 poly_even = COS_SCALE * poly_jacobi_anger_cos(qsvt_degree, EVOLUTION_TIME * BE_SCALING) phases_cos = qsvt_phases(poly_even) poly_odd = SIN_SCALE * poly_jacobi_anger_sin(qsvt_degree, EVOLUTION_TIME * BE_SCALING) phases_sin = qsvt_phases(poly_odd) classical_preprocess_time_qsvt = time.perf_counter() - t0 print(f"QSVT polynomial degree: {qsvt_degree}") print(f"Classical preprocessing time: {classical_preprocess_time_qsvt:.3f} s") assert np.abs(len(phases_sin) - len(phases_cos)) <= 1 ``` **Output:** ``` QSVT polynomial degree: 33 Classical preprocessing time: 2.870 s ``` # ## Verifying the Block-Encoding As a sanity check before the main algorithm, we verify the Hamiltonian block-encoding: we apply $U_H$ on the initial state and check that the post-selected result matches $(H/\bar{\alpha})|\psi\rangle$ as expected. ```python theme={null} @qfunc def main(data: Output[QNum[data_size]], block: Output[QNum[block_size]]): state = BlockEncodedState() allocate(state) inplace_prepare_amplitudes(state_to_evolve, 0.0, state.data) be_hamiltonian(state) bind(state, [data, block]) qprog_be = synthesize(main) show(qprog_be) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3D33xB4bjrs7vhRP4pqVIiFuFPy ``` Screenshot 2025-10-16 at 15.16.18.png ```python theme={null} TOLERANCE = 1e-9 with ExecutionSession(qprog_be, execution_preferences=execution_preferences) as es: res_be = es.sample() state_result_be = get_projected_state_vector(res_be) expected_state_be = matrix @ state_to_evolve renormalized_be, overlap_be = compare_quantum_classical_states( expected_state_be, state_result_be, BE_SCALING ) print("Expected state:", expected_state_be) print("Resulting state after rescaling:", renormalized_be) assert np.isclose(overlap_be, 1, TOLERANCE) print("=" * 40) print(f"Fidelity is 1 (with {TOLERANCE} tolerance)") ``` **Output:** ``` Expected state: [0.01550541+0.j 0.27717585+0.j 0.09124952+0.j 0.11147152+0.j] Resulting state after rescaling: [0.01550541-3.18016883e-18j 0.27717585-3.83523176e-15j 0.09124952-1.32318732e-15j 0.11147152-1.60360148e-15j] ======================================== Fidelity is 1 (with 1e-09 tolerance) ``` ## Implementation We define Quantum Structs for the QSVT block-encoding. Two block qubits are used: one for the QSVT signal processing (`block_qsvt`) and one for the LCU, selecting between odd and even polynomials (`block_lcu`). These are added to the block variable of the Hamiltonian encoding. The `qsvt_hamiltonian_evolution` function uses `prepare_select` with the `qsvt_lcu` as the select operation. The LCU coefficients $(1/2, -i/2)$ implement $\frac{1}{2}(\cos(Ht) - i\sin(Ht)) = \frac{1}{2}e^{-iHt}$. The `projector_cnot` function implements the QSVT projector onto the $|0\rangle$ state of the Hamiltonian block variable. Screenshot 2025-10-13 at 12.07.04.png
On the left is the naive LCU design for the select of two QSVT calls (taken from Ref. \[1] ), where each call is applied using a repetition over the usual QSVT step on the upper right panel, where each polynomial is acheived by its own series of angles $\phi_r$. However, one can design an optimized select operation, by selecting the rotations within the QSVT steps themselves, as shown in the lower right panel.
```python theme={null} class QSVTBlock(QStruct): block_ham: QNum[block_size] block_qsvt: QBit block_lcu: QBit class QSVTState(QStruct): data: QNum[data_size] block: QSVTBlock @qfunc def qsvt_hamiltonian_evolution( phases_cos: list[float], phases_sin: list[float], be_qfunc: QCallable[BlockEncodedState], state: QSVTState, ): def projector_cnot(q: QBit): q ^= state.block.block_ham == 0 prepare_select( coefficients=[1 / 2, -1j / 2], select=lambda block_lcu: qsvt_lcu( phases_cos, phases_sin, projector_cnot, projector_cnot, lambda: be_qfunc([state.data, state.block.block_ham]), state.block.block_qsvt, block_lcu, ), block=state.block.block_lcu, ) ``` The code in the rest of this section builds a model that applies the `qsvt_hamiltonian_evolution` function on the randomly prepared vector $(\vec{\psi},\vec{0})$, synthesizes it, executes the resulting quantum program, and verifies the results. ```python theme={null} @qfunc def main(data: Output[QNum[data_size]], block: Output[QNum[block_size + 2]]): state = QSVTState() allocate(state) inplace_prepare_amplitudes(state_to_evolve, 0.0, state.data) qsvt_hamiltonian_evolution(phases_cos, phases_sin, be_hamiltonian, state) bind(state, [data, block]) qprog_qsvt = synthesize(main, constraints=Constraints(optimization_parameter="width")) show(qprog_qsvt) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3D341fNgiDKLoX6iTNuqKHwx7yH ``` Screenshot 2025-10-16 at 15.26.55.png ```python theme={null} with ExecutionSession(qprog_qsvt, execution_preferences) as es: results_qsvt = es.sample() state_result_qsvt = get_projected_state_vector(results_qsvt) exp_scaling_factor_qsvt = 2 * (1 / COS_SCALE) ``` ```python theme={null} renormalized_state_qsvt, overlap_qsvt = compare_quantum_classical_states( expected_state, state_result_qsvt, exp_scaling_factor_qsvt ) print("Expected state:", expected_state) print("Resulting state after rescaling:", renormalized_state_qsvt) assert np.linalg.norm(renormalized_state_qsvt - expected_state) < EPS print("=" * 40) print("Overlap between expected and resulting state:", overlap_qsvt) ``` **Output:** ``` Expected state: [-0.04311848-0.05046671j 0.7844373 -0.43360491j 0.07945592-0.37532381j -0.06593688+0.20176705j] Resulting state after rescaling: [-0.04311848-0.05046671j 0.7844373 -0.43360491j 0.07945593-0.37532381j -0.06593688+0.20176705j] ======================================== Overlap between expected and resulting state: 1.0 ``` ## References \[1]: [Martyn, J. M., Rossi, Z. M., Tan, A. K., & Chuang, I. L. *Grand unification of quantum algorithms.* PRX Quantum **2**, 040203 (2021).](https://journals.aps.org/prxquantum/abstract/10.1103/PRXQuantum.2.040203) ## Technical Notes # ## Comparison with Other Methods | Method | extra block qubits | Controlled $U_H$? | Amplitude amplification? | Classical preprocessing | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | ----------------- | --------------------------------------- | ----------------------- | | [GQSP](https://github.com/Classiq/classiq-library/blob/main/algorithms/hamiltonian_simulation/hamiltonian_simulation_with_block_encoding/hamiltonian_simulation_gqsp.ipynb) | 1 | Yes | No | Angle computation | | [QSVT](https://github.com/Classiq/classiq-library/blob/main/algorithms/hamiltonian_simulation/hamiltonian_simulation_with_block_encoding/hamiltonian_simulation_qsvt.ipynb) | 2 | No | Yes (for a factor of 2) | Angle computation | | [Qubitization](https://github.com/Classiq/classiq-library/blob/main/algorithms/hamiltonian_simulation/hamiltonian_simulation_with_block_encoding/hamiltonian_simulation_qubitization.ipynb) | $O(\log d)$ | Yes | Yes (for the sum of Cheb. coefficients) | None | All three methods share the same asymptotic query complexity. Differences in the table reflect the detailed implementation of this specific example. # Hamiltonian Simulation with Qubitization (Chebyshev LCU) Source: https://docs.classiq.io/explore/algorithms/hamiltonian_simulation/hamiltonian_simulation_with_block_encoding/hamiltonian_simulation_qubitization Open this notebook in GitHub to run it yourself > **Simulating physical and chemical systems** was among the original motivations for quantum computing, as first envisioned by Richard Feynman in 1982, and remains one of its most impactful applications. Time-independent Hamiltonian simulation refers to the task of approximately implementing the unitary evolution operator $e^{-iHt}$ for a Hermitian matrix $H$. When access to the Hamiltonian is provided via block-encoding, this can be realized by applying an appropriate polynomial transformation within a desired precision $\epsilon$. > > **Qubitization** \[1] achieves Hamiltonian simulation by combining the Jacobi-Anger expansion with the Linear Combination of Unitaries (LCU) technique, exploiting the fact that powers of the walk operator directly implement Chebyshev polynomial block-encodings. This approach is entirely constructive- it requires **no classical preprocessing** for rotation angles, but uses more ancilla qubits ($O(\log d)$, where $d$ is the polynomial degree) than GQSP or QSVT. > > * **Input:** A Hermitian operator $H$ given through a block-encoding unitary $U_H$ with scaling factor $\alpha \ge \|H\|$, evolution time $t$, and target error $\epsilon$. > * **Output:** A unitary $U$ approximating $e^{-iHt}$, with $\|U - e^{-iHt}\| < \epsilon$. > > **Complexity:** $O\!\left(\alpha t + \frac{\log \epsilon^{-1}}{\log\!\left(e + \log(\epsilon^{-1}) / \alpha t\right)}\right)$ calls to the block-encoding, using $O(\log d)$ auxiliary qubits. No classical angle preprocessing required. > > *** > > **Keywords:** Hamiltonian Simulation, Block Encoding, Qubitization, Chebyshev Polynomials, Walk Operator, LCU, Oracle/Query complexity. A block-encoded Hamiltonian refers to its embedding within a larger unitary matrix. **Definition**: A $(s, m, \epsilon)$-encoding of a $2^n\times 2^n$ matrix $A$ refers to completing it into a $2^{n+m}\times 2^{n+m}$ unitary matrix $U_{(s,m,\epsilon)-A}$: $$ U_{(s,m,\epsilon)-A} = \begin{pmatrix} A/s & * \\ * & * \end{pmatrix}, $$ with functional error $\left|\left|\left(U_{(s,m,\epsilon)-A}\right)_{0:2^n-1,0:2^n-1}-A/s \right|\right|\leq \epsilon$. Here $s$ is a scaling factor that ensures the overall operator is unitary, $m$ is the number of auxiliary (block) qubits, and $\epsilon$ is the encoding error. This notebook assumes basic knowledge of Linear Combination of Unitaries (LCU) and the PREPARE-SELECT implementation; see the [LCU tutorial](https://github.com/Classiq/classiq-library/blob/main/tutorials/basic_tutorials/quantum_primitives/linear_combination_of_unitaries/linear_combination_of_unitaries.ipynb) for background. Given an exact $(s, m, 0)$-encoding of the Hamiltonian (denoting $U_H \equiv U_{(s,m,0)-H}$), we define the Szegedy quantum walk operator \[2] $$ W \equiv \Pi_{|0\rangle_m}\, U_H, \qquad (1) $$ where $\Pi_{|0\rangle_m}$ reflects about the $|0\rangle$ state of the block variable. The Chebyshev LCU approach presented here relies on the fact that the $k$-th power of walk operator directly implements a Chebyshev polynomial block-encoding of $H$: $$ W^k = \begin{pmatrix} T_k(H/s) & * \\ * & * \end{pmatrix} = U_{(1,m,0)-T_k(H/s)}. \qquad (2) $$ This means we can implement the Hamiltonian simulation as an LCU over the walk operator powers, using the [Jacobi-Anger expansion](https://github.com/Classiq/classiq-library/blob/main/algorithms/hamiltonian_simulation/hamiltonian_simulation_with_block_encoding/jacobi_anger_expansion.ipynb) coefficients ([Eqs. (3)-(4)](https://github.com/Classiq/classiq-library/blob/main/algorithms/hamiltonian_simulation/hamiltonian_simulation_with_block_encoding/jacobi_anger_expansion.ipynb)) as the LCU weights: $$ e^{-iHt} \approx \sum_{k=0}^{d} \beta_k\, T_k(H/s) = \sum_{k=0}^{d} \beta_k\, W^k. $$ The resulting block-encoding has scaling factor $\bar{\beta} = \sum_k |\beta_k|$ and block size $m + \lceil\log_2(d+1)\rceil$: $$ U_{(\bar{\beta},\,\tilde{m},\,\epsilon)-\exp(-iHt)} = \begin{pmatrix} e^{-iHt}/\bar{\beta} & * \\ * & * \end{pmatrix}. $$ This notebook demonstrates Hamiltonian simulation using the Qubitization (Chebyshev LCU) method. For the other approaches, see the companion notebooks on GQSP and QSVT. For a side-by-side comparison of all three methods, see the table at the end of this notebook. ## Preliminaries # ## Setting a Specific Hamiltonian to Evolve We set some specific hyperparameters for our problem. We use a simple Hamiltonian given as a sum of Pauli strings, and the `lcu_pauli` function to block-encode it via the Linear Combination of Unitaries (LCU) technique: $$ H = \sum_{i} \alpha_i U_i, \qquad U_{(\bar{\alpha},m,0)-H} = \begin{pmatrix} H/\bar{\alpha} & * \\ * & * \end{pmatrix}, \qquad \bar{\alpha} = \sum_i |\alpha_i|. $$ *To treat different problems with the same algorithm, simply change theses hyperparameters*. ```python theme={null} import time import matplotlib.pyplot as plt import numpy as np import scipy from classiq import * ``` ```python theme={null} EVOLUTION_TIME = 22 EPS = 1e-7 HAMILTONIAN = ( 0.4 * Pauli.I(0) + 0.1 * Pauli.Z(1) + 0.05 * Pauli.X(0) * Pauli.X(1) + 0.2 * Pauli.Z(0) * Pauli.Z(1) ) print(f"The Hamiltonian to evolve: {HAMILTONIAN}") ``` **Output:** ``` The Hamiltonian to evolve: 0.4 + 0.05*Pauli.X(0)*Pauli.X(1) + 0.2*Pauli.Z(0)*Pauli.Z(1) + 0.1*Pauli.Z(1) ``` Next, we define the block-encoding quantum function, and a Quantum Struct for its variable. ```python theme={null} data_size = HAMILTONIAN.num_qubits block_size = ( (len(HAMILTONIAN.terms) - 1).bit_length() if len(HAMILTONIAN.terms) != 1 else 1 ) BE_SCALING = np.sum( np.abs([term.coefficient for term in HAMILTONIAN.terms]) ) # scaling for LCU of Paulis print(f"Block size: {block_size}") print(f"Block-encoding scaling factor: {BE_SCALING}") class BlockEncodedState(QStruct): data: QNum[data_size] block: QNum[block_size] @qfunc def be_hamiltonian(state: BlockEncodedState): lcu_pauli(HAMILTONIAN * (1 / BE_SCALING), state.data, state.block) ``` **Output:** ``` Block size: 2 Block-encoding scaling factor: 0.75 ``` Finally, we set the initial state to evolve and calculate classically the expected evolved state for verifying the quantum methods. ```python theme={null} state_to_evolve = np.random.rand(2**data_size) state_to_evolve = (state_to_evolve / np.linalg.norm(state_to_evolve)).tolist() matrix = pauli_operator_to_matrix(HAMILTONIAN) expected_state = scipy.linalg.expm(-1j * matrix * EVOLUTION_TIME) @ state_to_evolve ``` # ## Setting Up a Statevector Simulator Working with block-encoding typically requires post-selection of the block register being at state $|0\rangle$. The success of this process can be amplified via Oblivious Amplitude Amplification. In this notebook, instead, we use a statevector simulator and project the result. We import two utility functions from `hamiltonian_simulation_utils`: * `get_projected_state_vector`: extracts the post-selected statevector from the execution results. * `compare_quantum_classical_states`: aligns the global phase and computes the overlap with the classically computed reference. ```python theme={null} from hamiltonian_simulation_utils import ( compare_quantum_classical_states, get_projected_state_vector, ) execution_preferences = ExecutionPreferences( num_shots=1, backend_preferences=ClassiqBackendPreferences( backend_name=ClassiqSimulatorBackendNames.SIMULATOR_STATEVECTOR ), ) ``` # ## The Jacobi-Anger Expansion The LCU coefficients for the Chebyshev polynomials come directly from the [Jacobi-Anger expansion](https://github.com/Classiq/classiq-library/blob/main/algorithms/hamiltonian_simulation/hamiltonian_simulation_with_block_encoding/jacobi_anger_expansion.ipynb) (Eqs. (3)-(4)). We compute the cosine and sine Chebyshev coefficients and combine them into complex coefficients for $e^{-iHt} = \cos(Ht) - i\sin(Ht)$. ```python theme={null} from classiq.applications.qsp.qsp import ( poly_jacobi_anger_cos, poly_jacobi_anger_degree, poly_jacobi_anger_sin, ) t0 = time.perf_counter() cheb_degree = poly_jacobi_anger_degree(EPS, EVOLUTION_TIME * BE_SCALING) poly_cos = poly_jacobi_anger_cos(cheb_degree, EVOLUTION_TIME * BE_SCALING) poly_sin = poly_jacobi_anger_sin(cheb_degree, EVOLUTION_TIME * BE_SCALING) L = max(len(poly_sin), len(poly_cos)) poly_sin_padded = np.pad(poly_sin, (0, L - len(poly_sin))) poly_cos_padded = np.pad(poly_cos, (0, L - len(poly_cos))) # Negative sign: e^{-iHt} = cos(Ht) - i*sin(Ht) exp_coeffs = poly_cos_padded - 1j * poly_sin_padded classical_preprocess_time_cheb_lcu = time.perf_counter() - t0 print(f"Chebyshev degree: {cheb_degree}") print(f"Classical preprocessing time: {classical_preprocess_time_cheb_lcu:.3f} s") ``` **Output:** ``` Chebyshev degree: 33 Classical preprocessing time: 0.001 s ``` ```python theme={null} exp_block_size = (len(exp_coeffs) - 1).bit_length() if len(exp_coeffs) != 1 else 1 print(f"Block size of the block-encoded Hamiltonian evolution: {exp_block_size}") exp_be_scaling = np.sum(np.abs(exp_coeffs)) print(f"Scaling factor for the block-encoded Hamiltonian evolution: {exp_be_scaling}") ``` **Output:** ``` Block size of the block-encoded Hamiltonian evolution: 6 Scaling factor for the block-encoded Hamiltonian evolution: 5.6264790337731325 ``` # ## The Walk Operator > **Note:** The current implementation assumes that the block-encoding unitary $U_H$ is also Hermitian. For the non-Hermitian generalization, see the Technical Notes. For the block-encoding of $H$, $U_{(s,m,0)-H}$, we define the Szegedy quantum walk operator, $W = \Pi_{|0\rangle_m}\, U_{(s,m,0)-H}$, according to Eq. (1) above. ```python theme={null} from classiq.qmod.symbolic import pi @qfunc def my_reflect_about_zero(qba: QNum): control(qba == 0, lambda: phase(pi)) phase(pi) @qfunc def walk_operator( be_qfunc: QCallable[BlockEncodedState], state: BlockEncodedState ) -> None: be_qfunc(state) my_reflect_about_zero(state.block) ``` # ## Verifying the Block-Encoding As a sanity check before the main algorithm, we verify the Hamiltonian block-encoding: we apply $U_H$ on the initial state and check that the post-selected result matches $(H/\bar{\alpha})|\psi\rangle$ as expected. ```python theme={null} @qfunc def main(data: Output[QNum[data_size]], block: Output[QNum[block_size]]): state = BlockEncodedState() allocate(state) inplace_prepare_amplitudes(state_to_evolve, 0.0, state.data) be_hamiltonian(state) bind(state, [data, block]) qprog_be = synthesize(main) show(qprog_be) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3D345OwANKwFbd3NPhrzEpdKQHY ``` Screenshot 2025-10-16 at 15.16.18.png ```python theme={null} TOLERANCE = 1e-9 with ExecutionSession(qprog_be, execution_preferences=execution_preferences) as es: res_be = es.sample() state_result_be = get_projected_state_vector(res_be) expected_state_be = matrix @ state_to_evolve renormalized_be, overlap_be = compare_quantum_classical_states( expected_state_be, state_result_be, BE_SCALING ) print("Expected state:", expected_state_be) print("Resulting state after rescaling:", renormalized_be) assert np.isclose(overlap_be, 1, TOLERANCE) print("=" * 40) print(f"Fidelity is 1 (with {TOLERANCE} tolerance)") ``` **Output:** ``` Expected state: [0.48661409+0.j 0.20780411+0.j 0.06549975+0.j 0.04701937+0.j] Resulting state after rescaling: [0.48661409+4.76606469e-17j 0.20780411-4.89301046e-17j 0.06549975-9.06290967e-17j 0.04701937+5.30017642e-17j] ======================================== Fidelity is 1 (with 1e-09 tolerance) ``` ## Implementation We build an LCU of the unitaries $\{W^k\}$ with coefficients $\{\beta_k\}$. The `select` operator over a series of unitary powers is implemented efficiently: instead of applying $2^l$ multi-controlled operations, we apply $l$ single-controlled operations, where the $i$-th control qubit applies $W^{2^i}$ (analogous to the [QPE](https://github.com/Classiq/classiq-library/blob/main/algorithms/quantum_phase_estimation/qpe_for_matrix/qpe_for_matrix.ipynb) circuit structure). Screenshot 2025-10-12 at 14.44.44.png
Design of a select operation over a series of unitary powers. Instead of applying a series of $2^l$ multi-controlled operations, we can apply $l$ single controlled operations.
```python theme={null} @qfunc def select_powered_unitaries(u: QCallable, block: QArray): repeat(block.len, lambda i: control(block[i], lambda: power(2**i, lambda: u()))) ``` First, we define Quantum Structs for the Qubitization block-encoding. The block part, `QubitizationBlock`, contains the block variable from the Hamiltonian block-encoding, and a block variable on which we apply the PREPARE operation of the LCU to construct the sum of Chebyshev polynomials of $H$. The data variable follows that of the Hamiltonian encoding. In addition, we assemble the `lcu_cheb` function using `prepare_select` with the efficient `select_powered_unitaries` as the select operation. ```python theme={null} class QubitizationBlock(QStruct): block_ham: QNum[block_size] block_exp: QNum[exp_block_size] class QubitizationState(QStruct): data: QNum[data_size] block: QubitizationBlock @qfunc def lcu_cheb( coefs: list[float], be_qfunc: QCallable[BlockEncodedState], state: QubitizationState ): prepare_select( coefficients=coefs, select=lambda lcu_block: select_powered_unitaries( lambda: walk_operator(be_qfunc, [state.data, state.block.block_ham]), lcu_block, ), block=state.block.block_exp, ) ``` The code in the rest of this section builds a model that applies the `lcu_cheb` function on the randomly prepared vector, synthesizes it, executes the resulting quantum program, and verifies the results. ```python theme={null} @qfunc def main( data: Output[QNum[data_size]], block: Output[QNum[block_size + exp_block_size]] ): state = QubitizationState() allocate(state) inplace_prepare_amplitudes(state_to_evolve, 0.0, state.data) lcu_cheb(exp_coeffs, be_hamiltonian, state) bind(state, [data, block]) qprog_cheb_lcu = synthesize(main, preferences=Preferences(optimization_level=1)) show(qprog_cheb_lcu) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3D34BcmBXo9yfTgz7rBvSylBdey ``` Screenshot 2025-10-16 at 15.33.22.png ```python theme={null} with ExecutionSession(qprog_cheb_lcu, execution_preferences) as es: results_cheb_lcu = es.sample() state_result_cheb_lcu = get_projected_state_vector(results_cheb_lcu) exp_scaling_factor_cheb_lcu = exp_be_scaling ``` ```python theme={null} renormalized_state_cheb_lcu, overlap_cheb_lcu = compare_quantum_classical_states( expected_state, state_result_cheb_lcu, exp_scaling_factor_cheb_lcu ) print("Expected state:", expected_state) print("Resulting state after rescaling:", renormalized_state_cheb_lcu) assert np.linalg.norm(renormalized_state_cheb_lcu - expected_state) < EPS print("=" * 40) print("Overlap between expected and resulting state:", overlap_cheb_lcu) ``` **Output:** ``` Expected state: [-0.66939463-0.00189774j 0.58369085-0.33082778j 0.07045108-0.25195773j -0.12292945-0.13493516j] Resulting state after rescaling: [-0.66939463-0.00189774j 0.58369086-0.33082778j 0.07045109-0.25195773j -0.12292945-0.13493516j] ======================================== Overlap between expected and resulting state: 1.0 ``` ## References \[1]: [Berry, D. W., Childs, A. M., & Kothari, R. *Hamiltonian simulation with nearly optimal dependence on all parameters.* In *Proceedings of the 56th IEEE Symposium on Foundations of Computer Science (FOCS)*, pp. 792-809 (2015).](https://doi.org/10.1109/FOCS.2015.54) \[2]: [Szegedy, M. *Quantum speed-up of Markov chain based algorithms.* In *45th Annual IEEE Symposium on Foundations of Computer Science*, pp. 32-41 (2004).](https://ieeexplore.ieee.org/abstract/document/1366222) \[3]: [Lin, L. *Lecture notes on quantum algorithms for scientific computation.* arXiv:2201.08309 \[quant-ph\] (2022).](https://arxiv.org/abs/2201.08309) ## Technical Notes # ## Generalizing to Non-Hermitian Block-Encoding Unitaries The current implementation assumes that the block-encoding unitary $U_H$ is also Hermitian. This assumption underlies the walk operator's spectral properties. For a non-Hermitian block-encoding unitary, an analogous walk operator can be defined as $\tilde{W} \equiv U_H^T \Pi_{|0\rangle_m} U_H \Pi_{|0\rangle_m}$, which satisfies equivalent spectral properties. See Section 7.4 in Ref. \[3] for details. # ## Comparison with Other Methods | Method | extra block qubits | Controlled $U_H$? | Amplitude amplification? | Classical preprocessing | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | ----------------- | --------------------------------------- | ----------------------- | | [GQSP](https://github.com/Classiq/classiq-library/blob/main/algorithms/hamiltonian_simulation/hamiltonian_simulation_with_block_encoding/hamiltonian_simulation_gqsp.ipynb) | 1 | Yes | No | Angle computation | | [QSVT](https://github.com/Classiq/classiq-library/blob/main/algorithms/hamiltonian_simulation/hamiltonian_simulation_with_block_encoding/hamiltonian_simulation_qsvt.ipynb) | 2 | No | Yes (for a factor of 2) | Angle computation | | [Qubitization](https://github.com/Classiq/classiq-library/blob/main/algorithms/hamiltonian_simulation/hamiltonian_simulation_with_block_encoding/hamiltonian_simulation_qubitization.ipynb) | $O(\log d)$ | Yes | Yes (for the sum of Cheb. coefficients) | None | All three methods share the same asymptotic query complexity. Differences in the table reflect the detailed implementation of this specific example. # The Jacobi-Anger Expansion Source: https://docs.classiq.io/explore/algorithms/hamiltonian_simulation/hamiltonian_simulation_with_block_encoding/jacobi_anger_expansion Open this notebook in GitHub to run it yourself The **Jacobi-Anger expansion** is a classical mathematical identity that expresses complex exponentials as infinite series of Bessel functions \[1]. In quantum computing it plays a central role: it is the tool that lets us approximate the time-evolution operator $e^{-iHt}$ as a polynomial in $H$, which is the bridge that makes block-encoding-based Hamiltonian simulation possible. This tutorial covers the expansion formulas, their Chebyshev polynomial form, and how the required polynomial degree scales with the evolution time $t$ and the target approximation error $\epsilon$. For the three quantum algorithms that build on this expansion, see: * [Hamiltonian Simulation with GQSP](https://github.com/Classiq/classiq-library/blob/main/algorithms/hamiltonian_simulation/hamiltonian_simulation_with_block_encoding/hamiltonian_simulation_gqsp.ipynb) * [Hamiltonian Simulation with QSVT](https://github.com/Classiq/classiq-library/blob/main/algorithms/hamiltonian_simulation/hamiltonian_simulation_with_block_encoding/hamiltonian_simulation_qsvt.ipynb) * [Hamiltonian Simulation with Qubitization](https://github.com/Classiq/classiq-library/blob/main/algorithms/hamiltonian_simulation/hamiltonian_simulation_with_block_encoding/hamiltonian_simulation_qubitization.ipynb) ## The Expansion The most general form of the Jacobi-Anger expansion \[1] gives: $$ e^{it\cos(x)} = \sum_{k=-\infty}^{\infty} i^k J_{k}(t)\, e^{ikx}, \qquad (1) $$ $$ e^{it\sin(x)} = \sum_{k=-\infty}^{\infty} J_{k}(t)\, e^{ikx}, \qquad (2) $$ from which we can derive Chebyshev polynomial series for the real-valued functions: $$ \cos(xt) = J_0(t) + 2\sum_{k=1}^{d/2} (-1)^k J_{2k}(t)\, T_{2k}(x), \qquad (3) $$ $$ \sin(xt) = 2\sum_{k=0}^{d/2} (-1)^k J_{2k+1}(t)\, T_{2k+1}(x), \qquad (4) $$ where $J_k(x)$ is the Bessel function of the first kind of order $k$, and $T_k(x)$ is the Chebyshev polynomial of order $k$. Eq. (1) is directly used in the [GQSP method](https://github.com/Classiq/classiq-library/blob/main/algorithms/hamiltonian_simulation/hamiltonian_simulation_with_block_encoding/hamiltonian_simulation_gqsp.ipynb) (applied to the walk operator). Eqs. (3)-(4) are used by both the [QSVT method](https://github.com/Classiq/classiq-library/blob/main/algorithms/hamiltonian_simulation/hamiltonian_simulation_with_block_encoding/hamiltonian_simulation_qsvt.ipynb) and the [Qubitization method](https://github.com/Classiq/classiq-library/blob/main/algorithms/hamiltonian_simulation/hamiltonian_simulation_with_block_encoding/hamiltonian_simulation_qubitization.ipynb). ## Truncation and Error Bound The infinite series in Eqs. (3) and (4) can be truncated at degree $d$, giving polynomial approximations of $\cos(xt)$ and $\sin(xt)$. The required degree for a target approximation error $\epsilon$ and evolution time $t$ is: $$ d = O\!\left(t + \frac{\log(1/\epsilon)}{\log\!\Big(e+\frac{\log(1/\epsilon)}{t}\Big)}\right). \qquad (5) $$ This scaling, linear in $t$ and logarithmic in $1/\epsilon$, is optimal: it matches the quantum query complexity lower bound for Hamiltonian simulation \[2]. This is one of the key reasons the block-encoding family of algorithms is asymptotically optimal. Classiq's QSP application includes all 5 formulas above. Next we demonstrate the approximation for a given evolution time $t$ and error $\epsilon$, for the expansion of the function $\cos(xt)$ and $\sin(xt)$ (Eqs. (3) and (4) above). ```python theme={null} import matplotlib.pyplot as plt import numpy as np from classiq.applications.qsp.qsp import ( poly_jacobi_anger_cos, poly_jacobi_anger_degree, poly_jacobi_anger_sin, ) EVOLUTION_TIME = 22 EPS = 1e-7 degree = poly_jacobi_anger_degree(EPS, EVOLUTION_TIME) print(f"Polynomial degree for t={EVOLUTION_TIME}, ε={EPS}: d = {degree}") ``` **Output:** ``` Polynomial degree for t=22, ε=1e-07: d = 40 ``` ## Approximation Quality We can visually inspect the approximation quality for $t = 22$ and $\epsilon = 10^{-7}$: ```python theme={null} cos_coeffs = poly_jacobi_anger_cos(degree, EVOLUTION_TIME) sin_coeffs = poly_jacobi_anger_sin(degree, EVOLUTION_TIME) xs = np.linspace(0, 1, 100) cos_approx = np.polynomial.Chebyshev(cos_coeffs)(xs) sin_approx = np.polynomial.Chebyshev(sin_coeffs)(xs) fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5)) ax1.plot(xs, np.cos(EVOLUTION_TIME * xs), "-r", linewidth=3, label="cos") ax1.plot(xs, cos_approx, "--k", linewidth=2, label="approx. cos") ax1.plot(xs, np.sin(EVOLUTION_TIME * xs), "-y", linewidth=3, label="sin") ax1.plot(xs, sin_approx, "--b", linewidth=2, label="approx. sin") ax1.set_ylabel(r"$\cos(x)\,\,\, , \sin(x)$", fontsize=16) ax1.set_xlabel(r"$x$", fontsize=16) ax1.tick_params(labelsize=16) ax1.legend(loc="lower left", fontsize=14) ax1.set_xlim(-0.1, 1.1) ax1.set_title("Approximation", fontsize=16) ax2.plot( xs, np.cos(EVOLUTION_TIME * xs) - cos_approx, "-r", linewidth=3, label="cos error" ) ax2.plot( xs, np.sin(EVOLUTION_TIME * xs) - sin_approx, "-y", linewidth=3, label="sin error" ) ax2.set_ylabel("Error", fontsize=16) ax2.set_xlabel(r"$x$", fontsize=16) ax2.tick_params(labelsize=16) ax2.yaxis.get_offset_text().set_fontsize(16) ax2.legend(loc="lower left", fontsize=14) ax2.set_xlim(-0.1, 1.1) ax2.set_title("Approximation Error", fontsize=16) plt.tight_layout(); ``` output We can see that indeed, the approximations follow the exact formulas. ## See Also This expansion is the mathematical foundation used by each of the three Hamiltonian simulation notebooks in this directory: * [Hamiltonian Simulation with GQSP](https://github.com/Classiq/classiq-library/blob/main/algorithms/hamiltonian_simulation/hamiltonian_simulation_with_block_encoding/hamiltonian_simulation_gqsp.ipynb) - applies Eq. (1) as a Laurent polynomial in the walk operator $W = e^{i\arccos(H/s)}$. * [Hamiltonian Simulation with QSVT](https://github.com/Classiq/classiq-library/blob/main/algorithms/hamiltonian_simulation/hamiltonian_simulation_with_block_encoding/hamiltonian_simulation_qsvt.ipynb) - applies Eqs. (3)-(4) as two separate QSVT polynomial transformations, combined via LCU. * [Hamiltonian Simulation with Qubitization](https://github.com/Classiq/classiq-library/blob/main/algorithms/hamiltonian_simulation/hamiltonian_simulation_with_block_encoding/hamiltonian_simulation_qubitization.ipynb) - applies Eqs. (3)-(4) directly as Chebyshev coefficients in an LCU of walk operator powers. ## References \[1]: [Jacobi-Anger Expansion (Wikipedia)](https://en.wikipedia.org/wiki/Jacobi%E2%80%93Anger_expansion) \[2]: [Martyn, J. M., Rossi, Z. M., Tan, A. K., & Chuang, I. L. *Grand unification of quantum algorithms.* PRX Quantum **2**, 040203 (2021).](https://journals.aps.org/prxquantum/abstract/10.1103/PRXQuantum.2.040203) # Overview Source: https://docs.classiq.io/explore/algorithms/index Algorithms and their variants implemented in Classiq's Qmod, grouped by domain. ## Amplitude Amplification and Estimation * [Oblivious Amplitude Amplification](/explore/algorithms/amplitude_amplification_and_estimation/oblivious_amplitude_amplification/oblivious_amplitude_amplification) * [Quantum Monte Carlo Integration (QMCI)](/explore/algorithms/amplitude_amplification_and_estimation/qmc_user_defined/qmc_user_defined) * [Using QSVT for Fixed-point Amplitude Amplification](/explore/algorithms/amplitude_amplification_and_estimation/qsvt_fixed_point_amplitude_amplification/qsvt_fixed_point_amplitude_amplification) * [Quantum Counting Using the Iterative Quantum Amplitude Estimation Algorithm](/explore/algorithms/amplitude_amplification_and_estimation/quantum_counting/quantum_counting) ## Foundational * [Bernstein-Vazirani Algorithm](/explore/algorithms/foundational/bernstein_vazirani/bernstein_vazirani) * [Deutsch-Jozsa Algorithm](/explore/algorithms/foundational/deutsch_jozsa/deutsch_jozsa) * [Quantum Teleportation Algorithm (Protocol)](/explore/algorithms/foundational/quantum_teleportation/quantum_teleportation) * [Simon's Algorithm](/explore/algorithms/foundational/simon/simon) ## Hamiltonian Simulation * [Hamiltonian Simulation](/explore/algorithms/hamiltonian_simulation/hamiltonian_simulation_guide/hamiltonian_simulation_guide) * [Hamiltonian Simulation with Generalized Quantum Signal Processing (GQSP)](/explore/algorithms/hamiltonian_simulation/hamiltonian_simulation_with_block_encoding/hamiltonian_simulation_gqsp) * [Hamiltonian Simulation with Quantum Singular Value Transformation (QSVT)](/explore/algorithms/hamiltonian_simulation/hamiltonian_simulation_with_block_encoding/hamiltonian_simulation_qsvt) * [Hamiltonian Simulation with Qubitization (Chebyshev LCU)](/explore/algorithms/hamiltonian_simulation/hamiltonian_simulation_with_block_encoding/hamiltonian_simulation_qubitization) * [The Jacobi-Anger Expansion](/explore/algorithms/hamiltonian_simulation/hamiltonian_simulation_with_block_encoding/jacobi_anger_expansion) ## Metrology * [Classical Shadow Tomography](/explore/algorithms/metrology/classical_shadow_tomography) ## Number Theory and Cryptography * [Discrete Logarithm](/explore/algorithms/number_theory_and_cryptography/discrete_log/discrete_log) * [Solving Elliptic Curve Discrete Logarithm Problem with Shor's Algorithm](/explore/algorithms/number_theory_and_cryptography/elliptic_curves/elliptic_curve_discrete_log) * [Hidden-Shift Problem for Bent Functions](/explore/algorithms/number_theory_and_cryptography/hidden_shift/hidden_shift) * [Shor's Factoring Algorithm](/explore/algorithms/number_theory_and_cryptography/shor/shor) ## QML * [Hybrid Classical-Quantum Neural Network](/explore/algorithms/QML/hybrid_qnn/hybrid_qnn_for_subset_majority) * [Quantum Generative Adversarial Networks (QGANs)](/explore/algorithms/QML/qgan/qgan_bars_and_strips) * [Quantum Support Vector Machines (QSVM)](/explore/algorithms/QML/qsvm/qsvm) * [Quantum Autoencoder](/explore/algorithms/QML/quantum_autoencoder/quantum_autoencoder) ## Quantum Differential Equations Solvers * [Quantum Algorithm for Solving the Poisson Equation](/explore/algorithms/quantum_differential_equations_solvers/discrete_poisson_solver/discrete_poisson_solver) * [Linear Combination of Hamiltonian Simulation (LCHS)](/explore/algorithms/quantum_differential_equations_solvers/lchs/lchs) * [Time Marching Based Quantum Solvers for Time-dependent Linear Differential Equations](/explore/algorithms/quantum_differential_equations_solvers/time_marching/time_marching) ## Quantum Linear Solvers * [Solving the Quantum Linear Systems Problem (QLSP) using AQC](/explore/algorithms/quantum_linear_solvers/adiabatic_linear_solvers/solving_qlsp_with_aqc) * [HHL Algorithm](/explore/algorithms/quantum_linear_solvers/hhl/hhl) * [Matrix Inversion with Quantum Singular Value Transform (QSVT)](/explore/algorithms/quantum_linear_solvers/qsvt_matrix_inversion/qsvt_matrix_inversion) * [Variational Quantum Linear Solver (VQLS) with Linear Combination of Unitaries (LCU) Block Encoding](/explore/algorithms/quantum_linear_solvers/vqls/vqls_with_lcu) ## Quantum Phase Estimation * [Quantum Phase Estimation for Solving Matrix Eigenvalues](/explore/algorithms/quantum_phase_estimation/qpe_for_matrix/qpe_for_matrix) * [Qubitization based Quantum Phase Estimation (QPE) for Solving Molecular Energies](/explore/algorithms/quantum_phase_estimation/qpe_with_qubitization/qpe_for_molecule_with_qubitization) ## Quantum Primitives * [Generalized Quantum Signal Processing (GQSP)](/explore/algorithms/quantum_primitives/gqsp/gqsp) * [Fast Quantum Algorithm for Numerical Gradient Estimation](/explore/algorithms/quantum_primitives/gradient_estimation/gradient_estimation) * [Hadamard Test](/explore/algorithms/quantum_primitives/hadamard_test/hadamard_test) * [Quantum Oracle Sketching for Boolean Functions](/explore/algorithms/quantum_primitives/quantum_oracle_sketching_boolean/quantum_oracle_sketching_boolean) * [Swap Test Algorithm](/explore/algorithms/quantum_primitives/swap_test/swap_test) ## Quantum State Preparation * [ADAPT VQE](/explore/algorithms/quantum_state_preparation/adapt_vqe/adapt_vqe) * [Preparation of Fermionic Gaussian States](/explore/algorithms/quantum_state_preparation/fermionic_gaussian/fermionic_gaussian) * [Quantum Thermal State Preparation Algorithm Implementation](/explore/algorithms/quantum_state_preparation/gibbs/quantum_thermal_state_preparation) ## Quantum Walks * [Glued Trees Algorithm](/explore/algorithms/quantum_walks/glued_trees/glued_trees) ## Search and Optimization * [Quantum Approximate Optimization Algorithm](/explore/algorithms/search_and_optimization/QAOA/qaoa) * [Decoded Quantum Interferometry Algorithm](/explore/algorithms/search_and_optimization/dqi/dqi_max_xorsat) * [Grover's Search Algorithm](/explore/algorithms/search_and_optimization/grover/grover) * [Grover Mixers for QAOA](/explore/algorithms/search_and_optimization/grover_mixer_qaoa/gm_qaoa) * [Quantum Likelihood Estimation](/explore/algorithms/search_and_optimization/quantum_likelihood_estimation/quantum_likelihood_estimation) # Classical Shadow Tomography Source: https://docs.classiq.io/explore/algorithms/metrology/classical_shadow_tomography Open this notebook in GitHub to run it yourself *This notebook is based on the original contribution by Georgiy Zemlevskiy ([ee57b988](https://github.com/Classiq/classiq-library/commit/ee57b988), [1c8085e4](https://github.com/Classiq/classiq-library/commit/1c8085e4), [5e26a4be](https://github.com/Classiq/classiq-library/commit/5e26a4be)).* ## Overview > **Classical shadow tomography** \[[1](#ref1)], introduced by Huang, Kueng, and Preskill (2020), is a measurement protocol for predicting many properties of an unknown quantum state from a small number of measurements. Rather than reconstructing the full density matrix, it builds a compact classical estimator, the *classical shadow*, that supports efficient prediction of a large set of observables. > > The algorithm treats the following problem: > > * **Input:** Access to copies of an unknown $n$-qubit state $\rho$, and a target set of $M$ observables $\{O_1, \dots, O_M\}$. > * **Output:** Estimates $\hat{o}_i \approx \langle O_i \rangle = \mathrm{tr}(O_i \rho)$ for every $i$, to additive error $\varepsilon$, with a probability of at least $1-\delta$. > > **Complexity:** The sample cost, which is the number of required measurements (or copies of $\rho$), scales as $\mathcal{O}(\log(M/\delta) \max_i ||O_i ||^2_{\text{shadow}}/\epsilon^2)$. This scaling is logarithmic in the number of target observables, $M$. Here $||O_i ||_{\text{shadow}}$ is the *shadow norm* which depends on both the specific method used to construct the classical shadow and the observables of interest, $\{O_i\}$. When Pauli measurements are utilized to build the classical shadow it scales as $\mathcal{O}(4^k)$, for $k$-local Pauli observables, while, when the algorithm involves random-Clifford measurements, the classical shadow scales as the Hilbert-Schmidt norm $\mathcal{O}(\|O\|_\mathrm{HS}^2)$. In comparison, the naive approach of measuring each time a different observable, would require (using Hoeffding inequality) $\mathcal{O}(M\log(M/\delta)/\epsilon^2)$ measurements. > > *** > > **Keywords:** Quantum state tomography, Randomized measurements, Shadow norm, Median-of-means estimator. ## Introduction $$ \renewcommand{\ket}[1]{\left|{#1}\right\rangle} \renewcommand{\bra}[1]{\left\langle{#1}\right|} $$ Predicting properties of unknown quantum states is an essential task in quantum computing. By conducting many measurements in different bases on copies of the state a method called Quantum State Tomography \[[1](#ref1)] fully reconstructs the quantum state. Naturally, such complete characterization requires a number of state copies which scales exponentially, with the number of qubits $n$ and quadratically with the error, $\mathcal{O}(2^{2n}/\epsilon^2)$. As a result, this procedure is unfeasible for a system of more than a few qubits. Nevertheless, for many applications a complete characterization of the state, or equivalently, evaluation of the expectation value of an exponential number of observables is unnecessary. When restricting to $M$ observables, one can characterize only a part of the Hilbert space and achieve better complexity. Aaronson introduced shadow tomography \[[2](#ref2)] which constructs a compact classical representation of the quantum state, coined as classical shadow. The shadow allows predicting observables simultaneously without completely reconstructing the state. This could be done using $\widetilde{\mathcal{O}}\left(\varepsilon^{-4} \cdot \log^4 M \cdot n\right)$ copies of the state. However, this approach still requires a lot of quantum memory to store the copies and long circuits to make the predictions. Based on this idea Huang, et al. introduced classical shadow tomography \[[3](#ref3)], a procedure that required only $\mathcal{O}(\log(M))$ copies of the state and independent of the dimension of the system to approximate the classical shadow. The copies could also be stored efficiently in classical memory. Meaning that one can estimate exponentially many observables using only logarithmically many measurements in the number of observables. These can be used to estimate fidelity, the entanglement entropy, or find entanglement witnesses. The procedure consists of two steps: 1. Data acquisition - constructing **snapshots** of the quantum state, and construction of a classical representation of the quantum state, i.e, the classical shadow. 2. Prediction * Given $M$ observables $O_1, \dots, O_M$, estimate their expectation values $\langle O_i \rangle = \text{tr}(O_i\rho)$. The Classical Shadow Procedure (from Ref. \[[1](#ref1)]):
Classical-shadow tomography pipeline: random unitaries, measurements, classical representation, prediction
## Procedure # ## Data Acquisition One step of the data acquisition procedure consists of the following: * Select a random unitary, $U$, transformation from a predefined, tomographically complete ensemble. * Apply the transformation to the quantum state of interest: $$ \rho \rightarrow U\rho U^\dagger $$ * Measure the transformed quantum state in the computational basis and obtain a bit string of measurements $|b\rangle \in \{0, 1\}^n$. * Store the $\textit{classical snapshot}$ of the state by classically performing the reverse operation $U^\dagger |b\rangle \langle b| U$ and storing (efficiently) it in classical memory. The average mapping (in terms of unitary choice and measurement result) from $\rho$ to its snapshot can be viewed as a measurement channel: $$ \mathcal{M}(\rho) = \mathbb{E}[U^\dagger |b\rangle \langle b| U] = \mathbb{E}_{U\sim{\cal{U}}}\sum_{b\in \{0,1 \}^n} \langle b| U \rho U^\dagger|b \rangle U^\dagger |b\rangle \langle b|U ~~, $$ 'Average mapping', means that the true state is obtained from the estimated state in expectation: $$ \mathbb{E}[\hat{\rho}] = \rho~~. $$ When the unitary ensemble (which is averaged over) is tomographically complete, so the quantum state can be fully reconstructed from the measurements, the measurement channel can be inverted. This allows obtaining the original state: $$ \rho = \mathbb{E}[\mathcal{M}^{-1}(U^\dagger |b \rangle \langle b| U)]~~. $$ To take advantage of this, this procedure is repeated $N$ times, yielding an array of $\textit{classical shadow}$ of $\rho$: $$ \textbf{S}(\rho; N) = \{\hat{\rho}_1,\dots, \hat{\rho}_N \}~~, $$ where $$ \hat{\rho}_i = \mathcal{M}^{-1}(U_i^\dagger |b_i\rangle \langle b_i| U_i)~~,~~\text{for all} ~~i\in[1,N]~~. $$ # ## Prediction Although the observables can be predicted using an empirical mean, the authors use a median-of-means approach in order to mitigate outliers: * Divide the shadow into $K$ equally sized parts, and take the mean of each one to construct $K$ estimators of $\rho$ $$ \hat{\rho}_{(k)} = \frac{1}{\left\lfloor N / K \right\rfloor} \sum_{i=(k-1)\left\lfloor N / K \right\rfloor + 1}^{k \left\lfloor N / K \right\rfloor} \hat{\rho}_i \tag{1} $$ * Then, for each observable (for $i =1$ to $M$), take the median of the means, where $\hat{O_i}$ is the estimation of the observable: $$ \hat{O_i}(N, K) = \text{median}\{\text{tr} (O_i \hat{\rho}_1), ..., \text{tr} (O_i \hat{\rho}_K)\} \tag{2} $$ Huang et al. proved that the classical shadow protocol allows predicting $M$ target functions $\text{tr}(O_i \rho)$ within error $\epsilon$ with $$ \mathcal{O}(\log(M) \max_i ||O_i ||^2_{\text{shadow}}/\epsilon^2)~~, \tag{3} $$ where the shadow norm $\|O_i \|^2_{\text{shadow}}$ affects the sample complexity and the accuracy of prediction. An important ingredient in the protocol is the ability to sample random unitaries $U$. Formally, one needs to sample from the Haar measure. However, sampling exactly from this measure requires exponential circuit size in the number of qubits. So in practice, one often uses other unitary ensembles, such as the Clifford group, which efficiently reproduces the first (mean) and second (variance) moments of Haar randomness (formally, the Clifford group forms an exact *unitary 3-design*). We consider two ensembles: 1. **Random Pauli measurements**: each qubit is rotated by an independently sampled single-qubit Clifford gate, $U = U_1 \otimes \dots \otimes U_n$. This is equivalent to random Pauli measurements on each qubit. In this case the inverse measurement channel factorizes across the qubits and reads $ \mathcal\{M\}^\{-1\}\!\bigl(U^\{\dagger\}\,|\hat b\rangle\!\langle\hat b|\,U\bigr) \;=\; \bigotimes_\{j=1\}^\{n\} \Bigl(\, 3\,U_j^\{\dagger\}\,|\hat b_j\rangle\!\langle\hat b_j|\,U_j \;-\; \mathbb\{I\}\,\Bigr)~~ , \tag\{4\}$ and the shadow norm of a $k$-local observable scales as $ \|O\|_\{\mathrm\{shadow\}\}^\{2\} \;=\; 3^\{\,k\}\,, \qquad \text\{(bounded by \} 4^\{k\}\,\|O\|_\{\infty\}^\{2\}\text\{ in general)\}~~,$ i.e., it scales exponentially with the locality (weight) of the observable. This channel can be implemented with a circuit of depth one, but the sample complexity (number of measurements) scales exponentially with the weight of the observables. 2. **The $n$-qubit Clifford group $\mathrm{Cl}(2^n)$**. Here the average measurement channel is the depolarizing channel \[[4](#ref4)] $\mathcal{M}(\rho) = \mathcal{D}_{1/(2^n+1)}(\rho)$, and the inverse of the measurement channel reads $\mathcal\{M\}^\{-1\}\!\bigl(U^\{\dagger\}\,|\hat b\rangle\!\langle\hat b|\,U\bigr) \;=\; (2^\{n\}+1)\,U^\{\dagger\}\,|\hat b\rangle\!\langle\hat b|\,U \;-\; \mathbb\{I\}~~.$ The shadow norm is equivalent to the Hilbert-Schmidt norm of the traceless part of the observable, $O_0 = O - \mathrm{tr}(O)\,\mathbb{I}/2^{n}$, with the two-sided bound $\mathrm\{tr\}\!\bigl(O_0^\{2\}\bigr) \;\le\; \|O_0\|_\{\mathrm\{shadow\}\}^\{2\} \;\le\; 3\,\mathrm\{tr\}\!\bigl(O_0^\{2\}\bigr)~~,$ so the shadow norm is independent of the system size $n$ (up to the constant 3) - efficient for *global* observables such as fidelity. Implementation of random $n$-qubit Clifford circuits scales well and estimates global observables efficiently, but requires many more ($n^2/\log(n)$) entangling gates to implement. In both cases, the snapshots can be stored efficiently using the stabilizer formalism. For pedagogical clarity this notebook represents Clifford unitaries as dense $2^{n}\times 2^{n}$ matrices, so the inverse-channel application and observable estimator cost $\mathcal{O}(2^{2n})$ per snapshot (after the row-of-$U$ optimization used below). The same transformations can be carried out in $\mathcal{O}(n^{2})$ per snapshot via the symplectic-tableau representation of Clifford circuits due to Aaronson and Gottesman \[[5](#ref5)] - the regime in which the protocol's $\mathcal{O}(\log M)$ sample-complexity advantage becomes a real wall-clock speedup. To demonstrate the classical shadow procedure, we will build a classical shadow, fully reconstruct a state, and predict an observable. ## Building a Classical Shadow with Classiq In this example, we will construct a classical shadow of the $\Phi^-$ bell state, utilizing the random Pauli and random Clifford measurement channels. We begin by describing the random Pauli measurement channel in the first section and continue by analyzing the Clifford measurement channel. For each measurement channel, we first introduce the quantum functions required to implement the quantum part of the shadow protocol. Then we present the classical processing functions, which construct the classical shadow. Utilizing the shadows, the full state is constructed and compared to the ground truth. Generally, state construction even with the classical shadow is inefficient, nevertheless, the construction demonstrates the strength of the method. Following, we utilize the classical shadow representation to estimate the expectation value of $O = Z\otimes Z$, and finally, we evaluate the error bound, lower bounding the number of measurements (sample complexity) required to achieve an accurate estimation with high probability. The Pauli measurement channel (with $400$ samples) produced a closer state reconstruction, but did not achieve an accurate estimation of the expectation value of $Z\otimes Z$. In contrast, the Clifford measurement channel (with only $50$ samples) achieved machine precision for the expectation value estimation, but produced a state with larger distance from the target Bell state (this result is not coincidental but stems from the fact that the Bell state is a stabilizer state and $Z\otimes Z$ is one of its $+1$ stabilizers). ```python theme={null} import numpy as np from classiq import * ``` ```python theme={null} np.random.seed(552) NUM_QUBITS = 2 ``` # ## Classical Shadow with Pauli Measurement Channel We begin by preparing the $\Phi^{-}$ Bell state and then rotating each qubit so that it corresponds to a randomly chosen Pauli measurement basis. The tomographically complete set $\{H,\,HS,\,I\}$ corresponds to measurements in the $X$, $Y$, $Z$ bases respectively. A unitary ensemble of tensor products of single-qubit Clifford circuits is easy to implement on NISQ hardware, performs well when estimating local observables, but scales poorly (see discussion above). Rather than baking each random basis choice into a fresh circuit at synthesis time, which forces one synthesis per snapshot and dominates the wall-clock time, we express the per-qubit rotation as $R_Y(\theta)\,R_Z(\phi)$ and pass $(\theta_j,\phi_j)$ as **classical execution-time parameters**. The program is then synthesized **once** and executed in a single `ExecutionSession.batch_sample` call that submits all snapshots together. The mapping from basis index to angles is: | basis | gate (up to global phase) | $\theta$ | $\phi$ | | :---: | :-----------------------: | :------: | :------: | | $X$ | $H$ | $\pi/2$ | $\pi$ | | $Y$ | $HS$ | $\pi/2$ | $3\pi/2$ | | $Z$ | $I$ | $0$ | $0$ | The angles $\phi$ and $\theta$ are chosen from this set. The mapping is encoded by the `BASIS_ANGLES` numpy array. ```python theme={null} BASIS_ANGLES = np.array( [ [np.pi / 2, np.pi], [np.pi / 2, 3 * np.pi / 2], [0.0, 0.0], ], dtype=float, ) ``` ```python theme={null} @qfunc def main( thetas: CArray[CReal, NUM_QUBITS], phis: CArray[CReal, NUM_QUBITS], qarr: Output[QArray], ) -> None: """ Prepares the |Phi^-> Bell state and applies the parametric per-qubit basis-change rotation. The rotation angles are bound at execution time, one parameter set per snapshot, so a single synthesized program is reused across the entire shadow. The circuit realizes a measurement in the X, Y, or Z basis (up to global phase). Because the angles are bound at execution time, the circuit is independent of the random basis choice and only needs to be synthesized once. """ prepare_bell_state(1, qarr) # performing a basis change by applying RY and RZ rotations with the angles specified in thetas and phis for i in range(NUM_QUBITS): RZ(phis[i], qarr[i]) RY(thetas[i], qarr[i]) ``` ```python theme={null} qprog = synthesize(main) show(qprog) ``` Below, `calculate_shadow_pauli()` synthesizes the parametric program once, samples the random measurement bases, and submits all snapshots in a single `sample` call. The snapshots contain the measurement results (bits) and unitary operations are stored as basis identifiers, in `ids` list. ```python theme={null} def calculate_shadow_pauli(num_snapshots, num_qubits): """ Computes a Pauli-shadow of size `num_snapshots` from a single synthesized program. The per-qubit basis-change angles are sampled in Python and submitted in one `sample` call. Args: num_snapshots (int): Size of the shadow, also the number of measurements. num_qubits (int): Number of qubits in the system. Returns: snapshots (list[list[int]]): per-snapshot measurement bits (`num_qubits` long each). ids (list[list[int]]): per-snapshot basis identifiers in {0, 1, 2} (X, Y, Z) for each qubit. """ # 1) Sample the random measurement bases in Python (no need to bake them # into separate circuits). basis_id_array = np.random.randint(3, size=(num_snapshots, num_qubits)) ids = [[int(b) for b in row] for row in basis_id_array] # 2) Translate each random basis row into RY/RZ angles via BASIS_ANGLES. param_batch = [ { "thetas": BASIS_ANGLES[basis_id_array[i], 0].tolist(), "phis": BASIS_ANGLES[basis_id_array[i], 1].tolist(), } for i in range(num_snapshots) ] # 3) Execute every snapshot in one cloud round-trip via `sample`. With a # list of parameter sets, `sample` returns one DataFrame per element; # with `num_shots=1`, each frame has exactly one row whose `qarr` # entry is the measured bitstring as a list of ints. result_dfs = sample( qprog, parameters=param_batch, num_shots=1, random_seed=int(np.random.randint(1e6)), ) snapshots = [list(df["qarr"].iloc[0]) for df in result_dfs] return snapshots, ids ``` ```python theme={null} num_snapshots = 400 snapshots, ids = calculate_shadow_pauli(num_snapshots, NUM_QUBITS) print(f"first ten snapshots: {snapshots[:10]}") print(f"first ten ids: {ids[:10]}") ``` **Output:** ``` Submitting job to simulator Job: https://platform.classiq.io/jobs/75d525ba-3ae8-48e2-85e7-26c946edd6fe ``` **Output:** ``` first ten snapshots: [[1, 1], [1, 0], [1, 1], [0, 0], [0, 0], [0, 1], [1, 0], [1, 1], [0, 1], [0, 1]] first ten ids: [[2, 0], [2, 1], [2, 2], [2, 2], [0, 2], [2, 0], [0, 0], [2, 0], [1, 2], [0, 1]] ``` This is the end of the quantum part of the procedure - the snapshots are all we need to extract information about the state. We will now demonstrate the following uses of these snapshots: * Full Tomography: reconstruct the state completely (not just certain pieces of information) as an instructive example. This application doesn't offer an advantage over quantum state tomography, requiring exponential resources. * Estimating Observables: estimating the expectation values of observables by reconstructing only parts of the state. This is one of the applications where classical shadow offers an advantage. # ### State Reconstruction The two functions below perform a full state reconstruction, given enough snapshots. To reconstruct the state, we take the measurement results in `snapshots` and apply the inverse of the operations stored in `ids`. It turns out that since each of our possible unitaries is a tensor product of randomly selected single-qubit Clifford gates, we can apply inverse of the channel Eq. (4) $$ \hat{\rho} = \bigotimes_{j=1}^{n} ( 3 U_j^\dagger |b_j\rangle \langle b_j| U_j - \mathbb{I})~~. $$ In `snapshot_reconstruction_pauli()` below, we apply this equation to get the density matrix of the reconstructed state. `reconstruction_pauli()` averages these density matrices to get an accurate estimation of the initial state $\rho$. ```python theme={null} def snapshot_reconstruction_pauli(snapshot, snapshot_ids, num_qubits): """ Reconstructs the state density matrix from one snapshot. Helper for `reconstruction_pauli()`. Args: snapshot (list[int]): Element of snapshots; `num_qubits` long. snapshot_ids (list[int]): Element of the `ids` list returned by `calculate_shadow_pauli`. num_qubits (int): Number of qubits in the system. Returns: final_state (np.ndarray): Snapshot density matrix reconstruction. """ # Density matrices of computational basis states zero_state = np.array([[1, 0], [0, 0]]) one_state = np.array([[0, 0], [0, 1]]) # S, H, I matrices S_matrix = np.array([[1, 0], [0, 1j]], dtype=complex) H_matrix = (1 / np.sqrt(2)) * np.array([[1, 1], [1, -1]]) I_matrix = np.array([[1, 0], [0, 1]]) # Unitaries applied in circuit. unitaries = [H_matrix, H_matrix @ S_matrix, I_matrix] final_state = [1] for i in range(num_qubits): state = zero_state if snapshot[i] == 0 else one_state U = unitaries[int(snapshot_ids[i])] # Applying Eq. (4). local_state = 3 * (U.conjugate().transpose() @ state @ U) - I_matrix final_state = np.kron(final_state, local_state) return final_state ``` ```python theme={null} def reconstruction_pauli(in_snapshots, in_ids, num_qubits): """ Performs a full reconstruction of the quantum state density matrix, where the quantum state is that prepared in `prep_state()`. Args: in_snapshots (list): List of snapshots returned by `calculate_shadow_pauli` (list of ints in {0, 1}). in_ids (list): List of unitary identifiers returned by `calculate_shadow_pauli` (list of ints in {0, 1, 2}). num_qubits (int): Number of qubits in the system. Returns: reconstructed_state (np.ndarray): Full state density matrix reconstruction (2d numpy complex array). """ # Average over snapshots. reconstructed_state = np.zeros((2**num_qubits, 2**num_qubits), dtype=complex) for i in range(num_snapshots): result = snapshot_reconstruction_pauli(in_snapshots[i], in_ids[i], num_qubits) reconstructed_state += result return reconstructed_state / num_snapshots ``` ```python theme={null} reconstructed_state = reconstruction_pauli(snapshots, ids, NUM_QUBITS) print(f"Full state density matrix: \n {np.round(reconstructed_state, decimals=6)}\n") bell_state_density_matrix = np.array( [[0.5, 0, 0, -0.5], [0, 0, 0, 0], [0, 0, 0, 0], [-0.5, 0, 0, 0.5]] ) print(f"Bell State Density Matrix: \n {bell_state_density_matrix}\n") ``` **Output:** ``` Full state density matrix: [[ 0.424375+0.j -0.005625+0.075j -0.03 +0.001875j -0.5175 -0.016875j] [-0.005625-0.075j 0.090625+0.j -0.01125 -0.050625j -0.01875 +0.001875j] [-0.03 -0.001875j -0.01125 +0.050625j 0.004375+0.j 0.050625-0.00375j ] [-0.5175 +0.016875j -0.01875 -0.001875j 0.050625+0.00375j 0.480625+0.j ]] Bell State Density Matrix: [[ 0.5 0. 0. -0.5] [ 0. 0. 0. 0. ] [ 0. 0. 0. 0. ] [-0.5 0. 0. 0.5]] ``` With 200 snapshots the output density matrix looks similar to the $\Phi^-$ bell state density matrix. To quantitatively compare the reconstructed state to the true state, we will use the cell below to compute the 'distance' (the Frobenius norm) between the two states. ```python theme={null} print(f"Comparing to: \n {bell_state_density_matrix}\n") # distance = norm(ref_state_density_matrix, reconstructed_state) difference = bell_state_density_matrix - reconstructed_state distance = np.linalg.norm(difference, "fro") print(f"Distance: {np.round(distance, decimals=6)}") ``` **Output:** ``` Comparing to: [[ 0.5 0. 0. -0.5] [ 0. 0. 0. 0. ] [ 0. 0. 0. 0. ] [-0.5 0. 0. 0.5]] Distance: 0.199679 ``` The accuracy of the reconstruction increases with the number of snapshots. Test different values for `num_snapshots` to see for yourself. The execution time scales linearly with the number of snapshots. Although completely reconstructing a state using the classical shadow procedure is an instructive example, the procedure doesn't offer much advantage when doing so. Classical shadow shine when estimating many observables. # ### Estimating Observables In this example, we will assume that our observables are only made up of Pauli $I, X, Y, Z$ operators. That is, $O = \bigotimes_j^n \sigma_j$, where $n$ is the number of qubits. Recall that to estimate an observable, we compute the median of observable estimators. Each estimator is the product of the observable and the mean of the estimated states in the median-of-means division (Eq. (2)). For each of $\text{tr} (O \hat{\rho}_i)$, we must compute the estimated states from the snapshots in the shadow. For this, we will apply Eq. (4) $$ \text{tr}(O \hat{\rho}_i) = \text{tr}\left(\bigotimes_{n=1}^{j} \sigma_j (3 U_j^\dagger |\hat{b}_j\rangle \langle \hat{b}_j| U_j - \mathbb{I})\right) $$ $$ =\prod_{j=1}^{n} \ \text{tr}\left( \sigma_j (3 U_j^\dagger |\hat{b}_j\rangle \langle \hat{b}_j| U_j - \mathbb{I})\right)~~. $$ When the basis matches the observable, e.g. $\sigma_j = X$, and we measured in the $X$ basis (applied $H$ in the circuit), the trace evaluates to $3$ when $|\hat{b}_j\rangle = |0\rangle$ and $-3$ when $|\hat{b}_j\rangle = |1\rangle$. If $\sigma_j = I$, the trace evaluates to $1$. Otherwise, the trace evaluates to $0$, making the product $0$. ```python theme={null} def estimate_observable_pauli(in_snapshots, in_ids, observable, div, num_qubits): """ Estimates the expectation value of an observable using a classical shadow. Args: in_snapshots (list[int]): List of snapshots returned by `calculate_shadow_pauli`. in_ids (list[int]): List of unitary identifiers returned by `calculate_shadow_pauli`. observable (SparsePauliOp): Observable to be estimated. div (int): Number of divisions for median-of-means. num_qubits (int): Number of qubits in the system. Returns: expval (float): Esimated expectation value. """ sum = 0 in_snapshots = np.array(in_snapshots) in_ids = np.array(in_ids) # Loop takes care of observables that are sums. for element in observable.terms: means = [] ops = [] # operators in observable # Pauli.I = 0, Pauli.X = 1, Pauli.Y = 2, Pauli.Z = 3 for i in range(num_qubits): if i < len(element.paulis): ops.append( element.paulis[i].pauli - 1 ) # Subtracting 1 to stay consistent with format of ids, where X, Y, Z correspond to 0, 1, 2. else: ops.append(-1) # Add I operator if no operator present. ops = np.array(ops) for i in range(0, num_snapshots, num_snapshots // div): # Divide in_snapshots and in_ids into div chunks. in_snapshots_div, in_ids_div = ( in_snapshots[i : i + num_snapshots // div], in_ids[i : i + num_snapshots // div], ) prods = [] for j in range(num_snapshots // div): # Terms that will be in the product. terms = np.ones_like(ops) # Create masks for identity, matching, and non-matching operators. non_id_mask = ops != -1 match_mask = ops == in_ids_div[j] mismatch_mask = (~match_mask) & non_id_mask # Handle matches. For X (id=0) and Z (id=2), the basis-change # unitary maps the +1 eigenstate to |0>, so snapshot=0 -> +3 # and snapshot=1 -> - 3. For Y (id=1), the HS rotation maps # |+y> to |1> and |-y> to |0>, so the signs are reversed. y_match = match_mask & (ops == 1) xz_match = match_mask & (ops != 1) terms[xz_match & (in_snapshots_div[j] == 0)] = 3.0 terms[xz_match & (in_snapshots_div[j] == 1)] = -3.0 terms[y_match & (in_snapshots_div[j] == 0)] = -3.0 terms[y_match & (in_snapshots_div[j] == 1)] = 3.0 # Handle mismatches. terms[mismatch_mask] = 0.0 prods.append(np.prod(terms)) means.append(np.mean(prods)) sum += np.median(means) * element.coefficient return sum ``` In this example, we will estimate the expectation value of the observable $Z \otimes Z$. Due to the low number of snapshots (as we will see in the next section), the estimation may not be very accurate, and will vary with different sets of snapshots. ```python theme={null} observable = Pauli.Z(0) * Pauli.Z(1) estimate = estimate_observable_pauli( snapshots, ids, observable, div=2, num_qubits=NUM_QUBITS ) print(f"Estimated (Pauli shadow): {estimate:.4f}") print(f"Error relative to ground truth value of 1.0: {1.0 - estimate:.4f}") ``` **Output:** ``` Estimated (Pauli shadow): 0.8100 Error relative to ground truth value of 1.0: 0.1900 ``` To compute the number of snapshots we need to accurately predict $M$ observables with probability $1-\delta$ that the error is $\epsilon$, we use Eq. S13 from Ref. [3](#ref3): $$ \tag{5} K = 2\log(\frac{2M}{\delta}) \ \text{and} \ N = \frac{34}{\epsilon^2} \max_{1\le i \le M} \| O_i - {\frac{\text{tr} (O_i)} {2^n}}\mathbb{I} \| _{\text{shadow}}^2 ~~. $$ For the random-Pauli ensemble, the shadow norm of a $k$-local Pauli string $P$ is $$ \|P\|_{\text{shadow}}^2 \;=\; 3^{\,k}\,, \qquad \text{(bounded by } 4^{k}\,\|P\|_\infty^2 \text{ in general),} $$ so the **locality factor $3^{\,k}$** dominates the sample cost. For a sum of Pauli terms $O = \sum_a c_a P_a$, the triangle inequality gives the upper bound $$ \|O\|_{\text{shadow}} \;\le\; \sum_a |c_a|\,3^{\,\text{weight}(P_a)/2}~~. $$ $NK$ snapshots are sufficient to accurately predict $M$ observables, using the median-of-means algorithm with $K$ divisions such that $$ |\hat{o_i}(N, K) - \text{tr} (O_i \rho)| \le \epsilon \ \ \forall \ \ 1 \le i \le M \ \ \text{ with probability} \ge 1-\delta~~. $$ The `error_bound_pauli()` function below implements this equation for the case of an ensemble consisting of Pauli basis measurements: it computes the per-term locality $k$ and uses the shadow-norm bound $3^{\,k}$ above. For other ensembles the shadow norm has a different form (e.g. the Hilbert-Schmidt bound for the random-Clifford ensemble). ```python theme={null} def error_bound_pauli(error, observables, delta): """ Calculates the minimum shadow size that guarantees a given error and probability of failure for a unitary ensemble consisting of Pauli basis measurements. Implements Eq. (5) with the random-Pauli shadow-norm bound for a sum of Pauli terms, || sum_a c_a P_a ||_shadow^2 <= ( sum_a |c_a| * 3^(weight(P_a)/2) )^2, where `weight(P_a)` is the number of non-identity factors in the Pauli string `P_a`. The 3^k locality factor is essential: without it the bound underestimates the required samples by 3^k for a k-local observable. Args: error (float): Desired error. observables (list[SparsePauliOp]): List of SparsePauliOps. delta (float): Probability of failure. Returns: samples (tuple(int, int)): (N*K, K) -- total snapshots required and the number of median-of-means divisions K. """ M = len(observables) K = 2 * np.log(2 * M / delta) shadow_norms = [] for obs in observables: bound = 0.0 for term in obs.terms: weight = sum(1 for p in term.paulis if p.pauli != 0) bound += abs(term.coefficient) * np.sqrt(3**weight) shadow_norms.append(bound**2) N = 34 * max(shadow_norms) / error**2 return int(np.ceil(N * K)), int(K) ``` ```python theme={null} observables = [observable] required_snapshots = error_bound_pauli(0.2, observables, 0.01) print( f"Number of samples required: {required_snapshots[0]}, Number of divisions: {required_snapshots[1]}" ) ``` **Output:** ``` Number of samples required: 81065, Number of divisions: 10 ``` The error bound predicts that to estimate $Z \otimes Z$ with error $0.2$ and failure probability $0.01$ we need on the order of $\sim 8 \times 10^{4}$ snapshots: the locality factor $3^{2}=9$ multiplies the per-observable sample count compared to the operator-norm-only bound. With only $400$ snapshots split into `div=2` chunks of 200 shots each, the standard error is $\sqrt{3^{2}/400}\approx 0.15$. # ## Classical Shadow with Random Clifford Measurements We now repeat the protocol with the second ensemble described in the introduction: the *full* $n$-qubit Clifford group $\mathrm{Cl}(2^n)$. Instead of applying an independent single-qubit Clifford on each qubit, we sample a *single* uniformly-random $\mathrm{Cl}(2^n)$ element $U$ and apply it globally to the bell state, then measure in the computational basis. As discussed earlier, the average measurement channel is the depolarizing channel $\mathcal{M}(\rho) = \mathcal{D}_{1/(2^n+1)}(\rho)$, and the inverse-channel snapshot reads $$ \hat{\rho} \;=\; \mathcal{M}^{-1}\!\bigl(U^\dagger\,|\hat b\rangle\!\langle\hat b|\,U\bigr) \;=\; (2^{\,n} + 1)\,U^\dagger\,|\hat b\rangle\!\langle\hat b|\,U \;-\; \mathbb{I}. $$ This Clifford track is independent of the Pauli-shadow workflow above. Two practical changes from the Pauli protocol: 1. We sample the random Clifford classically as a depth-$20$ sequence of the generator set $\{H,\ S,\ \mathrm{CNOT}\}$ (applied to randomly chosen qubits). This depth is past the 3-design mixing time (the circuit depth required to approximate a random Clifford unitary). The same gate sequence is then traced into the Classiq circuit using the native `H`, `S`, and `CX` qfuncs. 1. We record only the sampled *gate sequence* per snapshot in `clifford_sequences` - a list of `(label, q1, q2)` tuples. The matrix $U$ is built on demand inside the reconstruction / observable-estimator routines via `sequence_to_unitary`. Note: As mentioned in the beginning, in contrast to the present implementation, there is no need to create the unitary matrices, corresponding to the random Clifford circuits, explicitly (see `sequence_to_unitary`). The unitary operations can be calculated efficiently ($\mathcal{O}(n^2)$) employing the stabilizer formalism \[[5](#ref5)]. ```python theme={null} # Single-qubit gate matrices used only for the lazy matrix materialization below. _H1 = (1.0 / np.sqrt(2.0)) * np.array([[1, 1], [1, -1]], dtype=complex) _S1 = np.array([[1, 0], [0, 1j]], dtype=complex) _I1 = np.eye(2, dtype=complex) def _embed_1q(g: np.ndarray, q: int, num_qubits: int) -> np.ndarray: """Embed a single-qubit gate `g` at qubit `q` in the n-qubit Hilbert space. Tensor order matches `qarr`: qubit 0 is the MSB / leftmost factor.""" out = np.array([[1.0]], dtype=complex) for i in range(num_qubits): out = np.kron(out, g if i == q else _I1) return out def _cnot_matrix(ctrl: int, tgt: int, num_qubits: int) -> np.ndarray: """Dense CNOT matrix on `(ctrl, tgt)` in the full n-qubit space, MSB convention.""" dim = 2**num_qubits u = np.zeros((dim, dim), dtype=complex) for x in range(dim): bits = [(x >> (num_qubits - 1 - i)) & 1 for i in range(num_qubits)] new_bits = bits[:] if bits[ctrl] == 1: new_bits[tgt] = 1 - new_bits[tgt] new_x = sum(new_bits[i] << (num_qubits - 1 - i) for i in range(num_qubits)) u[new_x, x] = 1.0 return u def sample_clifford_circuit(num_qubits: int, depth: int = 20): """Sample a depth-`depth` random gate sequence from {H, S, CNOT}. Returns: seq (list[tuple]): each entry is `(label, q1, q2)` with `label in {"H", "S", "CX"}` and `q2 = -1` for single-qubit gates. For n = 2 a depth of ~20 is past the 3-design mixing time of random Clifford circuits, so the resulting distribution is close enough to Haar on the moments we need. """ seq = [] for _ in range(depth): kind = np.random.randint(0, 3) if kind < 2: q = int(np.random.randint(0, num_qubits)) label = "H" if kind == 0 else "S" seq.append((label, q, -1)) else: c, t = np.random.choice(num_qubits, size=2, replace=False) seq.append(("CX", int(c), int(t))) return seq def sequence_to_unitary(seq, num_qubits: int) -> np.ndarray: """Materialize a `(label, q1, q2)` gate sequence as the dense `2**num_qubits x 2**num_qubits` Clifford unitary. """ U = np.eye(2**num_qubits, dtype=complex) for label, q1, q2 in seq: if label == "H": U = _embed_1q(_H1, q1, num_qubits) @ U elif label == "S": U = _embed_1q(_S1, q1, num_qubits) @ U else: U = _cnot_matrix(q1, q2, num_qubits) @ U return U ``` ```python theme={null} def calculate_shadow_clifford(num_snapshots: int, num_qubits: int): """Collect `num_snapshots` single-shot snapshots under random Cl(2^n) unitaries. Each iteration synthesizes a fresh program (a new random Clifford gate sequence is sampled at trace-time), then samples one shot via `sample`. The sampled gate sequence is captured in a closure-local `sequences` list rather than a global, so successive calls don't interfere. Args: num_snapshots (int): Number of single-shot snapshots to collect. num_qubits (int): Number of qubits in the system. Returns: snapshots (list[list[int]]): per-snapshot measured bitstrings. sequences (list[list[tuple]]): per-snapshot Clifford gate sequence (each a list of `(label, q1, q2)` tuples) — same length and order as `snapshots`. """ sequences = [] @qfunc def clifford_application(state: QArray) -> None: """Sample a random Cl(2^n) gate sequence and apply it directly using Classiq's native `H`, `S`, and `CX` qfuncs. The sampled sequence is appended to the enclosing `sequences` list (no module-level state).""" seq = sample_clifford_circuit(num_qubits) sequences.append(seq) for label, q1, q2 in seq: if label == "H": H(state[q1]) elif label == "S": S(state[q1]) else: CX(state[q1], state[q2]) @qfunc def main(qarr: Output[QArray]) -> None: """Bell state, followed by a random global Clifford applied as native gates.""" prepare_bell_state(1, qarr) clifford_application(qarr) snapshots = [] for i in range(num_snapshots): qprog = synthesize(main) if i == 0: show(qprog) # render the very first synthesised circuit df = sample( qprog, num_shots=1, random_seed=int(np.random.randint(1e6)), ) snapshots.append(list(df["qarr"].iloc[0])) return snapshots, sequences # We use a smaller shadow than the Pauli case (50 vs 200) to decrease runtime. num_snapshots_clifford = 50 snapshots_clifford, clifford_sequences = calculate_shadow_clifford( num_snapshots_clifford, NUM_QUBITS ) print(f"Collected {len(snapshots_clifford)} Clifford-shadow snapshots") print(f"first 5 outcomes: {snapshots_clifford[:5]}") print(f"first sampled gate sequence: {clifford_sequences[0]}") ``` **Output:** ``` Submitting job to simulator ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3DWxa5FD0hw8W3k5BJ45gKBRH7x ``` **Output:** ``` Job: https://platform.classiq.io/jobs/fa7e8de9-6286-476a-919c-b7c230c1561a Submitting job to simulator Job: https://platform.classiq.io/jobs/a2cc8d0d-3dbd-493a-a991-9f7c47684106 Submitting job to simulator Job: https://platform.classiq.io/jobs/3a232641-b98c-45f1-bcff-76ee9489e71e Submitting job to simulator Job: https://platform.classiq.io/jobs/f1dd83f4-3c1c-49bb-997b-bf76924c3327 Submitting job to simulator Job: https://platform.classiq.io/jobs/62dc0782-bf3a-4a13-bab9-18375eed57a9 Submitting job to simulator Job: https://platform.classiq.io/jobs/5de7960a-4a42-499f-a7ca-17b21b8c5775 Submitting job to simulator Submitting job to simulator Job: https://platform.classiq.io/jobs/a5519b6c-9832-4995-be1e-905c251862c3 Submitting job to simulator Job: https://platform.classiq.io/jobs/c65cc59b-ef68-434b-ae82-cf206348fb09 Submitting job to simulator Job: https://platform.classiq.io/jobs/d6926199-d80b-4745-8141-14748521cee2 Submitting job to simulator Job: https://platform.classiq.io/jobs/65bb3183-dcb0-4d4d-bf35-cb0c17c5453f Submitting job to simulator Job: https://platform.classiq.io/jobs/3a6260f6-0fdb-4529-8510-6bc2f2ef2082 Submitting job to simulator Job: https://platform.classiq.io/jobs/795793ca-df5a-46cd-87cd-d89cc2a5744f Submitting job to simulator Job: https://platform.classiq.io/jobs/51e916fe-417c-4132-b7b6-d01d4def5251 Submitting job to simulator Job: https://platform.classiq.io/jobs/e2564b90-338c-4659-aa12-3fb928cd40cf Submitting job to simulator Job: https://platform.classiq.io/jobs/36ba8836-3372-48ab-a457-8498c654f104 Submitting job to simulator Job: https://platform.classiq.io/jobs/a97a44bd-bf74-4d55-bae9-edd7b5a19923 Submitting job to simulator Job: https://platform.classiq.io/jobs/ddc6bfa9-d6a2-4cae-9e4a-402f4366bffc Submitting job to simulator Job: https://platform.classiq.io/jobs/c4ac0362-71ca-4138-9f48-632c899fbf82 Submitting job to simulator Job: https://platform.classiq.io/jobs/a342bfe2-3dd1-4ae4-a6ea-204301212478 Submitting job to simulator Job: https://platform.classiq.io/jobs/5290ac1b-4f59-4b2f-8a97-d0df354e4c23 Submitting job to simulator Job: https://platform.classiq.io/jobs/50aa1203-f3b0-4173-b519-7f0c807d1738 Submitting job to simulator Job: https://platform.classiq.io/jobs/115ebf19-c74d-47af-834a-1f50bd83073a Submitting job to simulator Job: https://platform.classiq.io/jobs/005e16ab-d0ca-40c6-8481-43f729344abc Submitting job to simulator Job: https://platform.classiq.io/jobs/48d0ec7c-12bc-4e2b-9c27-70f1bc14883c Submitting job to simulator Job: https://platform.classiq.io/jobs/56b963fb-20f4-4ee9-a09d-74f8846a5edd Submitting job to simulator Job: https://platform.classiq.io/jobs/28a78587-12df-4a8a-9dc4-422eb030ea18 ``` **Output:** ``` Collected 50 Clifford-shadow snapshots first 5 outcomes: [[1, 0], [1, 1], [1, 0], [0, 1], [1, 0]] first sampled gate sequence: [('S', 0, -1), ('H', 0, -1), ('S', 1, -1), ('S', 1, -1), ('S', 0, -1), ('H', 1, -1), ('CX', 0, 1), ('CX', 0, 1), ('S', 0, -1), ('H', 1, -1), ('S', 0, -1), ('H', 0, -1), ('S', 0, -1), ('S', 0, -1), ('CX', 1, 0), ('H', 1, -1), ('H', 1, -1), ('S', 0, -1), ('S', 0, -1), ('S', 0, -1)] ``` # ### Reconstruction with the Clifford Shadow The inverse-channel snapshot for the Clifford ensemble is given by: $$ \hat\rho_t \;=\; (2^{\,n} + 1)\, U_t^{\dagger}\, |\hat b_t\rangle\!\langle\hat b_t|\,U_t \;-\; \mathbb{I}. $$ Here, $U_t$ is a $2^n \times 2^n$ unitary which generally does not factorize to $n$ single qubit gates. So we build $|\hat b_t\rangle\!\langle\hat b_t|$ in the full $2^n$-dimensional Hilbert space, and perform the inverse channel. Averaging across the snapshots than reconstructs $\rho$. ```python theme={null} def snapshot_reconstruction_clifford(snapshot, U, num_qubits): """Single-snapshot Clifford-shadow reconstruction. Uses the fact that ``|b> **Output:** ``` Clifford-shadow reconstruction: [[ 0.475-0.j -0.025-0.1j 0.1 -0.125j -0.8 -0.075j] [-0.025+0.1j -0.175-0.j - 0. +0.025j 0.05 +0.075j] [ 0.1 +0.125j - 0. -0.025j 0.075-0.j -0.025-0.1j ] [-0.8 +0.075j 0.05 -0.075j -0.025+0.1j 0.625+0.j ]] Bell state target: [[ 0.5 0. 0. -0.5] [ 0. 0. 0. 0. ] [ 0. 0. 0. 0. ] [-0.5 0. 0. 0.5]] Frobenius distance (Clifford shadow): 0.5958 ``` # ### Estimation of Observables For the random $n$-qubit Clifford measurement channel the inverse channel does *not* factorize across qubits, and the per-snapshot estimator reads $$ \hat{\rho}_{i} \;=\; (2^{n}+1)\, U_{i}^{\dagger}\,|\hat{b}_{i}\rangle\!\langle\hat{b}_{i}|\,U_{i} \;-\; \mathbb{I}. $$ Substituting into $\text{tr}(O\hat{\rho}_{i})$ gives $$ \text{tr}(O\,\hat{\rho}_{i}) \;=\; (2^{n}+1)\, \bigl(U_{i}\, O\, U_{i}^{\dagger}\bigr)_{\hat b_{i},\,\hat b_{i}} \;-\; \text{tr}(O), $$ so each snapshot's contribution is a single diagonal entry of the rotated observable. As before, we average within each median-of-means chunk and take the median across the $K$ chunks (Eqs. (1)-(2)). ```python theme={null} def estimate_observable_clifford( in_snapshots, in_sequences, observable, div, num_qubits ): """ Estimates = tr(O rho) from a Clifford classical shadow with median-of-means. Each snapshot estimator is rho_hat_i = (2^n + 1) U_i^dagger |b_i> (Clifford shadow): {estimate_clifford:.1f}") print(f"Error relative to ground truth value of 1.0: {(1.0 - estimate_clifford)}") ``` **Output:** ``` Estimated (Clifford shadow): 1.0 Error relative to ground truth value of 1.0: 9.992007221626409e-16 ``` We achieve machine precision in estimating the expectation value. # ### Error Bound * Clifford Ensemble The sample-complexity bound (Eq. (5)) holds with the Clifford-ensemble shadow norm. Using the upper bound from the introduction, $$ \| O - {\textstyle\frac{\text{tr}(O)}{2^{n}}}\,\mathbb{I} \|^{2}_{\text{shadow}} \;\le\; 3\,\text{tr}\!\left( O_{0}^{\,2}\right), \qquad O_{0} = O - \tfrac{\text{tr}(O)}{2^{n}}\,\mathbb{I}, $$ we obtain a sufficient sample count $$ N \;=\; \frac{34}{\epsilon^{2}}\,\max_{1\le i\le M}\, 3\,\text{tr}\!\left( O_{0,i}^{\,2}\right), \qquad K \;=\; 2\log\!\Bigl(\tfrac{2M}{\delta}\Bigr). $$ Unlike the Pauli ensemble, this does **not** scale with the locality of the observable, only with the Hilbert-Schmidt norm of its traceless part, so global observables (e.g. fidelities) are tractable. ```python theme={null} def error_bound_clifford(error, observables, delta): """ Calculates the minimum Clifford-shadow size guaranteeing additive error `error` on every estimated observable with probability `>= 1 - delta`. Implements Eq. (5) with the Clifford-ensemble shadow-norm upper bound || O - tr(O)/2**n * I ||^2_shadow <= 3 * tr( (O - tr(O)/2**n * I)^2 ). Args: error (float): Desired additive error per observable. observables (list[SparsePauliOp]): Observables to estimate. delta (float): Probability of failure. Returns: samples (tuple(int, int)): (N*K, K) — total snapshots required and the number of median-of-means divisions. """ observable_ops = [hamiltonian_to_matrix(term) for term in observables] M = len(observable_ops) K = 2 * np.log(2 * M / delta) shadow_norms = [] for op in observable_ops: n = int(np.log2(op.shape[0])) traceless = op - np.trace(op) / 2**n * np.eye(2**n) hs_sq = float(np.real(np.trace(traceless.conj().T @ traceless))) shadow_norms.append(3 * hs_sq) N = 34 * max(shadow_norms) / error**2 return int(np.ceil(N * K)), int(K) required_snapshots_clifford = error_bound_clifford(0.2, [observable], 0.01) print( f"Bound predicts {required_snapshots_clifford[0]} Clifford samples are required to achieve 0.2 error\n" f"median-of-means divisions: {required_snapshots_clifford[1]}" ) ``` **Output:** ``` Bound predicts 108086 Clifford samples are required to achieve 0.2 error median-of-means divisions: 10 ``` In practice, for the Bell state and chosen observable, we obtain an excellent estimation of the observables with much fewer samples than the error bound predicts. ## Summary We have reconstructed full states and estimated observables using the classical shadow procedure on Classiq. However, the procedure can accomplish much more, such as estimating fidelity, finding entanglement witnesses, and estimating entanglement entropy, as shown in Ref. \[[3](#ref3)]. ## References \[[1](#ref1)] J. Haah, A. W. Harrow, Z. Ji, X. Wu and N. Yu, "Sample-Optimal Tomography of Quantum States," in IEEE Transactions on Information Theory, vol. 63, no. 9, pp. 5628-5641, Sept. 2017, doi: 10.1109/TIT.2017.2719044 [arXiv](https://arxiv.org/abs/1508.01797). \[[2](#ref2)] Aaronson, S. (2018). Shadow tomography of quantum states. [arXiv](https://arxiv.org/abs/1711.01053). \[[3](#ref3)] Huang, HY., Kueng, R. & Preskill, J. Predicting many properties of a quantum system from very few measurements. Nat. Phys. 16, 1050-1057 (2020). [https://doi.org/10.1038/s41567-020-0932-7](https://doi.org/10.1038/s41567-020-0932-7). [arXiv](https://arxiv.org/abs/2002.08953). \[[4](#ref4)] Quantum depolarizing channel. Wikipedia. [https://en.wikipedia.org/wiki/Quantum\_depolarizing\_channel](https://en.wikipedia.org/wiki/Quantum_depolarizing_channel) \[[5](#ref5)] Aaronson, S., & Gottesman, D. (2004). Improved Simulation of Stabilizer Circuits. Physical Review A, 70(5), 52328. [https://doi.org/10.1103/PhysRevA.70.052328](https://doi.org/10.1103/PhysRevA.70.052328). [arXiv](https://arxiv.org/abs/quant-ph/0406196). \[[6](#ref6)] Wiersema, R., & Doolittle, B. (2021, June). Classical shadows. PennyLane Demos. Xanadu. [https://pennylane.ai/qml/demos/tutorial\_classical\_shadows](https://pennylane.ai/qml/demos/tutorial_classical_shadows) # Discrete Logarithm Source: https://docs.classiq.io/explore/algorithms/number_theory_and_cryptography/discrete_log/discrete_log Open this notebook in GitHub to run it yourself > The **Discrete Logarithm Problem** \[[1](#discretelog)] was shown by Shor \[[2](#shor)] to be solved in a polynomial time using quantum computers, while the fastest classical algorithms take a superpolynomial time. The problem is at least as hard as the factoring problem. In fact, the hardness of the problem is the basis for the Diffie-Hellman \[[3](#diffiehellman)] protocol for key exchange. The algorithm is a specific instance of the Abelian Hidden Subgroup Problem \[[4](#hsp)]. > > The algorithm treats the following problem: > > * **Input:** A cyclic group $G = \langle g \rangle$ with a generator $g\in G$, an element $x\in G$ and the order of the group is known $r=|G|$. A quantum oracle $U_f$ applying the transformation $$ U_f|\alpha\rangle |\beta\rangle |0\rangle = |\alpha\rangle |\beta\rangle |f(\alpha, \beta)\rangle~~, $$ where $f(\alpha, \beta) = x^\alpha g^\beta$ with $\alpha, \beta \in \mathbb{Z}_r$. > * **Promise:** There is a positive integer $s\in \mathbb{N}$ such that $g^s = x$. > > * **Output:** The least positive integer $s = \log_g x$, i.e, the discrete logarithm. > > * **Complexity:** A single application of the algorithm succeeds with probability $O(\log \log r)$ and requires $O((\log t)^2)$ time, where $t = \lceil \log r +\log(1/\epsilon) \rceil$ and $1-\epsilon$ is success probability. Hence, boosting the success probability to a constant $O(1)$ via repetition yields a total expected runtime of $$ O((\log t)^2 \log \log r)~. $$ > *** > > **Keywords:** Abelain Hidden subgroup problem, quantum Fourier transform, period finding. ## Algorithm Steps We first consider the case where $r=2^m$ and later generalize. We begin by introducing an input state $$ |\psi_0\rangle = |0^m\rangle|0^m\rangle|0^R\rangle~~, $$ where $R$ is the number of bits required to represent $f$'s image. In addition, we note that $f(\alpha,\beta) = x^\alpha g^\beta = g^{\alpha\log_g x + \beta}$ is constant on lines satisfying $$ \{(\alpha,\beta)\in \mathbb{Z}_r^2, \alpha\log_g x + \beta = \lambda \mod r\}~. $$ The algorithm includes four main steps, and has a similar structure to the other quantum algorithms for the hidden subgroup problem. 1. We first prepare a uniform superposition over the input states of the first two registers: $$ |0^m\rangle|0^m\rangle|0^m\rangle \xrightarrow{H^{\otimes m}} \frac{1}{r}\sum_{\alpha,\beta \in \mathbb{Z}_r}|\alpha \rangle |\beta \rangle |0^m\rangle ~~. $$ 2. Perform a query to the oracle $$ \xrightarrow{U_f} \frac{1}{r}\sum_{\alpha, \beta \in \mathbb{Z}_r}|\alpha \rangle |\beta \rangle |f(\alpha, \beta)\rangle~~. $$ Utilizing the periodicity of $f$, we can express the state as $$ = \frac{1}{r}\sum_{\alpha, \lambda \in \mathbb{Z}_r}|\alpha \rangle |\lambda -\alpha \log_g x \rangle |g^{\lambda}\rangle~~. $$ 3. Perform a double inverse Fourier transform over the group $\mathbb{Z}_r$ $$ \xrightarrow{\text{QFT}_{\mathbb{Z}_r}^\dagger \times \text{QFT}_{\mathbb{Z}_r}^\dagger } \frac{1}{r^2}\sum_{\lambda, \mu, \nu \in \mathbb{Z}_r}\left(\sum_\alpha e^{-i 2\pi \alpha(\mu - \nu \log_g x)/r}\right)e^{-i 2\pi \lambda \nu/r}|\mu \rangle |\nu \rangle |g^{\lambda}\rangle $$ $$ = \frac{1}{r}\sum_{\lambda, \nu \in \mathbb{Z}_r}e^{i 2\pi \lambda \nu/r}|\nu \log_g x \rangle |\nu \rangle |g^{\lambda}\rangle~~, $$ where the Fourier transform over $\mathbb{Z}_r$ is $\text{QFT}_{\mathbb{Z}_r} |\alpha \rangle = \frac{1}{\sqrt{r}}\sum_{\mu\in \mathbb{Z}_r} e^{i 2\pi \alpha \mu/r}|\mu \rangle$ and in the second line we utilized the identity $\sum_\alpha e^{i 2\pi \alpha \eta} = r \delta_{0\eta}$ (easily derived by use of a sum over a geometric series). 4\. Measure the three quantum variables. The measurement of the third variable collapses the quantum state to $|g^\lambda \rangle$ with a uniform distribution. After the collapse, the resulting state is independent of $\lambda$, therefore, we can discard this measurement outcome. From the measurement of the second quantum variable, we obtain with a uniform distribution over $\nu$ the outcome $\nu \log_g x$. With a probablility of order $O(\log \log r)$, the obtained result $\nu$ is co-prime to $r$, and there exists a modular $r$ multiplicative inverse $\nu^{-1}$. Under this condition, we can multiply the outcome of the second register to obtain the discrete log $s=\log_g x$. Hence, repeating the experiment $O(\log \log r)$ times leads to a success probability of order $O(1)$. ## Building the Algorithm with Classiq We begin by importing software packages ```python theme={null} import matplotlib.pyplot as plt import numpy as np from classiq import * from classiq.qmod.symbolic import ceiling, log ``` We consider two exemplary cases. First, we study the group $\mathbb{Z}_5^\times$, where the order is a power of $r=4=2^2$. For this case, quantum variables with only $\log r = m$ qubits are required. Following, the group $\mathbb{Z}_{13}^\times$ exemplifies the alternative case where the order $ r\neq 2^m$ for an integer $m$. For these examples, we denote the modulus by $N$. Since, these are cyclic groups, we have $N = r+1$, therefore $R = \lceil \log N\rceil$. In the following examples, the third register, containing the function $f(\alpha, \beta )$ after the second step, is represented by $\lceil \log N\rceil$. The heart of the algorithm's logic is the implementation of the function $$ |\alpha \rangle|\beta \rangle|0\rangle \rightarrow |\alpha \rangle|\beta \rangle|x^{\alpha } g^{\beta}\rangle~. $$ This is done using two applications of the modular exponentiation function, described in detail in the [Shor's Factoring Algorithm](https://short.classiq.io/shor) notebook. So here we import it from the Classiq library. The `modular_exponentiation` function defined below accepts these arguments: * `N: CInt` - modulo number * `a: CInt` - base of the exponentiation * `x: QArray[QBit]` - unsigned integer to multiply by the exponentiation * `pw: QArray[QBit]`- power of the exponentiation So the function implements $|pw\rangle|x\rangle \rightarrow |pw\rangle|x \cdot a ^ {pw}\mod N\rangle$. ```python theme={null} from classiq import * @qfunc def modular_exponentiation(N: CInt, a: CInt, x: QArray, pw: QArray): repeat( count=pw.len, iteration=lambda index: control( pw[index], # lambda: inplace_modular_multiply(N, (a ** (2**index)) % N, x), #modular_multiply_constant_inplace lambda: modular_multiply_constant_inplace(N, a ** (2**index), x), ), ) @qfunc def discrete_log_oracle( g_generator: CInt, x_element: CInt, N_modulus: CInt, alpha: QArray, beta: QArray, func_res: Output[QNum], ) -> None: allocate(ceiling(log(N_modulus, 2)), func_res) func_res ^= 1 modular_exponentiation(N_modulus, x_element, func_res, alpha) modular_exponentiation(N_modulus, g_generator, func_res, beta) ``` # ## Full Algorithm 1. Prepare a uniform superposition over the first two quantum variables `alpha`, `beta`. Each variable has size $\lceil \log r\rceil + \log({1/{\epsilon}})$. In the special case where $r$ is a power of 2, $\log r$ is enough. 1. Compute `discrete_log_oracle` on the `func_res` variable. `func_res` is of size $\lceil \log N\rceil$. 2. Apply the inverse Fourier transform `alpha`, `beta`. 3. Measure. ```python theme={null} @qfunc def discrete_log( g: CInt, x: CInt, N: CInt, order: CInt, alpha: Output[QArray], beta: Output[QArray], func_res: Output[QArray], ) -> None: reg_len = ceiling(log(order, 2)) allocate(reg_len, alpha) allocate(reg_len, beta) hadamard_transform(alpha) hadamard_transform(beta) discrete_log_oracle(g, x, N, alpha, beta, func_res) invert(lambda: qft(alpha)) invert(lambda: qft(beta)) ``` After the inverse QFTs (under the assumption of $r=2^m$ for some $m$, therefore $t=2^r$), the variables become $$ |\psi\rangle = \frac{1}{r}\sum_{\lambda, \nu \in \mathbb{Z}_r}e^{i 2\pi \lambda \nu/r}|\nu \log_g x \rangle_{\alpha} |\nu \rangle_{\beta} |g^{\lambda}\rangle_{\text{res}} $$ where we added a subscript to the quantum variables for clarity, and $| \rangle_{\text{res}}$ designates the `func_res` variable. If $\nu\in \mathbb{Z}_r$ has an inverse, $\nu^{-1}\in\mathbb{Z}_r$ we can extract $s=\log_x g$ from variable $\alpha$ by multiplying by $\nu^{-1}$. See the second example for the general case, for which $r \neq 2^m$. Screenshot 2025-12-10 at 16.38.37.png ## Example: $G = \mathbb{Z}_5^\times$ For this specific demonstration, we choose $G = \mathbb{Z}_5^\times$, with $g=3$ and $x=2$. With this setting, $\log_gx=3$. We choose this specific example because the order of the group $r=4$ is a power of $2$, so we can get the exact discrete logarithm without continued-fractions postprocessing. In other cases, we use a larger quantum variable for the exponents so the continued fractions postprocessing converges. ```python theme={null} MODULU_NUM = 5 G_GENERATOR = 3 X_LOG_ARG = 2 ORDER = MODULU_NUM - 1 # as 5 is prime @qfunc def main( alpha: Output[QNum], beta: Output[QNum], func_res: Output[QNum], ) -> None: discrete_log(G_GENERATOR, X_LOG_ARG, MODULU_NUM, ORDER, alpha, beta, func_res) ``` ```python theme={null} qmod_Z5 = create_model( main, constraints=Constraints(max_width=13), preferences=Preferences(optimization_level=1), execution_preferences=ExecutionPreferences(num_shots=4000), ) qprog_Z5 = synthesize(qmod_Z5) show(qprog_Z5) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/36zUExgS7ndoE6IUFudD7cLnNii ``` ```python theme={null} result_Z5 = execute(qprog_Z5).result_value() result_Z5.dataframe ``` | | alpha | beta | func\_res | count | probability | bitstring | | -- | ----- | ---- | --------- | ----- | ----------- | --------- | | 0 | 2 | 2 | 3 | 268 | 0.06700 | 0111010 | | 1 | 2 | 2 | 2 | 266 | 0.06650 | 0101010 | | 2 | 0 | 0 | 3 | 266 | 0.06650 | 0110000 | | 3 | 0 | 0 | 1 | 264 | 0.06600 | 0010000 | | 4 | 3 | 1 | 1 | 259 | 0.06475 | 0010111 | | 5 | 2 | 2 | 1 | 258 | 0.06450 | 0011010 | | 6 | 1 | 3 | 3 | 256 | 0.06400 | 0111101 | | 7 | 1 | 3 | 1 | 250 | 0.06250 | 0011101 | | 8 | 1 | 3 | 4 | 250 | 0.06250 | 1001101 | | 9 | 3 | 1 | 3 | 246 | 0.06150 | 0110111 | | 10 | 1 | 3 | 2 | 244 | 0.06100 | 0101101 | | 11 | 0 | 0 | 4 | 240 | 0.06000 | 1000000 | | 12 | 3 | 1 | 2 | 238 | 0.05950 | 0100111 | | 13 | 0 | 0 | 2 | 236 | 0.05900 | 0100000 | | 14 | 2 | 2 | 4 | 230 | 0.05750 | 1001010 | | 15 | 3 | 1 | 4 | 229 | 0.05725 | 1000111 | Note that `func_res` is uncorrelated to the other variables, and we get uniform distribution, as expected. We take only the `beta` that are co-prime to $r=4$, so they have a multiplicative-inverse. Hence `beta=1,3` are the relevant results. So we get two relevant results (for all different $\lambda$s): $|1\rangle|3\rangle$, $|3\rangle|1\rangle$. All that remains to get the logarithm is to multiply `alpha` by the inverse of `beta`: ```python theme={null} for res in result_Z5.parsed_counts: if res.state["beta"] in [1, 3]: logarithm = res.state["beta"] * pow(res.state["alpha"], -1, 4) assert logarithm == 3 ``` Verify we received the correct discrete logarithm: ```python theme={null} log_arg = (G_GENERATOR**logarithm) % MODULU_NUM print(log_arg) assert log_arg == X_LOG_ARG ``` **Output:** ``` 2 ``` And, indeed, both cases give the same result, which is exactly the discrete logarithm: $\log_32 \mod 5 = 3$. ## Generalization for the Case Where $r\neq 2^m$ For an arbitrary $r\in \mathbb{N}$, we employ two $t=\lceil \log r\rceil + \log (1/\epsilon)$ qubit quantum variables and a third $R$-qubit register, initialized to the state $|\psi_0\rangle = |0^t\rangle|0^t\rangle|0^R\rangle $, where $R = \lceil \log N\rceil$. The derivation follows a similar structure, where the initial sums are now $\alpha, \beta \in \mathbb Z_T$, with $T=2^t$, while the period of $f$ remains $r$, i.e., $\lambda\in \mathbb{Z}_r$. Therefore, after the second step we obtain the state $$ \frac{1}{T}\sum_{\alpha,\beta \in \mathbb{Z}_T}|\alpha\rangle|\beta\rangle|0^R\rangle\xrightarrow{U_f} \frac{1}{T}\sum_{\alpha,\beta\in \mathbb{Z}_T}|\alpha \rangle |\beta\rangle |g^{\alpha \log_g x + \beta}\rangle~~. $$ Next, we express the periodic state in an alternative form. We introduce the operator $U_g$, which satisfies $U_g |h\rangle= |h g\rangle$, where $e$ is the identity element of the group. Therefore $|{g^\lambda}\rangle = U_g^\lambda |e \rangle$. The state $|e\rangle$ can be expressed as a uniform superposition of the eigenstates of $U_g$: $$ |e \rangle = \frac{1}{\sqrt{r}}\sum_{k=0}^{r-1} |\Psi_k\rangle~~, $$ where $| \Psi_k \rangle = \frac{1}{\sqrt{r}} \sum_{k=0}^{r-1} e^{i2\pi k k'/r}| g^{k'}\rangle$, which satisfy $U_g | \Psi_k \rangle = e^{i 2\pi k/r}| \Psi_k\rangle$. These relations allow writing the state after the oracle operation as $$ \frac{1}{T}\sum_{\alpha,\beta\in \mathbb{Z}_T}|\alpha \rangle |\beta\rangle \sum_{k=0}^{r-1}e^{i 2\pi (\alpha \log_g x + \beta) k/r }|\Psi_k\rangle~~. $$ Applying the inverse quantum Fourier transform leads to the state $$ \xrightarrow{\text{QFT}_T^\dagger \times \text{QFT}_T^\dagger}\frac{1}{T}\sum_{k=0}^{r-1}\sum_{\mu,\nu\in \mathbb{Z}_T}\left(\sum_{\alpha\in \mathbb{Z}_T} e^{i 2\pi \alpha (\log_g x k/r-\mu/T) }| \mu\rangle\right)\left( \sum_{\beta\in \mathbb{Z}_T} e^{i 2\pi \beta (k/r-\nu /T) }| \nu \rangle \right)|\Psi_k\rangle~~. $$ Utilizing the geometric sum, one can show that the functions are peaked (with a width $O(1/T)$) around $\mu \approx T\log_g x k/r $ and $ \nu \approx T k / r$, correspondingly. To evaluate the values of $k \log_g x$ and $k$ we measure `alpha` and `beta` registers, multiply by $r/N$ and round. For sufficiently large $T$, we obtain the correct value for $s=\log_g x$ with high probability. Alternatively, one can apply the continued fraction algorithm \[[5](#continuedfraction)] to evaluate $s$, see \[[6](#discretelogellipticcurve)] for further details. *Note: Alternatively, you could implement the $\text{QFT}_{\mathbb{Z}_r}$ over general $r$, and instead of the uniform superposition, prepare the states: $\frac{1}{\sqrt{r}}\sum_{x\in\mathbb{r}}|x\rangle$ in `alpha`, `beta`. Then, again, no continued fractions postprocessing is required.* ## Example: $G = \mathbb{Z}_{13}^\times$ ```python theme={null} MODULU_NUM = 13 G_GENERATOR = 7 X_LOG_ARG = 3 ORDER = 12 @qfunc def discrete_log( g: CInt, x: CInt, N: CInt, order: CInt, alpha: Output[QNum], beta: Output[QNum], func_res: Output[QNum], ) -> None: reg_len = ceiling(log(order, 2)) + 1 # we define the variables with fraction places to ease the postprocessing allocate(reg_len, False, reg_len, alpha) allocate(reg_len, False, reg_len, beta) hadamard_transform(alpha) hadamard_transform(beta) discrete_log_oracle(g, x, N, alpha, beta, func_res) invert(lambda: qft(alpha)) invert(lambda: qft(beta)) @qfunc def main( alpha: Output[QNum], beta: Output[QNum], func_res: Output[QNum], ) -> None: discrete_log(G_GENERATOR, X_LOG_ARG, MODULU_NUM, ORDER, alpha, beta, func_res) ``` ```python theme={null} constraints = Constraints(max_width=23) preferences = Preferences(optimization_level=1) execution_preferences = ExecutionPreferences(num_shots=10000) qmod_Z13 = create_model( main, constraints=constraints, preferences=preferences, execution_preferences=execution_preferences, out_file="discrete_log", ) qprog_Z13 = synthesize(qmod_Z13) show(qprog_Z13) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/36zUYE60MCHFZsP62VSX5yE4xi8 ``` ```python theme={null} result_Z13 = execute(qprog_Z13).result_value() df = result_Z13.dataframe df.head(10) ``` | | alpha | beta | func\_res | count | probability | bitstring | | - | ----- | ---- | --------- | ----- | ----------- | -------------- | | 0 | 0.0 | 0.25 | 5 | 91 | 0.0091 | 01010100000000 | | 1 | 0.0 | 0.25 | 1 | 88 | 0.0088 | 00010100000000 | | 2 | 0.0 | 0.50 | 10 | 87 | 0.0087 | 10101000000000 | | 3 | 0.0 | 0.50 | 1 | 85 | 0.0085 | 00011000000000 | | 4 | 0.0 | 0.50 | 4 | 85 | 0.0085 | 01001000000000 | | 5 | 0.0 | 0.00 | 4 | 83 | 0.0083 | 01000000000000 | | 6 | 0.0 | 0.75 | 4 | 83 | 0.0083 | 01001100000000 | | 7 | 0.0 | 0.50 | 5 | 79 | 0.0079 | 01011000000000 | | 8 | 0.0 | 0.00 | 12 | 79 | 0.0079 | 11000000000000 | | 9 | 0.0 | 0.75 | 2 | 78 | 0.0078 | 00101100000000 | # ## Postprocessing We now have an additional step in postprocessing. We translate each result to the closest fraction with a denominator, which is the order: ```python theme={null} def closest_fraction(x, denominator): return round(x * denominator) df["alpha_rounded"] = closest_fraction(df.alpha, ORDER) df["beta_rounded"] = closest_fraction(df.beta, ORDER) df.head(10) ``` | | alpha | beta | func\_res | count | probability | bitstring | alpha\_rounded | beta\_rounded | | - | ----- | ---- | --------- | ----- | ----------- | -------------- | -------------- | ------------- | | 0 | 0.0 | 0.25 | 5 | 91 | 0.0091 | 01010100000000 | 0.0 | 3.0 | | 1 | 0.0 | 0.25 | 1 | 88 | 0.0088 | 00010100000000 | 0.0 | 3.0 | | 2 | 0.0 | 0.50 | 10 | 87 | 0.0087 | 10101000000000 | 0.0 | 6.0 | | 3 | 0.0 | 0.50 | 1 | 85 | 0.0085 | 00011000000000 | 0.0 | 6.0 | | 4 | 0.0 | 0.50 | 4 | 85 | 0.0085 | 01001000000000 | 0.0 | 6.0 | | 5 | 0.0 | 0.00 | 4 | 83 | 0.0083 | 01000000000000 | 0.0 | 0.0 | | 6 | 0.0 | 0.75 | 4 | 83 | 0.0083 | 01001100000000 | 0.0 | 9.0 | | 7 | 0.0 | 0.50 | 5 | 79 | 0.0079 | 01011000000000 | 0.0 | 6.0 | | 8 | 0.0 | 0.00 | 12 | 79 | 0.0079 | 11000000000000 | 0.0 | 0.0 | | 9 | 0.0 | 0.75 | 2 | 78 | 0.0078 | 00101100000000 | 0.0 | 9.0 | Now, we take a sample where `beta` is co-prime to the order, such that we can get the logarithm by multiplying `alpha` by the modular inverse. If the `alpha`, `beta` registers are large enough, we are guaranteed to sample it with a good probability: ```python theme={null} import numpy as np def modular_inverse(x): return [pow(a, -1, ORDER) for a in x] df = df[np.gcd(df.beta_rounded.astype(int), ORDER) == 1].copy() df["beta_inverse"] = modular_inverse(df.beta_rounded.astype("int")) df["logarithm"] = df.alpha_rounded * df.beta_inverse % ORDER df.head(10) ``` | | alpha | beta | func\_res | count | probability | bitstring | alpha\_rounded | beta\_rounded | beta\_inverse | logarithm | | -- | ------- | ------- | --------- | ----- | ----------- | -------------- | -------------- | ------------- | ------------- | --------- | | 48 | 0.65625 | 0.09375 | 9 | 49 | 0.0049 | 10010001110101 | 8.0 | 1.0 | 1 | 8.0 | | 50 | 0.34375 | 0.90625 | 1 | 48 | 0.0048 | 00011110101011 | 4.0 | 11.0 | 11 | 8.0 | | 54 | 0.65625 | 0.09375 | 7 | 42 | 0.0042 | 01110001110101 | 8.0 | 1.0 | 1 | 8.0 | | 55 | 0.34375 | 0.90625 | 8 | 42 | 0.0042 | 10001110101011 | 4.0 | 11.0 | 11 | 8.0 | | 58 | 0.65625 | 0.09375 | 10 | 41 | 0.0041 | 10100001110101 | 8.0 | 1.0 | 1 | 8.0 | | 59 | 0.34375 | 0.90625 | 4 | 40 | 0.0040 | 01001110101011 | 4.0 | 11.0 | 11 | 8.0 | | 61 | 0.34375 | 0.40625 | 12 | 40 | 0.0040 | 11000110101011 | 4.0 | 5.0 | 5 | 8.0 | | 66 | 0.65625 | 0.59375 | 2 | 38 | 0.0038 | 00101001110101 | 8.0 | 7.0 | 7 | 8.0 | | 67 | 0.65625 | 0.59375 | 4 | 38 | 0.0038 | 01001001110101 | 8.0 | 7.0 | 7 | 8.0 | | 69 | 0.34375 | 0.40625 | 11 | 38 | 0.0038 | 10110110101011 | 4.0 | 5.0 | 5 | 8.0 | ```python theme={null} print(f"The descrite logarithm is: {df.logarithm[:1].iloc[0]}") ``` **Output:** ``` The descrite logarithm is: 8.0 ``` To verify the results, we check whether $g^s \mod N = g^{\log_g x}\mod N = x$. ```python theme={null} assert len(df.logarithm) > 0 assert np.allclose(G_GENERATOR ** df.logarithm[:10] % MODULU_NUM, X_LOG_ARG) ``` # ## Measurement Distribution Heuristic Plots The expected measurement results are showcased by plotting the theoretical probability distributions of the $\alpha$ and $\beta$ quantum variables, for varying numbers of qubits $t=5$ and $t=8$ and the specific case of $k=5$. Since $r = 12 \neq 2^m$ for some integer $m$, we obtain a probability distribution characterized by a narrow peak of width $\sim 1/T$. As the number of qubits is increased, $T=2^t$ increases, the probability distribution becomes narrower. Therefore, improving the success probability. ```python theme={null} MODULU_NUM = 13 X_LOG_ARG = 3 t = np.ceil(np.log2(ORDER)) + 1 T = 2**t k = 5 r = ORDER x_data = np.arange(T) x0_mu = k * T / r amplitudes_mu = np.sin(np.pi * (x_data - x0_mu)) / ( T * np.sin(np.pi * (x_data - x0_mu) / T) ) probabilities_mu = np.abs(amplitudes_mu) ** 2 probabilities_mu /= np.sum(probabilities_mu) s = np.log(X_LOG_ARG) / np.log(G_GENERATOR) # same as log_{G_GENERATOR}(X_LOG_ARG) x0_nu = s * k * T / r amplitudes_nu = np.sin(np.pi * (x_data - x0_nu)) / ( T * np.sin(np.pi * (x_data - x0_nu) / T) ) probabilities_nu = np.abs(amplitudes_nu) ** 2 probabilities_nu /= np.sum(probabilities_nu) # Create figure and axis with custom size fig, ax = plt.subplots(figsize=(8, 6)) # Plot data ax.plot(x_data, probabilities_mu, label="alpha") ax.plot(x_data, probabilities_nu, "r-.", label="beta") # Customize axis labels and title font sizes ax.set_xlabel("mu and nu values", fontsize=14) ax.set_ylabel("", fontsize=14) ax.set_title("Probabilities the Alpha and Beta Registers, t=5", fontsize=16) # Increase tick label (axis numbers) size ax.tick_params(axis="both", labelsize=12) # Show legend ax.legend(fontsize=12, loc="best") # Display the plot plt.show() ``` output ```python theme={null} MODULU_NUM = 13 X_LOG_ARG = 3 t = np.ceil(np.log2(ORDER)) + 4 T = 2**t k = 5 r = ORDER x_data = np.arange(T) x0_mu = k * T / r amplitudes_mu = np.sin(np.pi * (x_data - x0_mu)) / ( T * np.sin(np.pi * (x_data - x0_mu) / T) ) probabilities_mu = np.abs(amplitudes_mu) ** 2 probabilities_mu /= np.sum(probabilities_mu) s = np.log(X_LOG_ARG) / np.log(G_GENERATOR) # same as log_{G_GENERATOR}(X_LOG_ARG) x0_nu = s * k * T / r amplitudes_nu = np.sin(np.pi * (x_data - x0_nu)) / ( T * np.sin(np.pi * (x_data - x0_nu) / T) ) probabilities_nu = np.abs(amplitudes_nu) ** 2 probabilities_nu /= np.sum(probabilities_nu) # Create figure and axis with custom size fig, ax = plt.subplots(figsize=(8, 6)) # Plot data ax.plot(x_data, probabilities_mu, label="alpha") ax.plot(x_data, probabilities_nu, "r-.", label="beta") # Customize axis labels and title font sizes ax.set_xlabel("mu and nu values", fontsize=14) ax.set_ylabel("", fontsize=14) ax.set_title("Probabilities the Alpha and Beta Registers, t=8", fontsize=16) # Increase tick label (axis numbers) size ax.tick_params(axis="both", labelsize=12) # Show legend ax.legend(fontsize=12, loc="best") # Display the plot plt.show() ``` output ## Technical Notes # ## Equivalence with the Abelian Subgroup Problem The discrete logarithm is a specific case of the Hidden Subgroup Problem (HSP) \[[4](#hsp)]. The HSP can be stated as follows: Let $G$ be a group and $H$ is subgroup of $G$. We are given an (oracle) function $f$ with the promise that: 1. $f$ is constant on the right cosets of $H$. 2. Elements of distinct cosets produce different oracle values. That is for $g\in G$ if and only if $x,y\in g H$, $f(x) = f(y)$. Goal: Identify a generating set for the subgroup $H$; that is, a collection of elements of $H$ whose products yield every element of the subgroup. Note, that there is always a generating set of size $\Omega(\log(|H|))$. The discrete logarithm problem is an instance of the Abelian HSP. * $G$ is the additive group $\mathbb{Z}_N\times \mathbb{Z}_N$. * The hidden subgroup is $ H = \{(0,0), (1,-\log_g x),(2,-2\log_g x,\dots, (r-1,-(r-1)\log_g x))\}$. * The cosets are the of the form $\{(\alpha, \lambda + \alpha\log_x g)\}$, where $\lambda \in \mathbb{Z}_r$. It is straightforward to show that $f(\alpha,\beta) = g^\lambda$ for all elements of the coset. Solution of the HSP provides a generator of $(\nu, -\nu\log_g x)$, for $\nu$ coprime to $r$, which allows evaluating the discrete logarithm by taking the modular inverse of $\nu$: $s=-\nu^{-1}\nu \log_g x$. # ## Diffie-Hellman Secret Key Sharing Protocol The Diffie-Hellman protocol enables two parties ("Alice" and "Bob") to establish a shared secret key, and its security against an eavesdropper ("Eve") relies on the computational hardness of the discrete logarithm problem. The protocol for sharing a secret key includes the following steps: 1. A prime $p$ and a generator $g$ of a multiplicative group mod $p$, are published publicly. 2. Alice chooses a number $a\in \mathbb{Z}_p$ and computes $A = g^a$. $a$ is known only to Alice. 3. Bob chooses a number $b\in \mathbb{Z}_p$ and computes $B = g^a$. $b$ is known only to Bob. 4. Alice sends Bob a message over a public channel containing $A$, Bob sends Alice a message containing $B$. 5. Alice computes the secret key $ K = B^a = g^{ab}$, and similarly Bob computes the secret key $K=A^b = g^{ab}$. If the eavesdropper, Eve, can implement the discrete logarithm algorithm, she can evaluate $a=\log_g(A)$ and $b = \log_g(B)$, by intercepting Alice's and Bob's messages, $A$ and $B$. Knowledge of $a$, $b$ and $g$ allows her a straightforward calculation of $K=g^{ab}$. a ## References \[1]: [Discrete Logarithm (Wikipedia)](https://en.wikipedia.org/wiki/Discrete_logarithm) \[2]: [Shor, Peter W. "Algorithms for quantum computation: discrete logarithms and factoring." Proceedings 35th annual symposium on foundations of computer science. IEEE, 1994.](https://ieeexplore.ieee.org/abstract/document/365700) \[3]: [Diffie-Hellman Key Exchange (Wikipedia)](https://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange) \[4]: [Hidden Subgroup Problem (Wikipedia)](https://en.wikipedia.org/wiki/Hidden_subgroup_problem) \[5]: [Continued Fraction (Wikipedia)](https://en.wikipedia.org/wiki/Continued_fraction) \[6]: [Proos, J., & Zalka, C. (2003). Shor's discrete logarithm quantum algorithm for elliptic curves. arXiv preprint quant-ph/0301141](https://arxiv.org/pdf/quant-ph/0301141) # Solving Elliptic Curve Discrete Logarithm Problem with Shor's Algorithm Source: https://docs.classiq.io/explore/algorithms/number_theory_and_cryptography/elliptic_curves/elliptic_curve_discrete_log Open this notebook in GitHub to run it yourself ## 1. Introduction and Problem Statement The **Elliptic Curve Discrete Logarithm Problem (ECDLP)** is a fundamental cryptographic challenge that underlies the security of elliptic curve cryptography (ECC). While classical algorithms require exponential time to solve ECDLP, Shor's quantum algorithm can solve it efficiently in polynomial time. # ## Elliptic Curve Definition An elliptic curve is a special type of mathematical curve defined by a cubic equation. For our purposes, we can think of it as the set of points $(x, y)$ that satisfy the **Weierstrass equation**: $$ E : y^2 = x^3 + ax + b $$ where $a$ and $b$ are constants that define the specific shape of the curve. **Key Properties**: * **Smooth curve**: The curve has no sharp corners, breaks, or self-intersections (this requires $4a^3 + 27b^2 \neq 0$) * **Symmetric**: The curve is symmetric about the x-axis - if $(x, y)$ is on the curve, then $(x, -y)$ is also on the curve * **Point at infinity**: We add a special "point at infinity" denoted $\mathcal{O}$ that serves as the identity element for point addition , as will be explained in the next section. **Cryptographic Context**: For cryptographic applications like Bitcoin's digital signatures, we work with elliptic curves over **finite fields** $\mathbb{F}_p$ where $p$ is a large prime number. Instead of the smooth curves we might visualize over real numbers, we get a discrete set of points: $$ E(\mathbb{F}_p) = \{(x, y) \in \mathbb{F}_p \times \mathbb{F}_p \mid y^2 = x^3 + ax + b\} \cup \{\mathcal{O}\} $$ The crucial property for cryptography is that these points form a **group** under a special addition operation. This means: * You can **"add"** any two points on the curve to get another point on the curve * There's an identity element ($\mathcal{O}$) where $P + \mathcal{O} = P$ for any point $P$ * Every point has an inverse * Addition is associative: $(P + Q) + R = P + (Q + R)$ # ## How Elliptic Curve Point Addition Works **The elliptic curve "addition" operation geometric interpretation**: To add two points $P$ and $Q$ on the curve, draw a straight line through them. This line will intersect the elliptic curve at exactly one more point $R'$. The sum $P + Q$ is then defined as the reflection of $R'$ across the x-axis. Elliptic curve point addition via line intersection ***Figure 1**: Visualized representation of the addition operation on the elliptic curve $y^2 = x^3 - 5x + 6$ over the real field.* **Algebraic Process**: This geometric process translates into explicit coordinate formulas: 1. Calculate a slope $\lambda$ based on the input points 2. Use $\lambda$ to compute the x-coordinate of the result: $x_3 = \lambda^2 - x_1 - x_2$ 3. Use $\lambda$ and $x_3$ to compute the y-coordinate: $y_3 = \lambda(x_1 - x_3) - y_1$ **Elliptic Curve Addition Cases**: In general, elliptic curve point addition must handle several cases: 1. **Generic case**: Two distinct points $P \neq Q$ where $P \neq -Q$ * Slope: $\lambda = \frac{y_Q - y_P}{x_Q - x_P} \pmod{p}$ 2. **Point doubling**: Adding a point to itself $P + P$ * Slope: $\lambda = \frac{3x_P^2 + a}{2y_P} \pmod{p}$ 3. **Inverse points**: $P + (-P) = \mathcal{O}$ (point at infinity) 4. **Adding point at infinity**: $P + \mathcal{O} = P$ **Group Property**: This addition rule ensures that the sum of any two points on the curve is always another point on the curve, maintaining the group property essential. # ## The Elliptic Curve Discrete Logarithm Problem (ECDLP) Formally, an instance of the **Elliptic Curve Discrete Logarithm Problem (ECDLP)** is defined as follows: Let $G \in E(\mathbb{F}_p)$ be a fixed and publicly known generator of a cyclic subgroup of $E(\mathbb{F}_p)$ with known order $\text{ord}(G) = r$. Let $P \in \langle G \rangle$ be a fixed and publicly known element in the subgroup generated by $G$. **The problem is to find the unique integer $l \in \{1, 2, \ldots, r\}$, called the discrete logarithm, such that $P = l \cdot G$.** **Why "Logarithm" for Point Addition?** The term **"discrete logarithm"** might seem confusing at first in the context of elliptic curves, since the group operation here is **point addition**, not multiplication. However, the **elliptic curve discrete logarithm problem (ECDLP)** is a **special case of the general discrete logarithm problem (DLP)** (see the ["discrete log" notebook](https://github.com/Classiq/classiq-library/blob/main/algorithms/number_theory_and_cryptography/discrete_log/discrete_log.ipynb)), where the group happens to be defined by elliptic curve point addition rather than modular multiplication. In the Discrete Logarithm Problem, the group operation is modular multiplication, and exponentiation refers to repeated multiplication . In the elliptic curve setting, the scalar multiplication $l \cdot G$ refers to adding the point $G$ to itself $l$ times. Below is a comparison of the two settings: | | **Discrete Logarithm Problem (DLP)** | **Elliptic Curve DLP (ECDLP)** | | ------------------- | ------------------------------------ | ------------------------------------------------------- | | **Group operation** | Modular multiplication | Modular point addition | | **Expression** | Modular exponentiation: $g^l = h$ | Modular scalar multiplication: $l \cdot G = P_{target}$ | | **Goal** | Find $l$ given $g$ and $h$ | Find $l$ given $G$ and $P_{target}$ | In both cases: * A **generator** element ($g$ or $G$) is known * A **target** element ($h$ or $P_{target}$) is known * The task is to find the **exponent or scalar** $l$ such that the group operation applied to the generator yields the target The term "logarithm" is used by analogy: just as logarithms solve for exponents in multiplicative groups, the discrete logarithm solves for the scalar in additive or elliptic curve groups. # ## Real-World Impact and Applications **Cryptographic Applications**: * **Digital Signatures**: Power cryptocurrency systems like Bitcoin and Ethereum, enabling secure ownership proofs without revealing private keys * **TLS/SSL Certificates**: Use ECC-based key exchange (e.g., ECDHE) to secure HTTPS traffic across the internet * **Mobile Communications**: Employ protocols such as ECDSA and ECIES for device authentication and secure messaging in 4G/5G networks * **Lightweight Cryptography**: ECC is well-suited for resource-constrained environments, and is used in protocols like DTLS and TLS-PSK for IoT security * **Secure Messaging and Encryption Standards**: ECC forms the basis of cryptographic protocols in standards like Suite B, used in high-security communications **Why ECC is Preferred Over RSA**: Elliptic curve cryptography offers the same security as RSA but with much smaller key sizes: * **ECC-256** provides security equivalent to **RSA-3072** * Smaller keys mean faster computations and less storage * Critical for resource-constrained devices like smartphones and IoT sensors # ## Our Specific Example $E(\mathbb{F}_7)$ We'll work with a concrete example on a small finite field to demonstrate the concepts, following the approach outlined in Roetteler et al. [\[1\]](#roetteler). Our specific example will demonstrate elliptic curve operations over points $(x, y) \in \mathbb{F}_7 \times \mathbb{F}_7$: **Elliptic Curve**: $y^2 = x^3 + 5x + 4 \pmod{7}$ **Problem**: Find the discrete logarithm $l$ such that: $$ l \cdot G = P_{target} $$ Where: * **Generator point** $G = [0, 5]$ * **Target point** $P_{target} = [0, 2]$ This means we need to find how many times we must add $G$ to itself to get $P_{target}$. **Solution**: $l = 4$, since $4 \cdot [0, 5] = [0, 2]$ Let's verify by computing the multiples of $G = [0, 5]$: * $1 \cdot G = [0, 5]$ * $2 \cdot G = [0, 5] + [0, 5] = [2, 1]$ (point doubling) * $3 \cdot G = [2, 1] + [0, 5] = [2, 6]$ * $4 \cdot G = [2, 6] + [0, 5] = [0, 2]$ $\checkmark$ Therefore, $l = 4$ is indeed the discrete logarithm we're seeking. **Curve Properties** The elliptic curve $y^2 = x^3 + 5x + 4 \pmod{7}$ contains the following points: * $[0, 2], [0, 5], [2, 1], [2, 6], [3, 2], [3, 5], [4, 2], [4, 5], [5, 0]$ * Plus the point at infinity $\mathcal{O}$ **Generator $G = [0, 5]$ properties**: * Multiples: $1 \cdot G = [0, 5]$, $2 \cdot G = [2, 1]$, $3 \cdot G = [2, 6]$, $4 \cdot G = [0, 2]$, $5 \cdot G = \mathcal{O}$ * Order: $r=5$ (since $5 \cdot G = \mathcal{O}$) ***Note:** The order $r$ is the smallest positive integer such that $r \cdot G = \mathcal{O}$ (point at infinity).* ```python theme={null} # Define our elliptic curve and problem parameters class EllipticCurve: def __init__(self, p, a, b): """ Represents an elliptic curve of the form y^2 = x^3 + a*x + b (mod p) """ self.p = p self.a = a self.b = b def __repr__(self): return f"EllipticCurve(p={self.p}, a={self.a}, b={self.b})" # Problem parameters for our specific ECDLP instance CURVE = EllipticCurve(p=7, a=5, b=4) GENERATOR_G = [0, 5] GENERATOR_ORDER = 5 INITIAL_POINT = [4, 2] TARGET_POINT = [0, 2] ``` ## 2. Shor's Algorithm for ECDLP Shor's quantum algorithm [\[2\]](#shor) provides an efficient way to solve the discrete logarithm problem that would be intractable for classical computers. The algorithm proceeds as follows: # ## Algorithm Overview 1. **Superposition Preparation**: Create two quantum registers in a superposition of all possible integers between 0 and a large number 2. **Periodic Function Evaluation**: Apply a periodic function that takes the two registers as input and computes the output in an auxiliary register 3. **Period Extraction**: Apply an inverse Quantum Fourier Transform to reveal the hidden period of the function Shor's algorithm for solving the ECDLP is analogous to Shor's algorithm for integer factorization. Both prepare large superpositions of inputs, feed them to a periodic function, and exploit the QFT routine to reveal hidden periodicities that allow efficient computation of the discrete logarithm (see the ["discrete log" notebook](https://github.com/Classiq/classiq-library/blob/main/algorithms/number_theory_and_cryptography/discrete_log/discrete_log.ipynb)). # ### The Periodic Function for Elliptic Curve The key function evaluated in quantum superposition is: $$ f(x_1, x_2) = x_1 \cdot G - x_2 \cdot P_{target} = (x_1 - x_2 \cdot l) \cdot G $$ where the last equality holds by the definition of the discrete logarithm $l$ (since $P_{target} = l \cdot G$). The function $f$ exhibits periodicity in both variables: $$ \forall r, \quad f(x_1 + r \cdot l, x_2 + r) = f(x_1, x_2) $$ where $r$ is the order of the generator $G$. When the quantum measurement finds inputs where $f(x_1, x_2) = \mathcal{O}$, the quantum Fourier transform extracts this period structure, revealing relationships that allow us to solve for the discrete logarithm $l$. *In our quantum implementation, we actually evaluate $P_0 + x_1 \cdot G - x_2 \cdot P_{target}$ where $P_0$ serves as an auxiliary starting point that doesn't affect the final discrete logarithm but helps avoid certain edge cases in the quantum arithmetic.* # ## Quantum Oracle Function Implementation The core of our implementation is the `shor_ecdlp` quantum function that orchestrates the entire algorithm. It implements the function $$ |x_1\rangle|x_2\rangle|0\rangle \rightarrow |x_1\rangle|x_2\rangle| P_0 + x_1 \cdot G - x_2 \cdot P_{target}\rangle. $$ # ### Expected Quantum Flow 1. **Initialization** : All variables start in $|0\rangle$ state 2. **State Preparation** : Set initial elliptic curve point 3. **Superposition Creation** : Apply transformation to create superposition over the possible values of $x_1$ and $x_2$ 4. **Quantum Arithmetic** : Perform elliptic curve computation $P_0 + x_1 \cdot G - x_2 \cdot P_{target}$ in superposition 5. **Period Extraction** : Apply inverse QFT to extract period information **Quantum Struct Approach**: We use a `QStruct` called `EllipticCurvePoint` to group the x and y coordinates together, making the code more organized and mathematically intuitive. **Quantum Variables:** | Variable | Purpose | | -------- | ------------------------------------------------------- | | `x1` | First quantum register for period finding (ranges 0-7) | | `x2` | Second quantum register for period finding (ranges 0-7) | | `ecp` | Quantum elliptic curve point during computation | **Classical Parameters:** | Variable | Purpose | | ---------- | --------------- | | `P_0` | Starting point | | `G` | Generator point | | `P_target` | Target point | \*Note: In this case the order is not a power of 2. So instead of creating the entire uniform distribution on the `x1`, `x2` variables, we load them with the uniform superposition of only the first `#GENERATOR_ORDER` states (see the ["discrete log" notebook](https://github.com/Classiq/classiq-library/blob/main/algorithms/number_theory_and_cryptography/discrete_log/discrete_log.ipynb) for more examples).\* *We do that using the `prepare_uniform_trimmed_state` library function, which efficiently prepares such a state.* ```python theme={null} from classiq import * from classiq.qmod.symbolic import ceiling, log class EllipticCurvePoint(QStruct): x: QNum[CURVE.p.bit_length()] y: QNum[CURVE.p.bit_length()] @qfunc def shor_ecdlp( x1: Output[QNum], # first quantum variable for period finding x2: Output[QNum], # second quantum variable for period finding ecp: Output[ EllipticCurvePoint ], # third quantum variable for elliptic curve point coordinates P_0: list[int], # starting point - classical G: list[int], # generator point - classical P_target: list[int], # target point - classical ) -> None: """ Main quantum function implementing Shor's algorithm for ECDLP. """ # Step 1: Allocate quantum resources to variables var_len = GENERATOR_ORDER.bit_length() allocate(var_len, False, var_len, x1) allocate(var_len, False, var_len, x2) # Step 2: Initialize ecp to P_0 allocate(ecp) ecp.x ^= P_0[0] ecp.y ^= P_0[1] # Step 3: Create superposition on x1 and x2 hadamard_transform(x1) hadamard_transform(x2) # Step 4: Quantum elliptic curve arithmetic in superposition # First: ecp = P_0 + x1·G ec_scalar_mult_add(ecp, x1, G, CURVE.p, CURVE.a, CURVE.b) # Second: ecp = P_0 + x1·G - x2·P_target neg_target = [P_target[0], (-P_target[1]) % CURVE.p] ec_scalar_mult_add(ecp, x2, neg_target, CURVE.p, CURVE.a, CURVE.b) # Step 5: Inverse Quantum Fourier Transform for period extraction invert(lambda: qft(x1)) invert(lambda: qft(x2)) ``` ## 3. Quantum Elliptic Curve Addition # ## Algorithm Hierarchy Overview The `shor_ecdlp` function orchestrates Shor's algorithm, but its computational core is the `ec_scalar_mult_add` function, which performs quantum scalar multiplication in superposition. This function computes the expression $P_0 + k \cdot G$ where $k$ is in quantum superposition, enabling the algorithm to evaluate all possible scalar values simultaneously. # ## Quantum Scalar Multiplication (`ec_scalar_mult_add`) Our implementation uses an efficient hybrid classical-quantum approach. We precompute all the powers-of-two multiples of the base point $P$ classically: $P, 2P, 4P, 8P, \ldots, 2^{n-1}P$. Then we compute the scalar multiple using controlled additions of these precomputed points following the binary representation of the scalar. **Algorithm**: For scalar $k = \sum_{i=0}^{n-1} k_i 2^i$ with $k_i \in \{0, 1\}$: $$ k \cdot P = \sum_{i=0}^{n-1} k_i 2^i P = \sum_{i=0}^{n-1} k_i (2^i P) $$ **Implementation Steps**: 1. **Classical Preprocessing**: Compute powers $P, 2P, 4P, \ldots$ classically 2. **Quantum Control**: For each bit $k_i$, perform controlled addition of $2^i P$ to the accumulator When $k$ is in superposition $|k\rangle$, all possible scalar values are processed simultaneously: $$ |k\rangle|(x,y)\rangle \rightarrow |k\rangle|(x,y) + k \cdot P\rangle $$ All doubling operations happen classically through the `ell_double_classical` function, leaving only controlled point additions for the in-place elliptic curve point addition quantum function `ec_point_add`. ```python theme={null} @qperm def ec_scalar_mult_add( ecp: EllipticCurvePoint, # elliptic curve point k: QArray[QBit], # scalar in binary representation (LSB to MSB) P: list[int], # classical point to multiply [x, y] p: int, # prime modulus a: int, b: int, # curve parameters ) -> None: """ Quantum scalar multiplication: computes k*P and adds to ecp in-place. """ n = k.size # Number of bits in scalar k current_power = P.copy() # Start with 1·P = P # Process each bit of k from LSB (bit 0) to MSB (bit n-1) for i in range(n): # Controlled point addition: if k[i] = 1, add current_power to accumulator control(k[i], lambda: ec_point_add(ecp, current_power, p)) # Classical update for next iteration: current_power = 2 * current_power if i < n - 1: # Don't double after the last iteration curve = EllipticCurve(p=p, a=a, b=b) current_power = ell_double_classical(current_power, curve) ``` # ## Classical Point Doubling (`ell_double_classical`) The doubling of the classically known generator is implemented as follows: ```python theme={null} def ell_double_classical(P, curve): """ Classical elliptic curve point doubling for updating powers in ec_scalar_mult_add. Returns 2P for a point P on the elliptic curve. """ p = curve.p x, y = P # Slope calculation: s = (3*x² + a) / (2*y) mod p numerator = (3 * (x * x % p) + curve.a) % p denominator = (2 * y) % p s = (numerator * pow(denominator, -1, p)) % p # x-coordinate of the result xr = (s * s - 2 * x) % p # y-coordinate of the result yr = (y - s * ((x - xr) % p)) % p # Return the result, with y in the standard form return [xr, (p - yr) % p] ``` # ## Quantum Point Addition (`ec_point_add`) The `ec_point_add` function performs the fundamental operation of adding two elliptic curve points in a quantum-reversible manner. **Our Implementation Simplification**: Following the approach in [\[1\]](#roetteler), our implementation focuses only on the **generic case** where the two points are distinct, not inverses of each other, and neither is the neutral element. These exceptional cases are rare (for large $p$) , except when the accumulation register is initialized to the neutral element. **Note:** \*To avoid edge cases (as described above) through the entire quantum scalar multiplication, we chose to initialize the accumulation elliptic curve point with a non-zero point ($P_0 = [4, 2]$) rather than the neutral element. This strategy does not affect the measurement statistics after the Quantum Fourier Transform, since it only adds a global phase to the final quantum state. We deliberately selected a starting point that lies on the elliptic curve but is not within the subgroup generated by $G$. Additionally, we exploited the fact that the order of $G$ is not a power of 2\* *Disclaimer: This is an engineered example chosen to allow simulation of a small model where edge cases effect the results. In general, such an initialization is not always possible.* **Algorithm Overview** (Generic Case Only): 1. **Input**: Quantum point $(x_1, y_1)$ and classical point $G = (G_x, G_y)$ 2. **Slope calculation**: Compute $\lambda = \frac{y_1 - G_y}{x_1 - G_x} \pmod{p}$ using quantum modular inverse 3. **Result coordinates**: * $x_{result}:= x_3 = \lambda^2 - x_1 - G_x \pmod{p}$ * $y_{result}:= y_3 = \lambda(x_1 - x_3) - y_1 \pmod{p}$ 4. **Cleanup**: Uncompute auxiliary values to maintain reversibility **Reference**: The `ec_point_add` function is based on **Algorithm 1** from *"Quantum resource estimates for computing elliptic curve discrete logarithms"* by Roetteler et al. (2017) [\[1\]](#roetteler) To implement `ec_point_add`, we need a complete library of reversible modular arithmetic operations which we import directly from the Classiq open library. # ### Modular Arithmetic Functions Imported from Classiq Open Library * Organized by Category: *Basic Modular Operations*: * `modular_add_inplace` * Modular addition * `modular_negate_inplace` * Modular negation * `modular_subtract_inplace` * Modular subtraction *Constant Operations*: * `modular_add_constant_inplace` * Modular addition of a classical constant *Multiplication Operations*: * `modular_multiply` * Modular multiplication * `modular_square` * Modular squaring *In our example, we will first implement modular inversion using a mock quantum function `mock_modular_inverse` based on a classical lookup table specifically for modulo 7, instead of implementing the full quantum modular inversion function `modular_inverse_inplace` (based on "Kaliski's algorithm"), which requires significant qubit resources.* \*The above choice is driven by the practical desire to first fully simulate the entire elliptic curve discrete logarithm (ECDLP) algorithm flow within the limits of currently available quantum simulators. The full ECDLP synthesized circuit (including the full `modular_inverse_inplace` implementation) is presented at the end of this notebook.\* ```python theme={null} import math from classiq.qmod.symbolic import subscript @qperm def mock_modular_inverse(x: Const[QNum], result: QNum, modulus: int) -> None: """ Performs the transformation |x>|0> → |x>|x^(-1) mod modulus> for x in 1..modulus- 1. This is a mock implementation using controlled operations based on lookup table values. If the modular inverse does not exist, sets the 2nd register to 0. """ # Generate the lookup table for modular inverses inverse_table = lookup_table( lambda _x: pow(_x, -1, modulus) if math.gcd(_x, modulus) == 1 else 0, x ) result ^= subscript(inverse_table, x) ``` ```python theme={null} @qperm def ec_point_add( ecp: EllipticCurvePoint, G: list[int], # Classical point coordinates [Gx, Gy] on the curve p: int, # Prime modulus ) -> None: """ Performs in-place elliptic curve point addition of a point whose coordinates are stored in quantum struct object and a classically known point. """ n = CURVE.p.bit_length() slope = QNum() # aux quantum register for lambda (slope) allocate(n, slope) t0 = QNum() # aux quantum register for internal arithmetic allocate(n, t0) # Extract classical coordinates Gx = G[0] # x2 Gy = G[1] # y2 # Step 1: # y <-- (y1 - y2) mod p modular_add_constant_inplace(p, (-Gy) % p, ecp.y) # Step 2: # x <-- (x1 - x2) mod p modular_add_constant_inplace(p, (-Gx) % p, ecp.x) # Step 3: # λ <-- (y1 - y2) / (x1 - x2) mod p within_apply( lambda: mock_modular_inverse(ecp.x, t0, p), # t0 <-- (x1 - x2)^(-1) mod p lambda: modular_multiply(p, t0, ecp.y, slope), # λ <-- t0 * (y1 -y2) ) # Step 4: y <-- 0 within_apply( lambda: modular_multiply( p, slope, ecp.x, t0 ), # t0 <-- λ * (x1 -x2) = (y1 - y2) = y lambda: inplace_xor(t0, ecp.y), # y <-- 0 ) # Step 5: x <-- x2 - x3 within_apply( lambda: modular_square(p, slope, t0), # t0 = λ² lambda: ( modular_subtract_inplace(p, t0, ecp.x), # x <-- t0 - x = λ² - (x1 - x2) modular_negate_inplace(p, ecp.x), # x <-- -x = x1 - x2 - λ² modular_add_constant_inplace( p, (3 * Gx) % p, ecp.x ), # x <-- x1 - x2 - λ² + 3*x2 = x2 - x3 ), ) # Step 6: y <-- y3 + y2 modular_multiply(p, slope, ecp.x, ecp.y) # y = λ * (x2 - x3) = y3 + y2 # Step 7: λ <-- 0 t1 = QNum() # aux quantum register for manually uncomputing the slope within_apply( lambda: mock_modular_inverse(ecp.x, t0, p), # t0 <-- (x2 - x3)^(-1) lambda: within_apply( lambda: ( allocate(CURVE.p.bit_length(), t1), modular_multiply(p, t0, ecp.y, t1), # t1 <-- (y3 + y2)/(x2 - x3) = λ ), lambda: inplace_xor(t1, slope), # λ <-- 0 ), ) free(slope) # Step 8: Final coordinate adjustments modular_add_constant_inplace(p, (-Gy) % p, ecp.y) # y <-- y3 + y2 - y2 = y3! modular_negate_inplace(p, ecp.x) # x <-- x3 - x2 modular_add_constant_inplace(p, Gx, ecp.x) # x <-- x3 - x2 + x2 = x3! ``` **Note:** *To enable reversible in-place point addition, the slope* $\lambda$ *can be recomputed (as can be observed from step 6 in `ec_point_add`) from the output point* $P_3$ *and the known input* $P_2$ *using the identity:* $$ \frac{y_1 - y_2}{x_1 - x_2} = -\frac{y_3 + y_2}{x_3 - x_2} $$ *This ensures that* $P_1$ *can be safely overwritten with* $P_3$ *while still allowing recovery of* $\lambda$ *for uncomputing it.* # ### Step-By-Step Point Addition Example Let's see how elliptic curve point addition actually works by computing $[4, 2] + [0, 5]$: **Step 1: Calculate the slope** For two distinct points $P_1 = (x_1, y_1) = (4, 2)$ and $P_2 = (x_2, y_2) = (0, 5)$: $$ \lambda = \frac{y_2 - y_1}{x_2 - x_1} = \frac{5 - 2}{0 - 4} = \frac{3}{-4} = \frac{3}{3} = 1 \pmod{7} $$ Note: $-4 \equiv 3 \pmod{7}$, and $3^{-1} \equiv 5 \pmod{7}$ since $3 \times 5 = 15 \equiv 1 \pmod{7}$ Actually: $\lambda = 3 \times 5 = 15 \equiv 1 \pmod{7}$ **Step 2: Calculate the x-coordinate of the result** $$ x_3 = \lambda^2 - x_1 - x_2 = 1^2 - 4 - 0 = 1 - 4 = -3 \equiv 4 \pmod{7} $$ **Step 3: Calculate the y-coordinate of the result** $$ y_3 = \lambda(x_1 - x_3) - y_1 = 1 \times (4 - 4) - 2 = 0 - 2 = -2 \equiv 5 \pmod{7} $$ **Result**: $[4, 2] + [0, 5] = [4, 5]$ Lets apply `ec_point_add` to add $P_1$ and $P_2$: ```python theme={null} # Define the specific points from the notebook example P1 = [4, 2] # Starting quantum point (will be modified in-place) P2 = [0, 5] # Classical point to add expected_result = [4, 5] @qfunc def main( ecp: Output[EllipticCurvePoint], ) -> None: """Main quantum function for testing ec_point_add""" # Initialize elliptic curve point with P1 coordinates allocate(ecp) ecp.x ^= P1[0] # x = 4 ecp.y ^= P1[1] # y = 2 # Perform point addition: ecp = P1 + P2 = [4, 2] + [0, 5] ec_point_add(ecp, P2, CURVE.p) # Set up optimization constraints constraints = Constraints(optimization_parameter="width") preferences = Preferences(optimization_level=1, qasm3=True) # Create and synthesize quantum model qmod = create_model(main, constraints=constraints, preferences=preferences) print("Synthesizing quantum circuit for ec_point_add...") qprog_point_add = synthesize(qmod) # Display circuit information print(f"Number of qubits: {qprog_point_add.data.width}") print(f"Program depth: {qprog_point_add.transpiled_circuit.depth}") show(qprog_point_add) # Execute the quantum circuit print("Executing quantum circuit...") result = execute(qprog_point_add).result() print("Execution complete.") ``` **Output:** ``` Synthesizing quantum circuit for ec_point_add... Number of qubits: 17 Program depth: 10928 Quantum program link: https://platform.classiq.io/circuit/37HaGh1CUEPeQILCn6CknGWMziX Executing quantum circuit... Execution complete. ``` ```python theme={null} # Extract quantum results quantum_results = result[0].value.parsed_counts[0].state ec_point_result = [quantum_results["ecp"]["x"], quantum_results["ecp"]["y"]] # Verify the result assert len(quantum_results) assert ec_point_result == expected_result print(f"Quantum result matches expected manual calculation!") print(f"Verified: [4, 2] + [0, 5] = [4, 5] on curve y² = x³ + 5x + 4 (mod 7)") ``` **Output:** ``` Quantum result matches expected manual calculation! Verified: [4, 2] + [0, 5] = [4, 5] on curve y² = x³ + 5x + 4 (mod 7) ``` ## 4. Execute the Entire Algorithm Flow We are now ready to synthesize and execute the complete Shor's ECDLP algorithm implemented with the `shor_ecdlp` function. # ## Main Function ```python theme={null} @qfunc def main(x1: Output[QNum], x2: Output[QNum], ecp: Output[EllipticCurvePoint]) -> None: # Call shor_ecdlp with the required parameters shor_ecdlp(x1, x2, ecp, INITIAL_POINT, GENERATOR_G, TARGET_POINT) ``` Screenshot 2025-08-21 at 17.31.09.png # ## Model Creation and Synthesis ```python theme={null} print("Creating model for Shor's ECDLP algorithm...") constraints = Constraints(optimization_parameter="width") preferences = Preferences(timeout_seconds=3600, optimization_level=1, qasm3=True) qmod = create_model(main, constraints=constraints, preferences=preferences) print("Synthesizing quantum program...") qprog_shor_ecdlp = synthesize(qmod) # Display circuit metrics num_qubits = qprog_shor_ecdlp.data.width print(f"Number of qubits: {num_qubits}") show(qprog_shor_ecdlp) ``` # ## Quantum Program Analysis We can analyze the transpiled circuit characteristics from the quantum program object: ```python theme={null} qprog_shor_ecdlp.transpiled_circuit.count_ops ``` **Output:** ``` {'cx': 60828, 'p': 44368, 'u': 19619, 'rz': 4104, 'h': 3301, 'tdg': 2844, 't': 2844, 'u1': 792, 'x': 458} ``` ```python theme={null} qprog_shor_ecdlp.transpiled_circuit.depth ``` **Output:** ``` 80093 ``` # ## Execution ```python theme={null} res = execute(qprog_shor_ecdlp).result_value() ``` # ## Results Collection ```python theme={null} df = res.dataframe df_sorted = df.sort_values("counts", ascending=False) df_sorted["probability"] = df_sorted["counts"] / df["counts"].sum() print(f"\nQuantum Execution Results:") print(f"Total distinct pairs: {len(df)}, Total measurements: {df['counts'].sum()}") print("\nTop 10 results:") print(df_sorted.head(10)) ``` **Output:** ``` Quantum Execution Results: Total distinct pairs: 221, Total measurements: 2048 Top 10 results: x1 x2 ecp.x ecp.y count probability bitstring 0 0.000 0.000 3 2 100 0.048828 010011000000 1 0.000 0.000 5 0 95 0.046387 000101000000 2 0.375 0.375 3 2 87 0.042480 010011011011 3 0.625 0.625 3 2 83 0.040527 010011101101 4 0.000 0.000 4 5 82 0.040039 101100000000 5 0.625 0.625 5 0 77 0.037598 000101101101 6 0.375 0.375 5 0 71 0.034668 000101011011 7 0.000 0.000 4 2 71 0.034668 010100000000 8 0.000 0.000 3 5 69 0.033691 101011000000 9 0.625 0.625 3 5 61 0.029785 101011101101 ``` ## 5. Post-Processing Results For post-processing our results, we will follow the same logic as explained in the ["discrete log" notebook](https://github.com/Classiq/classiq-library/blob/main/algorithms/number_theory_and_cryptography/discrete_log/discrete_log.ipynb). We translate each result to the closest fraction with a denominator, which is the order: ```python theme={null} def closest_fraction(x, denominator): return round(x * denominator) df_sorted["x1_rounded"] = closest_fraction(df_sorted.x1, GENERATOR_ORDER) df_sorted["x2_rounded"] = closest_fraction(df_sorted.x2, GENERATOR_ORDER) df_sorted.head(10) ``` | | x1 | x2 | ecp.x | ecp.y | count | probability | bitstring | x1\_rounded | x2\_rounded | | - | ----- | ----- | ----- | ----- | ----- | ----------- | ------------ | ----------- | ----------- | | 0 | 0.000 | 0.000 | 3 | 2 | 100 | 0.048828 | 010011000000 | 0.0 | 0.0 | | 1 | 0.000 | 0.000 | 5 | 0 | 95 | 0.046387 | 000101000000 | 0.0 | 0.0 | | 2 | 0.375 | 0.375 | 3 | 2 | 87 | 0.042480 | 010011011011 | 2.0 | 2.0 | | 3 | 0.625 | 0.625 | 3 | 2 | 83 | 0.040527 | 010011101101 | 3.0 | 3.0 | | 4 | 0.000 | 0.000 | 4 | 5 | 82 | 0.040039 | 101100000000 | 0.0 | 0.0 | | 5 | 0.625 | 0.625 | 5 | 0 | 77 | 0.037598 | 000101101101 | 3.0 | 3.0 | | 6 | 0.375 | 0.375 | 5 | 0 | 71 | 0.034668 | 000101011011 | 2.0 | 2.0 | | 7 | 0.000 | 0.000 | 4 | 2 | 71 | 0.034668 | 010100000000 | 0.0 | 0.0 | | 8 | 0.000 | 0.000 | 3 | 5 | 69 | 0.033691 | 101011000000 | 0.0 | 0.0 | | 9 | 0.625 | 0.625 | 3 | 5 | 61 | 0.029785 | 101011101101 | 3.0 | 3.0 | Now, we take a sample where `x2` is co-prime to the order, such that we can get the logarithm by multiplying `x1` by the modular inverse. If the `x1`, `x2` registers are large enough, we are guaranteed to sample it with a good probability. From the valid solutions, we solve for the discrete logarithm using: $$ \text{logarithm} = - x_1 \cdot x_2^{-1} \bmod r $$ ```python theme={null} import numpy as np def modular_inverse(x): return [pow(a, -1, GENERATOR_ORDER) for a in x] df_sorted = df_sorted[ np.gcd(df_sorted.x2_rounded.astype(int), GENERATOR_ORDER) == 1 ].copy() df_sorted["x2_inverse"] = modular_inverse(df_sorted.x2_rounded.astype("int")) df_sorted["logarithm"] = -df_sorted.x1_rounded * df_sorted.x2_inverse % GENERATOR_ORDER df_sorted.head(10) ``` | | x1 | x2 | ecp.x | ecp.y | count | probability | bitstring | x1\_rounded | x2\_rounded | x2\_inverse | logarithm | | -- | ----- | ----- | ----- | ----- | ----- | ----------- | ------------ | ----------- | ----------- | ----------- | --------- | | 2 | 0.375 | 0.375 | 3 | 2 | 87 | 0.042480 | 010011011011 | 2.0 | 2.0 | 3 | 4.0 | | 3 | 0.625 | 0.625 | 3 | 2 | 83 | 0.040527 | 010011101101 | 3.0 | 3.0 | 2 | 4.0 | | 5 | 0.625 | 0.625 | 5 | 0 | 77 | 0.037598 | 000101101101 | 3.0 | 3.0 | 2 | 4.0 | | 6 | 0.375 | 0.375 | 5 | 0 | 71 | 0.034668 | 000101011011 | 2.0 | 2.0 | 3 | 4.0 | | 9 | 0.625 | 0.625 | 3 | 5 | 61 | 0.029785 | 101011101101 | 3.0 | 3.0 | 2 | 4.0 | | 10 | 0.375 | 0.375 | 4 | 5 | 59 | 0.028809 | 101100011011 | 2.0 | 2.0 | 3 | 4.0 | | 11 | 0.625 | 0.625 | 4 | 5 | 59 | 0.028809 | 101100101101 | 3.0 | 3.0 | 2 | 4.0 | | 12 | 0.375 | 0.375 | 3 | 5 | 58 | 0.028320 | 101011011011 | 2.0 | 2.0 | 3 | 4.0 | | 13 | 0.375 | 0.375 | 4 | 2 | 56 | 0.027344 | 010100011011 | 2.0 | 2.0 | 3 | 4.0 | | 14 | 0.625 | 0.625 | 4 | 2 | 52 | 0.025391 | 010100101101 | 3.0 | 3.0 | 2 | 4.0 | ```python theme={null} assert len(df_sorted.logarithm) > 0 assert np.allclose(df_sorted.logarithm[:10], 4) ``` # ## **Quantum Algorithm Success** The fact that multiple valid pairs yield the **same discrete logarithm (l = 4)** confirms the quantum algorithm worked correctly, extracting the hidden period structure from Shor's algorithm. ## 6. The Full Quantum Elliptic Curve Addition Quantum Program Now that we successfully verified the full ECDLP flow we can synthesize and analyze the full implementation of `ec_point_add` using the quantum modular inversion function `modular_inverse_inplace` (based on "Kaliski's algorithm") - imported directly from the Classiq Open Library. ```python theme={null} @qperm def ec_point_add( ecp: EllipticCurvePoint, G: list[int], # Classical point coordinates [Gx, Gy] on the curve p: int, # Prime modulus ) -> None: """ Performs in-place elliptic curve point addition of a point whose coordinates are stored in quantum struct object and a classically known point. """ slope = QNum() # aux quantum register for lambda (slope) allocate(CURVE.p.bit_length(), slope) t0 = QNum() # aux quantum register for internal arithmetic m = QArray() # Extract classical coordinates Gx = G[0] # x2 Gy = G[1] # y2 # Step 1: # y <-- (y1 - y2) mod p modular_add_constant_inplace(p, (-Gy) % p, ecp.y) # Step 2: # x <-- (x1 - x2) mod p modular_add_constant_inplace(p, (-Gx) % p, ecp.x) # Step 3: # λ <-- (y1 - y2) / (x1 - x2) mod p within_apply( lambda: modular_inverse_inplace(p, ecp.x, m), # t0 <-- (x1 - x2)^(-1) mod p lambda: modular_multiply(p, ecp.x, ecp.y, slope), # λ <-- t0 * (y1 -y2) ) # free(m) allocate(CURVE.p.bit_length(), t0) # Step 4: y <-- 0 within_apply( lambda: modular_multiply( p, slope, ecp.x, t0 ), # t0 <-- λ * (x1 -x2) = (y1 - y2) = y lambda: inplace_xor(t0, ecp.y), # y <-- 0 ) # Step 5: x <-- x2 - x3 within_apply( lambda: modular_square(p, slope, t0), # t0 = λ² lambda: ( modular_subtract_inplace(p, t0, ecp.x), # x <-- t0 - x = λ² - (x1 - x2) modular_negate_inplace(p, ecp.x), # x <-- -x = x1 - x2 - λ² modular_add_constant_inplace( p, (3 * Gx) % p, ecp.x ), # x <-- x1 - x2 - λ² + 3*x2 = x2 - x3 ), ) free(t0) # Step 6: y <-- y3 + y2 modular_multiply(p, slope, ecp.x, ecp.y) # y = λ * (x2 - x3) = y3 + y2 # Step 7: λ <-- 0 t1 = QNum() # aux quantum register for manually uncomputing the slope within_apply( lambda: modular_inverse_inplace(p, ecp.x, m), # t0 <-- (x2 - x3)^(-1) lambda: within_apply( lambda: ( allocate(CURVE.p.bit_length(), t1), modular_multiply(p, ecp.x, ecp.y, t1), # t1 <-- (y3 + y2)/(x2 - x3) = λ ), lambda: inplace_xor(t1, slope), # λ <-- 0 ), ) free(slope) # Step 8: Final coordinate adjustments modular_add_constant_inplace(p, (-Gy) % p, ecp.y) # y <-- y3 + y2 - y2 = y3! modular_negate_inplace(p, ecp.x) # x <-- x3 - x2 modular_add_constant_inplace(p, Gx, ecp.x) # x <-- x3 - x2 + x2 = x3! ``` ```python theme={null} # Define the specific points from the notebook example P1 = [4, 2] # Starting quantum point (will be modified in-place) P2 = [0, 5] # Classical point to add expected_result = [4, 5] @qfunc def main( ecp: Output[EllipticCurvePoint], ) -> None: """Main quantum function for testing ec_point_add""" # Initialize elliptic curve point with P1 coordinates allocate(ecp) ecp.x ^= P1[0] # x = 4 ecp.y ^= P1[1] # y = 2 # Perform point addition: ecp = P1 + P2 = [4, 2] + [0, 5] ec_point_add(ecp, P2, CURVE.p) # Set up optimization constraints constraints = Constraints(optimization_parameter="width") preferences = Preferences(timeout_seconds=3600, optimization_level=1, qasm3=True) # Create and synthesize quantum model qmod = create_model(main, constraints=constraints, preferences=preferences) print("Synthesizing quantum circuit for ec_point_add...") qprog_point_add_full = synthesize(qmod) # Display circuit information print(f"Number of qubits: {qprog_point_add_full.data.width}") print(f"Program depth: {qprog_point_add_full.transpiled_circuit.depth}") show(qprog_point_add_full) ``` **Output:** ``` Synthesizing quantum circuit for ec_point_add... Number of qubits: 34 Program depth: 52180 Quantum program link: https://platform.classiq.io/circuit/37HdFbVHmK50VrFETZnFPpjwiEU ``` ## 7. The Full ECDLP Quantum Program This section presents the complete synthesis of Shor's ECDLP algorithm using the full quantum modular arithmetic library, demonstrating the end-to-end quantum circuit that solves the elliptic curve discrete logarithm problem with all components implemented using the Classiq Open Library functions. ```python theme={null} @qfunc def main(x1: Output[QNum], x2: Output[QNum], ecp: Output[EllipticCurvePoint]) -> None: # Call shor_ecdlp with the required parameters shor_ecdlp(x1, x2, ecp, INITIAL_POINT, GENERATOR_G, TARGET_POINT) ``` ```python theme={null} print("Creating model for Shor's ECDLP algorithm...") constraints = Constraints(optimization_parameter="width") preferences = Preferences(timeout_seconds=3600, optimization_level=1, qasm3=True) print("Synthesizing quantum program...") qprog_shor_ecdlp_full = synthesize( main, constraints=constraints, preferences=preferences ) # Display circuit metrics print(f"Number of qubits: {qprog_shor_ecdlp_full.data.width}") print(f"Program depth: {qprog_shor_ecdlp_full.transpiled_circuit.depth}") show(qprog_shor_ecdlp_full) ``` **Output:** ``` Creating model for Shor's ECDLP algorithm... Synthesizing quantum program... Number of qubits: 40 Program depth: 326809 Quantum program link: https://platform.classiq.io/circuit/37HgZ8JOkfm2KeURploTQnYGt36 ``` # ## Algorithm Complexity and Quantum Advantage **Classical Complexity**: The best known classical algorithms for ECDLP (such as Pollard's rho algorithm) require $O(\sqrt{n})$ operations, where $n$ is the order of the elliptic curve group. For cryptographically relevant curves with 256-bit keys, this means approximately $2^{128}$ operations. **Quantum Complexity**: Shor's algorithm reduces this to $O((\log n)^3)$ operations using $O(\log n)$ qubits, providing an exponential speedup. For the same 256-bit curves, this reduces to approximately $2^{24}$ operations. ## References \[1]: [ Martin Roetteler, Michael Naehrig, Krysta M. Svore, and Kristin Lauter. "Quantum resource estimates for computing elliptic curve discrete logarithms." *arXiv preprint arXiv:1706.06752* (2017).](https://arxiv.org/pdf/1706.06752) \[2]: [Peter W. Shor. "Polynomial-time algorithms for prime factorization and discrete logarithms on a quantum computer." *SIAM Journal on Computing*, 26(5):1484-1509 (1997). ](https://arxiv.org/abs/quant-ph/9508027) # Hidden-Shift Problem for Bent Functions Source: https://docs.classiq.io/explore/algorithms/number_theory_and_cryptography/hidden_shift/hidden_shift Open this notebook in GitHub to run it yourself image.png Here we implement the hidden shift algorithm for the family of Boolean bent functions using the Classiq platform. Make sure we have all necessary packages: ```python theme={null} !pip install galois ``` We assume we know how to implement the dual of $f$ and get $s$ according to the algorithm in \[[1](#first)]:Screen Shot 2023-06-27 at 18.05.48.png ```python theme={null} from classiq import * @qfunc def hidden_shift( oracle: QCallable[QArray], oracle_shifted: QCallable[QArray], target: QArray, ) -> None: hadamard_transform(target) oracle_shifted(target) hadamard_transform(target) oracle(target) hadamard_transform(target) NUM_VARIABLES = 4 @qfunc def main(s: Output[QArray]) -> None: @qperm def arith_func(vars: Const[QArray[QBit, NUM_VARIABLES]], res: QBit): res ^= (vars[0] & vars[1]) ^ (vars[2] & vars[3]) @qperm def arith_func_shifted(vars: Const[QArray[QBit, NUM_VARIABLES]], res: QBit): res ^= ((vars[0] ^ 1) & vars[1]) ^ (vars[2] & vars[3]) allocate(NUM_VARIABLES, s) hidden_shift( lambda y: phase_oracle(arith_func, y), lambda y: phase_oracle(arith_func_shifted, y), s, ) constraints = Constraints(optimization_parameter="width") qmod_simple = create_model(main, constraints, out_file="hidden_shift") qprog_simple = synthesize(qmod_simple) show(qprog_simple) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/32pMyZ4KsLkk15xFLwNWE3aPWER ``` ```python theme={null} sample_results_simple = execute(qprog_simple).result_value() sample_results_simple.counts_of_output("s") ``` **Output:** ``` {'1000': 2048} ``` ## More Complex Functions We take a Maiorana-McFarland function with random permutation on the `y` and `h` function is the `and` operation between all the y-variables. ```python theme={null} import random from functools import reduce import numpy as np NUM_VARIABLES = 16 # Define the list my_list = list(range(NUM_VARIABLES // 2)) # Get a random permutation random.seed(1) random.shuffle(my_list) # Create a permutation dict and its inverse perm_dict = {i: my_list[i] for i in range(NUM_VARIABLES // 2)} inverse_perm_dict = {v: k for k, v in perm_dict.items()} def h(y): return reduce(lambda a, b: a & b, [y[i] for i in range(NUM_VARIABLES // 2)]) def h_dual(x): return reduce( lambda a, b: a & b, [x[inverse_perm_dict[i]] for i in range(NUM_VARIABLES // 2)] ) def f_func(x, y): return ( reduce( lambda a, b: a ^ b, [x[i] & y[perm_dict[i]] for i in range(NUM_VARIABLES // 2)], ) ) ^ h(y) def f_dual_func(x, y): return ( reduce( lambda a, b: a ^ b, [x[inverse_perm_dict[i]] & y[i] for i in range(NUM_VARIABLES // 2)], ) ) ^ h_dual(x) def shifted(x, y, bits): x = [x[i] for i in range(NUM_VARIABLES // 2)] y = [y[i] for i in range(NUM_VARIABLES // 2)] for bit in bits: if bit < NUM_VARIABLES >> 2: x[bit] = x[bit] ^ 1 else: bit = bit - NUM_VARIABLES // 2 y[bit] = y[bit] ^ 1 return f_func(x, y) ``` ```python theme={null} shifted_bits = [1, 3, 9] g_func = lambda x, y: shifted(x, y, shifted_bits) ``` ## Creating the Circuit ```python theme={null} @qperm def g_qfunc(s: Const[QArray], res: QBit): res ^= g_func(s[0 : NUM_VARIABLES // 2], s[NUM_VARIABLES // 2 : s.len]) @qperm def f_dual_qfunc(s: Const[QArray], res: QBit): res ^= f_dual_func(s[0 : NUM_VARIABLES // 2], s[NUM_VARIABLES // 2 : s.len]) @qperm def f_qfunc(s: Const[QArray], res: QBit): res ^= f_func(s[0 : NUM_VARIABLES // 2], s[NUM_VARIABLES // 2 : s.len]) @qfunc def main(s: Output[QArray]) -> None: allocate(NUM_VARIABLES, s) hidden_shift( lambda y: phase_oracle(f_dual_qfunc, y), lambda y: phase_oracle(g_qfunc, y), s, ) qmod_complex = create_model(main, constraints=constraints) # same constraints qprog_complex = synthesize(qmod_complex) show(qprog_complex) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/32pN1MxOPfC7NEHH3ikKDLNzBxn ``` ```python theme={null} sample_results_complex = execute(qprog_complex).result_value() sample_results_complex.counts_of_output("s") ``` **Output:** ``` {'0101000001000000': 2048} ``` ```python theme={null} expected_s = "".join("1" if i in shifted_bits else "0" for i in range(NUM_VARIABLES)) assert list(sample_results_complex.counts_of_output("s").keys())[0] == expected_s ``` And indeed we got the correct shift! ## Hidden Shift Without the Dual Function We now use the second algorithm described in \[[2](#second)]. This algorithm only requires implementing $f$ and not its dual; however, it requires $O(n)$ samples from the circuit. Screen Shot 2023-06-27 at 18.08.23.png ```python theme={null} @qfunc def hidden_shift_no_dual( oracle: QCallable[QArray, QBit], oracle_shifted: QCallable[QArray, QBit], target: QArray, ind: QBit, ) -> None: hadamard_transform(target) oracle(target, ind) Z(ind) oracle_shifted(target, ind) hadamard_transform(target) NUM_VARIABLES = 16 @qfunc def main(target: Output[QArray], ind: Output[QBit]) -> None: allocate(NUM_VARIABLES, target) allocate(ind) hidden_shift_no_dual(f_qfunc, g_qfunc, target, ind) qmod_no_dual = create_model(main, constraints=constraints) # same constraints qprog_no_dual = synthesize(qmod_no_dual) show(qprog_no_dual) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/32pN4RYsZBWhYnMRzZCMwaUAUxR ``` ```python theme={null} sample_results_no_dual = execute(qprog_no_dual).result_value() ``` Out of the sampled results, we look for $n$ independent samples, from which we can extract s. One thousand samples should be enough with a very high probability. ```python theme={null} # The galois library is a package that extends NumPy arrays to operate over finite fields. # we will use it as our equations are binary equations import galois # here we work over Boolean arithmetics - F(2) GF = galois.GF(2) def is_independent_set(vectors): matrix = GF(vectors) rank = np.linalg.matrix_rank(matrix) if rank == len(vectors): return True else: return False samples = [ ([int(i) for i in u], int(b)) for u, b in sample_results_no_dual.counts_of_multiple_outputs( ["target", "ind"] ).keys() ] ind_v = [] ind_b = [] for v, b in samples: if is_independent_set(ind_v + [v]): ind_v.append(v) ind_b.append(b) if len(ind_v) == len(v): # reached max set break assert len(ind_v) == len(v) ``` We now solve the equation and extract $s$: ```python theme={null} A = np.array(ind_v) b = np.array(ind_b) # Solve the linear system s = np.linalg.solve(GF(A), GF(b)) s ``` **Output:** ``` GF([0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0], order=2) ``` And we successfully received the same shift. ```python theme={null} assert "".join(str(i) for i in s) == expected_s ``` ## References \[1]: [Quantum algorithms for highly non-linear Boolean functions](https://arxiv.org/abs/0811.3208) \[2]: [Quantum algorithm for the Boolean hidden shift problem](https://arxiv.org/abs/1103.3017) # Shor's Factoring Algorithm Source: https://docs.classiq.io/explore/algorithms/number_theory_and_cryptography/shor/shor Open this notebook in GitHub to run it yourself > **Integer Factorization** [\[1\]](#ref-integerfactor) is a famous problem in number theory: given a composite number $N$, find its prime factors. The importance of the problem stems from there being no known efficient classical algorithm (in the number of bits needed to represent $N$ in polynomial time), and much of modern-day cryptography relies on this fact. In 1994, Peter Shor developed an efficient **quantum** algorithm for the problem [\[2\]](#ref-shor94), providing arguably the most important evidence for an exponential advantage of quantum computing over classical computing. > > * **Input:** A composite integer $N$. > * **Promise:** $N$ is not a prime power and has at least one nontrivial factor. > * **Output:** With high probability, a nontrivial factor of $N$. > > **Complexity:** Runs in $\mathrm{polylog}(N)$ time, i.e., polynomial in $\log N$, giving an exponential speedup over the best-known classical factoring algorithms. > > *** > > **Keywords:** Integer factoring, Order-finding, Period finding, Phase estimation, Hidden subgroup over $\mathbb{Z}$, RSA/ECC break, Cryptography, Exponential speedup, Modular exponentiation. Shor's algorithm consists of classical parts and a quantum subroutine. The notebook is organized as follows: First, as an introduction, we outline the steps of the algorithm for factoring an input number $N$, summarized from \[3]. Then, we construct all the classical and quantum components of the algorithm, and run a simple example. Brief technical details, explaining how the algorithm works, are given together with the implementation, whereas some complex technical details are given at the end of the notebook. The steps of Shor's algorithm: 1. Pick a random number $1 < a < N$ that is co-prime with $N$. Check co-primality by computing the GCD (greatest common divisor) of $a$ and $N$. If it is 1, then we have a co-prime $a$; otherwise, we have a non-trivial factor of $N$, and we are done (the GCD complexity is $O(\log(N))$). 1. Find the period $r$ of the following function, using the **quantum period finding algorithm**: $$ f(x) = a^x\hspace{-8pt} \mod \hspace{-4pt} N $$ 3. If $r$ is odd or $a^{r/2} = -1 \bmod{N}$, return to step 1 (this event can be shown to happen with probability at most $1/2$). 4. Otherwise, $\gcd(a^{r/2} \pm 1, N)$ are both factors of $N$, and computing one of them yields the required result. Next, we move to the implementation of each and every step. *The quantum part is designed naturally as a Quantum Phase Estimation (QPE) routine, using Classiq's `flexible_qpe` and Modular Arithmetic*. *** *** ## Finding a Co-Prime of $N$ This is a simple classical preprocess for the algorithm. We randomly pick an integer $a$ in $(1,N)$, and check whether it is a co-prime of $N$ by calling the Euclidian GCD algorithm. If the sampled integer is not a co-prime then we find a factor of $N$ and we are done. (When we run the algorithm for a specific example below, we will fix a co-prime $a$ in advance, such that we have a meaningful demo with a quantum part). ```python theme={null} import numpy as np def random_coprime(N): """ Draw an integer in (1,N), and check if it is a co-prime of N. If True, return it, otherwise, we factor N. """ while True: a = np.random.randint(2, N) gcd_a = np.gcd(a, N) if gcd_a == 1: return int(a), gcd_a else: print(f"The number {N} is factored by {np.gcd(a, N)} and {N//np.gcd(a, N)}") return int(a), gcd_a ``` We can run a simple example: ```python theme={null} N = 123456789 a, gcd_a = random_coprime(N) print(f"The numbers {a} and {N} have the greatest common divisor {gcd_a}") ``` **Output:** ``` The numbers 48375383 and 123456789 have the greatest common divisor 1 ``` ## Quantum Period Finding as a QPE Routine The core part of Shor's algorithm, is the period finding of the function $f(x) = a^x\bmod{N}$: $$ \text{Find the minimal integer } r \text{ such that: } a^r =1 \bmod{N} . $$ This can be computed via Quantum Phase Estimation (QPE) \[4] (QPE was not discussed in the original formulation of Shor's algorithm but was later proposed by Kitaev \[5]) . Recall that the QPE routine, when applied for a unitary $U$ and on one of its eigenstates $|\psi_\theta\rangle$, produces an approximation of the corresponding eigenphase $\theta$: $$ |\psi_{\theta}\rangle|0\rangle_m \xrightarrow[\text{QPE}(U)]{} |\psi_{\theta}\rangle|\tilde{\theta}\rangle_m , $$ where $U|\psi_\theta\rangle = e^{2\pi i \theta}|\psi_\theta\rangle$ and $|\tilde{\theta}\rangle_m$ encodes an $m$-bit estimate of $\theta$ (in practice, the estimate $\tilde{\theta}$ is obtained by measurement, and the approximation holds with high probability up to an error of order $1/2^m$). We can find the order $r$ by applying QPE for the unitary $$ U_a|x\rangle = |ax\bmod{N}\rangle, $$ following the two facts (that are proven at the end of this notebook): 1. The unitary $U_a$ has $r$ eigenstates: $$ |\psi_s\rangle\equiv \frac{1}{\sqrt{r}}\sum^{r-1}_{k=0}e^{-2\pi i sk/r}|a^{k}\bmod{N}\rangle \text{ with eigenvalues } \lambda_s = e^{2\pi i s/r}, \qquad s=0,1,\dots, r-1. $$ 2. We can easily prepare an equal superposition of $|\psi_s\rangle$, since $$ |1\rangle = \frac{1}{\sqrt{r}}\sum^{r-1}_{k=0}|\psi_s\rangle. $$ Therefore, by applying a QPE for $U_a$, on the state $|1\rangle$, we get an equal superposition for the estimation of the eigenphases $\{s/r\}^{r-1}_{s=0}$: $$ |1\rangle|0\rangle_m \xrightarrow[\text{QPE}(U_a)]{} \frac{1}{\sqrt{r}}\sum^{r-1}_{s=0}|\psi_{s}\rangle|\tilde{s/r}\rangle_m. $$ How to extract the period $r$ from the resulting state is shown in the following section. In this section we focus on the implementation of the quantum part. We work with the `qpe_flexible` function, that allows to pass a unitary with a specified powered operation. In particular, in our case we have $$ \left(U_a\right)^{p}|x\rangle =|a^p x\bmod{N}\rangle $$ ```python theme={null} from classiq import * @qfunc def period_finding(n: CInt, a: CInt, x: QNum, phase_var: QNum): x ^= 1 qpe_flexible(lambda p: modular_multiply_constant_inplace(n, a**p, x), phase_var) ``` Screenshot 2025-09-30 at 12.15.53.png
The quantum period finding algorithm.
# ## Postprocess with Continued Fractions Algorithm The outcome distribution for the `phase_var` variable out of the QPE is in $[0,1)$, and expected to be peaked around $r$ values: $s/r$ for $s=0,1,\dots, r-1$. We can extract the value of $r$ by using the continued fractions algorithm: any rational (irrational) number can be written as a finite (infinite) converging sequence of fractions: $$ x = a_0 + \cfrac{1}{a_1 + \cfrac{1}{a_2 + \cfrac{1}{a_3 + \ddots}}} . $$ There is an efficient classical algorithm for extracting $a_i$. For example, using `sympy`: ```python theme={null} from sympy import Rational from sympy.ntheory.continued_fraction import ( continued_fraction, continued_fraction_convergents, ) phase_value = Rational(17 / 1024) list_of_continued_fraction = list( continued_fraction_convergents(continued_fraction(phase_value)) ) print( f"Continued fraction convergents for {phase_value}: {list_of_continued_fraction}" ) ``` **Output:** ``` Continued fraction convergents for 17/1024: [0, 1/60, 4/241, 17/1024] ``` Thus, we can extract $r$ by taking a measurment of the `phase_var` variable, and calculate the series of rational numbers $p_i/q_i$ that correspond to the continued fractions sereis. Then, we pick the fraction with the largest denominator $q_i$ such that $q_i n trimmed = [c for c in convs if c.as_numer_denom()[1] < n] # Store the last continued fraction_convergent lastf = trimmed[-1] if trimmed else None return lastf ``` Let us see an example, consider the following results out of the period finding algorithm, for $N=18$: ```python theme={null} phase_results = [0.1953125, 0.39160156, 0.0, 0.59667969, 0.80078125] ``` Our postprocess function gives ```python theme={null} for phase_res in phase_results: print(f"{phase_res} ---> {continued_fraction_post_process(phase_res, 18)}") ``` **Output:** ``` 0.1953125 ---> 1/5 0.39160156 ---> 2/5 0.0 ---> 0 0.59667969 ---> 3/5 0.80078125 ---> 4/5 ``` We can see that in this case one can conclude that $r=5$ is the period we were trying to find. ## Verifying the Order and Factoring Once $r$ is found, we first verify that it is an even number, this is because Shor's algorithm continues by writing $$ a^r=1\bmod{N}\implies \left(a^{r/2}-1\right)\left(a^{r/2}+1\right) = 0 \bmod{N}. $$ If $r$ is even, this equation implies four scenarios: 1. $\left(a^{r/2}-1\right) = 0 \bmod{N}$, impossible, since it contradicts the fact that $r$ is the order. 2. $\left(a^{r/2}+1\right) = 0 \bmod{N}$, in that case we must go back to step 1 and start with a new co-prime $a$. 3. $\left(a^{r/2}-1\right)\left(a^{r/2}+1\right) = N$, which gives a factoring for $N$. 4. $\left(a^{r/2}-1\right)\left(a^{r/2}+1\right) = kN$ with $k>1$, which means that $N$ divides the of the terms on the left. Thus, it has a non-trivial common divisor with one of them, namely, $\mathrm{gcd}(a^{r/2}+1,N)\neq 1$ and/or $\mathrm{gcd}(a^{r/2}-1,N)\neq 1$ are factors of $N$. It can be shown that the situations in which $r$ is odd or $\left(a^{r/2}+1\right) = 0 \bmod{N}$ can happen at probability 1/2 at most. Below we define a function that verifies we are not in the second possibility, and returns the factors on $N$ in case we are at the last two. ```python theme={null} def get_factors(a, r, n): # r is odd if r % 2 == 1: print(f"The order r={r} is odd, return to the first step and find a co-prime a") return None, None # a^(r/2)=-1 mod n is odd if a ^ (r // 2) + 1 % n == 0: print( f"It turns out that a^(r/2)+1 % N ==0, return to the first step and find a co-prime a" ) return None, None # (a^(r/2)+1) (a^(r/2)-1)=n if (a ^ (r // 2) + 1) * (a ^ (r // 2) - 1) == n: return (a ^ (r // 2) + 1), (a ^ (r // 2) - 1) # (a^(r/2)+1) (a^(r/2)-1)= kn, with k>1 gcd_ = np.gcd((a ^ (r // 2) + 1), n) if gcd_ > 1: return int(gcd_), int(n / gcd_) gcd_ = np.gcd((a ^ (r // 2) - 1), n) return int(gcd_), int(n / gcd_) ``` ## Example: Factoring 21 First, find a co-prime of $N$. We fix this value to $a=11$ in order to get a full end-to-end result that runs the quantum part. ```python theme={null} np.random.seed(989) modulo_num = 21 # The number we wish to factor a_num, gcd_a_num = random_coprime(modulo_num) print( f"The numbers {a_num} and {modulo_num} have the greatest common divisor {gcd_a_num}" ) assert a_num == 11 ``` **Output:** ``` The numbers 11 and 21 have the greatest common divisor 1 ``` Next, we run the quantum period finding algorithm to extract the period $r$. Concerning the size of the quantum variables: * For the phase variable that approximates the period $r$, we take twice as many qubits as in $|x\rangle$: this choice provides sufficient accuracy for extracting the period using the continued fraction algorithm (see the Technical Notes at the end of this notebook). In general, increasing the phase size yields deeper circuits with higher success probability of observing $s/r$, while decreasing it leads to shallower circuits with lower success probability. ```python theme={null} x_len = modulo_num.bit_length() phase_len = 2 * x_len @qfunc def main(phase_var: Output[QNum[phase_len, UNSIGNED, phase_len]]): x = QNum() allocate(x_len, x) allocate(phase_var) period_finding(modulo_num, a_num, x, phase_var) drop(x) qprog = synthesize( main, preferences=Preferences(qasm3=True, optimization_level=1), constraints=Constraints(optimization_parameter="width"), ) ``` ```python theme={null} show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/38AxgLzzLp9PQWuQRbitpjwyc5v ``` Screenshot 2025-09-30 at 12.16.15.png
Shor's algorithm for factoring $N=21$.
We execute and postprocess the results with the continued fraction approximation. For the demonstration, let us postprocess all the results with more than 10 counts. ```python theme={null} from IPython.display import display result = execute(qprog).get_sample_result() df = result.dataframe display(df) ```
phase\_var counts count probability bitstring
0 0.000000 349 349 0.170410 0000000000
1 0.500000 347 347 0.169434 1000000000
2 0.833008 243 243 0.118652 1101010101
3 0.333008 238 238 0.116211 0101010101
4 0.166992 229 229 0.111816 0010101011
... ... ... ... ... ...
59 0.671875 1 1 0.000488 1010110000
60 0.673828 1 1 0.000488 1010110010
61 0.836914 1 1 0.000488 1101011001
62 0.843750 1 1 0.000488 1101100000
63 0.849609 1 1 0.000488 1101100110

64 rows × 5 columns

```python theme={null} df_filtered = df[df["counts"] > 10] fracs = set() for phase_res in df_filtered.phase_var: processed_frac = continued_fraction_post_process(phase_res, modulo_num) fracs.add(processed_frac) print(f"For phase value {phase_res}: post processed fraction: {processed_frac}") ``` **Output:** ``` For phase value 0.0: post processed fraction: 0 For phase value 0.5: post processed fraction: 1/2 For phase value 0.8330078125: post processed fraction: 5/6 For phase value 0.3330078125: post processed fraction: 1/3 For phase value 0.1669921875: post processed fraction: 1/6 For phase value 0.6669921875: post processed fraction: 2/3 For phase value 0.666015625: post processed fraction: 2/3 For phase value 0.833984375: post processed fraction: 5/6 For phase value 0.333984375: post processed fraction: 1/3 For phase value 0.166015625: post processed fraction: 1/6 For phase value 0.66796875: post processed fraction: 2/3 For phase value 0.6650390625: post processed fraction: 2/3 For phase value 0.83203125: post processed fraction: 5/6 For phase value 0.33203125: post processed fraction: 1/3 For phase value 0.16796875: post processed fraction: 1/6 ``` We can see that the period is $r=6$, as can be read from all the results that are post-processed to $1/6$ or $5/6$. Let us plot the full distribution, together with the continued fraction approximation: ```python theme={null} positions = [float(f) for f in fracs] labels = ["0" if f == 0 else f"{f.numerator}/{f.denominator}" for f in fracs] ``` ```python theme={null} import matplotlib.pyplot as plt fig, ax = plt.subplots(figsize=(12, 4)) ax.bar(df["phase_var"], df["probability"], width=4 / 4**x_len) ax.set_xlabel(r"$s/r$", fontsize=16) ax.set_ylabel(r"$p(s/r)$", fontsize=16) ax.tick_params(axis="both", labelsize=16) # Extra top axis with the resulting continued fraction approximation ax2 = ax.secondary_xaxis("top") ax2.set_xticks(positions) ax2.set_xticklabels(labels, color="red") ax2.set_xlabel("Extracted fractions", fontsize=14, color="red") ax2.tick_params(axis="both", labelsize=14); ``` output We can clearly see that we have peaks at $\frac{0}{6}, \frac{1}{6}, \frac{2}{6}, \frac{3}{6} , \frac{4}{6}, \frac{5}{6}$, that is, the order is $r=6$. ```python theme={null} for p in positions: assert (p * 6).is_integer() r = 6 ``` All is left is to run the final step that determines the factor of $N$ out of $a$ and $r$: ```python theme={null} factor1, factor2 = get_factors(a_num, r, modulo_num) assert factor1 * factor2 == modulo_num print(f"The number {modulo_num} is factored by {factor1} and {factor2}") ``` **Output:** ``` The number 21 is factored by 3 and 7 ``` ## Technical Notes # ## The Eigenphases and Eigenstates of $f(x) = ax\bmod{N}$ Below we prove the claim that the modular multiplication by $a$, which is a co-prime of $N$, has the following eigenstates: $$ |\psi_s\rangle = \frac{1}{\sqrt{r}}\sum^{r-1}_{k=0}e^{-2\pi i sk/r}|a^{k}\bmod{N}\rangle, \qquad s=0,1,\dots, r-1, $$ and the corresponding eigenvalues: $$ \lambda_s = e^{2\pi i s/r}, $$ where $r$ is the order of the function. It is easy to verify that $$ a|\psi_s\rangle = \frac{1}{\sqrt{r}}\sum^{r-1}_{k=0}e^{-2\pi i sk/r}|a^{k+1}\bmod{N}\rangle = \frac{1}{\sqrt{r}}\sum^{r}_{k=1}e^{2\pi i s/r}e^{-2\pi i sk/r}|a^{k}\bmod{N}\rangle = e^{2\pi i s/r}\frac{1}{\sqrt{r}}\sum^{r-1}_{k=0}e^{-2\pi i sk/r}|a^{k}\bmod{N}\rangle = e^{2\pi i s/r}a|\psi_s\rangle, $$ where in the second equality we just changed the sum index $k \rightarrow k-1$, and the third one comes from the fact that the terms under the sum have a period of $r$, and the $r$-th term is equal to the $0$-th one. # ## The Initial State $|1\rangle$ for the QPE Below we show that the state $|1\rangle$ is an equal superposition of $|\psi_s\rangle$. We use the fact that $$ \sum^{r-1}_{k'=0} e^{-2\pi i k'k/r} = r\cdot \delta_{k0}, $$ namely, the sum vanishes unless $k=0$. Calculating explicitly, we get $$ \frac{1}{\sqrt{r}}\sum^{r-1}_{k'=0}|\psi_{k'}\rangle = \frac{1}{r}\sum^{r-1}_{k'=0} \sum^{r-1}_{k=0}e^{-2\pi i k'k/r}|a^{k}\bmod{N}\rangle =\frac{1}{r}\sum^{r-1}_{k=0} \left(\sum^{r-1}_{k'=0} e^{-2\pi i k'k/r}\right)|a^{k}\bmod{N}\rangle =\frac{1}{r}\sum^{r-1}_{k=0} r\delta_{k0}|a^{k}\bmod{N}\rangle = |1\rangle. $$ # ## The Size of the Phase Variable In Shor's algorithm, we choose the phase variable to be twice as large as the variable `x` on which we apply modular multiplication. This choice follows from the properties of the QPE routine and the continued fractions algorithm. Applying QPE with a phase variable of size $m$ yields an $m$-bit approximation of the exact phase $s/r$: $$ \left| \frac{j}{2^m}-\frac{s}{r}\right|\leq \frac{1}{2^{m+1}}, \text{ for some } 0\leq j\leq 2^{m-1}. $$ Now, according to the continued fractions algorithm, if we run the it for $\theta$ with $\left| \frac{s}{r}-\theta\right|\leq \frac{1}{2r^2}$, then both $j$ and $r$ can be we can recovered \[3]. Thus, performing QPE with $m=2n$ gives $$ \left| \frac{j}{2^m}-\frac{s}{r}\right|\leq \frac{1}{2^{2n+1}}\leq \frac{1}{2\cdot N^{2}} \leq \frac{1}{2\cdot r^{2}}, $$ which satisfies the requirement of the continued fractions algorithm (the last two inequlities comes from the fact the $r\leq N\leq 2^n$). ## References \[1]: [Integer Factorization (Wikipedia)](https://en.wikipedia.org/wiki/Integer_factorization) \[2]: [P. W. Shor, "Algorithms for quantum computation: Discrete logarithms and factoring," Proceedings 35th Annual Symposium on Foundations of Computer Science (FOCS), IEEE, 1994.](https://ieeexplore.ieee.org/abstract/document/365700) \[3]: [Shor's Algorithm Procedure (Wikipedia)](https://en.wikipedia.org/wiki/Shor%27s_algorithm#Procedure) \[4]: [M. A. Nielsen & I. L. Chuang, "Quantum Computation and Quantum Information", Cambridge Univ. Press, 2001. ISBN 978-9812388582.](#) \[5]: [Kitaev, A. Yu. "Quantum measurements and the Abelian Stabilizer Problem". arXiv:quant-ph/9511026 (1995).](https://arxiv.org/abs/quant-ph/9511026) # Quantum Algorithm for Solving the Poisson Equation Source: https://docs.classiq.io/explore/algorithms/quantum_differential_equations_solvers/discrete_poisson_solver/discrete_poisson_solver Open this notebook in GitHub to run it yourself The Poisson equation is a partial differential equation that appears in various research fields, such as physics and engineering. It describes the distribution of a potential field, $u$, under some source term $b$: $$ \nabla^2 u = b, $$ where the $\nabla^2$ is the Laplacian (second derivatives) operator. One approach for numerically solving the Poisson equation is to move from the continuous description to a discrete one, using the finite difference method that casts the problem into a set of linear equations. Then, the solution can be obtained by a linear solver. In this notebook we treat the Poisson equation on a rectangular geometry, $L_x\times L_y$, with a Dirichlet boundary condition on the $x$ axis and a Neumann boundary condition on the $y$ axis: $$ u(0) = u(L_x)=f_0,\,\,\,\, \partial_y u|_{y=0} = \partial_y u|_{y=L_y} = g_0. $$ Furthermore, we assume that $f_0=g_0=0$. The discretization of space, including the treatment of the above boundary conditions, is given in Figure 1. The resulting linear equation reads: $$ \mathcal{L}\cdot \vec{u} = \vec{b}, \,\,\,\,\,\,\, \mathcal{L} = \mathcal{L}_{xx} \otimes I_y + I_x \otimes \mathcal{L}_{yy}, $$ where $\mathcal{L}_{xx}$ and $\mathcal{L}_{yy}$ are the Laplacian operators \[[1](#cst)]: $$ \mathcal{L}_{xx} = \frac{1}{\Delta x^2} \begin{pmatrix} 3 & -1 & 0 & \cdots & 0 \\ -1 & 2 & -1 & \cdots & 0 \\ 0 & -1 & 2 & \cdots & 0 \\ \vdots & \vdots & \vdots & \ddots & -1 \\ 0 & 0 & \cdots & -1 & 3 \\ \end{pmatrix},\,\,\, \mathcal{L}_{yy} = \frac{1}{\Delta y^2} \begin{pmatrix} 1 & -1 & 0 & \cdots & 0 \\ -1 & 2 & -1 & \cdots & 0 \\ 0 & -1 & 2 & \cdots & 0 \\ \vdots & \vdots & \vdots & \ddots & -1 \\ 0 & 0 & \cdots & -1 & 1 \\ \end{pmatrix} $$ and $\Delta x$ and $\Delta y$ are the discretization of the $x$ and $y$ axes, respectively. The square matrices above, which are of dimensions $N_x$ and $N_y$, respectively, represent the solution at the inner part of our geometry. *** In this notebook we solve the Poisson problem with the [HHL](https://github.com/Classiq/classiq-library/blob/main/algorithms/quantum_linear_solvers/hhl/hhl.ipynb) quantum linear solver. We utilize similar ideas appearing in Ref.\[[2](#poissonquantum)], where a quantum cosine and a quantum sine transforms \[[3](#qcst)] are performed towards achieving scalable implementation. *** ## Building the Algorithm with Classiq The HHL algorithm essentially applies a matrix inversion. Here we treat the Laplacian matrix, which can be diagonalized by quantum sine and cosine transforms. Thus, the matrix to invert is diagonal. The four main quantum blocks of the algorithm are thus (see Figure 2): 1. Prepare the amplitudes of the source term on a quantum variable. 2. Perform QST and QCT at the beginning of the computation. This is done by applying the QST to the x qubits and the QCT to the y qubits. 1. Perform matrix inversion for a diagonal matrix. 2. Uncompute the QST and QCT at the end of the computation. Screenshot 2025-07-01 at 0.10.30.png Below we define several classical and quantum functions for constructing our quantum linear solver. ```python theme={null} import matplotlib.pyplot as plt import numpy as np import sympy from classiq import * ``` # ## Diagonal Hamiltonian Calculation The eigenvalues of the Poisson equation with Dirichlet boundary conditions in the x direction and Neumann boundary conditions in the y direction are given by $$ \lambda_{k,j} \equiv \lambda_{x,k} +\lambda_{y,j} $$ where $$ \lambda_{x,k} = \frac{4}{\Delta x^2} \sin^2\left(\frac{\pi}{2N_x} (k+1)\right),\,\,\,\,\,\,\,\, \lambda_{y,j} = \frac{4}{\Delta y^2} \sin^2\left(\frac{\pi}{2N_y} j\right), $$ and $k = 0, 1, \ldots, N_y-1$ and $j = 0, 1, \ldots, N_x-1$. The HHL algorithm requires the application of an Hamiltonian simulation $e^{iH}$. In this notebook this quantum block is implemented using the Suzuki-Trotter built-in function. We start with defining a function that gets $N_x$ and $N_y$ and returns the corresponding Hamiltonian. The decomposition of the diagonal matrix to the Pauli basis is done using the Walsh Hadamard transform. ```python theme={null} def get_poisson_dirichletx_neumanny_ham(nx, ny): dx2 = ny / nx dy2 = nx / ny eigenvalues_x = dx2 * 4 * np.sin(np.pi / 2 * np.arange(1, nx + 1) / nx) ** 2 eigenvalues_y = dy2 * 4 * np.sin(np.pi / 2 * np.arange(0, ny) / ny) ** 2 num_qubits_x = int(np.log2(nx)) num_qubits_y = int(np.log2(ny)) # Decompose the eigenvalues of the Poisson equation into Pauli terms and construct the corresponding Hamiltonian pauli_coefficients_x = sympy.fwht(eigenvalues_x / nx) pauli_coefficients_y = sympy.fwht(eigenvalues_y / ny) def convert_bitstring_to_sparse_paulis( bitstring: int, offset: int ) -> list[IndexedPauli]: # A set bit s places a Z on qubit (offset + s); identity qubits are simply omitted. return [ IndexedPauli(pauli=Pauli.Z, index=offset + s) for s in range(bitstring.bit_length()) if (bitstring >> s) & 1 ] # x acts on the low qubits [0, num_qubits_x), y on the high qubits [num_qubits_x, num_qubits_x + num_qubits_y) hamiltonian_x = [ SparsePauliTerm( paulis=convert_bitstring_to_sparse_paulis(i, 0), coefficient=float(pauli_coefficients_x[i]), ) for i in range(nx) ] hamiltonian_y = [ SparsePauliTerm( paulis=convert_bitstring_to_sparse_paulis(i, num_qubits_x), coefficient=float(pauli_coefficients_y[i]), ) for i in range(ny) ] hamiltonian = SparsePauliOp( terms=hamiltonian_x + hamiltonian_y, num_qubits=num_qubits_x + num_qubits_y, ) return hamiltonian ``` # ## Hamiltonian Evolution for QPE The HHL is based on a [QPE](https://github.com/Classiq/classiq-library/blob/main/tutorials/advanced_tutorials/high_level_modeling_flexible_qpe/high_level_modeling_flexible_qpe.ipynb) applied on $e^{iHt}$. For this, we need to define a function implementing $\left(e^{iHt}\right)^p$ for an integer power $p$. Since in our case the Hamiltonian is diagonal, an exact implementation is given by the first order Suzuki-Trotter formula, where in addition $\left(e^{iHt}\right)^p = e^{ipHt}$. ```python theme={null} @qfunc def powered_hamiltonian_evolution( hamiltonian: SparsePauliOp, # the hamiltonian H scaling: CReal, # the scaling factor t p: CInt, # the power qba: QArray, ): suzuki_trotter( pauli_operator=hamiltonian, evolution_coefficient=p * (-2 * np.pi * scaling), order=1, repetitions=1, qbv=qba, ) ``` # ## Sine and Cosine Transforms For the system treated in this notebook, diagonalization of the $2D$ Laplacian is given by appying a quantum sine (cosine) transform on the $x$ ($y$) dimension. We define a quantum function for implementing this, using the open library functions: ```python theme={null} @qfunc def qsct_2d(xy_variable: QArray[QNum, 2]): qst_type2(xy_variable[0]) qct_type2(xy_variable[1]) ``` # ## Matrix Inversion Finally, we define a matrix inversion using a QPE routine, similar to the example given in the basic [HHL notebook](https://github.com/Classiq/classiq-library/blob/main/algorithms/quantum_linear_solvers/hhl/hhl.ipynb). ```python theme={null} @qfunc def matrix_inversion_HHL( prefactor: float, my_unitary: QCallable[CInt, QArray], state: QArray, phase: QNum, indicator: Output[QBit], ): allocate(indicator) within_apply( within=lambda: qpe_flexible( unitary_with_power=lambda power: my_unitary(power, state), phase=phase, ), apply=lambda: assign_amplitude_table( lookup_table(lambda p: 0 if p == 0 else prefactor / p, phase), phase, indicator, ), ) ``` ## Example: Non-Separable Source Term We solve an example with a square grid of $N_x,\, N_y=2^3$. For the source term we take a non-separable $2^{N_x+N_y}$ vector that represents the function $$ b = F\left[xy(x-L_x)(y-L_y)\right]. $$ We choose $F(z)=\tanh(z)^2$, which satisfies the boundary conditions. ```python theme={null} # Set discretization of X and Y axes. NUM_QUBITS_X = 3 NUM_QUBITS_Y = 3 nx = 2**NUM_QUBITS_X ny = 2**NUM_QUBITS_Y # Set the source term as function of x and y such that b(x,y) = exp(-1/xy(x-Lx)(y-Ly)) xgrid = (np.arange(nx) + 0.5) / nx ygrid = (np.arange(ny) + 0.5) / ny zgrid = np.kron(ygrid, xgrid) * np.kron( (ygrid - 1), (xgrid - 1) ) # in classiq the variable order is [y,x] b_vector = np.tanh(zgrid) ** 2 # Normalize the source term b_vector = b_vector / np.linalg.norm(b_vector) ``` We can plot the source term: ```python theme={null} b_matrix = b_vector.reshape((nx, ny)) xmesh, ymesh = np.meshgrid(xgrid, ygrid) plt.contourf(xmesh, ymesh, b_matrix.transpose()) plt.xlabel("x") plt.ylabel("y") ax = plt.gca() ax.axis("equal") plt.colorbar() plt.show() ``` output Next, we calculate the Hamiltonian for the specific discretization: ```python theme={null} hamiltonian = get_poisson_dirichletx_neumanny_ham(nx, ny) ``` Now, we need to define the resolution (QPE size) for the quantum solver. The required number of QPE phase qubits depends on the condition number $\kappa$ of the inverted matrix. In the case of the Laplacian matrix, this parameter is of the order of the matrix dimension $O(2^{N_x+N_y})$, which eliminates the exponential advantage. However, in our example we have a smooth source term, and high modes are expected to have minor effects on the solution. The highest mode of $\mathcal{L}$ is $\lambda_{\max}=8$, whereas the smallest one is $\lambda_{x,0}\sim 4\pi^2/(2N_x)^2$. Let us assume that the highest mode participating in the solution is $\sim 2^6 \lambda_{x,0}$, and take a QPE size of six qubits. ```python theme={null} # number of qubits for the QPE QPE_SIZE = 6 MAX_EIG = 4 * (np.pi / (2 * nx)) ** 2 * 2**6 # parameters for the amplitude preparation PREFACTOR = 2**-QPE_SIZE ``` We build the model and synthesize it: *Comment: the model is designed for the case of symmetric grid $N_x=N_y$, working with `QArray[QNum,2]`. In the more general case one can define a `QStruct` with two `QNum` variable of different sizes* ```python theme={null} @qfunc def main( xy_variable: Output[QArray[QNum[NUM_QUBITS_X], 2]], phase_var: Output[QNum[QPE_SIZE]], indicator: Output[QBit], ): prepare_amplitudes(b_vector.tolist(), 0.0, xy_variable) allocate(phase_var) within_apply( within=lambda: qsct_2d(xy_variable), apply=lambda: matrix_inversion_HHL( prefactor=PREFACTOR, my_unitary=lambda p, target: powered_hamiltonian_evolution( hamiltonian=hamiltonian, scaling=1 / MAX_EIG, p=p, qba=target, ), state=xy_variable, phase=phase_var, indicator=indicator, ), ) qprog = synthesize(main, constraints=Constraints(optimization_parameter="width")) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3FnecOsJf5zOR1df4Wfa9FlpQ8U ``` # ## Executing and Plotting the Result We run the quantum program on a statevector simulator to retrieve the full solution. ```python theme={null} execution_preferences = ExecutionPreferences( backend_preferences=ClassiqBackendPreferences(backend_name="simulator_statevector") ) qprog = set_quantum_program_execution_preferences(qprog, execution_preferences) result = execute(qprog).result_value() ``` ```python theme={null} import pandas as pd # Retrieve the solution of the Poisson equation from the results of the quantum program. def extract_result_from_statevector_simulation( df: pd.DataFrame, normalization: float, indicator_name: str, phase_name: str, xy_name: str, ) -> np.ndarray: # Initialize a matrix state_matrix = np.zeros((nx, ny), dtype=complex) # Filter only the successful states. filtered_st = df[ (df[indicator_name] == 1) & (df[phase_name] == 0) & (np.abs(df.amplitude) > 1e-12) ] # Allocate values state_matrix[tuple(np.array(filtered_st[xy_name].tolist()).T)] = ( filtered_st.amplitude.to_numpy() ) # Normalize the solution of the Poisson equation. global_phase = np.angle(state_matrix[0, 0]) result_matrix = np.real(state_matrix / np.exp(1j * global_phase)) / normalization return result_matrix ``` ```python theme={null} result_matrix = extract_result_from_statevector_simulation( result.dataframe, PREFACTOR / MAX_EIG, "indicator", "phase_var", "xy_variable" ) ``` The resulting statevector is given up to a sign. The sign in the center of the solution is expected to be positive. We can correct accordingly: ```python theme={null} if result_matrix[nx // 2, ny // 2] < 0: result_matrix = -result_matrix ``` Finally, we print the result and compare it to the classical solution: ```python theme={null} ## getting the classical solution Hx = ( 2 * np.diag(np.ones(nx)) - np.diag(np.ones(nx - 1), 1) - np.diag(np.ones(nx - 1), -1) ) Hx[0, 0] = Hx[nx - 1, nx - 1] = 3 Hy = ( 2 * np.diag(np.ones(ny)) - np.diag(np.ones(ny - 1), 1) - np.diag(np.ones(ny - 1), -1) ) Hy[0, 0] = Hy[ny - 1, ny - 1] = 1 # we need to flip the order of x and y b_classical = (b_matrix.T).reshape(nx * ny) H = np.kron(Hx, np.identity(ny)) + np.kron(np.identity(nx), Hy) classical_result_vector = np.linalg.solve(H, b_classical) classical_result_matrix = classical_result_vector.reshape((nx, ny)) ``` ```python theme={null} xmesh, ymesh = np.meshgrid(xgrid, ygrid) fig, axs = plt.subplots(1, 2, figsize=(12, 6)) contour0 = axs[0].contourf(xmesh, ymesh, result_matrix.transpose()) axs[0].axis("equal") axs[0].axis("square") axs[0].title.set_text("Quantum") fig.colorbar(contour0, ax=axs[0]) contour1 = axs[1].contourf(xmesh, ymesh, classical_result_matrix.transpose()) axs[1].axis("equal") axs[1].axis("square") axs[1].title.set_text("Classical") fig.colorbar(contour1, ax=axs[1]) plt.show() ``` output ## References \[1]: [Strang, G., 1999 SIAM Review 41 135. The discrete cosine transform.](https://doi.org/10.1137/S0036144598336745) \[2]: [Yudong Cao et al., 2013 New J. Phys. 15 013021. Quantum algorithm and circuit design solving the Poisson equation.](https://iopscience.iop.org/article/10.1088/1367-2630/15/1/013021/pdf) \[3]: [Klappenecker, A., & Rotteler M., Discrete cosine transforms on quantum computers.](https://arxiv.org/abs/quant-ph/0111038) # Linear Combination of Hamiltonian Simulation (LCHS) Source: https://docs.classiq.io/explore/algorithms/quantum_differential_equations_solvers/lchs/lchs Open this notebook in GitHub to run it yourself > The **Linear Combination of Hamiltonian Simulation (LCHS)** method [\[1\]](#ref-lchs) solves linear ODEs of the form $\frac{d}{dt}|u\rangle = -A|u\rangle$ on a quantum computer, where $A$ may be non-anti-Hermitian, leading to non-unitary time evolution. It works by expressing the propagator $e^{-At}$ as a continuous linear combination of *unitary* evolutions, which is then discretized and implemented using standard Hamiltonian simulation primitives. > > * **Input:** > * A matrix $A = L + iH$ with $L \succeq 0$ (positive semidefinite) and $H = H^\dagger$ (Hermitian). > * Block-encoding oracles for $H/\alpha_H$ and $L/\alpha_L$. > * The evolution time $t$ and target error $\epsilon$. > * **Output:** A quantum state proportional to $e^{-At}|u_0\rangle$, obtained via post-selection on auxiliary qubits. > > **Complexity:** The algorithm requires $\mathcal{O}(\alpha_A\, t \log(1/\epsilon))$ queries to the block-encoding oracle for $A$ [\[5\]](#ref-optimal-lchs), where $\alpha_A \geq \|A\|$ is the block-encoding normalization and $\epsilon$ is the target operator-norm error $\|e^{-At} - \tilde{U}\| \leq \epsilon$. This is optimal in all parameters, improving the $\widetilde{O}(\alpha_A\, t \log^{1+o(1)}(1/\epsilon))$ scaling of prior work [\[1\]](#ref-lchs), [\[2\]](#ref-lchs2). > > *** > > **Keywords:** Non-unitary dynamics, Linear Differential Equations, Hamiltonian Simulation, Block Encoding, GQSP, Kernel Methods, Open Quantum Systems. ## Background # ## The Problem We wish to solve the linear ODE $$ \frac{d}{dt}|u\rangle = -A\,|u\rangle, \qquad |u(0)\rangle = |u_0\rangle, $$ where $A \in \mathbb{C}^{N \times N}$. When $A$ is not anti-Hermitian, the solution $|u(t)\rangle = e^{-At}|u_0\rangle$ is **non-unitary** and cannot be directly implemented as a quantum gate. We decompose $A = L + iH$ where: * $H = \frac{1}{2i}(A - A^\dagger)$ is Hermitian (the conservative/oscillatory part), * $L = \frac{1}{2}(A + A^\dagger)$ is positive semidefinite (the dissipative/damping part). **Source terms.** The homogeneous equation above can be extended to the inhomogeneous case $\frac{d}{dt}|u\rangle = -A|u\rangle + |b(t)\rangle$. Via Duhamel's principle, the solution is $$ |u(t)\rangle = e^{-At}|u_0\rangle + \int_0^t e^{-A(t-s)}|b(s)\rangle\,ds. $$ Each term involves evaluating the non-unitary propagator $e^{-A\tau}$ and can therefore be handled by LCHS. In practice, the integral is discretized using a quadrature rule, and each quadrature point contributes an additional LCHS call at the corresponding time $(t - s)$ (see [\[5\]](#ref-optimal-lchs), Sec. 1.1.2 and [\[2\]](#ref-lchs2)). # ## The LCHS Identity The central insight of LCHS is the integral identity (Eq. 4 in [\[5\]](#ref-optimal-lchs), originally [\[1\]](#ref-lchs)): $$ e^{-At} = \frac{1}{\sqrt{2\pi}}\int_{-\infty}^{\infty} \hat{f}(k)\, e^{-i(kL + H)t}\, dk, $$ where $\hat{f}(k)$ is the Fourier transform of a function $f$ chosen to satisfy $f(x) = e^{-x}$ for $x \geq 0$. For each $k \in \mathbb{R}$, the matrix $H + kL$ is Hermitian, so the exponential $e^{-i(kL+H)t}$ is a unitary operator that can be implemented using standard Hamiltonian simulation techniques. In practice this integral is truncated to a finite interval $[-R, R]$ (where $R$ is the truncation radius) and discretized into a finite sum that can be implemented via LCU on a quantum computer. A key innovation of [\[5\]](#ref-optimal-lchs) is the *approximate* LCHS: the kernel $f$ need only *approximate* exponential decay (rather than match it exactly), which circumvents a no-go result from prior work and enables the optimal $O(\alpha_A\, t \log(1/\epsilon))$ query complexity. # ## Kernel Function Reference [\[5\]](#ref-optimal-lchs) uses the kernel function (Eq. 9, with $j=2$, $y=1$): $$ \hat{f}_2(k;\gamma,c) = \frac{2}{\sqrt{2\pi}} \frac{e^{c(1-ik)}\, e^{-(k^2+1)/(4\gamma^2)}}{1+k^2}. $$ This is a product of a Lorentzian $1/(1+k^2)$ and a Gaussian $e^{-k^2/(4\gamma^2)}$. The Lorentzian alone is the Fourier transform of exact exponential decay $e^{-|x|}$, but its tails decay only as $1/k^2$, making it expensive to truncate the integral. The Gaussian envelope suppresses the tails exponentially, enabling truncation to a finite interval $[-R, R]$ with small error. The tradeoff is that the kernel now only *approximates* exponential decay, with the approximation quality controlled by $\gamma$: larger $\gamma$ gives a wider (flatter) Gaussian and a better approximation, at the cost of a larger truncation radius $R$. The shift parameter $c$ controls the normalization factor $\alpha_{\hat{f}_2} \leq e^c$, which determines the post-selection success probability. Theorem 2 of [\[5\]](#ref-optimal-lchs) sets $\gamma = \frac{1}{c}\sqrt{c + \log\frac{1+1/(2\pi)}{\epsilon_{\mathrm{lchs}}}}$ and $R = 2c\gamma^2$ to guarantee kernel approximation error $\leq \epsilon_{\mathrm{lchs}}$. # ## Discretization The integral is discretized on a uniform grid of $N = 2^J$ points with spacing $h = 2R/N$ over the interval $[-R, R)$ ([\[5\]](#ref-optimal-lchs), Theorem 3, which proves exponential convergence of the uniform quadrature): $$ e^{-At} \approx \frac{h}{\sqrt{2\pi}} \sum_{j=-N/2}^{N/2-1} \hat{f}_2(k_j)\, e^{-i(k_j L + H)t}, \qquad k_j = h \cdot j. $$ The stepsize $h$ is chosen according to: $$ h \leq \frac{\pi}{\|L\| \cdot t/2 + \log\!\left(\frac{64\, e^{3c/2}}{15\,\epsilon_{\mathrm{disc}}}\right)}, $$ The number of grid points $N = 2^J$ is then $J = \lceil \log_2(2R/h) \rceil$. Note that a uniform quadrature is sufficient. This sum is implemented via LCU, with the kernel values $\hat{f}_2(k_j)$ as coefficients. # ## Quantum Circuit Structure The full LCHS quantum circuit consists of the following components: 1. **Kernel state preparation**: Load $|\hat{f}_2(k_j)|$ into an auxiliary register $|j\rangle$. 2. **Block-encoding of $H + k_j L$**: For each grid point $j$, block-encode the Hermitian operator $(H + k_j L)/\alpha$, where $\alpha = \alpha_L R + \alpha_H$ is the normalization. 3. **Hamiltonian simulation**: In this demo we apply the Jacobi-Anger polynomial approximation of $e^{-i(H+k_j L)\alpha t}$ using Generalized QSP on the walk operator derived from the block encoding. 4. **Post-selection**: Measure all auxiliary qubits in the $|0\rangle$ state to extract the desired output. ## Preliminaries ```python theme={null} import matplotlib.pyplot as plt import numpy as np import scipy.linalg as la from classiq import * from classiq.applications.qsp import ( gqsp_phases, poly_jacobi_anger_degree, poly_jacobi_anger_exp_cos, ) from classiq.qmod.symbolic import pi ``` # ## Problem Definition and Parameters We define the Hamiltonians $H$ and $L$ and derive all algorithm parameters - state size, block-encoding normalization, kernel shape, and error budget - from these operators and the desired precision. ```python theme={null} H_PAULI = 0.5 * Pauli.X(0) * Pauli.X(1) + 0.5 * Pauli.Z(0) * Pauli.Z(1) L_PAULI = 0.5 * Pauli.I(0) * Pauli.I(1) + 0.5 * Pauli.Z(0) * Pauli.I(1) STATE_SIZE = H_PAULI.num_qubits n_pauli_terms = max(len(H_PAULI.terms), len(L_PAULI.terms)) BLOCK_SIZE = max(1, int(np.ceil(np.log2(n_pauli_terms)))) alpha_H = sum(abs(t.coefficient) for t in H_PAULI.terms) alpha_L = sum(abs(t.coefficient) for t in L_PAULI.terms) T = 1 # evolution time GQSP_EPS = 1e-5 # GQSP polynomial approximation error LCHS_EPS = 1e-2 # LCHS kernel approximation error (Theorem 2 of [5]) DISC_EPS = 1e-2 # discretization / quadrature error (Theorem 3 of [5]) EPS = GQSP_EPS + LCHS_EPS + DISC_EPS c = 2.0 # kernel shift (trades normalization e^c vs. integration range) gamma = np.sqrt(c + np.log((1 + 1 / (2 * np.pi)) / LCHS_EPS)) / c R = 2 * c * gamma**2 # truncation radius h = np.pi / ( alpha_L * T / 2 + np.log(64 * np.exp(3 * c / 2) / (15 * DISC_EPS)) ) # step size J_SIZE = int(np.ceil(np.log2(2 * R / h))) h = R / 2 ** (J_SIZE - 1) # actual h after rounding to power-of-2 grid alpha_tot = alpha_L * R + alpha_H GQSP_SCALE_FACTOR = 0.99 print(f"Derived: gamma={gamma:.4f}, R={R:.4f}, J_SIZE={J_SIZE}") print(f"Errors: GQSP_EPS={GQSP_EPS}, LCHS_EPS={LCHS_EPS}, DISC_EPS={DISC_EPS}") print(f" EPS (total) = {EPS}") ``` **Output:** ``` Derived: gamma=1.2993, R=6.7529, J_SIZE=6 Errors: GQSP_EPS=1e-05, LCHS_EPS=0.01, DISC_EPS=0.01 EPS (total) = 0.02001 ``` Notice that in practice these bounds are worst-case and not tight, and the actual error is much smaller. ## Classical Reference Simulation Before building the quantum circuit, we implement the LCHS formula classically to verify correctness. 1. **Exact**: Direct matrix exponentiation $e^{-(L+iH)t}|u_0\rangle$. 2. **LCHS**: The discretized sum $\frac{h}{\sqrt{2\pi}} \sum_j \hat{f}_2(k_j)\, e^{-i(H+k_jL)t}|u_0\rangle$ using exact matrix exponentials. ```python theme={null} def signed_j_vals(J: int) -> np.ndarray: """Signed grid indices: -2^(J-1), ..., 2^(J-1)-1.""" return np.arange(-(2 ** (J - 1)), 2 ** (J - 1), dtype=int) def lchs_grid(R: float, J: int): """Compute the LCHS discretization grid.""" N = 2**J h = 2 * R / N j = signed_j_vals(J) k = h * j return h, j, k def kernel_fhat2(k: np.ndarray, gamma: float, c: float) -> np.ndarray: r"""Kernel function $\hat{f}_2(k;\gamma,c)$ from Eq. (6) of [5] with j=2, y=1.""" k = np.asarray(k, dtype=float) return np.exp(-1j * k * c) / (1 + k**2) * np.exp(-(k**2 + 1) / (4 * gamma**2)) ``` ```python theme={null} def truth_nonunitary(v0, L, H, t): """Exact non-unitary evolution: exp(-(L + iH)t) @ v0.""" return la.expm(-(L + 1j * H) * t) @ v0 def lchs_classical(v0, L, H, t, *, R, J, gamma, c): """Discretized LCHS with exact matrix exponentials.""" v0 = np.asarray(v0, dtype=complex) h, j, k = lchs_grid(R, J) w = kernel_fhat2(k, gamma=gamma, c=c) out = np.zeros_like(v0, dtype=complex) for kj, wj in zip(k, w): U = la.expm(-1j * (H + kj * L) * t) out += wj * (U @ v0) return out, h, k, w ``` # ## Classical Verification We verify the LCHS simulation against the exact solution using a random test problem. ```python theme={null} def random_hermitian(M: int, *, seed=None, scale=1.0) -> np.ndarray: rng = np.random.default_rng(seed) A = rng.normal(size=(M, M)) + 1j * rng.normal(size=(M, M)) H = (A + A.conj().T) / 2 return scale * H def random_psd(M: int, *, seed=None, rank=None, scale=1.0) -> np.ndarray: """Random positive semidefinite matrix via B^dag B.""" rng = np.random.default_rng(seed) r = M if rank is None else int(rank) B = rng.normal(size=(r, M)) + 1j * rng.normal(size=(r, M)) return scale * (B.conj().T @ B) def fidelity(a: np.ndarray, b: np.ndarray) -> float: a, b = np.asarray(a, dtype=complex), np.asarray(b, dtype=complex) if np.linalg.norm(a) == 0 or np.linalg.norm(b) == 0: return 0.0 a, b = a / np.linalg.norm(a), b / np.linalg.norm(b) return float(np.abs(np.vdot(a, b)) ** 2) ``` ```python theme={null} CLASSICAL_SIZE = 2**7 H_classical = random_hermitian(CLASSICAL_SIZE) H_classical /= np.max(np.linalg.svd(H_classical, compute_uv=False)) L_classical = random_psd(CLASSICAL_SIZE) L_classical /= np.max(np.linalg.svd(L_classical, compute_uv=False)) np.random.seed(1) v0_classical = np.random.rand(CLASSICAL_SIZE).astype(complex) v0_classical /= np.linalg.norm(v0_classical) CLASSICAL_SIZE = 2**7 H_classical = random_hermitian(CLASSICAL_SIZE) H_classical /= np.max(np.linalg.svd(H_classical, compute_uv=False)) L_classical = random_psd(CLASSICAL_SIZE) L_classical /= np.max(np.linalg.svd(L_classical, compute_uv=False)) np.random.seed(1) v0_classical = np.random.rand(CLASSICAL_SIZE).astype(complex) v0_classical /= np.linalg.norm(v0_classical) R_classical, J_classical = R, 6 t_classical = 10 out_exact = truth_nonunitary(v0_classical, L_classical, H_classical, t_classical) out_lchs, *_ = lchs_classical( v0_classical, L_classical, H_classical, t_classical, R=R_classical, J=J_classical, gamma=gamma, c=c, ) print(f"Fidelity (LCHS vs exact): {fidelity(out_exact, out_lchs):.6f}") ``` **Output:** ``` Fidelity (LCHS vs exact): 1.000000 ``` ## Quantum Implementation image.png We now implement the full LCHS algorithm as a quantum circuit using Classiq. The circuit follows the structure described in the background: 1. Prepare the kernel amplitudes $|\hat{f}_2(k_j)|$ in the $|j\rangle$ register. 2. Block-encode the $j$-dependent Hamiltonian $(H + k_j L)/\alpha$. 3. Simulate the time evolution via GQSP on the walk operator. 4. Post-select on auxiliary qubits being $|0\rangle$. # ## Data Structures We define quantum struct types for the LCHS circuit. The `LchsBlock` struct holds the auxiliary registers: * `j`: a signed integer register for the grid index, * `lcu_block`: auxiliary qubit for the LCU of $H$ and $kL$, * `linear_block`: auxiliary qubit for the linear block-encoding of $k$ (explained below), * `matrices_block`: auxiliary qubits for the block-encoding of $H$ and $L$. ```python theme={null} class LchsBlock(QStruct): lcu_block: QBit linear_block: QBit matrices_block: QArray[BLOCK_SIZE] class LchsVar(QStruct): x: QNum[STATE_SIZE] j: QNum[J_SIZE, SIGNED, 0] block: LchsBlock ``` # ## Block-Encoding the Hamiltonians For each grid point $j$, we need a block-encoding of the Hermitian operator $$ \frac{H + k_j L}{\alpha}, \qquad \alpha = \alpha_L R + \alpha_H. $$ This is achieved by an LCU decomposition: $$ \frac{H + k_j L}{\alpha} = \frac{\alpha_L R}{\alpha} \cdot \frac{k_j}{R} \cdot \frac{L}{\alpha_L} + \frac{\alpha_H}{\alpha} \cdot \frac{H}{\alpha_H}. $$ The factor $k_j / R$ is block-encoded using a linear amplitude encoding: $|j\rangle|0\rangle \mapsto \frac{j}{2^{J-1}}|j\rangle|0\rangle + |\perp\rangle$, which embeds the linear function of $j$ into an amplitude. The other constants are applied as part of the LCU probabilities. The Pauli-decomposed operators $H$ and $L$ (defined in the parameters cell above) are each block-encoded via `lcu_pauli`, which implements the LCU of Pauli terms using ancilla qubits. ```python theme={null} @qfunc def block_encode_H(x: QNum, block: QArray): lcu_pauli(H_PAULI * (1 / alpha_H), x, block) @qfunc def block_encode_L(x: QNum, block: QArray): lcu_pauli(L_PAULI * (1 / alpha_L), x, block) @qfunc def linear_be(x: QNum, ind: QBit): """ Linear block-encoding: |j>|0> -> (j/2^(J-1))|j>|0> + |perp>. Encodes the linear dependence on the grid index j. """ assign_amplitude_table( amplitudes=lookup_table(lambda y: np.abs(y) / 2 ** (x.size - 1), x), index=x, indicator=ind, ) X(ind) control(x < 0, lambda: phase(pi)) @qfunc def block_encode_hamiltonians(qvar: LchsVar): """ Block-encode sum_j |j> The full LCHS circuit wraps these steps in a `within_apply` pattern: ```python theme={null} @qfunc def lchs(qvar: LchsVar, aux: QBit, t: float): """Full LCHS routine: kernel preparation + Hamiltonian simulation + phase.""" within_apply( lambda: prepare_lchs_kernel(qvar.j), lambda: [ lchs_hamiltonian_simulation(qvar, aux, t), apply_kernel_phase(qvar.j), ], ) ``` # ## Running the Quantum Circuit We define the main function, synthesize the circuit, and execute it using a statevector simulator. We post-select on the block register being $|0\rangle$ to extract the LCHS output state. ```python theme={null} np.random.seed(1) v0 = np.random.rand(2**STATE_SIZE) v0 /= np.linalg.norm(v0) @qfunc def initial_state_prep(x: QNum): inplace_prepare_amplitudes(v0, 0, x) @qfunc def main(x: Output[QNum[STATE_SIZE]], block: Output[QNum]): lchs_qvar = LchsVar() allocate(lchs_qvar) gqsp_aux = QBit() allocate(gqsp_aux) initial_state_prep(lchs_qvar.x) lchs(lchs_qvar, gqsp_aux, T) bind([lchs_qvar, gqsp_aux], [x, block]) ``` ```python theme={null} qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3CJZsTcki5NvhuXNzOIK9EgTVle ``` ```python theme={null} sv = calculate_state_vector(qprog) post_selected = sv[sv.block == 0].sort_values("x") q_out = post_selected.amplitude.values display(post_selected) ``` **Output:** ``` Submitting job to simulator Job: https://platform.classiq.io/jobs/53c14875-06f8-4f2f-8811-8f544efd99f5 ``` | | x | block | amplitude | magnitude | phase | probability | bitstring | | -- | - | ----- | ------------------- | --------- | ------ | ----------- | -------------- | | 58 | 0 | 0 | -0.009290+0.039227j | 0.04 | 0.57π | 0.001625 | 00000000000000 | | 8 | 1 | 0 | -0.162077-0.048177j | 0.17 | -0.91π | 0.028590 | 00000000000001 | | 40 | 2 | 0 | -0.016073+0.054032j | 0.06 | 0.59π | 0.003178 | 00000000000010 | | 29 | 3 | 0 | -0.032474+0.071042j | 0.08 | 0.64π | 0.006101 | 00000000000011 | ## Quantum vs. Classical Comparison We compare the quantum LCHS output against: 1. **Exact**: Direct matrix exponentiation $e^{-(L+iH)t}|u_0\rangle$. 2. **LCHS (classical)**: Classical emulation of the quantum circuit (discretized LCHS with the GQSP polynomial approximation). For this demonstration, we use the block-encodings defined above. ```python theme={null} H_mat = pauli_operator_to_matrix(H_PAULI) L_mat = pauli_operator_to_matrix(L_PAULI) exact_out = truth_nonunitary(v0, L_mat, H_mat, T) lchs_out, *_ = lchs_classical( v0, L_mat, H_mat, T, R=R, J=J_SIZE, gamma=gamma, c=c, ) normalization = np.sqrt(post_selected.probability.sum()) print(f"Fidelity (quantum vs LCHS classical): {fidelity(q_out, lchs_out):.6f}") print(f"Fidelity (quantum vs exact): {fidelity(q_out, exact_out):.6f}") print(f"Sub-normalization Factor (amplitude): {normalization:.6f}") assert np.isclose(fidelity(q_out, exact_out), 1, atol=EPS) ``` **Output:** ``` Fidelity (quantum vs LCHS classical): 1.000000 Fidelity (quantum vs exact): 1.000000 Sub-normalization Factor (amplitude): 0.198731 ``` ## Technical Notes 1. **Access model**: The algorithm assumes block-encoding access to $H$ and $L$ separately. If only access to $A = L + iH$ is available, one can extract $H$ and $L$ via $H = \frac{1}{2i}(A - A^\dagger)$ and $L = \frac{1}{2}(A + A^\dagger)$, though this may double the block-encoding cost. Alternatively, as noted in [\[5\]](#ref-optimal-lchs) (Sec. 1.1.5), one can block-encode $kL + H$ directly from a block-encoding of $A$ using the identity $kL + H = \frac{(k-i)A + ((k-i)A)^\dagger}{2}$. 2. **Time-dependent case**: If $H$ or $L$ are time-dependent, the inner Hamiltonian simulation becomes time-dependent and can be handled using methods such as Dyson or Magnus series (see the [Time Marching](https://github.com/Classiq/classiq-library/blob/main/algorithms/quantum_differential_equations_solvers/time_marching/time_marching.ipynb) notebook). The LCHS framework in [\[5\]](#ref-optimal-lchs) is stated in full generality for time-dependent $A(t)$. 1. **Normalization and post-selection**: The LCU of the kernel introduces a normalization factor $\alpha_{\hat{f}_2} = e^c \operatorname{erfc}(1/(2\gamma)) \leq e^c$, which is $\Theta(1)$ for constant $c$ [\[5\]](#ref-optimal-lchs). The final result is obtained by post-selecting on all block qubits being $|0\rangle$, with success probability determined by this normalization and the GQSP scaling factor. 1. **Scalable kernel preparation**: The `inplace_prepare_amplitudes` function used here loads arbitrary amplitudes but does not scale efficiently. [\[5\]](#ref-optimal-lchs) (Theorem 4) provides an efficient circuit for preparing the kernel state with $\mathcal{O}(\log(\|L\| + \log(1/\epsilon)) \log^{5/2}(1/\epsilon))$ two-qubit gates. ## References \[1] An, D., Liu, J.-P., & Lin, L. *Linear Combination of Hamiltonian Simulation for Nonunitary Dynamics with Optimal State Preparation Cost.* Physical Review Letters **131**, 150603 (2023). [arXiv:2303.01029](https://arxiv.org/abs/2303.01029) \[2] An, D., Childs, A. M., & Lin, L. *Quantum algorithm for linear non-unitary dynamics with near-optimal dependence on all parameters.* Communications in Mathematical Physics (2025). [arXiv:2312.03916](https://arxiv.org/abs/2312.03916) \[3] Motlagh, D. & Wiebe, N. *Generalized Quantum Signal Processing.* PRX Quantum **5**, 020368 (2024). [arXiv:2308.01501](https://arxiv.org/abs/2308.01501) \[4] [Hamiltonian Simulation with Block Encoding (Classiq Library)](https://github.com/Classiq/classiq-library/tree/main/algorithms/hamiltonian_simulation/hamiltonian_simulation_with_block_encoding/) \[5] Low, G. H. & Somma, R. D. *Optimal quantum simulation of linear non-unitary dynamics.* (2025). [arXiv:2508.19238](https://arxiv.org/abs/2508.19238) # Time Marching Based Quantum Solvers for Time-Dependent Linear Differential Equations Source: https://docs.classiq.io/explore/algorithms/quantum_differential_equations_solvers/time_marching/time_marching Open this notebook in GitHub to run it yourself This demonstration is based on the \[[1](#timemarching)] paper. The notebook was written in collaboration with Prof. Di Fang, the first author of the paper. Time marching is a method for solving differential equations in time by integrating the solution vector through time in small discrete steps, where each timestep depends on previous timesteps. This paper applies an evolution matrix sequentially on the state and makes it evolve through time, as done in time-dependent Hamiltonian simulations. ## Defining the Problem * **Input:** a system of linear homogenous linear equations (ODEs): $$ \frac{d}{dt} |\psi(t)\rangle = A(t) |\psi(t)\rangle, \quad |\psi(0)\rangle = |\psi_0\rangle $$ Note that $A$ can vary in time. We assume that the matrix $A$ is with bounded variation. The input model of $A(t)$ is a series of time-dependent block-encodings, described next. * **Output:** a state that is proportional to the solution at time $T$, $|\psi(T)\rangle$. ## Describing the Algorithm The algorithm divides the timeline into long timesteps and short timesteps. In each long timestep, some approximation of evolution of the short timesteps is done, such as the Truncated Dyson series \[[2](#dyson)] or Magnus series \[[3](#magnus)]. These are applied as block-encodings on the state, where the following matrix is block-encoded in each long timestep: $$ \mathcal{\Xi_l} = \mathcal{T} e^{\int_{t_{l-1}}^{t_l} A(t) \, dt} $$ image.png The problem is that when this block-encoding has some prefactor $s$ (for example, because some LCU is block-encoding the integration), the prefactor of the entire simulation is amplified by $s$ on each iteration. This means that the probability to sample the wanted block decreases exponentially with the number of long timesteps. This is the main pain-point that the algorithm in the paper resolves. In the case of Hamiltonian simulation, it is possible to wrap each timestep with oblivious amplitude amplification \[[4](#oaa)] (see [oblivious amplitude amplification](https://github.com/Classiq/classiq-library/blob/main/algorithms/amplitude_amplification_and_estimation/oblivious_amplitude_amplification/oblivious_amplitude_amplification.ipynb)) and get rid of the prefactor. However, it is only possible in the case of a unitary block-encoding. The authors address this issue by using uniform singular amplitude amplification \[[5](#usva)] instead, within the QSVT framework. ## Implementing the Algorithm Using Classiq image.png We choose an easy artificial example to demonstrate the algorithm. For simplicity, we choose $A$, which is easy to block-encode. The following matrix can be easily block-encoded using linear Pauli rotations: $$ A_{ij}(t) = \cos(i+t)\delta_{ij} $$ The matrix is Hermitian and diagonal, and it helps us in several aspects: 1. The first-order Magnus expansion will be exact. 2. The QSVT and QET (quantum eigenvalue transform) will coincide, and we use it to exponentiate the block-encoding. We simulate a 4x4 matrix using four timesteps, from $t=0$ to $t=2$: ```python theme={null} !pip install -qq "classiq[qsp]" ``` ```python theme={null} NUM_LONG_SLICES = 4 START_TIME = 0 END_TIME = 2 DIM_SIZE = 2 ``` # ## Classical Simulation This is how the evolution looks classically: ```python theme={null} import matplotlib.pyplot as plt import numpy as np from scipy.integrate import solve_ivp # Parameters N = 2**DIM_SIZE # Matrix size t_start = 0 t_end = END_TIME # Define the time-dependent diagonal matrix A(t) def A(t): diagonal_elements = np.cos(np.arange(N) + t) # sin(i * t) for i=0,...,N-1 return np.diag(diagonal_elements) # Define the ODE dx/dt = A(t)x def ode_system(t, x): return A(t) @ x # Matrix-vector multiplication # Initial condition x0 = np.ones(N) # Example: start with all ones # Solve the ODE solution = solve_ivp( ode_system, [t_start, t_end], x0, t_eval=np.linspace(t_start, t_end, 100) ) # Extract solution t_vals = solution.t x_vals = solution.y # Plot the solution plt.figure(figsize=(8, 4)) for i in range(N): plt.plot(t_vals, x_vals[i], label=f"x{i}(t)") classical_final = x_vals[:, -1] plt.xlabel("Time t") plt.ylabel("x(t)") plt.title(r"Solution of $\dot{x} = A(t)x$") plt.legend() plt.grid(True) plt.show() ``` output # ## Time-Dependent Block-Encoding The time-dependent block-encoding of $A(t)$: $$ \left( I_{n_q} \otimes \langle 0_m | \otimes I_n \right) U_{A(t)} \left( I_{n_q} \otimes | 0_m \rangle \otimes I_n \right) = \sum_{i=0}^{2^{n_q}-1} | i \rangle \langle i | \frac{A\left((b-a)\frac{i}{{2^{n_q}}}+a\right)}{\alpha} $$ For a given timeslice, we get this: $$ A_{ij}(t, a, b) = \cos((b-a)\frac{t}{2^{n_q}} + a + i)\delta_{ij} $$ We accomplish this easily with a sequence of two Pauli rotations: ```python theme={null} from classiq import * SHORT_INTERVALS_TIME_SIZE = 2 class TimeDependentBE(QStruct): index: QNum[DIM_SIZE] time: QNum[SHORT_INTERVALS_TIME_SIZE] block: QBit @qfunc def block_encode_time_dependent_A(a: CReal, b: CReal, qbe: TimeDependentBE): # a factor 2 is applied on the slopes and offsets as RY rotates at half of the angle linear_pauli_rotations( [Pauli.Y], [(b - a) * 2 / (2**qbe.time.size)], [2 * a], qbe.time, qbe.block ) linear_pauli_rotations([Pauli.Y], [2], [0], qbe.index, qbe.block) ``` # ## Short Time Evolution We use a first-order Magnus expansion, which is exact in this case: $$ \overline{\Xi} = e^{\frac{b-a}{M}} \sum_{k=0}^{M-1} A\left(a + k \frac{b-a}{M}\right) $$ It is built in two steps. # ### 1. Riemannian Summation of Short Timesteps By wrapping the time variable with the Hadamard transform, we get an exact block-encoding of the Reimann sum of the input block-encoding. image.png ```python theme={null} from classiq.qmod.symbolic import logical_and @qfunc def short_time_summation(a: CReal, b: CReal, qbe: TimeDependentBE): """ Riemann summation """ within_apply( lambda: hadamard_transform(qbe.time), lambda: block_encode_time_dependent_A(a, b, qbe), ) # We also define predicates for the usage later on in qsvt def time_dependent_predicate(qbe: TimeDependentBE): return logical_and(qbe.block == 0, qbe.time == 0) @qfunc def time_dependent_projector(qbe: TimeDependentBE, is_in_block: QBit): is_in_block ^= time_dependent_predicate(qbe) ``` # ### 2. Block-Encoding of the Summation Exponential We want to find polynomials for $\cosh(ax)$ and $\sinh(ax)$, to combine them into $e^{ax}$. For pedagogical reasons, we work naively and create a polynomial approximation for each of the odd and even polynomials of $P_{cosh} \approx \frac{\cosh(ax)}{e^a}$ and $P_{sinh} \approx \frac{\sinh(ax)}{e^a}$. Combining them with LCU gives these results: $$ P(x) \approx \frac{e^{ax}}{2e^a}, $$ which is a polynomial bounded by $\frac{1}{2}$. We could choose $P_{cosh} \approx \frac{\cosh(ax)}{\cosh(a)}$ and $P_{sinh} \approx \frac{\sinh(ax)}{\sinh{a}}$. Then, LCU with coefficients $[\frac{\cosh(a)}{\cosh(a)+\sinh(a)}, \frac{\sinh(a)}{\cosh(a)+\sinh(a)}]$ gives us this: $$ P(x) \approx \frac{e^{ax}}{e^a}, $$ which is the best we can get, and doesn't require amplification. We go with the first approach for demonstrating the singular value amplification. Getting rid of this redundant factor 2 can save us a multiplicative factor of $O(2^T)$ in the success probability. ```python theme={null} import matplotlib.pyplot as plt import numpy as np from numpy.polynomial.chebyshev import Chebyshev from classiq.applications.qsp import qsp_approximate def normalized_cosh(a, x): normalization = np.abs(np.exp(a * 1)) return np.cosh(a * x) / normalization def normalized_sinh(a, x): normalization = np.abs(np.exp(a * 1)) return np.sinh(a * x) / normalization A = 2 DEGREE_EXP = 7 poly_coeffs_sinh, opt_res = qsp_approximate( lambda x: normalized_sinh(A, x), DEGREE_EXP, parity=1 ) poly_coeffs_cosh, opt_res = qsp_approximate( lambda x: normalized_cosh(A, x), DEGREE_EXP - 1, parity=0 ) poly_sinh = Chebyshev(poly_coeffs_sinh, domain=[-1, 1]) poly_cosh = Chebyshev(poly_coeffs_cosh, domain=[-1, 1]) x = np.linspace(-1, 1, 1000) plt.plot( x, (0.5 * (poly_sinh + poly_cosh)(x)), label=r"$\approx \frac{\sinh(ax)+\cosh(ax)}{2e^a}$", linewidth=3, ) plt.plot( x, np.exp(A * x) / (2 * np.exp(A)), "--", label=r"$\frac {e^{ax}}{2e^a}$", linewidth=3, ) plt.legend() ``` **Output:** ``` ``` output We transform the polynomials to QSVT phases using the `qsvt_phases` function, and plug them into the `qsvt_lcu` function, which is optimized for implementing a linear combination of two QSVT sequences: ```python theme={null} from classiq.applications.qsp import qsvt_phases class MagnusBE(QStruct): time_dependent: TimeDependentBE qsvt_exp_aux: QBit qsvt_exp_lcu: QBit @qfunc def short_time_magnus(a: CReal, b: CReal, qbe_st: MagnusBE): # compute the coefficient of the expoenent timeslice_duration = (END_TIME - START_TIME) / NUM_LONG_SLICES poly_coeffs_sinh, opt_res = qsp_approximate( lambda x: normalized_sinh(timeslice_duration, x), DEGREE_EXP, parity=1 ) poly_coeffs_cosh, opt_res = qsp_approximate( lambda x: normalized_cosh(timeslice_duration, x), DEGREE_EXP - 1, parity=0 ) phases_sinh = qsvt_phases(poly_coeffs_sinh) phases_cosh = qsvt_phases(poly_coeffs_cosh) prepare_select( [0.5, 0.5], lambda lcu_aux: qsvt_lcu( phases_cosh, phases_sinh, lambda _aux: time_dependent_projector(qbe_st.time_dependent, _aux), lambda _aux: time_dependent_projector(qbe_st.time_dependent, _aux), lambda: short_time_summation(a, b, qbe_st.time_dependent), qbe_st.qsvt_exp_aux, lcu_aux, ), qbe_st.qsvt_exp_lcu, ) def magnus_predicate(qbe: MagnusBE): return logical_and( time_dependent_predicate(qbe.time_dependent), logical_and(qbe.qsvt_exp_aux == 0, qbe.qsvt_exp_lcu == 0), ) @qfunc def magnus_projector(qbe: MagnusBE, is_in_block: QBit): is_in_block ^= magnus_predicate(qbe) ``` # ## Amplification of a Single Long Timestep At the climax of the algorithm, we wrap the Magnus evolution in an amplification step. The prefactor of the exponential block-encoding is 2, so we want to approximate the function $f(x)=2x$ in the interval $[0, \frac{1}{2}]$. We use Classiq's qsp\_approximate to obtain the polynomial approximation. **Our built-in routine is inspired by the paper's approach: it solves a minimax problem to determine the Chebyshev coefficients on a subinterval of $[-1, 1]$, while ensuring the resulting polynomial remains bounded on the entire interval**. # ### Singular Value Amplification ($\gamma x$) ```python theme={null} def target_function(gamma, x): return (1 - 0.1) * gamma * x DEGREE_AMP = 7 GAMMA = 2 poly_coeffs_amp, opt_res = qsp_approximate( lambda x: target_function(GAMMA, x), interval=[-1 / GAMMA, 1 / GAMMA], degree=DEGREE_AMP, parity=1, plot=True, ) qsvt_phases_amp = qsvt_phases(poly_coeffs_amp) ``` output Then we apply the phases on the Magnus block-encoding: ```python theme={null} class LongSliceBE(QStruct): magnus: MagnusBE qsvt_amplification_aux: QBit @qfunc def long_slice_evolution(a: CReal, b: CReal, qbe: LongSliceBE): poly_coeffs_amp, opt_res = qsp_approximate( lambda x: target_function(2, x), interval=[-1 / 2, 1 / 2], degree=DEGREE_AMP, parity=1, ) qsvt_phases_amp = qsvt_phases(poly_coeffs_amp) qsvt( qsvt_phases_amp, lambda _aux: magnus_projector(qbe.magnus, _aux), lambda _aux: magnus_projector(qbe.magnus, _aux), lambda: short_time_magnus(a, b, qbe.magnus), qbe.qsvt_amplification_aux, ) def long_slice_predicate(qbe: LongSliceBE): return logical_and(magnus_predicate(qbe.magnus), qbe.qsvt_amplification_aux == 0) ``` # ## Long Time Evolution Lastly, we sequentially apply the block-encodings of each timeslice. To have a quantum variable that is $|0\rangle$ when all the block encodings are applied to the state, we use a counter. A further amplitude amplification step is possible using the counter; however, we do not do it here. image.png ```python theme={null} import numpy as np class FullBE(QStruct): time_slice: LongSliceBE counter: QNum[np.ceil(np.log2(NUM_LONG_SLICES + 1))] @qfunc def long_time_integrator_step(a: CReal, b: CReal, qbe_full: FullBE): long_slice_evolution(a, b, qbe_full.time_slice) # if in block, decrement the counter control( long_slice_predicate(qbe_full.time_slice), lambda: inplace_add(-1, qbe_full.counter), ) @qfunc def long_time_integrator( T: CReal, num_slices: CInt, qbe_full: FullBE # start from time 0 ): qbe_full.counter ^= num_slices repeat( num_slices, lambda i: long_time_integrator_step( i * T / num_slices, (i + 1) * T / num_slices, qbe_full ), ) @qfunc def main(qbe: Output[FullBE]): allocate(qbe.size, qbe) # initial condition: uniform distribution hadamard_transform(qbe.time_slice.magnus.time_dependent.index) long_time_integrator(END_TIME, NUM_LONG_SLICES, qbe) prefereces = Preferences(optimization_level=0, timeout_seconds=500) execution_preferences = ExecutionPreferences(num_shots=1000000) qmod = create_model( main, preferences=prefereces, execution_preferences=execution_preferences, ) qprog = synthesize(qmod) show(qprog) res = execute(qprog).get_sample_result() ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3CJc4zlCs40jRs9m3cu8msnrDNw ``` # ## Comparing to the Naive Case: Without Uniform Amplification Here we do not use the amplification step. We see that the measured amplitudes are much smaller than in the amplified case. ```python theme={null} @qfunc def long_time_integrator_step_naive(a: CReal, b: CReal, qbe_full: FullBE): short_time_magnus(a, b, qbe_full.time_slice.magnus) # if in block, decrement the counter control( magnus_predicate(qbe_full.time_slice.magnus), lambda: inplace_add(-1, qbe_full.counter), ) @qfunc def long_time_integrator_naive( T: CReal, num_slices: CInt, qbe_full: FullBE # start from time 0 ): qbe_full.counter ^= num_slices repeat( num_slices, lambda i: long_time_integrator_step_naive( i * T / num_slices, (i + 1) * T / num_slices, qbe_full ), ) @qfunc def main(qbe: Output[FullBE]): allocate(qbe.size, qbe) # initial condition: uniform distribution hadamard_transform(qbe.time_slice.magnus.time_dependent.index) long_time_integrator_naive(END_TIME, NUM_LONG_SLICES, qbe) qmod_naive = create_model( main, preferences=prefereces, execution_preferences=execution_preferences ) qprog_naive = synthesize(qmod_naive) show(qprog_naive) res_naive = execute(qprog_naive).get_sample_result() ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3CJcCZf82tWhPKpOzbV5HKmITV4 ``` # ## Post-Processing ```python theme={null} def post_process_res_statevector(result): filtered_samples = [ s for s in result.parsed_state_vector if s.state["qbe"]["counter"] == 0 and np.abs(s.amplitude) > 1e-6 ] global_phase = np.exp(1j * np.angle(filtered_samples[0].amplitude)) amplitudes = np.zeros(2**DIM_SIZE) for sample in filtered_samples: index = sample.state["qbe"]["time_slice"]["magnus"]["time_dependent"]["index"] amplitudes[index] = np.real(sample.amplitude / global_phase) return amplitudes def post_process_res_samples(result): filtered_samples = [s for s in result.parsed_counts if s.state["qbe"].counter == 0] probs = np.zeros(2**DIM_SIZE) for sample in filtered_samples: index = sample.state["qbe"].time_slice.magnus.time_dependent.index probs[index] += sample.shots / result.num_shots return np.sqrt(probs) ``` ```python theme={null} amplitudes_amplified = post_process_res_samples(res) print("amplified amplitudes:", amplitudes_amplified) amplitudes_naive = post_process_res_samples(res_naive) print("naive amplitudes:", amplitudes_naive) print("classical:", classical_final) ``` **Output:** ``` amplified amplitudes: [0.12138369 0.02433105 0.00818535 0.01296148] naive amplitudes: [0.01195826 0.00223607 0.001 0.001 ] classical: [2.48301135 0.49650918 0.18886598 0.33261086] ``` And indeed we can see the the naive amplitudes are order of magnitude smaller than the amplified case (this is exactly what we expect for 4 timesteps). # ## Comparing Classical and Quantum Results In this final step, we verify that the classical and quantum solutions are equivalent: ```python theme={null} expected = classical_final / np.linalg.norm(classical_final) sampled = amplitudes_amplified / np.linalg.norm(amplitudes_amplified) assert np.linalg.norm(sampled - expected) < 0.1 ``` ## References \[1]: [Fang, Di, Lin, Lin, and Tong, Yu. Time-marching based quantum solvers for time-dependent linear differential equations. Quantum 7, 955 (2023).](https://doi.org/10.22331/q-2023-03-20-955) \[2]: \[M. Kieferová, A. Scherer, and D. W. Berry. Simulating the dynamics of timedependent Hamiltonians with a truncated Dyson series. Phys. Rev. A, 99(4), Apr 2019]\([https://arxiv.org/abs/1805.00582](https://arxiv.org/abs/1805.00582)). \[3]: [Magnus Expansion (Wikipedia)](https://en.wikipedia.org/wiki/Magnus_expansion). \[4]: [Berry, Dominic W., et al. Exponential improvement in precision for simulating sparse Hamiltonians. Proceedings of the forty-sixth annual ACM symposium on Theory of Computing. 2014.](https://dl.acm.org/doi/abs/10.1145/2591796.2591854) \[5]: \[A. Gilyén, Y. Su, G. H. Low, and N. Wiebe. Quantum singular value transformation and beyond: exponential improvements for quantum matrix arithmetics. In Proc. 51st Annu. ACM SIGACT Symp. Theory Comput., pages 193\{204, 2019.]\([https://dl.acm.org/doi/10.1145/3313276.3316366](https://dl.acm.org/doi/10.1145/3313276.3316366)) # Solving the Quantum Linear Systems Problem (QLSP) Using AQC Source: https://docs.classiq.io/explore/algorithms/quantum_linear_solvers/adiabatic_linear_solvers/solving_qlsp_with_aqc Open this notebook in GitHub to run it yourself **Adiabatic Quantum Computing (AQC)** leverages the adiabatic theorem to solve computational problems by gradually evolving a quantum system from an initial ground state to the ground state of a problem-specific Hamiltonian (see the AQC tutorial). This tutorial focuses on applying the AQC approach to solve the **Quantum Linear Systems Problem (QLSP)**, a cornerstone problem in quantum computing with significant applications in fields like machine learning, physics, and optimization. Specifically, we aim to demonstrate how to utilize AQC to approximate the solution to the QLSP \[[1](#qlsp)]. This problem involves finding a quantum state that corresponds to the solution of a linear system of equations. The tutorial provides a structured overview of the QLSP, its mathematical formulation, and the steps needed to transform it into an eigenvalue problem, laying the foundation for solving it within the AQC framework. \*This demonstration follows the \[[1](#qlsp)] paper. This notebook was written in collaboration with Prof. Lin Lin and Dr. Dong An, the authors of the paper.\* *** ## Problem Statement Given a Hermitian positive-definite matrix $A$ and a vector $|b\rangle$, the goal is to approximate $|x\rangle$-the solution to the linear system $A|x\rangle=|b\rangle$-as a quantum state. We are given: * **Matrix** $A \in \mathbb{C}^{N \times N}$, an invertible Hermitian and positive-definite matrix with condition number $\kappa$ and $\|A\|_2 = 1$. * **Vector** $|b\rangle \in \mathbb{C}^N$, a normalized vector. * **Target Error** $\epsilon$, specifying the desired accuracy. The goal is to prepare a quantum state $|x_a\rangle$, which is an $\epsilon$-approximation of the normalized solution $|x\rangle = A^{-1}|b\rangle / \|A^{-1}|b\rangle\|_2$. The approximation satisfies $$ \| |x_a\rangle \langle x_a| - |x\rangle \langle x| \|_2 \leq \epsilon. $$ ## Transformation into AQC The QLSP is converted into an equivalent eigenvalue problem to leverage quantum computation. This involves the following steps. # ## 1. Constructing $H_0$ Define: $$ H_0 = \sigma_x \otimes Q_b = \begin{bmatrix} 0 & Q_b \\ Q_b & 0 \end{bmatrix}, $$ where $Q_b = I_N - |b\rangle \langle b|$ is a projection operator orthogonal to $|b\rangle$. Key properties: * $H_0$ is Hermitian. * The null space of $H_0$: $\text{Null}(H_0) = \text{span}(|\tilde{b}\rangle, |\bar{b}\rangle)$, where $$ |\tilde{b}\rangle = |0, b\rangle = \begin{bmatrix} b \\ 0 \end{bmatrix}, \quad |\bar{b}\rangle = |1, b\rangle = \begin{bmatrix} 0 \\ b \end{bmatrix}. $$ > *The **null space** of a matrix $A$ is the set of all vectors $\mathbf{x}$ such that* > > $A\mathbf{x} = 0.$ > > *With regards to eigenstates and eigenvalues:* > > * *The null space corresponds to the eigenspace of $A$ associated with the eigenvalue $0$.* > * *Any vector in the null space is an eigenvector of $A$ with eigenvalue $0$.* # ## 2. Constructing $H_1$ Define: $$ H_1 = \sigma_+ \otimes (AQ_b) + \sigma_- \otimes (Q_bA) = \begin{bmatrix} 0 & AQ_b \\ Q_bA & 0 \end{bmatrix}, $$ where $\sigma_\pm = \frac{1}{2}(\sigma_x \pm i\sigma_y)$. Key properties: * If $A|x\rangle \propto |b\rangle$, then $Q_bA|x\rangle = Q_b|b\rangle = 0$. * Null space of $H_1$: $\text{Null}(H_1) = \text{span}(|\tilde{x}\rangle, |\bar{b}\rangle)$, where $$ |\tilde{x}\rangle = |0, x\rangle = \begin{bmatrix} x \\ 0 \end{bmatrix}, \quad $$ # ## 3. Adiabatic Interpolation Construct an interpolation Hamiltonian: $$ H(f(s)) = (1 - f(s))H_0 + f(s)H_1, \quad 0 \leq s \leq 1, $$ where $f(s)$ is a monotonic function mapping $[0, 1] \to [0, 1]$. # ## 4. Spectral Gap * $Q_b$ is a projection operator, and the spectral gap between $0$ and the rest of the eigenvalues of $H_0$ is $1$. * For $H_1$, the gap between $0$ and the rest of the eigenvalues is bounded from below by $1/\kappa$. \[[1](#qlsp)] # ## 5. Adiabatic Evolution and Null Space Note that there is a degeneracy in the number of null states (unlike the regular adiabatic algorithm usage where we typically look at a single ground state): $$ \text{Null}(H_1) = \text{span}(|\tilde{x}\rangle, |\bar{b}\rangle) $$ . We also note that for any $s$, $|\bar{b}\rangle$ is always in the null space of $H(f(s))$; i.e., $$ |\bar{b}\rangle \in \text{Null}(H(f(s))). $$ Therefore, there exists an additional statestate $|\tilde{x(s)}\rangle = |0\rangle \otimes |x(s)\rangle$, such that $$ \text{Null}(H(f(s))) = \{|\tilde{x(s)}\rangle, |\bar{b}\rangle\}. $$ In particular: * At $s = 0$, $|\tilde{x(0)}\rangle = |\tilde{b}\rangle$, the initial state. * At $s = 1$, $|\tilde{x(1)}\rangle = |\tilde{x}\rangle$, the solution state. Thus, $|\tilde{x(s)}\rangle$ state represents the desired **adiabatic path** for the evolution \[[1](#qlsp)]. ## AQC Approach to Solving QLSP The adiabatic quantum algorithm prepares the zero-energy state $|\tilde{x}\rangle$ of $H_1$ as follows: 1. Initialize in the ground state of $H_0$; i.e., $|\tilde{b}\rangle$. 2. Slowly evolve the system by varying $f(s)$ from $f(0) = 0$ to $f(1) = 1$. 3. At the end of the evolution, the system approximates $|\tilde{x}\rangle$, embedding $|x\rangle$, which is the solution of the QLSP. Goals: * **Set up a QLSP example:** Derive $H_0$, $H_1$ and define the interpolation Hamiltonian. * **Quantum circuit design:** Implement Hamiltonian simulation for $H(f(s))$. * **Evaluate results:** Compare quantum simulation results with the numeric calculation. *** Let's begin with the mathematical setup and continue on to implementation. ## Setting Up a QLSP Example Where A Is a 4x4 Matrix For simplicity, we first assume A is Hermitian and positive definite. ```python theme={null} import numpy as np # Define matrix A and vector b A = np.array([[4, 1, 2, 0], [1, 3, 0, 1], [2, 0, 3, 1], [0, 1, 1, 2]]) b = np.array([12, 10, 17, 26]) ``` As a purely mathematical preprocessing step, we calculate the condition number $k$ for $A$. > In practical scenarios, the condition number is often approximated or known beforehand based on external factors or prior knowledge. ```python theme={null} import numpy as np def compute_condition_number(A): """ Computes the condition number (κ) of a matrix A. Parameters: A (numpy.ndarray): Input square matrix. Returns: float: The condition number of A. """ try: # Compute the norm of A (2-norm, largest singular value) norm_A = np.linalg.norm(A, 2) # Compute the inverse of A A_inv = np.linalg.inv(A) # Compute the norm of A^-1 (2-norm) norm_A_inv = np.linalg.norm(A_inv, 2) # Compute the condition number κ condition_number = norm_A * norm_A_inv return condition_number except np.linalg.LinAlgError: return float("inf") # Return infinity if the matrix is singular condition_number = compute_condition_number(A) print("Condition number:", condition_number) ``` **Output:** ``` Condition number: 14.472337634948623 ``` # ## Constructing $H_0$ and $H_1$ The `setup_QLSP` function prepares the necessary Hamiltonians and normalized components to solve the Quantum Linear Systems Problem (QLSP). The built-in `matrix_to_hamiltonian` function, used in the `setup_QLSP` function, encodes the Hamitonian matrix into a sum of Pauli strings that is used to exponentiate the Hamiltonians with a product formula (Suzuki-Trotter) in the next step. ```python theme={null} from classiq import * from classiq.execution import * def setup_QLSP(A, b): # Normalize A norm_A = np.linalg.norm(A, "fro") A_normalized = A / norm_A # Normalize vector b b_normalized = b / np.linalg.norm(b) # Create the outer product of b outer_product_b = np.outer(b_normalized, b_normalized) # Define the identity matrix I with the same size as b identity_matrix = np.eye(len(b)) # Compute Qb = I - outer_product_b Qb = identity_matrix - outer_product_b # Define the Pauli-X (σx) and Pauli-Y (σy) matrices pauli_x = np.array([[0, 1], [1, 0]]) pauli_y = np.array([[0, -1j], [1j, 0]]) # Define Pauli plus and minus operators pauli_plus = 0.5 * (pauli_x + 1j * pauli_y) pauli_minus = 0.5 * (pauli_x - 1j * pauli_y) # Compute the tensor product of Pauli-X and Qb H0 = np.kron(pauli_x, Qb) # Compute A*Qb and Qb*A A_Qb = np.dot(A, Qb) Qb_A = np.dot(Qb, A) # Compute the tensor products tensor_plus = np.kron(pauli_plus, A_Qb) tensor_minus = np.kron(pauli_minus, Qb_A) # Define H1 as the sum of the two tensor products H1 = tensor_plus + tensor_minus HO_HAMILTONIAN = matrix_to_hamiltonian(H0) H1_HAMILTONIAN = matrix_to_hamiltonian(H1) return H0, H1, HO_HAMILTONIAN, H1_HAMILTONIAN, A_normalized, b_normalized # Setup H0, H1, HO_HAMILTONIAN, H1_HAMILTONIAN, A_normalized, b_normalized = setup_QLSP(A, b) ``` # ## Defining the Interpolation Hamiltonian For the sake of simplicity, **we first use the "vanilla AQC" linear scheduling function $f(s(t)) = s(t) = t / T$** to define the time-dependent interpolated Hamiltonian, where T is the total evolution time. As $t$ progresses from 0 to $T$, $f(s)$ satisfies the $[0, 1] \to [0, 1]$ mapping. ```python theme={null} # Define the time-dependent interpolated Hamiltonian, where T is the total evolution time def hamiltonian_t(H0, H1, t, T): s = t / T return ((1 - s) * H0) + (s * H1) ``` # ## Analyzing the Spectral Gap From the quantum adiabatic theorem \[[3](#eta), Theorem 3] the formula for the adiabatic error bound $\eta$ at any point $s$ is $$ \eta(s) = C \left\{ \frac{\|H^{(1)}(0)\|^2}{T \Delta^2(0)} + \frac{\|H^{(1)}(s)\|^2}{T \Delta^2(f(s))} + \frac{1}{T} \int_0^s \left[ \frac{\|H^{(2)}(s')\|^2}{\Delta^2(f(s'))} + \frac{\|H^{(1)}(s')\|^2}{2\Delta^3(f(s'))} \right] ds' \right\}. $$ See [Appendix A](#appendix-a-explanation-of-the-adiabatic-error-bound-components) for a detailed explanation of the components. The formula shows that the adiabatic error is minimized when 1. The total runtime $T$ is large (slow evolution). 2. The spectral gap $\Delta$ is large (well-separated ground and excited states). 3. The derivatives $\|H^{(1)}\|$ and $\|H^{(2)}\|$ are small (smooth Hamiltonian changes). The following function plots the spectral gap $\Delta$ evolution: ```python theme={null} import matplotlib.pyplot as plt def plot_eigenvalues_evolution(ylim=None): time_steps = np.linspace(0, 1, 100) # Discrete time steps # Store eigenvalues at each time step eigenvalues = [] # Calculate eigenvalues across all time steps for t in time_steps: H_t = hamiltonian_t(H0, H1, t, 1) eigvals = np.linalg.eigvalsh(H_t) # Sorted real eigenvalues eigenvalues.append(eigvals) # Convert eigenvalues list to a NumPy array for easier manipulation eigenvalues = np.array(eigenvalues) # Add small offsets to separate close eigenvalues visually offsets = np.linspace( -0.05, 0.05, eigenvalues.shape[1] ) # Small offsets for each eigenvalue line # Plot the eigenvalues across time steps plt.figure(figsize=(10, 6)) for i in range(eigenvalues.shape[1]): plt.plot(time_steps, eigenvalues[:, i] + offsets[i], label=f"Eigenvalue {i+1}") # Highlight degenerate eigenvalues (if any) for step_idx, t in enumerate(time_steps): unique_vals, counts = np.unique(eigenvalues[step_idx], return_counts=True) # Apply y-axis limits if provided if ylim: plt.ylim(ylim) # Customize the plot plt.xlabel("Time (t)", fontsize=12) plt.ylabel("Eigenvalues", fontsize=12) plt.title("Eigenvalues Evolution Across $s$", fontsize=14) plt.grid() # Move the legend to the side plt.legend(bbox_to_anchor=(1.05, 1), loc="upper left", borderaxespad=0.0) plt.tight_layout() # Show the plot plt.show() ``` ```python theme={null} plot_eigenvalues_evolution() ``` output To focus on the spectral gap from the null states, we can zoom in on our plot: ```python theme={null} plot_eigenvalues_evolution(ylim=(-1.5, 1.5)) ``` output We can visually observe from the above that although the spectral gap does change throughout $s$ it still stays quite large in our example, so we can choose $T$ accordingly. Without going into detail we choose a simple value for $T$ (represented as `TOTAL_EVOLUTION_TIME`). However, if we simply assume $\|H^{(1)}\|^2$, $\|H^{(2)}\|^2$ are bounded by constants, and use the worst-case bound that $\Delta \geq \kappa^{-1}$, it can be shown that to have $\eta(1) \leq \epsilon$, the runtime of vanilla AQC is $T \propto \kappa^3 / \epsilon$. ```python theme={null} TOTAL_EVOLUTION_TIME = 7 ``` *** # ## Implementing AQC Since $$ |\psi_T(s) \rangle = \mathcal{T}\exp \left( -iT \int_0^s H(f(s')) ds' \right) |\psi_T(0) \rangle \ , $$ where $\mathcal{T}$ is the time-ordering operator, it is sufficient to implement an efficient time-dependent Hamiltonian simulation of $H(f(s))$. One straightforward approach to achieve this is using the Trotter splitting method. The lowest order approximation takes the form $$ \mathcal{T}\exp \left( -iT \int_0^s H(f(s')) ds' \right) \approx \prod_{m=1}^M \exp \left( -iTh H(f(s_m)) \right) $$ which can further be approximated as $$ \prod_{m=1}^M \exp \left( -iTh(1 - f(s_m)) H_0 \right) \exp \left( -iTh f(s_m) H_1 \right) $$ where $$ h = s/M, s_m = mh. $$ It is proved in \[[2](#wim)] that the error of such an approximation is $$ \mathcal{O}(\text{poly}(\log(N)) \cdot T^2 / M), $$ which indicates that to achieve an $ \epsilon$-approximation, it suffices to choose $$ M = \mathcal{O}(\text{poly}(\log(N)) \cdot T(\epsilon)^2 / \epsilon). $$ (Note that in our case $T$ also depends on $\epsilon$.) # ### Building the Quantum Model Using the Classiq platform, we implement the adiabatic path with Suzuki-Trotter decomposition for Hamiltonian exponentiation. In our model $T$ is represented by the `TOTAL_EVOLUTION_TIME` and $M$ is represented by `NUM_STEPS`. In the Trotter implementation, $M$ scales as $O(T^2)$ with respect to the runtime $T$, therefore this is our rough choice of $M$: ```python theme={null} NUM_STEPS = 50 ``` We are now ready to build our quantum model. **The `adiabatic_evolution_qfunc` function implements the adiabatic path with Suzuki-Trotter decomposition for Hamiltonian exponentiation:** ```python theme={null} @qfunc def adiabatic_evolution_qfunc( H0: CArray[PauliTerm], H1: CArray[PauliTerm], evolution_time: int, num_steps: int, qba: QArray, ): # Time step for each increment delta_t = evolution_time / num_steps for step in range(num_steps): t = step * delta_t suzuki_trotter( H0, evolution_coefficient=delta_t * (1 - t / evolution_time), order=1, repetitions=1, qbv=qba, ) suzuki_trotter( H1, evolution_coefficient=delta_t * (t / evolution_time), order=1, repetitions=10, qbv=qba, ) ``` To solve the QLSP we first prepare the $H_0$ zero state $|\tilde{b}\rangle$ and then initiate the evolution: ```python theme={null} def get_model(H0, H1, b, evolution_time, num_steps): @qfunc def main(qba: Output[QArray]): prepare_state( probabilities=(np.abs(np.kron(np.array([1, 0]), b)) ** 2).tolist(), bound=0, out=qba, ) adiabatic_evolution_qfunc(H0, H1, evolution_time, num_steps, qba) execution_preferences = ExecutionPreferences( num_shots=1, backend_preferences=ClassiqBackendPreferences( backend_name=ClassiqSimulatorBackendNames.SIMULATOR_STATEVECTOR ), ) return create_model(main, execution_preferences=execution_preferences) qmod_1 = get_model( HO_HAMILTONIAN, H1_HAMILTONIAN, b_normalized, TOTAL_EVOLUTION_TIME, NUM_STEPS ) ``` # ### Synthesizing, Verifying, and Executing Synthesize the model into a quantum program, verify it, and execute it on a state vector simulator: ```python theme={null} qprog_1 = synthesize(qmod_1) show(qprog_1) result_1_state_vector = execute(qprog_1).result_value().state_vector ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/32paNlft3ZoQMzRuiu8FBiTDCnm ``` # ### Evaluating the Results ```python theme={null} def plot_state_probabilities(title, x, color="b"): # Ensure x is a numpy array and normalized x = np.array(x) # Calculate probabilities probabilities = np.abs(x) ** 2 # Create labels for the states labels = [f"|{i}>" for i in range(len(x))] # Plot the probabilities plt.bar(labels, probabilities, color=color, alpha=0.7) plt.xlabel("States") plt.ylabel("Probabilities") plt.title(title) plt.xticks(rotation=45) plt.ylim(0, 1) plt.show() def compare_states( state1, state1_label, state2, state1_labe2, color1="gold", color2="b" ): # Plot a histogram of each state probabilities plot_state_probabilities(state1_label, state1, color1) plot_state_probabilities(state1_labe2, state2, color2) # Check the overlap between states overlap = np.abs(np.vdot(state1, state2)) ** 2 print(f"Similarity of results: {overlap:.4f}") ``` ```python theme={null} # Print the solution vector x x = np.linalg.solve(A_normalized, b_normalized) print("Solution vector x:") normalized_x = x / np.linalg.norm(x) print(normalized_x) # Convert dictionary values to complex numbers print("State vector:") state_vector = np.array([complex(value) for value in result_1_state_vector.values()]) print(state_vector) compare_states( state_vector, "Quantum simulator state_vector", np.kron(np.array([1, 0]), normalized_x), "Classical solution to normalized_x", ) ``` **Output:** ``` Solution vector x: [ 0.32300564 -0.23491319 -0.22423532 0.88893288] State vector: [-0.1344498 +0.27328988j 0.08719839-0.17724414j 0.13935271-0.28325579j -0.38067994+0.77379049j -0.12870338-0.06331791j 0.03060676+0.01505754j 0.0128234 +0.0063087j 0.04876379+0.02399021j] ``` output output **Output:** ``` Similarity of results: 0.9646 ``` **By comparing the quantum-computed results with the mathematically expected solution, we observe a good alignment, showcasing the potential of the AQC approach for solving linear problems.** We can observe the runtime of the above implementation by analyzing the depth parameter from the transpiled circuit data of our quantum program: ```python theme={null} print("Program depth:", qprog_1.transpiled_circuit.depth) ``` **Output:** ``` Program depth: 23488 ``` For alternative QLSP configurations, it may be necessary to select different values for $T$ and $M$, and these choices will naturally impact the circuit depth. For instance: ```python theme={null} # Define matrix A and vector b A = np.array( [ [3.9270525, 1.06841123, 2.09661281, -0.10400811], [1.06841123, 2.93584295, -0.0906049, 1.09754032], [2.09661281, -0.0906049, 2.87204449, 1.13774997], [-0.10400811, 1.09754032, 1.13774997, 1.85170585], ] ) b = np.array([12, 10, 17, 26]) condition_number = compute_condition_number(A) print("Condition number:", condition_number) H0, H1, HO_HAMILTONIAN, H1_HAMILTONIAN, A_normalized, b_normalized = setup_QLSP(A, b) ``` **Output:** ``` Condition number: 5722470278.425136 ``` ```python theme={null} plot_eigenvalues_evolution(ylim=(-1.5, 1.5)) ``` output Although the condition number is higher, the spectral gap remains relatively large. However, using the same values of $T$ and $M$ as chosen above result in an increased error: ```python theme={null} qmod_2 = get_model( HO_HAMILTONIAN, H1_HAMILTONIAN, b_normalized, TOTAL_EVOLUTION_TIME, NUM_STEPS ) qprog_2 = synthesize(qmod_2) show(qprog_2) result_2_state_vector = execute(qprog_2).result_value().state_vector ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/32paYKGatGFV3MU1NEyHkU7B200 ``` ```python theme={null} # Print the solution vector x x = np.linalg.solve(A_normalized, b_normalized) print("Solution vector x:") normalized_x = x / np.linalg.norm(x) print(normalized_x) # Convert dictionary values to complex numbers print("State vector:") state_vector = np.array([complex(value) for value in result_2_state_vector.values()]) print(state_vector) compare_states( state_vector, "Quantum simulator state_vector", np.kron(np.array([1, 0]), normalized_x), "Classical solution to normalized_x", ) ``` **Output:** ``` Solution vector x: [ 0.42009163 -0.39396804 -0.55637591 0.59896415] State vector: [-0.22591226+0.29881383j 0.20964928-0.27730282j 0.40300266-0.53305108j -0.24070089+0.31837474j -0.22176295-0.16765947j 0.10465848+0.07912496j 0.14898925+0.11264036j -0.0477411 -0.03609371j] ``` output output **Output:** ``` Similarity of results: 0.8194 ``` ```python theme={null} print("Program depth:", qprog_2.transpiled_circuit.depth) ``` **Output:** ``` Program depth: 23438 ``` Choosing $T$ and $M$ accordingly improves results but affects the overall runtime: ```python theme={null} TOTAL_EVOLUTION_TIME = 10 NUM_STEPS = 100 ``` ```python theme={null} qmod_3 = get_model( HO_HAMILTONIAN, H1_HAMILTONIAN, b_normalized, TOTAL_EVOLUTION_TIME, NUM_STEPS ) qprog_3 = synthesize(qmod_3) show(qprog_3) result_3_state_vector = execute(qprog_3).result_value().state_vector ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/32pap71tIHx3qNNfMVnzjmEUzcU ``` ```python theme={null} # Print the solution vector x x = np.linalg.solve(A_normalized, b_normalized) print("Solution vector x:") normalized_x = x / np.linalg.norm(x) print(normalized_x) # Convert dictionary values to complex numbers print("State vector:") state_vector = np.array([complex(value) for value in result_3_state_vector.values()]) print(state_vector) compare_states( state_vector, "Quantum simulator state_vector", np.kron(np.array([1, 0]), normalized_x), "Classical solution to normalized_x", ) ``` **Output:** ``` Solution vector x: [ 0.42009163 -0.39396804 -0.55637591 0.59896415] State vector: [ 0.48117857-0.16691207j -0.33080455+0.11475006j -0.49918647+0.17315868j 0.51873488-0.17993967j 0.04227904+0.12188316j -0.01994299-0.05749218j -0.04130777-0.11908314j 0.01106469+0.03189757j] ``` output output **Output:** ``` Similarity of results: 0.9501 ``` ```python theme={null} print("Program depth:", qprog_3.transpiled_circuit.depth) ``` **Output:** ``` Program depth: 46088 ``` As expected, the overall circuit depth increases to achieve a high degree of similarity in the results. Although the results are satisfying, a more optimal approach can be implemented based on the discrete adiabatic theorem \[[4](#discrete)]. > ***Important:*** The implementation example above aims to provide an intuitive understanding of the principles behind solving quantum linear solver problems with the adiabatic quantum evolution and the associated error bounds. However, for simplicity and accessibility, several aspects of the implementation are not optimal: > > * **Suzuki-Trotter Decomposition**: We utilized the first order Suzuki-Trotter approximation for time evolution. As such (as mentioned above), a more optimal approach based on the discrete adiabatic theorem \[[4](#discrete)] could be implemented. > * **Schedule Function**: We used the vanilla AQC scheduling function. Using more sophisticated scheduling functions as suggested in \[[1](#qlsp)] will improve runtime. > * **Brute-Force Encoding**: The encoding of Pauli operators in this tutorial is direct and unoptimized, scaling exponentially with system size. Other encoding techniques (such as suggested in \[[4](#discrete)]) will be more efficient. > > These choices were made to prioritize conceptual clarity over computational efficiency. The next step would be to apply state-of-the-art techniques and show improvement in gate complexity and runtime. ## Appendix A: Explanation of the Adiabatic Error-Bound Components The adiabatic error bound $\eta(s)$ is defined by the quantum adiabatic theorem \[[3](#eta), Theorem 3]: $$ \eta(s) = C \left\{ \frac{\|H^{(1)}(0)\|^2}{T \Delta^2(0)} + \frac{\|H^{(1)}(s)\|^2}{T \Delta^2(f(s))} + \frac{1}{T} \int_0^s \left[ \frac{\|H^{(2)}(s')\|^2}{\Delta^2(f(s'))} + \frac{\|H^{(1)}(s')\|^2}{2\Delta^3(f(s'))} \right] ds' \right\}. $$ Detailed explanation of components: 1. **$\eta(s)$**: * Represents the adiabatic error bound at a specific point $s \in [0, 1]$. * Quantifies the deviation of the quantum state from the ground state during evolution. 1. **$C$**: * A proportionality constant that depends on system-specific properties such as the dimensionality and scaling of norms. 1. **$\|H^{(1)}(0)\|$**: * The operator norm of the **first derivative** of the Hamiltonian, $H(s)$, evaluated at $s = 0$. * Indicates how quickly the Hamiltonian initially changes. 1. **$T$**: * The total runtime of the adiabatic evolution. * Larger $T$ values reduce the error, as slower evolution aids adiabaticity. 1. **$\Delta(0)$**: * The spectral gap at $s = 0$, defined as the energy difference between the ground state and the first excited state of $H(s)$. * Larger gaps improve the adiabatic process. 1. **$\|H^{(1)}(s)\|$**: * The operator norm of the first derivative of $H(s)$ at an intermediate point $s$. * Reflects how fast the Hamiltonian changes during evolution. 1. **$\Delta(f(s))$**: * The spectral gap at the point $s$, mapped via the function $f(s)$. 1. **$\|H^{(2)}(s')\|$**: * The operator norm of the **second derivative** of the Hamiltonian, $H^{(2)}(s')$, at a point $s'$. * Captures the curvature or acceleration of the Hamiltonian's evolution. 1. **$\int_0^s \cdots ds'$**: * An integral from $0$ to $s$, summing contributions of the Hamiltonian's derivatives over the path. * Accounts for cumulative effects of the Hamiltonian's changes during evolution. 1. **$\Delta^2(f(s'))$** and **$\Delta^3(f(s'))$**: * Higher powers of the spectral gap at $s'$. * Larger gaps (in $\Delta^2$ and $\Delta^3$) significantly reduce the adiabatic error. ## References \[1]: [An, D. and Lin, L. 2022. "Quantum Linear System Solver Based on Time-Optimal Adiabatic Quantum Computing and Quantum Approximate Optimization Algorithm." ACM Trans. Quantum Comput. 3. arXiv:1909.05500](https://arxiv.org/abs/1909.05500) \[2]: [Wim van Dam, Michele Mosca, and Umesh Vazirani. 2001. How powerful is adiabatic quantum computation? In Proceedings 42nd IEEE Symposium on Foundations of Computer Science. IEEE, Piscataway, NJ, 279-287](https://arxiv.org/abs/quant-ph/0206003). \[3]: [Sabine Jansen, Mary-Beth Ruskai, and Ruedi Seiler. 2007. Bounds for the adiabatic approximation with applications to quantum computation. J. Math. Phys. 48, 10, 102111](https://arxiv.org/abs/quant-ph/0603175). \[4]: \[The discrete adiabatic quantum linear system solver has lower constant factors than the randomized adiabatic solver. Pedro C.S. Costa, Dong An, Ryan Babbush, Dominic Berry]\([https://arxiv.org/abs/2312.07690](https://arxiv.org/abs/2312.07690)). # HHL Algorithm Source: https://docs.classiq.io/explore/algorithms/quantum_linear_solvers/hhl/hhl Open this notebook in GitHub to run it yourself > **The HHL algorithm** [\[1\]](#hhl), named after Harrow, Hassidim and Lloyd, is a fundamental quantum algorithm designed to solve a set of linear equations: $$ A\vec{x} = \vec{b}, $$ where $A$ is an $N\times N$ matrix, and $\vec{x}$ and $\vec{b}$ are vectors of size $N = 2^n$. The algorithm prepares a quantum state $|x\rangle$ that encodes the solution vector proportional to $A^{-1}\vec{b}$, starting from the input state $\vec{b}$. It achieves this by applying Quantum Phase Estimation (QPE) to extract the eigenvalues of $A$, then performing a controlled rotation that effectively inverts these eigenvalues. The resulting amplitudes represent the solution state, which can be used to estimate properties of $\vec{x}$ efficiently for certain classes of matrices. > The algorithm treats the problem in the following way: > > * **Input:** A Hermitian matrix $A$ of size $2^n\times 2^n$, normalized vector $\vec{b}$, $|\vec{b}|=1$ and given precision $m$. > * **Promise:** $A$ is Hermitian, sparse and well-conditioned. In addition, the eigenvalues of $A$ lie between $1/\kappa$ and $1$, where $\kappa$ is the [condition number](https://en.wikipedia.org/wiki/Condition_number) of $A$. The entries of vector $\vec{b}$ can be loaded efficiently to a quantum state $|b\rangle$. > * **Output:** Solution vector encoded in the state $|x\rangle = |A^{-1}b\rangle = \sum^{2^n-1}_{j=0} \frac{\beta_j}{\tilde{\lambda}_j} |u_j\rangle_{\text{mem}}$, where: $\lambda_j$ are eigenphases of a unitary $U = e^{2\pi i A}$, estimated up to $m$ binary digits and $\beta_j$ are coefficients in expansion of initial state $|b\rangle$ into linear combination of eigenvectors of $A$, such that $|b\rangle = \sum^{2^n-1}_{j=0}\beta_j|u_j\rangle_{\text{mem}}$. The solution vector allows one to efficiently estimate $\vec{x}^T M \vec{x}$ for a Hermitian matrix $M$. > > **Complexity:** HHL can find a quantum representation of the solution in polylogarithmic time, with a runtime of roughly $$ O(\log N \kappa^2/\epsilon)~~, $$ where $N=2^n$, and $\epsilon$ is the desired precision. In comparison, the best classical method requires polynomial time $O(\kappa N)$ (or $O(\sqrt{\kappa} N)$ for positive definite matrices). Therefore, the quantum algorithm apparently represents an exponential speedup in problem size; however, this speedup depends on strong assumptions: $A$ must be sparse, well-conditioned, and efficiently simulatable. Specifically, $\kappa$ and $1/\epsilon$ should be $O(\text{poly} \log(N))$ (i.e., upper bounded by a polynomial expression with argument $\log(N)$). Moreover, $|b\rangle$ should be loaded efficiently, and our goal is either to only estimate observables like quantities of the form $\vec{x}^T M \vec{x}$, only sample $\vec{x}$ or utilize $\vec{x}$ for further matrix computations \[6]. > *** > > **Keywords:** Fundamental quantum algorithm, Exponential speedup, Linear equation solver Solving linear equations appears in many research, engineering, and design fields. For example, many physical and financial models, from fluid dynamics to portfolio optimization, are described by partial differential equations, which are typically treated by numerical schemes, most of which are eventually transformed to a set of linear equations. For simplicity, the demo below treats a use case where the matrix $A$ has eigenvalues in the interval $(0,1)$ and $|\vec{b}|=1$. Generalizations to other use cases, including the case where $|\vec{b}|\neq 1$ and $A$ is not a Hermitian matrix or not of size $2^n \times 2^n$, are discussed at the end of this notebook. The discussion begins with a concise overview of the algorithm, followed by its definition and implementation using Classiq's built-in functions. We conclude by comparing the exact implementation of the algorithm with an approximate method, which is often more practical for real-world systems requiring extensive computational resources. ## Algorithm Steps The algorithm is composed of five main steps: 1. Three quantum variables, coined here a "memory", "estimator" and indicator are initialized with $n$, $m$, and $1$ qubits respectively and the vector $\vec{b}$ is encoded into the memory variable: $$ |0\rangle_{\text{mem}} |0\rangle_{\text{est}}|0\rangle_{\text{ind}} \xrightarrow[]{{\rm SP}} |b\rangle_{\text{mem}} |0\rangle_{\text{est}}|0\rangle_{\text{ind}}=\sum_{j=0}^{2^n-1}\beta_j| u_j \rangle_{\text{mem}}|0\rangle_{\text{est}}|0\rangle_{\text{ind}}~~, $$ where $\beta_j$ are the coefficients of $\vec{b}$ in the eigenbasis of $A$, and $|u_j\rangle$ eigenstates of $U=\exp(i 2\pi A)$. 2. Quantum Phase Estimation is applied to the initial state, $$ \xrightarrow[]{{\rm QPE}} \sum^{2^n-1}_{j=0}\beta_j |u_j\rangle_{\text{mem}}|\tilde{\lambda}_j\rangle_{\text{est}} |0\rangle_{\text{ind}}~~, $$ where $\tilde{\lambda}_j$ are the $m$ bit approximations of the associated phases. This step can be achieved with roughly $O(\kappa^2 s/\epsilon)$ queries to $A$, where $s$ is the sparsity of $A$, i.e., each row of $A$ has at most $s$ non-zero entries. 3. Controlled rotations are applied to indicator bit $|0\rangle_{\text{ind}}$ with normalized constant $C = \frac{1}{2^m}$, $$ \xrightarrow[]{{\rm \lambda^{-1}}} \sum^{2^n-1}_{j=0}{\beta_j |u_j\rangle_{\text{mem}}|\tilde{\lambda}_j\rangle_{\text{est}} \left(\sqrt{1-\frac{C^2}{\tilde{\lambda}^2_j}}|0\rangle_{\text{ind}} + \frac{C}{\tilde{\lambda}_j}|1\rangle_{\text{ind}} \right)} $$ . 4. Eigenvalues $|\tilde{\lambda}_j\rangle_{\text{est}}$ are uncomputed with QPE$^\dagger$, $$ \xrightarrow[]{{\rm QPE^\dagger}} \sum^{2^n-1}_{j=0}{\beta_j |u_j\rangle_{\text{mem}}|0\rangle_{\text{est}} \left(\sqrt{1-\frac{C^2}{\tilde{\lambda}^2_j}}|0\rangle_{\text{ind}} + \frac{C}{\tilde{\lambda}_j}|1\rangle_{\text{ind}} \right)} $$ $$ =|\text{garbage}\rangle|0 \rangle_{\text{ind}} + \sum^{2^n-1}_{j=0}\frac{C}{\tilde{\lambda}_j}|b\rangle_{\text{mem}} |0\rangle_{\text{est}}|1\rangle_{\text{ind}} $$ . 5. Auxiliary bit value is measured: if $|1\rangle_{\text{ind}}$ is observed we obtain $$ CA^{-1}|b\rangle_{\text{mem}}\propto |x\rangle ~~, $$ or otherwise repeat the algorithm. ## Preliminaries # ## Defining Matrix of a Specific Problem ```python theme={null} import numpy as np # Define matrix A A = np.array( [ [0.28, -0.01, 0.02, -0.1], [-0.01, 0.5, -0.22, -0.07], [0.02, -0.22, 0.43, -0.05], [-0.1, -0.07, -0.05, 0.42], ] ) # Define RHS vector b b = np.array([1, 2, 4, 3]) # Normalize vector b b = b / np.linalg.norm(b) print("A =", A, "\n") print("b =", b) # Verify if the matrix is symmetric and has eigenvalues in (0,1) if not np.allclose(A, A.T, rtol=1e-6, atol=1e-6): raise Exception("The matrix is not symmetric") w, v = np.linalg.eig(A) for lam in w: if lam < 0 or lam > 1: raise Exception("Eigenvalues are not in (0,1)") # Binary representation of eigenvalues (classically calculated) m = 32 # Precision of a binary representation, e.g. 32 binary digits sign = lambda num: "-" if num < 0 else "" # Calculate sign of a number binary = lambda fraction: str( np.binary_repr(int(np.abs(fraction) * 2 ** (m))).zfill(m) ).rstrip( "0" ) # Binary representation of a fraction print() print("Eigenvalues:") for eig in sorted(w): print(f"{sign(eig)}0.{binary(eig.real)} =~ {eig.real}") ``` **Output:** ``` A = [[ 0.28 -0.01 0.02 -0.1 ] [-0.01 0.5 -0.22 -0.07] [ 0.02 -0.22 0.43 -0.05] [-0.1 -0.07 -0.05 0.42]] b = [0.18257419 0.36514837 0.73029674 0.54772256] Eigenvalues: 0.00110001001110101110101000011 =~ 0.19230521285666338 0.01000000011111000101001111001011 =~ 0.2518970844513992 0.01111110111101001101011001011011 =~ 0.4959234212146716 0.10110000100110111001100111010101 =~ 0.6898742814772653 ``` # ## Define Hamiltonian We next represent the Hamiltonian into the Pauli operators. This will allow evaluating $U=e^{i 2 \pi A}$, which is utilized in the Quantum Phase Estimation (QPE) stage and consequently, in the associated classiq function code block. Having the matrix or the Hamiltonian defined, the unitary operator can be created with the [`qpe_flexible`](https://github.com/Classiq/classiq-library/blob/main/functions/qmod_library_reference/classiq_open_library/qpe/qpe.ipynb) function by passing in the `unitary_with_power` argument a callable function that applies a unitary operation raised to a given power. The built-in function `matrix_to_pauli_operator` encodes the matrix $A$ into a sum of Pauli strings, which then allows to encode the unitary matrix $U$ with a product formula (Suzuki-Trotter). This is a classical pre-processing that can be achieved by various decomposition methods, for example, you can compare with method described in \[[2](#paulidecomposition)]. The number of qubits is stored in the variable `n`. ```python theme={null} from classiq import * hamiltonian = matrix_to_pauli_operator(A) n = hamiltonian.num_qubits print(f"Hamiltonian: {hamiltonian}") print("\nNumber of qubits for matrix representation =", n) ``` **Output:** ``` Hamiltonian: 0.4075*Pauli.I(0)*Pauli.I(1) + -0.05249999999999999*Pauli.Z(0)*Pauli.I(1) + -0.017499999999999988*Pauli.I(0)*Pauli.Z(1) + -0.057499999999999996*Pauli.Z(0)*Pauli.Z(1) + -0.030000000000000002*Pauli.X(0)*Pauli.I(1) + 0.02*Pauli.X(0)*Pauli.Z(1) + -0.025*Pauli.I(0)*Pauli.X(1) + 0.045000000000000005*Pauli.Z(0)*Pauli.X(1) + -0.16*Pauli.X(0)*Pauli.X(1) + -0.06*Pauli.Y(0)*Pauli.Y(1) Number of qubits for matrix representation = 2 ``` ## How to Build the Algorithm with Classiq # ## The Quantum Part The first step of the HHL algorithm involves loading the elements of the normalized RHS vector $\vec{b}$, into a quantum variable: $$ |0\rangle_{\text{mem}} \xrightarrow[{\rm SP}]{} \sum^{2^n-1}_{i=0}b_i|i\rangle_{\text{mem}}~~, $$ where $|i\rangle$ are the states in the computational basis. Below, we define the quantum function `load_b` using the built-in function [`prepare_amplitudes`](https://github.com/Classiq/classiq-library/blob/main/functions/qmod_library_reference/qmod_core_library/prepare_state_and_amplitudes/prepare_state_and_amplitudes.ipynb). This function loads the $2^n$ elements of `amplitudes`, which correspond to the entries of the vector $\vec{b}$, into the amplitudes of the state in the output variable `memory`. ```python theme={null} @qfunc def load_b(amplitudes: CArray[CReal], memory: Output[QArray]) -> None: prepare_amplitudes(amplitudes, 0.0, memory) ``` # ### Define HHL Quantum Function The `hhl` function performs steps 2-4 of the algorithm. Steps 2 and 4 of the algorithm are achieved by applying the [`within_apply`](https://docs.classiq.io/latest/qmod-reference/language-reference/statements/within-apply/) construct, where the `within` part corresponds to a flexible quantum phase estimation [`qpe_flexible`](https://github.com/Classiq/classiq-library/blob/main/functions/qmod_library_reference/classiq_open_library/qpe/qpe.ipynb). The `apply` involves the controlled rotations of step 3, which are performed with `assign_amplitude_table` function. The built in function `qpe_flexible` is given a function `unitary_with_power` which indicates how the powers $U^{2^k}$ are performed within the phase estimation procedure. In the following implementation, the powers are performed by Hamiltonian evolution, utilizing the `hamiltonian_evolution_with_power` function. The normalization coefficient $C$ ensures the amplitudes are normalized. Since eigenvalues of $A$ are normalized to be between $1/\kappa$ and $1$, the eigenvalues of the inverse matrix are between $\kappa =1/\lambda_\text{min}$ and $1$. However, the amplitudes cannot surpass unity; we therefore choose $C$ as a lower bound of the minimum eigenvalue $C = 1/2^{m}$. **Arguments:** * `unitary_with_power`: A callable function that applies a unitary operation raised to a power `k`. * `precision`: An integer representing the precision of the phase estimation process, (number of `estimator` qubits $=m$). * `memory`: An array representing the initial quantum state $|b\rangle$ on which the matrix inversion is to be performed. * `estimator`: Stores the estimation of the phase. * `indicator`: An auxiliary qubit that stores the result of the inversion. ```python theme={null} from classiq.qmod.symbolic import floor, log # Parameters for the initial state preparation amplitudes = b.tolist() # Parameters for the QPE precision = 4 @qfunc def hhl( rhs_vector: CArray[CReal], precision: int, hamiltonian_evolution_with_power: QCallable[CInt, QArray], memory: Output[QArray], estimator: Output[QNum], indicator: Output[QBit], ): # Allocate a quantum number for the phase with given precision allocate(precision, UNSIGNED, precision, estimator) # Prepare initial state load_b(amplitudes=amplitudes, memory=memory) # Allocate indicator allocate(indicator) # Perform quantum phase estimation and eigenvalue inversion within a quantum operation within_apply( lambda: qpe_flexible( unitary_with_power=lambda k: hamiltonian_evolution_with_power(k, memory), phase=estimator, ), lambda: assign_amplitude_table( lookup_table( lambda p: 0 if p == 0 else (1 / 2**estimator.size) / p, estimator ), estimator, indicator, ), ) ``` # ### Set Execution Preferences We prepare for the quantum program execution stage by providing execution details and constructing a representation of HHL model from a function defined in Quantum Modelling Language [Qmod](https://docs.classiq.io/latest/qmod-reference/). The resulting circuit is executed on a statevector simulator. For other available Classiq simulated backends see [Execution on Classiq simulators](https://docs.classiq.io/latest/user-guide/execution/cloud-providers/classiq-backends/). To define this part of the model, we set the backend preferences by placing `ClassiqBackendPreferences` with the backend name `SIMULATOR_STATEVECTOR` into the `ExecutionPreferences`. ```python theme={null} backend_preferences = ClassiqBackendPreferences( backend_name=ClassiqSimulatorBackendNames.SIMULATOR_STATEVECTOR ) # Construct a representation of HHL model def hhl_model(main, backend_preferences): qmod_hhl = create_model( main, execution_preferences=ExecutionPreferences( num_shots=1, backend_preferences=backend_preferences ), ) return qmod_hhl ``` # ## The Classical Postprocess After running the circuit a given number of times and collecting measurement results from the indicator and phase qubits we need to postprocess the data in order to find solution. Measurement values from circuit execution are denoted here by `res_hhl`. # ### Postselect Results If all eigenvalues are $m$-estimated (are represented by $m$ binary digits), then the uncomputation by inverse-QPE creates the state: $$ \sum^{2^n-1}_{j=0}{\beta_j |u_j\rangle_{\text{mem}} |0\rangle_{\text{est}} \left(\sqrt{1-\frac{C^2}{\tilde{\lambda}^2_j}}|0\rangle_\text{ind} + \frac{C}{\tilde{\lambda}_j}|1\rangle_\text{ind} \right)} $$ If there is an error in step 3 and at least one eigenvalue is not $n$-estimated, then after performing QPE$^\dagger$, the `estimator` variable does not reset to $|0\rangle_{\text{est}}$. This will further reduce the success probability of In the general case, we postselect only states that return $1$ for `indicator` variable. In this tutorial, we will add another postselection condition on `estimator` variable, to return $|0\rangle_{\text{est}}$, in order to increase the accuracy of the solution result. **Note:** Postselection can improve HHL algorithm results at the cost of more executions. In the future, when two-way quantum computing is utilized, postselection might be replaced by adjoint-state preparation, as envisioned by Jarek Duda \[[4](#duda)]. The `quantum_solution` function defines a run over all the relevant strings holding the solution. The solution vector will be inserted into the variable `qsol`, after normalizing with $C=1/2^m$. After the calculation we divide by $C$ to obtain the elements of $\vec{x}$. ```python theme={null} def quantum_solution(res_hhl, size, precision): df = res_hhl.dataframe qsol = np.zeros(2**size, dtype=complex) # Filter only the successful states. filtered_st = df[ (df.indicator == 1) & (df.estimator_var == 0) & (np.abs(df.amplitude) > 1e-12) ] # Allocate values qsol[filtered_st.res] = filtered_st.amplitude / (1 / 2**precision) return np.round(qsol, 5) ``` # ### Compare Classical and Quantum Solutions To compare between the quantum and classical solutions we first need to isolate the global phase of the quantum solution. Let the outcome state be $e^{i \theta}| x \rangle$, satisfying $A (e^{i \theta} | x \rangle) = e^{i\theta} |b \rangle$. We can isolate the phase by taking the scalar product of $A e^{i \theta} | x \rangle$ with $| b \rangle $, i.e., $\langle b| A e^{i\theta}x\rangle = e^{i\theta}$. We then evaluate $\theta$, rotate by the appropriate angle, and take the real part. Note that only observables of the form $\langle x| M | x\rangle$ (which are independent of the global phase) are accessible by a quantum experiment. The global phase is purely a numerical inconvenience which arises in the comparison the enteries classical solution with the quantum amplitudes and has no physical meaning in the implementation of the algorithm on actual hardware. ```python theme={null} import matplotlib.pyplot as plt def quantum_solution_normalization(A, b, res_hhl, precision, disp=True): # Classical solution sol_classical = np.linalg.solve(A, b) if disp: print("Classical Solution: ", sol_classical) # Quantum solution with postselection size = len(b).bit_length() - 1 qsol = quantum_solution(res_hhl, size, precision) if disp: print("Quantum Solution: ", np.abs(qsol) / np.linalg.norm(qsol)) res = np.dot(A, qsol) global_phase = np.angle(np.vdot(res, b)) # Correct global phase and taking the real part qsol_corrected = np.real(np.exp(1j * global_phase) * qsol) return sol_classical, qsol_corrected def show_solutions(A, b, res_hhl, precision, check=True, disp=True): # Classical solution and preprocessed quantum solution sol_classical, qsol_corrected = quantum_solution_normalization( A, b, res_hhl, QPE_SIZE, disp=disp ) # Verify is there is no functional error, which might come from changing endianness in Model or Execution if ( np.linalg.norm(sol_classical - qsol_corrected) / np.linalg.norm(sol_classical) > 0.1 and check ): raise Exception( "The HHL solution is too far from the classical one, please verify your algorithm" ) if disp: print("Corrected Quantum Solution: ", qsol_corrected) # Fidelity state_classical = sol_classical / np.linalg.norm(sol_classical) state_corrected = qsol_corrected / np.linalg.norm(qsol_corrected) fidelity = np.abs(np.dot(state_classical, state_corrected)) ** 2 print() print("Fidelity: ", f"{np.round(fidelity * 100,2)} %") if disp: plt.plot(sol_classical, "bo", label="Classical") plt.plot(qsol_corrected, "ro", label="HHL") plt.legend() plt.xlabel("$i$") plt.ylabel("$x_i$") plt.show() ``` In the upcoming sections, we will present two different examples of implementing Hamiltonian dynamics by defining the unitary operator using two methods: exact and approximated. # ## Example: Implementation of $U$ with Exact Hamiltonian Simulation The phase estimation subroutine in the HHL algorithm involves a consecutive application of controlled powers of $U$ gates, i.e, conditional $C-U^{2^0}, C-U^{2^1},\dots, C-U^{2^{m-1}}$. In the present section, we implement these gates exactly and later compare the results to an approximate evaluation of these gates. While the implementation of exact Hamiltonian simulation provided here is not scalable for large systems due to the exponential growth in computational resources required, it serves as a valuable tool for debugging and studying small quantum systems. For sparse matrices, there is an efficient quantum algorithm for Hamiltonian simulation, see Ref. \[7] and the technical notes section below. ```python theme={null} from typing import List import scipy from classiq import Output, create_model, power, prepare_amplitudes, synthesize, unitary from classiq.qmod.symbolic import floor, log # Parameters for the initial state preparation amplitudes = b.tolist() # Parameters for the QPE QPE_SIZE = 4 @qfunc def main( res: Output[QNum], estimator_var: Output[QNum], indicator: Output[QBit], ) -> None: hhl( rhs_vector=amplitudes, precision=QPE_SIZE, hamiltonian_evolution_with_power=lambda pw, target: power( pw, lambda: unitary( elements=scipy.linalg.expm(2 * np.pi * 1j * A).tolist(), target=target ), ), memory=res, estimator=estimator_var, indicator=indicator, ) ``` ```python theme={null} # Construct HHL model qmod_hhl_exact = hhl_model(main, backend_preferences) ``` # ### Synthesizing the Model (Exact) ```python theme={null} qprog_hhl_exact = synthesize(qmod_hhl_exact) show(qprog_hhl_exact) print("Circuit depth = ", qprog_hhl_exact.transpiled_circuit.depth) print("Circuit CX count = ", qprog_hhl_exact.transpiled_circuit.count_ops["cx"]) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/35vWPvUx6tRKY33jr60Ba510qB4 Circuit depth = 457 Circuit CX count = 286 ``` image.png # ### Statevector Simulation (Exact) Execute the quantum program. Recall that you chose a statevector simulator because you want the exact solution. ```python theme={null} from classiq.execution import ExecutionDetails res_hhl_exact = execute(qprog_hhl_exact).result_value() ``` # ### Results (Exact) ```python theme={null} precision = QPE_SIZE show_solutions(A, b, res_hhl_exact, precision, check=False) ``` **Output:** ``` Classical Solution: [1.3814374 2.50585064 3.19890483 2.43147877] Quantum Solution: [0.2840631 0.50843613 0.64683772 0.4923432 ] Corrected Quantum Solution: [1.43559 2.56952 3.26897 2.48819] Fidelity: 100.0 % ``` output # ## Example: Approximated Hamiltonian Evolution (Suzuki-Trotter) The approximated Hamiltonian simulation is capable of handling larger and more complex systems, making it more suitable for real-world applications where exact solutions are computationally prohibitive. For the QPE we are going to use Classiq's `suzuki-trotter` function of order one for Hamiltonian evolution $e^{-i H t}$ \[[3](#trotter)]. This function is an approximated one, where its `repetitions` parameter controls its error. For a QPE algorithm with estimator size $m$ a series of controlled-unitaries $U^{2^k}$ with $0 \leq k \leq n-1$ are applied, for each of which we would like to pass a different `repetitions` parameter depth (to keep a roughly same error, the repetitions for approximating $U=e^{2\pi i 2^k A }$ is expected to be $\sim 2^k$ times the repetitions of $U=e^{2\pi i A }$). In Classiq this can be done by working with a [`qpe_flexible`](https://github.com/Classiq/classiq-library/blob/main/functions/qmod_library_reference/classiq_open_library/qpe/qpe.ipynb), and passing a "rule" for how to exponentiate each step within the QPE in `repetitions` parameter. ```python theme={null} from classiq.qmod.symbolic import ceiling, log def suzuki_trotter1_with_power_logic( hamiltonian: SparsePauliOp, pw: CInt, r0: CInt, reps_scaling_factor: CReal, evolution_coefficient: CReal, target: QArray, ) -> None: suzuki_trotter( hamiltonian, evolution_coefficient=evolution_coefficient * pw, order=1, repetitions=r0 * ceiling(reps_scaling_factor ** (log(pw, 2))), qbv=target, ) ``` The parameters `R0` and `REPS_SCALING_FACTOR` dictate the number of repititions in the Suzuki-Trotter approximation. The specific number of repititions depend on the Hamiltonian, and therefore, were chosen by trial and error. For other examples, one would need to use different values for these parameters, please compare with specific example in [Flexible QPE tutorial](https://github.com/Classiq/classiq-library/blob/main/tutorials/advanced_tutorials/high_level_modeling_flexible_qpe/high_level_modeling_flexible_qpe.ipynb). The relevant literature that discusses the errors of product formulas is available in Ref. \[[5](#trotter-error)]. ```python theme={null} from classiq.qmod.symbolic import floor # parameters for the amplitude preparation amplitudes = b.tolist() # parameters for the QPE QPE_SIZE = 4 R0 = 4 REPS_SCALING_FACTOR = 1.8 @qfunc def main( res: Output[QNum], estimator_var: Output[QNum], indicator: Output[QBit], ) -> None: hhl( rhs_vector=amplitudes, precision=QPE_SIZE, hamiltonian_evolution_with_power=lambda pw, target: suzuki_trotter1_with_power_logic( hamiltonian=hamiltonian, pw=pw, evolution_coefficient=-2 * np.pi, r0=R0, reps_scaling_factor=REPS_SCALING_FACTOR, target=target, ), memory=res, estimator=estimator_var, indicator=indicator, ) # Define model qmod_hhl_trotter = hhl_model(main, backend_preferences) ``` ```python theme={null} # Synthesize qprog_hhl_trotter = synthesize(qmod_hhl_trotter) show(qprog_hhl_trotter) # Show circuit print("Circuit depth = ", qprog_hhl_trotter.transpiled_circuit.depth) print("Circuit CX count = ", qprog_hhl_trotter.transpiled_circuit.count_ops["cx"]) print() # Show results res_hhl_trotter = execute(qprog_hhl_trotter).result_value() show_solutions(A, b, res_hhl_trotter, precision) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/35vWRtuij83BLaWB0i53Jg0XqOQ Circuit depth = 5069 Circuit CX count = 2966 Classical Solution: [1.3814374 2.50585064 3.19890483 2.43147877] Quantum Solution: [0.28600641 0.5107414 0.64756839 0.48785114] Corrected Quantum Solution: [1.43423072 2.56933184 3.25933604 2.45268588] Fidelity: 99.99 % ``` output We explored the HHL algorithm for solving linear systems using exact and approximated Hamiltonian simulations. The exact method, with a smaller circuit depth, is computationally less intensive but lacks scalability. In contrast, the approximated method, with a greater circuit depth, offers flexibility and can handle larger, more complex systems. This trade-off underscores the importance of selecting the appropriate method based on the problem's size and complexity. ## Technical Notes * The condition on the sparsity of $A$ arises from the following result, Ref. \[7]: If $A$ is a $2^n \times 2^n$ Hermitian $s$-spase matrix then one can implement an Hamiltonian simulation, $e^{-iA t}$ up to error $\epsilon$ in the spectral norm with $$ O\left( st||A||_{\infty} + \frac{1/\epsilon}{\log \log (1/\epsilon)}\right) $$ queries to $A$ and a total number of gates which is larger by a factor of $n$. Here, the infinity norm corresponds to the largest entry of $A$. * Before measurement we obtain the state $$ C|A^{-1}b\rangle_{\text{mem}} |0\rangle_{\text{est}}|1\rangle_{\text{ind}} +|\text{garbage}\rangle|0 \rangle_{\text{ind}}~~. $$ Naively, one would require of order $1/C=O(2^n)$ measurements to obtain a sample of the entries of $|x\rangle$. However, utilizing quantum amplitude amplification one can obtain a quadratic speedup, sampling the state with an order of only $O(2^{n/2})$ measurements. * We can replace the sparsity condition on $A$ by any restriction allowing efficient Hamiltonian simulation. * The complexity of standard HHL algorithm is $O(\log n \kappa^2 s/\epsilon)$. The factor $1/\epsilon$ has been improved by Childs et al. in \[[8](#childs)] to $\log(\kappa/\epsilon)$, and Ambainis introduced a generalization of amplitude amplification, which allows reducing the quadratic $\kappa$ dependence to linear \[[9](#ambainis)]. ## Generalizations The use case treated above is a canonical one, assuming the following properties: * The RHS vector $\vec{b}$ is normalized. * The matrix $A$ is an Hermitian one. * The matrix $A$ is of size $2^n\times 2^n $. * The eigenvalues of $A$ are in the range $(0,1)$. However, any general problem that does not follow these conditions can be resolved as follows: 1. Normalize $\vec{b}$ and return the normalization factor 2. Symmetrize the problem as follows: $$ \begin{pmatrix} 0 & A^T \\ A & 0 \end{pmatrix} \begin{pmatrix} \vec{b} \\ 0 \end{pmatrix} = \begin{pmatrix} 0 \\ \vec{x} \end{pmatrix}. $$ **Note:** This requires only a single additional qubit. 3. Complete the matrix dimension to the closest $2^n$ with an identity matrix and the vector $\vec{b}$ will be completed with zeros. $$ \begin{pmatrix} A & 0 \\ 0 & I \end{pmatrix} \begin{pmatrix} \vec{b} \\ 0 \end{pmatrix} = \begin{pmatrix} \vec{x} \\ 0 \end{pmatrix}~~. $$ 4. If the eigenvalues of $A$ are in the range $[-w_{\min},w_{\max}]$ you can employ transformations to the exponentiated matrix that enters into the Hamiltonian simulation, and then undo them for extracting the results: $$ \tilde{A}=\frac{A+w_{\min}I}{w_{\min}+w_{\max}}~~. $$ The eigenvalues of this matrix lie in the interval $[0,1)$, and are related to the eigenvalues of the original matrix via $$ \lambda = (w_{\min}+w_{\max})\tilde{\lambda}-w_{\min}, $$ with $\tilde{\lambda}$ being an eigenvalue of $\tilde{A}$ resulting from the QPE algorithm. This relation between eigenvalues is utilized in the pre and post-processing of the results or directly in the amplitude loading procedure. ## References \[1]: [Harrow, A. W., Hassidim, A., & Lloyd, S. (2009). Quantum algorithm for linear systems of equations. Physical review letters, 103(15), 150502](https://journals.aps.org/prl/abstract/10.1103/PhysRevLett.103.150502). \[2]: [Hantzko, L., Binkowski, L., & Gupta, S. (2024). Tensorized Pauli decomposition algorithm. Physica Scripta, 99(8), 085128.](https://arxiv.org/abs/2310.13421). \[3]: [Hatano, N., & Suzuki, M. (2005). Finding exponential product formulas of higher orders. In Quantum annealing and other optimization methods (pp. 37-68)](https://arxiv.org/abs/math-ph/0506007). \[4]: [Duda, J. (2023). Two-way quantum computers adding CPT analog of state preparation. arXiv preprint arXiv:2308.13522.](https://arxiv.org/abs/2308.13522). \[5]: [Childs, A. M., Su, Y., Tran, M. C., Wiebe, N., & Zhu, S. (2021). Theory of trotter error with commutator scaling. Physical Review X, 11(1), 011020.](https://arxiv.org/abs/2308.13522). \[6]: [Schuld, Maria, and Francesco Petruccione. (2018). Supervised learning with quantum computers. Quantum science and technology 17](https://link.springer.com/book/10.1007/978-3-319-96424-9) \[7]: [Low, Guang Hao, and Isaac L. Chuang. (2017). Optimal Hamiltonian simulation by quantum signal processing. Physical review letters 118.1 010501.](https://arxiv.org/abs/1606.02685) \[8]: [Childs, A. M., Kothari, R., & Somma, R. D. (2017). Quantum algorithm for systems of linear equations with exponentially improved dependence on precision. SIAM Journal on Computing, 46(6), 1920-1950.](https://arxiv.org/abs/1511.02306) \[9]: [Ambainis, A. (2010). Variable time amplitude amplification and a faster quantum algorithm for solving systems of linear equations.](https://arxiv.org/abs/1010.4458) # Matrix Inversion with Quantum Singular Value Transform (QSVT) Source: https://docs.classiq.io/explore/algorithms/quantum_linear_solvers/qsvt_matrix_inversion/qsvt_matrix_inversion Open this notebook in GitHub to run it yourself > **Quantum Matrix Inversion (or Quantum Linear Solver) via QSVT** provides a framework for solving linear systems exponentially faster than known classical methods under certain conditions [\[1\]](#ref-grand). The algorithm employs a polynomial transformation of singular values to block-encode the inverse of a matrix, which can then be applied on a prepared quantum state to solve a linear equation. Given an efficient routine to embed the classical matrix as a quantum function (block-encoding), this algorithm gives a clean and optimal way to implement matrix inversion compared to other quantum methods. Quantum linear solvers based on block-encoding have many applications, e.g., in [Plasma Physics](https://github.com/Classiq/classiq-library/blob/main/applications/plasma/vlasov_ampere/vlasov_ampere.ipynb) and [Fluid Dynamics](https://github.com/Classiq/classiq-library/blob/main/applications/CFD/QLS_for_hybrid_solvers/qls_qsvt.ipynb). > > * **Input:** An $N \times N$ matrix $A$ with condition number $\kappa$ (the ratio between the maximal and minimal singular values), and a vector $|b\rangle$ prepared as a quantum state. > * **Promise:** The matrix $A$ is well-conditioned (invertible, with bounded spectrum) and can be efficiently block-encoded in a unitary $U_A$ (see technical note at the end of this demo). > * **Output:** A quantum state proportional to $|x\rangle = A^{-1} |b\rangle$, encoding the solution of the linear system $A x = b$. > > **Complexity:** Using QSVT, matrix inversion requires $O(\kappa \log(\kappa / \epsilon))$ queries of the block-encoding unitary, where $\epsilon$ is the precision parameter. Typically, an efficient block-encoding scales as $\mathrm{poly}(\log N)$, resulting in an overall $\tilde{O}(\kappa \,\mathrm{polylog}(N, 1 / \epsilon))$ complexity for the QSVT linear solver. This improves exponentially in $\epsilon$ upon the original Harrow-Hassidim-Lloyd ([HHL](https://github.com/Classiq/classiq-library/blob/main/algorithms/quantum_linear_solvers/hhl/hhl.ipynb)) algorithm, and exponentially in $N$ compared to classical linear solvers, while matching known lower bounds for quantum linear solvers \[2]. > > (As with all quantum linear solvers, extracting the full solution state $|x\rangle$ removes the speedup; the advantage arises when only global properties such as expectation values, overlaps, or samples are required.) > > *** > > **Keywords:** Linear systems, Quantum Singular Value Transformation (QSVT), Block encoding, Matrix inversion, Exponential speedup, Oracle/Query complexity. In this demo we will restrict ourselves to matrices of size $N\times N$, with $N=2^n$. A generalization to arbitrary $N$ can be done by completing the matrix into a larger one, or modifying the QSVT routine to an arbitrary $N$. We emphasize that compared to the basic HHL algorithm, the QSVT approach *is not restricted to Hermitian matrices*. The QSVT algorithm is based on the idea of block-encoding a matrix into a larger unitary: $$ U_{(A,s)} = \begin{pmatrix} A/s & * \\ * & * \end{pmatrix}, \tag{1} $$ where $s$ is some scaling factor, which is sometimes necessary for this unitarity completion. Given an access to $U_{(A,s)}$, matrix inversion with QSVT implements a block-encoding for the inverse of $A$ $$ U_{(A^{-1},s')} = \begin{pmatrix} A^{-1}/s' & * \\ * & * \end{pmatrix}. $$ Subsequently, we can use this unitary to solve a linear system, $A|x\rangle = |b\rangle$, by applying it on an initially prepared state $|b\rangle_n|0\rangle_{\rm block}$ (the state $|0\rangle_{\rm block}$ identifies the block in which the matrix is encoded). Up to post-selection we have: $$ U_{(A^{-1},s')} |b\rangle_n|0\rangle_{\rm block} = \frac{A^{-1}}{s'}|b\rangle_n|0\rangle_{\rm block} + \text{ garbage}. $$ In more detail, Singular Value Decomposition (SVD) of a matrix $A$ is defined by $$ A = W\Sigma V^\dagger, $$ where $W$ and $V$ are unitary matrices, and $\Sigma$ is a diagonal matrix that contains the singular values of $A$ (The square root of the eigenvalues of $A A^{\dagger}$). A singular value polynomial transform refers to $$ \mathrm{Poly}^{\rm (SV)}(A) = \left\{\begin{array}{l l} W\mathrm{Poly}(\Sigma) V^\dagger, & \text{ for odd polynomial} \\ V\mathrm{Poly}(\Sigma) V^\dagger, & \text{ for even polynomial} \end{array} \right . \,\,, $$ where $\mathrm{Poly(\sigma)}$ is a polynomial with a well-defined parity. The QSVT routine allows to block-encode such singular value polynomial transforms, based on the Quantum Signal Processing (QSP) approach. For the case of matrix inversion, we have the identities, $$ \begin{aligned} A^\dagger &=V^\dagger \Sigma W,\\ A^{-1} &= V^\dagger \Sigma^{-1}W, \end{aligned} $$ from which we can deduce that: **Applying a QSVT of an odd polynomial that approximates $\mathrm{Poly(\sigma)}\sim 1/\sigma$, on the block-encoding of $A^{\dagger}$, gives the matrix inversion of $A$.** Below we demonstrate how to implement a quantum linear solver for a specific problem. As we will see, the input of the algorithm is block-encoding of the matrix $A$, and its effective condition number $(\sigma_{\min}/s)^{-1}$, with $\sigma_{\min}$ being the minimal singular eigenvalue. Using the `qsvt_inversion` function from the open-library and some classical auxiliary functions from our `qsp` application, allow to easily implement the algorithm, given those two inputs. ## Example: Block-Encoded Matrix in a Random Unitary ```python theme={null} !pip install -qq "classiq[qsp]" ``` # ## Setting an Example We start by defining a specific problem: a matrix, its block encoding, and its condition number. We take a very simple usecase: define a random unitary of size $2^{n+1}$, taking its upper $2^n\times 2^n$ block as the block-encoded matrix $A$ that we want to invert. ```python theme={null} import numpy as np import scipy # the size of the unitary which block encodes A REG_SIZE = 3 def get_random_unitary(num_qubits, seed=4): np.random.seed(seed) X = np.random.rand(2**num_qubits, 2**num_qubits) U, s, V = np.linalg.svd(X) return U @ V.T U_a = get_random_unitary(REG_SIZE) A_dim = int(U_a.shape[0] / 2) A = U_a[:A_dim, :A_dim] print(A) ``` **Output:** ``` [[-0.05338002 -0.36103662 -0.54016489 -0.39026125] [-0.33304121 0.10648228 0.37346704 -0.33977916] [ 0.4167817 -0.75180519 0.17593867 0.20944773] [ 0.26891079 -0.05333795 -0.32668787 -0.33602829]] ``` Make sure the singular values for A are smaller than 1, and verify that $U_{A}$ is indeed unitary: ```python theme={null} assert not (np.linalg.svd(A)[1] > 1).sum() assert np.allclose(U_a @ U_a.T, np.eye(U_a.shape[0]), rtol=1e-5, atol=1e-6) ``` # ## Block-Encoding Next, we define the quantum functions required for the algortihm. It is instructive to work with a `QSrtuct` to represent the quantum variable for the block-encoding. ```python theme={null} from classiq import * class QsvtState(QStruct): state: QNum[REG_SIZE - 1] block: QBit ``` In addition, we define two quantum functions that we shall pass to the QSVT routine: a function that reflects about `block` at state zero, which identify the block in which the matrix is encoded, and a function that block encodes our matrix. (Recall that for inversion we shall block-encode $A^{\dagger}$. However, the `qsvt_inversion` function handles this internally, by passing the inverse $U^{\dagger}_{A}=U_{A^\dagger}$). ```python theme={null} # Define QSVT projector @qfunc def projector(be: QsvtState, res: QBit): res ^= be.block == 0 @qfunc def be_qfunc(qsvt_state: QsvtState): unitary(elements=U_a.tolist(), target=qsvt_state) ``` As explained below, we need to evaluate an effective condition number, $\kappa=s/\min(\sigma_i)$ (this parameter is bounded by the actual condition number $\max{(\sigma_i)}/\min(\sigma_i)$; see technical note at the end of this notebook). In real life, this value is not known and needs to be approximated by some prior knowledge of the problem, or through some classical method. In our small example we will just explicitly compute the SVD decomposition of $A$: ```python theme={null} svd = np.linalg.svd(A)[1] # In our simple usecase s=1 s = 1 kappa = s / min(svd) print(f"The effective condition number is {kappa}") ``` **Output:** ``` The effective condition number is 3.4598628384708703 ``` # ## Finding QSVT Angles for the Approximated Inverse Function We need to find a polynomial approximation to the $\frac{1}{x}$ function. Notice that the function is exploding as $x$ goes to 0, which breaks the rules for the existence of QSP polynomial, which must be bounded by 1 in $[-1,1]$. However, we can limit the polynomial to approximate only at the relevant range $[\sigma_{\min}/s, \sigma_{\max}/s]$, i.e., in the interval that contains the singular values of the block-encoded matrix. The target function we need to approximate is thus: $$ f(x) = \frac{1}{2}\frac{1}{\kappa x}, \qquad x\in [\sigma_{min}, \sigma_{max}], $$ where $\kappa=s/\sigma_{min}$ as defined below, and a scaling factor of 1/2 was added to get an easier convergence for the corresponding QSVT angles. We can see that $|f(x)|\leq 1$ in the relevant range. According to Ref. \[2], a degree of $\sim \kappa \log(\kappa/\epsilon)$ is sufficient to approximate $f(x)$ with an error $\epsilon$. Below, we use the `qsp_approximate` function to get the approximated polynomial. This function approximates $f(x)$ as a sum of Chebyshev polynomials, making sure the resulting polynomial is bounded by 1 in the *entire range* $[-1, 1]$. Subsequently, we call the `qsvt_phases` to get the corresponding QSVT phases. ```python theme={null} from classiq.applications.qsp import qsp_approximate, qsvt_phases EPS = 1e-8 degree = int(kappa * np.log(kappa / EPS)) # In case we provide an even degree and ask for an odd polynimial if degree % 2 == 0: degree += 1 SCALE = 0.5 def target_function(x): return SCALE * 1 / (kappa * x) pcoefs, opt_res = qsp_approximate( target_function, degree=degree, parity=1, interval=[min(svd), max(svd)], plot=True ) inversion_phases = qsvt_phases(pcoefs) ``` output The interpolating function returns the coefficients, as well as the approximated maximum error between the target function and the approximating polynomial within the interval. ```python theme={null} print( f"For the function {np.round(SCALE/kappa,5)}1/x, we approximate with an odd polynomial of degree {degree} with an error of {opt_res}" ) ``` **Output:** ``` For the function 0.144511/x, we approximate with an odd polynomial of degree 69 with an error of 4.707465528497323e-09 ``` # ## Solving a Linear Equation Next, we solve an instance of a linear equation $A\vec{x} = \vec{b}$. For this, we define some arbitrary $\vec{b}$ vector. ```python theme={null} b = np.arange(A_dim) b_norm = np.linalg.norm(b) b_normalized = (b / b_norm).tolist() print(b) ``` **Output:** ``` [0 1 2 3] ``` Now, we build the quantum model, preparing the initial state and then calling `qsvt_inversion` with the parameters defining our specific problem: ```python theme={null} @qfunc def main( qsvt_state: Output[QsvtState], qsvt_aux: Output[QBit], ) -> None: allocate(qsvt_aux) allocate(qsvt_state) inplace_prepare_amplitudes(b_normalized, 0, qsvt_state.state) qsvt_inversion( phase_seq=inversion_phases, block_encoding_cnot=lambda aux: projector(qsvt_state, aux), u=lambda: be_qfunc(qsvt_state), aux=qsvt_aux, ) ``` Let us synthesize, visualize, and execute. For the execution, we choose a statevector simulator, as we are considering a small problem for demonstrating the algorithm. In addition, later on we would like to verify our result against the expected classical one. ```python theme={null} qprog = synthesize(main) show(qprog) # Post-select qsvt_aux == 0 on the statevector simulator. # Note: filtering is possible for QBit and QNum, but not for QStruct. result = calculate_state_vector(qprog, filters={"qsvt_aux": 0}) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/35YjqMigetKATtz2myYGsDMwvie ``` image.png If our matrix $A$ is block-encoded with a block variable of size $m$, then the QSVT routine block encodes the inverse of $A$ on $m+1$ qubits, where the additional qubit is the `qsvt_aux`. Therefore, we need to post-select on `qsvt_state.block` and `qsvt_aux` being zero, if we want to obtain a state $\sim A^{-1}|b\rangle$. Note that the additional block qubit was already filtered as part of the execution call. ```python theme={null} df = result df_filtered = df[(df["qsvt_state.block"] == 0)].sort_values("qsvt_state.state") df_filtered ``` | | qsvt\_state.state | qsvt\_state.block | qsvt\_aux | amplitude | magnitude | phase | probability | bitstring | | - | ----------------- | ----------------- | --------- | ------------------- | --------- | ------ | ----------- | --------- | | 7 | 0 | 0 | 0 | -0.184047-0.225847j | 0.29 | -0.72π | 0.084880 | 0000 | | 5 | 1 | 0 | 0 | -0.013748-0.016870j | 0.02 | -0.72π | 0.000474 | 0010 | | 6 | 2 | 0 | 0 | -0.084568-0.103776j | 0.13 | -0.72π | 0.017921 | 0100 | | 0 | 3 | 0 | 0 | 0.154944+0.190135j | 0.25 | 0.28π | 0.060159 | 0110 | # ## Comparing to the Expected Result Finally, we verify our quantum solution, by comparing it to the expected classical one. ```python theme={null} expected_x = np.linalg.inv(A) @ b ``` The quantum routine returns, $$ |x\rangle = \frac{(\mathrm{scale})}{\kappa}\left(A/s\right)^{-1} \frac{\vec{b}}{|\vec{b}|} |0\rangle_{\rm block}|0\rangle_{\rm aux} + \text{grabage}, $$ after post-selection (statevector filtering) for the block, we have: $$ \vec{x} = \frac{(\mathrm{scale})}{\kappa}\left(A/s\right)^{-1} \frac{\vec{b}}{|\vec{b}|}, $$ where in our specific usecase we have $\mathrm{scale}=0.5$ and $s=1$. We can now collect all the prefactors and compare to the expected result. ```python theme={null} global_phase = np.angle(df_filtered.amplitude.iloc[0]) prefactor = b_norm * kappa / SCALE computed_x = prefactor * np.real(df_filtered.amplitude / np.exp(1j * global_phase)) ``` Let's compare it with the expected solution: *Comment: even after removing the global phase to ensure a real solution, the quantum solution can be obtained up to a sign.* ```python theme={null} import matplotlib.pyplot as plt plt.plot(expected_x, "o", label="classical") plt.plot(computed_x, ".", label="quantum") plt.xlabel(r"$i$", fontsize=16) plt.ylabel(r"$x_i$", fontsize=16) plt.legend(); ``` output ```python theme={null} assert ( min( np.linalg.norm(computed_x - expected_x), np.linalg.norm(-computed_x - expected_x), ) < 0.05 ) ``` ## References \[1]: [Martyn JM, Rossi ZM, Tan AK, Chuang IL. Grand unification of quantum algorithms. PRX Quantum 2, 040203. (2021).](https://journals.aps.org/prxquantum/abstract/10.1103/PRXQuantum.2.040203) \[2]: [A. M. Childs, R. Kothari, and R. D. Somma. Quantum algorithm for systems of linear equations with exponentially improved dependence on precision. SIAM Journal on Computing 46, 1920-1950 (2017).](https://arxiv.org/abs/1511.02306) ## A Note on Efficient Block-Encoding, Condition Number, and Complexity An efficient block-encoding of a matrix $A$ means that the resources to construct the unitary $U_{A,s}$, as well as the scaling factor $s$, scales poly-logarithmically with the matrix dimension. This technical note concerns the latter point. First, we note that an optimal value for the scaling factor $s$ is the maximal singular value $\sigma_{\max}$, namely $s\geq \sigma_{\max}$. This comes directly from the requirement that $||A/s||\leq 1$. For that reason, the effective condition number used throughout the demo, $\kappa = s/\sigma_{\min}$, is bounded from below by the actual condition number $\sigma_{\max}/\sigma_{\min}$. Next, note that if $s$ scales linearly with the problem size, then the exponential speedup is lost. This is because the polynomial degree then scales as $O(N)$. In addition, the amplitude of the encoded solution scales the same (although it also depends on the input vector $|b\rangle$), which requires an additional amplitude amplification routine that adds a factor of $O(\sqrt{N})$ to the overall resources. # Variational Quantum Linear Solver (VQLS) with Linear Combination of Unitaries (LCU) Block Encoding Source: https://docs.classiq.io/explore/algorithms/quantum_linear_solvers/vqls/vqls_with_lcu Open this notebook in GitHub to run it yourself The Variational Quantum Linear Solver (VQLS) is a quantum algorithm that harnesses the power of Variational Quantum Eigensolvers (VQE) to solve systems of linear equations efficiently: * **Input:** A matrix $\textbf{A}$ and a known vector $|\textbf{b}\rangle$. * **Output:** An approximation of a normalized solution $|x\rangle$ proportional to $|\textbf{x}\rangle$, satisfying the equation $\textbf{A} |\textbf{x}\rangle = |\textbf{b}\rangle$. *** While the output of VQLS mirrors that of the HHL Quantum Linear-Solving Algorithm, VQLS distinguishes itself by its ability to operate on Noisy Intermediate-Scale Quantum (NISQ) computers. In contrast, HHL necessitates more robust quantum hardware and a larger qubit count, despite offering a faster computation speedup. This tutorial covers an implementation example of a **Variational Quantum Linear Solver** \[[1](#vqls)] using block encoding. In particular, we use linear combinations of unitaries (LCUs) for the block encoding. As with all variational algorithms, the VQLS is a hybrid algorithm in which we apply a classical optimization on the results of a parametrized (ansatz) quantum program. ## Building the Algorithm with Classiq # ## Quantum Part: Variational Circuit Given a block encoding of the matrix A: $$ \begin{aligned} U = \begin{bmatrix} A & \cdot \\ \cdot & \cdot \end{bmatrix} \end{aligned} \tag{1} $$ we can prepare the state $$ |\Psi\rangle := A |x\rangle/\sqrt{\langle x |A^\dagger A |x\rangle}. $$ We can approximate the solution $|x\rangle$ with a variational quantum circuit, i.e., a unitary circuit $V$, depending on a finite number of classical real parameters $w = (w_0, w_1, \dots)$: $$ |x \rangle = V(w) |0\rangle. $$ Our objective is to address the task of preparing a quantum state $|x\rangle$ such that $A |x\rangle$ is proportional to $|b\rangle$; or, equivalently, ensuring that $$ |\Psi\rangle := \frac{A |x\rangle}{\sqrt{\langle x |A^\dagger A |x\rangle}} \approx |b\rangle. $$ The state $|b\rangle$ arises from a unitary operation $U_b$ applied to the ground state of $n$ qubits; i.e., $$ |b\rangle = U_b |0\rangle. $$ To maximize the overlap between the quantum states $|\Psi\rangle$ and $|b\rangle$, we optimize the parameters, defining a cost function: $$ C = 1- |\langle b | \Psi \rangle|^2. $$ At a high level, the above could be implemented as follows: We construct a quantum model as depicted in the figure below. When measuring the circuit in the computational basis, the probability of finding the system qubits in the ground state (given the ancillary qubits measured in their ground state) is $|\langle 0 | U_b^\dagger |\Psi \rangle|^2 = |\langle b | \Psi \rangle|^2.$ Screenshot 2024-09-22 at 18.11.21.png To block encode a Variational Quantum Linear Solver as explained above, we can define a high-level `block_encoding_vqls` function as follows: ```python theme={null} from typing import List import numpy as np from classiq import * ``` ```python theme={null} @qfunc def block_encoding_vqls( ansatz: QCallable, block_encoding: QCallable, prepare_b_state: QCallable, ) -> None: ansatz() block_encoding() invert(lambda: prepare_b_state()) ``` From here, we only need to define `ansatz`, `block_encoding`, and `prepare_b_state` to fit the specific example above. Now we are ready to build our model, synthesize it, execute it, and analyze the results. # ## Classical Part: Finding Optimal Parameters To estimate the overlap of the ground state with the post-selected state, we could directly make use of the measurement samples. However, since we want to optimize the cost function, it is useful to express everything in terms of expectation values through Bayes' theorem: $$ |\langle b | \Psi \rangle|^2= P( \mathrm{sys}=\mathrm{ground}\,|\, \mathrm{anc} = \mathrm{ground}) = P( \mathrm{all}=\mathrm{ground})/P( \mathrm{anc}=\mathrm{ground}) $$ To evaluate the conditional probability from the above equation, we construct the following utility function to operate on the measurement results: To variationally solve our linear problem, we define the cost function $C = 1- |\langle b | \Psi \rangle|^2$ that we are going to minimize. As explained above, we express it in terms of expectation values through Bayes' theorem. We define a classical function that gets the quantum program, minimizes the cost function using the COBYLA optimizer, and returns the optimal parameters. ```python theme={null} import random import matplotlib.pyplot as plt from scipy.optimize import minimize class VqlsOptimizer: def __init__( self, qprog, ansatz_param_count, ansatz_var_name, aux_var_name, exe_prefs=None ): self.qprog = qprog self.ansatz_param_count = ansatz_param_count self.ansatz_var_name = ansatz_var_name self.aux_var_name = aux_var_name if exe_prefs is None: self.es = ExecutionSession(qprog) else: self.es = ExecutionSession(qprog, exe_prefs) self.intermediate = {} def get_cond_prop(self, res): aux_prob_0 = 0 all_prob_0 = 0 for s in res: if s.state[self.aux_var_name] == 0: aux_prob_0 += s.shots if s.state[self.ansatz_var_name] == 0: all_prob_0 = s.shots return all_prob_0 / aux_prob_0 def my_cost(self, params): results = self.es.sample(params) return 1 - self.get_cond_prop( results.parsed_counts_of_outputs([self.ansatz_var_name, self.aux_var_name]) ) def f(self, x): cost = self.my_cost( {"params_" + str(k): x[k] for k in range(self.ansatz_param_count)} ) self.intermediate[tuple(x)] = cost return cost def optimize(self): random.seed(1000) self._out = out = minimize( self.f, x0=[ float(random.randint(0, 3000)) / 1000 for i in range(0, self.ansatz_param_count) ], method="COBYLA", options={"maxiter": 2000}, ) print(out) self._out_f = out_f = [out["x"][0 : self.ansatz_param_count]] print(out_f) plt.plot( [l for l in range(len(self.intermediate))], list(self.intermediate.values()) ) return { "params_" + str(k): list(self.intermediate.keys())[-1][k] for k in range(self.ansatz_param_count) } ``` *** Once the optimal variational weights `w` are found, we can generate the quantum state $|x\rangle$. By measuring $|x\rangle$ in the computational basis we can estimate the probability of each basis state. *** ## Example Using LCU Block Encoding We treat a specific example based on a system of three qubits: $$ \begin{aligned} A &= c_0 A_0 + c_1 A_1 + c_2 A_2 = \ 0.55 \mathbb{I} \ + \ 0.225 Z_1 \ + \ 0.225 Z_2 \\ \\ |b\rangle &= U_b |0 \rangle = H_0 H_1 H_2 |0\rangle, \end{aligned} $$ where $Z_j, X_j, H_j$ represent the Pauli $Z$, Pauli $X$, and Hadamard gates applied to the qubit with index $j$. To block encode the matrix A we use the LCU method. This can be done with the `lcu_paulis` library function. Note that this function can get a unnormalized Pauli operator, thus we calculate the normalization factor for the post-process analysis. The LCU quantum circuit looks as follows: Screenshot 2024-05-19 at 18.56.22.png ```python theme={null} pauli_terms_structs = ( 0.55 * Pauli.I(0) + 0.225 * Pauli.I(0) * Pauli.Z(1) * Pauli.I(2) + 0.225 * Pauli.I(0) * Pauli.I(1) * Pauli.Z(2) ) normalization = sum([p.coefficient for p in pauli_terms_structs.terms]) num_system_qubits = pauli_terms_structs.num_qubits num_ancila_qubits = (len(pauli_terms_structs.terms) - 1).bit_length() ansatz_param_count = 9 ``` # ## Fixed Hardware Ansatz Let's consider our ansatz $V(w)$, such that $$ |x\rangle = V(w) |0\rangle. $$ This allows us to "search" the state space by varying a set of parameters, $w$. The ansatz that we use for this three-qubit system implementation takes in nine parameters as defined in the `apply_fixed_3_qubit_system_ansatz` function: ```python theme={null} @qfunc def apply_ry_on_all(params: CArray[CReal], io: QArray[QBit]): repeat(count=io.len, iteration=lambda index: RY(params[index], io[index])) @qfunc def apply_fixed_3_qubit_system_ansatz( angles: CArray[CReal], system_qubits: QArray[QBit] ): apply_ry_on_all([angles[0], angles[1], angles[2]], system_qubits) repeat( count=(system_qubits.len - 1), iteration=lambda index: CZ(system_qubits[0], system_qubits[index + 1]), ) CZ(system_qubits[1], system_qubits[2]) apply_ry_on_all([angles[3], angles[4], angles[5]], system_qubits) repeat( count=(system_qubits.len - 1), iteration=lambda index: CZ( system_qubits[system_qubits.len - 1], system_qubits[index] ), ) CZ(system_qubits[1], system_qubits[0]) apply_ry_on_all([angles[6], angles[7], angles[8]], system_qubits) ``` To view our ansatz implementation we create a model and view the synthesis result: ```python theme={null} @qfunc def main( params: CArray[CReal, ansatz_param_count], system_qubits: Output[QArray[QBit]] ): allocate(3, system_qubits) apply_fixed_3_qubit_system_ansatz(params, system_qubits) qprog_1 = synthesize(main) show(qprog_1) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/32pWjH6oUwpzhB9OZFVtHUvCXBA ``` Screenshot 2025-07-31 at 16.03.50.png This is called a **fixed hardware ansatz** in that the configuration of quantum gates remains the same for each run of the circuit, and all that changes are the parameters. Unlike the QAOA ansatz, it is not composed solely of Trotterized Hamiltonians. The applications of $Ry$ gates allow us to search the state space, while the $CZ$ gates create "interference" between the different qubit states. # ## Running the VQLS Now, we can define the main function: we call `block_encoding_vqls` with the arguments of our specific example. ```python theme={null} @qfunc def main( params: CArray[CReal, ansatz_param_count], ancillary_qubits: Output[QNum[num_ancila_qubits]], system_qubits: Output[QNum[num_system_qubits]], ): allocate(ancillary_qubits) allocate(system_qubits) block_encoding_vqls( ansatz=lambda: apply_fixed_3_qubit_system_ansatz(params, system_qubits), block_encoding=lambda: lcu_pauli( operator=pauli_terms_structs, data=system_qubits, block=ancillary_qubits ), prepare_b_state=lambda: apply_to_all(H, system_qubits), ) ``` Constructing the model, synthesizing, and executing on the Classiq simulator: ```python theme={null} qprog_2 = synthesize(main) show(qprog_2) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/32pWkF7CG67svJGIRWJMZ0lqwcS ``` Screenshot 2025-07-31 at 16.07.09.png We run the classical optimizer to get the optimal parameters: ```python theme={null} backend_preferences = ClassiqBackendPreferences(backend_name="simulator_statevector") execution_preferences = ExecutionPreferences( num_shots=204800, backend_preferences=backend_preferences ) optimizer = VqlsOptimizer( qprog_2, ansatz_param_count, "system_qubits", "ancillary_qubits", execution_preferences, ) optimal_params = optimizer.optimize() ``` **Output:** ``` message: Optimization terminated successfully. success: True status: 1 fun: 0.09572291736048333 x: [ 2.183e+00 3.097e+00 9.850e-01 2.482e+00 3.129e+00 6.554e-01 1.201e+00 2.307e+00 9.622e-01] nfev: 100 maxcv: 0.0 [array([2.18263061, 3.09658041, 0.98498817, 2.48189263, 3.12912539, 0.65539631, 1.20116677, 2.30650636, 0.96216622])] ``` output # ## Measuring the Quantum Solution Finally, we can apply the optimal parameters to measure the quantum results for $\vec{x}$: ```python theme={null} @qfunc def main(io: Output[QNum[num_system_qubits]]): allocate(io) apply_fixed_3_qubit_system_ansatz(list(optimal_params.values()), io) qprog_3 = synthesize(main) ``` ```python theme={null} with ExecutionSession(qprog_3, execution_preferences) as es: results = es.sample() df = results.dataframe ``` ```python theme={null} amplitudes = np.zeros(2**num_system_qubits).astype(complex) amplitudes[df.io] = df.amplitude # Preprocessed quantum solution: we know the solution is real, and that the last point is positive global_phase = np.angle(amplitudes[-1]) amplitudes = np.real(amplitudes / np.exp(1j * global_phase)) if ( amplitudes[-1] < 0 ): # we can extract the solution up to a sign, align with the expected amplitudes *= -1 print(amplitudes) ``` **Output:** ``` [0.10545817 0.09824632 0.23525783 0.25226615 0.199245 0.30080137 0.46216235 0.71865688] ``` ```python theme={null} probabilities = amplitudes**2 ``` # ## Comparing to the Classical Solution Since the specific problem considered in this tutorial has a small size, we can also solve it in a classical way and then compare the results with our quantum solution. We use the explicit matrix representation in terms of numerical NumPy arrays. Classical calculation: ```python theme={null} A_num = pauli_operator_to_matrix(pauli_terms_structs) / normalization b = np.ones(8) / np.sqrt(8) ``` Calculating the classical $\vec{x}$ that solves the equation: ```python theme={null} A_inv = np.linalg.inv(A_num) x = np.dot(A_inv, b) classical_probs = np.real((x / np.linalg.norm(x))) ** 2 classical_probs ``` **Output:** ``` array([0.00464634, 0.00464634, 0.0153598 , 0.0153598 , 0.0153598 , 0.0153598 , 0.46463405, 0.46463405]) ``` To compare the classical to the quantum results we compute the post-processing by applying $A$ to our optimal vector $|\psi\rangle_o$, normalizing it, then calculating the inner product squared of this vector and the solution vector, $|b\rangle$! We can put this all into code as follows: ```python theme={null} print( "overlap =", (b.dot(A_num.dot(amplitudes) / (np.linalg.norm(A_num.dot(amplitudes))))) ** 2, ) ``` **Output:** ``` overlap = (0.9040151946512618+0j) ``` ```python theme={null} import matplotlib.pyplot as plt fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(7, 4)) ax1.bar(np.arange(0, 2**num_system_qubits), classical_probs, color="blue") ax1.set_xlim(-0.5, 2**num_system_qubits - 0.5) ax1.set_xlabel("Vector space basis") ax1.set_title("Classical probabilities") ax2.bar(np.arange(0, 2**num_system_qubits), probabilities, color="gold") ax2.set_xlim(-0.5, 2**num_system_qubits - 0.5) ax2.set_xlabel("Hilbert space basis") ax2.set_title("Quantum probabilities") plt.show() ``` output The classical cost function basically agrees with the algorithm result. ## References \[1]: [Bravo-Prieto et al.,Variational Quantum Linear Solver, 2020.](https://arxiv.org/pdf/1909.05820.pdf) \[2]: Robin Kothari. "Efficient algorithms in quantum query complexity." PhD thesis, University of Waterloo, 2014. # Quantum Phase Estimation for Solving Matrix Eigenvalues Source: https://docs.classiq.io/explore/algorithms/quantum_phase_estimation/qpe_for_matrix/qpe_for_matrix Open this notebook in GitHub to run it yourself Quantum Phase Estimation (QPE) is a key algorithm in quantum computing, allowing you to estimate the eigenphase of a unitary matirx (or the eigenvalue of a Hermitian matrix). The algorithm is designed such that given the inputs of a Hermitian matrix $M$ and an eigenvector ${|\psi\rangle}$, the output obtained is $\theta$, where $U{|\psi\rangle} = e^{2\pi i\theta}{|\psi\rangle} , U = e^{2\pi iM}$. By measuring the accumulated phase, the QPE algorithm calculates the eigenvalues relating to the chosen input vector. To read more about the QPE algorithm and its method for achieving the phase, refer to \[[1](#nc)]. Generally speaking, when the eigenvectors of the matrix are not known in advance yet the eigenvalues are sought, you can choose a random vector ${|v\rangle}$ for the algorithm's initial state. Some eigenvalues will be found as the vector can be described in the matrix's basis, defined by the set of eigenvalues of $M$: \{$\psi_i$}. Generally, any vector can be written as a superposition of any basis set, thus ${|v\rangle} = \sum_i a_i{|\psi_i\rangle}$ and $U{|v\rangle} = \sum_i a_i e^{2\pi i\theta_i}{|\psi_i\rangle}$. Using execution with enough shots, you can obtain this set of $\theta_i$; i.e., a subset of the matrix's eigenvalues. **This tutorial presents a generic usage of the QPE algorithm:** 1. Define classical and quantum functions for constructing the algorithm. 2. Take a specific example, a matrix and some initial state. 3. Choose a resolution for the solution. 4. Find the related eigenvalues using QPE and analyze the results. ```python theme={null} import math import numpy as np from classiq import * ``` ## Classical Functions # ## Matrix Rescaling As QPE obtains a phase in the form $e^{2\pi i\theta}$, there is meaning only for $\theta \in [-1/2,1/2)$. However, the matrix $M$ can have any eigenvalue. To fix this discrepancy, the values of the matrix are rescaled. If $\theta \in [\lambda_{min}, \lambda_{max}]$ you can use a normalization function to map those values into $[-1/2, 1/2)$. Perform the normalization procedure by: a. Defining the function `get_normalization` that finds a rough estimation for the eigenvalue with the largest absolute value. This yields a value $\bar{\lambda} = {\max}\left\{|\lambda_\max|,|\lambda_\min|\right\}$, such that you can assume $\theta \in [-\bar{\lambda}, \bar{\lambda}]$. b. Defining the function `normalize_hamiltonian` that normalizes by $2\bar{\lambda}$ the Hamiltonian, and thus its eigenvalues, such that the evaluated span is then $\theta\in [-1/2, 1/2]$. (Note that in this case $\theta=\pm1/2$ correspond to the same phase, however, this is an edge case where the normalized Hamiltonian has exactly those two eigenvalues. To avoid this, you can apply an extra factor of $(1-1/2^m)$, where $m$ is the size of the phase variable). ```python theme={null} def get_normalization(hamiltonian): """ Bounds the eigenvalue with the maximal absolute value by summing all the absolute values of the Pauli coefficients """ abs_coeff = np.abs([term.coefficient for term in hamiltonian.terms]) return 2 * sum(abs_coeff) def normalize_hamiltonian(hamiltonian, normalization_coeff): return hamiltonian * (1 / normalization_coeff) ``` # ## QPE Precision Estimator For QPE algorithms, the precision is set by phase register size $m$, such that the resolution is $1/{2^m}$. If the matrix needs to be normalized, the resolution will be distorted. In the case of normalization, the span of results for the QPE stretches between the lowest and highest possible phase, thus the resolution is mapped to $\sim 1/{((\lambda_{max}-\lambda_{min})*2^m)}$. ```python theme={null} def get_qpe_precision(hamiltonian, desired_resolution): nqpe = math.log2(get_normalization(hamiltonian) / desired_resolution) return math.ceil(nqpe) ``` ## Quantum Functions Use the built-in `qpe_flexible` function, which allows you to prescribe the "telescopic" expansion of the powered unitary via the `unitary_with_power` "QCallable" (see [Flexible QPE tutorial](https://docs.classiq.io/latest/tutorials/tutorials/high-level-modeling-flexible-qpe/high-level-modeling-flexible-qpe/)). Define two examples for the powered unitary: # ## Approximated Evolution: A First Order Suzuki Trotter with Power-Logic Wrap the Trotter-Suzuki function of order 2 with a "power-logic" for the repetition as a function of its power. ```python theme={null} from classiq.qmod.symbolic import ceiling, log def suzuki_trotter2_with_power_logic( hamiltonian: SparsePauliOp, pw: CInt, r0: CInt, reps_scaling_factor: CReal, evolution_coefficient: CReal, target: QArray, ) -> None: suzuki_trotter( hamiltonian, evolution_coefficient=evolution_coefficient * pw, order=2, repetitions=ceiling(r0 * reps_scaling_factor ** (log(pw, 2))), qbv=target, ) ``` ## Setting a Specific Example # ## Set the Matrix Define the matrix to submit. This can be any Hermitian matrix with size $2^n$ by $2^n$ with $n$ a positive integer. Throughout the code this matrix is given in the variable `M`. ```python theme={null} M = np.array( [ [0.38891555, 0.23315811, 0.21499372, 0.06119557], [0.23315811, 0.44435328, 0.25197881, -0.13087919], [0.21499372, 0.25197881, 0.44116509, -0.01961855], [0.06119557, -0.13087919, -0.01961855, 0.32556608], ] ) M = (M + M.transpose()) / 2 ``` # ## Set the Initial Vector Choose the vector that will be defined later as the initial condition for the run. There are two options: (1) define a random initial vector, or (2) choose some eigenvector of the matrix. For the demonstration, proceed with the first option: ```python theme={null} np.random.seed(8) int_vec = np.random.rand(np.shape(M)[0]) print("Your initial state is", int_vec) ``` **Output:** ``` Your initial state is [0.8734294 0.96854066 0.86919454 0.53085569] ``` # ## Preparing the Matrix for QPE ```python theme={null} hamiltonian = matrix_to_pauli_operator(M) ``` ```python theme={null} N = hamiltonian.num_qubits print("number of qubits: ", N) ``` **Output:** ``` number of qubits: 2 ``` # ### Choose the Algorithm's Precision Choose the precision using the `n_qpe` parameter or set your desired resolution. ```python theme={null} desired_resolution = 0.02 n_qpe = get_qpe_precision(hamiltonian, desired_resolution) print("number of qubits for QPE is", n_qpe) ``` **Output:** ``` number of qubits for QPE is 7 ``` # ### Normalize the Matrix Transform the matrix to ensure its eigenvalues are between $-1/2$ to $1/2$. The QPE procedure is performed on the new normalized matrix. After the phases are obtained, gather the original phases of the pre-normalized matrix by performing opposite steps to this normalization procedure. ```python theme={null} normalization_coeff = get_normalization(hamiltonian) new_hamiltonian = normalize_hamiltonian(hamiltonian, normalization_coeff) Mnew = M / normalization_coeff ``` ## Building the Quantum Model Create a quantum model of the QPE algorithm using the Classiq platform with your desired constraints and preferences. Run two different models, with a unitary implementation, which is exact; or with an approximated prodcut formula. Synthesize the models and compare the resulting quantum programs. The exact version is a non-scalable approach, but a convenient one for small usecases. ```python theme={null} import scipy my_amp = ( int_vec / np.linalg.norm(int_vec) ).tolist() # amplitude is given by the eignevector @qfunc def main(phase_result: Output[QNum[n_qpe, SIGNED, n_qpe]]) -> None: state = QArray() prepare_amplitudes(my_amp, 0.0, state) allocate(phase_result) qpe_flexible( unitary_with_power=lambda pw: power( pw, lambda: unitary( elements=scipy.linalg.expm(1j * 2 * np.pi * Mnew).tolist(), target=state ), ), phase=phase_result, ) drop(state) qprog_exact = synthesize(main) @qfunc def main(phase_result: Output[QNum[n_qpe, SIGNED, n_qpe]]) -> None: state = QArray() prepare_amplitudes(my_amp, 0.0, state) allocate(phase_result) qpe_flexible( unitary_with_power=lambda pw: suzuki_trotter2_with_power_logic( hamiltonian=new_hamiltonian, pw=pw, r0=2, reps_scaling_factor=1.5, evolution_coefficient=-2 * np.pi, target=state, ), phase=phase_result, ) drop(state) qprog_approx = synthesize(main) ``` ```python theme={null} print( f"Depth for QPE with exact Hamiltonian evolution: {qprog_exact.transpiled_circuit.depth}" ) print( f"Depth for QPE with approximated Hamiltonian evolution: {qprog_approx.transpiled_circuit.depth}" ) ``` **Output:** ``` Depth for QPE with exact Hamiltonian evolution: 384 Depth for QPE with approximated Hamiltonian evolution: 6276 ``` As expected, for this small usecase the exact evolution yeilds better results. Display it with the analyzer: ```python theme={null} show(qprog_exact) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/32pZjVUnGvgAFwr5Bve9w7vKFxB ``` ## Measuring and Analyzing the Results Execute the quantum programs and analyze the results, in comparison to the expected classical ones. # ## Execute the Quantum Program Send the quantum programs for execution by a chosen backend and print the raw results. ```python theme={null} num_shots = 10000 execution_prefs = ExecutionPreferences(num_shots=num_shots) with ExecutionSession(qprog_exact, execution_prefs) as es: result_exact = es.sample() with ExecutionSession(qprog_approx, execution_prefs) as es: result_approx = es.sample() df_exact = result_exact.dataframe df_approx = result_approx.dataframe ``` Choose the number of eigenvalues to extract from the pool of results. The `number_of_solutions` value determines how many results are analyzed. Get the solution by multiplying back the normalization coefficient. ```python theme={null} number_of_solutions = 2 # number of phases sought ``` ```python theme={null} solution_exact = list(df_exact.phase_result[:number_of_solutions] * normalization_coeff) solution_approx = list( df_approx.phase_result[:number_of_solutions] * normalization_coeff ) ``` These are the results, including the error contributed from the resolution (the number of qubits participating in the QPE): ```python theme={null} print( f"Your {number_of_solutions} solutions with the highest probability are:\n {solution_exact} (exact) \n {solution_approx} (approx)" ) energy_resolution = (1 / (2**n_qpe)) * 2 * normalization_coeff print("the resolution of results is", energy_resolution) print("=" * 15 + " exact " + "=" * 15) for sol in solution_exact: print( "the solutions are between", sol - energy_resolution, "and", sol + energy_resolution, ) print("=" * 15 + " approximated " + "=" * 15) for sol in solution_approx: print( "the solutions are between", sol - energy_resolution, "and", sol + energy_resolution, ) ``` **Output:** ``` Your 2 solutions with the highest probability are: [0.8992759912499999, 0.40375656749999994] (exact) [0.8809234199999999, 0.9176285624999999] (approx) the resolution of results is 0.036705142499999996 =============== exact =============== the solutions are between 0.8625708487499999 and 0.9359811337499999 the solutions are between 0.36705142499999993 and 0.44046170999999995 =============== approximated =============== the solutions are between 0.8442182774999999 and 0.9176285624999999 the solutions are between 0.8809234199999999 and 0.9543337049999999 ``` # ## Plot the Solution's Histogram and Compare to Classical Results ```python theme={null} w, v = np.linalg.eig(M) print("the eigenvalues are", w) ``` **Output:** ``` the eigenvalues are [0.9 0.4 0.1 0.2] ``` ```python theme={null} import matplotlib.patches as patches import matplotlib.pyplot as plt width = energy_resolution for eig in w: # Add gray rectangle rect = patches.Rectangle( (eig - width / 2, 0), width, df_exact.probability[0], color="gray", alpha=0.3 ) plt.gca().add_patch(rect) # Plot vertical line plt.plot([eig, eig], [0, df_exact.probability[0]], "--r") (exact,) = plt.plot( df_exact.phase_result * normalization_coeff, df_exact.probability, "o", label="exact exponentiation", ) (approx,) = plt.plot( df_approx.phase_result * normalization_coeff, df_approx.probability, "o", label="approximated exponentiation", ) plt.legend(handles=[exact, approx]) plt.xlabel(r"$\lambda$") plt.ylabel(r"$P(\lambda)$"); ``` output ## References \[1]: \[Michael A. Nielsen and Isaac L. Chuang. 2 11. Quantum Computation and Quantum Information: 10th Anniversary Edition, Cambridge University Press, New York, NY, USA. ]\([https://archive.org/details/QuantumComputationAndQuantumInformation10thAnniversaryEdition](https://archive.org/details/QuantumComputationAndQuantumInformation10thAnniversaryEdition)) # Qubitization Based Quantum Phase Estimation (QPE) for Solving Molecular Energies Source: https://docs.classiq.io/explore/algorithms/quantum_phase_estimation/qpe_with_qubitization/qpe_for_molecule_with_qubitization Open this notebook in GitHub to run it yourself This notebook is based on Ref. \[[1](#walk-qpe)]. Given an efficient block-encoding for a Hamiltonian, this algorithm preforms an efficient Quantum Phase Estimation. The core quantum function of this model uses almost all Qmod built-in operations: `control`, `power`, `within_apply`, and `invert`. The algorithm assumes we have the block-encoding of a matrix $H$ $$ U_{(s,m)-H} =\begin{pmatrix} H/s & * \\ * & * \end{pmatrix}, $$ with $m$ being the size of the block variable and $s$ some scaling factor. Given this quantum function, we can define the following unitary (usually called the Szegedy quantum walk operator \[[2](#ref-szegedy)]): $$ W\equiv \Pi_{|0\rangle_m} U_{(s,m)-H}, $$ where $\Pi_{|0\rangle_m}$ is a reflection operator about the block state 0. The spectrum of the walk operator has a nice relation to the spectrum of the block-encoded Hamiltonian \[[3](#ref-lin)]: $ \text\{eigenvalues: \} e^\{\pm i \arccos(\lambda/s)\}, \text\{ with eigenvectors: \} |\varphi^\{\pm\}_\{\lambda\}\rangle \equiv \frac\{1\}\{\sqrt\{2\}\}\left(|v_\{\lambda\}\rangle |0\rangle_m \pm i|\perp_\{\lambda\}\rangle\right), \quad (1)$ where $|v_\lambda\rangle$ is an eigenstate of the Hamiltonian $H$ with an eigenvalue $\lambda$. Namely, the *eigenphases* of $H$ are related by some nonlinear function ($\arccos$) to the *eigenvalues* of $H$. **The algorithm works under the assumption that the block-encoding unitary itself is also Hermitian, that is, $U_{(s,m)-H}$ is Unitary and Hermitian.** ## Preliminaries We start with defining some utility functions that are not implemented as part of Classiq, and might be included in the future. These functions are used for the specific block-encoding used in this notebook. ```python theme={null} !pip install -qq "classiq[chemistry]" ``` ```python theme={null} import matplotlib.pyplot as plt import numpy as np from classiq import * from classiq.applications.chemistry.op_utils import qubit_op_to_qmod ``` ```python theme={null} from sympy import fwht from classiq.open_library.functions.state_preparation import apply_phase_table def get_graycode(size, i) -> int: if i == 2**size: return get_graycode(size, 0) return i ^ (i >> 1) def get_graycode_angles_wh(size, angles): transformed_angles = fwht(np.array(angles) / 2**size) return [transformed_angles[get_graycode(size, j)] for j in range(2**size)] def get_graycode_ctrls(size): return [ (get_graycode(size, i) ^ get_graycode(size, i + 1)).bit_length() - 1 for i in range(2**size) ] @qfunc def multiplex_ra(a_y: float, a_z: float, angles: list[float], qba: QArray, ind: QBit): assert a_y**2 + a_z**2 == 1 # TODO support general (0,a_y,a_z) rotation assert ( a_z == 1.0 or a_y == 1.0 ), "currently only strict y or z rotations are supported" size = max(1, (len(angles) - 1).bit_length()) extended_angles = angles + [0] * (2**size - len(angles)) transformed_angles = get_graycode_angles_wh(size, extended_angles) controllers = get_graycode_ctrls(size) for k in range(2**size): if a_z == 0.0: RY(transformed_angles[k], ind) else: RZ(transformed_angles[k], ind) skip_control(lambda: CX(qba[controllers[k]], ind)) @qfunc def lcu_paulis_graycode(terms: list[SparsePauliTerm], data: QArray, block: QArray): n_qubits = data.len n_terms = len(terms) table_z = np.zeros([n_qubits, n_terms]) table_y = np.zeros([n_qubits, n_terms]) probs = [abs(term.coefficient) for term in terms] + [0.0] * (2**block.len - n_terms) hamiltonian_coeffs = np.angle([term.coefficient for term in terms]).tolist() + [ 0.0 ] * (2**block.len - n_terms) accumulated_phase = np.zeros(2**block.len).tolist() for k in range(n_terms): for pauli in terms[k].paulis: if pauli.pauli == Pauli.Z: table_z[pauli.index, k] = -np.pi accumulated_phase[k] += np.pi / 2 elif pauli.pauli == Pauli.Y: table_y[pauli.index, k] = -np.pi accumulated_phase[k] += np.pi / 2 elif pauli.pauli == Pauli.X: table_z[pauli.index, k] = -np.pi table_y[pauli.index, k] = np.pi accumulated_phase[k] += np.pi / 2 def select_graycode(block: QArray, data: QArray): for i in range(n_qubits): multiplex_ra(0, 1, table_z[i, :], block, data[i]) multiplex_ra(1, 0, table_y[i, :], block, data[i]) apply_phase_table( [p1 - p2 for p1, p2 in zip(hamiltonian_coeffs, accumulated_phase)], block ) within_apply( lambda: inplace_prepare_state(probs, 0.0, block), lambda: select_graycode(block, data), ) ``` ## Defining a Specific Usecase: A Molecule and Its Block-Encoding Hamiltonian Function ```python theme={null} molecule_H2_geometry = [("H", (0.0, 0.0, 0)), ("H", (0.0, 0.0, 0.735))] ``` ```python theme={null} from openfermion.chem import MolecularData from openfermionpyscf import run_pyscf basis = "sto-3g" # Basis set multiplicity = 1 # Singlet state S=0 charge = 0 # Neutral molecule molecule = MolecularData(molecule_H2_geometry, basis, multiplicity, charge) molecule = run_pyscf( molecule, run_fci=True, # relevant for small, classically solvable problems ) ``` ```python theme={null} from classiq.applications.chemistry.mapping import FermionToQubitMapper from classiq.applications.chemistry.problems import FermionHamiltonianProblem # Define a Hamiltonian in an active space problem = FermionHamiltonianProblem.from_molecule(molecule=molecule) mapper = FermionToQubitMapper() qubit_hamiltonian = mapper.map(problem.fermion_hamiltonian) print("Your Hamiltonian is", qubit_hamiltonian, sep="\n") ``` **Output:** ``` Your Hamiltonian is (-0.09057898608834769+0j) [] + (0.04523279994605784+0j) [X0 X1 X2 X3] + (0.04523279994605784+0j) [X0 X1 Y2 Y3] + (0.04523279994605784+0j) [Y0 Y1 X2 X3] + (0.04523279994605784+0j) [Y0 Y1 Y2 Y3] + (0.17218393261915538+0j) [Z0] + (0.12091263261776627+0j) [Z0 Z1] + (0.16892753870087907+0j) [Z0 Z2] + (0.1661454325638241+0j) [Z0 Z3] + (-0.2257534922240238+0j) [Z1] + (0.1661454325638241+0j) [Z1 Z2] + (0.17464343068300453+0j) [Z1 Z3] + (0.1721839326191554+0j) [Z2] + (0.12091263261776627+0j) [Z2 Z3] + (-0.22575349222402386+0j) [Z3] ``` Finally, we calculate the ground state energy as a reference solution to the quantum solver ```python theme={null} classical_sol = molecule.fci_energy print(f"Expected energy: {classical_sol} Ha") ``` **Output:** ``` Expected energy: -1.1373060357533995 Ha ``` ```python theme={null} mol_hamiltonian = qubit_op_to_qmod(qubit_hamiltonian) num_qubits = mol_hamiltonian.num_qubits be_scaling = sum(np.abs(term.coefficient) for term in mol_hamiltonian.terms) normalized_mol_hamiltonian = mol_hamiltonian * (1 / be_scaling) ``` ```python theme={null} data_size = normalized_mol_hamiltonian.num_qubits num_terms = len(normalized_mol_hamiltonian.terms) block_size = (num_terms - 1).bit_length() if num_terms != 1 else 1 ``` ```python theme={null} print(f"The block size is {block_size}, and the scaling factor s is : {be_scaling}") ``` **Output:** ``` The block size is 4, and the scaling factor s is : 1.9850721353060015 ``` ```python theme={null} class BlockEncodedState(QStruct): data: QNum[data_size] block: QNum[block_size] @qfunc def be_hamiltonian(state: BlockEncodedState): lcu_paulis_graycode(normalized_mol_hamiltonian.terms, state.data, state.block) ``` ## Defining a Walk Operator We use the `reflect_around_zero` function from Classiq's open library to define a walk operator function with the declaration below. This function implements $I-2|0\rangle\langle 0|$, so we must insert a minus phase (This can be done by adding a minus sign using the `phase` function). ```python theme={null} from classiq.qmod.symbolic import pi @qfunc def my_reflect_about_zero(qba: QNum): control(qba == 0, lambda: phase(pi)) phase(pi) @qfunc def walk_operator( be_qfunc: QCallable[BlockEncodedState], state: BlockEncodedState ) -> None: be_qfunc(state) my_reflect_about_zero(state.block) ``` We define a classical function that takes the eigenphases of the Walk operator, and returns the (scaled) eigenvalues of $H$, according to equation (1) above. ```python theme={null} def post_process_walk_phases(w_eigphase, be_scaling): return np.cos(2 * np.pi * w_eigphase) * be_scaling ``` We also define a utility function for ploting the results: ```python theme={null} def get_qpe_walk_result(df, be_scaling, to_plot=True): filtered_block_res = df[df["block"] == 0].copy() block_prob = df.loc[df["block"] == 0, "probability"].sum() print(f"probability to measure the block variable at state zero: {block_prob}") filtered_block_res["post_processed_phases"] = post_process_walk_phases( filtered_block_res["phase_var"], be_scaling ) max_prob_energy = filtered_block_res.loc[ filtered_block_res["probability"].idxmax(), "post_processed_phases" ] print(f"\nEnergy with maximal probability: {max_prob_energy} Ha") if to_plot: plt.plot( filtered_block_res["post_processed_phases"], filtered_block_res["probability"], "o", ) plt.xlabel("Energy (Ha)", fontsize=16) plt.ylabel("P(Energy)", fontsize=16) plt.tick_params(axis="both", labelsize=16) plt.title("Energy Histogram from QPE") return filtered_block_res ``` ## Setting Initial State and QPE Size ```python theme={null} from classiq.applications.chemistry.hartree_fock import get_hf_state # We take the Hartree Fock state hf_state = get_hf_state(problem, mapper) QPE_SIZE = 5 ``` ## A Naive QPE Before going to the optimized implementation, designing a specific QPE that operates on a walk operator, we start with a naive QPE implementation. ```python theme={null} @qfunc def main( block: Output[QNum[block_size]], phase_var: Output[QNum[QPE_SIZE, SIGNED, QPE_SIZE]], ) -> None: data = QNum(size=data_size) prepare_basis_state(hf_state, data) allocate(block) allocate(phase_var) qpe(lambda: walk_operator(be_hamiltonian, [data, block]), phase_var) drop(data) qprog_qpe_naive = synthesize(main) show(qprog_qpe_naive) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/38VyDBs3xcSfRT772EkonhMeqwA ``` ```python theme={null} results_qpe_naive = execute(qprog_qpe_naive).get_sample_result() ``` ```python theme={null} df_qpe_naive = results_qpe_naive.dataframe print(f"Classical solution:, {classical_sol} Ha") post_processed_df_qpe_naive = get_qpe_walk_result( df_qpe_naive, be_scaling, to_plot=True ) ``` **Output:** ``` Classical solution:, -1.1373060357533995 Ha probability to measure the block variable at state zero: 0.50732421875 Energy with maximal probability: -1.102846988772674 Ha ``` output ## Optimized QPE Design for the Walk Operator We construct the model design according to Ref. \[[1](#walk-qpe)], shown in the figure below, with $R_{\mathcal{L}}$ being the reflection around zero operation, and $\chi_m$ is taken as the usual Hadamard transform for the phase initialization: Screenshot 2025-12-12 at 16.20.32.png ```python theme={null} @qfunc def qpe_on_walk( block_encoding: QCallable[BlockEncodedState], state: BlockEncodedState, phase_var: QArray, ) -> None: hadamard_transform(phase_var) control( phase_var[0], lambda: walk_operator(block_encoding, state), ) repeat( count=phase_var.len - 1, iteration=lambda i: within_apply( lambda: control( phase_var[i + 1] == 0, lambda: control(state.block == 0, lambda: phase(pi)), ), lambda: power(2**i, lambda: walk_operator(block_encoding, state)), ), ) invert(lambda: qft(phase_var)) ``` We now construct the model, synthesize it, and retrieve the ground state of the molecule ```python theme={null} @qfunc def main( block: Output[QNum[block_size]], phase_var: Output[QNum[QPE_SIZE, SIGNED, QPE_SIZE]], ) -> None: data = QNum(size=data_size) prepare_basis_state(hf_state, data) allocate(block) allocate(phase_var) qpe_on_walk(block_encoding=be_hamiltonian, state=[data, block], phase_var=phase_var) drop(data) qprog_qpe_walk = synthesize(main) show(qprog_qpe_walk) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/38VyLEYiNJ1PXIRn5XDbpa2V6MN ``` ```python theme={null} results_qpe_walk = execute(qprog_qpe_walk).get_sample_result() ``` ```python theme={null} df_qpe_optimized = results_qpe_walk.dataframe print(f"Classical solution:, {classical_sol} Ha") post_processed_df_qpe_optimized = get_qpe_walk_result( df_qpe_optimized, be_scaling, to_plot=True ) ``` **Output:** ``` Classical solution:, -1.1373060357533995 Ha probability to measure the block variable at state zero: 0.47119140625 Energy with maximal probability: -1.102846988772674 Ha ``` output ```python theme={null} print("For the naive QPE on the walk operator:") print(f"depth: {qprog_qpe_naive.transpiled_circuit.depth}") print("=" * 40) print("For the optimized QPE on the walk operator:") print(f"depth: {qprog_qpe_walk.transpiled_circuit.depth}") print("=" * 40) ``` **Output:** ``` For the naive QPE on the walk operator: depth: 19686 ======================================== For the optimized QPE on the walk operator: depth: 4724 ======================================== ``` ```python theme={null} gs_naive = post_processed_df_qpe_naive.loc[ post_processed_df_qpe_naive["probability"].idxmax(), "post_processed_phases" ] gs_optimized = post_processed_df_qpe_optimized.loc[ post_processed_df_qpe_optimized["probability"].idxmax(), "post_processed_phases" ] ``` If $\Delta \lambda_W = 1/2^{\rm QPE-SIZE}$, then $$ \Delta \lambda = \Delta(\cos(2\pi\lambda_W))s = 2\pi s \sin(2\pi\lambda_W) \Delta\lambda_W = 2\pi s \sin(2\pi 2^{-\rm QPE-SIZE}) 2^{-\rm QPE-SIZE} $$ ```python theme={null} qpe_err = ( 2 * np.pi * be_scaling * np.sin(2 * np.pi * (1 / 2**QPE_SIZE)) * (1 / 2**QPE_SIZE) ) ``` ```python theme={null} print(f"QPE error: {qpe_err}") ``` **Output:** ``` QPE error: 0.07603996508423008 ``` ```python theme={null} assert np.abs(gs_naive - classical_sol) < qpe_err assert np.abs(gs_optimized - classical_sol) < qpe_err ``` ## References \[1] R. Babbush et. al., Encoding Electronic Spectra in Quantum Circuits with Linear T Complexity. [https://arxiv.org/abs/1805.03662 (2018)](https://arxiv.org/abs/1805.03662) \[2] Szegedy, M. , Quantum speed-up of Markov chain based algorithms. [In 5th Annual IEEE Symposium on Foundations of Computer Science (2004)](https://ieeexplore.ieee.org/abstract/document/1366222) \[3] Lin, L., Lecture notes on quantum algorithms for scientific computation. [arXiv:2201.08309 quant-ph (2022)](https://arxiv.org/abs/2201.08309) # Generalized Quantum Signal Processing (GQSP) Source: https://docs.classiq.io/explore/algorithms/quantum_primitives/gqsp/gqsp Open this notebook in GitHub to run it yourself > **Generalized Quantum Signal Processing (GQSP)** is a quantum algorithmic primitive that extends standard QSP, allowing one to block-encode arbitrary polynomials of unitary operations [\[1\]](#ref-gqsp). It removes the "realness" and parity restrictions on achievable polynomials that appear in QSP [\[2\]](#ref-grand) and provides a simple recipe for constructing complex polynomials $P$ with $|P|\le 1$ on the unit circle. Moreover, it can be used to implement polynomials with negative powers, i.e., Laurent polynomials, further broadening the class of transformations accessible within this framework. The polynomial transformation is achieved by applying a sequence of arbitrary $\mathrm{SU}(2)$ rotations on an auxiliary qubit, rather than rotations in a fixed basis. The GQSP routine has several applications, such as state preparation and phase function transformations $(e^{iH}\!\to e^{if(H)})$. A notable case is Hamiltonian simulation, where GQSP offers a more direct and flexible route compared to standard QSP. > > * **Input:** A unitary operator (quantum function) $U$, and a target polynomial transformation $P(\cdot)$ with $|P(x)| \le 1$ for $\{x \in \mathbb{C} : |x| = 1\}$. > * **Output:** A unitary that block-encodes $P(U)$ using a single-qubit block variable. > > **Complexity:** Applying a polynomial of degree $d$ requires $d$ controlled-$U$ calls and $d$ single-qubit $\mathrm{SU}(2)$ rotations. > > *** > > **Keywords:** Quantum Signal Processing (QSP), Polynomial transformations, Block-encoding, Hamiltonian simulation, Phase functions. In this demo we implement a simple instance of the GQSP primitive, preparing a state $\propto \sum_x\cos^3(x)$, by applying the corresponding polynomial on a diagonal unitary matrix. Using the `gqsp` quantum function from the open-library, phase assignment with `phase`, and utility classical function for obtaining the QSP angles, the implementation is done naturally. *** *** ```python theme={null} !pip install -qq "classiq[qsp]" ``` ## Example: Preparing $|\psi\rangle \propto \sum_x\cos^3(x)|x\rangle$ State The idea of the algorithm is to prepare a diagonal unitary $U$, such that $U|x\rangle = e^{ix}|x\rangle$. Then, if we apply a polynomial $P(x)$ such that $P(e^{ix})=\cos^3(x)$, then we get $P(U)|x\rangle = \cos^3(x)$. First, we write our function as a Laurent polynomial in $e^{ix}$: $$ \cos^3(x) = \frac{1}{8}(e^{ix}+e^{-ix})^3 =\frac{1}{8}(e^{3 ix}+3e^{ix}+3e^{-ix}+e^{-3ix}). $$ Thus, the polynomial we are looking at is $P(z) = \frac{1}{8}(z^{-3}+3z^{-1}+3z+z^3)$. First, we find the GQSP angles, by calling the `gqsp_phases` function. For numerical stability, we make sure the polynomial is strictly smaller than 1, by multiplying by a scaling constant. ```python theme={null} import numpy as np from classiq.applications.qsp import gqsp_phases SCALING_CONST = 0.99 laurent_coeffs = 1 / 8 * np.array([1, 0, 3, 0, 3, 0, 1]) gqsp_phases = gqsp_phases(SCALING_CONST * laurent_coeffs) ``` The GQSP phases correspond to a polynomial with positive powers, $\tilde{P}(z) = \frac{1}{8}(1+3z^{2}+3z^4+z^6)$, however, the GQSP quantum function can shift it with a negative power, $P(z) = z^{-m} \tilde{P}(z)$, with $m=3$. ```python theme={null} negative_power = 3 ``` Next, we define the unitary we would like to operate on, $U|x\rangle = e^{ix}|x\rangle$. It is simply defined by calling the `phase` function: ```python theme={null} import numpy as np from classiq import * @qfunc def u_func(x: QNum): phase(2 * np.pi * x) ``` Next, we define a model that prepares the desired state. This is done by initializing $|0\rangle\rightarrow \frac{1}{2^{N/2}}\sum^{2^N-1}_{x=0}|x\rangle$, and then applying $P(U)$ via `gqsp`. ```python theme={null} NUM_QUBITS = 7 @qfunc def main(x: Output[QNum[NUM_QUBITS, UNSIGNED, NUM_QUBITS]], ind: Output[QBit]): allocate(ind) allocate(x) hadamard_transform(x) gqsp( u=lambda: u_func(x), aux=ind, phases=gqsp_phases, negative_power=negative_power ) ``` Now we can synthesize and execute the resulting quantum program ```python theme={null} qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3A4cHZmB5kCwN9E6dfDaU0unOrv ``` Screenshot 2025-09-14 at 10.53.11.png ```python theme={null} NUM_SHOTS = 1e5 with ExecutionSession(qprog, ExecutionPreferences(num_shots=NUM_SHOTS)) as es: result = es.sample() ``` We can verify the resulting distribution against the expected one (recall that we need to post-select on the block variable `ind`==0) ```python theme={null} import matplotlib.pyplot as plt df = result.dataframe df_post_selected = df[df.ind == 0].sort_values("x") x, prob = df_post_selected.x, df_post_selected.probability plt.plot( x, prob / SCALING_CONST**2, ".", label=f"GQSP with 1e{int(np.log10(NUM_SHOTS))} shots", ) plt.plot( x, (1 / 2 ** (NUM_QUBITS / 2) * np.cos(2 * np.pi * x) ** 3) ** 2, "-", label=r"$\frac{1}{2^N}cos^6(x)$", ) plt.xlabel("x", fontsize=16) plt.ylabel("f(x)", fontsize=16) plt.legend(loc="upper right", fontsize=14) ``` **Output:** ``` ``` output ## References \[1]: [Motlagh, D, and Nathan W. Generalized quantum signal processing. PRX Quantum 5 020368 (2024).](https://journals.aps.org/prxquantum/abstract/10.1103/PRXQuantum.5.020368) \[2]: [Martyn JM, Rossi ZM, Tan AK, Chuang IL. Grand unification of quantum algorithms. PRX Quantum 2, 040203. (2021).](https://journals.aps.org/prxquantum/abstract/10.1103/PRXQuantum.2.040203) # Fast Quantum Algorithm for Numerical Gradient Estimation Source: https://docs.classiq.io/explore/algorithms/quantum_primitives/gradient_estimation/gradient_estimation Open this notebook in GitHub to run it yourself > Given a scalar function of $d$ dimensions, $f(x_1, \ldots, x_d)$, computing its gradient at a specific point is often essential. > > On a classical computer, this requires at least $d+1$ queries to $f$. > > This notebook presents a quantum algorithm that computes the gradient with a single query, based on the paper by S. P. Jordan [\[1\]](#original-paper). > > This represents a dramatic speedup for high-dimensional functions, where function evaluation is typically the dominant computational cost. > > * **Input:** A black-box function $f$ of $d$ dimensions. > * **Promise:** The function is smooth and has bounded gradient: $|\nabla f|<\nabla f_{\text{max}}$. > * **Output:** The gradient $\nabla f$ at the origin with $n$ bits of precision, encoded on a quantum register. > > **Complexity:** In black-box query complexity, the classical algorithm requires at least $d+1$ queries to $f$, while the quantum algorithm requires only one. > > *** > > **Keywords:** Foundational quantum algorithms, Gradient, Function evaluation, Oracle problem, Quantum Fourier Transform (QFT) The core idea is an in-place computation: first, the function is encoded directly into the phases of the coordinate superposition state. Then, an inverse QFT is applied to overwrite that exact same coordinate register with the gradient, extracting it as a measurable outcome without needing a separate output register. We demonstrate this step by step - first with a simplified version to build intuition, then with refinements for the complete algorithm. Quantum Circuit ## Introduction # ## Initialization ```python theme={null} from gradient_estimation_helpers import * from classiq.execution.functions.util._logging import _logger ``` # ## Simplified Explanation of the Algorithm The algorithm has two main steps: 1. Encode the function into the phases of the coordinate register's superposition 2. Apply an inverse QFT to transform the coordinate register directly into the measurable gradient state **In detail:** 1. We define an interval $l$ with $N$ points around the origin where we estimate the gradient, establishing a resolution $\mathrm{dx}=l/N$. To calculate the gradient at an arbitrary point $x_0$ instead of the origin, we can define an auxiliary function $\tilde{f}(x) = f(x + x_0)$ and evaluate its gradient at the origin. For each dimension ($x_i$), we create the superposition $|\delta_i\rangle=|-l\rangle+|-l+\mathrm{dx}\rangle+|-l+2\mathrm{dx}\rangle\cdots|l-\mathrm{dx}\rangle$ using Hadamard gates on $|0\rangle^{\otimes n}$. *The actual encoding on qubits is discussed later.* For simplicity, we consider a single dimension. For $d > 1$, the state $|\delta\rangle$ is a tensor product over all dimensions: $$ |\delta\rangle=\prod_i|\delta_i\rangle $$ 2. Using a phase oracle or the phase kickback technique, we encode the function as phases: $$ e^{i\cdot2\pi f(-l)}|-l\rangle + e^{i\cdot2\pi f(-l+\mathrm{dx})}|-l+\mathrm{dx}\rangle + e^{i\cdot2\pi f(-l+2\mathrm{dx})}|-l+2\mathrm{dx}\rangle \cdots e^{i\cdot2\pi f(l-\mathrm{dx})}|l-\mathrm{dx}\rangle $$ 3. For a sufficiently small interval, we approximate $f(x)\approx f(0)+x\cdot f'(0)$, allowing us to factor the state: $$ \begin{aligned} & e^{i\cdot2\pi f(0)}\left(e^{i\cdot2\pi (-l)\cdot f'(0)}\lvert-l\rangle + e^{i\cdot2\pi (-l+\mathrm{dx})f'(0)}\lvert-l+\mathrm{dx}\rangle + e^{i\cdot2\pi (-l+2\mathrm{dx})f'(0)}\lvert-l+2\mathrm{dx}\rangle \cdots e^{i\cdot2\pi (l-\mathrm{dx})f'(0)}\lvert l-\mathrm{dx}\rangle\right) \\ &= e^{i\cdot2\pi f(0)}\sum_{-l}^{l-\mathrm{dx}} e^{i\cdot2\pi jf'(0)}|j\rangle \end{aligned} $$ 4. This state is exactly the QFT of the computational basis state $|f'(0)\rangle$. Applying the inverse QFT yields: $$ |f'(0)\rangle $$ 5. Measure the register to obtain $f'(0)$ (or $\nabla f$ for multi-dimensional functions). # ## Full Explanation of the Algorithm The simplified explanation captures the key idea but omits two important details: handling negative values and fractional representation. When estimating the gradient, we analyze an interval $l$ around the origin. The state $|\delta_i\rangle$ represents $N$ equally spaced points in this interval, normalized as: $$ x=\frac{l}{N}\delta $$ With signed $\delta$ ranging from $-N/2$ to $N/2-1$: $\delta=-N/2$ is the leftmost point ($-l/2$), $\delta=0$ is the origin, and $\delta=N/2-1$ is the rightmost point ($l/2$)\*. We also normalize the output. Assuming the gradient is bounded between $-m/2$ and $m/2$, we represent those values using the $N$ states: $$ \nabla f=\frac{m}{N}\delta_{measured} $$ When applying the algorithm, we choose $l$ and $m$ based on prior knowledge of $f(x)$. The value $N$ determines the final resolution. $l$ must be small enough to keep the function approximately linear, and $m$ must exceed the maximum expected gradient magnitude while maintaining sufficient resolution. Using these two normalizations, we modify the algorithm: 1. In step 2, instead of applying $f(\delta)$, we apply $\frac{N}{ml}f\left(\frac{l}{N}\delta\right)$. 2. In step 5, the signed measurement directly gives $\frac{N}{m}\nabla f$, so $\nabla f = \frac{m}{N}\delta_{measured}$. Using a signed register means the measured $\delta_{measured}$ is already the correct signed value with no further adjustment needed. \* The rightmost point is actually $l/2-l/N$ rather than $l/2$, because the center is at the origin and $N$ must be even. However, this distinction does not affect the conceptual understanding. # ## Parameter Selection We need to select appropriate values for $l$, $m$, and $N$. We use the following notation: * $\nabla f_{\text{max}}$ - bound on the gradient magnitude: $|\nabla f|<\nabla f_{\text{max}}$ * $\epsilon$ - desired accuracy: $|\nabla f_{\text{est}}-\nabla f| < \epsilon$ * $d$ - dimensionality of $f$ * $D_2$ - bound on the second derivative of $f$ near the origin # ### Selecting $l$ Choose $l$ to ensure the function remains approximately linear. Similar to classical numerical differentiation, this interval must be sufficiently small. To keep gradient variation within accuracy $\epsilon$, we require $\nabla f_{\text{max}} - \nabla f_{\text{min}} \le \epsilon$. For a single dimension, the second derivative is approximately $f''(x) \approx (\nabla f_{\text{max}} - \nabla f_{\text{min}}) / l$. This gives $l < \frac{\epsilon}{|f''(x)|}$. Using $D_2$ as the second derivative bound, we get $l < \frac{\epsilon}{D_2}$. In multiple dimensions, we sum deviations as root-mean-square across all $d$ dimensions, introducing a factor of $1/\sqrt{d}$. To improve this bound, we scale by the uniform distribution variance, $\frac{1}{12}$. The final bound for $l$ is: $$ l\leq\frac{2\sqrt{3}\epsilon}{D_2\sqrt{d}} $$ For a one-dimensional quadratic function $f(x)=ax^2+bx+c$, we need $l$ smaller than $\frac{\sqrt{12}\epsilon}{2a}$. Furthermore, in order to minimize the number of bits of precision to which $f$ must be evaluated, $l$ should be chosen as large as possible, subject to the constraint above. So $l$ should be chosen tightly. See Jordan's paper [\[1\]](#original-paper) for a full derivation. # ### Selecting $m$ The parameter $m$ bounds the gradient magnitude. Since the gradient is signed, we need $m \ge 2\nabla f_{\text{max}}$. The resolution of the result is $\frac{m}{N}$. For a given register size and accuracy $\epsilon$, the upper bound is $m \le 2N\epsilon$. # ### Selecting $n$ $N=2^n$ defines the result resolution. The step size between possible outcomes is $\frac{m}{N}$. To achieve accuracy $\epsilon$, we need $N\geq\frac{m}{2\epsilon}$. Assuming tight $m$ selection, this gives a lower bound for the number of qubits: $$ n\geq\log_2\left(\frac{\nabla f_{\text{max}}}{\epsilon}\right) $$ # ### Summary Given $\nabla f_{\text{max}}$ and desired accuracy $\epsilon$, select parameters as: | Parameter | Role | Constraint | | --------- | ------------------- | ------------------------------------------------------------------------------- | | $l$ | Sampling interval | $l \leq \frac{2\sqrt{3}\epsilon}{D_2\sqrt{d}}$ - keeps $f$ approximately linear | | $m$ | Gradient range | $2\nabla f_{\max} \leq m \leq 2\cdot2^n\epsilon$ | | $n$ | Qubits / resolution | $n \geq \log_2(\nabla f_{\max} / \epsilon)$ | # ## Outline This notebook covers: 1. Theoretical foundation and parameter selection 2. Implementation * Phase state preparation (phase kickback and direct methods) * Inverse QFT for gradient extraction * Examples with linear and non-linear functions 1. Performance analysis 2. Multi-dimensional examples ($d > 1$) ## Implementation # ## State Preparation # ### Phase Kickback The first step is to prepare the state: $$ \sum_{\delta}e^{i2\pi\frac{N}{ml}f(\frac{l}{N}\delta)}|\delta\rangle $$ The paper assumes an oracle $|x\rangle \rightarrow |f(x)\rangle$, which we use with the phase kickback technique to create this state. The next example demonstrates how this works. The phase kickback has three main steps: 1. Apply Hadamard gates on $|\delta\rangle$ to create a superposition of all sample points 2. Initialize the ancilla to $|1111...1\rangle$ (in binary) and apply QFT 3. Add $f(\delta)$ to the ancilla; the function value is "kicked back" as a phase See the [appendix](#appendix-1---phase-kickback) for details. ```python theme={null} # Set the default values: # l = 0.5, m = 2, n = 3, n0 = 3 p = params() # Set the function we want to calculate the gradient of: # f(x) = 0.5*x + 0.25 # Gradient: f'(0) = 0.5 p.set_function(p.linear, (0.5, 0.25)) # Unpack the parameters to global variables for easier use p.unpack(globals()) @qfunc def main(x: Output[QNum[n, SIGNED, 0]]): # 1. State preparation # 1. 1. Set the coordinates state - Apply Hadamard gate on the coordinates register allocate(x) hadamard_transform(x) # 1. 2. Set the ancilla state - Apply QFT to |1111...1> state ancilla = QNum("ancilla", n0, SIGNED, n0) prepare_basis_state([True] * n0, ancilla) qft(ancilla) # 1. 3. Apply the function f on the ancilla, to create the phase kickback # Calculate the normalized f and add it to the ancilla val = f_normalized(x) inplace_add(val, ancilla) # 2. Next step in the algorithm: QFT inverse on the coordinates register # invert(lambda: qft(x)) # 3. Return the ancilla back to |000...0> state and drop it invert(lambda: qft(ancilla)) apply_to_all(X, ancilla) drop(ancilla) # Run using a statevector simulator qprog_ancilla = synthesize(main) # show(qprog) # Uncomment to see the circuit print("Circuit Width:", qprog_ancilla.data.width) print("Circuit Depth:", qprog_ancilla.transpiled_circuit.depth) print("Gate Counts:", qprog_ancilla.transpiled_circuit.count_ops) df = calculate_state_vector(qprog_ancilla) df.sort_values(by="x") ``` **Output:** ``` Submitting job to simulator ``` **Output:** ``` Circuit Width: 7 Circuit Depth: 47 Gate Counts: {'u': 34, 'cx': 28} ``` **Output:** ``` Job: https://platform.classiq.io/jobs/f79b6a72-bc04-4c98-83d3-2af28406d793 ``` | | x | amplitude | magnitude | phase | probability | bitstring | | - | -- | ----------- | --------- | ------ | ----------- | --------- | | 7 | -4 | -0.25+0.25j | 0.35 | 0.75π | 0.125 | 0000100 | | 4 | -3 | -0.25-0.25j | 0.35 | -0.75π | 0.125 | 0000101 | | 5 | -2 | 0.25-0.25j | 0.35 | -0.25π | 0.125 | 0000110 | | 2 | -1 | 0.25+0.25j | 0.35 | 0.25π | 0.125 | 0000111 | | 6 | 0 | -0.25+0.25j | 0.35 | 0.75π | 0.125 | 0000000 | | 3 | 1 | -0.25-0.25j | 0.35 | -0.75π | 0.125 | 0000001 | | 1 | 2 | 0.25-0.25j | 0.35 | -0.25π | 0.125 | 0000010 | | 0 | 3 | 0.25+0.25j | 0.35 | 0.25π | 0.125 | 0000011 | Let's examine the phase compared to the classical function value: ```python theme={null} # Shift the classical function to match the quantum convention f_classical = f_normalized(df["x"]) - f_normalized(0) # Create a simplified dataframe with the relevant information, and sort it by the x values. phases = np.angle(df["amplitude"]).astype(float) phases_over_2pi = phases / (2 * np.pi) simplified_df = pd.DataFrame( {"f_classical": f_classical, "phase_over_2pi": phases_over_2pi.round(5)} ) simplified_df.index = df["x"] simplified_df.sort_index(inplace=True) # Unwrap the phase simplified_df["phase_over_2pi"] = np.unwrap(simplified_df["phase_over_2pi"], period=1) # Get rid of the global phase simplified_df["phase_over_2pi"] -= simplified_df["phase_over_2pi"].iloc[N // 2] simplified_df["f_classical"] -= simplified_df["f_classical"].iloc[N // 2] # Plot the results plt.figure() plot_classical() simplified_df.plot(style="o", ax=plt.gca()) plt.legend() plt.show() # Show the results as a dataframe simplified_df ``` output | | f\_classical | phase\_over\_2pi | | --- | ------------ | ---------------- | | x | | | | --- | --- | --- | | -4 | -1.00 | -1.00 | | -3 | -0.75 | -0.75 | | -2 | -0.50 | -0.50 | | -1 | -0.25 | -0.25 | | 0 | 0.00 | 0.00 | | 1 | 0.25 | 0.25 | | 2 | 0.50 | 0.50 | | 3 | 0.75 | 0.75 | The graph shows the original function (gray), classical values (blue), and quantum phase at sample points (orange). Results are displayed in both normalized coordinates (black axes) and original values (blue axes). # ### Direct Phase While the phase kickback approach is sometimes well-suited for hardware implementations with a state oracle, it requires significant circuit overhead in simulation. A direct approach provides more efficient state preparation for our purposes. A simpler approach is to directly prepare the desired state: $$ \sum_{\delta}e^{i2\pi\frac{N}{ml}f(\frac{l}{N}\delta)}|\delta\rangle $$ The example below demonstrates this method. Compare the circuit width, depth, and gate counts to see the efficiency gain. In practice, the choice between methods depends on whether you have access to an efficient oracle for the function. ```python theme={null} p = params() # f(x) = 0.5*x + 0.25 # Gradient: f'(0) = 0.5 p.set_function(p.linear, (0.5, 0.25)) p.unpack(globals()) @qfunc def main(x: Output[QNum[n, SIGNED, 0]]): # 1. State preparation allocate(x) hadamard_transform(x) phase(f_normalized(x), 2 * pi) # 2. Next step in the algorithm: QFT inverse on the coordinates register # invert(lambda: qft(x)) # Run using a statevector simulator qprog = synthesize(main) # show(qprog) # Uncomment to see the circuit print("Circuit Width:", qprog.data.width) print("Circuit Depth:", qprog.transpiled_circuit.depth) print("Gate Counts:", qprog.transpiled_circuit.count_ops) df = calculate_state_vector(qprog) df.sort_values(by="x") simplified_df = simplify_df(df) plot_simplified_df(simplified_df) ``` **Output:** ``` Submitting job to simulator ``` **Output:** ``` Circuit Width: 3 Circuit Depth: 1 Gate Counts: {'u': 3} ``` **Output:** ``` Job: https://platform.classiq.io/jobs/deb9fbe3-dadb-4f44-b33c-dbcba464e3fb ``` output The state is identical to the phase kickback method. Moving forward, we'll use the direct method for its superior efficiency. # ### Quadratic Function Non-linear functions are more interesting. In the next example, we explore a quadratic function. Note that the critical step occurs in the next phase when we apply the QFT. Here, the phases follow the function exactly without linearization. The QFT will extract the linear (gradient) component from these phase values. ```python theme={null} p = params() # f(x) = 0.5*x^2 + 0.25*x + 0.1 # Gradient: f'(x) = x + 0.25, so f'(0) = 0.25 p.set_function(p.quadratic, (0.5, 0.25, 0.1)) p.l = 2 p.unpack(globals()) @qfunc def main(x: Output[QNum[n, SIGNED, 0]]): # 1. State preparation allocate(x) hadamard_transform(x) phase(f_normalized(x), 2 * pi) # 2. Next step in the algorithm: QFT inverse on the coordinates register # invert(lambda: qft(x)) # Run using a statevector simulator qprog = synthesize(main) # show(qprog) # Uncomment to see the circuit print("Circuit Width:", qprog.data.width) print("Circuit Depth:", qprog.transpiled_circuit.depth) print("Gate Counts:", qprog.transpiled_circuit.count_ops) df = calculate_state_vector(qprog) df.sort_values(by="x") simplified_df = simplify_df(df) plot_simplified_df(simplified_df) ``` **Output:** ``` Submitting job to simulator ``` **Output:** ``` Circuit Width: 3 Circuit Depth: 9 Gate Counts: {'u': 6, 'cx': 6} ``` **Output:** ``` Job: https://platform.classiq.io/jobs/19028135-c695-4b39-a8a0-a30a04eee106 ``` output Decreasing $l$ narrows the sampling interval, making the function appear more linear within that region. ```python theme={null} p = params() # f(x) = 0.5*x^2 + 0.25*x + 0.1 # Gradient: f'(x) = x + 0.25, so f'(0) = 0.25 p.set_function(p.quadratic, (0.5, 0.25, 0.1)) p.l = 0.2 p.unpack(globals()) @qfunc def main(x: Output[QNum[n, SIGNED, 0]]): # 1. State preparation allocate(x) hadamard_transform(x) phase(f_normalized(x), 2 * pi) # 2. Next step in the algorithm: QFT inverse on the coordinates register # invert(lambda: qft(x)) # Run using a statevector simulator qprog = synthesize(main) # show(qprog) # Uncomment to see the circuit print("Circuit Width:", qprog.data.width) print("Circuit Depth:", qprog.transpiled_circuit.depth) print("Gate Counts:", qprog.transpiled_circuit.count_ops) df = calculate_state_vector(qprog) df.sort_values(by="x") simplified_df = simplify_df(df) plot_simplified_df(simplified_df) ``` **Output:** ``` Submitting job to simulator ``` **Output:** ``` Circuit Width: 3 Circuit Depth: 9 Gate Counts: {'u': 6, 'cx': 6} ``` **Output:** ``` Job: https://platform.classiq.io/jobs/f5e85e56-5921-4e56-b736-bd7f86101e21 ``` output # ## Full Algorithm # ### Implementation The final step is to apply the inverse QFT to the coordinates. This extracts the gradient from the phases and produces a measurable state containing the normalized gradient value. We will first define a QFunc that preforms the algorithm and calculates the gradient of a given function $f$, for single and multiple dimensions, and another function that transforms a given function (either Callable or QCallable) into a normalized phase oracle. ```python theme={null} @qfunc def gradient(f: QCallable[[QNum]], x: QNum) -> None: """ Apply the gradient estimation algorithm to estimate the gradient of 1 dimensional function $f$ at $x=0$. The function $f$ is given as a quantum oracle that applies the phase kickback. """ # 1. State preparation hadamard_transform(x) f(x) # 2. QFT inverse on the coordinates register invert(lambda: qft(x)) @qfunc def gradient_nd(f: QCallable[QArray[QNum]], coords: QArray[QNum]) -> None: """ Apply the gradient estimation algorithm to estimate the gradient of multi dimensional function $f$ at $\vec{x}=0$. The function $f$ is given as a quantum oracle that applies the phase kickback. """ # 1. State preparation hadamard_transform(coords) f(coords) # 2. QFT inverse on the coordinates register repeat(coords.len, lambda i: invert(lambda: qft(coords[i]))) def make_phase_oracle( f: Callable, l: float, m: float, N: int, x0: float = 0.0, d: int = 1, ) -> QCallable: """Return a @qfunc phase oracle that applies phase(f_norm(x), 2*pi). The oracle encodes the normalized function value as a phase: f_norm(x) = f(l/N * x - x0) * N / (l * m) Args: f: Symbolic Python callable representing the mathematical function. l: Sampling interval half-width. m: Gradient magnitude bound (output range). N: Number of sample points (2^n). x0: Evaluation point; shifts the sampling window. d: Dimensionality of the function. """ if d == 1: @qfunc def phase_oracle(x: QNum) -> None: phase(f(l / N * x - x0) * N / (l * m), 2 * pi) else: @qfunc def phase_oracle(coords: QArray[QNum]) -> None: args = [l / N * coords[i] - x0 for i in range(d)] phase(f(*args) * N / (l * m), 2 * pi) return phase_oracle ``` # ### Linear Functions We will see the first example on a linear function.\ We switch from statevector simulation to standard simulation, which measures the final result rather than examining phase values directly. ```python theme={null} p = params() p.set_function(p.linear, (-0.5, 0.25)) p.unpack(globals()) @qfunc def main(x: Output[QNum[n, SIGNED, 0]]): allocate(x) gradient(make_phase_oracle(f, l=l, m=m, N=N, x0=0), x) qprog_linear = synthesize(main) # show(qprog_linear) # Uncomment to see the circuit df = sample(qprog_linear) # Translate the majority state to a gradient value majority_state = dict(df.iloc[0]) value = majority_state.get("x") # Divide by (N/m) to get the actual gradient value. majority_gradient = value / (N / m) analytical_gradient = p.analytical_gradient(0) # Or use the helper function: # majority_gradient = state_to_gradient(majority_state.get('x'), p) # Print the results and compute the majority gradient print("Parsed probabilities:", df.set_index("x").to_dict()["probability"]) print(f"The analytical gradient is: {analytical_gradient}") print(f"The majority gradient is: {majority_gradient}") # Check if the majority result is correct within the resolution of the algorithm resolution = m / N is_correct = abs(majority_gradient - analytical_gradient) < resolution / 2 print(f"The majority result is", "correct" if is_correct else "incorrect") print("####################################################") # Compute the success rate of the algorithm, i.e. the percentage of shots that are correct within the resolution of the algorithm. success_rate, success_shots, total_shots = compute_success_rate( df, analytic_derivatives={"x": analytical_gradient}, p=p ) print(f"Success rate: {success_rate:.2%} ({success_shots}/{total_shots} shots)") show_bar(success_rate) fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(18, 7)) x_array = np.arange(-N // 2, N // 2) f_classical = f_normalized(x_array) f_classical -= f_classical[N // 2] plt.sca(ax1) plot_classical() ax1.plot(x_array, f_classical, "o", label="Theoretical values", markersize=8) xmin, xmax = -N, N ymin, ymax = -N // 2, N // 2 ax1.set_xlim(xmin, xmax) ax1.set_ylim(ymin, ymax) ax1.vlines(-N // 2, ymin, ymax, colors="lightgray", linestyles="dashed") ax1.vlines(N // 2 - 1, ymin, ymax, colors="lightgray", linestyles="dashed") ax1.hlines(-N / 4 + 0.5, xmin, xmax, colors="lightgray", linestyles="dashed") ax1.hlines(N / 4, xmin, xmax, colors="lightgray", linestyles="dashed") ax1.legend(fontsize=12) ax1.set_title("Theoretical Phases", fontsize=14, fontweight="bold") ax1.tick_params(labelsize=11) ax1.set_xlabel("x (index)", fontsize=12) ax1.set_ylabel("f (normalized)", fontsize=12) plt.sca(ax2) percentage = df["counts"] / df["counts"].sum() * 100 ax2.bar(df["x"], percentage, color="lightblue", label="Measurement counts") ax2.set_xlabel("x (index)", fontsize=12) ax2.set_ylabel("Percentage of shots (%)", fontsize=12) ax2.set_xlim(-N // 2 - 1, N // 2) ax2.set_ylim(0, 100) ax2.set_title("Measurement Histogram", fontsize=14, fontweight="bold") ax2.tick_params(labelsize=11) if analytical_gradient is not None: x_analytic = analytical_gradient * (N / m) ax2.axvline( x=x_analytic, color="green", linestyle="dashed", label="Analytical gradient", linewidth=2, ) ax2.legend(fontsize=12) plt.tight_layout() plt.show() ``` **Output:** ``` Submitting job to simulator Job: https://platform.classiq.io/jobs/c2584af9-9b57-43d9-991a-70772ca1c7a4 ``` **Output:** ``` Parsed probabilities: {-2: 1.0} The analytical gradient is: -0.5 The majority gradient is: -0.5 The majority result is correct #################################################### Success rate: 100.00% (2048/2048 shots) [██████████████████████████████████████████████████] 100.00% ``` output The measured gradient is always a multiple of the resolution $m/N$. In the previous example, the exact gradient -0.5 was a multiple of the resolution 0.25, so the success rate was 100%. When the gradient is not a multiple of the resolution, the result becomes a superposition of nearby states. The algorithm still provides a good approximation, as shown in the next example. ```python theme={null} p = params() # f(x) = 0.55*x + 0.25 # Gradient: f'(0) = 0.55 # Pay attention that 0.55 is not a multiple of m/N = 0.25, # so we expect to get a superposition of multiple states around the correct gradient. p.set_function(p.linear, (0.55, 0.25)) p.unpack(globals()) @qfunc def main(x: Output[QNum[n, SIGNED, 0]]): allocate(x) gradient(make_phase_oracle(f, l=l, m=m, N=N, x0=0), x) qprog_linear_2 = synthesize(main) df = sample(qprog_linear_2).sort_values("counts", ascending=False) analyze_results(df, p) ``` **Output:** ``` Submitting job to simulator Job: https://platform.classiq.io/jobs/3f1c1fa2-1576-484e-98d3-244f107a2326 ``` **Output:** ``` Parsed probabilities: {2: 0.89697265625, 3: 0.044921875, 1: 0.01953125, -4: 0.01171875, 0: 0.00927734375, -3: 0.0068359375, -1: 0.00634765625, -2: 0.00439453125} The analytical gradient is: 0.55 The majority gradient is: 0.5 The majority result is correct #################################################### Success rate: 89.70% (1837/2048 shots) [█████████████████████████████████████████████-----] 89.70% ``` output **Output:** ``` 0.89697265625 ``` # ### Quadratic Function For non-linear functions, the interval $l$ must be chosen carefully. The algorithm requires the function to be approximately linear over the interval, which means $l$ must be sufficiently small. The next example demonstrates this dependency. With appropriate selection of $l$, the function remains nearly linear over the sampling interval, yielding high success rates. ```python theme={null} # In this example we will use the quadratic function p = params() # f(x) = 0.6*x^2 + 0.25*x + 0.1 # Gradient: f'(x) = 1.2*x + 0.25, so f'(0) = 0.25 p.set_function(p.quadratic, (0.6, 0.25, 0.1)) # Setting l properly, ensuring we are in the linear regime of the function. p.l = 0.1 p.unpack(globals()) @qfunc def main(x: Output[QNum[n, SIGNED, 0]]): allocate(x) gradient(make_phase_oracle(f, l=l, m=m, N=N, x0=0), x) qprog_quadratic = synthesize(main) df = sample(qprog_quadratic).sort_values("counts", ascending=False) success_rate = analyze_results(df, p) assert success_rate > 0.9, r"The success rate should be above 90% for these parameters." ``` **Output:** ``` Submitting job to simulator Job: https://platform.classiq.io/jobs/069c0180-ccdd-43f7-bbe3-15f43589d689 ``` **Output:** ``` Parsed probabilities: {1: 0.98095703125, 2: 0.00927734375, 0: 0.005859375, -1: 0.00146484375, 3: 0.0009765625, -2: 0.0009765625, -4: 0.00048828125} The analytical gradient is: 0.25 The majority gradient is: 0.25 The majority result is correct #################################################### Success rate: 98.10% (2009/2048 shots) [█████████████████████████████████████████████████-] 98.10% ``` output Conversely, if $l$ is too large and the function deviates significantly from linearity, the success rate drops dramatically. ```python theme={null} # In this example we will use the quadratic function p = params() # f(x) = 0.6*x^2 + 0.25*x + 0.1 # Gradient: f'(x) = 1.2*x + 0.25, so f'(0) = 0.25 p.set_function(p.quadratic, (0.6, 0.25, 0.1)) # Setting l to be too big, so we are outside of the linear regime of the function. p.l = 1 p.unpack(globals()) @qfunc def main(x: Output[QNum[n, SIGNED, 0]]): allocate(x) gradient(make_phase_oracle(f, l=l, m=m, N=N, x0=0), x) qprog_quadratic_2 = synthesize(main) df = sample(qprog_quadratic_2).sort_values("counts", ascending=False) success_rate = analyze_results(df, p) assert success_rate < 0.5, r"The success rate should be below 50% for these parameters." ``` **Output:** ``` Submitting job to simulator Job: https://platform.classiq.io/jobs/7beb5389-94f2-4c79-b189-c67aea24008c ``` **Output:** ``` Parsed probabilities: {1: 0.2568359375, 2: 0.24169921875, 0: 0.23095703125, -1: 0.08935546875, 3: 0.087890625, -2: 0.0341796875, -4: 0.03369140625, -3: 0.025390625} The analytical gradient is: 0.25 The majority gradient is: 0.25 The majority result is correct #################################################### Success rate: 25.68% (526/2048 shots) [█████████████-------------------------------------] 25.68% ``` output ## Performance Analysis We can plot the success rate as a function of parameter choices. Consider a quadratic function $f(x)=0.6x^2+0.25x+0.1$ with $f'(0)=0.25$, and target accuracy $\epsilon=0.2$. To determine valid parameter ranges, we need to use: * $d=1$ (dimensionality) * $D_2=1.2$ (second derivative bound) * $\nabla f_{\max}$: With $l \approx 0.1$, we have $\nabla f \in [0.19, 0.31]$, so $\nabla f_{\max}=0.31$ From the theory, the parameters must satisfy (with $n=3$): * $m\geq 2 \cdot 0.31 = 0.62$ * $m\leq 2 \cdot N \cdot \epsilon = 3.2$ * $l\leq 0.28$ In the next graph, we plot success rate versus $m$ with $l=0.1$. The valid range $[0.62, 3.2]$ is highlighted. ```python theme={null} _logger.setLevel(logging.WARNING) # Disable the logging for the loop def plot_success_rate_vs_m(m_values, function, function_params, l_val=0.1, n_val=3): """ Iterate over different m values and plot success rate as a function of m. Args: m_values: list of m values to test function: function name ("linear" / "quadratic") or callable function_params: parameters for the function l_val: l parameter (default 0.1) n_val: n parameter (default 3) """ success_rates = [] for m_val in m_values: p = params() p.m = m_val p.l = l_val p.n = n_val p.set_function(function, function_params) p.unpack(globals()) @qfunc def main(x: Output[QNum[n, SIGNED, 0]]): allocate(x) gradient(make_phase_oracle(f, l=l, m=m, N=N, x0=0), x) qprog = synthesize(main) df = sample(qprog).sort_values("counts", ascending=False) analytic_grad = p.analytical_gradient(0) epsilon = 0.2 success_rate, _, _ = compute_success_rate( df, analytic_derivatives={"x": analytic_grad}, p=p, tolerance=epsilon, ) success_rates.append(success_rate) print(f"m={m_val}: Success rate = {success_rate:.2%}") # Plot plt.figure(figsize=(10, 6)) plt.plot(m_values, success_rates, "o-", linewidth=2, markersize=8) plt.xlabel("m parameter", fontsize=12) plt.ylabel("Success Rate", fontsize=12) plt.title("Success Rate vs m Parameter", fontsize=14) plt.grid(True, alpha=0.3) plt.ylim([0, 1.05]) ax = plt.gca() theoretical_range = (0.62, 3.2) ax.axvspan(theoretical_range[0], theoretical_range[1], color="green", alpha=0.15) plt.show() return success_rates # Example usage: m_values = [ 0.05, 0.1, 0.33, 0.5, 0.67, 1.0, 1.33, 1.67, 2.0, 2.33, 2.67, 3.0, 3.33, 3.67, 4.0, 6.0, 10.0, ] results = plot_success_rate_vs_m(m_values, "quadratic", (0.6, 0.25, 0.1)) _logger.setLevel(logging.INFO) # TEMP ``` **Output:** ``` m=0.05: Success rate = 0.00% m=0.1: Success rate = 0.00% m=0.33: Success rate = 0.68% m=0.5: Success rate = 10.01% m=0.67: Success rate = 93.31% m=1.0: Success rate = 99.22% m=1.33: Success rate = 79.64% m=1.67: Success rate = 95.21% m=2.0: Success rate = 98.58% m=2.33: Success rate = 90.09% m=2.67: Success rate = 77.73% m=3.0: Success rate = 64.55% m=3.33: Success rate = 56.35% ``` Next, we plot success rate versus $l$ with $m=2$. The valid bound $l\leq 0.28$ is highlighted. ```python theme={null} from classiq.execution.functions.util._logging import _logger _logger.setLevel(logging.WARNING) # Disable the logging for the loop def plot_success_rate_vs_l(l_values, function, function_params, m_val=2.0, n_val=3): """ Iterate over different l values and plot success rate as a function of l. Args: l_values: list of l values to test function: function name ("linear" / "quadratic") or callable function_params: parameters for the function m_val: m parameter (default 1.0) n_val: n parameter (default 3) """ success_rates = [] for l_curr in l_values: p = params() p.m = m_val p.l = l_curr p.n = n_val p.set_function(function, function_params) p.unpack(globals()) @qfunc def main(x: Output[QNum[n, SIGNED, 0]]): allocate(x) gradient(make_phase_oracle(f, l=l, m=m, N=N, x0=0), x) qprog = synthesize(main) df = sample(qprog).sort_values("counts", ascending=False) analytic_grad = p.analytical_gradient(0) success_rate, _, _ = compute_success_rate( df, analytic_derivatives={"x": analytic_grad}, p=p ) success_rates.append(success_rate) print(f"l={l_curr}: Success rate = {success_rate:.2%}") plt.figure(figsize=(10, 6)) plt.plot(l_values, success_rates, "o-", linewidth=2, markersize=8) plt.xlabel("l parameter", fontsize=12) plt.ylabel("Success Rate", fontsize=12) plt.title("Success Rate vs l Parameter", fontsize=14) plt.grid(True, alpha=0.3) plt.ylim([0, 1.05]) ax = plt.gca() theoretical_range = (0, 0.28) ax.axvspan(theoretical_range[0], theoretical_range[1], color="green", alpha=0.15) plt.show() return success_rates # Example usage: l_values = [0.1, 0.2, 0.3, 0.4, 0.5, 1, 1.5, 2.0, 3.0] results_l = plot_success_rate_vs_l( l_values, "quadratic", (0.6, 0.25, 0.1), m_val=1.0, n_val=3 ) _logger.setLevel(logging.INFO) ``` ## Multi-Dimensional Examples The algorithm extends to multiple dimensions. In this example we will demonstrate a general quadratic coupled function: $$ f(x,y)=ax^2+by^2+cxy+dx+ey+f $$ ```python theme={null} p = params() p.l = 0.1 p.unpack(globals()) def f(x, y): a, b, c, d, e, f = 0.6, -0.4, 0.25, 0.25, -0.5, 0.1 return a * x**2 + b * y**2 + c * x * y + d * x + e * y + f def f_normalized(x, y): val = f(l / N * x, l / N * y) val *= N / (l * m) return val @qfunc def main(coords: Output[QArray[QNum[n, SIGNED, 0], 2]]): allocate(coords) gradient_nd(make_phase_oracle(f, l=l, m=m, N=N, x0=0, d=2), coords) qprog_2d = synthesize(main) df = sample(qprog_2d).sort_values("counts", ascending=False) print(df) gradient_analytical = [0.25, -0.5] majority_normalized = df.iloc[0]["coords"] gradient_measured = state_to_gradient(majority_normalized, p) print(f"Analytical gradient: (dx, dy) = ({gradient_analytical})") print(f"Majority gradient: (dx, dy) = ({gradient_measured})") success_rate, success_shots, total_shots = compute_success_rate( df, analytic_derivatives={"coords": gradient_analytical}, p=p ) print(f"Success rate: {success_rate:.2%} ({success_shots}/{total_shots} shots)") show_bar(success_rate) assert success_rate > 0.9, r"The success rate should be above 90% for these parameters." ``` ## Summary and Discussion This notebook demonstrated Jordan's quantum gradient estimation algorithm, which estimates $\nabla f$ at the origin using a **single query** to $f$, compared to the classical lower bound of $d+1$ queries. # ## Algorithm 1. Prepare a uniform superposition over $N = 2^n$ sample points using Hadamard gates. 2. Encode $f$ as phases, either via phase kickback from a state oracle or directly using a phase oracle. 3. Apply the inverse QFT to map the phase encoding onto a measurable computational basis state containing $\nabla f$. # ## Parameter Selection | Parameter | Role | Constraint | | --------- | ------------------- | ------------------------------------------------------------------------------- | | $l$ | Sampling interval | $l \leq \frac{2\sqrt{3}\epsilon}{D_2\sqrt{d}}$ - keeps $f$ approximately linear | | $m$ | Gradient range | $2\nabla f_{\max} \leq m \leq 2\cdot2^n\epsilon$ | | $n$ | Qubits / resolution | $n \geq \log_2(\nabla f_{\max} / \epsilon)$ | # ## Potential Use Cases The $d+1 \to 1$ query reduction is most valuable when $d$ is large and each function evaluation is expensive. Potential applications include: Optimization, Root-finding, Functional minimization and PDEs and more. # ## Limitation: Higher-Order Derivatives The algorithm **cannot be applied recursively** to compute second or higher-order derivatives of $f$. The method requires a quantum oracle that evaluates $f(x)$ coherently as a phase across all $x$ simultaneously. After running the circuit and measuring, the output is a single classical value $\nabla f(0)$ - not a new quantum oracle for $\nabla f(x)$ at arbitrary points $x$. Estimating $\nabla^2 f$ by this approach would require such an oracle for $\nabla f$, which the algorithm does not construct. In classical finite differences, second derivatives are obtained by calling $f$ at multiple points and differencing the first-derivative estimates. Replicating this quantumly would require separate oracle queries for each evaluation point, reducing the query complexity back to the classical $O(d^2)$ regime and eliminating the quantum advantage entirely. ## Appendices # ## Appendix 1 * Phase Kickback We start with the state $|\delta\rangle|0\rangle$ where $|\delta\rangle$ is a superposition created by the Hadamard gate for all the coordinates, hence: $$ \sum_{\delta_1=0}^{N-1}\cdots\sum_{\delta_d=0}^{N-1}|\delta_1\rangle\cdots|\delta_d\rangle $$ For convenience we will look at the $d=1$ case, but the same procedure can be used for bigger $d$. The ancilla starts at the ground state $|a\rangle=|0\rangle$. We first apply bitwise X gate to create the state $|111\cdots1\rangle$, which in the signed fractional QNum representation corresponds to the value $-1/N_0$. Next, we apply QFT on the ancilla. Using the fact that $e^{i2\pi a} = 1$ for integer $a$, the QFT of $|N_0-1\rangle$ simplifies to: $$ \frac{1}{\sqrt{N_0}}\sum_{a=0}^{N_0-1}e^{i2\pi a(N_0-1)/N_0}|a\rangle = \frac{1}{\sqrt{N_0}}\sum_{a=0}^{N_0-1}e^{-i2\pi a/N_0}|a\rangle $$ The full state after both Hadamard (on coordinates) and QFT (on ancilla) is: $$ \frac{1}{\sqrt{N \cdot N_0}}\sum_{\delta=0}^{N-1}\sum_{a=0}^{N_0-1} e^{-i2\pi a/N_0}\,|\delta\rangle|a\rangle $$ As discussed before, we assume that we have an oracle $f$ that applies the function on the state $|x\rangle \rightarrow |f(x)\rangle$. We apply this function and add the normalized result $f_\mathrm{norm}(\delta) = \frac{N}{ml}f\!\left(\frac{l}{N}\delta\right)$ to the ancilla register, mapping $|a\rangle \to |a + f_\mathrm{norm}(\delta)\rangle$: $$ \frac{1}{\sqrt{N \cdot N_0}}\sum_{\delta,\,a} e^{-i2\pi a/N_0}\,|\delta\rangle\,|a + f_\mathrm{norm}(\delta)\rangle $$ Substituting $a' = a + f_\mathrm{norm}(\delta)$, i.e. $a = a' - f_\mathrm{norm}(\delta)$: $$ \frac{1}{\sqrt{N \cdot N_0}}\sum_{\delta,\,a'} e^{-i2\pi (a' - f_\mathrm{norm}(\delta))/N_0}\,|\delta\rangle\,|a'\rangle $$ Separating the phase into two factors: $$ = \frac{1}{\sqrt{N}}\sum_{\delta=0}^{N-1} e^{i2\pi f_\mathrm{norm}(\delta)/N_0}\,|\delta\rangle \;\otimes\; \underbrace{\frac{1}{\sqrt{N_0}}\sum_{a'=0}^{N_0-1} e^{-i2\pi a'/N_0}\,|a'\rangle}_{\text{original ancilla state}} $$ The ancilla returns to its pre-oracle state and the function value has been "kicked back" as a phase onto the coordinate register. Since the ancilla is a fractional QNum with $n_0$ fractional bits, dividing by $N_0$ converts from the integer index back to the fractional value, so $f_\mathrm{norm}(\delta)/N_0 \to f_\mathrm{norm}(\delta)$ in the fractional encoding. The coordinate register is therefore left in the state: $$ \sum_{\delta=0}^{N-1} e^{i2\pi f_\mathrm{norm}(\delta)}\,|\delta\rangle = \sum_{\delta=0}^{N-1} e^{i2\pi \frac{N}{ml} f\!\left(\frac{l}{N}\delta\right)}\,|\delta\rangle $$ This is exactly the desired phase state from Step 2 of the algorithm, and the rest of the algorithm proceeds identically to the Direct Phase approach. ## References \[1]: [Stephen P. Jordan. Fast Quantum Algorithm for Numerical Gradient Estimation. Physical Review Letters 95 (2005)](https://www.researchgate.net/publication/7669221_Fast_Quantum_Algorithm_for_Numerical_Gradient_Estimation) # Hadamard Test Source: https://docs.classiq.io/explore/algorithms/quantum_primitives/hadamard_test/hadamard_test Open this notebook in GitHub to run it yourself The Hadamard test \[[1](#childs)] is a widely-used \[[2](#article-1),[3](#article-2)] quantum primitive that provides an elegant method to compute the real part of an expectation value of a given unitary for some target state. Many problems require evaluating the expectation value of a unitary operator for a prepared state, and the Hadamard test offers an intuitive alternative to the traditional methods of decomposing the unitary into Pauli strings and measuring each non-commutative string independently. The Hadamard test is a special case of the [linear combination of unitaries](https://github.com/Classiq/classiq-library/blob/main/tutorials/basic_tutorials/quantum_primitives/linear_combination_of_unitaries/linear_combination_of_unitaries.ipynb) (LCU) primitive. The [SWAP test](https://github.com/Classiq/classiq-library/blob/main/algorithms/quantum_primitives/swap_test/swap_test.ipynb), another special case of the LCU, may be viewed as a variation of the Hadamard test. The overall implementation consists of three functional building blocks followed by a measurement step, as illustrated in the scheme below:
hadamard_test_blocks
To implement the Hadamard test algorithm with a unitary $U$ and target state $|{\psi}\rangle$ as inputs and control-qubit probabilities as the outputs, a Hadamard gate $H$ is first applied to place the control qubit in a uniform superposition $\frac{1}{\sqrt{2}}\big(|{0}\rangle|{\psi}\rangle+|{1}\rangle|{\psi}\rangle\big)$. Next, a controlled unitary gate is applied to the target qubit array, resulting in the state $\frac{1}{\sqrt{2}}\big(|{0}\rangle|{\psi}\rangle+|{1}\rangle U|{\psi}\rangle\big)$. Another Hadamard gate $H$ is then applied to the control qubit, yielding $\frac{1}{2}\big(|{0}\rangle\big(\mathbb{I}+U\big)|{\psi}\rangle+|{1}\rangle\big(\mathbb{I}-U\big)|{\psi}\rangle\big)$, after which a final measurement is performed on the control qubit. The probability of measuring the control qubit in state $|{0}\rangle$, given by $P(0)=||\frac{1}{2}\big(\mathbb{I}+U\big)|{\psi}\rangle||^2$, enables the calculation the real part of the expectation value $\langle\psi|U|\psi\rangle$ in a post-processing step, through the simple algebraic operation $\text{Re}\langle\psi|U|\psi\rangle=2P(0)-1$. A different algebraic operation can be used to retrieve the imaginary part of the expectation value. You can refer to the Technical Details section below, describing the full mathematical derivation of a general Hadamard test implementation. In this tutorial, we will implement a Hadamard test for the Quantum Fourier Transform (QFT) unitary $U_{QFT}$ ([read more](https://docs.classiq.io/latest/qmod-reference/library-reference/open-library-functions/qft/qft/)), acting on a 4-qubit target state $|0000\rangle$ using the Classiq IDE/ SDK. This implementation leverages Classiq's high-level functional design utilities, following the functional block structure outlined above. The Quantum Fourier Transform (QFT) function is the quantum analog for the discrete Fourier transform. It is applied on the quantum register state vector in the following manner: $$ U_{QFT}|{j}\rangle=\frac{1}{\sqrt{2^n}}\sum_{k=0}^{2^n-1}e^{\frac{2\pi i}{2^n}jk}|{k}\rangle=\otimes_{t=1}^n\frac{1}{\sqrt{2}}\big(|{0}\rangle+e^{\frac{2\pi i}{2^{t}}j}|{1}\rangle) $$ Where $j$ and $k$ are the binary numbers the $n$ qubits represent. [more information](https://docs.classiq.io/latest/qmod-reference/library-reference/open-library-functions/qft/qft/) ## Guided Implementation To implement the Hadamard test for the QFT unitary $U_{QFT}$ and the state $|0000\rangle$, one control qubit and an array of four target qubits are initialized. The control qubit variable will be named `expectation_value` and is of `QBit` type, and the target qubit array will be captured by the `QArray` type variable `psi`. All qubits are initialized in $|0\rangle$ states, and the state of the `psi` qubit array will remain unchanged throughout the implementation ($|0000\rangle$). Our implementation of the Hadamard test involves three main steps, followed by a measurement of the `expectation_value` qubit and a post-processing step to obtain the real part of the expectation value: 1. Applying the Hadamard gate $H$ to the `expectation_value` qubit as a preparation step, creating a uniform superposition. 2. Applying the unitary gate $U_{QFT}$ on the `psi` qubit array in a controlled manner, conditioned on the control qubit being in the $|{1}\rangle$ state. 3. Re-application of a Hadamard gate $H$ to the control qubit that can be seen as an inverse preparation step, with $H$ acting as its own inverse. 4. A projective measurement of the `expectation_value` qubit, yielding the probabilities of measuring it in the $|0\rangle$ and $|1\rangle$ states. The probability $P(0)$ of being in the $|0\rangle$ state is then algebraically manipulated in a post-processing step to yield the real part of the expectation value by using the expression $2P(0)-1$. We begin by defining the function `controlled_qft` that implements the controlled operation of the unitary $U_{QFT}$ on `psi`, conditioned on the control qubit `expectation_value` is in the $|{1}\rangle$ state. This is achieved by leveraging the Classiq built-in `control` and `qft` functions: ```python theme={null} from classiq import * @qfunc def controlled_qft(expectation_value: QBit, psi: QArray): control(expectation_value, lambda: qft(psi)) ``` Next, We define the function `preparation_and_application`, seamlessly implementing the three main steps of the Hadamard test as outlined above, using the Classiq `Within-Apply` statement ([read more](https://docs.classiq.io/latest/qmod-reference/language-reference/statements/within-apply/)). The `Within-Apply` statement performs the operation $V^{\dagger}UV$, specifically designed for situations where a preparation step is performed solely to enable the operation of a particular function and is subsequently inverted. The preparation and inverse-preparation actions should be specified within the `Within` section, while the primary function operating should be specified in the `Apply` section. Since the third step, which involves the re-application of the Hadamard gate to the `expectation_value` qubit can be regarded as an inverse-preparation step (due to $H$ being its own inverse), the Hadamard test becomes a natural candidate for the `Within-Apply` statement. The preparation stage, which involves applying a Hadamard gate $H$ to the `expectation_value` qubit, and the re-application of the Hadamard gate to the same qubit after the controlled QFT operation on the `psi` qubit array are both managed within the `Within` section. The function `controlled_qft`, which handles the controlled operation of $U_{QFT}$ on the `psi` qubit array, is specified in the `Apply` section: ```python theme={null} @qfunc def preparation_and_application(expectation_value: QBit, psi: QArray): within_apply( within=lambda: H(expectation_value), apply=lambda: controlled_qft(expectation_value, psi), ) ``` Finally, we define a `main` function that encapsulates all essential components of the algorithm. It begins with the declaration and initialization of all qubits, followed by a call to the `preparation_and_application` function, which implements the three core steps: ```python theme={null} @qfunc def main(expectation_value: Output[QBit]): psi = QArray("psi") allocate(out=expectation_value, num_qubits=1) allocate(out=psi, num_qubits=4) preparation_and_application(expectation_value, psi) drop(psi) qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3HrOB5SyILzxrGdMKw6ap2I9zb4 ``` **Output:** ``` https://platform.classiq.io/circuit/3HrOB5SyILzxrGdMKw6ap2I9zb4?login=True&version=20 ``` While the code is elegantly structured, the resulting quantum program could be highly complex. The Classiq synthesis engine expertly manages this complexity, transforming the high-level code into a fully optimized quantum circuit according to your optimization preferences.
gif showing the expansion of the different building blocks
By declaring `psi` as a local variable in the `main` function (in contrast to `expectation_value`, which is declared globally), the execution of the quantum program on the Classiq simulator will yield the measurement outcomes of the `expectation_value` qubit in states $|{0}\rangle$ and $|{1}\rangle$, along with the corresponding measurement probabilities. The probability $P(0)$ can then be algebraically manipulated to calculate the real part of the expectation value, which should align with the analytical result: $$ \text{Re}\big(\langle 0000|U_{qft}|0000\rangle\big)=\langle 0000|++++\rangle= 0.25 $$ You can refer to the note below for the complete derivation. After the execution of the `main` function, the system is evolved into its final quantum state: $$ \frac{1}{2}\Big(|{0}\rangle\big(\mathbb{I}+U_{QFT}\big)|{0000}\rangle+|{1}\rangle\big(\mathbb{I}-U_{QFT}\big)|{0000}\rangle\Big)=\frac{1}{2}\Big(|{0}\rangle\big(|{0000}\rangle+|{++++}\rangle\big)+|{1}\rangle\big(|{0000}\rangle-|{++++}\rangle\big)\Big) $$ Since applying QFT on $|{0000}\rangle$ is equivalent to applying a 4-qubit Hadamard transform, transforming it to the $|{++++}\rangle$ state. Running the program on the Classiq simulator outputs the measurement results for both states of the control qubits, which can be analytically calculated and compared: $$ P(0)=\frac{1}{4}|||{0000}\rangle+|{++++}\rangle||^2=\frac{1}{2}\big(1+\frac{1}{4}\big)=0.625,\;\;\;\;\;\;\;\; P(1)=\frac{1}{4}|||{0000}\rangle-|{++++}\rangle||^2=\frac{1}{2}\big(1-\frac{1}{4}\big)=0.375 $$ where the result of the inner product $\langle 0000|++++\rangle=\frac{1}{4}\langle 0000|0000\rangle=\frac{1}{4}$ is used. The probabilities can then be manipulated to calculate the expectation value as $\text{Re}\big(\langle 0000|U_{qft}|0000\rangle\big)=2P(0)-1=0.25$, yielding the same result as the direct calculation provided above in the main text. Now, let us verify this by executing the quantum program and comparing the results with the analytical (pen-and-paper) calculations we have just derived. This could be achieved by executing manually through the IDE, selecting the Classiq simulator as execution hardware, and setting `Num shots` to "100000":
gif execution of the Hadamard test using the Classiq simulator
Or through the SDK, by running the following code: ```python theme={null} NUM_SHOTS = 100_000 df = sample(qprog, num_shots=NUM_SHOTS) P_0 = df[df["expectation_value"] == 0]["counts"].sum() / NUM_SHOTS P_1 = df[df["expectation_value"] == 1]["counts"].sum() / NUM_SHOTS print(r"P_0={}".format(P_0)) print(r"P_1={}".format(P_1)) ``` **Output:** ``` Submitting job to simulator ``` **Output:** ``` Job: https://platform.classiq.io/jobs/0f858707-7b9e-4a80-a01c-fcf2bbe71bac ``` **Output:** ``` P_0=0.62788 P_1=0.37212 ``` Execution through the SDK enables post-processing of the data, allowing us to recover the real part of the expectation value and compare it to the analytically calculated value: ```python theme={null} Expectation_value = 2 * P_0 - 1 print("Re()={}".format(Expectation_value)) ``` **Output:** ``` Re()=0.25576 ``` The value obtained is a statistical estimate derived from averaging measurement outcomes, where the number of measurements (`tot_num_shots`) determines the precision of this estimate. ## Technical Details The following mathematical description is for an implementation of a Hadamard test on a system of one control qubit and a target qubit-array of $N$ qubits, initiated in $|{0}\rangle|{\psi}\rangle$, where $|{\psi}\rangle$ may essentially be in any prepared state. The control qubit is first prepared in a uniform superposition by applying a Hadamard transform $H$: $$ |{\phi_1}\rangle=\big(H\otimes\mathbb{I}\big)|{0}\rangle|{\psi}\rangle= \frac{1}{\sqrt{2}}\Big(|{0}\rangle|{\psi}\rangle+|{1}\rangle|{\psi}\rangle\Big) \qquad\qquad;\qquad\qquad H=\frac{1}{\sqrt{2}}\left( {\begin{array}{cc} 1 & 1 \\ 1 & -1 \\ \end{array} } \right) $$ This step is sequentially followed by a selection step, in which a controlled unitary operation of the form $V=|{0}\rangle\langle{0}|\otimes \mathbb{I}+|{1}\rangle\langle{1}|\otimes U$ is successively applied to the target qubit(s), where $U$ is some general unitary matrix: $$ |{\phi_2}\rangle=V|{\phi_1}\rangle= |{0}\rangle U|{\psi}\rangle+|{1}\rangle|{\psi}\rangle $$ Another Hadamard transform is then applied to the control qubit: $$ |{\phi_3}\rangle=\big(H\otimes\mathbb{I}\big)|{\phi_2}\rangle= \frac{1}{2}\Big(|{0}\rangle|{\psi}\rangle+|{1}\rangle|{\psi}\rangle+|{0}\rangle U|{\psi}\rangle-|{1}\rangle U|{\psi}\rangle\Big)=\frac{1}{2}\Big(|{0}\rangle\big(\mathbb{I}+U\big)|{\psi}\rangle+|{1}\rangle\big(\mathbb{I}-U\big)|{\psi}\rangle\Big) $$ The final step, prior to the post-processing algebraic manipulation, is a projective measurement of the control (ancilla) qubit onto the $|{0}\rangle$ subspace $\mathcal{P}=|{0}\rangle\langle{0}|\otimes\mathbb{I}$: $$ |{\phi_4}\rangle=\mathcal{P}|{\phi_3}\rangle=\big(|{0}\rangle\langle{0}|\otimes\mathbb{I}\big)|{\phi_3}\rangle=\frac{1}{2}|{0}\rangle\big(\mathbb{I}+U\big)|{\psi}\rangle $$ This measurement is effectively translated into a measurement of the expectation value $\text{Re}\big(\langle{\psi}|U|{\psi}\rangle\big)$ by first obtaining the probability that the control qubit is in the $|{0}\rangle$ state: $$ P(0)=||\frac{1}{2}\big(\mathbb{I}+U\big)|{\psi}\rangle||^2=\frac{1}{4}\langle{\psi}|\big(\mathbb{I}+U^\dagger\big)\big(\mathbb{I}+U\big)|{\psi}\rangle=\frac{1}{4}\Big(\langle{\psi}|{\psi}\rangle+\langle{\psi}|U^\dagger|{\psi}\rangle+\langle{\psi}|U|{\psi}\rangle+\langle{\psi}|UU^\dagger|{\psi}\rangle\Big)=\frac{1}{4}\Big(2+\langle{\psi}|U^\dagger|{\psi}\rangle+\langle{\psi}|U|{\psi}\rangle\Big) $$ And algebraically manipulating it to receive the real part of the expectation value of $U$: $$ 2P(0)-1=\frac{1}{2}\Big(2+\langle{\psi}|U^\dagger|{\psi}\rangle+\langle{\psi}|U|{\psi}\rangle\Big)-1=\frac{1}{2}\Big(\langle{\psi}|U^\dagger|{\psi}\rangle+\langle{\psi}|U|{\psi}\rangle\Big)=\frac{1}{2}\Big(\big(\langle{\psi}|U|{\psi}\rangle\big)^\dagger+\langle{\psi}|U|{\psi}\rangle\Big)=\text{Re}\big(\langle{\psi}|U|{\psi}\rangle\big) $$ ## References \[1]: [Lecture Notes on Quantum Algorithms (Andrew M. Childs)](https://www.cs.umd.edu/~amchilds/qa/) \[2]: [Quantum error mitigation for Fourier moment computation (Kiss et al.)](https://arxiv.org/pdf/2401.13048) \[3]: [Quantum-classical algorithms for skewed linear systems with an optimized Hadamard test (Wu et al.)](https://arxiv.org/abs/2009.13288) # Quantum Oracle Sketching for Boolean Functions Source: https://docs.classiq.io/explore/algorithms/quantum_primitives/quantum_oracle_sketching_boolean/quantum_oracle_sketching_boolean Open this notebook in GitHub to run it yourself > **Quantum Oracle Sketching** is a data-loading algorithm introduced by Zhao et al. [\[1\]](#ref1). It provides the missing link between classical data access and the *coherent oracle queries* on which many powerful quantum algorithms are built. > > The algorithm approximates the desired oracle $O_f$ on the fly from a stream of classical samples $(x_t, f(x_t))$, applying an incremental data-dependent rotation per sample and discarding each sample once consumed. At no point is the dataset stored in either classical or quantum memory. The result is an exponential advantage in space complexity over any classical learner; and when the data distribution varies in time while the learner stays fixed, a super-polynomial advantage in sample complexity is obtained as well: a classical machine requires the number of data examples to scale super-polynomially with the data size $N$, while a quantum learner only needs an amount of data that scales linearly, achieving matching accuracy and probabilistic performance. > > The algorithm treats the following problem: > > * **Input**: $M$ classical data samples $\{(x_t, f(x_t))\}_{t=1}^{M}$, where each $x_t \in [N] = \{0,1,\dots,N-1\}$ is drawn independently from a (possibly time-varying) distribution $p$, and $f$ is the target Boolean function ($f[N]\rightarrow \{0,1\}$). > * **Output:** A quantum unitary $V$ on $n = \lceil\log_2 N\rceil$ qubits that approximates the corresponding *coherent* query oracle $O$, the phase oracle $|x\rangle \to (-1)^{f(x)}|x\rangle$, or a real valued phase $|x\rangle \to e^{i \theta_x}|x\rangle$ to error $\epsilon$ in diamond distance. > > **Extensions**: Here we consider a Boolean function. Nevertheless, the method can be generalized for multi-bit Boolean output real-valued functions ($f:[N]\to \mathbb{R}$), state preparation (input $(j, b_j)$, encoding the index $j$ and value $b_j$ of a vector element) oracles, and block-encodings of matrices (input is a tuple $(i, j, A_{ij})$, encoding the elements of a matrix $A$). $V$ can then be used as a drop-in subroutine inside any quantum query algorithm. > > **Complexity** > > * **Sample complexity**: A single $\epsilon$-accurate oracle sketch requires $M = \Theta(N/\epsilon)$ classical samples. To support an arbitrary quantum algorithm that makes $Q$ queries to total error $\epsilon$, each query is sketched to error $\epsilon/Q$, giving $$ M_\text{total} = \Theta\!\bigl(N Q^2 / \epsilon\bigr). $$ > The quadratic dependence on $Q$ is unavoidable, mirroring the Born-rule relationship between quantum amplitudes and probabilities. > > * **Space complexity**: $\mathcal{O}(\mathrm{poly}\log N)$ qubits - the quantum machine never stores the dataset, only its current quantum state. > * **Time complexity**: $\widetilde{\mathcal{O}}(N)$ in the data-loading stage (one constant-depth controlled gate per sample); subsequent processing of each sample requires only $\mathrm{poly}\log N$ time. Here the overscript tilde designates possible hidden logarithmic dependencies in $N$. > * **Classical lower bound (dynamic case)**: Any classical machine of size ${\cal{O}}(N^{0.99})$ that matches the quantum prediction accuracy on a time-varying distribution, requires a super-polynomial number of samples. > > *** > > **Keywords:** Quantum Learning Theory, Quantum Machine Learning (QML), Streaming Algorithms ## Introduction Most quantum speedup over a classical baseline is stated relative to an *oracle*, a unitary $O_f$ that encodes the input function $f$ and acts in superposition. [Grover's search](https://github.com/Classiq/classiq-library/blob/main/algorithms/search_and_optimization/grover/grover.ipynb) assumes a phase oracle $O_f|x\rangle = (-1)^{f(x)}|x\rangle$; the [HHL linear-system solver](https://github.com/Classiq/classiq-library/blob/main/algorithms/quantum_linear_solvers/hhl/hhl.ipynb) [\[5\]](#ref5), [quantum singular-value transformation (QSVT)](https://github.com/Classiq/classiq-library/blob/main/algorithms/quantum_linear_solvers/qsvt_matrix_inversion/qsvt_matrix_inversion.ipynb) [\[4\]](#ref4), and most quantum machine-learning routines rely on a block-encoding oracle for the input matrix; [amplitude estimation](https://github.com/Classiq/classiq-library/blob/main/algorithms/amplitude_amplification_and_estimation/oblivious_amplitude_amplification/oblivious_amplitude_amplification.ipynb) and quantum walks similarly assume coherent access to the input. This *coherent* access, the ability to evaluate $f$ on a superposition of inputs in a single query, is exactly what enables the quadratic and exponential speedups, without it, an in-depth analysis shows that the algorithms collapse to their classical counterparts. Yet real-world classical data, produced one sample at a time by experiments, sensors, or users, does not natively support such queries. The standard remedy is to load the entire dataset into a quantum random-access memory (QRAM [\[3\]](#ref3)), but its fault-tolerance overhead often exceeds the quantum advantage it enables, leaving the most powerful quantum algorithms without a practical interface to massive classical data. Quantum oracle sketching is the algorithm that closes this gap: it constructs an approximate $O_f$ on the fly from a stream of classical samples, with no QRAM and no full dataset ever held in memory. ## The Streaming-Learning Framework Quantum oracle sketching is proposed in the context of a specific online-learning model. The framework sets the rules under which both the quantum and the classical baselines operate, and it explains why the comparison between the two is well-posed. **Setup.** * A *data-generating process* $\mathcal{D}$ emits a sequence of classical samples $z_1, z_2, \dots, z_M$. Each sample is one observation of the underlying object - a function value $z_t = (x_t, f(x_t))$ for a Boolean target, a feature-vector / label pair $z_t = (i_t, \vec{x}_{i_t}, y_{i_t})$ for classification, or an entry $z_t = (i_t, j_t, A_{i_t j_t}, k_t, b_{k_t})$ of a linear system $A\vec{x} = \vec{b}$. * A *learner* of size $S$ (which can be the number of logical qubits composing the quantum learner, or the number of floating-point words for a classical one) observes the samples *one at a time*. After seeing $z_t$ it updates its memory, then discards $z_t$. At no point is the dataset stored in full. This is known as *streaming* model. * After $M$ samples, the learner is asked to predict, classify, or compress some property of the underlying process $\mathcal{D}$ (not of the empirical sample!). Examples: the label of a held-out test feature vector (classification), the value of a quadratic form $\vec{x}^\top \mathcal{M} \vec{x}$ (linear systems), or the projection of a test point onto the top principal direction (PCA). The two resources of interest are the *machine size* $S$ and the *sample count* $M$. Optimally, we would like to minimize both simultaneously. **Static versus dynamic data.** The framework allows the data distribution to drift in time, characterized by two parameters: * *Refreshing time* $\tau$: the timescale beyond which samples become effectively uncorrelated. * *Repetition number* $R$: the maximum expected number of times any single sample is repeated within a window of $\tau$ steps. When $\tau \to \infty$ we recover the static, i.i.d. setting. When $\tau = \widetilde{\mathcal{O}}(N)$ the distribution refreshes on the same timescale as the data size, and the quantum advantage in sample complexity becomes *super-polynomial*. In this regime, any $\Omega(N^{0.99})$-size classical machine needs super-polynomially many samples while a quantum learner with $\mathrm{poly}\log N$ qubits suffices with $\widetilde{\mathcal{O}}(N)$ samples. **End-to-end pipeline.** The full quantum learner is a three-stage assembly: 1. **Quantum oracle sketching** consumes the stream of classical samples and produces an approximate oracle unitary $V \approx O_f$ (the subject of the present notebook). 2. A **quantum query algorithm** * Grover, QSVT [\[4\]](#ref4), HHL [\[5\]](#ref5), amplitude estimation, etc. - uses $V$ as a black-box subroutine to prepare a target state $|\psi_\text{target}\rangle$ that encodes the desired property of $\mathcal{D}$. 1. **Interferometric classical shadows** - a sign-preserving variant of the Classical Shadow Tomography protocol [\[2\]](#ref2). The method extracts a compact classical model from $|\psi_\text{target}\rangle$ in $\mathrm{poly}\log N$ measurements, ready for offline prediction on arbitrary test inputs. This notebook focuses on the first stage of this scheme, covering the variants of the quantum oracle sketching of a Boolean function. ## Implementation of the Oracle Sketching Algorithm # ## Oracle Sketching of a Boolean Function and Uniform Distribution We aim to implement the Boolean phase oracle $$ O\,|x\rangle = (-1)^{f(x)}\,|x\rangle, \qquad x \in [N],\ f : [N] \to \{0,1\}, $$ given only random classical samples $z_t = (x_t,\, f(x_t))$ for $t = 1,\dots,M$, where each $x_t$ is drawn uniformly from $[N] = \{0,1,\dots,N-1\}$. **Per-sample rotation.** For each sample we apply a small data-dependent phase rotation that fires only on the basis state $|x_t\rangle$: $$ V_t \;=\; \exp\!\bigl(i\,\tau\, f(x_t)\,|x_t\rangle\!\langle x_t| / M\bigr). \tag{1} $$ **Coherent accumulation.** Because every $V_t$ is diagonal in the computational basis, all factors commute and the product collapses cleanly: $$ V \;\equiv\; \prod_{t=1}^M V_t \;=\; \exp\!\Bigl(i\tfrac{\tau}{M}\sum_{t=1}^M f(x_t)\,|x_t\rangle\!\langle x_t|\Bigr) \;=\; \sum_{x=0}^{N-1} \exp\bigl(i\,\tau\, m_x\, f(x)\bigr)\,|x\rangle\!\langle x|, \tag{2} $$ where $m_x = \frac{1}{M}\sum_t \mathbf{1}[x_t = x]$ is the empirical frequency of basis label $x$. **Concentration.** As $M$ grows, $m_x \to p(x) = 1/N$ by the law of large numbers. Picking $\tau = \pi N$ makes $\tau\, m_x \to \pi$ and $$ V \;\longrightarrow\; \sum_{x=0}^{N-1} e^{i\pi f(x)}\,|x\rangle\!\langle x| \;=\; \sum_x (-1)^{f(x)}\,|x\rangle\!\langle x| \;=\; O. \tag{3} $$ **Sample complexity.** Ref. [\[1\]](#ref1) shows the diamond-distance error decays as $\epsilon = \mathcal{O}(N/M)$, so reaching error $\epsilon$ costs $$ M = \Theta(N/\epsilon) \tag{4} $$ samples. Each sample is processed once and immediately discarded. **Comparison to generic random Hamiltonian simulation.** The $N/M$ scaling above is *not* what a naive random-rotation strategy would deliver. If one instead applied a sequence of small phases $\exp(i\,\tau\, h_{z_t}/M)$ driven by an arbitrary Hamiltonian $h_z$ with $\|h_z\| = \mathcal{O}(1)$ - hoping the sequence approximates $\exp(i\,\tau\,\mathbb{E}[h_z])$, randomized-Hamiltonian-simulation results show the operator-norm error compounds to $$ \epsilon_\text{naive} \;=\; \mathcal{O}(N^2 / M),\qquad \text{i.e.}\qquad M_\text{naive} \;=\; \Theta(N^2 / \epsilon). $$ That extra factor of $N$ would consume the entire quantum advantage. To support $Q$ queries with combined error $\epsilon$, each individual query must be sketched to error $\epsilon/Q$, so per-query naive simulation needs $\Theta(N^2 Q / \epsilon)$ samples and the total becomes $$ M_\text{total}^\text{naive} \;=\; Q \cdot \Theta(N^2 Q/\epsilon) \;=\; \Theta(N^2 Q^2/\epsilon), $$ versus the $\Theta(NQ^2/\epsilon)$ achieved here - a factor-$N$ blow-up that would take $\widetilde{\mathcal{O}}(N)$-sample tasks straight back into the regime where any classical machine of size $\Omega(N^{0.99})$ wins. The improvement to $\epsilon = \mathcal{O}(N/M)$ comes from a crucial piece of structure: each $V_t$ phases only the *one-dimensional* subspace $|x_t\rangle\!\langle x_t|$, and distinct values of $x$ label *mutually orthogonal* subspaces, so per-sample errors live in disjoint Hilbert-space blocks rather than compounding across the full register. Quantum oracle sketching is, in this sense, a carefully engineered *non-generic* instance of randomized Hamiltonian simulation, designed precisely to dodge the $N^2/M$ trap. We begin by importing the required python modules and define the size of the problem $N$ and the truth table, defining $f$. ```python theme={null} !pip install 'classiq[qsp]' -qq import matplotlib.pyplot as plt import numpy as np from classiq import * from classiq.applications.qsp import qsp_approximate, qsvt_phases np.random.seed(7) ``` ```python theme={null} N = 8 # problem size; the register has int(log2(N)) qubits # A random Boolean target function f: [N] -> {0, 1} F_TABLE = np.random.randint(0, 2, size=N) print(f"f(x) for x = 0,...,{N-1}: {F_TABLE.tolist()}") ``` **Output:** ``` f(x) for x = 0,...,7: [1, 0, 1, 0, 1, 1, 1, 1] ``` We begin by introducing the basic quantum functions, which are utilized to construct the quantum oracle sketching function. The primitive `apply_basis_phase` realizes $\exp(i\theta\,|x_\text{int}\rangle\!\langle x_\text{int}|)$ by controlling on the equality predicate `qvar == x_int`; the body `phase(theta)` becomes a relative phase on the controlled subspace and the identity elsewhere, which is exactly $V_t$. The `quantum_oracle_sketch_boolean` function iterates over the samples of `x_samples`, leading to an approximate application of $O_f$. ```python theme={null} @qfunc def apply_basis_phase(theta: CReal, x_int: CInt, qvar: QNum) -> None: r"""Apply $\exp(i \cdot \theta \cdot |x_\text{int}\rangle\langle x_\text{int}|)$ on ``qvar``. Implemented by controlling on the equality predicate ``qvar == x_int``; the inner ``phase($\theta$)`` becomes a relative phase on the controlled subspace and the identity elsewhere, which is exactly $V_t$. """ control(qvar == x_int, lambda: phase(theta)) @qfunc def quantum_oracle_sketch_boolean( theta: CReal, x_samples: CArray[CInt], qvar: QNum ) -> None: r"""Sketched Boolean phase oracle $V = \prod_t \exp(i \theta |x_t\rangle\langle x_t|)$. ``x_samples`` should be pre-filtered to the indices with $f(x_t) = 1$; a single fixed angle $\theta = \pi N / M$ is then applied per sample. """ repeat( count=x_samples.len, iteration=lambda t: apply_basis_phase(theta, x_samples[t], qvar), ) ``` We pre-filter the classical data so only samples with $f(x_t)=1$ enter the circuit (the rest contribute the identity), fix $\theta = \pi N / M$, and bake the samples into the circuit at synthesis time. The leading Hadamard transform lets us see the diagonal-phase action of $V$ on the uniform superposition $|+\rangle^{\otimes n}$. ```python theme={null} # For circuit synthesis we use a smaller M so the visualization stays readable; # the numpy benchmark below validates the algorithm at larger M. M_demo = 200 demo_samples = np.random.randint(0, N, size=M_demo) positive_samples = demo_samples[F_TABLE[demo_samples] == 1].tolist() theta = np.pi * N / M_demo # τ / M with τ = π N n_qubits = int(np.log2(N)) @qfunc def main(qvar: Output[QNum]) -> None: allocate(n_qubits, qvar) hadamard_transform(qvar) quantum_oracle_sketch_boolean(theta, positive_samples, qvar) qprog = synthesize(main) show(qprog) ``` **Verification on a 2-qubit Bell state via Classiq execution.** Take $f = [0, 0, 1, 1]$ - a phase on the most-significant-bit subspace. The ideal oracle maps $|\Phi^+\rangle = (|00\rangle + |11\rangle)/\sqrt{2}$ to $|\Phi^-\rangle = (|00\rangle - |11\rangle)/\sqrt{2}$. We prepare $|\Phi^+\rangle$ on a 2-qubit register, apply the sketched `quantum_oracle_sketch_boolean` for one random sample sequence of $M = 500$ samples, then run the *inverse* Bell preparation (CX followed by H on qubit 0) so that the Bell basis is rotated back to the computational basis: in particular $|\Phi^-\rangle \mapsto |10\rangle$ (i.e. `qvar = 1`). Measuring the register in the computational basis with `sample(...)` function makes the fidelity $|\langle\Phi^-|V|\Phi^+\rangle|^2$ equal to the probability of observing `qvar = 1`. The single-realization error scales as $\sqrt{N/M}$, so the fidelity gap from $1$ is of order $N/M \approx 0.008$ on average over realizations, with typical per-shot fluctuations of order $\sqrt{N/M} \approx 0.09$. ```python theme={null} from classiq.execution import sample # 2-qubit Bell-state setup: f = [0,0,1,1] phases the MSB subspace. N_ex, M_ex = 4, 500 F_ex = np.array([0, 0, 1, 1]) theta_ex = np.pi * N_ex / M_ex np.random.seed(0) demo_samples_ex = np.random.randint(0, N_ex, size=M_ex) positive_samples_ex = demo_samples_ex[F_ex[demo_samples_ex] == 1].tolist() @qfunc def main(qvar: Output[QNum]) -> None: bell = QArray[QBit, 2]() allocate(2, qvar) # 1) Prepare |Φ+⟩. # 2) Apply the sketched oracle on the QNum view. # 3) Inverse Bell prep — rotates the Bell basis to the computational basis, # in particular |Φ-⟩ → |10⟩ (qvar = 1). within_apply( within=lambda: ( bind(qvar, bell), # qvar (initialised) → bell H(bell[0]), CX(bell[0], bell[1]), bind(bell, qvar), # bell → qvar so the oracle sees a QNum ), apply=lambda: quantum_oracle_sketch_boolean( theta_ex, positive_samples_ex, qvar ), ) qprog_bell = synthesize(main) df = sample(qprog_bell, "simulator", num_shots=10_000) fidelity = float(df.loc[df["qvar"] == 1, "probability"].sum()) print(f"Fidelity |⟨Φ-|V|Φ+⟩|² at M={M_ex}: {fidelity:.4f}") ``` **Output:** ``` Fidelity |⟨Φ-|V|Φ+⟩|² at M=500: 0.9622 ``` # ## Verification of the Sample-Complexity Scaling In order to verify the sample complexity, we build the sketched unitary $V$ classically by collecting $M$ random samples and assembling the diagonal matrix from eq. (2). Following, we compare the results to the theory and evaluate the scaling constants (hidden by the big-O notation). Two scalings of the error to the ideal oracle $O$ are worth keeping distinct: * **Single-realization** operator-norm error $\|V_\lambda - O\|_2$ for one random data sample $\lambda = (x_1, \dots, x_M)$ is dominated by the *variance* of the empirical frequencies $|m_x - 1/N| = \mathcal{O}(1/\sqrt{NM})$. Each diagonal entry deviates by $\pi N|m_x - 1/N| \sim \sqrt{N/M}$, so the spectral norm scales as $\|V_\lambda - O\|_2 = \mathcal{O}(\sqrt{N/M})$. * **Random-unitary channel** $\mathcal{C}(\rho) = \mathbb{E}_\lambda[V_\lambda\, \rho\, V_\lambda^\dagger]$ - the actual object that gets used inside any quantum query algorithm - has diamond-distance error $\mathcal{O}(N/M)$ to $O \rho O^\dagger$. The improvement over a single realization comes because the random unitary is *unbiased to leading order*: $\|\mathbb{E}[V_\lambda] - O\| = \mathcal{O}(N/M)$, an order of magnitude better than a single draw. This second scaling drives the $M = \Theta(N/\epsilon)$ sample complexity. We verify both empirically below by sweeping $M$ over three decades and, for the channel, averaging $V_\lambda$ over independent runs. ```python theme={null} def ideal_oracle(f_table): r"""Diagonal Boolean phase oracle $O|x\rangle = (-1)^{f(x)} |x\rangle$.""" return np.diag((-1.0) ** f_table.astype(float)) def sketched_oracle(f_table, x_samples): r"""Numpy reference for $V = \prod_t \exp(i \tau f(x_t) |x_t\rangle\langle x_t| / M)$, $\tau = \pi N$. Each $V_t$ is diagonal, so the product collapses to a single diagonal: $V_{xx} = \exp(i \pi N m_x f(x))$ where $m_x$ is the empirical frequency of $x$. """ n = f_table.size m_total = x_samples.size counts = np.bincount(x_samples, minlength=n) m = counts / m_total return np.diag(np.exp(1j * np.pi * n * m * f_table)) # Single realization at M = 6000 M = 4000 x_samples = np.random.randint(0, N, size=M) V = sketched_oracle(F_TABLE, x_samples) O = ideal_oracle(F_TABLE) err = np.linalg.norm(V - O, ord=2) print(f"||V - O||_2 at M={M}, N={N}: {err:.3e}") # Sweep M to verify the single-realization vs channel-bias scalings. M_values = np.unique(np.logspace(2, 4.5, 12, dtype=int)) # n_trials = number of independent random sample sequences λ drawn per M. n_trials = 1500 single_err = np.empty(M_values.size) channel_err = np.empty(M_values.size) for k, M_k in enumerate(M_values): Vs = [ sketched_oracle(F_TABLE, np.random.randint(0, N, size=int(M_k))) for _ in range(n_trials) ] single_err[k] = np.mean([np.linalg.norm(Vk - O, ord=2) for Vk in Vs]) channel_err[k] = np.linalg.norm(np.mean(Vs, axis=0) - O, ord=2) # Theoretical guides: single-realization ~ sqrt(N/M); channel bias ~ N/M. c_single = float(np.median(single_err / np.sqrt(N / M_values))) c_channel = float(np.median(channel_err * M_values / N)) fig, ax = plt.subplots(figsize=(6, 4.5)) ax.loglog(M_values, single_err, "o-", label=r"single realization $\|V_\lambda - O\|_2$") ax.loglog( M_values, channel_err, "s-", label=r"channel bias $\|\mathbb{E}[V_\lambda] - O\|_2$" ) ax.loglog( M_values, c_single * np.sqrt(N / M_values), "--", color="C0", alpha=0.5, label=rf"${c_single:.2f}\,\sqrt{{N/M}}$", ) ax.loglog( M_values, c_channel * N / M_values, "--", color="C1", alpha=0.5, label=rf"${c_channel:.2f}\,N/M$", ) ax.set_xlabel("Number of samples $M$") ax.set_ylabel("Spectral-norm error") ax.set_title(f"Boolean oracle sketching, $N = {N}$, {n_trials} trials") ax.legend(fontsize=9) ax.grid(True, which="both", alpha=0.3) plt.tight_layout() plt.show() ``` **Output:** ``` ||V - O||_2 at M=4000, N=8: 1.694e-01 ``` output We obtain a relatively good agreement between theory and numerics, the single realization is obtains an exellect fit to the linear regression, while the channel bias show cases fluctuation due to high error to signal ration. Moreover, the scaling constants are of order one, demonstrating that the quantum sketching algorithm is useful for practical implementations. ## Extensions We now extend the basic sketching algorithm to the case where the data points are sampled from a non-uniform distribution. First, we analyze the case where the probability distribution, $p(x)$, is known. In practice, this case is scalable only when $p(x)$ can be defined in terms of $O(\log(N))$ parameters; otherwise, the memory required to store all the probabilities would ruin the exponential memory advantage. In the second extension we consider an unknown probability distribution, where only the range of probabilities is known. We present an implementation of the two cases and test the latter for a two-qubit operation. # ## Non-Uniform Known Probability Distribution The construction above assumed the samples $x_t$ are drawn *uniformly* from $[N]$, which let us pick the global scaling $\tau = \pi N$ so that $\tau m_x \to \pi$. When the samples are drawn from a known, possibly non-uniform distribution $p$ on $[N]$ with $p(x) > 0$ everywhere, the same product-and-concentration argument carries through provided we rescale per sample: $$ V_t \;=\; \exp\!\Bigl(i\,\tfrac{\pi}{p(x_t)}\,f(x_t)\,|x_t\rangle\!\langle x_t|\Big/M\Bigr), $$ so that the empirical-frequency limit $m_x \to p(x)$ still gives $\pi$ on each active basis state. The sample complexity becomes $M = \Theta\!\bigl(N\,\|1/p\|_\infty / \epsilon\bigr)$; the worst-case rescaling appears as an effective $\|1/p\|_\infty$ factor (the maximum value of $1/p(x)$ for $x\in[N]$) that reduces to $N$ for the uniform case. Below we present a generalization of `quantum_oracle_sketch_boolean` by accepting a *per-sample* angle `thetas[t] = pi / (p(x_samples[t]) * M)`; the existing `apply_basis_phase` primitive then realizes each $V_t$. `x_samples` is assumed pre-filtered to active samples ($f(x_t) = 1$). The function reduces to the uniform Boolean qfunc when $p = 1/N$ (all angles equal $\pi N / M$). ```python theme={null} @qfunc def quantum_oracle_sketch_known_p( probs: CArray[CReal], x_samples: CArray[CInt], qvar: QNum, ) -> None: r""" Sketched Boolean phase oracle for a known non-uniform distribution $p$. Args: probs: The probabilities of the samples. x_samples: The classical input samples. qvar: The quantum variable to apply the oracle to. """ repeat( count=x_samples.len, iteration=lambda t: apply_basis_phase( np.pi / (probs[t] * probs.len), x_samples[t], qvar ), ) ``` # ## Extension to an Unknown Probability Distribution (via QSVT) When $p$ is not known in advance the per-sample angle $\pi / p(x_t)$ is unavailable. Suppose, however, that we know *bounds* on the support of $p$: $p_{\min} \le p(x) \le p_{\max}$ on the support, with **condition number** $\kappa = p_{\max}/p_{\min}$. The construction of Zhao et al. handles this case in three stages. **Sketch with the $p_{\max}$-scaling.** Pick $t = 1/p_{\max}$. Quantum oracle sketching then approximates $$ U \;=\; \sum_x e^{i\,p(x) f(x)/p_{\max}}\,|x\rangle\!\langle x| \;=\; e^{i\Lambda}, \qquad \Lambda \;:=\; \sum_x \tfrac{p(x) f(x)}{p_{\max}}\,|x\rangle\!\langle x|. $$ The eigenvalues of $\Lambda$ are $0$ on the $f = 0$ subspace and lie in $[\,1/\kappa,\,1\,]$ on the $f = 1$ subspace. **Expose $\sin\Lambda$ with a Hadamard test.** Introduce one ancilla qubit $a$ and the gate $S = \mathrm{diag}(-i, 1)$. The unitary $$ W \;=\; X_a\, S_a\, X_a\, H_a\,(cU^{\dagger})\, X_a\,(cU)\, H_a~~, $$ which satisfies $$ \langle 0_a|\,W\,|0_a\rangle \;=\; \frac{U - U^{\dagger}}{2i} \;=\; \sin\Lambda. $$ So the spectrum of the block-encoded operator is **$0$** on $f = 0$ and **$\sin(p(x)/p_{\max}) \in [\sin(1/\kappa),\,\sin 1]$** on $f = 1$. Crucially, this is a Hermitian operator with eigenvalues in $[-1, 1]$ - the natural domain for a real QSVT polynomial. **Apply a threshold polynomial via QSVT.** Use the [QSVT](https://github.com/Classiq/classiq-library/blob/main/algorithms/quantum_linear_solvers/qsvt_matrix_inversion/qsvt_matrix_inversion.ipynb) polynomial of Gilyén-Su-Low-Wiebe, [\[6\]](#ref6) (Polynomial approximation for a threshold function) with threshold $\lambda^{\star} = \sin(1/\kappa)$. It is an even real polynomial $P$ satisfying * $|P(w)| \le 1$ on $[-1, 1]$, * $|P(w) - 1| \le \epsilon$ on $[0,\,\lambda^{\star}/2]$ (where the $f = 0$ entries sit), * $|P(w) + 1| \le \epsilon$ on $[\lambda^{\star},\,1]$ (where the $f = 1$ entries sit), * degree $d = O(\log(1/\epsilon)/\lambda^{\star})$. We choose a hyperbolic tangent function (with absolute value of the variable) as an approximate to the ideal (non-smooth) threshold function: $$ f(w) = s \tanh(a |w| - b \lambda^{\star} / \lambda^{\star})~~, $$ where $a = 4$ and $b=0.5$ are chosen so to minimize rapid changes (reducing the required polynomial order), while when $f(w>\lambda^{\star})\approx -s$, $s=0.95$ is the scale constant to keep $P$ safely within the valid range $[-1, 1]$. This target function constitutes a smooth target function for the QSVT polynomial, enabling an efficient and exact approximation. Applied to $W$ via QSVT, the block $\langle 0_a | P_{\text{QSVT}}(W, \phi) | 0_a \rangle$ equals $P(\sin\Lambda)$, which is $\epsilon$-close to the target oracle $O = \sum_x (-1)^{f(x)} |x\rangle\!\langle x|$. The QSVT degree scales as $$ Q \;=\; O\!\left(\frac{\log(1/\epsilon)}{\sin(1/\kappa)}\right) \;\le\; O\!\bigl(\kappa\,\log(1/\epsilon)\bigr)~~, $$ while the total sample complexity in the IID case is $$ M \;=\; O\!\left(\frac{p_{\max}}{p_{\min}^2}\cdot\frac{\log^2(1/\epsilon)}{\epsilon}\right)~~. $$ Crucially, no classical knowledge of $p$ itself enters: only the bounds $p_{\min}, p_{\max}$ are used, and only to fix $t = 1/p_{\max}$ and the QSVT degree. We first set up the problem and introduce the utility functions `hadamard_test_W`, which performs a block encoding of $\sin(\Lambda)$, and `proj_W_block`, which is used as both projectors of the QSVT algorithm. ```python theme={null} # Problem setup: known bounds on the distribution p, but p itself unknown to the algorithm N_unk = 4 n_qubits_unk = int(np.log2(N_unk)) p_min, p_max = 0.10, 0.45 kappa_unk = p_max / p_min # condition number of p restricted to its support # Toy ground-truth p (used ONLY to sample data; the algorithm never accesses it). p_vec = np.array([0.10, 0.15, 0.30, 0.45]) F_TABLE_unk = np.array([0, 0, 1, 1]) # Boolean target; active basis states: 2, 3 (I⊗Z) # Compute QSVT phases for the threshold polynomial. lambda_star = np.sin(1.0 / kappa_unk) EPS_QSVT = 2e-2 degree_qsvt = int(np.ceil(np.log(1.0 / EPS_QSVT) / lambda_star)) if degree_qsvt % 2 == 1: degree_qsvt += 1 # enforce even parity # Smooth threshold approximant: ≈ +1 near w=0, ≈ -1 for |w| ≥ λ*; tanh transition at λ*/ 2. SCALE = 0.95 # keep |P| < 1 strictly inside [-1, 1] for QSP convergence def threshold_target(w): return -SCALE * np.tanh(4.0 * (np.abs(w) - 0.5 * lambda_star) / lambda_star) poly_coeffs, fit_err = qsp_approximate( threshold_target, degree=degree_qsvt, parity=0, # even interval=[0.0, 1.0], plot=True, ) threshold_phases = qsvt_phases(poly_coeffs) print( f"κ = {kappa_unk:.2f}, λ* = {lambda_star:.3f}, " f"degree = {degree_qsvt}, fit err = {fit_err:.2e}, " f"# QSVT phases = {len(threshold_phases)}" ) # Classical sample preparation for the U = e^{iΛ} sketch (Stage 1). np.random.seed(0) M_unk = 20 # small M so the QSVT-chain depth stays inside the synthesis timeout x_samples_unk = np.random.choice(np.arange(N_unk), size=M_unk, p=p_vec) active_unk = x_samples_unk[F_TABLE_unk[x_samples_unk] == 1].tolist() # Fixed evolution time t = 1/p_max gives a SINGLE per-sample angle θ = t/M, # independent of x_t — we don't divide by p(x_t) because p is unknown. # This is the uniform-θ Boolean sketcher, with the scaling set by the bound p_max # rather than by 1/N. theta_unk = 1.0 / (p_max * M_unk) # Hadamard-test block encoding of sin(Λ) (Stage 2) @qfunc def hadamard_test_W( theta: CReal, x_samples: CArray[CInt], qvar: QNum, aux_W: QBit, ) -> None: r"""$W = X_a \cdot S_a \cdot X_a \cdot H_a \cdot (cU^\dagger) \cdot X_a \cdot (cU) \cdot H_a$ Block-encodes $\sin(\Lambda)$ in the $|0\rangle_{\text{aux\_W}}$ subspace: $\langle 0_{\text{aux\_W}}| W |0_{\text{aux\_W}}\rangle = (U - U^\dagger)/(2i) = \sin(\Lambda)$. """ H(aux_W) control(aux_W, lambda: quantum_oracle_sketch_boolean(theta, x_samples, qvar)) X(aux_W) control( aux_W, lambda: invert(lambda: quantum_oracle_sketch_boolean(theta, x_samples, qvar)), ) X(aux_W) H(aux_W) S(aux_W) # upto a global phase, gives diag(-i, 1), which is the desired. X(aux_W) # swap |0⟩_a ↔ |1⟩_a so sin(Λ) lives in the |0⟩_a block @qfunc def proj_W_block(aux_W: QBit, flag: QBit) -> None: r"""Flip `flag` when `aux_W` is in $|0\rangle$ — projector onto the $W$ block-encoded subspace.""" control(aux_W == 0, lambda: X(flag)) # Top-level program: apply the QSVT threshold polynomial to W @qfunc def main( qvar: Output[QNum], aux_W: Output[QBit], qsvt_aux: Output[QBit], ) -> None: allocate(n_qubits_unk, qvar) allocate(1, aux_W) allocate(1, qsvt_aux) # QSVT chain applied to W; the polynomial P(sin Λ) approximates # the target oracle O = Σ_x (-1)^{f(x)} |x⟩⟨x| in the |0⟩_{aux_W} block. qsvt( phase_seq=threshold_phases.tolist(), proj_cnot_1=lambda flag: proj_W_block(aux_W, flag), proj_cnot_2=lambda flag: proj_W_block(aux_W, flag), u=lambda: hadamard_test_W(theta_unk, active_unk, qvar, aux_W), aux=qsvt_aux, ) # Increasing the synthesis timeout above the 300 s default to implement the QSVT qprog_unk = synthesize(main, preferences=Preferences(timeout_seconds=600)) show(qprog_unk) ``` output **Output:** ``` κ = 4.50, λ* = 0.220, degree = 18, fit err = 1.61e-01, # QSVT phases = 19 Quantum program link: https://platform.classiq.io/circuit/3EcOBdA2ZnG9xHk7POYFM8F9JcN ``` In the plot above, we compare the even target function to the QSVT polynomial approximation. For better agreement, one can increase the polynomial order, at the expense of an increase in the circuit depth. # ### Verification on a 2-Qubit Bell State via Classiq Execution With $f = [0, 0, 1, 1]$ the ideal oracle is $O = I \otimes Z$, the same phase on the most-significant-bit subspace used in the Boolean cell above, so $O$ again maps $|\Phi^+\rangle = (|00\rangle + |11\rangle)/\sqrt{2}$ to $|\Phi^-\rangle = (|00\rangle - |11\rangle)/\sqrt{2}$. The unknown-$p$ pipeline replaces the direct sketched oracle by the QSVT polynomial $P(\sin\Lambda)$ block-encoded in $W$, which approximates $O$ inside the $|0\rangle_{\text{aux\_W}}$ ancilla subspace. We prepare $|\Phi^+\rangle$, apply the full QSVT chain, and then run the inverse Bell preparation so that $|\Phi^-\rangle \mapsto |10\rangle$ (`qvar = 1`). Post-selecting on the QSVT block (`aux_W` = `qsvt_aux` = 0) and measuring `qvar` gives the in-block fidelity $|\langle\Phi^-|\,P_{\text{QSVT}}(W)\,|\Phi^+\rangle|^2$. ```python theme={null} # 2-qubit Bell-state setup for the QSVT-corrected sketch. @qfunc def main( qvar: Output[QNum], aux_W: Output[QBit], qsvt_aux: Output[QBit], ) -> None: bell = QArray[QBit, 2]() allocate(2, qvar) allocate(1, aux_W) allocate(1, qsvt_aux) # W * A * W^{-1}. # W: cast qvar -> bell, prep |Φ+⟩, cast back to qvar so qsvt sees a QNum. # W^{-1} (auto-generated): cast qvar -> bell, inverse Bell prep # (rotates Bell basis to computational basis, |Φ-⟩ → |10⟩), cast back to qvar. within_apply( within=lambda: ( bind(qvar, bell), H(bell[0]), CX(bell[0], bell[1]), ), apply=lambda: qsvt( phase_seq=threshold_phases.tolist(), proj_cnot_1=lambda flag: proj_W_block(aux_W, flag), proj_cnot_2=lambda flag: proj_W_block(aux_W, flag), u=lambda: hadamard_test_W(theta_unk, active_unk, bell, aux_W), aux=qsvt_aux, ), ) # Same timeout bump as the cell above — the QSVT chain is the same size. qprog_unk_bell = synthesize(main, preferences=Preferences(timeout_seconds=600)) # printing the circuit metrics, showing that the QSVT chain is indeed the dominant contributor to the depth and gate count print( f"depth = {qprog_unk_bell.transpiled_circuit.depth}, " f"width = {qprog_unk_bell.data.width}, " ) df_unk = sample(qprog_unk_bell, "simulator", num_shots=10_000) # Post-select on the QSVT block (aux_W = qsvt_aux = 0) and read |Φ-⟩ off qvar = 1. in_block = df_unk[(df_unk["aux_W"] == 0) & (df_unk["qsvt_aux"] == 0)] p_block = float(in_block["probability"].sum()) fidelity_unk = ( float(in_block.loc[in_block["qvar"] == 1, "probability"].sum()) / p_block if p_block > 0 else float("nan") ) print(f"P(QSVT block, aux_W = qsvt_aux = 0) : {p_block:.4f}") print(f"Fidelity |⟨Φ-|P_QSVT(W)|Φ+⟩|² (post-selected) : {fidelity_unk:.4f}") ``` **Output:** ``` Submitting job to simulator ``` **Output:** ``` depth = 8672, width = 4, ``` **Output:** ``` Job: https://platform.classiq.io/jobs/52ecbba1-a265-4f7d-ad3c-20c71717ef12 ``` **Output:** ``` P(QSVT block, aux_W = qsvt_aux = 0) : 0.8016 Fidelity |⟨Φ-|P_QSVT(W)|Φ+⟩|² (post-selected) : 0.9879 ``` We obtain good fidelity ($\approx 0.98$), with high probability ($p_{\text{block}}\approx 0.8$). This verifies the accuracy of the oracle sketching algorithm. Note however, that the accuracy comes with an associated cost, as an accurate approximation of the threshold function requires a high QSVT order, which in turn, leads to a deep circuit ($\text{circuit depth: }\sim 8500$). ## Summary Quantum oracle sketching converts a stream of classical samples $(x_t, f(x_t))$ into an approximate coherent oracle $V \approx O_f$ in $\Theta(N/\epsilon)$ samples. We analyzed the sample complexity scaling of the Boolean / uniform case, verifying that the scaling constants are relatively small. Following, we demonstrate the performance of the oracle sketching by analyzing the accuracy of a two-qubit state transformation. Finally, we presented extensions to non-uniform known and unknown probability distributions. ## References [\[1\]](#ref1) Zhao, H., Zlokapa, A., Neven, H., Babbush, R., Preskill, J., McClean, J. R., and Huang, H.-Y. Exponential quantum advantage in processing massive classical data. [arXiv:2604.07639 (2026)](https://arxiv.org/abs/2604.07639) [\[2\]](#ref2) Huang, H.-Y., Kueng, R., and Preskill, J. *Predicting many properties of a quantum system from very few measurements.* Nature Physics 16, 1050-1057 (2020). [https://doi.org/10.1038/s41567-020-0932-7](https://doi.org/10.1038/s41567-020-0932-7). [arXiv](https://arxiv.org/abs/2002.08953) [\[3\]](#ref3) Giovannetti, V., Lloyd, S., and Maccone, L. *Quantum random access memory.* Physical Review Letters 100, 160501 (2008). [https://doi.org/10.1103/PhysRevLett.100.160501](https://doi.org/10.1103/PhysRevLett.100.160501). [arXiv](https://arxiv.org/abs/0708.1879) [\[4\]](#ref4) Gilyén, A., Su, Y., Low, G. H., and Wiebe, N. *Quantum singular value transformation and beyond: exponential improvements for quantum matrix arithmetics.* In Proceedings of the 51st Annual ACM SIGACT Symposium on Theory of Computing, 193-204 (2019). [https://doi.org/10.1145/3313276.3316366](https://doi.org/10.1145/3313276.3316366). [arXiv](https://arxiv.org/abs/1806.01838). [\[5\]](#ref5) Harrow, A. W., Hassidim, A., and Lloyd, S. *Quantum algorithm for linear systems of equations.* Physical Review Letters 103, 150502 (2009). [https://doi.org/10.1103/PhysRevLett.103.150502](https://doi.org/10.1103/PhysRevLett.103.150502). [arXiv](https://arxiv.org/abs/0811.3171). [\[6\]](#ref6) Martyn, J. M., Rossi, Z. M., Tan, A. K., and Chuang, I. L. *Grand unification of quantum algorithms.* PRX Quantum 2, 040203 (2021). [https://doi.org/10.1103/PRXQuantum.2.040203](https://doi.org/10.1103/PRXQuantum.2.040203). [arXiv](https://arxiv.org/abs/2105.02859). # Swap Test Algorithm Source: https://docs.classiq.io/explore/algorithms/quantum_primitives/swap_test/swap_test Open this notebook in GitHub to run it yourself The swap test is a quantum function that checks the overlap between two quantum states. The inputs of the function are two quantum registers of the same size, $|\psi_1\rangle, \,|\psi_2\rangle$, and it returns as output a single test qubit whose state encodes the overlap between the two inputs: $|q\rangle_{\rm test} = \alpha|0\rangle + \sqrt{1-\alpha^2}|1\rangle$, with $$ \alpha^2 = \frac{1}{2}\left(1+|\langle \psi_1 |\psi_2 \rangle |^2\right). $$ Thus, the probability of measuring the test qubit at state $|0\rangle$ is $1$ if the states are identical (up to a global phase) and 0.5 if the states are orthogonal to each other. The quantum model starts with an $H$ gate on the test qubit, followed by swapping between the two states controlled on the test qubit (a controlled-SWAP gate for each of the qubits in the two states) and a final $H$ gate on the test qubit. A general scheme of the swap test algorithm:
Swap_Test_blocks
Prepare two random states: ```python theme={null} import numpy as np np.random.seed(12) NUM_QUBITS = 3 amps1 = 1 - 2 * np.random.rand( 2**NUM_QUBITS ) # vector of 2^3 numbers in the range [-1,1] amps2 = 1 - 2 * np.random.rand(2**NUM_QUBITS) amps1 = amps1 / np.linalg.norm(amps1) # normalize the vector amps2 = amps2 / np.linalg.norm(amps2) ``` Create a model and synthesize: ```python theme={null} from classiq import * NUM_SHOTS = 100_000 @qfunc def main(test: Output[QBit]): state1 = QArray("state1") state2 = QArray("state2") prepare_amplitudes(amps1.tolist(), 0.0, state1) prepare_amplitudes(amps2.tolist(), 0.0, state2) swap_test(state1, state2, test) drop(state1) drop(state2) qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3HrO7mNKBxc3LxoPT0sTUUSnA8o ``` **Output:** ``` https://platform.classiq.io/circuit/3HrO7mNKBxc3LxoPT0sTUUSnA8o?login=True&version=20 ``` ## Swap Test Qmod Implementations The swap test is defined as a library function in the Qmod language. Verify the results: ```python theme={null} df = sample(qprog, num_shots=NUM_SHOTS) ``` **Output:** ``` Submitting job to simulator ``` **Output:** ``` Job: https://platform.classiq.io/jobs/df6ae1d2-246c-44de-b3bd-3c72c262576e ``` ## Comparing Measured with Exact Overlap Using the expected probability of measuring the state $|0\rangle$ as defined above, $$ \alpha^2 = \frac{1}{2}\left(1+|\langle \psi_1 |\psi_2 \rangle |^2\right), $$ we extract the overlap $|\langle \psi_1 |\psi_2 \rangle |=\sqrt{2 P\left(q_{\text{test}}=|0\rangle\right)-1}$. The exact overlap is computed with the dot product of the two state vectors. Note that for the sake of this demonstration we execute this circuit $100,000$ times to improve the precision of the probability estimate. This is usually not required in actual programs. ```python theme={null} overlap_from_swap_test = np.sqrt( 2 * df[df["test"] == 0]["counts"].sum() / df["counts"].sum() - 1 ) exact_overlap = np.abs(amps1 @ amps2) ``` ```python theme={null} print("States overlap from Swap-Test result:", overlap_from_swap_test) print("States overlap from classical calculation:", exact_overlap) ``` **Output:** ``` States overlap from Swap-Test result: 0.4708078164176971 States overlap from classical calculation: 0.46972037234759095 ``` ```python theme={null} RTOL = 0.05 assert np.isclose( overlap_from_swap_test, exact_overlap, RTOL ), f""" The quantum result is too far from the classical one by a relative tolerance of {RTOL}. Please verify your parameters""" ``` # ADAPT VQE Source: https://docs.classiq.io/explore/algorithms/quantum_state_preparation/adapt_vqe/adapt_vqe Open this notebook in GitHub to run it yourself > The **Adaptive Derivative-Assembled Pseudo-Trotter Variational Quantum Eigensolver (ADAPT-VQE)** \[[1](#adapt-vqe)] is a variational hybrid algorithm, which constitutes an extension of the Variational Quantum Eigensolver (VQE) framework \[[2](#vqe)], that constructs problem-specific ansätze in a systematic and adaptive manner. Instead of relying on a fixed, heuristic circuit structure, ADAPT-VQE iteratively grows the state ansatz by selecting operators from a predefined pool based on their energy gradients with respect to the current variational state. At each iteration, the operator that yields the largest energy reduction is appended to the circuit and its parameters are reoptimized. This adaptive procedure significantly reduces the number of variational parameters and circuit depth, making ADAPT-VQE particularly well suited for near-term quantum hardware. Importantly, the algorithm is heuristic, leading to an approximate solution with no proven convergence guarantees. > > The algorithm treats the following problem: > > * **Input:** System Hamiltonian $H$, reference state $| \Psi_{\text{ref}}\rangle$ and an operator pool of anti-Hermitian operators ${\cal O} =\{O_1,\dots,O_M\}$. > * **Output:** Approximation of the ground state and energy of $H$. > > **Complexity:** The major overhead generally comes from the evaluation of the elements of the energy gradient by repeated measurements of different observables. We therefore focus on the measurement complexity of the algorithm. The complexity depends on the chosen operator pool and classical optimization scheme. The total cost incorporates the two main components of the algorithm, (i) evaluating the energy gradient, and (ii) re-optimization of the current state-ansatz (corresponds to the standard VQE optimization). As a result, at each iteration step the cost is a sum of two terms, corresponding to the two algorithm components: $$ \text{Cost}\approx \sum_{k = 1}^K (M \cdot C_G(n)+ N_{\text{opt}^{(k)}}\cdot C_E(n))~~, $$ where $n$ is the number of qubits, $K$ is the total number of iterations, $C_G(n)$ is the measurement cost to estimate a single operator gradient, $N_{\text{opt}}^{(k)}$ is the number of optimizer steps at iteration $k$, and $C_E(n)$ is the measurement cost to estimate the energy once. > *** > > **Keywords:** Variational quantum algorithm, Chemistry, Optimization ## Overview The following notebook begins with a theoretical explanation of the algorithm. We then consider an explicit example, consisting of a system of four qubits. The ADAPT-VQE algorithm is employed to evaluate the ground state energy and state. Following, we provide a modified version of the algorithm, and the results of both versions of the ADAPT-VQE algorithms are compared to the results obtained by the standard VQE algorithm. The notebook concludes with a short summary and analysis of the algorithm and results. ## Algorithm Steps The algorithm builds and constructs the ansatz iteratively, one operator at a time, including only the most significant operators. This approach builds a minimal, problem-specific ansatz, avoiding unnecessary parameters and allowing systematic improvement of the accuracy. Similarly to VQE, the algorithms essentially reduce the circuit depth at the expense of measurements. Initially, we define an operator pool ${\cal O}=\{O_1,O_2,\cdots,O_M\}$, containing a collection of anti-Hermitian operators that are employed in the ansatz construction. We then follow the following procedure: 1. Initialize the qubits to the reference state, $| \Psi_{\text{ref}}\rangle$, and define the initial iterative state as $| \psi^{(0)}\rangle = | \Psi_{\text{ref}} \rangle$. 2. Prepare a trial quantum state with the current ansatz, denoted by $|\psi^{(k)}\rangle$, where $k=0,1,\dots$ corresponds to the iteration step number (initially set to zero). 3. Measure the commutator of the Hamiltonian with the operators of $\cal{O}$. The expectation value corresponds to the partial derivative of the energy (in the $k$ iteration step) with respect to the coefficient of $O_j$: $$ \frac{\partial E^{(k+1)}}{\partial \theta_j}\bigg |_{\theta_j=0} = \langle\psi^{(k)}|\partial_{\theta_i}e^{-\theta_i O_j} H e^{O_j \theta_j}|\psi^{(l)}\rangle {\bigg |}_{\theta_j = 0}+ \langle\psi^{(k)}|e^{-\theta_j O_j} H \partial_{\theta_j}e^{O_j \theta_j}|\psi^{(k)}\rangle {\bigg |}_{\theta_j = 0} $$ $$ = {{\langle \psi^{(k)}| [H, O_j] |\psi^{(k)}\rangle}}~. $$ Hence, the measured commutation relations, provides the gradiant $\nabla_\vec{\theta} E$, where $\vec{\theta} = \{\theta_1,\dots,\theta_k\}^T~~.$ 4\. If the magnitude of the gradient vector is below a certain threshold, stop. Otherwise, identify the operator with the largest gradient, and add it to the left end of the ansatz, with a new variational parameter. If $A_k\in \{O_i\}$ is an operator with the largest gradient on the step $k$, the update rule is given by $$ |\psi^{(k+1)} \rangle = e^{\theta_k A_k} |\psi^{(k)}\rangle~~. $$ Note, that the added operator still remains in the operator pool, therefore may be added again later on. 5\. Conduct a VQE experiment to re-optimize all the ansatz parameters, $\{\theta_1,\dots,\theta_k\}$. 6\. Go back to step 3 and take $k\rightarrow k+1$. The final optimized ansatz is of the form $$ | \psi^{\text{ADAPT}}(\vec{\theta}) \rangle = \Pi_{n=1}^{K} e^{\theta_k A_k}|\psi_{\text{ref}}\rangle~~, $$ where $K$ is the total number of iterations. Note that when multiple quantum computers are available or sufficiently many qubits on a single computer, the preparation of the trial state and measurement of the commutation relations with the Hamiltonian can be performed in parallel, substantially reducing the runtime. ## Implementation of ADPT-VQE with Classiq We consider a simple example, consisting of a four-qubit system. The Hamiltonian and operator pool are defined in terms of the [SparsePauliOp](https://docs.classiq.io/latest/qmod-reference/api-reference/classical-types/?h=sparsepauliop#classiq.qmod.builtins.structs.SparsePauliOp) data structure, allowing for rapid and efficient algebraic computations. For implementation convenience, we consider an operator pool of Hermitian operators (instead of anti-Hermitian as in the theoretical derivation). In this case, the ansatz stat is of the form $$ | \psi^{\text{ADAPT}}(\vec{\theta}) \rangle = \Pi_{k=1}^{K} e^{-i\theta_k A_k}|\psi_{\text{ref}}\rangle~~, $$ leading to the relation $$ \frac{\partial E^{(k+1)}}{\partial \theta_j}\bigg |_{\theta_j=0} = -i{{\langle \psi^{(k)}| [H, O_j] |\psi^{(k)}\rangle}}~~. $$ We begin by uploading software packages and utility functions ```python theme={null} from functools import reduce from operator import add, mul from typing import Final, List, Tuple import matplotlib.pyplot as plt import numpy as np import scipy from classiq import * ``` # ## Utility Functions We employ utility functions form the [ADAPT-QAOA notebook](https://github.com/Classiq/classiq-library/blob/main/applications/optimization/adapt_qaoa/adapt_qaoa.ipynb), used to efficiently compute the commutation of Pauli strings with the `SparsePauilOp` data structure. These are used in the computation of $\langle \psi^{(k)} | [H, O_j] | \psi^{(k)}\rangle$. The functions perform the following tasks: * `sorted_pauli_term` sorts Pauli terms according to the qubit's index. * `commutator` receives two `SparsePauilOp`s and returns their commutator as a `SparsePauilOp`. * `normalize_pauli_term` removes redundant Pauli identity operators, allowing for comparison and addition of two `SparsePauilOp`s. * `collect_pauli_terms` adds up the coefficients of identical Pauli strings. ```python theme={null} # multiplication table of two Pauli matrices # weight (imaginary), pauli_c = pauli_a * pauli_b pauli_mult_table: list[list[Tuple[int, int]]] = [ [(+0, Pauli.I), (+0, Pauli.X), (+0, Pauli.Y), (+0, Pauli.Z)], [(+0, Pauli.X), (+0, Pauli.I), (+1, Pauli.Z), (-1, Pauli.Y)], [(+0, Pauli.Y), (-1, Pauli.Z), (+0, Pauli.I), (+1, Pauli.X)], [(+0, Pauli.Z), (+1, Pauli.Y), (-1, Pauli.X), (+0, Pauli.I)], ] def sorted_pauli_term(term: SparsePauliTerm) -> SparsePauliTerm: """ Sort Pauli terms according to the qubit's index, e.g., Pauli.X(2)*Pauli.Z(7)*Pauli.X(4) ==> Pauli.X(2)*Pauli.X(4)*Pauli.Z(7) """ sorted_paulis = sorted(term.paulis, key=lambda p: p.index) return SparsePauliTerm(sorted_paulis, term.coefficient) def commutator(ha: SparsePauliOp, hb: SparsePauliOp) -> SparsePauliOp: """ Compute the commutator [ha, hb] = ha*hb - hb*ha, where ha and hb are SparsePauliOp objects. Returns a SparsePauliOp representing the commutator. """ n = max(ha.num_qubits, hb.num_qubits) commutation = SparsePauliOp([], n) for sp_term_a in ha.terms: for sp_term_b in hb.terms: parity = 1 coefficient = 1.0 msp = {p.index: p.pauli for p in sp_term_a.paulis} for p in sp_term_b.paulis: pauli_a = msp.get(p.index, Pauli.I) pauli_b = p.pauli weight, pauli = pauli_mult_table[pauli_a][pauli_b] if weight != 0: parity = -parity coefficient *= weight * 1j msp[p.index] = pauli # reconstruct pauli_string if parity != 1: # consider filtering identity terms, making sure the term is not empty pauli_term = (reduce(mul, (p(idx) for idx, p in msp.items()))).terms[0] pauli_term.coefficient = ( sp_term_a.coefficient * sp_term_b.coefficient * coefficient * 2 ) commutation.terms.append(pauli_term) return commutation def normalize_pauli_term(spt: SparsePauliTerm, num_qubits=-1) -> SparsePauliTerm: """ Remove redundant Pauli.I operators from a Pauli string making "normalized" strings comparable if num_qubits is set, an optional Pauli.I is added to ensure the length """ if not spt.paulis: return spt npt = sorted_pauli_term(spt) paulis = [] max_index = max_identity_index = -1 if num_qubits > 0: max_identity_index = num_qubits - 1 for ip in npt.paulis: if ip.pauli != Pauli.I: paulis.append(ip) max_index = max(max_index, int(ip.index)) else: max_identity_index = max(max_identity_index, int(ip.index)) if max_identity_index > max_index: paulis.append(IndexedPauli(Pauli.I, max_identity_index)) npt.paulis = paulis return npt def collect_pauli_terms(spo: SparsePauliOp) -> SparsePauliOp: """ Collect the coefficient of identical Pauli strings for example: 1.5*"IXZI"-0.3*"IXXZ"+0.4*"IXZI", would result, in: 1.9*"IXZI"-0.3*"IXXZ". The function correctly ignores "I" when comparing strings, and sets the correct `num_qubits` terms with abs(coefficient) SparsePauliOp: p_int, idx = pair term = SparsePauliTerm([IndexedPauli(Pauli(p_int), idx)], 1.0) return SparsePauliOp([term], num_qubits=idx + 1) paulistrings = [] for key, coeff in pauliterms.items(): if np.abs(coeff) < TOLERANCE: continue key_op = reduce(mul, (single_qubit_op(pair) for pair in key)) paulistrings.append(coeff * key_op) if not paulistrings: return SparsePauliOp([], spo.num_qubits) # Sum all strings return reduce(add, paulistrings) ``` Next, we define the `grad_energy` function, which receives an operator, `op`, and Hamiltonian, `hamiltonian`, as `SpasePauilOp`s, and evaluates the absolute value of the expectation value of the commutation relation $\nabla_{\vec{\theta}} E_j=i\langle\psi^{(k)}|[H,O_j] |\psi^{(k)}\rangle~~,$ with respect to an execution session `es` and optimization parameters `params`. ```python theme={null} def grad_energy( op: SparsePauliOp, hamiltonian: SparsePauliOp, es: ExecutionSession, params: list[float], isFirstIteration: bool, ) -> float: h_op_comm = -1j * commutator(hamiltonian, op) if isFirstIteration: return es.estimate(h_op_comm).value return es.estimate(h_op_comm, {"params": params}).value ``` # ## Defining the Example System and Operator Pool ```python theme={null} # Hamiltonian HAMILTONIAN = ( -0.1 * Pauli.Z(0) * Pauli.Z(1) + 0.2 * Pauli.X(0) - 0.3 * Pauli.Z(1) * Pauli.Z(2) + 0.4 * Pauli.Z(1) - 0.1 * Pauli.Y(2) * Pauli.Z(3) + 0.2 * Pauli.Y(2) - 0.5 * Pauli.X(2) * Pauli.Z(3) + 0.2 * Pauli.Z(3) - 0.1 * Pauli.Y(1) + 0.3 * Pauli.I(0) ) NUM_QUBITS = 4 # operators single_x_ops = [Pauli.X(i) for i in range(4)] single_y_ops = [Pauli.Y(i) for i in range(4)] single_z_ops = [Pauli.Z(i) for i in range(4)] xx_ops = [Pauli.X(i) * Pauli.X(j) for i in range(4) for j in range(4) if i != j] yy_ops = [Pauli.Y(i) * Pauli.Y(j) for i in range(4) for j in range(4) if i != j] zx_ops = [Pauli.Z(i) * Pauli.X(j) for i in range(4) for j in range(4) if i != j] yx_ops = [Pauli.Y(i) * Pauli.X(j) for i in range(4) for j in range(4) if i != j] # operator pool op_pool = single_x_ops + single_y_ops + xx_ops + yy_ops + zx_ops + yx_ops # Ground state energy Hamiltonian_matrix = hamiltonian_to_matrix(HAMILTONIAN) eigvals, eigvecs = np.linalg.eig(Hamiltonian_matrix) ground_state_energy = np.real(np.min(eigvals)) ``` ```python theme={null} print(f"Ground state energy: {ground_state_energy}") ``` **Output:** ``` Ground state energy: -1.189380715792109 ``` # ## Main Program Before defining the ADAPT-VQE main function, we first define the `adapt_vqe_ansatz` function, which prepares the product state of phases, $|\psi^{\text{ADAPT}}(\vec{\theta})) \rangle = \Pi_{n=1}^{K} e^{-i\theta_k A_k}|\psi_{\text{ref}}\rangle$, defined by the optimization parameters, `thetas`, and the associated operators of the operator pool, $\cal O$. The function builds the state, phase by phase, utilizing a helper function `adapt_layer`. The reference state is taken to be $$ |\psi_{\text{ref}}\rangle=|0^n \rangle~~, $$ where $n=4$ is the number of qubits. ```python theme={null} @qfunc def adapt_layer(idx: int, theta: CReal, qba: QArray): suzuki_trotter(op_pool[idx], theta, 1, 1, qba) @qfunc def adapt_vqe_ansatz( thetas: CArray[CReal], ansatz_ops: List[int], qba: QArray, ): n = thetas.len for i in range(n): adapt_layer(ansatz_ops_indicies[i], thetas[i], qba) ``` In order to obtain accurate evaluations of the observables and obtain deterministic results, we execute the quantum program on a state vector simulator. ```python theme={null} execution_preferences = ExecutionPreferences( backend_preferences=ClassiqBackendPreferences( backend_name=ClassiqSimulatorBackendNames.SIMULATOR_STATEVECTOR ) ) ``` Following the algorithm steps, the main ADAPT-VQE performs an iterative calculation, each time adding another phase to the ansatz, thereby increasing the ansatz length, `k`, (starting from $k=1$) until the gradient converges and satisfies $|\nabla E|/k <$`tol`. If the procedure does not converge, we limit the number of iterations by `MAX_ITERATIONS`. Each iteration begins with a preparation of the ansatz state $\Pi_{k=1}^{K} e^{-i\theta_k A_k}|\psi_{\text{ref}}\rangle$ for the current $K$. The state preparation is performed by the first `main` quantum function, the quantum circuit is synthesized, and the optimization parameters are optimized by the `es` execution call and saved as a Python list in `params`. The `params` parameters, along with the operators represented by the associated operator indices stored in the `ansatz_ops`, completely define the ansatz state. Finally, the elements of the gradient, $\nabla_{\vec{\theta}}E$, are evaluated by the execution call using the `grad_energy` function. A convergence check is made; if the ansatz state converged or the number of iterations exceeds the limit, the iterative procedure is terminated; otherwise, the iteration cycle continues. ```python theme={null} # maximum number of iterations MAX_ITERATIONS = 10 ansatz_ops_indicies: list[int] = [] # stores the gradient norms gradient_norms = [] # stores the optimized parameters in each iteration params_history = [] # ansatz state energy energies = [] # circuit depths and widths circuit_depths = [] circuit_widths = [] # gradient norm tolerance for stopping criterion tol: Final[float] = 0.001 isFirstIteration = True K = 0 for i in range(MAX_ITERATIONS): # loop until gradient norm exceeds tolerance # (break-ing out of the loop) or the maximum number of iterations is reached # number of layers ## compute gradients of operator pool elements @qfunc def main( params: CArray[CReal, K], v: Output[QArray[QBit, NUM_QUBITS]], # type: ignore ): allocate(v) # build the ansatz state only from the second iteration, otherwise return the reference state if i > 0: adapt_vqe_ansatz(params, ansatz_ops_indicies, v) if i == 0: params = 0 qprog_grads = synthesize(main) with ExecutionSession(qprog_grads, execution_preferences) as es: gradients = np.array( [ grad_energy(mp, HAMILTONIAN, es, params, isFirstIteration) for mp in op_pool ] ) isFirstIteration = False # evaluate the scaled gradient norm scaled_gradient_norm = np.linalg.norm(gradients) / len(gradients) gradient_norms.append(scaled_gradient_norm) g_idx = np.argmin(gradients) ansatz_ops_indicies.append(g_idx) @qfunc def main(params: CArray[CReal, K], v: Output[QArray[QBit, NUM_QUBITS]]): allocate(v) adapt_vqe_ansatz(params, ansatz_ops_indicies, v) qprog = synthesize(main) circuit_widths.append(qprog.data.width) circuit_depths.append(qprog.transpiled_circuit.depth) K = len(ansatz_ops_indicies) # initial optimization parameters for the VQE re-optimization initial_params = np.linspace(0, 1, K).tolist() with ExecutionSession(qprog, execution_preferences) as es: optimization_results = es.minimize( cost_function=HAMILTONIAN, # Hamiltonian problem initial_params={"params": initial_params}, max_iteration=100, ) params = optimization_results[-1][1]["params"] params_history.append(params) energy = optimization_results[-1][0] energies.append(energy) print( f"Iteration {i+1} completed; selected operator index: {g_idx}; partial derivative: {np.real(gradients[g_idx])}; current energy: {energy}" ) # convergence check positive_gradient = np.all(gradients >= 0) if scaled_gradient_norm < tol: print(f"Gradient converged") break elif i == MAX_ITERATIONS - 1: print(f"Reached maximum number of iterations") break elif positive_gradient: print(f"Optimization reached a minima") break ``` **Output:** ``` Iteration 1 completed; selected operator index: 6; partial derivative: -1.0; current energy: 0.5 Iteration 2 completed; selected operator index: 6; partial derivative: -0.9999399950001001; current energy: 0.21690481532906059 Iteration 3 completed; selected operator index: 39; partial derivative: -0.1987502812343046; current energy: 0.21690482371265463 Iteration 4 completed; selected operator index: 20; partial derivative: -0.18783601422763072; current energy: -0.38461421370076104 Iteration 5 completed; selected operator index: 45; partial derivative: -0.1488457198127166; current energy: -0.584353693801049 Iteration 6 completed; selected operator index: 6; partial derivative: -0.7058905898639278; current energy: -0.6468182838487597 Iteration 7 completed; selected operator index: 50; partial derivative: -0.7628700586126583; current energy: -0.7064952091448862 Iteration 8 completed; selected operator index: 6; partial derivative: -1.0591603104849427; current energy: -0.5143836023499864 Iteration 9 completed; selected operator index: 50; partial derivative: -0.46278789280498533; current energy: -0.6629077394236031 Iteration 10 completed; selected operator index: 6; partial derivative: -0.5210529087183913; current energy: -0.707848681231239 Reached maximum number of iterations ``` ## Results and Discussion Optimized ansatz summary ```python theme={null} min_energy_idx = np.argmin(np.array(energies)) optimal_energy_adapt_vqe = energies[min_energy_idx] optimized_parameters_adapt_vqe = params_history[min_energy_idx] optimized_op_idxs = ansatz_ops_indicies[: (min_energy_idx + 1)] print(f"Optimized parameters: {np.round(optimized_parameters_adapt_vqe,4)}") print(f"Optimized energy: {energies[min_energy_idx]}") error_adapt_vqe = abs( 100 * (optimal_energy_adapt_vqe - ground_state_energy) / ground_state_energy ) print(f"Error: {np.round(error_adapt_vqe,1)}%") ``` **Output:** ``` Optimized parameters: [ 0.5848 0.4323 1.5439 -0.0094 0.8553 0.5134 1.7361 0.6801 0.7741 0.9886] Optimized energy: -0.707848681231239 Error: 40.5% ``` # ## Energy Graph ```python theme={null} plt.plot(energies, label="ansatz state") plt.plot(np.array(len(energies) * [ground_state_energy]), label="ground state energy") plt.xlabel("Iterations") plt.ylabel("Energy") plt.title("Ansatz State Energy") plt.legend() plt.show() ``` output # ## Convergence Graph ```python theme={null} plt.plot(gradient_norms, label="gradient norm") plt.plot(np.array(len(gradient_norms) * [tol]), label="tolerance") plt.xlabel("Iterations") plt.ylabel("Gradient norm") plt.title("Gradient norm convergence") plt.legend() plt.show() ``` output # ## Circuit Information The circuit width and depth are: ```python theme={null} # circuit width print(f"circuit width: {circuit_widths[min_energy_idx]}") # circuit depth print(f"circuit depth: {circuit_widths[min_energy_idx]}") ``` **Output:** ``` circuit width: 4 circuit depth: 4 ``` # ## Summary ADAPT-VQE Ansatz For the chosen operator pool and classical optimization algorithm, the optimal ansatz state has $10$ parameters converged to a result $E_{\text{min}}= -0.707848681231239 $ The ansatz operators, indexed by their associated index in the operator pool: $[6,6,39,20,45,6,50,6,50,6]$. The optimization parameters are: $$ \vec{\theta} = [0.5848, 0.4323, 1.5439, -0.0094, 0.8553, 0.5134, 1.7361, 0.6801, 0.7741, 0.9886]^T~~, $$ The ansatz state can be constructed with a quantum circuit with a circuit width of $4$ and circuit depth of $4$. ## Modified ADAPT-VQE Ansatz We provide a modified version of the algorithm, where the algorithm starts with a pre-optimization with respect to a chosen operator. The algorithm showed improved results for the considered Hamiltonian and operator pool. ```python theme={null} # maximum number of iterations MAX_ITERATIONS = 10 ansatz_ops_indicies: list[int] = [] # start with default parameter ansatz_ops_indicies.append(10) # stores the gradient norms gradient_norms = [] # stores the minimal gradient element in each step picked_derivatives = [] # ansatz state energy energies = [] # parameter history params_history = [] # circuit depths and widths circuit_depths = [] circuit_widths = [] # gradient norm tolerance for stopping criterion tol: Final[float] = 0.001 # in this variation of the algorithm, there is no need for a special treatment of the first iteration isFirstIteration = False for i in range( MAX_ITERATIONS ): # loop until gradient norm exceeds tolerance (break-ing out of the loop) # number of layers (initially=1) K = len(ansatz_ops_indicies) # trace and history for analysis and plotting @qfunc def main(params: CArray[CReal, K], v: Output[QArray[QBit, NUM_QUBITS]]): allocate(v) adapt_vqe_ansatz(params, ansatz_ops_indicies, v) qprog = synthesize(main) circuit_widths.append(qprog.data.width) circuit_depths.append(qprog.transpiled_circuit.depth) # initial optimization parameters for the VQE re-optimization initial_params = np.linspace(0, 1, K).tolist() with ExecutionSession(qprog, execution_preferences) as es: optimization_results = es.minimize( cost_function=HAMILTONIAN, # Hamiltonian problem initial_params={"params": initial_params}, max_iteration=100, ) params = optimization_results[-1][1]["params"] params_history.append(params) energy = optimization_results[-1][0] energies.append(energy) ## Compute gradients of operator pool elements @qfunc def main( params: CArray[CReal, K], v: Output[QArray[QBit, NUM_QUBITS]], # type: ignore ): allocate(v) adapt_vqe_ansatz(params, ansatz_ops_indicies, v) qprog_grads = synthesize(main) with ExecutionSession(qprog_grads, execution_preferences) as es: gradients = np.array( [ grad_energy(mp, HAMILTONIAN, es, params, isFirstIteration) for mp in op_pool ] ) # evaluate the scaled gradient norm scaled_gradient_norm = np.linalg.norm(gradients) / len(gradients) gradient_norms.append(scaled_gradient_norm) # convergence check if scaled_gradient_norm < tol: print(f"Gradient converged") break elif i == MAX_ITERATIONS - 1: print( f"Iteration {i+1} completed; selected operator index: {g_idx}; partial derivative: {np.real(gradients[g_idx])}; current energy: {energy}" ) print(f"Reacheck maximum number of iterations") break positive_gradient = np.all(gradients >= 0) if positive_gradient: print(f"Optimization reached a minima") break g_idx = np.argmin(gradients) picked_derivatives.append(gradients[g_idx]) print( f"Iteration {i+1} completed; selected operator index: {g_idx}; partial derivative: {np.real(gradients[g_idx])}; current energy: {energy}" ) ansatz_ops_indicies.append(g_idx) ``` **Output:** ``` Iteration 1 completed; selected operator index: 2; partial derivative: -0.5999999962884589; current energy: 0.3000000018557699 Iteration 2 completed; selected operator index: 4; partial derivative: -0.3999999999998979; current energy: 0.17573593351317066 Iteration 3 completed; selected operator index: 50; partial derivative: -0.6325932167777434; current energy: -0.14787085528494587 Iteration 4 completed; selected operator index: 42; partial derivative: -0.19999999893498477; current energy: -0.3580880911310245 Iteration 5 completed; selected operator index: 45; partial derivative: -0.19352465309641967; current energy: -1.1671907029792876 Iteration 6 completed; selected operator index: 39; partial derivative: -0.4118947318009357; current energy: -1.091179784345635 Iteration 7 completed; selected operator index: 21; partial derivative: -0.17398470514855552; current energy: -1.1804771751890988 Iteration 8 completed; selected operator index: 51; partial derivative: -0.36752952620020923; current energy: -0.9960267166185478 Iteration 9 completed; selected operator index: 4; partial derivative: -0.2149596926385491; current energy: -1.1377253083959993 Iteration 10 completed; selected operator index: 4; partial derivative: 0.0101716932596762; current energy: -0.993454636616868 Reacheck maximum number of iterations ``` ## Results and Discussion Ansatz state summary ```python theme={null} min_energy_idx = np.argmin(np.array(energies)) optimal_energy_modified_adapt_vqe = energies[min_energy_idx] optimized_parameters_modified_adapt_vqe = params_history[min_energy_idx] optimized_op_idxs = ansatz_ops_indicies[: (min_energy_idx + 1)] print(f"Optimized parameters: {np.round(optimized_parameters_modified_adapt_vqe,4)}") print(f"Optimized energy: {optimal_energy_modified_adapt_vqe}") error_modified_adapt_vqe = abs( 100 * (optimal_energy_modified_adapt_vqe - ground_state_energy) / ground_state_energy ) print(f"Error: {error_modified_adapt_vqe}%") ``` **Output:** ``` Optimized parameters: [ 1.5757 1.8084 0.7077 0.5244 -0.1062 -0.1099 1.5939] Optimized energy: -1.1804771751890988 Error: 0.7485862587809506% ``` # ## Energy Graph ```python theme={null} plt.plot(energies, label="ansatz state") plt.plot(np.array(len(energies) * [ground_state_energy]), label="ground state energy") plt.xlabel("Iterations") plt.ylabel("Energy") plt.title("Ansatz State Energy") plt.legend() plt.show() ``` output # ## Convergence Graph ```python theme={null} plt.plot(gradient_norms, label="gradient norm") plt.plot(np.array(len(gradient_norms) * [tol]), label="tolerance") plt.xlabel("Iterations") plt.ylabel("Gradient norm") plt.title("Gradient norm convergence") plt.legend() plt.show() ``` output # ## Circuit Information The circuit width and depth are: ```python theme={null} # circuit width print(f"circuit width: {circuit_widths[min_energy_idx]}") # circuit depth print(f"circuit depth: {circuit_depths[min_energy_idx]}") ``` **Output:** ``` circuit width: 4 circuit depth: 19 ``` # ## Summary Modified ADAPT-VQE Ansatz For the chosen operator pool and classical optimization algorithm, the optimal ansatz state has $7$ parameters converged to a result $E_{\text{min}}=-1.1804771$ The ansatz operators, indexed by their associated index in the operator pool: $[2,4,50,42,45,39,21]$. The optimization parameters are: $$ \vec{\theta} = [1.5757, 1.8084, 0.7077, 0.5244, -0.1062, -0.1099, 1.5939]^T~~, $$ The ansatz state can be constructed with a quantum circuit with a circuit width of $4$ and circuit depth of $19$. ## Comparison to the VQE Algorithm We now validate the results of the ADAPT-VQE by comparing them to the outcome of the standard VQE algorithm with ten optimization parameters ($K=10$), and picking the operator pool as the first ten even indices (the choice is arbitrary and independent of the Hamiltonian). We fix the ansatz state by considering a product form of phases, $$ |\psi_{\text{VQE}}\rangle = \Pi_{k=1}^{K} e^{-i \theta_k O_k}|0^n\rangle~~, $$ where $n=4$, for the presented test case. The operators $\{O_k\}$ are determined by their operator pool indices in `vqe_ansatz_ops_indicies` and the associated parameters $\{\theta_k\}$ are optimized by applying the VQE algorithm. ```python theme={null} K = 10 modular_range = [i * 2 % len(op_pool) for i in range(K)] modular_range = [0] + modular_range vqe_ansatz_ops_indicies = modular_range @qfunc def main(params: CArray[CReal, K], v: Output[QArray[QBit, NUM_QUBITS]]): allocate(v) n = params.len adapt_vqe_ansatz(params, vqe_ansatz_ops_indicies, v) qprog_vqe = synthesize(main) with ExecutionSession(qprog_vqe) as es: vqe_optimization_results = es.minimize( cost_function=HAMILTONIAN, # Hamiltonian problem initial_params={"params": [0] * K}, max_iteration=100, ) vqe_minimal_energy = vqe_optimization_results[-1][0] print(f"VQE minimal energy: {vqe_minimal_energy}") # optimized thetas (rounded) vqe_optimized_thetas = [vqe_optimization_results[-1][1]["params"]][0] vqe_rounded_thetas = [ round(vqe_optimized_thetas[i], 3) for i in range(len(vqe_optimized_thetas)) ] print(f"VQE optimized thetas: {vqe_rounded_thetas}") print( f"error: {abs(100*(vqe_minimal_energy-ground_state_energy)/ground_state_energy)}%" ) ``` **Output:** ``` VQE minimal energy: -1.16826171875 VQE optimized thetas: [1.676, -0.304, 0.885, 0.535, 1.502, 1.376, 0.022, -0.1, -0.008, 1.004] error: 1.7756296837252943% ``` # ## Circuit Information ```python theme={null} # circuit width print(f"circuit width: {qprog_vqe.data.width}") # circuit depth print(f"circuit depth: {qprog_vqe.transpiled_circuit.depth}") ``` **Output:** ``` circuit width: 4 circuit depth: 26 ``` # ## Summary VQE Ansatz The VQE ansatz is characterized by: Optimized optimization parameters: $$ \vec{\theta} = [1.536, -0.564, 1.271, 0.144, 1.94, 1.511, -0.215, -0.062, -0.391, 0.855]^T $$ The ansatz operators are originally set (an arbitrary choice): $[0,2,4,6,8,10,12,14,16,18]$. and minimal energy $\langle\psi^{\text{VQE}}|H |\psi^{\text{VQE}}\rangle$: $-0.96816$. The ansatz state can be constructed with a quantum circuit with a circuit width of $4$ and circuit depth of $26$. ## Analysis and Summary By comparing the results obtained with ADAPT-VQE, modified ADAPT-VQE and standard VQE, and exact diagonalization, we observe that for an equal number of variational parameters, modified ADAPT-VQE converges to an ansatz that yields a more accurate estimate of the ground-state energy, $E_{\text{g.s}}= -1.189380715792109$. Specifically, modified ADAPT-VQE achieves an energy within approximately $~0.7$% of the exact value. In contrast, the original ADAPT-VQE achieves a minimal energy which is $40$% above the true ground state energy. The VQE ansatz - whose operator structure is chosen independently of the Hamiltonian - converges to an optimal energy that remains $18.6$% above the true ground-state energy. The result highlights the potential of an ADAPT-VQE type ansatz, which is constructed iteratively based on the energy gradient, that is directly determined by the Hamiltonian under consideration. As a consequence, only operators that most effectively lower the energy are included in the ansatz, leading to faster and more accurate convergence. The ADAPT-VQE achieves a better accuracy, with a smaller circuit depth ($4$ and $19$ compared to $26$ for the VQE). This contrasts with a general, Hamiltonian-independent ansatz, which lacks such targeted optimization and therefore exhibits inferior convergence performance and tends to require larger circuit depths to achieve the same accuracy. Importantly, the large difference between the two variations of ADAPT-VQE proposed showcases that the algorithm doesn't guarantee better results. A clever choice of the operator pool and the initial state can potentially greatly improve the algorithm's performance. ## References
\[1]: [Grimsley, H. R., Economou, S. E., Barnes, E., & Mayhall, N. J. (2019). An adaptive variational algorithm for exact molecular simulations on a quantum computer. Nature communications, 10(1), 3007.](https://arxiv.org/abs/1812.11173) \[2]: [Tilly, J., Chen, H., Cao, S., Picozzi, D., Setia, K., Li, Y., ... & Tennyson, J. (2022). The variational quantum eigensolver: a review of methods and best practices. Physics Reports, 986, 1-128.](https://arxiv.org/abs/2111.05176) # Preparation of Fermionic Gaussian States Source: https://docs.classiq.io/explore/algorithms/quantum_state_preparation/fermionic_gaussian/fermionic_gaussian Open this notebook in GitHub to run it yourself > **Fermionic Gaussian states** are ground states of quadratic fermionic Hamiltonians [\[1\]](#fermionic-gaussian-state). They are widely used as initial states for simulations of electronic systems and as mean-field ansatz states, including Hartree-Fock states, BCS superconductors, and filled Fermi seas. This notebook prepares such states on a quantum register from the matrices defining the underlying quadratic Hamiltonian $$ H = \sum_{\mu \nu} c_\mu^\dagger h_{\mu \nu} c_{\nu} + \frac{1}{2}\sum_{\mu \nu}\Delta_{\mu \nu}\left(c_\mu^\dagger c_\nu^\dagger + \text{h.c.}\right)~~, \tag{1} $$ > where $c_{\mu}^\dagger / c_{\mu}$ are the (particle) fermionic creation / annihilation operators (also termed Dirac operators) and $h_{\mu \nu}, \Delta_{\mu \nu}$ are the coupling frequencies ($\hbar = 1$ notation throughout the notebook). > > The algorithm treats the problem in the following way: > > * **Input:** A Hermitian one-body matrix $h$ ($M \times M$) and a antisymmetric pairing matrix $\Delta$ ($M \times M$), where $M$ is the number of fermionic modes. For number-conserving Slater determinants ($\Delta = 0$), a target particle number $N$ is also specified. > * **Output:** An $M$-qubit register (under the Jordan-Wigner mapping) prepared in the ground state $|\psi_{\rm g.s.}\rangle$ of $H$, up to a global phase. > > **Complexity:** For $M$ fermionic modes, the classical pre-processing costs $O(M^3)$ (diagonalization of an $O(M)$ matrix) and the state preparation quantum circuit requires $N_G = O(M^2)$ two-qubit Givens rotations. When $\Delta =0$, the circuit depth satisfies $\le M-1$ (preparation of a Slater determinant). In general, ($\Delta \neq 0$ case) the depth is upper bounded by $2M-1$. > > *** > > **Keywords:** Fermionic Gaussian state, Slater determinant, Bogoliubov-de Gennes, Givens rotations, Jordan-Wigner, mean-field ## Overview Gaussian fermionic states are widely used as initial states of simulations of electronic systems and as mean field ansatz states [\[2\]](#google-paper). The presented algorithm was introduced by Jiang et al. [\[1\]](#fermionic-gaussian-state). It includes two stages: 1. **Classical pre-processing.** The quadratic Hamiltonian of $M$ fermionic modes is diagonalized to obtain a basis transformation matrix. This matrix relates the original Dirac operators $\{c_\mu, c_\mu^\dagger\}$ to the diagonal-basis quasiparticle operators $\{d_\eta, d_\eta^\dagger\}$. Following, the transformation matrix is decomposed into a sequence of $O(M^2)$ Givens rotations, these are two-mode operations which conserve the number of modes. Following, a Jordan-Wigner transformation maps the fermionic modes to a qubit representation, where the Givens rotations correspond to two-qubit gates on adjacent qubits. 1. **Quantum state preparation.** Apply the resulting two-qubit gates (encoded under the Jordan-Wigner mapping) in reverse order to the vacuum $|0^M\rangle$, yielding the desired ground state with circuit depth $\le 2M-1$. We treat two cases: * **Number-conserving (Slater determinants):** Ground states of Hamiltonians of the form: $H = \sum_{\mu\nu=1}^M c_\mu^\dagger h_{\mu\nu} c_\nu$. The required gates are (number-conserving) Givens rotations and the algorithm reduces to a modified $QR$ decomposition of the occupied-orbital matrix. Demonstrated on a $1$-D periodic lattice. * **Ground state of General quadratic Hamiltonian:** In addition to the particle conserving terms, $H$ also contains pairing terms $\frac{1}{2}\sum_{\mu\nu}\Delta_{\mu\nu} c_\mu^\dagger c_\nu^\dagger + \text{h.c.}$, which break particle-number conservation. Here, $\Delta_{\mu\nu}$ are frequencies. The gate set is augmented with particle-hole transformations on the last mode (an $X$ gate on the last qubit), and each tuple becomes a *phased* Givens rotation $e^{i\varphi c_j^\dagger c_j}\,e^{\theta(c_i^\dagger c_j - c_j^\dagger c_i)}$. Demonstrated on a small $n=2$ example whose ground state is verified by direct exact diagonalization in the $4$-dimensional Fock space. ## Introduction Fermionic Gaussian States (FGS) are a class of many-body quantum states of fermions, which are completely characterized by their "second moments" (two-point correlation functions). They underpin many approximate techniques in condensed matter physics and quantum chemistry. In these approaches, the interacting many-electron problem is treated by determining the optimal set of effective non-interacting orbitals that best approximate the full system \[[3](#numerical)]. The structure of FGSs is governed by the anticommutation relations of the Dirac operators $$ \{c_\mu, c_\nu^\dagger \} = \delta_{\mu \nu}~~,~~\{c_\mu, c_\nu \} = \{c_\mu^\dagger, c_\nu^\dagger \} =0~~, $$ where $\{a,b\} = ab + ba$, while $c_\mu$ and $c_\nu^\dagger$ annihilate and create a fermion in the $\mu$'th fermionic mode, respectively. Important fermionic gaussian states include, * The vacuum state $|\text{vac}\rangle$ (defined as the state for which $c_{\mu}|\text{vac}\rangle=|\text{vac}\rangle$ for all $\mu$). * Slater Determinants: $\Pi_{\mu=1}^M b_\mu^\dagger |\text{vac}\rangle$, where $b_\mu$ are linear combinations of $\{c_\mu\}$. These include Hartree-Fock states and a Filled Fermi sea. * BCS states: $$ \propto \exp \left( \sum_{\mu \nu}\Delta_{\mu \nu}c_\mu^\dagger c_\nu^\dagger \right)|\text{vac}\rangle~~. $$ These states, named after Nobel laureates Bardeen, Cooper and Schrieffer, describe the pairing of electrons (forming Cooper pairs), which leads to superconducting charge flow in solid state materials. * Ground states of quadratic Hamiltonians (see overview). As ground states of such Hamiltonians, they have an important role in quantum simulations of fermionic systems. For example, in the celebrated [Fermi-Hubbard model](https://github.com/Classiq/classiq-library/blob/main/applications/physical_systems/fermi_hubbard_model_1D/fermi_hubbard_1D.ipynb), simulations are usually initialized in easy to prepare fermionic gaussian state, or as ground states of mean-field Hamiltonians. Gaussian fermionic states have a number of equivalent properties (these properties can also constitute as alternative definitions of these states) \[[3](#numerical)]: Consider an $M$-mode fermionic gaussian state $\rho$ * $\rho$ can be represented (or defined) as the exponential of a quadratic form in the fermionic operators: $$ \rho = Z^{-1} \exp(-\mathbf{c}^T{\cal H} \mathbf{c})~~, $$ where $\rho$ is the density matrix, $\mathbf{c} = \{c_1,\dots,c_M \}^T$ and $\cal H$ is an Hermitian matrix. * All higher order correlation functions (i.e. $\langle c_1^\dagger...c_M^\dagger c_{M+1}\dots c_{2M} \rangle$) are determined by [Wick's theorem](https://en.wikipedia.org/wiki/Wick%27s_theorem) from the two-point correlators. * They maximise the [von-Neumann entropy](https://en.wikipedia.org/wiki/Von_Neumann_entropy) given the second order expectation values. These constitute the elements of the correlation matrix: $\Gamma = \langle \mathbf{c} \mathbf{c}^\dagger \rangle.$ * Alternatively, their properties can be fully described by the [covariance matrix](https://en.wikipedia.org/wiki/Covariance_matrix), which is related to the correlation matrix, $\gamma = -i \Omega (2\Gamma -\mathbb{I}\Omega^\dagger)$, where $\Omega$ is a transformation to the fermionic Majorana operators, defined below. ## Classiq Implementation We will next describe the classical calculation involved in deriving the state preparation circuit, and implement the circuit with Classiq. Throughout this notebook, we will use the Jordan-Wigner transformation, which maps fermionic creation and annihilation operators to qubit operators suitable for quantum computing. Under this encoding, Fock states are represented directly as computational basis states: each occupation state maps to the same bitstring. Therefore, particle number and particle conservation have the same meaning in the qubit representation. In addition, we can define certain unitaries directly by their action on the encoded Fock space. For example, a unitary that maps $|01\rangle$ to $|10\rangle$, corresponds to moving a particle from the second mode to the first, i.e., annihilating it in one mode and creating it in the other. We begin by importing the required software packages and defining global constants ```python theme={null} !pip install -qq -U "classiq[chemistry]" ``` **Output:** ``` ERROR: Could not install packages due to an OSError: [Errno 2] No such file or directory   ``` ```python theme={null} import numpy as np import pandas as pd import scipy.linalg from openfermion.linalg.givens_rotations import ( double_givens_rotate, fermionic_gaussian_decomposition, givens_decomposition, givens_rotate, swap_columns, ) from scipy import linalg from classiq import * # Numerical tolerance used for assertions throughout the notebook. TOLERANCE = 1e-10 ``` # ## Preparation of a Slater Determinant State A Slater-Determinant is the ground state of a quadratic Fermionic Hamiltonian which conserves the number of particles. In the following section, we define such an Hamiltonian and prepare its ground state. We begin, by demonstrating a simple example utilizing only numpy data structures. Following, we introduce Classiq quantum functions which prepare the desired fermionic gaussian state. The form of general number conserving quadratic fermionic Hamiltonian is ($\Delta = 0$ in Eq. (1)) $$ H = \sum_{\mu,\nu=1}^M c_\mu^\dagger h_{\mu\nu}c_\nu~~,~~~~\tag{2} $$ where $h$ is known as the **one-body Hamiltonian**. Remarkably, this implies that the diagonalization of $h$, (an $M$ by $M$ matrix) allows us to prepare the ground state. The idea is the following: We assume that the number of particles $N$ is known. The Hamiltonian conserves the number of particles, that is, it commutes the number operator $$ \sum_{\mu = 1}^M c_\mu^\dagger c_\mu~~. $$ Diagonalizing the single particle Hamiltonian $h= \bar{Q} D \bar{Q}^\dagger$, where $\bar{Q}$ is an $M$-by-$M$ unitary matrix and $D = \text{diag}(\epsilon_1,...,\epsilon_M)$, where $\epsilon_1 \leq \epsilon_2 \leq \cdots \leq \epsilon_M$, we obtain $H=\sum_{k=1}^M \epsilon_k d_k^\dagger d_k$, where $$ d_\eta^\dagger= \sum_{\mu=1}^{M} \bar{Q}_{\mu \eta}c_\mu^\dagger~~. \tag{3} $$ Then since each $d_\eta^\dagger$ is a linear combination of the Dirac operators $\{c_\mu^\dagger\}$, it excites only a single particle, and the number conservation ($N$ particles) implies that the ground state can be written as $$ |\psi_{g.s}\rangle = \Pi_{k=1}^N d_k^\dagger |0^M\rangle ~~. $$ The basis transformation can also be expressed in terms of the single-particle transformation, $U\mathbf{c}^\dagger = \mathbf{d}^\dagger~,$ where we collected the creation operators to form an operator valued vector. Each element of the vector corresponds to a transformation by a unitary operator $\cal U$, ${\cal U} c_j^\dagger {\cal U}^\dagger = U_{[j,:]} \mathbf{d}^\dagger = d_j^\dagger$. Crucially, the cost of evaluating $U$ and diagonalizing of $h$ scales only polynomially with the number of fermionic modes, $O(M^3)$ and can be efficiently done on a classical computer. In order to prepare the desired ground state we focus on a part of $\bar{Q}$ which corresponds to the occupied modes, and denote the $N$ by $M$ matrix describing these modes by $Q = (\bar{Q}^T)_{[{\text{occupied modes}},:]}$. This identification leads to an alternative form for Eq. (3), for the occupied modes we have $$ d_\eta^\dagger= \sum_{\mu=1}^{M} {Q}_{\eta \mu}c_\mu^\dagger~~. $$ An efficient quantum circuit for the ground state preparation can be obtained by a modified QR decomposition of $Q$, using a product of elementary two-mode rotations, called Givens rotations. Each rotation can be expressed as $$$ \begin{pmatrix} \mathcal{G}c_j^\dagger\mathcal{G}\\ \mathcal{G}c_k^\dagger\mathcal{G} \end{pmatrix} = G_{jk}(\theta,\varphi)\, \begin{pmatrix} c_j^\dagger\\ c_k^\dagger \end{pmatrix}~~, $ and the form of a Givens rotation is $$G_\{jk\}(\theta,\varphi) = \begin\{pmatrix\} \cos(\theta) & -e^\{i\varphi\}\sin(\theta)\\ \sin(\theta) & e^\{i\varphi\}\cos(\theta) \end\{pmatrix\}~~. \tag\{4\} $$$ Within the algorithm, the Givens rotations operate on the subspace with a defined number of excitations, therefore, they conserve the number of particles. To simplify the decomposition we begin by utilizing the invariance of the Slater determinant (up to a global phase) under the mapping ${Q}\rightarrow V{Q}$, where $V$ is a unitary transformation. For the transformation of basis to be valid we require that the first $N$ rows of $VQ$ are equal to $U$, or alternatively $$ V\{Q\}U^\dagger = (I_N, \boldsymbol\{0\})~~. $$ Each Givens transformation operates only on two columns, and its parameters, $\theta$ and $\phi$, are set so as to nullify elements in the upper right part of the matrix. Due to the orthogonality of the rows of $Q$, some transformations nullify more than a single matrix element. As a result, the total number of required Givens transformations is $N_G = N(M-N)$, where $N$ is the number of electrons and $M$ is the number of fermionic modes. The diagonalization procedure results in a product of Givens rotations $$ U = G_\{N_G\}\cdots G_2 G_1~~. $$ After the Jordan-Wigner transformation, each two-mode ($j,k$) rotations correspond to a rotation in the single particle subspace of the two qubits $j$ and $k$ ($|01\rangle$ and $|10\rangle$). This completely defines the state preparation circuit in terms of a sequence of two-qubit rotations. The gate complexity is $O(N_G) = O(N^2)$ (worst case achieved for $M=N/2$), and parallelization leads to a circuit depth of $M-1$. #### Summary of the Algorithmic Steps: 1. Evaluate the diagonalizing matrix $\bar{Q}$, and filter the non-occupied states by calculating $Q$. 2. Zero out the upper-right matrix elements of $Q$, applying the transformation $Q\rightarrow V Q$. 3. Diagonalize $VQ$ applying a sequence of Givens rotations. 4. Map the Givens rotations to corresponding quantum gates, to obtain the quantum circuit. #### Givens Rotations Example In order to understand the state preparation algorithm, we first show how Givens rotations are utilized to diagonalize a simple one-body Hamiltonian. Consider a simple $4$-by-$4$ Hermitian matrix, representing a one-body Hamiltonian: $$ h = \begin\{pmatrix\} \varepsilon_0 & t_\{01\} & 0 & t_\{03\} \\ t_\{01\} & \varepsilon_1 & t_\{12\} & 0 \\ 0 & t_\{12\} & \varepsilon_2 & t_\{23\} \\ t_\{03\} & 0 & t_\{23\} & \varepsilon_3 \end\{pmatrix\} $ $$ ```python theme={null} ## Defining the single-body Hamiltonian # Onsite energies eps0, eps1, eps2, eps3 = 0.8, -0.4, 0.3, -0.1 # Hopping amplitudes (real for simplicity) t01 = 0.25 t12 = -0.35 t23 = 0.20 t03 = 0.15 # 4x4 Hermitian one-body matrix h_{pq} h = np.array( [ [eps0, t01, 0.0, t03], [t01, eps1, t12, 0.0], [0.0, t12, eps2, t23], [t03, 0.0, t23, eps3], ], dtype=complex, ) ``` For a number-conserving quadratic Hamiltonian the diagonalizing Bogoliubov transformation reduces to ordinary diagonalization of the one-body matrix $h$. We obtain the orbital energies and the transformation matrix $\bar{Q}$ via `numpy.linalg.eigh`; the rows of $\bar{Q}^T$ are the diagonal-basis modes, and the matrix $Q$ describing the Slater determinant is the subset of those rows corresponding to the occupied orbitals (here, the modes with negative single-particle energy). ```python theme={null} orbital_energies, Qbar = np.linalg.eigh(h) transformation_matrix = Qbar.T E = orbital_energies # alias used downstream # Take the occupied orbitals to be those with negative single-particle energy. occupied_orbitals = np.where(orbital_energies < 0.0)[0] slater_determinant_matrix = transformation_matrix[occupied_orbitals] ``` We extract the Givens rotations utilizing OpenFermion's `givens_decomposition`, returning a list of tuples: `[(G_1,G_2),(G_3,),...]`. Each tuple includes the Givens rotations which can be operated in parallel. The Givens rotations are encoded as a tuple: $G_k = (i_k,j_k,\theta_k,\phi_k)$, where $i_k$ and $j_k$ are the columns of $U^\dagger$ which the Givens rotation $G_k^\dagger$ is operated on from the right (see calculation below). ```python theme={null} rotations, V, diag = givens_decomposition(slater_determinant_matrix) ``` We introduce two utility functions that classically replay the decomposition: `build_U_from_rotations` reassembles the unitary $U$ as a product of Givens rotations, and `build_state_from_rotations` applies the Givens rotations in single-particle space starting from a reference vector with the first $N$ entries equal to $1$. ```python theme={null} def build_U_from_rotations(M: int, rotations) -> np.ndarray: """ Reconstruct U (M x M) from OpenFermion's `givens_rotations` list. OpenFermion applies these to columns during decomposition; updating columns by right-multiplying with G^\dagger reproduces the same effect. """ U_dagger = np.eye(M, dtype=complex) for parallel_ops in rotations: for i, j, theta, phi in parallel_ops: c, s, e = np.cos(theta), np.sin(theta), np.exp(1j * phi) G = np.array([[c, -e * s], [s, e * c]], dtype=complex) cols = U_dagger[:, [i, j]] U_dagger[:, [i, j]] = cols @ G.conj().T return U_dagger.conj().T def build_state_from_rotations(M: int, N: int, circuit_description: list) -> np.ndarray: """Classical replay of the slater state preparation in the single-particle subspace. Applies a phased Givens rotation [[c, -e^{i phi} s], [s, e^{i phi} c]] to entries (i, j) of the M-vector `v`, starting from the reference state with the first N entries set to 1. """ v = np.zeros((M,), dtype=complex) v[:N] = np.ones_like(v[:N]) for parallel_ops in circuit_description: for i, j, theta, phi in parallel_ops: c, s, e = np.cos(theta), np.sin(theta), np.exp(1j * phi) G = np.array([[c, -e * s], [s, e * c]], dtype=complex) v[[i, j]] = G @ v[[i, j]] return v ``` Next, we decompose $U$ into Givens rotations: $$ U = G_{N_G},\dots,G_1 $$ and verify that $V Q U^\dagger = (I,\mathbf{0})$. ```python theme={null} Q = np.asarray(slater_determinant_matrix, dtype=complex) n, m = Q.shape U = build_U_from_rotations(m, rotations) # Check V Q.T U^\dagger = D where D has diag entries in first m columns and zeros elsewhere D = np.zeros((n, m), dtype=complex) D[np.arange(n), np.arange(n)] = diag A = V @ Q @ U.conj().T # Normalize to (I,0) by removing the diagonal unitary on the left: # Let Dm = diag(diag) (m x m). Then Dm^\dagger (V Q U^\dagger) = (I,0). # So define V' = Dm^\dagger V. Dm_dag = np.diag( np.conjugate(diag) ) # Dm^\dagger since diag entries are unit-modulus in theory Vprime = Dm_dag @ V I0 = np.zeros((n, m), dtype=complex) I0[:, :n] = np.eye(n, dtype=complex) B = Vprime @ Q @ U.conj().T assert np.allclose(A, D, atol=TOLERANCE) assert np.allclose(B, I0, atol=TOLERANCE) print(f"A: {A}\n") print(f"B: {B}\n") ``` **Output:** ``` A: [[-1.00000000e+00+0.j 3.97455329e-17+0.j -2.72758106e-16+0.j -1.12950755e-17+0.j] [ 9.08932067e-17+0.j -1.00000000e+00+0.j -4.52515433e-16+0.j 2.43591787e-16+0.j]] B: [[ 1.00000000e+00+0.j -4.61836778e-17+0.j 2.96337584e-16+0.j -1.79337970e-18+0.j] [-1.07623812e-16+0.j 1.00000000e+00+0.j 4.34956407e-16+0.j -2.18513361e-16+0.j]] ``` State preparation check ```python theme={null} # State preparation check for the single excitation subspace slater_determinant_matrix = transformation_matrix[[0]] rotations, V, diag = givens_decomposition(slater_determinant_matrix) circuit_description = reversed(rotations) ground_state = build_state_from_rotations(m, n, circuit_description) assert np.allclose(h @ Qbar[:, 0], E[0] * Qbar[:, 0], atol=TOLERANCE) # assert np.allclose(ground_state, Qbar[:,0], atol=TOLERANCE) ``` #### Preparation of an Electronic State Ground State We now prepare the ground state of a fermionic lattice model. This is a Slater determinant state of the form: $$ \Pi_{\mu=1}^M d_\mu^\dagger |\text{vac}\rangle~~, $$ where $d_\mu$ are linear combinations of $\{c_\mu\}$. First, we define the model, introduce global parameters, and define utility quantum functions. The model considers nearest neighbor interaction with periodic boundary conditions. Each fermionic mode $c_{j\sigma}$ has two degrees of freedom * The lattice site number, $j$ * Spin, $\sigma\in\{0,1\}$ The global parameters of the problem are therefore: * $L$ - number of lattice sites * $M$ - total number of fermionic modes ```python theme={null} L = 4 M = 2 * L ``` In addition, we introduce functions mapping between the qubit index and the lattice spin degrees of freedom. The mapping corresponds to a Jordan-Wigner transformation (fermionic operators to qubits) with an interleaved up-down (even-odd) ordering (similar to `OpenFermion` convention). The Jordan-Wigner transformation maps the states and operations of the fermionic Fock space to corresponding operators and states in the qubit Hilbert space. The particle conserving property of the Givens rotation maps to an operation on states of fixed particle number. For example, for a single excitation on two modes, the subspace is spanned by $|01\rangle$ and $|10\rangle$. Hence, a rotation within this space corresponds to annihilation/creation of a particle in the second state and creation/annihilation of one in the first. Next, we introduce quantum functions that convert the sequence of Givens rotations to a corresponding state preparation quantum circuit $U = G_{N_G}\dots G_1$. We utilize `OpenFermion`'s `givens_decomposition` to obtain the sequence of rotations. The decomposition includes a list of sets, where each set includes the rotations which can be operated in parallel, and each rotation operates on adjacent qubits. Each entry $(i, j, \theta, \varphi)$ returned by openfermion's `givens_decomposition` (and likewise `fermionic_gaussian_decomposition`) corresponds to the operator $$ M_{ij}(\theta,\varphi) \;=\; e^{i\varphi\,n_j}\;e^{\theta\,(a_i^\dagger a_j - a_j^\dagger a_i)}~~, $$ i.e. a Givens rotation in the single-particle subspace (Eq. 4) followed by a phase $e^{i\varphi}$ on mode $j$. In the four-dimensional Fock subspace spanned by modes $i,j$, $\{|\text{vac}\rangle,\, |1_i\rangle,\, |1_j\rangle,\, |1_i 1_j\rangle\}$, this acts as $$ M_{ij}(\theta,\varphi) \;=\; \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & \cos\theta & \sin\theta & 0 \\ 0 & -e^{i\varphi}\sin\theta & e^{i\varphi}\cos\theta & 0 \\ 0 & 0 & 0 & e^{i\varphi} \end{pmatrix}~~. $$ Two observations: * The central $2\times 2$ block (acting on $|1_i\rangle, |1_j\rangle$) is the transpose of Eq. (4). The transpose appears because Eq. (4) is written in the Heisenberg picture (transformation of $c_i^\dagger, c_j^\dagger$), whereas the matrix above acts on **states**. * The corner element $e^{i\varphi}$ on $|1_i 1_j\rangle$ comes from $e^{i\varphi\,n_j}$. For a Slater determinant whose pair $(i,j)$ is never doubly occupied during the circuit, this factor is invisible and a plain Givens would suffice; for a non-particle-conserving (BCS-style) state the doubly-occupied corner does get amplitude, and dropping the phase produces wrong relative signs. The `phased_givens` qfunc implements exactly the matrix above. We invoke it via `phased_givens(theta, phi, [qba[i], qba[j]])`: in Classiq's qubit-ordering convention the first qubit in the list is the least-significant bit, so the basis index seen by the `unitary` gate is $2\,n_j + n_i$, which lines up with the Fock ordering $\{|\text{vac}\rangle, |1_i\rangle, |1_j\rangle, |1_i 1_j\rangle\}$. ```python theme={null} @qfunc def phased_givens(theta: float, phi: float, qba: QArray[QBit, 2]) -> None: """ Implements the openfermion gate exp(i*phi*n_j) exp(theta*(a_i^dagger a_j - a_j^dagger a_i)) used both by openfermion's slater_determinant_preparation_circuit and gaussian_state_preparation_circuit. """ c = np.cos(theta) s = np.sin(theta) e = np.exp(1j * phi) U = [ [1, 0, 0, 0], [0, c, s, 0], [0, -e * s, e * c, 0], [0, 0, 0, e], ] unitary(U, qba) ``` ```python theme={null} @qfunc def prepare_slater_det( h: list[list[float, M], M], Nparticles: int, qba: QArray[QBit, M] ): """ Prepares the ground state associated with the single electron matrix h. The Hamiltonian satisfies H = \sum_{\mu,\nu}c_{\mu}^\dagger h_{\mu,\nu} c_{\nu} Args: h (ndarray): single electron matrix Nparticles (int): number of particles qba (list[QBit]): list of qubits """ # preparing the reference state, as the state with the first Nparticles qubits in state |1> and the rest in state |0>. repeat(Nparticles, lambda i: X(qba[i])) # diagonalizing h _, Qbar = np.linalg.eigh(h) Q = (Qbar.T)[:Nparticles, :] # occupying the N lowest energy states rotations, _, _ = givens_decomposition(Q) circuit_description = list(reversed(rotations)) for parallel_ops in circuit_description: for op in parallel_ops: i, j, theta, phi = op phased_givens(theta, phi, [qba[i], qba[j]]) ``` The circuit is verified by considering the single excitation subspace and comparing the prepared state to the analytical result. The ground state of the single excitation subspace is up to a global phase just $$ d_1^\dagger | 0^M\rangle =\sum_\mu Q_{1,\mu} c^{\dagger}_\mu | 0^M\rangle ~~, $$ which after the Jordan-Wigner transformation corresponds to the qubit state with amplitudes $$ [Q_{1,1},Q_{1,2},\dots, Q_{1,M}]~~. $$ After introducing the utility functions we can now define the Hamiltonian, and apply the Classiq functions to prepare its ground state. ```python theme={null} def sym(A): """ Symmetrizes the matrix A """ return (A + A.T) / 2 def qubit_idx(site: int, spin: int): """ Maps lattice site and spin to qubit indices, in a periodic 1D lattice with L sites and two spin states (0 and 1). Args: site (int): Lattice site index, the range [0,L-1] spin (int): Spin index, either 0 or 1 Returns: qubit_idx (int): qubit index """ return 2 * (site % L) + (spin % 2) def kinetic_energy(L: int, J: float) -> np.ndarray: """ Builds single electron matrix of nearest-neighbor hopping term, hopping strength t, associated with an L site periodic lattice. """ K = np.zeros((2 * L, 2 * L), dtype=float) for site in range(L): for spin in range(2): mu = qubit_idx(site, spin) nu = qubit_idx(site + 1, spin) K[mu, nu] = -J K = 2 * sym(K) return K def spin_potential(L: int, spin=0, parameters: tuple[float] = (1, 1, 1)) -> np.ndarray: """ Builds single electron , matrix associated with the external potential. Associated with an L site periodic lattice. """ lam, mean, std = parameters V = np.zeros((2 * L, 2 * L), dtype=float) for site in range(L): mu = qubit_idx(site, spin) V[mu, mu] = -lam * np.exp(-((site - mean) ** 2) / (2 * std**2)) return V def prepare_single_electron_hamiltonian( L: int, J: float, parameters: tuple ) -> np.ndarray: return kinetic_energy(L, J) + spin_potential(L, spin=0, parameters=parameters) ``` ```python theme={null} lam = 10.0 mean = L / 2 std = L / 6 h = prepare_single_electron_hamiltonian(L, J=1, parameters=(lam, mean, std)) N = 1 ``` ```python theme={null} @qfunc def main(qba: Output[QArray[QBit, M]]) -> None: allocate(M, qba) prepare_slater_det(h, N, qba) qprog = synthesize(main) ``` We next syntesize the quantum program, check the circuit depth and extract the amplitudes of the prepared state. ```python theme={null} qprog_state_check = synthesize(main) print("Circuit depth = ", qprog_state_check.transpiled_circuit.depth) df_state_check = calculate_state_vector(qprog_state_check) ``` **Output:** ``` Submitting job to simulator ``` **Output:** ``` Circuit depth = 25 ``` ```python theme={null} import matplotlib.pyplot as plt def get_quantum_amplitudes(df): """Single-excitation slice: pull amplitudes indexed by the occupied qubit.""" mask = np.abs(df["amplitude"]) > 1e-12 filtered = df.loc[mask, ["qba", "amplitude"]].copy() qubit_index = np.array([int(np.asarray(row).argmax()) for row in filtered["qba"]]) amps = filtered["amplitude"].to_numpy() idx = np.argsort(qubit_index) qubit_index = qubit_index[idx] amps = amps[idx] / np.linalg.norm(amps) qsol = np.zeros(M, dtype=complex) qsol[qubit_index] = amps return qsol ``` For the single excitation subspace we can easily compare the preparation of the quantum state with the exact diagonalization. To compare between the two we normalize the quantum solution so as to cancel the difference in the global phase with respect to the classical result. ```python theme={null} def quantum_solution_normalization(exact_amplitudes, df): qsol = get_quantum_amplitudes(df) global_phase = np.angle(np.vdot(qsol, exact_amplitudes)) # Correct global phase and taking the real part qsol_corrected = np.real(np.exp(1j * global_phase) * qsol) return qsol_corrected # Quantum amplitudes quantum_amplitudes = get_quantum_amplitudes(df_state_check) # Exact diagonalization E0, V0 = np.linalg.eigh(h) exact_ground_state = V0[:, 0] / np.linalg.norm(V0[:, 0]) quantum_amplitudes_corrected = quantum_solution_normalization( exact_ground_state, df_state_check ) grid = np.arange(0, M) plt.figure() plt.title("Initial State") plt.plot(grid, quantum_amplitudes_corrected, "o", label="quantum state") plt.plot( grid, exact_ground_state, "+", markersize=16, label="exact state for single excitation state", ) plt.xlim(0, M - 1) plt.ylim(-1, 1) plt.xlabel(r"Qubit Index") plt.ylabel(r"Amplitudes") plt.legend() plt.show() ``` output ```python theme={null} fidelity = np.abs(np.vdot(exact_ground_state, quantum_amplitudes)) ** 2 print(f"Fidelity of the initial state preparation: {fidelity:.3f}") assert np.isclose(fidelity, 1.0, atol=TOLERANCE), "Fidelity is not close to 1" ``` **Output:** ``` Fidelity of the initial state preparation: 1.000 ``` ### General Quadratic Hamiltonian In the general case the number of particles is not conserved under the dynamics of $H$ (Eq. (1)). This scenario is more involved than the particle-conserving case. Here, we will employ several classical transformations that will effectively diagonalize the Hamiltonian. Following, similarly to the simpler particle conserving case, we construct a unitary transformation that excites the ground state from the vacuum. The general quadratic fermionic Hamiltonian can be written in a concise form $$ H =\sum_{\mu, \nu = 1}^M h_{\mu \nu}c_\mu^{\dagger} c_\nu + \frac{1}{2} \sum_{\mu,\nu} \Delta_{\mu\nu} c_\mu^\dagger c_\nu^{\dagger} + \text{h.c} ~~, $$ where $h$ and $\Delta$ matrices are hermitian and anti-symmetric, correspondingly. For numerical diagonalization it is convenient to introduce the **Majorana operators**: $$ x_\mu = \frac{1}{\sqrt{2}}\left(c_\mu^\dagger + c_\mu \right)~~~,~~~p_\mu = \frac{i}{\sqrt{2}}\left(c_\mu^\dagger - c_\mu \right)~~, $$ for $\mu=1,\dots ,M$. The Majorana operators satisfy the anticommutation relations $$ \{x_\mu,x_\nu \}= \{p_\mu,p_\nu \} = \delta_{\mu\nu}~~,~~\{x_\mu, p_\nu\} = 0 ~~. $$ A unitary mapping relates between the two representations [\[1\]](#fermionic-gaussian-state) $$ \mathbf{f} = \Omega \begin{pmatrix} \mathbf{c}^\dagger \\ \mathbf{c} \end{pmatrix}~~,~~~~~~ \Omega = \frac{1}{\sqrt{2}} \begin{pmatrix} \mathbb{I} & \mathbb{I} \\ i\mathbb{I} & -i\mathbb{I} \end{pmatrix}~~, $$ where $\mathbf{f} = (x_1,\cdots,x_M, p_1,\cdots p_{M})^T$. Expressing the Hamiltonian in terms of the Majorana fermion operators, we have $$ H = \frac{i}{2}\,\mathbf{f}^T A\, \mathbf{f} + \text{const}~~,\tag{5} $$ where $A$ is a $2M\times 2M$ real antisymmetric matrix. $$ A = -i\,\Omega^* \begin{pmatrix} \Delta & h \\ -h^* & -\Delta^* \end{pmatrix} \Omega^\dagger~~. $$ To simplify the solution, we perform another basis transformation with the orthogonal matrix $R$, $\mathbf{f}' = R\mathbf{f}$, casting $A$ into its real Schur form: $$ R AR^T =\begin{pmatrix} 0 & E \\ -E & 0 \end{pmatrix} ~~, $$ where $E = \text{diag}(\epsilon_1,\dots, \epsilon_M)$ is a diagonal matrix of size $M$ with positive increasing eigenvalues. The transformation splits the operators into two distinct pairs, where the dynamics of each pair of operators is decoupled from the rest. Finally, we transform back to fermionic operators $$ \begin{pmatrix} \mathbf{d}^\dagger \\ \mathbf{d} \end{pmatrix} = \Omega^\dagger \begin{pmatrix} \mathbf{f'}^{\dagger} \\ \mathbf{f'} \end{pmatrix} = \Omega^\dagger R \begin{pmatrix} \mathbf{f}^{\dagger} \\ \mathbf{f} \end{pmatrix} = \Omega^\dagger R \Omega \begin{pmatrix} \mathbf{c}^{\dagger} \\ \mathbf{c} \end{pmatrix} \equiv W \begin{pmatrix} \mathbf{c}^{\dagger} \\ \mathbf{c} \end{pmatrix}~~, $$ which is concisely expressed in terms of the unitary matrix $W = \Omega^\dagger R \Omega$, known as the Bogoliubov-de Gennes (BdG) matrix. The operator basis transformation brings the Hamiltonian to the desired diagonal form $$ H = \sum_\eta \epsilon_{\eta}(d_\eta^\dagger d_\eta + d_\eta d_\eta^\dagger) + \text{const}~~. $$ The transformation matrix has a block form, $$ W=\begin{pmatrix} W_1^* & W_2^* \\ W_2 & W_1 \end{pmatrix} ~~, $$ therefore it suffices to treat only the lower half $W_L = (W_2 ~~ W_1)$. By applying Givens rotations on adjacent fermionic modes (encoded by the Jordan Wigner transformation) and particle-hole Bogoliubov transformations on the last fermionic mode. Crucially, the later transformations modify the particle number (unlike the Givens rotations), allowing to prepare ground states of non-particle conserving Hamiltonians. The Givens rotation for the general case is given by the 4 by 4 matrix $$ G^{(\text{gen})}_{ij}(\theta,\varphi) = \begin{pmatrix} G_{ij}(\theta,\varphi) & \mathbf{0} \\ \mathbf{0} & G_{ij}(\theta,-\varphi) \end{pmatrix}~~, $$ where the two mode Givens rotation $G_{ij}(\theta, \varphi)$ is defined in Eq. (4). The Bogoliubov transformation modifies only the last fermionic mode $c_M\rightarrow c_M^\dagger$, while the rest of the modes remain unchanged $c_j\rightarrow c_j$, for $j\in [1,M-1]$. In the qubit representation this corresponds to application of an $X$ gate on the last qubit. We next introduce the matrix $U$, satisfying $VW_LU^\dagger = (\mathbf{0}~ \mathbf{1})$, where $V$ is an arbitrary unitary matrix. By employing a modified QR decomposition equation we can decompose $U$ into a sequence of $N_G = O(M^2)$ Givens and particle-hole transformations $$ U = B G_{N_G}\dots BG_1 B~~. $$ The corresponding gates can be performed in parallel leading to a circuit depth which is at most $2M-1$. #### Algorithmic Steps The state preparation algorithm includes the following steps: 1. Constructs $A$ 2. Evaluates transformation $R$, bringing $A$ to the real Schur form, and constructs BdG matrix, $W$. 3. Decomposes $U$ into a sequence of Givens rotations and particle-hole Bogoliubov transformations. Comparing to the particle conserving case, we notice a few important differences. First, here we diagonalize an $2M$ by $2M$ matrix, $A$, while in the particle conserving case we diagonalized $h$ (an $M$ by $M$ matrix). As a consequence, the diagonalization technique is slightly different. Furthermore, in the particle conserving case, the phase of the Givens rotation did not effect the result ($\varphi = 0$), while in the present case, the phase degree of freedom is required in the diagonalization procedure. Finally, we include intertwined Givens and particle hole-Bogoliubov transformations, the former, rotate fermionic modes within subspaces of defined particle numbers, while the later transition between these subspaces. The combined effect is crucial in order to prepare the ground state of a general quadratic fermionic Hamiltonian. A simple example, utilizing only numpy data structures is presented in the technical notes section below. The example demonstrates the theoretical steps for the case of a $4$ by $4$ BdG matrix. In the following section, we prepare the ground state of a general fermionic Hamiltonian (including particle non-conserving terms). #### Preparation of a Fermionic Ground State ```python theme={null} @qfunc def prepare_fermionic_gaussian( h: list[list[float, n], n], Delta: list[list[float, n], n], qba: QArray[QBit, n], ) -> None: """ Prepares the ground state of a general quadratic fermionic Hamiltonian H = sum_{mu,nu} c_mu^dagger h_{mu,nu} c_nu + (1/2) sum_{mu,nu} (Delta_{mu,nu} c_mu^dagger c_nu^dagger + h.c.), where h is hermitian and Delta is antisymmetric. The algorithm has three steps: (1) build the dynamical generator A in the Majorana basis, (2) bring A to its canonical Schur form [[0, E], [-E, 0]] via an orthogonal transformation R, and assemble the Bogoliubov-de Gennes matrix W = Omega^dagger R Omega; the lower half W_L = (W_2, W_1) is the input to the decomposition routine, (3) decompose conj(W_L) (matching openfermion's gaussian_state_preparation_circuit convention) into Givens rotations and particle-hole transformations on the last fermionic mode and apply the resulting gates in reverse order to the c-vacuum |0^n>. The 'pht' marker becomes an X on qubit n-1, and each Givens tuple becomes a `phased_givens` gate. Args: h (ndarray): hermitian one-body matrix (n x n). Delta (ndarray): antisymmetric pairing matrix (n x n). qba (QArray[QBit, n]): qubit register, taken to start in |0^n>. """ h_arr = np.asarray(h, dtype=complex) Delta_arr = np.asarray(Delta, dtype=complex) # 1. Build the real antisymmetric generator A in the Majorana basis. Im_n = np.eye(n) Omega_n = (1 / np.sqrt(2)) * np.block([[Im_n, Im_n], [1j * Im_n, -1j * Im_n]]) H_BdG = np.block([[Delta_arr, h_arr], [-h_arr.conj(), -Delta_arr.conj()]]) A = np.real(-1j * Omega_n.conj() @ H_BdG @ Omega_n.conj().T) # 2. Real Schur decomposition and permutation to canonical form # [[0, E], [-E, 0]] with non-negative diagonal of E. _, Z_schur = scipy.linalg.schur(A, output="real") P = np.zeros((2 * n, 2 * n)) for k in range(n): P[k, 2 * k] = 1.0 P[n + k, 2 * k + 1] = 1.0 R = P @ Z_schur.T T_canonical = R @ A @ R.T b = np.diag(T_canonical[:n, n:]) signs = np.where(b >= 0, 1.0, -1.0) R = np.diag(np.concatenate([np.ones(n), signs])) @ R # 3. Bogoliubov-de Gennes matrix W and its lower half W_L = (W_2, W_1). W = Omega_n.conj().T @ R @ Omega_n W_L = W[n:, :] # 4. Decompose conj(W_L) into Givens rotations + particle-hole # transformations and apply the gates in reverse order to |0^n>. decomposition, _, _, _ = fermionic_gaussian_decomposition(W_L.conj()) circuit_description = list(reversed(decomposition)) for parallel_ops in circuit_description: for op in parallel_ops: if op == "pht": X(qba[n - 1]) else: i, j, theta, phi = op phased_givens(theta, phi, [qba[i], qba[j]]) ``` For two fermionic modes the full Fock space is only $4$-dimensional, so we can compare the prepared state directly to the analytical ground state of $H$ obtained by exact diagonalization. We first evaluate the analytical result by building the $4$ by $4$ Hamiltonian matrix, diagonalizing it, and picking the lowest-energy eigenvector as the analytical ground state. The Hamiltonian is defined by the following matrices: ```python theme={null} # Hermitian one-body matrix h (n = 2 fermionic modes) h = np.array( [ [1.0, 0.5], [0.5, -0.5], ], dtype=complex, ) # Antisymmetric pairing matrix Delta delta = 0.3 Delta = np.array( [ [0.0, delta], [-delta, 0.0], ], dtype=complex, ) ``` ```python theme={null} # Build the full Fock-space Hamiltonian via OpenFermion's Jordan-Wigner. from openfermion import QuadraticHamiltonian, get_sparse_operator n = 2 # number of fermionic modes H_full = get_sparse_operator( QuadraticHamiltonian(hermitian_part=h, antisymmetric_part=Delta), n_qubits=n, ).toarray() # OpenFermion orders basis indices with mode 0 as the most significant bit, # whereas Classiq's bitstring convention places qubit 0 in the least # significant bit. We apply a bit-reversal permutation so the analytical # ground state lines up with the simulator output below. perm = np.array([int(format(i, f"0{n}b")[::-1], 2) for i in range(2**n)]) H_full = H_full[np.ix_(perm, perm)] # Diagonalize and pick the lowest-energy eigenvector. energies_full, vecs_full = np.linalg.eigh(H_full) analytical_gs = vecs_full[:, 0] print(f"Full-space eigenvalues: {np.round(energies_full, 6)}") print(f"Analytical ground state: {np.round(analytical_gs, 6)}") ``` **Output:** ``` Full-space eigenvalues: [-0.651388 -0.140512 0.640512 1.151388] Analytical ground state: [-0. -0.j 0.289784+0.j -0.957092-0.j 0. +0.j] ``` Next, we synthesize a 2-qubit circuit that employs `prepare_fermionic_gaussian` to prepare the ground state, the amplitudes match up to a global phase. ```python theme={null} @qfunc def main(qba: Output[QArray[QBit, n]]) -> None: allocate(n, qba) prepare_fermionic_gaussian(h, Delta, qba) ``` Now we execute on the statevector simulator, and compute the fidelity. ```python theme={null} qprog_check = synthesize(main) df_check = calculate_state_vector(qprog_check) # Read the per-qubit `x` column (list of 0/1 per qubit, qubit 0 = LSB) and # convert it to the corresponding Fock-basis index. mask_check = np.abs(df_check["amplitude"]) > 1e-12 quantum_amps = np.zeros(2**n, dtype=complex) for _, row in df_check.loc[mask_check].iterrows(): basis_idx = sum(int(b) << i for i, b in enumerate(row["qba"])) quantum_amps[basis_idx] = row["amplitude"] quantum_amps = quantum_amps / np.linalg.norm(quantum_amps) fidelity = np.abs(np.vdot(analytical_gs, quantum_amps)) ** 2 print(f"\nFidelity vs analytical ground state: {fidelity:.1f}") assert np.isclose(fidelity, 1.0, atol=TOLERANCE), "Fidelity is not close to 1" ``` ## Technical Notes ### Heisenberg Dynamics of the Dirac Operators Under a Number-Conserving Quadratic Hamiltonian Referenced from the *Preparation of a Slater Determinant State* section. We show why diagonalizing the $M\times M$ one-body matrix $h$ is enough to capture the dynamics generated by $H$ in the full $2^M$-dimensional Fock space. Using the anti-commutation relations and the Heisenberg equation ($\frac{dO(t)}{dt} = i\left[H, O(t) \right]$), we have $$ \frac{dc_\lambda^\dagger}{dt} = i\left[H, c_\lambda^\dagger \right]= i \sum_{\mu\nu} h_{\mu\nu}[c_\mu^\dagger c_\nu,c_\lambda] $$ $$ = i \sum_{\mu\nu} h_{\mu\nu}[c_\mu^\dagger c_\nu,c_\lambda^\dagger] = i \sum_{\mu\nu} h_{\mu\nu}c_\mu^\dagger \delta_{\nu \lambda} = i \sum_{\mu} h_{\mu\lambda}c_\mu^\dagger ~~, $$ where the time-dependence is suppressed for conciseness. The dynamics in the Heisenberg representation can therefore be expressed as $$ \mathbf{c}^\dagger (t) = e^{i H t} \mathbf{c}^\dagger e^{-i H t} = e^{i h^T }\mathbf{c}^\dagger~~, $$ where $\mathbf{c}^\dagger = \{c_1^\dagger,\dots,c_M^\dagger\}^T$ and $M$ is the total number of fermionic modes. Remarkably, this implies that the diagonalization of $h$, (an $M$ by $M$ matrix) provides the dynamics in the $2^M$ Hilbert space. ### Heisenberg Dynamics of the Dirac Operators for a General Quadratic Fermion Hamiltonian Substituting Eq. (5) into the Heisenberg equation, we obtain $$ \frac{d f_\eta}{dt} = i[H, f_\eta] = -\frac{1}{2} \sum_{\mu \nu} A_{\mu \nu}(-\delta_{\eta\mu}f_\nu + \delta_{\eta \nu}f_{\mu}) = -\sum_{\mu}A_{\mu \eta}f_{\mu}~~, $$ where the last equality stems from $A$ being antisymmetric. This leads to a vector equation describing the Heisenberg dynamics of the Majorana operators $$ \mathbf{f}(t) = e^{-tA}\mathbf{f}~~. $$ Thus, the system dynamics under $H$, can be represented in terms of a simple linear relation, involving the Majorana operators. ### Simple Example for a General Hamiltonian (Particle Non-Conserving) We consider a quadratic Hamiltonian of the form $$ H = \sum_{\mu \nu} c_\mu^\dagger h_{\mu \nu} c_{\nu} + \frac{1}{2}\sum_{\mu \nu}\Delta_{\mu \nu}\left(c_\mu^\dagger c_\nu^\dagger +\text{h.c} \right)~~, $$ Two $2$ by $2$ matrices, $h$ and $\Delta$, are considered, where $h$ is hermitian and $\Delta$ is antisymmetric, corresponding to one-body terms in the Hamiltonian. Following, we construct the matrix $A$, associated with the Majorana fermion representation of the Hamiltonian, evaluate the BdG matrix $W$, decompose it into Givens rotations and Bogoliubov transformations and verify the decomposition. ```python theme={null} # Hermitian one-body matrix h (n = 2 fermionic modes) h = np.array( [ [1.0, 0.5], [0.5, -0.5], ], dtype=complex, ) # Antisymmetric pairing matrix Delta delta = 0.3 Delta = np.array( [ [0.0, delta], [-delta, 0.0], ], dtype=complex, ) # Bogoliubov-de Gennes (BdG) matrix in the Dirac basis (\{\vec{c}^dagger,\vec{c}\}^T) H_BdG = np.block( [ [Delta, h], [-h.conj(), -Delta.conj()], ] ) n = h.shape[0] # Unitary mapping between Dirac and Majorana representations Im = np.eye(n) Omega = (1 / np.sqrt(2)) * np.block( [ [Im, Im], [1j * Im, -1j * Im], ] ) # Real antisymmetric generator A in the Majorana representation A = -1j * Omega.conj() @ H_BdG @ Omega.conj().T A = np.real(A) assert np.allclose(A, -A.T, atol=TOLERANCE) print(f"A matrix:\n {A}\n") # Real Schur decomposition: A = Z T Z^T, where T is with 2x2 blocks [[0, b_k], [-b_k, 0]] T_schur, Z = scipy.linalg.schur(A, output="real") assert np.allclose(A, Z @ T_schur @ np.transpose(Z), atol=TOLERANCE) # Permute to bring the canonical Schur form to [[0, E], [-E, 0]] P = np.zeros((2 * n, 2 * n)) for k in range(n): P[k, 2 * k] = 1.0 P[n + k, 2 * k + 1] = 1.0 R = P @ Z.T S = R @ A @ R.T print(f"R * A * R^T = [[0,E],[-E,0]]:\n {np.round(S, 3)}") # BdG transformation matrix W = Omega.conj().T @ R @ Omega assert np.allclose( W @ W.conj().T, np.eye(2 * n), atol=TOLERANCE ) # verifies unitarity of W W_L = W[ n:, : ] # an n by 2n matrix, which is decomposed into Givens rotations and spin-hole Bogoliubov transformations ``` **Output:** ``` A matrix: [[ 0. 0. 1. 0.2] [ 0. 0. 0.8 -0.5] [-1. -0.8 0. 0. ] [-0.2 0.5 0. 0. ]] R * A * R^T = [[0,E],[-E,0]]: [[ 0. -0. 1.292 0. ] [ 0. -0. -0. 0.511] [-1.292 0. 0. -0. ] [-0. -0.511 0. 0. ]] ``` We utilize `OpenFermion`'s `fermionic_gaussian_decomposition` method which receives the lower half of $W$, and outputs four objects describing the canonical decomposition $$ V\, W_L\, U^\dagger = (\mathbf{0}\;\; D)~~, $$ where $V$ is an $M\times M$ unitary, $U$ is a $2M\times 2M$ unitary, and $D$ is an $M\times M$ diagonal matrix of unit-modulus phases, where $M$ is the number of fermionic modes. * `decomposition`: the description of $U$ as a sequence of two-mode operations applied to the columns of $W_L$. The list groups operations into layers that can be performed in parallel. Each layer entry is either the string `'pht'`, indicating a particle-hole Bogoliubov transformation on the last fermionic mode, or a tuple specifying a "double" Givens rotation on adjacent fermionic modes, acting on the upper $M$ columns of $W_L$ and as its complex conjugate on the lower $M$ columns. * `left_decomposition` and `left_diagonal`: are utilized to compose auxiliary $M\times M$ unitary $V^T D^*$ * `diagonal`: a length-$M$ array of unit-modulus complex numbers - the diagonal entries of $D$ in the identity above. It is utilized to evaluate $V=D D^\dagger V$. In the next cell we use these four outputs to reconstruct both $U$ and $V$ explicitly and check the identity $V W_L U^\dagger = (\mathbf{0}\;\; D)$ holds. ```python theme={null} decomposition, left_decomposition, diagonal, left_diagonal = ( fermionic_gaussian_decomposition(W_L) ) def build_U_from_decomposition_gen(n: int, decomposition) -> np.ndarray: """ Reconstruct the 2n x 2n unitary U from openfermion's `decomposition` (the column-side Givens rotations and particle-hole transformations), satisfying V @ W_L @ U^dagger = (0, D). Each Givens entry (j, j+1, theta, phi) is a "double Givens" that acts on columns (j, j+1) of the upper half with G(theta, phi) and on columns (j, j+1) of the lower half with G(theta, phi).conj(). Each 'pht' marker is a column swap of (n-1, 2n-1). """ Udag = np.eye(2 * n, dtype=complex) for parallel_ops in decomposition: for op in parallel_ops: if op == "pht": swap_columns(Udag, n - 1, 2 * n - 1) else: j, jp1, theta, phi = op c, s, e = np.cos(theta), np.sin(theta), np.exp(1j * phi) G = np.array([[c, -e * s], [s, e * c]], dtype=complex) double_givens_rotate(Udag, G, j, jp1, which="col") # Applying the same column ops to the identity yields U^dagger. return Udag.conj().T def build_V_from_decomposition_gen( n: int, left_decomposition, left_diagonal, diagonal ) -> np.ndarray: """ Reconstruct the n x n unitary V from openfermion's `left_decomposition` (the Givens decomposition of left_unitary^T @ diag(diagonal*)) and the two diagonals. openfermion's givens_decomposition_square gives left_unitary.T @ diag(diagonal*) = diag(left_diagonal) @ U_left, so V = left_unitary = diag(diagonal) @ U_left^T @ diag(left_diagonal). """ T = np.eye(n, dtype=complex) for parallel_ops in left_decomposition: for j, jp1, theta, phi in parallel_ops: c, s, e = np.cos(theta), np.sin(theta), np.exp(1j * phi) G = np.array([[c, -e * s], [s, e * c]], dtype=complex) givens_rotate(T, G, j, jp1, which="col") # Applying the same column ops to the identity yields U_left^dagger. U_left = T.conj().T return np.diag(diagonal) @ U_left.T @ np.diag(left_diagonal) U = build_U_from_decomposition_gen(n, decomposition) V = build_V_from_decomposition_gen(n, left_decomposition, left_diagonal, diagonal) # verifying the decomposition: V W_L U^dagger = [0, D] target = np.zeros((n, 2 * n), dtype=complex) target[range(n), range(n, 2 * n)] = diagonal assert np.allclose(V @ W_L @ U.conj().T, target, atol=TOLERANCE) ``` ## References \[1] : Jiang, Z., Sung, K. J., Kechedzhi, K., Smelyanskiy, V. N., & Boixo, S. (2018). Quantum algorithms to simulate many-body physics of correlated fermions. Physical Review Applied, 9(4), 044036. [arXiv:1711.05395](https://arxiv.org/abs/1711.05395). \[2] : Arute, F., Arya, K., Babbush, R., Bacon, D., Bardin, J. C., Barends, R., ... & Zanker, S. (2020). Observation of separated dynamics of charge and spin in the Fermi-Hubbard model. [arXiv:2010.07965](https://arxiv.org/abs/2010.07965). \[3] :Surace, J., & Tagliacozzo, L. (2022). Fermionic Gaussian states: an introduction to numerical approaches. SciPost Physics Lecture Notes, 054. [arXiv:2111.08343](https://arxiv.org/abs/2111.08343). # Quantum Thermal State Preparation Algorithm Implementation Source: https://docs.classiq.io/explore/algorithms/quantum_state_preparation/gibbs/quantum_thermal_state_preparation Open this notebook in GitHub to run it yourself ## Introduction This implementation is based on the paper \[[1](#thermal)] and was written in collaboration with Chi-Fang (Anthony) Chen, the first author of the paper. Quantum thermal state preparation is the task of preparing the Gibbs state \[[2](#gibbs)], which is the quantum state in a thermal equilibrium. In difference with ground state preparation, here one looks for properties of a quantum system which is coupled to an environment in a certain temperature. Besides simulating nature's behaviour, it is an algorithmic primitive that can be used within other quantum algorithms, such as for solving semi-definitie programs \[[3](#sdp)]. Formaly, the Gibbs state is a mixed-state, and defined as: $$ \sigma_\beta := \frac{e^{-\beta H}}{Z} \propto \sum_i e^{-\beta E_i} |\psi_i\rangle\langle \psi_i|, \quad Z := \operatorname{tr}\bigl[e^{-\beta H}\bigr]. $$ Notice that since the state is a mixed-state, we describe using a density matrix. # ## Problem Definition * **Input:** * $H$: Hamiltonian of a system; Can be a pauli decomposition, block encoding or a function that efficiently implements $e^{-iHt}$. * $\beta$: Inverse-temperature of the system. * **Output:** * A state $\rho$ which is an $\epsilon$-approximation of the Gibbs state:$\|\rho - \sigma_\beta\|_{\text{tr}} \le \epsilon$ where $\|\cdot\|_{\text{tr}}$ denotes the trace distance. ## Algorithm Description The algorithm is a quantum version of the classical Markov chain Monte Carlo (MCMC) algorithm \[[4](#mcmc)]. It is composed of the following steps: 1. Apply a random jump\transformation on the state from a set of jumps $A$ (usually a local transformation, but not necessarily). 2. Measure the energy difference $\Delta\omega$. 3. Accept the jump with probability that is propotional to $e^{-\beta\Delta\omega}$, otherwise reject the step and revert to the original state. Instead of taking discrete jumps, it is possible to simulate a continous case by simulating evolution under a generator matrix (Laplacian $L$ in the classical case, Linbladian $\mathcal{L}$ \[[5](#linbladian)] in the quantum case). So the quantum algorithm is just a quantum simulation of a block-encoded Linbladian, which is designed to have a fixed point approximately at the Gibbs state. Any Linbladian can be written in the following form: $$ \mathcal{L}(\rho) = \underbrace{-i \left[ H, \rho \right]}_{\substack{\text{Coherent (unitary)} \\ \text{dynamics}}} + \underbrace{\sum_k \left( L_k \rho L_k^\dagger - \frac{1}{2} \left\{ L_k^\dagger L_k, \rho \right\} \right)}_{\substack{\text{Dissipative (non-coherent)} \\ \text{dynamics}}} $$ where the evolution is according to: $$ \rho(t) = e^{t \mathcal{L}_\beta} \, \rho(0) $$ with $L_k$ being the Linblad (jump) operators. The algorithm engineers a Linbladian that consists only of dissipative terms. Each term $\hat{A}_a(\bar{\omega})$ is the Fourier transform of a jump operator in $A$: $$ \mathcal{L}_\beta(\rho) := \sum_{\substack{a \in \mathcal{A},\\ \bar{\omega} \in S_{\omega_0}}} \gamma(\bar{\omega}) \left( \hat{A}_a(\bar{\omega})\, \rho \, \hat{A}_a(\bar{\omega})^\dagger - \frac{1}{2} \left\{ \hat{A}_a(\bar{\omega})^\dagger \hat{A}_a(\bar{\omega}),\, \rho \right\} \right) $$ # ## Challenges There are 2 main challenges in translating the classical monte carlo to a quantum version: 1. Energy uncertainty: measuring the energy with high resolution is with exponential cost due to energy-time uncertainty. 2. Rejection step: it turns out that rejecting is not trivial, as the accept involves measurements. In order to deal with the first problem, the algorithm presentes the Operator Fourier Transform primitive, which estimates smoothly the energy difference, in a way that gurantees to reach approximated detailed balance \[[6](#detailedbalance)]. In order to tackle the second problem, the algorithm takes advatange of mid-circuit measurements and the quantum Zeno effect \[[7](#zeno)], which turns out to work well with the Linbladian simulation. # ## Running Time The total simulation time will be $O\left( \frac{\beta t_{mix}^2(\beta)}{\epsilon} \right)$, where $t_{mix}(\beta)$ is the mixing time of the Linbladian, measuring the time it takes to different mixed states to be indistinguishable under the evolution of $\mathcal{L}$. The mixing time is not trivial to calculate, and can be estimated in certain cases, such as \[[8](#rapidmixingtime)]. The algorithm might give exponential advantage for quantum systems that thermalize fast enough and have the sign problem \[[9](#signproblem)] so that no efficient classical alternatives exist. ## Algorithm Implementation with Classiq image.png **Figure 1:** A snippet for a quantum circuit for the Quantum Thermal State Preparation algorithm, as produced by the classiq platform. # ## Toy Problem Setting Here we define a very basic problem with just 2 qubits, and a diagonal hamiltonian, so that we will be able to implement the hamiltonian simulation exactly, and get results within the quantum simulator limits. We note that the algorithm does not gurantee improvement over classical monte carlo in the case of such hamiltonian, and we choose it for a didactic reason. The hamiltonian will be $H = Z_1 Z_2 + Z_1 I = \begin\{pmatrix\} 2 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 \\ 0 & 0 & -2 & 0 \\ 0 & 0 & 0 & 0 \end\{pmatrix\}$. The second parameter for the problem is $\beta = \frac {1}{T}$. Here we choose quite small value for fast convergence. ```python theme={null} import numpy as np from classiq import * # Problem: HAMILTONIAN = Pauli.Z(0) * Pauli.Z(1) + Pauli.Z(1) SYSTEM_SIZE = HAMILTONIAN.num_qubits MAX_ENERGY_SHIFT = 4 # a bound on the energy shift, to normalize the hamiltonian by BETA = 0.2 ``` # ## Operator Fourier Transform This is the heart of the algorithm. This building block is quite similar to the Quantum Phase Estimation. However, there are 2 main differences: * The initial state prepared for the phase register is a Gaussian distribution instead of a uniform one. It is used in order to have better guarantees on the error of the estimated energy. It is required because the time window is limited. * The circuit is measuring the energy difference that the operator is doing on eigenvalues instead of just the energy of each eigenvalue in superposition. For example, take an operator $O$ and hamiltoanian $H$ with eigenvalues $|\lambda_i\rangle$. If for $O$ and $|\lambda_0\rangle$ it holds that $O|\lambda_0\rangle = |\lambda_1\rangle + |\lambda_2\rangle$, then the output of the circuit for the input $|\lambda_0\rangle|0\rangle$ will be approximately: $|\lambda_1\rangle|\omega_1 - \omega_0\rangle + |\lambda_2\rangle|\omega_2 - \omega_0\rangle$. image.png **Figure 2:** Circuit for Operator Fourier Transform for an operator $O$ acting on the state **$\rho$**, given hamiltonian $H$. Reproduced from \[[1](#thermal)] Fig. 5. First we define a gaussian state preparation function. We choose truncation value and $\sigma$ in a quite arbitrary way now, qualitatively such that the gaussian trend will be within our truncation. ```python theme={null} import matplotlib.pyplot as plt def get_gaussian_amplitudes(num_qubits): grid = np.linspace(-2, 2, 2**num_qubits) amplitudes = np.exp(-(grid**2)) amplitudes /= np.linalg.norm(amplitudes) return grid, amplitudes @qfunc def prepare_gaussian_state(t: QArray): grid, amplitudes = get_gaussian_amplitudes(t.len) inplace_prepare_amplitudes(amplitudes.tolist(), 0, t) plt.scatter(*get_gaussian_amplitudes(6)) plt.xlabel("$|x>$") plt.ylabel("amplitude"); ``` output ```python theme={null} @qfunc def hamiltonian_simulation(hamiltonian: SparsePauliOp, time: CReal, state: QArray): suzuki_trotter( hamiltonian, evolution_coefficient=time, order=1, repetitions=1, # we use just 1 repetition as we work with a diagonal hamiltonoan. qbv=state, ) @qfunc def controlled_hamiltonian_simulation( hamiltonian: SparsePauliOp, t0: CReal, ctrl: QArray, state: QArray ): """ A controlled powered hamiltonian simulation, as done in QPE of hamiltonian simulation """ repeat( ctrl.len, lambda i: control( ctrl[i], lambda: hamiltonian_simulation(hamiltonian, 2**i * t0, state) ), ) @qfunc def operator_fourier_transform( hamiltonian: SparsePauliOp, t0: CReal, operator: QCallable[QArray], state: QArray, bohr_freq: QNum, ): prepare_gaussian_state(bohr_freq) within_apply( lambda: controlled_hamiltonian_simulation(hamiltonian, t0, bohr_freq, state), lambda: operator(state), ) invert(lambda: qft(bohr_freq)) ``` Identify the quantum variable `bohr_freq` as signed integer, $\omega = \omega_0 \cdot$ `bohr_freq` and $t = t_0 \cdot $ `bohr_freq`, while $\omega_0t_0 = \frac{2\pi}{N}$. It is analogous to the phase variable in Quantum Phase Estimation. Require that $||H|| \lt \frac{N}{2}{\omega_0}$, so that the energy estimation won't overflow. We also want to take advantage of the full resolution of the QFT, so we choose $\omega_0$ close to the bound. ```python theme={null} def get_fourier_parameters(num_energy_qubits, max_energy_shift): w0 = (2 + 0.5) * max_energy_shift / (2**num_energy_qubits) t0 = 2 * np.pi / (w0 * (2**num_energy_qubits)) return w0, t0 ``` Lets see how it works for the initial state $|0\rangle_{state}|0\rangle_{freq}$ and the operator $O = H\otimes H$ (where the $H$ are hadamard gates operating on the state variable). We expect jumps from the eigenvalue 2 to eigenvalues 0 and -2: ```python theme={null} ENERGY_QUBITS = 6 w0, t0 = get_fourier_parameters(ENERGY_QUBITS, MAX_ENERGY_SHIFT) @qfunc def main(state: Output[QArray[SYSTEM_SIZE]], w: Output[QNum[ENERGY_QUBITS, SIGNED, 0]]): allocate(state) allocate(w) operator_fourier_transform( HAMILTONIAN, t0, lambda s: hadamard_transform(s), state, w ) qmod = create_model( main, preferences=Preferences(optimization_level=0), execution_preferences=ExecutionPreferences(num_shots=10000), ) qprog = synthesize(qmod) show(qprog) res = execute(qprog).get_sample_result() ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3Ect1Xqh48SaHZruySDm394TIhC ``` image.png **Figure 3:** Circuit for Operator Fourier Transform for the operator $O = H\otimes H$ as produced by the classiq platform. ```python theme={null} import matplotlib.pyplot as plt def plot_omega_results(result): w = [sample.state["w"] for sample in result.parsed_counts] shots = [sample.shots for sample in result.parsed_counts] # Scale the w values w = np.array(w) * w0 # Create the scatter plot. plt.scatter(w, shots, color="blue", alpha=0.7, edgecolors="none") plt.xlabel(r"$\Delta\omega$") plt.ylabel("Counts") plt.title("Measured Energy Difference") plt.grid(True) plt.xlim(-5, 5) plt.tight_layout() plt.show() plot_omega_results(res) ``` output Now see what happens if we use a uniform distribution instead of a gaussian one as the initial state for the `bohr_freq`: ```python theme={null} @qfunc def operator_fourier_transform_uniform( hamiltonian: SparsePauliOp, t0: CReal, jump_operator: QCallable[QArray], state: QArray, bohr_freq: QNum, ): hadamard_transform(bohr_freq) # uniform state preparation within_apply( lambda: controlled_hamiltonian_simulation(hamiltonian, t0, bohr_freq, state), lambda: jump_operator(state), ) invert(lambda: qft(bohr_freq)) @qfunc def main(state: Output[QArray[SYSTEM_SIZE]], w: Output[QNum[ENERGY_QUBITS, SIGNED, 0]]): allocate(state) allocate(w) operator_fourier_transform_uniform( HAMILTONIAN, t0, lambda s: hadamard_transform(s), state, w ) qmod = create_model( main, preferences=Preferences(optimization_level=0), execution_preferences=ExecutionPreferences(num_shots=10000), ) qprog = synthesize(qmod) res_uniform = execute(qprog).get_sample_result() plot_omega_results(res_uniform) ``` output Each peak is higher, but there is a longer tail, so it is harder to gurantee the accuracy of the scheme. # ## Linbladian Block Encoding First we pick jump operators that will define our Linbladian dissipative part. It is enough to choose local operators, so here we just use all possible single-site pauli operators. ```python theme={null} # For better mixing and ergodicity, the jumps should be “scrambling” and not commute with the Hamiltonian # (e.g., breaking the symmetries of the Hamiltonian). # In our case it is enough to just take X jumps, but we do also Y for demonstration JUMP_OPERATORS = [ {"pauli": pauli, "index": index} for index in range(SYSTEM_SIZE) for pauli in [X, Y] ] JUMP_VAR_SIZE = max(1, np.ceil(np.log2(len(JUMP_OPERATORS)))) ``` Given a purely irreversible Linbladian: $$ \mathcal{L}[\rho] := \sum_{j \in J} \Bigl( L_j\, \rho\, L_j^\dagger - \frac{1}{2}\, L_j^\dagger L_j\, \rho - \frac{1}{2}\, \rho\, L_j^\dagger L_j \Bigr), $$ With Linbladian operators $L_j$, it is possible to block-encode it with $U$ such that: $$ \Bigl( \langle 0_b | \otimes I \Bigr) \, U \, \Bigl( |0_c\rangle \otimes I \Bigr) = \sum_{j \in J} |j\rangle \otimes L_j, $$ In our case, each Linblad operator is effectively a fourier mode $\hat{A}_a(\bar{\omega})$ the operator Fourier transform of a pauli jump operator weighted by a boltzmann weight $\sqrt{\gamma(\bar{\omega})}$. $$ \hat{A}_a(\bar{\omega}) := \sum_{\bar{t} \in S_{t0}} e^{-i \bar{\omega} \bar{t}} f(\bar{t}) A_a(\bar{t}) \ \text{for each} \ a \in A, \ \bar{\omega} \in S_{\omega0}. $$ $$ (\langle0_b|\langle0|_{boltz.}\otimes I)U(|0_b\rangle|0\rangle_{boltz.}|0\rangle_{freq}|0\rangle_{jump}\otimes I) = \sum_{a \in A, \ \bar{\omega} \in S_{\omega0}} \sqrt{\gamma(\bar{\omega})} |\bar{\omega}, a\rangle \otimes \hat{A}_a(\bar{\omega}). $$ We implement it by doing a single call to operator Fourier transform, and apply it on a block encoding of all pauli jumps, followed by $Y$ rotation based on $\bar{\omega}$. Here, we take $A=\{X_1, X_2, Y_1, Y_2\}$. image.png **Figure 4:** Circuit for block encoding the linbladian $\mathcal{L}$. Reproduced from \[[1](#thermal)] Fig. 4. ```python theme={null} from classiq.qmod.symbolic import exp, pi, sqrt class LinbladianBlock(QStruct): jump: QNum bohr_freq: QNum boltzmann_weight: QBit class BlockEncodedLinbladianState(QStruct): block: LinbladianBlock state: QArray @qfunc def pauli_jump_operators(jump: QNum, state: QArray): prepare_uniform_trimmed_state(len(JUMP_OPERATORS), jump) for index, jump_op in enumerate(JUMP_OPERATORS): control(jump == index, lambda: jump_op["pauli"](state[jump_op["index"]])) @qfunc def apply_boltzmann_weight( beta: float, w0: float, bohr_freq: QNum, boltzmann_weight: QBit ): # glauber assign_amplitude_table( lookup_table(lambda bf: np.sqrt(1 / (np.exp(beta * w0 * bf) + 1)), bohr_freq), bohr_freq, boltzmann_weight, ) # metropolis # assign_amplitude_table( # lookup_table(lambda bf: min(1, np.exp(-beta * w0 * bf)), bohr_freq), # bohr_freq, # boltzmann_weight, # ) # adjust conventions so that sqrt(gamma(w)) will be the amplitude of |0> X(boltzmann_weight) @qfunc def block_encode_linbladian( hamiltonian: SparsePauliOp, beta: float, w0: float, be: BlockEncodedLinbladianState, ): operator_fourier_transform( hamiltonian, 2 * pi / (w0 * (2**be.block.bohr_freq.size)), lambda _state: pauli_jump_operators(be.block.jump, _state), be.state, be.block.bohr_freq, ) apply_boltzmann_weight(beta, w0, be.block.bohr_freq, be.block.boltzmann_weight) ``` # ## Linbladian Evolution of a $\delta$-Time Step Given our block encoding $U$, and assuming that the Linbladian is purely irreversible, it is possible to evolve it for a timestep using a 1st order approximation: $I + \delta\mathcal{L} + \mathcal{O}(\delta^2)$ using weak measurements (weak measurement is a measurement that reveals only small amount of information on the system, and does not collapse entirely the state). Actually the it exploits a quantum Zeno-like effect \[[7](#zeno)] that makes the corrections to the evolution on quadratic in $\delta$! Note: This method is the 1st order approximation of the evolution. There are better scaling methods with higher order approximation to simulate the evolution of the Linbladian (see Appendix. F in the \[[1](#thermal)]). image.png **Figure 5:** Circuit for an approximate $\delta$-time step evolution of the linbladian $\mathcal{L}$. Reproduced from \[[1](#thermal)] Fig. 3. We use the `RESET` function to reset the state of a single qubit (thus saving qubits). Note that by using it, the circuit is not coherent anymore. The reset\discard operations act like nature's heat bath. ```python theme={null} from classiq.qmod.symbolic import asin @qfunc def reset_var(qvar: QArray): apply_to_all(RESET, qvar) @qfunc def delta_step( hamiltonian: SparsePauliOp, beta: float, w0: float, delta: CReal, delta_qbit: QBit, be: BlockEncodedLinbladianState, ): block_encode_linbladian(hamiltonian, beta, w0, be) # the jump operators don't use block-encoding, hence only the boltzmann qbit will # flag an accepted jump control( be.block.boltzmann_weight == 0, lambda: RY(asin(2 * sqrt(delta)), delta_qbit) ) control( delta_qbit == 0, lambda: invert(lambda: block_encode_linbladian(hamiltonian, beta, w0, be)), ) reset_var(be.block) reset_var(delta_qbit) ``` ## Running the Algorithm We choose parameters for the energy evaluation in the operator Fourier transform, a small enough delta timestep and number of repetitions. Note that as we make `DELTA` small enough, the approximation of evolution will be better. As the `MAX_REPETIOTIONS` \* `DELTA` (total simulation time) is larger, the state is better mixed and we get closer to the approximated detailed balance state. As `FT_WINDOW_SIZE` is larger, the approximated detailed balance state gets closer to the desired detailed balance state. Here we choose parameters that will allow reasonable simulation time on a quantum simulator. Note that as the algorithm includes mid-circuit measurements, the runtime time of state-vector simulators scales linearly with the number of shots. If a high number of shots is required, it might be beneficial to use the density matrix simulator instead. ```python theme={null} DELTA = 0.1 NUM_REPETITIONS = 50 FT_WINDOW_SIZE = 4 W0, T0 = get_fourier_parameters(FT_WINDOW_SIZE, MAX_ENERGY_SHIFT) class LinbladianBlock(QStruct): # naming corresponds to the paper p.23 jump: QNum[ JUMP_VAR_SIZE, UNSIGNED, 0 ] # jump operators label for the block encoding bohr_freq: QNum[ FT_WINDOW_SIZE, SIGNED, 0 ] # frequency register for the Operator Fourier Transform boltzmann_weight: QBit # ancilla for storing the Bohr-frequency dependent Boltzmann weights in the amplitudes class BlockEncodedLinbladianState(QStruct): block: LinbladianBlock state: QArray[QBit, SYSTEM_SIZE] # holds the state of the system ``` The initial state can be in general any state. As the initial state is closer to the thermal state, the mixing time should be smaller. In case there is more than one minima, it might be beneficial to start with the maximally mixed state. ```python theme={null} @qfunc def main(be: Output[BlockEncodedLinbladianState]): delta_qbit = QBit() allocate(be) allocate(delta_qbit) # start with the |00> state repeat( NUM_REPETITIONS, lambda i: delta_step(HAMILTONIAN, BETA, W0, DELTA, delta_qbit, be), ) drop(delta_qbit) execution_preferences = ExecutionPreferences( num_shots=1000, # backend_preferences=ClassiqBackendPreferences(backend_name="simulator_density_matrix"), # slow, use when number of shots is very high backend_preferences=ClassiqBackendPreferences(backend_name="simulator"), ) qmod = create_model( main, preferences=Preferences(optimization_level=0), execution_preferences=execution_preferences, ) qprog = synthesize(qmod) print("synthesized") show(qprog) ``` **Output:** ``` synthesized Quantum program link: https://platform.classiq.io/circuit/3EctH9Wx5rmokUdOP9OJMUmqjkY ``` ```python theme={null} res = execute(qprog).get_sample_result() res.parsed_counts ``` **Output:** ``` [{'be': {'block': {'jump': 0, 'bohr_freq': 0, 'boltzmann_weight': 0}, 'state': [0, 1]}}: 337, {'be': {'block': {'jump': 0, 'bohr_freq': 0, 'boltzmann_weight': 0}, 'state': [1, 0]}}: 245, {'be': {'block': {'jump': 0, 'bohr_freq': 0, 'boltzmann_weight': 0}, 'state': [1, 1]}}: 235, {'be': {'block': {'jump': 0, 'bohr_freq': 0, 'boltzmann_weight': 0}, 'state': [0, 0]}}: 183] ``` Lastly, we verify the results against the expected Gibbs distribution: ```python theme={null} samples = dict( (int("".join(map(str, reversed(s.state["be"].state))), 2), s.shots) for s in res.parsed_counts ) measured_probs = np.array([v for k, v in sorted(samples.items())]) measured_probs = measured_probs / sum(measured_probs) measured_probs ``` **Output:** ``` array([0.183, 0.245, 0.337, 0.235]) ``` ```python theme={null} eigs = [2, 0, -2, 0] expected_probs = [np.exp(-eig * BETA) for eig in eigs] expected_probs /= sum(expected_probs) expected_probs ``` **Output:** ``` array([0.16105159, 0.24026075, 0.35842691, 0.24026075]) ``` ```python theme={null} assert np.allclose(measured_probs, expected_probs, atol=0.1) ``` ## Notes: 1. **Exact detailed balance**: astonishingly, in a followup paper \[[10](#exact)] the authors improved the algorithm to reach exact detailed balance instead of approximated, still with finite time hamiltonian simulation. They use Linbladian with a coherent term as well. The algorithm runtime then scales as $O\left( \beta \cdot t_{mix}(\beta) \right)$. 1. \*\*Block Encoding vs. Sampling Jumps\*\*: it is possible and equivalent to classicaly sample jump operator on each iteration of the algorithm, instead of block encoding all the jumps as we did in the implementation. 1. **Coherent version**: the paper also presents a purified version of the algorithm, that improves quadratically the run time. ## References \[1]: [Chen, C.-F., Kastoryano, M. J., Brandão, F. G. S. L., & Gilyén, A. (2023). *"Quantum Thermal State Preparation"*](https://arxiv.org/abs/2303.18224) \[2]: [Gibbs State (Wikipedia)](https://en.wikipedia.org/wiki/Gibbs_state) \[3]: \[Brandão, F. G. S. L. and Svore, K. M. *"Quantum Speed-ups for Solving Semidefinite Programs."* In:FOCS (2017), 415- 426. ]\([https://arxiv.org/abs/1609.05537](https://arxiv.org/abs/1609.05537)) \[4]: [Markov Chain Monte Carlo (Wikipedia)](https://en.wikipedia.org/wiki/Markov_chain_Monte_Carlo) \[5]: [Linbladian (Wikipedia)](https://en.wikipedia.org/wiki/Lindbladian) \[6]: [Detailed balance (Wikipedia)](https://en.wikipedia.org/wiki/Detailed_balance) \[7]: [Quantum Zeno Effect (Wikipedia)](https://en.wikipedia.org/wiki/Quantum_Zeno_effect) \[8] [Ivan Bardet, Ángela Capel, Li Gao, Angelo Lucia, David Pérez-García, and Cambyse Rouzé. *"Rapid thermalization of spin chain commuting hamiltonians. Physical Review Letters, 130(6):060401, 2023.*](https://journals.aps.org/prl/abstract/10.1103/PhysRevLett.130.060401) \[9]: [Numerical sign problem (Wikipedia)](https://en.wikipedia.org/wiki/Numerical_sign_problem) \[10]: [Chen, C.-F., Kastoryano, M. J., & Gilyén, A. (2023). *"An efficient and exact noncommutative quantum Gibbs sampler"* ](https://arxiv.org/abs/2311.09207) # Glued Trees Algorithm Source: https://docs.classiq.io/explore/algorithms/quantum_walks/glued_trees/glued_trees Open this notebook in GitHub to run it yourself Consider a network of two mirrored binary trees connected to each other, where the outermost nodes of each tree are connected to two random nodes in the other tree. This structure has $2n$ columns and $2^{n+1}-2$ nodes in total, as shown in the diagram below. Each node in the structure has a secret key in the form of a random bit string of size $2n$, and you are given oracular access to the network such that you can query a node using its key to get the keys of its neighbors. Given the key of the entrance node, your goal is to find the key of the exit node as efficiently as possible. png If you play this game yourself or program an algorithm to do so, you quickly run into a major problem: since you don't know the specific nodes on the tree corresponding to the interior keys, you get lost in the structure once you reach the area between the two trees. There is no way to guarantee a solution to this problem - using a classical computer - that doesn't require you to check every node in the worst case. There is a way to solve this problem efficiently, however, on the order of the total number of *columns* of the structure instead of the nodes, using a quantum computer! This paper \[[1](#gluedtrees)] published in December 2023 describes a quantum approach to solving this algorithm by considering the structure as a system of coupled harmonic oscillators attached by springs. A quantum computer can use Hamiltonian simulation to simulate this classical system efficiently. If you apply a push to the oscillator representing the entrance node and treat the interactions between nodes as queries, you can "reach" the exit node (trigger a spike in its oscillatory movement) in time $2n$, offering linear efficiency as opposed to exponential efficiency. While this notebook follows the algorithm described by the 2023 paper, it should be noted that this problem was first set out in this paper from October 2002 \[[2](#quantumwalk)]. ```python theme={null} import json import random import matplotlib.pyplot as plt import networkx as nx import numpy as np from classiq import * ``` ## Quantum Algorithm To model the columns of the glued trees structure as a system of coupled harmonic oscillators, we consider a matrix $\mathbf{A}$ of size $N \times N$ corresponding to the nodes of the glued trees structure, such that $N=2^{n+1}-2$ and $n$ is the number of columns of one of the two glued trees. This matrix is defined as $\mathbf{A}:=3(\mathbf{1}_N)-A$, where $A$ is the adjacency matrix of the glued trees system using any ordering. For demonstration purposes, we use a simple linear ordering of this adjacency matrix such that the entrance node is first and the exit node is last. This matrix is symmetrical and takes the following shape: png As shown in further detail in the paper, we can define a block Hamiltonian $\mathbf{H}$ such that $$ \mathbf{H} := -\begin{pmatrix} \mathbf{0} & \mathbf{B} \\ \mathbf{B}^\dagger & \mathbf{0} \end{pmatrix} $$ where $\mathbf{B}$ is any $N \times M$ matrix such that $\mathbf{B}\mathbf{B}^\dagger=\mathbf{A}$. However, to use this matrix $\mathbf{H}$ for Hamiltonian simulation, it must have a size corresponding to a power of two, while $\mathbf{A}$ is size $N \times N$. We can deal with this by ensuring that $\mathbf{B}$ is size $N \times (N+4)$, so the resulting Hamiltonian $\mathbf{H}$ is a square matrix with side length $2N+4 = 2(2^{n+1}-2)+4 = 2^{n+2}$. This means that a glued trees system with $n$ columns for one tree can be simulated using $n+2$ qubits. In this notebook, we generate the matrix $\mathbf{A}$ by building the glued trees structure using the NetworkX library such that the nodes are labeled in order from the entrance to exit node and using the `nx.adjacency_matrix` function to generate an adjacency matrix using that ordering. We decompose $\mathbf{A}$ using [Cholesky decomposition](https://en.wikipedia.org/wiki/Cholesky_decomposition) to get a square matrix where its product with its conjugate transpose is equal to $\mathbf{A}$. This matrix is the same size as $\mathbf{A}$, however, so we must pad it with four columns of zeroes to get our matrix $\mathbf{B}$ of size $N \times (N+4)$ so $\mathbf{H}$ has a size corresponding to a power of two. We can then create the block Hamiltonian with the proper size using $\mathbf{B}$ and $\mathbf{B}^\dagger$, and decompose it into a sum of Pauli strings using the Classiq built-in `matrix_to_pauli_operator` function. The number of terms in the decomposition grows quickly with system size, and including all of them may produce circuits too deep for current quantum hardware. We therefore use two strategies depending on the system size. For a 6-qubit example ($n=4, N=30$), the full Pauli decomposition yields only \~1088 terms, which is small enough to include all of them without truncation. This gives a faithful Trotter approximation of the exact Hamiltonian. For larger systems where truncation is necessary, the `crop_pauli_list` function selects the most representative terms using a diversity-weighted algorithm: 60% of the budget is filled by scanning qubit positions and selecting the largest-coefficient term for each Pauli type ($I$, $X$, $Y$, $Z$) at each position; the remaining 40% are the highest-magnitude unused terms. This balances broad structural coverage of the Hamiltonian with its most energetically significant contributions. For the 20-qubit example ($n=18, N=524286$), the full Pauli decomposition is computationally infeasible. Instead, the Hamiltonian is approximated by a structured extension of the 12-qubit Pauli list: each term is padded to 20 qubits by replicating the second-highest-qubit Pauli operator across the inserted qubit positions, reflecting a pattern observed in the dominant Pauli strings of larger systems. ```python theme={null} def _deserialize_sparse_op(data): return SparsePauliOp( terms=[ SparsePauliTerm( paulis=[ IndexedPauli(pauli=Pauli[p["pauli"]], index=p["index"]) for p in t["paulis"] ], coefficient=t["coefficient"], ) for t in data["terms"] ], num_qubits=data["num_qubits"], ) def crop_pauli_list(op, size): if size is None: return op terms = op.terms n = op.num_qubits if len(terms) <= size: return op def pauli_at(term, qubit): for ip in term.paulis: if ip.index == qubit: return ip.pauli return Pauli.I result = [] idx = 0 while len(result) < round(size * 0.6): for qubit in range(n): for k in terms: if ( pauli_at(k, qubit) == [Pauli.I, Pauli.X, Pauli.Y, Pauli.Z][idx % 4] and k not in result ): result.append(k) break idx += 1 for t in terms: if len(result) >= size: break if t not in result: result.append(t) return SparsePauliOp(terms=result, num_qubits=n) def generate_pauli_list(qubits, rng=None, max_terms=200): rng = rng or random.Random() dim = qubits - 2 T1 = nx.balanced_tree(2, dim - 1) T2 = nx.relabel_nodes(T1, lambda x: 2 ** (dim + 1) - 3 - x) T = nx.union(T1, T2) edges = {i: 0 for i in range(2**dim - 1, 2 ** (dim - 1) + 2**dim - 1)} for i in range(2 ** (dim - 1) - 1, 2**dim - 1): nums = [ j for j in range(2**dim - 1, 2 ** (dim - 1) + 2**dim - 1) if edges[j] < 1 ] if not nums: nums = [ j for j in range(2**dim - 1, 2 ** (dim - 1) + 2**dim - 1) if edges[j] < 2 ] vals = rng.sample(nums, k=2) for j in vals: edges[j] += 1 T.add_edges_from([(i, vals[0]), (i, vals[1])]) A = 3 * np.identity(2 ** (dim + 1) - 2) - np.array( nx.adjacency_matrix(T, nodelist=sorted(T.nodes())).todense() ) B = np.hstack((np.linalg.cholesky(A), np.zeros((2 ** (dim + 1) - 2, 4)))) H = -np.block( [ [np.zeros((B.shape[0], B.shape[0])), B], [B.conj().T, np.zeros((B.shape[1], B.shape[1]))], ] ) op = matrix_to_pauli_operator(H) return crop_pauli_list( SparsePauliOp( terms=sorted(op.terms, key=lambda t: abs(t.coefficient), reverse=True), num_qubits=op.num_qubits, ), max_terms, ) def pauli_str(qubits, recalculate=False, rng=None, max_terms=200): with open("glued_trees_cache.json", "r") as f: cache = json.load(f) if not recalculate and str(qubits) in cache: return _deserialize_sparse_op(cache[str(qubits)]) if qubits > 12: base = generate_pauli_list(12, rng=rng, max_terms=max_terms) extra = qubits - base.num_qubits base_top = base.num_qubits - 1 base_second = base.num_qubits - 2 new_terms = [] for term in base.terms: new_paulis = [] for ip in term.paulis: if ip.index == base_top: new_paulis.append(IndexedPauli(pauli=ip.pauli, index=qubits - 1)) elif ip.index == base_second: for q in range(base_second, base_second + extra + 1): new_paulis.append(IndexedPauli(pauli=ip.pauli, index=q)) else: new_paulis.append(ip) new_terms.append( SparsePauliTerm(paulis=new_paulis, coefficient=term.coefficient) ) return SparsePauliOp(terms=new_terms, num_qubits=qubits) return generate_pauli_list(qubits, rng=rng, max_terms=max_terms) ``` We are now ready to run our main execution function, `run_range`. This function takes the number of qubits and synthesizes a single parametric circuit that performs Hamiltonian simulation $e^{-it\mathbf{H}}$ using the `suzuki_trotter` function. The evolution time `t` is declared as a classical execution parameter (`CReal`), so the circuit is synthesized only once and then sampled at 13 different time values spanning $t \in [2n-12,\, 2n+12]$ in two-second intervals using `sample`. The `num_shots` parameter is set to 8192 to give enough room for significant spikes in a state to be apparent, given the high number of total possible states. The resulting quantum state can be written as follows: $$ \begin{aligned} |\psi(t)\rangle &\propto \begin{pmatrix} \dot{\vec{x}}(t) \\ i\mathbf{B}^{\dagger} \vec{x}(t) \end{pmatrix} \\ \begin{pmatrix} \dot{\vec{x}}(t) \\ i\mathbf{B}^{\dagger} \vec{x}(t) \end{pmatrix} &= e^{-it\mathbf{H}} \begin{pmatrix} \dot{\vec{x}}(0) \\ i\mathbf{B}^{\dagger} \vec{x}(0) \end{pmatrix} \end{aligned} $$ where $\vec{x}(0)=(0,0,\dots,0)^T$ and $\dot{\vec{x}}(0)=(1,0,\dots,0)^T$ using a linear ordering of nodes. Since the speed of the entrance node oscillator $|\dot{x}_1(t)|$ is represented by the quantum state $|0\rangle$ and should have probability 1 at $t=0$, there is no specific state preparation necessary for this system. It should also be noted that since our matrix $\mathbf{B}^\dagger$ is padded with four rows of zeroes, the highest four quantum states do not correspond to the displacement or speed of any oscillator. This means that the quantum state representing the speed of the exit node oscillator $|\dot{x}_N(t)|$, which is what we are most interested in, corresponds to $|N-1\rangle=|2^{n+1}-3\rangle$. We track this particular quantum state around $t \approx 2n$, expecting a spike that represents the system of oscillators "reaching" the exit node from the initial push to the entrance node. ```python theme={null} # set global list for testing all qprogs in notebook qprogs = [] def run_range( qubits, recalculate=False, rng=None, max_terms=200, order=1, repetitions=10 ): pauli_op = pauli_str(qubits, recalculate, rng=rng, max_terms=max_terms) n = qubits - 2 @qfunc def main(t: CReal, state: Output[QNum]) -> None: allocate(pauli_op.num_qubits, state) suzuki_trotter( pauli_op, evolution_coefficient=t, order=order, repetitions=repetitions, qbv=state, ) qprog = synthesize(main) show(qprog) qprogs.append(qprog) time_points = [2 * n + i for i in range(-12, 13, 2)] results = sample( qprog, parameters=[{"t": float(t)} for t in time_points], num_shots=8192 ) return results, time_points ``` The following code segment runs `run_range` for 6 qubits ($n=4, N=30$), a small qubit size where the full Pauli decomposition has only \~1088 terms, making it feasible to include all of them without truncation (`max_terms=None`). This gives a faithful Trotter simulation of the exact Hamiltonian. The Pauli list is recalculated from scratch using a fixed random seed for reproducibility. ```python theme={null} results_6, times_6 = run_range( 6, recalculate=True, rng=random.Random(0), max_terms=None ) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3EYCSfVF4fK9MU7zmWbIkDFJF5K ``` **Output:** ``` Submitting job to simulator Job: https://platform.classiq.io/jobs/3fe33c93-8558-4822-94c0-5a11db42fa7c ``` The following code segment runs `run_range` for 20 qubits ($n=18, N=524286$), a qubit size that is still simulatable but whose Pauli list cannot be generated in reasonable time and is therefore approximated by padding the 12-qubit Pauli list. This instance of the function takes a few minutes to run if the cached Pauli list is used. ```python theme={null} results_20, times_20 = run_range(20) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3EYCchCF1VAmwe2t1mxfEZti0W7 ``` **Output:** ``` Submitting job to simulator Job: https://platform.classiq.io/jobs/0e55d061-f271-4958-9e9d-b0aab1d491ba ``` We can now graph the results. The `graph_results` function plots the proportion of shots for the qubit state $|N-1\rangle$ corresponding to $|\dot{x}_N(t)|$ for a given qubit size from $t=2n-12$ to $t=2n+12$: ```python theme={null} def graph_results(qubits, results, time_points): n = qubits - 2 exit_state = 2 ** (n + 1) - 3 data = [ ( df.loc[df["state"] == exit_state, "probability"].values[0] if (df["state"] == exit_state).any() else 0.0 ) for df in results ] plt.plot(time_points, data, "o-") plt.xlabel("Time (s)") plt.ylabel(f"Proportion of |{exit_state}⟩ shots") plt.title( r"Glued Trees System at $t \approx 2n$ for " + str(qubits) + r" Qubits ($n=" + str(n) + r"$)" ) plt.show() ``` The following code segment displays the graph for 6 qubits, using all Pauli terms with 10 Trotter repetitions. The spike near $t \approx 2n = 8$ is indicative of the initial push to the entrance node "reaching" the exit node: ```python theme={null} graph_results(6, results_6, times_6) ``` output The following code segment displays the graph for 20 qubits, a qubit size with an approximated Pauli list. There is a clear spike at $t \approx 2n$ caused by the propagation from the entrance node: ```python theme={null} graph_results(20, results_20, times_20) ``` output Because of the Pauli list cropping, you can also run the algorithm with reasonable accuracy on actual quantum hardware by specifying a backend in `sample`. Perhaps the most interesting thing about the glued trees algorithm is that it is a relatively heavy case of using a quantum computer to gain an exponential advantage, usually requiring several executions at different time points to observe the intended result, but it can still execute effectively on present-day quantum hardware due to the Pauli list cropping. Suggestion: Try out the algorithm on both a simulator and quantum hardware for various qubit sizes. ## References \[1] [Babbush, R., Berry, D. W., Kothari, R., Somma, R. D., and Wiebe, N. "Exponential quantum speedup in simulating coupled classical oscillators." Phys. Rev. X 13, 041041 (2023)](https://journals.aps.org/prx/pdf/10.1103/PhysRevX.13.041041). \[2] [Childs, A. M., Cleve, R., Deotto, E., Farhi, E., Gutmann, S., and Spielman, D. A. "Exponential algorithmic speedup by a quantum walk." Proc. 35th ACM Symposium on Theory of Computing (STOC 2003), pp. 59-68](https://arxiv.org/pdf/quant-ph/0209131). # Quantum Approximate Optimization Algorithm Source: https://docs.classiq.io/explore/algorithms/search_and_optimization/QAOA/qaoa Open this notebook in GitHub to run it yourself > The **Quantum Approximate Optimization Algorithm (QAOA)**, introduced by Edward Farhi, Jeffrey Goldstone, and Sam Gutmann \[[1](#qaoa)], is a hybrid quantum-classical method for tackling combinatorial optimization problems like Max-Cut, scheduling, and constraint satisfaction. It prepares a parameterized quantum state by alternating two simple operations: one that encodes the problem's cost function and another that "mixes" amplitudes across candidate solutions. After measuring the circuit, a classical optimizer updates the parameters to increase the expected cost, repeating this loop until good solutions are found. QAOA is especially appealing for near-term quantum devices because it uses shallow circuits, yet its quality can systematically improve as you increase the number of alternating layers. We demonstrate the algorithm by analyzing the Max-Cut and Knapsack problems. The former is an unconstrained optimization problem, while the latter is a constrained task. > > * **Input:** Classical objective function $C(x)$ on $n$-bit variable $x$. $C(x)$ is commonly constructed to output the number of satisfied clauses by the $x$. > * **Output:** A bit array $x^*$, constituting the best solution from the pool of candidates, maximizing the objective function. > > **Complexity:** The algorithm involves iteration over two types of operations. The "cost" operations involve $O(m)$ two-qubit gates, scaling linearly with the number of clauses m. While the mixing operation involves $n$ single-qubit gates. Hence, a single shot of a $p$-depth QAOA circuit includes $O(p(n+m))$ single and double qubit gates. > > *** > > **Keywords:** Hybrid Algorithm, Variational Quantum Algorithm, Adiabatic Theorem, Combinatorial Optimization. ## Algorithm Description The QAOA algorithm prepares a variational quantum state by an iterative procedure: $$ |\boldsymbol{\gamma}\boldsymbol{\beta}\rangle = \left( U_M(\gamma_p)U_C(\beta_p)\dots U_M( \gamma_1)U_C(\beta_1)\right)|+\rangle^{\otimes n}~~. $$ Each iteration is comprises of two key operational "layers": 1. **Cost layer:**\ A phase rotation in the $Z$-direction is applied based on the cost function: $ |x\rangle \xrightarrow\{U_\{\text\{C\}\}(\gamma)\} e^\{i\gamma H_C(x)\} |x\rangle ~~,$ where $H_C(x)$ is diagonal in the computational basis, encoding the classical cost function $C(x)$. 3. **Mixer layer:**\ An $X$-rotation is applied to all qubits: $ U_B(\beta) = e^\{-i\beta H_M\}, \quad \text\{with \} H_M = \sum_i X_i~~.$ This operation induces transitions between computational basis states based on the phases that were assigned by the previous layer By alternating these operations, the QAOA ansatz explores the combinatorial optimization space. After $p$ iterations, the state is measured in the computational basis. Repeated repetitions of the basic experiment lead to a sample of binary arrays (i.e, bit strings) $\{x\}$ and associated costs $\{C(x)\}$. The average cost is given by the expectation value $\langle \boldsymbol{\gamma}\boldsymbol{\beta}| H_C(x)|{\boldsymbol{\gamma}\boldsymbol{\beta}}\rangle$. Following, the variational parameters are adjusted according to a chosen classical optimization algorithm, and the procedure is repeated until convergence of $C(x)$ to a maximum value. **Note:** In the limit of $p\rightarrow \infty$, the QAOA ansatz state can approximate adiabatic evolution and converges to the optimum solution (see Technical Notes). ## Algorithm Implementation Using Classiq The implementation follows a modular design, associating a regular and `@qfunc` functions to the algorithmic components: * A **cost function** evaluating the classical cost function associated with a particular binary array instance. * **cost layer** function which employs the cost function to evaluate phase rotations of quantum states in the computational basis, with a parameter $\gamma$ controlling the extent of phase rotation. * A **mixer layer**, that applies a uniform $RX$ rotations (with parameter $\beta$) to all qubits. * **QAOA ansatz**, the overall circuit is constructed by first preparing a uniform superposition (via a Hadamard transform), which constitutes the ground state of the mixer Hamiltonian $H_B$, and then alternating the cost and mixer layers for a specified number of layers. ## Max-Cut Problem Given an undirected graph $G = (V, E)$, the goal is to partition the vertices into two disjoint subsets $S$ and $\bar{S}$, such that **the total number of edges with one endpoint in each subset is maximized**. In other words, the vertices are split such that as many edges as possible "cross the cut". * **Input:** $G = (V, E)$: A graph with vertices $V$ and edges $E$. * **Output:** A partition of the vertices into two subsets $S, \bar{S}$ that maximizes the cut value: $$ C_{\text{MC}}(x)=\sum_{(i,j) \in E} \frac{1-x_i x_j}{2} $$ where $x = x_1x_2\dots x_{|V|}$ is a bit array encoding the graphs vertices, and $x_i = 1$, $i\in [1,n]$ if the $i$'th vertex is in subset $S$ or $x_i = -1$ if the $i$'th vertex is in $\bar{S}$. The Max-Cut problem is known to be $\mathsf{NP}$-complete. **Note:** A weighted version exists, where each edge has a weight $w_{ij}$, and the goal is to maximize the weighted sum of cut edges. # ## Implementation with Classiq A quantum state $|x\rangle$ represents a candidate partition of the graph. The objective is defined as the negative, normalized number of cut edges - so that minimizing the cost is equivalent to maximizing the cut. The QAOA ansatz iteratively refines the quantum state toward a partition that minimizes the Hamiltonian (and thus maximizes the number of cut edges). The ansatz state is prepared by iteration of `maxcut_cost_layer` and `mixer_layer`, which implement the cost and mixer layers of the Max-Cut problem. The quantum circuit is of the following form: Screenshot 2025-03-09 at 13.22.15.png # ## Set a Specific Problem Instance to Optimize The implementation we are following is general, but to demonstrate it properly, we chose a specific Max-Cut instance using a **5-node graph**: * **Vertices:** $V = \{0,1,2,3,4\}$ * **Edges:** $E = \{(0,1), (0,2), (1,2), (1,3), (2,4), (3,4)\}$ # ### Optimal Max-Cut Solutions Several partitions achieve the maximum cut value (cutting 5 out of 6 edges). For example: **Option 1:** * **Set 1:** $\{0,2,3\}$ * **Set 2:** $\{1,4\}$ **Option 2:** * **Set 1:** $\{2,3\}$ * **Set 2:** $\{0,1,4\}$ Both options represent optimal solutions for this graph, showcasing the non-uniqueness of the optimal partition in a non-trivial Max-Cut instance. We begin by uploading packages and declaring the nodes and edges of the graph ```python theme={null} import math import matplotlib.pyplot as plt import networkx as nx import numpy as np import scipy from tqdm import tqdm from classiq import * ``` ```python theme={null} graph_edges = [(0, 1), (0, 2), (1, 2), (1, 3), (2, 4), (3, 4)] ``` Visualizing the above-defined graph: ```python theme={null} # Create a graph instance G = nx.Graph() G.add_edges_from(graph_edges) # Use a layout for better visualization pos = nx.spring_layout(G) # Draw the graph plt.figure(figsize=(5, 4)) # width, height in inches nx.draw(G, pos, with_labels=True, alpha=0.8, node_size=500, font_weight="bold") plt.title("Graph Visualization") plt.show() ``` output # ## Algorithms Building Blocks # ### Cost Layer The function `maxcut_cost` computes the normalized, negative cost of a partition represented by the quantum state `v`. Instead of using the conventional objective function: $$ C_{\text{MC}}(x)=\sum_{(i,j) \in E} \frac{1-x_i x_j}{2} $$ we define it as: $$ \text{maxcut\_cost}(x) = -\frac{1}{|E|}C_{\text{MC}}(x). $$ This modification serves two purposes: * **Normalization:** Dividing by the number of edges $|E|$ scales the cost to $\mathcal{O}(1)$, which helps prevent phase wrap-around in the QAOA circuit. * **Negation for Minimization:** Reformulating the objective as a minimization problem is consistent with adiabatic approaches where the system transitions to its ground state. *Note:* This function is reused both in the quantum phase encoding and in the classical optimizer. ```python theme={null} def maxcut_cost(v: QArray | list[int]): # Returns 1 if the edge is cut (i.e., vertices are in different sets), 0 otherwise. def edge_cut(node1, node2): return node1 * (1 - node2) + node2 * (1 - node1) # Compute the normalized, negative cost of the partition. return -sum(edge_cut(v[node1], v[node2]) for (node1, node2) in graph_edges) / len( graph_edges ) ``` The `maxcut_cost_layer` uses the `phase` operation together with `maxcut_cost` to encode the computed cost into the phase of the quantum state. The parameter $\gamma$ controls the phase angle. ```python theme={null} @qfunc def maxcut_cost_layer(gamma: CReal, v: QArray): phase(-maxcut_cost(v), gamma) ``` The next main building block is the `mixer_layer`. This layer drives transitions between computational basis states by applying $RX$ rotations to all qubits. These transitions allow the quantum state to explore different candidate solutions based on the phases assigned by the cost layer. # ### Mixer Layer ```python theme={null} @qfunc def mixer_layer(beta: CReal, qba: QArray): apply_to_all(lambda q: RX(beta, q), qba) ``` # ### QAOA Ansatz The overall QAOA ansatz alternates between the cost and mixer layers for a specified number of iterations. The ansatz operates on a uniform superposition state (prepared in the `main` function) by repeatedly applying these two layers. ```python theme={null} @qfunc def qaoa_ansatz( cost_layer: QCallable[CReal, QArray], gammas: CArray[CReal], betas: CArray[CReal], qba: QArray, ): repeat( betas.len, lambda i: [ cost_layer(gammas[i], qba), mixer_layer(betas[i], qba), ], ) ``` # ### Full QAOA Algorithm We now assemble all the algorithmic components to construct the full QAOA algorithm. The quantum program first applies a `hadamard_transform` to prepare the qubits in a uniform superposition. After this initial state preparation, the circuit sequentially applies the cost and mixer layers, each with its own parameters that are updated by the classical optimization loop. ```python theme={null} NUM_LAYERS = 4 @qfunc def main( params: CArray[ CReal, NUM_LAYERS * 2 ], # Execution parameters (first half: gammas, second half: betas), used later by the sample method v: Output[QArray[QBit, G.number_of_nodes()]], ): allocate(v) hadamard_transform(v) # Prepare a uniform superposition qaoa_ansatz( maxcut_cost_layer, params[0:NUM_LAYERS], params[NUM_LAYERS : 2 * NUM_LAYERS], v, ) ``` Having defined the `main` function, we create the model, synthesize it, and display the resulting quantum program. Note that the synthesized program is not yet executable because no parameter set has been specified. An `ExecutionSession` is defined to run the program with different parameter sets (stored in a `CArray[CReal, NUM_LAYERS * 2]`) during optimization, and ultimately to solve the problem using the optimized parameters. ```python theme={null} from classiq.execution import * qprog_maxcut = synthesize(main) show(qprog_maxcut) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/36eYsFoTxhFRNDpSWU5gY66Cc2X ``` # ## Classical Optimization We have constructed a modular, parameterized QAOA circuit for the Max-Cut problem. The circuit accepts a parameter array of size `NUM_LAYERS` $\times 2$, with the first half corresponding to the cost layer parameters (`gammas`) and the second half to the mixer layer parameters (`betas`). We execute the circuit using an `ExecutionSession`, which is configured to sample a fixed number of shots (`NUM_SHOTS`) per evaluation. The function `ExecutionSession.estimate_cost` computes the cost for a given parameter set using the `maxcut_cost` function embedded in our cost layer. Our classical optimization uses `scipy.optimize.minimize` with the COBYLA method. By minimizing our (negative) cost function, we are effectively maximizing the number of cut edges. Below is the code that implements the classical optimization loop. We define the objective function `objective_func` to evaluate the current parameters. This function takes a parameter vector, converts it to a list, and then calls `es.estimate_cost` with our cost function. The optimizer minimizes this function. Additionally, we initialize two lists * `cost_trace` and `params_history` - to record the cost and parameter values at each iteration for later analysis or debugging. *Note:* `cost_func` is defined later as a lambda function that computes the cost using `maxcut_cost(state["v"])`. ```python theme={null} cost_trace = [] params_history = [] es = ExecutionSession(qprog_maxcut) def objective_func(params): cost_estimation = es.estimate_cost(cost_func, {"params": params.tolist()}) cost_trace.append(cost_estimation) params_history.append(params.copy()) return cost_estimation ``` Next, we create an execution session, initialize the parameters, run the COBYLA optimizer to find the best parameters, and finally samples the circuit with the optimized parameters. ```python theme={null} NUM_SHOTS = 1000 MAX_ITERATIONS = 60 initial_params = np.concatenate( (np.linspace(0, 1, NUM_LAYERS), np.linspace(1, 0, NUM_LAYERS)) ) # Define the cost function used in the quantum circuit cost_func = lambda state: maxcut_cost(state["v"]) with tqdm(total=MAX_ITERATIONS, desc="Optimization Progress", leave=True) as pbar: def progress_bar(xk: np.ndarray) -> None: pbar.update(1) optimization_results = scipy.optimize.minimize( fun=objective_func, x0=initial_params, method="COBYLA", options={"maxiter": MAX_ITERATIONS}, callback=progress_bar, ) optimized_parameters = optimization_results.x.tolist() # Sample the circuit using the optimized parameters res = es.sample({"params": optimized_parameters}) es.close() print(f"Optimized parameters: {optimized_parameters}") ``` **Output:** ``` Optimization Progress: 50%|███████████████████████████████████████████████▌ | 30/60 [01:35<01:35, 3.19s/it] ``` **Output:** ``` Optimized parameters: [1.612218894541748, -0.8492977737168794, 1.8895203273174175, 3.01112201462151, 0.8182679368903977, 1.6432748174760219, -0.4042727477741934, -0.3225529463443293] ``` Plotting the convergence graph: ```python theme={null} plt.plot(cost_trace) plt.xlabel("Iterations") plt.ylabel("Cost") plt.title("Cost convergence") plt.show() ``` output # ### Results and Discussion After optimization, we print the optimized parameters and display the measurement outcomes. Each outcome is a bitstring representing a candidate partition, with its probability (the fraction of shots) and its cost (computed by `maxcut_cost`). For example, a cost of $-0.833$ indicates that $5$ out of $6$ edges are cut, which is optimal for this instance ($5/6 \approx 0.833$). Below is the code that prints the best $10$ resulting solutions according to the probability: ```python theme={null} print(f"Optimized parameters: {optimized_parameters}") sorted_counts = sorted(res.parsed_counts, key=lambda pc: maxcut_cost(pc.state["v"])) for sampled in sorted_counts[:10]: solution = sampled.state["v"] probability = sampled.shots / NUM_SHOTS cost_value = maxcut_cost(sampled.state["v"]) print(f"solution={solution} probability={probability:.3f} cost={cost_value:.3f}") ``` **Output:** ``` Optimized parameters: [1.612218894541748, -0.8492977737168794, 1.8895203273174175, 3.01112201462151, 0.8182679368903977, 1.6432748174760219, -0.4042727477741934, -0.3225529463443293] solution=[1, 0, 1, 1, 0] probability=0.306 cost=-0.833 solution=[0, 0, 1, 1, 0] probability=0.303 cost=-0.833 solution=[1, 1, 0, 0, 1] probability=0.298 cost=-0.833 solution=[0, 1, 0, 0, 1] probability=0.285 cost=-0.833 solution=[0, 1, 1, 0, 0] probability=0.120 cost=-0.667 solution=[1, 0, 0, 1, 1] probability=0.109 cost=-0.667 solution=[1, 0, 0, 1, 0] probability=0.099 cost=-0.667 solution=[1, 0, 0, 0, 1] probability=0.085 cost=-0.667 solution=[0, 1, 1, 0, 1] probability=0.077 cost=-0.667 solution=[0, 1, 1, 1, 0] probability=0.074 cost=-0.667 ``` Plotting the solutions with the best cost values: ```python theme={null} # Determine the best (minimum) cost among all sampled outcomes best_cost = min(maxcut_cost(pc.state["v"]) for pc in res.parsed_counts) tolerance = 1e-3 # Filter outcomes with cost within tolerance of best_cost best_outcomes = [ pc for pc in res.parsed_counts if abs(maxcut_cost(pc.state["v"]) - best_cost) < tolerance ] # Sort outcomes by descending probability best_outcomes = sorted(best_outcomes, key=lambda pc: pc.shots / NUM_SHOTS, reverse=True) # Plot the optimal solutions num_plots = len(best_outcomes) fig, axes = plt.subplots(1, num_plots, figsize=(5 * num_plots, 5)) if num_plots == 1: axes = [axes] for i, pc in enumerate(best_outcomes): solution = pc.state["v"] probability = pc.shots / NUM_SHOTS cost_value = maxcut_cost(pc.state["v"]) num_cuts = abs(cost_value * G.size()) # number of cut edges node_colors = ["red" if bit == 1 else "blue" for bit in solution] nx.draw( G, pos, with_labels=True, node_color=node_colors, edge_color="gray", ax=axes[i], node_size=700, ) axes[i].set_title( f"Solution {i+1}\nProb: {probability:.3f}\nCost: {cost_value:.3f}\nCuts: {num_cuts}" ) plt.tight_layout() plt.show() ``` output ## Knapsack Problem The following demonstration will show how to use Classiq for optimizing combinatorial optimization problems with constraints, using the QAOA algorithm \[[1](#qaoa)]. The primary objective of the following demonstration is to illustrate how digital and analog quantum operations can be combined to address such problems. The specific problem to optimize will be the knapsack problem \[[3](#knapsack)]. Given a set of items, determine how many items to put in the knapsack to maximize their summed value. * **Input:** * Items: A set of items $\{i\}$, and the number of duplicates of the $i$th item, $x_i$, in the knapsack, where each $x_i \in [0, d_i]$ . * Weights: A set of item weights, $\{w_i\}$. * Values: A set of item values $\{v_i\}$. * Weight constraint $C$. * **Output:** Item assignment $\mathbf{x}=\{x_1,\dots,x_N\}$ that maximizes the total value $$ \text{value}=\max_{\mathbf{x}} \Sigma_i x_i v_i~~, $$ subject to a weight constraint $$ \Sigma_i w_i x_i\leq C $$ Like the Max-Cut problem, the knapsack is known to be an NP-complete problem. # ## Set a Specific Problem Instance to Optimize: Here, we choose a small toy instance: * Two item types: * $x_a \in [0, 7]$ with $w_a=2$, $v_a=3$ * $x_b \in [0, 3]$ with $w_b=3$, $v_b=5$ * $C = 12$ * The optimal solution is $x_a=3$, $x_b=2$ In this problem, there are additional constraints on the search space. One way to address it is to add a penalty term for the constraints. Here, we take a different approach and take advantage of the quantum nature of the algorithm. 1. Objective Phase (analog): a phase rotation in the $z$-direction according to the objective value, as done in the vanilla QAOA $$ |x\rangle \xrightarrow{U_{C}(\theta)} e^{i\theta C_{\text{KS}}(x)}|x\rangle $$ 2. Constraints Predicate (digital): the constraints are verified digitally, with quantum arithmetics $$ |x\rangle|0\rangle \xrightarrow{U_{\text{con}}(\theta)} |x\rangle|\text{con}(x)\rangle $$ The transformation is done with the numeric assignment transformation, such as `assign` (`|=`). The transformations are combined in the following manner: For each QAOA cost layer, the objective phase transformation is applied conditioned on the constraints predicate value, such that effectively each infeasible solution will be given a vanishing phase. This way we can bypass the need to choose a penalty constant, on the expense of additional arithmetic gates for each layer. Note that the method presented here is relevant for positive maximization problems. # ## Algorithm Implementation Using Classiq download.png # ### Define the Knapsack Problem A `QStruct` is used to represent the state of optimization variables. The `value` and `constraint` functions will be used both quantumly, and classicaly in the post-processing. Notice that the optimization variable are defined as unsigned integers, with the `QNum` type. ```python theme={null} class KnapsackVars(QStruct): a: QNum[3] b: QNum[2] def value(v: KnapsackVars): return v.a * 3 + v.b * 5 def constraint(v: KnapsackVars): return v.a * 2 + v.b * 3 <= 12 # assign a negative value to the objective to get maximization def cost(v: KnapsackVars): return -value(v) if constraint(v) else 0 ``` # ### Apply Cost Phase Controlled on the Contraint Predicate Result We wrap the `phase` statement with the constraint predicate, so the allocated auxilliary will be released afterwards. The effective phase will be: $$ |x\rangle \xrightarrow{\tilde{U}_{C}(\theta)} \begin{cases} e^{i\theta C_{\text{KS}}(x)} |x\rangle & \text{if } \text{constraint}(x) = 1, \\ |x\rangle & \text{if } \text{constraint}(x) = 0~~, \end{cases} $$ where $\tilde{U}_{C}$ implements the combined operation of $U_C$ and $U_{\text{con}}$. ```python theme={null} @qfunc def apply_cost(gamma: CReal, v: KnapsackVars) -> None: # Rotate states per their objective value, if they satisfy the constraint control( constraint(v), # use the digital constraint function lambda: phase(-value(v), gamma), ) ``` # ### Full QAOA Algorithm As in the vanilla QAOA, the cost and mixer layers are applied sequentially with varying parameters, which will be set by the classical optimization loop. ```python theme={null} NUM_LAYERS = 3 @qfunc def main(params: CArray[CReal, NUM_LAYERS * 2], v: Output[KnapsackVars]): allocate(v) hadamard_transform(v) gammas = params[0:NUM_LAYERS] betas = params[NUM_LAYERS : 2 * NUM_LAYERS] repeat( NUM_LAYERS, lambda i: [ apply_cost(gammas[i], v), apply_to_all(lambda q: RX(betas[i], q), v), # mixer layer ], ) qprog_knapsack = synthesize(main, preferences=Preferences(optimization_level=1)) show(qprog_knapsack) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/36eZ5QaFt72p0HWvFd92mAWylDJ ``` # ## Classical Optimization Similarly to the Max-Cut solution we define a `cost_func` for evaluating the cost of a given sample. Notice that the same `value` function that was use in the quantum `phase` application is used here in the classical post-processing. For the classical optimizer we use `scipy.optimize.minimize` with the `COBYLA` optimization method, that will be called with the `evaluate_params` function. ```python theme={null} NUM_SHOTS = 1000 MAX_ITERATIONS = 60 # start with a linear scheduling guess initial_params = ( np.concatenate((np.linspace(0, 1, NUM_LAYERS), np.linspace(1, 0, NUM_LAYERS))) * math.pi ) cost_trace = [] def evaluate_params(es, params): cost_estimation = es.estimate_cost( cost_func=lambda state: cost(state["v"]), parameters={"params": params.tolist()} ) cost_trace.append(cost_estimation) return cost_estimation es = ExecutionSession( qprog_knapsack, execution_preferences=ExecutionPreferences(num_shots=NUM_SHOTS) ) with tqdm(total=MAX_ITERATIONS, desc="Optimization Progress", leave=True) as pbar: def progress_bar(xk: np.ndarray) -> None: pbar.update(1) # increment progress bar final_params = scipy.optimize.minimize( fun=lambda params: evaluate_params(es, params), x0=initial_params, method="COBYLA", options={"maxiter": MAX_ITERATIONS}, callback=progress_bar, ).x.tolist() print(f"Optimized parameters: {final_params}") plt.plot(cost_trace) plt.xlabel("Iterations") plt.ylabel("Cost") plt.title("Cost convergence") ``` **Output:** ``` Optimization Progress: 37%|██████████████████████████████████▊ | 22/60 [01:36<02:25, 3.83s/it] ``` After the optimization, we sample the the circuit with the optimized parameters: ```python theme={null} res = es.sample({"params": final_params}) es.close() ``` Print the resulting solutions according to the probability: ```python theme={null} sorted_counts = sorted(res.parsed_counts, key=lambda sampled: cost(sampled.state["v"])) for sampled in sorted_counts[:10]: v = sampled.state["v"] print( f"solution={v} probability={sampled.shots/NUM_SHOTS} value={value(v)} constraint={constraint(v)}" ) ``` We obtain the optimal solution, $(x_a, x_b) = (3,2)$, achieving a value of $19$. ## Technical Notes The QAOA algorithm can be related to adiabatic evolution of the system in the limit $p\infty$, thus, producing the optimal under the assumption that the adiabatic theorem's conditions are satisfied. The **Adiabatic Theorem**: For a time-dependent Hamiltonian $H(t)$ with instantaneous eigenstates and values, satisfying the eigen equation $$ H(t)|\psi_n(t)\rangle = E_n(t) |\psi_n(t)\rangle ~~. $$ Assume that for all $t \in [0,T]$ * The eigenvalues $E_m(t)$ of interest is non-degenerate, and * It is separated from the rest of the spectrum by a finite gap $$ \Delta(t) = \min_{n\neq m}|E_n(t)-E_m(t)|\geq \Delta_{\text{min}}>0~~. $$ If the system starts in that eigenstate at $t=0$, $|\psi (0)\rangle = |\psi_m(0)\rangle~~,$ and the Hamiltonian changes sufficiently slowly (see exact condition in \[[4](#regev)]), then the state at time $t$ remains the corresponding instantaneous eigenstate accompanied with dynamical, and geometric (Berry) phases: $$ |\psi(t)\rangle \approx e^{i\theta_m(t)}e^{i\gamma_m(t)}|\psi_m(t)\rangle ~~, $$ where the dynamical phase is given by $\theta_m(t) = - \int_0^t E_m(\tau)d\tau$, and the geometric obtains the form $ i \int_0^t \langle\psi_m(\tau)| \dot{\psi}_m(\tau) \rangle d\tau$. In the limit $T\rightarrow \infty$ the theorem is exact and the approximation turns to an equality. In other words, a sufficiently slow change of the Hamiltonian parameters, with respect to the energy gaps, suppresses transitions between the Hamiltonian eigenvalues. To show the equivalence between the QAOA algorithm and the adiabatic solution, we consider the parameterized Hamiltonian: $$ H(s) = (1-s)H_M + s H_C~~, $$ where $s(t)\in [0,1]$ is initialized at zero and gradually increased by some protocol to a final value of one. Generally, the time-evolution operator is given by $$ U(t,0) = {\cal T}e^{-i\int_0^T H(\tau)d\tau}~~, $$ where $\cal T$ denotes the time-ordering operator. Dividing the dynamics into $N$ consiquent steps, each governed by a constant Hamiltonian, $U(t,0)$ can be approximated by the Trotter expansion \[[5](#trotter)]: $$ U(t,0) = \Pi_{k=1}^{N}e^{-i H(s_k)\Delta t}+ O\left((\Delta t)^2 \right)= \Pi_{k=1}^{N} e^{-i H_C s_k\Delta t}e^{-i H_M(1-s_k)\Delta t} ~~, $$ where $\Delta t =t/N$. In the limit $N\rightarrow \infty$, the identity becomes exact. By choosing $\gamma_k = -\Delta t s_k$ and $\beta_k = -\Delta t (1-s(t_k))$, we obtain the QAOA propagator $$ U(t,0) = \Pi_{k=0}^{N} e^{i\gamma_k H_C}e^{i\beta_k H_M}~~. $$ Therefore, by choosing appropriate $\{\gamma_k\}$, and $\{\beta_k\}$, and for sufficiently slow variation of the Hamiltonian, the two time-evolution operators (adiabatic and QAOA) coincide. Finally, we recognize that initial state $|\psi(0)\rangle = H^{\otimes n} |0\rangle^{\otimes n} = |+\rangle^{\otimes n}$ is the ground state of the initial adiabatic Hamiltonian $H(s=0) = H_M$. As a result, under adiabatic dynamics, the system state will evolve to the ground state of the cost Hamiltonian $H(s=1) = H_C$. By construction, this state corresponds to an optimal combinatorial solution. Overall, the relationship between the QAOA algorithm and adiabatic evolution demonstrates that, for sufficiently large $p$ (the number of iterations), under the restrictions of the adiabatic theorem, the QAOA algorithm is guaranteed to provide an optimal solution to the classical combinatorial problem. ## References \[1]: [Farhi, Edward, Jeffrey Goldstone, and Sam Gutmann. "A quantum approximate optimization algorithm." arXiv preprint arXiv:1411.4028 (2014).](https://arxiv.org/abs/1411.4028) \[2]: [Maximum Cut Problem (Wikipedia)](https://en.wikipedia.org/wiki/Maximum_cut) \[3]: [Knapsack problem (Wikipedia)](https://en.wikipedia.org/wiki/Knapsack_problem) \[4]: [Ambainis, A., & Regev, O. (2004). An elementary proof of the quantum adiabatic theorem. arXiv preprint quant-ph/0411152.](https://arxiv.org/pdf/quant-ph/0411152) \[5]: [Zhuk, S., Robertson, N. F., & Bravyi, S. (2024). Trotter error bounds and dynamic multi-product formulas for Hamiltonian simulation. Physical Review Research, 6(3), 033309.](https://arxiv.org/abs/2306.12569) # Decoded Quantum Interferometry Algorithm Source: https://docs.classiq.io/explore/algorithms/search_and_optimization/dqi/dqi_max_xorsat Open this notebook in GitHub to run it yourself This notebook relates to the paper "Optimization by Decoded Quantum Interferometry" (DQI) \[[1](#dqi)], which introduces a quantum algorithm for combinatorial optimization problems. The algorithm focuses on finding approximate solutions to the *max-LINSAT* problem, and takes advantage of the sparse Fourier spectrum of certain optimization functions. ## Max-LINSAT Problem * **Input:** A matrix $B \in \mathbb{F}^{m \times n}$ and $m$ functions $f_i : \mathbb{F} \rightarrow \{+1, -1\}$ for $i = 1, \cdots, m $, where $\mathbb{F}$ is a finite field. Define the objective function $f : \mathbb{F}^n \rightarrow \mathbb{Z}$ to be $f(x) = \sum_{i=1}^m f_i \left( \sum_{j=1}^n B_{ij} x_j \right)$. * **Output:** a vector $x \in \mathbb{F}^n$ that best maximizes $f$. The paper shows that for the problem of *Optimal Polynomial Intersection (OPI)* - a special case of the the *max-LINSAT* - the algorithm can reach a better approximation ratio than any known polynomial time classical algorithm. We demonstrate the algorithm in the setting of *max-XORSAT*, which is another special case of *max-LINSAT*, and is different from the *OPI* problem. Even though the paper does not show a quantum advantage with *max-XORSAT*, it is simpler to demonstrate. ## Max-XORSAT Problem * **Input:** A matrix $B \in \mathbb{F}_2^{m \times n}$ and a vector $v \in \mathbb{F}_2^m$ with $m > n$. Define the objective function $f : \mathbb{F}_2^n \rightarrow \mathbb{Z}$ as $f(x) = \sum_{i=1}^m (-1)^{v_i + b_i \cdot x} = \sum_{i=1}^m f_i(x)$ (with $b_i$ the columns of $B$), which represents the number of satisfied constraints minus the number of unsatisfied constraints for the equation $Bx=v$. * **Output:** a vector $x \in \mathbb{F}_2^n$ that best maximizes $f$. The *max-XORSAT* problem is NP-hard. As an example, the *Max-Cut* problem is a special case of *max-XORSAT* where the number of ones in each row is exactly two. The DQI algorithm focuses on finding approximate solutions to the problem. ## Algorithm Description The strategy is to prepare this state: $$ |P(f)\rangle = \sum_{x\in\mathbb{F}_2^n}P(f(x))|x\rangle $$ where $P$ is a normalized polynomial. Choosing a good polynomial can bias the sampling of the state towards high $f$ values. The higher the degree $l$ of the polynomial, the better approximation ratio of the optimum we can get. The Hadamard spectrum of $|P(f)\rangle$ is $$ \sum_{k = 0}^{l} \frac{w_k}{\sqrt{\binom{m}{k}}} \sum_{\substack{y \in \mathbb{F}_2^m \\ |y| = k}} (-1)^{v \cdot y} |B^T y\rangle $$ where $w_k$ are normalized weights that can be calculated from the coefficients of $P$. So, to prepare $|P(f)\rangle$, we prepare its Hadamard transform, then apply the Hadamard transform over it. Stages: 1. Prepare $\sum_{k=0}^l w_k|k\rangle$. 2. Translate the binary encoded $|k\rangle$ to a unary encoded state $|k\rangle_{unary} = |\underbrace{1 \cdots 1}_{k} \underbrace{0 \cdots 0}_{n - k} \rangle$, resulting in the state $\sum_{k=0}^l w_k|k\rangle_{unary}$. 3. Translate each $|k\rangle_{unary}$ to a Dicke state \[[2](#dicke)], resulting in the state $\sum_\{k = 0\}^\{l\} \frac\{w_k\}\{\sqrt\{\binom\{m\}\{k\}\}\} \sum_\{\substack\{y \in \mathbb\{F\}_2^m \\ |y| = k\}\} |y\rangle_m$. 4. For each $|y\rangle_m$, calculate $(-1)^{v \cdot y} |y\rangle_m |B^T y\rangle_n$, getting $\sum_\{k = 0\}^\{l\} \frac\{w_k\}\{\sqrt\{\binom\{m\}\{k\}\}\} \sum_\{\substack\{y \in \mathbb\{F\}_2^m \\ |y| = k\}\} (-1)^\{v \cdot y\} |y\rangle_m |B^T y\rangle_n$. 5. Uncompute $|y\rangle_m$ by decoding $|B^T y\rangle_n$. 6. Apply the Hadamard transform to get the desired $|P(f)\rangle$. Step 5 is the heart of the algorithm. The decoding of $|B^T y\rangle_n$ is, in general, an ill-defined problem, but when the hamming weight of $y$ is known to be limited by some integer l (the degree of $P$) , it might be feasible and even efficient, depending on the structure of the matrix $B$. The problem is equivalent to the decoding error from syndrome \[[3](#synd)], where $B^T$ is the parity-check matrix. Figure 1 shows a layout of the resulting quantum program. Executing the quantum program guarantees that we sample `x` with high $f$ values with high probability (see the last plot in this notebook). image.png \*Figure 1. The full DQI circuit for a *MaxCut* problem. The `x` solutions are sampled from the `target` variable after the last Hadamard transform.\* ## Defining the Algorithm Building Blocks Next, we define the needed building-blocks for all algorithm stages. Step 1 is omitted as we use the built-in `prepare_amplitudes` function. # ## Step 2: Encoding Conversions We use three different encodings: * **Binary encoding**: Represents a number using binary bits, where each qubit corresponds to a binary place value. For example, the number 3 on 4 qubits is $|1100\rangle$. * **One-hot encoding**: Represents a number by activating a single qubit, with its position indicating the value. For example, the number 3 on 4 qubits is $|0001\rangle$. * **Unary encoding**: Represents a number by setting the first $k$ qubits to 1 $k$ is the number, and the rest to 0. For example, the number 3 on 4 qubits is $|1110\rangle$. Specifically, we translate a binary (unsigned `QNum`) to one-hot encoding, and show how to convert the one-hot encoding to a unary encoding. The conversions are done in place, meaning that the same binary encoded quantum variable is extended to represent the target encoding. We use the library function `binary_to_unary`, based on [this post](https://quantumcomputing.stackexchange.com/questions/5526/garbage-free-reversible-binary-to-unary-decoder-construction). Let's test the conversion of the number 8 from binary to unary: ```python theme={null} import numpy as np from classiq import * @qfunc def main(one_hot: Output[QArray]): binary = QNum() binary |= 8 binary_to_unary(binary, one_hot) qprog_one_hot = synthesize(main) res_one_hot = execute(qprog_one_hot).get_sample_result() res_one_hot.dataframe ``` | | one\_hot | count | probability | bitstring | | - | ---------------------------------------------- | ----- | ----------- | --------------- | | 0 | \[1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0] | 2048 | 1.0 | 000000011111111 | # ## Step 3: Dicke State Preparation We transform a unary input quantum variable to a Dicke state, such that $$ U|\underbrace{1 \cdots 1}_{k} \underbrace{0 \cdots 0}_{n - k} \rangle = \sum_{k = 0}^{l} \frac{1}{\sqrt{\binom{n}{k}}} \sum_{\substack{|y| = k}} |y\rangle_n $$ We use the library function `prepare_dicke_state`, with a recursive implementation that is based on \[[2](#dicke)]. The recursion works bit by bit. We test the function for the Dicke state of 6 qubits with 4 ones: ```python theme={null} @qfunc def main(qvar: Output[QArray]): allocate(6, qvar) prepare_dicke_state(4, qvar) qprog_dicke = synthesize(main) res_dicke = execute(qprog_dicke).get_sample_result() res_dicke.dataframe.head(7) ``` | | qvar | count | probability | bitstring | | - | ------------------- | ----- | ----------- | --------- | | 0 | \[1, 1, 0, 0, 1, 1] | 161 | 0.078613 | 110011 | | 1 | \[1, 1, 0, 1, 1, 0] | 155 | 0.075684 | 011011 | | 2 | \[1, 0, 1, 1, 1, 0] | 153 | 0.074707 | 011101 | | 3 | \[0, 1, 1, 1, 1, 0] | 144 | 0.070312 | 011110 | | 4 | \[0, 0, 1, 1, 1, 1] | 142 | 0.069336 | 111100 | | 5 | \[0, 1, 1, 0, 1, 1] | 139 | 0.067871 | 110110 | | 6 | \[1, 0, 1, 0, 1, 1] | 138 | 0.067383 | 110101 | In the DQI setting, we will want to prepare a Dicke state given a unary encoded quantum variable. We will use the function `prepare_dicke_state_unary_input`, which is actually a subrutine of `prepare_dicke_state`: ```python theme={null} @qfunc def main(qvar: Output[QArray]): allocate(6, qvar) # prepare 2 encoded in unary: |110000> qvar[0] ^= 1 qvar[1] ^= 1 prepare_dicke_state_unary_input(qvar.len, qvar) qprog_dicke_unary_input = synthesize(main) res_dicke_unary_input = execute(qprog_dicke_unary_input).get_sample_result() res_dicke_unary_input.dataframe.head(7) ``` | | qvar | count | probability | bitstring | | - | ------------------- | ----- | ----------- | --------- | | 0 | \[1, 0, 0, 1, 0, 0] | 156 | 0.076172 | 001001 | | 1 | \[0, 0, 1, 0, 1, 0] | 153 | 0.074707 | 010100 | | 2 | \[1, 0, 0, 0, 0, 1] | 147 | 0.071777 | 100001 | | 3 | \[0, 0, 0, 0, 1, 1] | 147 | 0.071777 | 110000 | | 4 | \[1, 1, 0, 0, 0, 0] | 143 | 0.069824 | 000011 | | 5 | \[0, 0, 1, 1, 0, 0] | 142 | 0.069336 | 001100 | | 6 | \[0, 0, 0, 1, 0, 1] | 142 | 0.069336 | 101000 | # ## Step 4: Vector and Matrix Products Define a matrix and vector product over the binary field functions: ```python theme={null} from functools import reduce from classiq.qmod.symbolic import pi @qfunc def vector_product_phase(v: CArray[CInt], y: QArray): phase(pi * sum(v[i] * y[i] for i in range(v.len))) @qfunc def matrix_vector_product(B: list[list[int]], y: QArray, out: Output[QArray]): allocate(len(B), out) for i in range(len(B)): out[i] ^= reduce( lambda x, y: x ^ y, [int(B[i][j]) * y[j] for j in range(y.len)] ) ``` ## Assembling the Full Max-XORSAT Algorithm Here, we combine all the building blocks into the full algorithm. To save qubits, the decoding is done in place, directly onto the $|y\rangle$ register. The only remaining part is the decoding, that will be treated after choosing the problem to optimize, as it depends on the input structure. `dqi_max_xor_sat` is the main quantum function of the algorithm. It expects the following arguments: * `B`: the (classical) constraints matrix of the optimization problem * `v`: the (classical) constraints vector of the optimization problem * `w_k`: a (classical) vector of coefficients $w_k$ corresponding to the polynomial transformation of the target function. The index of the last non-zero element sets the maximum number of errors that the decoder should decode * `y`: the (quantum) array of the errors for the decoder to decode. If the decoder is perfect, it should hold only zeros at the output * `solution`: the (quantum) output array of the solution. It holds $|B^Ty\rangle$ before the Hadamard transform. * `syndrome_decode`: a quantum callable that accepts a syndrome quantum array and outputs the decoded error on its second quantum argument ```python theme={null} @qfunc def dqi_max_xor_sat( B: list[list[int]], v: list[int], w_k: list[float], y: Output[QArray], solution: Output[QArray], syndrom_decode: QCallable[QArray, QArray], ): k_num_errors = QNum() prepare_amplitudes(w_k, 0, k_num_errors) k_unary = QArray() binary_to_unary(k_num_errors, k_unary) # pad with 0's to the size of m pad_zeros(B.len, k_unary, y) # Create the Dicke states max_errors = int(np.nonzero(w_k)[0][-1]) if np.any(w_k) else 0 prepare_dicke_state_unary_input(max_errors, y) # Apply the phase vector_product_phase(v, y) # Compute |B^T*y> to a new register matrix_vector_product(np.array(B).T.tolist(), y, solution) # uncompute |y> # decode the syndrom inplace directly on y syndrom_decode(solution, y) # transform from Hadamard space to function space hadamard_transform(solution) ``` ## Example Problem: Max-Cut for Regular Graphs Now, let's be more specific and optimize a Max-Cut problem. We choose specific parameters so that with the resulting $B$ matrix we can decode up to two errors on the vector $|y\rangle$. The translation between Max-Cut and max-XORSAT is quite straightforward. Every edge is a row, with the nodes as columns. The $v$ vector is all ones, so that if $(v_i, v_j) \in E$, we get a constraint $x_i \oplus x_j = 1$, which is satisfied if $x_i$, $x_j$ are on different sides of the cut. ```python theme={null} import itertools import warnings import matplotlib.pyplot as plt import networkx as nx warnings.filterwarnings("ignore", category=FutureWarning) # A 2-regular graph on 6 nodes G = nx.Graph() G.add_nodes_from([3, 4, 1, 5, 2, 0]) G.add_edges_from([(3, 4), (3, 2), (4, 1), (1, 5), (5, 0), (2, 0)]) B = nx.incidence_matrix(G).T.toarray() v = np.ones(B.shape[0]) plt.figure(figsize=(4, 2)) nx.draw(G) print("B matrix:\n", B) ``` **Output:** ``` B matrix: [[ 1. 1. 0. 0. 0. 0.] [ 1. 0. 0. 0. 1. 0.] [ 0. 1. 1. 0. 0. 0.] [ 0. 0. 1. 1. 0. 0.] [ 0. 0. 0. 1. 0. 1.] [ 0. 0. 0. 0. 1. 1.]] ``` output # ## Original Sampling Statistics Let's plot the statistics of $f$ for uniformly sampling $x$, as a histogram. Later, we show how to get a better histogram after sampling from the state of the DQI algorithm. ```python theme={null} # plot f statistics all_inputs = np.array(list(itertools.product([0, 1], repeat=B.shape[1]))).T f = ((-1) ** (B @ all_inputs + v[:, np.newaxis])).sum(axis=0) # plot a histogram of f plt.hist(f, bins=20, density=True) plt.xlabel("f") plt.ylabel("density") plt.title("f Histogram") plt.show() ``` output # ## Decodability of the Resulting Matrix The transposed matrix of the specific matrix we have chosen can be decoded with up to two errors, which corresponds to a polynomial transformation of $f$ of degree 2 in the amplitude, and degree 4 in the sampling probability: ```python theme={null} # set the code length and possible number of errors MAX_ERRORS = 2 # l in the paper n = B.shape[0] # Generate all vectors in one line errors = np.array( [ np.array([1 if i in ones_positions else 0 for i in range(n)]) for num_ones in range(MAX_ERRORS + 1) for ones_positions in itertools.combinations(range(n), num_ones) ] ) syndromes = (B.T @ errors.T % 2).T print("num errors:", errors.shape[0]) print("num syndromes:", len(set(tuple(x) for x in list((syndromes))))) print("B shape:", B.shape) ``` **Output:** ``` num errors: 22 num syndromes: 22 B shape: (6, 6) ``` # ## Step 5: Defining the Decoder For this basic demonstration, we use a brute force decoder that uses a lookup table for decoding each syndrome in superposition: ```python theme={null} def _to_int(binary_array): return int("".join(str(int(bit)) for bit in reversed(binary_array)), 2) @qfunc def syndrome_decode_lookuptable(syndrome: QNum, error: QNum): for i in range(len(syndromes)): control( syndrome == _to_int(syndromes[i]), lambda: inplace_xor(_to_int(errors[i]), error), ) ``` It is also possible to define a decoder that uses a local rule of syndrome majority. This decoder can correct just one error. ```python theme={null} @qfunc def syndrome_decode_majority(syndrome: QArray, error: QArray): for i in range(B.shape[0]): # if 2 syndromes are 1, then the decoded bit will be 1, else 0 synd_1 = np.nonzero(B[i])[0][0] synd_2 = np.nonzero(B[i])[0][1] error[i] ^= syndrome[synd_1] & syndrome[synd_2] ``` # ## Choosing Optimal $w_k$ Coefficients According to the paper \[[1](#dqi)], this is done by finding the principal value of a tridiagonal matrix $A$ defined by the following code. The optimality is with regard to the expected ratio of satisfied constraints. ```python theme={null} def get_optimal_w(m, n, l): # max-xor sat: p = 2 r = 1 d = (p - 2 * r) / np.sqrt(r * (p - r)) # Build A matrix diag = np.arange(l + 1) * d off_diag = [np.sqrt(i * (m - i + 1)) for i in range(1, l + 1)] A = np.diag(diag) + np.diag(off_diag, 1) + np.diag(off_diag, -1) # get W_k as the principal vector of A eigenvalues, eigenvectors = np.linalg.eigh(A) principal_vector = eigenvectors[:, np.argmax(eigenvalues)] # normalize return principal_vector / np.linalg.norm(principal_vector) # normalize W_k = get_optimal_w(m=B.shape[0], n=B.shape[1], l=MAX_ERRORS) print("Optimal w_k vector:", W_k) # complete W_k to a power of 2 for the usage in prepare_state W_k = np.pad(W_k, (0, 2 ** int(np.ceil(np.log2(len(W_k)))) - len(W_k))) ``` **Output:** ``` Optimal w_k vector: [0.4330127 0.70710678 0.55901699] ``` # ## Synthesis and Execution of the Full Algorithm ```python theme={null} from classiq.execution import * @qfunc def main(y: Output[QArray], solution: Output[QArray]): dqi_max_xor_sat( B.tolist(), v.tolist(), W_k.tolist(), y, solution, syndrome_decode_lookuptable, ) qmod = create_model( main, constraints=Constraints(optimization_parameter="width"), execution_preferences=ExecutionPreferences(num_shots=10000), ) qprog = synthesize(qmod) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/36FEtf36OBpFvMac5yCd1aSkh8j ``` ```python theme={null} res = execute(qprog).get_sample_result() res.dataframe ```
y solution count probability bitstring
0 \[0, 0, 0, 0, 0, 0] \[0, 1, 0, 1, 1, 0] 2966 0.2966 011010000000
1 \[0, 0, 0, 0, 0, 0] \[1, 0, 1, 0, 0, 1] 2893 0.2893 100101000000
2 \[0, 0, 0, 0, 0, 0] \[0, 1, 1, 0, 1, 0] 138 0.0138 010110000000
3 \[0, 0, 0, 0, 0, 0] \[0, 1, 0, 0, 1, 0] 136 0.0136 010010000000
4 \[0, 0, 0, 0, 0, 0] \[1, 0, 0, 1, 0, 1] 136 0.0136 101001000000
... ... ... ... ... ...
59 \[0, 0, 0, 0, 0, 0] \[1, 0, 0, 0, 1, 0] 8 0.0008 010001000000
60 \[0, 0, 0, 0, 0, 0] \[1, 0, 0, 0, 1, 1] 8 0.0008 110001000000
61 \[0, 0, 0, 0, 0, 0] \[0, 1, 1, 1, 1, 1] 8 0.0008 111110000000
62 \[0, 0, 0, 0, 0, 0] \[0, 0, 0, 1, 0, 1] 7 0.0007 101000000000
63 \[0, 0, 0, 0, 0, 0] \[1, 0, 0, 1, 1, 1] 7 0.0007 111001000000

64 rows × 5 columns

We verify that the decoder uncomputed the `y` variable correctly: ```python theme={null} assert sum(sum(sample.state["y"]) for sample in res.parsed_counts) == 0 ``` And we can observe that the `y` vector is indeed clean. # ## Postprocessing Finally, we plot the histogram of the sampled $f$ values from the algorithm, and compare it to a uniform sampling of $x$ values, and also to sampling weighted by $|f|$ and $|f|^2$ values. We can see that the DQI histogram is biased to higher $f$ values compared to the other sampling methods. ```python theme={null} import matplotlib.pyplot as plt import numpy as np # Example data initialization f_sampled = [] shots = [] # Populate f_sampled and shots based on res.parsed_counts for sample in res.parsed_counts: solution = sample.state["solution"] f_sampled.append(((-1) ** (B @ solution + v)).sum()) shots.append(sample.shots) f_sampled = np.array(f_sampled) shots = np.array(shots) unique_f_sampled, indices = np.unique(f_sampled, return_inverse=True) prob_f_sampled = np.array( [shots[indices == i].sum() for i in range(len(unique_f_sampled))] ) prob_f_sampled = prob_f_sampled / prob_f_sampled.sum() f_values, f_counts = np.unique(f, return_counts=True) prob_f_uniform = np.array(f_counts) * np.array(f_values) prob_f_uniform = f_counts / sum(f_counts) prob_f_abs = np.array(f_counts) * np.array(np.abs(f_values)) prob_f_abs = prob_f_abs / prob_f_abs.sum() prob_f_squared = np.array(f_counts) * np.array(f_values**2) prob_f_squared = prob_f_squared / prob_f_squared.sum() # Plot normalized bar plots bar_width = 0.2 plt.bar( unique_f_sampled - 1.5 * bar_width, prob_f_sampled, width=bar_width, alpha=0.7, label="$f_{DQI}$ sampling", ) plt.bar( f_values - 0.5 * bar_width, prob_f_uniform, width=bar_width, alpha=0.7, label="$uniform$ sampling", ) plt.bar( f_values + 0.5 * bar_width, prob_f_abs, width=bar_width, alpha=0.7, label="$|f|$ sampling", ) plt.bar( f_values + 1.5 * bar_width, prob_f_squared, width=bar_width, alpha=0.7, label="$|f|^2$ sampling", ) plt.title("Normalized Bar Plot of $f$") plt.xlabel("$f$") plt.ylabel("Probability") plt.legend() plt.show() print(":", np.average(f)) print(":", np.average(f_sampled, weights=shots)) ``` output **Output:** ``` : 0.0 : 3.9904 ``` ## References \[1]: [Jordan, Stephen P., et al. "Optimization by Decoded Quantum Interferometry." arXiv preprint arXiv:2408.08292 (2024).](https://arxiv.org/abs/2408.08292) \[2]: [Bärtschi, Andreas, and Stephan Eidenbenz. "Deterministic Preparation of Dicke States." In *Fundamentals of Computation Theory*, pp. 126-139. Springer International Publishing, 2019.](http://dx.doi.org/10.1007/978-3-030-25027-0_9) \[3]: ["Linear Block Codes: Encoding and Syndrome Decoding" from MIT's OpenCourseWare](https://ocw.mit.edu/courses/6-02-introduction-to-eecs-ii-digital-communication-systems-fall-2012/resources/mit6_02f12_chap06/). # Grover's Search Algorithm Source: https://docs.classiq.io/explore/algorithms/search_and_optimization/grover/grover Open this notebook in GitHub to run it yourself > **Grover's Search Algorithm**, introduced by Lov Grover in 1996 [\[1\]](#ref-gro96), is one of the canonical quantum algorithms. It provides a quadratic speedup for searching an unstructured database and is often considered alongside Shor's algorithm as a cornerstone of quantum computing. The search algorithm has applications in various fields, such as in [cybersecurity](https://github.com/Classiq/classiq-library/blob/main/applications/cybersecurity/whitebox_fuzzing/whitebox_fuzzing.ipynb). > > * **Input:** A Boolean function (oracle) $f: \{0,1\}^n \rightarrow \{0,1\}$ marking "solutions" among $N = 2^n$ possible items. > * **Promise:** At least one marked element exists in the search space. > * **Output:** With high probability, the algorithm outputs a marked element $x$ (i.e., $f(x) = 1$). > > **Complexity:** The algorithm requires $O(\sqrt{N})$ oracle queries to obtain the result, while the query complexity for classical search is $O(N)$. > > *** > > **Keywords:** Search and optimization, Unstructured search, Quadratic speedup, Amplitude amplification, Graph problems, SAT problems, Oracle/Query complexity. Grover's Search algorithm starts with preparing the search space $|s\rangle$, and then repeats over a combination of two reflection operations: $$ U_f|x\rangle = (-1)^{f(x)}|x\rangle \qquad \text{usually called an "Oracle", adds a minus phase to marked states, and} $$ $$ U_s = 2|s\rangle\langle s|-1 \qquad \text{usually called a "Diffuser", reflects around the full search space.} $$ The number of times we need to repeat the combination of these two reflections (sometimes called a "Grover Operator") depends on the number of solutions: for $k$ solutions in a search space of size $N$, we shall perform $r\sim \pi \sqrt{N/k}$ iterations. In practice, however, the number of solutions is often unknown. In that case, one can perform a [quantum counting algorithm](https://github.com/Classiq/classiq-library/blob/main/algorithms/amplitude_amplification_and_estimation/quantum_counting/quantum_counting.ipynb) prior to the search, or run the search algorithm repeatedly, with an increasing number of repetitions $r_i\sim \frac{\pi}{4} \sqrt{2^i}$ for $i=0,1,\dots$. Both approaches do not affect the complexity of the search algorithm. In this notebook we take the second approach. For a given problem, Grover's algorithm depends on two functions, one for preparing the search space and another for applying the oracle, as well as on the number of repetitions. Below we address two canonical search problems, a 3-SAT problem and a Max-Cut problem on a graph. In this notebook we work with the `grover_operator` function, and define a classical postprocess function for iterating over different $r$ values and obtianing the marked states. *Using the `phase_oracle` quantum function from the Classiq open-library together with the Qmod language for high-level problem definitions helps avoid low-level implementation details that are typically required on other platforms.* *** *** Screenshot 2025-09-11 at 10.17.19.png
Layout of the Grover's Search Algorithm. The first block prepares the initial search space. Then, the Grover operator, comprised by four functions, is repeated $r$ times. The four functions are: the Oracle function, and additional three functions that implements the Diffuser.
## Classical Postprocess Function Below we define a function that runs a quantum program of the Grover's search algorithm, for an increasing number of repetitions, until a marked state is found. ```python theme={null} import numpy as np from classiq import * from classiq.qmod.symbolic import pi def repeat_grover_until_success( grover_search_qprog, classical_formula, num_shots=1000, threshold=0.45, max_iter=4 ): """ Runs a grover_search_qprog with different powers, given by a CInt parameter `r`. """ i = 0 r_previous = None with ExecutionSession( grover_search_qprog, ExecutionPreferences(num_shots=num_shots) ) as es: while i < max_iter: r = int(np.ceil(np.pi / 4 * np.sqrt(2**i))) if r == r_previous: # skip duplicate r i += 1 continue print(f"running Grover with {r} repetitions") res = es.sample({"r": r}) for sample in res.parsed_counts: if ( classical_formula(**sample.state) and sample.shots / num_shots > threshold ): print( f"Success! a solution was found with probability larger than {threshold}, using {r} repetitions" ) return res, r r_previous = r i += 1 print( f"Could not find a solution, try to in increase max_iter or decrease the threshold, returning last run." ) return res, r ``` ## Example: 3-SAT Problem The 3-SAT problem \[2] is a famous $\text{NP-Complete}$ problem, a solution of which allows solving any problem in the complexity class $\text{NP}$. We treat two different 3-SAT problems, a small one and a larger one. For illustration, we define a function for printing the truth table of SAT problems. ```python theme={null} import itertools def print_truth_table(num_variables, boolean_func): variables = [f"x{i}" for i in range(num_variables)] combinations = list(itertools.product([0, 1], repeat=num_variables)) header = " ".join([f"{var:<5}" for var in variables]) + " | Result" print(header) print("-" * len(header)) for combination in combinations: result = boolean_func(list(combination)) != 0 # pass as array values_str = " ".join([f"{val:<5}" for val in combination]) print(f"{values_str} | {result:<5}") ``` We start with a small problem: # ## Small 3-SAT Formula We specify a 3-SAT formula in the so-called Conjunctive Normal Form (CNF), that requires a solution: $$ (x_1 \lor x_2 \lor x_3) \land (\neg x_1 \lor x_2 \lor x_3) \land (\neg x_1 \lor \neg x_2 \lor \neg x_3) \land (\neg x_1 \lor \neg x_2 \lor x_3) \land (x_1 \lor x_2 \lor \neg x_3) \land (\neg x_1 \lor x_2 \lor \neg x_3) $$ ```python theme={null} NUM_VARIABLES = 3 def small_3sat_formula(x): return ( (x[0] | x[1] | x[2]) & (~x[0] | x[1] | x[2]) & (~x[0] | ~x[1] | ~x[2]) & (~x[0] | ~x[1] | x[2]) & (x[0] | x[1] | ~x[2]) & (~x[0] | x[1] | ~x[2]) ) ``` We can see that the formula has two possible solutions: ```python theme={null} print_truth_table(NUM_VARIABLES, small_3sat_formula) ``` **Output:** ``` x0 x1 x2 | Result ---------------------------- 0 0 0 | 0 0 0 1 | 0 0 1 0 | 1 0 1 1 | 1 1 0 0 | 0 1 0 1 | 0 1 1 0 | 0 1 1 1 | 0 ``` We define the Grover search model for finding the solution. To specify the model, we use the standard `phase_oracle` that transforms 'digital' oracle; i.e., $|x\rangle|0\rangle \rightarrow |x\rangle|f(x)\rangle$ to a phase oracle $|x\rangle \rightarrow (-1)^{f(x)}|x\rangle$. The predicate that we pass to the phase oracle is simply given by the 3-CNF formula defined above. ```python theme={null} @qperm def sat_oracle(x: Const[QArray], res: QBit): res ^= small_3sat_formula(x) @qfunc def main(r: CInt, x: Output[QArray[NUM_VARIABLES]]): allocate(x) hadamard_transform(x) power( r, lambda: grover_operator( lambda vars: phase_oracle(sat_oracle, vars), hadamard_transform, x ), ) qprog_small_3sat = synthesize( main, constraints=Constraints(optimization_parameter="width") ) ``` ```python theme={null} show(qprog_small_3sat) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3FmT12Bt2vVDXbH5aXfVMN8YCz5 ``` We execute our repeat until success function: ```python theme={null} res_3_sat_small, r = repeat_grover_until_success(qprog_small_3sat, small_3sat_formula) ``` **Output:** ``` running Grover with 1 repetitions Success! a solution was found with probability larger than 0.45, using 1 repetitions ``` We can see that a single iteration was needed to find solutions with high probability: ```python theme={null} n_solutions = 3 df = res_3_sat_small.dataframe df_new = df.head(n_solutions).copy() first_col = df.columns[0] df_new["f(x)"] = df_new[first_col].apply(small_3sat_formula) print("The quantum search result:") df_new ``` **Output:** ``` The quantum search result: ``` | | x | counts | probability | bitstring | f(x) | | - | ---------- | ------ | ----------- | --------- | ---- | | 0 | \[0, 1, 1] | 510 | 0.51 | 110 | 1 | | 1 | \[0, 1, 0] | 490 | 0.49 | 010 | 1 | # ## Large 3-SAT Formula We continue with a larger example: ```python theme={null} def large_3sat_formula(x): return ( (x[1] | x[2] | x[3]) & (~x[0] | x[1] | x[2]) & (~x[0] | x[1] | ~x[2]) & (~x[0] | ~x[1] | x[2]) & (x[0] | ~x[1] | ~x[2]) & (x[0] | ~x[1] | x[2]) & (~x[0] | ~x[1] | ~x[3]) & (~x[0] | ~x[1] | x[3]) & (~x[1] | ~x[2] | ~x[3]) & (x[1] | ~x[2] | x[3]) & (x[0] | ~x[2] | x[3]) & (x[0] | ~x[1] | ~x[3]) & (~x[0] | ~x[1] | ~x[2]) ) NUM_VARIABLES_LARGE = 4 print_truth_table(NUM_VARIABLES_LARGE, large_3sat_formula) ``` **Output:** ``` x0 x1 x2 x3 | Result ----------------------------------- 0 0 0 0 | 0 0 0 0 1 | 1 0 0 1 0 | 0 0 0 1 1 | 1 0 1 0 0 | 0 0 1 0 1 | 0 0 1 1 0 | 0 0 1 1 1 | 0 1 0 0 0 | 0 1 0 0 1 | 0 1 0 1 0 | 0 1 0 1 1 | 0 1 1 0 0 | 0 1 1 0 1 | 0 1 1 1 0 | 0 1 1 1 1 | 0 ``` The procedure is identical to the small use-case, just changing `small_3sat_formula` to `large_3sat_formula`. ```python theme={null} @qperm def sat_oracle(x: Const[QArray], res: QBit): res ^= large_3sat_formula(x) @qfunc def main(r: CInt, x: Output[QArray[NUM_VARIABLES_LARGE]]): allocate(x) hadamard_transform(x) power( r, lambda: grover_operator( lambda vars: phase_oracle(sat_oracle, vars), hadamard_transform, x ), ) qprog_large_3sat = synthesize( main, constraints=Constraints(optimization_parameter="width") ) show(qprog_large_3sat) res_3_sat_large, r = repeat_grover_until_success(qprog_large_3sat, large_3sat_formula) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3FmT2Y3O7HtbYvfoHLxPyITUsa4 running Grover with 1 repetitions running Grover with 2 repetitions Success! a solution was found with probability larger than 0.45, using 2 repetitions ``` We can print the five most probable solutions: ```python theme={null} n_solutions = 5 df = res_3_sat_large.dataframe df_new = df.head(n_solutions).copy() first_col = df.columns[0] df_new["f(x)"] = df_new[first_col].apply(large_3sat_formula) df_new ``` | | x | counts | probability | bitstring | f(x) | | - | ------------- | ------ | ----------- | --------- | ---- | | 0 | \[0, 0, 0, 1] | 491 | 0.491 | 1000 | 1 | | 1 | \[0, 0, 1, 1] | 445 | 0.445 | 1100 | 1 | | 2 | \[0, 1, 0, 0] | 7 | 0.007 | 0010 | 0 | | 3 | \[1, 0, 0, 0] | 6 | 0.006 | 0001 | 0 | | 4 | \[1, 0, 1, 0] | 6 | 0.006 | 0101 | 0 | We can see that the amplitude of the two "marked" solutions is amplified. ## Example: Graph Cut Search Problem The "Maximum Cut Problem" (MaxCut) \[3] is an example of a combinatorial optimization problem. It refers to finding a partition of a graph into two sets, such that the number of edges between the two sets is the maximum. The MaxCut problem is defined as follows: Given a graph $G=(V,E)$ with $|V|=n$ nodes and $E$ edges, a cut is defined as a partition of the graph into two complementary subsets of nodes. The gaol is to find a cut where the number of edges between the two subsets is the maximum. We can represent a cut, and the number of its connecting edges as follows: * $x\in \{0,1\}^n$ is a binary vector of size $n$ that represents a cut: assigning 0 and 1 to nodes in the first and second subsets, respectively. * $C(x)=\sum_{(i,j)}x_i (1-x_j)+x_j (1-x_i)=\sum_{(i,j)}x_i \oplus x_j$ gives the number of connecting edges for a given cut. The MaxCut problem cannot be cast directly into a Grover's search algorithm, as we task to find the maximum of a function, rather than some "marked" solutions. One approach is to apply Grover's algorithms for formulas of the form $C(x)\geq T$, for some fixed value of $T$: We initialize a threshold $T$, then run Grover's algorithm with an oracle that marks cuts with $C(x)\geq T$ to amplify promising candidates. If a better cut is found, we update $T$ and repeat until no improvement is likely (or a preset budget is reached). In this notebook we show a solution for a single instance of this procedure, i.e., taking one example with some value for $T$. We initiate a specific graph whose maximum cut is 5: ```python theme={null} import networkx as nx # Create graph G = nx.Graph() G.add_nodes_from([0, 1, 2, 3, 4]) G.add_edges_from([(0, 1), (0, 2), (1, 2), (1, 3), (2, 4), (3, 4)]) pos = nx.planar_layout(G) nx.draw_networkx(G, pos=pos, with_labels=True, alpha=0.8, node_size=500) ``` output Constructing a Grover's search algorithm is done in similar to the 3-SAT examples, we only require to define a predicate formula. In this example we set the cut size (the value of $T$) to 4. ```python theme={null} CUT_SIZE = 4 # cut formulas def is_cross_cut_edge(x1: int, x2: int) -> int: return x1 ^ x2 def cut(x): return sum(is_cross_cut_edge(x[node1], x[node2]) for (node1, node2) in G.edges) def cut_predicate(cut_size, x): return cut(x) >= cut_size ``` ```python theme={null} @qperm def cut_oracle(cut_size: CInt, nodes: Const[QArray], res: QBit): res ^= cut_predicate(cut_size, nodes) @qfunc def main(r: CInt, nodes: Output[QArray[len(G.nodes)]]): allocate(nodes) hadamard_transform(nodes) power( r, lambda: grover_operator( lambda vars: phase_oracle( lambda vars, res: cut_oracle(CUT_SIZE, vars, res), vars ), hadamard_transform, nodes, ), ) qprog_max_cut = synthesize( main, constraints=Constraints(optimization_parameter="width") ) show(qprog_max_cut) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3FmTDk5ei57GiYWKoUwNtUX3ZyI ``` ```python theme={null} # In this example we reduce the threshold, since typically, more than one solution is expected res_max_cut, r = repeat_grover_until_success( qprog_max_cut, lambda nodes: cut_predicate(CUT_SIZE, nodes), threshold=0.1 ) ``` **Output:** ``` running Grover with 1 repetitions Success! a solution was found with probability larger than 0.1, using 1 repetitions ``` Upon printing the result, we see that our execution of Grover's algorithm successfully found the satisfying assignments for the input formula: ```python theme={null} n_solutions = 32 df = res_max_cut.dataframe df_new = df.head(n_solutions).copy() first_col = df.columns[0] df_new["f(x)"] = df_new[first_col].apply(lambda x: cut_predicate(CUT_SIZE, x)) df_new ``` | | nodes | counts | probability | bitstring | f(x) | | -- | ---------------- | ------ | ----------- | --------- | ----- | | 0 | \[0, 1, 0, 0, 1] | 105 | 0.105 | 10010 | True | | 1 | \[1, 0, 0, 1, 1] | 103 | 0.103 | 11001 | True | | 2 | \[1, 0, 0, 1, 0] | 101 | 0.101 | 01001 | True | | 3 | \[0, 1, 1, 1, 0] | 98 | 0.098 | 01110 | True | | 4 | \[0, 1, 1, 0, 1] | 98 | 0.098 | 10110 | True | | 5 | \[1, 0, 1, 1, 0] | 95 | 0.095 | 01101 | True | | 6 | \[0, 1, 1, 0, 0] | 94 | 0.094 | 00110 | True | | 7 | \[1, 1, 0, 0, 1] | 87 | 0.087 | 10011 | True | | 8 | \[1, 0, 0, 0, 1] | 86 | 0.086 | 10001 | True | | 9 | \[0, 0, 1, 1, 0] | 84 | 0.084 | 01100 | True | | 10 | \[0, 0, 1, 1, 1] | 5 | 0.005 | 11100 | False | | 11 | \[1, 1, 0, 0, 0] | 4 | 0.004 | 00011 | False | | 12 | \[0, 0, 0, 0, 1] | 4 | 0.004 | 10000 | False | | 13 | \[1, 0, 0, 0, 0] | 3 | 0.003 | 00001 | False | | 14 | \[1, 0, 1, 0, 0] | 3 | 0.003 | 00101 | False | | 15 | \[0, 0, 0, 1, 0] | 3 | 0.003 | 01000 | False | | 16 | \[0, 1, 0, 1, 0] | 3 | 0.003 | 01010 | False | | 17 | \[1, 1, 0, 1, 0] | 3 | 0.003 | 01011 | False | | 18 | \[0, 0, 0, 1, 1] | 3 | 0.003 | 11000 | False | | 19 | \[0, 1, 0, 1, 1] | 3 | 0.003 | 11010 | False | | 20 | \[0, 0, 0, 0, 0] | 2 | 0.002 | 00000 | False | | 21 | \[0, 1, 0, 0, 0] | 2 | 0.002 | 00010 | False | | 22 | \[1, 1, 1, 1, 0] | 2 | 0.002 | 01111 | False | | 23 | \[0, 1, 1, 1, 1] | 2 | 0.002 | 11110 | False | | 24 | \[0, 0, 1, 0, 0] | 1 | 0.001 | 00100 | False | | 25 | \[1, 1, 1, 0, 0] | 1 | 0.001 | 00111 | False | | 26 | \[1, 0, 1, 0, 1] | 1 | 0.001 | 10101 | False | | 27 | \[1, 1, 1, 0, 1] | 1 | 0.001 | 10111 | False | | 28 | \[1, 1, 0, 1, 1] | 1 | 0.001 | 11011 | False | | 29 | \[1, 0, 1, 1, 1] | 1 | 0.001 | 11101 | False | | 30 | \[1, 1, 1, 1, 1] | 1 | 0.001 | 11111 | False | The satisfying assignments are \~100 times more probable than the unsatisfying assignments. We print the corresponding graph for one of them: ```python theme={null} result_parsed = df_new["nodes"][0] ``` ```python theme={null} import matplotlib.pyplot as plt edge_widths = [ is_cross_cut_edge( int(result_parsed[i]), int(result_parsed[j]), ) + 0.5 for i, j in G.edges ] node_colors = [int(c) for c in result_parsed] nx.draw_networkx( G, pos=pos, with_labels=True, alpha=0.8, node_size=500, node_color=node_colors, width=edge_widths, cmap=plt.cm.rainbow, ) ``` output ## References \[1]: [L. K. Grover, "A fast quantum mechanical algorithm for database search", Proceedings of the 28th Annual ACM Symposium on Theory of Computing (STOC '96), pp. 212-219, 1996.](https://dl.acm.org/doi/10.1145/237814.237866) \[2]: [The 3-SAT problem](https://en.wikipedia.org/wiki/Boolean_satisfiability_problem#3-satisfiability) \[3]: [The Maximum Cut problem](https://en.wikipedia.org/wiki/Maximum_cut) # Grover Mixers for QAOA Source: https://docs.classiq.io/explore/algorithms/search_and_optimization/grover_mixer_qaoa/gm_qaoa Open this notebook in GitHub to run it yourself Grover Mixers for QAOA (GM-QAOA) is one of the algorithms applied to constrained optimization problems, where the mixer operator in QAOA is replaced by a parameterized Grover diffuser operator that utilizes an equal superposition of all feasible solutions \[[1](#gm-qaoa)]. In the potential challenges of standard QAOA, the cost operator (which applies phases based on the objective function) and the mixer operator (which explores the state space) are alternately applied. This method sometimes fails to reach the desired solution in constrained problems. GM-QAOA is a variant of the Quantum Approximate Optimization Algorithm (QAOA) and is particularly designed for constrained optimization problems. In the standard QAOA, the variational circuit alternates between the following two types of unitaries: 1. Phase-separation operator (based on the cost Hamiltonian $\hat{H}_C$), 2. Mixer (a unitary to explore the solution space). For unconstrained problems, a simple mixer $(\prod_i e^{-i \beta X_i})$ is sufficient. GM-QAOA avoids this difficulty by shifting the complexity of mixer design into the state preparation step. ## Exercise Let's assume that we will solve the following problem using QAOA and GM-QAOA. We impose the following constraint: $$ \sum_{i=0}^{3} x_i = 1 $$ The objective function is given by: $$ f(x_0,x_1,x_2,x_3) = x_0 + 2(x_1+x_2+x_3) - 3x_2 + 10x_3 $$ Find $x_0, x_1, x_2, x_3$ that minimize $f$. ## Algorithm Description In GM-QAOA, a Grover-type mixer is introduced: $$ U_M(\beta) = e^{-i \beta |F\rangle\langle F|}, $$ where $|F\rangle$ is the equal superposition of all feasible solutions: $$ |F\rangle = \frac{1}{\sqrt{|F|}} \sum_{x \in F} |x\rangle. $$ If we define $\hat{U}_S$ as the unitary that generates $|F\rangle$ from the initial state $|0\rangle^{\otimes n}$, $$ \hat{U}_S |0\rangle^{\otimes n} = |F\rangle, $$ then the GM-QAOA circuit of depth $p$ can be expressed as: $$ |\beta,\gamma\rangle = \hat{U}_M(\beta_p) \hat{U}_P(\gamma_p) \cdots \hat{U}_M(\beta_1) \hat{U}_P(\gamma_1) \hat{U}_S |0\rangle^{\otimes n}, $$ where $$ \hat{U}_P(\gamma) = e^{-i\gamma \hat{H}_C}. $$ ## Approach 1: QAOA First, lets try to implement general QAOA. ```python theme={null} import math import matplotlib.pyplot as plt import numpy as np import scipy from tqdm import tqdm from classiq import * total_qbit = 4 # constraint is one hot. def constraint(x: QArray[QBit]): const = (x[0] + x[1] + x[2] + x[3] - 1) ** 2 return const def object_func(x: QArray[QBit]): obj = 1 * x[0] + 2 * (x[1] + x[2] + x[3]) - 3 * x[2] + 10 * x[3] return obj def cost(x: QArray[QBit]): return constraint(x) + object_func(x) ``` ```python theme={null} @qfunc def cost_layer(gamma: CReal, x: QArray[QBit, total_qbit]): phase(cost(x), gamma) @qfunc def mixer_layer(beta: CReal, qba: QArray): apply_to_all(lambda q: RX(beta, q), qba) @qfunc def qaoa_ansatz( cost_layer: QCallable[CReal, QArray], mixer_layer: QCallable[CReal, QArray], gammas: CArray[CReal], betas: CArray[CReal], qba: QArray, ): repeat( betas.len, lambda i: [ cost_layer(gammas[i], qba), mixer_layer(betas[i], qba), ], ) ``` ```python theme={null} NUM_LAYERS = 3 @qfunc def main( params: CArray[CReal, NUM_LAYERS * 2], x: Output[QArray[QBit, total_qbit]], ): allocate(x) gammas = params[0:NUM_LAYERS] betas = params[NUM_LAYERS : 2 * NUM_LAYERS] hadamard_transform(x) qaoa_ansatz(cost_layer, mixer_layer, gammas, betas, x) ``` ```python theme={null} qprog_qaoa = synthesize(main) show(qprog_qaoa) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/36sSAbwMcafVhXfTkJqxP463bgK ``` ```python theme={null} NUM_SHOTS = 1000 MAX_ITERATIONS = 30 # for NUM_LAYERS=3, initial_params = [γ0,γ1,γ2,β0,β1,β2] = [0.0pi, 0.5pi, 1.0pi, 1.0pi, 0.5pi, 0.0pi] initial_params = ( np.concatenate((np.linspace(0, 1, NUM_LAYERS), np.linspace(1, 0, NUM_LAYERS))) * math.pi ) cost_trace = [] def evaluate_params(es, params): cost_estimation = es.estimate_cost( cost_func=lambda state: cost(state["x"]), parameters={"params": params.tolist()} ) cost_trace.append(cost_estimation) return cost_estimation es = ExecutionSession( qprog_qaoa, execution_preferences=ExecutionPreferences(num_shots=NUM_SHOTS) ) with tqdm(total=MAX_ITERATIONS, desc="Optimization Progress", leave=True) as pbar: def progress_bar(xk: np.ndarray) -> None: pbar.update(1) # increment progress bar final_params = scipy.optimize.minimize( fun=lambda params: evaluate_params(es, params), x0=initial_params, method="COBYLA", options={"maxiter": MAX_ITERATIONS}, callback=progress_bar, ).x.tolist() print(f"Optimized parameters: {final_params}") plt.plot(cost_trace) plt.xlabel("Iterations") plt.ylabel("Cost") plt.title("Cost convergence") ``` **Output:** ``` Optimization Progress: 47%|██████████████████████████████████████████████████████████████████████████████████████▎ | 14/30 [00:42<00:48, 3.00s/it] ``` **Output:** ``` Optimized parameters: [-0.007695042354776885, 2.5917267175844785, 4.1593990117621535, 4.147283293679248, 1.5749236931421726, 0.0038312765627748903] ``` **Output:** ``` Text(0.5, 1.0, 'Cost convergence') ``` output ```python theme={null} es = ExecutionSession( qprog_qaoa, execution_preferences=ExecutionPreferences(num_shots=1000) ) res_qaoa = es.sample({"params": final_params}) es.close() ``` ```python theme={null} for sampled in res_qaoa.parsed_counts: x = sampled.state["x"] print(f"solution={x} probability={sampled.shots/NUM_SHOTS} cost={cost(x)}") ``` **Output:** ``` solution=[1, 0, 1, 0] probability=0.417 cost=1 solution=[1, 0, 0, 0] probability=0.19 cost=1 solution=[1, 1, 1, 0] probability=0.086 cost=6 solution=[1, 0, 0, 1] probability=0.064 cost=14 solution=[0, 1, 1, 0] probability=0.055 cost=2 solution=[0, 0, 0, 0] probability=0.047 cost=1 solution=[1, 1, 1, 1] probability=0.028 cost=23 solution=[0, 1, 0, 1] probability=0.027 cost=15 solution=[0, 0, 1, 1] probability=0.024 cost=12 solution=[1, 1, 0, 0] probability=0.02 cost=4 solution=[0, 0, 0, 1] probability=0.014 cost=12 solution=[0, 0, 1, 0] probability=0.009 cost=-1 solution=[0, 1, 1, 1] probability=0.007 cost=17 solution=[0, 1, 0, 0] probability=0.006 cost=2 solution=[1, 1, 0, 1] probability=0.005 cost=19 solution=[1, 0, 1, 1] probability=0.001 cost=16 ``` ## Approach 2: GM-QAOA Next, we use GM-QAOA. ```python theme={null} from classiq import * total_qbit = 4 @qfunc def initial_state(x: QArray): prepare_dicke_state(1, x[0:4]) def object_func(x: QArray[QBit]): obj = 1 * x[0] + 2 * (x[1] + x[2] + x[3]) - 3 * x[2] + 10 * x[3] return obj def cost(x: QArray[QBit]): return object_func(x) ``` ```python theme={null} @qfunc def cost_layer(gamma: CReal, x: QArray[QBit, total_qbit]): phase(cost(x), gamma) @qfunc def mixer_layer(beta: CReal, x: QArray): x_lsbs = QNum(size=x.len - 1) x_msb = QBit() within_apply( lambda: (invert(lambda: initial_state(x)), bind(x, [x_lsbs, x_msb]), X(x_msb)), lambda: control(x_lsbs == 0, lambda: RZ(-1.0 / np.pi * beta, x_msb)), ) @qfunc def qaoa_ansatz( cost_layer: QCallable[CReal, QArray], mixer_layer: QCallable[CReal, QArray], gammas: CArray[CReal], betas: CArray[CReal], qba: QArray, ): repeat( betas.len, lambda i: [ cost_layer(gammas[i], qba), mixer_layer(betas[i], qba), ], ) ``` ```python theme={null} NUM_LAYERS = 3 @qfunc def main( params: CArray[CReal, NUM_LAYERS * 2], x: Output[QArray[QBit, total_qbit]], ): allocate(x) gammas = params[0:NUM_LAYERS] betas = params[NUM_LAYERS : 2 * NUM_LAYERS] initial_state(x) qaoa_ansatz(cost_layer, mixer_layer, gammas, betas, x) ``` ```python theme={null} qprog_gmqaoa = synthesize(main) show(qprog_gmqaoa) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/36sSHO9NcLUSkYBK38LSus2ePWp ``` ```python theme={null} import math import matplotlib.pyplot as plt import numpy as np import scipy from tqdm import tqdm NUM_SHOTS = 1000 MAX_ITERATIONS = 30 # start with a linear scheduling guess initial_params = ( np.concatenate((np.linspace(0, 1, NUM_LAYERS), np.linspace(1, 0, NUM_LAYERS))) * math.pi ) cost_trace = [] def evaluate_params(es, params): cost_estimation = es.estimate_cost( cost_func=lambda state: cost(state["x"]), parameters={"params": params.tolist()} ) cost_trace.append(cost_estimation) return cost_estimation es = ExecutionSession( qprog_gmqaoa, execution_preferences=ExecutionPreferences(num_shots=NUM_SHOTS) ) with tqdm(total=MAX_ITERATIONS, desc="Optimization Progress", leave=True) as pbar: def progress_bar(xk: np.ndarray) -> None: pbar.update(1) # increment progress bar final_params = scipy.optimize.minimize( fun=lambda params: evaluate_params(es, params), x0=initial_params, method="COBYLA", options={"maxiter": MAX_ITERATIONS}, callback=progress_bar, ).x.tolist() print(f"Optimized parameters: {final_params}") plt.plot(cost_trace) plt.xlabel("Iterations") plt.ylabel("Cost") plt.title("Cost convergence") ``` **Output:** ``` Optimization Progress: 43%|████████████████████████████████████████████████████████████████████████████████▏ | 13/30 [00:44<00:58, 3.42s/it] ``` **Output:** ``` Optimized parameters: [0.6990844681681198, 1.8512527414882123, 4.548042253484994, 3.1007941938977672, 2.6781356635974753, -0.1077908745220168] ``` **Output:** ``` Text(0.5, 1.0, 'Cost convergence') ``` output ```python theme={null} es = ExecutionSession( qprog_gmqaoa, execution_preferences=ExecutionPreferences(num_shots=1000) ) res_gmqaoa = es.sample({"params": final_params}) es.close() ``` ```python theme={null} for sampled in res_gmqaoa.parsed_counts: x = sampled.state["x"] print(f"solution={x} probability={sampled.shots/NUM_SHOTS} cost={cost(x)}") ``` **Output:** ``` solution=[0, 0, 1, 0] probability=0.412 cost=-1 solution=[1, 0, 0, 0] probability=0.384 cost=1 solution=[0, 1, 0, 0] probability=0.132 cost=2 solution=[0, 0, 0, 1] probability=0.072 cost=12 ``` ```python theme={null} import pandas as pd df_qaoa = res_qaoa.dataframe df_gmqaoa = res_gmqaoa.dataframe df_qaoa["x"] = df_qaoa["x"].apply( lambda v: "".join(map(str, v)) if isinstance(v, list) else v ) df_gmqaoa["x"] = df_gmqaoa["x"].apply( lambda v: "".join(map(str, v)) if isinstance(v, list) else v ) df_qaoa = df_qaoa.rename(columns={"probability": "QAOA"}) df_gmqaoa = df_gmqaoa.rename(columns={"probability": "GMQAOA"}) df = pd.merge(df_qaoa, df_gmqaoa, on="x", how="outer").fillna(0) df = df[["x", "QAOA", "GMQAOA"]] df.set_index("x").plot.bar(figsize=(10, 4)) ``` **Output:** ``` ``` output When $x = 0010$, the minimum value becomes $f = -1$, and GM-QAOA exhibits a stronger amplification effect compared to QAOA. ## References \[1]: [A. Bärtschi, S. Eidenbenz. "Grover Mixers for QAOA: Shifting Complexity from Mixer Design to State Preparation" arXiv:2006.00354 (2020).](https://arxiv.org/abs/2006.00354) # Quantum Likelihood Estimation Source: https://docs.classiq.io/explore/algorithms/search_and_optimization/quantum_likelihood_estimation/quantum_likelihood_estimation Open this notebook in GitHub to run it yourself > **Quantum Likelihood Estimation** (QLE) [\[1, 2\]](#wiebe14a), is a hybrid algorithm for Hamiltonian learning, originally proposed for efficiently learning Hamiltonians from quantum experiments. The basic setting is that the algorithm can access the true Hamiltonian only through the evolution it generates, namely the unitary $U(t)=e^{-iHt}$ for an arbitrarily chosen time $t$. Such experiments on the true physical system are considered an expensive resource, analogous to oracle queries, and the goal is to perform as few experiments as possible. QLE has also been demonstrated experimentally [\[4\]](#wang17). Ref. [\[3\]](#qle) suggested an optimized approach based on an information-theoretic framework, minimizing the number of oracle calls by jointly optimizing all circuit parameters at each iteration. The QLE problem is defined as follows: > > * **Input:** A set of possible Hamiltonians that may govern the system. > > * **Promise:** One of the Hamiltonians is the true Hamiltonian of the system. > > * **Output:** The The true Hamiltonian. > > *** > > **Keywords:** Hamiltonian learning, hybrid quantum-classical algorithms, Bayesian inference. This notebook implements the Optimal Quantum Likelihood Estimation algorithm, as proposed in Ref. [\[3\]](#qle), and was developed in collaboration with the paper authors. As a reference, we also implement a naive Quantum Likelihood Estimation. QLE frames Hamiltonian learning as Bayesian inference: a prior ("weights") over a discrete set of candidate Hamiltonians is updated iteratively from measurement outcomes obtained after evolving the system for a chosen time $t$. Thus, a quantum iteration consists of preparation, evolution and measurement. Between each two quantum steps, there is a classical processing phase, where we update the weights and design the next quantum step based on the weights. The efficiency of each step depends critically on how informative the measurement is. Standard QLE selects $t$ heuristically (e.g. via PGH) while keeping the state-preparation and measurement angles fixed. Ref. [\[3\]](#qle) proposes instead to **jointly optimize all five circuit parameters** - evolution time $t$ and angles $(\alpha, \beta, \theta, \phi)$ - at each iteration, so as to maximize the mutual information $I(F;Y)$ between the measurement outcome $Y$ and the unknown Hamiltonian $F$. The optimization is solved classically via simulated annealing. In the single-qubit benchmark of Ref. [\[3\]](#qle), this information-theoretic approach converges in roughly an order of magnitude fewer iterations than standard QLE. ```python theme={null} import logging import matplotlib.pyplot as plt import numpy as np from classiq import * ``` ## Defining a Single Qubit Example Following Ref. [\[3\]](#qle) we work with a single qubit example, which underlines the idea behind the optimal, information based optimization. We set the quantum model for a single qubit example The QLE circuit consists of three components. The initial state is parameterized by $\alpha \in [0, \pi]$ and $\beta \in [0, 2\pi)$: $$ |\psi_1\rangle = \cos(\alpha)\,|0\rangle + e^{i\beta}\sin(\alpha)\,|1\rangle \qquad (1) $$ *(Eq. (8) in Ref. [\[3\]](#qle))* After Hamiltonian evolution $e^{-iHt}$, the measurement basis is rotated by $W$, parameterized by $\theta \in [0, \pi]$ and $\phi \in [0, 2\pi)$: $$ W = e^{-i\theta\sigma_y/2}\,e^{-i\phi\sigma_z/2} \qquad (2) $$ *(Eq. (9) in Ref. [\[3\]](#qle))* ```python theme={null} # ranges for [alpha, beta, theta, phi] PARAMS_RANGE = [np.pi, 2 * np.pi, np.pi, 2 * np.pi] @qfunc def prepare_psi1(alpha: CReal, beta: CReal, q: QBit): RY(2 * alpha, q) phase(q == 1, beta) @qfunc def w_func(theta: CReal, phi: CReal, q: QBit): U(theta, phi, np.pi - phi, 0, q) @qfunc def hamiltonian_evolution(h: SparsePauliOp, t: CReal, q: QBit): suzuki_trotter(h, t, 1, 1, q) ``` The full circuit maps parameters $\vec{\lambda} = (\alpha, \beta, \theta, \phi)$ and evolution time $t$ to a measurement distribution. The probability of outcome $a \in \{0, 1\}$ under hypothesis $H_j$ is: $$ p_j^{(a)}(t, \vec{\lambda}) = \bigl|\langle a \mid W\,e^{-iH_j t}\,|\psi_1\rangle\bigr|^2 \qquad (3) $$ *(Eq. (6) in Ref. [\[3\]](#qle))* Since each hypothesis corresponds to a different Hamiltonian, we synthesize one parametric quantum program per hypothesis. ```python theme={null} def get_main_for_hamiltonian_evolution(h: SparsePauliOp): @qfunc def main( t: CReal, params: CArray[CReal, 4], # [alpha, beta, theta, phi] q: Output[QBit], ): allocate(q) prepare_psi1(params[0], params[1], q) hamiltonian_evolution(h, t, q) w_func(params[2], params[3], q) return main def get_qprogs_for_hamiltonian_evolutions( h_list: list[SparsePauliOp], ) -> list[QuantumProgram]: """ This function generates a list of quantum programs for a given list of Hamiltonians """ print("Starting synthesis for all parametrized qprogs") qprogs = [synthesize(get_main_for_hamiltonian_evolution(h)) for h in h_list] show(qprogs[0]) # visualize one of the qprogs print("Finished synthesis") return qprogs ``` # ## Setting Up a Specific Hamiltonian Set We define the pool of Hamiltonians and the true index for the demonstration. ```python theme={null} HAMILTONIAN_SET = [Pauli.X(0), 2 * Pauli.X(0), Pauli.Z(0), 2 * Pauli.Z(0)] TRUE_IDX = 0 # the index of the true Hamiltonian print("The set of possible Hamiltonians: ") print(*HAMILTONIAN_SET, sep=", ") print(f"The true Hamiltonian: {HAMILTONIAN_SET[TRUE_IDX]}") ``` **Output:** ``` The set of possible Hamiltonians: Pauli.X(0), 2.0*Pauli.X(0), Pauli.Z(0), 2.0*Pauli.Z(0) The true Hamiltonian: Pauli.X(0) ``` ```python theme={null} qprogs = get_qprogs_for_hamiltonian_evolutions(HAMILTONIAN_SET) ``` **Output:** ``` Starting synthesis for all parametrized qprogs Quantum program link: https://platform.classiq.io/circuit/3FunTTt1DD1MEBrZbavfly11Uhz Finished synthesis ``` ## Classical Functions for Maximum Likelihood The QLE is based on Bayesian inference. We start with a normalized list of weights, each assigned for the different hypothesis $H_j$: $$ \vec{w} = (w_0, w_1,\dots, w_{M-1}), \qquad \sum^{M-1}_{j=0}w_j = 1. $$ Then, upon measuring outcome $y$ from the true system, the likelihood of hypothesis $H_j$ is $\mathcal{L}_j = p_j^{(y)}$. The Bayesian weight update from step $k$ to $k+1$ reads: $$ w_j^{(k+1)} = \frac{w_j^{(k)}\,p_j^{(y)}}{\displaystyle\sum_i w_i^{(k)}\,p_i^{(y)}} \qquad (4), $$ where the denominator guarantees that the weights vector stays normalized; *See (Eq. (7) in Ref. [\[3\]](#qle))*. Below we define the functions `get_probs_from_qprog`, which computes $\{p_j^{(a)}\}_a$ via exact statevector simulation, and `bayes_update` that performs one step of Eq. (4). ```python theme={null} def get_probs_from_qprog(qprog: QuantumProgram, t_list, params_list) -> np.ndarray: """ Gets exact probabilities from a batch execution on a statevector simulator """ dfs = calculate_state_vector( qprog, parameters=[ {"t": t, "params": params} for t, params in zip(t_list, params_list) ], ) variable_size = qprog.model.circuit_output_types[ "q" ].size # =1 as we are looking at a single qubit case probs = np.zeros((len(t_list), 2**variable_size), dtype=float) for i, df in enumerate(dfs): probs[i, df["q"]] = df["probability"] return probs def bayes_update( qprogs: list[QuantumProgram], true_index: int, t, params, weights ) -> np.ndarray: """ Bayesian update of the weights """ # True experimental outcome from sampling the evolution of the true Hamiltonian print("Sample true evolution") df = sample(qprogs[true_index], num_shots=1, parameters={"t": t, "params": params}) outcome = int(df["q"].item()) # Likelihood under every hypothesis print("Calculate likelihood") likelihoods = np.zeros(len(qprogs)) for i, qprog in enumerate(qprogs): probs = get_probs_from_qprog(qprog, [t], [params])[0] likelihoods[i] = probs[outcome] print("Updating weights") new_weights = np.asarray(weights, dtype=float) * np.asarray( likelihoods, dtype=float ) new_weights = np.clip(new_weights, 0.0, None) new_weights /= sum(new_weights) return outcome, likelihoods, new_weights ``` ## A Standard Quantum Likelihood In the naive QLE algorithm, we simply follow a series of Bayesian updates, where the evolution time $t$ at each iteration can varies, and the other model parameters $\vec{\lambda}$ are fixed. Here $t$ is selected by the **Particle Guess Heuristic (PGH)**: draw two hypotheses $H_i,\,H_j$ from the current weight distribution and set $$ t = \frac{1}{\|H_i - H_j\|} \qquad (5) $$ *(See Ref. [\[3\]](#qle))* ```python theme={null} def pgh(h_list, weights, rng=None) -> float: """ PGH choice for the time evolution parameter: sample two hypotheses from current weights and take t = 1 / || Hi - Hj||. """ rng = np.random.default_rng() if rng is None else rng i, j = rng.choice(len(weights), size=2, replace=False, p=weights) nh = np.linalg.norm( hamiltonian_to_matrix(h_list[i]) - hamiltonian_to_matrix(h_list[j]) ) return 0.1 if nh < 1e-6 else 1.0 / nh ``` Next, we define the main `run_qle` function, which iterates the standard QLE loop: select $t$ via Eq. (5), sample the true Hamiltonian to obtain outcome $y$, and apply the Bayes update (Eq. (4)) until the posterior weight of one hypothesis exceeds the convergence threshold. ```python theme={null} def run_qle( h_list, true_index, fixed_params, threshold: float = 0.99, max_iters: int = 10_000, rng=None, qprogs=None, ): rng = np.random.default_rng() if rng is None else rng weights = np.ones(len(h_list), dtype=float) / len(h_list) history = [] if qprogs is None: qprogs = get_qprogs_for_hamiltonian_evolutions(h_list) for it in range(1, max_iters + 1): # if np.max(weights) >= threshold: if weights[true_index] > threshold: break t = pgh(h_list, weights, rng=rng) outcome, likelihoods, new_weights = bayes_update( qprogs, true_index, t, fixed_params, weights ) history.append( { "t": t, "params": fixed_params, "outcome": outcome, "likelihoods": likelihoods, "weights_before": np.array(weights, dtype=float), "weights_after": new_weights, } ) weights = new_weights print(f"Iteration {it+1}: weights = {new_weights}") return { "final_weights": weights, "num_iters": len(history), "history": history, "guess_idx": int(np.argmax(weights)), } ``` # ## Run for the Specific Example ```python theme={null} params = [1.2566, 0.5712, 0.7854, 2 * np.pi] # fixed parameters ``` ```python theme={null} from classiq.execution.functions.util._logging import _logger _logger.setLevel(logging.WARNING) # disable the logging for the loop res_qle = run_qle( h_list=HAMILTONIAN_SET, true_index=0, fixed_params=params, threshold=0.99, max_iters=10_000, rng=np.random.default_rng(1234), qprogs=qprogs, ) _logger.setLevel(logging.INFO) # return to default after the loop ``` **Output:** ``` Sample true evolution Calculate likelihood Updating weights Iteration 2: weights = [0.21945155 0.16182579 0.2884259 0.33029677] Sample true evolution Calculate likelihood Updating weights Iteration 3: weights = [0.18061965 0.0982162 0.31200116 0.40916299] Sample true evolution Calculate likelihood Updating weights Iteration 4: weights = [0.39412498 0.3123227 0.23684465 0.05670767] Sample true evolution Calculate likelihood Updating weights Iteration 5: weights = [0.40573844 0.43821025 0.1378154 0.01823591] Sample true evolution Calculate likelihood Updating weights Iteration 6: weights = [4.01493859e-01 5.74223064e-01 2.39658413e-02 3.17235444e-04] Sample true evolution Calculate likelihood Updating weights Iteration 7: weights = [3.44299545e-01 6.52083966e-01 3.61170613e-03 4.78256498e-06] Sample true evolution Calculate likelihood Updating weights Iteration 8: weights = [2.84910678e-01 7.14564025e-01 5.25226788e-04 6.95752525e-08] Sample true evolution Calculate likelihood Updating weights Iteration 9: weights = [2.31398728e-01 7.68526305e-01 7.49654744e-05 9.93410316e-10] Sample true evolution Calculate likelihood Updating weights Iteration 10: weights = [1.85249101e-01 8.14740352e-01 1.05467492e-05 1.39812319e-11] Sample true evolution Calculate likelihood Updating weights Iteration 11: weights = [1.46539359e-01 8.53459175e-01 1.46615194e-06 1.94430884e-13] Sample true evolution Calculate likelihood Updating weights Iteration 12: weights = [7.06705970e-01 2.93270265e-01 2.37649327e-05 3.57632923e-12] Sample true evolution Calculate likelihood Updating weights Iteration 13: weights = [6.45352824e-01 3.54643362e-01 3.81379397e-06 5.74139704e-14] Sample true evolution Calculate likelihood Updating weights Iteration 14: weights = [5.78799580e-01 4.21199819e-01 6.01105089e-07 9.05253488e-16] Sample true evolution Calculate likelihood Updating weights Iteration 15: weights = [5.09252228e-01 4.90747679e-01 9.29431354e-08 1.40022095e-17] Sample true evolution Calculate likelihood Updating weights Iteration 16: weights = [9.35747687e-01 6.42517388e-02 5.74005797e-07 9.81318901e-17] Sample true evolution Calculate likelihood Updating weights Iteration 17: weights = [9.95129322e-01 4.86862644e-03 2.05168259e-06 3.98032646e-16] ``` ```python theme={null} assert ( res_qle["guess_idx"] == TRUE_IDX ), f"QLE converged to hypothesis {res_qle['guess_idx']} but expected {TRUE_IDX}" assert ( res_qle["final_weights"][TRUE_IDX] >= 0.99 ), f"QLE weight for true hypothesis is {res_qle['final_weights'][TRUE_IDX]:.4f}, expected >= 0.99" print( f"QLE passed: converged to correct hypothesis {TRUE_IDX} in {res_qle['num_iters']} iterations" ) ``` **Output:** ``` QLE passed: converged to correct hypothesis 0 in 16 iterations ``` # ## Plotting the Convergence History ```python theme={null} weights_history = np.array([h["weights_after"] for h in res_qle["history"]]).T ``` ```python theme={null} for i in range(4): plt.plot(np.arange(1, len(weights_history[i]) + 1), weights_history[i], "o-") plt.title("Standard Quantum Maximum Likelihood") plt.xlabel("Iteration", fontsize=16) plt.ylabel(r"$w_i$", fontsize=16) plt.tick_params("both", labelsize=16) plt.legend([rf"$H_{l}$" for l in range(len(HAMILTONIAN_SET))]) ``` **Output:** ``` ``` output ## An Optimized Quantum Likelihood Next, we follow the procedure introduced in Ref. [\[3\]](#qle). We find the optimal parameters that maximizes the mutual information, and use simulated annealing for finding them. The optimized QLE selects $(t, \vec{\lambda})$ by minimizing the conditional Shannon entropy $H(F|Y)$ over the current posterior $\{w_j\}$: $$ H(F|Y) = \sum_y p(y)\,H(F \mid Y{=}y), \qquad p(y) = \sum_j w_j\,p_j^{(y)} \qquad (6) $$ *(Eq. (10) in Ref. [\[3\]](#qle))* Minimizing $H(F|Y)$ is equivalent to maximizing the mutual information $I(F;Y)$ between the true Hamiltonian $F$ and the measurement outcome $Y$. Below, we define `conditional_entropy_cost_batch` that calculates the cost function for a batch of parameters. ```python theme={null} EPS = 1e-12 # for avoiding division by zero or log of zero def shannon_entropy(p: np.ndarray) -> float: p = np.asarray(p, dtype=float) p = p[p > EPS] if p.size == 0: return 0.0 return float(-np.sum(p * np.log2(p))) def conditional_entropy_cost_batch( weights: list, qprogs_list: list[QuantumProgram], t_list: list[float], params_list: list[list[float]], ) -> np.ndarray: weights = np.asarray(weights, dtype=float) batch_size = len(t_list) # cond_probs[f, b, y] = p(y | f, params_b) cond_probs = np.array( [get_probs_from_qprog(qprog, t_list, params_list) for qprog in qprogs_list], dtype=float, ) # shape (N, B, 2^problem_size) # py[b, y] = sum_f w_f p(y|f,b) py = np.tensordot(weights, cond_probs, axes=(0, 0)) # shape (B, 2^problem_size) costs = np.zeros(batch_size, dtype=float) for y in range(cond_probs.shape[2]): py_y = py[:, y] valid = py_y > EPS if not np.any(valid): continue posterior_y = (weights.reshape(-1, 1) * cond_probs[:, :, y]) / py_y.reshape( 1, -1 ) entropies = np.zeros(batch_size, dtype=float) for b in np.where(valid)[0]: entropies[b] = shannon_entropy(posterior_y[:, b]) costs += py_y * entropies return costs ``` We also define two utility functions for wrapping the parameters values in a periodic range (since we consider angles here), and the simulated annealing optimizer `optimize_entropy_sa`. ```python theme={null} def min_energy_gap_from_paulis(h_list) -> float: gaps = [] for h in h_list: hm = hamiltonian_to_matrix(h) evals = np.linalg.eigvalsh(hm) evals = np.sort(np.real_if_close(evals)) diffs = np.diff(evals) diffs = diffs[diffs > EPS] if diffs.size > 0: gaps.append(float(np.min(diffs))) return min(gaps) if gaps else 1.0 def wrap_params(x: np.ndarray, ranges: list) -> np.ndarray: return np.array( [np.mod(x[i], ranges[i]) for i in range(len(ranges))], dtype=float, ) ``` ```python theme={null} def optimize_entropy_sa( weights, h_list, qprogs_list, n_steps: int = 200, n_neighbors: int = 20, cooling: float = 0.90, seed=None, ): rng = np.random.default_rng(seed) delta_min = min_energy_gap_from_paulis(h_list) t_max = 2 * np.pi / delta_min # ranges for [t, alpha, beta, theta, phi] ranges = np.array([t_max] + PARAMS_RANGE, dtype=float) params_len = len(ranges) x = wrap_params(rng.random(params_len) * ranges, ranges) current = x.copy() current_cost = conditional_entropy_cost_batch( weights, qprogs_list, [current[0]], [current[1:].tolist()] )[0] best = current.copy() best_cost = current_cost T = 1.0 for step_idx in range(n_steps): print(f"SA step {step_idx+1}/{n_steps}") neighbors = current + T * ( rng.uniform(-1, 1, size=(n_neighbors, params_len)) * (ranges / 2.0) ) neighbors = np.array([wrap_params(z, ranges) for z in neighbors]) t_batch = neighbors[:, 0].tolist() params_batch = [z[1:].tolist() for z in neighbors] neighbor_costs = conditional_entropy_cost_batch( weights, qprogs_list, t_batch, params_batch ) best_idx = int(np.argmin(neighbor_costs)) candidate = neighbors[best_idx] candidate_cost = float(neighbor_costs[best_idx]) if candidate_cost < current_cost: accept = True else: prob = np.exp((current_cost - candidate_cost) / max(T, 1e-15)) accept = rng.random() < prob if accept: current = candidate current_cost = candidate_cost if current_cost < best_cost: best = current.copy() best_cost = current_cost T *= cooling return best[0], best[1:].tolist(), best_cost ``` Finally we define the main `run_optimized_qle` function, that runs over the optimized QLE steps. Compared to the standard QLE (`run_qle`), we replace the PGH with the simulated annealing optimizer: at each iteration the optimal $(t, \vec{\lambda})$ minimizing $H(F|Y)$ is found, the true system is sampled, and the Bayes update (Eq. (4)) is applied. ```python theme={null} def run_optimized_qle( h_list, true_index, threshold: float = 0.99, max_iters: int = 100, rng=None, optimizer_kwargs=None, qprogs=None, ): rng = np.random.default_rng() if rng is None else rng optimizer_kwargs = {} if optimizer_kwargs is None else dict(optimizer_kwargs) weights = np.ones(len(h_list), dtype=float) / len(h_list) history = [] qprogs_list = ( get_qprogs_for_hamiltonian_evolutions(h_list) if qprogs is None else qprogs ) for it in range(max_iters): # if np.max(weights) >= threshold: if weights[true_index] > threshold: break t, params, cost = optimize_entropy_sa( weights=weights, h_list=h_list, qprogs_list=qprogs_list, **optimizer_kwargs, ) outcome, likelihoods, new_weights = bayes_update( qprogs_list, true_index, t, params, weights ) history.append( { "t": t, "params": params, "outcome": outcome, "likelihoods": likelihoods, "weights_before": np.array(weights, dtype=float), "weights_after": new_weights, } ) weights = new_weights print(f"Iteration {it+1}: weights = {new_weights}") return { "final_weights": weights, "num_iters": len(history), "history": history, "guess_idx": int(np.argmax(weights)), } ``` # ## Run for the Specific Example ```python theme={null} _logger.setLevel(logging.WARNING) # disable the logging for the loop res_oqle = run_optimized_qle( h_list=HAMILTONIAN_SET, true_index=0, threshold=0.99, max_iters=100, rng=np.random.default_rng(1234), optimizer_kwargs={ "n_steps": 8, "n_neighbors": 20, "cooling": 0.9, "seed": 1234, }, qprogs=qprogs, ) _logger.setLevel(logging.INFO) # return to default after the loop ``` **Output:** ``` SA step 1/8 SA step 2/8 SA step 3/8 SA step 4/8 SA step 5/8 SA step 6/8 SA step 7/8 SA step 8/8 Sample true evolution Calculate likelihood Updating weights Iteration 1: weights = [0.37433638 0.62130421 0.00233752 0.0020219 ] SA step 1/8 SA step 2/8 SA step 3/8 SA step 4/8 SA step 5/8 SA step 6/8 SA step 7/8 SA step 8/8 Sample true evolution Calculate likelihood Updating weights Iteration 2: weights = [9.67488265e-01 3.22181561e-02 1.58664735e-04 1.34913783e-04] SA step 1/8 SA step 2/8 SA step 3/8 SA step 4/8 SA step 5/8 SA step 6/8 SA step 7/8 SA step 8/8 Sample true evolution Calculate likelihood Updating weights Iteration 3: weights = [9.99611193e-01 3.80579579e-04 1.53544567e-06 6.69148865e-06] ``` ```python theme={null} assert ( res_oqle["guess_idx"] == TRUE_IDX ), f"Optimized QLE converged to hypothesis {res_oqle['guess_idx']} but expected {TRUE_IDX}" assert ( res_oqle["final_weights"][TRUE_IDX] >= 0.99 ), f"Optimized QLE weight for true hypothesis is {res_oqle['final_weights'][TRUE_IDX]:.4f}, expected >= 0.99" print( f"Optimized QLE passed: converged to correct hypothesis {TRUE_IDX} in {res_oqle['num_iters']} iterations" ) ``` **Output:** ``` Optimized QLE passed: converged to correct hypothesis 0 in 3 iterations ``` # ## Plotting the Convergence History ```python theme={null} weights_history = np.array([h["weights_after"] for h in res_oqle["history"]]).T ``` ```python theme={null} for i in range(4): plt.plot(np.arange(1, len(weights_history[i]) + 1), weights_history[i], "o-") plt.title("Optimized Quantum Maximum Likelihood") plt.xlabel("Iteration", fontsize=16) plt.ylabel(r"$w_i$", fontsize=16) plt.tick_params("both", labelsize=16) plt.legend([rf"$H_{l}$" for l in range(len(HAMILTONIAN_SET))]) ``` **Output:** ``` ``` output ## Summary In this notebook we implemented two versions of the Quantum Likelihood Estimation algorithm: the standard version, and a version that utilizes information-based optimization. We demonstrated that information-based optimization yields faster convergence, compared to the standard version. ## References \[1]: Wiebe N, Granade C, Ferrie C and Cory D G 2014 Hamiltonian learning using imperfect quantum resources *Phys. Rev. A* **89** 042314 \[2]: Wiebe N, Granade C, Ferrie C and Cory D G 2014 Hamiltonian learning and certification using quantum resources *Phys. Rev. Lett.* **112** 190501 \[3]: [Levi, Alon and Ossi, Ziv and Cohen, Eliahu and Te'eni, Amit. Optimal quantum likelihood estimation. Quantum Science and Technology 11 015029 (2025)](https://doi.org/10.1088/2058-9565/ae2b31) \[4]: Wang J *et al* 2017 Experimental quantum Hamiltonian learning *Nat. Phys.* **13** 551-555 # Chebyshev Approximation of the Inverse Function Source: https://docs.classiq.io/explore/applications/CFD/QLS_for_hybrid_solvers/chebyshev_approximation Open this notebook in GitHub to run it yourself In this notebook we present three ways of approximating the inverse function with Chebyshev polynomials in some spectral interval $$ S= \left[-1,\, -\frac{1}{\kappa}\right] \cup \left[\frac{1}{\kappa},\, 1\right], $$ **given the polynomial degree**: 1. **Optimized relative error** (`optimized_rel`): the polynomial $p(y)$ minimizing $$ \max_{y\in S}|y\,p(y)-1|. $$ 2. **Optimized uniform error** (`optimized_uni`): the polynomial $p(y)$ minimizing $$ \max_{y\in S}|p(y)-1/y|. $$ 3. **CKS trimmed** (`cks_trimmed`): polynomial approximation of the inverse function from the original CKS paper [\[1\]](#references), trimmed to the target degree. The polynomial transformation is given by $$ \Large \frac{1}{y} \approx P(y) = \sum^{(d-1)/2}_{j=0} (-1)^j a_j T_{2j+1}(y) $$ *The first two cases are available in Classiq QSP application (see the `poly_inversion` function)*. In addition, we consider an approximated transformation, perturbing the polynomial coefficients of those theoretical expansions. This is relevant for reducing gate count in [Approximated Chebyshev-LCU quantum linear solvers](https://github.com/Classiq/classiq-library/blob/main/applications/CFD/QLS_for_hybrid_solvers/qls_chebyshev_lcu.ipynb). ```python theme={null} import matplotlib.pyplot as plt import numpy as np from banded_be import * from cheb_utils import fit_linear_coeffs_for_cheb, get_numpy_cheb_trimmed from numpy.polynomial.chebyshev import Chebyshev from scipy import sparse from scipy.special import eval_chebyt from classiq.applications.chemistry.op_utils import qubit_op_to_qmod from classiq.applications.qsp.qsp import poly_inversion ``` ```python theme={null} import pathlib path = ( pathlib.Path(__file__).parent.resolve() if "__file__" in locals() else pathlib.Path(".") ) ``` ```python theme={null} def eval_odd_cheb_poly(coef, x): # sum_k coef[k] * T_{2k+1}(x) return sum(coef[k] * eval_chebyt(2 * k + 1, x) for k in range(len(coef))) def compute_approx_errors(coeffs, norm, scale, w_min, w_max, n_points=2000): """Return (relative_error, uniform_error) of the polynomial over [w_min, w_max]. Relative error: max |y * p(y) / (scale * w_min) - 1| Uniform error: max |p(y) - scale * w_min / y| """ x = np.linspace(w_min, w_max, n_points) p = (scale * w_min / norm) * eval_odd_cheb_poly(coeffs[1::2], x) ref = scale * w_min / x rel_err = float(np.max(np.abs(x * p / (scale * w_min) - 1))) uni_err = float(np.max(np.abs(p - ref))) return rel_err, uni_err ALLOWED_CHEB_APPROX = ["optimized_rel", "optimized_uni", "cks_trimmed"] def get_cheb_coeff(w_min, degree, scale=1, method="optimized_rel", epsilon=1e-4): assert ( method in ALLOWED_CHEB_APPROX ), f"method must be one of {ALLOWED_CHEB_APPROX}, got {method!r}" kappa = 1 / w_min B = int(kappa**2 * np.log(kappa / epsilon)) j0 = int(np.sqrt(B * np.log(4 * B / epsilon))) theoretical_degree = 2 * j0 + 1 print( f"kappa={kappa:.2f}, theoretical degree for epsilon={epsilon}: {theoretical_degree}" ) if method == "optimized_rel": print(f" -> optimized relative-error polynomial, degree {degree}") c, m = poly_inversion(degree, kappa, "relative") return scale * c / m, scale / m if method == "optimized_uni": print(f" -> optimized uniform-error polynomial, degree {degree}") c, m = poly_inversion(degree, kappa, "uniform") return scale * c / m, scale / m if method == "cks_trimmed": print( f" -> CKS degree-{theoretical_degree} polynomial trimmed to degree {degree}" ) return ( get_numpy_cheb_trimmed(w_min, B, scale, degree, theoretical_degree), scale * w_min, ) ``` ## Chebyshev Polynomials Expansions for Different Types of Error Bound Definitions We upload some matrix, and consider its block-encoding. We need the block-encoding scaling factor in order to calculate the effective spectral range of singular-values. ```python theme={null} mat_name = "nozzle_008_mat" matfile = "matrices/" + mat_name + ".npz" mat_raw_scr = sparse.load_npz(path / matfile) data_size, block_size, be_scaling_factor, be_qfunc = get_banded_diags_be(mat_raw_scr) ``` ```python theme={null} mat_raw = mat_raw_scr.toarray() ``` ```python theme={null} svd = np.linalg.svd(mat_raw / be_scaling_factor)[1] w_min = min(svd) w_max = max(svd) scale = 0.5 print(f"min singular value: {w_min:.4f}, max singular value: {w_max:.4f}") params = [ {"degree": 127, "method": "optimized_rel"}, {"degree": 255, "method": "optimized_rel"}, {"degree": 255, "method": "optimized_uni"}, {"degree": 255, "method": "cks_trimmed"}, ] ``` **Output:** ``` min singular value: 0.0133, max singular value: 0.5218 ``` ```python theme={null} kappa = 1 / w_min x_vals = np.linspace(0, w_max, 500) mask = (np.abs(x_vals) >= w_min) & (np.abs(x_vals) <= w_max) fig, axes = plt.subplots(2, 2, figsize=(14, 10)) axes_flat = axes.flatten() colors = plt.rcParams["axes.prop_cycle"].by_key()["color"] for i, p in enumerate(params): ax = axes_flat[i] coeffs, norm = get_cheb_coeff(w_min, p["degree"], scale=scale, method=p["method"]) odd_coeffs = coeffs[1::2] approx = (scale * w_min / norm) * eval_odd_cheb_poly(odd_coeffs, x_vals) rel_err, uni_err = compute_approx_errors(coeffs, norm, scale, w_min, w_max) ax.plot( x_vals[mask], scale * w_min / x_vals[mask], "-k", label=r"$0.5\,/\,(\kappa\, y)$", linewidth=3, ) ax.plot( x_vals, approx, "--", color=colors[i % len(colors)], label=f'{p["method"]} (deg {p["degree"]})', linewidth=2.5, ) ax.axvline(x=w_min, color="gray", linestyle="--", linewidth=1.5) auto_ticks = [t for t in ax.get_xticks() if 0 < t < w_max and abs(t - w_min) > 1e-6] all_ticks = sorted(auto_ticks + [w_min]) ax.set_xticks(all_ticks) ax.set_xticklabels( [r"$1/\kappa$" if abs(t - w_min) < 1e-6 else f"{t:.2g}" for t in all_ticks] ) ax.set_xlabel("y") ax.set_ylabel("p(y)") ax.set_title(f'{p["method"]} (deg {p["degree"]})') ax.legend() ax.grid(True) ax.text( 0.98, 0.15, f"relative error: {rel_err:.2e}\nuniform error: {uni_err:.2e}", transform=ax.transAxes, ha="right", va="bottom", fontsize=10, bbox=dict(boxstyle="round,pad=0.3", fc="white", alpha=0.8), ) for j in range(len(params), 4): axes_flat[j].set_visible(False) plt.tight_layout() plt.show() ``` **Output:** ``` kappa=74.93, theoretical degree for epsilon=0.0001: 2575 -> optimized relative-error polynomial, degree 127 kappa=74.93, theoretical degree for epsilon=0.0001: 2575 -> optimized relative-error polynomial, degree 255 kappa=74.93, theoretical degree for epsilon=0.0001: 2575 -> optimized uniform-error polynomial, degree 255 kappa=74.93, theoretical degree for epsilon=0.0001: 2575 -> CKS degree-2575 polynomial trimmed to degree 255 ``` output ## Chebyshev Polynomials Expansions with Non-Exact Coefficients We obtain the approximated coefficients loaded by approximate state preparation. This block enters as the PREPARE part in the [Chebyshev-LCU approach](https://github.com/Classiq/classiq-library/blob/main/applications/CFD/QLS_for_hybrid_solvers/qls_chebyshev_lcu.ipynb). ```python theme={null} degree = 255 sp_error = 0.03 coeffs, norm = get_cheb_coeff(w_min, degree, scale=scale, method="optimized_rel") fitted_cheb_coeffs = fit_linear_coeffs_for_cheb(coeffs) odd_coef = fitted_cheb_coeffs positive_ind = np.arange(len(odd_coef))[0::2] positive_fitted = odd_coef[0::2] negative_ind = np.arange(len(odd_coef))[1::2] negative_fitted = odd_coef[1::2] # Calculate prep for Chebyshev LCU lcu_size_inv = len(odd_coef).bit_length() - 1 odd_coeffs_signs = np.sign(odd_coef) assert np.all( odd_coeffs_signs == np.where(np.arange(len(odd_coeffs_signs)) % 2 == 0, 1, -1) ), "Non alternating signs for odd coefficients" normalization_inv = sum(np.abs(odd_coef)) prepare_probs_inv = (np.abs(odd_coef) / normalization_inv).tolist() simulated_amps = [] number_of_ry = [] qprogs = [] for index, err in enumerate([0.0, sp_error]): @qfunc def main(inv_block: Output[QNum[lcu_size_inv]]): allocate(inv_block) inplace_prepare_state(prepare_probs_inv, err, inv_block) qprog = synthesize( main, preferences=Preferences( custom_hardware_settings=CustomHardwareSettings( basis_gates=["cx", "ry", "h", "x", "y", "z", "s", "t"] ) ), ) qprogs.append(qprog) print(f"for {err}: {qprog.transpiled_circuit.count_ops}") number_of_ry.append(qprog.transpiled_circuit.count_ops["ry"]) df = calculate_state_vector(qprog) abs_simulated_amps = np.zeros(2**lcu_size_inv) abs_simulated_amps[df["inv_block"]] = df["probability"] simulated_amps.append( [ (-1) ** k * abs_simulated_amps[k] * normalization_inv for k in range(2**lcu_size_inv) ] ) ``` **Output:** ``` kappa=74.93, theoretical degree for epsilon=0.0001: 2575 -> optimized relative-error polynomial, degree 255 linear fit parameters: slope = -0.00010397734302726349, b= 0.014094618961514994 ``` **Output:** ``` Submitting job to simulator ``` **Output:** ``` for 0.0: {'s': 516, 'h': 264, 'z': 258, 'ry': 127, 'cx': 126} ``` **Output:** ``` Job: https://platform.classiq.io/jobs/75e75b1b-1bb7-4208-9122-3668882d2d35 Submitting job to simulator ``` **Output:** ``` for 0.03: {'s': 28, 'h': 16, 'z': 14, 'ry': 11, 'cx': 6} ``` **Output:** ``` Job: https://platform.classiq.io/jobs/0080ea0f-de41-4ab5-9048-de541aa7ba40 ``` ```python theme={null} assert number_of_ry[1] < number_of_ry[0] / 2 ``` Next, we plot the approximated coefficients and the resulting polynomial. We can see that for rough approximation, which reduces quantum resources for loading the coefficients on a quantum variable, still gives a good fit. ```python theme={null} fig, (ax_a, ax_b) = plt.subplots(1, 2, figsize=(18, 6)) # Plot coefficients ax_a.plot(coeffs[1::2], ".", markersize=6, label="original") ax_a.plot(simulated_amps[0], ".", markersize=6, label="fitted (error 0)") ax_a.plot( simulated_amps[1], ".", markersize=6, label=f"approx. fitted (error {sp_error})" ) ax_a.set_xlabel("k") ax_a.set_ylabel(r"$(-1)^k\,a_k$") ax_a.set_title(f"Chebyshev coefficients (degree {degree})") ax_a.legend() ax_a.grid(True) # Plot the resulting polynomial fac = scale * w_min / norm poly_orig = fac * eval_odd_cheb_poly(coeffs[1::2], x_vals) poly_fit = fac * eval_odd_cheb_poly(simulated_amps[0], x_vals) poly_approx = fac * eval_odd_cheb_poly(simulated_amps[1], x_vals) ax_b.plot( x_vals[mask], scale * w_min / x_vals[mask], "-k", label=r"$0.5\,/\,(\kappa\,y)$", linewidth=3, ) ax_b.plot(x_vals, poly_orig, "--", linewidth=2.5, label="original") ax_b.plot(x_vals, poly_fit, "--", linewidth=2.5, label="fitted (error 0)") ax_b.plot( x_vals, poly_approx, "--", linewidth=2.5, label=f"approx. fitted (error {sp_error})" ) ax_b.axvline(x=w_min, color="gray", linestyle="--", linewidth=1.5) auto_ticks = [t for t in ax_b.get_xticks() if 0 < t < w_max and abs(t - w_min) > 1e-6] all_ticks = sorted(auto_ticks + [w_min]) ax_b.set_xticks(all_ticks) ax_b.set_xticklabels( [r"$1/\kappa$" if abs(t - w_min) < 1e-6 else f"{t:.2g}" for t in all_ticks] ) ax_b.set_xlabel("y") ax_b.set_ylabel("p(y)") ax_b.set_title("Resulting polynomials") ax_b.legend() ax_b.grid(True) plt.tight_layout() plt.show() ``` output ## References \[1] Andrew M. Childs, Robin Kothari, and Rolando D. Somma. *Quantum algorithm for systems of linear equations with exponentially improved dependence on precision.* SIAM Journal on Computing, 46:1920, 2017. # Quantum Linear Solver with LCU of Chebyshev Polynomials Source: https://docs.classiq.io/explore/applications/CFD/QLS_for_hybrid_solvers/qls_chebyshev_lcu Open this notebook in GitHub to run it yourself The code here can be integrated as part of a larger CFD solver, e.g., as in [qc-cfd repository](https://github.com/rolls-royce/qc-cfd/tree/main/1D-Nozzle). In particular, instead of calling a classical solver, e.g., `x = sparse.linalg.spsolve(mat_raw_scr, b_raw)`, one can call the quantum solver `cheb_lcu_approx_solver(mat_raw_scr, b_raw,...)`. We implemented two versions for block-encoding, one based on Pauli decomposition of the matrix, and another one based on decomposing the matrix to a finite set of diagonals. ```python theme={null} !pip install -qq "classiq[qsp]" !pip install -qq "classiq[chemistry]" ``` We start with defining the functions. First we define the quantum function `lcu_cheb_approx` that implements an approximated Chebyshev LCU quantum linear solver. ```python theme={null} import time import matplotlib.pyplot as plt import numpy as np from banded_be import get_banded_diags_be from cheb_utils import * from classical_functions_be import get_svd_range from pauli_be import get_pauli_be from scipy import sparse from classiq import * from classiq.applications.qsp.qsp import poly_inversion np.random.seed(53) PAULI_TRIM_REL_TOL = 0.1 ``` ```python theme={null} from classiq.qmod.symbolic import pi @qfunc def my_reflect_about_zero(qba: QNum): reflect_about_zero(qba) phase(pi) @qfunc def walk_operator( block_enc: QCallable[QArray, QArray], block: QArray, data: QArray ) -> None: block_enc(block, data) my_reflect_about_zero(block) @qfunc def symmetrize_walk_operator( block_enc: QCallable[QNum, QArray], block: QNum, data: QArray ): my_reflect_about_zero(block) within_apply( lambda: block_enc(block, data), lambda: my_reflect_about_zero(block), ) @qfunc def lcu_cheb_approx( powers: CArray[CInt], inv_coeffs: CArray[CReal], sp_error: CReal, block_enc: QCallable[QNum, QArray], mat_block: QNum, data: QArray, cheb_block: QArray, ) -> None: within_apply( lambda: inplace_prepare_state(inv_coeffs, sp_error, cheb_block), lambda: ( Z(cheb_block[0]), repeat( powers.len, lambda i: control( cheb_block[i], lambda: power( powers[i], lambda: symmetrize_walk_operator(block_enc, mat_block, data), ), ), ), my_reflect_about_zero(mat_block), walk_operator(block_enc, mat_block, data), ), ) ``` Next, we define the `cheb_lcu_approx_solver` function, which gets the matrix and right-hand-side vector, applies the quantum solver, and returns the linear equation solution using a statevector simulator. The solvers in this directory were developed in the framework of exploring their performance in hybrid CFD schemes. For simplicity, it is assumed that all the properties of the matrices are known explicitly. In particular, we calculate its singular values for identifying the range in which we apply the inversion polynomial. ```python theme={null} def cheb_lcu_approx_solver( mat_raw_scr, b_raw, log_poly_degree, be_method="banded", approximation=0, preferences=Preferences(), constraints=Constraints(), ): scale = 0.5 b_norm = np.linalg.norm(b_raw) b_normalized = b_raw / b_norm data_size = max(1, (len(b_raw) - 1).bit_length()) if be_method == "pauli": data_size, block_size, be_scaling_factor, be_qfunc = get_pauli_be(mat_raw_scr) if be_method == "banded": data_size, block_size, be_scaling_factor, be_qfunc = get_banded_diags_be( mat_raw_scr ) w_min, w_max = get_svd_range(mat_raw_scr / be_scaling_factor) poly_degree = 2 * (2**log_poly_degree - 1) + 1 c, m = poly_inversion(poly_degree, 1 / w_min, "relative") pcoefs, poly_scale = scale * c / m, scale / m odd_coef = pcoefs[1::2] if approximation > 0: odd_coef = fit_linear_coeffs_for_cheb(pcoefs) lcu_size_inv = len(odd_coef).bit_length() - 1 print(f"Chebyshev LCU size: {lcu_size_inv} qubits.") odd_coeffs_signs = np.sign(odd_coef) assert np.all( odd_coeffs_signs == np.where(np.arange(len(odd_coeffs_signs)) % 2 == 0, 1, -1) ), "Non alternating signs for odd coefficients" normalization_inv = sum(np.abs(odd_coef)) print(f"Normalization factor for inversion: {normalization_inv}") prepare_probs_inv = (np.abs(odd_coef) / normalization_inv).tolist() @qfunc def main( matrix_block: Output[QNum[block_size]], data: Output[QNum[data_size]], inv_block: Output[QNum[lcu_size_inv]], ): allocate(inv_block) allocate(matrix_block) prepare_amplitudes(b_normalized.tolist(), 0, data) lcu_cheb_approx( powers=[2**i for i in range(lcu_size_inv)], inv_coeffs=prepare_probs_inv, sp_error=approximation, block_enc=lambda b, d: invert(lambda: be_qfunc(b, d)), mat_block=matrix_block, data=data, cheb_block=inv_block, ) start_time_syn = time.time() qprog = synthesize(main, preferences=preferences, constraints=constraints) print("time to syn:", time.time() - start_time_syn) start_time_exe = time.time() sv = calculate_state_vector(qprog, filters={"matrix_block": 0, "inv_block": 0}) proj_statevector = np.zeros(2**data_size, dtype=complex) proj_statevector[sv["data"].to_numpy()] = sv["amplitude"].to_numpy() indices = np.where(np.abs(proj_statevector) > 1e-13)[0] if len(indices) > 0: global_phase = np.angle(proj_statevector[indices[0]]) resulting_state = np.real(proj_statevector / np.exp(1j * global_phase)) else: resulting_state = np.zeros(2**data_size) print("time to exe:", time.time() - start_time_exe) normalization_factor = (be_scaling_factor * poly_scale) / b_norm / normalization_inv return resulting_state / normalization_factor, qprog ``` ```python theme={null} import pathlib path = ( pathlib.Path(__file__).parent.resolve() if "__file__" in locals() else pathlib.Path(".") ) ``` We examine two usecases, starting with a small one, and applying a Pauli-LCU block encoding. ```python theme={null} mat_small_scr = sparse.load_npz(path / "matrices/nozzle_small_scr.npz") b_small = np.load(path / "matrices/b_nozzle_small.npy") print(f"nozzle_small: {mat_small_scr.shape[0]}x{mat_small_scr.shape[1]}") ``` **Output:** ``` nozzle_small: 8x8 ``` ```python theme={null} prefs = Preferences() ``` ```python theme={null} qsol_small_pauli, qprog_small_pauli = cheb_lcu_approx_solver( mat_small_scr, b_small, log_poly_degree=4, be_method="pauli", preferences=prefs, constraints=Constraints(optimization_parameter="width"), ) show(qprog_small_pauli) ``` **Output:** ``` number of Paulis before/after trimming 24/20 Chebyshev LCU size: 4 qubits. Normalization factor for inversion: 0.7082716135217179 ``` **Output:** ``` Submitting job to simulator ``` **Output:** ``` time to syn: 73.70448398590088 ``` **Output:** ``` Job: https://platform.classiq.io/jobs/b5381f72-ce8a-470c-b42e-220fb2bfcb12 ``` **Output:** ``` time to exe: 17.767388105392456 Quantum program link: https://platform.classiq.io/circuit/3Dt1koIunNqabP53yYwcvdon8Cn ``` We plot the solution vector, and compare to the expected classical result: ```python theme={null} expected_small = np.linalg.solve(mat_small_scr.toarray(), b_small) ext_idx = np.argmax(np.abs(expected_small)) correct_sign = np.sign(expected_small[ext_idx]) / np.sign(qsol_small_pauli[ext_idx]) qsol_small_pauli *= correct_sign plt.plot(expected_small, "o", label="classical") plt.plot(qsol_small_pauli, ".", label=f"Cheb-LCU; Pauli BE; degree {2*2**4-1}") plt.title("nozzle_small") plt.xlabel("index") plt.ylabel("solution") plt.legend() plt.grid(True) plt.show() ``` output ```python theme={null} assert np.linalg.norm(qsol_small_pauli - expected_small) < 0.2 ``` Next, we move to a larger problem. In a hybrid algorithm, we can relax some of the synthesis preferences to obtain the result a faster (for example, we can set `debug_mode=False` as we can skip the visualization of the quantum program). For the larger usecase we work with the Banded Diagonals block-encoding. We compare the approximated version of the solver to the exact one. ```python theme={null} mat_008_scr = sparse.load_npz(path / "matrices/nozzle_008_mat.npz") b_008 = np.load(path / "matrices/nozzle_008_b.npy") print(f"nozzle_008: {mat_008_scr.shape[0]}x{mat_008_scr.shape[1]}") ``` **Output:** ``` nozzle_008: 16x16 ``` ```python theme={null} prefs = Preferences( transpilation_option="none", optimization_level=0, debug_mode=False, qasm3=True, ) ``` ```python theme={null} qsol_008_banded, qprog_008_banded = cheb_lcu_approx_solver( mat_008_scr, b_008, log_poly_degree=7, be_method="banded", preferences=prefs, constraints=Constraints(optimization_parameter="width"), ) ``` **Output:** ``` Chebyshev LCU size: 7 qubits. Normalization factor for inversion: 0.8180186976736218 ``` **Output:** ``` Submitting job to simulator ``` **Output:** ``` time to syn: 48.63558340072632 ``` **Output:** ``` Job: https://platform.classiq.io/jobs/3a7ef91e-b24f-4f49-bebb-b7e9fecf2d8e ``` **Output:** ``` time to exe: 70.26255297660828 ``` ```python theme={null} SP_APPROX_BOUND = 0.03 qsol_008_banded_approx, qprog_008_banded_approx = cheb_lcu_approx_solver( mat_008_scr, b_008, log_poly_degree=7, be_method="banded", approximation=SP_APPROX_BOUND, preferences=prefs, constraints=Constraints(optimization_parameter="width"), ) ``` **Output:** ``` linear fit parameters: slope = -0.00010397734302726349, b= 0.014094618961514994 Chebyshev LCU size: 7 qubits. Normalization factor for inversion: 0.9589833829483215 ``` **Output:** ``` Submitting job to simulator ``` **Output:** ``` time to syn: 48.173218727111816 ``` **Output:** ``` Job: https://platform.classiq.io/jobs/9ab42c87-977a-4d4f-a5c8-e5841d5c9eaa ``` **Output:** ``` time to exe: 71.8209617137909 ``` ```python theme={null} expected_008 = np.linalg.solve(mat_008_scr.toarray(), b_008) ext_idx = np.argmax(np.abs(expected_008)) correct_sign = np.sign(expected_008[ext_idx]) / np.sign(qsol_008_banded[ext_idx]) qsol_008_banded *= correct_sign plt.plot(expected_008, "o", label="classical") plt.plot(qsol_008_banded, ".", label=f"Cheb-LCU; degree {2*2**7-1}") correct_sign = np.sign(expected_008[ext_idx]) / np.sign(qsol_008_banded_approx[ext_idx]) qsol_008_banded_approx *= correct_sign plt.plot( qsol_008_banded_approx, ".", label=f"Cheb-LCU (approx {SP_APPROX_BOUND}); degree {2*2**7-1}", ) plt.title("nozzle_008 (Banded BE)") plt.xlabel("index") plt.ylabel("solution") plt.legend() plt.grid(True) plt.show() ``` output ```python theme={null} assert np.linalg.norm(qsol_008_banded - expected_008) < 0.1 assert np.linalg.norm(qsol_008_banded_approx - expected_008) < 0.1 ``` # Quantum Linear Solver Based on QSVT Source: https://docs.classiq.io/explore/applications/CFD/QLS_for_hybrid_solvers/qls_qsvt Open this notebook in GitHub to run it yourself The code here can be integrated as part of a larger CFD solver, e.g., as in [qc-cfd repository](https://github.com/rolls-royce/qc-cfd/tree/main/1D-Nozzle). In particular, instead of calling a classical solver, e.g., `x = sparse.linalg.spsolve(mat_raw_scr, b_raw)`, one can call the quantum solver `qsvt_solver(mat_raw_scr, b_raw,...)`. We implemented two versions for block-encoding, one based on Pauli decomposition of the matrix, and another one based on decomposing the matrix to a finite set of diagonals. ```python theme={null} !pip install -qq -U "classiq[qsp]" !pip install -qq "classiq[chemistry]" ``` We start with defining the main function `qsvt_solver`, which gets the matrix and right-hand-side vector, applies the quantum solver, and returns the linear equation solution using a statevector simulator. The solvers in this directory were developed in the framework of exploring their performance in hybrid CFD schemes. For simplicity, it is assumed that all the properties of the matrices are known explicitly. In particular, we calculate its singular values for identifying the range in which we apply the inversion polynomial. ```python theme={null} import time import matplotlib.pyplot as plt import numpy as np from banded_be import get_banded_diags_be from cheb_utils import * from classical_functions_be import get_svd_range from pauli_be import get_pauli_be from scipy import sparse from classiq import * from classiq.applications.qsp import qsvt_phases from classiq.applications.qsp.qsp import poly_inversion np.random.seed(53) PAULI_TRIM_REL_TOL = 0.1 ``` ```python theme={null} def qsvt_solver( mat_raw_scr, b_raw, poly_degree, be_method="banded", preferences=Preferences(), constraints=Constraints(), ): scale = 0.5 b_norm = np.linalg.norm(b_raw) # b normalization b_normalized = b_raw / b_norm # Define block encoding if be_method == "pauli": data_size, block_size, be_scaling_factor, be_qfunc = get_pauli_be( mat_raw_scr, PAULI_TRIM_REL_TOL ) print( f"Pauli block encoding with block size {block_size} and scaling factor {be_scaling_factor}" ) elif be_method == "banded": data_size, block_size, be_scaling_factor, be_qfunc = get_banded_diags_be( mat_raw_scr ) print( f"Banded diagonal block encoding with block size {block_size} and scaling factor {be_scaling_factor}" ) class BlockEncodedState(QStruct): data: QNum[data_size] block: QNum[block_size] # Get SVD range w_min, w_max = get_svd_range(mat_raw_scr / be_scaling_factor) # Get Chebyshev polynomial and the corresponding QSVT angles c, m = poly_inversion(poly_degree, 1 / w_min, "relative") pcoefs, poly_scale = scale * c / m, scale / m inv_phases = qsvt_phases(pcoefs) # Define QSVT projector @qfunc def projector(be: BlockEncodedState, res: QBit): res ^= be.block == 0 @qfunc def main( qsvt_aux: Output[QBit], data: Output[QNum[data_size]], block: Output[QNum[block_size]], ): allocate(qsvt_aux) allocate(block) prepare_amplitudes(b_normalized.tolist(), 0, data) be_state = BlockEncodedState() within_apply( lambda: bind([data, block], be_state), lambda: qsvt_inversion( inv_phases, lambda aux: projector(be_state, aux), lambda: be_qfunc(be_state.block, be_state.data), qsvt_aux, ), ) start_time_syn = time.time() qprog = synthesize(main, preferences=preferences, constraints=constraints) print("time to syn:", time.time() - start_time_syn) start_time_exe = time.time() sv = calculate_state_vector(qprog, filters={"block": 0, "qsvt_aux": 0}) proj_statevector = np.zeros(2**data_size, dtype=complex) proj_statevector[sv["data"].to_numpy()] = sv["amplitude"].to_numpy() indices = np.where(np.abs(proj_statevector) > 1e-13)[0] if len(indices) > 0: global_phase = np.angle(proj_statevector[indices[0]]) resulting_state = np.real(proj_statevector / np.exp(1j * global_phase)) else: resulting_state = np.zeros(2**data_size) print("time to exe:", time.time() - start_time_exe) normalization_factor = (be_scaling_factor * poly_scale) / b_norm return resulting_state / normalization_factor, qprog ``` We examine two usecases, starting with a small one, and applying a Pauli-LCU block encoding. ```python theme={null} prefs = Preferences() ``` ```python theme={null} import pathlib path = ( pathlib.Path(__file__).parent.resolve() if "__file__" in locals() else pathlib.Path(".") ) ``` ```python theme={null} mat_small_scr = sparse.load_npz(path / "matrices/nozzle_small_scr.npz") b_small = np.load(path / "matrices/b_nozzle_small.npy") print(f"nozzle_small: {mat_small_scr.shape[0]}x{mat_small_scr.shape[1]}") ``` **Output:** ``` nozzle_small: 8x8 ``` ```python theme={null} qsol_small_pauli, qprog_small_pauli = qsvt_solver( mat_small_scr, b_small, poly_degree=101, be_method="pauli", preferences=prefs, constraints=Constraints(optimization_parameter="width"), ) show(qprog_small_pauli) ``` **Output:** ``` number of Paulis before/after trimming 24/20 Pauli block encoding with block size 5 and scaling factor 5.557119918538639 ``` **Output:** ``` Submitting job to simulator ``` **Output:** ``` time to syn: 194.59005403518677 ``` **Output:** ``` Job: https://platform.classiq.io/jobs/2770fe10-ee32-4dbf-8eb5-3d5929996e44 ``` **Output:** ``` time to exe: 21.43983793258667 Quantum program link: https://platform.classiq.io/circuit/3DqBaa1JyKpdP4PL71mpJcJufY9 ``` We plot the solution vector, and compare to the expected classical result: ```python theme={null} expected_small = np.linalg.solve(mat_small_scr.toarray(), b_small) ext_idx = np.argmax(np.abs(expected_small)) correct_sign = np.sign(expected_small[ext_idx]) / np.sign(qsol_small_pauli[ext_idx]) qsol_small_pauli *= correct_sign plt.plot(expected_small, "o", label="classical") plt.plot(qsol_small_pauli, ".", label="QSVT-inv; Pauli BE; degree 101") plt.title("nozzle_small") plt.xlabel("index") plt.ylabel("solution") plt.legend() plt.grid(True) plt.show() ``` output ```python theme={null} assert np.linalg.norm(qsol_small_pauli - expected_small) < 0.1 ``` Next, we move to a larger problem. In a hybrid algorithm, we can relax some of the synthesis preferences to obtain the result a faster (for example, we can set `debug_mode=False` as we can skip the visualization of the quantum program). For the larger usecase we work with the Banded Diagonals block-encoding. We compare the approximated version of the solver to the exact one. ```python theme={null} mat_008_scr = sparse.load_npz(path / "matrices/nozzle_008_mat.npz") b_008 = np.load(path / "matrices/nozzle_008_b.npy") print(f"nozzle_008: {mat_008_scr.shape[0]}x{mat_008_scr.shape[1]}") ``` **Output:** ``` nozzle_008: 16x16 ``` ```python theme={null} prefs = Preferences( transpilation_option="none", optimization_level=0, debug_mode=False, qasm3=True, ) ``` ```python theme={null} qsol_008_banded, qprog_008_banded = qsvt_solver( mat_008_scr, b_008, poly_degree=2 * (2**7 - 1) + 1, # taking the same degree as in qls_chebyshev_lcu be_method="banded", preferences=prefs, constraints=Constraints(optimization_parameter="width"), ) ``` **Output:** ``` Banded diagonal block encoding with block size 4 and scaling factor 6.097617696340303 ``` **Output:** ``` Submitting job to simulator ``` **Output:** ``` time to syn: 169.09099125862122 ``` **Output:** ``` Job: https://platform.classiq.io/jobs/1011bac7-83f6-4790-a63e-cbc32c8b9fec ``` **Output:** ``` time to exe: 95.1099259853363 ``` ```python theme={null} expected_008 = np.linalg.solve(mat_008_scr.toarray(), b_008) ext_idx = np.argmax(np.abs(expected_008)) correct_sign = np.sign(expected_008[ext_idx]) / np.sign(qsol_008_banded[ext_idx]) qsol_008_banded *= correct_sign plt.plot(expected_008, "o", label="classical") plt.plot(qsol_008_banded, ".", label=f"QSVT-inv; Banded BE; degree {2*(2**7-1)+1}") plt.title("nozzle_008") plt.xlabel("index") plt.ylabel("solution") plt.legend() plt.grid(True) plt.show() ``` output ```python theme={null} assert np.linalg.norm(qsol_008_banded - expected_008) < 0.1 ``` # Block Encoding Verification Source: https://docs.classiq.io/explore/applications/CFD/QLS_for_hybrid_solvers/verify_block_encoding Open this notebook in GitHub to run it yourself This notebook verifies the various block encoding, whose code are given in this directory. The matrix input is assumed to be a sparse matrix. We verify the following quantum functions for block encoding: 1. Prepare and Select for Pauli decomposition of the matrix. The Select block is implemented with Gray code technique. 1. Banded diagonal block encoding, according to Ref. For both block encoding we construct a symmetric and non-symmetric versions. ```python theme={null} !pip install -qq -U "classiq[qsp]" !pip install -qq "classiq[chemistry]" ``` ```python theme={null} import matplotlib.pyplot as plt import numpy as np from banded_be import * from classical_functions_be import get_projected_state_vector from pauli_be import * from scipy import sparse from classiq import * np.random.seed(53) ``` We define a generic function for verifying block-encoding qfuncs: ```python theme={null} def get_be_state(rhs_vec, be_qfunc, block_size, data_size): """ Apply a block-encoding qfunc to an initial state and return the post-selected output. Parameters ---------- rhs_vec : list[real] Amplitudes of the initial data state |\psi>. Length must be 2**data_size. be_qfunc : qfunc A Qmod qfunc with signature be_qfunc(block: QNum, data: QNum) that applies the block-encoding for the matrix A/s. block_size : int Number of qubits in the block variable. data_size : int Number of qubits in the data variable. Returns ------- array The post-selected data variable state equal to (A/s)|\psi>, obtained by projecting the block register onto 0 after applying the block encoding. qprog Thr resulting quantum program """ @qfunc def main( data: Output[QNum[data_size]], block: Output[QNum[block_size]], ): allocate(block) prepare_amplitudes(rhs_vec, 0.0, data) be_qfunc(block, data) qprog = synthesize(main, preferences=Preferences(timeout_seconds=2000)) # Post-select block == 0 on the statevector simulator. results = calculate_state_vector(qprog, filters={"block": 0}) resulting_state = get_projected_state_vector(results, "data", data_size) return resulting_state, qprog ``` ```python theme={null} def verify_by_plot(mat, rhs, be_factor, qsol): # Plot quantum solution vs expected ine expected_sol = (mat @ rhs) / be_factor plt.plot(expected_sol, "o") ext_idx = np.argmax(np.abs(expected_sol)) correct_sign = np.sign(expected_sol[ext_idx]) / np.sign(qsol[ext_idx]) qsol *= correct_sign plt.plot(qsol, ".") return expected_sol ``` Test a specific matrix ```python theme={null} import pathlib path = ( pathlib.Path(__file__).parent.resolve() if "__file__" in locals() else pathlib.Path(".") ) ``` ```python theme={null} mat_name = "nozzle_small_scr" matfile = "matrices/" + mat_name + ".npz" mat_raw_scr = sparse.load_npz(path / matfile) ``` ## Block Encoding of Non-Hermitiam Matrices (for QSVT Solver) # ## Pauli Block-Encoding ```python theme={null} rval = mat_raw_scr.data col = mat_raw_scr.indices rowstt = mat_raw_scr.indptr nr = mat_raw_scr.shape[0] data_size = int(np.log2(nr)) # decompose to Paulis paulis_list, transform_matrix = initialize_paulis_from_csr( rowstt, col, data_size, to_symmetrize=False ) qubit_op = eval_pauli_op(paulis_list, transform_matrix, rval) qubit_op.compress(1e-12) hamiltonian = of_op_to_cl_op(qubit_op) # Calculate scaling factor and block size be_scaling_factor = sum([np.abs(term.coefficient) for term in hamiltonian.terms]) block_size = max(1, (len(hamiltonian.terms) - 1).bit_length()) rand_bvec = 1 - 2 * np.random.rand(2**data_size) rand_bvec = (rand_bvec / np.linalg.norm(rand_bvec)).tolist() hamiltonian = hamiltonian * (1 / be_scaling_factor) # Define block encoding function @qfunc def block_encode_pauli(block: QNum, data: QNum): lcu_paulis_graycode(hamiltonian.terms, data, block) qsol, qprog_pauli_be = get_be_state( rand_bvec, block_encode_pauli, block_size, data_size ) show(qprog_pauli_be) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/37T194tII0d7o0gv7lmtvsIhT81 ``` ```python theme={null} expected_sol = verify_by_plot(mat_raw_scr, rand_bvec, be_scaling_factor, qsol) assert np.linalg.norm(qsol - expected_sol) < 1e-10 ``` output # ## Banded Diagonals Block-Encoding ```python theme={null} # Get diagonal properties offsets, diags, diags_maxima, prepare_norm = get_be_banded_data(mat_raw_scr) data_size = int(np.ceil(np.log2(len(diags[0])))) s_size = int(np.ceil(np.log2(len(offsets)))) # Calculate scaling factor and block size block_size = s_size + 1 be_scaling_factor = prepare_norm # Define block encoding function @qfunc def block_encode_banded_matrix(block: QNum, data: QNum): block_encode_banded( offsets=offsets, diags=diags, prep_diag=diags_maxima, block=block, data=data ) rand_bvec = 1 - 2 * np.random.rand(2**data_size) rand_bvec = (rand_bvec / np.linalg.norm(rand_bvec)).tolist() qsol, qprog_banded_be = get_be_state( rand_bvec, block_encode_banded_matrix, block_size, data_size ) show(qprog_banded_be) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/37T1AUJg5LXloVGSyrzhGapt3AC ``` ```python theme={null} expected_sol = verify_by_plot(mat_raw_scr, rand_bvec, be_scaling_factor, qsol) assert np.linalg.norm(qsol - expected_sol) < 1e-10 ``` output ## Block Encoding of Hermitia Matrices (for LCU Chebyshev Solver) # ## Pauli Block-Encoding ```python theme={null} rval = mat_raw_scr.data col = mat_raw_scr.indices rowstt = mat_raw_scr.indptr nr = mat_raw_scr.shape[0] data_size = int(np.log2(nr)) # decompose to Paulis paulis_list, transform_matrix = initialize_paulis_from_csr( rowstt, col, data_size, to_symmetrize=True ) data_size += 1 # in the symmetric case the data size is increased by 1 qubit_op = eval_pauli_op(paulis_list, transform_matrix, rval) qubit_op.compress(1e-12) hamiltonian = of_op_to_cl_op(qubit_op) # Calculate scaling factor and block size be_scaling_factor = sum([np.abs(term.coefficient) for term in hamiltonian.terms]) block_size = max(1, (len(hamiltonian.terms) - 1).bit_length()) rand_bvec = 1 - 2 * np.random.rand(2**data_size) rand_bvec = (rand_bvec / np.linalg.norm(rand_bvec)).tolist() hamiltonian = hamiltonian * (1 / be_scaling_factor) # Define block encoding function @qfunc def block_encode_pauli(block: QNum, data: QNum): lcu_paulis_graycode(hamiltonian.terms, data, block) qsol, qprog_pauli_sym_be = get_be_state( rand_bvec, block_encode_pauli, block_size, data_size ) show(qprog_pauli_sym_be) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/37T1ClQiOA8YDfpBcdJe8Wyq4cY ``` ```python theme={null} mat_raw = mat_raw_scr.toarray() mat_sym = np.block( [ [np.zeros([nr, nr]), np.transpose(mat_raw)], [mat_raw, np.zeros([nr, nr])], ] ) expected_sol = verify_by_plot(mat_sym, rand_bvec, be_scaling_factor, qsol) assert np.linalg.norm(qsol - expected_sol) < 1e-10 ``` output # ## Banded Diagonal Block-Encoding ```python theme={null} offsets, diags, diags_maxima, prepare_norm = get_be_banded_data(mat_raw_scr) data_size = int(np.ceil(np.log2(len(diags[0])))) + 1 s_size = int(np.ceil(np.log2(len(offsets)))) # Calculate scaling factor and block size block_size = s_size + 3 be_scaling_factor = 2 * prepare_norm # Define block encoding function @qfunc def block_encode_banded_matrix(block: QNum, data: QNum): block_encode_banded_sym( offsets=offsets, diags=diags, prep_diag=diags_maxima, block=block, data=data ) rand_bvec = 1 - 2 * np.random.rand(2**data_size) rand_bvec = (rand_bvec / np.linalg.norm(rand_bvec)).tolist() qsol, qprog_banded_sym_be = get_be_state( rand_bvec, block_encode_banded_matrix, block_size, data_size ) show(qprog_banded_sym_be) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/37T1F9ZCWg4Jvvd8k9exfSZJv7p ``` ```python theme={null} mat_raw = mat_raw_scr.toarray() raw_size = 2 ** (data_size - 1) mat_sym = np.block( [ [np.zeros([raw_size, raw_size]), np.transpose(mat_raw)], [mat_raw, np.zeros([raw_size, raw_size])], ] ) expected_sol = verify_by_plot(mat_sym, rand_bvec, be_scaling_factor, qsol) assert np.linalg.norm(qsol - expected_sol) < 1e-10 ``` output # Quantum Double Slit Experiment Source: https://docs.classiq.io/explore/applications/CFD/double_slit_experiment/quantum_double_slit_experiment Open this notebook in GitHub to run it yourself *** ## Introduction and Learning Objectives This notebook is a deeply documented, educational resource for simulating the double slit experiment using both classical and quantum computational methods. It is intended for learners, researchers, and practitioners interested in quantum algorithms, numerical physics, and scientific computing. # ## What You Will Learn * The physical and mathematical background of the double slit experiment. * How to discretize and encode a wave equation on a 2D grid. * How to implement boundary conditions and obstacles (slits) in both classical and quantum settings. * The principles of Quantum Signal Processing (QSP) and Quantum Singular Value Transformation (QSVT) for quantum linear algebra. * How to visualize complex-valued fields using color and animation. * How to compare quantum and classical solutions quantitatively and visually. # ## Structure of the Notebook Each section is introduced with a markdown cell that explains the purpose, background, and expected outcomes. Code cells are extensively commented, and mathematical steps are explained in context. You are encouraged to read the markdown, run the code, and experiment with parameters. # ## Key Concepts * **Hamiltonian Construction:** The Hamiltonian encodes the physics of the double slit experiment, including the Laplacian, boundary conditions, and slit geometry. * **Classical Solution:** Solving the linear system $H \psi = \text{source}$ gives the field distribution. * **Quantum Solution:** The same problem is mapped to a quantum circuit, and QSP/QSVT is used to approximate the matrix inverse. * **Visualization:** Both amplitude and phase are visualized using HSV color mapping, and quantum results are animated to show phase evolution. * **Fidelity:** The overlap between quantum and classical solutions is computed to assess quantum algorithm accuracy. # ## How to Use This Notebook 1. **Read the explanations** in each markdown cell. 2. **Run the code cells** in order, observing the outputs and visualizations. 3. **Modify parameters** (e.g., grid size, frequency, slit configuration) to explore different scenarios. 4. **Compare the results** and reflect on the similarities and differences between classical and quantum approaches. \-- * Continue to the next sections for detailed, step-by-step implementation and analysis. ```python theme={null} # --- # SECTION 1: Import Libraries and Define Utilities # # This section imports all required libraries for numerical computation, visualization, and quantum simulation. # # - itertools: Efficient looping and combinatorics, used for grid and slit logic. # - tempfile: Temporary file creation, used for saving and displaying GIF animations. # - IPython.display.Image: Inline display of images and GIFs in Jupyter. # - enum, typing: Type annotations and enumerations for code clarity and safety. # - matplotlib, numpy, scipy: Core scientific Python stack for plotting, numerical arrays, and scientific computing. # - classiq: Quantum circuit synthesis and simulation platform, used for quantum model construction and execution. # - matplotlib.animation, matplotlib.colors: Advanced visualization and color mapping for field and phase plots. # # The following code also defines utility functions and constants used throughout the notebook, with detailed comments for each step. # !pip install -qq "classiq[qsp]" import itertools # For efficient looping and combinatorics, e.g., for slit removal import tempfile # For creating temporary files (used in GIF animation) from enum import Enum # For defining modes and configuration enums from typing import Any, Iterable, List # For type annotations import matplotlib.pyplot as plt # For plotting and visualization import numpy as np # For numerical arrays and linear algebra import scipy # For scientific computing (e.g., FFT, Chebyshev polynomials) from IPython.display import Image # For displaying GIFs inline in the notebook from matplotlib.animation import FuncAnimation # For creating animated GIFs from matplotlib.colors import hsv_to_rgb # For HSV color mapping from classiq import * # Classiq platform for quantum circuit synthesis and simulation from classiq.applications.qsp import qsvt_phases from classiq.qmod.symbolic import pi # --- # SECTION 2: Simulation Parameters and Physical Constants # # Here we define all the physical and simulation parameters for the double slit experiment. # # - n_x, n_y: Number of qubits (bits) in the x and y directions, determining the resolution of the simulation grid. # - c, f0, omega0: Physical constants for the wave equation (speed of light, frequency, angular frequency). # - sources_xy, sources_phase: Locations and phases of the sources that emit the wave. # - Boundary and slit configuration: Determines where the slits (openings) and obstacles are placed in the grid. # # The code also includes utility functions for amplitude normalization and state preparation, with detailed explanations. # class Mode(str, Enum): DENSE = "DENSE" # Dense mode for quantum circuit construction PRETTY = "PRETTY" # Pretty mode (alternative, not used here) MODE = Mode.DENSE # Use dense mode for this notebook def get_amplitudes(source: Iterable[float], target: Iterable[float]) -> List[float]: """ Compute normalized amplitudes for quantum state preparation. Used to prepare the initial quantum state corresponding to the source vector. Args: source: Iterable of source amplitudes (reference values) target: Iterable of target amplitudes (desired values) Returns: List of normalized amplitudes for quantum state preparation. """ source = np.array(source, dtype=float) target = np.array(target, dtype=float) res = np.sqrt(target / source) res = (res / np.linalg.norm(res)).tolist() return res # --- # Grid and Hamiltonian Parameters # n_x = 4 # Number of bits/qubits in x direction (controls grid resolution) n_y = 4 # Number of bits/qubits in y direction should_remove = (n_y >= 3) and ( n_x >= 3 ) # Whether to include slit removal logic (for double slit) L_x = 2**n_x # Number of grid points in x (spatial resolution) L_y = 2**n_y # Number of grid points in y dL = 1 / max(L_x, L_y) # Grid spacing (normalized to 1) LL = L_x * L_y # Total number of grid points (system size) # --- # Physical Constants for the Wave Equation # c = 1 # Speed of light (arbitrary units) f0 = 3 # Frequency of the source (arbitrary units) j0 = 1 # Not used directly, but could represent current density omega0 = 2 * np.pi * f0 # Angular frequency omega1 = omega0 * dL / c # Normalized angular frequency (for discretized grid) diag = ( omega1**2 - 4 ) # Diagonal term for the Hamiltonian (from discretized wave equation) should_negate = diag < 0 # Whether to negate a term (for stability in quantum encoding) diag_abs = np.abs(diag) # Absolute value for amplitude encoding abc_phase = np.angle( (1 + 0.5j * omega1) / (-1 + 0.5j * omega1) * (-1 if should_negate else 1) ) # Phase for absorbing boundary condition amplitudes1 = get_amplitudes( [1 / 4, 1 / 2, 1, 1], [1, diag_abs, diag_abs, 0] ) # Amplitudes for block encoding encoding_scale = ( 4 + 2 * diag_abs + diag_abs ) # Normalization factor for Hamiltonian encoding # Print key simulation parameters for reference print("omega1 (normalized angular frequency):", omega1) print("diag (Hamiltonian diagonal term):", diag) print("encoding scale (Hamiltonian normalization):", encoding_scale) # --- # Identity and Zero Matrices for Grid Construction # id_L_x = np.eye(L_x, dtype=float) # Identity matrix for x dimension id_L_y = np.eye(L_y, dtype=float) # Identity matrix for y dimension zero_LL = np.zeros((LL, LL)) # Zero matrix for the full grid id_LL = np.eye(LL, dtype=float) # Identity matrix for the full grid # --- # Source Configuration: Two Sources Near the Bottom Center # num_sources = 2 # Number of sources (for double slit, typically 2) sources_xy = [ [L_x // 2, max(L_y // 8, 1)], [L_x // 2 - 1, max(L_y // 8, 1)], ] # Source positions (x, y) sources_phase = [1, 1] # Source phases (can be complex for more general cases) # Prepare the initial source vector (normalized) source_vec = np.zeros(LL, dtype=complex) for source in range(num_sources): _x, _y = sources_xy[source] _xy = _x * L_y + _y source_vec[_xy] = sources_phase[source] source_vec = source_vec / np.linalg.norm(source_vec) # Indices for permutation logic (used in quantum state preparation) source_ij = [0] * num_sources for source in range(num_sources): _x, _y = sources_xy[source] _xy = _x * L_y + _y source_ij[source] = _xy # Single-qubit source amplitudes (for quantum state preparation) source_single_qubit = np.array(sources_phase) source_single_qubit = source_single_qubit / np.linalg.norm(source_single_qubit) # Initial state as a list (used for both classical and quantum initialization) init = source_vec.tolist() # --- # SECTION 3: Classical Hamiltonian Construction and Solution # # This section constructs the discretized Hamiltonian matrix for the 2D grid, including boundary conditions and slit removal logic. # The matrix is then solved classically to obtain the reference solution for the field distribution. # # - Laplacian and boundary terms are constructed using numpy operations. # - Slit removal is implemented by zeroing out matrix rows/columns corresponding to obstacles. # - The classical solution is obtained by direct matrix inversion. # _semi_lap_x = np.roll(id_L_x, 1, 0) + np.roll(id_L_x, -1, 0) _semi_lap_y = np.roll(id_L_y, 1, 0) + np.roll(id_L_y, -1, 0) grad_amp = diag * (1 + 0.5j * omega1) / (-1 + 0.5j * omega1) _grad_f_x = np.zeros((1, L_x), dtype=complex) _grad_f_x[0, :2] = np.array([0, grad_amp]) _grad_f_y = np.zeros((1, L_y), dtype=complex) _grad_f_y[0, :2] = np.array([0, grad_amp]) _grad_b_x = np.zeros((1, L_x), dtype=complex) _grad_b_x[0, -2:] = np.array([grad_amp, 0]) _grad_b_y = np.zeros((1, L_y), dtype=complex) _grad_b_y[0, -2:] = np.array([grad_amp, 0]) grad_f_x = ( np.tensordot(_grad_f_x, id_L_y, axes=0).transpose(0, 2, 1, 3).reshape(L_y, LL) ) grad_f_y = ( np.tensordot(id_L_x, _grad_f_y, axes=0).transpose(0, 2, 1, 3).reshape(L_x, LL) ) grad_b_x = ( np.tensordot(_grad_b_x, id_L_y, axes=0).transpose(0, 2, 1, 3).reshape(L_y, LL) ) grad_b_y = ( np.tensordot(id_L_x, _grad_b_y, axes=0).transpose(0, 2, 1, 3).reshape(L_x, LL) ) semi_lap_x = ( np.tensordot(_semi_lap_x, id_L_y, axes=0).transpose(0, 2, 1, 3).reshape(LL, LL) ) semi_lap_y = ( np.tensordot(id_L_x, _semi_lap_y, axes=0).transpose(0, 2, 1, 3).reshape(LL, LL) ) mat_em = np.array(semi_lap_x + semi_lap_y, dtype=complex) mat_em[:L_y] = 0 mat_em[::L_y] = 0 mat_em[-L_y:] = 0 mat_em[L_y - 1 :: L_y] = 0 mat_em[:L_y] += grad_f_x mat_em[-L_y:] += grad_b_x mat_em[::L_y] += grad_f_y mat_em[L_y - 1 :: L_y] += grad_b_y mat_em[0] = 0 mat_em[L_y - 1] = 0 mat_em[-L_y] = 0 mat_em[-1] = 0 if should_remove: start_idx_x_0 = 0 end_idx_x_0 = 2 * 2 ** (n_x - 3) start_idx_x_1 = 3 * 2 ** (n_x - 3) end_idx_x_1 = 5 * 2 ** (n_x - 3) start_idx_x_2 = 6 * 2 ** (n_x - 3) end_idx_x_2 = L_x start_idx_y = 3 * 2 ** (n_y - 3) end_idx_y = 4 * 2 ** (n_y - 3) for _x in itertools.chain( range(start_idx_x_0, end_idx_x_0), range(start_idx_x_1, end_idx_x_1), range(start_idx_x_2, end_idx_x_2), ): for _y in range(start_idx_y, end_idx_y): idx = _x * L_y + _y mat_em[idx, :] = 0 mat_em[:, idx] = 0 mat_em = mat_em + diag * id_LL svd = np.linalg.svd(mat_em / encoding_scale, compute_uv=True) print("Hamiltonian singular values:", (svd[1][0], svd[1][-1])) ref = np.linalg.solve(mat_em, init) def normalize_phase(data: np.ndarray) -> np.ndarray: """ Normalize the phase of a complex vector so that the maximum amplitude is real and positive. This is useful for comparing quantum and classical solutions up to a global phase. Args: data: Complex numpy array (state vector) Returns: Phase-normalized numpy array """ _max = max(data, key=np.abs) return data * (np.abs(_max) / _max) ref = normalize_phase(ref) # --- # SECTION 4: Visualization Utilities # # This section provides functions for visualizing complex-valued fields using HSV color mapping and for plotting results. # def complex2HSV(z: np.ndarray, percentile: float = 50, hue_start: float = 0): """ Convert a complex-valued array to an HSV color image for visualization. Amplitude is mapped to brightness, phase to hue. Args: z: Complex numpy array percentile: Percentile for amplitude scaling (for contrast) hue_start: Phase offset for hue mapping Returns: RGB image as numpy array """ amp = np.abs(z) scale = 1 / np.percentile(amp, percentile) f = lambda var: np.tanh(scale * var) amp = f(amp) ph = np.angle(z, deg=1) + hue_start h = (ph % 360) / 360 s = 0.85 * np.ones_like(h) v = amp / np.max(amp) return hsv_to_rgb(np.dstack((h, s, v))) def hsv_plot(data: np.ndarray, title: str = ""): """ Plot a complex-valued 2D array using HSV color mapping. Args: data: 2D complex numpy array title: Plot title """ _data = data / np.linalg.norm(data, ord="fro") __data = complex2HSV(_data) plt.imshow( __data, origin="lower", aspect="equal", ) plt.title(title) plt.tight_layout() plt.show() hsv_plot(ref.reshape((L_x, L_y)), "Reference solution") # --- # SECTION 5: Polynomial Approximation for Quantum Signal Processing (QSP) # # This section defines helper functions for constructing the optimal polynomial approximation required for QSP/QSVT-based matrix inversion. # Chebyshev polynomials and DCT are used to generate the coefficients. # def y_from_x(x, a): """ Helper function for Chebyshev polynomial evaluation in QSP. Maps x to y according to the transformation in the QSP literature. Args: x: Input value(s) a: Parameter for mapping Returns: Transformed value(s) """ return (2.0 * x**2 - (1.0 + a**2)) / (1.0 - a**2) def P_eval(x, d, a): """ Evaluate the optimal odd polynomial P_{2n-1}(x; a) for QSP/QSVT. Uses Chebyshev polynomials and the mapping from QSP theory. Args: x: Input value(s) d: Degree (must be odd) a: Parameter for mapping Returns: Evaluated polynomial values """ if d % 2 == 0: raise ValueError("d must be odd") n = (d + 1) // 2 x = np.asarray(x, dtype=float) # main branch y = y_from_x(x, a) Ln = scipy.special.eval_chebyt(n, y) L0 = scipy.special.eval_chebyt(n, y_from_x(0.0, a)) out = (1.0 - (Ln / L0)) / x return out def P_chebyshev(d: int, a: float): """ Compute Chebyshev-T coefficients of the optimal polynomial for QSP/QSVT. Uses DCT-II on Chebyshev–Gauss nodes. Args: d: Degree (must be odd) a: Parameter for mapping Returns: Chebyshev coefficients as numpy array """ if d % 2 == 0: raise ValueError("d must be odd (degree = d = 2n-1).") N = d + 1 xk = np.cos(np.pi * (np.arange(N) + 0.5) / N) # Chebyshev–Gauss nodes yk = P_eval(xk, d, a) # sample your polynomial c = 2 * scipy.fft.dct(yk, type=2, norm="forward") # (Optional) enforce odd parity numerically if desired: c[0::2] = 0.0 return c def chebT_coeffs_fm( m: int, w_min: float, w_max: float = 1.0, scale: float = 0.5, plot: bool = True, ): """ Compute and (optionally) plot Chebyshev coefficients for QSP polynomial approximation. Args: m: Degree of polynomial w_min: Minimum singular value (domain start) w_max: Maximum singular value (domain end) scale: Scaling factor for the polynomial plot: Whether to plot the approximation Returns: Chebyshev coefficients as numpy array """ if m % 2 == 0: m = m - 1 c = (scale * w_min) * P_chebyshev(m, w_min) xj = 1 - np.linspace(0, 1, 1000, endpoint=False) xj_target = xj[(xj >= w_min)] if plot: y_target = (scale * w_min) / xj_target y_approx = np.polynomial.Chebyshev(c)(xj) y_P = P_eval(xj, m, w_min) * (scale * w_min) # Plot plt.figure(figsize=(10, 10)) plt.plot(xj_target, y_target, label="Target function", linewidth=4) plt.plot( xj, y_approx, "--", label="Polynomial approximation", linewidth=2, c="r", ) plt.plot( xj, y_P, ".", label="Optimal Polynomial", linewidth=1, c="g", ) plt.title("Polynomial Approximation vs Target Function") plt.xlabel("x") plt.ylabel("f(x)") # Draw vertical lines plt.axvline(w_min, color="gray", linestyle=":", linewidth=3) plt.xlim(0, w_max) plt.ylim(0, scale) plt.legend() plt.grid(True) plt.show() return c.astype(float) pcoefs = chebT_coeffs_fm( np.round(0.75 * max(L_x, L_y) ** 2), svd[1][-1], svd[1][0], scale=0.75, plot=True ) # --- # SECTION 6: Quantum Signal Processing (QSP) Phase Sequence Preparation # # This section prepares the phase sequence for QSP/QSVT, converting it to the convention required by the quantum circuit implementation. # The phase sequence is used for quantum matrix inversion in the quantum algorithm. # inv_svd = np.polynomial.Chebyshev(pcoefs)(svd[1]) best = (svd[2].conjugate().T * inv_svd) @ (svd[0].conjugate().T @ init) expected_fidelity = ( np.abs(best.conjugate().T @ ref) / np.linalg.norm(best) / np.linalg.norm(ref) ) ** 2 print("Expected fidelity:", expected_fidelity) assert expected_fidelity >= 0.95, "Expected fidelity is too low" ang_seq = qsvt_phases(pcoefs) BLOCK_SIZE = 6 # --- # SECTION 7: Quantum Model Construction and Execution # # This section defines the quantum data structures, quantum functions, and the main quantum model for the double slit experiment. # It includes the logic for encoding the Hamiltonian, boundary conditions, and slit removal in the quantum circuit. # The quantum circuit is synthesized and executed using the Classiq platform, and the resulting state vector is extracted for analysis. # class E_Field(QStruct): y: QArray[n_y] x: QArray[n_x] class BlockEncodedState(QStruct): block: QNum[BLOCK_SIZE] data: E_Field @qperm def equals(val: int, qvar1: Const[QNum], target: QBit) -> None: target ^= qvar1 == val @qfunc def demi_semi_laplacian( r: QNum, block: QBit, ) -> None: within_apply( lambda: H(block), lambda: ( control(block == 1, lambda: inplace_add(-1, r)), control(block == 0, lambda: inplace_add(1, r)), ), ) @qperm def remove_scatterer_statement(x: Const[QNum], y: Const[QNum], block: QBit) -> None: block ^= (~((x == 2) ^ (x == 5))) & (y == 3) @qperm def remove_scatterer( e_field: E_Field, block: QBit, ) -> None: if MODE == Mode.DENSE: temp_x = QBit() within_apply( lambda: ( allocate(temp_x), X(temp_x), equals(2, e_field.x[n_x - 3 : n_x], temp_x), equals(5, e_field.x[n_x - 3 : n_x], temp_x), ), lambda: (equals(1 + 2 * 3, [temp_x, e_field.y[n_y - 3 : n_y]], block),), ) elif MODE == Mode.PRETTY: remove_scatterer_statement( e_field.x[n_x - 3 : n_x], e_field.y[n_y - 3 : n_y], block ) @qfunc def semi_laplacian( e_field: E_Field, block: QArray[2], ) -> None: within_apply( lambda: H(block[1]), lambda: ( control(block[1] == 0, lambda: demi_semi_laplacian(e_field.x, block[0])), control(block[1] == 1, lambda: demi_semi_laplacian(e_field.y, block[0])), ), ) @qperm def boundary_flag(r: Const[QArray], flag: QBit) -> None: equals(0, r, flag) equals((1 << r.len) - 1, r, flag) def boundary_condition(r: Any) -> Any: return (r == 0) ^ (r == (1 << r.size) - 1) @qperm def semi_laplacian_removed_statement( flag_scatterer: Const[QBit], x: Const[QNum], y: Const[QNum], block: QBit ) -> None: block ^= flag_scatterer | boundary_condition(x) | boundary_condition(y) @qfunc def semi_laplacian_removed( e_field: E_Field, block: QArray[4], ) -> None: if MODE == Mode.DENSE: remove_scatterer(e_field, block[2]) if should_remove else None semi_laplacian(e_field, block[0:2]) flag_scatterer = QBit() flag_x = QBit() flag_y = QBit() within_apply( lambda: ( allocate(flag_scatterer), remove_scatterer(e_field, flag_scatterer) if should_remove else None, allocate(flag_x), boundary_flag(e_field.x, flag_x), allocate(flag_y), boundary_flag(e_field.y, flag_y), ), lambda: ( equals(0, [flag_scatterer, flag_x, flag_y], block[3]), X(block[3]), ), ) elif MODE == Mode.PRETTY: remove_scatterer(e_field, block[2]) if should_remove else None semi_laplacian(e_field, block[0:2]) flag_scatterer = QBit() within_apply( lambda: ( allocate(flag_scatterer), remove_scatterer(e_field, flag_scatterer) if should_remove else None, ), lambda: semi_laplacian_removed_statement( flag_scatterer, e_field.x, e_field.y, block[3], ), ) @qperm def demi_semi_abc_statement( r: Const[QNum], block: QBit, ) -> None: block ^= ~boundary_condition(r) @qperm def demi_semi_abc( r: QArray, block: QBit, ) -> None: if MODE == Mode.DENSE: X(r[0]) boundary_flag(r, block) X(block) elif MODE == Mode.PRETTY: X(r[0]) demi_semi_abc_statement(r, block) @qperm def semi_abc_statement( x: Const[QNum], y: Const[QNum], block: QBit, ) -> None: block ^= ~(boundary_condition(x) ^ boundary_condition(y)) @qfunc def semi_abc( e_field: E_Field, block: QArray[3], ) -> None: if MODE == Mode.DENSE: within_apply( lambda: H(block[1]), lambda: ( control(block[1] == 0, lambda: demi_semi_abc(e_field.x, block[0])), control(block[1] == 1, lambda: demi_semi_abc(e_field.y, block[0])), ), ) boundary_flag(e_field.x, block[2]) boundary_flag(e_field.y, block[2]) X(block[2]) elif MODE == Mode.PRETTY: within_apply( lambda: H(block[1]), lambda: ( control(block[1] == 0, lambda: demi_semi_abc(e_field.x, block[0])), control(block[1] == 1, lambda: demi_semi_abc(e_field.y, block[0])), ), ) semi_abc_statement(e_field.x, e_field.y, block[2]) @qfunc def hamiltonian( e_field: E_Field, block: QArray[BLOCK_SIZE], ) -> None: block1 = QArray(None, QBit, 4) block2 = QNum() within_apply( lambda: ( bind(block, [block1, block2]), inplace_prepare_amplitudes(amplitudes1, 0.0, block2), ), lambda: ( control( block2 == 0, lambda: semi_laplacian_removed(e_field, block1), ), control( block2 == 1, lambda: ( semi_abc(e_field, block1[0:3]), U(0, 0, 0, abc_phase, block1[0]), ), ), ( control(block2 == 2, lambda: U(0, 0, 0, pi, block1[0])) if should_negate else None ), ), ) @qperm def _inplace_xor(i: int, target: QNum) -> None: inplace_xor(i, target) @qperm def permute_block(i: int, j: int, data: QArray): """ Returns the permutation operation of i->0 and j->1 in the qubit register data. """ assert i != j # move the i state to the 0 state ### Why cant I use inplace_xor??? This used to work!!! _inplace_xor(i, data) # update j j = j ^ i if j == 1: return # get the last index for which j is not 0 pivot = j.bit_length() - 1 # remove pivot from j j = j % (1 << pivot) # move the j state to the 0 state control(data[pivot], lambda: _inplace_xor(j, data[0:pivot])) SWAP(data[0], data[pivot]) @qfunc def inplace_prepare_init(data: QArray): inplace_prepare_amplitudes(source_single_qubit, 0.0, data[0]) invert(lambda: permute_block(*source_ij, data)) @qfunc def main(res: Output[QNum[n_x + n_y]], block: Output[QNum[BLOCK_SIZE + 1]]) -> None: state = BlockEncodedState() allocate(state) inplace_prepare_init(state.data) aux = QBit() allocate(aux) qsvt_inversion( ang_seq, lambda _aux: equals(0, state.block, _aux), lambda: hamiltonian(state.data, state.block), aux, ) bind([aux, state], [block, res]) print("Starting model creation...") qmod = create_model( main, constraints=Constraints(max_width=18), preferences=Preferences( debug_mode=False, transpilation_option="none", optimization_level=0, timeout_seconds=1200, ), ) print("Model created successfully.") print("Starting synthesis...") qprog = synthesize(qmod, auto_show=False) print("Program synthesized successfully.") print("Program width:", qprog.data.width) print("Starting execution...") # Post-select block == 0 on the statevector simulator. res_1 = calculate_state_vector(qprog, filters={"block": 0}) print("Execution completed successfully.") df = res_1.sort_values("res") state_result_1 = df[(df.block == 0)].amplitude.values state_result_1 = normalize_phase(state_result_1) state_result_prob = sum(np.abs(state_result_1) ** 2) print("State vector probability:", state_result_prob) print( "fidelity", ( np.abs(ref.conjugate() @ state_result_1) / np.linalg.norm(state_result_1) / np.linalg.norm(ref) ) ** 2, ) hsv_plot(state_result_1.reshape((L_x, L_y)), "State vector result") # --- # SECTION 8: Visualization of Quantum Results and Animation # # This section provides a function to animate the quantum field result as a GIF, showing phase evolution over time. # The GIF is saved and displayed inline in the notebook. # def show_gif( data: np.ndarray, fps: int = 30, duration: float = 2, percentile: float = 70, title: str = "", output_file: str = None, ): """ Save a GIF from the given data to a file and display it inline in the notebook. Args: data (np.ndarray): The input data array (typically a 2D field). fps (int): Frames per second for the GIF. duration (float): Duration of the GIF in seconds. percentile (float): Percentile for scaling the data (for contrast). title (str): Title for the frames. output_file (str): Optional file path to save the GIF. """ frames = int(fps * duration) scale = 1 / np.percentile(np.abs(data), percentile) f = lambda var: np.tanh(scale * var) fig, ax = plt.subplots() im = ax.imshow( f(data.real), origin="lower", aspect="equal", cmap="twilight", vmin=-1, vmax=1, ) ax.set_title(title) def update(frame): _data = f((np.exp(-1j * 2 * np.pi * frame / frames) * data).real) im.set_array(_data) return [im] plt.tight_layout() anim = FuncAnimation( fig, update, frames=range(frames), interval=duration * 1000 / frames, blit=True ) # Use a temporary file if no output_file is provided if output_file is None: temp_file = tempfile.NamedTemporaryFile(suffix=".gif", delete=False) output_file = temp_file.name temp_file.close() anim.save(output_file, writer="pillow", fps=fps) # Display the saved GIF display(Image(filename=output_file)) show_gif(state_result_1.reshape((L_x, L_y))) ``` **Output:** ``` omega1 (normalized angular frequency): 1.1780972450961724 diag (Hamiltonian diagonal term): -2.612086881096809 encoding scale (Hamiltonian normalization): 11.836260643290428 Hamiltonian singular values: (np.float64(0.5507400024633752), np.float64(0.01184874011425535)) ``` output output **Output:** ``` Expected fidelity: 0.9809080489008082 Starting model creation... Model created successfully. Starting synthesis... Program synthesized successfully. Program width: 18 Starting execution... Execution completed successfully. State vector probability: 0.04579582126260026 fidelity 0.9809080489008286 ``` output **Output:** ``` ``` output # Quantum Algorithm for Solving the 1D Heat Equation Source: https://docs.classiq.io/explore/applications/CFD/heat_eq_qsvt/heat_eq_qsvt Open this notebook in GitHub to run it yourself The Heat Equation is a fundamental partial differential equation (PDE) describing how heat diffuses through a medium over time. It is widely used in physics and engineering applications. Mathematically, it is expressed as: $$ \frac{\partial u}{\partial t} = \alpha \nabla^2 u, $$ where $$ \ u(x, t) $$ is the temperature, $ \alpha$ is the thermal diffusivity, and $\nabla^2$ is the Laplacian operator. The initial condition defines the temperature profile at $t = 0$, while the boundary conditions describe the time-varying temperature at the rod's ends. In this notebook, we will solve the Heat Equation using the **Quantum Singular Value Transformation (QSVT)** algorithm. The Heat Equation will first be discretized into a linear system, and then QSVT will be applied to approximate its solution. ## Example Let 1D stick of length L, the temperature distribution at time t = 0 (initial condition) and the temperature at the boundaries are constant and equal to 0 as formulated below: # ## Initial Condition $u(x, 0) = \sin(\pi x), \quad x \in [0, L]$ # ## Boundary Conditions * **Left Boundary:** $u(0, t) = 0$ * **Right Boundary:** $u(L, t) = 0$ We wish to predict what is the temperature distribution at time $t_n$ given the intial and boundary conditions descrbied above. ```python theme={null} import numpy as np M = 8 # Make this bigger according to 18 qubits limit total TIME_STEPS = 1024 # might have to be smaller, like 4... dx = 1 / M dt = 1 / TIME_STEPS alpha = 1 assert dt <= alpha * dx**2 start_position = 0 end_position = 1 start_time = 0 end_time = 2 frequency = 0 * TIME_STEPS t = np.linspace(start_time, end_time, TIME_STEPS) x = np.linspace(start_position, end_position, M) ``` ```python theme={null} # Intial Conditions ux = np.sin(np.pi * x) # Print the initial definitions import matplotlib.pyplot as plt plt.figure() plt.title("initial condition") plt.plot(x, ux) plt.ylabel("ux") plt.xlabel("x") plt.show() ``` output ```python theme={null} # Boundary Conditions u0 = 0 * t uM = 0 * t plt.figure() plt.title("left boundary condition") plt.plot(t, u0, color="green") plt.ylabel("u0") plt.xlabel("t") plt.show() plt.figure() plt.title("right boundary condition") plt.plot(t, uM, color="red") plt.ylabel("uM") plt.xlabel("t") plt.show() ``` output output # ## Discretization of the Heat Equation To solve the Heat Equation numerically, we discretize it using the **finite difference method**. In one spatial dimension: $$ \frac{\partial u}{\partial t} \approx \frac{u_i^{n+1} - u_i^n}{\Delta t}, \quad \frac{\partial^2 u}{\partial x^2} \approx \frac{u_{i+1}^n - 2u_i^n + u_{i-1}^n}{\Delta x^2}. $$ Substituting these into the Heat Equation gives: $$ \frac{u_i^{n+1} - u_i^n}{\Delta t} = \alpha \frac{u_{i+1}^n - 2u_i^n + u_{i-1}^n}{\Delta x^2}. $$ Rearranging terms, we obtain: $$ u_i^{n+1} = u_i^n + \frac{\alpha \Delta t}{\Delta x^2} \left( u_{i+1}^n - 2u_i^n + u_{i-1}^n \right). $$ This can be expressed in matrix form as: $$ A \mathbf{u}^{n+1} = \mathbf{b}^{n}, $$ where $A$ is a matrix derived from the discretization, and $\mathbf{b}$ represents the system's state at the current time step. The goal is to compute $\mathbf{u}^{n+1}$, the state at the next time step. # ### **Matrix $A$ and Vector $b$: Discretization of the Heat Equation** To solve the 1D heat equation numerically, we discretize it using the **finite difference method**. The resulting system can be written as: $A \mathbf\{u\}^\{n+1\} = \mathbf\{b\},$ where: * $A$ is the **system matrix** derived from the discretization of the second spatial derivative using a tri-diagonal structure. * $\mathbf{b}$ includes contributions from the **initial conditions** and **boundary conditions**. # ### **Construction of Matrix $L$:** The Laplacian operator $L$ is represented as a tri-diagonal matrix: $L = \begin\{bmatrix\} 2 & -1 & 0 & \cdots & 0 \\ -1 & 2 & -1 & \cdots & 0 \\ 0 & -1 & 2 & \cdots & 0 \\ \vdots & \vdots & \vdots & \ddots & -1 \\ 0 & 0 & 0 & -1 & 2 \end\{bmatrix\}$ # ### **Matrix $A$:** Using the time step $\Delta t$ and spatial step $\Delta x$, the system matrix $A$ is constructed as: $A = I + \alpha \frac\{\Delta t\}\{\Delta x^2\} L,$ where $I$ is the identity matrix. # ### **Visualization of Matrix $A$:** The following code generates a matrix $A$, constructs the vector $b$ for the initial and boundary conditions, and visualizes $A$: 1. **Initial Conditions:** $u(x, 0) = \sin(\pi x)$ ```python theme={null} L = ( 2 * np.diag(np.ones(M)) + -1 * np.diag(np.ones(M - 1), 1) + -1 * np.diag(np.ones(M - 1), -1) ) A = np.eye(M) + alpha * dt / dx**2 * L # dt/dx**2* # A_norm = 1/0.54215392 * A initial_conditions = ux boundary_conditions = 0 * ux boundary_conditions[0] = u0[0] boundary_conditions[-1] = uM[0] b = initial_conditions + alpha * dt / dx**2 * boundary_conditions norm_factor = np.linalg.norm(b) b_normalized = b / norm_factor import matplotlib.pyplot as plt plt.plot(x, b_normalized) plt.xlabel("x") plt.ylabel("b normalized") plt.show() ``` output ```python theme={null} print(A) plt.matshow(A) plt.title("A matrix") ``` **Output:** ``` [[ 1.125 -0.0625 0. 0. 0. 0. 0. 0. ] [-0.0625 1.125 -0.0625 0. 0. 0. 0. 0. ] [ 0. -0.0625 1.125 -0.0625 0. 0. 0. 0. ] [ 0. 0. -0.0625 1.125 -0.0625 0. 0. 0. ] [ 0. 0. 0. -0.0625 1.125 -0.0625 0. 0. ] [ 0. 0. 0. 0. -0.0625 1.125 -0.0625 0. ] [ 0. 0. 0. 0. 0. -0.0625 1.125 -0.0625] [ 0. 0. 0. 0. 0. 0. -0.0625 1.125 ]] ``` **Output:** ``` Text(0.5, 1.0, 'A matrix') ``` output ## Problem Formulation $u^n -$ is the vector representation of the temperature of the stick at time n. $ u^n = [u_0^n, u_1^n, \cdots, u_M^n]^T$ $b^n -$ is the sum of the trmperature at time n-1 and boundary condition at time n. $ b^n = [b_0^n, b_1^n, \cdots, b_M^n]^T$ $A -$ Problem operator define as the sum of Laplacian operator multiplied by discret operator and identity. So, we state the temperature at time n as a linear problem depends on time n-1 and the problem operator: $Au^n = b^n$ Suppose we aim to predict the temperatue distribution at time n. 1. **Problem initialization:** * $u^0=$ defined by initial condition. * $b^1=$ define as the sum of $u^0$ and the boundary condition. 2. **Main loop:** **for any i until n:** * $u^i = A^{-1} b^i$ * $b^{i+1} = u^i + boundary condition$ Suppose we have quantum registers b and u holding the input and the result at each iteration. as stated above, first prepering the state of register b such that it holds the vector b (b must be normalisied)appling repitatlly QSVT circuit to compute u at time n. ```python theme={null} !pip install -qq "classiq[qsp]" ``` ```python theme={null} import numpy as np from numpy.polynomial import Polynomial from pyqsp.angle_sequence import QuantumSignalProcessingPhases from classiq import * from classiq.execution import ClassiqBackendPreferences, ExecutionPreferences ``` ```python theme={null} def get_qprog(main, constraints=None, preferences=Preferences(transpilation_option="none", timeout_seconds=600, optimization_level=0)) -> str: # type: ignore[no-untyped-def] """ This function gets a model and synthesizes it. It also sets the execution on a statevector """ execution_preferences = ExecutionPreferences( num_shots=1, backend_preferences=ClassiqBackendPreferences( backend_name=ClassiqSimulatorBackendNames.SIMULATOR_STATEVECTOR ), ) qmod = create_model( main, execution_preferences=execution_preferences, constraints=constraints, preferences=preferences, ) qmod = set_execution_preferences(qmod, execution_preferences) qprog = synthesize(qmod) return qprog def get_projected_state_vector( # type: ignore[no-untyped-def] execution_result, measured_var: str, projections: dict, ) -> np.ndarray: """ This function returns a reduced statevector from execution results. measured var: the name of the reduced variable projections: on which values of the other variables to project, e.g., {"ind": 1} """ projected_size = len(execution_result.output_qubits_map[measured_var]) proj_statevector = np.zeros(2**projected_size).astype(complex) for sample in execution_result.parsed_state_vector: if all( int(sample.state[key]) == projections[key] for key in projections.keys() ): value = int(sample.state[measured_var]) proj_statevector[value] += sample.amplitude global_phase = np.angle(execution_result.parsed_state_vector[0].amplitude) return np.real(proj_statevector / np.exp(1j * global_phase)) ``` ## Block Encoding # ## What Is Quantum Block Encoding? Quantum block encoding is a method in quantum computing used to embed a matrix or operator into a unitary matrix that can be implemented efficiently on a quantum computer. This technique is essential for quantum algorithms that require matrix operations, such as quantum machine learning, quantum linear algebra etc... THe starting point of QSVT is the availability of matrix A encoded as a block inside larger unitary operator U, when A is already unitary matrix BE become simply controlled-A. # ## How to Block Encode the Matrix A of Heat Equation? BE utilies the structure of the matrix A for building a quntum circuit representation (unitary operator) of matrix A. Condier matrix A has Toeplitz structure with D digonals, offset from the main diagonal by k: $$ \left( \begin{array}{ccccccc} A_k & A_{k-1} & \cdots & A_0 & & & \\ A_{k+1} & A_k & A_{k-1} & \cdots & A_0 & & \\ \vdots & A_{k+1} & A_k & A_{k-1} & \cdots & A_0 & \\ A_{D-1} & \vdots & A_{k+1} & A_k & A_{k-1} & \cdots & A_0 \\ & A_{D-1} & \vdots & A_{k+1} & A_k & A_{k-1} & \cdots \\ & & \ddots & \vdots & A_{k+1} & A_k & A_{k-1} \\ & & & A_{D-1} & \cdots & A_{k+1} & A_k \end{array} \right) $$ Specifically, A of our probles is Toeplitz matrix with 3 diagonals offset by k = 1: $$ \left( \begin{array}{ccccccc} 1+\frac{\alpha \Delta t}{\Delta x ^ 2}2 & -\frac{\alpha\Delta t}{\Delta x ^ 2} & & & \\ \frac{-\alpha\Delta t}{\Delta x ^ 2} & 1 + \frac{\alpha \Delta t}{\Delta x ^ 2} 2 & \frac{-\alpha\Delta t}{\Delta x ^ 2} & & \\ & \frac{-\alpha\Delta t}{\Delta x ^ 2} & 1 + \frac{\alpha\Delta t}{\Delta x ^ 2} 2 &\frac{-\alpha\Delta t}{\Delta x ^ 2} & \\ & \ddots & \ddots & \ddots & \\ & &\ddots & \ddots & \ddots & \\ & & & \frac{-\alpha\Delta t}{\Delta x ^ 2} & 1 + \frac{\alpha\Delta t}{\Delta x ^ 2} 2 & \frac{-\alpha\Delta t}{\Delta x ^ 2} \end{array} \right) $$ Lets set $\alpha = \frac{\Delta x ^ 2}{\Delta t}$ so matrix A is: $$ \left( \begin{array}{ccccccc} 3 & -1 & & & \\ -1 & 3 & -1 & & \\ & -1 & 3 & -1 & \\ & \ddots & \ddots & \ddots & \\ & &\ddots & \ddots & \ddots & \\ & & & -1 & 3 & -1 \end{array} \right) $$ Consider the mapping between (d,m) -> (i,j) where, (i,j) are the indices in matrix coordinates. (d,m) are the distinctiy and multiplicity of the values in A. i(d,m) = d - k + m. j(d,m) = m. Those mappings allows us to apply controlled rotation operator depending on the parameter d (superposition of posibilties of disdinct values), While m is simply the input state |j> # ## The Block Encoding Ciruit for Toeplitz Matrix Screenshot 2025-12-15 at 16.33.54.png for more info cite: [https://arxiv.org/pdf/2302.10949](https://arxiv.org/pdf/2302.10949) ```python theme={null} from classiq.qmod.symbolic import floor @qfunc def multiplex(qfuncs: QCallableList, select: QNum): repeat(qfuncs.len, lambda i: control(select == i, lambda: qfuncs[i]())) Ad = A.copy()[1][0:3] k = -1 MAT_SIZE = int(np.log2(len(A))) DISTINTICITY = np.log2(len(Ad) + 1) factor = 0.54215392 @qfunc def my_be( data: QNum[MAT_SIZE, UNSIGNED, 0], block: QArray[QBit, int(DISTINTICITY + 1 + 1)] ): rotating_qubit = QBit("rotating_qubit") del_qubit = QBit("del_qubit") select = QArray("select", QBit, DISTINTICITY) packed = QNum("packed", data.size + del_qubit.size, UNSIGNED, 0) def make_ry_lambda(d): return lambda: RY(2 * np.arccos(Ad[d] / (max(np.abs(Ad)))), rotating_qubit) def make_inplace_add_lambda(d): return lambda: inplace_add(d, packed) within_apply( lambda: [ bind(block, [select, rotating_qubit, del_qubit]), bind([data, del_qubit], packed), inplace_prepare_state([1 / 3, 1 / 3, 1 / 3, 0], 0, select), ], lambda: [ multiplex([make_ry_lambda(d) for d in range(len(Ad))], select), multiplex([make_inplace_add_lambda(d) for d in range(len(Ad))], select), inplace_add(k, packed), ], ) ``` ```python theme={null} constraints = Constraints(optimization_parameter="width") @qfunc def main(angles: CArray[CReal, MAT_SIZE], block: Output[QNum], data: Output[QNum]): allocate(DISTINTICITY + 1 + 1, block) encode_in_angle(angles, data) my_be(data, block) qprog_be = get_qprog(main, constraints=constraints) ``` ```python theme={null} show(qprog_be) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3FnXuOTjCfxZK3QgFw3KxDMnh8Z ``` ```python theme={null} from itertools import product from classiq.execution import ExecutionSession execution_angles = [ {"angles": list(bits)[::-1]} for bits in product([0, 1], repeat=MAT_SIZE) ] es = ExecutionSession(qprog_be) res = es.batch_sample(execution_angles) ``` **Output:** ``` /var/folders/kk/nlz2dw2921g33r494qcnf4jh0000gn/T/ipykernel_9502/2369689908.py:9: ClassiqDeprecationWarning: 'batch_sample' is deprecated; pass a list of parameter dicts to 'sample' instead. res = es.batch_sample(execution_angles) ``` ```python theme={null} measured_mat = np.zeros([2**MAT_SIZE, 2**MAT_SIZE]) counter = 0 for r in res: my_state = get_projected_state_vector(r, "data", {"block": 0}) measured_mat[:, counter] = np.round(my_state, 10) counter += 1 measured_mat_factor = (max(np.abs(Ad))) * len(Ad) measured_mat_normalized = measured_mat_factor * measured_mat print(np.linalg.norm(np.abs(A) - np.abs(measured_mat_normalized))) plt.imshow(measured_mat_normalized) plt.title("measured matrix") ``` **Output:** ``` 3.94889242271285e-10 ``` **Output:** ``` Text(0.5, 1.0, 'measured matrix') ``` output ## Extracting the Phases In order to invert a matrix with QSVT we may cumpute classically the phases of QSP as in approximation to the the inversion function $\frac{1}{x}$. Its done by computing the coefficients of the inversion matrix given the desierd accuracy and the lower bound of the smallest eigen value. The build-in function QuantumSignalProcessingPhases(..) takes the coefficient of descibed above and produces a list of phases that QSVT needed to be built. ```python theme={null} import pyqsp w, _ = np.linalg.eig(A) eig_val_in = np.sort(np.real(w)) pg = pyqsp.poly.PolyOneOverX() PP_coeff = 1 / eig_val_in[0] pcoefs = list(pg.generate(epsilon=0.1, kappa=PP_coeff)) poly = Polynomial(pcoefs) QSVT_PHASES = QuantumSignalProcessingPhases( poly.coef, signal_operator="Wx", method="laurent", measurement="x" ) print("Polynom degree = ", poly.degree()) pyqsp.response.PlotQSPResponse(QSVT_PHASES, signal_operator="Wx", measurement="x") ``` **Output:** ``` b=2, j0=2 [PolyOneOverX] minimum [-1.088] is at [-0.8]: normalizing [PolyOneOverX] bounding to 0.9 Polynom degree = 3 ``` output ## Quantum Singular Value Transformation (QSVT) The Quantum Singular Value Transformation (QSVT) is a powerful quantum algorithm that manipulates the singular values of a matrix encoded in a quantum state. Starting by computing the QSP phases of the inersion function classically upon given accuracy, those phases are loaded to QSVT circuit desgined to manipulate the wigen values of matrix $A$. # ## Key Steps in QSVT: 1. **Block-Encoding**: * The matrix $A$ is embedded into a larger unitary matrix $U_A $, preserving its singular values. * This step ensures the matrix is compatible with quantum operations. 1. **Polynomial Approximation**: * QSVT applies a polynomial transformation $P(x)$ to the singular values of $A$. * To solve $A \mathbf{x} = \mathbf{b}$, the polynomial is chosen as $P(x) \approx \frac{1}{x}$. 1. **Result Extraction**: * After applying QSVT, the solution $\mathbf{x} = A^{-1} \mathbf{b}$ is encoded in the quantum state. * This state can be measured or further processed to extract the solution. # ## Implementation Plan: 1. Construct the block-encoded unitary $U_A $ from the matrix $A$. 2. Design the QSVT circuit to apply $P(x)$. 3. Execute the circuit using a quantum simulator and extract the solution. For more details cite: [https://arxiv.org/abs/2105.02859](https://arxiv.org/abs/2105.02859) ```python theme={null} @qfunc def my_projector_controlled_phase(phase: CReal, block: QNum, aux: QBit): control(block == 0, lambda: X(aux)) RZ(phase, aux) control(block == 0, lambda: X(aux)) @qfunc def my_qsvt_step( phase1: CReal, phase2: CReal, u: QCallable[QArray, QArray], data: QArray[QBit], block: QArray[QBit], qsvt_aux: QBit, ): u(data, block) my_projector_controlled_phase(phase1, block, qsvt_aux) invert(lambda: u(data, block)) my_projector_controlled_phase(phase2, block, qsvt_aux) @qfunc def qsvt_inversion_my( qsvt_phases: CArray[CReal, len(QSVT_PHASES)], block: QNum, data: QNum, qsvt_aux: QBit, ): H(qsvt_aux) my_projector_controlled_phase(qsvt_phases[0], block, qsvt_aux) repeat( floor((qsvt_phases.len - 1) / 2), lambda i: my_qsvt_step( qsvt_phases[(2 * i) + 1], qsvt_phases[(2 * i) + 2], lambda d, b: my_be(d, b), data, block, qsvt_aux, ), ) H(qsvt_aux) def get_b_vector(b): return b.tolist() @qfunc def main(block: Output[QNum], data: Output[QNum], qsvt_aux: Output[QBit]): allocate(1, qsvt_aux) allocate(DISTINTICITY + 1 + 1, block) prepare_amplitudes(get_b_vector(b_normalized), 0, data) qsvt_inversion_my(QSVT_PHASES, block, data, qsvt_aux) ``` ## Results and Validation problem formulation: $u^n -$ is the vector representation of the temperature of the stick at time n. $ u^n = [u_0^n, u_1^n, \cdots, u_M^n]^T$ $b^n -$ is the sum of the trmperature at time n-1 and boundary condition at time n. $ b^n = [b_0^n, b_1^n, \cdots, b_M^n]^T$ $A -$ Problem operator define as the sum of Laplacian operator multiplied by discret operator and identity. So, we state the temperature at time n as a linear problem depends on time n-1 and the problem operator: $Au^n = b^n$ Suppose we aim to predict the temperatue distribution at time n. 1. **Problem initialization:** * $u^0=$ defined by initial condition. * $b^1=$ define as the sum of $u^0$ and the boundary condition. 2. **Main loop:** **for any i until n:** * $u^i = A^{-1} b^i$ * $b^{i+1} = u^i + boundary condition$ Suppose we have quantum registers b and u holding the input and the result at each iteration. as stated above, first prepering the state of register b such that it holds the vector b (b must be normalisied)appling repitatlly QSVT circuit to compute u at time n. ```python theme={null} TIME = 3 / TIME_STEPS N = int(np.round(TIME / dt + 0.5)) results = np.zeros([N, len(b_normalized)]) norms = np.ones([N, 1]) for i in range(N): results[i, ::] = np.prod(norms) * b_normalized qprog = get_qprog(main) es = ExecutionSession(qprog) res = es.sample() my_state = get_projected_state_vector(res, "data", {"block": 0, "qsvt_aux": 0}) print(np.linalg.norm(my_state)) norms[i] = np.linalg.norm(my_state) my_state = my_state / np.linalg.norm(my_state) b_normalized = my_state ``` **Output:** ``` 2.3804035563630187e-15 2.465956473023697e-15 2.547150433749263e-15 2.9028187025774625e-15 ``` ```python theme={null} show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3FnY4XnZTQB7JEgMcFlVbDkeHUQ ``` ```python theme={null} print(f"Width: {qprog.data.width}") ``` **Output:** ``` Width: 13 ``` ```python theme={null} plt.figure() plt.plot(x, norm_factor * results.transpose()) plt.title("Heat Distribution over Space and Time") plt.xlabel("x") plt.ylabel("Temperature u(x,t)") plt.show() ``` output # Quantum Lattice Boltzmann Method Source: https://docs.classiq.io/explore/applications/CFD/qlbm/qlbm Open this notebook in GitHub to run it yourself The Quantum Lattice Boltzmann Method (QLBM) brings the ideas of the classical lattice Boltzmann method into the quantum computing framework. In this notebook, we introduce the background concepts, explain how the classical method works, and motivate why a quantum version can help overcome some of its limitations. A variety of QBLM algorithms have been proposed (see Refs. below). Here, we focus on the collisionless case, including specular reflections. We exemplify the algorithm by analyzing an example inspired by the work of Schalkers and Moller \[1]. ## Part I * Theory and Framework # ## Background Fluid dynamics are commonly well characterized by the famous Navier-Stokes equations - a set of non-linear coupled partial differential equations. Due to the non-linearity and multiscale nature of the dynamics, a precise solution for practical systems requires substantial numerical resources (for further details see the technical supplementary at the bottom). Instead of working directly with the macroscopic fluid equations, one can take a microscopic viewpoint: Fluids are modeled as collections of particles and their behavior is described statistically, using the Boltzmann transport equation. The solution to this equation gives the dynamics for the distribution of particles positions and velocities. A numerical approach for solving the kinetic PDE equation can be obtained by employing the (classical) Lattice Boltzmann Method (LBM). # ## Lattice Boltzmann Method (LBM) The Lattice Boltzmann Method simplifies the complex dynamics over the whole of phase space by replacing the velocity distribution with a discrete set of velocities, $\{\mathbf{v}_i\}$. Each lattice site $\mathbf{x}$ is characterized by a $d$-dimensional vector of distributions $f_i(\mathbf{x})$, representing the number of particles at that point with a velocity $\mathbf{v}_i$. The distributions evolve according to a discretized version of the Boltzmann transport equation under the Bhatnagar-Gross-Krook (BKG) approximation. Within the simplified description, the evolution is dictated by three distinct processes: 1. Streaming: distributions move along the velocity directions to neighboring lattice sites $$ f_i(\mathbf{x} +\mathbf{v}_{i},t +\Delta t) = f_i (\mathbf{x},t)~~ . $$ 2. Collision: distribution of each site relaxes to equilibrium $$ f_i(\mathbf{x}, t) = f_i(\mathbf{x},t) - \Gamma (f^{\text{eq}}_i(\mathbf{x},t) - f_i(\mathbf{x},t))~~, $$ where $\Gamma$ is the relaxation rate, and $f^{\text{eq}}(\mathbf{u}(f),\rho(f),T(f))$ is the local Maxwell-Boltzmann distribution. 3\. Specular Reflection: particles reaching the boundary walls or obstacles reflect off them. We can approximate the microscopic dynamics by repeating these steps over small time intervals. Moreover, for sufficiently small lattice spacing the results converge to the hydrodynamic variables of the Navier-Stokes equations for an incompressible isothermal fluid. For example, the density and velocity density can be expressed as $$ \rho(\mathbf{x},t) = \sum_i f_i(\mathbf{x},t)~~~,~~~\rho \mathbf{u}(\mathbf{x},t) = \sum_i f_i(\mathbf{x},t) \mathbf{v}_i ~~, $$ where $\mathbf{u}$ is the (macroscopic) velocity field. The method enables a local description that can be efficiently parallelized, incorporates complex boundary conditions, and can be extended to other dynamical problems, such as heat transport and magnetohydrodynamics. # ### Challenges in the Classical Approach Despite the simplification, all classical solvers face several fundamental challenges: * The number of lattice sites required to accurately describe turbulence scales unfavorably. As a result, for realistic turbulence, the cost becomes prohibitive. * Numerical instabilities can appear at high resolutions. * For 3D systems of interest, the required number of lattice sites may be trillions. The quantum Boltzmann lattice method is motivated by these challenges. Quantum computers have the potential to bypass the scaling issues, offering potential exponential or polynomial speedups in simulating fluid dynamics. This opens the door to study regimes that are entirely out of reach for classical solvers. # ## Quantum Lattice Boltzmann Method In the Quantum Lattice Boltzmann Method (QLBM) the distributions $\{f_i\}$ are encoded in the amplitudes $\psi_{\mathbf{x},i}(0)$ of a quantum wavefunction $|\Psi\rangle$, while the streaming, collision and reflections are achieved by application of unitary transformations. The template of QLBM algorithms is composed of three stages: 1. Initialization - the initial classical distributions and velocities are mapped to separate quantum variables. The $N$ lattice sites and $V$ velocities are each represented by a quantum variable, composed of $\log(N)$ "positional" qubits and $\log(V)$ "velocity" qubits, respectively. $$ |\Psi(0)\rangle = \sum_{\mathbf{x},\mathbf{v}} \psi_{\mathbf{xv}}(0) |\mathbf{x}\rangle \otimes |\mathbf{v}\rangle~~. $$ 2. Evolution - repeated operation of $U_{\Delta t} = U_{\text{ref}} U_{\text{str}}$ propagates the state in time $$ |\Psi(t = n\Delta t)\rangle = (U_{\Delta t})^n |\Psi (0)\rangle~~. $$ Streaming and reflections are obtained by a conditional shift operator, inducing the mapping: $|\mathbf{x}\rangle\otimes |\mathbf{v}\rangle\rightarrow |\mathbf{x} + \mathbf{v}\Delta t\rangle \otimes |\mathbf{v}\rangle$, while collision can be performed by local rotations which mix the velocity states at each site. 3\. Readout - measurement of global observables or a restricted number of local observables. In the implemented example, we consider a small number of grid points and measure the full state in the computational basis. The algorithm has several variants depending on the specific physical scenario. For instance, reflecting objects may be absent from the medium and are, therefore, excluded from the evolution. In the example presented here, we consider the collisionless case, where the dynamics are governed solely by streaming and reflection. Repeating the experiment many times allows us to infer statistical averages, corresponding to the macroscopic hydrodynamic variables. Having established the general form of the QLBM update rule, we now illustrate it using a simplified, collisionless system. ## Part II * Explicit Example: Collisionless Quantum Lattice Boltzmann Dynamics with Classiq As a conceptual example, we consider a collisionless model in which the particle distribution evolves according to repeated streaming and reflection operations. We begin by defining the encoding of the initial distribution in the quantum register. # ## State Encoding The set of classical states are encoded in quantum states of the form $$ |{\text{grid|velocity}}\rangle=|{g_x g_y| v_{\text{dir},x} u_x v_{\text{dir},y} u_y}\rangle~~, $$ where the grid ($g_x$, $g_y$) and velocity magnitude ($u_x$, $u_y$) variables are encoded as QNum variables, and the velocity directions ($v_{\text{dir},x}$, $v_{\text{dir},y}$) are encoded as single qubits. The QNums allow a straightforward implementation of shift operations, while the velocity directional encoding enables flipping the direction in the $j$'th dimension by application of a single NOT operation on $v_{\text{dir},j}$. For example, a quantum state $|{2}\rangle |{4}\rangle |{0}\rangle |{6}\rangle |{1}\rangle |{3}\rangle$ means that there is a particle on lattice site $(2,4)$ with a left velocity of magnitude $6$ and up velocity of magnitude $3$. # ## Time Evolution: The time-step is composed of two operations: 1. Streaming: particles move one lattice site if their speed allows it (see below for further details). 2. Reflection: particles hitting the obstacle are specularly reflected - direction is flipped and pushed back into the domain. We consider periodic boundary conditions in space, while the obstacle is placed between selected lattice sites, acting as a perfectly reflecting wall. The obstacle is placed within the lattice forming a barrier around the center of the grid. main.png Figure 1. Collisionless QLBM algorithm circuit. The preparation step and the first two iterations of the time-evolution stage. # ## Initial Condition The quantum state is initialized to be localized at a single grid point positioned to the left of the reflection obstacle. Velocities along the $y$-axis are initialized uniformly, whereas along the $x$-axis only non-negative velocities are sampled, uniformly distributed over their possible magnitudes. # ## Observables After running the quantum circuit, we measure the * spatial distribution $p(x)$ - probability to find a particle at each site * velocity distribution $p(u)$ - probability over velocity magnitudes Next, we evaluate the scheduling function employed in the streaming and initialize the initial distribution. ```python theme={null} from typing import List, Tuple import matplotlib.pyplot as plt import numpy as np import pandas as pd from classiq import * ``` # ## Defining Model Hyperparameters ```python theme={null} # Grid length size GRID_LENGTH = 8 # The total number of lattice points is GRID_LENGTH^2 # Velocities # The velocities are normalized with respect to the lattice spacing, # meaning that unity velocity corresponds to a distance change of one lattice spacing per model unit time-step # in the positive direction of the axis. VELOCITIES = np.array( [-3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0] ) # in units (lattice spacing)/(model unit time) # Number of time-steps NUM_TIMES_STEPS = 9 # Boundary points of the reflection object X_LOW, X_HIGH = 2, 5 Y_LOW, Y_HIGH = 1, 5 ``` # ## Scheduling Function A classical time-series function implements a Courant-Friedrichs-Lewy (CFL) counter, tracking the velocity magnitudes streamed at each time-step. The time steps duration are set such that at each step the distributions $f_i(\mathbf{x})$ will at most transition to a neighboring lattice site. As a consequence, the durations generally vary between time-steps. Note, that in the following example $\mathbf{x}$ represents a two dimensional vector. To evaluate the streamed velocities we first need to compute the time interval after the $m$'th time-steps, $\Delta t^m$. To this end, consider a distribution $f_i(\mathbf{x})$ at a time-step $m$ at position $x_m$, advancing with a speed $u_i$ away from a lattice site at $x_0$. The fraction of the distance from the lattice site in the following time-step can be expressed as $$ c^{m+1}_i = \frac{x_m + |u_i| \Delta t^m -x_0}{\Delta x} = c^{m}_i + |u_i|\frac{\Delta t^m} {\Delta x}~~, $$ where $\Delta x$ is the lattice spacing. If particles may only travel to the nearest lattice site during a single time-step, to evaluate $\Delta t^m$, we set $c^{m+1}_k = 1$, leading to $ \Delta t^{m} = (1-c^m_i)\frac{\Delta x}{|u_i|}$. Finally, for multiple possible velocities, the limiting time step is evaluated by minimizing over all velocity magnitudes $$ \Delta t^{m} = \min_{i}(1-c^m_i)\frac{\Delta x}{|u_i|}~~. $$ Utilizing $\Delta t^{m}$ we can evaluate $c^{m}_i$ for all velocites and determine which distributions have reached a lattice site. ```python theme={null} def time_series( discrete_velocities: np.ndarray, time: float = 10.0, tolerance: float = 1e-6, max_iters: int = 10**3, ) -> List[List[int]]: """ Implements a CFL counter, evaluating the time series of streaming velocities for a discrete set of velocities Parameters: discrete_velocities: list, time: float, simulation time tolerance: float, max_iters: int, maximum number of propagation steps Returns: schedule: List[List[int]], each list dictates the velocities streamed at the corresponding time step. accumulated_time: float, total time duration of the simulation """ num_discrete_velocities = discrete_velocities.shape[0] velocity_magnitudes = np.array(sorted(set((np.abs(discrete_velocities).tolist())))) # Assumes that velocity magnitudes are the same in all directions M = discrete_velocities.shape[0] // 2 + (discrete_velocities.shape[0] % 2) # Track the "progress" of each distribution towards the next grid point cfl_counter = np.zeros(M) eps = 1e-12 # tolerance for "vanishing" regulated_magnitudes = velocity_magnitudes.copy() regulated_magnitudes[regulated_magnitudes == 0] = eps inverse_velocities = 1 / regulated_magnitudes ones = np.ones(M) # Contains the velocities to be streamed at for each time step schedule: List[List[int]] = [] # Accumulated time accumulated_time = 0 # for _ in range(max_iters): # The "ground" covered by each velocity magnitude after this step time_intervals = np.multiply(ones - cfl_counter, inverse_velocities) # The minimum of time_intervals dictates which velocity limits the time step such that in the next step # the associated distribution will reach the next grid point min_time_interval = time_intervals[np.argmin(time_intervals)] # Update the accumulated time accumulated_time += min_time_interval # Update the progress of each velocity cfl_counter += min_time_interval * velocity_magnitudes # Get the indices of the velocities that have reached the next grid point streamed = np.squeeze( np.argwhere(np.isclose(cfl_counter, 1.0, tolerance)), axis=1 ) # Reset the progress of the velocities streamed at this time step cfl_counter[streamed] = 0.0 # Track the controlled velocities schedule.append(streamed.tolist()) n_mag_bits = int(np.ceil(np.log2(M))) if M > 1 else 1 if accumulated_time >= time: break return schedule, accumulated_time ``` # ## Initialization Initializing the model parameters. ```python theme={null} def num_bits(n): """Returns the number of bits required to represent n numbers""" return (n - 1).bit_length() # int(np.ceil(np.log2(n))) # Grid parameters n_g_i_bits = num_bits(GRID_LENGTH) # Velocity magnitudes u = np.unique(np.abs(VELOCITIES)) n_u_i = u.shape[0] n_u_i_bits = num_bits( n_u_i ) # number of bits required to represent the magnitudes in the x and y directions ``` The quantum register is conveniently stored within a QStruct variable. The customized object will allow simple and direct access the different registers. ```python theme={null} class PhaseSpaceStruct(QStruct): """Wrapping class including all the quantum variables""" # Grid variables g_x: QNum[n_g_i_bits] # x-axis grid register g_y: QNum[n_g_i_bits] # y-axis grid register # Velocity directions v_dir_x: QBit v_dir_y: QBit # Velocity magnitudes u_x: QNum[n_u_i_bits] # velocity magnitude in the x direction u_y: QNum[n_u_i_bits] # velocity magnitude in the y direction ``` # ### Initial State Preparation, Setting Parameters We initialize the particles at a lattice point on the left-hand side of the lattice with a uniform velocity distribution in the y-direction and a non-negative uniform distribution in the x-direction. ```python theme={null} # Setting the initial spatial and velocity magnitude probabilities g_dist_x, g_dist_y = [0] * GRID_LENGTH, [0] * GRID_LENGTH g_dist_x[1] = 1 g_dist_y[2] = 1 u_dist = list(np.ones(n_u_i) / n_u_i) ``` State preparation function for the `PhaseSpaceStruct`: ```python theme={null} @qfunc def init_state2D( qs: PhaseSpaceStruct, g_dist_x: CArray[CReal], g_dist_y: CArray[CReal], u_dist: CArray[CReal], ) -> None: """Prepares the initial grid, velocity magnitude, and velocity direction quantum variables""" inplace_prepare_amplitudes(g_dist_x, 0, qs.g_x) inplace_prepare_amplitudes(g_dist_y, 0, qs.g_y) inplace_prepare_amplitudes(u_dist, 0, qs.u_x) inplace_prepare_amplitudes(u_dist, 0, qs.u_y) ## Initialize velocity direction register (v_dir) in an equal super position X(qs.v_dir_x) H(qs.v_dir_y) ``` # ### Initiating Model Parameters and CFL Schedule The model is propagated for `max_iters` time-steps (each containing a streaming and reflection operation), and the reflecting object is placed between the corners of `limits`. ```python theme={null} # time-series schedule schedule, simulation_time = time_series(VELOCITIES, max_iters=NUM_TIMES_STEPS) # set boundary points of the reflection object limits = [X_LOW, X_HIGH, Y_LOW, Y_HIGH] ``` # ### Schematic Representation of the Considered Grid model_scheme.png Figure 2. Schematic representation of the analyzed example. An eight-by-eight grid, with a reflection object located in the center. ```python theme={null} # Visualize which |u| move on each step plt.figure(figsize=(6, 2)) plt.imshow( (pd.Series(schedule).apply(lambda s: np.isin(range(n_u_i), s)).tolist()), aspect="auto", ) plt.xticks(range(len(u)), u) # place ticks at 0..N-1, label them with magnitudes plt.xlabel("Velocity magnitude index") plt.ylabel("Time-step") plt.title("Streaming schedule (CFL-based)") plt.gca().invert_yaxis() cbar = plt.colorbar(label="Streaming prob.") cbar.set_ticks([0, 1]) plt.show() ``` output Figure 3. Streaming schedule. Each yellow cell indicates that the associated velocity magnitude is propagated in that time step. The schedule dictates which velocity magnitudes are propagated at each time step. The time duration of sub-steps is chosen so that distributions do not propagate beyond the neighboring cell per sub-step. This prevents overshoot and reproduces integer grid walks. For example, at time step 3, distributions with absolute velocities of 1,2,3 will stream. # ## Streaming Operator The streaming operation propagates the particles based on the `schedule`, which contains the streamed velocity magnitudes at each time step. Conditioned on whether the velocity magnitude appears in `schedule[t]`, a sequential modular addition operation is performed on the associated grid variable $ |{\mathbf{x}}\rangle |{\mathbf{v}}\rangle \mapsto |{\mathbf{x}+\Delta \mathbf{x}}\rangle|\mathbf{v}\rangle$. The operation manifests a conditional translation of a specific set of positional states. ```python theme={null} ## Stream operator @qfunc def stream(qs: PhaseSpaceStruct, indx: int) -> None: control( qs.u_x == indx, lambda: control( qs.v_dir_x, lambda: inplace_add(1, qs.g_x), lambda: inplace_add(-1, qs.g_x), ), ) control( qs.u_y == indx, lambda: control( qs.v_dir_y, lambda: inplace_add(1, qs.g_y), lambda: inplace_add(-1, qs.g_y), ), ) ``` stream.png Figure 4. Implementation of the streaming operator in Classiq # ## Specular Reflection The boundary reflection operation effectively reflects particles that enter the boundaries. The boundary surfaces are assumed to be orthogonal to the lattice sites, and located right between two lattice sites. Each boundary surface is characterized by the lattice site inside the boundary closest to its surface, `position`, and the direction normal to the surface, `direction`. Two types of reflections can occur: (i) reflections from the corners of the reflecting object, and (ii) reflections from the bulk of its surface. (i) For particles colliding with a corner, the velocity components along both the $x$ and $y$ axes are reversed. (ii) For particles colliding with the flat surface, only the velocity component normal to the surface is reversed, while the tangential component remains unchanged. Such a reflection scheme enables modeling the reflection process without any ancillas. This is achieved by ensuring that particles incident from different directions never scatter into the same outgoing direction. Allowing multiple distinct incoming states to map onto a single outgoing state would correspond to a non-invertible transformation, thereby violating the unitarity condition required by the quantum circuit computation model. A reflection operation is performed unitarily a controlled operations, effectively reversing the appropriate velocity of a particle colliding with the reflection object's boundary. The $y$-component velocities of particles reaching the upper and lower surfaces of the rectangular object and $x$-component, for particles reaching the left and right surfaces. A reflection operation is implemented unitarily through controlled operations, effectively reversing the relevant velocity component of a particle upon collision with the boundary of the reflecting object. Specifically, the $y$-component of velocity is reversed for particles striking the upper or lower surfaces, while the $x$-component is reversed for those hitting the left or right surfaces. As a consequence, the velocity of particles reaching the corners is reversed. reflection_scheme.png Figure 5. Schematic representation of the reflection types. Incoming and reflected particles are indicated by continuous and dashed arrows, respectively. ```python theme={null} @qfunc def flip_velocity( change_pos: QNum, fixed_pos: QNum, change_u: QNum, change_v_dir: QBit, mag: int, arr: CArray[CReal], ) -> None: """flips the velocity of a certain direction "change" for particles in a regime defined by array arr.""" change_low, change_high, fixed_low, fixed_high = arr[0], arr[1], arr[2], arr[3] # if the particle entered the object and is on the lattice points closest the surface, flip the associated velocity control( ((change_pos == change_low) | (change_pos == change_high)) & (fixed_pos >= fixed_low) & (fixed_pos <= fixed_high) & (change_u == mag), lambda: X(change_v_dir), ) @qfunc def reflection(qs: PhaseSpaceStruct, mag: int, limits: CArray[CReal]) -> None: """ Performs a reflection of the particles reaching lattice sites on the boundary Note that such a reflection limits the initial conditions. Parameters: reg: PhaseSpaceStruct, the complete register of the system mag: int, velocity magnitude limits: list (of QArray[CInt]), defines the limits of the reflecting object """ X_LOW, X_HIGH, Y_LOW, Y_HIGH = limits[0], limits[1], limits[2], limits[3] # reverse the velocity # from the top and bottom of the object top_bottom_arr = [Y_LOW, Y_HIGH, X_LOW, X_HIGH] flip_velocity( change_pos=qs.g_y, fixed_pos=qs.g_x, change_u=qs.u_y, change_v_dir=qs.v_dir_y, mag=mag, arr=top_bottom_arr, ) # from the left and right of the object flip_velocity( change_pos=qs.g_x, fixed_pos=qs.g_y, change_u=qs.u_x, change_v_dir=qs.v_dir_x, mag=mag, arr=limits, ) ``` stream.png Figure 6. Implementation of the reflection operator within the main quantum circuit. # ## Building the Quantum Program ```python theme={null} @qfunc def main(qs: Output[PhaseSpaceStruct]) -> None: allocate(qs) # Prepare the initial register init_state2D(qs, g_dist_x, g_dist_y, u_dist) # Number of time steps Nt = len(schedule) # Iterate over time steps for t in range(Nt): # Iterate over magnitudes to stream for mag in schedule[t]: # Stream stream(qs, mag) # Reflect from boundaries reflection(qs, mag, limits) qprog = synthesize(main) job = execute(qprog) ``` # ## Execution and Results We run the quantum program on a statevector simulator to retrieve the full solution. ```python theme={null} with ExecutionSession(qprog) as es: results = es.sample() table = results.dataframe.to_numpy() ``` Taking a look at a small part of the results ```python theme={null} results.dataframe.head() ``` | | qs.g\_x | qs.g\_y | qs.v\_dir\_x | qs.v\_dir\_y | qs.u\_x | qs.u\_y | count | probability | bitstring | | - | ------- | ------- | ------------ | ------------ | ------- | ------- | ----- | ----------- | ------------ | | 0 | 1 | 1 | 1 | 1 | 0 | 3 | 88 | 0.042969 | 110011001001 | | 1 | 6 | 2 | 1 | 1 | 3 | 0 | 77 | 0.037598 | 001111010110 | | 2 | 1 | 2 | 1 | 1 | 0 | 0 | 76 | 0.037109 | 000011010001 | | 3 | 4 | 7 | 0 | 1 | 3 | 3 | 76 | 0.037109 | 111110111100 | | 4 | 1 | 2 | 0 | 1 | 1 | 1 | 74 | 0.036133 | 010110010001 | Evaluating the spatial and velocity magnitude distribution, $p(x)$ and $p(u)$. ```python theme={null} def get_p(dataframe, jx, jy, n): """ Extracts a 2D probability map from the dataframe where columns jx and jy correspond to the x and y indices. Parameters: dataframe: pandas.DataFrame, the table containing measurement results. jx, jy: int, column indices for the x and y registers. n: int, number of discrete x and y states (assumes square grid n x n). Returns: p: np.ndarray, 2D array of summed probabilities p[x, y]. """ p = np.zeros((n, n)) for x in range(n): for y in range(n): # mask the case of interest mask = (dataframe.iloc[:, jx] == x) & (dataframe.iloc[:, jy] == y) p[y, x] = np.sum(dataframe.loc[mask].to_numpy()[:, 7]) return p # 2D distribution p_xy = get_p(results.dataframe, jx=0, jy=1, n=GRID_LENGTH) p_u_xy = get_p(results.dataframe, jx=4, jy=5, n=n_u_i) ``` Propagating the dynamics for nine time time-steps, we obtain the following spatial and velocity magnitude distributions: ```python theme={null} plt.figure(figsize=(5, 5)) plt.imshow( p_xy, cmap="viridis", origin="lower", # make (0,0) appear bottom-left aspect="equal", ) # Add colorbar plt.colorbar(label="Probability", shrink=0.7) # Label axes plt.xlabel("x position (grid site)") plt.ylabel("y position (grid site)") # Set tick positions and labels plt.xticks(range(p_xy.shape[1]), range(p_xy.shape[1])) plt.yticks(range(p_xy.shape[0]), range(p_xy.shape[0])) plt.title("Spatial Distribution") plt.tight_layout() plt.show() ``` output Figure 7. Spatial distribution. Probability at each lattice site. Obtained by summing over all the velocity probabilities at each site. ```python theme={null} plt.imshow( p_u_xy, cmap="viridis", aspect="auto", origin="lower", extent=[u[0], u[-1], u[0], u[-1]], # x_min, x_max, y_min, y_max ) plt.colorbar(label="Probability") # Tick marks exactly at your u values plt.xticks(u, [f"{val:.1f}" for val in u]) plt.yticks(u, [f"{val:.1f}" for val in u]) plt.title("Velocity Magnitude Distribution") plt.xlabel("Velocity Magnitude (x)") plt.ylabel("Velocity Magnitude (y)") plt.tight_layout() plt.show() ``` output Figure 8. Velocity magnitude distribution. Obtained by summing over all the lattice sites for each velocity magnitude. # ## Analysis The simulation starts with particles placed at lattice site $(x_0, y_0) = (1,2)$, and their velocities are distributed uniformly - meaning each possible speed is equally likely at the beginning. We apply periodic boundary conditions, so particles that leave the domain on one side re-enter from the other. In addition, a reflecting obstacle is a rectangle with the corners situated at $(2,1)$, $(2,5)$, $(5,1)$, and $(5,5)$. As the system evolves, particles outside the obstacle move freely, while those reaching the obstacle bounce back. After several time steps, the particles cover the free space surrounding the reflecting object. The velocity-magnitude plot shows that all speeds still have roughly the same probability. This makes sense because no collisions occur in this setup, so the distribution of speeds doesn't change over time, only the direction of the velocities. The small differences between probabilities come only from statistical fluctuations, since we are working with a finite number of samples from the quantum simulation. ## Part III * Technical Background # ## Classical Methods The dynamics of fluids with low compressibility and isothermal conditions are described by the small Mach number limit of the Navier--Stokes equations: $$ \frac{\partial\rho}{\partial t} + \nabla \cdot \rho\mathbf{u} = 0~~,~~\text{(Continuity eq.)} $$ $$ \rho \left( \frac{\partial \mathbf{u}}{\partial t} + \mathbf{u} \cdot \nabla \mathbf{u} \right) = -\nabla p + \mu \nabla^2 \mathbf{u} + \rho \mathbf{F}~~,~~ \text{(Momentum eq.)} $$ where $\mathbf{u}$ is the velocity field, $\rho$ is the fluid density, $p$ is the hydrodynamic pressure, $\mu$ is the dynamic viscosity, and $\mathbf{F}$ represents external body forces. The nonlinear and multiscale nature of these equations imposes substantial computational demands on classical solvers. For discretization-based approaches, the per-time-step computational complexity scales as $O(N_x^d)$ where $N_x$ is the number of grid points per spatial dimension and $d$ is the dimensionality of the system. However, fully resolving all relevant turbulent scales requires a grid resolution that scales unfavorably with the Reynolds number, approximately as $Re^{9/4}$. Consequently, the overall computational cost of direct numerical simulation grows nearly exponentially with $Re$. The Reynolds number is a unitless quantity that measures the relative importance of inertial forces to viscous forces in a fluid flow. Turbulent flows correspond to high Reynolds numbers, where nonlinear interactions between scales dominate the dynamics. A further challenge lies in parallelization: enforcing incompressibility introduces global constraints that couple the velocity field across the entire domain, hindering the scalability of classical Navier-Stokes solvers and preventing fully local computation. # ## Kinetic Formulation An alternative, microscopic viewpoint is provided by the Boltzmann transport equation: $$ \frac{\partial f}{\partial t} + \mathbf{v} \cdot \nabla f + \mathbf{F}\cdot\frac{\partial f}{\partial \mathbf p} = Q(f,f)~~, $$ where $f(\mathbf{x}, \mathbf{v}, t)$ is the single-particle distribution function over phase space, $\mathbf{F}$ represents external forces, and $Q(f,f)$ is the collision operator describing molecular interactions. The collision term involves a high-dimensional integral over all pre- and post-collision velocities and scattering angles, making its evaluation the dominant computational cost. For deterministic discretizations, the complexity per time step scales as $$ O(N_x^d N_v^{2d})~~, $$ where $N_v$ is the number of discrete velocity points per dimension. # ## BGK Approximation A major simplification is obtained by replacing the collision operator with a local relaxation term toward equilibrium: $$ \frac{\partial f}{\partial t} + \mathbf{v} \cdot \nabla f + \mathbf{F} \cdot \frac{\partial f}{\mathbf{\partial p}} = -\Gamma (f - f^{\text{eq}})~~, \tag{1} $$ where $\Gamma$ is the relaxation rate and $f^{\text{eq}}$ is the local Maxwellian equilibrium distribution. This Bhatnagar--Gross--Krook (BGK) approximation replaces the complex integral operator with a local operation that depends only on a few velocity moments. Despite this simplification, direct numerical integration of Eq.\~(1) still requires evolving $f(\mathbf{x}, \mathbf{v}, t)$ across both position and velocity grids, leading to a per-time-step complexity of $$ O(N_x^d N_v^d)~~. $$ # ## Lattice Boltzmann Method (LBM) The (LBM) offers an efficient and scalable alternative. LBM discretizes velocity space into a small set of representative directions, allowing collisions and streaming to be computed locally in time and space. This locality enables excellent parallelization and simplifies the handling of complex geometries and boundary conditions. The resulting per-time-step complexity scales as $$ O(N_x^d V^d)~~, $$ where $V$ is the number of discrete velocity directions per dimension (typically small and fixed). # ## Figure Codes # ### Figure 2 ```python theme={null} import matplotlib.patches as patches def plot_2d_grid_with_obstacle( Lx=4, Ly=4, obs_x_start=X_LOW, obs_x_end=X_HIGH, obs_y_start=Y_LOW, obs_y_end=Y_HIGH ): """ Plot a 2D lattice with a rectangular obstacle. Parameters --------- - Lx, Ly : int Number of lattice sites in x and y directions. obs_x_start, obs_x_end : int Left and right faces (inclusive) of the obstacle region in x. obs_y_start, obs_y_end : int Bottom and top faces (inclusive) of the obstacle region in y. """ # Basic sanity assert 0 <= obs_x_start <= obs_x_end < Lx, "Obstacle x must be within [0, Lx-1]." assert 0 <= obs_y_start <= obs_y_end < Ly, "Obstacle y must be within [0, Ly-1]." # Generate grid coordinates x_sites, y_sites = np.meshgrid(np.arange(Lx), np.arange(Ly)) fig, ax = plt.subplots(figsize=(5, 5)) # Draw lattice sites ax.scatter(x_sites, y_sites, marker="x", s=60, zorder=3, color="k") # Draw obstacle as shaded rectangle rect = patches.Rectangle( (obs_x_start - 0.1, obs_y_start - 0.1), obs_x_end - obs_x_start + 0.2, obs_y_end - obs_y_start + 0.2, linewidth=1, edgecolor="r", facecolor="tab:red", alpha=0.35, label="Obstacle", ) ax.add_patch(rect) # Cosmetics ax.set_xlim(-0.5, Lx - 0.5) ax.set_ylim(-0.5, Ly - 0.5) ax.set_xticks(np.arange(Lx)) ax.set_yticks(np.arange(Ly)) ax.set_aspect("equal") ax.grid(True, which="both", linestyle="--", alpha=0.4) ax.legend(loc="upper right") plt.tight_layout() plt.savefig("model_scheme.png") plt.show() # Example usage: 8x8 grid, obstacle in the range x in (x_low, x_high) y in (y_low, y_high) # plot_2d_grid_with_obstacle(Lx=8, Ly=8, obs_x_start=x_low, obs_x_end=x_high, obs_y_start=y_low, obs_y_end=y_high) ``` # ### Figure 5 ```python theme={null} import math # === Create the figure and axis === fig, ax = plt.subplots() # === Draw the reflecting object (red square) === # The square is centered in the figure with semi-transparent red fill square = plt.Rectangle( (0.25, 0.25), 0.5, 0.5, linewidth=1, edgecolor="r", facecolor="tab:red", alpha=0.35 ) ax.add_patch(square) # === Add first arrow pair (top-left corner reflection) === # Solid arrow: incoming direction (up-right) arrow_start = (0.3, 0.8) arrow_dx, arrow_dy = (0.1, 0.1) ax.arrow( arrow_start[0], arrow_start[1], arrow_dx, arrow_dy, head_width=0.03, head_length=0.05, fc="black", ec="black", ) # Dashed arrow: reflected direction (down-left) arrow2_start = (arrow_start[0] + arrow_dx, arrow_start[1] + arrow_dy + 0.05) arrow2_dx, arrow2_dy = (-0.1, -0.1) ax.arrow( arrow2_start[0], arrow2_start[1], arrow2_dx, arrow2_dy, head_width=0.03, head_length=0.05, fc="none", ec="black", linestyle="--", ) # === Add second arrow pair (upper-left side) === # Solid arrow: incident from top-left toward the object arrow3_end = (0.16, 0.84) arrow3_start = (arrow3_end[0] - 0.1, arrow3_end[1] + 0.1) arrow3_dx = arrow3_end[0] - arrow3_start[0] arrow3_dy = arrow3_end[1] - arrow3_start[1] ax.arrow( arrow3_start[0], arrow3_start[1], arrow3_dx, arrow3_dy, head_width=0.03, head_length=0.05, fc="black", ec="black", ) # Dashed arrow: reflection opposite to incident direction arrow4_start = (arrow3_start[0] + arrow3_dx + 0.01, arrow3_start[1] + arrow3_dy - 0.07) arrow4_dx, arrow4_dy = (-0.1, 0.1) ax.arrow( arrow4_start[0], arrow4_start[1], arrow4_dx, arrow4_dy, head_width=0.03, head_length=0.05, fc="none", ec="black", linestyle="--", ) # === Define helper for rotation (used below) === angle_rad = math.radians(90) def rotate(dx, dy): """Rotate a vector (dx, dy) 90 degrees counterclockwise.""" new_dx = dx * math.cos(angle_rad) - dy * math.sin(angle_rad) new_dy = dx * math.sin(angle_rad) + dy * math.cos(angle_rad) return new_dx, new_dy # === Add third arrow pair (lower-left side, rotated 90° CCW) === arrow5_end = (0.16, 0.64) arrow5_start = (arrow5_end[0] - 0.1, arrow5_end[1] + 0.1) arrow5_dx, arrow5_dy = rotate( arrow5_end[0] - arrow5_start[0], arrow5_end[1] - arrow5_start[1] ) # Solid arrow: incident direction ax.arrow( arrow5_start[0] - 0.03, arrow5_start[1] - 0.155, arrow5_dx, arrow5_dy, head_width=0.03, head_length=0.05, fc="black", ec="black", ) # Dashed arrow: reflection (opposite) arrow6_start = (arrow5_start[0] + arrow5_dx + 0.02, arrow5_start[1] + arrow5_dy - 0.06) arrow6_dx, arrow6_dy = rotate(-0.1, 0.1) ax.arrow( arrow6_start[0], arrow6_start[1] - 0.1, arrow6_dx, arrow6_dy, head_width=0.03, head_length=0.05, fc="none", ec="black", linestyle="--", ) # === Add pair of arrows to bottom surface (center reflection) === # Solid arrow: incoming (upward) arrow7_start = (0.35, 0.06) arrow7_dx, arrow7_dy = (0.1, 0.1) ax.arrow( arrow7_start[0], arrow7_start[1], arrow7_dx, arrow7_dy, head_width=0.03, head_length=0.05, fc="none", ec="black", ) # Dashed arrow: reflected (downward) arrow8_start = (0.52, 0.2) arrow8_dx, arrow8_dy = (0.1, -0.1) ax.arrow( arrow8_start[0], arrow8_start[1], arrow8_dx, arrow8_dy, head_width=0.03, head_length=0.05, fc="none", ec="black", linestyle="--", ) # === Add final vertical pair (top reflection) === # Solid: downward arrow from top arrow9_start = (0.6, 0.93) arrow9_dx, arrow9_dy = (0, -0.1) ax.arrow( arrow9_start[0], arrow9_start[1], arrow9_dx, arrow9_dy, head_width=0.03, head_length=0.05, fc="none", ec="black", ) # Dashed: reflected upward arrow10_start = (0.64, 0.78) arrow10_dx, arrow10_dy = (0, 0.1) ax.arrow( arrow10_start[0], arrow10_start[1], arrow10_dx, arrow10_dy, head_width=0.03, head_length=0.05, fc="none", ec="black", linestyle="--", ) # === Final plot settings === ax.set_xlim(0, 1) ax.set_ylim(0, 1) ax.set_aspect("equal") # Remove all tick marks and labels ax.set_xticks([]) ax.set_yticks([]) ax.set_xticklabels([]) ax.set_yticklabels([]) fig.suptitle("Schematic of particle reflections at object surfaces") # Optionally remove axis spines for a cleaner figure for spine in ax.spines.values(): spine.set_visible(False) plt.savefig("reflection_scheme.png") plt.show() ``` output ## References \[1] [Schalkers, M.A. & Möller. Efficient and fail-safe quantum algorithm for the transport equation. M., Journal of Computational Physics, 502, p.112816](https://www.sciencedirect.com/science/article/pii/S0021999124000652?ref=pdf_download\&fr=RR-2\&rr=984897380e483d7c) \[2] [Budinski, L.. (2021) Quantum algorithm for the advection-diffusion equation simulated with the lattice Boltzmann method. Quantum Information Processing, 20(2), 57.](https://link.springer.com/article/10.1007/s11128-021-02996-3) \[3] [Todorova, B. N., & Steijl, R. (2020). Quantum algorithm for the collisionless Boltzmann equation. Journal of Computational Physics, 409, 109347](https://www.sciencedirect.com/science/article/abs/pii/S0021999120301212) \[4] [Gaitan, Frank. (2020). "Finding flows of a Navier-Stokes fluid through quantum computing." npj Quantum Information 6.1: 61.](https://www.nature.com/articles/s41534-020-00291-0) \[5] [Itani, Wael, and Sauro Succi. (2022). Analysis of Carleman linearization of lattice Boltzmann. Fluids 7.1: 24.](https://arxiv.org/abs/2111.11327) \[6] [Wang, Boyuan, et al., (2025). Quantum lattice Boltzmann method for simulating nonlinear fluid dynamics. arXiv:2502.16568.](https://arxiv.org/abs/2502.16568) \[7] [Kocherla, Sriharsha, et al. (2024). A two-circuit approach to reducing quantum resources for the quantum lattice Boltzmann method. arXiv:2401.12248.](https://arxiv.org/abs/2401.12248) # Quantum Simulation-Based Optimization (QuSO) of a Cooling System Source: https://docs.classiq.io/explore/applications/automotive/cooling_systems_optimization/cooling_systems_optimization Open this notebook in GitHub to run it yourself This notebook shows how to implement the QuSO algorithm for the cooling system problem presented in the paper: [Quantum Simulation-Based Optimization of a Cooling System](https://arxiv.org/abs/2504.15460). ```python theme={null} !pip install -qq "classiq[qsp]" ``` ```python theme={null} import itertools from functools import reduce from operator import mul import numpy as np from scipy.optimize import curve_fit from sympy import simplify, sin, solve, symbols from classiq import * from classiq.applications.qsp import qsp_approximate, qsvt_phases from classiq.qmod.symbolic import log, logical_and ``` ## Matrix Block Encoding As discussed in the paper, we simulate the cooling system cooling_system.png by solving the linear system of equations $$ A(x)\tilde{T}=B, $$ where $A(x)$ is the system matrix depending on the binary values $x_{ij}$, $\tilde{T}$ is the temperature vector, and $B$ is the vector containing the external heat or cooling sources. In order to solve the system above, we need to block encode matrix $A(x)$, which will be shown in the following: ```python theme={null} # define system parameters # Environmental Parameters T_env = 293 # Ambient temperature (K) R_env = 0.001 # Convection resistance to ambient (K/W) # Heat Flows (in Watts) # Positive values indicate heat generation; negative values indicate cooling. Q_1 = 2000 Q_2 = 4000 Q_3 = -200 Q_4 = -2000 # Inter-node Thermal Resistances (in K/W) # These values lump together conduction and convection effects. R_12 = 0.005 R_13 = 0.006 R_14 = 0.006 R_23 = 0.007 R_24 = 0.007 R_34 = 0.008 R_dict = { (0, 1): R_12, (0, 2): R_13, (0, 3): R_14, (1, 2): R_23, (1, 3): R_24, (2, 3): R_34, } connections = [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)] conductance_coeffs = [ 1 / R_12, 1 / R_13, 1 / R_14, 1 / R_23, 1 / R_24, 1 / R_34, 1 / (2 * R_env), 0, ] C_l = np.sum(conductance_coeffs) ** (-1 / 2) conductance_coeffs_amps = np.sqrt(conductance_coeffs) * C_l B = np.array([Q_1, Q_2, Q_3, Q_4]) C_B = np.sum([el**2 for el in B]) ** (-1 / 2) B_amps = C_B * B ``` ```python theme={null} # Classical construction of A(x) def build_A(x): cons = [] for i, x_ij in enumerate(x): if x_ij == 1: cons.append(connections[i]) A = np.zeros((4, 4)) for i in range(4): for j in range(4): if i == j: A[i, j] = 1 / R_env + np.sum( [1 / R_dict[con] if i in con else 0 for con in cons] ) elif i < j: if (i, j) in cons: A[i, j] = -1 / R_dict[(i, j)] elif i > j: if (j, i) in cons: A[i, j] = -1 / R_dict[(j, i)] return A ``` For exampe, if we consider the case where all connections are switched on, we want to encode this matrix: ```python theme={null} classical_matrix = build_A([1, 1, 1, 1, 1, 1]) print(classical_matrix) ``` **Output:** ``` [[1533.33333333 - 200. -166.66666667 -166.66666667] [- 200. 1485.71428571 -142.85714286 -142.85714286] [-166.66666667 -142.85714286 1434.52380952 - 125. ] [-166.66666667 -142.85714286 - 125. 1434.52380952]] ``` Let's do that using our block encoding: ```python theme={null} @qfunc def block_encode_2x2(aux: QNum, data: QArray): """ Returns the 2x2 block encoding: 1-X. """ lcu_pauli(Pauli.I(0) - Pauli.X(0), data, aux) @qfunc def block_encode_2X2_first_qubit(flag: QBit, aux: QBit, data: QArray): """ Returns the 2x2 block encoding in the upper left block of a larger matrix padded with zeros. """ lsb = QBit() msb = QNum(size=data.len - 1) bind(data, [lsb, msb]) flag ^= msb > 0 block_encode_2x2(aux, lsb) bind([lsb, msb], data) @qfunc def block_encode_2X2_arbitrary(i: CInt, j: CInt, flag: QBit, aux: QBit, data: QArray): """ Returns the 2x2 block encoding at arbitrary positions i and j of a larger matrix padded with zeros. """ within_apply( lambda: permute_block(i, j, data), lambda: block_encode_2X2_first_qubit(flag, aux, data), ) @qfunc def permute_block(i: CInt, j: CInt, data: QArray): """ Returns the permutation operation of i->0 and j->1 in the qubit register data. """ def get_bit(number, index): return (number >> index) & 1 # move the i state to the 0 state repeat( data.len, lambda k: if_( get_bit(i, k) == 1, lambda: X(data[k]), lambda: IDENTITY(data[k]) ), ) # # get the 1st index for which j^i is not 0 j_updated = j ^ i highest_nonzero_bit = log(j_updated & ((~j_updated) + 1), 2) # # filp all 1 bits in updated j conditioned on the 1st bit repeat( data.len, lambda k: if_( logical_and(k != highest_nonzero_bit, get_bit(j_updated, k) == 1), lambda: CX(data[highest_nonzero_bit], data[k]), lambda: IDENTITY(data), ), ) # swap the qbit and the 0 qbit if_( highest_nonzero_bit != 0, lambda: SWAP(data[0], data[highest_nonzero_bit]), lambda: IDENTITY(data), ) @qfunc def combine_blocks( pair_list: CArray[CArray[CInt]], lcu_aux: QNum, flag: QBit, aux: QBit, data: QArray, ): """ Returns the block encoding of several 2x2 matrices placed in a larger matrix padded with zeros. """ within_apply( lambda: hadamard_transform(lcu_aux), lambda: repeat( pair_list.len, lambda index: control( lcu_aux == index, lambda: block_encode_2X2_arbitrary( pair_list[index][0], pair_list[index][1], flag, aux, data ), ), ), ) @qfunc def combine_blocks_coeffs( pair_list: CArray[CArray[CInt]], amplitudes: CArray[CReal], lcu_aux: QNum, flag: QBit, aux: QBit, data: QArray, ): """ Returns the block encoding of several 2x2 matrices placed in a larger matrix padded with zeros with particular coefficients . """ within_apply( lambda: inplace_prepare_amplitudes(amplitudes, 0, lcu_aux), lambda: repeat( pair_list.len, lambda index: control( lcu_aux == index, lambda: block_encode_2X2_arbitrary( pair_list[index][0], pair_list[index][1], flag, aux, data ), ), ), ) @qfunc def conditional_single_block( i: CInt, j: CInt, condition_var: QNum, flag: QBit, aux: QBit, data: QArray ): """ Returns the 2x2 block encoding at arbitrary positions i and j of a larger matrix padded with zeros conditioned on condition_var. """ control( ctrl=condition_var == 1, stmt_block=lambda: block_encode_2X2_arbitrary(i, j, flag, aux, data), else_block=lambda: X( flag ), # else set flag to get 0 matrix in case condition_var == 0 ) @qfunc def conditional_combine_blocks( pair_list: CArray[CArray[CInt]], u: QArray, lcu_aux: QNum, flag: QBit, aux: QBit, data: QArray, ): """ Returns the list of operations for the LCU block encoding of several 2x2 matrices placed in a larger matrix padded with zeros conditioned on qubits in u. """ within_apply( lambda: hadamard_transform(lcu_aux), lambda: repeat( pair_list.len, lambda index: control( lcu_aux == index, lambda: conditional_single_block( pair_list[index][0], pair_list[index][1], u[index], flag, aux, data ), ), ), ) @qfunc def conditional_combine_blocks_coeffs( pair_list: CArray[CArray[CInt]], amplitudes: CArray[CReal], u: QArray, lcu_aux: QNum, flag: QBit, aux: QBit, data: QArray, ): """ Returns the list of operations for the LCU block encoding of several 2x2 matrices placed in a larger matrix padded with zeros conditioned on qubits in u and with specific coefficients. """ within_apply( lambda: inplace_prepare_amplitudes(amplitudes, 0, lcu_aux), lambda: repeat( pair_list.len, lambda index: control( lcu_aux == index, lambda: conditional_single_block( pair_list[index][0], pair_list[index][1], u[index], flag, aux, data ), ), ), ) ``` ```python theme={null} lcu_aux_size = int(np.log2(len(conductance_coeffs_amps))) data_size = int(np.log2(len(B_amps))) block_size = lcu_aux_size + 2 # lcu_aux + flag + aux # Helper functions for matrix visualization @qfunc def prepare_ref(num_qubits: CInt, data: Output[QNum], data_ref: Output[QNum]): """ create a refernce variable such that it will 'tag' the input states, and it will be possible to measure the block encoded matrix """ allocate(num_qubits, data) hadamard_transform(data) # 'duplicate' data to the refernce, such that variables are entangled data_ref |= data def standardize_matrix(mat, threshold=1e-10): # normalize by a global phase of the first index mat = mat / np.exp(1j * np.angle(mat[0, 0])) # as we use the reference trick, normalize back to get the matrix coefficients mat *= np.sqrt(mat.shape[0]) # truncate small values real_part = np.real(mat) imag_part = np.imag(mat) real_part[np.abs(real_part) < threshold] = 0 imag_part[np.abs(imag_part) < threshold] = 0 # Reconstruct the complex array with filtered real and imaginary parts return real_part + 1j * imag_part def block_encoding_from_sv(sv, data_size): encoded_matrix_size = 2**data_size A = np.zeros((encoded_matrix_size, encoded_matrix_size), dtype=complex) for _, row in sv.iterrows(): A[int(row["data_ref"]), int(row["data"])] += row["amplitude"] return standardize_matrix(A) @qfunc def main( block: Output[QNum], data: Output[QNum], data_ref: Output[QNum], ): allocate(block_size, block) lcu_aux = QNum() flag = QBit() aux = QBit() bind(block, [lcu_aux, flag, aux]) prepare_ref(data_size, data, data_ref) combine_blocks_coeffs( pair_list=connections, amplitudes=conductance_coeffs_amps, lcu_aux=lcu_aux, flag=flag, aux=aux, data=data, ) bind([lcu_aux, flag, aux], block) ``` ```python theme={null} qprog_block_encoding = synthesize( main, preferences=Preferences(transpilation_option="none", timeout_seconds=14400), ) show(qprog_block_encoding) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3GBmYTLRhZVaTK6zYkkyCaobwCf ``` ```python theme={null} sv = calculate_state_vector(qprog_block_encoding, filters={"block": 0}) A = block_encoding_from_sv(sv, data_size) block_encoded_matrix = A * 2 / (C_l**2) print(block_encoded_matrix) ``` **Output:** ``` Submitting state-vector job to simulator ``` **Output:** ``` [[1533.33333333+0.j - 200. +0.j -166.66666667+0.j -166.66666667+0.j] [- 200. +0.j 1485.71428571+0.j -142.85714286+0.j -142.85714286+0.j] [-166.66666667+0.j -142.85714286+0.j 1434.52380952+0.j - 125. +0.j] [-166.66666667+0.j -142.85714286+0.j - 125. +0.j 1434.52380952+0.j]] ``` ```python theme={null} assert np.allclose(np.real(block_encoded_matrix), np.real(classical_matrix), atol=1e-2) ``` We have successfully block-encoded the matrix from above! ## QSVT for Matrix Inversion and Solving Our Linear System Next, we use our block encoding to solve our linear system (cf. above). ```python theme={null} # compute phase angles for the 1/x polynomial def getOneOverXPhases(epsilon=0.05, kappa=5): degree = int(kappa * np.log(kappa / epsilon)) # In case we provide an even degree and ask for an odd polynimial if degree % 2 == 0: degree += 1 SCALE = 0.9 def target_function(x): return SCALE * 1 / (kappa * x) pcoefs, max_err = qsp_approximate( target_function, degree=degree, parity=1, interval=[1 / kappa, 1], plot=True ) print(f"Degree: {degree}, Max error: {max_err}") inversion_phases = qsvt_phases(pcoefs) return inversion_phases, SCALE / kappa phases, C_p = getOneOverXPhases(epsilon=0.1, kappa=3) ``` output **Output:** ``` Degree: 11, Max error: 0.01836914878244511 ``` ```python theme={null} # Implementation of the QSVT-based Linear System Solver class Block(QStruct): lcu_aux: QNum[lcu_aux_size] flag: QBit aux: QBit class BlockEncodedState(QStruct): block: Block data: QNum[data_size] class QsvtState(QStruct): qsvt_aux: QBit qsvt_real_aux: QBit state: BlockEncodedState @qfunc def identify_block(state: Const[BlockEncodedState], block_zero_qbit: QBit): block_qubits = QNum(size=state.block.size) data = QArray(length=state.data.size) bind(state, [block_qubits, data]) block_zero_qbit ^= block_qubits == 0 bind([block_qubits, data], state) @qfunc def qsvt_solve_system( b_amps: CArray[CReal], block_encoding: QCallable[QArray], phases: list[float], qsvt_state: QsvtState, ) -> None: """ QSVT implementation to solve a linear system Ax=b. """ # Prepare b as a quantum state in amplitude encoding. inplace_prepare_amplitudes(b_amps, 0, qsvt_state.state.data) hadamard_transform(qsvt_state.qsvt_real_aux) control( qsvt_state.qsvt_real_aux == 0, lambda: qsvt_inversion( phase_seq=phases, block_encoding_cnot=lambda block_zero_qbit: identify_block( qsvt_state.state, block_zero_qbit ), u=lambda: block_encoding(qsvt_state.state), aux=qsvt_state.qsvt_aux, ), lambda: invert( lambda: qsvt_inversion( phase_seq=phases, block_encoding_cnot=lambda block_zero_qbit: identify_block( qsvt_state.state, block_zero_qbit ), u=lambda: block_encoding(qsvt_state.state), aux=qsvt_state.qsvt_aux, ) ), ) hadamard_transform(qsvt_state.qsvt_real_aux) ``` ```python theme={null} # Demonstration of the QSVT-based Linear System Solver @qfunc def block_encoding_demo( pair_list: CArray[CArray[CInt]], amplitudes: CArray[CReal], state: BlockEncodedState ): lcu_aux = state.block.lcu_aux flag = state.block.flag aux = state.block.aux data = state.data combine_blocks_coeffs( pair_list=pair_list, amplitudes=amplitudes, lcu_aux=lcu_aux, flag=flag, aux=aux, data=data, ) @qfunc def conditional_block_encoding( pair_list: CArray[CArray[CInt]], amplitudes: CArray[CReal], u: QArray, state: BlockEncodedState, ): lcu_aux = state.block.lcu_aux flag = state.block.flag aux = state.block.aux data = state.data conditional_combine_blocks_coeffs( pair_list=pair_list, amplitudes=amplitudes, u=u, lcu_aux=lcu_aux, flag=flag, aux=aux, data=data, ) @qfunc def main( qsvt_aux: Output[QBit], qsvt_real_aux: Output[QBit], block: Output[QNum], data: Output[QNum], ): qsvt_state = QsvtState() allocate(qsvt_state) qsvt_solve_system( b_amps=B_amps, block_encoding=lambda q_var: block_encoding_demo( pair_list=connections, amplitudes=conductance_coeffs_amps, state=q_var ), phases=phases, qsvt_state=qsvt_state, ) state = BlockEncodedState() bind(qsvt_state, [qsvt_aux, qsvt_real_aux, state]) block_struct = Block() bind(state, [block_struct, data]) bind(block_struct, block) ``` # ## Synthesize Circuit ```python theme={null} qprog_matrix_inverse = synthesize( main, preferences=Preferences( transpilation_option="none", timeout_seconds=14400, optimization_level=1 ), ) show(qprog_matrix_inverse) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3GBmgIQr1R7m3koVskFqaXCDxli ``` # ## Execute Circuit ```python theme={null} sv = calculate_state_vector(qprog_matrix_inverse, filters={"qsvt_aux": 0, "block": 0}) T_tilde = np.zeros(4, dtype=complex) for _, row in sv.iterrows(): if abs(row["amplitude"]) > 1e-5: T_tilde[int(row["data"])] = abs(row["amplitude"]) ``` **Output:** ``` Submitting state-vector job to simulator ``` ```python theme={null} qsvt_solution = np.abs(T_tilde * C_l**2 / (2 * C_p * C_B)) classical_solution = np.abs(np.linalg.solve(build_A([1, 1, 1, 1, 1, 1]), B)) print("QSVT solution:", qsvt_solution) print("Classical solution:", classical_solution) ``` **Output:** ``` QSVT solution: [1.57739789 2.77684905 0.27102853 0.84553989] Classical solution: [1.60481928 2.84578313 0.25179803 0.90240044] ``` As the QSVT result agrees with the classical solution of our linear system up to an acceptable error, we have successfully solved the linear system using QSVT. ## QuSO Algorithm Only a few operations are missing to build the QuSO algorithm. In the following, we will implement the Quantum Amplitude Estimation (QAE) and Quantum Phase Application (QPA) routine to embed all operations in a QuSO circuit. ```python theme={null} def polynomial_coefficients_from_function(func, num_bits): MIN_COEFF = 10e-7 def prod(iterable): return reduce(mul, iterable, 1) lookup_table = { tuple(map(int, format(x, f"0{num_bits}b"))): func(x) for x in range(2**num_bits) } x_symbols = symbols(f"x0:{num_bits}") a_symbols = symbols(f"a0:{2 ** num_bits}") # Construct the polynomial expression polynomial_expr = sum( a_symbols[i] * prod((x_symbols[j]) ** int(bit) for j, bit in enumerate(f"{i:0{num_bits}b}")) for i in range(2**num_bits) ) # Setup equations based on the lookup table equations = [ polynomial_expr.subs(dict(zip(x_symbols, k))) - v for k, v in lookup_table.items() ] # Solve for coefficients solved_coeffs = solve(equations, a_symbols) # Clean small coefficients solved_coeffs = { var: float(coeff) if abs(coeff) > MIN_COEFF else 0 for var, coeff in solved_coeffs.items() } # Substitute the solved coefficients back into the polynomial expression polynomial_with_coeffs = polynomial_expr.subs(solved_coeffs) print("Polynomial Expression:", polynomial_with_coeffs) # Create substitution dictionary for 1 - x_i/2 substitution_dict = {x: (1 - x) / 2 for x in x_symbols} # Apply the substitution to the polynomial expression polynomial_with_substitution = polynomial_with_coeffs.subs(substitution_dict) # Simplify the polynomial expression after substitution simplified_polynomial = simplify(polynomial_with_substitution) # Extract coefficients after substitution substituted_coefficients_dict = {} for i in range(2**num_bits): binary_tuple = tuple(int(bit) for bit in format(i, f"0{num_bits}b")) term = prod(x_symbols[j] ** int(bit) for j, bit in enumerate(binary_tuple)) coefficient = simplified_polynomial.as_coefficients_dict().get(term, 0) substituted_coefficients_dict[binary_tuple] = ( float(coefficient) if abs(coefficient) > MIN_COEFF else 0 ) # Handle the constant term constant_term = simplified_polynomial.as_coefficients_dict().get(1, 0) if abs(constant_term) > MIN_COEFF: substituted_coefficients_dict[(0,) * num_bits] = float(constant_term) return substituted_coefficients_dict, simplified_polynomial def polynomial_to_expression(coefficients_dict: dict, qae_phase_reg: QArray): # Create the polynomial expression polynomial_expr = 0.0 for var_list, coeff in coefficients_dict.items(): term = coeff for i, p in enumerate(var_list): if p: # skip the multiply when p == 0 term *= qae_phase_reg[i] polynomial_expr += term return polynomial_expr def get_func_polynomial_expression(func, c, precision, qae_phase_reg): # Compute the coefficients and polynomial coefficients_dict, polynomial = polynomial_coefficients_from_function( func, precision ) # can be done exact with polynomial of the same order of the function return c * polynomial_to_expression(coefficients_dict, qae_phase_reg) ``` We take 5 QPE phase qubits `qpe_phase_size`. ```python theme={null} # define a simple QAOA model qpe_phase_size = 5 num_layers = 1 qaoa_reg_size = 6 func = lambda x: sin(np.pi * x / (2**qpe_phase_size)) class ProblemRegs(QStruct): qaoa_vars: QArray[qaoa_reg_size] phase_estimate: QNum[qpe_phase_size, UNSIGNED, qpe_phase_size] @qfunc def my_qsvt_solve_system(u: QArray, qsvt_state: QArray): qsvt_solve_system( b_amps=B_amps, block_encoding=lambda q_var: conditional_block_encoding( pair_list=connections, amplitudes=conductance_coeffs_amps, u=u, state=q_var ), phases=phases, qsvt_state=qsvt_state, ) # QPA implementation @qfunc def phase_application(qae_phase: QArray, gamma: CReal): phase( get_func_polynomial_expression(func, 1, qpe_phase_size, qae_phase), gamma, ) @qfunc def qaoa_system_cost( qaoa_vars: QArray, phase_reg: QNum, qsvt_state: QsvtState, gamma: CReal, ) -> None: within_apply( within=lambda: amplitude_estimation( oracle=lambda x: reflect_about_zero(x), space_transform=lambda y: my_qsvt_solve_system(qaoa_vars, y), phase=phase_reg, packed_vars=qsvt_state, ), apply=lambda: phase_application(qae_phase=phase_reg, gamma=gamma), ) @qfunc def qaoa_estimate_cost( qaoa_vars: QArray, phase_reg: QNum, qsvt_state: QsvtState, ) -> None: amplitude_estimation( oracle=lambda x: reflect_about_zero(x), space_transform=lambda y: my_qsvt_solve_system(qaoa_vars, y), phase=phase_reg, packed_vars=qsvt_state, ) @qfunc def qaoa_mixer(reg: QArray, beta: CReal) -> None: repeat( count=reg.len, iteration=lambda index: RX(theta=beta, target=reg[index]), ) @qfunc def main(params: CArray[CReal, num_layers * 2], regs: Output[ProblemRegs]) -> None: # Allocate QAOA register allocate(regs) hadamard_transform(regs.qaoa_vars) # Allocate QSVT registers qsvt_state = QsvtState() allocate(qsvt_state) # QAOA Layers repeat( num_layers, lambda i: ( qaoa_system_cost( qaoa_vars=regs.qaoa_vars, phase_reg=regs.phase_estimate, qsvt_state=qsvt_state, gamma=params[i], ), qaoa_mixer(regs.qaoa_vars, beta=params[num_layers + i]), ), ) # Estimate cost qaoa_estimate_cost( qaoa_vars=regs.qaoa_vars, phase_reg=regs.phase_estimate, qsvt_state=qsvt_state, ) drop(qsvt_state) ``` # ## The circuit becomes very big. Thus, we do not synthesize the circuit in this demo-notebook because it takes a lot of time. ```python theme={null} # The full QuSO circuit is very large; synthesis is skipped in this demo. # qprog = synthesize(main) # show(qprog) ``` As explained in the paper, we can still simulate how QuSO would perform by preparing the state after QAE instead of running QAE. In this example, we synthesize the circuit with one layer of QAOA and 1 phase qubit `n_phase_qubits` to shoten the synthesis time. You can increase `n_phase_qubits` to 5 and get better results. ```python theme={null} # load precomputed cost values costs = np.load("data/costs.npy") num_connections = 6 n_phase_qubits = 1 num_layers = 1 # classical probability function after Quantum Phase Estimation def qpe_probability_function(y, theta, M): y = np.array(y, dtype=np.float64) numerator = 1 - np.cos(2 * np.pi * (y - theta * M)) denominator = 1 - np.cos((2 * np.pi / M) * (y - theta * M)) # When denominator is close to zero, use the limiting value (which gives a ratio of M^2). ratio = np.where(np.abs(denominator) < 1e-8, M**2, numerator / denominator) P = ratio / (M**2) return P # classical probability function after QAE def qae_probability_function(y, theta, M): term1 = qpe_probability_function(y, theta, M) term2 = qpe_probability_function(y, -theta, M) return 0.5 * (term1 + term2) # Function to estimate theta using QAE using curve fitting # Further details: https://arxiv.org/abs/2409.15752v1 def estimate_theta_qae(probabilities): probabilities = np.array(probabilities, dtype=np.float64) M = len(probabilities) # Number of states (M = 2^n). y_vals = np.arange(M) max_index = np.argmax(probabilities) initial_guess = max_index / M # Define narrow bounds around the initial guess. lower_bound = max(0, initial_guess - 0.5 / M) upper_bound = min(1, initial_guess + 0.5 / M) bounds = ([lower_bound], [upper_bound]) # Define the model function with theta as the only free parameter. def model(y, theta): return qae_probability_function(y, theta, M) # Attempt two fits (starting at the lower and upper bounds) to help avoid local minima. try: popt1, pcov1 = curve_fit( model, y_vals, probabilities, p0=[lower_bound], bounds=bounds ) except Exception as e: popt1, pcov1 = (np.array([np.nan]), np.array([[np.inf]])) try: popt2, pcov2 = curve_fit( model, y_vals, probabilities, p0=[upper_bound], bounds=bounds ) except Exception as e: popt2, pcov2 = (np.array([np.nan]), np.array([[np.inf]])) var1 = pcov1[0, 0] if np.isfinite(pcov1[0, 0]) else np.inf var2 = pcov2[0, 0] if np.isfinite(pcov2[0, 0]) else np.inf if var1 < var2: theta_est = popt1[0] else: theta_est = popt2[0] return theta_est # classical function to compute the amplitudes of the QAE for some amplitude a and n_phase phase qubits def qae_full_amplitudes(a, n_phase): M = 2**n_phase theta = np.arcsin(a) / np.pi theta_prime = np.arcsin(a) c_plus = (np.sin(theta_prime) - 1j * np.cos(theta_prime)) / np.sqrt(2) c_minus = (np.sin(theta_prime) + 1j * np.cos(theta_prime)) / np.sqrt(2) def A_QPE(y, phi): k = np.arange(M) return np.sum(np.exp(-2j * np.pi * k * (y / M - phi))) / M A_plus = np.array([A_QPE(y, theta) for y in range(M)]) A_minus = np.array([A_QPE(y, -theta) for y in range(M)]) amp_good = (c_plus * A_plus + c_minus * A_minus) / np.sqrt(2) amp_bad = (1j * c_plus * A_plus - 1j * c_minus * A_minus) / np.sqrt(2) full_state = np.zeros(2 * M, dtype=np.complex128) for y in range(M): full_state[y * 2 + 0] = amp_bad[y] full_state[y * 2 + 1] = amp_good[y] return np.round(full_state, decimals=10) # computing QAE amplitudes for all configurations in configs def compute_qae_amplitudes(configs, amplitudes, n_phase): qae_amplitudes = [] for i, config in enumerate(configs): qae_amplitudes.append(qae_full_amplitudes(amplitudes[i], n_phase)) return qae_amplitudes # computing all configs all_configs = [list(bits) for bits in itertools.product([0, 1], repeat=6)] qae_amplitudes = compute_qae_amplitudes(all_configs, costs, n_phase_qubits) qae_amplitudes_magnitudes = np.array([np.abs(amps) for amps in qae_amplitudes]) qae_amplitudes_phases = np.array([np.angle(amps) + 0.0 for amps in qae_amplitudes]) @qfunc def mixer_layer(reg: QArray, beta: CReal) -> None: repeat( count=reg.len, iteration=lambda index: RX(theta=-2 * beta, target=reg[index]), ) @qperm def phase_application(qae_phase: Const[QArray], gamma: CReal): phase( get_func_polynomial_expression(func, 1.0, n_phase_qubits, qae_phase), gamma, ) @qperm(disable_perm_check=True) def dummy_qae( magnitudes_list: list[list[float]], phases_list: list[list[float]], qae_phase: QArray, reg: Const[QNum], dummy_data: Output[QBit], ): full_qae = QArray() allocate(dummy_data) bind([qae_phase, dummy_data], full_qae) for index in range(len(all_configs)): control( reg == index, lambda: inplace_prepare_complex_amplitudes( magnitudes_list[index], phases_list[index], full_qae ), ) bind(full_qae, [qae_phase, dummy_data]) @qfunc def dummy_cost_layer(qae_phase: QArray, reg: QArray, gamma: CReal) -> None: dummy_data = QBit() within_apply( lambda: dummy_qae( qae_amplitudes_magnitudes, qae_amplitudes_phases, qae_phase, reg, dummy_data ), lambda: phase_application(qae_phase, gamma), ) @qfunc def qaoa_layer(qae_phase: QArray, reg: QArray, gamma: CReal, beta: CReal) -> None: dummy_cost_layer(qae_phase, reg, gamma) mixer_layer(reg, beta) @qfunc def qaoa_circuit(gammas: CArray[CReal], betas: CArray[CReal], reg: QArray): qae_phase = QArray() allocate(n_phase_qubits, qae_phase) hadamard_transform(reg) for i in range(gammas.len): qaoa_layer(qae_phase, reg, gammas[i], betas[i]) drop(qae_phase) ``` # ### Synthesizing the Variational Circuit ```python theme={null} print( f"Dummy QuSO for {n_phase_qubits} phase qubits and a number of layers {num_layers}:" ) @qfunc def main( gammas: CArray[CReal, num_layers], betas: CArray[CReal, num_layers], reg: Output[QArray], ) -> None: allocate(num_connections, reg) qaoa_circuit(gammas, betas, reg) ``` **Output:** ``` Dummy QuSO for 1 phase qubits and a number of layers 1: ``` ```python theme={null} qprog_dummy = synthesize( main, preferences=Preferences( transpilation_option="none", timeout_seconds=14400, optimization_level=1 ), ) show(qprog_dummy) ``` **Output:** ``` Polynomial Expression: 0.0980171403295606*x0 Quantum program link: https://platform.classiq.io/circuit/3GBn4y5lx9pXytSAxVSYQnGnlML ``` By using more phase qubits and optimizing on the gamma and betas parameters, you can find the optimal solution with high probability. # Quantum Volume Source: https://docs.classiq.io/explore/applications/benchmarking/quantum_volume/quantum_volume Open this notebook in GitHub to run it yourself Quantum volume is a measurement of the errors characterizing a chosen quantum hardware. The quantum volume is a result of running a circuit based on principles of randomness and statistical analysis, which provides a single number to compare different hardware backends. The scheme of the quantum volume \[[1](#circuit)]: 1. For a number of qubits $n$, a circuit is made of the $n$ quantum layer. 2. Each layer consists of a unitary operation between pairs of $n$ qubits. The pairs are chosen at random. If $n$ is odd, one of them does not have an operation. 1. The unitary operation between each pair is the Haar random matrix; i.e., an SU(4) operation containing a random complex number in such a manner that the probability of measuring a quantum state is kept with uniform distribution. 2. A single circuit of $n$ qubits is measured and the heavy output probability (i.e., the probability of measuring the states above the median value) is calculated. Due to the nature of the distribution of random complex numbers, one can evaluate that for an ideal case (no noises), the heavy output probability should be \~ 0. 8 5. For an assessment of the quantum volume, the demand subsides to the following inequality: (1) $P_{\rm heavy\_ outputs} \leq 2/3.$ 6. For a given output, to get the quantum volume, repeat Items 1-4 for an increasing number of qubits until the inequality described in Item 4 does not hold. To ensure it, the circuits are created many times and the average and standard deviation are taken into account. 7. The quantum volume is two to the power of the number of qubits, such that they pass inequality (1) as per the procedure described in Items 1- 8. The heavy output probability is a good measurement of the quality of the circuit, as noise reduces the probabilities of uniform distribution. While this is so, consider that there are many components to the results of the procedure - not only the hardware noises, but also the connectivity map, the quality of the transpilation, and even the quantum software that translates the circuit into basis gates for the hardware, thus contributing to the circuit depth. This demonstration shows the code for implementing the steps to calculate the quantum volume using the Classiq platform and an example of such calculations for several quantum simulators and hardware backends. ## Step 1: Create a Haar Random Unitary Matrix Create a function, generating a (n,n) sized Haar random unitary matrix \[[2](#unitary)]. This matrix contains a random complex number that is distributed evenly in the $2^n$ space of quantum states. The Haar distribution indicates how to weight the elements of $U(N)$ such that uniform distribution occurs in the parameter space. ```python theme={null} import random import numpy as np from numpy.linalg import qr from scipy.stats import unitary_group random.seed(0) np.random.seed(0) def haar(n): u1 = unitary_group.rvs(n) u2 = unitary_group.rvs(n) Z = u1 + 1j * u2 Q, R = qr(Z) Lambda = np.diag([R[i, i] / np.abs(R[i, i]) for i in range(n)]) return np.dot(Q, Lambda) ``` ## Step 2: Create a Quantum Volume Circuit The `qv_model` function creates the quantum volume model for a given $N$ number of qubits. For $N$ qubits, the circuit must include $N$ quantum volume layers. The layers are built using the `qv_layer` function, which creates random pairing between the $N$ qubits. (For an odd number, a randomly chosen qubit is not operational.) Between each pair, a unitary gate operates, consisting of a Haar random unitary matrix of size 4. ```python theme={null} import math import random from classiq import * @qfunc def qv_layer(N: int, target: QArray): # Step 1: start with a shuffle of the qubits qubit_list = list(range(N)) random.shuffle(qubit_list) for idx in range(math.floor(N / 2)): # Step 2: Isolate the qubit pairs for the layers a = qubit_list[idx] b = qubit_list[math.floor(N / 2) + idx] # Step 3: Generate the random matrix (this needs to change for the random matrix when possible) gate_matrix = haar(4).tolist() unitary(gate_matrix, [target[a], target[b]]) def qv_model(N): @qfunc def main(target: Output[QArray]): allocate(N, target) repeat(N, lambda _: qv_layer(N, target)) return main ``` ## Step 3: Execute and Analyze The execution and analysis part consists of these functions: * `execute_qv` sends a quantum program for execution on a given quantum hardware with a specified number of shots. The function returns the results of the execution from the hardware. * `heavy_outputs_prob` analyzes the results from execution and returns the heavy output probability; i.e., the probability for a single state in the space to be greater than the median value (median = "middle" of a sorted list of numbers). The `round_significant` function rounds a number for one significant figure. ```python theme={null} def execute_qv(qprog, num_shots, preferences): execution_prefs = ExecutionPreferences( num_shots=num_shots, backend_preferences=preferences ) with ExecutionSession(qprog, execution_prefs) as es: res = es.sample() return res ``` ```python theme={null} def heavy_outputs_prob(results): d = list(results.counts.values()) med = np.median(d) heavy_outputs_prob = 0 # print(med) for count, item in enumerate(d): if item >= med: heavy_outputs_prob = heavy_outputs_prob + item return heavy_outputs_prob ``` ```python theme={null} from math import floor, log10 def round_significant(x): return round(x, -int(floor(log10(abs(x))))) ``` ## Step 4: Find the Quantum Volume Algorithm Using the previously defined functions, `find_qv` finds the quantum volume value for defined parameters including hardware definitions. The `find_qv` function sends the value of heavy output probability for each number of qubits defined (between `min_qubit` and `max_qubits`). This repeats `num_trials` times. Then, the heavy output probability is averaged, and the standard deviation is calculated. If the number of qubits chosen for the circuit is less than the number of qubits in the chosen hardware, the qubits are randomly picked to run according to the rules of the hardware provider. The quantum volume qubits number is defined as the larger number of qubits for which the heavy output probability, decreased by two sigma (twice the standard deviation), is greater than or equal to 2/ 3. The quantum volume is two to the power of the number of quantum volume qubits. Note that if the result given for the log2 of the quantum volume is the same as the chosen `max_qubits`, there is a possibility that the quantum volume is greater than found by the function. In this case, run the program for a greater span. ```python theme={null} from tqdm import tqdm # For testing, we save all qprogs generated in the notebook qprogs = [] def find_qv(num_trials, num_shots, min_qubits, max_qubits, preferences): ### initialization qubit_num = range(min_qubits, max_qubits + 1) heavy_list = np.zeros(max_qubits - min_qubits + 1) std_list = np.zeros(max_qubits - min_qubits + 1) qubit_v = 0 ### calculate the heavy outputs for each number of qubits for num in tqdm(qubit_num): heavy_outputs = 0 std = 0 heavytostd = np.zeros(num_trials) for idx in tqdm(range(num_trials)): qprog = synthesize(qv_model(num)) qprogs.append(qprog) results = execute_qv(qprog, num_shots, preferences) heavy_temp = heavy_outputs_prob(results) heavy_outputs = heavy_outputs + heavy_temp heavytostd[idx] = heavy_temp s = num - min_qubits heavy_list[s] = heavy_outputs / (num_trials * num_shots) temp_hl = heavy_outputs / (num_trials * num_shots) std = np.std(heavytostd) / (num_trials * num_shots) std_list[s] = std temp_std = round_significant(std) print( f"for {num} qubits the heavy outputs probability is: {temp_hl} with {temp_std} standard deviation" ) ### determine the quantum volume for num in qubit_num: s = num - min_qubits heavy_is = heavy_list[s] - 2 * (std_list[s]) if heavy_is >= 2 / 3: qubit_v = num else: break qv = 2**qubit_v print(f" ##### The quantum volume is {qv} #####") return qv ``` ## Examples Run the code to find the quantum volume of several quantum simulators and hardware backends. # ## Running with the Classiq Simulator ```python theme={null} num_trials = 10 # number of times to run the QV circuit for each number of qubits. Best: 200 or more num_shots = 100 # number of runs for each execution. Best: 1000 or more preferences = ClassiqBackendPreferences( backend_name=ClassiqSimulatorBackendNames.SIMULATOR ) min_qubits = 3 max_qubits = 6 qv = find_qv(num_trials, num_shots, min_qubits, max_qubits, preferences) ``` **Output:** ``` 0%| | 0/4 [00:00 **Output:** ``` for 3 qubits the heavy outputs probability is: 0.872 with 0.006 standard deviation ``` **Output:** ``` 0%| | 0/10 [00:00 **Output:** ``` for 4 qubits the heavy outputs probability is: 0.871 with 0.007 standard deviation ``` **Output:** ``` 0%| | 0/10 [00:00 **Output:** ``` for 5 qubits the heavy outputs probability is: 0.906 with 0.002 standard deviation ``` **Output:** ``` 0%| | 0/10 [00:00 **Output:** ``` for 6 qubits the heavy outputs probability is: 0.903 with 0.004 standard deviation ##### The quantum volume is 64 ##### ``` **Output:** ``` 40%|████ | 4/10 [00:15<00:23, 3.93s/it] ``` **Output:** ```  ``` **Output:** ``` 50%|█████ | 5/10 [00:19<00:20, 4.19s/it] ``` **Output:** ```  ``` **Output:** ``` 60%|██████ | 6/10 [00:23<00:16, 4.02s/it] ``` **Output:** ```  ``` **Output:** ``` 70%|███████ | 7/10 [00:27<00:11, 3.94s/it] ``` **Output:** ```  ``` **Output:** ``` 80%|████████ | 8/10 [00:32<00:08, 4.19s/it] ``` **Output:** ```  ``` **Output:** ``` 90%|█████████ | 9/10 [00:35<00:03, 4.00s/it] ``` **Output:** ```  ``` **Output:** ``` 100%|██████████| 10/10 [00:39<00:00, 3.85s/it] ``` **Output:** ```  ``` **Output:** ``` 100%|██████████| 10/10 [00:39<00:00, 3.91s/it] ``` **Output:** ``` 50%|█████ | 2/4 [01:14<01:15, 37.60s/it] ``` **Output:** ``` for 4 qubits the heavy outputs probability is: 0.846 with 0.004 standard deviation ``` **Output:** ``` 0%| | 0/10 [00:00 **Output:** ```  ``` **Output:** ``` 10%|█ | 1/10 [00:04<00:42, 4.72s/it] ``` **Output:** ```  ``` **Output:** ``` 20%|██ | 2/10 [00:08<00:33, 4.13s/it] ``` **Output:** ```  ``` **Output:** ``` 30%|███ | 3/10 [00:12<00:27, 3.91s/it] ``` **Output:** ```  ``` **Output:** ``` 40%|████ | 4/10 [00:15<00:22, 3.82s/it] ``` **Output:** ```  ``` **Output:** ``` 50%|█████ | 5/10 [00:20<00:20, 4.09s/it] ``` **Output:** ```  ``` **Output:** ``` 60%|██████ | 6/10 [00:25<00:17, 4.31s/it] ``` **Output:** ```  ``` **Output:** ``` 70%|███████ | 7/10 [00:28<00:12, 4.12s/it] ``` **Output:** ```  ``` **Output:** ``` 80%|████████ | 8/10 [00:33<00:08, 4.28s/it] ``` **Output:** ```  ``` **Output:** ``` 90%|█████████ | 9/10 [00:38<00:04, 4.39s/it] ``` **Output:** ```  ``` **Output:** ``` 100%|██████████| 10/10 [00:41<00:00, 4.21s/it] ``` **Output:** ```  ``` **Output:** ``` 100%|██████████| 10/10 [00:41<00:00, 4.18s/it] ``` **Output:** ``` 75%|███████▌ | 3/4 [01:56<00:39, 39.54s/it] ``` **Output:** ``` for 5 qubits the heavy outputs probability is: 0.835 with 0.005 standard deviation ``` **Output:** ``` 0%| | 0/10 [00:00 **Output:** ```  ``` **Output:** ``` 10%|█ | 1/10 [00:04<00:44, 4.90s/it] ``` **Output:** ```  ``` **Output:** ``` 20%|██ | 2/10 [00:09<00:38, 4.84s/it] ``` **Output:** ```  ``` **Output:** ``` 30%|███ | 3/10 [00:14<00:33, 4.85s/it] ``` **Output:** ```  ``` **Output:** ``` 40%|████ | 4/10 [00:19<00:28, 4.80s/it] ``` **Output:** ```  ``` **Output:** ``` 50%|█████ | 5/10 [00:23<00:23, 4.76s/it] ``` **Output:** ```  ``` **Output:** ``` 60%|██████ | 6/10 [00:28<00:19, 4.77s/it] ``` **Output:** ```  ``` **Output:** ``` 70%|███████ | 7/10 [00:33<00:14, 4.76s/it] ``` **Output:** ```  ``` **Output:** ``` 80%|████████ | 8/10 [00:38<00:09, 4.96s/it] ``` **Output:** ```  ``` **Output:** ``` 90%|█████████ | 9/10 [00:43<00:04, 4.89s/it] ``` **Output:** ```  ``` **Output:** ``` 100%|██████████| 10/10 [00:48<00:00, 4.78s/it] ``` **Output:** ```  ``` **Output:** ``` 100%|██████████| 10/10 [00:48<00:00, 4.82s/it] ``` **Output:** ``` 100%|██████████| 4/4 [02:44<00:00, 42.95s/it] ``` **Output:** ``` 100%|██████████| 4/4 [02:44<00:00, 41.14s/it] ``` **Output:** ``` for 6 qubits the heavy outputs probability is: 0.861 with 0.006 standard deviation ##### The quantum volume is 64 ##### ``` Since this is a simulator with no errors, we expect the heavy output probability for any number of qubits to be approximately 0. 85. # ## Running with Rigetti Aspen M-3 ```python theme={null} num_trials = 10 # number of times to run the QV circuit for each number of qubits num_shots = 3 # number of runs for each execution preferences = AzureBackendPreferences(backend_name="Rigetti.Qpu.Aspen-M-3") min_qubits = 2 max_qubits = 3 # qv = find_qv_trials, num_(numshots, min_qubits, max_qubits, preferences) ``` # ## Running with IBM Cloud Targets Run on a few IBM machines: * ibm\_fez with a reported quantum volume of 8 * ibm\_marrakesh with a reported quantum volume of 16 * ibm\_sherbrooke with a reported quantum volume of 32 Refer to the [IBM website](https://quantum.cloud.ibm.com/computers) ```python theme={null} preferences = IBMBackendPreferences( backend_name="ibm_fez", access_token="my-access-token", channel="ibm_cloud", instance_crn="instance-CRN", ) num_trials = 5 # number of times to run the QV circuit for each number of qubits num_shots = 10 # number of runs for each execution min_qubits = 2 max_qubits = 4 # qv = find_qv(num_trials, num_shots, min_qubits, max_qubits, preferences) ``` ```python theme={null} preferences = IBMBackendPreferences( backend_name="ibm_marrakesh", access_token="my-access-token", channel="ibm_cloud", instance_crn="instance-CRN", ) num_trials = 1 # number of times to run the QV circuit for each number of qubits num_shots = 10 # number of runs for each execution min_qubits = 2 max_qubits = 3 # qv = find_qv(num_trials, num_shots, min_qubits, max_qubits, preferences) ``` ```python theme={null} preferences = IBMBackendPreferences( backend_name="ibm_sherbrooke", access_token="my-access-token", channel="ibm_cloud", instance_crn="instance-CRN", ) num_trials = 1 # number of times to run the QV circuit for each number of qubits num_shots = 10 # number of runs for each execution min_qubits = 2 max_qubits = 3 # qv = find_qv(num_trials, num_shots, min_qubits, max_qubits, preferences) ``` ## References \[1] [Andrew W. Cross, Lev S. Bishop, Sarah Sheldon, Paul D. Nation, and Jay M. Gambetta (2019). Validating quantum computers using randomized model circuits.](https://arxiv.org/pdf/1811.12926.pdf) \[2] [Maris Ozols (2009). How to generate a random unitary matrix.](http://home.lu.lv/~sd20008/papers/essays/Random%20unitary%20\[paper].pdf) # Randomized Benchmarking Source: https://docs.classiq.io/explore/applications/benchmarking/randomized_benchmarking/randomized_benchmarking Open this notebook in GitHub to run it yourself This notebook explains how to perform a full, end-to-end, randomized benchmarking (RB) experiment using the Classiq platform. The notebook is divided into several parts describing the different steps of the workflow: model definition, synthesis, execution, and analysis. ## 1) Model Definition Start by defining the model, then the high-level function and its constraints: a) Define the number of qubits and number of cliffords that will define each benchmark model. b) Define hardware settings for the problem. Set transpilation preferences to None, to avoid gate cancellation. c) Define the clifford gates and how to apply them. d) Create a set of models for the RB, where num\_of\_qubits determines the width and num\_of\_cliffords determines the depth. For each model draw a random choice of Clifford gates. ```python theme={null} import random from functools import partial import numpy as np from classiq import * from classiq.qmod.symbolic import pi random.seed(0) np.random.seed(0) # a) Parameter definitions num_of_qubits = 1 numbers_of_cliffords = [5, 10, 15, 20, 25] # b) Hardware definitions hw_basis_gates = ["id", "rz", "sx", "x", "cx"] hw_settings = CustomHardwareSettings(basis_gates=hw_basis_gates) preferences = Preferences( custom_hardware_settings=hw_settings, transpilation_option="none" ) # c) Gates defenition, theta is chosen to be pi for it to be a clifford gate clifford_gates_map = { "id": I, "x": X, "y": Y, "z": Z, "rx": partial(RX, theta=pi), "ry": partial(RY, theta=pi), "rz": partial(RZ, theta=pi), "h": H, "s": S, "sdg": SDG, "sx": SX, } def get_random_clifford_gates(num_clifford_gates: int): supported_clifford_gates = [ gate for gate in hw_basis_gates if gate in clifford_gates_map ] return [random.choice(supported_clifford_gates) for _ in range(num_clifford_gates)] def apply_clifford_gates(target, clifford_gates): for gate in clifford_gates: clifford_gates_map[gate](target=target) # d) Model creation def get_model(num_cliffords): @qfunc def main(target: Output[QArray[QBit, num_of_qubits]]): allocate(target) clifford_gates = get_random_clifford_gates(num_cliffords) apply_clifford_gates(target, clifford_gates) invert(lambda: apply_clifford_gates(target, clifford_gates)) return create_model(main, preferences=preferences) qmods = [get_model(num_cliffords) for num_cliffords in numbers_of_cliffords] ``` ## 2) Synthesis Synthesize the constructed models using the synthesize\_async command to get the quantum program of each model. ```python theme={null} import asyncio async def synthesize_all_models(models): return await asyncio.gather(*[synthesize_async(qmod) for qmod in qmods]) quantum_programs = asyncio.run(synthesize_all_models(qmods)) ``` ## 3) Execution When you have the programs you are ready to run. Classiq allows running multiple programs on multiple backends in a single command. You specify the hardware (see details in the [execution user guide](https://docs.classiq.io/latest/user-guide/execution/)). This example runs on IBM Quantum simulators but may be replaced by any hardware with the proper access credentials. For IBM Quantum hardware access, for example, replace `ibmq_access_t` with an API token from [IBMQ's website](https://quantum-computing.ibm.com/) and specify the hardware name in the `backend_name` field of the `BackendPreferences` objects. ```python theme={null} # Execution from itertools import product ibmq_access_t = None backend_names = ( ClassiqSimulatorBackendNames.SIMULATOR_STATEVECTOR, ClassiqSimulatorBackendNames.SIMULATOR, ) backend_prefs = ClassiqBackendPreferences.batch_preferences( backend_names=backend_names, ) qprogs_with_preferences = list() for qprog, backend_pref in product(quantum_programs, backend_prefs): preferences = ExecutionPreferences( backend_preferences=backend_pref, transpilation_option="none" ) qprogs_with_preferences.append( set_quantum_program_execution_preferences(qprog, preferences) ) async def execute_program(qprog): job = await execute_async(qprog) return await job.result_async() async def execute_all_programs(qprogs): batch_size = 3 qprogs_batches = [ qprogs[i : i + batch_size] for i in range(0, len(qprogs), batch_size) ] results = [] for qprogs_batch in qprogs_batches: results.extend( await asyncio.gather(*[execute_program(qprog) for qprog in qprogs_batch]) ) return results results = asyncio.run(execute_all_programs(qprogs_with_preferences)) samples_results = [res[0].value for res in results] ``` ## 4) Analysis The final step is to analyze the RB data. While the last two steps were independent of the problem at hand, this part is RB unique. Start by reordering the data, which is given in a 'batch'. For RB analysis, match a program to the number of Clifford gates it represents, hence the `clifford_number_mapping` variable. Then, reorder the data according to the hardware, calling the `RBAnalysis` class to present the hardware comparison histograms. Note: If the backends are not replaced with real hardware, expect the trivial result of 100% fidelity for both backends. ```python theme={null} from classiq.analyzer.rb import RBAnalysis, order_executor_data_by_hardware mixed_data = tuple( zip( backend_prefs * len(quantum_programs), numbers_of_cliffords * len(backend_names), samples_results, ) ) rb_analysis_params = order_executor_data_by_hardware(mixed_data=mixed_data) multiple_hardware_data = RBAnalysis(experiments_data=rb_analysis_params) total_data = asyncio.run(multiple_hardware_data.show_multiple_hardware_data_async()) fig = multiple_hardware_data.plot_multiple_hardware_results() fig.show() ``` # Classiq Chemistry Application Source: https://docs.classiq.io/explore/applications/chemistry/classiq_chemistry_application/classiq_chemistry_application Open this notebook in GitHub to run it yourself This tutorial presents the functionality of Classiq's Chemistry application module. The application is based on the OpenFermion package \[[1](#of)], which is a comprehensive library for defining and analyzing Fermionic systems, in particular quantum chemistry problems. It provides efficient tools for transforming Fermionic operators to Pauli operators, which are then can be used with Qmod to define quantum algorithms (for more details see their [OpenFermion intro tutorial](https://quantumai.google/openfermion/tutorials/intro_to_openfermion) \[[2](#ofintro)]). The different classes and functions of Classiq's chemistry application is demonstrated below by implementing a Variational Quantum Eigensolver (VQE). A collection of concrete and concise VQE and QPE examples for chemistry can be found in the [chemistry application directory](https://github.com/Classiq/classiq-library/tree/main/applications/chemistry). *** ## The `FermionHamiltonianProblem` Class: Defining an Electronic Structure Problem. There are two ways of defining an electronic structure problem, either providing `MolecularData` of a molecule, or by directly defining a Fermionic Hamiltonian together with the number of spin up/down ($\alpha/\beta$) particles. Below we demonstrate the former, which is a more practical usecase. For the direct definition see [this](https://github.com/Classiq/classiq-library/blob/main/applications/chemistry/second_quantized_hamiltonian/second_quantized_hamiltonian.ipynb) example. *** # ## Defining a Molecule We start with defining a molecule, specifying its geometry (elements and their 3D position), multiplicity ($2\cdot(\text{total spin})+1$), basis, and an optional string for its description. In this tutorial we focus on the LiH molecule. There are several ways to get geometry of molecules, typical way involves using the *SMILES* (Simplified Molecular Input Line Entry System) of a molecule and use a chemical package such as `RDkit` to extract the geometry as an `xyz` file (a code example is given in the Appendix A of this notebook). For simplicity, we store the geometry in advance in the `lih.xyz` file and load it. *Comment: For complex molecules it is possible to call directly `from openfermion.chem import geometry_from_pubchem`* ```python theme={null} !pip install -qq "classiq[chemistry]" ``` ```python theme={null} from openfermion.chem import MolecularData geometry_file = "lih.xyz" # Set up molecule parameters basis = "sto-3g" # Basis set multiplicity = 1 # Singlet state S=0 charge = 0 # Neutral molecule # geometry with open(geometry_file, "r") as f: lines = f.readlines() atom_lines = lines[2:] # skip atom count and comment geometry = [] for line in atom_lines: parts = line.strip().split() symbol = parts[0] coords = tuple(float(x) for x in parts[1:4]) geometry.append((symbol, coords)) print(geometry) description = "LiH" # Create MolecularData object molecule = MolecularData(geometry, basis, multiplicity, charge, description) ``` **Output:** ``` [('Li', (0.833472, 0.0, 0.0)), ('H', (-0.833472, 0.0, 0.0))] ``` Next, we run a pyscf plugin for calculating various objects for our molecule problem, such as the second quantized Hamiltonian that is at the core of the VQE algorithm. For small problems, we can also get the Full Configuration Interaction (FCI), which calculates classically the ground state energy, i.e., for validating our quantum approach. *Comment: For complex problems running pyscf can take time, it is possible to run it only once, and load the data later on, using the `save` and `load` methods*. ```python theme={null} from openfermionpyscf import run_pyscf RECALCULATE_MOLECULE = True # can be set to False after initial run if RECALCULATE_MOLECULE: molecule = run_pyscf( molecule, run_mp2=True, run_cisd=True, run_ccsd=True, run_fci=True, # relevant for small, classically solvable problems ) molecule.save() molecule.load() ``` Now we can get several properties of our molecular problem. The electronic structure problem is described as a second quantized Hamiltonian $$ \Large H = h_0 + \sum_{p,q=0}^{2N-1} h_{pq}\, a^\dagger_p a_q + \frac{1}{2} \sum_{p,q,r,s=0}^{2N-1} h_{pqrs} \, a^\dagger_p a^\dagger_q a_r a_s, \tag{1} $$ where $h_0$ is a constant nuclear repulsion energy, and $h_{pq}$ and $h_{pqrs}$ are the well-known one-body and two-body molecular integrals, respectively. The sum is over all spin orbitals, which is twice the number of spatial orbitals $N$, as for each spatial orbital we have a spin up and spin down space (also known, and refer hereafter, as $\alpha$ and $\beta$ particles). This, together with the number of free electrons that can occupy those orbitals, define the electronic structure problem. ```python theme={null} print( f"The electronic structure problems has {2*molecule.n_orbitals} spin orbitals, and we need to occupy {molecule.n_electrons} electrons." ) print(f"The spatial orbitals energies are: {molecule.orbital_energies}") ``` **Output:** ``` The electronic structure problems has 12 spin orbitals, and we need to occupy 4 electrons. The spatial orbitals energies are: [-2.35046066 -0.27949547 0.07757097 0.16391248 0.16391248 0.52864126] ``` # ## Defining a Reduced Problem (Active Space/Freeze Core) In some cases, we can "freeze" some of the orbitals, occupying them with both spin up and spin down electrons. This will be orbitals with very low energy, such as the core orbitals, which are expected to be "classically" (with both spin up and down) occupied. In addition, we can exclude orbitals with very high energies, as they are unlikely to contribute significantly to the ground state. In other words, we can choose the active space for our molecular problem --- the spatial orbitals that are relevant to the quantum problem. This of-course reduces the problem we need to tackle. Below we define a `FermionHamiltonianProblem` for the LiH molecule, freezing its core ($0^{\rm th}$) orbital. The updated number of spatial orbitals and electrons are a property of the class. ```python theme={null} from classiq.applications.chemistry.problems import FermionHamiltonianProblem # Define a FermionHamiltonianProblem in an active space first_active_index = 1 problem = FermionHamiltonianProblem.from_molecule( molecule=molecule, first_active_index=first_active_index, # freeze orbitals below the first index remove_orbitals=[], # remove orbitals ) print(f"Reduced number of spatial orbitals after freeze core: {problem.n_orbitals}") print( f"Reduced number of (alpha,beta) electrons after freeze core: {problem.n_particles}" ) print( f"Length of Hamiltonian in Fermionic representation: {len(problem.fermion_hamiltonian.terms)}" ) ``` **Output:** ``` Reduced number of spatial orbitals after freeze core: 5 Reduced number of (alpha,beta) electrons after freeze core: (1, 1) Length of Hamiltonian in Fermionic representation: 811 ``` Let us look at several terms of our Fermionic operator: ```python theme={null} print(*list(problem.fermion_hamiltonian.terms.items())[:5], sep="\n") print(*list(problem.fermion_hamiltonian.terms.items())[::-1][:5], sep="\n") ``` **Output:** ``` ((), -6.817329071667983) (((0, 1), (0, 0)), -0.7621826148518264) (((0, 1), (1, 0)), 0.049739077075891036) (((0, 1), (4, 0)), -0.12350856641975391) (((5, 1), (5, 0)), -0.7621826148518264) (((9, 1), (9, 1), (9, 0), (9, 0)), 0.22555659839798667) (((9, 1), (9, 1), (9, 0), (6, 0)), -0.022199146730809072) (((9, 1), (9, 1), (9, 0), (5, 0)), 0.06490533159641326) (((9, 1), (9, 1), (8, 0), (8, 0)), 0.00990347751080244) (((9, 1), (9, 1), (7, 0), (7, 0)), 0.00990347751080244) ``` We can see one-body terms $((i,1),(j,0))$ that refer to $a_i^{\dagger}a_j$, and two-body terms $((i,1),(j,1),(k,0),(l,0))$ that corresponds to $a_i^{\dagger}a_j^{\dagger}a_ka_l$. > Orbital labeling: For $N$ spatial orbitals we have $N_\alpha (\text{spin up})+N_\beta (\text{spin down})=2N$ electron orbitals. The Classiq object for the electronic structure problem is defined according to block spin labeling $(0_\uparrow, 1_\uparrow, \dots, (N-1)_\uparrow, 0_\downarrow,1_\downarrow\dots, (N-1)_\downarrow$). This is opposed to the OpenFermion conventions, that has alternating spin labeling $(0_\uparrow, 0_\downarrow, 1_\uparrow, 1_\downarrow,\dots, (N-1)_\uparrow, (N-1)_\downarrow$). When transforming the problem to a Qubit Hamiltonian, described by Pauli strings, different labeling conventions can result in different Hamiltonians, which in turn, might lead to different quantum circuits in terms of depth or cx-counts. *** ## The `FermionToQubitMapper` Class: From Fock Space to Qubit Space *** # ## Transforming to Qubit Hamiltonian (Pauli Strings) Typically, when dealing with Fock space operators we need to transform the creation/annihilation operators to Pauli operators, suitable for quantum algorithms. There are several known transforms, such as Jordan Wigner (JW) and Bravyi Kitaev (BK) transforms. ```python theme={null} from classiq.applications.chemistry.mapping import FermionToQubitMapper, MappingMethod mapper = FermionToQubitMapper(method=MappingMethod.JORDAN_WIGNER) qubit_hamiltonian = mapper.map(problem.fermion_hamiltonian) qubit_hamiltonian.compress(abs_tol=1e-13) # trimming print(f"Length of Hamiltonian in Pauli representation: {len(qubit_hamiltonian.terms)}") print("Example of Pauli Hamiltonian terms:") print(*list(qubit_hamiltonian.terms.items())[:5], sep="\n") ``` **Output:** ``` Length of Hamiltonian in Pauli representation: 276 Example of Pauli Hamiltonian terms: ((), -5.750184614764152) (((0, 'Z'),), -0.29670485079927406) (((0, 'Y'), (1, 'Y')), -0.0025472629069889555) (((0, 'X'), (1, 'X')), -0.0025472629069889555) (((0, 'Y'), (1, 'Z'), (2, 'Z'), (3, 'Z'), (4, 'Y')), -0.01778020141438094) ``` # ## Hartree Fock State Once we have a problem and a mapper in hand, we can construct some quantum primitives. One example is the Hartree Fock state, typically used as an initial condition for ground state solvers. For the Jordan Wigner or the Bravyi Kitaev transforms, the Hartree Fock state, which is an elementary basis state in the Fock space, is mapped into a single computational basis state. This state can be determined using the `get_hf_state` function. ```python theme={null} from classiq.applications.chemistry.hartree_fock import get_hf_state hf_state = get_hf_state(problem, mapper) print(f"The HF state: {''.join(['1' if val else '0' for val in hf_state])}") ``` **Output:** ``` The HF state: 1000010000 ``` > HF state under the JW transform: : Working with the JW transform, there is a simple relation between the original Fock (occupation number) and transformed (computational) basis states: the state $|\underbrace{0\dots 0}_{k-1}10\dots 0\rangle$ corresponds to occupation of the $k$-th spin orbital in both spaces. Therefore, the Hartree Fock state under this transformation is the string $|\underbrace{1\dots 1}_{N_{\alpha}}00\dots 0\underbrace{1\dots 1}_{N_{\beta}}0\dots 0\rangle$. (This is as opposed to the BK transform, that gives a different computational basis state). *** ## The `Z2SymTaperMapper` Class: From Fock Space to Reduced Qubit Space by Using Symmetries *** Using symmetries of the second quantized Hamiltonian, one can reduce the number of qubits representing the problem by removing (tapering) qubits. The theory of qubit tapering is broad and complex, see for example Refs \[[3](#sym1)] and \[[4](#sym2)]. The `Z2SymTaperMapper` defines a mapper that includes qubit tapering. It can be initialized by providing $\mathbb{Z}_2$ symmetries data (set of generators and Pauli $X$ operators) explicitly, or by providing a Fermionic Hamiltonian problem. In the latter case, that is introduced below, the $\mathbb{Z}_2$ symmetries are deduced from the problem Hamiltonian. This is done via the `from_problem` method. \*\*In the following section we provide some mathematical explanation of what is happening behind the secens when defining the `Z2SymTaperMapper`. All the logic presented below is incorporated as part of this class. The advanced reader is encouraged to follow this part, while less experienced readers may choose to skip ahead to the *Constructing a VQE* section without loss of continuity.\*\* # ## Reducing the Problem Size with $\mathbb{Z}_2$ Symmetries (Qubit Tapering) The main steps of qubit tapering is as follows (see some technical details in Appendix B at the end of this notebook): 1. Find generators $\left\{g^{(i)}\right\}^k_{i=1}$ for a group of operators that commute with the Hamiltonian $H$: for all $g\in \langle g^{(1)},\dots g^{(k)}\rangle$, $\left[H, g\right] = 0$. That means that there is a basis in which both $H$ and such $g$ operators are diagonal. These operators are assumed to be a single Pauli string, typically containing only Pauli $Z$ operators. 1. Find a unitary transformation $U$ that diagonalizes all $g^{(i)}$, such that each generator operates trivially on all qubits except one, e.g., they transform to operators of the form $X_{l}$ for some qubit number $l$. It can be shown that such unitary can be constructed as $\Pi^k_{i=1}\frac{1}{\sqrt{2}}\left(X_{m^{(i)}}+g^{(i)}\right)$, where $X_{m^{(i)}}$ is operating on a some single qubit $m^{(i)}$. 2. Apply the transformation $U^{\dagger} H U$, whose eigenspace will be identical to those of $U^{\dagger} g_i U$. That means that on some qubits the transformed Hamiltonian is acting trivially, returning $\pm 1$ (thus is the name $\mathbb{Z}_2$ symmetries), and we can taper them off. 1. Taper off qubits from the transformed Hamiltonian. Let us define a qubit tapering mapper and inspect its properties: ```python theme={null} from classiq.applications.chemistry.z2_symmetries import Z2SymTaperMapper z2taper_mapper = Z2SymTaperMapper.from_problem( problem, method=MappingMethod.JORDAN_WIGNER ) generators = z2taper_mapper.generators x_ops = z2taper_mapper.x_ops ``` We can verify that the generators are indeed commuting with the Hamiltonian ```python theme={null} from openfermion.utils import commutator print(f"Number of generators: {len(generators)}") for gen in generators: print( f"For generator {gen}: the Norm of commutator with the Hamiltonian {commutator(qubit_hamiltonian, gen).induced_norm(1)}" ) ``` **Output:** ``` Number of generators: 4 For generator 1.0 [Z0 Z1 Z2 Z3 Z4]: the Norm of commutator with the Hamiltonian 0.0 For generator 1.0 [Z2 Z7]: the Norm of commutator with the Hamiltonian 0.0 For generator 1.0 [Z3 Z8]: the Norm of commutator with the Hamiltonian 0.0 For generator 1.0 [Z2 Z3 Z5 Z6 Z9]: the Norm of commutator with the Hamiltonian 0.0 ``` The generators for the symmetry group $\left\{g^{(i)}\right\}$ are accompained by $\left\{X_{m^{(i)}}\right\}$ operators, such that $g^{(i)} X_{m^{(j)}}= (-1)^{\delta_{ij}} X_{m^{(j)}}g^{(i)} $. We can verify this as well: ```python theme={null} print(f"The set of Pauli X operators: {[list(op.terms.keys()) for op in x_ops]}") print("=" * 65) for pauli_x in x_ops: x_position = list(pauli_x.terms.keys())[0][0][0] for j in range(len(generators)): print( f"For Pauli X_{x_position} and generator {j}: the commutator reads {commutator(pauli_x, generators[j])}" ) ``` **Output:** ``` The set of Pauli X operators: [[((0, 'X'),)], [((7, 'X'),)], [((8, 'X'),)], [((5, 'X'),)]] ================================================================= For Pauli X_0 and generator 0: the commutator reads -2j [Y0 Z1 Z2 Z3 Z4] For Pauli X_0 and generator 1: the commutator reads 0 For Pauli X_0 and generator 2: the commutator reads 0 For Pauli X_0 and generator 3: the commutator reads 0 For Pauli X_7 and generator 0: the commutator reads 0 For Pauli X_7 and generator 1: the commutator reads -2j [Z2 Y7] For Pauli X_7 and generator 2: the commutator reads 0 For Pauli X_7 and generator 3: the commutator reads 0 For Pauli X_8 and generator 0: the commutator reads 0 For Pauli X_8 and generator 1: the commutator reads 0 For Pauli X_8 and generator 2: the commutator reads -2j [Z3 Y8] For Pauli X_8 and generator 3: the commutator reads 0 For Pauli X_5 and generator 0: the commutator reads 0 For Pauli X_5 and generator 1: the commutator reads 0 For Pauli X_5 and generator 2: the commutator reads 0 For Pauli X_5 and generator 3: the commutator reads -2j [Z2 Z3 Y5 Z6 Z9] ``` > Intuition for conserved quantities under the JW transform: In electronic structure problems we have, for example, particle number conservation, spin conservation, and number of particles with fixed spin orientation. The latter corresponds to the two Fermionic operators: $$ >\text{Total number of spin-up/down particles operator:} \qquad N_{\uparrow} = \sum_i a^{\dagger}_{i\uparrow} a_{i\uparrow}, \qquad >N_{\downarrow} = \sum_i a^{\dagger}_{i\downarrow} a_{i\downarrow}. > $$ > As explained in the previous info box, working with the JW transform gives that Fock basis state are trasformed to the computational basis states. In particular, there is a relation between the orbital number operator and the $Z$ operators: $n_i \equiv a_i^{\dagger}a_i = \frac{1}{2}\left(1-Z_i\right)$. We cannot use the transformation of $N_{\uparrow(\downarrow)}$ as our symmetry generators, since they correspond to a sum of Pauli strings rather than a single string. However, we can use any function of those, >for example $g_{\uparrow(\downarrow)} = e^{\pi i \hat{N}_{\uparrow(\downarrow)}}$, which, up to a global phase, gives the generators $$ >g_{\uparrow} = \Pi^{N/2-1}_{k=0}Z_{k}, \qquad g_{\downarrow} = \Pi^{N-1}_{k=N/2}Z_{k}. > $$ > We can see that $g_{\uparrow}$ is indeed a generator in the example above. Next, we can define a transformation from the generators the the Pauli $X$ operators, which means that it block-diagonalizes the Hamiltonian according to symmetry subspaces. This unitary is given by $U = \Pi^k_{i=1}\frac{1}{\sqrt{2}}\left(X_{m^{(i)}}+g^{(i)}\right)$. ```python theme={null} from openfermion import QubitOperator blk_diagonalizing_op = QubitOperator(()) for gen, x_op in zip(generators, x_ops): blk_diagonalizing_op *= (2 ** (-0.5)) * (x_op + gen) ``` Let us verify, for example, that indeed this diagonalizing operator map each generator into a single computational basis subspace: ```python theme={null} for gen, x_op in zip(generators, x_ops): print( f"Generator in the new basis: {blk_diagonalizing_op*gen*blk_diagonalizing_op} --- compared to: {x_op}" ) ``` **Output:** ``` Generator in the new basis: (1.0000000000000002+0j) [X0] --- compared to: 1.0 [X0] Generator in the new basis: (1.0000000000000002+0j) [X7] --- compared to: 1.0 [X7] Generator in the new basis: (1.0000000000000002+0j) [X8] --- compared to: 1.0 [X8] Generator in the new basis: (1.0000000000000002+0j) [X5] --- compared to: 1.0 [X5] ``` Next, let us examine the block-diagonalized Hamiltonian-- * We shall see that after transformation, the Hamiltonian acts trivially on some of the qubits, with the identity or with the $\left\{X_{m^{(i)}}\right\}$ operators found above. Thus, we can reduce it by going to one of the two eigenspaces of these operators, with eigenvalues $\pm 1$. ```python theme={null} block_diagonal_hamiltonian = ( blk_diagonalizing_op * qubit_hamiltonian * blk_diagonalizing_op ) block_diagonal_hamiltonian.compress(1e-12) print("Example of Pauli Hamiltonian terms:") print(*list(block_diagonal_hamiltonian.terms.items())[:16], sep="\n") ``` **Output:** ``` Example of Pauli Hamiltonian terms: ((), -5.750184614764152) (((0, 'X'), (1, 'Z'), (2, 'Z'), (3, 'Z'), (4, 'Z')), -0.2967048507992741) (((1, 'X'), (2, 'Z'), (3, 'Z'), (4, 'Z')), 0.002547262906988957) (((0, 'X'), (1, 'X')), -0.002547262906988957) (((4, 'X'),), 0.017780201414380945) (((0, 'X'), (1, 'Z'), (2, 'Z'), (3, 'Z'), (4, 'X')), -0.017780201414380945) (((2, 'Z'), (3, 'Z'), (5, 'X'), (6, 'Z'), (9, 'Z')), -0.2967048507992741) (((2, 'Z'), (3, 'Z'), (6, 'X'), (9, 'Z')), 0.002547262906988951) (((5, 'X'), (6, 'X')), -0.002547262906988951) (((7, 'X'), (8, 'X'), (9, 'X')), 0.017780201414380938) (((2, 'Z'), (3, 'Z'), (5, 'X'), (6, 'Z'), (7, 'X'), (8, 'X'), (9, 'X')), -0.017780201414380938) (((1, 'Z'),), -0.39063013875079544) (((1, 'Y'), (2, 'Z'), (3, 'Z'), (4, 'Y')), 0.024546035949578163) (((1, 'X'), (2, 'Z'), (3, 'Z'), (4, 'X')), 0.024546035949578163) (((6, 'Z'),), -0.39063013875079533) (((2, 'Z'), (3, 'Z'), (6, 'Y'), (7, 'X'), (8, 'X'), (9, 'Y')), 0.024546035949578177) ``` We can see that on the 0$^{th}$ qubit we have only $X$ operations, therefore, we know that the eigenstates of our Hamiltonian will be of the form: $$ |\psi\rangle_{10} = |\pm 1\rangle |\bar{\psi}\rangle_{9}. $$ That is, the first qubit is either at state $|+\rangle$ or $|-\rangle$ (the eigenvectors of the Puali $X$ matrix). The same is true for all the other Pauli $X$ operators in `x_ops`. We shall choose a sector, i.e., the $+1$ or $-1$ subspace, for each of the subspaces $X_{(m_i)}$ operations. Which eigenspace to choose? The answer to this question depends on the problem at hand. If we would like to find the minimal energy of the Hamiltonian, then we shall take the subspace containing the minimal energy. One possibility is to solve multiple ($2^4$) problems on all sectors. However, another approach is to fix the sector according to the HF state, which is assumed to be in the optimal sector with minimal energy. This is the default sector defined in `Z2SymTaperMapper` when initializing with the `.from_problem` method. To emphasize the effect of choosing different sectors, we construct $2^4$ tapered operators, each for the subspaces (sectors) $\pm 1 \otimes \pm \otimes \pm 1 \otimes \pm 1$, and classically calculate the ground state for each tapered Hamiltonian. ```python theme={null} import itertools import numpy as np from openfermion.linalg import get_sparse_operator for sector in itertools.product([1, -1], repeat=len(x_ops)): z2taper_mapper.set_sector(sector) tapered_hamiltonian = z2taper_mapper.map(problem.fermion_hamiltonian) tapered_hamiltonian_sparse = get_sparse_operator(tapered_hamiltonian) w, v = np.linalg.eig(tapered_hamiltonian_sparse.toarray()) print(f"For sector {sector}: minimal eigenvalue: {np.min(w)}") ``` **Output:** ``` For sector (1, 1, 1, 1): minimal eigenvalue: (-7.768908584655453+0j) For sector (1, 1, 1, -1): minimal eigenvalue: (-7.804992414357773+0j) For sector (1, 1, -1, 1): minimal eigenvalue: (-7.724554496308053+0j) For sector (1, 1, -1, -1): minimal eigenvalue: (-7.717412859272543+0j) For sector (1, -1, 1, 1): minimal eigenvalue: (-7.724554496308075+0j) For sector (1, -1, 1, -1): minimal eigenvalue: (-7.717412859272538+0j) For sector (1, -1, -1, 1): minimal eigenvalue: (-7.312750852207606+0j) For sector (1, -1, -1, -1): minimal eigenvalue: (-7.555931078744113+0j) For sector (-1, 1, 1, 1): minimal eigenvalue: (-7.8049924143578036+0j) For sector (-1, 1, 1, -1): minimal eigenvalue: (-7.880416053961544+0j) For sector (-1, 1, -1, 1): minimal eigenvalue: (-7.717412859272534+0j) For sector (-1, 1, -1, -1): minimal eigenvalue: (-7.724554496308089+0j) For sector (-1, -1, 1, 1): minimal eigenvalue: (-7.717412859272531+0j) For sector (-1, -1, 1, -1): minimal eigenvalue: (-7.724554496308072+0j) For sector (-1, -1, -1, 1): minimal eigenvalue: (-7.555931078744102+0j) For sector (-1, -1, -1, -1): minimal eigenvalue: (-7.3127508522075795+0j) ``` *** ## Constructing a VQE Model with Classiq *** Next, we use all the classical pre-processing and definitions from the previous sections to build, synthesize, and execute a VQE model. We will take the following steps: 1. Defining the transformed and tapered-off Hartree Fock state, which serves as an initial condition for the problem. 2. Constructing the transformed and tapered-off UCC ansatz. 3. Defining, synthesizing, and executing the full model As a preliminary step, we define the Hamiltonian of the VQE problem. Since this is the final Hamiltonian (after a series of transformation, from second quantized Hamiltonian, tapering, etc.), let us trim small values according to some rough threshold. ```python theme={null} from classiq import * from classiq.applications.chemistry.op_utils import qubit_op_to_qmod ``` ```python theme={null} THRESHOLD = 1e-3 z2taper_mapper = Z2SymTaperMapper.from_problem(problem) tapered_hamiltonian = z2taper_mapper.map(problem.fermion_hamiltonian, is_invariant=True) tapered_hamiltonian.compress(THRESHOLD) n_vqe_qubits = z2taper_mapper.get_num_qubits(problem) vqe_hamiltonian = qubit_op_to_qmod(tapered_hamiltonian) print( f"Hamiltonian for VQE has: {len(vqe_hamiltonian.terms)} terms, and is operating on {n_vqe_qubits} qubits" ) ``` **Output:** ``` Hamiltonian for VQE has: 231 terms, and is operating on 6 qubits ``` Next, we use Classiq built-in functions to get the Hartree Fock state and the UCC ansatz Hamiltonians, given the problem and mapper. > Moving to symmetry subspaces: The Hartree Fock and the UCC operators that are defined below do not necessarily have the same symmetries of the molecular Hamiltonian. Thus, after the block-diagonalization, the resulting operators are not restricted to the symmetries' subspaces. We take the following approach: we remove terms which do not satisfy the symmetry relation, i.e., commute with symmetry generators. This is done automatically by calling the corresponding functions. # ## 1. Hartree Fock in the Tapered-Off Space We have already calculated the HF state under the JW transform, let us find the HF state after qubit tapering: ```python theme={null} hf_tapered_state = get_hf_state(problem, z2taper_mapper) print(f"The HF state: {''.join(['1' if val else '0' for val in hf_tapered_state])}") ``` **Output:** ``` The HF state: 000000 ``` # ## 2. UCC Ansatz The Unitary Coupled Cluster ansatz assumes that the HF state is initially occupied. Then, it includes excitations from the occupied to un-occupied states, where the former is defined by the HF state. In this tutorial we focus on the UCCSD ansatz, in which only singlet and doublet excitation are taken. The corresponding Fermionic operator reads: $$ \large U_{\text{UCCSD}} \equiv e^{T - T^\dagger}, \qquad T = T_1 + T_2 $$ where: $$ \large T_1 = \sum_{i \in \text{occ}} \sum_{a \in \text{virt}} t_i^a a_a^\dagger a_i, \qquad T_2 = \sum_{i None: prepare_basis_state(hf_tapered_state, state) multi_suzuki_trotter( hamiltonians=uccsd_hamiltonians, evolution_coefficients=params, order=1, repetitions=1, qbv=state, ) qprog = synthesize(main) show(qprog) ``` **Output:** ``` Number of parameters: 10 Quantum program link: https://platform.classiq.io/circuit/3BJyXS8riB3GXGj9iw4WRQqL6aG ``` To get a quick execution, we run on a statevector simulator. ```python theme={null} qprog = set_quantum_program_execution_preferences( qprog, preferences=ExecutionPreferences( num_shots=1000, backend_preferences=ClassiqBackendPreferences( backend_name="simulator_statevector" ), ), ) ``` We run simple optimization using the `minimize` method of `ExecutionSession` ```python theme={null} with ExecutionSession(qprog) as es: result = es.minimize( cost_function=vqe_hamiltonian, initial_params={"params": [0] * num_params}, max_iteration=200, ) ``` ```python theme={null} expected_energy = float(molecule.fci_energy) optimizer_res = result[-1][0] print(f"optimizer result classiq: {optimizer_res}") vqe_results = {k: np.real(result[k][0]) for k in range(len(result))} plt.plot(vqe_results.keys(), vqe_results.values(), "-") plt.ylabel("Energy [Ha]", fontsize=16) plt.xlabel("iteration", fontsize=16) plt.tick_params(axis="both", labelsize=16) plt.title("VQE result for " + description) plt.text( 50, -7.75, f"vqe energy: {optimizer_res} Ha,\n fci_energy: {expected_energy} Ha", fontsize=12, bbox=dict(facecolor="lightgray", edgecolor="black", boxstyle="round,pad=0.3"), ); ``` **Output:** ``` optimizer result classiq: -7.880416045623365 ``` output ## Appendix A * Loading Molecule Geometry ``` from rdkit import Chem from rdkit. Chem import AllChem # Generate 3D coordinates mol = Chem.MolFromSmiles('[LiH]') mol = Chem.AddHs(mol) AllChem.EmbedMolecule(mol) # Prepare XYZ string conf = mol.GetConformer() n_atoms = mol.GetNumAtoms() xyz_lines = [f"{n_atoms}", "LiH generated by RDKit"] for atom in mol.GetAtoms(): idx = atom.GetIdx() pos = conf.GetAtomPosition(idx) line = f"{atom.GetSymbol()} {pos.x:.6f} {pos.y:.6f} {pos.z:.6f}" xyz_lines.append(line) xyz_string = "\n".join(xyz_lines) # Save to file with open("lih.xyz", "w") as f: f.write(xyz_string) ``` ## Appendix B * Techical Details on Qubit Tapering Below we provide some technical details concerning qubit tapering and $\mathbb{Z}_2$ symmetries. It is a well-known fact in linear algebra that if two operators commute, $[A,B]=0$, then they can be mutually diagonalized. In particular, if $|v\rangle$ is an eigenvector of $B$ with an eigenvalue $\lambda$, we have $$ [A,B]=0\implies AB = BA \implies AB|v\rangle = BA|v\rangle \implies \lambda \left(A|v\rangle\right) = B\left(A|v\rangle\right). $$ That is, $A|v\rangle$ is also an eigenvector of $B$ with eigenvalue $\lambda$. Thus, $A|v\rangle$ must be in the eigenspace $V_{\lambda} \equiv \left\{|u\rangle, B|u\rangle = \lambda|u\rangle\right\}$. Now, Refs. \[[3](#sym1)] and \[[4](#sym2)] show, and this is implemented explicitly in Appendix B, that we can find a Clifford transformation $U$, such that the transformed Hamiltonian $H'=U^{\dagger} H U$ commutes with the transformed symmetries $X_{m^{(i)}} = U^{\dagger} g_i U$. We know how the eigenspaces of $X_{m^{(i)}}$ look like. For example, $X_{0}$ has two eigenspaces that correspond to the eigenvalues $\pm 1$: $V_{\pm} = \left\{|u\rangle_N, X_{0}|u\rangle_N = \pm |u\rangle_N\right\} = \left\{|\pm\rangle \otimes |\tilde{u}\rangle_{N-1},\, |\tilde{u}\rangle_{N-1} \text{ some state on } N-1 \text{ qubits}\right\} $. From the arguments above we get that $$ H'\cdot \left(|\pm\rangle |u\rangle\right) \in V_{\pm}, $$ which means that $H'$ must acts with $X_0$ or the Identity on the first qubit. ## References \[1]: [McClean et. al. Quantum Sci. Technol. 5 034014 (2020). OpenFermion: the electronic structure package for quantum computers.](https://arxiv.org/abs/1710.07629) \[2]: [Introduction to OpenFermion.](https://quantumai.google/openfermion/tutorials/intro_to_openfermion) \[3]: \[Bravyi et. al., arXiv preprint arXiv:1701.08213 (2017). Tapering off qubits to simulate fermionic Hamiltonians. ]\([https://arxiv.org/abs/1701.08213](https://arxiv.org/abs/1701.08213)) \[4]: [Kanav et al. J. Chem. Theo. Comp. 16 10 (2020). Reducing qubit requirements for quantum simulations using molecular point group symmetries.](https://arxiv.org/abs/1910.14644) # Creating a Molecule's Potential Energy Curve Source: https://docs.classiq.io/explore/applications/chemistry/molecular_energy_curve/molecular_energy_curve Open this notebook in GitHub to run it yourself A potential energy curve gives the ground energy of an assembly of atoms as a function of the distances between them. The global minima of the curve indicates the binding energy and internuclear distance for the stable molecule. Therefore, such a curve can be powerful tool in computational chemistry for predicting the molecular structure and spectrum. This tutorial demonstrates how to use the Classiq chemistry package to create a molecule's potential energy curve. It compares the result with the Hartree-Fock approximation method and the FCI (Full Configuration Interaction) energy. ```python theme={null} !pip install -qq "classiq[chemistry]" ``` ## Definitions and Initialization Define the range of internuclear distances for the model to simulate, and choose the number of sampling points in this range, which determines the graph's resolution. ```python theme={null} import numpy as np # define the sampling params num1 = 5 # how many sampling points - determines your resolution start1 = 0.20 # what is your sampling start distance stop1 = 1 # what is your sampling end distance num2 = 7 # how many sampling points - determines your resolution start2 = 1.4 # what is your sampling start distance stop2 = 3.5 # what is your sampling end distance # prepare x,y vectors distance = np.append(np.linspace(start1, stop1, num1), np.linspace(start2, stop2, num2)) VQE_energy = [] HF_energy = [] exact_energy = [] print(distance) ``` **Output:** ``` [0.2 0.4 0.6 0.8 1. 1.4 1.75 2.1 2.45 2.8 3.15 3.5 ] ``` ## Energy Estimations Define a for-loop, which takes these steps: 1. Creating a molecule at changing distances between the atoms. 2. Constructing a chemistry model for the corresponding Hamiltonian, using the Hartree-Fock initial state and UCC ansatz. 3. Synthesizing the model to get a quantum program. 4. Executing the quantum program to extract a solution for the ground energy. 5. Obtaining the exact solution and Hartree-Fock solution. ```python theme={null} import time from classiq import * ``` ```python theme={null} from openfermion.chem import MolecularData from openfermionpyscf import run_pyscf from classiq.applications.chemistry.hartree_fock import get_hf_state from classiq.applications.chemistry.op_utils import qubit_op_to_qmod from classiq.applications.chemistry.problems import FermionHamiltonianProblem from classiq.applications.chemistry.ucc import get_ucc_hamiltonians from classiq.applications.chemistry.z2_symmetries import Z2SymTaperMapper # create the molecule, insert the distance, prepare H, create UCC anzats and solve in energy qmods = [] qprogs = [] results = [] durations = [] for x in distance: time1 = time.time() # Define a molecule geometry = [("H", (0.0, 0.0, 0)), ("H", (0.0, 0.0, float(x)))] molecule = MolecularData(geometry, basis="sto-3g", multiplicity=1, charge=0) molecule = run_pyscf( molecule, run_mp2=True, run_cisd=True, run_ccsd=True, run_fci=True, # relevant for small, classically solvable problems ) # Define a problem and a mapper problem = FermionHamiltonianProblem.from_molecule(molecule) mapper = Z2SymTaperMapper.from_problem(problem) # Construct a model hf_state = get_hf_state(problem, mapper) uccsd_hamiltonians = get_ucc_hamiltonians(problem, mapper, excitations=[1, 2]) num_params = len(uccsd_hamiltonians) vqe_hamiltonian = qubit_op_to_qmod(mapper.map(problem.fermion_hamiltonian)) @qfunc def main(params: CArray[CReal, num_params], state: Output[QArray]): prepare_basis_state(hf_state, state) multi_suzuki_trotter(uccsd_hamiltonians, params, 1, 1, state) # Synthesize qprog = synthesize(main) qprog = set_quantum_program_execution_preferences( qprog, preferences=ExecutionPreferences( num_shots=1000, backend_preferences=ClassiqBackendPreferences( backend_name="simulator_statevector" ), ), ) qprogs.append(qprog) # Execute with ExecutionSession(qprog) as es: result = es.minimize( cost_function=vqe_hamiltonian, initial_params={"params": [0] * num_params}, max_iteration=500, ) VQE_energy.append(result[-1][0]) HF_energy.append(molecule.hf_energy) exact_energy.append(molecule.fci_energy) time2 = time.time() duration = time2 - time1 durations.append(duration) print(duration) ``` **Output:** ``` 6.627692937850952 3.974622964859009 3.1045420169830322 2.99271297454834 3.1956491470336914 2.8807778358459473 3.190415859222412 3.426532745361328 3.10728120803833 3.5942270755767822 3.1468281745910645 3.6718368530273438 ``` **Output:** ``` 10.790261507034302 ``` **Output:** ``` 9.763712167739868 ``` **Output:** ``` 9.770148515701294 ``` **Output:** ``` 9.85509705543518 ``` **Output:** ``` 15.547704696655273 ``` ## Graph Creation ```python theme={null} import matplotlib.pyplot as plt plt.plot( distance, VQE_energy, "r--", distance, HF_energy, "bs", distance, exact_energy, "g^" ) plt.xlabel("distance [Å]") plt.ylabel("energy [Ha]") plt.legend(["Classiq VQE", "Hartree-Fock", "Exact solution"]) plt.title("Binding Curve H_{2}") plt.show() ``` output This graph presents the ground state for the $H_{2}$ molecule as a function of the distance between the two hydrogen atoms. Note that both the HF solution and Classiq VQE present decent results around the global minima. For further distances, Classiq VQE stays close to the exact solution while the HF solution gradually deviates. The source of this lack of correspondence is with the lack of flexible correlations within the HF model, which is enabled within the VQE scope. You can explore more curves, creating graphs for different molecules (even n-dimensional or larger atom assemblies) in a similar fashion. # Molecule Eigensolver (VQE Method) Source: https://docs.classiq.io/explore/applications/chemistry/molecule_eigensolver/molecule_eigensolver Open this notebook in GitHub to run it yourself Evaluating the ground state of a molecular Hamiltonian allows you to understand the chemical properties of the molecule. This tutorial demonstrates the use of Variational Quantum Eigensolver (VQE) to find the ground states and energies of $H_2$, $H_2O$, and $LiH$ molecules. VQE is a leading method for finding approximate values of ground state wave functions and energies for complicated quantum systems and can give solutions for complex molecular structures. The overview of the VQE method is as follows: a problem (i.e., a molecule) is defined by a Hamiltonian whose ground state is sought. Then, a choice of a parameterized ansatz is made. A hybrid quantum-classical algorithm finds a solution for the defined parameters that minimizes the expectation value for the energy. A clever ansatz leads to an estimated ground state solution. Within the scope of Classiq's VQE algorithm, define a molecule that is translated to a concise Hamiltonian. Then, choose among types of well studied ansatzes, which are carefully selected to fit your molecule type. In the last stage, the Hamiltonian and ansatz are sent to a classical optimizer. This tutorial demonstrates the steps and options in Classiq's VQE algorithm. It presents the optimization strength of Classiq's VQE algorithm and its state-of-the-art results in terms of efficient quantum circuit, with the ultimate combination of low depth and high accuracy while minimizing the number of CX gates. ```python theme={null} !pip install -qq "classiq[chemistry]" ``` ## Generating a Qubit Hamiltonian Define the molecule to simulate, declaring the `MolecularData` class and inserting a list of atoms and their spatial positions (the distances are received in $\AA = 10^{-10} m$). In addition, provide basis, multiplicity, and charge. As mentioned above, this tutorial demonstrates how to define and find the ground state and energies for these molecules: ```python theme={null} molecule_H2_geometry = [("H", (0.0, 0.0, 0)), ("H", (0.0, 0.0, 0.735))] molecule_O2_geometry = [("O", (0.0, 0.0, 0)), ("O", (0.0, 0.0, 1.16))] molecule_LiH_geometry = [("H", (0.0, 0.0, 0.0)), ("Li", (0.0, 0.0, 1.596))] molecule_H2O_geometry = [ ("O", (0.0, 0.0, 0.0)), ("H", (0, 0.586, 0.757)), ("H", (0, 0.586, -0.757)), ] molecule_BeH2_geometry = [ ("Be", (0.0, 0.0, 0.0)), ("H", (0, 0, 1.334)), ("H", (0, 0, -1.334)), ] ``` You can construct any valid assembly of atoms in a similar manner. ```python theme={null} from openfermion.chem import MolecularData from openfermionpyscf import run_pyscf geometry = molecule_H2_geometry basis = "sto-3g" # Basis set multiplicity = 1 # Singlet state S=0 charge = 0 # Neutral molecule molecule = MolecularData(molecule_H2_geometry, basis, multiplicity, charge) molecule = run_pyscf( molecule, run_mp2=True, run_cisd=True, run_ccsd=True, run_fci=True, # relevant for small, classically solvable problems ) ``` Define the parameters of the Hamiltonian problem (`FermionHamiltonianProblem`) and the mapper (`FermionToQubitMapper`) between Fermionic Hamiltonian and qubit Hamiltonians (Jordan Wigner or Bravyi Kitaev). If you want to use Z2-symmteries for reducing the problem size you can use `Z2SymTaperMapper` (see below). $$ \langle \psi_{hf}| H|\psi_{hf}\rangle $$ ```python theme={null} from classiq.applications.chemistry.mapping import FermionToQubitMapper from classiq.applications.chemistry.problems import FermionHamiltonianProblem # Define a Hamiltonian in an active space problem = FermionHamiltonianProblem.from_molecule(molecule=molecule) mapper = FermionToQubitMapper() qubit_hamiltonian = mapper.map(problem.fermion_hamiltonian) print("Your Hamiltonian is", qubit_hamiltonian, sep="\n") num_qubits = mapper.get_num_qubits(problem) print(f"number of qubits {num_qubits}") ``` **Output:** ``` Your Hamiltonian is (-0.09057898608834769+0j) [] + (0.04523279994605784+0j) [X0 X1 X2 X3] + (0.04523279994605784+0j) [X0 X1 Y2 Y3] + (0.04523279994605784+0j) [Y0 Y1 X2 X3] + (0.04523279994605784+0j) [Y0 Y1 Y2 Y3] + (0.17218393261915538+0j) [Z0] + (0.12091263261776627+0j) [Z0 Z1] + (0.16892753870087907+0j) [Z0 Z2] + (0.1661454325638241+0j) [Z0 Z3] + (-0.2257534922240238+0j) [Z1] + (0.1661454325638241+0j) [Z1 Z2] + (0.17464343068300453+0j) [Z1 Z3] + (0.1721839326191554+0j) [Z2] + (0.12091263261776627+0j) [Z2 Z3] + (-0.22575349222402386+0j) [Z3] number of qubits 4 ``` ## Constructing and Synthesizing a Ground State Solver A ground state solver model consists of a parameterized eigenfunction ("the ansatz"), on which to run a VQE. Start with a Hardware (HW) efficient ansatz: # ## HW Efficient Ansatz The suggested HW efficient ansatz solution is generated to fit a specific hardware \[1]. The ansatz creates a state with a given number of parameters according to your choice of the number of qubits that fits the Hamiltonian, and creates entanglement between the qubits using the inputed connectivity map. This example uses a four qubit map, which is specifically made for $H_2$ without using qubit tapering. After constructing the model, synthesize it and view the output circuit. For groundstate solvers, it is typical to initialize the ansatz with the Hartree-Fock state. Use the `get_hf_state` and the `prepare_basis_state` qfunc. ```python theme={null} from classiq import * from classiq.applications.chemistry.hartree_fock import get_hf_state from classiq.applications.chemistry.op_utils import qubit_op_to_qmod reps = 3 num_params = reps * num_qubits hf_state = get_hf_state(problem, mapper) vqe_hamiltonian = qubit_op_to_qmod(mapper.map(problem.fermion_hamiltonian)) @qfunc def main(params: CArray[CReal, num_params], state: Output[QArray]): prepare_basis_state(hf_state, state) full_hea( num_qubits=num_qubits, operands_1qubit=[lambda _, q: X(q), lambda theta, q: RY(theta, q)], operands_2qubit=[lambda _, q1, q2: CX(q1, q2)], is_parametrized=[0, 1, 0], angle_params=params, connectivity_map=[(0, 1), (1, 2), (2, 3)], reps=reps, x=state, ) qmod_hwea = create_model( main, execution_preferences=ExecutionPreferences(num_shots=1000) ) qprog_hwea = synthesize(qmod_hwea) show(qprog_hwea) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3BJuF9XP8wwhjjLnVHhZtPDnbOO ``` # ## Unitary Coupled Cluster (UCC) Ansatz Create the commonly used chemistry-inspired UCC ansatz, which is a unitary version of the classical coupled cluster (CC) method \[2]. The parameter that defines the UCC ansatz: `excitations` (List\[int] or List\[str]): list of desired excitations, e.g., * 1 for singles * 2 for doubles * 3 for triples * 4 for quadruples Once again, after running the code lines below, you can view the output circuit that creates the state with an interactive interface and print the depth of the circuit. For the current example, use the `Z2SymTaperMapper` that exploits Z2-symmetries of the molecule Hamiltonian to reduce the problem size. You can confirm that using `Z2SymTaperMapper.from_problem` compared to `FermionToQubitMapper`, the number of qubits is reduced as: * for $H_2$ - from 4 to 1 * for $LiH$ from 12 to 8 (together with freezing the core orbital `first_active_index=1`) * for $H_{2}O$ from 14 to 10 (together with freezing the core orbital `first_active_index=1`) ```python theme={null} from classiq.applications.chemistry.ucc import get_ucc_hamiltonians from classiq.applications.chemistry.z2_symmetries import Z2SymTaperMapper problem = FermionHamiltonianProblem.from_molecule(molecule=molecule) mapper = Z2SymTaperMapper.from_problem(problem) qubit_hamiltonian = mapper.map(problem.fermion_hamiltonian) print("Your Hamiltonian is", qubit_hamiltonian, sep="\n") num_qubits = mapper.get_num_qubits(problem) print(f"number of qubits {num_qubits}") hf_state = get_hf_state(problem, mapper) uccsd_hamiltonians = get_ucc_hamiltonians(problem, mapper, excitations=[1, 2]) num_params = len(uccsd_hamiltonians) vqe_hamiltonian = qubit_op_to_qmod(mapper.map(problem.fermion_hamiltonian)) @qfunc def main(params: CArray[CReal, num_params], state: Output[QArray]): prepare_basis_state(hf_state, state) multi_suzuki_trotter(uccsd_hamiltonians, params, 1, 1, state) qmod_ucc = create_model(main, execution_preferences=ExecutionPreferences(num_shots=1e6)) qprog_ucc = synthesize(qmod_ucc) show(qprog_ucc) print(f"circuit depth: {qprog_ucc.transpiled_circuit.depth}") ``` **Output:** ``` Your Hamiltonian is -0.32112414706764497 [] + 0.18093119978423144 [X0] + 0.7958748496863588 [Z0] number of qubits 1 Quantum program link: https://platform.classiq.io/circuit/3BJuFWbJxXYUpK5TSRyPJPyTCV3 circuit depth: 3 ``` The Classiq UCC algorithm provides a highly efficient solution in terms of circuit depth and number of CX gates. These ultimately reduce the gate's time and amount of resources needed for operation. ## Executing to Find the Ground State After synthesizing the model you can execute it: After you specified a Hamiltonian and an ansatz, send the resulting quantum program to the VQE algorithm to find the Hamiltonian's ground state. In the process, the algorithm sends requests to a classical server, whose task is to minimize the energy expectation value and return the optimized parameters. The simulator and optimizing parameters are defined as part of the VQE part of the model. You can control the `max_iteration` value so the solution reaches a stable convergence. In addition, the `num_shots` value sets the number of measurements performed after each iteration, thus influencing the accuracy of the solutions. ```python theme={null} with ExecutionSession(qprog_ucc) as es: result_ucc = es.minimize( cost_function=vqe_hamiltonian, initial_params={"params": [0.0] * num_params}, max_iteration=200, ) ``` ```python theme={null} optimizer_res = result_ucc[-1][0] optimal_params = result_ucc[-1][1] print(f"optimizer result: {optimizer_res}") print(f"optimal parameter: {optimal_params}") ``` **Output:** ``` optimizer result: -1.137157513350175 optimal parameter: {'params': [-0.22005]} ``` Note that energy is presented in units of Hartree. Finally, compare the VQE solution to the classical solution: ```python theme={null} expected_energy = molecule.fci_energy print("exact result:", expected_energy) print("vqe result:", optimizer_res) ``` **Output:** ``` exact result: -1.1373060357533995 vqe result: -1.137157513350175 ``` \[1] [Abhinav Kandala, Antonio Mezzacapo, Kristan Temme, Maika Takita, Markus Brink, Jerry M. Chow, Jay M. Gambetta Hardware-efficient variational quantum eigensolver for small molecules and quantum magnets. Nature 549, 242 (2017).](https://arxiv.org/abs/1704.05018) \[2] [Panagiotis Kl. Barkoutsos, Jerome F. Gonthier, Igor Sokolov, Nikolaj Moll, Gian Salis, Andreas Fuhrer, Marc Ganzhorn, Daniel J. Egger, Matthias Troyer, Antonio Mezzacapo, Stefan Filipp, and Ivano Tavernelli Quantum algorithms for electronic structure calculations: Particle-hole Hamiltonian and optimized wave-function expansions. Phys. Rev. A 98, 022322 (2018).](https://arxiv.org/abs/1805.04340) # Projected Based Embedding Tutorial Source: https://docs.classiq.io/explore/applications/chemistry/projection_based_embedding/projected_based_embedding_tutorial Open this notebook in GitHub to run it yourself The **Projected Based Embedding method** is a multiscale quantum-classical approach that enables the simulation of complex molecular systems, such as enzymes or large materials, with quantum mechanical accuracy while keeping computational costs manageable \[[1](#rossmannek), [2](#lee), [3](#manby)]. The core idea is a partition of a large atomic system into two parts: * Subsystem A (fragment/active region): The chemically relevant fragment (e.g., an enzyme active site or reaction center) that requires an accurate quantum treatment, such as correlated wave function or quantum computer algorithms (like VQE). * Subsystem B (environment): The surrounding region, treated with a less expensive mean field method such as Hartree-Fock or density functional theory (DFT). The procedure consists of four mains steps: 1. The environment is treated using efficient numerical methods, yielding a mean-field description that serves as input to the quantum calculation. 2. From this mean-field output, an embedding Hamiltonian, $H_{\text{emb}}$ is constructed, which governs the dynamics of the active subsystem within its environment. 3. $H_{\text{emb}}$ is then solved using correlated methods, which take into account quantum effects, and can be evaluated efficiently utilizing quantum algorithms. 4. Finally, the results are combined to recover the total system energy, ensuring that the fragment (system A) is accurately described while system B provides the correct environmental influence. \*\*The classiq chemistry application provides functions that streamline all of these steps, starting from a molecular specification and a set of parameters defining the input model. It enables users to set up and run the full embedding workflow, From the initial mean-field calculation through Hamiltonian construction and VQE optimization. This notebook demonstrates how to apply this functionality.\*\* We begin by providing a brief introduction to the field, describing the essence of quantum chemistry mean-field methods. We focus on Hartree-Fock (HF) method and Density Functional Theory (DFT). Following, we present the theory behind projection-based embedding, leading to the Embedding Hamiltonian. An explanatory model of a water molecule is used as a case study to demonstrate the application of the methods. In the last part of the tutorial, we perform a WF-in-DFT calculation (WF = Wave Function), first starting with a mean-field calculation of the total water molecule, obtaining the molecular orbitals (MOs) and Kohn-Sham energies. Following, we localize occupied MOs and identify a restricted set, which corresponds to the active space (the space of quantum states describing the active region in the molecule). The active space MOs are employed in the calculation of the embedded Hamiltonian. Finally, the embedded Hamiltonian and the Hartree-Fock state of the active space are the inputs to quantum wave-function methods such as VQE, or ADAPT-VQE, which can be evaluated by a quantum computer. ## Background: Quantum Chemistry One begins by solving for the ground state of the electronic time-independent Schrödinger equation (TISE). Within the Born-Oppenheimer approximation, the nuclei are treated as fixed point charges and the electronic Hamiltonian for $N$ electrons and $M$ nuclei is $$ H_{\text{elec}} = -\frac{1}{2}\sum_{i=1}^{N}\nabla_i^2 - \sum_{i=1}^{N}\sum_{A=1}^{M}\frac{Z_A}{|\mathbf{r}_i - \mathbf{R}_A|} + \sum_{i ## Mean-Field Methods Solving the electronic Schrödinger equation exactly scales exponentially with the number of electrons, so we rely on **mean-field** methods, in which each electron moves in the averaged field created by all the others. Two common methods underpin the embedding scheme: * **Hartree-Fock (HF):** approximates the many-electron wavefunction as a single Slater determinant and solves the resulting one-electron (Fock) eigenvalue problem self-consistently. It treats exchange exactly but neglects electron correlation beyond the mean field. The computational cost is dominated by the two-electron integrals and scales as $O(N_b^4)$, where $N_b$ is the number of basis functions. * **Density functional theory (DFT):** recasts the problem in terms of the electron density $n(\mathbf{r})$ through the Kohn-Sham equations, folding the quantum many-body effects into an approximate exchange-correlation functional (here, B3LYP). Pure (local/GGA) functionals scale as $O(N_b^3)$. Hybrid functionals such as B3LYP include a fraction of exact exchange, raising the cost to $O(N_b^4)$. Both are solved by a self-consistent field (SCF) iteration and yield the molecular orbitals and density that seed the embedding stage. This tutorial runs the full-system mean field on the Classiq backend using DFT. The detailed HF and DFT formulations are collected in the [Technical Notes](#technical-notes) at the end. # ## Studied Example: Mean-Field with the Classiq SDK We now run the full-system mean field for the water molecule. With the SDK we describe the molecule with a `MoleculeSpec`, create an `EmbeddingCalculator`, and call `run_dft`. **`MoleculeSpec`** - the molecular description (a frozen dataclass). Its attributes are: * `atom` *(str)*: the geometry, given as an inline PySCF atom string or the textual contents of a `.pdb`/`.xyz` file. Filesystem paths are **not** accepted - use the `MoleculeSpec.from_pdb_file` / `MoleculeSpec.from_xyz_file` constructors to read a file client-side. * `basis` *(str, default `"cc-pVDZ"`)*: the PySCF basis-set name. * `charge` *(int, default `0`)*: the net molecular charge. * `spin` *(int, default `0`)*: `2S`, the number of unpaired electrons (`0` = closed-shell singlet). * `unit` *(str, default `"Angstrom"`)*: the length unit of the coordinates in `atom`. **`EmbeddingCalculator`** - the pipeline driver. Its constructor takes: * `spec` *(MoleculeSpec)*: the molecule defined above. * `spin_mode` *(`SpinMode`, default `AUTO`)*: `RESTRICTED`, `UNRESTRICTED`, or `AUTO` (resolves to restricted iff `spec.spin == 0`); the resolved value is exposed on the read-only `effective_spin_mode` property. * `auto_validations` *(sequence of `ValidationCheck`, default empty)*: checks to run automatically at the end of `run_dft_embedding`. We leave this empty here and run the checks explicitly in a later section. Its main methods map to the four-step procedure outlined above: * `run_dft` corresponds **step 1** above. It runs the full-system mean-field (DFT) calculation, returning a `DFTState`. * `run_dft_embedding` corresponds to **step 2**. It partitions the system, builds the embedding potential, and constructs $H_{\text{emb}}$, returning `(MeanFieldData, QuantumData)`. * **Steps 3-4** (solving $H_{\text{emb}}$ with VQE and recovering the total energy) are carried out explicitly in the notebook below. * `run_validations`: post-hoc consistency diagnostics on the embedding. The Kohn-Sham (DFT) SCF runs on the backend, and the returned `DFTState` is a light handle (resolved spin mode, functional, method), while the heavy converged SCF object stays server-side and is threaded automatically into the embedding stage. The consistency checks are run later, in their own section. ```python theme={null} !pip install -qq "classiq[chemistry]" ``` ```python theme={null} import warnings import numpy as np from openfermion.ops import FermionOperator from classiq.applications.chemistry.embedding import ( EmbeddingCalculator, EmbeddingConfig, MoleculeSpec, SpinMode, ValidationCheck, ) ``` ```python theme={null} HARTREE_TO_EV = 27.2114079527 # Water in a standard near-equilibrium geometry (Angstrom). Atom index 0 is the # oxygen; indices 1 and 2 are the hydrogens. spec = MoleculeSpec( atom=( "O 0.000000 0.000000 0.000000\n" "H 0.000000 -0.757000 0.587000\n" "H 0.000000 0.757000 0.587000" ), basis="cc-pVDZ", # try "sto-3g" for a coarser, faster run charge=0, spin=0, # 2S = N_alpha - N_beta; 0 => closed-shell singlet ) calc = EmbeddingCalculator( spec, spin_mode=SpinMode.AUTO, # restricted iff spin == 0 ) dft_state = calc.run_dft(xc_functional="B3LYP") print(f"spin mode : {dft_state.spin_mode}") print(f"functional: {dft_state.xc_functional}") print(f"method : {dft_state.method}") ``` **Output:** ``` spin mode : restricted functional: B3LYP method : dft ``` ## Construction of the Embedding Hamiltonian After performing the mean field calculation, we partition the total Hilbert space into an active (or alternatively "fragment") space and the environment space. Following, we derive the embedding Hamiltonian, $H_{\text{emb}}$, which is an effective Hamiltonian of the active space, incorporating averaged environmental effects. Such a calculation is known as a WF-in-MF scheme, i.e., wave-function in a mean-field description. In the present tutorial, MF stands for HF or DFT, and the WF part is conducted by a VQE quantum calculation. There are a number of possible methods by which such an embedding Hamiltonian can be derived. In the present tutorial, we focus on the "Projection (Huzinaga) based embedding". The key benefit of projection embedding compared to other embedding approaches is that it preserves additivity of the kinetic energy between the fragment (active space) and its environment. As a result, when both subsystems are treated at the same theoretical level, the fragment and environment energies combine to exactly reproduce the total energy of the full system. Within this framework, the total energy is given by: $$ E_{\text{WF-in-MF}}[{\Psi}_A, \mathbf{D}_A, \mathbf{D}_B] = E_{\text{WF}}[\tilde{\Psi}_A] + E_{\text{MF}}[\mathbf{D}_A + \mathbf{D}_B] - E_{\text{MF}}[\mathbf{D}_A] + \text{tr}\!\left[(\tilde{\mathbf{D}}_{A}-\mathbf{D}_A)\,\mathbf{v}_{\text{emb}}[\mathbf{D}_A, \mathbf{D}_B]\right] + \mu \,\text{tr}[\tilde{\mathbf{D}}_{A}\mathbf{P}_B]~~. \tag{1} $$ Where ${\Psi}_A$ is the wave-function for subsystem $A$. For a classical wave-function method $\tilde{\mathbf{D}}_{A}$ is the single-electron reduced density matrix, associated with the state , ${\Psi}_A$. However, for the quantum calculation such as VQE, one does not generally have efficient access to the density matrix, but to the probablities in the computational basis. Therefore, in the following calculation, $\tilde{\mathbf{D}}_{A}$ is obtained by an optimization procedure over the Fock operator of the embedded system (see details below) \[[2](#lee)]. $E_{\text{WF}}[\tilde{\Psi}_A]$ is evaluated by a correlated wave-function calculation, such as VQE. The embedding potential is $$ \mathbf{v}_{\text{emb}}[\mathbf{D}_A, \mathbf{D}_B] = \mathbf{g}[\mathbf{D}_A +\mathbf{D}_B] - \mathbf{g}[\mathbf{D}_A]~~, $$ where $\mathbf{g}$ includes all the two-electron interactions. $\mu\gg 1$ is a model hyperparameter, introducing an energy penalty on the environment states, and $\mathbf{P}_B$ is the projection on the environment's Hilbert space. The construction proceeds in three steps, all carried out automatically by `run_dft_embedding` (the full derivation of each is collected in the [Technical Notes](#technical-notes)): 1. **Partition the occupied orbitals** into the active fragment $A$ and the environment $B$. The occupied orbitals are localized and assigned to $A$ when their Mulliken weight on the fragment atoms exceeds `w_cut`, yielding the fragment and environment density matrices $\mathbf{D}_A,\mathbf{D}_B$. 1. **Build the embedded one-electron Hamiltonian** $\mathbf{h}^{A\text{-in-}B} = \mathbf{h} + \mathbf{v}_{\text{emb}}[\mathbf{D}_A,\mathbf{D}_B] + \mu\mathbf{P}_B$, where $\mathbf{v}_{\text{emb}}$ is the embedding potential, $\mathbf{P}_B = \mathbf{S}\mathbf{D}_B\mathbf{S}$ projects onto the environment, and the level shift $\mu$ keeps the environment orbitals out of the active space. 2. **Select the active space and second-quantize.** The fragment-occupied orbitals are augmented with a small set of active virtuals chosen by concentric localization (`n_active_virtuals` / `sv_tol`), and the Hamiltonian is expressed in this basis as the embedded second-quantized Hamiltonian $H_{\text{emb}} = \sum_{pq} h_{pq}\,c_p^\dagger c_q + \tfrac{1}{2}\sum V_{pqlm}\,c_p^\dagger c_q^\dagger c_m c_l$. # ## Studied Example: Building the Embedding Hamiltonian with the SDK The entire embedding construction - fragmentation of the occupied space, concentric localization of the active virtuals, assembly of the embedded one-electron operator $h^{A\text{-in-}B}$, and projection to the second-quantized Hamiltonian - is performed in a single `run_dft_embedding` call. We choose the oxygen atom (index 0) as the active fragment. `EmbeddingConfig` exposes the following knob: `w_cut` (Mulliken-weight threshold for assigning orbitals to the fragment), `n_active_virtuals` (number of concentric-localized virtuals to keep), `sv_tol` (SVD cutoff used when `n_active_virtuals` is `None`), and `mu` (the level-shift projector strength). The cached `DFTState` from `run_dft` is reused automatically. ```python theme={null} config = EmbeddingConfig( fragment_atoms=(0,), # oxygen is the active fragment xc_functional="B3LYP", w_cut=0.3, # matches the tutorial's oxygen-fragment partition n_active_virtuals=3, mu=1e6, # level-shift projector strength ) mean_field_data, quantum_data = calc.run_dft_embedding(config) print(f"fragment atom indices : {mean_field_data.atom_indices}") print(f"electrons in fragment (A) : {mean_field_data.n_electrons_A}") print(f"electrons in environment(B): {mean_field_data.n_electrons_B}") print(f"fragment DFT energy E_DFT_A : {mean_field_data.E_DFT_fragment:.6f} Ha") C_active = mean_field_data.C_active if isinstance(C_active, np.ndarray): n_ao, n_mo = C_active.shape print(f"active-space MO basis shape: ({n_ao} AOs, {n_mo} MOs) [restricted]") else: print( f"active-space MO basis shapes: " f"alpha ({C_active[0].shape[0]} AOs, {C_active[0].shape[1]} MOs), " f"beta ({C_active[1].shape[0]} AOs, {C_active[1].shape[1]} MOs) [unrestricted]" ) ``` **Output:** ``` fragment atom indices : (0,) electrons in fragment (A) : 10 electrons in environment(B): 0 fragment DFT energy E_DFT_A : -76.420378 Ha active-space MO basis shape: (24 AOs, 8 MOs) [restricted] ``` With $w_{\text{cut}} = 0.3$ and the oxygen atom as the fragment, all five occupied molecular orbitals have a Mulliken weight on oxygen that exceeds the threshold. Consequently, all 10 electrons are assigned to subsystem $A$ and none to the environment $B$. The active space then consists of these 5 occupied MOs together with the 3 concentric-localized virtual MOs, giving the 8 active-space orbitals reported above. # ## Consistency Tests The `ValidationCheck` diagnostics run through `run_validations` on the embedding state produced above. We request the DFT-in-DFT energy match, two geometric sanity checks (trace conservation and probability leakage), and the FCI-in-active-space reference in a single call; the results are cached on `calc.validation_results`. (Alternatively, the cheaper checks can be registered as `auto_validations` when constructing the `EmbeddingCalculator`, in which case they run automatically at the end of `run_dft_embedding`.) ```python theme={null} validation_results = calc.run_validations( [ ValidationCheck.DFT_IN_DFT, ValidationCheck.TRACE_CONSERVATION, ValidationCheck.PROBABILITY_LEAK, ValidationCheck.FCI_ACTIVE_SPACE, ] ) for check, (passed, info) in calc.validation_results.items(): status = "PASS" if passed else "FAIL" print(f"{check.value:<22} {status}") for key, value in info.items(): print(f" {key}: {value}") ``` **Output:** ``` dft_in_dft PASS E_embedded: -76.42037833355558 E_full: -76.42037833355553 error: 4.263256414560601e-14 trace_term: 4.656821072922328e-22 leakage: 0.0 dE_mu: 0.0 trace_conservation PASS trA: 10.000000000000007 trB: 0.0 expected: 10 tol: 1e-08 probability_leak PASS leak: 0.0 tol: 1e-08 fci_active_space PASS E_fci_embedded: -76.03806086432257 norb_active: 8 n_particles: [5, 5] ``` **Interpreting the validation results:** The basic validation results are the `dft_in_dft` and `trace_conservation`, verifying physical consistency, while the rest of the validity checks are designed for more advanced usage. * **`dft_in_dft`**: Reconstructs the full-system DFT energy from the embedded fragment and environment contributions. The embedded energy ($\approx -76.420$ Ha) matches the full-system energy to $\sim 10^{-13}$ Ha, confirming the projection-based embedding is exact when both subsystems use the same level of theory. The vanishing `trace_term`, `leakage`, and `dE_mu` indicate that no density leaked across the partition boundary. * **`trace_conservation`**: Verifies that $\text{tr}(\mathbf{D}_A) + \text{tr}(\mathbf{D}_B) = N$. Here $\text{tr}_A = 10$ and $\text{tr}_B = 0$, summing to the expected 10 electrons. * **`probability_leak`**: Checks that the fragment density matrix has no weight on environment orbitals. A leak of $0.0$ confirms a clean separation. * **`fci_active_space`**: Runs an exact (FCI) diagonalization of the embedded Hamiltonian within the 8-orbital active space ($\approx -76.038$ Ha). This serves as the classical "gold standard" against which the VQE result is compared below. ## Quantum-Ready Hamiltonians The embedded and physical Hamiltonians are returned as `openfermion.FermionOperator` objects on `quantum_data`, together with the active-space particle numbers and the environment energy correction: * `hamiltonian_emb`: the embedded Hamiltonian, optimized over the fragment; * `hamiltonian_phys`: the physical Hamiltonian (used to evaluate the energy); * `n_particles`: the active-space `(n_alpha, n_beta)` electron counts; * `env_correction`: the environment contribution $E_{\text{DFT}}[\mathbf{D}_A+\mathbf{D}_B] - E_{\text{DFT}}[\mathbf{D}_A]$ added back to recover the total energy. Both Hamiltonians already include the nuclear-repulsion constant. ```python theme={null} fermion_hamiltonian_emb = quantum_data.hamiltonian_emb fermion_hamiltonian_phys = quantum_data.hamiltonian_phys n_particles = quantum_data.n_particles print(f"n_particles (alpha, beta) : {n_particles}") print(f"# fermionic terms (embedded) : {len(fermion_hamiltonian_emb.terms)}") print(f"# fermionic terms (physical) : {len(fermion_hamiltonian_phys.terms)}") print(f"environment energy correction : {quantum_data.env_correction:.6f} Ha") ``` **Output:** ``` n_particles (alpha, beta) : (5, 5) # fermionic terms (embedded) : 16513 # fermionic terms (physical) : 16513 environment energy correction : -0.000000 Ha ``` # ## Theory The Variational Quantum Eigensolver (VQE) is a hybrid algorithm that utilizes a quantum computer to prepare and measure quantum states while a classical computer optimizes them. It is primarily used to find the ground-state energy of a quantum system. The algorithm performs this task as follows (in the following, we omit the subscript $A$ for conciseness): 1. Determine an ansatz state by choosing a parameterized quantum circuit $|{\Psi}(\theta)\rangle = U(\theta)| 0\rangle$. A good ansatz must be expressive enough to represent the true ground state, while shallow enough to run on the contemporary noisy hardware. 2. Prepare the quantum state $|\Psi (\theta)\rangle$ 3. Measure the expectation value $E(\theta) = \langle \Psi (\theta)| H_{\text{emb}} | \Psi \rangle $. 4. Classical optimization is performed, updating the parameters $\theta$ to reduce the energy. 5. Repeats until convergence, the resulting state approximates the ground state (by the variational principle) $E(\theta)\geq E_{\text{g.s}} = \min_{\Psi} \langle \Psi | H_{\text{emb}}|\Psi \rangle$ The original variational quantum eigensolver algorithm employed the Unitary Coupled Cluster (UCC) ansatz $$ | \Psi_{\text{UCC}}\rangle = e^{T-T^\dagger}| 0\rangle~~, $$ where $T $ is the excitation cluster operator that accounts for the correlation effects between the electrons. $T = T_1 + T_2 +\cdots~~,$ where $T_k = \frac{1}{(k!)^2}\sum_{ij\dots=1}^{N_\text{occ}}\sum_{ab\dots=1}^{N_\text{vir}}\theta_{ij}^{ab}\tau_{ij}^{ab}$, is the $k$-order excitation operator, with $\tau_{ij}^{ab} = a_{b}^\dagger a_a^\dagger\dots a_j a_i$. This ansatz is a quantum variation of the classical coupled cluster method. In this tutorial, we truncate the cluster operator at first order, retaining only the single excitation operators. This defines the UCCS (Singles) ansatz $$ | \Psi_{\text{UCCS}}\rangle = e^{T_1- T_1^\dagger}|0\rangle~~, $$ which yields shallower circuits at the cost of neglecting double excitations. Another option is the hardware-efficient ansatz, which parameterizes the native gates supported by the hardware, e.g., single-qubit rotations and entangling gates for a desired depth. This method enables shallow circuit implementations, but lacks a chemical motivation and may be prone to barren plateaus, which limit the optimization procedure. # ## Mapping the Embedded Hamiltonian to Appropriate OpenFermion Data Structures We employ the `OpenFermion` Python library to perform the VQE algorithm with Classiq. OpenFermion is an open-source Python library for working with fermionic systems on quantum computers. ```python theme={null} from classiq import * from classiq.applications.chemistry.hartree_fock import get_hf_state from classiq.applications.chemistry.mapping import FermionToQubitMapper from classiq.applications.chemistry.op_utils import qubit_op_to_qmod from classiq.applications.chemistry.problems import FermionHamiltonianProblem from classiq.applications.chemistry.ucc import get_ucc_hamiltonians from classiq.applications.chemistry.z2_symmetries import Z2SymTaperMapper from classiq.execution.functions import observe from classiq.execution.functions.minimize import variational_minimize ``` We inspect the first few terms of the embedded fermionic Hamiltonian: ```python theme={null} # Print the first few terms of the embedded Hamiltonian, sorted by operator index for term, coeff in sorted(fermion_hamiltonian_emb.terms.items(), key=lambda x: x[0])[ :5 ]: print(FermionOperator(term, coeff)) ``` **Output:** ``` 9.188258417746113 [] -8.514917580385305 [0^ 0] 0.4377252680041523 [0^ 0^ 0 0] -0.0577305403512616 [0^ 0^ 0 1] 0.005176469296566565 [0^ 0^ 0 2] ``` # ## Studied Example: Constructing the Initial Ansatz We wrap the embedded Hamiltonian in a `FermionHamiltonianProblem`, taking the active-space particle numbers directly from `quantum_data.n_particles`, and map it to qubits. ```python theme={null} problem = FermionHamiltonianProblem( fermion_hamiltonian=fermion_hamiltonian_emb, n_particles=n_particles, ) mapper = FermionToQubitMapper() num_qubits = mapper.get_num_qubits(problem) print(f"number of qubits: {num_qubits}") ``` **Output:** ``` number of qubits: 16 ``` We can reduce the problem's size by identifying $Z2$-symmetries of the embedded Hamiltonian \[[8](#bravyi-tapering)]. The reduction is performed by utilizing the Classiq package `Z2SymTaperMapper`. The transformation reduces the number of qubits from $16$ to $14$. Moreover, the transformation modifies the form of the Hartree-Fock state. ```python theme={null} mapper = Z2SymTaperMapper.from_problem(problem) qubit_ham = mapper.map(problem.fermion_hamiltonian) # Tapered Hartree-Fock state hf_state = get_hf_state(problem, mapper) num_qubits = mapper.get_num_qubits(problem) print(f"number of qubits after tapering: {num_qubits}") ``` **Output:** ``` number of qubits after tapering: 14 ``` ```python theme={null} print(f"Encoding of the initial Hartree-Fock state: {hf_state}") ``` **Output:** ``` Encoding of the initial Hartree-Fock state: [True, True, True, True, False, False, False, True, True, True, True, False, False, False] ``` # ### Possibly Trimming the Hamiltonian The computational resources can be reduced by trimming the qubit Hamiltonian. This corresponds to neglecting Pauli terms whose coefficient in the Hamiltonian is below a certain threshold. Below we trim terms with coefficients below `THRESHOLD = 0.1`. This is a relatively aggressive threshold that trades accuracy for reduced circuit depth. To preserve more accuracy, decrease the threshold (e.g., `0.01`) or comment out the trimming block entirely. ```python theme={null} ## Trimming the Hamiltonian THRESHOLD = 0.1 qubit_ham.compress(THRESHOLD) print(f"Length of trimmed Hamiltonian: {len(qubit_ham.terms)}") # Defining the SparsePauliOp hamiltonian HAMILTONIAN = qubit_op_to_qmod(qubit_ham) ``` **Output:** ``` Length of trimmed Hamiltonian: 160 ``` # ## Exact Diagonalization The exact diagonalization of the trimmed qubit Hamiltonian provides a classical reference for the VQE result. Note that this step scales exponentially with the number of qubits and takes approximately 30 minutes for the 14-qubit problem. The precomputed result is used below to avoid rerunning the expensive calculation. ```python theme={null} # Exact diagonalization (uncomment to recompute — takes ~30 min for 14 qubits): # Hamiltonian_matrix = hamiltonian_to_matrix(HAMILTONIAN) # eigvals, eigvecs = np.linalg.eig(Hamiltonian_matrix) # ground_state_energy = np.real(np.min(eigvals)) # print(f"ground state energy: {ground_state_energy}") ground_state_energy = -77.07666711404943 ``` # ## Solution of the Active Fragment by VQE ```python theme={null} uccsd_hamiltonians = get_ucc_hamiltonians(problem, mapper, excitations=[1]) num_params = len(uccsd_hamiltonians) @qfunc def main(params: CArray[CReal, num_params], state: Output[QArray]): prepare_basis_state(hf_state, state) multi_suzuki_trotter(uccsd_hamiltonians, params, 1, 1, state) qprog = synthesize(main) ``` ```python theme={null} vqe_result = variational_minimize( qprog, cost_function=HAMILTONIAN, initial_params={"params": [0.0] * num_params}, max_iteration=100, tolerance=1e-4, ) opt_energy, opt_params_dict = vqe_result[-1] opt_params = np.array(opt_params_dict["params"]) ``` **Output:** ``` Submitting job to simulator ``` Summary of VQE result ```python theme={null} print(f"optimized params: {opt_params}") print(f"optimized energy: {opt_energy}") print(f"error: {np.abs(100*(opt_energy - ground_state_energy)/ground_state_energy)}%") ``` **Output:** ``` optimized params: [-0.02373629 -0.04936612 -0.05173874 -0.12818058 0.00972557 -0.07164823 -0.02888005 -0.00773019 -0.01142646 0.00177649 -0.01025428 -0.00280544 -0.02932838 -0.00967372 -0.03254592 -0.00492063 0.02903898 0.00501263 -0.01290379 -0.00576706 -0.01757737 0.00317835 -0.00495551 -0.01813467 0.00355787 -0.03284931 0.06389685 -0.04651961 -0.03327415 -0.01868666] optimized energy: -77.31914162597833 error: 0.31458873483737637% ``` The VQE obtains a ground-state energy within \~1.5% of the exact diagonalization result for the embedded Hamiltonian, confirming the accuracy of the variational approach. Note that the FCI energy from the `fci_active_space` validation check above provides an equivalent classical reference and can be used in place of the exact diagonalization for benchmarking. ## WF-in-DFT Energy Calculation After obtaining an approximation for the ground state of the embedded Hamiltonian, utilizing the VQE algorithm, we evaluate the energy with respect to the physical Hamiltonian. We introduce an `energy_evaluation` function which receives a set of parameters and Hamiltonian and evaluates the expectation value with respect to the Hamiltonian. In order to evaluate $E_{WF}[\tilde{\Psi}_A]$ (see Eq. (1)) we input the optimized parameters and qubit representation of the physical Hamiltonian, `opt_params` and `qubit_ham_phys`. ```python theme={null} def energy_evaluation(params, hamiltonian): return np.real(observe(qprog, hamiltonian, parameters={"params": params.tolist()})) ``` ```python theme={null} problem_phys = FermionHamiltonianProblem( fermion_hamiltonian=fermion_hamiltonian_phys, n_particles=n_particles, ) # Reuse the same tapering mapper as the embedded problem. qubit_ham_phys = mapper.map(problem_phys.fermion_hamiltonian) qubit_ham_phys.compress(THRESHOLD) print(f"Length of trimmed physical Hamiltonian: {len(qubit_ham_phys.terms)}") HAMILTONIAN_PHYS = qubit_op_to_qmod(qubit_ham_phys) ``` **Output:** ``` Length of trimmed physical Hamiltonian: 160 ``` ```python theme={null} E_WF = energy_evaluation(opt_params, HAMILTONIAN_PHYS) print(f"E_WF = {E_WF}") ``` **Output:** ``` Submitting job to simulator /Users/roiedann/projects/classiq-library/.venv/lib/python3.11/site-packages/classiq/execution/functions/expectation_value.py:167: ClassiqDeprecationWarning: submit_estimate() is deprecated and will no longer be supported starting on 2026-06-22; use submit_observe() instead. job = session.submit_estimate(hamiltonian=observable, parameters=parameters) Job: https://platform.classiq.io/jobs/752e5b83-e57a-4a9f-a74b-77eb2a193efd ``` **Output:** ``` E_WF = -76.94325339004854 ``` Finally, we recover the total WF-in-DFT energy. The physical Hamiltonian already includes the nuclear-repulsion constant, and `quantum_data.env_correction` supplies the environment term $E_{\text{DFT}}[\mathbf{D}_A+\mathbf{D}_B] - E_{\text{DFT}}[\mathbf{D}_A]$, so the manual recombination of trace and level-shift terms from the original notebook collapses to a single addition: ```python theme={null} E_total = E_WF + quantum_data.env_correction print(f"WF-in-DFT total energy: {E_total} Ha") ``` **Output:** ``` WF-in-DFT total energy: -76.94325339004857 Ha ``` ```python theme={null} E_DFT_full = calc.validation_results[ValidationCheck.DFT_IN_DFT][1]["E_full"] print(f"WF-in-DFT total energy : {E_total:.6f} Ha") print(f"Full-system DFT energy : {E_DFT_full:.6f} Ha") print( f"Difference (WF - DFT) : {E_total - E_DFT_full:.6f} Ha ({(E_total - E_DFT_full) * HARTREE_TO_EV:.4f} eV)" ) ``` **Output:** ``` WF-in-DFT total energy : -76.943253 Ha Full-system DFT energy : -76.420378 Ha Difference (WF - DFT) : -0.522875 Ha (-14.2282 eV) ``` We compare the WF-in-DFT total energy to the full-system DFT energy. Here, the WF-in-DFT energy is a bit lower (more negative) than the pure DFT result by approximately $0.5$ Ha ($\sim 14$ eV). It is important to note that unlike Hartree-Fock, DFT is not a variational method, and the approximate exchange-correlation functional can introduce systematic biases, particularly in the description of core electrons. Comparing absolute total energies between DFT and correlated methods can therefore be misleading, and a more robust assessment is to examine energy *gaps* (e.g., excitation energies or reaction energies), where much of the core-electron bias cancels. ## Summary This tutorial demonstrated the full projection-based embedding workflow for a water molecule: 1. **Mean-field calculation**: A DFT (B3LYP) SCF was run on the full system via `run_dft`, yielding converged Kohn-Sham orbitals. 2. **Embedding construction**: `run_dft_embedding` partitioned the occupied space into fragment and environment, built the embedding potential, and produced the second-quantized embedded Hamiltonian. 3. **Validation**: Four consistency checks (DFT-in-DFT energy match, trace conservation, probability leak, FCI reference) confirmed the embedding is exact and internally consistent. 4. **VQE optimization**: A UCCS (singles) ansatz was optimized on the embedded Hamiltonian, and the result was evaluated against the physical Hamiltonian to recover the total WF-in-DFT energy. ## Technical Notes # ## Hartree-Fock The Hartree-Fock method provides an approximate, yet tractable, mean-field solution by representing the many-electron wavefunction as a single Slater determinant. The wave-function is approximated as a product of individual electron wave functions, known as the Hartree product: $$ \psi_{\text{elec}}(\mathbf{x}_1,\dots,\mathbf{x}_N)\approx\phi_1(\mathbf{x}_1)\phi_2(\mathbf{x}_2) \dots \phi_1(\mathbf{x}_N)~~. $$ Since electrons are indistinguishable particles, we must incorporate the particle statistics by anti-symmetrizing the product form. The anti-symmetrization procedure is conveniently expressed in terms of the so-called Slater determinant $$ \begin{aligned} \psi_{\text{elec}}(\mathbf{x}_1, \mathbf{x}_2, \ldots, \mathbf{r}_N) &\approx \frac{1}{\sqrt{N!}} \begin{vmatrix} \phi_1(\mathbf{x}_1) & \phi_2(\mathbf{x}_1) & \cdots & \phi_N(\mathbf{x}_1) \\ \phi_1(\mathbf{x}_2) & \phi_2(\mathbf{x}_2) & \cdots & \phi_N(\mathbf{x}_2) \\ \vdots & \vdots & \ddots & \vdots \\ \phi_1(\mathbf{x}_N) & \phi_2(\mathbf{x}_N) & \cdots & \phi_N(\mathbf{x}_N) \end{vmatrix}~~, \end{aligned} $$ where $\phi_j(\mathbf{x_i})$ is the value of the $j$'th spin-orbital evaluated at electron $i$'th coordinates, and the prefactor $\frac{1}{\sqrt{N!}}$ ensures normalization. Each spin-orbital is a product of spatial and spin wave-functions $$ \phi_j(\mathbf{x}_i)=\phi_j(\mathbf{r}_i,s_i) = \psi_j(\mathbf{r}_i)\chi(s_i)~~. $$ The calculation of the system's ground state energy and wavefunction is obtained by employing the variational principle: 1. The best approximation of the ground state wave-function minimizes the energy $$ E_{\text{elec}} = \frac{\langle \psi_{\text{elec}}|H_{\text{elec}}| \psi_{\text{elec}}\rangle}{\langle \psi_{\text{elec}}| \psi_{\text{elec}}\rangle } ~~. $$ By varying the orbitals $\phi_j(\mathbf{x}_i)$ under the constraint that they remain orthonormal leads to the Hartree-Fock equations. 2\. The variation yields an eigen-like equation for the electron orbital $\{\phi_i\}$: $$ F(\mathbf{x}_i) \phi_i (\mathbf{x}_i) = \epsilon_i \phi_i (\mathbf{x}_i)~~. $$ Here $$ F(\mathbf{x}_i)= \sum_i h(\mathbf{x}_i) + \sum_{j=1}^{N_{\text{occ}}}\left[J_j(\mathbf{x}_i)- K_j(\mathbf{x}_i)\right]~~, $$ is effective one-electron operator, called the Fock operator. * $J_j$ is the Coulomb operator (classical electron-electron repulsion) * $K_j$ is the exchange operator (arises from antisymmetry). The solution of the eigen equation yields the (molecular) electron orbitals $\phi_i$ and orbital energies. Because the Fock operator depends on all the orbitals (through the Coulomb and exchange terms), this is a non-linear eigenvalue equation which requires an iterative solution. 3. A set of basis functions is next defined and the molecular orbitals are expanded as a linear combination of basis functions $\{\chi_\mu\}$: $$ \phi_i(\mathbf{x}) = \sum_{\mu} C_{\mu i}\chi_\mu(\mathbf{x})~~, $$ leading to an equivalent matrix form, known as the Roothan-Hall equation $$ \mathbf{F}\mathbf{C} = \mathbf{S}\mathbf{C}\mathbf{\epsilon}~~, $$ where $\mathbf{F}_{\mu\nu} = \langle \chi_\mu |F|\chi_\nu \rangle$ and $\mathbf\{S \}_\{\mu\nu\} = \langle \chi_\mu|\chi_\nu \rangle$ are the element of the the Fock and Overlap matrices, $\mathbf\{C\}_\{i \mu\} = C_\{\mu i\}$ is the coefficient matrix and $\mathbf\{\epsilon\}$ is a diagonal matrix containing the orbital energies. The Roothaan-Hall is a generalized eigenfunction equation, which can be solved via classical numerical methods. # ## Density Functional Theory Density functional theory provides a major computational simplification of the electronic problem. The key insight is that the electron density, $$ n(\mathbf{r}) = 2 \sum_{i=1}^{N_{\text{occ}}}\phi_i^*(\mathbf{r})\phi_i(\mathbf{r}) ~~, $$ which is a function of only three coordinates, contains a lot of information that is actually observable from the full wave function, which is a function of $3N$ coordinates. The theory is based on two fundamental theorems, proved by Kohn and Hohenberg: 1. The ground-state energy of the Schrodinger equation is a unique functional of the electron density, $E = E[n(\mathbf{r})]$. 2. The electron density that minimizes the energy of the overall functional is the true electron density, corresponding to the solution of the Schrodinger equation. These theorems indicate that the true electron density of the interacting many-body system can be evaluated by finding the density that minimizes the energy functional. The energy functional includes two major contributions: $$ E[\{ \phi_i \}] = E_{\text{known}}[\{ \phi_i \}] + E_{\text{XC}}[\{ \phi_i \}]~~, $$ where the first part includes the known contributions: the electronic kinetic energy, and the nuclei-electron, electron-electron, and nuclei-nuclei interactions. The second term, called the exchange-correlation term, includes all the quantum mechanical effects not included in the known term. The task of finding the ground state electron density involves solving the Kohn-Sham equations, each one includes only a single electron: $$ F^{(\text{KS})}(\mathbf{r})\phi_i^{(\text{KS})}(\mathbf{r}) = \epsilon_i \phi_i^{(\text{KS})}(\mathbf{r})~~, $$ where the Kohn-Sham operator is given by $$ F^{(\text{KS})}(\mathbf{r})= \frac{1}{2}\nabla^2 + V_{\text{eN}}(\mathbf{r}) +V_H(\mathbf{r})+V_{XC}(\mathbf{r})~~. $$ The first two terms represent the kinetic energy and the repulsion potential between the single electron and all the nuclei. The third term, called the Hartree potential, represents the repulsion between the electron and the total electron density $$ V_H = \int \frac{n(\mathbf{r'})}{|\mathbf{r}-\mathbf{r}'|}d\mathbf{r}' ~~, $$ created by all electrons, while the last term defines the exchange and correlation contributions to the single-electron equations. It can be defined in terms of a functional derivative of the exchange-correlation contribution $$ V_{\text{XC}} = \frac{\delta E_{\text{XC}}}{\delta n(\mathbf{r})} ~~. $$ **Note:** The Kohn-Sham orbitals are only auxiliary mathematical objects whose only strict physical meaning is to reproduce the exact electron density of the real, interacting system. Similar to the Hartree-Fock equations, due to the dependence of $V_{H}$ and $V_{\text{XC}}$, the Kohn-Sham equations are non-linear, and therefore solved iteratively. 1. We define an initial, trail electron density $n(\mathbf{r})$. 2. Solve the Kohn-Sham equations to find the single-particle wave-function $\phi_i^{(\text{KS})}(\mathbf{r})$, termed the Kohn-Sham wave-functions. 3. Calculate the electron density $n_{\text{KS}}(\mathbf{r}) = 2 \sum_i\phi_i^{(\text{KS})*}(\mathbf{r})\phi_i^{(\text{KS})}(\mathbf{r}) $. 4. Check the convergence. If the density converged, stop. Otherwise, update the electron density and return to step 5. The major caveat of the theory is that the true form of the energy functional $E_{\text{XC}}[\{\phi_i\}]$ is not known and must be approximated. Luckily, for a uniform electron gas, the functional can be derived analytically, providing an initial approximation for the exchange-correlation potential. By evaluating $V_{\text{XC}}$ for a uniform electron gas at the local density, we obtain the so-called local-density approximation (LDA). Alternatively, numerous higher-order approximations offer potentially better results. # ## Projection-Based Embedding: Detailed Derivation The sections below give the full theory behind the three-step construction summarized in *Construction of the Embedding Hamiltonian*: partitioning the occupied space into fragment and environment, the projector-based embedding potential, and building the active-space embedded Hamiltonian (including concentric localization of the virtual orbitals). The expression for the embedded system energy, $E_{\text{WF-in-MF}}$, can be understood as an extension of the DFT-in-DFT scheme. In this framework, one writes the total system energy as $E_{\text{DFT-in-DFT}}[\mathbf{D}_{\text{emb},A}, D_B] = E_{\text{DFT}}[\tilde{\mathbf{D}}_{A} + D_B] + \mu \text{tr}[\tilde{\mathbf{D}}_{A} \mathbf{P}_B]$. Minimization of the total energy with respect to system's $A$ density matrix, ${\mathbf{D}}_{\text{emb},A}$, leads to the expression of the Fock matrix of the embedded system, $\mathbf{F}_A$. Self-consistent optimization of $$ \mathbf{F}_A[\tilde{\mathbf{D}}_{A}] = \mathbf{h}^{\text{A-in-B}}[\mathbf{D}_A,\mathbf{D}_B]+\mathbf{g}[\tilde{\mathbf{D}}_{A}]~~, $$ with respect to $\tilde{\mathbf{D}}_{A}$, recovers subsystem's $A$ original density matrix $\mathbf{D}_A$. In this sense, the embedding method is internally consistent. The expression for $\mathbf{F}_A$ involves a costly re-evaluation of the embedding potential in each SCF iteration. Therefore, to bypass the expensive calculation, one expands the embedding potential to first-order, giving $$ E_{\text{DFT}}[\tilde{\mathbf{D}}_{A} + \mathbf{D}_B] \approx E_{\text{DFT}}[\tilde{\mathbf{D}}_{A}] + E_{\text{DFT}}[\mathbf{D}_A + \mathbf{D}_B] - E_{\text{DFT}}[\mathbf{D}_A] + \text{tr}[(\tilde{\mathbf{D}}_{A} - \mathbf{D}_A)\mathbf{v}_{\text{emb}}[\mathbf{D}_A, \mathbf{D}_B]]~~. $$ Here the embedding potential is $\mathbf{v}_{\text{emb}}[\mathbf{D}_A, \mathbf{D}_B] = \mathbf{g}[\mathbf{D}_A + \mathbf{D}_B] - \mathbf{g}[\mathbf{D}_A]$, where $\mathbf{g}$ collects all the two-electron mean-field interactions ($\mathbf{J} - \mathbf{K}$ for HF, $\mathbf{J} + \mathbf{V}_{\text{XC}}$ for DFT). The expression for the total energy of the WF-in-DFT can be understood as a generalization of the DFT-in-DFT method, where the DFT calculation over system $A$ is replaced by a correlated wavefunction approach. We divide the WF-in-DFT calculation into three main steps: 1. Fragmentation of the full system into an active fragment and environment, resulting in localized (or unlocalized) molecular orbitals of the two subsystems. 2. The MOs of the first mean-field calculation are then used to evaluate the mean-field embedding potential, $\mathbf{v}_{\text{emb}}$, and the associated embedded Fock operator $\mathbf{F}_A[\tilde{\mathbf{D}}_A]$. Solution of the associated Hartree-Fock problem yields optimized MOs that are used for the correlated calculation of system $A$. We denote the optimized MO basis of system $A$ by $\{\tilde{\phi}_i(\mathbf{x})\}$. 1. Evaluate the embedded Hamltonian $H_{\text{emb}}$ in the optimized MOs basis. Each of these steps is detailed in the sections that follow. # ## Partition of the Occupied Orbitals into Sub-System A (Active Fragment) and Sub-System B (Environment) Before deriving the embedded Hamiltonian, we first must partition the system's occupied molecular orbitals to the fragment and environment orbitals. Two possible methods are explored: (1) Fragmentation by localized molecular orbitals, and (2) by atomic orbitals. # ### Fragmentation by Localized Orbitals The occupied molecular orbitals $\{\phi_i\}$ are delocalized over the entire molecule because they diagonalize the Fock matrix. But chemically, we know the electrons tend to localize: e.g., each bond or lone pair "belongs" roughly to one region of space. To obtain orbitals that reflect this intuition, we rotate the occupied orbitals among themselves using a unitary transformation that spatially localizes each orbital. Formally, we define a unitary $\mathbf{U}$ that defines new orbitals: $\mathbf{C}^{(\text{loc})} = \mathbf{C}_{\text{occ}}\mathbf{U}$. The localized orbitals (LOs) span the same occupied subspace (the transformation is unitary), but are much more spatially confined. The Boys and Pipek-Mezey localizations correspond to two different optimization procedures leading to an associated $\mathbf{U}$. Once one has the localized orbitals, we can define the fragment orbitals as the subset of LOs that are mostly on a given set of atoms. Each LO can be projected onto atoms of the fragment using the Mulliken population on the chosen AO subset $$ w_i = \sum_{\mu\in A} \sum_\nu C^{(\text{loc})\dagger}_{i\mu}S_{\mu \nu} C^{(\text{loc})}_{\nu i}~~, $$ which indicates the fraction of the $i$'th MO density on the fragment atoms. Next, we select all localized orbitals for which the population is above a certain threshold, $w_i \geq w_{\text{cut}}$, those $i$'s correspond to the fragment indices. These indicies allow defining the fragment $A$, and environment $B$, of occupied orbitals $ \mathbf{C}_{A} = \mathbf{C}^{(\text{loc})}[:,\text{frag indices}]$, $ \mathbf{C}_{B} = \mathbf{C}^{(\text{loc})}[:,\text{env indices}]$, respectively. Finally, the associated density matrices are evaluated $$ \mathbf{D}_A =\mathbf{C}_{A}\mathbf{C}_{A}^\dagger ~~~~,~~~~ \mathbf{D}_B =\mathbf{C}_{B}\mathbf{C}_{B}^\dagger ~~. $$ Note that the localization is performed only on the occupied MO's, localization of virtual (unoccupied) is unstable and may lead to contradictions within the embedding method. # ### Fragmentation by Atomic Orbital In this fragmentation procedure, we first partition the atomic orbitals into fragment and environment orbitals. Following, we evaluate the Mulliken populations, $\{w_i\}$, of the molecular orbitals. If $w_i$ is above a predetermined threshold, $w_{\text{cut}}$, the molecular orbital is included within the fragment Hilbert space. Otherwise, it is set as a part of the environment's Hilbert space. Similarly, to the fragmentation by localized orbital, the partition of molecular orbitals into two sets, defines $\mathbf{C}_{A}$, $\mathbf{C}_{B}$, and the associated density matrices $\mathbf{D}_{A}$, $\mathbf{D}_{B}$. # ## Projector-Based Embedding (PBE) via Orthogonal Complement The projector-based embedding introduces a projection operator, allowing the construction of the fragment's embedded Hamiltonian, $H_{\text{emb}}$. This Hamiltonian governs the dynamics of the fragment system, embedded within the environment. The embedded Hamiltonian includes three main physical contributions: * Kinetic and electron nuclei interaction term, included within the one-electron Hamiltonian term and projected onto the fragment's Hilbert space. * An embedding potential $\mathbf{v}_{\text{emb}}[\mathbf{D}_A,\mathbf{D}_B]$ which represents the effective interaction between the fragment and environment electrons, projected onto the fragment's Hilbert space. * The Coulomb repulsion between the fragment electrons. Following the mean-field calculation on the full system and a partition of the total system into a fragment and environment sub-systems, the evaluation of $H_{\text{emb}}$ can be encapsulated in terms of the following steps: 1. Construct the projection operator, $\mathbf{P}_B = \mathbf{S} \mathbf{D}_B \mathbf{S}$, where $\mathbf{S}$ is the overlap matrix of the full system. 2. Evaluate $\mathbf{v}_{\text{emb}}[\mathbf{D}_A, \mathbf{D}_B]$, the form of which depends on the chosen mean field method. For a HF calculation (WF-in-HF scheme) $\mathbf{g} = \mathbf{J}-\mathbf{K}$, therefore $$ \mathbf{v}_{\text{emb}}^{(\text{HF})}[\mathbf{D}_A, \mathbf{D}_B] = \mathbf{J}[\mathbf{D}_A +\mathbf{D}_B] - \mathbf{J}[\mathbf{D}_A] - \mathbf{K}[\mathbf{D}_A +\mathbf{D}_B] + \mathbf{K}[\mathbf{D}_A]~~. $$ When the mean-field method is chosen to be density functional theory (WF-in-DFT scheme) $\mathbf{g} = \mathbf{J}+\mathbf{V}_{\text{XC}}$, hence, $$ \mathbf{v}_{\text{emb}}^{(\text{DFT})}[\mathbf{D}_A, \mathbf{D}_B] = \mathbf{J}[\mathbf{D}_A +\mathbf{D}_B] - \mathbf{J}[\mathbf{D}_A] + \mathbf{V}_{\text{XC}}[\mathbf{D}_A +\mathbf{D}_B] - \mathbf{V}_{\text{XC}}[\mathbf{D}_A]~~. $$ A third possiblity is a hybrid approach, where a combined HF and DFT is utilized for the mean-field calculation, in that case the embedded Hamiltonian would include both exchange and exchange-correlation terms. 4\. Gather the terms to construct the one-core Hamiltonian in the AO basis $\{\chi_\mu\}$: $$ \mathbf{h}^{\text{A-in-B}} = \mathbf{h} + \mathbf{v}_{\text{emb}}[\mathbf{D}_A, \mathbf{D}_B] + \mu \mathbf{P}_B~~, $$ where $\mathbf{h}$ is the one-electron core Hamiltonian matrix, including the kinetic energy and the electron nuclei interaction terms. # ## Construct the Embedding Hamiltonian The embedding Hamiltonian is constructed in terms of the system's $A$ MOs, we denote these by $\{\xi_p\}$: * Occupied (possibly localized) molecular orbitals * Concentric localized virtual orbitals The fragment-occupied MOs of $\{\xi_p\}$ are obtained in the fragmentation procedure of the total system's occupied. However, virtual orbitals are more spatially extended than occupied ones, moreover, localizing them destroys their energetic and physical ordering, and breaks the clean separation between the active (embedded) space and the environment, making the resulting embedded Hamiltonian ill-conditioned and harder to converge. The truncation of of the number of virtual orbitals and association of a restricted number of virtual orbitals in the fragment's Hilbert space is achieved by concentric localization. # ### Concentric Localization of Virtual Orbitals The partition of virtual orbitals into active fragment and environment virtual orbitals is achieved through concentric localization, which includes only orbitals that have a large overlap (in a pre-defined sense) with the occupied orbitals of the active fragment. Specifically, we employ a one-shell concentric localization procedure, where one selects the active virtual orbitals as the smallest set of virtual MOs that couple most strongly (via the Fock/overlap metric) to the fragment's occupied localized orbitals, i.e., the first "shell" of virtuals surrounding the fragment. The calculation includes the following steps: 1. We first obtain a set of canonical virtual orbitals, which contain all the unoccupied MO orbitals. The orbitals are stored as columns of the matrix `C_virt_A`, which contains the associated coefficients of the AO basis set. 1. Compute the coupling matrix $ M = C_{\text{virt}}^TSF C_{\text{occ},A}$, where the columns of $C_{\text{occ,A}}$ correspond to the active fragment's occupied MOs, and the columns of $C_{\text{virt}}$ correspond to the canonical virtual orbitals $| v_i^{\text{virt}} \rangle$. The form of the coupling matrix, $M$, can be understood as follows: The Fock operator $F$ constitutes an effective Hamiltonian of the mean-field problem, through which the occupied and virtual orbitals "feel" each other. Applying $F$ to an occupied MO answers the question: where can this occupied orbital "go" when excited? Since the AO are not orthogonal, the projector from AO to MO requires an AO overlap correction. Therefore, $SFC_{\text{occ},A}$ is a properly normalized way of expressing the operation of the Fock operator on each fragment-occupied orbital and expressing the result in the AO space. Finally, we project the result on the space of virtual orbitals 1. Perform a singular value decomposition (SVD) of `M`: $$ M = U \Sigma V^T~~, $$ here the columns of $U$ are orthonormal combinations of virtual orbitals, ordered by coupling strength (the singular values). 4\. We keep the first $k$ columns (largest singular values), where $k$ is determined by introducing a lower bound threshold, `sv_tol`, for the singular values. These columns define the active virtual orbitals $$ C_{\text{virt},A} = C_{\text{virt}}U_{[:,0:k]}~~. $$ Each column $j$ of $C_{\text{virt},A}$ contains the AO coefficients of a linear combination of canonical virtual orbitals with the largest coupling to the occupied space of subsystem $A$, these are the "virtual-concentric localized" orbitals: $$ | v_{j}^{(CL)}\rangle =\sum_i^{n_{\text{virt }}}| v_i^{\text{virt}} \rangle U_{ij}~~. $$ 5. Combine the fragment's occupied MOs and the concentric localized virtual MOs: $$ C_A = [C_{\text{occ},A}|C_{\text{virt},A}]~~. $$ Next, we build the embedded Hamiltonian $$ H_{\text{emb}} = \sum_{p,q=1}^{N_{\text{frag}}} h_{pq} c_{p}^{\dagger} c_{q} + \frac{1}{2}\sum V_{pqlm} c^{\dagger}_{p}c^{\dagger}_{q}c_{m}c_{l}~~, $$ where $c^\dagger/c$ are the second quantization fermionic creation/annihilation operators. $h_{pq} = \langle {\xi}_p |\mathbf{h}^{\text{A-in-B}}|\xi_q \rangle$ are the elements of $\mathbf{h}^{\text{A-in-B}}$ in the optimized MOs of system $A$. $V_{pqlm}$ are the components of the two-electron Coulomb tensor in the $ \{\xi_p(\mathbf{x})\}$ basis. # ## Coulomb Two-Electron Integral In the orbital basis $\{\phi_\mu\}$, the two-electron repulsion integrals (ERIs) are: $$ (\mu \nu|\lambda \sigma)=\int\int \phi_{\mu}^*(\mathbf{r}_1) \phi_{\nu} (\mathbf{r_1})\frac{1}{|\mathbf{r}_1-\mathbf{r}_2|}\phi_{\lambda}^*(\mathbf{r}_2) \phi_{\sigma}(\mathbf{r}_2)d \mathbf{r}_1 d \mathbf{r}_2 $$ ## References \[1]: [Rossmannek, M., Pavosevic, F., Rubio, A., & Tavernelli, I. (2023). Quantum embedding method for the simulation of strongly correlated systems on quantum computers. The Journal of Physical Chemistry Letters, 14(14), 3491-3497.](https://arxiv.org/abs/2302.03052) \[2]: [Lee, S. J., Welborn, M., Manby, F. R., & Miller III, T. F. (2019). Projection-based wavefunction-in-DFT embedding. Accounts of chemical research, 52(5), 1359-1368.](https://research-information.bris.ac.uk/ws/portalfiles/portal/199581966/Full_text_PDF_accepted_author_manuscript_.pdf) \[3]: [Manby, F. R., Stella, M., Goodpaster, J. D., & Miller III, T. F. (2012). A simple, exact density-functional-theory embedding scheme. Journal of chemical theory and computation, 8(8), 2564-2568.](https://pubs.acs.org/doi/10.1021/ct300544e) \[4]: [Helgaker, T., Jorgensen, P., & Olsen, J. (2013). Molecular electronic-structure theory. John Wiley & Sons.](https://onlinelibrary.wiley.com/doi/book/10.1002/9781119019572) \[5]: [Argaman, N., & Makov, G. (2000). Density functional theory: An introduction. American Journal of Physics, 68(1), 69-79.](https://arxiv.org/abs/physics/9806013) \[6]: [Baer, R. (2009). Electron density functional theory. Lecture notes, Institute of Chemistry, The Fritz Haber Center for Molecular Dynamics.](https://scholars.huji.ac.il/sites/default/files/roibaer/files/dft-roi.baer_.pdf) \[7]: [Tilly, J., Chen, H., Cao, S., Picozzi, D., Setia, K., Li, Y., ... & Tennyson, J. (2022). The variational quantum eigensolver: a review of methods and best practices. Physics Reports, 986, 1-128.](https://arxiv.org/abs/2111.05176) \[8]: [Bravyi, S., Gambetta, J. M., Mezzacapo, A., & Temme, K. (2017). Tapering off qubits to simulate fermionic Hamiltonians. arXiv preprint arXiv:1701.08213.](https://arxiv.org/abs/1701.08213) # Protein Folding Algorithm Source: https://docs.classiq.io/explore/applications/chemistry/protein_folding/protein_folding_with_qaoa/protein_folding Open this notebook in GitHub to run it yourself Protein folding is the description of a three-dimensional structure of an amino acid chain, arranged in space to become a biologically functional protein. Understanding the structure of protein is extremely valuable for various applications in medicine. Discovering the structure of proteins is a highly intricate problem, as each protein is constructed of a chain of hundreds or thousands of amino acids, and the number of configurations is roughly evaluated to be $3^{2(N-1)}$ with N being the number of amino acids \[1]. The exponential growth of conformations with the chain length N makes the problem very complex for classical computers. For a quantum computer, the algorithm grows linearly with the number of amino acids N that are conceived, as the one simulated below. This tutorial presents a method of achieving a folding geometry for a given amino acid sequence. Following paper \[2], we create a Pyomo optimization model, and send it to Classiq's Quantum Approximated Optimization Algorithm (QAOA), which finds the configuration with minimal energy. The results are later visualized and compared to a classical solution. ## Prerequisites The model uses Classiq libraries in addition to basic Python tools: ```python theme={null} import numpy as np import pandas as pd import plotly.graph_objects as go import pyomo.core as pyo from sympy import * ``` ## Defining the Optimization Problem Following the paper \[2], create a Python Pyomo model to describe an optimization problem where a paramatrized cost expression characterizes the geometrical configuration of a protein (amino acid sequence). Without going into too many details, the paper places the protein on the grid of a tetrahedral lattice. Each amino acid can be located in any vertex of the lattice, and an index is set to each amino acid. Since each vertex has four neighbors, after locating an index in space, the next location is set by pointing in one of the four directions indicated by an integer $[0,1,2,3]$. To ascribe a direction, assign each amino acid two qubits, thus mapping each index to a direction: * $[00] \rightarrow 0$ * $[01] \rightarrow 1$ * $[10] \rightarrow 2$ * $[11] \rightarrow 3$ For the convenience of the coding, each even and odd index of amino acid has an opposite meaning in space; i.e., "0" means "left" for an odd index and "right" for an even index. A protein of $N$ amino acids has $N-1$ directions (or edges). However, since the first two directions only set the orientation of the molecule in space but do not determine the relative location of the amino acids, there are $N-3$ directions. (In thethrader lattice, there is an equal angle between each three vertices, so the relations are the same for the first three, regardless of the chosen direction.) Thus, the required number of qubits to describe the directions in the lattice is $2(N-3)$, and the two first directions are set arbitrarily. Next, set the Hamiltonian; i.e., the cost function that is sent to minimization. The Hamiltonian consists of two terms: $H_{gc}$ describing geometrical constraints, and $H_{int}$ describing the interactions between the amino acids (the paper also discusses a third chirality constraint that is only relevant to side chains, which are not considered in the scope of this algorithm): * $H_{gc}$ - prevents two consecutive directions to fold back. Since this tutorial uses a convention where the odd and even indexes have an opposite meaning in space, it is sufficient to add a large penalty term if two consecutive directions are the same. * $H_{int}$ * For the interaction, define extra qubits, which determine if the interaction is "turned on". If so, they add a negative (beneficial) energy term $epsilon$, which indicates the interaction between amino acids. $epsilon$ is a nearest neighbor (NN) interaction term, relevant for distance 1 only; therefore, add a penalty for other distances. Note that this tutorial takes the interaction to be a constant value ($epsilon$) regardless of the type of amino acid interaction. You can modify this by inserting the Miyazawa & Jernigan table \[3]. Since it is trivial that the following amino acid in the sequence is the nearest neighbor, it is irrelevant to calculate the contributing energy from such an interaction (it adds a constant number regardless of the qubit's value). In fact, due to the structure of the tetrahedral lattice, only amino acids further away than fifth in the sequence might have non-trivial interaction. Thus, the number of possible interactions (and thus the number of interaction qubits) is (N-5)\*(N-4)/2 (calculated via arithmetic progression). In addition, to prevent folding back into the chain, while encouraging distance 1 interactions, make sure that the indices following the two interacting amino acids do not overlap with the interacting amino acid themselves. In other words, for NN $(i,j)$, \{i-1,i+1} must be at distance 2 (or else they will be at distance 0; i.e., overlapping). To account for that, add a penalty to the interaction term. ```python theme={null} def folding_hamiltonian(main_chain: str) -> pyo.ConcreteModel: model = pyo.ConcreteModel("protein_folding") N = len(main_chain) # number of amino acids # Calc number of possible interactions: Ninteraction = 0 if N > 5: Ninteraction = int((N - 5) * (N - 4) / 2) # Define the variables: model.f = pyo.Var(range(2 * (N - 3)), domain=pyo.Binary) model.interaction = pyo.Var(range(Ninteraction), domain=pyo.Binary) f_array = np.array(list(model.f.values())) interaction_array = np.array(list(model.interaction.values())) # Setting the two locations: a = np.array([1, 0, 0, 1]) full_f_array = np.append(a, f_array) # Define Hgc: T = lambda i, j: (1 - (full_f_array[2 * i] - full_f_array[2 * j]) ** 2) * ( 1 - (full_f_array[2 * i + 1] - full_f_array[2 * j + 1]) ** 2 ) L = 500 model.Hgc = 0 for i in range(N - 2): model.Hgc = model.Hgc + L * T( i, i + 1 ) # adds panelty if two consecutive index has the opposite direction # convert {0,1}^2 to 4 functions, each giving 1 for one vector and 0 for the others: fun0 = lambda i, j: (1 - full_f_array[i]) * (1 - full_f_array[j]) fun1 = lambda i, j: full_f_array[i] * (1 - full_f_array[j]) fun2 = lambda i, j: full_f_array[j] * (1 - full_f_array[i]) fun3 = lambda i, j: full_f_array[i] * full_f_array[j] # calculate distance between i,j amino acids: d_units_0 = lambda i, j: sum( [((-1) ** k) * fun0(2 * k, 2 * k + 1) for k in range(i, j, 1)] ) d_units_1 = lambda i, j: sum( [((-1) ** k) * fun1(2 * k, 2 * k + 1) for k in range(i, j, 1)] ) d_units_2 = lambda i, j: sum( [((-1) ** k) * fun2(2 * k, 2 * k + 1) for k in range(i, j, 1)] ) d_units_3 = lambda i, j: sum( [((-1) ** k) * fun3(2 * k, 2 * k + 1) for k in range(i, j, 1)] ) d = lambda i, j: ( (d_units_0(i, j)) ** 2 + (d_units_1(i, j)) ** 2 + (d_units_2(i, j)) ** 2 + (d_units_3(i, j)) ** 2 ) # define Hint: epsilon = -5000 L2 = 300 L1 = 500 h = lambda i, j: interaction_array[ sum([N - 5 - k for k in range(0, i + 1, 1)]) - (N - j) ] * ( epsilon + L1 * (d(i, j) - 1) ** 2 + L2 * ( (2 - d(j - 1, i)) ** 2 + (2 - d(j + 1, i)) ** 2 + (2 - d(i - 1, j)) ** 2 + (2 - d(i + 1, j)) ** 2 ) ) model.Hint = 0 for i in range(N - 5): j = i + 5 while j < N: model.Hint = model.Hint + h(i, j) j = j + 1 # setting the objective: model.cost = pyo.Objective(expr=model.Hint + model.Hgc, sense=pyo.minimize) return model ``` ## Creating the Protein Sequence Define the amino acid sequence as a string that is sent to the `folding_hamiltonian()` function to create an optimization model for the sequence: ```python theme={null} my_protein = "ABCDEF" # ABCDEFG" protein_model = folding_hamiltonian(my_protein) ``` ## Solving with the Classiq Platform We go through the steps of solving the problem with the Classiq platform, using QAOA algorithm \[[4](#qaoa)]. The solution is based on defining a pyomo model for the optimization problem we would like to solve. # ## Setting Up the Classiq Problem Instance In order to solve the Pyomo model defined above, we use the `CombinatorialProblem` quantum object. Under the hood it tranlastes the Pyomo model to a quantum model of the QAOA algorithm, with a cost function translated from the Pyomo model. We can choose the number of layers for the QAOA ansatz using the argument `num_layers`. ```python theme={null} from classiq import * from classiq.applications.combinatorial_optimization import CombinatorialProblem combi = CombinatorialProblem(pyo_model=protein_model, num_layers=5) qmod = combi.get_model() ``` ## Synthesizing the QAOA Circuit and Solving the Problem We can now synthesize and view the QAOA circuit (ansatz) used to solve the optimization problem: ```python theme={null} qprog = combi.get_qprog() show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/38w7UhP743dxfO3yMtC8EvwE49T ``` **Output:** ``` https://platform.classiq.io/circuit/38w7UhP743dxfO3yMtC8EvwE49T?login=True&version=15 ``` We now solve the problem by calling the `optimize` method of the `CombinatorialProblem` object. For the classical optimization part of the QAOA algorithm we define the maximum number of classical iterations (`maxiter`) and the $\alpha$-parameter (`quantile`) for running CVaR-QAOA, an improved variation of the QAOA algorithm \[[3](#cvar)]: ```python theme={null} optimized_params = combi.optimize(maxiter=70, quantile=0.7) ``` We can check the convergence of the run: ```python theme={null} import matplotlib.pyplot as plt fig, axes = plt.subplots(nrows=1, ncols=1) axes.plot(combi.cost_trace) axes.set_xlabel("Iterations") axes.set_ylabel("Cost") axes.set_title("Cost convergence") ``` **Output:** ``` Text(0.5, 1.0, 'Cost convergence') ``` output ## Presenting the Quantum Results In order to get samples with the optimized parameters, we call the `sample` method. Since this is a quantum solution with probablistic results, there is a defined probability for each result (shown in a histogram), where the solution is chosen as the most probable one. Translate the solution in terms of qubits, to the location in space of the amino acids, thereby creating a sketch of the protein folding for the sequence: ```python theme={null} optimization_result = combi.sample(optimized_params) optimization_result.sort_values(by="cost").head(5) ``` | | solution | probability | cost | | --- | ------------------------------------------------ | ----------- | ----- | | 14 | \{'f': \[0, 0, 1, 0, 0, 1], 'interaction': \[1]} | 0.019531 | -2600 | | 13 | \{'f': \[1, 1, 1, 0, 0, 1], 'interaction': \[1]} | 0.021484 | -2600 | | 0 | \{'f': \[1, 0, 1, 0, 0, 1], 'interaction': \[1]} | 0.055664 | -900 | | 114 | \{'f': \[0, 1, 1, 0, 0, 0], 'interaction': \[1]} | 0.000488 | -900 | | 64 | \{'f': \[0, 1, 1, 0, 0, 1], 'interaction': \[1]} | 0.004883 | -900 | We will also want to compare the optimized results to uniformly sampled results: ```python theme={null} uniform_result = combi.sample_uniform() ``` And compare the histograms: ```python theme={null} optimization_result["cost"].plot( kind="hist", bins=50, edgecolor="black", weights=optimization_result["probability"], alpha=0.6, label="optimized", ) uniform_result["cost"].plot( kind="hist", bins=50, edgecolor="black", weights=uniform_result["probability"], alpha=0.6, label="uniform", ) plt.legend() plt.ylabel("Probability", fontsize=16) plt.xlabel("cost", fontsize=16) plt.tick_params(axis="both", labelsize=14) ``` output Let us plot the solution: ```python theme={null} best_solution = optimization_result.solution[optimization_result.cost.idxmin()] best_solution ``` **Output:** ``` {'f': [1, 1, 1, 0, 0, 1], 'interaction': [1]} ``` ```python theme={null} best_solution = optimization_result.solution[optimization_result.cost.idxmin()]["f"] a = np.array([1, 0, 0, 1]) N = len(my_protein) R = np.append(a, list(best_solution[0 : 2 * (N - 3)])) x = [0] y = [0] z = [0] for i in range(N - 1): if (1 - R[2 * i]) * (1 - R[2 * i + 1]) == 1: x.append(x[i] + (-1) ** (i + 1)) y.append(y[i] + (-1) ** (i)) z.append(z[i] + (-1) ** (i + 1)) if R[2 * i] * (1 - R[2 * i + 1]) == 1: x.append(x[i] + (-1) ** (i + 1)) y.append(y[i] + (-1) ** (i + 1)) z.append(z[i] + (-1) ** (i)) if R[2 * i + 1] * (1 - R[2 * i]) == 1: x.append(x[i] + (-1) ** (i)) y.append(y[i] + (-1) ** (i + 1)) z.append(z[i] + (-1) ** (i + 1)) if R[2 * i] * R[2 * i + 1] == 1: x.append(x[i] + (-1) ** (i)) y.append(y[i] + (-1) ** (i)) z.append(z[i] + (-1) ** (i)) fig = go.Figure(data=[go.Scatter3d(x=x, y=y, z=z)]) fig.update_scenes(xaxis_visible=False, yaxis_visible=False, zaxis_visible=False) fig.show() ``` ## Comparing to Classical Results Solve the optimization model for the defined amino sequence by classical optimization, and present the results, thereby comparing to the QAOA performance. A mismatch of the classical and quantum solution indicates a need to tune the QAOA parameters: ```python theme={null} from pyomo.opt import SolverFactory solver = SolverFactory("couenne") solver.solve(protein_model) protein_model.display() ``` **Output:** ``` Model protein_folding Variables: f : Size=6, Index={0, 1, 2, 3, 4, 5} Key : Lower : Value : Upper : Fixed : Stale : Domain 0 : 0 : 0.0 : 1 : False : False : Binary 1 : 0 : 0.0 : 1 : False : False : Binary 2 : 0 : 1.0 : 1 : False : False : Binary 3 : 0 : 0.0 : 1 : False : False : Binary 4 : 0 : 0.0 : 1 : False : False : Binary 5 : 0 : 1.0 : 1 : False : False : Binary interaction : Size=1, Index={0} Key : Lower : Value : Upper : Fixed : Stale : Domain 0 : 0 : 1.0 : 1 : False : False : Binary Objectives: cost : Size=1, Index=None, Active=True Key : Active : Value None : True : -2600.0 Constraints: None ``` ```python theme={null} best_classical_solution = [pyo.value(protein_model.f[i]) for i in range(2 * (N - 3))] a = np.array([1, 0, 0, 1]) N = len(my_protein) R_c = np.append(a, best_classical_solution) x = [0] y = [0] z = [0] for i in range(N - 1): if (1 - R_c[2 * i]) * (1 - R_c[2 * i + 1]) == 1: x.append(x[i] + (-1) ** (i + 1)) y.append(y[i] + (-1) ** (i)) z.append(z[i] + (-1) ** (i + 1)) if R_c[2 * i] * (1 - R_c[2 * i + 1]) == 1: x.append(x[i] + (-1) ** (i + 1)) y.append(y[i] + (-1) ** (i + 1)) z.append(z[i] + (-1) ** (i)) if R_c[2 * i + 1] * (1 - R_c[2 * i]) == 1: x.append(x[i] + (-1) ** (i)) y.append(y[i] + (-1) ** (i + 1)) z.append(z[i] + (-1) ** (i + 1)) if R_c[2 * i] * R_c[2 * i + 1] == 1: x.append(x[i] + (-1) ** (i)) y.append(y[i] + (-1) ** (i)) z.append(z[i] + (-1) ** (i)) fig = go.Figure(data=[go.Scatter3d(x=x, y=y, z=z)]) fig.update_scenes(xaxis_visible=False, yaxis_visible=False, zaxis_visible=False) fig.show() ``` ## References \[1] [Levinthal's paradox.](https://en.wikipedia.org/wiki/Levinthal%27s_paradox) \[2] [Robert, Anton, Panagiotis Kl Barkoutsos, Stefan Woerner, and Ivano Tavernelli. "Resource-efficient quantum algorithm for protein folding." npj Quantum Information 7, no. 1 (2021): 1-5.](https://www.nature.com/articles/s41534-021-00368-4) \[3] [Miyazawa, S. & Jernigan, R. L. Residue-residue potentials with a favorable contact pair term and an unfavorable high packing density term, for simulation and threading. J. Mol. Biol. 256, 623-644 (1996).](https://pubmed.ncbi.nlm.nih.gov/8604144/) \[4]: [Farhi, Edward, Jeffrey Goldstone, and Sam Gutmann. "A quantum approximate optimization algorithm." arXiv preprint arXiv:1411.4028 (2014).](https://arxiv.org/abs/1411.4028) # QFold: Quantum Walks and Deep Learning to Solve Protein Folding Source: https://docs.classiq.io/explore/applications/chemistry/protein_folding/protein_folding_with_quantum_walk/qfold Open this notebook in GitHub to run it yourself ## Introduction Protein folding is an important problem at the foundation of biological phenomena. If we could predict how a sequence of amino acids folds into a three-dimensional structure, it would greatly contribute to drug design and understanding diseases. However, the folding problem is extremely difficult in computational science. 1. Structure space grows explosively * Each amino-acid residue has dihedral angles $\psi$ and $\phi$, and their combinations generate countless possible spatial structures. * For example, if $\phi$ and $\psi$ are each discretized into several dozen levels, the number of possible structures grows exponentially with respect to the number of residues N. 1. Limitations of classical computation * Classically, the search is performed using molecular dynamics (MD) simulations or Monte Carlo (MC) methods. * In the MC method in particular, the procedure "compute energy difference $\Delta E \to$ determine whether to accept the new structure using the classical Metropolis rule" is repeated. * However, because many energy minima exist, classical searches tend to fall into local optima, and large-scale exploration is computationally difficult. 1. Potential of quantum computation * Quantum computers can make use of superposition (parallelism) and amplitude amplification. * In particular, the Szegedy-type quantum walk is a quantized version of a classical Markov chain, and the paper shows the possibility of accelerating the Metropolis method. In other words, by representing local moves with a quantum walk, it is expected that the structure space can be explored more efficiently. ## Method: QFold, A Protein-Folding Method Based on Quantum Walks In the proposed method, there is a classical processing part (Initialization module) that uses deep learning, and a quantum/classical Metropolis method (Simulation module) that executes quantum computation based on the obtained classical processing. The combination of this classical processing part and the quantum computation part is called QFold. The quantum computation part alone is referred to as the quantum Metropolis method. The quantum Metropolis method aims to perform an approximate exploration of the energy landscape by embedding an update rule similar to the Metropolis-Hastings method into a quantum circuit based on the Szegedy-type quantum walk. # ## * Classical Processing \[2] In QFold's classical preprocessing, prior to executing the quantum computation, a finite set of $\phi$-$\psi$ angle configurations is sampled. For each configuration, using quantum chemistry software (e.g., **Psi4**), the **energy difference $\Delta E$** in the transition from the current structure to the proposed structure is calculated and saved as a dataset. Through this process, the "stability" of each local structural change is provided as numerical values. However, in this notebook, classical processing is not performed; instead, the dataset already computed in \[2] is used, and the focus is placed only on the quantum computation. # ## * Quantum Encoding In the quantum encoding of the protein, the degrees of freedom of the structure are mapped onto qubits as follows: * Rotation-angle qubits: encode the discretized angles $\phi$ and $\psi$ * Move-id register: selects the dihedral angle to be updated * Coin qubit: encodes the Metropolis acceptance probability Through this correspondence, candidate protein structures are represented within the quantum circuit. # ## * Coin Preparation and Acceptance Probability For each move, the acceptance probability is defined as $$ A = \min \left( 1, e^{-\beta \Delta E} \right) $$ ($\beta$ is the inverse-temperature parameter). This probability is converted into the rotation angle $$ \theta = 2 \arcsin \left( \sqrt{A} \right) $$ and Ry($\theta$) is applied to the coin qubit. As a result, low-energy structures are assigned higher amplitudes, and the transitions of the quantum walk are biased toward energetically favorable directions. Next, when the coin qubit is 1, the dihedral angle ($\phi$ or $\psi$) indicated by the move-id is updated. Afterwards, the auxiliary computation is uncomputed, and the reversibility of the entire operation is preserved. Furthermore, a **Grover-type reflection operator** is applied, and according to Szegedy's quantization of the Markov chain, the amplitudes of the low-energy states are amplified through interference effects. # ## * Measurement and Interpretation of the Results After repeating the walk operator for multiple steps, the $\phi$ and $\psi$ registers are measured. Combinations that appear with high frequency in the measurement results correspond to low free-energy, stable folding structures. ## Dataset The dataset used in this study is a table of **energy differences $\Delta E$** that were precomputed by classical computation (e.g., Psi4) after restricting the protein folding problem to a **finite number of discrete states**. In this notebook, the energy set by Minifold is not computed; instead, the dataset described in reference \[2] is used. Each state is uniquely represented by a **binary string of five types of bits**, and each bit is assigned the following meaning. The dataset stores "**the $\Delta E$ obtained when a state ($\phi$, $\psi$) is changed according to the move-id**." | Quantum register | Physical meaning | Notes | | ---------------- | --------------------------------------------------------------------- | ---------------------------------------------------------------------------- | | $\phi$ register | Current value of the dihedral angle $\phi$ (0 = $0^\circ$, 1 = $\pi$) | Rotational degree of freedom of the protein backbone | | $\psi$ register | Current value of the dihedral angle $\psi$ (0 = $0^\circ$, 1 = $\pi$) | The other main dihedral angle | | $M$ register | move-id (0 $\to$ change $\phi$ / 1 $\to$ change $\psi$) | Specifies which angle to move | | move-val | (0 = $-\pi$, 1 = $+\pi$) | In the 1-bit discretization this is always **1** (fixed at +$\pi$) | | coin (auxiliary) | coin placeholder | The actual coin qubit is generated inside the circuit; the input is always 0 | Example: **key = "00100"** * $b_0 = 0 \rightarrow \phi = 0^\circ$ * $b_1 = 0 \rightarrow \psi = 0^\circ$ * $b_2 = 1 \rightarrow {\rm move}\,\, \psi$ * $b_3 = 0 \rightarrow -\pi$ (the dataset always uses 1, so fixed to 1) * $b_4 = 0 \rightarrow$ coin placeholder The $\Delta E$ value corresponding to this key means "the energy difference when the current structure ($\phi = 0, \psi = 0$) has $\psi = 0$ changed by +$\pi$." ## Loading Dataset ```python theme={null} import numpy as np from classiq import * # import classiq # classiq.authenticate() ``` We use the simple dataset from the github repository \[2]. ```python theme={null} protein_data = { "protein": "alanylalanine", "numberBitsRotation": 1, "psi4_min_energy": -567.4480058904624, "deltas": { "00000": 0.9746468282557998, "00001": 0.9746468282557998, "00100": 25.86237460092684, "00101": 25.86237460092684, "01000": 0.8548282413310062, "01001": 0.8548282413310062, "01100": -25.86237460092684, "01101": -25.86237460092684, "10000": -0.9746468282557998, "10001": -0.9746468282557998, "10100": 25.742556014002048, "10101": 25.742556014002048, "11000": -0.8548282413310062, "11001": -0.8548282413310062, "11100": -25.742556014002048, "11101": -25.742556014002048, }, "initial_min_energy": -567.4480058903016, "index_min_energy": "0-0", "initialization_stats": { "phis_precision": [100.0], "psis_precision": [100.0], "phi_angles_psi4": [2.5801230429979163], "psi_angles_psi4": [-2.6017795753816184], "phis_initial_rotation": [2.5801230429979163], "psis_initial_rotation": [-2.6017795753816184], }, } ``` ```python theme={null} protein_data["deltas"] ``` **Output:** ``` {'00000': 0.9746468282557998, '00001': 0.9746468282557998, '00100': 25.86237460092684, '00101': 25.86237460092684, '01000': 0.8548282413310062, '01001': 0.8548282413310062, '01100': -25.86237460092684, '01101': -25.86237460092684, '10000': -0.9746468282557998, '10001': -0.9746468282557998, '10100': 25.742556014002048, '10101': 25.742556014002048, '11000': -0.8548282413310062, '11001': -0.8548282413310062, '11100': -25.742556014002048, '11101': -25.742556014002048} ``` ```python theme={null} import json def read_dataset_info(data): delta_table = data["deltas"] num_bits_rotation = data.get("numberBitsRotation") phi_angles = data.get("initialization_stats", {}).get("phi_angles_psi4", []) psi_angles = data.get("initialization_stats", {}).get("psi_angles_psi4", []) return delta_table, num_bits_rotation, phi_angles, psi_angles ``` ```python theme={null} delta_tbl, num_bits_rotation, phi_angles, psi_angles = read_dataset_info(protein_data) ``` ```python theme={null} # in_bits = self.n_angles * self.angle_precision_bits + self.move_id_len + 1 qbit_rotation_angle = num_bits_rotation * len(phi_angles) + num_bits_rotation * len( psi_angles ) qbit_move_id = int(np.ceil(np.log2(len(phi_angles) + len(psi_angles)))) print("numberBitsRotation:", num_bits_rotation) print("number of qubits representing roations φ_i, ψ_i:", qbit_rotation_angle) print("number of qubits representing move id:", qbit_move_id) print( "total qbit = qbit_rotation_angle+qbit_move_id+1:", qbit_rotation_angle + qbit_move_id + 1, ) print("phi_angles_psi4:", phi_angles) print("psi_angles_psi4:", psi_angles) ``` **Output:** ``` numberBitsRotation: 1 number of qubits representing roations φ_i, ψ_i: 2 number of qubits representing move id: 1 total qbit = qbit_rotation_angle+qbit_move_id+1: 4 phi_angles_psi4: [2.5801230429979163] psi_angles_psi4: [-2.6017795753816184] ``` ## Implementation of Walk Operator # ## Coin Preparation Following Algorithm 1, for each state $(\phi, \psi, M)$, $\Delta E$ is read out and the corresponding $\theta$ is computed. For example, in the case $M = 0$ (updating $\psi$), $\Delta E$ is obtained for the current structure with $\phi = 0$ and $\psi = 1$, and by taking its average, $A_\phi$ is determined. This is converted into $\theta_\phi$, and a controlled rotation is applied to the coin register. Similarly, when $M = 1$, $\theta_\psi$ for updating $\psi$ is computed and applied to the coin register. If the coin register is 1, the $\phi$ or $\psi$ register is flipped according to the corresponding move-id. This corresponds to accepting the new structure. After that, the coin rotation is uncomputed to erase the auxiliary information, and by adding the oracle operator, the amplitude of states corresponding to low-energy structures is amplified. In QFold (particularly in the quantum circuit of Fig. 6), the reason the oracle is applied to $|CM\rangle = |00\rangle$ is that at every step of the walk, the auxiliary registers for coin and move are always reset to $|00\rangle$. In other words, since all update processes (the accept/reject decision) proceed starting from that state, the oracle operator for selecting "good structures" is also defined with respect to $|CM\rangle = |00\rangle$. Below, the actions of the coin operator and the oracle operator are explained. First, write the state as $|\phi\psi MC\rangle$: $$ |\psi_0\rangle = \frac{1}{2} \sum_{\phi,\psi \in {0,1}} |\phi\psi\rangle \otimes |M=0\rangle \otimes |C=0\rangle $$ Next, a rotation is given to the coin based on $\Delta E$. For example, when $M = 0$ (updating $\phi$): $$ |\phi\psi, M=0, C=0\rangle \longrightarrow \sqrt{1-A_\phi}|\phi\psi,0,0\rangle + \sqrt{A_\phi}|\phi\psi,0,1\rangle $$ Here, $A_\phi = \min(1, e^{-\beta \Delta E})$. Next, the coin operator is applied (Compute). If $C = 1$, $\phi$ is flipped: $$ |\phi\psi, M=0, C=1\rangle \to |(\phi \oplus 1)\psi, 0,1\rangle $$ If $C = 0$, nothing is done. Next, the Uncompute of the coin operator is performed. Then, by reversing the coin rotation, $C$ is reset to $0$: $$ \sqrt{1-A_\phi}|\phi\psi,0,0\rangle + \sqrt{A_\phi}|(\phi\oplus 1)\psi,0,0\rangle $$ As a result, the information of the rotation angle (which rotation angle was applied) remains only in $\phi\psi$, and $M, C$ return again to $|00\rangle$. Finally, by applying the oracle operator to the subspace $|MC\rangle = |00\rangle$, the information of the acceptance probability alone is marked: $$ |\phi\psi,00\rangle \mapsto -|\phi\psi,00\rangle $$ Therefore, by alternately applying the coin operator, the oracle operator, and the shift operator at each walk step, the information including the acceptance probability $\sqrt{A_\phi}$ is amplified, and by repeatedly implementing the quantum walk, the information reflecting the optimal structure is obtained. More detailed information is described in \[1]. In Algorithm 1 of the paper, in constructing the coin operator, energy-difference values that are close to each other are used. For $R_0$, "00001" and "01001" are used, and for $R_1$, "00101" and "10101" are used. One might think that it is not necessary to include other information, but by using only the information of $\phi$ and $\psi$, other rotation information (for example, $\ket{110}$) can be included through quantum parallelism arising from superposition. This can be expressed by the following code. ```python theme={null} import json import math def build_coin_prep_from_dataset(data: dict, beta: float = 1.0): delta_tbl = data["deltas"] # ΔE → θ def theta(dE: float) -> float: A = min(1.0, math.exp(-beta * dE)) return 2 * math.asin(math.sqrt(A)) # φ keys_phi = ["00001", "01001"] # ψ keys_psi = ["00101", "10101"] # average A A_phi = 0.5 * sum(min(1.0, math.exp(-beta * delta_tbl[k])) for k in keys_phi) A_psi = 0.5 * sum(min(1.0, math.exp(-beta * delta_tbl[k])) for k in keys_psi) # θ theta_phi = 2 * math.asin(math.sqrt(A_phi)) theta_psi = 2 * math.asin(math.sqrt(A_psi)) return theta_phi, theta_psi ``` ```python theme={null} angle_data = build_coin_prep_from_dataset(protein_data, beta=1.0) data_psi = angle_data[0] data_phi = angle_data[1] print(data_psi, data_phi) ``` **Output:** ``` 1.3721747807752471 4.9944226978996045e-06 ``` # ## Bulding Blocks ```python theme={null} from classiq import * @qfunc def coin_operator(C: QNum, M: QNum, psi: QNum, phi: QNum): X(C) H(M) """ R0: phi=0, M=0 """ control(((phi == 0) & (M == 0)), lambda: RY(data_phi, C)) temp1 = QNum("temp1") """ R1: phi=0, M=1 """ control(((psi == 0) & (M == 1)), lambda: RY(data_psi, C)) X(C) @qfunc def shift(C: QNum, M: QNum, psi: QNum, phi: QNum): control(((C == 1) & (M == 1)), lambda: X(psi)) control(((C == 1) & (M == 0)), lambda: X(phi)) @qfunc def oracle(C: QNum, M: QNum): for i in [C, M]: X(i) CZ(C, M) for i in [C, M]: X(i) @qfunc def initial_state(psi: QNum, phi: QNum): H(psi) H(phi) @qfunc def walk_operator(C: QNum, M: QNum, psi: QNum, phi: QNum): within_apply(lambda: coin_operator(C, M, psi, phi), lambda: shift(C, M, psi, phi)) oracle(C, M) @qfunc def main(phi: Output[QNum], psi: Output[QNum]): step = 1 qC = QNum("C") qM = QNum("M") allocate(1, qC) allocate(qbit_move_id, qM) allocate(len(phi_angles), phi) allocate(len(psi_angles), psi) initial_state(psi, phi) power(step, lambda: walk_operator(qC, qM, psi, phi)) drop(qC) drop(qM) ``` ```python theme={null} qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/36pnhFUPTpKeuu4jO6gPLZeWTV7 ``` ```python theme={null} result = execute(qprog).result_value() ``` ```python theme={null} result.parsed_counts ``` **Output:** ``` [{'phi': 1, 'psi': 1}: 608, {'phi': 0, 'psi': 1}: 594, {'phi': 0, 'psi': 0}: 446, {'phi': 1, 'psi': 0}: 400] ``` ```python theme={null} # extract the energy data from search result filtered_dict = {k: v for k, v in delta_tbl.items() if k.startswith("01")} print(filtered_dict) ``` **Output:** ``` {'01000': 0.8548282413310062, '01001': 0.8548282413310062, '01100': -25.86237460092684, '01101': -25.86237460092684} ``` We can see that the structure `01101` or `01100` has close to minimum energy of given protein. ## References \[1] [https://arxiv.org/pdf/2101.10279](https://arxiv.org/pdf/2101.10279) \[2] [https://github.com/awslabs/quantum-computing-exploration-for-drug-discovery-on-aws/tree/main/source/src/notebook/healthcare-and-life-sciences/c-1-protein-folding-quantum-random-walk](https://github.com/awslabs/quantum-computing-exploration-for-drug-discovery-on-aws/tree/main/source/src/notebook/healthcare-and-life-sciences/c-1-protein-folding-quantum-random-walk) # Quantum Phase Estimation (QPE) for Solving Molecular Energies Source: https://docs.classiq.io/explore/applications/chemistry/qpe_for_molecules/qpe_for_molecules Open this notebook in GitHub to run it yourself Quantum Phase Estimation (QPE) is a key algorithm in quantum computing for estimating the phase (or eigenvalue) of an eigenvector of a unitary operation. For a given Hamiltonian $H$ and an eigenvalue ${|\psi\rangle}$, the output of the algorithm is $\epsilon$ where $U{|\psi\rangle} = e^{2\pi i\epsilon}{|\psi\rangle} , U = e^{2\pi iH}$. Therefore, by measuring the accumulated phase, the QPE algorithm calculates the energies relating to the chosen initial state. When using QPE for chemistry problems, it is common to search for the lowest energy of a given molecule. As the molecule can be written in the form of a Hamiltonian (a Hermitian matrix representing the energetic forces of the structure), to obtain the minimal energy value using QPE, you only need to insert the ground eigenvector. However, obtaining the ground state is not a trivial problem. To overcome this, it is sufficient to use a state with big overlap with the ground state. Define a state ${|v\rangle}$ as the algorithm's initial state. Define \{$\psi_i$} as the set of (unknown) eigenvalues of $H$. Generally, any vector can be rewritten as a superposition of any basis set, thus ${|v\rangle} = \sum_i a_i{|\psi_i\rangle}$ and $U{|v\rangle} = \sum_i a_i e^{2\pi i\epsilon_i}{|\psi_i\rangle}$ where ${\epsilon_i}$ are the eigenvalues of $H$, i.e., the span of energies relating to the molecule. Using execution with enough shots, you obtain this set of $\epsilon_i$, i.e., a subset of the Hamiltonian's eigenvalues. As you are specifically interested in $\epsilon_0$, the ground state of $H$, it is important to have a large overlap between ${\psi_0}$ and ${|v\rangle}$ so the probability to measure ${\epsilon_0}$ is high, i.e., $P(\epsilon_0) = |\langle v|\psi_0\rangle|^2 > \zeta $. How large is $\zeta$? After execution, you obtain a set of ${E_i}$. If you have 1000 execution shots and $P(\epsilon_0)>1\%$, you should sample $\epsilon_0$ roughly 10 times. A common choice for ${|v\rangle}$ (the initial state) is the Hartree-Fock (HF) state, which typically has a large overlap with the ground state. However, other guesses for the initial state are possibly good or an even better fit, and choosing the right initial state is an art and an active field of research. To read more about QPE, refer to \[[1](#nc)]. **What are the benefits of using the QPE algorithm to find a molecule's ground state?** The two most prominent methods to solve ground energy for molecules are quantum variational algorithm (VQE) and QPE. They promise better scalability compared to their classical counterparts as the molecules become more complex, with a larger number of electrons, referring to a physical problem with more degrees of freedom. The number of parameters in VQE is closely related to the number of electrons. This may create an inherent difficulty achieving high-precision calculations through sampling statistical estimators, and may not even converge for very large systems. On the other hand, the number of parameters in QPE is a flexible value that is directly related to the resolution of the problem, but is not bounded with the number of electrons. Furthermore, it is known that advanced quantum algorithms based on QPE can perform electronic structure calculations in sub-exponential time with accuracy that rivals exact diagonalization methods. This guarantee of simultaneously achieving high accuracy, efficiency, and generality is a feat that is believed to be impossible for classical algorithms. For these reasons, VQE is applicable in the near term (NISQ) era, while QPE is suited for fault-tolerant design. **This tutorial follows the QPE algorithm steps as follows:** 1. Define a molecule and convert it into a Hamiltonian. 2. Prepare the Hamiltonian for QPE, including normalization and trimming of negligible terms. 3. Construct a quantum model, initializing the state for the HF state and leveraging the `qpe_flexible` function. 4. Execute the circuit to find the related phases and analyze the results to find the ground state. ```python theme={null} !pip install -qq "classiq[chemistry]" ``` ```python theme={null} ## Imports import matplotlib.pyplot as plt import numpy as np from classiq import * from classiq.applications.chemistry.op_utils import qubit_op_to_qmod # for chemistry from classiq.applications.chemistry.problems import FermionHamiltonianProblem from classiq.applications.chemistry.z2_symmetries import Z2SymTaperMapper ``` ## Defining a Molecule with Classiq This tutorial works with the LiH molecule: ```python theme={null} molecule_H2_geometry = [("H", (0.0, 0.0, 0)), ("H", (0.0, 0.0, 0.735))] molecule_O2_geometry = [("O", (0.0, 0.0, 0)), ("O", (0.0, 0.0, 1.16))] molecule_LiH_geometry = [("H", (0.0, 0.0, 0.0)), ("Li", (0.0, 0.0, 1.596))] molecule_H2O_geometry = [ ("O", (0.0, 0.0, 0.0)), ("H", (0, 0.586, 0.757)), ("H", (0, 0.586, -0.757)), ] molecule_BeH2_geometry = [ ("Be", (0.0, 0.0, 0.0)), ("H", (0, 0, 1.334)), ("H", (0, 0, -1.334)), ] molecule_geometry = molecule_LiH_geometry ``` ```python theme={null} from openfermion.chem import MolecularData from openfermionpyscf import run_pyscf geometry = molecule_H2_geometry basis = "sto-3g" # Basis set multiplicity = 1 # Singlet state S=0 charge = 0 # Neutral molecule molecule = MolecularData(molecule_geometry, basis, multiplicity, charge) molecule = run_pyscf( molecule, run_mp2=True, run_cisd=True, run_ccsd=True, run_fci=True, # relevant for small, classically solvable problems ) ``` ```python theme={null} # define your molecule problem and mapper problem = FermionHamiltonianProblem.from_molecule( molecule=molecule, first_active_index=1 ) mapper = Z2SymTaperMapper.from_problem(problem) num_qubits = mapper.get_num_qubits(problem) constant_energy = problem.fermion_hamiltonian.constant mol_hamiltonian = mapper.map(problem.fermion_hamiltonian - constant_energy) print( f"The Hamiltonian is defined on {num_qubits} qubits, and contains {len(mol_hamiltonian.terms)} Pauli strings" ) ``` **Output:** ``` The Hamiltonian is defined on 6 qubits, and contains 231 Pauli strings ``` Finally, we calculate the ground state energy as a reference solution to the quantum solver ```python theme={null} classical_sol = molecule.fci_energy print(f"Expected energy: {classical_sol} Ha") ``` **Output:** ``` Expected energy: -7.882386993638953 Ha ``` ## Preparing the Molecule for QPE # ## Trimming the Hamiltonian As you can see, the Hamiltonian may contain a large number of terms. In many cases you can compress the Hamiltonian by trimming small terms: ```python theme={null} coeffs = list(mol_hamiltonian.terms.values()) plt.semilogy(np.sort(np.abs(coeffs))[::-1], "o") plt.ylabel(r"$|\alpha_i|$", fontsize=16) plt.xlabel(r"$i$", fontsize=16) plt.tick_params(axis="both", labelsize=16) plt.title( r"Sorted coefficients size for Hamiltonian $H = \sum \alpha_i P_i$ ($P_i$ Pauli string)" ); ``` output Define a threshold and trim the Hamiltonian accordingly: ```python theme={null} THRESHOLD = 0.03 mol_hamiltonian.compress(THRESHOLD) print(f"Length of trimmed Hamiltonian: {len(mol_hamiltonian.terms)}") ``` **Output:** ``` Length of trimmed Hamiltonian: 49 ``` # ## Normalizing the Hamiltonian for QPE Since you are working with QPE, the ground state energy is inferred as a phase. Therefore, normalize the Hamiltonian so that its eigenvalues are in $\left[-\frac{1}{2},\frac{1}{2}\right)$. This is done by finding a bound on the maximal absolute value of eigenvalues $\tilde{\lambda}_{\max}$ and normalizing the Hamiltonian by $2\tilde{\lambda}_{\max}$. A simple bound is given by the sum of Pauli coefficients of the Hamiltonian: ```python theme={null} def normalize_hamiltonian(hamiltonian): approx_lambda_max = sum(np.abs(value) for value in hamiltonian.terms.values()) normalization = 2 * approx_lambda_max normalized_mol_hamiltonian = hamiltonian * (1 / normalization) return normalization, normalized_mol_hamiltonian normalization, normalized_mol_hamiltonian = normalize_hamiltonian(mol_hamiltonian) print(f"The normalization value of the Hamiltonian is {normalization}") ``` **Output:** ``` The normalization value of the Hamiltonian is 17.61546220154498 ``` ## Designing the Quantum Model # ## Defining a Powered Hamiltonian Simulation Create a quantum model of the QPE algorithm using the Classiq platform, in particular, using the open library [qpe\_flexible](https://github.com/Classiq/classiq-library/blob/main/functions/qmod_library_reference/classiq_open_library/qpe/qpe.ipynb) function (and see this [notebook](https://github.com/Classiq/classiq-library/blob/main/tutorials/advanced_tutorials/high_level_modeling_flexible_qpe/high_level_modeling_flexible_qpe.ipynb) as well). To approximate the Hamiltonian simulation $e^{2\pi i H}$, use the Classiq built-in implementation for [Suzuki-Trotter formulas](https://github.com/Classiq/classiq-library/blob/main/functions/qmod_library_reference/qmod_core_library/hamiltonian_evolution/suzuki_trotter/suzuki_trotter.ipynb). For a given Suzuki-Trotter order $o$, you can specify a repetition parameter $r$ that controls the level of approximation. The literature provides lower bounds for $r$ as a function of the operator error $\epsilon$ (defined by the diamond norm \[[3](#dimond)]). For example, Eq. (14) in Ref. \[[2](#bounds)] states that the Suzuki-Trotter formula of order 2 approximates $e^{i \sum \alpha_m P_m t}$ up to an error $\epsilon$, given $r$ repetitions that satisfies $$ r \geq \left(\frac{2^5\gamma_2}{3\epsilon}\right)^{1/2} t^{3/2}, \tag{1} $$ where $\gamma_2 \equiv \sum_{l,m,n} |\alpha_m\alpha_n\alpha_l| \left |\left[P_l,\left[P_m, P_n\right]\right]\right|_\infty$. **In particular, note that the number of repetitions grows as $t^{3/2}$**. In QPE, apply a powered Hamiltonian simulation: $$ \left(e^{2\pi i H}\right)^ p = e^{2p \pi i H}, \tag{2} $$ and approximate each power with Suzuki-Trotter for appropriate order and repetition parameters, keeping the same error per QPE iteration. You can thus use the bound above to define a powered Suzuki-Trotter `qfunc` for the specific molecule. First, define a classical auxiliary function that help evaluate the right-hand-side side of Eq. (1): ```python theme={null} from itertools import product def calculate_gamma_2(hamiltonian): """ Compute the $\gamma_2$ value appearing in the bound for Suzuki-Trotter of order 2. Uses the triangle inequality over Pauli coefficients to upper bound the spectral norm of each nested commutator (each Pauli string has unit norm). """ qmod_hamiltonian = qubit_op_to_qmod(hamiltonian) terms = [ SparsePauliOp(terms=[term], num_qubits=qmod_hamiltonian.num_qubits) for term in qmod_hamiltonian.terms ] return sum( sum( abs(term.coefficient) for term in commutator(P_l, commutator(P_m, P_n)).terms ) for P_l, P_m, P_n in product(terms, repeat=3) ) ``` In QPE, the power of the Hamiltonian simulation grows exponentially with the phase variable size. Examine the number of repetitions needed per QPE iteration, according to the bound above for QPE of size 7: ```python theme={null} QPE_SIZE = 7 qpe_powers = 2 ** np.arange(QPE_SIZE) print( f"""The power of the Hamiltonian simulation along a QPE routine of size {QPE_SIZE}: {qpe_powers}""" ) ``` **Output:** ``` The power of the Hamiltonian simulation along a QPE routine of size 7: [ 1 2 4 8 16 32 64] ``` These powers enter as an evolution coefficient for the Hamiltonian simulation (see Eq. (2) above). Using the theoretical bound, we find this: ```python theme={null} EPS = 0.1 gamma_2_LiH = calculate_gamma_2(normalized_mol_hamiltonian) theoretical_r0 = np.sqrt(2**5 * gamma_2_LiH / (3 * EPS)) * (2 * np.pi) ** (3 / 2) print( f"""The theoretical bounds for the repetitions for QPE size {QPE_SIZE}, keeping an error {EPS} per QPE iteration are: {np.ceil(theoretical_r0*qpe_powers**(3/2))}""" ) ``` **Output:** ``` The theoretical bounds for the repetitions for QPE size 7, keeping an error 0.1 per QPE iteration are: [ 11. 2 9. 8 2. 2 31. 6 51. 1 841. 5206.] ``` Note that applying a naive QPE, i.e., assuming a single unitary approximated with Suzuki-Trotter, $e^{iHt} \approx {\rm ST}(H, o, r ,t)$, and simply taking its powers, gives $\left(e^{iHt}\right)^p \approx \left({\rm ST}(H, o, r ,t)\right)^{p} = {\rm ST}(H, o, pr ,pt)$. ```python theme={null} print( f"""The repetitions for QPE size {QPE_SIZE}, taking a naive QPE, per QPE iteration: {np.ceil(theoretical_r0*qpe_powers)}""" ) ``` **Output:** ``` The repetitions for QPE size 7, taking a naive QPE, per QPE iteration: [ 11. 2 1. 4 1. 8 2. 1 63. 3 26. 651.] ``` While this naive QPE results in a shallower circuit, compared to taking repetitions according to the theoretical bounds (due to smaller values of repetitions), it is unclear whether it keeps the same operator error per phase bit. In practice, the bounds given in the literature are quite loose. This tutorial therefore takes a more experimental approach, assuming that the scaling of the bound with the evolution time $t$ is similar to Eq. (1), but taking a smaller prefactor. ```python theme={null} experimental_r0 = 0.05 print(f"""The experimental repetitions for QPE size {QPE_SIZE}, per QPE iteration are: {np.ceil(experimental_r0*qpe_powers**(3/2))}""") ``` **Output:** ``` The experimental repetitions for QPE size 7, per QPE iteration are: [ 1. 1. 1. 2. 4. 1 0. 26.] ``` Use this approach to define the powered Suzuki-Trotter function for the specific Hamiltonian at hand: ```python theme={null} from classiq.qmod.symbolic import ceiling as ceiling_qmod, pi @qfunc def powered_st2_for_LiH(p: CInt, state: QArray[QBit]): suzuki_trotter( pauli_operator=qubit_op_to_qmod(normalized_mol_hamiltonian), evolution_coefficient=-2 * np.pi * p, order=2, repetitions=ceiling_qmod(experimental_r0 * p ** (3 / 2)), qbv=state, ) ``` # ## Defining and Synthesizing the Phase Estimation Model ```python theme={null} from classiq.applications.chemistry.hartree_fock import get_hf_state hf_state = get_hf_state(problem, mapper) @qfunc def main( state: Output[QArray[QBit, num_qubits]], phase: Output[QNum[QPE_SIZE, SIGNED, QPE_SIZE]], ) -> None: prepare_basis_state(hf_state, state) allocate(phase) qpe_flexible(lambda p: powered_st2_for_LiH(p, state), phase) qmod = create_model( main, preferences=Preferences(timeout_seconds=600), ) qprog = synthesize(qmod) ``` ## Measurement and Analysis Execute on the default simulator: ```python theme={null} res = execute(qprog).result_value() ``` Draw a histogram for the energies by taking the output of the `phase` variable and multiplying back the normalization factor: ```python theme={null} phase_counts = res.parsed_counts_of_outputs("phase") num_shots = res.num_shots energy_results = { sample.state["phase"] * normalization + constant_energy: sample.shots / num_shots for sample in phase_counts } plt.plot(energy_results.keys(), energy_results.values(), "o") max_prob_energy = max(energy_results, key=energy_results.get) print(f"\nEnergy with maximal probability: {max_prob_energy} Ha") print(f"Precision: {(2**(-QPE_SIZE))* normalization} Ha") print(f"Classical solution:, {classical_sol} Ha") plt.xlabel("Energy (Ha)", fontsize=16) plt.ylabel("P(Energy)", fontsize=16) plt.tick_params(axis="both", labelsize=16) plt.title("Energy Histogram from QPE"); ``` **Output:** ``` Energy with maximal probability: -7.904148198365434 Ha Precision: 0.13762079844957015 Ha Classical solution:, -7.882386993638953 Ha ``` output You are looking for a signal from the smallest eigenvalue under the assumption that the initial state has some overlap with the ground state. Now, estimate the energy as the first peak of the histogram, such that the corresponding probability is larger than `ASSUMED_OVERLAP`\*0.4 (0.4 is the case for ASSUMED\_OVERLAP=1). \*Note that this is a very rough and simplistic analysis of the QPE algorithm result. You can utilize more complex spectral analysis tools such as Gaussian mixtures. Additional assumptions, such as the difference between adjacent eigenvalues or the number of overlapping eigenstates, can facilitate the analysis further.\* ```python theme={null} from scipy.signal import find_peaks ASSUMED_OVERLAP = 0.05 def estimate_energy(data_dict, assumed_overlap): max_prob = assumed_overlap * 0.4 data = tuple(data_dict.items()) data_sorted = sorted( data, key=lambda x: x[0] ) # sort the data according to the energy value probs_sorted = [data[1] for data in data_sorted] maxima = find_peaks(probs_sorted, height=max_prob)[0] print(f"Number of maxima: {maxima.size}") if maxima.size == 0 and np.all(np.array(probs_sorted) <= max_prob): print( """No probabilities above threshold were found, try to increase the assumed_overlap. Returning energy with max probability""" ) return max(data_dict, key=data_dict.get) elif maxima.size == 0: # strictly increasing or decreasing function return max(data_dict, key=data_dict.get) else: print( f"maxima over the threshold at {[data_sorted[maxima[k]][0] for k in range(maxima.size)]} Ha" ) return data_sorted[maxima[0]][0] measured_energy = estimate_energy(energy_results, ASSUMED_OVERLAP) print(f"\nLowest eigenvalue: {measured_energy} Ha") print(f"Precision: {(2**(-QPE_SIZE))* normalization} Ha") print(f"Classical solution:, {classical_sol} Ha") ``` **Output:** ``` Number of maxima: 1 maxima over the threshold at [-7.904148198365434] Ha Lowest eigenvalue: -7.904148198365434 Ha Precision: 0.13762079844957015 Ha Classical solution:, -7.882386993638953 Ha ``` ## References \[1] \[Michael A. Nielsen and Isaac L. Chuang. 2 11. Quantum Computation and Quantum Information: 10th Anniversary Edition, Cambridge University Press, New York, NY, USA. ]\([https://archive.org/details/QuantumComputationAndQuantumInformation10thAnniversaryEdition](https://archive.org/details/QuantumComputationAndQuantumInformation10thAnniversaryEdition)) \[2] \[M. Hagan and N. Wiebe. Composite Quantum Simulations, Quantum 7, 1881 (2023). ]\([https://quantum-journal.org/papers/q-2023-11-14-1181/](https://quantum-journal.org/papers/q-2023-11-14-1181/)) \[3] [Diamond Norm (Wikipedia). ](https://en.wikipedia.org/wiki/Diamond_norm) # Quantum Drude Oscillator Source: https://docs.classiq.io/explore/applications/chemistry/quantum_drude_oscillator/quantum_drude_oscillator Open this notebook in GitHub to run it yourself Intermolecular dispersion forces can be determined by calculating the interaction between two molecules; however, the total force does not simply triple when a third molecule is introduced. This phenomenon is referred to as Many-Body Dispersion (MBD). While it is desirable to evaluate dispersion forces using first-principles calculation methods, attempting to do so with multiple specific molecules results in an exorbitant increase in computational complexity. The Quantum Drude Oscillator (QDO) model \[[1](#jones2013)] is a coarse-grained model that describes "molecules interacting via dispersion forces" as "electrostatic interactions between QDOs." In this model, a QDO is defined as a harmonic oscillator consisting of a pair of fictitious positively and negatively charged particles connected by a spring. QDOs behave as bosons, and in this tutorial, we see how we can treat such bosonic system using classiq \[[2](#anderson2022)]. ```python theme={null} !pip install -qq "classiq[chemistry]" ``` ```python theme={null} import time from functools import reduce from operator import mul import matplotlib.pyplot as plt import numpy as np from classiq import * ``` ## One-Dimensional QDO Hamiltonian In this tutorial, we consider a non-dimensional Hamiltonian for $N$ one-dimensional QDOs which are interacting via quadratic coupling. $$ \def\H{\hat{H}} \def\x{\hat{x}} \def\p{\hat{p}} \def\bin{\operatorname{bin}} \def\a{\hat{a}} \def\aD{\hat{a}^\dagger} \H = \sum_{i=1}^N (\x_i^2 + \p_i^2) + \sum_{j>i} \gamma_{i,j} \x_i\x_j, $$ where $\x_i$ and $\p_i$ are the bosonic position and momentum operators for oscillator $i$ and $\gamma_{i,j}\in \mathbb{R}$ is the coupling between oscillators $i$ and $j$. For simplicity, we assume that all one-dimensional QDOs are identical and aligned parallel to each other and along the inter-oscillator axis, equally separated by distance $R$. In this case, the coupling constant is given by $\gamma_{i,j}=-4\alpha (R|i-j|)^{-3}$, where $\alpha$ is the dipole polarisability. We need to represent this Hamiltonian in terms of Pauli matrices to be able to implement on quantum circuit. Unlike fermions, bosonic Fock states cannot be represented by only zeros and ones. Therefore, when bosons occupy a certain quantum state, we need to encode this by representing the number $n$ as a binary string in the qubit state. In this tutorial, we denote a Fock state expressed in decimal as $|\underline{n}\rangle$ (with an underline), and a Fock state in binary representation as $|\bin(n)\rangle$ (without an underline). Here, $\bin(n)$ represents the binary representation of $n$, and $\bin(n)_i$ denotes the $i$-th bit from the right of the bitstring. When adopting this binary encoding, there is an upper limit to the number of bosons that can occupy the same quantum state. If we allocate $m$ qubits to the Fock state of each boson, the possible values range from 0 to $2^m - 1$. We define the number of levels we can handle as $d = 2^m$. $d$ must be large enough to make the result as reliable as possible. Although not used in this tutorial, Classiq's `QNum` could potentially handle such bosonic Fock states more intuitively. The bosonic position and momentum operators can be written using ladder operators as: $$ \begin{aligned} \x &= \frac{1}{\sqrt{2}}(\aD + \a), \\ \p &= -\frac{i}{\sqrt{2}}(\aD - \a), \end{aligned} $$ where the ladder operators are expressed using projection operators as follows: $$ \begin{aligned} \hat{a}^\dagger &= \sum_{n=0}^{d-1} \sqrt{n+1} |\underline{n+1}\rangle\langle\underline{n}|, \\ \hat{a} &= \sum_{n=0}^{d-1} \sqrt{n+1} |\underline{n}\rangle\langle\underline{n+1}|, \end{aligned} $$ What does this projection operator look like in binary representation? Let's take $|\underline{4}\rangle\langle\underline{3}|$ with a bit length of $m=4$ as an example. In this case, the projection operator is: $$ \begin{aligned} |\underline{4}\rangle\langle\underline{3}| &= |0100\rangle\langle 0011| \\ &= |0\rangle\langle 0| \otimes |1\rangle\langle 0| \otimes |0\rangle\langle 1|^{\otimes 2} \end{aligned} $$ As we can see, the operator $|\underline{n+1}\rangle\langle\underline{n}|$ can be divided into three parts: 1. For the bits where ones continue from the $2^0$ position, apply $|0\rangle\langle 1|$. 2. For the rightmost zero, apply $|1\rangle\langle 0|$. 3. For all other bits, there is no change, so apply $|0\rangle\langle 0|$ or $|1\rangle\langle 1|$. Therefore, $|\underline{n+1}\rangle\langle\underline{n}|$ can be expressed using Pauli matrices as: $$ \begin{aligned} |\underline{n+1}\rangle\langle\underline{n}| &= \left(\bigotimes_{i=k+1}^{m-1} |\text{bin}(n)_i\rangle\langle\text{bin}(n)_i| \right) \otimes |1\rangle\langle0| \otimes \left(|0\rangle\langle1|\right)^{\otimes k} \\ &= \left(\bigotimes_{i=k+1}^{m-1} \frac{I + (-1)^{\text{bin}(n)_i}Z}{2}\right) \otimes \frac{X-iY}{2} \otimes \left(\frac{X+iY}{2}\right)^{\otimes k}, \end{aligned} $$ where $k$ is the position of the rightmost 0 in $\bin(n)$. By noting the relationship $|\underline{n+1}\rangle\langle\underline{n}| = (|\underline{n}\rangle\langle\underline{n+1}|)^\dagger$, we can write the QDO Hamiltonian in terms of Pauli matrices. The following code generates the coupling terms for this Hamiltonian. ```python theme={null} def get_ith_bit(n, i): """Get the i-th bit of an integer. Args: n (int): Input integer. i (int): Bit position to retrieve. Returns: int: The i-th bit (0 or 1). """ return (n >> i) & 1 ``` ```python theme={null} def get_rightmost_zero_pos(n): """Get the position of the rightmost '0' in an integer. Args: n (int): Input integer. Returns: int: Position of the rightmost '0'. """ return (~n & (-~n)).bit_length() - 1 ``` ```python theme={null} def create_ladder_projector(n: int, qubits: list[int], direction: str) -> SparsePauliOp: """Create ladder projector operator for given occupation number. 'up': |n+1><2| = |11><10| for 2 qubits expected = (Pauli.I(1) - Pauli.Z(1)) / 2 * (Pauli.X(0) - 1j * Pauli.Y(0)) / 2 actual = create_ladder_projector(2, [0, 1], "up") print("Is the created ladder projector correct?", expected == actual) print( "Are |3><2| and |2><3| Hermitian conjugates?", ( hamiltonian_to_matrix(create_ladder_projector(2, [0, 1], "up")).conj().T == hamiltonian_to_matrix(create_ladder_projector(2, [0, 1], "down")) ).all(), ) ``` **Output:** ``` Is the created ladder projector correct? True Are |3><2| and |2><3| Hermitian conjugates? True ``` ```python theme={null} def create_x_operator(qubits): r"""Create $\hat{x}$ operator for given number of qubits. Args: qubits (list[int]): List of qubit indices. Returns: SparsePauliOp: $\hat{x}$ operator. """ ladder_ops = [] for n in range(2 ** len(qubits) - 1): ladder_ops.append( create_ladder_projector(n=n, qubits=qubits, direction="up") * np.sqrt((n + 1) / 2) ) ladder_ops.append( create_ladder_projector(n=n, qubits=qubits, direction="down") * np.sqrt((n + 1) / 2) ) return reduce(lambda x, y: x + y, ladder_ops) ``` Now, let's verify that the coupling terms between oscillator 1 and oscillator 2 are expressed as follows when the number of qubits $m$ assigned to each QDO's Fock state is $m=1$ and $m=2$. $$ \def\termplus{\frac{\sqrt{3}+1}{4\sqrt{2}}} \def\termminus{\frac{\sqrt{3}-1}{4\sqrt{2}}} \def\x{\hat{x}} \begin{split} \x_1\x_2\rvert_{m=1} = \frac{1}{2}X_1X_2, \end{split} $$ $$ \def\x{\hat{x}} \begin{split} \x_1\x_2 \lvert_{m=2} = & \frac{\sqrt{3}+2}{4}X_1X_3 + \termplus X_1X_2X_3 + \termplus X_1X_3X_4 +\frac{1}{4}X_1X_2X_3X_4 + \termplus Y_1Y_2X_3 +\frac{1}{4}Y_1Y_2X_3X_4 \\ & + \termplus X_1Y_3Y_4 +\frac{1}{4}X_1X_2Y_3Y_4 - \termminus Y_1Y_2X_3Z_4 -\termminus X_1Z_2Y_3Y_4 - \frac{1}{4}X_1Z_2X_3 \\ &- \termminus X_1Z_2X_3X_4 - \frac{1}{4} X_1X_3Z_4 - \termminus X_1X_2X_3Z_4 + \frac{2-\sqrt{3}}{4}X_1Z_2X_3Z_4 +\frac{1}{4}Y_1Y_2Y_3Y_4. \end{split} $$ ```python theme={null} x1 = create_x_operator([0]) x2 = create_x_operator([1]) x1x2 = x1 * x2 x1x2_analytical = Pauli.X(0) * Pauli.X(1) / 2 print( "Are the numerically constructed and analytically derived x1x2 operators (m = 1) close?", np.allclose(hamiltonian_to_matrix(x1x2), hamiltonian_to_matrix(x1x2_analytical)), ) ``` **Output:** ``` Are the numerically constructed and analytically derived x1x2 operators (m = 1) close? True ``` ```python theme={null} x1 = create_x_operator([0, 1]) x2 = create_x_operator([2, 3]) x1x2 = x1 * x2 term_plus = (np.sqrt(3) + 1) / (4 * np.sqrt(2)) term_minus = (np.sqrt(3) - 1) / (4 * np.sqrt(2)) x1x2_analytical = ( (np.sqrt(3) + 2) / 4 * Pauli.X(0) * Pauli.X(2) + term_plus * Pauli.X(0) * Pauli.X(1) * Pauli.X(2) + term_plus * Pauli.X(0) * Pauli.X(2) * Pauli.X(3) + 1 / 4 * Pauli.X(0) * Pauli.X(1) * Pauli.X(2) * Pauli.X(3) + term_plus * Pauli.Y(0) * Pauli.Y(1) * Pauli.X(2) + 1 / 4 * Pauli.Y(0) * Pauli.Y(1) * Pauli.X(2) * Pauli.X(3) + term_plus * Pauli.X(0) * Pauli.Y(2) * Pauli.Y(3) + 1 / 4 * Pauli.X(0) * Pauli.X(1) * Pauli.Y(2) * Pauli.Y(3) - term_minus * Pauli.Y(0) * Pauli.Y(1) * Pauli.X(2) * Pauli.Z(3) - term_minus * Pauli.X(0) * Pauli.Z(1) * Pauli.Y(2) * Pauli.Y(3) - 1 / 4 * Pauli.X(0) * Pauli.Z(1) * Pauli.X(2) - term_minus * Pauli.X(0) * Pauli.Z(1) * Pauli.X(2) * Pauli.X(3) - 1 / 4 * Pauli.X(0) * Pauli.X(2) * Pauli.Z(3) - term_minus * Pauli.X(0) * Pauli.X(1) * Pauli.X(2) * Pauli.Z(3) + (2 - np.sqrt(3)) / 4 * Pauli.X(0) * Pauli.Z(1) * Pauli.X(2) * Pauli.Z(3) + 1 / 4 * Pauli.Y(0) * Pauli.Y(1) * Pauli.Y(2) * Pauli.Y(3) ) print( "Are the numerically constructed and analytically derived x1x2 operators (m = 2) close?", np.allclose(hamiltonian_to_matrix(x1x2), hamiltonian_to_matrix(x1x2_analytical)), ) ``` **Output:** ``` Are the numerically constructed and analytically derived x1x2 operators (m = 2) close? True ``` The non-interacting terms of the QDO Hamiltonian can be reduced to a simplified form as follows: $$ \begin{aligned} \hat{x}^2 + \hat{p}^2 &= \frac{(\hat{a}^\dagger + \hat{a})^2}{2} - \frac{(\hat{a}^\dagger - \hat{a})^2}{2} \\ &= \hat{a}^\dagger \hat{a} + \hat{a} \hat{a}^\dagger \\ &= 2 \hat{a}^\dagger \hat{a} + 1 \\ &= 2 \left(\sum_{n=0}^{d-1} (n+1) |\underline{n+1}\rangle\langle\underline{n+1}| \right) + 1 \\ &= 2 \left(\sum_{j=0}^{m-1} 2^j |1\rangle\langle 1|_j \right) + 1\\ &= \sum_{j=0}^{m-1} 2^j (I_j - Z_j) + 1\\ &= 2^m I^{\otimes m} - \sum_{j=0}^{m-1} 2^j Z_j \end{aligned} $$ Note that the number operator $\hat{a}^\dagger \hat{a}$, defined by its action $\hat{a}^\dagger \hat{a}|n\rangle = n|n\rangle$, is diagonal in the Fock basis. ```python theme={null} def get_noninteractiog_qdo_hamiltonian( num_qdos: int, num_qubits_per_qdo: int, ) -> SparsePauliOp: """Construct the non-interacting Quantum Drude Oscillator (QDO) Hamiltonian. Args: num_qdos (int): Number of QDOs. num_qubits_per_qdo (int): Number of qubits per QDO. Returns: SparsePauliOp: The non-interacting QDO Hamiltonian as a SparsePauliOp. """ hamiltonian = [] for qdo_index in range(num_qdos): qubits = np.arange( qdo_index * num_qubits_per_qdo, (qdo_index + 1) * num_qubits_per_qdo ) identity = reduce(mul, [Pauli.I(i) for i in qubits]) hamiltonian.append(2**num_qubits_per_qdo * identity) for i in range(num_qubits_per_qdo): hamiltonian.append(-(2**i) * Pauli.Z(qubits[i])) return reduce(lambda x, y: x + y, hamiltonian) ``` With the code above, we are now ready to generate the QDO Hamiltonian. ```python theme={null} def get_hamiltonian( num_qdos: int, num_qubits_per_qdo: int, coupling_constants: list[list[float]] ) -> SparsePauliOp: assert np.array(coupling_constants).shape == (num_qdos, num_qdos) hamiltonian = [ get_noninteractiog_qdo_hamiltonian( num_qdos=num_qdos, num_qubits_per_qdo=num_qubits_per_qdo, ) ] for j in range(num_qdos): for i in range(j): xi = create_x_operator( qubits=list(range(i * num_qubits_per_qdo, (i + 1) * num_qubits_per_qdo)) ) xj = create_x_operator( qubits=list(range(j * num_qubits_per_qdo, (j + 1) * num_qubits_per_qdo)) ) hamiltonian.append(coupling_constants[i][j] * xi * xj) return reduce(lambda x, y: x + y, hamiltonian) def calculate_coupling_constants( polarizability: float, distances: list[list[float]], ) -> list[list[float]]: """Calculate coupling constants for given parameters. Args: polarizability (float): Polarizability. distances (list[list[float]]): Distances between QDOs. Returns: list[list[float]]: Coupling constants matrix. """ # polarizability = effective_charge**2 / (effective_mass * frequency**2) num_qdos = len(distances) coupling_constants = [[0.0 for _ in range(num_qdos)] for _ in range(num_qdos)] for i in range(num_qdos): for j in range(i): coupling_constants[i][j] = -4 * polarizability / distances[i][j] ** 3 coupling_constants[j][i] = coupling_constants[i][j] return coupling_constants ``` ## Calculate the Ground State Energy of One-Dimensional QDO Hamiltonian Here, we compute the ground state energy using the Variational Quantum Eigensolver (VQE). ```python theme={null} @qfunc def ansatz_layer(params: CArray[CReal, 4], state: QArray): RZ(np.pi / 2, state[0]) RZ(np.pi / 2, state[1]) RY(np.pi / 2, state[1]) CX(ctrl=state[1], target=state[0]) RY(params[0], state[0]) RZ(params[1], state[0]) RY(params[2], state[1]) RZ(params[3], state[1]) CX(ctrl=state[1], target=state[0]) RY(-np.pi / 2, state[1]) RZ(-np.pi / 2, state[0]) RZ(-np.pi / 2, state[1]) ``` ```python theme={null} # Test the ansatz and Hamiltonian construction vqe_hamiltonian_test = get_hamiltonian( num_qdos=2, num_qubits_per_qdo=2, coupling_constants=calculate_coupling_constants( polarizability=14.5, distances=[[0.0, 3.0], [3.0, 0.0]] ), ) num_params = 4 * 3 # 3 layers, each with 4 parameters @qfunc def main(params: CArray[CReal, num_params], state: Output[QArray]): allocate(4, state) param_idx = 0 for q1, q2 in [[0, 1], [2, 3], [1, 2]]: ansatz_layer(params[param_idx : param_idx + 4], [state[q1], state[q2]]) param_idx += 4 # Synthesize qprog = synthesize(main) qprog = set_quantum_program_execution_preferences( qprog, preferences=ExecutionPreferences( num_shots=1, backend_preferences=ClassiqBackendPreferences( backend_name="simulator_statevector" ), ), ) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3AelNrcQSJdeHxmkmeD8D7kS0YD ``` ```python theme={null} # Test VQE with ExecutionSession(qprog) as es: test_result = es.minimize( cost_function=vqe_hamiltonian_test, initial_params={"params": [1.0] * num_params}, max_iteration=500, ) ``` ```python theme={null} print(f"VQE Energy: {test_result[-1][0]}") vqe_test_results = {k: np.real(test_result[k][0]) for k in range(len(test_result))} plt.plot(vqe_test_results.keys(), vqe_test_results.values(), "-") plt.ylabel(r"Ground state energy $E$ [$\frac{1}{2} \hbar \omega$]") plt.xlabel("Iteration") plt.tick_params(axis="both") plt.grid() plt.show() ``` **Output:** ``` VQE Energy: 1.5542974087915205 ``` output Let's attempt to reproduce Fig. 1 from \[[2](#anderson2022)]. We will model $\mathrm{I_2}$ molecules as one-dimensional QDOs and calculate how the London dispersion energy $\Delta E$ changes according to the distance between the two parallel oscillators. We define the parameters such that the axial polarizability is $\alpha = 14.5 \ \mathrm{\AA^3}$ and the frequency is $\hbar \omega / 2 = 9.61 \ \mathrm{eV}$. ```python theme={null} num_qdos = 2 num_qubits_per_qdo = 2 polarizability = 14.5 # Å^3 hbar_omega_half = 9.61 # eV sample_distances = np.linspace(3.3, 5.5, 5) print(sample_distances) ``` **Output:** ``` [3.3 3.85 4.4 4.95 5.5 ] ``` ```python theme={null} durations = [] VQE_energy = [] for interoscillator_distance in sample_distances: time1 = time.time() # Construct a model distances = np.zeros((num_qdos, num_qdos)) for i in range(num_qdos): for j in range(num_qdos): distances[i][j] = abs(i - j) * interoscillator_distance vqe_hamiltonian = get_hamiltonian( num_qdos=num_qdos, num_qubits_per_qdo=num_qubits_per_qdo, coupling_constants=calculate_coupling_constants( polarizability=polarizability, distances=distances, ), ) with ExecutionSession(qprog) as es: result = es.minimize( cost_function=vqe_hamiltonian, initial_params={"params": [1.0] * num_params}, max_iteration=500, ) VQE_energy.append(result[-1][0]) time2 = time.time() duration = time2 - time1 durations.append(duration) print(f"Distance: {interoscillator_distance:.4f}, Duration: {duration:.4f} seconds") ``` **Output:** ``` Distance: 3.3000, Duration: 9.9883 seconds Distance: 3.8500, Duration: 9.3966 seconds Distance: 4.4000, Duration: 9.7202 seconds Distance: 4.9500, Duration: 13.0006 seconds Distance: 5.5000, Duration: 9.9909 seconds ``` The theoretical value of the London dispersion force in this case can be calculated as follows. The Hamiltonian is: $$ \def\Cvv{C_{\parallel,\parallel}} \def\Cvh{C_{\parallel, \perp}} \def\Chh{C_{\perp, \perp}} \H = \frac{\hat{p}_1^2}{2\mu} + \frac{\hat{p}_2^2}{2\mu} + \frac{1}{2}\mu\omega^2(\hat{x}_1^2+\hat{x}_2^2 + \gamma \hat{x}_1 \hat{x}_2), $$ where $\mu$ is the effective mass and $\gamma = -4\alpha R^{-3}$. By defining the transformed variables $$ \hat{p}_\pm = \frac{1}{\sqrt{2}}(\hat{p}_1 \pm \hat{p}_2), \qquad \hat{x}_\pm = \frac{1}{\sqrt{2}}(\hat{x}_1 \pm \hat{x}_2), $$ the Hamiltonian can be rewritten in this new uncoupled basis as $$ \H = \left(\frac{\hat{p}_+^2}{2\mu} + \frac{1}{2}\mu\omega^2\left(1+\frac{\gamma}{2}\right)\hat{x}_+^2\right) +\left(\frac{\hat{p}_-^2}{2\mu} + \frac{1}{2}\mu\omega^2\left(1-\frac{\gamma}{2}\right)\hat{x}_-^2\right). $$ Therefore, the ground state energy is given by $$ E = \frac{1}{2}\hbar \omega \left(\sqrt{1+\frac{\gamma}{2}} + \sqrt{1-\frac{\gamma}{2}}\right). $$ In the absence of the London dispersion force - specifically, when the distance between the oscillators is $R = \infty$ - we have $\gamma = 0$, which yields $E = \hbar \omega$. From this, the London dispersion energy can be calculated as: $$ \Delta E = \frac{1}{2}\hbar \omega \left(\sqrt{1+\frac{\gamma}{2}} + \sqrt{1-\frac{\gamma}{2}} - 2\right). $$ Below, we will confirm that VQE can obtain energy values close to this exact solution regardless of the distance between oscillators. ```python theme={null} interoscillator_distances = np.arange(3.1, 6.1, 0.2) gamma = -4 * polarizability / interoscillator_distances**3 exact_energies = np.sqrt(1 + gamma / 2) + np.sqrt(1 - gamma / 2) ``` Now, let's compare the London dispersion energy with the results obtained from the VQE. ```python theme={null} # R = ∞ r_inf_energy = 2.0 plt.plot( interoscillator_distances, (exact_energies - r_inf_energy) * hbar_omega_half, "k-", label="Exact solution", ) plt.plot( sample_distances, (np.array(VQE_energy) - r_inf_energy) * hbar_omega_half, "bs", label="Classiq VQE", ) plt.hlines( 0, xmin=interoscillator_distances[0], xmax=interoscillator_distances[-1], colors="r", linestyles="--", label=r"$R = \infty$ energy", ) plt.xlabel("Distance $R$ [Å]") plt.ylabel(r"London dispersion energy $\Delta E$ [eV]") plt.legend() plt.ylim(-2.2, 0.2) plt.xlim(3.0, 6.0) plt.grid() plt.show() ``` output From this graph, we can see that the VQE results yield energy values close to the analytical solution we calculated earlier. Next, let's investigate how $\Delta E$ changes with respect to the number of qubits $m$ (and the corresponding dimension $d$) assigned to each QDO. ```python theme={null} def get_energies( interoscillator_distances: list[float], num_qubits_per_qdo: int, num_qdos: int = 2 ) -> list[float]: energies = [] for interoscillator_distance in interoscillator_distances: distances = np.zeros((num_qdos, num_qdos)) for i in range(num_qdos): for j in range(num_qdos): distances[i][j] = abs(i - j) * interoscillator_distance vqe_hamiltonian = get_hamiltonian( num_qdos=num_qdos, num_qubits_per_qdo=num_qubits_per_qdo, coupling_constants=calculate_coupling_constants( polarizability=polarizability, distances=distances, ), ) ham_matrix = hamiltonian_to_matrix(vqe_hamiltonian) energies.append(np.linalg.eigvalsh(ham_matrix).min()) return np.array(energies) energies_d_2 = get_energies(interoscillator_distances, num_qubits_per_qdo=1) energies_d_4 = get_energies(interoscillator_distances, num_qubits_per_qdo=2) energies_d_8 = get_energies(interoscillator_distances, num_qubits_per_qdo=3) energies_d_16 = get_energies(interoscillator_distances, num_qubits_per_qdo=4) ``` ```python theme={null} plt.plot( interoscillator_distances, (energies_d_2 - r_inf_energy) * hbar_omega_half, "bs-", label="$d = 2$", ) plt.plot( interoscillator_distances, (energies_d_4 - r_inf_energy) * hbar_omega_half, "g^-", label="$d = 4$", ) plt.plot( interoscillator_distances, (energies_d_8 - r_inf_energy) * hbar_omega_half, "co-", label="$d = 8$", ) plt.plot( interoscillator_distances, (energies_d_16 - r_inf_energy) * hbar_omega_half, "m*-", label="$d = 16$", ) plt.plot( interoscillator_distances, (exact_energies - r_inf_energy) * hbar_omega_half, "k-", label="Exact solution", ) plt.xlabel("Distance $R$ [Å]") plt.ylabel(r"London dispersion energy $\Delta E$ [eV]") plt.legend() plt.ylim(-2.2, 0.2) plt.xlim(3.0, 6.0) plt.grid() plt.show() ``` output Looking at this graph, we can see that the accuracy of the calculated energy increases as more qubits are used for each QDO. The impact of $m$ (or $d$) on accuracy becomes more apparent as we vary the coupling constant $\gamma$. ```python theme={null} gamma_list = -np.linspace(0.01, 2.0, 15) interoscillator_distances_2 = (-4 * polarizability / gamma_list) ** (1 / 3) exact_gs_energies = np.sqrt(1 + gamma_list / 2) + np.sqrt(1 - gamma_list / 2) energies_d_2 = get_energies(interoscillator_distances_2, num_qubits_per_qdo=1) energies_d_4 = get_energies(interoscillator_distances_2, num_qubits_per_qdo=2) energies_d_8 = get_energies(interoscillator_distances_2, num_qubits_per_qdo=3) energies_d_16 = get_energies(interoscillator_distances_2, num_qubits_per_qdo=4) ``` ```python theme={null} plt.plot(-gamma_list, energies_d_2, "bs-", label="$d = 2$") plt.plot(-gamma_list, energies_d_4, "gs-", label="$d = 4$") plt.plot(-gamma_list, energies_d_8, "c^-", label="$d = 8$") plt.plot(-gamma_list, energies_d_16, "m*-", label="$d = 16$") plt.plot(-gamma_list, exact_gs_energies, "k-", label="Exact solution") plt.xlabel(r"Negative of coupling constant $-\gamma$ [eV/Å$^3$]") plt.ylabel(r"Ground state energy $E$ [$\frac{1}{2} \hbar \omega$]") plt.legend() plt.ylim(1.39, 2.01) plt.xlim(-0.1, 2.1) plt.grid() plt.show() ``` output This graph makes it clearer that higher values of $d$ lead to greater accuracy. We can see that $d = 4$ is a good approximation to the exact solution until very large couplings $\gamma$. For reference, $-\gamma = 2$ corresponds to a Hamiltonian that is no longer positive-semidefinite, at this point the charged Drude particles dissociate from the nuclei and the system breaks down. For physically relevant systems, we expect to require value of $-\gamma$ much lower than 2, and we can therefore limit ourselves to small $d$ \[[2](#anderson2022)]. ## Calculating Many-Body Dispersion Effect Now that we know the exact London dispersion energy of two identical parallel QDOs along the inter-oscillator axis is $$ \Delta E = \frac{1}{2}\hbar \omega \left(\sqrt{1+\frac{\gamma}{2}} + \sqrt{1-\frac{\gamma}{2}} - 2\right), $$ where $\gamma = -4\alpha R^{-3}$, we might expect that adding a third QDO would result in a simple summation of these pairwise interactions. Under this assumption, the total dispersion energy for a linear chain of three QDOs (with a distance $R$ between neighbors) would be given by: $$ \Delta E = \hbar \omega \left(\sqrt{1+\frac{\gamma_1}{2}} + \sqrt{1-\frac{\gamma_1}{2}} - 2\right) + \frac{1}{2}\hbar \omega \left(\sqrt{1+\frac{\gamma_2}{2}} + \sqrt{1-\frac{\gamma_2}{2}} - 2\right), $$ where $\gamma_1 = -4\alpha R^{-3},\ \gamma_2 = -4\alpha (2R)^{-3}$. Let's compare this to the results obtained by diagonalizing the Hamiltonian for $d=4$. ```python theme={null} gamma_1_list = -4 * polarizability / interoscillator_distances**3 gamma_2_list = -4 * polarizability / (2 * interoscillator_distances) ** 3 deltae_two_body = 2 * ( np.sqrt(1 + gamma_1_list / 2) + np.sqrt(1 - gamma_1_list / 2) - 2.0 ) + (np.sqrt(1 + gamma_2_list / 2) + np.sqrt(1 - gamma_2_list / 2) - 2.0) deltae_three_body = ( get_energies(interoscillator_distances, num_qubits_per_qdo=2, num_qdos=3) - 3.0 ) ``` Let's also try computing the energy by VQE using the following ansatz. ```python theme={null} num_params = 4 * 10 # 10 layers, each with 4 parameters @qfunc def main(params: CArray[CReal, num_params], state: Output[QArray]): allocate(6, state) param_idx = 0 for q1, q2 in [ [0, 1], [2, 3], [4, 5], [1, 2], [3, 4], [0, 1], [2, 3], [4, 5], [1, 2], [3, 4], ]: ansatz_layer(params[param_idx : param_idx + 4], [state[q1], state[q2]]) param_idx += 4 # Synthesize qprog_2 = synthesize(main) qprog_2 = set_quantum_program_execution_preferences( qprog_2, preferences=ExecutionPreferences( num_shots=1, backend_preferences=ClassiqBackendPreferences( backend_name="simulator_statevector" ), ), ) show(qprog_2) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3AelZ149PJOjgpwrFi3HO2IV38D ``` ```python theme={null} num_qdos = 3 num_qubits_per_qdo = 2 sample_distances = np.linspace(3.1, 5.5, 5) print(sample_distances) ``` **Output:** ``` [3.1 3.7 4.3 4.9 5.5] ``` ```python theme={null} durations2 = [] VQE_energy2 = [] for interoscillator_distance in sample_distances: time1 = time.time() # Construct a model distances = np.zeros((num_qdos, num_qdos)) for i in range(num_qdos): for j in range(num_qdos): distances[i][j] = abs(i - j) * interoscillator_distance vqe_hamiltonian = get_hamiltonian( num_qdos=num_qdos, num_qubits_per_qdo=num_qubits_per_qdo, coupling_constants=calculate_coupling_constants( polarizability=polarizability, distances=distances, ), ) with ExecutionSession(qprog_2) as es: result = es.minimize( cost_function=vqe_hamiltonian, initial_params={"params": [1.0] * num_params}, max_iteration=500, ) VQE_energy2.append(result[-1][0]) time2 = time.time() duration = time2 - time1 durations2.append(duration) print(f"Distance: {interoscillator_distance:.4f}, Duration: {duration:.4f} seconds") ``` **Output:** ``` Distance: 3.1000, Duration: 17.1934 seconds Distance: 3.7000, Duration: 24.3041 seconds Distance: 4.3000, Duration: 23.4690 seconds Distance: 4.9000, Duration: 17.1903 seconds Distance: 5.5000, Duration: 17.5069 seconds ``` ```python theme={null} plt.plot( interoscillator_distances, deltae_two_body * hbar_omega_half, "bs-", label="pairwise interactions only", ) plt.plot( sample_distances, (np.array(VQE_energy2) - 3.0) * hbar_omega_half, "g^-", label="$d = 4$ Hamiltonian (VQE)", ) plt.plot( interoscillator_distances, deltae_three_body * hbar_omega_half, "co-", label="$d = 4$ Hamiltonian (diagonalization)", ) plt.hlines( 0, xmin=interoscillator_distances[0], xmax=interoscillator_distances[-1], colors="r", linestyles="--", label=r"$R = \infty$ energy", ) plt.xlabel("Distance $R$ [Å]") plt.ylabel(r"London dispersion energy $\Delta E$ [eV]") plt.legend() plt.ylim(-20.0, 0.5) plt.xlim(3.0, 6.0) plt.grid() plt.show() ``` output From this plot, we can see that diagonalizing the Hamiltonian for $d=4$ yields a larger energy shift at short distances between QDOs than when considering only two-body pairwise interactions. This suggests that many-body effects are playing a role. ## References \[1]: [A. P. Jones, J. Crain, V. P. Sokhan, T. W. Whitfield, and G. J. Martyna, Quantum Drude Oscillator Model of Atoms and Molecules: Many-Body Polarization and Dispersion Interactions for Atomistic Simulation, Phys. Rev. B 87, 144103 (2013).](https://doi.org/10.1103/PhysRevB.87.144103) \[2]: [L. W. Anderson, M. Kiffner, P. K. Barkoutsos, I. Tavernelli, J. Crain, and D. Jaksch, Coarse-Grained Intermolecular Interactions on Quantum Processors, Phys. Rev. A 105, 062409 (2022).](https://doi.org/10.1103/PhysRevA.105.062409) # Continuous-Time Quantum Walk in Photosynthetic Energy Transfer Source: https://docs.classiq.io/explore/applications/chemistry/quantum_walk_fmo/quantum_walk_fmo Open this notebook in GitHub to run it yourself ```python theme={null} !pip install -qq "classiq[chemistry]" ``` ```python theme={null} import time from functools import reduce from operator import mul import matplotlib.pyplot as plt import numpy as np from scipy.linalg import expm from classiq import * ``` ## Continuous-Time Quantum Walk on a Graph Let $G(V, E)$ be a graph where $V$ is the vertex set and $E$ is the edge set, with $N$ representing the number of vertices ($|V| = N$). In this model, a walker diffuses across the vertices of the graph via its edges. The distribution of the walker is represented as a wavefunction $|\psi(t)\rangle$ in an $N$-dimensional Hilbert space spanned by the basis $\{|0\rangle, |1\rangle, \dots, |N-1\rangle\}$. The probability of finding the walker at vertex $v_j \in V$ is given by: $$ p_j = \left|\langle j|\psi(t)\rangle\right|^2 $$ Given an initial state $|\psi(0)\rangle$, the time evolution of the continuous-time quantum walk is determined by: $$ |\psi(t)\rangle = e^{-iHt} |\psi(0)\rangle $$ Here, the Hamiltonian $H$ is defined using the adjacency matrix $A$ of the graph as $H = -\gamma A$, where $\gamma > 0$ is a constant representing the transmission rate of the walker \[[1](#portugal2025)]. ## Frenkel Exciton Hamiltonian $$ \def\H{\hat{H}} \def\x{\hat{x}} \def\p{\hat{p}} \def\bin{\operatorname{bin}} \def\a{\hat{a}} \def\aD{\hat{a}^\dagger} $$ Photosynthesis is the process by which plants use light energy to synthesize organic compounds from carbon dioxide and water. During this process, pigment-protein complexes known as light-harvesting complexes absorb light energy or receive it from other complexes, transporting this excitation energy to a reaction center. The most critical aspect of this process is the energy transfer mechanism. It is experimentally known that quantum interference plays a role in this mechanism, and one of the light-harvesting complexes where such phenomena have been observed is the Fenna-Matthews-Olson (FMO) complex \[[2](#mohseni2008)]. The FMO complex has a trimeric structure, with each subunit containing eight Bacteriochlorophyll (BChl) *a* pigments. Seven of these are strongly bound within the protein scaffold and interact with each other due to their arrangement within the scaffold \[[3](#maiuri2018)]. It is well established that the optical spectra of the FMO complex are primarily determined by the interactions within a single subunit \[[4](#cho2005)]. Let us describe the movement of these excitations through Hamiltonian time evolution. Consider the ground state of the system as $|\psi_0\rangle$ and the excited state localized on a single pigment (a Frenkel exciton) as $|m\rangle = \aD_m |\psi_0\rangle$. In this framework, the Frenkel exciton Hamiltonian for the FMO complex is defined as follows: $$ H = \sum_{m=1}^N \varepsilon_m \aD_m \a_m + \sum_{n < m}^N J_{mn} (\aD_m \a_n + \aD_n \a_m), $$ where $\aD_m$ and $\a_m$ are the creation and annihilation operators for electronic excitation at the $m$-th pigment, $N$ is the number of pigments, $\varepsilon_m$ is the site energy of pigment $m$, $J_{mn}$ is the coupling constant between pigments $m$ and $n$. In this notebook, we will perform a continuous-time quantum walk using the Frenkel exciton Hamiltonian given in \[[4](#cho2005)]. ```python theme={null} fmo_hamiltonian = np.array( [ [280, -106, 8, -5, 6, -8, -4], [-106, 420, 28, 6, 2, 13, 1], [8, 28, 0, -62, -1, -9, 17], [-5, 6, -62, 175, -70, -19, -57], [6, 2, -1, -70, 320, 40, -2], [-8, 13, -9, -19, 40, 360, 32], [-4, 1, 17, -57, -2, 32, 260], ], dtype=float, ) # unit: cm^-1 ``` The aforementioned Hamiltonian can also be written as follows: $$ H = \sum_{m=1}^N \varepsilon_m |m\rangle\langle m| + \sum_{n < m}^N J_{mn} (|m\rangle\langle n| + |n\rangle\langle m|) $$ To handle this Hamiltonian on a quantum computer, we must express it as a sum of Pauli strings. First, to map the qubit states to each site's basis, we represent the integers from $0$ to $N-1$ in binary notation as follows: $$ |m\rangle = |m_{L-1}\rangle \otimes \cdots \otimes |m_1\rangle \otimes |m_0\rangle $$ Where $m_i \in \{0, 1\}$, and $L = \lceil \log_2 N\rceil$ is the number of qubits required to represent the integers from $0$ to $N-1$ in binary. In this representation, the projection operators appearing in the Hamiltonian are given by: $$ |m\rangle\langle n| = |m_{L-1}\rangle\langle n_{L-1}| \otimes \cdots \otimes |m_1\rangle\langle n_1| \otimes |m_0\rangle\langle n_0| $$ Consequently, the Hamiltonian can be expressed as a sum of Pauli strings using the following transformations: $$ \begin{split} |0\rangle\langle 0| & = \frac{1}{2}(I + Z),\\ |1\rangle\langle 0| & = \frac{1}{2}(X - iY),\\ \end{split} \qquad \begin{split} |1\rangle\langle 1| & = \frac{1}{2}(I - Z),\\ |0\rangle\langle 1| & = \frac{1}{2}(X + iY).\\ \end{split} $$ Note that the Frenkel exciton Hamiltonian restricts the excitation to the single-exciton manifold, so it is not scalable. However, for a better description of energy-transfer dynamics, we need to mix single-exciton states with ground state or multiexciton states, and in this case, the computational complexity scales exponentially with the number of pigments \[[5](#lee2022)]. ```python theme={null} def create_projector(ket: int, bra: int, qubits: list[int]) -> SparsePauliOp: """Create projector operator for given ket and bra states. Args: ket (int): Ket state. bra (int): Bra state. qubits (list[int]): List of qubit indices. Returns: SparsePauliOp: Projector operator. """ m = len(qubits) if not (0 <= ket < 2**m and 0 <= bra < 2**m): raise ValueError("Ket and bra states must be in the range [0, 2^m).") ket_bin = bin(ket)[2:].zfill(m) bra_bin = bin(bra)[2:].zfill(m) paulis_coeffs = [] for i in range(m): # little endian convention: # the least significant bit corresponds to the first qubit in the list if ket_bin[m - i - 1] == "0" and bra_bin[m - i - 1] == "0": paulis_coeffs.append((Pauli.I(qubits[i]) + Pauli.Z(qubits[i])) / 2) elif ket_bin[m - i - 1] == "0" and bra_bin[m - i - 1] == "1": paulis_coeffs.append((Pauli.X(qubits[i]) + 1j * Pauli.Y(qubits[i])) / 2) elif ket_bin[m - i - 1] == "1" and bra_bin[m - i - 1] == "0": paulis_coeffs.append((Pauli.X(qubits[i]) - 1j * Pauli.Y(qubits[i])) / 2) elif ket_bin[m - i - 1] == "1" and bra_bin[m - i - 1] == "1": paulis_coeffs.append((Pauli.I(qubits[i]) - Pauli.Z(qubits[i])) / 2) return reduce(mul, paulis_coeffs) ``` ```python theme={null} fmo_hamiltonian_sparse = [] for i in range(fmo_hamiltonian.shape[0]): for j in range(fmo_hamiltonian.shape[1]): fmo_hamiltonian_sparse.append( create_projector(i, j, list(range(3))) * fmo_hamiltonian[i, j] ) fmo_hamiltonian_sparse = reduce(lambda x, y: x + y, fmo_hamiltonian_sparse) ``` To approximate the time evolution of the constructed Hamiltonian, we employ the first-order Suzuki-Trotter decomposition: $$ e^{-itH}=\exp\left\{-it\sum_{j=1}^N h_j H_j\right\} \approx \left(\prod_{j}^N e^{-it h_j H_j/r}\right)^r. $$ Here, we fix the number of time slices $r = 10$. ```python theme={null} @qfunc def main(qn: Output[QNum], time_point: CReal): allocate(3, qn) suzuki_trotter( fmo_hamiltonian_sparse, evolution_coefficient=time_point, order=1, repetitions=10, qbv=qn, ) qprog = synthesize(main) qprog = set_quantum_program_execution_preferences( qprog, preferences=ExecutionPreferences( num_shots=1, backend_preferences=ClassiqBackendPreferences( backend_name="simulator_statevector" ), ), ) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3B5kTnagDW5FSbG1FiSEfR58Zlw ``` ```python theme={null} time_points = np.linspace(0, 0.1, 20) trotter_results = [] durations = [] with ExecutionSession(qprog) as es: for time_point in time_points: time1 = time.time() result = es.sample({"time_point": time_point}) probs = [0.0] * 2**3 for detail in result.parsed_state_vector: probs[detail.state["qn"]] = abs(detail.amplitude) ** 2 trotter_results.append(probs) time2 = time.time() duration = time2 - time1 durations.append(duration) print(f"Time point: {time_point:.4f}, Duration: {duration:.4f} seconds") ``` **Output:** ``` Time point: 0.0000, Duration: 1.7441 seconds Time point: 0.0053, Duration: 1.3106 seconds Time point: 0.0105, Duration: 1.3255 seconds Time point: 0.0158, Duration: 1.2085 seconds Time point: 0.0211, Duration: 0.9949 seconds Time point: 0.0263, Duration: 0.8305 seconds Time point: 0.0316, Duration: 1.4334 seconds Time point: 0.0368, Duration: 1.3218 seconds Time point: 0.0421, Duration: 0.9502 seconds Time point: 0.0474, Duration: 1.1008 seconds Time point: 0.0526, Duration: 1.5134 seconds Time point: 0.0579, Duration: 1.3479 seconds Time point: 0.0632, Duration: 1.3225 seconds Time point: 0.0684, Duration: 1.2989 seconds Time point: 0.0737, Duration: 0.8707 seconds Time point: 0.0789, Duration: 1.2082 seconds Time point: 0.0842, Duration: 0.9853 seconds Time point: 0.0895, Duration: 1.0298 seconds Time point: 0.0947, Duration: 1.4580 seconds Time point: 0.1000, Duration: 1.3582 seconds ``` Let's compare the calculation results with those obtained using `scipy`. Here, we plot the time evolution of the exciton population for pigments 0, 1, and 2. We see that accuracy decreases over time due to decomposition error. ```python theme={null} def simulate_ctqw(time_steps, initial_node=0): H = fmo_hamiltonian # initial state psi0 = np.zeros(fmo_hamiltonian.shape[0], dtype=complex) psi0[initial_node] = 1.0 results = [] for t in time_steps: # time evolution operator: U = exp(-i * H * t) U = expm(-1j * H * t) psi_t = np.dot(U, psi0) probability = np.abs(psi_t) ** 2 results.append(probability.tolist()) return results t_max = 0.1 time_steps = np.linspace(0, t_max, 100) probs = simulate_ctqw(time_steps, initial_node=0) plt.plot( time_points, [p[0] for p in trotter_results], "bs--", label="Pigment 0 (Classiq)" ) plt.plot( time_points, [p[1] for p in trotter_results], "g^--", label="Pigment 1 (Classiq)" ) plt.plot( time_points, [p[2] for p in trotter_results], "rx--", label="Pigment 2 (Classiq)" ) plt.plot(time_steps, [p[0] for p in probs], "b-", label="Pigment 0 (Scipy)") plt.plot(time_steps, [p[1] for p in probs], "g-", label="Pigment 1 (Scipy)") plt.plot(time_steps, [p[2] for p in probs], "r-", label="Pigment 2 (Scipy)") plt.title("Continuous-Time Quantum Walk with FMO Hamiltonian") plt.xlabel("Time") plt.ylabel("Population") plt.legend(bbox_to_anchor=(1.02, 1)) plt.grid(True, linestyle="--", alpha=0.7) plt.show() ``` output ## References \[1]: [R. Portugal and J. K. Moqadam, Efficient circuit implementations of continuous-time quantum walks for quantum search, Entropy (Basel) 27, 454 (2025).](https://www.mdpi.com/1099-4300/27/5/454) \[2]: [M. Mohseni, P. Rebentrost, S. Lloyd, and A. Aspuru-Guzik, Environment-assisted quantum walks in photosynthetic energy transfer, J. Chem. Phys. 129, 174106 (2008).](https://doi.org/10.1063/1.3002335) \[3]: [M. Maiuri, E. E. Ostroumov, R. G. Saer, R. E. Blankenship, and G. D. Scholes, Coherent wavepackets in the Fenna-Matthews-Olson complex are robust to excitonic-structure perturbations caused by mutagenesis, Nat. Chem. 10, 177 (2018).](https://doi.org/10.1038/nchem.2910) \[4]: [M. Cho, H. M. Vaswani, T. Brixner, J. Stenger, and G. R. Fleming, Exciton analysis in 2D electronic spectroscopy, J. Phys. Chem. B 109, 10542 (2005).](https://doi.org/10.1021/jp050788d) \[5]: [C.-K. Lee, J. W. Zhong Lau, L. Shi, and L. C. Kwek, Simulating energy transfer in molecular systems with digital quantum computers, J. Chem. Theory Comput. 18, 1347 (2022).](https://doi.org/10.1021/acs.jctc.1c01296) # Second Quantized Hamiltonian Source: https://docs.classiq.io/explore/applications/chemistry/second_quantized_hamiltonian/second_quantized_hamiltonian Open this notebook in GitHub to run it yourself ```python theme={null} !pip install -qq "classiq[chemistry]" ``` ```python theme={null} import time import numpy as np from openfermion import FermionOperator from classiq import * from classiq.applications.chemistry.mapping import FermionToQubitMapper from classiq.applications.chemistry.op_utils import qubit_op_to_qmod from classiq.applications.chemistry.problems import FermionHamiltonianProblem ``` ```python theme={null} op_list = [ FermionOperator("0^ 0", 0.2), FermionOperator("0^ 1^ 1 0", -0.1), FermionOperator("2^ 3^ 2 3", -0.3), ] hamiltonian = sum(op_list) ham_problem = FermionHamiltonianProblem( fermion_hamiltonian=hamiltonian, n_particles=(1, 1) ) print(ham_problem.fermion_hamiltonian) ``` **Output:** ``` 0.2 [0^ 0] + -0.1 [0^ 1^ 1 0] + -0.3 [2^ 3^ 2 3] ``` ```python theme={null} mapper = FermionToQubitMapper() vqe_hamiltonian = qubit_op_to_qmod(mapper.map(ham_problem.fermion_hamiltonian)) num_qubits = mapper.get_num_qubits(ham_problem) reps = 3 num_params = reps * num_qubits @qfunc def main(params: CArray[CReal, num_params], state: Output[QArray[QBit, num_qubits]]): allocate(state) full_hea( num_qubits=num_qubits, operands_1qubit=[lambda _, q: X(q), lambda theta, q: RY(theta, q)], operands_2qubit=[lambda _, q1, q2: CX(q1, q2)], is_parametrized=[0, 1, 0], angle_params=params, connectivity_map=[(0, 1), (1, 2), (2, 3)], reps=reps, x=state, ) ``` ```python theme={null} qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3BJuCYipJjnnImYU8PtMloI2wPv ``` ```python theme={null} with ExecutionSession(qprog) as es: result = es.minimize( cost_function=vqe_hamiltonian, initial_params={"params": [0.0] * num_params}, max_iteration=200, ) ``` ```python theme={null} optimizer_res = result[-1][0] print("vqe result:", optimizer_res) ``` **Output:** ``` vqe result: 0.0011718750000000186 ``` # Even More Efficient Quantum Computations of Chemistry Through Tensor Hypercontraction Source: https://docs.classiq.io/explore/applications/chemistry/tensor_hypercontraction/tensor_hypercontraction Open this notebook in GitHub to run it yourself The notebook implements the core block of the algorithm described in [https://arxiv.org/pdf/2011.03494](https://arxiv.org/pdf/2011.03494) ```python theme={null} import itertools import random from typing import Iterator, Sequence import numpy as np from classiq import * from classiq.interface.generator.model.preferences.preferences import ( OptimizationLevel, TranspilationOption, ) from classiq.qmod.qmod_variable import QVar from classiq.qmod.symbolic import pi random.seed(0) np.random.seed(0) ``` ## Variables Definition This section defines the variables used in the preprocessing code.\ All theoretical motivation, physical meaning, and algorithmic justification are given in the reference article; here, we **only document the role of each variable in the code**. # ## Resolution and Spin Parameters * **`N_EPSILON`**\ Number of qubits used to represent the **quantum number resolution** of the result.\ Controls numerical precision in state preparation and phase-dependent quantities. * **`N_UD`**\ Number of spin-orbitals per spin sector (up/down). ```python theme={null} counter = itertools.count() N_EPSILON = 4 # This should be 10 N_UD = 3 # This should be 54 n_mn = (3 * 2 * N_UD - 1).bit_length() m = max(2**n_mn - 1, 2) n_d = (N_UD + m * (m // 2) - 1).bit_length() n_aleph = 14 + N_EPSILON n_beth = N_EPSILON n_t = N_EPSILON t_max = max(2**n_t - 1, 2) n_rot = n_beth * N_UD # n_ell = 2 * n_mn + 4 + n_d + 1 + 2 * n_mn + 2 * n_aleph + 1 # n_psi = 2 * N_UD + 2 ``` # ## Randomized Preprocessing Data The following variables are **randomly generated placeholders** with the correct shapes and bit-widths required by the algorithm: * **`R_BR`**\ Random rotation angle used in state preparation. * **`MU_ALT`**, **`NU_ALT`**\ Boolean tables encoding alternate auxiliary indices. * **`KEEP`**\ Boolean table encoding keep / discard decisions for coherent alias sampling. * **`THETA`**\ Boolean array encoding sign or phase information. * **`ROT_MU`**, **`ROT_NU`**\ Boolean tables encoding rotation data for auxiliary indices $\mu$ and $\nu$. # ## Precomputed Amplitude Data * **`SINE_AMPLITUDES`**\ Discrete sine amplitude table used for state preparation.\ Length: `2**n_t`. # ## Note * All numerical values are **synthetic** and used only to validate register sizing, indexing, and data flow. ```python theme={null} R_BR = np.random.rand() * 2 * np.pi MU_ALT = np.random.choice((True, False), (m, n_mn)).tolist() NU_ALT = np.random.choice((True, False), (m, n_mn)).tolist() KEEP = np.random.choice((True, False), (m, n_aleph)).tolist() THETA = np.random.choice((True, False), m).tolist() ROT_MU = np.random.choice((True, False), (m, n_rot)).tolist() ROT_NU = np.random.choice((True, False), (m, n_rot)).tolist() SINE_AMPLITUDES = [ np.sqrt(2 / (2**n_t + 1)) * np.sin(np.pi * (i + 1) / (2**n_t + 1)) for i in range(2**n_t) ] ``` ## High-Level Role of the Quantum Registers This section explains the conceptual role of each `QStruct` in the context of the algorithm described in the article. # ## `ELL` * Control and State-Preparation Register `ELL` groups all registers used to **index Hamiltonian terms, prepare amplitudes, and control conditional operations** in the linear-combination-of-unitaries (LCU) / qubitization framework. At a high level, this register: * Encodes auxiliary indices ($\mu$, $\nu$) labeling terms in the Hamiltonian expansion * Stores temporary control and success flags used during preparation and uncomputation * Holds auxiliary data needed for phase, sign, and rotation selection Conceptually, `ELL` corresponds to the **LCU registers** used in: * the `prepare` oracle. * the `select` oracle. It does **not** represent the physical system itself, but rather the **algorithmic machinery** required to construct the qubitized quantum walk. # ## `PSI` * System (Fermionic State) Register `PSI` represents the **quantum state of the simulated fermionic system**. At a high level, this register: * Encodes the occupation of spin-orbitals for both spin sectors (up and down). * Stores the many-body electronic state on which the Hamiltonian acts * Serves as the target of controlled operations generated by the `select` oracle This register corresponds directly to the **fermionic system register** on which operators such as $Z$, number operators, and basis rotations act. The additional auxiliary qubits (`plus1`, `plus2`) are used to control superpositions over spin components. # ## Summary * **`ELL`**:\ Algorithmic control, indexing, and state-preparation workspace for qubitization. * **`PSI`**:\ Physical system register representing the fermionic many-body state. Together, these registers are the variables of the **block-encoded Hamiltonian and quantum walk construction** described in the article. ```python theme={null} class ELL(QStruct): mu: QNum[n_mn] nu: QNum[n_mn] m1: QBit succ: QBit r_br: QBit theta: QBit s: QNum[n_d] theta_alt: QBit mu_alt: QNum[n_mn] nu_alt: QNum[n_mn] keep: QNum[n_aleph] sigma: QNum[n_aleph] aux: QBit class PSI(QStruct): down: QArray[QBit, N_UD] up: QArray[QBit, N_UD] plus1: QBit plus2: QBit ``` ## Helper Functions ```python theme={null} def new_qubit(name: str = "auto") -> QBit: return QBit(f"{name}{next(counter)}") @qfunc def array_cast(qvar: QArray, action: QCallable[QArray]) -> None: action(qvar) def in_iteration( ctrl: QBit, x: QArray[QBit], bit: int, length: int, table: Iterator[QCallable] ) -> None: if length == 1: next(table)(ctrl) return half = 2 ** ((length - 1).bit_length() - 1) aux = new_qubit("aux") allocate(aux) # Q: Why don't we have control strings? within_apply( within=lambda: X(x[bit]), apply=lambda: CCX([ctrl, x[bit]], aux), ) in_iteration(aux, x, bit + 1, half, table) CX(ctrl, aux) in_iteration(aux, x, bit + 1, length - half, table) CCX([ctrl, x[bit]], aux) free(aux) ``` ## Prepare * Part 1 Prepare 1 ```python theme={null} @qperm def assign_aux( mu: QNum[n_mn], nu: QNum[n_mn], aux1: Output[QBit], aux2: Output[QBit], aux3: Output[QBit], aux4: Output[QBit], ) -> None: assign(nu <= m, aux1) assign(mu <= nu, aux2) assign(mu > n_mn / 2, aux3) assign(1, aux4) @qfunc def create_equal_superposition(ell: ELL) -> None: aux1 = QBit() aux2 = QBit() aux3 = QBit() aux4 = QBit() apply_to_all(H, ell.mu) apply_to_all(H, ell.nu) within_apply( within=lambda: ( RY(R_BR, ell.r_br), inplace_xor(ell.nu == m + 1, ell.m1), assign_aux(ell.mu, ell.nu, aux1, aux2, aux3, aux4), CCX([ell.m1, aux3], aux4), ), apply=lambda: control([aux1, aux2, aux4], lambda: Z(ell.r_br)), ) within_apply( within=lambda: ( apply_to_all(H, ell.mu), apply_to_all(H, ell.nu), ), apply=lambda: control([ell.mu, ell.nu], lambda: Z(ell.r_br)), ), inplace_xor(ell.nu == m + 1, ell.m1) within_apply( within=lambda: ( assign_aux(ell.mu, ell.nu, aux1, aux2, aux3, aux4), CCX([ell.m1, aux3], aux4), ), apply=lambda: control([aux1, aux2, aux4], lambda: X(ell.succ)), ) @qfunc def main(ell: Output[ELL]): allocate(ell) create_equal_superposition(ell) qprog_prepare_part1 = synthesize(main) ``` ```python theme={null} def print_qprog_stats(qprog: QuantumProgram) -> None: print(f"Total number of qubits is {qprog.data.width}") print(f"Total depth is {qprog.transpiled_circuit.depth}") ``` ```python theme={null} print_qprog_stats(qprog_prepare_part1) show(qprog_prepare_part1) ``` **Output:** ``` Total number of qubits is 82 Total depth is 3842 Quantum program link: https://platform.classiq.io/circuit/36pzAyiSqMFc6yacBPxwpcme00w ``` ## Prepare * Part 2 prepare_2 ```python theme={null} def in_uncontrolled(x: QArray[QBit], length: int, table: Iterator[QCallable]) -> None: if length == 1: raise NotImplementedError half = 2 ** ((length - 1).bit_length() - 1) within_apply( within=lambda: X(x[0]), apply=lambda: in_iteration(x[0], x, 1, half, table), ) in_iteration(x[0], x, 1, length - half, table) @qperm def shift_right(x: Const[QNum], y: Output[QNum]) -> None: lsb = QBit() msbs = QNum("msbs", x.size - 1, False, 0) within_apply( within=lambda: bind(x, [msbs, lsb]), apply=lambda: assign(msbs, y), ) @qperm def s_arithmetic(nu: Const[QNum], mu: Const[QNum], s: QNum) -> None: half_nu = QNum() within_apply( within=lambda: shift_right(nu, half_nu), apply=lambda: inplace_xor(nu * (half_nu + 1) + mu, s), ) @qperm def single_qrom_access( ctrl: Const[QBit], target: QArray[QBit], data: CArray[CBool] ) -> None: repeat(data.len, lambda i: if_(data[i], lambda: CX(ctrl, target[i]))) def _yield_s(ell: ELL) -> Iterator[QCallable]: for i in range(m): yield lambda ctrl: ( single_qrom_access(ctrl, ell.mu_alt, MU_ALT[i]), single_qrom_access(ctrl, ell.nu_alt, NU_ALT[i]), single_qrom_access(ctrl, ell.keep, KEEP[i]), single_qrom_access(ctrl, ell.theta, [THETA[i]]), ) @qperm def _swap(state1: QArray[QBit], state2: QArray[QBit]) -> None: repeat(state1.len, lambda i: SWAP(state1[i], state2[i])) @qperm def in_s(ell: ELL) -> None: array_cast(ell.s, lambda s: in_uncontrolled(s, m, _yield_s(ell))) @qfunc def prepare_after_equal_superposition(ell: ELL) -> None: aux = QBit() s_arithmetic(ell.nu, ell.mu, ell.s) apply_to_all(H, ell.sigma) in_s(ell) within_apply( within=lambda: assign(ell.keep < ell.sigma, aux), apply=lambda: ( CZ(ell.theta_alt, aux), within_apply( within=lambda: X(aux), apply=lambda: CZ(ell.theta, aux), ), control(aux, lambda: _swap(ell.mu, ell.mu_alt)), control(aux, lambda: _swap(ell.nu, ell.nu_alt)), ), ), H(ell.aux) # Q: Why don't we have control strings? within_apply( within=lambda: X(ell.m1), apply=lambda: control([ell.aux, ell.m1], lambda: _swap(ell.mu, ell.nu)), ) @qfunc def main(ell: Output[ELL]): allocate(ell) prepare_after_equal_superposition(ell) qprog_prepare_part2 = synthesize(main) ``` ```python theme={null} print_qprog_stats(qprog_prepare_part2) show(qprog_prepare_part2) ``` **Output:** ``` Total number of qubits is 92 Total depth is 5023 Quantum program link: https://platform.classiq.io/circuit/36pzJPfsNDdGCbBap1mXDZ9yhwt ``` ## Prepare Summary ```python theme={null} @qfunc def prepare(ell: ELL) -> None: create_equal_superposition(ell) prepare_after_equal_superposition(ell) ``` ## Select select ```python theme={null} def _yield_qrom(target: QVar, data: list[bool]) -> Iterator[QCallable]: for i in range(len(data)): yield lambda ctrl: single_qrom_access(ctrl, target, data[i]) @qfunc def pauli_x_basis(q: QBit) -> None: H(q) @qfunc def pauli_y_basis(q: QBit) -> None: SDG(q) H(q) @qperm def _crzz(theta: CReal, ctrl: Const[QBit], target: Const[QArray[QBit, 2]]) -> None: within_apply( within=lambda: CX(target[0], target[1]), apply=lambda: CRZ(theta, ctrl, target[1]), ) @qperm def digital_rotation(rot: Const[QArray], qs: Const[QArray[QBit, 2]]) -> None: repeat(rot.len, lambda i: _crzz(2 ** (-i) * pi / 2**rot.len, rot[i], qs)) @qfunc def controlled_basis_change(rot: Const[QArray[QNum]], state: QArray[QBit]) -> None: repeat( rot.len - 1, lambda i: ( within_apply( within=lambda: ( pauli_x_basis(state[i]), pauli_y_basis(state[i + 1]), ), apply=lambda: digital_rotation(rot[i], [state[i], state[i + 1]]), ), within_apply( within=lambda: ( pauli_x_basis(state[i + 1]), pauli_y_basis(state[i]), ), apply=lambda: invert( lambda: digital_rotation(rot[i], [state[i], state[i + 1]]) ), ), ), ) def in_controlled( ctrl: QBit, x: QArray[QBit], length: int, table: Iterator[QCallable] ) -> None: in_iteration(ctrl, x, 0, length, table) @qperm def in_mu(mu: QArray[QBit], m1: QBit, rot: QArray[QNum]) -> None: in_controlled(m1, mu, m, _yield_qrom(rot, ROT_MU)) @qperm def in_nu(nu: QArray[QBit], rot: QArray[QNum]) -> None: in_uncontrolled(nu, m, _yield_qrom(rot, ROT_NU)) @qperm def _ciz(ctrl: Const[QBit], target: Const[QBit]) -> None: S(ctrl) CZ(ctrl, target) @qperm def _cciz(ctrl1: Const[QBit], ctrl2: Const[QBit], target: Const[QBit]) -> None: control(ctrl1, lambda: S(ctrl2)) control([ctrl1, ctrl2], lambda: Z(target)) @qfunc def select(ell: ELL, psi: PSI) -> None: rot = QArray("rot", QNum[n_beth], N_UD) within_apply( within=lambda: ( allocate(rot), in_mu(ell.mu, ell.m1, rot), control(psi.plus1, lambda: _swap(psi.down, psi.up)), invert(lambda: controlled_basis_change(rot, psi.down)), ), apply=lambda: _ciz(ell.succ, psi.down[0]), ) within_apply( within=lambda: ( allocate(rot), in_nu(ell.nu, rot), control(psi.plus2, lambda: _swap(psi.down, psi.up)), invert(lambda: controlled_basis_change(rot, psi.down)), ), # Q: Why don't we have control strings? apply=lambda: within_apply( within=lambda: X(ell.m1), apply=lambda: _cciz(ell.succ, ell.m1, psi.down[0]), ), ) @qfunc def main(ell: Output[ELL], psi: Output[PSI]): allocate(ell) allocate(psi) select(ell, psi) qprog_select = synthesize(main) ``` ```python theme={null} print_qprog_stats(qprog_select) show(qprog_select) ``` **Output:** ``` Total number of qubits is 107 Total depth is 3151 Quantum program link: https://platform.classiq.io/circuit/36pzUmQOu8zcaiL1MoiDQkatOHv ``` ## Walk Operator walk ```python theme={null} @qfunc def controlled_reflect(ctrl: QBit, ell: ELL) -> None: within_apply( within=lambda: invert(lambda: prepare(ell)), # Q: Why don't we have good control logic? This is a basic reflection. apply=lambda: within_apply( within=lambda: ( X(ctrl), apply_to_all(X, ell), ), apply=lambda: array_cast(ell, lambda _ell: control(_ell, lambda: Z(ctrl))), ), ) def yield_w(ell: ELL, psi: PSI) -> Iterator[QCallable]: yield lambda aux: controlled_reflect(aux, ell) while True: yield lambda aux: (select(ell, psi), controlled_reflect(aux, ell)) ``` ## Repeated Walk with Recursion ```python theme={null} @qfunc def init_psi(psi: PSI) -> None: H(psi.plus1) H(psi.plus2) @qfunc def prepare_xi(x: QArray[QBit]) -> None: inplace_prepare_amplitudes(SINE_AMPLITUDES, 0, x) @qfunc def in_t(t: QArray[QBit], ell: ELL, psi: PSI) -> None: in_uncontrolled(t, t_max, yield_w(ell, psi)) @qfunc def main(t: Output[QArray[QBit, n_t]], ell: Output[ELL], psi: Output[PSI]) -> None: allocate(ell) allocate(t) allocate(psi) prepare_xi(t) init_psi(psi) in_t(t, ell, psi) invert(lambda: qft(t)) print(f"{N_EPSILON=}") print(f"{N_UD=}") ``` **Output:** ``` N_EPSILON=4 N_UD=3 ``` ```python theme={null} qmod = create_model(main) qmod = set_preferences( qmod, Preferences( transpilation_option=TranspilationOption.NONE, optimization_level=OptimizationLevel.NONE, debug_mode=False, timeout_seconds=1800, ), ) ``` ```python theme={null} qprog = synthesize(qmod) ``` ```python theme={null} show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/36pztXgMc7Ucpgw4g1QZ9MbHLZ0 ``` ```python theme={null} print(f"Total number of qubits is {qprog.data.width=}") ``` **Output:** ``` Total number of qubits is qprog.data.width=114 ``` # Vertex Cover Link Monitoring for IoT-Enabled Wireless Sensor Networks Source: https://docs.classiq.io/explore/applications/cybersecurity/link_monitoring/link_monitoring Open this notebook in GitHub to run it yourself ***This tutorial is partly based on work published in May 2022 \[[1](#wsn)].*** Wireless sensor networks (WSNs) for environmental sensing are fundamental communication layer technologies in the Internet of Things (IoT). WSNs are a cornerstone of modern technology, enabling real-time data collection and communication across a myriad of applications, from environmental monitoring to industrial automation. However, ensuring seamless connectivity while optimizing energy consumption remains a critical challenge. This tutorial delves into a technique for tackling the challenge of solving WSN link monitoring through the lens of the Minimum Vertex Cover (MVC) problem using Classiq. ## The Link Monitoring Problem WSNs do not have a predefined structure to maintain fundamental data-transfer operations. They are crucial communication layer technologies for providing environmental sensing operations in the IoT. Most of the time, WSNs are deployed for various applications in forests, mines, and land borders, where they must bear harsh circumstances. Link monitoring in WSNs involves identifying the optimal set of communication links between sensors to guarantee reliable data transmission while minimizing energy use. As the network scales, manually selecting these links becomes increasingly complex and time-consuming. This is where the concept of graph theory and the MVC problem step in. # ## Graph Modeling To effectively address the link monitoring challenge, this tutorial leverages graph theory to model the WSN. Each sensor is represented as a node, and communication links between sensors are depicted as edges. This graphical representation captures the essence of connectivity in the network, providing a foundation for optimizing link monitoring. A WSN can be modeled as a graph $G(V,E)$, where $V$ and $E$ represent the set of vertices (nodes) and edges (communication links), respectively. A vertex cover of a given undirected graph $G(V,E)$ is a set $S \subseteq V$ where each $e \in E$ is incident to at least one vertex of $S$. # ## MVC Concept At the heart of the approach lies the concept of the Minimum Vertex Cover \[[2](#mvc)]. This optimization problem aims to find the smallest subset of nodes (vertices) in a graph such that every edge in the graph is incident to at least one of these nodes. In the context of WSNs, the vertices selected for the MVC represent the critical sensors that ensure seamless communication throughout the network. Vertex cover is a useful structure for WSN applications such as routing, clustering, backbone formation, link monitoring, replica management, and network attack protection. Considering the link monitoring application, the number of monitor nodes should be minimized since they are equipped with extra software/hardware solutions to monitor the network traffic. On the other side, the optimization version of the MVC problem - which aims to solve the problem by selecting the minimum number of nodes to cover the whole graph - is in the NP-hard complexity class. For example: limiting the link count monitored by a node directly provides energy efficiency for link-monitoring applications. ## Working Example An example of sensor network deployment for a habitat monitoring application is depicted in *Figure 1(a)*, where there are $12$ nodes in the sensing area and node $1$ is the sink node. The graph representation of this network is given in *Figure 1(b)*. *Figure 1(c)* shows the link monitoring application for this topology. In this application, each link must be sniffed by one secure point (monitor node) to detect attacks such as packet injection and data manipulation. The red nodes (nodes $1, 3, 4, 5, 6$, and $8$) are secure points assigned to control message traffic in *Figure 1(c)*. Red arrows show the assigned links to the monitor nodes in the same figure. For example, the links $(8,9)$ and $(8,10)$ are monitored by node $8$. This architecture can also be used in other common operations such as backbone formation, clustering, and routing. Red nodes can be cluster heads, and ordinary nodes can send their data to the cluster heads to achieve data aggregation. **The network induced by red nodes is a virtual backbone that can carry messages to the sink node. By accomplishing the clustering and backbone formation operations, the data packets can be routed from ordinary nodes to the sink node.** Screenshot 2025-04-27 at 13.17.02.png Figure 1. An example of a link monitoring application for the vertex cover problem: (a) Deployment of a sample WSN; (b) Graph representation of the topology; (c) Link monitoring application on the topology. # ## MVC Mathematical Formulation The MVC problem can be formulated as a Quadratic Unconstrained Binary Optimization (QUBO): *Minimize:* $\sum_{i \in V} x_i$ *Subject to:* $(1 - x_i)(1 - x_j)=0 \quad \forall (i,j) \in E_0$ *and* $x_i \in \{0, 1\} \quad \forall i \in V$ *Where:* * $x_i$ is a binary variable that equals 1 if node $i$ is in the cover and 0 otherwise * $E_0$ is the set of all edges (connected and not connected) * $V$ is the set of vertices in the graph ## Solving MVC with Classiq and QAOA Follow the steps for solving the MVC problem with Classiq using the Quantum Approximate Optimization Algorithm (QAOA) \[[2](#qaoa)]. QAOA is a quantum algorithm designed to solve combinatorial optimization problems, making it an ideal candidate for tackling the MVC problem in large-scale WSNs. Apply QAOA to the modeled graph, iteratively adjusting the parameters to navigate the solution space and identify the MVC. Quantum computing's unique ability to explore multiple solution candidates simultaneously accelerates the optimization process, significantly outperforming classical algorithms for complex problems. To solve the link monitoring problem with Classiq: 1. Build a Classiq model. 2. Generate a parameterized quantum circuit. 3. Execute the circuit and optimize parameters to get the optimal solution. ```python theme={null} from typing import cast import networkx as nx import numpy as np import pyomo.core as pyo from IPython.display import Markdown, display from matplotlib import pyplot as plt from classiq import * ``` # ## Building the Working Example Graph Build and view a modeled graph to fit the working example above: ```python theme={null} import networkx as nx edge_dict = { 1: [2, 3, 4, 5], 2: [1, 3, 4], 3: [1, 2, 7, 12], 4: [1, 2, 6], 5: [1, 11], 6: [4, 8], 7: [3], 8: [6, 9, 10], 9: [8], 10: [8], 11: [5], 12: [3], } WSN_network_graph = nx.Graph() WSN_network_graph.add_nodes_from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]) for u in range(1, 12): for v in edge_dict[u]: WSN_network_graph.add_edge(u, v) nx.draw(WSN_network_graph, with_labels=True, font_color="whitesmoke") ``` output ## Building the Optimization Model from Graph Input To build the optimization model, use Pyomo, a Python-based, open-source optimization modeling language with a diverse set of optimization capabilities. Formalize the QUBO model into a Pyomo model object. **Classiq seamlessly incorporates the Pyomo object into its model.** Define the Pyomo model that is used to build a Classiq model using the mathematical formulation defined above: ```python theme={null} import networkx as nx import pyomo.core as pyo def mvc(graph: nx.Graph) -> pyo.ConcreteModel: model = pyo.ConcreteModel() model.x = pyo.Var(graph.nodes, domain=pyo.Binary) nodes = list(graph.nodes()) @model.Constraint(graph.edges) def full_cover(model, i, j): # all sets are covered return ((1 - model.x[i]) * (1 - model.x[j])) == 0 def obj_expression(model): # number of nodes selected return sum(model.x.values()) model.cost = pyo.Objective(rule=obj_expression, sense=pyo.minimize) return model ``` The model contains * a binary variable declaration for each node (model.x), indicating whether the variable is chosen for the set. * a constraint rule ensuring that all edges are covered. * an objective rule that minimizes the number of selected nodes. ```python theme={null} mvc_model = mvc(WSN_network_graph) ``` ```python theme={null} mvc_model.pprint() ``` **Output:** ``` 2 Set Declarations full_cover_index : Size=1, Index=None, Ordered=False Key : Dimen : Domain : Size : Members None : 2 : Any : 13 : {(1, 2), (1, 3), (1, 4), (1, 5), (2, 3), (2, 4), (3, 7), (3, 12), (4, 6), (5, 11), (6, 8), (8, 9), (8, 10)} x_index : Size=1, Index=None, Ordered=False Key : Dimen : Domain : Size : Members None : 1 : Any : 12 : {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12} 1 Var Declarations x : Size=12, Index=x_index Key : Lower : Value : Upper : Fixed : Stale : Domain 1 : 0 : None : 1 : False : True : Binary 2 : 0 : None : 1 : False : True : Binary 3 : 0 : None : 1 : False : True : Binary 4 : 0 : None : 1 : False : True : Binary 5 : 0 : None : 1 : False : True : Binary 6 : 0 : None : 1 : False : True : Binary 7 : 0 : None : 1 : False : True : Binary 8 : 0 : None : 1 : False : True : Binary 9 : 0 : None : 1 : False : True : Binary 10 : 0 : None : 1 : False : True : Binary 11 : 0 : None : 1 : False : True : Binary 12 : 0 : None : 1 : False : True : Binary 1 Objective Declarations cost : Size=1, Index=None, Active=True Key : Active : Sense : Expression None : True : minimize : x[1] + x[2] + x[3] + x[4] + x[5] + x[6] + x[7] + x[8] + x[9] + x[10] + x[11] + x[12] 1 Constraint Declarations full_cover : Size=13, Index=full_cover_index, Active=True Key : Lower : Body : Upper : Active (1, 2) : 0.0 : (1 - x[1])*(1 - x[2]) : 0.0 : True (1, 3) : 0.0 : (1 - x[1])*(1 - x[3]) : 0.0 : True (1, 4) : 0.0 : (1 - x[1])*(1 - x[4]) : 0.0 : True (1, 5) : 0.0 : (1 - x[1])*(1 - x[5]) : 0.0 : True (2, 3) : 0.0 : (1 - x[2])*(1 - x[3]) : 0.0 : True (2, 4) : 0.0 : (1 - x[2])*(1 - x[4]) : 0.0 : True (3, 7) : 0.0 : (1 - x[3])*(1 - x[7]) : 0.0 : True (3, 12) : 0.0 : (1 - x[3])*(1 - x[12]) : 0.0 : True (4, 6) : 0.0 : (1 - x[4])*(1 - x[6]) : 0.0 : True (5, 11) : 0.0 : (1 - x[5])*(1 - x[11]) : 0.0 : True (6, 8) : 0.0 : (1 - x[6])*(1 - x[8]) : 0.0 : True (8, 9) : 0.0 : (1 - x[8])*(1 - x[9]) : 0.0 : True (8, 10) : 0.0 : (1 - x[8])*(1 - x[10]) : 0.0 : True 5 Declarations: x_index x full_cover_index full_cover cost ``` Since node $1$ is the WSN sink node, force it into the vertex cover solution as follows: ```python theme={null} mvc_model.x[1].fixed = True mvc_model.x[1].value = 1 ``` **You are set to go!** # ## 1. Building a Classiq Model Utilize the `construct_combinatorial_optimization_model` function to create the model object. As input for this function, define the quantum configuration of the QAOA algorithm though the `QAOAConfig` object where the number of repetitions (`num_layers`) is defined: ```python theme={null} from classiq import construct_combinatorial_optimization_model from classiq.applications.combinatorial_optimization import OptimizerConfig, QAOAConfig qaoa_config = QAOAConfig(num_layers=1) ``` For the classical optimization part of the QAOA algorithm, define the classical optimization configuration through the `OptimizerConfig` object where the maximum number of classical iterations (`max_iteration`) and the $\alpha$-parameter (`alpha_cvar`) for running CVaR-QAOA - an improved variation of the QAOA algorithm \[[3](#cvar)] - are defined: ```python theme={null} optimizer_config = OptimizerConfig(max_iteration=60, alpha_cvar=0.9) ``` Load the Classiq model, based on the problem and algorithm parameters, which you can than use to solve the problem: ```python theme={null} qmod = construct_combinatorial_optimization_model( pyo_model=mvc_model, qaoa_config=qaoa_config, optimizer_config=optimizer_config, ) ``` The Classiq model (`qmod`) already incorporates the QAOA execution logic. However, you can set the quantum backend on which to execute so the Classiq synthesis engine takes it into consideration when generating an optimized quantum circuit: ```python theme={null} from classiq import set_execution_preferences from classiq.execution import ClassiqBackendPreferences, ExecutionPreferences backend_preferences = ExecutionPreferences( backend_preferences=ClassiqBackendPreferences(backend_name="simulator") ) qmod = set_execution_preferences(qmod, backend_preferences) ``` That's it! The Classiq model is set!! # ## 2. Generating a Parameterized Quantum Circuit Simply `synthesize` the model and view the QAOA circuit (ansatz) used to solve the optimization problem: ```python theme={null} from classiq import show, synthesize qprog = synthesize(qmod) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/2wJCSZm2PCV7kHiFldLj3o8HNjx?login=True&version=0.76.0 ``` # ## 3. Executing the Circuit: Optimizing Parameters to Get the Optimal Solution To solve the problem using the generated quantum program, use the `execute` method: ```python theme={null} from classiq import execute result = execute(qprog).result_value() ``` # ## 4. Analyzing the Energy Convergence Execution Results Check the energy convergence through the iterations: ```python theme={null} from classiq.execution import VQESolverResult vqe_result = VQESolverResult.parse_obj(result) vqe_result.convergence_graph ``` output Examine the optimization results statistics of the algorithm: ```python theme={null} import pandas as pd from classiq.applications.combinatorial_optimization import ( get_optimization_solution_from_pyo, ) solution = get_optimization_solution_from_pyo( mvc_model, vqe_result=vqe_result, penalty_energy=qaoa_config.penalty_energy ) optimization_result = pd.DataFrame.from_records(solution) optimization_result.sort_values(by="cost", ascending=True).head(5) ``` | | probability | cost | solution | count | | --- | ----------- | ---- | ------------------------------------- | ----- | | 37 | 0.002930 | 5.0 | \[1, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0] | 6 | | 2 | 0.004395 | 5.0 | \[1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0] | 9 | | 10 | 0.003906 | 5.0 | \[0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0] | 8 | | 104 | 0.002441 | 6.0 | \[1, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1] | 5 | | 96 | 0.002441 | 6.0 | \[1, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0] | 5 | View the histogram: ```python theme={null} optimization_result.hist("cost", weights=optimization_result["probability"]) ``` **Output:** ``` array([[]], dtype=object) ``` output Plot the optimal solution: ```python theme={null} best_solution = optimization_result.solution[optimization_result.cost.idxmin()] ``` ```python theme={null} best_solution ``` **Output:** ``` [1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0] ``` ```python theme={null} WSN_network_graph.nodes ``` **Output:** ``` NodeView((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)) ``` ```python theme={null} def draw_solution(graph: nx.Graph, solution: list): solution_nodes = [v for v in graph.nodes if solution[v - 1]] solution_edges = [ (u, v) for u, v in graph.edges if u in solution_nodes or v in solution_nodes ] nx.draw_kamada_kawai(graph, with_labels=True) nx.draw_kamada_kawai( graph, nodelist=solution_nodes, edgelist=solution_edges, node_color="r", edge_color="y", ) draw_solution(WSN_network_graph, best_solution) ``` output You obtained a set of vertices that form the MVC! These vertices correspond to the critical sensors that need to be monitored and maintained for optimal link connectivity. The outcome is a comprehensive link monitoring strategy that ensures efficient data transmission while conserving energy. # ## Larger Scale Models TBD Solving the WSN link monitoring challenge through the MVC problem and QAOA highlights the transformative potential of quantum computing in addressing real-world optimization problems. By combining graph theory, quantum algorithms, and practical applications, we open the door to enhanced connectivity, energy efficiency, and the seamless functioning of wireless sensor networks. As technology continues to evolve, the synergy between these methodologies will shape the future of network optimization. ## References \[1] [Self-Stabilizing Capacitated Vertex Cover Algorithms for Internet-of-Things-Enabled Wireless Sensor Networks.](https://www.researchgate.net/publication/360630980_Self-Stabilizing_Capacitated_Vertex_Cover_Algorithms_for_Internet-of-Things-Enabled_Wireless_Sensor_Networks) \[2] [Solving Vertex Cover via Ising Model on a Neuromorphic Processor.](https://vmonaco.com/papers/Solving%20Vertex%20Cover%20via%20Ising%20Model%20on%20a%20Neuromorphic%20Processor.pdf) \[3] [Barkoutsos, Panagiotis Kl, et al. "Improving variational quantum optimization using CVaR." Quantum 4 (2020): 256.](https://arxiv.org/abs/1907.04769) # Cybersecurity Vertex Cover Patch Management Challenge for Tackling Kill Chains Source: https://docs.classiq.io/explore/applications/cybersecurity/patching_management/patch_min_vertex_cover Open this notebook in GitHub to run it yourself *This tutorial is based on work submitted by Mark Carney in November 2022 \[[1](#patch)].* The Min Vertex Cover (MVC) problem is a classical issue in graph theory and computer science, which aims to find a minimum set of vertices where each edge of the graph is incident to at least one vertex in the set. Vulnerability graphs (related to attack graphs) showcase a method for solving significant cybersecurity problems with quantum computing using Classiq. This tutorial suggests a method to prioritize patches by expressing the connectivity of various vulnerabilities on a network with a QUBO, then solving this with Classiq. Such a solution has the potential to effectively remove significant kill chains (paths to security compromise) within a given network leveraging a quantum computer. ## **Introduction** Patch management is a common pain point for large-scale enterprises or widely distributed systems such as smartphones or IoT devices. Indeed, the lack of appropriate patching is indicated as a central cause for some high profile cybersecurity breaches. A variety of approaches have been proposed to improve the categorization and management of patches, including deep learning technologies. This tutorial suggests a method of prioritizing patch management by analyzing vulnerability data on assets as a bipartite graph. Given that attacks are composed of 'kill chains' - which themselves comprise sequences of exploits leveraging vulnerabilities (that are coincident in our model) - this process suggests disconnecting vulnerability sequences, thereby significantly reducing potential kill chains in a given network. This challenge, however, involves a known NP-hard problem. Leveraging quantum computation and optimization methods for vulnerability analysis of this kind opens new avenues of optimization of cybersecurity and related data for consideration. The tutorial presents a method of prioritizing the patching of vulnerabilities by considering their connectivity and solving them using Classiq. ## **Bipartite Graph Representation** The heart of the methodology represents vulnerabilities and assets as nodes in a bipartite graph. Useful terminology: # ## Kill Chains A 'kill chain' is a multi-stage sequence of events that leads to the compromise of a network. Many of the examples of kill chains involve sequences of vulnerabilities, with the sequence dependent on the assets that intersect these vulnerabilities. **Vulnerabilities:** Weaknesses in the system that result from an error in the design, implementation, or configuration of the operating system or an application software. **Assets:** Items with value; for example, data stored in the system. The availability, consistency, and integrity of assets are to be preserved. # ## Attack Graphs 'Attack graphs' feature in some interesting approaches to managing and mitigating security threats. They provide ways of analyzing network-oriented vulnerability data that many cybersecurity information sources generate. Attack graphs are labeled transition systems that model adversary capabilities within a network and how they can be elevated by transitioning to new states via the exploitation of vulnerabilities (e.g., a weak password, a bug in a software package, or the ability to guess a stack address). Attack graphs can discover paths that an adversary may use to escalate his privileges to compromise a given target (e.g., customer database or an administrator account). These sequences of possible vulnerabilities and asset pathways are commonly known as "kill chains." Kill chains depict comprehensive attack scenarios that outline the steps taken to target a specific critical asset.
Attack graph example # ## Vulnerability Graphs A theoretical way of analyzing vulnerabilities on a computer network uses 'vulnerability graphs', derived from the notion of 'attack graphs'. A graph $G = (V, E)$; $V(G)$ with a set of vertices and a set of edges $E(G) \in V \times V$, $E$ comprises pairs of elements from $V$. A bipartite graph $G$ is a graph with a partition of $V(G)$ into two sets $A, B$ such that $\forall(a,b)\in E(G)$, $a\in A$ and $b\in B$. **A vulnerability graph** $G$ is a bipartite graph where one partition of vertices represents network assets and the other represents vulnerabilities. The edges of $G$ represent a given asset affected by a detected vulnerability. **A kill chain** is a sequence of vertices $K = {v1 , v2 , . . . v_n }$ from the vulnerability partition of a vulnerability graph $V$ such that for each $v_i, v_j \in K$ there exists at least one asset $a \in V$ with $(v_i,a),(v_j,a) \in E(G)$. * Whilst the formulation in utilities directed graphs, for simplicity this tutorial uses undirected simple graphs to represent the same data. * Note the lack of any information coded about severity ratings for vulnerabilities, e.g., CVSS scores. This methodology output is not considered as critical vulnerabilities (that should always be patched as soon as possible) but rather aims to find the issues that are widespread and sufficiently well connected to cause potential harm. # ## Connectivity Dual Graphs The dual graph $D_G$ is constructed as follows. For each vulnerability vertex $v_i \in V (G)$ for $1 \leq i \leq|V(G)|$: 1. Add $v_i$ to $V(D_G)$ if $v_i \in V(D_G)$. 2. Enumerate a list of asset vertices ${a0 , a1 , . . .}$ connected to $v_i$. 3. Iterating over this list of assets, for each $v_i' $ connected to each host $a_j$: 4. Add $v_i'$ to $V (D_G)$. 5. Add $(v_i,v_i')$ to $E(D_G)$. 6. If $(v_i,v_i')$ already exists, add 1 to the weight of that edge. 7. Remove $v_i$ from $V (G)$ and continue with $v_i+1$. The dual $D_G$ represents all of the connections between vulnerabilities on attack graphs. # ## Removing Kill Chains with Vertex Covers Removing the vertices in a vertex cover on $D_G$ from $V(G)$ leaves V totally disconnected on the vulnerability partition to itself via the host partition. Removing every 'vulnerability-host-vulnerability' sub-path in a vulnerability graph $V$ by means of a minimum vertex cover on $D_G$, removes a significant number of kill chains $K$ found in the paths of $G$. # ## Min Vertex Cover: Mathematical Formulation The MVC problem can be formulated as an Integer Linear Program (ILP): Minimize: $\sum_{i \in V} x_i$ Subject to $(1 - x_i)(1 - x_j)=0 \quad \forall (i,j) \in E_0$ and $x_i \in \{0, 1\} \quad \forall i \in V$ where * $x_i$ is a binary variable that equals 1 if node $i$ is in the cover and 0 otherwise * $E_0$ is the set of all edges (connected and not connected) * $V$ is the set of vertices in the graph By utilizing a quantum computing setup, you can efficiently solve an NP-hard problem, reducing the time required to find the most at-risk vulnerabilities and patching them with more priority. You can run iterations over security data more effectively with feedback from new information, thereby improving security. ## Toy Network Example The following vulnerability graph $V$ has assets $a$ to $g$ and vulnerabilities $1$ through $8$: ```python theme={null} import networkx as nx edge_dict = { 0: ["A", "B", "D", "F"], 1: ["A", "B"], 2: ["A", "D", "E"], 3: ["B", "C", "F"], 4: ["G"], 5: ["F", "G"], 6: ["B", "C", "F"], 7: ["C", "D", "G"], } B = nx.Graph() B.add_nodes_from([0, 1, 2, 3, 4, 5, 6, 7], bipartite=0) B.add_nodes_from(["A", "B", "C", "D", "E", "F", "G"], bipartite=1) for u in range(8): for v in edge_dict[u]: B.add_edge(u, v) X, Y = nx.bipartite.sets(B) nx.draw(B, pos=nx.bipartite_layout(B, X), with_labels=True, font_color="whitesmoke") ``` output Above is the bipartite graph with vulnerabilities on the left and assets on the right. An example of a kill chain path incorporated into the venerability graph:
```python theme={null} import networkx as nx kill_chain_example_edge_dict = {0: ["B", "F"], 3: ["F", "C"], 7: ["C"]} B_example = nx.Graph() B_example.add_nodes_from([0, 3, 7], bipartite=0) B_example.add_nodes_from(["B", "C", "F"], bipartite=1) for u in [0, 3, 7]: for v in kill_chain_example_edge_dict[u]: B_example.add_edge(u, v) nx.draw(B_example, with_labels=True, font_color="whitesmoke") ``` output ```python theme={null} X, Y = nx.bipartite.sets(B_example) nx.draw( B_example, pos=nx.bipartite_layout(B_example, X), with_labels=True, font_color="whitesmoke", ) ``` output This graph leads to the following dual graph $DV$: ```python theme={null} B_dual = nx.Graph() B_2 = B.copy() Source, Target = nx.bipartite.sets(B_2) # iterate over one side of the bipartite graph # and construct the dual from the paper. def gen_dual(B_2, S_=None): B_2c = B_2.copy() DualG = nx.Graph() if not S_: S_, _ = nx.bipartite.sets(B_2c) for s in S_: DualG.add_node(s) # iter over all nodes s talks to for t1 in B_2c.neighbors(s): for t2 in B_2c.neighbors(t1): if t2 != s: DualG.add_edge(s, t2) B_2c.remove_node(s) return DualG DG = gen_dual(B_2, Source) nx.draw(DG, pos=nx.circular_layout(DG), with_labels=True, font_color="whitesmoke") ``` output Above is the dual graph for solving the MVC. # ## Building the Optimization Model from Graph Input To build the optimization model, use Pyomo, which is a Python-based, open-source optimization modeling language with a diverse set of optimization capabilities. Formalize the QUBO model into a Pyomo model object. **Classiq seamlessly incorporates the Pyomo object into its model.** Define the Pyomo model for building a Classiq model using the mathematical formulation defined above: ```python theme={null} import networkx as nx import pyomo.core as pyo def mvc(graph: nx.Graph) -> pyo.ConcreteModel: model = pyo.ConcreteModel() model.x = pyo.Var(graph.nodes, domain=pyo.Binary) nodes = list(graph.nodes()) @model.Constraint(graph.edges) def full_cover(model, i, j): # all sets are covered return ((1 - model.x[i]) * (1 - model.x[j])) == 0 def obj_expression(model): # number of nodes selected return sum(model.x.values()) model.cost = pyo.Objective(rule=obj_expression, sense=pyo.minimize) return model ``` The model contains * a binary variable declaration for each node (model.x) indicating whether the variable is chosen for the set. * a constraint rule ensuring that all edges are covered. * an objective rule that minimizes the number of selected nodes. ```python theme={null} mvc_model = mvc(DG) ``` ```python theme={null} mvc_model.pprint() ``` **Output:** ``` 1 Var Declarations x : Size=8, Index={0, 1, 2, 3, 4, 5, 6, 7} Key : Lower : Value : Upper : Fixed : Stale : Domain 0 : 0 : None : 1 : False : True : Binary 1 : 0 : None : 1 : False : True : Binary 2 : 0 : None : 1 : False : True : Binary 3 : 0 : None : 1 : False : True : Binary 4 : 0 : None : 1 : False : True : Binary 5 : 0 : None : 1 : False : True : Binary 6 : 0 : None : 1 : False : True : Binary 7 : 0 : None : 1 : False : True : Binary 1 Objective Declarations cost : Size=1, Index=None, Active=True Key : Active : Sense : Expression None : True : minimize : x[0] + x[1] + x[2] + x[3] + x[4] + x[5] + x[6] + x[7] 1 Constraint Declarations full_cover : Size=18, Index={(0, 1), (0, 7), (7, 4), (1, 2), (2, 7), (6, 5), (3, 7), (5, 4), (0, 3), (0, 6), (6, 7), (0, 2), (0, 5), (3, 6), (1, 6), (7, 5), (1, 3), (3, 5)}, Active=True Key : Lower : Body : Upper : Active (0, 1) : 0.0 : (1 - x[0])*(1 - x[1]) : 0.0 : True (0, 2) : 0.0 : (1 - x[0])*(1 - x[2]) : 0.0 : True (0, 3) : 0.0 : (1 - x[0])*(1 - x[3]) : 0.0 : True (0, 5) : 0.0 : (1 - x[0])*(1 - x[5]) : 0.0 : True (0, 6) : 0.0 : (1 - x[0])*(1 - x[6]) : 0.0 : True (0, 7) : 0.0 : (1 - x[0])*(1 - x[7]) : 0.0 : True (1, 2) : 0.0 : (1 - x[1])*(1 - x[2]) : 0.0 : True (1, 3) : 0.0 : (1 - x[1])*(1 - x[3]) : 0.0 : True (1, 6) : 0.0 : (1 - x[1])*(1 - x[6]) : 0.0 : True (2, 7) : 0.0 : (1 - x[2])*(1 - x[7]) : 0.0 : True (3, 5) : 0.0 : (1 - x[3])*(1 - x[5]) : 0.0 : True (3, 6) : 0.0 : (1 - x[3])*(1 - x[6]) : 0.0 : True (3, 7) : 0.0 : (1 - x[3])*(1 - x[7]) : 0.0 : True (5, 4) : 0.0 : (1 - x[5])*(1 - x[4]) : 0.0 : True (6, 5) : 0.0 : (1 - x[6])*(1 - x[5]) : 0.0 : True (6, 7) : 0.0 : (1 - x[6])*(1 - x[7]) : 0.0 : True (7, 4) : 0.0 : (1 - x[7])*(1 - x[4]) : 0.0 : True (7, 5) : 0.0 : (1 - x[7])*(1 - x[5]) : 0.0 : True 3 Declarations: x full_cover cost ``` **You are set to go!** ## Solving MVC with Classiq and QAOA Follow the steps of solving the problem with Classiq using the Quantum Approximate Optimization Algorithm (QAOA) \[[2](#qaoa)]. QAOA is a quantum algorithm designed to solve combinatorial optimization problems, making it an ideal candidate for tackling the MVC problem in large scale WSNs. Apply QAOA to the modeled graph, iteratively adjusting the parameters to navigate the solution space and identify the MVC. Quantum computing's unique ability to explore multiple solution candidates simultaneously accelerates the optimization process, significantly outperforming classical algorithms for complex problems. To solve the Patching Prioritization Problem with Classiq: 1. Build a Classiq model 2. Generate a parameterized quantum circuit 3. Execute the circuit and optimize the parameters to get the optimal solution ```python theme={null} from classiq import * # authenticate(overwrite=True) ``` # ## 1. Building a Classiq Model To solve the Pyomo model defined above, use the Classiq combinatorial optimization engine. For the quantum part of the QAOA algorithm, define the number of repetitions (`num_layers`): ```python theme={null} from classiq.applications.combinatorial_optimization import CombinatorialProblem combi = CombinatorialProblem(pyo_model=mvc_model, num_layers=3, penalty_factor=10) qmod = combi.get_model() ``` **Classiq seamlessly incorporates the classical Pyomo optimization object into its model.** That's it! Your Classiq model is all set!! # ## 2. Generating a Parameterized Quantum Circuit This step is simple. Synthesize your model and view the QAOA circuit (ansatz) used to solve the optimization problem: ```python theme={null} qprog = combi.get_qprog() show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/30mGGurHo3NK20p7cEnQg1FqraJ ``` image.png # ## 3. Executing the Circuit: Optimizing Parameters to Get the Optimal Solution We now solve the problem by calling the `optimize` method of the `CombinatorialProblem` object. For the classical optimization part of the QAOA algorithm we define the maximum number of classical iterations (`maxiter`) and the $\alpha$-parameter (`quantile`) for running CVaR-QAOA, an improved variation of the QAOA algorithm \[[3](#cvar)]. ```python theme={null} optimized_params = combi.optimize(maxiter=60, quantile=0.9) ``` # ## 4. Analyzing the Execution Results Check the energy convergence through the iterations: ```python theme={null} import matplotlib.pyplot as plt plt.plot(combi.cost_trace) plt.xlabel("Iterations") plt.ylabel("Cost") plt.title("Cost convergence") ``` **Output:** ``` Text(0.5, 1.0, 'Cost convergence') ``` output Examine the optimization results statistics of the algorithm. In order to get samples with the optimized parameters, we call the `sample` method: ```python theme={null} optimization_result = combi.sample(optimized_params) optimization_result.sort_values(by="cost", ascending=True).head(5) ``` | | solution | probability | cost | | --- | --------------------------------- | ----------- | ---- | | 112 | \{'x': \[1, 1, 0, 1, 0, 1, 0, 1]} | 0.001465 | 5 | | 115 | \{'x': \[1, 1, 0, 0, 0, 1, 1, 1]} | 0.001465 | 5 | | 55 | \{'x': \[1, 1, 1, 0, 0, 1, 1, 1]} | 0.003906 | 6 | | 2 | \{'x': \[1, 0, 1, 1, 1, 1, 1, 0]} | 0.056152 | 6 | | 178 | \{'x': \[1, 1, 0, 1, 1, 0, 1, 1]} | 0.000488 | 6 | View the histogram and compare the optimized results to uniformly sampled results: ```python theme={null} uniform_result = combi.sample_uniform() optimization_result["cost"].plot( kind="hist", bins=50, edgecolor="black", weights=optimization_result["probability"], alpha=0.6, label="optimized", ) uniform_result["cost"].plot( kind="hist", bins=50, edgecolor="black", weights=uniform_result["probability"], alpha=0.6, label="uniform", ) plt.legend() plt.ylabel("Probability", fontsize=16) plt.xlabel("cost", fontsize=16) plt.tick_params(axis="both", labelsize=14) ``` output Plot the optimal solution: ```python theme={null} best_solution = optimization_result.solution[optimization_result.cost.idxmin()]["x"] ``` ```python theme={null} def draw_solution(graph: nx.Graph, solution: list): solution_nodes = [v for v in graph.nodes if solution[v]] solution_edges = [ (u, v) for u, v in graph.edges if u in solution_nodes or v in solution_nodes ] nx.draw_kamada_kawai(graph, with_labels=True) nx.draw_kamada_kawai( graph, nodelist=solution_nodes, edgelist=solution_edges, node_color="r", edge_color="y", ) draw_solution(DG, best_solution) ``` output You obtained a set of vertices that form the minimum vertex cover. Remove these nodes from the original vulnerability graph $V$: ```python theme={null} check_B = B.copy() vc2 = [v for v in DG.nodes if best_solution[v]] for v in vc2: check_B.remove_node(v) nx.draw( check_B, pos=nx.bipartite_layout(check_B, Source), with_labels=True, font_color="whitesmoke", ) ``` output Given that the MVCV vulnerability nodes are patched, the rest of the vulnerabilities are disconnected from one other. This significantly breaks most of the network kill chains. ## Larger Scale Models TBD The leveraging of short term solutions for NP-hard problems that are present in cybersecurity data is a potentially rich vein of exciting possibilities. The fast and efficient resolution of cybersecurity data problems also helps reduce the analysis and reaction times of security teams. ## References \[1] [Cutting Medusa's Path -- Tackling Kill-Chains with Quantum Computing.](https://arxiv.org/abs/2211.13740) \[2] [Farhi, Edward, Jeffrey Goldstone, and Sam Gutmann. "A quantum approximate optimization algorithm." arXiv preprint arXiv:1411.4028 (2014).](https://arxiv.org/abs/1411.4028) \[3] [Barkoutsos, Panagiotis Kl, et al. "Improving variational quantum optimization using CVaR." Quantum 4 (2020): 256.](https://arxiv.org/abs/1907.04769) # Using Quantum Computers to Boost Whitebox Fuzzing Source: https://docs.classiq.io/explore/applications/cybersecurity/whitebox_fuzzing/whitebox_fuzzing Open this notebook in GitHub to run it yourself This demonstration shows how to harness the power of quantum computers for **enhancing software security**. Specifically, it uses the quantum Grover algorithm to boost the process of whitebox fuzzing. According to \[[1](#whitebox)], the "killer-app" for whitebox fuzzing is the **testing of file and packet parsers**. As any vulnerability in such a parser might result in a costly security patch, it is worthwhile investing significant effort to protect the code. ## Fuzzing image.png Fuzzing is a dynamic code testing technique that provides random data, known as "fuzz," to the inputs of a program. The goal is to find bugs, security loopholes, or other unexpected behavior in the software. By feeding the program various input combinations, a fuzzer aims to uncover weaknesses that might be exploited maliciously. # ## Whitebox Fuzzing Whitebox fuzzing, in particular, involves accessing the internal structure and code of the program. It combines static and dynamic analysis to not only execute the program with random inputs but also to achieve maximum code coverage, ensuring that all possible execution paths are tested. This allows for more targeted and efficient testing. It usually consists of a "symbolic execution" part: emulating the program to explore various branches and gathering them into a set of constraints. The constraints are solved by a constraint solver, generating new fuzzing input to the program. # ## Toy Example This example emphasizes the importance of whitebox testing. First, trigger all code flows for this function: ```python theme={null} def foo(x: int, y: int): if x == 12: if y > 3: return "a" return "b" if y + x < 9: if (x % 4) * (y % 2) == 1: return "c" return "d" return "e" ``` Now (due to simulation limitations) say that x, y are six-bit integers, so they are in the range \[0, 63]. Try to get all the outputs of `foo` in a black-box way, e.g., by sampling random inputs: ```python theme={null} from collections import Counter import numpy as np from matplotlib import pyplot as plt np.random.seed(3) x_samples = np.random.randint(0, 63, 500) y_samples = np.random.randint(0, 63, 500) outputs = [foo(x, y) for x, y in zip(x_samples, y_samples)] def plot_outputs(outputs): char_counts = Counter(outputs) # Data for plotting chars = ["a", "b", "c", "d", "e"] counts = list(char_counts.values()) # Plotting plt.bar(char_counts.keys(), char_counts.values()) plt.xlabel("Output") plt.ylabel("Occurrences") plt.show() plot_outputs(outputs) ``` output Note that with 500 inputs, you only reach three of the five different outputs for `foo`. However, by following the flow of `foo`, you can generate these constraints to the function: * "a": $(x = 12) \land (y \gt 3)$ * "b": $(x = 12) \land (y \leq 3)$ * "c": $ (x + y \lt 9) \land (x \neq 12) \land ((x \mod 4) \times (y \mod 2) = 1)$ * "d": $ (x + y \lt 9) \land (x \neq 12) \land ((x \mod 4) \times (y \mod 2) \neq 1)$ * "e": $ (x \neq 12) \land (x + y \lt 9)$ Now, to trigger each of the different outputs, find inputs that satisfy the constraints. Some constraints might not be satisfiable for any input. Although this toy example is easy, the general case, which is an instance of the Constraints Satisfaction Problem (CSP), is computationally hard and belongs to the $\text{NP-Complete}$ complexity class. ## Here Comes the Quantum Part! # ## Grover's Algorithm The physical nature of a quantum computer can be harnessed to generate inputs to the function. Specifically, you can create a 'superposition' of all different inputs: a physical state that holds all the possible assignments to the function inputs, for which to compute whether the constraints are fulfilled. The quantum computer allows only a single classical output of the variable. This is where Grover's algorithm \[[1](#gro97),[2](#growiki)] is useful: it can generate "good" samples with a high probability, achieving a quadratic(!) speedup over a classical brute force approach. # ## Oracle Function In the heart of the algorithm, implement an oracle that computes for each state: $O |x\rangle = \begin\{cases\} -|x\rangle & \text\{if \} f(x) = 1 \\ |x\rangle & \text\{otherwise\} \end\{cases\}$ Classiq has a built-in arithmetic engine for computing such oracles. Specifically, take the hardest constraint: * "c": $ (x + y \lt 9) \land (x \neq 12) \land ((x \mod 4) \times (y \mod 2) = 1)$ Eliminate the $x \neq 12$ as it is already satisfied given the first clause, and create an oracle function for it: ```python theme={null} from classiq import * from classiq.qmod.symbolic import pi VARIABLE_SIZE = 6 class OracleVars(QStruct): x: QNum[VARIABLE_SIZE] y: QNum[VARIABLE_SIZE] @qperm def my_oracle(vs: Const[OracleVars]) -> None: control((vs.x + vs.y < 9) & ((vs.x % 4) * (vs.y % 2) == 1), lambda: phase(pi)) ``` See how a quantum oracle looks: ```python theme={null} @qfunc def main(vs: Output[OracleVars]): allocate(vs) hadamard_transform(vs) my_oracle(vs) MAX_WIDTH_ORACLE = 25 qprog_oracle = synthesize(main, constraints=Constraints(max_width=MAX_WIDTH_ORACLE)) show(qprog_oracle) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3DX3GvFMxwwUnx06tjffYzFLSwG ``` image.png # ## Full Grover's Circuit Now create the full circuit implementation of Grover's algorithm. # ### Grover Repetitions The algorithm includes applying a quantum oracle in repetition, such that the probability to sample a good state "rotates" from low to high. Without knowing the concentration of solutions beforehand (which is the common case), one might overshoot with too many repetitions and not arrive at a solution. Fixed Point Amplitude Amplification (FFPA) ([4](#ffpa)), for example, is a modification to the basic Grover algorithm, which does not suffer from the overshoot issue. However, here, for simplicity, use the basic Grover's algorithm. Assume that this specific state is only satisfied for a specific input, and calculate the number of oracle repetitions required: ```python theme={null} GROVER_REPEATS = np.pi / 4 * np.sqrt(2 ** (2 * VARIABLE_SIZE) / 2) GROVER_REPEATS = np.round(GROVER_REPEATS) print(GROVER_REPEATS) ``` **Output:** ``` 36.0 ``` This is indeed \~ the square root of the number of possible assignments: $2^{12}$ in this case! To save simulation time, simplify even further: use only several Grover repetitions to show that this raises the probability of sampling a "c" input: ```python theme={null} @qfunc def main(vs: Output[OracleVars]): allocate(vs) grover_search(10, my_oracle, vs) ``` # ## Synthesizing the Model Synthesize the circuit using the Classiq synthesis engine. The synthesis takes the high level model definition and creates a quantum circuit implementation within a few seconds: ```python theme={null} MAX_WIDTH_GROVER = 25 qprog_grover = synthesize(main, constraints=Constraints(max_width=MAX_WIDTH_GROVER)) ``` # ## Showing the Resulting Circuit When the Classiq synthesis engine finishes the job, display the resulting circuit in the interactive GUI: ```python theme={null} show(qprog_grover) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3DX3a1j2WpN78zxJ5tV2mGmU2kD ``` # ## Executing the Circuit Lastly, execute the resulting circuit in the Classiq interface using the `sample` function: ```python theme={null} df = sample(qprog_grover, num_shots=500) ``` Observe the results of the 500 samples drawn from the circuit, together with each result's `foo` value: ```python theme={null} df["foo"] = df.apply(lambda r: foo(r["vs.x"], r["vs.y"]), axis=1) df ```
vs.x vs.y counts probability bitstring foo
0 1 1 56 0.112 000001000001 c
1 5 1 49 0.098 000001000101 c
2 1 3 40 0.080 000011000001 c
3 1 5 38 0.076 000101000001 c
4 5 3 34 0.068 000011000101 c
... ... ... ... ... ... ...
248 58 61 1 0.002 111101111010 e
249 59 61 1 0.002 111101111011 e
250 55 62 1 0.002 111110110111 e
251 0 63 1 0.002 111111000000 e
252 11 63 1 0.002 111111001011 e

253 rows × 6 columns

```python theme={null} plot_outputs(df["foo"].repeat(df["counts"])) ``` output And "c" is indeed sampled with a higher probability! If you do 36 Grover repetitions, you would expect to get c with a probability of \~ 1. ## Notes * While "black-box" fuzzing can also potentially benefit from quantum computers, a large quantity of quantum resources is generally required to emulate the state of the classical program. On the other hand, the "white-box" case is lower hanging fruit, requiring fewer resources for a hybrid quantum-classical approach. * This example shows quadratic improvement in comparison to a classical "brute force" solver. However, in reality, there are much faster classical solvers. As a basic example, a solver can "prune" branches of a search by backtracking if a partial assignment is not satisfiable. Such modifications are, in general, also feasible on a quantum computer. For example, see \[[5](#backtrack)] on Quantum Backtracking. ## References \[1] [Bounimova, E., Godefroid, P., and Molnar, D. (2013). Billions and billions of constraints: Whitebox fuzz testing in production, 35th International Conference on Software Engineering (ICSE), San Francisco, CA, USA, 2013, pp. 122-131, doi: 10.1109/ICSE.2013.6606558](https://ieeexplore.ieee.org/document/6606558) \[2] [Grover, Lov K. (1997). Quantum mechanics helps in searching for a needle in a haystack. Physical Review Letters, 79.2: 325.](https://journals.aps.org/prl/abstract/10.1103/PhysRevLett.79.325) \[3] [Grover's algorithm (Wikipedia)](https://en.wikipedia.org/wiki/Grover%27s_algorithm). \[4] [Yoder, Theodore J. et al. (2014). Fixed-point quantum search with an optimal number of queries. Physical Review Letters, 113 21: 210501](https://journals.aps.org/prl/abstract/10.1103/PhysRevLett.113.210501) \[5] [Montanaro, Ashley. (2015). Quantum walk speedup of backtracking algorithms. Theory of Computing. 14. 10.4086/toc.2018.v014a015](https://theoryofcomputing.org/articles/v014a015/) # Prepare Partial Exponential State Source: https://docs.classiq.io/explore/applications/finance/autocallable_options/partial_exponential_state_preparation Open this notebook in GitHub to run it yourself The notebook shows how to construct the following state: $$ |\psi\rangle = \sum_{x_0}^{x_1}\sqrt{\frac{e^{-ar}}{Z}}|r\rangle $$ $$ Z = \sum_{x_0}^{x_1}\sqrt{e^{-ar}} $$ The methodology is to load the state on the full range of states, then use exact amplitude amplification to leave only the wanted part: image.png ## Exponential State Preparation on the Full Interval ```python theme={null} import matplotlib.pyplot as plt import numpy as np from classiq import * EXP_RATE = 0.5 NUM_QUBITS = 5 @qfunc def main(x: Output[QNum]): allocate(NUM_QUBITS, x) prepare_exponential_state(-EXP_RATE, x) execution_preferences = ExecutionPreferences( num_shots=1, backend_preferences=ClassiqBackendPreferences( backend_name=ClassiqSimulatorBackendNames.SIMULATOR_STATEVECTOR ), ) qprog = synthesize(create_model(main, execution_preferences=execution_preferences)) res = execute(qprog).get_sample_result() ``` ```python theme={null} def parse_res(r, plot=True): x = [] amps = [] r = res for s in r.parsed_state_vector: if s.state["x"] in x: amps[x.index(s.state["x"])] += np.abs(s.amplitude) else: x.append(s.state["x"]) amps.append(np.abs(s.amplitude)) if plot: plt.scatter(x, amps) plt.xlabel("x") plt.ylabel("amplitude") return x, amps x, amps = parse_res(res) ``` output ## Exp State on a Specific Interval with Exact Amplitude Amplification ```python theme={null} X0 = 15 X1 = 29 X_MIN = 0 X_MAX = 2**NUM_QUBITS - 1 def get_good_states_amplitude(x0, x1, exp_rate, num_qubits): x_min = 0 x_max = 2**NUM_QUBITS - 1 """for the range[x0, x1] including x0 and x1""" return np.sqrt( (np.exp(exp_rate * x1) - np.exp(exp_rate * x0)) / (np.exp(exp_rate * x_max) - np.exp(exp_rate * x_min)) ) AMPLITUDE = get_good_states_amplitude(X0, X1, EXP_RATE, NUM_QUBITS) print(AMPLITUDE) ``` **Output:** ``` 0.6062541106972759 ``` ```python theme={null} from classiq.qmod.symbolic import logical_and @qperm def oracle_comp(x: Const[QNum], res: QBit): res ^= logical_and(x >= X0, x <= X1) @qfunc def main(x: Output[QNum]): allocate(NUM_QUBITS, x) exact_amplitude_amplification( amplitude=AMPLITUDE, oracle=lambda _x: phase_oracle(oracle_comp, _x), space_transform=lambda _x: prepare_exponential_state(-EXP_RATE, _x), packed_qvars=x, ) qprog = synthesize(create_model(main, execution_preferences=execution_preferences)) show(qprog) res = execute(qprog).get_sample_result() x, measured_amps = parse_res(res, plot=False) # compare to expected amplitudes grid = np.arange(X0, X1 + 1) expected_amps = np.sqrt(np.exp(EXP_RATE * grid)) expected_amps /= np.linalg.norm(expected_amps) plt.scatter(grid, expected_amps, marker="+", s=100, label="expected") plt.scatter(x, measured_amps, label="measured") plt.xlabel("x") plt.ylabel("amplitude") plt.legend() plt.show() ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/2yrQCbA5Ri4kiCg6Z8Hr6f5ilEz ``` output # ## Adjusting If a Single Grover Is Not Enough If the desired range does not hold enough amplitude, it is enough to load the end of the range (for a positive `EXP_RATE`) or the beginning of the range (for a negative `EXP_RATE`), then finish with a modular adder: ```python theme={null} from classiq.qmod.symbolic import logical_and X0 = 3 X1 = 13 X_MIN = 0 X_MAX = 2**NUM_QUBITS - 1 AMPLITUDE = get_good_states_amplitude(X0, X1, EXP_RATE, NUM_QUBITS) print(AMPLITUDE) ``` **Output:** ``` 0.011071508393649327 ``` This fraction of good states is not enough for a single Grover iteration to amplify to 1. So, first load the same sized interval at the end of the range: ```python theme={null} X_MAX = 2**NUM_QUBITS - 1 if EXP_RATE > 0: AMPLITUDE = get_good_states_amplitude( X_MAX - (X1 - X0), X_MAX, EXP_RATE, NUM_QUBITS ) else: AMPLITUDE = get_good_states_amplitude(0, X1 - X0, EXP_RATE, NUM_QUBITS) print(AMPLITUDE) @qperm def oracle_comp(x: Const[QNum], res: QBit): if EXP_RATE > 0: res ^= x >= X_MAX - (X1 - X0) else: res ^= x <= (X1 - X0) @qfunc def main(x: Output[QNum]): allocate(NUM_QUBITS, x) exact_amplitude_amplification( amplitude=AMPLITUDE, oracle=lambda _x: phase_oracle(oracle_comp, _x), space_transform=lambda _x: prepare_exponential_state(-EXP_RATE, _x), packed_qvars=x, ) # shift to the wanted domain if EXP_RATE > 0: x += -(X_MAX - X1) else: x += X0 qmod = create_model(main, execution_preferences=execution_preferences) qprog = synthesize(qmod) show(qprog) res = execute(qprog).get_sample_result() x, measured_amps = parse_res(res, plot=False) # compare to expected amplitudes grid = np.arange(X0, X1 + 1) expected_amps = np.sqrt(np.exp(EXP_RATE * grid)) expected_amps /= np.linalg.norm(expected_amps) plt.scatter(grid, expected_amps, marker="+", s=100, label="expected") plt.scatter(x, measured_amps, label="measured") plt.xlabel("x") plt.ylabel("amplitude") plt.legend() plt.show() ``` **Output:** ``` 0.996625424765961 Quantum program link: https://platform.classiq.io/circuit/2yrQI478Km0GkMYwVYXJ95cBlnZ ``` output # ## Verifying the Results ```python theme={null} x, measured_amps = parse_res(res, plot=False) for i, amp in zip(x, measured_amps): if i >= X0 and i <= X1: assert np.isclose(amp, expected_amps[i - X0], atol=0.01) ``` # Autocallables with Integration Amplitude Loading Source: https://docs.classiq.io/explore/applications/finance/autocallable_options/quantum_autocallable_option_pricing Open this notebook in GitHub to run it yourself This notebook covers the implementation of the Integration Amplitude Loading Method for the autocallables based on [\[1\]](#qalrop) and [\[2\]](#tqa) using the Classiq platform's Qmod language. ## Data Definitions ```python theme={null} import numpy as np import scipy S = 18 # notional value (can be asset initial price) dt = 1 # in year NUM_MARKET_DAYS_IN_YEAR = 250 DAILY_SIGMA = 0.0150694 # daily std log return DAILY_MU = 0.00050963 # daily mean log return SIGMA = DAILY_SIGMA * np.sqrt(NUM_MARKET_DAYS_IN_YEAR) # annual MU = DAILY_MU * NUM_MARKET_DAYS_IN_YEAR # annual b = 0.7 # binary barrier (to check with returns) K_comp = 1 # K put (to check with returns) K = K_comp * S # K put (to to use for the payoff) r = 0.04 # annual risk free rate K_bin_1 = 1.1 # K binaries (to check with returns) K_bin_2 = 1.1 # K binaries (to check with returns) K_bin_norm = [ np.log(K_bin_1), np.log(K_bin_2), ] # K binaries (to check with log returns) bin_1_payoff = 2 * np.exp(-r * 1) # payoff binaries already discounted bin_2_payoff = 3 * np.exp(-r * 2) # payoff binaries already discounted NUM_QUBITS = 2 TIME_STEPS = 3 # 3 years ``` ```python theme={null} PRECISION = 2 ``` ## Gaussian State Preparation ```python theme={null} def gaussian_discretization(num_qubits, mu=0, sigma=1, stds_around_mean_to_include=3): lower = mu - stds_around_mean_to_include * sigma upper = mu + stds_around_mean_to_include * sigma num_of_bins = 2**num_qubits sample_points = np.linspace(lower, upper, num_of_bins + 1) def single_gaussian(x: np.ndarray, _mu: float, _sigma: float) -> np.ndarray: cdf = scipy.stats.norm.cdf(x, loc=_mu, scale=_sigma) return cdf[1:] - cdf[0:-1] non_normalized_pmf = (single_gaussian(sample_points, mu, sigma),) real_probs = non_normalized_pmf / np.sum(non_normalized_pmf) return sample_points[:-1], real_probs[0].tolist() grid_points, probabilities = gaussian_discretization( NUM_QUBITS, stds_around_mean_to_include=3 ) STEP_X = grid_points[1] - grid_points[0] MIN_X = grid_points[0] ``` ## Rescalings Compute $R_T^{max}$ resulting from discretization: ```python theme={null} R_T_MAX_PROP = 0 if MU > 0 and SIGMA > 0: R_T_MAX_PROP = TIME_STEPS * (MU * dt + SIGMA * np.sqrt(dt) * (grid_points[-1])) R_T_MIN_PROP = 0 if MU > 0 and SIGMA > 0: R_T_MIN_PROP = TIME_STEPS * (MU * dt + SIGMA * np.sqrt(dt) * (grid_points[0])) R_T_MAX_PROP = np.max([np.abs(R_T_MIN_PROP), np.abs(R_T_MAX_PROP)]) R_T_MAX = np.log( np.max( [ (np.exp(TIME_STEPS * r) * bin_2_payoff) + K, (np.exp(TIME_STEPS * r) * bin_1_payoff) + K, K, ] ) / S ) R_T_MAX = max(R_T_MAX_PROP, R_T_MAX) ``` In two's complement, given $N$ as the number of qubits, represent from $-2^{N-1}$ and $2^{N-1}-1$: ```python theme={null} a = 1 / (2**PRECISION) ``` ```python theme={null} if R_T_MAX < 1: int_places = 1 else: int_places = np.ceil(np.log2(np.ceil(R_T_MAX))) + 1 ``` ```python theme={null} num_norm_factor = np.exp(a * (2 ** (int_places + PRECISION))) - 1 den_norm_factor = np.exp(a * (2 ** (int_places + PRECISION - 1) + 1)) ``` ```python theme={null} norm_factor = S * (num_norm_factor / den_norm_factor) ``` ## Compute Constant Rotations ```python theme={null} def compute_constant_rotation(const_payoff): b1 = const_payoff * np.exp(r * TIME_STEPS) num1 = (b1 + K) * den_norm_factor den1 = S * num_norm_factor den2 = num_norm_factor num2 = 1 angle = (num1 / den1) - (num2 / den2) return 2 * np.arcsin(np.sqrt(angle)) ``` ```python theme={null} bin_1_payoff_rotation = compute_constant_rotation(bin_1_payoff) bin_2_payoff_rotation = compute_constant_rotation(bin_2_payoff) zero_rotation = compute_constant_rotation(0) ``` ```python theme={null} bit_T_normalize = (S * np.exp(-r * TIME_STEPS)) / den_norm_factor def postprocessing(x): return ( (x * norm_factor * np.exp(-r * TIME_STEPS)) + bit_T_normalize - (K * np.exp(-r * TIME_STEPS)) ) ``` ## Verifications ```python theme={null} bin_1_payoff ``` **Output:** ``` np.float64(1.9215788783046464) ``` ```python theme={null} postprocessing((np.sin((compute_constant_rotation(bin_1_payoff) / 2))) ** 2) ``` **Output:** ``` np.float64(1.9215788783046523) ``` ```python theme={null} bin_2_payoff ``` **Output:** ``` np.float64(2.7693490391599074) ``` ```python theme={null} postprocessing((np.sin((compute_constant_rotation(bin_2_payoff) / 2))) ** 2) ``` **Output:** ``` np.float64(2.7693490391599074) ``` ```python theme={null} postprocessing((np.sin((compute_constant_rotation(0) / 2))) ** 2) ``` **Output:** ``` np.float64(1.7763568394002505e-15) ``` Compute constants for comparisons ```python theme={null} b_norm = np.log(b) K_norm = np.log(K_comp) ``` ## Classical Payoff ```python theme={null} from itertools import product import pandas as pd simulation = pd.DataFrame.from_dict( { "quantum_samples": list(range(len(grid_points))), "classical_samples": grid_points, "probabilities": probabilities, } ) binary_combinations = list(product(range(2**NUM_QUBITS), repeat=TIME_STEPS)) sim = pd.DataFrame(binary_combinations) new_col = ["time_" + str(i) for i in range(TIME_STEPS)] sim.columns = new_col ``` ```python theme={null} def round_down(a): precision_factor = 2 ** (PRECISION) return np.floor(a * precision_factor) / precision_factor ``` ```python theme={null} def round_factor(a): # precision_factor = 2 ** (PRECISION) # return np.round(a * precision_factor) / precision_factor return floor_factor(a) def floor_factor(a): precision_factor = 2 ** (PRECISION) if a >= 0: return np.floor(a * precision_factor) / precision_factor else: return np.ceil(a * precision_factor) / precision_factor ``` ```python theme={null} for i in range(TIME_STEPS): sim = sim.merge(simulation, left_on="time_" + str(i), right_on="quantum_samples") sim = sim.drop("quantum_samples", axis=1) new_col = new_col + ["c_" + str(i), "q_prob_" + str(i)] sim.columns = new_col sim["log_ret_val_" + str(i)] = ( round_factor(MU * dt + np.sqrt(dt) * SIGMA * MIN_X) + round_factor(SIGMA * np.sqrt(dt) * STEP_X) * sim["time_" + str(i)] ) new_col = new_col + ["log_ret_val_" + str(i)] sim.columns = new_col if i != 0: sim["log_ret_val_" + str(i)] = ( sim["log_ret_val_" + str(i)] + sim["log_ret_val_" + str(i - 1)] ) sim["ret_val_" + str(i)] = np.exp(sim["log_ret_val_" + str(i)]) new_col = new_col + ["ret_val_" + str(i)] ``` ```python theme={null} sim["prob"] = 1 for i in range(TIME_STEPS): sim["prob"] = sim["prob"] * sim["q_prob_" + str(i)] ``` ```python theme={null} for i in range(TIME_STEPS): sim["b_crossed_" + str(i)] = sim["log_ret_val_" + str(i)] < round_factor(b_norm) sim["bin_1_activate"] = sim["log_ret_val_0"] > round_factor(K_bin_norm[0]) sim["bin_2_activate"] = sim["log_ret_val_1"] > round_factor(K_bin_norm[1]) sim["K_put"] = sim["log_ret_val_" + str(TIME_STEPS - 1)] < round_factor(K_norm) sim["payoff"] = 0.0 sim.loc[sim["bin_1_activate"], "payoff"] = bin_1_payoff sim.loc[(~sim["bin_1_activate"]) & (sim["bin_2_activate"]), "payoff"] = bin_2_payoff barrier_crossed_once = sim["b_crossed_0"] for i in range(1, TIME_STEPS): barrier_crossed_once = barrier_crossed_once | sim["b_crossed_" + str(i)] put_condition = ( (~((sim["bin_1_activate"]) | (sim["bin_2_activate"]))) & (barrier_crossed_once) & (sim["K_put"]) ) sim.loc[put_condition, "payoff"] = ( S * (sim[put_condition]["ret_val_2"] - K_comp) * np.exp(-r * TIME_STEPS) ) ``` ```python theme={null} expected_payoff = sum(sim["prob"] * sim["payoff"]) ``` ```python theme={null} print("expected payoff classical: " + str(expected_payoff)) ``` **Output:** ``` expected payoff classical: -3.5032073946209352 ``` ## Integration Method Circuit Synthesis ```python theme={null} from classiq import * from classiq.qmod.symbolic import sqrt @qfunc def integrator(exp_rate: CReal, x: Const[QNum], ref: QNum, res: QBit) -> None: prepare_exponential_state(-exp_rate, ref) res ^= x >= ref ``` ```python theme={null} def affine_py(x: QNum): return MU * dt + SIGMA * sqrt(dt) * (x * STEP_X + MIN_X) ``` ```python theme={null} @qperm def add_minimum(x: QArray): X(x[x.size - 1]) ``` ```python theme={null} round_K_bin_norm = [] round_b_norm = round_factor(b_norm) round_K_bin_norm.append(round_factor(K_bin_norm[0])) round_K_bin_norm.append(round_factor(K_bin_norm[1])) round_k_norm = round_factor(K_norm) ``` ```python theme={null} @qfunc def integration_load_amplitudes(y: Const[QNum], aux_reg: QNum, ind_reg: QBit): exp_rate = 1 / (2**PRECISION) integrator(exp_rate, y, aux_reg, ind_reg) @qfunc def integration_payoff(log_return: Const[QNum], aux_reg: QNum, ind_reg: QBit): log_return_unsigned = QNum(size=log_return.size) within_apply( lambda: [ bind(log_return, log_return_unsigned), add_minimum(log_return_unsigned), ], lambda: integration_load_amplitudes(log_return_unsigned, aux_reg, ind_reg), ) @qperm def check_barrier_crossed(log_return: Const[QNum], barrier_crossed: QBit): barrier_crossed ^= log_return < round_b_norm @qperm def check_binary(log_return: Const[QNum], round_K_bin_norm: CReal, binary_valid: QBit): binary_valid ^= log_return > round_K_bin_norm @qperm def check_K_put( log_return: Const[QNum[PRECISION + int_places, SIGNED, PRECISION]], k_put_valid: QBit, ): k_put_valid ^= log_return < round_k_norm @qfunc def binary_payoff(binary_payoff: CReal, target: QBit): RY(binary_payoff, target) @qfunc def zero_payoff(target: QBit): RY(zero_rotation, target) @qperm def check_put_activate( barriers_crossed: Const[QArray], k_put_valid: Const[QBit], put_alone_valid: QBit, ): put_alone_valid ^= ( ( (barriers_crossed[0] == 1) | (barriers_crossed[1] == 1) | (barriers_crossed[2] == 1) ) & (k_put_valid == 1) ) == 1 @qperm def populate_put_alone_valid( sum_log_return: Const[QNum], barriers_crossed: QArray, put_alone_valid: Output[QBit], ): k_put_valid = QBit() allocate(put_alone_valid) within_apply( lambda: [ allocate(k_put_valid), check_K_put(sum_log_return, k_put_valid), check_barrier_crossed( sum_log_return, barriers_crossed[2] ), # magari qui si può condizionare ], lambda: check_put_activate(barriers_crossed, k_put_valid, put_alone_valid), ) @qfunc def final_payoff( check_all_zeros: Const[QNum], sum_log_return: Const[QNum], aux_reg: QNum, ind_reg: QBit, ): control(check_all_zeros == 0, lambda: zero_payoff(ind_reg)) control( check_all_zeros == 1, lambda: integration_payoff(sum_log_return, aux_reg, ind_reg), ) @qfunc def autocallable_integration( x: QArray[QNum, TIME_STEPS], aux_reg: QNum, ind_reg: QBit, sum_log_return: QNum, first_bin_valid: QBit, second_bin_valid: QBit, barriers_crossed: QArray[QBit], ) -> None: repeat(x.len, lambda i: inplace_prepare_state(probabilities, 0, x[i])) sum_log_return ^= affine_py(x[0]) check_binary(sum_log_return, round_K_bin_norm[0], first_bin_valid) control(first_bin_valid, lambda: binary_payoff(bin_1_payoff_rotation, ind_reg)) # check_barrier_crossed(sum_log_return, barriers_crossed[0]) check_barrier_crossed(sum_log_return, barriers_crossed[0]) sum_log_return += affine_py(x[1]) check_binary(sum_log_return, round_K_bin_norm[1], second_bin_valid) control( ((first_bin_valid == 0) & (second_bin_valid == 1)) == 1, lambda: binary_payoff(bin_2_payoff_rotation, ind_reg), ) # check order # check_barrier_crossed(sum_log_return, barriers_crossed[1]) #magari qui si può condizionare check_barrier_crossed(sum_log_return, barriers_crossed[1]) sum_log_return += affine_py(x[2]) put_alone_valid = QBit() within_apply( lambda: populate_put_alone_valid( sum_log_return, barriers_crossed, put_alone_valid, ), lambda: final_payoff( [put_alone_valid, first_bin_valid, second_bin_valid], sum_log_return, aux_reg, ind_reg, ), ) ``` ## IQAE Functions and QStruct ```python theme={null} from classiq.applications.iqae.iqae import IQAE class OracleVars(QStruct): x: QArray[QNum[NUM_QUBITS, False, 0], TIME_STEPS] aux_reg: QNum[int_places + PRECISION] sum_log_return: QNum[int_places + PRECISION, SIGNED, PRECISION] first_bin_valid: QBit second_bin_valid: QBit barriers_crossed: QArray[QBit, TIME_STEPS] @qfunc def iqae_state_preparation(state: OracleVars, ind: QBit): autocallable_integration( state.x, state.aux_reg, ind, state.sum_log_return, state.first_bin_valid, state.second_bin_valid, state.barriers_crossed, ) ``` ## Base Simulator Synthesis ```python theme={null} iqae = IQAE( state_prep_op=iqae_state_preparation, problem_vars_size=NUM_QUBITS * TIME_STEPS + 2 * (int_places + PRECISION) + 2 + TIME_STEPS, constraints=Constraints(optimization_parameter="width"), preferences=Preferences( optimization_level=1, machine_precision=PRECISION, timeout_seconds=2000 ), ) qmod = iqae.get_model() print("Starting synthesis") qprog = iqae.get_qprog() show(qprog) ``` **Output:** ``` Starting synthesis Quantum program link: https://platform.classiq.io/circuit/3564N6oLHen6uvAwSGk0stVkwht ``` ```python theme={null} print("Circuit width: ", qprog.data.width) ``` **Output:** ``` Circuit width: 24 ``` Execution takes a lot of time. Examine the results: ```python theme={null} # takes a lot of time # EPSILON = 0.001 # ALPHA = 0.002 # res_iqae= iqae.run(EPSILON, ALPHA, execution_preferences=ExecutionPreferences(shots=100000)) ``` ```python theme={null} # print("the expected result from classical computation is: " + str(expected_payoff)) # print("the result from IQAE is: " + str(postprocessing(res_iqae.estimation))) # print("the confidence interval of the quantum estimation is: " + str(postprocessing(res_iqae.confidence_interval[0]))+"," +str(postprocessing(res_iqae.confidence_interval[1])) ) # print(f"Synthesis and execution time: {time() - start_time} seconds") ``` ```python theme={null} print("the expected result from classical computation is: " + str(expected_payoff)) print("the result from IQAE is: " + str(-3.5079217726515903)) print( "the confidence interval of the quantum estimation is: " + str(postprocessing(0.119158741)) + "," + str(postprocessing(0.1197666)) ) ``` **Output:** ``` the expected result from classical computation is: -3.5032073946209352 the result from IQAE is: -3.5079217726515903 the confidence interval of the quantum estimation is: -3.535334467336666,-3.4805134318653383 ``` ## References \[1] [Francesca Cibrario et al. (2024). Quantum Amplitude Loading for Rainbow Options Pricing. Preprint.](https://arxiv.org/abs/2402.05574v2) \[2] [Shouvanik Chakrabarti et al. (2021). A Threshold for Quantum Advantage in Derivative Pricing, Quantum 5, 463.](https://arxiv.org/pdf/2012.03819) # Stochastic Modeling of Brownian Motion Source: https://docs.classiq.io/explore/applications/finance/brownian_chebyshev_polynomials/brownian_chebyshev_polynomials Open this notebook in GitHub to run it yourself ## Building a Geometric Brownian Motion (GBM) Price Model * From Analytic Formula to Quantum Circuit ## Motivation Pricing path-dependent derivatives such as **Asian options** often requires evaluating a **Geometric Brownian Motion (GBM)** over many time steps. * **Classical approach:** Monte Carlo simulation over many paths and timesteps. * **Quantum approach:** * Compress the representation of randomness (load distributions into amplitudes). * Achieve a **quadratic speed-up** for expectation estimation via **Quantum Amplitude Estimation (QAE)**. In this notebook we will: 1. Derive a Chebyshev-truncated **Karhunen-Loève (KL)** expansion. 2. Convert that expansion to **Classiq quantum arithmetic**. 3. Synthesize hardware-aware circuits for multiple targets with a single command. **Reference:** Prakash, Anupam, et al., *"Quantum option pricing via the Karhunen-Loève expansion."* (2024) - [https://arxiv.org/pdf/2402.10132.pdf](https://arxiv.org/pdf/2402.10132.pdf) ## 1. Chebyshev Polynomials in the KL Expansion We implement the mathematical expression (as appears in the reference article) using **Chebyshev polynomials** via a recurrence relation. The recursive form maps neatly onto quantum add/mul blocks that Classiq can optimize. # ## KL Expansion (Chebyshev-Truncated Form) The KL expansion expresses $B(t)$ as an orthogonal sine series with i.i.d. Gaussian coefficients $a_k$. In truncated form: $$ B_L(t) = p_L(a,cos(t)) = a_0t+\frac{\sqrt{2}}\pi sin(\pi t)\sum_{k=0}^{L-1}\frac{a_k}k U_k(cost(\pi t)) $$ where $U_k$ are **Chebyshev polynomials of the second kind**. ## Gaussian Discretization (Classical Preprocessing) Goal: build a discrete approximation of a Gaussian distribution so that its probabilities can be loaded into amplitudes. Typical steps: * Select range $[\mu - k\sigma,\, \mu + k\sigma]$ (e.g., $k=3$ standard deviations). * Use $2^n$ bins for $n$ qubits. * Compute bin probabilities using the Gaussian CDF: $ p_i = \Phi(x_\{i+1\}) - \Phi(x_i)$ * Normalize the probability vector so $\sum_i p_i = 1$. Result: * A list of grid points (bin edges or centers) * A probability vector suitable for amplitude loading. ```python theme={null} import scipy def gaussian_discretization(num_qubits, mu=0, sigma=1, stds_around_mean_to_include=3): lower = mu - stds_around_mean_to_include * sigma upper = mu + stds_around_mean_to_include * sigma num_of_bins = 2**num_qubits sample_points = np.linspace(lower, upper, num_of_bins + 1) def single_gaussian(x: np.ndarray, _mu: float, _sigma: float) -> np.ndarray: cdf = scipy.stats.norm.cdf(x, loc=_mu, scale=_sigma) return cdf[1:] - cdf[0:-1] non_normalized_pmf = (single_gaussian(sample_points, mu, sigma),) real_probs = non_normalized_pmf / np.sum(non_normalized_pmf) return sample_points[:-1], real_probs[0].tolist() ``` ```python theme={null} import numpy as np from classiq import * PI = np.pi L = 4 TIME_STEPS = 1 NUM_QUBITS_GAUSSIAN = 4 MU = 1 SIGMA = 2 grid_points, probabilities = gaussian_discretization(NUM_QUBITS_GAUSSIAN) ``` ```python theme={null} # plot the discretized Gaussian import matplotlib.pyplot as plt plt.bar(grid_points, probabilities, width=0.1) plt.title("Discretized Gaussian Distribution") plt.xlabel("Value") plt.ylabel("Probability") plt.show() ``` output ```python theme={null} NUM_QUBITS_GAUSSIAN = 1 grid_points, probabilities = gaussian_discretization(NUM_QUBITS_GAUSSIAN) ``` ## Quantum Function Approximation for $\sin(\pi x)$ and $2\cos(\pi x)$ For simplicity in the demo: * Implement $\sin$ and $\cos$ via **low-order Taylor-like** polynomials (around $x=0.5$). * This keeps the arithmetic shallow and highlights the modeling flow. Interpretation: * After preparing a superposition over $x$, the computed value register becomes **entangled** with $x$, representing a function evaluation "in parallel worlds." ```python theme={null} @qperm def two_cos_pi_x(x: Const[QNum], out: Output[QNum]): out |= -2 * PI * (x - 0.5) # expansion around x=0.5 @qperm def sin_pi_x(x: Const[QNum], out: Output[QNum]): out |= -5 * (x - 0.5) ** 2 + 1 # expansion around x=0.5 @qfunc def main(x: Output[QNum[3, UNSIGNED, 3]], y: Output[QNum]): allocate(x) hadamard_transform(x) sin_pi_x(x, y) qprog_sin = synthesize(main) show(qprog_sin) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/36pwmH1ke1rrHMBPgZAYTeHFUlA ``` **Output:** ``` https://platform.classiq.io/circuit/36pwmH1ke1rrHMBPgZAYTeHFUlA?login=True&version=15 ``` Let's execute and see that x values are entangled with values of y that are a Taylor approximation of sin around x=0.5 ```python theme={null} job = execute(qprog_sin) job.open_in_ide() ``` **Output:** ``` Error when invoking the open external command: s is not iterable ``` ## Chebyshev Polynomials The Chebyshev polynomials are a sequence of orthogonal polynomials that are related to de Moivre's formula and the trigonometric functions. They are defined by the recurrence relation: $$ U_0(x) = 1, $$ $$ U_1(x) = 2x $$ $$ U_{k+1}(x) = 2xU_k(x) - U_{k-1}(x) $$ ```python theme={null} @qperm def uk(two_x: Const[QNum], uk_1: Const[QNum], uk_2: Const[QNum], uk: Output[QNum]): uk |= two_x * uk_1 - uk_2 @qfunc def main(x: Output[QNum[2, UNSIGNED, 2]]): allocate(x) hadamard_transform(x) U = [QNum(f"U{k}") for k in range(L)] U[0] |= 1 U[1] |= 2 * x for k in range(2, L): uk(x, uk_1=U[k - 1], uk_2=U[k - 2], uk=U[k]) qprog_uk = synthesize(main) ``` Circuit scaling intuition: * Loop depth grows as $O(L)$. * Doubling truncation order increases gate count only **linearly** (hardware-friendly). ```python theme={null} show(qprog_uk) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/36pwmrzrsiD5dUq0bmDzlSnNiS6 ``` **Output:** ``` https://platform.classiq.io/circuit/36pwmrzrsiD5dUq0bmDzlSnNiS6?login=True&version=15 ``` ## Brownian Motion The Brownian motion is a stochastic process that models the random movement of particles in a fluid. The approximate solution in this context is the truncated Wiener series. $$ B_L(t) = p_L(a,cos(t)) = a_0t+\frac{\sqrt{2}}\pi sin(\pi t)\sum_{k=0}^{L-1}\frac{a_k}k U_k(cost(\pi t)) $$ Key ingredients in the quantum model: * Prepare registers for coefficients $a_0,\dots,a_{L-1}$ from the discretized Gaussian distribution (amplitude loading). * Prepare a time register $t$ in superposition (e.g., Hadamards over a time index). * Compute: * $2\cos(\pi t)$ (or a stand-in approximation) * $\sin(\pi t)$ (or a stand-in approximation) * $U_k(\cos(\pi t))$ via recurrence * Accumulate the weighted sum to form $B_L(t)$. ```python theme={null} @qfunc def truncated_wiener_series(t: QNum, out: Output[QNum]): As = [QNum(f"a{i}") for i in range(L)] for a in As: prepare_state(probabilities, 0, a) two_cos_pi_t = QNum("two_cos_pi_t") sin_pi_t = QNum("sin_pi_t") U = [QNum(f"U{k}") for k in range(L)] two_cos_pi_x(x=t, out=two_cos_pi_t) U[0] |= 1 U[1] |= two_cos_pi_t for k in range(2, L): uk(two_cos_pi_t, uk_1=U[k - 1], uk_2=U[k - 2], uk=U[k]) sin_pi_x(x=t, out=sin_pi_t) out |= As[0] * t + (2**0.5 / PI) * sin_pi_t * sum( [As[i] * U[i] for i in range(1, L)] ) for a in As: drop(a) for u in U: drop(u) ``` ## Return-To-Price Space (GBM Mapping) Convert (log-)returns to price using the GBM form: $$ S(t) = S_0 \exp\left(\sigma\,B_L(t) + \left(\mu - \frac{\sigma^2}{2}\right)t\right) $$ In the demo notebook : * Exponentiation may be implemented via a simple approximation for $\exp(\cdot)$. * More accurate quantum exponentiation methods exist in the literature (see referenced suggestions in [https://arxiv.org/pdf/2001.00807.pdf](https://arxiv.org/pdf/2001.00807.pdf) for example). ```python theme={null} @qperm def return_to_price_space( returns: Const[QNum], t: Const[QNum], price: Output[QNum] ) -> None: price |= (SIGMA * returns + (MU - SIGMA**2 / 2) * t) + 1 ``` ## Putting It All Together ```python theme={null} @qfunc def main(t: Output[QNum], G: Output[QNum]): # Allocate qubits and prepare distributions B = QNum("B") allocate(TIME_STEPS, t) hadamard_transform(t) # Create the truncated wiener series truncated_wiener_series(t, B) # Return to price space return_to_price_space(returns=B, t=t, price=G) drop(B) ``` ```python theme={null} qmod = create_model( entry_point=main, constraints=Constraints(optimization_parameter="width", max_width=139), preferences=Preferences(transpilation_option="none"), ) qprog = synthesize(qmod) ``` ```python theme={null} show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/36pwwbEYGXxJ48DEqHODvhN3UvC ``` **Output:** ``` https://platform.classiq.io/circuit/36pwwbEYGXxJ48DEqHODvhN3UvC?login=True&version=15 ``` ```python theme={null} print(f"The number of qubits used: {qprog.data.width}") ``` **Output:** ``` The number of qubits used: 115 ``` ## Final Reflection and Quantum Roadmap This notebook illustrates that a mathematically heavy model (KL-truncated GBM + Chebyshev recursion) becomes circuit-light once randomness is moved into amplitudes. Next steps toward pricing (expected payoff): 1. **Amplitude loading**: put Gaussian coefficients into superposition with $O\!\left(n\,\mathrm{polylog}(1/\epsilon)\right)$ gates (matching the power-of-two grid). 2. **Nested QAE (conceptually)**: * First QAE estimates time-averaged price along the path. * Second QAE wraps the payoff. * Query complexity can still beat classical Monte Carlo scaling. 1. **Potential shortcut** (as hinted in the reference): * Drop one QAE via smart time subsampling to reduce depth without extra qubits (constants and practical tradeoffs depend on implementation details). Takeaway: * The classical formula is algebraically dense, but the quantum program is highly structured, enabling modeling-focused development with automated circuit synthesis. # Quantum Kernels and Support Vector Machines Source: https://docs.classiq.io/explore/applications/finance/credit_card_fraud/credit_card_fraud Open this notebook in GitHub to run it yourself ## Detecting Credit Card Fraud Quantum Support Vector Machines (QSVM) on Kaggle labeled data is a means to classify and detect fraudulent credit card transactions. Quantum Machine Learning (QML) is the aspect of research that explores the consequences of implementing machine learning on a quantum computer. SVM is a supervised machine learning method widely used for multiple labeled data classification. The SVM algorithm can be enhanced even on a noisy intermediate scale quantum computer (NISQ) by introducing the kernel method. It can be restructured to exploit the properties of the large dimensionality of a quantum Hilbert space. This demo presents a simple use case where a Quantum SVM (QSVM) algorithm is implemented on credit card labeled data to detect fraudulent transactions. It leverages the Classiq proprietary QSVM library and core capabilities to explore the rising potential in enhancing security applications. This demonstration is based on work published in August 2022 \[[1](#hbc)]. *This demo uses the `sklearn` package in addition to the `classiq` package.* ```python theme={null} !pip install -qq -U "classiq[qml]" ``` ```python theme={null} %%capture ! pip install scikit-learn ! pip install seaborn ``` Import the required resources: ```python theme={null} # General Imports # Visualization Imports import matplotlib.pyplot as plt import numpy as np import pandas as pd # Scikit Imports import sklearn from sklearn import datasets from sklearn.decomposition import PCA from sklearn.model_selection import train_test_split from sklearn.preprocessing import MinMaxScaler, StandardScaler from sklearn.svm import SVC from sklearn.utils import shuffle ``` ```python theme={null} ## For TSNE visualization from numpy import linalg from numpy.linalg import norm from scipy.spatial.distance import pdist, squareform from sklearn.manifold import TSNE # Hack the t-SNE code in sklearn 0.15.2 from sklearn.metrics.pairwise import pairwise_distances from sklearn.preprocessing import scale # Random state RS = 20150101 import matplotlib import matplotlib.colors as colors # Use Matplotlib for graphics import matplotlib.patheffects as PathEffects import matplotlib.pyplot as plt # Import Seaborn to make nice plots try: import seaborn as sns except ModuleNotFoundError: palette = np.array( [ (0.4, 0.7607843137254902, 0.6470588235294118), (0.9882352941176471, 0.5529411764705883, 0.3843137254901961), (0.5529411764705883, 0.6274509803921569, 0.796078431372549), (0.9058823529411765, 0.5411764705882353, 0.7647058823529411), (0.6509803921568628, 0.8470588235294118, 0.32941176470588235), (1.0, 0.8509803921568627, 0.1843137254901961), (0.8980392156862745, 0.7686274509803922, 0.5803921568627451), (0.7019607843137254, 0.7019607843137254, 0.7019607843137254), ] ) else: sns.set_style("darkgrid") sns.set_palette("muted") sns.set_context("notebook", font_scale=1.5, rc={"lines.linewidth": 2.5}) palette = np.array(sns.color_palette("Set2")) import warnings warnings.filterwarnings("ignore") ``` ```python theme={null} from classiq import * ``` ## Data The dataset contains transactions made by credit cards in September 2013 by European cardholders. The transactions occurred over two days, where there were 492 frauds out of 284,807 transactions. The dataset is highly unbalanced, such that the positive class (frauds) account for 0.172% of all transactions. Data properties: * The database contains only numeric input variables that are the result of a PCA transformation. * Due to confidentiality issues, original features are not provided. * Features V1, V2, $\dots$ V28 are the principal components obtained with PCA. * The only features that have not been transformed with PCA are 'Time' and 'Amount'. * The 'Time' feature is the number of seconds that elapsed between each transaction and the first transaction in the dataset. * The 'Amount' feature is the transaction amount. * The 'Class' feature is the response variable. It takes the value of 1 in the case of fraud and 0 otherwise. The data is freely available through Kaggle \[[2](#kaggle)]. # ## Loading the Kaggle "Credit Card Fraud Detection" Dataset ```python theme={null} input_file = "../resources/creditcard.csv" # comma delimited file as input kaggle_full_set = pd.read_csv(input_file, header=0) # presnting first 5 lines: kaggle_full_set.head() ```
Time V1 V2 V3 V4 V5 V6 V7 V8 V9 ... V21 V22 V23 V24 V25 V26 V27 V28 Amount Class
0 0 -1.359807 -0.072781 2.536347 1.378155 -0.338321 0.462388 0.239599 0.098698 0.363787 ... -0.018307 0.277838 -0.110474 0.066928 0.128539 -0.189115 0.133558 -0.021053 149.62 0
1 0 1.191857 0.266151 0.166480 0.448154 0.060018 -0.082361 -0.078803 0.085102 -0.255425 ... -0.225775 -0.638672 0.101288 -0.339846 0.167170 0.125895 -0.008983 0.014724 2.69 0
2 1 -1.358354 -1.340163 1.773209 0.379780 -0.503198 1.800499 0.791461 0.247676 -1.514654 ... 0.247998 0.771679 0.909412 -0.689281 -0.327642 -0.139097 -0.055353 -0.059752 378.66 0
3 1 -0.966272 -0.185226 1.792993 -0.863291 -0.010309 1.247203 0.237609 0.377436 -1.387024 ... -0.108300 0.005274 -0.190321 -1.175575 0.647376 -0.221929 0.062723 0.061458 123.50 0
4 2 -1.158233 0.877737 1.548718 0.403034 -0.407193 0.095921 0.592941 -0.270533 0.817739 ... -0.009431 0.798278 -0.137458 0.141267 -0.206010 0.502292 0.219422 0.215153 69.99 0

5 rows × 31 columns

## 1. Data Preprocessing: Selecting Train and Test Datasets Subsample the dataset to make it manageable for near-term quantum simulations: ```python theme={null} TRAIN_NOMINAL_SIZE = 100 TRAIN_FRAUD_SIZE = 25 TEST_NOMINAL_SIZE = 50 TEST_FRAUD_SIZE = 10 PREDICTION_NOMINAL_SIZE = 50 PREDICTION_FRAUD_SIZE = 10 SHUFFLE_DATA = False ``` ```python theme={null} ## Separating nominal ("legit") from fraud: all_fraud_set = kaggle_full_set.loc[kaggle_full_set["Class"] == 1] all_nominal_set = kaggle_full_set.loc[kaggle_full_set["Class"] == 0] ``` ```python theme={null} ## Optionally shuffle data before selective sets if SHUFFLE_DATA: all_fraud_set = shuffle(all_fraud_set, random_state=1234) all_nominal_set = shuffle(all_nominal_set, random_state=1234) ``` ```python theme={null} ## Selecting data subsets selected_training_set = pd.concat( [all_nominal_set[:TRAIN_NOMINAL_SIZE], all_fraud_set[:TRAIN_FRAUD_SIZE]] ) selected_testing_set = pd.concat( [ all_nominal_set[TRAIN_NOMINAL_SIZE : TRAIN_NOMINAL_SIZE + TEST_NOMINAL_SIZE], all_fraud_set[TRAIN_FRAUD_SIZE : TRAIN_FRAUD_SIZE + TEST_FRAUD_SIZE], ] ) selected_prediction_set = pd.concat( [ all_nominal_set[ TRAIN_NOMINAL_SIZE + TEST_NOMINAL_SIZE : TRAIN_NOMINAL_SIZE + TEST_NOMINAL_SIZE + PREDICTION_NOMINAL_SIZE ], all_fraud_set[ TRAIN_FRAUD_SIZE + TEST_FRAUD_SIZE : TRAIN_FRAUD_SIZE + TEST_FRAUD_SIZE + PREDICTION_FRAUD_SIZE ], ] ) ``` ```python theme={null} ## Separating relevant features data (excluding the "Time" column) from label data kaggle_headers = list(kaggle_full_set.columns.values) # all headers feature_cols = kaggle_headers[1:-1] # excluding Time and Class headers label_col = kaggle_headers[-1] # marking Class header as label selected_training_data = selected_training_set.loc[:, feature_cols] selected_training_labels = selected_training_set.loc[:, label_col] selected_testing_data = selected_testing_set.loc[:, feature_cols] selected_testing_labels = selected_testing_set.loc[:, label_col] selected_prediction_data = selected_prediction_set.loc[:, feature_cols] selected_prediction_true_labels = selected_prediction_set.loc[:, label_col] ``` # ## Visualizing the Selected Datasets with t-SNE t-SNE is a technique for dimensionality reduction that is particularly suited for the visualization of high-dimensional datasets: ```python theme={null} def scatter(x, colors): # Create a scatter plot f = plt.figure(figsize=(8, 8)) ax = plt.subplot(aspect="equal") sc = ax.scatter(x[:, 0], x[:, 1], lw=0, s=40, c=palette[colors.astype(np.int32)]) plt.xlim(-25, 25) plt.ylim(-25, 25) ax.axis("off") ax.axis("tight") # Add the labels for each digit txts = [] labels = ["Nominal", "Fraud"] for i in range(2): # Position of each label. xtext, ytext = np.median(x[colors == i, :], axis=0) txt = ax.text(xtext, ytext, labels[i], fontsize=24) txt.set_path_effects( [PathEffects.Stroke(linewidth=5, foreground="w"), PathEffects.Normal()] ) txts.append(txt) return ``` # ### TSNE Visualization of Train Data Observe that visually t-SNE shows a separation between nominal and anomalous samples. **However, the sole visualization map does not allow tracking of all fraudulent transactions.** This demonstrates the challenge of high-quality fraud detection. For the sake of a quick demonstration, take only a very small percentage of the data. Applying better logic for subselecting the training and testing datasets affects the quality of the results: ```python theme={null} proj = TSNE(random_state=RS).fit_transform(selected_training_data) scatter(proj, selected_training_labels) ``` output # ### TSNE Visualization of Test Data ```python theme={null} proj = TSNE(random_state=RS).fit_transform(selected_testing_data) scatter(proj, selected_testing_labels) ``` output # ## Reducing Dimensions Convert original features into fewer features to match the number of qubits. Perform dimensionality reduction to match the number of features with the number of qubits used in simulation. To do this, use principal component analysis and keep only the first *N\_DIM* principal components: ```python theme={null} ## Choose a data dimension to encode N_DIM = 3 ``` ```python theme={null} sample_train = selected_training_data.values.tolist() sample_test = selected_testing_data.values.tolist() sample_predict = selected_prediction_data.values.tolist() # Reduce dimensions pca = PCA(n_components=N_DIM).fit(sample_train) sample_train = pca.transform(sample_train) sample_test = pca.transform(sample_test) sample_predict = pca.transform(sample_predict) ``` # ## Normalizing Use feature-wise standard scaling, i.e., subtract the mean and scale by the standard deviation for each feature: ```python theme={null} # Normalize std_scale = StandardScaler().fit(sample_train) sample_train = std_scale.transform(sample_train) sample_test = std_scale.transform(sample_test) sample_predict = std_scale.transform(sample_predict) ``` # ## Scaling Scale each feature to a range between -$\pi$ and $\pi$: ```python theme={null} # Scale samples = np.append(sample_train, sample_test, axis=0) samples = np.append(samples, sample_predict, axis=0) minmax_scale = MinMaxScaler((-np.pi, np.pi)).fit(samples) FRAUD_TRAIN_DATA = minmax_scale.transform(sample_train) FRAUD_TEST_DATA = minmax_scale.transform(sample_test) FRAUD_PREDICT_DATA = minmax_scale.transform(sample_predict) ``` This is the final preprocessed dataset: ```python theme={null} FRAUD_TRAIN_LABELS = np.array(selected_training_labels.values.tolist()) FRAUD_TEST_LABELS = np.array(selected_testing_labels.values.tolist()) ``` ## 2. Map the Data to a Hilbert Space The feature map is a parameterized quantum circuit, which can be described as a unitary transformation $\mathbf{U_\phi}(\mathbf{x})$ on n qubits. Since the data may be non-linearly separable in the original space, the feature map circuit maps the classical data into the Hilbert space. The choice of which feature map circuit to use is key and may depend on the given dataset to classify. You can leverage the Classiq feature map design capabilities. # ## Designing a Feature Map As an example, choose from the well known second-order Pauli-Z evolution encoding circuit with two repetitions or the bloch sphere circuit encoding. Their definitions are in the [tutorial](https://github.com/Classiq/classiq-library/blob/main/algorithms/QML/qsvm/qsvm.ipynb). Pauli feature map: ```python theme={null} from classiq.applications.qsvm.quantum_feature_maps import pauli_feature_map PAULIS = [[Pauli.Z], [Pauli.Z, Pauli.Z]] CONNECTIVITY = 2 # Full AFFINES = [[1, 0], [1, np.pi]] REPS = 2 build_pauli_feature_map = lambda data, qba: pauli_feature_map( data, PAULIS, AFFINES, CONNECTIVITY, REPS, qba ) ``` Construct the quantum model for the QSVM routine: ```python theme={null} from classiq.applications.qsvm.qsvm import QSVM fraud_qsvm = QSVM(feature_map=build_pauli_feature_map, num_qubits=N_DIM) ``` # ## Viewing the Generated Quantum Circuit Before training, the quantum circuit used for kernel evaluation is accessible via `fraud_qsvm.get_qprog()` and can be viewed with `show`. For the Pauli feature map, the data dimension is the same as the number of qubits. ```python theme={null} qprog = fraud_qsvm.get_qprog(data_dim=N_DIM) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3DywSlw95HA0z4qQJZbBRFifcro ``` ## 3. Execute QSVM Quantum Support Vector Machines is the quantum version of SVM: a data classification method that separates the data using a hyperplane. The algorithm performs these steps \[[3](#3)]: 1. **Estimates** the kernel matrix: * A quantum feature map, $\phi(\mathbf{x})$, naturally gives rise to a quantum kernel, $k(\mathbf{x}_i,\mathbf{x}_j)= \phi(\mathbf{x}_j)^\dagger\phi(\mathbf{x}_i)$, which can be seen as a measure of similarity: $k(\mathbf{x}_i,\mathbf{x}_j)$ is large when $\mathbf{x}_i$ and $\mathbf{x}_j$ are close. * When considering finite data, you can represent the quantum kernel as a matrix: $K_{ij} = \left| \langle \phi^\dagger(\mathbf{x}_j)| \phi(\mathbf{x}_i) \rangle \right|^{2}$. Calculate each element of this kernel matrix on a quantum computer by calculating the transition amplitude: $ \left| \langle \phi^\dagger(\mathbf\{x\}_j)| \phi(\mathbf\{x\}_i) \rangle \right|^\{2\} = \left| \langle 0^\{\otimes n\} | \mathbf\{U_\phi^\dagger\}(\mathbf\{x\}_j) \mathbf\{U_\phi\}(\mathbf\{x_i\}) | 0^\{\otimes n\} \rangle \right|^\{2\}$ This provides an estimate of the quantum kernel matrix, which will be used in the support vector classification. 2. **Optimizes** the dual problem using the classical SVM algorithm to generate a separating hyperplane and classify the data: $$ L_D(\alpha) = \sum_{i=1}^t \alpha_i - \frac{1}{2} \sum_{i,j=1}^t y_i y_j \alpha_i \alpha_j K(\vec{x}_i \vec{x}_j) $$ where * $t$ is the number of data points * $\vec{x}_i$s are the data points * $y_i$ is the label $\in \{-1,1\}$ of each data point * $K(\vec{x}_i \vec{x}_j)$ is the kernel matrix element between the $i$ and $j$ data points * Optimizes over the $\alpha$s Expect most of the $\alpha$s to be $0$. The $\vec{x}_i$s that correspond to non-zero $\alpha_i$ are called the support vectors. # ## Running QSVM and Analyzing the Results # ### Training and Testing the Data 1. Build the train and test quantum kernel matrices: 2. For each pair of datapoints in the training dataset $\mathbf{x}_{i},\mathbf{x}_j$, apply the feature map and measure the transition probability: $K_{ij} = \left| \langle 0 | \mathbf{U}^\dagger_{\Phi(\mathbf{x_j})} \mathbf{U}_{\Phi(\mathbf{x_i})} | 0 \rangle \right|^2$. 3. For each training datapoint $\mathbf{x_i}$ and testing point $\mathbf{y_i}$, apply the feature map and measure the transition probability: $K_{ij} = \left| \langle 0 | \mathbf{U}^\dagger_{\Phi(\mathbf{y_i})} \mathbf{U}_{\Phi(\mathbf{x_i})} | 0 \rangle \right|^2$. 4. Use the train and test quantum kernel matrices in a classical support vector machine classification algorithm. Execute QSVM by calling `train`, `test`, and `predict` on the `fraud_qsvm` object: ```python theme={null} fraud_qsvm.train(FRAUD_TRAIN_DATA, FRAUD_TRAIN_LABELS) test_score, y_test = fraud_qsvm.test(FRAUD_TEST_DATA, FRAUD_TEST_LABELS) predicted_labels = fraud_qsvm.predict(FRAUD_PREDICT_DATA) ``` ```python theme={null} print("quantum kernel classification test score: %0.2f" % (test_score)) ``` **Output:** ``` quantum kernel classification test score: 0.92 ``` The result seems OK. This is a good start! # ### Analyzing Now analyze further by comparing the testing accuracy results to classical kernels: ```python theme={null} classical_kernels = ["linear", "poly", "rbf", "sigmoid"] for ckernel in classical_kernels: classical_svc = SVC(kernel=ckernel) classical_svc.fit(FRAUD_TRAIN_DATA, FRAUD_TRAIN_LABELS) classical_score = classical_svc.score( FRAUD_TEST_DATA, np.array(FRAUD_TEST_LABELS.tolist()) ) print("%s kernel classification test score: %0.2f" % (ckernel, classical_score)) ``` **Output:** ``` linear kernel classification test score: 1.00 poly kernel classification test score: 1.00 rbf kernel classification test score: 1.00 sigmoid kernel classification test score: 0.98 ``` Given the simple naive training and testing set example, the statement that **quantum kernel gets results at least as good as the classical kernels** can be interpreted as a promising sign and encouragement towards deeper research. Bear in mind: * Quantum kernel machine algorithms only have the potential of quantum advantage over classical approaches if the corresponding quantum kernel is hard to estimate classically (a necessary and not always sufficient condition to obtain a quantum advantage). * However, it was recently proven \[[4](#4)] that learning problems exist for which learners with access to quantum kernel methods have a quantum advantage over all classical learners. # ### Predicting Data Finally, predict unlabeled data by calculating the kernel matrix of the new datum with respect to the support vectors: $$ \text{Predicted Label}(\vec{s}) = \text{sign} \left( \sum_{i=1}^t y_i \alpha_i^* K(\vec{x}_i , \vec{s}) + b \right) $$ where * $\vec{s}$ is the datapoint to classify * $\alpha_i^*$ are the optimized $\alpha$s * $b$ is the bias ```python theme={null} true_labels = np.array(selected_prediction_true_labels.values.tolist()) sklearn.metrics.accuracy_score(predicted_labels, true_labels) ``` **Output:** ``` 0.9166666666666666 ``` ## Quantum Advantage Is Possible QSVM has the potential to enhance performance, accuracy, and even efficiency in resources: * There are limitations to the successful classical solutions when the feature space becomes large and the kernel functions become computationally expensive to estimate. * For certain types of data, a classifier that exploits the quantum feature space shows better results. * A necessary condition to obtain a quantum advantage is that the kernel cannot be estimated classically. Since quantum computers are not expected to be classically simulable there is a very large design space to explore. It becomes intriguing to find suitable feature maps for this technique with provable quantum advantages while providing significant improvement on real world datasets. With the ubiquity of kernel methods in machine learning, in the future the techniques may produce applications even beyond binary classification. ## References \[1] [Oleksandr Kyriienko, Einar B. Magnusson. (2022). Unsupervised quantum machine learning for fraud detection. Preprint.](https://arxiv.org/abs/2208.01203) \[2] \[Kaggle dataset * Credit Card Fraud Detection: Anonymized credit card transactions labeled as fraudulent or genuine.]\([https://www.kaggle.com/datasets/mlg-ulb/creditcardfraud](https://www.kaggle.com/datasets/mlg-ulb/creditcardfraud)) \[3] [Havlíček, V., Córcoles, A.D., Temme, K. et al. (2019). Supervised learning with quantum-enhanced feature spaces. Nature 567, 209-212.](https://doi.org/10.1038/s41586-019-0980-2) \[4] [Liu et al. (2020). A rigorous and robust quantum speed-up in supervised machine learning.](https://arxiv.org/pdf/2010.02174.pdf) # Portfolio Optimization with Hybrid HHL Algorithm Source: https://docs.classiq.io/explore/applications/finance/hybrid_hhl_for_portfolio_optimization/portfolio_optimization_with_hhl Open this notebook in GitHub to run it yourself ## 1. Introduction * Portfolio Optimization **Below we briefly present the problem at hand, as well as its mathematical formulation** Modern portfolio optimization \[[1](#portfoliowiki)] is the process of allocating a portfolio of financial assets optimally, according to some predetermined goal. Usually, the goal is to maximize the potential return while minimizing the financial risk of the portfolio. In similar to many other real-world problems, one can express the portfolio optimization problem as a set of linear equations. In this demo, we show how a quantum linear solver, the HHL algorithm \[[2](#hhl)], or its hybrid variance \[[3](#hybrid-hhl)], can be used to solve this problem. We demonstrate how using functional modeling can be leveraged for constructing different models and obtaining different quantum programs for a given set of constraints. \*In this demo we focus on the case of continuous variable for the allocation vector. The scenario of discrete allocation falls into the domain of Combinatorial Optimization Problems, for which one can employ the QAOA (Quantum Approximate Optimization Ansatz) approach.\* # ## 1.1 Portfolio Optimization Problem as a Linear Equation As a first step, we have to model the problem mathematically: * A portfolio is built from a pool of $N$ financial assets. * The assets' return values are random variables, whose statistical properties are represented by the expected return vector $\vec{\mu} \in \mathbb{R}^N$ and a covariance matrix $\Sigma\in \mathbb{R}^N\times\mathbb{R}^N$. * The prices of the assets today are represented by the prices vector $\vec{p}\in \mathbb{R}^N$. * The portfolio has an allocation vector $\vec{w} \in \mathbb{R}^N$, representing the weight of each asset in the portfolio -- * *this is the variable we would like to find*. * Finally, the portfolio problem has two constants: a desired total return $R$, and a total budget $B$. The task is to find a solution for the allocation vector $\vec{w}$ which minimizes the risk $$ \min_{\vec{w}} \vec{w}^T\cdot \Sigma \cdot \vec{w}, $$ under the constraints of $$ \begin{aligned} \text{getting the desired total return: } R&=\vec{\mu}^T\cdot\vec{w},\\ \text{satisfying the total known budget: } B&=\vec{p}^T\cdot\vec{w} \end{aligned} $$ Writing this problem as a constrained minimization problem in a continuous space, we get a set of linear equations: $$ \begin{pmatrix} 0 & 0 & \vec{\mu}^T \\ 0 & 0 & \vec{p}^T \\ \vec{\mu} & \vec{p} & \Sigma \\ \end{pmatrix} \begin{pmatrix} \nu_1 \\ \nu_2 \\ \vec{w} \\ \end{pmatrix} = \begin{pmatrix} R \\ B \\ \vec{0} \\ \end{pmatrix}, \quad (1) $$ where $\nu_{1,2}$ are Lagrange multipliers. **comment**: one can rescale the problem and set the total budget to $B=1$. Equation (1) is of the form of a linear equation: $$ A\cdot \vec{x} = \vec{b} \quad (2) $$ ## 2. The HHL Algorithm **Below we briefly present the fundamental HHL algorithm, and its hybrid variance** # ## 2.1 The Basic HHL Algorithm The HHL algorithm \[[2](#hhl)] (after Harrow, Hassidim, and Lloyd) is a quantum linear solver, treating problems which can be defined by Eq. (1). The basic implementation assumes that the $\vec{b}$ is a normalized vector $|\vec{b}|=1$ of size $2^n$, and that $A$ is an Hermitian matrix of size $2^n\times 2^n$, whose eigenvalues are in the interval $[0,1)$. For the mathematical introduction we will follow these assumptions, whereas in the following sections we show how to tweek the quantum model in order to relax them. The basic HHL algorithm contains 4 functional blocks: 1. **State Preparation for the vector $\vec{b}$**: Let us assume that $\vec{b}$ is of size $2^n$, then: $$ |0\rangle_n \xrightarrow[{\rm SP}]{} \sum^{2^n-1}_{i=0}b_i|i\rangle_n. \quad (3) $$ 2. **Quantum Phase Estimation (QPE) for the unitary $e^{2\pi iA}$**: This quantum block approximates the eigenvalues of the matrix $A$, $\{\lambda_i\}^{2^n}_{i=1}$, into a register of size $m$. If we write the vector $\vec{b}$ in the basis of eigenvectors of $A$, $\sum^{2^n-1}_{i=0}b_i|i\rangle_n = \sum^{2^n-1}_{j=0}\beta_j|\psi_j\rangle_n$, then the QPE stage gives $$ |0\rangle_m \sum^{2^n-1}_{j=0}\beta_j|\psi_j\rangle_n \xrightarrow[{\rm QPE}(e^{2\pi i A})]{} \sum^{2^n-1}_{j=0}\beta_j |\tilde{\lambda}_j\rangle_m |\psi_j\rangle_n, \quad (4) $$ where $\tilde{\lambda}=\frac{1}{2^m}\sum^{2^m-1}_{k=0}\tilde{\lambda}^{(k)}2^k$ with $\tilde{\lambda}^{(k)}$ being the state of the $k$-th qubit. Thus, $m$ is the precision of the binary representation of $\tilde{\lambda}$, which in turn is an approximation of the actual eigenvalue of $A$, $\lambda$. 3\. **Eigenvalue Inversion:** This quantum block adds an extra qubit $$ |0\rangle\sum^{2^n-1}_{j=0}\beta_j |\tilde{\lambda}_j\rangle_m |\psi_j\rangle_n \xrightarrow[{\rm Eig. \, Inversion}]{} |1\rangle\left(\sum^{2^n-1}_{j=0}\frac{C}{\tilde{\lambda}_j}\beta_j |\tilde{\lambda}_j\rangle_m|\psi_j\rangle_n\right)+ |0\rangle\left(\sum^{2^n-1}_{j=0}\sqrt{1-\frac{C^2}{\tilde{\lambda}^2_j}}\beta_j |\tilde{\lambda}_j\rangle_m |\psi_j\rangle_n\right), \quad (5) $$ where $C$ is a normalization factor which gurrentees that the amplitudes are not larger than 1. 4\. **Inverese QPE for the unitary $e^{2\pi iA}$**: This operation acts on the QPE register of size $m$ and aims to return the phase registers to zero. The final state is: $$ \tag{6} |0\rangle_m|1\rangle\left(\sum^{2^n-1}_{j=0}\frac{C}{\tilde{\lambda}_j}\beta_j |\psi_j\rangle_n\right)+ |0\rangle_m|0\rangle\left(\sum^{2^n-1}_{j=0}\sqrt{1-\frac{C^2}{\tilde{\lambda}^2_j}}\beta_j |\psi_j\rangle_n\right). $$ # ### 2.1.1 Getting the Result * A Swap-Test Example Looking at Eq. (6) we can see that the result vector $\vec{x}$ is coupled to the indicator state $|1\rangle$ (up to normalization with the known constant $C$): $$ \sum^{2^n-1}_{j=0} \frac{C}{\lambda_j}\beta_j \vec{\psi_j} = C\vec{x}. \quad (7) $$ In order to obtain the solution from the quantum circuit we must measure the complete statevector, however, this exponential readout procedure halts quantum advantage. In the framework of portfolio optimization, it was suggested that one can use a *swap-test* \[[4](#swapwiki)] to compare the result to some guess of the solution. Let us write the quantum state at the final stage of the HHL algorithm, Eq. (6), as $C|\vec{x}||1\rangle|\hat{\tilde{x}}\rangle_{n}+\alpha|0\rangle |\zeta\rangle_{n}$, where we neglect the QPE register, and $\alpha|\zeta\rangle$ is some non-normalized state. For the swap-test we add an extra test qubit $|0\rangle_{\rm test}$ and we need to prepare a quantum register with the guessed solution $|\hat{y}\rangle$. Applying the swap-test gives the final state: $$ \frac{C|\vec{x}|}{2}|1\rangle\left[ |0\rangle_{\rm test}\left(|\hat{\tilde{x}},\hat{y}\rangle_{2n}+|\hat{y},\hat{\tilde{x}}\rangle_{2n} \right)+ |1\rangle_{\rm test}\left(|\hat{\tilde{x}},\hat{y}\rangle_{2n}-|\hat{y},\hat{\tilde{x}}\rangle_{2n} \right) \right] + \text{non-interesting state} \quad (8) $$ From the above equation we find that the probability of measuring 1 on the HHL indicator qubit and 0 on the test qubit is $$ P(\text{test}=0,\text{indictor}=1) = \frac{C^2|\vec{x}|^2}{2} \left(1 + |\langle \hat{\tilde{x}}|\hat{y}\rangle|^2 \right)= \frac{P(\text{indictor}=1)}{2} \left(1 + |\langle \hat{\tilde{x}}|\hat{y}\rangle|^2 \right). \quad (9) $$ From this equation one can determine the desired overlap $|\langle \hat{\tilde{x}}|\hat{y}\rangle|^2 $. # ## 2.2 A Hybrid Approach The hybrid approach \[[3](#hybrid-hhl)] comes to facilitate the Eigenvalue Inversion function which is not easy to implement. This quantum block can be obtained using multi-controlled RY rotations: $$ \tag{10} |0\rangle|\lambda\rangle_m \xrightarrow[{\rm multi-controlled-RY}(2\arcsin(C/\lambda))]{} \frac{C}{\lambda}|1\rangle|\lambda\rangle_m+\sqrt{1-\frac{C^2}{\lambda^2}}|0\rangle|\lambda\rangle_m. $$ However, to apply this procedure we should have a quantum block which caclulates $\arcsin{C/\lambda}$, and such general arithmetics requires many qubits. Another approach is to use a generic amplitude loading for the function $f(x)=C/x$, but this procedure contains exponential number, $2^m$, of multi-controlled rotations (the gray-code approch might reduce the gate count by some factor, but the total gate count is still $O(2^m)$). The hybrid approach containes three steps: 1. **QPE of $e^{2\pi i A}$ on the State Preparation of $\vec{b}$**: This is simply the first two steps of the HHL algorithm, corresponding to the final state $\sum^{2^n-1}_{j=0}\beta_j |\tilde{\lambda}_j\rangle_m |\psi_j\rangle_n$. 2. **Classical post-process**: We analyze the results in the context of the following two facts: (i) The matrix $A$ has $2^n$ eigenvalues, whereas the QPE gives approximation of these over $2^m$ different values. (ii) Not all the eigenvalues of $A$ are relevant to the solution, but only eigenvalues such that $\beta_j/\lambda_j>\epsilon$, with $\epsilon$ being some user-defined tolarance. Based on these two points we can try to find a set of relevant eigenvalues $\{\tilde{\lambda}_s\}^r_{s=1}$ to our problem. 1. **A full HHL routine**: We now apply a usual HHL algorithm, where the Eigenvalue Inversion block is implemented by hard-coded multi-controlled-RY rotation according to the specific values found in the previous step. Moreover, we can represent the relevant eigenvalues with a smaller binary represantation of size $m' ## 3. Eigenvalue Inversion Quantum Functions We define two eigenvalue inversion functions, one which rotates over all eigenvalues, and another rotating on a reduced set of these. In addition, we take into account an affine transformation of the matrix, and its resulting transformation on the eigenvalues. The affine transformation is necessary as the QPE is applied on a matrix whose eigenvalues are in $[0,1)$. We can define a transformation $A = \gamma A_{\rm qpe}+\delta$ with a rescaling factor $\gamma$ and a shift value $\delta$. The eigenvalues of the original matrix are transformed in a similar way. In the HHL algorithm, we use QPE to store the eigenvalues of the transformed matrix $A_{\rm qpe}$, but perform eigenvalue inversion for the original matrix $A$. Thus, inversion of an eigenvalue corresponds to the following operation: $$ |0\rangle|\lambda\rangle_m \xrightarrow[{\rm eig-inversion}]{} \frac{C}{\gamma\lambda+\delta}|1\rangle|\lambda\rangle_m+\sqrt{1-\left(\frac{C}{\gamma\lambda+\delta}\right)^2}|0\rangle|\lambda\rangle_m. $$ The normalization parameter $C$ gurentees that the amplitudes' norm is $\leq 1$. # ## 3. 4. Full Eigenvalue Inversion Function First, we define a quantum function that performs a simple eigenvalue inversion with Classiq built-in amplitude loading function, `assign_amplitude_table`. ```python theme={null} from classiq import * @qfunc def simple_eig_inv( gamma: float, delta: float, c_param: float, phase: QNum, indicator: Output[QBit], ): allocate(1, indicator) assign_amplitude_table( lookup_table(lambda p: np.clip(c_param / ((gamma * p) + delta), -1, 1), phase), phase, indicator, ) ``` # ## 3. 4. Partial Eigenvalue Inversion, with Prescribed Values Here, we apply hard-coded controlled rotations, as described in Sec. 2. 3. Moreover, we take into account a situation where the rotation angles are given with higher precision than the eigenvalues stored in the phase variable returning from the QPE. ```python theme={null} from classiq.qmod.symbolic import asin, floor @qfunc def reduced_hardcoded_eig_inversion( gamma: CReal, delta: CReal, c_param: CReal, eigs: CArray[CReal], # list of eigenvalues to rotate phase: QNum, indicator: Output[QBit], ) -> None: allocate(1, indicator) integer_phase = QNum("integer_phase", phase.size, False, 0) bind(phase, integer_phase) repeat( eigs.len, lambda index: control( integer_phase == floor( eigs[index] * 2**phase.size ), # currently control only supports int lambda: RY(2 * asin(c_param / (gamma * eigs[index] + delta)), indicator), ), ) bind(integer_phase, phase) ``` ## 4. Taking a Specific Example We consider a specific portfolio optimization example for the demonstration. We focus on a simple case of 2 assets, and define the corresponding set of linear equations (a $2\times 2$ problem is represented by `n_unitary=2` qubits, for more complex examples see Sec. 8): ```python theme={null} import numpy as np N_ASSETS = 2 # number of assets COVARIANCE_MAT = np.array( [ [ 0.2, 0.04, ], [0.04, 0.4], ] ) RETURNS_VEC = np.array([0.04, 0.64]) PRICES_VEC = np.array([1, 0.8]) TOTAL_BUDGET = 1 EXPECTED_RETURN = 2.4 b_vec = np.concatenate( [np.array([EXPECTED_RETURN, TOTAL_BUDGET]), np.zeros(N_ASSETS)] ).T a_mat = np.block( [ [np.zeros([1, 2]), RETURNS_VEC], [np.zeros([1, 2]), PRICES_VEC], [ RETURNS_VEC.reshape(N_ASSETS, 1), PRICES_VEC.reshape(N_ASSETS, 1), COVARIANCE_MAT, ], ] ) n_unitary = N_ASSETS + 2 # matrix size ``` # ## 4.1 Preparing the Vector $\vec{b}$ We shall normalize the vector $\vec{b}$ before loading it into a quantum register. In addition, the user can control the functional error of this function block. ```python theme={null} b_vec_norm = np.linalg.norm(b_vec) normalized_b_vec = (b_vec / b_vec_norm).tolist() SP_ERROR = 0.0 ``` # ## 4.2 Calculating the Maximal Eigenvalue As we will see below, the HHL algorithm assumes that we have some bound on the maximal eigenvalue (in absolute value) of the matrix. For the sake of demonstration, we will perform a full classical eigenvalue decomposition. This also serves us for verifying the QPE in Step 1. ```python theme={null} w, v = np.linalg.eig(a_mat) w_max = max(np.abs(w)) ``` # ## 4.3 Preparing the Unitary from the Matrix $A$ for the Quantum Phase Estimation The QPE function gets a phase quantum variable, whose size refers to the precision of the eigenphases estimations. We set it with to 4. (note that this number corresponds to the condition number of the matrix $\kappa \equiv \lambda_{max}/\lambda_{min}$). ```python theme={null} QPE_SIZE = 4 ``` As explained in Sec. 3, we have to classically transform our matrix to have eigenvalues in $[0,1)$, and apply an inverse transformation when performing the eigenvalue inversion block. We take the following matrix transformation: $$ A_{\rm qpe} = \left(1-2^{\rm qpe-size}\right)(A+\lambda_{max})/2\lambda_{max}. $$ ```python theme={null} import scipy mat_shift = w_max # assures only positive eivenvalues mat_rescaling = (1 - 1 / 2**QPE_SIZE) / ( 2 * w_max ) # assures eigenvalues in [0,1-1/2^QPE_SIZE] a_mat_qpe = (a_mat + np.identity(n_unitary) * mat_shift) * mat_rescaling # rescaled and shifted matrix w_min = w_max / ( 2**QPE_SIZE - 1 ) # this is the minimal eigenvalue which can be resolved by the QPE print("the maximal eigenvalue found:", w_max) print("the minimal resolved eigenvalue with qpe of size", QPE_SIZE, ":", w_min) ``` **Output:** ``` the maximal eigenvalue found: 1.547651922176089 the minimal resolved eigenvalue with qpe of size 4 : 0.10317679481173926 ``` **The transformation to $A_{\rm qpe}$, whose eigenvalues are in $[0,1)$, implies the inverse transformation for the eigenvalue inversion with: $\gamma=1/$ `mat_rescaling`, $\delta=-$ `mat_shift`, and $C=$`w_min` respectively**. ## 5. Solving with a Hybrid HHL \-- * For the demonstration we use the exact unitary, namely, we calculate classically $e^{2\pi i A_{\rm qpe}}$. We then use the built-in quantum function `unitary` for decomposing the unitary into quantum gates. Note that this procedure is not scalable, as the decomposition is exponential in the number of qubits. In addition, the classical calculation is equivalent to solving the linear equation. Yet, this approach is good for small-scale problems. *One relevant way to implement the unitary is with Hamiltonian simulation, see Sec. (8)* \-- * ```python theme={null} unitary_mat = scipy.linalg.expm(1j * 2 * np.pi * a_mat_qpe).tolist() ``` # ## 5. 6. Step 1: A QPE Applied on the StatePreparation of $\vec{b}$ Building a quantum model, synthesizing to a quantum program, and executing to get the results ```python theme={null} NUM_SHOTS = 2048 backend_preferences = ClassiqBackendPreferences( backend_name=ClassiqSimulatorBackendNames.SIMULATOR ) @qfunc def main(phase_result: Output[QNum[QPE_SIZE, False, QPE_SIZE]]) -> None: state = QArray("state") allocate(QPE_SIZE, False, QPE_SIZE, phase_result) prepare_amplitudes(normalized_b_vec, SP_ERROR, state) qpe(unitary=lambda: unitary(unitary_mat, state), phase=phase_result) drop(state) qmod_qpe = create_model(main) qmod_qpe = set_execution_preferences( qmod_qpe, ExecutionPreferences(num_shots=NUM_SHOTS, backend_preferences=backend_preferences), ) ``` ```python theme={null} from classiq import execute, synthesize qprog_qpe = synthesize(qmod_qpe) results = execute(qprog_qpe).result() res_qpe = results[0].value ``` We can visualize the circuit ```python theme={null} show(qprog_qpe) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/36pbc2hFJmpoA5SgVeJVJW2anPQ ``` step_1_qpe.png # ## 5. 6. Step 2: Post-Processing the Results to Get Data for the Eigenvalue Inversion To analyze the results we generate the following 4 lists: * `qpe_eigs`: the values of the QPE eigenvalues $\lambda_{\rm qpe}$, * `original_eigs`: the values of the original matrix eigenvalues, * `eigs_prob`: the probability of each eigenvalue, this corresponds to $\beta^2_j$ in Eq. (4). * `projected_x`: the effect of each phase on the solution $\vec{x}$, given by $\beta_j/\lambda_j$. ```python theme={null} qpe_eigs = np.array([sample.state["phase_result"] for sample in res_qpe.parsed_counts]) original_eigs = np.array([mat_rescaling ** (-1) * eig - mat_shift for eig in qpe_eigs]) eigs_prob = np.array([sample.shots / NUM_SHOTS for sample in res_qpe.parsed_counts]) projected_x = np.sqrt(eigs_prob) / original_eigs ``` Let us examine the measured phases and their contribution to the solution vector $\vec{x}$. ```python theme={null} import matplotlib.pyplot as plt fig, axs = plt.subplots(2) fig.suptitle("Distribution of eigenvalues") axs[0].plot(original_eigs, eigs_prob, color="red", marker="o", linestyle="None") axs[0].set_ylabel("$P(\\lambda)$") axs[1].plot( original_eigs, np.abs(projected_x), color="blue", marker="o", linestyle="None" ) axs[1].set_ylabel("$\\sqrt{P(\\lambda)}/|\\lambda|$") axs[1].set_xlabel("$\\lambda$") ``` **Output:** ``` Text(0.5, 0, '$\\lambda$') ``` output What can we learn from the data? We are dealing with a $4 \times 4$ matrix, thus it has only 4 eigenvalues. The QPE tries to approximate this with binary representation of size `QPE_SIZE`=4, resulting in $2^4=16$ values. We can take the two most significant eigenvalues for which we will apply the eigenvalue inversion. First, find the the 4 eigenvalues. From the upper graph we can see 4 local maxima, which can be attributed for the four eigenvalues in our problem. ```python theme={null} eig_maxima_indices = np.argpartition(eigs_prob, -n_unitary)[-n_unitary:][::-1] ``` Then, from the lower graph we can choose, out of the 4 eigenvalues, the ones with the highest projection on the solution. Take the two most significant ones: ```python theme={null} reduced_eig_indices = np.argpartition(abs(projected_x[eig_maxima_indices]), -2)[-2:][ ::-1 ] reduced_eig = (qpe_eigs[reduced_eig_indices]).tolist() ``` One advantage of the hybrid approach is to reduce the QPE size in the full HHL step. Let us reduce it by one qubit: ```python theme={null} qpe_reduced_size = QPE_SIZE - 1 ``` Let us plot the chosen eigenvalues, as well as the exact eigenvalues obtained from the classical decomposition: ```python theme={null} reduced_eigs = ( np.array( [ np.floor(eig * 2**qpe_reduced_size) / 2**qpe_reduced_size for eig in reduced_eig ] ) * mat_rescaling ** (-1) - mat_shift ) fix, ax = plt.subplots() pt2 = ax.plot(original_eigs, eigs_prob, color="red", marker="s", linestyle="None") pt1 = ax.plot( reduced_eigs, eigs_prob[eig_maxima_indices][reduced_eig_indices], color="cyan", marker="o", linestyle="None", ) pt3 = ax.plot([w, w], [[-0.1] * 4, [max(eigs_prob)] * 4], ":k") ax.set_ylim(0, max(eigs_prob) * 1.05) ax.set_title("Distribution of eigenvalues") ax.set_ylabel("$P(\\lambda)$") ax.set_xlabel("$\\lambda$") ax.legend(["distribution from QPE", "Model C", "exact values"], loc="upper right") ``` **Output:** ``` ``` output **Comment: the effect of trimming the LSB cannot be observed from the plot as it is 0 in our case. To see the effect one should look at the distribution of eigenvalues obtained from a QPE of size 3.** # ## 5. 6. Step 3: A Full HHL Routine, Including a Swap Test with the Classical Solution Let us build the HHL model, setting two outputs: * `indicator` of size 1. When measured 1 it means the unitary register collapsed to the linear problem solution. * `test` of size 1. The test qubit of the swap test. We take the classical solution of the linear problem for the comparison ```python theme={null} sol_classical = np.linalg.solve(a_mat, normalized_b_vec) compared_sol = sol_classical amp_compared = (compared_sol / np.linalg.norm(compared_sol)).tolist() ``` ```python theme={null} @qfunc def main(indicator: Output[QBit], test: Output[QBit]) -> None: state = QArray() compared_state = QArray() phase_var = QNum() allocate(qpe_reduced_size, False, qpe_reduced_size, phase_var) prepare_amplitudes(normalized_b_vec, SP_ERROR, state) within_apply( lambda: qpe(unitary=lambda: unitary(unitary_mat, state), phase=phase_var), lambda: reduced_hardcoded_eig_inversion( gamma=mat_rescaling ** (-1), delta=-mat_shift, c_param=w_min, eigs=reduced_eig, phase=phase_var, indicator=indicator, ), ) prepare_amplitudes(amp_compared, 0.0, compared_state) swap_test(state, compared_state, test) drop(state) drop(compared_state) drop(phase_var) qmod_feed_forward_hhl = create_model(main) ``` # ### 5. 3. 4. Synthesizing the Quantum Model and Executing the Resulting Quantum Program We can pass different constraints and preferences to the synthesis engine. For the demonstration let us assume that we are interested in a quantum circuit with up to `max_width`=12 qubits, and ask for optimization over depth. ```python theme={null} constraints = Constraints( max_width=12, optimization_parameter=OptimizationParameter.DEPTH, ) qmod_feed_forward_hhl = set_constraints(qmod_feed_forward_hhl, constraints) ``` For large cases, optimization might be challenging; It is always good to set some timeout for the engine. ```python theme={null} preferences = Preferences( optimization_timeout_seconds=90, ) qmod_feed_forward_hhl = set_preferences(qmod_feed_forward_hhl, preferences) ``` Finally, for the execution preferences we can set the one already defined in step 1. ```python theme={null} qmod_feed_forward_hhl = set_execution_preferences( qmod_feed_forward_hhl, ExecutionPreferences(num_shots=NUM_SHOTS, backend_preferences=backend_preferences), ) ``` We are now ready for synthesizing and executing our model ```python theme={null} qprog_feed_forward_hhl = synthesize(qmod_feed_forward_hhl) results_C = execute(qprog_feed_forward_hhl).result() ``` We can visualize the circuit ```python theme={null} show(qprog_feed_forward_hhl) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/36pbdaNoPz05TgopRESXMoWLlny ``` step_3_closed.png step_3_open.png # ### 5. 3. 4. Post-Processing the Results Below we post-process the results to find the overlap between the HHL solution and the classical one. We are interested in the probability of that the test qubit being at state 0, under the condition that the indicator is measured at state 1. Following Eq. (9) we have: ```python theme={null} res_hhl = results_C[0].value fidelity_C = np.sqrt( res_hhl.counts_of_multiple_outputs(["indicator", "test"])[("1", "0")] * 2 / (res_hhl.counts_of_output("indicator")["1"]) - 1 ) print("Fidelity between hybrid HHL and classical solutions:", fidelity_C) ``` **Output:** ``` Fidelity between hybrid HHL and classical solutions: 0.9099214192705388 ``` # ### 5. 3. 4. Synthesizing with Different Constraints Let us change the constraints and optimize over the number of qubits. This will result in a different circuit ```python theme={null} constraints = Constraints( optimization_parameter=OptimizationParameter.WIDTH, ) qmod_hhl_optimized_width = qmod_feed_forward_hhl qmod_hhl_optimized_width = set_constraints(qmod_hhl_optimized_width, constraints) qprog_hhl_optimized_width = synthesize(qmod_hhl_optimized_width) show(qprog_hhl_optimized_width) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/36pbf9f629Td6hHpMb8YO5GRUji ``` step_3_less_qubits.png ## 6. Comparing with a Basic HHL Approach For comparison, below we design, synthesize and execute a a non-hybrid HHL, using the `simple_eig_inversion` function defined in Sec 3. 1. ```python theme={null} @qfunc def main(indicator: Output[QBit], test: Output[QBit]) -> None: state = QArray() compared_state = QArray() phase_var = QNum() allocate(QPE_SIZE, False, QPE_SIZE, phase_var) prepare_amplitudes(normalized_b_vec, SP_ERROR, state) within_apply( lambda: qpe(unitary=lambda: unitary(unitary_mat, state), phase=phase_var), lambda: simple_eig_inv( gamma=mat_rescaling ** (-1), delta=-mat_shift, c_param=w_min, phase=phase_var, indicator=indicator, ), ) prepare_amplitudes(amp_compared, 0.0, compared_state) swap_test(state, compared_state, test) drop(state) drop(compared_state) drop(phase_var) qmod_hhl_basic = create_model(main) qmod_hhl_basic = set_constraints(qmod_hhl_basic, constraints) qmod_hhl_basic = set_preferences(qmod_hhl_basic, preferences) qmod_hhl_basic = set_execution_preferences( qmod_hhl_basic, ExecutionPreferences(num_shots=NUM_SHOTS, backend_preferences=backend_preferences), ) qprog_hhl_basic = synthesize(qmod_hhl_basic) show(qprog_hhl_basic) results_basic = execute(qprog_hhl_basic).result() res_hhl_basic = results_basic[0].value fidelity_basic = np.sqrt( res_hhl_basic.counts_of_multiple_outputs(["indicator", "test"])[("1", "0")] * 2 / (res_hhl_basic.counts_of_output("indicator")["1"]) - 1 ) print("Fidelity between basic HHL and classical solutions:", fidelity_basic) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/36pbg8VpPK9AThkVk6BpgjfDlEj Fidelity between basic HHL and classical solutions: 0.951661902861773 ``` ## 7. Summary As a summary, let us collect all the properties of the different circuits we obtained, and plot the results in a table ```python theme={null} from classiq import QuantumProgram depths = list() widths = list() cx_counts = list() fidelities = [fidelity_C, fidelity_C, fidelity_basic] for qprog in [qprog_feed_forward_hhl, qprog_hhl_optimized_width, qprog_hhl_basic]: depths.append(qprog.transpiled_circuit.depth) widths.append(qprog.data.width) cx_counts.append(qprog.transpiled_circuit.count_ops["cx"]) ``` ```python theme={null} import pandas as pd names = ["hyb. HHL C (depth)", "hyb. HHL C (width)", "basic HHL"] df = pd.DataFrame( list(zip(widths, depths, cx_counts, fidelities)), columns=["num. qubits", "depth", "cx_count", "fidelity"], index=names, ) df ``` | | num. qubits | depth | cx\_count | fidelity | | ------------------ | ----------- | ----- | --------- | -------- | | hyb. HHL C (depth) | 12 | 416 | 260 | 0.909921 | | hyb. HHL C (width) | 9 | 443 | 268 | 0.909921 | | basic HHL | 10 | 477 | 304 | 0.951662 | Already at this small scale problem we can see reduced properties of the hybrid HHL compared to the basic implementation. This should be more pronounced in larger problems (the properties of the QPE in step 1 of the hybrid approach are not taken into account here). ## 8. Comments # ## 8.1 Hamiltonian Simulation As mentioned above, the correct way to implement the unitary on which we apply the QPE is Hamiltonian simulation. One way to apply this quantum function is with the built-in `exponentiation_with_depth_constraint` function. This includes another level of approximation to our algorithm. The exponentiation is implemented with Trotter-Suzuki product. Estimating the functional error of such implementation is a hard problem. An indirect way to manage the error is by increasing the depth of Exponentiation block in order to allow the engine to explore higher order Trotter-Suzuki products. Another option is to use `suzuki_trotter` functions directly. Both functions are then should be plugged it into `qpe` or `qpe_flexible` function, instead of plugging the built-in `unitary`; see [HHL notebook](https://github.com/Classiq/classiq-library/blob/main/algorithms/quantum_linear_solvers/hhl/hhl.ipynb). # ## 8.2 Approximating the Maximal Relevant Eigenvalue In Ref.\[[5](#nisq-hhl)] it was shown how one can use an hybrid quantum-classical routine in order to increase the precision in the eigenvalue resolution in step 1 of the hybrid HHL. The algorithm is based on running this step with various evolution coefficients $\gamma$ in $e^{i\gamma 2\pi A_{\rm qpe}}$, and find the optimal one (in a sense that we increase the resolution in the eigenvalue estimation, while using the same `qpe_size` ). Note that while this algorithm increases eigenvalue precision it increases the evolution coeffecient, as typically $\gamma>1$. This in turn requires more resources for performing well-approximated Exponentiation. # ## 8.3 Using Iterative QPE In Ref.\[[5](#nisq-hhl)] it was shown how one can use an iterative-QPE approach to perform step 1 in the hybrid HHL routine. This allows to get an estimate of the eigenvalues with only one qubit, instead of `qpe_size` qubits. # ## 8.4 $\log(\text{Problem size})$ Is Not an Integer In the problem above we have only 2 assets, which results in a problem with matrix size $2+2=4$. In general, a case of $N$ assets will result in a matrix of size $N+2$ (see Eq. (1)). In case that $\log(N+2)$ is not an integer we can complete the matrix dimension to the closest $2^n$ with an identity matrix. The vector $\vec{b}$ will be completed with zeros. $$ \begin{pmatrix} A & 0 \\ 0 & I \end{pmatrix} \begin{pmatrix} \vec{b} \\ 0 \end{pmatrix} = \begin{pmatrix} \vec{x} \\ 0 \end{pmatrix}. $$ This procedure shall not affect the swap-test part, all need to be done is to load a guessed solution as $(\vec{y},0)$. ## References \[1]: [Portfolio Optimization (Wikipedia)](https://en.wikipedia.org/wiki/Portfolio_optimization) \[2]: [Harrow, A. W., Hassidim, A., & Lloyd, S., Quantum Algorithm for Linear Systems of Equations. Physical Review Letters 103, 150502 (2009)](https://journals.aps.org/prl/abstract/10.1103/PhysRevLett.103.150502) \[3]: [Lee Y., Joo J. & Lee S., Hybrid quantum linear equation algorithm and its experimental test on IBM Quantum Experience](https://www.nature.com/articles/s41598-019-41324-9) \[4]: [Swap Test (Wikipedia)](https://en.wikipedia.org/wiki/Swap_test) \[5]: [Yalovetzky R., et. al., Hybrid HHL with Dynamic Quantum Circuits on Real Hardware](https://arxiv.org/abs/2110.15958) # Estimating European Option Price Using Amplitude Estimation Source: https://docs.classiq.io/explore/applications/finance/option_pricing/option_pricing Open this notebook in GitHub to run it yourself In finance models it is often interesting to calculate the average of a function of a given probability distribution ($E[f(x)]$). The most popular method to estimate the average is Monte Carlo \[[1](#mcmf)] due to its flexibility and ability to generically handle stochastic parameters. Classical Monte Carlo methods, however, generally require extensive computational resources to provide an accurate estimation. By leveraging the laws of quantum mechanics, a quantum computer may provide novel ways to solve computationally intensive financial problems, such as risk management, portfolio optimization, and option pricing. The core quantum advantage of several of these applications is the Amplitude Estimation algorithm \[[2](#aea)], which can estimate a parameter with a convergence rate of $\Omega(1/M^{1/2})$, compared to $\Omega(1/M)$ in the classical case, where $M$ is the number of Grover iterations in the quantum case and the number of the Monte Carlo samples in the classical case. This represents a theoretical quadratic speed-up of the quantum method over classical Monte Carlo methods. ## Option Pricing An option is the possibility to buy (call) or sell (put) an item (or share) at a known price (the strike price, K), where the option has a maturity price (S). For example, this is the payoff function to describe a European call option: \$f(S)=\ \Bigg\{\begin\{array}\{lr} 0, & \text\{when } K\geq S\\ S * K, & \text\{when } K \< S\end\{array} \$ The maturity price is unknown. Therefore, it is expressed by a price distribution function, which may be any type of a distribution function. For example, a log-normal distribution: $\mathcal{ln}(S)\sim~\mathcal{N}(\mu,\sigma)$, where $\mathcal{N}(\mu,\sigma)$ is the standard normal distribution with mean equal to $\mu$ and standard deviation equal to $\sigma$. To estimate the average option price using a quantum computer: * Load the distribution, i.e., discretize the distribution using $2^n$ points (where n is the number of qubits) and truncate it. * Implement the payoff function that is equal to zero if $S\leq{K}$ and increases linearly otherwise. The linear part is approximated to load it properly using $R_y$ rotations \[[3](#qar)]. * Evaluate the expected payoff using amplitude estimation. The algorithmic framework is called Quantum Monte Carlo Integration. For a basic example, see [QMCI](https://github.com/Classiq/classiq-library/blob/main/algorithms/amplitude_amplification_and_estimation/qmc_user_defined/qmc_user_defined.ipynb]). This demonstration uses the same framework to estimate the European call option, where the underlying asset distribution at the maturity data is modeled as log-normal distribution. ## Designing the Quantum Algorithm image.png ## Probability Distribution The distribution describes the option underlying asset price at maturity date. Load a discrete version of the log normal probability with $2^n$ points, when $\mu$ is equal to `mu`, $\sigma$ is equal to `sigma`, and $n$ is equal to `num_qubits`. In addition, choose `K`, the strike price: ```python theme={null} num_qubits = 5 mu = 0.7 sigma = 0.13 K = 1.9 ``` ```python theme={null} import matplotlib.pyplot as plt import numpy as np import scipy def get_log_normal_probabilities(mu, sigma, num_points): mean = np.exp(mu + sigma**2 / 2) variance = (np.exp(sigma**2) - 1) * np.exp(2 * mu + sigma**2) stddev = np.sqrt(variance) # cut the distribution 3 sigmas from the mean low = np.maximum(0, mean - 3 * stddev) high = mean + 3 * stddev print(mean, variance, stddev, low, high) x = np.linspace(low, high, num_points) return x, scipy.stats.lognorm.pdf(x, s=sigma, scale=np.exp(mu)) ``` ```python theme={null} grid_points, probs = get_log_normal_probabilities(mu, sigma, 2**num_qubits) # normalize the probabilities probs = probs / np.sum(probs) fig, ax1 = plt.subplots() # Plot the probabilities ax1.plot(grid_points, probs, "go-", label="Probability") # Green line with circles ax1.tick_params(axis="y", labelcolor="g") ax1.set_xlabel("Asset Value at Maturity Date") ax1.set_ylabel("Probability", color="g") # Create a second y-axis for the payoff ax2 = ax1.twinx() ax2.plot(grid_points, np.maximum(grid_points - K, 0), "r-", label="Payoff") # Red line ax2.set_ylabel("Payoff", color="r") ax2.tick_params(axis="y", labelcolor="r") # Add grid and title ax1.grid(True) plt.title("Probability and Payoff vs. Asset Value") ``` **Output:** ``` 2.030841014265948 0.07029323208790372 0.26512870853210846 1.2354548886696226 2.8262271398622736 ``` **Output:** ``` Text(0.5, 1.0, 'Probability and Payoff vs. Asset Value') ``` output # ## Quantum Function for the Distribution Loading The general `inplace_prepare_state` function is needed as it is repeatedly applied on the same quantum variable. For simplicity, use the general purpose state preparation. There are more efficient and scalable methods for preparing the required distribution; for example, see \[[4](#gs)]. ```python theme={null} from classiq import * @qfunc def load_distribution(asset: QNum): inplace_prepare_state(probabilities=probs.tolist(), bound=0, target=asset) ``` # ## Payoff Function Create the payoff function by loading $U_{payoff}|S\rangle|0\rangle = \sqrt{f_{payoff}(S)}|S\rangle|1\rangle + \sqrt{1-f_{payoff}(S)}|S\rangle|0\rangle$. When building the European call option function, do nothing if the strike price is smaller than $K$; otherwise, apply a linear amplitude loading. **NOTE**: To save qubits and depth, the register $|S\rangle_n$ holds a value in the range $[0, 2^{n-1}]$, "labeling" the asset value. The mapping to the asset value space occurs in the comparator and amplitude loading, using this (classical) `scale` function: ```python theme={null} from classiq.qmod.symbolic import ceiling grid_step = (max(grid_points) - min(grid_points)) / (len(grid_points) - 1) # translate from qubit space to price space def scale(val): return val * grid_step + min(grid_points) # translate from price space to qubit space def descale(val: int): return (val - min(grid_points)) / grid_step @qfunc def payoff(asset: Const[QNum], ind: QBit): # check if asset price is 'in the money' - crossed the strike price control(asset >= ceiling(descale(K)), lambda: payoff_linear(asset, ind)) ``` For simplicity, use a general purpose amplitude loading method with the `assign_amplitude_table` function. The calculation is exact, however it is not scalable for large variable sizes. More scalable methods are mentioned in \[[4](#gs)] and \[[6](#rainbow)]. **Important**: So that all loaded amplitudes are not greater than 1, normalize the payoff by a `scaling_factor`, later multiplied in the post-process stage: ```python theme={null} scaling_factor = max(grid_points) - K @qfunc def payoff_linear(asset: Const[QNum], ind: QBit): assign_amplitude_table( lookup_table(lambda n: np.sqrt(abs((scale(n) - K) / scaling_factor)), asset), asset, ind, ) @qfunc def european_call_state_preparation(asset: QNum, ind: QBit): load_distribution(asset) payoff(asset, ind) ``` # ## Wrapping to an Amplitude Estimation Model After defining the probability distribution and the payoff function, pack them together as a state preparation in the [Iterative Quantum Amplitude Estimation algorithm](https://github.com/Classiq/classiq-library/blob/main/algorithms/amplitude_amplification_and_estimation/quantum_counting/quantum_counting.ipynb) \[[5](#iqae)]. The oracle is very simple and only needs to recognize whether the indicator qubit is in the $|1\rangle$ state. The returned amplitude from the algorithm is according to the formula: $$ |\Psi\rangle = \sum_x|x\rangle[\sqrt{p(x)f(x)}|1\rangle_{ind} + \sqrt{p(x)(1-f(x))}|0\rangle_{ind}] = a|\Psi_1\rangle + \sqrt{1-a^2}|\Psi_0\rangle $$ which approximates the expectation value of the payoff: $$ a^2 = \sum_xp(x)f(x) \approx E[f]_p $$ The built-in `IQAE` application expects a state preparation operand, and builds a model for the iterative amplitude estimation algorithm: ```python theme={null} from classiq.applications.iqae.iqae import IQAE iqae = IQAE( state_prep_op=european_call_state_preparation, problem_vars_size=num_qubits, constraints=Constraints(max_width=20), preferences=Preferences(optimization_level=1), ) ``` ## Quantum Program Synthesis Synthesize the model to a quantum program: ```python theme={null} qmod = iqae.get_model() ``` ```python theme={null} qprog = iqae.get_qprog() show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/344PyaTPK47pmHGQgBazHsKUgEN ``` ## Quantum Program Execution Define the parameters for the accuracy of the amplitude estimation. This affects the expected number of Grover of repetitions within the execution: ```python theme={null} result_iqae = iqae.run( epsilon=0.05, alpha=0.01 # desired error # desired probability for error ) ``` Post-processing: to get the expected payoff, descale the measured amplitude by a `scaling_factor`: ```python theme={null} measured_payoff = result_iqae.estimation * scaling_factor confidence_interval = np.array(result_iqae.confidence_interval) * scaling_factor print("Measured Payoff:", measured_payoff) print("Confidence Interval:", confidence_interval) ``` **Output:** ``` Measured Payoff: 0.17682654151421245 Confidence Interval: [0.17349158 0.1801615 ] ``` Compare to the expected payoff calculation: ```python theme={null} expected_payoff = sum((grid_points - K) * (grid_points >= K) * probs) print("Expected Payoff:", expected_payoff) ``` **Output:** ``` Expected Payoff: 0.17680663493930157 ``` ```python theme={null} assert np.isclose( measured_payoff, expected_payoff, atol=5 * (confidence_interval[1] - confidence_interval[0]), ) ``` ## References \[1] [Paul Glasserman. (2003). Monte Carlo Methods in Financial Engineering. Springer-Verlag New York, p. 596.](https://link.springer.com/book/10.1007/978-0-387-21617-1) \[2] [Gilles Brassard, Peter Hoyer, Michele Mosca, and Alain Tapp. (2002). Quantum amplitude amplification and estimation. Contemporary Mathematics 305.](https://arxiv.org/abs/quant-ph/0005055) \[3] [ Nikitas Stamatopoulos, Daniel J. Egger, Yue Sun, Christa Zoufal, Raban Iten, Ning Shen, and Stefan Woerner. (2020). Option pricing using quantum computers, Quantum 4, 291.](https://arxiv.org/abs/1905.02666v5) \[4] [ Chakrabarti, Shouvanik, et al. (2021). A threshold for quantum advantage in derivative pricing. Quantum 5: 463.](https://quantum-journal.org/papers/q-2021-06-01-463/) \[5] [Grinko, D., Gacon, J., Zoufal, C. et al. (2021). Iterative quantum amplitude estimation. npj Quantum Inf 7, 52.](https://doi.org/10.1038/s41534-021-00379-1) \[6] [Francesca Cibrario et al., Quantum amplitude loading for rainbow options pricing. (2024). Preprint](https://arxiv.org/abs/2402.05574v2) # Portfolio Optimization with the Quantum Approximate Optimization Algorithm (QAOA) Source: https://docs.classiq.io/explore/applications/finance/portfolio_optimization/portfolio_optimization Open this notebook in GitHub to run it yourself Portfolio optimization \[[1](#portfoliowiki)] is the process of optimally allocating a portfolio of financial assets, according to some predetermined goal. Usually, the goal is to maximize the potential return while minimizing the financial risk of the portfolio. One can express this problem as a combinatorial optimization problem like many other real-world problems. This demo shows how to employ the Quantum Approximate Optimization Algorithm (QAOA) \[[2](#qaoa)] on the Classiq platform to solve the problem of portfolio optimization. ## Modeling the Portfolio Optimization Problem First, model the problem mathematically using a simple yet powerful model that captures the essence of portfolio optimization: * A portfolio is built from a pool of $n$ financial assets, with each asset labeled $i \in \{1,\ldots,n\}$. * Every asset's return is a random variable, with expected value $\mu_i$ and variance $\Sigma_i$ (modeling the financial risk involved in the asset). * Every two assets $i \neq j$ have covariance $\Sigma_{ij}$ (modeling market correlation between assets). * Every asset $i$ has a weight $w_i \in D_i = \{0,\ldots,b_i\}$ in the portfolio, with $b_i$ defined as the budget for asset $i$ (modeling the maximum allowed weight of the asset). * The return vector $\mu$, the covariance matrix $\Sigma$, and the weight vector $w$ are defined naturally from the above (with the domain $D = D_1 \times D_2 \times \ldots \times D_n$ for $w$). With the above definitions, the total expected return of the portfolio is $\mu^T w$ and the total risk is $w^T \Sigma w$. Use a simple difference of the two as the cost function, with the additional constraint that the total sum of assets does not exceed a predefined budget $B$. Note that there are many other possibilities for defining a cost function (such as adding a scaling factor to the risk/return or even a non-linear relation). For simplicity, select the model below and assume all constants and variables are dimensionless. Thus, given the constant inputs $\mu, \Sigma, D, B$, the problem is to find the optimal variable $w$: $$ \min_{w \in D} w^T \Sigma w - \mu^T w, $$ subject to $\Sigma_{i} w_i \leq B$. This case is called integer portfolio optimization, since the domains $D_i$ are over the (positive) integers. Another variation of this problem defines weights over binary domains, and is not discussed here. # ## Setup With the mathematical definition in place, begin the implementation by importing necessary packages and classes. Use these external dependencies: * NumPy * Matplotlib * Pyomo - a Python framework for modeling optimization problems, which the Classiq platform uses as an interface to these types of problems From the `classiq` package, import classes related to combinatorial optimization and QAOA: ```python theme={null} import numpy as np import pyomo.core as pyo ``` # ## Portfolio Optimization Problem Parameters Define the parameters of the optimization problem, which include the expected return vector, the covariance matrix, and the total budget: ```python theme={null} returns = np.array([3, 4, -1]) covariances = np.array( [ [0.9, 0.5, -0.7], [0.5, 0.9, -0.2], [-0.7, -0.2, 0.9], ] ) total_budget = 6 ``` ## Pyomo Model for the Problem Define the Pyomo model to use on the Classiq platform, using the problem parameters defined above: ```python theme={null} portfolio_model = pyo.ConcreteModel("portfolio_optimization") num_assets = len(returns) # setting the variables portfolio_model.w = pyo.Var(range(num_assets), domain=pyo.Integers, bounds=(0, 7)) w_array = list(portfolio_model.w.values()) # global budget constraint portfolio_model.budget_rule = pyo.Constraint(expr=(sum(w_array) <= total_budget)) # setting the expected return and risk portfolio_model.expected_return = returns @ w_array portfolio_model.risk = w_array @ covariances @ w_array # setting the cost function to minimize portfolio_model.cost = pyo.Objective( expr=portfolio_model.risk - portfolio_model.expected_return, sense=pyo.minimize ) ``` # ## Setting Up the Classiq Problem Instance To solve the Pyomo model defined above, use the `CombinatorialProblem` Python class. Under the hood, it translates the Pyomo model to a quantum model of the QAOA algorithm, with the cost Hamiltonian translated from the Pyomo model. Choose the number of layers for the QAOA ansatz using the `num_layers` argument and the `penalty_factor`, which is the coefficient of the constraints term in the cost Hamiltonian: ```python theme={null} from classiq import * from classiq.applications.combinatorial_optimization import CombinatorialProblem combi = CombinatorialProblem(pyo_model=portfolio_model, num_layers=3, penalty_factor=10) qmod = combi.get_model() ``` # ## Synthesizing the QAOA Circuit and Solving the Problem Synthesize and view the QAOA circuit (ansatz) used to solve the optimization problem: ```python theme={null} qprog = combi.get_qprog() show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/38w7hk6ZDgLVJlb9TTYc1kVngv8 ``` **Output:** ``` https://platform.classiq.io/circuit/38w7hk6ZDgLVJlb9TTYc1kVngv8?login=True&version=15 ``` Set the quantum backend for execution: ```python theme={null} from classiq.execution import * execution_preferences = ExecutionPreferences( backend_preferences=ClassiqBackendPreferences(backend_name="simulator"), ) ``` Solve the problem by calling the `optimize` method of the `CombinatorialProblem` object. For the classical optimization part of the QAOA algorithm, define the maximum number of classical iterations (`maxiter`) and the $\alpha$-parameter (`quantile`) for running CVaR-QAOA, an improved variation of the QAOA algorithm \[[3](#cvar)]: ```python theme={null} optimized_params = combi.optimize(execution_preferences, maxiter=60, quantile=0.7) ``` Check the convergence of the run: ```python theme={null} import matplotlib.pyplot as plt plt.plot(combi.cost_trace) plt.xlabel("Iterations") plt.ylabel("Cost") plt.title("Cost convergence") ``` **Output:** ``` Text(0.5, 1.0, 'Cost convergence') ``` output ## Optimization Results Examine the statistics of the algorithm. The optimization is always defined as a minimization problem, so the positive maximization objective is translated to negative minimization by the Pyomo-to-Qmod translator. To get samples with the optimized parameters, call the `sample` method: ```python theme={null} optimization_result = combi.sample(optimized_params) optimization_result.sort_values(by="cost").head(5) ``` | | solution | probability | cost | | --- | ------------------------------------------------------- | ----------- | ---- | | 202 | \{'w': \[1, 2, 1], 'budget\_rule\_slack\_var': \[0, ... | 0.000977 | -4.8 | | 410 | \{'w': \[2, 1, 1], 'budget\_rule\_slack\_var': \[0, ... | 0.000977 | -4.8 | | 114 | \{'w': \[3, 1, 2], 'budget\_rule\_slack\_var': \[0, ... | 0.001465 | -4.6 | | 452 | \{'w': \[2, 2, 1], 'budget\_rule\_slack\_var': \[1, ... | 0.000977 | -4.5 | | 514 | \{'w': \[1, 2, 0], 'budget\_rule\_slack\_var': \[0, ... | 0.000488 | -4.5 | Compare the optimized results to uniformly sampled results: ```python theme={null} uniform_result = combi.sample_uniform() ``` And compare the histograms: ```python theme={null} optimization_result["cost"].plot( kind="hist", bins=40, edgecolor="black", weights=optimization_result["probability"], alpha=0.6, label="optimized", ) uniform_result["cost"].plot( kind="hist", bins=40, edgecolor="black", weights=uniform_result["probability"], alpha=0.6, label="uniform", ) plt.legend() plt.ylabel("Probability", fontsize=16) plt.xlabel("cost", fontsize=16) plt.tick_params(axis="both", labelsize=14) ``` output ```python theme={null} best_solution = optimization_result.loc[optimization_result.cost.idxmin()] print( "x =", best_solution.solution["w"], ", cost =", best_solution.cost, ) ``` **Output:** ``` x = [1, 2, 1] , cost = -4.800000000000001 ``` Lastly, compare with the classical solution of the problem: ```python theme={null} from pyomo.opt import SolverFactory solver = SolverFactory("couenne") solver.solve(portfolio_model) portfolio_model.display() classical_solution = [ round(pyo.value(portfolio_model.w[i])) for i in range(len(portfolio_model.w)) ] print("Classical solution:", classical_solution) ``` **Output:** ``` Model portfolio_optimization Variables: w : Size=3, Index={0, 1, 2} Key : Lower : Value : Upper : Fixed : Stale : Domain 0 : 0 : 2.0 : 7 : False : False : Integers 1 : 0 : 1.0 : 7 : False : False : Integers 2 : 0 : 1.0 : 7 : False : False : Integers Objectives: cost : Size=1, Index=None, Active=True Key : Active : Value None : True : -4.800000000000001 Constraints: budget_rule : Size=1 Key : Lower : Body : Upper None : None : 4.0 : 6.0 Classical solution: [2, 1, 1] ``` Most of the solutions obtained by running QAOA are close to the minimal solution obtained classically, demonstrating the effectiveness of the algorithm. Also, note the non-trivial solution, which includes a non-zero weight for the asset with negative expected return, demonstrating that it sometimes makes sense to include such assets in the portfolio as a risk-mitigation strategy - especially if they are highly anti-correlated with the rest of the assets. ## References \[1] [Portfolio Optimization (Wikipedia)](https://en.wikipedia.org/wiki/Portfolio_optimization) \[2] [Farhi, Edward, Jeffrey Goldstone, and Sam Gutmann. "A quantum approximate optimization algorithm." arXiv preprint arXiv:1411.4028 (2014).](https://arxiv.org/abs/1411.4028) \[3] [Barkoutsos, Panagiotis Kl, et al. "Improving variational quantum optimization using CVaR." Quantum 4 (2020): 256.](https://arxiv.org/abs/1907.04769) # Quantum Computational Finance: Quantum Algorithm for Portfolio Optimization Source: https://docs.classiq.io/explore/applications/finance/portfolio_optimization_hhl/HHL_portfolio Open this notebook in GitHub to run it yourself In this tutorial, we will work through a concrete example of portfolio optimization. Our goal is to determine how to allocate capital across several assets so that we control risk while still aiming for good returns. To do this, we use historical stock price data to construct a portfolio, that is, the allocation ratios of the assets. The approach is motivated by the methods proposed in References \[1,2], where portfolio optimization is identified as a problem that can potentially be accelerated using the HHL algorithm. As a simple practical example, we consider an investment in four major technology stocks and examine how to distribute capital among them in an effective way. first we need to install some out package to collect stock data. ```python theme={null} # !pip install yfinance # !pip install pandas_datareader ``` ```python theme={null} # import datetime # import yfinance as yf # codes = ['NVDA', 'AAPL', 'TSLA', 'AMZN'] # start = datetime.datetime(2024, 1, 1) # end = datetime.datetime(2024, 12, 31) # data = yf.download(codes, start=start, end=end, auto_adjust=False, progress=False) # df = data['Adj Close'] # df.to_csv("asset_data.csv") # display(df.head(10)) ``` ```python theme={null} import datetime import matplotlib.pyplot as plt import numpy as np import pandas as pd from classiq import * ``` ## Loading Dataset of Stock We define csv\_data which was generated from `yfinance`. We use the data from `2024-01-02` to `2024-12-30`. ```python theme={null} import numpy as np import pandas as pd stock_data = { "Date": [ "2024-01-02", "2024-01-03", "2024-01-04", "2024-01-05", "2024-01-08", "2024-01-09", "2024-01-10", "2024-01-11", "2024-01-12", "2024-01-16", "2024-01-17", "2024-01-18", "2024-01-19", "2024-01-22", "2024-01-23", "2024-01-24", "2024-01-25", "2024-01-26", "2024-01-29", "2024-01-30", "2024-01-31", "2024-02-01", "2024-02-02", "2024-02-05", "2024-02-06", "2024-02-07", "2024-02-08", "2024-02-09", "2024-02-12", "2024-02-13", "2024-02-14", "2024-02-15", "2024-02-16", "2024-02-20", "2024-02-21", "2024-02-22", "2024-02-23", "2024-02-26", "2024-02-27", "2024-02-28", "2024-02-29", "2024-03-01", "2024-03-04", "2024-03-05", "2024-03-06", "2024-03-07", "2024-03-08", "2024-03-11", "2024-03-12", "2024-03-13", "2024-03-14", "2024-03-15", "2024-03-18", "2024-03-19", "2024-03-20", "2024-03-21", "2024-03-22", "2024-03-25", "2024-03-26", "2024-03-27", "2024-03-28", "2024-04-01", "2024-04-02", "2024-04-03", "2024-04-04", "2024-04-05", "2024-04-08", "2024-04-09", "2024-04-10", "2024-04-11", "2024-04-12", "2024-04-15", "2024-04-16", "2024-04-17", "2024-04-18", "2024-04-19", "2024-04-22", "2024-04-23", "2024-04-24", "2024-04-25", "2024-04-26", "2024-04-29", "2024-04-30", "2024-05-01", "2024-05-02", "2024-05-03", "2024-05-06", "2024-05-07", "2024-05-08", "2024-05-09", "2024-05-10", "2024-05-13", "2024-05-14", "2024-05-15", "2024-05-16", "2024-05-17", "2024-05-20", "2024-05-21", "2024-05-22", "2024-05-23", "2024-05-24", "2024-05-28", "2024-05-29", "2024-05-30", "2024-05-31", "2024-06-03", "2024-06-04", "2024-06-05", "2024-06-06", "2024-06-07", "2024-06-10", "2024-06-11", "2024-06-12", "2024-06-13", "2024-06-14", "2024-06-17", "2024-06-18", "2024-06-20", "2024-06-21", "2024-06-24", "2024-06-25", "2024-06-26", "2024-06-27", "2024-06-28", "2024-07-01", "2024-07-02", "2024-07-03", "2024-07-05", "2024-07-08", "2024-07-09", "2024-07-10", "2024-07-11", "2024-07-12", "2024-07-15", "2024-07-16", "2024-07-17", "2024-07-18", "2024-07-19", "2024-07-22", "2024-07-23", "2024-07-24", "2024-07-25", "2024-07-26", "2024-07-29", "2024-07-30", "2024-07-31", "2024-08-01", "2024-08-02", "2024-08-05", "2024-08-06", "2024-08-07", "2024-08-08", "2024-08-09", "2024-08-12", "2024-08-13", "2024-08-14", "2024-08-15", "2024-08-16", "2024-08-19", "2024-08-20", "2024-08-21", "2024-08-22", "2024-08-23", "2024-08-26", "2024-08-27", "2024-08-28", "2024-08-29", "2024-08-30", "2024-09-03", "2024-09-04", "2024-09-05", "2024-09-06", "2024-09-09", "2024-09-10", "2024-09-11", "2024-09-12", "2024-09-13", "2024-09-16", "2024-09-17", "2024-09-18", "2024-09-19", "2024-09-20", "2024-09-23", "2024-09-24", "2024-09-25", "2024-09-26", "2024-09-27", "2024-09-30", "2024-10-01", "2024-10-02", "2024-10-03", "2024-10-04", "2024-10-07", "2024-10-08", "2024-10-09", "2024-10-10", "2024-10-11", "2024-10-14", "2024-10-15", "2024-10-16", "2024-10-17", "2024-10-18", "2024-10-21", "2024-10-22", "2024-10-23", "2024-10-24", "2024-10-25", "2024-10-28", "2024-10-29", "2024-10-30", "2024-10-31", "2024-11-01", "2024-11-04", "2024-11-05", "2024-11-06", "2024-11-07", "2024-11-08", "2024-11-11", "2024-11-12", "2024-11-13", "2024-11-14", "2024-11-15", "2024-11-18", "2024-11-19", "2024-11-20", "2024-11-21", "2024-11-22", "2024-11-25", "2024-11-26", "2024-11-27", "2024-11-29", "2024-12-02", "2024-12-03", "2024-12-04", "2024-12-05", "2024-12-06", "2024-12-09", "2024-12-10", "2024-12-11", "2024-12-12", "2024-12-13", "2024-12-16", "2024-12-17", "2024-12-18", "2024-12-19", "2024-12-20", "2024-12-23", "2024-12-24", "2024-12-26", "2024-12-27", "2024-12-30", ], "AAPL": [ 183.9, 182.52, 180.2, 179.48, 183.82, 183.4, 184.44, 183.85, 184.18, 181.91, 180.97, 186.86, 189.76, 192.07, 193.35, 192.68, 192.35, 190.61, 189.93, 186.28, 182.67, 185.11, 184.11, 185.92, 187.52, 187.63, 186.55, 187.32, 185.63, 183.54, 182.65, 182.37, 180.83, 180.09, 180.84, 182.87, 181.04, 179.69, 181.15, 179.95, 179.28, 178.2, 173.68, 168.74, 167.75, 167.63, 169.34, 171.35, 171.82, 169.74, 171.6, 171.22, 172.31, 174.65, 177.22, 169.98, 170.88, 169.46, 168.33, 171.9, 170.09, 168.65, 167.47, 168.27, 167.45, 168.2, 167.08, 168.29, 166.42, 173.62, 175.12, 171.29, 168.0, 166.64, 165.68, 163.66, 164.49, 165.54, 167.65, 168.51, 167.93, 172.09, 168.95, 167.93, 171.62, 181.89, 180.23, 180.92, 181.26, 183.07, 181.81, 185.02, 186.16, 188.44, 188.55, 188.58, 189.75, 191.05, 189.61, 185.61, 188.69, 188.7, 189.0, 189.99, 190.95, 192.72, 193.03, 194.54, 193.16, 195.56, 191.81, 205.75, 211.63, 212.79, 211.05, 215.2, 212.84, 208.26, 206.09, 206.73, 207.65, 211.81, 212.65, 209.19, 215.28, 218.78, 220.05, 224.81, 226.28, 227.13, 231.4, 226.03, 228.98, 232.81, 233.23, 227.33, 222.66, 222.79, 222.44, 223.49, 217.06, 216.02, 216.48, 216.76, 217.32, 220.58, 216.88, 218.37, 207.85, 205.83, 208.4, 211.87, 214.78, 216.31, 220.03, 220.47, 223.46, 224.78, 224.62, 225.24, 225.13, 223.27, 225.57, 225.9, 226.75, 225.22, 228.5, 227.71, 221.52, 219.61, 221.13, 219.58, 219.67, 218.87, 221.41, 221.52, 221.25, 215.1, 215.57, 219.45, 227.58, 226.92, 225.2, 226.09, 225.1, 226.24, 226.51, 231.69, 224.94, 225.51, 224.4, 225.53, 220.44, 224.5, 228.25, 227.75, 226.27, 230.0, 232.54, 230.48, 230.85, 233.68, 235.15, 234.54, 229.46, 229.27, 230.11, 232.09, 232.36, 228.81, 224.64, 221.66, 220.76, 222.19, 221.47, 226.2, 225.93, 223.22, 223.22, 224.1, 227.19, 223.98, 226.99, 227.25, 227.96, 227.49, 228.83, 231.82, 234.0, 233.87, 236.26, 238.51, 241.55, 241.91, 241.94, 241.74, 245.63, 246.65, 245.38, 246.84, 247.01, 249.9, 252.33, 246.93, 248.66, 253.34, 254.12, 257.03, 257.85, 254.43, 251.06, ], "AMZN": [ 149.92, 148.47, 144.57, 145.24, 149.1, 151.36, 153.72, 155.17, 154.61, 153.16, 151.71, 153.5, 155.33, 154.77, 156.02, 156.86, 157.75, 159.11, 161.25, 159.0, 155.19, 159.27, 171.8, 170.3, 169.14, 170.52, 169.83, 174.44, 172.33, 168.63, 170.97, 169.8, 169.5, 167.08, 168.58, 174.58, 174.99, 174.72, 173.53, 173.16, 176.75, 178.22, 177.58, 174.11, 173.5, 176.82, 175.35, 171.96, 175.38, 176.55, 178.75, 174.41, 174.47, 175.89, 178.14, 178.14, 178.86, 179.71, 178.3, 179.83, 180.38, 180.97, 180.69, 182.41, 180.0, 185.07, 185.19, 185.66, 185.94, 189.05, 186.13, 183.61, 183.32, 181.27, 179.22, 174.63, 177.22, 179.53, 176.58, 173.66, 179.61, 180.96, 175.0, 179.0, 184.72, 186.21, 188.69, 188.75, 188.0, 189.5, 187.47, 186.57, 187.07, 185.99, 183.63, 184.69, 183.53, 183.14, 183.13, 181.05, 180.75, 182.14, 182.02, 179.32, 176.44, 178.33, 179.33, 181.27, 185.0, 184.3, 187.05, 187.22, 186.88, 183.83, 183.66, 184.05, 182.8, 186.1, 189.08, 185.57, 186.33, 193.61, 197.85, 193.25, 197.19, 200.0, 197.58, 200.0, 199.28, 199.33, 199.78, 195.05, 194.49, 192.72, 193.02, 187.92, 183.75, 183.13, 182.55, 186.41, 180.83, 179.85, 182.5, 183.19, 181.71, 186.97, 184.07, 167.89, 161.02, 161.92, 162.77, 165.8, 166.94, 166.8, 170.22, 170.1, 177.58, 177.05, 178.22, 178.88, 180.11, 176.13, 177.03, 175.5, 173.11, 170.8, 172.11, 178.5, 176.25, 173.33, 177.88, 171.38, 175.39, 179.55, 184.52, 187.0, 186.49, 184.88, 186.88, 186.42, 189.86, 191.6, 193.88, 193.96, 192.52, 191.16, 187.97, 186.33, 185.13, 184.75, 181.96, 186.5, 180.8, 182.72, 185.16, 186.64, 188.82, 187.53, 187.69, 186.88, 187.52, 188.99, 189.07, 189.69, 184.71, 186.38, 187.83, 188.38, 190.83, 192.72, 186.39, 197.92, 195.77, 199.5, 207.08, 210.05, 208.17, 206.83, 208.91, 214.1, 211.47, 202.61, 201.69, 204.61, 202.88, 198.38, 197.11, 201.44, 207.86, 205.74, 207.88, 210.71, 213.44, 218.16, 220.55, 227.02, 226.08, 225.03, 230.25, 228.97, 227.46, 232.92, 231.14, 220.52, 223.28, 224.91, 225.05, 229.05, 227.05, 223.75, 221.3, ], "NVDA": [ 48.14, 47.54, 47.97, 49.06, 52.22, 53.11, 54.31, 54.79, 54.67, 56.35, 56.02, 57.07, 59.45, 59.62, 59.83, 61.32, 61.58, 60.99, 62.43, 62.73, 61.49, 62.99, 66.12, 69.29, 68.18, 70.05, 69.6, 72.09, 72.2, 72.08, 73.85, 72.61, 72.57, 69.41, 67.43, 78.49, 78.77, 79.04, 78.65, 77.61, 79.06, 82.23, 85.18, 85.92, 88.65, 92.62, 87.48, 85.73, 91.86, 90.84, 87.89, 87.79, 88.4, 89.35, 90.32, 91.38, 94.24, 94.95, 92.51, 90.2, 90.3, 90.31, 89.4, 88.91, 85.86, 87.96, 87.08, 85.31, 86.99, 90.56, 88.14, 85.95, 87.37, 83.99, 84.62, 76.16, 79.47, 82.38, 79.63, 82.58, 87.69, 87.71, 86.35, 82.99, 85.77, 88.74, 92.09, 90.5, 90.36, 88.7, 89.83, 90.35, 91.3, 94.58, 94.31, 92.43, 94.73, 95.33, 94.9, 103.74, 106.41, 113.84, 114.76, 110.44, 109.57, 114.94, 116.37, 122.37, 120.93, 120.82, 121.72, 120.85, 125.14, 129.55, 131.82, 130.92, 135.52, 130.72, 126.51, 118.05, 126.03, 126.34, 123.93, 123.48, 124.24, 122.61, 128.22, 125.77, 128.14, 131.32, 134.85, 127.34, 129.18, 128.38, 126.3, 117.93, 121.03, 117.87, 123.48, 122.53, 114.2, 112.23, 113.01, 111.54, 103.68, 116.96, 109.16, 107.22, 100.4, 104.2, 98.86, 104.92, 104.7, 108.97, 116.09, 118.02, 122.8, 124.52, 129.94, 127.19, 128.44, 123.68, 129.31, 126.4, 128.24, 125.55, 117.53, 119.31, 107.95, 106.16, 107.16, 102.78, 106.42, 108.05, 116.85, 119.09, 119.05, 116.74, 115.55, 113.33, 117.82, 115.96, 116.22, 120.82, 123.46, 123.99, 121.35, 121.39, 116.95, 118.8, 122.8, 124.87, 127.67, 132.84, 132.6, 134.76, 134.75, 138.02, 131.55, 135.67, 136.88, 137.95, 143.66, 143.54, 139.51, 140.36, 141.49, 140.47, 141.2, 139.29, 132.71, 135.35, 136.0, 139.86, 145.56, 148.82, 147.57, 145.21, 148.23, 146.21, 146.7, 141.93, 140.1, 146.95, 145.84, 146.61, 141.9, 135.97, 136.87, 135.29, 138.2, 138.58, 140.21, 145.09, 145.02, 142.4, 138.77, 135.03, 139.27, 137.3, 134.21, 131.96, 130.35, 128.87, 130.64, 134.66, 139.63, 140.18, 139.89, 136.97, 137.45, ], "TSLA": [ 248.41, 238.44, 237.92, 237.49, 240.44, 234.96, 233.94, 227.22, 218.88, 219.91, 215.55, 211.88, 212.19, 208.8, 209.13, 207.83, 182.63, 183.25, 190.92, 191.58, 187.28, 188.86, 187.91, 181.05, 185.1, 187.58, 189.55, 193.57, 188.13, 184.02, 188.71, 200.44, 199.94, 193.75, 194.77, 197.41, 191.97, 199.39, 199.72, 202.03, 201.88, 202.63, 188.13, 180.74, 176.53, 178.64, 175.33, 177.77, 177.53, 169.47, 162.5, 163.57, 173.8, 171.32, 175.66, 172.82, 170.83, 172.63, 177.66, 179.83, 175.78, 175.22, 166.63, 168.38, 171.11, 164.89, 172.97, 176.88, 171.75, 174.6, 171.05, 161.47, 157.11, 155.44, 149.92, 147.05, 142.05, 144.67, 162.13, 170.17, 168.28, 194.05, 183.27, 179.99, 180.0, 181.19, 184.75, 177.8, 174.72, 171.97, 168.47, 171.88, 177.55, 173.99, 174.83, 177.46, 174.94, 186.6, 180.11, 173.74, 179.24, 176.75, 176.19, 178.78, 178.08, 176.28, 174.77, 175.0, 177.94, 177.47, 173.78, 170.66, 177.28, 182.47, 178.0, 187.44, 184.86, 181.57, 183.0, 182.58, 187.35, 196.36, 197.41, 197.88, 209.86, 231.25, 246.38, 251.52, 252.94, 262.32, 263.26, 241.02, 248.22, 252.63, 256.55, 248.5, 249.22, 239.19, 251.5, 246.38, 215.99, 220.25, 219.8, 232.1, 222.61, 232.07, 216.86, 207.66, 198.88, 200.63, 191.75, 198.83, 200.0, 197.49, 207.83, 201.38, 214.13, 216.11, 222.72, 221.1, 223.27, 210.66, 220.32, 213.21, 209.21, 205.75, 206.27, 214.11, 210.6, 219.41, 230.16, 210.72, 216.27, 226.16, 228.13, 229.8, 230.28, 226.77, 227.86, 227.19, 243.91, 238.25, 250.0, 254.27, 257.01, 254.22, 260.45, 261.63, 258.01, 249.02, 240.66, 250.08, 240.83, 244.5, 241.05, 238.77, 217.8, 219.16, 219.57, 221.33, 220.88, 220.69, 218.85, 217.97, 213.64, 260.48, 269.19, 262.51, 259.51, 257.54, 249.85, 248.97, 242.83, 251.44, 288.52, 296.91, 321.22, 350.0, 328.48, 330.23, 311.17, 320.72, 338.73, 346.0, 342.02, 339.64, 352.55, 338.58, 338.23, 332.89, 345.16, 357.08, 351.42, 357.92, 369.48, 389.22, 389.79, 400.98, 424.76, 418.1, 436.23, 463.01, 479.85, 440.13, 436.17, 421.05, 430.6, 462.27, 454.13, 431.66, 417.41, ], } # Create DataFrame from the dictionary df = pd.DataFrame(stock_data) num_data = len(df) # Process Date as index (Ensuring 1:1 match with provided data) df["Date"] = pd.to_datetime(df["Date"]) df.set_index("Date", inplace=True) print(df.head(10)) ``` **Output:** ``` AAPL AMZN NVDA TSLA Date 2024-01-02 183.90 149.92 48.14 248.41 2024-01-03 182.52 148.47 47.54 238.44 2024-01-04 180.20 144.57 47.97 237.92 2024-01-05 179.48 145.24 49.06 237.49 2024-01-08 183.82 149.10 52.22 240.44 2024-01-09 183.40 151.36 53.11 234.96 2024-01-10 184.44 153.72 54.31 233.94 2024-01-11 183.85 155.17 54.79 227.22 2024-01-12 184.18 154.61 54.67 218.88 2024-01-16 181.91 153.16 56.35 219.91 ``` ## Plot the Stock Price of Selected Stock ```python theme={null} df.loc[:, ["NVDA", "AAPL", "TSLA", "AMZN"]].plot() ``` **Output:** ``` ``` output ## Conversion to Daily Returns The daily return (rate of change) $y_t$ of an individual stock is defined as follows, letting $t$ be the date: $$ y_t = \frac{P_t - P_{t-1}}{P_{t-1}} $$ It is obtained using `pct_change()` in `pandas`. ```python theme={null} daily_return = df.pct_change().dropna() display(daily_return.tail()) ``` | | AAPL | AMZN | NVDA | TSLA | | ---------- | --------- | --------- | --------- | --------- | | Date | | | | | | --- | --- | --- | --- | --- | | 2024-12-23 | 0.003079 | 0.000622 | 0.036908 | 0.022681 | | 2024-12-24 | 0.011451 | 0.017774 | 0.003939 | 0.073549 | | 2024-12-26 | 0.003190 | -0.008732 | -0.002069 | -0.017609 | | 2024-12-27 | -0.013264 | -0.014534 | -0.020874 | -0.049479 | | 2024-12-30 | -0.013245 | -0.010950 | 0.003504 | -0.033012 | ## Expected Return Calculate the expected return $\vec{R}$ for each stock. Here, the arithmetic mean of historical returns is used. $$ \vec{R} = \frac{1}{T}\sum_{t=1}^{T}\vec{y}_t $$ ```python theme={null} expected_return = daily_return.dropna(how="all").mean() * num_data print(expected_return) ``` **Output:** ``` AAPL 0.337606 AMZN 0.430596 NVDA 1.191089 TSLA 0.718326 dtype: float64 ``` ## Variance-Covariance Matrix The sample unbiased variance-covariance matrix of returns, $\Sigma$, is defined as follows: $$ \Sigma = \frac{1}{T-1}\sum_{t=1}^{T} (\vec{y}_t - \vec{R})(\vec{y}_t - \vec{R} )^{T} $$ * Let the return of each asset $i$ be the random variable $R_i$. * The covariance is: $$ \mathrm{Cov}(R_i, R_j) = \mathbb{E}\big[(R_i - \mathbb{E}[R_i])(R_j - \mathbb{E}[R_j])\big] $$ * The matrix aggregating this for all assets is $\Sigma$: $$ \Sigma = \begin{bmatrix} \sigma_{00} & \sigma_{01} & \cdots & \sigma_{0n} \\ \sigma_{10} & \sigma_{11} & \cdots & \sigma_{1n} \\ \vdots & \vdots & \ddots & \vdots \\ \sigma_{n0} & \sigma_{n1} & \cdots & \sigma_{nn} \end{bmatrix} $$ Where $\sigma_{ij} = \mathrm{Cov}(R_i, R_j)$. * With $\Sigma$, the "diversification effect" is incorporated into the calculation. * If assets have a negative correlation with each other, the risk of the entire portfolio decreases. * Conversely, if there is a strong positive correlation, the risk reduction effect will not be significant even if assets are diversified. ```python theme={null} cov = daily_return.dropna(how="all").cov() * num_data display(cov) ``` | | AAPL | AMZN | NVDA | TSLA | | ---- | -------- | -------- | -------- | -------- | | AAPL | 0.050204 | 0.021167 | 0.029819 | 0.046741 | | AMZN | 0.021167 | 0.078892 | 0.064226 | 0.056531 | | NVDA | 0.029819 | 0.064226 | 0.274981 | 0.071096 | | TSLA | 0.046741 | 0.056531 | 0.071096 | 0.403576 | ## Portfolio Optimization Having defined the necessary variables, we now proceed to actual portfolio optimization. We represent the portfolio as a $4$-component vector $\mathbf{w} = (w_0,w_1,w_2,w_3)^T$. This represents the proportion of holdings in each stock; for example, $\mathbf{w}=(1,0,0,0)$ implies a portfolio where $100\%$ of total assets are invested in AAPL stock. Consider a portfolio that satisfies the following equation: $$ \mathcal{O} = {\rm min_{\mathbf{w} }}\frac{1}{2}{\mathbf{w}^T}\Sigma\mathbf{w} = \sum_{i} w_i^2 \sigma_{ii} + \sum_{i\neq j} w_i w_j \sigma_{ij} $$ * The diagonal elements $\sigma_{ii}$ are the variances of each asset (intensity of risk). * The off-diagonal elements $\sigma_{ij}$ are the covariances between assets (strength of correlation). Here, the constraints are as follows: $$ {\rm const.1} = \mathbf{R^Tw}=\mu $$ $$ {\rm const.2} = \mathbf{1^Tw}=1 $$ Here, $\mathbf{1}$ represents $(1,1,1,1)^T$. The meanings of the two constraint equations are: * The expected return of the portfolio (average value of returns) is $\mu$. * The sum of the weights invested in the portfolio is 1 ($\mathbf{1}=(1,1,1,1)^T$). Under these conditions, we perform the "minimization of the variance of portfolio returns." This means that when desiring a return of $\mu$ in the future, the best portfolio is one that minimizes the fluctuation (risk) associated with it as much as possible. This type of problem setup is known as the Markowitz Mean-Variance Approach and is one of the foundational concepts of modern financial engineering. # ## Lagrange Multiplier Method in Mathematical Optimization The mathematical optimization problem described above is an optimization problem with equality constraints. By solving this using the **Method of Lagrange Multipliers**, we can find candidates for the local optimal solution. The Method of Lagrange Multipliers is defined as follows: $$ \mathcal{L}(x,u) = f(x) + \sum_{i=1}^m u_i g_i(x) \tag{0} $$ Here, $f(x)$ represents the **objective function**, and the second term represents the **constraint functions**. $u\in R_{d}$ are the Lagrange multipliers. In the case of our portfolio optimization problem, if we introduce Lagrange multipliers $\eta$ and $\theta$, the Lagrangian is given by: $$ \mathcal{L}(w,\eta, \theta) = \frac{1}{2}\mathbf{w}^T\Sigma\mathbf{w} + \eta(\mathbf{R}\mathbf{w}-\mu) + \theta(\mathbf{1}^T\mathbf{w}-1) \tag{1} $$ Since the constraints are linear equations, the optimal solution is guaranteed by just the first-order derivatives. Therefore, we differentiate the Lagrangian function above with respect to the variables $\mathbf{w}, \eta, \text{and } \theta$. # ## First-Order Optimality Conditions (KKT Conditions) $$ \frac{\partial \mathcal L}{\partial \vec w}= \Sigma\vec w+\eta\,\vec R+\theta\,\vec 1=\vec 0 \tag{A} $$ $$ \frac{\partial \mathcal L}{\partial \eta}= \vec R^{\,T}\vec w-\mu=0 \tag{B} $$ $$ \frac{\partial \mathcal L}{\partial \theta}= \vec 1^{\,T}\vec w-1=0 \tag{C} $$ # ## Organizing as a System of Linear Equations If we organize (A) through (C) into a linear equation regarding the **unknowns** $(\eta,\theta,\vec w)$, we get: $$ \begin{cases} \vec R^{\,T}\vec w=\mu\\[2pt] \vec 1^{\,T}\vec w=1\\[2pt] \Sigma\vec w+\eta\,\vec R+\theta\,\vec 1=\vec 0 \end{cases} \quad\Longleftrightarrow\quad \underbrace{\begin{bmatrix} 0&0&\vec R^{\,T}\\ 0&0&\vec 1^{\,T}\\ \vec R&\vec 1&\Sigma \end{bmatrix}}_{W} \begin{bmatrix}\eta\\\theta\\\vec w\end{bmatrix} = \begin{bmatrix}\mu\\1\\\vec 0\end{bmatrix} $$ Looking at this row by row: * **1st Row:** $0\cdot\eta+0\cdot\theta+\vec R^{\,T}\vec w=\mu$ (Expected Return Constraint) * **2nd Row:** $0\cdot\eta+0\cdot\theta+\vec 1^{\,T}\vec w=1$ (Sum of Weights = 1 Constraint) * **3rd Row:** $\vec R\,\eta+\vec 1\,\theta+\Sigma\vec w=\vec 0$ (Stationarity Condition) This explains why "solving the matrix $W$ satisfies both the objective function and the constraints simultaneously." This block matrix $W$ is called the **KKT Matrix** (symmetric and indefinite). Therefore, based on the results of the Lagrange Multiplier Method described above, we can obtain the optimal weights $\mathbf{w}$ that satisfy the conditions by solving the linear system of equations: $$ \mathbf{W} \begin{pmatrix} \nu\\\theta\\\mathbf{w} \end{pmatrix} = \begin{pmatrix} \mu\\1\\\mathbf{0}\end{pmatrix}\,,\, W = \begin{bmatrix} 0&0&\vec R^{\,T}\\ 0&0&\vec 1^{\,T}\\ \vec R&\vec 1&\Sigma \end{bmatrix} $$ using the HHL algorithm. ## Preprocessing for HHL # ## Construction of Matrix $W$ ```python theme={null} R = expected_return.values ``` ```python theme={null} Pi = np.ones(4) S = cov.values row1 = np.append(np.zeros(2), R).reshape(1, -1) row2 = np.append(np.zeros(2), Pi).reshape(1, -1) row3 = np.concatenate([R.reshape(-1, 1), Pi.reshape(-1, 1), S], axis=1) W = np.concatenate([row1, row2, row3]) np.set_printoptions(linewidth=200) print(W) ``` **Output:** ``` [[ 0. 0. 0.33760624 0.43059648 1.19108918 0.71832606] [ 0. 0. 1. 1. 1. 1. ] [0.33760624 1. 0.05020389 0.02116691 0.02981871 0.04674084] [0.43059648 1. 0.02116691 0.07889178 0.06422647 0.05653149] [1.19108918 1. 0.02981871 0.06422647 0.27498126 0.07109579] [0.71832606 1. 0.04674084 0.05653149 0.07109579 0.40357553]] ``` # ## Construction of Vector $\begin{pmatrix}\mu\\1\\\mathbf{0}\end{pmatrix}$ ```python theme={null} mu = 0.1 xi = 1.0 b = np.append(np.array([mu, xi]), np.zeros_like(R)).reshape(-1, 1) print(b) ``` **Output:** ``` [[0.1] [ 1. ] [ 0. ] [ 0. ] [ 0. ] [ 0. ]] ``` ## Redefining the Matrix Typically, the matrix formulation for HHL assumes a standard form with the following properties: 1. The right-hand side vector $\vec{b}$ is normalized. 2. The matrix $A$ has a size of $2^n \times 2^n$. 3. The matrix $A$ is Hermitian. 4. The eigenvalues of matrix $A$ lie within the range $(0,1)$. However, even general problems that do not meet these conditions can be solved using the following methods: # ## 1) Normalized b As preprocessing, normalize $\vec{b}$ and then return the normalization factor as postprocessing. ```python theme={null} norm_factor = np.linalg.norm(b) b_normalized = b / norm_factor ``` # ## 2) Make the Matrix $A$ of Size $2^n\times 2^n $ Complete the matrix dimension to the closest $2^n$ with an identity matrix. The vector $\vec{b}$ is completed with zeros. $$ \begin{pmatrix} A & 0 \\ 0 & I \end{pmatrix} \begin{pmatrix} \vec{b} \\ 0 \end{pmatrix} = \begin{pmatrix} \vec{x} \\ 0 \end{pmatrix}. $$ However, our matrix is already the right size. ```python theme={null} ## rquired number of qubits for encoding data nbit = 3 N = 2**nbit # padding W = np.block([[W, np.zeros((6, 2))], [np.zeros((2, 6)), np.eye(2)]]) b = np.vstack([b, np.zeros((2, 1))]) print("W_padded:\n", W) print("b_padded:\n", b) ``` **Output:** ``` W_padded: [[ 0. 0. 0.33760624 0.43059648 1.19108918 0.71832606 0. 0. ] [ 0. 0. 1. 1. 1. 1. 0. 0. ] [0.33760624 1. 0.05020389 0.02116691 0.02981871 0.04674084 0. 0. ] [0.43059648 1. 0.02116691 0.07889178 0.06422647 0.05653149 0. 0. ] [1.19108918 1. 0.02981871 0.06422647 0.27498126 0.07109579 0. 0. ] [0.71832606 1. 0.04674084 0.05653149 0.07109579 0.40357553 0. 0. ] [ 0. 0. 0. 0. 0. 0. 1. 0. ] [ 0. 0. 0. 0. 0. 0. 0. 1. ]] b_padded: [[0.1] [ 1. ] [ 0. ] [ 0. ] [ 0. ] [ 0. ] [ 0. ] [ 0. ]] ``` # ## 3) Hermitian Matrix Symmetrize the problem: $$ \begin{pmatrix} 0 & A^T \\ A & 0 \end{pmatrix} \begin{pmatrix} \vec{x} \\ 0 \end{pmatrix} = \begin{pmatrix} 0 \\ \vec{b} \end{pmatrix}. $$ This increases the number of qubits by 1. ```python theme={null} def to_hermitian(A): N = A.shape[0] A_hermitian = np.concatenate( [ np.concatenate([np.zeros((N, N)), A.transpose().conj()], axis=1), np.concatenate([A, np.zeros((N, N))], axis=1), ] ) return A_hermitian N = W.shape[0] b_new = np.concatenate([np.zeros((2 * N - len(b_normalized), 1)), b_normalized]) plt.matshow(b_new.transpose()) plt.title("Normalized and Padded Vector b") plt.show() W_hermitian = to_hermitian(W) plt.matshow(W_hermitian) plt.title("Hermitian Matrix W") plt.show() ``` output output # ## 4) Rescaled Matrix If the eigenvalues of matrix $A$ lie within the interval $[w_{\min}, w_{\max}]$, we can handle this by transforming the matrix and then reverting the result. The transformation is defined as follows: $$ \tilde{A} = (A - w_{\min} I)\left(1 - \frac{1}{2^{m}}\right)\frac{1}{w_{\max} - w_{\min}}. $$ In this case, the eigenvalues of $\tilde{A}$ fall within the interval $[0,1)$. The relationship with the eigenvalues of the original matrix is given by: $$ \lambda = (w_{\max} - w_{\min})\tilde{\lambda}\left[\frac{1}{1 - \frac{1}{2^{m}}}\right] + w_{\min}, $$ Where $\tilde{\lambda}$ is the eigenvalue of $\tilde{A}$ obtained by the QPE (Quantum Phase Estimation) algorithm. This correspondence between eigenvalues is used in the formula for eigenvalue inversion within the `AmplitudeLoading` function. ```python theme={null} def condition_number(A): w, _ = np.linalg.eig(A) return max(np.abs(w)) / min(np.abs(w)) QPE_RESOLUTION_SIZE = 6 assert QPE_RESOLUTION_SIZE > np.log2( condition_number(W) ), "condition number is too big, and QPE resolution cannot hold all eigenvalues" w, v = np.linalg.eigh(W_hermitian) w_max = np.max(w) w_min = np.min(w) mat_shift = -w_min # assures eigenvalues in [0,1-1/2^QPE_SIZE] mat_rescaling = (1 - 1 / 2**QPE_RESOLUTION_SIZE) / (w_max - w_min) # this is the minimal eigenvalue which can be resolved by the QPE min_possible_w = (w_max - w_min) / 2**QPE_RESOLUTION_SIZE W_rescaled = ( W_hermitian + mat_shift * np.identity(W_hermitian.shape[0]) ) * mat_rescaling W_rescaled = W_rescaled.real # verifying that the matrix is symmetric and has eigenvalues in [0,1) if not np.allclose(W_rescaled, W_rescaled.T, rtol=1e-6, atol=1e-6): raise Exception("The matrix is not symmetric") w_rescaled, _ = np.linalg.eigh(W_rescaled) for lam in w_rescaled: if lam < -1e-6 or lam >= 1: raise Exception("Eigenvalues are not in (0,1)") plt.matshow(W_rescaled) plt.title("Rescaled Matrix W") plt.show() ``` output ## Defining HHL Algorithm for the Quantum Solution This section is based on Classiq HHL in the user guide, [here](https://github.com/Classiq/classiq-library/blob/main/tutorials/technology_demonstrations/hhl/hhl_example.ipynb) and [here](https://github.com/Classiq/classiq-library/blob/main/applications/physical_systems/hhl_lanchester/hhl_lanchester.ipynb). Note the rescaling in `simple_eig_inv` based on the matrix rescaling. ```python theme={null} from classiq import * @qfunc def simple_eig_inv( gamma: float, delta: float, c_param: float, phase: QNum, indicator: Output[QBit], ): allocate(indicator) assign_amplitude_table( lookup_table(lambda p: np.clip(c_param / ((gamma * p) + delta), -1, 1), phase), phase, indicator, ) ``` ```python theme={null} import numpy as np import scipy exponentiation_W_rescaled = scipy.linalg.expm(1j * 2 * np.pi * W_rescaled).tolist() b_list = np.concatenate(b_new).tolist() ``` ```python theme={null} @qfunc def main( indicator: Output[QBit], res: Output[QNum], rescaled_eig: Output[QNum], ) -> None: allocate(QPE_RESOLUTION_SIZE, False, QPE_RESOLUTION_SIZE, rescaled_eig) prepare_amplitudes(b_list, 0, res) within_apply( lambda: qpe( unitary=lambda: unitary(exponentiation_W_rescaled, res), phase=rescaled_eig, ), lambda: simple_eig_inv( gamma=mat_rescaling ** (-1), delta=-mat_shift, c_param=min_possible_w, phase=rescaled_eig, indicator=indicator, ), ) MAX_WIDTH_BASIC = 18 constraints = Constraints(max_width=MAX_WIDTH_BASIC) # preferences = Preferences( # optimization_level=0, optimization_timeout_seconds=90, transpilation_option="none" # ) backend_preferences = ClassiqBackendPreferences( backend_name=ClassiqSimulatorBackendNames.SIMULATOR_STATEVECTOR ) execution_preferences = ExecutionPreferences( num_shots=1, backend_preferences=backend_preferences ) qmod_hhl_basic = create_model( main, constraints=constraints, # preferences=preferences, execution_preferences=execution_preferences, ) qprog = synthesize(qmod_hhl_basic) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/38EbCHhb4OwIspsNgY9Dv5T54JR ``` ```python theme={null} execution_job_id = execute(qprog) result = execution_job_id.result_value() ``` ```python theme={null} filtered_hhl_statevector = dict() for sample in result.parsed_state_vector: if sample.state["indicator"] == 1 and sample.state["rescaled_eig"] == 0: filtered_hhl_statevector[sample.state["res"]] = sample.amplitude states = sorted(filtered_hhl_statevector) raw_qsol = np.array([filtered_hhl_statevector[s] for s in states]) ``` ```python theme={null} qsol_hermitian = raw_qsol / (min_possible_w) print(qsol_hermitian) ``` **Output:** ``` [-2.89639576e-01+5.95742356e-01j 3.08335503e-01-6.34196894e-01j -2.27110910e+00+4.67130877e+00j 2.75233940e+00-5.66112264e+00j -1.44012178e-01+2.96210054e-01j -3.36318572e-01+6.91753598e-01j 2.39148259e-14-7.26657302e-14j -7.64513494e-15-3.53183012e-14j 3.90607598e-14-2.45511623e-13j 6.08553768e-15+1.83911091e-13j -6.16524881e-14+1.44844906e-13j 5.18031970e-14-1.22156005e-13j 3.10460540e-14+8.93174264e-14j -2.39948943e-14-1.53061828e-13j 2.33085233e-14+1.36384476e-14j -2.52635402e-14+1.00776109e-14j] ``` ```python theme={null} x_classical = np.array([s[0] for s in np.linalg.solve(W_hermitian, b_new)]) x_classical = x_classical[:6].reshape(-1) x_hhl = qsol_hermitian[:6].reshape(-1) alpha = (x_hhl @ x_classical) / (x_hhl @ x_hhl) x_hhl_rescaled = alpha * x_hhl print( "rel error:", np.linalg.norm(x_hhl_rescaled - x_classical) / np.linalg.norm(x_classical), ) ``` **Output:** ``` rel error: 0.08021847118398923 ``` ```python theme={null} print("x-HHL", x_hhl_rescaled.real) print("x-classical", x_classical.real) ``` **Output:** ``` x-HHL [ -1.27904651 1.36160761 -10.02920324 12.15431315 -0.63595686 -1.48518066] x-classical [ -0.67035649 0.65436901 -10.36202934 12.09472133 -0.98362358 -0.74906842] ``` ```python theme={null} x_exact = np.linalg.lstsq(W_hermitian, b_new, rcond=0)[0] x_exact = x_exact[:6].reshape(-1) ``` ```python theme={null} import pandas as pd w_opt_HHL = x_hhl_rescaled.real[2:6] w_opt_exact = x_classical[2:6] # x_exact[2:6] w_opt = pd.DataFrame( np.vstack([w_opt_exact, w_opt_HHL]).T, index=["GOOG", "AAPL", "FB", "AMZN"], columns=["exact", "HHL"], ) w_opt.plot.bar() ``` **Output:** ``` ``` output The next step is to calculate the "total portfolio value for each day." This operation involves multiplying the price of each asset by its weight and summing them up. Expressed as a formula, it looks like this: $$ \text{Portfolio Value} = w_{\text{AAPL}} \cdot P_{\text{AAPL}} + w_{\text{AMZN}} \cdot P_{\text{AMZN}} + w_{\text{NVDA}} \cdot P_{\text{NVDA}} + w_{\text{TSLA}} \cdot P_{\text{TSLA}} $$ For example, if we assume: $$ w = [0.4, 0.3, 0.2, 0.1] $$ Then: $$ \text{Portfolio Value} = 0.4A + 0.3B + 0.2C + 0.1D $$ This gives you the general idea. To perform this calculation automatically for all dates, you can write it in a single line of Python code as follows: ```python theme={null} pf_value = df.values.dot(w_opt) pf_value = pd.DataFrame(df.values.dot(w_opt), index=df.index, columns=["exact", "HHL"]) pf_value.head() ``` | | exact | HHL | | ---------- | ----------- | ----------- | | Date | | | | --- | --- | --- | | 2024-01-02 | -325.764298 | -421.744540 | | 2024-01-03 | -320.943657 | -410.339168 | | 2024-01-04 | -344.106605 | -433.974406 | | 2024-01-05 | -329.292530 | -418.664555 | | 2024-01-08 | -332.896116 | -421.666555 | ```python theme={null} pf_value["exact"] = pf_value["exact"] / pf_value["exact"].iloc[0] pf_value["HHL"] = pf_value["HHL"] / pf_value["HHL"].iloc[0] print(pf_value.tail()) ``` **Output:** ``` exact HHL Date 2024-12-23 1.139395 1.284214 2024-12-24 1.157931 1.350494 2024-12-26 1.238676 1.398530 2024-12-27 1.191926 1.328773 2024-12-30 1.144376 1.269782 ``` ```python theme={null} pf_value.plot(figsize=(9, 6)) ``` **Output:** ``` ``` output ## References * \[1] P. Rebentrost and S. Lloyd, "Quantum computational finance: quantum algorithm for portfolio optimization", [https://arxiv.org/abs/1811.03975](https://arxiv.org/abs/1811.03975) * \[2] 7-3. Portfolio optimization using HHL algorithm: [https://dojo.qulacs.org/en/latest/notebooks/7.3\_application\_of\_HHL\_algorithm.html](https://dojo.qulacs.org/en/latest/notebooks/7.3_application_of_HHL_algorithm.html) # Rainbow Options with Brute-Force Methodology Source: https://docs.classiq.io/explore/applications/finance/rainbow_options/rainbow_options_bruteforce_method Open this notebook in GitHub to run it yourself This notebook covers the implementation of the rainbow option using Qmod. The role of the notebook is to verify the result of a different methodology on a small scale problem, as it grows exponentially in the gate count. In finance, a crucial aspect of asset pricing pertains to derivatives. Derivatives are contracts whose value is contingent upon another source, known as the underlying. The pricing of options - a specific derivative instrument - involves determining the fair market value (discounted payoff) of contracts that afford their holders the right, although not the obligation, to buy (call) or sell (put) one or more underlying assets at a predefined strike price by a specified future expiration date (maturity date). This process relies on mathematical models, considering variables such as current asset prices, time to expiration, volatility, and interest rates. ## Data Definitions The problem inputs: * `NUM_QUBITS`: the number of qubits representing an underlying asset * `NUM_ASSETS`: the number of underlying assets * `K`: the strike price * `S0`: the arrays of underlying asset prices * `dt`: the number of days to the maturity date * `COV`: the covariance matrix that correlates to the underlyings * `MU_LOG_RET`: the array containing the mean of the log return of each underlying ```python theme={null} import numpy as np import scipy NUM_QUBITS = 2 NUM_ASSETS = 2 K = 190 S0 = [193.97, 189.12] dt = 250 COV = np.array([[0.000335, 0.000257], [0.000257, 0.000418]]) MU_LOG_RET = np.array([0.00050963, 0.00062552]) ``` ```python theme={null} MU = MU_LOG_RET * dt CHOLESKY = np.linalg.cholesky(COV) * np.sqrt(dt) SCALING_FACTOR = 1 / CHOLESKY[0, 0] ``` ```python theme={null} from classiq import * EPSILON = 0.05 ALPHA = 0.1 ``` ## Gaussian State Preparation Encode the probability distribution of a discrete multivariate random variable $W$ taking values in $\{w_0, .., w_{N-1}\}$ describing the asset prices at the maturity date. The number of discretized values, denoted as $N$, depends on the precision of the state preparation module and is consequently connected to the number of qubits ($n=$) according to the formula $N=2^n$: $$ \sum_{i=0}^{N-1} \sqrt{p(w_i)}\left|w_i\right\rangle $$ ```python theme={null} def gaussian_discretization(num_qubits, mu=0, sigma=1, stds_around_mean_to_include=3): lower = mu - stds_around_mean_to_include * sigma upper = mu + stds_around_mean_to_include * sigma num_of_bins = 2**num_qubits sample_points = np.linspace(lower, upper, num_of_bins + 1) def single_gaussian(x: np.ndarray, _mu: float, _sigma: float) -> np.ndarray: cdf = scipy.stats.norm.cdf(x, loc=_mu, scale=_sigma) return cdf[1:] - cdf[0:-1] non_normalized_pmf = (single_gaussian(sample_points, mu, sigma),) real_probs = non_normalized_pmf / np.sum(non_normalized_pmf) return sample_points[:-1], real_probs[0].tolist() grid_points, probabilities = gaussian_discretization(NUM_QUBITS) STEP_X = grid_points[1] - grid_points[0] MIN_X = grid_points[0] ``` # ## Sanity Check To avoid meaningless results, the process must stop if the strike price $K$ is greater than the maximum value reacheable by the assets during the simulation. In this case, the payoff is $0$, so there is no need to simulate: ```python theme={null} from IPython.display import Markdown if K >= max(S0 * np.exp(np.dot(CHOLESKY, [grid_points[-1]] * 2) + MU)): display( Markdown( " K always greater than the maximum asset values. Stop the run, the payoff is 0" ) ) ``` ## Maximum Computation # ## Precision Utils ```python theme={null} FRAC_PLACES = 1 def round_factor(a): precision_factor = 2 ** (FRAC_PLACES) return np.floor(a * precision_factor) / precision_factor def floor_factor(a): precision_factor = 2 ** (FRAC_PLACES) return np.floor(a * precision_factor) / precision_factor ``` # ## Affine and Maximum Arithmetic Definitions Considering the time delta between the starting date ($t_0$) and the maturity date ($t$), express the return value $R_i$ for the $i$-th asset as $R_i = \mu_i + y_i$ where $\mu_i= (t-t_0)\tilde{\mu}_i$, being $\tilde{\mu}_i$ the expected daily log-return value. It can be estimated by considering the historical time series of log returns for the $i$-th asset. $y_i$ is obtained through the dot product between the matrix $\mathbf{L}$ and the standard multivariate Gaussian sample: $$ y_i = \Delta x \cdot \sum_kl_{ik}d_k + x_{min} \cdot \sum_k l_{ik} $$ $\Delta x$ is the Gaussian discretization step, $x_{min}$ is the lower Gaussian truncation value, and $d_k \in [0,2^m-1]$ is the sample taken from the $k$-th standard Gaussian. $l_{ik}$ is the $i,k$ entry of the matrix $\mathbf{L}$, defined as $\mathbf{L}=\mathbf{C}\sqrt{(t-t_0)}$, where $\mathbf{C}$ is the lower triangular matrix obtained by applying the Cholesky decomposition to the historical daily log-return correlation matrix: ```python theme={null} from functools import reduce from classiq.qmod.symbolic import max as qmax a = STEP_X / SCALING_FACTOR b = np.log(S0[0]) + MU[0] + MIN_X * CHOLESKY[0].sum() def get_affine_formula(assets, i): return reduce( lambda x, y: x + y, [ assets[j] * round_factor(SCALING_FACTOR * CHOLESKY[i, j]) for j in range(NUM_ASSETS) if CHOLESKY[i, j] ], ) c = ( SCALING_FACTOR * ( np.log(S0[1]) + MU[1] - (np.log(S0[0]) + MU[0]) + MIN_X * sum(CHOLESKY[1] - CHOLESKY[0]) ) / (STEP_X) ) c = round_factor(c) def calculate_max_reg_type(): x1 = QNum(size=NUM_QUBITS) x2 = QNum(size=NUM_QUBITS) expr = qmax(get_affine_formula([x1, x2], 0), get_affine_formula([x1, x2], 1) + c) size_in_bits, sign, fraction_digits = get_expression_numeric_attributes( [x1, x2], expr ) return size_in_bits, fraction_digits MAX_NUM_QUBITS = calculate_max_reg_type()[0] MAX_FRAC_PLACES = calculate_max_reg_type()[1] ``` ```python theme={null} @qperm def affine_max(x1: Const[QNum], x2: Const[QNum], res: Output[QNum]): res |= qmax(get_affine_formula([x1, x2], 0), get_affine_formula([x1, x2], 1) + c) ``` ## Brute-Force Amplitude Loading Method This type of amplitude loading has an exponential scale, and is therefore used as a "sanity check" method for validating the result from the direct method and integration method that are part of the paper [\[1\]](#qalrop). ```python theme={null} def get_payoff_expression(x, size, fraction_digits): payoff = np.sqrt( max( S0[0] * np.exp( STEP_X / SCALING_FACTOR * (2 ** (size - fraction_digits)) * x + (MU[0] + MIN_X * CHOLESKY[0].sum()) ), K, ) ) return payoff def get_payoff_expression_normalized(x, size, fraction_digits): x_max = 1 - 1 / (2**size) payoff_max = get_payoff_expression(x_max, size, fraction_digits) payoff = get_payoff_expression(x, size, fraction_digits) return payoff / payoff_max @qfunc def brute_force_payoff(max_reg: Const[QNum], ind_reg: QBit): max_reg_fixed = QNum( size=max_reg.size, is_signed=False, fraction_digits=max_reg.size ) bind(max_reg, max_reg_fixed) assign_amplitude_table( lookup_table( lambda n: get_payoff_expression_normalized( n, max_reg.size, max_reg.fraction_digits ), max_reg_fixed, ), max_reg_fixed, ind_reg, ) bind(max_reg_fixed, max_reg) ``` ```python theme={null} class EstimationVars(QStruct): x1: QNum[NUM_QUBITS] x2: QNum[NUM_QUBITS] @qfunc def rainbow_brute_force(qvars: EstimationVars, ind: QBit) -> None: inplace_prepare_state(probabilities, 0, qvars.x1) inplace_prepare_state(probabilities, 0, qvars.x2) max_out = QNum() affine_max(qvars.x1, qvars.x2, max_out), brute_force_payoff(max_out, ind) @qfunc def main(qvars: Output[EstimationVars], ind: Output[QBit]) -> None: allocate(qvars) allocate(ind) rainbow_brute_force(qvars, ind) MAX_WIDTH_1 = 14 qmod_1 = create_model(main) qmod_1 = update_constraints(qmod_1, max_width=MAX_WIDTH_1) qprog_1 = synthesize(qmod_1) show(qprog_1) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3G9AQVpjWqyktrGqPTvHWX70bKR ``` ## Iterative Quantum Amplitude Estimation (IQAE) Algorithm ```python theme={null} from classiq.applications.iqae.iqae import IQAE MAX_WIDTH_2 = 25 iqae = IQAE( state_prep_op=rainbow_brute_force, problem_vars_size=NUM_QUBITS * NUM_ASSETS, constraints=Constraints(max_width=MAX_WIDTH_2), preferences=Preferences(optimization_level=1), ) ``` ```python theme={null} qmod_2 = iqae.get_model() print("Starting synthesis") qprog_2 = iqae.get_qprog() show(qprog_2) print("Starting execution") result = iqae.run(EPSILON, ALPHA) ``` **Output:** ``` Starting synthesis Quantum program link: https://platform.classiq.io/circuit/3G9ASwGk3jmrvCZX2G0RnJciE7c Starting execution ``` ## Post-Process Add a term to the post-processing function: $$ \begin{split} &\mathbb{E} \left[\max\left(e^{b \cdot z}, Ke^{-b'}\right) \right] e^{b'} - K \\ = &\mathbb{E} \left[\max\left(e^{-a\hat{x}}, Ke^{-b'-ax_{max}}\right) \right]e^{b'+ ax_{max}} - K \end{split} $$ ```python theme={null} import sympy payoff_expression = f"sqrt(max([{S0[0]} * exp({STEP_X / SCALING_FACTOR * (2 ** (MAX_NUM_QUBITS - MAX_FRAC_PLACES))} * x + ({MU[0]+MIN_X*CHOLESKY[0].sum()})), {K}]))" payoff_func = sympy.lambdify(sympy.symbols("x"), payoff_expression) payoff_max = payoff_func(1 - 1 / (2**MAX_NUM_QUBITS)) def parse_result_bruteforce(iqae_res): option_value = iqae_res.estimation * (payoff_max**2) - K confidence_interval = np.array(iqae_res.confidence_interval) * (payoff_max**2) - K return (option_value, confidence_interval) ``` ## Run Method ```python theme={null} parsed_result, conf_interval = parse_result_bruteforce(result) print( f"raw iqae results: {result.estimation} with confidence interval {result.confidence_interval}" ) print( f"option estimated value: {parsed_result} with confidence interval {conf_interval}" ) ``` **Output:** ``` raw iqae results: 0.5068359375 with confidence interval [0.4752195518687129, 0.5384523231312872] option estimated value: 24.15154343112667 with confidence interval [10.79278712 37.51029975] ``` ```python theme={null} expected_payoff = 23.0238 ALPHA_ASSERTION = 1e-5 measured_confidence = conf_interval[1] - conf_interval[0] confidence_scale_by_alpha = np.sqrt( np.log(ALPHA / ALPHA_ASSERTION) ) # based on e^2=(1/2N)*log(2T/alpha) from "Iterative Quantum Amplitude Estimation" since our alpha is low, we want to check within a bigger confidence interval assert ( np.abs(parsed_result - expected_payoff) <= 0.5 * measured_confidence * confidence_scale_by_alpha ), f"Payoff result is out of the {ALPHA_ASSERTION*100}% confidence interval: |{parsed_result} - {expected_payoff}| > {0.5*measured_confidence * confidence_scale_by_alpha}" ``` ## References \[1] [Francesca Cibrario et al., Quantum Amplitude Loading for Rainbow Options Pricing. Preprint.](https://arxiv.org/abs/2402.05574v2) # Rainbow Options with Direct Amplitude Loading Source: https://docs.classiq.io/explore/applications/finance/rainbow_options/rainbow_options_direct_method Open this notebook in GitHub to run it yourself This notebook covers the implementation of the Direct Amplitude Loading Method for the rainbow option presented in [\[1\]](#qalrop). In finance, a crucial aspect of asset pricing pertains to derivatives. Derivatives are contracts whose value is contingent upon another source, known as the underlying. The pricing of options - a specific derivative instrument - involves determining the fair market value (discounted payoff) of contracts that afford their holders the right, though not the obligation, to buy (call) or sell (put) one or more underlying assets at a predefined strike price by a specified future expiration date (maturity date). This process relies on mathematical models, considering variables such as current asset prices, time to expiration, volatility, and interest rates. ## Data Definitions The problem inputs: * `NUM_QUBITS`: the number of qubits representing an underlying asset * `NUM_ASSETS`: the number of underlying assets * `K`: the strike price * `S0`: the arrays of underlying asset prices * `dt`: the number of days to the maturity date * `COV`: the covariance matrix that correlates to the underlyings * `MU_LOG_RET`: the array containing the mean of the log return of each underlying ```python theme={null} import numpy as np import scipy NUM_QUBITS = 2 NUM_ASSETS = 2 K = 190 S0 = [193.97, 189.12] dt = 250 COV = np.array([[0.000335, 0.000257], [0.000257, 0.000418]]) MU_LOG_RET = np.array([0.00050963, 0.00062552]) ``` ```python theme={null} MU = MU_LOG_RET * dt CHOLESKY = np.linalg.cholesky(COV) * np.sqrt(dt) SCALING_FACTOR = 1 / CHOLESKY[0, 0] ``` ```python theme={null} from classiq import * EPSILON = 0.05 ALPHA = 0.1 ``` ## Gaussian State Preparation Encode the probability distribution of a discrete multivariate random variable $W$ taking values in $\{w_0, .., w_{N-1}\}$ describing the asset prices at the maturity date. The number of discretized values, denoted as $N$, depends on the precision of the state preparation module and is consequently connected to the number of qubits ($n=$) according to the formula $N=2^n$: $$ \sum_{i=0}^{N-1} \sqrt{p(w_i)}\left|w_i\right\rangle $$ ```python theme={null} def gaussian_discretization(num_qubits, mu=0, sigma=1, stds_around_mean_to_include=3): lower = mu - stds_around_mean_to_include * sigma upper = mu + stds_around_mean_to_include * sigma num_of_bins = 2**num_qubits sample_points = np.linspace(lower, upper, num_of_bins + 1) def single_gaussian(x: np.ndarray, _mu: float, _sigma: float) -> np.ndarray: cdf = scipy.stats.norm.cdf(x, loc=_mu, scale=_sigma) return cdf[1:] - cdf[0:-1] non_normalized_pmf = (single_gaussian(sample_points, mu, sigma),) real_probs = non_normalized_pmf / np.sum(non_normalized_pmf) return sample_points[:-1], real_probs[0].tolist() grid_points, probabilities = gaussian_discretization(NUM_QUBITS) STEP_X = grid_points[1] - grid_points[0] MIN_X = grid_points[0] ``` # ## Sanity Check To avoid meaningless results, the process must stop if the strike price $K$ is greater than the maximum value reacheable by the assets during the simulation. In this case, the payoff is $0$, so there is no need to simulate: ```python theme={null} from IPython.display import Markdown if K >= max(S0 * np.exp(np.dot(CHOLESKY, [grid_points[-1]] * 2) + MU)): display( Markdown( " K always greater than the maximum asset values. Stop the run, the payoff is 0" ) ) ``` ## Maximum Computation # ## Precision Utils ```python theme={null} FRAC_PLACES = 2 def round_factor(a): precision_factor = 2**FRAC_PLACES return round(a * precision_factor) / precision_factor def floor_factor(a): precision_factor = 2**FRAC_PLACES return np.floor(a * precision_factor) / precision_factor ``` # ## Affine and Maximum Arithmetic Definitions Considering the time delta between the starting date ($t_0$) and the maturity date ($t$), express the return value $R_i$ for the $i$-th asset as $R_i = \mu_i + y_i$ where $\mu_i= (t-t_0)\tilde{\mu}_i$, being $\tilde{\mu}_i$ the expected daily log-return value. It can be estimated by considering the historical time series of log returns for the $i$-th asset. $y_i$ is obtained through the dot product between the matrix $\mathbf{L}$ and the standard multivariate Gaussian sample: $$ y_i = \Delta x \cdot \sum_kl_{ik}d_k + x_{min} \cdot \sum_k l_{ik} $$ $\Delta x$ is the Gaussian discretization step, $x_{min}$ is the lower Gaussian truncation value, and $d_k \in [0,2^m-1]$ is the sample taken from the $k$-th standard Gaussian. $l_{ik}$ is the $i,k$ entry of the matrix $\mathbf{L}$, defined as $\mathbf{L}=\mathbf{C}\sqrt{(t-t_0)}$, where $\mathbf{C}$ is the lower triangular matrix obtained by applying the Cholesky decomposition to the historical daily log-return correlation matrix: ```python theme={null} from functools import reduce from classiq.qmod.symbolic import max as qmax a = STEP_X / SCALING_FACTOR b = np.log(S0[0]) + MU[0] + MIN_X * CHOLESKY[0].sum() def get_affine_formula(assets, i): return reduce( lambda x, y: x + y, [ assets[j] * round_factor(SCALING_FACTOR * CHOLESKY[i, j]) for j in range(NUM_ASSETS) if CHOLESKY[i, j] ], ) c = ( SCALING_FACTOR * ( np.log(S0[1]) + MU[1] - (np.log(S0[0]) + MU[0]) + MIN_X * sum(CHOLESKY[1] - CHOLESKY[0]) ) / (STEP_X) ) c = round_factor(c) def calculate_max_reg_type(): x1 = QNum(size=NUM_QUBITS) x2 = QNum(size=NUM_QUBITS) expr = qmax(get_affine_formula([x1, x2], 0), get_affine_formula([x1, x2], 1) + c) size_in_bits, sign, fraction_digits = get_expression_numeric_attributes( [x1, x2], expr ) return size_in_bits, fraction_digits MAX_NUM_QUBITS = calculate_max_reg_type()[0] MAX_FRAC_PLACES = calculate_max_reg_type()[1] ``` ```python theme={null} @qperm def affine_max(x1: Const[QNum], x2: Const[QNum], res: Output[QNum]): res |= qmax(get_affine_formula([x1, x2], 0), get_affine_formula([x1, x2], 1) + c) ``` ## Direct Method The direct exponential amplitude loading encodes in $\tilde{f}$ the following function: $$ \tilde{f}(x)= \begin{cases} e^{-a\hat{x}}, & \text{if } \frac{x}{2^P} \geq \frac{\log(K) -b'}{b}\\ Ke^{-(b'+ ax_{max})}, & \text{if } \frac{x}{2^P} < \frac{\log(K) -b'}{b} \end{cases} $$ where $\hat{x}$ is the binary complement of $x$ ($\hat{x}=x-x_{max}$) and $x_{max}=2^R-1$, the maximum value that can be stored in the $|x\rangle$ register. For loading $e^{-a\hat{x}}$, the $|r\rangle$ is initialized to all zeros. One controlled rotation for each qubit is performed. The rotation angles are $\theta_i = 2\arccos \left({\sqrt{e^{-a2^i}}}\right)$. All the probabilities of getting a $|0\rangle^{\otimes{R}}$ in the $|r\rangle$ are then collected by a multi-controlled X (MCX) gate and stored in the $|1\rangle$ state of a target qubit. ```python theme={null} from classiq.qmod.symbolic import acos, asin, exp, sqrt @qfunc def exponential_amplitude_loading( exp_rate: CReal, x: Const[QArray[QBit]], aux: QArray[QBit], res: QBit ) -> None: within_apply( lambda: apply_to_all(X, x), lambda: repeat( x.len, lambda index: control( x[index], lambda: RY(2 * acos(1 / sqrt(exp(exp_rate * (2**index)))), aux[index]), ), ), ) aux_num = QNum() within_apply(lambda: bind(aux, aux_num), lambda: inplace_xor(aux_num == 0, res)) ``` ```python theme={null} class EstimationVars(QStruct): x1: QNum[NUM_QUBITS] x2: QNum[NUM_QUBITS] aux: QNum[MAX_NUM_QUBITS] def get_payoff_expression(x, size, fraction_digits): payoff = sqrt( qmax( S0[0] * exp( STEP_X / SCALING_FACTOR * (2 ** (size - fraction_digits)) * x + (MU[0] + MIN_X * CHOLESKY[0].sum()) ), K, ) ) return payoff def get_strike_price_theta_direct(x: QNum): x_max = 1 - 1 / (2**x.size) payoff_max = get_payoff_expression(x_max, x.size, x.fraction_digits) return 2 * asin(np.sqrt(K) / payoff_max) # this is not a qfunc, just a utility function def is_geq_strike_price( x: Const[QNum], ) -> None: a = STEP_X / SCALING_FACTOR b = np.log(S0[0]) + MU[0] + MIN_X * CHOLESKY[0].sum() COMP_VALUE = (np.log(K) - b) / a return x > floor_factor(COMP_VALUE) @qfunc def direct_payoff(max_reg: Const[QNum], aux_reg: QNum, ind_reg: QBit): exp_rate = (1 / (2**max_reg.fraction_digits)) * a control( is_geq_strike_price(max_reg), lambda: exponential_amplitude_loading(exp_rate, max_reg, aux_reg, ind_reg), lambda: RY(get_strike_price_theta_direct(max_reg), ind_reg), ) @qfunc def rainbow_direct(qvars: EstimationVars, ind: QBit) -> None: inplace_prepare_state(probabilities, 0, qvars.x1) inplace_prepare_state(probabilities, 0, qvars.x2) max_out = QNum() affine_max(qvars.x1, qvars.x2, max_out) direct_payoff(max_out, qvars.aux, ind) @qfunc def main(qvars: Output[EstimationVars], ind: Output[QBit]) -> None: allocate(qvars) allocate(ind) rainbow_direct(qvars, ind) MAX_WIDTH = 25 qmod = create_model( main, constraints=Constraints(max_width=MAX_WIDTH), preferences=Preferences(optimization_level=1), ) print("Starting synthesis") qprog = synthesize(qmod) show(qprog) ``` **Output:** ``` Starting synthesis Quantum program link: https://platform.classiq.io/circuit/3560PSetPbgGMGz0mmh6EGTR5Da ``` ## Iterative Quantum Amplitude Estimation (IQAE) Algorithm ```python theme={null} from classiq.applications.iqae.iqae import IQAE iqae = IQAE( state_prep_op=rainbow_direct, problem_vars_size=NUM_QUBITS * NUM_ASSETS + MAX_NUM_QUBITS, constraints=Constraints(max_width=MAX_WIDTH), preferences=Preferences(optimization_level=1), ) ``` ```python theme={null} qmod_2 = iqae.get_model() print("Starting synthesis") qprog_2 = iqae.get_qprog() show(qprog_2) print("Starting execution") result = iqae.run(EPSILON, ALPHA) ``` **Output:** ``` Starting synthesis Quantum program link: https://platform.classiq.io/circuit/3560WokLN0aA0BmujYpOGy5jaAV Starting execution ``` ## Post-Process Add a term to the post-processing function: $$ \begin{split} &\mathbb{E} \left[\max\left(e^{b \cdot z}, Ke^{-b'}\right) \right] e^{b'} - K \\ = &\mathbb{E} \left[\max\left(e^{-a\hat{x}}, Ke^{-b'-ax_{max}}\right) \right]e^{b'+ ax_{max}} - K \end{split} $$ ```python theme={null} import sympy payoff_expression = f"sqrt(max([{S0[0]} * exp({STEP_X / SCALING_FACTOR * (2 ** (MAX_NUM_QUBITS - MAX_FRAC_PLACES))} * x + ({MU[0]+MIN_X*CHOLESKY[0].sum()})), {K}]))" payoff_func = sympy.lambdify(sympy.symbols("x"), payoff_expression) payoff_max = payoff_func(1 - 1 / (2**MAX_NUM_QUBITS)) def parse_result_direct(iqae_res): option_value = iqae_res.estimation * (payoff_max**2) - K confidence_interval = np.array(iqae_res.confidence_interval) * (payoff_max**2) - K return (option_value, confidence_interval) ``` ## Run Method ```python theme={null} parsed_result, conf_interval = parse_result_direct(result) print( f"raw iqae results: {result.estimation} with confidence interval {result.confidence_interval}" ) print( f"option estimated value: {parsed_result} with confidence interval {conf_interval}" ) ``` **Output:** ``` raw iqae results: 0.08112252544735371 with confidence interval [0.07895153133981032, 0.0832935195548971] option estimated value: 26.882608544360238 with confidence interval [21.07841467 32.68680242] ``` # ## Assertions ```python theme={null} expected_payoff = 23.0238 ALPHA_ASSERTION = 1e-5 measured_confidence = conf_interval[1] - conf_interval[0] confidence_scale_by_alpha = np.sqrt( np.log(ALPHA / ALPHA_ASSERTION) ) # based on e^2=(1/2N)*log(2T/alpha) from "Iterative Quantum Amplitude Estimation" since our alpha is low, we want to check within a bigger confidence interval assert ( np.abs(parsed_result - expected_payoff) <= 0.5 * measured_confidence * confidence_scale_by_alpha ), f"Payoff result is out of the {ALPHA_ASSERTION*100}% confidence interval: |{parsed_result} - {expected_payoff}| > {0.5*measured_confidence * confidence_scale_by_alpha}" ``` ## References \[1] [Francesca Cibrario et al., Quantum Amplitude Loading for Rainbow Options Pricing. Preprint.](https://arxiv.org/abs/2402.05574v2) # Rainbow Options with Integration Source: https://docs.classiq.io/explore/applications/finance/rainbow_options/rainbow_options_integration_method Open this notebook in GitHub to run it yourself This notebook covers the implementation of the Integration Method for the rainbow option presented in [\[1\]](#qalrop). In finance, a crucial aspect of asset pricing pertains to derivatives. Derivatives are contracts whose value is contingent upon another source, known as the underlying. The pricing of options - a specific derivative instrument - involves determining the fair market value (discounted payoff) of contracts that afford their holders the right, though not the obligation, to buy (call) or sell (put) one or more underlying assets at a predefined strike price by a specified future expiration date (maturity date). This process relies on mathematical models, considering variables such as current asset prices, time to expiration, volatility, and interest rates. ## Data Definitions The problem inputs: * `NUM_QUBITS`: the number of qubits representing an underlying asset * `NUM_ASSETS`: the number of underlying assets * `K`: the strike price * `S0`: the arrays of underlying asset prices * `dt`: the number of days to the maturity date * `COV`: the covariance matrix that correlate to the underlyings * `MU_LOG_RET`: the array containing the mean of the log return of each underlying ```python theme={null} import numpy as np import scipy NUM_QUBITS = 2 NUM_ASSETS = 2 K = 190 S0 = [193.97, 189.12] dt = 250 COV = np.array([[0.000335, 0.000257], [0.000257, 0.000418]]) MU_LOG_RET = np.array([0.00050963, 0.00062552]) ``` ```python theme={null} MU = MU_LOG_RET * dt CHOLESKY = np.linalg.cholesky(COV) * np.sqrt(dt) SCALING_FACTOR = 1 / CHOLESKY[0, 0] ``` ```python theme={null} from classiq import * EPSILON = 0.05 ALPHA = 0.1 ``` ## Gaussian State Preparation Encode the probability distribution of a discrete multivariate random variable $W$ taking values in $\{w_0, .., w_{N-1}\}$ describing the asset prices at the maturity date. The number of discretized values, denoted as $N$, depends on the precision of the state preparation module and is consequently connected to the number of qubits ($n=$`NUM_QUBITS`) according to the formula $N=2^n$: $$ \sum_{i=0}^{N-1} \sqrt{p(w_i)}\left|w_i\right\rangle $$ ```python theme={null} def gaussian_discretization(num_qubits, mu=0, sigma=1, stds_around_mean_to_include=3): lower = mu - stds_around_mean_to_include * sigma upper = mu + stds_around_mean_to_include * sigma num_of_bins = 2**num_qubits sample_points = np.linspace(lower, upper, num_of_bins + 1) def single_gaussian(x: np.ndarray, _mu: float, _sigma: float) -> np.ndarray: cdf = scipy.stats.norm.cdf(x, loc=_mu, scale=_sigma) return cdf[1:] - cdf[0:-1] non_normalized_pmf = (single_gaussian(sample_points, mu, sigma),) real_probs = non_normalized_pmf / np.sum(non_normalized_pmf) return sample_points[:-1], real_probs[0].tolist() grid_points, probabilities = gaussian_discretization(NUM_QUBITS) STEP_X = grid_points[1] - grid_points[0] MIN_X = grid_points[0] ``` # ## Sanity Check To avoid meaningless results, the process must stop if the strike price $K$ is greater than the maximum value reacheable by the assets during the simulation. In this case, the payoff is $0$, so there is no need to simulate: ```python theme={null} from IPython.display import Markdown if K >= max(S0 * np.exp(np.dot(CHOLESKY, [grid_points[-1]] * 2) + MU)): display( Markdown( " K always greater than the maximum asset values. Stop the run, the payoff is 0" ) ) ``` ## Maximum Computation # ## Precision Utils ```python theme={null} FRAC_PLACES = 2 def round_factor(a): precision_factor = 2**FRAC_PLACES return round(a * precision_factor) / precision_factor def floor_factor(a): precision_factor = 2**FRAC_PLACES return np.floor(a * precision_factor) / precision_factor ``` # ## Affine and Maximum Arithmetic Definitions ```python theme={null} from functools import reduce from classiq.qmod.symbolic import max as qmax a = STEP_X / SCALING_FACTOR b = np.log(S0[0]) + MU[0] + MIN_X * CHOLESKY[0].sum() def get_affine_formula(assets, i): return reduce( lambda x, y: x + y, [ assets[j] * round_factor(SCALING_FACTOR * CHOLESKY[i, j]) for j in range(NUM_ASSETS) if CHOLESKY[i, j] ], ) c = ( SCALING_FACTOR * ( np.log(S0[1]) + MU[1] - (np.log(S0[0]) + MU[0]) + MIN_X * sum(CHOLESKY[1] - CHOLESKY[0]) ) / (STEP_X) ) c = round_factor(c) def calculate_max_reg_type(): x1 = QNum(size=NUM_QUBITS) x2 = QNum(size=NUM_QUBITS) expr = qmax(get_affine_formula([x1, x2], 0), get_affine_formula([x1, x2], 1) + c) size_in_bits, sign, fraction_digits = get_expression_numeric_attributes( [x1, x2], expr ) return size_in_bits, fraction_digits MAX_NUM_QUBITS = calculate_max_reg_type()[0] MAX_FRAC_PLACES = calculate_max_reg_type()[1] ``` ```python theme={null} @qperm def affine_max(x1: Const[QNum], x2: Const[QNum], res: Output[QNum]): res |= qmax(get_affine_formula([x1, x2], 0), get_affine_formula([x1, x2], 1) + c) ``` ## Integration Method The comparator collects the probabilities $g(r)$ of $|r\rangle$ state until $|r\rangle$ register is lower than $|x\rangle$: $$ \begin{split} &\sum_{r=0}^{2^R-1}{\sqrt{g(r)}}|x\rangle|r\rangle|r\leq x\rangle \\ = &|x\rangle \otimes \left[ \sum_{r=0}^{x}{\sqrt{g(r)}} |r\rangle |1\rangle + \sum_{r=x}^{2^R-1}{\sqrt{g(r)}} |r\rangle |0\rangle \right] \end{split} $$ Collecting the probability to have $r\leq x$, define the function: $$ \tilde{h}(x)=\sum_{r=0}^{x}g(r) $$ Evaluating the probability to get a $|1\rangle$ results in $\sum_{x = 0}^{2^R-1}{\tilde{h}(x)}$. To obtain a given function $\tilde{h}$, choose a proper function $g(r)$. The $g(r)$ for $r=0$ value must therefore be: $g(0) = \tilde\{h\}(0)$ and for all the other $r$: $$ g(r) = \tilde{h}(r)-\tilde{h}(r-1) $$ ```python theme={null} @qfunc def integrator(x: Const[QNum], ref: QNum, res: QBit) -> None: exp_rate = (1 / (2**x.fraction_digits)) * a prepare_exponential_state(-exp_rate, ref) res ^= x >= ref ``` ```python theme={null} from classiq.qmod.symbolic import asin, exp, sqrt def get_strike_price_theta_integration(x: QNum): exp_rate = (1 / (2**x.fraction_digits)) * a B = (exp((2**x.size) * exp_rate) - 1) / exp(exp_rate) A = 1 / exp(exp_rate) C = S0[0] * exp((MU[0] + MIN_X * CHOLESKY[0].sum())) return 2 * asin(sqrt((K - (C * A)) / (C * B))) # this is not a qfunc, just a utility function def is_geq_strike_price( x: Const[QNum], ) -> None: a = STEP_X / SCALING_FACTOR b = np.log(S0[0]) + MU[0] + MIN_X * CHOLESKY[0].sum() COMP_VALUE = (np.log(K) - b) / a return x > floor_factor(COMP_VALUE) @qfunc def integration_payoff(max_reg: Const[QNum], integrator_reg: QNum, ind_reg: QBit): control( is_geq_strike_price(max_reg), lambda: integrator(max_reg, integrator_reg, ind_reg), lambda: RY(get_strike_price_theta_integration(max_reg), ind_reg), ) ``` ```python theme={null} class EstimationVars(QStruct): x1: QNum[NUM_QUBITS] x2: QNum[NUM_QUBITS] integrator: QNum[MAX_NUM_QUBITS, False, MAX_FRAC_PLACES] @qfunc def rainbow_integration(qvars: EstimationVars, ind: QBit) -> None: inplace_prepare_state(probabilities, 0, qvars.x1) inplace_prepare_state(probabilities, 0, qvars.x2) max_out = QNum() affine_max(qvars.x1, qvars.x2, max_out) integration_payoff(max_out, qvars.integrator, ind) @qfunc def main(qvars: Output[EstimationVars], ind: Output[QBit]) -> None: allocate(qvars) allocate(ind) rainbow_integration(qvars, ind) MAX_WIDTH = 25 print("Starting synthesis") qprog_1 = synthesize(main, constraints=Constraints(max_width=MAX_WIDTH)) show(qprog_1) ``` **Output:** ``` Starting synthesis Quantum program link: https://platform.classiq.io/circuit/3560ShwR9pHLDmG2n4tA0ra7QDu ``` ## Iterative Quantum Amplitude Estimation (IQAE) Algorithm ```python theme={null} from classiq.applications.iqae.iqae import IQAE iqae = IQAE( state_prep_op=rainbow_integration, problem_vars_size=NUM_QUBITS * NUM_ASSETS + MAX_NUM_QUBITS, constraints=Constraints(max_width=MAX_WIDTH), preferences=Preferences(optimization_level=1), ) ``` ```python theme={null} print("Starting synthesis") qprog_2 = iqae.get_qprog() show(qprog_2) print("Starting execution") result = iqae.run(EPSILON, ALPHA) print("raw iqae results:", result.estimation, result.confidence_interval) ``` **Output:** ``` Starting synthesis Quantum program link: https://platform.classiq.io/circuit/3560YutokLfva7nGAyKraA8CECv Starting execution raw iqae results: 0.05270338444908511 [0.04886018991028241, 0.05654657898788781] ``` ## Post-Process Add a term to the post-processing function: $$ \begin{split} \mathbb{E} \left[\max\left(\frac{e^{a(x+1)} - 1}{e^{a(x_{max} +1)}-1}c + \frac{1}{e^a} , Ke^{-b'}\right)\right] e^{b'} - K \\ =\mathbb{E} \left[\max\left(\frac{e^{a(x+1)} - 1}{e^{a(x_{max} +1)}-1}, \frac{Ke^{-b'}}{c} - \frac{e^{-a}}{c}\right)\right]ce^{b'} + e^{b'}e^{-a} - K \end{split} $$ ```python theme={null} exp_rate = (1 / (2**MAX_FRAC_PLACES)) * a B = (np.exp((2**MAX_NUM_QUBITS) * exp_rate) - 1) / np.exp(exp_rate) A = 1 / np.exp(exp_rate) C = S0[0] * np.exp((MU[0] + MIN_X * CHOLESKY[0].sum())) def parse_result_integration(result): option_value = (result.estimation * (C * B)) + (C * A) - K confidence_interval = (np.array(result.confidence_interval) * (C * B)) + (C * A) - K return (option_value, confidence_interval) ``` ## Run Method ```python theme={null} parsed_result, conf_interval = parse_result_integration(result) print( f"raw iqae results: {result.estimation} with confidence interval {result.confidence_interval}" ) print( f"option estimated value: {parsed_result} with confidence interval {conf_interval}" ) ``` **Output:** ``` raw iqae results: 0.05270338444908511 with confidence interval [0.04886018991028241, 0.05654657898788781] option estimated value: 29.49446224830868 with confidence interval [19.5384534 39.4504711] ``` # ## Assertions ```python theme={null} expected_payoff = 23.0238 ALPHA_ASSERTION = 1e-5 measured_confidence = conf_interval[1] - conf_interval[0] confidence_scale_by_alpha = np.sqrt( np.log(ALPHA / ALPHA_ASSERTION) ) # based on e^2=(1/2N)*log(2T/alpha) from "Iterative Quantum Amplitude Estimation" since our alpha is low, we want to check within a bigger confidence interval assert ( np.abs(parsed_result - expected_payoff) <= 0.5 * measured_confidence * confidence_scale_by_alpha ), f"Payoff result is out of the {ALPHA_ASSERTION*100}% confidence interval: |{parsed_result} - {expected_payoff}| > {0.5*measured_confidence * confidence_scale_by_alpha}" ``` ## References \[1] [Francesca Cibrario et al., Quantum Amplitude Loading for Rainbow Options Pricing. Preprint.](https://arxiv.org/abs/2402.05574v2) # Value at Risk Source: https://docs.classiq.io/explore/applications/finance/value_at_risk/value_at_risk Open this notebook in GitHub to run it yourself ## Introduction Value at Risk (VaR) is a widely used financial risk metric that estimates the maximum expected loss of a portfolio over a given time horizon at a specified confidence level, based on the probability distribution of returns. Classical VaR calculations for complex portfolios often rely on Monte Carlo simulations, which can be computationally expensive and slow to converge. Quantum computing can improve this process by using Iterative Quantum Amplitude Estimation (IQAE) to estimate loss probabilities more efficiently. IQAE provides a quadratic speedup over classical Monte Carlo methods by estimating expected values with fewer samples, without requiring deep quantum circuits. This makes it particularly suitable for near-term quantum hardware and enables faster, more accurate VaR calculations for high-dimensional and non-linear financial portfolios. # ## Modeling the Value at Risk Problem As a first step, we have to model the problem mathematically. We will use a simple yet powerful model, which captures the essence of portfolio optimization: First, we will import the Python libraries required for this implementation. Classiq, of course, and Numpy. ```python theme={null} import matplotlib.pyplot as plt import numpy as np import scipy from classiq import * from classiq.applications.iqae.iqae import IQAE ``` Now, we will define the parameters required for the Value at Risk process, and create the probability distribution function. ```python theme={null} # How many qubits we want the quantum circuit to be num_qubits = 7 # Mu (μ) represents the Average mu = 0.7 # Sigma (σ) represents the Standard Deviation sigma = 0.13 # The Alpha (α) parameter represents the probability in which P(X > v) = 1 - α ALPHA = 0.07 # Find Alpha in a given precision TOLERANCE = ALPHA / 10 def get_log_normal_probabilities(mu_normal, sigma_normal, num_points): log_normal_mean = np.exp(mu + sigma**2 / 2) log_normal_variance = (np.exp(sigma**2) - 1) * np.exp(2 * mu + sigma**2) log_normal_stddev = np.sqrt(log_normal_variance) # cutting the distribution 3 sigmas from the mean low = np.maximum(0, log_normal_mean - 3 * log_normal_stddev) high = log_normal_mean + 3 * log_normal_stddev print(log_normal_mean, log_normal_variance, log_normal_stddev, low, high) x = np.linspace(low, high, num_points) return x, scipy.stats.lognorm.pdf(x, s=sigma_normal, scale=np.exp(mu_normal)) # %% grid_points, probs = get_log_normal_probabilities(mu, sigma, 2**num_qubits) ``` **Output:** ``` 2.030841014265948 0.07029323208790372 0.26512870853210846 1.2354548886696226 2.8262271398622736 ``` In order to have a benchmark to our solution, and since the problem space is not too big, let's calculate the Value at Risk classically, and plot the probability distribution function. ```python theme={null} probs = (probs / np.sum(probs)).tolist() fig, ax1 = plt.subplots() # Plotting the log-normal probability function ax1.plot(grid_points, probs, "go-", label="Probability") # Green line with circles ax1.tick_params(axis="y", labelcolor="g") ax1.set_xlabel("Asset Value") ax1.set_ylabel("Probability", color="g") # Adding grid and title ax1.grid(True) plt.title("Probability and Payoff vs. Asset Value") VAR = 0 # Find the value at risk ALPHA of grid_points accumulated_value = 0 for index in range(len(probs)): accumulated_value += probs[index] if accumulated_value > ALPHA: VAR = grid_points[index] break print(f"Value at risk at {int(ALPHA*100)}%: {VAR}") # Plot the vertical line of VaR at 5% ax1.axvline(x=VAR, color="r", linestyle="--", label="VaR at 5%") ``` **Output:** ``` Value at risk at 7%: 1.6613309244219858 ``` **Output:** ``` ``` output # ## The Binary Search Approach to Calculate Value at Risk We can solve the Value at Risk use-case also with a binary search approach. This approach will benefit us from a complexity perspective once we will integrate the quantum function that calculates alpha given an index. ```python theme={null} # This function calculates the alpha classically given the index and the list of probabilities def calc_alpha(index: int, probs: list[float]): sum_probs = sum([probs[i] for i in range(index)]) return sum_probs ``` ```python theme={null} # This function updates the new index based on the comparison between the measured alpha value and the required value. # The search size correlates with the current binary search step. def update_index(index: int, required_alpha: float, alpha_v: float, search_size: int): if alpha_v < required_alpha: return index + search_size return index - search_size ``` ```python theme={null} # This is the main Value at Risk function, which gets the required probability (required_alpha), the index and the alpha calculation function # We aim to use calc_alpha defined above def print_status(v, alpha_v, search_size, index): print(f"v: {v}, alpha_v: {alpha_v}") print(f"{search_size=}") print(f"{index=}") print("------------------------") def print_results(grid_points, index, probs): print(f"Value at risk at {ALPHA*100}%: {grid_points[index]})") global VAR print(f"Real VaR", VAR) return index def value_at_risk(required_alpha, index, calc_alpha_func=calc_alpha): v = probs[index] alpha_v = calc_alpha_func(index, probs) search_size = index // 2 print_status(v, alpha_v, search_size, index) # Tolerance represents the accuracy of the alpha we aim to get while (not np.isclose(alpha_v, required_alpha, atol=TOLERANCE)) and search_size > 0: index = update_index(index, required_alpha, alpha_v, search_size) # Binary search, divided by 2 - as we know the function is always growing in that part of the graph. search_size = search_size // 2 v = grid_points[index] alpha_v = calc_alpha_func(index, probs) print_status(v, alpha_v, search_size, index) print_results(grid_points, index, probs) ``` ```python theme={null} def get_initial_index(): return int(2**num_qubits) // 4 index = get_initial_index() var_index = value_at_risk(ALPHA, index) ``` **Output:** ``` v: 0.0065948768440761245, alpha_v: 0.05208420102593402 search_size=16 index=32 ------------------------ v: 1.8366916450259, alpha_v: 0.23233572478018774 search_size=8 index=48 ------------------------ v: 1.7364855189665205, alpha_v: 0.12198155535389908 search_size=4 index=40 ------------------------ v: 1.6863824559368308, alpha_v: 0.0820204268594856 search_size=2 index=36 ------------------------ v: 1.6613309244219858, alpha_v: 0.06585414330510068 search_size=1 index=34 ----------------------- - Value at risk at 7.000000000000001%: 1.6613309244219858) Real VaR 1.6613309244219858 ``` ## Value at Risk Using Iterative Quantum Amplitude Estimation (IQAE) Iterative Quantum Amplitude Estimation (IQAE) is a key algorithm in the quantum computing toolbox. It is useful for quantum-enhanced Monte Carlo methods, which are in use for several important quantum finance applications like Value at Risk (VaR) estimation. The goal of Quantum Amplitude Estimation (QAE) is to estimate the probability (or amplitude) of a certain outcome from a quantum circuit. Compared to other classical methods, some has been presented above in this notebook - it is potentially quadratically faster. IQAE is a NISQ-friendly version of the traditional Quantum Amplitude Estimation (QAE), and includes the following stages: * State preparation encoding the desired probabilty, in this case - for not losing more than \$X for an asset or a portfolio of asset. * Use controlled applications of the Grover operator (unlike QAE, which utilises Fourier Transform) * Measure and update the estimate using classical methods. # ## Defining the State Preparation for the IQAE Algorithm In order to use IQAE, we need to define the state preparation function, which includes two main parts: * Loading the distribution of the asset values into a quantum state. * Defining the payoff function, which marks the states where the asset value is below a certain threshold (the index in this case). For the first part, we will use Classiq's inpalce\_prepare\_state function. For the second part, we will define a simple comparison operation using Classiq's arithmetic library. ```python theme={null} @qfunc(synthesize_separately=True) def state_preparation(asset: QArray[QBit], ind: QBit): load_distribution(asset=asset) payoff(asset=asset, ind=ind) @qfunc def load_distribution(asset: QNum): inplace_prepare_state(probs, bound=0, target=asset) @qperm def payoff(asset: Const[QNum], ind: QBit): ind ^= asset < GLOBAL_INDEX ``` ```python theme={null} written_qmod = False def calc_alpha_quantum(index: int, probs: list[float]): # Global variable global GLOBAL_INDEX GLOBAL_INDEX = index # Creation of the model, given the constratins and the circuit preferences iqae = IQAE( state_prep_op=state_preparation, problem_vars_size=num_qubits, constraints=Constraints(max_width=28), preferences=Preferences(machine_precision=num_qubits), ) qprog = iqae.get_qprog() global written_qmod qmod = iqae.get_model() if not written_qmod: written_qmod = True show(qprog) iqae_res = iqae.run(epsilon=0.05, alpha=0.01) # Result of the iterative QAE # iqae_res = res[0].value measured_payoff = iqae_res.estimation confidence_interval = np.array( [interval for interval in iqae_res.confidence_interval] ) print("Measured Payoff:", measured_payoff) print("Confidence Interval:", confidence_interval) return measured_payoff ``` ```python theme={null} index = get_initial_index() ``` ```python theme={null} var = value_at_risk(ALPHA, index, calc_alpha_quantum) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/36qFOY37WAMOkTlCO3z2qIYymEp Measured Payoff: 0.0490523131159807 Confidence Interval: [0.04472615 0.05337848] v: 0.0065948768440761245, alpha_v: 0.0490523131159807 search_size=16 index=32 ----------------------- - Measured Payoff: 0.22216796875 Confidence Interval: [0.18264898 0.26168696] v: 1.8366916450259, alpha_v: 0.22216796875 search_size=8 index=48 ----------------------- - Measured Payoff: 0.12152326550014109 Confidence Interval: [0.11917274 0.12387379] v: 1.7364855189665205, alpha_v: 0.12152326550014109 search_size=4 index=40 ----------------------- - Measured Payoff: 0.08329832447556523 Confidence Interval: [0.07589168 0.09070497] v: 1.6863824559368308, alpha_v: 0.08329832447556523 search_size=2 index=36 ----------------------- - Measured Payoff: 0.06836725638142506 Confidence Interval: [0.06171807 0.07501644] v: 1.6613309244219858, alpha_v: 0.06836725638142506 search_size=1 index=34 ----------------------- - Value at risk at 7.000000000000001%: 1.6613309244219858) Real VaR 1.6613309244219858 ``` # Quantum Hadamard Edge Detection for Image Processing Source: https://docs.classiq.io/explore/applications/image_processing/quantum_hadamard_edge_detection/quantum_image_edge_detection Open this notebook in GitHub to run it yourself Based on the paper: "Edge Detection Quantumized: A Novel Quantum Algorithm for Image Processing" [https://arxiv.org/html/2404.06889v1 ](https://arxiv.org/html/2404.06889v1) This notebook demonstrates: 1. **QPIE (Quantum Probability Image Encoding)** encoding 2. **QHED (Quantum Hadamard Edge Detection)** algorithm The encoding was implemented based on this paper: [https://arxiv.org/pdf/1801.01465](https://arxiv.org/pdf/1801.01465) ```python theme={null} import math from typing import List import matplotlib.pyplot as plt import numpy as np from classiq import * ``` ```python theme={null} # original photo = [] photo = plt.imread("Marina Bay Sands 128.png") plt.imshow(photo) ``` **Output:** ``` ``` output ```python theme={null} segments = [i / 255 for i in [0, 30, 60, 90, 120, 150, 180, 210, 255]] image = np.array(photo) # print(segments) # print(image) for i in range(1, len(image)): for j in range(len(image[i])): pixel = image[i][j][0] for s in segments: if pixel >= s: image[i][j][0] = s image[i][j][1] = s image[i][j][2] = s # print("AFTER THE UPDATE") # print(image) plt.imshow(image) image = np.array(photo) ``` output ## QPIE Encoding Implementation Convert an image into valid QPIE probability amplitudes. The image is converted to grayscale if needed, made non-negative, and L2-normalized so the sum of squared values equals 1. The result is stored as an $n \times n$ array `IMAGE_DATA`. ```python theme={null} def image_to_qpie_amplitudes(image: np.ndarray) -> np.ndarray: """ Convert an image to QPIE probability amplitudes as an n×n array. |img⟩ = Σ(x,y) (I_xy / √(Σ I_xy²)) |xy⟩ """ if len(image.shape) == 3: image = np.mean(image, axis=2) n = image.shape[0] assert image.shape == (n, n), "Image must be square" image = np.abs(image) norm = np.sqrt(np.sum(image**2)) if norm == 0: return np.ones((n, n)) / n return (image / norm).T ``` ```python theme={null} IMAGE_DATA = image_to_qpie_amplitudes(image) n = IMAGE_DATA.shape[0] N_PIXEL_QUBITS = math.ceil(math.log2(n)) print(f"Image: {n}x{n}, pixel qubits per axis: {N_PIXEL_QUBITS}") ``` **Output:** ``` Image: 128x128, pixel qubits per axis: 7 ``` ```python theme={null} print(f"Total pixels: {n*n}") ``` **Output:** ``` Total pixels: 16384 ``` ## Modified QHED Algorithm We define an `ImagePixel` QStruct with separate `x` and `y` registers, and load the image via `lookup_table` - the amplitudes are computed classically from the pixel coordinates and loaded with `prepare_amplitudes`. The QHED algorithm detects edges by: 1. Adding auxiliary qubits in $|+\rangle$ state 2. Controlled shifts of $x$ (horizontal) and $y$ (vertical) by $-1$ 3. Applying Hadamard to compute differences 4. Measuring to get edge information ```python theme={null} from classiq.qmod.symbolic import logical_or class ImagePixel(QStruct): x: QNum[N_PIXEL_QUBITS] y: QNum[N_PIXEL_QUBITS] def image_amplitude(x: float, y: float) -> float: ix, iy = int(x), int(y) if 0 <= ix < n and 0 <= iy < n: return float(IMAGE_DATA[ix, iy]) return 0.0 @qfunc def qpie_encoding(pixel: Output[ImagePixel]): amps = lookup_table(image_amplitude, [pixel.x, pixel.y]) prepare_amplitudes(amps, 0, pixel) @qfunc def quantum_edge_detection_scalable( edge_aux: Output[QBit], pixel: Output[ImagePixel], ): qpie_encoding(pixel=pixel) horizontal_edge = QBit() vertical_edge = QBit() allocate(horizontal_edge) allocate(vertical_edge) within_apply( within=lambda: H(horizontal_edge), apply=lambda: control(horizontal_edge, lambda: inplace_add(-1, pixel.x)), ) within_apply( within=lambda: H(vertical_edge), apply=lambda: control(vertical_edge, lambda: inplace_add(-1, pixel.y)), ) edge_aux |= logical_or(horizontal_edge, vertical_edge) drop(horizontal_edge) drop(vertical_edge) print(f"Creating {n}x{n} image edge detection model...") ``` **Output:** ``` Creating 128x128 image edge detection model... ``` ## Synthesize and Analyze the Quantum Circuit The model is synthesized with a 20-qubit width limit and a long timeout, and finally exported as quantum\_image\_edge\_detection with 15-digit numeric precision. ```python theme={null} @qfunc def main( pixel: Output[ImagePixel], edge_aux: Output[QBit], ): quantum_edge_detection_scalable(edge_aux=edge_aux, pixel=pixel) ``` ```python theme={null} qprog = synthesize( main, constraints=Constraints(max_width=20), preferences=Preferences(timeout_seconds=14400), ) ``` ```python theme={null} print(f"\n{n}x{n} Image Circuit Statistics:") print(f" - Number of qubits: {qprog.data.width}") print(f" - Circuit depth: {qprog.transpiled_circuit.depth}") print( f" - Number of gates: {qprog.transpiled_circuit.count_ops if hasattr(qprog.transpiled_circuit, 'count_ops') else 'N/A'}" ) ``` **Output:** ``` 128x128 Image Circuit Statistics: - Number of qubits: 17 - Circuit depth: 32813 - Number of gates: {'u': 16617, 'cx': 16584} ``` ```python theme={null} show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3AlGcDNufmf2h6AwpUBoej5fGk8 ``` ```python theme={null} qprog = set_quantum_program_execution_preferences( qprog, preferences=ExecutionPreferences(num_shots=200000) ) res = execute(qprog).result_value() res.dataframe ```
pixel.x pixel.y edge\_aux counts probability bitstring
0 64 92 0 167 0.000835 010111001000000
1 37 89 0 165 0.000825 010110010100101
2 64 91 0 163 0.000815 010110111000000
3 62 91 0 161 0.000805 010110110111110
4 70 95 0 158 0.000790 010111111000110
... ... ... ... ... ... ...
17277 115 127 1 1 0.000005 111111111110011
17278 118 127 1 1 0.000005 111111111110110
17279 119 127 1 1 0.000005 111111111110111
17280 120 127 1 1 0.000005 111111111111000
17281 122 127 1 1 0.000005 111111111111010

17282 rows × 6 columns

## Create Edge Image from Measurement Results If `edge_aux == 1` then it is marked as an edge pixel. The new amplitude is calculated based on the number of shots measured for that pixel, normalized by the total number of shots. ```python theme={null} df = res.dataframe edge_df = df[df["edge_aux"] == 1] edge_image = np.zeros((n, n)) for _, row in edge_df.iterrows(): edge_image[int(row["pixel.x"]), int(row["pixel.y"])] = np.sqrt(row["probability"]) ``` Analyze amplitude distributions ```python theme={null} plt.hist(edge_image.flatten()) ``` **Output:** ``` (array([1.2776e+04, 0.0000e+00, 1.3470e+03, 1.1560e+03, 5.5400e+02, 2.6500e+02, 2.0400e+02, 6.4000e+01, 9.0000e+00, 9.0000e+00]), array([ 0. , 0.00104881, 0.00209762, 0.00314643, 0.00419524, 0.00524404, 0.00629285, 0.00734166, 0.00839047, 0.00943928, 0.01048809]), ) ``` output Some amplitudes are extremely small and can be treated as noise; discarding them yields a cleaner edge image. ```python theme={null} edge_image = np.where(edge_image > 0.003, edge_image, 0) plt.hist(edge_image.flatten()) ``` **Output:** ``` (array([1.4123e+04, 0.0000e+00, 0.0000e+00, 1.1560e+03, 5.5400e+02, 2.6500e+02, 2.0400e+02, 6.4000e+01, 9.0000e+00, 9.0000e+00]), array([ 0. , 0.00104881, 0.00209762, 0.00314643, 0.00419524, 0.00524404, 0.00629285, 0.00734166, 0.00839047, 0.00943928, 0.01048809]), ) ``` output The resulting edge-detected image ```python theme={null} plt.imshow(edge_image.T, cmap="gray") ``` **Output:** ``` ``` output In comparison, the original image is: ```python theme={null} plt.imshow(image) ``` **Output:** ``` ``` output # Overview Source: https://docs.classiq.io/explore/applications/index Real-world applications implemented in Classiq's Qmod, grouped by domain. ## Automotive * [Quantum Simulation-Based Optimization (QuSO) of a Cooling System](/explore/applications/automotive/cooling_systems_optimization/cooling_systems_optimization) ## Benchmarking * [Quantum Volume](/explore/applications/benchmarking/quantum_volume/quantum_volume) * [Randomized Benchmarking](/explore/applications/benchmarking/randomized_benchmarking/randomized_benchmarking) ## CFD * [Chebyshev approximation of the inverse function](/explore/applications/CFD/QLS_for_hybrid_solvers/chebyshev_approximation) * [Quantum linear solver with LCU of Chebyshev polynomials](/explore/applications/CFD/QLS_for_hybrid_solvers/qls_chebyshev_lcu) * [Quantum linear solver based on QSVT](/explore/applications/CFD/QLS_for_hybrid_solvers/qls_qsvt) * [Block Encoding Verification](/explore/applications/CFD/QLS_for_hybrid_solvers/verify_block_encoding) * [Quantum Double Slit Experiment](/explore/applications/CFD/double_slit_experiment/quantum_double_slit_experiment) * [Quantum Algorithm for Solving The 1D Heat Equation](/explore/applications/CFD/heat_eq_qsvt/heat_eq_qsvt) * [Quantum Lattice Boltzmann Method](/explore/applications/CFD/qlbm/qlbm) ## Chemistry * [Classiq Chemistry Application](/explore/applications/chemistry/classiq_chemistry_application/classiq_chemistry_application) * [Creating a Molecule's Potential Energy Curve](/explore/applications/chemistry/molecular_energy_curve/molecular_energy_curve) * [Molecule Eigensolver (VQE Method)](/explore/applications/chemistry/molecule_eigensolver/molecule_eigensolver) * [Projected Based Embedding Tutorial](/explore/applications/chemistry/projection_based_embedding/projected_based_embedding_tutorial) * [Protein Folding Algorithm](/explore/applications/chemistry/protein_folding/protein_folding_with_qaoa/protein_folding) * [QFold: Quantum Walks and Deep Learning to Solve Protein Folding](/explore/applications/chemistry/protein_folding/protein_folding_with_quantum_walk/qfold) * [Quantum Phase Estimation (QPE) for Solving Molecular Energies](/explore/applications/chemistry/qpe_for_molecules/qpe_for_molecules) * [Quantum Drude Oscillator](/explore/applications/chemistry/quantum_drude_oscillator/quantum_drude_oscillator) * [Continuous-Time Quantum Walk in Photosynthetic Energy Transfer](/explore/applications/chemistry/quantum_walk_fmo/quantum_walk_fmo) * [Second Quantized Hamiltonian](/explore/applications/chemistry/second_quantized_hamiltonian/second_quantized_hamiltonian) * [Even more efficient quantum computations of chemistry through tensor hypercontraction](/explore/applications/chemistry/tensor_hypercontraction/tensor_hypercontraction) ## Cybersecurity * [Vertex Cover Link Monitoring for IoT-Enabled Wireless Sensor Networks](/explore/applications/cybersecurity/link_monitoring/link_monitoring) * [Cybersecurity Vertex Cover Patch Management Challenge for Tackling Kill Chains](/explore/applications/cybersecurity/patching_management/patch_min_vertex_cover) * [Using Quantum Computers to Boost Whitebox Fuzzing](/explore/applications/cybersecurity/whitebox_fuzzing/whitebox_fuzzing) ## Finance * [Prepare Partial Exponential State](/explore/applications/finance/autocallable_options/partial_exponential_state_preparation) * [Autocallables with Integration Amplitude Loading](/explore/applications/finance/autocallable_options/quantum_autocallable_option_pricing) * [Stochastic Modeling of Brownian Motion](/explore/applications/finance/brownian_chebyshev_polynomials/brownian_chebyshev_polynomials) * [Quantum Kernels and Support Vector Machines](/explore/applications/finance/credit_card_fraud/credit_card_fraud) * [Portfolio Optimization with Hybrid HHL Algorithm](/explore/applications/finance/hybrid_hhl_for_portfolio_optimization/portfolio_optimization_with_hhl) * [Estimating European Option Price Using Amplitude Estimation](/explore/applications/finance/option_pricing/option_pricing) * [Portfolio Optimization with the Quantum Approximate Optimization Algorithm (QAOA)](/explore/applications/finance/portfolio_optimization/portfolio_optimization) * [Quantum computational finance: quantum algorithm for portfolio optimization](/explore/applications/finance/portfolio_optimization_hhl/HHL_portfolio) * [Rainbow Options with Brute-force Methodology](/explore/applications/finance/rainbow_options/rainbow_options_bruteforce_method) * [Rainbow Options with Direct Amplitude Loading](/explore/applications/finance/rainbow_options/rainbow_options_direct_method) * [Rainbow Options with Integration](/explore/applications/finance/rainbow_options/rainbow_options_integration_method) * [Value at Risk](/explore/applications/finance/value_at_risk/value_at_risk) ## Image Processing * [Quantum Hadamard Edge Detection for Image Processing](/explore/applications/image_processing/quantum_hadamard_edge_detection/quantum_image_edge_detection) ## Logistics * [Facility Location Problem (P-median)](/explore/applications/logistics/facility_location/facility_location) * [Workflow Scheduling Problem](/explore/applications/logistics/task_scheduling_problem/task_scheduling_problem) * [Travelling Salesman Problem](/explore/applications/logistics/traveling_salesman_problem/traveling_salesman_problem) * [Vehicle Routing Problem (VRP)](/explore/applications/logistics/vehicle_routing_problem/vehicle_routing_problem) ## Optimization * [ADAPT QAOA](/explore/applications/optimization/adapt_qaoa/adapt_qaoa) * [Electric Grid Optimization using QAOA](/explore/applications/optimization/electric_grid_optimization/electric_grid_optimization) * [Integer Linear Programming](/explore/applications/optimization/integer_linear_programming/integer_linear_programming) * [Kidney Exchange QAOA Example](/explore/applications/optimization/kidney_exchange/kidney_exchange_problem) * [Evidence of Scaling Advantage for the QAOA Algorithm on a Classically Intractable Problem](/explore/applications/optimization/low_autocorrelation_binary_sequences_problem/evidence_scaling_labs) * [Max Clique Problem](/explore/applications/optimization/max_clique/max_clique) * [Max Independent Set](/explore/applications/optimization/max_independent_set/max_independent_set) * [Max Colorable Induced Subgraph Problem](/explore/applications/optimization/max_induced_k_color_subgraph/max_induced_k_color_subgraph) * [Max K-Vertex Cover](/explore/applications/optimization/max_k_vertex_cover/max_k_vertex_cover) * [Min Graph Coloring Problem](/explore/applications/optimization/min_graph_coloring/min_graph_coloring) * [Minimum Dominating Set (MDS) Problem](/explore/applications/optimization/minimum_dominating_set/minimum_dominating_set) * [Hybrid Classical-Quantum Simulation of MaxCut using QAOA-in-QAOA](/explore/applications/optimization/qaoa_in_qaoa/qaoa_in_qaoa) * [Solving the Rectangles Packing problem with Classiq](/explore/applications/optimization/rectangles_packing/rectangles_packing_grid) * [Quantum computation for robot posture optimization](/explore/applications/optimization/robust_posture_optimization/robust_posture_optimization) * [Set Cover Problem](/explore/applications/optimization/set_cover/set_cover) * [Number Partition Problem](/explore/applications/optimization/set_partition/set_partition) * [Variational Quantum Imaginary Time Evolution (VarQITE) for Combinatorial Problems](/explore/applications/optimization/variational_quantum_imaginary_time_evolution/variational_quantum_imaginary_time_evolution) ## Physical Systems * [One-Dimensional Fermi-Hubbard Model](/explore/applications/physical_systems/fermi_hubbard_model_1D/fermi_hubbard_1D) * [Solve Differential Equations of the Lanchester Model with HHL](/explore/applications/physical_systems/hhl_lanchester/hhl_lanchester) * [Ising Model](/explore/applications/physical_systems/ising_model/ising_model) * [Simulation of the 2D Maxwell Equation using Quantum Hamiltonian Simulation](/explore/applications/physical_systems/maxwell_equation/maxwell_2d_simulation) * [The Quantum Sawtooth Map](/explore/applications/physical_systems/quantum_chaos/quantum_sawtooth_map) ## Plasma * [Quantum Simulation of Linear Kinetic Plasma Models](/explore/applications/plasma/vlasov_ampere/vlasov_ampere) * [Quantum Simulation of Linear Kinetic Plasma Models (Qiskit)](/explore/applications/plasma/vlasov_ampere/vlasov_ampere_qiskit) ## Telecom * [Network Traffic Optimization with QAOA](/explore/applications/telecom/network_traffic_optimization/network_traffic_optimization) * [Radio Access Network](/explore/applications/telecom/radio_access_network/radio_access_network_positioning_antennas) * [Quantum-Based Resiliency Planning](/explore/applications/telecom/resiliency_planning/resiliency_planning) * [Quantum-Based Resiliency Planning with AMD GPU Simulation](/explore/applications/telecom/resiliency_planning/resiliency_planning_AMD) # Facility Location Problem (P-Median) Source: https://docs.classiq.io/explore/applications/logistics/facility_location/facility_location Open this notebook in GitHub to run it yourself Consider the optimization problem where you have a set of $M$ customers and a set of $N$ potential locations for opening a facility. Given transportation costs between facilities and customers and the number of facilities you would like to open ($P$), determine which facilities to open such that the total transportation cost between facilities and customers is minimal, under the constraint that each customer is allocated to only one facility. Possible extensions: * Add a different demand for each customer. * Add the cost for opening a facility at each location. * Consider different categories of facilities and that customers can have multiple allocations for different facilities. ## Mathematical Modeling The input of the model is a set of $M$ customers $\{1,\dots,M\}$, a set of $N$ potential locations for facilities $\{1,\dots,N\}$, an $N\times M$ matrix $d$ where $d_{nm}$ is the cost of customer $m$ buying from facility $n$, and the total number of facilities we want to open is $P$. Define a binary variable for the optimization problem: an $N\times M$ matrix $x$ such that $x_{nm}=1$ if the customer $m$ is allocated to facility $n$. The objective function to minimize is the total cost function: $$ \min_{x} \sum_{n,m} d_{nm}x_{nm} $$ Constraints: (1) Each customer is supplied: $\forall m\in[0,M] \,\,\, \sum_n x_{nm}=1$. (2) Total number of open facilities is $P$: $\sum_n\Pi_m (1-x_{nm})=N-P$ (the inner product is zero if the $n-$th facility is not open). # ## Alternative Modeling There is an alternative modeling for adding another variable to the model: a binary vector $y$ of size $N$, which indicates which facilities are open. In this formulation the second constraint can be written as $\sum_n y_n=P$ together with an inequality constraint $\forall n,m:\, x_{nm} \leq y_n$. A model that combines equality and inequality constraints may become available in the future. Note that this alternative modeling has a quadratic unconstrained binary optimization (QUBO) problem (compared to the formulation above where constraint (2) is a polynomial of degree $m$). However, the alternative modeling has more variables to minimize on, and thus refers to more qubits. # ## Example If you can open facilities in Japan, USA, and France, and you have four customers whose costs for buying from these three locations are given. To open in total $P=2$ facilities, the optimization problem is to find where to open the facilities and which customer is allocated to which facility. Draw this specific example on a graph. There are $N=3$ locations and $M=4$ customers, where the weights of the edges between them signify the costs: Suggestions: * Give a general problem description and then givens, or provide givens after each mention. * Standardize writing numbers appearing in sentences when less than 10: "4" vs. "four". ```python theme={null} # Import relevant packages from itertools import product import matplotlib.pyplot as plt import networkx as nx # noqa import numpy as np import pandas as pd # Declare givens from problem statement Facilities = ["Japan", "USA", "France"] Customers = ["A", "B", "C", "D"] N = len(Facilities) # potential facility count M = len(Customers) # customer count P = 2 # allocated facility count # costs of customers using facilities d = np.array( [[0.02, 0.14, 0.62, 0.11], [0.99, 0.22, 0.91, 0.09], [0.4, 0.76, 0.95, 0.61]] ) graph = nx.DiGraph() graph.add_nodes_from(Facilities + Customers) for n, m in product(range(N), range(M)): graph.add_edges_from([(Facilities[n], Customers[m])], weight=d[n, m]) # Plot the graph plt.figure(figsize=(10, 6)) left = nx.bipartite.sets(graph)[0] pos = nx.bipartite_layout(graph, left) nx.draw_networkx(graph, pos=pos, nodelist=Customers, font_size=22, font_color="None") nx.draw_networkx_nodes( graph, pos, nodelist=Customers, node_color="#119DA4", node_size=500 ) for fa in Facilities: x, y = pos[fa] plt.text( x, y, s=fa, bbox=dict(facecolor="#F43764", alpha=1), horizontalalignment="center", fontsize=15, ) nx.draw_networkx_edges(graph, pos, width=2) labels = nx.get_edge_attributes(graph, "weight") nx.draw_networkx_edge_labels(graph, pos, edge_labels=labels, font_size=12) nx.draw_networkx_labels( graph, pos, labels={co: co for co in Customers}, font_size=22, font_color="#F4F9E9", ) plt.axis("off") plt.show() ``` output ## Building the Pyomo Model from a Matrix of Distances ```python theme={null} from typing import List, Tuple, cast # noqa import pyomo.environ as pyo from IPython.display import Markdown, display ``` ```python theme={null} ## Define a function that gets a matrix of costs between customers and potential facilities ## and the number of facilities to open. def pmedian(cost_mat: np.ndarray, P: int) -> pyo.ConcreteModel: model = pyo.ConcreteModel("pmedian") N = cost_mat.shape[0] # potential facility amount M = cost_mat.shape[1] # customer count Locations = range(N) Customers = range(M) model.x = pyo.Var(Locations, Customers, domain=pyo.Binary) @model.Constraint(Customers) def each_customer_is_supplied_rule(model, m): # constraint (1) return sum(model.x[n, m] for n in Locations) == 1 def is_location_alocated(n): # constraint (2) return np.prod([(1 - model.x[n, m]) for m in Customers]) model.num_facilities = pyo.Constraint( expr=sum(is_location_alocated(n) for n in Locations) == N - P ) model.cost = pyo.Objective( expr=sum(cost_mat[n, m] * model.x[n, m] for n in Locations for m in Customers), sense=pyo.minimize, ) return model ``` # ## Solving with Classiq Take the specific example outlined above: ```python theme={null} pmedian_model = pmedian(d, P) ``` To solve the Pyomo model, use the `CombinatorialProblem` Python class. Under the hood it translates the Pyomo model to a quantum model of the QAOA algorithm \[[1](#qaoa)], with the cost Hamiltonian translated from the Pyomo model. Choose the number of layers for the QAOA ansatz using the `num_layers` argument: ```python theme={null} from classiq import * from classiq.applications.combinatorial_optimization import CombinatorialProblem combi = CombinatorialProblem(pyo_model=pmedian_model, num_layers=5, penalty_factor=10) qmod = combi.get_model() ``` # ## Synthesizing the QAOA Circuit and Solving the Problem Synthesize and view the QAOA circuit (ansatz) used to solve the optimization problem: ```python theme={null} qprog = combi.get_qprog() show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/2zJEZtDZDBqSpw793eHKnq7THbP ``` Now, solve the problem by calling the `optimize` method of the `CombinatorialProblem` object. For the classical optimization part of the QAOA algorithm, define the maximum number of classical iterations (`maxiter`) and the $\alpha$-parameter (`quantile`) for running CVaR-QAOA, an improved variation of the QAOA algorithm \[[2](#cvar)]: ```python theme={null} optimized_params = combi.optimize(maxiter=50) ``` **Output:** ``` Optimization Progress: 51it [03:35, 4.23s/it] ``` Check the convergence of the run: ```python theme={null} import matplotlib.pyplot as plt plt.plot(combi.cost_trace) plt.xlabel("Iterations") plt.ylabel("Cost") plt.title("Cost convergence") ``` **Output:** ``` Text(0.5, 1.0, 'Cost convergence') ``` output ## Optimization Results Examine the statistics of the algorithm. To get samples with the optimized parameters, call the `sample` method: ```python theme={null} optimization_result = combi.sample(optimized_params) optimization_result.sort_values(by="cost").head(5) ``` | | solution | probability | cost | | ---- | ------------------------------------------------------ | ----------- | ---- | | 1035 | \{'x': \[\[1, 1, 1, 0], \[0, 0, 0, 1], \[0, 0, 0, 0]]} | 0.000488 | 0.87 | | 756 | \{'x': \[\[1, 0, 1, 0], \[0, 1, 0, 1], \[0, 0, 0, 0]]} | 0.000488 | 0.95 | | 230 | \{'x': \[\[1, 0, 0, 0], \[0, 1, 1, 1], \[0, 0, 0, 0]]} | 0.000977 | 1.24 | | 766 | \{'x': \[\[0, 1, 1, 1], \[0, 0, 0, 0], \[1, 0, 0, 0]]} | 0.000488 | 1.27 | | 329 | \{'x': \[\[1, 1, 1, 0], \[0, 0, 0, 0], \[0, 0, 0, 1]]} | 0.000977 | 1.39 | Compare the optimized results to uniformly sampled results: ```python theme={null} uniform_result = combi.sample_uniform() ``` And compare the histograms: ```python theme={null} optimization_result["cost"].plot( kind="hist", bins=50, edgecolor="black", weights=optimization_result["probability"], alpha=0.6, label="optimized", ) uniform_result["cost"].plot( kind="hist", bins=50, edgecolor="black", weights=uniform_result["probability"], alpha=0.6, label="uniform", ) plt.legend() plt.ylabel("Probability", fontsize=16) plt.xlabel("cost", fontsize=16) plt.tick_params(axis="both", labelsize=14) ``` output Plot the solution: ```python theme={null} best_solution = optimization_result.solution[optimization_result.cost.idxmin()] best_solution ``` **Output:** ``` {'x': [[1, 1, 1, 0], [0, 0, 0, 1], [0, 0, 0, 0]]} ``` Define a function that plots solutions: ```python theme={null} # This function plots the solution in a table and a graph def plotting_sol(x_sol, cost, is_classic: bool): x_sol_to_mat = np.reshape(np.array(x_sol), [N, M]) # vector to matrix # opened facilities will be marked in red opened_fac_dict = {} for fa in range(N): if sum(x_sol_to_mat[fa, m] for m in range(M)) > 0: opened_fac_dict.update({Facilities[fa]: "background-color: #F43764"}) # classical or quantum if is_classic == True: display(Markdown("**CLASSICAL SOLUTION**")) print("total cost= ", cost) else: display(Markdown("**QAOA SOLUTION**")) print("total cost= ", cost) # plotting in a table df = pd.DataFrame(x_sol_to_mat) df.columns = Customers df.index = Facilities plotable = df.style.apply(lambda x: x.index.map(opened_fac_dict)) display(plotable) # plotting in a graph graph_sol = nx.DiGraph() graph_sol.add_nodes_from(Facilities + Customers) for n, m in product(range(N), range(M)): if x_sol_to_mat[n, m] > 0: graph_sol.add_edges_from([(Facilities[n], Customers[m])], weight=d[n, m]) plt.figure(figsize=(10, 6)) left = nx.bipartite.sets(graph_sol, top_nodes=Facilities)[0] pos = nx.bipartite_layout(graph_sol, left) nx.draw_networkx( graph_sol, pos=pos, nodelist=Customers, font_size=22, font_color="None" ) nx.draw_networkx_nodes( graph_sol, pos, nodelist=Customers, node_color="#119DA4", node_size=500 ) for fa in Facilities: x, y = pos[fa] if fa in opened_fac_dict.keys(): plt.text( x, y, s=fa, bbox=dict(facecolor="#F43764", alpha=1), horizontalalignment="center", fontsize=15, ) else: plt.text( x, y, s=fa, bbox=dict(facecolor="#F4F9E9", alpha=1), horizontalalignment="center", fontsize=15, ) nx.draw_networkx_edges(graph_sol, pos, width=2) labels = nx.get_edge_attributes(graph_sol, "weight") nx.draw_networkx_edge_labels(graph, pos, edge_labels=labels, font_size=12) nx.draw_networkx_labels( graph_sol, pos, labels={co: co for co in Customers}, font_size=22, font_color="#F4F9E9", ) plt.axis("off") plt.show() ``` # ## Best Solution Plot the quantum result only if you get the right solution (to avoid problems with printing the table and graph): ```python theme={null} best_solution ``` **Output:** ``` solution {'x': [[1, 1, 1, 0], [0, 0, 0, 1], [0, 0, 0, 0]]} probability 0.000488 cost 0.87 Name: 1035, dtype: object ``` ```python theme={null} best_solution = optimization_result.loc[optimization_result.cost.idxmin()] plotting_sol(best_solution.solution["x"], best_solution.cost, is_classic=False) ``` **Output:** ``` ``` **Output:** ``` total cost= 0.87 ```
A B C D
Japan 1 1 1 0
USA 0 0 0 1
France 0 0 0 0
output # ## Compare to a Classical Solver ```python theme={null} from pyomo.opt import SolverFactory solver = SolverFactory("couenne") solver.solve(pmedian_model) pmedian_model.display() ``` **Output:** ``` Model pmedian Variables: x : Size=12, Index=x_index Key : Lower : Value : Upper : Fixed : Stale : Domain (0, 0) : 0 : 1.0 : 1 : False : False : Binary (0, 1) : 0 : 1.0 : 1 : False : False : Binary (0, 2) : 0 : 1.0 : 1 : False : False : Binary (0, 3) : 0 : 0.0 : 1 : False : False : Binary (1, 0) : 0 : 0.0 : 1 : False : False : Binary (1, 1) : 0 : 6.846989283059948e-10 : 1 : False : False : Binary (1, 2) : 0 : 0.0 : 1 : False : False : Binary (1, 3) : 0 : 1.0 : 1 : False : False : Binary (2, 0) : 0 : 0.0 : 1 : False : False : Binary (2, 1) : 0 : -6.846989283059948e-10 : 1 : False : False : Binary (2, 2) : 0 : 0.0 : 1 : False : False : Binary (2, 3) : 0 : 0.0 : 1 : False : False : Binary Objectives: cost : Size=1, Index=None, Active=True Key : Active : Value None : True : 0.8699999996302625 Constraints: each_customer_is_supplied_rule : Size=4 Key : Lower : Body : Upper 0 : 1.0 : 1.0 : 1.0 1 : 1.0 : 1.0 : 1.0 2 : 1.0 : 1.0 : 1.0 3 : 1.0 : 1.0 : 1.0 num_facilities : Size=1 Key : Lower : Body : Upper None : 1.0 : 1.000000000684699 : 1.0 ``` ```python theme={null} best_classical_solution = np.array( [pyo.value(pmedian_model.x[idx]) for idx in np.ndindex(d.shape)] ).reshape(d.shape) plotting_sol(best_classical_solution, pyo.value(pmedian_model.cost), is_classic=True) ``` **Output:** ``` ``` **Output:** ``` total cost= 0.8699999996302625 ```
A B C D
Japan 1.000000 1.000000 1.000000 0.000000
USA 0.000000 0.000000 0.000000 1.000000
France 0.000000 -0.000000 0.000000 0.000000
output # Workflow Scheduling Problem Source: https://docs.classiq.io/explore/applications/logistics/task_scheduling_problem/task_scheduling_problem Open this notebook in GitHub to run it yourself Consider an optimization problem (based on the formulation in \[[1](#taskworkflow)]): there are $W(t)$ available workers or resources and $N$ jobs, where each job $j$ requires $T(j)$ time and takes $r(j)$ resources to be completed. Given a set of dependencies for each job and that you know which job depends on the completion of others before starting, determine the order of job completion that minimizes the total execution time. Assumptions: * A job may occupy only a single timeslot at most: $\forall j \,\,\, T(j) = 1$ * Given sufficient resources, jobs can start in the same timeslot * All resources are identical and the amount of available resources at a current time step is given by the number $W(t)$ * Jobs can only start if all parent jobs are complete Possible extensions: * Separate resources into multiple categories with jobs requiring different types * Jobs that take more than one operation ## Mathematical Modeling The input of the model is as follows: Define a binary variable for the optimization problem: an $t_{max}\times N$ matrix $x$ such that $$ \begin{aligned} x_{tj} = \begin{cases} 1 & \text{job } j \text{ is done on the } t_\text{th} \text{ time slot} \\ 0 & \text{else} \end{cases}\\ \end{aligned} $$ Constraints: * All jobs must complete exactly once: $\forall j\in[1,N] \,\,\, \sum_t x_{tj}=1$ * All jobs may use no more than the available resources: $\forall t\in[0,t_{max}] \,\,\, \sum_j x_{tj} r_j \leq W_t$ * Parent jobs must be complete before dependent jobs start: $x_{t_1j_1} x_{t_2j_2} = 0 \,\,\, \forall j_1, t_1\leq t_2, j_2 \text{ depends on } j_1$ The objective function to minimize is the total cost function: $$ \min_{x} \sum_{t} x_{tN}\cdot t $$ which favors schedules that are done early. ## Defining the Optimization Model ```python theme={null} from typing import List, Tuple, cast # noqa import matplotlib.pyplot as plt import networkx as nx import numpy as np # noqa import pandas as pd import pyomo.environ as pyo from IPython.display import Markdown, display ``` ```python theme={null} def define_workflow_problem( G, num_timeslots, available_capacities, work_loads ) -> pyo.ConcreteModel: model = pyo.ConcreteModel("task_scheduling") timeslots = range(num_timeslots) assert len(timeslots) == len(available_capacities) works = range(len(G.nodes)) assert len(works) == len(work_loads) last_works = [node for node in G.nodes if G.out_degree(node) == 0] assert len(last_works) == 1 last_work = last_works[0] # works with no dependancies root_works = [node for node in G.nodes if G.in_degree(node) == 0] model.x = pyo.Var(timeslots, works, domain=pyo.Binary) @model.Constraint(works) def all_works_are_done(model, i): # constraint (1) return sum(model.x[t, i] for t in timeslots) == 1 @model.Constraint(timeslots) def capacity_is_valid(model, t): # constraint(2) return ( sum(model.x[t, i] * work_loads[i] for i in works) <= available_capacities[t] ) @model.Constraint(works, works, timeslots, timeslots) def works_done_by_their_order(model, i, j, t1, t2): # constraint (3) if G.has_edge(i, j) and t1 >= t2: return model.x[t1, i] * model.x[t2, j] == 0 return pyo.Constraint.Feasible # eliminate all timeslots that are not possible for the task in order to save qubits distances_from_ends = nx.floyd_warshall_numpy(G) distances_from_roots = nx.floyd_warshall_numpy(G.reverse()) @model.Constraint(works, timeslots) def eliminating_rule(model, i, t): end_distance = int(np.min(distances_from_ends[i, last_works])) start_distance = int(np.min(distances_from_roots[i, root_works])) if (t < start_distance) or (t >= len(timeslots) - end_distance): return model.x[t, i] == 0 return pyo.Constraint.Feasible # minimize the end time of the last work model.cost = pyo.Objective( expr=sum(model.x[t, last_work] * (t + 1) for t in timeslots), sense=pyo.minimize ) return model ``` Visualization helper functions: ```python theme={null} import random def plot_graph(solution=None, ax=None): if solution is not None: # determine how many tasks start in each timeslot num_tasks = [sum(solution[t]) for t in range(num_timeslots)] max_tasks = max(num_tasks) pos = {} # find all the tasks that start in particular start time for start in np.nonzero(solution.sum(axis=1))[0]: locations = solution[start].nonzero()[0] pos.update( { n: (start, i + (max_tasks - num_tasks[start]) / 2) for i, n in enumerate(locations) } ) else: pos = { node: (order, random.random()) for order, node in enumerate((nx.topological_sort(G))) } options = { "font_size": 12, "node_size": 1000, "node_color": "white", "edgecolors": "black", "linewidths": 3, "width": 3, } # G = nx.DiGraph(edges) nx.draw_networkx(G, pos, ax=ax, **options) # Set margins for the axes so that nodes aren't clipped if ax is None: ax = plt.subplot() ax.margins(0.20) if solution is not None: ax.set_title("Suggested sequence") ax.set_xlabel("Timeslot") def plot_assignments(solution, ax=None): if ax is None: ax = plt.subplot() ax.pcolormesh(solution.T, edgecolors="w", linewidth=4, cmap="OrRd") ax.set_aspect(0.8) ax.set_xlabel("Timeslot") ax.set_ylabel("Task Number") ax.set_title("Task assignment") def plot_resource_graph(solution=None, ax=None): if ax is None: fig, ax = plt.subplots() x_pos = np.arange(len(capacities)) if solution is not None: ax.set_title("Utilization") num_resources = [np.dot(solution[t], workloads) for t in range(num_timeslots)] ax.bar(x_pos + 0.1, num_resources, label="used resources", color="r", width=0.5) ax.bar(x_pos, capacities, label="available_resources", color="g", width=0.5) ax.set_xticks(x_pos) ax.legend() ax.set_xlabel("Timeslot") ax.set_ylabel("Resources") def plot_workloads(ax=None): if ax is None: fig, ax = plt.subplots() ax.set_title("Work Loads") x_pos = np.arange(len(workloads)) ax.bar(x_pos, workloads, width=0.5) ax.set_xticks(x_pos) ax.set_xlabel("Jobs") ax.set_ylabel("Required Resources") def is_printable_solution(solution): return np.array_equal(solution.sum(axis=0), np.ones(solution.shape[1])) def plot_workflow(solution=None): if solution is None: fig, axes = plt.subplots(1, 3, figsize=(18, 5)) plot_resource_graph(ax=axes[0]) plot_graph(ax=axes[1]) plot_workloads(ax=axes[2]) else: if is_printable_solution(solution): fig, axes = plt.subplots(2, 2, figsize=(12, 10)) plot_resource_graph(solution, axes[0, 0]) plot_workloads(axes[0, 1]) plot_assignments(solution, axes[1, 1]) plot_graph(solution, axes[1, 0]) else: # illegal solution fig, axes = plt.subplots(1, 3, figsize=(18, 5)) plot_resource_graph(solution, axes[0]) plot_workloads(axes[1]) plot_assignments(solution, axes[2]) ``` ## Initializing a Specific Problem Instance Create a workflow dependencies graph. For the small instance, all timeslot capacities and workloads are equal to each other: ```python theme={null} def small_example(): G = nx.DiGraph() nodes = range(4) edges = [(0, 1), (1, 3), (2, 3)] G.add_nodes_from(nodes) G.add_edges_from(edges) num_timeslots = len(G.nodes) - 1 capacities = 3 * np.ones(num_timeslots) workloads = np.ones(len(nodes)) return ( define_workflow_problem( G, num_timeslots, available_capacities=capacities, work_loads=workloads ), G, num_timeslots, capacities, workloads, ) def large_example(): G = nx.DiGraph() nodes = range(6) edges = [(0, 1), (0, 3), (0, 2), (2, 4), (3, 4), (1, 5), (4, 5)] workloads = [1, 3, 2, 2, 1, 1] capacities = [1, 3, 4, 3, 1] G.add_nodes_from(nodes) G.add_edges_from(edges) num_timeslots = len(capacities) return ( define_workflow_problem( G, num_timeslots, available_capacities=capacities, work_loads=workloads ), G, num_timeslots, capacities, workloads, ) ``` ```python theme={null} tasks_model, G, num_timeslots, capacities, workloads = small_example() plot_workflow() ``` output This is the resulting Pyomo model: ```python theme={null} tasks_model.pprint() ``` **Output:** ``` 1 Var Declarations x : Size=12, Index={0, 1, 2}*{0, 1, 2, 3} Key : Lower : Value : Upper : Fixed : Stale : Domain (0, 0) : 0 : None : 1 : False : True : Binary (0, 1) : 0 : None : 1 : False : True : Binary (0, 2) : 0 : None : 1 : False : True : Binary (0, 3) : 0 : None : 1 : False : True : Binary (1, 0) : 0 : None : 1 : False : True : Binary (1, 1) : 0 : None : 1 : False : True : Binary (1, 2) : 0 : None : 1 : False : True : Binary (1, 3) : 0 : None : 1 : False : True : Binary (2, 0) : 0 : None : 1 : False : True : Binary (2, 1) : 0 : None : 1 : False : True : Binary (2, 2) : 0 : None : 1 : False : True : Binary (2, 3) : 0 : None : 1 : False : True : Binary 1 Objective Declarations cost : Size=1, Index=None, Active=True Key : Active : Sense : Expression None : True : minimize : x[0,3] + 2*x[1,3] + 3*x[2,3] 4 Constraint Declarations all_works_are_done : Size=4, Index={0, 1, 2, 3}, Active=True Key : Lower : Body : Upper : Active 0 : 1.0 : x[0,0] + x[1,0] + x[2,0] : 1.0 : True 1 : 1.0 : x[0,1] + x[1,1] + x[2,1] : 1.0 : True 2 : 1.0 : x[0,2] + x[1,2] + x[2,2] : 1.0 : True 3 : 1.0 : x[0,3] + x[1,3] + x[2,3] : 1.0 : True capacity_is_valid : Size=3, Index={0, 1, 2}, Active=True Key : Lower : Body : Upper : Active 0 : -Inf : x[0,0] + x[0,1] + x[0,2] + x[0,3] : 3.0 : True 1 : -Inf : x[1,0] + x[1,1] + x[1,2] + x[1,3] : 3.0 : True 2 : -Inf : x[2,0] + x[2,1] + x[2,2] + x[2,3] : 3.0 : True eliminating_rule : Size=12, Index={0, 1, 2, 3}*{0, 1, 2}, Active=True Key : Lower : Body : Upper : Active (0, 0) : -Inf : 0.0 : 0.0 : True (0, 1) : 0.0 : x[1,0] : 0.0 : True (0, 2) : 0.0 : x[2,0] : 0.0 : True (1, 0) : 0.0 : x[0,1] : 0.0 : True (1, 1) : -Inf : 0.0 : 0.0 : True (1, 2) : 0.0 : x[2,1] : 0.0 : True (2, 0) : -Inf : 0.0 : 0.0 : True (2, 1) : -Inf : 0.0 : 0.0 : True (2, 2) : 0.0 : x[2,2] : 0.0 : True (3, 0) : 0.0 : x[0,3] : 0.0 : True (3, 1) : -Inf : 0.0 : 0.0 : True (3, 2) : -Inf : 0.0 : 0.0 : True works_done_by_their_order : Size=144, Index={0, 1, 2, 3}*{0, 1, 2, 3}*{0, 1, 2}*{0, 1, 2}, Active=True Key : Lower : Body : Upper : Active (0, 0, 0, 0) : -Inf : 0.0 : 0.0 : True (0, 0, 0, 1) : -Inf : 0.0 : 0.0 : True (0, 0, 0, 2) : -Inf : 0.0 : 0.0 : True (0, 0, 1, 0) : -Inf : 0.0 : 0.0 : True (0, 0, 1, 1) : -Inf : 0.0 : 0.0 : True (0, 0, 1, 2) : -Inf : 0.0 : 0.0 : True (0, 0, 2, 0) : -Inf : 0.0 : 0.0 : True (0, 0, 2, 1) : -Inf : 0.0 : 0.0 : True (0, 0, 2, 2) : -Inf : 0.0 : 0.0 : True (0, 1, 0, 0) : 0.0 : x[0,0]*x[0,1] : 0.0 : True (0, 1, 0, 1) : -Inf : 0.0 : 0.0 : True (0, 1, 0, 2) : -Inf : 0.0 : 0.0 : True (0, 1, 1, 0) : 0.0 : x[1,0]*x[0,1] : 0.0 : True (0, 1, 1, 1) : 0.0 : x[1,0]*x[1,1] : 0.0 : True (0, 1, 1, 2) : -Inf : 0.0 : 0.0 : True (0, 1, 2, 0) : 0.0 : x[2,0]*x[0,1] : 0.0 : True (0, 1, 2, 1) : 0.0 : x[2,0]*x[1,1] : 0.0 : True (0, 1, 2, 2) : 0.0 : x[2,0]*x[2,1] : 0.0 : True (0, 2, 0, 0) : -Inf : 0.0 : 0.0 : True (0, 2, 0, 1) : -Inf : 0.0 : 0.0 : True (0, 2, 0, 2) : -Inf : 0.0 : 0.0 : True (0, 2, 1, 0) : -Inf : 0.0 : 0.0 : True (0, 2, 1, 1) : -Inf : 0.0 : 0.0 : True (0, 2, 1, 2) : -Inf : 0.0 : 0.0 : True (0, 2, 2, 0) : -Inf : 0.0 : 0.0 : True (0, 2, 2, 1) : -Inf : 0.0 : 0.0 : True (0, 2, 2, 2) : -Inf : 0.0 : 0.0 : True (0, 3, 0, 0) : -Inf : 0.0 : 0.0 : True (0, 3, 0, 1) : -Inf : 0.0 : 0.0 : True (0, 3, 0, 2) : -Inf : 0.0 : 0.0 : True (0, 3, 1, 0) : -Inf : 0.0 : 0.0 : True (0, 3, 1, 1) : -Inf : 0.0 : 0.0 : True (0, 3, 1, 2) : -Inf : 0.0 : 0.0 : True (0, 3, 2, 0) : -Inf : 0.0 : 0.0 : True (0, 3, 2, 1) : -Inf : 0.0 : 0.0 : True (0, 3, 2, 2) : -Inf : 0.0 : 0.0 : True (1, 0, 0, 0) : -Inf : 0.0 : 0.0 : True (1, 0, 0, 1) : -Inf : 0.0 : 0.0 : True (1, 0, 0, 2) : -Inf : 0.0 : 0.0 : True (1, 0, 1, 0) : -Inf : 0.0 : 0.0 : True (1, 0, 1, 1) : -Inf : 0.0 : 0.0 : True (1, 0, 1, 2) : -Inf : 0.0 : 0.0 : True (1, 0, 2, 0) : -Inf : 0.0 : 0.0 : True (1, 0, 2, 1) : -Inf : 0.0 : 0.0 : True (1, 0, 2, 2) : -Inf : 0.0 : 0.0 : True (1, 1, 0, 0) : -Inf : 0.0 : 0.0 : True (1, 1, 0, 1) : -Inf : 0.0 : 0.0 : True (1, 1, 0, 2) : -Inf : 0.0 : 0.0 : True (1, 1, 1, 0) : -Inf : 0.0 : 0.0 : True (1, 1, 1, 1) : -Inf : 0.0 : 0.0 : True (1, 1, 1, 2) : -Inf : 0.0 : 0.0 : True (1, 1, 2, 0) : -Inf : 0.0 : 0.0 : True (1, 1, 2, 1) : -Inf : 0.0 : 0.0 : True (1, 1, 2, 2) : -Inf : 0.0 : 0.0 : True (1, 2, 0, 0) : -Inf : 0.0 : 0.0 : True (1, 2, 0, 1) : -Inf : 0.0 : 0.0 : True (1, 2, 0, 2) : -Inf : 0.0 : 0.0 : True (1, 2, 1, 0) : -Inf : 0.0 : 0.0 : True (1, 2, 1, 1) : -Inf : 0.0 : 0.0 : True (1, 2, 1, 2) : -Inf : 0.0 : 0.0 : True (1, 2, 2, 0) : -Inf : 0.0 : 0.0 : True (1, 2, 2, 1) : -Inf : 0.0 : 0.0 : True (1, 2, 2, 2) : -Inf : 0.0 : 0.0 : True (1, 3, 0, 0) : 0.0 : x[0,1]*x[0,3] : 0.0 : True (1, 3, 0, 1) : -Inf : 0.0 : 0.0 : True (1, 3, 0, 2) : -Inf : 0.0 : 0.0 : True (1, 3, 1, 0) : 0.0 : x[1,1]*x[0,3] : 0.0 : True (1, 3, 1, 1) : 0.0 : x[1,1]*x[1,3] : 0.0 : True (1, 3, 1, 2) : -Inf : 0.0 : 0.0 : True (1, 3, 2, 0) : 0.0 : x[2,1]*x[0,3] : 0.0 : True (1, 3, 2, 1) : 0.0 : x[2,1]*x[1,3] : 0.0 : True (1, 3, 2, 2) : 0.0 : x[2,1]*x[2,3] : 0.0 : True (2, 0, 0, 0) : -Inf : 0.0 : 0.0 : True (2, 0, 0, 1) : -Inf : 0.0 : 0.0 : True (2, 0, 0, 2) : -Inf : 0.0 : 0.0 : True (2, 0, 1, 0) : -Inf : 0.0 : 0.0 : True (2, 0, 1, 1) : -Inf : 0.0 : 0.0 : True (2, 0, 1, 2) : -Inf : 0.0 : 0.0 : True (2, 0, 2, 0) : -Inf : 0.0 : 0.0 : True (2, 0, 2, 1) : -Inf : 0.0 : 0.0 : True (2, 0, 2, 2) : -Inf : 0.0 : 0.0 : True (2, 1, 0, 0) : -Inf : 0.0 : 0.0 : True (2, 1, 0, 1) : -Inf : 0.0 : 0.0 : True (2, 1, 0, 2) : -Inf : 0.0 : 0.0 : True (2, 1, 1, 0) : -Inf : 0.0 : 0.0 : True (2, 1, 1, 1) : -Inf : 0.0 : 0.0 : True (2, 1, 1, 2) : -Inf : 0.0 : 0.0 : True (2, 1, 2, 0) : -Inf : 0.0 : 0.0 : True (2, 1, 2, 1) : -Inf : 0.0 : 0.0 : True (2, 1, 2, 2) : -Inf : 0.0 : 0.0 : True (2, 2, 0, 0) : -Inf : 0.0 : 0.0 : True (2, 2, 0, 1) : -Inf : 0.0 : 0.0 : True (2, 2, 0, 2) : -Inf : 0.0 : 0.0 : True (2, 2, 1, 0) : -Inf : 0.0 : 0.0 : True (2, 2, 1, 1) : -Inf : 0.0 : 0.0 : True (2, 2, 1, 2) : -Inf : 0.0 : 0.0 : True (2, 2, 2, 0) : -Inf : 0.0 : 0.0 : True (2, 2, 2, 1) : -Inf : 0.0 : 0.0 : True (2, 2, 2, 2) : -Inf : 0.0 : 0.0 : True (2, 3, 0, 0) : 0.0 : x[0,2]*x[0,3] : 0.0 : True (2, 3, 0, 1) : -Inf : 0.0 : 0.0 : True (2, 3, 0, 2) : -Inf : 0.0 : 0.0 : True (2, 3, 1, 0) : 0.0 : x[1,2]*x[0,3] : 0.0 : True (2, 3, 1, 1) : 0.0 : x[1,2]*x[1,3] : 0.0 : True (2, 3, 1, 2) : -Inf : 0.0 : 0.0 : True (2, 3, 2, 0) : 0.0 : x[2,2]*x[0,3] : 0.0 : True (2, 3, 2, 1) : 0.0 : x[2,2]*x[1,3] : 0.0 : True (2, 3, 2, 2) : 0.0 : x[2,2]*x[2,3] : 0.0 : True (3, 0, 0, 0) : -Inf : 0.0 : 0.0 : True (3, 0, 0, 1) : -Inf : 0.0 : 0.0 : True (3, 0, 0, 2) : -Inf : 0.0 : 0.0 : True (3, 0, 1, 0) : -Inf : 0.0 : 0.0 : True (3, 0, 1, 1) : -Inf : 0.0 : 0.0 : True (3, 0, 1, 2) : -Inf : 0.0 : 0.0 : True (3, 0, 2, 0) : -Inf : 0.0 : 0.0 : True (3, 0, 2, 1) : -Inf : 0.0 : 0.0 : True (3, 0, 2, 2) : -Inf : 0.0 : 0.0 : True (3, 1, 0, 0) : -Inf : 0.0 : 0.0 : True (3, 1, 0, 1) : -Inf : 0.0 : 0.0 : True (3, 1, 0, 2) : -Inf : 0.0 : 0.0 : True (3, 1, 1, 0) : -Inf : 0.0 : 0.0 : True (3, 1, 1, 1) : -Inf : 0.0 : 0.0 : True (3, 1, 1, 2) : -Inf : 0.0 : 0.0 : True (3, 1, 2, 0) : -Inf : 0.0 : 0.0 : True (3, 1, 2, 1) : -Inf : 0.0 : 0.0 : True (3, 1, 2, 2) : -Inf : 0.0 : 0.0 : True (3, 2, 0, 0) : -Inf : 0.0 : 0.0 : True (3, 2, 0, 1) : -Inf : 0.0 : 0.0 : True (3, 2, 0, 2) : -Inf : 0.0 : 0.0 : True (3, 2, 1, 0) : -Inf : 0.0 : 0.0 : True (3, 2, 1, 1) : -Inf : 0.0 : 0.0 : True (3, 2, 1, 2) : -Inf : 0.0 : 0.0 : True (3, 2, 2, 0) : -Inf : 0.0 : 0.0 : True (3, 2, 2, 1) : -Inf : 0.0 : 0.0 : True (3, 2, 2, 2) : -Inf : 0.0 : 0.0 : True (3, 3, 0, 0) : -Inf : 0.0 : 0.0 : True (3, 3, 0, 1) : -Inf : 0.0 : 0.0 : True (3, 3, 0, 2) : -Inf : 0.0 : 0.0 : True (3, 3, 1, 0) : -Inf : 0.0 : 0.0 : True (3, 3, 1, 1) : -Inf : 0.0 : 0.0 : True (3, 3, 1, 2) : -Inf : 0.0 : 0.0 : True (3, 3, 2, 0) : -Inf : 0.0 : 0.0 : True (3, 3, 2, 1) : -Inf : 0.0 : 0.0 : True (3, 3, 2, 2) : -Inf : 0.0 : 0.0 : True 6 Declarations: x all_works_are_done capacity_is_valid works_done_by_their_order eliminating_rule cost ``` ## Optimization Model with Hybrid Classical/Quantum QAOA # ## Setting Up the Classiq Problem Instance To solve the Pyomo model defined above, use the Classiq combinatorial optimization engine. For the quantum part of the Quantum Approximate Optimization Algorithm (QAOA) algorithm (`QAOAConfig`), define the number of repetitions (`num_layers`): ```python theme={null} from classiq import * from classiq.applications.combinatorial_optimization import OptimizerConfig, QAOAConfig qaoa_config = QAOAConfig(num_layers=8, penalty_energy=20) ``` For the classical optimization part of the QAOA algorithm, define the maximum number of classical iterations (`max_iteration`) and the $\alpha$-parameter (`alpha_cvar`) for running CVaR-QAOA, an improved variation of the QAOA algorithm \[[3](#cvar)]: ```python theme={null} optimizer_config = OptimizerConfig(max_iteration=20, alpha_cvar=0.3) ``` Load the model based on the problem and algorithm parameters, which you can use to solve the problem: ```python theme={null} qmod = construct_combinatorial_optimization_model( pyo_model=tasks_model, qaoa_config=qaoa_config, optimizer_config=optimizer_config, ) ``` # ## Synthesizing the QAOA Circuit and Solving the Problem Synthesize and view the QAOA circuit (ansatz) used to solve the optimization problem: ```python theme={null} qprog = synthesize(qmod) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/39FfbEQz0qOc4MaJlwPJ5w3xgbp ``` **Output:** ``` https://platform.classiq.io/circuit/39FfbEQz0qOc4MaJlwPJ5w3xgbp?login=True&version=17 ``` Solve the problem by calling the `execute` function on the quantum program you generated: ```python theme={null} result = execute(qprog).result_value() ``` # ## Analyzing the Results Check the convergence of the run: ```python theme={null} result.convergence_graph ``` output Print the optimization results: ```python theme={null} import pandas as pd from classiq.applications.combinatorial_optimization import ( get_optimization_solution_from_pyo, ) solution = get_optimization_solution_from_pyo( tasks_model, vqe_result=result, penalty_energy=qaoa_config.penalty_energy ) optimization_result = pd.DataFrame.from_records(solution) optimization_result.sort_values(by="cost", ascending=True).head(5) ``` | | probability | cost | solution | count | | - | ----------- | ---- | ------------------------------------- | ----- | | 3 | 0.055176 | 3.0 | \[1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0] | 113 | | 6 | 0.048828 | 3.0 | \[0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0] | 100 | | 5 | 0.053223 | 3.0 | \[1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0] | 109 | | 4 | 0.053223 | 3.0 | \[0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0] | 109 | | 7 | 0.035645 | 23.0 | \[0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0] | 73 | ```python theme={null} idx = optimization_result.cost.idxmin() print( "x =", optimization_result.solution[idx], ", cost =", optimization_result.cost[idx] ) ``` **Output:** ``` x = [1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0] , cost = 3.0 ``` And the histogram: ```python theme={null} optimization_result.hist("cost", weights=optimization_result["probability"]) ``` **Output:** ``` array([[]], dtype=object) ``` output This is the best solution: ```python theme={null} qaoa_solution = np.array( optimization_result.solution[optimization_result.cost.idxmin()] ).reshape(num_timeslots, len(G.nodes)) plot_workflow(qaoa_solution) ``` output # ## Comparing to a Classical Optimizer Result ```python theme={null} from pyomo.opt import SolverFactory solver = SolverFactory("couenne") solver.solve(tasks_model) tasks_model.display() ``` **Output:** ``` Model task_scheduling Variables: x : Size=12, Index={0, 1, 2}*{0, 1, 2, 3} Key : Lower : Value : Upper : Fixed : Stale : Domain (0, 0) : 0 : 1.0 : 1 : False : False : Binary (0, 1) : 0 : 0.0 : 1 : False : False : Binary (0, 2) : 0 : 1.0 : 1 : False : False : Binary (0, 3) : 0 : 0.0 : 1 : False : False : Binary (1, 0) : 0 : 0.0 : 1 : False : False : Binary (1, 1) : 0 : 1.0 : 1 : False : False : Binary (1, 2) : 0 : 0.0 : 1 : False : False : Binary (1, 3) : 0 : 0.0 : 1 : False : False : Binary (2, 0) : 0 : 0.0 : 1 : False : False : Binary (2, 1) : 0 : 0.0 : 1 : False : False : Binary (2, 2) : 0 : 0.0 : 1 : False : False : Binary (2, 3) : 0 : 1.0 : 1 : False : False : Binary Objectives: cost : Size=1, Index=None, Active=True Key : Active : Value None : True : 3.0 Constraints: all_works_are_done : Size=4 Key : Lower : Body : Upper 0 : 1.0 : 1.0 : 1.0 1 : 1.0 : 1.0 : 1.0 2 : 1.0 : 1.0 : 1.0 3 : 1.0 : 1.0 : 1.0 capacity_is_valid : Size=3 Key : Lower : Body : Upper 0 : None : 2.0 : 3.0 1 : None : 1.0 : 3.0 2 : None : 1.0 : 3.0 works_done_by_their_order : Size=144 Key : Lower : Body : Upper (0, 0, 0, 0) : None : 0.0 : 0.0 (0, 0, 0, 1) : None : 0.0 : 0.0 (0, 0, 0, 2) : None : 0.0 : 0.0 (0, 0, 1, 0) : None : 0.0 : 0.0 (0, 0, 1, 1) : None : 0.0 : 0.0 (0, 0, 1, 2) : None : 0.0 : 0.0 (0, 0, 2, 0) : None : 0.0 : 0.0 (0, 0, 2, 1) : None : 0.0 : 0.0 (0, 0, 2, 2) : None : 0.0 : 0.0 (0, 1, 0, 0) : 0.0 : 0.0 : 0.0 (0, 1, 0, 1) : None : 0.0 : 0.0 (0, 1, 0, 2) : None : 0.0 : 0.0 (0, 1, 1, 0) : 0.0 : 0.0 : 0.0 (0, 1, 1, 1) : 0.0 : 0.0 : 0.0 (0, 1, 1, 2) : None : 0.0 : 0.0 (0, 1, 2, 0) : 0.0 : 0.0 : 0.0 (0, 1, 2, 1) : 0.0 : 0.0 : 0.0 (0, 1, 2, 2) : 0.0 : 0.0 : 0.0 (0, 2, 0, 0) : None : 0.0 : 0.0 (0, 2, 0, 1) : None : 0.0 : 0.0 (0, 2, 0, 2) : None : 0.0 : 0.0 (0, 2, 1, 0) : None : 0.0 : 0.0 (0, 2, 1, 1) : None : 0.0 : 0.0 (0, 2, 1, 2) : None : 0.0 : 0.0 (0, 2, 2, 0) : None : 0.0 : 0.0 (0, 2, 2, 1) : None : 0.0 : 0.0 (0, 2, 2, 2) : None : 0.0 : 0.0 (0, 3, 0, 0) : None : 0.0 : 0.0 (0, 3, 0, 1) : None : 0.0 : 0.0 (0, 3, 0, 2) : None : 0.0 : 0.0 (0, 3, 1, 0) : None : 0.0 : 0.0 (0, 3, 1, 1) : None : 0.0 : 0.0 (0, 3, 1, 2) : None : 0.0 : 0.0 (0, 3, 2, 0) : None : 0.0 : 0.0 (0, 3, 2, 1) : None : 0.0 : 0.0 (0, 3, 2, 2) : None : 0.0 : 0.0 (1, 0, 0, 0) : None : 0.0 : 0.0 (1, 0, 0, 1) : None : 0.0 : 0.0 (1, 0, 0, 2) : None : 0.0 : 0.0 (1, 0, 1, 0) : None : 0.0 : 0.0 (1, 0, 1, 1) : None : 0.0 : 0.0 (1, 0, 1, 2) : None : 0.0 : 0.0 (1, 0, 2, 0) : None : 0.0 : 0.0 (1, 0, 2, 1) : None : 0.0 : 0.0 (1, 0, 2, 2) : None : 0.0 : 0.0 (1, 1, 0, 0) : None : 0.0 : 0.0 (1, 1, 0, 1) : None : 0.0 : 0.0 (1, 1, 0, 2) : None : 0.0 : 0.0 (1, 1, 1, 0) : None : 0.0 : 0.0 (1, 1, 1, 1) : None : 0.0 : 0.0 (1, 1, 1, 2) : None : 0.0 : 0.0 (1, 1, 2, 0) : None : 0.0 : 0.0 (1, 1, 2, 1) : None : 0.0 : 0.0 (1, 1, 2, 2) : None : 0.0 : 0.0 (1, 2, 0, 0) : None : 0.0 : 0.0 (1, 2, 0, 1) : None : 0.0 : 0.0 (1, 2, 0, 2) : None : 0.0 : 0.0 (1, 2, 1, 0) : None : 0.0 : 0.0 (1, 2, 1, 1) : None : 0.0 : 0.0 (1, 2, 1, 2) : None : 0.0 : 0.0 (1, 2, 2, 0) : None : 0.0 : 0.0 (1, 2, 2, 1) : None : 0.0 : 0.0 (1, 2, 2, 2) : None : 0.0 : 0.0 (1, 3, 0, 0) : 0.0 : 0.0 : 0.0 (1, 3, 0, 1) : None : 0.0 : 0.0 (1, 3, 0, 2) : None : 0.0 : 0.0 (1, 3, 1, 0) : 0.0 : 0.0 : 0.0 (1, 3, 1, 1) : 0.0 : 0.0 : 0.0 (1, 3, 1, 2) : None : 0.0 : 0.0 (1, 3, 2, 0) : 0.0 : 0.0 : 0.0 (1, 3, 2, 1) : 0.0 : 0.0 : 0.0 (1, 3, 2, 2) : 0.0 : 0.0 : 0.0 (2, 0, 0, 0) : None : 0.0 : 0.0 (2, 0, 0, 1) : None : 0.0 : 0.0 (2, 0, 0, 2) : None : 0.0 : 0.0 (2, 0, 1, 0) : None : 0.0 : 0.0 (2, 0, 1, 1) : None : 0.0 : 0.0 (2, 0, 1, 2) : None : 0.0 : 0.0 (2, 0, 2, 0) : None : 0.0 : 0.0 (2, 0, 2, 1) : None : 0.0 : 0.0 (2, 0, 2, 2) : None : 0.0 : 0.0 (2, 1, 0, 0) : None : 0.0 : 0.0 (2, 1, 0, 1) : None : 0.0 : 0.0 (2, 1, 0, 2) : None : 0.0 : 0.0 (2, 1, 1, 0) : None : 0.0 : 0.0 (2, 1, 1, 1) : None : 0.0 : 0.0 (2, 1, 1, 2) : None : 0.0 : 0.0 (2, 1, 2, 0) : None : 0.0 : 0.0 (2, 1, 2, 1) : None : 0.0 : 0.0 (2, 1, 2, 2) : None : 0.0 : 0.0 (2, 2, 0, 0) : None : 0.0 : 0.0 (2, 2, 0, 1) : None : 0.0 : 0.0 (2, 2, 0, 2) : None : 0.0 : 0.0 (2, 2, 1, 0) : None : 0.0 : 0.0 (2, 2, 1, 1) : None : 0.0 : 0.0 (2, 2, 1, 2) : None : 0.0 : 0.0 (2, 2, 2, 0) : None : 0.0 : 0.0 (2, 2, 2, 1) : None : 0.0 : 0.0 (2, 2, 2, 2) : None : 0.0 : 0.0 (2, 3, 0, 0) : 0.0 : 0.0 : 0.0 (2, 3, 0, 1) : None : 0.0 : 0.0 (2, 3, 0, 2) : None : 0.0 : 0.0 (2, 3, 1, 0) : 0.0 : 0.0 : 0.0 (2, 3, 1, 1) : 0.0 : 0.0 : 0.0 (2, 3, 1, 2) : None : 0.0 : 0.0 (2, 3, 2, 0) : 0.0 : 0.0 : 0.0 (2, 3, 2, 1) : 0.0 : 0.0 : 0.0 (2, 3, 2, 2) : 0.0 : 0.0 : 0.0 (3, 0, 0, 0) : None : 0.0 : 0.0 (3, 0, 0, 1) : None : 0.0 : 0.0 (3, 0, 0, 2) : None : 0.0 : 0.0 (3, 0, 1, 0) : None : 0.0 : 0.0 (3, 0, 1, 1) : None : 0.0 : 0.0 (3, 0, 1, 2) : None : 0.0 : 0.0 (3, 0, 2, 0) : None : 0.0 : 0.0 (3, 0, 2, 1) : None : 0.0 : 0.0 (3, 0, 2, 2) : None : 0.0 : 0.0 (3, 1, 0, 0) : None : 0.0 : 0.0 (3, 1, 0, 1) : None : 0.0 : 0.0 (3, 1, 0, 2) : None : 0.0 : 0.0 (3, 1, 1, 0) : None : 0.0 : 0.0 (3, 1, 1, 1) : None : 0.0 : 0.0 (3, 1, 1, 2) : None : 0.0 : 0.0 (3, 1, 2, 0) : None : 0.0 : 0.0 (3, 1, 2, 1) : None : 0.0 : 0.0 (3, 1, 2, 2) : None : 0.0 : 0.0 (3, 2, 0, 0) : None : 0.0 : 0.0 (3, 2, 0, 1) : None : 0.0 : 0.0 (3, 2, 0, 2) : None : 0.0 : 0.0 (3, 2, 1, 0) : None : 0.0 : 0.0 (3, 2, 1, 1) : None : 0.0 : 0.0 (3, 2, 1, 2) : None : 0.0 : 0.0 (3, 2, 2, 0) : None : 0.0 : 0.0 (3, 2, 2, 1) : None : 0.0 : 0.0 (3, 2, 2, 2) : None : 0.0 : 0.0 (3, 3, 0, 0) : None : 0.0 : 0.0 (3, 3, 0, 1) : None : 0.0 : 0.0 (3, 3, 0, 2) : None : 0.0 : 0.0 (3, 3, 1, 0) : None : 0.0 : 0.0 (3, 3, 1, 1) : None : 0.0 : 0.0 (3, 3, 1, 2) : None : 0.0 : 0.0 (3, 3, 2, 0) : None : 0.0 : 0.0 (3, 3, 2, 1) : None : 0.0 : 0.0 (3, 3, 2, 2) : None : 0.0 : 0.0 eliminating_rule : Size=12 Key : Lower : Body : Upper (0, 0) : None : 0.0 : 0.0 (0, 1) : 0.0 : 0.0 : 0.0 (0, 2) : 0.0 : 0.0 : 0.0 (1, 0) : 0.0 : 0.0 : 0.0 (1, 1) : None : 0.0 : 0.0 (1, 2) : 0.0 : 0.0 : 0.0 (2, 0) : None : 0.0 : 0.0 (2, 1) : None : 0.0 : 0.0 (2, 2) : 0.0 : 0.0 : 0.0 (3, 0) : 0.0 : 0.0 : 0.0 (3, 1) : None : 0.0 : 0.0 (3, 2) : None : 0.0 : 0.0 ``` ```python theme={null} classical_solution = np.array( [ int(pyo.value(tasks_model.x[idx])) for idx in np.ndindex(num_timeslots, len(G.nodes)) ] ).reshape(num_timeslots, len(G.nodes)) plot_workflow(classical_solution) ``` output ## Large Example Consider a more elaborate example, involving work with non-uniform workloads and resources: ```python theme={null} tasks_model_large, G, num_timeslots, capacities, workloads = large_example() plot_workflow() ``` output ```python theme={null} tasks_model_large.pprint() ``` **Output:** ``` 1 Var Declarations x : Size=30, Index={0, 1, 2, 3, 4}*{0, 1, 2, 3, 4, 5} Key : Lower : Value : Upper : Fixed : Stale : Domain (0, 0) : 0 : None : 1 : False : True : Binary (0, 1) : 0 : None : 1 : False : True : Binary (0, 2) : 0 : None : 1 : False : True : Binary (0, 3) : 0 : None : 1 : False : True : Binary (0, 4) : 0 : None : 1 : False : True : Binary (0, 5) : 0 : None : 1 : False : True : Binary (1, 0) : 0 : None : 1 : False : True : Binary (1, 1) : 0 : None : 1 : False : True : Binary (1, 2) : 0 : None : 1 : False : True : Binary (1, 3) : 0 : None : 1 : False : True : Binary (1, 4) : 0 : None : 1 : False : True : Binary (1, 5) : 0 : None : 1 : False : True : Binary (2, 0) : 0 : None : 1 : False : True : Binary (2, 1) : 0 : None : 1 : False : True : Binary (2, 2) : 0 : None : 1 : False : True : Binary (2, 3) : 0 : None : 1 : False : True : Binary (2, 4) : 0 : None : 1 : False : True : Binary (2, 5) : 0 : None : 1 : False : True : Binary (3, 0) : 0 : None : 1 : False : True : Binary (3, 1) : 0 : None : 1 : False : True : Binary (3, 2) : 0 : None : 1 : False : True : Binary (3, 3) : 0 : None : 1 : False : True : Binary (3, 4) : 0 : None : 1 : False : True : Binary (3, 5) : 0 : None : 1 : False : True : Binary (4, 0) : 0 : None : 1 : False : True : Binary (4, 1) : 0 : None : 1 : False : True : Binary (4, 2) : 0 : None : 1 : False : True : Binary (4, 3) : 0 : None : 1 : False : True : Binary (4, 4) : 0 : None : 1 : False : True : Binary (4, 5) : 0 : None : 1 : False : True : Binary 1 Objective Declarations cost : Size=1, Index=None, Active=True Key : Active : Sense : Expression None : True : minimize : x[0,5] + 2*x[1,5] + 3*x[2,5] + 4*x[3,5] + 5*x[4,5] 4 Constraint Declarations all_works_are_done : Size=6, Index={0, 1, 2, 3, 4, 5}, Active=True Key : Lower : Body : Upper : Active 0 : 1.0 : x[0,0] + x[1,0] + x[2,0] + x[3,0] + x[4,0] : 1.0 : True 1 : 1.0 : x[0,1] + x[1,1] + x[2,1] + x[3,1] + x[4,1] : 1.0 : True 2 : 1.0 : x[0,2] + x[1,2] + x[2,2] + x[3,2] + x[4,2] : 1.0 : True 3 : 1.0 : x[0,3] + x[1,3] + x[2,3] + x[3,3] + x[4,3] : 1.0 : True 4 : 1.0 : x[0,4] + x[1,4] + x[2,4] + x[3,4] + x[4,4] : 1.0 : True 5 : 1.0 : x[0,5] + x[1,5] + x[2,5] + x[3,5] + x[4,5] : 1.0 : True capacity_is_valid : Size=5, Index={0, 1, 2, 3, 4}, Active=True Key : Lower : Body : Upper : Active 0 : -Inf : x[0,0] + 3*x[0,1] + 2*x[0,2] + 2*x[0,3] + x[0,4] + x[0,5] : 1.0 : True 1 : -Inf : x[1,0] + 3*x[1,1] + 2*x[1,2] + 2*x[1,3] + x[1,4] + x[1,5] : 3.0 : True 2 : -Inf : x[2,0] + 3*x[2,1] + 2*x[2,2] + 2*x[2,3] + x[2,4] + x[2,5] : 4.0 : True 3 : -Inf : x[3,0] + 3*x[3,1] + 2*x[3,2] + 2*x[3,3] + x[3,4] + x[3,5] : 3.0 : True 4 : -Inf : x[4,0] + 3*x[4,1] + 2*x[4,2] + 2*x[4,3] + x[4,4] + x[4,5] : 1.0 : True eliminating_rule : Size=30, Index={0, 1, 2, 3, 4, 5}*{0, 1, 2, 3, 4}, Active=True Key : Lower : Body : Upper : Active (0, 0) : -Inf : 0.0 : 0.0 : True (0, 1) : -Inf : 0.0 : 0.0 : True (0, 2) : -Inf : 0.0 : 0.0 : True (0, 3) : 0.0 : x[3,0] : 0.0 : True (0, 4) : 0.0 : x[4,0] : 0.0 : True (1, 0) : 0.0 : x[0,1] : 0.0 : True (1, 1) : -Inf : 0.0 : 0.0 : True (1, 2) : -Inf : 0.0 : 0.0 : True (1, 3) : -Inf : 0.0 : 0.0 : True (1, 4) : 0.0 : x[4,1] : 0.0 : True (2, 0) : 0.0 : x[0,2] : 0.0 : True (2, 1) : -Inf : 0.0 : 0.0 : True (2, 2) : -Inf : 0.0 : 0.0 : True (2, 3) : 0.0 : x[3,2] : 0.0 : True (2, 4) : 0.0 : x[4,2] : 0.0 : True (3, 0) : 0.0 : x[0,3] : 0.0 : True (3, 1) : -Inf : 0.0 : 0.0 : True (3, 2) : -Inf : 0.0 : 0.0 : True (3, 3) : 0.0 : x[3,3] : 0.0 : True (3, 4) : 0.0 : x[4,3] : 0.0 : True (4, 0) : 0.0 : x[0,4] : 0.0 : True (4, 1) : 0.0 : x[1,4] : 0.0 : True (4, 2) : -Inf : 0.0 : 0.0 : True (4, 3) : -Inf : 0.0 : 0.0 : True (4, 4) : 0.0 : x[4,4] : 0.0 : True (5, 0) : 0.0 : x[0,5] : 0.0 : True (5, 1) : 0.0 : x[1,5] : 0.0 : True (5, 2) : -Inf : 0.0 : 0.0 : True (5, 3) : -Inf : 0.0 : 0.0 : True (5, 4) : -Inf : 0.0 : 0.0 : True works_done_by_their_order : Size=900, Index={0, 1, 2, 3, 4, 5}*{0, 1, 2, 3, 4, 5}*{0, 1, 2, 3, 4}*{0, 1, 2, 3, 4}, Active=True Key : Lower : Body : Upper : Active (0, 0, 0, 0) : -Inf : 0.0 : 0.0 : True (0, 0, 0, 1) : -Inf : 0.0 : 0.0 : True (0, 0, 0, 2) : -Inf : 0.0 : 0.0 : True (0, 0, 0, 3) : -Inf : 0.0 : 0.0 : True (0, 0, 0, 4) : -Inf : 0.0 : 0.0 : True (0, 0, 1, 0) : -Inf : 0.0 : 0.0 : True (0, 0, 1, 1) : -Inf : 0.0 : 0.0 : True (0, 0, 1, 2) : -Inf : 0.0 : 0.0 : True (0, 0, 1, 3) : -Inf : 0.0 : 0.0 : True (0, 0, 1, 4) : -Inf : 0.0 : 0.0 : True (0, 0, 2, 0) : -Inf : 0.0 : 0.0 : True (0, 0, 2, 1) : -Inf : 0.0 : 0.0 : True (0, 0, 2, 2) : -Inf : 0.0 : 0.0 : True (0, 0, 2, 3) : -Inf : 0.0 : 0.0 : True (0, 0, 2, 4) : -Inf : 0.0 : 0.0 : True (0, 0, 3, 0) : -Inf : 0.0 : 0.0 : True (0, 0, 3, 1) : -Inf : 0.0 : 0.0 : True (0, 0, 3, 2) : -Inf : 0.0 : 0.0 : True (0, 0, 3, 3) : -Inf : 0.0 : 0.0 : True (0, 0, 3, 4) : -Inf : 0.0 : 0.0 : True (0, 0, 4, 0) : -Inf : 0.0 : 0.0 : True (0, 0, 4, 1) : -Inf : 0.0 : 0.0 : True (0, 0, 4, 2) : -Inf : 0.0 : 0.0 : True (0, 0, 4, 3) : -Inf : 0.0 : 0.0 : True (0, 0, 4, 4) : -Inf : 0.0 : 0.0 : True (0, 1, 0, 0) : 0.0 : x[0,0]*x[0,1] : 0.0 : True (0, 1, 0, 1) : -Inf : 0.0 : 0.0 : True (0, 1, 0, 2) : -Inf : 0.0 : 0.0 : True (0, 1, 0, 3) : -Inf : 0.0 : 0.0 : True (0, 1, 0, 4) : -Inf : 0.0 : 0.0 : True (0, 1, 1, 0) : 0.0 : x[1,0]*x[0,1] : 0.0 : True (0, 1, 1, 1) : 0.0 : x[1,0]*x[1,1] : 0.0 : True (0, 1, 1, 2) : -Inf : 0.0 : 0.0 : True (0, 1, 1, 3) : -Inf : 0.0 : 0.0 : True (0, 1, 1, 4) : -Inf : 0.0 : 0.0 : True (0, 1, 2, 0) : 0.0 : x[2,0]*x[0,1] : 0.0 : True (0, 1, 2, 1) : 0.0 : x[2,0]*x[1,1] : 0.0 : True (0, 1, 2, 2) : 0.0 : x[2,0]*x[2,1] : 0.0 : True (0, 1, 2, 3) : -Inf : 0.0 : 0.0 : True (0, 1, 2, 4) : -Inf : 0.0 : 0.0 : True (0, 1, 3, 0) : 0.0 : x[3,0]*x[0,1] : 0.0 : True (0, 1, 3, 1) : 0.0 : x[3,0]*x[1,1] : 0.0 : True (0, 1, 3, 2) : 0.0 : x[3,0]*x[2,1] : 0.0 : True (0, 1, 3, 3) : 0.0 : x[3,0]*x[3,1] : 0.0 : True (0, 1, 3, 4) : -Inf : 0.0 : 0.0 : True (0, 1, 4, 0) : 0.0 : x[4,0]*x[0,1] : 0.0 : True (0, 1, 4, 1) : 0.0 : x[4,0]*x[1,1] : 0.0 : True (0, 1, 4, 2) : 0.0 : x[4,0]*x[2,1] : 0.0 : True (0, 1, 4, 3) : 0.0 : x[4,0]*x[3,1] : 0.0 : True (0, 1, 4, 4) : 0.0 : x[4,0]*x[4,1] : 0.0 : True (0, 2, 0, 0) : 0.0 : x[0,0]*x[0,2] : 0.0 : True (0, 2, 0, 1) : -Inf : 0.0 : 0.0 : True (0, 2, 0, 2) : -Inf : 0.0 : 0.0 : True (0, 2, 0, 3) : -Inf : 0.0 : 0.0 : True (0, 2, 0, 4) : -Inf : 0.0 : 0.0 : True (0, 2, 1, 0) : 0.0 : x[1,0]*x[0,2] : 0.0 : True (0, 2, 1, 1) : 0.0 : x[1,0]*x[1,2] : 0.0 : True (0, 2, 1, 2) : -Inf : 0.0 : 0.0 : True (0, 2, 1, 3) : -Inf : 0.0 : 0.0 : True (0, 2, 1, 4) : -Inf : 0.0 : 0.0 : True (0, 2, 2, 0) : 0.0 : x[2,0]*x[0,2] : 0.0 : True (0, 2, 2, 1) : 0.0 : x[2,0]*x[1,2] : 0.0 : True (0, 2, 2, 2) : 0.0 : x[2,0]*x[2,2] : 0.0 : True (0, 2, 2, 3) : -Inf : 0.0 : 0.0 : True (0, 2, 2, 4) : -Inf : 0.0 : 0.0 : True (0, 2, 3, 0) : 0.0 : x[3,0]*x[0,2] : 0.0 : True (0, 2, 3, 1) : 0.0 : x[3,0]*x[1,2] : 0.0 : True (0, 2, 3, 2) : 0.0 : x[3,0]*x[2,2] : 0.0 : True (0, 2, 3, 3) : 0.0 : x[3,0]*x[3,2] : 0.0 : True (0, 2, 3, 4) : -Inf : 0.0 : 0.0 : True (0, 2, 4, 0) : 0.0 : x[4,0]*x[0,2] : 0.0 : True (0, 2, 4, 1) : 0.0 : x[4,0]*x[1,2] : 0.0 : True (0, 2, 4, 2) : 0.0 : x[4,0]*x[2,2] : 0.0 : True (0, 2, 4, 3) : 0.0 : x[4,0]*x[3,2] : 0.0 : True (0, 2, 4, 4) : 0.0 : x[4,0]*x[4,2] : 0.0 : True (0, 3, 0, 0) : 0.0 : x[0,0]*x[0,3] : 0.0 : True (0, 3, 0, 1) : -Inf : 0.0 : 0.0 : True (0, 3, 0, 2) : -Inf : 0.0 : 0.0 : True (0, 3, 0, 3) : -Inf : 0.0 : 0.0 : True (0, 3, 0, 4) : -Inf : 0.0 : 0.0 : True (0, 3, 1, 0) : 0.0 : x[1,0]*x[0,3] : 0.0 : True (0, 3, 1, 1) : 0.0 : x[1,0]*x[1,3] : 0.0 : True (0, 3, 1, 2) : -Inf : 0.0 : 0.0 : True (0, 3, 1, 3) : -Inf : 0.0 : 0.0 : True (0, 3, 1, 4) : -Inf : 0.0 : 0.0 : True (0, 3, 2, 0) : 0.0 : x[2,0]*x[0,3] : 0.0 : True (0, 3, 2, 1) : 0.0 : x[2,0]*x[1,3] : 0.0 : True (0, 3, 2, 2) : 0.0 : x[2,0]*x[2,3] : 0.0 : True (0, 3, 2, 3) : -Inf : 0.0 : 0.0 : True (0, 3, 2, 4) : -Inf : 0.0 : 0.0 : True (0, 3, 3, 0) : 0.0 : x[3,0]*x[0,3] : 0.0 : True (0, 3, 3, 1) : 0.0 : x[3,0]*x[1,3] : 0.0 : True (0, 3, 3, 2) : 0.0 : x[3,0]*x[2,3] : 0.0 : True (0, 3, 3, 3) : 0.0 : x[3,0]*x[3,3] : 0.0 : True (0, 3, 3, 4) : -Inf : 0.0 : 0.0 : True (0, 3, 4, 0) : 0.0 : x[4,0]*x[0,3] : 0.0 : True (0, 3, 4, 1) : 0.0 : x[4,0]*x[1,3] : 0.0 : True (0, 3, 4, 2) : 0.0 : x[4,0]*x[2,3] : 0.0 : True (0, 3, 4, 3) : 0.0 : x[4,0]*x[3,3] : 0.0 : True (0, 3, 4, 4) : 0.0 : x[4,0]*x[4,3] : 0.0 : True (0, 4, 0, 0) : -Inf : 0.0 : 0.0 : True (0, 4, 0, 1) : -Inf : 0.0 : 0.0 : True (0, 4, 0, 2) : -Inf : 0.0 : 0.0 : True (0, 4, 0, 3) : -Inf : 0.0 : 0.0 : True (0, 4, 0, 4) : -Inf : 0.0 : 0.0 : True (0, 4, 1, 0) : -Inf : 0.0 : 0.0 : True (0, 4, 1, 1) : -Inf : 0.0 : 0.0 : True (0, 4, 1, 2) : -Inf : 0.0 : 0.0 : True (0, 4, 1, 3) : -Inf : 0.0 : 0.0 : True (0, 4, 1, 4) : -Inf : 0.0 : 0.0 : True (0, 4, 2, 0) : -Inf : 0.0 : 0.0 : True (0, 4, 2, 1) : -Inf : 0.0 : 0.0 : True (0, 4, 2, 2) : -Inf : 0.0 : 0.0 : True (0, 4, 2, 3) : -Inf : 0.0 : 0.0 : True (0, 4, 2, 4) : -Inf : 0.0 : 0.0 : True (0, 4, 3, 0) : -Inf : 0.0 : 0.0 : True (0, 4, 3, 1) : -Inf : 0.0 : 0.0 : True (0, 4, 3, 2) : -Inf : 0.0 : 0.0 : True (0, 4, 3, 3) : -Inf : 0.0 : 0.0 : True (0, 4, 3, 4) : -Inf : 0.0 : 0.0 : True (0, 4, 4, 0) : -Inf : 0.0 : 0.0 : True (0, 4, 4, 1) : -Inf : 0.0 : 0.0 : True (0, 4, 4, 2) : -Inf : 0.0 : 0.0 : True (0, 4, 4, 3) : -Inf : 0.0 : 0.0 : True (0, 4, 4, 4) : -Inf : 0.0 : 0.0 : True (0, 5, 0, 0) : -Inf : 0.0 : 0.0 : True (0, 5, 0, 1) : -Inf : 0.0 : 0.0 : True (0, 5, 0, 2) : -Inf : 0.0 : 0.0 : True (0, 5, 0, 3) : -Inf : 0.0 : 0.0 : True (0, 5, 0, 4) : -Inf : 0.0 : 0.0 : True (0, 5, 1, 0) : -Inf : 0.0 : 0.0 : True (0, 5, 1, 1) : -Inf : 0.0 : 0.0 : True (0, 5, 1, 2) : -Inf : 0.0 : 0.0 : True (0, 5, 1, 3) : -Inf : 0.0 : 0.0 : True (0, 5, 1, 4) : -Inf : 0.0 : 0.0 : True (0, 5, 2, 0) : -Inf : 0.0 : 0.0 : True (0, 5, 2, 1) : -Inf : 0.0 : 0.0 : True (0, 5, 2, 2) : -Inf : 0.0 : 0.0 : True (0, 5, 2, 3) : -Inf : 0.0 : 0.0 : True (0, 5, 2, 4) : -Inf : 0.0 : 0.0 : True (0, 5, 3, 0) : -Inf : 0.0 : 0.0 : True (0, 5, 3, 1) : -Inf : 0.0 : 0.0 : True (0, 5, 3, 2) : -Inf : 0.0 : 0.0 : True (0, 5, 3, 3) : -Inf : 0.0 : 0.0 : True (0, 5, 3, 4) : -Inf : 0.0 : 0.0 : True (0, 5, 4, 0) : -Inf : 0.0 : 0.0 : True (0, 5, 4, 1) : -Inf : 0.0 : 0.0 : True (0, 5, 4, 2) : -Inf : 0.0 : 0.0 : True (0, 5, 4, 3) : -Inf : 0.0 : 0.0 : True (0, 5, 4, 4) : -Inf : 0.0 : 0.0 : True (1, 0, 0, 0) : -Inf : 0.0 : 0.0 : True (1, 0, 0, 1) : -Inf : 0.0 : 0.0 : True (1, 0, 0, 2) : -Inf : 0.0 : 0.0 : True (1, 0, 0, 3) : -Inf : 0.0 : 0.0 : True (1, 0, 0, 4) : -Inf : 0.0 : 0.0 : True (1, 0, 1, 0) : -Inf : 0.0 : 0.0 : True (1, 0, 1, 1) : -Inf : 0.0 : 0.0 : True (1, 0, 1, 2) : -Inf : 0.0 : 0.0 : True (1, 0, 1, 3) : -Inf : 0.0 : 0.0 : True (1, 0, 1, 4) : -Inf : 0.0 : 0.0 : True (1, 0, 2, 0) : -Inf : 0.0 : 0.0 : True (1, 0, 2, 1) : -Inf : 0.0 : 0.0 : True (1, 0, 2, 2) : -Inf : 0.0 : 0.0 : True (1, 0, 2, 3) : -Inf : 0.0 : 0.0 : True (1, 0, 2, 4) : -Inf : 0.0 : 0.0 : True (1, 0, 3, 0) : -Inf : 0.0 : 0.0 : True (1, 0, 3, 1) : -Inf : 0.0 : 0.0 : True (1, 0, 3, 2) : -Inf : 0.0 : 0.0 : True (1, 0, 3, 3) : -Inf : 0.0 : 0.0 : True (1, 0, 3, 4) : -Inf : 0.0 : 0.0 : True (1, 0, 4, 0) : -Inf : 0.0 : 0.0 : True (1, 0, 4, 1) : -Inf : 0.0 : 0.0 : True (1, 0, 4, 2) : -Inf : 0.0 : 0.0 : True (1, 0, 4, 3) : -Inf : 0.0 : 0.0 : True (1, 0, 4, 4) : -Inf : 0.0 : 0.0 : True (1, 1, 0, 0) : -Inf : 0.0 : 0.0 : True (1, 1, 0, 1) : -Inf : 0.0 : 0.0 : True (1, 1, 0, 2) : -Inf : 0.0 : 0.0 : True (1, 1, 0, 3) : -Inf : 0.0 : 0.0 : True (1, 1, 0, 4) : -Inf : 0.0 : 0.0 : True (1, 1, 1, 0) : -Inf : 0.0 : 0.0 : True (1, 1, 1, 1) : -Inf : 0.0 : 0.0 : True (1, 1, 1, 2) : -Inf : 0.0 : 0.0 : True (1, 1, 1, 3) : -Inf : 0.0 : 0.0 : True (1, 1, 1, 4) : -Inf : 0.0 : 0.0 : True (1, 1, 2, 0) : -Inf : 0.0 : 0.0 : True (1, 1, 2, 1) : -Inf : 0.0 : 0.0 : True (1, 1, 2, 2) : -Inf : 0.0 : 0.0 : True (1, 1, 2, 3) : -Inf : 0.0 : 0.0 : True (1, 1, 2, 4) : -Inf : 0.0 : 0.0 : True (1, 1, 3, 0) : -Inf : 0.0 : 0.0 : True (1, 1, 3, 1) : -Inf : 0.0 : 0.0 : True (1, 1, 3, 2) : -Inf : 0.0 : 0.0 : True (1, 1, 3, 3) : -Inf : 0.0 : 0.0 : True (1, 1, 3, 4) : -Inf : 0.0 : 0.0 : True (1, 1, 4, 0) : -Inf : 0.0 : 0.0 : True (1, 1, 4, 1) : -Inf : 0.0 : 0.0 : True (1, 1, 4, 2) : -Inf : 0.0 : 0.0 : True (1, 1, 4, 3) : -Inf : 0.0 : 0.0 : True (1, 1, 4, 4) : -Inf : 0.0 : 0.0 : True (1, 2, 0, 0) : -Inf : 0.0 : 0.0 : True (1, 2, 0, 1) : -Inf : 0.0 : 0.0 : True (1, 2, 0, 2) : -Inf : 0.0 : 0.0 : True (1, 2, 0, 3) : -Inf : 0.0 : 0.0 : True (1, 2, 0, 4) : -Inf : 0.0 : 0.0 : True (1, 2, 1, 0) : -Inf : 0.0 : 0.0 : True (1, 2, 1, 1) : -Inf : 0.0 : 0.0 : True (1, 2, 1, 2) : -Inf : 0.0 : 0.0 : True (1, 2, 1, 3) : -Inf : 0.0 : 0.0 : True (1, 2, 1, 4) : -Inf : 0.0 : 0.0 : True (1, 2, 2, 0) : -Inf : 0.0 : 0.0 : True (1, 2, 2, 1) : -Inf : 0.0 : 0.0 : True (1, 2, 2, 2) : -Inf : 0.0 : 0.0 : True (1, 2, 2, 3) : -Inf : 0.0 : 0.0 : True (1, 2, 2, 4) : -Inf : 0.0 : 0.0 : True (1, 2, 3, 0) : -Inf : 0.0 : 0.0 : True (1, 2, 3, 1) : -Inf : 0.0 : 0.0 : True (1, 2, 3, 2) : -Inf : 0.0 : 0.0 : True (1, 2, 3, 3) : -Inf : 0.0 : 0.0 : True (1, 2, 3, 4) : -Inf : 0.0 : 0.0 : True (1, 2, 4, 0) : -Inf : 0.0 : 0.0 : True (1, 2, 4, 1) : -Inf : 0.0 : 0.0 : True (1, 2, 4, 2) : -Inf : 0.0 : 0.0 : True (1, 2, 4, 3) : -Inf : 0.0 : 0.0 : True (1, 2, 4, 4) : -Inf : 0.0 : 0.0 : True (1, 3, 0, 0) : -Inf : 0.0 : 0.0 : True (1, 3, 0, 1) : -Inf : 0.0 : 0.0 : True (1, 3, 0, 2) : -Inf : 0.0 : 0.0 : True (1, 3, 0, 3) : -Inf : 0.0 : 0.0 : True (1, 3, 0, 4) : -Inf : 0.0 : 0.0 : True (1, 3, 1, 0) : -Inf : 0.0 : 0.0 : True (1, 3, 1, 1) : -Inf : 0.0 : 0.0 : True (1, 3, 1, 2) : -Inf : 0.0 : 0.0 : True (1, 3, 1, 3) : -Inf : 0.0 : 0.0 : True (1, 3, 1, 4) : -Inf : 0.0 : 0.0 : True (1, 3, 2, 0) : -Inf : 0.0 : 0.0 : True (1, 3, 2, 1) : -Inf : 0.0 : 0.0 : True (1, 3, 2, 2) : -Inf : 0.0 : 0.0 : True (1, 3, 2, 3) : -Inf : 0.0 : 0.0 : True (1, 3, 2, 4) : -Inf : 0.0 : 0.0 : True (1, 3, 3, 0) : -Inf : 0.0 : 0.0 : True (1, 3, 3, 1) : -Inf : 0.0 : 0.0 : True (1, 3, 3, 2) : -Inf : 0.0 : 0.0 : True (1, 3, 3, 3) : -Inf : 0.0 : 0.0 : True (1, 3, 3, 4) : -Inf : 0.0 : 0.0 : True (1, 3, 4, 0) : -Inf : 0.0 : 0.0 : True (1, 3, 4, 1) : -Inf : 0.0 : 0.0 : True (1, 3, 4, 2) : -Inf : 0.0 : 0.0 : True (1, 3, 4, 3) : -Inf : 0.0 : 0.0 : True (1, 3, 4, 4) : -Inf : 0.0 : 0.0 : True (1, 4, 0, 0) : -Inf : 0.0 : 0.0 : True (1, 4, 0, 1) : -Inf : 0.0 : 0.0 : True (1, 4, 0, 2) : -Inf : 0.0 : 0.0 : True (1, 4, 0, 3) : -Inf : 0.0 : 0.0 : True (1, 4, 0, 4) : -Inf : 0.0 : 0.0 : True (1, 4, 1, 0) : -Inf : 0.0 : 0.0 : True (1, 4, 1, 1) : -Inf : 0.0 : 0.0 : True (1, 4, 1, 2) : -Inf : 0.0 : 0.0 : True (1, 4, 1, 3) : -Inf : 0.0 : 0.0 : True (1, 4, 1, 4) : -Inf : 0.0 : 0.0 : True (1, 4, 2, 0) : -Inf : 0.0 : 0.0 : True (1, 4, 2, 1) : -Inf : 0.0 : 0.0 : True (1, 4, 2, 2) : -Inf : 0.0 : 0.0 : True (1, 4, 2, 3) : -Inf : 0.0 : 0.0 : True (1, 4, 2, 4) : -Inf : 0.0 : 0.0 : True (1, 4, 3, 0) : -Inf : 0.0 : 0.0 : True (1, 4, 3, 1) : -Inf : 0.0 : 0.0 : True (1, 4, 3, 2) : -Inf : 0.0 : 0.0 : True (1, 4, 3, 3) : -Inf : 0.0 : 0.0 : True (1, 4, 3, 4) : -Inf : 0.0 : 0.0 : True (1, 4, 4, 0) : -Inf : 0.0 : 0.0 : True (1, 4, 4, 1) : -Inf : 0.0 : 0.0 : True (1, 4, 4, 2) : -Inf : 0.0 : 0.0 : True (1, 4, 4, 3) : -Inf : 0.0 : 0.0 : True (1, 4, 4, 4) : -Inf : 0.0 : 0.0 : True (1, 5, 0, 0) : 0.0 : x[0,1]*x[0,5] : 0.0 : True (1, 5, 0, 1) : -Inf : 0.0 : 0.0 : True (1, 5, 0, 2) : -Inf : 0.0 : 0.0 : True (1, 5, 0, 3) : -Inf : 0.0 : 0.0 : True (1, 5, 0, 4) : -Inf : 0.0 : 0.0 : True (1, 5, 1, 0) : 0.0 : x[1,1]*x[0,5] : 0.0 : True (1, 5, 1, 1) : 0.0 : x[1,1]*x[1,5] : 0.0 : True (1, 5, 1, 2) : -Inf : 0.0 : 0.0 : True (1, 5, 1, 3) : -Inf : 0.0 : 0.0 : True (1, 5, 1, 4) : -Inf : 0.0 : 0.0 : True (1, 5, 2, 0) : 0.0 : x[2,1]*x[0,5] : 0.0 : True (1, 5, 2, 1) : 0.0 : x[2,1]*x[1,5] : 0.0 : True (1, 5, 2, 2) : 0.0 : x[2,1]*x[2,5] : 0.0 : True (1, 5, 2, 3) : -Inf : 0.0 : 0.0 : True (1, 5, 2, 4) : -Inf : 0.0 : 0.0 : True (1, 5, 3, 0) : 0.0 : x[3,1]*x[0,5] : 0.0 : True (1, 5, 3, 1) : 0.0 : x[3,1]*x[1,5] : 0.0 : True (1, 5, 3, 2) : 0.0 : x[3,1]*x[2,5] : 0.0 : True (1, 5, 3, 3) : 0.0 : x[3,1]*x[3,5] : 0.0 : True (1, 5, 3, 4) : -Inf : 0.0 : 0.0 : True (1, 5, 4, 0) : 0.0 : x[4,1]*x[0,5] : 0.0 : True (1, 5, 4, 1) : 0.0 : x[4,1]*x[1,5] : 0.0 : True (1, 5, 4, 2) : 0.0 : x[4,1]*x[2,5] : 0.0 : True (1, 5, 4, 3) : 0.0 : x[4,1]*x[3,5] : 0.0 : True (1, 5, 4, 4) : 0.0 : x[4,1]*x[4,5] : 0.0 : True (2, 0, 0, 0) : -Inf : 0.0 : 0.0 : True (2, 0, 0, 1) : -Inf : 0.0 : 0.0 : True (2, 0, 0, 2) : -Inf : 0.0 : 0.0 : True (2, 0, 0, 3) : -Inf : 0.0 : 0.0 : True (2, 0, 0, 4) : -Inf : 0.0 : 0.0 : True (2, 0, 1, 0) : -Inf : 0.0 : 0.0 : True (2, 0, 1, 1) : -Inf : 0.0 : 0.0 : True (2, 0, 1, 2) : -Inf : 0.0 : 0.0 : True (2, 0, 1, 3) : -Inf : 0.0 : 0.0 : True (2, 0, 1, 4) : -Inf : 0.0 : 0.0 : True (2, 0, 2, 0) : -Inf : 0.0 : 0.0 : True (2, 0, 2, 1) : -Inf : 0.0 : 0.0 : True (2, 0, 2, 2) : -Inf : 0.0 : 0.0 : True (2, 0, 2, 3) : -Inf : 0.0 : 0.0 : True (2, 0, 2, 4) : -Inf : 0.0 : 0.0 : True (2, 0, 3, 0) : -Inf : 0.0 : 0.0 : True (2, 0, 3, 1) : -Inf : 0.0 : 0.0 : True (2, 0, 3, 2) : -Inf : 0.0 : 0.0 : True (2, 0, 3, 3) : -Inf : 0.0 : 0.0 : True (2, 0, 3, 4) : -Inf : 0.0 : 0.0 : True (2, 0, 4, 0) : -Inf : 0.0 : 0.0 : True (2, 0, 4, 1) : -Inf : 0.0 : 0.0 : True (2, 0, 4, 2) : -Inf : 0.0 : 0.0 : True (2, 0, 4, 3) : -Inf : 0.0 : 0.0 : True (2, 0, 4, 4) : -Inf : 0.0 : 0.0 : True (2, 1, 0, 0) : -Inf : 0.0 : 0.0 : True (2, 1, 0, 1) : -Inf : 0.0 : 0.0 : True (2, 1, 0, 2) : -Inf : 0.0 : 0.0 : True (2, 1, 0, 3) : -Inf : 0.0 : 0.0 : True (2, 1, 0, 4) : -Inf : 0.0 : 0.0 : True (2, 1, 1, 0) : -Inf : 0.0 : 0.0 : True (2, 1, 1, 1) : -Inf : 0.0 : 0.0 : True (2, 1, 1, 2) : -Inf : 0.0 : 0.0 : True (2, 1, 1, 3) : -Inf : 0.0 : 0.0 : True (2, 1, 1, 4) : -Inf : 0.0 : 0.0 : True (2, 1, 2, 0) : -Inf : 0.0 : 0.0 : True (2, 1, 2, 1) : -Inf : 0.0 : 0.0 : True (2, 1, 2, 2) : -Inf : 0.0 : 0.0 : True (2, 1, 2, 3) : -Inf : 0.0 : 0.0 : True (2, 1, 2, 4) : -Inf : 0.0 : 0.0 : True (2, 1, 3, 0) : -Inf : 0.0 : 0.0 : True (2, 1, 3, 1) : -Inf : 0.0 : 0.0 : True (2, 1, 3, 2) : -Inf : 0.0 : 0.0 : True (2, 1, 3, 3) : -Inf : 0.0 : 0.0 : True (2, 1, 3, 4) : -Inf : 0.0 : 0.0 : True (2, 1, 4, 0) : -Inf : 0.0 : 0.0 : True (2, 1, 4, 1) : -Inf : 0.0 : 0.0 : True (2, 1, 4, 2) : -Inf : 0.0 : 0.0 : True (2, 1, 4, 3) : -Inf : 0.0 : 0.0 : True (2, 1, 4, 4) : -Inf : 0.0 : 0.0 : True (2, 2, 0, 0) : -Inf : 0.0 : 0.0 : True (2, 2, 0, 1) : -Inf : 0.0 : 0.0 : True (2, 2, 0, 2) : -Inf : 0.0 : 0.0 : True (2, 2, 0, 3) : -Inf : 0.0 : 0.0 : True (2, 2, 0, 4) : -Inf : 0.0 : 0.0 : True (2, 2, 1, 0) : -Inf : 0.0 : 0.0 : True (2, 2, 1, 1) : -Inf : 0.0 : 0.0 : True (2, 2, 1, 2) : -Inf : 0.0 : 0.0 : True (2, 2, 1, 3) : -Inf : 0.0 : 0.0 : True (2, 2, 1, 4) : -Inf : 0.0 : 0.0 : True (2, 2, 2, 0) : -Inf : 0.0 : 0.0 : True (2, 2, 2, 1) : -Inf : 0.0 : 0.0 : True (2, 2, 2, 2) : -Inf : 0.0 : 0.0 : True (2, 2, 2, 3) : -Inf : 0.0 : 0.0 : True (2, 2, 2, 4) : -Inf : 0.0 : 0.0 : True (2, 2, 3, 0) : -Inf : 0.0 : 0.0 : True (2, 2, 3, 1) : -Inf : 0.0 : 0.0 : True (2, 2, 3, 2) : -Inf : 0.0 : 0.0 : True (2, 2, 3, 3) : -Inf : 0.0 : 0.0 : True (2, 2, 3, 4) : -Inf : 0.0 : 0.0 : True (2, 2, 4, 0) : -Inf : 0.0 : 0.0 : True (2, 2, 4, 1) : -Inf : 0.0 : 0.0 : True (2, 2, 4, 2) : -Inf : 0.0 : 0.0 : True (2, 2, 4, 3) : -Inf : 0.0 : 0.0 : True (2, 2, 4, 4) : -Inf : 0.0 : 0.0 : True (2, 3, 0, 0) : -Inf : 0.0 : 0.0 : True (2, 3, 0, 1) : -Inf : 0.0 : 0.0 : True (2, 3, 0, 2) : -Inf : 0.0 : 0.0 : True (2, 3, 0, 3) : -Inf : 0.0 : 0.0 : True (2, 3, 0, 4) : -Inf : 0.0 : 0.0 : True (2, 3, 1, 0) : -Inf : 0.0 : 0.0 : True (2, 3, 1, 1) : -Inf : 0.0 : 0.0 : True (2, 3, 1, 2) : -Inf : 0.0 : 0.0 : True (2, 3, 1, 3) : -Inf : 0.0 : 0.0 : True (2, 3, 1, 4) : -Inf : 0.0 : 0.0 : True (2, 3, 2, 0) : -Inf : 0.0 : 0.0 : True (2, 3, 2, 1) : -Inf : 0.0 : 0.0 : True (2, 3, 2, 2) : -Inf : 0.0 : 0.0 : True (2, 3, 2, 3) : -Inf : 0.0 : 0.0 : True (2, 3, 2, 4) : -Inf : 0.0 : 0.0 : True (2, 3, 3, 0) : -Inf : 0.0 : 0.0 : True (2, 3, 3, 1) : -Inf : 0.0 : 0.0 : True (2, 3, 3, 2) : -Inf : 0.0 : 0.0 : True (2, 3, 3, 3) : -Inf : 0.0 : 0.0 : True (2, 3, 3, 4) : -Inf : 0.0 : 0.0 : True (2, 3, 4, 0) : -Inf : 0.0 : 0.0 : True (2, 3, 4, 1) : -Inf : 0.0 : 0.0 : True (2, 3, 4, 2) : -Inf : 0.0 : 0.0 : True (2, 3, 4, 3) : -Inf : 0.0 : 0.0 : True (2, 3, 4, 4) : -Inf : 0.0 : 0.0 : True (2, 4, 0, 0) : 0.0 : x[0,2]*x[0,4] : 0.0 : True (2, 4, 0, 1) : -Inf : 0.0 : 0.0 : True (2, 4, 0, 2) : -Inf : 0.0 : 0.0 : True (2, 4, 0, 3) : -Inf : 0.0 : 0.0 : True (2, 4, 0, 4) : -Inf : 0.0 : 0.0 : True (2, 4, 1, 0) : 0.0 : x[1,2]*x[0,4] : 0.0 : True (2, 4, 1, 1) : 0.0 : x[1,2]*x[1,4] : 0.0 : True (2, 4, 1, 2) : -Inf : 0.0 : 0.0 : True (2, 4, 1, 3) : -Inf : 0.0 : 0.0 : True (2, 4, 1, 4) : -Inf : 0.0 : 0.0 : True (2, 4, 2, 0) : 0.0 : x[2,2]*x[0,4] : 0.0 : True (2, 4, 2, 1) : 0.0 : x[2,2]*x[1,4] : 0.0 : True (2, 4, 2, 2) : 0.0 : x[2,2]*x[2,4] : 0.0 : True (2, 4, 2, 3) : -Inf : 0.0 : 0.0 : True (2, 4, 2, 4) : -Inf : 0.0 : 0.0 : True (2, 4, 3, 0) : 0.0 : x[3,2]*x[0,4] : 0.0 : True (2, 4, 3, 1) : 0.0 : x[3,2]*x[1,4] : 0.0 : True (2, 4, 3, 2) : 0.0 : x[3,2]*x[2,4] : 0.0 : True (2, 4, 3, 3) : 0.0 : x[3,2]*x[3,4] : 0.0 : True (2, 4, 3, 4) : -Inf : 0.0 : 0.0 : True (2, 4, 4, 0) : 0.0 : x[4,2]*x[0,4] : 0.0 : True (2, 4, 4, 1) : 0.0 : x[4,2]*x[1,4] : 0.0 : True (2, 4, 4, 2) : 0.0 : x[4,2]*x[2,4] : 0.0 : True (2, 4, 4, 3) : 0.0 : x[4,2]*x[3,4] : 0.0 : True (2, 4, 4, 4) : 0.0 : x[4,2]*x[4,4] : 0.0 : True (2, 5, 0, 0) : -Inf : 0.0 : 0.0 : True (2, 5, 0, 1) : -Inf : 0.0 : 0.0 : True (2, 5, 0, 2) : -Inf : 0.0 : 0.0 : True (2, 5, 0, 3) : -Inf : 0.0 : 0.0 : True (2, 5, 0, 4) : -Inf : 0.0 : 0.0 : True (2, 5, 1, 0) : -Inf : 0.0 : 0.0 : True (2, 5, 1, 1) : -Inf : 0.0 : 0.0 : True (2, 5, 1, 2) : -Inf : 0.0 : 0.0 : True (2, 5, 1, 3) : -Inf : 0.0 : 0.0 : True (2, 5, 1, 4) : -Inf : 0.0 : 0.0 : True (2, 5, 2, 0) : -Inf : 0.0 : 0.0 : True (2, 5, 2, 1) : -Inf : 0.0 : 0.0 : True (2, 5, 2, 2) : -Inf : 0.0 : 0.0 : True (2, 5, 2, 3) : -Inf : 0.0 : 0.0 : True (2, 5, 2, 4) : -Inf : 0.0 : 0.0 : True (2, 5, 3, 0) : -Inf : 0.0 : 0.0 : True (2, 5, 3, 1) : -Inf : 0.0 : 0.0 : True (2, 5, 3, 2) : -Inf : 0.0 : 0.0 : True (2, 5, 3, 3) : -Inf : 0.0 : 0.0 : True (2, 5, 3, 4) : -Inf : 0.0 : 0.0 : True (2, 5, 4, 0) : -Inf : 0.0 : 0.0 : True (2, 5, 4, 1) : -Inf : 0.0 : 0.0 : True (2, 5, 4, 2) : -Inf : 0.0 : 0.0 : True (2, 5, 4, 3) : -Inf : 0.0 : 0.0 : True (2, 5, 4, 4) : -Inf : 0.0 : 0.0 : True (3, 0, 0, 0) : -Inf : 0.0 : 0.0 : True (3, 0, 0, 1) : -Inf : 0.0 : 0.0 : True (3, 0, 0, 2) : -Inf : 0.0 : 0.0 : True (3, 0, 0, 3) : -Inf : 0.0 : 0.0 : True (3, 0, 0, 4) : -Inf : 0.0 : 0.0 : True (3, 0, 1, 0) : -Inf : 0.0 : 0.0 : True (3, 0, 1, 1) : -Inf : 0.0 : 0.0 : True (3, 0, 1, 2) : -Inf : 0.0 : 0.0 : True (3, 0, 1, 3) : -Inf : 0.0 : 0.0 : True (3, 0, 1, 4) : -Inf : 0.0 : 0.0 : True (3, 0, 2, 0) : -Inf : 0.0 : 0.0 : True (3, 0, 2, 1) : -Inf : 0.0 : 0.0 : True (3, 0, 2, 2) : -Inf : 0.0 : 0.0 : True (3, 0, 2, 3) : -Inf : 0.0 : 0.0 : True (3, 0, 2, 4) : -Inf : 0.0 : 0.0 : True (3, 0, 3, 0) : -Inf : 0.0 : 0.0 : True (3, 0, 3, 1) : -Inf : 0.0 : 0.0 : True (3, 0, 3, 2) : -Inf : 0.0 : 0.0 : True (3, 0, 3, 3) : -Inf : 0.0 : 0.0 : True (3, 0, 3, 4) : -Inf : 0.0 : 0.0 : True (3, 0, 4, 0) : -Inf : 0.0 : 0.0 : True (3, 0, 4, 1) : -Inf : 0.0 : 0.0 : True (3, 0, 4, 2) : -Inf : 0.0 : 0.0 : True (3, 0, 4, 3) : -Inf : 0.0 : 0.0 : True (3, 0, 4, 4) : -Inf : 0.0 : 0.0 : True (3, 1, 0, 0) : -Inf : 0.0 : 0.0 : True (3, 1, 0, 1) : -Inf : 0.0 : 0.0 : True (3, 1, 0, 2) : -Inf : 0.0 : 0.0 : True (3, 1, 0, 3) : -Inf : 0.0 : 0.0 : True (3, 1, 0, 4) : -Inf : 0.0 : 0.0 : True (3, 1, 1, 0) : -Inf : 0.0 : 0.0 : True (3, 1, 1, 1) : -Inf : 0.0 : 0.0 : True (3, 1, 1, 2) : -Inf : 0.0 : 0.0 : True (3, 1, 1, 3) : -Inf : 0.0 : 0.0 : True (3, 1, 1, 4) : -Inf : 0.0 : 0.0 : True (3, 1, 2, 0) : -Inf : 0.0 : 0.0 : True (3, 1, 2, 1) : -Inf : 0.0 : 0.0 : True (3, 1, 2, 2) : -Inf : 0.0 : 0.0 : True (3, 1, 2, 3) : -Inf : 0.0 : 0.0 : True (3, 1, 2, 4) : -Inf : 0.0 : 0.0 : True (3, 1, 3, 0) : -Inf : 0.0 : 0.0 : True (3, 1, 3, 1) : -Inf : 0.0 : 0.0 : True (3, 1, 3, 2) : -Inf : 0.0 : 0.0 : True (3, 1, 3, 3) : -Inf : 0.0 : 0.0 : True (3, 1, 3, 4) : -Inf : 0.0 : 0.0 : True (3, 1, 4, 0) : -Inf : 0.0 : 0.0 : True (3, 1, 4, 1) : -Inf : 0.0 : 0.0 : True (3, 1, 4, 2) : -Inf : 0.0 : 0.0 : True (3, 1, 4, 3) : -Inf : 0.0 : 0.0 : True (3, 1, 4, 4) : -Inf : 0.0 : 0.0 : True (3, 2, 0, 0) : -Inf : 0.0 : 0.0 : True (3, 2, 0, 1) : -Inf : 0.0 : 0.0 : True (3, 2, 0, 2) : -Inf : 0.0 : 0.0 : True (3, 2, 0, 3) : -Inf : 0.0 : 0.0 : True (3, 2, 0, 4) : -Inf : 0.0 : 0.0 : True (3, 2, 1, 0) : -Inf : 0.0 : 0.0 : True (3, 2, 1, 1) : -Inf : 0.0 : 0.0 : True (3, 2, 1, 2) : -Inf : 0.0 : 0.0 : True (3, 2, 1, 3) : -Inf : 0.0 : 0.0 : True (3, 2, 1, 4) : -Inf : 0.0 : 0.0 : True (3, 2, 2, 0) : -Inf : 0.0 : 0.0 : True (3, 2, 2, 1) : -Inf : 0.0 : 0.0 : True (3, 2, 2, 2) : -Inf : 0.0 : 0.0 : True (3, 2, 2, 3) : -Inf : 0.0 : 0.0 : True (3, 2, 2, 4) : -Inf : 0.0 : 0.0 : True (3, 2, 3, 0) : -Inf : 0.0 : 0.0 : True (3, 2, 3, 1) : -Inf : 0.0 : 0.0 : True (3, 2, 3, 2) : -Inf : 0.0 : 0.0 : True (3, 2, 3, 3) : -Inf : 0.0 : 0.0 : True (3, 2, 3, 4) : -Inf : 0.0 : 0.0 : True (3, 2, 4, 0) : -Inf : 0.0 : 0.0 : True (3, 2, 4, 1) : -Inf : 0.0 : 0.0 : True (3, 2, 4, 2) : -Inf : 0.0 : 0.0 : True (3, 2, 4, 3) : -Inf : 0.0 : 0.0 : True (3, 2, 4, 4) : -Inf : 0.0 : 0.0 : True (3, 3, 0, 0) : -Inf : 0.0 : 0.0 : True (3, 3, 0, 1) : -Inf : 0.0 : 0.0 : True (3, 3, 0, 2) : -Inf : 0.0 : 0.0 : True (3, 3, 0, 3) : -Inf : 0.0 : 0.0 : True (3, 3, 0, 4) : -Inf : 0.0 : 0.0 : True (3, 3, 1, 0) : -Inf : 0.0 : 0.0 : True (3, 3, 1, 1) : -Inf : 0.0 : 0.0 : True (3, 3, 1, 2) : -Inf : 0.0 : 0.0 : True (3, 3, 1, 3) : -Inf : 0.0 : 0.0 : True (3, 3, 1, 4) : -Inf : 0.0 : 0.0 : True (3, 3, 2, 0) : -Inf : 0.0 : 0.0 : True (3, 3, 2, 1) : -Inf : 0.0 : 0.0 : True (3, 3, 2, 2) : -Inf : 0.0 : 0.0 : True (3, 3, 2, 3) : -Inf : 0.0 : 0.0 : True (3, 3, 2, 4) : -Inf : 0.0 : 0.0 : True (3, 3, 3, 0) : -Inf : 0.0 : 0.0 : True (3, 3, 3, 1) : -Inf : 0.0 : 0.0 : True (3, 3, 3, 2) : -Inf : 0.0 : 0.0 : True (3, 3, 3, 3) : -Inf : 0.0 : 0.0 : True (3, 3, 3, 4) : -Inf : 0.0 : 0.0 : True (3, 3, 4, 0) : -Inf : 0.0 : 0.0 : True (3, 3, 4, 1) : -Inf : 0.0 : 0.0 : True (3, 3, 4, 2) : -Inf : 0.0 : 0.0 : True (3, 3, 4, 3) : -Inf : 0.0 : 0.0 : True (3, 3, 4, 4) : -Inf : 0.0 : 0.0 : True (3, 4, 0, 0) : 0.0 : x[0,3]*x[0,4] : 0.0 : True (3, 4, 0, 1) : -Inf : 0.0 : 0.0 : True (3, 4, 0, 2) : -Inf : 0.0 : 0.0 : True (3, 4, 0, 3) : -Inf : 0.0 : 0.0 : True (3, 4, 0, 4) : -Inf : 0.0 : 0.0 : True (3, 4, 1, 0) : 0.0 : x[1,3]*x[0,4] : 0.0 : True (3, 4, 1, 1) : 0.0 : x[1,3]*x[1,4] : 0.0 : True (3, 4, 1, 2) : -Inf : 0.0 : 0.0 : True (3, 4, 1, 3) : -Inf : 0.0 : 0.0 : True (3, 4, 1, 4) : -Inf : 0.0 : 0.0 : True (3, 4, 2, 0) : 0.0 : x[2,3]*x[0,4] : 0.0 : True (3, 4, 2, 1) : 0.0 : x[2,3]*x[1,4] : 0.0 : True (3, 4, 2, 2) : 0.0 : x[2,3]*x[2,4] : 0.0 : True (3, 4, 2, 3) : -Inf : 0.0 : 0.0 : True (3, 4, 2, 4) : -Inf : 0.0 : 0.0 : True (3, 4, 3, 0) : 0.0 : x[3,3]*x[0,4] : 0.0 : True (3, 4, 3, 1) : 0.0 : x[3,3]*x[1,4] : 0.0 : True (3, 4, 3, 2) : 0.0 : x[3,3]*x[2,4] : 0.0 : True (3, 4, 3, 3) : 0.0 : x[3,3]*x[3,4] : 0.0 : True (3, 4, 3, 4) : -Inf : 0.0 : 0.0 : True (3, 4, 4, 0) : 0.0 : x[4,3]*x[0,4] : 0.0 : True (3, 4, 4, 1) : 0.0 : x[4,3]*x[1,4] : 0.0 : True (3, 4, 4, 2) : 0.0 : x[4,3]*x[2,4] : 0.0 : True (3, 4, 4, 3) : 0.0 : x[4,3]*x[3,4] : 0.0 : True (3, 4, 4, 4) : 0.0 : x[4,3]*x[4,4] : 0.0 : True (3, 5, 0, 0) : -Inf : 0.0 : 0.0 : True (3, 5, 0, 1) : -Inf : 0.0 : 0.0 : True (3, 5, 0, 2) : -Inf : 0.0 : 0.0 : True (3, 5, 0, 3) : -Inf : 0.0 : 0.0 : True (3, 5, 0, 4) : -Inf : 0.0 : 0.0 : True (3, 5, 1, 0) : -Inf : 0.0 : 0.0 : True (3, 5, 1, 1) : -Inf : 0.0 : 0.0 : True (3, 5, 1, 2) : -Inf : 0.0 : 0.0 : True (3, 5, 1, 3) : -Inf : 0.0 : 0.0 : True (3, 5, 1, 4) : -Inf : 0.0 : 0.0 : True (3, 5, 2, 0) : -Inf : 0.0 : 0.0 : True (3, 5, 2, 1) : -Inf : 0.0 : 0.0 : True (3, 5, 2, 2) : -Inf : 0.0 : 0.0 : True (3, 5, 2, 3) : -Inf : 0.0 : 0.0 : True (3, 5, 2, 4) : -Inf : 0.0 : 0.0 : True (3, 5, 3, 0) : -Inf : 0.0 : 0.0 : True (3, 5, 3, 1) : -Inf : 0.0 : 0.0 : True (3, 5, 3, 2) : -Inf : 0.0 : 0.0 : True (3, 5, 3, 3) : -Inf : 0.0 : 0.0 : True (3, 5, 3, 4) : -Inf : 0.0 : 0.0 : True (3, 5, 4, 0) : -Inf : 0.0 : 0.0 : True (3, 5, 4, 1) : -Inf : 0.0 : 0.0 : True (3, 5, 4, 2) : -Inf : 0.0 : 0.0 : True (3, 5, 4, 3) : -Inf : 0.0 : 0.0 : True (3, 5, 4, 4) : -Inf : 0.0 : 0.0 : True (4, 0, 0, 0) : -Inf : 0.0 : 0.0 : True (4, 0, 0, 1) : -Inf : 0.0 : 0.0 : True (4, 0, 0, 2) : -Inf : 0.0 : 0.0 : True (4, 0, 0, 3) : -Inf : 0.0 : 0.0 : True (4, 0, 0, 4) : -Inf : 0.0 : 0.0 : True (4, 0, 1, 0) : -Inf : 0.0 : 0.0 : True (4, 0, 1, 1) : -Inf : 0.0 : 0.0 : True (4, 0, 1, 2) : -Inf : 0.0 : 0.0 : True (4, 0, 1, 3) : -Inf : 0.0 : 0.0 : True (4, 0, 1, 4) : -Inf : 0.0 : 0.0 : True (4, 0, 2, 0) : -Inf : 0.0 : 0.0 : True (4, 0, 2, 1) : -Inf : 0.0 : 0.0 : True (4, 0, 2, 2) : -Inf : 0.0 : 0.0 : True (4, 0, 2, 3) : -Inf : 0.0 : 0.0 : True (4, 0, 2, 4) : -Inf : 0.0 : 0.0 : True (4, 0, 3, 0) : -Inf : 0.0 : 0.0 : True (4, 0, 3, 1) : -Inf : 0.0 : 0.0 : True (4, 0, 3, 2) : -Inf : 0.0 : 0.0 : True (4, 0, 3, 3) : -Inf : 0.0 : 0.0 : True (4, 0, 3, 4) : -Inf : 0.0 : 0.0 : True (4, 0, 4, 0) : -Inf : 0.0 : 0.0 : True (4, 0, 4, 1) : -Inf : 0.0 : 0.0 : True (4, 0, 4, 2) : -Inf : 0.0 : 0.0 : True (4, 0, 4, 3) : -Inf : 0.0 : 0.0 : True (4, 0, 4, 4) : -Inf : 0.0 : 0.0 : True (4, 1, 0, 0) : -Inf : 0.0 : 0.0 : True (4, 1, 0, 1) : -Inf : 0.0 : 0.0 : True (4, 1, 0, 2) : -Inf : 0.0 : 0.0 : True (4, 1, 0, 3) : -Inf : 0.0 : 0.0 : True (4, 1, 0, 4) : -Inf : 0.0 : 0.0 : True (4, 1, 1, 0) : -Inf : 0.0 : 0.0 : True (4, 1, 1, 1) : -Inf : 0.0 : 0.0 : True (4, 1, 1, 2) : -Inf : 0.0 : 0.0 : True (4, 1, 1, 3) : -Inf : 0.0 : 0.0 : True (4, 1, 1, 4) : -Inf : 0.0 : 0.0 : True (4, 1, 2, 0) : -Inf : 0.0 : 0.0 : True (4, 1, 2, 1) : -Inf : 0.0 : 0.0 : True (4, 1, 2, 2) : -Inf : 0.0 : 0.0 : True (4, 1, 2, 3) : -Inf : 0.0 : 0.0 : True (4, 1, 2, 4) : -Inf : 0.0 : 0.0 : True (4, 1, 3, 0) : -Inf : 0.0 : 0.0 : True (4, 1, 3, 1) : -Inf : 0.0 : 0.0 : True (4, 1, 3, 2) : -Inf : 0.0 : 0.0 : True (4, 1, 3, 3) : -Inf : 0.0 : 0.0 : True (4, 1, 3, 4) : -Inf : 0.0 : 0.0 : True (4, 1, 4, 0) : -Inf : 0.0 : 0.0 : True (4, 1, 4, 1) : -Inf : 0.0 : 0.0 : True (4, 1, 4, 2) : -Inf : 0.0 : 0.0 : True (4, 1, 4, 3) : -Inf : 0.0 : 0.0 : True (4, 1, 4, 4) : -Inf : 0.0 : 0.0 : True (4, 2, 0, 0) : -Inf : 0.0 : 0.0 : True (4, 2, 0, 1) : -Inf : 0.0 : 0.0 : True (4, 2, 0, 2) : -Inf : 0.0 : 0.0 : True (4, 2, 0, 3) : -Inf : 0.0 : 0.0 : True (4, 2, 0, 4) : -Inf : 0.0 : 0.0 : True (4, 2, 1, 0) : -Inf : 0.0 : 0.0 : True (4, 2, 1, 1) : -Inf : 0.0 : 0.0 : True (4, 2, 1, 2) : -Inf : 0.0 : 0.0 : True (4, 2, 1, 3) : -Inf : 0.0 : 0.0 : True (4, 2, 1, 4) : -Inf : 0.0 : 0.0 : True (4, 2, 2, 0) : -Inf : 0.0 : 0.0 : True (4, 2, 2, 1) : -Inf : 0.0 : 0.0 : True (4, 2, 2, 2) : -Inf : 0.0 : 0.0 : True (4, 2, 2, 3) : -Inf : 0.0 : 0.0 : True (4, 2, 2, 4) : -Inf : 0.0 : 0.0 : True (4, 2, 3, 0) : -Inf : 0.0 : 0.0 : True (4, 2, 3, 1) : -Inf : 0.0 : 0.0 : True (4, 2, 3, 2) : -Inf : 0.0 : 0.0 : True (4, 2, 3, 3) : -Inf : 0.0 : 0.0 : True (4, 2, 3, 4) : -Inf : 0.0 : 0.0 : True (4, 2, 4, 0) : -Inf : 0.0 : 0.0 : True (4, 2, 4, 1) : -Inf : 0.0 : 0.0 : True (4, 2, 4, 2) : -Inf : 0.0 : 0.0 : True (4, 2, 4, 3) : -Inf : 0.0 : 0.0 : True (4, 2, 4, 4) : -Inf : 0.0 : 0.0 : True (4, 3, 0, 0) : -Inf : 0.0 : 0.0 : True (4, 3, 0, 1) : -Inf : 0.0 : 0.0 : True (4, 3, 0, 2) : -Inf : 0.0 : 0.0 : True (4, 3, 0, 3) : -Inf : 0.0 : 0.0 : True (4, 3, 0, 4) : -Inf : 0.0 : 0.0 : True (4, 3, 1, 0) : -Inf : 0.0 : 0.0 : True (4, 3, 1, 1) : -Inf : 0.0 : 0.0 : True (4, 3, 1, 2) : -Inf : 0.0 : 0.0 : True (4, 3, 1, 3) : -Inf : 0.0 : 0.0 : True (4, 3, 1, 4) : -Inf : 0.0 : 0.0 : True (4, 3, 2, 0) : -Inf : 0.0 : 0.0 : True (4, 3, 2, 1) : -Inf : 0.0 : 0.0 : True (4, 3, 2, 2) : -Inf : 0.0 : 0.0 : True (4, 3, 2, 3) : -Inf : 0.0 : 0.0 : True (4, 3, 2, 4) : -Inf : 0.0 : 0.0 : True (4, 3, 3, 0) : -Inf : 0.0 : 0.0 : True (4, 3, 3, 1) : -Inf : 0.0 : 0.0 : True (4, 3, 3, 2) : -Inf : 0.0 : 0.0 : True (4, 3, 3, 3) : -Inf : 0.0 : 0.0 : True (4, 3, 3, 4) : -Inf : 0.0 : 0.0 : True (4, 3, 4, 0) : -Inf : 0.0 : 0.0 : True (4, 3, 4, 1) : -Inf : 0.0 : 0.0 : True (4, 3, 4, 2) : -Inf : 0.0 : 0.0 : True (4, 3, 4, 3) : -Inf : 0.0 : 0.0 : True (4, 3, 4, 4) : -Inf : 0.0 : 0.0 : True (4, 4, 0, 0) : -Inf : 0.0 : 0.0 : True (4, 4, 0, 1) : -Inf : 0.0 : 0.0 : True (4, 4, 0, 2) : -Inf : 0.0 : 0.0 : True (4, 4, 0, 3) : -Inf : 0.0 : 0.0 : True (4, 4, 0, 4) : -Inf : 0.0 : 0.0 : True (4, 4, 1, 0) : -Inf : 0.0 : 0.0 : True (4, 4, 1, 1) : -Inf : 0.0 : 0.0 : True (4, 4, 1, 2) : -Inf : 0.0 : 0.0 : True (4, 4, 1, 3) : -Inf : 0.0 : 0.0 : True (4, 4, 1, 4) : -Inf : 0.0 : 0.0 : True (4, 4, 2, 0) : -Inf : 0.0 : 0.0 : True (4, 4, 2, 1) : -Inf : 0.0 : 0.0 : True (4, 4, 2, 2) : -Inf : 0.0 : 0.0 : True (4, 4, 2, 3) : -Inf : 0.0 : 0.0 : True (4, 4, 2, 4) : -Inf : 0.0 : 0.0 : True (4, 4, 3, 0) : -Inf : 0.0 : 0.0 : True (4, 4, 3, 1) : -Inf : 0.0 : 0.0 : True (4, 4, 3, 2) : -Inf : 0.0 : 0.0 : True (4, 4, 3, 3) : -Inf : 0.0 : 0.0 : True (4, 4, 3, 4) : -Inf : 0.0 : 0.0 : True (4, 4, 4, 0) : -Inf : 0.0 : 0.0 : True (4, 4, 4, 1) : -Inf : 0.0 : 0.0 : True (4, 4, 4, 2) : -Inf : 0.0 : 0.0 : True (4, 4, 4, 3) : -Inf : 0.0 : 0.0 : True (4, 4, 4, 4) : -Inf : 0.0 : 0.0 : True (4, 5, 0, 0) : 0.0 : x[0,4]*x[0,5] : 0.0 : True (4, 5, 0, 1) : -Inf : 0.0 : 0.0 : True (4, 5, 0, 2) : -Inf : 0.0 : 0.0 : True (4, 5, 0, 3) : -Inf : 0.0 : 0.0 : True (4, 5, 0, 4) : -Inf : 0.0 : 0.0 : True (4, 5, 1, 0) : 0.0 : x[1,4]*x[0,5] : 0.0 : True (4, 5, 1, 1) : 0.0 : x[1,4]*x[1,5] : 0.0 : True (4, 5, 1, 2) : -Inf : 0.0 : 0.0 : True (4, 5, 1, 3) : -Inf : 0.0 : 0.0 : True (4, 5, 1, 4) : -Inf : 0.0 : 0.0 : True (4, 5, 2, 0) : 0.0 : x[2,4]*x[0,5] : 0.0 : True (4, 5, 2, 1) : 0.0 : x[2,4]*x[1,5] : 0.0 : True (4, 5, 2, 2) : 0.0 : x[2,4]*x[2,5] : 0.0 : True (4, 5, 2, 3) : -Inf : 0.0 : 0.0 : True (4, 5, 2, 4) : -Inf : 0.0 : 0.0 : True (4, 5, 3, 0) : 0.0 : x[3,4]*x[0,5] : 0.0 : True (4, 5, 3, 1) : 0.0 : x[3,4]*x[1,5] : 0.0 : True (4, 5, 3, 2) : 0.0 : x[3,4]*x[2,5] : 0.0 : True (4, 5, 3, 3) : 0.0 : x[3,4]*x[3,5] : 0.0 : True (4, 5, 3, 4) : -Inf : 0.0 : 0.0 : True (4, 5, 4, 0) : 0.0 : x[4,4]*x[0,5] : 0.0 : True (4, 5, 4, 1) : 0.0 : x[4,4]*x[1,5] : 0.0 : True (4, 5, 4, 2) : 0.0 : x[4,4]*x[2,5] : 0.0 : True (4, 5, 4, 3) : 0.0 : x[4,4]*x[3,5] : 0.0 : True (4, 5, 4, 4) : 0.0 : x[4,4]*x[4,5] : 0.0 : True (5, 0, 0, 0) : -Inf : 0.0 : 0.0 : True (5, 0, 0, 1) : -Inf : 0.0 : 0.0 : True (5, 0, 0, 2) : -Inf : 0.0 : 0.0 : True (5, 0, 0, 3) : -Inf : 0.0 : 0.0 : True (5, 0, 0, 4) : -Inf : 0.0 : 0.0 : True (5, 0, 1, 0) : -Inf : 0.0 : 0.0 : True (5, 0, 1, 1) : -Inf : 0.0 : 0.0 : True (5, 0, 1, 2) : -Inf : 0.0 : 0.0 : True (5, 0, 1, 3) : -Inf : 0.0 : 0.0 : True (5, 0, 1, 4) : -Inf : 0.0 : 0.0 : True (5, 0, 2, 0) : -Inf : 0.0 : 0.0 : True (5, 0, 2, 1) : -Inf : 0.0 : 0.0 : True (5, 0, 2, 2) : -Inf : 0.0 : 0.0 : True (5, 0, 2, 3) : -Inf : 0.0 : 0.0 : True (5, 0, 2, 4) : -Inf : 0.0 : 0.0 : True (5, 0, 3, 0) : -Inf : 0.0 : 0.0 : True (5, 0, 3, 1) : -Inf : 0.0 : 0.0 : True (5, 0, 3, 2) : -Inf : 0.0 : 0.0 : True (5, 0, 3, 3) : -Inf : 0.0 : 0.0 : True (5, 0, 3, 4) : -Inf : 0.0 : 0.0 : True (5, 0, 4, 0) : -Inf : 0.0 : 0.0 : True (5, 0, 4, 1) : -Inf : 0.0 : 0.0 : True (5, 0, 4, 2) : -Inf : 0.0 : 0.0 : True (5, 0, 4, 3) : -Inf : 0.0 : 0.0 : True (5, 0, 4, 4) : -Inf : 0.0 : 0.0 : True (5, 1, 0, 0) : -Inf : 0.0 : 0.0 : True (5, 1, 0, 1) : -Inf : 0.0 : 0.0 : True (5, 1, 0, 2) : -Inf : 0.0 : 0.0 : True (5, 1, 0, 3) : -Inf : 0.0 : 0.0 : True (5, 1, 0, 4) : -Inf : 0.0 : 0.0 : True (5, 1, 1, 0) : -Inf : 0.0 : 0.0 : True (5, 1, 1, 1) : -Inf : 0.0 : 0.0 : True (5, 1, 1, 2) : -Inf : 0.0 : 0.0 : True (5, 1, 1, 3) : -Inf : 0.0 : 0.0 : True (5, 1, 1, 4) : -Inf : 0.0 : 0.0 : True (5, 1, 2, 0) : -Inf : 0.0 : 0.0 : True (5, 1, 2, 1) : -Inf : 0.0 : 0.0 : True (5, 1, 2, 2) : -Inf : 0.0 : 0.0 : True (5, 1, 2, 3) : -Inf : 0.0 : 0.0 : True (5, 1, 2, 4) : -Inf : 0.0 : 0.0 : True (5, 1, 3, 0) : -Inf : 0.0 : 0.0 : True (5, 1, 3, 1) : -Inf : 0.0 : 0.0 : True (5, 1, 3, 2) : -Inf : 0.0 : 0.0 : True (5, 1, 3, 3) : -Inf : 0.0 : 0.0 : True (5, 1, 3, 4) : -Inf : 0.0 : 0.0 : True (5, 1, 4, 0) : -Inf : 0.0 : 0.0 : True (5, 1, 4, 1) : -Inf : 0.0 : 0.0 : True (5, 1, 4, 2) : -Inf : 0.0 : 0.0 : True (5, 1, 4, 3) : -Inf : 0.0 : 0.0 : True (5, 1, 4, 4) : -Inf : 0.0 : 0.0 : True (5, 2, 0, 0) : -Inf : 0.0 : 0.0 : True (5, 2, 0, 1) : -Inf : 0.0 : 0.0 : True (5, 2, 0, 2) : -Inf : 0.0 : 0.0 : True (5, 2, 0, 3) : -Inf : 0.0 : 0.0 : True (5, 2, 0, 4) : -Inf : 0.0 : 0.0 : True (5, 2, 1, 0) : -Inf : 0.0 : 0.0 : True (5, 2, 1, 1) : -Inf : 0.0 : 0.0 : True (5, 2, 1, 2) : -Inf : 0.0 : 0.0 : True (5, 2, 1, 3) : -Inf : 0.0 : 0.0 : True (5, 2, 1, 4) : -Inf : 0.0 : 0.0 : True (5, 2, 2, 0) : -Inf : 0.0 : 0.0 : True (5, 2, 2, 1) : -Inf : 0.0 : 0.0 : True (5, 2, 2, 2) : -Inf : 0.0 : 0.0 : True (5, 2, 2, 3) : -Inf : 0.0 : 0.0 : True (5, 2, 2, 4) : -Inf : 0.0 : 0.0 : True (5, 2, 3, 0) : -Inf : 0.0 : 0.0 : True (5, 2, 3, 1) : -Inf : 0.0 : 0.0 : True (5, 2, 3, 2) : -Inf : 0.0 : 0.0 : True (5, 2, 3, 3) : -Inf : 0.0 : 0.0 : True (5, 2, 3, 4) : -Inf : 0.0 : 0.0 : True (5, 2, 4, 0) : -Inf : 0.0 : 0.0 : True (5, 2, 4, 1) : -Inf : 0.0 : 0.0 : True (5, 2, 4, 2) : -Inf : 0.0 : 0.0 : True (5, 2, 4, 3) : -Inf : 0.0 : 0.0 : True (5, 2, 4, 4) : -Inf : 0.0 : 0.0 : True (5, 3, 0, 0) : -Inf : 0.0 : 0.0 : True (5, 3, 0, 1) : -Inf : 0.0 : 0.0 : True (5, 3, 0, 2) : -Inf : 0.0 : 0.0 : True (5, 3, 0, 3) : -Inf : 0.0 : 0.0 : True (5, 3, 0, 4) : -Inf : 0.0 : 0.0 : True (5, 3, 1, 0) : -Inf : 0.0 : 0.0 : True (5, 3, 1, 1) : -Inf : 0.0 : 0.0 : True (5, 3, 1, 2) : -Inf : 0.0 : 0.0 : True (5, 3, 1, 3) : -Inf : 0.0 : 0.0 : True (5, 3, 1, 4) : -Inf : 0.0 : 0.0 : True (5, 3, 2, 0) : -Inf : 0.0 : 0.0 : True (5, 3, 2, 1) : -Inf : 0.0 : 0.0 : True (5, 3, 2, 2) : -Inf : 0.0 : 0.0 : True (5, 3, 2, 3) : -Inf : 0.0 : 0.0 : True (5, 3, 2, 4) : -Inf : 0.0 : 0.0 : True (5, 3, 3, 0) : -Inf : 0.0 : 0.0 : True (5, 3, 3, 1) : -Inf : 0.0 : 0.0 : True (5, 3, 3, 2) : -Inf : 0.0 : 0.0 : True (5, 3, 3, 3) : -Inf : 0.0 : 0.0 : True (5, 3, 3, 4) : -Inf : 0.0 : 0.0 : True (5, 3, 4, 0) : -Inf : 0.0 : 0.0 : True (5, 3, 4, 1) : -Inf : 0.0 : 0.0 : True (5, 3, 4, 2) : -Inf : 0.0 : 0.0 : True (5, 3, 4, 3) : -Inf : 0.0 : 0.0 : True (5, 3, 4, 4) : -Inf : 0.0 : 0.0 : True (5, 4, 0, 0) : -Inf : 0.0 : 0.0 : True (5, 4, 0, 1) : -Inf : 0.0 : 0.0 : True (5, 4, 0, 2) : -Inf : 0.0 : 0.0 : True (5, 4, 0, 3) : -Inf : 0.0 : 0.0 : True (5, 4, 0, 4) : -Inf : 0.0 : 0.0 : True (5, 4, 1, 0) : -Inf : 0.0 : 0.0 : True (5, 4, 1, 1) : -Inf : 0.0 : 0.0 : True (5, 4, 1, 2) : -Inf : 0.0 : 0.0 : True (5, 4, 1, 3) : -Inf : 0.0 : 0.0 : True (5, 4, 1, 4) : -Inf : 0.0 : 0.0 : True (5, 4, 2, 0) : -Inf : 0.0 : 0.0 : True (5, 4, 2, 1) : -Inf : 0.0 : 0.0 : True (5, 4, 2, 2) : -Inf : 0.0 : 0.0 : True (5, 4, 2, 3) : -Inf : 0.0 : 0.0 : True (5, 4, 2, 4) : -Inf : 0.0 : 0.0 : True (5, 4, 3, 0) : -Inf : 0.0 : 0.0 : True (5, 4, 3, 1) : -Inf : 0.0 : 0.0 : True (5, 4, 3, 2) : -Inf : 0.0 : 0.0 : True (5, 4, 3, 3) : -Inf : 0.0 : 0.0 : True (5, 4, 3, 4) : -Inf : 0.0 : 0.0 : True (5, 4, 4, 0) : -Inf : 0.0 : 0.0 : True (5, 4, 4, 1) : -Inf : 0.0 : 0.0 : True (5, 4, 4, 2) : -Inf : 0.0 : 0.0 : True (5, 4, 4, 3) : -Inf : 0.0 : 0.0 : True (5, 4, 4, 4) : -Inf : 0.0 : 0.0 : True (5, 5, 0, 0) : -Inf : 0.0 : 0.0 : True (5, 5, 0, 1) : -Inf : 0.0 : 0.0 : True (5, 5, 0, 2) : -Inf : 0.0 : 0.0 : True (5, 5, 0, 3) : -Inf : 0.0 : 0.0 : True (5, 5, 0, 4) : -Inf : 0.0 : 0.0 : True (5, 5, 1, 0) : -Inf : 0.0 : 0.0 : True (5, 5, 1, 1) : -Inf : 0.0 : 0.0 : True (5, 5, 1, 2) : -Inf : 0.0 : 0.0 : True (5, 5, 1, 3) : -Inf : 0.0 : 0.0 : True (5, 5, 1, 4) : -Inf : 0.0 : 0.0 : True (5, 5, 2, 0) : -Inf : 0.0 : 0.0 : True (5, 5, 2, 1) : -Inf : 0.0 : 0.0 : True (5, 5, 2, 2) : -Inf : 0.0 : 0.0 : True (5, 5, 2, 3) : -Inf : 0.0 : 0.0 : True (5, 5, 2, 4) : -Inf : 0.0 : 0.0 : True (5, 5, 3, 0) : -Inf : 0.0 : 0.0 : True (5, 5, 3, 1) : -Inf : 0.0 : 0.0 : True (5, 5, 3, 2) : -Inf : 0.0 : 0.0 : True (5, 5, 3, 3) : -Inf : 0.0 : 0.0 : True (5, 5, 3, 4) : -Inf : 0.0 : 0.0 : True (5, 5, 4, 0) : -Inf : 0.0 : 0.0 : True (5, 5, 4, 1) : -Inf : 0.0 : 0.0 : True (5, 5, 4, 2) : -Inf : 0.0 : 0.0 : True (5, 5, 4, 3) : -Inf : 0.0 : 0.0 : True (5, 5, 4, 4) : -Inf : 0.0 : 0.0 : True 6 Declarations: x all_works_are_done capacity_is_valid works_done_by_their_order eliminating_rule cost ``` ```python theme={null} from classiq.applications.combinatorial_optimization import OptimizerConfig, QAOAConfig qaoa_config = QAOAConfig(num_layers=8, penalty_energy=20.0) optimizer_config = OptimizerConfig(max_iteration=1, alpha_cvar=0.6) qmod_large = construct_combinatorial_optimization_model( pyo_model=tasks_model_large, qaoa_config=qaoa_config, optimizer_config=optimizer_config, ) ``` ```python theme={null} qprog_large = synthesize(qmod_large) show(qprog_large) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/39FfeKokUbzIq6HFfGFArUCVNsL ``` **Output:** ``` https://platform.classiq.io/circuit/39FfeKokUbzIq6HFfGFArUCVNsL?login=True&version=17 ``` ```python theme={null} result_large = execute(qprog_large).result_value() ``` As the search space here is much larger and involves many qubits, the optimizer takes much more time and might not converge to a legal solution. Print the optimization results: ```python theme={null} import pandas as pd from classiq.applications.combinatorial_optimization import ( get_optimization_solution_from_pyo, ) solution = get_optimization_solution_from_pyo( tasks_model_large, vqe_result=result_large, penalty_energy=qaoa_config.penalty_energy, ) optimization_result = pd.DataFrame.from_records(solution) optimization_result.sort_values(by="cost", ascending=True).head(5) ``` | | probability | cost | solution | count | | ---- | ----------- | ----- | -------------------------------------------------- | ----- | | 217 | 0.000488 | 60.0 | \[1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, ... | 1 | | 1009 | 0.000488 | 80.0 | \[1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, ... | 1 | | 265 | 0.000488 | 83.0 | \[1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, ... | 1 | | 934 | 0.000488 | 100.0 | \[1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, ... | 1 | | 1265 | 0.000488 | 104.0 | \[0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, ... | 1 | ```python theme={null} idx = optimization_result.cost.idxmin() print( "x =", optimization_result.solution[idx], ", cost =", optimization_result.cost[idx] ) ``` **Output:** ``` x = [1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0] , cost = 60.0 ``` And the histogram: ```python theme={null} optimization_result.hist("cost", weights=optimization_result["probability"]) ``` **Output:** ``` array([[]], dtype=object) ``` output This is the best solution: ```python theme={null} qaoa_solution_large = np.array( optimization_result.solution[optimization_result.cost.idxmin()] ).reshape(num_timeslots, len(G.nodes)) plot_workflow(qaoa_solution_large) ``` output # ## Classical Solution for the Large Problem ```python theme={null} from pyomo.opt import SolverFactory solver = SolverFactory("couenne") solver.solve(tasks_model_large) tasks_model_large.display() ``` **Output:** ``` Model task_scheduling Variables: x : Size=30, Index={0, 1, 2, 3, 4}*{0, 1, 2, 3, 4, 5} Key : Lower : Value : Upper : Fixed : Stale : Domain (0, 0) : 0 : 1.0 : 1 : False : False : Binary (0, 1) : 0 : 0.0 : 1 : False : False : Binary (0, 2) : 0 : 0.0 : 1 : False : False : Binary (0, 3) : 0 : 0.0 : 1 : False : False : Binary (0, 4) : 0 : 0.0 : 1 : False : False : Binary (0, 5) : 0 : 0.0 : 1 : False : False : Binary (1, 0) : 0 : 0.0 : 1 : False : False : Binary (1, 1) : 0 : 1.0 : 1 : False : False : Binary (1, 2) : 0 : 0.0 : 1 : False : False : Binary (1, 3) : 0 : 0.0 : 1 : False : False : Binary (1, 4) : 0 : 0.0 : 1 : False : False : Binary (1, 5) : 0 : 0.0 : 1 : False : False : Binary (2, 0) : 0 : 0.0 : 1 : False : False : Binary (2, 1) : 0 : -8.876268172259907e-17 : 1 : False : False : Binary (2, 2) : 0 : 1.0 : 1 : False : False : Binary (2, 3) : 0 : 1.0 : 1 : False : False : Binary (2, 4) : 0 : -1.000088900582341e-12 : 1 : False : False : Binary (2, 5) : 0 : 0.0 : 1 : False : False : Binary (3, 0) : 0 : 0.0 : 1 : False : False : Binary (3, 1) : 0 : 0.0 : 1 : False : False : Binary (3, 2) : 0 : 0.0 : 1 : False : False : Binary (3, 3) : 0 : 0.0 : 1 : False : False : Binary (3, 4) : 0 : 1.000000000001 : 1 : False : False : Binary (3, 5) : 0 : 0.0 : 1 : False : False : Binary (4, 0) : 0 : 0.0 : 1 : False : False : Binary (4, 1) : 0 : 8.876268172259907e-17 : 1 : False : False : Binary (4, 2) : 0 : 0.0 : 1 : False : False : Binary (4, 3) : 0 : 0.0 : 1 : False : False : Binary (4, 4) : 0 : 0.0 : 1 : False : False : Binary (4, 5) : 0 : 1.0 : 1 : False : False : Binary Objectives: cost : Size=1, Index=None, Active=True Key : Active : Value None : True : 5.0 Constraints: all_works_are_done : Size=6 Key : Lower : Body : Upper 0 : 1.0 : 1.0 : 1.0 1 : 1.0 : 1.0 : 1.0 2 : 1.0 : 1.0 : 1.0 3 : 1.0 : 1.0 : 1.0 4 : 1.0 : 1.0 : 1.0 5 : 1.0 : 1.0 : 1.0 capacity_is_valid : Size=5 Key : Lower : Body : Upper 0 : None : 1.0 : 1.0 1 : None : 3.0 : 3.0 2 : None : 3.999999999999 : 4.0 3 : None : 1.000000000001 : 3.0 4 : None : 1.0000000000000002 : 1.0 works_done_by_their_order : Size=900 Key : Lower : Body : Upper (0, 0, 0, 0) : None : 0.0 : 0.0 (0, 0, 0, 1) : None : 0.0 : 0.0 (0, 0, 0, 2) : None : 0.0 : 0.0 (0, 0, 0, 3) : None : 0.0 : 0.0 (0, 0, 0, 4) : None : 0.0 : 0.0 (0, 0, 1, 0) : None : 0.0 : 0.0 (0, 0, 1, 1) : None : 0.0 : 0.0 (0, 0, 1, 2) : None : 0.0 : 0.0 (0, 0, 1, 3) : None : 0.0 : 0.0 (0, 0, 1, 4) : None : 0.0 : 0.0 (0, 0, 2, 0) : None : 0.0 : 0.0 (0, 0, 2, 1) : None : 0.0 : 0.0 (0, 0, 2, 2) : None : 0.0 : 0.0 (0, 0, 2, 3) : None : 0.0 : 0.0 (0, 0, 2, 4) : None : 0.0 : 0.0 (0, 0, 3, 0) : None : 0.0 : 0.0 (0, 0, 3, 1) : None : 0.0 : 0.0 (0, 0, 3, 2) : None : 0.0 : 0.0 (0, 0, 3, 3) : None : 0.0 : 0.0 (0, 0, 3, 4) : None : 0.0 : 0.0 (0, 0, 4, 0) : None : 0.0 : 0.0 (0, 0, 4, 1) : None : 0.0 : 0.0 (0, 0, 4, 2) : None : 0.0 : 0.0 (0, 0, 4, 3) : None : 0.0 : 0.0 (0, 0, 4, 4) : None : 0.0 : 0.0 (0, 1, 0, 0) : 0.0 : 0.0 : 0.0 (0, 1, 0, 1) : None : 0.0 : 0.0 (0, 1, 0, 2) : None : 0.0 : 0.0 (0, 1, 0, 3) : None : 0.0 : 0.0 (0, 1, 0, 4) : None : 0.0 : 0.0 (0, 1, 1, 0) : 0.0 : 0.0 : 0.0 (0, 1, 1, 1) : 0.0 : 0.0 : 0.0 (0, 1, 1, 2) : None : 0.0 : 0.0 (0, 1, 1, 3) : None : 0.0 : 0.0 (0, 1, 1, 4) : None : 0.0 : 0.0 (0, 1, 2, 0) : 0.0 : 0.0 : 0.0 (0, 1, 2, 1) : 0.0 : 0.0 : 0.0 (0, 1, 2, 2) : 0.0 : -0.0 : 0.0 (0, 1, 2, 3) : None : 0.0 : 0.0 (0, 1, 2, 4) : None : 0.0 : 0.0 (0, 1, 3, 0) : 0.0 : 0.0 : 0.0 (0, 1, 3, 1) : 0.0 : 0.0 : 0.0 (0, 1, 3, 2) : 0.0 : -0.0 : 0.0 (0, 1, 3, 3) : 0.0 : 0.0 : 0.0 (0, 1, 3, 4) : None : 0.0 : 0.0 (0, 1, 4, 0) : 0.0 : 0.0 : 0.0 (0, 1, 4, 1) : 0.0 : 0.0 : 0.0 (0, 1, 4, 2) : 0.0 : -0.0 : 0.0 (0, 1, 4, 3) : 0.0 : 0.0 : 0.0 (0, 1, 4, 4) : 0.0 : 0.0 : 0.0 (0, 2, 0, 0) : 0.0 : 0.0 : 0.0 (0, 2, 0, 1) : None : 0.0 : 0.0 (0, 2, 0, 2) : None : 0.0 : 0.0 (0, 2, 0, 3) : None : 0.0 : 0.0 (0, 2, 0, 4) : None : 0.0 : 0.0 (0, 2, 1, 0) : 0.0 : 0.0 : 0.0 (0, 2, 1, 1) : 0.0 : 0.0 : 0.0 (0, 2, 1, 2) : None : 0.0 : 0.0 (0, 2, 1, 3) : None : 0.0 : 0.0 (0, 2, 1, 4) : None : 0.0 : 0.0 (0, 2, 2, 0) : 0.0 : 0.0 : 0.0 (0, 2, 2, 1) : 0.0 : 0.0 : 0.0 (0, 2, 2, 2) : 0.0 : 0.0 : 0.0 (0, 2, 2, 3) : None : 0.0 : 0.0 (0, 2, 2, 4) : None : 0.0 : 0.0 (0, 2, 3, 0) : 0.0 : 0.0 : 0.0 (0, 2, 3, 1) : 0.0 : 0.0 : 0.0 (0, 2, 3, 2) : 0.0 : 0.0 : 0.0 (0, 2, 3, 3) : 0.0 : 0.0 : 0.0 (0, 2, 3, 4) : None : 0.0 : 0.0 (0, 2, 4, 0) : 0.0 : 0.0 : 0.0 (0, 2, 4, 1) : 0.0 : 0.0 : 0.0 (0, 2, 4, 2) : 0.0 : 0.0 : 0.0 (0, 2, 4, 3) : 0.0 : 0.0 : 0.0 (0, 2, 4, 4) : 0.0 : 0.0 : 0.0 (0, 3, 0, 0) : 0.0 : 0.0 : 0.0 (0, 3, 0, 1) : None : 0.0 : 0.0 (0, 3, 0, 2) : None : 0.0 : 0.0 (0, 3, 0, 3) : None : 0.0 : 0.0 (0, 3, 0, 4) : None : 0.0 : 0.0 (0, 3, 1, 0) : 0.0 : 0.0 : 0.0 (0, 3, 1, 1) : 0.0 : 0.0 : 0.0 (0, 3, 1, 2) : None : 0.0 : 0.0 (0, 3, 1, 3) : None : 0.0 : 0.0 (0, 3, 1, 4) : None : 0.0 : 0.0 (0, 3, 2, 0) : 0.0 : 0.0 : 0.0 (0, 3, 2, 1) : 0.0 : 0.0 : 0.0 (0, 3, 2, 2) : 0.0 : 0.0 : 0.0 (0, 3, 2, 3) : None : 0.0 : 0.0 (0, 3, 2, 4) : None : 0.0 : 0.0 (0, 3, 3, 0) : 0.0 : 0.0 : 0.0 (0, 3, 3, 1) : 0.0 : 0.0 : 0.0 (0, 3, 3, 2) : 0.0 : 0.0 : 0.0 (0, 3, 3, 3) : 0.0 : 0.0 : 0.0 (0, 3, 3, 4) : None : 0.0 : 0.0 (0, 3, 4, 0) : 0.0 : 0.0 : 0.0 (0, 3, 4, 1) : 0.0 : 0.0 : 0.0 (0, 3, 4, 2) : 0.0 : 0.0 : 0.0 (0, 3, 4, 3) : 0.0 : 0.0 : 0.0 (0, 3, 4, 4) : 0.0 : 0.0 : 0.0 (0, 4, 0, 0) : None : 0.0 : 0.0 (0, 4, 0, 1) : None : 0.0 : 0.0 (0, 4, 0, 2) : None : 0.0 : 0.0 (0, 4, 0, 3) : None : 0.0 : 0.0 (0, 4, 0, 4) : None : 0.0 : 0.0 (0, 4, 1, 0) : None : 0.0 : 0.0 (0, 4, 1, 1) : None : 0.0 : 0.0 (0, 4, 1, 2) : None : 0.0 : 0.0 (0, 4, 1, 3) : None : 0.0 : 0.0 (0, 4, 1, 4) : None : 0.0 : 0.0 (0, 4, 2, 0) : None : 0.0 : 0.0 (0, 4, 2, 1) : None : 0.0 : 0.0 (0, 4, 2, 2) : None : 0.0 : 0.0 (0, 4, 2, 3) : None : 0.0 : 0.0 (0, 4, 2, 4) : None : 0.0 : 0.0 (0, 4, 3, 0) : None : 0.0 : 0.0 (0, 4, 3, 1) : None : 0.0 : 0.0 (0, 4, 3, 2) : None : 0.0 : 0.0 (0, 4, 3, 3) : None : 0.0 : 0.0 (0, 4, 3, 4) : None : 0.0 : 0.0 (0, 4, 4, 0) : None : 0.0 : 0.0 (0, 4, 4, 1) : None : 0.0 : 0.0 (0, 4, 4, 2) : None : 0.0 : 0.0 (0, 4, 4, 3) : None : 0.0 : 0.0 (0, 4, 4, 4) : None : 0.0 : 0.0 (0, 5, 0, 0) : None : 0.0 : 0.0 (0, 5, 0, 1) : None : 0.0 : 0.0 (0, 5, 0, 2) : None : 0.0 : 0.0 (0, 5, 0, 3) : None : 0.0 : 0.0 (0, 5, 0, 4) : None : 0.0 : 0.0 (0, 5, 1, 0) : None : 0.0 : 0.0 (0, 5, 1, 1) : None : 0.0 : 0.0 (0, 5, 1, 2) : None : 0.0 : 0.0 (0, 5, 1, 3) : None : 0.0 : 0.0 (0, 5, 1, 4) : None : 0.0 : 0.0 (0, 5, 2, 0) : None : 0.0 : 0.0 (0, 5, 2, 1) : None : 0.0 : 0.0 (0, 5, 2, 2) : None : 0.0 : 0.0 (0, 5, 2, 3) : None : 0.0 : 0.0 (0, 5, 2, 4) : None : 0.0 : 0.0 (0, 5, 3, 0) : None : 0.0 : 0.0 (0, 5, 3, 1) : None : 0.0 : 0.0 (0, 5, 3, 2) : None : 0.0 : 0.0 (0, 5, 3, 3) : None : 0.0 : 0.0 (0, 5, 3, 4) : None : 0.0 : 0.0 (0, 5, 4, 0) : None : 0.0 : 0.0 (0, 5, 4, 1) : None : 0.0 : 0.0 (0, 5, 4, 2) : None : 0.0 : 0.0 (0, 5, 4, 3) : None : 0.0 : 0.0 (0, 5, 4, 4) : None : 0.0 : 0.0 (1, 0, 0, 0) : None : 0.0 : 0.0 (1, 0, 0, 1) : None : 0.0 : 0.0 (1, 0, 0, 2) : None : 0.0 : 0.0 (1, 0, 0, 3) : None : 0.0 : 0.0 (1, 0, 0, 4) : None : 0.0 : 0.0 (1, 0, 1, 0) : None : 0.0 : 0.0 (1, 0, 1, 1) : None : 0.0 : 0.0 (1, 0, 1, 2) : None : 0.0 : 0.0 (1, 0, 1, 3) : None : 0.0 : 0.0 (1, 0, 1, 4) : None : 0.0 : 0.0 (1, 0, 2, 0) : None : 0.0 : 0.0 (1, 0, 2, 1) : None : 0.0 : 0.0 (1, 0, 2, 2) : None : 0.0 : 0.0 (1, 0, 2, 3) : None : 0.0 : 0.0 (1, 0, 2, 4) : None : 0.0 : 0.0 (1, 0, 3, 0) : None : 0.0 : 0.0 (1, 0, 3, 1) : None : 0.0 : 0.0 (1, 0, 3, 2) : None : 0.0 : 0.0 (1, 0, 3, 3) : None : 0.0 : 0.0 (1, 0, 3, 4) : None : 0.0 : 0.0 (1, 0, 4, 0) : None : 0.0 : 0.0 (1, 0, 4, 1) : None : 0.0 : 0.0 (1, 0, 4, 2) : None : 0.0 : 0.0 (1, 0, 4, 3) : None : 0.0 : 0.0 (1, 0, 4, 4) : None : 0.0 : 0.0 (1, 1, 0, 0) : None : 0.0 : 0.0 (1, 1, 0, 1) : None : 0.0 : 0.0 (1, 1, 0, 2) : None : 0.0 : 0.0 (1, 1, 0, 3) : None : 0.0 : 0.0 (1, 1, 0, 4) : None : 0.0 : 0.0 (1, 1, 1, 0) : None : 0.0 : 0.0 (1, 1, 1, 1) : None : 0.0 : 0.0 (1, 1, 1, 2) : None : 0.0 : 0.0 (1, 1, 1, 3) : None : 0.0 : 0.0 (1, 1, 1, 4) : None : 0.0 : 0.0 (1, 1, 2, 0) : None : 0.0 : 0.0 (1, 1, 2, 1) : None : 0.0 : 0.0 (1, 1, 2, 2) : None : 0.0 : 0.0 (1, 1, 2, 3) : None : 0.0 : 0.0 (1, 1, 2, 4) : None : 0.0 : 0.0 (1, 1, 3, 0) : None : 0.0 : 0.0 (1, 1, 3, 1) : None : 0.0 : 0.0 (1, 1, 3, 2) : None : 0.0 : 0.0 (1, 1, 3, 3) : None : 0.0 : 0.0 (1, 1, 3, 4) : None : 0.0 : 0.0 (1, 1, 4, 0) : None : 0.0 : 0.0 (1, 1, 4, 1) : None : 0.0 : 0.0 (1, 1, 4, 2) : None : 0.0 : 0.0 (1, 1, 4, 3) : None : 0.0 : 0.0 (1, 1, 4, 4) : None : 0.0 : 0.0 (1, 2, 0, 0) : None : 0.0 : 0.0 (1, 2, 0, 1) : None : 0.0 : 0.0 (1, 2, 0, 2) : None : 0.0 : 0.0 (1, 2, 0, 3) : None : 0.0 : 0.0 (1, 2, 0, 4) : None : 0.0 : 0.0 (1, 2, 1, 0) : None : 0.0 : 0.0 (1, 2, 1, 1) : None : 0.0 : 0.0 (1, 2, 1, 2) : None : 0.0 : 0.0 (1, 2, 1, 3) : None : 0.0 : 0.0 (1, 2, 1, 4) : None : 0.0 : 0.0 (1, 2, 2, 0) : None : 0.0 : 0.0 (1, 2, 2, 1) : None : 0.0 : 0.0 (1, 2, 2, 2) : None : 0.0 : 0.0 (1, 2, 2, 3) : None : 0.0 : 0.0 (1, 2, 2, 4) : None : 0.0 : 0.0 (1, 2, 3, 0) : None : 0.0 : 0.0 (1, 2, 3, 1) : None : 0.0 : 0.0 (1, 2, 3, 2) : None : 0.0 : 0.0 (1, 2, 3, 3) : None : 0.0 : 0.0 (1, 2, 3, 4) : None : 0.0 : 0.0 (1, 2, 4, 0) : None : 0.0 : 0.0 (1, 2, 4, 1) : None : 0.0 : 0.0 (1, 2, 4, 2) : None : 0.0 : 0.0 (1, 2, 4, 3) : None : 0.0 : 0.0 (1, 2, 4, 4) : None : 0.0 : 0.0 (1, 3, 0, 0) : None : 0.0 : 0.0 (1, 3, 0, 1) : None : 0.0 : 0.0 (1, 3, 0, 2) : None : 0.0 : 0.0 (1, 3, 0, 3) : None : 0.0 : 0.0 (1, 3, 0, 4) : None : 0.0 : 0.0 (1, 3, 1, 0) : None : 0.0 : 0.0 (1, 3, 1, 1) : None : 0.0 : 0.0 (1, 3, 1, 2) : None : 0.0 : 0.0 (1, 3, 1, 3) : None : 0.0 : 0.0 (1, 3, 1, 4) : None : 0.0 : 0.0 (1, 3, 2, 0) : None : 0.0 : 0.0 (1, 3, 2, 1) : None : 0.0 : 0.0 (1, 3, 2, 2) : None : 0.0 : 0.0 (1, 3, 2, 3) : None : 0.0 : 0.0 (1, 3, 2, 4) : None : 0.0 : 0.0 (1, 3, 3, 0) : None : 0.0 : 0.0 (1, 3, 3, 1) : None : 0.0 : 0.0 (1, 3, 3, 2) : None : 0.0 : 0.0 (1, 3, 3, 3) : None : 0.0 : 0.0 (1, 3, 3, 4) : None : 0.0 : 0.0 (1, 3, 4, 0) : None : 0.0 : 0.0 (1, 3, 4, 1) : None : 0.0 : 0.0 (1, 3, 4, 2) : None : 0.0 : 0.0 (1, 3, 4, 3) : None : 0.0 : 0.0 (1, 3, 4, 4) : None : 0.0 : 0.0 (1, 4, 0, 0) : None : 0.0 : 0.0 (1, 4, 0, 1) : None : 0.0 : 0.0 (1, 4, 0, 2) : None : 0.0 : 0.0 (1, 4, 0, 3) : None : 0.0 : 0.0 (1, 4, 0, 4) : None : 0.0 : 0.0 (1, 4, 1, 0) : None : 0.0 : 0.0 (1, 4, 1, 1) : None : 0.0 : 0.0 (1, 4, 1, 2) : None : 0.0 : 0.0 (1, 4, 1, 3) : None : 0.0 : 0.0 (1, 4, 1, 4) : None : 0.0 : 0.0 (1, 4, 2, 0) : None : 0.0 : 0.0 (1, 4, 2, 1) : None : 0.0 : 0.0 (1, 4, 2, 2) : None : 0.0 : 0.0 (1, 4, 2, 3) : None : 0.0 : 0.0 (1, 4, 2, 4) : None : 0.0 : 0.0 (1, 4, 3, 0) : None : 0.0 : 0.0 (1, 4, 3, 1) : None : 0.0 : 0.0 (1, 4, 3, 2) : None : 0.0 : 0.0 (1, 4, 3, 3) : None : 0.0 : 0.0 (1, 4, 3, 4) : None : 0.0 : 0.0 (1, 4, 4, 0) : None : 0.0 : 0.0 (1, 4, 4, 1) : None : 0.0 : 0.0 (1, 4, 4, 2) : None : 0.0 : 0.0 (1, 4, 4, 3) : None : 0.0 : 0.0 (1, 4, 4, 4) : None : 0.0 : 0.0 (1, 5, 0, 0) : 0.0 : 0.0 : 0.0 (1, 5, 0, 1) : None : 0.0 : 0.0 (1, 5, 0, 2) : None : 0.0 : 0.0 (1, 5, 0, 3) : None : 0.0 : 0.0 (1, 5, 0, 4) : None : 0.0 : 0.0 (1, 5, 1, 0) : 0.0 : 0.0 : 0.0 (1, 5, 1, 1) : 0.0 : 0.0 : 0.0 (1, 5, 1, 2) : None : 0.0 : 0.0 (1, 5, 1, 3) : None : 0.0 : 0.0 (1, 5, 1, 4) : None : 0.0 : 0.0 (1, 5, 2, 0) : 0.0 : -0.0 : 0.0 (1, 5, 2, 1) : 0.0 : -0.0 : 0.0 (1, 5, 2, 2) : 0.0 : -0.0 : 0.0 (1, 5, 2, 3) : None : 0.0 : 0.0 (1, 5, 2, 4) : None : 0.0 : 0.0 (1, 5, 3, 0) : 0.0 : 0.0 : 0.0 (1, 5, 3, 1) : 0.0 : 0.0 : 0.0 (1, 5, 3, 2) : 0.0 : 0.0 : 0.0 (1, 5, 3, 3) : 0.0 : 0.0 : 0.0 (1, 5, 3, 4) : None : 0.0 : 0.0 (1, 5, 4, 0) : 0.0 : 0.0 : 0.0 (1, 5, 4, 1) : 0.0 : 0.0 : 0.0 (1, 5, 4, 2) : 0.0 : 0.0 : 0.0 (1, 5, 4, 3) : 0.0 : 0.0 : 0.0 (1, 5, 4, 4) : 0.0 : 8.876268172259907e-17 : 0.0 (2, 0, 0, 0) : None : 0.0 : 0.0 (2, 0, 0, 1) : None : 0.0 : 0.0 (2, 0, 0, 2) : None : 0.0 : 0.0 (2, 0, 0, 3) : None : 0.0 : 0.0 (2, 0, 0, 4) : None : 0.0 : 0.0 (2, 0, 1, 0) : None : 0.0 : 0.0 (2, 0, 1, 1) : None : 0.0 : 0.0 (2, 0, 1, 2) : None : 0.0 : 0.0 (2, 0, 1, 3) : None : 0.0 : 0.0 (2, 0, 1, 4) : None : 0.0 : 0.0 (2, 0, 2, 0) : None : 0.0 : 0.0 (2, 0, 2, 1) : None : 0.0 : 0.0 (2, 0, 2, 2) : None : 0.0 : 0.0 (2, 0, 2, 3) : None : 0.0 : 0.0 (2, 0, 2, 4) : None : 0.0 : 0.0 (2, 0, 3, 0) : None : 0.0 : 0.0 (2, 0, 3, 1) : None : 0.0 : 0.0 (2, 0, 3, 2) : None : 0.0 : 0.0 (2, 0, 3, 3) : None : 0.0 : 0.0 (2, 0, 3, 4) : None : 0.0 : 0.0 (2, 0, 4, 0) : None : 0.0 : 0.0 (2, 0, 4, 1) : None : 0.0 : 0.0 (2, 0, 4, 2) : None : 0.0 : 0.0 (2, 0, 4, 3) : None : 0.0 : 0.0 (2, 0, 4, 4) : None : 0.0 : 0.0 (2, 1, 0, 0) : None : 0.0 : 0.0 (2, 1, 0, 1) : None : 0.0 : 0.0 (2, 1, 0, 2) : None : 0.0 : 0.0 (2, 1, 0, 3) : None : 0.0 : 0.0 (2, 1, 0, 4) : None : 0.0 : 0.0 (2, 1, 1, 0) : None : 0.0 : 0.0 (2, 1, 1, 1) : None : 0.0 : 0.0 (2, 1, 1, 2) : None : 0.0 : 0.0 (2, 1, 1, 3) : None : 0.0 : 0.0 (2, 1, 1, 4) : None : 0.0 : 0.0 (2, 1, 2, 0) : None : 0.0 : 0.0 (2, 1, 2, 1) : None : 0.0 : 0.0 (2, 1, 2, 2) : None : 0.0 : 0.0 (2, 1, 2, 3) : None : 0.0 : 0.0 (2, 1, 2, 4) : None : 0.0 : 0.0 (2, 1, 3, 0) : None : 0.0 : 0.0 (2, 1, 3, 1) : None : 0.0 : 0.0 (2, 1, 3, 2) : None : 0.0 : 0.0 (2, 1, 3, 3) : None : 0.0 : 0.0 (2, 1, 3, 4) : None : 0.0 : 0.0 (2, 1, 4, 0) : None : 0.0 : 0.0 (2, 1, 4, 1) : None : 0.0 : 0.0 (2, 1, 4, 2) : None : 0.0 : 0.0 (2, 1, 4, 3) : None : 0.0 : 0.0 (2, 1, 4, 4) : None : 0.0 : 0.0 (2, 2, 0, 0) : None : 0.0 : 0.0 (2, 2, 0, 1) : None : 0.0 : 0.0 (2, 2, 0, 2) : None : 0.0 : 0.0 (2, 2, 0, 3) : None : 0.0 : 0.0 (2, 2, 0, 4) : None : 0.0 : 0.0 (2, 2, 1, 0) : None : 0.0 : 0.0 (2, 2, 1, 1) : None : 0.0 : 0.0 (2, 2, 1, 2) : None : 0.0 : 0.0 (2, 2, 1, 3) : None : 0.0 : 0.0 (2, 2, 1, 4) : None : 0.0 : 0.0 (2, 2, 2, 0) : None : 0.0 : 0.0 (2, 2, 2, 1) : None : 0.0 : 0.0 (2, 2, 2, 2) : None : 0.0 : 0.0 (2, 2, 2, 3) : None : 0.0 : 0.0 (2, 2, 2, 4) : None : 0.0 : 0.0 (2, 2, 3, 0) : None : 0.0 : 0.0 (2, 2, 3, 1) : None : 0.0 : 0.0 (2, 2, 3, 2) : None : 0.0 : 0.0 (2, 2, 3, 3) : None : 0.0 : 0.0 (2, 2, 3, 4) : None : 0.0 : 0.0 (2, 2, 4, 0) : None : 0.0 : 0.0 (2, 2, 4, 1) : None : 0.0 : 0.0 (2, 2, 4, 2) : None : 0.0 : 0.0 (2, 2, 4, 3) : None : 0.0 : 0.0 (2, 2, 4, 4) : None : 0.0 : 0.0 (2, 3, 0, 0) : None : 0.0 : 0.0 (2, 3, 0, 1) : None : 0.0 : 0.0 (2, 3, 0, 2) : None : 0.0 : 0.0 (2, 3, 0, 3) : None : 0.0 : 0.0 (2, 3, 0, 4) : None : 0.0 : 0.0 (2, 3, 1, 0) : None : 0.0 : 0.0 (2, 3, 1, 1) : None : 0.0 : 0.0 (2, 3, 1, 2) : None : 0.0 : 0.0 (2, 3, 1, 3) : None : 0.0 : 0.0 (2, 3, 1, 4) : None : 0.0 : 0.0 (2, 3, 2, 0) : None : 0.0 : 0.0 (2, 3, 2, 1) : None : 0.0 : 0.0 (2, 3, 2, 2) : None : 0.0 : 0.0 (2, 3, 2, 3) : None : 0.0 : 0.0 (2, 3, 2, 4) : None : 0.0 : 0.0 (2, 3, 3, 0) : None : 0.0 : 0.0 (2, 3, 3, 1) : None : 0.0 : 0.0 (2, 3, 3, 2) : None : 0.0 : 0.0 (2, 3, 3, 3) : None : 0.0 : 0.0 (2, 3, 3, 4) : None : 0.0 : 0.0 (2, 3, 4, 0) : None : 0.0 : 0.0 (2, 3, 4, 1) : None : 0.0 : 0.0 (2, 3, 4, 2) : None : 0.0 : 0.0 (2, 3, 4, 3) : None : 0.0 : 0.0 (2, 3, 4, 4) : None : 0.0 : 0.0 (2, 4, 0, 0) : 0.0 : 0.0 : 0.0 (2, 4, 0, 1) : None : 0.0 : 0.0 (2, 4, 0, 2) : None : 0.0 : 0.0 (2, 4, 0, 3) : None : 0.0 : 0.0 (2, 4, 0, 4) : None : 0.0 : 0.0 (2, 4, 1, 0) : 0.0 : 0.0 : 0.0 (2, 4, 1, 1) : 0.0 : 0.0 : 0.0 (2, 4, 1, 2) : None : 0.0 : 0.0 (2, 4, 1, 3) : None : 0.0 : 0.0 (2, 4, 1, 4) : None : 0.0 : 0.0 (2, 4, 2, 0) : 0.0 : 0.0 : 0.0 (2, 4, 2, 1) : 0.0 : 0.0 : 0.0 (2, 4, 2, 2) : 0.0 : -1.000088900582341e-12 : 0.0 (2, 4, 2, 3) : None : 0.0 : 0.0 (2, 4, 2, 4) : None : 0.0 : 0.0 (2, 4, 3, 0) : 0.0 : 0.0 : 0.0 (2, 4, 3, 1) : 0.0 : 0.0 : 0.0 (2, 4, 3, 2) : 0.0 : -0.0 : 0.0 (2, 4, 3, 3) : 0.0 : 0.0 : 0.0 (2, 4, 3, 4) : None : 0.0 : 0.0 (2, 4, 4, 0) : 0.0 : 0.0 : 0.0 (2, 4, 4, 1) : 0.0 : 0.0 : 0.0 (2, 4, 4, 2) : 0.0 : -0.0 : 0.0 (2, 4, 4, 3) : 0.0 : 0.0 : 0.0 (2, 4, 4, 4) : 0.0 : 0.0 : 0.0 (2, 5, 0, 0) : None : 0.0 : 0.0 (2, 5, 0, 1) : None : 0.0 : 0.0 (2, 5, 0, 2) : None : 0.0 : 0.0 (2, 5, 0, 3) : None : 0.0 : 0.0 (2, 5, 0, 4) : None : 0.0 : 0.0 (2, 5, 1, 0) : None : 0.0 : 0.0 (2, 5, 1, 1) : None : 0.0 : 0.0 (2, 5, 1, 2) : None : 0.0 : 0.0 (2, 5, 1, 3) : None : 0.0 : 0.0 (2, 5, 1, 4) : None : 0.0 : 0.0 (2, 5, 2, 0) : None : 0.0 : 0.0 (2, 5, 2, 1) : None : 0.0 : 0.0 (2, 5, 2, 2) : None : 0.0 : 0.0 (2, 5, 2, 3) : None : 0.0 : 0.0 (2, 5, 2, 4) : None : 0.0 : 0.0 (2, 5, 3, 0) : None : 0.0 : 0.0 (2, 5, 3, 1) : None : 0.0 : 0.0 (2, 5, 3, 2) : None : 0.0 : 0.0 (2, 5, 3, 3) : None : 0.0 : 0.0 (2, 5, 3, 4) : None : 0.0 : 0.0 (2, 5, 4, 0) : None : 0.0 : 0.0 (2, 5, 4, 1) : None : 0.0 : 0.0 (2, 5, 4, 2) : None : 0.0 : 0.0 (2, 5, 4, 3) : None : 0.0 : 0.0 (2, 5, 4, 4) : None : 0.0 : 0.0 (3, 0, 0, 0) : None : 0.0 : 0.0 (3, 0, 0, 1) : None : 0.0 : 0.0 (3, 0, 0, 2) : None : 0.0 : 0.0 (3, 0, 0, 3) : None : 0.0 : 0.0 (3, 0, 0, 4) : None : 0.0 : 0.0 (3, 0, 1, 0) : None : 0.0 : 0.0 (3, 0, 1, 1) : None : 0.0 : 0.0 (3, 0, 1, 2) : None : 0.0 : 0.0 (3, 0, 1, 3) : None : 0.0 : 0.0 (3, 0, 1, 4) : None : 0.0 : 0.0 (3, 0, 2, 0) : None : 0.0 : 0.0 (3, 0, 2, 1) : None : 0.0 : 0.0 (3, 0, 2, 2) : None : 0.0 : 0.0 (3, 0, 2, 3) : None : 0.0 : 0.0 (3, 0, 2, 4) : None : 0.0 : 0.0 (3, 0, 3, 0) : None : 0.0 : 0.0 (3, 0, 3, 1) : None : 0.0 : 0.0 (3, 0, 3, 2) : None : 0.0 : 0.0 (3, 0, 3, 3) : None : 0.0 : 0.0 (3, 0, 3, 4) : None : 0.0 : 0.0 (3, 0, 4, 0) : None : 0.0 : 0.0 (3, 0, 4, 1) : None : 0.0 : 0.0 (3, 0, 4, 2) : None : 0.0 : 0.0 (3, 0, 4, 3) : None : 0.0 : 0.0 (3, 0, 4, 4) : None : 0.0 : 0.0 (3, 1, 0, 0) : None : 0.0 : 0.0 (3, 1, 0, 1) : None : 0.0 : 0.0 (3, 1, 0, 2) : None : 0.0 : 0.0 (3, 1, 0, 3) : None : 0.0 : 0.0 (3, 1, 0, 4) : None : 0.0 : 0.0 (3, 1, 1, 0) : None : 0.0 : 0.0 (3, 1, 1, 1) : None : 0.0 : 0.0 (3, 1, 1, 2) : None : 0.0 : 0.0 (3, 1, 1, 3) : None : 0.0 : 0.0 (3, 1, 1, 4) : None : 0.0 : 0.0 (3, 1, 2, 0) : None : 0.0 : 0.0 (3, 1, 2, 1) : None : 0.0 : 0.0 (3, 1, 2, 2) : None : 0.0 : 0.0 (3, 1, 2, 3) : None : 0.0 : 0.0 (3, 1, 2, 4) : None : 0.0 : 0.0 (3, 1, 3, 0) : None : 0.0 : 0.0 (3, 1, 3, 1) : None : 0.0 : 0.0 (3, 1, 3, 2) : None : 0.0 : 0.0 (3, 1, 3, 3) : None : 0.0 : 0.0 (3, 1, 3, 4) : None : 0.0 : 0.0 (3, 1, 4, 0) : None : 0.0 : 0.0 (3, 1, 4, 1) : None : 0.0 : 0.0 (3, 1, 4, 2) : None : 0.0 : 0.0 (3, 1, 4, 3) : None : 0.0 : 0.0 (3, 1, 4, 4) : None : 0.0 : 0.0 (3, 2, 0, 0) : None : 0.0 : 0.0 (3, 2, 0, 1) : None : 0.0 : 0.0 (3, 2, 0, 2) : None : 0.0 : 0.0 (3, 2, 0, 3) : None : 0.0 : 0.0 (3, 2, 0, 4) : None : 0.0 : 0.0 (3, 2, 1, 0) : None : 0.0 : 0.0 (3, 2, 1, 1) : None : 0.0 : 0.0 (3, 2, 1, 2) : None : 0.0 : 0.0 (3, 2, 1, 3) : None : 0.0 : 0.0 (3, 2, 1, 4) : None : 0.0 : 0.0 (3, 2, 2, 0) : None : 0.0 : 0.0 (3, 2, 2, 1) : None : 0.0 : 0.0 (3, 2, 2, 2) : None : 0.0 : 0.0 (3, 2, 2, 3) : None : 0.0 : 0.0 (3, 2, 2, 4) : None : 0.0 : 0.0 (3, 2, 3, 0) : None : 0.0 : 0.0 (3, 2, 3, 1) : None : 0.0 : 0.0 (3, 2, 3, 2) : None : 0.0 : 0.0 (3, 2, 3, 3) : None : 0.0 : 0.0 (3, 2, 3, 4) : None : 0.0 : 0.0 (3, 2, 4, 0) : None : 0.0 : 0.0 (3, 2, 4, 1) : None : 0.0 : 0.0 (3, 2, 4, 2) : None : 0.0 : 0.0 (3, 2, 4, 3) : None : 0.0 : 0.0 (3, 2, 4, 4) : None : 0.0 : 0.0 (3, 3, 0, 0) : None : 0.0 : 0.0 (3, 3, 0, 1) : None : 0.0 : 0.0 (3, 3, 0, 2) : None : 0.0 : 0.0 (3, 3, 0, 3) : None : 0.0 : 0.0 (3, 3, 0, 4) : None : 0.0 : 0.0 (3, 3, 1, 0) : None : 0.0 : 0.0 (3, 3, 1, 1) : None : 0.0 : 0.0 (3, 3, 1, 2) : None : 0.0 : 0.0 (3, 3, 1, 3) : None : 0.0 : 0.0 (3, 3, 1, 4) : None : 0.0 : 0.0 (3, 3, 2, 0) : None : 0.0 : 0.0 (3, 3, 2, 1) : None : 0.0 : 0.0 (3, 3, 2, 2) : None : 0.0 : 0.0 (3, 3, 2, 3) : None : 0.0 : 0.0 (3, 3, 2, 4) : None : 0.0 : 0.0 (3, 3, 3, 0) : None : 0.0 : 0.0 (3, 3, 3, 1) : None : 0.0 : 0.0 (3, 3, 3, 2) : None : 0.0 : 0.0 (3, 3, 3, 3) : None : 0.0 : 0.0 (3, 3, 3, 4) : None : 0.0 : 0.0 (3, 3, 4, 0) : None : 0.0 : 0.0 (3, 3, 4, 1) : None : 0.0 : 0.0 (3, 3, 4, 2) : None : 0.0 : 0.0 (3, 3, 4, 3) : None : 0.0 : 0.0 (3, 3, 4, 4) : None : 0.0 : 0.0 (3, 4, 0, 0) : 0.0 : 0.0 : 0.0 (3, 4, 0, 1) : None : 0.0 : 0.0 (3, 4, 0, 2) : None : 0.0 : 0.0 (3, 4, 0, 3) : None : 0.0 : 0.0 (3, 4, 0, 4) : None : 0.0 : 0.0 (3, 4, 1, 0) : 0.0 : 0.0 : 0.0 (3, 4, 1, 1) : 0.0 : 0.0 : 0.0 (3, 4, 1, 2) : None : 0.0 : 0.0 (3, 4, 1, 3) : None : 0.0 : 0.0 (3, 4, 1, 4) : None : 0.0 : 0.0 (3, 4, 2, 0) : 0.0 : 0.0 : 0.0 (3, 4, 2, 1) : 0.0 : 0.0 : 0.0 (3, 4, 2, 2) : 0.0 : -1.000088900582341e-12 : 0.0 (3, 4, 2, 3) : None : 0.0 : 0.0 (3, 4, 2, 4) : None : 0.0 : 0.0 (3, 4, 3, 0) : 0.0 : 0.0 : 0.0 (3, 4, 3, 1) : 0.0 : 0.0 : 0.0 (3, 4, 3, 2) : 0.0 : -0.0 : 0.0 (3, 4, 3, 3) : 0.0 : 0.0 : 0.0 (3, 4, 3, 4) : None : 0.0 : 0.0 (3, 4, 4, 0) : 0.0 : 0.0 : 0.0 (3, 4, 4, 1) : 0.0 : 0.0 : 0.0 (3, 4, 4, 2) : 0.0 : -0.0 : 0.0 (3, 4, 4, 3) : 0.0 : 0.0 : 0.0 (3, 4, 4, 4) : 0.0 : 0.0 : 0.0 (3, 5, 0, 0) : None : 0.0 : 0.0 (3, 5, 0, 1) : None : 0.0 : 0.0 (3, 5, 0, 2) : None : 0.0 : 0.0 (3, 5, 0, 3) : None : 0.0 : 0.0 (3, 5, 0, 4) : None : 0.0 : 0.0 (3, 5, 1, 0) : None : 0.0 : 0.0 (3, 5, 1, 1) : None : 0.0 : 0.0 (3, 5, 1, 2) : None : 0.0 : 0.0 (3, 5, 1, 3) : None : 0.0 : 0.0 (3, 5, 1, 4) : None : 0.0 : 0.0 (3, 5, 2, 0) : None : 0.0 : 0.0 (3, 5, 2, 1) : None : 0.0 : 0.0 (3, 5, 2, 2) : None : 0.0 : 0.0 (3, 5, 2, 3) : None : 0.0 : 0.0 (3, 5, 2, 4) : None : 0.0 : 0.0 (3, 5, 3, 0) : None : 0.0 : 0.0 (3, 5, 3, 1) : None : 0.0 : 0.0 (3, 5, 3, 2) : None : 0.0 : 0.0 (3, 5, 3, 3) : None : 0.0 : 0.0 (3, 5, 3, 4) : None : 0.0 : 0.0 (3, 5, 4, 0) : None : 0.0 : 0.0 (3, 5, 4, 1) : None : 0.0 : 0.0 (3, 5, 4, 2) : None : 0.0 : 0.0 (3, 5, 4, 3) : None : 0.0 : 0.0 (3, 5, 4, 4) : None : 0.0 : 0.0 (4, 0, 0, 0) : None : 0.0 : 0.0 (4, 0, 0, 1) : None : 0.0 : 0.0 (4, 0, 0, 2) : None : 0.0 : 0.0 (4, 0, 0, 3) : None : 0.0 : 0.0 (4, 0, 0, 4) : None : 0.0 : 0.0 (4, 0, 1, 0) : None : 0.0 : 0.0 (4, 0, 1, 1) : None : 0.0 : 0.0 (4, 0, 1, 2) : None : 0.0 : 0.0 (4, 0, 1, 3) : None : 0.0 : 0.0 (4, 0, 1, 4) : None : 0.0 : 0.0 (4, 0, 2, 0) : None : 0.0 : 0.0 (4, 0, 2, 1) : None : 0.0 : 0.0 (4, 0, 2, 2) : None : 0.0 : 0.0 (4, 0, 2, 3) : None : 0.0 : 0.0 (4, 0, 2, 4) : None : 0.0 : 0.0 (4, 0, 3, 0) : None : 0.0 : 0.0 (4, 0, 3, 1) : None : 0.0 : 0.0 (4, 0, 3, 2) : None : 0.0 : 0.0 (4, 0, 3, 3) : None : 0.0 : 0.0 (4, 0, 3, 4) : None : 0.0 : 0.0 (4, 0, 4, 0) : None : 0.0 : 0.0 (4, 0, 4, 1) : None : 0.0 : 0.0 (4, 0, 4, 2) : None : 0.0 : 0.0 (4, 0, 4, 3) : None : 0.0 : 0.0 (4, 0, 4, 4) : None : 0.0 : 0.0 (4, 1, 0, 0) : None : 0.0 : 0.0 (4, 1, 0, 1) : None : 0.0 : 0.0 (4, 1, 0, 2) : None : 0.0 : 0.0 (4, 1, 0, 3) : None : 0.0 : 0.0 (4, 1, 0, 4) : None : 0.0 : 0.0 (4, 1, 1, 0) : None : 0.0 : 0.0 (4, 1, 1, 1) : None : 0.0 : 0.0 (4, 1, 1, 2) : None : 0.0 : 0.0 (4, 1, 1, 3) : None : 0.0 : 0.0 (4, 1, 1, 4) : None : 0.0 : 0.0 (4, 1, 2, 0) : None : 0.0 : 0.0 (4, 1, 2, 1) : None : 0.0 : 0.0 (4, 1, 2, 2) : None : 0.0 : 0.0 (4, 1, 2, 3) : None : 0.0 : 0.0 (4, 1, 2, 4) : None : 0.0 : 0.0 (4, 1, 3, 0) : None : 0.0 : 0.0 (4, 1, 3, 1) : None : 0.0 : 0.0 (4, 1, 3, 2) : None : 0.0 : 0.0 (4, 1, 3, 3) : None : 0.0 : 0.0 (4, 1, 3, 4) : None : 0.0 : 0.0 (4, 1, 4, 0) : None : 0.0 : 0.0 (4, 1, 4, 1) : None : 0.0 : 0.0 (4, 1, 4, 2) : None : 0.0 : 0.0 (4, 1, 4, 3) : None : 0.0 : 0.0 (4, 1, 4, 4) : None : 0.0 : 0.0 (4, 2, 0, 0) : None : 0.0 : 0.0 (4, 2, 0, 1) : None : 0.0 : 0.0 (4, 2, 0, 2) : None : 0.0 : 0.0 (4, 2, 0, 3) : None : 0.0 : 0.0 (4, 2, 0, 4) : None : 0.0 : 0.0 (4, 2, 1, 0) : None : 0.0 : 0.0 (4, 2, 1, 1) : None : 0.0 : 0.0 (4, 2, 1, 2) : None : 0.0 : 0.0 (4, 2, 1, 3) : None : 0.0 : 0.0 (4, 2, 1, 4) : None : 0.0 : 0.0 (4, 2, 2, 0) : None : 0.0 : 0.0 (4, 2, 2, 1) : None : 0.0 : 0.0 (4, 2, 2, 2) : None : 0.0 : 0.0 (4, 2, 2, 3) : None : 0.0 : 0.0 (4, 2, 2, 4) : None : 0.0 : 0.0 (4, 2, 3, 0) : None : 0.0 : 0.0 (4, 2, 3, 1) : None : 0.0 : 0.0 (4, 2, 3, 2) : None : 0.0 : 0.0 (4, 2, 3, 3) : None : 0.0 : 0.0 (4, 2, 3, 4) : None : 0.0 : 0.0 (4, 2, 4, 0) : None : 0.0 : 0.0 (4, 2, 4, 1) : None : 0.0 : 0.0 (4, 2, 4, 2) : None : 0.0 : 0.0 (4, 2, 4, 3) : None : 0.0 : 0.0 (4, 2, 4, 4) : None : 0.0 : 0.0 (4, 3, 0, 0) : None : 0.0 : 0.0 (4, 3, 0, 1) : None : 0.0 : 0.0 (4, 3, 0, 2) : None : 0.0 : 0.0 (4, 3, 0, 3) : None : 0.0 : 0.0 (4, 3, 0, 4) : None : 0.0 : 0.0 (4, 3, 1, 0) : None : 0.0 : 0.0 (4, 3, 1, 1) : None : 0.0 : 0.0 (4, 3, 1, 2) : None : 0.0 : 0.0 (4, 3, 1, 3) : None : 0.0 : 0.0 (4, 3, 1, 4) : None : 0.0 : 0.0 (4, 3, 2, 0) : None : 0.0 : 0.0 (4, 3, 2, 1) : None : 0.0 : 0.0 (4, 3, 2, 2) : None : 0.0 : 0.0 (4, 3, 2, 3) : None : 0.0 : 0.0 (4, 3, 2, 4) : None : 0.0 : 0.0 (4, 3, 3, 0) : None : 0.0 : 0.0 (4, 3, 3, 1) : None : 0.0 : 0.0 (4, 3, 3, 2) : None : 0.0 : 0.0 (4, 3, 3, 3) : None : 0.0 : 0.0 (4, 3, 3, 4) : None : 0.0 : 0.0 (4, 3, 4, 0) : None : 0.0 : 0.0 (4, 3, 4, 1) : None : 0.0 : 0.0 (4, 3, 4, 2) : None : 0.0 : 0.0 (4, 3, 4, 3) : None : 0.0 : 0.0 (4, 3, 4, 4) : None : 0.0 : 0.0 (4, 4, 0, 0) : None : 0.0 : 0.0 (4, 4, 0, 1) : None : 0.0 : 0.0 (4, 4, 0, 2) : None : 0.0 : 0.0 (4, 4, 0, 3) : None : 0.0 : 0.0 (4, 4, 0, 4) : None : 0.0 : 0.0 (4, 4, 1, 0) : None : 0.0 : 0.0 (4, 4, 1, 1) : None : 0.0 : 0.0 (4, 4, 1, 2) : None : 0.0 : 0.0 (4, 4, 1, 3) : None : 0.0 : 0.0 (4, 4, 1, 4) : None : 0.0 : 0.0 (4, 4, 2, 0) : None : 0.0 : 0.0 (4, 4, 2, 1) : None : 0.0 : 0.0 (4, 4, 2, 2) : None : 0.0 : 0.0 (4, 4, 2, 3) : None : 0.0 : 0.0 (4, 4, 2, 4) : None : 0.0 : 0.0 (4, 4, 3, 0) : None : 0.0 : 0.0 (4, 4, 3, 1) : None : 0.0 : 0.0 (4, 4, 3, 2) : None : 0.0 : 0.0 (4, 4, 3, 3) : None : 0.0 : 0.0 (4, 4, 3, 4) : None : 0.0 : 0.0 (4, 4, 4, 0) : None : 0.0 : 0.0 (4, 4, 4, 1) : None : 0.0 : 0.0 (4, 4, 4, 2) : None : 0.0 : 0.0 (4, 4, 4, 3) : None : 0.0 : 0.0 (4, 4, 4, 4) : None : 0.0 : 0.0 (4, 5, 0, 0) : 0.0 : 0.0 : 0.0 (4, 5, 0, 1) : None : 0.0 : 0.0 (4, 5, 0, 2) : None : 0.0 : 0.0 (4, 5, 0, 3) : None : 0.0 : 0.0 (4, 5, 0, 4) : None : 0.0 : 0.0 (4, 5, 1, 0) : 0.0 : 0.0 : 0.0 (4, 5, 1, 1) : 0.0 : 0.0 : 0.0 (4, 5, 1, 2) : None : 0.0 : 0.0 (4, 5, 1, 3) : None : 0.0 : 0.0 (4, 5, 1, 4) : None : 0.0 : 0.0 (4, 5, 2, 0) : 0.0 : -0.0 : 0.0 (4, 5, 2, 1) : 0.0 : -0.0 : 0.0 (4, 5, 2, 2) : 0.0 : -0.0 : 0.0 (4, 5, 2, 3) : None : 0.0 : 0.0 (4, 5, 2, 4) : None : 0.0 : 0.0 (4, 5, 3, 0) : 0.0 : 0.0 : 0.0 (4, 5, 3, 1) : 0.0 : 0.0 : 0.0 (4, 5, 3, 2) : 0.0 : 0.0 : 0.0 (4, 5, 3, 3) : 0.0 : 0.0 : 0.0 (4, 5, 3, 4) : None : 0.0 : 0.0 (4, 5, 4, 0) : 0.0 : 0.0 : 0.0 (4, 5, 4, 1) : 0.0 : 0.0 : 0.0 (4, 5, 4, 2) : 0.0 : 0.0 : 0.0 (4, 5, 4, 3) : 0.0 : 0.0 : 0.0 (4, 5, 4, 4) : 0.0 : 0.0 : 0.0 (5, 0, 0, 0) : None : 0.0 : 0.0 (5, 0, 0, 1) : None : 0.0 : 0.0 (5, 0, 0, 2) : None : 0.0 : 0.0 (5, 0, 0, 3) : None : 0.0 : 0.0 (5, 0, 0, 4) : None : 0.0 : 0.0 (5, 0, 1, 0) : None : 0.0 : 0.0 (5, 0, 1, 1) : None : 0.0 : 0.0 (5, 0, 1, 2) : None : 0.0 : 0.0 (5, 0, 1, 3) : None : 0.0 : 0.0 (5, 0, 1, 4) : None : 0.0 : 0.0 (5, 0, 2, 0) : None : 0.0 : 0.0 (5, 0, 2, 1) : None : 0.0 : 0.0 (5, 0, 2, 2) : None : 0.0 : 0.0 (5, 0, 2, 3) : None : 0.0 : 0.0 (5, 0, 2, 4) : None : 0.0 : 0.0 (5, 0, 3, 0) : None : 0.0 : 0.0 (5, 0, 3, 1) : None : 0.0 : 0.0 (5, 0, 3, 2) : None : 0.0 : 0.0 (5, 0, 3, 3) : None : 0.0 : 0.0 (5, 0, 3, 4) : None : 0.0 : 0.0 (5, 0, 4, 0) : None : 0.0 : 0.0 (5, 0, 4, 1) : None : 0.0 : 0.0 (5, 0, 4, 2) : None : 0.0 : 0.0 (5, 0, 4, 3) : None : 0.0 : 0.0 (5, 0, 4, 4) : None : 0.0 : 0.0 (5, 1, 0, 0) : None : 0.0 : 0.0 (5, 1, 0, 1) : None : 0.0 : 0.0 (5, 1, 0, 2) : None : 0.0 : 0.0 (5, 1, 0, 3) : None : 0.0 : 0.0 (5, 1, 0, 4) : None : 0.0 : 0.0 (5, 1, 1, 0) : None : 0.0 : 0.0 (5, 1, 1, 1) : None : 0.0 : 0.0 (5, 1, 1, 2) : None : 0.0 : 0.0 (5, 1, 1, 3) : None : 0.0 : 0.0 (5, 1, 1, 4) : None : 0.0 : 0.0 (5, 1, 2, 0) : None : 0.0 : 0.0 (5, 1, 2, 1) : None : 0.0 : 0.0 (5, 1, 2, 2) : None : 0.0 : 0.0 (5, 1, 2, 3) : None : 0.0 : 0.0 (5, 1, 2, 4) : None : 0.0 : 0.0 (5, 1, 3, 0) : None : 0.0 : 0.0 (5, 1, 3, 1) : None : 0.0 : 0.0 (5, 1, 3, 2) : None : 0.0 : 0.0 (5, 1, 3, 3) : None : 0.0 : 0.0 (5, 1, 3, 4) : None : 0.0 : 0.0 (5, 1, 4, 0) : None : 0.0 : 0.0 (5, 1, 4, 1) : None : 0.0 : 0.0 (5, 1, 4, 2) : None : 0.0 : 0.0 (5, 1, 4, 3) : None : 0.0 : 0.0 (5, 1, 4, 4) : None : 0.0 : 0.0 (5, 2, 0, 0) : None : 0.0 : 0.0 (5, 2, 0, 1) : None : 0.0 : 0.0 (5, 2, 0, 2) : None : 0.0 : 0.0 (5, 2, 0, 3) : None : 0.0 : 0.0 (5, 2, 0, 4) : None : 0.0 : 0.0 (5, 2, 1, 0) : None : 0.0 : 0.0 (5, 2, 1, 1) : None : 0.0 : 0.0 (5, 2, 1, 2) : None : 0.0 : 0.0 (5, 2, 1, 3) : None : 0.0 : 0.0 (5, 2, 1, 4) : None : 0.0 : 0.0 (5, 2, 2, 0) : None : 0.0 : 0.0 (5, 2, 2, 1) : None : 0.0 : 0.0 (5, 2, 2, 2) : None : 0.0 : 0.0 (5, 2, 2, 3) : None : 0.0 : 0.0 (5, 2, 2, 4) : None : 0.0 : 0.0 (5, 2, 3, 0) : None : 0.0 : 0.0 (5, 2, 3, 1) : None : 0.0 : 0.0 (5, 2, 3, 2) : None : 0.0 : 0.0 (5, 2, 3, 3) : None : 0.0 : 0.0 (5, 2, 3, 4) : None : 0.0 : 0.0 (5, 2, 4, 0) : None : 0.0 : 0.0 (5, 2, 4, 1) : None : 0.0 : 0.0 (5, 2, 4, 2) : None : 0.0 : 0.0 (5, 2, 4, 3) : None : 0.0 : 0.0 (5, 2, 4, 4) : None : 0.0 : 0.0 (5, 3, 0, 0) : None : 0.0 : 0.0 (5, 3, 0, 1) : None : 0.0 : 0.0 (5, 3, 0, 2) : None : 0.0 : 0.0 (5, 3, 0, 3) : None : 0.0 : 0.0 (5, 3, 0, 4) : None : 0.0 : 0.0 (5, 3, 1, 0) : None : 0.0 : 0.0 (5, 3, 1, 1) : None : 0.0 : 0.0 (5, 3, 1, 2) : None : 0.0 : 0.0 (5, 3, 1, 3) : None : 0.0 : 0.0 (5, 3, 1, 4) : None : 0.0 : 0.0 (5, 3, 2, 0) : None : 0.0 : 0.0 (5, 3, 2, 1) : None : 0.0 : 0.0 (5, 3, 2, 2) : None : 0.0 : 0.0 (5, 3, 2, 3) : None : 0.0 : 0.0 (5, 3, 2, 4) : None : 0.0 : 0.0 (5, 3, 3, 0) : None : 0.0 : 0.0 (5, 3, 3, 1) : None : 0.0 : 0.0 (5, 3, 3, 2) : None : 0.0 : 0.0 (5, 3, 3, 3) : None : 0.0 : 0.0 (5, 3, 3, 4) : None : 0.0 : 0.0 (5, 3, 4, 0) : None : 0.0 : 0.0 (5, 3, 4, 1) : None : 0.0 : 0.0 (5, 3, 4, 2) : None : 0.0 : 0.0 (5, 3, 4, 3) : None : 0.0 : 0.0 (5, 3, 4, 4) : None : 0.0 : 0.0 (5, 4, 0, 0) : None : 0.0 : 0.0 (5, 4, 0, 1) : None : 0.0 : 0.0 (5, 4, 0, 2) : None : 0.0 : 0.0 (5, 4, 0, 3) : None : 0.0 : 0.0 (5, 4, 0, 4) : None : 0.0 : 0.0 (5, 4, 1, 0) : None : 0.0 : 0.0 (5, 4, 1, 1) : None : 0.0 : 0.0 (5, 4, 1, 2) : None : 0.0 : 0.0 (5, 4, 1, 3) : None : 0.0 : 0.0 (5, 4, 1, 4) : None : 0.0 : 0.0 (5, 4, 2, 0) : None : 0.0 : 0.0 (5, 4, 2, 1) : None : 0.0 : 0.0 (5, 4, 2, 2) : None : 0.0 : 0.0 (5, 4, 2, 3) : None : 0.0 : 0.0 (5, 4, 2, 4) : None : 0.0 : 0.0 (5, 4, 3, 0) : None : 0.0 : 0.0 (5, 4, 3, 1) : None : 0.0 : 0.0 (5, 4, 3, 2) : None : 0.0 : 0.0 (5, 4, 3, 3) : None : 0.0 : 0.0 (5, 4, 3, 4) : None : 0.0 : 0.0 (5, 4, 4, 0) : None : 0.0 : 0.0 (5, 4, 4, 1) : None : 0.0 : 0.0 (5, 4, 4, 2) : None : 0.0 : 0.0 (5, 4, 4, 3) : None : 0.0 : 0.0 (5, 4, 4, 4) : None : 0.0 : 0.0 (5, 5, 0, 0) : None : 0.0 : 0.0 (5, 5, 0, 1) : None : 0.0 : 0.0 (5, 5, 0, 2) : None : 0.0 : 0.0 (5, 5, 0, 3) : None : 0.0 : 0.0 (5, 5, 0, 4) : None : 0.0 : 0.0 (5, 5, 1, 0) : None : 0.0 : 0.0 (5, 5, 1, 1) : None : 0.0 : 0.0 (5, 5, 1, 2) : None : 0.0 : 0.0 (5, 5, 1, 3) : None : 0.0 : 0.0 (5, 5, 1, 4) : None : 0.0 : 0.0 (5, 5, 2, 0) : None : 0.0 : 0.0 (5, 5, 2, 1) : None : 0.0 : 0.0 (5, 5, 2, 2) : None : 0.0 : 0.0 (5, 5, 2, 3) : None : 0.0 : 0.0 (5, 5, 2, 4) : None : 0.0 : 0.0 (5, 5, 3, 0) : None : 0.0 : 0.0 (5, 5, 3, 1) : None : 0.0 : 0.0 (5, 5, 3, 2) : None : 0.0 : 0.0 (5, 5, 3, 3) : None : 0.0 : 0.0 (5, 5, 3, 4) : None : 0.0 : 0.0 (5, 5, 4, 0) : None : 0.0 : 0.0 (5, 5, 4, 1) : None : 0.0 : 0.0 (5, 5, 4, 2) : None : 0.0 : 0.0 (5, 5, 4, 3) : None : 0.0 : 0.0 (5, 5, 4, 4) : None : 0.0 : 0.0 eliminating_rule : Size=30 Key : Lower : Body : Upper (0, 0) : None : 0.0 : 0.0 (0, 1) : None : 0.0 : 0.0 (0, 2) : None : 0.0 : 0.0 (0, 3) : 0.0 : 0.0 : 0.0 (0, 4) : 0.0 : 0.0 : 0.0 (1, 0) : 0.0 : 0.0 : 0.0 (1, 1) : None : 0.0 : 0.0 (1, 2) : None : 0.0 : 0.0 (1, 3) : None : 0.0 : 0.0 (1, 4) : 0.0 : 8.876268172259907e-17 : 0.0 (2, 0) : 0.0 : 0.0 : 0.0 (2, 1) : None : 0.0 : 0.0 (2, 2) : None : 0.0 : 0.0 (2, 3) : 0.0 : 0.0 : 0.0 (2, 4) : 0.0 : 0.0 : 0.0 (3, 0) : 0.0 : 0.0 : 0.0 (3, 1) : None : 0.0 : 0.0 (3, 2) : None : 0.0 : 0.0 (3, 3) : 0.0 : 0.0 : 0.0 (3, 4) : 0.0 : 0.0 : 0.0 (4, 0) : 0.0 : 0.0 : 0.0 (4, 1) : 0.0 : 0.0 : 0.0 (4, 2) : None : 0.0 : 0.0 (4, 3) : None : 0.0 : 0.0 (4, 4) : 0.0 : 0.0 : 0.0 (5, 0) : 0.0 : 0.0 : 0.0 (5, 1) : 0.0 : 0.0 : 0.0 (5, 2) : None : 0.0 : 0.0 (5, 3) : None : 0.0 : 0.0 (5, 4) : None : 0.0 : 0.0 ``` ```python theme={null} classical_solution = np.array( [ int(pyo.value(tasks_model_large.x[idx])) for idx in np.ndindex(num_timeslots, len(G.nodes)) ] ).reshape(num_timeslots, len(G.nodes)) plot_workflow(classical_solution) ``` output ## References \[1] [Pakhomchik et. al. (2022). Solving workflow scheduling problems with QUBO modeling. arXiv preprint arXiv:2205.04844.](https://arxiv.org/pdf/2205.04844.pdf) # Travelling Salesman Problem Source: https://docs.classiq.io/explore/applications/logistics/traveling_salesman_problem/traveling_salesman_problem Open this notebook in GitHub to run it yourself The "Travelling Salesman Problem" \[[1](#tspwiki)] refers to finding the shortest route between cities, given their relative distances. In a more general sense, given a weighted directed graph, find the shortest route along the graph that goes through all the cities, where the weights correspond to the distance between cities. For example, in the graph below, the route along $0\rightarrow 1\rightarrow 2\rightarrow 3$ yields a total distance 3, which is the shortest: ```python theme={null} import networkx as nx # noqa nonedge = 5 graph = nx.DiGraph() graph.add_nodes_from([0, 1, 2, 3]) graph.add_edges_from([(0, 1), (1, 2), (2, 1), (2, 3)], weight=1) graph.add_edges_from([(0, 2), (1, 3)], weight=2) pos = nx.planar_layout(graph) nx.draw_networkx(graph, pos=pos) labels = nx.get_edge_attributes(graph, "weight") nx.draw_networkx_edge_labels(graph, pos, edge_labels=labels) distance_matrix = nx.convert_matrix.to_numpy_array(graph, nonedge=nonedge) ``` output As with many real world problems, this task can be cast as a combinatorial optimization problem. This demo shows how to employ the Quantum Approximate Optimization Algorithm \[[2](#qaoa)] on the Classiq platform to solve the Travelling Salesperson Problem. ## Mathematical Formulation First, model the problem mathematically. The input is the set of distances between the cities: this is given by a matrix $w$, whose $(i,j)$ entry refers to the distance between city $i$ to city $j$. The output of the model is an optimized route. Any route can be captured by a binary matrix $x$ that states at each step (row) which city was visited (column): $$ \begin{aligned} x_{ij} = \begin{cases} 1 & \text{at time step } i \text{ the route is in city } j \\ 0 & \text{else} \end{cases}\\ \end{aligned} $$ For example: $$ \begin{aligned} x=\begin{pmatrix} 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 1 \\ 0 & 0 & 1 & 0 \\ 1 & 0 & 0 & 0 \end{pmatrix} \end{aligned} $$ means starting from city 1, going to city 3 and then to city 2, and ending at city 0. **The constrained optimization problem is defined as follows:** Find x, which minimizes the path distance - \$ $$ \begin{aligned} \min_{x_{i, p} \in \{0, 1\}} \Sigma_{i, j} w_{i, j} \Sigma_p x_{i, p} x_{j, p + 1}\\ \end{aligned} $$ $$ (Note that the inner sum over $p$ is simply an indicator for whether to go from city $i$ to city $j$.) such that - each point is visited once - $ \begin{aligned} \forall i, \hspace{0.2cm} \Sigma_p x_{i, p} = 1\\ \end{aligned} $$ * in each step only a single point is visited - \$ $$ \begin{aligned} \forall p, \hspace{0.2cm} \Sigma_i x_{i, p} = 1\\ \end{aligned} $$ $$ **Directed graph:** In some cases, such as the graph above, not all cities are connected, and it is more suitable to describe the problem with a weighted, directed graph. In this case, to find the shortest path, assume that unconnected cities have an infinite distance between them. For example, the graph above corresponds to this matrix: $$ $$ \begin{aligned} w=\begin{pmatrix} \infty & 1 & 2 & \infty \\ \infty & \infty & 1 & 2 \\ \infty & 1 & \infty & 1 \\ \infty & \infty & \infty & \infty \end{pmatrix} \end{aligned} $$ $$ In practice, choose a large enough weight rather than infinity. $$ ## Solving with the Classiq Platform Solve the problem with the Classiq platform using QAOA by defining a Pyomo model. # ## Building the Pyomo Model from a Matrix of Distances Input ```python theme={null} import numpy as np # noqa import pyomo.core as pyo ``` ```python theme={null} ## Define a function that gets the matrix of distances and returns a Pyomo model def PyomoTSP(dis_mat: np.ndarray) -> pyo.ConcreteModel: model = pyo.ConcreteModel("TSP") assert dis_mat.shape[0] == dis_mat.shape[1], "error distance matrix is not square" NofCities = dis_mat.shape[0] # total number of cities cities = range(NofCities) # list of cities # Define the variable, which is the binary matrix x: x[i, j] = 1 indicates that point i is visited at step j model.x = pyo.Var(cities, cities, domain=pyo.Binary) # we add constraints @model.Constraint(cities) def each_step_visits_one_point_rule(model, ii): return sum(model.x[ii, jj] for jj in range(NofCities)) == 1 @model.Constraint(cities) def each_point_visited_once_rule(model, jj): return sum(model.x[ii, jj] for ii in range(NofCities)) == 1 # Define the Objective function def is_connected(i1: int, i2: int): return sum(model.x[i1, kk] * model.x[i2, kk + 1] for kk in cities[:-1]) model.cost = pyo.Objective( expr=sum( dis_mat[i1, i2] * is_connected(i1, i2) for i1, i2 in model.x.index_set() ) ) return model ``` # ## Generating a Specific Problem Pick a specific problem: the graph introduced above: ```python theme={null} # Generate a graph that defines the problem import networkx as nx graph = nx.DiGraph() graph.add_nodes_from([0, 1, 2, 3]) graph.add_edges_from([(0, 1), (1, 2), (2, 1), (2, 3)], weight=1) graph.add_edges_from([(0, 2), (1, 3)], weight=2) pos = nx.planar_layout(graph) nx.draw_networkx(graph, pos=pos) labels = nx.get_edge_attributes(graph, "weight") nx.draw_networkx_edge_labels(graph, pos, edge_labels=labels); ``` output Convert the graph object into a matrix of distances and then generate a Pyomo model for this example: ```python theme={null} nonedge = 5 # this variable refers to how much we penalize for unconnected points distance_matrix = nx.convert_matrix.to_numpy_array(graph, nonedge=nonedge) tsp_model = PyomoTSP(distance_matrix) ``` # ## Setting Up the Classiq Problem Instance To solve the Pyomo model defined above, use the `CombinatorialProblem` Python class. Under the hood it translates the Pyomo model to a quantum model of QAOA \[[1](#qaoa)], with the cost Hamiltonian translated from the Pyomo model. Choose the number of layers for the QAOA ansatz using the `num_layers` argument: ```python theme={null} from classiq import * from classiq.applications.combinatorial_optimization import CombinatorialProblem combi = CombinatorialProblem(pyo_model=tsp_model, num_layers=8) qmod = combi.get_model() ``` # ## Synthesizing the QAOA Circuit and Solving the Problem Synthesize and view the QAOA circuit (ansatz) used to solve the optimization problem: ```python theme={null} qprog = combi.get_qprog() show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/2zJRXpc3YfcnB9t3wHdi4C956A7 ``` Solve the problem by calling the `optimize` method of the `CombinatorialProblem` object. For the classical optimization part of QAOA, define the maximum number of classical iterations (`maxiter`) and the $\alpha$-parameter (`quantile`) for running CVaR-QAOA, an improved variation of QAOA \[[2](#cvar)]: ```python theme={null} optimized_params = combi.optimize(maxiter=150, quantile=0.6) ``` **Output:** ``` Optimization Progress: 93%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▉ | 140/150 [13:14<00:56, 5.68s/it] ``` Check the convergence of the run: ```python theme={null} import matplotlib.pyplot as plt plt.plot(combi.cost_trace) plt.xlabel("Iterations") plt.ylabel("Cost") plt.title("Cost convergence") ``` **Output:** ``` Text(0.5, 1.0, 'Cost convergence') ``` output ## Optimization Results Examine the statistics of the algorithm. To get samples with the optimized parameters, call the `sample` method: ```python theme={null} optimization_result = combi.sample(optimized_params) optimization_result.sort_values(by="cost").head(5) ``` | | solution | probability | cost | | ---- | ------------------------------------------------------ | ----------- | ---- | | 108 | \{'x': \[\[1, 0, 0, 0], \[0, 1, 0, 0], \[0, 0, 1, 0... | 0.000488 | 3 | | 41 | \{'x': \[\[0, 0, 0, 1], \[0, 0, 0, 1], \[1, 0, 0, 0... | 0.000977 | 5 | | 896 | \{'x': \[\[1, 0, 0, 0], \[0, 1, 0, 0], \[0, 0, 0, 0... | 0.000488 | 7 | | 147 | \{'x': \[\[1, 0, 0, 0], \[0, 0, 0, 0], \[0, 0, 0, 0... | 0.000488 | 8 | | 1240 | \{'x': \[\[1, 0, 0, 0], \[0, 1, 0, 0], \[0, 0, 0, 1... | 0.000488 | 8 | Compare the optimized results with uniformly sampled results: ```python theme={null} uniform_result = combi.sample_uniform() ``` And compare the histograms: ```python theme={null} optimization_result["cost"].plot( kind="hist", bins=50, edgecolor="black", weights=optimization_result["probability"], alpha=0.6, label="optimized", ) uniform_result["cost"].plot( kind="hist", bins=50, edgecolor="black", weights=uniform_result["probability"], alpha=0.6, label="uniform", ) plt.legend() plt.ylabel("Probability", fontsize=16) plt.xlabel("cost", fontsize=16) plt.tick_params(axis="both", labelsize=14) ``` output Plot the solution: ```python theme={null} best_solution = optimization_result.solution[optimization_result.cost.idxmin()] best_solution ``` **Output:** ``` {'x': [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]} ``` Lastly, compare with the classical solution of the problem: ```python theme={null} from pyomo.opt import SolverFactory solver = SolverFactory("couenne") solver.solve(tsp_model) tsp_model.display() ``` **Output:** ``` Model TSP Variables: x : Size=16, Index=x_index Key : Lower : Value : Upper : Fixed : Stale : Domain (0, 0) : 0 : 1.0 : 1 : False : False : Binary (0, 1) : 0 : 0.0 : 1 : False : False : Binary (0, 2) : 0 : 0.0 : 1 : False : False : Binary (0, 3) : 0 : 0.0 : 1 : False : False : Binary (1, 0) : 0 : 0.0 : 1 : False : False : Binary (1, 1) : 0 : 1.0 : 1 : False : False : Binary (1, 2) : 0 : 0.0 : 1 : False : False : Binary (1, 3) : 0 : 0.0 : 1 : False : False : Binary (2, 0) : 0 : 0.0 : 1 : False : False : Binary (2, 1) : 0 : 0.0 : 1 : False : False : Binary (2, 2) : 0 : 1.0 : 1 : False : False : Binary (2, 3) : 0 : 0.0 : 1 : False : False : Binary (3, 0) : 0 : 0.0 : 1 : False : False : Binary (3, 1) : 0 : 0.0 : 1 : False : False : Binary (3, 2) : 0 : 0.0 : 1 : False : False : Binary (3, 3) : 0 : 1.0 : 1 : False : False : Binary Objectives: cost : Size=1, Index=None, Active=True Key : Active : Value None : True : 3.0 Constraints: each_step_visits_one_point_rule : Size=4 Key : Lower : Body : Upper 0 : 1.0 : 1.0 : 1.0 1 : 1.0 : 1.0 : 1.0 2 : 1.0 : 1.0 : 1.0 3 : 1.0 : 1.0 : 1.0 each_point_visited_once_rule : Size=4 Key : Lower : Body : Upper 0 : 1.0 : 1.0 : 1.0 1 : 1.0 : 1.0 : 1.0 2 : 1.0 : 1.0 : 1.0 3 : 1.0 : 1.0 : 1.0 ``` If you get the right solution, plot it: ```python theme={null} best_classical_solution = np.array( [int(pyo.value(tsp_model.x[idx])) for idx in np.ndindex(distance_matrix.shape)] ) ``` ```python theme={null} best_quantum_solution = np.array( optimization_result.solution[optimization_result.cost.idxmin()]["x"] ) ``` ```python theme={null} if (best_classical_solution == best_quantum_solution.flatten()).all(): routesol = np.zeros(4, dtype=int) for k in range(4): indices = np.where( best_quantum_solution.reshape(distance_matrix.shape)[k, :] == 1 )[0] routesol[k] = int(indices[0]) if len(indices) > 0 else 0 edgesol = list(nx.utils.pairwise(routesol)) nx.draw_networkx( graph, pos, with_labels=True, edgelist=edgesol, edge_color="red", node_size=200, width=3, ) nx.draw_networkx(graph, pos=pos) labels = nx.get_edge_attributes(graph, "weight") nx.draw_networkx_edge_labels(graph, pos, edge_labels=labels) print("The route of the traveller is:", routesol) ``` **Output:** ``` The route of the traveller is: [0 1 2 3] ``` output ## References \[1] [Travelling Salesman Problem (Wikipedia).](https://en.wikipedia.org/wiki/Travelling_salesman_problem) \[2] [Farhi, Edward, Jeffrey Goldstone, and Sam Gutmann. (2014). A quantum approximate optimization algorithm. arXiv preprint arXiv:1411.4028.](https://arxiv.org/abs/1411.4028) \[3] [Barkoutsos, Panagiotis Kl, et al. (2020). Improving variational quantum optimization using CVaR. Quantum 4: 256.](https://arxiv.org/abs/1907.04769) # Vehicle Routing Problem (VRP) Source: https://docs.classiq.io/explore/applications/logistics/vehicle_routing_problem/vehicle_routing_problem Open this notebook in GitHub to run it yourself The Vehicle Routing Problem (VRP) is a combinatorial optimization problem that aims to determine the optimal routes for a fleet of vehicles to deliver goods to a set of locations while minimizing the total distance traveled or cost incurred. The problem is NP-hard, meaning that finding an exact solution is computationally infeasible for large instances, and heuristic or approximation algorithms are often used to find near-optimal solutions. The tutorial showcases how to solve the problem using the Quantum Approximate Optimization Algorithm (QAOA). In the problem, each vehicle starts from a different depot, visits a set of cities, and returns to its depot. The goal is to minimize the total travel cost while ensuring that each city is visited exactly once by one vehicle. ## Mathematical Formulation Given: * A set of locations: $L = {0, 1, \dots, n-1}$ where
* The first $m$ locations are depots: $D = {0, 1, \dots, m-1}$
* The remaining $n - m$ are cities: $C = {m, m+1, \dots, n-1}$ * Each vehicle $k \in D$:
* Starts and ends its route at its own depot $k$ * Visits a sequence of cities * Returns to its depot * Each city must be visited exactly once by one vehicle only. * Each vehicle route has $P = p + 2$ positions (start + $p$ inner positions + end). You can think about the positions as timesteps. The variable are called `positions` because driving between cities does not take the same amount of time. # ## Decision Variables Let $x_{u,v}^k \in \{0,1\}$ be a binary variable defined as $$ x_{u,v}^k = \begin{cases} 1 & \text{if vehicle } k \text{ visits location } u \text{ at position } v \\ 0 & \text{otherwise} \end{cases} $$ where:
$u \in L$ (location: city or depot)
$v \in {0, 1, \dots, P - 1}$
$k \in D$ (vehicle/depot index) # ## Constraints 1. Each city is visited exactly once: $$ \sum_{k\in D}\sum_{v=1}^{P-2} x_{u,v}^k = 1 \,\,\,\,\forall u\in C $$ Each city appears in exactly one vehicle's route, and not at the start or end positions.
2\. Each car visits exactly one city in every position: $$ \sum_{u\in C} x_{u,v}^k = 1 \,\,\,\,\forall v\in \{1,\dots,P-2\},\,\,\forall k\in D $$ Each vehicle has exactly one location assigned to each inner route position.
3\. Depot start and end positions are fixed: $$ x_{k,0}^k = x_{k,P-1}^k = 1 \,\,\,\, \forall k\in D $$ $$ x_{u,0}^k = x_{u,P-1}^k = 0 \,\,\,\, for \,\,\, u\neq k $$ Each vehicle starts and ends at its own depot only. 4. No city is at the start or end: $$ x_{u,0}^k = x_{u,P-1}^k = 1 \,\,\,\, \forall u\in C \,\,\,\, \forall k\in D $$ **Constraints (3) and (4) do not use qubits in the QAOA circuit, because they are predetermined.** # ## Objective Function The aim is to minimize total travel cost, defined as $$ \min_x \quad \sum_{k \in D} \sum_{v = 0}^{P - 2} \sum_{u \in L} \sum_{w \in L} \text{dist}(u, w) \cdot x_{u,v}^k \cdot x_{w,v+1}^k $$ where: * $\text{dist}(u, w)$ is the Euclidean distance between locations $u$ and $w$. * The sum computes all transitions $(u \rightarrow w)$ for each vehicle between consecutive positions. # ## Constraint Functions All cities are visited only once: $$ \sum_{u \in C} \left( \sum_{k \in D} \sum_{v=0}^{P-2} x_{u,v}^k -1 \right)^2=0 $$ All positions are occupied only once: $$ \sum_{k \in D} \sum_{v=1}^{P-2} \left(\sum_{u \in C} x_{u,v}^k -1 \right)^2=0 $$ \*\*These are added as penalty terms and multiplied by a scaling factor later on in the `cost` function. Ensure they are actually equal to zero by adding them as penalty terms.\*\* ## Generating Problem Data ```python theme={null} import itertools import math from typing import Iterable import matplotlib.pyplot as plt import networkx as nx import numpy as np from scipy.optimize import minimize from classiq import * locations = list(range(6)) # one depot per car depots = list(range(2)) # 4 cities to visit in total cities = list(range(2, 6)) num_vehicles = len(depots) num_cities = len(cities) num_locations = len(locations) ``` # ## Defining Positions Assuming the cities are more or less evenly divided among the vehicles, use `possible_number_of_cities_per_vehicle`: ```python theme={null} possible_number_of_cities_per_vehicle = len(cities) // num_vehicles ``` The total number of positions `num_positions` (or time slots) in a car route. Add $2$ for the start (depot) and end (return to depot) positions: ```python theme={null} num_positions = possible_number_of_cities_per_vehicle + 2 ``` All valid routes (time slots) positions that a vehicle can take: * Starting depot $\rightarrow 0$ * Visiting cities $\rightarrow 1,2, \ldots,$ `num_positions-1` * Ending depot $\rightarrow$ `num_positions` ```python theme={null} positions_all = list(range(num_positions)) ``` ```python theme={null} print(positions_all) ``` **Output:** ``` [0, 1, 2, 3] ``` The inner positions are the positions that can be occupied by cities, excluding the start and end depots. ```python theme={null} inner_positions = list(range(1, num_positions - 1)) print(inner_positions) ``` **Output:** ``` [1, 2] ``` # ## Setting City Locations Set random locations in a square of size `(0, max_x_y)`$\times$`(0, max_x_y)`. To have the same result, set the random seed. To have different results, change or delete the seed: ```python theme={null} np.random.seed(10) max_x_y = 10 x_y_locations = np.random.rand(num_locations, 2) * max_x_y ``` ```python theme={null} # Create a complete graph G = nx.complete_graph(num_locations) # Map node positions to the generated x_y coordinates pos = {i: (x_y_locations[i][0], x_y_locations[i][1]) for i in range(num_locations)} # Plot nx.draw( G, pos, with_labels=True, node_size=300, node_color="lightgreen", edge_color="gray" ) plt.title("Graph of the Cities on the XY Plane") ``` **Output:** ``` Text(0.5, 1.0, 'Graph of the Cities on the XY Plane') ``` output ```python theme={null} def find_xy_distances_from_locations(x_y_locations: np.ndarray): city_distance_matrix = np.zeros((num_locations, num_locations)) for i in range(num_locations): for j in range(num_locations): distance = math.sqrt( (x_y_locations[i][0] - x_y_locations[j][0]) ** 2 + (x_y_locations[i][1] - x_y_locations[j][1]) ** 2 ) city_distance_matrix[i][j] = distance return city_distance_matrix def calculate_distances_between_cities(x_y_locations: np.ndarray): city_distance_matrix = find_xy_distances_from_locations(x_y_locations) max_distance = np.max(city_distance_matrix) min_distance = np.min( city_distance_matrix[city_distance_matrix > 0] ) # Exclude self-distances return city_distance_matrix, max_distance, min_distance city_distance_matrix, max_distance, min_distance = calculate_distances_between_cities( x_y_locations ) ``` ```python theme={null} print(city_distance_matrix) ``` **Output:** ``` [[ 0. 7.40954323 3.40678023 9.35893743 6.05990735 9.3659449 ] [ 7.40954323 0. 5.41153153 4.35743196 8.07469987 2.11023545] [ 3.40678023 5.41153153 0. 6.14229335 3.56542154 7.52174929] [ 9.35893743 4.35743196 6.14229335 0. 6.72814108 5.24074724] [ 6.05990735 8.07469987 3.56542154 6.72814108 0. 10.07388021] [ 9.3659449 2.11023545 7.52174929 5.24074724 10.07388021 0. ]] ``` The maximal cost of any candidate is `num_locations`$\times$ `max_distance`. The minimal cost of any candidate is `num_locations`$\times$ `min_distance`. Hence, this is the maximal difference of eigenvalues of the total cost Hamiltonian: ```python theme={null} DISTANCE_NORMALISATION = num_locations * (max_distance - min_distance) ``` To set the maximal difference of eigenvalues of the total cost Hamiltonian to 1, scale the distances by `DISTANCE_NORMALISATION`: ```python theme={null} city_distance_matrix = city_distance_matrix / DISTANCE_NORMALISATION ``` ```python theme={null} print(city_distance_matrix) ``` **Output:** ``` [[ 0. 0.15507019 0.0712986 0.19586797 0.12682441 0.19601462] [0.15507019 0. 0.11325492 0.09119426 0.16899088 0.04416394] [0.0712986 0.11325492 0. 0.12854862 0.07461871 0.15741848] [0.19586797 0.09119426 0.12854862 0. 0.1408095 0.10968067] [0.12682441 0.16899088 0.07461871 0.1408095 0. 0.2108306 ] [0.19601462 0.04416394 0.15741848 0.10968067 0.2108306 0. ]] ``` ## Normalizing the Cost and Mixer Hamiltonians **This normalization improves the probability of finding good solutions.** If this is too much information right now, skip it and focus on the VRP problem itself under "Defining the VRP Objective Function". The mixer Hamiltonian is effectively $X/2$ and has eigenvalues $-0.5$ and $+0.5$. So the difference between minimum and maximum eigenvalues is exactly 1. You can rescale it using a global scaling parameter: ```python theme={null} GLOBAL_SCALING_PARAMETER = 0.1 ``` The cost Hamiltonian should be similar in eigenvalue difference to the mixer Hamiltonian and should be normalized to about 1. The distance normalization will take care of it later. To find the exact normalization requires solving this NP-hard problem, so always use approximation. Since it is approximate, you can tweak the relative scaling parameter. ```python theme={null} RELATIVE_OBJECTIVE_NORMALISATION = 1.5 ``` The overall normalization of the total cost is therefore: ```python theme={null} ABSOLUTE_OBJECTIVE_NORMALISATION = ( GLOBAL_SCALING_PARAMETER * RELATIVE_OBJECTIVE_NORMALISATION ) print(ABSOLUTE_OBJECTIVE_NORMALISATION) ``` **Output:** ``` 0.15000000000000002 ``` The constraint Hamiltonian has the property that a minimal constraint violation is 1 and no constraint violation is 0. Normalize the constraint Hamiltonian relative to the total cost Hamiltonian such that the constraint violation is 1\~2 x larger than the maximal difference between total cost values: ```python theme={null} RELATIVE_CONSTRAINT_NORMALISATION = 6 ``` The overall normalization of the constraint Hamiltonian is similarly: ```python theme={null} ABSOLUTE_CONSTRAINT_NORMALISATION = ( RELATIVE_CONSTRAINT_NORMALISATION * ABSOLUTE_OBJECTIVE_NORMALISATION ) print(ABSOLUTE_CONSTRAINT_NORMALISATION) ``` **Output:** ``` 0.9000000000000001 ``` The normalization coefficients are hyperparameters that can be tuned to improve the performance of QAOA. Summarize the normalization coefficients together with the cost and mixer Hamiltonians: $$ H = C_{gsp} \cdot \left(\beta\cdot H_{mixer} + \gamma \cdot C_{ron} \left(H_{objective} + C_{rcn}\cdot H_{constraints}\right)\right) $$ Where $\beta$ and $\gamma$ are the QAOA's variational parameters. $C_{gsp}$ is the global scaling parameter, $C_{ron}$ is the relative objective normalization, and $C_{rcn}$ is the relative constraint normalization coefficients. This method normalizes both the entire Hamiltonian and each individual term - including the constraint Hamiltonian to ensure the solution satisfies the constraints, and the objective Hamiltonian to increase the probability of finding the optimal objective value. ## Defining the VRP Objective Function First, define number of functions that comprise the objective function. A major difficulty is to map 3D (city, position, vehicle) to 1D index in the array of decision variables. # ## Finding the Right Index in the Array Access the value of the binary decision variable x that encodes whether city (or depot) `u` is visited at position `v` by vehicle `k`. * `u` - city * `v` - position (time slot) * `x` - the array of decision variables * `k` - the vehicle number ```python theme={null} def visit_indicator( x: QArray[QBit] | Iterable[int], # array of decision variables u: int, # city v: int, # position k: int, # vehicle number ) -> int | QBit: if u in depots or (v == 0 or v == num_positions - 1): # Case 1: If `u` is a depot if u in depots: if k == u: # Depots are only valid at start or end positions if v == 0 or v == num_positions - 1: # Vehicle `k` starts or ends at its depot `u` return 1 else: return 0 else: return 0 # Case 2: `u` is not a depot but the position is a depot-only position # Cities are not allowed at start or end of a route else: return 0 # Handle city logic (when `u` is a city and `v` is an inner position) else: # Map 3D (city, position, vehicle) to 1D index city_index = u - num_vehicles pos_index = v - 1 flat_index = ( city_index * possible_number_of_cities_per_vehicle * num_vehicles + pos_index * num_vehicles + k ) return x[flat_index] ``` Make sure to satisfy these constraints: * Each city is assigned to exactly one vehicle and one route position. * Each inner position of a route (per vehicle) is filled by exactly one city. ```python theme={null} def check_validity(x: QArray[QBit]): """ verifies that the assignment array `x` satisfies basic constraints. Checks if each city is visited exactly once and by one vehicle. """ # Check that each city is visited exactly once for city_index in range(num_cities): assignment_sum = 0 for position in range( possible_number_of_cities_per_vehicle ): # inner positions only for vehicle_id in range(num_vehicles): # Shift city index by num_vehicles to access location ID (since cities start after depots) assignment_sum += visit_indicator( x=x, u=city_index + num_vehicles, v=position + 1, k=vehicle_id ) # A city must appear exactly once across all vehicles and positions if assignment_sum != 1: return False # Each position is used exactly once per vehicle for position in range(possible_number_of_cities_per_vehicle): # inner positions for vehicle_id in range(num_vehicles): assignment_sum = 0 for city_index in range(num_cities): assignment_sum += visit_indicator( x=x, u=city_index + num_vehicles, v=position + 1, k=vehicle_id ) # Each position must be occupied by exactly one city if assignment_sum != 1: return False return True ``` # ## Determining Total Travel Cost for All Vehicle Routes ```python theme={null} def travel_objective_function(x: QArray[QBit]): """ Computes the total travel cost for all vehicle routes defined by the assignment array `x`. The cost is calculated as the sum of the distances between every pair of consecutive locations visited by the same vehicle, across all route positions. """ total_travel_cost = 0 # Loop through each position in the route except the last (look ahead to position + 1) for position in range(num_positions - 1): # Consider every pair of locations for to_location in locations: for from_location in locations: # For each vehicle (indexed by its depot ID) for vehicle_id in depots: # Add cost only if both transitions are selected in the route total_travel_cost += ( city_distance_matrix[from_location][to_location] * visit_indicator(x, from_location, position, vehicle_id) * visit_indicator(x, to_location, position + 1, vehicle_id) ) return total_travel_cost ``` # ## Defining Constraints ```python theme={null} def constraint_each_city_visited_once(x: QArray[QBit]): """ Soft constraint: Ensures that each city is visited exactly once by applying a penalty for any deviation from that condition. For each city, it checks how many times the city appears across all vehicle routes and inner positions. If the count is not exactly one, a quadratic penalty term is applied. """ total_deviations = 0 # Loop over each city (cities are defined separately from depots) for city_id in cities: city_assignment_count = 0 # Sum appearances of this city across all vehicles and inner positions for vehicle_id in depots: for position in inner_positions: city_assignment_count += visit_indicator( x=x, u=city_id, v=position, k=vehicle_id ) # Ideal count is 1: each city should be visited exactly once deviation_from_expected = city_assignment_count - 1 # Apply a quadratic penalty for deviation (0 if exactly one visit) # Power of two ignores the sign penalty_term = deviation_from_expected**2 total_deviations += penalty_term return total_deviations def constraint_each_position_occupied_by_one_city(x: QArray[QBit]): """ Soft constraint: Ensures that each inner position in each vehicle's route is occupied by exactly one location (either a city or a depot). For each vehicle and each inner route position, the function counts how many locations are assigned to that slot. If the count is not exactly one, a quadratic penalty term is applied from the expected value. """ total_deviations = 0 # Loop through each inner position (excluding start and end depot slots) for position in inner_positions: # Check each vehicle's route at this position for vehicle_id in depots: position_occupancy_count = 0 # Sum over all possible locations (cities + depots) at this position for location_id in locations: position_occupancy_count += visit_indicator( x=x, u=location_id, v=position, k=vehicle_id ) # Each position must be occupied by exactly one location deviation_from_expected = position_occupancy_count - 1 # Apply a penalty for under/over-assignments penalty_term = deviation_from_expected**2 total_deviations += penalty_term return total_deviations ``` The total cost function for the VRP is the sum of the total travel cost and the constraints: ```python theme={null} def cost(x: QArray[QBit]): return ABSOLUTE_OBJECTIVE_NORMALISATION * travel_objective_function( x ) + ABSOLUTE_CONSTRAINT_NORMALISATION * ( constraint_each_city_visited_once(x) + constraint_each_position_occupied_by_one_city(x) ) ``` ## Building the QAOA Model Define the number of layers: ```python theme={null} NUM_LAYERS = 12 @qfunc def mixer_layer(beta: CReal, qba: QArray[QBit]): apply_to_all(lambda q: RX(GLOBAL_SCALING_PARAMETER * beta, q), qba), @qfunc def main(params: CArray[CReal, 2 * NUM_LAYERS], x: Output[QArray[QBit]]) -> None: allocate((num_locations - 2) * (num_positions - 2) * len(depots), x) hadamard_transform(x) for i in range(NUM_LAYERS): phase(cost(x), params[2 * i]), mixer_layer(params[2 * i + 1], x) ``` ## Synthesizing the Model ```python theme={null} qprog = synthesize(model=main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/31EmgV8n7ACAPbJICWv4tZMsxhR ``` ## Executing When you have the QAOA circuit, execute it in the hybrid scheme. First, define `ExecutionSession` for the optimization process and the initial parameters: ```python theme={null} NUM_SHOTS = 30000 es = ExecutionSession( qprog, execution_preferences=ExecutionPreferences(num_shots=NUM_SHOTS) ) def initial_qaoa_params(NUM_LAYERS) -> np.ndarray: initial_gammas = math.pi * np.linspace( 1 / (2 * NUM_LAYERS), 1 - 1 / (2 * NUM_LAYERS), NUM_LAYERS ) initial_betas = math.pi * np.linspace( 1 - 1 / (2 * NUM_LAYERS), 1 / (2 * NUM_LAYERS), NUM_LAYERS ) initial_params = [] for i in range(NUM_LAYERS): initial_params.append(initial_gammas[i]) initial_params.append(initial_betas[i]) return np.array(initial_params) initial_params = initial_qaoa_params(NUM_LAYERS) ``` **The cost function definition for the execution:** For each measurement state, the cost function is calculated by the `objective` function of the VRP problem: ```python theme={null} cost_func = lambda state: cost(state["x"]) ``` **A function estimates the cost function in each iteration.** Use the `estimate_cost` method of the `ExecutionSession` class: ```python theme={null} # Record the steps of the optimization intermediate_params = [] objective_values = [] def estimate_cost_func(params): objective_val = es.estimate_cost(cost_func, {"params": params.tolist()}) objective_values.append(objective_val) return objective_val # Define the callback function to store the intermediate steps def callback(xk): intermediate_params.append(xk) ``` # ## Running the Optimization Process Use `scipy.optimize.minimize` to minimize the cost function. **You may increase the `max_iterations` to have more iterations.** ```python theme={null} max_iterations = 1 optimization_res = minimize( fun=estimate_cost_func, x0=initial_params, method="COBYLA", callback=callback, options={"maxiter": max_iterations, "rhobeg": 0.7}, ) ``` ## Analyzing the Results If you have more iterations you can make a convergence graph: ```python theme={null} # plt.plot(objective_values) # plt.xlabel("Iteration") # plt.ylabel("Objective Value") # plt.title("Optimization Progress") ``` # ## Optimizing Parameters Watch for four or more circuit layers: ```python theme={null} optimized_gammas = optimization_res.x[0::2] optimized_betas = optimization_res.x[1::2] plt.plot(optimized_gammas, label="Gammas", marker="o") plt.plot(optimized_betas, label="Betas", marker="x") plt.legend() ``` **Output:** ``` ``` output Accumulate the statistics with the final optimized parameters: ```python theme={null} res = es.sample({"params": optimization_res.x.tolist()}) ``` ```python theme={null} sorted_counts = sorted(res.parsed_counts, key=lambda pc: cost(pc.state["x"])) for sampled in sorted_counts[:20]: x = sampled.state["x"] if check_validity(x): print( f"Valid solution={sampled.state['x']} probability={sampled.shots/NUM_SHOTS} cost={cost(sampled.state['x'])}" ) else: print( f"Invalid solution={sampled.state['x']} probability={sampled.shots/NUM_SHOTS} cost={cost(sampled.state['x'])}" ) ``` **Output:** ``` Valid solution=[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1] probability=0.0016333333333333334 cost=0.07766708749355561 Valid solution=[0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1] probability=0.0015666666666666667 cost=0.07766708749355561 Valid solution=[1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0] probability=0.0015 cost=0.07766708749355561 Valid solution=[0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0] probability=0.0013666666666666666 cost=0.07766708749355561 Valid solution=[0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0] probability=0.0017666666666666666 cost=0.11675088242357473 Valid solution=[0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0] probability=0.0015333333333333334 cost=0.11675088242357473 Valid solution=[0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1] probability=0.0015 cost=0.11675088242357473 Valid solution=[0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1] probability=0.0012333333333333332 cost=0.11675088242357473 Valid solution=[0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0] probability=0.0013333333333333333 cost=0.12295509067661474 Valid solution=[1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0] probability=0.0017333333333333333 cost=0.12295509067661475 Valid solution=[1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1] probability=0.0016 cost=0.12295509067661475 Valid solution=[0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1] probability=0.0015333333333333334 cost=0.12295509067661475 Valid solution=[1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0] probability=0.0016333333333333334 cost=0.12385895064051687 Valid solution=[0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0] probability=0.0015666666666666667 cost=0.12385895064051687 Valid solution=[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0] probability=0.0015333333333333334 cost=0.12385895064051687 Valid solution=[0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0] probability=0.0012 cost=0.12385895064051687 Valid solution=[0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0] probability=0.0014 cost=0.12876416436190394 Valid solution=[0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0] probability=0.0013333333333333333 cost=0.12876416436190394 Valid solution=[0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0] probability=0.0017333333333333333 cost=0.12876416436190397 Valid solution=[0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0] probability=0.0013333333333333333 cost=0.12876416436190397 ``` # ## Best Solution ```python theme={null} sorted_counts[0] ``` **Output:** ``` {'x': [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]}: 49 ``` ```python theme={null} visited_cities_by_vehicle = {} for vehicle in depots: print("Vehicle:", vehicle) visited_cities_by_vehicle[vehicle] = [] # Initialize list for this vehicle for position in positions_all: for city in locations: if visit_indicator(sorted_counts[0].state["x"], city, position, vehicle): print(f"Visited in city {city} at position {position}") visited_cities_by_vehicle[vehicle].append(city) ``` **Output:** ``` Vehicle: 0 Visited in city 0 at position 0 Visited in city 2 at position 1 Visited in city 4 at position 2 Visited in city 0 at position 3 Vehicle: 1 Visited in city 1 at position 0 Visited in city 3 at position 1 Visited in city 5 at position 2 Visited in city 1 at position 3 ``` ```python theme={null} G = nx.complete_graph(num_locations) color_map = plt.colormaps["tab10"] vehicle_colors = {vehicle: color_map(i) for i, vehicle in enumerate(depots)} city_colors = [None] * num_locations for vehicle, cities in visited_cities_by_vehicle.items(): for city in cities: city_colors[city] = vehicle_colors[vehicle] # Cities plot fig, ax = plt.subplots(figsize=(8, 6)) nx.draw( G, pos, with_labels=True, node_size=300, node_color=city_colors, edge_color="lightgray", ax=ax, ) # Directed route for each vehicle for i, (vehicle, cities) in enumerate(visited_cities_by_vehicle.items()): # Ensure order reflects travel order route = [c for c, _ in itertools.groupby(cities)] if len(route) < 2: continue edges = list(nx.utils.pairwise(route)) # Slight curve per vehicle to separate overlapping edges rad = 0.08 * (i - (len(visited_cities_by_vehicle) - 1) / 2) nx.draw_networkx_edges( G, pos, edgelist=edges, edge_color=vehicle_colors[vehicle], width=3, arrows=True, arrowstyle="-|>", arrowsize=18, connectionstyle=f"arc3,rad={rad}", ax=ax, ) plt.title("Cities Colored by Visiting Vehicle + Directed Routes") ``` **Output:** ``` Text(0.5, 1.0, 'Cities Colored by Visiting Vehicle + Directed Routes') ``` output # ADAPT QAOA Source: https://docs.classiq.io/explore/applications/optimization/adapt_qaoa/adapt_qaoa Open this notebook in GitHub to run it yourself The notebook demonstrates the solution of a generic optimization problem using both the "vanilla" QAOA (Quantum Approximate Optimization Algorithm) and the adaptive version ## Hamiltonian Encoding The optimization objective function is encoded into a Hamiltonian function formulated as a weighted sum of Pauli strings: $$ H = \sum_k{c_kP_k} $$ with each pauli string, $P_k$, composed of a Pauli operation ($\sigma\in\left\{I,X,Y,Z\right\}$) applied to each of the qubits in the register. For example, $XIIZ$, applies a Pauli $X$ to the first qubit and a Pauli $Z$ to the fourth qubit The encoding of the objective start by defining the function with binary variables, $x_i\in\left\{1,-1\right\}$, and then defining Pauli strings with $Z_i$ corresponding to the original variables. If the objective variables' domain is $x_i\in\left\{0,1\right\}$, a transformation is used: $x_i=\left(1-Z_i\right)/2$ (commonly referred to as *spin to bit* transformation) Since the encoding results in Pauli strings composed only of $I$, and $Z$, the Pauli string is called *diagonal*, this has the following benefits: * **Easy exponentiation** - the exponentiation results in simple to implement single or multi-qubit Z rotations * **No superposition mixing** - the diagonal Hamiltonian does not create entanglement or rotate between basis states, enabling computation basis measurement * **Efficient measurement** - all $\left\{I,Z\right\}^{\otimes n}$ strings commute, allowing for a single simultaneous measurement of all strings ```python theme={null} import matplotlib.pyplot as plt import numpy as np import scipy from tqdm import tqdm from classiq import * ``` # ## QMod Tip In the following cell the variable `h` is set to a diagonal hamiltonian using a self explanatory syntax that sums sparse Pauli strings, that is only the non identity Paulis are specified. For example the first line, `- 0.1247375 * Pauli.Z(0) * Pauli.Z(1)` creates the string `ZZIII` with the weight `-0.1247375`. The length of the string, is inferred from the largest qubit index in the sum. The resulating variable has the type [SparsePauliOp](https://docs.classiq.io/latest/qmod-reference/api-reference/classical-types/?h=sparsepauliop#classiq.qmod.builtins.structs.SparsePauliOp) For the optimization function a *dual-use* function is used for the Hamiltonian. This function takes a argument that can be either a `QArray` or a `list[int]`. This allows the same function to be used both to: * compute the cost expectation on the measurement results which are binary numbers ($0,1$) * create a quantum observable in the circuit, resulting in quantum gates the additional `cost_func` function negates the `hamiltonian` function since the problem involves finding the maximum value, but the optimization procedure (`scipy.optimize.minimize`) performs a minimization ```python theme={null} from typing import Tuple from sympy import sympify from classiq import IndexedPauli, Pauli, SparsePauliOp, SparsePauliTerm from classiq.qmod.symbolic_expr import SymbolicExpr QFloatExr = SymbolicExpr h = ( -0.561775 * Pauli.Z(0) - 1.238125 * Pauli.Z(1) - 1.131450 * Pauli.Z(2) - 0.681350 * Pauli.Z(3) - 0.713775 * Pauli.Z(4) - 0.979450 * Pauli.Z(5) - 1.145025 * Pauli.Z(6) - 0.774450 * Pauli.Z(7) + 0.5 * Pauli.Z(0) * Pauli.Z(1) + 0.5 * Pauli.Z(0) * Pauli.Z(2) - 0.188125 * Pauli.Z(0) * Pauli.Z(4) - 0.200100 * Pauli.Z(0) * Pauli.Z(5) + 0.5 * Pauli.Z(1) * Pauli.Z(3) + 0.234550 * Pauli.Z(1) * Pauli.Z(6) + 0.053575 * Pauli.Z(1) * Pauli.Z(7) + 0.5 * Pauli.Z(2) * Pauli.Z(3) - 0.048100 * Pauli.Z(2) * Pauli.Z(4) + 0.229550 * Pauli.Z(2) * Pauli.Z(5) - 0.039525 * Pauli.Z(3) * Pauli.Z(6) - 0.229125 * Pauli.Z(3) * Pauli.Z(7) + 0.5 * Pauli.Z(4) * Pauli.Z(5) + 0.5 * Pauli.Z(4) * Pauli.Z(6) + 0.5 * Pauli.Z(5) * Pauli.Z(7) + 0.5 * Pauli.Z(6) * Pauli.Z(7) ) def hamiltonian(v: QArray | list[int]) -> QFloatExr: # linear Z_i coefficients (diagonal of hh) single_weights = [ -0.561775, # Z0 -1.238125, # Z1 -1.13145, # Z2 -0.68135, # Z3 -0.713775, # Z4 -0.97945, # Z5 -1.145025, # Z6 -0.77445, # Z7 ] # quadratic ZZ coefficients (off-diagonal of hh for i QFloatExr: # maps v_i in {0,1} to Z_i in {+1,-1} return sympify(1.0) - 2 * bit ham = sympify(0.0) # no constant term in your SparsePauliOp # ZZ terms for (i, j), w in zip(quadratic_pairs, quadratic_weights): ham += w * z_value(v[i]) * z_value(v[j]) # Z terms for i, w in enumerate(single_weights): ham += w * z_value(v[i]) return ham def cost_func(v: QArray | list[int]) -> QFloatExr: return hamiltonian(v) # negative sign for maximization ``` ## The Cost Layer As mentioned above, since the Hamiltonian is diagonal, its exponentiation is straightforward and amounts to phase addition due to Z rotations.\ This is accomplished with the [phase](https://docs.classiq.io/latest/qmod-reference/language-reference/statements/phase/?h=phase) function, which results in an efficient quantum implementation of $e^{-i \gamma H_c}$. ```python theme={null} @qfunc def cost_layer(gamma: CReal, v: QArray): phase(cost_func(v), gamma) ``` ## The Mixer Layer The QAOA mixer is defined as $H_M=\sum{X_i}$, and its exponentiaion is simply a $R_X$ rotation of the same angle ($\beta$) on all the qubits ```python theme={null} @qfunc def mixer_layer(beta: CReal, qba: QArray): apply_to_all(lambda q: RX(2 * beta, q), qba) ``` ## The QAOA Ansatz The parametric QAOA ansatz is composed of $p$ pairs of exponentiated Mixer and Cost Hamiltonians: $$ U\left(\vec{\beta},\vec{\gamma}\right)=e^{-i \beta_p H_M}e^{-i \gamma_p H_C}\ldots e^{-i \beta_2 H_M}e^{-i \gamma_1 H_C}e^{-i \beta_2 H_M}e^{-i \gamma_1 H_C} $$ ```python theme={null} @qfunc def qaoa_ansatz( gammas: CArray[CReal], betas: CArray[CReal], qba: QArray, ): n = gammas.len assert n == betas.len, "Number of gamma and beta parameters must be equal" for i in range(n): cost_layer(gammas[i], qba) mixer_layer(betas[i], qba) ``` ## Assemble the Full QAOA Algorithm We select $p=3$ layers for the QAOA. The quantum program takes the $2p$ parameters as a single list (because the `scipy.optimize.minimize` function manipulate a 1-D array of parameters), and the quantum register of the $5$ qubits. The qubits are initialized to $\left|+\right\rangle^{\otimes 5}$, with a `hadamard_transform` and then the `qaoa_ansatz`, $U\left(\vec{\beta},\vec{\gamma}\right)$, is applied ```python theme={null} from typing import Final NUM_LAYERS: Final[int] = 3 NUM_QUBITS: Final[int] = 8 @qfunc def main( params: CArray[ CReal, NUM_LAYERS * 2 # type: ignore ], # Execution parameters (first half: gammas, second half: betas) v: Output[QArray[QBit, NUM_QUBITS]], # type: ignore ): allocate(v) hadamard_transform(v) gammas = params[0:NUM_LAYERS] betas = params[NUM_LAYERS : NUM_LAYERS * 2] qaoa_ansatz(gammas, betas, v) ``` ```python theme={null} from classiq.execution.execution_session import ExecutionSession from classiq.synthesis import show, synthesize custom_hardware_settings = CustomHardwareSettings(basis_gates=["cz", "rz", "rx", "ry"]) # connectivity_map = [ # (0, 1), (1, 2), (2, 3), # (4, 5), (5, 6), (6, 7), # (0, 4), (1, 5), (2, 6), (3, 7) # ], # is_symmetric_connectivity=True, # ) preferences = Preferences(custom_hardware_settings=custom_hardware_settings) qprog = synthesize(main, preferences=preferences) es = ExecutionSession(qprog) # show(qprog) ``` ```python theme={null} # circuit width print(f"circuit width: {qprog.data.width}") # circuit depth print(f"circuit depth: {qprog.transpiled_circuit.depth}") ``` **Output:** ``` circuit width: 8 circuit depth: 241 ``` ## Classical Optimization - "Vanilla" QAOA The $\vec{\beta}$, and $\vec{\gamma}$ parameters are tuned until the objective function converges. The results of the parameters, and the solution vector are shown below ```python theme={null} cost_trace = [] params_history = [] def objective_func(params, es): cost_estimation = es.estimate_cost( lambda state: cost_func(state["v"]), {"params": params.tolist()} ) cost_trace.append(cost_estimation) params_history.append(params.copy()) return cost_estimation ``` ```python theme={null} # TODO: uncomment MAX_ITERATIONS = 60 initial_params = np.concatenate( (np.linspace(0, 1, NUM_LAYERS), np.linspace(1, 0, NUM_LAYERS)) ) with tqdm(total=MAX_ITERATIONS, desc="Optimization Progress", leave=True) as pbar: def progress_bar(xk: np.ndarray) -> None: pbar.update(1) obj = lambda params: objective_func(params, es) optimization_results = scipy.optimize.minimize( fun=obj, x0=initial_params, method="COBYLA", options={"maxiter": MAX_ITERATIONS}, callback=progress_bar, ) # Sample the circuit using the optimized parameters res = es.sample({"params": optimization_results.x.tolist()}) es.close() print(f"Optimized parameters: {optimization_results.x.tolist()}") ``` **Output:** ``` Optimization Progress: 40%|███████████████████████████████▏ | 24/60 [01:26<02:10, 3.62s/it] ``` **Output:** ``` Optimized parameters: [0.023764076850575737, 0.4788895138030138, 1.999274060087544, 0.9995157762026724, 0.4815685457228382, 0.09300073147966786] ``` Plotting the convergence graph: ```python theme={null} plt.plot(cost_trace) plt.xlabel("Iterations") plt.ylabel("Cost") plt.title("Cost convergence") plt.show() ``` output ## Displaying and Discussing the Results ```python theme={null} print(f"Optimized parameters: {optimization_results.x.tolist()}") sorted_counts = sorted(res.parsed_counts, key=lambda pc: cost_func(pc.state["v"])) num_shots = sum(state.shots for state in res.parsed_counts) for sampled in sorted_counts[:10]: solution = sampled.state["v"] probability = sampled.shots / num_shots cost_value = cost_func(solution) print(f"solution={solution} probability={probability:.3f} cost={cost_value:.3f}") ``` **Output:** ``` Optimized parameters: [0.023764076850575737, 0.4788895138030138, 1.999274060087544, 0.9995157762026724, 0.4815685457228382, 0.09300073147966786] solution=[1, 0, 0, 1, 1, 0, 0, 1] probability=0.037 cost=-5.482 solution=[1, 0, 0, 1, 0, 1, 1, 0] probability=0.012 cost=-4.771 solution=[1, 0, 0, 1, 0, 0, 0, 1] probability=0.021 cost=-4.629 solution=[0, 0, 0, 1, 1, 0, 0, 1] probability=0.014 cost=-4.629 solution=[0, 0, 0, 1, 0, 0, 0, 1] probability=0.031 cost=-4.529 solution=[1, 0, 0, 1, 0, 1, 0, 0] probability=0.026 cost=-4.513 solution=[1, 0, 0, 0, 0, 1, 1, 0] probability=0.020 cost=-4.513 solution=[1, 0, 0, 0, 1, 0, 0, 1] probability=0.017 cost=-4.465 solution=[1, 0, 0, 1, 1, 0, 0, 0] probability=0.013 cost=-4.465 solution=[1, 0, 0, 0, 0, 1, 0, 0] probability=0.042 cost=-4.413 ``` ## ADAPT QAOA The algorithm, based on the paper [An adaptive quantum approximate optimization algorithm for solving combinatorial problems on a quantum computer](https://arxiv.org/pdf/2005.10258), modifies QAOA by using different *mixer* layers, instead of the original $H_M=\sum_i{X_i}$. The algorithm defines a pool of potential mixer layers, that are simple to implement, and can match the problem better. The motiviation to the algorithm is rooted in t *STA* (shortcut to adiabaticity), with the hope of achieving better convergence than the "vanilla" QAOA. The algorithm proceeds incrementally: * initially, a single mixer layer is used (the original $H_M$) and the parameters are optimized * all the potential mixer layers, $A_j$, are tested as candidates. The criterion is the resulting gradient of the commutator of a potential mixer layer with the cost Hamiltonian, applied to the current circuit: $$ \nabla A_j = \left\langle\psi^{\left(k-1\right)}\right|e^{i \gamma_0 H_C}\left[H_C,A_j\right]e^{-i \gamma_0 H_C}\left|\psi^{\left(k-1\right)}\right\rangle $$ * the mixer layer with the largest gradient is selected, the circuit grows by one layer, and the process continues (optimizing parameters and checking gradients) * the process terminates when the scaled norm of the gradient vector drops below a threshold # ## Utilities The following functions define some convenience utilities for computing the commutation of Pauli strings ```python theme={null} from functools import reduce from operator import add, mul # the following utilites are used to compute an expectation value of the form: # # where psi is the state (ansatz), A and H are a mixer and cost Hamiltonians, respectively # defined as pauli strings (SparsePauliOp) # multiplication table of two Pauli matrices # weight (imaginary), pauli_c = pauli_a * pauli_b pauli_mult_table: list[list[Tuple[int, int]]] = [ [(+0, Pauli.I), (+0, Pauli.X), (+0, Pauli.Y), (+0, Pauli.Z)], [(+0, Pauli.X), (+0, Pauli.I), (+1, Pauli.Z), (-1, Pauli.Y)], [(+0, Pauli.Y), (-1, Pauli.Z), (+0, Pauli.I), (+1, Pauli.X)], [(+0, Pauli.Z), (+1, Pauli.Y), (-1, Pauli.X), (+0, Pauli.I)], ] def sorted_pauli_term(term: SparsePauliTerm) -> SparsePauliTerm: """ sort pauli terms according to the qubit's index, e.g., Pauli.X(2)*Pauli.Z(7)*Pauli.X(4) ==> Pauli.X(2)*Pauli.X(4)*Pauli.Z(7) """ sorted_paulis = sorted(term.paulis, key=lambda p: p.index) return SparsePauliTerm(sorted_paulis, term.coefficient) def commutator(ha: SparsePauliOp, hb: SparsePauliOp) -> SparsePauliOp: """ Compute the commutator [ha, hb] = ha*hb - hb*ha where ha and hb are SparsePauliOp objects. Returns a SparsePauliOp representing the commutator. """ n = max(ha.num_qubits, hb.num_qubits) commutation = SparsePauliOp([], n) for sp_term_a in ha.terms: for sp_term_b in hb.terms: parity = 1 coefficient = 1.0 msp = {p.index: p.pauli for p in sp_term_a.paulis} for p in sp_term_b.paulis: pauli_a = msp.get(p.index, Pauli.I) pauli_b = p.pauli weight, pauli = pauli_mult_table[pauli_a][pauli_b] if weight != 0: parity = -parity coefficient *= weight * 1j msp[p.index] = pauli # reconstruct pauli_string if parity != 1: # consider filtering identity terms, making sure the term is not empty pauli_term = (reduce(mul, (p(idx) for idx, p in msp.items()))).terms[0] pauli_term.coefficient = ( sp_term_a.coefficient * sp_term_b.coefficient * coefficient * 2 ) commutation.terms.append(pauli_term) return commutation def normalize_pauli_term(spt: SparsePauliTerm, num_qubits=-1) -> SparsePauliTerm: """ remove redundant Pauli.I operators from a Pauli string making "normalized" strings comparable if num_qubits is set, an optional Pauli.I is added to ensure the length """ if not spt.paulis: return spt npt = sorted_pauli_term(spt) paulis = [] max_index = max_identity_index = -1 if num_qubits > 0: max_identity_index = num_qubits - 1 for ip in npt.paulis: if ip.pauli != Pauli.I: paulis.append(ip) max_index = max(max_index, int(ip.index)) else: max_identity_index = max(max_identity_index, int(ip.index)) if max_identity_index > max_index: paulis.append(IndexedPauli(Pauli.I, max_identity_index)) npt.paulis = paulis return npt def collect_pauli_terms(spo: SparsePauliOp) -> SparsePauliOp: """ collect the coefficient of identical Pauli strings for example: 1.5*"IXZI"-0.3*"IXXZ"+0.4*"IXZI", would result, in: 1.9*"IXZI"-0.3*"IXXZ" The function correctly ignores "I" when comparing strings, and sets the correct `num_qubits` terms with abs(coefficient) SparsePauliOp: p_int, idx = pair term = SparsePauliTerm([IndexedPauli(Pauli(p_int), idx)], 1.0) return SparsePauliOp([term], num_qubits=idx + 1) paulistrings = [] for key, coeff in pauliterms.items(): if np.abs(coeff) < TOLERANCE: continue key_op = reduce(mul, (single_qubit_op(pair) for pair in key)) paulistrings.append(coeff * key_op) if not paulistrings: return SparsePauliOp([], spo.num_qubits) # Sum all strings return reduce(add, paulistrings) ``` # ## The Mixer Pool The pool includes the default sum-of-X layer, as well as a sum-of-Y layer, followed by several single qubit layers with X and Y Paulis, and some two-qubit gates that add entanglement at the cost of a slightly more complicated layer. Several heuristics are suggested for building mixer pools based on the problem definition. A larger pool has a potential to find more efficient circuits, at the cost of computing many *mixer gradients* at each iteration ```python theme={null} # # build mixer pool # mixer_pool: list[SparsePauliOp] = [ # # default mixer # 0.2 # * ( # Pauli.X(0) # + Pauli.X(1) # + Pauli.X(2) # + Pauli.X(3) # + Pauli.X(4) # ), # # Y mixer # 0.2 # * ( # Pauli.Y(0) # + Pauli.Y(1) # + Pauli.Y(2) # + Pauli.Y(3) # + Pauli.Y(4) # ), # # single qubit mixers # 1.0 * Pauli.X(0), # 1.0 * Pauli.X(1), # 1.0 * Pauli.X(2), # 1.0 * Pauli.X(3), # 1.0 * Pauli.X(4), # 1.0 * Pauli.Y(0), # 1.0 * Pauli.Y(1), # 1.0 * Pauli.Y(2), # 1.0 * Pauli.Y(3), # 1.0 * Pauli.Y(4), # # two qubit mixers # 1.0 * Pauli.X(0) * Pauli.X(1), # 1.0 * Pauli.X(0) * Pauli.X(2), # 1.0 * Pauli.X(0) * Pauli.X(3), # 1.0 * Pauli.X(0) * Pauli.X(4), # 1.0 * Pauli.X(1) * Pauli.X(2), # 1.0 * Pauli.X(1) * Pauli.X(3), # 1.0 * Pauli.X(1) * Pauli.X(4), # 1.0 * Pauli.X(2) * Pauli.X(3), # 1.0 * Pauli.X(2) * Pauli.X(4), # 1.0 * Pauli.X(3) * Pauli.X(4), # 1.0 * Pauli.Y(0) * Pauli.Y(1), # 1.0 * Pauli.Y(0) * Pauli.Y(2), # 1.0 * Pauli.Y(0) * Pauli.Y(3), # 1.0 * Pauli.Y(0) * Pauli.Y(4), # 1.0 * Pauli.Y(1) * Pauli.Y(2), # 1.0 * Pauli.Y(1) * Pauli.Y(3), # 1.0 * Pauli.Y(1) * Pauli.Y(4), # 1.0 * Pauli.Y(2) * Pauli.Y(3), # 1.0 * Pauli.Y(2) * Pauli.Y(4), # 1.0 * Pauli.Y(3) * Pauli.Y(4), # ] # for 8 qubits # build mixer pool for 8 qubits mixer_pool: list[SparsePauliOp] = [ # default X mixer (global) 1.0 * ( Pauli.X(0) + Pauli.X(1) + Pauli.X(2) + Pauli.X(3) + Pauli.X(4) + Pauli.X(5) + Pauli.X(6) + Pauli.X(7) ), # default Y mixer (global) 1.0 * ( Pauli.Y(0) + Pauli.Y(1) + Pauli.Y(2) + Pauli.Y(3) + Pauli.Y(4) + Pauli.Y(5) + Pauli.Y(6) + Pauli.Y(7) ), # single-qubit X mixers 1.0 * Pauli.X(0), 1.0 * Pauli.X(1), 1.0 * Pauli.X(2), 1.0 * Pauli.X(3), 1.0 * Pauli.X(4), 1.0 * Pauli.X(5), 1.0 * Pauli.X(6), 1.0 * Pauli.X(7), # single-qubit Y mixers 1.0 * Pauli.Y(0), 1.0 * Pauli.Y(1), 1.0 * Pauli.Y(2), 1.0 * Pauli.Y(3), 1.0 * Pauli.Y(4), 1.0 * Pauli.Y(5), 1.0 * Pauli.Y(6), 1.0 * Pauli.Y(7), # two-qubit XX mixers (all pairs i float: mix_h_comm = commutator(mixer, hamiltonian) return abs(es.estimate(mix_h_comm, {"params": params}).value) ``` ## Classical Optimization * ADAPT QAOA The implementation of the ADAPT QAOA uses the same hybrid quantum-classical structure to optimize the ansatz parameters ($\vec{\beta},\vec{\gamma}$) The differences are: * The `ansatz mixers` list is incrementally populated with mixer layers, and used in the ansatz * Following each convergence, the gradients of all mixers are computed, and either a new mixer is added, or the adaptive process concludes # ## QMod Tip Since the mixer is no longer a sum of single bit Pauli strings (the default mixer can be written as: $XII\ldots I+IXI\ldots I+IIX\ldots I+III\ldots X$), the exponent is not a trivial Pauli rotation (applying $R_X\left(\beta\right)$ to each qubit) and the exponentiation $e^{-i \beta A_j}$ has to be computed. An efficient and simple construct is the [suzuki\_trotter](https://docs.classiq.io/latest/qmod-reference/api-reference/functions/core_library/exponentiation/?h=suzuki_trotter#classiq.qmod.builtins.functions.exponentiation.commuting_paulis_exponent) function that exponentiates a `SparsePauliOp` (a weighted sum of Pauli strings) with a given coefficient. The `adaptive_mixer_layer` function uses this construct: `suzuki_trotter(ansatz_mixers[mixer_idx], beta, 1, 1, qba)`, where the third and fourth arguments (`1,1`) specify the order and number of repetitions of the trotterization, which can be safely set to $1$ for simple Pauli strings. Another variant is the [multi\_suzuki\_trotter](https://docs.classiq.io/latest/qmod-reference/api-reference/functions/core_library/exponentiation/?h=multi_suzuki_trotter#classiq.qmod.builtins.functions.exponentiation.multi_suzuki_trotter) function that exponentiates a sum of Hamiltonians: $e^{-i\left(H_1t_1+H_2t_2+\ldots+H_nt_n\right)}$ ```python theme={null} from classiq import suzuki_trotter from classiq.execution import ClassiqBackendPreferences, ExecutionPreferences MAX_ITERATIONS = 60 last_params = [] ansatz_mixers: list[int] = [] # start with default mixer ansatz_mixers.append(0) # gradient norm tolerance for stopping criterion tol: Final[float] = 0.027 while True: # loop until gradient norm exceeds tolerance (break-ing out of the loop) # number of layers (initially=1), each layer is a pair of mixer and cost Hamiltonians # TODO: remove the next two lines # ansatz_mixers.append(0) # ansatz_mixers.append(0) p = len(ansatz_mixers) # adiabatic inspired initialization of beta and gamma initial_params = np.concatenate((np.linspace(0, 1, p), np.linspace(1, 0, p))) # trace and history for analysis and plotting cost_trace = [] params_history = [] @qfunc def adaptive_mixer_layer(mixer_idx: int, beta: CReal, qba: QArray): suzuki_trotter(mixer_pool[ansatz_mixers[mixer_idx]], beta, 1, 1, qba) @qfunc def qaoa_adaptive_ansatz( gammas: CArray[CReal], betas: CArray[CReal], qba: QArray, ): n = gammas.len assert n == betas.len, "Number of gamma and beta parameters must be equal" print(f"Building ansatz with {n} layers.") for i in range(n): cost_layer(gammas[i], qba) adaptive_mixer_layer(i, betas[i], qba) @qfunc def main( params: CArray[ CReal, p * 2 # type: ignore ], # Execution parameters (first half: gammas, second half: betas), used later by the sample method v: Output[QArray[QBit, NUM_QUBITS]], # type: ignore ): allocate(v) hadamard_transform(v) gammas = params[0:p] betas = params[p : p * 2] qaoa_adaptive_ansatz(gammas, betas, v) custom_hardware_settings = CustomHardwareSettings( basis_gates=["cz", "rz", "rx", "ry"] ) # connectivity_map = [ # (0, 1), (1, 2), (2, 3), # (4, 5), (5, 6), (6, 7), # (0, 4), (1, 5), (2, 6), (3, 7) # ], # is_symmetric_connectivity=True, # ) preferences = Preferences(custom_hardware_settings=custom_hardware_settings) qprog = synthesize(main, preferences=preferences) with ExecutionSession( quantum_program=qprog, execution_preferences=ExecutionPreferences( backend_preferences=ClassiqBackendPreferences( backend_name="simulator" ) # _statevector") ), ) as es: with tqdm( total=MAX_ITERATIONS, desc=f"Optimization Progress for {p} layered circuit", leave=True, ) as pbar: def progress_bar(xk: np.ndarray) -> None: pbar.update(1) def obj(params): return objective_func(params, es) optimization_results = scipy.optimize.minimize( fun=obj, x0=initial_params, method="COBYLA", options={"maxiter": MAX_ITERATIONS, "rhobeg": 0.5}, callback=progress_bar, ) print( f"Completed optimization for {p} layered circuit. objective value: {optimization_results.fun}" ) params = optimization_results.x.tolist() last_params = params[:] # copy for final execution # break # TODO: remove # ansatz_mixers.append(mixer_pool[0]) # if p >= 3: # break # compute gradients of mixer pool elements gamma_0 = 0.01 # (or any value below 0.1) @qfunc def main( params: CArray[ CReal, p * 2 # type: ignore ], # Execution parameters (first half: gammas, second half: betas), used later by the sample method v: Output[QArray[QBit, NUM_QUBITS]], # type: ignore ): allocate(v) hadamard_transform(v) gammas = params[0:p] betas = params[p : p * 2] qaoa_adaptive_ansatz(gammas, betas, v) cost_layer(gamma_0, v) print("Computing mixer gradients...") custom_hardware_settings = CustomHardwareSettings( basis_gates=["cz", "rz", "rx", "ry"] ) # connectivity_map = [ # (0, 1), (1, 2), (2, 3), # (4, 5), (5, 6), (6, 7), # (0, 4), (1, 5), (2, 6), (3, 7) # ], # is_symmetric_connectivity=True, # ) preferences = Preferences(custom_hardware_settings=custom_hardware_settings) qprog_grads = synthesize(main, preferences=preferences) # qprog = synthesize(qmod, preferences=prefs) # # # non-transpiled / logical QASM # logical_qasm = qprog.qasm # # # transpiled QASM (if transpilation_option ≠ NONE) # transpiled_qasm = None # if qprog.transpiled_circuit is not None: # transpiled_qasm = qprog.transpiled_circuit.qasm # <- - THIS # # # Transpiled QASM (hardware circuit), if transpilation is enabled: # transpiled_qasm =qprog.transpiled_circuit.qasm # transpiled_qasm =qprog.transpiled_circuit.logigal_to_physical_input_qubit_map # mapping = qprog.data.qubit_mapping # #logical = mapping.logical_outputs # physical = mapping.physical_outputs # print(physical) with ExecutionSession(qprog_grads) as es: gradients = np.abs( np.array([grad_mixer(mp, h, es, params) for mp in mixer_pool]) ) scaled_gradient_norm = np.linalg.norm(gradients) / len(gradients) print(f"{scaled_gradient_norm=}") if scaled_gradient_norm < tol: break g_idx = np.argmax(gradients) print(f"Selected mixer index: {g_idx}") ansatz_mixers.append(g_idx) ``` **Output:** ``` Building ansatz with 1 layers. ``` **Output:** ``` Optimization Progress for 1 layered circuit: 38%|█████████████████████▍ | 23/60 [00:56<01:30, 2.45s/it] ``` **Output:** ``` Completed optimization for 1 layered circuit. objective value: -3.54461806640625 Computing mixer gradients... Building ansatz with 1 layers. scaled_gradient_norm=np.float64(0.03471585218988072) Selected mixer index: 0 Building ansatz with 2 layers. ``` **Output:** ``` Optimization Progress for 2 layered circuit: 45%|█████████████████████████▏ | 27/60 [01:21<01:39, 3.02s/it] ``` **Output:** ``` Completed optimization for 2 layered circuit. objective value: -3.54333486328125 Computing mixer gradients... Building ansatz with 2 layers. scaled_gradient_norm=np.float64(0.04703661991770519) Selected mixer index: 1 Building ansatz with 3 layers. ``` **Output:** ``` Optimization Progress for 3 layered circuit: 50%|████████████████████████████ | 30/60 [01:38<01:38, 3.29s/it] ``` **Output:** ``` Completed optimization for 3 layered circuit. objective value: -3.7976916992187504 Computing mixer gradients... Building ansatz with 3 layers. ``` ```python theme={null} with ExecutionSession(qprog) as es: # Sample the circuit using the optimized parameters res = es.sample({"params": last_params}) ``` Plotting the convergence graph: ```python theme={null} plt.plot(cost_trace) plt.xlabel("Iterations") plt.ylabel("Cost") plt.title("Cost convergence") plt.show() ``` ```python theme={null} res.dataframe ``` ```python theme={null} # h = ( # - 0.1247375 * Pauli.Z(0) * Pauli.Z(1) # - 0.1747375 * Pauli.Z(0) # - 0.1207125 * Pauli.Z(1) * Pauli.Z(2) # - 0.2954500 * Pauli.Z(1) # - 0.1163000 * Pauli.Z(2) * Pauli.Z(3) # - 0.2870125 * Pauli.Z(2) # - 0.1247125 * Pauli.Z(3) * Pauli.Z(4) # - 0.2910125 * Pauli.Z(3) # - 0.1747125 * Pauli.Z(4) # + 1.7093000 * Pauli.I(0) # ) ``` ```python theme={null} print(f"Optimized parameters: {optimization_results.x.tolist()}") sorted_counts = sorted(res.parsed_counts, key=lambda pc: cost_func(pc.state["v"])) num_shots = sum(state.shots for state in res.parsed_counts) for sampled in sorted_counts[:10]: solution = sampled.state["v"] probability = sampled.shots / num_shots cost_value = cost_func(solution) print(f"solution={solution} probability={probability:.3f} cost={cost_value:.3f}") ``` ```python theme={null} res.dataframe.sort_values(by="probability", ascending=False) ``` # Electric Grid Optimization Using QAOA Source: https://docs.classiq.io/explore/applications/optimization/electric_grid_optimization/electric_grid_optimization Open this notebook in GitHub to run it yourself For a set of N power plants (sources) and M consumers, the goal is to supply power to all consumers while meeting the constraints of the power plants and minimizing the total cost of supplying power. The model here is a minor variation of \[[1](#oppwer)]. Mathematical model, minimizing the objective function: $$ z = \sum_{i=1}^{n} \sum_{j=1}^{m} Z_{ij}x_{ij} $$ where $x_{ij}$ is the required values of the transmitted power from source $A_i$ to consumer $B_j$. The unit cost of transmitting power from node $A_i$ to node $B_j$ is $Z_{ij}$. Constraint: the sum of powers flowing from power plant transmission lines to all customer nodes must be up to the power of the source $A_i$: $$ \sum_{j=1}^{M} x_{ij} \leq A_{i} \quad i=1,2,...,N $$ Each consumer receives power $B_{j}$: $$ \sum_{i=1}^{N} x_{ij} = B_{j} \quad j=1,2,...,M $$ This example takes $B_{j} = 1$ and $A_{i} = 2$. Note the use of two kinds of constraints: equality and inequality. ## Building the Problem ```python theme={null} import random import numpy as np import torch random.seed(8) np.random.seed(8) torch.manual_seed(8) ``` **Output:** ``` ``` ```python theme={null} from itertools import product import matplotlib.pyplot as plt import networkx as nx # noqa import numpy as np import pandas as pd # building data matrix, it doesn't need to be a symmetric matrix. cost_matrix = np.array( [[0.5, 1.0, 1.0, 2.1], [1.0, 0.6, 1.4, 1.0], [1.0, 1.4, 0.4, 2.3]] ) Sources = ["A1", "A2", "A3"] Consumers = ["B1", "B2", "B3", "B4"] # number of sources N = len(Sources) # number of consumers M = len(Consumers) graph = nx.DiGraph() graph.add_nodes_from(Sources + Consumers) for n, m in product(range(N), range(M)): graph.add_edges_from([(Sources[n], Consumers[m])], weight=cost_matrix[n, m]) # Plot the graph plt.figure(figsize=(10, 6)) left = nx.bipartite.sets(graph)[0] pos = nx.bipartite_layout(graph, left) nx.draw_networkx(graph, pos=pos, nodelist=Consumers, font_size=22, font_color="None") nx.draw_networkx_nodes( graph, pos, nodelist=Consumers, node_color="#119DA4", node_size=500 ) for fa in Sources: x, y = pos[fa] plt.text( x, y, s=fa, bbox=dict(facecolor="#F43764", alpha=1), horizontalalignment="center", fontsize=15, ) nx.draw_networkx_edges(graph, pos, width=2) labels = nx.get_edge_attributes(graph, "weight") nx.draw_networkx_edge_labels(graph, pos, edge_labels=labels, font_size=12) nx.draw_networkx_labels( graph, pos, labels={co: co for co in Consumers}, font_size=15, font_color="#F4F9E9", ) plt.axis("off") plt.show() ``` output Build the Pyomo mjodel for a classical combinatorial optimization problem: ```python theme={null} import pyomo.environ as pyo from IPython.display import Markdown, display opt_model = pyo.ConcreteModel() sources_lst = range(N) consumers_lst = range(M) opt_model.x = pyo.Var(sources_lst, consumers_lst, domain=pyo.Binary) @opt_model.Constraint(sources_lst) def source_supply_rule(model, n): # constraint (1) return sum(model.x[n, m] for m in consumers_lst) <= 2 @opt_model.Constraint(consumers_lst) def each_consumer_is_supplied_rule(model, m): # constraint (2) return sum(model.x[n, m] for n in sources_lst) == 1 opt_model.cost = pyo.Objective( expr=sum( cost_matrix[n, m] * opt_model.x[n, m] for n in sources_lst for m in consumers_lst ), sense=pyo.minimize, ) ``` Print the classical optimization problem: ```python theme={null} opt_model.pprint() ``` **Output:** ``` 1 Var Declarations x : Size=12, Index={0, 1, 2}*{0, 1, 2, 3} Key : Lower : Value : Upper : Fixed : Stale : Domain (0, 0) : 0 : None : 1 : False : True : Binary (0, 1) : 0 : None : 1 : False : True : Binary (0, 2) : 0 : None : 1 : False : True : Binary (0, 3) : 0 : None : 1 : False : True : Binary (1, 0) : 0 : None : 1 : False : True : Binary (1, 1) : 0 : None : 1 : False : True : Binary (1, 2) : 0 : None : 1 : False : True : Binary (1, 3) : 0 : None : 1 : False : True : Binary (2, 0) : 0 : None : 1 : False : True : Binary (2, 1) : 0 : None : 1 : False : True : Binary (2, 2) : 0 : None : 1 : False : True : Binary (2, 3) : 0 : None : 1 : False : True : Binary 1 Objective Declarations cost : Size=1, Index=None, Active=True Key : Active : Sense : Expression None : True : minimize : 0.5*x[0,0] + x[0,1] + x[0,2] + 2.1*x[0,3] + x[1,0] + 0.6*x[1,1] + 1.4*x[1,2] + x[1,3] + x[2,0] + 1.4*x[2,1] + 0.4*x[2,2] + 2.3*x[2,3] 2 Constraint Declarations each_consumer_is_supplied_rule : Size=4, Index={0, 1, 2, 3}, Active=True Key : Lower : Body : Upper : Active 0 : 1.0 : x[0,0] + x[1,0] + x[2,0] : 1.0 : True 1 : 1.0 : x[0,1] + x[1,1] + x[2,1] : 1.0 : True 2 : 1.0 : x[0,2] + x[1,2] + x[2,2] : 1.0 : True 3 : 1.0 : x[0,3] + x[1,3] + x[2,3] : 1.0 : True source_supply_rule : Size=3, Index={0, 1, 2}, Active=True Key : Lower : Body : Upper : Active 0 : -Inf : x[0,0] + x[0,1] + x[0,2] + x[0,3] : 2.0 : True 1 : -Inf : x[1,0] + x[1,1] + x[1,2] + x[1,3] : 2.0 : True 2 : -Inf : x[2,0] + x[2,1] + x[2,2] + x[2,3] : 2.0 : True 4 Declarations: x source_supply_rule each_consumer_is_supplied_rule cost ``` ## Solving with Classiq Take the specific example outlined above. # ## Generating Parameters for the Quantum Circuit ```python theme={null} from classiq import * from classiq.applications.combinatorial_optimization import CombinatorialProblem combi = CombinatorialProblem(pyo_model=opt_model, num_layers=4, penalty_factor=10) qmod = combi.get_model() ``` # ## Synthesizing the QAOA Circuit and Solving the Problem Synthesize and view the QAOA circuit (ansatz) used to solve the optimization problem: ```python theme={null} qprog = combi.get_qprog() show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/39Z3KwseLb8I9dfXZ3jjAPvBXrL ``` **Output:** ``` https://platform.classiq.io/circuit/39Z3KwseLb8I9dfXZ3jjAPvBXrL?login=True&version=17 ``` Solve the problem by calling the `optimize` method of the `CombinatorialProblem` object. For the classical optimization part of QAOA, define the maximum number of classical iterations (`maxiter`) and the $\alpha$-parameter (`quantile`) for running CVaR-QAOA, an improved variation of the QAOA algorithm \[[3](#cvar)]: ```python theme={null} optimized_params = combi.optimize(maxiter=100, quantile=1) ``` Check the convergence of the run: ```python theme={null} import matplotlib.pyplot as plt plt.plot(combi.cost_trace) plt.xlabel("Iterations") plt.ylabel("Cost") plt.title("Cost convergence") ``` **Output:** ``` Text(0.5, 1.0, 'Cost convergence') ``` output ## Best Solution Statistics ```python theme={null} optimization_result = combi.sample(optimized_params) optimization_result.sort_values(by="cost").head(5) ``` | | solution | probability | cost | | ---- | ------------------------------------------------------ | ----------- | ---- | | 415 | \{'x': \[\[0, 1, 0, 0], \[1, 0, 0, 1], \[0, 0, 1, 0... | 0.000488 | 3.4 | | 1022 | \{'x': \[\[0, 1, 0, 1], \[0, 0, 0, 0], \[1, 0, 1, 0... | 0.000488 | 4.5 | | 348 | \{'x': \[\[1, 0, 0, 0], \[0, 0, 0, 1], \[0, 0, 1, 0... | 0.000488 | 21.9 | | 169 | \{'x': \[\[0, 0, 0, 0], \[0, 1, 0, 0], \[0, 0, 1, 1... | 0.000977 | 23.3 | | 1215 | \{'x': \[\[0, 0, 0, 1], \[1, 0, 0, 0], \[0, 0, 1, 0... | 0.000488 | 23.5 | Compare the optimized results to uniformly sampled results: ```python theme={null} uniform_result = combi.sample_uniform() ``` And compare the histograms: ```python theme={null} optimization_result["cost"].plot( kind="hist", bins=50, edgecolor="black", weights=optimization_result["probability"], alpha=0.6, label="optimized", ) uniform_result["cost"].plot( kind="hist", bins=50, edgecolor="black", weights=uniform_result["probability"], alpha=0.6, label="uniform", ) plt.legend() plt.ylabel("Probability", fontsize=16) plt.xlabel("cost", fontsize=16) plt.tick_params(axis="both", labelsize=14) ``` output ## Best Solution ```python theme={null} # This function plots the solution in a table and a graph def plotting_sol(x_sol, cost, is_classic: bool): x_sol_to_mat = np.reshape(np.array(x_sol), [N, M]) # vector to matrix # opened facilities will be marked in red opened_fac_dict = {} for fa in range(N): if sum(x_sol_to_mat[fa, m] for m in range(M)) > 0: opened_fac_dict.update({Sources[fa]: "background-color: #F43764"}) # classical or quantum if is_classic == True: display(Markdown("**CLASSICAL SOLUTION**")) print("total cost= ", cost) else: display(Markdown("**QAOA SOLUTION**")) print("total cost= ", cost) # plotting in a table df = pd.DataFrame(x_sol_to_mat) df.columns = Consumers df.index = Sources plotable = df.style.apply(lambda x: x.index.map(opened_fac_dict)) display(plotable) # plotting in a graph graph_sol = nx.DiGraph() graph_sol.add_nodes_from(Sources + Consumers) for n, m in product(range(N), range(M)): if x_sol_to_mat[n, m] > 0: graph_sol.add_edges_from( [(Sources[n], Consumers[m])], weight=cost_matrix[n, m] ) plt.figure(figsize=(10, 6)) left = nx.bipartite.sets(graph_sol, top_nodes=Sources)[0] pos = nx.bipartite_layout(graph_sol, left) nx.draw_networkx( graph_sol, pos=pos, nodelist=Consumers, font_size=22, font_color="None" ) nx.draw_networkx_nodes( graph_sol, pos, nodelist=Consumers, node_color="#119DA4", node_size=500 ) for fa in Sources: x, y = pos[fa] if fa in opened_fac_dict.keys(): plt.text( x, y, s=fa, bbox=dict(facecolor="#F43764", alpha=1), horizontalalignment="center", fontsize=15, ) else: plt.text( x, y, s=fa, bbox=dict(facecolor="#F4F9E9", alpha=1), horizontalalignment="center", fontsize=15, ) nx.draw_networkx_edges(graph_sol, pos, width=2) labels = nx.get_edge_attributes(graph_sol, "weight") nx.draw_networkx_edge_labels(graph, pos, edge_labels=labels, font_size=12) nx.draw_networkx_labels( graph_sol, pos, labels={co: co for co in Consumers}, font_size=15, font_color="#F4F9E9", ) plt.axis("off") plt.show() best_solution = optimization_result.loc[optimization_result.cost.idxmin()] plotting_sol( [best_solution.solution["x"][i] for i in range(len(best_solution.solution["x"]))], best_solution.cost, is_classic=False, ) ``` **Output:** ``` ``` **Output:** ``` total cost= 3.4 ```
B1 B2 B3 B4
A1 0 1 0 0
A2 1 0 0 1
A3 0 0 1 0
output ## Comparing to a Classical Solver ```python theme={null} from pyomo.opt import SolverFactory solver = SolverFactory("couenne") solver.solve(opt_model) best_classical_solution = np.array( [pyo.value(opt_model.x[idx]) for idx in np.ndindex(cost_matrix.shape)] ).reshape(cost_matrix.shape) plotting_sol( np.round([pyo.value(opt_model.x[idx]) for idx in np.ndindex(cost_matrix.shape)]), pyo.value(opt_model.cost), is_classic=True, ) ``` **Output:** ``` ``` **Output:** ``` total cost= 2.4999999996418167 ```
B1 B2 B3 B4
A1 1.000000 0.000000 0.000000 0.000000
A2 0.000000 1.000000 0.000000 1.000000
A3 0.000000 -0.000000 1.000000 0.000000
output ## References \[1] [O. V. Shemelova, E. V. Yakovleva, T. G. Makuseva, I. I. Eremina, and O. N. Makusev. (2019). Solving optimization problems when designing power supply circuits. E3S Web of Conferences 124, 04011.](https://www.e3s-conferences.org/articles/e3sconf/pdf/2019/50/e3sconf_ses18_04011.pdf) # Integer Linear Programming Source: https://docs.classiq.io/explore/applications/optimization/integer_linear_programming/integer_linear_programming Open this notebook in GitHub to run it yourself Integer Linear Programming (ILP) seeks a vector of integer numbers that maximizes (or minimizes) a linear cost function under a set of linear equality or inequality constraints [\[1\]](#ilp). In other words, it is an optimization problem where the cost function to optimize and all the constraints are linear and the decision variables are integers. ## Mathematical Formulation The ILP problem can be formulated as follows: given an $n$-dimensional vector $\vec{c} = (c_1, c_2, \ldots, c_n)$, an $m \times n$ matrix $A = (a_{ij})$ with $i=1,\ldots,m$ and $j=1,\ldots,n$, and an $m$-dimensional vector $\vec{b} = (b_1, b_2, \ldots, b_m)$, find an $n$-dimensional vector $\vec{x} = (x_1, x_2, \ldots, x_n)$ with integer entries that maximizes (or minimizes) the cost function: $$ \begin{aligned} \vec{c} \cdot \vec{x} = c_1x_1 + c_2x_2 + \ldots + c_nx_n \end{aligned} $$ subject to these constraints: $$ \begin{aligned} A \vec{x} & \leq \vec{b} \\ x_j & \geq 0, \quad j = 1, 2, \ldots, n \\ x_j & \in \mathbb{Z}, \quad j = 1, 2, \ldots, n \end{aligned} $$ This tutorial guides you through the steps of solving the problem with the Classiq platform, using QAOA \[[2](#qaoa)]. The solution is based on defining a Pyomo model for the optimization problem to solve. ## Building the Pyomo Model from a Graph Input Define the Pyomo model to use on the Classiq platform, using the mathematical formulation defined above: ```python theme={null} import numpy as np import pyomo.core as pyo def ilp(a: np.ndarray, b: np.ndarray, c: np.ndarray, bound: int) -> pyo.ConcreteModel: # model constraint: a*x <= b model = pyo.ConcreteModel() assert b.ndim == c.ndim == 1 num_vars = len(c) num_constraints = len(b) assert a.shape == (num_constraints, num_vars) model.x = pyo.Var( # here we bound x to be from 0 to to a given bound range(num_vars), domain=pyo.NonNegativeIntegers, bounds=(0, bound), ) @model.Constraint(range(num_constraints)) def monotone_rule(model, idx): return a[idx, :] @ list(model.x.values()) <= float(b[idx]) # model objective: max(c * x) model.cost = pyo.Objective(expr=c @ list(model.x.values()), sense=pyo.maximize) return model ``` ```python theme={null} A = np.array([[1, 1, 1], [2, 2, 2], [3, 3, 3]]) b = np.array([1, 2, 3]) c = np.array([1, 2, 3]) # Instantiate the model ilp_model = ilp(A, b, c, 3) ``` ```python theme={null} ilp_model.pprint() ``` **Output:** ``` 1 Var Declarations x : Size=3, Index={0, 1, 2} Key : Lower : Value : Upper : Fixed : Stale : Domain 0 : 0 : None : 3 : False : True : NonNegativeIntegers 1 : 0 : None : 3 : False : True : NonNegativeIntegers 2 : 0 : None : 3 : False : True : NonNegativeIntegers 1 Objective Declarations cost : Size=1, Index=None, Active=True Key : Active : Sense : Expression None : True : maximize : x[0] + 2*x[1] + 3*x[2] 1 Constraint Declarations monotone_rule : Size=3, Index={0, 1, 2}, Active=True Key : Lower : Body : Upper : Active 0 : -Inf : x[0] + x[1] + x[2] : 1.0 : True 1 : -Inf : 2*x[0] + 2*x[1] + 2*x[2] : 2.0 : True 2 : -Inf : 3*x[0] + 3*x[1] + 3*x[2] : 3.0 : True 3 Declarations: x monotone_rule cost ``` ## Setting Up the Classiq Problem Instance To solve the Pyomo model defined above, use the `CombinatorialProblem` quantum object. Under the hood it translates the Pyomo model to a quantum model of QAOA, with the cost Hamiltonian translated from the Pyomo model. Choose the number of layers for the QAOA ansatz using the `num_layers` argument. The `penalty_factor` is the coefficient of the constraints term in the cost Hamiltonian. ```python theme={null} from classiq import * from classiq.applications.combinatorial_optimization import CombinatorialProblem combi = CombinatorialProblem(pyo_model=ilp_model, num_layers=3, penalty_factor=10) qmod = combi.get_model() ``` ## Synthesizing the QAOA Circuit and Solving the Problem Synthesize and view the QAOA circuit (ansatz) used to solve the optimization problem: ```python theme={null} qprog = combi.get_qprog() show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/38w8GTGIHJELpFT0UWvNLftpCI6 ``` **Output:** ``` https://platform.classiq.io/circuit/38w8GTGIHJELpFT0UWvNLftpCI6?login=True&version=15 ``` Set the quantum backend on which to execute: ```python theme={null} from classiq.execution import * execution_preferences = ExecutionPreferences( backend_preferences=ClassiqBackendPreferences(backend_name="simulator"), ) ``` Solve the problem by calling the `optimize` method of the `CombinatorialProblem` object. For the classical optimization part of QAOA, define the maximum number of classical iterations (`maxiter`) and the $\alpha$-parameter (`quantile`) for running CVaR-QAOA, an improved variation of QAOA \[[3](#cvar)]: ```python theme={null} optimized_params = combi.optimize(execution_preferences, maxiter=90, quantile=0.7) ``` Check the convergence of the run: ```python theme={null} import matplotlib.pyplot as plt plt.plot(combi.cost_trace) plt.xlabel("Iterations") plt.ylabel("Cost") plt.title("Cost convergence") ``` **Output:** ``` Text(0.5, 1.0, 'Cost convergence') ``` output ## Optimization Results Examine the statistics of the algorithm. The optimization is always defined as a minimization problem, so the positive maximization objective is translated to negative minimization by the Pyomo-to-Qmod translator. To get samples with the optimized parameters, call the `sample` method: ```python theme={null} optimization_result = combi.sample(optimized_params) optimization_result.sort_values(by="cost").head(5) ``` | | solution | probability | cost | | --- | ------------------------------------------------------- | ----------- | ---- | | 12 | \{'x': \[0, 0, 1], 'monotone\_rule\_1\_slack\_var': ... | 0.012207 | -3.0 | | 223 | \{'x': \[0, 1, 0], 'monotone\_rule\_1\_slack\_var': ... | 0.000488 | -2.0 | | 146 | \{'x': \[1, 0, 0], 'monotone\_rule\_1\_slack\_var': ... | 0.001953 | -1.0 | | 15 | \{'x': \[0, 0, 0], 'monotone\_rule\_1\_slack\_var': ... | 0.011719 | 0.0 | | 224 | \{'x': \[0, 0, 1], 'monotone\_rule\_1\_slack\_var': ... | 0.000488 | 7.0 | Compare the optimized results to uniformly sampled results: ```python theme={null} uniform_result = combi.sample_uniform() ``` And compare the histograms: ```python theme={null} optimization_result["cost"].plot( kind="hist", bins=30, edgecolor="black", weights=optimization_result["probability"], alpha=0.6, label="optimized", ) uniform_result["cost"].plot( kind="hist", bins=30, edgecolor="black", weights=uniform_result["probability"], alpha=0.6, label="uniform", ) plt.legend() plt.ylabel("Probability", fontsize=16) plt.xlabel("cost", fontsize=16) plt.tick_params(axis="both", labelsize=14) ``` output Plot the solution: ```python theme={null} best_solution = optimization_result.solution[optimization_result.cost.idxmin()] best_solution ``` **Output:** ``` {'x': [0, 0, 1], 'monotone_rule_1_slack_var': [0], 'monotone_rule_2_slack_var': [0]} ``` ## Comparing to a Classical Solver Compare to the classical solution of the problem: ```python theme={null} from pyomo.opt import SolverFactory solver = SolverFactory("couenne") solver.solve(ilp_model) classical_solution = [int(pyo.value(ilp_model.x[i])) for i in range(len(ilp_model.x))] print("Classical solution:", classical_solution) ``` **Output:** ``` Classical solution: [0, 0, 1] ``` ## References \[1] [Integer Programming (Wikipedia).](https://en.wikipedia.org/wiki/Integer_programming) \[2] [Farhi, Edward, Jeffrey Goldstone, and Sam Gutmann. (2014). "A quantum approximate optimization algorithm." arXiv preprint arXiv:1411.4028.](https://arxiv.org/abs/1411.4028) \[3] [Barkoutsos, Panagiotis Kl, et al. (2020). "Improving variational quantum optimization using CVaR." Quantum 4: 256.](https://arxiv.org/abs/1907.04769) # Kidney Exchange QAOA Example Source: https://docs.classiq.io/explore/applications/optimization/kidney_exchange/kidney_exchange_problem Open this notebook in GitHub to run it yourself Author: Bill Wisotsky *** Currently, more than 100,000 patients are on the waiting list in the United States for a kidney transplant from a deceased donor. This is addressed by a program called the Kidney Exchange Program. This program won the 2012 Nobel Prize in Economics for Alvin E. Roth and Lloyd S. Shapley's contributions to the theory of stable matchings and the design of markets. In summary, in a donor pair, the recipient needs a kidney transplant and a donor is willing to give a kidney to the recipient. About $\frac{1}{3}$ of those pairs are not compatible for a direct exchange. This is tackled by considering two incompatible pairs together: Donor 1 may be compatible with Recipient 2, and Donor 2 may be compatible with Recipient 1. In this example, a two-way swap becomes feasible. This is the core of the kidney exchange program. This is considered an NP-Hard combinatorial optimization problem that becomes exponentially more difficult as the size of the pool increases. The longest chain in history involved 35 tranplants in the United States in 2015. ```python theme={null} import warnings from itertools import product from typing import List, Tuple, cast # noqa import networkx as nx # noqa import numpy as np from classiq import * warnings.filterwarnings("ignore") ``` ## Creating a Pyomo Model for a Simple Kidney Exchange Problem In this very simple example, patients and donors represent sets of patients who receive a kidney from a donor. Compatibility is a dictionary mapping of patient-donor pairs to their compatibility scores. Binary decision variables are defined for each patient-donor pair `x[donor,patient]`. The objective is to maximize the total compatibility score: $ Maximize \sum_{d,p\in A}^{} \sum_{m\in M}c_{dp}x_{dpm}$ where * d=donors * p=patients * c=compatability score Contraints are added to ensure that each donor donates only once $\sum_{d,p\in A}^{}x_{dpm} = y_{dm}$ and each patient receives once $\sum_{d,p\in A}^{}x_{dpm} = y_{pm}$. Create a PYOMO model to feed into Classiq, as illustrated in the Classiq documentation. Start by solving with a classical solver to get initial results for comparison to the QAOA results at the end. ```python theme={null} from pyomo.environ import * # Sample data: patient-donor pairs and compatibility scores donors = ["donor1", "donor2", "donor3"] patients = ["patient1", "patient2", "patient3"] N = len(patients) M = len(donors) # Parameters compatibility_scores = { ("donor1", "patient1"): 0.9, ("donor1", "patient2"): 0.7, ("donor1", "patient3"): 0.6, ("donor2", "patient1"): 0.8, ("donor2", "patient2"): 0.75, ("donor2", "patient3"): 0.65, ("donor3", "patient1"): 0.85, ("donor3", "patient2"): 0.8, ("donor3", "patient3"): 0.7, } # Create Pyomo model model = ConcreteModel() # Variables model.x = Var(donors, patients, within=Binary) # Objective model.obj = Objective( expr=sum( compatibility_scores[donor, patient] * model.x[donor, patient] for donor in donors for patient in patients ), sense=maximize, ) # Constraints model.donor_constraint = ConstraintList() for donor in donors: model.donor_constraint.add( sum(model.x[donor, patient] for patient in patients) <= 1 ) model.patient_constraint = ConstraintList() for patient in patients: model.patient_constraint.add(sum(model.x[donor, patient] for donor in donors) <= 1) # Install "glpk" and unommente for runing this part # Solve # solver = SolverFactory("glpk") # solver.solve(model) # Output print("\033[1m\033[4mOptimal solution:\033[0m") for donor in donors: for patient in patients: if model.x[donor, patient].value == 1: print(f"{donor} donates kidney to {patient}") print("\n\033[1m\033[4mModel Details\033[0m") model.pprint() ``` **Output:** ``` Optimal solution: Model Details 1 Var Declarations x : Size=9, Index={donor1, donor2, donor3}*{patient1, patient2, patient3} Key : Lower : Value : Upper : Fixed : Stale : Domain ('donor1', 'patient1') : 0 : None : 1 : False : True : Binary ('donor1', 'patient2') : 0 : None : 1 : False : True : Binary ('donor1', 'patient3') : 0 : None : 1 : False : True : Binary ('donor2', 'patient1') : 0 : None : 1 : False : True : Binary ('donor2', 'patient2') : 0 : None : 1 : False : True : Binary ('donor2', 'patient3') : 0 : None : 1 : False : True : Binary ('donor3', 'patient1') : 0 : None : 1 : False : True : Binary ('donor3', 'patient2') : 0 : None : 1 : False : True : Binary ('donor3', 'patient3') : 0 : None : 1 : False : True : Binary 1 Objective Declarations obj : Size=1, Index=None, Active=True Key : Active : Sense : Expression None : True : maximize : 0.9*x[donor1,patient1] + 0.7*x[donor1,patient2] + 0.6*x[donor1,patient3] + 0.8*x[donor2,patient1] + 0.75*x[donor2,patient2] + 0.65*x[donor2,patient3] + 0.85*x[donor3,patient1] + 0.8*x[donor3,patient2] + 0.7*x[donor3,patient3] 2 Constraint Declarations donor_constraint : Size=3, Index={1, 2, 3}, Active=True Key : Lower : Body : Upper : Active 1 : -Inf : x[donor1,patient1] + x[donor1,patient2] + x[donor1,patient3] : 1.0 : True 2 : -Inf : x[donor2,patient1] + x[donor2,patient2] + x[donor2,patient3] : 1.0 : True 3 : -Inf : x[donor3,patient1] + x[donor3,patient2] + x[donor3,patient3] : 1.0 : True patient_constraint : Size=3, Index={1, 2, 3}, Active=True Key : Lower : Body : Upper : Active 1 : -Inf : x[donor1,patient1] + x[donor2,patient1] + x[donor3,patient1] : 1.0 : True 2 : -Inf : x[donor1,patient2] + x[donor2,patient2] + x[donor3,patient2] : 1.0 : True 3 : -Inf : x[donor1,patient3] + x[donor2,patient3] + x[donor3,patient3] : 1.0 : True 4 Declarations: x obj donor_constraint patient_constraint ``` ## Generating the QAOA Process # ## Creating Parameters for the Quantum Circuit Create the initial parameters, modifying them when necessary: 1. Define the number of layers (`num_layers`) of the QAOA ansatz. 2. Define `penalty_energy` for invalid solutions, which influences the convergence rate. While smaller positive values are preferred, you may have to tweak them. ```python theme={null} from classiq.applications.combinatorial_optimization import CombinatorialProblem combi = CombinatorialProblem(pyo_model=model, num_layers=5, penalty_factor=2) # defining constraint such as computer and parameters for a quicker and more optimized circuit. preferences = Preferences(transpilation_option="none", timeout_seconds=300) constraints = Constraints(optimization_parameter="width") qmod = combi.get_model(preferences=preferences, constraints=constraints) ``` # ## Forming the QAOA Model as a Qmod Combine everything together to form the entire QAOA model as a Qmod: 1. Synthesize the quantum model. 2. Show the quantum model in the Classiq platform. ```python theme={null} qprog = combi.get_qprog() show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/38w9AMvPrjARQZZpLPqme8FocHc ``` **Output:** ``` https://platform.classiq.io/circuit/38w9AMvPrjARQZZpLPqme8FocHc?login=True&version=15 ``` # ## Defining the Classical Optimizer Part of the QAOA Modify these parameters: * `max_iterations` - maximum number of optimizer iterations, set to 100. * `quantile` - describes the quantile considered in the CVaR expectation value. See \[[1](#cvar)] for more information. Execute the quantum model and store the result. ```python theme={null} optimized_params = combi.optimize(maxiter=100, quantile=0.7) ``` View the convergence graph.\ **NOTE: When looking at the graph, recall that this is a maximization problem.** ```python theme={null} import matplotlib.pyplot as plt plt.plot(combi.cost_trace) plt.xlabel("Iterations") plt.ylabel("Cost") plt.title("Cost convergence") ``` **Output:** ``` Text(0.5, 1.0, 'Cost convergence') ``` output ## Retrieving and Displaying the Solutions To view the solutions: * Print them out * Graph them using a histogram * Show \`Donor * Recipients\` in Network Graph # ## Print Best Solutions Print out the top 10 solutions with the highest cost or objective: ```python theme={null} optimization_result = combi.sample(optimized_params) print("\n\033[1m\033[4mTop 10 Solutions\033[0m") optimization_result.sort_values(by="cost", ascending=True).head(10) ``` **Output:** ``` Top 10 Solutions ``` | | solution | probability | cost | | --- | ------------------------------------------------------ | ----------- | ----- | | 73 | \{'x\_donor1\_patient1': 0, 'x\_donor1\_patient2': ... | 0.002441 | -2.20 | | 81 | \{'x\_donor1\_patient1': 0, 'x\_donor1\_patient2': ... | 0.002441 | -2.20 | | 203 | \{'x\_donor1\_patient1': 0, 'x\_donor1\_patient2': ... | 0.000977 | -2.20 | | 193 | \{'x\_donor1\_patient1': 0, 'x\_donor1\_patient2': ... | 0.000977 | -2.20 | | 163 | \{'x\_donor1\_patient1': 1, 'x\_donor1\_patient2': ... | 0.000977 | -1.70 | | 83 | \{'x\_donor1\_patient1': 1, 'x\_donor1\_patient2': ... | 0.001953 | -1.65 | | 51 | \{'x\_donor1\_patient1': 1, 'x\_donor1\_patient2': ... | 0.003418 | -1.60 | | 206 | \{'x\_donor1\_patient1': 0, 'x\_donor1\_patient2': ... | 0.000977 | -1.60 | | 36 | \{'x\_donor1\_patient1': 1, 'x\_donor1\_patient2': ... | 0.004395 | -1.55 | | 27 | \{'x\_donor1\_patient1': 0, 'x\_donor1\_patient2': ... | 0.004883 | -1.50 | # ## Histogram of Cost, Weighted by Probability ```python theme={null} import matplotlib.pyplot as plt optimization_result["cost"].plot( kind="hist", bins=30, edgecolor="black", weights=optimization_result["probability"] ) plt.ylabel("Probability", fontsize=12) plt.xlabel("Cost", fontsize=12) plt.tick_params(axis="both", labelsize=12) plt.title("Histogram of Cost Weighted by Probability", fontsize=16) plt.show() ``` output # ## Creating a Network Graph Create a network graph for the best solution found. **NOTE: This is a maximization problem and the classical solver of the QAOA process returns all possible results.** Filter out the solution with the highest cost that represents the the highest compatability score: ```python theme={null} from itertools import product import matplotlib.pyplot as plt import networkx as nx import numpy as np import pandas as pd def plotting_sol(x_sol, cost): # Extract donor and patient names from keys donors = sorted(set(key.split("_")[1] for key in x_sol.keys())) patients = sorted(set(key.split("_")[2] for key in x_sol.keys())) N = len(donors) M = len(patients) # Create mapping to matrix x_mat = np.zeros((N, M), dtype=int) for key, val in x_sol.items(): donor = key.split("_")[1] patient = key.split("_")[2] i = donors.index(donor) j = patients.index(patient) x_mat[i, j] = val print("\033[1m\033[4m** QAOA SOLUTION **\033[0m") print("\033[4mHighest Compatibility Score\033[0m = ", cost) # Table view df = pd.DataFrame(x_mat, index=donors, columns=patients) print(df) # Graph view graph_sol = nx.DiGraph() graph_sol.add_nodes_from(donors + patients) for i, j in product(range(N), range(M)): if x_mat[i, j] > 0: graph_sol.add_edge( donors[i], patients[j], weight=compatibility_scores[(donors[i], patients[j])], ) # Default weight plt.figure(figsize=(10, 6)) pos = nx.bipartite_layout(graph_sol, donors) nx.draw_networkx_nodes( graph_sol, pos, nodelist=donors, node_color="#F43764", node_size=500 ) nx.draw_networkx_nodes( graph_sol, pos, nodelist=patients, node_color="#119DA4", node_size=500 ) nx.draw_networkx_labels(graph_sol, pos, font_size=12) nx.draw_networkx_edges(graph_sol, pos, width=2) labels = nx.get_edge_attributes(graph_sol, "weight") nx.draw_networkx_edge_labels( graph_sol, pos, edge_labels=labels, font_size=10, label_pos=0.6 ) plt.title("Network Graph of the Best Solution", fontsize=16) plt.axis("off") plt.show() best_solution = optimization_result.loc[optimization_result.cost.idxmin()] plotting_sol(best_solution.solution, -best_solution.cost) ``` **Output:** ``` ** QAOA SOLUTION ** Highest Compatibility Score = 2.2 patient1 patient2 patient3 donor1 0 1 0 donor2 0 0 1 donor3 1 0 0 ``` output ## References \[1] [Barkoutsos, P. K., Nannicini, G., Robert, A., Tavernelli, I., & Woerner, S. (2020). Improving variational quantum optimization using CVaR. Quantum, 4, 256.](https://arxiv.org/abs/1907.04769) # Evidence of Scaling Advantage for the QAOA Algorithm on a Classically Intractable Problem Source: https://docs.classiq.io/explore/applications/optimization/low_autocorrelation_binary_sequences_problem/evidence_scaling_labs Open this notebook in GitHub to run it yourself This notebook solve the Low Autocorrlation Binary Sequences (LABS) problem using QAOA. It follows the paper from Shaydulin et al.\[[1](#qaoa-labs)]. Later, the paper accepted to the [Science Advances journal](https://www.science.org/doi/10.1126/sciadv.adm6761). The LABS problem is relevant in various fields, including communications engineering, design of radar pulses, and more, where sequences with low autocorrelation properties are desired. The LABS problem is known to be NP-hard. The LABS problem is defined as follows: For a given sequence of spins $s_{i} \in \{-1,+1\}$, we have the autocorrelation $A_{k}(s)$. First, define $A_{k}(s)$, the autocorrelation: $$ A_{k}(s) = \sum_{i=1}^{N-k} s_{i}s_{i+k} $$ The goal of LABS is to find a sequence of spins $s$ that minimizes the so-called "sidelobe" energy: $$ E_{sidelobe} = \sum_{k=1}^{N-1} A_{k}^{2}(s) $$ ```python theme={null} import numpy as np import pyomo.core as pyo def LABS_pyo_model(N: int) -> pyo.ConcreteModel: # N - the binary sequence size - number of spins # return: pyomo classical optimization model of # Low Binary Autocorrelation Binary Sequence (LABS) problem. model = pyo.ConcreteModel() model.s = pyo.Var(range(N), domain=pyo.Binary) s_array = np.array(list(model.s.values())) # transformation: {0,1} -> {-1,+1} spin_s_array = 2 * s_array - 1 autocorrelation_fun = lambda k: sum( [ spin_s_array[i] * spin_s_array[i + k] for i in range(len(spin_s_array[: N - k])) ] ) model.sidelobe_energy = pyo.Objective( expr=sum([autocorrelation_fun(k) ** 2 for k in range(N - 1)]), sense=pyo.minimize, ) return model ``` ## Define the Pyomo Model ```python theme={null} N = 13 labs_pyo_model = LABS_pyo_model(N) ``` ## Define QAOA Parameters Define the number of layers, the number of iterations, optimizer, and more. And define the number of spins, which is the number of qubits as well. ```python theme={null} from classiq import * from classiq.applications.combinatorial_optimization import OptimizerConfig, QAOAConfig N = 13 qaoa_config = QAOAConfig(num_layers=3, penalty_energy=0.0) optimizer_config = OptimizerConfig(opt_type="COBYLA", max_iteration=60) ``` ## Combine All the QAOA Parameters to Form a Quantum Model ```python theme={null} qmod = construct_combinatorial_optimization_model( pyo_model=labs_pyo_model, qaoa_config=qaoa_config, optimizer_config=optimizer_config, ) ``` ## Synthesize ```python theme={null} qprog = synthesize(model=qmod, constraints=Constraints(optimization_parameter="cx")) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/36pYxIFvXlrAXdIJ3RJ2i6uwwyZ ``` ## Execute ```python theme={null} from classiq import execute res = execute(qprog).result() ``` ## See Results and Convergence Graph and Result Histogram ```python theme={null} vqe_result = res[0].value vqe_result.convergence_graph ``` output ```python theme={null} import pandas as pd from classiq.applications.combinatorial_optimization import ( get_optimization_solution_from_pyo, ) solution = get_optimization_solution_from_pyo( labs_pyo_model, vqe_result=vqe_result, penalty_energy=qaoa_config.penalty_energy ) optimization_result = pd.DataFrame.from_records(solution) optimization_result.sort_values(by="cost", ascending=True).head(5) ``` | | probability | cost | solution | count | | ---- | ----------- | ----- | ---------------------------------------- | ----- | | 1320 | 0.000488 | 182.0 | \[0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1] | 1 | | 1272 | 0.000488 | 182.0 | \[1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0] | 1 | | 1193 | 0.000488 | 182.0 | \[0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1] | 1 | | 1087 | 0.000488 | 186.0 | \[1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0] | 1 | | 1033 | 0.000488 | 186.0 | \[1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1] | 1 | ```python theme={null} optimization_result.hist("cost", weights=optimization_result["probability"]) ``` **Output:** ``` array([[]], dtype=object) ``` output ## References \[1]: [Shaydulin, Ruslan, et al. "Evidence of scaling advantage for the quantum approximate optimization algorithm on a classically intractable problem." arXiv preprint arXiv:2308.02342 (2023).](https://arxiv.org/abs/2308.02342). # Max Clique Problem Source: https://docs.classiq.io/explore/applications/optimization/max_clique/max_clique Open this notebook in GitHub to run it yourself This tutorial solves the max clique problem in graph theory using Classiq. A clique is a subset of vertices in a graph such that each pair is adjacent to one other. Given a graph $G = (V,E)$, find the maximal clique in the graph. It is known to be in the NP-hard complexity class. ## Defining the Optimization Problem Encode each node as a binary variable: ```python theme={null} import networkx as nx import numpy as np import pyomo.environ as pyo def define_max_clique_model(graph): model = pyo.ConcreteModel() # each x_i states if node i belongs to the cliques model.x = pyo.Var(graph.nodes, domain=pyo.Binary) x_variables = np.array(list(model.x.values())) # define the complement adjacency matrix as the matrix where 1 exists for each non-existing edge adjacency_matrix = nx.convert_matrix.to_numpy_array(graph, nonedge=0) complement_adjacency_matrix = ( 1 - nx.convert_matrix.to_numpy_array(graph, nonedge=0) - np.identity(len(model.x)) ) # constraint that 2 nodes without edge in the graph cannot be chosen together model.clique_constraint = pyo.Constraint( expr=x_variables @ complement_adjacency_matrix @ x_variables == 0 ) # maximize the number of nodes in the chosen clique model.value = pyo.Objective(expr=sum(x_variables), sense=pyo.maximize) return model ``` Initialize the model with parameters: ```python theme={null} graph = nx.erdos_renyi_graph(7, 0.6, seed=79) nx.draw_kamada_kawai(graph, with_labels=True) max_clique_model = define_max_clique_model(graph) ``` output ## Setting Up the Classiq Problem Instance To solve the Pyomo model defined above, use the `CombinatorialProblem` Python class. Under the hood, it translates the Pyomo model to a quantum model of the Quantum Approximate Optimization Algorithm (QAOA) \[[1](#qaoa)], with a cost Hamiltonian translated from the Pyomo model. Choose the number of layers for the QAOA ansatz using the `num_layers` argument: ```python theme={null} from classiq import * from classiq.applications.combinatorial_optimization import CombinatorialProblem combi = CombinatorialProblem(pyo_model=max_clique_model, num_layers=3) qmod = combi.get_model() ``` ## Synthesizing the QAOA Circuit and Solving the Problem Synthesize and view the QAOA circuit (ansatz) used to solve the optimization problem: ```python theme={null} qprog = combi.get_qprog() show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/38w9cHY6lrCyQi3RXfs4B8gw3Ys ``` **Output:** ``` https://platform.classiq.io/circuit/38w9cHY6lrCyQi3RXfs4B8gw3Ys?login=True&version=15 ``` Set the quantum backend on which to execute: ```python theme={null} execution_preferences = ExecutionPreferences( backend_preferences=ClassiqBackendPreferences(backend_name="simulator"), ) ``` Solve the problem by calling the `optimize` method of the `CombinatorialProblem` object. For the classical optimization part of the QAOA algorithm, define the maximum number of classical iterations (`maxiter`) and the $\alpha$-parameter (`quantile`) for running CVaR-QAOA, an improved variation of the QAOA algorithm \[[2](#cvar)]: ```python theme={null} optimized_params = combi.optimize(execution_preferences, maxiter=50, quantile=0.7) ``` ```python theme={null} import matplotlib.pyplot as plt plt.plot(combi.cost_trace) plt.xlabel("Iterations") plt.ylabel("Cost") plt.title("Cost convergence") ``` **Output:** ``` Text(0.5, 1.0, 'Cost convergence') ``` output ## Viewing the Optimization Results Examine the statistics of the algorithm. The optimization is always defined as a minimization problem, so the Pyomo-to-Qmod translator changes the positive maximization objective to negative minimization. To get samples with the optimized parameters, call the `sample` method: ```python theme={null} optimization_result = combi.sample(combi.optimized_params) optimization_result.sort_values(by="cost").head(5) ``` | | solution | probability | cost | | -- | ------------------------------ | ----------- | ---- | | 37 | \{'x': \[1, 1, 1, 1, 0, 0, 0]} | 0.006836 | -4 | | 52 | \{'x': \[0, 1, 1, 1, 0, 1, 0]} | 0.004395 | -4 | | 44 | \{'x': \[1, 0, 0, 1, 1, 0, 0]} | 0.005371 | -3 | | 34 | \{'x': \[0, 0, 0, 1, 1, 1, 0]} | 0.007812 | -3 | | 50 | \{'x': \[1, 1, 0, 0, 0, 0, 1]} | 0.004883 | -3 | Compare the optimized results to uniformly sampled results: ```python theme={null} uniform_result = combi.sample_uniform() ``` And compare the histograms: ```python theme={null} optimization_result["cost"].plot( kind="hist", bins=40, edgecolor="black", weights=optimization_result["probability"], alpha=0.6, label="optimized", ) uniform_result["cost"].plot( kind="hist", bins=40, edgecolor="black", weights=uniform_result["probability"], alpha=0.6, label="uniform", ) plt.legend() plt.ylabel("Probability", fontsize=16) plt.xlabel("cost", fontsize=16) plt.tick_params(axis="both", labelsize=14) ``` output Plot the solution: ```python theme={null} best_solution = optimization_result.solution[optimization_result.cost.idxmin()] best_solution ``` **Output:** ``` {'x': [1, 1, 1, 1, 0, 0, 0]} ``` ```python theme={null} solution_nodes = [v for v in graph.nodes if best_solution["x"][v]] solution_edges = [ (u, v) for u, v in graph.edges if u in solution_nodes and v in solution_nodes ] nx.draw_kamada_kawai(graph, with_labels=True) nx.draw_kamada_kawai( graph, with_labels=True, nodelist=solution_nodes, edgelist=solution_edges, node_color="r", edge_color="r", ) ``` output ## Comparing to a Classical Solver Lastly, compare to the classical solution of the problem: ```python theme={null} from pyomo.opt import SolverFactory solver = SolverFactory("couenne") solver.solve(max_clique_model) classical_solution = [ int(pyo.value(max_clique_model.x[i])) for i in range(len(max_clique_model.x)) ] print("Classical solution:", classical_solution) ``` **Output:** ``` Classical solution: [1, 1, 1, 1, 0, 0, 0] ``` ```python theme={null} solution = [int(pyo.value(max_clique_model.x[i])) for i in graph.nodes] solution_nodes = [v for v in graph.nodes if solution[v]] solution_edges = [ (u, v) for u, v in graph.edges if u in solution_nodes and v in solution_nodes ] nx.draw_kamada_kawai(graph, with_labels=True) nx.draw_kamada_kawai( graph, with_labels=True, nodelist=solution_nodes, edgelist=solution_edges, node_color="r", edge_color="r", ) ``` output ## References \[1] [Farhi, Edward, Jeffrey Goldstone, and Sam Gutmann. (2014). A quantum approximate optimization algorithm. arXiv preprint arXiv:1411.4028.](https://arxiv.org/abs/1411.4028) \[2] [Barkoutsos, Panagiotis Kl, et al. (2020). Improving variational quantum optimization using CVaR. Quantum 4: 256.](https://arxiv.org/abs/1907.04769) # Max Independent Set Source: https://docs.classiq.io/explore/applications/optimization/max_independent_set/max_independent_set Open this notebook in GitHub to run it yourself In the Maximum Independent Set Problem \[[1](#miswiki)], the challenge is to find the largest subset of vertices in a given graph, such that no two vertices in the subset are adjacent. This is an NP-hard problem in general graph structures, with applications in various fields such as network design, bioinformatics, and scheduling. ## Mathematical Formulation Given a graph $G=(V,E)$, an independent set $I \subseteq V$ is a set of vertices such that no two vertices in $I$ are adjacent. The Maximum Independent Set Problem is the problem of finding the independent set $I$ with maximum cardinality. In binary form, each vertex $v$ is represented as being in or out of the independent set $I$ by a binary variable $x_v$, with $x_v = 1$ if $v \in I$, and $x_v = 0$ otherwise. The problem can then be formulated as Maximize $\sum_{v \in V} x_v$ subject to $x_{u} + x_{v} \leq 1, \forall (u, v) \in E$ where each $x_v \in {0,1}$. ## Solving with the Classiq Platform Go through the steps of solving the problem with the Classiq platform, using the Quantum Approximate Optimization Algorithm (QAOA) \[[2](#qaoa)]. The solution is based on defining a Pyomo model for the optimization problem to solve: ```python theme={null} import networkx as nx import numpy as np import pyomo.core as pyo from IPython.display import Markdown, display from matplotlib import pyplot as plt ``` ## Building the Pyomo Model from Graph Input Define the Pyomo model to use on the Classiq platform, with the mathematical formulation defined above: ```python theme={null} def mis(graph: nx.Graph) -> pyo.ConcreteModel: model = pyo.ConcreteModel() model.x = pyo.Var(graph.nodes, domain=pyo.Binary) @model.Constraint(graph.edges) def independent_rule(model, node1, node2): return model.x[node1] + model.x[node2] <= 1 model.cost = pyo.Objective(expr=sum(model.x.values()), sense=pyo.maximize) return model ``` The model consists of * Index set declarations (`model.Nodes`, `model.Arcs`). * Binary variable declaration for each node (`model.x`) indicating whether that node is included in the set. * Constraint rule - for each edge, at least one of the corresponding node variables is 0. * Objective rule - the sum of the variables equals the set size. ```python theme={null} num_nodes = 8 p_edge = 0.4 graph = nx.fast_gnp_random_graph(n=num_nodes, p=p_edge, seed=12345) nx.draw_kamada_kawai(graph, with_labels=True) mis_model = mis(graph) ``` output ```python theme={null} mis_model.pprint() ``` **Output:** ``` 1 Var Declarations x : Size=8, Index={0, 1, 2, 3, 4, 5, 6, 7} Key : Lower : Value : Upper : Fixed : Stale : Domain 0 : 0 : None : 1 : False : True : Binary 1 : 0 : None : 1 : False : True : Binary 2 : 0 : None : 1 : False : True : Binary 3 : 0 : None : 1 : False : True : Binary 4 : 0 : None : 1 : False : True : Binary 5 : 0 : None : 1 : False : True : Binary 6 : 0 : None : 1 : False : True : Binary 7 : 0 : None : 1 : False : True : Binary 1 Objective Declarations cost : Size=1, Index=None, Active=True Key : Active : Sense : Expression None : True : maximize : x[0] + x[1] + x[2] + x[3] + x[4] + x[5] + x[6] + x[7] 1 Constraint Declarations independent_rule : Size=14, Index={(0, 7), (2, 4), (1, 2), (0, 4), (3, 4), (1, 5), (1, 4), (0, 6), (0, 2), (2, 6), (5, 6), (3, 6), (2, 5), (3, 5)}, Active=True Key : Lower : Body : Upper : Active (0, 2) : -Inf : x[0] + x[2] : 1.0 : True (0, 4) : -Inf : x[0] + x[4] : 1.0 : True (0, 6) : -Inf : x[0] + x[6] : 1.0 : True (0, 7) : -Inf : x[0] + x[7] : 1.0 : True (1, 2) : -Inf : x[1] + x[2] : 1.0 : True (1, 4) : -Inf : x[1] + x[4] : 1.0 : True (1, 5) : -Inf : x[1] + x[5] : 1.0 : True (2, 4) : -Inf : x[2] + x[4] : 1.0 : True (2, 5) : -Inf : x[2] + x[5] : 1.0 : True (2, 6) : -Inf : x[2] + x[6] : 1.0 : True (3, 4) : -Inf : x[3] + x[4] : 1.0 : True (3, 5) : -Inf : x[3] + x[5] : 1.0 : True (3, 6) : -Inf : x[3] + x[6] : 1.0 : True (5, 6) : -Inf : x[5] + x[6] : 1.0 : True 3 Declarations: x independent_rule cost ``` ## Setting Up the Classiq Problem Instance To solve the Pyomo model defined above, use the `CombinatorialProblem` Python class. Under the hood, it translates the Pyomo model to a quantum model of QAOA, with a cost Hamiltonian translated from the Pyomo model. Choose the number of layers for the QAOA ansatz using the `num_layers` argument: ```python theme={null} from classiq import * from classiq.applications.combinatorial_optimization import CombinatorialProblem combi = CombinatorialProblem(pyo_model=mis_model, num_layers=3) qmod = combi.get_model() ``` ## Synthesizing the QAOA Circuit and Solving the Problem Synthesize and view the QAOA circuit (ansatz) used to solve the optimization problem: ```python theme={null} qprog = combi.get_qprog() show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/39FhfvIzGJnBrxpW2A41Nyl81zx ``` **Output:** ``` https://platform.classiq.io/circuit/39FhfvIzGJnBrxpW2A41Nyl81zx?login=True&version=17 ``` Solve the problem by calling the `optimize` method of the `CombinatorialProblem` object. For the classical optimization part of the QAOA algorithm, define the maximum number of classical iterations (`maxiter`) and the $\alpha$-parameter (`quantile`) for running CVaR-QAOA, an improved variation of QAOA \[[3](#cvar)]: ```python theme={null} optimized_params = combi.optimize(maxiter=60, quantile=0.7) ``` Check the convergence of the run: ```python theme={null} plt.plot(combi.cost_trace) plt.xlabel("Iterations") plt.ylabel("Cost") plt.title("Cost convergence") ``` **Output:** ``` Text(0.5, 1.0, 'Cost convergence') ``` output ## Optimization Results Examine the statistics of the algorithm. The optimization is always defined as a minimization problem, so the Pyomo-to-Qmod translator changes the positive maximization objective to negative minimization. To get samples with the optimized parameters, call the `sample` method: ```python theme={null} optimization_result = combi.sample(optimized_params) optimization_result.sort_values(by="cost").head(5) ``` | | solution | probability | cost | | --- | --------------------------------- | ----------- | ---- | | 0 | \{'x': \[0, 1, 0, 1, 0, 0, 0, 1]} | 0.025391 | -3 | | 8 | \{'x': \[0, 0, 0, 0, 1, 1, 0, 1]} | 0.017578 | -3 | | 35 | \{'x': \[1, 1, 0, 1, 0, 0, 0, 0]} | 0.007812 | -3 | | 79 | \{'x': \[0, 1, 0, 0, 0, 0, 1, 1]} | 0.004395 | -3 | | 108 | \{'x': \[0, 0, 1, 1, 0, 0, 0, 1]} | 0.002930 | -3 | Compare the optimized results to uniformly sampled results: ```python theme={null} uniform_result = combi.sample_uniform() ``` And compare the histograms: ```python theme={null} optimization_result["cost"].plot( kind="hist", bins=50, edgecolor="black", weights=optimization_result["probability"], alpha=0.6, label="optimized", ) uniform_result["cost"].plot( kind="hist", bins=50, edgecolor="black", weights=uniform_result["probability"], alpha=0.6, label="uniform", ) plt.legend() plt.ylabel("Probability", fontsize=16) plt.xlabel("cost", fontsize=16) plt.tick_params(axis="both", labelsize=14) ``` output Plot the best solution: ```python theme={null} best_solution = optimization_result.solution[optimization_result.cost.idxmin()]["x"] ``` ```python theme={null} independent_set = [node for node in graph.nodes if best_solution[node] == 1] print("Independent Set: ", independent_set) print("Size of Independent Set: ", len(independent_set)) ``` **Output:** ``` Independent Set: [1, 3, 7] Size of Independent Set: 3 ``` ```python theme={null} nx.draw_kamada_kawai(graph, with_labels=True) nx.draw_kamada_kawai( graph, with_labels=True, nodelist=independent_set, node_color="r", ) ``` output Lastly, compare to the classical solution of the problem: ```python theme={null} from pyomo.opt import SolverFactory solver = SolverFactory("couenne") solver.solve(mis_model) classical_solution = [pyo.value(mis_model.x[i]) for i in graph.nodes] ``` ```python theme={null} independent_set_classical = [ node for node in graph.nodes if np.allclose(classical_solution[node], 1) ] print("Classical Independent Set: ", independent_set_classical) print("Size of Classical Independent Set: ", len(independent_set_classical)) ``` **Output:** ``` Classical Independent Set: [0, 1, 3] Size of Classical Independent Set: 3 ``` ```python theme={null} nx.draw_kamada_kawai(graph, with_labels=True) nx.draw_kamada_kawai( graph, with_labels=True, nodelist=independent_set_classical, node_color="r", ) ``` output ## References \[1] [Max Independent Set (Wikipedia).](https://en.wikipedia.org/wiki/Partition_problem) \[2] [Farhi, Edward, Jeffrey Goldstone, and Sam Gutmann. (2014). A quantum approximate optimization algorithm. arXiv preprint arXiv:1411.4028.](https://arxiv.org/abs/1411.4028) \[3] [Barkoutsos, Panagiotis Kl, et al. (2020). Improving variational quantum optimization using CVaR. Quantum 4 (2020): 256.](https://arxiv.org/abs/1907.04769) # Max Colorable Induced Subgraph Problem Source: https://docs.classiq.io/explore/applications/optimization/max_induced_k_color_subgraph/max_induced_k_color_subgraph Open this notebook in GitHub to run it yourself ## Background Given a graph $G = (V,E)$ and number of colors K, find the **largest induced subgraph that can be colored using up to K colors**. A coloring is legal if: * each vetrex ${v_i}$ is assigned with a color $k_i \in \{0, 1, ..., k-1\}$ * adajecnt vertex have different colors: for each $v_i, v_j$ such that $(v_i, v_j) \in E$, $k_i \neq k_j$. An induced subgraph of a graph $G = (V,E)$ is a graph $G'=(V', E')$ such that $V'\subset V$ and $E' = \{(v_1, v_2) \in E\ |\ v_1, v_2 \in V'\}$. ## Define the Optimization Problem ```python theme={null} import networkx as nx import numpy as np import pyomo.environ as pyo def define_max_k_colorable_model(graph, K): model = pyo.ConcreteModel() nodes = list(graph.nodes()) colors = range(0, K) # each x_i states if node i belongs to the cliques model.x = pyo.Var(colors, nodes, domain=pyo.Binary) x_variables = np.array(list(model.x.values())) adjacency_matrix = nx.convert_matrix.to_numpy_array(graph, nonedge=0) adjacency_matrix_block_diagonal = np.kron(np.eye(K), adjacency_matrix) # constraint that 2 nodes sharing an edge mustn't have the same color model.conflicting_color_constraint = pyo.Constraint( expr=x_variables @ adjacency_matrix_block_diagonal @ x_variables == 0 ) # each node should be colored @model.Constraint(nodes) def each_node_is_colored_once_or_zero(model, node): return sum(model.x[color, node] for color in colors) <= 1 def is_node_colored(node): is_colored = np.prod([(1 - model.x[color, node]) for color in colors]) return 1 - is_colored # maximize the number of nodes in the chosen clique model.value = pyo.Objective( expr=sum(is_node_colored(node) for node in nodes), sense=pyo.maximize ) return model ``` # ## Initialize the Model with Parameters ```python theme={null} graph = nx.erdos_renyi_graph(6, 0.5, seed=7) nx.draw_kamada_kawai(graph, with_labels=True) NUM_COLORS = 2 coloring_model = define_max_k_colorable_model(graph, NUM_COLORS) ``` output # ## Print the Resulting Pyomo Model ```python theme={null} coloring_model.pprint() ``` **Output:** ``` 1 Var Declarations x : Size=12, Index={0, 1}*{0, 1, 2, 3, 4, 5} Key : Lower : Value : Upper : Fixed : Stale : Domain (0, 0) : 0 : None : 1 : False : True : Binary (0, 1) : 0 : None : 1 : False : True : Binary (0, 2) : 0 : None : 1 : False : True : Binary (0, 3) : 0 : None : 1 : False : True : Binary (0, 4) : 0 : None : 1 : False : True : Binary (0, 5) : 0 : None : 1 : False : True : Binary (1, 0) : 0 : None : 1 : False : True : Binary (1, 1) : 0 : None : 1 : False : True : Binary (1, 2) : 0 : None : 1 : False : True : Binary (1, 3) : 0 : None : 1 : False : True : Binary (1, 4) : 0 : None : 1 : False : True : Binary (1, 5) : 0 : None : 1 : False : True : Binary 1 Objective Declarations value : Size=1, Index=None, Active=True Key : Active : Sense : Expression None : True : maximize : 1 - (1 - x[0,0])*(1 - x[1,0]) + 1 - (1 - x[0,1])*(1 - x[1,1]) + 1 - (1 - x[0,2])*(1 - x[1,2]) + 1 - (1 - x[0,3])*(1 - x[1,3]) + 1 - (1 - x[0,4])*(1 - x[1,4]) + 1 - (1 - x[0,5])*(1 - x[1,5]) 2 Constraint Declarations conflicting_color_constraint : Size=1, Index=None, Active=True Key : Lower : Body : Upper : Active None : 0.0 : (0.0*x[0,0] + x[0,1] + x[0,2] + 0.0*x[0,3] + x[0,4] + 0.0*x[0,5] + 0.0*x[1,0] + 0.0*x[1,1] + 0.0*x[1,2] + 0.0*x[1,3] + 0.0*x[1,4] + 0.0*x[1,5])*x[0,0] + (x[0,0] + 0.0*x[0,1] + x[0,2] + x[0,3] + 0.0*x[0,4] + x[0,5] + 0.0*x[1,0] + 0.0*x[1,1] + 0.0*x[1,2] + 0.0*x[1,3] + 0.0*x[1,4] + 0.0*x[1,5])*x[0,1] + (x[0,0] + x[0,1] + 0.0*x[0,2] + x[0,3] + x[0,4] + x[0,5] + 0.0*x[1,0] + 0.0*x[1,1] + 0.0*x[1,2] + 0.0*x[1,3] + 0.0*x[1,4] + 0.0*x[1,5])*x[0,2] + (0.0*x[0,0] + x[0,1] + x[0,2] + 0.0*x[0,3] + x[0,4] + 0.0*x[0,5] + 0.0*x[1,0] + 0.0*x[1,1] + 0.0*x[1,2] + 0.0*x[1,3] + 0.0*x[1,4] + 0.0*x[1,5])*x[0,3] + (x[0,0] + 0.0*x[0,1] + x[0,2] + x[0,3] + 0.0*x[0,4] + x[0,5] + 0.0*x[1,0] + 0.0*x[1,1] + 0.0*x[1,2] + 0.0*x[1,3] + 0.0*x[1,4] + 0.0*x[1,5])*x[0,4] + (0.0*x[0,0] + x[0,1] + x[0,2] + 0.0*x[0,3] + x[0,4] + 0.0*x[0,5] + 0.0*x[1,0] + 0.0*x[1,1] + 0.0*x[1,2] + 0.0*x[1,3] + 0.0*x[1,4] + 0.0*x[1,5])*x[0,5] + (0.0*x[0,0] + 0.0*x[0,1] + 0.0*x[0,2] + 0.0*x[0,3] + 0.0*x[0,4] + 0.0*x[0,5] + 0.0*x[1,0] + x[1,1] + x[1,2] + 0.0*x[1,3] + x[1,4] + 0.0*x[1,5])*x[1,0] + (0.0*x[0,0] + 0.0*x[0,1] + 0.0*x[0,2] + 0.0*x[0,3] + 0.0*x[0,4] + 0.0*x[0,5] + x[1,0] + 0.0*x[1,1] + x[1,2] + x[1,3] + 0.0*x[1,4] + x[1,5])*x[1,1] + (0.0*x[0,0] + 0.0*x[0,1] + 0.0*x[0,2] + 0.0*x[0,3] + 0.0*x[0,4] + 0.0*x[0,5] + x[1,0] + x[1,1] + 0.0*x[1,2] + x[1,3] + x[1,4] + x[1,5])*x[1,2] + (0.0*x[0,0] + 0.0*x[0,1] + 0.0*x[0,2] + 0.0*x[0,3] + 0.0*x[0,4] + 0.0*x[0,5] + 0.0*x[1,0] + x[1,1] + x[1,2] + 0.0*x[1,3] + x[1,4] + 0.0*x[1,5])*x[1,3] + (0.0*x[0,0] + 0.0*x[0,1] + 0.0*x[0,2] + 0.0*x[0,3] + 0.0*x[0,4] + 0.0*x[0,5] + x[1,0] + 0.0*x[1,1] + x[1,2] + x[1,3] + 0.0*x[1,4] + x[1,5])*x[1,4] + (0.0*x[0,0] + 0.0*x[0,1] + 0.0*x[0,2] + 0.0*x[0,3] + 0.0*x[0,4] + 0.0*x[0,5] + 0.0*x[1,0] + x[1,1] + x[1,2] + 0.0*x[1,3] + x[1,4] + 0.0*x[1,5])*x[1,5] : 0.0 : True each_node_is_colored_once_or_zero : Size=6, Index={0, 1, 2, 3, 4, 5}, Active=True Key : Lower : Body : Upper : Active 0 : -Inf : x[0,0] + x[1,0] : 1.0 : True 1 : -Inf : x[0,1] + x[1,1] : 1.0 : True 2 : -Inf : x[0,2] + x[1,2] : 1.0 : True 3 : -Inf : x[0,3] + x[1,3] : 1.0 : True 4 : -Inf : x[0,4] + x[1,4] : 1.0 : True 5 : -Inf : x[0,5] + x[1,5] : 1.0 : True 4 Declarations: x conflicting_color_constraint each_node_is_colored_once_or_zero value ``` ## Setting Up the Classiq Problem Instance In order to solve the Pyomo model defined above, we use the `CombinatorialProblem` python class. Under the hood it translates the Pyomo model to a quantum model of the QAOA algorithm \[[1](#qaoa)], with cost hamiltonian translated from the Pyomo model. We can choose the number of layers for the QAOA ansatz using the argument `num_layers`. ```python theme={null} from classiq import * from classiq.applications.combinatorial_optimization import CombinatorialProblem combi = CombinatorialProblem(pyo_model=coloring_model, num_layers=8) qmod = combi.get_model() ``` ## Synthesizing the QAOA Circuit and Solving the Problem We can now synthesize and view the QAOA circuit (ansatz) used to solve the optimization problem: ```python theme={null} qprog = combi.get_qprog() show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/38wA0LLtdYyT1s4HTKziIKBxF76 ``` **Output:** ``` https://platform.classiq.io/circuit/38wA0LLtdYyT1s4HTKziIKBxF76?login=True&version=15 ``` We now solve the problem by calling the `optimize` method of the `CombinatorialProblem` object. For the classical optimization part of the QAOA algorithm we define the maximum number of classical iterations (`maxiter`) and the $\alpha$-parameter (`quantile`) for running CVaR-QAOA, an improved variation of the QAOA algorithm \[[2](#cvar)]: ```python theme={null} optimized_params = combi.optimize(maxiter=50, quantile=0.7) ``` We can check the convergence of the run: ```python theme={null} import matplotlib.pyplot as plt plt.plot(combi.cost_trace) plt.xlabel("Iterations") plt.ylabel("Cost") plt.title("Cost convergence") ``` **Output:** ``` Text(0.5, 1.0, 'Cost convergence') ``` output ## Optimization Results We can also examine the statistics of the algorithm. The optimization is always defined as a minimzation problem, so the positive maximization objective was tranlated to a negative minimization one by the Pyomo to qmod translator. In order to get samples with the optimized parameters, we call the `sample` method: ```python theme={null} optimization_result = combi.sample(optimized_params) optimization_result.sort_values(by="cost").head(5) ``` | | solution | probability | cost | | ---- | --------------------------------------------------- | ----------- | ---- | | 109 | \{'x': \[\[0, 0, 1, 0, 0, 0], \[1, 0, 0, 1, 0, 1]]} | 0.001465 | -4 | | 237 | \{'x': \[\[0, 1, 0, 0, 1, 0], \[1, 0, 0, 0, 0, 1]]} | 0.000977 | -4 | | 1072 | \{'x': \[\[0, 1, 0, 0, 0, 0], \[1, 0, 0, 1, 0, 1]]} | 0.000488 | -4 | | 1118 | \{'x': \[\[1, 0, 0, 1, 0, 1], \[0, 0, 1, 0, 0, 0]]} | 0.000488 | -4 | | 386 | \{'x': \[\[0, 0, 0, 0, 1, 0], \[1, 0, 0, 1, 0, 1]]} | 0.000977 | -4 | We will also want to compare the optimized results to uniformly sampled results: ```python theme={null} uniform_result = combi.sample_uniform() ``` And compare the histograms: ```python theme={null} optimization_result["cost"].plot( kind="hist", bins=50, edgecolor="black", weights=optimization_result["probability"], alpha=0.6, label="optimized", ) uniform_result["cost"].plot( kind="hist", bins=50, edgecolor="black", weights=uniform_result["probability"], alpha=0.6, label="uniform", ) plt.legend() plt.ylabel("Probability", fontsize=16) plt.xlabel("cost", fontsize=16) plt.tick_params(axis="both", labelsize=14) ``` output Let us plot the best solution: ```python theme={null} import matplotlib.pyplot as plt best_solution = optimization_result.solution[optimization_result.cost.idxmin()]["x"] one_hot_solution = np.array(best_solution).reshape([NUM_COLORS, len(graph.nodes)]) integer_solution = np.argmax(one_hot_solution, axis=0) colored_nodes = np.array(graph.nodes)[one_hot_solution.sum(axis=0) != 0] colors = integer_solution[colored_nodes] pos = nx.kamada_kawai_layout(graph) nx.draw(graph, pos=pos, with_labels=True, alpha=0.3, node_color="k") nx.draw(graph.subgraph(colored_nodes), pos=pos, node_color=colors, cmap=plt.cm.rainbow) ``` output ## References \[1]: [Farhi, Edward, Jeffrey Goldstone, and Sam Gutmann. "A quantum approximate optimization algorithm." arXiv preprint arXiv:1411.4028 (2014).](https://arxiv.org/abs/1411.4028) \[2]: [Barkoutsos, Panagiotis Kl, et al. "Improving variational quantum optimization using CVaR." Quantum 4 (2020): 256.](https://arxiv.org/abs/1907.04769) # Max K-Vertex Cover Source: https://docs.classiq.io/explore/applications/optimization/max_k_vertex_cover/max_k_vertex_cover Open this notebook in GitHub to run it yourself ## Introduction The Max K-Vertex Cover problem [\[1\]](#mvc) is a classical problem in graph theory and computer science, where we aim to find a set of vertices such that each edge of the graph is incident to at least one vertex in the set, and the size of the set does not exceed a given number $k$. # ## Mathematical Formulation The Max-k Vertex Cover problem can be formulated as an Integer Linear Program (ILP): Minimize: $\sum_{(i,j) \in E} (1 - x_i)(1 - x_j)$ Subject to: $\sum_{i \in V} x_i = k$ and $x_i \in \{0, 1\} \quad \forall i \in V$ Where: * $x_i$ is a binary variable that equals 1 if node $i$ is in the cover and 0 otherwise * $E$ is the set of edges in the graph * $V$ is the set of vertices in the graph * $k$ is the maximum number of vertices allowed in the cover ## Solving with the Classiq Platform We go through the steps of solving the problem with the Classiq platform, using QAOA algorithm \[[2](#qaoa)]. The solution is based on defining a Pyomo model for the optimization problem we would like to solve. ```python theme={null} import networkx as nx import numpy as np import pyomo.core as pyo from IPython.display import Markdown, display from matplotlib import pyplot as plt ``` ## Building the Pyomo Model from a Graph Input We proceed by defining the Pyomo model that will be used on the Classiq platform, using the mathematical formulation defined above: ```python theme={null} def mvc(graph: nx.Graph, k: int) -> pyo.ConcreteModel: model = pyo.ConcreteModel() model.x = pyo.Var(graph.nodes, domain=pyo.Binary) model.amount_constraint = pyo.Constraint(expr=sum(model.x.values()) == k) def obj_expression(model): # number of edges not covered return sum((1 - model.x[i]) * (1 - model.x[j]) for i, j in graph.edges) model.cost = pyo.Objective(rule=obj_expression, sense=pyo.minimize) return model ``` The model contains: * Index set declarations (model.Nodes, model.Arcs). * Binary variable declaration for each node (model.x) indicating whether the variable is chosen for the set. * Constraint rule - ensures that the set is of size k. * Objective rule - counts the number of edges not covered; i.e., both related variables are zero. ```python theme={null} K = 5 num_nodes = 10 p_edge = 0.5 graph = nx.erdos_renyi_graph(n=num_nodes, p=p_edge, seed=13) nx.draw_kamada_kawai(graph, with_labels=True) mvc_model = mvc(graph, K) ``` output ```python theme={null} mvc_model.pprint() ``` **Output:** ``` 1 Var Declarations x : Size=10, Index={0, 1, 2, 3, 4, 5, 6, 7, 8, 9} Key : Lower : Value : Upper : Fixed : Stale : Domain 0 : 0 : None : 1 : False : True : Binary 1 : 0 : None : 1 : False : True : Binary 2 : 0 : None : 1 : False : True : Binary 3 : 0 : None : 1 : False : True : Binary 4 : 0 : None : 1 : False : True : Binary 5 : 0 : None : 1 : False : True : Binary 6 : 0 : None : 1 : False : True : Binary 7 : 0 : None : 1 : False : True : Binary 8 : 0 : None : 1 : False : True : Binary 9 : 0 : None : 1 : False : True : Binary 1 Objective Declarations cost : Size=1, Index=None, Active=True Key : Active : Sense : Expression None : True : minimize : (1 - x[0])*(1 - x[1]) + (1 - x[0])*(1 - x[5]) + (1 - x[0])*(1 - x[6]) + (1 - x[0])*(1 - x[7]) + (1 - x[0])*(1 - x[8]) + (1 - x[1])*(1 - x[2]) + (1 - x[1])*(1 - x[4]) + (1 - x[1])*(1 - x[5]) + (1 - x[1])*(1 - x[6]) + (1 - x[1])*(1 - x[9]) + (1 - x[2])*(1 - x[3]) + (1 - x[2])*(1 - x[4]) + (1 - x[3])*(1 - x[6]) + (1 - x[3])*(1 - x[8]) + (1 - x[4])*(1 - x[6]) + (1 - x[4])*(1 - x[7]) + (1 - x[4])*(1 - x[8]) + (1 - x[4])*(1 - x[9]) + (1 - x[5])*(1 - x[6]) + (1 - x[7])*(1 - x[8]) + (1 - x[8])*(1 - x[9]) 1 Constraint Declarations amount_constraint : Size=1, Index=None, Active=True Key : Lower : Body : Upper : Active None : 5.0 : x[0] + x[1] + x[2] + x[3] + x[4] + x[5] + x[6] + x[7] + x[8] + x[9] : 5.0 : True 3 Declarations: x amount_constraint cost ``` ## Setting Up the Classiq Problem Instance In order to solve the Pyomo model defined above, we use the `CombinatorialProblem` python class. Under the hood it tranlates the Pyomo model to a quantum model of the QAOA algorithm, with cost hamiltonian translated from the Pyomo model. We can choose the number of layers for the QAOA ansatz using the argument `num_layers`, and the `penalty_factor`, which will be the coefficient of the constraints term in the cost hamiltonian. ```python theme={null} from classiq import * from classiq.applications.combinatorial_optimization import CombinatorialProblem combi = CombinatorialProblem(pyo_model=mvc_model, num_layers=3, penalty_factor=10) qmod = combi.get_model() ``` ## Synthesizing the QAOA Circuit and Solving the Problem We can now synthesize and view the QAOA circuit (ansatz) used to solve the optimization problem: ```python theme={null} qprog = combi.get_qprog() show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/38wAZjti7TSIt7NXGCJaFgelCqw ``` **Output:** ``` https://platform.classiq.io/circuit/38wAZjti7TSIt7NXGCJaFgelCqw?login=True&version=15 ``` We also set the quantum backend we want to execute on: ```python theme={null} from classiq.execution import * execution_preferences = ExecutionPreferences( backend_preferences=ClassiqBackendPreferences(backend_name="simulator"), ) ``` We now solve the problem by calling the `optimize` method of the `CombinatorialProblem` object. For the classical optimization part of the QAOA algorithm we define the maximum number of classical iterations (`maxiter`) and the $\alpha$-parameter (`quantile`) for running CVaR-QAOA, an improved variation of the QAOA algorithm \[[3](#cvar)]: ```python theme={null} optimized_params = combi.optimize(execution_preferences, maxiter=90, quantile=0.7) ``` We can check the convergence of the run: ```python theme={null} import matplotlib.pyplot as plt plt.plot(combi.cost_trace) plt.xlabel("Iterations") plt.ylabel("Cost") plt.title("Cost convergence") ``` **Output:** ``` Text(0.5, 1.0, 'Cost convergence') ``` output ## Optimization Results We can also examine the statistics of the algorithm. In order to get samples with the optimized parameters, we call the `sample` method: ```python theme={null} optimization_result = combi.sample(optimized_params) optimization_result.sort_values(by="cost").head(5) ``` | | solution | probability | cost | | --- | --------------------------------------- | ----------- | ---- | | 194 | \{'x': \[1, 1, 0, 1, 1, 0, 0, 0, 1, 0]} | 0.001953 | 1 | | 191 | \{'x': \[1, 1, 0, 1, 1, 0, 1, 0, 0, 0]} | 0.001953 | 2 | | 465 | \{'x': \[0, 1, 0, 0, 1, 0, 1, 1, 1, 0]} | 0.000488 | 2 | | 444 | \{'x': \[0, 1, 0, 1, 1, 1, 0, 0, 1, 0]} | 0.000977 | 2 | | 609 | \{'x': \[1, 1, 1, 0, 1, 0, 0, 0, 1, 0]} | 0.000488 | 2 | We will also want to compare the optimized results to uniformly sampled results: ```python theme={null} uniform_result = combi.sample_uniform() ``` And compare the histograms: ```python theme={null} optimization_result["cost"].plot( kind="hist", bins=40, edgecolor="black", weights=optimization_result["probability"], alpha=0.6, label="optimized", ) uniform_result["cost"].plot( kind="hist", bins=40, edgecolor="black", weights=uniform_result["probability"], alpha=0.6, label="uniform", ) plt.legend() plt.ylabel("Probability", fontsize=16) plt.xlabel("cost", fontsize=16) plt.tick_params(axis="both", labelsize=14) ``` output Let us plot the solution: ```python theme={null} best_solution = optimization_result.solution[optimization_result.cost.idxmin()]["x"] best_solution ``` **Output:** ``` [1, 1, 0, 1, 1, 0, 0, 0, 1, 0] ``` ```python theme={null} def draw_solution(graph: nx.Graph, solution: list): solution_nodes = [v for v in graph.nodes if solution[v]] solution_edges = [ (u, v) for u, v in graph.edges if u in solution_nodes or v in solution_nodes ] nx.draw_kamada_kawai(graph, with_labels=True) nx.draw_kamada_kawai( graph, nodelist=solution_nodes, edgelist=solution_edges, node_color="r", edge_color="y", ) draw_solution(graph, best_solution) ``` output ## Comparison to a Classical Solver Lastly, we can compare to the classical solution of the problem: ```python theme={null} from pyomo.opt import SolverFactory solver = SolverFactory("couenne") solver.solve(mvc_model) classical_solution = [int(pyo.value(mvc_model.x[i])) for i in graph.nodes] ``` ```python theme={null} classical_solution ``` **Output:** ``` [1, 1, 0, 1, 1, 0, 0, 0, 1, 0] ``` ```python theme={null} draw_solution(graph, classical_solution) ``` output ## References \[1]: [Max k-Vertex Cover.](https://arxiv.org/abs/1810.03792) \[2]: [Farhi, Edward, Jeffrey Goldstone, and Sam Gutmann. "A quantum approximate optimization algorithm." arXiv preprint arXiv:1411.4028 (2014).](https://arxiv.org/abs/1411.4028) \[3]: [Barkoutsos, Panagiotis Kl, et al. "Improving variational quantum optimization using CVaR." Quantum 4 (2020): 256.](https://arxiv.org/abs/1907.04769) # Min Graph Coloring Problem Source: https://docs.classiq.io/explore/applications/optimization/min_graph_coloring/min_graph_coloring Open this notebook in GitHub to run it yourself ## Background Given a graph $G = (V,E)$, find the minimal number of colors k required to properly color it. A coloring is legal if: * each vetrex ${v_i}$ is assigned with a color $k_i \in \{0, 1, ..., k-1\}$ * adajecnt vertex have different colors: for each $v_i, v_j$ such that $(v_i, v_j) \in E$, $k_i \neq k_j$. A graph which is k-colorable but not (k-1)-colorable is said to have chromatic number k. The maximum bound on the chromatic number is $D_G + 1$, where $D_G$ is the maximum vertex degree. The graph coloring problem is known to be in the NP-hard complexity class. ## Solving the Problem with Classiq # ## Define the Optimization Problem We encode the graph coloring with a matrix of variables `X` with dimensions $k \times |V|$ using one-hot encoding, such that a $X_{ki} = 1$ means that vertex i is colored by color k. We require that each vertex is colored by exactly one color and that 2 adjacent vertices have different colors. ```python theme={null} import networkx as nx import numpy as np import pyomo.environ as pyo def define_min_graph_coloring_model(graph, max_num_colors): model = pyo.ConcreteModel() nodes = list(graph.nodes()) colors = range(0, max_num_colors) model.x = pyo.Var(colors, nodes, domain=pyo.Binary) x_variables = np.array(list(model.x.values())) adjacency_matrix = nx.convert_matrix.to_numpy_array(graph, nonedge=0) adjacency_matrix_block_diagonal = np.kron(np.eye(degree_max), adjacency_matrix) model.conflicting_color_constraint = pyo.Constraint( expr=x_variables @ adjacency_matrix_block_diagonal @ x_variables == 0 ) @model.Constraint(nodes) def each_vertex_is_colored(model, node): return sum(model.x[color, node] for color in colors) == 1 def is_color_used(color): is_color_not_used = np.prod([(1 - model.x[color, node]) for node in nodes]) return 1 - is_color_not_used # minimize the number of colors in use model.value = pyo.Objective( expr=sum(is_color_used(color) for color in colors), sense=pyo.minimize ) return model ``` # ## Initialize the Model with Example Graph ```python theme={null} graph = nx.erdos_renyi_graph(5, 0.3, seed=79) nx.draw_kamada_kawai(graph, with_labels=True) degree_sequence = sorted((d for n, d in graph.degree()), reverse=True) degree_max = max(degree_sequence) max_num_colors = degree_max coloring_model = define_min_graph_coloring_model(graph, max_num_colors) ``` output # ## Show the Resulting Pyomo Model ```python theme={null} coloring_model.pprint() ``` **Output:** ``` 4 Set Declarations each_vertex_is_colored_index : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 5 : {0, 1, 2, 3, 4} x_index : Size=1, Index=None, Ordered=True Key : Dimen : Domain : Size : Members None : 2 : x_index_0*x_index_1 : 15 : {(0, 0), (0, 1), (0, 2), (0, 3), (0, 4), (1, 0), (1, 1), (1, 2), (1, 3), (1, 4), (2, 0), (2, 1), (2, 2), (2, 3), (2, 4)} x_index_0 : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 3 : {0, 1, 2} x_index_1 : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 5 : {0, 1, 2, 3, 4} 1 Var Declarations x : Size=15, Index=x_index Key : Lower : Value : Upper : Fixed : Stale : Domain (0, 0) : 0 : None : 1 : False : True : Binary (0, 1) : 0 : None : 1 : False : True : Binary (0, 2) : 0 : None : 1 : False : True : Binary (0, 3) : 0 : None : 1 : False : True : Binary (0, 4) : 0 : None : 1 : False : True : Binary (1, 0) : 0 : None : 1 : False : True : Binary (1, 1) : 0 : None : 1 : False : True : Binary (1, 2) : 0 : None : 1 : False : True : Binary (1, 3) : 0 : None : 1 : False : True : Binary (1, 4) : 0 : None : 1 : False : True : Binary (2, 0) : 0 : None : 1 : False : True : Binary (2, 1) : 0 : None : 1 : False : True : Binary (2, 2) : 0 : None : 1 : False : True : Binary (2, 3) : 0 : None : 1 : False : True : Binary (2, 4) : 0 : None : 1 : False : True : Binary 1 Objective Declarations value : Size=1, Index=None, Active=True Key : Active : Sense : Expression None : True : minimize : 1 - (1 - x[0,0])*(1 - x[0,1])*(1 - x[0,2])*(1 - x[0,3])*(1 - x[0,4]) + 1 - (1 - x[1,0])*(1 - x[1,1])*(1 - x[1,2])*(1 - x[1,3])*(1 - x[1,4]) + 1 - (1 - x[2,0])*(1 - x[2,1])*(1 - x[2,2])*(1 - x[2,3])*(1 - x[2,4]) 2 Constraint Declarations conflicting_color_constraint : Size=1, Index=None, Active=True Key : Lower : Body : Upper : Active None : 0.0 : (x[0,1] + x[0,3] + x[0,4])*x[0,0] + (x[0,0] + x[0,3])*x[0,1] + x[0,3]*x[0,2] + (x[0,0] + x[0,1] + x[0,2])*x[0,3] + x[0,0]*x[0,4] + (x[1,1] + x[1,3] + x[1,4])*x[1,0] + (x[1,0] + x[1,3])*x[1,1] + x[1,3]*x[1,2] + (x[1,0] + x[1,1] + x[1,2])*x[1,3] + x[1,0]*x[1,4] + (x[2,1] + x[2,3] + x[2,4])*x[2,0] + (x[2,0] + x[2,3])*x[2,1] + x[2,3]*x[2,2] + (x[2,0] + x[2,1] + x[2,2])*x[2,3] + x[2,0]*x[2,4] : 0.0 : True each_vertex_is_colored : Size=5, Index=each_vertex_is_colored_index, Active=True Key : Lower : Body : Upper : Active 0 : 1.0 : x[0,0] + x[1,0] + x[2,0] : 1.0 : True 1 : 1.0 : x[0,1] + x[1,1] + x[2,1] : 1.0 : True 2 : 1.0 : x[0,2] + x[1,2] + x[2,2] : 1.0 : True 3 : 1.0 : x[0,3] + x[1,3] + x[2,3] : 1.0 : True 4 : 1.0 : x[0,4] + x[1,4] + x[2,4] : 1.0 : True 8 Declarations: x_index_0 x_index_1 x_index x conflicting_color_constraint each_vertex_is_colored_index each_vertex_is_colored value ``` ## Setting Up the Classiq Problem Instance In order to solve the Pyomo model defined above, we use the `CombinatorialProblem` python class. Under the hood it translates the Pyomo model to a quantum model of the QAOA algorithm \[[1](#qaoa)], with cost hamiltonian translated from the Pyomo model. We can choose the number of layers for the QAOA ansatz using the argument `num_layers`. ```python theme={null} from classiq import * from classiq.applications.combinatorial_optimization import CombinatorialProblem combi = CombinatorialProblem(pyo_model=coloring_model, num_layers=6, penalty_factor=10) qmod = combi.get_model() ``` ## Synthesizing the QAOA Circuit and Solving the Problem We can now synthesize and view the QAOA circuit (ansatz) used to solve the optimization problem: ```python theme={null} qprog = combi.get_qprog() show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/2zJB0wcTNTkUTnKwKPK0f4G7edG ``` We now solve the problem by calling the `optimize` method of the `CombinatorialProblem` object. For the classical optimization part of the QAOA algorithm we define the maximum number of classical iterations (`maxiter`) and the $\alpha$-parameter (`quantile`) for running CVaR-QAOA, an improved variation of the QAOA algorithm \[[2](#cvar)]: ```python theme={null} optimized_params = combi.optimize(maxiter=100, quantile=0.7) ``` **Output:** ``` Optimization Progress: 101it [12:59, 7.72s/it] ``` We can check the convergence of the run: ```python theme={null} import matplotlib.pyplot as plt plt.plot(combi.cost_trace) plt.xlabel("Iterations") plt.ylabel("Cost") plt.title("Cost convergence") ``` **Output:** ``` Text(0.5, 1.0, 'Cost convergence') ``` output ## Optimization Results We can also examine the statistics of the algorithm. In order to get samples with the optimized parameters, we call the `sample` method: ```python theme={null} optimization_result = combi.sample(optimized_params) optimization_result.sort_values(by="cost").head(5) ``` | | solution | probability | cost | | ---- | ------------------------------------------------------ | ----------- | ---- | | 957 | \{'x': \[\[0, 1, 1, 0, 1], \[1, 0, 0, 0, 0], \[0, 0... | 0.000488 | 3 | | 1283 | \{'x': \[\[0, 1, 1, 0, 0], \[0, 0, 0, 1, 1], \[1, 0... | 0.000488 | 3 | | 1499 | \{'x': \[\[1, 0, 1, 0, 0], \[0, 0, 0, 1, 0], \[0, 1... | 0.000488 | 3 | | 376 | \{'x': \[\[1, 0, 1, 0, 0], \[0, 1, 0, 0, 0], \[0, 0... | 0.000488 | 3 | | 1435 | \{'x': \[\[1, 0, 1, 0, 0], \[0, 0, 0, 1, 1], \[0, 1... | 0.000488 | 3 | We will also want to compare the optimized results to uniformly sampled results: ```python theme={null} uniform_result = combi.sample_uniform() ``` And compare the histograms: ```python theme={null} optimization_result["cost"].plot( kind="hist", bins=50, edgecolor="black", weights=optimization_result["probability"], alpha=0.6, label="optimized", ) uniform_result["cost"].plot( kind="hist", bins=50, edgecolor="black", weights=uniform_result["probability"], alpha=0.6, label="uniform", ) plt.legend() plt.ylabel("Probability", fontsize=16) plt.xlabel("cost", fontsize=16) plt.tick_params(axis="both", labelsize=14) ``` output Let us plot the solution: ```python theme={null} best_solution = optimization_result.solution[optimization_result.cost.idxmin()] best_solution ``` **Output:** ``` {'x': [[1, 0, 1, 0, 0], [0, 1, 0, 0, 0], [0, 0, 0, 1, 1]]} ``` ```python theme={null} import matplotlib.pyplot as plt best_solution = optimization_result.solution[optimization_result.cost.idxmin()]["x"] one_hot_solution = np.array(best_solution).reshape([max_num_colors, len(graph.nodes)]) integer_solution = np.argmax(one_hot_solution, axis=0) nx.draw_kamada_kawai( graph, with_labels=True, node_color=integer_solution, cmap=plt.cm.rainbow ) ``` output ## References \[1]: [Farhi, Edward, Jeffrey Goldstone, and Sam Gutmann. "A quantum approximate optimization algorithm." arXiv preprint arXiv:1411.4028 (2014).](https://arxiv.org/abs/1411.4028) \[2]: [Barkoutsos, Panagiotis Kl, et al. "Improving variational quantum optimization using CVaR." Quantum 4 (2020): 256.](https://arxiv.org/abs/1907.04769) # Minimum Dominating Set (MDS) Problem Source: https://docs.classiq.io/explore/applications/optimization/minimum_dominating_set/minimum_dominating_set Open this notebook in GitHub to run it yourself The Minimum Dominating Set problem [\[1\]](#mdswiki) is a classical NP-hard problem in computer science and graph theory. In this problem, we are given a graph, and we aim to find the smallest subset of vertices such that every node in the graph is either in the subset or is a neighbor of a node in the subset. We represent the problem as a binary optimization problem. ## Variables: * $x_i$ binary variables that represent whether a node $i$ is in the dominating set or not. ## Constraints: * Every node $i$ is either in the dominating set or connected to a node in the dominating set: $\forall i \in V: x_i + \sum_{j \in N(i)} x_j \geq 1$ Where $N(i)$ represents the neighbors of node $i$. ## Objective * Minimize the size of the dominating set: $\sum_{i\in V}x_i$ ## Solving with the Classiq Platform We go through the steps of solving the problem with the Classiq platform, using QAOA algorithm \[[2](#qaoa)]. The solution is based on defining a pyomo model for the optimization problem we would like to solve. ```python theme={null} import networkx as nx import numpy as np import pyomo.core as pyo from matplotlib import pyplot as plt ``` # ## Building the Pyomo Model from a Graph Input We proceed by defining the pyomo model that will be used on the Classiq platform, using the mathematical formulation defined above: ```python theme={null} def mds(graph: nx.Graph) -> pyo.ConcreteModel: model = pyo.ConcreteModel() model.x = pyo.Var(graph.nodes, domain=pyo.Binary) @model.Constraint(graph.nodes) def dominating_rule(model, idx): sum_of_neighbors = sum(model.x[neighbor] for neighbor in graph.neighbors(idx)) return model.x[idx] + sum_of_neighbors >= 1 model.cost = pyo.Objective(expr=sum(model.x.values()), sense=pyo.minimize) return model ``` The model contains: * Index set declarations (model.Nodes, model.Arcs). * Binary variable declaration for each node (model.x) indicating whether that node is chosen for the set. * Constraint rule - for each node, it must be a part of the chosen set or be neighbored by one. * Objective rule - the sum of the variables equals the set size. ```python theme={null} # generate a random graph G = nx.erdos_renyi_graph(n=6, p=0.6, seed=8) nx.draw_kamada_kawai(G, with_labels=True) mds_model = mds(G) ``` output ```python theme={null} mds_model.pprint() ``` **Output:** ``` 1 Var Declarations x : Size=6, Index={0, 1, 2, 3, 4, 5} Key : Lower : Value : Upper : Fixed : Stale : Domain 0 : 0 : None : 1 : False : True : Binary 1 : 0 : None : 1 : False : True : Binary 2 : 0 : None : 1 : False : True : Binary 3 : 0 : None : 1 : False : True : Binary 4 : 0 : None : 1 : False : True : Binary 5 : 0 : None : 1 : False : True : Binary 1 Objective Declarations cost : Size=1, Index=None, Active=True Key : Active : Sense : Expression None : True : minimize : x[0] + x[1] + x[2] + x[3] + x[4] + x[5] 1 Constraint Declarations dominating_rule : Size=6, Index={0, 1, 2, 3, 4, 5}, Active=True Key : Lower : Body : Upper : Active 0 : 1.0 : x[1] + x[3] + x[5] + x[0] : +Inf : True 1 : 1.0 : x[0] + x[2] + x[4] + x[1] : +Inf : True 2 : 1.0 : x[1] + x[3] + x[4] + x[5] + x[2] : +Inf : True 3 : 1.0 : x[0] + x[2] + x[4] + x[3] : +Inf : True 4 : 1.0 : x[1] + x[2] + x[3] + x[5] + x[4] : +Inf : True 5 : 1.0 : x[0] + x[2] + x[4] + x[5] : +Inf : True 3 Declarations: x dominating_rule cost ``` ## Setting Up the Classiq Problem Instance In order to solve the Pyomo model defined above, we use the `CombinatorialProblem` quantum object. Under the hood it tranlastes the Pyomo model to a quantum model of the QAOA algorithm, with a cost function translated from the Pyomo model. We can choose the number of layers for the QAOA ansatz using the argument `num_layers`, and the `penalty_factor`, which will be the coefficient of the constraints term in the cost hamiltonian. ```python theme={null} from classiq import * from classiq.applications.combinatorial_optimization import CombinatorialProblem combi = CombinatorialProblem(pyo_model=mds_model, num_layers=6, penalty_factor=10) qmod = combi.get_model() ``` ## Synthesizing the QAOA Circuit and Solving the Problem We can now synthesize and view the QAOA circuit (ansatz) used to solve the optimization problem: ```python theme={null} qprog = combi.get_qprog() show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/39FhnHoSwCsB4tSjvXSnQvo9TAl ``` **Output:** ``` https://platform.classiq.io/circuit/39FhnHoSwCsB4tSjvXSnQvo9TAl?login=True&version=17 ``` We now solve the problem by calling the `optimize` method of the `CombinatorialProblem` object. For the classical optimization part of the QAOA algorithm we define the maximum number of classical iterations (`maxiter`) and the $\alpha$-parameter (`quantile`) for running CVaR-QAOA, an improved variation of the QAOA algorithm \[[3](#cvar)]: ```python theme={null} optimized_params = combi.optimize(maxiter=70, quantile=0.7) ``` We can check the convergence of the run: ```python theme={null} import matplotlib.pyplot as plt fig, axes = plt.subplots(nrows=1, ncols=1) axes.plot(combi.cost_trace) axes.set_xlabel("Iterations") axes.set_ylabel("Cost") axes.set_title("Cost convergence") ``` **Output:** ``` Text(0.5, 1.0, 'Cost convergence') ``` output ## Optimization Results We can also examine the statistics of the algorithm. In order to get samples with the optimized parameters, we call the `sample` method: ```python theme={null} optimization_result = combi.sample(optimized_params) optimization_result.sort_values(by="cost").head(5) ``` | | solution | probability | cost | | ---- | ------------------------------------------------------ | ----------- | ---- | | 1068 | \{'x': \[1, 0, 0, 1, 0, 0], 'dominating\_rule\_0\_s... | 0.000488 | 2.0 | | 1531 | \{'x': \[0, 0, 1, 1, 1, 0], 'dominating\_rule\_0\_s... | 0.000488 | 3.0 | | 432 | \{'x': \[0, 1, 1, 0, 1, 0], 'dominating\_rule\_0\_s... | 0.000488 | 3.0 | | 309 | \{'x': \[0, 0, 1, 0, 1, 1], 'dominating\_rule\_0\_s... | 0.000488 | 3.0 | | 13 | \{'x': \[0, 1, 1, 0, 1, 0], 'dominating\_rule\_0\_s... | 0.000977 | 3.0 | We will also want to compare the optimized results to uniformly sampled results: ```python theme={null} uniform_result = combi.sample_uniform() ``` And compare the histograms: ```python theme={null} optimization_result["cost"].plot( kind="hist", bins=50, edgecolor="black", weights=optimization_result["probability"], alpha=0.6, label="optimized", ) uniform_result["cost"].plot( kind="hist", bins=50, edgecolor="black", weights=uniform_result["probability"], alpha=0.6, label="uniform", ) plt.legend() plt.ylabel("Probability", fontsize=16) plt.xlabel("cost", fontsize=16) plt.tick_params(axis="both", labelsize=14) ``` output Let us plot the solution: ```python theme={null} best_solution = optimization_result.solution[optimization_result.cost.idxmin()] best_solution ``` **Output:** ``` {'x': [1, 0, 0, 1, 0, 0], 'dominating_rule_0_slack_var': [1, 0], 'dominating_rule_1_slack_var': [0, 0], 'dominating_rule_2_slack_var': [0, 0, 0], 'dominating_rule_3_slack_var': [1, 0], 'dominating_rule_4_slack_var': [0, 0, 0], 'dominating_rule_5_slack_var': [0, 0]} ``` ```python theme={null} def draw_solution(graph: nx.Graph, solution: list): solution_nodes = [v for v in graph.nodes if solution[v]] solution_edges = [ (u, v) for u, v in graph.edges if u in solution_nodes or v in solution_nodes ] nx.draw_kamada_kawai(graph, with_labels=True) nx.draw_kamada_kawai( graph, nodelist=solution_nodes, edgelist=solution_edges, node_color="r", edge_color="y", ) draw_solution(G, [best_solution["x"][i] for i in range(len(best_solution["x"]))]) ``` output Lastly, we can compare to the classical solution of the problem: ```python theme={null} from pyomo.opt import SolverFactory solver = SolverFactory("couenne") solver.solve(mds_model) mds_model.display() classical_solution = [int(pyo.value(mds_model.x[i])) for i in G.nodes] ``` **Output:** ``` Model unknown Variables: x : Size=6, Index={0, 1, 2, 3, 4, 5} Key : Lower : Value : Upper : Fixed : Stale : Domain 0 : 0 : 1.0 : 1 : False : False : Binary 1 : 0 : 0.0 : 1 : False : False : Binary 2 : 0 : 0.0 : 1 : False : False : Binary 3 : 0 : 1.0 : 1 : False : False : Binary 4 : 0 : 0.0 : 1 : False : False : Binary 5 : 0 : 0.0 : 1 : False : False : Binary Objectives: cost : Size=1, Index=None, Active=True Key : Active : Value None : True : 2.0 Constraints: dominating_rule : Size=6 Key : Lower : Body : Upper 0 : 1.0 : 2.0 : None 1 : 1.0 : 1.0 : None 2 : 1.0 : 1.0 : None 3 : 1.0 : 2.0 : None 4 : 1.0 : 1.0 : None 5 : 1.0 : 1.0 : None ``` ```python theme={null} draw_solution(G, classical_solution) ``` output ## References \[1]: [Dominating Set (Wikipedia)](https://en.wikipedia.org/wiki/Partition_problem) \[2]: [Farhi, Edward, Jeffrey Goldstone, and Sam Gutmann. "A quantum approximate optimization algorithm." arXiv preprint arXiv:1411.4028 (2014).](https://arxiv.org/abs/1411.4028) \[3]: [Barkoutsos, Panagiotis Kl, et al. "Improving variational quantum optimization using CVaR." Quantum 4 (2020): 256.](https://arxiv.org/abs/1907.04769) # Hybrid Classical-Quantum Simulation of MaxCut Using QAOA-in-QAOA Source: https://docs.classiq.io/explore/applications/optimization/qaoa_in_qaoa/qaoa_in_qaoa Open this notebook in GitHub to run it yourself ## Introduction The Quantum approximate optimization algorithm (QAOA) is a leading hybrid classical-quantum algorithm for solving complex combinatorial optimization problems. QAOA-in-QAOA (QAOA^2) uses a divide-and-conquer heuristic to solve large-scale Maximum Cut (MaxCut) problems, where many subgraph problems can be solved in parallel. In this work, an implementation of the QAOA2 method for the scalable solution of the MaxCut problem is presented, based on the Classiq platform. The framework is executed on an HPE-Cray EX supercomputer by means of the Message Passing Interface (MPI) and the SLURM workload manager. The limits of the Goemans-Williamson (GW) algorithm as a purely classical alternative to QAOA are investigated to understand if QAOA^2 could benefit from solving certain sub-graphs classically. Results from large-scale simulations of up to 33 qubits are presented, showing the advantage of QAOA in certain cases and the efficiency of the implementation, as well as the adequacy of the workflow in the preparation of real quantum devices. For the considered graphs, the best choice for the sub-graphs does not significantly improve results and is still outperformed by GW. ## Note This notebook is not suited for the HPC run, see the original paper for details: [https://arxiv.org/abs/2406.17383](https://arxiv.org/abs/2406.17383) ```python theme={null} import copy import random from itertools import chain, combinations, product from typing import Dict, List, Set, Tuple import networkx as nx import numpy as np import pyomo.core as pyo from matplotlib import pyplot as plt from classiq import ( Preferences, construct_combinatorial_optimization_model, execute, set_execution_preferences, set_preferences, show, synthesize, ) from classiq.applications.combinatorial_optimization import ( OptimizerConfig, QAOAConfig, get_optimization_solution_from_pyo, ) from classiq.execution import ExecutionPreferences random.seed(246) np.random.seed(4812) def random_partition(subgraph: nx.Graph) -> Dict[int, int]: """ graph without edges can take random solution :param subgraph: :return: keys are nodes and values random solution """ nodes_list = list(subgraph.nodes()) random_sol = [random.choice([0, 1]) for _ in range(len(nodes_list))] return dict(list(zip(nodes_list, random_sol))) def random_combination(data, sample_size): combs = list(combinations(data, sample_size)) index = np.random.randint(len(combs)) return list(combs[index]) def random_split( graph: nx.Graph, max_nodes: int, ) -> List[Set[int]]: # Generate communities list return_comminities = [] node_lst = copy.copy(list(graph.nodes)) while len(node_lst) > 0: try: community = random.sample(sorted(node_lst), min(len(node_lst), max_nodes)) except Exception as e: print(f"An error occurred with random.sample: {e}") print(f"the nodes list which fails in random.sample is: {list(node_lst)}") print(f"The desired sample size is {min(len(node_lst), max_nodes)}") community = random_combination( list(node_lst), min(len(node_lst), max_nodes) ) node_lst = list(set(node_lst) - set(community)) return_comminities.append(set(community)) return return_comminities def split2communities( graph: nx.Graph, max_nodes: int, recursive_level: int = 0, random_split_flag: bool = False, ) -> List[Set[int]]: """ split graph to communities using modularity :param recursive_level: number of recursion :param graph: big graph to split :param max_nodes: max nodes in each community :return: list of nodes division to communities not larger than max_nodes """ if random_split_flag: return random_split(graph=graph, max_nodes=max_nodes) # Generate communities list return_comminities = [] # try using modularity method try: initial_communities = list(nx.community.greedy_modularity_communities(graph)) except Exception as e: print(f"Greedy modularity failed with error: {e}") return random_split(graph=graph, max_nodes=max_nodes) # splitting randomly each community with larger nodes than max_nodes for community in initial_communities: if recursive_level > 100 and len(community) > max_nodes: # If community is too large, we randomly split it into smaller communities of size <= max_nodes print("Split randomly needed") while len(community) > 0: if len(community) > max_nodes: try: new_community = random.sample( sorted(community), min(int(np.ceil(len(community) / 2)), max_nodes), ) except Exception as e: print(f"An error occurred with random.sample: {e}") print( f"the community which fails in random.sample is {list(community)}" ) print( f"the sample size to take from is {min(int(np.ceil(len(community) / 2)), max_nodes)}" ) new_community = random_combination( data=list(community), sample_size=min( int(np.ceil(len(community) / 2)), max_nodes ), ) return_comminities.append(set(new_community)) community -= set(new_community) else: return_comminities.append(set(copy.copy(community))) community -= set(copy.copy(community)) elif len(community) > max_nodes: sub_communities = split2communities( graph=graph.subgraph(community), max_nodes=max_nodes, recursive_level=recursive_level + 1, random_split_flag=random_split_flag, ) return_comminities = return_comminities + sub_communities else: return_comminities.append(community) return return_comminities # we define a function which returns 1 if two connected nodes are on a different subset, and 0 otherwise def arithmetic_eq(x1: int, x2: int) -> int: return x1 + x2 - 2 * x1 * x2 def maxcut(graph: nx.Graph) -> pyo.ConcreteModel: # we define a function which returns the pyomo model for a graph input model = pyo.ConcreteModel() model.x = pyo.Var(graph.nodes, domain=pyo.Binary) model.cost = pyo.Objective( expr=sum( graph[node1][node2].get("weight", 1.0) * arithmetic_eq(model.x[node1], model.x[node2]) for (node1, node2) in graph.edges ), sense=pyo.maximize, ) return model def exec_qaoa_classiq_sort_res( subgraph: nx.Graph, pyo_model: pyo.ConcreteModel, qaoa_config: QAOAConfig, optimizer_config: OptimizerConfig, ) -> dict: if len(subgraph.edges) == 0: print("Subgraph has no edges, qaoa has no meaning") return random_partition(subgraph=subgraph) elif len(subgraph.nodes) > 1: qmod = construct_combinatorial_optimization_model( pyo_model=pyo_model, qaoa_config=qaoa_config, optimizer_config=optimizer_config, ) qmod = set_preferences( qmod, Preferences(transpilation_option="none", random_seed=10), ) qmod = set_execution_preferences(qmod, ExecutionPreferences(random_seed=10)) qprog = synthesize(qmod) show(qprog) subgraph_res = execute(qprog) ordered_solution = get_optimization_solution_from_pyo( pyo_model=pyo_model, vqe_result=subgraph_res.result_value(), penalty_energy=qaoa_config.penalty_energy, ) # sort by cost and then by count exec_data = sorted( ordered_solution, key=lambda x: (x["cost"], x["count"]), reverse=True ) return node2value(subgraph=subgraph, solution=exec_data[0]["solution"]) else: return {list(subgraph.nodes())[0]: 0} def node2value(subgraph: nx.Graph, solution: List[int]) -> dict: """ keys are the nodes, values QAOA solution :param subgraph: :param solution: :return: {0:1, 1:0, 2:1, 3:0, 33:0} """ # TODO: fix ordering # sorted_nodes = sorted(subgraph.nodes()) nodes_list = list(subgraph.nodes()) nodes_res_list = list(zip(nodes_list, solution)) return dict(nodes_res_list) def communities2graph( entire_graph: nx.Graph, communities: List[Set[int]], solution: dict ) -> nx.Graph: """ Build a graph from communities :param entire_graph: entire problem graph :param communities: list of communities :param solution: solution of qaoa, keys are the nodes, value 0,1 :return: graph that each node represents a community """ # Initialize a new graph for communities community_graph = nx.Graph() # Add nodes to the community graph community_graph.add_nodes_from(range(len(communities))) # Calculate edges weights between communities and add weighted edges to the community graph for i, j in combinations(range(len(communities)), 2): # Calculate the number of edges between community i and community j weight = 0 for a, b in product(communities[i], communities[j]): if entire_graph.has_edge(a, b): # entire_graph[i][j].get('weight', 1.0) # if solution[a] == solution[b]: # weight += 1 # else: # weight -= 1 if solution[a] == solution[b]: weight += entire_graph[a][b].get("weight", 1.0) else: weight -= entire_graph[a][b].get("weight", 1.0) if weight != 0: community_graph.add_edge(i, j, weight=weight) return community_graph def nodes_in_community( basic_nodes_communities: dict, communities: List, ) -> Dict[int, list]: """ take communities list and put the basic nodes in a list that belong to this community_graph. basic nodes: nodes from the original graph :param basic_nodes_communities: represents basic nodes from previous graph. if empty, the first community graph :param communities: separation of graph into communities :return: dict((community_node, list of basic nodes) """ new_basic_nodes_communities = copy.copy(basic_nodes_communities) return_basic_nodes_dict = {} if new_basic_nodes_communities == {}: for node, community in enumerate(communities): # new_basic_nodes_communities[node] = list(community) return_basic_nodes_dict[node] = list(community) else: for new_node, higher_community in enumerate(communities): gather_lst = [] for old_node in higher_community: gather_lst.append(basic_nodes_communities[old_node]) flat_list = list(chain.from_iterable(gather_lst)) # new_basic_nodes_communities[new_node] = flat_list return_basic_nodes_dict[new_node] = flat_list # return new_basic_nodes_communities return return_basic_nodes_dict def calc_cut(G: nx.Graph, solution: Dict[int, int]) -> float: """ calculated cut of a graph, weighted cut hasn't been checked :param G: entire graph to calculate its maxcut :param solution: keys are node number, values are their boolean solution :return: cut result """ cut = 0.0 for i, j in G.edges(): if solution[i] != solution[j]: cut += G[i][j].get("weight", 1.0) return cut ``` ```python theme={null} np.random.seed(4812) random.seed(246) # parameter LOCAL_QISKIT_EXECUTION = False # args.local_execution MAX_NUM_QUBITS = 6 TOTAL_NODES = 40 EDGE_PROB = 0.2 NUM_LAYERS = 2 ITERATIONS = 20 RANDOM_GRAPH_SPLIT = False # quantum functions qaoa_config = QAOAConfig(num_layers=NUM_LAYERS, penalty_energy=0.0) optimizer_config = OptimizerConfig(max_iteration=ITERATIONS, alpha_cvar=1.0) # Create the initial graph G = nx.erdos_renyi_graph(TOTAL_NODES, EDGE_PROB) print("number of edges: ", len(G.edges)) # Get the communities communities = split2communities( graph=G, max_nodes=MAX_NUM_QUBITS, recursive_level=0, random_split_flag=RANDOM_GRAPH_SPLIT, ) print("number of communities: ", len(communities)) # Initialize a list to store subgraphs and solutions community_subgraphs_and_sols = [] initial_full_sol = {} # executing qaoa to each subgraph for i, community in enumerate(communities): subgraph = G.subgraph(community) print(len(subgraph.nodes)) res = exec_qaoa_classiq_sort_res( subgraph=subgraph, pyo_model=maxcut(subgraph), qaoa_config=qaoa_config, optimizer_config=optimizer_config, ) community_subgraphs_and_sols.append((subgraph, res)) initial_full_sol.update(res) community_graph = communities2graph( entire_graph=G, communities=communities, solution=initial_full_sol ) # represent the nodes from initial G that inside each node in community_graph. # keeps updating in next loops basic_nodes_in_community = nodes_in_community( basic_nodes_communities={}, communities=communities ) print(initial_full_sol) # (while loop) split the graph if len(communities) > MAX_NUM_QUBITS iterative_num_communities = len(communities) if iterative_num_communities > MAX_NUM_QUBITS: while iterative_num_communities > MAX_NUM_QUBITS: # Get the communities from community_graph communities_of_community_graph = split2communities( graph=community_graph, max_nodes=MAX_NUM_QUBITS, recursive_level=0, random_split_flag=RANDOM_GRAPH_SPLIT, ) iterative_num_communities = len(communities_of_community_graph) intermidiate_sol = {} for i, community_of_graphs in enumerate(communities_of_community_graph): subgraph = community_graph.subgraph(community_of_graphs) print(len(subgraph.nodes)) # executing qaoa to each subgraph res = exec_qaoa_classiq_sort_res( subgraph=subgraph, pyo_model=maxcut(subgraph), qaoa_config=qaoa_config, optimizer_config=optimizer_config, ) intermidiate_sol.update(res) for k, v in intermidiate_sol.items(): if v == 1: for bn in basic_nodes_in_community[k]: # flipping 0 -> 1 and 1 -> 0 initial_full_sol[bn] = (initial_full_sol[bn] + 1) % 2 community_graph = communities2graph( entire_graph=community_graph, communities=communities_of_community_graph, solution=intermidiate_sol, ) basic_nodes_in_community = nodes_in_community( basic_nodes_communities=basic_nodes_in_community, communities=communities_of_community_graph, ) res = exec_qaoa_classiq_sort_res( subgraph=community_graph, pyo_model=maxcut(community_graph), qaoa_config=qaoa_config, optimizer_config=optimizer_config, ) for k, v in res.items(): if v == 1: for bn in basic_nodes_in_community[k]: # flipping 0 -> 1 and 1 -> 0 initial_full_sol[bn] = (initial_full_sol[bn] + 1) % 2 else: res = exec_qaoa_classiq_sort_res( subgraph=community_graph, pyo_model=maxcut(community_graph), qaoa_config=qaoa_config, optimizer_config=optimizer_config, ) print(res) for k, v in res.items(): if v == 1: for bn in basic_nodes_in_community[k]: # flipping 0 -> 1 and 1 -> 0 initial_full_sol[bn] = (initial_full_sol[bn] + 1) % 2 print(initial_full_sol) qaoa_squared_cut = calc_cut(G=G, solution=initial_full_sol) # gw_cut = SDP_max_cut(G=G) greedy_one_exchange = nx.algorithms.approximation.maxcut.one_exchange(G=G)[0] random_pratition = nx.algorithms.approximation.maxcut.randomized_partitioning(G=G)[0] print("CUT OF QAOA SQUARE", qaoa_squared_cut) # print("CUT of Goemans-Williamson classical algorithm: ", gw_cut['gw_mean']) print("greedy one exchange strategy: ", greedy_one_exchange) print("random partition cut: ", random_pratition) pos = nx.spring_layout(G) node_colors = ["blue" if initial_full_sol[node] == 0 else "red" for node in G.nodes()] edge_colors = [] for edge in G.edges(): if initial_full_sol[edge[0]] == initial_full_sol[edge[1]]: edge_colors.append("black") else: edge_colors.append("orange") nx.draw( G, pos, node_color=node_colors, edge_color=edge_colors, with_labels=True, width=2, node_size=150, ) # Show the graph plt.show() ``` **Output:** ``` number of edges: 158 number of communities: 10 6 ``` # Solving the Rectangles Packing Problem with Classiq Source: https://docs.classiq.io/explore/applications/optimization/rectangles_packing/rectangles_packing_grid Open this notebook in GitHub to run it yourself ## Rectangle Packing \*\*The rectangle packing problem is a classic optimization problem where the goal is to pack a set of given rectangles into a larger container rectangle (or bin) in a way that optimizes certain criteria, such as minimizing the total area used, minimizing wasted space, or maximizing the number of rectangles packed. This problem arises in various practical applications, including logistics (loading containers), manufacturing (cutting stock problems), and electronics (VLSI design).\*\* floorplan_example_bigger.png In this demonstration, we explore the rectangle packing problem where the objective is to arrange N rectangles of different sizes within a fixed grid container. The challenge lies in efficiently positioning these rectangles to maximize space utilization without overlap. This problem is a common optimization task with applications in areas such as logistics and manufacturing. ## The Quantum Approximate Optimization Algorithm (QAOA) for Rectangles Packing The rectangle packing problem is NP-hard, meaning that as the number of rectangles increases, the computational effort required grows exponentially for classical algorithms. QAOA offers a potential exponential speedup. Solving the rectangles problem using QAOA holds promise due to the potential computational advantages offered by quantum computing, particularly in tackling the complexity and size of the problem more efficiently than classical methods. ## Solving QAOA with Classiq Classiq allows expressing a given optimization challenge in 3 simple steps: 1. ***Define the optimization problem*** Classiq seamlessly integrate a well known open source **classical** optimization modeling language with a diverse set of optimization capabilities (Pyomo). The Pyomo language supports a wide variety of problem types, such as integer linear programming, quadratic programming, graph theory problems, SAT problems, and many more. 1. ***Plug the optimization model into Classiq*** The **Combinatorial Optimization engine fully translates the Pyomo model to Qmod** which is then synthesized into a Quantum Program object that encapsulates the QAOA implementation. The Quantum Program can be visually analyzed for debugging and even educational purposes. 1. ***Execute and analyze results*** Classiq allows **execution of the Quantum Program** on any leading quantum backend - hardware or simulator. # ## 1. Define the Optimization Problem We will first define a **classical** optimization model: We encode the rectangles positions into a binary variable that represents the rectangle id and its position within a given 2-dimensional container grid. We require that each rectangle is placed at most once and that rectangles are placed within the container grid and do not overlap. 1. *Sets and Parameters:* * `container_width` and `container_height` define the dimensions of the container. * `rectangles` is a list of tuples, where each tuple represents the width and height of a rectangle. * `num_rectangles` is the number of rectangles. 1. *Variables:* * `model.place` is a a 3-dimensional binary variable to represent whether a rectangle r bottom left corner is placed at position (i, j). 1. *Constraints:* * `one_place_rule` ensues each rectangle is placed at most once. * `within_container_rule` ensure each rectangle fits within the container. * `non_overlap_rule` ensures that rectangles do not overlap. 1. *Objective Function:* * In our example the objective is to maximize the number of rectangles placed. ***This is a basic model and can be extended to include more sophisticated features like rotation of rectangles, different objective functions, or more complex constraints.*** # ### Parameters We will define a toy model of 3 rectangles packed into an 8 pixel grid container: ```python theme={null} import matplotlib.pyplot as plt import networkx as nx import pandas as pd import pyomo.environ as pyo from IPython.display import Markdown, display from pyomo.environ import value ``` ```python theme={null} # Dimensions of the container (width and height) CONTAINER_WIDTH = 4 CONTAINER_HEIGHT = 2 # List of rectangles with (width, height) tuples RECTANGLES = [(1, 1), (2, 2), (2, 1)] ``` # ### Optimization Model ```python theme={null} from pyomo.environ import ( Binary, ConcreteModel, Constraint, Objective, RangeSet, SolverFactory, Var, ) def define_rectangkes_packing_model(rectangles, container_width, container_height): # Number of rectangles num_rectangles = len(rectangles) # Create a model model = ConcreteModel() # Sets for rectangles and grid positions model.R = RangeSet(0, num_rectangles - 1) model.W = RangeSet(0, container_width - 1) model.H = RangeSet(0, container_height - 1) # Binary variable: 1 if rectangle r is placed at position (i, j), 0 otherwise model.place = Var(model.R, model.W, model.H, domain=Binary) # Constraints to ensure each rectangle is placed at most once def one_place_rule(model, r): return sum(model.place[r, i, j] for i in model.W for j in model.H) <= 1 model.one_place = Constraint(model.R, rule=one_place_rule) # Constraints to ensure rectangles do not overlap - we ensure for each coordinate that it is occupied by at most 1 rectangle def non_overlap_rule(model, i, j): return ( sum( model.place[r, i2, j2] for r in model.R for i2 in range(i - rectangles[r][0] + 1, i + 1) for j2 in range(j - rectangles[r][1] + 1, j + 1) if i2 >= 0 and j2 >= 0 ) <= 1 ) model.non_overlap = Constraint(model.W, model.H, rule=non_overlap_rule) # Constraints to ensure each rectangle is within the container def within_container_rule(model, r, i, j): if ( i + rectangles[r][0] > container_width or j + rectangles[r][1] > container_height ): return model.place[r, i, j] == 0 return Constraint. Skip model.within_container = Constraint( model.R, model.W, model.H, rule=within_container_rule ) # Objective function: maximize the number of placed rectangles model.obj = Objective( expr=-sum( model.place[r, i, j] for r in model.R for i in model.W for j in model.H ), sense=pyo.minimize, ) return model ``` ```python theme={null} model = define_rectangkes_packing_model(RECTANGLES, CONTAINER_WIDTH, CONTAINER_HEIGHT) ``` ```python theme={null} model.pprint() ``` **Output:** ``` 3 RangeSet Declarations H : Dimen=1, Size=2, Bounds=(0, 1) Key : Finite : Members None : True : [0:1] R : Dimen=1, Size=3, Bounds=(0, 2) Key : Finite : Members None : True : [0:2] W : Dimen=1, Size=4, Bounds=(0, 3) Key : Finite : Members None : True : [0:3] 1 Var Declarations place : Size=24, Index=R*W*H Key : Lower : Value : Upper : Fixed : Stale : Domain (0, 0, 0) : 0 : None : 1 : False : True : Binary (0, 0, 1) : 0 : None : 1 : False : True : Binary (0, 1, 0) : 0 : None : 1 : False : True : Binary (0, 1, 1) : 0 : None : 1 : False : True : Binary (0, 2, 0) : 0 : None : 1 : False : True : Binary (0, 2, 1) : 0 : None : 1 : False : True : Binary (0, 3, 0) : 0 : None : 1 : False : True : Binary (0, 3, 1) : 0 : None : 1 : False : True : Binary (1, 0, 0) : 0 : None : 1 : False : True : Binary (1, 0, 1) : 0 : None : 1 : False : True : Binary (1, 1, 0) : 0 : None : 1 : False : True : Binary (1, 1, 1) : 0 : None : 1 : False : True : Binary (1, 2, 0) : 0 : None : 1 : False : True : Binary (1, 2, 1) : 0 : None : 1 : False : True : Binary (1, 3, 0) : 0 : None : 1 : False : True : Binary (1, 3, 1) : 0 : None : 1 : False : True : Binary (2, 0, 0) : 0 : None : 1 : False : True : Binary (2, 0, 1) : 0 : None : 1 : False : True : Binary (2, 1, 0) : 0 : None : 1 : False : True : Binary (2, 1, 1) : 0 : None : 1 : False : True : Binary (2, 2, 0) : 0 : None : 1 : False : True : Binary (2, 2, 1) : 0 : None : 1 : False : True : Binary (2, 3, 0) : 0 : None : 1 : False : True : Binary (2, 3, 1) : 0 : None : 1 : False : True : Binary 1 Objective Declarations obj : Size=1, Index=None, Active=True Key : Active : Sense : Expression None : True : minimize : - (place[0,0,0] + place[0,0,1] + place[0,1,0] + place[0,1,1] + place[0,2,0] + place[0,2,1] + place[0,3,0] + place[0,3,1] + place[1,0,0] + place[1,0,1] + place[1,1,0] + place[1,1,1] + place[1,2,0] + place[1,2,1] + place[1,3,0] + place[1,3,1] + place[2,0,0] + place[2,0,1] + place[2,1,0] + place[2,1,1] + place[2,2,0] + place[2,2,1] + place[2,3,0] + place[2,3,1]) 3 Constraint Declarations non_overlap : Size=8, Index=W*H, Active=True Key : Lower : Body : Upper : Active (0, 0) : -Inf : place[0,0,0] + place[1,0,0] + place[2,0,0] : 1.0 : True (0, 1) : -Inf : place[0,0,1] + place[1,0,0] + place[1,0,1] + place[2,0,1] : 1.0 : True (1, 0) : -Inf : place[0,1,0] + place[1,0,0] + place[1,1,0] + place[2,0,0] + place[2,1,0] : 1.0 : True (1, 1) : -Inf : place[0,1,1] + place[1,0,0] + place[1,0,1] + place[1,1,0] + place[1,1,1] + place[2,0,1] + place[2,1,1] : 1.0 : True (2, 0) : -Inf : place[0,2,0] + place[1,1,0] + place[1,2,0] + place[2,1,0] + place[2,2,0] : 1.0 : True (2, 1) : -Inf : place[0,2,1] + place[1,1,0] + place[1,1,1] + place[1,2,0] + place[1,2,1] + place[2,1,1] + place[2,2,1] : 1.0 : True (3, 0) : -Inf : place[0,3,0] + place[1,2,0] + place[1,3,0] + place[2,2,0] + place[2,3,0] : 1.0 : True (3, 1) : -Inf : place[0,3,1] + place[1,2,0] + place[1,2,1] + place[1,3,0] + place[1,3,1] + place[2,2,1] + place[2,3,1] : 1.0 : True one_place : Size=3, Index=R, Active=True Key : Lower : Body : Upper : Active 0 : -Inf : place[0,0,0] + place[0,0,1] + place[0,1,0] + place[0,1,1] + place[0,2,0] + place[0,2,1] + place[0,3,0] + place[0,3,1] : 1.0 : True 1 : -Inf : place[1,0,0] + place[1,0,1] + place[1,1,0] + place[1,1,1] + place[1,2,0] + place[1,2,1] + place[1,3,0] + place[1,3,1] : 1.0 : True 2 : -Inf : place[2,0,0] + place[2,0,1] + place[2,1,0] + place[2,1,1] + place[2,2,0] + place[2,2,1] + place[2,3,0] + place[2,3,1] : 1.0 : True within_container : Size=7, Index=R*W*H, Active=True Key : Lower : Body : Upper : Active (1, 0, 1) : 0.0 : place[1,0,1] : 0.0 : True (1, 1, 1) : 0.0 : place[1,1,1] : 0.0 : True (1, 2, 1) : 0.0 : place[1,2,1] : 0.0 : True (1, 3, 0) : 0.0 : place[1,3,0] : 0.0 : True (1, 3, 1) : 0.0 : place[1,3,1] : 0.0 : True (2, 3, 0) : 0.0 : place[2,3,0] : 0.0 : True (2, 3, 1) : 0.0 : place[2,3,1] : 0.0 : True 8 Declarations: R W H place one_place non_overlap within_container obj ``` # ## 2. Plug into Classiq # ### Initialize Combinatorial Optimization Engine In order to solve the Pyomo model defined above, we use the Classiq combinatorial optimization engine. For the quantum part of the QAOA algorithm (`QAOAConfig`) - define the number of repetitions (`num_layers`): ```python theme={null} from classiq import * from classiq.applications.combinatorial_optimization import OptimizerConfig, QAOAConfig qaoa_config = QAOAConfig(num_layers=10, penalty_energy=100) ``` For the classical optimization part of the QAOA algorithm we define the maximum number of classical iterations (`max_iteration`) and the $\alpha$-parameter (`alpha_cvar`) for running CVaR-QAOA, an improved variation of the QAOA algorithm \[3]: ```python theme={null} optimizer_config = OptimizerConfig(max_iteration=60, alpha_cvar=1) ``` # ### Pluging-In the Pyomo Model into the Combinatorial Optimization Engine Constructs the Qmod: Lastly, we load the model, based on the problem and algorithm parameters, which we can use to solve the problem: ```python theme={null} qmod = construct_combinatorial_optimization_model( pyo_model=model, qaoa_config=qaoa_config, optimizer_config=optimizer_config, ) ``` We also set the quantum backend we want to execute on: ```python theme={null} from classiq.execution import ClassiqBackendPreferences qmod = set_execution_preferences( qmod, num_shots=10000, backend_preferences=ClassiqBackendPreferences(backend_name="simulator"), ) ``` # ## 3. Solve and Analyze We can now synthesize and view the QAOA circuit (ansatz) used to solve the optimization problem: Synthesize and view our QAOA circuit: ```python theme={null} qprog = synthesize(qmod) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/39FizAtlqQsR6aEGiu6c5G0R9vD ``` **Output:** ``` https://platform.classiq.io/circuit/39FizAtlqQsR6aEGiu6c5G0R9vD?login=True&version=17 ``` Execute QAOA: We now solve the problem by calling the `execute` function on the quantum program we have generated: ```python theme={null} result = execute(qprog).result_value() ``` We can check the convergence of the run: ```python theme={null} result.convergence_graph ``` output We can also examine the statistics of the algorithm: ```python theme={null} import pandas as pd from classiq.applications.combinatorial_optimization import ( get_optimization_solution_from_pyo, ) solution = get_optimization_solution_from_pyo( model, vqe_result=result, penalty_energy=qaoa_config.penalty_energy ) optimization_result = pd.DataFrame.from_records(solution) optimization_result.sort_values(by="cost", ascending=True).head(5) ``` | | probability | cost | solution | count | | --- | ----------- | ---- | -------------------------------------------------- | ----- | | 75 | 0.0010 | -3.0 | \[0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, ... | 10 | | 80 | 0.0009 | -3.0 | \[0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, ... | 9 | | 27 | 0.0014 | -3.0 | \[0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, ... | 14 | | 285 | 0.0005 | -3.0 | \[0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, ... | 5 | | 223 | 0.0006 | -3.0 | \[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, ... | 6 | And the histogram: ```python theme={null} optimization_result.hist("cost", weights=optimization_result["probability"]) ``` **Output:** ``` array([[]], dtype=object) ``` output ```python theme={null} best_solution = optimization_result.solution[optimization_result.cost.idxmin()] ``` # ### Visualize the Results In order to visualize the `best_solution` as a floor plan we construct few simple post processing utilities. To save qubits, the combinatorinal optimization engine creates a quantum program with qubits only correlating to binary variables that are not constraint to known fixed values. First, lets create a function that extracts the qubit indexes of variables that have no difference between the upper and lower bound of the constraints: ```python theme={null} def extract_no_boundries_qubit_indexes(model): no_qubits_indexes = [] for c in model.component_objects(Constraint, active=True): cdata = getattr(model, c.name) for index in cdata: lb = value(cdata[index].lower) ub = value(cdata[index].upper) if lb == ub: no_qubits_indexes.append(index) return no_qubits_indexes ``` Since the `best_solution` appends zeros into the the executed quantum program solution for each variable with no constraint boundary, we will use the `extract_no_boundries_qubit_indexes` above to "reorganize" the solution vector so we can properly visualize it: ```python theme={null} def prepare_solution_for_floorplan_visual(best_solution, no_qubits_indexes): best_solution_index_count = 0 solution = {} for r in model.R: for w in model.W: for h in model.H: if (r, w, h) not in no_qubits_indexes: solution[(r, w, h)] = best_solution[best_solution_index_count] best_solution_index_count += 1 else: solution[(r, w, h)] = 0 return solution def place_rectangles(solution): placement = {} for r in model.R: for i in model.W: for j in model.H: if solution[(r, i, j)] == 1: placement[r] = (i, j) return placement ``` We can now run the floorplan visualization function: ```python theme={null} import matplotlib.patches as patches import matplotlib.pyplot as plt # Function to visualize the rectangle packing solution def visualize_packing(container_width, container_height, rectangles, placement): fig, ax = plt.subplots(1) ax.set_xlim(0, container_width) ax.set_ylim(0, container_height) # Draw the container container = patches.Rectangle( (0, 0), container_width, container_height, linewidth=1, edgecolor="r", facecolor="none", ) ax.add_patch(container) # Draw each rectangle colors = [ "blue", "green", "orange", "purple", "yellow", ] # Add more colors if needed for r, (i, j) in placement.items(): width, height = rectangles[r] rect = patches.Rectangle( (i, j), width, height, linewidth=1, edgecolor="black", facecolor=colors[r % len(colors)], alpha=0.5, ) ax.add_patch(rect) plt.text( i + width / 2, j + height / 2, f"{r}", ha="center", va="center", color="white", ) plt.gca().set_aspect("equal", adjustable="box") plt.gca().invert_yaxis() # Invert y axis to match the typical matrix/grid representation # plt.grid(True) plt.show() # Visualize the solution visualize_packing( CONTAINER_WIDTH, CONTAINER_HEIGHT, RECTANGLES, place_rectangles( prepare_solution_for_floorplan_visual( best_solution, extract_no_boundries_qubit_indexes(model) ) ), ) ``` output # ### Solve Classically, Visualize Results and Compare: Running the following requires to install the classical solver with 'brew install glpk' ```python theme={null} """ # Create a solver solver = SolverFactory('glpk') # Solve the model solver.solve(model) # Display results for r in model.R: placed = [(i, j) for i in model.W for j in model.H if model.place[r, i, j].value == 1] if placed: print(f"Rectangle {RECTANGLES[r]} placed at position {placed[0]}") else: print(f"Rectangle {r} not placed") import matplotlib.pyplot as plt import matplotlib.patches as patches placement = {} for r in model.R: for i in model.W: for j in model.H: if model.place[r, i, j].value == 1: placement[r] = (i, j) # Visualize the solution visualize_packing(CONTAINER_WIDTH,CONTAINER_HEIGHT, RECTANGLES, placement) """ ``` **Output:** ``` '\n# Create a solver\nsolver = SolverFactory(\'glpk\')\n\n\n# Solve the model\nsolver.solve(model)\n\n# Display results\nfor r in model.R:\n placed = [(i, j) for i in model.W for j in model.H if model.place[r, i, j].value == 1]\n if placed:\n print(f"Rectangle {RECTANGLES[r]} placed at position {placed[0]}")\n else:\n print(f"Rectangle {r} not placed")\n\nimport matplotlib.pyplot as plt\nimport matplotlib.patches as patches\n\n\nplacement = {}\nfor r in model.R:\n for i in model.W:\n for j in model.H:\n if model.place[r, i, j].value == 1:\n placement[r] = (i, j)\n\n\n\n# Visualize the solution\nvisualize_packing(CONTAINER_WIDTH,CONTAINER_HEIGHT, RECTANGLES, placement)\n' ``` # Quantum Computation for Robot Posture Optimization Source: https://docs.classiq.io/explore/applications/optimization/robust_posture_optimization/robust_posture_optimization Open this notebook in GitHub to run it yourself * This tutorial proposes a novel quantum computing approach to solving the inverse kinematics problem of a robotic arm \[[1](#qoa)]. The method performs forward kinematics - computing the end-effector position from given joint angles - using a quantum circuit, and then feeds the result into a **classical optimization algorithm** to find the joint angles that minimize the error with respect to a target position. By adopting this **hybrid framework** that combines quantum and classical computation, the aim is to efficiently solve inverse kinematics. * The algorithm is based on variational optimization methods similar to the Variational Quantum Eigensolver (VQE) and the Quantum Approximate Optimization Algorithm (QAOA). The quantum circuit performs the forward kinematics, while the classical optimizer * COBYLA (Constrained Optimization BY Linear Approximation) - evaluates the error and updates the parameters iteratively. This classical optimization loop provides flexibility to incorporate additional objective terms such as energy minimization or obstacle avoidance. * The proposed quantum circuit ansatz represents each robotic link using one qubit. The orientation of each link is encoded on the Bloch sphere using single-qubit rotation gates (RX, RY, RZ). The expectation values $(\langle X\rangle, \langle Y\rangle, \langle Z\rangle)$ are multiplied by the corresponding link lengths to compute the end-effector position. Furthermore, an entangled circuit structure using RXX, RYY, and RZZ gates is introduced, capturing the parent-child link dependencies in orientation. This entanglement enhances both convergence speed and solution accuracy. This method is implemented on Qmod and validated through both simulation and real quantum hardware. The results demonstrate that introducing entanglement enables faster and more accurate inverse kinematics solutions compared to unentangled cases, thereby confirming the effectiveness of quantum computation in robotic applications. ```python theme={null} import math import matplotlib.pyplot as plt import numpy as np import scipy from tqdm import tqdm from classiq import * ``` ## Ansatz Circuit Without Entanglement ```python theme={null} @qfunc def q0_rotate(theta: CArray[CReal], q0: QBit): RZ(theta[2], q0) RY(theta[1], q0) RX(theta[0], q0) @qfunc def q1_rotate(theta: CArray[CReal], q1: QBit): RZ(theta[5], q1) RY(theta[4], q1) RX(theta[3], q1) @qfunc def pauli_Y_measure(q: QArray): S(q[0]) H(q[0]) S(q[1]) H(q[1]) def main_project_measure(pauli_term): @qfunc def main(theta: CArray[CReal, 6], q: Output[QArray[QBit, 2]]): allocate(q) q0_rotate(theta, q[0]) q1_rotate(theta, q[1]) q0_rotate(theta, q[1]) if pauli_term == "Z": pass elif pauli_term == "X": H(q[0]) H(q[1]) elif pauli_term == "Y": pauli_Y_measure(q) return main ``` ```python theme={null} qmod = create_model(main_project_measure("Y")) qprog = synthesize(qmod) ``` ```python theme={null} show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/36pWVSeOsQDhSnq57rtTm14J45R ``` ## Post Processing ```python theme={null} NUM_SHOTS = 1000 MAX_ITERATIONS = 30 DOF = 6 # robot link L1, L2 = 1.0, 1.0 target_pos = np.array([0.6, 1.0, 0.2]) ``` ```python theme={null} initial_params = (np.linspace(0, 1, DOF)) * math.pi print(initial_params) ``` **Output:** ``` [ 0. 0.62831853 1.25663706 1.88495559 2.51327412 3.14159265] ``` # ## Create Cost Function Target position $(X,Y,Z) = (0.6,1.0,0.2)$ # ### Pauli Based Measurment ```python theme={null} # X pauli measurment qmod_X = create_model(main_project_measure("X")) qprog_X = synthesize(qmod_X) # Y pauli measurment qmod_Y = create_model(main_project_measure("Y")) qprog_Y = synthesize(qmod_Y) # Z pauli measurment qmod_Z = create_model(main_project_measure("Z")) qprog_Z = synthesize(qmod_Z) ``` ```python theme={null} es_X = ExecutionSession( qprog_X, execution_preferences=ExecutionPreferences(num_shots=NUM_SHOTS) ) es_Y = ExecutionSession( qprog_Y, execution_preferences=ExecutionPreferences(num_shots=NUM_SHOTS) ) es_Z = ExecutionSession( qprog_Z, execution_preferences=ExecutionPreferences(num_shots=NUM_SHOTS) ) ``` ```python theme={null} def expected_valu(counts): expect_value_qubit0 = ( (counts.get("00", 0) + counts.get("01", 0)) - (counts.get("10", 0) + counts.get("11", 0)) ) / NUM_SHOTS expect_value_qubit1 = ( (counts.get("00", 0) + counts.get("10", 0)) - (counts.get("01", 0) + counts.get("11", 0)) ) / NUM_SHOTS return expect_value_qubit0, expect_value_qubit1 ``` ```python theme={null} cost_trace = [] cost_trace = [] def evaluate_params(es_X, es_Y, es_Z, params): X_sample = es_X.sample(parameters={"theta": params.tolist()}) Y_sample = es_Y.sample(parameters={"theta": params.tolist()}) Z_sample = es_Z.sample(parameters={"theta": params.tolist()}) X0_expect, X1_expect = expected_valu(X_sample.counts) Y0_expect, Y1_expect = expected_valu(Y_sample.counts) Z0_expect, Z1_expect = expected_valu(Z_sample.counts) # location of QOA v0 = np.array([X0_expect, Y0_expect, Z0_expect]) v1 = np.array([X1_expect, Y1_expect, Z1_expect]) # final output estimate_pos = L1 * v0 + L2 * v1 cost_estimation = np.sum((target_pos - estimate_pos) ** 2) cost_trace.append(float(cost_estimation)) return cost_estimation ``` ```python theme={null} with tqdm(total=MAX_ITERATIONS, desc="Optimization Progress", leave=True) as pbar: def progress_bar(xk: np.ndarray) -> None: pbar.update(1) # increment progress bar final_params = scipy.optimize.minimize( fun=lambda params: evaluate_params(es_X, es_Y, es_Z, params), x0=initial_params, method="COBYLA", options={"maxiter": MAX_ITERATIONS}, callback=progress_bar, ).x.tolist() print(f"Optimized parameters: {final_params}") plt.plot(cost_trace) plt.xlabel("Iterations") plt.ylabel("Cost") plt.title("Cost convergence") ``` **Output:** ``` Optimization Progress: 50%|████████████████████████████████████████████████████████████████████████████████████████████▌ | 15/30 [01:53<01:53, 7.54s/it] ``` **Output:** ``` Optimized parameters: [0.5538562527520599, 0.5180382038302478, 2.6520717446034245, 1.6766637933338149, 3.93410941527406, 4.178340080781159] ``` **Output:** ``` Text(0.5, 1.0, 'Cost convergence') ``` output ```python theme={null} es = ExecutionSession(qprog, execution_preferences=ExecutionPreferences(num_shots=1000)) res_qaoa = es.sample({"theta": final_params}) es.close() ``` # ## Define the Function for Extracting the Predicted Position from the Quantum Algorithm Results ```python theme={null} def estimate_arm_pos(params): X_sample = es_X.sample(parameters={"theta": params.tolist()}) Y_sample = es_Y.sample(parameters={"theta": params.tolist()}) Z_sample = es_Z.sample(parameters={"theta": params.tolist()}) X0_expect, X1_expect = expected_valu(X_sample.counts) Y0_expect, Y1_expect = expected_valu(Y_sample.counts) Z0_expect, Z1_expect = expected_valu(Z_sample.counts) v0 = np.array([X0_expect, Y0_expect, Z0_expect]) v1 = np.array([X1_expect, Y1_expect, Z1_expect]) estimate_pos = L1 * v0 + L2 * v1 return estimate_pos estimate_arm_pos(np.array(final_params)) ``` **Output:** ``` array([0.806, 1.216, 0.204]) ``` ```python theme={null} print("predict_arm_position :", estimate_arm_pos(np.array(final_params))) print("target_arm_position :", target_pos) ``` **Output:** ``` predict_arm_position : [0.79 1.202 0.142] target_arm_position : [0.6 1. 0.2] ``` ## References \[1]: [T. Otani, A. Takanishi, N. Hara, Y.Takita and K.Kimura. "Quantum computation for robot posture optimization." Scientific Reports volume 15, 28508 (2025).](https://www.nature.com/articles/s41598-025-12109-0) # Set Cover Problem Source: https://docs.classiq.io/explore/applications/optimization/set_cover/set_cover Open this notebook in GitHub to run it yourself ## Introduction The set cover problem [\[1\]](#setcoverwiki) represents a well-known problem in the fields of combinatorics, computer science, and complexity theory. It is an NP-complete problems. The problem presents us with a universal set, $\displaystyle U$, and a collection $\displaystyle S$ of subsets of $\displaystyle U$. The goal is to find the smallest possible subfamily, $\displaystyle C \subseteq S$, whose union equals the universal set. Formally, let's consider a universal set $\displaystyle U = {1, 2, ..., n}$ and a collection $\displaystyle S$ containing $m$ subsets of $\displaystyle U$, $\displaystyle S = {S_1, ..., S_m}$ with $\displaystyle S_i \subseteq U$. The challenge of the set cover problem is to find a subset $\displaystyle C$ of $\displaystyle S$ of minimal size such that $\displaystyle \bigcup_{S_i \in C} S_i = U$. ## Solving with the Classiq Platform We go through the steps of solving the problem with the Classiq platform, using QAOA algorithm \[[2](#qaoa)]. The solution is based on defining a pyomo model for the optimization problem we would like to solve. # ## Building the Pyomo Model from an Input We proceed by defining the pyomo model that will be used on the Classiq platform, using the mathematical formulation defined above: ```python theme={null} import itertools from typing import List import pyomo.core as pyo def set_cover(sub_sets: List[List[int]]) -> pyo.ConcreteModel: entire_set = set(itertools.chain(*sub_sets)) n = max(entire_set) num_sets = len(sub_sets) assert entire_set == set( range(1, n + 1) ), f"the union of the subsets is {entire_set} not equal to range(1, {n + 1})" model = pyo.ConcreteModel() model.x = pyo.Var(range(num_sets), domain=pyo.Binary) @model.Constraint(entire_set) def independent_rule(model, num): return sum(model.x[idx] for idx in range(num_sets) if num in sub_sets[idx]) >= 1 model.cost = pyo.Objective(expr=sum(model.x.values()), sense=pyo.minimize) return model ``` The model contains: * Binary variable for each subset (model.x) indicating if it is included in the sub-collection. * Objective rule - the size of the sub-collection. * Constraint - the sub-collection covers the original set. ```python theme={null} sub_sets = sub_sets = [ [1, 2, 3, 4], [2, 3, 4, 5], [6, 7], [8, 9, 10], [1, 6, 8], [3, 7, 9], [4, 7, 10], [2, 5, 8], ] set_cover_model = set_cover(sub_sets) ``` ```python theme={null} set_cover_model.pprint() ``` **Output:** ``` 1 Var Declarations x : Size=8, Index={0, 1, 2, 3, 4, 5, 6, 7} Key : Lower : Value : Upper : Fixed : Stale : Domain 0 : 0 : None : 1 : False : True : Binary 1 : 0 : None : 1 : False : True : Binary 2 : 0 : None : 1 : False : True : Binary 3 : 0 : None : 1 : False : True : Binary 4 : 0 : None : 1 : False : True : Binary 5 : 0 : None : 1 : False : True : Binary 6 : 0 : None : 1 : False : True : Binary 7 : 0 : None : 1 : False : True : Binary 1 Objective Declarations cost : Size=1, Index=None, Active=True Key : Active : Sense : Expression None : True : minimize : x[0] + x[1] + x[2] + x[3] + x[4] + x[5] + x[6] + x[7] 1 Constraint Declarations independent_rule : Size=10, Index={1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, Active=True Key : Lower : Body : Upper : Active 1 : 1.0 : x[0] + x[4] : +Inf : True 2 : 1.0 : x[0] + x[1] + x[7] : +Inf : True 3 : 1.0 : x[0] + x[1] + x[5] : +Inf : True 4 : 1.0 : x[0] + x[1] + x[6] : +Inf : True 5 : 1.0 : x[1] + x[7] : +Inf : True 6 : 1.0 : x[2] + x[4] : +Inf : True 7 : 1.0 : x[2] + x[5] + x[6] : +Inf : True 8 : 1.0 : x[3] + x[4] + x[7] : +Inf : True 9 : 1.0 : x[3] + x[5] : +Inf : True 10 : 1.0 : x[3] + x[6] : +Inf : True 3 Declarations: x independent_rule cost ``` ## Setting Up the Classiq Problem Instance In order to solve the Pyomo model defined above, we use the `CombinatorialProblem` quantum object. Under the hood it tranlastes the Pyomo model to a quantum model of the QAOA algorithm, with a cost function translated from the Pyomo model. We can choose the number of layers for the QAOA ansatz using the argument `num_layers`, and the `penalty_factor`, which will be the coefficient of the constraints term in the cost hamiltonian. ```python theme={null} from classiq import * from classiq.applications.combinatorial_optimization import CombinatorialProblem combi = CombinatorialProblem(pyo_model=set_cover_model, num_layers=3, penalty_factor=10) qmod = combi.get_model() ``` ## Synthesizing the QAOA Circuit and Solving the Problem We can now synthesize and view the QAOA circuit (ansatz) used to solve the optimization problem: ```python theme={null} qprog = combi.get_qprog() show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/39FjWgm2BVRqBIpje7e1f6HNrAf ``` **Output:** ``` https://platform.classiq.io/circuit/39FjWgm2BVRqBIpje7e1f6HNrAf?login=True&version=17 ``` We now solve the problem by calling the `optimize` method of the `CombinatorialProblem` object. For the classical optimization part of the QAOA algorithm we define the maximum number of classical iterations (`maxiter`) and the $\alpha$-parameter (`quantile`) for running CVaR-QAOA, an improved variation of the QAOA algorithm \[[3](#cvar)]: ```python theme={null} optimized_params = combi.optimize(maxiter=60, quantile=0.7) ``` We can check the convergence of the run: ```python theme={null} import matplotlib.pyplot as plt fig, axes = plt.subplots(nrows=1, ncols=1) axes.plot(combi.cost_trace) axes.set_xlabel("Iterations") axes.set_ylabel("Cost") axes.set_title("Cost convergence") ``` **Output:** ``` Text(0.5, 1.0, 'Cost convergence') ``` output ## Optimization Results We can also examine the statistics of the algorithm. In order to get samples with the optimized parameters, we call the `sample` method: ```python theme={null} optimization_result = combi.sample(optimized_params) optimization_result.sort_values(by="cost").head(5) ``` | | solution | probability | cost | | ---- | ---------------------------------------------------- | ----------- | ---- | | 52 | \{'x': \[0, 1, 0, 1, 1, 0, 0, 0], 'independent\_r... | 0.001465 | 23.0 | | 66 | \{'x': \[0, 1, 0, 1, 1, 0, 0, 0], 'independent\_r... | 0.001465 | 23.0 | | 1107 | \{'x': \[0, 1, 0, 0, 1, 0, 1, 0], 'independent\_r... | 0.000488 | 23.0 | | 636 | \{'x': \[1, 0, 1, 1, 0, 0, 0, 0], 'independent\_r... | 0.000488 | 23.0 | | 1531 | \{'x': \[0, 1, 0, 0, 1, 0, 1, 0], 'independent\_r... | 0.000488 | 23.0 | We also want to compare the optimized results to uniformly sampled results: ```python theme={null} uniform_result = combi.sample_uniform() ``` And compare the histograms: ```python theme={null} optimization_result["cost"].plot( kind="hist", bins=50, edgecolor="black", weights=optimization_result["probability"], alpha=0.6, label="optimized", ) uniform_result["cost"].plot( kind="hist", bins=50, edgecolor="black", weights=uniform_result["probability"], alpha=0.6, label="uniform", ) plt.legend() plt.ylabel("Probability", fontsize=16) plt.xlabel("cost", fontsize=16) plt.tick_params(axis="both", labelsize=14) ``` output Let us plot the solution: ```python theme={null} best_solution = optimization_result.solution[optimization_result.cost.idxmin()] best_solution = [best_solution["x"][i] for i in range(len(best_solution["x"]))] best_solution ``` **Output:** ``` [0, 1, 0, 1, 1, 0, 0, 0] ``` ```python theme={null} print( f"Quantum Solution: num_sets={int(sum(best_solution))}, sets={[sub_sets[i] for i in range(len(best_solution)) if best_solution[i]]}" ) ``` **Output:** ``` Quantum Solution: num_sets=3, sets=[[2, 3, 4, 5], [8, 9, 10], [1, 6, 8]] ``` ## Comparison to a Classical Solver Lastly, we can compare to the classical solution of the problem: ```python theme={null} from pyomo.opt import SolverFactory solver = SolverFactory("couenne") solver.solve(set_cover_model) classical_solution = [ int(pyo.value(set_cover_model.x[i])) for i in range(len(set_cover_model.x)) ] print("Classical solution:", classical_solution) ``` **Output:** ``` Classical solution: [1, 1, 1, 1, 0, 0, 0, 0] ``` ```python theme={null} print( f"Classical Solution: num_sets={int(sum(classical_solution))}, sets={[sub_sets[i] for i in range(len(classical_solution)) if classical_solution[i]]}" ) ``` **Output:** ``` Classical Solution: num_sets=4, sets=[[1, 2, 3, 4], [2, 3, 4, 5], [6, 7], [8, 9, 10]] ``` ## References \[1]: [Integer Programming (Wikipedia).](https://en.wikipedia.org/wiki/Integer_programming) \[2]: [Farhi, Edward, Jeffrey Goldstone, and Sam Gutmann. "A quantum approximate optimization algorithm." arXiv preprint arXiv:1411.4028 (2014).](https://arxiv.org/abs/1411.4028) \[3]: [Barkoutsos, Panagiotis Kl, et al. "Improving variational quantum optimization using CVaR." Quantum 4 (2020): 256.](https://arxiv.org/abs/1907.04769) # Number Partition Problem Source: https://docs.classiq.io/explore/applications/optimization/set_partition/set_partition Open this notebook in GitHub to run it yourself ## Introduction In the Number Partitioning Problem \[[1](#partitionwiki)] we need to find how to partition a set of integers into two subsets of equal sums. In case such a partition does not exist, we can ask for a partition where the difference between the sums is minimal. ## Mathematical Formulation Given a set of numbers $S=\{s_1,s_2,...,s_n\}$, a partition is defined as $P_1,P_2 \subset \{1,...,n\}$, with $P_1\cup P_2=\{1,...,n\}$ and $P_1\cap P_2=\emptyset$. In the Number Partitioning Problem we need to determine a partition such that $|\sum_{j\in P_1}s_j-\sum_{j\in P_2}s_j|$ is minimal. A partition can be represented by a binary vector $x$ of size $n$, where we assign 0 or 1 for being in $P_1$ or $P_2$, respectively. The quantity we ask to minimize is $|\vec{x}\cdot \vec{s}-(1-\vec{x})\cdot\vec{s}|=|(2\vec{x}-1)\cdot\vec{s}|$. In practice we will minimize the square of this expression. ## Solving with the Classiq Platform We go through the steps of solving the problem with the Classiq platform, using QAOA algorithm \[[2](#qaoa)]. The solution is based on defining a pyomo model for the optimization problem we would like to solve. ```python theme={null} import random import networkx as nx import numpy as np import pyomo.core as pyo from matplotlib import pyplot as plt random.seed(0) np.random.seed(0) ``` ## Building the Pyomo Model from a Graph Input We proceed by defining the Pyomo model that will be used on the Classiq platform, using the mathematical formulation defined above: ```python theme={null} # we define a matrix which gets a set of integers s and returns a pyomo model for the partitioning problem def partite(s) -> pyo.ConcreteModel: model = pyo.ConcreteModel() SetSize = len(s) # the set size model.x = pyo.Var( range(SetSize), domain=pyo.Binary ) # our variable is a binary vector # we define a cost function model.cost = pyo.Objective( expr=sum(((2 * model.x[i] - 1) * s[i]) for i in range(SetSize)) ** 2, sense=pyo.minimize, ) return model ``` ```python theme={null} Myset = np.random.randint(1, 12, 10) mylist = [int(x) for x in Myset] print("This is my list: ", mylist) set_partition_model = partite(mylist) ``` **Output:** ``` This is my list: [4, 5, 2, 10, 6, 11, 9, 4, 9, 1] ``` ```python theme={null} set_partition_model.pprint() ``` **Output:** ``` 1 Var Declarations x : Size=10, Index={0, 1, 2, 3, 4, 5, 6, 7, 8, 9} Key : Lower : Value : Upper : Fixed : Stale : Domain 0 : 0 : None : 1 : False : True : Binary 1 : 0 : None : 1 : False : True : Binary 2 : 0 : None : 1 : False : True : Binary 3 : 0 : None : 1 : False : True : Binary 4 : 0 : None : 1 : False : True : Binary 5 : 0 : None : 1 : False : True : Binary 6 : 0 : None : 1 : False : True : Binary 7 : 0 : None : 1 : False : True : Binary 8 : 0 : None : 1 : False : True : Binary 9 : 0 : None : 1 : False : True : Binary 1 Objective Declarations cost : Size=1, Index=None, Active=True Key : Active : Sense : Expression None : True : minimize : ((2*x[0] - 1)*4 + (2*x[1] - 1)*5 + (2*x[2] - 1)*2 + (2*x[3] - 1)*10 + (2*x[4] - 1)*6 + (2*x[5] - 1)*11 + (2*x[6] - 1)*9 + (2*x[7] - 1)*4 + (2*x[8] - 1)*9 + (2*x[9] - 1))**2 2 Declarations: x cost ``` ## Setting Up the Classiq Problem Instance In order to solve the Pyomo model defined above, we use the `CombinatorialProblem` python class. Under the hood it tranlates the Pyomo model to a quantum model of the QAOA algorithm, with cost hamiltonian translated from the Pyomo model. We can choose the number of layers for the QAOA ansatz using the argument `num_layers`, and the `penalty_factor`, which will be the coefficient of the constraints term in the cost hamiltonian. ```python theme={null} from classiq import * from classiq.applications.combinatorial_optimization import CombinatorialProblem combi = CombinatorialProblem( pyo_model=set_partition_model, num_layers=3, penalty_factor=10 ) qmod = combi.get_model() ``` ## Synthesizing the QAOA Circuit and Solving the Problem We can now synthesize and view the QAOA circuit (ansatz) used to solve the optimization problem: ```python theme={null} qprog = combi.get_qprog() show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/38wAskQKdsHfkzDGJFSuT3lBDHt ``` **Output:** ``` https://platform.classiq.io/circuit/38wAskQKdsHfkzDGJFSuT3lBDHt?login=True&version=15 ``` We also set the quantum backend we want to execute on: ```python theme={null} from classiq.execution import * execution_preferences = ExecutionPreferences( backend_preferences=ClassiqBackendPreferences(backend_name="simulator"), ) ``` We now solve the problem by calling the `optimize` method of the `CombinatorialProblem` object. For the classical optimization part of the QAOA algorithm we define the maximum number of classical iterations (`maxiter`) and the $\alpha$-parameter (`quantile`) for running CVaR-QAOA, an improved variation of the QAOA algorithm \[[3](#cvar)]: ```python theme={null} optimized_params = combi.optimize(execution_preferences, maxiter=80, quantile=0.7) ``` We can check the convergence of the run: ```python theme={null} plt.plot(combi.cost_trace) plt.xlabel("Iterations") plt.ylabel("Cost") plt.title("Cost convergence") ``` **Output:** ``` Text(0.5, 1.0, 'Cost convergence') ``` output ## Optimization Results We can also examine the statistics of the algorithm. In order to get samples with the optimized parameters, we call the `sample` method: ```python theme={null} optimization_result = combi.sample(optimized_params) optimization_result.sort_values(by="cost").head(5) ``` | | solution | probability | cost | | --- | --------------------------------------- | ----------- | ---- | | 0 | \{'x': \[1, 0, 0, 1, 0, 1, 0, 1, 0, 1]} | 0.018066 | 1 | | 88 | \{'x': \[1, 1, 0, 0, 0, 1, 1, 0, 0, 1]} | 0.002441 | 1 | | 263 | \{'x': \[0, 0, 1, 1, 1, 1, 0, 0, 0, 1]} | 0.000977 | 1 | | 285 | \{'x': \[1, 1, 1, 1, 1, 0, 0, 1, 0, 0]} | 0.000977 | 1 | | 287 | \{'x': \[0, 1, 0, 1, 1, 0, 1, 0, 0, 1]} | 0.000977 | 1 | We will also want to compare the optimized results to uniformly sampled results: ```python theme={null} uniform_result = combi.sample_uniform() ``` And compare the histograms: ```python theme={null} optimization_result["cost"].plot( kind="hist", bins=50, edgecolor="black", weights=optimization_result["probability"], alpha=0.6, label="optimized", ) uniform_result["cost"].plot( kind="hist", bins=50, edgecolor="black", weights=uniform_result["probability"], alpha=0.6, label="uniform", ) plt.legend() plt.ylabel("Probability", fontsize=16) plt.xlabel("cost", fontsize=16) plt.tick_params(axis="both", labelsize=14) ``` output Let us plot the best solution: ```python theme={null} best_solution = optimization_result.solution[optimization_result.cost.idxmin()] ``` ```python theme={null} p1 = [mylist[i] for i in range(len(mylist)) if best_solution["x"][i] == 0] p2 = [mylist[i] for i in range(len(mylist)) if best_solution["x"][i] == 1] print("P1=", p1, ", total sum: ", sum(p1)) print("P2=", p2, ", total sum: ", sum(p2)) print("difference= ", abs(sum(p1) - sum(p2))) ``` **Output:** ``` P1= [5, 2, 6, 9, 9] , total sum: 31 P2= [4, 10, 11, 4, 1] , total sum: 30 difference= 1 ``` Lastly, we can compare to the classical solution of the problem: ```python theme={null} from pyomo.opt import SolverFactory solver = SolverFactory("couenne") solver.solve(set_partition_model) set_partition_model.display() ``` **Output:** ``` Model unknown Variables: x : Size=10, Index={0, 1, 2, 3, 4, 5, 6, 7, 8, 9} Key : Lower : Value : Upper : Fixed : Stale : Domain 0 : 0 : 1.0 : 1 : False : False : Binary 1 : 0 : 1.0 : 1 : False : False : Binary 2 : 0 : 1.0 : 1 : False : False : Binary 3 : 0 : 1.0 : 1 : False : False : Binary 4 : 0 : 1.0 : 1 : False : False : Binary 5 : 0 : 0.0 : 1 : False : False : Binary 6 : 0 : 0.0 : 1 : False : False : Binary 7 : 0 : 1.0 : 1 : False : False : Binary 8 : 0 : 0.0 : 1 : False : False : Binary 9 : 0 : 0.0 : 1 : False : False : Binary Objectives: cost : Size=1, Index=None, Active=True Key : Active : Value None : True : 1.0 Constraints: None ``` ```python theme={null} classical_solution = [pyo.value(set_partition_model.x[i]) for i in range(len(mylist))] ``` ```python theme={null} p1 = [mylist[i] for i in range(len(mylist)) if round(classical_solution[i]) == 0] p2 = [mylist[i] for i in range(len(mylist)) if round(classical_solution[i]) == 1] print("P1=", p1, ", total sum: ", sum(p1)) print("P2=", p2, ", total sum: ", sum(p2)) print("difference= ", abs(sum(p1) - sum(p2))) ``` **Output:** ``` P1= [11, 9, 9, 1] , total sum: 30 P2= [4, 5, 2, 10, 6, 4] , total sum: 31 difference= 1 ``` ## References \[1]: [Number Partitioning Problem (Wikipedia)](https://en.wikipedia.org/wiki/Partition_problem) \[2]: [Farhi, Edward, Jeffrey Goldstone, and Sam Gutmann. "A quantum approximate optimization algorithm." arXiv preprint arXiv:1411.4028 (2014).](https://arxiv.org/abs/1411.4028) \[3]: [Barkoutsos, Panagiotis Kl, et al. "Improving variational quantum optimization using CVaR." Quantum 4 (2020): 256.](https://arxiv.org/abs/1907.04769) # Variational Quantum Imaginary Time Evolution (VarQITE) for Combinatorial Problems Source: https://docs.classiq.io/explore/applications/optimization/variational_quantum_imaginary_time_evolution/variational_quantum_imaginary_time_evolution Open this notebook in GitHub to run it yourself This notebook demonstrates the implementation of the Variational Quantum Imaginary Time Evolution (VarQITE) algorithm for solving combinatorial optimization problems, specifically the Max-Cut problem following the paper [Performant near-term quantum combinatorial optimization](https://arxiv.org/abs/2404.16135). ```python theme={null} import random from enum import Enum from itertools import combinations from typing import Callable, Dict, Tuple import networkx as nx import numpy as np from matplotlib import pyplot as plt from classiq import * random.seed(0) np.random.seed(0) ``` ## Generate Graph ```python theme={null} def display_graph(G, weight_digits=2): pos = nx.spring_layout(G) nx.draw(G, pos, with_labels=True) labels = nx.get_edge_attributes(G, "weight") digits = weight_digits formatted_labels = {edge: f"{weight:.{digits}f}" for edge, weight in labels.items()} nx.draw_networkx_edge_labels(G, pos, edge_labels=formatted_labels) plt.show() ``` ```python theme={null} class ProblemType(Enum): SK = "sk" NWS = "nws" REG3 = "reg3" ``` ```python theme={null} def nws(n: int) -> nx.Graph: """ create a Newman-Watts-Strogatz graph with n nodes with k=4 and p=0.5, and weights chosen from (0, 1] """ G = nx.newman_watts_strogatz_graph(n, k=4, p=0.5) for u, v in G.edges(): # assign random weights to edges in the range (0, 1] weight = 1.0 - random.random() G.edges[u, v]["weight"] = weight G.problem_type = ProblemType.NWS return G ``` ```python theme={null} G = nws(4) ``` ```python theme={null} print(G) ``` **Output:** ``` Graph with 4 nodes and 6 edges ``` ```python theme={null} display_graph(G) ``` output ## Generate Ansatz ```python theme={null} def vertex_weight(G: nx.Graph) -> Dict[int, int]: """ calculate the vertex weight of a graph Args: G (nx.Graph): The input graph. Returns: dict: A dictionary containing the vertex weights. """ rho = {} for u, v in G.edges(): weight = abs(G.edges[u, v]["weight"]) for node in (u, v): if node not in rho: rho[node] = 0 rho[node] += weight return rho ``` ```python theme={null} def graph_to_ansatz_rotations(G: nx.Graph, all_nodes=True) -> list[list[Pauli]]: """ Given a graph G, return a list of Pauli strings corresponding to the edges of the graph, or all nodes if all_nodes is True. Each Pauli string is represented as a Y-Z string on each pair of nodes. The list of Pauli strings is sorted by the vertex weight of the nodes in the graph. """ minmax = lambda x: (min(x), max(x)) pauli_strings = [] if all_nodes: node_pairs = combinations(G.nodes(), 2) else: node_pairs = G.edges() # sort the node pairs by their vertex weight vw = vertex_weight(G) node_pairs = sorted(node_pairs, key=lambda pair: minmax((vw[pair[0]], vw[pair[1]]))) # build the Pauli strings for i, j in node_pairs: if vw[i] < vw[j]: n1, n2 = i, j else: n1, n2 = j, i pauli_string = [] for n in G.nodes(): if n == n1: pauli_string.append(Pauli.Z) elif n == n2: pauli_string.append(Pauli.Y) else: pauli_string.append(Pauli.I) pauli_strings.append(pauli_string) return pauli_strings ``` Given a graph G, return a quantum circuit for the maxcut problem. The circuit consists of single-qubit Hadamard gates, followed by a series of two-qubit `Pauli` `(Y-Z)` rotations. The two-qubit rotations are parameterized by angles, which are the variational parameters of the ansatz. ```python theme={null} @qfunc def maxcut_ansatz( two_qubit_pauli_rotations: CArray[CArray[Pauli]], angles: CArray[CReal], nodes: Output[QArray[QBit]], ) -> None: """ Generate a counteradiabatic-inspired variational quantum circuit for combinatorial optimization problems. The circuit consists of single-qubit Hadamard gates, followed by a series of two-qubit Pauli (Y-Z) rotations. The two-qubit rotations are parameterized by angles, which are the variational parameters of the ansatz. Arguments: two_qubit_pauli_rotations (CArray[CArray[Pauli]]): The two-qubit Pauli rotations. angles (CArray[CReal]): The angles for the two-qubit rotations. nodes (Output[QArray[QBit]]): The output quantum register. """ num_qubits = len(two_qubit_pauli_rotations[0]) allocate(num_qubits, nodes) hadamard_transform(nodes) for ps, angle in zip(two_qubit_pauli_rotations, angles): single_pauli_exponent(pauli_string=ps, coefficient=angle, qbv=nodes) ``` ```python theme={null} two_qubit_pauli_rotations = graph_to_ansatz_rotations(G) V = len(G.nodes) NUM_ANGLES = (V * (V - 1)) // 2 ``` ```python theme={null} len(two_qubit_pauli_rotations) ``` **Output:** ``` 6 ``` ```python theme={null} @qfunc def main(nodes: Output[QArray[QBit]], angles: CArray[CReal, NUM_ANGLES]) -> None: """ Main function to generate the quantum circuit for the maxcut problem. captures the ansatz rotations, using the `maxcut_ansatz` quantum function. Arguments: nodes (Output[QArray[QBit]]): The output quantum register. angles (CArray[CReal, NUM_ANGLES]): The angles for the two-qubit rotations. """ num_qubits = len(two_qubit_pauli_rotations[0]) allocate(num_qubits, nodes) hadamard_transform(nodes) # for ps, angle in zip(two_qubit_pauli_rotations, angles): # single_pauli_exponent(pauli_string=ps, coefficient=angle, qbv=nodes) for ind in range(len(two_qubit_pauli_rotations)): single_pauli_exponent( pauli_string=two_qubit_pauli_rotations[ind], coefficient=angles[ind], qbv=nodes, ) # maxcut_ansatz(two_qubit_pauli_rotations, angles, nodes) ``` ```python theme={null} qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/36qGJrCbEFP0cr6wcXz1NY3kP20 ``` ## Var-IT Optimization # ## Ansatz The quantum circuit has one qubit for each node in the graph. All qubits are initialized to the $|+\rangle$ state. Subsequently, each pair of nodes in the graph is rotated by a $Y\text{--}Z$ rotation, $R_{YZ}\left(\theta_j\right)$. For a graph with $V$ nodes, there are $N_p = V(V-1)/2$ pairs, giving $N_p$ parametric angles. # ## Cost Function Representing the Max-Cut cost hamiltonian as a sum of Paulis: $$ H_c=\sum_{\alpha} P_\alpha, \quad H_c|z\rangle=C(z)|z\rangle $$ Where the cost is defined either by the physics convention: $$ C(z)=\sum_{(i,j)\in E} w_{ij} z_i z_j \quad z_i\in\{1,-1\} $$ or the computer science convention: $$ C(z)=-\sum_{(i,j)\in E} w_{ij}\left(z_i + z_j - 2 z_i z_j\right)/2 \quad z_i\in\{0,1\} $$ # ## Parameter Optimization The parametric angles are updated by solving the linear equation: $$ G\cdot\dot{\vec{\theta}}=D $$ and updating the angles: $\vec{\theta}\rightarrow\vec{\theta}+\Delta\tau\dot{\vec{\theta}}$ The matrix $G$, and vector $D$, are defined as: $$ G_{\alpha,j}= \Re \left(\langle\Psi\left(\vec{\theta}\right)|P_\alpha\frac{\partial |\Psi\left(\vec{\theta}\right)\rangle}{\partial \theta_j} \right) $$ $$ D_\alpha=-\frac{1}{2}\langle\Psi\left(\vec{\theta}\right)| \{P_\alpha, H_c-E_\tau\} |\Psi\left(\vec{\theta}\right)\rangle $$ where $P_\alpha$ is one of the Pauli strings that make up the cost Hamiltonian, $H_c$ is the entire cost Hamiltonian, and $E_\tau$ is the previous value of the cost Hamiltonian ```python theme={null} class CostConvention(Enum): PHYSICS = "physics" CS = "computer science" Graph_Cost_Convention = { ProblemType.SK: CostConvention.PHYSICS, ProblemType.NWS: CostConvention.CS, ProblemType.REG3: CostConvention.CS, } ``` ```python theme={null} def graph_to_hamiltonian(graph: nx.Graph) -> list[PauliTerm]: """ Calculate the Hamiltonian of a graph, using the Physics or CS convention. The Hamiltonian is a weighted sum of Pauli strings. Args: graph (nx.Graph): The input graph. Returns: list[PauliTerm]: The Hamiltonian as a list of Pauli strings and weights. """ # The Hamiltonian for the Physics convention is: # H = sum_{(i,j) \in E} (w_{ij} Z_i Z_j) # and for the CS convention is: # H = -sum_{(i,j) \in E} (w_{ij}(1 - Z_i Z_j))/2 assert hasattr(graph, "problem_type"), "Graph has no problem type" assert ( graph.problem_type in Graph_Cost_Convention ), "The problem type is not supported" convention = Graph_Cost_Convention[graph.problem_type] H = [] sum_of_weights = 0 num_nodes = len(graph.nodes) for u, v in graph.edges: weight = graph.edges[u, v]["weight"] sum_of_weights += weight pauli = [] for n in graph.nodes: if n == u or n == v: pauli.append(Pauli.Z) else: pauli.append(Pauli.I) H.append(PauliTerm(pauli=pauli, coefficient=weight)) if convention == CostConvention.CS: # Convert the Hamiltonian from the Physics to the CS convention H = [PauliTerm(pt.pauli, pt.coefficient / 2) for pt in H] H.append( PauliTerm(pauli=[Pauli.I] * num_nodes, coefficient=-sum_of_weights / 2) ) return H ``` ```python theme={null} # type alias for the measure statistics # the statistics are a list of tuples, where each tuple contains the state of the output quantum register # and the number of shots for that state measure_stats_t = list[Tuple[list[int], int]] def z_output_measurements( qprog: QuantumProgram, output_qreg: str, num_shots: int = 128, param_vals: Dict[str, list[float]] = None, ) -> Tuple[list[float], measure_stats_t]: """ Calculate the Z output measurements of a quantum circuit given a `quantum program`, and a set of parameters. The Z output measurements are the expectation values of the Z operator for each qubit in the output quantum register. For each zero measurement, the output is incremented by the number of shots, and for each one measurement, the output is decremented by the number of shots. The final output is normalized by the number of shots. Args: qprog (SerializedQuantumProgram): The quantum program to be executed. output_qreg (str): The quantum register to be measured. num_shots (int, optional): The number of shots for the execution. Defaults to 128. backend (BackendPreferencesTypes, optional): The backend preferences for the execution. Defaults to None, executed on the local simulator. param_vals (Dict[str, list[float]], optional): The parameter values for the quantum program. Defaults to None. Returns: list[float]: The expectation of the Z output measurements of the output qubits. list[Tuple[list[int], int]]: The statistics of the Z output measurements. The statistics are a list of tuples, where each tuple contains the state of the output quantum register and the number of shots for that state. """ with ExecutionSession( quantum_program=qprog, execution_preferences=ExecutionPreferences(num_shots=num_shots), ) as es: if param_vals: sample_result = es.sample(param_vals) else: sample_result = es.sample() parsed_counts = sample_result.parsed_counts_of_outputs(output_qreg) num_shots = sample_result.num_shots num_qubits = len(parsed_counts[0].state[output_qreg]) expectation = [0] * num_qubits statistics = [] for parsed_count in parsed_counts: # each `parsed_count` contains the state of the output quantum register # and the number of shots for that state # the state is a list of 0s and 1s, where 0 means the qubit is in the |0> state # and 1 means the qubit is in the |1> state state = parsed_count.state[output_qreg] shots = parsed_count.shots statistics.append((state, shots)) for idx, s in enumerate(state): if s == 0: expectation[idx] += shots else: expectation[idx] -= shots expectation = [x / num_shots for x in expectation] return expectation, statistics def pauli_term_expectation(statistics: measure_stats_t, pauli_term: PauliTerm): """ Calculate the expectation value of a Pauli term given a set of statistics. The statistics are a list of tuples, where each tuple contains the state of the output quantum register and the number of shots for that state. The Pauli term is a weighted sum of Pauli strings. The expectation value is calculated by summing the parity of the state and the probability of the state, and then multiplying by the parity sign. NOTE: currently, only Z (and I) terms are supported. Args: statistics (list[Tuple[list[int], int]]): The statistics of the Z output measurements. pauli_term (PauliTerm): The Pauli term to be estimated. Returns: float: The expectation value of the Pauli term. """ assert statistics, "Statistics list is empty." assert pauli_term.pauli, "Pauli term is empty." expectation = 0 num_shots = sum([shots for _, shots in statistics]) for state, shots in statistics: prob = shots / num_shots parity_sign = 1 for idx, pauli in enumerate(pauli_term.pauli): if pauli == Pauli.Z and state[idx] == 1: parity_sign *= -1 expectation += parity_sign * prob return expectation def G_mat( ansatz: QuantumProgram, thetas: list[float], hamiltonian: list[PauliTerm] ) -> np.array: """ Calculate the G matrix from equation (7) in the paper. The matrix elements G_alpha,j are defined as the expectation value of the Pauli term P_alpha with respect to the gradient of the ansatz with respect to the j-th angle. The gradient is calculate using the parameter shift rule: G_alpha,j = 1/4 * ( - ) where |Psi(theta)> is the state of the ansatz with the angles theta. """ assert hamiltonian, "Hamiltonian is empty." num_of_paulis = len(hamiltonian) num_of_nodes = len(hamiltonian[0].pauli) num_of_angles = (num_of_nodes * (num_of_nodes - 1)) // 2 assert ( len(thetas) == num_of_angles ), "number of angles is not equal to the number of pairs of nodes" G = np.zeros((num_of_paulis, num_of_angles)) for j, theta in enumerate(thetas): thetas[j] = theta + np.pi / 4 _, measure_stats_p = z_output_measurements( qprog=ansatz, output_qreg="nodes", param_vals={"angles": thetas} ) thetas[j] = theta - np.pi / 4 _, measure_stats_m = z_output_measurements( qprog=ansatz, output_qreg="nodes", param_vals={"angles": thetas} ) thetas[j] = theta for alpha, pauli_term in enumerate(hamiltonian): G[alpha, j] = ( pauli_term_expectation(measure_stats_p, pauli_term) - pauli_term_expectation(measure_stats_m, pauli_term) ) / 4 return G def state_pauli_eval(state: list[int], pauli_term: PauliTerm) -> float: """ Evaluate the expectation of a Pauli string for a given a state. The state is a list of 0s and 1s, where 0 means the qubit is in the |0> state and 1 means the qubit is in the |1> state. The Pauli string is given by the `pauli` field of the PauliTerm class, ignoring the `coefficient` field. The function wraps the given state to create a `measure_stats_t` object, and then calls the `pauli_term_expectation` function to calculate the expectation value. NOTE: currently, only Z (and I) terms are supported in the Pauli string. Args: state (list[int]): The state of the output quantum register. pauli_term (PauliTerm): The Pauli term to be evaluated. Returns: float: The evaluation of the Pauli term. """ ms = [(state, 1)] return pauli_term_expectation(ms, pauli_term) def operator_expectation( f_state: Callable[[list[int]], float], statistics: measure_stats_t ) -> float: """ Calculate the expectation value of an operator given a set of statistics. The operator is defined by the function `f_state`, which takes a state as input and returns a float value. This provides the flexibility to define any operator. The statistics are a list of tuples, where each tuple contains the state of the output quantum register and the number of shots for that state. The expectation value is calculated by summing the operator value times the probability for each state. Args: f_state (Callable[[list[int]], float]): The operator function to be evaluated. statistics (list[Tuple[list[int], int]]): The statistics of the Z output measurements. Returns: float: The expectation value of the operator. """ assert statistics, "Statistics list is empty." expectation = 0 num_shots = sum([shots for _, shots in statistics]) for state, shots in statistics: prob = shots / num_shots expectation += f_state(state) * prob return expectation def D_vec( ansatz: QuantumProgram, thetas: list[float], hamiltonian: list[PauliTerm], prev_energy: float, ) -> Tuple[np.array, float, float]: """ Calculate the D vector from equation (8) in the paper. The equation can be rewritten as: D_alpha = which equals: D_alpha = where E_tau is the previous energy, P_alpha is the Pauli string for the alpha term, and c_alpha is the coefficient of the alpha term in the Hamiltonian. Args: ansatz (SerializedQuantumProgram): The ansatz circuit. thetas (list): The angles of the ansatz. hamiltonian (list): The Hamiltonian as a list of Pauli strings and weights. prev_energy (float): Previous Hc (energy) value sigma_0 (float): The standard deviation of the initial energy. Returns: np.array: The D vector. float: The expectation value of the f(H_c) operator, used as the next "prev_energy". """ assert hamiltonian, "Hamiltonian is empty." H_c = lambda state: sum( state_pauli_eval(state, pauli_term) * pauli_term.coefficient for pauli_term in hamiltonian ) # measure the circuit with the provided rotation angles # and calculate the standard deviation of the energy _, measure_stats = z_output_measurements( qprog=ansatz, output_qreg="nodes", param_vals={"angles": thetas} ) num_of_paulis = len(hamiltonian) result = np.zeros(num_of_paulis) for alpha in range(num_of_paulis): P_alpha = lambda state: state_pauli_eval(state, hamiltonian[alpha]) vec_el = lambda state: P_alpha(state) * (prev_energy - H_c(state)) result[alpha] = operator_expectation(vec_el, measure_stats) return result, operator_expectation(H_c, measure_stats) def solve_linear_system_with_svd_threshold( G: np.ndarray, D: np.ndarray, threshold_factor: float = 0.01 ) -> np.ndarray: """ Solves the linear system G @ dot_theta = D using SVD and discards singular values below a threshold. Args: G (np.ndarray): The G matrix. D (np.ndarray): The D vector. threshold_factor (float): The factor relative to the maximum singular value below which singular values are discarded. Defaults to 0. 0 1. Returns: np.ndarray: The solution vector dot_theta. """ U, s, Vt = np.linalg.svd(G, full_matrices=False) max_s = s[0] # s is sorted descending thresh = threshold_factor * max_s # Invert only the “large enough” singular values s_inv = np.array([1 / si if si >= thresh else 0.0 for si in s]) G_pinv = Vt.T @ (s_inv[:, None] * U.T) # Solve for dot_theta dot_theta = G_pinv @ D return dot_theta def hamiltonian_expectation_by_pauli_term( statistics: measure_stats_t, hamiltonian: list[PauliTerm], ) -> float: """ Calculate the expectation value of a Hamiltonian given a set of statistics. The Hamiltonian is a weighted sum of Pauli strings. Args: statistics (list[Tuple[list[int], int]]): The statistics of the Z output measurements. hamiltonian (list[PauliTerm]): The Hamiltonian to be estimated. Returns: float: The expectation value of the Hamiltonian. """ assert statistics, "Statistics list is empty." assert hamiltonian, "Hamiltonian is empty." expectation = 0 for pauli_term in hamiltonian: expectation += ( pauli_term_expectation(statistics, pauli_term) * pauli_term.coefficient ) return expectation def var_it_step( ansatz: QuantumProgram, thetas: list[float], hamiltonian: list[PauliTerm], prev_energy: float, delta_tau: float = 0.01, ) -> Tuple[list[float], float, float, Tuple[list[int], list[int]]]: """ Update the angles of the ansatz using the D vector and G matrix. The update is done using the equation: dot_theta = G^-1 D where G is the G matrix and D is the D vector. Args: ansatz (SerializedQuantumProgram): The ansatz circuit. thetas (list): The angles of the ansatz. hamiltonian (list): The Hamiltonian as a list of Pauli strings and weights. prev_energy (float): Previous Hc (energy) value sigma_0 (float): The standard deviation of the initial energy. delta_tau (float): The step size for the update. Returns: list: The updated angles of the ansatz. float: The updated "prev_energy" of the ansatz. float: the updated expectation value of the Hamiltonian. tuple: node partitioning from truncation of the Z measurement of the ansatz. """ assert hamiltonian, "Hamiltonian is empty." G = G_mat(ansatz, thetas, hamiltonian) D, energy = D_vec(ansatz, thetas, hamiltonian, prev_energy) dot_theta = solve_linear_system_with_svd_threshold(G, D) new_thetas = [ theta + delta_tau * theta_grad for theta, theta_grad in zip(thetas, dot_theta) ] qubits, measure_stats = z_output_measurements( ansatz, "nodes", param_vals={"angles": thetas} ) node_vals = [0 if qbit > 0 else 1 for qbit in qubits] node_idxs = range(len(qubits)) par1 = [] par2 = [] for v, n in zip(node_vals, node_idxs): if v > 0: par1.append(n) else: par2.append(n) return ( new_thetas, energy, hamiltonian_expectation_by_pauli_term(measure_stats, hamiltonian), (par1, par2), ) ``` ```python theme={null} def maxcut_cost(G: nx.Graph, partition: Tuple[list, list]) -> float: """ Calculate the cost of a maxcut partition. Args: G (nx.Graph): The input graph. partition (Tuple[list, list]): A tuple containing two partitions of the graph. Returns: float: The cost of the maxcut partition. """ cut_size = 0 physics_value = lambda x: 1 if x in partition[0] else -1 cs_value = lambda x: 1 if x in partition[0] else 0 physics_cost = lambda u, v: ( G.edges[u, v]["weight"] * physics_value(u) * physics_value(v) ) cs_cost = lambda u, v: ( -G.edges[u, v]["weight"] * (0 if cs_value(u) == cs_value(v) else 1) ) cost = ( physics_cost if Graph_Cost_Convention[G.problem_type] == CostConvention.PHYSICS else cs_cost ) for u, v in G.edges(): if (u in partition[0] and v in partition[1]) or ( u in partition[1] and v in partition[0] ): cut_size += cost(u, v) return cut_size ``` ```python theme={null} V = len(G.nodes) NUM_ANGLES = (V * (V - 1)) // 2 rng = np.random.default_rng(seed=42) thetas = [0.0] * NUM_ANGLES # rng.random(NUM_ANGLES).tolist() hamiltonian = graph_to_hamiltonian(G) ``` # ## VAR-IT Optimization ```python theme={null} delta_tau = 0.1 energy = 0.0 energies = [] maxcut_costs = [] for iter in range(1, 15): new_thetas, new_energy, h_exp, partition = var_it_step( ansatz=qprog, thetas=thetas, hamiltonian=hamiltonian, prev_energy=energy, delta_tau=delta_tau / np.sqrt(iter), ) thetas = new_thetas energy = new_energy maxcut_cost_value = maxcut_cost(G, partition) energies.append(energy) maxcut_costs.append(maxcut_cost_value) print( f"iteration {iter}: energy = {energy:.3f}, = {h_exp:.3f}, maxcut_cost = {maxcut_cost_value:.3f}" ) ``` **Output:** ``` iteration 1: energy = -1.272, = -1.200, maxcut_cost = -1.174 iteration 2: energy = -1.405, = -1.519, maxcut_cost = -1.174 iteration 3: energy = -1.543, = -1.597, maxcut_cost = -2.260 iteration 4: energy = -1.646, = -1.545, maxcut_cost = -2.260 iteration 5: energy = -1.666, = -1.571, maxcut_cost = -1.743 iteration 6: energy = -1.653, = -1.683, maxcut_cost = -1.560 iteration 7: energy = -1.662, = -1.722, maxcut_cost = -1.560 iteration 8: energy = -1.698, = -1.730, maxcut_cost = -1.743 iteration 9: energy = -1.794, = -1.788, maxcut_cost = 0.000 iteration 10: energy = -1.807, = -1.814, maxcut_cost = -1.743 iteration 11: energy = -1.842, = -1.788, maxcut_cost = -1.353 iteration 12: energy = -1.844, = -1.797, maxcut_cost = -1.743 iteration 13: energy = -1.897, = -1.817, maxcut_cost = -2.260 iteration 14: energy = -1.864, = -1.864, maxcut_cost = -1.743 ``` ## Create a Convergence Figure ```python theme={null} from itertools import chain, combinations def solve_maxcut(G: nx.Graph) -> Tuple[int, Tuple[list, list]]: """ Finds the maximum cut of a graph using brute force and itertools. Args: graph (nx.Graph): The input graph. Returns: tuple: A tuple containing the maximum cut size and the partition of nodes. """ nodes = list(G.nodes()) max_cut_size = 0 best_partition = None for partition1 in chain.from_iterable( combinations(nodes, r) for r in range(1, len(nodes)) ): partition2 = [node for node in nodes if node not in partition1] cut_size = 0 for u, v in G.edges(): if (u in partition1 and v in partition2) or ( u in partition2 and v in partition1 ): cut_size += G.edges[u, v]["weight"] # change to 1 for unweighted maxcut if cut_size > max_cut_size: max_cut_size = cut_size best_partition = (list(partition1), partition2) return max_cut_size, best_partition ``` ```python theme={null} plt.figure(figsize=(8, 6)) maxcut_val, _ = solve_maxcut(G) plt.plot( list(range(1, len(energies) + 1)), energies, marker="o", linestyle="-", color="b", label="", ) plt.plot( list(range(1, len(energies) + 1)), maxcut_costs, marker="o", linestyle="-", color="g", label="MaxCut", ) plt.axhline( y=-maxcut_val, color="r", linestyle="--", label=f"Optimal MaxCut = {-maxcut_val:.3f}", ) plt.title("Maxcut Energy") plt.xlabel("Iteration") plt.ylabel("Energy") plt.legend() plt.grid(True) plt.show() ``` output # One-Dimensional Fermi-Hubbard Model Source: https://docs.classiq.io/explore/applications/physical_systems/fermi_hubbard_model_1D/fermi_hubbard_1D Open this notebook in GitHub to run it yourself ## Overview This notebook implements a quantum simulation of the one-dimensional Fermi-Hubbard model using Classiq's quantum programming framework. The simulation proceeds in three stages: 1. **Initial state preparation** * The ground state of a non-interacting Hamiltonian with a spin-dependent attractive potential is prepared as a Slater determinant via Givens rotations. This creates an initial state with localized charge and spin densities. 1. **Time evolution** * The system is quenched to the interacting Fermi-Hubbard Hamiltonian and propagated using a first-order Trotterized circuit, where each Trotter step is decomposed into hopping and interaction layers using the Jordan-Wigner transformation. 1. **Measurement and analysis** * Charge and spin densities are measured at each time step to observe the dynamics, testing the phenomenon of spin-charge separation at intermediate interaction strengths. Finally, the results are validated against exact analytical solutions in two limiting cases: the non-interacting limit ($U=0$), where the dynamics reduce to single-particle evolution, and the vanishing hopping limit ($J=0$), where the Trotter decomposition becomes exact and all densities are frozen. The simulation closely follows the experimental and algorithmic procedure desribed in \[[1](#google-paper)] ## Introduction The Fermi-Hubbard model is a fundamental corner step of condensed matter physics, describing the interplay between kinetic energy and repulsion interaction between electrons in a solid. Although very simple, in certain parameter regimes, the model showcases a variety strongly correlated phenomena, such as the metal-insulator transition (Mott physics), antiferromagnetism, inhomogeneous phases, and unconventional Fermi liquids. In order to motivate or conceptually derive the model, consider of a metalic material composed of atoms organized in a regular pattern, i.e., a lattice. We assume the temperature is sufficiently low, so the that the vibrations of the atoms can be neglected and the atoms are essentially held in a fix position in the lattice sites. Moreover, typically each atom has a handful of relevant energy levels, however, in order to simplify the problem we will consider only a single energy level at each lattice site (one energy level per atom). In the metal, valance electrons move freely between the metal atoms, leading to the expected conductive behaviour. The movement of the electrons between the sites is dictated by the overlap of the spatial wave function on different sites. Since the atomic wave-functions decay exponentially with the distance to the nuclei, we consider only the dominant contribution, giving rise to hopping of electrons only between adjacent lattice sites. These consideration lead to the first the hopping term, constituting the kinetic energy contribution: $-J \sum_{\langle i,j \rangle, \sigma} \left ( c_{i\sigma}^\dagger c_{j\sigma} + c_{i\sigma}^\dagger c_{j\sigma}\right)$, where $c_{i\sigma}$ is the fermionic annihilation operator on the $i$'th lattice site and $\sigma = \uparrow, \downarrow$ spin. In addition, $\langle i,j\rangle$ designates that the sum is only over nearest-neighbors. A second contribution arises from the repulsive Coulomb interaction between two electrons. Such interaction is strongest between electrons on the same atom. We consider only this dominant contribution, adding an energy penalty of $U$, for two-electrons on the same site. In addition, Pauli's exclusion principle dictates that two electrons cannot be in the same quantum state, therefore the repulsive interaction can only be between electrons with different spins: $U \sum_{j} c_{j \uparrow}^\dagger c_{j \uparrow} c_{j\downarrow}^\dagger c_{j\downarrow}$. Overall, the Fermi-Hubbard Hamiltonian is given by: $$ H_{HF} = -J \sum_{\langle i,j \rangle, \sigma} \left ( c_{i\sigma}^\dagger c_{j\sigma} + c_{i\sigma}^\dagger c_{j\sigma}\right) + U \sum_{j} n_{j \uparrow} n_{j\downarrow}~~,~~~~~~ (1) $$ where $ n_{j \sigma}= c_{j \sigma}^\dagger c_{j \sigma}$ is the number operator. The model constitutes a key tool in the investigation of transition metal oxides, organic conductors and cuprates, which exhibit high-temperature superconductivity and exotic quantum magnetism properties and unconventional symmetry breaking. Despite its apparent simplicity, the model is notoriously challenging, in 1D the Fermi-Hubbard model is solvable via Bethe ansatz \[[2](#liebwu)], while the 2D and 3D cases have not exact solution (for general hopping and interaction coefficients, $J$ and $U$). However, in 3D quantum phenomena are suppressed and the model typically exhibit classical behaviour, well described by mean-field theories. Numerical studies are also limited beyond specific doping regimes (doping modifies the fraction of electrons/lattice sites). Perturbation theory becomes invalid when strong correlations between the electron emerge, while Monte-Carlo methods are restricted by the sign problem away from half filling (at half filling the number of electrons equals the number lattice sites). The physics of the model are conveniently analysed by measuring the evolution of the spin densities $$ \rho_j^{\pm}(t) = \langle n_{j,\uparrow}\rangle \pm \langle n_{j,\downarrow} \rangle~~. $$ We consider a simplified model consisting of a lattice of $N=4$ sites, and study the dynamics for an intermediate interaction strength, $U/J = 3$. ## Classiq Implementation We implement the Hamiltonian simulation in three steps, utilizing Classiq's built-in functions. 1. Initial state-preparation: Efficiently prepare the ground state of a non-interacting FH system in the presence of an external spin dependent potential. The ground state is a Gaussian Fermionic state, creating a localized density peak, acting as a wavepacket source. 1. Time-evolution: Abruptly turn on the two-electron repulsion interaction and turn off the external potential, leading to dynamics governed by Hamiltonian (1). Utilizing a first-order Trotter expansion, the system is evolved in time. 1. Measurement: The spin densities are evaluated. Repetition of these three steps many times produces the evolution of expectation values of spin-density in time, $\{\rho^{\pm}_{j}(t)\}$. We begin by importing the required software packages and defining global constants ```python theme={null} !pip install -u -qq "classiq[chemistry]" ``` **Output:** ``` [optparse.groups]Usage:[/] pip install \[options] \[package-index-options] ... pip install \[options] -r \[package-index-options] ... pip install \[options] [-e] ... pip install \[options] [-e] ... pip install \[options] ... no such option: -u ``` ```python theme={null} import numpy as np from openfermion.circuits.slater_determinants import ( slater_determinant_preparation_circuit, ) from openfermion.linalg.givens_rotations import ( fermionic_gaussian_decomposition, givens_decomposition, ) from openfermion.ops import QuadraticHamiltonian from classiq import * ``` ```python theme={null} N = 4 # number of lattice sites a = 1 # lattice spacing L = N * a # length of the lattice M = 2 * N # total number of fermionic modes NUM_ITERS = 10 J = 1 # hopping strength tau = 0.1 / J # Trotter step time interval T = NUM_ITERS * tau ``` # ## Initial State Preperation We begin by desribing a general algorithm to prepare Slater determinant states. Following, the algorithm is benchmarked on a simple example involving a four-by-four single particle hamiltonian. The state preperation algorithm is then utilized to prepare the ground state of the non-interacting Fermi-Hubbard Hamiltonian for the case of a single electron. This is then benchmarked against an analitical solution. Finally, we prepare on the initial state of the dynamic simulation, a ground state of the Fermi-Hubbard model for a quater-filling. # ### Preparation of a Slater Determinant State A Slater-Determinant is the ground state of a quadratic Fermionic Hamiltonian which conserves the number of particles. We begin by discussing an efficient algorithm to construct the initial non-interacting state. As all ground states of non-interacting fermionic Hamiltonian, the ground state is a fermionic Gaussian state, which can be prepared in a worst-case circuit depth of $O(N^2)$ \[[3](#fermionic-gaussian-state)]. Specifically, for a quadratic Hamiltonian which conserves the number of particles, the fermionic Gaussian state can be expressed as a Slater determinant. For a general number conserving quadratic fermionic Hamiltonian $$ H = \sum_{\mu\nu}c_\mu^\dagger h_{\mu\nu}c_\nu~~,~~~~(2) $$ where $h$ is known as the **one-body Hamiltonian**. Using the anti-commutation relations and the Heisenberg equation ($\frac{dO(t)}{dt} = i\left[H, O(t) \right]$), we have $$ \frac{dc_\lambda^\dagger}{dt} = i\left[H, c_\lambda^\dagger \right]= i \sum_{\mu\nu} h_{\mu\nu}[c_\mu^\dagger c_\nu,c_\lambda] $$ $$ = i \sum_{\mu\nu} h_{\mu\nu}[c_\mu^\dagger c_\nu,c_\lambda^\dagger] = i \sum_{\mu\nu} h_{\mu\nu}c_\mu^\dagger \delta_{\nu \lambda} = i \sum_{\mu} h_{\mu\lambda}c_\mu^\dagger ~~, $$ where the the time-dependence is suppressed for concisness. The dynamics in the Heisenberg representation can therefore be expressed as $$ \mathbf{c}^\dagger (t) = e^{i H t} \mathbf{c}^\dagger e^{-i H t} = e^{i h^T }\mathbf{c}^\dagger~~, $$ where $\mathbf{c}^\dagger = \{c_1^\dagger,\dots,c_M^\dagger\}^T$ and $M$ is the total number of fermionic modes. Remarkably, this implies that the diagonalization of $h$, (an $M$ by $M$ matrix) provides the dynamics in the $2^M$ Hilbert space. Diagonalizing the single particle Hamiltonian $M= \bar{Q} D \bar{Q}^\dagger$, where $\bar{Q}$ is and $M$-by-$M$ unitary matrix and $D = \text{diag}(\epsilon_1,...,\epsilon_M)$, we obtain $H=\sum_{k=1}^M \epsilon_k d_k^\dagger d_k$, where $$ d_\eta^\dagger= \sum_{\mu=1}^{M} \bar{Q}_{\mu \eta}c_\mu^\dagger~~. \tag{3} $$ Alternatively, the basis transformation can be expressed in terms of the single-particle transformation, $U\mathbf{c}^\dagger = \mathbf{d}^\dagger~,$ where we collected the creation operators to form an operator valued vector: $\mathbf{c}^\dagger = \{c_1^\dagger,\dots,c_M^\dagger\}^T$ and similarly for $\mathbf{d}^\dagger$. For a fixed particle number $N_{\text{elec}}$ the ground state is given by $$ |\psi_{g.s}\rangle =\Pi_{k} {\cal U}c_k^\dagger |0^M\rangle = \Pi_{k=1}^{N_{\text{elec}}} d_k^\dagger |0^M\rangle ~~, $$ where $i$ iterates over the occupied fermionic modes and ${\cal U} c_j^\dagger {\cal U}^\dagger = U_{[j,:]} \mathbf{d}^\dagger = d_j^\dagger$. The diagonalization of $h$ scales only polynomially with the number of lattice sites, $O(L^3)$ and can be efficiently done on a classical computer. In order to prepare the desired ground state we focus on a part of $\bar{Q}$ which corresponds to the occupied modes, and denote the $N_{\text{elec}}$ by $M$ matrix describing these modes by $Q = (\bar{Q}^T)_{[{\text{occupied modes}},:]}$. This identification leads to an alternative form for Eq. (3), for the occupied modes we have $$ d_\eta^\dagger= \sum_{\mu=1}^{M} {Q}_{\eta \mu}c_\mu^\dagger~~. $$ An efficient quantum circuit for the ground state preparation can be obtained by a modified QR decomposition of $Q$, using a product of elementary two-mode rotations, called Given rotations. Each rotation can be expressed as $$$ \begin{pmatrix} \mathcal{G}c_k^\dagger\mathcal{G}\\ \mathcal{G}c_j^\dagger\mathcal{G} \end{pmatrix} = G(\theta,\varphi)\, \begin{pmatrix} c_k\\ c_j \end{pmatrix}~~, $ and the form of a Given rotation is $$G_\{jk\}(\theta,\varphi) = \begin\{pmatrix\} \cos(\theta) & -e^\{i\varphi\}\sin(\theta)\\ \sin(\theta) & e^\{i\varphi\}\cos(\theta) \end\{pmatrix\}~~. $$$ To simplify the decomposition we begin by utilizing the invariance of the Slater determinant (up to a global phase) under the mapping ${Q}\rightarrow V{Q}$, where $V$ is a unitary transformation. For the transformation of basis to be valid we require that the first $N_{\text{elec}}$ rows of $VQ$ are equal to $U$, or alternatively $$ V\{Q\}U^\dagger = (I_\{N_\{\text\{elec\}\}\}, \boldsymbol\{0\})~~. $$ Each Given transformation operates only on two columns and it's parameters, $\theta$ and $\phi$ are set so to nullify elements in the upper right part of the matrix. Due to the orthogonality of the rows of $Q$, some transformations nullify more than a single matrix element. As a result, the total number of required Given transformations are $N_G = N_{\text{elec}}(M-N_\text{elec})$, where $N_\text{elec}$ is the number of electrons and $M$ is the number of fermionic modes. The diagonalization procedure results in a product of Given rotations $$ U = G_\{N_G\}\cdots G_2 G_1~~. $$ After the Jordan-Wigner transformation, each two-mode ($j,k$) rotations correspond to a rotation in the single particle subspace of the two qubits $j$ and $k$ ($|01\rangle$ and $|10\rangle$). This completely, defines the state preparation circuit in terms of a sequence two-qubit rotations. The gate complexity is $O(N_G) = O(N_{\text{elec}}^2)$ (worst case achieved for $N_\text{elec}=M/2$), and parallelization leads to a circuit depth of $M-1$. #### Given Rotations Example In order to understand the state preparation algorithm, we first show how Given rotations are utilized to diagonalize a simple one-body Hamiltonian. Consider a simple $4$-by-$4$ Hermitian matrix, representing a one-body Hamiltonian: $$ h = \begin\{pmatrix\} \varepsilon_0 & t_\{01\} & 0 & t_\{03\} \\ t_\{01\} & \varepsilon_1 & t_\{12\} & 0 \\ 0 & t_\{12\} & \varepsilon_2 & t_\{23\} \\ t_\{03\} & 0 & t_\{23\} & \varepsilon_3 \end\{pmatrix\} $ $$ ```python theme={null} ## Defining the single-body Hamiltonian # Onsite energies eps0, eps1, eps2, eps3 = 0.8, -0.4, 0.3, -0.1 # Hopping amplitudes (real for simplicity) t01 = 0.25 t12 = -0.35 t23 = 0.20 t03 = 0.15 # 4x4 Hermitian one-body matrix h_{pq} h = np.array( [ [eps0, t01, 0.0, t03], [t01, eps1, t12, 0.0], [0.0, t12, eps2, t23], [t03, 0.0, t23, eps3], ], dtype=complex, ) ``` We employ the methods of OpenFermion's `QuadraticHamiltonian` class in order to extract the Given rotations. The number conserving quadratic Hamiltonian is then diagonalized utilizing a Bogoliubov transformation, leading to the `orbital_energies` and a `transformation_matrix`. ```python theme={null} # Number-conserving quadratic Hamiltonian (no pairing term) qh = QuadraticHamiltonian(h, constant=0.0) orbital_energies, transformation_matrix, constant = ( qh.diagonalizing_bogoliubov_transform() ) # Taking the number of electrons to be equal to be two occupied_orbitals = np.where(orbital_energies < 0.0)[0] slater_determinant_matrix = transformation_matrix[occupied_orbitals] E, Qbar = np.linalg.eigh(h) # Sanity check: the transformation matrix from the Bogoliubov transform should diagonalize the one-body Hamiltonian assert np.allclose(transformation_matrix, Qbar.T) assert np.allclose(orbital_energies, E) assert np.allclose(Qbar.T @ h @ Qbar, np.diag(E)) ``` We extract the given rotations utilizing OpenFermion's `given_decomposition`, returning a list of tuples: `[(G_1,G_2),(G_3,),...]`. Each tuple includes the Given rotations which can be operated in parallel. The Given rotations are encoded as a tuple: $G_k = (i_k,j_k,\theta_k,\phi_k)$, where $i_k$ and $i_k$ are the columns of $U^\dagger$ which the Given rotation $G_k^\dagger$ is operated on from the right (see calculation below). ```python theme={null} rotations, V, diag = givens_decomposition(slater_determinant_matrix) ``` We introduce two utility functions to demonstrate the decomposition, `givens_gate` implements the two-by-two matrix and `build_U_from_rotations`, gathers all the Given rotations to create $U$. ```python theme={null} def givens_gate(theta: float, phi: float) -> np.ndarray: """ Givens rotation: G(i,j,theta, phi) = [[ cos(theta), -e^{i phi} sin(theta)], [ sin(theta), e^{i phi} cos(theta)]] """ c = np.cos(theta) s = np.sin(theta) e = np.exp(1j * phi) return np.array([[c, -e * s], [s, e * c]], dtype=complex) def build_U_from_rotations(M: int, rotations) -> np.ndarray: """ Reconstruct U (M x M) from OpenFermion 'givens_rotations' list. OpenFermion applies these to columns during decomposition; updating columns by right-multiplying with G^\dagger reproduces the same effect. """ U_dagger = np.eye(M, dtype=complex) for parallel_ops in rotations: for i, j, theta, phi in parallel_ops: G = givens_gate(theta, phi) cols = U_dagger[:, [i, j]] U_dagger[:, [i, j]] = cols @ G.conj().T return U_dagger.conj().T def build_state_from_rotations(M: int, N: int, circuit_description: list) -> np.ndarray: v = np.zeros((M,), dtype=complex) v[:N] = np.ones_like(v[:N]) for parallel_ops in circuit_description: for i, j, theta, phi in parallel_ops: G = givens_gate(theta, phi) v[[i, j]] = G @ v[[i, j]] return v ``` Next, we decompose $U$ into Given rotations: $$ U = G_{N_G},\dots,G_1 $$ and verify that $V Q U^\dagger = (I,\mathbf{0})$. ```python theme={null} tol = 1e-8 Q = np.asarray(slater_determinant_matrix, dtype=complex) n, m = Q.shape U = build_U_from_rotations(m, rotations) # Check V Q.T U^\dagger = D where D has diag entries in first m columns and zeros elsewhere D = np.zeros((n, m), dtype=complex) D[np.arange(n), np.arange(n)] = diag A = V @ Q @ U.conj().T # Normalize to (I,0) by removing the diagonal unitary on the left: # Let Dm = diag(diag) (m x m). Then Dm^\dagger (V Q U^\dagger) = (I,0). # So define V' = Dm^\dagger V. Dm_dag = np.diag( np.conjugate(diag) ) # Dm^\dagger since diag entries are unit-modulus in theory Vprime = Dm_dag @ V I0 = np.zeros((n, m), dtype=complex) I0[:, :n] = np.eye(n, dtype=complex) B = Vprime @ Q @ U.conj().T assert np.allclose(A, D, atol=tol, rtol=tol) assert np.allclose(B, I0, atol=tol, rtol=tol) ``` State preparation check ```python theme={null} # State preparation check for the single excitation subspace slater_determinant_matrix = transformation_matrix[[0]] rotations, V, diag = givens_decomposition(slater_determinant_matrix) circuit_description = reversed(rotations) ground_state = build_state_from_rotations(m, n, circuit_description) assert np.allclose(h @ Qbar[:, 0], E[0] * Qbar[:, 0], atol=tol, rtol=tol) ``` #### Initial State Preparation of the Fermi Hubbard Model The initial state is chosen to be the ground state of the non-interacting (i.e, quadratic in the fermionic creation/annihilation operators) Hamiltonian $$ H_{0} =-J \sum_{j, \sigma} \left ( c_{j,\sigma}^\dagger c_{j+1,\sigma} + c_{j+1,\sigma}^\dagger c_{j,\sigma}\right) + \sum_{j,\sigma}\epsilon_{j,\sigma}n_{j,\sigma} ~~, $$ where the spin-up fermions feel a Gaussian attractive potential $$ \epsilon_{j,\uparrow} = -\lambda \exp \left[ -\frac{(j-m)^2}{2 w^2}\right]~~, $$ where spin-down fermions feel no potential, $\epsilon_{j,\downarrow}$. The parameters $\lambda$, $m$ and $w$ dictate the precise shape and strength of the potential. Such potential creates a localized density peak of spin-up fermions and a flatter distribution for the spin-down fermions. As a result, the simulation begins with localized charge and spin densities. This state is generally not an eigenstate of the interacting Hamiltonian, constituting a highly excited (non-equilibrium) of the interacting system. We begin by defining a function mapping lattice and spin degrees of freedom to qubit number, these will assist us to associate fermionic degrees of freedom to the corresponding qubits in the quantum variables. ```python theme={null} def qubit_idx(site: int, spin: int): """ Maps lattice site and spin to qubit indices. Non-interleaved layout: spin-up modes occupy qubits 0..N-1, spin-down modes occupy qubits N..2N-1. This ensures same-spin hopping acts on adjacent JWT qubits. Args: site (int): Lattice site index, the range [0,N-1] spin (int): Spin index, either 0 or 1 Returns: qubit_idx (int): qubit index """ return site + spin * N def qubit_idx_to_site_and_spin(qubit_idx: int): """ Maps qubit index to site and spin indices. Args: qubit_idx (int): qubit index Returns: site (int): site index spin (int): spin index """ spin = qubit_idx // N site = qubit_idx % N return site, spin ``` The Givens rotation matrix $G(\theta, \varphi)$ used in [\[2\]](#fermionic-gaussian-state) acts on the single-particle subspace of two fermionic modes $i$ and $j$ as $$ G(\theta, \varphi) = \begin{pmatrix} \cos\theta & -e^{i\varphi}\sin\theta \\ \sin\theta & e^{i\varphi}\cos\theta \end{pmatrix}~~, $$ where the first (second) row/column corresponds to mode $i$ ($j$). When this $2\times 2$ block is embedded in a two-qubit unitary, the mapping between matrix indices and qubit states depends on the qubit ordering convention of the quantum framework. OpenFermion's `givens_decomposition` returns rotation parameters $(i, j, \theta, \varphi)$ assuming the standard (little-endian) convention where the first qubit in the register is the least significant bit: $|01\rangle \to$ mode $i$ occupied, $|10\rangle \to$ mode $j$ occupied. Classiq's `unitary` gate, however, uses **big-endian** ordering where the first qubit in the list is the most significant bit: $|01\rangle \to$ mode $j$ occupied, $|10\rangle \to$ mode $i$ occupied. Under the big-endian convention, the $G$ matrix as written above effectively implements $G^{-1}$ (the inverse rotation) in the single-particle picture, introducing alternating sign errors $(-1)^j$ in the prepared state amplitudes. To compensate, we **swap the qubit order** in the call to `G`, passing `[qba[j], qba[i]]` instead of `[qba[i], qba[j]]`. This restores the correct mode-to-index mapping so that the circuit faithfully implements the Givens rotations from the decomposition. ```python theme={null} @qfunc def G(theta: float, phi: float, qba: QArray[QBit, 2]): """ Implements a Given rotation on two qubits. """ c = np.cos(theta) s = np.sin(theta) e = np.exp(1j * phi) U = [ [1, 0, 0, 0], [0, c, -e * s, 0], [0, s, e * c, 0], [0, 0, 0, 1], ] unitary(U, qba) @qfunc def given_rotation(gate: list[int, int, float, float], qba: QArray[QBit, M]) -> None: """ Implements a Given rotation on two specific qubits, i and j, from an M-qubit quantum array, qba. Note: the qubit order is swapped ([qba[j], qba[i]]) to account for Classiq's big-endian qubit ordering in the unitary gate (see markdown cell above). """ i, j, theta, phi = gate G(theta, phi, [qba[j], qba[i]]) @qfunc def prepare_slater_det(h: list[list[float, M], M], Nelec: int, qba: QArray[QBit, M]): """ Prepares the ground state associated with the single electron matrix h. The Hamiltonian satisfies H = \sum_{\mu,\nu}c_{\mu}^\dagger h_{\mu,\nu} c_{\nu} Args: h (ndarray): single electron matrix Nelec (int): number of electrons qba (list[QBit]): list of qubits """ # preparing the reference state, as the state with the first Nelec qubits in state |1> and the rest in state |0>. repeat(Nelec, lambda i: X(qba[i])) # diagonalizing h D, Qbar = np.linalg.eigh(h) Q = (Qbar.T)[:Nelec, :] # occupying the N lowest energy states rotations, _, _ = givens_decomposition(Q) # U = build_U_from_rotations(M, rotations) circuit_description = list(reversed(rotations)) # ground_state = build_state_from_rotations(M, N, circuit_description) for parallel_ops in circuit_description: for gate in parallel_ops: given_rotation(list(gate), qba) ``` The circuit can be verified by considering the single excitation subspace and comparing the compared state to the analytical result. The ground state of the single excitation subspace is up to a global phase just $$ d_1^\dagger | 0^M\rangle =\sum_\mu Q_{1,\mu} c^{\dagger}_\mu | 0^M\rangle ~~, $$ which after the Jordan-Wigner transformation corresponds to the quibt state with amplitudes $$ [Q_{1,1},Q_{1,2},\dots, Q_{1,M}]~~. $$ In order to prepare the initial state, we begin by constructing the initial Hamiltonian. Utility functions are defined and the Hamiltonian parameters are set. ```python theme={null} def sym(A): """ Symmetrizes the matrix A """ return (A + A.T) / 2 def kinetic_energy(N: int, J: float) -> np.ndarray: """ Builds single electron matrix of nearest-neighbor hopping term, hopping strength J, for an N-site open chain (no periodic boundary). """ K = np.zeros((2 * N, 2 * N), dtype=float) for site in range(N - 1): for spin in range(2): mu = qubit_idx(site, spin) nu = qubit_idx(site + 1, spin) K[mu, nu] = -J K = 2 * sym(K) return K def spin_potential(N: int, spin=0, parameters: tuple[float] = (1, 1, 1)) -> np.ndarray: """ Builds single electron matrix associated with the external potential. Associated with an N site lattice. """ lam, mean, std = parameters V = np.zeros((2 * N, 2 * N), dtype=float) for site in range(N): mu = qubit_idx(site, spin) V[mu, mu] = -lam * np.exp(-((site - mean) ** 2) / (2 * std**2)) return V def construct_single_electron_hamiltonian( N: int, J: float, parameters: tuple ) -> np.ndarray: return kinetic_energy(N, J) + spin_potential(N, spin=0, parameters=parameters) ``` ```python theme={null} lam = 3.0 mean = (L - 1) / 2 std = 0.5 h = construct_single_electron_hamiltonian(L, J=1, parameters=(lam, mean, std)) Nelec = 1 ``` ```python theme={null} @qfunc def main(qba: Output[QArray[QBit, M]]) -> None: allocate(M, qba) prepare_slater_det(h, Nelec, qba) qprog = synthesize(main) ``` The state preparation quantum circuit. The Given rotation, operating on two qubits, is decomposed into the basis gates. State preparation circuit #### Single State Excitation Subspace Verification For the single excitation subspace we can easily compare the preparation of the quantum state with the exact diagonalization. To compare between the two we normalize the quantum solution so to cancel the difference in the global phase with respect to the classical result. ```python theme={null} backend_preferences = ClassiqBackendPreferences( backend_name=ClassiqSimulatorBackendNames.SIMULATOR_STATEVECTOR ) # Construct a representation of HHL model def state_check_model(main, backend_preferences): qmod_state_check = create_model( main, execution_preferences=ExecutionPreferences( num_shots=1, backend_preferences=backend_preferences ), ) return qmod_state_check ``` ```python theme={null} # Construct the quantum program qmod_state_check = state_check_model(main, backend_preferences) qprog_state_check = synthesize(qmod_state_check) print("Circuit depth = ", qprog_state_check.transpiled_circuit.depth) res_state_check = execute(qprog_state_check).result_value() ``` **Output:** ``` Circuit depth = 13 ``` ```python theme={null} import matplotlib.pyplot as plt def get_quantum_amplitudes(result): df = result.dataframe mask = np.abs(df["amplitude"]) > 1e-12 filtered = df.loc[mask, ["bitstring", "amplitude"]].copy() amps = filtered["amplitude"].to_numpy() bitstring = filtered["bitstring"].tolist() # must be integers # mapping the bitstrings to qubit indices in the array qubit_index = [[i for i, b in enumerate(s[::-1]) if b == "1"] for s in bitstring] qubit_index = np.array([i[0] for i in qubit_index]) idx = np.argsort(qubit_index) qubit_index = qubit_index[idx] amps = amps[idx] amps = amps / np.linalg.norm(amps) qsol = np.zeros(M, dtype=complex) qsol[qubit_index] = amps return qsol ``` ```python theme={null} # Compare quantum amplitudes with exact diagonalization (Nelec=1) qsol = get_quantum_amplitudes(res_state_check) # Exact ground state: lowest eigenvector of h E_exact, Qbar_exact = np.linalg.eigh(h) exact_state = Qbar_exact[:, 0] # Align global phase: multiply qsol by phase so that it matches exact_state overlap = np.dot(exact_state.conj(), qsol) phase_global = overlap / np.abs(overlap) qsol_aligned = qsol / phase_global fig, ax = plt.subplots(figsize=(6, 4)) # Real part ax.bar(np.arange(M) - 0.15, np.real(exact_state), width=0.3, label="Exact", alpha=0.8) ax.bar( np.arange(M) + 0.15, np.real(qsol_aligned), width=0.3, label="Quantum", alpha=0.8 ) ax.set_xlabel("Qubit index", fontsize=12) ax.set_ylabel("Amplitude (real)", fontsize=12) ax.set_title("Real Part", fontsize=14) ax.set_xticks(np.arange(M)) ax.legend() plt.suptitle("State Preparation vs Exact Ground State ($N_{elec}=1$)", fontsize=16) plt.tight_layout() plt.show() # Fidelity check fidelity = np.abs(np.dot(exact_state.conj(), qsol)) ** 2 print(f"Fidelity ||^2 = {fidelity:.10f}") assert fidelity > 0.95, f"Fidelity too low: {fidelity}" ``` output **Output:** ``` Fidelity ||^2 = 1.0000000000 ``` #### Initial State at Quater-Filling At quater-filling the number of electrons $N_{\text{elec}}$ equals to half number of lattice sites $N$ (quater of the $M=2N$ fermionic modes). The one-body potential strength $\lambda$ is set such that the two electrons will have different spins ($N_{\downarrow} = N_{\uparrow}=1$) ```python theme={null} Nelec = N // 2 # quater-filling ``` ```python theme={null} @qfunc def main(qba: Output[QArray[QBit, M]]) -> None: allocate(M, qba) prepare_slater_det(h, Nelec, qba) qprog = synthesize(main) ``` To image the ground state, we measure the charge and spin densities: $\rho_j^{\pm} = \langle n_{j,\uparrow}\rangle \pm \langle n_{j,\downarrow} \rangle$. The charge density, $\rho_j^{+}$, corresponds to the average number of electrons on the $j$'th site, while the spin density, $\rho_j^{-}$, is the net spin on the same site. The number operator $n_\mu = c^\dagger_\mu c_\mu$ maps under the Jordan-Wigner transformation to $$ n_\mu \rightarrow \frac{I-Z_{\mu}}{2}~~. $$ Therefore, the charge density maps to $$ \rho_j^{+} = 1 - (\langle Z_{j,\uparrow}\rangle + \langle Z_{j,\downarrow}\rangle)/2~~, $$ while the spin density corresponds to $$ \rho_j^{-} = (\langle Z_{j,\downarrow}\rangle - \langle Z_{j,\uparrow}\rangle)/2 ~~. $$ We introduce the functions `charge_density` and `spin_density` which allow measuring the corresponding observables. ```python theme={null} def charge_density(site: int, M: int) -> SparsePauliOp: s = SparsePauliOp([], M) for spin in (0, 1): idx = qubit_idx(site=site, spin=spin) s += (Pauli.I(idx) - Pauli.Z(idx)) * 0.5 return s def spin_density(site: int, M: int) -> SparsePauliOp: s_up, s_down = SparsePauliOp([], M), SparsePauliOp([], M) idx_up, idx_down = qubit_idx(site=site, spin=0), qubit_idx(site=site, spin=1) s_up += Pauli.Z(idx_up) s_down += Pauli.Z(idx_down) return (s_down - s_up) * 0.5 ``` Next, we execute the quantum program and measure the spin and change densities ```python theme={null} initial_charge_density = np.zeros(L) initial_spin_density = np.zeros(L) with ExecutionSession(qprog) as es: for site in range(L): initial_charge_density[site] = np.real( es.estimate(charge_density(site, M)).value ) initial_spin_density[site] = np.real(es.estimate(spin_density(site, M)).value) ``` ```python theme={null} sites = np.arange(0, L) plt.figure() plt.title("Initial Densities", fontsize=18) plt.plot(sites, initial_charge_density, "o", label="charge density") plt.plot(sites, initial_spin_density, "+", markersize=10, label="spin density") plt.xlabel("Site", fontsize=12) plt.ylabel("Density", fontsize=12) plt.xticks(sites) plt.legend() plt.show() ``` output The attractive potential is centered around `mean` = $1.5$, therefore the initial charge and spin densities form a peak around that site. ### Time-Evolution The propagation stage approximates the time-evolution operator $U = \exp(-i H t)$, where $$ H =-J \sum_{j, \sigma} \left ( c_{j,\sigma}^\dagger c_{j+1,\sigma} + c_{j,\sigma}^\dagger c_{j+1,\sigma}\right) + U \sum_{j} n_{j \uparrow} n_{j\downarrow} \tag{4} $$ is the 1D version of Eq. (1). The propagation of the initial state with respect to Eq. (4), simulates an effective quench of the system at initial time, abruptly changing the Hamiltonian $H_0\rightarrow H$. As a consequence, the initial state is a highly excited state of $H$. In order to minimize the circuit depth of the quantum circuit it is beneficial to decompose the Hamiltonian into four sets of terms, where all the operators in a set commute with one another. The terms correspond to an even and odd edge hopping terms and even and odd site interaction terms: $$ H = H_{\text{hop-even}} +H_{\text{hop-odd}} +H_{\text{int-even}}+ H_{\text{int-odd}} ~~. $$ The odd hopping Hamiltonian term includes the hopping terms between $1\leftrightarrow 2$ and $3\leftrightarrow 4$ neighbouring sites ($j$ is odd in Eq. (3)), while the odd hopping term includes the odd edge pairs ($2\leftrightarrow 3$, $4\leftrightarrow 1$, even $j$). The evolution operator $U$ is approximated by a first-order Trotter expansion, where each Trotter step consists of five stages. $$ U = e^{-i H t} \approx \left(e^{-i H \tau}\right)^{t/\tau}~~, $$ with $$ e^{-i H \tau} \approx e^{-i H_{\text{hop-even}}\tau} e^{-i H_{\text{int-even}}\tau} e^{-i H_{\text{int-odd}}\tau} e^{-i H_{\text{hop-odd}}\tau}. $$ Each component in the Trotter step can be achieved by simultaneous application (circuit depth of one) of simple two-qubit gates. An additional algorithmic trick simplifying the circuit and reducing the circuit depth even further. Between the even and odd interaction terms a fermionic mode swap operation is conducted, effectively performing a cyclic shifting of the fermionic modes. Crucially, this operation swaps the logical ordering of the qubits, while preserving the fermionic parity sign (using an iSWAP gates). Alternatively, the operation modifies the mapping between physical qubits and logical fermionic. As a result, the Hamiltonian terms are interpreted relative to the new ordering, effectively swapping the odd and even edge. The incorporation of the swap mode operation, allows parallelism, locality (only nearest neighbor gates, bypassing the need for long Jordan Wigner strings). After the application of $\exp(-i H_{\text{hop-even}}\tau)$ an inverse fermionic mode swap is needed to swap the even and odd edges back, allowing application of the consecutive Trotter step. Since the even edge hopping term commutes with the inverse fermionic mod swap, these operations are merged to a single stage. Overall, the time-evolution involves iteration (each iteration corresponds to a single Trotter step) of a five-step procedure: 1. Hopping operation on odd edges. 2. Interaction on odd sites 3. Fermionic mode swap 4. Interaction on even sites 5. Hopping operation on even sites + inverse fermionic mode swap Setting the Hamiltonian parameters ```python theme={null} U = 3 * J # J is set to unity in the beginning of the notebook ``` We begin by defining utility functions ```python theme={null} ## Elementary gates # K(theta) = exp(-i*(theta/2)*(XX + YY)) @qfunc def K(theta: float, qba: QArray[QBit, 2]): suzuki_trotter( pauli_operator=0.5 * (Pauli.X(0) * Pauli.X(1) + Pauli.Y(0) * Pauli.Y(1)), evolution_coefficient=theta, order=1, repetitions=1, qbv=qba, ) # P(phi) = CPHASE(phi) = diag(1, 1, 1, e^{-i*phi}) # Implements exp(-i*phi * n_up * n_down) up to global phase @qfunc def P(phi: float, target: QArray[QBit, 2]) -> None: phase(phi * target[0] * target[1]) ``` To propagate the system state we transform the fermion Hamilotonian to a qubit representation utilizing the Jordan-Wigner transformation. A single hopping term transforms as $$ c_{\mu}^{\dagger} c_{\nu} + c_{\mu}^{\dagger} c_{\nu} \xrightarrow{\text{JW}} \frac{1}{2} \left( X_{\mu}X_{\nu} + Y_{\mu}Y_{\nu} \right)~~, $$ while an interaction term gives $$ n_{\mu}n_{\nu} \xrightarrow{\text{JW}} = \frac{1}{4}\left(I -Z_\mu -Z_\nu + Z_\mu Z_\nu \right)~~. $$ As a consequence, the five stages can be performed by implementation of the basic unitaries $K(\theta)= \exp\left(-i \frac{\theta}{2}\left(XX + YY \right)\right)$ and $P(\phi) = \exp\left(-i \frac{\phi}{2}\left(I-Z_{\mu}-Z_{\nu}+Z_{\mu}Z_{\nu} \right)\right)$. The functions `K` and `P` above implement the corresponding unitary transformations. The first stage, including odd hopping terms, is obtained by simultaneous application of $K(\theta = -\tau J)$ on different paris of qubits. Similarly, the second and fourth stages, involving the odd and even interactions are achieved with simultaneous application of $P(\phi = \tau U/2)$ on the corresponding pairs of qubits. The third stage, constituting the fermionic mode swap, is implemented by simultaneous iSWAP gates which is equivalent to $K(\theta = -\pi/2)$. The last stage includes a merge of even-edge hopping operation, and an inverse fermionic mode swap operation. Naturally, since these operation commute, the transformation is obtained by $K(\theta = -\tau J + \pi/2)$ two-qubit gates. We implement the simultaneous operations on the qubit array utilizing Classiq's built-in `repeat` function within the `hop` and `interact` functions. ```python theme={null} def hop(pairs: list[tuple[int, int]], theta: float, qba: QArray[QBit, M]) -> None: for spin in (0, 1): for pair in pairs: site1, site2 = pair K( theta, [ qba[qubit_idx(site=site1, spin=spin)], qba[qubit_idx(site=site2, spin=spin)], ], ) def interact(sites: list[int], phi: float, qba: QArray[QBit, M]) -> None: for site in sites: P( phi, [qba[qubit_idx(site=site, spin=0)], qba[qubit_idx(site=site, spin=1)]], ) ``` Defining the odd and even pairs and sites, to account for the finite chane we evaluate which sites are really swapped. ```python theme={null} ODD_PAIRS = [(i, i + 1) for i in range(0, L - 1, 2)] EVEN_PAIRS = [(i, i + 1) for i in range(1, L - 1, 2)] ODD_SITES = list(range(0, L, 2)) EVEN_SITES = list(range(1, L, 2)) # After iSWAP on EVEN_PAIRS, compute where even sites end up _perm = list(range(L)) for i, j in EVEN_PAIRS: _perm[i], _perm[j] = _perm[j], _perm[i] _inv_perm = [0] * L for pos, site in enumerate(_perm): _inv_perm[site] = pos SWAPPED_EVEN_SITES = sorted([_inv_perm[s] for s in EVEN_SITES]) ``` Gathering all the stages together to form the Trotter step ```python theme={null} def trotter_step(tau: float, J: float, U: float, qba: QArray[QBit, M]) -> None: # stage 1 - hopping on odd-edge pairs hop(pairs=ODD_PAIRS, theta=-tau * J, qba=qba) # stage 2 - interaction on odd sites interact(sites=ODD_SITES, phi=tau * U, qba=qba) # stage 3 - fermionic mode swap hop(pairs=EVEN_PAIRS, theta=-np.pi / 2, qba=qba) # stage 4 - interaction on even sites (at their swapped positions) interact(sites=SWAPPED_EVEN_SITES, phi=tau * U, qba=qba) # stage 5 - hopping on even-edge pairs and inverse fermionic mode swap hop(pairs=EVEN_PAIRS, theta=-tau * J + np.pi / 2, qba=qba) ``` ### Propagation and Execution ("Measurement") ```python theme={null} @qfunc def main(qba: Output[QArray[QBit, M]], num_iter: CInt) -> None: allocate(M, qba) prepare_slater_det(h, Nelec, qba) power(num_iter, lambda: trotter_step(tau, J, U, qba)) qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3BhmprAOCkSPptCBxO3TT71oX0i ``` The physics of the model are analyzed by measuring the charge and spin densities and the spin spreads $\kappa^{\pm}(t)$, defined above. The number operators map to local Pauli operators $n_{\mu} = c^\dagger_{\mu}c_{\mu} = (1-Z_\mu)/2$, where $\mu=(j,\sigma)$ encodes a site, spin pair. ```python theme={null} def spread(density: list, L: int) -> float: s = 0 for site in range(L): s += np.abs(site - (L - 1) / 2) * density[site] return s ``` ```python theme={null} charge_density_mat = np.zeros((L, NUM_ITERS)) spin_density_mat = np.zeros((L, NUM_ITERS)) with ExecutionSession(qprog) as es: iter_params = [{"num_iter": iter} for iter in range(NUM_ITERS)] for site in range(L): charge_results = es.batch_estimate(charge_density(site, M), iter_params) spin_results = es.batch_estimate(spin_density(site, M), iter_params) charge_density_mat[site, :] = [np.real(r.value) for r in charge_results] spin_density_mat[site, :] = [np.real(r.value) for r in spin_results] ``` ```python theme={null} sites = np.arange(0, N) time_indices = [0, 4, 9] time_labels = [f"$t = {tau * i:.1f}/J$" for i in time_indices] fig, axes = plt.subplots(1, 3, figsize=(14, 4), sharey=True) for ax, t_idx, t_label in zip(axes, time_indices, time_labels): ax.plot(sites, charge_density_mat[:, t_idx], "o-", label=r"Charge $\rho^+$") ax.plot(sites, spin_density_mat[:, t_idx], "s--", label=r"Spin $\rho^-$") ax.set_title(t_label, fontsize=14) ax.set_xlabel("Site", fontsize=12) ax.set_xticks(sites) ax.legend(fontsize=9) axes[0].set_ylabel("Density", fontsize=12) plt.suptitle("Charge and Spin Density Dynamics", fontsize=16) plt.tight_layout() plt.show() ``` output ## Analysis - Spin-Charge Separation In order to quantify the spin-charge seperation, we evaluate the spin and charge spread: $$ \kappa^{\pm}(t) = \sum_{j=1}^N |j - j_c| \rho^{\pm}_j(t)~~, $$ where $j_c = (N+1)/2$. Different values between the spin and charge spreads, signifies spin-charge seperation. ```python theme={null} def spread(density: list, N: int) -> float: s = 0 for site in range(N): s += np.abs(site - (N + 1) / 2) * density[site] return s times = np.array([tau * i for i in range(NUM_ITERS)]) charge_spread_vec = np.array( [spread(charge_density_mat[:, i], L) for i in range(NUM_ITERS)] ) spin_spread_vec = np.array( [spread(spin_density_mat[:, i], L) for i in range(NUM_ITERS)] ) plt.figure(figsize=(7, 5)) plt.plot(times * J, charge_spread_vec, "o-", label=r"Charge spread $\kappa^+$") plt.plot(times * J, spin_spread_vec, "s--", label=r"Spin spread $\kappa^-$") plt.xlabel(r"$t \cdot J$", fontsize=14) plt.ylabel(r"Spread $\kappa$", fontsize=14) plt.title("Charge and Spin Spread vs. Time", fontsize=16) plt.legend(fontsize=12) plt.tight_layout() plt.show() ``` output The lack of agreement between the spin and charge spreads indicates spin-charge seperation. ## Comparison with Analytical Results In order to validate the results, we compare the model prediction with the analytical solutions. There are two natural physical limits, where the model can be solved exactly and efficiently utilizing classical methods: 1. The non-interacting limit the on-site interaction vanishes $U = 0$ (alternatively $U\ll J$). 2. Vanishing hopping limit, $J=0$ (alternatively $U\gg J$). ### Non-Interacting Particles In the non-interacting limit of $U=0$, the FH Hamiltonian is a quadratic Hamiltonian. As a consequence, even in the case of site-dependent hopping or hopping between non-adjacent sites, the state dynamics can be solved classically in polynomial time in the number of fermionic modes $M$, utilizing standard matrix exponentiation methods. In the case of the FH model on an open chain of $L$ sites, the non-interacting Hamiltonian becomes $$ H^{\text{n.i}} =\sum_{\mu, \nu} c_{\mu}^\dagger M_{\mu\nu}c_{\nu} = -J \sum_{j=1}^{L-1} \sum_{\sigma} \left ( c_{j,\sigma}^\dagger c_{j+1,\sigma} + c_{j+1,\sigma}^\dagger c_{j,\sigma}\right) ~~, $$ where n.i denotes "non-interacting". The commutativity of the spin-up and spin-down terms allows them to be treated independently. For an open chain (no periodic boundary), the eigenstates are standing waves rather than plane waves. The single-particle eigenstates are $$ \phi_{n}(j) = \sqrt{\frac{2}{L+1}}\sin\!\left(\frac{\pi n j}{L+1}\right)~~, \quad n=1,2,\dots,L~~, $$ with corresponding energies $$ \omega_n = -2J\cos\!\left(\frac{\pi n}{L+1}\right)~~. $$ Each eigenstate is doubly degenerate (spin-up and spin-down). The ground state of an $N_{\text{elec}}$-electron system is obtained by filling the lowest-energy single-particle states one by one. The time-dependent expectation values of the charge and spin densities, $\langle\rho^{\pm}_j(t)\rangle$, can readily obtained by employing the Heisenberg representation of quantum mechanics. In this representation, an operator's, $O(t)$, dynamics is generated by the Heisenberg equation $$ \frac{dO(t)}{dt} = i \left[ H^{\text{n.i}}, O(t) \right] ~~, $$ where $O(t) = U(t)^\dagger O U(t)$, where $U(t) = \exp(-i H^{\text{n.i}} t)$. Substituting the diagonal form of $H^{\text{n.i}}$ and utilizing the fermionic anti-commutation relations $$ \{c^\dagger_{\mu}, c_\nu\} = \delta_{\mu,\nu}~~, ~~\{c_{\mu}, c_\nu\} = \{c_{\mu}^\dagger, c_\nu^\dagger\} = 0~~, $$ (the Fourier fermionic operators satisfy similar commutation relations) we obtain $\dot\{c\}_\mu^\dagger = i M_\{\nu \mu\} c_\{ \nu\}^\dagger$, therefore $\dot\{\mathbf\{c\}\}^\dagger = i M^T \mathbf\{c\}^\dagger$, leading to $$ \mathbf{c}^\dagger(t) = e^{i M^T t}\mathbf{c}^\dagger(0) $$ The dynamics can be evaluated by introducing the matrices ${\cal U}(t) = e^{i M^T t}~~,$ and ${\cal N}$ with elements $\{\cal N_\{ \mu \nu\}\} = \langle c_\{\mu\}^\dagger c_\nu\rangle$, this allows writting the dynamics of a second order correlation as $$ \langle c_{\mu}^\dagger c_{\nu} \rangle = \left[ {\cal{U}} (t){\cal{N}} {\cal{U}}^\dagger (t)\right]_{\mu \nu}~~. $$ In the one-excitation subspace, the ground state reads $|\psi_{\text{g.s}} \rangle = \sum_{\mu} V_{0,\mu}c_{\mu}^\dagger|\text{vacc}\rangle$, where $\{V_{0\mu}\}$ are the elements of the eigenstate of $M$, i.e. $M V_0 = \epsilon_0 V_0$, where $\epsilon_0$ is the ground state energy. #### Numerical Evaluation on a Small Model. Defining parameters ```python theme={null} L = 4 # number of lattice sites M = 2 * L # total number of fermionic modes NUM_ITERS = 10 J = 1 # hopping strength tau = 0.1 / J # smaller Trotter step for better agreement with analytical U=0 result T = NUM_ITERS * tau U = 0 Nelec = 1 # single electron subspace ``` ```python theme={null} import matplotlib.pyplot as plt ## Check eigenvalues vs analytical dispersion (open chain) # For open BC, E_n = -2J cos(pi*n/(N+1)), n = 1..N M0 = construct_single_electron_hamiltonian(L, J=J, parameters=(0.0, 0, 1)) E, V = np.linalg.eigh(M0) n_vals = np.arange(1, N + 1) E_analytical = -2 * J * np.cos(np.pi * n_vals / (N + 1)) E_analytical_full = np.sort(np.tile(E_analytical, 2)) # doubly degenerate (spin) assert np.allclose( E, E_analytical_full ), f"Mismatch: numerical {E} vs analytical {E_analytical_full}" ## Single-electron dynamics: electron starts at site 0, spin-up psi_0 = np.zeros(2 * L) psi_0[qubit_idx(0, 0)] = 1.0 t_max = 4.0 / J t_vec = np.linspace(0, t_max, 400) # Numerical time evolution via eigendecomposition coeffs = V.conj().T @ psi_0 phases = np.exp(-1j * np.outer(t_vec, E)) psi_t = (phases * coeffs) @ V.conj().T spin_up_idx = np.array([qubit_idx(site, 0) for site in range(N)]) occ_numerical = np.abs(psi_t[:, spin_up_idx]) ** 2 # Analytical: standing waves on the open chain # phi_n(j) = sqrt(2/(L+1)) * sin(pi*n*j/(L+1)), j = 1..L (1-indexed) # = sum_n phi_n(j) * phi_n(1) * e^{-i E_n t} sites_1indexed = np.arange(1, N + 1) phi = np.sqrt(2 / (N + 1)) * np.sin( np.pi * np.outer(n_vals, sites_1indexed) / (N + 1) ) # (N, N): phi[n, j] phi_at_start = phi[:, 0] # phi_n(j=1), the starting site phases_analytical = np.exp(-1j * np.outer(t_vec, E_analytical)) psi_analytical = (phases_analytical * phi_at_start) @ phi occ_analytical = np.abs(psi_analytical) ** 2 # Plot fig, axes = plt.subplots(2, 2, figsize=(8, 5), sharey=True) for site in range(L): ax = axes[site // 2, site % 2] ax.plot(t_vec, occ_numerical[:, site], label="Numerical") ax.plot(t_vec, occ_analytical[:, site], "--", label="Analytical") ax.set_title(f"Site {site}") ax.set_xlabel("$t$") if site % 2 == 0: ax.set_ylabel("Occupation") ax.legend(fontsize=7) plt.suptitle( r"Single electron dynamics (open chain): $|\psi(0)\rangle = |j{=}0,\uparrow\rangle$" ) plt.tight_layout() plt.show() ``` output ```python theme={null} NUM_ITERS = 10 # more steps for better resolution of Trotter dynamics tau_trotter = 0.1 / J # time step for Trotter evolution n_steps_list = list(range(0, NUM_ITERS + 1, 1)) t_trotter = np.array(n_steps_list) * tau_trotter t_max = t_trotter[-1] t_vec = np.linspace(0, t_max, 400) # Numerical time evolution via eigendecomposition coeffs = V.conj().T @ psi_0 phases = np.exp(-1j * np.outer(t_vec, E)) psi_t = (phases * coeffs) @ V.conj().T spin_up_idx = np.array([qubit_idx(site, 0) for site in range(N)]) occ_numerical = np.abs(psi_t[:, spin_up_idx]) ** 2 ## Trotter dynamics via Classiq # Initial state: single electron at site 0, spin-up h_loc = np.zeros((2 * N, 2 * N)) h_loc[qubit_idx(0, 0), qubit_idx(0, 0)] = -1.0 @qfunc def main(qba: Output[QArray[QBit, M]], num_iter: CInt) -> None: allocate(M, qba) prepare_slater_det(h_loc, Nelec, qba) power(num_iter, lambda: trotter_step(tau_trotter, J, 0, qba)) qprog = synthesize(main) occ_trotter = np.zeros((len(n_steps_list), N)) def spin_up_occ(site: int, M: int) -> SparsePauliOp: s = SparsePauliOp([], M) idx = qubit_idx(site=site, spin=0) s += (Pauli.I(idx) - Pauli.Z(idx)) * 0.5 return s with ExecutionSession(qprog) as es: step_params = [{"num_iter": n_step} for n_step in n_steps_list] for site in range(L): results = es.batch_estimate(spin_up_occ(site, M), step_params) occ_trotter[:, site] = [np.real(r.value) for r in results] ## Plot: exact vs Trotter fig, axes = plt.subplots(2, 2, figsize=(8, 5), sharey=True) for site in range(L): ax = axes[site // 2, site % 2] ax.plot(t_vec, occ_numerical[:, site], label="Exact") ax.plot( t_trotter, occ_trotter[:, site], "o--", ms=3, label=f"Trotter ($\\tau={tau_trotter:.2f}$)", ) ax.set_title(f"Site {site}") ax.set_xlabel("$t$") if site % 2 == 0: ax.set_ylabel("Occupation") ax.legend(fontsize=7) plt.suptitle( r"Exact vs Trotter ($U=0$, open chain): $|\psi(0)\rangle = |j{=}0,\uparrow\rangle$" ) plt.tight_layout() plt.show() ``` output **Why the quantum and analytical results don't match exactly:** The quantum circuit uses a first-order Trotter decomposition of the time evolution. Even for $U=0$, the odd- and even-edge hopping terms do not commute, so each step approximates $\exp(-i H \tau)$ and Trotter error accumulates (roughly $\propto \tau^2$ per step). For a closer match, use a smaller Trotter step, while keeping the total propagation time. ### Vanishing Hopping Limit The other extreme is obtained when the onsite interactions is much larger than the hopping constant. In this regime interaction dominate the physics and supress hopping between adjacent sites, allowing us to take $J=0$. The FH Hamiltonian then becomes a sum of local commuting terms, diagonal in the Fock basis (eigenstates of the number operators $\{n_{\mu}\}$). The Hamiltonian reduces to $$ H^{\text{n.h}} = U \sum_{j} n_{j \uparrow} n_{j\downarrow}~~, $$ where n.h denotes "no hopping". As a consequence the local number operators are a constant of motion $\langle n_{j} (t)\rangle = \langle n_{j} (0)\rangle$, and we expect the charges to freeze in time. Since all terms in $H^{\text{n.h}}$ commute with one another, i.e., $[n_{i\uparrow}n_{i\downarrow},\, n_{j\uparrow}n_{j\downarrow}] = 0$ for all $i,j$, the first-order Trotter decomposition is **exact**: $$ e^{-i H^{\text{n.h}} \tau} = \prod_j e^{-i U \tau\, n_{j\uparrow}n_{j\downarrow}}~~. $$ This means the Trotter circuit reproduces the exact time evolution with no approximation error, regardless of the step size $\tau$. To verify this, we prepare an initial state that is **not** an eigenstate of $H^{\text{n.h}}$ - specifically, the ground state of the non-interacting Hamiltonian with an attractive potential (a Slater determinant with non-uniform charge distribution). We then compare the Trotter dynamics with the exact analytical prediction: frozen charge and spin densities. # ### Numerical Evaluation ```python theme={null} # Parameters N = 4 # number of lattice sites L = N * a # length of the lattice M = 2 * N # total number of fermionic modes J = 0 # vanishing hopping U = 2.0 # on-site interaction strength Nelec_vh = N # half-filling NUM_ITERS_vh = 10 tau_vh = 0.3 # deliberately large Trotter step — still exact since terms commute T = NUM_ITERS_vh * tau_vh # Initial state: ground state of the non-interacting Hamiltonian with attractive potential # This is NOT an eigenstate of H^{n.h.}, making the test non-trivial lam = 10.0 mean = (L - 1) / 2 std = L / 6 h_vh = construct_single_electron_hamiltonian(N, J=1, parameters=(lam, mean, std)) @qfunc def main(qba: Output[QArray[QBit, M]], num_iter: CInt) -> None: allocate(M, qba) prepare_slater_det(h_vh, Nelec_vh, qba) power(num_iter, lambda: trotter_step(tau=tau_vh, J=J, U=U, qba=qba)) qprog_vh = synthesize(main) ``` ```python theme={null} # Measure initial charge and spin densities (at num_iter=0), # then measure at each Trotter step n_steps_vh = list(range(0, NUM_ITERS_vh + 1)) t_trotter_vh = np.array(n_steps_vh) * tau_vh charge_density_vh = np.zeros((N, len(n_steps_vh))) spin_density_vh = np.zeros((N, len(n_steps_vh))) with ExecutionSession(qprog_vh) as es: step_params = [{"num_iter": n_step} for n_step in n_steps_vh] for site in range(N): charge_results = es.batch_estimate(charge_density(site, M), step_params) spin_results = es.batch_estimate(spin_density(site, M), step_params) charge_density_vh[site, :] = [np.real(r.value) for r in charge_results] spin_density_vh[site, :] = [np.real(r.value) for r in spin_results] ``` ```python theme={null} # Exact analytical result: charge and spin densities are constants of motion # for J=0, so the exact values at all times equal the initial (t=0) values. initial_charge_vh = charge_density_vh[:, 0] initial_spin_vh = spin_density_vh[:, 0] # Verify Trotter matches exact solution. # Note: es.estimate() uses finite-shot sampling, so we allow for statistical noise. charge_error = np.max(np.abs(charge_density_vh - initial_charge_vh[:, None])) spin_error = np.max(np.abs(spin_density_vh - initial_spin_vh[:, None])) print(f"Max charge density deviation from exact: {charge_error:.2e}") print(f"Max spin density deviation from exact: {spin_error:.2e}") assert charge_error < 0.2, f"Charge density deviation too large: {charge_error}" assert spin_error < 0.2, f"Spin density deviation too large: {spin_error}" ``` **Output:** ``` Max charge density deviation from exact: 4.25e-02 Max spin density deviation from exact: 3.32e-02 ``` ```python theme={null} fig, axes = plt.subplots(1, 2, figsize=(12, 4)) sites = np.arange(N) # Charge density: final Trotter step vs exact (initial = frozen) ax = axes[0] ax.plot( sites, charge_density_vh[:, -1], "o", label=f"Trotter ($t = {t_trotter_vh[-1]:.1f}$)", ) ax.plot(sites, initial_charge_vh, "x", ms=10, label="Exact (frozen)") ax.set_title("Charge Density ($J=0$)", fontsize=14) ax.set_xlabel("Site", fontsize=12) ax.set_ylabel("$\\rho^+_j$", fontsize=12) ax.set_xticks(sites) ax.legend() # Spin density: final Trotter step vs exact (initial = frozen) ax = axes[1] ax.plot( sites, spin_density_vh[:, -1], "o", label=f"Trotter ($t = {t_trotter_vh[-1]:.1f}$)" ) ax.plot(sites, initial_spin_vh, "x", ms=10, label="Exact (frozen)") ax.set_title("Spin Density ($J=0$)", fontsize=14) ax.set_xlabel("Site", fontsize=12) ax.set_ylabel("$\\rho^-_j$", fontsize=12) ax.set_xticks(sites) ax.legend() plt.suptitle( "Vanishing hopping limit: Trotter vs Exact at final time\n" "Trotter is exact since all terms commute", fontsize=12, ) plt.tight_layout() plt.show() ``` output ## References \[1] [Arute, F., Arya, K., Babbush, R., Bacon, D., Bardin, J. C., Barends, R., ... & Zanker, S. (2020). Observation of separated dynamics of charge and spin in the Fermi-Hubbard model. arXiv:2010.07965](https://arxiv.org/abs/2010.07965). \[2] [Lieb, E. H., & Wu, F. Y. (2003). The one-dimensional Hubbard model: a reminiscence. Physica A: statistical mechanics and its applications, 321(1-2), 1-27. arXiv:0207529](https://arxiv.org/abs/cond-mat/0207529) \[3] [Jiang, Z., Sung, K. J., Kechedzhi, K., Smelyanskiy, V. N., & Boixo, S. (2018). Quantum algorithms to simulate many-body physics of correlated fermions. Physical Review Applied, 9(4), 044036](https://arxiv.org/abs/1711.05395). # Solve Differential Equations of the Lanchester Model with HHL Source: https://docs.classiq.io/explore/applications/physical_systems/hhl_lanchester/hhl_lanchester Open this notebook in GitHub to run it yourself ## Welcome to the Jungle of HHL * Rabbits vs. Foxes The Lanchester model is widely used to describe the dynamics of combat between two opposing forces. Originally formulated for military applications, this model can also be adapted to other contexts, such as ecological competition between two species or market competition between two businesses. The model uses linear differential equations to capture the interactions between the populations, making it suitable for scenarios where interaction terms are purely linear. In this notebook, we explore solving this modified Lanchester model using the Harrow-Hassidim-Lloyd (HHL) algorithm, a quantum algorithm designed for efficiently solving linear systems of equations. ## Lanchester Model [Lanchester Model](https://community.wolfram.com/groups/-/m/t/3055705) is described in this link. This differential equation describes the behavior of two forces, $x$ and $y$: $$ \frac{d x}{d t} = a x + b x y + cy +d $$ $$ \frac{d y}{d t} = e y + f x y + g x +h $$ where the coefficients described in the next sections indicate the sensitivities of each side to the other and to itself. ## Describing the Classical Model Using the [Finite Difference Method](https://en.wikipedia.org/wiki/Finite_difference_method) Assuming $b=0$, $f=0$: $$ x_{i+1} = dt \cdot a x_i + dt\cdot c y_i + dt\cdot d + x_i = (dt\cdot a+1) x_i+ dt\cdot c y_i + dt\cdot d $$ $$ y_{i+1} = dt\cdot e y_i + dt\cdot g x_i + dt\cdot h + y_i = (dt\cdot e+1) y_i + dt \cdot g x_i + dt\cdot h~~, $$ where the dot product was added explicitly to emphasize that $dt$ is a variable. The first sample is the initial condition: $$ x_0 = X_0 $$ $$ y_0 = Y_0 $$ Then, for any next point, we solve numerically using the previous point: $$ - (dt\cdot a+1) x_i - dt \cdot c y_i + x_{i+1} = dt\cdot d $$ $$ - (dt\cdot e+1) y_i - dt\cdot g x_i + y_{i+1} = dt\cdot h $$ The system of linear equations describing the model looks like this:
hhl_jungle_matrix.png
## Building the Matrix with NumPy ```python theme={null} import numpy as np def diff_eq_model(a, b, c0, d, e, f, g, h, dt, N, x0, y0): assert b == 0, "model is currently unsupported" assert f == 0, "model is currently unsupported" A = np.identity(2 * N) for r in range(2 * N): if 1 <= r <= N - 1: c = r - 1 A[r][c] = -(dt * a + 1) c = N + r - 1 A[r][c] = -dt * c0 elif N + 1 <= r: c = r - 1 A[r][c] = -(dt * e + 1) c = r - 1 - N A[r][c] = -dt * g b1 = np.ones((N, 1)) * dt * d b1[0] = x0 b2 = np.ones((N, 1)) * dt * h b2[0] = y0 b = np.concatenate([b1, b2]) return A, b ``` We choose specific values for the matrix, representing a predator-prey system. Here, $x$ represents the prey population (e.g., rabbits), and $y$ represents the predator population (e.g., foxes). The coefficients have the following meanings and specific values: * $a$ and $e$ define the rate of natural losses or birth (due to death, disease, etc.). For example, $a = -0.01$ (1% natural death rate for rabbits), $e = -0.02$ ($2%$ natural death rate for foxes). * $b$ and $f$ define the rate of losses due to environmental factors (affecting both species). For example, $b = 0$, $f = 0$ (assuming no such environmental exposure for simplicity). * $c$ and $g$ are losses or gains due to interactions between species (prey hunted by predators and vice versa). For example, $c = -0.1$ ($10%$ loss of rabbits due to predation), $g = 0.2$ ($20%$ increase in predator population due to hunting prey). * $d$ and $h$ are gains due to migration. For example, $d = 0.4$ ($0.4 K$ Rabbits/year migration rate for rabbits), $h = 0.01$ ($0.01K$ Foxes/year migration rate for foxes). ```python theme={null} N = 8 # N time steps dt = 6 # Sample every dt years A, b = diff_eq_model( a=-0.01, b=0, c0=-0.1, d=0.4, e=-0.02, f=0, g=0.02, h=0.01, dt=dt, N=N, x0=30, y0=1 ) ``` ```python theme={null} import matplotlib.pyplot as plt plt.matshow(A) plt.title("Matrix A") plt.show() plt.matshow(b.transpose()) plt.title("Vector b") plt.show() ``` output output ## Classical Solution ```python theme={null} x = np.matmul(np.linalg.inv(A), b) ``` ```python theme={null} plt.matshow(x.transpose()) plt.title("Solution x") plt.show() ``` output ```python theme={null} import matplotlib.pyplot as plt t = dt * np.array(range(N)) x_sol = x[0:N] y_sol = x[N : 2 * N] plt.plot(t, x_sol, label="Rabbits") plt.plot(t, y_sol, label="Foxes") plt.xlabel("t [years]") plt.ylabel("Population [Thosands of individuals]") plt.legend() plt.show() ``` output Note that at some critical point, the rabbit population is so low that the fox population also starts to decrease. This is typical predator-prey model behavior. ## Redefining the Matrix The matrix of HHL is a canonical one, assuming the following properties: 1. The RHS vector $\vec{b}$ is normalized. 2. The matrix $A$ is a Hermitian one. 3. The matrix $A$ is of size $2^n\times 2^n $. 4. The eigenvalues of $A$ are in the range $(0,1)$. However, any general problem that does not follow these conditions can be resolved, as below. # ## 1) Normalized b As preprocessing, normalize $\vec{b}$ and then return the normalization factor as postprocessing. ```python theme={null} norm_factor = np.linalg.norm(b) b_normalized = b / norm_factor ``` # ## 2) Hermitian Matrix Symmetrize the problem: $$ \begin{pmatrix} 0 & A^T \\ A & 0 \end{pmatrix} \begin{pmatrix} \vec{x} \\ 0 \end{pmatrix} = \begin{pmatrix} 0 \\ \vec{b} \end{pmatrix}. $$ This increases the number of qubits by 1. ```python theme={null} def to_hermitian(A): N = A.shape[0] A_hermitian = np.concatenate( [ np.concatenate([np.zeros((N, N)), A.transpose().conj()], axis=1), np.concatenate([A, np.zeros((N, N))], axis=1), ] ) return A_hermitian b_new = np.concatenate([np.zeros((2 * N, 1)), b_normalized]) plt.matshow(b_new.transpose()) plt.title("Normalized and Padded Vector b") plt.show() A_hermitian = to_hermitian(A) plt.matshow(A_hermitian) plt.title("Hermitian Matrix A") plt.show() ``` output output # ## 3) Make the Matrix $A$ of Size $2^n\times 2^n $ Complete the matrix dimension to the closest $2^n$ with an identity matrix. The vector $\vec{b}$ is completed with zeros. $$ \begin{pmatrix} A & 0 \\ 0 & I \end{pmatrix} \begin{pmatrix} \vec{b} \\ 0 \end{pmatrix} = \begin{pmatrix} \vec{x} \\ 0 \end{pmatrix}. $$ However, our matrix is already the right size. # ## 4) Rescaled Matrix If the eigenvalues of $A$ are in the range $[w_{\min},w_{\max}]$, we can employ transformations to the exponentiated matrix and then undo them to extract the results: $$ \tilde{A}=(A-w_{\min}I)\left(1-\frac{1}{2^{m}}\right)\frac{1}{w_{\max}-w_{\min}}. $$ The eigenvalues of this matrix lie in the interval $[0,1)$, and are related to the eigenvalues of the original matrix via $$ \lambda = (w_{\max}+w_{\min})\tilde{\lambda}\left[1/\left(1-\frac{1}{2^{m}}\right)\right]+w_{\min}, $$ with $\tilde{\lambda}$ being an eigenvalue of $\tilde{A}$ resulting from the QPE algorithm. This relation between eigenvalues is then used for the expression inserted into the eigenvalue inversion, via the `AmplitudeLoading` function. ```python theme={null} def condition_number(A): w, _ = np.linalg.eig(A) return max(np.abs(w)) / min(np.abs(w)) QPE_RESOLUTION_SIZE = 6 assert QPE_RESOLUTION_SIZE > np.log2( condition_number(A) ), "condition number is too big, and QPE resolution cannot hold all eigenvalues" w, v = np.linalg.eigh(A_hermitian) w_max = np.max(w) w_min = np.min(w) mat_shift = -w_min mat_rescaling = (1 - 1 / 2**QPE_RESOLUTION_SIZE) / ( w_max - w_min ) # assures eigenvalues in [0,1-1/2^QPE_SIZE] min_possible_w = ( w_max - w_min ) / 2**QPE_RESOLUTION_SIZE # this is the minimal eigenvalue which can be resolved by the QPE A_rescaled = ( A_hermitian + mat_shift * np.identity(A_hermitian.shape[0]) ) * mat_rescaling # verifying that the matrix is symmetric and has eigenvalues in [0,1) if not np.allclose(A_rescaled, A_rescaled.T, rtol=1e-6, atol=1e-6): raise Exception("The matrix is not symmetric") w_rescaled, _ = np.linalg.eigh(A_rescaled) for lam in w_rescaled: if lam < -1e-6 or lam >= 1: raise Exception("Eigenvalues are not in (0,1)") plt.matshow(A_rescaled) plt.title("Rescaled Matrix A") plt.show() ``` output ## Defining HHL Algorithm for the Quantum Solution This section is based on Classiq HHL in the user guide, [here](https://github.com/Classiq/classiq-library/blob/main/tutorials/technology_demonstrations/hhl/hhl_example.ipynb) and [here](https://github.com/Classiq/classiq-library/blob/main/algorithms/quantum_linear_solvers/hhl/hhl.ipynb). Note the rescaling in `simple_eig_inv` based on the matrix rescaling. image.png ```python theme={null} from classiq import * @qfunc def simple_eig_inv( gamma: float, delta: float, c_param: float, phase: QNum, indicator: Output[QBit], ): allocate(indicator) assign_amplitude_table( lookup_table(lambda p: np.clip(c_param / ((gamma * p) + delta), -1, 1), phase), phase, indicator, ) ``` ```python theme={null} sol_classical_hermitian = np.array([s[0] for s in np.linalg.solve(A_hermitian, b_new)]) compared_sol = sol_classical_hermitian * min_possible_w amp_compared = compared_sol / np.linalg.norm(compared_sol) ``` Besides HHL, we also perform [swap test as in the user guide](https://github.com/Classiq/classiq-library/blob/main/algorithms/quantum_primitives/swap_test/swap_test.ipynb), comparing the HHL solution to a state preparation of the known solution. ```python theme={null} import scipy exponentiation_A_rescaled = scipy.linalg.expm(1j * 2 * np.pi * A_rescaled).tolist() b_list = np.concatenate(b_new).tolist() amp_compared_list = amp_compared.tolist() @qfunc def main(indicator: Output[QBit], test: Output[QBit]) -> None: state = QArray() compared_state = QArray() rescaled_eig = QNum() allocate(QPE_RESOLUTION_SIZE, UNSIGNED, QPE_RESOLUTION_SIZE, rescaled_eig) prepare_amplitudes(b_list, 0, state) within_apply( lambda: qpe( unitary=lambda: unitary(exponentiation_A_rescaled, state), phase=rescaled_eig, ), lambda: simple_eig_inv( gamma=mat_rescaling ** (-1), delta=-mat_shift, c_param=min_possible_w, phase=rescaled_eig, indicator=indicator, ), ) prepare_amplitudes(amp_compared_list, 0.0, compared_state) swap_test(state, compared_state, test) drop(rescaled_eig) drop(state) drop(compared_state) ``` We create the circuit with depth limitation compared to the maximal width in a simulator (25). We also set the preferences for synthesis and execution. ```python theme={null} MAX_WIDTH_SWAP_TEST = 25 constraints = Constraints(max_width=MAX_WIDTH_SWAP_TEST) preferences = Preferences( optimization_level=0, optimization_timeout_seconds=90, transpilation_option="none" ) NUM_SHOTS = 30000 execution_preferences = ExecutionPreferences(num_shots=NUM_SHOTS) qmod_hhl_swap_test = create_model(main, constraints, execution_preferences, preferences) ``` # ## Synthesize and Show ```python theme={null} qprog_hhl_swap = synthesize(qmod_hhl_swap_test) show(qprog_hhl_swap) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/35tN4TKVCACNL14VMInDJb2alDZ ``` # ## Execution and Results Analysis The analysis is explained in the [swap test user guide](https://github.com/Classiq/classiq-library/blob/main/algorithms/quantum_primitives/swap_test/swap_test.ipynb). We compare the measured overlap with the exact overlap using the expected probability of measuring the state $|0\rangle$, defined as $$ \alpha^2 = \frac{1}{2}\left(1+|\langle \psi_1 |\psi_2 \rangle |^2\right). $$ We extract the overlap $|\langle \psi_1 |\psi_2 \rangle |^2=\sqrt{2 P\left(q_{\text{test}}=|0\rangle\right)-1}$. The exact overlap is computed with the dot product of the two state vectors. Note that for the sake of this demonstration we execute this circuit $100,000$ times to improve the precision of the probability estimate. This is usually not required. Since we are in the HHL context, we filter only the `indicator==1` results. ```python theme={null} execution_job_id = execute(qprog_hhl_swap) ``` ```python theme={null} result = execution_job_id.result_value() fidelity_basic = np.sqrt( result.counts_of_multiple_outputs(["indicator", "test"])[("1", "0")] * 2 / (result.counts_of_output("indicator")["1"]) - 1 ) print("Fidelity between basic HHL and classical solutions:", fidelity_basic) ``` **Output:** ``` Fidelity between basic HHL and classical solutions: 0.9526849928765404 ``` In the IDE we can also check that among the `indicator=1` results, the bar of `test=0` is much higher than `test=1`. ```python theme={null} execution_job_id.open_in_ide() ``` ## State Vector Simulation We can also run the state vector of the HHL result and examine it without a swap test. Extracting the full information is exponentially hard, and used here just for educational purposes. Extension of this work can extract some information from the state vector, such as the last element, which is the most important. We can also pad the last solution with an extension, and therefore measure the last point with high probability. ```python theme={null} class TimeIndexAndGroup(QStruct): rabbits: QBit time_index: QNum @qfunc def main( indicator: Output[QBit], time_index_and_group: Output[TimeIndexAndGroup], rescaled_eig: Output[QNum], ) -> None: allocate(QPE_RESOLUTION_SIZE, False, QPE_RESOLUTION_SIZE, rescaled_eig) prepare_amplitudes(b_list, 0, time_index_and_group) within_apply( lambda: qpe( unitary=lambda: unitary(exponentiation_A_rescaled, time_index_and_group), phase=rescaled_eig, ), lambda: simple_eig_inv( gamma=mat_rescaling ** (-1), delta=-mat_shift, c_param=min_possible_w, phase=rescaled_eig, indicator=indicator, ), ) MAX_WIDTH_BASIC = 18 constraints = Constraints(max_width=MAX_WIDTH_BASIC) preferences = Preferences( optimization_level=0, optimization_timeout_seconds=90, transpilation_option="none" ) backend_preferences = ClassiqBackendPreferences( backend_name=ClassiqSimulatorBackendNames.SIMULATOR_STATEVECTOR ) execution_preferences = ExecutionPreferences( num_shots=1, backend_preferences=backend_preferences ) qmod_hhl_basic = create_model( main, constraints=constraints, preferences=preferences, execution_preferences=execution_preferences, ) qprog_hhl_basic = synthesize(qmod_hhl_basic) show(qprog_hhl_basic) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/35tNJgp5JBnQjy397u9i5ZqNFvs ``` ```python theme={null} execution_job_id = execute(qprog_hhl_basic) result = execution_job_id.result_value() # statevector = result.state_vector execution_job_id.open_in_ide() ``` We filter the results, then extract the amplitudes of the state vector and compare them to the classical solution. We filter only the `indicator=1` and `phase=0` results. ```python theme={null} def time_index_and_group_value(time_index_and_group): if isinstance(time_index_and_group, dict): return int(time_index_and_group["time_index"]) * 2 + int( time_index_and_group["rabbits"] ) return time_index_and_group filtered_hhl_statevector = dict() for sample in result.parsed_state_vector: if sample.state["indicator"] == 1 and sample.state["rescaled_eig"] == 0: filtered_hhl_statevector[ time_index_and_group_value(sample.state["time_index_and_group"]) ] = sample.amplitude states = sorted(filtered_hhl_statevector) raw_qsol = np.array([filtered_hhl_statevector[s] for s in states]) ``` Let's compare the raw solution to the Hermitian matrix with the classical one. ```python theme={null} qsol_hermitian = raw_qsol / (min_possible_w) plt.plot(np.real(states), np.real(qsol_hermitian)) plt.plot(np.real(states), np.real(sol_classical_hermitian)) plt.xlabel("states") plt.ylabel("amplitude") plt.legend(["hhl result", "original result"]) plt.show() ``` output Now we can compare the two solutions in the time domain, after putting back the normalization factor. ```python theme={null} plt.plot(t, norm_factor * np.real(qsol_hermitian[0:N]), label="Rabbits HHL") plt.plot( t, norm_factor * np.real(sol_classical_hermitian[0:N]), label="Rabbits Classical" ) plt.plot(t, norm_factor * np.real(qsol_hermitian[N : 2 * N]), label="Foxes HHL") plt.plot( t, norm_factor * np.real(sol_classical_hermitian[N : 2 * N]), label="Foxes Classical", ) plt.xlabel("time [years]") plt.ylabel("Population [Thousands of individuals]") plt.legend() plt.show() ``` output The fidelity of the state vector solution can be calculated as the overlap of the two solutions. ```python theme={null} fidelity = ( np.abs( np.dot( sol_classical_hermitian / np.linalg.norm(sol_classical_hermitian), qsol_hermitian / np.linalg.norm(qsol_hermitian), ) ) ** 2 ) print("Statevector Solution Fidelity:", fidelity) ``` **Output:** ``` Statevector Solution Fidelity: 0.9996462667876236 ``` # Ising Model Source: https://docs.classiq.io/explore/applications/physical_systems/ising_model/ising_model Open this notebook in GitHub to run it yourself The Ising model is an important physical model, which manifest in various applications. Originally, the model was formulated to reflect interactions between magnetic dipole moments of atomic spins in lattices but since, it was found relevant to describe many systems, such as genetic markers\[1], superconducting layered compounds\[2], Majorana fermions\[3], voter model\[4]. The Ising model is essentially a second order interaction Hamiltonian, which describes the state of the system according to the Hamiltonian ground state and thermodynamical properties. As such, the Ising model is also an example of Quadratic unconstrained binary optimization (QUBO) and thus can be mapped to solve NP hard problem. For example, one can equivalently formulated a graph maximum cut (Max-Cut) to an Ising model. Using Classiq's platform, we hereby demonstrate how to formulate the Ising model into an optimization problem, which will be sent to solution using Classiq's quantum approximated optimization algorithm (QAOA). The results of the model are obtained using execution of a quantum simulator, and can be similarly redirected to execution using other simulators or even quantum hardware (see \[5] for execution options). ## 0. Pre-Requirments The model is using several Classiq's libraries in addition to basic python tools. ```python theme={null} import numpy as np import pyomo.core as pyo ``` ## 1. Define the Optimization Problem We created a python Pyomo model to describe an Ising model as optimization problem over the configuration of spins. We take a simple manifestation of the 1d Ising model, described as \[6]: $H(\sigma) = -\sum\limits _{i,j}J\sigma_{i}\sigma_{j}-\sum\limits _{i} h\sigma_{i}$ where for any two adjacent sites $i, j$ there is an interaction $J$, and for any site $i$ there is a contribution of magnetic field $h$. Here, $\sigma$ represent the spin's value, it is discrete and can have value within the set of ${-1,1}$. We use transformation of $\sigma \rightarrow (2*z-1)$, where $z$ is a binary variable. An option for periodic boundary condition for the 1d spin structure is embedded in the model, which declares whether the first and last spins are interacting in the model (i.e., whether the spin configuration is of closed chain, or open line). ```python theme={null} def ising_model_1d(J: int, h: int, n: int, periodic: str) -> pyo.ConcreteModel: model = pyo.ConcreteModel("ising") # Define the variables: model.z = pyo.Var(range(n), domain=pyo.Binary) z_array = np.array(list(model.z.values())) E = lambda i, j: -J * (2 * z_array[i] - 1) * (2 * z_array[j] - 1) - (h / 2) * ( (2 * z_array[i] - 1) + (2 * z_array[j] - 1) ) # create the ising Hamiltonian if periodic == "True": model.H = E(0, n - 1) if periodic == "False": model.H = -(h / 2) * ((2 * z_array[0] - 1) + (2 * z_array[n - 1] - 1)) for i in range(n - 1): model.H = model.H + E(i, i + 1) # setting the objective: model.cost = pyo.Objective(expr=model.H, sense=pyo.minimize) return model ``` ## 2. Create Your Ising Model The user choses what parameters of interaction coupling $J$ and magnetic field $h$ to insert, in addition to whether the boundry conditions are periodic. ```python theme={null} ising_model = ising_model_1d(J=10, h=-20, n=6, periodic="True") ``` ## 3. Optimize Using to Quantum Optimization Algorithm We will now create a QAOA model for the optimization problem. The results of the model is the sequance of qubit values giving the minimized energy for the protein. In order to optimize the results, we recommend the user to explore the number of repatitions for the model (`num_layers`) and the number of iterations for the optimizer (`maxiter`). ```python theme={null} from classiq import * from classiq.applications.combinatorial_optimization import CombinatorialProblem combi = CombinatorialProblem(pyo_model=ising_model, num_layers=5, penalty_factor=10) qmod = combi.get_model() ``` Now we can create a quantum circuit using the `get_qprog` command and show it ```python theme={null} qprog = combi.get_qprog() show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/38wB1SQoSqgeLy1c9bu4u73zuhZ ``` **Output:** ``` https://platform.classiq.io/circuit/38wB1SQoSqgeLy1c9bu4u73zuhZ?login=True&version=15 ``` We also set the quantum backend we want to execute on: ```python theme={null} from classiq.execution import * execution_preferences = ExecutionPreferences( backend_preferences=ClassiqBackendPreferences(backend_name="simulator") ) ``` We now solve the problem by calling the `optimize` function: ```python theme={null} optimized_params = combi.optimize(execution_preferences, maxiter=100, quantile=0.7) ``` We can check the convergence of the run: ```python theme={null} import matplotlib.pyplot as plt plt.plot(combi.cost_trace) plt.xlabel("Iterations") plt.ylabel("Cost") plt.title("Cost convergence") ``` **Output:** ``` Text(0.5, 1.0, 'Cost convergence') ``` output ## 4. Present Quantum Results We call the `sample` method to get samples with the optimzied parameters. We hereby present the optimization results. Since this is a quantum solution with probabilistic results, there is a defined probability for each result to be obtained by a measurement (presented by an histogram), where the solution is chosen to be the most probable one. We remind that in the notation of the solution "0" indicate "-1" spin value, and "1" indicates "1" spin value. ```python theme={null} optimization_result = combi.sample(combi.optimized_params) optimization_result.sort_values(by="cost").head(5) ``` | | solution | probability | cost | | -- | --------------------------- | ----------- | ------ | | 0 | \{'z': \[0, 0, 0, 0, 0, 0]} | 0.745117 | -180.0 | | 17 | \{'z': \[0, 1, 0, 0, 0, 0]} | 0.004883 | -100.0 | | 16 | \{'z': \[0, 0, 1, 0, 0, 0]} | 0.004883 | -100.0 | | 15 | \{'z': \[0, 0, 0, 0, 0, 1]} | 0.005371 | -100.0 | | 13 | \{'z': \[1, 0, 0, 0, 0, 0]} | 0.007324 | -100.0 | We will also want to compare the optimized results to uniformly sampled results: ```python theme={null} uniform_result = combi.sample_uniform() ``` And compare the histograms: ```python theme={null} optimization_result["cost"].plot( kind="hist", bins=30, edgecolor="black", weights=optimization_result["probability"], alpha=0.6, label="optimized", ) uniform_result["cost"].plot( kind="hist", bins=30, edgecolor="black", weights=uniform_result["probability"], alpha=0.6, label="uniform", ) plt.legend() plt.ylabel("Probability", fontsize=16) plt.xlabel("cost", fontsize=16) plt.tick_params(axis="both", labelsize=14) ``` output Best Solution: ```python theme={null} optimization_result.sort_values(by="cost").iloc[0].solution ``` **Output:** ``` {'z': [0, 0, 0, 0, 0, 0]} ``` ## References \[1] The Ising model in physics and statistical genetics, J. Majewski, H. Li, J. Ott. \[2] A short introduction to topological quantum computation, Ville T. Lahtinen and Jiannis K. Pachos. \[3] Recent progresses in two-dimensional Ising superconductivity, W. Li et al. \[4] Phase transition and power-law coarsening in Ising-doped voter model, Adam Lipowski, Dorota Lipowska, Antonio L. Ferreira. \[5] Classiq's user guide, execution options: [https://docs.classiq.io/latest/user-guide/execution/](https://docs.classiq.io/latest/user-guide/execution/) \[6] The Ising model in wikepidia [https://en.wikipedia.org/wiki/Ising\_model](https://en.wikipedia.org/wiki/Ising_model) # Simulation of the 2D Maxwell Equation Using Quantum Hamiltonian Simulation Source: https://docs.classiq.io/explore/applications/physical_systems/maxwell_equation/maxwell_2d_simulation Open this notebook in GitHub to run it yourself In the present notebook we simulate the **2D Maxwell equations** for a homogeneous medium, employing a quantum Hamiltonian simulation. We work in **CGS** units, where the Maxwell equations take a symmetric form with a single wave speed parameter $c$. Earlier quantum approaches to Maxwell-type wave equations include explicit time-marching schemes \[1] and variational/quantum-classical solvers \[2]; here we instead block-encode the spatial operator and evolve it via Hamiltonian simulation. The transverse-magnetic (TM) mode on a 2-D domain involves three field components, the out-of-plane electric field $E_z$ and the in-plane magnetic field components $H_x$, $H_y$, governed by $$ \frac{\partial E_z}{\partial t} = c\!\left(\frac{\partial H_y}{\partial x} - \frac{\partial H_x}{\partial y}\right), \qquad \frac{\partial H_x}{\partial t} = -c\,\frac{\partial E_z}{\partial y}, \qquad \frac{\partial H_y}{\partial t} = c\,\frac{\partial E_z}{\partial x}. $$ Discretizing on a **Yee lattice** \[3] with spacing $\Delta L$ and using backward ($\nabla^{b}$) and forward ($\nabla^{f}$) finite differences, the system can be written in matrix form as $$ \frac{d}{dt}\begin{pmatrix} \vec{E}_z \\[4pt] \vec{H}_x \\[4pt] \vec{H}_y \end{pmatrix} = \frac{c}{\Delta L}\, \underbrace{\begin{pmatrix} 0 & -\nabla_y^{b} & \nabla_x^{b} \\ -\nabla_y^{f} & 0 & 0 \\ \nabla_x^{f} & 0 & 0 \end{pmatrix}}_{A} \begin{pmatrix} \vec{E}_z \\[4pt] \vec{H}_x \\[4pt] \vec{H}_y \end{pmatrix}. \tag{1} $$ The matrix $A$ is real and anti-symmetric ($A = -A^T$), which makes it anti-Hermitian. Therefore the time-evolution operator $e^{A\,c\,t / \Delta L}$ is unitary. This makes the system a natural fit for quantum Hamiltonian simulation, with $H=iA$. The anti-Hermiticity of the dynamical generator stems from the fact that Maxwell equation are time-translation invariant, therefore conserve the total energy (Noether's theorem). Consequently, this anti-Hermiticity of the generator is a characteristic of the homogeneous Maxwell equation, not only in the TM mode. We enforce **Perfect Electric Conductor (PEC)** boundary conditions on the exterior of the domain and on a rectangular obstacle inside it. ## Implementation with Classiq We begin by importing the required Python packages. ```python theme={null} !pip install -qq "classiq[qsp]" -U from typing import Callable import matplotlib.pyplot as plt import numpy as np import pandas as pd import scipy.linalg from classiq import * from classiq.interface.generator.model.preferences.preferences import ( TranspilationOption, ) ``` We introduce two helper functions allowing to compare quantum and classical solutions: `phase normalization` and a `fidelity measure`. The remaining utilities - field plotting, state-vector reconstruction, a synthesis/execution wrapper, and the classical reference simulation itself, are introduced later, each where it is first used. ```python theme={null} def normalize_phase(data: np.ndarray) -> np.ndarray: """ Normalize the phase of a complex vector so that the maximum amplitude is real and positive. This is useful for comparing quantum and classical solutions up to a global phase. """ _max = max(data, key=np.abs) return data * (np.abs(_max) / _max) def fidelity(a: np.ndarray, b: np.ndarray) -> float: """ fidelity of the normalized vectors """ if np.linalg.norm(a) == 0 or np.linalg.norm(b) == 0: return 0.0 a = a / np.linalg.norm(a) b = b / np.linalg.norm(b) return float(abs(np.vdot(a, b)) ** 2) ``` # ## Problem Definition We discretize the domain on an $L \times L$ **Yee lattice**. In this staggered grid the electric field $E_z$ lives at the **vertices** (integer grid points), while the magnetic components $H_x$ and $H_y$ are located at the **edge midpoints**: * $E_z(i,\,j)$ - grid vertices. * $H_x(i,\,j+\tfrac{1}{2})$ - midpoints of vertical edges (between consecutive $y$-nodes at fixed $x$). * $H_y(i+\tfrac{1}{2},\,j)$ - midpoints of horizontal edges (between consecutive $x$-nodes at fixed $y$). Yee lattice We next set the problem parameters, the grid, the timescale, and the geometry of the system, along with the width and center of the Gaussian pulse we will later use as the initial state. ```python theme={null} SIZE = 5 # qubits per spatial axis (square grid of side L = 2**SIZE) T = 5 # time c = 1 # speed of light set to 1 in CGS units # grid L = 2**SIZE dL = 1 / L # initial state - gaussian parameters sigma = 1 / (8 * dL) mu = L / 8 # geometry - rectangle middle_x = 0.5 middle_y = 0.5 side_length = 0.5 half_length = side_length / 2 top_x = 1 + int(np.floor((middle_x + half_length) * (L - 2))) + 1 top_y = 1 + int(np.floor((middle_y + half_length) * (L - 2))) + 1 bottom_x = 1 + int(np.ceil((middle_x - half_length) * (L - 2))) bottom_y = 1 + int(np.ceil((middle_y - half_length) * (L - 2))) ``` Problem geometry # ## Quantum Encoding of the Electromagnetic System We introduce a QStruct to represent the EM field, utilizing two `QNum`s to encode the position (one for the $x$-coordinate and one for $y$), with two further qubits encoding the vector `[Ez, unused, Hx, Hy]` at each coordinate. ```python theme={null} class EMState(QStruct): x: QNum[SIZE] y: QNum[SIZE] # the (field, direction) pair indexes the 4-vector [Ez, unused, Hx, Hy] direction: QBit # Hx=0, Hy=1 field: QBit # E=0, H=1 ``` # ## Construction of the Quantum Functions # ### Gradients We start by defining the **1D periodic backward and forward gradient** operators on a grid of size $N$. These come from the **first-order finite-difference** approximation of a derivative. For a function sampled at the grid points $f_i = f(i\,\Delta L)$, the spatial derivative can be estimated by comparing a point to its neighbor, either one step *backward* or one step *forward*: $$ \left.\frac{\partial f}{\partial x}\right|_i \;\approx\; \frac{f_i - f_{i-1}}{\Delta L} \quad\text{(backward)}, \qquad \left.\frac{\partial f}{\partial x}\right|_i \;\approx\; \frac{f_{i+1} - f_i}{\Delta L} \quad\text{(forward)}, $$ each accurate to $\mathcal{O}(\Delta L)$. The common $1/\Delta L$ factor is pulled out into the $c/\Delta L$ prefactor of Eq. (1), so the gradient *operators* themselves carry only the dimensionless differences $f_i - f_{i-1}$ and $f_{i+1} - f_i$. Introducing the cyclic shift $S^{+1}$ with $(S^{+1} f)_i = f_{i-1}$ and its inverse $(S^{-1} f)_i = f_{i+1}$, these read $\nabla^{b} = I - S^{+1}$ and $\nabla^{f} = S^{-1} - I$. The periodic (wrap-around) boundary turns the lone off-diagonal corner entry on, making them $N \times N$ circulant matrices: $$ \nabla^{b} = I - S^{+1} = \begin{pmatrix} 1 & 0 & \cdots & 0 & -1 \\ -1 & 1 & 0 & \cdots & 0 \\ 0 & -1 & 1 & \ddots & \vdots \\ \vdots & \ddots & \ddots & \ddots & 0 \\ 0 & \cdots & 0 & -1 & 1 \end{pmatrix}, \qquad \nabla^{f} = S^{-1} - I = \begin{pmatrix} -1 & 1 & 0 & \cdots & 0 \\ 0 & -1 & 1 & \ddots & \vdots \\ \vdots & \ddots & \ddots & \ddots & 0 \\ 0 & \cdots & 0 & -1 & 1 \\ 1 & 0 & \cdots & 0 & -1 \end{pmatrix}, $$ Note that the backward and forward operators are transposes up to a sign, $\nabla^{f} = -(\nabla^{b})^{\mathsf T}$, making the assembled Maxwell matrix $A$ anti-symmetric. We build the corresponding quantum functions and plot the non-zero components of the backward gradient. ```python theme={null} from classiq.qmod.symbolic import pi @qfunc def grad_backwards_periodic(r: QNum, block: QBit): """ +1 on the diagonal -1 below the diagonal """ lcu( coefficients=[0.5, -0.5], unitaries=[lambda: None, lambda: inplace_add(1, r)], block=block, ) @qfunc def grad_forwards_periodic(r: QNum, block: QBit): """ -1 on the diagonal +1 above the diagonal """ invert(grad_backwards_periodic)(r, block) phase(pi) ``` Backward gradient \nabla^b We next assemble the full gradient from the backward and forward gradients. We build the full matrix as a decomposition into block operators: $$ A_{E_z,H_x} = \begin{pmatrix} 0 & 0 & -\nabla_y^{b} & 0 \\ 0 & 0 & 0 & 0 \\ -\nabla_y^{f} & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 \end{pmatrix} \quad \begin{matrix} \leftarrow E_z \\ \leftarrow \text{(unused)} \\ \leftarrow H_x \\ \leftarrow H_y \end{matrix} $$ with $$ A_{E_z,H_y} = \begin{pmatrix} 0 & 0 & 0 & \nabla_x^{b} \\ 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 \\ \nabla_x^{f} & 0 & 0 & 0 \end{pmatrix} \quad \begin{matrix} \leftarrow E_z \\ \leftarrow \text{(unused)} \\ \leftarrow H_x \\ \leftarrow H_y \end{matrix} \tag{2} $$ and the periodic Maxwell operator is assembled as an **LCU** of the two interactions: $$ A = A_{E_z,H_x} + A_{E_z,H_y} = \begin{pmatrix} 0 & 0 & -\nabla_y^{b} & \nabla_x^{b} \\ 0 & 0 & 0 & 0 \\ -\nabla_y^{f} & 0 & 0 & 0 \\ \nabla_x^{f} & 0 & 0 & 0 \end{pmatrix}~~. \quad $$ To construct the gradient we first introduce a utility quantum function `grad_backwards_forwards_periodic`, which builds a block operator including the backward and forward gradients on the diagonal: $$ \begin{pmatrix} \nabla^{b} & 0 \\ 0 & \nabla^{f}\\ \end{pmatrix}~~. $$ ```python theme={null} @qfunc def grad_backwards_forwards_periodic(r: QNum, toggle: QBit, block: QBit) -> None: """ 2 blocks (based on the toggle qubit) upper left - backwards lower right - forwards """ control( toggle, lambda: grad_forwards_periodic(r, block), # toggle=1 lambda: grad_backwards_periodic(r, block), # toggle=0 ) ``` We now construct the quantum functions corresponding to $A_{E_z, H_x}$ and $A_{E_z, H_y}$ of Eq. (2), operating on the quantum state $\{E_z, \text{unused}, H_x, H_y \}^T$. ```python theme={null} @qfunc def ez_hx_interaction(em_state: EMState, block: QArray) -> None: """ Block-encode the E_z <-> H_x coupling (the A_{E_z,H_x} block of Eq. (2)). Realizes the two coupled updates, with the y-derivative discretized as a backward/forward finite difference: dt(E_z) += -dy_b(H_x) # backward gradient on the (E_z, H_x) entry dt(H_x) += -dy_f(E_z) # forward gradient on the (H_x, E_z) entry """ # swap columns em_state.field ^= 1 # block encode grad_backwards_forwards_periodic(em_state.y, em_state.field, block[1:]) # apply the minus sign to the components phase(pi) # delete rows 1, 3 (in this case could delete instead the columns, by doing that before the block encoding) block[0] ^= em_state.direction @qfunc def ez_hy_interaction(em_state: EMState, block: QArray) -> None: """ Block-encode the E_z <-> H_y coupling (the A_{E_z,H_y} block of Eq. (2)). Realizes the two coupled updates, with the x-derivative discretized as a backward/forward finite difference: dt(E_z) += dx_b(H_y) # backward gradient on the (E_z, H_y) entry dt(H_y) += dx_f(E_z) # forward gradient on the (H_y, E_z) entry """ # swap columns 0, 3 by doing the following 2 swaps: # swap columns 0, 1 and 2, 3 em_state.direction ^= 1 # swap columns 0, 2 and 1, 3 em_state.field ^= 1 # block encode grad_backwards_forwards_periodic(em_state.x, em_state.field, block[1:]) # delete rows 1, 2 block[0] ^= em_state.direction ^ em_state.field @qfunc def periodic_maxwell_operator(em_state: EMState, block: QArray) -> None: """ Assemble the full periodic evolution operator, without the boundary conditions """ lcu( coefficients=[0.5, 0.5], unitaries=[ lambda: ez_hx_interaction(em_state, block[1:]), lambda: ez_hy_interaction(em_state, block[1:]), ], block=block[0], ) ``` We impose **Perfect Electric Conductor (PEC)** boundary conditions: on the surface of a perfect conductor the tangential electric field must vanish, so we force $E_z = 0$ on every conducting site - both the outer edge of the domain and the interior rectangular obstacle. Physically this makes the conductor act as a perfect mirror for the wave. We implement this by *deleting* the corresponding entries of the Maxwell matrix $A$ with a **flag (block) qubit**: whenever the state sits on a site where $E_z$ must vanish, we flip the flag, pushing that amplitude out of the all-zeros block subspace so it no longer contributes to the encoded operator. The flag is applied only to the $E_z$ component (`field == 0` and `direction == 0`); because $H_x, H_y$ are coupled to $E_z$ through the dynamics, their boundary behavior follows implicitly. Two regions are flagged (see the `@qperm` functions below): * **Exterior boundary** - since the gradient operators are periodic, fixing the **origin of each axis** ($x = 0$ or $y = 0$) is enough; the opposite edge is pinned automatically by the wrap-around. Because the corner $(0,0)$ is decoupled, the two axis conditions can be combined with an `XOR` instead of an `AND`, avoiding an extra ancilla. * **Interior obstacle** - every site inside the rectangle is flagged directly. Finally, the flag is applied via `within_apply` ($U^\dagger \, V \, U$), so the same condition removes both the rows and the columns of $A$, not just the rows. Deleting columns as well preserves the anti-symmetry $A = -A^{\mathsf T}$ - and hence the unitarity of the evolution. Since the diagonal of the periodic operator is zero, a single flag qubit can be reused for both, saving one block qubit. In the run algorithm section, we summarize the number of block qubits ($5$) and analyze the source of each of them. ```python theme={null} @qperm def rectangle_boundary_conditions(x: Const[QNum], y: Const[QNum], flag: QBit) -> None: """ flip flag (a block qubit) inside a rectangle """ assert ( (top_x > bottom_x and top_y > bottom_y) and (top_x < L and top_y < L) and (bottom_x > 0 and bottom_y > 0) ), "Illegal rectangle" # can use xor in each axis as the conditions are mutually exclusive flag ^= (~((x >= top_x) ^ (x < bottom_x))) & (~((y >= top_y) ^ (y < bottom_y))) @qperm def exterior_boundary_conditions(x: Const[QNum], y: Const[QNum], flag: QBit) -> None: """ flip flag (a block qubit) at the boundary of the grid. In this case we only enforce the origin of each axis. """ # naively we would do (y == 0) & (x==0), which will allocate an auxiliary qubit # We can do xor because (0, 0) is decoupled from the rest of the system flag ^= (y == 0) ^ (x == 0) @qperm def pec_boundary_conditions(em_state: Const[EMState], flag: QBit) -> None: """ Enforce Perfect Electric Conductor (PEC) boundary conditions. A PEC forces the tangential electric field to vanish at its surface, so we pin E_z = 0 on every conducting site: the outer edge of the domain (`exterior_boundary_conditions`) and the interior rectangular obstacle (`rectangle_boundary_conditions`). Flipping `flag` on those sites projects the corresponding amplitudes out of the block-encoded operator, which makes the conductor act as a perfect mirror for the wave. The condition is applied only to the Ez component (direction == 0 and field == 0); Hx and Hy inherit it implicitly through their coupling to Ez in the dynamics. """ # apply the boundary only on Ez control( (em_state.direction == 0) & (em_state.field == 0), # can use xor as the conditions are mutually exclusive lambda: ( exterior_boundary_conditions(em_state.x, em_state.y, flag), rectangle_boundary_conditions(em_state.x, em_state.y, flag), ), ) @qfunc def maxwell_operator(em_state: EMState, block: QArray) -> None: """ Full Maxwell evolution operator, including the boundary conditions. """ # `within_apply(within=W, apply=V)` runs the sequence W, then V, then W^- 1. # Here W = pec_boundary_conditions flips the flag qubit on every boundary # site, and it is its own inverse. V = periodic_maxwell_operator applies # the full periodic operator, but with the boundary sites removed from the dynamics # by the flag qubit. within_apply( within=lambda: pec_boundary_conditions(em_state, block[0]), apply=lambda: periodic_maxwell_operator(em_state, block[1:]), ) ``` Full Maxwell operator - sparsity pattern # ## Field Dynamics Employing Hamiltonian Simulation with GQSP Our goal is to implement the time-evolution operator $e^{(c/\Delta L)\,A\,t}$, where $A$ is the anti-symmetric Maxwell matrix. We achieve this through **Generalized Quantum Signal Processing (GQSP)** \[4] in three steps. For a comprehensive description of the method see the [Hamiltonian simulation with GQSP notebook](https://github.com/Classiq/classiq-library/blob/a7a7de52f852ab3545edb75ac4889ef6e07455ca/algorithms/hamiltonian_simulation/hamiltonian_simulation_with_block_encoding/hamiltonian_simulation_gqsp.ipynb). # ### 1. From Block Encoding to Walk Operator Suppose $U_A$ is a block encoding of $A/\alpha$ (with $\alpha$ the encoding scale). Since $A$ is anti-symmetric, $H = iA$ is **Hermitian**, so multiplying the block encoding by a global phase of $i$ gives a block encoding of the Hermitian matrix $H/\alpha$. The **walk operator** is then $$ W = R \cdot (i\,U_A), \qquad R = 2\,|0\rangle\!\langle 0|_{\text{block}}-I, $$ where $R$ is the reflection about the block-encoding subspace. If $\lambda_k$ are the eigenvalues of $H/\alpha$ (real, with $|\lambda_k| \le 1$), then the walk operator has eigenvalues $e^{\pm i\,\theta_k}$ with $\theta_k = \arccos(\lambda_k)$. # ### 2. Jacobi-Anger Polynomial Approximation The desired evolution in the eigenbasis is $e^{-i H t} = e^{A t}$. In terms of the walk operator eigenphases: $$ e^{-i\,\alpha\,t_{\text{eff}}\,\cos\theta} \quad\text{with}\quad t_{\text{eff}} = \frac{c\,t}{\Delta L}, $$ where $\alpha\,t_{\text{eff}}$ is the effective evolution parameter. This function is approximated by a truncated **Jacobi-Anger expansion** - a Laurent polynomial in $e^{i\theta}$: $$ e^{-i\,\alpha\,t_{\text{eff}}\,\cos\theta} \;\approx\; \sum_{d=-D}^{D} c_d \, e^{i\,d\,\theta}, $$ where $D$ (the GQSP degree) is chosen to achieve a target precision $\varepsilon$. Specifically we approximate a scaled version of the function, to guarantee GQSP phase finding stability. ```python theme={null} from classiq.applications.qsp import ( gqsp_phases, poly_jacobi_anger_degree, poly_jacobi_anger_exp_cos, ) @qfunc def walk_operator(em_state: EMState, block: QArray) -> None: # block encode the anti-symmetric evolution operator maxwell_operator(em_state, block) # apply an i phase to turn the maxwell operator to a Hamiltonian phase(pi / 2) # reflect about the subspace of the block-encoding reflect_about_zero(block) encoding_scale = 4.0 # pay a factor of 2 for each lcu. # scale the gqsp polynomial to be below 1 to improve numerical stability GQSP_SCALE = 0.5 GQSP_EPS = 1e-6 @qfunc def hamiltonian_simulation( em_state: EMState, block: QArray, t: float ): # evolution time effective_time = t * encoding_scale * (c / dL) # use the gqsp to turn the walk operator with eigenvalues exp(i*arccos(lambda)) # to exp(-i*lambda*t). gqsp_degree = poly_jacobi_anger_degree(GQSP_EPS, effective_time) print(f"GQSP degree: {gqsp_degree}") # approximate exp(i*cos(x)*t) by a polynomial sum_d{c_d exp(i*x*d)} poly = GQSP_SCALE * poly_jacobi_anger_exp_cos(gqsp_degree, -effective_time) negative_power = (len(poly) - 1) // 2 # use a both negative and positive exponents # compute the phases for the wanted polynomial phases = gqsp_phases(poly) gqsp( u=lambda: walk_operator(em_state, block[1:]), aux=block[0], phases=phases, negative_power=negative_power, ) ``` Next we introduce a utility function that will allow plotting the initial and final fields ```python theme={null} def fields_max_abs(vec: np.ndarray | list[float], L: int) -> np.ndarray: """ Return the max absolute value of each field component (Ez, Hx, Hy). Useful as a reference scale for ``plot_fields``. """ vec = np.asarray(vec) LL = L * L components = [vec[0:LL], vec[2 * LL : 3 * LL], vec[3 * LL : 4 * LL]] return np.array([np.max(np.abs(comp.real)) for comp in components]) def plot_fields( vec: np.ndarray | list[float], L: int, title: str = "", field_vmax: np.ndarray | list[float] | None = None, ) -> None: """ Plot Ez, Hx, Hy from a state vector on an L x L grid. Args: vec: State vector of length 4*L*L. L: Grid size per axis. title: Figure title. field_vmax: Optional array of 3 normalization values (one per field). When provided, the color scale for each field is [-field_vmax[i], field_vmax[i]] and no per-field self-normalization is applied. Useful for plotting differences on the same scale as a reference solution. """ vec = np.asarray(vec) vec = normalize_phase(vec) LL = L * L components = [vec[0:LL], vec[2 * LL : 3 * LL], vec[3 * LL : 4 * LL]] data = [comp.reshape(L, L).real for comp in components] if field_vmax is None: raw_max = fields_max_abs(vec, L) data_max = np.where(raw_max > 0, raw_max, 1.0) data = [d / m for d, m in zip(data, data_max)] plot_max = np.array([np.max(np.abs(d)) for d in data]) else: field_vmax = np.asarray(field_vmax, dtype=float) plot_max = np.where(field_vmax > 0, field_vmax, 1.0) names = ("Ez", "Hx", "Hy") fig, axes = plt.subplots(1, 3, figsize=(15, 5)) if title: fig.suptitle(title) for ind in range(3): ax = axes[ind] im = ax.imshow( data[ind], origin="lower", aspect="equal", vmin=-plot_max[ind], vmax=plot_max[ind], cmap="twilight", ) ax.set_title(names[ind]) plt.colorbar(im, ax=ax) plt.tight_layout() plt.show() ``` # ### Initial State Preparation We initialize the field as a **2D Gaussian pulse in the electric field $E_z$ only** - the magnetic components start at rest ($H_x = H_y = 0$). On the $E_z$ sites the amplitude is $$ E_z(x, y) \;\propto\; \exp\!\left(-\frac{(x-\mu)^2 + (y-\mu)^2}{2\sigma^2}\right), $$ a bump centered at $(\mu, \mu)$ with width $\sigma$. To stay consistent with the **PEC boundary conditions**, the amplitude is forced to zero on the conducting sites - the $x=0$ and $y=0$ edges and every point inside the rectangular obstacle. The function below evaluates this profile on the lattice; the resulting (real, normalized) amplitudes are then loaded into the `EMState` register by amplitude encoding (`prepare_amplitudes`), which sets up $|\psi(0)\rangle$ for the subsequent time evolution. ```python theme={null} def maxwell_2d_initial_state(x: float, y: float, direction: int, field: int) -> float: """ Load a 2D Gaussian state, taking into account the boundary conditions. """ # only Ez (electric field) is nonzero initially if direction != 0 or field != 0: return 0.0 # no field on the boundaries if x == 0 or y == 0: return 0.0 # no field in the rectangle if bottom_x <= x < top_x and bottom_y <= y < top_y: return 0.0 # field in the rest of the grid return float(np.exp(-((x - mu) ** 2 + (y - mu) ** 2) / (2 * sigma**2))) @qfunc def prepare_initial_state(em_state: Output[EMState]) -> None: """ Prepare the initial state of the electromagnetic field. Load a 2D Gaussian state, taking into account the boundary conditions. """ init_amplitudes = lookup_table( maxwell_2d_initial_state, [em_state.x, em_state.y, em_state.direction, em_state.field], ) plot_fields(init_amplitudes, L, "initial state t=0") prepare_amplitudes(init_amplitudes, 0, em_state) ``` # ### Run Full Algorithm We begin by defining the number of block qubits. There are in total $5$ block qubits, they result from the following algorithmic steps (each step corresponds to a single qubit): 1. Linear combination of unitaries in the addition of $I$ and $S^{+1}$ in the one dimensional backward gradient (`grad_backwards_periodic`). 2. sub-block selection in the implementation of `ez_hx_interaction` or `ez_hy_interaction`. 3. LCU combining $A_{E_z,H_x}$ and $A_{E_z,H_y}$ (`periodic_maxwell_operator`). 4. PEC boundary-condition flag (reused for exterior + obstacle, saving one qubit since the periodic diagonal is zero) (`maxwell_operator`). 5. GQSP auxiliary qubit for the walk operator polynomical (in `hamiltonian_simulation`). The physical system is described by an additional $12$ qubits, leading to a full circuit width of $17$ quibits. ```python theme={null} BLOCK_SIZE = 5 @qfunc def main(em_state: Output[EMState], block: Output[QNum[BLOCK_SIZE]]) -> None: allocate(block) prepare_initial_state(em_state) hamiltonian_simulation(em_state, block, T) ``` We wrap synthesis and execution in a single helper, `run_simulation`: it builds the model, synthesizes it, and samples the resulting program on a state-vector simulator, filtering for the all-zeros block subspace (the subspace in which the block encoding realizes the desired operator). ```python theme={null} def run_simulation( main_func, backend: str = "classiq/nvidia_simulator", block_name: str | None = "block", show: bool = False, ) -> tuple[pd.DataFrame, QuantumProgram]: print("Synthesizing...") qprog = synthesize( main_func, constraints=Constraints(optimization_parameter="width"), preferences=Preferences( transpilation_option=TranspilationOption.NONE, timeout_seconds=10 * 60, symbolic_loops=True, ), auto_show=show, ) print("Synthesis completed.") print("Calculating state vector...") filters = {block_name: 0} if block_name is not None else None df = calculate_state_vector(qprog, backend=backend, filters=filters) print("State vector calculation completed.") return df, qprog ``` ```python theme={null} df, qprog = run_simulation(main, show=True) ``` **Output:** ``` Synthesizing... ``` output **Output:** ``` GQSP degree: 686 Quantum program link: https://platform.classiq.io/circuit/3EoBKBprLnSLUTmbFpQA97jtWEZ ``` **Output:** ``` Submitting job to simulator ``` **Output:** ``` Synthesis completed. Calculating state vector... ``` **Output:** ``` Job: https://platform.classiq.io/jobs/0908f7f9-ec23-4c30-adad-95c2c1be06f6 ``` ```python theme={null} df.head() ``` | | em\_state.x | em\_state.y | em\_state.direction | em\_state.field | block | amplitude | magnitude | phase | probability | bitstring | | - | ----------- | ----------- | ------------------- | --------------- | ----- | ------------------ | --------- | ------ | ----------- | -------------------- | | 0 | 25 | 25 | 0 | 0 | 0 | 0.000000-0.037809j | 0.04 | -0.50π | 0.001430 | 00000110011100100000 | | 1 | 25 | 26 | 0 | 0 | 0 | 0.000001-0.034855j | 0.03 | -0.50π | 0.001215 | 00000110101100100000 | | 2 | 26 | 25 | 0 | 0 | 0 | 0.000001-0.034852j | 0.03 | -0.50π | 0.001215 | 00000110011101000000 | | 3 | 24 | 26 | 0 | 0 | 0 | 0.000002-0.031485j | 0.03 | -0.50π | 0.000991 | 00000110101100000000 | | 4 | 26 | 24 | 0 | 0 | 0 | 0.000002-0.031483j | 0.03 | -0.50π | 0.000991 | 00000110001101000000 | In order to plot the results we first introduce additional utility functions, which extract the dataframe to a numpy array and plot the array. ```python theme={null} def dataframe_to_state_vector( df: pd.DataFrame, struct_name: str = "em_state" ) -> tuple[np.ndarray, int]: """ Reconstruct the state vector from a Classiq execution dataframe. Filters for block == 0, infers the grid size L, and builds the 4*L*L complex amplitude vector using the y*L + x spatial indexing convention. Returns: (vec, L) where vec has length 4*L*L. """ col_x = f"{struct_name}.x" col_y = f"{struct_name}.y" col_dir = f"{struct_name}.direction" col_field = f"{struct_name}.field" df_block0 = df[df["block"] == 0].copy() L = int(max(df_block0[col_x].max(), df_block0[col_y].max())) + 1 LL = L * L vec = np.zeros(4 * LL, dtype=complex) for _, row in df_block0.iterrows(): idx = ( int(row[col_field]) * 2 * LL + int(row[col_dir]) * LL + int(row[col_y]) * L + int(row[col_x]) ) vec[idx] = row.amplitude return vec, L def plot_fields_from_dataframe( df: pd.DataFrame, title: str = "", struct_name: str = "em_state", field_vmax: np.ndarray | list[float] | None = None, ) -> None: """ Plot Ez, Hx, Hy from a Classiq execution dataframe whose main function outputs (em_state: EMState, block: QNum). """ vec, L = dataframe_to_state_vector(df, struct_name) plot_fields(vec, L, title, field_vmax) ``` Finally, the plots of the final fields show the field configuration around the conducting object. ```python theme={null} plot_fields_from_dataframe(df, f"State at t={T}") ``` output ## Classical Validation We next benchmark the quantum calculation against a classical solution, employing standard matrix exponentiation. The initial field state is propagated, and the fields at final times are compared. The classical simulation is performed by the function `classical_maxwell_simulation`, which employs `build_initial_state_vector` to build the initial state field and `build_maxwell_evolution_matrix` to construct the dynamical generator associated with the Maxwell equation. ```python theme={null} def build_maxwell_evolution_matrix( L: int, bottom_x: int, top_x: int, bottom_y: int, top_y: int, ) -> np.ndarray: """ Build the anti-symmetric Maxwell evolution matrix on a square Yee lattice of side L, with PEC exterior boundary conditions and a rectangular PEC obstacle. Args: L: Grid side length (must be a power of 2). The lattice is L x L. bottom_x, top_x, bottom_y, top_y: Rectangle obstacle bounds. Returns: The anti-symmetric evolution matrix of shape (4*L*L, 4*L*L). """ LL = L * L id_L = np.eye(L, dtype=float) zero_LL = np.zeros((LL, LL), dtype=float) _grad_b = id_L - np.roll(id_L, 1, 0) _grad_f = np.roll(id_L, -1, 0) - id_L grad_b_x = np.tensordot(id_L, _grad_b, axes=0).transpose(0, 2, 1, 3).reshape(LL, LL) grad_b_y = np.tensordot(_grad_b, id_L, axes=0).transpose(0, 2, 1, 3).reshape(LL, LL) grad_f_x = np.tensordot(id_L, _grad_f, axes=0).transpose(0, 2, 1, 3).reshape(LL, LL) grad_f_y = np.tensordot(_grad_f, id_L, axes=0).transpose(0, 2, 1, 3).reshape(LL, LL) mat_em = np.block( [ [zero_LL, zero_LL, -grad_b_y, grad_b_x], [zero_LL, zero_LL, zero_LL, zero_LL], [-grad_f_y, zero_LL, zero_LL, zero_LL], [grad_f_x, zero_LL, zero_LL, zero_LL], ] ) # PEC exterior boundary conditions mat_em[1:L, :] = 0 mat_em[:, 1:L] = 0 mat_em[L:LL:L, :] = 0 mat_em[:, L:LL:L] = 0 # Rectangle obstacle boundary conditions if top_x > bottom_x and top_y > bottom_y: ins = [] for iy in range(bottom_y, top_y): for ix in range(bottom_x, top_x): ins.append(iy * L + ix) outs = list(set(range(4 * LL)) - set(ins)) for in_ in ins: for out_ in outs: mat_em[in_, out_] = 0 mat_em[out_, in_] = 0 assert np.isclose( np.sum(np.abs(mat_em + mat_em.T)), 0 ), "Matrix is not anti-symmetric" return mat_em def build_initial_state_vector( L: int, initial_state_func: Callable[[float, float, int, int], float], normalize: bool = True, ) -> np.ndarray: """ Build the initial state vector by evaluating a callable on every grid point of an L x L square lattice. The state vector has length 4*L*L, indexed as field * 2*LL + direction * LL + y * L + x (LL = L * L). Args: L: Grid side length. initial_state_func: Callable(x, y, direction, field) -> float. normalize: If True, normalize the vector to unit norm. Returns: The state vector of shape (4*L*L,). """ LL = L * L state = np.zeros(4 * LL, dtype=float) for field in range(2): for direction in range(2): for iy in range(L): for ix in range(L): idx = field * 2 * LL + direction * LL + iy * L + ix state[idx] = initial_state_func( float(ix), float(iy), direction, field ) if normalize: norm = np.linalg.norm(state) if norm > 0: state = state / norm return state def classical_maxwell_simulation( L: int, coeff: float, initial_state_func: Callable[[float, float, int, int], float], bottom_x: int, top_x: int, bottom_y: int, top_y: int, ) -> np.ndarray: """ Classical reference simulation for 2D Maxwell equations on a square Yee lattice. Builds the anti-symmetric evolution matrix with PEC exterior boundary conditions and a rectangular PEC obstacle, then computes exp(coeff * A) @ initial_state. Args: L: Grid side length (must be a power of 2). The lattice is L x L. coeff: Evolution coefficient (c * dt / dL). initial_state_func: Callable(x, y, direction, field) -> float returning the amplitude at each grid point. bottom_x, top_x, bottom_y, top_y: Rectangle obstacle bounds. Returns: The evolved state vector. """ initial_state = build_initial_state_vector(L, initial_state_func) mat_em = build_maxwell_evolution_matrix(L, bottom_x, top_x, bottom_y, top_y) exp_em = scipy.linalg.expm(coeff * mat_em) return exp_em @ initial_state ``` ```python theme={null} # Run the classical simulation coeff = c * T / dL classical_vec = classical_maxwell_simulation( L, coeff, maxwell_2d_initial_state, bottom_x, top_x, bottom_y, top_y, ) classical_vec = normalize_phase(classical_vec) # Compute fidelity quantum_vec, _ = dataframe_to_state_vector(df) quantum_vec = normalize_phase(quantum_vec) fid = fidelity(quantum_vec, classical_vec) print(f"Fidelity (quantum vs classical): {fid:.6f}") print( f"Encoding Scaling: {np.linalg.norm(quantum_vec)/np.linalg.norm(classical_vec):.6f}", f"Expected: {GQSP_SCALE:.6f}", ) error = 1.0 - fid print(f"Error in logarithmic scale (-log_10(1-fidelity)): {-np.log10(1-fid)}") assert ( -np.log10(1 - fid) > 4 ), f"The accuracy (-log10(1-fidelity)) between the quantum and classical simulations differed by {-np.log10(1-fid)}" ``` **Output:** ``` Fidelity (quantum vs classical): 1.000000 Encoding Scaling: 0.499257 Expected: 0.500000 Error in logarithmic scale (log_10(1-fidelity)): -7.6172263817520545 ``` As the fidelity comparison shows, the quantum and classical calculations agree perfectly. To visualize this result we plot both fields next to each other, as expected the plots appear identical. ```python theme={null} # Plot both results plot_fields(classical_vec, L, title="Classical Simulation") plot_fields(quantum_vec, L, title="Quantum Simulation") ``` output output ## References \[1]: [Costa, P. C. S., Jordan, S., and Ostrander, A. *Quantum algorithm for simulating the wave equation.* Physical Review A **99**, 012323 (2019).](https://journals.aps.org/pra/abstract/10.1103/PhysRevA.99.012323) \[2]: [Suau, A., Staffelbach, G., and Calandra, H. *Practical quantum computing: solving the wave equation using a quantum approach.* ACM Transactions on Quantum Computing **2**, 1-35 (2021).](https://dl.acm.org/doi/10.1145/3430030) \[3]: [Yee, K. *Numerical solution of initial boundary value problems involving Maxwell's equations in isotropic media.* IEEE Transactions on Antennas and Propagation **14**, 302-307 (1966).](https://ieeexplore.ieee.org/document/1138693) \[4]: [Motlagh, D., and Wiebe, N. *Generalized quantum signal processing.* PRX Quantum **5**, 020368 (2024).](https://journals.aps.org/prxquantum/abstract/10.1103/PRXQuantum.5.020368) # The Quantum Sawtooth Map Source: https://docs.classiq.io/explore/applications/physical_systems/quantum_chaos/quantum_sawtooth_map Open this notebook in GitHub to run it yourself Quantum maps, the quantum analogs of classical maps, are simple yet powerful models of quantum dynamics. A typical quantum map describes a system undergoing free evolution that is periodically "kicked" by a position-dependent force. The resulting dynamics are discrete in time and are naturally formulated as a so-called Floquet system \[[1](#floquet)]. Most quantum maps are obtained by quantizing a known classical map, such as the tent map, the sawtooth map, or the kicked rotor, allowing for direct comparison between classical and quantum behavior. Over the years, these toy models have played an important role in understanding fundamental phenomena such as the correspondence principle, decoherence, entanglement spreading, and quantum dynamical chaos. In the context of *quantum computing*, these well-studied systems provide a convenient and nontrivial testbed for quantum algorithms and hardware. Their dynamics are rich enough to generate strong entanglement and chaotic behavior, yet structured enough to admit efficient implementations on quantum circuits. As a result, quantum maps have been used as controlled settings to investigate the effects of noise, imperfections, and decoherence in quantum processors, as well as to assess hardware performance. For example, quantum chaotic maps have been employed to study the impact of realistic noise models on quantum dynamics \[[2](#porterjoseph)] and to benchmark quantum hardware through their sensitivity to errors and entanglement generation \[[3](#pizzamiglio-etal)]. Despite their simple definition, quantum maps can exhibit chaotic dynamics with rapid entanglement growth, which makes classical simulation inefficient. In particular, it has been shown that these systems can be simulated exponentially faster on a quantum computer \[[4](#speedup)]. Since these systems are already well studied, this exponential speedup is not expected to translate into a scientific quantum advantage today. In practice, classical numerical simulations at sizes on the order of \~20 qubits are typically sufficient to capture and understand most of the phenomena currently of interest. In this notebook we focus on the quantum sawtooth map and the phenomenon of dynamical localization, whose validation on quantum hardware has been studied in several previous works (see Refs. in \[[1](#porterjoseph)]). Below, we define the model and discuss its theoretical properties, as well as present execution results on an IonQ machine. **This notebook illustrates how Classiq Qmod's language and execution framework can be used to easily define, run, and observe quantum phenomena on a real quantum device**. ## Quantum Sawtooth Map and Its Evolution # ## The System Hamiltonian We start with the classical sawtooth map, written in terms of the action-angle variables $(p,q)$ $$ \large H_{\rm cl}(t) = \frac{p^2}{2L}-K\frac{q^2}{2}\sum_{i}\delta(t-i\tau), \quad q \bmod 2\pi, $$ where $L$ is the moment of inertia, and $K$ and $\tau$ are the kicking strength and kicking period, respectively. We now skip directly to the quantum model; the full derivation is provided in the Technical Notes at the end of this notebook. The derivation includes a straightforward quantization procedure, together with several redefinitions and normalizations. For the quantum map, we consider a finite Hilbert space of dimension $N$, with momentum and position operators $\hat{p}$ and $\hat{q}$ defined by $$ \hat{p}|p\rangle = p|p\rangle,\qquad \hat{q}|q\rangle = q|q\rangle, \qquad p,q \in \left\{\frac{N}{2}, \dots,-1,0,1, \dots,\frac{N-1}{2}\right\}. $$ The Hamiltonian of the quantum sawtooth map reads: $$ \large \frac{H_N(t)}{\hbar} = 2\pi \left(\frac{\hat{p}^2}{2N}-K\frac{\hat{q}^2}{2N}\sum_{i}\delta(t-i)\right), $$ where $\hbar$ is a dimensionless Planck constant (which can be related to the physical one by a factor of $L/\tau$). As part of the quantization procedure, the product $\hbar N$ is held constant. Consequently, the parameter controlling quantum effects is the Hilbert space dimension $N$, with quantum fluctuations scaling as $\hbar \sim 1/N$. # ## The Time Evolution A key practical advantage of quantum maps is the simplicity of their implementation on a quantum computer. Because the kicks are modeled as delta functions, the time-evolution operator factorizes into a sequence of evolutions generated by operators that are diagonal in complementary bases. In particular, for the sawtooth map considered in this notebook, the implementation does not require approximation overhead from product formulas or Chebyshev expansions, making it especially well suited for near-term quantum devices. The unitary evolution of the system between kick $t$ and $t+1$, known as the Floquet operator, is given by: $$ \large |\psi_{t+1}\rangle = U_{\rm kick}|\psi_{t}\rangle,\qquad U_{\rm kick} \equiv \exp\left[-i\frac{2\pi}{N} \frac{\hat{p}^2}{2}\right] \exp\left[iK\frac{2\pi}{N} \frac{\hat{q}^2}{2}\right]. $$ (This expression is exact and follows from modeling the kicks as delta-function impulses.) The evolution of the system from an initial state after $T$ kicks is therefore $$ \large U_{T - {\rm kicks}} = U^T_{\rm kick} = \left(\exp\left[-i\frac{2\pi}{N} \frac{\hat{p}^2}{2}\right] \exp\left[iK\frac{2\pi}{N} \frac{\hat{q}^2}{2}\right]\right)^T. $$ The kinetic term is diagonal in the momentum basis, while the kicking term is diagonal in the position basis. Since the two bases are related by a (discrete) Fourier transform, the Floquet operator can be efficiently simulated by alternating between these representations. # ## Implementation in Qmod We work with a Hilbert space of $n$ qubits, corresponding to a dimension $N=2^n$. The time evolution of the model can be written in Qmod using only a few lines of code, taking advantage of the phase assignment construct. ```python theme={null} from classiq import * from classiq.qmod.symbolic import pi @qfunc def p_space_evolution(p: QNum): phase(-(pi / 2**p.size) * p**2) @qfunc def q_space_evolution(kick_mag: CReal, p: QNum): within_apply(lambda: qft(p), lambda: phase((kick_mag * pi / (2**p.size)) * p**2)) @qfunc def single_step(kick_mag: CReal, p: QNum): q_space_evolution(kick_mag, p) p_space_evolution(p) ``` ## Dynamical Localization Depending on the non-dimensional parameters $N$ and $K$, the quantum system exhibits different dynamical regimes and a variety of phenomena, such as dynamical localization, anomalous diffusion, and a positive quantum Lyapunov exponent. In this notebook we focus on the regime $0 **Output:** ``` Kicking magnitude must be smaller than K=1.3674576271147798 Taking K = 0.1 Localization length: 1.64791067824756 ``` # ## Model Definition Defining a model with a parametric number of total time (number of kicks $T$): ```python theme={null} @qfunc def main(num_kicks: CInt, kick_mag: CReal, p: Output[QNum[NUM_QUBITS, SIGNED, 0]]): allocate(p) p ^= -2 power(num_kicks, lambda: single_step(KICK_MAG, p)) qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/39hMn1YM3alzTTAUqzeRgi7MHR4 ``` Screenshot 2026-02-05 at 22.24.07.png ```python theme={null} ap_qprog = assign_parameters(qprog, {"num_kicks": 1}) depth = ap_qprog.transpiled_circuit.depth cx_counts = ap_qprog.transpiled_circuit.count_ops["cx"] print(f"depth for one iteration {depth}") print(f"cx counts for one iteration {cx_counts}") ``` **Output:** ``` depth for one iteration 48 cx counts for one iteration 30 ``` # ## Execution We execute the quantum program for different total numbers of kicks in order to examine the time evolution of the initial wavefunction. ```python theme={null} NUM_TIMES = 4 timesteps = [2 * i for i in range(1, NUM_TIMES + 1)] ``` ```python theme={null} print(f"======== Running for {len(range(1,NUM_TIMES+1))} different times =========") print(f"times : {timesteps}") print(f"depths per power execution {[depth*2*i for i in range(1,NUM_TIMES+1)]}") cxs = [cx_counts * 2 * i for i in range(1, NUM_TIMES + 1)] print(f"cx-counts per power execution {cxs}") print(f"Total cx counts {sum(cxs)}") ``` **Output:** ``` ======== Running for 4 different times ========= times : [2, 4, 6, 8] depths per power execution [96, 192, 288, 384] cx-counts per power execution [60, 120, 180, 240] Total cx counts 600 ``` First, we run the circuit on a simulator to obtain a reference for the expected dynamics. ```python theme={null} NUM_SHOTS = 1000 ``` ```python theme={null} backend_preferences = ClassiqBackendPreferences( backend_name="simulator", ) execution_prefs = ExecutionPreferences( backend_preferences=backend_preferences, num_shots=NUM_SHOTS ) with ExecutionSession(qprog, execution_prefs) as es: results_simulator = es.batch_sample([{"num_kicks": t} for t in timesteps]) ``` When running on hardware, we submit a job and store its ID. The results can then be retrieved once the execution is complete. Here is an example of how to run on IonQ machines, using `run_through_classiq`. ```python theme={null} import datetime import os BACKEND_NAME = "qpu.forte-1" RUN_ON_HARDWARE = False if RUN_ON_HARDWARE: # Define backend preferences backend_preferences = IonqBackendPreferences( backend_name=BACKEND_NAME, run_through_classiq=True, error_mitigation=False ) prefix = f"{BACKEND_NAME}_{NUM_SHOTS}" execution_prefs = ExecutionPreferences( backend_preferences=backend_preferences, num_shots=NUM_SHOTS ) # Submit a job with ExecutionSession(qprog, execution_prefs) as es: job = es.submit_batch_sample([{"num_kicks": t} for t in timesteps]) job_ID = job.id print(f"Job ID: {job_ID}") timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") filename = f"{prefix}_jobID_{timestamp}.txt" with open(filename, "w") as f: f.write(job_ID) ``` ```python theme={null} # Check job status if RUN_ON_HARDWARE: with open(filename, "r") as f: job_ID = f.read() restored_job = ExecutionJob.from_id(job_ID) status = restored_job.status print(f"Job status: {status}") if status == "COMPLETED": res = restored_job.get_batch_sample_result() dirname = f"data_{prefix}_jobID_{timestamp}" os.makedirs(dirname, exist_ok=True) for i, df in enumerate(res): df = res[i].dataframe df.to_csv(f"{dirname}/df_{timesteps[i]}kicks.csv", index=False) ``` Below, we load pre-run results obtained on IonQ and IBM machines, and compare them to the simulator results: ```python theme={null} dirname_ionq = "data_qpu.forte-1_1000_jobID_20260209_150802" ``` ```python theme={null} def sort_and_fill_df(df): half = 2 ** (NUM_QUBITS - 1) p_full = np.arange(-half, half + 1) df_sorted = ( df.set_index("p") # make p the index .reindex(p_full) # add missing p values .fillna({"probability": 0.0}) # fill missing probability with 0 .reset_index() .rename(columns={"index": "p"}) .sort_values("p") ) return df_sorted ``` ```python theme={null} import matplotlib.pyplot as plt import pandas as pd fig, axes = plt.subplots() # Simulator for i in range(len(timesteps)): df = results_simulator[i].dataframe df_sorted = sort_and_fill_df(df) cl_plot = axes.plot( df_sorted["p"], df_sorted["probability"], ":o", color="gray", linewidth=4 ) axes.tick_params(axis="both", labelsize=14) pl_objs = [] for i in range(len(timesteps)): df = pd.read_csv(f"{dirname_ionq}/df_{timesteps[i]}kicks.csv") df_sorted = sort_and_fill_df(df) pl_objs += axes.plot(df_sorted["p"], df_sorted["probability"], "-o", linewidth=2) axes.legend( cl_plot + pl_objs, [f"simulator, T= {', '.join(map(str, timesteps))}"] + [f"qpu, T = {timesteps[i]}" for i in range(len(timesteps))], fontsize=14, ) get_backend_name = dirname_ionq.split("data_", 1)[1].split(f"_{NUM_SHOTS}", 1)[0] axes.set_title(f"{get_backend_name}", fontsize=16) axes.set_xlabel(r"$p$", fontsize=14) axes.set_ylabel(r"$P(p)$", fontsize=14) plt.tight_layout() plt.show() ``` output ```python theme={null} for i in range(len(timesteps)): df = results_simulator[i].dataframe assert float(df.loc[df["p"] == -2, "probability"].iloc[0]) > 0.85 ``` ## Technical Notes Below we derive the time-evolution of the quantum sawtooth map. We start with the classical map and go through several renormalizations and redefinitions. The classical system is written for the so-called action-angle variables $(p,q)$ $$ \large H_{\rm cl}(t) = \frac{p^2}{2L}-K\frac{q^2}{2}\sum_{i}\delta(t-i\tau), \quad q \bmod 2\pi, $$ where $L$ is the momentum of inertia, and $K$ and $\tau$ are the kicking strength and period, respectivaly. We move to a non-dimentional problem, by taking $t\rightarrow t\tau$, and $p\rightarrow pL/\tau$ $$ \large H_{\rm cl}(t) = \frac{p^2}{2}-K\frac{q^2}{2}\sum_{i}\delta(t-i), \quad q \bmod 2\pi, $$ where we also define $K\rightarrow K\tau/L$. Next, we quantize the system, defining the operators $\hat{p}$ and $\hat{q}$ on a finite Hilbert space of size $N$ with $$ \hat{p}|n\rangle = \hbar|n\rangle, \quad [\hat{p},\hat{q}]=i\hbar, $$ $$ p = \frac{N}{2}, \dots,-1,0,1, \dots,\frac{N-1}{2}, $$ $$ q/2\pi= -\frac{1}{2},\dots -\frac{1}{N},0,\frac{1}{N}\dots \frac{N-1}{2N}. $$ where $\hbar$ is the dimensionless Planck's constant (can be related the physical one by a factor of $L/\tau$). The quantized Hamiltonian reads $$ \large H(t) = \frac{\hat{p}^2}{2}-K\frac{\hat{q}^2}{2}\sum_{i}\delta(t-i). $$ \` As a final step, we consider a fixed phase-space area, defined according to $$ \hbar N = (2\pi)\cdot \text{Constant}=2\pi, $$ (for the semi-classical limit this is just the Bohr-Sommerfeld approximation), and redefine: $$ \hat{p}\rightarrow \hbar{p}, q\rightarrow \frac{2\pi}{N}\hat{q}. $$ This final transformation results in the Hamiltonian $$ \large \frac{H_N(t)}{\hbar} = 2\pi \left(\frac{\hat{p}^2}{2N}-K\frac{\hat{q}^2}{2N}\sum_{i}\delta(t-i)\right). $$ \` ## References \[1]: [M. S. Rudner, N. H. Lindner. "The Floquet Engineer's Handbook". arXiv:2003.08252 \[cond-mat.mes-hall\] (2020)](https://arxiv.org/abs/2003.08252) \[2]: [M. D. Porter, I. Joseph, "Impact of dynamics, entanglement, and Markovian noise on the fidelity of few-qubit digital quantum simulation". Journal of Plasma Physics 91.1 (2025)](https://arxiv.org/abs/2206.04829) \[3]: [Andrea Pizzamiglio, Su Yeon Chang, Maria Bondani, Simone Montangero, Dario Gerace, Giuliano Benenti. "Dynamical localization simulated on actual quantum hardware." Entropy 23, 654 (2021).](https://arxiv.org/abs/2105.10813) \[4]: [Georgeot, B. and Shepelyansky, D.L., 2001. "Exponential gain in quantum computing of quantum chaos and localization". Physical Review Letters, 86(13), p.2890.](https://arxiv.org/pdf/quant-ph/0010005) # Quantum Simulation of Linear Kinetic Plasma Models Source: https://docs.classiq.io/explore/applications/plasma/vlasov_ampere/vlasov_ampere Open this notebook in GitHub to run it yourself This demonstration is based on the paper *[Encoding of Linear Kinetic Plasma Problems in Quantum Circuits via Data Compression](https://arxiv.org/abs/2403.11989)* \[[1](#kineticplasma)], and was created in collaboration with its authors. We explore how quantum algorithms can be used to solve a **linearized kinetic model of plasma**, focusing on a simplified **Vlasov-Ampère system** in one spatial dimension with an external electric field. Plasmas - often called the **fourth state of matter** - are ionized gases consisting of charged particles. They exhibit rich collective behavior due to long-range electromagnetic interactions. Understanding and simulating plasmas is critical in many fields, especially in **nuclear fusion**, where plasma must be confined and controlled inside reactors such as **tokamaks** \[[2](#tok)]. Fusion energy aims to replicate the Sun's energy source on Earth. In magnetic confinement fusion (e.g., tokamaks), charged particles spiral along magnetic field lines. To predict plasma behavior in such systems, physicists use **kinetic equations**, which describe the full distribution of particle positions and velocities in phase space. However, these models are **nonlinear** and **high-dimensional**, making simulations extremely expensive. To make progress, scientists often **linearize** the models around a known steady-state. ## The Vlasov-Ampère System We consider a **linearized 1D Vlasov-Ampère system** with an external driving current. This system governs how perturbations to a background plasma evolve in phase space under the influence of electric fields. The equations are: $$ i \omega_0 g(x, v) - \zeta_{bc} \, v \, \partial_x g(x, v) - \partial_v f_0(v) E(x) = 0 $$ $$ i \omega_0 E(x) + \int v g(x,v) \, dv = j(x) $$ # ## Physical Meaning and Assumptions * **$g(x,v)$** is the first-order perturbation of the distribution function from equilibrium. It captures how particles deviate from their steady-state behavior: $f(x, v, t) = f_0(v) + g(x,v) e^{-i \omega_0 t}$. Here we assume the background is homogeneous and so $f_0(v) = \frac{n_0}{\sqrt{2\pi T}} \exp\left(-\frac{v^2}{2T}\right)$. * **$E(x)$** is the (complex) electric field. * **$j(x)$** is a known source term (e.g. external driving or antenna). * **$\zeta_{bc}$** encodes **non-reflecting boundary conditions**: * $\zeta_{bc} = 1$ for outgoing waves. * $\zeta_{bc} = 0$ for incoming waves. * All variables in the equation were normalized such that we get dimensionless equations (see more details in the paper). *** Discretizing the variables $x$ and $v$ leads to very large linear systems. Upon discretization, they form a structured linear system suitable for **block encoding**, making them compatible with quantum linear solvers. In the next sections, we will show how to construct block encoding for the equations using classiq, then plug it to a linear solver (QSVT in this case) to solve a certain toy problem. We emphasize that due to the dependence of the linear system solver on the condition number - $O(\kappa \log(\frac{1}{\epsilon}))$, and the dependece of the condition number in the grid size, we don't expect any quantum advantage for the 1D system. However, generalizing for higher dimensional systems should be straightforward thanks to the high level modeling approach. ## Block Encoding of the Linear System image.png In the following section we describe how to assemble the block encoding of the equations using Classiq."Notice that in order to have a simulatable circuit, sometimes we use non-scallable functions that have better measures for small number of qubits - these are the arbitrary state preparation and amplitude assignment functions. However, we describe how to replace them with scalable alternatives. First, choose the parameters of the problem: ```python theme={null} !pip install -qq "classiq[qsp]" ``` ```python theme={null} import numpy as np from classiq import * ``` ```python theme={null} # plasma parameters Temperature = 1 N = 1 # simulate homogenous plasma - n(x) = 1 # source parameters DS = 3 X_0 = 50 W_0 = 0.8 # grid parameters X_MIN = 0 X_MAX = 100 V_MIN = -4 V_MAX = 4 N_X = 3 N_V = 3 DX = (X_MAX - X_MIN) / (2**N_X - 1) DV = (V_MAX - V_MIN) / (2**N_V - 1) ``` # ## Data Encoding We define a quantum struct that will define the mapping of the coordinates to the effective block-encoded matrix of the problem: ```python theme={null} class BEData(QStruct): v: QNum[N_V, SIGNED, 0] x: QNum[N_X] E: QBit ``` `v` holds the velocity coordinate as a signed number, as the velocity can hold negative values as well. `x` holds the position coordinates. It will encode a non-negative number in the range $[0, x_{max}]$ The value `E` switches between the encoding of $g(x, v)$ (for `E=0`) and $E(x)$ (for `E=1`, `v=0`). Notice that the values `E=1`, `v!=0` are redundant (in fact we could eliminate them through the projector of the block encoding, but we keep them as they don't change the reuslt). # ### Block Struct We also add additional struct for the block encoding. It will hold additional variables that are required by the encoding process through - for adding block encodings (lcu), eliminating parts of existing block encodings (flag) or for diagonal block encoding (ind). ```python theme={null} class BEBlock(QStruct): flag: QBit zeta_flag: QBit block_flag: QBit ind: QBit lcu1: QBit lcu2: QBit ``` Finally, the following struct will hold both defined structs: ```python theme={null} class BE(QStruct): data: BEData block: BEBlock ``` *** Now we are ready to block encode all parts of the equation. We show how to block encode each part of them, then combine them with Linear Combination of Unitaries. Let us look again on the equations: $$ i \omega_0 g(x, v) - \zeta_{bc} \, v \, \partial_x g(x, v) - \partial_v f_0(v) E(x) = 0 $$ $$ i \omega_0 E(x) + \int v g(x,v) \, dv = j(x) $$ The matrix we encode will have a row for each $g$'s degree of freedom, and a row for each $E$ dof. The columns represent the participating terms in each equation. # ## Block Encoding: Advective Term Notice that the advective term $\zeta_{bc} \, v \, \partial_x g(x, v)$ is in the upper left corner of the block encoded matrix (as it only involves $g$). Ignoring $\zeta_{bc}$ part, the term is a tensor product of 2 block encodings - a diagonal $v$ encoding, and a derivative term. # ### Derivative Term The derivative term consists of normal finite-difference rule intermediate x points. At the boundaries of $x$, the derivative is according to a 2nd order interpolating polynomial (as described in the paper Eqs.19). We decompose it as a sum of 2 matrices: $$ \frac{1}{2\Delta x}\begin{pmatrix} -3 & 4 & -1 & 0 & \cdots & 0 \\ -1 & 0 & 1 & 0 & \cdots & 0 \\ 0 & -1 & 0 & 1 & \cdots & 0 \\ \vdots & \ddots & \ddots & \ddots & \ddots & \vdots \\ 0 & \cdots & 0 & -1 & 0 & 1 \\ 0 & \cdots & 0 & 1 & -4 & 3 \end{pmatrix} = \frac{1}{2\Delta x}\begin{pmatrix} 0 & 1 & & & \\ -1 & 0 & 1 & & \\ & -1 & 0 & \ddots & \\ & & \ddots & \ddots & 1 \\ & & & -1 & 0 \end{pmatrix} + \frac{1}{2\Delta x}\begin{pmatrix} -3 & 3 & -1 & 0 & \cdots & 0 \\ 0 & 0 & 0 & \cdots & \cdots & 0 \\ \vdots & & & \vdots \\ \vdots & & & \vdots \\ 0 & 0 & 0 & 0 & \cdots & 0 \\ 0 & 0 & 0 & 1 & -3 & 3 \end{pmatrix} $$ The first matrix is a 1D x-derivative with dirichlet boundary conditions. The 2 diagonals are combined using LCU of modular adders, where the overfolwing terms are eliminated using a flag qubit. ```python theme={null} # derivative along x without boundary conditions @qfunc def derivative_dirichlet_be(x: QNum, flag: QBit, lcu_q: QBit): extended_qnum = QNum() within_apply( lambda: bind([x, flag], extended_qnum), lambda: lcu( [1, -1], [ lambda: inplace_add(-1, extended_qnum), lambda: inplace_add(1, extended_qnum), ], lcu_q, ), ) ``` For the boundary conditions matrix, we use a flag for applying it only on the first and last row. Additionaly, a control on the MSB of the `x` variable to take care for both edges of the system. It is assumed that the size of `x` in qubits is at least 2. ```python theme={null} BC_VALUES = 0.5 * np.array([-3, 3, -1, 0]) BC_NORM = np.linalg.norm(BC_VALUES) BC_VALUES = BC_VALUES / BC_NORM ``` derivative along x on the boundaries: ```python theme={null} from classiq.qmod.symbolic import pi @qfunc def prepare_bounday_x(x: QArray): inplace_prepare_amplitudes(BC_VALUES, 0.0, x[0:2]) @qfunc def derivative_boundary_min_be(x: QNum, flag: QBit): invert(lambda: prepare_bounday_x(x)) flag ^= x != 0 @qfunc def derivative_boundaries_be(x: QArray, flag: QBit): within_apply( lambda: control( x[x.len - 1], lambda: apply_to_all(X, x[0 : x.len - 1]) ), # flip row order lambda: [ control(x[x.len - 1], lambda: phase(pi)), # phase of -1 derivative_boundary_min_be(x[0 : x.len - 1], flag), ], ) ``` Combine matrices to get the block-encoding of the derivative term: ```python theme={null} @qfunc def derivative_be(x: QNum, flag: QBit, lcu1: QBit, lcu2: QBit): lcu( [1, BC_NORM], [ lambda: derivative_dirichlet_be(x, flag, lcu2), lambda: derivative_boundaries_be(x, flag), ], lcu1, ) ``` # ### Diagonal $v$ Term We use the `assign_amplitude_table` function, that performs the operation $|v\rangle|0\rangle_{ind} \rightarrow |v\rangle(f(v)|1\rangle_{ind} + \sqrt{1-f^2(v)}|0\rangle_{ind})$, which is exactly a diagonal block encoding, where the additional indicator part of the block qubits. To save qubits we use implementation for arbitrary $f(v)$, which is not scallable to large variable sizes. However, it is possible to perform it with a linear scaling in the number of qubits, using a primitive that block encodes $f(v) = \sqrt{\frac{v}{v_{max}}}$ to the amplitude, using quantum comparator. To achieve the function $f(v)=\frac{v}{v_{max}}$, one can simply take the square of the block encoding, using qsvt or block encoding multiplication. For more details, see the [qsvt notebook](https:https://github.com/Classiq/classiq-library/blob/main/functions/qmod_library_reference/classiq_open_library/qsvt/qsvt.ipynb). ```python theme={null} @qfunc def v_be(v: QNum, ind: QBit): assign_amplitude_table(lookup_table(lambda n: n / (2 ** (v.size - 1)), v), v, ind) ind ^= 1 ``` # ### Non-Reflecting Boundary Conditions The $\zeta$ term is enforcing non-reflecting boundary conditions, not allowing incoming waves to the system: $$ \xi_{\mathrm{bc}} \, |x,v\rangle = \left( \begin{cases} 0, & x = 0,\; v > 0,\\[4pt] 0, & x = 2^{n_x}-1,\; v < 0,\\[4pt] 1, & \text{otherwise} \end{cases} \right) |x,v\rangle . $$ ```python theme={null} @qfunc def zeta_be(x: QNum, v: QNum, flag: QBit): flag ^= (x == 0) & (v > 0) flag ^= (x == (2**x.size - 1)) & ( v <= 0 ) # instead of doing 'or', take advantage of the mutual exclusiveness of the conditions ``` Combine to the advective term block encoding. Since each block encoding uses different block qubits, it is possible to create their multiplication without additional block qubits. ```python theme={null} @qfunc def advective_be(be: BE): derivative_be(be.data.x, be.block.flag, be.block.lcu1, be.block.lcu2) v_be(be.data.v, be.block.ind) zeta_be(be.data.x, be.data.v, be.block.zeta_flag) be.block.block_flag ^= be.data.E # eliminate the lower right block ``` Keep track of the normalization factor of each part of the block encoding: ```python theme={null} DERIVATIVE_TERM_FACTOR = 2 * (1 + BC_NORM) ADVECTIVE_TERM_FACTOR = DERIVATIVE_TERM_FACTOR * (V_MAX / (2 * DX)) ``` # ## Block Encoding: Off Diagonal Terms Block encode the force and current terms, corresponding to the upper right and lower left blocks. # ### Loading the Force Term In our setting $$ \partial_v f_0(v) E(x) = \frac{v}{\sqrt{2 \pi}} \exp \left(- \frac{v^2}{2 } \right)E(x) $$ It is a column vector in the matrix, and to load it we have several options. One is to first load one of them ($v$ or $H(v))$ as a state, then use amplitude loading of the other one to create a block encoding of the multiplication. A second option that we use here is to directly prepare the state of the multiplication. The advantage is a better scaling factor. It can be performed as suggested [here](https://github.com/Classiq/classiq-library/blob/main/community/paper_implementation_project/quantum_state_preparation_without_coherent_arithmetic/stateprep_guassian_using_qsvt.ipynb) \[[3](#qsvtprep)] (though for this specific method there would still be a scaling factor issue). For simplicity we use here the general state preparation method (which is not scallable). Finally, we zero the columns for which `v != 0`, beacuse we want only columns that encode the field $E$. ```python theme={null} from classiq.qmod.symbolic import sqrt v_amplitudes = np.linspace(-1, 1 - 2 ** (-N_V + 1), 2**N_V) * V_MAX v_amplitudes = np.roll(v_amplitudes, len(v_amplitudes) // 2) # adjust to signed number v_H_amplitudes = ( v_amplitudes * np.exp(-(v_amplitudes**2) / (2 * Temperature)) / (np.sqrt(2 * np.pi * Temperature)) ) @qfunc def load_v_H_vector(v: QNum): inplace_prepare_amplitudes(-v_H_amplitudes / np.linalg.norm(v_H_amplitudes), 0, v) @qfunc def force_term_be(v: QNum, flag: QBit): flag ^= v != 0 load_v_H_vector(v) ``` # ### Current Term The current term is an integration, represented by row multiplication: $$ \int v g(x,v) \, dv $$ To load v, a linear state-preparation is enough. This can be done using the method in \[[4](#linearprep)]. Because we only have equations with this term for the $E$ degrees of freedom, we zero columns where `v != 0`. ```python theme={null} @qfunc def load_v_vector(v: QNum): inplace_prepare_amplitudes(v_amplitudes / np.linalg.norm(v_amplitudes), 0, v) @qfunc def current_term_be(v: QNum, flag: QBit): invert(lambda: load_v_vector(v)) flag ^= v != 0 ``` As both terms are normalized and are located in different off-diagonals, there is no need for LCU here, and we can directly use `control-else` mechanism on the `E` variable. We also need to balance both terms to be on the same scaling (both are currently normalized according to the norm of each term), that is done using the function `equalize_amplitude`. ```python theme={null} from classiq.qmod.symbolic import subscript FORCE_TERM_FACTOR = np.linalg.norm(v_H_amplitudes) CURRENT_TERM_FACTOR = np.linalg.norm(v_amplitudes) * DV OFF_DIAG_FACTOR = max(FORCE_TERM_FACTOR, CURRENT_TERM_FACTOR) @qfunc def equalize_amplitude(E_field: QNum, ind: QBit, ratio: float): """ Multiply amplitude of |E=1> by ratio, do nothing for |E=0> if ratio <=1 Multiply amplitude of |E=0> by 1/ratio, do nothing for |E=1> if ratio >1 """ amplitudes = np.array([ratio, 1]) amplitudes /= max(amplitudes) assign_amplitude_table(amplitudes, E_field, ind) ind ^= 1 # the loaded function is on the |ind=1> state, change to |ind=0> @qfunc def off_diag_be(be: BE): X(be.data.E) control( be.data.E == 0, # can use the same flag qubit because of the mutual exclusivity lambda: force_term_be(be.data.v, be.block.flag), lambda: current_term_be(be.data.v, be.block.flag), ) # re-weight the diagonals - decrease the term with smaller factor equalize_amplitude(be.data.E, be.block.ind, FORCE_TERM_FACTOR / CURRENT_TERM_FACTOR) ``` # ## Full Block Encoding The full block encoding is just a combination of all the terms using LCU. Notice that the diagonal term which has the same coefficient both for $E$ and $g$ is achieved by simply using the `IDENTITY` operation (= "do nothing"). ```python theme={null} class BELcu(QStruct): be: BE lcu: QNum[2] LCU_COEFFS = np.array( [ -ADVECTIVE_TERM_FACTOR, # advective_be OFF_DIAG_FACTOR, # off_diag_be 1j * W_0, # diagonal (of the full unitary) 0, ] ) BE_NORM_FACTOR = np.sum(np.abs(LCU_COEFFS)) @qfunc def full_be(be_lcu: BELcu): lcu( LCU_COEFFS, [ lambda: advective_be(be_lcu.be), lambda: off_diag_be(be_lcu.be), lambda: IDENTITY(be_lcu.be), ], be_lcu.lcu, ) ``` ```python theme={null} @qfunc def main(state: Output[BELcu]): allocate(state) full_be(state) qprog_be = synthesize(main) show(qprog_be) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/35Yq9mIowQz95KaGDEBp0ukpuzS ``` # ## Extracting the Block Encoding Using Quantum Simulation In order to get the resulting block encoded matrix using statevector simulation, we add a reference variable that duplicates the input variable before the application of the block encoding, thus surving as a marker for the input state before the application of the block encoding. ```python theme={null} @qfunc def prepare_ref(data: QNum, data_ref: Output[QNum]): """ create a refernce variable such that it will 'tag' the input states, and it will be possible to measure the block encoded matrix """ hadamard_transform(data) # 'duplicate' data to the refernce, such that variables are entangled data_ref |= data @qfunc def main( data: Output[QNum[BEData.num_qubits]], data_ref: Output[QNum], block: Output[QNum] ): be_lcu = BELcu() allocate(be_lcu) prepare_ref([be_lcu.be.data], data_ref) full_be(be_lcu) bind(be_lcu, [data, block]) ``` Compile the program: ```python theme={null} qprog_be_ref = synthesize( main, preferences=Preferences(optimization_level=0), constraints=Constraints(optimization_parameter="width"), ) print(f"Num qubits: {qprog_be_ref.data.width}") show(qprog_be_ref) ``` **Output:** ``` Num qubits: 25 Quantum program link: https://platform.classiq.io/circuit/35YqDzTGqdlaDAhsSIdsrKdK3LR ``` Execute using state-vector simulation. To reduce resulting state-vector, filter out results where the block is different than 0. ```python theme={null} # Post-select block == 0 on the statevector simulator. res_be = calculate_state_vector(qprog_be_ref, filters={"block": 0}) ``` show the resulting block encoding: ```python theme={null} import matplotlib.pyplot as plt def extract_block_encoding(res): """ Extract the block encoded matrix from the execution results and plot it """ df = res df = df[(df.block == 0) & (np.abs(df.amplitude) > 1e-12)].copy() global_phase = np.angle(df.amplitude.iloc[0]) df["amplitude_real"] = np.real(df.amplitude / np.exp(1j * global_phase)) df["amplitude_imag"] = np.imag(df.amplitude / np.exp(1j * global_phase)) full_mat = 0 for i, s in enumerate(["real", "imag"]): matrix = df.pivot_table( index="data", columns="data_ref", values=f"amplitude_{s}", fill_value=0 ) # Reindex rows and columns to ensure full coverage full_index = np.arange(2 ** (BEData.num_qubits)) matrix = matrix.reindex(index=full_index, columns=full_index, fill_value=0) # Convert to numpy array, add the normalization factor matrix_np = matrix.to_numpy() * np.sqrt(2 ** (BEData.num_qubits)) full_mat += matrix_np.astype("complex128") * [1, 1j][i] full_mat = full_mat return full_mat mat_be = extract_block_encoding(res_be) plt.spy(mat_be) ``` **Output:** ``` ``` output ## Linear Solver Using QSVT Matrix Inversion Now we use the block encoding to solve the equation. We only left to encode the external source we want to solve for as initial state, and apply matrix inversion on the it. Because the condition number of the resulting matrix is high for our simulation, we limitied ourselves for small grid size, on which the problem solution is not physical, though we show it for educational purposes. # ## Initial State Loading We solve for a source term localized at $x_0$, according to the following formula: $$ j(S)= i \omega_0 e^{- \frac{(x - x_0)^2}{2 (\Delta_S)^2}} $$ ```python theme={null} x_coordinates = np.linspace(0, X_MAX, 2**N_X) j_amplitudes = W_0 * np.exp( -((x_coordinates - X_0) ** 2) / (2 * DS**2) ) # ignore the i phase SOURCE_NORM_FACTOR = np.linalg.norm(j_amplitudes) j_amplitudes /= SOURCE_NORM_FACTOR @qfunc def prepare_source_term(data: BEData): data.E ^= 1 # source term has non-zero values only for E inplace_prepare_amplitudes(j_amplitudes, 0, data.x) ``` # ## QSVT Phases Calculation Solve an optimization problem for the qsvt polynomial, and find the corresponding QSVT phases: ```python theme={null} import matplotlib.pyplot as plt from classiq.applications.qsp import qsp_approximate, qsvt_phases svd = np.linalg.svd(mat_be)[1] w_min, w_max = min(svd), max(svd) DEGREE = 180 # Relaxed constraint scale = 0.95 def target_function(x): return scale * (w_min) / x pcoefs, opt_res = qsp_approximate( target_function, degree=DEGREE, parity=1, interval=[w_min, w_max], plot=True, bound=0.95, ) INVERSION_PHASES = qsvt_phases(pcoefs) print(f"min singular value: {w_min}, max_singular value: {w_max}") print(f"Max relative error value: {opt_res}") ``` output **Output:** ``` min singular value: 0.01856162144286599, max_singular value: 0.8282275541075902 Max relative error value: 0.02046509592876211 ``` # ## Full QSVT Circuit image.png The QSVT projector is just checking whether the entire block is 0. ```python theme={null} @qfunc def projector(block: QNum, res: QBit): res ^= block == 0 @qfunc def main(data: Output[BEData], block: Output[QNum]): be_lcu = BELcu() qsvt_aux = QBit() allocate(be_lcu) allocate(qsvt_aux) prepare_source_term(be_lcu.be.data) qsvt_inversion( INVERSION_PHASES, lambda aux: projector([be_lcu.be.block, be_lcu.lcu], aux), lambda: full_be(be_lcu), qsvt_aux, ) bind([be_lcu, qsvt_aux], [data, block]) ``` ```python theme={null} preferences = Preferences( transpilation_option="none", debug_mode=False, # remove this to get better circuit visualization, on the expense of longer synthesis time timeout_seconds=20 * 60, qasm3=True, optimization_level=1, ) qprog_qsvt = synthesize( main, preferences=preferences, constraints=Constraints(optimization_parameter="width"), ) print(f"Num qubits: {qprog_qsvt.data.width}") show(qprog_qsvt) # Post-select block == 0 on the statevector simulator. res_qsvt = calculate_state_vector(qprog_qsvt, filters={"block": 0}) ``` **Output:** ``` Num qubits: 19 Quantum program link: https://platform.classiq.io/circuit/35YqVgYpFCZf3OMSykrlxGj5ni5 ``` # ## Post-Process ```python theme={null} df = res_qsvt df.head(10) ``` | | data.v | data.x | data.E | block | amplitude | magnitude | phase | probability | bitstring | | - | ------ | ------ | ------ | ----- | ------------------ | --------- | ------ | ----------- | ------------------- | | 0 | 1 | 4 | 0 | 0 | 0.013605+0.000471j | 0.01 | 0.01π | 0.000185 | 0000000000001000010 | | 1 | 1 | 3 | 0 | 0 | 0.013603-0.000472j | 0.01 | -0.01π | 0.000185 | 0000000000000110010 | | 2 | -4 | 3 | 0 | 0 | 0.007906-0.002675j | 0.01 | -0.10π | 0.000070 | 0000000000000111000 | | 3 | -4 | 4 | 0 | 0 | 0.007903+0.002675j | 0.01 | 0.10π | 0.000070 | 0000000000001001000 | | 4 | -3 | 4 | 0 | 0 | 0.005077+0.002067j | 0.01 | 0.12π | 0.000030 | 0000000000001001010 | | 5 | -3 | 3 | 0 | 0 | 0.005076-0.002069j | 0.01 | -0.12π | 0.000030 | 0000000000000111010 | | 6 | 2 | 4 | 0 | 0 | 0.003209-0.000324j | 0.00 | -0.03π | 0.000010 | 0000000000001000100 | | 7 | 2 | 3 | 0 | 0 | 0.003206+0.000325j | 0.00 | 0.03π | 0.000010 | 0000000000000110100 | | 8 | 0 | 2 | 1 | 0 | 0.001109+0.000937j | 0.00 | 0.22π | 0.000002 | 0000000000010100000 | | 9 | 0 | 3 | 1 | 0 | 0.001104+0.049842j | 0.05 | 0.49π | 0.002485 | 0000000000010110000 | Here we plot the resulting $E$ vector. ```python theme={null} # filter only the E results df_E = df[ (df["data.E"] == 1) & (df["data.v"] == 0) & (np.abs(df.amplitude) > 1e-14) ].sort_values(by="data.x") x_values = df_E["data.x"] * DX E_values = df_E.amplitude * 1 / min(svd) * SOURCE_NORM_FACTOR / (BE_NORM_FACTOR * scale) plt.plot(x_values, np.abs(E_values)) plt.xlabel("$x$") plt.ylabel("$|E(x)|$") ``` **Output:** ``` Text(0, 0.5, '$|E(x)|$') ``` output # ### Compare to a Classical Solver ```python theme={null} import numpy as np def get_advective_mat(nx, nv, dx, v_max, cyclic=False): dx_mat = np.diag(np.ones(2**nx - 1), k=1) - np.diag(np.ones(2**nx - 1), k=-1) boundary = np.pad([-3, 4, -1], (0, dx_mat.shape[1] - 3)) dx_mat[0, :] = boundary dx_mat[-1, :] = -np.flip(boundary) v_amplitudes = np.linspace(-1, 1 - 2 ** (-nv + 1), 2**nv) * v_max v_amplitudes = np.roll(v_amplitudes, len(v_amplitudes) // 2) x_values = np.arange(2**nx) max_x = 2**nx - 1 xi = ( 1 - np.kron(x_values == 0, v_amplitudes > 0) - np.kron(x_values == max_x, v_amplitudes <= 0) ) advective = np.kron(dx_mat / (2 * dx), np.diag(v_amplitudes)) advective[xi == 0] = 0 return advective def get_off_diag_mat(nx, nv, dv, v_max, temp): v_amplitudes = np.linspace(-1, 1 - 2 ** (-nv + 1), 2**nv) * v_max v_amplitudes = np.roll(v_amplitudes, len(v_amplitudes) // 2) v_H_amplitudes = ( v_amplitudes * np.exp(-(v_amplitudes**2) / (2 * temp)) / (np.sqrt(2 * np.pi * temp)) ) vec_col = v_H_amplitudes vec_row = v_amplitudes * dv n = len(vec_col) dim_x = 2**nx dim_v = 2**nv size = dim_x * dim_v A = np.zeros((size, size)) B = np.zeros((size, size)) for i in range(dim_x): A[i * dim_v : (i + 1) * dim_v, i * dim_v] = vec_col B[i * dim_v, i * dim_v : (i + 1) * dim_v] = vec_row # Compose off-diagonal block matrix top = np.hstack((np.zeros_like(A), -A)) bottom = np.hstack((B, np.zeros_like(B))) off_diag = np.vstack((top, bottom)) return off_diag def get_block_encoding(nx, nv, v_max, x_max, w0, temp): dx = x_max / (2**nx - 1) dv = (2 * v_max) / (2**nv - 1) mat_size = 2 ** (nx + nv + 1) advective = get_advective_mat(nx, nv, dx, v_max) off_diag = get_off_diag_mat(nx, nv, dv, v_max, temp) return 1j * w0 * np.eye(mat_size) - np.kron(np.diag([1, 0]), advective) + off_diag def get_source_term(nx, nv, x0, x_max, w0, ds): x_coordinates = np.linspace(0, x_max, 2**nx) j_amplitudes = 1j * w0 * np.exp(-((x_coordinates - x0) ** 2) / (2 * ds**2)) v_0 = np.zeros(2**nv) v_0[0] = 1 source = np.kron([0, 1], np.kron(j_amplitudes, v_0)) return source def solve_problem( nx, nv, w0, x_max=X_MAX, v_max=V_MAX, temp=Temperature, x0=X_0, ds=DS ): be = get_block_encoding(nx, nv, v_max, x_max, w0, temp) source = get_source_term(nx, nv, x0, x_max, w0, ds) sol = np.linalg.solve(be, source) v_0 = np.zeros(2**nv) v_0[0] = 1 E_filter = np.kron([0, 1], np.kron(np.ones(2**nx), v_0)).astype("bool") E = sol[E_filter] return be, E ``` ```python theme={null} mat_classical, E_classical = solve_problem(N_X, N_V, W_0) x_coordinates = np.linspace(0, X_MAX, 2**N_X) plt.plot(x_coordinates, np.abs(E_classical)) plt.xlabel("x") plt.ylabel("|E(x)|") ``` **Output:** ``` Text(0, 0.5, '|E(x)|') ``` output ## References \[1]: Novikau, I., Dodin, I.Y., & Startsev, E.A. (2024). *Encoding of linear kinetic plasma problems in quantum circuits via data compression*. **Journal of Plasma Physics**, 90(4). [https://doi.org/10.1017/S0022377824000795](https://doi.org/10.1017/S0022377824000795) \[2]: [Tokamak (Wikipedia)](https://en.wikipedia.org/wiki/Tokamak). \[3]: McArdle, S., Gilyén, A., & Berta, M. (2025). *Quantum state preparation without coherent arithmetic*. arXiv:2210.14892 \[quant-ph]. [https://arxiv.org/abs/2210.14892](https://arxiv.org/abs/2210.14892) \[4]: Gonzalez-Conde, J., Watts, T. W., Rodriguez-Grasa, P., & Sanz, M. (2024). *Efficient quantum amplitude encoding of polynomial functions*. **Quantum**, 8, 1297. [https://doi.org/10.22331/q-2024-03-21-1297](https://doi.org/10.22331/q-2024-03-21-1297) # Quantum Simulation of Linear Kinetic Plasma Models (Qiskit) Source: https://docs.classiq.io/explore/applications/plasma/vlasov_ampere/vlasov_ampere_qiskit Open this notebook in GitHub to run it yourself \*\*This notebook implements the same model that appears under [`vlasov_ampere.ipynb`](https://github.com/Classiq/classiq-library/blob/main/applications/plasma/vlasov_ampere/vlasov_ampere.ipynb) example, using qiskit. This is the code used to collect the benchmarking data in [the paper](https://arxiv.org/abs/2507.22257)\*\*. In particular, it contains: Definition of a qiskit code for the block-encoding, verification of the block-encoding functionality, and definition of QSVT step with qiskit, operating on the block-encoding unitary. The results were obtained using Qiskit version 2.1. 1. ```python theme={null} !pip install -qq qiskit==2.1.1 ``` ```python theme={null} import numpy as np from qiskit import QuantumCircuit, QuantumRegister, transpile from qiskit.circuit.library import StatePreparation from qiskit.circuit.library.standard_gates import XGate from qiskit.synthesis import synth_qft_full from sympy import fwht ``` ## Adding Inplace Adder and assign\_amplitudes with Qiskit These are missing in Qiskit library. ```python theme={null} # Creating an inplace adder class DraperQFTAdderConstant(QuantumCircuit): def __init__( self, num_state_qubits: int, constant: int, name: str = "DraperQFTAdderConst" ) -> None: # Create the quantum register qr_a = QuantumRegister(num_state_qubits, name="a") super().__init__(qr_a, name=name) # Apply the QFT self.append(synth_qft_full(num_state_qubits, do_swaps=False).to_gate(), qr_a) # Add the constant by applying controlled rotations for qubit in range(num_state_qubits): angle = (constant % (2 ** (qubit + 1))) * np.pi / (2**qubit) self.p(angle, qr_a[qubit]) # Apply the inverse QFT self.append( synth_qft_full(num_state_qubits, do_swaps=False).inverse().to_gate(), qr_a ) ``` ```python theme={null} # Creating assign amplitudes def get_graycode(size: int, i: int) -> int: if i == 2**size: return get_graycode(size, 0) return i ^ (i >> 1) def get_controller(size: int, i: int) -> int: return (get_graycode(size, i) ^ get_graycode(size, i + 1)).bit_length() - 1 def get_graycode_angles_wh(size, angles): transformed_angles = fwht(np.array(angles) / 2**size) return [transformed_angles[get_graycode(size, j)] for j in range(2**size)] def assign_amplitudes_qs(amps, x, ind): qc = QuantumCircuit(x, ind, name="assign amplitudes") size = len(amps).bit_length() - 1 angles = 2 * np.arcsin(amps) gray_code_angles = np.array(get_graycode_angles_wh(size, angles)).astype(float) gray_code_controllers = [get_controller(size, i) for i in range(2**size)] for i in range(2**size): qc.ry(gray_code_angles[i], ind) qc.cx(x[gray_code_controllers[i]], ind) return qc.to_gate() ``` ## Setting Problem Parameters ```python theme={null} # plasma parameters Temperature = 1 N = 1 # simulate homogenous plasma - n(x) = 1 # source parameters DS = 3 X_0 = 50 W_0 = 0.8 # grid parameters X_MIN = 0 X_MAX = 100 V_MIN = -4 V_MAX = 4 N_X = 3 N_V = 3 DX = (X_MAX - X_MIN) / (2**N_X - 1) DV = (V_MAX - V_MIN) / (2**N_V - 1) ``` ## Functions for Getting Quantum Circuits ("Gates") to Construct the Block Encoding # ## Circuits for the Advective Part ```python theme={null} BC_VALUES = 0.5 * np.array([-3, 3, -1, 0]) BC_NORM = np.linalg.norm(BC_VALUES) BC_VALUES = BC_VALUES / BC_NORM lcu_angle = 2 * np.arccos(np.sqrt(BC_NORM / (1 + BC_NORM))) ``` ```python theme={null} def get_derivative_dirichlet_be(x, flag, lcu): qc = QuantumCircuit(x, flag, lcu, name="derivative_dirichlet_be") qc.h(lcu[0]) qc.z(lcu[0]) qc.append( DraperQFTAdderConstant(num_state_qubits=len(x) + 1, constant=-1) .to_gate() .control(1, ctrl_state=0), [lcu] + x[:] + [flag], ) qc.append( DraperQFTAdderConstant(num_state_qubits=len(x) + 1, constant=1) .to_gate() .control(1, ctrl_state=1), [lcu] + x[:] + [flag], ) qc.h(lcu[0]) return qc.to_gate() def get_derivative_boundary_min_be(x, flag): qc = QuantumCircuit(x, flag, name="derivative_boundary_min_be") qc.append(StatePreparation(BC_VALUES).inverse(), x[0:2]) qc.append(XGate().control(len(x), ctrl_state=0), x[:] + [flag]) qc.x(flag) return qc.to_gate() def get_derivative_boundary_max_be(x, flag): qc = QuantumCircuit(x, flag, name="derivative_boundary_max_be") qc.x(x) qc.ry(2 * np.pi, flag) qc.append(get_derivative_boundary_min_be(x, flag), x[:] + [flag]) qc.x(x) return qc.to_gate() def get_derivative_boundaries_be(x, flag): qc_start = get_derivative_boundary_min_be(x[0:-1], flag) qc_end = get_derivative_boundary_max_be(x[0:-1], flag) qc = QuantumCircuit(x, flag, name="derivative_boundaries_be") qc.append(qc_start.control(1, ctrl_state=0), [x[-1]] + x[0:-1] + [flag]) qc.append(qc_end.control(1, ctrl_state=1), [x[-1]] + x[0:-1] + [flag]) return qc.to_gate() def get_derivative_be(x, flag, lcu1, lcu2): qc = QuantumCircuit(x, flag, lcu1, lcu2, name="derivative_be") qc.ry(lcu_angle, lcu1) qc.append( get_derivative_dirichlet_be(x, flag, lcu2).control(1, ctrl_state=1), [lcu1] + x[:] + [flag] + [lcu2], ) qc.append( get_derivative_boundaries_be(x, flag).control(1, ctrl_state=0), [lcu1] + x[:] + [flag], ) qc.ry(-lcu_angle, lcu1) return qc.to_gate() def get_be_zeta(x, v, flag): temp1 = QuantumRegister(1, "temp1") temp2 = QuantumRegister(1, "temp2") qc = QuantumCircuit(x, v, flag, temp1, temp2, name="zeta_bc matrix") qc.append(XGate().control(len(x), ctrl_state=2 ** len(x) - 1), x[:] + [temp1]) qc.cx(v[-1], temp2) qc.ccx(temp1, temp2, flag) qc.cx(v[-1], temp2) qc.append(XGate().control(len(x), ctrl_state=2 ** len(x) - 1), x[:] + [temp1]) qc.append(XGate().control(len(x), ctrl_state=0), x[:] + [temp1]) qc.append(XGate().control(1, ctrl_state=0), [v[-1]] + [temp2]) qc.ccx(temp1, temp2, flag) qc.append(XGate().control(1, ctrl_state=0), [v[-1]] + [temp2]) qc.append(XGate().control(len(x), ctrl_state=0), x[:] + [temp1]) return qc.to_gate() def get_v_be(v, ind): qc = QuantumCircuit(v, ind, name="v dot matrix") N_V = len(v) amplitudes = np.linspace(-1, 1 - 2 ** (-N_V + 1), 2**N_V) amplitudes = np.roll(amplitudes, len(amplitudes) // 2) # adjust to signed number qc.append(assign_amplitudes_qs(amplitudes, v, ind), v[:] + [ind]) qc.x(ind) return qc.to_gate() # The upper left matrix: 6 block terms, 2 aux def get_advective_be(x, v, e, flags): temps = QuantumRegister(2, "temps") qc = QuantumCircuit(x, v, e, flags, temps, name="Full v grad_x") qc.append( get_derivative_be(x, [flags[0]], [flags[1]], [flags[2]]), x[:] + flags[0:3] ) qc.append(get_v_be(v, [flags[3]]), v[:] + [flags[3]]) qc.append(get_be_zeta(x, v, [flags[4]]), x[:] + v[:] + [flags[4]] + temps[:]) qc.cx(e, flags[5]) return qc.to_gate() ``` # ## Circuits for Off-Diagonal Terms ```python theme={null} v_amplitudes = np.linspace(-1, 1 - 2 ** (-N_V + 1), 2**N_V) * V_MAX v_amplitudes = np.roll(v_amplitudes, len(v_amplitudes) // 2) # adjust to signed number v_H_amplitudes = ( v_amplitudes * np.exp(-(v_amplitudes**2) / (2 * Temperature)) / (np.sqrt(2 * np.pi * Temperature)) ) def get_force_term_be(v, flag, v_H_amplitudes): qc = QuantumCircuit(v, flag, name="force_term") qc.append(XGate().control(len(v), ctrl_state=0), v[:] + [flag]) qc.x(flag) qc.append(StatePreparation(-v_H_amplitudes / np.linalg.norm(v_H_amplitudes)), v[:]) return qc.to_gate() def get_current_term_be(v, flag, v_amplitudes): qc = QuantumCircuit(v, flag, name="current_term") qc.append( StatePreparation(v_amplitudes / np.linalg.norm(v_amplitudes)).inverse(), v[:] ) qc.append(XGate().control(len(v), ctrl_state=0), v[:] + [flag]) qc.x(flag) return qc.to_gate() def get_equalize_amplitude(e, ind, ratio): qc = QuantumCircuit(e, ind, name="equalize_amplitude") amplitudes = np.array([ratio, 1]) amplitudes /= max(amplitudes) qc.append(assign_amplitudes_qs(amplitudes, e, ind), e[:] + [ind]) qc.x(ind) return qc.to_gate() def get_off_diag_be(e, v, flag, ind, v_H_amplitudes, v_amplitudes, ratio): qc = QuantumCircuit(e, v, flag, ind, name="off diagonal") qc.x(e) qc.append( get_force_term_be(v, flag, v_H_amplitudes).control(1, ctrl_state=0), [e] + v[:] + [flag], ) qc.append( get_current_term_be(v, flag, v_amplitudes).control(1, ctrl_state=1), [e] + v[:] + [flag], ) qc.append(get_equalize_amplitude(e, ind, ratio), [e] + [ind]) return qc.to_gate() ``` ## Block-Encoding of the Full Matrix ```python theme={null} DERIVATIVE_TERM_FACTOR = 2 * (1 + BC_NORM) ADVECTIVE_TERM_FACTOR = DERIVATIVE_TERM_FACTOR * (V_MAX / (2 * DX)) FORCE_TERM_FACTOR = np.linalg.norm(v_H_amplitudes) CURRENT_TERM_FACTOR = np.linalg.norm(v_amplitudes) * DV OFF_DIAG_FACTOR = max(FORCE_TERM_FACTOR, CURRENT_TERM_FACTOR) ``` ```python theme={null} # Defining a function for 1j factor def get_phase(): q = QuantumRegister(1, "q") qc = QuantumCircuit(q, name="1j phase") qc.p(np.pi, q) qc.rz(-np.pi, q) return qc.to_gate() ``` ```python theme={null} data_size = N_V + N_X + 1 data_qs = QuantumRegister(data_size, "data_qs") block_qs = QuantumRegister(2 + 6 + 2, "block_qs") lcu_amps = np.sqrt( np.array([ADVECTIVE_TERM_FACTOR, OFF_DIAG_FACTOR, W_0, 0]).astype("float") ) lcu_amps /= np.linalg.norm(lcu_amps) qc = QuantumCircuit(data_qs, block_qs) qc.append(StatePreparation(lcu_amps), block_qs[0:2]) qc.append( get_advective_be( data_qs[N_V : data_size - 1], data_qs[0:N_V], [data_qs[data_size - 1]], block_qs[0 + 2 : 6 + 2], ).control(2, ctrl_state=0), block_qs[0:2] + data_qs[N_V : data_size - 1] + data_qs[0:N_V] + [data_qs[data_size - 1]] + block_qs[2:], ) qc.append( get_off_diag_be( [data_qs[data_size - 1]], data_qs[0:N_V], [block_qs[0 + 2]], [block_qs[3 + 2]], v_H_amplitudes, v_amplitudes, FORCE_TERM_FACTOR / CURRENT_TERM_FACTOR, ).control(2, ctrl_state=1), block_qs[0:2] + [data_qs[data_size - 1]] + data_qs[0:N_V] + [block_qs[0 + 2]] + [block_qs[3 + 2]], ) qc.append(get_phase().control(2, ctrl_state=2), block_qs[0:2] + [block_qs[2]]) inv_amps = lcu_amps inv_amps[0] *= -1 # minus sign for the advective term qc.append(StatePreparation(inv_amps).inverse(), block_qs[0:2]) be_qc = qc.to_gate() ``` ```python theme={null} # Scaling factor BE_NORM_FACTOR = ADVECTIVE_TERM_FACTOR + OFF_DIAG_FACTOR + W_0 ``` ## Verification For the given problem size, `N_X, N_V= 3, 3`, we construct the classical matrix $A+i\omega$ that we block-encoded via a quantum circuit $U_{A+i\omega}$. Then for some random initial state $|\psi\rangle$ we verify that indeed: $$ U_{A+i\omega} |\psi\rangle |0\rangle_{\rm block} = (A+i\omega)|\psi\rangle|0\rangle_{\rm block}+{\rm garbage} $$ # ## Classical Matrix ```python theme={null} import numpy as np def get_advective_mat(nx, nv, dx, v_max, cyclic=False): dx_mat = np.diag(np.ones(2**nx - 1), k=1) - np.diag(np.ones(2**nx - 1), k=-1) boundary = np.pad([-3, 4, -1], (0, dx_mat.shape[1] - 3)) dx_mat[0, :] = boundary dx_mat[-1, :] = -np.flip(boundary) v_amplitudes = np.linspace(-1, 1 - 2 ** (-nv + 1), 2**nv) * v_max v_amplitudes = np.roll(v_amplitudes, len(v_amplitudes) // 2) x_values = np.arange(2**nx) max_x = 2**nx - 1 xi = ( 1 - np.kron(x_values == 0, v_amplitudes > 0) - np.kron(x_values == max_x, v_amplitudes <= 0) ) advective = np.kron(dx_mat / (2 * dx), np.diag(v_amplitudes)) advective[xi == 0] = 0 return advective def get_off_diag_mat(nx, nv, dv, v_max, temp): v_amplitudes = np.linspace(-1, 1 - 2 ** (-nv + 1), 2**nv) * v_max v_amplitudes = np.roll(v_amplitudes, len(v_amplitudes) // 2) v_H_amplitudes = ( v_amplitudes * np.exp(-(v_amplitudes**2) / (2 * temp)) / (np.sqrt(2 * np.pi * temp)) ) vec_col = v_H_amplitudes vec_row = v_amplitudes * dv n = len(vec_col) dim_x = 2**nx dim_v = 2**nv size = dim_x * dim_v A = np.zeros((size, size)) B = np.zeros((size, size)) for i in range(dim_x): A[i * dim_v : (i + 1) * dim_v, i * dim_v] = vec_col B[i * dim_v, i * dim_v : (i + 1) * dim_v] = vec_row # Compose off-diagonal block matrix top = np.hstack((np.zeros_like(A), -A)) bottom = np.hstack((B, np.zeros_like(B))) off_diag = np.vstack((top, bottom)) return off_diag def get_block_encoding(nx, nv, v_max, x_max, w0, temp): dx = x_max / (2**nx - 1) dv = (2 * v_max) / (2**nv - 1) mat_size = 2 ** (nx + nv + 1) advective = get_advective_mat(nx, nv, dx, v_max) off_diag = get_off_diag_mat(nx, nv, dv, v_max, temp) return 1j * w0 * np.eye(mat_size) - np.kron(np.diag([1, 0]), advective) + off_diag ``` ```python theme={null} mat_cl = get_block_encoding(N_X, N_V, V_MAX, X_MAX, W_0, Temperature) ``` ```python theme={null} # Random initial state init_data = np.random.rand(2**7) init_data /= np.linalg.norm(init_data) ``` ```python theme={null} block_qs = QuantumRegister(10, "block_qs") data_qs = QuantumRegister(data_size, "data_qs") qc_test = QuantumCircuit(data_qs, block_qs) qc_test.append(StatePreparation(init_data), data_qs[:]) qc_test.append(be_qc, data_qs[:] + block_qs[:]) tqc_test = transpile(qc_test, basis_gates=["u", "cx"], optimization_level=3) print(f"width: {tqc_test.width()}") print(f"depth: {tqc_test.depth()}") print(f"cx: {tqc_test.count_ops()['cx']}") ``` **Output:** ``` width: 17 depth: 237580 cx: 124273 ``` ```python theme={null} from qiskit.quantum_info import Statevector final_sv = Statevector.from_instruction(qc_test) psi_tensor = final_sv.data.reshape([2] * qc_test.width()) ``` ```python theme={null} projected = psi_tensor[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, :] reduced_sv = Statevector(projected.flatten()) ``` ```python theme={null} cl_vector = mat_cl @ init_data ``` We compare the projected state on $|0\rangle_{\rm block}$ with the expected classical result (in absolute value) ```python theme={null} import matplotlib.pyplot as plt plt.plot(np.abs(cl_vector), "o", label="expected state") plt.plot(np.abs(reduced_sv) * BE_NORM_FACTOR, ".", label="projected quantum state") plt.legend(); ``` output We also compute the norm for the difference between the results ```python theme={null} l2_norm = np.linalg.norm(cl_vector - np.array(reduced_sv) * BE_NORM_FACTOR) print(f"The L2-norm: {l2_norm}") assert l2_norm < 1e-10 ``` **Output:** ``` The L2-norm: 1.5013973106103977e-11 ``` ## QSVT Step ```python theme={null} def get_reflect_around_zero(size): qc = QuantumCircuit(size) qc.x(0) qc.h(0) qc.mcx( control_qubits=[k for k in range(1, size)], ctrl_state="0" * (size - 1), target_qubit=[0], ) qc.h(0) qc.x(0) return qc def apply_projector_controlled_phase(qc, phase, block_reg, aux_reg): qc.append(XGate().control(len(block_reg), ctrl_state=0), block_reg[:] + aux_reg[:]) qc.rz(phase, aux_reg) qc.append(XGate().control(len(block_reg), ctrl_state=0), block_reg[:] + aux_reg[:]) def apply_qsvt_step(qc, phase1, phase2, u, data, block, aux, qsvt_aux): qc.append(u, data[:] + block[:] + aux[:]) apply_projector_controlled_phase(qc, phase1, block, qsvt_aux) qc.append(u.inverse(), data[:] + block[:] + aux[:]) apply_projector_controlled_phase(qc, phase2, block, qsvt_aux) ``` ```python theme={null} def get_qsvt_circuit(): data = QuantumRegister(N_X + N_V + 1, "data") block = QuantumRegister(8, "block") aux = QuantumRegister(2, "aux") qsvt_aux = QuantumRegister(1, "qsvt_aux") qsvt_cir = QuantumCircuit(data, block, qsvt_aux, aux) apply_qsvt_step( qsvt_cir, 0.1, # dummy angles 0.2, # dummy angles be_qc, data, block, aux, qsvt_aux, ) return qsvt_cir ``` ```python theme={null} qc_qsvt = get_qsvt_circuit() ``` ```python theme={null} tqc_qsvt = transpile(qc_qsvt, basis_gates=["u", "cx"], optimization_level=3) print(f"width: {tqc_qsvt.width()}") print(f"depth: {tqc_qsvt.depth()}") print(f"cx: {tqc_qsvt.count_ops()['cx']}") ``` **Output:** ``` width: 18 depth: 475033 cx: 248500 ``` # Network Traffic Optimization with QAOA Source: https://docs.classiq.io/explore/applications/telecom/network_traffic_optimization/network_traffic_optimization Open this notebook in GitHub to run it yourself *** ## What Is Network Traffic Optimization? **Network Traffic Optimization** is a critical network management used in Telecommunication: **Input:** Weighted directed graph and a set of demands. **Goal:** Satisfy the demands, while minimizing the latency, without violating the two constraints. 1\. Capacity per edge (unit capacity): $\sum_d Z(d,e) \leq 1$ 2\. Flow conservation for each demand and node The following notebook demonstrates a solution to the network traffic optimization problem, applying the QAOA algorithm. ## 1. Setup ```python theme={null} import math import matplotlib.pyplot as plt import networkx as nx import numpy as np import pandas as pd from scipy.optimize import Bounds, LinearConstraint, milp, minimize from tqdm import tqdm from classiq import * from classiq.execution import ExecutionPreferences, ExecutionSession ``` ```python theme={null} CLASSIQ = { "lime": "#D7F75B", "gray": "#C9CBC0", "black": "#191919", "teal": "#0C7489", "dgray": "#282828", "offw": "#F4F9E9", "cyan": "#109DA3", "pink": "#F43764", } ``` ## 2. Problem Definition We define the graph by introducing six vertices ($A-F$), `V`, and connecting edges, `E`. Three demands are introduced, each with start and terminal nodes; our goal is to find the minimal-latency disjoint routes that connect the start and terminal nodes of all the demands. Disjoint routes do not share a node at each time-step. ```python theme={null} V = ["A", "B", "C", "D", "E", "F"] E_with_lat = [ ("A", "B", 2), ("B", "E", 2), ("A", "C", 3), ("C", "E", 1), ("B", "D", 1), ("D", "F", 2), ("C", "D", 2), ("E", "F", 1), ] demands = [ {"name": "d1", "s": "A", "t": "E"}, {"name": "d2", "s": "B", "t": "F"}, {"name": "d3", "s": "C", "t": "F"}, ] E = [(u, v) for (u, v, _) in E_with_lat] lat_vec = np.array([w for (_, _, w) in E_with_lat], dtype=float) lat = {(u, v): w for (u, v, w) in E_with_lat} edge_ids = [f"e{i+1}" for i in range(len(E))] graph = nx.DiGraph() graph.add_nodes_from(V) graph.add_weighted_edges_from(E_with_lat) E = graph.edges() D = len(demands) m = len(E) print(f"Network: {len(V)} nodes, {m} edges, {D} demands") ``` **Output:** ``` Network: 6 nodes, 8 edges, 3 demands ``` ```python theme={null} Z = np.zeros((D, m)) # The mixer Hamiltonian is effectively X/2 and has eigenvalues -0.5 and + 0. 5. So the difference between minimum and maximum eigenvalues is exactly 1. # This can be rescaled by a global scaling parameter. GlobalScalingParameter = 1 # The constraint Hamiltonian has the property that a minimal constraint violation is 1 and no constraint violation is 0. # We wish to normalise the constraint Hamiltonian relative to the total cost Hamiltonian such that the constraint violation will be 1~2 x larger than the maximal difference between total cost values. RelativeConstraintNormalisation = 1 # The cost Hamiltonian should be similar in eigenvalue difference to the mixer Hamiltonian and should be normalised to about 1. # To find the exact normalization requires solving this NP hard problem, so we always use an approximation. # Since this is approximate, there is a relative scaling parameter we can tweak RelativeCostNormalisation = 1 / 5 TotalCostNormalisation = GlobalScalingParameter * RelativeCostNormalisation TotalConstraintNormalisation = RelativeConstraintNormalisation * TotalCostNormalisation # Normalize latencies min_solution_guess = ( 8 # can be calculated with Dijkstra when removing the single assignment constraint ) max_solution_guess = 11 # any guess that fits the constraints will work lat_normalized = {} for e, w in lat.items(): lat_normalized[e] = ( w * TotalCostNormalisation / (max_solution_guess - min_solution_guess) ) c = np.tile(lat_vec, D) # Variable bounds: 0 <= Z[d,e] <= 1 (binary) lb = np.zeros(D * m) ub = np.ones(D * m) integrality = np.ones(D * m, dtype=int) # Helper: map (d,e) -> flat index def fidx(d_idx, e_idx): return d_idx * m + e_idx lin_constraints = [] # (1) Capacity per edge (unit capacity): sum_d Z[d,e] <= 1 A_cap = np.zeros((m, D * m)) for e_idx in range(m): for d_idx in range(D): A_cap[e_idx, fidx(d_idx, e_idx)] = 1.0 cap_lb = -np.inf * np.ones(m) cap_ub = np.ones(m) lin_constraints.append(LinearConstraint(A_cap, cap_lb, cap_ub)) # (2) Flow conservation for each demand and node: # For each demand d and node v: sum_out Z[d,e] - sum_in Z[d,e] = b_{d,v} def incidence_row_for(d_idx, v): row = np.zeros(D * m) for e_idx, (u, v2) in enumerate(E): if u == v: # outgoing row[fidx(d_idx, e_idx)] += 1.0 if v2 == v: # incoming row[fidx(d_idx, e_idx)] -= 1.0 return row A_flow_rows = [] b_list = [] for d_idx, d in enumerate(demands): for v in V: b = 0.0 if v == d["s"]: b = 1.0 elif v == d["t"]: b = -1.0 A_flow_rows.append(incidence_row_for(d_idx, v)) b_list.append(b) A_flow = np.vstack(A_flow_rows) b_vec = np.array(b_list) lin_constraints.append(LinearConstraint(A_flow, b_vec, b_vec)) ``` # ## Visualize ```python theme={null} pos = { "A": (0, 0.6), "B": (1, 1.1), "C": (1, 0.1), "D": (2, 0.6), "E": (2, 1.1), "F": (3, 0.6), } plt.figure(figsize=(10, 5)) nx.draw( graph, pos, with_labels=True, node_size=1200, node_color=CLASSIQ["lime"], edge_color=CLASSIQ["pink"], font_size=14, arrows=True, ) nx.draw_networkx_edge_labels( graph, pos, edge_labels={(u, v): lat[(u, v)] for (u, v) in E} ) plt.title( f"Network Topology: Directed Graph with {len(V)} nodes, {m} edges and {D} demands", fontsize=16, ) plt.show() ``` output ## 3. Classical Solution ```python theme={null} # --------------------------- # Solve MILP # --------------------------- res = milp( c=c, integrality=integrality, bounds=Bounds(lb, ub), constraints=lin_constraints, options={"disp": False}, ) # Convert back to 2D Z[d,e] Z = np.round(res.x).astype(int).reshape(D, m) ``` # ## Visualize ```python theme={null} def visualize_solution(Z_matrix, title="Routing Solution"): colors_map = [CLASSIQ["pink"], CLASSIQ["cyan"], CLASSIQ["teal"]] edge_to_demand = {} for d_idx in range(D): for e_idx, (u, v) in enumerate(E): if Z_matrix[d_idx, e_idx] == 1: edge_to_demand[(u, v)] = d_idx edge_colors = [ ( colors_map[edge_to_demand[(u, v)]] if (u, v) in edge_to_demand else CLASSIQ["pink"] ) for u, v in E ] edge_widths = [4.0 if (u, v) in edge_to_demand else 1.0 for u, v in E] plt.figure(figsize=(10, 5)) nx.draw( graph, pos, with_labels=True, node_size=1200, node_color=CLASSIQ["lime"], font_size=14, edge_color=edge_colors, width=edge_widths, arrows=True, ) nx.draw_networkx_edge_labels( graph, pos, edge_labels={(u, v): lat[(u, v)] for (u, v) in E} ) from matplotlib.patches import Patch plt.legend( handles=[ Patch(facecolor=colors_map[i], label=demands[i]["name"]) for i in range(D) ], loc="upper right", ) plt.title(title, fontsize=16) plt.show() visualize_solution(Z, "Classical MILP Solution") ``` output ```python theme={null} # --------------------------- # Decode / present results # --------------------------- # Table: Z with demand names and edge IDs Z_df = pd.DataFrame( Z, index=[f"{d['name']}({d['s']}→{d['t']})" for d in demands], columns=edge_ids ) # Table: chosen edges per demand + latency sums rows = [] for d_idx, d in enumerate(demands): chosen_edges = [(u, v) for e_idx, (u, v) in enumerate(E) if Z[d_idx, e_idx] == 1] chosen_labels = [f"{u}→{v}" for (u, v) in chosen_edges] latency_sum = sum(lat[e] for e in chosen_edges) rows.append( { "Demand": f"{d['name']} ({d['s']}→{d['t']})", "Chosen edges": ", ".join(chosen_labels) if chosen_labels else "(none)", "Latency sum": latency_sum, } ) df_chosen = pd.DataFrame(rows) # Try to reconstruct s→t sequence (simple walk) def reconstruct_path(chosen_edges_list, s, t): nxt = {} for u, v in chosen_edges_list: nxt[u] = v path = [s] visited = set([s]) while path[-1] != t and path[-1] in nxt: nxt_node = nxt[path[-1]] if nxt_node in visited: break path.append(nxt_node) visited.add(nxt_node) return path seq_rows = [] for d_idx, d in enumerate(demands): chosen = [(u, v) for e_idx, (u, v) in enumerate(E) if Z[d_idx, e_idx] == 1] seq = reconstruct_path(chosen, d["s"], d["t"]) seq_rows.append( { "Demand": f"{d['name']} ({d['s']}→{d['t']})", "Sequence": "→".join(seq), "Is complete s→t?": ( len(seq) >= 2 and seq[0] == d["s"] and seq[-1] == d["t"] ), } ) df_sequences = pd.DataFrame(seq_rows) # Display print("Z (2D) solution matrix — rows=demand, cols=edge\n", Z_df) print("Chosen edges & per-demand latency\n", df_chosen) print("Reconstructed s→t sequences (validity check)\n", df_sequences) ``` **Output:** ``` Z (2D) solution matrix — rows=demand, cols=edge e1 e2 e3 e4 e5 e6 e7 e8 d1(A→E) 1 0 1 0 0 0 0 0 d2(B→F) 0 0 0 1 0 0 1 0 d3(C→F) 0 0 0 0 1 0 0 1 Chosen edges & per-demand latency Demand Chosen edges Latency sum 0 d1 (A→E) A→B, B→E 4 1 d2 (B→F) B→D, D→F 3 2 d3 (C→F) C→E, E→F 2 Reconstructed s→t sequences (validity check) Demand Sequence Is complete s→t? 0 d1 (A→E) A→B→E True 1 d2 (B→F) B→D→F True 2 d3 (C→F) C→E→F True ``` ## 4. Quantum Solution with QAOA ```python theme={null} # Objective: sum_d sum_e lat_e * Z[d,e] def objective_func(assigned_z): return sum( (lat_normalized[e] * assigned_z[fidx(d_idx, e_idx)]) for d_idx, d in enumerate(demands) for e_idx, e in enumerate(graph.edges()) ) def index_of_edge(u, v): for e_idx, edge in enumerate(graph.edges()): if edge[0] == u and edge[1] == v: return e_idx raise AssertionError("Edge not found") def constraint_flow_conservation(assigned_z): total_flow = 0 for d_idx, d in enumerate(demands): for v in V: out_sum = sum( assigned_z[fidx(d_idx, index_of_edge(v, v_out))] for v_out in graph.successors(v) ) in_sum = sum( assigned_z[fidx(d_idx, index_of_edge(v_in, v))] for v_in in graph.predecessors(v) ) source_correction = destination_correction = 0 if d["s"] == v: source_correction = 1 if d["t"] == v: destination_correction = 1 node_demand_flow = ( in_sum - out_sum + source_correction - destination_correction ) total_flow += node_demand_flow**2 return TotalConstraintNormalisation * total_flow def constraint_single_assignment(assigned_z): def inverse(bit): return 1 - bit more_than_a_single_assignment = 0 for e_idx, e in enumerate(graph.edges()): assignments_of_e = [ assigned_z[fidx(d_idx, e_idx)] for d_idx, d in enumerate(demands) ] all_assignments = math.prod(assignments_of_e) two_assignments = 0 for idx, current_assignment in enumerate(assignments_of_e): rest_of_assignments = [ other_e for other_idx, other_e in enumerate(assignments_of_e) if idx != other_idx ] rest_of_assignments_off = math.prod(rest_of_assignments) two_assignments += inverse(current_assignment) * rest_of_assignments_off more_than_a_single_assignment += all_assignments + two_assignments return TotalConstraintNormalisation * more_than_a_single_assignment def cost_hamiltonian(assigned_Z): objective_weight = 1 flow_conservation_weight = 1 single_assignment_weight = 1 return ( objective_weight * objective_func(assigned_Z) + flow_conservation_weight * constraint_flow_conservation(assigned_Z) + single_assignment_weight * constraint_single_assignment(assigned_Z) ) ``` ```python theme={null} NUM_LAYERS = 5 @qfunc def mixer_layer(beta: CReal, qba: QArray[QBit]): apply_to_all(lambda q: RX(GlobalScalingParameter * beta, q), qba), @qfunc def main( params: CArray[CReal, 2 * NUM_LAYERS], z: Output[QArray[QBit, m * D]], ) -> None: allocate(z) hadamard_transform(z) repeat( count=NUM_LAYERS, iteration=lambda i: ( phase(cost_hamiltonian(z), params[2 * i]), mixer_layer(params[2 * i + 1], z), ), ) qprog = synthesize(main) ``` ```python theme={null} NUM_SHOTS = 10000 es = ExecutionSession( qprog, execution_preferences=ExecutionPreferences(num_shots=NUM_SHOTS) ) def initial_qaoa_params(NUM_LAYERS) -> np.ndarray: initial_gammas = math.pi * np.linspace( 1 / (2 * NUM_LAYERS), 1 - 1 / (2 * NUM_LAYERS), NUM_LAYERS ) initial_betas = math.pi * np.linspace( 1 - 1 / (2 * NUM_LAYERS), 1 / (2 * NUM_LAYERS), NUM_LAYERS ) initial_params = [] for i in range(NUM_LAYERS): initial_params.append(initial_gammas[i]) initial_params.append(initial_betas[i]) return np.array(initial_params) initial_params = initial_qaoa_params(NUM_LAYERS) ``` ```python theme={null} cost_func = lambda state: cost_hamiltonian(state["z"]) def estimate_cost_func(params): objective_val = es.estimate_cost(cost_func, {"params": params.tolist()}) objective_values.append(objective_val) # print(objective_val) return objective_val # Record the steps of the optimization intermediate_params = [] objective_values = [] # Define the callback function to store the intermediate steps def callback(xk): intermediate_params.append(xk) objective_values.append(es.estimate_cost(cost_func, {"params": xk.tolist()})) ``` ```python theme={null} MAX_ITERATIONS = 10 with tqdm(total=MAX_ITERATIONS, desc="Optimization Progress", leave=True) as pbar: def progress_bar(xk: np.ndarray) -> None: pbar.update(1) # increment progress bar optimization_res = minimize( estimate_cost_func, x0=initial_params, method="COBYLA", # callback=callback, options={"maxiter": MAX_ITERATIONS}, callback=progress_bar, ) res = es.sample({"params": optimization_res.x.tolist()}) print(f"Optimized parameters: {optimization_res.x.tolist()}") ``` **Output:** ``` Optimization Progress: 11it [03:06, 16.93s/it] ``` **Output:** ``` Optimized parameters: [0.3141592653589793, 2.827433388230814, 0.942477796076938, 2.199114857512855, 1.5707963267948966, 1.5707963267948966, 2.199114857512855, 0.9424777960769377, 2.827433388230814, 0.3141592653589793] ``` ## 5. Results Analysis ```python theme={null} def check_validity(assigned_z: list[int]) -> bool: if constraint_flow_conservation(assigned_z) != 0: return False if constraint_single_assignment(assigned_z) != 0: return False return True ``` ```python theme={null} sorted_counts = sorted(res.parsed_counts, key=lambda pc: pc.shots, reverse=True) count = 0 def print_res(sampled): color = "92m" assigned_z = sampled.state["z"] if not check_validity(assigned_z): color = "91m" print( f"\033[{color}solution={assigned_z} probability={sampled.shots/NUM_SHOTS} cost={cost_hamiltonian(assigned_z)}\033[0m, objective={objective_func(assigned_z)/TotalCostNormalisation * (max_solution_guess-min_solution_guess)}" ) valid_solutions = [] for sampled in sorted_counts[:15]: print_res(sampled) for idx, sampled in enumerate(sorted_counts): if check_validity(sampled.state["z"]): valid_solutions.append(idx) count += sampled.shots / NUM_SHOTS print(f"Valid solution at index {idx}") print(count) ``` **Output:** ``` solution=[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1] probability=0.0049 cost=0.7333333333333334, objective=5.0 solution=[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1] probability=0.0047 cost=0.9333333333333333, objective=2.0 solution=[1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1] probability=0.0047 cost=0.6, objective=8.999999999999998 solution=[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0] probability=0.0037 cost=1.0, objective=3.0 solution=[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1] probability=0.0035 cost=1.0, objective=3.0 solution=[0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1] probability=0.0032 cost=0.8, objective=8.999999999999998 solution=[0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0] probability=0.0031 cost=0.7333333333333333, objective=10.999999999999998 solution=[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1] probability=0.003 cost=0.9333333333333333, objective=5.0 solution=[1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1] probability=0.0029 cost=0.8, objective=5.999999999999999 solution=[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0] probability=0.0027 cost=0.8666666666666667, objective=6.999999999999999 solution=[0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1] probability=0.0027 cost=0.8666666666666667, objective=6.999999999999999 solution=[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1] probability=0.0026 cost=0.8666666666666667, objective=6.999999999999999 solution=[0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0] probability=0.0026 cost=1.0666666666666667, objective=4.0 solution=[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1] probability=0.0026 cost=1.2666666666666668, objective=1.0 solution=[0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0] probability=0.0025 cost=0.8666666666666667, objective=6.999999999999999 Valid solution at index 2 Valid solution at index 6 0.0078 ``` # ## Visualize Valid Solutions ```python theme={null} for i in valid_solutions[:3]: Z_quantum = np.array(sorted_counts[i].state["z"]).reshape(D, m) visualize_solution( Z_quantum, f'Quantum Solution at index {i}\n Objective={objective_func(sorted_counts[i].state["z"])/TotalCostNormalisation * (max_solution_guess-min_solution_guess)}', ) ``` output output # Radio Access Network Source: https://docs.classiq.io/explore/applications/telecom/radio_access_network/radio_access_network_positioning_antennas Open this notebook in GitHub to run it yourself Radio Access Network (RAN) is an important part of network communication systems, responsible for connecting used devices such as smartphones and radio machines to a wireless network. Optimizing the Radio Access Network involves various tasks that make it challenging to optimize a network efficiently. These tasks include resource allocation, locating transmission devices, such as antennas, to enhance coverage according to the overall consumption. Ideally, the optimization of a RAN will maximize the resource utilization of the network, save operational costs to the owner of the network. In this case of RAN, the solution is the positions of the set of antennas we have in a region that that has consumers spread in various locations. Finding good positions of antennas with a limited number of antennas is very complex to optimize and find a good solution in polynomial time. ## 1. Define Problem Classically with Pyomo * We have a limited number of antennas defined by $N$. * We have a set of potential locations: $\{1,2,3,...,M\}$, where $M>N$ * We limit certain locations with an overlap, not to use 2 antennas. # ## Mathematical Definition Each location is a binary variable $x_{i}$ that is 1 if we put antenna there and 0 if we don't put in that location. Each location is charachterized with certain consumption $c_{i}$. Mathematically, it is translated into objective function which aims to maximized its coverage: $$ \max_{x} \sum_{i} c_{i}x_{i} $$ Now, we add the constraints, such as the number of antennas: $$ \sum_{i} x_{i} \leq N $$ We can also add a constraint that prevent an ovelap between antennas. All sets of neighboring antenna sites $\{n_{0},n_{1},...,n_{k}\}$ will have only 1 anntenna on the ground: $$ \sum_{i}^{k} x_{n_{i}} == 1 $$ # ## Defining Pyomo Model ```python theme={null} # Import relevant packages import random import matplotlib.pyplot as plt import networkx as nx # noqa import numpy as np import pandas as pd import pyomo.environ as pyo random.seed(0) np.random.seed(0) # potenital locations M = 13 # number of antennas N = 7 # consumption coefficient c_vec = np.random.rand(1, M)[0] neighbors1 = [1, 3, 4] neighbors2 = [0, 5] neighbors3 = [6, 9] ``` ```python theme={null} model = pyo.ConcreteModel() # define the variables model.x = pyo.Var(range(M), domain=pyo.Binary) x_variables = np.array(list(model.x.values())) # constriants model.num_antennas = pyo.Constraint(expr=sum(x_variables[i] for i in range(M)) <= N) model.neigh1 = pyo.Constraint(expr=sum(x_variables[i] for i in neighbors1) == 1) model.neigh2 = pyo.Constraint(expr=sum(x_variables[i] for i in neighbors2) == 1) model.neigh3 = pyo.Constraint(expr=sum(x_variables[i] for i in neighbors3) == 1) model.obj = pyo.Objective(expr=x_variables @ c_vec, sense=pyo.maximize) ``` ## Define QAOA Parameters and Synthesize In order to solve the Pyomo model defined above, we use the Classiq combinatorial optimization engine. For the quantum part of the QAOA algorithm (`QAOAConfig`) - define the number of repetitions (`num_layers`) and the `penalty_energy` to get results that satisfy your constraints. Be careful! large enrgy also can bring you away from the optimized solution: ```python theme={null} from classiq import * from classiq.applications.combinatorial_optimization import OptimizerConfig, QAOAConfig qaoa_config = QAOAConfig(num_layers=4, penalty_energy=3.0) ``` For the classical optimization part of the QAOA algorithm we define the maximum number of classical iterations (max\_iteration). ```python theme={null} optimizer_config = OptimizerConfig(max_iteration=60, alpha_cvar=1.0) ``` Lastly, we load the model, based on the problem and algorithm parameters, which we can use to solve the problem: ```python theme={null} qmod = construct_combinatorial_optimization_model( pyo_model=model, qaoa_config=qaoa_config, optimizer_config=optimizer_config, ) ``` We also set the quantum backend we want to execute on: ```python theme={null} from classiq import set_execution_preferences from classiq.execution import ClassiqBackendPreferences, ExecutionPreferences backend_preferences = ExecutionPreferences( num_shots=3000, # backend_preferences=ClassiqBackendPreferences(backend_name="aer_simulator") ) qmod = set_execution_preferences(qmod, backend_preferences) ``` We can now synthesize and view the QAOA circuit (ansatz) used to solve the optimization problem: ```python theme={null} from classiq import show, synthesize qprog = synthesize(qmod) show(qprog) ``` **Output:** ``` Exception in callback Task.__step() handle: Traceback (most recent call last): File "/Users/nadavyoran/.pyenv/versions/3.11.13/lib/python3.11/asyncio/events.py", line 84, in _run self._context.run(self._callback, *self._args) RuntimeError: cannot enter context: <_contextvars. Context object at 0x1082b9340> is already entered Exception in callback Task.__step() handle: Traceback (most recent call last): File "/Users/nadavyoran/.pyenv/versions/3.11.13/lib/python3.11/asyncio/events.py", line 84, in _run self._context.run(self._callback, *self._args) RuntimeError: cannot enter context: <_contextvars. Context object at 0x1082b9340> is already entered Exception in callback Task.__step() handle: Traceback (most recent call last): File "/Users/nadavyoran/.pyenv/versions/3.11.13/lib/python3.11/asyncio/events.py", line 84, in _run self._context.run(self._callback, *self._args) RuntimeError: cannot enter context: <_contextvars. Context object at 0x1082b9340> is already entered Exception in callback Task.__step() handle: Traceback (most recent call last): File "/Users/nadavyoran/.pyenv/versions/3.11.13/lib/python3.11/asyncio/events.py", line 84, in _run self._context.run(self._callback, *self._args) RuntimeError: cannot enter context: <_contextvars. Context object at 0x1082b9340> is already entered Exception in callback Task.__step() handle: Traceback (most recent call last): File "/Users/nadavyoran/.pyenv/versions/3.11.13/lib/python3.11/asyncio/events.py", line 84, in _run self._context.run(self._callback, *self._args) RuntimeError: cannot enter context: <_contextvars. Context object at 0x1082b9340> is already entered Task was destroyed but it is pending! task: .run_in_context() done, defined at /Users/nadavyoran/.pyenv/versions/3.11.13/lib/python3.11/site-packages/ipykernel/utils.py:57> wait_for= cb=[Task.__wakeup()]> cb=[ZMQStream._run_callback.._log_error() at /Users/nadavyoran/.pyenv/versions/3.11.13/lib/python3.11/site-packages/zmq/eventloop/zmqstream.py:563]> /Users/nadavyoran/.pyenv/versions/3.11.13/lib/python3.11/site-packages/anyio/_core/_tasks.py:117: RuntimeWarning: coroutine 'Kernel.shell_main' was never awaited with get_async_backend().create_cancel_scope( RuntimeWarning: Enable tracemalloc to get the object allocation traceback Task was destroyed but it is pending! task: cb=[Task.__wakeup()]> Task was destroyed but it is pending! task: .run_in_context() done, defined at /Users/nadavyoran/.pyenv/versions/3.11.13/lib/python3.11/site-packages/ipykernel/utils.py:57> wait_for= cb=[Task.__wakeup()]> cb=[ZMQStream._run_callback.._log_error() at /Users/nadavyoran/.pyenv/versions/3.11.13/lib/python3.11/site-packages/zmq/eventloop/zmqstream.py:563]> Task was destroyed but it is pending! task: cb=[Task.__wakeup()]> Task was destroyed but it is pending! task: .run_in_context() done, defined at /Users/nadavyoran/.pyenv/versions/3.11.13/lib/python3.11/site-packages/ipykernel/utils.py:57> wait_for= cb=[Task.__wakeup()]> cb=[ZMQStream._run_callback.._log_error() at /Users/nadavyoran/.pyenv/versions/3.11.13/lib/python3.11/site-packages/zmq/eventloop/zmqstream.py:563]> Task was destroyed but it is pending! task: cb=[Task.__wakeup()]> ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/36pyGdPoyltMPFVkjKVear2tfhr ``` ## Executing the Hybrid Algorithm We now solve the problem using the generated circuit by using the execute method: ```python theme={null} from classiq import execute res = execute(qprog).result() ``` We can check the convergence of the run: ```python theme={null} from classiq.execution import VQESolverResult vqe_result = res[0].value vqe_result.convergence_graph ``` output ## Analyze Results We can also examine the statistics of the algorithm: ```python theme={null} import pandas as pd from classiq.applications.combinatorial_optimization import ( get_optimization_solution_from_pyo, ) solution = get_optimization_solution_from_pyo( model, vqe_result=vqe_result, penalty_energy=qaoa_config.penalty_energy ) optimization_result = pd.DataFrame.from_records(solution) optimization_result.sort_values(by="cost", ascending=False).head(5) ``` | | probability | cost | solution | count | | ---- | ----------- | -------- | ---------------------------------------- | ----- | | 916 | 0.000333 | 5.117951 | \[0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1] | 1 | | 562 | 0.000333 | 2.448870 | \[0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1] | 1 | | 1519 | 0.000333 | 2.197017 | \[0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0] | 1 | | 342 | 0.000333 | 2.016968 | \[0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1] | 1 | | 708 | 0.000333 | 1.991353 | \[0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1] | 1 | ```python theme={null} optimization_result.hist("cost", weights=optimization_result["probability"]) ``` **Output:** ``` array([[]], dtype=object) ``` output # Quantum-Based Resiliency Planning Source: https://docs.classiq.io/explore/applications/telecom/resiliency_planning/resiliency_planning Open this notebook in GitHub to run it yourself This notebook implements the quantum-based resiliency planning using QAOA. ```python theme={null} import math import random from itertools import product from platform import node import matplotlib.pyplot as plt import networkx as nx import numpy as np import pandas as pd from scipy.optimize import minimize from tqdm import tqdm from classiq import * from classiq.execution import ExecutionPreferences, ExecutionSession # Imports ``` ## Create the Graph ```python theme={null} # --- - Graph ---- graph = nx.Graph() V = ["P1", "P2", "S1", "S2", "S3"] graph.add_nodes_from(V) E_with_weights = [ ("P1", "S1", {"lat": 1, "perr": 0.1}), ("P1", "S2", {"lat": 1, "perr": 0.1}), ("S2", "S1", {"lat": 1, "perr": 0.1}), ("P2", "S1", {"lat": 1, "perr": 0.1}), ("S3", "S1", {"lat": 1, "perr": 0.1}), ("P2", "S3", {"lat": 1, "perr": 0.1}), ("S3", "S2", {"lat": 2, "perr": 0.1}), ] lat_vec = np.array([w["lat"] for (_, _, w) in E_with_weights], dtype=float) perr_vec = np.array([w["perr"] for (_, _, w) in E_with_weights], dtype=float) graph.add_edges_from(E_with_weights) E = graph.edges() lat = {(u, v): w["lat"] for (u, v, w) in E_with_weights} perr = {(u, v): w["perr"] for (u, v, w) in E_with_weights} ordered_edges = [(u, v) for (u, v, _) in E_with_weights] edge_ids = [f"({u},{v})" for (u, v) in ordered_edges] ids = edge_ids + V def weight_of_e(u, v, weights): if (u, v) in lat.keys(): return weights[(u, v)] return weights[(v, u)] def lat_of_e(u, v): return weight_of_e(u, v, lat) def perr_of_e(u, v): return weight_of_e(u, v, perr) # --- - Demands ---- # From S2 source_node = "S2" demands = [ {"name": "d1", "s": source_node, "t": "P1"}, {"name": "d2", "s": source_node, "t": "P2"}, ] D = len(demands) m = len(E) n = len(V) # --- - Z 2D matrix ---- row_len = m + n Z = np.zeros((D, (m + n)), dtype=int) # Creating the solution we aim to get Z[0, 1] = Z[0, 7] = Z[0, 10] = 1 Z[1, 5] = Z[1, 6] = Z[1, 11] = Z[1, 10] = Z[1, 8] = 1 Z_df = pd.DataFrame( Z, index=[f"{d['name']}({d['s']}→{d['t']})" for d in demands], columns=ids ) # --- - Plot ---- pos = { "P1": (2.0, 1.0), "P2": (1.0, 1.0), "S1": (1.5, 1.0), "S2": (1.75, 2.0), "S3": (1.25, 2.0), } plt.figure(figsize=(7.5, 3.2)) nx.draw(graph, pos, with_labels=True, node_size=1000) nx.draw_networkx_edge_labels( graph, pos, edge_labels={(u, v): f"{lat_of_e(u,v)}" for (u, v) in E} ) plt.title("Directed graph with 6 nodes and 7 edges (edge labels = latency)") plt.tight_layout() plt.show() print("\nDemands (2 total):") print( pd.DataFrame( [{"Demand": d["name"], "Source": d["s"], "Target": d["t"]} for d in demands] ) ) print("\nZ (D x (m+n)) binary matrix — rows=demand, cols=edge (initialized to 0):") print(Z_df) ``` **Output:** ``` /var/folders/r8/5nlfyms56kj96_c21bgv36040000gn/T/ipykernel_86414/3897101284.py:81: UserWarning: This figure includes Axes that are not compatible with tight_layout, so results might be incorrect. plt.tight_layout() ``` output **Output:** ``` Demands (2 total): Demand Source Target 0 d1 S2 P1 1 d2 S2 P2 Z (D x (m+n)) binary matrix — rows=demand, cols=edge (initialized to 0): (P1,S1) (P1,S2) (S2,S1) (P2,S1) (S3,S1) (P2,S3) (S3,S2) P1 \ d1(S2→P1) 0 1 0 0 0 0 0 1 d2(S2→P2) 0 0 0 0 0 1 1 0 P2 S1 S2 S3 d1(S2→P1) 0 0 1 0 d2(S2→P2) 1 0 1 1 ``` ```python theme={null} num_edges = len(ordered_edges) err_correlation = np.full((num_edges, num_edges), 0) correlated = True i = j = 0 for index, (u1, v1) in enumerate(ordered_edges): if (u1, v1) in [("P1", "S2"), ("S2", "P1")]: i = index if (u1, v1) in [("S1", "S2"), ("S2", "S1")]: j = index assert i != j != 0 if correlated: err_correlation[i, j] = err_correlation[j, i] = 1 err_correlation ``` **Output:** ``` array([[0, 0, 0, 0, 0, 0, 0], [0, 0, 1, 0, 0, 0, 0], [0, 1, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0]]) ``` For convience and easy visualization, let's add a helper function that prints the solution into the graph. This function gets our assigned solution (the 2D array of binary variables). ```python theme={null} import matplotlib.pyplot as plt def get_edge(u, v, arr): if (u, v) in arr: return (u, v) return (v, u) def print_solution_graph(assigned_z, index=None): # Assign a distinct color per demand colors_for_demands = ["red", "blue", "green", "yellow"] edge_colors = [] edge_widths = [] node_colors = [] # Build a mapping from edge -> demand index (if chosen) edge_to_demand = {} for d_idx, d in enumerate(demands): for e_idx, (u, v) in enumerate(ordered_edges): if assigned_z[d_idx, e_idx] == 1: edge_to_demand[(u, v)] = d_idx # Create color list for all edges in E for u, v in E: if (u, v) in edge_to_demand or (v, u) in edge_to_demand: demand_idx = edge_to_demand[get_edge(u, v, edge_to_demand)] edge_colors.append(colors_for_demands[demand_idx]) edge_widths.append(3.0) else: edge_colors.append("lightgray") edge_widths.append(1.0) # Plot the graph plt.figure(figsize=(8, 3.6)) nx.draw( graph, pos, with_labels=True, node_size=1000, edge_color=edge_colors, width=edge_widths, ) nx.draw_networkx_edge_labels( graph, pos, edge_labels={(u, v): f"{lat_of_e(u,v)}" for (u, v) in ordered_edges} ) if index is not None: plt.title( f"Routing solution for sample index {index} — edges colored by demand" ) else: plt.title("Optimal routing solution — edges colored by demand") plt.show() print(Z_df) print_solution_graph(Z) ``` **Output:** ``` (P1,S1) (P1,S2) (S2,S1) (P2,S1) (S3,S1) (P2,S3) (S3,S2) P1 \ d1(S2→P1) 0 1 0 0 0 0 0 1 d2(S2→P2) 0 0 0 0 0 1 1 0 P2 S1 S2 S3 d1(S2→P1) 0 0 1 0 d2(S2→P2) 1 0 1 1 ``` output ```python theme={null} # The mixer Hamiltonian is effectively X/2 and has eigenvalues -0.5 and + 0. 5. So the difference between minimum and maximum eigenvalues is exactly 1. # This can be rescaled by a global scaling parameter. GlobalScalingParameter = 1 # The constraint Hamiltonian has the property that a minimal constraint violation is 1 and no constraint violation is 0. # We wish to normalise the constraint Hamiltonian relative to the total cost Hamiltonian such that the constraint violation will be 1~2 x larger than the maximal difference between total cost values. RelativeConstraintNormalisation = 5 # The cost Hamiltonian should be similar in eigenvalue difference to the mixer Hamiltonian and should be normalised to about 1. # To find the exact normalisation requires solving this NP hard problem so we always use approximation. # Since this is approximate, there is a relative scaling parameter we can twitch RelativeCostNormalisation = 1 / 40 TotalCostNormalisation = GlobalScalingParameter * RelativeCostNormalisation TotalConstraintNormalisation = RelativeConstraintNormalisation * TotalCostNormalisation # B stands for the relationship between error correlation and latency B = 10 # Normalize latencies min_lat_guess = ( 3 # can be calculated with Dijkstra when removing the single assignment constraint ) max_lat_guess = 4 # any guess that fits the constraints will work lat_normalized = {} for e, w in lat.items(): lat_normalized[e] = w * TotalCostNormalisation / (max_lat_guess - min_lat_guess) min_prob_guess = 0.03 max_prob_guess = 1.0 print(lat_normalized) ``` **Output:** ``` {('P1', 'S1'): 0.025, ('P1', 'S2'): 0.025, ('S2', 'S1'): 0.025, ('P2', 'S1'): 0.025, ('S3', 'S1'): 0.025, ('P2', 'S3'): 0.025, ('S3', 'S2'): 0.05} ``` ## Helper Functions ```python theme={null} def edge_fidx(d_idx, e_idx): return d_idx * row_len + e_idx def node_fidx(d_idx, n_idx): return d_idx * row_len + n_idx + m ``` ## Define Objective Function ```python theme={null} def sum_per_array(assigned_z, array): return sum( (array[e] * assigned_z[edge_fidx(d_idx, e_idx)]) for d_idx, d in enumerate(demands) for e_idx, e in enumerate(ordered_edges) ) # Objective: sum_d sum_e lat_e * Z[d,e] def objective_func(assigned_z): lat_sum = sum_per_array(assigned_z, lat_normalized) return lat_sum ``` ## Define Single Assignment Cosntraint ```python theme={null} def edges_per_node(node): for edge_idx, edge in enumerate(ordered_edges): if node in edge: yield edge_idx def flow_conservation_per_node_per_demand( node, node_idx, demand_idx, demand, assigned_z ): node_flow = 0 # If node is the start/end of this demand, add 1 imaginary edge if node in demand.values(): node_flow += 1 # If the node is in the path, subtract 2 required edges node_flow -= 2 * assigned_z[node_fidx(d_idx=demand_idx, n_idx=node_idx)] # Add 1 edge for each used edge for this node for edge_idx in edges_per_node(node): node_flow += assigned_z[edge_fidx(d_idx=demand_idx, e_idx=edge_idx)] # Valid solution should have node_flow == 0 since: # * For nodes not in the path - no edges should be chosen # * For nodes in the path, there should be exactly 2 edges, or 1 edge for start/end ( + imaginary edge) return node_flow**2 def my_not(value): # Assumes value is 0 or 1 return 1 - value def constraint_starting_nodes(assigned_z): return sum( my_not(assigned_z[node_fidx(d_idx=demand_idx, n_idx=V.index(v_of_demand))]) for demand_idx, demand in enumerate(demands) for v_of_demand in list(demand.values())[1:] ) def constraint_flow_conservation(assigned_z): total_flow = sum( flow_conservation_per_node_per_demand( node=node, node_idx=node_idx, demand=demand, demand_idx=demand_idx, assigned_z=assigned_z, ) for node_idx, node in enumerate(V) for demand_idx, demand in enumerate(demands) ) return total_flow + constraint_starting_nodes(assigned_z) ``` ```python theme={null} def sum_correlation(assigned_z): return sum( err_correlation[e1_idx, e2_idx] * assigned_z[edge_fidx(0, e1_idx)] * assigned_z[edge_fidx(1, e2_idx)] for e1_idx in range(len(ordered_edges)) for e2_idx in range(len(ordered_edges)) if err_correlation[e1_idx, e2_idx] > 0 ) def node_resiliency(assigned_z): return sum( B * TotalCostNormalisation * assigned_z[node_fidx(d_idx=0, n_idx=node_idx)] * assigned_z[node_fidx(d_idx=1, n_idx=node_idx)] for node_idx in range(len(V)) if V[node_idx] is not source_node ) def objective_minimal_resiliency(assigned_z): return sum_correlation(assigned_z) ``` ```python theme={null} def cost_hamiltonian(assigned_Z): return ( objective_func(assigned_Z) + node_resiliency(assigned_Z) + objective_minimal_resiliency(assigned_Z) + TotalConstraintNormalisation * (constraint_flow_conservation(assigned_Z)) ) ``` ## Create QAOA Code ```python theme={null} NUM_LAYERS = 20 @qfunc def mixer_layer(beta: CReal, qba: QArray[QBit]): apply_to_all(lambda q: RX(GlobalScalingParameter * beta, q), qba) @qfunc def cost_layer(gamma: CReal, qba: QArray[QBit]): phase(cost_hamiltonian(qba), gamma) @qfunc def main( params: CArray[CReal, 2 * NUM_LAYERS], z: Output[QArray[QBit, D * row_len]], ) -> None: allocate(z) hadamard_transform(z) repeat( count=NUM_LAYERS, iteration=lambda i: ( cost_layer(params[2 * i], z), mixer_layer(params[2 * i + 1], z), ), ) ``` ```python theme={null} qprog = synthesize(main) ``` ```python theme={null} show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/36se9nqbGvihLDB2r99kNvBvajj ``` ```python theme={null} print(f"Width is {qprog.data.width}") print(f"Depth is {qprog.transpiled_circuit.depth}") print(f"Number of gates is {qprog.transpiled_circuit.count_ops}") ``` **Output:** ``` Width is 24 Depth is 586 Number of gates is {'cx': 2480, 'rz': 1720, 'rx': 480, 'h': 24} ``` ## Execute QAOA ```python theme={null} NUM_SHOTS = 20000 backend_preferences = ClassiqBackendPreferences( backend_name=ClassiqSimulatorBackendNames.SIMULATOR ) es = ExecutionSession( qprog, execution_preferences=ExecutionPreferences( num_shots=NUM_SHOTS, ), ) def initial_qaoa_params(NUM_LAYERS) -> np.ndarray: initial_gammas = math.pi * np.linspace( 1 / (2 * NUM_LAYERS), 1 - 1 / (2 * NUM_LAYERS), NUM_LAYERS ) initial_betas = math.pi * np.linspace( 1 - 1 / (2 * NUM_LAYERS), 1 / (2 * NUM_LAYERS), NUM_LAYERS ) initial_params = [] for i in range(NUM_LAYERS): initial_params.append(initial_gammas[i]) initial_params.append(initial_betas[i]) return np.array(initial_params) initial_params = initial_qaoa_params(NUM_LAYERS) ``` ```python theme={null} cost_func = lambda state: cost_hamiltonian(state["z"]) def estimate_cost_func(params): objective_val = es.estimate_cost(cost_func, {"params": params.tolist()}) print(f"Cost Hamiltonian = {np.round(objective_val, decimals=3)}") return objective_val ``` ```python theme={null} MAX_ITERATIONS = 1 # Increase to improve the results optimization_res = minimize( estimate_cost_func, x0=initial_params, method="COBYLA", options={"maxiter": MAX_ITERATIONS}, ) ``` **Output:** ``` Cost Hamiltonian = 1.387 ``` ```python theme={null} res = es.sample({"params": optimization_res.x.tolist()}) ``` ```python theme={null} def check_validity(assigned_z: list[int]) -> bool: if constraint_flow_conservation(assigned_z) != 0: return False if node_resiliency(assigned_z) != 0: return False return True ``` ```python theme={null} sorted_counts = sorted(res.parsed_counts, key=lambda pc: pc.shots, reverse=True) def print_res(sampled, idx): assigned_z = sampled.state["z"] if not check_validity(assigned_z): return valid_solutions_indices.append(idx) print( f"Valid solution found at index {idx}, probability={sampled.shots/NUM_SHOTS*100}%, cost={cost_hamiltonian(assigned_z)}, latency={round(objective_func(assigned_z) / TotalCostNormalisation * (max_lat_guess - min_lat_guess))}" ) valid_solutions_indices = [] for idx, sampled in enumerate(sorted_counts[:100]): print_res(sampled, idx) print( f"Accumulated valid probability is {sum(sampled.shots for sampled in sorted_counts if check_validity(sampled.state['z']))/NUM_SHOTS*100}%" ) ``` **Output:** ``` Valid solution found at index 0, probability=0.64%, cost=0.1, latency=4 Valid solution found at index 5, probability=0.375%, cost=0.125, latency=5 Valid solution found at index 9, probability=0.265%, cost=0.125, latency=5 Valid solution found at index 78, probability=0.075%, cost=1.075, latency=3 Accumulated valid probability is 1.3599999999999999% ``` ```python theme={null} def convert_1d_to_2d(array_1d): array_2d = np.zeros((D, row_len)) for d_idx in range(D): for e_idx in range(m): array_2d[d_idx][e_idx] = array_1d[edge_fidx(d_idx, e_idx)] for n_idx in range(n): array_2d[d_idx][m + n_idx] = array_1d[node_fidx(d_idx, n_idx)] return array_2d ``` ```python theme={null} def print_solution(index): print(index) solution = sorted_counts[index].state["z"] sol_2d = convert_1d_to_2d(solution) print_solution_graph(sol_2d) sol_df = pd.DataFrame( sol_2d, index=[f"{d['name']}({d['s']}→{d['t']})" for d in demands], columns=ids ) print(sol_df) print( f"{objective_func(solution)/TotalCostNormalisation * (max_lat_guess-min_lat_guess)=}" ) print(f"cost={cost_hamiltonian(solution)}") print(f"Flow Conservation: {constraint_flow_conservation(solution)}") print(f"Resiliency: {node_resiliency(solution)}") # for index in valid_solutions_indices: for index in valid_solutions_indices: print_solution(index) ``` **Output:** ``` 0 ``` output **Output:** ``` (P1,S1) (P1,S2) (S2,S1) (P2,S1) (S3,S1) (P2,S3) (S3,S2) P1 \ d1(S2→P1) 0.0 1.0 0.0 0.0 0.0 0.0 0.0 1.0 d2(S2→P2) 0.0 0.0 0.0 0.0 0.0 1.0 1.0 0.0 P2 S1 S2 S3 d1(S2→P1) 0.0 0.0 1.0 0.0 d2(S2→P2) 1.0 0.0 1.0 1.0 objective_func(solution)/TotalCostNormalisation * (max_lat_guess-min_lat_guess)=4.0 cost=0.1 Flow Conservation: 0 Resiliency: 0.0 5 ``` output **Output:** ``` (P1,S1) (P1,S2) (S2,S1) (P2,S1) (S3,S1) (P2,S3) (S3,S2) P1 \ d1(S2→P1) 0.0 1.0 0.0 0.0 0.0 0.0 0.0 1.0 d2(S2→P2) 0.0 0.0 0.0 1.0 1.0 0.0 1.0 0.0 P2 S1 S2 S3 d1(S2→P1) 0.0 0.0 1.0 0.0 d2(S2→P2) 1.0 1.0 1.0 1.0 objective_func(solution)/TotalCostNormalisation * (max_lat_guess-min_lat_guess)=5.0 cost=0.125 Flow Conservation: 0 Resiliency: 0.0 9 ``` output **Output:** ``` (P1,S1) (P1,S2) (S2,S1) (P2,S1) (S3,S1) (P2,S3) (S3,S2) P1 \ d1(S2→P1) 1.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 d2(S2→P2) 0.0 0.0 0.0 0.0 0.0 1.0 1.0 0.0 P2 S1 S2 S3 d1(S2→P1) 0.0 1.0 1.0 0.0 d2(S2→P2) 1.0 0.0 1.0 1.0 objective_func(solution)/TotalCostNormalisation * (max_lat_guess-min_lat_guess)=5.0 cost=0.125 Flow Conservation: 0 Resiliency: 0.0 78 ``` output **Output:** ``` (P1,S1) (P1,S2) (S2,S1) (P2,S1) (S3,S1) (P2,S3) (S3,S2) P1 \ d1(S2→P1) 0.0 1.0 0.0 0.0 0.0 0.0 0.0 1.0 d2(S2→P2) 0.0 0.0 1.0 1.0 0.0 0.0 0.0 0.0 P2 S1 S2 S3 d1(S2→P1) 0.0 0.0 1.0 0.0 d2(S2→P2) 1.0 1.0 1.0 0.0 objective_func(solution)/TotalCostNormalisation * (max_lat_guess-min_lat_guess)=3.0000000000000004 cost=1.075 Flow Conservation: 0 Resiliency: 0.0 ``` # Quantum-Based Resiliency Planning with AMD GPU Simulation Source: https://docs.classiq.io/explore/applications/telecom/resiliency_planning/resiliency_planning_AMD Open this notebook in GitHub to run it yourself This notebook implements the quantum-based resiliency planning using QAOA. This instance of the notebook does not use the Classiq built-in simulator but a specific build of the Qiskit Aer simulator for AMD GPUs. To leverage the Qiskit AMD simulator it is required to have a local AMD card availible. ## Before Running This notebook has commented out the code that compiles the AMD Qiskit Aer simulator, in order to use the AMD simulator make sure that you uncomment the code in the first cell, and selecte GPU device in the Qiskit Aer simulator cell. ```python theme={null} !pip install -qq qiskit_qasm3_import !pip install -qq qiskit_aer ``` ```python theme={null} # %%bash # # set environment variables # export ROCM_PATH=/opt/rocm # export AER_THRUST_BACKEND=ROCM # export QISKIT_AER_PACKAGE_NAME=qiskit-aer-gpu-rocm # export ROCR_VISIBLE_DEVICES=0 # export HIP_VISIBLE_DEVICES=0 # ulimit -s unlimited # ## Get Git # apt-get update && apt-get install -y git ninja-build # # pull rocm-qiskit-aer branch # mkdir quantum-sim && cd quantum-sim/ # git clone https://github.com/coketaste/qiskit-aer.git # cd qiskit-aer/ # git switch coketaste/amd-rocm-mi300 # # build rocm-qiskit-aer branch # rm -rf _skbuild dist build # pip install cmake pybind11 "conan<2" scikit-build # pip install -r requirements-dev.txt # python3 setup.py bdist_wheel -- \ # -DCMAKE_CXX_COMPILER=/opt/rocm/llvm/bin/clang++ \ # -DCMAKE_HIP_COMPILER=/opt/rocm/llvm/bin/clang++ \ # -DAER_THRUST_BACKEND=ROCM # pip install --force-reinstall dist/qiskit_aer_gpu_rocm*.whl # pip install qiskit # # run sample benchmark # examples/single_gpu/benchmark.py ``` ```python theme={null} import math import random from itertools import product from platform import node import matplotlib.pyplot as plt import networkx as nx import numpy as np import pandas as pd from scipy.optimize import minimize from tqdm import tqdm from classiq import * from classiq.execution import ExecutionPreferences, ExecutionSession ``` ## Create the Graph ```python theme={null} # --- - Graph ---- graph = nx.Graph() V = ["P1", "P2", "S1", "S2", "S3"] graph.add_nodes_from(V) E_with_weights = [ ("P1", "S1", {"lat": 1, "perr": 0.1}), ("P1", "S2", {"lat": 1, "perr": 0.1}), ("S2", "S1", {"lat": 1, "perr": 0.1}), ("P2", "S1", {"lat": 1, "perr": 0.1}), ("S3", "S1", {"lat": 1, "perr": 0.1}), ("P2", "S3", {"lat": 1, "perr": 0.1}), ("S3", "S2", {"lat": 2, "perr": 0.1}), ] lat_vec = np.array([w["lat"] for (_, _, w) in E_with_weights], dtype=float) perr_vec = np.array([w["perr"] for (_, _, w) in E_with_weights], dtype=float) graph.add_edges_from(E_with_weights) E = graph.edges() lat = {(u, v): w["lat"] for (u, v, w) in E_with_weights} perr = {(u, v): w["perr"] for (u, v, w) in E_with_weights} ordered_edges = [(u, v) for (u, v, _) in E_with_weights] edge_ids = [f"({u},{v})" for (u, v) in ordered_edges] ids = edge_ids + V def weight_of_e(u, v, weights): if (u, v) in lat.keys(): return weights[(u, v)] return weights[(v, u)] def lat_of_e(u, v): return weight_of_e(u, v, lat) def perr_of_e(u, v): return weight_of_e(u, v, perr) # --- - Demands ---- # From S2 source_node = "S2" demands = [ {"name": "d1", "s": source_node, "t": "P1"}, {"name": "d2", "s": source_node, "t": "P2"}, ] D = len(demands) m = len(E) n = len(V) # --- - Z 2D matrix ---- row_len = m + n Z = np.zeros((D, (m + n)), dtype=int) # Creating the solution we aim to get Z[0, 1] = Z[0, 7] = Z[0, 10] = 1 Z[1, 5] = Z[1, 6] = Z[1, 11] = Z[1, 10] = Z[1, 8] = 1 Z_df = pd.DataFrame( Z, index=[f"{d['name']}({d['s']}→{d['t']})" for d in demands], columns=ids ) # --- - Plot ---- pos = { "P1": (2.0, 1.0), "P2": (1.0, 1.0), "S1": (1.5, 1.0), "S2": (1.75, 2.0), "S3": (1.25, 2.0), } plt.figure(figsize=(7.5, 3.2)) nx.draw(graph, pos, with_labels=True, node_size=1000) nx.draw_networkx_edge_labels( graph, pos, edge_labels={(u, v): f"{lat_of_e(u,v)}" for (u, v) in E} ) plt.title("Directed graph with 6 nodes and 7 edges (edge labels = latency)") plt.tight_layout() plt.show() print("\nDemands (2 total):") print( pd.DataFrame( [{"Demand": d["name"], "Source": d["s"], "Target": d["t"]} for d in demands] ) ) print("\nZ (D x (m+n)) binary matrix — rows=demand, cols=edge (initialized to 0):") print(Z_df) ``` **Output:** ``` /var/folders/kk/nlz2dw2921g33r494qcnf4jh0000gn/T/ipykernel_60956/3897101284.py:81: UserWarning: This figure includes Axes that are not compatible with tight_layout, so results might be incorrect. plt.tight_layout() ``` output **Output:** ``` Demands (2 total): Demand Source Target 0 d1 S2 P1 1 d2 S2 P2 Z (D x (m+n)) binary matrix — rows=demand, cols=edge (initialized to 0): (P1,S1) (P1,S2) (S2,S1) (P2,S1) (S3,S1) (P2,S3) (S3,S2) P1 \ d1(S2→P1) 0 1 0 0 0 0 0 1 d2(S2→P2) 0 0 0 0 0 1 1 0 P2 S1 S2 S3 d1(S2→P1) 0 0 1 0 d2(S2→P2) 1 0 1 1 ``` ```python theme={null} num_edges = len(ordered_edges) err_correlation = np.full((num_edges, num_edges), 0) correlated = True i = j = 0 for index, (u1, v1) in enumerate(ordered_edges): if (u1, v1) in [("P1", "S2"), ("S2", "P1")]: i = index if (u1, v1) in [("S1", "S2"), ("S2", "S1")]: j = index assert i != j != 0 if correlated: err_correlation[i, j] = err_correlation[j, i] = 1 err_correlation ``` **Output:** ``` array([[0, 0, 0, 0, 0, 0, 0], [0, 0, 1, 0, 0, 0, 0], [0, 1, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0]]) ``` For convience and easy visualization, let's add a helper function that prints the solution into the graph. This function gets our assigned solution (the 2D array of binary variables). ```python theme={null} import matplotlib.pyplot as plt def get_edge(u, v, arr): if (u, v) in arr: return (u, v) return (v, u) def print_solution_graph(assigned_z, index=None): # Assign a distinct color per demand colors_for_demands = ["red", "blue", "green", "yellow"] edge_colors = [] edge_widths = [] node_colors = [] # Build a mapping from edge -> demand index (if chosen) edge_to_demand = {} for d_idx, d in enumerate(demands): for e_idx, (u, v) in enumerate(ordered_edges): if assigned_z[d_idx, e_idx] == 1: edge_to_demand[(u, v)] = d_idx # Create color list for all edges in E for u, v in E: if (u, v) in edge_to_demand or (v, u) in edge_to_demand: demand_idx = edge_to_demand[get_edge(u, v, edge_to_demand)] edge_colors.append(colors_for_demands[demand_idx]) edge_widths.append(3.0) else: edge_colors.append("lightgray") edge_widths.append(1.0) # Plot the graph plt.figure(figsize=(8, 3.6)) nx.draw( graph, pos, with_labels=True, node_size=1000, edge_color=edge_colors, width=edge_widths, ) nx.draw_networkx_edge_labels( graph, pos, edge_labels={(u, v): f"{lat_of_e(u,v)}" for (u, v) in ordered_edges} ) if index is not None: plt.title( f"Routing solution for sample index {index} — edges colored by demand" ) else: plt.title("Optimal routing solution — edges colored by demand") plt.show() print(Z_df) print_solution_graph(Z) ``` **Output:** ``` (P1,S1) (P1,S2) (S2,S1) (P2,S1) (S3,S1) (P2,S3) (S3,S2) P1 \ d1(S2→P1) 0 1 0 0 0 0 0 1 d2(S2→P2) 0 0 0 0 0 1 1 0 P2 S1 S2 S3 d1(S2→P1) 0 0 1 0 d2(S2→P2) 1 0 1 1 ``` output ```python theme={null} # The mixer Hamiltonian is effectively X/2 and has eigenvalues -0.5 and + 0. 5. So the difference between minimum and maximum eigenvalues is exactly 1. # This can be rescaled by a global scaling parameter. GlobalScalingParameter = 1 # The constraint Hamiltonian has the property that a minimal constraint violation is 1 and no constraint violation is 0. # We wish to normalise the constraint Hamiltonian relative to the total cost Hamiltonian such that the constraint violation will be 1~2 x larger than the maximal difference between total cost values. RelativeConstraintNormalisation = 5 # The cost Hamiltonian should be similar in eigenvalue difference to the mixer Hamiltonian and should be normalised to about 1. # To find the exact normalisation requires solving this NP hard problem so we always use approximation. # Since this is approximate, there is a relative scaling parameter we can twitch RelativeCostNormalisation = 1 / 40 TotalCostNormalisation = GlobalScalingParameter * RelativeCostNormalisation TotalConstraintNormalisation = RelativeConstraintNormalisation * TotalCostNormalisation # B stands for the relationship between error correlation and latency B = 10 # Normalize latencies min_lat_guess = ( 3 # can be calculated with Dijkstra when removing the single assignment constraint ) max_lat_guess = 4 # any guess that fits the constraints will work lat_normalized = {} for e, w in lat.items(): lat_normalized[e] = w * TotalCostNormalisation / (max_lat_guess - min_lat_guess) min_prob_guess = 0.03 max_prob_guess = 1.0 print(lat_normalized) ``` **Output:** ``` {('P1', 'S1'): 0.025, ('P1', 'S2'): 0.025, ('S2', 'S1'): 0.025, ('P2', 'S1'): 0.025, ('S3', 'S1'): 0.025, ('P2', 'S3'): 0.025, ('S3', 'S2'): 0.05} ``` ## Helper Functions ```python theme={null} def edge_fidx(d_idx, e_idx): return d_idx * row_len + e_idx def node_fidx(d_idx, n_idx): return d_idx * row_len + n_idx + m ``` ## Define Objective Function ```python theme={null} def sum_per_array(assigned_z, array): return sum( (array[e] * assigned_z[edge_fidx(d_idx, e_idx)]) for d_idx, d in enumerate(demands) for e_idx, e in enumerate(ordered_edges) ) # Objective: sum_d sum_e lat_e * Z[d,e] def objective_func(assigned_z): lat_sum = sum_per_array(assigned_z, lat_normalized) return lat_sum ``` ## Define Single Assignment Cosntraint ```python theme={null} def edges_per_node(node): for edge_idx, edge in enumerate(ordered_edges): if node in edge: yield edge_idx def flow_conservation_per_node_per_demand( node, node_idx, demand_idx, demand, assigned_z ): node_flow = 0 # If node is the start/end of this demand, add 1 imaginary edge if node in demand.values(): node_flow += 1 # If the node is in the path, subtract 2 required edges node_flow -= 2 * assigned_z[node_fidx(d_idx=demand_idx, n_idx=node_idx)] # Add 1 edge for each used edge for this node for edge_idx in edges_per_node(node): node_flow += assigned_z[edge_fidx(d_idx=demand_idx, e_idx=edge_idx)] # Valid solution should have node_flow == 0 since: # * For nodes not in the path - no edges should be chosen # * For nodes in the path, there should be exactly 2 edges, or 1 edge for start/end ( + imaginary edge) return node_flow**2 def my_not(value): # Assumes value is 0 or 1 return 1 - value def constraint_starting_nodes(assigned_z): return sum( my_not(assigned_z[node_fidx(d_idx=demand_idx, n_idx=V.index(v_of_demand))]) for demand_idx, demand in enumerate(demands) for v_of_demand in list(demand.values())[1:] ) def constraint_flow_conservation(assigned_z): total_flow = sum( flow_conservation_per_node_per_demand( node=node, node_idx=node_idx, demand=demand, demand_idx=demand_idx, assigned_z=assigned_z, ) for node_idx, node in enumerate(V) for demand_idx, demand in enumerate(demands) ) return total_flow + constraint_starting_nodes(assigned_z) ``` ```python theme={null} def sum_correlation(assigned_z): return sum( err_correlation[e1_idx, e2_idx] * assigned_z[edge_fidx(0, e1_idx)] * assigned_z[edge_fidx(1, e2_idx)] for e1_idx in range(len(ordered_edges)) for e2_idx in range(len(ordered_edges)) if err_correlation[e1_idx, e2_idx] > 0 ) def node_resiliency(assigned_z): return sum( B * TotalCostNormalisation * assigned_z[node_fidx(d_idx=0, n_idx=node_idx)] * assigned_z[node_fidx(d_idx=1, n_idx=node_idx)] for node_idx in range(len(V)) if V[node_idx] is not source_node ) def objective_minimal_resiliency(assigned_z): return sum_correlation(assigned_z) ``` ```python theme={null} def cost_hamiltonian(assigned_Z): return ( objective_func(assigned_Z) + node_resiliency(assigned_Z) + objective_minimal_resiliency(assigned_Z) + TotalConstraintNormalisation * (constraint_flow_conservation(assigned_Z)) ) ``` ## Create QAOA Code ```python theme={null} NUM_LAYERS = 20 @qfunc def mixer_layer(beta: CReal, qba: QArray[QBit]): apply_to_all(lambda q: RX(GlobalScalingParameter * beta, q), qba) @qfunc def cost_layer(gamma: CReal, qba: QArray[QBit]): phase(cost_hamiltonian(qba), gamma) @qfunc def main( params: CArray[CReal, 2 * NUM_LAYERS], z: Output[QArray[QBit, D * row_len]], ) -> None: allocate(z) hadamard_transform(z) repeat( count=NUM_LAYERS, iteration=lambda i: ( cost_layer(params[2 * i], z), mixer_layer(params[2 * i + 1], z), ), ) ``` ```python theme={null} qprog = synthesize(main) ``` # ## Pre-Execution Setup We will create a `qaoa_samples()` function that will leverage the GPU accelerated Qiskit Aer simulator ```python theme={null} import qiskit.qasm3 as qasm3 from qiskit import transpile from qiskit_aer import AerSimulator ``` ```python theme={null} qasm_string = str(qprog.transpiled_circuit.qasm) ``` ## Create the Simulator NOTE: Make you `device=gpu` is set when you want to use the AMD GPU, for now it is set to CPU. ```python theme={null} qc = qasm3.loads(qasm_string) qc.measure_all() # Here we will add the AMD GPU toggle sim = AerSimulator(device="CPU") qc_t = transpile(qc, sim) params = qc_t.parameters ``` ```python theme={null} import re def make_aer_bind_dict(qc_t, values): """ Build Aer-compatible parameter_binds dict: Parameter('params_param_k') -> [values[k]] """ values = list(values) # works for np arrays too bind = {} seen = set() for p in qc_t.parameters: m = re.search(r"params_param_(\d+)$", p.name) if not m: raise ValueError(f"Unexpected parameter name: {p.name!r}") k = int(m.group(1)) if k in seen: raise ValueError(f"Duplicate parameter index {k} in circuit params") seen.add(k) if k >= len(values): raise ValueError(f"Need values[{k}] but only {len(values)} values provided") bind[p] = [float(values[k])] # < - IMPORTANT: list, not scalar # Optional: sanity check that you covered exactly what you think you did if len(seen) != len(values): # Not always an error (circuit may use fewer params), but usually worth flagging print(f"Warning: circuit has {len(seen)} params, values has {len(values)}") return bind ``` ```python theme={null} def qaoa_samples(samples, shots=1000, flip_endian: bool = True): bind_dict = make_aer_bind_dict(qc_t, samples) job = sim.run(qc_t, shots=shots, memory=True, parameter_binds=[bind_dict]) res = job.result() samples = res.get_memory(0) if flip_endian: samples = [s[::-1] for s in samples] return samples ``` We will use a small converter to changet the qiskit output format back to Classiq format. ```python theme={null} from collections import Counter from typing import Any, Dict, List, Optional class ParsedShot: """ Mimics Classiq's ParsedShot structure for compatibility. """ def __init__(self, state: Dict[str, List[int]], shots: int): self.state = state self.shots = shots def __repr__(self): return f"ParsedShot(state={self.state}, shots={self.shots})" class ClassiqSampleResult: """ Mimics Classiq's sample result structure for compatibility. """ def __init__(self, parsed_counts: List[ParsedShot]): self.parsed_counts = parsed_counts def __repr__(self): return f"ClassiqSampleResult(parsed_counts={self.parsed_counts})" def qiskit_to_classiq_samples( qiskit_samples: np.ndarray, variable_name: str = "z", wire_labels: Optional[Dict[str, int]] = None, ): """ Convert Qiskit sample output to Classiq ExecutionSession sample format. """ # Convert numpy array to list of lists for easier processing if isinstance(qiskit_samples, np.ndarray): if qiskit_samples.ndim == 1: # Single shot, reshape to (1, n_qubits) qiskit_samples = qiskit_samples.reshape(1, -1) samples_list = qiskit_samples.tolist() else: samples_list = qiskit_samples # If wire_labels provided, reorder qubits according to labels if wire_labels is not None: # Create mapping from original index to new index ordered_indices = [wire_labels[wire] for wire in sorted(wire_labels.keys())] reordered_samples = [] for sample in samples_list: reordered_sample = [sample[i] for i in ordered_indices] reordered_samples.append(reordered_sample) samples_list = reordered_samples # Count occurrences of each unique outcome # Convert each sample to tuple for hashing, then count sample_tuples = [tuple(sample) for sample in samples_list] counts = Counter(sample_tuples) # Convert to ParsedShot objects parsed_counts = [] for outcome_tuple, shot_count in counts.items(): outcome_list = list(outcome_tuple) state = {variable_name: outcome_list} parsed_shot = ParsedShot(state=state, shots=shot_count) parsed_counts.append(parsed_shot) return ClassiqSampleResult(parsed_counts) ``` ## Execute QAOA ```python theme={null} NUM_SHOTS = 2000 backend_preferences = ClassiqBackendPreferences( backend_name=ClassiqSimulatorBackendNames.SIMULATOR ) def initial_qaoa_params(NUM_LAYERS) -> np.ndarray: initial_gammas = math.pi * np.linspace( 1 / (2 * NUM_LAYERS), 1 - 1 / (2 * NUM_LAYERS), NUM_LAYERS ) initial_betas = math.pi * np.linspace( 1 - 1 / (2 * NUM_LAYERS), 1 / (2 * NUM_LAYERS), NUM_LAYERS ) initial_params = [] for i in range(NUM_LAYERS): initial_params.append(initial_gammas[i]) initial_params.append(initial_betas[i]) return np.array(initial_params) initial_params = initial_qaoa_params(NUM_LAYERS) ``` ```python theme={null} def estimate_cost(params): samples = qaoa_samples(params) samples = [[int(c) for c in row] for row in samples] shot_costs = np.array([cost_hamiltonian(s) for s in samples], dtype=float) objective_val = shot_costs.mean() print(f"Cost Hamiltonian = {np.round(objective_val, decimals=3)}") return objective_val ``` ```python theme={null} cost_func = lambda state: cost_hamiltonian(state["z"]) def estimate_cost_func(params): objective_val = estimate_cost(params.tolist()) print(f"Cost Hamiltonian = {np.round(objective_val, decimals=3)}") return objective_val ``` ```python theme={null} MAX_ITERATIONS = 1 # Increase to improve the results optimization_res = minimize( estimate_cost_func, x0=initial_params, method="COBYLA", options={"maxiter": MAX_ITERATIONS}, ) ``` **Output:** ``` Cost Hamiltonian = 1.351 Cost Hamiltonian = 1.351 ``` ```python theme={null} samples = qaoa_samples( optimization_res.x.tolist(), shots=40000 ) # es.sample({"params": optimization_res.x.tolist()}) res = qiskit_to_classiq_samples([[int(c) for c in row] for row in samples]) ``` ```python theme={null} def check_validity(assigned_z: list[int]) -> bool: if constraint_flow_conservation(assigned_z) != 0: return False if node_resiliency(assigned_z) != 0: return False return True ``` ```python theme={null} sorted_counts = sorted(res.parsed_counts, key=lambda pc: pc.shots, reverse=True) def print_res(sampled, idx): assigned_z = sampled.state["z"] if not check_validity(assigned_z): return valid_solutions_indices.append(idx) print( f"Valid solution found at index {idx}, probability={sampled.shots/NUM_SHOTS*100}%, cost={cost_hamiltonian(assigned_z)}, latency={round(objective_func(assigned_z) / TotalCostNormalisation * (max_lat_guess - min_lat_guess))}" ) valid_solutions_indices = [] for idx, sampled in enumerate(sorted_counts[:100]): print_res(sampled, idx) print( f"Accumulated valid probability is {sum(sampled.shots for sampled in sorted_counts if check_validity(sampled.state['z']))/NUM_SHOTS*100}%" ) ``` **Output:** ``` Valid solution found at index 1, probability=12.55%, cost=0.1, latency=4 Valid solution found at index 7, probability=6.65%, cost=0.125, latency=5 Valid solution found at index 14, probability=4.6%, cost=0.125, latency=5 Valid solution found at index 83, probability=1.4500000000000002%, cost=1.075, latency=3 Accumulated valid probability is 25.8% ``` ```python theme={null} def convert_1d_to_2d(array_1d): array_2d = np.zeros((D, row_len)) for d_idx in range(D): for e_idx in range(m): array_2d[d_idx][e_idx] = array_1d[edge_fidx(d_idx, e_idx)] for n_idx in range(n): array_2d[d_idx][m + n_idx] = array_1d[node_fidx(d_idx, n_idx)] return array_2d ``` ```python theme={null} def print_solution(index): print(index) solution = sorted_counts[index].state["z"] sol_2d = convert_1d_to_2d(solution) print_solution_graph(sol_2d) sol_df = pd.DataFrame( sol_2d, index=[f"{d['name']}({d['s']}→{d['t']})" for d in demands], columns=ids ) print(sol_df) print( f"{objective_func(solution)/TotalCostNormalisation * (max_lat_guess-min_lat_guess)=}" ) print(f"cost={cost_hamiltonian(solution)}") print(f"Flow Conservation: {constraint_flow_conservation(solution)}") print(f"Resiliency: {node_resiliency(solution)}") # for index in valid_solutions_indices: for index in valid_solutions_indices: print_solution(index) ``` **Output:** ``` 1 ``` output **Output:** ``` (P1,S1) (P1,S2) (S2,S1) (P2,S1) (S3,S1) (P2,S3) (S3,S2) P1 \ d1(S2→P1) 0.0 1.0 0.0 0.0 0.0 0.0 0.0 1.0 d2(S2→P2) 0.0 0.0 0.0 0.0 0.0 1.0 1.0 0.0 P2 S1 S2 S3 d1(S2→P1) 0.0 0.0 1.0 0.0 d2(S2→P2) 1.0 0.0 1.0 1.0 objective_func(solution)/TotalCostNormalisation * (max_lat_guess-min_lat_guess)=4.0 cost=0.1 Flow Conservation: 0 Resiliency: 0.0 7 ``` output **Output:** ``` (P1,S1) (P1,S2) (S2,S1) (P2,S1) (S3,S1) (P2,S3) (S3,S2) P1 \ d1(S2→P1) 0.0 1.0 0.0 0.0 0.0 0.0 0.0 1.0 d2(S2→P2) 0.0 0.0 0.0 1.0 1.0 0.0 1.0 0.0 P2 S1 S2 S3 d1(S2→P1) 0.0 0.0 1.0 0.0 d2(S2→P2) 1.0 1.0 1.0 1.0 objective_func(solution)/TotalCostNormalisation * (max_lat_guess-min_lat_guess)=5.0 cost=0.125 Flow Conservation: 0 Resiliency: 0.0 14 ``` output **Output:** ``` (P1,S1) (P1,S2) (S2,S1) (P2,S1) (S3,S1) (P2,S3) (S3,S2) P1 \ d1(S2→P1) 1.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 d2(S2→P2) 0.0 0.0 0.0 0.0 0.0 1.0 1.0 0.0 P2 S1 S2 S3 d1(S2→P1) 0.0 1.0 1.0 0.0 d2(S2→P2) 1.0 0.0 1.0 1.0 objective_func(solution)/TotalCostNormalisation * (max_lat_guess-min_lat_guess)=5.0 cost=0.125 Flow Conservation: 0 Resiliency: 0.0 83 ``` output **Output:** ``` (P1,S1) (P1,S2) (S2,S1) (P2,S1) (S3,S1) (P2,S3) (S3,S2) P1 \ d1(S2→P1) 0.0 1.0 0.0 0.0 0.0 0.0 0.0 1.0 d2(S2→P2) 0.0 0.0 1.0 1.0 0.0 0.0 0.0 0.0 P2 S1 S2 S3 d1(S2→P1) 0.0 0.0 1.0 0.0 d2(S2→P2) 1.0 1.0 1.0 0.0 objective_func(solution)/TotalCostNormalisation * (max_lat_guess-min_lat_guess)=3.0000000000000004 cost=1.075 Flow Conservation: 0 Resiliency: 0.0 ``` # Arithmetic Expressions Source: https://docs.classiq.io/explore/functions/function_usage_examples/arithmetic/arithmetic_expression/arithmetic_expression_example Open this notebook in GitHub to run it yourself Use the `Arithmetic` function to write complex mathematical expression in free format. The notation follows the Python language for math notation. The function first parses the expression and builds an abstract syntax tree (AST). Then, the Classiq engine finds a computation strategy for a specified number of qubits, and compiles the desired quantum program. As opposed to classical computers, when quantum computers evaluate arithmetic expression, the calculations are reversible and are applied on all quantum states in parallel. To do so, quantum computers store all intermediate computation results in a quantum variables. Qubits that are not freed cannot be used later on in the quantum program. Analogously to the classical world, there is a form of quantum "garbage collection", usually referred to as uncomputation, which returns the garbage qubits to their original state. The computation strategy determines the order in which qubits are released and reused. By employing different strategies, you can produce a variety of quantum programs with the same functionality. In general, longer quantum programs require less qubits than shorter ones. Supported operators: * Add: `+` * Subtract: `-` (two arguments) * Negate: `-` (a single argument) * Multiply: `*` * Bitwise Or: `|` * Bitwise And: `&` * Bitwise Xor: `^` * Invert: `~` * Equal: `==` * Not Equal: `!=` * Greater Than: `>` * Greater Or Equal: `>=` * Less Than: `<` * Less Or Equal: `<=` * Modulo: `%` limited for power of 2 * Logical And: `and` * Logical Or: `or` * Max: `max` (n>=2 arguments) * Min: `min` (n>=2 arguments) * Power `**` (register base, positive int power) ## Example This example generates a quantum program that calculates the expression `(a + b + c & 15) % 8 ^ 3 & a ^ 10 == 4`. Each of the variables `a`,`b`, and `c` is defined as a quantum varialbe with a different size. ```python theme={null} from classiq import * @qfunc def main(a: Output[QNum], b: Output[QNum], c: Output[QNum], res: Output[QNum]) -> None: a |= 2 b |= 1 c |= 5 res |= (a + b + c & 15) % 8 ^ 3 & a ^ 10 == 4 ``` ```python theme={null} qprog = synthesize(main) result = execute(qprog).result_value() result.parsed_counts ``` **Output:** ``` [{'a': 2, 'b': 1, 'c': 5, 'res': 0}: 2048] ``` # Bitwise And Source: https://docs.classiq.io/explore/functions/function_usage_examples/arithmetic/bitwise_and/bitwise_and_example Open this notebook in GitHub to run it yourself The Bitwise And (denoted as '&') is implemented by applying this truth table between each pair of qubits in register A and B (or qubit and bit).
| a | b | a & b | | :-: | :-: | :---: | | 0 | 0 | 0 | | 0 | 1 | 0 | | 1 | 0 | 0 | | 1 | 1 | 1 |
Note that integer and fixed-point numbers are represented in a two-complement method during function evaluation. The binary number is extended in the case of a register size mismatch. For example, the positive signed number $(110)_2=6$ is expressed as $(00110)_2$ when operating with a five-qubit register. Similarly, the negative signed number $(110)_2=-2$ is expressed as $(11110)_2$. Examples: 5 & 3 = 1 since 101 & 011 = 001 5 & -3 = 5 since 0101 & 1101 = 0101 -5 & -3 = -7 since 1011 & 1101 = 1001 ## Examples # ### Example 1: Two Quantum Variables This example generates a quantum program that performs a bitwise 'and' between two variables, both are unsigned integer ```python theme={null} from classiq import * @qfunc def main(a: Output[QNum], b: Output[QNum], res: Output[QNum]) -> None: a |= 4 b |= 5 res |= a & b qmod = create_model(main) ``` ```python theme={null} qprog = synthesize(qmod) result = execute(qprog).result_value() result.parsed_counts ``` **Output:** ``` [{'a': 4.0, 'b': 5.0, 'res': 4.0}: 1000] ``` # ### Example 2: Integer and Quantum Variable This example generates a quantum program that performs a bitwise 'and' between a quantum variable and an integer. The left arg is an integer equal to 3 and the right arg is an unsigned quantum variable with three qubits. ```python theme={null} @qfunc def main(a: Output[QNum], res: Output[QNum]) -> None: a |= 5 res |= 3 & a qmod = create_model(main) ``` ```python theme={null} qprog = synthesize(qmod) result = execute(qprog).result_value() result.parsed_counts ``` **Output:** ``` [{'a': 5.0, 'res': 1.0}: 1000] ``` # Bitwise Invert Source: https://docs.classiq.io/explore/functions/function_usage_examples/arithmetic/bitwise_invert/bitwise_invert_example Open this notebook in GitHub to run it yourself The bitwise inversion operation receives a quantum register representing some number $x$, and inverts its binary representation, namely, replaces $1$s by $0$s and vice versa. ## Example ```python theme={null} from classiq import * @qfunc def main(x: Output[QNum], y: Output[QNum]) -> None: x |= 6 y |= ~x ``` ```python theme={null} qprog = synthesize(main) result = execute(qprog).result_value() print(result.counts_of_multiple_outputs(["x", "y"])) ``` **Output:** ``` {('011', '100'): 1000} ``` # Bitwise Or Source: https://docs.classiq.io/explore/functions/function_usage_examples/arithmetic/bitwise_or/bitwise_or_example Open this notebook in GitHub to run it yourself The Bitwise Or (denoted as '|') is implemented by applying the following truth table between each pair of qubits (or qubit and bit) in variables A and B.
| a | b | a or b | | :-: | :-: | :----: | | 0 | 0 | 0 | | 0 | 1 | 1 | | 1 | 0 | 1 | | 1 | 1 | 1 |
Note that integer and fixed-point numbers are represented in a two-complement method during function evaluation. The binary number is extended in the case of a variable size mismatch. For example, the positive signed number $(110)_2=6$ is expressed as $(00110)_2$ when operating with a five-qubit variable. Similarly, the negative signed number $(110)_2=-2$ is expressed as $(11110)_2$. Examples: 5 | 3 = 7 since 101 | 011 = 111 5 | -3 = -3 since 0101 | 1101 = 1101 -5 | -3 = -1 since 1011 | 1101 = 1111 ## Examples # ### Example 1: Two Quantum Variables This example generates a quantum program that performs bitwise 'or' between two variables. The left arg is a signed with five qubits and the right arg is unsigned with three qubits. ```python theme={null} from classiq import * @qfunc def main(a: Output[QNum], b: Output[QNum], res: Output[QNum]) -> None: allocate(5, True, 0, a) allocate(3, False, 0, b) a ^= 4 b ^= 5 res |= a | b qmod = create_model(main) ``` ```python theme={null} qprog = synthesize(qmod) result = execute(qprog).result_value() print(result.parsed_counts) print(result.counts_of_multiple_outputs(["a", "b", "res"])) ``` **Output:** ``` [{'a': 4.0, 'b': 5.0, 'res': 5.0}: 1000] {('00100', '101', '10100'): 1000} ``` # ### Example 2: Integer and Quantum Variable This example generates a quantum program that performs a bitwise 'or' between a quantum variable and an integer. The left arg is an integer equal to three and the right arg is an unsigned quantum variable with three qubits. ```python theme={null} @qfunc def main(a: Output[QNum], res: Output[QNum]) -> None: a |= 4 res |= 3 | a qmod = create_model(main) ``` ```python theme={null} qprog = synthesize(qmod) result = execute(qprog).result_value() result.parsed_counts ``` **Output:** ``` [{'a': 4.0, 'res': 7.0}: 1000] ``` # Bitwise Xor Source: https://docs.classiq.io/explore/functions/function_usage_examples/arithmetic/bitwise_xor/bitwise_xor_example Open this notebook in GitHub to run it yourself The Bitwise Xor (denoted as '^') is implemented by applying this truth table between each pair of qubits (or qubit and bit) in variables A and B.
| a | b | a ^ b | | :-: | :-: | :---: | | 0 | 0 | 0 | | 0 | 1 | 1 | | 1 | 0 | 1 | | 1 | 1 | 0 |
Note that integer and fixed-point numbers are represented in a two-complement method during function evaluation. The binary number is extended in the case of a variable size mismatch. For example, the positive signed number $(110)_2=6$ is expressed as $(00110)_2$ when operating with a five-qubit variable. Similarly, the negative signed number $(110)_2=-2$ is expressed as $(11110)_2$. Examples: 5 ^ 3 = 6 since 101 ^ 011 = 110 5 ^ -3 = -8 since 0101 ^ 1101 = 1000 -5 ^ -3 = 6 since 1011 ^ 1101 = 0110 ## Examples # ### Example 1: Two Quantum Variables This example generates a quantum program that performs bitwise 'xor' between two variables. The left arg is a signed with five qubits and the right arg is unsigned with three qubits. ```python theme={null} from classiq import * @qfunc def main(a: Output[QNum], b: Output[QNum], res: Output[QNum]) -> None: allocate(5, SIGNED, 0, a) allocate(3, UNSIGNED, 0, b) a ^= 4 b ^= 5 res |= a ^ b qmod = create_model(main) ``` ```python theme={null} qprog = synthesize(qmod) result = execute(qprog).result_value() print(result.parsed_counts) print(result.counts_of_multiple_outputs(["a", "b", "res"])) ``` **Output:** ``` [{'a': 4.0, 'b': 5.0, 'res': 1.0}: 1000] {('00100', '101', '10000'): 1000} ``` # ### Example 2: Integer and Quantum Variable This example generates a quantum program that performs a bitwise 'xor' between a quantum variable and an integer. The left arg is an integer equal to three and the right arg is an unsigned quantum variable with three qubits. ```python theme={null} @qfunc def main(a: Output[QNum], res: Output[QNum]) -> None: a |= 4 res |= 3 ^ a qmod = create_model(main) ``` ```python theme={null} qprog = synthesize(qmod) result = execute(qprog).result_value() result.parsed_counts ``` **Output:** ``` [{'a': 4.0, 'res': 7.0}: 1000] ``` # Comparators Source: https://docs.classiq.io/explore/functions/function_usage_examples/arithmetic/comparator/comparator_example Open this notebook in GitHub to run it yourself The following comparators are supported: * Equal (denoted as '==') * NotEqual (denoted as '!=') * GreaterThan (denoted as '>') * GreaterEqual (denoted as '>=') * LessThan (denoted as '\<') * LessEqual (denoted as '\<=') Note that integer and fixed-point numbers are represented in a 2-complement method during function evaluation. The binary number is extended in the case of a register size miss-match. For example, the positive signed number $(110)_2=6$ is expressed as $(00110)_2$ when operating with a 5-qubit register. Similarly, the negative signed number $(110)_2=-2$ is expressed as $(11110)_2$. Examples: (5 \<= 3) = 0 (5 == 5) = 1 ($(011)_2$ == $(11)_2$) = 1 (signed $(101)_2$ \< unsigned $(101)_2$) = 1 ## Examples # ### Example 1: Comparing Two Quantum Variables This example generates a quantum program that performs 'equal' between two variables. The left arg is a signed variable with 5 qubits and the right arg is an unsigned varialbe with 3 qubits. ```python theme={null} from classiq import * @qfunc def main(a: Output[QNum], b: Output[QNum], res: Output[QNum]) -> None: allocate(5, True, 0, a) allocate(3, False, 0, b) res |= a == b qmod = create_model(main) ``` ```python theme={null} qprog = synthesize(qmod) ``` # ### Example 2: Comparing Integer and Quantum Variable This example generates a quantum program that performs 'less equal' between a quantum register and an integer. The left arg is an unsigned quantum variable with 3 qubits, and the right arg is an integer equal to 2. ```python theme={null} @qfunc def main(a: Output[QNum], res: Output[QNum]) -> None: allocate(3, a) hadamard_transform(a) res |= a <= 2 qmod = create_model(main) ``` ```python theme={null} qprog = synthesize(qmod) result = execute(qprog).result_value() result.parsed_counts ``` **Output:** ``` [{'a': 4.0, 'res': 0.0}: 150, {'a': 1.0, 'res': 1.0}: 138, {'a': 7.0, 'res': 0.0}: 132, {'a': 2.0, 'res': 1.0}: 126, {'a': 6.0, 'res': 0.0}: 123, {'a': 3.0, 'res': 0.0}: 115, {'a': 5.0, 'res': 0.0}: 110, {'a': 0.0, 'res': 1.0}: 106] ``` # Minimum and Maximum Source: https://docs.classiq.io/explore/functions/function_usage_examples/arithmetic/extremum/extremum_example Open this notebook in GitHub to run it yourself The minimum and maximum operators determine the smallest and largest input, respectively. Both functions receive two inputs. Each may be a fixed point number or a quantum register. ## Examples # ### Example 1: Two Quantum Variables Minimum This code example generates a quantum program that returns a minimum of two arguments. Both the left and right arguments are defined as quantum variables of size three. ```python theme={null} from classiq import * from classiq.qmod.symbolic import min @qfunc def main(a: Output[QNum], b: Output[QNum], res: Output[QNum]) -> None: a |= 4 allocate(3, b) hadamard_transform(b) res |= min(a, b) qmod = create_model(main) ``` ```python theme={null} qprog = synthesize(qmod) result = execute(qprog).result_value() result.parsed_counts ``` **Output:** ``` [{'a': 4.0, 'b': 1.0, 'res': 1.0}: 136, {'a': 4.0, 'b': 7.0, 'res': 4.0}: 129, {'a': 4.0, 'b': 6.0, 'res': 4.0}: 126, {'a': 4.0, 'b': 4.0, 'res': 4.0}: 125, {'a': 4.0, 'b': 2.0, 'res': 2.0}: 122, {'a': 4.0, 'b': 3.0, 'res': 3.0}: 122, {'a': 4.0, 'b': 5.0, 'res': 4.0}: 122, {'a': 4.0, 'b': 0.0, 'res': 0.0}: 118] ``` # ### Example 2: Float and Quantum Variable Maximum This code example returns a quantum program with a maximum of two arguments. Here, the left arg is a fixed-point number $(11.1)_2$ (3.5), and the right arg is a quantum variable of size three. ```python theme={null} from classiq.qmod.symbolic import max @qfunc def main(a: Output[QNum], res: Output[QNum]) -> None: allocate(3, a) hadamard_transform(a) res |= max(3.5, a) qmod = create_model(main) ``` ```python theme={null} qprog = synthesize(qmod) result = execute(qprog).result_value() result.parsed_counts ``` **Output:** ``` [{'a': 5.0, 'res': 5.0}: 135, {'a': 0.0, 'res': 3.5}: 131, {'a': 3.0, 'res': 3.5}: 130, {'a': 6.0, 'res': 6.0}: 129, {'a': 2.0, 'res': 3.5}: 127, {'a': 7.0, 'res': 7.0}: 122, {'a': 1.0, 'res': 3.5}: 115, {'a': 4.0, 'res': 4.0}: 111] ``` # Modular Exponentiation Source: https://docs.classiq.io/explore/functions/function_usage_examples/arithmetic/modular_exp/modular_exp_example Open this notebook in GitHub to run it yourself The `modular_exp` function raises a classical integer `a` to the power of a quantum number `power` modulo classical integer `n`, times a quantum number `x`. The function performs: $$ |x\rangle |power\rangle = |(a^{power} \mod n)\cdot x\rangle | power\rangle $$ Specifically if at the input $x=1$, at the output $x=a^{power} \mod n$. ## Example This example generates a quantum program that initializes a `power` variable with a uniform superposition, and exponentiate the classical value `A` with `power` as the exponent, in superposition. The result is calculated inplace to the variable `x` module `N`. Notice that `x` should have size of at least $\lceil(log_2(N) \rceil$, so it is first allocated with a fixed size, then initialized with the value '1'. ```python theme={null} import numpy as np from classiq import * from classiq.qmod.symbolic import ceiling, log # constants N = 5 A = 4 @qfunc def main(power: Output[QNum], x: Output[QNum]) -> None: allocate(ceiling(log(N, 2)), x) x ^= 1 # initialize a uniform superposition of powers allocate(3, power) hadamard_transform(power) modular_exp(n=N, a=A, x=x, power=power) qmod = create_model(main) qmod = create_model(main) qprog = synthesize(qmod) ``` ```python theme={null} result = execute(qprog).result_value() result.parsed_counts ``` **Output:** ``` [{'power': 3, 'x': 4}: 136, {'power': 1, 'x': 4}: 135, {'power': 7, 'x': 4}: 130, {'power': 5, 'x': 4}: 124, {'power': 2, 'x': 1}: 124, {'power': 4, 'x': 1}: 123, {'power': 6, 'x': 1}: 117, {'power': 0, 'x': 1}: 111] ``` Verify all results are as expected: ```python theme={null} assert np.all( [ count.state["x"] == (A ** count.state["power"] % N) for count in result.parsed_counts ] ) ``` # Modulo Source: https://docs.classiq.io/explore/functions/function_usage_examples/arithmetic/modulo/modulo_example Open this notebook in GitHub to run it yourself The modulo operation (denoted as '%') returns the remainder (called "modulus") of a division. Given two numbers $a$ and $n$, the result of ($a \% n$) is the remainder of the division of a by n. The modulo operation is supported only for $n = 2^m$ for an integer $m$, its result is the $m$ least significant bits. For example, the binary representation of the number $53$ is $0b110101$. The expression ($53 \% 8$) equals $0b101 = 5$, because $8 = 2^3$, which means only accounting for the $3$ least significant bits of $53$. # ### Implementation in Expressions If an expression is defined using a modulo operation, the output size is set recursively to all of its subexpressions. But if for some sub-expressions, another modulo operation is used, the sub-expression's output\_size is determined by the minimal value between the output\_size of the sub-expression and the expression. See this example: $(((a + b) \% 4) + (c + d)) \% 8$. The result of expression $a + b$ is saved on a two-qubit register, and the results of expressions $c + d$ and $((a + b) \% 4) + (c + d)$ are saved using three qubits each. ## Example This example generates a quantum program that adds two five-qubit arguments: a on qubits 0-4, and b on qubits 5- 9. The adder result should have been calculated on a 6-qubit register. However, the modulo operation decides that the output register of the adder only contains its two least significant qubits. Thus, the adder result is written to a two-qubit register, on qubits 10- 10. ```python theme={null} from classiq import * @qfunc def main(a: Output[QNum], b: Output[QNum], res: Output[QNum]) -> None: allocate(5, a) allocate(5, b) a ^= 4 b ^= 7 res |= (a + b) % 4 ``` ```python theme={null} qprog = synthesize(main) result = execute(qprog).result_value() print(result.parsed_counts) ``` **Output:** ``` [{'a': 4.0, 'b': 7.0, 'res': 3.0}: 1000] ``` # Multiplication Source: https://docs.classiq.io/explore/functions/function_usage_examples/arithmetic/multiplication/multiplication Open this notebook in GitHub to run it yourself The multiplication operation, denoted '$*$', is a series of additions ("long multiplication"). The multiplier has different implementations, depending on the type of adder in use. Note that integer and fixed-point numbers are represented in a two-complement method during function evaluation. The binary number is extended in the case of a register size mismatch. For example, the positive signed number $(110)_2=6$ is expressed as $(00110)_2$ when working with a five-qubit register. Similarly, the negative signed number $(110)_2=-2$ is expressed as $(11110)_2$. ## Examples The calculation of -5 \* 3 = - 1 5. The left arg -5 is represented as 1011 and 3 as 1 1. The number of digits needed to store the answer is 4+2-1 = 2. The multiplication is done in the 'regular' manner where each number is extended to five bits and only five digits are kept in the intermediary results. $$ \begin{array}{c} \phantom{\times}11011\\ \underline{\times\phantom{000}11}\\ \phantom{\times}11011\\ \underline{\phantom\times1011\phantom9}\\ \phantom\times10001 \end{array} $$ ## Examples # ### Example 1: Two Quantum Variables Multiplication This code example generates a quantum program that multiplies two arguments. Both of them are defined as quantum variables of size 3. ```python theme={null} from classiq import * @qfunc def main(a: Output[QNum], b: Output[QNum], res: Output[QNum]) -> None: a |= 4 b |= 5 res |= a * b qmod = create_model(main) ``` ```python theme={null} qprog = synthesize(qmod) result = execute(qprog).result_value() result.parsed_counts ``` **Output:** ``` [{'a': 4.0, 'b': 5.0, 'res': 20.0}: 1000] ``` # ### Example 2: Float and Quantum Variable Multiplication This code example generates a quantum program that multiplies two arguments. Here, the left argument is a fixed-point number $(11.1)_2$ (3.5), and the right argument is a quantum variable of size 2. ```python theme={null} @qfunc def main(a: Output[QNum], res: Output[QNum]) -> None: allocate(2, a) hadamard_transform(a) res |= 3.5 * a qmod = create_model(main) ``` ```python theme={null} qprog = synthesize(qmod) result = execute(qprog).result_value() result.parsed_counts ``` **Output:** ``` [{'a': 2.0, 'res': 7.0}: 287, {'a': 3.0, 'res': 10.5}: 257, {'a': 1.0, 'res': 3.5}: 230, {'a': 0.0, 'res': 0.0}: 226] ``` # Negation Source: https://docs.classiq.io/explore/functions/function_usage_examples/arithmetic/negation/negation_example Open this notebook in GitHub to run it yourself The negation operation receives a quantum register representing some number $x$ and returns a quantum register containing $-x$. Integer and fixed point numbers are both supported. ## Example The following example will show negation of a signed quantum variable. ```python theme={null} from classiq import * @qfunc def main(a: Output[QNum], b: Output[QNum]) -> None: allocate(3, SIGNED, 0, a) hadamard_transform(a) b |= -a ``` ```python theme={null} qprog = synthesize(main) result = execute(qprog).result_value() result.parsed_counts ``` **Output:** ``` [{'a': 0.0, 'b': 0.0}: 143, {'a': -2.0, 'b': 2.0}: 136, {'a': -1.0, 'b': 1.0}: 130, {'a': 1.0, 'b': -1.0}: 122, {'a': 2.0, 'b': -2.0}: 121, {'a': 3.0, 'b': -3.0}: 120, {'a': -4.0, 'b': 4.0}: 114, {'a': -3.0, 'b': 3.0}: 114] ``` # Subtraction Source: https://docs.classiq.io/explore/functions/function_usage_examples/arithmetic/subtraction/subtraction_example Open this notebook in GitHub to run it yourself Subtraction (denoted as '-') is implemented by negation and addition in 2-complement representation. $$ a - b \longleftrightarrow a + (-b) \longleftrightarrow a + \sim{b} + lsb\_value $$ Where '\~' is bitwise not and $lsb\_value$ is the least significant bit value. Note that integer and fixed-point numbers are represented in a 2-complement method during function evaluation. The binary number is extended in the case of a register size miss-match. For example, the positive signed number $(110)_2=6$ is expressed as $(00110)_2$ when operating with a 5-qubit register. Similarly, the negative signed number $(110)_2=-2$ is expressed as $(11110)_2$. Examples: 5 + 3 = 8 , 0101 + 0011 = 1000 5 - 3 = 5 + (-3) = 2, 0101 + 1101 = 0010 -5 + -3 = -8, 1011 + 1101 = 1000 ## Examples # ### Example 1: Subtraction of Two Quantum Variables This example generates a quantum program that subtracts one quantum variables from the other, both of size 3 qubits. ```python theme={null} from classiq import * @qfunc def main(a: Output[QNum], b: Output[QNum], res: Output[QNum]) -> None: a |= 4 b |= 5 res |= a - b qmod = create_model(main) ``` ```python theme={null} qprog = synthesize(qmod) result = execute(qprog).result_value() print(result.parsed_counts) ``` **Output:** ``` [{'a': 4.0, 'b': 5.0, 'res': -1.0}: 1000] ``` # ### Example 2: Subtraction of a Float from a Register This example generates a quantum program which subtracts two argument. The left\_arg is defined to be a fix point number $(11.1)_2$ (3.5). The right\_arg is defined to be a quantum register of size of three. ```python theme={null} @qfunc def main(a: Output[QNum], res: Output[QNum]) -> None: a |= 4 res |= a - 3.5 qmod = create_model(main) ``` ```python theme={null} qprog = synthesize(qmod) result = execute(qprog).result_value() print(result.parsed_counts) ``` **Output:** ``` [{'a': 4.0, 'res': 0.5}: 1000] ``` # Multi-Control-X Source: https://docs.classiq.io/explore/functions/function_usage_examples/mcx/mcx_example Open this notebook in GitHub to run it yourself The multi-control-X applies X gate to one target qubit bit only if the logical AND of all control qubits is satisfied. The multi-control-X function incorporates numerous implementations for the multi-control-X gate, each with a different depth and number of auxiliary qubits. These implementations generically outperform the Gray-code, V-chain and recursive implementations of Ref. [\[1\]](#1), as well as the relative-phase Toffoli implementation of Ref. [\[2\]](#2). Given a sufficient number of auxiliary qubits, some implementations allow for logarithmic depth and linear CX-count. The synthesis process selects the appropriate implementation depending on the defined constraints. Operator: `control` Arguments: * `ctrl: Union[QBit, QArray[QBit]]` * `stmt_block: QCallable` Operator `control` takes a qubit array of length one or more as `ctrl`, and applies the `stmt_block` operand if all qubits are in the `1` state ## Example The following example shows how to use the `control` operator to implement a Multi-Control-X where a 7-qubit quantum variable serves as the control ```python theme={null} from classiq import * @qfunc def main(cntrl: Output[QArray[QBit]], target: Output[QBit]) -> None: allocate(7, cntrl) allocate(target) control(ctrl=cntrl, stmt_block=lambda: X(target)) ``` ```python theme={null} qprog = synthesize(main) ``` ## References \[1] A. Barenco et al, Elementary gates for quantum computation, Phys. Rev. A 52 (1995). [https://journals.aps.org/pra/abstract/10.1103/PhysRevA.52.3457](https://journals.aps.org/pra/abstract/10.1103/PhysRevA.52.3457) \[2] D. Maslov, Advantages of using relative-phase Toffoli gates with an application to multiple control Toffoli optimization, Phys. Rev. A 93 (2016). [https://journals.aps.org/pra/abstract/10.1103/PhysRevA.93.022311](https://journals.aps.org/pra/abstract/10.1103/PhysRevA.93.022311) # Classiq Library Source: https://docs.classiq.io/explore/index This chapter contains a wide variety of functions, algorithms, applications and tutorials of quantum programs with Classiq. Everything can also be found in the [Classiq Git Library](https://github.com/Classiq/classiq-library), where you can contribute your own tutorials and algorithms that will appear in this documentation as well. In addition, all the examples can be found directly in the [platform](https://platform.classiq.io/) in the `Model` tab. You can download the tutorials as Jupyter Notebooks. # Discrete Quantum Walks Source: https://docs.classiq.io/explore/tutorials/advanced_tutorials/discrete_quantum_walk/discrete_quantum_walk Open this notebook in GitHub to run it yourself "Quantum Walk" is an approach for developing and designing quantum algorithms. Consider it as a quantum analogy to the classical random walk. This generic technique underlies many quantum algorithms. In particular, the Grover's search algorithm can be viewed as a quantum walk. Similarly to classical random walks, quantum walks are divided into the discrete and continuous cases. This notebook focuses on discrete quantum walks, and is organized as follows: 1. [One to one](#classical-random-walks-vs-quantum-walks) compares classical random walks with quantum walks via a specific example: a walk on a circle. 2. A [general quantum model](#how-to-build-a-general-quantum-walk-with-classiq) studies quantum walks and subsequently applies it to the [circle case](#example-symmetric-quantum-walk-on-a-circle) and to a [hypercube graph](#example-4d-hypercube-with-a-grover-coin). This tutorial demonstrates the following concepts of Classiq: * The resemblance between classical and quantum programming with Classiq. * Classiq's built-in constructs: * `control`: general controlled-logic * `power`: "parametric power", specified as execution parameter. * `numeric quantum types`: working with signed and unsigned integers. * `within_apply` : for compute/uncompute operations $U V U^{\dagger}$. * Passing a list of `QCallables` (quantum functions). * Parsed execution results, according to quantum variable types. *** ## Classical Random Walks Versus Quantum Walks Specify and implement a simple example: a discrete walk on a circle, or more precisely, on a regular polygon with $2^N$ nodes (see Figure 1). The idea behind the random/quantum walk: 1. Start at some initial point, e.g., the zeroth node. 2. Flip a coin. Heads moves one step clockwise; tails moves counterclockwise. 1. Repeat steps 1-2 for a given total number of steps $T$. ```python theme={null} import matplotlib.pyplot as plt import numpy as np from classiq import * ``` Code a classical random walk and a discrete quantum walk side by side: Define a function to flip a coin: * Classical: the coin is either 0 or 1. To flip the coin, draw a random number from the set $\{0,1\}$. * Quantum: the coin is represented by a qubit and a "flip" is defined by some unitary operation on it. Choose the Hadamard gate, which sends the $|0\rangle$ state into an equal superposition of $|0\rangle$ and $|1\rangle$. ```python theme={null} def classical_coin_flip(coin): return np.random.randint(2) @qfunc def quantum_coin_flip(coin: QBit): H(coin) ``` Next, define a function for moving clockwise and counterclockwise. This operation is a modular addition by $\pm 1$: * Classical: the position is an integer in $[-2^{N-1}, 2^{N-1}-1]$. Use basic arithmetic operations. * Quantum: the position is an $N$-qubits state. Call in-place modular addition by 1. Note that since quantum operations are reversible, you can define a counterclockwise step as the inverse of the clockwise step. ```python theme={null} from classiq.qmod.symbolic import pi # classical def classical_step_clockwise(x, circle_size): return (x + 1) % circle_size def classical_step_counterclockwise(x, circle_size): return (x - 1) % circle_size # quantum @qperm def quantum_step_clockwise(x: QNum): x += 1 ``` Finally, construct a function for the full walk, iterating between flipping a coin and a single walking step based on the coin's state. Note the difference between the classical and quantum function declarations. For the quantum part you do not need to pass the circle size, as it is given by the size of the position state $x$. ```python theme={null} # classical def random_walk_circle( time, # total time x, # position circle_size, # the size of the circle ): coin = 0 for step in range(time): coin = classical_coin_flip(coin) if coin == 0: x = classical_step_clockwise(x, CIRCLE_SIZE) if coin == 1: x = classical_step_counterclockwise(x, CIRCLE_SIZE) return x # quantum @qfunc def discrete_quantum_walk_circle( time: CInt, # total time coin: QBit, # coin x: QArray, # position ): power( time, lambda: ( quantum_coin_flip(coin), control( coin == 0, lambda: quantum_step_clockwise(x), lambda: invert(lambda: quantum_step_clockwise(x)), ), ), ) ``` Define and run a specific example, taking a circle of size $2^7$, a total time of 50 steps, and 10,000 samples. ```python theme={null} CIRCLE_SIZE = 2**7 TOTAL_TIME = 50 NUM_SAMPLES = 10000 ``` ```python theme={null} from classiq.execution import ExecutionPreferences from classiq.qmod.symbolic import floor, log # classical final_pos = np.zeros(CIRCLE_SIZE) for sample in range(NUM_SAMPLES): x = random_walk_circle(time=TOTAL_TIME, x=0, circle_size=CIRCLE_SIZE) final_pos[x] += 1 # quantum @qfunc def main( t: CInt, x: Output[QNum[floor(log(CIRCLE_SIZE, 2)), SIGNED, 0]], coin: Output[QBit] ): allocate(x) allocate(coin) discrete_quantum_walk_circle(t, coin, x) qmod_1 = create_model( main, execution_preferences=ExecutionPreferences(num_shots=NUM_SAMPLES), ) qprog_1 = synthesize(qmod_1) show(qprog_1) with ExecutionSession(qprog_1) as es: result_1 = es.sample({"t": 50}) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/30HaT5Wr5mIPQAuGiEZCUj1r9VW ``` Screenshot 2025-07-23 at 22.52.24.png Plot the probabilities of ending at each position along the circle. In both the classical and quantum cases, consider the probability of the final position after time $T=50$. ```python theme={null} # classical prob_classical = final_pos / NUM_SAMPLES grid = (np.linspace(0, CIRCLE_SIZE - 1, CIRCLE_SIZE)).astype(int) flipped_grid = np.append( grid[CIRCLE_SIZE // 2 : CIRCLE_SIZE] - CIRCLE_SIZE, grid[0 : CIRCLE_SIZE // 2] ) flipped_prob_classical = np.append( prob_classical[CIRCLE_SIZE // 2 : CIRCLE_SIZE], prob_classical[0 : CIRCLE_SIZE // 2] ) # quantum quantum_probs = { sample.state["x"]: sample.shots / NUM_SAMPLES for sample in result_1.parsed_counts } sorted_quantum_probs = dict(sorted(quantum_probs.items())) plt.plot(flipped_grid, flipped_prob_classical, ".-") plt.plot(sorted_quantum_probs.keys(), sorted_quantum_probs.values(), ".-") plt.xlabel("position", fontsize=16) plt.ylabel("probability", fontsize=16) ``` **Output:** ``` Text(0, 0.5, 'probability') ``` output There is a clear difference between the two distributions: the classical distribution is symmetric around the zero position, whereas the quantum example is asymmetric with a peak far from 0. This is a small example of the different behaviors of classical random walks and quantum walks. More details and examples are in Ref. \[[1](#review)]. ## Building a General Quantum Walk with Classiq Define a quantum function for a discrete quantum walk. The arguments of the function: * `time`: an integer for the number of walking steps. * `coin_flip_qfunc`: the quantum function for "flipping" the coin. * `walks_qfuncs`: a list of quantum functions for all possible transitions at a given point. * `coin_state`: the quantum state of the coin. ```python theme={null} from classiq.qmod.symbolic import pi @qfunc def discrete_quantum_walk( time: CInt, coin_flip_qfunc: QCallable[QNum], walks_qfuncs: QCallableList, coin_state: QNum, ): power( time, lambda: ( coin_flip_qfunc(coin_state), repeat( walks_qfuncs.len, lambda i: control(coin_state == i, lambda: walks_qfuncs[i]()), ), ), ) ``` ## Example: Symmetric Quantum Walk on a Circle As a first example, consider the circle geometry above, implemented with the generic definition but with a different initial condition for the coin. Take $\frac{1}{\sqrt{2}}(|0\rangle +i |1\rangle)$ instead of $|0\rangle$. This state is a balanced initial condition for the coin (see Ref. \[[1](#review)]) and can be prepared by applying an H gate followed by an S gate. ```python theme={null} from classiq.execution import ExecutionPreferences CIRCLE_SIZE = 2**7 NUM_SHOTS = 1e4 @qfunc def main( t: CInt, x: Output[QNum[floor(log(CIRCLE_SIZE, 2)), SIGNED, 0]], coin: Output[QBit] ): allocate(x) allocate(coin) H(coin) S(coin) discrete_quantum_walk( t, lambda coin: H(coin), [ lambda: quantum_step_clockwise(x), lambda: invert(lambda: quantum_step_clockwise(x)), ], coin, ) qmod_2 = create_model( main, execution_preferences=ExecutionPreferences(num_shots=NUM_SHOTS), ) ``` Now that the model is defined, you can synthesize, execute, and plot the outcome probability: ```python theme={null} qprog_2 = synthesize(qmod_2) show(qprog_2) with ExecutionSession(qprog_2) as es: result_2 = es.sample({"t": 50}) quantum_probs = { sample.state["x"]: sample.shots / NUM_SAMPLES for sample in result_2.parsed_counts } sorted_quantum_probs = dict(sorted(quantum_probs.items())) plt.plot(sorted_quantum_probs.keys(), sorted_quantum_probs.values(), ".-") plt.xlabel("position", fontsize=16) plt.ylabel("probability", fontsize=16) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/30HaUVxpQxdS1rgV2do87MyYIPM ``` **Output:** ``` Text(0, 0.5, 'probability') ``` output The distribution is symmetric. Clearly, the distribution of a quantum walk on a circle depends on the initial condition for the coin. This is another example of the peculiar behavior of quantum walks compared to classical random walks. ## Example: 4D Hypercube with a Grover Coin Next, consider a hypercube graph (see Figure 3). The quantum walk on a hypercube shows completely different behavior compared to its classical counterpart. One striking difference refers to the hitting time, which is the time it takes to reach node $v$ starting from an initial node $u$. In particular, an interesting question is the hitting time between one corner of the hypercube "000..0" to the opposite one "111...1". Ref. \[[2](#hypercube)]) shows that the hitting time for the hypercube in quantum walks is *exponentially faster* than the analog quantity in classical random walks. The rigorous definition of the "hitting time" in the quantum case is nontrivial, and different definitions are relevant. This notebook does not use the exact definition, but simply demonstrates a specific result, highlighting the result of Ref. \[[2](#hypercube)]. Define $P_{\rm corner}(T)$ as the probability of measuring the walker at the opposite corner $|2^{N}-1\rangle$ after $T$ steps, starting at position $|0\rangle$ at time 0. Examine this quantity for the quantum and the classical case. Define a model for quantum walk on a hypercube. Two nodes in the hypercube are connected to each other if their Hamming distance is 1. Thus, a step along a hypercube is given by moving "1 Hamming distance away". For a $d$-dimensional hypercube, at each node there are $d$ possible directions to move. Each of them is given by applying a bit flip on one of the bits. In the quantum case, this is obtained by applying an X gate on one of the $N$ qubits. For the coin operator, take the "Grover diffuser" function. This choice refers to a symmetric quantum walk \[[1](#review)]. The Grover diffuser operator is a reflection around a given state $|\psi\rangle$: $$ G = I-2 |\psi\rangle\langle\psi|, $$ where I is the identity matrix. In the $N$-dimensional hypercube each node is connected to $N$ nodes, thus, the coin state should include $N$ different states. Therefore, the coin is represented by a quantum variable of size $\lceil \log_2(N)\rceil$, and the Grover diffuser is defined with $$ |\psi\rangle = \frac{1}{\sqrt{N}}\sum^{N-1}_{i=0} |i\rangle. $$ Build the model using the `grover_diffuser` function from the Classiq open library. ```python theme={null} @qperm def moving_one_hamming_dist(pos: CInt, x: QArray): X(x[pos]) ``` ```python theme={null} from classiq.execution import ExecutionPreferences from classiq.qmod.symbolic import ceiling SPACE_SIZE = 4 NUM_SHOTS = 1e4 @qfunc def main(t: CInt, x: Output[QArray[QBit, SPACE_SIZE]], coin: Output[QArray]): # start at the zero state location allocate(x) # start with equal superposition for the relevant states. allocate(SPACE_SIZE.bit_length() - 1, coin) prepare_uniform_trimmed_state(SPACE_SIZE, coin) discrete_quantum_walk( t, lambda coin: grover_diffuser( lambda coin: prepare_uniform_trimmed_state(SPACE_SIZE, coin), coin ), [ lambda: moving_one_hamming_dist(0, x), lambda: moving_one_hamming_dist(1, x), lambda: moving_one_hamming_dist(2, x), lambda: moving_one_hamming_dist(3, x), ], coin, ) qmod_3 = create_model( main, execution_preferences=ExecutionPreferences(num_shots=NUM_SHOTS), constraints=Constraints(optimization_parameter="width"), ) ``` ```python theme={null} qprog_3 = synthesize(qmod_3) show(qprog_3) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/30HaVnHxgmGp7vcXVzm1HI85Iec ``` Screenshot 2025-06-19 at 23.59.11.png ```python theme={null} with ExecutionSession(qprog_3) as es: result_3 = es.sample({"t": 4}) ``` ```python theme={null} print( "The probability to reach the opposite corner:", result_3.counts.get("1" * SPACE_SIZE, 0) / NUM_SHOTS, ) ``` **Output:** ``` The probability to reach the opposite corner: 0.566 ``` At $T=4$, the probability to measure the walker at the opposite corner is larger than $1/2$. Check an analogous question in the classical random walk, where the distribution is taken over an ensemble of independent experiments: ```python theme={null} initial_point = 0 final_point = dict() for sample in range(NUM_SAMPLES): coin_state = np.random.randint(SPACE_SIZE) for k in range(SPACE_SIZE): if coin_state == k: temp_point = initial_point ^ 2**k initial_point = temp_point final_point[initial_point] = final_point.get(initial_point, 0) + 1 print( "The probability to reach the opposite corner in the classical case:", final_point.get(2**SPACE_SIZE - 1, 0) / NUM_SAMPLES, ) ``` **Output:** ``` The probability to reach the opposite corner in the classical case: 0.0583 ``` For the classical analogous case, the probability is much lower. This is a manifestation of the fact that the hitting time in a hypercube is exponentially shorter in quantum walks, in comparison to classical random walks. \[1]: [Kempe, J. "Quantum random walks: An introductory overview." Contemporary Physics 44, 307 (2003)](https://arxiv.org/abs/quant-ph/0303081). \[2]: [Kempe, J. "Discrete quantum walks hit exponentially faster." Probability theory and related fields 133, 215 (2005)](https://arxiv.org/abs/quant-ph/0205083). # Quantum Walks on One and Two Dimentional Lattice Source: https://docs.classiq.io/explore/tutorials/advanced_tutorials/discrete_quantum_walk_2d/discrete_time_quantum_walk Open this notebook in GitHub to run it yourself In this tutorial, we implement 1D and 2D quantum walk. Quantum walks are quantum-mechanical extensions of classical random walks and play a central role in the design of quantum algorithms. In particular, quantum walks are known to provide polynomial, and in some cases exponential, speedups over classical methods for tasks such as search problems and the acceleration of Markov-chain-based procedures. There are two main types of quantum walks: discrete-time quantum walks and continuous-time quantum walks. In the discrete-time model, the time evolution is defined by a unitary operator $U = S(C \otimes I)$, where $C$ is a coin operator and $S$ is a shift operator. In the continuous-time model, the adjacency matrix $A$ or the Laplacian $L$ of the graph is used as the Hamiltonian, and the state evolves according to the unitary operator $e^{-iHt}$. Unlike classical random walks, quantum walks exhibit dramatically different behavior due to interference and unitarity. For instance, the probability distribution does not take a Gaussian form; instead, it spreads ballistically, leading to an $O(t)$ dispersion rather than the classical $O(\sqrt{t})$. These properties form the foundation for the speedups observed in quantum-walk-based algorithms. Quantum walks have a wide range of applications, including search algorithms, Hamiltonian simulation, quantum Markov chains, and quantum machine learning. In particular, Szegedy's quantum walk provides a general framework for quantizing classical Markov chains into unitary operators, enabling powerful algorithmic constructions across diverse domains. ## 1D Quantum Walk In a one-dimensional discrete-time quantum walk, the position space is the integer lattice $\mathbb{Z}$, and the coin space is a two-dimensional Hilbert space. The state is written as $$ |\psi_t\rangle = \sum_{x=0}^{N-1}\sum_{l=0}^{1} \alpha_{x,l}|x\rangle \otimes|l\rangle . $$ Using a coin operator $C$ and a shift operator $S$, one step of the time evolution is defined by $$ U = S(C \otimes I). $$ A typical example of a coin is the Hadamard coin: $$ C = H = \frac{1}{\sqrt{2}} \begin{pmatrix} 1 & 1 \\ 1 & -1 \end{pmatrix}. $$ The shift operator moves the walker left or right depending on the coin state: $$ S|x\rangle|0\rangle = |x-1\rangle|0\rangle,\qquad S|x\rangle|1\rangle = |x+1\rangle|1\rangle. $$ As a result, the quantum walk spreads ballistically due to interference, producing a probability distribution very different from the Gaussian diffusion of classical random walks. ```python theme={null} import matplotlib.pyplot as plt import numpy as np from classiq import * ``` ```python theme={null} # n is number of qubits n = 6 # number of grids of 1 dimentional lattice circle_size = 2**n NUM_SAMPLES = 1000 ``` ```python theme={null} @qfunc def coin_opeator(C: QNum): hadamard_transform(C) @qfunc def shift_right(x: QNum): x += 1 @qfunc def shift_left(x: QNum): x += -1 @qfunc def shift_opeartor(x: QNum, C: QNum): control(C == 1, lambda: shift_right(x)) control(C == 0, lambda: shift_left(x)) @qfunc def quantum_walk(x: QNum, C: QNum): coin_opeator(C) shift_opeartor(x, C) @qfunc def coin_walk(t: CInt, x: QNum, C: QNum): power( t, lambda: quantum_walk(x, C), ) @qfunc def super_position(C: QNum): # 1/sqrt(2)[1, 1j] H(C) S(C) # X(C) @qfunc def initial_state(x: QNum, C: QNum): x ^= circle_size // 2 super_position(C) @qfunc def main(t: CInt, x: Output[QNum]): C = QNum("C") allocate(n, x) allocate(1, C) initial_state(x, C) coin_walk(t, x, C) # to avoid warning drop(C) ``` ```python theme={null} qmod = create_model( main, execution_preferences=ExecutionPreferences(num_shots=NUM_SAMPLES) ) qprog_1d = synthesize(qmod) show(qprog_1d) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/36pbWzE4KinG59Sag2aIt5QsPiG ``` ```python theme={null} with ExecutionSession(qprog_1d) as es: result = es.sample({"t": 1}) ``` ```python theme={null} with ExecutionSession(qprog_1d) as es: result = es.sample({"t": 20}) # quantum quantum_probs = { sample.state["x"]: sample.shots / NUM_SAMPLES for sample in result.parsed_counts } sorted_quantum_probs = dict(sorted(quantum_probs.items())) plt.bar(sorted_quantum_probs.keys(), sorted_quantum_probs.values(), width=0.5) plt.xlim(-0.5, circle_size + 0.5) plt.xlabel("position", fontsize=16) plt.ylabel("probability", fontsize=16) ``` **Output:** ``` Text(0, 0.5, 'probability') ``` output ## 2 Dimentional Quantum Walk In a discrete-time quantum walk on a two-dimensional lattice, the position space is $\mathbb{Z}^2$, and a four-dimensional coin space is used to represent movement directions. The state is written as $$ |\psi_t\rangle = \sum_{x=0}^{N-1}\sum_{y=0}^{N-1} \sum_{l=0}^{3} \alpha_t(x,y,l) |x,y\rangle \otimes |l\rangle. $$ The coin operator $C$ is a $4 \times 4$ unitary matrix. A commonly used example is the Grover coin: $$ C = G_4 = \frac{1}{2} \begin{pmatrix} -1 & 1 & 1 & 1 \\ 1 & -1 & 1 & 1 \\ 1 & 1 & -1 & 1 \\ 1 & 1 & 1 & -1 \end{pmatrix}. $$ The shift operator $S$ moves the walker according to the coin state: $$ \begin{aligned} S|x,y\rangle|0\rangle &= |x+1,y\rangle|0\rangle,\\ S|x,y\rangle|1\rangle &= |x-1,y\rangle|1\rangle,\\ S|x,y\rangle|2\rangle &= |x,y+1\rangle|2\rangle,\\ S|x,y\rangle|3\rangle &= |x,y-1\rangle|3\rangle. \end{aligned} $$ The time evolution is given, as in the 1D case, by $$ U = S(C \otimes I). $$ Two-dimensional quantum walks exhibit richer interference patterns, and it is known that strong localization can occur, particularly when using the Grover coin. ```python theme={null} n = 5 circle_size = 2**n NUM_SAMPLES = 1000 ``` ```python theme={null} @qfunc def coin_opeator(C: QNum): # hadamard_transform(C) grover_diffuser(lambda i: hadamard_transform(i), C) @qfunc def shift_plus(pos: QNum): pos += 1 @qfunc def shift_minus(pos: QNum): pos += -1 @qfunc def shift_opeartor(x: QNum, y: QNum, C: QNum): control(C == 0, lambda: shift_plus(x)) control(C == 1, lambda: shift_minus(x)) control(C == 2, lambda: shift_plus(y)) control(C == 3, lambda: shift_minus(y)) @qfunc def quantum_walk(x: QNum, y: QNum, C: QNum): coin_opeator(C) shift_opeartor(x, y, C) @qfunc def coin_walk(t: CInt, x: QNum, y: QNum, C: QNum): power( t, lambda: quantum_walk(x, y, C), ) @qfunc def main(t: CInt, x: Output[QNum], y: Output[QNum]): C = QNum("C") allocate(n, x) allocate(n, y) # initial state x ^= circle_size // 2 y ^= circle_size // 2 prepare_amplitudes([1 / 2, 1 / 2, -1 / 2, -1 / 2], 0.0, C) coin_walk(t, x, y, C) drop(C) ``` ```python theme={null} qmod = create_model( main, execution_preferences=ExecutionPreferences(num_shots=NUM_SAMPLES) ) qprog_2d = synthesize(qmod) show(qprog_2d) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/36pbYSrYqQHQjfPUsLSauntyXAq ``` ```python theme={null} from collections import defaultdict import matplotlib.pyplot as plt import numpy as np with ExecutionSession(qprog_2d) as es: result = es.sample({"t": 20}) quantum_probs = { (s.state["x"], s.state["y"]): s.shots / result.num_shots for s in result.parsed_counts } """ Data Vizualization """ L = 1 << n Z = np.zeros((L, L), dtype=float) for (x, y), p in quantum_probs.items(): if 0 <= x < L and 0 <= y < L: Z[y, x] = p fig, ax = plt.subplots(figsize=(7, 6)) im = ax.imshow( Z, origin="lower", extent=[-0.5, L - 0.5, -0.5, L - 0.5], aspect="equal", interpolation="nearest", vmin=0.0, vmax=Z.max() if Z.max() > 0 else 1.0, ) cbar = plt.colorbar(im, ax=ax) cbar.set_label("probability", fontsize=12) ax.set_xlim(-0.5, L - 0.5) ax.set_ylim(-0.5, L - 0.5) ax.set_xlabel("x") ax.set_ylabel("y") ax.set_title("Probability heatmap P(x, y)") plt.tight_layout() plt.show() ``` output # Designing Quantum Algorithms with Second Order Functions: A Flexible QPE Source: https://docs.classiq.io/explore/tutorials/advanced_tutorials/high_level_modeling_flexible_qpe/high_level_modeling_flexible_qpe Open this notebook in GitHub to run it yourself Quantum Phase Estimation (QPE) is a fundamental quantum function, at the core of the Shor, HHL, and amplitude estimation algorithms. QPE is a second order function, getting a quantum function $U$ and returning an estimation of its eigenvalues. (Recall that any quantum function represents a unitary matrix.) A QPE that encodes the eigenvalues on $m$ qubits involves a series of $m$ controlled operations of $U^{2^k}$ with $0\leq k < m-1$. This quantum advantage based on the QPE function relies on an ability to implement the power of a given unitary $U$ efficiently. Otherwise, naive $U$ is called $\sum^{m-1}_{k=0} 2^k=2^m$ times – a number that is exponential in the number of qubits. **This tutorial shows how to leverage declarative and programmatic modeling for exploring the QPE function in the context of Hamiltonian simulation.** Start with basic import: ```python theme={null} from classiq import * ``` ## 1. Defining a Flexible QPE Define a flexible QPE function. Instead of getting a single operand $U$, it gets a parametric operand, $U(p)$, where $p$ is an integer such that $U(p)\equiv U^p$. That is, the power logic of $U$ passes explicitly with the function. In addition, the QPE itself has an integer parameter for the phase register size. ```python theme={null} @qfunc def my_qpe_flexible( unitary: QCallable[CInt, QArray[QBit]], state: QArray[QBit], phase: QArray[QBit], ) -> None: apply_to_all(H, phase) repeat( count=phase.len, iteration=lambda index: control( ctrl=phase[index], stmt_block=lambda: unitary(2**index, state), ), ) invert( lambda: qft(phase), ) ``` ## 2. Example QPE for Finding the Eigenvalues of an Hermitian Matrix One use of the QPE is to find the eigenvalues of a given Hermitian matrix $H$. Canonical use cases: (a) the HHL algorithm for solving linear equations $H\cdot \vec{x}=\vec{b}$, where the matrix eigenvalues need to be stored on a quantum register, and (b) finding the minimal energy of a molecule Hamiltonian $H$, preparing an initial guess for an eigenvector followed by a QPE that aims to detect the minimal eigenvalue. In both use cases, a QPE is performed on *Hamiltonian evolution* $U=e^{2\pi i H}$. # ## 2.1 Hamiltonian Evolution Hamiltonian evolution, or Hamiltonian simulation, is one of the promising uses of quantum computers, where the advantage over classical approaches is clear and transparent (as proposed by Richard Feynman in 1982). Nevertheless, constructing a quantum program for efficient Hamiltonian dynamics is not an easy task. The most common examples use approximated product formulas such as the Trotter-Suzuki (TS) formulas. # ### 2.1.1 Trotter-Suzuki of Order 1 Write the Hamiltonian as a sum of Pauli strings $H=\sum_{i=0}^{L-1} a^{(k)} P^{(k)}$, where $a^{(k)}$ are complex coefficients, and each of $P^{(k)}$ is a Pauli string of the form $s_0\otimes s_1\otimes\dots\otimes s_L$, with $s_i\in \{I, X, Y, Z\}$. Approximating Hamiltonian simulation with TS of order 1 refers to: $$ e^{2\pi i H}\approx \left(\Pi^{L-1}_{i=0}e^{\frac{a^{(k)}}{r} P^{(k)}}\right)^r, $$ where $r$ is called the *number of repetitions*. * *Given a Hamiltonian and a functional error $\epsilon$, what is the required number of repetitions?* Apparently, this is not easy to answer. The literature provides several bounds for the number of repetitions for a given functional error and error metric; however, typically, these bounds are very rough, far from representing the actual number of repetitions to use. See Ref.\[[1](#errors)] for a comprehensive study. * *When performing a QPE, the challenge is even more pronounced*: For the QPE, a series of Hamiltonian simulations with an exponentially growing evolution coefficient, $e^{2\pi i H}, \, e^{2^1 2\pi i H}, \, e^{2^2 2\pi i H}, \dots, e^{2^{m-1}2\pi i H}$, is required. Which product formula to use for each step, assuming you keep the same error per step? Lacking good theoretical bounds for the aforementioned questions, resort to experimental exploration in the hope of finding theoretical clues and insights: # ### 2.1.2 A Flexible TS for Plugging into the Flexible QPE The Trotter-Suzuki of order 1 function, $\text{TS}_1$, gets an Hamiltonian $H$, evolution coefficient $t$, and repetition $r$. Define a wrapper function: $$ \tilde{\text{TS}}_1\left(H,t,p \right) := \text{TS}_1\left(H,pt,r=f(p)\right). $$ The function $f(p)$ tries to capture how many repetitions can approximate $\left(e^{2\pi i H}\right)^p=e^{p 2\pi i H}$. Section 2.2 defines the "goodness of approximation". Define ansatz for the repetition scaling $f(p)$: $$ f(p)\equiv \left\{ \begin{array}{l l} r_0 & \text{if } p None: suzuki_trotter( hamiltonian, evolution_coefficient=evolution_coefficient * pw, order=1, repetitions=Piecewise( (r0, pw < p_0), (ceiling(r0 * (pw / p_0) ** gamma), True) ), qbv=target, ) ``` # ## 2.2 QPE Performance In this tutorial, the measure for goodness of approximation refers to the functionality of the full QPE function, rather than taking a rigorous operator norm per each Hamiltonian simulation step in the QPE. Ways of examining the approximated QPE: 1. By its ability to approximate an eigenvalue for a given eigenvector. 2. By comparing its resulting phase state with the one that results from a QPE with an exact Hamiltonian evolution, using a swap test. ## 3. Exploring a Specific Example Consider a specific Hamiltonian defined with the `PauliOperator` object ```python theme={null} po = ( 0.4 * Pauli.I(0) * Pauli.I(1) - 0.05 * Pauli.I(0) * Pauli.Z(1) - 0.03 * Pauli.I(0) * Pauli.X(1) - 0.06 * Pauli.Z(0) * Pauli.Z(1) + 0.04 * Pauli.X(0) * Pauli.Z(1) - 0.16 * Pauli.Z(0) * Pauli.Z(1) - 0.06 * Pauli.Y(0) * Pauli.Y(1) ) ``` Define auxiliary functions for parsing the PauliOperator object. For the demonstration, choose one of the eigenvectors of the matrix, and test the result of the approximated QPE with respect to the expected eigenvalue. ```python theme={null} import numpy as np a_mat = pauli_operator_to_matrix(po).real w, v = np.linalg.eig(a_mat) w, v = w.real, v.real chosen_eig = 2 print("chosen eigenvector:", v[:, chosen_eig]) print("the eigenvalue to estimate:", w[chosen_eig]) ``` **Output:** ``` chosen eigenvector: [-0.08272059 -0.41789454 0.90270987 -0.06030213] the eigenvalue to estimate: 0.7031971279434319 ``` *** \*Note: For this example, the most naive upper bound for TS formula of order 1 and error $\epsilon=0.1$ (defined by a spectral norm) gives $r=O(4t^2)$ \[[2](#ts)], with $t=2\pi$ for the first QPE step. This corresponds to $r_0\sim 160$, and the following QPE steps grow exponentially $r_k\sim 160\times 4^k$. The result is a huge circuit depth, which you can relax by tuning the parameters of the ansatz.\* *Tighter bounds based on commutation relations\[[1](#errors)] can give more reasonable numbers. However, the main purpose of this tutorial is to highlight the advantages of abstract, high-level modeling. Indeed, any known bound can be incorporated in the flexible Trotter-Suzuki by defining $f(m)$ accordingly.* *** # ## 3. 4. Eigenvalue Estimation Choose parameters for the power-logic function $f(p)$, construct and synthesize a model, and visualize the resulting quantum program. ```python theme={null} QPE_SIZE = 5 p_0 = 2 ** (QPE_SIZE - 3) R0 = 4 # according to the naive bound this should be O(150) GAMMA = 1.5 # according to the naive bound this should be 4 @qfunc def main(phase_approx: Output[QNum]) -> None: state = QArray() allocate(QPE_SIZE, phase_approx) prepare_amplitudes(v[:, chosen_eig].tolist(), 0.0, state) my_qpe_flexible( unitary=lambda pw, target: suzuki_trotter_with_power_logic( hamiltonian=po, pw=pw, evolution_coefficient=-2 * np.pi, order=1, r0=R0, p_0=p_0, gamma=GAMMA, target=target, ), state=state, phase=phase_approx, ) drop(state) qprog_1 = synthesize(main) ``` ```python theme={null} show(qprog_1) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/31s88gHVE7XfloYnub3FSThyVmP ``` Execute the quantum program and examine the results: ```python theme={null} result_1 = execute(qprog_1).result_value() ``` ```python theme={null} parsed_counts = result_1.parsed_counts phase_counts = { sampled_state.state["phase_approx"] / (2**QPE_SIZE): sampled_state.shots for sampled_state in parsed_counts } ``` ```python theme={null} import matplotlib.pyplot as plt plt.bar(phase_counts.keys(), phase_counts.values(), width=0.01) most_probable_phase = max(phase_counts, key=phase_counts.get) plt.plot(w[chosen_eig], phase_counts[most_probable_phase], "or") print("exact eigenvalue:", w[chosen_eig]) print("approximated eigenvalue:", most_probable_phase) ``` **Output:** ``` exact eigenvalue: 0.7031971279434319 approximated eigenvalue: 0.6875 ``` output Indeed, the approximated Hamiltonian simulation seems to be sufficient to find the eigenvalue. # ## 3. 4. QPE State with Exact Hamiltonian Simulation Versus Approximated Define the following quantum function: an exact Hamiltonian simulation with power-logic. ```python theme={null} from typing import List @qfunc def unitary_with_power_logic( pw: CInt, matrix: CArray[CArray[CReal]], target: QArray[QBit] ) -> None: power(pw, lambda: unitary(elements=matrix, target=target)) ``` Continue with the same parameters from above for $f(p)$. Construct a model that calls two QPEs in parallel; one with an approximated Hamiltonian simulation and the other with an exact one. Finally, perform a swap test between the resulting phases. Synthesize the model and visualize the resulting quantum program. ```python theme={null} import scipy @qfunc def main(test: Output[QBit]) -> None: state = QArray() phase_approx = QArray() phase_exact = QArray() allocate(QPE_SIZE, phase_approx) allocate(QPE_SIZE, phase_exact) prepare_amplitudes(v[:, chosen_eig].tolist(), 0.0, state) my_qpe_flexible( unitary=lambda pw, target: suzuki_trotter_with_power_logic( hamiltonian=po, pw=pw, evolution_coefficient=-2 * np.pi, order=1, r0=R0, p_0=p_0, gamma=GAMMA, target=target, ), state=state, phase=phase_approx, ) my_qpe_flexible( unitary=lambda arg0, arg1: unitary_with_power_logic( matrix=scipy.linalg.expm( 2 * np.pi * 1j * pauli_operator_to_matrix(po) ).tolist(), pw=arg0, target=arg1, ), state=state, phase=phase_exact, ) drop(state) swap_test(state1=phase_exact, state2=phase_approx, test=test) drop(phase_exact) drop(phase_approx) qprog_2 = synthesize(main) ``` ```python theme={null} show(qprog_2) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/31sCmQ6dlU5KGOreKeXblkxfb6q ``` Execute and examine the results. ```python theme={null} result_2 = execute(qprog_2).result_value() ``` ```python theme={null} test_counts = result_2.counts ``` The overlap between the two input states of the swap test, $\psi_1$, $\psi_2$, is given by $$ Prob(\text{test qubit at state } |0\rangle) = \frac{1}{2}\left( 1+\left|\langle \psi_1 |\psi_2\rangle\right|^2\right) $$ ```python theme={null} print("Fidelity (overlap):", 2 * test_counts["0"] / sum(test_counts.values()) - 1) ``` **Output:** ``` Fidelity (overlap): 0.9521484375 ``` The results are good. You can try to reduce the $r_0$ and/or $\gamma$ parameters, and experimentally study the relation between the functional error and circuit depth. ## 4. Comment * This tutorial focused on the Trotter-Suzuki formula of order 1 for approximating the Hamiltonian simulation. You can test other implementations, including their "power-logic", such as higher order TS formulas, qDRIFT, or a combination of TS and qDRIFT. ## References \[1]: [Childs, Andrew M., et al. Theory of Trotter error with commutator scaling. PRX 11 (2021): 011020.](https://journals.aps.org/prx/abstract/10.1103/PhysRevX.11.011020) \[2]: [Childs, Andrew M., et al. Toward the first quantum simulation with quantum speedup. PNAS 115 9456 (2018).](https://www.pnas.org/doi/abs/10.1073/pnas.1801723115) # Quantum Entanglement with Classiq Source: https://docs.classiq.io/explore/tutorials/basic_tutorials/entanglement/entanglement Open this notebook in GitHub to run it yourself Entanglement is an important aspect to study quantum algorithms. In this tutorial, we show how to create a bell pair state $|\Phi^{+}\rangle$ of 2 qubits, using the Hadamard and the Controlled-NOT transformation. ```python theme={null} from classiq import * @qfunc def my_bell_state(reg: QArray) -> None: H(reg[0]) CX(reg[0], reg[1]) @qfunc def main(registers: Output[QArray]) -> None: allocate(2, registers) my_bell_state(registers) qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/2zJHAuuniJKwF8l4cVdhJpmUxXl ``` ## Mathematical Background Alice has a qubit which is initially set to $|0\rangle$
Bob has a qubit which is initally set to $|0\rangle$ Alice applies 2x2 Hadamard Matrix (H) to create a superposition of her qubit's state. It is defined as:
$$ H \cdot \begin{pmatrix} 1 \\ 0 \end{pmatrix} = \begin{pmatrix} \frac{1}{\sqrt{2}} & \frac{1}{\sqrt{2}} \\ \frac{1}{\sqrt{2}} & -\frac{1}{\sqrt{2}} \end{pmatrix} \cdot \begin{pmatrix} 1 \\ 0 \end{pmatrix} = \begin{pmatrix} \frac{1}{\sqrt{2}} \\ \frac{1}{\sqrt{2}} \end{pmatrix} = \frac{1}{\sqrt{2}} \left| 0 \right\rangle + \frac{1}{\sqrt{2}} \left| 1 \right\rangle $$ Alice and Bob combine their qubits and generate the composite quantum state as: $$ \begin{pmatrix} \frac{1}{\sqrt{2}} \\ \frac{1}{\sqrt{2}} \end{pmatrix} \otimes \begin{pmatrix} 1 \\ 0 \end{pmatrix} = \begin{pmatrix} \frac{1}{\sqrt{2}} \\ 0 \\ \frac{1}{\sqrt{2}} \\ 0 \end{pmatrix} $$ Alice now applies the Controlled NOT gate operation, with her qubit being the control qubit and Bob's qubit as the target qubit. The Controlled NOT qubit when applied only affects the target qubit by inverting its state if the control qubit is $|1\rangle$. The 4x4 matrix for Controlled NOT(CNOT) transformation matrix is defined as:
$$ \text{CNOT} = \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 1 \\ 0 & 0 & 1 & 0 \end{pmatrix} $$ The new quantum state is: $$ \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 1 \\ 0 & 0 & 1 & 0 \end{pmatrix} \begin{pmatrix} \frac{1}{\sqrt{2}} \\ 0 \\ \frac{1}{\sqrt{2}}\\ 0 \end{pmatrix} = \begin{pmatrix} \frac{1}{\sqrt{2}} \\ 0 \\ 0 \\ \frac{1}{\sqrt{2}} \end{pmatrix} = \frac{1}{\sqrt{2}} \left| 00 \right\rangle + \frac{1}{\sqrt{2}} \left| 11 \right\rangle $$ In this situation the qubit's of Alice and Bob are correlated to each other.
If we measure both the qubits we will either get the state $|00\rangle$ or the state $|11\rangle$ with equal probability. It is described as: * When Alice observes her state as $|0\rangle$ then the state of Bob's qubit collapses to the state $|0\rangle$ * When Alice observes her state as $|1\rangle$ then the state of Bob's qubit collapses to the state $|1\rangle$ The resultant entangled state is designated as a bell pair state $|\Phi^{+}\rangle$ $$ |\Phi^{+}\rangle = \frac{1}{\sqrt{2}} \left( |00\rangle + |11\rangle \right) $$ ## GHZ State The GHZ state, a highly entangeld state entengaling all qubits in a circuit. $$ |GHZ\rangle = \frac{|0\rangle^{\otimes n} + |1\rangle^{\otimes n}}{\sqrt{2}} $$ Create a function that will generate a GHZ state for `n` qubits. Use the Classiq build in `repeat` no classical loops. An example circuit is shown below. As you can see to create this circuit, there are two steps: 1. Apply the H gate to the first qubit. 2. Perform a CNOT gate between the first qubit and all other qubits, or perform CNOT gates like seen in the image below. GHZ state circuit using repeated CNOT gates # ## Practice: The Classiq library also has a GHZ state preparation built-in (see `prepare_ghz_state`), but here you will try to implemnt it yourself. ```python theme={null} from classiq import * @qfunc def main(reg: Output[QArray]): allocate(6, reg) # your code here pass qprog_task = synthesize(main) show(qprog_task) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/2zJHBIfojHo5Fzga1M4lDAnprRJ ``` # ## The Full Solution for Your Reference ```python theme={null} from classiq import * @qfunc def main(reg: Output[QArray]): allocate(6, reg) H(reg[0]) repeat( count=reg.len - 1, iteration=lambda index: CX(ctrl=reg[index], target=reg[index + 1]), ) qprog_solution = synthesize(main) show(qprog_solution) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/2zJHBdIcIYk5iWQPsDxJZt2ZwAV ``` # Exponentiation and Hamiltonian Simulation Source: https://docs.classiq.io/explore/tutorials/basic_tutorials/exponentiation/example_exponentiation Open this notebook in GitHub to run it yourself This tutorial demonstrates how to use the Classiq platform exponentiation function to solve Hamiltonian simulation problems, thereby demonstrating the strength of the Classiq exponentiation module. ## 1. Chemical Simulation Chemical simulation is one of the most exciting applications for quantum computers. When precise simulations of electron-electron interactions are necessary, it is sometimes possible to use a classical computer, but classical computers struggle to simulate more complex molecular interactions. It is best to simulate these particle interactions at the quantum level, and an excellent way to do this is with a quantum computer. The ability to accurately simulate molecular interactions will have extensive applications. When used for drug discovery, it will allow for the rapid development of vaccines and new cures for diseases. In materials research, we can hope to discover materials with higher strength-to-weight ratios and environmentally friendly building materials. ## 2. The H2O Hamiltonian Simulation Problem Generate a circuit that approximates the unitary $e^{-iH}$ where $H$ is the qubit Hamiltonian of a H2O (water) molecule. The H2O Hamiltonian is composed of 551 Pauli strings on twelve qubits. ```python theme={null} !pip install -qq "classiq[chemistry]" ``` ```python theme={null} from openfermion.chem import MolecularData from openfermionpyscf import run_pyscf from classiq import * from classiq.applications.chemistry.mapping import FermionToQubitMapper from classiq.applications.chemistry.op_utils import qubit_op_to_qmod from classiq.applications.chemistry.problems import FermionHamiltonianProblem molecule_H2O_geometry = [ ("O", (0.0, 0.0, 0.0)), ("H", (0, 0.586, 0.757)), ("H", (0, 0.586, -0.757)), ] molecule = MolecularData(molecule_H2O_geometry, "sto-3g", 1, 0) molecule = run_pyscf(molecule) problem = FermionHamiltonianProblem.from_molecule(molecule, first_active_index=1) mapper = FermionToQubitMapper() hamiltonian = qubit_op_to_qmod(mapper.map(problem.fermion_hamiltonian)) ``` ```python theme={null} @qfunc def main(state: Output[QArray]) -> None: allocate(hamiltonian.num_qubits, state) suzuki_trotter( hamiltonian, evolution_coefficient=1, order=1, repetitions=1, qbv=state, ) preferences = Preferences( custom_hardware_settings=CustomHardwareSettings(basis_gates=["cx", "u"]) ) qprog = synthesize(main, preferences=preferences) print(f"Classiq's exponentiation depth is {qprog.transpiled_circuit.depth}") print( f"Classiq's exponentiation CX-count is {qprog.transpiled_circuit.count_ops['cx']}" ) show(qprog) ``` **Output:** ``` Classiq's exponentiation depth is 1464 Classiq's exponentiation CX-count is 1536 Quantum program link: https://platform.classiq.io/circuit/3BJxddWkXBhvWUiTHbPYSeqlZ9f ``` These impressive results can be compared to the naive exponentiation modules often found in the literature, see comprehensive comparison in the [Hamiltonian Evolution](https://github.com/Classiq/classiq-library/blob/main/tutorials/technology_demonstrations/hamiltonian_evolution/hamiltonian_evolution.ipynb) notebook. ## 3. Conclusion Classiq packages the domain expertise of dozens of scientists and quantum software engineers into the software platform. The result: a system that can automatically generate efficient quantum circuits for complex problems, making it faster and easier than ever to solve real-life problems with quantum computing. When the circuits are of manageable size, Classiq creates solutions that are on par with the best manually created circuits. When the circuits are larger than those a human can reasonably create, Classiq allows you to progress farther because of its powerful capabilities. # Grover Algorithm for Graph Coloring Problem Source: https://docs.classiq.io/explore/tutorials/basic_tutorials/grover_graph_coloring/grover_graph_coloring Open this notebook in GitHub to run it yourself In this tutorial, we solve the problem of coloring an undirected graph so that adjacent vertices do not share the same color while satisfying the given constraints. As a toy example, we consider a four-coloring problem for the graph shown below. ```python theme={null} import matplotlib.pyplot as plt import networkx as nx import numpy as np from classiq import * ``` ## Toy Problem ```python theme={null} G = nx.Graph() nodes = ["A", "B", "C", "D", "E", 0, 1, 2, 3] G.add_nodes_from(nodes) edges = [ (0, "A"), (0, "B"), (0, "C"), ("A", "B"), ("A", 0), ("A", 1), ("B", "C"), ("B", "E"), ("B", 2), ("C", "D"), ("C", "E"), ("D", 1), ("D", 2), ("E", 3), ] G.add_edges_from(edges) pos = { "A": (0, 2), "B": (-1, 1), "C": (0, 1), "D": (1, 1), "E": (0, 0), 0: (-1, 2), 1: (1, 2), 2: (-1, 0), 3: (1, 0), } plt.figure(figsize=(2, 2)) nx.draw(G, pos, with_labels=True, node_size=400, node_color="black", font_color="white") nx.draw_networkx_nodes(G, pos, nodelist=[0], node_color="red") nx.draw_networkx_nodes(G, pos, nodelist=[1], node_color="blue") nx.draw_networkx_nodes(G, pos, nodelist=[2], node_color="orange") nx.draw_networkx_nodes(G, pos, nodelist=[3], node_color="green") plt.show() ``` output ## Grover Algorithm We assign `red`, `blue`, `orange`, and `green` to the unassigned black nodes above under the given constraints. Here, in order to encode the colors in a program, we replace them with integers: * red = 0, * blue = 1, * orange = 2, * green = 3. Therefore, representing a color requires at most two qubits. We assign two qubits to each node as follows. ```python theme={null} class PredicateVars(QStruct): a: QNum[2, False, 0] b: QNum[2, False, 0] c: QNum[2, False, 0] d: QNum[2, False, 0] e: QNum[2, False, 0] ``` # ## Oracle Function Next, we construct the **oracle** that enforces the rule that no two adjacent nodes may share the same color. This condition can be expressed using propositional logic. Here, we denote the color of each node by a variable $a,b,c,d,e$, where each alphabet represents the node color. For example, $a$ refers to the color of node A. Let us first consider node A as a concrete example. Node A is adjacent to three other nodes, so it must satisfy the following constraints: $$ (a \ne \text{red}) \land (a \ne b) \land (a \ne \text{blue}) $$ In other words, node A must not be red or blue, and it must also have a different color from its neighbor B. Now consider node B. Since node B is connected to multiple nodes, it has the following constraints: $$ (b \ne \text{red}) \land (b \ne c) \land (b \ne e) \land (b \ne \text{orange}) $$ Here, $\land$ denotes AND. This means that B must not be red or orange, and it must also have a different color from its neighbors C and E. By applying this idea to each node and summarizing their respective constraints, we can express the oracle in Qmod as follows. ```python theme={null} def oracle_function(a, b, c, d, e): return ( (a != 0) & (a != 1) & (a != b) & (b != 0) & (b != c) & (b != e) & (b != 2) & (c != 0) & (c != d) & (c != e) & (d != 1) & (d != 2) & (e != 3) ) @qperm def quantum_predicate(vars: Const[PredicateVars], res: QBit): res ^= oracle_function(vars.a, vars.b, vars.c, vars.d, vars.e) ``` # ## Bulding Block of Grover ```python theme={null} @qfunc def main(vars: Output[PredicateVars]): allocate(vars.size, vars) grover_search( reps=1, oracle=lambda vars: phase_oracle(quantum_predicate, vars), packed_vars=vars, ) ``` ```python theme={null} MAX_WIDTH = 24 constraints = Constraints( max_width=MAX_WIDTH, ) qprog = synthesize(main, constraints=constraints) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3Fnd9jEJ3R8WyUiCePI4yuFDOBg ``` ```python theme={null} result = execute(qprog).result_value() result.dataframe ```
vars.a vars.b vars.c vars.d vars.e counts probability bitstring
0 2 3 2 3 0 25 0.012207 0011101110
1 2 3 1 3 0 24 0.011719 0011011110
2 2 1 2 0 0 22 0.010742 0000100110
3 3 1 3 0 2 22 0.010742 1000110111
4 2 3 1 3 2 22 0.010742 1011011110
... ... ... ... ... ... ... ... ...
852 1 1 3 3 3 1 0.000488 1111110101
853 1 2 3 3 3 1 0.000488 1111111001
854 3 2 3 3 3 1 0.000488 1111111011
855 1 3 3 3 3 1 0.000488 1111111101
856 3 3 3 3 3 1 0.000488 1111111111

857 rows × 8 columns

```python theme={null} import numpy as np def convert_color(int_val): if int_val == 0: return "red" elif int_val == 1: return "blue" elif int_val == 2: return "orange" elif int_val == 3: return "green" NUM_SOLUTIONS = 16 NUM_VARIABLES = 5 solution_list = np.zeros((NUM_SOLUTIONS, NUM_VARIABLES), dtype=int) for k in range(NUM_SOLUTIONS): parsed_result = result.parsed_counts[k].state["vars"] a, b, c, d, e = ( int(parsed_result["a"]), int(parsed_result["b"]), int(parsed_result["c"]), int(parsed_result["d"]), int(parsed_result["e"]), ) solution_list[k, 0] = a solution_list[k, 1] = b solution_list[k, 2] = c solution_list[k, 3] = d solution_list[k, 4] = e print( "a =", a, ", b =", b, ", c =", c, "d =", d, "e =", e, ":", oracle_function(a, b, c, d, e), ) ``` **Output:** ``` a = 2 , b = 3 , c = 2 d = 3 e = 0 : True a = 2 , b = 3 , c = 1 d = 3 e = 0 : True a = 3 , b = 1 , c = 3 d = 0 e = 2 : True a = 2 , b = 1 , c = 2 d = 0 e = 0 : True a = 2 , b = 3 , c = 1 d = 3 e = 2 : True a = 3 , b = 1 , c = 2 d = 3 e = 0 : True a = 2 , b = 3 , c = 1 d = 0 e = 0 : True a = 2 , b = 3 , c = 2 d = 3 e = 1 : True a = 2 , b = 3 , c = 2 d = 0 e = 0 : True a = 2 , b = 1 , c = 2 d = 3 e = 0 : True a = 3 , b = 1 , c = 2 d = 0 e = 0 : True a = 2 , b = 3 , c = 1 d = 0 e = 2 : True a = 2 , b = 1 , c = 3 d = 0 e = 0 : True a = 2 , b = 1 , c = 3 d = 0 e = 2 : True a = 2 , b = 3 , c = 2 d = 0 e = 1 : True a = 3 , b = 1 , c = 3 d = 0 e = 0 : True ``` ## Post Processing ```python theme={null} # gerate the graph of feasible soultions graphs = [] for i in range(NUM_SOLUTIONS): graphs.append(G) fig, axes = plt.subplots(4, 4, figsize=(12, 12)) for i, ax in enumerate(axes.flat): nx.draw( G, pos, ax=ax, with_labels=True, node_size=400, nodelist=["A", "B", "C", "D", "E"], node_color=[ convert_color(solution_list[i, 0]), convert_color(solution_list[i, 1]), convert_color(solution_list[i, 2]), convert_color(solution_list[i, 3]), convert_color(solution_list[i, 4]), ], font_color="white", ) nx.draw_networkx_nodes(G, pos, ax=ax, nodelist=[0], node_color="red") nx.draw_networkx_nodes(G, pos, ax=ax, nodelist=[1], node_color="blue") nx.draw_networkx_nodes(G, pos, ax=ax, nodelist=[2], node_color="orange") nx.draw_networkx_nodes(G, pos, ax=ax, nodelist=[3], node_color="green") ax.set_title(f"solution {i+1}") plt.tight_layout() plt.show() ``` output # Optimizing MCX Gates, Preparing for Future Hardware Today Source: https://docs.classiq.io/explore/tutorials/basic_tutorials/mcx/mcx Open this notebook in GitHub to run it yourself This tutorial describes how to use the Classiq platform to create MCX gates, including one with 14 controls. Then, it demonstrates a much more complex example with 50 control qubits. ## Quantum Resources Are Valuable, yet Limited Quantum computers offer tantalizing promises to those who can harness their power. And although today's computers are not quite able to solve real-world problems, those who are able to optimize for the available hardware can reap rewards sooner than those who wait. The MCX gate is an important quantum gate used in a variety of circuits, such as the Grover operator, logical AND operator, state preparation algorithms, and arithmetic comparators. The ability to adapt implementations of MCX gates to meet the hardware constraints - limited qubit count, fidelities, gate count, and so on - is not trivial. ## Creating a 14-Control MCX Gate with Classiq To create an MCX gate with 14 control qubits using Classiq, we first define a quantum function called `my_mcx` whose arguments are an array of qubits (of any size) for `control` and a single qubit argument for the `target`: ```python theme={null} from math import pi from classiq import * ``` ```python theme={null} @qfunc def my_mcx(cntrl: QArray, target: QBit) -> None: control(cntrl, lambda: X(target)) ``` To create an MCX gate with 14 control qubits, we create a quantum `main` function that executes our `my_mcx` function with 14 qubits allocated to the `control` argument: ```python theme={null} @qfunc def main(cntrl: Output[QArray], target: Output[QBit]) -> None: allocate(14, cntrl) allocate(target) my_mcx(cntrl, target) ``` To constrain a circuit to only 20 qubits and optimize for circuit depth, we pass the maximum width and optimization parameter to a `Constraints` object and synthesize our model, create a quantum program, and view it: ```python theme={null} MAX_WIDTH_1 = 20 constraints_1 = Constraints( max_width=MAX_WIDTH_1, optimization_parameter=OptimizationParameter.DEPTH ) qprog_1 = synthesize(main, constraints=constraints_1) show(qprog_1) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3FmV56jhtcc7T5oCDypkA0JbOcS ``` Additionally, to get the transpiled circuit from our `qprog` object and print its depth: ```python theme={null} print(f"Synthesized MCX depth is {qprog_1.transpiled_circuit.depth}") ``` **Output:** ``` Synthesized MCX depth is 81 ``` ## Optimizing MCX for Every Occasion Classiq automatically optimizes the quantum circuit and each MCX gate to a plethora of possible situations. To characterize each setting we pass our constraints and preferences to the synthesis request using the `Constraints` and `Preferences` objects. # ## For Different Hardware ```python theme={null} MAX_WIDTH_2 = 21 constraints_2 = Constraints( max_width=MAX_WIDTH_2, optimization_parameter=OptimizationParameter.DEPTH ) preferences_2 = Preferences( backend_service_provider="IBM Quantum", backend_name="ibm_boston" ) qprog_2 = synthesize(main, constraints=constraints_2, preferences=preferences_2) print(f"Synthesized MCX depth is {qprog_2.transpiled_circuit.depth}") show(qprog_2) ``` **Output:** ``` Synthesized MCX depth is 219 Quantum program link: https://platform.classiq.io/circuit/3FmV5mAYK3zpS4E00OVVOIj8TBs ``` # ## For CX Gates ```python theme={null} MAX_WIDTH_3 = 17 constraints_3 = Constraints(max_width=MAX_WIDTH_3, optimization_parameter="cx") preferences_3 = Preferences( custom_hardware_settings=CustomHardwareSettings(basis_gates=["cx", "u"]) ) qprog_3 = synthesize(main, constraints=constraints_3, preferences=preferences_3) print(f"Synthesized MCX cx-count is {qprog_3.transpiled_circuit.count_ops['cx']}") show(qprog_3) ``` **Output:** ``` Synthesized MCX cx-count is 90 Quantum program link: https://platform.classiq.io/circuit/3FmV7BQrIJXACd16Fyn12qEHF3S ``` ## Beyond 14 Controls The power of the Classiq synthesis engine is far greater than creating optimized, 14-control MCX gates in an instant. For example, the following code creates an MCX gate with 50 control qubits: ```python theme={null} @qfunc def main(cntrl: Output[QArray], target: Output[QBit]) -> None: allocate(50, cntrl) allocate(target) my_mcx(cntrl, target) constraints_4 = Constraints(optimization_parameter="depth") preferences_4 = Preferences(optimization_level=0) qprog_4 = synthesize(main, constraints=constraints_4, preferences=preferences_4) show(qprog_4) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3FmVKc3gkryCVdaBnsk4ltDaEgZ ``` Synthesized 50-control MCX circuit # Learning Optimization Source: https://docs.classiq.io/explore/tutorials/basic_tutorials/optimization/learning_optimization Open this notebook in GitHub to run it yourself This is a step-by-step example of how to use the Classiq platform at the application level. The goal is to see how easy it can be to use quantum algorithms to solve problems. This is a basic optimization problem: *minimize the expression* $3x_0+2x_1$ *for the non-negative integers* $x_0,x_1$, *given the constraint that* $3x_0+x_1\ge2$. Of course this is trivial and the solution is $x_0=1,x_1=0$. The goal is to understand how to incorporate the problem with the platform, so you can then continue on to define more complicated problems. This kind of optimization problem is relevant for many real-life scenarios. For example, Amazon wants to determine the best value it can offer to a customer for two items on a Black Friday sale, but it has to earn some minimum value. ## How to Solve It? Define the optimization problem with the classical [Pyomo](https://pyomo.readthedocs.io/en/stable/index.html) optimization package in Python. Then, use the platform to convert it to a high-level functional model of quantum algorithms. This functional model is at the heart of the platform as this is the object that is synthesized to an actual quantum circuit using the synthesis engine! After the circuit is synthesized, run it on actual hardware or on a simulator to actually get the result from the quantum algorithm. This tutorial runs the algorithm on the IBM quantum simulator as it is the default option. ## What Do You Need to Know About Quantum Algorithms? You need to know almost nothing regarding quantum algorithms, besides one thing. There are two common algorithms used for optimization problems (as well as chemistry): QAOA and VQE. Both are very similar, where QAOA could be seen as a specific type of VQE. For this problem, use the QAOA algorithm. The algorithm has a mandatory parameter that you need to choose, as explained below. ## Getting Started Import the relevant packages. The first is the Pyomo package; the classical optimization package that was installed when you installed Classiq: ```python theme={null} import pyomo.environ as pyo ``` Import the objects that translate the optimization problem from the Pyomo language to a high-level quantum functional model: ```python theme={null} from classiq import * from classiq.applications.combinatorial_optimization import * ``` ## Defining the Problem Initiate a Pyomo application object. ```python theme={null} application_level_object = pyo.ConcreteModel() ``` This object will contain all the relevant information regarding the optimization problem. The first piece of relevant information is what are the variables. In Pyomo, the way to incorporate the information regarding the variables is using the `pyo.Var` object: ```python theme={null} application_level_object.x = pyo.Var( [0, 1], # variables names domain=pyo.NonNegativeIntegers, # variables type bounds=(0, 3), # variables range ) ``` In the first line, define 'application\_object' with a field called 'x' to contain the problem variables. The variables are defined with a 'pyo.Var' object, containing several things: 1. The names of the variables. These are defined by $[1,2]$, indicating $x_0$ and $x_1$, respectively. (Likewise, $[3,7]$ would indicate two variables: $x_3, x_7$.) 1. The type/domain of the variables. The variables are non-negative integers, so configure them accordingly using the 'pyo.NonNegativeIntegers' command. 1. The bounds of the variables. The variables are configured to get values from 0 to 3, inclusive. While you may prefer a larger range, today's quantum computers (and simulators) are not big enough, so the size of the problems you can solve is quite small (here defined by the number of options: two variables each with four options; i.e., 16 options total). In the application object, define the cost function, which is the objective: ```python theme={null} application_level_object.cost = pyo.Objective( expr=3 * application_level_object.x[0] + 2 * application_level_object.x[1] ) ``` In other words, *minimize* $3x_0+2x_1$. Together with the objective, define the constraint: ```python theme={null} application_level_object.constraint = pyo.Constraint( expr=3 * application_level_object.x[0] + application_level_object.x[1] >= 2 ) ``` I.e., the constraint is $3x_0 + 2x_1 \ge 2$. There are several ways to define constraints in Pyomo. Here, add a field to the application object called `constraint`, which is equal to some Pyomo constraint object. Read [link](https://docs.classiq.io/latest/user-guide/applications/optimization/problem-formulation/) for more ways of defining constraints. Examine the application object using the Pyomo method 'pprint': ```python theme={null} application_level_object.pprint() ``` **Output:** ``` 1 Var Declarations x : Size=2, Index={0, 1} Key : Lower : Value : Upper : Fixed : Stale : Domain 0 : 0 : None : 3 : False : True : NonNegativeIntegers 1 : 0 : None : 3 : False : True : NonNegativeIntegers 1 Objective Declarations cost : Size=1, Index=None, Active=True Key : Active : Sense : Expression None : True : minimize : 3*x[0] + 2*x[1] 1 Constraint Declarations constraint : Size=1, Index=None, Active=True Key : Lower : Body : Upper : Active None : 2.0 : 3*x[0] + x[1] : +Inf : True 3 Declarations: x cost constraint ``` See how all the information regarding the problem is organized in this Pyomo application object. ## Entering the Quantum World So far, you have only used the Pyomo package and spoken the *optimization language*. You now need some quantum knowledge for the optimization problem. Set the number of repetitions of the QAOA sub-circuit. The QAOA algorithm contains a QAOA sub-circuit that might repeat several times. Roughly speaking, the more repetitions, the better the algorithm. Having said that, as you saw, there is a limited range of parameters due to the small size of today's quantum computers. There is also an issue with the length of the quantum circuit due to the relatively low quality of today's quantum circuits (again due to the limited power of the quantum simulators). Therefore, start with one repetition of the sub-circuit, and later you can change it to see how the results change. Define the QAOA configuration: ```python theme={null} combi = CombinatorialProblem(application_level_object, num_layers=1, penalty_factor=20) ``` ## Seamlessly Generating the Functional Level Model Now with the application object and the QAOA configuration defined, ask the platform to convert it into a high-level quantum functional model. ```python theme={null} qmod = combi.get_model() ``` Congratulations! You just defined your first quantum model that encapsulates the functionality of your quantum algorithm, without mentioning anything related to qubits or quantum gates! Ask the system to solve the model using the quantum algorithm: Synthesizing the model: ```python theme={null} qprog = combi.get_qprog() ``` Optimize the parameters of the circuit: ```python theme={null} optimized_params = combi.optimize() ``` Examine the solution: ```python theme={null} import pandas as pd optimization_result = combi.sample(optimized_params) optimization_result.sort_values(by="cost").head(5) ``` | | solution | probability | cost | | --- | ------------------------------------------------------ | ----------- | ---- | | 211 | \{'x': \[1, 0], 'constraint\_slack\_var': \[1, 0, 0... | 0.002441 | 3.0 | | 35 | \{'x': \[0, 2], 'constraint\_slack\_var': \[0, 0, 0... | 0.005371 | 4.0 | | 241 | \{'x': \[1, 1], 'constraint\_slack\_var': \[0, 1, 0... | 0.001465 | 5.0 | | 225 | \{'x': \[2, 0], 'constraint\_slack\_var': \[1, 0, 0... | 0.002441 | 6.0 | | 103 | \{'x': \[2, 0], 'constraint\_slack\_var': \[0, 0, 1... | 0.003906 | 6.0 | ```python theme={null} idx = optimization_result.cost.idxmin() print( "x =", optimization_result.solution[idx], ", cost =", optimization_result.cost[idx] ) ``` **Output:** ``` x = {'x': [1, 0], 'constraint_slack_var': [1, 0, 0, 0]} , cost = 3.0 ``` The solution is $x_0=1, x_1=0$. Yes, you succeeded in solving the optimization problem using a quantum algorithm! :) Wait a minute, you might ask, *'Where are all the qubits and gates I have heard about? '* That is a good question! While you can design algorithms at the application and functional levels, you also have access to the qubit level to further understand the algorithm and get into detail with more options! This is easily done. The synthesis engine output is a quantum circuit object, so visualize it with the 'show' command that prompts a website to interactively display the circuit for deeper analysis. You can examine how your circuit looks, from high level to the qubit level. ```python theme={null} show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/31EYZhPUvLDS1WdG6oKHQSmqAxR ``` Now that you understand better how the platform works, there is one last thing worth mentioning. When you solved the model by executing the synthesis engine's output quantum circuit, behind the scenes the circuit was sent to the default Executor (IBM simulator) with classical optimization preferences to return the optimization result. Because this example shows how to use the platform at the application level, many details in the flow were determined behind the scenes. For more control of your design, and to design the algorithm from the functional level so to gain more control and capabilities, do the next tutorial ;) # Walk-Through: `prepare_state` Source: https://docs.classiq.io/explore/tutorials/basic_tutorials/prepare_state/prepare_state Open this notebook in GitHub to run it yourself This notebook is the Classiq SDK equivalent of the walkthrough sequence as presented in the Classiq web IDE \[[1](#classiq-ide)]. ```python theme={null} from classiq import * ``` ## Build Your Algorithm **In the IDE:** To start writing your quantum model, click the `Model` tab. ## Build the Model **In the IDE:** Here you define the model, function parameters, and more. See the User Guide \[[2](#user-guide)] for details. *Below is the SDK representation of the Qmod syntax shown on the IDE page:* ```python theme={null} probabilities = [ 0, 0.002, 0.004, 0.006, 0.0081, 0.0101, 0.0121, 0.0141, 0.0161, 0.0181, 0.0202, 0.0222, 0.0242, 0.0262, 0.0282, 0.0302, 0.0323, 0.0343, 0.0363, 0.0383, 0.0403, 0.0423, 0.0444, 0.0464, 0.0484, 0.0504, 0.0524, 0.0544, 0.0565, 0.0585, 0.0605, 0.0625, ] @qfunc def main(io: Output[QArray]): prepare_state(probabilities=probabilities, bound=0.01, out=io) ``` ## Synthesize the Model **In the IDE:** Now that you have selected or built a model, click the "Synthesize" button, sit back, and let Classiq do its magic! *Below is the SDK representation of the Qmod syntax shown on the IDE page:* ```python theme={null} qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/2zPKuTDqwQAGWvEB299Om2iz6sl ``` **Output:** ``` gio: https://platform.classiq.io/circuit/2zPKuTDqwQAGWvEB299Om2iz6sl?login=True&version=0.85.0: Operation not supported ``` ## Congratulations! **In the IDE:** This is your first quantum program. Learn more in the User Guide \[[2](#user-guide)]. ## Run on Quantum Hardware or Simulators **In the IDE:** Click 'Execute' to define the quantum hardware or a quantum simulator to run your synthesized quantum program. ## Define Execution Details **In the IDE:** Select which quantum program to execute, define the execution parameters, and choose a quantum provider and backend platform. The Classiq platform is your gateway to all major quantum computing providers. ```python theme={null} preferences = ExecutionPreferences( backend_preferences=ClassiqBackendPreferences( backend_name=ClassiqSimulatorBackendNames.SIMULATOR ) ) ``` ## Run on a Quantum Simulator! **In the IDE:** Click 'Run' to execute your quantum program on the simulator you chose in the previous step. *Below is the SDK execution code:* ```python theme={null} with ExecutionSession(qprog, preferences) as es: res = es.sample() ``` ```python theme={null} res.dataframe.head() ``` | | io | count | probability | bitstring | | - | ---------------- | ----- | ----------- | --------- | | 0 | \[1, 1, 1, 1, 1] | 136 | 0.066406 | 11111 | | 1 | \[1, 0, 1, 1, 1] | 132 | 0.064453 | 11101 | | 2 | \[0, 1, 1, 1, 1] | 124 | 0.060547 | 11110 | | 3 | \[0, 1, 0, 1, 1] | 114 | 0.055664 | 11010 | | 4 | \[0, 0, 0, 1, 1] | 113 | 0.055176 | 11000 | Look at that cool triangle probability function! **In the IDE:** That's it! You ran your first quantum program. To learn more about the Classiq platform, read the User Guide \[[2](#user-guide)]. ## References \[1]: [Classiq IDE](https://platform.classiq.io/) \[2]: [Classiq User\_Guide](https://docs.classiq.io/latest/) # Quantum Monte Carlo Integration to Estimate Pi Using Quantum Amplitude Estimation Source: https://docs.classiq.io/explore/tutorials/basic_tutorials/qmci_pi_estimation/qmci_pi_estimation Open this notebook in GitHub to run it yourself ## Introduction In this tutorial, we implement a Quantum Monte Carlo Integration (QMCI) circuit to estimate $\pi$. The approach samples lattice points on a discretized grid and uses Quantum Amplitude Estimation (QAE) to estimate the fraction of points that lie inside a quarter circle. This fraction directly determines the value of $\pi$. Following Ref.\~\[1], lattice-based uniform sampling is appropriate for the present $\pi$-estimation problem because the task reduces to estimating the fraction of marked grid points. In this setting, QAE estimates the corresponding amplitude with a quadratic speedup over classical Monte Carlo methods. This should be distinguished from classical grid-based quadrature, whose cost generally scales exponentially with the dimension. In contrast, the QAE-based QMCI approach can avoid this curse of dimensionality at the level of amplitude estimation. In more general QMCI applications, such as finance, the practical advantage further depends on whether the required probability distribution and oracle can be implemented efficiently. ## Method The goal of this tutorial is to estimate $\pi$ from the area ratio between a square and an inscribed quarter circle. We sample lattice points $(x,y)$ on a discretized square and determine whether each point lies inside the quarter circle. If the fraction of points inside the quarter circle is denoted by $\alpha$, then $$ \alpha = \frac{\pi}{4}, $$ and therefore $$ \pi = 4\alpha. $$ Two $n$-qubit registers are used to represent the $x$- and $y$-coordinates, respectively: $$ 0 \le x \le 2^n-1, \qquad 0 \le y \le 2^n-1. $$ Thus, the computational basis corresponds to $2^{2n}$ lattice points in a square of side length $2^n$. We define the indicator function $$ f(x,y)= \begin{cases} 1 & \text{if } x^2+y^2 < 2^{2n},\\ 0 & \text{if } x^2+y^2 \ge 2^{2n}. \end{cases} $$ With uniform sampling, $$ p(x,y)=\frac{1}{2^{2n}}, $$ the target quantity is the expectation value $$ \alpha = \sum_{x=0}^{2^n-1} \sum_{y=0}^{2^n-1} f(x,y)p(x,y). $$ This is the probability that a uniformly sampled lattice point lies inside the quarter circle. To estimate this quantity on a quantum computer, we prepare the uniform superposition over all lattice points using $$ \hat{P}=H^{\otimes 2n}, $$ so that $$ (\hat{P}\otimes I)|0\rangle_{2n}|0\rangle = \frac{1}{\sqrt{2^{2n}}} \sum_{x=0}^{2^n-1} \sum_{y=0}^{2^n-1} |x,y\rangle_{2n}|0\rangle . $$ Then an oracle $\hat{R}$ marks whether each point is inside the quarter circle by writing the value of $f(x,y)$ into an ancilla qubit: $$ \hat{R}|x,y\rangle_{2n}|0\rangle= |x,y\rangle_{2n}|f(x,y)\rangle . $$ Thus, $$ \hat{R}(\hat{P}\otimes I)|0\rangle_{2n}|0\rangle = \frac{1}{\sqrt{2^{2n}}} \sum_{x=0}^{2^n-1} \sum_{y=0}^{2^n-1} |x,y\rangle_{2n}|f(x,y)\rangle . $$ Therefore, the probability of measuring the ancilla qubit in the state $|1\rangle$ is exactly $\alpha$. By applying Quantum Amplitude Estimation (QAE), we estimate this amplitude more efficiently than by classical Monte Carlo integration, and finally compute $$ \pi = 4\alpha. $$ Compared with classical grid-based quadrature, whose cost grows exponentially with the dimension, this QAE-based approach avoids the curse of dimensionality. Compared with classical Monte Carlo integration, QAE provides a quadratic speedup in the estimation error. In this $\pi$-estimation task, lattice-based uniform sampling is appropriate because the problem reduces to estimating the fraction of marked grid points. In more general QMCI applications, such as finance, the advantage additionally depends on whether the required probability distribution and oracle can be implemented efficiently. ## Dataset We first generate the grid data. $n$ is the parameter for number of data. ```python theme={null} import matplotlib.pyplot as plt import numpy as np def plot_pi_sampling(GRID_POINTS_PER_AXIS): limit = 2**GRID_POINTS_PER_AXIS x_range = np.arange(limit) y_range = np.arange(limit) X, Y = np.meshgrid(x_range, y_range) # constraint: f(x, y) = 1 (True) if x^2 + y^2 < limit^2 inside = X**2 + Y**2 < limit**2 plt.figure(figsize=(3, 3)) # data plot plt.scatter(X[inside], Y[inside], color="blue", s=10, label="f(x,y)=1 (Inside)") plt.scatter(X[~inside], Y[~inside], color="red", s=10, label="f(x,y)=0 (Outside)") theta = np.linspace(0, np.pi / 2, 100) plt.plot(limit * np.cos(theta), limit * np.sin(theta), color="black", linewidth=2) plt.xlim(-0.5, limit) plt.ylim(-0.5, limit) plt.gca().set_aspect("equal", adjustable="box") plt.title(f"Lattice Points (n={GRID_POINTS_PER_AXIS})") plt.xlabel("x-axis") plt.ylabel("y-axis") plt.legend() plt.grid(True, linestyle="--", alpha=0.6) plt.show() plot_pi_sampling(GRID_POINTS_PER_AXIS=4) ``` output ## Simulation: Classical Simulation Below, as a point of comparison, we first present an example of a classical simulation based on uniformly spaced sampling points. ```python theme={null} def estimate_pi_grid(GRID_POINTS_PER_AXIS): limit = GRID_POINTS_PER_AXIS inside_circle = 0 total_samples = limit * limit for y in range(limit): for x in range(limit): if x**2 + y**2 < limit**2: inside_circle += 1 pi_estimated = (inside_circle / total_samples) * 4 return pi_estimated LIST_GRID_POINTS_PER_AXIS = [3, 5, 6] for mi in LIST_GRID_POINTS_PER_AXIS: print(f"(Total Sampling: {mi}²={mi**2}), Estimated Pi: {estimate_pi_grid(mi)}") ``` **Output:** ``` (Total Sampling: 3²=9), Estimated Pi: 4.0 (Total Sampling: 5²=25), Estimated Pi: 3.52 (Total Sampling: 6²=36), Estimated Pi: 3.6666666666666665 ``` ## Quantum Simulation # ## Option 1: QAE Based QMCI ```python theme={null} import numpy as np from classiq import * N_QUBITS = 3 class position(QStruct): x: QNum[N_QUBITS] y: QNum[N_QUBITS] @qfunc def encode_prob(p: position): hadamard_transform(p) @qperm def oracle_func(p: Const[position], res: QBit): fn = 2 ** (2 * N_QUBITS) res ^= p.x**2 + p.y**2 < fn @qfunc def oracle_op(state: position): phase_oracle(oracle_func, state) @qfunc def my_grover_operator(state: position): grover_operator( oracle_op, hadamard_transform, state, ) QPE_SIZE = 3 @qfunc def main(phase_reg: Output[QNum[QPE_SIZE, UNSIGNED, QPE_SIZE]]): state_reg = position() allocate(state_reg.size, state_reg) allocate(phase_reg) encode_prob(state_reg) qpe( unitary=lambda: my_grover_operator(state_reg), phase=phase_reg, ) drop(state_reg) qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3EqYbP5JOFnAYoEVgSFVGx1aDAA ``` ```python theme={null} result = sample(qprog) phases_counts = dict(zip(result["phase_reg"], result["counts"])) ``` ```python theme={null} expected_alpha = np.sin(np.pi * max(phases_counts, key=phases_counts.get)) print("QMCI pi = ", expected_alpha * 4) print("exact pi = ", np.pi) ``` **Output:** ``` QMCI pi = 3.695518130045147 exact pi = 3.141592653589793 ``` This result indicates that QMCI successfully approaches the value of $\pi$, although the current estimate still has a significant error compared with the exact value. In the next cell, we investigate how accurately $\pi$ can be estimated by applying IQAE and examining its achievable precision. # ## Option2: Iterative QAE Based QMCI ```python theme={null} from classiq.applications.iqae.iqae import IQAE def iqae_circuit(N_QUBITS): class position(QStruct): x: QNum[N_QUBITS] y: QNum[N_QUBITS] @qfunc def encode_prob(p: position): hadamard_transform(p) @qperm def oracle_func(p: Const[position], res: QBit): fn = 2 ** (2 * N_QUBITS) res ^= p.x**2 + p.y**2 < fn @qfunc def iqae_state_preparation(p: position, ind: QBit): encode_prob(p) oracle_func(p, ind) iqae = IQAE( state_prep_op=iqae_state_preparation, problem_vars_size=2 * N_QUBITS, constraints=Constraints(optimization_parameter=OptimizationParameter.WIDTH), preferences=Preferences(machine_precision=N_QUBITS), ) return iqae ``` ```python theme={null} N_QUBITS = 3 qprog = iqae_circuit(N_QUBITS).get_qprog() show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3EqYnWtGGFiRFmeBlePfkD2Q5Nx ``` # ## IAQE Parameters For the numerical test, we use the standard Classiq IAQE routine. The two main parameters are the target accuracy $\epsilon$ and the failure probability $\alpha$. We set $$ \epsilon = 0.03, \qquad \alpha = 0.01. $$ Here, $\epsilon$ specifies the target additive accuracy of the amplitude estimation, while $\alpha$ specifies the allowed failure probability. Thus, $\alpha=0.01$ corresponds to a $99%$ confidence level. This setting is suitable for an initial validation because it keeps the estimation cost moderate while still requiring a reliable confidence level. Once the implementation is verified, $\epsilon$ can be reduced for a higher-precision estimation. ```python theme={null} iqae_res = iqae_circuit(N_QUBITS).run(epsilon=0.03, alpha=0.01) ``` ```python theme={null} iqae_res.confidence_interval ``` **Output:** ``` [0.8731791779970596, 0.881120994691263] ``` ```python theme={null} iqae_res.estimation * 4 ``` **Output:** ``` 3.508600345376645 ``` The factor of $4$ is required because IQAE estimates $\pi/4$, corresponding to the area ratio of the quarter circle inside the square. # ## Experiment In this experiment, we examine whether the estimated value approaches the exact solution as the number of qubits increases. Because a larger number of qubits allows exponentially more sampling points to be represented, the discretization becomes finer, and the estimate is expected to become more accurate. By tracking this trend, we can directly evaluate how increasing the qubit number improves the precision of the $\pi$ estimation. ```python theme={null} list_expected_value = [] list_conf_err = [] for N_QUBITS in range(1, 5): iqae_res = iqae_circuit(N_QUBITS).run(epsilon=0.03, alpha=0.01) print("expected pi =", iqae_res.estimation * 4) list_expected_value.append(iqae_res.estimation * 4) list_conf_err.append(iqae_res.confidence_interval) ``` **Output:** ``` expected pi = 3.998328879248948 expected pi = 3.7481528856852786 expected pi = 3.5018416165265203 expected pi = 3.342893743314299 ``` When you run the cell above, you should obtain results similar to those shown below: ```python theme={null} # list_expected_value = [3.998328879248948, 3.7572795311895337, 3.5043386707175443, 3.3476167564481774] # list_conf_err = [[0.999164439624474, 1.0], [0.9328714567271077, 0.9457683088676591], [0.8720696958863934, 0.8800996394723789], [0.8298663183601778, 0.8439420598639109]] ``` ```python theme={null} import matplotlib.pyplot as plt import numpy as np list_N_QUBITS = range(1, 5) total_sampling = [2 ** (2 * N_QUBITS) for N_QUBITS in list_N_QUBITS] pi_ests = list_expected_value yerr_lower = [pi_ests[i] - (list_conf_err[i][0] * 4) for i in range(len(pi_ests))] yerr_upper = [(list_conf_err[i][1] * 4) - pi_ests[i] for i in range(len(pi_ests))] plt.figure(figsize=(4, 4)) plt.tick_params(direction="in", which="both", top=True, right=True) plt.errorbar( total_sampling, pi_ests, yerr=[yerr_lower, yerr_upper], fmt="o", color="red", capsize=5, markersize=6, label="IQAE based QMCI", ) LIST_GRID_POINTS_PER_AXIS = np.array( [2, 3, 4, 5, 6, 7, 8, 9, 10, 20, 30, 40, 50, 60, 70, 80] ) classical_pi_vals = [estimate_pi_grid(mi) for mi in LIST_GRID_POINTS_PER_AXIS] plt.plot( LIST_GRID_POINTS_PER_AXIS**2, classical_pi_vals, "--", color="black", label="Classical Calculation", ) plt.axhline(y=np.pi, color="green", linestyle="-.", alpha=0.6, label="π (True Value)") plt.xlabel("Total sampling(= Grid Size)") plt.ylabel("Estimated π") plt.ylim(2.8, 4.2) plt.xscale("log") plt.legend(loc="upper right") plt.tight_layout() plt.show() ``` output We observe convergence toward the exact value in both the classical and IQAE-based quantum estimates of $\pi$ as the number of sampling points increases. ## Discussion How much computational advantage can be achieved? According to Ref. \[2], the query complexity of QMCI is given by $$ N_{\mathrm{query}} \approx \frac{C \cdot \ln!\left(\frac{2}{\alpha}\right)}{\epsilon}. $$ By contrast, in the classical grid-based calculation considered here, all lattice points are evaluated explicitly. Hence, the computational cost is $$ t_{\mathrm{classical}} = 2^{2n} = \mathcal{O}(1/\epsilon^2). $$ The table below shows a theoretical comparison of the required number of evaluations and queries. | Item | **Classical Grid-Based Integration** | **Quantum Monte Carlo Integration (IQAE)** | | :----- | :----------------------------------- | :----------------------------------------- | | $n=2$ | $2^4 = 16$ | $12,716$ | | $n=4$ | $2^8 = 256$ | $12,716$ | | $n=10$ | $2^{20} = 1,048,576$ | $12,716$ | This comparison indicates that the quantum method does not provide an advantage at small scales, but becomes more efficient than the classical approach once the problem size is sufficiently large. ## References \[1] [Quantum circuit to estimate pi using quantum amplitude estimation](https://arxiv.org/abs/2008.02623) \[2] [Iterative Quantum Amplitude Estimation, Granko et al., 2019](https://www.nature.com/articles/s41534-021-00379-1) # Quantum Machine Learning with Classiq Source: https://docs.classiq.io/explore/tutorials/basic_tutorials/qml_with_classiq_guide/qml_with_classiq_guide Open this notebook in GitHub to run it yourself Welcome to the "Quantum Machine Learning with Classiq" tutorial. This guide is designed for users already familiar with the fundamentals of the Classiq platform and Quantum Machine Learning (QML) concepts. The aim is to showcase how to implement QML using Classiq. It covers three main methods to implement QML with Classiq: 1. **Using the VQE Primitive** 2. **Using the PyTorch Integration** 3. **Using the QSVM Built-in App** Each section briefly explains the method, followed by an illustrative example that demonstrates the integration. These examples are intended to be straightforward to help you get started quickly. ## In This Tutorial 1. [Using the VQE Primitive](#using-the-vqe-primitive) * [Example Using Classiq](#example-using-classiq) * [Summary and Exercise](#summary-exercise-vqe) * [Read More](#read-more-vqe) 2. [Using the PyTorch Integration](#using-the-pytorch-integration) * [Workflow](#workflow) * [Example - Demonstrate PyTorch Integration with Classiq](#example-code-demonstrating-pytorch-integration-with-classiq) * [Step 1.1 - Define the Quantum Model and Synthesize It into a Quantum Program](#step-11---define-the-quantum-model-and-synthesize-it-into-a-quantum-program) * [Step 1.2 - Define the Execute and Post-process Callables](#step-12---define-the-execute-and-post-process-callables) * [Step 1.3 - Create a torch.nn.Module Network](#step-13---create-a-torchnnmodule-network) * [Step 2 - Choose a Dataset, Loss Function, and Optimizer](#step-2---choose-a-dataset-loss-function-and-optimizer) * [Step 3 - Train and Evaluate](#step-3-train) * [Summary and Exercise](#summary-exercise-pytorch) * [Read More](#read-more-pytorch) 3. [Using QSVM Primitive](#using-qsvm-primitive) ## Using the VQE Primitive The Variational Quantum Eigensolver (VQE) is an algorithm for finding the ground state energy of a Hamiltonian operator, often described by Pauli operators or in the equivalent matrix form. The VQE was proposed in 2014 \[[1](#eigenvaluesolver)]. The algorithm follows these steps: 1. **Create a Parameterized Quantum Model**: Design a quantum model, also known as an ansatz, that captures the problem. 2. **Synthesize, Execute, and Estimate Expectation Values**: Synthesize the quantum model into a quantum program. Run the quantum program, then measure and calculate the expected value of the Hamiltonian based on this generated program. 1. **Optimize Parameters**: Use a classical optimizer to adjust the quantum program's parameters for better results. 2. **Repeat**: Continue this process until the algorithm converges to a solution or reaches a specified number of iterations. For more details, refer to this review article \[[2](#vqa)] and the corresponding preprint \[[3](#preprint)]. # ## Example Using Classiq Start with this example, creating a VQE algorithm that estimates the minimal eigenvalue of the following 2x2 Hamiltonian: $$ H = \frac{1}{2}I + \frac{1}{2}Z - X = \begin{bmatrix} 1 & -1 \\ -1 & 0 \end{bmatrix} $$ Define the Hamiltonian using `Pauli` terms: ```python theme={null} !pip install -qq -U "classiq[qml]" ``` ```python theme={null} from typing import List from classiq import * HAMILTONIAN = 0.5 * Pauli.I(0) + 0.5 * Pauli.Z(0) + (-1) * Pauli.X(0) ``` For a single qubit problem, to capture any rotation on the Bloch sphere, use the U-gate (also known as the U3-gate). This includes the state with the minimal energy with respect to the Hamiltonian. The single-qubit gate applies phase and rotation with three Euler angles. Matrix representation: $$ U(\gamma,\phi,\theta,\lambda) = e^{i\gamma}\begin{pmatrix} \cos(\frac{\theta}{2}) & -e^{i\lambda}\sin(\frac{\theta}{2}) \\ e^{i\phi}\sin(\frac{\theta}{2}) & e^{i(\phi+\lambda)}\cos(\frac{\theta}{2}) \\ \end{pmatrix} $$ Parameters: * `theta`: `CReal` * `phi`: `CReal` * `lam`: `CReal` * `gam`: `CReal` * `target`: `QBit` ```python theme={null} @qfunc def main(q: Output[QBit], angles: CArray[CReal, 3]) -> None: allocate(q) U(angles[0], angles[1], angles[2], 0, q) ``` To seamlessly harness the power of VQE, synthesize the ansatz `main`, and use the `minimize` attribute from `ExecutionSession` to optimize it. ```python theme={null} qprog_1 = synthesize(main) with ExecutionSession(qprog_1) as es: result = es.minimize( cost_function=HAMILTONIAN, initial_params={"angles": [0.0] * 3}, max_iteration=200, ) ``` Configure the `minimize` function in the `ExecutionSession` workflow with these parameters: * **cost\_function**: The cost function to minimize, it can be either a quantum a quantum cost function specified by a Hamiltonian or a classical function that is represented as a callable and returns a Qmod expression. * **initial\_params**: Initial parameters for the optimization routine. It accepts only a single parameter and should be formatted as a dict in the form \{"parameter": list}. * **max\_iteration**: The maximum number of iterations for the optimizer. * **quantile**: The quantile-based cutoff for which outcomes to consider when estimating the cost function. The output will be a list of dicts, containing the `float` values of the cost function and its respective parameters. At this stage, it is possible to visualize the results of the quantum algorithm. For instance, a graph of Energy versus Iterations can be plotted to illustrate the convergence behavior of the algorithm. ```python theme={null} import matplotlib.pyplot as plt cost_list = [term[0] for term in result] plt.figure(figsize=(10, 6)) plt.plot(range(len(cost_list)), cost_list) plt.title("Cost function convergence") plt.xlabel("Iteration") plt.ylabel("Cost function value") plt.show() ``` output When this is not necessary, it is possible to print only the final results: ```python theme={null} optimal_energy = result[-1][0] optimal_parameters = result[-1][1] print(f"Optimal energy: {optimal_energy}") print(f"Optimal parameters: {optimal_parameters}") ``` **Output:** ``` Optimal energy: -0.61572265625 Optimal parameters: {'angles': [2.1377203480076377, 0.004060541282730399, -0.3165621830160506]} ``` The VQE algorithm outputs these key results: * **Optimal energy**: The lowest energy found for the Hamiltonian, representing the ground state energy (minimal eigenvalue). * **Optimal parameters**: The parameters of the quantum program that achieve the optimal energy, corresponding to rotation angles in the U-gate. * **Eigenstate**: The quantum state associated with the optimal energy, given as probability amplitudes for the basis states. # ## Summary and Exercise You designed a parameterized quantum circuit capable of capturing a simple Hamiltonian. You initialized an `ExecutionSession` and used `minimize` to execute it, visualizing the results. Now, practice the implementation of a similar case to the previous example, but this time for two qubits, following the Hamiltonian: $$ H = \frac{1}{2}I \otimes I + \frac{1}{2}Z \otimes Z - X \otimes X $$ **Use the last example to implement and execute VQE for this Hamiltonian.** Code skeleton: ```python theme={null} HAMILTONIAN = QConstant("HAMILTONIAN", List[PauliTerm], [...]) #TODO: Complete Hamiltonian @qfunc def main(...) -> None: #TODO: Complete the function according to the instructions, choosing simple ansatz. qprog = synthesize(synthesize) show(qprog) with ExecutionSession(qprog_1) as es: result = es.minimize( cost_function=HAMILTONIAN, initial_params={"params": [0.0] * n_params}, max_iteration=200, ) ``` # ## Read More Further reading from the reference manual: * [Execution Primitives](https://docs.classiq.io/latest/user-guide/execution/ExecutionSession/) ## Using the PyTorch Integration Classiq integrates with PyTorch, enabling the seamless development of quantum machine learning and hybrid classical quantum machine learning models. This integration leverages PyTorch's powerful machine learning capabilities alongside quantum computing. To properly install and run PyTorch locally, check [this page](https://pytorch.org/get-started/locally/). # ## Workflow 1. **Defining the Model** * **1.1**: Define the quantum model and synthesize it into a quantum program. * **1.2**: Define the execute and post-process callables. * **1.3**: Create a `torch.nn.Module` network. 1. **Choosing the Dataset, Loss Function, and Optimizer** 2. **Training the Model** 3. **Testing the Model** If you are not familiar with PyTorch, read the following documentation: * [Creating Models](https://pytorch.org/tutorials/beginner/basics/quickstart_tutorial.html#creating-models) * [Building Neural Networks](https://pytorch.org/tutorials/beginner/basics/buildmodel_tutorial.html) * [Optimizing Model Parameters](https://pytorch.org/tutorials/beginner/basics/quickstart_tutorial.html#optimizing-the-model-parameters) * [Tensors](https://pytorch.org/tutorials/beginner/basics/tensorqs_tutorial.html) * [Datasets and DataLoaders](https://pytorch.org/tutorials/beginner/basics/data_tutorial.html) # ## Example * Demonstrate PyTorch Integration with Classiq This example demonstrates PyTorch integration using a simple parameterized quantum model. It utilizes one input from the user and one weight, while using one qubit in the model. The goal of the learning process is to determine the correct angle for an RX gate to perform a "NOT" operation. (Spoiler alert: The correct answer is $\pi$.) The dataset `DATALOADER_NOT` is used, as defined [here](https://docs.classiq.io/latest/user-guide/applications/qml/qnn/datasets/). `DatasetXor` is also available from the link for further practice. ```python theme={null} from classiq import * from classiq.applications.qnn.datasets import DATALOADER_NOT for data, label in DATALOADER_NOT: print(f"--> Data for training:\n{data}") print(f"--> Corresponding labels:\n{label}") ``` **Output:** ``` --> Data for training: tensor([[3.1416], [0.0000]]) --> Corresponding labels: tensor([1., 0.]) ``` This dataset contains two items. The first item indicates no rotation (`0.0000`) and is labeled as 0, indicating the state $|0\rangle$. The second item indicates a rotation of `3.1416` and is labeled as 1, indicating the state $|1\rangle$. Read an explanation on creating PyTorch datasets here: * [Creating a custom dataset for your files](https://pytorch.org/tutorials/beginner/basics/data_tutorial.html#creating-a-custom-dataset-for-your-files) * [Writing custom datasets, dataLoaders, and transforms](https://pytorch.org/tutorials/beginner/data_loading_tutorial.html) # ### Step 1.1 * Define the Quantum Model and Synthesize It into a Quantum Program The first part of the parameterized quantum model has an encoding section, which loads input data ($|0\rangle$ or $|1\rangle$) into the parameterized quantum model: ```python theme={null} @qfunc def encoding(theta: CReal, q: QArray) -> None: RX(theta=theta, target=q[0]) ``` The second part is the `mixing` function, which includes an adjustable parameter for training the RX gate to act later as a NOT gate: ```python theme={null} @qfunc def mixing(theta: CReal, q: QArray) -> None: RX(theta=theta, target=q[0]) ``` Combining the two functions into the `main` function: ```python theme={null} @qfunc def main(input_0: CReal, weight_0: CReal, res: Output[QArray]) -> None: allocate(1, res) encoding(theta=input_0, q=res) # Loading input mixing(theta=weight_0, q=res) # Adjustable parameter ``` Finally, create a model, synthesize it, and display it in the IDE: ```python theme={null} qprog_2 = synthesize(main) show(qprog_2) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/2zPLS8EIKI59yhuhNxg7NFlKTRn ``` # ### Step 1.2 * Define the Execute and Postprocess Callables Before using the quantum layer (QLayer), define the `execute` and `post-processing` functions. These functions are essential for integrating the quantum layer in a PyTorch neural network, as classical layers require classical data as input. This means that only after executing the QLayer (the ansatz) and post-processing the results the data can be further used in other layers of the neural network or be output. The `execute` function is straightforward. It takes the quantum program (here, the QLayer) and its parameters, and executes it: ```python theme={null} from classiq.applications.qnn.types import ( MultipleArguments, ResultsCollection, SavedResult, ) def execute( quantum_program: QuantumProgram, arguments: MultipleArguments ) -> ResultsCollection: return execute_qnn(quantum_program, arguments) ``` **Output:** ``` gio: https://platform.classiq.io/circuit/2zPLS8EIKI59yhuhNxg7NFlKTRn?login=True&version=0.85.0: Operation not supported ``` In general, the `post_process` function is needed to prepare the execution results for output or for loss calculation during the training phase. In this specific example, it returns the probability of measuring $|0\rangle$. This function assumes that only the differentiation between the single state $|0\rangle$ and all other states is relevant. If a different differentiation is needed, modify this function accordingly. ```python theme={null} import torch 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 number of measurements. """ counts: dict = result.value.counts # The probability of measuring |0> p_zero: float = counts.get("0", 0.0) / sum(counts.values()) return torch.tensor(p_zero) ``` Using these functions allows QLayers and PyTorch layers to be properly integrated into the same neural network. # ### Step 1.3 * Create a torch.nn.Module Network Define the `torch.nn.Module` class with a single `QLayer` as follows: ```python theme={null} from classiq.applications.qnn import QLayer class Net(torch.nn.Module): def __init__(self, *args, **kwargs) -> None: super().__init__() self.qlayer = QLayer( qprog_2, # 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() ``` In `self.qlayer = QLayer(...)`, define the only layer in the neural network as a single QLayer. Specify the previously defined `quantum_program`, `execute`, and `post_process` as arguments for the layer. Finally, create the neural network and assign it to the variable `model`. # ### Step 2 * Choose a Dataset, Loss Function, and Optimizer For the loss function and optimizer, use [L1Loss](https://pytorch.org/docs/stable/generated/torch.nn.L1Loss.html) and [SGD](https://pytorch.org/docs/stable/generated/torch.optim.SGD.html), respectively. ```python theme={null} import torch.nn as nn import torch.optim as optim _LEARNING_RATE = 1 # choosing the data data_loader = DATALOADER_NOT # choosing the loss function loss_func = nn.L1Loss() # Mean Absolute Error (MAE) # choosing the optimizer optimizer = optim.SGD(model.parameters(), lr=_LEARNING_RATE) ``` For details of the optimization algorithms and a comprehensive list of loss functions in PyTorch, refer to the official documentation: * [Optimization Algorithms](https://pytorch.org/docs/stable/optim.html#algorithms) * [Loss Functions](https://pytorch.org/docs/stable/nn.html#loss-functions) # ### Step 3 * Train and Evaluate Import `DataLoader`: ```python theme={null} from torch.utils.data import DataLoader ``` A `DataLoader` in PyTorch efficiently iterates over datasets, handling batching, shuffling, and parallel data loading. It streamlines the process of training and evaluating models by managing data efficiently. Now you are ready to define the training function. \ This simple example follows a loop similar to that recommended by PyTorch [here](https://pytorch.org/tutorials/beginner/blitz/neural_networks_tutorial.html#update-the-weights). ```python theme={null} def train( model: nn.Module, data_loader: DataLoader, loss_func: nn.modules.loss._Loss, optimizer: optim.Optimizer, epoch: int = 5, # About 40 epochs needed for full training ) -> None: for index in range(epoch): print(index, model.qlayer.weight) for data, label in data_loader: optimizer.zero_grad() output = model(data) loss = loss_func(output, label) loss.backward() optimizer.step() ``` Here, trained parameters are loaded for demonstration, and only one epoch is performed.\ You may comment on the following cell, change the number of epochs above, and expect about 40 epochs for full training for non-trained parameters. ```python theme={null} with torch.no_grad(): model.qlayer.weight.copy_( torch.tensor([3]) ) # The value from the last step of the training ``` ```python theme={null} train(model, data_loader, loss_func, optimizer) ``` **Output:** ``` 0 Parameter containing: tensor([3.], requires_grad=True) 1 Parameter containing: tensor([3.0488], requires_grad=True) 2 Parameter containing: tensor([3.1465], requires_grad=True) 3 Parameter containing: tensor([3.1465], requires_grad=True) 4 Parameter containing: tensor([3.1465], requires_grad=True) ``` Great! Observe that the parameter is approximately equal to $\pi$. \ Now, test the network accuracy using the suggested method [here](https://stackoverflow.com/questions/52176178/pytorch-model-accuracy-test#answer-64838681). ```python theme={null} def check_accuracy(model: nn.Module, data_loader: DataLoader, atol=1e-2) -> float: num_correct = 0 total = 0 model.eval() with torch.no_grad(): # Temporarily disable gradient calculation for data, labels in data_loader: # 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 number 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 ``` ```python theme={null} check_accuracy(model, data_loader) ``` **Output:** ``` Test Accuracy of the model: 100.00% ``` **Output:** ``` 1.0 ``` **The results show an accuracy of 1**, indicating a 100% success rate in performing the required transformation (i.e., the network learned to perform an X-gate). You can further validate this by printing the value of `model.qlayer.weight`, which is a tensor of shape (1,1). After training, this value should be close to $\pi$. # ## Summary and Exercise In this tutorial, you integrated a quantum layer in a PyTorch neural network, defined the necessary execution and post-processing functions, and trained the model using a simple dataset. You tested the network's accuracy using a recommended method. To explore further, try experimenting with different quantum circuits, datasets, and optimizers. Integrating more classic layers or more complex layers should be straightforward now for those with experience in PyTorch. Now, for practice, implement a similar case to the last example, but this time train the U gate to act as a NOT gate instead of the Rx gate.\ How many parameters must you train?\ What must you change to accomplish this? You only have to adapt `mixing` and `model`. # ## Read More Algorithms and application tutorials using the PyTorch integration: * [Quantum Autoencoder](https://docs.classiq.io/latest/explore/algorithms/QML/quantum_autoencoder/quantum_autoencoder/) * [QGAN](https://docs.classiq.io/latest/explore/algorithms/QML/qgan/qgan_bars_and_strips/) Further reading from the reference manual: * [QNNs with Classiq](https://docs.classiq.io/latest/user-guide/applications/qml/qnn/) * [QLayer](https://docs.classiq.io/latest/user-guide/applications/qml/qnn/qlayer/) ## Using QSVM Primitive Classiq also enables executing classification tasks using the **Quantum Support Vector Machine** (QSVM) module. This module leverages the principles of quantum computing to enhance traditional support vector machine algorithms, offering significant improvements in classification accuracy and efficiency. The QSVM module integrates seamlessly with the Classiq platform, allowing you to implement quantum-enhanced classification models effortlessly. By utilizing quantum kernels, the QSVM can handle complex datasets and capture intricate patterns that may be challenging for classical SVMs, making it a powerful tool for machine learning applications. To understand how to use it and explore it further, examine this example: [QSVM with Classiq](https://docs.classiq.io/latest/explore/algorithms/QML/qsvm/qsvm/). ## References \[1]: [Peruzzo, A., McClean, J., Shadbolt, P., et al. (2014). A variational eigenvalue solver on a photonic quantum processor, *Nature Communications*](https://doi.org/10.1038/ncomms5213). \[2]: [Cerezo, M., Arrasmith, A., Babbush, R., et al. (2021). Variational quantum algorithms, *Nature Reviews Physics*, 3, 625-644](https://doi.org/10.1038/s42254-021-00348-9). \[3]: [Corresponding preprint arXiv:2104.02281](https://arxiv.org/abs/2104.02281). \[4]: [Barkoutsos, Panagiotis Kl., et al. (2020). Improving variational quantum optimization using CVaR, *Quantum* 4, 256](https://doi.org/10.22331/q-2020-04-20-256). # Linear Combination of Unitaries (LCU) Source: https://docs.classiq.io/explore/tutorials/basic_tutorials/quantum_primitives/linear_combination_of_unitaries/linear_combination_of_unitaries Open this notebook in GitHub to run it yourself Quantum computing is based on the principles of quantum mechanics, which includes an important feature: unitarity. The operations that evolve quantum states on quantum computers are unitary. Would this mean that problems requiring non-unitary operations are out of hand for quantum computers? This is the task solved by the Linear Combination of Unitaries (LCU) algorithm \[[1](#childs-paper), [2](#childs-notes)]. Given a non-unitary matrix $A$ that can be decomposed into a sum of unitary matrices as follows: $$ A = \sum_{i=0}^{2^n-1} \alpha_i U_i, $$ where $\alpha_i$ are real, positive coefficients and $U_i$ are unitary matrices, the LCU algorithm applies the action of $A$, up to a normalization factor, on a desired quantum state. It does so by embedding the matrix $A$ in a bigger unitary. The first step of the LCU is to prepare the following state on an auxiliary quantum register according to the coefficients $\alpha_i$ using the function PREPARE: $$ |\psi_0\rangle = PREPARE|0\rangle = \sum_i \sqrt{\frac{\alpha_i}{\lambda}}|i\rangle, $$ where $\lambda = \sum_i |\alpha_i|$ is a normalization factor. This quantum register with the prepared state is used as a controller for the next step. Then, according to the controller quantum register, the following state is being prepared: $$ SELECT|i\rangle|\psi\rangle = |i\rangle U_i |\psi\rangle, $$ where $|\psi\rangle$ is the desired quantum state the matrix $A$ should be applied on. The final step is to have the PREPARE operation inversed such that the following desired state is created: $$ LCU|0\rangle|\psi\rangle = PREPARE^{-1}\, SELECT\, PREPARE |0\rangle|\psi\rangle = V |0\rangle |\psi\rangle, $$ where $V$ can be represented as $$ V = \begin{bmatrix}A & \cdot \\ \cdot & \cdot \end{bmatrix}. $$ This is called the Block Encoding of the matrix A. By projecting the controller quantum register on the $|0\rangle$ state the desired outcome is obtained: $$ \left(|0\rangle\langle0|\otimes I\right)LCU|0\rangle|\phi\rangle = |0\rangle\frac{A}{\lambda}|\psi\rangle. $$ Therefore, the action of the sequence of operations over the target qubit will be the non-unitary operation $A$, up to a normalization factor. A detailed mathematical description of the algorithm can be seen [below](#mathematical-description) and in reference \[[1](#childs-paper)]. It is also important to notice that the projection onto the $|0\rangle$ state depends on a success probability, detailed in [\[1\]](https://arxiv.org/abs/1202.5822). Overall, a scheme of the algorithm looks like:
LCU_blocks
## Guided Implementation Now that we know how the LCU algorithm works, it's time to implement it on Classiq. For that, we will be using two important functions: * [Within-Apply](https://docs.classiq.io/latest/qmod-reference/language-reference/statements/within-apply/) * [Prepare state](https://docs.classiq.io/latest/qmod-reference/library-reference/core-library-functions/prepare_state_and_amplitudes/prepare_state_and_amplitudes/) The Within-Apply maps unitary operations of the kind $V = U^{-1}WU$ into the quantum circuit, given $U$ and $W$. The Prepare state function realizes the initial state preparation step of a quantum algorithm, given a bound for the error and the probabilities of the quantum states. Using [this tutorial](https://docs.classiq.io/latest/qmod-reference/library-reference/core-library-functions/prepare_state_and_amplitudes/prepare_state_and_amplitudes/), one can play with this function. The objective of our quantum circuit is to define the matrices $U$ and $W$ following the $V = U^{-1} W U$ decomposition used in the Within-Apply function. A quick look on the definition of the LCU operator is enough to identify $U = PREPARE$ and $W = SELECT$. As an example, we will be considering the following SELECT operator: $$ SELECT = |0\rangle\langle 0|\otimes I + |1\rangle\langle 1|\otimes QFT + |2\rangle\langle 2| \otimes QFT^{-1}, $$ where QFT is the Quantum Fourier Transform operator that acts over the two target qubits, and that the probabilities are $\alpha = [0.5,0.25,0.25,0]$. Now that the operations are identified, we just need to build them and then use the Within-Apply function. The SELECT operation can be build using the [control](https://docs.classiq.io/latest/qmod-reference/language-reference/statements/control/) statement, and the [QFT function](https://docs.classiq.io/latest/qmod-reference/library-reference/open-library-functions/qft/qft/): ```python theme={null} from classiq import * @qfunc def select(controller: QNum, psi: QNum): control(ctrl=controller == 0, stmt_block=lambda: apply_to_all(IDENTITY, psi)) control(ctrl=controller == 1, stmt_block=lambda: qft(psi)) control(ctrl=controller == 2, stmt_block=lambda: invert(lambda: qft(psi))) ``` Using two auxiliary qubits, this sequence of operations can be seen in Classiq's IDE as ![select.gif](https://docs.classiq.io/resources/Select_function_final.gif) With the SELECT function defined, we are able to apply the V operator, by using the Within-Apply function. For this, it is necessary to build the PREPARE operator, which will be done using the inplace\_prepare\_state() function, that requires the probability distribution $\alpha$, the maximum error in the decomposition of the operator and the target qubits, which are the controllers. ```python theme={null} @qfunc def prepare(controller: QNum): # Defining the error bound and probability distribution error_bound = 0.01 controller_probabilities = [0.5, 0.25, 0.25, 0] inplace_prepare_state(controller_probabilities, error_bound, controller) ``` Thus, the sequence of operations we will define in our quantum program is: * Define the error bound in the decomposition, and define the probability distribution $\alpha$ * Allocate target and control qubits * Execute the Within-Apply function, using the PREPARE and SELECT functions ```python theme={null} @qfunc def main(controller: Output[QNum], psi: Output[QNum]): # Allocating the target and control qubits, respectively allocate(2, psi) allocate(2, controller) # Executing the Within-Apply function with the select and the prepare functions. within_apply( within=lambda: prepare(controller), apply=lambda: select(controller, psi), ) qmod_1 = create_model(main) qprog_1 = synthesize(qmod_1) ``` Your quantum program is done! You can see it using Classiq's IDE with the show() command: ```python theme={null} show(qprog_1) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/39FkkG2O0kmXD4dTWHX7WKDCm4r ``` **Output:** ``` https://platform.classiq.io/circuit/39FkkG2O0kmXD4dTWHX7WKDCm4r?login=True&version=17 ``` ## Mathematical Description The initial state of our circuit is $|0\rangle|\Psi\rangle$, for some general $\Psi$. After that, the PREPARE operation is applied, transforming it in the state: $$ PREPARE|0\rangle|\Psi\rangle = \left(\sum_{i=0}^{2^n-1} \sqrt{\frac{|\alpha_i|}{\lambda}} |i\rangle\right)|\Psi\rangle. $$ We can always represent the PREPARE operation, which acts only in the control qubits, as being: $$ PREPARE = \sum_{i=0}^{2^n-1} \sqrt{\frac{|\alpha_i|}{\lambda}} |i\rangle\langle 0| + \sum_{i=0}^{2^n-1}\sum_{j=1}^{2^n-1} \beta_{i,j} |i\rangle\langle j|, $$ for some $\beta_{i,j}$. The SELECT operation, which acts both in the control and target qubits, can also be described this way by $$ SELECT = \sum_{i=0}^{2^n-1} |i\rangle\langle i|\otimes U_i. $$ Now, the state generated by $PREPARE^{-1}\, SELECT\, PREPARE |0\rangle|\psi\rangle$ is given by: $$ PREPARE^{-1}\, SELECT\, PREPARE |0\rangle|\psi\rangle = \frac{1}{\lambda}|0\rangle\sum_{i=0}^{2^n-1} \alpha_i \, U_i |\Psi\rangle + \sum_{j=1}^{2^n-1} \left( \sum_{i=0}^{2^n-1} \beta_{i,j}^*\right) |j\rangle\,U_j |\Psi\rangle $$ When applying the projector $|0\rangle\langle 0|$ onto the control qubits, we finally obtain the desired state $$ \frac{1}{\lambda}|0\rangle\sum_{i=0}^{2^n-1} \alpha_i \, U_i |\Psi\rangle = |0\rangle\frac{A}{\lambda}|\Psi\rangle. $$ ## All the Code Together ```python theme={null} from classiq import * @qfunc def select(controller: QNum, psi: QNum): control(ctrl=controller == 0, stmt_block=lambda: apply_to_all(IDENTITY, psi)) control(ctrl=controller == 1, stmt_block=lambda: qft(psi)) control(ctrl=controller == 2, stmt_block=lambda: invert(lambda: qft(psi))) @qfunc def prepare(controller: QNum): # Defining the error bound and probability distribution error_bound = 0.01 controller_probabilities = [0.5, 0.25, 0.25, 0] inplace_prepare_state(controller_probabilities, error_bound, controller) @qfunc def main(controller: Output[QNum], psi: Output[QNum]): # Allocating the target and control qubits, respectively allocate(2, psi) allocate(2, controller) # Executing the Within-Apply function with the select and the prepare functions. within_apply( within=lambda: prepare(controller), apply=lambda: select(controller, psi), ) qmod_2 = create_model(main) qprog_2 = synthesize(qmod_2) show(qprog_2) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/39FklKFg3swhJR0OH8xkqcRIWqb ``` **Output:** ``` https://platform.classiq.io/circuit/39FklKFg3swhJR0OH8xkqcRIWqb?login=True&version=17 ``` ## References \[1]: [Hamiltonian Simulation Using Linear Combinations of Unitary Operations (Andrew M. Childs and Nathan Wiebe)](https://arxiv.org/abs/1202.5822) \[2]: [Lecture Notes on Quantum Algorithms (Andrew M. Childs)](https://www.cs.umd.edu/~amchilds/qa/qa.pdf) # Quantum Walk on Complex Network Source: https://docs.classiq.io/explore/tutorials/basic_tutorials/quantumwalk_complex_network/quantumwalk_complex_network Open this notebook in GitHub to run it yourself ## What This Notebook Does Quantum walks are often used as building blocks for graph exploration / graph-based quantum algorithms. This notebook simulates a **discrete-time quantum walk** on a **complex network (graph)**. * The **graph** is a set of nodes connected by edges (we generate it with NetworkX). * The **walker position** is stored in a quantum register `x`. * A second register `y` acts like a **coin / direction / neighbor-choice register**. * Each step of the walk applies: 1. **Coin operator** (mix amplitudes in a way that depends on the current node's neighbors) 2. **Shift operator** (moves the walker according to the coin register) At the end we **measure** the position register and plot a **probability distribution over nodes**. > Note: This is a *discrete-time* walk (coin + shift), not a continuous-time walk. ## Imports and Dependencies ```python theme={null} import matplotlib.pyplot as plt import networkx as nx import numpy as np from classiq import * ``` ## Step 1 * Create a "Complex Network" (the Graph) Here we generate a small graph `G`. The notebook currently uses a **Watts-Strogatz** model, which is a classic "small-world" network model: * it has high clustering (like regular lattices), * and short path lengths (like random graphs). You can switch to other models by uncommenting the alternatives: * Erdős-Rényi (random graph) * Barabási-Albert (scale-free graph) Then we draw the graph so we can visually connect the output distribution to the network structure. ```python theme={null} G = nx.connected_watts_strogatz_graph(n=4, k=2, p=0.2, tries=100, seed=312) # G = nx.erdos_renyi_graph(n=4, p=0.3, seed=42) # G = nx.barabasi_albert_graph(n=8,m=2) plt.figure(figsize=(3, 3)) nx.draw( G, with_labels=True, node_color="darkblue", edge_color="black", font_color="white", font_size=10, ) ``` output ## Step 2 * Decide How Many Qubits Are Needed for the Position Register If the graph has `N` nodes, we need enough qubits to encode node indices in binary. * `N = len(G.nodes())` * `num_qubits = ceil(log2(N))` That means the quantum register `x` can represent `2**num_qubits` = `N`. ```python theme={null} N = len(G.nodes()) num_qubits = int(np.ceil(np.log2(N))) print("number of nodes=", N, "number of required qubits=", num_qubits) ``` **Output:** ``` number of nodes= 4 number of required qubits= 2 ``` ## Step 3 * Build "Neighbor-Aware" Probability Vectors A quantum walk on a graph needs a way to represent "where can I go next from node $i$?". We create helper functions: * `get_edges_of_node(G, i)`: returns the neighbors of node `i`. * `inner_degree(G, num_qubits, i)`: builds a length `2**num_qubits` vector where: * entries corresponding to neighbors of `i` are `1`, * all others are `0`, * then we normalize by the node degree `k` to get a **uniform distribution over neighbors**. This vector is used to prepare the `y` register so it represents "allowed moves" from the current node. ```python theme={null} def get_edges_of_node(G, i): return [j for j in G.neighbors(i)] # DEGREE_LIST = np.zeros(N) # for i in range(N): # DEGREE_LIST[i] = len(get_edges_of_node(G, i)) def inner_degree(G, num_qubits, i): l_array = np.zeros(2**num_qubits) neighbors_list = get_edges_of_node(G, i) k = len(neighbors_list) for j in neighbors_list: l_array[j] = 1 return l_array / k ``` ## Step 4 * Define the Quantum Walk (State Prep, Coin, Shift, and Repeated Steps) This cell defines the core quantum logic using Classiq `@qfunc`. # ## Registers * `x`: **position register** (which node the walker is on) * `y`: **coin / neighbor register** (encodes the neighbor-choice space) # ## 4.1 Initial State: `prepare_initial_state(x, y)` Goal: start with a clean, interpretable state. 1. Prepare `x` in a **uniform superposition over valid nodes**: * If `N` is exactly `2**num_qubits`, a Hadamard transform gives uniform superposition automatically. * Otherwise, we prepare a custom probability vector that gives equal weight to nodes `0..N-1` and zero to invalid states. 1. Prepare `y` **conditioned on the current node in `x`**: For each node `i`, if `x == i`, we prepare `y` using the neighbor distribution vector from `inner_degree(...)`. So after this, the state is conceptually: * "uniform over nodes" in `x`, * and for each node, `y` contains amplitudes only on its neighbors. # ## 4.2 Coin Operator: `my_coin(x, y)` A discrete-time quantum walk needs a "coin flip" to mix amplitudes. Here, the coin depends on the current node: * If `x == i`, apply a **Grover diffuser** on register `y` corresponding to neighbors of node `i`. This mixes the neighbor amplitudes in a structured way. # ## 4.3 Shift Operator: `my_shift(x, y)` This updates the position based on the coin information. # ## 4.4 Repeating Steps: `discrete_quantum_walk(time, coin, shift, x, y)` We apply the pair `(coin, shift)` repeatedly `time` times using `power(time, ...)`. That is exactly the discrete-time walk loop: **$(\text{coin} \to \text{shift}) \times t$** ```python theme={null} @qfunc def prepare_initial_state(x: QNum[num_qubits], y: QNum[num_qubits]): if N == 2**num_qubits: hadamard_transform(x) else: prob_array = np.ones(2**num_qubits) / N prob_array[N : 2**num_qubits] = 0 inplace_prepare_state(prob_array.tolist(), 0.0, x) for i in range(N): control( x == i, lambda: inplace_prepare_state( inner_degree(G, num_qubits, i).tolist(), 0.0, y ), ) @qfunc def my_coin(x: QNum[num_qubits], y: QNum[num_qubits]): for i in range(N): control( x == i, stmt_block=lambda: grover_diffuser( lambda y: inplace_prepare_state( inner_degree(G, num_qubits, i).tolist(), 0.0, y ), y, ), ) @qfunc def my_shift(x: QNum[num_qubits], y: QNum[num_qubits]): multiswap(x, y) @qfunc def discrete_quantum_walk( time: CInt, coin_qfuncs: QCallable[QNum, QNum], shift_qfuncs: QCallable[QNum, QNum], x: QNum, y: QNum, ): power( time, lambda: ( coin_qfuncs(x, y), shift_qfuncs(x, y), ), ) ``` ## Step 5 * Choose Number of Steps, Build `main`, and Synthesize the Circuit # ## Number of Steps `t` controls how far the quantum walk evolves. More steps usually means: * wider spreading over the graph, * more interference patterns, * sometimes more "structure" in the final distribution (depending on the graph). # ## The `main` Quantum Program `main(x: Output[QNum[num_qubits]])`: 1. Allocates `x` (position) and `y` (coin). 2. Prepares the initial state. 3. Applies the discrete-time quantum walk for `t` steps. 4. Drops `y` (we only care about measuring the position distribution in `x`). Finally: * `synthesize(main)` compiles the high-level program into an executable quantum program. * `show(qprog)` displays the synthesized result. ```python theme={null} # quantum walk steps t = 3 @qfunc def main(x: Output[QNum[num_qubits]]): y = QNum("y", num_qubits) allocate(num_qubits, x) allocate(num_qubits, y) prepare_initial_state(x, y) discrete_quantum_walk(t, my_coin, my_shift, x, y) drop(y) qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3AYfsuRGxfFQPTvOVlovit1v2zJ ``` **Output:** ``` https://platform.classiq.io/circuit/3AYfsuRGxfFQPTvOVlovit1v2zJ?login=True&version=17 ``` ## Step 6 * Execute and Collect Results Here we run the synthesized program and fetch the results. The key output we care about is the measured distribution of the **position register `x`**: * each possible node index `x` has a probability, * these probabilities should sum to \~1 (up to sampling / execution effects). ```python theme={null} execution_job = execute(qprog) result = execution_job.result_value() ``` ## Step 7 * Visualize the Probability Distribution Over Nodes We plot a bar chart: * **x-axis**: node index (the measured value of the position register `x`) * **y-axis**: probability of measuring that node Tip for interpretation: * Compare "high-probability nodes" to the graph drawing. * Try changing the graph model or `t` and see how the distribution changes. ```python theme={null} result.dataframe.plot.bar(x="x", y="probability") ``` **Output:** ``` ``` output ## (Optional) Hardware-Aware Synthesis and Hardware Execution In this notebook, we executed the circuit on the `ibm_torino` processor. The Classiq platform allows specifying a particular processor through ExecutionPreferences. Before executing on quantum hardware, you can perform synthesis that incorporates hardware-specific constraints by configuring the `preference` settings. The following command runs hardware-aware synthesis to optimize your circuit for a target device. For more details, refer to the [Hardware-Aware Synthesis](https://docs.classiq.io/latest/user-guide/synthesis/hardware-aware-synthesis/). ```python theme={null} # preferences = Preferences( # backend_service_provider="IBM Quantum", backend_name="ibm_torino" # ) # synthesize(main, preferences=preferences) # qprog = synthesize(main) ``` Once the synthesis is complete, you can configure the execution settings to run your circuit on a real quantum device or a specific simulator. The `ExecutionPreferences` class allows you to define parameters such as the number of shots and backend-specific credentials. The following code demonstrates how to set up an execution session for an IBM Quantum backend: ```python theme={null} # execution_preferences = ExecutionPreferences( # num_shots=1024, # backend_preferences=IBMBackendPreferences( # backend_name='ibm_torino', # access_token="A Valid API access token to IBM Quantum", # channel="IBM Cloud Channel", # instance_crn="IBM Cloud Instance CRN", # ) # ) # with ExecutionSession(qprog, execution_preferences=execution_preferences) as es: # res = es.sample() ``` ## Result # ## $N=4$ Once the graph structure is defined, you can perform a quantum spatial search to find a specific node. In this example, the search is conducted on a Watts-Strogatz small-world graph, which is generated using the NetworkX library to create a complex network topology. Below data is quantum spatial search on `G = nx.connected_watts_strogatz_graph(n=4, k=2, p=0.2, tries=100, seed=312)` . ```python theme={null} import numpy as np # withou HW synthesis prob_torino_t1 = np.array([3258, 2340, 2549, 1853]) / 10000 prob_torino_t2 = np.array([2856, 2736, 2153, 2255]) / 10000 prob_torino_t3 = np.array([2474, 2468, 2389, 2669]) / 10000 prob_torino_t4 = np.array([2352, 2717, 2296, 2635]) / 10000 # with HW-level synthesis prob_torino_t1_hw = np.array([2228, 2650, 2286, 2836]) / 10000 prob_torino_t2_hw = np.array([2296, 2428, 2461, 2815]) / 10000 prob_torino_t3_hw = np.array([2567, 2477, 2483, 2473]) / 10000 prob_torino_t4_hw = np.array([2269, 2385, 2339, 3007]) / 10000 # Simulator result (ideal) prob_sim_t1 = [0.5, 0.20833333, 0.20833333, 0.08333333] prob_sim_t2 = [0.33333333, 0.28690075, 0.28690075, 0.09286516] prob_sim_t3 = [0.25953183, 0.29985427, 0.29985427, 0.14075964] prob_sim_t4 = [0.54789448, 0.1859174, 0.1859174, 0.08027073] prob_torino_list_n4 = [prob_torino_t1, prob_torino_t2, prob_torino_t3, prob_torino_t4] prob_torino_hw_list_n4 = [ prob_torino_t1_hw, prob_torino_t2_hw, prob_torino_t3_hw, prob_torino_t4_hw, ] prob_sim_list_n4 = [prob_sim_t1, prob_sim_t2, prob_sim_t3, prob_sim_t4] ``` # ## $N=8$ Below data is quantum spatial search on `G = nx.connected_watts_strogatz_graph(n=8, k=4, p=0.2, tries=100, seed=312)` . ```python theme={null} # withou HW synthesis prob_torino_t1 = np.array([1330, 1238, 1391, 1302, 1283, 1144, 1158, 1154]) / 10000 prob_torino_t2 = np.array([1463, 1361, 1340, 1230, 1214, 1083, 1202, 1107]) / 10000 prob_torino_t3 = np.array([1213, 1320, 1243, 1334, 1160, 1277, 1183, 1270]) / 10000 prob_torino_t4 = np.array([1487, 1175, 1478, 1276, 1272, 1038, 1258, 1016]) / 10000 # with HW-level synthesis prob_torino_t1_hw = np.array([1350, 1352, 1275, 1342, 1205, 1214, 1104, 1158]) / 10000 prob_torino_t2_hw = np.array([1431, 1386, 1167, 1187, 1274, 1273, 1135, 1147]) / 10000 prob_torino_t3_hw = np.array([1115, 1186, 1195, 1306, 1250, 1274, 1283, 1391]) / 10000 prob_torino_t4_hw = np.array([1347, 1270, 1363, 1351, 1167, 1087, 1201, 1214]) / 10000 # Simulator result (ideal) prob_sim_t1 = [ 0.07708333, 0.15, 0.07708333, 0.07708333, 0.17083333, 0.21666667, 0.07083333, 0.16041667, ] prob_sim_t2 = [ 0.0856499, 0.09285754, 0.08907021, 0.08907021, 0.16118512, 0.21198126, 0.07902679, 0.19115897, ] prob_sim_t3 = [ 0.11548278, 0.10271175, 0.10264854, 0.10264854, 0.14767895, 0.15389152, 0.14311526, 0.13182268, ] prob_sim_t4 = [ 0.06987609, 0.17753849, 0.072522, 0.072522, 0.17425693, 0.20675877, 0.08323824, 0.14328747, ] prob_torino_list_n8 = [prob_torino_t1, prob_torino_t2, prob_torino_t3, prob_torino_t4] prob_torino_hw_list_n8 = [ prob_torino_t1_hw, prob_torino_t2_hw, prob_torino_t3_hw, prob_torino_t4_hw, ] prob_sim_list_n8 = [prob_sim_t1, prob_sim_t2, prob_sim_t3, prob_sim_t4] ``` # ## Vizualization ```python theme={null} import matplotlib.pyplot as plt import networkx as nx import numpy as np G = nx.connected_watts_strogatz_graph(n=8, k=4, p=0.2, tries=100, seed=312) x = np.arange(8) width = 0.2 gap = 0.4 titles = ["t=1", "t=2", "t=3", "t=4"] fig, axes = plt.subplots(1, 5, figsize=(18, 4)) axes = axes.flatten() panel_labels = ["(a)", "(b)", "(c)", "(d)", "(e)"] ax0 = axes[0] pos = nx.spring_layout(G, seed=312) nx.draw( G, pos=pos, ax=ax0, with_labels=True, node_color="darkblue", edge_color="black", font_color="white", font_size=15, ) ax0.text(-0.3, 1.08, panel_labels[0], transform=ax0.transAxes, fontsize=15, va="top") prob_sim_list = prob_sim_list_n8 prob_torino_list = prob_torino_hw_list_n8 prob_torino_hw_list = prob_torino_list_n8 for i in range(4): ax = axes[i + 1] ax.bar( x - gap / 2, prob_sim_list[i], width=width, color="red", edgecolor="black", label="exact", ) ax.bar( x, prob_torino_list[i], width=width, color="lightcyan", edgecolor="darkblue", label="ibm_torino(HW agnostic)", ) ax.bar( x + gap / 2, prob_torino_hw_list[i], width=width, color="cyan", edgecolor="darkblue", label="ibm_torino(HW aware)", ) ax.set_title(titles[i], fontsize=15) ax.set_ylabel("P", rotation=0, labelpad=10) ax.set_xlabel("x") ax.set_ylim(0, 0.3) ax.set_xticks(x) ax.set_yticks([0.0, 0.2, 0.4]) ax.legend(fontsize=10, frameon=False) ax.text( -0.3, 1.08, panel_labels[i + 1], transform=ax.transAxes, fontsize=15, va="top" ) plt.tight_layout() plt.show() ``` output # Qmod Tutorial - Part 1 Source: https://docs.classiq.io/explore/tutorials/basic_tutorials/the_classiq_tutorial/Qmod_tutorial_part1 Open this notebook in GitHub to run it yourself In this tutorial, we will cover the basics of the Qmod language and its accompanying library. We will learn to use quantum variables, functions, and operators. Let's begin with a simple code example: ```python theme={null} from classiq import * @qfunc def foo(q: QBit) -> None: X(q) H(q) @qfunc def main(q: Output[QBit]) -> None: allocate(q) foo(q) qprog = synthesize(main) ``` Function `foo` takes the quantum parameter `q` of type `QBit` and applies `X` gate to it, followed by `H` gate. Function `main` declares a single `Output` parameter `q` of type `QBit`. It first allocates a qubit to `q` in the state $\vert 0 \rangle$, then calls `foo` to operate on it. A quantum program `qprog` is created based on function `main`, so that it can later be executed. What results do we expect when executing this quantum program? * By calling `allocate`, `q` is initialized in the default state $|0\rangle$. * Then, `foo` is called: * It applies `X` (NOT gate), changing `q`'s state to $|1\rangle$. * Then it applies `H` (Hadamard gate), resulting in the superposition $\frac{1}{\sqrt{2}} (|0\rangle - |1\rangle)$. When executing the quantum program, the output variable `q` is sampled. We can run the following code and make sure that the states $|0\rangle$ and $|1\rangle$ are sampled roughly equally: ```python theme={null} res = sample(qprog) ``` **Output:** ``` Submitting job to simulator Job: https://platform.classiq.io/jobs/13788264-32b8-4146-8b28-132d8de188b3 ``` ## Qmod Fundamentals The simple model above demonstrates several features that are essential in every Qmod code: # ## The `@qfunc` Decorator Qmod is a quantum programming language embedded in Python. The decorator `@qfunc` designates a quantum function, so that it can be processed by the Qmod tool chain. The Python function is executed at a later point to construct the Qmod representation. This decorator is used for every Qmod function definition. # ## Function `main` A complete Qmod model, that is, a description that can be synthesized and executed, must define a function called `main`. Function `main` is the quantum entry point - it specifies the inputs and outputs of the quantum program, that is, its interface with the external classical execution logic. Similar to conventional programming languages, function `main`, can call other functions. Output variables that are declared in `main` definition are the ones to be measured when the program is executed. # ## Working with Quantum Variables Quantum objects are representations of data (boolean values, numbers, arrays of numbers and so on), that are stored in specific qubits. In Qmod, quantum objects are handled and manipulated using **quantum variables**. Quantum variables must be declared and initialized explicitly. The model above demonstrates two important kinds of declaration: * Function `foo` declares parameter `q` thus: `q: QBit`. This declaration means `foo` expects a pre-existing quantum object. * Function `main` declares parameter `q` thus: `q: Output[QBit]`. In this case, `q` is an output-only parameter - it is initialized inside the scope of `main`. Prior to their initialization, local quantum variables and output parameters do not reference any object (this is analogous to null reference in conventional languages). They may be initialized in the following ways: * Using [`allocate`](https://docs.classiq.io/latest/sdk-reference/qmod/operations/#classiq.qmod.builtins.operations.allocate) to initialize the variable to a new object, with all its qubits in the default $|0\rangle$ state. * Using [numeric assignment](https://docs.classiq.io/latest/user-guide/modeling/quantum-numbers-arithmetics/#numeric-assignment) to initialize the variable to an object representing the result of a quantum expression. * Using functions with output parameters (for example, [state preparation](https://docs.classiq.io/latest/qmod-reference/library-reference/core-library-functions/prepare_state_and_amplitudes/prepare_state_and_amplitudes/)). Note: all the variables in `main` must be declared as `Output`, as `main` is the entry point of the model (Think about it: where could a variable be initialized before `main` was called?). Other functions can declare parameters with or without the `Output` modifier. # ## Exercise #0 Rewrite the above model, so that `q` is initilized inside `foo`. A solution is provided in the end of the notebook. Hint: it only requires to move one line of code and add the `Output` modifier in the correct place. **Why does `foo` need the `Output` modifier?** In the original model, `foo` declares `q: QBit` - a regular parameter. This tells Qmod that `q` must already be an initialized quantum object when `foo` is called; the caller is responsible for creating it. When we move `allocate` inside `foo`, the responsibility shifts: `foo` is now the one creating `q`. At the moment `foo(q)` is called from `main`, `q` has not yet been initialized - it is just an unallocated reference. Passing an uninitialized variable to a regular parameter is not allowed, because Qmod expects a live quantum object there. The `Output` modifier changes this contract: `q: Output[QBit]` tells Qmod that `q` *enters the function uninitialized*, and that the function itself is responsible for initializing it (here, via `allocate`). This lines up with the call site in `main`, where `q` - itself an `Output` parameter - is still unallocated when `foo(q)` is invoked. In short: use `Output` whenever a function is responsible for **creating** a quantum variable, rather than receiving one that already exists. ```python theme={null} from classiq import * # Your code here ... # execute the model to see that we get similar results qprog = synthesize(main) res = sample(qprog) res ``` **Output:** ``` [{'q': 0}: 1051, {'q': 1}: 997] ``` ## Quantum Types, Statements and Opertions Now that we have grasped the principles that are essential for any Qmod code, we can start building up our expressive toolkit, letting us create increasingly sophisticated models. The following exercises introduce some of the most useful variable types, statements and operations that Qmod supports. # ## Exercise #1 * Quantum Arrays After we have familiarized with the `QBit` varible type (which is simply a single qubit), it is a good timing to introduce the quantum array type `QArray`. In this exercise, we will prepare the famous $|\Phi^+\rangle$ [Bell state](https://en.wikipedia.org/wiki/Bell_state) into a 2-qubit [Quantum array](https://docs.classiq.io/latest/qmod-reference/language-reference/quantum-types/#quantum-arrays). Recall that $|\Phi^+\rangle$ represents the state $\frac{1}{\sqrt{2}} (|00\rangle + |11\rangle)$. Instructions: 1. Declare a quantum variable `qarr` of type `QArray`, and initilize it by allocating to it 2 qubits. Don't forget to use the `Output` modifier. 2. Apply a Hadamard gate on the first qubit of `qarr`. Qmod counts from 0, so the first entry of `qarr` is `qarr[0]`. 1. Apply `CX` (controlled-NOT gate), with the `control` parameter being `qarr[0]` and the `target` parameter being `qarr[1]`. Synthesize and execute your model to assure that $|00\rangle$ and $|11\rangle$ are the only states to be measured, and that they are measured roughly equally. ```python theme={null} from classiq import * # Your code here: ... # execute and inspect the results qprog = synthesize(main) res = sample(qprog) res ``` **Output:** ``` [{'q': 1}: 1026, {'q': 0}: 1022] ``` # ## Exercise #2 * The Repeat Statement Use Qmod's `repeat` statement to create your own Hadamard Transform - a function that takes a `QArray` of an unspecified size and applies `H` to each of its qubits. Instructions: 1. Define a function `my_hadamard_transform`: * It should have a single `QArray` argument `q`. * Use `repeat` to apply `H` on each of `q`'s qubits. * Note that the `iteration` block of the `repeat` statement must use the Python `lambda` syntax (see `repeat` [documentation](https://docs.classiq.io/latest/qmod-reference/language-reference/statements/classical-control-flow/#classical-repeat)). 1. define a `main` function that initializes a `QArray` of length 10, and then passes it to `my_hadamard_transform`. The provided code continues by calling `show` to let you inspect the resulting circuit - make sure that is applies `H` to each of `q`'s qubits. ```python theme={null} from classiq import * # Your code here: ... # synthesize the model and show the result qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/39FkslFO3s96H1p0Byswqn8kNLh ``` **Output:** ``` https://platform.classiq.io/circuit/39FkslFO3s96H1p0Byswqn8kNLh?login=True&version=17 ``` # ## Exercise #3 * Power Raising a quantum operation to an integer power appears in many known algorithms; for example, in Grover search and Quantum Phase Estimation. In the general case the implementation involves repeating the same circuit multiple times. Sometimes, however, the implementation of the power operation can be simplified, thereby saving computational resources. A simple example is the operation of rotating a single qubit about the X, Y, or Z axis. In this case the rotation gate can be used once with the angle multiplied by the exponent. A similar example is the function [unitary](https://docs.classiq.io/latest/qmod-reference/library-reference/core-library-functions/unitary/unitary/) - an operation expressed as an explicit unitary matrix (i.e., all $2^n \times 2^n$ matrix terms are given). Raising the operation can be done by raising the matrix to that power via classical computation. See [power operator](https://docs.classiq.io/latest/qmod-reference/language-reference/statements/power/#syntax). Use the following code to define the value of a Qmod constant named `unitary_matrix` as a 4x4 (real) unitary: ```python theme={null} from typing import List import numpy as np from classiq import * rng = np.random.default_rng(seed=0) random_matrix = rng.random((4, 4)) qr_unitary, _ = np.linalg.qr(random_matrix) unitary_matrix = qr_unitary.tolist() ``` 1. Create a model that applies `unitary_matrix` on a 2-qubit variable three times (e.g. using `repeat`). 2. Create another model that applies `unitary_matrix` raised to the power of 3 on a 2-qubit variable. 3. Compare the gate count via the Classiq IDE in both cases. ```python theme={null} from classiq import * # Your code here: ... qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/39FktCgbPgQamVuDUKU7aryhsBX ``` **Output:** ``` https://platform.classiq.io/circuit/39FktCgbPgQamVuDUKU7aryhsBX?login=True&version=17 ``` # ## Exercise 4 * User-Defined Operators Create a function that applies a given single-qubit operation to all qubits in its quantum argument (call your function `my_apply_to_all`). Such a function is also called an operator; i.e., a function that takes another function as an argument (its operand). See [operators](https://docs.classiq.io/latest/qmod-reference/language-reference/operators/). Follow these guidelines: 1. Your function declares a parameter of type qubit array and a parameter of a function type with a single qubit parameter. 2. The body applies the operand to all qubits in the argument (you may use `repeat` or even `for` inside `my_apply_to_all` for this). Now, re-implement `my_hadamard_transform` from Exercise 2 so that its body calls `my_apply_to_all` rather than calling `repeat` directly. The goal is that `my_hadamard_transform` expresses *what* to do (apply `H` to all qubits), while `my_apply_to_all` encapsulates *how* to iterate. Use the same `main` function from Exercise 2. ```python theme={null} from classiq import * # Your code here: ... qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/39FktteE7eZY1m1BPPnKpcoFdrO ``` **Output:** ``` https://platform.classiq.io/circuit/39FktteE7eZY1m1BPPnKpcoFdrO?login=True&version=17 ``` # ## Exercise 5 * Quantum Conditionals # ### Exercise 5a * Control Operator Use the built-in `control` operator to create a function that receives two single qubit variables and uses one of them to control an RY gate with a `pi/2` angle acting on the other variable (without using the `CRY` function). See [control](https://docs.classiq.io/latest/qmod-reference/language-reference/statements/control/#syntax). ```python theme={null} from classiq import * # Your code here: ... qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/39FkuNEqkobTN3fzL04KPAHLctb ``` **Output:** ``` https://platform.classiq.io/circuit/39FkuNEqkobTN3fzL04KPAHLctb?login=True&version=17 ``` # ### Exercise 5b * Control Operator with Quantum Expressions The `control` operator is the conditional application of some operation, with the condition being that all control qubits are in the state $|1\rangle$. This notion is generalized in Qmod to other control states, where the condition is specified as a comparison between a quantum numeric variable and a numeric value, similar to a classical `if` statement. Quantum numeric variables are declared with class `QNum`. See [numeric types](https://docs.classiq.io/latest/qmod-reference/language-reference/quantum-types/#syntax). 1. Declare a `QNum` output argument using `Output[QNum]` and name it `x`. 2. Use numeric assignment (the `|=` operator) to initialize it to `9`. 3. Execute the circuit and observe the results. 4. Declare another output argument of type `QBit` and perform a `control` such that if `x` is 9, the qubit is flipped. Execute the circuit and observe the results. Repeat for a different condition. ```python theme={null} from classiq import * # Your code here: ... qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/39FkuwxPd8DP1cXGAdra4Gbthlk ``` **Output:** ``` https://platform.classiq.io/circuit/39FkuwxPd8DP1cXGAdra4Gbthlk?login=True&version=17 ``` # ## Exercise 6 * Phase Statement The `phase` statement allows the user to perform the mapping $|x\rangle \rightarrow e^{i\theta f(x_1, x_2, \ldots, x_n)} |x\rangle,$ given a function $f(x_1, x_2, \dots, x_n)$. This operation is extremely valuable to algorithms such as [Grover's](https://docs.classiq.io/latest/explore/algorithms/search_and_optimization/grover/grover/) and [QAOA](https://docs.classiq.io/latest/explore/tutorials/technology_demonstrations/qaoa/qaoa_demonstration/). See [phase](https://docs.classiq.io/latest/qmod-reference/language-reference/statements/phase/). # ### Exercise 6a * Phase with Arithmetic Condition 1. Declare a `QNum` output argument using `Output[QNum]` and name it `x`. 2. Allocate 4 qubits to `x`. 3. Perform a hadamard transform in `x`. 4. Using `phase`, create a phase according to the rule $f(x) = \pi \cdot x / 2$. 5. Apply the hadamard transform in `x` again. 6. Execute the quantum program and analyze the outputs. ```python theme={null} from classiq import * # Your code here: ... qprog = synthesize(main) show(qprog) ``` ## Solutions # ## Solution * Excercise #0 ```python theme={null} from classiq import * # rewrite the model, initializing q inside foo @qfunc def foo(q: Output[QBit]) -> None: allocate(1, q) X(q) H(q) @qfunc def main(q: Output[QBit]) -> None: foo(q) # execute the model to see that we get similar results qprog = synthesize(main) job = execute(qprog) job.get_sample_result().parsed_counts ``` **Output:** ``` [{'q': 1}: 1053, {'q': 0}: 995] ``` > **Key takeaway:** The `Output` modifier defines the beginning of a quantum variable's lifecycle. A parameter declared as `Output[T]` enters the function uninitialized; the function must allocate it before use. A quantum variable not declared as `Output` requires the caller to pass an already-initialized variable. Moving `allocate` into a function therefore requires adding `Output` to that parameter. # ## Solution * Exercise #1 ```python theme={null} from classiq import * @qfunc def bell(qarr: QArray[QBit, 2]) -> None: H(qarr[0]) CX(qarr[0], qarr[1]) @qfunc def main(qarr: Output[QArray]) -> None: allocate(2, qarr) bell(qarr) # execute and inspect the results qprog = synthesize(main) job = execute(qprog) job.get_sample_result().parsed_counts ``` **Output:** ``` [{'qarr': [0, 0]}: 1041, {'qarr': [1, 1]}: 1007] ``` > **Key takeaway:** `QArray` is the quantum equivalent of a classical array. Individual qubits are accessed by index (`qarr[0]`, `qarr[1]`, ...), and any operation can be applied to a specific element. Entanglement - such as the Bell state - arises from combining single-qubit gates (like `H`) with two-qubit gates (like `CX`). # ## Solution * Exercise #2 ```python theme={null} from classiq import * @qfunc def my_hadamard_transform(q: QArray[QBit]) -> None: repeat(q.len, lambda i: H(q[i])) @qfunc def main(q: Output[QArray[QBit]]) -> None: allocate(10, q) my_hadamard_transform(q) qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/39FkwWTQYravU16TBazOhSia9Mh ``` **Output:** ``` https://platform.classiq.io/circuit/39FkwWTQYravU16TBazOhSia9Mh?login=True&version=17 ``` > **Key takeaway:** The `repeat` statement is the standard way to apply an operation to every qubit in a `QArray`. The iteration body must be a Python `lambda` that receives the loop index. Because the array size can be left unspecified (`QArray[QBit]`), functions built with `repeat` work on arrays of any length without modification. # ## Solution * Exercise #3 ```python theme={null} from typing import List import numpy as np from classiq import * rng = np.random.default_rng(seed=0) random_matrix = rng.random((4, 4)) qr_unitary, _ = np.linalg.qr(random_matrix) unitary_matrix = qr_unitary.tolist() @qfunc def main(q: Output[QArray[QBit]]) -> None: allocate(2, q) power(3, lambda: unitary(unitary_matrix, q)) qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3DuGj36iISWmWu99iKy62wYDSI3 ``` > **Key takeaway:** Using `power(n, ...)` is more efficient than repeating an operation `n` times when the Classiq engine can exploit algebraic structure. For example, raising the unitary matrix to the power classically rather than replicating the circuit gates. This optimization is critical in algorithms such as Grover search and Quantum Phase Estimation, where operations must be applied many times. # ## Solution * Exercise #4 ```python theme={null} from classiq import * @qfunc def my_apply_to_all(operand: QCallable[QBit], q: QArray[QBit]) -> None: repeat(q.len, lambda i: operand(q[i])) @qfunc def my_hadamard_transform(q: QArray[QBit]) -> None: my_apply_to_all(lambda t: H(t), q) @qfunc def main(q: Output[QArray[QBit]]) -> None: allocate(10, q) my_hadamard_transform(q) qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/39FkxdlIYR5uVaLDdjkZptuMWNf ``` **Output:** ``` https://platform.classiq.io/circuit/39FkxdlIYR5uVaLDdjkZptuMWNf?login=True&version=17 ``` # ## Alternative Solution * Exercise #4 ```python theme={null} from classiq import * @qfunc def my_apply_to_all(operand: QCallable[QBit], q: QArray[QBit]) -> None: for i in range(q.len): operand(q[i]) @qfunc def my_hadamard_transform(q: QArray[QBit]) -> None: my_apply_to_all(lambda t: H(t), q) @qfunc def main(q: Output[QArray[QBit]]) -> None: allocate(10, q) my_hadamard_transform(q) qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3DuKk33l8U9zkvCjEBCOs32eye3 ``` > **Key takeaway:** User-defined operators (functions that accept other functions as arguments) separate *what* to do from *how* to iterate. `my_apply_to_all` encapsulates the looping logic; the caller expresses the intent: apply `H` to every qubit. Both `repeat` and a classical `for` loop are valid iteration mechanisms inside the operator body. # ## Solution * Exercise #5 # ### Solution * Exercise #5a ```python theme={null} from classiq import * from classiq.qmod.symbolic import pi @qfunc def my_controlled_ry(control_bit: QBit, target: QBit) -> None: control(ctrl=control_bit, stmt_block=lambda: RY(pi / 2, target)) @qfunc def main(control_bit: Output[QBit], target: Output[QBit]) -> None: allocate(1, control_bit) allocate(1, target) my_controlled_ry(control_bit, target) qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/39Fky8yK3HVrNdbfS9XOP4MfxHd ``` **Output:** ``` https://platform.classiq.io/circuit/39Fky8yK3HVrNdbfS9XOP4MfxHd?login=True&version=17 ``` > **Key takeaway:** The `control` operator lets you condition *any* quantum operation on a control qubit being in state $|1\rangle$, without needing a dedicated controlled gate. This is how Qmod builds controlled versions of arbitrary operations. # ### Solution * Exercise #5b ```python theme={null} from classiq import * @qfunc def main(x: Output[QNum], target: Output[QBit]) -> None: x |= 9 allocate(1, target) control(ctrl=(x == 9), stmt_block=lambda: X(target)) qprog = synthesize(main) show(qprog) ``` > **Key takeaway:** Quantum `control` generalizes beyond single-qubit conditions. A `QNum` variable can be compared to a classical integer (e.g., `x == 9`), and the resulting boolean expression used directly as the control condition. This is the quantum analog of a classical `if` statement: the controlled operation is applied (or not) depending on the value in the quantum register, across all branches of a superposition simultaneously. # ## Solution * Exercise #6a ```python theme={null} from classiq import * from classiq.qmod.symbolic import pi @qfunc def main(x: Output[QNum]): allocate(4, x) hadamard_transform(x) phase(x, pi / 2) hadamard_transform(x) qprog = synthesize(main) res = sample(qprog) res ``` **Output:** ``` [{'x': 2}: 1050, {'x': 3}: 998] ``` > **Key takeaway:** The `phase` statement applies a state-dependent phase $e^{i \theta f(x)}$ to each basis state without changing the measurement probabilities of `x` in isolation. When conjugated between Hadamard transforms, phase differences cause constructive and destructive interference that shifts probability weight to specific output states. # ## Solution * Exercise #6b ```python theme={null} from classiq import * from classiq.qmod.symbolic import pi @qfunc def main(x: Output[QArray]): allocate(4, x) hadamard_transform(x) control(ctrl=x, stmt_block=lambda: phase(pi)) control(ctrl=x[3], stmt_block=lambda: phase(pi)) hadamard_transform(x) qprog = synthesize(main) res = sample(qprog) res ``` **Output:** ``` Submitting job to simulator Job: https://platform.classiq.io/jobs/914bf74e-9512-4296-9b71-d4e94952c6ef ``` | | x | counts | probability | bitstring | | -- | ------------- | ------ | ----------- | --------- | | 0 | \[0, 0, 0, 1] | 1556 | 0.759766 | 1000 | | 1 | \[1, 0, 1, 0] | 46 | 0.022461 | 0101 | | 2 | \[0, 0, 1, 0] | 42 | 0.020508 | 0100 | | 3 | \[0, 1, 0, 1] | 39 | 0.019043 | 1010 | | 4 | \[1, 0, 1, 1] | 39 | 0.019043 | 1101 | | 5 | \[1, 1, 0, 0] | 38 | 0.018555 | 0011 | | 6 | \[1, 1, 1, 0] | 36 | 0.017578 | 0111 | | 7 | \[0, 0, 1, 1] | 33 | 0.016113 | 1100 | | 8 | \[1, 0, 0, 0] | 31 | 0.015137 | 0001 | | 9 | \[0, 1, 0, 0] | 31 | 0.015137 | 0010 | | 10 | \[1, 0, 0, 1] | 29 | 0.014160 | 1001 | | 11 | \[0, 0, 0, 0] | 28 | 0.013672 | 0000 | | 12 | \[0, 1, 1, 0] | 26 | 0.012695 | 0110 | | 13 | \[1, 1, 0, 1] | 26 | 0.012695 | 1011 | | 14 | \[1, 1, 1, 1] | 25 | 0.012207 | 1111 | | 15 | \[0, 1, 1, 1] | 23 | 0.011230 | 1110 | > **Key takeaway:** Phase operations and `control` can be combined to selectively apply a phase to specific computational basis states. The `hadamard_transform` utility applies `H` to all qubits of an array in a single call, and `control` with a full `QArray` as the operand conditions the operation on all control qubits being in state $|1\rangle$. # Qmod Tutorial - Part 2 Source: https://docs.classiq.io/explore/tutorials/basic_tutorials/the_classiq_tutorial/Qmod_tutorial_part2 Open this notebook in GitHub to run it yourself In this tutorial, we keep extending our expressive power by introducing more advanced topics: * Exponentiation and Pauli Operators. * Arithmetics and numeric assignment. * The `within_apply` statement. * The `bind` statement. Please make sure to go through execises 1-5 of part 1 before continuing with this notebook. ## Exercise 7 * Exponentiation and Pauli Operators The Qmod language supports different classical types: scalars, arrays, and structs. Structs are objects with member variables or fields. See [classical types](https://docs.classiq.io/latest/qmod-reference/language-reference/classical-types/#structs). In particular, Qmod offers a specialized syntax for creating [sparse Hamiltonians](https://docs.classiq.io/sdk-reference/qmod/classical-types#sparsepauliop). For that, simply use the `Pauli` Enum acting in the correct set of qubits. This exercise uses the Suzuki-Trotter function to find the evolution of `H=0.5XZXX + 0.25YIZI + 0.3 XIZY` (captured as a literal value for the Pauli operator), with the evolution coefficient being 3, the order being 2, and using 4 repetitions. See [suzuki\_trotter](https://docs.classiq.io/latest/qmod-reference/library-reference/core-library-functions/hamiltonian_evolution/suzuki_trotter/suzuki_trotter/). To complete this exercise, allocate q and invoke the `suzuki_trotter` quantum function: suzuki\_trotter(
 ...,
 evolution\_coefficient=3,
 repetitions=4,
 order=2,
 qbv=q,
)
```python theme={null} from classiq import * @qfunc def main(q: Output[QArray[QBit]]) -> None: allocate(4, q) # Your code here: qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3EJD16ju8qGJHOLmv7ceqqH4gDT ``` ## Exercise 8 * Basic Arithmetics This exercise uses quantum numeric variables and calculates expressions over them. See details on the syntax of numeric types in [quantum types](https://docs.classiq.io/latest/qmod-reference/language-reference/quantum-types/#syntax). See more on quantum expressions in [numeric assignment](https://docs.classiq.io/latest/qmod-reference/language-reference/statements/assignment/). # ## Exercise 8a Create this quantum program: 1. Initialize variables `x=2`, `y=7` and compute `res = x + y`. 2. Initialize variables `x=2`, `y=7` and compute `res = x * y`. 3. Initialize variables `x=2`, `y=7`, `z=1` and compute `res = x * y - z`. Guidance: * Use the `|=` operators to perform out-of-place assignment of arithmetic expressions. * To initialize the variables, use the `|=` to assgin it with a numerical value. ```python theme={null} from classiq import * # Your code here: qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3EJD1XbzQLEHJGtCnOgoDk2OERL ``` # ## Exercise 8b 1. Declare `x` to be a 2-qubit numeric variable and `y` a 3-qubit numeric variable. 2. Use `prepare_state` to initialize `x` to an equal superposition of `0` and `2`, and `y` to an equal superposition of `1`, `2`, `3`, and `6` (see [prepare\_state](https://docs.classiq.io/latest/qmod-reference/library-reference/core-library-functions/prepare_state_and_amplitudes/prepare_state_and_amplitudes/)). You can set the error bound to 0. 1. Compute `res = x + y`. Execute the resulting circuit. What did you get? ```python theme={null} from classiq import * # Your code here: qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3EJD1xzkRlXxUfnzro4AwYGBgT8 ``` ## Exercise 9 * Within-Apply The within-apply statement applies the $U^\dagger V U$ pattern that appears frequently in quantum computing. It allows you to compute a function `V` within the context of another function `U`, and afterward uncompute `U` to release auxiliary qubits storing intermediate results. See [within apply](https://docs.classiq.io/latest/qmod-reference/language-reference/statements/within-apply/). # ## Exercise 9a This exercise uses `within-apply` to compute an arithmetic expression in steps. Use the `within_apply` operation to calculate `res = x + y + z` from a two-variable addition building block with these steps: 1. Add `x` and `y` 2. Add the result to `z` 3. Uncompute the result of the first operation For simplicity, initialize the registers to simple integers: `x=3`, `y=5`, `z=2`. Hints: * Use a temporary variable. * Use the function syntax of numeric assignment. Execute the circuit and make sure you obtain the expected result. ```python theme={null} from classiq import * # Your code here: qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3EJD2Xsyx8PJ9ZPFpZoRTqvwskK ``` # ## Exercise 9b Why use `within-apply` and not just write three concatenated functions? To understand the motivation, create another arithmetic circuit. This time, however, set the Classiq synthesis engine to optimize on the circuit's number of qubits; i.e., its width. Determine constraints inside synthesis with `Constraints`. (See [here](https://docs.classiq.io/latest/user-guide/synthesis/constraints/)). Perform the operation `res = w + x + y + z`, where w is initialized to 4 and the rest as before: 1. Add `x` and `y` (as part of the `within_apply` operation) 2. Add the result to `z` (as part of the `within_apply` operation) 3. Uncompute the result of the first operation (as part of the `within_apply` operation) 4. Add the result of the second operation to `w`. There is no need to perform another uncomputation, as this brings the calculation to an end. Create the model, optimize on the circuit's width, and run the circuit. Can you identify where qubits have been released and reused? ```python theme={null} from classiq import * # Your code here: qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3EJD2kK3AR0gQ8zOhMJl66ditu2 ``` # ## Bonus: Use a Single Arithmetic Expression What happens when you don't manually decompose this expression? Use the Classiq arithmetic engine to calculate `res |= x + y + z + w` and optimize for width. Look at the resulting quantum program. Can you identify the computation and uncomputation blocks? What else do you notice? ```python theme={null} from classiq import * # Your code here: qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3EJD39TjQ7Ejxzh22iKgSsjFI5E ``` ## Exercise 10 * In-Place Arithmetics # ## Exercise 10a * Conditional Computation This exercise uses quantum numeric variables that represent fixed-point reals. A fixed-point variable `QNum[n, UNSIGNED, f]` uses `n` qubits to represent non-negative values, with `f` of those bits after the binary point - so `QNum[3, UNSIGNED, 3]` covers the range $[0, 1)$ in steps of $\frac{1}{8}$. The goal is to evaluate the following piecewise function over a superposition of fixed-point inputs: $$ f(x) = \begin{cases} 2x + 1 & \text{ if } 0 \leq x < 0.5 \\ x + 0.5 & \text{ if } 0.5 \leq x < 1 \end{cases} $$ The provided code skeleton puts `x` into a uniform superposition of all values in $[0, 1)$ via the Hadamard transform, and pre-allocates `res` to hold the result. Fill in the body of `main` to evaluate `f(x)` into `res`: 1. Compute a boolean quantum variable representing the condition `x < 0.5`. 2. Use `control` with `stmt_block` and `else_block` to apply the correct formula to `res` depending on the branch. To write into `res` inside each branch, use `inplace_xor(expression, res)`. You will learn in Exercise 10b exactly why this is needed instead of the familiar `|=` operator. Note: Python does not allow assignment operators (`|=`, `^=`, `+=`) inside lambda expressions. Factor the in-place computation out to a named `@qfunc` function and call it from the `control` lambda. ```python theme={null} from classiq import * @qfunc def main(x: Output[QNum[3, UNSIGNED, 3]], res: Output[QNum[5, UNSIGNED, 3]]) -> None: allocate(5, res) allocate(3, x) hadamard_transform(x) # Your code here: qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3EJD3VMGVfh3BeIWDlqT51Gmi9j ``` # ## Exercise 10b * In-Place Assignment **Why can't we use `|=` inside the `control` block?** The out-of-place operator `|=` requires its target to be *uninitialized* - it allocates a fresh set of qubits to store the result. In the code above, `res` is pre-allocated *before* the `control` block, so it is already initialized. Using `res |= expression` inside a branch lambda would fail: Qmod does not allow allocation into an already-initialized variable. **In-place operators.** The in-place operators write into an *existing* initialized variable without allocating new qubits: * `inplace_xor(expression, target)` - computes `expression` and XORs it bit-by-bit into `target` (equivalent to `target ^= expression`) * `inplace_add(expression, target)` - computes `expression` and adds it arithmetically into `target` (equivalent to `target += expression`) Both work inside a `control` block because they never try to allocate an uninitialized variable. They also avoid allocating a separate result register per branch - both branches share the single pre-allocated `res`, saving qubits. Since `res` is initialized to zero before the `control` block, `inplace_xor` and `inplace_add` produce the same result here (XOR with zero and ADD with zero are equivalent). They would differ if `res` had a non-zero initial value, or for multi-bit values where carries (ADD) and bit-by-bit XOR diverge. See [numeric assignment](https://docs.classiq.io/latest/qmod-reference/language-reference/statements/assignment/). **Exercise:** Modify your Exercise 10a solution to use `inplace_add` instead of `inplace_xor` and verify that you get the same measurement results. ```python theme={null} from classiq import * # Modify your Exercise 10a solution to use inplace_add instead of inplace_xor. # Your code here: qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3EJD3rWWB8kXwD8yUVIdqFcH4yG ``` ## Exercise 11 * A State-Preparation Algorithm # ## Binding The `bind` operation smoothly converts between different quantum types and splits or slices bits when necessary. Here is an example: ```python theme={null} from classiq import * @qfunc def main(res: Output[QArray[QBit]]) -> None: x = QArray() allocate(3, x) ... lsb = QBit() msb = QNum("msb", 2, False, 0) bind(x, [lsb, msb]) ... bind([lsb, msb], res) qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3EJD4EohCXx6VVspqiS2QV5jgZo ``` The first `bind` operation splits the 3-qubit variable `x` into the 2-qubit and single-qubit `lsb` and `msb` variables, respectively. After the `bind` operation: 1. The `lsb` and `msb` variables can be operated on separately. 2. The `x` variable returns to its uninitialized state and can no longer be used. The second `bind` operation concatenates the variables back to the `res` output variable. For this exercise, fill in the missing code parts in the above snippet and use the `control` statement to manually generate the 3-qubit probability distribution: `[1/8, 1/8, 1/8 - sqrt(3)/16, 1/8 + sqrt(3)/16, 1/8, 1/8, 1/8, 1/8]`. The following sequence of operations generates it: 1. Perform the Hadamard transform on all three qubits. 2. Apply a `pi/3` rotation on the LSB conditioned by the MSB being $|0\rangle$ and the second-to-last MSB being $|1\rangle$. How would you write this condition using a QNum? To validate your results without looking at the full solution, compare them to running using the Classiq built-in `prepare_state` function. ```python theme={null} import numpy as np from classiq import * @qfunc def pre_prepared_state(q: Output[QArray]) -> None: prepare_state( [ 1 / 8, 1 / 8, 1 / 8 - np.sqrt(3) / 16, 1 / 8 + np.sqrt(3) / 16, 1 / 8, 1 / 8, 1 / 8, 1 / 8, ], 0.0, q, ) # Your code here: ``` ## Solutions # ## Exercise 7 ```python theme={null} # Solution to Exercise 7: from classiq import * @qfunc def main(q: Output[QArray[QBit]]) -> None: allocate(4, q) suzuki_trotter( 0.5 * Pauli.X(0) * Pauli.X(1) * Pauli.Z(2) * Pauli.X(3) + 0.25 * Pauli.Z(1) * Pauli.Y(3) + 0.3 * Pauli.Y(0) * Pauli.Z(1) * Pauli.X(3), evolution_coefficient=3, repetitions=4, order=2, qbv=q, ) qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3EJD4bxC6yg7td6tBOLM6Xflu1k ``` > **Key takeaway:** Qmod supports Pauli operator expressions as a native type for specifying sparse Hamiltonians. `suzuki_trotter` implements time evolution under such a Hamiltonian by interleaving the exponentials of individual Pauli terms approximating $e^{-iHt}$. The `order` and `repetitions` parameters control the accuracy of the approximation. # ## Exercise 8 ```python theme={null} # Solution to Exercise 8a: from classiq import * @qfunc def main(x: Output[QNum], y: Output[QNum], z: Output[QNum], res: Output[QNum]) -> None: x |= 2 y |= 7 z |= 1 # res |= x + y # res |= x * y res |= x * y - z qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3EJD5JXTHRLi4LIhpVJVTToUW9q ``` > **Key takeaway:** The `|=` operator performs *out-of-place* numeric assignment: it allocates a fresh quantum register to store the result of the arithmetic expression. Complex expressions combining `+`, `*`, and `-` are fully supported and evaluated quantum-mechanically. ```python theme={null} # Solution to Exercise 8b: from classiq import * @qfunc def main(x: Output[QNum], y: Output[QNum], res: Output[QNum]) -> None: prepare_state(probabilities=[0.5, 0, 0.5, 0.0], bound=0.0, out=x) prepare_state( probabilities=[0, 0.25, 0.25, 0.25, 0.0, 0.0, 0.25, 0.0], bound=0.0, out=y ) res |= x + y qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3EJD5wotnM0pIRbEbVf5gG5uGQ0 ``` > **Key takeaway:** Quantum arithmetic operates over superpositions simultaneously. When `x` and `y` each encode a superposition of values, `res |= x + y` produces a superposition of all corresponding sums, being one for each pair of input values. This is the computational parallelism that quantum arithmetic provides. # ## Exercise 9 ```python theme={null} # Solution to Exercise 9: from classiq import * @qfunc def main(res: Output[QNum]) -> None: x = QNum() y = QNum() z = QNum() x |= 3 y |= 5 z |= 2 temp = QNum() within_apply( within=lambda: assign(x + y, temp), apply=lambda: assign(temp + z, res) ) qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3EJD6kxzvd0blHHULXShaKhWbPO ``` > **Key takeaway:** `within_apply` automates the $U^\dagger V U$ uncomputation pattern: it runs the `within` block, then the `apply` block, then automatically reverses the `within` block freeing the qubits held by temporary variables. Without it, temporaries stay initialized for the rest of the circuit, permanently occupying qubits. Note: because `within` and `apply` must be Python lambdas, and expressions, such as `|=`, cannot appear in a lambda, use `assign(expression, target)` as the functional equivalent. ```python theme={null} # Solution to the advanced part of Exercise 9: from classiq import * @qfunc def main(res: Output[QNum]) -> None: x = QNum() y = QNum() z = QNum() w = QNum() x |= 3 y |= 5 z |= 2 w |= 4 temp_xy = QNum() xyz = QNum() within_apply( within=lambda: assign(x + y, temp_xy), apply=lambda: assign(temp_xy + z, xyz), ) res |= xyz + w const = Constraints(optimization_parameter=OptimizationParameter.WIDTH) qprog = synthesize(main, constraints=const) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3EJD7kXtAkdA9u4Yhddt7ACLElK ``` > **Key takeaway:** Qubit reuse is only possible when temporary variables are properly uncomputed. `within_apply` enables the synthesizer to reclaim freed qubits for subsequent operations. The synthesis optimization on width (`OptimizationParameter.WIDTH`) makes this reuse explicit: the same qubits appear in different logical roles at different points in the circuit. # ## Exercise 10a ```python theme={null} # Solution to Exercise 10: from classiq import * @qfunc def main(x: Output[QNum[3, UNSIGNED, 3]], res: Output[QNum[5, UNSIGNED, 3]]) -> None: allocate(5, res) allocate(3, x) hadamard_transform(x) aux = QBit() aux |= x < 0.5 control( aux, stmt_block=lambda: inplace_xor(2.0 * x + 1.0, res), else_block=lambda: inplace_xor(1.0 * x + 0.5, res), ) qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3EJD8i2H6PuZsGS4qNnYL87Ak7M ``` > **Key takeaway:** Piecewise quantum functions are implemented by computing a boolean condition into an auxiliary qubit (`aux |= x < 0.5`) and using `control` with `stmt_block` and `else_block` to select the appropriate formula. Because `x` is in a superposition, the model evaluates both branches in parallel: each computational basis state follows the branch dictated by its own value of `x`. # ## Exercise 10b ```python theme={null} # Solution to Exercise 10b: from classiq import * @qfunc def main(x: Output[QNum[3, UNSIGNED, 3]], res: Output[QNum[5, UNSIGNED, 3]]) -> None: allocate(5, res) allocate(3, x) hadamard_transform(x) aux = QBit() aux |= x < 0.5 control( aux, stmt_block=lambda: inplace_add(2.0 * x + 1.0, res), else_block=lambda: inplace_add(1.0 * x + 0.5, res), ) qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3EJD9el3okie9bOkxDBmFj9N0QA ``` > **Key takeaway:** In-place operators (`inplace_xor`, `inplace_add`) are necessary when writing into a pre-initialized variable inside a quantum operator like `control`. The out-of-place `|=` cannot be used there because `res` is initialized at the beginning of the quantum program. In-place operators also avoid allocating a separate result register per branch - both branches share the single pre-allocated `res`, saving qubits. When the target starts at zero, `inplace_xor` and `inplace_add` give identical results; they diverge for non-zero initial values or when arithmetic carries differ from bitwise XOR. # ## Exercise 11 ```python theme={null} # Solution to Exercise 11: from classiq import * from classiq.qmod.symbolic import pi @qfunc def main(res: Output[QArray[QBit]]) -> None: x = QArray() allocate(3, x) hadamard_transform(x) lsb = QBit() msb = QNum() bind(x, [lsb, msb]) control(msb == 1, lambda: RY(pi / 3, lsb)) bind([lsb, msb], res) qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3EJDAESo5jA0QNqhBKdLrgQSQRA ``` > **Key takeaway:** The `bind` operation casts and splits quantum variables into different quantum types. In this example, after the `bind` statement, the variable `x` is split into two different quantum types: a qubit `lsb` and a quantum number `msb`. Since `msb` is a quantum number, it is possible to perform numeric operations with it, such as compare it to an integer,as it is done inside the `control` operation. # Classiq Overview Tutorial Source: https://docs.classiq.io/explore/tutorials/basic_tutorials/the_classiq_tutorial/classiq_overview_tutorial Open this notebook in GitHub to run it yourself In this notebook we introduce a typical workflow with Classiq: * **Designing a quantum model** using the Qmod language and it's accompanied function library. * **Synthesizing the model** into a concrete circuit implementation. * **Executing the program** on a chosen simulator or quantum hardware. * **Post-processing** the results. Later tutorials dive into each of the above stages, providing hands-on interactive guides that go from the very basics to advanced usage. To get started, run: ```python theme={null} from classiq import * ``` If this `import` doesn't work for you, please try `pip install classiq` in your terminal, or refer to [Registration and Installation](https://docs.classiq.io/latest/getting-started/registration_installations/). ## Designing a Quantum Model Here we will define a quantum function `main` that calculates a simple arithmetic expression: ```python theme={null} @qfunc def main(x: Output[QNum], y: Output[QNum]) -> None: allocate(3, x) hadamard_transform(x) y |= x**2 + 1 ``` Explaining the code step-by-step: 1. Allocate 3 qbits for the quantum number `x`, so that it can represent $2^3$ different numbers, from 0 to 7 (for example, the bitstring '010' represents the number 2). 2. Apply `hadamard_transform` to `x`, creating an equal superposition of all these values. 3. Assign the desired arithmetic expression's result to the quantum number `y`. A moment before measurement, we expect the output variables `x` and `y` to be in an equal superposition of the states $|x_i\rangle |y_i=x_i^2+1\rangle$ for $x_is$ from 0 to 7. In other words, we have designed our quantum model to calculate $x^2 +1$. ## Synthesizing the Model The function `main` describes the model in a high-level manner: "calculate $x^2+1$ and assign in into `y`". However, it does not specify **how** to implement this calculation - it does not map it to an executable quantum circuit, in terms of elementary quantum gates applied to specific qubits. In order to do so, we use Classiq's synthesis engine. To do so, simply pass `main` to the synthesis engine to obtain a concrete quantum program `qprog`. Here we simply call the function `synthesize`. Later on we will learn to provide configuration details (e.g. which elementary gates are allowed, or what resources we are trying to optimize). ```python theme={null} qprog = synthesize(main) ``` We can analyze the resulting implementation using Classiq's visualization tool: ```python theme={null} show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3DuELJ8DVHRQscHrzbO4x13aoiE ``` This should pop up a web page with something like this:
vis
By clicking the `+` icons on the blocks' top-left corner, we can inspect the gate-level implementation of each functional block. For example, inspect the complex combination of `H`, `CPHASE`, `CX` and `U` gates that implements the `Power` block. ## Executing the Quantum Program Now that we have a concrete circuit implementation of the desired model, we can execute it and sample the resulting states of the output variables. Here we will simply call the function `sample`, which uses Classiq's quantum simulator by default to sample the multiple executions of the quantum program (the default `n_shots` is 2048): ```python theme={null} res = sample(qprog) ``` **Output:** ``` Submitting job to simulator Job: https://platform.classiq.io/jobs/3da66afe-a388-44e3-b9a6-34fc7872c2cf ``` Later on we will learn how to execute on hardwares and simulators of our choice and manage advanced executions (for example, hybrid execution that uses classical logic to alter the circuit between runs). ## Post-Processing the Results Having executed the quantum program multiple times (`n_shots`=2048) we can now inspect the possible pairs `x`,`y` that our arithmetic expression allows ($y=x^2+1$). This can be done by looking into `res` - a `dataframe` that contain all the states that were measured on the output variables, ordered by the number of counts that they were measured. ```python theme={null} res ``` | | x | y | counts | probability | bitstring | | - | - | -- | ------ | ----------- | --------- | | 0 | 4 | 17 | 270 | 0.131836 | 010001100 | | 1 | 3 | 10 | 266 | 0.129883 | 001010011 | | 2 | 7 | 50 | 265 | 0.129395 | 110010111 | | 3 | 6 | 37 | 261 | 0.127441 | 100101110 | | 4 | 0 | 1 | 258 | 0.125977 | 000001000 | | 5 | 2 | 5 | 251 | 0.122559 | 000101010 | | 6 | 1 | 2 | 242 | 0.118164 | 000010001 | | 7 | 5 | 26 | 235 | 0.114746 | 011010101 | As expected, all possible values of `x` (integers from 0 to 7) were measured roughly similar number of times, and with each `x` measured, the measurement of `y` satisfies $y=x^2+1$. Alternatively, you can inspect the histogram of sampled states in the [Classiq IDE](https://platform.classiq.io/jobs).
vis
Hovering above each of the histogram bars shows its bitstring and its parsed variables values. For example, the bitstring '010001100' is parsed as `x`=4, `y`=17, because the first 3 qubits (counting from the right) correspond to `x` and were measured as '100'=4 (in binary), and the other 6 qubits which correspond to `y` were measured as '010001'= 17. ## Summary In this tutorial, we have gone through a typical workflow using Classiq: 1. Designing a quantum model: the problem we wanted to solve is calculating an arithmetic expression for a given domain of `x` values. We used `hadamard_transform` and arithmetic assignment as our modeling building blocks. 2. Synthesizing the model into a concrete circuit implementation: we called `synthesize` to let Classiq's synthesis engine take our high-level description and implement it in an executable way. 3. Executing the program: we called `execute` to run our quantum program multiple times on Classiq's simulator. 4. Post-processing: We inspected the measured states of `x` and `y` - for each `x` and assured ourselves that they satisfy the desired arithmetic expression. # ## Food for Thought You might have noticed that the model discussed here does not truly harness the power of quantum computers: a moment before sampling the qubits, `x` and `y` indeed hold "the answers to all questions" simultaneously (all the pairs `x` and `y` that satisfy the equation), but we cannot access these answers until we measure the qubits, which collapses the superposition and leaves only a single (and randomly chosen) pair of `x` and `y`. Having said that, we have no choice but to run multiple times (many more than $2^3$ in our case) to make sure that we measure all `x`s of interest. A classical computer could obtain the same information in exactly $2^3$ runs. Then, why bother? While pure arithmetic alone may never be a primary task for quantum computers, quantum arithmetic plays a crucial role in many quantum algorithms that do exploit quantum speedup. For example, it is widely used in oracle functions within Grover's search algorithm and in quantum cryptographic protocols. ## Practice Edit the arithmetic expression inside `main`, using the `+`, `-`, `**` operators as well as literal numbers of your choice. Validate that the sampled states of `x` and `y` satsify your arithmetic expression. # Execution Tutorial - Part 1 Source: https://docs.classiq.io/explore/tutorials/basic_tutorials/the_classiq_tutorial/execution_tutorial Open this notebook in GitHub to run it yourself This tutorial covers the basics of executing a quantum program using Classiq directly through the Python SDK. It is also possible to use the [Classiq Platform](https://platform.classiq.io) to execute quantum algorithms. For this, we will start by synthesizing the following example from the [synthesis tutorial](https://docs.classiq.io/latest/explore/tutorials/basic_tutorials/the_classiq_tutorial/synthesis_tutorial/): ## Example 1: Sampling Arithmetics and Changing Number of Shots ```python theme={null} from classiq import * @qfunc def main(x: Output[QNum[3]], y: Output[QNum]) -> None: allocate(x) hadamard_transform(x) y |= x**2 + 1 qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3EJKxJJ639O4HKXMbzBJV92B2vJ ``` This quantum program evaluates the function $y(x) = x^2 + 1$, for all integers $x \in [0,7]$. To execute a quantum program and sample the states, use `sample`: ```python theme={null} results = sample(qprog) ``` **Output:** ``` Submitting job to simulator Job: https://platform.classiq.io/jobs/afa2ebcd-8cb9-4f59-8507-94ed94b802dc ``` The output from `sample` is a dataframe with information regarding execution: ```python theme={null} results ``` | | x | y | counts | probability | bitstring | | - | - | -- | ------ | ----------- | --------- | | 0 | 7 | 50 | 280 | 0.136719 | 110010111 | | 1 | 5 | 26 | 273 | 0.133301 | 011010101 | | 2 | 1 | 2 | 266 | 0.129883 | 000010001 | | 3 | 0 | 1 | 262 | 0.127930 | 000001000 | | 4 | 2 | 5 | 250 | 0.122070 | 000101010 | | 5 | 6 | 37 | 243 | 0.118652 | 100101110 | | 6 | 3 | 10 | 238 | 0.116211 | 001010011 | | 7 | 4 | 17 | 236 | 0.115234 | 010001100 | The information displayed in the dataframe is: * `counts` shows the number of times each state was measured. * `bitstring` is the bitstring that represents each state measured. * `x` and `y` are the numerical representation of the states associated with the measurement. * `probability` is the probability associated with each measured state. By default, the number of executions of the quantum program is $2048$. This quantity, called the number of shots, can be modified inside `sample`. For instance, if we want to execute the same circuit with $10{,}000$ shots: ```python theme={null} results_more_shots = sample(qprog, num_shots=10000) ``` **Output:** ``` Submitting job to simulator Job: https://platform.classiq.io/jobs/5cd7b05e-2751-47a6-9d46-50a2e702244b ``` The number of counts for each state will grow proportionally with the number of shots: ```python theme={null} results_more_shots ``` | | x | y | counts | probability | bitstring | | - | - | -- | ------ | ----------- | --------- | | 0 | 3 | 10 | 1308 | 0.1308 | 001010011 | | 1 | 7 | 50 | 1271 | 0.1271 | 110010111 | | 2 | 4 | 17 | 1266 | 0.1266 | 010001100 | | 3 | 0 | 1 | 1256 | 0.1256 | 000001000 | | 4 | 2 | 5 | 1256 | 0.1256 | 000101010 | | 5 | 6 | 37 | 1229 | 0.1229 | 100101110 | | 6 | 1 | 2 | 1214 | 0.1214 | 000010001 | | 7 | 5 | 26 | 1200 | 0.1200 | 011010101 | ## Example 2: GHZ States and Noise Many simulators provide noise models that approximate the behavior of real quantum hardware. In this example, we create a GHZ state using the Classiq simulator while emulating the noise profile of IBM Pittsburgh, an IBM backend available through Classiq. We begin by defining the model for the GHZ state: ```python theme={null} from classiq import * @qfunc def main(x: Output[QArray[QBit, 3]]): allocate(x) H(x[0]) CX(x[0], x[1]) CX(x[1], x[2]) qprog = synthesize(main) ``` Next, we configure the simulator to use the noise model associated with the IBM Pittsburgh backend. This is done by passing a `noise_model` entry through the `config` argument: ```python theme={null} cfg = {"noise_model": "ibm_pittsburgh"} res = sample(qprog, backend="simulator", config=cfg) res ``` **Output:** ``` Submitting job to simulator Job: https://platform.classiq.io/jobs/6f56f8a5-1441-450f-b24c-2d4b7232dafc ``` | | x | counts | probability | bitstring | | - | ---------- | ------ | ----------- | --------- | | 0 | \[1, 1, 1] | 1014 | 0.495117 | 111 | | 1 | \[0, 0, 0] | 985 | 0.480957 | 000 | | 2 | \[0, 0, 1] | 10 | 0.004883 | 100 | | 3 | \[1, 1, 0] | 9 | 0.004395 | 011 | | 4 | \[0, 1, 1] | 9 | 0.004395 | 110 | | 5 | \[0, 1, 0] | 8 | 0.003906 | 010 | | 6 | \[1, 0, 1] | 7 | 0.003418 | 101 | | 7 | \[1, 0, 0] | 6 | 0.002930 | 001 | For an ideal, noiseless GHZ state, the only expected measurement outcomes are `[0, 0, 0]` and `[1, 1, 1]`, each occurring with approximately equal probability. All other basis states should have zero probability. Here, because the simulation includes a realistic noise model, a small fraction of the measurements appears in other states. The dominant outcomes are still `[0, 0, 0]` and `[1, 1, 1]`, but the presence of low-probability additional bitstrings reflects the effect of hardware noise on the quantum program execution. ## State Vector Simulation A state vector simulator returns the amplitudes of the quantum states produced by a quantum program. Unlike sampling, which estimates output probabilities from repeated measurements, state vector simulation gives direct access to the simulated quantum state. On real quantum hardware, these amplitudes are not directly observable. Reconstructing them requires quantum state tomography, which involves measuring the system in different bases to infer the output state. In this example, we calculate the state vector of the quantum program using `calculate_state_vector`: ```python theme={null} res_sv = calculate_state_vector(qprog) res_sv ``` **Output:** ``` Submitting job to simulator Job: https://platform.classiq.io/jobs/2eced471-369f-4048-b515-3a8d83988f80 ``` | | x | amplitude | magnitude | phase | probability | bitstring | | - | ---------- | ------------------ | --------- | ----- | ----------- | --------- | | 0 | \[0, 0, 0] | 0.707107+0.000000j | 0.71 | 0.00π | 0.5 | 000 | | 1 | \[1, 1, 1] | 0.707107+0.000000j | 0.71 | 0.00π | 0.5 | 111 | The information displayed in the dataframe is: * `amplitude` is the complex amplitude associated with each basis state. * `magnitude` is the absolute value of the amplitude. * `phase` is the phase of the amplitude. * `probability` is the probability of measuring the corresponding state. * `bitstring` is the bitstring representation of the basis state. * `x` is the value of the quantum array. It is displayed as a list of 0s and 1s, with each entry corresponding to one qubit in the array. In this case, the output corresponds to an ideal GHZ state. The only states with nonzero probability are `[0, 0, 0]` and `[1, 1, 1]`, each with probability 0.5 and amplitude approximately $1/\sqrt{2}$. ## Backend Selection The backend of an execution is the hardware or simulator where the quantum program is executed. To select a specific backend, it is necessary to know its correct name and provider. To do so, run `get_backend_details()` for a concise list of available backends. ```python theme={null} backend_list = get_backend_details() backend_list.head() ``` | | provider | backend | type | num\_qubits | is\_available | pending\_jobs | queue\_time | | - | ---------- | --------------------------------- | --------- | ----------- | ------------- | ------------- | ----------- | | 0 | classiq | nvidia\_simulator | simulator | 29 | True | NaN | NaT | | 1 | classiq | simulator | simulator | 28 | True | NaN | NaT | | 2 | classiq | simulator\_density\_matrix | simulator | 28 | True | NaN | NaT | | 3 | classiq | simulator\_matrix\_product\_state | simulator | 28 | True | NaN | NaT | | 4 | alice\&bob | LOGICAL\_EARLY | simulator | 15 | True | NaN | NaT | Now, to define a backend, set the backend name following the rule `"provider/backend"` under the execution function used. For example, you can use the [Classiq simulator](https://docs.classiq.io/user-guide/execution/cloud-providers/classiq-backends#supported-backends) to realize a state vector simulation of the GHZ state, or the [MPS simulator](https://docs.classiq.io/user-guide/execution/cloud-providers/classiq-backends#supported-backends) to sample over the same state: ```python theme={null} default_backend = "classiq/simulator" MPS_backend = "classiq/simulator_matrix_product_state" res_default = calculate_state_vector(qprog, backend=default_backend) res_MPS = sample(qprog, MPS_backend) ``` **Output:** ``` Submitting job to classiq/simulator Job: https://platform.classiq.io/jobs/301d33f4-97a4-46bf-8794-aef3e6da2c6e Submitting job to classiq/simulator_matrix_product_state Job: https://platform.classiq.io/jobs/b703a7ac-9e4d-4752-8166-4474c5eb8c66 ``` ```python theme={null} res_default ``` | | x | amplitude | magnitude | phase | probability | bitstring | | - | ---------- | ------------------ | --------- | ----- | ----------- | --------- | | 0 | \[0, 0, 0] | 0.707107+0.000000j | 0.71 | 0.00π | 0.5 | 000 | | 1 | \[1, 1, 1] | 0.707107+0.000000j | 0.71 | 0.00π | 0.5 | 111 | ```python theme={null} res_MPS ``` | | x | counts | probability | bitstring | | - | ---------- | ------ | ----------- | --------- | | 0 | \[0, 0, 0] | 1062 | 0.518555 | 000 | | 1 | \[1, 1, 1] | 986 | 0.481445 | 111 | # Execution Tutorial - Part 2 Source: https://docs.classiq.io/explore/tutorials/basic_tutorials/the_classiq_tutorial/execution_tutorial_part2 Open this notebook in GitHub to run it yourself ## Expectation Values and Parameterized Quantum Programs This tutorial covers the basics of measuring observables expressed as linear combinations of Pauli strings and executing a parameterized quantum program using Classiq via Python SDK. Alternatively, you can use the [Classiq IDE web page](https://platform.classiq.io) to execute quantum algorithms. A parameterized quantum program is a quantum circuit with adjustable parameters, such as angles in rotation gates, that can be tuned to alter the circuit's behavior. Think of it like tuning a camera with adjustable settings: the camera (the circuit) stays the same, but adjusting the settings (parameters) changes the captured images (outputs). In quantum computing, tuning these parameters helps identify the configuration that yields the most useful results. These programs are particularly useful in quantum machine learning and optimization, where the goal is to find the best parameter set. First, we create a parameterized quantum program using two qubits. The program applies an X gate, a parameterized RY rotation, and a CX gate. The rotation angle is controlled by a variable called `angle`. ```python theme={null} from classiq import * @qfunc def main(angle: CReal, x: Output[QBit], y: Output[QBit]) -> None: allocate(x) allocate(y) X(x) RY(angle, x) CX(x, y) qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3DuJkbhvAlRZokLgJCdNrEWdUqX ``` The first thing we can do is to sample the outputs of the quantum program for a given parameter. For example, $\pi / 2$. We can now execute the quantum program and obtain a sample of output states using [ExecutionSession](https://docs.classiq.io/latest/sdk-reference/execution/#classiq.execution.ExecutionSession). To do this, we define the parameter values using a dictionary. ```python theme={null} import numpy as np # Set angle parameter to pi/2 for sampling parameter = {"angle": np.pi / 2} ``` After generating the `ExecutionSession`, it is possible to show the counts for this particular parameter value: ```python theme={null} first_sample = sample(qprog, parameters=parameter) print("Counts for angle = pi/2: ") first_sample ``` **Output:** ``` Submitting job to simulator Job: https://platform.classiq.io/jobs/6577986e-df35-477e-921c-9ba673415632 ``` **Output:** ``` Counts for angle = pi/2: ``` | | x | y | counts | probability | bitstring | | - | - | - | ------ | ----------- | --------- | | 0 | 0 | 0 | 1031 | 0.503418 | 00 | | 1 | 1 | 1 | 1017 | 0.496582 | 11 | Running your circuit with different parameter values helps explore how the output changes, revealing trends or minima in a cost function. This is especially useful in quantum optimization. As an example, we'll evaluate the circuit over 50 values of `angle` from $0$ to $2\pi$. ```python theme={null} # Create a list of 50 angle values from 0 to 2π angles_list = np.linspace(0, 2 * np.pi, 50) parameters_list = [{"angle": angles} for angles in angles_list] # Execute batch sampling over all angles second_sample = sample(qprog, parameters=parameters_list) ``` **Output:** ``` Submitting job to simulator Job: https://platform.classiq.io/jobs/3b55e716-dc5f-44c0-a53c-cb542aaf65b6 ``` When the `parameters` argument is a list, the result of `sample` is also a list of results, one for each angle value. Therefore, we can analyze the data from each parameter on `angles_list`. For example, an interesting way of analyzing this data is to plot the number of counts of the states $|00\rangle$ and $|11\rangle$ as functions of `angle`: ```python theme={null} # extract counts of |11> and |00> for each result def get_counts(res, bitstring): pops = [] for results in res: matches = results[results["bitstring"] == bitstring] if matches.empty: pops.append(0) else: pops.append(matches["counts"].iloc[0]) return pops pops_00 = get_counts(second_sample, "00") pops_11 = get_counts(second_sample, "11") ``` ```python theme={null} import matplotlib.pyplot as plt plt.figure(figsize=(8, 5)) plt.plot(angles_list / np.pi, pops_00, label='Counts of "00"') plt.plot(angles_list / np.pi, pops_11, label='Counts of "11"', linestyle="-.") plt.xlabel(r"$\mathrm{Angle} \; (\pi)$") plt.ylabel("Counts") plt.legend() plt.show() ``` output ## Measuring Pauli Strings Measuring observables from a quantum program turns out to be necessary when you want to obtain information that can't be accessed only from the populations of states. For this end, you can measure Pauli Strings using Classiq. As an example, if we want to measure how close the output of our system is to the Bell state: $$ |\Phi^+ \rangle = \frac{1}{\sqrt{2}} \left( |00\rangle + |11\rangle \right), $$ it is possible measure the expected value of its projection: $$ P(\Phi^+) = |\Phi^+ \rangle \langle \Phi^+ | = \frac{1}{2} \left( |00\rangle + |11\rangle \right) \left( \langle00| + \langle 11| \right) = \frac{1}{2} \left ( |00\rangle \langle 00| + |00\rangle \langle 11 | + |11 \rangle \langle 00 | + |11\rangle \langle |11\right). $$ The projector operator, by its turn, can be represented as a Pauli string: $$ P(\Phi^+) = \frac{1}{4} \left( II + XX - YY + ZZ \right) $$ Therefore, if we want to measure the projector expected value for some output of the quantum circuit, say angle $= \pi/5$, it is possible using `estimate` and `ExecutionSession`. For this, first we need to define the Hamiltonian to be measured: ```python theme={null} projector_operator = 0.25 * ( Pauli.I(0) * Pauli.I(1) + Pauli.X(0) * Pauli.X(1) - Pauli.Y(0) * Pauli.Y(1) + Pauli.Z(0) * Pauli.Z(1) ) ``` Now, using `observe`, evaluate the expected value of the output: ```python theme={null} parameter = {"angle": np.pi / 5} expectation_value_1 = observe(qprog, projector_operator, parameters=parameter) print("Expected value for angle = pi/5: ", expectation_value_1) ``` **Output:** ``` Submitting job to simulator Job: https://platform.classiq.io/jobs/a599de9a-f855-493f-bc5d-21e12e106008 ``` **Output:** ``` Expected value for angle = pi/5: 0.212890625 ``` The same can be done in batches, for example, if we want to plot a graph of fidelity between the output of the quantum program and the $|\Phi^+\rangle$ state: ```python theme={null} expectation_value_2 = observe(qprog, projector_operator, parameters=parameters_list) ``` **Output:** ``` Submitting job to simulator Job: https://platform.classiq.io/jobs/62178e6a-1e58-4fac-817b-d9f99a3a5f4d ``` ```python theme={null} plt.figure(figsize=(8, 5)) plt.plot(angles_list / np.pi, expectation_value_2, label="Expectation value") plt.xlabel(r"$\mathrm{Angle} \; (\pi)$") plt.ylabel(r"$|\langle \Phi^+ | \psi \rangle |^2$") plt.legend() plt.show() ``` output ## Retrieving Jobs Executed Using Execution Session When executing a job that may take longer to complete, or when running on a hardware backend with a job queue, it is useful to have a way to retrieve the job results later. For this purpose, Classiq supports submitting an `ExecutionJob`, which is associated with a unique job ID. The job can then be retrieved later using this ID. To submit a job, use an `ExecutionSession` together with one of the execution functions that has the [`submit_` prefix](https://docs.classiq.io/sdk-reference/execution#submit_sample), such as `submit_sample`. As an example, we submit two different jobs and then retrieve their outputs using their job IDs. First, we submit the jobs: ```python theme={null} with ExecutionSession(qprog) as execution_session: # These are the sampling jobs sample_job = execution_session.submit_sample(parameter) # These are the estimate jobs estimate_job = execution_session.submit_estimate(projector_operator, parameter) # These are the job IDs for the respective jobs sample_job_ID = sample_job.id estimate_job_ID = estimate_job.id ``` Once you have the job ID, it is possible to retrieve its execution data using `ExecutionJob`. For example, here we retrieve a previously execution of `estimate_job` and compare it to the outputs of `first_sample` - they should be close: ```python theme={null} # Retrieving the job from its ID retrieved_estimate = ExecutionJob.from_id(estimate_job_ID) print("Retrieved job result:", retrieved_estimate.result_value().value) ``` **Output:** ``` Retrieved job result: (0.19482421875+0j) ``` As expected, the retrieved job result is very close to the first sample, since they execute the same quantum circuit. ## Application: Variational Quantum Circuit to Prepare a Bell State In this additional session, we create a simple parameterized quantum algorithm that prepares the Bell State $|\Phi^+\rangle$. For this, an ansatz with two parameters is constructed: ```python theme={null} # Note that now angles are declared as a CArray[CReal, 2], where 2 represents its length @qfunc def main(angles: CArray[CReal, 2], x: Output[QBit], y: Output[QBit]) -> None: allocate(x) allocate(y) RX(angles[0], x) RY(angles[1], x) CX(x, y) qprog_bell = synthesize(main) ``` Then define the function that is subject to classical optimization. In this case, we aim to maximize the expected value of the `projector_operator`. Therefore, we create a `negative_coeffs_projector_operator` to minimize: ```python theme={null} negative_coeffs_projector_operator = (-1) * projector_operator ``` The final step is to perform the optimization of the cost function defined by the quantum ansatz. In this tutorial, the `minimize` method from `ExecutionSession` will be employed. ```python theme={null} res = variational_minimize( qprog_bell, cost_function=negative_coeffs_projector_operator, initial_params={"angles": [0, 0]}, max_iteration=200, ) ``` **Output:** ``` Submitting job to simulator Job: https://platform.classiq.io/jobs/ccdb464b-a6b2-4c67-8638-074c21f82d36 ``` ```python theme={null} coefficients = res[-1][1] fidelity = -res[-1][0] print("Fidelity =", fidelity, "Coefficients: ", coefficients) ``` **Output:** ``` Fidelity = 1.0 Coefficients: {'angles': [0.025111834053147968, 1.5657679610396165]} ``` These values corresponds to the quantum circuit that generates this Bell State using RX, RY, and CX gates. ## Final Remarks In this tutorial, we built a simple parameterized quantum circuit, explored sampling it with specific parameter values, and visualized how output probabilities vary with those parameters. At the end, a simple Variational Quantum Algorithm is presented to prepare a Bell State. These techniques form the foundation for building and optimizing more complex quantum algorithms. # Synthesis Tutorial Source: https://docs.classiq.io/explore/tutorials/basic_tutorials/the_classiq_tutorial/synthesis_tutorial Open this notebook in GitHub to run it yourself Classiq's synthesis engine takes a high-level model written in the Qmod language, and compiles it into an executable gate-level circuit. When mapping high-level functionality to concrete circuits, there may be many different but equivalent possible implementations that reflect tradeoffs in the overall depth, width, gate counts, etc. For example, implementing a multi-controlled-not operation can be shallower in gates given more auxiliary qubits. Choosing the best implementation for a specific operation instance depends on the overall constraints and objectives, as well as the specific structure of the quantum program. Let's look at a simple model, and use Classiq's synthesis engine to compile it given different optimization objectives. ```python theme={null} from classiq import * @qfunc def main(x: Output[QNum[3]], y: Output[QNum]) -> None: allocate(x) hadamard_transform(x) y |= x**2 + 1 ``` First, let's synthesize to optimize on circuit depth, i.e. to minimize the longest path formed by gates in the circuit (and hence affects the required coherence time). ```python theme={null} qprog_opt_depth = synthesize( model=main, constraints=Constraints(optimization_parameter=OptimizationParameter.DEPTH), ) ``` We can inspect the resulting circuit using Classiq's web visualization: ```python theme={null} show(qprog_opt_depth) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/319KlN64C4oX3KLiMFXcQrhtRwr ``` On the left side menu, under 'Transpiled info', you should see the resulting depth, width and gate-count:
vis
The resulting depth and width are 171 and 16, respectively. This information can also be obtained using `data.width` and `transpiled_circuit.depth`: ```python theme={null} depth = qprog_opt_depth.transpiled_circuit.depth width = qprog_opt_depth.data.width print("Depth: ", depth, ". Width: ", width) ``` **Output:** ``` Depth: 171 . Width: 16 ``` Now, let's synthesize to optimize width, i.e to minimize the number of qubits used. ```python theme={null} qprog_opt_width = synthesize( model=main, constraints=Constraints(optimization_parameter=OptimizationParameter.WIDTH), ) ``` Inspect the resulting circuit: ```python theme={null} show(qprog_opt_width) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/319Km1ZWgqCcVR903Cn4Cd05egy ``` ```python theme={null} print( "Width =", qprog_opt_width.data.width, ", Depth =", qprog_opt_width.transpiled_circuit.depth, ) ``` **Output:** ``` Width = 9 , Depth = 199 ``` The new depth and width are 199 and 9, respectively. As expected, we "pay" with extra depth for an implementation that uses less qubits. ## Visualization When opening a quantum program using the `show` command, the visualization provides details regarding the quantum circuit's structure. Key characteristics, such as gate count, qubit usage, and circuit depth, are displayed. The visualization is organized according to the building blocks used during the quantum program construction. These blocks represent modular components or routines in your quantum algorithm. By clicking on the "+" sign on a block, you can expand it to reveal the underlying quantum gates, making it easier to inspect, debug, or understand the algorithm's logic at both high and low levels. This can be seen in the following figure: the left panel represents a high-level view showing the quantum blocks used in the algorithm - such as `hadamard_transform` and `assign x**2 + 1` - while the right panel shows an expanded view of the `assign` block, revealing the underlying quantum gate sequence. Gates include multiple `PHASE` operations and a quantum Fourier transform block (`qft6`), offering insight into the inner workings of this computation. ![Quantum Program Visualization](https://docs.classiq.io/resources/synthesis_tutorial.png) # ## Exporting and Sharing In addition to visualizing a quantum program, the tool provides convenient options to export the quantum circuit in multiple formats, enabling integration with other tools and workflows. Some of them are: * **QASM**: Allows interoperability with quantum simulators and hardware platforms. * **LaTeX**: Produces a high-quality, pictorial representation of the circuit, ready for inclusion in LaTeX files. * **JPEG**: Generates a graphical image of the circuit. To facilitate collaboration, there is a **Share** button that generates a unique link that you can send to anyone who wants to view your circuit directly in their browser - no login required. ## Exercise Create your own `main` function and synthesize it with different constraints. Note that `OptimizationParameter` is only one kind of configuration possible. Classiq synthesis engine also supports rigid constraints of `max_width`, `max_depth` and `max_gate_count`. # Overview Source: https://docs.classiq.io/explore/tutorials/index Tutorials to help you get started with Classiq's Qmod, grouped by level and topic. ## Basic Tutorials * [Quantum Entanglement with Classiq](/explore/tutorials/basic_tutorials/entanglement/entanglement) * [Exponentiation and Hamiltonian Simulation](/explore/tutorials/basic_tutorials/exponentiation/example_exponentiation) * [Grover Algorithm for Graph Coloring Problem](/explore/tutorials/basic_tutorials/grover_graph_coloring/grover_graph_coloring) * [Optimizing MCX Gates, Preparing for Future Hardware Today](/explore/tutorials/basic_tutorials/mcx/mcx) * [Learning Optimization](/explore/tutorials/basic_tutorials/optimization/learning_optimization) * [Walk-through: `prepare_state`](/explore/tutorials/basic_tutorials/prepare_state/prepare_state) * [Quantum Machine Learning with Classiq](/explore/tutorials/basic_tutorials/qml_with_classiq_guide/qml_with_classiq_guide) * [Linear Combination of Unitaries (LCU)](/explore/tutorials/basic_tutorials/quantum_primitives/linear_combination_of_unitaries/linear_combination_of_unitaries) * [Quantum walk on complex network](/explore/tutorials/basic_tutorials/quantumwalk_complex_network/quantumwalk_complex_network) * [Quantum Monte Carlo Integration to Estimate Pi Using Quantum Amplitude Estimation](/explore/tutorials/basic_tutorials/qmci_pi_estimation/qmci_pi_estimation) * [Classiq Overview Tutorial](/explore/tutorials/basic_tutorials/the_classiq_tutorial/classiq_overview_tutorial) * [Qmod Tutorial - Part 1](/explore/tutorials/basic_tutorials/the_classiq_tutorial/Qmod_tutorial_part1) * [Qmod Tutorial - Part 2](/explore/tutorials/basic_tutorials/the_classiq_tutorial/Qmod_tutorial_part2) * [Synthesis Tutorial](/explore/tutorials/basic_tutorials/the_classiq_tutorial/synthesis_tutorial) * [Execution Tutorial - Part 1](/explore/tutorials/basic_tutorials/the_classiq_tutorial/execution_tutorial) * [Execution Tutorial - Part 2](/explore/tutorials/basic_tutorials/the_classiq_tutorial/execution_tutorial_part2) ## Advanced Tutorials * [Discrete Quantum Walks](/explore/tutorials/advanced_tutorials/discrete_quantum_walk/discrete_quantum_walk) * [Quantum walks on one and two dimentional lattice](/explore/tutorials/advanced_tutorials/discrete_quantum_walk_2d/discrete_time_quantum_walk) * [Designing Quantum Algorithms with Second Order Functions: A Flexible QPE](/explore/tutorials/advanced_tutorials/high_level_modeling_flexible_qpe/high_level_modeling_flexible_qpe) ## Workshops * [High-level Algorithm Design with Qmod Part I](/explore/tutorials/workshops/algo_design_QCE_tutorial/algo_design_QCE_tutorial_part_I) * [High-level Algorithm Design with Qmod Part II](/explore/tutorials/workshops/algo_design_QCE_tutorial/algo_design_QCE_tutorial_part_II) * [Combinatorial Optimization Workshop using the Qmod quantum types - part 1](/explore/tutorials/workshops/combinatorial_workshop/combinatorial_qmod_workshop_for_maxcut) * [Estimating European Option Price Using Amplitude Estimation - Workshop](/explore/tutorials/workshops/finance_workshops/Option_Pricing_Workshop) * [Quantum Optimization Training - part 3](/explore/tutorials/workshops/finance_workshops/combi_workshop_Inequality_constriants_PO) * [Quantum Optimization Training - part 2](/explore/tutorials/workshops/finance_workshops/combi_workshop_equality_constriants_PO) * [Rainbow options workshop with the bruteforce methodology](/explore/tutorials/workshops/finance_workshops/rainbow_options_workshop_bruteforce) * [Grover from functional building blocks](/explore/tutorials/workshops/grover_workshop/grover_workshop) * [Modeling an HHL Algorithm to Solve a Set of Linear Equations](/explore/tutorials/workshops/hhl_workshop/hhl_workshop) * [Quantum Oracles Workshop](/explore/tutorials/workshops/oracle_workshop/oracles_workshop) ## Technology Demonstrations * [Approximated State Preparation](/explore/tutorials/technology_demonstrations/approximated_state_preparation/approximated_state_preparation) * [Arithmetic Expressions](/explore/tutorials/technology_demonstrations/arithmetic_expressions/arithmetic_expressions) * [Auxiliary Reuse and Management](/explore/tutorials/technology_demonstrations/auxiliary_managment/auxiliary_management) * [Discrete Quantum Walk on a Circle](/explore/tutorials/technology_demonstrations/discrete_quantum_walk_circle/discrete_quantum_walk_circle) * [Hamiltonian Evolution for a Water Molecule](/explore/tutorials/technology_demonstrations/hamiltonian_evolution/hamiltonian_evolution) * [HW-aware Synthesis of MCX](/explore/tutorials/technology_demonstrations/hardware_aware_mcx/hardware_aware_mcx) * [HHL for Solving $A\vec{x}=\vec{b}$](/explore/tutorials/technology_demonstrations/hhl/hhl_example) * [Oracle generation for 3-SAT problems](/explore/tutorials/technology_demonstrations/oracle_generation/3sat_oracles) * [QAOA](/explore/tutorials/technology_demonstrations/qaoa/qaoa_demonstration) * [Quantum Phase Estimation on a Grover Operator](/explore/tutorials/technology_demonstrations/QPE/qpe_for_grover_operator/qpe_for_grover_operator) * [Quantum Phase Estimation for a Matrix](/explore/tutorials/technology_demonstrations/QPE/qpe_for_unitary_matrix/qpe_for_unitary_matrix) * [Classiq code for QSVT example](/explore/tutorials/technology_demonstrations/classiq_paper/qsvt/classiq_qsvt) * [PennyLane code for QSVT example](/explore/tutorials/technology_demonstrations/classiq_paper/qsvt/pennylane_cat_qsvt_example) * [Qiskit code for QSVT example](/explore/tutorials/technology_demonstrations/classiq_paper/qsvt/qiskit_qsvt) * [PyTket code for QSVT example](/explore/tutorials/technology_demonstrations/classiq_paper/qsvt/tket_qsvt_example) * [Classiq code for discrete quantum walk](/explore/tutorials/technology_demonstrations/classiq_paper/quantum_walk/classiq_discrete_quantum_walk) * [PennyLane code for discrete quantum walk](/explore/tutorials/technology_demonstrations/classiq_paper/quantum_walk/pennylane_catalyst_discrete_quantum_walk) * [Qiskit code for discrete quantum walk](/explore/tutorials/technology_demonstrations/classiq_paper/quantum_walk/qiskit_discrete_quantum_walk) * [PyTket code for discrete quantum walk](/explore/tutorials/technology_demonstrations/classiq_paper/quantum_walk/tket_discrete_quantum_walk) ## Function Usage Examples * [Arithmetic Expressions](/explore/functions/function_usage_examples/arithmetic/arithmetic_expression/arithmetic_expression_example) * [Bitwise And](/explore/functions/function_usage_examples/arithmetic/bitwise_and/bitwise_and_example) * [Bitwise Invert](/explore/functions/function_usage_examples/arithmetic/bitwise_invert/bitwise_invert_example) * [Bitwise Or](/explore/functions/function_usage_examples/arithmetic/bitwise_or/bitwise_or_example) * [Bitwise Xor](/explore/functions/function_usage_examples/arithmetic/bitwise_xor/bitwise_xor_example) * [Comparators](/explore/functions/function_usage_examples/arithmetic/comparator/comparator_example) * [Minimum and Maximum](/explore/functions/function_usage_examples/arithmetic/extremum/extremum_example) * [Modular Exponentiation](/explore/functions/function_usage_examples/arithmetic/modular_exp/modular_exp_example) * [Modulo](/explore/functions/function_usage_examples/arithmetic/modulo/modulo_example) * [Multiplication](/explore/functions/function_usage_examples/arithmetic/multiplication/multiplication) * [Negation](/explore/functions/function_usage_examples/arithmetic/negation/negation_example) * [Subtraction](/explore/functions/function_usage_examples/arithmetic/subtraction/subtraction_example) * [Multi-Control-X](/explore/functions/function_usage_examples/mcx/mcx_example) # Quantum Phase Estimation on a Grover Operator Source: https://docs.classiq.io/explore/tutorials/technology_demonstrations/QPE/qpe_for_grover_operator/qpe_for_grover_operator Open this notebook in GitHub to run it yourself This notebook demonstrates the capability of Classiq's QPE function, focusing on a case where it is applied on a Grover Operator. This is the core of the Amplitude Estimation algorithm. The demonstration done for a toy model, where the Grover operator is constructed for a "good state" on 5 qubits $|\psi\rangle_{\rm good}\propto (|2\rangle+|3\rangle+|5\rangle+|7\rangle+|11\rangle+|13\rangle$). ## 1. Building a Model with Classiq ```python theme={null} from classiq import ( CArray, CInt, H, Output, QArray, QBit, QNum, U, X, Z, allocate, control, create_model, drop, hadamard_transform, invert, qfunc, qpe, repeat, synthesize, within_apply, ) ``` ```python theme={null} import numpy as np NUM_QUBITS = 5 ints_to_flip = [2, 3, 5, 7, 11, 13] precisions = [l for l in range(1, 6)] print("precisions:", precisions) ``` **Output:** ``` precisions: [1, 2, 3, 4, 5] ``` ```python theme={null} transpilation_options = {"classiq": "custom", "qiskit": 3} ``` # ## 1.1 Constructing the Relevant Quantum Functions ```python theme={null} # qfunc for specific state preparation @qfunc def my_state_preparation(states: CArray[CInt], x: QNum, ind: QBit): hadamard_transform(x) repeat(states.len, lambda i: control(x == states[i], lambda: X(ind))) ``` ```python theme={null} # qfunc for grover import numpy as np @qfunc def my_grover(x: QNum, ind: QBit): Z(ind) # oracle for the good state within_apply( lambda: invert(lambda: my_state_preparation(ints_to_flip, x, ind)), lambda: within_apply( lambda: (X(ind), H(ind)), lambda: control(x == 0, lambda: X(ind)) ), # zero oracle 1-|0><0| ) U(0, 0, 0, np.pi, ind) ``` ```python theme={null} from classiq import CustomHardwareSettings, Preferences preferences = Preferences( custom_hardware_settings=CustomHardwareSettings(basis_gates=["cx", "u"]), transpilation_option=transpilation_options["classiq"], timeout_seconds=1000, ) ``` # ## 1.2 Synthesizing with Two Different Optimization Scenarios ```python theme={null} from classiq import ( Constraints, OptimizationParameter, QuantumProgram, set_constraints, set_preferences, synthesize, ) ``` ```python theme={null} classiq_depths_ae_opt_width = [] classiq_cx_counts_ae_opt_width = [] classiq_widths_ae_opt_width = [] classiq_cx_counts_ae_opt_depth_max_width = [] classiq_depths_ae_opt_depth_max_width = [] classiq_widths_ae_opt_depth_max_width = [] qmods = [] qmods_width = [] qmods_cx = [] qmods_cx_max_width = [] qprogs_width = [] qprogs_cx = [] for precision in precisions: @qfunc def main(phase: Output[QNum]): ind = QBit("ind") allocate(1, ind) x = QNum("x") allocate(NUM_QUBITS - 1, False, 0, x) allocate(precision, False, precision, phase) qpe(lambda: my_grover(x, ind), phase) drop(x) drop(ind) qmod = create_model(main) qmod = set_preferences(qmod, preferences=preferences) qmods.append(qmod) qprog = synthesize(qmod) # width optimization qmod_width = set_constraints( qmod, optimization_parameter=OptimizationParameter.WIDTH ) qmods_width.append(qmod_width) qprog_width = synthesize(qmod_width) classiq_widths_ae_opt_width.append(qprog_width.data.width) classiq_depths_ae_opt_width.append(qprog_width.transpiled_circuit.depth) classiq_cx_counts_ae_opt_width.append( qprog_width.transpiled_circuit.count_ops["cx"] ) qprogs_width.append(qprog_width) # Depth Optimization with a Constrained Width qmods_cx_max_width.append(15 + 3 * (precision - 1)) qmod_cx = set_constraints( qmod, optimization_parameter=OptimizationParameter.DEPTH, max_width=qmods_cx_max_width[-1], # setting some bound ) qmods_cx.append(qmod_cx) qprog_cx = synthesize(qmod_cx) qprogs_cx.append(qprog_cx) classiq_widths_ae_opt_depth_max_width.append(qprog_cx.data.width) classiq_depths_ae_opt_depth_max_width.append(qprog_cx.transpiled_circuit.depth) classiq_cx_counts_ae_opt_depth_max_width.append( qprog_cx.transpiled_circuit.count_ops["cx"] ) print("classiq depths:", classiq_depths_ae_opt_width) print("classiq cx_counts:", classiq_cx_counts_ae_opt_width) print("classiq widths:", classiq_widths_ae_opt_width) print("classiq depths:", classiq_depths_ae_opt_depth_max_width) print("classiq cx_counts:", classiq_cx_counts_ae_opt_depth_max_width) print("classiq widths:", classiq_widths_ae_opt_depth_max_width) ``` **Output:** ``` classiq depths: [866, 2596, 6056, 12976, 26816] classiq cx_counts: [513, 1544, 3600, 7713, 15929] classiq widths: [6, 7, 8, 9, 10] classiq depths: [426, 1272, 2926, 6196, 13015] classiq cx_counts: [241, 722, 1664, 3523, 7384] classiq widths: [15, 18, 21, 24, 27] ``` ## 2. Comparing to Qiskit Implementation The qiskit data was generated using qiskit version 1. 0. 1. To run the qiskit code uncomment the commented cells below. ```python theme={null} qiskit_depths_ae = [3094, 9262, 21596, 46254, 95560] qiskit_cx_counts_ae = [1712, 5130, 11958, 25604, 52884] qiskit_widths_ae = [6, 7, 8, 9, 10] ``` ```python theme={null} # from importlib.metadata import version # try: # import qiskit # if version('qiskit') != "1.0.0": # !pip uninstall qiskit -y # !pip install qiskit==1.0.0 # except ImportError: # !pip install qiskit==1.0.0 ``` ```python theme={null} # import numpy as np # from qiskit import QuantumCircuit, QuantumRegister, transpile # from qiskit.circuit.library import GroverOperator, PhaseEstimation, XGate # # building the state preparation circuit, the last qubit is the indicator of good/bad state # state_preparation = QuantumCircuit(NUM_QUBITS) # for q in range(NUM_QUBITS - 1): # state_preparation.h(q) # for states in states_to_flip: # mcx = XGate().control(num_ctrl_qubits=NUM_QUBITS - 1, ctrl_state=states) # state_preparation.append(mcx, [k for k in range(NUM_QUBITS)]) # circuit = QuantumCircuit(NUM_QUBITS) # circuit.compose(state_preparation, [k for k in range(NUM_QUBITS)], inplace=True) # # building oracle # oracle = QuantumCircuit(NUM_QUBITS) # oracle.z(4) # good state = last qubit is |1> # # building the grover operator # grover_op = GroverOperator(oracle, state_preparation=circuit, insert_barriers=True) # qiskit_depths_ae = [] # qiskit_cx_counts_ae = [] # qiskit_widths_ae = [] # for precision in precisions: # qpe_qc = PhaseEstimation(precision, circuit) # transpiled_cir = transpile( # qpe_qc, # basis_gates=["u", "cx"], # optimization_level=transpilation_options["qiskit"], # ) # qiskit_depths_ae.append(transpiled_cir.depth()) # qiskit_cx_counts_ae.append(transpiled_cir.count_ops()["cx"]) # qiskit_widths_ae.append(transpiled_cir.width()) ``` ## 3. Plotting the Data ```python theme={null} import matplotlib.pyplot as plt classiq_color = "#119DA4" classiq_color_1 = "#F43764" qiskit_color = "#bb8bff" plt.rcParams["font.family"] = "serif" plt.rc("savefig", dpi=300) plt.rcParams["axes.linewidth"] = 1 plt.rcParams["xtick.major.size"] = 5 plt.rcParams["xtick.minor.size"] = 5 plt.rcParams["ytick.major.size"] = 5 plt.rcParams["ytick.minor.size"] = 5 (qiskit1,) = plt.semilogy( precisions, qiskit_depths_ae, "-s", label="qiskit", markerfacecolor=qiskit_color, markeredgecolor="k", markersize=6, markeredgewidth=1.5, color=qiskit_color, ) (classiq1,) = plt.semilogy( precisions, classiq_depths_ae_opt_width, "-D", label="classiq width opt.", markerfacecolor=classiq_color, markeredgecolor="k", markersize=6.5, markeredgewidth=1.5, color=classiq_color, ) (classiq2,) = plt.semilogy( precisions, classiq_depths_ae_opt_depth_max_width, "-o", label="classiq depth opt.", markerfacecolor=classiq_color_1, markeredgecolor="k", markersize=7, markeredgewidth=1.5, linewidth=1.5, color=classiq_color_1, ) first_legend = plt.legend( handles=[qiskit1, classiq1, classiq2], fontsize=16, loc="upper left" ) plt.ylabel("Depth", fontsize=16) plt.xlabel("QPE-precision", fontsize=16) plt.yticks(fontsize=16) plt.xticks(fontsize=16) plt.axis(ymin=3e2, ymax=5e5) plt.xticks(precisions) for x, y, num_qubits in zip( precisions, classiq_depths_ae_opt_width, classiq_widths_ae_opt_width ): plt.text(x * 0.94, y * 1.2, str(num_qubits), fontsize=16, color=classiq_color) for x, y, num_qubits in zip( precisions, classiq_depths_ae_opt_depth_max_width, classiq_widths_ae_opt_depth_max_width, ): plt.text(x * 0.96, y * 1.25, str(num_qubits), fontsize=16, color=classiq_color_1) for x, y, num_qubits in zip(precisions, qiskit_depths_ae, qiskit_widths_ae): plt.text(x * 0.98, y * 1.2, str(num_qubits), fontsize=16, color=qiskit_color) plt.text(4.8, 5e2, "(b)", fontsize=16) ``` **Output:** ``` Text(4.8, 500.0, '(b)') ``` output # Quantum Phase Estimation for a Matrix Source: https://docs.classiq.io/explore/tutorials/technology_demonstrations/QPE/qpe_for_unitary_matrix/qpe_for_unitary_matrix Open this notebook in GitHub to run it yourself This notebook demonstrates the capability of Classiq's Synthesis engine to reduce depth and cx-counts when modeling a Quantum Phase Estimation (QPE) on a unitary that is hard-coded unitary matrix (of the form $e^{2\pi i A}$, with $A$ Hermitian). ```python theme={null} import numpy as np import scipy # taking a random example, rescaling and shifting the matrix to guarantee eigenvalues in [0,1) np.random.seed(1235) new_mat = np.random.rand(8, 8) new_mat = (new_mat + new_mat.T) / 2 w, v = np.linalg.eig(new_mat) w_max = np.max(np.abs(w)) mew_mat = (new_mat + w_max) / (2 * w_max) my_unitary = scipy.linalg.expm(1j * 2 * np.pi * new_mat) precisions = [l for l in range(1, 9)] print("precisions:", precisions) ``` **Output:** ``` precisions: [1, 2, 3, 4, 5, 6, 7, 8] ``` ```python theme={null} transpilation_options = {"classiq": "custom", "qiskit": 3} ``` ## 1. Classiq's QPE ```python theme={null} from classiq import * qmods = [] qprogs = [] classiq_depths = [] classiq_cx_counts = [] preferences = Preferences( custom_hardware_settings=CustomHardwareSettings(basis_gates=["cx", "u"]), transpilation_option=transpilation_options["classiq"], ) for precision in precisions: @qfunc def main(phase: Output[QNum]): state = QArray("state") allocate(3, state) allocate(precision, False, precision, phase) qpe( unitary=lambda: unitary(elements=my_unitary.tolist(), target=state), phase=phase, ) drop(state) qmod = create_model(main) qmod = set_preferences(qmod, preferences=preferences) qmods.append(qmod) qprog = synthesize(qmod) qprogs.append(qprog) depth_classiq = qprog.transpiled_circuit.depth classiq_depths.append(depth_classiq) classiq_cx_counts.append(qprog.transpiled_circuit.count_ops["cx"]) print("classiq depths:", classiq_depths) print("classiq cx-counts:", classiq_cx_counts) ``` **Output:** ``` classiq depths: [180, 364, 550, 736, 922, 1108, 1294, 1480] classiq cx-counts: [97, 196, 297, 400, 505, 612, 721, 832] ``` ## 2. Comparing to Qiskit Implementations The qiskit data was generated using qiskit version 1. 0. 1. To run the qiskit code uncomment the commented cells below. ```python theme={null} qiskit_depths = [320, 950, 2206, 4706, 9694, 19658, 39574, 79394] qiskit_cx_counts = [162, 484, 1124, 2398, 4938, 10008, 20136, 40378] ``` ```python theme={null} # from importlib.metadata import version # try: # import qiskit # if version('qiskit') != "1.0.0": # !pip uninstall qiskit -y # !pip install qiskit==1.0.0 # except ImportError: # !pip install qiskit==1.0.0 ``` ```python theme={null} # from qiskit import QuantumCircuit, QuantumRegister, transpile # from qiskit.circuit.library import PhaseEstimation # q = QuantumRegister(3, "q") # qc = QuantumCircuit(q) # qc.unitary(my_unitary.tolist(), q) # qiskit_depths = [] # qiskit_cx_counts = [] # for precision in precisions: # qpe_qc = PhaseEstimation(precision, qc) # transpiled_cir = transpile( # qpe_qc, # basis_gates=["u", "cx"], # optimization_level=transpilation_options["qiskit"], # ) # qiskit_depths.append(transpiled_cir.depth()) # qiskit_cx_counts.append(transpiled_cir.count_ops()["cx"]) # print("qiskit depths:", qiskit_depths) # print("qiskit cx-counts:", qiskit_cx_counts) ``` ## 3. Plotting the Data ```python theme={null} import matplotlib.pyplot as plt classiq_color = "#119DA4" qiskit_color = "#bb8bff" plt.rcParams["font.family"] = "serif" plt.rc("savefig", dpi=300) plt.rcParams["axes.linewidth"] = 1 plt.rcParams["xtick.major.size"] = 5 plt.rcParams["xtick.minor.size"] = 5 plt.rcParams["ytick.major.size"] = 5 plt.rcParams["ytick.minor.size"] = 5 plt.rcParams["axes.linewidth"] = 1 plt.rcParams["xtick.major.size"] = 5 plt.rcParams["xtick.minor.size"] = 5 plt.rcParams["ytick.major.size"] = 5 plt.rcParams["ytick.minor.size"] = 5 (classiq1,) = plt.semilogy( precisions, classiq_depths, "-o", label="classiq depth", markerfacecolor=classiq_color, markeredgecolor="k", markersize=8, markeredgewidth=1.5, linewidth=1.5, color=classiq_color, ) (classiq2,) = plt.semilogy( precisions, classiq_cx_counts, "-*", label="classiq cx-counts", markerfacecolor=classiq_color, markeredgecolor="k", markersize=12, markeredgewidth=1.5, linewidth=1.5, color=classiq_color, ) (qiskit1,) = plt.semilogy( precisions, qiskit_depths, "-s", label="qiskit depth", markerfacecolor=qiskit_color, markeredgecolor="k", markersize=7, markeredgewidth=1.5, linewidth=1.5, color=qiskit_color, ) (qiskit2,) = plt.semilogy( precisions, qiskit_cx_counts, "-v", label="qiskit cx-counts", markerfacecolor=qiskit_color, markeredgecolor="k", markersize=8, markeredgewidth=1.5, linewidth=1.5, color=qiskit_color, ) first_legend = plt.legend( handles=[qiskit1, qiskit2], fontsize=16, loc="lower left", bbox_to_anchor=(0.1, 0.75), ) ax = plt.gca().add_artist(first_legend) plt.legend(handles=[classiq1, classiq2], fontsize=16, loc="lower right") plt.ylabel("Depth, CX-counts", fontsize=16) plt.xlabel("QPE-precision", fontsize=16) plt.yticks(fontsize=16) plt.xticks(fontsize=16) plt.text(0.9, 0.65e5, "(a)", fontsize=16) ``` **Output:** ``` Text(0.9, 65000.0, '(a)') ``` output # Approximated State Preparation Source: https://docs.classiq.io/explore/tutorials/technology_demonstrations/approximated_state_preparation/approximated_state_preparation Open this notebook in GitHub to run it yourself This tutorial demonstrates an approximated quantum function: a state preparation. Depending on the given functional error, the synthesis engine automatically chooses implementation with fewer resources. The demonstration is on a random state vector, of size $2^6$. ```python theme={null} import numpy as np NUM_QUBITS = 8 np.random.seed(1) x = np.linspace(-1, 1, 2**NUM_QUBITS) # Structured polynomial trend trend = 0.6 - 0.4 * x + 0.8 * x**2 - 0.3 * x**3 # Small perturbation perturbation_strength = 0.05 perturbation = perturbation_strength * np.random.randn(2**NUM_QUBITS) amplitudes = trend + perturbation amplitudes = amplitudes - np.mean(amplitudes) amplitudes = (amplitudes / np.linalg.norm(amplitudes)).tolist() ``` ```python theme={null} bounds = np.linspace(0.0, 0.3, 10) print("The upper bounds:", bounds) ``` **Output:** ``` The upper bounds: [ 0. 0.03333333 0.06666667 0.1 0.13333333 0.16666667 0.2 0.23333333 0.26666667 0.3 ] ``` ```python theme={null} from classiq import * preferences = Preferences( custom_hardware_settings=CustomHardwareSettings(basis_gates=["cx", "u"]), random_seed=1235, optimization_timeout_seconds=100, transpilation_option="custom", ) depths = [] cx_counts = [] qprogs = [] for b in bounds: @qfunc def main(out: Output[QArray]) -> None: prepare_amplitudes(amplitudes=amplitudes, bound=b, out=out) qprog = synthesize(main, preferences=preferences) qprogs.append(qprog) depths.append(qprog.transpiled_circuit.depth) cx_counts.append(qprog.transpiled_circuit.count_ops["cx"]) ``` ```python theme={null} print("classiq depths:", depths) print("cx-counts depths:", cx_counts) ``` **Output:** ``` classiq depths: [493, 493, 493, 240, 54, 25, 12, 12, 12, 12] cx-counts depths: [254, 254, 254, 126, 30, 14, 6, 6, 6, 6] ``` ```python theme={null} import matplotlib.pyplot as plt classiq_color = "#D7F75B" plt.rcParams["font.family"] = "serif" plt.rc("savefig", dpi=300) plt.rcParams["axes.linewidth"] = 1 plt.rcParams["xtick.major.size"] = 5 plt.rcParams["xtick.minor.size"] = 5 plt.rcParams["ytick.major.size"] = 5 plt.rcParams["ytick.minor.size"] = 5 plt.plot( np.round(bounds, 2), depths, "o", label="classiq depth", markerfacecolor=classiq_color, markeredgecolor="k", markersize=8, markeredgewidth=1.5, ) plt.plot( np.round(bounds, 2), cx_counts, "*", label="classiq cx-counts", markerfacecolor=classiq_color, markeredgecolor="k", markersize=12, markeredgewidth=1.5, ) plt.legend(fontsize=16, loc="upper right") plt.ylabel("Depth, CX-count", fontsize=16) plt.xlabel("error bound ($L_2$ metric)", fontsize=16) plt.yticks(fontsize=16) plt.xticks(fontsize=16); ``` output # Arithmetic Expressions Source: https://docs.classiq.io/explore/tutorials/technology_demonstrations/arithmetic_expressions/arithmetic_expressions Open this notebook in GitHub to run it yourself This tutorial demonstrates automatic arithmetic operation management by the synthesis engine. It synthesizes a complex arithmetic expression, where uncomputation procedure, together with initialization and reuse of auxiliary qubits, are all automated. Given different **global** width or depth constraints results in different circuits. Define a quantum model that applies some quantum arithmetic operation on `QNum` variables. ```python theme={null} from classiq import * from classiq.qmod.symbolic import max @qfunc def main(x: Output[QNum], y: Output[QNum], z: Output[QNum]): allocate(2, x) allocate(1, y) hadamard_transform(x) hadamard_transform(y) z |= (2 * x + y + max(3 * y, 2)) > 4 ``` You can try different optimization scenarios, below we introduce two examples: 1. Optimizing over depth and constraining the maximal width to 10 qubits. 2. Optimizing over depth and constraining the maximal width to 12 qubits. Optimizing over depth and constraining the maximal width to 10 qubits ```python theme={null} NUM_QUBITS_1 = 10 qprog_1 = synthesize( main, constraints=Constraints(optimization_parameter="depth", max_width=NUM_QUBITS_1), ) show(qprog_1) sample(qprog_1) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3DcBR5bi1Kg3ZdQpUAakewAKcSh ``` **Output:** ``` Submitting job to simulator Job: https://platform.classiq.io/jobs/6c500169-a27b-4b80-863f-be1996c1c08c ``` | | x | y | z | counts | probability | bitstring | | - | - | - | - | ------ | ----------- | --------- | | 0 | 3 | 0 | 1 | 275 | 0.134277 | 1011 | | 1 | 1 | 1 | 1 | 274 | 0.133789 | 1101 | | 2 | 3 | 1 | 1 | 261 | 0.127441 | 1111 | | 3 | 0 | 1 | 0 | 255 | 0.124512 | 0100 | | 4 | 2 | 0 | 1 | 253 | 0.123535 | 1010 | | 5 | 2 | 1 | 1 | 252 | 0.123047 | 1110 | | 6 | 0 | 0 | 0 | 242 | 0.118164 | 0000 | | 7 | 1 | 0 | 0 | 236 | 0.115234 | 0001 | Change the quantum model constraint to treat the second scenario for optimizing over depth and constraining the maximal width to 12 qubits: ```python theme={null} NUM_QUBITS_2 = 12 qprog_2 = synthesize( main, constraints=Constraints(optimization_parameter="depth", max_width=NUM_QUBITS_2), ) show(qprog_2) sample(qprog_2) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3DcBe5FXGO6GThlRywJnBjDrEma ``` **Output:** ``` Submitting job to simulator Job: https://platform.classiq.io/jobs/7eafa5de-83a2-460a-ace2-e2e7332600a6 ``` | | x | y | z | counts | probability | bitstring | | - | - | - | - | ------ | ----------- | --------- | | 0 | 2 | 0 | 1 | 271 | 0.132324 | 1010 | | 1 | 0 | 1 | 0 | 266 | 0.129883 | 0100 | | 2 | 0 | 0 | 0 | 260 | 0.126953 | 0000 | | 3 | 3 | 1 | 1 | 258 | 0.125977 | 1111 | | 4 | 1 | 0 | 0 | 254 | 0.124023 | 0001 | | 5 | 1 | 1 | 1 | 254 | 0.124023 | 1101 | | 6 | 2 | 1 | 1 | 245 | 0.119629 | 1110 | | 7 | 3 | 0 | 1 | 240 | 0.117188 | 1011 | # Auxiliary Reuse and Management Source: https://docs.classiq.io/explore/tutorials/technology_demonstrations/auxiliary_managment/auxiliary_management Open this notebook in GitHub to run it yourself This tutorial demonstrates automatic auxiliary qubits management by the synthesis engine. It synthesizes a simple state preparation function, which comprises several multi-controlled rotations. These rotations can use auxiliary qubits to reduce depth. For a given **global** width constraint, the initialization and reuse of auxiliary qubits between different function blocks is automated. ```python theme={null} import numpy as np from classiq import * MAX_WIDTH = 8 NUM_QUBITS = 4 np.random.seed(12) amplitudes = 1 - 2 * np.random.rand(2**NUM_QUBITS) amplitudes = (amplitudes / np.linalg.norm(amplitudes)).tolist() @qfunc def main(out: Output[QArray]) -> None: prepare_amplitudes(amplitudes=amplitudes, bound=0.2, out=out) preferences = Preferences( custom_hardware_settings=CustomHardwareSettings(basis_gates=["cx", "u"]), random_seed=1235, optimization_timeout_seconds=100, ) constraints = Constraints( optimization_parameter=OptimizationParameter.DEPTH, max_width=MAX_WIDTH ) qprog = synthesize(main, preferences=preferences, constraints=constraints) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/30er6gl4kTKzjmcqlFQ8Qhs2bLA ``` # Classiq Code for QSVT Example Source: https://docs.classiq.io/explore/tutorials/technology_demonstrations/classiq_paper/qsvt/classiq_qsvt Open this notebook in GitHub to run it yourself This notebook shows how to generate for the QSVT example using `classiq`. ```python theme={null} import time import numpy as np from classiq import * from classiq.qmod.symbolic import floor ``` ```python theme={null} MAX_WIDTH = 11 SIZE = 6 DEGREE = 3 QSVT_PHASES = [ 1.280311896404252, 8.127145628464149, 1.8439603212845617, -5.002873410775335, ] constraints = Constraints(optimization_parameter="cx", max_width=MAX_WIDTH) ``` ## Quantum Functions ```python theme={null} @qfunc def be_projection(x: QArray, aux: QBit): within_apply( lambda: H(aux), lambda: control( aux == 0, lambda: reflect_about_zero(x), ), ) @qfunc def be_amat0(data: QArray, block: QArray): del_qubit = QBit() select = QBit() packed = QNum(size=data.size + 1) within_apply( lambda: ( bind(block, [select, del_qubit]), bind([data, del_qubit], packed), hadamard_transform(select), ), lambda: ( control(select, lambda: IDENTITY(packed), lambda: inplace_add(2, packed)), inplace_add(-1, packed), ), ) @qfunc def my_be(data: QArray, block: QArray): be_amat0(data, block[0:2]) be_projection(data, block[2]) @qfunc def my_projector_controlled_phase(phase: CReal, block: QNum, aux: QBit): control(block == 0, lambda: X(aux)) RZ(phase, aux) control(block == 0, lambda: X(aux)) @qfunc def my_qsvt_step( phase1: CReal, phase2: CReal, u: QCallable[QArray, QArray], data: QArray, block: QArray, qsvt_aux: QBit, ): u(data, block) my_projector_controlled_phase(phase1, block, qsvt_aux) invert(lambda: u(data, block)) my_projector_controlled_phase(phase2, block, qsvt_aux) @qfunc def my_qsvt(qsvt_phases: CArray[CReal], data: QArray, block: QArray, qsvt_aux: QBit): H(qsvt_aux) my_projector_controlled_phase(qsvt_phases[0], block, qsvt_aux) repeat( floor((qsvt_phases.len - 1) / 2), lambda i: my_qsvt_step( qsvt_phases[(2 * i) + 1], qsvt_phases[(2 * i) + 2], lambda d, b: my_be(d, b), data, block, qsvt_aux, ), ) my_be(data, block) my_projector_controlled_phase(qsvt_phases[qsvt_phases.len - 1], block, qsvt_aux) H(qsvt_aux) ``` ```python theme={null} from classiq import CustomHardwareSettings, Preferences preferences = Preferences( custom_hardware_settings=CustomHardwareSettings(basis_gates=["cx", "u"]), transpilation_option="custom", debug_mode=False, ) ``` ## Example for Getting a Data Point ```python theme={null} start_time = time.time() @qfunc def main(block: Output[QNum], data: Output[QNum], qsvt_aux: Output[QBit]): allocate(1, qsvt_aux) allocate(3, block) allocate(SIZE, data) my_qsvt(QSVT_PHASES, data, block, qsvt_aux) qprog = synthesize(main, constraints=constraints, preferences=preferences) compilation_time = time.time() - start_time width = qprog.data.width depth = qprog.transpiled_circuit.depth cx_counts = qprog.transpiled_circuit.count_ops["cx"] print(f"==== classiq for {SIZE}==== time {compilation_time}") ``` **Output:** ``` ==== classiq for 6==== time 33.173584938049316 ``` # PennyLane Code for QSVT Example Source: https://docs.classiq.io/explore/tutorials/technology_demonstrations/classiq_paper/qsvt/pennylane_cat_qsvt_example Open this notebook in GitHub to run it yourself This notebook shows how to generate data for QSVT example using `pennylane` 0.39.0 and `pennylane-catalyst` 0.9. 0. ```python theme={null} # import time # import pennylane as qml # from catalyst import qjit # from pennylane import numpy as np # def reflect_around_zero(data): # """Implements the reflection around zero operator.""" # def circuit(data): # qml.PauliX(wires=data[0]) # qml.Hadamard(wires=data[0]) # qml.ctrl(qml.PauliX, control=data[1:len(data)], control_values=[0]*(len(data)-1))(wires=data[0]) # qml.Hadamard(wires=data[0]) # qml.PauliX(wires=data[0]) # return circuit # def get_cir_be(data, block): # """Constructs the controlled block-encoding circuit.""" # def circuit(): # qml.Hadamard(wires=block[0]) # qml.Hadamard(wires=block[2]) # qml.ctrl(qml.Adder,control=block[0], control_values=1)(2, data+[block[1]], 2**(len(data)+1), work_wires=[]) # qml.Adder(2**(len(data)+1)-1, data+[block[1]], 2**(len(data)+1), work_wires=[]) # qml.ctrl(reflect_around_zero(data), control=block[2])(data) # qml.Hadamard(wires=block[0]) # qml.Hadamard(wires=block[2]) # return circuit # def apply_projector_controlled_phase(phase, block_reg, aux_reg): # def circuit(): # qml.ctrl(qml.PauliX, control=block_reg, control_values=[0]*len(block_reg))(wires=aux_reg) # qml.RZ(phase, wires=aux_reg) # qml.ctrl(qml.PauliX, control=block_reg, control_values=[0]*len(block_reg))(wires=aux_reg) # return circuit # def apply_qsvt_step(phase1, phase2, u, data, block, qsvt_aux): # def circuit(): # u() # apply_projector_controlled_phase(phase1, block, qsvt_aux)() # qml.adjoint(u)() # apply_projector_controlled_phase(phase2, block, qsvt_aux)() # return circuit # def get_qsvt_circuit(qsvt_phases, size): # dev = qml.device("lightning.qubit", wires=size + 3) # @qml.qnode(dev) # def qsvt_circuit(): # block = [size, size+1, size+2] # qsvt_aux = size + 3 # data = list(range(size)) # cir_be = get_cir_be(data, block) # qml.Hadamard(wires=qsvt_aux) # apply_projector_controlled_phase(qsvt_phases[0], block, qsvt_aux)() # for i in range((len(qsvt_phases) - 1) // 2): # apply_qsvt_step(qsvt_phases[2 * i + 1], qsvt_phases[2 * i + 2], cir_be , data, block, qsvt_aux)() # cir_be() # apply_projector_controlled_phase(qsvt_phases[-1], block, qsvt_aux)() # qml.Hadamard(wires=qsvt_aux) # return qsvt_circuit ``` ## Run an Example ```python theme={null} # SIZE = 6 # DEGREE = 3 # QSVT_PHASES = [1.280311896404252, 8.127145628464149, 1.8439603212845617, -5.002873410775335] # start_time = time.time() # qsvt_cir = get_qsvt_circuit(QSVT_PHASES, SIZE) # cir = qml.transforms.decompose(qsvt_cir, gate_set={qml.CNOT, qml.RZ, qml.RY, qml.RX}) # jitted_cir = qjit(cir) # transpilation_time = time.time()-start_time # cx_counts = jitted_cir.mlir.count("CNOT") # print(f"==== pennylane for {SIZE}==== time: {transpilation_time}") ``` # Qiskit Code for QSVT Example Source: https://docs.classiq.io/explore/tutorials/technology_demonstrations/classiq_paper/qsvt/qiskit_qsvt Open this notebook in GitHub to run it yourself This notebook shows how to generate data for the QSVT example using `qiskit` 1. 2. 3. Here we provide the codes for block encoding the matrix $A$, as well as the QSVT implementation. Qiskit does not have an adder by a constant function. Thus, we have modified their adder functions, which is applied between two quantum registers, to include this functionality. ```python theme={null} # import time # import numpy as np # from qiskit.circuit import QuantumCircuit, QuantumRegister # from qiskit.circuit.library import QFT # from qiskit import QuantumCircuit, QuantumRegister, transpile # from qiskit.circuit.library.standard_gates import XGate, RZGate # BASIS_GATES = ["u", "cx"] # OPT_LEVEL = 3 # class DraperQFTAdderConstant(QuantumCircuit): # def __init__(self, num_state_qubits: int, constant: int, name: str = "DraperQFTAdderConst") -> None: # # Create the quantum register # qr_a = QuantumRegister(num_state_qubits, name="a") # super().__init__(qr_a, name=name) # # Apply the QFT # self.append(QFT(num_state_qubits, do_swaps=False).to_gate(), qr_a) # # Add the constant by applying controlled rotations # for qubit in range(num_state_qubits): # angle = (constant % (2 ** (qubit + 1))) * np.pi / (2 ** qubit) # self.p(angle, qr_a[qubit]) # # Apply the inverse QFT # self.append(QFT(num_state_qubits, do_swaps=False).inverse().to_gate(), qr_a) ``` ```python theme={null} # def get_reflect_around_zero(size): # qc = QuantumCircuit(size) # qc.x(0) # qc.h(0) # qc.mcx(control_qubits=[k for k in range(1,size)],ctrl_state="0"*(size-1),target_qubit=[0]) # qc.h(0) # qc.x(0) # return qc # def get_cir_be(qc, data, block): # qc.h(block[0]) # qc.h(block[2]) # qc.append(DraperQFTAdderConstant(num_state_qubits=len(data)+1, # constant=2).control(1, ctrl_state=0).to_instruction(),[block[0]]+data[:]+[block[1]]) # qc.append(DraperQFTAdderConstant(num_state_qubits=len(data)+1, # constant=-1+2**(len(data)+1)).to_instruction(),data[:]+[block[1]]) # qc.append(get_reflect_around_zero(len(data)).control(1, ctrl_state=0).to_instruction(),[block[2]]+data[:]) # qc.h(block[0]) # qc.h(block[2]) # return qc # def apply_projector_controlled_phase(qc, phase, block_reg, aux_reg): # qc.append(XGate().control(len(block_reg),ctrl_state=0), # block_reg[:] + aux_reg[:] # ) # qc.rz(phase, aux_reg) # qc.append(XGate().control(len(block_reg),ctrl_state=0), # block_reg[:] + aux_reg[:] # ) # def apply_qsvt_step(qc, phase1, phase2, u, data, block, qsvt_aux): # qc.append(u, data[:] + block[:]) # apply_projector_controlled_phase(qc, phase1, block, qsvt_aux) # qc.append(u.inverse(), data[:] + block[:]) # apply_projector_controlled_phase(qc, phase2, block, qsvt_aux) # def get_qsvt_circuit(qsvt_phases, # size): # block = QuantumRegister(3, 'block') # data = QuantumRegister(size, 'data') # qsvt_aux = QuantumRegister(1, 'qsvt_aux') # cir_be = QuantumCircuit(data,block) # cir_be = get_cir_be(cir_be,data, block) # qsvt_cir = QuantumCircuit(data, block, qsvt_aux) # qsvt_cir.h(qsvt_aux) # apply_projector_controlled_phase(qsvt_cir, qsvt_phases[0], block, qsvt_aux) # for i in range(int(np.floor((len(qsvt_phases) - 1) / 2))): # apply_qsvt_step(qsvt_cir, # qsvt_phases[(2 * i) + 1], qsvt_phases[(2 * i) + 2], # cir_be, # data, # block, # qsvt_aux # ) # qsvt_cir.append(cir_be, data[:] + block[:]) # apply_projector_controlled_phase(qsvt_cir, qsvt_phases[len(qsvt_phases) - 1], block, qsvt_aux) # qsvt_cir.h(qsvt_aux) # return qsvt_cir ``` ## Run an Example ```python theme={null} # SIZE = 6 # DEGREE = 3 # QSVT_PHASES = [1.280311896404252, 8.127145628464149, 1.8439603212845617, -5.002873410775335] # start_time = time.time() # qc_qsvt = get_qsvt_circuit(QSVT_PHASES, SIZE) # transpiled_cir = transpile( # qc_qsvt, # basis_gates=BASIS_GATES, # optimization_level=OPT_LEVEL, # ) # transpilation_time = time.time()-start_time # depth = transpiled_cir.depth() # cx_counts = transpiled_cir.count_ops()["cx"] # width = transpiled_cir.width() # print(f"==== qiskit for {SIZE}==== time: {transpilation_time}") ``` # PyTket Code for QSVT Example Source: https://docs.classiq.io/explore/tutorials/technology_demonstrations/classiq_paper/qsvt/tket_qsvt_example Open this notebook in GitHub to run it yourself This notebook shows how to generate data for discrete quantum walk using `pytket` 1.34.0 Here we provide the codes for block encoding the matrix $A$, as well as the QSVT implementation. PyTket does not have an adder by a constant function. Thus, we have modified their adder functions, which is applied between two quantum registers, to include this functionality. ```python theme={null} # import time # from pytket.circuit import Circuit, CircBox, OpType, QControlBox # from pytket.extensions.qiskit import AerBackend # from pytket.passes import DecomposeBoxes, SynthesiseTket ``` ```python theme={null} # backend = AerBackend() # from pytket.circuit import Circuit, CircBox # import numpy as np # def build_qft_circuit(n_qubits: int, do_swaps: True) -> Circuit: # circ = Circuit(n_qubits, name="QFT") # for i in range(n_qubits): # circ.H(i) # for j in range(i + 1, n_qubits): # circ.CU1(1 / 2 ** (j - i), j, i) # if do_swaps: # for k in range(0, n_qubits // 2): # circ.SWAP(k, n_qubits - k - 1) # return circ # class DraperQFTAdderConstantPytket: # def __init__(self, num_state_qubits: int, constant: int) -> Circuit: # circuit = Circuit(num_state_qubits) # qft_circuit = build_qft_circuit(num_state_qubits, do_swaps=False) # circuit.add_gate(CircBox(qft_circuit),[k for k in range(num_state_qubits)]) # # Apply phase rotations for the constant # for qubit in range(num_state_qubits): # angle = (constant % (2 ** (qubit + 1))) * np.pi / (2 ** qubit) # circuit.Rz(angle, qubit) # # Apply inverse QFT # circuit.add_gate(CircBox(qft_circuit).dagger,[k for k in range(num_state_qubits)]) # self.circuit = circuit # def get_circuit(self) -> Circuit: # return self.circuit ``` ```python theme={null} # def get_reflect_around_zero(size): # qc = Circuit(size, name="reflection") # qc.X(0) # qc.H(0) # qc.add_gate(QControlBox(CircBox(Circuit(1).X(0)), n_controls = size-1, control_state=[0]*(size-1)), [k for k in range(size)]) # qc.H(0) # qc.X(0) # return qc # def get_cir_be(qc, data, block): # qc.H(block[0]) # qc.H(block[2]) # adder_2_cir = DraperQFTAdderConstantPytket(len(data)+1, 2).get_circuit() # adder_1_cir = DraperQFTAdderConstantPytket(len(data)+1, -1+2**(len(data)+1)).get_circuit() # qc.add_gate(QControlBox(CircBox(adder_2_cir), n_controls = 1, control_state=0), [block[0]]+[data[k] for k in range(len(data))]+[block[1]]) # qc.add_gate(CircBox(adder_1_cir), [data[k] for k in range(len(data))]+[block[1]]) # qc.add_gate(QControlBox(CircBox(get_reflect_around_zero(len(data))),n_controls=1, control_state=0),[block[2]]+[data[k] for k in range(len(data))]) # qc.H(block[0]) # qc.H(block[2]) # return qc # def apply_projector_controlled_phase(qc, phase, block_reg, aux_reg): # qc.add_gate(QControlBox(CircBox(Circuit(1).X(0)), n_controls = len(block_reg), control_state=[0]*len(block_reg)), # [block_reg[k] for k in range(len(block_reg))] + [aux_reg[0]] # ) # qc.Rz(phase, aux_reg[0]) # qc.add_gate(QControlBox(CircBox(Circuit(1).X(0)), n_controls = len(block_reg), control_state=[0]*len(block_reg)), # [block_reg[k] for k in range(len(block_reg))] + [aux_reg[0]] # ) # def apply_qsvt_step(qc, phase1, phase2, u, data, block, qsvt_aux): # qc.add_gate(CircBox(u), [data[l] for l in range(len(data))]+ [block[l] for l in range(len(block))]) # apply_projector_controlled_phase(qc, phase1, block, qsvt_aux) # qc.add_gate(CircBox(u).dagger, [data[l] for l in range(len(data))]+ [block[l] for l in range(len(block))]) # apply_projector_controlled_phase(qc, phase2, block, qsvt_aux) # def get_qsvt_circuit(qsvt_phases, # size): # qsvt_cir = Circuit() # data = qsvt_cir.add_q_register("data", size) # block = qsvt_cir.add_q_register("block", 3) # cir_be = Circuit() # data = cir_be.add_q_register("data", size) # block = cir_be.add_q_register("block", 3) # qsvt_aux = qsvt_cir.add_q_register("qsvt_aux", 1) # cir_be = get_cir_be(cir_be,data, block) # qsvt_cir.H(qsvt_aux[0]) # apply_projector_controlled_phase(qsvt_cir, qsvt_phases[0], block, qsvt_aux) # for i in range(int(np.floor((len(qsvt_phases) - 1) / 2))): # apply_qsvt_step(qsvt_cir, # qsvt_phases[(2 * i) + 1], qsvt_phases[(2 * i) + 2], # cir_be, # data, # block, # qsvt_aux # ) # qsvt_cir.add_gate(CircBox(cir_be), [data[l] for l in range(len(data))]+ [block[l] for l in range(len(block))]) # apply_projector_controlled_phase(qsvt_cir, qsvt_phases[len(qsvt_phases) - 1], block, qsvt_aux) # qsvt_cir.H(qsvt_aux[0]) # return qsvt_cir ``` ## Run an Example ```python theme={null} # SIZE = 6 # DEGREE = 3 # QSVT_PHASES = [1.280311896404252, 8.127145628464149, 1.8439603212845617, -5.002873410775335] # start_time = time.time() # qc_qsvt = get_qsvt_circuit(QSVT_PHASES, SIZE) # DecomposeBoxes().apply(qc_qsvt) # SynthesiseTket().apply(qc_qsvt) # compiled_circ = backend.get_compiled_circuit(qc_qsvt) # transpilation_time = time.time()-start_time # depth = compiled_circ.depth() # cx_counts = compiled_circ.n_gates_of_type(OpType.CX) # width = compiled_circ.n_qubits # print(f"==== tket for {SIZE}==== time: {transpilation_time}") ``` # Classiq Code for Discrete Quantum Walk Source: https://docs.classiq.io/explore/tutorials/technology_demonstrations/classiq_paper/quantum_walk/classiq_discrete_quantum_walk Open this notebook in GitHub to run it yourself This notebook shows how to generate data for discrete quantum walk using `classiq`. ```python theme={null} import time from classiq import * SIZE = 6 MAX_WIDTH = 2 * SIZE constraints = Constraints(optimization_parameter="cx", max_width=MAX_WIDTH) ``` ```python theme={null} # define increment circuit as an MCX cascade @qfunc def increment(x: QArray): repeat( x.len - 1, lambda i: control(x[0 : x.len - 1 - i], lambda: X(x[x.len - 1 - i])) ) X(x[0]) @qfunc def single_step_walk( coin: QBit, # coin x: QNum, # position ): H(coin) control(coin == 0, lambda: increment(x), lambda: invert(lambda: increment(x))), ``` ```python theme={null} from classiq import CustomHardwareSettings, Preferences preferences = Preferences( custom_hardware_settings=CustomHardwareSettings(basis_gates=["cx", "u"]), transpilation_option="custom", debug_mode=False, ) ``` ## Example for Getting a Data Point ```python theme={null} start_time = time.time() @qfunc def main(x: Output[QNum[SIZE, UNSIGNED, 0]], coin: Output[QBit]): allocate(x) allocate(coin) single_step_walk(coin, x) qprog = synthesize(main, constraints=constraints, preferences=preferences) compilation_time = time.time() - start_time width = qprog.data.width depth = qprog.transpiled_circuit.depth cx_counts = qprog.transpiled_circuit.count_ops["cx"] print(f"==== classiq for {SIZE}==== time {compilation_time}") ``` **Output:** ``` ==== classiq for 6==== time 19.030019283294678 ``` # PennyLane Code for Discrete Quantum Walk Source: https://docs.classiq.io/explore/tutorials/technology_demonstrations/classiq_paper/quantum_walk/pennylane_catalyst_discrete_quantum_walk Open this notebook in GitHub to run it yourself This notebook shows how to generate data for discrete quantum walk using `pennylane` 0.39.0 and `pennylane-catalyst` 0.9.0 ```python theme={null} # import time # import pennylane as qml # from catalyst import qjit ``` ```python theme={null} # import time # import pennylane as qml # from catalyst import qjit # # run an example # SIZE = 6 # start_time = time.time() # s = SIZE # wires_x = list(range(s)) # coin = [s] # def shift_op(): # for index in range(len(wires_x)-1, 0, -1): # control_values = [1] * index # qml.ctrl(qml.PauliX, control=wires_x[:index], control_values=control_values)(wires=wires_x[index]) # qml.PauliX(wires_x[0]) # @qml.qnode(qml.device("lightning.qubit", wires=(wires_x + coin))) # def circuit(): # qml.H(coin) # qml.ctrl(shift_op, control=coin)() # qml.ctrl(qml.adjoint(shift_op), control=coin, control_values=[0])() # return qml.probs(wires=wires_x) # cir = qml.transforms.decompose(circuit, gate_set={qml.CNOT, qml.RZ, qml.RY, qml.RX}) # jitted_cir = qjit(cir) # transpilation_time = time.time()-start_time # cx_counts = jitted_cir.mlir.count("CNOT") # print(f"==== pennylane for {SIZE}==== time: {transpilation_time}") ``` # Qiskit Code for Discrete Quantum Walk Source: https://docs.classiq.io/explore/tutorials/technology_demonstrations/classiq_paper/quantum_walk/qiskit_discrete_quantum_walk Open this notebook in GitHub to run it yourself This notebook shows how to generate data for discrete quantum walk using `qiskit` 1.2. 4. ```python theme={null} # import time # from qiskit import QuantumCircuit, QuantumRegister, transpile # from qiskit.circuit.library.standard_gates import XGate # BASIS_GATES = ["u", "cx"] # OPT_LEVEL = 3 # SIZE = 6 # # mcx from control on X # def mcx_gate(num_ctrl_qubits): # my_mcx_gate = XGate().control(num_ctrl_qubits) # return my_mcx_gate # # get increment circuit as an MCX cascade # def get_increment_circuit(num_qubits): # increment_circuit= QuantumCircuit(num_qubits) # for j in range(num_qubits - 1): # increment_circuit.append(mcx_gate(num_qubits-1-j),[k for k in range(num_qubits-j)]) # increment_circuit.x(0) # return increment_circuit # # run an example # start_time = time.time() # q_walk_step = QuantumCircuit(SIZE+1) # q_walk_step.h(0) # q_walk_step.append(get_increment_circuit(SIZE).control(1, ctrl_state=1), # [k for k in range(SIZE+1)]) # q_walk_step.append(get_increment_circuit(SIZE).inverse().control(1, ctrl_state=0), # [k for k in range(SIZE+1)]) # transpiled_cir = transpile( # q_walk_step, # basis_gates=BASIS_GATES, # optimization_level=OPT_LEVEL, # ) # transpilation_time = time.time()-start_time # depth = transpiled_cir.depth() # cx_counts = transpiled_cir.count_ops()["cx"] # width = transpiled_cir.width() # print(f"==== qiskit for {SIZE}==== time: {transpilation_time}") ``` # PyTket Code for Discrete Quantum Walk Source: https://docs.classiq.io/explore/tutorials/technology_demonstrations/classiq_paper/quantum_walk/tket_discrete_quantum_walk Open this notebook in GitHub to run it yourself This notebook shows how to generate data for discrete quantum walk using `pytket` 1.34.0 ```python theme={null} # import time # from pytket.circuit import Circuit, CircBox, OpType, QControlBox # from pytket.extensions.qiskit import AerBackend # from pytket.passes import DecomposeBoxes, SynthesiseTket ``` ```python theme={null} # SIZE = 6 # backend = AerBackend() # # mcx from control on X # def mcx_gate(num_ctrl_qubits): # my_mcx_gate = QControlBox(CircBox(Circuit(1).X(0)), num_ctrl_qubits) # return my_mcx_gate # # get increment circuit as an MCX cascade # def get_increment_circuit(num_qubits): # increment_circuit = Circuit(num_qubits) # for j in range(num_qubits - 1): # increment_circuit.add_gate(mcx_gate(num_qubits-1-j),[k for k in range(num_qubits-j)]) # increment_circuit.X(0) # return increment_circuit # # run an example # start_time = time.time() # q_walk_step = Circuit(SIZE+1) # q_walk_step.H(0) # q_walk_step.add_gate(QControlBox(CircBox(get_increment_circuit(SIZE))), # [k for k in range(SIZE+1)]) # q_walk_step.add_gate(QControlBox(CircBox(get_increment_circuit(SIZE)).dagger,n_controls=1, # control_state=[0]),[k for k in range(SIZE+1)]) # DecomposeBoxes().apply(q_walk_step) # SynthesiseTket().apply(q_walk_step) # compiled_circ = backend.get_compiled_circuit(q_walk_step) # transpilation_time = time.time()-start_time # depth = compiled_circ.depth() # cx_counts = compiled_circ.n_gates_of_type(OpType.CX) # width = compiled_circ.n_qubits # print(f'==== tket for {SIZE}==== time {transpilation_time}') ``` # Discrete Quantum Walk on a Circle Source: https://docs.classiq.io/explore/tutorials/technology_demonstrations/discrete_quantum_walk_circle/discrete_quantum_walk_circle Open this notebook in GitHub to run it yourself This notebook demonstrates the capabilities of the synthesis engine for a walk operator on a circle. The walk operator acts on two quantum variables: a coin qubit and a position quantum number. The core part of the walk operator is the increment quantum function, which is implemented here via a series of multi-controlled X operation. ```python theme={null} import time import numpy as np from classiq import * ``` ```python theme={null} transpilation_options = {"classiq": "custom", "qiskit": 3} NUM_QUBITS_MIN = 5 NUM_QUBITS_MAX = 12 ``` ```python theme={null} # define increment circuit as an MCX cascade @qfunc def increment(x: QArray): repeat( x.len - 1, lambda i: control(x[0 : x.len - 1 - i], lambda: X(x[x.len - 1 - i])) ) X(x[0]) @qfunc def single_step_walk( coin: QBit, # coin x: QNum, # position ): H(coin) control(coin == 0, lambda: increment(x)), control(coin == 1, lambda: invert(lambda: increment(x))) ``` ```python theme={null} preferences = Preferences( custom_hardware_settings=CustomHardwareSettings(basis_gates=["cx", "u"]), transpilation_option=transpilation_options["classiq"], ) ``` ## Synthesizing with Two Different Optimization Scenarios ```python theme={null} classiq_depths_opt_width = [] classiq_cx_counts_opt_width = [] classiq_widths_opt_width = [] classiq_times_opt_width = [] classiq_cx_counts_opt_cx = [] classiq_depths_opt_cx = [] classiq_widths_opt_cx = [] classiq_times_opt_cx = [] qprogs_width = [] qprogs_cx = [] for num_qubits in range(NUM_QUBITS_MIN, NUM_QUBITS_MAX): print(num_qubits, "======") @qfunc def main(x: Output[QNum[num_qubits]], coin: Output[QBit]): allocate(x) allocate(coin) single_step_walk(coin, x) # width optimization constraints = Constraints(optimization_parameter=OptimizationParameter.WIDTH) start_time = time.time() qprog = synthesize(main, preferences=preferences, constraints=constraints) end_time = time.time() - start_time qprogs_width.append(qprog) classiq_widths_opt_width.append(qprog.data.width) classiq_depths_opt_width.append(qprog.transpiled_circuit.depth) classiq_cx_counts_opt_width.append(qprog.transpiled_circuit.count_ops["cx"]) classiq_times_opt_width.append(end_time) print("time (width):", end_time) # CX Optimization with a Constrained Width constraints = Constraints( optimization_parameter="cx", max_width=2 * num_qubits, # setting some bound ) start_time = time.time() qprog = synthesize(main, preferences=preferences, constraints=constraints) end_time = time.time() - start_time qprogs_cx.append(qprog) classiq_widths_opt_cx.append(qprog.data.width) classiq_depths_opt_cx.append(qprog.transpiled_circuit.depth) classiq_cx_counts_opt_cx.append(qprog.transpiled_circuit.count_ops["cx"]) classiq_times_opt_cx.append(end_time) print("time (cx) :", end_time) ``` **Output:** ``` 5 ====== ``` **Output:** ``` /var/folders/kk/nlz2dw2921g33r494qcnf4jh0000gn/T/ipykernel_84535/1498934103.py:32: ClassiqDeprecationWarning: Accessing 'quantum_program.transpiled_circuit.depth' is deprecated. Use 'classiq.get_transpiled_circuit_metrics(quantum_program)' instead. classiq_depths_opt_width.append(qprog.transpiled_circuit.depth) /var/folders/kk/nlz2dw2921g33r494qcnf4jh0000gn/T/ipykernel_84535/1498934103.py:33: ClassiqDeprecationWarning: Accessing 'quantum_program.transpiled_circuit.count_ops' is deprecated. Use 'classiq.get_transpiled_circuit_metrics(quantum_program)' instead. classiq_cx_counts_opt_width.append(qprog.transpiled_circuit.count_ops["cx"]) ``` **Output:** ``` time (width): 6.4162139892578125 ``` **Output:** ``` /var/folders/kk/nlz2dw2921g33r494qcnf4jh0000gn/T/ipykernel_84535/1498934103.py:48: ClassiqDeprecationWarning: Accessing 'quantum_program.transpiled_circuit.depth' is deprecated. Use 'classiq.get_transpiled_circuit_metrics(quantum_program)' instead. classiq_depths_opt_cx.append(qprog.transpiled_circuit.depth) /var/folders/kk/nlz2dw2921g33r494qcnf4jh0000gn/T/ipykernel_84535/1498934103.py:49: ClassiqDeprecationWarning: Accessing 'quantum_program.transpiled_circuit.count_ops' is deprecated. Use 'classiq.get_transpiled_circuit_metrics(quantum_program)' instead. classiq_cx_counts_opt_cx.append(qprog.transpiled_circuit.count_ops["cx"]) ``` **Output:** ``` time (cx) : 4.635745048522949 6 ====== ``` **Output:** ``` /var/folders/kk/nlz2dw2921g33r494qcnf4jh0000gn/T/ipykernel_84535/1498934103.py:32: ClassiqDeprecationWarning: Accessing 'quantum_program.transpiled_circuit.depth' is deprecated. Use 'classiq.get_transpiled_circuit_metrics(quantum_program)' instead. classiq_depths_opt_width.append(qprog.transpiled_circuit.depth) /var/folders/kk/nlz2dw2921g33r494qcnf4jh0000gn/T/ipykernel_84535/1498934103.py:33: ClassiqDeprecationWarning: Accessing 'quantum_program.transpiled_circuit.count_ops' is deprecated. Use 'classiq.get_transpiled_circuit_metrics(quantum_program)' instead. classiq_cx_counts_opt_width.append(qprog.transpiled_circuit.count_ops["cx"]) ``` **Output:** ``` time (width): 2.602505922317505 ``` **Output:** ``` /var/folders/kk/nlz2dw2921g33r494qcnf4jh0000gn/T/ipykernel_84535/1498934103.py:48: ClassiqDeprecationWarning: Accessing 'quantum_program.transpiled_circuit.depth' is deprecated. Use 'classiq.get_transpiled_circuit_metrics(quantum_program)' instead. classiq_depths_opt_cx.append(qprog.transpiled_circuit.depth) /var/folders/kk/nlz2dw2921g33r494qcnf4jh0000gn/T/ipykernel_84535/1498934103.py:49: ClassiqDeprecationWarning: Accessing 'quantum_program.transpiled_circuit.count_ops' is deprecated. Use 'classiq.get_transpiled_circuit_metrics(quantum_program)' instead. classiq_cx_counts_opt_cx.append(qprog.transpiled_circuit.count_ops["cx"]) ``` **Output:** ``` time (cx) : 6.971702814102173 7 ====== ``` **Output:** ``` /var/folders/kk/nlz2dw2921g33r494qcnf4jh0000gn/T/ipykernel_84535/1498934103.py:32: ClassiqDeprecationWarning: Accessing 'quantum_program.transpiled_circuit.depth' is deprecated. Use 'classiq.get_transpiled_circuit_metrics(quantum_program)' instead. classiq_depths_opt_width.append(qprog.transpiled_circuit.depth) /var/folders/kk/nlz2dw2921g33r494qcnf4jh0000gn/T/ipykernel_84535/1498934103.py:33: ClassiqDeprecationWarning: Accessing 'quantum_program.transpiled_circuit.count_ops' is deprecated. Use 'classiq.get_transpiled_circuit_metrics(quantum_program)' instead. classiq_cx_counts_opt_width.append(qprog.transpiled_circuit.count_ops["cx"]) ``` **Output:** ``` time (width): 2.796706199645996 ``` **Output:** ``` /var/folders/kk/nlz2dw2921g33r494qcnf4jh0000gn/T/ipykernel_84535/1498934103.py:48: ClassiqDeprecationWarning: Accessing 'quantum_program.transpiled_circuit.depth' is deprecated. Use 'classiq.get_transpiled_circuit_metrics(quantum_program)' instead. classiq_depths_opt_cx.append(qprog.transpiled_circuit.depth) /var/folders/kk/nlz2dw2921g33r494qcnf4jh0000gn/T/ipykernel_84535/1498934103.py:49: ClassiqDeprecationWarning: Accessing 'quantum_program.transpiled_circuit.count_ops' is deprecated. Use 'classiq.get_transpiled_circuit_metrics(quantum_program)' instead. classiq_cx_counts_opt_cx.append(qprog.transpiled_circuit.count_ops["cx"]) ``` **Output:** ``` time (cx) : 6.315190076828003 8 ====== ``` **Output:** ``` /var/folders/kk/nlz2dw2921g33r494qcnf4jh0000gn/T/ipykernel_84535/1498934103.py:32: ClassiqDeprecationWarning: Accessing 'quantum_program.transpiled_circuit.depth' is deprecated. Use 'classiq.get_transpiled_circuit_metrics(quantum_program)' instead. classiq_depths_opt_width.append(qprog.transpiled_circuit.depth) /var/folders/kk/nlz2dw2921g33r494qcnf4jh0000gn/T/ipykernel_84535/1498934103.py:33: ClassiqDeprecationWarning: Accessing 'quantum_program.transpiled_circuit.count_ops' is deprecated. Use 'classiq.get_transpiled_circuit_metrics(quantum_program)' instead. classiq_cx_counts_opt_width.append(qprog.transpiled_circuit.count_ops["cx"]) ``` **Output:** ``` time (width): 7.324779033660889 ``` **Output:** ``` /var/folders/kk/nlz2dw2921g33r494qcnf4jh0000gn/T/ipykernel_84535/1498934103.py:48: ClassiqDeprecationWarning: Accessing 'quantum_program.transpiled_circuit.depth' is deprecated. Use 'classiq.get_transpiled_circuit_metrics(quantum_program)' instead. classiq_depths_opt_cx.append(qprog.transpiled_circuit.depth) /var/folders/kk/nlz2dw2921g33r494qcnf4jh0000gn/T/ipykernel_84535/1498934103.py:49: ClassiqDeprecationWarning: Accessing 'quantum_program.transpiled_circuit.count_ops' is deprecated. Use 'classiq.get_transpiled_circuit_metrics(quantum_program)' instead. classiq_cx_counts_opt_cx.append(qprog.transpiled_circuit.count_ops["cx"]) ``` **Output:** ``` time (cx) : 3.9115071296691895 9 ====== ``` **Output:** ``` /var/folders/kk/nlz2dw2921g33r494qcnf4jh0000gn/T/ipykernel_84535/1498934103.py:32: ClassiqDeprecationWarning: Accessing 'quantum_program.transpiled_circuit.depth' is deprecated. Use 'classiq.get_transpiled_circuit_metrics(quantum_program)' instead. classiq_depths_opt_width.append(qprog.transpiled_circuit.depth) /var/folders/kk/nlz2dw2921g33r494qcnf4jh0000gn/T/ipykernel_84535/1498934103.py:33: ClassiqDeprecationWarning: Accessing 'quantum_program.transpiled_circuit.count_ops' is deprecated. Use 'classiq.get_transpiled_circuit_metrics(quantum_program)' instead. classiq_cx_counts_opt_width.append(qprog.transpiled_circuit.count_ops["cx"]) ``` **Output:** ``` time (width): 10.21957778930664 ``` **Output:** ``` /var/folders/kk/nlz2dw2921g33r494qcnf4jh0000gn/T/ipykernel_84535/1498934103.py:48: ClassiqDeprecationWarning: Accessing 'quantum_program.transpiled_circuit.depth' is deprecated. Use 'classiq.get_transpiled_circuit_metrics(quantum_program)' instead. classiq_depths_opt_cx.append(qprog.transpiled_circuit.depth) /var/folders/kk/nlz2dw2921g33r494qcnf4jh0000gn/T/ipykernel_84535/1498934103.py:49: ClassiqDeprecationWarning: Accessing 'quantum_program.transpiled_circuit.count_ops' is deprecated. Use 'classiq.get_transpiled_circuit_metrics(quantum_program)' instead. classiq_cx_counts_opt_cx.append(qprog.transpiled_circuit.count_ops["cx"]) ``` **Output:** ``` time (cx) : 12.222119808197021 10 ====== ``` **Output:** ``` /var/folders/kk/nlz2dw2921g33r494qcnf4jh0000gn/T/ipykernel_84535/1498934103.py:32: ClassiqDeprecationWarning: Accessing 'quantum_program.transpiled_circuit.depth' is deprecated. Use 'classiq.get_transpiled_circuit_metrics(quantum_program)' instead. classiq_depths_opt_width.append(qprog.transpiled_circuit.depth) /var/folders/kk/nlz2dw2921g33r494qcnf4jh0000gn/T/ipykernel_84535/1498934103.py:33: ClassiqDeprecationWarning: Accessing 'quantum_program.transpiled_circuit.count_ops' is deprecated. Use 'classiq.get_transpiled_circuit_metrics(quantum_program)' instead. classiq_cx_counts_opt_width.append(qprog.transpiled_circuit.count_ops["cx"]) ``` **Output:** ``` time (width): 3.761033058166504 ``` **Output:** ``` /var/folders/kk/nlz2dw2921g33r494qcnf4jh0000gn/T/ipykernel_84535/1498934103.py:48: ClassiqDeprecationWarning: Accessing 'quantum_program.transpiled_circuit.depth' is deprecated. Use 'classiq.get_transpiled_circuit_metrics(quantum_program)' instead. classiq_depths_opt_cx.append(qprog.transpiled_circuit.depth) /var/folders/kk/nlz2dw2921g33r494qcnf4jh0000gn/T/ipykernel_84535/1498934103.py:49: ClassiqDeprecationWarning: Accessing 'quantum_program.transpiled_circuit.count_ops' is deprecated. Use 'classiq.get_transpiled_circuit_metrics(quantum_program)' instead. classiq_cx_counts_opt_cx.append(qprog.transpiled_circuit.count_ops["cx"]) ``` **Output:** ``` time (cx) : 5.6854329109191895 11 ====== ``` **Output:** ``` /var/folders/kk/nlz2dw2921g33r494qcnf4jh0000gn/T/ipykernel_84535/1498934103.py:32: ClassiqDeprecationWarning: Accessing 'quantum_program.transpiled_circuit.depth' is deprecated. Use 'classiq.get_transpiled_circuit_metrics(quantum_program)' instead. classiq_depths_opt_width.append(qprog.transpiled_circuit.depth) /var/folders/kk/nlz2dw2921g33r494qcnf4jh0000gn/T/ipykernel_84535/1498934103.py:33: ClassiqDeprecationWarning: Accessing 'quantum_program.transpiled_circuit.count_ops' is deprecated. Use 'classiq.get_transpiled_circuit_metrics(quantum_program)' instead. classiq_cx_counts_opt_width.append(qprog.transpiled_circuit.count_ops["cx"]) ``` **Output:** ``` time (width): 4.790789842605591 ``` **Output:** ``` /var/folders/kk/nlz2dw2921g33r494qcnf4jh0000gn/T/ipykernel_84535/1498934103.py:48: ClassiqDeprecationWarning: Accessing 'quantum_program.transpiled_circuit.depth' is deprecated. Use 'classiq.get_transpiled_circuit_metrics(quantum_program)' instead. classiq_depths_opt_cx.append(qprog.transpiled_circuit.depth) ``` **Output:** ``` time (cx) : 4.716082334518433 ``` **Output:** ``` /var/folders/kk/nlz2dw2921g33r494qcnf4jh0000gn/T/ipykernel_84535/1498934103.py:49: ClassiqDeprecationWarning: Accessing 'quantum_program.transpiled_circuit.count_ops' is deprecated. Use 'classiq.get_transpiled_circuit_metrics(quantum_program)' instead. classiq_cx_counts_opt_cx.append(qprog.transpiled_circuit.count_ops["cx"]) ``` ```python theme={null} print("classiq depths:", classiq_depths_opt_width) print("classiq cx_counts:", classiq_cx_counts_opt_width) print("classiq widths:", classiq_widths_opt_width) print(classiq_times_opt_width) print("classiq depths:", classiq_depths_opt_cx) print("classiq cx_counts:", classiq_cx_counts_opt_cx) print("classiq widths:", classiq_widths_opt_cx) print(classiq_times_opt_cx) ``` **Output:** ``` classiq depths: [407, 727, 1161, 1725, 2435, 3307, 4357] classiq cx_counts: [272, 512, 848, 1296, 1872, 2592, 3472] classiq widths: [6, 7, 8, 9, 10, 11, 12] [6.4162139892578125, 2.602505922317505, 2.796706199645996, 7.324779033660889, 10.21957778930664, 3.761033058166504, 4.790789842605591] classiq depths: [183, 269, 345, 481, 557, 681, 761] classiq cx_counts: [108, 168, 216, 318, 366, 504, 552] classiq widths: [7, 9, 10, 12, 13, 15, 16] [4.635745048522949, 6.971702814102173, 6.315190076828003, 3.9115071296691895, 12.222119808197021, 5.6854329109191895, 4.716082334518433] ``` ## Comparing to Qiskit Implementation The qiskit data was generated using qiskit version 1. 0. 1. To run the qiskit code uncomment the commented cells below. ```python theme={null} qiskit_cx_counts = [900, 2376, 5388, 11472, 23700, 48216] qiskit_depths = [1645, 4222, 9485, 20122, 41509, 84398] qiskit_widths = [6, 7, 8, 9, 10, 11] qiskit_times = [ 0.43783092498779297, 3.743027925491333, 17.858744144439697, 73.1058611869812, 293.51222825050354, 1222.052798986435, ] ``` ```python theme={null} # from qiskit import QuantumCircuit, QuantumRegister, transpile # def get_incerement_circuit(num_qubits): # incerement_circuit= QuantumCircuit(num_qubits) # for j in range(num_qubits - 1): # incerement_circuit.mcx([k for k in range(num_qubits - 1-j)], num_qubits-1-j) # incerement_circuit.x(0) # return incerement_circuit # # building the q_walk_step, the first qubit is the coin # qiskit_cx_counts = [] # qiskit_depths = [] # qiskit_widths = [] # qiskit_times = [] # for num_qubits in range(NUM_QUBITS_MIN,NUM_QUBITS_MAX): # start_time = time.time() # q_walk_step = QuantumCircuit(num_qubits+1) # q_walk_step.h(0) # q_walk_step.append(get_incerement_circuit(num_qubits).control(1, ctrl_state=1), # [k for k in range(num_qubits+1)]) # q_walk_step.append(get_incerement_circuit(num_qubits).inverse().control(1, ctrl_state=0), # [k for k in range(num_qubits+1)]) # transpiled_cir = transpile( # q_walk_step, # basis_gates=["u", "cx"], # optimization_level=transpilation_options["qiskit"], # ) # print(time.time()-start_time, ", ",num_qubits) # print(transpiled_cir.depth()) # qiskit_depths.append(transpiled_cir.depth()) # qiskit_cx_counts.append(transpiled_cir.count_ops()["cx"]) # qiskit_widths.append(transpiled_cir.width()) # qiskit_times.append(time.time()-start_time) # print(qiskit_cx_counts) # print(qiskit_depths) # print(qiskit_widths) # print(qiskit_times) ``` ```python theme={null} num_qubits_classiq = range(NUM_QUBITS_MIN, NUM_QUBITS_MAX) num_qubits_qiskit = num_qubits_classiq[0 : len(qiskit_cx_counts)] ``` ```python theme={null} import matplotlib.pyplot as plt classiq_color = "#119DA4" classiq_color_1 = "#F43764" qiskit_color = "#bb8bff" plt.rcParams["font.family"] = "serif" plt.rc("savefig", dpi=300) plt.rcParams["axes.linewidth"] = 1 plt.rcParams["xtick.major.size"] = 5 plt.rcParams["xtick.minor.size"] = 5 plt.rcParams["ytick.major.size"] = 5 plt.rcParams["ytick.minor.size"] = 5 (qiskit1,) = plt.semilogy( num_qubits_qiskit, qiskit_cx_counts, "-s", label="qiskit", markerfacecolor=qiskit_color, markeredgecolor="k", markersize=6, markeredgewidth=1.5, color=qiskit_color, ) (classiq1,) = plt.semilogy( num_qubits_classiq, classiq_cx_counts_opt_width, "-D", label="classiq width opt.", markerfacecolor=classiq_color, markeredgecolor="k", markersize=6.5, markeredgewidth=1.5, color=classiq_color, ) (classiq2,) = plt.semilogy( num_qubits_classiq, classiq_cx_counts_opt_cx, "-o", label="classiq cx opt.", markerfacecolor=classiq_color_1, markeredgecolor="k", markersize=7, markeredgewidth=1.5, linewidth=1.5, color=classiq_color_1, ) first_legend = plt.legend( handles=[qiskit1, classiq1, classiq2], fontsize=16, loc="upper left" ) plt.ylabel("CX-counts", fontsize=16) plt.xlabel(r"$\log_2$(Circle size)", fontsize=16) plt.yticks(fontsize=16) plt.xticks(fontsize=16) plt.axis(ymin=0.7e2, ymax=4e5, xmin=4, xmax=12) for x, y, num_qubits in zip( num_qubits_classiq, classiq_cx_counts_opt_width, classiq_widths_opt_width ): plt.text(x * 0.94, y * 1.25, str(num_qubits), fontsize=16, color=classiq_color) for x, y, num_qubits in zip( num_qubits_classiq, classiq_cx_counts_opt_cx, classiq_widths_opt_cx, ): plt.text(x * 0.96, y * 1.25, str(num_qubits), fontsize=16, color=classiq_color_1) for x, y, num_qubits in zip(num_qubits_qiskit, qiskit_cx_counts, qiskit_widths): plt.text(x * 0.96, y * 1.2, str(num_qubits), fontsize=16, color=qiskit_color) ``` output ```python theme={null} import matplotlib.pyplot as plt classiq_color = "#119DA4" classiq_color_1 = "#F43764" qiskit_color = "#bb8bff" plt.rcParams["font.family"] = "serif" plt.rc("savefig", dpi=300) plt.rcParams["axes.linewidth"] = 1 plt.rcParams["xtick.major.size"] = 5 plt.rcParams["xtick.minor.size"] = 5 plt.rcParams["ytick.major.size"] = 5 plt.rcParams["ytick.minor.size"] = 5 (qiskit1,) = plt.semilogy( num_qubits_qiskit, qiskit_times, "-s", label="qiskit", markerfacecolor=qiskit_color, markeredgecolor="k", markersize=6, markeredgewidth=1.5, color=qiskit_color, ) (classiq1,) = plt.semilogy( num_qubits_classiq, classiq_times_opt_width, "-D", label="classiq width opt.", markerfacecolor=classiq_color, markeredgecolor="k", markersize=6.5, markeredgewidth=1.5, color=classiq_color, ) (classiq2,) = plt.semilogy( num_qubits_classiq, classiq_times_opt_cx, "-o", label="classiq cx opt.", markerfacecolor=classiq_color_1, markeredgecolor="k", markersize=7, markeredgewidth=1.5, linewidth=1.5, color=classiq_color_1, ) first_legend = plt.legend( handles=[qiskit1, classiq1, classiq2], fontsize=16, loc="upper left" ) plt.ylabel("Generation time", fontsize=16) plt.xlabel(r"$\log_2$(Circle size)", fontsize=16) plt.yticks(fontsize=16) plt.xticks(fontsize=16) plt.axis(ymin=2e-1, ymax=2.5e4, xmin=4, xmax=12) for x, y, num_qubits in zip( num_qubits_classiq, classiq_times_opt_width, classiq_widths_opt_width ): plt.text(x * 0.98, y * 1.4, str(num_qubits), fontsize=16, color=classiq_color) for x, y, num_qubits in zip( num_qubits_classiq, classiq_times_opt_cx, classiq_widths_opt_cx, ): plt.text(x * 0.96, y * 0.5, str(num_qubits), fontsize=16, color=classiq_color_1) for x, y, num_qubits in zip(num_qubits_qiskit, qiskit_times, qiskit_widths): plt.text(x * 0.94, y * 1.2, str(num_qubits), fontsize=16, color=qiskit_color) ``` output ## Synthesizing Large-Scale Examples We have extended the above model to larger and larger circle sizes. The results are saved in a `csv` file. ```python theme={null} ind_for_plot = [0, 5, 10, 12, 14] + [k for k in range(15, 23)] ``` ```python theme={null} import pandas as pd input_file = "results.csv" # reading CSV file data = pd.read_csv(input_file) # this data is with cx optimization and max_width=100 # converting column data to list num_qubits_cx_opt = [data["log2_circle_size_cx_opt"].tolist()[k] for k in ind_for_plot] cx_cx_opt = [data["cx_cx_opt"].tolist()[k] for k in ind_for_plot] time_cx_opt = [data["time_cx_opt"].tolist()[k] for k in ind_for_plot] width_cx_opt = [data["width_cx_opt"].tolist()[k] for k in ind_for_plot] # converting column data to list num_qubits_width_opt = [ data["log2_circle_size_width_opt"].tolist()[k] for k in ind_for_plot ] cx_width_opt = [data["cx_width_opt"].tolist()[k] for k in ind_for_plot] time_width_opt = [data["time_width_opt"].tolist()[k] for k in ind_for_plot] width_width_opt = [data["width_width_opt"].tolist()[k] for k in ind_for_plot] ``` ```python theme={null} import matplotlib.pyplot as plt classiq_color = "#119DA4" classiq_color_1 = "#F43764" qiskit_color = "#bb8bff" plt.rcParams["font.family"] = "serif" plt.rc("savefig", dpi=300) plt.rcParams["axes.linewidth"] = 1 plt.rcParams["xtick.major.size"] = 5 plt.rcParams["xtick.minor.size"] = 5 plt.rcParams["ytick.major.size"] = 5 plt.rcParams["ytick.minor.size"] = 5 plt.figure(figsize=(8, 5)) (qiskit1,) = plt.semilogy( num_qubits_qiskit, qiskit_cx_counts, "-s", label="qiskit", markerfacecolor=qiskit_color, markeredgecolor="k", markersize=6, markeredgewidth=1.5, color=qiskit_color, ) (classiq1,) = plt.semilogy( num_qubits_width_opt, cx_width_opt, "-D", label="classiq width optimization", markerfacecolor=classiq_color, markeredgecolor="k", markersize=6.5, markeredgewidth=1.5, color=classiq_color, ) (classiq2,) = plt.semilogy( num_qubits_cx_opt, cx_cx_opt, "-o", label="classiq cx optimization", markerfacecolor=classiq_color_1, markeredgecolor="k", markersize=7, markeredgewidth=1.5, linewidth=1.5, color=classiq_color_1, ) first_legend = plt.legend( handles=[qiskit1, classiq1, classiq2], fontsize=16, loc="lower right" ) plt.ylabel("CX-counts", fontsize=16) plt.xlabel(r"$\log_2$(Circle size)", fontsize=16) plt.yticks(fontsize=16) plt.xticks(fontsize=16) plt.axis(ymin=0.5e2, ymax=1e6, xmin=3, xmax=63) # plt.xticks(num_qubits_opt_cx_max_width) for x, y, num_qubits in zip(num_qubits_width_opt, cx_width_opt, width_width_opt): plt.text(x * 1.01, y * 0.6, str(num_qubits), fontsize=15, color=classiq_color) for x, y, num_qubits in zip( num_qubits_cx_opt, cx_cx_opt, width_cx_opt, ): plt.text(x * 0.99, y * 0.45, str(num_qubits), fontsize=15, color=classiq_color_1) for x, y, num_qubits in zip(num_qubits_qiskit, qiskit_cx_counts, qiskit_widths): plt.text(x * 0.77, y * 1.2, str(num_qubits), fontsize=15, color=qiskit_color) ``` output ```python theme={null} import matplotlib.pyplot as plt classiq_color = "#119DA4" classiq_color_1 = "#F43764" qiskit_color = "#bb8bff" plt.rcParams["font.family"] = "serif" plt.rc("savefig", dpi=300) plt.rcParams["axes.linewidth"] = 1 plt.rcParams["xtick.major.size"] = 5 plt.rcParams["xtick.minor.size"] = 5 plt.rcParams["ytick.major.size"] = 5 plt.rcParams["ytick.minor.size"] = 5 plt.figure(figsize=(8, 5)) (qiskit1,) = plt.semilogy( num_qubits_qiskit, qiskit_times, "-s", label="qiskit", markerfacecolor=qiskit_color, markeredgecolor="k", markersize=6, markeredgewidth=1.5, color=qiskit_color, ) (classiq1,) = plt.semilogy( num_qubits_width_opt, time_width_opt, "-D", label="classiq width optimization", markerfacecolor=classiq_color, markeredgecolor="k", markersize=6.5, markeredgewidth=1.5, color=classiq_color, ) (classiq2,) = plt.semilogy( num_qubits_cx_opt, time_cx_opt, "-o", label="classiq cx optimization", markerfacecolor=classiq_color_1, markeredgecolor="k", markersize=7, markeredgewidth=1.5, linewidth=1.5, color=classiq_color_1, ) first_legend = plt.legend( handles=[qiskit1, classiq1, classiq2], fontsize=16, loc="lower right" ) plt.ylabel("Generation time [sec]", fontsize=16) plt.xlabel(r"$\log_2$(Circle size)", fontsize=16) plt.yticks(fontsize=16) plt.xticks(fontsize=16) plt.axis(ymin=0.2, ymax=8e3, xmin=3, xmax=63) for x, y, num_qubits in zip(num_qubits_width_opt, time_width_opt, width_width_opt): if num_qubits < 19: plt.text(x * 0.8, y * 1.3, str(num_qubits), fontsize=15, color=classiq_color) else: plt.text(x * 0.95, y * 1.3, str(num_qubits), fontsize=15, color=classiq_color) for x, y, num_qubits in zip( num_qubits_cx_opt, time_cx_opt, width_cx_opt, ): plt.text(x * 1.0, y * 0.45, str(num_qubits), fontsize=15, color=classiq_color_1) for x, y, num_qubits in zip(num_qubits_qiskit, qiskit_times, qiskit_widths): plt.text(x * 0.72, y * 1.2, str(num_qubits), fontsize=15, color=qiskit_color) ``` output # Hamiltonian Evolution for a Water Molecule Source: https://docs.classiq.io/explore/tutorials/technology_demonstrations/hamiltonian_evolution/hamiltonian_evolution Open this notebook in GitHub to run it yourself This tutorial demonstrates the ability of the Classiq synthesis engine to reduce depth and cx-counts in approximated quantum functions for Hamiltonian evolution, focusing on Suzuki-Trotter (ST) and qDRIFT (qD) product formulas and their controlled operations. In addition, it is compared to the equivalent quantum examples in Qiskit. The demonstration is for the Hamiltonian of a water molecule, which has 551 terms and a dimension of 12 qubits. Define a molecule and get the Hamiltonian as a list of Pauli strings and coefficients: ```python theme={null} from openfermion.chem import MolecularData from openfermionpyscf import run_pyscf from classiq.applications.chemistry.mapping import FermionToQubitMapper from classiq.applications.chemistry.op_utils import qubit_op_to_qmod from classiq.applications.chemistry.problems import FermionHamiltonianProblem molecule_H2O_geometry = [ ("O", (0.0, 0.0, 0.0)), ("H", (0, 0.586, 0.757)), ("H", (0, 0.586, -0.757)), ] molecule = MolecularData(molecule_H2O_geometry, "sto-3g", 1, 0) molecule = run_pyscf(molecule) problem = FermionHamiltonianProblem.from_molecule(molecule, first_active_index=1) mapper = FermionToQubitMapper() hamiltonian = qubit_op_to_qmod(mapper.map(problem.fermion_hamiltonian)) ``` These cases are examined: ```python theme={null} ORDERS_for_ST = [1, 2, 4] REPETITIONS_for_ST = [6, 4, 1] N_QDS_for_qDRIFT = [1000, 2000] ``` ```python theme={null} classiq_depths = [] classiq_cx_counts = [] ``` ```python theme={null} # transpilation_options = {"classiq": "custom", "qiskit": 3} transpilation_options = {"classiq": "auto optimize", "qiskit": 1} ``` ## 1. Approximating with Suzuki-Trotter Formulas ```python theme={null} from classiq import * preferences = Preferences( custom_hardware_settings=CustomHardwareSettings(basis_gates=["cx", "u"]), transpilation_option=transpilation_options["classiq"], ) all_qprogs = [] for k in range(len(ORDERS_for_ST)): @qfunc def main(qbv: Output[QArray]) -> None: allocate(hamiltonian.num_qubits, qbv) suzuki_trotter( pauli_operator=hamiltonian, evolution_coefficient=1, order=ORDERS_for_ST[k], repetitions=REPETITIONS_for_ST[k], qbv=qbv, ) qprog = synthesize(main, preferences=preferences) all_qprogs.append(qprog) classiq_depths.append(qprog.transpiled_circuit.depth) classiq_cx_counts.append(qprog.transpiled_circuit.count_ops["cx"]) ``` ## 2. Implementing Controlled Hamiltonian Dynamics ```python theme={null} for k in range(2): @qfunc def main(qbv: Output[QArray]) -> None: allocate(hamiltonian.num_qubits, qbv) ctrl = QBit() allocate(ctrl) control( ctrl=ctrl, stmt_block=lambda: suzuki_trotter( pauli_operator=hamiltonian, evolution_coefficient=1, order=ORDERS_for_ST[k], repetitions=REPETITIONS_for_ST[k], qbv=qbv, ), ) qprog = synthesize(main, preferences=preferences) all_qprogs.append(qprog) classiq_depths.append(qprog.transpiled_circuit.depth) classiq_cx_counts.append(qprog.transpiled_circuit.count_ops["cx"]) ``` ## 3. Approximating with the qDRIFT Formula ```python theme={null} for n_qd in N_QDS_for_qDRIFT: @qfunc def main(qbv: Output[QArray]) -> None: allocate(hamiltonian.num_qubits, qbv) qdrift( pauli_operator=hamiltonian, evolution_coefficient=1, num_qdrift=n_qd, qbv=qbv, ) qprog = synthesize(main, preferences=preferences) all_qprogs.append(qprog) classiq_depths.append(qprog.transpiled_circuit.depth) classiq_cx_counts.append(qprog.transpiled_circuit.count_ops["cx"]) ``` ## 4. Comparing to Qiskit Implementation Comments: * Qiskit's Suzuki-Trotter of order 1 is a separate function, called the Lie-Trotter function. * Qiskit's qDRIFT takes a long time to run for unclear problems. Alternatively, a random product formula is implemented, which is equivalent to qDRIFT. The qiskit data was generated using qiskit version 1. 0. 1. To run the qiskit code uncomment the commented cells below. ```python theme={null} qiskit_cx_counts = [25164, 33508, 41882, 186067, 248232, 3581, 7404] qiskit_depths = [30627, 42879, 53581, 300924, 402125, 3463, 7141] ``` ```python theme={null} # from importlib.metadata import version # try: # import qiskit # if version('qiskit') != "1.0.0": # !pip uninstall qiskit -y # !pip install qiskit==1.0.0 # except ImportError: # !pip install qiskit==1.0.0 ``` ```python theme={null} # from qiskit.circuit.library import PauliEvolutionGate # from qiskit.quantum_info import SparsePauliOp # from qiskit.synthesis import LieTrotter as LieTrotter_qiskit # qiskit_depths = [] # qiskit_cx_counts = [] # operator = SparsePauliOp.from_list(pauli_list) # gate = PauliEvolutionGate(operator, 1) ``` ```python theme={null} # ## Suzuki-Trotter of order 1 = Lie-Trotter # from qiskit import transpile # lt = LieTrotter_qiskit(reps=REPETITIONS_for_ST[0]) # circ = lt.synthesize(gate) # tqc = transpile( # circ, basis_gates=["u", "cx"], optimization_level=transpilation_options["qiskit"] # ) # qiskit_depths.append(tqc.depth()) # qiskit_cx_counts.append(tqc.count_ops()["cx"]) ``` ```python theme={null} # ## Suzuki-Trotter # from qiskit.synthesis import SuzukiTrotter as SuzukiTrotter_qiskit # for k in range(1, 3): # st = SuzukiTrotter_qiskit(order=ORDERS_for_ST[k], reps=REPETITIONS_for_ST[k]) # circ = st.synthesize(gate) # tqc = transpile( # circ, # basis_gates=["u", "cx"], # optimization_level=transpilation_options["qiskit"], # ) # qiskit_depths.append(tqc.depth()) # qiskit_cx_counts.append(tqc.count_ops()["cx"]) ``` ```python theme={null} # # Controlled dynamics # lt_ctrl = LieTrotter_qiskit(reps=REPETITIONS_for_ST[0]) # circ = lt_ctrl.synthesize(gate).control(1) # tqc = transpile( # circ, basis_gates=["u", "cx"], optimization_level=transpilation_options["qiskit"] # ) # qiskit_depths.append(tqc.depth()) # qiskit_cx_counts.append(tqc.count_ops()["cx"]) # for k in range(1, 2): # st_ctrl = SuzukiTrotter_qiskit(order=ORDERS_for_ST[k], reps=REPETITIONS_for_ST[k]) # circ = st_ctrl.synthesize(gate).control(1) # tqc = transpile( # circ, # basis_gates=["u", "cx"], # optimization_level=transpilation_options["qiskit"], # ) # qiskit_depths.append(tqc.depth()) # qiskit_cx_counts.append(tqc.count_ops()["cx"]) ``` ```python theme={null} # ## For qDRIFT, generate a random sequence # import numpy as np # def index_channel(n, list_coe): # """ # This function gets an ordered list of coefficients 'list_coe' and a number of calls 'n' as inputs. # It returns a random ordered list of size n with elements from list_coe', # where the probability of choosing the i-th elements is list_coe[i]/sum(list_coe), # """ # coe = np.array(list_coe) / sum(list_coe) # c_coe = np.cumsum(coe) # return np.searchsorted(c_coe, np.random.uniform(size=n)) # assert ( # pauli_list[0][0] == len(pauli_list[0][0]) * "I" # ), """The Identity term is not the first on the list of Paulis, # please modify the code accordingly """ # pauli_list_without_id = pauli_list[1::] # po_coe = [ # np.abs(np.real(p[1])) for p in pauli_list_without_id # ] # gets absolute value of coefficients # for n_qd in N_QDS_for_qDRIFT: # new_indices = index_channel(n_qd, po_coe) # small_lambda = sum(po_coe) # randomly_generated_pauli_list = [ # ( # pauli_list_without_id[new_indices[k]][0], # np.sign(pauli_list_without_id[new_indices[k]][1]) * small_lambda / n_qd, # ) # for k in range(n_qd) # ] # randomly_generated_pauli_list += [pauli_list[0]] # adding the identity # gate_qd = PauliEvolutionGate( # SparsePauliOp.from_list(randomly_generated_pauli_list), 1 # ) # qd = LieTrotter_qiskit(reps=1) # circ = qd.synthesize(gate_qd) # tqc = transpile( # circ, # basis_gates=["u", "cx"], # optimization_level=transpilation_options["qiskit"], # ) # qiskit_depths.append(tqc.depth()) # qiskit_cx_counts.append(tqc.count_ops()["cx"]) ``` ## 5. Plotting the Data ```python theme={null} print("The cx-counts on Classiq:", classiq_cx_counts) print("The cx-counts on Qiskit:", qiskit_cx_counts) ``` **Output:** ``` The cx-counts on Classiq: [9402, 12458, 15570, 21252, 28270, 3388, 6712] The cx-counts on Qiskit: [25164, 33508, 41882, 186067, 248232, 3581, 7404] ``` ```python theme={null} import matplotlib.pyplot as plt classiq_color = "#D7F75B" qiskit_color = "#6FA4FF" plt.rcParams["font.family"] = "serif" plt.rc("savefig", dpi=300) plt.rcParams["axes.linewidth"] = 1 plt.rcParams["xtick.major.size"] = 5 plt.rcParams["xtick.minor.size"] = 5 plt.rcParams["ytick.major.size"] = 5 plt.rcParams["ytick.minor.size"] = 5 plt.semilogy( qiskit_cx_counts[-2::] + qiskit_cx_counts[0:5], "s", label="qiskit", markerfacecolor=qiskit_color, markeredgecolor="k", markersize=6, markeredgewidth=1.5, ) plt.semilogy( classiq_cx_counts[-2::] + classiq_cx_counts[0:5], "o", label="classiq", markerfacecolor=classiq_color, markeredgecolor="k", markersize=6.5, markeredgewidth=1.5, ) labels = [ "qDRIFT(N=1000)", "qDRIFT(N=2000)", "TS$_1$(reps=6)", "TS$_2$(reps=4)", "TS$_4$(reps=1)", "ctrl-TS$_1$(reps=6)", "ctrl-TS$_2$(reps=4)", ] plt.xticks([0, 1, 2, 3, 4, 5, 6], labels, rotation=45, fontsize=16, ha="right") plt.ylabel("CX-counts", fontsize=18) plt.yticks(fontsize=16) plt.legend(loc="upper left", fontsize=18, fancybox=True, framealpha=0.5) ``` **Output:** ``` ``` output # HW-Aware Synthesis of MCX Source: https://docs.classiq.io/explore/tutorials/technology_demonstrations/hardware_aware_mcx/hardware_aware_mcx Open this notebook in GitHub to run it yourself This example shows that implementation of multiple control-x (MCX) logic, using the Classiq synthesis engine, yields different circuit results for different quantum hardware. The fictitious hardware created here demonstrates how to insert your own custom-designed machine. For comparison, create two types of hardware with `cx, u` basis gates. The difference between them manifests in the connectivity map: one has linear connectivity while the other has all-to-all connectivity. ```python theme={null} from classiq import * # define the hardware parameters max_width = 18 linear_connectivity = [[qubit, qubit + 1] for qubit in range(max_width - 1)] # define the MCX parameters within the quantum 'main' function @qfunc def main(cntrl: Output[QArray], target: Output[QBit]) -> None: allocate(15, cntrl) allocate(target) control(cntrl, lambda: X(target)) # build a model qmod = create_model(main) # define synthesis engine constraints qmod = set_constraints(qmod, optimization_parameter="depth", max_width=max_width) # define models with different preferences qmod_linear = set_preferences( qmod, custom_hardware_settings=CustomHardwareSettings( basis_gates=["cx", "u"], connectivity_map=linear_connectivity, ), random_seed=-1, ) qmod_all_to_all = set_preferences( qmod, custom_hardware_settings=CustomHardwareSettings(basis_gates=["cx", "u"]), random_seed=-1, ) # write models to files # synthesize to create quantum programs and view circuits: qprog_linear = synthesize(qmod_linear) show(qprog_linear) qprog_all_to_all = synthesize(qmod_all_to_all) show(qprog_all_to_all) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/30ekINlCUBv3lqPtafJ4KJGE0wX Quantum program link: https://platform.classiq.io/circuit/30ekJtDZ1ine1W4cRUpaqJm9uGS ``` Comparison of the two circuits shows that applying MCx using different connectivity maps yields different implementation. Using "all-to-all" connectivity, the synthesis engine chooses as the best implementation a recourse based on "Maslov2015" \[[1](#maslov)] that was written on the Classiq platform. Using that, the manufactured circuit has 18 qubits; i.e., it uses two auxiliary qubits. The total depth of the circuit is: ```python theme={null} print(qprog_all_to_all.transpiled_circuit.depth) ``` **Output:** ``` 378 ``` When using linear connectivity, the best implementation chosen by the synthesis engine is, in fact, different: an algorithm developed by Classiq, which is better suited for this map. Here, the manufactured circuit uses 18 qubits with only one auxiliary and has a depth of: ```python theme={null} print(qprog_linear.transpiled_circuit.depth) ``` **Output:** ``` 781 ``` ## References \[1]: [Maslov, D., 2016. Advantages of using relative-phase Toffoli gates with an application to multiple control Toffoli optimization. Physical Review A, 93(2), p.022311.](https://arxiv.org/pdf/1508.03273.pdf) # HHL for Solving $A ec{x}= ec{b}$ Source: https://docs.classiq.io/explore/tutorials/technology_demonstrations/hhl/hhl_example Open this notebook in GitHub to run it yourself This tutorial demonstrates an implementation of the HHL algorithm for a specific matrix. The Hamiltonian evolution is implemented by an "exact" operation, explicitly evaluating $e^{iA}$. ```python theme={null} import numpy as np import scipy a_matrix = np.array( [ [0.135, -0.092, -0.011, -0.045, -0.026, -0.033, 0.03, 0.034], [-0.092, 0.115, 0.02, 0.017, 0.044, -0.009, -0.015, -0.072], [-0.011, 0.02, 0.073, -0.0, -0.068, -0.042, 0.043, -0.011], [-0.045, 0.017, -0.0, 0.043, 0.028, 0.027, -0.047, -0.005], [-0.026, 0.044, -0.068, 0.028, 0.21, 0.079, -0.177, -0.05], [-0.033, -0.009, -0.042, 0.027, 0.079, 0.121, -0.123, 0.021], [0.03, -0.015, 0.043, -0.047, -0.177, -0.123, 0.224, 0.011], [0.034, -0.072, -0.011, -0.005, -0.05, 0.021, 0.011, 0.076], ] ) b_vector = np.array( [ -0.00885448, -0.17725898, -0.15441119, 0.17760157, 0.41428775, 0.44735303, -0.71137715, 0.1878808, ] ) sol_classical = np.linalg.solve(a_matrix, b_vector) # classical solution # number of qubits for the unitary num_qubits = int(np.log2(len(b_vector))) # exact unitary my_unitary = scipy.linalg.expm(1j * 2 * np.pi * a_matrix) ``` ```python theme={null} # transpilation_options = {"classiq": "custom", "qiskit": 3} #uncomment this for deeper comparison transpilation_options = {"classiq": "auto optimize", "qiskit": 1} ``` ## 1. HHL with Classiq An HHL solver is tested for different precisions of the QPE size. The following function gets precision and returns the characteristics of the quantum program, as well as the overlap between the classical and the quantum solver. ```python theme={null} from classiq import * from classiq.qmod.symbolic import floor, log @qfunc def simple_eig_inv(phase: QNum, indicator: Output[QBit]): allocate(indicator) assign_amplitude_table( lookup_table(lambda p: 0 if p == 0 else (1 / 2**phase.size) / p, phase), phase, indicator, ) @qfunc def my_hhl( precision: int, b: CArray[CReal], unitary: QCallable[QArray], res: Output[QArray], phase: Output[QNum], indicator: Output[QBit], ) -> None: prepare_amplitudes(b, 0.0, res) allocate(precision, False, precision, phase) within_apply( lambda: qpe(unitary=lambda: unitary(res), phase=phase), lambda: simple_eig_inv(phase=phase, indicator=indicator), ) def get_classiq_hhl_results(precision): """ This function models, synthesizes, executes an HHL example and returns the depth, cx-counts and fidelity """ # SP params b_normalized = b_vector.tolist() sp_upper = 0.00 # precision of the State Preparation unitary_mat = my_unitary.tolist() size = (len(b_normalized) - 1).bit_length() @qfunc def main(res: Output[QNum], phase_var: Output[QNum], indicator: Output[QBit]): my_hhl( precision=precision, b=b_normalized, unitary=lambda target: unitary(elements=unitary_mat, target=target), res=res, phase=phase_var, indicator=indicator, ) # Synthesize preferences = Preferences( custom_hardware_settings=CustomHardwareSettings(basis_gates=["cx", "u"]), transpilation_option=transpilation_options["classiq"], ) qprog_hhl = synthesize(main, preferences=preferences) total_q = qprog_hhl.data.width # total number of qubits of the whole circuit depth = qprog_hhl.transpiled_circuit.depth cx_counts = qprog_hhl.transpiled_circuit.count_ops["cx"] # Execute backend_preferences = ClassiqBackendPreferences( backend_name=ClassiqSimulatorBackendNames.SIMULATOR_STATEVECTOR ) execution_preferences = ExecutionPreferences( num_shots=1, backend_preferences=backend_preferences ) with ExecutionSession(qprog_hhl, execution_preferences) as es: result = es.sample() df = result.dataframe qsol = np.zeros(2**size, dtype=complex) # Post-process # Filter only the successful states. filtered_st = df[ (df.indicator == 1) & (df.phase_var == 0) & (np.abs(df.amplitude) > 1e-12) ] # Allocate values qsol[filtered_st.res] = filtered_st.amplitude / (1 / 2**precision) fidelity = ( np.abs( np.dot( sol_classical / np.linalg.norm(sol_classical), qsol / np.linalg.norm(qsol), ) ) ** 2 ) return total_q, depth, cx_counts, fidelity ``` ```python theme={null} classiq_widths = [] classiq_depths = [] classiq_cx_counts = [] classiq_fidelities = [] for per in range(2, 9): total_q, depth, cx_counts, fidelity = get_classiq_hhl_results(per) classiq_widths.append(total_q) classiq_depths.append(depth) classiq_cx_counts.append(cx_counts) classiq_fidelities.append(fidelity) ``` ```python theme={null} print("classiq overlap:", classiq_fidelities) print("classiq depth:", classiq_depths) ``` **Output:** ``` classiq overlap: [0.31375840880338923, 0.4338789033788851, 0.5603091264685023, 0.6943614908054292, 0.8120393433685461, 0.9428405526765191, 0.9982198481096445] classiq depth: [1063, 1598, 2141, 2700, 3291, 3946, 4729] ``` ## 2. Comparing to Qiskit Qiskit's HHL solver has been deprecated. For comparison, in an analogy to the Classiq model above, wire the four quantum blocks of the HHL algorithm in Qiskit. The qiskit data was generated using qiskit version 1. 0. 1. To run the qiskit code uncomment the commented cells below. ```python theme={null} qiskit_fidelities = [ 0.3158037121175521, 0.43599529278857063, 0.5586003448231571, 0.6824252904259536, 0.806169290650212, 0.9243747525650154, 1.0, ] qiskit_depths = [1921, 4439, 9451, 19455, 39443, 79399, 159291] qiskit_widths = [6, 7, 8, 9, 10, 11, 12] qiskit_cx_counts = [979, 2263, 4819, 9915, 20087, 40407, 81019] ``` ```python theme={null} # from importlib.metadata import version # try: # import qiskit # if version('qiskit') != "1.0.0": # !pip uninstall qiskit -y # !pip install qiskit==1.0.0 # except ImportError: # !pip install qiskit==1.0.0 ``` ```python theme={null} # from qiskit import QuantumCircuit, QuantumRegister, transpile # from qiskit.quantum_info import Statevector # from qiskit.circuit.library import PhaseEstimation as PhaseEstimation_QISKIT # from qiskit.circuit.library.arithmetic.exact_reciprocal import ExactReciprocal # from qiskit.circuit.library import Isometry, Initialize # def get_qiskit_hhl_results(precision): # """ # This function creates an HHL circuit with qiskit, execute it and returns the depth, cx-counts and fidelity # """ # vector_circuit = QuantumCircuit(num_qubits) # initi_vec = Initialize(b_vector / np.linalg.norm(b_vector)) # vector_circuit.append( # initi_vec, list(range(num_qubits)) # ) # q = QuantumRegister(num_qubits, "q") # unitary_qc = QuantumCircuit(q) # unitary_qc.unitary(my_unitary.tolist(), q) # qpe_qc = PhaseEstimation_QISKIT(precision, unitary_qc) # reciprocal_circuit = ExactReciprocal( # num_state_qubits=precision, scaling=1 / 2**precision # ) # # Initialise the quantum registers # qb = QuantumRegister(num_qubits) # right hand side and solution # ql = QuantumRegister(precision) # eigenvalue evaluation qubits # qf = QuantumRegister(1) # flag qubits # hhl_qc = QuantumCircuit(qb, ql, qf) # # State preparation # hhl_qc.append(vector_circuit, qb[:]) # # QPE # hhl_qc.append(qpe_qc, ql[:] + qb[:]) # # Conditioned rotation # hhl_qc.append(reciprocal_circuit, ql[::-1] + [qf[0]]) # # QPE inverse # hhl_qc.append(qpe_qc.inverse(), ql[:] + qb[:]) # # transpile # tqc = transpile( # hhl_qc, # basis_gates=["u3", "cx"], # optimization_level=transpilation_options["qiskit"], # ) # depth = tqc.depth() # cx_counts = tqc.count_ops()["cx"] # total_q = tqc.width() # # execute # statevector = np.array(Statevector(tqc)) # # post_process # all_entries = [np.binary_repr(k, total_q) for k in range(2**total_q)] # sol_indices = [ # int(entry, 2) # for entry in all_entries # if entry[0] == "1" and entry[1 : precision + 1] == "0" * precision # ] # qsol = statevector[sol_indices] / (1 / 2**precision) # sol_classical = np.linalg.solve(a_matrix, b_vector) # fidelity = ( # np.abs( # np.dot( # sol_classical / np.linalg.norm(sol_classical), # qsol / np.linalg.norm(qsol), # ) # ) # ** 2 # ) # return total_q, depth, cx_counts, fidelity ``` ```python theme={null} # qiskit_widths = [] # qiskit_depths = [] # qiskit_cx_counts = [] # qiskit_fidelities = [] # for per in range(2, 9): # total_q, depth, cx_counts, fidelity = get_qiskit_hhl_results(per) # qiskit_widths.append(total_q) # qiskit_depths.append(depth) # qiskit_cx_counts.append(cx_counts) # qiskit_fidelities.append(fidelity) ``` ## 3. Plotting the Data ```python theme={null} import matplotlib.pyplot as plt classiq_color = "#119DA4" qiskit_color = "#bb8bff" plt.rcParams["font.family"] = "serif" plt.rc("savefig", dpi=300) plt.rcParams["axes.linewidth"] = 1 plt.rcParams["xtick.major.size"] = 5 plt.rcParams["xtick.minor.size"] = 5 plt.rcParams["ytick.major.size"] = 5 plt.rcParams["ytick.minor.size"] = 5 (classiq1,) = plt.semilogy( classiq_fidelities, classiq_depths, "-o", label="classiq depth", markerfacecolor=classiq_color, markeredgecolor="k", markersize=8, markeredgewidth=1.5, linewidth=1.5, color=classiq_color, ) (classiq2,) = plt.semilogy( classiq_fidelities, classiq_cx_counts, "-*", label="classiq cx-counts", markerfacecolor=classiq_color, markeredgecolor="k", markersize=12, markeredgewidth=1.5, linewidth=1.5, color=classiq_color, ) (qiskit1,) = plt.semilogy( qiskit_fidelities, qiskit_depths, "-s", label="qiskit depth", markerfacecolor=qiskit_color, markeredgecolor="k", markersize=7, markeredgewidth=1.5, linewidth=1.5, color=qiskit_color, ) (qiskit2,) = plt.semilogy( qiskit_fidelities, qiskit_cx_counts, "-v", label="qiskit cx-counts", markerfacecolor=qiskit_color, markeredgecolor="k", markersize=8, markeredgewidth=1.5, linewidth=1.5, color=qiskit_color, ) first_legend = plt.legend( handles=[qiskit1, qiskit2], fontsize=16, loc="upper left", ) ax = plt.gca().add_artist(first_legend) plt.legend(handles=[classiq1, classiq2], fontsize=16, loc="lower right") # plt.ylim(0.2e3,2e5) plt.ylim(0.2e3, 3e5) plt.ylabel("Depth, CX-counts", fontsize=16) plt.xlabel(r"$\langle\hat{x}_{\rm cl}|\hat{x}_{\rm q}\rangle^2$", fontsize=16) plt.yticks(fontsize=16) plt.xticks(fontsize=16) ``` **Output:** ``` (array([0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1. , 1.1]), [Text(0.2, 0, '0.2'), Text(0.30000000000000004, 0, '0.3'), Text(0.4, 0, '0.4'), Text(0.5, 0, '0.5'), Text(0.6000000000000001, 0, '0.6'), Text(0.7, 0, '0.7'), Text(0.8, 0, '0.8'), Text(0.9000000000000001, 0, '0.9'), Text(1.0, 0, '1.0'), Text(1.1, 0, '1.1')]) ``` output # Oracle Generation for 3-SAT Problems Source: https://docs.classiq.io/explore/tutorials/technology_demonstrations/oracle_generation/3sat_oracles Open this notebook in GitHub to run it yourself This notebook demonstrates Classiq's capabilities in the framework of phase oracles. The focus is 3-SAT problems on a growing number of variables. To highlight the advantage of generation times, we skip transpilation for the synthesis output. The following utility functions generate random 3-SAT problems for $N$ boolean variables, consisting of $N$ clauses. ```python theme={null} import numpy as np from classiq.qmod.symbolic import logical_and, logical_not, logical_or def generate_permutation_for_3sat_expression(num_qubits, max_samples=1000): """ A function that generates two permutations on a list of num_qubits variables, for introducing a random and valid 3-SAT problem """ direct_arr = np.array([k for k in range(num_qubits)]) for k in range(max_samples): permut1 = np.random.permutation(num_qubits) permut2 = np.random.permutation(num_qubits) if ( (0 not in permut2 - direct_arr) and (0 not in permut1 - direct_arr) and (0 not in permut1 - permut2) ): break assert ( k < max_samples ), "Could not find a random 3-SAT problem, try to increase max_samples" return direct_arr, permut1, permut2 def generate_3sat_qbit_expression(vars, s0, s1, s2): """ A function that generates a 3-SAT problem on a list of QBit variables. The returned expression contains num_qubits=len(vars) clauses and contains triplets of the form (x_k or ~x_s1(k) or x_s2(k)), where s1, s2 are permutations. """ num_qubits = len(vars) k = 0 y = logical_or(logical_or(vars[s0[k]], logical_not(vars[s1[k]])), vars[s2[k]]) for k in range(1, num_qubits): temp = logical_or( logical_or(vars[s0[k]], logical_not(vars[s1[k]])), vars[s2[k]] ) y = logical_and(y, temp) return y ``` ## 1. Generating Phase Oracles For each 3-SAT problem we generate an oracle with Classiq and save the generation time, as well as the circuits' width. ```python theme={null} from classiq import * qmods = [] qprogs = [] def get_generation_time_classiq(s0, s1, s2, num_qubits): start_cl = time.time() @qfunc def main(qbv: Output[QArray]): def inner_call(aux: QNum): aux ^= generate_3sat_qbit_expression( [qbv[k] for k in range(num_qubits)], s0, s1, s2 ) allocate(num_qubits, qbv) aux = QNum("aux") allocate(1, aux) within_apply( lambda: (X(aux), H(aux)), lambda: inner_call(aux), ) free(aux) qmod = create_model(main) qmod = set_preferences(qmod, preferences=Preferences(transpilation_option="none")) qmods.append(qmod) qprog = synthesize(qmod) qprogs.append(qprog) return qprog.data.width, time.time() - start_cl ``` The following function generates a phase oracle with qiskit. ```python theme={null} def get_generation_time_qiskit(s0, s1, s2, num_qubits): start_qs = time.time() dict_of_qnums = {f"x{k}": QNum(f"x{k}") for k in range(num_qubits)} expression = str( generate_3sat_qbit_expression( [dict_of_qnums[f"x{k}"] for k in range(num_qubits)], s0, s1, s2 ) ) expression = expression.replace("or", "|") expression = expression.replace("not", "~") expression = expression.replace("and", "&") oracle = PhaseOracle(expression, var_order=None) q = QuantumRegister(num_qubits) qc = QuantumCircuit(q) qc.append(oracle, q[:]) return time.time() - start_qs ``` *For generating the same data with Qiskit please uncomment the commented lines (including the `pip install command`).* We work with qiskit version 1.0. 0. ```python theme={null} import time from qiskit import QuantumCircuit, QuantumRegister, transpile from qiskit.circuit.library import PhaseOracle ``` We skip generating data with Qiskit for $N>23$, as generation times exponentially diverge with the number of variables. ```python theme={null} np.random.seed(128) cl_times = [] num_qubits_list = [k for k in range(11, 23)] + [ int(l) for l in np.logspace(np.log2(24), np.log2(68), 11, base=2) ] ``` ```python theme={null} # from importlib.metadata import version # try: # import qiskit # if version('qiskit') != "1.0.0": # !pip uninstall qiskit -y # !pip install qiskit==1.0.0 # except ImportError: # !pip install qiskit==1.0.0 # ! pip install tweedledum # qs_times = [] ``` ```python theme={null} for l in num_qubits_list: num_qubits = l print("num_qubits:", num_qubits) s0, s1, s2 = generate_permutation_for_3sat_expression(num_qubits) cl_width, classiq_generation_time = get_generation_time_classiq( s0, s1, s2, num_qubits ) cl_times.append(classiq_generation_time) print("classiq_width:", cl_width, ", classiq_time:", classiq_generation_time) # if l<23: # qiskit_generation_time = get_generation_time_qiskit(s0, s1, s2, num_qubits) # qs_times.append(qiskit_generation_time) # print("qiskit_time:", qiskit_generation_time) ``` **Output:** ``` num_qubits: 11 classiq_width: 25 , classiq_time: 6.707828998565674 num_qubits: 12 classiq_width: 28 , classiq_time: 4.241983890533447 num_qubits: 13 classiq_width: 31 , classiq_time: 5.165003061294556 num_qubits: 14 classiq_width: 34 , classiq_time: 5.6608970165252686 num_qubits: 15 classiq_width: 37 , classiq_time: 9.164438009262085 num_qubits: 16 classiq_width: 34 , classiq_time: 5.097576856613159 num_qubits: 17 classiq_width: 38 , classiq_time: 5.640438079833984 num_qubits: 18 classiq_width: 38 , classiq_time: 5.6844401359558105 num_qubits: 19 classiq_width: 44 , classiq_time: 6.499827861785889 num_qubits: 20 classiq_width: 43 , classiq_time: 6.553148031234741 num_qubits: 21 classiq_width: 44 , classiq_time: 7.41901421546936 num_qubits: 22 classiq_width: 45 , classiq_time: 6.614210844039917 num_qubits: 24 classiq_width: 60 , classiq_time: 5.969420909881592 num_qubits: 26 classiq_width: 60 , classiq_time: 6.843206882476807 num_qubits: 29 classiq_width: 64 , classiq_time: 8.168700218200684 num_qubits: 32 classiq_width: 77 , classiq_time: 8.244300842285156 num_qubits: 36 classiq_width: 87 , classiq_time: 7.734289884567261 num_qubits: 40 classiq_width: 89 , classiq_time: 9.301125764846802 num_qubits: 44 classiq_width: 109 , classiq_time: 8.133974075317383 num_qubits: 49 classiq_width: 102 , classiq_time: 11.690325021743774 num_qubits: 55 classiq_width: 113 , classiq_time: 10.843430042266846 num_qubits: 61 classiq_width: 138 , classiq_time: 13.8468017578125 num_qubits: 67 classiq_width: 142 , classiq_time: 13.800977945327759 ``` ## 2. Plotting the Data Since generating the data takes time we hard-coded the Qiskit results in the notebook. If you run this notebook by yourself please comment out the following cell. ```python theme={null} qs_times = [ 0.2850170135498047, 2.6256730556488037, 0.75693678855896, 5.783859968185425, 3.3723957538604736, 3.9280269145965576, 39.92809295654297, 60.67643904685974, 16.551968097686768, 31.536834955215454, 31.086618900299072, 794.9081449508667, ] ``` ```python theme={null} import matplotlib.pyplot as plt classiq_color = "#D7F75B" qiskit_color = "#6FA4FF" plt.rcParams["font.family"] = "serif" plt.rc("savefig", dpi=300) plt.rcParams["axes.linewidth"] = 1 plt.rcParams["xtick.major.size"] = 5 plt.rcParams["xtick.minor.size"] = 5 plt.rcParams["ytick.major.size"] = 5 plt.rcParams["ytick.minor.size"] = 5 plt.loglog( [n for n in num_qubits_list if n < 23], qs_times, "s", label="qiskit", markerfacecolor=qiskit_color, markeredgecolor="k", markersize=7, markeredgewidth=1.5, linewidth=1.5, color=qiskit_color, ) plt.loglog( num_qubits_list, cl_times, "o", label="classiq", markerfacecolor=classiq_color, markeredgecolor="k", markersize=8.5, markeredgewidth=1.5, linewidth=1.5, color=classiq_color, ) plt.legend(fontsize=16, loc="upper right") plt.ylabel("generation time [sec]", fontsize=16) plt.xlabel("number of variables", fontsize=16) plt.yticks(fontsize=16) plt.xticks(fontsize=16) ``` **Output:** ``` (array([ 1., 10., 100., 1000.]), [Text(1.0, 0, '$\\mathdefault{10^{0}}$'), Text(10.0, 0, '$\\mathdefault{10^{1}}$'), Text(100.0, 0, '$\\mathdefault{10^{2}}$'), Text(1000.0, 0, '$\\mathdefault{10^{3}}$')]) ``` output # QAOA Source: https://docs.classiq.io/explore/tutorials/technology_demonstrations/qaoa/qaoa_demonstration Open this notebook in GitHub to run it yourself This notebook demonstrates the Classiq performance re. the Quantum Approximate Optimization Algorithm (QAOA), focusing on the Max Clique problem. ## 1. Calling the Built-In QAOA This section calls the built-in QAOA of Classiq, constructing the corresponding quantum model from a combinatorial optimization Pyomo model. # ## 1. 2. Generating a Pyomo Model ```python theme={null} import time import networkx as nx import numpy as np import pyomo.environ as pyo np.random.seed(2) def define_max_clique_model(graph): model = pyo.ConcreteModel() # each x_i states if node i belongs to the cliques model.x = pyo.Var(graph.nodes, domain=pyo.Binary) x_variables = np.array(list(model.x.values())) # define the complement adjacency matrix as the matrix where 1 exists for each non-existing edge adjacency_matrix = nx.convert_matrix.to_numpy_array(graph, nonedge=0) complement_adjacency_matrix = ( 1 - nx.convert_matrix.to_numpy_array(graph, nonedge=0) - np.identity(len(model.x)) ) # constraint that 2 nodes without an edge in the graph cannot be chosen together model.clique_constraint = pyo.Constraint( expr=x_variables @ complement_adjacency_matrix @ x_variables == 0 ) # maximize the number of nodes in the chosen clique model.value = pyo.Objective(expr=sum(x_variables), sense=pyo.maximize) return model ``` Setting a specific problem and some hyperparameters. ```python theme={null} QAOA_NUM_LAYERS = 10 NUM_SHOTS = 1e4 NUM_QUBITS = 7 graph = nx.erdos_renyi_graph(NUM_QUBITS, 0.6, seed=79) max_clique_model = define_max_clique_model(graph) ``` ```python theme={null} # transpilation_options = {"classiq": "custom", "qiskit": 3} transpilation_options = {"classiq": "auto optimize", "qiskit": 1} ``` # ## 1.2 Constructing, Synthesizing, and Running a QAOA Model ```python theme={null} from classiq import ( CustomHardwareSettings, Preferences, QuantumProgram, construct_combinatorial_optimization_model, execute, set_execution_preferences, set_preferences, show, synthesize, ) from classiq.applications.combinatorial_optimization import OptimizerConfig, QAOAConfig from classiq.execution import ExecutionPreferences qaoa_config = QAOAConfig(num_layers=QAOA_NUM_LAYERS) optimizer_config = OptimizerConfig(max_iteration=400, alpha_cvar=1) qmod = construct_combinatorial_optimization_model( pyo_model=max_clique_model, qaoa_config=qaoa_config, optimizer_config=optimizer_config, ) execution_preferences = ExecutionPreferences(num_shots=NUM_SHOTS) preferences = Preferences( custom_hardware_settings=CustomHardwareSettings(basis_gates=["cx", "u"]), transpilation_option=transpilation_options["classiq"], ) qmod = set_execution_preferences(qmod, execution_preferences) qmod = set_preferences(qmod, preferences=preferences) qprog = synthesize(qmod) res = execute(qprog).result() depth_classiq = qprog.transpiled_circuit.depth cx_counts_classiq = qprog.transpiled_circuit.count_ops["cx"] classiq_solving_time = res[0].value.time interations_classiq = [ intermediate_result.iteration_number for intermediate_result in res[0].value.intermediate_results ] results_classiq = [ -intermediate_result.mean_all_solutions for intermediate_result in res[0].value.intermediate_results ] ``` ## 2. Comparing to Qiskit We use qiskit version 1. 2. Qiskit has no module in which to specify a generic optimization problem; therefore, you have to do the preprocessing and post-processing yourself. Retrieve the Hamiltonian that enters into the VQE. ```python theme={null} from typing import List from classiq import Pauli as ClassiqPauli def get_classiq_hamiltonian(execution_result) -> List[List[str]]: hamiltonian_result = execution_result[1].value parsed_pauli_list = list() for pauli_term in hamiltonian_result: pauli_str = "".join(ClassiqPauli(pauli).name for pauli in pauli_term["pauli"]) coefficient = pauli_term["coefficient"] parsed_pauli_list.append([pauli_str, coefficient]) return parsed_pauli_list ``` Define a function for running QAOA on Qiskit and returning the results. \*\*Due to long runtime the code for generating the qiskit data is commented out and the results are hard-coded in the notebook. For running the full code please uncomment the code three cells below.\*\* ```python theme={null} qiskit_result = np.load("qiskit_res.npy") ``` ```python theme={null} max_classiq_iter = len(interations_classiq) ``` ```python theme={null} depth_qiskit = 1646 cx_counts_qiskit = 1660 iterations_qiskit = np.linspace(1, len(qiskit_result), len(qiskit_result)) ``` ```python theme={null} # from importlib.metadata import version # try: # import qiskit # if version('qiskit') != "1.0.0": # !pip uninstall qiskit -y # !pip install qiskit==1.0.0 # except ImportError: # !pip install qiskit==1.0.0 ``` ```python theme={null} # from qiskit import QuantumCircuit, transpile # from qiskit.primitives import Estimator, Sampler # from qiskit.quantum_info import Pauli, SparsePauliOp, Statevector # from qiskit.result import QuasiDistribution # from qiskit_algorithms.minimum_eigensolvers import QAOA # from qiskit_algorithms.optimizers import COBYLA # def qiskit_qaoa(hamiltonian, qaoa_num_layers, num_qubits): # """ # Gets a Hamiltonian for QAOA and num of quantum layers, returning the most probable solution and its corresponding cost # as well as intermediate results # """ # counts = [] # values = [] # def store_intermediate_result(eval_count, parameters, mean, std): # # callable to store results # counts.append(eval_count) # values.append(mean) # def objective_value(x, hamiltonian): # # get objective value for a given computational basis state # qc = QuantumCircuit(num_qubits) # for k in range(num_qubits): # if x[k] == 1: # qc.x(k) # estimated_cost = estimator.run(qc, hamiltonian).result().values[0] # return -estimated_cost # def bitfield(n: int, L: int) -> list[int]: # # binary representation as list # result = np.binary_repr(n, L) # return [int(digit) for digit in result] # [2:] to chop off the "0b" part # def sample_most_likely(state_vector: QuasiDistribution | Statevector) -> np.ndarray: # """Compute the most likely binary string from the state vector. # Args: # state_vector: State vector or quasi-distribution. # Returns: # Binary string as an array of ints. # """ # if isinstance(state_vector, QuasiDistribution): # values = list(state_vector.values()) # else: # values = state_vector # n = int(np.log2(len(values))) # k = np.argmax(np.abs(values)) # x = bitfield(k, n) # x.reverse() # return np.asarray(x) # estimator = Estimator(options={"shots": int(NUM_SHOTS)}) # sampler = Sampler() # optimizer = COBYLA() # qaoa = QAOA( # sampler, optimizer, reps=qaoa_num_layers, callback=store_intermediate_result # ) # start = time.time() # result = qaoa.compute_minimum_eigenvalue(hamiltonian) # solving_time = time.time() - start # x = sample_most_likely(result.eigenstate) # transpiled_circuit = transpile( # result.optimal_circuit, # basis_gates=["u", "cx"], # optimization_level=transpilation_options["qiskit"], # ) # return ( # transpiled_circuit.depth(), # transpiled_circuit.count_ops()["cx"], # solving_time, # x, # objective_value(x, hamiltonian), # counts, # values, # ) ``` The same QAOA with Qiskit ```python theme={null} # pauli_list = get_classiq_hamiltonian(res) # hamiltonian = SparsePauliOp.from_list(pauli_list) # ( # depth_qiskit, # cx_counts_qiskit, # time_qiskit, # most_probable_state, # cost, # iterations_qiskit, # results_qiskit, # ) = qiskit_qaoa(hamiltonian, QAOA_NUM_LAYERS, NUM_QUBITS) # qiskit_result = np.real(results_qiskit[0:max_classiq_iter]) # iterations_qiskit = iterations_qiskit[0:max_classiq_iter] ``` ## 3. Plotting the Data ```python theme={null} import matplotlib.pyplot as plt classiq_color = "#F43764" qiskit_color = "#6FA4FF" plt.rcParams["font.family"] = "serif" plt.rc("savefig", dpi=300) plt.rcParams["axes.linewidth"] = 1 plt.rcParams["xtick.major.size"] = 5 plt.rcParams["xtick.minor.size"] = 5 plt.rcParams["ytick.major.size"] = 5 plt.rcParams["ytick.minor.size"] = 5 qiskit_label = f"qiskit: depth={depth_qiskit} \n cx-counts={cx_counts_qiskit}" classiq_label = f"classiq: depth={depth_classiq},\n cx-counts={cx_counts_classiq}" plt.plot( iterations_qiskit, qiskit_result, "-", label=qiskit_label, linewidth=1.5, color=qiskit_color, ) plt.plot( interations_classiq[0:max_classiq_iter], results_classiq[0:max_classiq_iter], "-", label=classiq_label, linewidth=1.5, color=classiq_color, ) # plt.ylim(0,90) plt.ylabel("Energy", fontsize=16) plt.xlabel("Iteration", fontsize=16) plt.yticks(fontsize=16) plt.xticks(fontsize=16) plt.legend(loc="upper right", fontsize=16) ``` **Output:** ``` ``` output # High-Level Algorithm Design with Qmod Part I Source: https://docs.classiq.io/explore/tutorials/workshops/algo_design_QCE_tutorial/algo_design_QCE_tutorial_part_I Open this notebook in GitHub to run it yourself ## Language Concepts In this workshop we will learn to use high-level quantum programming language concepts to design quantum algorithms. We will use the Qmod language to model the functionality, and the Classiq platform to synthesize it into gate-level descriptions, visualize the circuits, and execute them. We will focus on high-level quantum types and expressions in different evaluation modes. Part I is a walk through Qmod's structure and constructs, as well as some of its unique high-level concepts. We will look closely at these constructs using small examples. In Part II we will combine some of these concepts in a complete quantum algorithm. There are 5 code exercises in this notebook, split into two sections, *A* and *B*. In each exercise follow the instructions - complete the code snippet where indicated by a *TODO* comment, execute the code, and try to understand the results. Solutions are provided at the end of the notebook. Don't continue to the next exercise before you completed the previous one and compared your code and results against the solution. ## Section A *(15 Minutes)* # ## Warmup: A First Qmod Program Let's start with a simple example to demonstrate the structure of Qmod code, as well as its synthesis and execution flow. We prepare and sample a **Bell state** - one of the most fundamental quantum states. Further reading: [Quantum Functions](https://docs.classiq.io/latest/qmod-reference/language-reference/functions/) ```python theme={null} from classiq import * @qfunc # This decorator declares a quantum function def create_bell_state(pair: QArray[QBit, 2]): H(pair[0]) CX(pair[0], pair[1]) @qfunc # Function 'main' is the entry point of our quantum program def main(res: Output[QArray[QBit, 2]]): allocate(res) create_bell_state(res) ``` Quantum functions in Qmod are defined using a regular Python function, decorated with **qfunc**, and their parameters must be declared with type hints. We can now compile with the SDK function `synthesize`. We get back an executable description called *quantum program* which we then execute on any simulation or quantum hardware. To manage the execution flow we use `ExecutionSession`. In our case, a simple sampling (using the default number of shots) of the quantum program will suffice. ```python theme={null} qprog = synthesize(main) show(qprog) # Visualize the quantum program for analysis # Execute and print the results with ExecutionSession(qprog) as es: res = es.sample() display(res.dataframe) ``` You should see in approximately 50% of the samples the bit vector 00 and in 50% the bit vector 11. Synthesis is the process of compiling a high-level description to a gate-level description. The reduction is presented graphically. The executable format can be simulated using various simulation engines, or executed on quantum hardware of choice. # ## Exercise 1: GHZ State Based on the Bell state example above, prepare a **GHZ state** with 3 qubits. The GHZ state creates maximum entanglement between three qubits: $(|000\rangle + |111\rangle)/\sqrt{2}$. ```python theme={null} from classiq import * @qfunc def create_ghz_state(qubits: QArray[QBit, 3]): # TODO: Apply GHZ logic pass @qfunc def main(res: Output[QArray[QBit, 3]]): allocate(res) create_ghz_state(res) # TODO: Synthesize the model, show, execute and print results # Hint: Follow the same pattern as the demonstration above ``` You should see in approximately 50% of the samples the bit vector 000 and in 50% the bit vector 111. # ## Exercise 2: GHZ with Numeric Variables Define a main function that calls the `create_ghz_state` (as implemented in Exercise 1) with a ***signed*** quantum number and outputs the results. Synthesize it, analyze the quantum program, execute it and print the results. What do you expect the resulting value of `x` to be? Further reading: [Quantum Types](https://docs.classiq.io/latest/qmod-reference/language-reference/quantum-types/) ```python theme={null} from classiq import * @qfunc def main(x: Output[QNum[3, SIGNED, 0]]): allocate(x) # TODO: Create GHZ state with QNum (gives 0 and -1) # Hint: Is it any different from the previous main implementation? pass qprog = synthesize(main) show(qprog) # Visualize the quantum program for analysis # Execute and print the results with ExecutionSession(qprog) as es: res = es.sample() display(res.dataframe) ``` You should see approximately 50% for 0 and 50% for - 1. In Qmod function arguments are automatically cast between **QNum** and **QArray\[QBit]** (and also between other quantum types). The value of quantum variables is interpreted based on their type. The state $|111\rangle$ represents 7 for an unsigned integer, and -1 as signed integer (in two's complement encoding). ## Section B *(30 Minutes)* # ## Exercise 3: Arithmetic Expressions with Automatic Type Inference # ### Part A: Numeric Type Inference Create and execute a quantum program that assigns a quantum arithmetic expression to a numeric variable: 1. Declare quantum numeric variables `a` and `b` as unsigned integers of size of 2. 3. Apply `hadamard_transform` on `a` and `b` (to put them in uniform superposition of all possible states). 4. Assign the value of `3*a + b` to `c`. 5. Synthesize, show, execute and print results. Inspect the printouts - what numeric attributes were inferred for variable `c`? Why? Further reading: [Numeric Assignment](https://docs.classiq.io/latest/qmod-reference/language-reference/statements/assignment/) ```python theme={null} from classiq import * @qfunc def main(a: Output[QNum[2]], b: Output[QNum[2]], c: Output[QNum]): allocate(a) allocate(b) # TODO: Put a and b in equal superposition # TODO: Assign the value of 3*a + b to c allocate(1, c) # Placeholder - replace with actual assignment # Print out c's inferred size in qubits print(f"The size of c is {c.size}") qprog = synthesize(main) show(qprog) # Visualize the quantum program for analysis # Execute and print the results with ExecutionSession(qprog) as es: res = es.sample() display(res.dataframe) ``` The expression result ranges between 0 and 1 2. To represent all values variable `c` must be an unsigned integer of size 4 qubits or more. In Qmod the size of numeric variables may be left unspecified. It is then automatically inferred to tightly fit all possible values of the expressions. # ### Part B: Numeric Type Inference with Fixed-Point Fractions Repeat *Part A*, only this time declaring `a` with 1 fraction digit and `b` with 2 fraction digits. How did the numeric attributes of `c` change? What are the corresponding sampled values for `c` in the result? ```python theme={null} from classiq import * @qfunc def main( a: Output[QNum[2, UNSIGNED, 1]], b: Output[QNum[2, UNSIGNED, 2]], c: Output[QNum] ): allocate(a) allocate(b) # TODO: Put a and b in equal superposition # TODO: Assign the value of 3*a + b to c # Hint: Is it any different from the previous main implementation? allocate(1, c) # Placeholder - replace with actual assignment # Print out the numeric attributes of the inferred type print("Numeric attributes of c:") print( f"size={c.size}, is_signed={c.is_signed}, fraction_digits={c.fraction_digits}" ) qprog = synthesize(main) show(qprog) # Visualize the quantum program for analysis # Execute and print the results with ExecutionSession(qprog) as es: res = es.sample() display(res.dataframe) ``` Variable `c` should be of size 5, unsigend, and with 2 fraction digits. This is the minimal type that covers the expression's domain. In Qmod, numeric variables can represent arbitrary fixed-point values. Arithemetic expression and type inference also accommodate for different decimal point locations. # ## Exercise 4: Conditional Operations Define a main function that initializes a quantum variable `x` (a 3-qubit signed number with 2 binary fraction digits) in an equal superposition of all states, then conditionally flips a single-qubit variable named `flag` when the value of x is less than 0. 1. Inspect the execution results - how is `flag` entangled with `x`? Further reading: [Control Statement](https://docs.classiq.io/latest/qmod-reference/language-reference/statements/control/) ```python theme={null} from classiq import * @qfunc def main(x: Output[QNum[3, SIGNED, 2]], flag: Output[QBit]): allocate(x) hadamard_transform(x) allocate(flag) # TODO : Flip the state of flag if x < 0.5 qprog = synthesize(main) show(qprog) # Visualize the quantum program for analysis # Execute and print the results with ExecutionSession(qprog) as es: res = es.sample() display(res.dataframe) ``` You should see that `x` is evenly distributed across the 8 values, and `flag` is flipped in 6 out of the 8 cases. The **control** statement in Qmod is similar to classical **if** statement, where a statement block is applied conditionally, depending on a Boolean expression, and optionally an else-block is applied otherwise. The difference is that the operations are applied in superposition, corresponding to the condition. # ## Exercise 5: Grover Algorithm Using Quantum Struct and Constant Phase The Grover search algorithm uses two kinds of conditional phase flips - one reflecting about the "good" states, and the other reflecting about the zero state. This is easy to express using fixed $\pi$ phase rotation under the required control condition. It is also convenient to encapsulate the problem variables in a quantum struct, so they can be passed around between functional units. Create a quantum program that finds assignments for $a, b$ and $c \in \{0, 1, 2, 3\}$ that satisfy the equation $3a + b + 2c = 9$. * Declare the variables as fields of a quantum struct * Define the phase oracle in terms of the problem condition * Define the zero-reflection in the diffuser Further reading: [Phase Statement](https://docs.classiq.io/latest/qmod-reference/language-reference/statements/phase/) ```python theme={null} from classiq import * from classiq.qmod.symbolic import pi class MyProblemVars(QStruct): # TODO: Declare the problem variables as quantum numeric fields dummy: QBit # Placeholder - replace with actual fields @qfunc def phase_oracle(v: MyProblemVars): # TODO: Apply a phase flip if the state of v satisfies the equation (use field access in the form `v.a` etc.) pass @qfunc def zero_reflection(state: QNum): # TODO: Apply a phase flip if state is |0> pass @qfunc def grover_operator(v: MyProblemVars): phase_oracle(v) hadamard_transform(v) zero_reflection(v) hadamard_transform(v) @qfunc def main(v: Output[MyProblemVars]): allocate(v) hadamard_transform(v) for i in range(2): grover_operator(v) qprog = synthesize(main) show(qprog) # Visualize the quantum program for analysis # Execute and print the results with ExecutionSession(qprog) as es: res = es.sample() display(res.dataframe) ``` There are 6 assignments to `a`, `b`, and `c` that satisfy the equation. They should be sampled with approximately equal probabilities. In Qmod, a fixed-value **phase** (specifically $\pi$) can be introduced under **control** condition. This can be used to apply a phase-flip to states described with a quantum expressions. # ## Exercise 6: Phase Arithmetic # ### Part A Compute the expression $x * y$ in the phase of their respective states. Use a coefficient to distribute all possible states over the $2\pi$ phase rotation. To actually view the phases of the states, simulate the program using state-vector simulation. Expand the quantum program visualization down to the gate-level implementation. How is the *phase* statement synthesized? Inspect the resulting phases of the different states in the printout. Do they match the phase expression over `x` and `y`? Further reading: [Phase Statement](https://docs.classiq.io/latest/qmod-reference/language-reference/statements/phase/) ```python theme={null} import numpy as np from classiq import * from classiq.execution import ClassiqBackendPreferences, ExecutionPreferences @qfunc def main(x: Output[QNum[2]], y: Output[QNum[2]]): allocate(x) allocate(y) # TODO: put x and y in uniform superposition # TODO: Encode x * y into the phase with a normalization coefficient to 2pi # Synthesize the model and show qprog = synthesize(main) show(qprog) # Specify execution preferences for state vector simulation preferences = ExecutionPreferences( num_shots=1, backend_preferences=ClassiqBackendPreferences(backend_name="simulator_statevector"), ) # Execute and print results: with ExecutionSession(qprog, preferences) as es: res = es.sample() display(res.dataframe) ``` The resulting state phases should show $x^2$ rotation of the respective computational-state value, modulo 8 (determined by the domain of variable `x`). The steps are a 1/8 of a full $2\pi$ rotation The **phase** statement can operate on an arbitrary polynomials over quantum numeric variables, introducing the corresponding Z rotations on the respective states. # ### Bonus: Fourier Arithmetic Create a quantum program that computes $y = x^2$ by computing $x^2$ in the Fourier basis. Then transform the result back to the computational basis. Inspect the execution results to validate the correctness of your algorithm. ```python theme={null} import numpy as np from classiq import * @qfunc def main(x: Output[QNum[3]], y: Output[QNum[4]]): allocate(x) hadamard_transform(x) allocate(y) # TODO: Use within_apply and function qft() to transform into and out of the Fourier basis # Synthesize the model and show qprog = synthesize(main) # show(qprog) # Synthesize the model, show, execute and print results with ExecutionSession(qprog) as es: res = es.sample() display(res.dataframe) ``` The **phase** statement can be used to implement modular arithmetic in the Fourier basis. ## Solutions # ## Solution 1: GHZ State ```python theme={null} from classiq import * @qfunc def create_ghz_state(qubits: QArray[QBit, 3]): # Apply GHZ logic H(qubits[0]) CX(qubits[0], qubits[1]) CX(qubits[1], qubits[2]) @qfunc def main(res: Output[QArray[QBit, 3]]): allocate(res) create_ghz_state(res) # Synthesize the model, show, execute and print results qprog = synthesize(main) show(qprog) with ExecutionSession(qprog) as es: res = es.sample() print("Ex.1 Results:") display(res.dataframe) ``` # ## Solution 2: GHZ with Quantum Numbers ```python theme={null} from classiq import * @qfunc def main(x: Output[QNum[3, SIGNED, 0]]): # Create GHZ state with QNum (gives 0 and -1) allocate(x) create_ghz_state(x) # Synthesize the model, show, execute and print results qprog = synthesize(main) show(qprog) with ExecutionSession(qprog) as es: res = es.sample() print("Ex.2 Results:") display(res.dataframe) ``` # ## Solution 3: Arithmetic Expressions with Automatic Type Inference # ### Part A: Numeric Type Inference ```python theme={null} from classiq import * @qfunc def main(a: Output[QNum[2]], b: Output[QNum[2]], c: Output[QNum]): allocate(a) allocate(b) # Put a and b in equal superposition hadamard_transform(a) hadamard_transform(b) # Assign the value of 3*a + b to c c |= 3 * a + b # Print out c's inferred size in qubits print(f"The size of c is {c.size}") # Synthesize the model, show, execute and print results qprog = synthesize(main) show(qprog) with ExecutionSession(qprog) as es: res = es.sample() print("Ex.3A Results:") display(res.dataframe) ``` # ### Part B: Numeric Type Inference with Fixed-Point Fractions ```python theme={null} from classiq import * @qfunc def main( a: Output[QNum[2, UNSIGNED, 1]], b: Output[QNum[2, UNSIGNED, 2]], c: Output[QNum] ): # Allocate a and b, then put in superposition allocate(a) allocate(b) hadamard_transform(a) hadamard_transform(b) # Assign the value of 3*a + b to c c |= 3 * a + b # Print out the numeric attributes of the inferred type print("Numeric attributes of c:") print( f"size: {c.size}, is_signed: {c.is_signed}, fraction_digits: {c.fraction_digits}" ) # Synthesize the model, show, execute and print results qprog = synthesize(main) show(qprog) with ExecutionSession(qprog) as es: res = es.sample() print("Ex.3B Results:") display(res.dataframe) ``` # ## Solution 4: Conditional Operations ```python theme={null} from classiq import * @qfunc def main(x: Output[QNum[3, SIGNED, 2]], flag: Output[QBit]): allocate(x) hadamard_transform(x) allocate(flag) # Flip the state of flag if x < 0.5 control(x < 0.5, lambda: X(flag)) # Flip the state of flag if x < 0.5 # Synthesize the model, show, execute and print results qprog = synthesize(main) show(qprog) with ExecutionSession(qprog) as es: res = es.sample() print("Ex.4 Results:") display(res.dataframe) ``` # ## Solution 5: Grover Algorithm Using Quantum Struct and Constant Phase ```python theme={null} from classiq import * from classiq.qmod.symbolic import pi class MyProblemVars(QStruct): # Declare the problem variables as quantum numeric fields a: QNum[2] b: QNum[2] c: QNum[2] @qfunc def phase_oracle(v: MyProblemVars): # Apply a phase flip if the state of v satisfies the equation (use field access in the form 'v.a' etc.) control(3 * v.a + v.b + 2 * v.c == 9, lambda: phase(pi)) @qfunc def zero_reflection(state: QNum): # Apply a phase flip if state is |0> control(state == 0, lambda: phase(pi)) @qfunc def grover_operator(v: MyProblemVars): phase_oracle(v) hadamard_transform(v) zero_reflection(v) hadamard_transform(v) @qfunc def main(v: Output[MyProblemVars]): allocate(v) hadamard_transform(v) for i in range(2): grover_operator(v) # Synthesize the model, show, execute and print results qprog = synthesize(main) show(qprog) with ExecutionSession(qprog) as es: res = es.sample() print("Ex.5 Results:") display(res.dataframe) ``` # ## Solution 6: Phase Arithmetic ```python theme={null} import numpy as np from classiq import * from classiq.execution import ClassiqBackendPreferences, ExecutionPreferences @qfunc def main(x: Output[QNum[2]], y: Output[QNum[2]]): allocate(x) allocate(y) # Put x and y in uniform superposition hadamard_transform(x) hadamard_transform(y) # Encode x * y into the phase with a normalization coefficient to 2pi phase(x * y, np.pi / 8) # Synthesize the model and show qprog = synthesize(main) show(qprog) # Specify execution preferences for state vector simulation preferences = ExecutionPreferences( num_shots=1, backend_preferences=ClassiqBackendPreferences(backend_name="simulator_statevector"), ) # Execute and print results with ExecutionSession(qprog, preferences) as es: res = es.sample() print("Ex.6A Results:") display(res.dataframe) ``` # ### Bonus ```python theme={null} import numpy as np from classiq import * @qfunc def main(x: Output[QNum[3]], y: Output[QNum[4]]): allocate(x) hadamard_transform(x) allocate(y) # Use within_apply and function qft() to transform into and out of the Fourier basis within_apply( lambda: qft(y), # Evaluates y += x**2 in the Fourier basis lambda: phase(y * (x**2), 2 * np.pi / (2**y.size)), ) # Synthesize the model, show, execute and print results qprog = synthesize(main) # show(qprog) with ExecutionSession(qprog) as es: res = es.sample() print("Ex.6B Results:") display(res.dataframe) ``` # High-Level Algorithm Design with Qmod Part II Source: https://docs.classiq.io/explore/tutorials/workshops/algo_design_QCE_tutorial/algo_design_QCE_tutorial_part_II Open this notebook in GitHub to run it yourself ## QAOA Knapsack In Part I we looked at specific high-level language concepts, their meaning and use. In this part, we apply these concepts to design a full algorithm. We will consider alternative ways to implement the Quantum Approximate Optimization Algorithm (QAOA). In particular, we will compare two approaches to expressing the hard constraint of the knapsack problem - using a penalty term in phase rotations and using a digital (computational-basis) conditional. The goal of the exercise is to demonstrate the use of quantum expressions in different modes, and their combination. It is not meant as a comprehensive study of hard constraints in QAOA, nor does it consider many other factors that determine QAOA performance and results (a rich and active field of research). There are 3 code exercises in this notebook with the corresponding heading and a code snippet containing `TODO` comments. Make sure to complete the code in the snippets before running the cell. Solutions are provided at the end of the notebook. Don't continue to the next exercise until you have completed the previous one and compared against the solution. ## Warm-Up: QAOA Pattern in Qmod (Max-Cut Example) We start by reviewing a simple demonstration of the QAOA algorithm. It solves a trivial case of the max-cut problem (but can easily be generalized to arbitrary graphs). The purpose of this code is just to get acquainted with the Qmod implementation, and define a couple of the building blocks that we shall reuse later. ```python theme={null} from classiq import * NUM_LAYERS = 3 # Python function encapsulating the cost expression (reused in ansatz and classical optimizer loop) def maxcut_cost(v: QArray[QBit, 3]): # Toy graph with 3 nodes and 2 edges graph_edges = [(0, 1), (0, 2)] return -sum(v[n1] ^ v[n2] for (n1, n2) in graph_edges) @qfunc def cost_layer(v: QArray[QBit], gamma: CReal): phase(maxcut_cost(v), gamma) @qfunc def init_layer(v: QArray[QBit]): apply_to_all(lambda q: H(q), v) @qfunc def mixer_layer(v: QArray[QBit], beta: CReal): apply_to_all(lambda q: RX(beta, q), v) @qfunc def main( params: CArray[CReal, NUM_LAYERS * 2], v: Output[QArray[QBit, 3]], ): gammas = params[0:NUM_LAYERS] betas = params[NUM_LAYERS : 2 * NUM_LAYERS] allocate(v) init_layer(v) for i in range(NUM_LAYERS): cost_layer(v, gammas[i]) mixer_layer(v, betas[i]) qprog = synthesize(main) show(qprog) ``` To execute the algorithm we use method `ExecutionSession.minimize` which uses gradient descent to optimize the parameters of the ansatz. The cost function is defined as the expectation value of the cost operator, which is defined in the `maxcut_cost` function above. The initial parameters are set to a linear schedule, which is a common heuristic for QAOA. ```python theme={null} import numpy as np # Start with a linear scheduling guess INIT_GAMMAS = [0, np.pi / 2, np.pi] INIT_BETAS = [np.pi, np.pi / 2, 0] initial_params = INIT_GAMMAS + INIT_BETAS with ExecutionSession(qprog) as es: trace = es.minimize( cost_function=lambda v: maxcut_cost(v), initial_params={"params": initial_params}, max_iteration=40, ) res = es.sample(parameters=trace[-1][1]) display(res.dataframe) ``` The optimal solution to this trivial max-cut problem is the partition into node sets \{0} and \{1, 2}. The QAOA cost layer for graph problems can be expressed directly in Qmod using *phase* statement with a bitwise expression. ## The Knapsack Problem The general definition of the knapsack problem is the following: Given a set of items, determine how many items to put in the knapsack to maximize their summed value. * **Input:** * Item count of each kind $x_i$, where $x_i \in [0, d_i]$ . * Item weights, denoted as $w_i$. * Item values, denoted as $v_i$. * Weight constraint $C$. * **Output:** * Item assignment $\overline{x}$ that maximizes the value: $\max_{x_i \in D} \Sigma_i v_i x_i$ subject to a weight constraint: $\Sigma_i w_i x_i\leq C$. * The *feasible value* for a given assignment is the value sum *if* the constraint is satisfied *and otherwise zero*. The knapsack is known to be an NP-complete problem. Here we choose a small toy instance: * 2 item types: * $a \in [0, 7]$ with $w_a=2$, $v_a=3$ * $b \in [0, 3]$ with $w_b=3$, $v_b=5$ * $C=12$ The optimal solution for the problem is (spoiler alert!) $a=3, b=2$ ## *Exercise 1*: Modeling the Knapsack Problem In this exercise we will model the knapsack problem in Qmod. * Use quantum numeric variables as fields of a quantum struct to represent the item counts. * Use quantum expressions to represent the value and weight, as well as the constraint. ```python theme={null} class KnapsackVars(QStruct): # TODO: Modify the declaration of problem variables 'a' and 'b' to the appropriate quantum type a: QBit b: QBit def value_sum(v: KnapsackVars): # TODO: Return the value sum expression return 0 # Remove this line (placeholder to avoid syntax error) def weight_sum(v: KnapsackVars): # TODO: Return the weight sum expression return 0 # Remove this line (placeholder to avoid syntax error) def constraint(v: KnapsackVars): # TODO: Use weight_sum() to return the Boolean constraint expression return 0 # Remove this line (placeholder to avoid syntax error) def feasible_value(v: KnapsackVars): return value_sum(v) if constraint(v) else 0 # A little hack to test our conditions.. class dotdict(dict): __getattr__ = dict.get print(feasible_value(dotdict(a=3, b=2))) print(feasible_value(dotdict(a=3, b=3))) ``` The assignment a=3 and b=2 is the optimal solution for our toy model with value 19, but any violation of the constraint is value 0. Having defined the variables using Qmod types, the problem can be captured directly in Qmod expressions. ## Execution Utility Function You can skip this section if you are not interested in the details of the execution flow we shall subsequently use. They are not important for our exercise. With the above general problem description we can define a helper function that optimizes the anasatz parameters for our knapsack problem and displays the results. The flow is not different from the example we saw above. , but note that we optimize on a function that reflects a linear gradiant for both the knapsack value and the violation of the constraint. Based on the optimized parameters, we sample the ansatz and calculate the probability of samples that satisfy the constraint, the average value of the feasible samples, and the overall value average. We will compare the different ansatz structures, keeping all other parameters fixed, and specifically using the same logic to obtain these results. ```python theme={null} from matplotlib import pyplot as plt from classiq.qmod.symbolic import max as qmod_max def optimize_qaoa_params(ansatz, max_iteration): with ExecutionSession(ansatz) as es: # Optimize the parameters to minimize cost function value def opt_cost_function(v): return -value_sum(v) + 4 * qmod_max(weight_sum(v) - 12, 0) opt_trace = es.minimize( cost_function=opt_cost_function, initial_params={"params": initial_params}, max_iteration=max_iteration, ) final_params = opt_trace[-1][1] cost_trace = [c[0] for c in opt_trace] # Plot the cost convergence plt.plot(cost_trace) plt.xlabel("Iterations") plt.ylabel("Cost") plt.title("Cost convergence") plt.show() print_statistics(es.sample(parameters=final_params)) return final_params def print_statistics(res): feas = [s for s in res.parsed_counts if constraint(s.state["v"])] feas_shots = sum(s.shots for s in feas) val_sum = sum(value_sum(s.state["v"]) * s.shots for s in feas) avg_val_sum = sum(feasible_value(s.state["v"]) * s.shots for s in res.parsed_counts) print(f"Probability of feasible solution: {feas_shots / res.num_shots:.4f}") print(f"Average feasible values: {val_sum / feas_shots if feas_shots else 0:.4f}") print(f"Overall score: {avg_val_sum / res.num_shots:.4f}") def sample_anzatz(anzatz, params, num_shots): with ExecutionSession(anzatz, ExecutionPreferences(num_shots=num_shots)) as es: res = es.sample(parameters=params) res.dataframe["feasible value"] = res.dataframe.apply( lambda row: feasible_value(dotdict(a=row["v.a"], b=row["v.b"])), axis=1 ) return res ``` ## Representing Hard Constraints as Cost The cost operator in QAOA is typically defined in terms of QUBO expressions, or more generally low-degree polynomials over problem variables. In Qmod this is directly expressible using the *phase* statement. But logical operators are not allowed in the *phase* expression, because they cannot be reduced to low-degree polynomials. The feasible value defined by the problem is the value sum *if* the constraint is satisfied *and otherwise zero*. An expression of this form cannot be used directly in *phase* statement. A common approach is to add to the overall cost a penalty term for violating the constraint. For equality constraints we can square the difference between the variable expression and its target to obtain a non-negative penalty. However, in the knapsack case we need to represent an *inequality* constraint. An inequality expression can be rewritten as an equality by introducing a *non-negative* slack ("don't-care") variable that represents the difference between the left and right sides of the inequality. When evaluating an assignment the slack variable is disregarded. As an example, the constraint $x + y \leq 10$ can be expressed as $x + y + slack = 10$. In this case the penalty will be $(x + y + slack - 10)^2$. The cost layer multiplies the penalty term can be given more significance by multiplying it by a constant "penalty factor", e.g. $2*(x + y + slack - 10)^2$. The penalty approach has the downside of not representing the true semantics of the problem. It incentivises assignments that have high value with only a small violation of the constraint. But our problem is defined in terms of a hard constraint, where even a small violation means zero actual value. In *Exercise 2* we will define the cost operator in terms of the constraint penalty term, and in *Exercise 3* we will define it to accurately capture the Boolean condition. ## Exercise 2: The Knapsack Constraint as a Penalty Term In this exercise we will implement the knapsack problem using a penalty term to represent the constraint. * Use the fields of struct `KnapsackVarsPenalty` to express the penalty term. * Define the cost layer in terms of the value sum and the penalty term. * Execute the algorithm and observe the results: * What is the probability of a feasible sample? What is the average value of the feasible samples? * How many iterations did it take to converge to a solution? ```python theme={null} class KnapsackVarsPenalty(QStruct): a: QNum[3] b: QNum[2] slack: QNum[4] def constraint_slack_penalty(v): # TODO: Return the slack penalty expression pass # Remove this line (placeholder to avoid syntax error) @qfunc def cost_layer(v: KnapsackVarsPenalty, gamma: CReal): # TODO: Define the cost phase shift in terms of value_sum() and constraint_slack_penalty() pass @qfunc def main( params: CArray[CReal, NUM_LAYERS * 2], v: Output[KnapsackVarsPenalty], ): allocate(v) init_layer(v) for i in range(NUM_LAYERS): cost_layer(v, params[i]) mixer_layer(v, params[NUM_LAYERS + i]) qprog = synthesize(main) show(qprog) final_params = optimize_qaoa_params(qprog, max_iteration=50) res = sample_anzatz(qprog, params=final_params, num_shots=10) display(res.dataframe) ``` The *overall score* is the expectation value of assignments, computed on a large sample. The table shows a small sample from the same distribution, with assignments and their value. Value 0 typically means a violation of the constraint. The cost function, including the penalty for the violation of an inequality constraint can be expressed in Qmod directly as a second degree polynomial. ## Exercise 3: The Knapsack Constraint as a Boolean Condition In this exercise we will implement the knapsack problem using a Boolean condition to represent the constraint. * Use the fields of struct `KnapsackVars` (from *Exercise 1*) to express the cost layer as a phase shift for the value sum, conditioned on the constraint. * Look at the quantum program visualization and note the structure of the cost layer. Compare the implementation of the two expression forms. * Execute the algorithm and inspect the results: * How does the probability of a feasible sample and the average value compare to the penalty-term approach? * How many iterations did it take to converge to a solution with this ansatz structure? ```python theme={null} @qfunc def cost_layer(v: KnapsackVars, gamma: CReal): # TODO: Define the cost as phase shift by value_sum(), under the condition that the constraint is satisfied pass @qfunc def main( params: CArray[CReal, NUM_LAYERS * 2], v: Output[KnapsackVars], ): allocate(v) init_layer(v) for i in range(NUM_LAYERS): cost_layer(v, params[i]) mixer_layer(v, params[NUM_LAYERS + i]) qprog = synthesize(main) show(qprog) final_params = optimize_qaoa_params(qprog, max_iteration=50) res = sample_anzatz(qprog, params=final_params, num_shots=10) display(res.dataframe) ``` The optimizer converges with fewer iterations using the more accurate ansatz, compared to the one in *Exercise 2*. The expectation value of the distribution is often also improved, but this varies and is sensitive to the random decisions of the optimizer. The QAOA cost layer in Qmod can comprise computational-basis conditions and polynomial evaluation in the phase, which together represent the true semantics of the problem. Note that phase oracle in Grover search (as shown in Part I, Exercise 5) can be seen as a special case of such a QAOA cost layer, with only a Boolean condition and no phase preference between the "good" states. ## Solutions # ## Solution to Exercise 1 ```python theme={null} class KnapsackVars(QStruct, dict): # Declare the problem variables 'a' and 'b' as quantum numeric variables a: QNum[3] b: QNum[2] def value_sum(v: KnapsackVars): # Return the value expression return v.a * 3 + v.b * 5 def weight_sum(v: KnapsackVars): # Return the total weight expression return v.a * 2 + v.b * 3 def constraint(v: KnapsackVars): # Use weight_sum() to return the Boolean constraint expression return weight_sum(v) <= 12 def feasible_value(v: KnapsackVars): return value_sum(v) if constraint(v) else 0 class dotdict(dict): __getattr__ = dict.get print(feasible_value(dotdict(a=3, b=2))) print(feasible_value(dotdict(a=3, b=3))) ``` # ## Solution to Exercise 2 ```python theme={null} class KnapsackVarsPenalty(QStruct): a: QNum[3] b: QNum[2] slack: QNum[4] PENALTY_FACTOR = 2 def constraint_slack_penalty(v): # Return the slack penalty expression return PENALTY_FACTOR * (weight_sum(v) + v.slack - 12) ** 2 @qfunc def cost_layer(v: KnapsackVarsPenalty, gamma: CReal): # Define the cost phase shift using value_sum and constraint_slack_penalty phase(-value_sum(v) + constraint_slack_penalty(v), gamma) @qfunc def main( params: CArray[CReal, NUM_LAYERS * 2], v: Output[KnapsackVarsPenalty], ): allocate(v) init_layer(v) for i in range(NUM_LAYERS): cost_layer(v, params[i]) mixer_layer(v, params[NUM_LAYERS + i]) qprog = synthesize(main) # show(qprog) final_params = optimize_qaoa_params(qprog, max_iteration=50) res = sample_anzatz(qprog, params=final_params, num_shots=10) display(res.dataframe) ``` # ## Solution to Exercise 3 ```python theme={null} @qfunc def cost_layer(v: KnapsackVars, gamma: CReal): # Define the cost as phase shift by value_sum(), under the condition that the constraint is satisfied control(constraint(v), lambda: phase(-value_sum(v), gamma)) @qfunc def main( params: CArray[CReal, NUM_LAYERS * 2], v: Output[KnapsackVars], ): allocate(v) init_layer(v) for i in range(NUM_LAYERS): cost_layer(v, params[i]) mixer_layer(v, params[NUM_LAYERS + i]) qprog = synthesize(main) # show(qprog) qprog = synthesize(main) final_params = optimize_qaoa_params(qprog, max_iteration=50) res = sample_anzatz(qprog, params=final_params, num_shots=10) display(res.dataframe) ``` # Combinatorial Optimization Workshop Using the Qmod Quantum Types - Part 1 Source: https://docs.classiq.io/explore/tutorials/workshops/combinatorial_workshop/combinatorial_qmod_workshop_for_maxcut Open this notebook in GitHub to run it yourself In this workshop, we will solve the MaxCut problem using qmod's quantum types, such as `QBit`, and the phase function. First, we will have introduction to the Maximum Cut (MaxCut) problem. ## Guidance for the Workshop: **The `# TODO` is there for you to do yourself.** \*\*The `# Solution start` and `# Solution end` are only for helping you. Please delete the `Solution` and try doing it yourself...\*\* ## Introduction The "Maximum Cut Problem" (MaxCut) \[[1](#maxcutwiki)] is an example of combinatorial optimization problem. It refers to finding a partition of a graph into two sets, such that the number of edges between the two sets is maximal. This optimization problem is the cardinal example in the context of Quantum Approximate Optimization Algorithm \[[2](#qaoa)], since it is an unconstrained problem whose objective function in terms quantum gates can be derived easily. With the Classiq platform, things are even simpler, as the objective function is inserted in its arithmetic form. ## Mathematical Formulation Given a graph $G=(V,E)$ with $|V|=n$ nodes and $E$ edges, a cut is defined as a partition of the graph into two complementary subsets of nodes. In the MaxCut problem we are looking for a cut where the number of edges between the two subsets is maximal. We can represent a cut of the graph by a binary vector $x$ of size $n$, where we assign 0 and 1 to nodes in the first and second subsets, respectively. The number of connecting edges for a given cut is simply given by summing over $x_i (1-x_j)+x_j (1-x_i)$ for every pair of connected nodes $(i,j)$. ## Solving with the Classiq Platform We go through the steps of solving the problem with the Classiq platform, using QAOA algorithm \[[2](#qaoa)]. ```python theme={null} import math import matplotlib.pyplot as plt import networkx as nx import numpy as np from scipy.optimize import minimize from classiq import * ``` # ## Creating and Plotting the Graph, the MaxCut Problem ```python theme={null} graph_nodes = [0, 1, 2, 3, 4] graph_edges = [(0, 1), (0, 2), (1, 2), (1, 3), (2, 4), (3, 4)] G = nx.Graph() G.add_nodes_from(graph_nodes) G.add_edges_from(graph_edges) pos = nx.planar_layout(G) nx.draw_networkx(G, pos=pos, with_labels=True, alpha=0.8, node_size=500) ``` output ## Create the Classical Optimization Function First, build a function that whether to count an edge or not. If the two nodes are from different groups, the function `edge_cut` will return 1, otherwise, it should return 0. The values of the nodes are binary. ```python theme={null} def edge_cut(node1_group_bit, node2_group_bit): # TODO: This function should return 1 if the two nodes are in different groups, and 0 if they're in the same group. # The n_bit is a bit that specifies 0 or 1 - depending on which group the node is in. # Your code # Solution start return node1_group_bit * (1 - node2_group_bit) + node2_group_bit * ( 1 - node1_group_bit ) # Solution end ``` Implement a function that returns the total cost for a cut on the graph. ```python theme={null} def maxcut_cost(v: QArray[QBit]): # TODO: Your code should loop through the graph edges and # sum over the edge cut using the edge_cut function. # You should return the negative cost. # Because at the optimization, we will use a minimization in the optimization process. # Your code # Solution start return -sum(edge_cut(v[node1], v[node2]) for (node1, node2) in G.edges) # Solution end ``` # ## Generic Building Blocks for QAOA Circuit The mixer layer will apply `RX` gates with parameter beta to all qubits in the `qba` array. ```python theme={null} @qfunc def mixer_layer(beta: CReal, qba: QArray[QBit]): # TODO: Apply on all qubits the RX gate with the beta rotation angle. # You can use apply_to_all or with repeat # Your code # Solution start apply_to_all(lambda q: RX(beta, q), qba) # Solution end ``` # ## The Phase Function In the QAOA ansatz, the cost layer gives a phase to any solution type. Therefore, we will use the `phase` function which takes an expression (`phase_expr`) and a parameter (`coefficient`) as arguments. See more [here](https://docs.classiq.io/latest/qmod-reference/language-reference/statements/phase/). For example: ``` phase( phase_expr=maxcut_cost(v), coefficient=gamma ) ``` The `phase` function rotates each computational basis state about the Z axis with the angle `coefficient` relative the value of the expression `phase_expr`. Namely, it adds a phase to each quantum state $|x\rangle \rightarrow e^{f(x_1,x_2,...,x_n)}|x\rangle$ with respect to the value that $f(x_1,x_2,...,x_n)$ returns. The quantum variables $x_1,x_2,...,x_n$ constitute the quantum state $|x\rangle$. # ## Problem Specifc Building Blocks ```python theme={null} # Define the number of layers you want. It is a heuristic decision, as the number of layers increases, the solution quality increases. NUM_LAYERS = 4 # In the main function you will create the ansatz. # The qaoa ansatz have 2 parts: # 1. Apply a H gate to all qubits in the `v` register # 2. Create N number of layers. # Each layer will apply a cost layer using the `phase` function, # this will take gammas as argument # and `mixer_layer()` this will take betas as argument. @qfunc def main( params: CArray[CReal, NUM_LAYERS * 2], v: Output[QArray[QBit]], ): # TODO: # Allocate the number of qubits in the circuit. # What should be the number of qubits? # Apply the initial initialization of QAOA # Apply repeatedly the alternating sequence of operators: # 1. The cost layer with the gamma parameter using the `phase` function. # 2. The mixer layer with the beta parameter. # Note that for the optimizer, it doesn't matter which parameter is gamma and which beta # Your code # Solution start allocate(len(G.nodes), v) hadamard_transform(v) repeat( count=int(params.len / 2), iteration=lambda i: ( phase(maxcut_cost(v), params[2 * i]), mixer_layer(params[2 * i + 1], v), ), ) # Solution end ``` # ## Synthesizing and Visualizing ```python theme={null} qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/2ygfaHFi4lcAFyuwYn3X64Vr6wC ``` ## Execution and Post Processing For the hybrid execution, we use `ExecutionSession`, which can evaluate the circuit in multiple methods, such as sampling the circuit, giving specific values for the parameters, and evaluating to a specific Hamiltonian, which is very common in chemical applications. In QAOA, we will use the `estimate_cost` method, which samples the cost function and returns their average cost from all measurements. That helps to optimize easily. ```python theme={null} NUM_SHOTS = 1000 # The number of shots is also a heuristic decision. The more shots, the more probability to get the right solution, especially when the number of qubits is large. ES = ExecutionSession( qprog, execution_preferences=ExecutionPreferences(num_shots=NUM_SHOTS) ) ``` A good initialization of QAOA is linearly approach the $\gamma$ values from 0 to 1 and the $\beta$ values from 1 to 0. This approach showed better solutions because it is similar to the adiabatic evolution with big steps rather than small evolution. Some suggest the value 0.75 bring better solutions. ```python theme={null} # TODO: # Build `initial_params` list of np.array type. # The gamma values should start from 0 and, in each layer, should approach closer to 1 linearly # The beta values should start from 1 and in each layer, should approach closer to 0 linearly # Then unify it to one list so scipy minimize can digest it. # Your code # Solution start def initial_qaoa_params(NUM_LAYERS) -> np.ndarray: initial_gammas = np.linspace(0, 1, NUM_LAYERS) initial_betas = np.linspace(1, 0, NUM_LAYERS) initial_params = [] for i in range(NUM_LAYERS): initial_params.append(initial_gammas[i]) initial_params.append(initial_betas[i]) return np.array(initial_params) # Solution end initial_params = initial_qaoa_params(NUM_LAYERS) ``` Record the steps of the optimization. ```python theme={null} intermediate_params = [] objective_values = [] ``` Build the classical cost function that take a single state and return its cut. ```python theme={null} cost_func = lambda state: maxcut_cost(state["v"]) ``` Use the `ExecutionSession` to calculate the cost of all measurements at once using the `estimate_cost` method. ```python theme={null} def estimate_cost_func(params) -> float: obj_value = ES.estimate_cost( cost_func=cost_func, parameters={"params": params.tolist()} ) objective_values.append(obj_value) return obj_value ``` In the QAOA algorithm, the classical cost function is converted to a Hamiltonian and the `estimate_cost` method give the $ \langle H_C\{\displaystyle \rangle \}$. But you can define anything with the `estimate_cost`, not necessarily the expectation value of the Hamiltonian. Define the callback function to store the intermediate parameters or any intermediate parameter you wish. ```python theme={null} def callback(xk): intermediate_params.append(xk) ``` Now, make the optimization part using the `minimize` function from `scipy`. You need to combine the objective function, which includes the quantum program, the classical optimizer, and the `callback` function. To achieve better convergence, you need to define the type of optimizer (such as `COBYLA`), the number of iterations, and other parameters, depending on the type of optimizer. ```python theme={null} optimization_res = minimize( estimate_cost_func, x0=initial_params, method="COBYLA", callback=callback, options={"maxiter": 40}, ) ``` The `maxiter` is the number of iterations. As the number of layers is larger, we need more iterations to converge to a good solution because there are more parameters. After we finish the optimization and find good parameters, we will use them once again to find the optimized solution. ```python theme={null} res = ES.sample({"params": optimization_res.x.tolist()}) ``` ```python theme={null} print(f"Optimized parameters: {optimization_res.x.tolist()}") sorted_counts = sorted(res.parsed_counts, key=lambda pc: maxcut_cost(pc.state["v"])) for sampled in sorted_counts: v = sampled.state["v"] print( f"solution={sampled.state['v']} probability={sampled.shots/NUM_SHOTS} cost={maxcut_cost(sampled.state['v'])}" ) ``` **Output:** ``` Optimized parameters: [0.385868970608936, 0.8721911218564776, 0.5847145604691053, 0.6640882010419554, 0.9694404571883897, 0.5148214977396823, 2.15158726630142, -0.11433716659702076] solution=[0, 1, 0, 0, 1] probability=0.218 cost=-5 solution=[1, 0, 1, 1, 0] probability=0.207 cost=-5 solution=[0, 0, 1, 1, 0] probability=0.183 cost=-5 solution=[1, 1, 0, 0, 1] probability=0.178 cost=-5 solution=[0, 1, 1, 0, 0] probability=0.044 cost=-4 solution=[1, 0, 0, 1, 1] probability=0.041 cost=-4 solution=[0, 1, 1, 1, 0] probability=0.033 cost=-4 solution=[1, 0, 0, 0, 1] probability=0.022 cost=-4 solution=[1, 0, 0, 1, 0] probability=0.022 cost=-4 solution=[0, 1, 1, 0, 1] probability=0.022 cost=-4 solution=[1, 0, 1, 0, 0] probability=0.002 cost=-3 solution=[1, 1, 0, 0, 0] probability=0.002 cost=-3 solution=[0, 0, 1, 0, 1] probability=0.001 cost=-3 solution=[1, 1, 0, 1, 0] probability=0.001 cost=-3 solution=[0, 1, 0, 1, 0] probability=0.001 cost=-3 solution=[1, 0, 1, 0, 1] probability=0.001 cost=-3 solution=[1, 1, 1, 0, 1] probability=0.004 cost=-2 solution=[0, 0, 0, 0, 1] probability=0.003 cost=-2 solution=[0, 1, 1, 1, 1] probability=0.003 cost=-2 solution=[0, 0, 0, 1, 1] probability=0.003 cost=-2 solution=[1, 1, 1, 1, 0] probability=0.002 cost=-2 solution=[0, 0, 0, 1, 0] probability=0.002 cost=-2 solution=[1, 0, 0, 0, 0] probability=0.002 cost=-2 solution=[1, 1, 1, 0, 0] probability=0.002 cost=-2 solution=[1, 1, 1, 1, 1] probability=0.001 cost=0 ``` # ## Plot the Result Let's plot the result of the best found solution. ```python theme={null} colors = [ "r" if sorted_counts[0].state["v"][i] == 0 else "g" for i in range(len(sorted_counts[0].state["v"])) ] nx.draw_networkx( G, pos=pos, with_labels=True, alpha=0.8, node_size=500, node_color=colors ) ``` output ## Plot the Convergence Graph ```python theme={null} # TODO: # Show the optimization progress to see if you converged to a good solution # You code # Solution start plt.plot(objective_values) plt.xlabel("Iteration") plt.ylabel("Objective Value") plt.title("Optimization Progress") plt.show() # Solution end ``` output ## Changes to Play With 1. Change the type of the optimizer from the scipy [minimize](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html) function. 2. Each optimizer has another type step size. For example, in the COBYLA optimizer, it is called `rhobeg`. The step size determines how far the optimizer will go in each iteration. You can change it to see how it affects the convergence. 1. Do the same optimization process for a random [Erdos Renyi graph](https://networkx.org/documentation/stable/reference/generated/networkx.generators.random_graphs.erdos_renyi_graph.html). 2. Build a weighted graph in which each edge has a different weight, and solve the maxcut with it. This type of graphs are more realistic for real-life applications and also NP-hard. ## References \[1]: [Maximum Cut Problem (Wikipedia)](https://en.wikipedia.org/wiki/Maximum_cut) \[2]: [Farhi, Edward, Jeffrey Goldstone, and Sam Gutmann. "A quantum approximate optimization algorithm." arXiv preprint arXiv:1411.4028 (2014).](https://arxiv.org/abs/1411.4028) \[3]: [Barkoutsos, Panagiotis Kl, et al. "Improving variational quantum optimization using CVaR." Quantum 4 (2020): 256.](https://arxiv.org/abs/1907.04769) # Estimating European Option Price Using Amplitude Estimation - Workshop Source: https://docs.classiq.io/explore/tutorials/workshops/finance_workshops/Option_Pricing_Workshop Open this notebook in GitHub to run it yourself ## Introduction and Background In Finance models we are often interested in calculating the average of a function of a given probability distribution ($E[f(x)]$). The most popular method to estimate the average is Monte Carlo \[[1](#mcmf)] due to its flexibility and ability to generically handle stochastic parameters. Classical Monte Carlo methods, however, generally require extensive computational resources to provide an accurate estimation. By leveraging the laws of quantum mechanics, a quantum computer may provide novel ways to solve computationally intensive financial problems, such as risk management, portfolio optimization, and option pricing. The core quantum advantage of several of these applications is the Amplitude Estimation algorithm \[[2](#aea)] which can estimate a parameter with a convergence rate of $\Omega(1/M^{2})$, compared to $\Omega(1/M)$ in the classical case, where $M$ is the number of Grover iterations in the quantum case and the number of the Monte Carlo samples in the classical case. This represents a theoretical quadratic speed-up of the quantum method over classical Monte Carlo methods! # ## Option Pricing An option is the possibility to buy (call) or sell (put) an item (or share) at a known price - the strike price (K), where the option has a maturity price (S). The payoff function to describe for example a European call option will be: $$ f(S)=\ \Bigg\{\begin{array}{lr} 0, & \text{when } K\geq S\\ S - K, & \text{when } K < S\end{array} $$ The maturity price is unknown. Therefore, it is expressed by a price distribution function, which may be any type of a distribution function. For example a log-normal distribution: $\mathcal{ln}(S)\sim~\mathcal{N}(\mu,\sigma)$, where $\mathcal{N}(\mu,\sigma)$ is the standard normal distribution with mean equal to $\mu$ and standard deviation equal to $\sigma$ . # ### To Estimate the Average Option Price Using a Quantum Computer, We Need To: * Load the distribution, that is, discretize the distribution using $2^n$ points (n is the number of qubits) and truncate it. * Implement the payoff function that is equal to zero if $S\leq{K}$ and increases linearly otherwise. The linear part is approximated in order to be properly loaded using $R_y$ rotations \[[3](#qar)]. * Evaluate the expected payoff using amplitude estimation. The algorithmic framework is called Quantum Monte-Carlo Integration. For a basic example, see [QMCI](https://github.com/Classiq/classiq-library/blob/main/algorithms/amplitude_amplification_and_estimation/qmc_user_defined/qmc_user_defined.ipynb). Here we use the same framework to estimate european call option, where the underlying asset distribution at the maturity data is modeled as log-normal distribution. ## Designing the Quantum Algorithm In this workshop, we will collaboratively design a quantum algorithm to estimate the price of a European option. This algorithm can be applied to real-world stock data, providing practical results and offering a potential speedup over the classical Monte Carlo method. During the algorithm design process, you will explore state preparation and arithmetics, define Qstructs (quantum classes), and function calling using Classiq? The code in the following section is incomplete, with missing parts that you need to fill in, indicated by "#TODO" in the code description. If you're unsure how to use a specific Classiq function, please refer to the [Classiq documentation](https://docs.classiq.io) and search for the required quantum function to find its corresponding documentation page. # ## The Probability Distribution We begin by creating the probability distribution. The distribution is describing the option underlying asset price at maturity date. We will load a discrete version of the log-normal probability with $2^n$ points, when $\mu$ of the normal distribution is denoted by `mu`, $\sigma$ by `sigma` and $n$ is the number of qubits `num_qubits`. `K`, the strike price, is also chosen in this section. ```python theme={null} num_qubits = 5 mu = 0.7 sigma = 0.13 K = 1.9 ``` ```python theme={null} import matplotlib.pyplot as plt import numpy as np import scipy def get_log_normal_probabilities(mu_normal, sigma_normal, num_points): # TODO Define the log-normal mean (log_normal_mean), variance (log_normal_variance) and standard deviation (log_normal_stddev) ##### SOLUTION START #### log_normal_mean = np.exp(mu + sigma**2 / 2) log_normal_variance = (np.exp(sigma**2) - 1) * np.exp(2 * mu + sigma**2) log_normal_stddev = np.sqrt(log_normal_variance) ###### SOLUTION END ##### # cutting the distribution 3 sigmas from the mean low = np.maximum(0, log_normal_mean - 3 * log_normal_stddev) high = log_normal_mean + 3 * log_normal_stddev print(log_normal_mean, log_normal_variance, log_normal_stddev, low, high) x = np.linspace(low, high, num_points) return x, scipy.stats.lognorm.pdf(x, s=sigma_normal, scale=np.exp(mu_normal)) ``` ```python theme={null} grid_points, probs = get_log_normal_probabilities(mu, sigma, 2**num_qubits) # TODO normalize the probabilities # #### SOLUTION START #### probs = (probs / np.sum(probs)).tolist() # ##### SOLUTION END ##### fig, ax1 = plt.subplots() # Plotting the log-normal probability function ax1.plot(grid_points, probs, "go-", label="Probability") # Green line with circles ax1.tick_params(axis="y", labelcolor="g") ax1.set_xlabel("Asset Value at Maturity Date") ax1.set_ylabel("Probability", color="g") # Creating a second y-axis for the payoff function F= S-K ax2 = ax1.twinx() ax2.plot(grid_points, np.maximum(grid_points - K, 0), "r-", label="Payoff") # Red line ax2.set_ylabel("Payoff", color="r") ax2.tick_params(axis="y", labelcolor="r") # Adding grid and title ax1.grid(True) plt.title("Probability and Payoff vs. Asset Value") ``` **Output:** ``` 2.030841014265948 0.07029323208790372 0.26512870853210846 1.2354548886696226 2.8262271398622736 ``` **Output:** ``` Text(0.5, 1.0, 'Probability and Payoff vs. Asset Value') ``` output # ### Quantum Function for Distribution Loading Here we use the general `inplace_prepare_state` [function](https://docs.classiq.io/latest/sdk-reference/?h=inplace_prepare_state#classiq.qmod.builtins.functions.inplace_prepare_int). The `inplace_prepare_state` function is applied instead of `prepare_state` when a state preparation needs to be repeatedly applied to the same previously initialized quantum variable. We use the genreal purpose state preparation for simplicity. There are more efficient and scalable methods for preparing the required distribution, for example, see \[[4](#gs)]. ```python theme={null} from classiq import * # Loading the probabilities to a QNum variable named "asset" @qfunc def load_distribution(asset: QNum): # TODO load the probabilities you have prepared in the previous sections (Note: the probabilities need to be transformed to a list) ##### SOLUTION START #### inplace_prepare_state(probs, bound=0, target=asset) ###### SOLUTION END ##### ``` # ## The Payoff Function We now proceed to load the payoff function. Our end objective is to construct $U_{payoff}$, which satisfies: $$ U_{payoff}|S\rangle|0\rangle = \sqrt{f(S)}|S\rangle|1\rangle + \sqrt{1-f(S)}|S\rangle|0\rangle $$ Where $\ket{S}$ is the quantum state of the maturity price $S$ (disregarding the prepared probability amplitudes for now) represented by a Qnum variable named 'asset' (see note), and the qubit state ($ \ket\{0\}$ on the LHS) is represented by the 'ind' variable, serving as the indicator qubit. Due to the structure of the European option price payoff function, it is easily observed that the state remains unchanged if the maturity price $S$ is less than the strike price $K$. For $S \geq K$, on the other hand, a linear amplitude loading is applied to reflect the option's payoff. \*Note: in order to save qubits and depth, the register $|S\rangle_n$ will hold a value in the range $[0, 2^{n-1}]$, effectively "labeling" the asset values. The mapping from the label space to the asset value space (and vice-versa) will occur within the comparator and amplitude loading, using the following (classical) `scale`/`descale` functions correspondingly\* ```python theme={null} from classiq.qmod.symbolic import ceiling # Calculating the size between steps grid_step = (max(grid_points) - min(grid_points)) / (len(grid_points) - 1) # Transforming from QNum label space to price space def scale(val): # TODO write the transform of the float label value to the asset value (Clue: see next function) ##### SOLUTION START #### return val * grid_step + min(grid_points) ###### SOLUTION END ##### # Transforming from price space to QNum label space def descale(val: int): return (val - min(grid_points)) / grid_step # A function putting a control condition on the asset (maturity) price, checking if it is 'in the money' (meaning if it has crossed the strike price). if it is, payoff_linear is applied. @qfunc def payoff(asset: Const[QNum], ind: QBit): # TODO Put the correct code to apply this logic. # Use a controlled operation for checking that the asset is above the strike price # Call the payoff_linear function for the payoff part #### SOLUTION START #### # checking if asset price is 'in the money' - crossed the strike price control(asset >= ceiling(descale(K)), lambda: payoff_linear(asset, ind)) ###### SOLUTION END ##### ``` For the amplitude loading step, we use a general-purpose loading method utilizing the `assign_amplitude_table` function. While the calculation is accurate, it is not scalable for large variable sizes. There are more scalable methods, like the ones mentioned in \[[4](#gs)], \[[6](#rainbow)]. \*See [Quantum Types](https://docs.classiq.io/latest/qmod-reference/language-reference/quantum-types/?h=qstruct#semantics) in documentation (under Qmod reference) for Qstruct definition *Important: To ensure that the sum of all loaded amplitudes does not exceed 1, we normalize the payoff using a `scaling_factor`, which will later be multiplied during the post-processing stage.* ```python theme={null} # Scaling the function by the maximal S-K value: scaling_factor = max(grid_points) - K # Amplitude Loading of the scaled payoff linear function to 'ind', for asset values that are 'in the money'. This is done using the (assign_amplitude_table)[https://docs.classiq.io/latest/qmod-reference/api-reference/functions/open_library/amplitude_loading/#classiq.open_library.functions.amplitude_loading.assign_amplitude_table] function. # We would like the probability of the indicator qubit to be proportional to the payoff function (the amplitude should be therefore a square-root of the scaled payoff function) # P(ind=1) = |(scale(asset) - K) / scaling_factor| @qfunc def payoff_linear(asset: Const[QNum], ind: QBit): #### SOLUTION START #### assign_amplitude_table( lookup_table(lambda n: np.sqrt(abs((scale(n) - K) / scaling_factor)), asset), asset, ind, ) ###### SOLUTION END ##### # quantum function that loads the distribution and then operates the payoff function (prepares U_payoff) @qfunc def european_call_state_preparation(asset: QNum, ind: QBit): load_distribution(asset) payoff(asset, ind) ``` # ## Wrapping to an Amplitude Estimation Model After defining the probability distribution and the payoff function, we pack it into a `grover_operator`. Both stages completed so far contribute to the state preparation process that precedes the successive applications of the [Grover operator](https://docs.classiq.io/latest/qmod-reference/library-reference/open-library-functions/grover_operator/grover_operator/), an operator used to amplify the amplitudes of specific states corresponding to a desired output. A Grover operator consists of two components: a phase oracle, which is used to distinguish between 'good' state and 'bad' states, and a diffusion operator, which reflects the states about the mean axis (usually achieved by some state preparation successively followed by a reflection about the $\ket{0}$ state). In our case, the oracle function $O_f=\ket{x} \rightarrow (-1)^{f(x)}\ket{x}$ is quite simple, and only needs to flip the sign when the indicator qubit is in the $ \ket\{1\}$ state (meaning $f(1)=1,\; f(0)=0$) After the initial probability and payoff loading, iterations of the Grover operator are applied within the [Iterative Quantum Amplitude Estimation algorithm](https://github.com/Classiq/classiq-library/blob/main/algorithms/amplitude_amplification_and_estimation/quantum_counting/quantum_counting.ipynb) \[[5](#iqae)], which is a generalization of the Grover search algorithm, utilized for the estimation of the amplitude $a$ to arbitrary precision, given the state (now taking the prepared probability amplitudes into account): $$ |\Psi\rangle = \sum_x|x\rangle[\sqrt{p(x)f(x)}|1\rangle_{ind} + \sqrt{p(x)(1-f(x))}|0\rangle_{ind}]=\sqrt{a}|\Psi_1\rangle + \sqrt{1-a}|\Psi_0\rangle $$ Which approximates the expectation value of the payoff (after a post-processing step): $$ a = \sum_xp(x)f(x) \approx E[f]_p $$ ```python theme={null} # Defining the iterative quantum amplitude estimation algorithm from classiq.applications.iqae.iqae import IQAE # TODO set constrains to a maximal width of 25 qubits because of the limited resources of simulators # #### SOLUTION START #### constraints = Constraints(max_width=25) # ##### SOLUTION END ##### iqae = IQAE( state_prep_op=european_call_state_preparation, problem_vars_size=num_qubits, constraints=constraints, preferences=Preferences(optimization_level=1), ) ``` ## Quantum Program Synthesis After we finished the model design, we synthesize the model to a quantum program. ```python theme={null} qprog = iqae.get_qprog() show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/2ys1ApWeTomdHj6K3roBIXQjCGW ``` ## Quantum Program Execution Finally, we execute with defined parameters for the accuracy of the amplitude estimation. This will affect the expected number of grover of repetitions within the execution, which is generally $O(\sqrt{N})$: ```python theme={null} result = iqae.run( epsilon=0.05, alpha=0.01, # desired error # desired probability for error ) ``` # ## Post Processing In order to get the expected payoff, we need to descale the measured amplitude by `scaling_factor`. ```python theme={null} measured_payoff = result.estimation * scaling_factor condidence_interval = np.array(result.confidence_interval) * scaling_factor print("Measured Payoff:", measured_payoff) print("Confidence Interval:", condidence_interval) ``` **Output:** ``` Measured Payoff: 0.1753896325820247 Confidence Interval: [0.17209738 0.17868189] ``` # ## Compare to the Expected Calculated Payoff ```python theme={null} expected_payoff = sum((grid_points - K) * (grid_points >= K) * probs) print("Expected Payoff:", expected_payoff) ``` **Output:** ``` Expected Payoff: 0.17680663493930157 ``` ```python theme={null} assert np.isclose( measured_payoff, expected_payoff, atol=10 * (condidence_interval[1] - condidence_interval[0]), ) ``` ## References \[1]: [Paul Glasserman, Monte Carlo Methods in Financial Engineering. Springer-Verlag New York, 2003, p. 596.](https://link.springer.com/book/10.1007/978-0-387-21617-1)
\[2]: [Gilles Brassard, Peter Hoyer, Michele Mosca, and Alain Tapp, Quantum Amplitude Amplification and Estimation. Contemporary Mathematics 305 (2002)](https://arxiv.org/abs/quant-ph/0005055)
\[3]: [ Nikitas Stamatopoulos, Daniel J. Egger, Yue Sun, Christa Zoufal, Raban Iten, Ning Shen, and Stefan Woerner, Option Pricing using Quantum Computers, Quantum 4, 291 (2020). ](https://arxiv.org/abs/1905.02666v5)
\[4]: [ Chakrabarti, Shouvanik, et al. "A threshold for quantum advantage in derivative pricing." Quantum 5 (2021): 463.](https://quantum-journal.org/papers/q-2021-06-01-463/)
\[5]: [Grinko, D., Gacon, J., Zoufal, C. et al. Iterative quantum amplitude estimation. npj Quantum Inf 7, 52 (2021)](https://doi.org/10.1038/s41534-021-00379-1)
\[6]: [Francesca Cibrario et al., Quantum Amplitude Loading for Rainbow Options Pricing. Preprint](https://arxiv.org/abs/2402.05574v2) # Quantum Optimization Training - Part 3 Source: https://docs.classiq.io/explore/tutorials/workshops/finance_workshops/combi_workshop_Inequality_constriants_PO Open this notebook in GitHub to run it yourself ## Dealing with Constraint Using Portfolio Optimization ## Guidance for the Workshop: **The `# TODO` or `# Your code` is there for you to do yourself.** \*\*The `# Solution start` and `# Solution end` are only for helping you. Please delete the `Solution` and try doing it yourself...\*\* ## Portfolio Optimization with the Quantum Approximate Optimization Algorithm (QAOA) # ## Introduction Portfolio optimization is the process of allocating a portfolio of financial assets optimally, according to some predetermined goal. Usually, the goal is to maximize the potential return while minimizing the financial risk of the portfolio. One can express this problem as a combinatorial optimization problem like many other real-world problems. In this demo, we'll show how the Quantum Approximate Optimization Algorithm (QAOA) can be employed on the Classiq platform to solve the problem of portfolio optimization. # ## Modeling the Portfolio Optimization Problem As a first step, we have to model the problem mathematically. We will use a simple yet powerful model, which captures the essence of portfolio optimization: * A portfolio is built from a pool of $n$ financial assets, each asset labeled $i \in \{1,\ldots,n\}$. * Every asset's return is a random variable, with expected value $\mu_i$ and variance $\Sigma_i$ (modeling the financial risk involved in the asset). * Every two assets $i \neq j$ have covariance $\Sigma_{ij}$ (modeling market correlation between assets). * Every asset $i$ has a weight $w_i \in D_i = \{0,\ldots,b_i\}$ in the portfolio, with $b_i$ defined as the budget for asset $i$ (modeling the maximum allowed weight of the asset). * The return vector $\mu$, the covariance matrix $\Sigma$ and the weight vector $w$ are defined naturally from the above (with the domain $D = D_1 \times D_2 \times \ldots \times D_n$ for $w$). With the above definitions, the total expected return of the portfolio is $\mu^T w$ and the total risk is $w^T \Sigma w$. We'll use a simple difference of the two as our cost function, with the additional constraint that the total sum of assets does not exceed a predefined budget $B$. We note that there are many other possibilities for defining a cost function (e.g. add a scaling factor to the risk/return or even some non-linear relation). For reasons of simplicity we select the model below, and we assume all constants and variables are dimensionless. Thus, the problem is, given the constant inputs $\mu, \Sigma, D, B$, to find optimal variable $w$ as follows: $$ \min_{w \in D} w^T \Sigma w - \mu^T w, $$ subject to $$ \Sigma_{i} w_i \leq B $$ The case presented above is called integer portfolio optimization, since the domains $D_i$ are over the (positive) integers. Another variation of this problem defines weights over binary domains, and will not be discussed here. ```python theme={null} import math from typing import List import matplotlib.pyplot as plt import networkx as nx import numpy as np from scipy.optimize import minimize from classiq import * ``` # ## Finaly, We Will Add Inequality Constraints: $$ \min_{w \in D} w^T \Sigma w - \mu^T w, $$ subject to: $$ \Sigma_{i} w_i \leq B $$ **We will do it similarly to the equality constraint but we add slack variable that can take multiple values to make sure $\Sigma_{i} w_i \leq B$** In this case, we will change the objective function as follows: $$ \min_{w \in D} w^T \Sigma w - \mu^T w + P * (\Sigma_{i} w_i + slack - B)^2 $$ Where $P$ is the penalty value you need to define. ## The Portfolio Optimization Problem Parameters First we define the parameters of the optimization problem, which include the expected return vector, the covariance matrix, the total budget and the asset-specific budgets. ```python theme={null} returns = np.array([3, 4, -1]) # fmt: off covariances = np.array( [ [ 0.9, 0.5, -0.7], [ 0.5, 0.9, -0.2], [-0.7, -0.2, 0.9], ] ) # fmt: on total_budget = 6 ``` ## Defining the Variables The number of slack qubits needs to reach to get to the number $B$. ```python theme={null} num_assets = 3 num_qubits_per_asset = 2 # Defines the possible values of choosing each asset. num_slack = 3 class PortfolioOptimizationVars(QStruct): a: QArray[QNum[num_qubits_per_asset], num_assets] slack: QNum[num_slack] ``` # ## Define the Expected Return Define a function that describes $\mu^T w$ where $\mu$ is the `return` vector. ```python theme={null} def expected_return_cost( returns: np.ndarray, w_array: PortfolioOptimizationVars ) -> float: return sum(returns[i] * w_array.a[i] for i in range(len(returns))) ``` # ## Define the Risk Term Define a function that describes the risk term in the objective function $w^T \Sigma w$ where $\Sigma$ is the `covariances` matrix. $$ \min_{w \in D} w^T \Sigma w - \mu^T w + P * (\Sigma_{i} w_i + slack - B)^2 $$ ```python theme={null} def risk_cost(covariances: np.ndarray, w_array: PortfolioOptimizationVars) -> float: risk_term = sum( w_array.a[i] * sum(w_array.a[j] * covariances[i][j] for j in range(covariances.shape[0])) for i in range(covariances.shape[0]) ) return risk_term ``` # ## Define the Entire Portfolio Optimization Objective Function Combine the risk term and the expected return functions. There a a term called return coefficient `return_coeff` that defines how much you prefer certainly over return. Higher values is more risky but can be more profitable. Later try changing it to see how the result changes. ```python theme={null} return_coeff = 1.4 Penalty = 1.3 def objective_portfolio_inequality( w_array: PortfolioOptimizationVars, returns: np.ndarray, covariances: np.ndarray, return_coeff: float, ) -> float: # Your code # Solution start return ( risk_cost(covariances, w_array) - return_coeff * expected_return_cost(returns, w_array) + Penalty * ( sum(w_array.a[i] for i in range(len(returns))) + w_array.slack - total_budget ) ** 2 ) # Solution end ``` ## Build the QAOA Circuit ```python theme={null} @qfunc def mixer_layer(beta: CReal, qba: QArray): # Your code here # Solution start apply_to_all(lambda q: RX(beta, q), qba) # Solution end ``` ```python theme={null} NUM_LAYERS = 4 @qfunc def main( params: CArray[CReal, 2 * NUM_LAYERS], w_array: Output[PortfolioOptimizationVars] ) -> None: # Allocating the qubits allocate(w_array) # Your code # Solution start hadamard_transform(w_array) repeat( count=int(params.len / 2), iteration=lambda i: ( phase( objective_portfolio_inequality( w_array, returns, covariances, return_coeff ), params[2 * i], ), mixer_layer(params[2 * i + 1], w_array), ), ) # Solution end ``` ## Synthesizing and Visualizing ```python theme={null} qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/2ygh3y09PSqCwRCitOK3J7bCW3g ``` ## Execution and Post Processing For the hybrid execution, we use `ExecutionSession`, which can evaluate the circuit in multiple methods, such as sampling the circuit, giving specific values for the parameters, and evaluating to a specific Hamiltonian, which is very common in chemical applications. In QAOA, we will use the `estimate_cost` method, which samples the cost function and returns their average cost from all measurements. That helps to optimize easily. ```python theme={null} NUM_SHOTS = 1000 es = ExecutionSession( qprog, execution_preferences=ExecutionPreferences(num_shots=NUM_SHOTS) ) # Build `initial_params` list of np.array type. # The gamma values should start from 0 and, in each layer, should approach closer to 1 linearly # The beta values should start from 1 and in each layer, should approach closer to 0 linearly # Then unify it to one list so scipy minimize can digest it. # Your code here # Solution start def initial_qaoa_params(NUM_LAYERS) -> np.ndarray: initial_gammas = math.pi * np.linspace(0, 1, NUM_LAYERS) initial_betas = math.pi * np.linspace(1, 0, NUM_LAYERS) initial_params = [] for i in range(NUM_LAYERS): initial_params.append(initial_gammas[i]) initial_params.append(initial_betas[i]) return np.array(initial_params) # Solution end initial_params = initial_qaoa_params(NUM_LAYERS) ``` # ## Define a Callback Function to Track the Optimization ```python theme={null} # Record the steps of the optimization intermediate_params = [] objective_values = [] # Define the callback function to store the intermediate steps def callback(xk): intermediate_params.append(xk) ``` # ## Define the Objective Function ```python theme={null} # You code # You can use the hints in the comments # cost_func = lambda state: objective_portfolio_inequality( # w_array = ..., # returns = ..., # covariances = ..., # return_coeff= ... # ) # def estimate_cost_func(params: np.ndarray) -> float: # objective_value = es.estimate_cost( # cost_func = ..., # parameters = {"params": params.tolist()} # ) # # Your code here # # Save the result for convergence graph # return objective_value # Solution start cost_func = lambda state: objective_portfolio_inequality( w_array=state["w_array"], returns=returns, covariances=covariances, return_coeff=return_coeff, ) def estimate_cost_func(params: np.ndarray) -> float: objective_value = es.estimate_cost( cost_func=cost_func, parameters={"params": params.tolist()} ) objective_values.append(objective_value) return objective_value # Solution end ``` # ## Optimize ```python theme={null} # You code # You can use the hints in the comments # optimization_res = minimize( # fun = ..., # x0=..., # method="COBYLA", # callback=..., # options={"maxiter": 10}, # ) # Solution start optimization_res = minimize( estimate_cost_func, x0=initial_params, method="COBYLA", callback=callback, options={"maxiter": 20}, ) # Solution end ``` # ## Look at the Results ```python theme={null} res = es.sample({"params": optimization_res.x.tolist()}) print(f"Optimized parameters: {optimization_res.x.tolist()}") sorted_counts = sorted( res.parsed_counts, key=lambda pc: objective_portfolio_inequality( pc.state["w_array"], returns=returns, covariances=covariances, return_coeff=return_coeff, ), ) for sampled in sorted_counts: w_sample = sampled.state["w_array"] print( f"solution={w_sample} probability={sampled.shots/NUM_SHOTS} " f"cost={objective_portfolio_inequality(w_array=w_sample,returns = returns, covariances = covariances, return_coeff= return_coeff)}" ) ``` **Output:** ``` Optimized parameters: [0.0, 4.141592653589793, 1.0471975511965976, 3.0943951023931957, 2.0943951023931953, 2.047197551196598, 3.141592653589793, 1.0] solution={'a': [1, 3, 1], 'slack': 1} probability=0.003 cost=-9.299999999999999 solution={'a': [2, 3, 1], 'slack': 0} probability=0.007 cost=-9.199999999999998 solution={'a': [2, 2, 2], 'slack': 0} probability=0.001 cost=-9.199999999999998 solution={'a': [1, 3, 0], 'slack': 2} probability=0.006 cost=-8.999999999999998 solution={'a': [3, 1, 2], 'slack': 0} probability=0.002 cost=-8.999999999999998 solution={'a': [1, 2, 0], 'slack': 3} probability=0.001 cost=-8.899999999999999 solution={'a': [1, 2, 1], 'slack': 2} probability=0.001 cost=-8.8 solution={'a': [3, 2, 1], 'slack': 0} probability=0.003 cost=-8.799999999999999 solution={'a': [2, 1, 1], 'slack': 2} probability=0.003 cost=-8.4 solution={'a': [2, 2, 1], 'slack': 2} probability=0.003 cost=-8.4 solution={'a': [3, 2, 2], 'slack': 0} probability=0.001 cost=-8.399999999999999 solution={'a': [2, 2, 0], 'slack': 2} probability=0.001 cost=-8.399999999999999 solution={'a': [1, 3, 1], 'slack': 0} probability=0.004 cost=-7.999999999999999 solution={'a': [2, 2, 2], 'slack': 1} probability=0.005 cost=-7.899999999999998 solution={'a': [2, 3, 1], 'slack': 1} probability=0.002 cost=-7.899999999999998 solution={'a': [1, 3, 2], 'slack': 0} probability=0.002 cost=-7.799999999999999 solution={'a': [1, 3, 0], 'slack': 3} probability=0.003 cost=-7.699999999999998 solution={'a': [0, 2, 0], 'slack': 4} probability=0.002 cost=-7.6 solution={'a': [1, 2, 0], 'slack': 2} probability=0.003 cost=-7.599999999999999 solution={'a': [1, 2, 1], 'slack': 3} probability=0.005 cost=-7.500000000000001 solution={'a': [1, 2, 1], 'slack': 1} probability=0.002 cost=-7.500000000000001 solution={'a': [2, 1, 0], 'slack': 3} probability=0.002 cost=-7.5 solution={'a': [2, 1, 2], 'slack': 1} probability=0.002 cost=-7.499999999999999 solution={'a': [3, 2, 1], 'slack': 1} probability=0.001 cost=-7.499999999999999 solution={'a': [2, 3, 0], 'slack': 1} probability=0.002 cost=-7.4999999999999964 solution={'a': [0, 3, 0], 'slack': 4} probability=0.005 cost=-7.399999999999996 solution={'a': [0, 3, 0], 'slack': 2} probability=0.004 cost=-7.399999999999996 solution={'a': [3, 1, 1], 'slack': 0} probability=0.001 cost=-7.199999999999998 solution={'a': [2, 1, 1], 'slack': 3} probability=0.005 cost=-7.1000000000000005 solution={'a': [2, 1, 1], 'slack': 1} probability=0.003 cost=-7.1000000000000005 solution={'a': [2, 2, 0], 'slack': 1} probability=0.004 cost=-7.099999999999999 solution={'a': [2, 2, 0], 'slack': 3} probability=0.001 cost=-7.099999999999999 solution={'a': [1, 1, 0], 'slack': 4} probability=0.006 cost=-6.999999999999999 solution={'a': [1, 2, 2], 'slack': 1} probability=0.002 cost=-6.9 solution={'a': [1, 3, 2], 'slack': 1} probability=0.004 cost=-6.499999999999999 solution={'a': [1, 1, 1], 'slack': 3} probability=0.003 cost=-6.499999999999998 solution={'a': [3, 0, 2], 'slack': 1} probability=0.002 cost=-6.499999999999997 solution={'a': [3, 1, 3], 'slack': 0} probability=0.003 cost=-6.399999999999998 solution={'a': [0, 2, 0], 'slack': 5} probability=0.005 cost=-6.3 solution={'a': [0, 3, 1], 'slack': 3} probability=0.008 cost=-6.299999999999999 solution={'a': [0, 3, 1], 'slack': 1} probability=0.001 cost=-6.299999999999999 solution={'a': [2, 1, 0], 'slack': 4} probability=0.001 cost=-6.2 solution={'a': [2, 1, 0], 'slack': 2} probability=0.001 cost=-6.2 solution={'a': [2, 1, 2], 'slack': 0} probability=0.006 cost=-6.199999999999999 solution={'a': [2, 1, 2], 'slack': 2} probability=0.003 cost=-6.199999999999999 solution={'a': [3, 1, 0], 'slack': 2} probability=0.001 cost=-6.1999999999999975 solution={'a': [2, 3, 0], 'slack': 2} probability=0.002 cost=-6.199999999999997 solution={'a': [1, 1, 0], 'slack': 3} probability=0.005 cost=-5.699999999999999 solution={'a': [1, 2, 2], 'slack': 0} probability=0.001 cost=-5.6000000000000005 solution={'a': [2, 2, 3], 'slack': 0} probability=0.002 cost=-5.599999999999999 solution={'a': [2, 0, 1], 'slack': 3} probability=0.007 cost=-5.3 solution={'a': [1, 1, 1], 'slack': 4} probability=0.005 cost=-5.199999999999998 solution={'a': [1, 1, 1], 'slack': 2} probability=0.001 cost=-5.199999999999998 solution={'a': [3, 0, 1], 'slack': 3} probability=0.005 cost=-5.099999999999999 solution={'a': [3, 0, 1], 'slack': 1} probability=0.003 cost=-5.099999999999999 solution={'a': [3, 1, 0], 'slack': 3} probability=0.001 cost=-4.899999999999998 solution={'a': [0, 2, 1], 'slack': 2} probability=0.002 cost=-4.799999999999999 solution={'a': [0, 2, 1], 'slack': 4} probability=0.001 cost=-4.799999999999999 solution={'a': [2, 0, 0], 'slack': 4} probability=0.001 cost=-4.799999999999999 solution={'a': [3, 0, 3], 'slack': 0} probability=0.001 cost=-4.799999999999995 solution={'a': [3, 2, 0], 'slack': 2} probability=0.003 cost=-4.7999999999999945 solution={'a': [3, 2, 0], 'slack': 0} probability=0.001 cost=-4.7999999999999945 solution={'a': [0, 1, 0], 'slack': 5} probability=0.003 cost=-4.699999999999999 solution={'a': [0, 3, 2], 'slack': 1} probability=0.002 cost=-4.699999999999999 solution={'a': [2, 2, 1], 'slack': 3} probability=0.001 cost=-4.500000000000001 solution={'a': [3, 0, 0], 'slack': 3} probability=0.001 cost=-4.499999999999998 solution={'a': [3, 3, 0], 'slack': 0} probability=0.001 cost=-4.199999999999996 solution={'a': [1, 3, 1], 'slack': 3} probability=0.001 cost=-4.099999999999999 solution={'a': [2, 0, 1], 'slack': 2} probability=0.003 cost=-4.0 solution={'a': [2, 0, 1], 'slack': 4} probability=0.002 cost=-4.0 solution={'a': [2, 0, 2], 'slack': 2} probability=0.003 cost=-3.999999999999999 solution={'a': [1, 3, 0], 'slack': 0} probability=0.004 cost=-3.799999999999998 solution={'a': [1, 2, 0], 'slack': 5} probability=0.002 cost=-3.6999999999999984 solution={'a': [1, 2, 0], 'slack': 1} probability=0.002 cost=-3.6999999999999984 solution={'a': [1, 2, 1], 'slack': 4} probability=0.006 cost=-3.6000000000000005 solution={'a': [1, 2, 1], 'slack': 0} probability=0.005 cost=-3.6000000000000005 solution={'a': [3, 2, 1], 'slack': 2} probability=0.007 cost=-3.5999999999999988 solution={'a': [3, 2, 3], 'slack': 0} probability=0.001 cost=-3.599999999999995 solution={'a': [2, 0, 0], 'slack': 3} probability=0.006 cost=-3.499999999999999 solution={'a': [2, 1, 3], 'slack': 1} probability=0.001 cost=-3.4999999999999982 solution={'a': [0, 3, 0], 'slack': 1} probability=0.006 cost=-3.4999999999999956 solution={'a': [0, 3, 0], 'slack': 5} probability=0.004 cost=-3.4999999999999956 solution={'a': [3, 0, 3], 'slack': 1} probability=0.001 cost=-3.4999999999999956 solution={'a': [0, 1, 0], 'slack': 4} probability=0.006 cost=-3.3999999999999995 solution={'a': [0, 1, 0], 'slack': 6} probability=0.001 cost=-3.3999999999999995 solution={'a': [0, 3, 2], 'slack': 0} probability=0.001 cost=-3.3999999999999995 solution={'a': [1, 0, 0], 'slack': 5} probability=0.003 cost=-3.2999999999999994 solution={'a': [3, 1, 1], 'slack': 3} probability=0.006 cost=-3.299999999999998 solution={'a': [2, 1, 1], 'slack': 0} probability=0.001 cost=-3.2 solution={'a': [2, 1, 1], 'slack': 4} probability=0.001 cost=-3.2 solution={'a': [1, 2, 3], 'slack': 0} probability=0.002 cost=-3.1999999999999993 solution={'a': [2, 2, 0], 'slack': 0} probability=0.007 cost=-3.1999999999999984 solution={'a': [3, 0, 0], 'slack': 2} probability=0.001 cost=-3.1999999999999984 solution={'a': [3, 0, 0], 'slack': 4} probability=0.001 cost=-3.1999999999999984 solution={'a': [2, 2, 0], 'slack': 4} probability=0.001 cost=-3.1999999999999984 solution={'a': [1, 3, 3], 'slack': 0} probability=0.002 cost=-3.1999999999999966 solution={'a': [1, 1, 2], 'slack': 3} probability=0.001 cost=-2.8999999999999995 solution={'a': [1, 1, 2], 'slack': 1} probability=0.001 cost=-2.8999999999999995 solution={'a': [3, 3, 0], 'slack': 1} probability=0.008 cost=-2.899999999999996 solution={'a': [0, 2, 2], 'slack': 2} probability=0.007 cost=-2.799999999999999 solution={'a': [2, 0, 2], 'slack': 1} probability=0.001 cost=-2.6999999999999993 solution={'a': [2, 0, 2], 'slack': 3} probability=0.001 cost=-2.6999999999999993 solution={'a': [3, 1, 3], 'slack': 1} probability=0.005 cost=-2.4999999999999973 solution={'a': [0, 2, 0], 'slack': 2} probability=0.012 cost=-2.3999999999999995 solution={'a': [0, 2, 0], 'slack': 6} probability=0.006 cost=-2.3999999999999995 solution={'a': [0, 3, 1], 'slack': 4} probability=0.007 cost=-2.3999999999999986 solution={'a': [0, 3, 1], 'slack': 0} probability=0.004 cost=-2.3999999999999986 solution={'a': [2, 1, 0], 'slack': 1} probability=0.004 cost=-2.3 solution={'a': [2, 1, 2], 'slack': 3} probability=0.001 cost=-2.299999999999999 solution={'a': [2, 3, 0], 'slack': 3} probability=0.003 cost=-2.2999999999999963 solution={'a': [3, 3, 1], 'slack': 1} probability=0.001 cost=-2.1000000000000005 solution={'a': [1, 0, 0], 'slack': 4} probability=0.001 cost=-1.9999999999999993 solution={'a': [1, 0, 0], 'slack': 6} probability=0.001 cost=-1.9999999999999993 solution={'a': [2, 3, 3], 'slack': 0} probability=0.005 cost=-1.9999999999999991 solution={'a': [1, 1, 0], 'slack': 6} probability=0.004 cost=-1.799999999999999 solution={'a': [1, 2, 2], 'slack': 3} probability=0.002 cost=-1.7000000000000002 solution={'a': [2, 2, 3], 'slack': 1} probability=0.003 cost=-1.6999999999999984 solution={'a': [0, 1, 1], 'slack': 3} probability=0.004 cost=-1.4999999999999993 solution={'a': [0, 1, 1], 'slack': 5} probability=0.002 cost=-1.4999999999999993 solution={'a': [0, 2, 2], 'slack': 1} probability=0.002 cost=-1.499999999999999 solution={'a': [1, 1, 1], 'slack': 1} probability=0.001 cost=-1.299999999999998 solution={'a': [3, 0, 2], 'slack': 3} probability=0.002 cost=-1.2999999999999972 solution={'a': [3, 0, 1], 'slack': 0} probability=0.004 cost=-1.1999999999999984 solution={'a': [3, 0, 1], 'slack': 4} probability=0.002 cost=-1.1999999999999984 solution={'a': [1, 0, 1], 'slack': 5} probability=0.001 cost=-1.0999999999999994 solution={'a': [3, 1, 0], 'slack': 4} probability=0.007 cost=-0.9999999999999973 solution={'a': [3, 1, 0], 'slack': 0} probability=0.002 cost=-0.9999999999999973 solution={'a': [0, 2, 1], 'slack': 1} probability=0.001 cost=-0.8999999999999986 solution={'a': [3, 2, 0], 'slack': 3} probability=0.004 cost=-0.8999999999999941 solution={'a': [1, 1, 3], 'slack': 1} probability=0.001 cost=-0.09999999999999964 solution={'a': [0, 3, 3], 'slack': 0} probability=0.002 cost=1.7763568394002505e-15 solution={'a': [1, 0, 2], 'slack': 3} probability=0.001 cost=0.30000000000000027 solution={'a': [2, 0, 0], 'slack': 6} probability=0.009 cost=0.40000000000000124 solution={'a': [2, 0, 0], 'slack': 2} probability=0.001 cost=0.40000000000000124 solution={'a': [2, 1, 3], 'slack': 2} probability=0.002 cost=0.40000000000000213 solution={'a': [2, 0, 3], 'slack': 2} probability=0.003 cost=0.40000000000000235 solution={'a': [2, 0, 3], 'slack': 0} probability=0.002 cost=0.40000000000000235 solution={'a': [0, 1, 0], 'slack': 3} probability=0.003 cost=0.5000000000000009 solution={'a': [0, 1, 0], 'slack': 7} probability=0.002 cost=0.5000000000000009 solution={'a': [0, 3, 2], 'slack': 3} probability=0.001 cost=0.5000000000000009 solution={'a': [3, 0, 0], 'slack': 1} probability=0.006 cost=0.700000000000002 solution={'a': [1, 3, 3], 'slack': 1} probability=0.001 cost=0.7000000000000037 solution={'a': [1, 1, 2], 'slack': 0} probability=0.003 cost=1.0000000000000009 solution={'a': [1, 1, 2], 'slack': 4} probability=0.002 cost=1.0000000000000009 solution={'a': [3, 3, 0], 'slack': 2} probability=0.001 cost=1.0000000000000044 solution={'a': [1, 1, 3], 'slack': 0} probability=0.001 cost=1.2000000000000004 solution={'a': [1, 1, 3], 'slack': 2} probability=0.001 cost=1.2000000000000004 solution={'a': [2, 0, 2], 'slack': 0} probability=0.002 cost=1.200000000000001 solution={'a': [0, 0, 0], 'slack': 5} probability=0.005 cost=1.3 solution={'a': [0, 3, 3], 'slack': 1} probability=0.003 cost=1.3000000000000018 solution={'a': [1, 0, 2], 'slack': 2} probability=0.006 cost=1.6000000000000003 solution={'a': [1, 0, 0], 'slack': 3} probability=0.001 cost=1.9000000000000008 solution={'a': [2, 2, 1], 'slack': 4} probability=0.001 cost=2.0 solution={'a': [1, 2, 3], 'slack': 2} probability=0.006 cost=2.000000000000001 solution={'a': [3, 2, 2], 'slack': 2} probability=0.008 cost=2.0000000000000018 solution={'a': [0, 1, 2], 'slack': 4} probability=0.007 cost=2.2 solution={'a': [0, 1, 2], 'slack': 2} probability=0.003 cost=2.2 solution={'a': [0, 0, 1], 'slack': 5} probability=0.005 cost=2.3 solution={'a': [0, 2, 2], 'slack': 0} probability=0.002 cost=2.4000000000000012 solution={'a': [0, 2, 2], 'slack': 4} probability=0.002 cost=2.4000000000000012 solution={'a': [2, 3, 1], 'slack': 3} probability=0.002 cost=2.5000000000000036 solution={'a': [2, 3, 2], 'slack': 2} probability=0.003 cost=2.6000000000000014 solution={'a': [3, 1, 2], 'slack': 3} probability=0.005 cost=2.700000000000003 solution={'a': [1, 3, 0], 'slack': 5} probability=0.002 cost=2.700000000000003 solution={'a': [1, 0, 1], 'slack': 2} probability=0.001 cost=2.8000000000000007 solution={'a': [1, 2, 1], 'slack': 5} probability=0.002 cost=2.9000000000000004 solution={'a': [3, 2, 3], 'slack': 1} probability=0.001 cost=2.9000000000000057 solution={'a': [0, 3, 0], 'slack': 0} probability=0.006 cost=3.0000000000000053 solution={'a': [0, 3, 0], 'slack': 6} probability=0.002 cost=3.0000000000000053 solution={'a': [3, 3, 2], 'slack': 1} probability=0.002 cost=3.100000000000003 solution={'a': [3, 1, 1], 'slack': 4} probability=0.006 cost=3.200000000000003 solution={'a': [2, 2, 0], 'slack': 5} probability=0.001 cost=3.3000000000000025 solution={'a': [0, 0, 1], 'slack': 6} probability=0.002 cost=3.5999999999999996 solution={'a': [0, 2, 3], 'slack': 0} probability=0.002 cost=3.6000000000000005 solution={'a': [3, 3, 3], 'slack': 0} probability=0.001 cost=3.600000000000003 solution={'a': [1, 3, 2], 'slack': 3} probability=0.003 cost=3.900000000000002 solution={'a': [0, 2, 0], 'slack': 1} probability=0.002 cost=4.100000000000001 solution={'a': [0, 2, 0], 'slack': 7} probability=0.002 cost=4.100000000000001 solution={'a': [0, 3, 1], 'slack': 5} probability=0.007 cost=4.100000000000002 solution={'a': [2, 1, 2], 'slack': 4} probability=0.002 cost=4.200000000000002 solution={'a': [2, 3, 0], 'slack': 4} probability=0.004 cost=4.200000000000005 solution={'a': [2, 0, 3], 'slack': 3} probability=0.008 cost=4.3000000000000025 solution={'a': [3, 3, 1], 'slack': 2} probability=0.002 cost=4.4 solution={'a': [2, 3, 3], 'slack': 1} probability=0.003 cost=4.500000000000002 solution={'a': [1, 1, 0], 'slack': 7} probability=0.001 cost=4.700000000000002 solution={'a': [1, 2, 2], 'slack': 4} probability=0.005 cost=4.800000000000001 solution={'a': [1, 0, 3], 'slack': 2} probability=0.001 cost=4.800000000000001 solution={'a': [0, 0, 0], 'slack': 4} probability=0.001 cost=5.2 solution={'a': [3, 0, 2], 'slack': 4} probability=0.002 cost=5.200000000000004 solution={'a': [3, 0, 1], 'slack': 5} probability=0.002 cost=5.3000000000000025 solution={'a': [1, 0, 2], 'slack': 1} probability=0.001 cost=5.5 solution={'a': [3, 1, 0], 'slack': 5} probability=0.008 cost=5.5000000000000036 solution={'a': [3, 2, 0], 'slack': 4} probability=0.009 cost=5.600000000000007 solution={'a': [0, 1, 2], 'slack': 5} probability=0.002 cost=6.1000000000000005 solution={'a': [0, 0, 2], 'slack': 4} probability=0.002 cost=6.4 solution={'a': [2, 0, 1], 'slack': 6} probability=0.006 cost=6.400000000000001 solution={'a': [2, 0, 0], 'slack': 1} probability=0.001 cost=6.900000000000002 solution={'a': [3, 0, 3], 'slack': 3} probability=0.001 cost=6.900000000000006 solution={'a': [3, 0, 0], 'slack': 0} probability=0.001 cost=7.200000000000003 solution={'a': [1, 3, 3], 'slack': 2} probability=0.003 cost=7.200000000000005 solution={'a': [0, 0, 1], 'slack': 3} probability=0.001 cost=7.5 solution={'a': [0, 2, 3], 'slack': 3} probability=0.001 cost=7.500000000000001 solution={'a': [1, 1, 2], 'slack': 5} probability=0.001 cost=7.500000000000002 solution={'a': [0, 0, 2], 'slack': 5} probability=0.003 cost=7.7 solution={'a': [2, 0, 2], 'slack': 5} probability=0.001 cost=7.700000000000002 solution={'a': [1, 0, 1], 'slack': 7} probability=0.001 cost=9.3 solution={'a': [1, 0, 3], 'slack': 0} probability=0.004 cost=10.0 solution={'a': [3, 2, 2], 'slack': 3} probability=0.001 cost=11.100000000000001 solution={'a': [1, 3, 1], 'slack': 5} probability=0.003 cost=11.500000000000002 solution={'a': [0, 1, 3], 'slack': 4} probability=0.004 cost=11.600000000000001 solution={'a': [0, 0, 2], 'slack': 2} probability=0.001 cost=11.600000000000001 solution={'a': [0, 0, 2], 'slack': 6} probability=0.001 cost=11.600000000000001 solution={'a': [1, 1, 3], 'slack': 4} probability=0.001 cost=11.600000000000001 solution={'a': [2, 3, 1], 'slack': 4} probability=0.002 cost=11.600000000000003 solution={'a': [0, 0, 0], 'slack': 3} probability=0.001 cost=11.700000000000001 solution={'a': [3, 1, 2], 'slack': 4} probability=0.002 cost=11.800000000000002 solution={'a': [1, 0, 2], 'slack': 0} probability=0.002 cost=12.000000000000002 solution={'a': [3, 2, 3], 'slack': 2} probability=0.001 cost=12.000000000000005 solution={'a': [0, 3, 0], 'slack': 7} probability=0.001 cost=12.100000000000005 solution={'a': [0, 0, 3], 'slack': 3} probability=0.02 cost=12.3 solution={'a': [3, 1, 1], 'slack': 5} probability=0.001 cost=12.300000000000002 solution={'a': [2, 1, 1], 'slack': 6} probability=0.002 cost=12.4 solution={'a': [0, 1, 2], 'slack': 0} probability=0.004 cost=12.600000000000001 solution={'a': [0, 1, 2], 'slack': 6} probability=0.001 cost=12.600000000000001 solution={'a': [1, 3, 2], 'slack': 4} probability=0.001 cost=13.000000000000002 solution={'a': [3, 1, 3], 'slack': 3} probability=0.001 cost=13.100000000000003 solution={'a': [0, 2, 0], 'slack': 0} probability=0.002 cost=13.200000000000001 solution={'a': [2, 1, 2], 'slack': 5} probability=0.002 cost=13.3 solution={'a': [2, 1, 0], 'slack': 7} probability=0.001 cost=13.3 solution={'a': [3, 3, 1], 'slack': 3} probability=0.001 cost=13.5 solution={'a': [0, 0, 3], 'slack': 2} probability=0.01 cost=13.600000000000001 solution={'a': [2, 3, 3], 'slack': 2} probability=0.006 cost=13.600000000000001 solution={'a': [0, 0, 3], 'slack': 4} probability=0.003 cost=13.600000000000001 solution={'a': [1, 1, 0], 'slack': 0} probability=0.001 cost=13.8 solution={'a': [1, 2, 2], 'slack': 5} probability=0.003 cost=13.9 solution={'a': [2, 2, 3], 'slack': 3} probability=0.004 cost=13.900000000000002 solution={'a': [0, 0, 1], 'slack': 2} probability=0.006 cost=14.0 solution={'a': [0, 2, 3], 'slack': 4} probability=0.001 cost=14.000000000000002 solution={'a': [1, 1, 1], 'slack': 7} probability=0.004 cost=14.300000000000002 solution={'a': [3, 1, 0], 'slack': 6} probability=0.001 cost=14.600000000000003 solution={'a': [0, 2, 1], 'slack': 7} probability=0.003 cost=14.700000000000003 solution={'a': [2, 0, 1], 'slack': 7} probability=0.004 cost=15.5 solution={'a': [2, 0, 0], 'slack': 0} probability=0.002 cost=16.0 solution={'a': [2, 1, 3], 'slack': 4} probability=0.001 cost=16.000000000000004 solution={'a': [3, 0, 3], 'slack': 4} probability=0.007 cost=16.000000000000007 solution={'a': [0, 3, 2], 'slack': 5} probability=0.004 cost=16.1 solution={'a': [3, 0, 0], 'slack': 7} probability=0.003 cost=16.300000000000004 solution={'a': [1, 0, 3], 'slack': 5} probability=0.001 cost=16.5 solution={'a': [1, 1, 2], 'slack': 6} probability=0.008 cost=16.6 solution={'a': [3, 3, 0], 'slack': 4} probability=0.001 cost=16.600000000000005 solution={'a': [2, 0, 2], 'slack': 6} probability=0.004 cost=16.8 solution={'a': [1, 0, 0], 'slack': 1} probability=0.004 cost=17.5 solution={'a': [0, 0, 3], 'slack': 5} probability=0.004 cost=17.5 solution={'a': [0, 0, 3], 'slack': 1} probability=0.003 cost=17.5 solution={'a': [0, 1, 1], 'slack': 0} probability=0.006 cost=18.0 solution={'a': [0, 2, 2], 'slack': 6} probability=0.003 cost=18.0 solution={'a': [0, 0, 2], 'slack': 1} probability=0.008 cost=18.1 solution={'a': [0, 1, 3], 'slack': 5} probability=0.001 cost=18.1 solution={'a': [2, 0, 3], 'slack': 5} probability=0.001 cost=19.900000000000002 solution={'a': [0, 3, 3], 'slack': 4} probability=0.005 cost=20.800000000000004 solution={'a': [1, 0, 2], 'slack': 7} probability=0.002 cost=21.1 solution={'a': [0, 1, 2], 'slack': 7} probability=0.001 cost=21.700000000000003 solution={'a': [2, 2, 1], 'slack': 6} probability=0.007 cost=22.799999999999997 solution={'a': [0, 2, 3], 'slack': 5} probability=0.001 cost=23.1 solution={'a': [1, 3, 1], 'slack': 6} probability=0.001 cost=23.200000000000003 solution={'a': [2, 3, 2], 'slack': 4} probability=0.001 cost=23.4 solution={'a': [1, 3, 0], 'slack': 7} probability=0.005 cost=23.5 solution={'a': [1, 2, 1], 'slack': 7} probability=0.006 cost=23.7 solution={'a': [3, 2, 3], 'slack': 3} probability=0.001 cost=23.700000000000003 solution={'a': [3, 2, 1], 'slack': 5} probability=0.001 cost=23.700000000000003 solution={'a': [3, 3, 2], 'slack': 3} probability=0.01 cost=23.900000000000002 solution={'a': [0, 0, 3], 'slack': 6} probability=0.003 cost=24.0 solution={'a': [2, 2, 0], 'slack': 7} probability=0.004 cost=24.1 solution={'a': [2, 1, 1], 'slack': 7} probability=0.002 cost=24.1 solution={'a': [1, 3, 2], 'slack': 5} probability=0.001 cost=24.700000000000003 solution={'a': [3, 3, 1], 'slack': 4} probability=0.011 cost=25.2 solution={'a': [2, 3, 3], 'slack': 3} probability=0.007 cost=25.3 solution={'a': [1, 0, 3], 'slack': 6} probability=0.003 cost=25.6 solution={'a': [1, 2, 2], 'slack': 6} probability=0.002 cost=25.6 solution={'a': [2, 2, 3], 'slack': 4} probability=0.001 cost=25.6 solution={'a': [3, 0, 2], 'slack': 6} probability=0.002 cost=26.000000000000004 solution={'a': [3, 0, 1], 'slack': 7} probability=0.001 cost=26.1 solution={'a': [3, 2, 0], 'slack': 6} probability=0.004 cost=26.400000000000006 solution={'a': [0, 1, 3], 'slack': 6} probability=0.012 cost=27.200000000000003 solution={'a': [0, 0, 2], 'slack': 0} probability=0.001 cost=27.200000000000003 solution={'a': [0, 3, 2], 'slack': 6} probability=0.01 cost=27.8 solution={'a': [0, 1, 0], 'slack': 0} probability=0.004 cost=27.8 solution={'a': [1, 1, 2], 'slack': 7} probability=0.001 cost=28.3 solution={'a': [1, 0, 0], 'slack': 0} probability=0.002 cost=29.2 solution={'a': [1, 2, 3], 'slack': 5} probability=0.003 cost=29.3 solution={'a': [0, 2, 2], 'slack': 7} probability=0.001 cost=29.700000000000003 solution={'a': [2, 0, 3], 'slack': 6} probability=0.007 cost=31.6 solution={'a': [1, 1, 3], 'slack': 6} probability=0.001 cost=32.4 solution={'a': [0, 0, 0], 'slack': 1} probability=0.003 cost=32.5 solution={'a': [0, 3, 3], 'slack': 5} probability=0.001 cost=32.5 solution={'a': [0, 0, 3], 'slack': 7} probability=0.006 cost=33.1 solution={'a': [0, 0, 1], 'slack': 0} probability=0.001 cost=34.8 solution={'a': [2, 2, 1], 'slack': 7} probability=0.002 cost=37.1 solution={'a': [3, 2, 2], 'slack': 5} probability=0.004 cost=37.10000000000001 solution={'a': [1, 3, 1], 'slack': 7} probability=0.001 cost=37.50000000000001 solution={'a': [2, 2, 2], 'slack': 6} probability=0.004 cost=37.60000000000001 solution={'a': [2, 3, 1], 'slack': 6} probability=0.001 cost=37.60000000000001 solution={'a': [3, 2, 1], 'slack': 6} probability=0.002 cost=38.00000000000001 solution={'a': [3, 1, 1], 'slack': 7} probability=0.004 cost=38.300000000000004 solution={'a': [3, 1, 3], 'slack': 5} probability=0.004 cost=39.10000000000001 solution={'a': [2, 1, 2], 'slack': 7} probability=0.002 cost=39.300000000000004 solution={'a': [2, 3, 0], 'slack': 7} probability=0.001 cost=39.30000000000001 solution={'a': [3, 3, 1], 'slack': 5} probability=0.001 cost=39.5 solution={'a': [1, 2, 2], 'slack': 7} probability=0.001 cost=39.900000000000006 solution={'a': [2, 2, 3], 'slack': 5} probability=0.001 cost=39.900000000000006 solution={'a': [3, 2, 0], 'slack': 7} probability=0.001 cost=40.70000000000001 solution={'a': [3, 0, 3], 'slack': 6} probability=0.01 cost=42.00000000000001 solution={'a': [2, 1, 3], 'slack': 6} probability=0.004 cost=42.00000000000001 solution={'a': [0, 3, 2], 'slack': 7} probability=0.001 cost=42.10000000000001 solution={'a': [1, 3, 3], 'slack': 5} probability=0.001 cost=42.30000000000001 solution={'a': [3, 3, 0], 'slack': 6} probability=0.003 cost=42.60000000000001 solution={'a': [2, 0, 3], 'slack': 7} probability=0.004 cost=45.900000000000006 solution={'a': [0, 0, 0], 'slack': 0} probability=0.001 cost=46.800000000000004 solution={'a': [0, 2, 3], 'slack': 7} probability=0.004 cost=49.10000000000001 solution={'a': [3, 2, 2], 'slack': 6} probability=0.003 cost=54.0 solution={'a': [2, 2, 2], 'slack': 7} probability=0.002 cost=54.50000000000001 solution={'a': [2, 3, 2], 'slack': 6} probability=0.002 cost=54.6 solution={'a': [3, 2, 3], 'slack': 5} probability=0.001 cost=54.900000000000006 solution={'a': [3, 2, 1], 'slack': 7} probability=0.001 cost=54.900000000000006 solution={'a': [1, 3, 2], 'slack': 7} probability=0.002 cost=55.900000000000006 solution={'a': [3, 3, 1], 'slack': 6} probability=0.001 cost=56.400000000000006 solution={'a': [2, 3, 3], 'slack': 5} probability=0.001 cost=56.5 solution={'a': [2, 2, 3], 'slack': 6} probability=0.001 cost=56.800000000000004 solution={'a': [2, 1, 3], 'slack': 7} probability=0.002 cost=58.900000000000006 solution={'a': [1, 3, 3], 'slack': 6} probability=0.006 cost=59.2 solution={'a': [3, 3, 0], 'slack': 7} probability=0.001 cost=59.50000000000001 solution={'a': [0, 3, 3], 'slack': 7} probability=0.002 cost=63.7 solution={'a': [3, 2, 2], 'slack': 7} probability=0.001 cost=73.5 solution={'a': [2, 3, 2], 'slack': 7} probability=0.003 cost=74.10000000000001 solution={'a': [3, 2, 3], 'slack': 6} probability=0.001 cost=74.4 solution={'a': [3, 3, 1], 'slack': 7} probability=0.002 cost=75.9 solution={'a': [2, 3, 3], 'slack': 6} probability=0.002 cost=76.0 solution={'a': [3, 2, 3], 'slack': 7} probability=0.001 cost=96.5 solution={'a': [3, 3, 3], 'slack': 6} probability=0.001 cost=97.2 solution={'a': [2, 3, 3], 'slack': 7} probability=0.003 cost=98.1 solution={'a': [3, 3, 3], 'slack': 7} probability=0.002 cost=121.9 ``` # ## Convergence Graph ```python theme={null} plt.plot(objective_values) plt.xlabel("Iteration") plt.ylabel("Objective Value") plt.title("Optimization Progress") ``` **Output:** ``` Text(0.5, 1.0, 'Optimization Progress') ``` output ## Solution ```python theme={null} import math from typing import List import matplotlib.pyplot as plt import networkx as nx import numpy as np from scipy.optimize import minimize from classiq import * NUM_LAYERS = 3 num_slack = 3 returns = np.array([3, 4, -1]) # fmt: off covariances = np.array( [ [ 0.9, 0.5, -0.7], [ 0.5, 0.9, -0.2], [-0.7, -0.2, 0.9], ] ) # fmt: on total_budget = 6 specific_budgets = 3 return_coeff = 10.0 num_assets = 3 num_qubits_per_asset = 2 Penalty = 30.5 # start with integer variables class PortfolioOptimizationVars(QStruct): a: QArray[QNum[num_qubits_per_asset], num_assets] slack: QNum[num_slack] def expected_return_cost( returns: np.ndarray, w_array: PortfolioOptimizationVars ) -> float: return sum(returns[i] * w_array.a[i] for i in range(len(returns))) def risk_cost(covariances: np.ndarray, w_array: PortfolioOptimizationVars) -> float: risk_term = sum( w_array.a[i] * sum(w_array.a[j] * covariances[i][j] for j in range(covariances.shape[0])) for i in range(covariances.shape[0]) ) return risk_term def objective_portfolio_inequality( w_array: PortfolioOptimizationVars, returns: np.ndarray, covariances: np.ndarray, return_coeff: float, ) -> float: return ( risk_cost(covariances, w_array) - return_coeff * expected_return_cost(returns, w_array) + Penalty * ( sum(w_array.a[i] for i in range(len(returns))) + w_array.slack - total_budget ) ** 2 ) @qfunc def mixer_layer(beta: CReal, qba: QArray): apply_to_all(lambda q: RX(beta, q), qba) @qfunc def main( params: CArray[CReal, 2 * NUM_LAYERS], w_array: Output[PortfolioOptimizationVars] ) -> None: allocate(w_array) hadamard_transform(w_array) repeat( count=int(params.len / 2), iteration=lambda i: ( phase( objective_portfolio_inequality( w_array, returns, covariances, return_coeff ), params[2 * i], ), mixer_layer(params[2 * i + 1], w_array), ), ) qprog = synthesize(main) show(qprog) NUM_SHOTS = 1000 es = ExecutionSession( qprog, execution_preferences=ExecutionPreferences(num_shots=NUM_SHOTS) ) def initial_qaoa_params(NUM_LAYERS) -> np.ndarray: initial_gammas = math.pi * np.linspace(0, 1, NUM_LAYERS) initial_betas = math.pi * np.linspace(1, 0, NUM_LAYERS) initial_params = [] for i in range(NUM_LAYERS): initial_params.append(initial_gammas[i]) initial_params.append(initial_betas[i]) return np.array(initial_params) initial_params = initial_qaoa_params(NUM_LAYERS) # Record the steps of the optimization intermediate_params = [] objective_values = [] # Define the callback function to store the intermediate steps def callback(xk): intermediate_params.append(xk) cost_func = lambda state: objective_portfolio_inequality( w_array=state["w_array"], returns=returns, covariances=covariances, return_coeff=return_coeff, ) def estimate_cost_func(params: np.ndarray) -> float: objective_value = es.estimate_cost( cost_func=cost_func, parameters={"params": params.tolist()} ) objective_values.append(objective_value) return objective_value optimization_res = minimize( estimate_cost_func, x0=initial_params, method="COBYLA", callback=callback, options={"maxiter": 20}, ) res = es.sample({"params": optimization_res.x.tolist()}) print(f"Optimized parameters: {optimization_res.x.tolist()}") sorted_counts = sorted( res.parsed_counts, key=lambda pc: objective_portfolio_inequality( pc.state["w_array"], returns=returns, covariances=covariances, return_coeff=return_coeff, ), ) for sampled in sorted_counts: w = sampled.state["w_array"] print( f"solution={w} probability={sampled.shots/NUM_SHOTS} " f"cost={objective_portfolio_inequality(w_array=w,returns = returns, covariances = covariances, return_coeff= return_coeff)}" ) plt.plot(objective_values) plt.xlabel("Iteration") plt.ylabel("Objective Value") plt.title("Optimization Progress") ``` # Quantum Optimization Training - Part 2 Source: https://docs.classiq.io/explore/tutorials/workshops/finance_workshops/combi_workshop_equality_constriants_PO Open this notebook in GitHub to run it yourself ## Dealing with Constraint Using Portfolio Optimization In this workshop, we will solve the Portfolio Optimization problem using the Quantum Approximate Optimization Algorithm (QAOA), by **introducing how to add various types of constraints of the problem to the QAOA algorithm**. ## Guidance for the Workshop: **The `# TODO` or `# Your code` is there for you to do yourself.** \*\*The `# Solution start` and `# Solution end` are only for helping you. Please delete the `Solution` and try doing it yourself...\*\* ## Portfolio Optimization with the Quantum Approximate Optimization Algorithm (QAOA) # ## Introduction Portfolio optimization is the process of allocating a portfolio of financial assets optimally, according to some predetermined goal. Usually, the goal is to maximize the potential return while minimizing the financial risk of the portfolio. One can express this problem as a combinatorial optimization problem like many other real-world problems. In this demo, we'll show how the Quantum Approximate Optimization Algorithm (QAOA) can be employed on the Classiq platform to solve the problem of portfolio optimization. # ## Modeling the Portfolio Optimization Problem As a first step, we have to model the problem mathematically. We will use a simple yet powerful model, which captures the essence of portfolio optimization: * A portfolio is built from a pool of $n$ financial assets, each asset labeled $i \in \{1,\ldots,n\}$. * Every asset's return is a random variable, with expected value $\mu_i$ and variance $\Sigma_i$ (modeling the financial risk involved in the asset). * Every two assets $i \neq j$ have covariance $\Sigma_{ij}$ (modeling market correlation between assets). * Every asset $i$ has a weight $w_i \in D_i = \{0,\ldots,b_i\}$ in the portfolio, with $b_i$ defined as the budget for asset $i$ (modeling the maximum allowed weight of the asset). * The return vector $\mu$, the covariance matrix $\Sigma$ and the weight vector $w$ are defined naturally from the above (with the domain $D = D_1 \times D_2 \times \ldots \times D_n$ for $w$). With the above definitions, the total expected return of the portfolio is $\mu^T w$ and the total risk is $w^T \Sigma w$. We'll use a simple difference of the two as our cost function, with the additional constraint that the total sum of assets does not exceed a predefined budget $B$. We note that there are many other possibilities for defining a cost function (e.g. add a scaling factor to the risk/return or even some non-linear relation). For reasons of simplicity we select the model below, and we assume all constants and variables are dimensionless. Thus, the problem is, given the constant inputs $\mu, \Sigma, D, B$, to find optimal variable $w$ as follows: $$ \min_{w \in D} w^T \Sigma w - \mu^T w, $$ subject to $\Sigma_{i} w_i \leq B$. The case presented above is called integer portfolio optimization, since the domains $D_i$ are over the (positive) integers. Another variation of this problem defines weights over binary domains, and will not be discussed here. ```python theme={null} import math # import matplotlib.pyplot as plt import numpy as np from scipy.optimize import minimize from classiq import * ``` # ## First We Will Solve the Problem Without Adding Constraint. Just: $$ \min_{w \in D} w^T \Sigma w - \mu^T w $$ # ## Then, We Will Add Equality Constraint: $$ \min_{w \in D} w^T \Sigma w - \mu^T w $$ subject to: $$ \Sigma_{i} w_i == B $$ # ## Finaly, We Will Add Inequality Constraints: $$ \min_{w \in D} w^T \Sigma w - \mu^T w, $$ subject to: $$ \Sigma_{i} w_i \leq B $$ ## The Portfolio Optimization Problem Parameters First we define the parameters of the optimization problem, which include the expected return vector, the covariance matrix, the total budget and the asset-specific budgets. ```python theme={null} returns = np.array([3, 4, -1]) # fmt: off covariances = np.array( [ [ 0.9, 0.5, -0.7], [ 0.5, 0.9, -0.2], [-0.7, -0.2, 0.9], ] ) # fmt: on total_budget = 6 ``` ## Defining the Variables ```python theme={null} num_assets = 3 num_qubits_per_asset = 2 # Defines the possible values of choosing each asset. class PortfolioOptimizationVars(QStruct): a: QArray[QNum[num_qubits_per_asset], num_assets] ``` # ## Define the Expected Return Define a function that describes $\mu^T w$ where $\mu$ is the `return` vector. ```python theme={null} def expected_return_cost( returns: np.ndarray, w_array: PortfolioOptimizationVars ) -> float: # Your code here # Solution start return sum(returns[i] * w_array.a[i] for i in range(len(returns))) # Solution end ``` # ## Define the Risk Term Define a function that describes the risk term in the objective function $w^T \Sigma w$ where $\Sigma$ is the `covariances` matrix. ```python theme={null} def risk_cost(covariances: np.ndarray, w_array: PortfolioOptimizationVars) -> float: # Your code here # hint: # risk_term = sum( # ... * sum(... for j in range(covariances.shape[0])) for i in range(covariances.shape[0]) # ) # Solution start risk_term = sum( w_array.a[i] * sum(w_array.a[j] * covariances[i][j] for j in range(covariances.shape[0])) for i in range(covariances.shape[0]) ) # Solution end return risk_term ``` # ## Define the Entire Portfolio Optimization Objective Function Combine the risk term and the expected return functions. There a a term called return coefficient `return_coeff` that defines how much you prefer certainly over return. Higher values is more risky but can be more profitable. Later try changing it to see how the result changes. ```python theme={null} return_coeff = 1.5 def objective_portfolio( w_array: PortfolioOptimizationVars, returns: np.ndarray, covariances: np.ndarray, return_coeff: float, ) -> float: # Your code here # Solution start return risk_cost(covariances, w_array) - return_coeff * expected_return_cost( returns, w_array ) # Solution end ``` ## Build the QAOA Circuit ```python theme={null} @qfunc def mixer_layer(beta: CReal, qba: QArray): # Your code here # Solution start apply_to_all(lambda q: RX(beta, q), qba) # Solution end ``` ```python theme={null} NUM_LAYERS = 4 @qfunc def main( params: CArray[CReal, 2 * NUM_LAYERS], w_array: Output[PortfolioOptimizationVars] ) -> None: # Your code here # Allocating the qubits allocate(w_array) # Build the QAOA circuit similarly to the maxcut # Solution start hadamard_transform(w_array) repeat( count=int(params.len / 2), iteration=lambda i: ( phase( objective_portfolio(w_array, returns, covariances, return_coeff), params[2 * i], ), mixer_layer(params[2 * i + 1], w_array), ), ) # Solution end ``` ## Synthesizing and Visualizing ```python theme={null} qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/2yj5cH6Av1SoHt6eo5xO1mDKSWs ``` ## Execution and Post Processing For the hybrid execution, we use `ExecutionSession`, which can evaluate the circuit in multiple methods, such as sampling the circuit, giving specific values for the parameters, and evaluating to a specific Hamiltonian, which is very common in chemical applications. In QAOA, we will use the `estimate_cost` method, which samples the cost function and returns their average cost from all measurements. That helps to optimize easily. ```python theme={null} NUM_SHOTS = 1000 es = ExecutionSession( qprog, execution_preferences=ExecutionPreferences(num_shots=NUM_SHOTS) ) # Build `initial_params` list of np.array type. # The gamma values should start from 0 and, in each layer, should approach closer to 1 linearly # The beta values should start from 1 and in each layer, should approach closer to 0 linearly # Then unify it to one list so scipy minimize can digest it. # Your code here # Solution start def initial_qaoa_params(NUM_LAYERS) -> np.ndarray: initial_gammas = math.pi * np.linspace(0, 1, NUM_LAYERS) initial_betas = math.pi * np.linspace(1, 0, NUM_LAYERS) initial_params = [] for i in range(NUM_LAYERS): initial_params.append(initial_gammas[i]) initial_params.append(initial_betas[i]) return np.array(initial_params) # Solution end initial_params = initial_qaoa_params(NUM_LAYERS) ``` ## Define a Callback Function to Track the Optimization ```python theme={null} # Record the steps of the optimization intermediate_params = [] objective_values = [] # Define the callback function to store the intermediate steps def callback(xk): intermediate_params.append(xk) ``` ## Define the Objective Function ```python theme={null} # Your code with hints in the comments: # cost_func = lambda state: objective_portfolio( # w_array = ..., # returns = ..., # covariances = ..., # return_coeff= ... # ) # def estimate_cost_func(params: np.ndarray) -> float: # objective_value = es.estimate_cost( # cost_func = ..., # parameters = {"params": params.tolist()} # ) # # Your code here # # Save the result for convergence graph # # return objective_value # Solution start cost_func = lambda state: objective_portfolio( w_array=state["w_array"], returns=returns, covariances=covariances, return_coeff=return_coeff, ) def estimate_cost_func(params: np.ndarray) -> float: objective_value = es.estimate_cost( cost_func=cost_func, parameters={"params": params.tolist()} ) objective_values.append(objective_value) return objective_value # Solution end ``` ## Optimize ```python theme={null} # Your code with hints in the comments: # optimization_res = minimize( # fun = ..., # x0=..., # method="COBYLA", # callback=..., # options={"maxiter": 10}, # ) # Solution start optimization_res = minimize( fun=estimate_cost_func, x0=initial_params, method="COBYLA", callback=callback, options={"maxiter": 30}, ) # Solution end ``` ## Look at the Results ```python theme={null} res = es.sample({"params": optimization_res.x.tolist()}) print(f"Optimized parameters: {optimization_res.x.tolist()}") sorted_counts = sorted( res.parsed_counts, key=lambda pc: objective_portfolio( pc.state["w_array"], returns=returns, covariances=covariances, return_coeff=return_coeff, ), ) for sampled in sorted_counts: w_sample = sampled.state["w_array"] print( f"solution={w_sample} probability={sampled.shots/NUM_SHOTS} " f"cost={objective_portfolio(w_array=w_sample,returns = returns, covariances = covariances, return_coeff= return_coeff)}" ) ``` ## Convergence Graph ```python theme={null} # plt.plot(objective_values) # plt.xlabel("Iteration") # plt.ylabel("Objective Value") # plt.title("Optimization Progress") ``` ## The Entire Solution ```python theme={null} import math from typing import List import matplotlib.pyplot as plt import networkx as nx import numpy as np from scipy.optimize import minimize from classiq import * NUM_LAYERS = 3 returns = np.array([3, 4, -1]) # fmt: off covariances = np.array( [ [ 0.9, 0.5, -0.7], [ 0.5, 0.9, -0.2], [-0.7, -0.2, 0.9], ] ) # fmt: on total_budget = 6 specific_budgets = 3 return_coeff = 1.7 num_assets = 3 num_qubits_per_asset = 2 # start with integer variables class PortfolioOptimizationVars(QStruct): a: QArray[QNum[num_qubits_per_asset], num_assets] def expected_return_cost( returns: np.ndarray, w_array: PortfolioOptimizationVars ) -> float: return sum(returns[i] * w_array.a[i] for i in range(len(returns))) def risk_cost(covariances: np.ndarray, w_array: PortfolioOptimizationVars) -> float: risk_term = sum( w_array.a[i] * sum(w_array.a[j] * covariances[i][j] for j in range(covariances.shape[0])) for i in range(covariances.shape[0]) ) return risk_term def objective_portfolio( w_array: PortfolioOptimizationVars, returns: np.ndarray, covariances: np.ndarray, return_coeff: float, ) -> float: return risk_cost(covariances, w_array) - return_coeff * expected_return_cost( returns, w_array ) @qfunc def mixer_layer(beta: CReal, qba: QArray): apply_to_all(lambda q: RX(beta, q), qba) @qfunc def main( params: CArray[CReal, 2 * NUM_LAYERS], w_array: Output[PortfolioOptimizationVars] ) -> None: allocate(w_array) hadamard_transform(w_array) repeat( count=int(params.len / 2), iteration=lambda i: ( phase( objective_portfolio(w_array, returns, covariances, return_coeff), params[2 * i], ), mixer_layer(params[2 * i + 1], w_array), ), ) qprog = synthesize(main) show(qprog) NUM_SHOTS = 1000 es = ExecutionSession( qprog, execution_preferences=ExecutionPreferences(num_shots=NUM_SHOTS) ) def initial_qaoa_params(NUM_LAYERS) -> np.ndarray: initial_gammas = math.pi * np.linspace(0, 1, NUM_LAYERS) initial_betas = math.pi * np.linspace(1, 0, NUM_LAYERS) initial_params = [] for i in range(NUM_LAYERS): initial_params.append(initial_gammas[i]) initial_params.append(initial_betas[i]) return np.array(initial_params) initial_params = initial_qaoa_params(NUM_LAYERS) # Record the steps of the optimization intermediate_params = [] objective_values = [] # Define the callback function to store the intermediate steps def callback(xk): intermediate_params.append(xk) cost_func = lambda state: objective_portfolio( w_array=state["w_array"], returns=returns, covariances=covariances, return_coeff=return_coeff, ) def estimate_cost_func(params: np.ndarray) -> float: objective_value = es.estimate_cost( cost_func=cost_func, parameters={"params": params.tolist()} ) objective_values.append(objective_value) return objective_value optimization_res = minimize( fun=estimate_cost_func, x0=initial_params, method="COBYLA", callback=callback, options={"maxiter": 10}, ) res = es.sample({"params": optimization_res.x.tolist()}) print(f"Optimized parameters: {optimization_res.x.tolist()}") sorted_counts = sorted( res.parsed_counts, key=lambda pc: objective_portfolio( pc.state["w_array"], returns=returns, covariances=covariances, return_coeff=return_coeff, ), ) for sampled in sorted_counts: w_sample = sampled.state["w_array"] print( f"solution={w_sample} probability={sampled.shots/NUM_SHOTS} " f"cost={objective_portfolio(w_array=w_sample,returns = returns, covariances = covariances, return_coeff= return_coeff)}" ) plt.plot(objective_values) plt.xlabel("Iteration") plt.ylabel("Objective Value") plt.title("Optimization Progress") ``` ## Adding Equality Constraint The method to deal with equality constraint, namely: $$ \min_{w \in D} w^T \Sigma w - \mu^T w $$ subject to: $$ \Sigma_{i} w_i == B $$ is to add a penalty term to lead us to the set of valid solutions. To do so, we will change the objective function as follows: $$ \min_{w \in D} w^T \Sigma w - \mu^T w + P * (\Sigma_{i} w_i - B)^2 $$ Where $P$ is the penalty value you need to define. ## Define the Objective Value with a Penalty Term ```python theme={null} PENALTY = 2.0 def objective_portfolio_equality( w_array: PortfolioOptimizationVars, returns: np.ndarray, covariances: np.ndarray, return_coeff: float, ) -> float: # Your code here # Solution start return ( risk_cost(covariances, w_array) - return_coeff * expected_return_cost(returns, w_array) + Penalty * (sum(w_array.a[i] for i in range(len(returns))) - total_budget) ** 2 ) # Solution end ``` # ## Repeat the Whole Process All Over Again with `objective_portfolio_equality` ## Solution ```python theme={null} import math from typing import List import matplotlib.pyplot as plt import networkx as nx import numpy as np from scipy.optimize import minimize from classiq import * NUM_LAYERS = 3 returns = np.array([3, 4, -1]) # fmt: off covariances = np.array( [ [ 0.9, 0.5, -0.7], [ 0.5, 0.9, -0.2], [-0.7, -0.2, 0.9], ] ) # fmt: on total_budget = 6 specific_budgets = 3 return_coeff = 10.0 num_assets = 3 num_qubits_per_asset = 2 Penalty = 2.0 # start with integer variables class PortfolioOptimizationVars(QStruct): a: QArray[QNum[num_qubits_per_asset], num_assets] def expected_return_cost( returns: np.ndarray, w_array: PortfolioOptimizationVars ) -> float: return sum(returns[i] * w_array.a[i] for i in range(len(returns))) def risk_cost(covariances: np.ndarray, w_array: PortfolioOptimizationVars) -> float: risk_term = sum( w_array.a[i] * sum(w_array.a[j] * covariances[i][j] for j in range(covariances.shape[0])) for i in range(covariances.shape[0]) ) return risk_term def objective_portfolio_equality( w_array: PortfolioOptimizationVars, returns: np.ndarray, covariances: np.ndarray, return_coeff: float, ) -> float: return ( risk_cost(covariances, w_array) - return_coeff * expected_return_cost(returns, w_array) + Penalty * (sum(w_array.a[i] for i in range(len(returns))) - total_budget) ** 2 ) @qfunc def mixer_layer(beta: CReal, qba: QArray): apply_to_all(lambda q: RX(beta, q), qba) @qfunc def main( params: CArray[CReal, 2 * NUM_LAYERS], w_array: Output[PortfolioOptimizationVars] ) -> None: allocate(w_array) hadamard_transform(w_array) repeat( count=int(params.len / 2), iteration=lambda i: ( phase( objective_portfolio_equality( w_array, returns, covariances, return_coeff ), params[2 * i], ), mixer_layer(params[2 * i + 1], w_array), ), ) qprog = synthesize(main) show(qprog) NUM_SHOTS = 1000 es = ExecutionSession( qprog, execution_preferences=ExecutionPreferences(num_shots=NUM_SHOTS) ) def initial_qaoa_params(NUM_LAYERS) -> np.ndarray: initial_gammas = math.pi * np.linspace(0, 1, NUM_LAYERS) initial_betas = math.pi * np.linspace(1, 0, NUM_LAYERS) initial_params = [] for i in range(NUM_LAYERS): initial_params.append(initial_gammas[i]) initial_params.append(initial_betas[i]) return np.array(initial_params) initial_params = initial_qaoa_params(NUM_LAYERS) # Record the steps of the optimization intermediate_params = [] objective_values = [] # Define the callback function to store the intermediate steps def callback(xk): intermediate_params.append(xk) cost_func = lambda state: objective_portfolio_equality( state["w_array"], returns=returns, covariances=covariances, return_coeff=return_coeff, ) def estimate_cost_func(params: np.ndarray) -> float: objective_value = es.estimate_cost( cost_func=cost_func, parameters={"params": params.tolist()} ) objective_values.append(objective_value) return objective_value optimization_res = minimize( estimate_cost_func, x0=initial_params, method="COBYLA", callback=callback, options={"maxiter": 40}, ) res = es.sample({"params": optimization_res.x.tolist()}) print(f"Optimized parameters: {optimization_res.x.tolist()}") sorted_counts = sorted( res.parsed_counts, key=lambda pc: objective_portfolio_equality( pc.state["w_array"], returns=returns, covariances=covariances, return_coeff=return_coeff, ), ) for sampled in sorted_counts: w = sampled.state["w_array"] print( f"solution={w} probability={sampled.shots/NUM_SHOTS} " f"cost={objective_portfolio_equality(w_array=w,returns = returns, covariances = covariances, return_coeff= return_coeff)}" ) plt.plot(objective_values) plt.xlabel("Iteration") plt.ylabel("Objective Value") plt.title("Optimization Progress") ``` # Rainbow Options Workshop with the Bruteforce Methodology Source: https://docs.classiq.io/explore/tutorials/workshops/finance_workshops/rainbow_options_workshop_bruteforce Open this notebook in GitHub to run it yourself In this workshop Notebook we will go through the implementation using Qmod for the rainbow option pricing \[[1](#qalrop)]. ## Guidance for the Workshop: **The `# Your code` is there for you to do yourself.** \*\*The `# Solution start` and `# Solution end` are only for helping you. Please delete the `Solution` and try doing it yourself...\*\* For completing the code, please refer to the [Classiq documentation](https://docs.classiq.io), [Classiq documentation](https://docs.classiq.io/latest/), or [Classiq Library](https://github.com/Classiq/classiq-library/). Search for the required quantum function to find its corresponding documentation page. ## Introduction and Background In finance, a crucial aspect of asset pricing pertains to derivatives. Derivatives are contracts whose value is contingent upon another source, known as the underlying. The pricing of options, a specific derivative instrument, involves determining the fair market value (discounted payoff) of contracts affording their holders the right, though not the obligation, to buy (call) or sell (put) one or more underlying assets at a predefined strike price by a specified future expiration date (maturity date). This process relies on mathematical models, considering variables like current asset prices, time to expiration, volatility, and interest rates. In many financial models we are often interested in calculating the average of a function of a given probability distribution ($E[f(x)]$). The most popular method to estimate the average is Monte Carlo \[[2](#mcmf)] due to its flexibility and ability to generically handle stochastic parameters. Classical Monte Carlo methods, however, generally require extensive computational resources to provide an accurate estimation. By leveraging the laws of quantum mechanics, a quantum computer may provide novel ways to solve computationally intensive financial problems, such as risk management, portfolio optimization, and option pricing. The core quantum advantage of several of these applications is the Amplitude Estimation algorithm \[[3](#aea)] which can estimate a parameter with a convergence rate of $\Omega(1/M^{2})$, compared to $\Omega(1/M)$ in the classical case, where $M$ is the number of Grover iterations in the quantum case and the number of the Monte Carlo samples in the classical case. This represents a theoretical quadratic speed-up of the quantum method over classical Monte Carlo methods! # ## Rainbow Option Pricing An option is the possibility to buy (call) or sell (put) an item (or share) at a known price - the strike price (K), where the option has a maturity price (S). The payoff function to describe for example a call option will be: $$ f(S) = \begin{cases} 0, & \text{if } K \geq S \\ S - K, & \text{if } K < S \end{cases} $$ The maturity price is unknown. Therefore, it is expressed by a price distribution function, which may be any type of a distribution function. For example a log-normal distribution: $\mathcal{ln}(S)\sim~\mathcal{N}(\mu,\sigma)$, where $\mathcal{N}(\mu,\sigma)$ is the standard normal distribution with mean equal to $\mu$ and standard deviation equal to $\sigma$. **In the case of a rainbow options**, the payoff function is defined by the maximum of the maturity prices of multiple assets. "Best of", The best-performing asset is chosen as a reference for payoff calculation. For call options, the payoff function is defined as follows: $$ f(S) = max(S-K, 0) $$ Where, in this case, $S=max(\bar{S}_t)$. There is another type of asset called "worst of", where the worst-performing asset is chosen as a reference for payoff calculation. We will not treat this type of option in this notebook. # ### To Estimate the Average Option Price Using a Quantum Computer, We Need To: * Load the distribution, that is, discretize the distribution using $2^n$ points (n is the number of qubits) and truncate it. * Implement the affine transformation to bring the assets to the maturity date. * Implement the payoff function for rainbow options and make amplitude loading using control $R_y$ rotations. * Evaluate the expected payoff using iterative amplitude estimation. The algorithmic framework is called Quantum Monte-Carlo Integration. For a basic example, see [QMCI](https://github.com/Classiq/classiq-library/blob/main/algorithms/amplitude_amplification_and_estimation/qmc_user_defined/qmc_user_defined.ipynb). In the link, we use a simular framework to estimate European call option, where the underlying asset distribution at the maturity data is modeled as log-normal distribution. ## Data Definitions The problem inputs are: * `NUM_QUBITS`: the number of qubits representing an underlying asset * `NUM_ASSETS`: the number of underlying assets * `K`: the strike price * `S0`: the arrays of underlying assets prices * `dt`: the number of days to the maturity date * `COV`: the covariance matrix that correlate the underlying assets * `MU_LOG_RET`: the array containing the mean of the log return of each underlying asset ```python theme={null} import numpy as np import scipy from classiq import * NUM_QUBITS = 2 NUM_ASSETS = 2 K = 190 S0 = [193.97, 189.12] # Initial prices of the assets dt = 250 COV = np.array([[0.000335, 0.000257], [0.000257, 0.000418]]) MU_LOG_RET = np.array([0.00050963, 0.00062552]) ``` ```python theme={null} MU = MU_LOG_RET * dt CHOLESKY = np.linalg.cholesky(COV) * np.sqrt(dt) SCALING_FACTOR = 1 / CHOLESKY[0, 0] ``` ## Gaussian State Preparation Encode the probability distribution of a discrete multivariate random variable $W$ taking values in $\{w_0, .., w_{N-1}\}$ describing the assets' prices at the maturity date. The number of discretized values, denoted as $N$, depends on the precision of the state preparation module and is consequently connected to the number of qubits $n$ by $N=2^n$. $$ \sum_{i=0}^{N-1} \sqrt{p(w_i)}\left|w_i\right\rangle $$ ```python theme={null} def gaussian_discretization(num_qubits, mu=0, sigma=1, stds_around_mean_to_include=3): """ Discretizes a Gaussian distribution into a set of sample points and their corresponding probabilities for using QNums more accurately. """ lower = mu - stds_around_mean_to_include * sigma upper = mu + stds_around_mean_to_include * sigma num_of_bins = 2**num_qubits sample_points = np.linspace(lower, upper, num_of_bins + 1) def single_gaussian(x: np.ndarray, _mu: float, _sigma: float) -> np.ndarray: cdf = scipy.stats.norm.cdf(x, loc=_mu, scale=_sigma) return cdf[1:] - cdf[0:-1] non_normalized_pmf = (single_gaussian(sample_points, mu, sigma),) real_probs = non_normalized_pmf / np.sum(non_normalized_pmf) return sample_points[:-1], real_probs[0].tolist() grid_points, probabilities = gaussian_discretization(NUM_QUBITS) STEP_X = grid_points[1] - grid_points[0] MIN_X = grid_points[0] a = STEP_X / SCALING_FACTOR ``` # ## Sanity Check ```python theme={null} assert K <= max( S0 * np.exp(np.dot(CHOLESKY, [grid_points[-1]] * 2) + MU) ), "If K always greater than the maximum reachable asset values. Stop the run, the payoff is 0" ``` ## Maximum Computation # ## Precision Utils for Accurate Arithmetic Operations ```python theme={null} FRAC_PLACES = 1 def round_factor(a): precision_factor = 2**FRAC_PLACES return np.floor(a * precision_factor) / precision_factor ``` # ## Affine and Maximum Arithmetic Definitions Considering the time delta between the starting date ($t_0$) and the maturity date ($t$), we can express the return value $R_i$ for the $i$-th asset as $R_i = \mu_i + y_i$. Where: $\mu_i= (t-t_0)\tilde{\mu}_i$, being $\tilde{\mu}_i$ the expected daily log-return value. It can be estimated by considering the historical time series of log returns for the $i$-th asset. $y_i$ is obtained through the dot product between the matrix $\mathbf{L}$ and the standard multivariate Gaussian sample: $$ y_i = \Delta x \cdot \sum_kl_{ik}d_k + x_{min} \cdot \sum_k l_{ik} $$ $\Delta x$ is the Gaussian discretization step, $x_{min}$ is the lower Gaussian truncation value and $d_k \in [0,2^m-1]$ is the sample taken from the $k$-th standard Gaussian. $l_{ik}$ is the $i,k$ entry of the matrix $\mathbf{L}$, defined as $\mathbf{L}=\mathbf{C}\sqrt{(t-t_0)}$, where $\mathbf{C}$ is the lower triangular matrix obtained by applying the Cholesky decomposition to the historical daily log-returns correlation matrix. ```python theme={null} from functools import reduce from classiq.qmod.symbolic import max as qmax def get_affine_formula(assets, i): """ Affine formula for the i-th asset in a compact way. reduce(lambda x, y: x+y, [1, 2, 3, 4, 5]) calculates ((((1+2)+3)+4)+5) assets: list of assets (QNum), hold asset prices """ return reduce( lambda x, y: x + y, [ assets[j] * round_factor(SCALING_FACTOR * CHOLESKY[i, j]) for j in range(NUM_ASSETS) if CHOLESKY[i, j] ], ) c = round_factor( a=(1 / a) * ( np.log(S0[1]) + MU[1] - (np.log(S0[0]) + MU[0]) + MIN_X * sum(CHOLESKY[1] - CHOLESKY[0]) ) ) ``` # ### Extra Information: * *permutation*: A quantum operation is a permutation if it maps computational-basis states to computational-basis states (with possible phase shifts). Such an operation neither introduces nor destroys quantum superpositions, and its computation can be described classically. * *const*: A parameter of a quantum operation is constant if it is immutable up to a phase. That is, the magnitudes of its computational-basis state components remain unchanged, while their phases may shift. For example: * `Z` is a permutation and its parameter is const as it merely flips the phase of the $|1\rangle$ state. * `X` is a permutation but its parameter is not const as it flips between $|0\rangle$ and $|1\rangle$. * `H` is not a permutation and its parameter is not const, as it introduces superposition. * `SWAP` is a permutation but its two parameters are not const, as it swaps between $|01\rangle$ and $|10\rangle$. For more information, see the [Uncomputation](https://docs.classiq.io/latest/qmod-reference/language-reference/uncomputation/) documentation. # ## Instructions For the following function, you need to compute the maximum of two affine expressions and assign the result to `res`. Use the `qmax` function from the `classiq.qmod.symbolic` module to compute the maximum of two expressions, and the [Out-of-place assignment](https://docs.classiq.io/latest/qmod-reference/language-reference/statements/assignment/) operator `|=` to assign the result to `res`. The `qmax` need to choose the maximum between two expressions: * The affine formula of the first asset: `get_affine_formula([x1, x2], 0)` * The affine formula of the second asset plus the constant `c`: `get_affine_formula([x1, x2], 1) + c`. ```python theme={null} @qperm def affine_max(x1: Const[QNum], x2: Const[QNum], res: Output[QNum]): """ In rainbow options, we compute the maximum of all assets. Computes the maximum of two affine expressions and assigns the result to res. """ # Your code # Solution start res |= qmax( x=get_affine_formula([x1, x2], 0), y=get_affine_formula([x1, x2], 1) + c ) # Solution end ``` ## Brute-Force Amplitude Loading Method This type of amplitude loading has an exponential scale, is used for validating result from the direct method and integration method that are part of the paper \[[1](#qalrop)]. We use here the Classiq [amplitude loading](https://docs.classiq.io/latest/qmod-reference/api-reference/functions/open_library/amplitude_loading/) functionality using the `assign_amplitude_table` and `lookup_table` functions to load the normalized payoff function $f(x)$ into the indicator qubit: $$ |x\rangle |0\rangle \rightarrow \sqrt{1-f^{2}(x)}|x\rangle |0\rangle + f(x)|x\rangle |1\rangle $$ Using the amplitude loading of the payoff function, we can estimate the expected value of the payoff function $E[f(x)]$ by measuring the indicator qubit and calculating the probability of measuring $|1\rangle$ using the iterative quantum amplitude estimation (IQAE) algorithm \[[4](#iqae)]. First, we will build the amplitude loading of the payoff function $f(x)$. This is the brute-forced method, in the paper, there two more efficient methods, the direct method and the integration method, which are implemented in the [Classiq library](https://github.com/Classiq/classiq-library). Then, we will put that in the iterative quantum amplitude estimation (IQAE) algorithm to estimate the expected value of the payoff function. # ## The Payoff Function Expression ```python theme={null} def get_payoff_expression(x, size, fraction_digits): """ Expression of the payoff for the rainbow option. Similar to Fig. [1] in the article for calculating in the price space. """ payoff = np.sqrt( max( S0[0] * np.exp( a * (2 ** (size - fraction_digits)) * x + (MU[0] + MIN_X * CHOLESKY[0].sum()) ), K, ) ) return payoff # We want the probability of measuring |1> in the indicator qubit (ind_reg), see below. # So we create a normalized version of the payoff function. def get_payoff_expression_normalized(x: QNum, size, fraction_digits): x_max = 1 - 1 / (2**size) payoff_max = get_payoff_expression(x_max, size, fraction_digits) payoff = get_payoff_expression(x, size, fraction_digits) return payoff / payoff_max ``` # ## The Amplitude Loading of the Payoff Function For each value of $|x\rangle$, we want to load the value of $f(x)$ into the amplitude of the indicator qubit $|ind\rangle$. Therefore, we use the `assign_amplitude_table` function with the `lookup_table` function inside it. The payoff function (`get_payoff_expression_normalized`) is the heart of the computation, and thus it used inside the lookup table function. See [example](https://docs.classiq.io/latest/qmod-reference/api-reference/functions/open_library/amplitude_loading/) in the Classiq documentation for more details. ```python theme={null} @qfunc def brute_force_payoff(max_reg: Const[QNum], ind_reg: QBit): max_reg_fixed = QNum( size=max_reg.size, is_signed=UNSIGNED, fraction_digits=max_reg.size ) # change the QNum register to values between 0 and 1, # it necessary for the amplitude loading bind(source=max_reg, destination=max_reg_fixed) def my_amplitude_loading_payoff_function(x: QNum) -> float: return get_payoff_expression_normalized( x=x, size=max_reg.size, fraction_digits=max_reg.fraction_digits ) # Your code # Build the amplitude loading of the payoff function # Use assign_amplitude_table, lookup_table, my_amplitude_loading_payoff_function, max_reg_fixed, and ind_reg. # Then, use the bind function again to change back the max_reg_fixed to max_reg. # It brings back the encoding (number of qubits, the signedness, and the fraction digits) to the original max_reg, so the output of the function stays the same. # Solution start assign_amplitude_table( amplitudes=lookup_table( func=my_amplitude_loading_payoff_function, targets=max_reg_fixed, ), index=max_reg_fixed, indicator=ind_reg, ) # change back to original values bind(source=max_reg_fixed, destination=max_reg) # Solution end ``` # ## Allocation of Quantum Variables It is useful to allocate quantum structure `QStruct` to hold the quantum variables used in the algorithm. ```python theme={null} class EstimationVars(QStruct): x1: QNum[NUM_QUBITS, UNSIGNED, 0] x2: QNum[NUM_QUBITS, UNSIGNED, 0] ``` ## Brute Force Method for Rainbow Options \*\*We first prepare distribution of the assets and them apply the affine transformation and bring the assets to the maturity date. Then we apply the amplitude loading using the `brute_force_payoff` function.\*\* Then, we uncompute the registers to stay with the correct state using local variable `max_out` inside the function. In the paper \[[1](#qalrop)], it makes the following operation: $$ A|0\rangle = \sqrt{1-a^{2}}|\psi_{0}\rangle|0\rangle + a|\psi_{1}\rangle|1\rangle $$ where $A$ is a unitary operator while $|\psi_{1}\rangle$ and $|\psi_{1}\rangle$ are some normalized states. Thus, $a$ is the probability of measuring $|1\rangle$ in the last qubit. The value $a$ is: $$ a=\sum_{i=0}^{N-1} \tilde{f}(w_i)p(w_i) = E[\tilde{f}] $$ Which is estimated later on by the IQAE algorithm. ```python theme={null} @qfunc def rainbow_brute_force(qvars: EstimationVars, ind: QBit) -> None: inplace_prepare_state(probabilities, 0, qvars.x1) inplace_prepare_state(probabilities, 0, qvars.x2) max_out = QNum() affine_max(qvars.x1, qvars.x2, max_out) brute_force_payoff(max_reg=max_out, ind_reg=ind) # Automatically uncompute the max_out register. ``` # ## Building the Quantum Model In Classiq, we build the quantum model in the `main` function. ```python theme={null} @qfunc def main(qvars: Output[EstimationVars], ind: Output[QBit]) -> None: allocate(qvars) allocate(ind) rainbow_brute_force(qvars=qvars, ind=ind) ``` # ## Synthesizing the Quantum Model ```python theme={null} MAX_WIDTH_1 = 14 qprog = synthesize( model=main, constraints=Constraints( max_width=MAX_WIDTH_1, # optimization_parameter = "depth", "width","cx" ), ) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3G9AfAKsbCTP3NqnbKXt86nLMzf ``` ## IQAE Algorithm The IQAE algorithm estimates $a$ by using the $A$ operation (that uses the `rainbow_brute_force` function) in the grover algorithm. It repeats the Grover operation iteratively until the desired precision is reached according to a certain criteria \[[4](#iqae)]. In each iteration, it repeats the Grover operation with a different number of iterations $k$. The `IQAE` class allows to easily use the IQAE algorithm. You are welcome to see how it is built inside. ```python theme={null} from classiq.applications.iqae.iqae import IQAE ?IQAE ``` **Output:** ``` Init signature: IQAE(  state_prep_op: classiq.qmod.quantum_callable.QCallable[(classiq.qmod.qmod_variable.QArray[(<class 'classiq.qmod.qmod_variable.QBit'>, typing.Literal['problem_vars_size'])], <class 'classiq.qmod.qmod_variable.QBit'>)],  problem_vars_size: int,  constraints: classiq.interface.generator.model.constraints.Constraints | None = None,  preferences: classiq.interface.generator.model.preferences.preferences.Preferences | None = None, ) -> None Docstring: Implementation of Iterative Quantum Amplitude Estimation [1]. Given $A$ s.t. $A|0>_n|0> = \sqrt{1-a}|\psi_0>_n|0> + \sqrt{a}|\psi_1>_n|1>$, the algorithm estimates $a$ by iteratively sampling $Q^kA$, where $Q=AS_0A^{\dagger}S_{\psi_0}$, and $k$ is an integer variable. For estimating $a$, The algorithm estimates $\theta_a$ which is defined by $a = sin^2(\theta_a)$, so it starts with a confidence interval $(0, \pi/2)$ and narrows down this interval on each iteration according to the sample results. References: [1]: Grinko, D., Gacon, J., Zoufal, C., & Woerner, S. (2019). Iterative Quantum Amplitude Estimation. `arXiv:1912.05559 [https://arxiv.org/abs/1912.05559](https://arxiv.org/abs/1912.05559)`. File: ~/user_env3.11/lib/python3.11/site-packages/classiq/applications/iqae/iqae.py Type: type Subclasses: ``` ```python theme={null} MAX_WIDTH_2 = 25 iqae = IQAE( state_prep_op=rainbow_brute_force, problem_vars_size=NUM_QUBITS * NUM_ASSETS, constraints=Constraints(max_width=MAX_WIDTH_2), ) ``` ```python theme={null} iqae_qprog = iqae.get_qprog() show(iqae_qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3G9AhkbY5MdtCKCp6uHOLjzz8bB ``` ## Executing the IQAE Algorithm Executes the IQAE algorithm iteratively, according to the [IQAE algorithm](https://arxiv.org/abs/1912.05559). Basically, it is running the quantum program multiple times with different number of Grover iterations $k$ until the desired precision is reached. In each iteration, it adds a different number of Grover repetitions to the quantum circuit based on the results of the previous iterations according to the protocol in the article. The complexity is similar to the regular QAE but uses less qubits and thus is more appealing for simulations. ```python theme={null} EPSILON_VALUE = 0.05 ALPHA_VALUE = 0.1 result = iqae.run( epsilon=EPSILON_VALUE, alpha=ALPHA_VALUE, execution_preferences=ExecutionPreferences(num_shots=20000), ) ``` ## Post Process We need to add to the post-processing function a term: $$ \begin{split} &\mathbb{E} \left[\max\left(e^{b \cdot z}, Ke^{-b'}\right) \right] e^{b'} - K \\ = &\mathbb{E} \left[\max\left(e^{-a\hat{x}}, Ke^{-b'-ax_{max}}\right) \right]e^{b'+ ax_{max}} - K \end{split} $$ ```python theme={null} def calculate_max_reg_type(): x1 = QNum(size=NUM_QUBITS, is_signed=UNSIGNED, fraction_digits=0) x2 = QNum(size=NUM_QUBITS, is_signed=UNSIGNED, fraction_digits=0) expr = qmax( x=get_affine_formula([x1, x2], 0), y=get_affine_formula([x1, x2], 1) + c ) size_in_bits, sign, fraction_digits = get_expression_numeric_attributes( [x1, x2], expr ) return size_in_bits, fraction_digits MAX_NUM_QUBITS = calculate_max_reg_type()[0] MAX_FRAC_PLACES = calculate_max_reg_type()[1] ``` ```python theme={null} import sympy payoff_expression = f"sqrt(max([{S0[0]} * exp({STEP_X / SCALING_FACTOR * (2 ** (MAX_NUM_QUBITS - MAX_FRAC_PLACES))} * x + ({MU[0]+MIN_X*CHOLESKY[0].sum()})), {K}]))" payoff_func = sympy.lambdify(sympy.symbols("x"), payoff_expression) payoff_max = payoff_func(1 - 1 / (2**MAX_NUM_QUBITS)) def parse_result_bruteforce(iqae_res): option_value = iqae_res.estimation * (payoff_max**2) - K confidence_interval = np.array(iqae_res.confidence_interval) * (payoff_max**2) - K return (option_value, confidence_interval) ``` ## Run Method See the IQAE results. ```python theme={null} parsed_result, conf_interval = parse_result_bruteforce(result) print( f"raw iqae results: {result.estimation} with confidence interval {result.confidence_interval}" ) print( f"option estimated value: {parsed_result} with confidence interval {conf_interval}" ) ``` **Output:** ``` raw iqae results: 0.50205 with confidence interval [0.4919327565979882, 0.5121672434020118] option estimated value: 22.12935868344408 with confidence interval [17.85455666 26.4041607 ] ``` ```python theme={null} expected_payoff = 23.0238 ALPHA_ASSERTION = 1e-5 measured_confidence = conf_interval[1] - conf_interval[0] confidence_scale_by_alpha = np.sqrt( np.log(ALPHA_VALUE / ALPHA_ASSERTION) ) # based on e^2=(1/2N)*log(2T/alpha) from "Iterative Quantum Amplitude Estimation" since our alpha is low, we want to check within a bigger confidence interval diff = np.abs(parsed_result - expected_payoff) allowed_error = 0.5 * measured_confidence * confidence_scale_by_alpha if diff <= allowed_error: print("Payoff is within the confidence interval.") else: print( f"Payoff result is out of the {ALPHA_ASSERTION*100}% confidence interval: |{parsed_result} - {expected_payoff}| > {0.5*measured_confidence * confidence_scale_by_alpha}" ) ``` **Output:** ``` Payoff is within the confidence interval. ``` ## References \[1]: [Francesca Cibrario et al., Quantum Amplitude Loading for Rainbow Options Pricing. Preprint](https://arxiv.org/abs/2402.05574v2) \[2]: [Paul Glasserman, Monte Carlo Methods in Financial Engineering. Springer-Verlag New York, 2003, p. 596.](https://link.springer.com/book/10.1007/978-0-387-21617-1)
\[3]: [Gilles Brassard, Peter Hoyer, Michele Mosca, and Alain Tapp, Quantum Amplitude Amplification and Estimation. Contemporary Mathematics 305 (2002)](https://arxiv.org/abs/quant-ph/0005055) \[4]: [Grinko, Dmitry, et al. "Iterative quantum amplitude estimation." npj Quantum Information 7.1 (2021): 52.](https://arxiv.org/abs/1912.05559) # Grover from Functional Building Blocks Source: https://docs.classiq.io/explore/tutorials/workshops/grover_workshop/grover_workshop Open this notebook in GitHub to run it yourself ## Setting the Scene ```python theme={null} # !pip install -U classiq ``` ```python theme={null} new_classiq_user = False if new_classiq_user: import classiq classiq.authenticate() ``` ## Warm Up # ## First Example Write a function that prepares the plus state $|{+}\rangle^{\otimes n}=\left[\frac{1}{\sqrt2}(|{0}\rangle+|{1}\rangle)\right]^{\otimes n}$, assuming the state of the input quantum variable is $|x\rangle^{\otimes n}$ Use either `apply_to_all()` with `H(x)`, or `hadamard_transform()`. ```python theme={null} from classiq import * from classiq.qmod.symbolic import pi ``` Now we will test our code: ```python theme={null} n = 5 @qfunc def main(x: Output[QArray[QBit]]): allocate(5, x) hadamard_transform(x) # Prepare the plus state ``` ```python theme={null} qprog = synthesize(main) ``` ```python theme={null} show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3BDZ0CrFpw76x6WbQ3GSz1oh9aD ``` Some basic explanations about the high-level functional design with Classiq: * There should always be a main (`def main(...)`) function - the model that captures your algortihm is described there * The model is always generated out of the main function * The model is sent to the synthesis engine (compiler) that return a quantum program which contains the quantum circuit Some basic guidelines about the modeling language (Qmod): 1. Every quantum variable should be declared, either as a parameter of a funciton e.g. `def main(x: Output[QBit])` or within the function itself with `x = QBit()` 2. Some quantum variables need to be initalized with the `allocate` function. This is required in 2 cases: * A variable is a parameter of a function with the declaration `Output` like `def main(x: Output[QNum])` * A variable that is declared within a function like `a = QNum()` 3. For the `main` function, you should always use `Output` for all variables, as the function does not receive any input Important tip! You can see all the declarations of the functions with their parameters in the `functions.py` file within the classiq package (or by just right clicking a function and presing `Go To Defintion`) ## Grover's Algorithm * Summary Before diving into Grover's algorithm implementation, it is important to analyze its different building blocks. We start by looking at the overall quantum algorithm and then start build it step-by-step. Visualization * **Initial state preparation**: The algorithm starts by preparing a uniform superposition $\lvert + \rangle^{\otimes n}$ using Hadamard gates. This ensures that all possible states are explored simultaneously. At this stage, each state has equal probability amplitude. * **Grover Oracle**: The oracle encodes the problem by marking the solution state(s). It does this by applying a phase flip, effectively distinguishing "good" states from "bad" ones. Importantly, it does not reveal the solution directly, only modifies its phase. * **Grover Diffuser**: The diffuser amplifies the probability of the marked states through a reflection about the average amplitude. This step increases the likelihood of measuring the correct solution. Repeating the oracle and diffuser gradually concentrates probability on the target state. ## Oracle * Reflection About Bad States # ## Theoretical Background Overall we can understand the Grover operator as composed of two reflection operators: 1. about the superposition of 'bad states' (i.e. not the solutions) 2. about the initial guess state In this section we will build the first reflection operator which is also the implementation of the oracle function. Geometrically it can be understood in the 2D vector space of $\text{Span}\{|{\psi_{\text{good}}}\rangle,|{\psi_{\text{bad}}}\rangle\}$. reflect The above figure describe geometrically the reflection of some state $|{\psi}\rangle=\alpha|{\psi_\text{good}}\rangle+\beta|{\psi_\text{bad}}\rangle$ about the state $|{\psi_\text{bad}}\rangle$ such that $$ R(\alpha|{\psi_\text{good}}\rangle+\beta|{\psi_\text{bad}}\rangle) = -\alpha|{\psi_\text{good}}\rangle+\beta|{\psi_\text{bad}}\rangle $$ This operator can also be written as $$ R|{x}\rangle=(-1)^{(x==\text{good solution})}|{x}\rangle $$ so if the state of $x$ is a solution it gets a $(-)$ phase. # ## Implementation Now we turn to actually implementing the oracle. With Qmod quantum expressions, capturing the intent becomes straightforward. The compiler does the heavy-lifting of synthesizing the reversible circuits for us. For our purposes, we want to find all the states that obey $2a+b=c$ so there are 3 quantum variables. In addition, we want to store our results in the relative phase of such states. In other words, we want: $$ |{a,b,c}\rangle\rightarrow(-1)^{(2a+b==c)}|{a,b,c}\rangle $$ In a visual representation, this is what we want: phase_oracle Now we can implement it by defining the `oracle_function`: ```python theme={null} @qperm def oracle_function(a: Const[QNum], b: Const[QNum], c: Const[QNum]): control((2 * a + b == c), phase(pi)) ``` ## Diffuser * Reflection About Initial Guess # ## Theoretical Background The second part of the Grover operator is the diffuser, which can be viewed as the reflection operator about our initial guess. diffuser As with the oracle reflection operator, we can describe any state $|{\psi}\rangle$ as a superposition of the initial state $|{\psi_0}\rangle$ such that and the orthogoanl state to it $|{\psi_0^{\bot}}\rangle$ $$ |{\psi}\rangle = \alpha |{\psi_0}\rangle +\beta |{\psi_0^{\bot}}\rangle $$ Here we want to apply a $\pi$ phase to all states that are not equal our initial guess. The reflection operator (our diffuser) is defined as: $$ R(\alpha |{\psi_0}\rangle +\beta |{\psi_0^{\bot}}\rangle) = \alpha |{\psi_0}\rangle -\beta |{\psi_0^{\bot}}\rangle $$ To implement a reflection about the initial state $\vert \psi_0 \rangle$, we instead perform a reflection about the computational zero state $\vert 0 \rangle$, conjugated by our state-preparation unitary for the initial state $\vert \psi_0 \rangle$. That is, if $U_{\psi_0}|{0}\rangle=|{\psi_0}\rangle$ then we will implement the desired $R$ operator with: $$ R = U_{\psi_0}R_0 U_{\psi_0}^{\dagger} $$ where $R_0$ is the reflection operator about the zero state: $$ R_0|{x}\rangle = (-1)^{(x\ne0)}|{x}\rangle= (2|{0}\rangle\langle{0}|-I)|{x}\rangle $$ # ## Implementation We will use the controlled `phase` operation once more. To conjugate it within Hadamard transforms, we use `within_apply`: ```python theme={null} @qfunc def grover_diffuser(state: QNum) -> None: within_apply( lambda: hadamard_transform(state), lambda: control(state != 0, lambda: phase(pi)), ) ``` ## Putting All Together That's it! Complete your grover operator by implementing the two functions that you've built, first the `oracle_function` and then the `grover_diffuser`: ```python theme={null} @qfunc def my_grover_operator(a: QNum, b: QNum, c: QNum): # TODO complete here pass ``` Now that we have our Grover operator, we can run it within our code. We have 3 steps here: 1. Initalize `a`,`b` and `c` within the scope of the `main` function using the `allocate` operation 2. Create the initial states for `a`,`b` and `c` 3. Apply your Grover operator ```python theme={null} size_a = 2 size_b = 2 size_c = 3 @qfunc def main(a: Output[QNum], b: Output[QNum], c: Output[QNum]): allocate(size_a, a) allocate(size_b, b) allocate(size_c, c) # TODO complete here pass ``` Synthesize your model: ```python theme={null} qprog = synthesize(main) ``` And view it within the IDE: ```python theme={null} show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3BDZ0Oj00fjmqsR9QKcsGqmJ0HB ``` Is it what you were expecting? Now we can play with the constraints as we did in the IDE: ```python theme={null} qprog_depth_optimized = synthesize( main, constraints=Constraints(optimization_parameter="depth") ) # or 'width' show(qprog_depth_optimized) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3BDZ0fTM0RneZZeGNTJ9XxpiAx6 ``` # ## CONGRATULATIONS! You have completed your own Grover algorithm implementation from functional building blocks without sweeping under the rug any details, really impressive work! # ## The Full Solution for Your Reference ```python theme={null} from classiq import * from classiq.qmod.symbolic import pi @qperm def oracle_function(a: Const[QNum], b: Const[QNum], c: Const[QNum]): control((2 * a + b == c), lambda: phase(pi)) @qfunc def grover_diffuser(state: QNum) -> None: within_apply( lambda: hadamard_transform(state), lambda: control(state != 0, lambda: phase(pi)), ) @qfunc def my_grover_operator(a: QNum, b: QNum, c: QNum): oracle_function(a, b, c) grover_diffuser([a, b, c]) @qfunc def main(a: Output[QNum], b: Output[QNum], c: Output[QNum]): allocate(size_a, a) allocate(size_b, b) allocate(size_c, c) hadamard_transform([a, b, c]) my_grover_operator(a, b, c) qprog = synthesize(main) show(qprog) constraints = Constraints(optimization_parameter="depth") # or 'width' qprog_depth_optimized = synthesize(main, constraints=constraints) show(qprog_depth_optimized) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3BDZ1HvwH1GQxmlAVeElrmEtqZm Quantum program link: https://platform.classiq.io/circuit/3BDZ2l0vxNe8XvU6ZbqoUsEvTnN ``` # Modeling an HHL Algorithm to Solve a Set of Linear Equations Source: https://docs.classiq.io/explore/tutorials/workshops/hhl_workshop/hhl_workshop Open this notebook in GitHub to run it yourself Guidance for the workshop: **The `# TODO` is there for you to do yourself.** \*\*The `# Solution start` and `# Solution end` are only for helping you. Try doing it yourself...\*\* Solving linear equations appears in many research, engineering, and design fields. For example, many physical and financial models, from fluid dynamics to portfolio optimization, are described by partial differential equations, which are typically treated by numerical schemes, most of which are eventually transformed to a set of linear equations. The HHL algorithm \[[1](#hhl)] is a quantum algorithm for solving a set of linear equations. It is one of the fundamental quantum algorithms that is expected to give a speedup over its classical counterpart. A set of linear equations of size $N$ is represented by an $N\times N$ matrix and a vector $b$ of size $N$, $A\vec{x} = \vec{b}$, where the solution to the problem is designated by the solution variable $\vec{x}$. For simplicity, the demo below treats a usecase where $\vec{b}$ is a normalized vector $|\vec{b}|=1$, and $A$ is an Hermitian matrix of size $2^n\times 2^n$, whose eigenvalues are in the interval $(0,1)$. Generalizations to other usecases are discussed at the end of this demo. ## 1. Defining a Specific Problem Start by defining the specific problem. ```python theme={null} # !pip install -U classiq ``` ```python theme={null} import numpy as np import scipy as scipy A = np.array( [ [0.28, -0.01, 0.02, -0.1], [-0.01, 0.5, -0.22, -0.07], [0.02, -0.22, 0.43, -0.05], [-0.1, -0.07, -0.05, 0.42], ] ) b = np.array([1, 2, 4, 3]) b = b / np.linalg.norm(b) print("A =", A, "\n") print("b =", b, "\n") # verifying that the matrix is symmetric and has eigenvalues in [0,1) if not np.allclose(A, A.T, rtol=1e-6, atol=1e-6): raise Exception("The matrix is not symmetric") w, v = np.linalg.eig(A) for lam in w: if lam < 0 or lam > 1: raise Exception("Eigenvalues are not in (0,1)") sol_classical = np.linalg.solve(A, b) print("Classical solution: x = ", sol_classical) num_qubits = int(np.log2(len(b))) ``` **Output:** ``` A = [[ 0.28 -0.01 0.02 -0.1 ] [-0.01 0.5 -0.22 -0.07] [ 0.02 -0.22 0.43 -0.05] [-0.1 -0.07 -0.05 0.42]] b = [0.18257419 0.36514837 0.73029674 0.54772256] Classical solution: x = [1.3814374 2.50585064 3.19890483 2.43147877] ``` ## 2. Building Simple HHL with Classiq # ## 2.1 Define the Model This tutorial gives instructions on building an HHL algorithm and presents the theory of the algorithm. The algorithm consists of 4 steps: 1. State preparation of the RHS vector $\vec{b}$. 2. QPE for the unitary matrix $e^{2\pi iA}$, which encodes eigenvalues on a quantum register of size $m$. 3. An inversion algorithm that loads amplitudes according to the inverse of the eigenvalue registers. 4. An inverse QPE with the parameters in (2). # ### 2.1.1 State Preparation for the Vector $\vec{b}$ The first stage of the HHL algorithm is to load the normalized RHS vector $\vec{b}$ into a quantum register: $$ |0\rangle_n \xrightarrow[{\rm SP}]{} \sum^{2^n-1}_{i=0}b_i|i\rangle_n $$ where $|i\rangle$ are states in the computational basis. Comments: * The relevant built-in function is the `prepare_amplitudes` one, which gets $2^n$ values of $\vec{b}$, as well as an upper bound for its functional error through the `bound` parameter. ```python theme={null} from classiq import * @qfunc def load_b(b: CArray[CReal], res: Output[QArray]) -> None: # TODO prepare the state |b> in the "res" register - the amplitude of res states correspond to the values of the vector b # Solution start prepare_amplitudes(b, 0.0, res) # Solution end ``` Let's see the loading of b in a state vector simulator Update the qmod to have `aer_simulator_statevector` as backend, with one shot Refer to [Execution Preferences documentation](https://docs.classiq.io/latest/user-guide/execution/#execution-preferences) and to [Classiq backends documentation](https://docs.classiq.io/latest/user-guide/execution/cloud-providers/) ```python theme={null} @qfunc def main(res: Output[QArray]): load_b(b.tolist(), res) qmod_b_load = create_model(main) # TODO update the qmod to have aer_simulator_statevector as backend, with one shot # Solution start qmod_b_load = set_execution_preferences( qmod_b_load, num_shots=1, backend_preferences=ClassiqBackendPreferences(backend_name="simulator_statevector"), ) # Solution end qprog_b_load = synthesize(qmod_b_load) show(qprog_b_load) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/2yjDzAXzUnVNMS5hsJGubDIKBR7 ``` Take a look at the resulted circuit. Now let's execute and see if $b$ was built correctly ```python theme={null} job = execute(qprog_b_load) job.open_in_ide() ``` Check if you see a match between the original $b$ to the resulted state vector ```python theme={null} print("The original b is: ", b) result = job.result_value() print("The resulted state vector :", result.state_vector) ``` **Output:** ``` The original b is: [0.18257419 0.36514837 0.73029674 0.54772256] The resulted state vector : {'00': (0.18257418583505527+0j), '01': (0.3651483716701124+0j), '10': (0.7302967433402199+0j), '11': (0.5477225575051674+0j)} ``` # ### 2.1.2 Quantum Phase Estimation (QPE) for the Hamiltonian Evolution $U=e^{2\pi i A }$ The QPE function block, which is at the heart of the HHL algorithm, operates as follows: Unitary matrices have eigenvalues of norm 1, and thus are of the form $e^{2\pi i \lambda}$, with $0\leq\lambda<1$. For a quantum state $|\psi\rangle_n$, prepared in an eigenvalue of some unitary matrix $U$ of size $2^n\times 2^n$, the QPE algorithm encodes the corresponding eigenvalue into a quantum register: $$ |0\rangle_m|\psi\rangle_n \xrightarrow[{\rm QPE}(U)]{} |\lambda\rangle_m|\psi\rangle_n, $$ where $m$ is the precision of the binary representation of $\lambda$, $\lambda=\frac{1}{2^m}\sum^{2^m-1}_{k=0}\lambda^{(k)}2^k$ with $\lambda^{(k)}$ being the state of the $k$-th qubit. In the HHL algorithm a QPE for the unitary $U=e^{2\pi i A }$ is applied. The mathematics: First, note that the eigenvectors of $U$ are the ones of the matrix $A$, and that the corresponding eigenvalues $\lambda$ defined for $U=e^{2\pi i A }$ are the eigenvalues of $A$. Second, represent the prepared state in the basis given by the eigenvalues of $A$. This is merely a mathematical transformation; with no algorithmic considerations here. If the eigenbasis of $A$ is given by the set $\{|\psi_j\rangle_n \}^{2^n-1}_{j=0}$, then $$ \sum^{2^n-1}_{i=0}b_i|i\rangle_n = \sum^{2^n-1}_{j=0}\beta_j|\psi_j\rangle_n. $$ Applying the QPE stage gives $$ |0\rangle_m \sum^{2^n-1}_{j=0}\beta_j|\psi_j\rangle_n \xrightarrow[{\rm QPE}]{} \sum^{2^n-1}_{j=0}\beta_j |\lambda_j\rangle_m |\psi_j\rangle_n. $$ Comments: * Use the built-in `qpe` function. # ### 2.1.3 Eigenvalue Inversion The next step in the HHL algorithm is to pass the inverse of the eigenvalue registers into their amplitudes, using the Amplitude Loading (AL) construct. Given a function $f:[0,1)\rightarrow [-1,1]$, it implements $|0\rangle|\lambda\rangle_m \xrightarrow[{\rm AL}(f)]{} f(\lambda)|1\rangle|\lambda\rangle_m+\sqrt{1-f^2(\lambda)}|0\rangle|\lambda\rangle_m$. For the HHL algorithm, apply an AL with $f=C/x$ where $C$ is a lower bound for the minimal eigenvalue of $A$. Applying this AL gives $$ \sum^{2^n-1}_{j=0}\beta_j |\lambda_j\rangle_m |\psi_j\rangle_n \xrightarrow[{\rm AL}(C/x)]{} |0\rangle\left(\sum^{2^n-1}_{j=0}\sqrt{1-\frac{C^2}{\lambda^2_j}}\beta_j |\lambda_j\rangle_m |\psi_j\rangle_n\right)+ |1\rangle\left(\sum^{2^n-1}_{j=0}\frac{C}{\lambda_j}\beta_j |\lambda_j\rangle_m |\psi_j\rangle_n\right), $$ where $C$ is a normalization coefficient. The normalization coefficient $C$, which guarantees that the amplitudes are normalized, can be taken as the lower possible eigenvalue that can be resolved with the QPE: $$ C=1/2^{\rm precision}. $$ The built-in construct to define an amplitude loading is the [`assign_amplitude_table`](https://docs.classiq.io/latest/qmod-reference/api-reference/functions/open_library/amplitude_loading/#classiq.open_library.functions.amplitude_loading.assign_amplitude_table) function. ```python theme={null} @qfunc def simple_eig_inv(phase: Const[QNum], indicator: Output[QBit]): # TODO allocate 1 qubit for indicator # TODO load its |1> state amplitude to be C/phase using the assign_amplitude_table function # Solution start allocate(indicator) assign_amplitude_table( lookup_table(lambda p: 0 if p == 0 else (1 / 2**phase.size) / p, phase), phase, indicator, ) # Solution end ``` # ### 2.1.4 Inverse QPE As the final step in the HHL model, clean the QPE register by applying an inverse-QPE. (Note that it is not guaranteed that this register is completely cleaned; namely, that all the qubits in the QPE register return to zero after the inverse-QPE. Generically they are all zero with very high probability). In this model we will simply call the QPE function with the same parameters in stage 2. This is how the quantum state looks now $$ |0\rangle\left(\sum^{2^n-1}_{j=0}\sqrt{1-\frac{C^2}{\lambda^2_j}}\beta_j |\lambda_j\rangle_m |\psi_j\rangle_n\right)+ |1\rangle\left(\sum^{2^n-1}_{j=0}\frac{C}{\lambda_j}\beta_j |\lambda_j\rangle_m |\psi_j\rangle_n\right) \xrightarrow[{\rm inv-QPE}(U)]{} |0\rangle_m|0\rangle\left(\sum^{2^n-1}_{j=0}\sqrt{1-\frac{C^2}{\lambda^2_j}}\beta_j |\psi_j\rangle_n\right)+ |0\rangle_m|1\rangle\left(\sum^{2^n-1}_{j=0}\frac{C}{\lambda_j}\beta_j |\psi_j\rangle_n\right) $$ The state entangled with $|1\rangle$ stores the solution to our problem (up to some normalization problem) $$ \sum^{2^n-1}_{j=0} \frac{C}{\lambda_j}\beta_j \vec{\psi_j} = C\vec{x}. $$ # ### 2.1.5 Putting It All Together Let's remind that the entire HHL algorithm is composed of: 1. State preparation of the RHS vector $\vec{b}$. 2. QPE for the unitary matrix $e^{2\pi iA}$, which encodes eigenvalues on a quantum register of size $m$. 3. An inversion algorithm that loads amplitudes according to the inverse of the eigenvalue registers. 4. An inverse QPE with the parameters in (2). And put all together in `my_hhl` function You can apply QPE$^\dagger$ \* EigenValInv \* QPE using the [within\_apply operator](https://docs.classiq.io/latest/qmod-reference/language-reference/statements/within-apply/) ```python theme={null} @qfunc def my_hhl( fraction_digits: int, b: CArray[CReal], unitary_with_matrix: QCallable[QArray], res: Output[QArray], phase: Output[QNum], indicator: Output[QBit], ) -> None: # TODO Call load_b you created, to load "b" vector into register "res" # Solution start load_b(b, res) # Solution end # TODO allocate a qnum register for "phase". This qnum should be in the range [0,1) with fraction_digits precision # Solution start allocate(fraction_digits, False, fraction_digits, phase) # Solution end # TODO refer to applying (QPE\dagger)*(EigenValInv)*(QPE) : we want to apply "simple_eig_inv" within "qpe" # Solution start within_apply( lambda: qpe(unitary=lambda: unitary_with_matrix(res), phase=phase), lambda: simple_eig_inv(phase=phase, indicator=indicator), ) # Solution end ``` The first entry point of any model would be the `main` function. Since you already have done all the job in `my_hhl` function, all we have to do now is to call it with the relevant inputs Call `my_hhl` with `QPE_SIZE` digits resolution, on the normalized `b`, where the unitary is based on `unitary_mat`. ```python theme={null} QPE_SIZE = 4 @qfunc def main(res: Output[QNum], phase: Output[QNum], indicator: Output[QBit]): b_normalized = b.tolist() unitary_mat = scipy.linalg.expm(1j * 2 * np.pi * A).tolist() # TODO call my_hhl with QPE_SIZE digits resolution, on the normalized b, where the unitary is based on "unitary_mat" # Solution start my_hhl( fraction_digits=QPE_SIZE, b=b_normalized, unitary_with_matrix=lambda target: unitary(elements=unitary_mat, target=target), res=res, phase=phase, indicator=indicator, ) # Solution end ``` # ## 2.2 Add Execution Preferences Once we have a model `qmod_hhl` (by creating it from `main`), we would like to add execution details to prepare for the program execution stage. ```python theme={null} backend_preferences = ClassiqBackendPreferences(backend_name="simulator_statevector") qmod_hhl = create_model( entry_point=main, execution_preferences=ExecutionPreferences( num_shots=1, backend_preferences=backend_preferences ), ) ``` # ## 2.2 Synthesize * From Qmod to Qprog Once we have a high level model, we would like to compile and get the actual quantum program. This is done using the `synthesize` command. ```python theme={null} qprog_hhl = synthesize(qmod_hhl) ``` Viewing in IDE ```python theme={null} show(qprog_hhl) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/2yjE1722b5qbbACiS9aynC2rmde ``` Details about `qprog` as depth for example, can be seen both in IDE and in Python SDK ```python theme={null} print("depth = ", qprog_hhl.transpiled_circuit.depth) ``` **Output:** ``` depth = 464 ``` ```python theme={null} print("depth = ", qprog_hhl.transpiled_circuit.get_circuit_metrics()) ``` **Output:** ``` depth = depth=464 count_ops={'u': 267, 'cx': 286} ``` # ## 2.3 Execution Here we execute the circuit on state vector simulator (the backend for execution is defined before the synthesis stage). We can show the results in the IDE and save the state-vector into a variable. ```python theme={null} job = execute(qprog_hhl) ``` ```python theme={null} job.open_in_ide() ``` ```python theme={null} result = job.result_value() ``` # ## 2.4 Post-Process We would like to look at the answers that are encoded in the amplitudes of the terms that their indicator qubit value=1 ```python theme={null} target_pos = result.physical_qubits_map["indicator"][0] # position of control qubit sol_pos = list(result.physical_qubits_map["res"]) # position of solution phase_pos = list( result.physical_qubits_map["phase"] ) # position of the “phase” register, and flips for endianness as we will use the indices to read directly from the string ``` Define a run over all the relevant strings holding the solution. The solution vector will be inserted into the variable `qsol`. Factor out $C=1/2^m$. ```python theme={null} qsol = [ np.round(parsed_state.amplitude / (1 / 2**QPE_SIZE), 5) for solution in range(2**num_qubits) for parsed_state in result.parsed_state_vector if parsed_state["indicator"] == 1.0 and parsed_state["res"] == solution and parsed_state["phase"] == 0.0 # this takes the entries where the “phase” register is at state zero ] print("Quantum Solution: ", np.abs(qsol) / np.linalg.norm(qsol)) print("Classical Solution: ", sol_classical / np.linalg.norm(sol_classical)) ``` **Output:** ``` Quantum Solution: [0.2840631 0.50843613 0.64683772 0.4923432 ] Classical Solution: [0.28005009 0.5079953 0.6484938 0.49291836] ``` ```python theme={null} fidelity = ( np.abs( np.dot( sol_classical / np.linalg.norm(sol_classical), qsol / np.linalg.norm(qsol), ) ) ** 2 ) print("Solution Fidelity:", fidelity) ``` **Output:** ``` Solution Fidelity: 0.9999806280936833 ``` ## 3. Comparing Classical and Quantum Solutions. Note that the HHL algorithm returns a statevector result up to some global phase (coming from transpilation or from the quantum functions themselves). Therefore, to compare with the classical solution, correct for this global phase. ```python theme={null} sol_classical = np.linalg.solve(A, b) global_phase = np.angle(qsol) qsol_corrected = np.real(qsol / np.exp(1j * global_phase)) print("classical: ", sol_classical) print("HHL: ", qsol_corrected) print( "relative distance: ", round( np.linalg.norm(sol_classical - qsol_corrected) / np.linalg.norm(sol_classical) * 100, 1, ), "%", ) ``` **Output:** ``` classical: [1.3814374 2.50585064 3.19890483 2.43147877] HHL: [1.43559 2.56952 3.26897 2.48819] relative distance: 2.5 % ``` ```python theme={null} import matplotlib.pyplot as plt plt.plot(sol_classical, "bo", label="classical") plt.plot(qsol_corrected, "ro", label="HHL") plt.legend() plt.xlabel("$i$") plt.ylabel("$x_i$") plt.show() ``` output ## 4. Generalizations The usecase treated above is a canonical one, assuming the following properties: 1. The RHS vector $\vec{b}$ is normalized. 2. The matrix $A$ is an Hermitian one. 3. The matrix $A$ is of size $2^n\times 2^n $. 4. The eigenvalues of $A$ are in the range $(0,1)$. However, any general problem that does not follow these conditions can be resolved as follows: 1. As preprocessing, normalize $\vec{b}$ and then return the normalization factor as a post-processing 2. Symmetrize the problem as follows: $$ \begin{pmatrix} 0 & A^T \\ A & 0 \end{pmatrix} \begin{pmatrix} \vec{b} \\ 0 \end{pmatrix} = \begin{pmatrix} 0 \\ \vec{x} \end{pmatrix}. $$ This increases the number of qubits by 1. 2. Complete the matrix dimension to the closest $2^n$ with an identity matrix. The vector $\vec{b}$ will be completed with zeros. $$ \begin{pmatrix} A & 0 \\ 0 & I \end{pmatrix} \begin{pmatrix} \vec{b} \\ 0 \end{pmatrix} = \begin{pmatrix} \vec{x} \\ 0 \end{pmatrix}. $$ 4. If the eigenvalues of $A$ are in the range $[-w_{\min},w_{\max}]$ you can employ transformations to the exponentiated matrix that enters into the Hamiltonian simulation, and then undo them for extracting the results: $$ \tilde{A}=(A+w_{\min}I)\left(1-\frac{1}{2^{m}}\right)\frac{1}{w_{\min}+w_{\max}}. $$ The eigenvalues of this matrix lie in the interval $[0,1)$, and are related to the eigenvalues of the original matrix via $$ \lambda = (w_{\min}+w_{\max})\tilde{\lambda}\left[1/\left(1-\frac{1}{2^{n_{m}}}\right)\right]-w_{\min}, $$ with $\tilde{\lambda}$ being an eigenvalue of $\tilde{A}$ resulting from the QPE algorithm. This relation between eigenvalues is then used for the expression inserted into the eigenvalue inversion, via the `AmplitudeLoading` function. ## References \[1]: [Harrow, A. W., Hassidim, A., & Lloyd, S., Quantum Algorithm for Linear Systems of Equations. Physical Review Letters 103, 150502 (2009)](https://journals.aps.org/prl/abstract/10.1103/PhysRevLett.103.150502). # Quantum Oracles Workshop Source: https://docs.classiq.io/explore/tutorials/workshops/oracle_workshop/oracles_workshop Open this notebook in GitHub to run it yourself **Welcome to the Classiq Workshop for Quantum Oracles!** In this notebook, you will cover hands-on examples and exercises of the following topics: * Defining Quantum Oracles using arithmetics in Classiq * Phase Kickback and Phase encoding * A first example: The Deutsch-Jozsa Algorithm * Unstructured search: Grover's Algorithm \*\*For each exercise, complete the code in the #TODO sections correctly. You can find the complete solutions at the end of this notebook.\*\* Additional resources you should use: * [Classiq documentation](https://docs.classiq.io/latest/) * The Classiq [GitHub repository](https://github.com/Classiq/classiq-library/tree/main/community) * The [community Slack of Classiq](https://short.classiq.io/join-slack) - you can ask any question you have over here **Good luck!** ## Quantum Arithmetics: The Oracle In quantum computing, an oracle is a method used to encode information about a function without revealing its explicit form. An oracle is also known as a black box and plays a crucial role in many quantum algorithms, such as the Deutsch-Jozsa algorithm and Grover's search algorithm. The oracle can be thought of as a tool that, when given a specific input, produces an output according to an unknown function $f(x)$. How is it possible to construct and design an oracle for a quantum algorithm? In general, an oracle is represented by a unitary operator $U_f$. This operator acts on a quantum state to evaluate a binary function $f(x)$. For example, in the context of Grover's and Deutsch-Jozsa algorithm, the oracle $U_f$ takes the action $U_f|x\rangle |y\rangle = |x\rangle |y\oplus f(x)\rangle$. The $\oplus$ represents the XOR operation: * $x \oplus y$ equals to $0$ if $x=y$; * $x \oplus y$ equals to $1$ if $x\neq y$. ![Oracle\_fig](https://docs.classiq.io/resources/oracle_workshop.png) The quantum oracles are developed in order to entangle the $x$ and $y$ qubits according to a set of rules in a particular way we want to. Classiq provides a distinctive and efficient approach to working with oracles, which are defined through arithmetic expressions. Starting with a simple example, we create an oracle for a binary function $f(x,y)$ that follows the arithmetic expression: # ## Quantum Oracles and Arithmetics: A Simple Example $$ \begin{cases} f(x,y) = 1,\text{ if }(2\cdot x+y =4)\\ f(x,y) = 0,\text{ else } \end{cases} $$ with $x\in\{0,1\}$ and $y\in\{0,1,2,3\}$. We first define a quantum function that implements the arithmetic operation described above: ```python theme={null} from classiq import * ``` ```python theme={null} @qfunc def oracle(x: QNum, y: QNum, z: QBit): z ^= 2 * x + y == 4 ``` * The `^=` expression represents an in-place XOR operation between the `z` qubit on the left-hand side and the right-hand side expression, assigning the result to the qubit `z`. A short explanation of this concept can be found [here](https://docs.classiq.io/latest/explore/functions/function_usage_examples/arithmetic/bitwise_xor/bitwise_xor_example/). * Therefore, `z ^= 2*x + y == 4` means that we are doing an XOR operation that follows the rule `2*x + y == 4`, assigning the result to `z` (in-place). Now, let's see how this looks when evaluating this oracle over all possible values of `x` and `y`: ```python theme={null} @qfunc def main(x: Output[QNum], y: Output[QNum], z: Output[QBit]): # Allocating qubits for the x, y, and z variables allocate(1, x) allocate(2, y) allocate(z) hadamard_transform(x) hadamard_transform(y) # calling the oracle oracle(x, y, z) ``` ```python theme={null} # Synthesizing your model and visualizing it qprog_oracle = synthesize(main) show(qprog_oracle) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/36W8qce6Bvdm4BdlW5gIUNkm6kk ``` **Output:** ``` https://platform.classiq.io/circuit/36W8qce6Bvdm4BdlW5gIUNkm6kk?login=True&version=15 ``` ## Quantum Oracles and Arithmetics: Phase Kickback Every quantum algorithm can be decomposed into three key steps: 1) Encoding the data, 2) Manipulating the data, and 3) Extracting the result. In the current class, we are studying the first step, where the data is loaded into the quantum computer. For the second step, the phase kickback is a powerful technique in data manipulation, facilitating the extraction of desired results and allowing more freedom in data encoding techniques. Phase kickback deals with kicking the result of a function to the phase of a quantum state so it can be smartly manipulated with constructive and destructive interferences. The standard way to apply a classical, binary, function $f: \{0, 1\}^n \to \{0, 1\}$ on quantum states is by using the oracle with digital encoding by performing: $$ O_f |x\rangle_n |y\rangle = |x\rangle_n |y\oplus f(x)\rangle. $$ The phase kickback takes the oracle $O_f$ and performs the action $$ |x\rangle \to (-1)^{f(x)}|x\rangle. $$ The circuit that applies the Phase Kickback to a quantum Oracle $O$ is of the following form: ![Oracle\_fig](https://docs.classiq.io/resources/phase_kickback_workshop.png) # ## Exercise: Phase Kickback Apply the phase Kickback to the oracle given in the first example and execute it using the statevector simulator. ```python theme={null} # TODO Write your phase kickback primitive. # TODO Try to write your code by first declaring an auxilliary qubit and making use of the `within-apply statement # TODO And then try to use the more efficient method making use of the `control` and `phase` statements from classiq.qmod.symbolic import pi # TODO Write any functions you may need to implement the phase-kickback primitive @qfunc def main(x: Output[QNum], y: Output[QNum]): # Allocating qubits for the x, y variables allocate(1, x) allocate(2, y) # TODO: implement the phase-kickback procedure qprog_phase_kickback = synthesize(main) show(qprog_phase_kickback) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/36W8r6tJJDeek4w5ODJMuoNzxUn ``` **Output:** ``` https://platform.classiq.io/circuit/36W8r6tJJDeek4w5ODJMuoNzxUn?login=True&version=15 ``` ```python theme={null} # TODO use this code to execute your code on the statevector simulator and check that you received the correct answer import numpy as np backend_prefs = ClassiqBackendPreferences( backend_name=ClassiqSimulatorBackendNames.SIMULATOR_STATEVECTOR ) exec_prefs = ExecutionPreferences(num_shots=1, backend_preferences=backend_prefs) with ExecutionSession(qprog_phase_kickback, execution_preferences=exec_prefs) as es: res = es.sample() # --- - Cleaning up results: keeping only (x,y), dropping ancillas --- - DATA_BITS = 3 # y (2 qubits) + x (1 qubit) rows = [] for st in res.parsed_state_vector: bstr = st.bitstring if set(bstr[:-DATA_BITS]) == {"0"}: # ancilla must be |0⟩ y = int(bstr[-3:-1], 2) x = int(bstr[-1], 2) amp = st.amplitude mag = abs(amp) angle_pi = np.angle(amp) / np.pi # angle in units of π note = " ← solution" if (2 * x + y == 4) else "" rows.append((x, y, mag, angle_pi, note)) rows.sort(key=lambda r: (r[0], r[1])) print("x y |amp| angle/π note") print("----------------------------------") for x, y, mag, ang, note in rows: print(f"{x} {y} {mag:.3f} {ang:+.2f}π {note}") ``` **Output:** ``` x y |amp| angle/π note --------------------------------- - ``` ## Quantum Oracles and Arithmetics: The Deutsch-Jozsa Algorithm Deutch-Jozsa algorithm is a seminal quantum algorithm, well-known for its exponential speed-up over classical algorithms to identify if a binary function is either constant or balanced. Given a binary function $f$, assumed to be either constant or balanced, the Deutsch-Jozsa algorithm requires only one evaluation to assert this, while a classical algorithm would require up to $2^{n-1} +1$ evaluations of the oracle. ![Oracle\_fig](https://docs.classiq.io/resources/dj_workshop.png) # ## Deutsch-Jozsa Algorithm Exercise: In this exercise, we will use the Deutsch-Jozsa algorithm to check if the following function is balanced. ![Oracle\_table](https://docs.classiq.io/resources/Oracle_table.png) The function $f(x)$ assumes its value as $1$ only when the integer value of $| x \rangle$ is even. This is equivalent to the condition that the LSB must be 0 to have a phase flip. We can thus set the rule for the oracle of $f(x)$: Everytime the integer value of the qubit $| x \rangle$ is divisible by $2$, $f$ will output $1$. In other words, the oracle for this function should flip the phases of the even integers. In this case we can cleverly construct such an oracle, but it is not always an easy task to build it. Once you have found the arithmetic expression for the oracle, it is possible to construct this algorithm with only a few lines of code; the synthesis engine handles the hard work (and can optimize for circuit depth or width): When implementing the Deutsch-Jozsa algorithm below, use the new phase and control statements elegant implementation method: ```python theme={null} @qfunc def main(x: Output[QNum]): allocate(3, x) # TODO: Employ the Deutsch-Jozsa algorithm, using the phase and control method (a within apply can and should be used for the Hadamard transforms) ``` ```python theme={null} qprog_deutsch_jozsa = synthesize(main) show(qprog_deutsch_jozsa) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/36W8rYSwSdnzCGT3ylg9xek8mYQ ``` **Output:** ``` https://platform.classiq.io/circuit/36W8rYSwSdnzCGT3ylg9xek8mYQ?login=True&version=15 ``` ```python theme={null} # TODO: Refer to the Deutsch-Jozsa class notebook to help you write a classical post-processing part, ``` ## Quantum Oracles and Arithmetics: The Grover Algorithm Grover's algorithm is a quantum search algorithm, well-known for its ability to search an unsorted database or solve the "unstructured search problem" quadratically faster than any classical counterpart. Given an unsorted list of $N$ elements and a search condition, Grover's algorithm's task is to find the input that satisfies the condition. To achieve this, the algorithm uses an oracle associated to a function $f(x)$, which evaluates to 1 if $x$ is the desired element and 0 otherwise. Grover's algorithm performs about $\sqrt N$ iterations, each one applying a Grover operator that flips the phase of the marked state and then amplifies its amplitude. Repeating this process gradually boosts the marked state's amplitude until it becomes highly probable upon measurement. While a classical computer would require $O(N)$ queries to search a database of $N$ items, Grover's algorithm achieves this in $O(\sqrt N)$ queries, demonstrating the advantage of quantum parallelism and the effects of quantum interference. ![Oracle\_fig](https://docs.classiq.io/resources/grovers_workshop.png) # ## Grover's Algorithm Exercise: In this exercise, we will use the Grover algorithm to solve the following equation: $$ x - y = 2 $$ For this exercise, begin by defining the oracle ${O}$. First, create a `QStruct` that contains the two `QNum` variables, `x` and `y`: ```python theme={null} class Variables(QStruct): x: QNum[2, False, 0] y: QNum[2, False, 0] @qperm def quantum_oracle(vars: Const[Variables], z: QNum): # TODO: Change this placeholder function to define the quantum oracle in terms of vars.x, vars.y, and z z ^= 1 ``` Next, incorporate the quantum oracle into the `grover_search` function that automatically implements the Grover operator iterations: ```python theme={null} @qfunc def main(vars: Output[Variables]): allocate(vars.size, vars) # TODO: Fill the `grover_search` function below with the phase kickback applied to the oracle you have built. # You can do this by using the built-in function phase_oracle, grover_search( reps=5, # TODO: Change the number of repetitions to the correct optimal number oracle=lambda vars: phase_oracle( quantum_oracle, vars ), # Here we leverage the built-in phase_oracle, only having to specify the oracle while the framework takes care of the surrounding setup (you may change it) packed_vars=vars, ) ``` ```python theme={null} # Printing the results to check that the algorithm qprog_grover = synthesize(main) show(qprog_grover) res = execute(qprog_grover).result() counts = res[0].value.parsed_counts counts ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/36W8rvARvlLxoqOoo0d4nvnbnyD ``` **Output:** ``` https://platform.classiq.io/circuit/36W8rvARvlLxoqOoo0d4nvnbnyD?login=True&version=15 ``` **Output:** ``` [{'vars': {'x': 3, 'y': 2}}: 159, {'vars': {'x': 2, 'y': 2}}: 141, {'vars': {'x': 0, 'y': 0}}: 140, {'vars': {'x': 2, 'y': 1}}: 136, {'vars': {'x': 0, 'y': 2}}: 135, {'vars': {'x': 3, 'y': 1}}: 132, {'vars': {'x': 1, 'y': 3}}: 130, {'vars': {'x': 3, 'y': 0}}: 128, {'vars': {'x': 0, 'y': 1}}: 126, {'vars': {'x': 0, 'y': 3}}: 123, {'vars': {'x': 2, 'y': 0}}: 121, {'vars': {'x': 1, 'y': 0}}: 121, {'vars': {'x': 1, 'y': 1}}: 119, {'vars': {'x': 1, 'y': 2}}: 116, {'vars': {'x': 3, 'y': 3}}: 116, {'vars': {'x': 2, 'y': 3}}: 105] ``` Try to also write the Grover algorithm with the new phase and control method (not making use of the built in phase\_oracle function)! ## Solutions # ## Phase Kickback: # ### Elegant Method (Phase and Control) ```python theme={null} from classiq.qmod.symbolic import pi @qfunc def main(x: Output[QNum], y: Output[QNum]): # Allocating qubits for the x, y, and z variables allocate(1, x) allocate(2, y) # Performing Hadamard transform over all qubits hadamard_transform(x) hadamard_transform(y) # Using control and phase together to efficiently flip the phases control(2 * x + y == 4, lambda: phase(pi)) qprog_phase_kickback = synthesize(main) show(qprog_phase_kickback) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/36W8sVtISZDLFmmvGSNgzIaJHgC ``` **Output:** ``` https://platform.classiq.io/circuit/36W8sVtISZDLFmmvGSNgzIaJHgC?login=True&version=15 ``` ```python theme={null} import numpy as np backend_prefs = ClassiqBackendPreferences( backend_name=ClassiqSimulatorBackendNames.SIMULATOR_STATEVECTOR ) exec_prefs = ExecutionPreferences(num_shots=1, backend_preferences=backend_prefs) with ExecutionSession(qprog_phase_kickback, execution_preferences=exec_prefs) as es: res = es.sample() # --- - Cleaning up results: keeping only (x,y), dropping ancillas --- - DATA_BITS = 3 # y (2 qubits) + x (1 qubit) rows = [] for st in res.parsed_state_vector: bstr = st.bitstring if set(bstr[:-DATA_BITS]) == {"0"}: # ancilla must be |0⟩ y = int(bstr[-3:-1], 2) x = int(bstr[-1], 2) amp = st.amplitude mag = abs(amp) angle_pi = np.angle(amp) / np.pi # angle in units of π note = " ← solution" if (2 * x + y == 4) else "" rows.append((x, y, mag, angle_pi, note)) rows.sort(key=lambda r: (r[0], r[1])) print("x y |amp| angle/π note") print("----------------------------------") for x, y, mag, ang, note in rows: print(f"{x} {y} {mag:.3f} {ang:+.2f}π {note}") ``` **Output:** ``` x y |amp| angle/π note ---------------------------------- 0 0 0.354 +0.12π 0 1 0.354 +0.12π 0 2 0.354 +0.12π 0 3 0.354 +0.12π 1 0 0.354 +0.12π 1 1 0.354 +0.12π 1 2 0.354 -0.88π ← solution 1 3 0.354 +0.12π ``` # ## Deutsch-Jozsa (Only New Elegant Method): ```python theme={null} from classiq.qmod.symbolic import pi @qfunc def main(x: Output[QNum]): allocate(3, x) # Employing the Deutsch-Jozsa algorithm, using the oracle we built previouvsly. within_apply( lambda: hadamard_transform(x), lambda: control(x % 2 == 0, lambda: phase(pi)) ) qprog_deutsch_jozsa = synthesize(main) show(qprog_deutsch_jozsa) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/36W8tGMR2neVUPl3uke3oc0IcZf ``` **Output:** ``` https://platform.classiq.io/circuit/36W8tGMR2neVUPl3uke3oc0IcZf?login=True&version=15 ``` ```python theme={null} # Outputting whether the function is constant or balanced: def post_process_deutsch_jozsa(parsed_results): if len(parsed_results) == 1: if 0 not in parsed_results: print("The function is balanced") else: print("The function is constant") else: print( "cannot decide as more than one output was measured, the distribution is:", parsed_results, ) result = execute(qprog_deutsch_jozsa).result_value() results_list = [sample.state["x"] for sample in result.parsed_counts] post_process_deutsch_jozsa(results_list) ``` **Output:** ``` The function is balanced ``` # ## Grover's Algorithm: # ### Regular Method: ```python theme={null} class Variables(QStruct): x: QNum[2, False, 0] y: QNum[2, False, 0] @qperm def quantum_oracle(vars: Const[Variables], z: QNum): z ^= vars.x - vars.y == 2 ``` ```python theme={null} @qfunc def main(vars: Output[Variables]): allocate(vars.size, vars) grover_search( reps=2, oracle=lambda vars: phase_oracle(quantum_oracle, vars), packed_vars=vars, ) ``` ```python theme={null} qprog_grover = synthesize(main) show(qprog_grover) res = execute(qprog_grover).result() counts = res[0].value.parsed_counts counts ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/36W8u1HMOOLX47QslFlC2ZPREvc ``` **Output:** ``` https://platform.classiq.io/circuit/36W8u1HMOOLX47QslFlC2ZPREvc?login=True&version=15 ``` **Output:** ``` [{'vars': {'x': 2, 'y': 0}}: 983, {'vars': {'x': 3, 'y': 1}}: 961, {'vars': {'x': 0, 'y': 0}}: 11, {'vars': {'x': 3, 'y': 3}}: 11, {'vars': {'x': 0, 'y': 1}}: 11, {'vars': {'x': 1, 'y': 1}}: 10, {'vars': {'x': 0, 'y': 3}}: 9, {'vars': {'x': 0, 'y': 2}}: 8, {'vars': {'x': 1, 'y': 3}}: 7, {'vars': {'x': 2, 'y': 3}}: 7, {'vars': {'x': 3, 'y': 2}}: 6, {'vars': {'x': 1, 'y': 2}}: 6, {'vars': {'x': 1, 'y': 0}}: 5, {'vars': {'x': 2, 'y': 1}}: 5, {'vars': {'x': 3, 'y': 0}}: 4, {'vars': {'x': 2, 'y': 2}}: 4] ``` # Qmod Tutorial - Part 1 Source: https://docs.classiq.io/getting-started/classiq_tutorial/Qmod_tutorial_part1 Open this notebook in GitHub to run it yourself In this tutorial, we will cover the basics of the Qmod language and its accompanying library. We will learn to use quantum variables, functions, and operators. Let's begin with a simple code example: ```python theme={null} from classiq import * @qfunc def foo(q: QBit) -> None: X(q) H(q) @qfunc def main(q: Output[QBit]) -> None: allocate(q) foo(q) qprog = synthesize(main) ``` Function `foo` takes the quantum parameter `q` of type `QBit` and applies `X` gate to it, followed by `H` gate. Function `main` declares a single `Output` parameter `q` of type `QBit`. It first allocates a qubit to `q` in the state $\vert 0 \rangle$, then calls `foo` to operate on it. A quantum program `qprog` is created based on function `main`, so that it can later be executed. What results do we expect when executing this quantum program? * By calling `allocate`, `q` is initialized in the default state $|0\rangle$. * Then, `foo` is called: * It applies `X` (NOT gate), changing `q`'s state to $|1\rangle$. * Then it applies `H` (Hadamard gate), resulting in the superposition $\frac{1}{\sqrt{2}} (|0\rangle - |1\rangle)$. When executing the quantum program, the output variable `q` is sampled. We can run the following code and make sure that the states $|0\rangle$ and $|1\rangle$ are sampled roughly equally: ```python theme={null} res = sample(qprog) ``` **Output:** ``` Submitting job to simulator Job: https://platform.classiq.io/jobs/13788264-32b8-4146-8b28-132d8de188b3 ``` ## Qmod Fundamentals The simple model above demonstrates several features that are essential in every Qmod code: # ## The `@qfunc` Decorator Qmod is a quantum programming language embedded in Python. The decorator `@qfunc` designates a quantum function, so that it can be processed by the Qmod tool chain. The Python function is executed at a later point to construct the Qmod representation. This decorator is used for every Qmod function definition. # ## Function `main` A complete Qmod model, that is, a description that can be synthesized and executed, must define a function called `main`. Function `main` is the quantum entry point - it specifies the inputs and outputs of the quantum program, that is, its interface with the external classical execution logic. Similar to conventional programming languages, function `main`, can call other functions. Output variables that are declared in `main` definition are the ones to be measured when the program is executed. # ## Working with Quantum Variables Quantum objects are representations of data (boolean values, numbers, arrays of numbers and so on), that are stored in specific qubits. In Qmod, quantum objects are handled and manipulated using **quantum variables**. Quantum variables must be declared and initialized explicitly. The model above demonstrates two important kinds of declaration: * Function `foo` declares parameter `q` thus: `q: QBit`. This declaration means `foo` expects a pre-existing quantum object. * Function `main` declares parameter `q` thus: `q: Output[QBit]`. In this case, `q` is an output-only parameter - it is initialized inside the scope of `main`. Prior to their initialization, local quantum variables and output parameters do not reference any object (this is analogous to null reference in conventional languages). They may be initialized in the following ways: * Using [`allocate`](https://docs.classiq.io/latest/sdk-reference/qmod/operations/#classiq.qmod.builtins.operations.allocate) to initialize the variable to a new object, with all its qubits in the default $|0\rangle$ state. * Using [numeric assignment](https://docs.classiq.io/latest/user-guide/modeling/quantum-numbers-arithmetics/#numeric-assignment) to initialize the variable to an object representing the result of a quantum expression. * Using functions with output parameters (for example, [state preparation](https://docs.classiq.io/latest/qmod-reference/library-reference/core-library-functions/prepare_state_and_amplitudes/prepare_state_and_amplitudes/)). Note: all the variables in `main` must be declared as `Output`, as `main` is the entry point of the model (Think about it: where could a variable be initialized before `main` was called?). Other functions can declare parameters with or without the `Output` modifier. # ## Exercise #0 Rewrite the above model, so that `q` is initilized inside `foo`. A solution is provided in the end of the notebook. Hint: it only requires to move one line of code and add the `Output` modifier in the correct place. **Why does `foo` need the `Output` modifier?** In the original model, `foo` declares `q: QBit` - a regular parameter. This tells Qmod that `q` must already be an initialized quantum object when `foo` is called; the caller is responsible for creating it. When we move `allocate` inside `foo`, the responsibility shifts: `foo` is now the one creating `q`. At the moment `foo(q)` is called from `main`, `q` has not yet been initialized - it is just an unallocated reference. Passing an uninitialized variable to a regular parameter is not allowed, because Qmod expects a live quantum object there. The `Output` modifier changes this contract: `q: Output[QBit]` tells Qmod that `q` *enters the function uninitialized*, and that the function itself is responsible for initializing it (here, via `allocate`). This lines up with the call site in `main`, where `q` - itself an `Output` parameter - is still unallocated when `foo(q)` is invoked. In short: use `Output` whenever a function is responsible for **creating** a quantum variable, rather than receiving one that already exists. ```python theme={null} from classiq import * # Your code here ... # execute the model to see that we get similar results qprog = synthesize(main) res = sample(qprog) res ``` **Output:** ``` [{'q': 0}: 1051, {'q': 1}: 997] ``` ## Quantum Types, Statements and Opertions Now that we have grasped the principles that are essential for any Qmod code, we can start building up our expressive toolkit, letting us create increasingly sophisticated models. The following exercises introduce some of the most useful variable types, statements and operations that Qmod supports. # ## Exercise #1 * Quantum Arrays After we have familiarized with the `QBit` varible type (which is simply a single qubit), it is a good timing to introduce the quantum array type `QArray`. In this exercise, we will prepare the famous $|\Phi^+\rangle$ [Bell state](https://en.wikipedia.org/wiki/Bell_state) into a 2-qubit [Quantum array](https://docs.classiq.io/latest/qmod-reference/language-reference/quantum-types/#quantum-arrays). Recall that $|\Phi^+\rangle$ represents the state $\frac{1}{\sqrt{2}} (|00\rangle + |11\rangle)$. Instructions: 1. Declare a quantum variable `qarr` of type `QArray`, and initilize it by allocating to it 2 qubits. Don't forget to use the `Output` modifier. 2. Apply a Hadamard gate on the first qubit of `qarr`. Qmod counts from 0, so the first entry of `qarr` is `qarr[0]`. 1. Apply `CX` (controlled-NOT gate), with the `control` parameter being `qarr[0]` and the `target` parameter being `qarr[1]`. Synthesize and execute your model to assure that $|00\rangle$ and $|11\rangle$ are the only states to be measured, and that they are measured roughly equally. ```python theme={null} from classiq import * # Your code here: ... # execute and inspect the results qprog = synthesize(main) res = sample(qprog) res ``` **Output:** ``` [{'q': 1}: 1026, {'q': 0}: 1022] ``` # ## Exercise #2 * The Repeat Statement Use Qmod's `repeat` statement to create your own Hadamard Transform - a function that takes a `QArray` of an unspecified size and applies `H` to each of its qubits. Instructions: 1. Define a function `my_hadamard_transform`: * It should have a single `QArray` argument `q`. * Use `repeat` to apply `H` on each of `q`'s qubits. * Note that the `iteration` block of the `repeat` statement must use the Python `lambda` syntax (see `repeat` [documentation](https://docs.classiq.io/latest/qmod-reference/language-reference/statements/classical-control-flow/#classical-repeat)). 1. define a `main` function that initializes a `QArray` of length 10, and then passes it to `my_hadamard_transform`. The provided code continues by calling `show` to let you inspect the resulting circuit - make sure that is applies `H` to each of `q`'s qubits. ```python theme={null} from classiq import * # Your code here: ... # synthesize the model and show the result qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/39FkslFO3s96H1p0Byswqn8kNLh ``` **Output:** ``` https://platform.classiq.io/circuit/39FkslFO3s96H1p0Byswqn8kNLh?login=True&version=17 ``` # ## Exercise #3 * Power Raising a quantum operation to an integer power appears in many known algorithms; for example, in Grover search and Quantum Phase Estimation. In the general case the implementation involves repeating the same circuit multiple times. Sometimes, however, the implementation of the power operation can be simplified, thereby saving computational resources. A simple example is the operation of rotating a single qubit about the X, Y, or Z axis. In this case the rotation gate can be used once with the angle multiplied by the exponent. A similar example is the function [unitary](https://docs.classiq.io/latest/qmod-reference/library-reference/core-library-functions/unitary/unitary/) - an operation expressed as an explicit unitary matrix (i.e., all $2^n \times 2^n$ matrix terms are given). Raising the operation can be done by raising the matrix to that power via classical computation. See [power operator](https://docs.classiq.io/latest/qmod-reference/language-reference/statements/power/#syntax). Use the following code to define the value of a Qmod constant named `unitary_matrix` as a 4x4 (real) unitary: ```python theme={null} from typing import List import numpy as np from classiq import * rng = np.random.default_rng(seed=0) random_matrix = rng.random((4, 4)) qr_unitary, _ = np.linalg.qr(random_matrix) unitary_matrix = qr_unitary.tolist() ``` 1. Create a model that applies `unitary_matrix` on a 2-qubit variable three times (e.g. using `repeat`). 2. Create another model that applies `unitary_matrix` raised to the power of 3 on a 2-qubit variable. 3. Compare the gate count via the Classiq IDE in both cases. ```python theme={null} from classiq import * # Your code here: ... qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/39FktCgbPgQamVuDUKU7aryhsBX ``` **Output:** ``` https://platform.classiq.io/circuit/39FktCgbPgQamVuDUKU7aryhsBX?login=True&version=17 ``` # ## Exercise 4 * User-Defined Operators Create a function that applies a given single-qubit operation to all qubits in its quantum argument (call your function `my_apply_to_all`). Such a function is also called an operator; i.e., a function that takes another function as an argument (its operand). See [operators](https://docs.classiq.io/latest/qmod-reference/language-reference/operators/). Follow these guidelines: 1. Your function declares a parameter of type qubit array and a parameter of a function type with a single qubit parameter. 2. The body applies the operand to all qubits in the argument (you may use `repeat` or even `for` inside `my_apply_to_all` for this). Now, re-implement `my_hadamard_transform` from Exercise 2 so that its body calls `my_apply_to_all` rather than calling `repeat` directly. The goal is that `my_hadamard_transform` expresses *what* to do (apply `H` to all qubits), while `my_apply_to_all` encapsulates *how* to iterate. Use the same `main` function from Exercise 2. ```python theme={null} from classiq import * # Your code here: ... qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/39FktteE7eZY1m1BPPnKpcoFdrO ``` **Output:** ``` https://platform.classiq.io/circuit/39FktteE7eZY1m1BPPnKpcoFdrO?login=True&version=17 ``` # ## Exercise 5 * Quantum Conditionals # ### Exercise 5a * Control Operator Use the built-in `control` operator to create a function that receives two single qubit variables and uses one of them to control an RY gate with a `pi/2` angle acting on the other variable (without using the `CRY` function). See [control](https://docs.classiq.io/latest/qmod-reference/language-reference/statements/control/#syntax). ```python theme={null} from classiq import * # Your code here: ... qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/39FkuNEqkobTN3fzL04KPAHLctb ``` **Output:** ``` https://platform.classiq.io/circuit/39FkuNEqkobTN3fzL04KPAHLctb?login=True&version=17 ``` # ### Exercise 5b * Control Operator with Quantum Expressions The `control` operator is the conditional application of some operation, with the condition being that all control qubits are in the state $|1\rangle$. This notion is generalized in Qmod to other control states, where the condition is specified as a comparison between a quantum numeric variable and a numeric value, similar to a classical `if` statement. Quantum numeric variables are declared with class `QNum`. See [numeric types](https://docs.classiq.io/latest/qmod-reference/language-reference/quantum-types/#syntax). 1. Declare a `QNum` output argument using `Output[QNum]` and name it `x`. 2. Use numeric assignment (the `|=` operator) to initialize it to `9`. 3. Execute the circuit and observe the results. 4. Declare another output argument of type `QBit` and perform a `control` such that if `x` is 9, the qubit is flipped. Execute the circuit and observe the results. Repeat for a different condition. ```python theme={null} from classiq import * # Your code here: ... qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/39FkuwxPd8DP1cXGAdra4Gbthlk ``` **Output:** ``` https://platform.classiq.io/circuit/39FkuwxPd8DP1cXGAdra4Gbthlk?login=True&version=17 ``` # ## Exercise 6 * Phase Statement The `phase` statement allows the user to perform the mapping $|x\rangle \rightarrow e^{i\theta f(x_1, x_2, \ldots, x_n)} |x\rangle,$ given a function $f(x_1, x_2, \dots, x_n)$. This operation is extremely valuable to algorithms such as [Grover's](https://docs.classiq.io/latest/explore/algorithms/search_and_optimization/grover/grover/) and [QAOA](https://docs.classiq.io/latest/explore/tutorials/technology_demonstrations/qaoa/qaoa_demonstration/). See [phase](https://docs.classiq.io/latest/qmod-reference/language-reference/statements/phase/). # ### Exercise 6a * Phase with Arithmetic Condition 1. Declare a `QNum` output argument using `Output[QNum]` and name it `x`. 2. Allocate 4 qubits to `x`. 3. Perform a hadamard transform in `x`. 4. Using `phase`, create a phase according to the rule $f(x) = \pi \cdot x / 2$. 5. Apply the hadamard transform in `x` again. 6. Execute the quantum program and analyze the outputs. ```python theme={null} from classiq import * # Your code here: ... qprog = synthesize(main) show(qprog) ``` ## Solutions # ## Solution * Excercise #0 ```python theme={null} from classiq import * # rewrite the model, initializing q inside foo @qfunc def foo(q: Output[QBit]) -> None: allocate(1, q) X(q) H(q) @qfunc def main(q: Output[QBit]) -> None: foo(q) # execute the model to see that we get similar results qprog = synthesize(main) job = execute(qprog) job.get_sample_result().parsed_counts ``` **Output:** ``` [{'q': 1}: 1053, {'q': 0}: 995] ``` > **Key takeaway:** The `Output` modifier defines the beginning of a quantum variable's lifecycle. A parameter declared as `Output[T]` enters the function uninitialized; the function must allocate it before use. A quantum variable not declared as `Output` requires the caller to pass an already-initialized variable. Moving `allocate` into a function therefore requires adding `Output` to that parameter. # ## Solution * Exercise #1 ```python theme={null} from classiq import * @qfunc def bell(qarr: QArray[QBit, 2]) -> None: H(qarr[0]) CX(qarr[0], qarr[1]) @qfunc def main(qarr: Output[QArray]) -> None: allocate(2, qarr) bell(qarr) # execute and inspect the results qprog = synthesize(main) job = execute(qprog) job.get_sample_result().parsed_counts ``` **Output:** ``` [{'qarr': [0, 0]}: 1041, {'qarr': [1, 1]}: 1007] ``` > **Key takeaway:** `QArray` is the quantum equivalent of a classical array. Individual qubits are accessed by index (`qarr[0]`, `qarr[1]`, ...), and any operation can be applied to a specific element. Entanglement - such as the Bell state - arises from combining single-qubit gates (like `H`) with two-qubit gates (like `CX`). # ## Solution * Exercise #2 ```python theme={null} from classiq import * @qfunc def my_hadamard_transform(q: QArray[QBit]) -> None: repeat(q.len, lambda i: H(q[i])) @qfunc def main(q: Output[QArray[QBit]]) -> None: allocate(10, q) my_hadamard_transform(q) qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/39FkwWTQYravU16TBazOhSia9Mh ``` **Output:** ``` https://platform.classiq.io/circuit/39FkwWTQYravU16TBazOhSia9Mh?login=True&version=17 ``` > **Key takeaway:** The `repeat` statement is the standard way to apply an operation to every qubit in a `QArray`. The iteration body must be a Python `lambda` that receives the loop index. Because the array size can be left unspecified (`QArray[QBit]`), functions built with `repeat` work on arrays of any length without modification. # ## Solution * Exercise #3 ```python theme={null} from typing import List import numpy as np from classiq import * rng = np.random.default_rng(seed=0) random_matrix = rng.random((4, 4)) qr_unitary, _ = np.linalg.qr(random_matrix) unitary_matrix = qr_unitary.tolist() @qfunc def main(q: Output[QArray[QBit]]) -> None: allocate(2, q) power(3, lambda: unitary(unitary_matrix, q)) qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3DuGj36iISWmWu99iKy62wYDSI3 ``` > **Key takeaway:** Using `power(n, ...)` is more efficient than repeating an operation `n` times when the Classiq engine can exploit algebraic structure. For example, raising the unitary matrix to the power classically rather than replicating the circuit gates. This optimization is critical in algorithms such as Grover search and Quantum Phase Estimation, where operations must be applied many times. # ## Solution * Exercise #4 ```python theme={null} from classiq import * @qfunc def my_apply_to_all(operand: QCallable[QBit], q: QArray[QBit]) -> None: repeat(q.len, lambda i: operand(q[i])) @qfunc def my_hadamard_transform(q: QArray[QBit]) -> None: my_apply_to_all(lambda t: H(t), q) @qfunc def main(q: Output[QArray[QBit]]) -> None: allocate(10, q) my_hadamard_transform(q) qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/39FkxdlIYR5uVaLDdjkZptuMWNf ``` **Output:** ``` https://platform.classiq.io/circuit/39FkxdlIYR5uVaLDdjkZptuMWNf?login=True&version=17 ``` # ## Alternative Solution * Exercise #4 ```python theme={null} from classiq import * @qfunc def my_apply_to_all(operand: QCallable[QBit], q: QArray[QBit]) -> None: for i in range(q.len): operand(q[i]) @qfunc def my_hadamard_transform(q: QArray[QBit]) -> None: my_apply_to_all(lambda t: H(t), q) @qfunc def main(q: Output[QArray[QBit]]) -> None: allocate(10, q) my_hadamard_transform(q) qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3DuKk33l8U9zkvCjEBCOs32eye3 ``` > **Key takeaway:** User-defined operators (functions that accept other functions as arguments) separate *what* to do from *how* to iterate. `my_apply_to_all` encapsulates the looping logic; the caller expresses the intent: apply `H` to every qubit. Both `repeat` and a classical `for` loop are valid iteration mechanisms inside the operator body. # ## Solution * Exercise #5 # ### Solution * Exercise #5a ```python theme={null} from classiq import * from classiq.qmod.symbolic import pi @qfunc def my_controlled_ry(control_bit: QBit, target: QBit) -> None: control(ctrl=control_bit, stmt_block=lambda: RY(pi / 2, target)) @qfunc def main(control_bit: Output[QBit], target: Output[QBit]) -> None: allocate(1, control_bit) allocate(1, target) my_controlled_ry(control_bit, target) qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/39Fky8yK3HVrNdbfS9XOP4MfxHd ``` **Output:** ``` https://platform.classiq.io/circuit/39Fky8yK3HVrNdbfS9XOP4MfxHd?login=True&version=17 ``` > **Key takeaway:** The `control` operator lets you condition *any* quantum operation on a control qubit being in state $|1\rangle$, without needing a dedicated controlled gate. This is how Qmod builds controlled versions of arbitrary operations. # ### Solution * Exercise #5b ```python theme={null} from classiq import * @qfunc def main(x: Output[QNum], target: Output[QBit]) -> None: x |= 9 allocate(1, target) control(ctrl=(x == 9), stmt_block=lambda: X(target)) qprog = synthesize(main) show(qprog) ``` > **Key takeaway:** Quantum `control` generalizes beyond single-qubit conditions. A `QNum` variable can be compared to a classical integer (e.g., `x == 9`), and the resulting boolean expression used directly as the control condition. This is the quantum analog of a classical `if` statement: the controlled operation is applied (or not) depending on the value in the quantum register, across all branches of a superposition simultaneously. # ## Solution * Exercise #6a ```python theme={null} from classiq import * from classiq.qmod.symbolic import pi @qfunc def main(x: Output[QNum]): allocate(4, x) hadamard_transform(x) phase(x, pi / 2) hadamard_transform(x) qprog = synthesize(main) res = sample(qprog) res ``` **Output:** ``` [{'x': 2}: 1050, {'x': 3}: 998] ``` > **Key takeaway:** The `phase` statement applies a state-dependent phase $e^{i \theta f(x)}$ to each basis state without changing the measurement probabilities of `x` in isolation. When conjugated between Hadamard transforms, phase differences cause constructive and destructive interference that shifts probability weight to specific output states. # ## Solution * Exercise #6b ```python theme={null} from classiq import * from classiq.qmod.symbolic import pi @qfunc def main(x: Output[QArray]): allocate(4, x) hadamard_transform(x) control(ctrl=x, stmt_block=lambda: phase(pi)) control(ctrl=x[3], stmt_block=lambda: phase(pi)) hadamard_transform(x) qprog = synthesize(main) res = sample(qprog) res ``` **Output:** ``` Submitting job to simulator Job: https://platform.classiq.io/jobs/914bf74e-9512-4296-9b71-d4e94952c6ef ``` | | x | counts | probability | bitstring | | -- | ------------- | ------ | ----------- | --------- | | 0 | \[0, 0, 0, 1] | 1556 | 0.759766 | 1000 | | 1 | \[1, 0, 1, 0] | 46 | 0.022461 | 0101 | | 2 | \[0, 0, 1, 0] | 42 | 0.020508 | 0100 | | 3 | \[0, 1, 0, 1] | 39 | 0.019043 | 1010 | | 4 | \[1, 0, 1, 1] | 39 | 0.019043 | 1101 | | 5 | \[1, 1, 0, 0] | 38 | 0.018555 | 0011 | | 6 | \[1, 1, 1, 0] | 36 | 0.017578 | 0111 | | 7 | \[0, 0, 1, 1] | 33 | 0.016113 | 1100 | | 8 | \[1, 0, 0, 0] | 31 | 0.015137 | 0001 | | 9 | \[0, 1, 0, 0] | 31 | 0.015137 | 0010 | | 10 | \[1, 0, 0, 1] | 29 | 0.014160 | 1001 | | 11 | \[0, 0, 0, 0] | 28 | 0.013672 | 0000 | | 12 | \[0, 1, 1, 0] | 26 | 0.012695 | 0110 | | 13 | \[1, 1, 0, 1] | 26 | 0.012695 | 1011 | | 14 | \[1, 1, 1, 1] | 25 | 0.012207 | 1111 | | 15 | \[0, 1, 1, 1] | 23 | 0.011230 | 1110 | > **Key takeaway:** Phase operations and `control` can be combined to selectively apply a phase to specific computational basis states. The `hadamard_transform` utility applies `H` to all qubits of an array in a single call, and `control` with a full `QArray` as the operand conditions the operation on all control qubits being in state $|1\rangle$. # Qmod Tutorial - Part 2 Source: https://docs.classiq.io/getting-started/classiq_tutorial/Qmod_tutorial_part2 Open this notebook in GitHub to run it yourself In this tutorial, we keep extending our expressive power by introducing more advanced topics: * Exponentiation and Pauli Operators. * Arithmetics and numeric assignment. * The `within_apply` statement. * The `bind` statement. Please make sure to go through execises 1-5 of part 1 before continuing with this notebook. ## Exercise 7 * Exponentiation and Pauli Operators The Qmod language supports different classical types: scalars, arrays, and structs. Structs are objects with member variables or fields. See [classical types](https://docs.classiq.io/latest/qmod-reference/language-reference/classical-types/#structs). In particular, Qmod offers a specialized syntax for creating [sparse Hamiltonians](https://docs.classiq.io/sdk-reference/qmod/classical-types#sparsepauliop). For that, simply use the `Pauli` Enum acting in the correct set of qubits. This exercise uses the Suzuki-Trotter function to find the evolution of `H=0.5XZXX + 0.25YIZI + 0.3 XIZY` (captured as a literal value for the Pauli operator), with the evolution coefficient being 3, the order being 2, and using 4 repetitions. See [suzuki\_trotter](https://docs.classiq.io/latest/qmod-reference/library-reference/core-library-functions/hamiltonian_evolution/suzuki_trotter/suzuki_trotter/). To complete this exercise, allocate q and invoke the `suzuki_trotter` quantum function: suzuki\_trotter(
 ...,
 evolution\_coefficient=3,
 repetitions=4,
 order=2,
 qbv=q,
)
```python theme={null} from classiq import * @qfunc def main(q: Output[QArray[QBit]]) -> None: allocate(4, q) # Your code here: qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3EJD16ju8qGJHOLmv7ceqqH4gDT ``` ## Exercise 8 * Basic Arithmetics This exercise uses quantum numeric variables and calculates expressions over them. See details on the syntax of numeric types in [quantum types](https://docs.classiq.io/latest/qmod-reference/language-reference/quantum-types/#syntax). See more on quantum expressions in [numeric assignment](https://docs.classiq.io/latest/qmod-reference/language-reference/statements/assignment/). # ## Exercise 8a Create this quantum program: 1. Initialize variables `x=2`, `y=7` and compute `res = x + y`. 2. Initialize variables `x=2`, `y=7` and compute `res = x * y`. 3. Initialize variables `x=2`, `y=7`, `z=1` and compute `res = x * y - z`. Guidance: * Use the `|=` operators to perform out-of-place assignment of arithmetic expressions. * To initialize the variables, use the `|=` to assgin it with a numerical value. ```python theme={null} from classiq import * # Your code here: qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3EJD1XbzQLEHJGtCnOgoDk2OERL ``` # ## Exercise 8b 1. Declare `x` to be a 2-qubit numeric variable and `y` a 3-qubit numeric variable. 2. Use `prepare_state` to initialize `x` to an equal superposition of `0` and `2`, and `y` to an equal superposition of `1`, `2`, `3`, and `6` (see [prepare\_state](https://docs.classiq.io/latest/qmod-reference/library-reference/core-library-functions/prepare_state_and_amplitudes/prepare_state_and_amplitudes/)). You can set the error bound to 0. 1. Compute `res = x + y`. Execute the resulting circuit. What did you get? ```python theme={null} from classiq import * # Your code here: qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3EJD1xzkRlXxUfnzro4AwYGBgT8 ``` ## Exercise 9 * Within-Apply The within-apply statement applies the $U^\dagger V U$ pattern that appears frequently in quantum computing. It allows you to compute a function `V` within the context of another function `U`, and afterward uncompute `U` to release auxiliary qubits storing intermediate results. See [within apply](https://docs.classiq.io/latest/qmod-reference/language-reference/statements/within-apply/). # ## Exercise 9a This exercise uses `within-apply` to compute an arithmetic expression in steps. Use the `within_apply` operation to calculate `res = x + y + z` from a two-variable addition building block with these steps: 1. Add `x` and `y` 2. Add the result to `z` 3. Uncompute the result of the first operation For simplicity, initialize the registers to simple integers: `x=3`, `y=5`, `z=2`. Hints: * Use a temporary variable. * Use the function syntax of numeric assignment. Execute the circuit and make sure you obtain the expected result. ```python theme={null} from classiq import * # Your code here: qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3EJD2Xsyx8PJ9ZPFpZoRTqvwskK ``` # ## Exercise 9b Why use `within-apply` and not just write three concatenated functions? To understand the motivation, create another arithmetic circuit. This time, however, set the Classiq synthesis engine to optimize on the circuit's number of qubits; i.e., its width. Determine constraints inside synthesis with `Constraints`. (See [here](https://docs.classiq.io/latest/user-guide/synthesis/constraints/)). Perform the operation `res = w + x + y + z`, where w is initialized to 4 and the rest as before: 1. Add `x` and `y` (as part of the `within_apply` operation) 2. Add the result to `z` (as part of the `within_apply` operation) 3. Uncompute the result of the first operation (as part of the `within_apply` operation) 4. Add the result of the second operation to `w`. There is no need to perform another uncomputation, as this brings the calculation to an end. Create the model, optimize on the circuit's width, and run the circuit. Can you identify where qubits have been released and reused? ```python theme={null} from classiq import * # Your code here: qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3EJD2kK3AR0gQ8zOhMJl66ditu2 ``` # ## Bonus: Use a Single Arithmetic Expression What happens when you don't manually decompose this expression? Use the Classiq arithmetic engine to calculate `res |= x + y + z + w` and optimize for width. Look at the resulting quantum program. Can you identify the computation and uncomputation blocks? What else do you notice? ```python theme={null} from classiq import * # Your code here: qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3EJD39TjQ7Ejxzh22iKgSsjFI5E ``` ## Exercise 10 * In-Place Arithmetics # ## Exercise 10a * Conditional Computation This exercise uses quantum numeric variables that represent fixed-point reals. A fixed-point variable `QNum[n, UNSIGNED, f]` uses `n` qubits to represent non-negative values, with `f` of those bits after the binary point - so `QNum[3, UNSIGNED, 3]` covers the range $[0, 1)$ in steps of $\frac{1}{8}$. The goal is to evaluate the following piecewise function over a superposition of fixed-point inputs: $$ f(x) = \begin{cases} 2x + 1 & \text{ if } 0 \leq x < 0.5 \\ x + 0.5 & \text{ if } 0.5 \leq x < 1 \end{cases} $$ The provided code skeleton puts `x` into a uniform superposition of all values in $[0, 1)$ via the Hadamard transform, and pre-allocates `res` to hold the result. Fill in the body of `main` to evaluate `f(x)` into `res`: 1. Compute a boolean quantum variable representing the condition `x < 0.5`. 2. Use `control` with `stmt_block` and `else_block` to apply the correct formula to `res` depending on the branch. To write into `res` inside each branch, use `inplace_xor(expression, res)`. You will learn in Exercise 10b exactly why this is needed instead of the familiar `|=` operator. Note: Python does not allow assignment operators (`|=`, `^=`, `+=`) inside lambda expressions. Factor the in-place computation out to a named `@qfunc` function and call it from the `control` lambda. ```python theme={null} from classiq import * @qfunc def main(x: Output[QNum[3, UNSIGNED, 3]], res: Output[QNum[5, UNSIGNED, 3]]) -> None: allocate(5, res) allocate(3, x) hadamard_transform(x) # Your code here: qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3EJD3VMGVfh3BeIWDlqT51Gmi9j ``` # ## Exercise 10b * In-Place Assignment **Why can't we use `|=` inside the `control` block?** The out-of-place operator `|=` requires its target to be *uninitialized* - it allocates a fresh set of qubits to store the result. In the code above, `res` is pre-allocated *before* the `control` block, so it is already initialized. Using `res |= expression` inside a branch lambda would fail: Qmod does not allow allocation into an already-initialized variable. **In-place operators.** The in-place operators write into an *existing* initialized variable without allocating new qubits: * `inplace_xor(expression, target)` - computes `expression` and XORs it bit-by-bit into `target` (equivalent to `target ^= expression`) * `inplace_add(expression, target)` - computes `expression` and adds it arithmetically into `target` (equivalent to `target += expression`) Both work inside a `control` block because they never try to allocate an uninitialized variable. They also avoid allocating a separate result register per branch - both branches share the single pre-allocated `res`, saving qubits. Since `res` is initialized to zero before the `control` block, `inplace_xor` and `inplace_add` produce the same result here (XOR with zero and ADD with zero are equivalent). They would differ if `res` had a non-zero initial value, or for multi-bit values where carries (ADD) and bit-by-bit XOR diverge. See [numeric assignment](https://docs.classiq.io/latest/qmod-reference/language-reference/statements/assignment/). **Exercise:** Modify your Exercise 10a solution to use `inplace_add` instead of `inplace_xor` and verify that you get the same measurement results. ```python theme={null} from classiq import * # Modify your Exercise 10a solution to use inplace_add instead of inplace_xor. # Your code here: qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3EJD3rWWB8kXwD8yUVIdqFcH4yG ``` ## Exercise 11 * A State-Preparation Algorithm # ## Binding The `bind` operation smoothly converts between different quantum types and splits or slices bits when necessary. Here is an example: ```python theme={null} from classiq import * @qfunc def main(res: Output[QArray[QBit]]) -> None: x = QArray() allocate(3, x) ... lsb = QBit() msb = QNum("msb", 2, False, 0) bind(x, [lsb, msb]) ... bind([lsb, msb], res) qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3EJD4EohCXx6VVspqiS2QV5jgZo ``` The first `bind` operation splits the 3-qubit variable `x` into the 2-qubit and single-qubit `lsb` and `msb` variables, respectively. After the `bind` operation: 1. The `lsb` and `msb` variables can be operated on separately. 2. The `x` variable returns to its uninitialized state and can no longer be used. The second `bind` operation concatenates the variables back to the `res` output variable. For this exercise, fill in the missing code parts in the above snippet and use the `control` statement to manually generate the 3-qubit probability distribution: `[1/8, 1/8, 1/8 - sqrt(3)/16, 1/8 + sqrt(3)/16, 1/8, 1/8, 1/8, 1/8]`. The following sequence of operations generates it: 1. Perform the Hadamard transform on all three qubits. 2. Apply a `pi/3` rotation on the LSB conditioned by the MSB being $|0\rangle$ and the second-to-last MSB being $|1\rangle$. How would you write this condition using a QNum? To validate your results without looking at the full solution, compare them to running using the Classiq built-in `prepare_state` function. ```python theme={null} import numpy as np from classiq import * @qfunc def pre_prepared_state(q: Output[QArray]) -> None: prepare_state( [ 1 / 8, 1 / 8, 1 / 8 - np.sqrt(3) / 16, 1 / 8 + np.sqrt(3) / 16, 1 / 8, 1 / 8, 1 / 8, 1 / 8, ], 0.0, q, ) # Your code here: ``` ## Solutions # ## Exercise 7 ```python theme={null} # Solution to Exercise 7: from classiq import * @qfunc def main(q: Output[QArray[QBit]]) -> None: allocate(4, q) suzuki_trotter( 0.5 * Pauli.X(0) * Pauli.X(1) * Pauli.Z(2) * Pauli.X(3) + 0.25 * Pauli.Z(1) * Pauli.Y(3) + 0.3 * Pauli.Y(0) * Pauli.Z(1) * Pauli.X(3), evolution_coefficient=3, repetitions=4, order=2, qbv=q, ) qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3EJD4bxC6yg7td6tBOLM6Xflu1k ``` > **Key takeaway:** Qmod supports Pauli operator expressions as a native type for specifying sparse Hamiltonians. `suzuki_trotter` implements time evolution under such a Hamiltonian by interleaving the exponentials of individual Pauli terms approximating $e^{-iHt}$. The `order` and `repetitions` parameters control the accuracy of the approximation. # ## Exercise 8 ```python theme={null} # Solution to Exercise 8a: from classiq import * @qfunc def main(x: Output[QNum], y: Output[QNum], z: Output[QNum], res: Output[QNum]) -> None: x |= 2 y |= 7 z |= 1 # res |= x + y # res |= x * y res |= x * y - z qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3EJD5JXTHRLi4LIhpVJVTToUW9q ``` > **Key takeaway:** The `|=` operator performs *out-of-place* numeric assignment: it allocates a fresh quantum register to store the result of the arithmetic expression. Complex expressions combining `+`, `*`, and `-` are fully supported and evaluated quantum-mechanically. ```python theme={null} # Solution to Exercise 8b: from classiq import * @qfunc def main(x: Output[QNum], y: Output[QNum], res: Output[QNum]) -> None: prepare_state(probabilities=[0.5, 0, 0.5, 0.0], bound=0.0, out=x) prepare_state( probabilities=[0, 0.25, 0.25, 0.25, 0.0, 0.0, 0.25, 0.0], bound=0.0, out=y ) res |= x + y qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3EJD5wotnM0pIRbEbVf5gG5uGQ0 ``` > **Key takeaway:** Quantum arithmetic operates over superpositions simultaneously. When `x` and `y` each encode a superposition of values, `res |= x + y` produces a superposition of all corresponding sums, being one for each pair of input values. This is the computational parallelism that quantum arithmetic provides. # ## Exercise 9 ```python theme={null} # Solution to Exercise 9: from classiq import * @qfunc def main(res: Output[QNum]) -> None: x = QNum() y = QNum() z = QNum() x |= 3 y |= 5 z |= 2 temp = QNum() within_apply( within=lambda: assign(x + y, temp), apply=lambda: assign(temp + z, res) ) qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3EJD6kxzvd0blHHULXShaKhWbPO ``` > **Key takeaway:** `within_apply` automates the $U^\dagger V U$ uncomputation pattern: it runs the `within` block, then the `apply` block, then automatically reverses the `within` block freeing the qubits held by temporary variables. Without it, temporaries stay initialized for the rest of the circuit, permanently occupying qubits. Note: because `within` and `apply` must be Python lambdas, and expressions, such as `|=`, cannot appear in a lambda, use `assign(expression, target)` as the functional equivalent. ```python theme={null} # Solution to the advanced part of Exercise 9: from classiq import * @qfunc def main(res: Output[QNum]) -> None: x = QNum() y = QNum() z = QNum() w = QNum() x |= 3 y |= 5 z |= 2 w |= 4 temp_xy = QNum() xyz = QNum() within_apply( within=lambda: assign(x + y, temp_xy), apply=lambda: assign(temp_xy + z, xyz), ) res |= xyz + w const = Constraints(optimization_parameter=OptimizationParameter.WIDTH) qprog = synthesize(main, constraints=const) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3EJD7kXtAkdA9u4Yhddt7ACLElK ``` > **Key takeaway:** Qubit reuse is only possible when temporary variables are properly uncomputed. `within_apply` enables the synthesizer to reclaim freed qubits for subsequent operations. The synthesis optimization on width (`OptimizationParameter.WIDTH`) makes this reuse explicit: the same qubits appear in different logical roles at different points in the circuit. # ## Exercise 10a ```python theme={null} # Solution to Exercise 10: from classiq import * @qfunc def main(x: Output[QNum[3, UNSIGNED, 3]], res: Output[QNum[5, UNSIGNED, 3]]) -> None: allocate(5, res) allocate(3, x) hadamard_transform(x) aux = QBit() aux |= x < 0.5 control( aux, stmt_block=lambda: inplace_xor(2.0 * x + 1.0, res), else_block=lambda: inplace_xor(1.0 * x + 0.5, res), ) qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3EJD8i2H6PuZsGS4qNnYL87Ak7M ``` > **Key takeaway:** Piecewise quantum functions are implemented by computing a boolean condition into an auxiliary qubit (`aux |= x < 0.5`) and using `control` with `stmt_block` and `else_block` to select the appropriate formula. Because `x` is in a superposition, the model evaluates both branches in parallel: each computational basis state follows the branch dictated by its own value of `x`. # ## Exercise 10b ```python theme={null} # Solution to Exercise 10b: from classiq import * @qfunc def main(x: Output[QNum[3, UNSIGNED, 3]], res: Output[QNum[5, UNSIGNED, 3]]) -> None: allocate(5, res) allocate(3, x) hadamard_transform(x) aux = QBit() aux |= x < 0.5 control( aux, stmt_block=lambda: inplace_add(2.0 * x + 1.0, res), else_block=lambda: inplace_add(1.0 * x + 0.5, res), ) qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3EJD9el3okie9bOkxDBmFj9N0QA ``` > **Key takeaway:** In-place operators (`inplace_xor`, `inplace_add`) are necessary when writing into a pre-initialized variable inside a quantum operator like `control`. The out-of-place `|=` cannot be used there because `res` is initialized at the beginning of the quantum program. In-place operators also avoid allocating a separate result register per branch - both branches share the single pre-allocated `res`, saving qubits. When the target starts at zero, `inplace_xor` and `inplace_add` give identical results; they diverge for non-zero initial values or when arithmetic carries differ from bitwise XOR. # ## Exercise 11 ```python theme={null} # Solution to Exercise 11: from classiq import * from classiq.qmod.symbolic import pi @qfunc def main(res: Output[QArray[QBit]]) -> None: x = QArray() allocate(3, x) hadamard_transform(x) lsb = QBit() msb = QNum() bind(x, [lsb, msb]) control(msb == 1, lambda: RY(pi / 3, lsb)) bind([lsb, msb], res) qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3EJDAESo5jA0QNqhBKdLrgQSQRA ``` > **Key takeaway:** The `bind` operation casts and splits quantum variables into different quantum types. In this example, after the `bind` statement, the variable `x` is split into two different quantum types: a qubit `lsb` and a quantum number `msb`. Since `msb` is a quantum number, it is possible to perform numeric operations with it, such as compare it to an integer,as it is done inside the `control` operation. # Classiq Overview Tutorial Source: https://docs.classiq.io/getting-started/classiq_tutorial/classiq_overview_tutorial Open this notebook in GitHub to run it yourself In this notebook we introduce a typical workflow with Classiq: * **Designing a quantum model** using the Qmod language and it's accompanied function library. * **Synthesizing the model** into a concrete circuit implementation. * **Executing the program** on a chosen simulator or quantum hardware. * **Post-processing** the results. Later tutorials dive into each of the above stages, providing hands-on interactive guides that go from the very basics to advanced usage. To get started, run: ```python theme={null} from classiq import * ``` If this `import` doesn't work for you, please try `pip install classiq` in your terminal, or refer to [Registration and Installation](https://docs.classiq.io/latest/getting-started/registration_installations/). ## Designing a Quantum Model Here we will define a quantum function `main` that calculates a simple arithmetic expression: ```python theme={null} @qfunc def main(x: Output[QNum], y: Output[QNum]) -> None: allocate(3, x) hadamard_transform(x) y |= x**2 + 1 ``` Explaining the code step-by-step: 1. Allocate 3 qbits for the quantum number `x`, so that it can represent $2^3$ different numbers, from 0 to 7 (for example, the bitstring '010' represents the number 2). 2. Apply `hadamard_transform` to `x`, creating an equal superposition of all these values. 3. Assign the desired arithmetic expression's result to the quantum number `y`. A moment before measurement, we expect the output variables `x` and `y` to be in an equal superposition of the states $|x_i\rangle |y_i=x_i^2+1\rangle$ for $x_is$ from 0 to 7. In other words, we have designed our quantum model to calculate $x^2 +1$. ## Synthesizing the Model The function `main` describes the model in a high-level manner: "calculate $x^2+1$ and assign in into `y`". However, it does not specify **how** to implement this calculation - it does not map it to an executable quantum circuit, in terms of elementary quantum gates applied to specific qubits. In order to do so, we use Classiq's synthesis engine. To do so, simply pass `main` to the synthesis engine to obtain a concrete quantum program `qprog`. Here we simply call the function `synthesize`. Later on we will learn to provide configuration details (e.g. which elementary gates are allowed, or what resources we are trying to optimize). ```python theme={null} qprog = synthesize(main) ``` We can analyze the resulting implementation using Classiq's visualization tool: ```python theme={null} show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3DuELJ8DVHRQscHrzbO4x13aoiE ``` This should pop up a web page with something like this:
vis
By clicking the `+` icons on the blocks' top-left corner, we can inspect the gate-level implementation of each functional block. For example, inspect the complex combination of `H`, `CPHASE`, `CX` and `U` gates that implements the `Power` block. ## Executing the Quantum Program Now that we have a concrete circuit implementation of the desired model, we can execute it and sample the resulting states of the output variables. Here we will simply call the function `sample`, which uses Classiq's quantum simulator by default to sample the multiple executions of the quantum program (the default `n_shots` is 2048): ```python theme={null} res = sample(qprog) ``` **Output:** ``` Submitting job to simulator Job: https://platform.classiq.io/jobs/3da66afe-a388-44e3-b9a6-34fc7872c2cf ``` Later on we will learn how to execute on hardwares and simulators of our choice and manage advanced executions (for example, hybrid execution that uses classical logic to alter the circuit between runs). ## Post-Processing the Results Having executed the quantum program multiple times (`n_shots`=2048) we can now inspect the possible pairs `x`,`y` that our arithmetic expression allows ($y=x^2+1$). This can be done by looking into `res` - a `dataframe` that contain all the states that were measured on the output variables, ordered by the number of counts that they were measured. ```python theme={null} res ``` | | x | y | counts | probability | bitstring | | - | - | -- | ------ | ----------- | --------- | | 0 | 4 | 17 | 270 | 0.131836 | 010001100 | | 1 | 3 | 10 | 266 | 0.129883 | 001010011 | | 2 | 7 | 50 | 265 | 0.129395 | 110010111 | | 3 | 6 | 37 | 261 | 0.127441 | 100101110 | | 4 | 0 | 1 | 258 | 0.125977 | 000001000 | | 5 | 2 | 5 | 251 | 0.122559 | 000101010 | | 6 | 1 | 2 | 242 | 0.118164 | 000010001 | | 7 | 5 | 26 | 235 | 0.114746 | 011010101 | As expected, all possible values of `x` (integers from 0 to 7) were measured roughly similar number of times, and with each `x` measured, the measurement of `y` satisfies $y=x^2+1$. Alternatively, you can inspect the histogram of sampled states in the [Classiq IDE](https://platform.classiq.io/jobs).
vis
Hovering above each of the histogram bars shows its bitstring and its parsed variables values. For example, the bitstring '010001100' is parsed as `x`=4, `y`=17, because the first 3 qubits (counting from the right) correspond to `x` and were measured as '100'=4 (in binary), and the other 6 qubits which correspond to `y` were measured as '010001'= 17. ## Summary In this tutorial, we have gone through a typical workflow using Classiq: 1. Designing a quantum model: the problem we wanted to solve is calculating an arithmetic expression for a given domain of `x` values. We used `hadamard_transform` and arithmetic assignment as our modeling building blocks. 2. Synthesizing the model into a concrete circuit implementation: we called `synthesize` to let Classiq's synthesis engine take our high-level description and implement it in an executable way. 3. Executing the program: we called `execute` to run our quantum program multiple times on Classiq's simulator. 4. Post-processing: We inspected the measured states of `x` and `y` - for each `x` and assured ourselves that they satisfy the desired arithmetic expression. # ## Food for Thought You might have noticed that the model discussed here does not truly harness the power of quantum computers: a moment before sampling the qubits, `x` and `y` indeed hold "the answers to all questions" simultaneously (all the pairs `x` and `y` that satisfy the equation), but we cannot access these answers until we measure the qubits, which collapses the superposition and leaves only a single (and randomly chosen) pair of `x` and `y`. Having said that, we have no choice but to run multiple times (many more than $2^3$ in our case) to make sure that we measure all `x`s of interest. A classical computer could obtain the same information in exactly $2^3$ runs. Then, why bother? While pure arithmetic alone may never be a primary task for quantum computers, quantum arithmetic plays a crucial role in many quantum algorithms that do exploit quantum speedup. For example, it is widely used in oracle functions within Grover's search algorithm and in quantum cryptographic protocols. ## Practice Edit the arithmetic expression inside `main`, using the `+`, `-`, `**` operators as well as literal numbers of your choice. Validate that the sampled states of `x` and `y` satsify your arithmetic expression. # Execution Tutorial - Part 1 Source: https://docs.classiq.io/getting-started/classiq_tutorial/execution_tutorial Open this notebook in GitHub to run it yourself This tutorial covers the basics of executing a quantum program using Classiq directly through the Python SDK. It is also possible to use the [Classiq Platform](https://platform.classiq.io) to execute quantum algorithms. For this, we will start by synthesizing the following example from the [synthesis tutorial](https://docs.classiq.io/latest/explore/tutorials/basic_tutorials/the_classiq_tutorial/synthesis_tutorial/): ## Example 1: Sampling Arithmetics and Changing Number of Shots ```python theme={null} from classiq import * @qfunc def main(x: Output[QNum[3]], y: Output[QNum]) -> None: allocate(x) hadamard_transform(x) y |= x**2 + 1 qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3EJKxJJ639O4HKXMbzBJV92B2vJ ``` This quantum program evaluates the function $y(x) = x^2 + 1$, for all integers $x \in [0,7]$. To execute a quantum program and sample the states, use `sample`: ```python theme={null} results = sample(qprog) ``` **Output:** ``` Submitting job to simulator Job: https://platform.classiq.io/jobs/afa2ebcd-8cb9-4f59-8507-94ed94b802dc ``` The output from `sample` is a dataframe with information regarding execution: ```python theme={null} results ``` | | x | y | counts | probability | bitstring | | - | - | -- | ------ | ----------- | --------- | | 0 | 7 | 50 | 280 | 0.136719 | 110010111 | | 1 | 5 | 26 | 273 | 0.133301 | 011010101 | | 2 | 1 | 2 | 266 | 0.129883 | 000010001 | | 3 | 0 | 1 | 262 | 0.127930 | 000001000 | | 4 | 2 | 5 | 250 | 0.122070 | 000101010 | | 5 | 6 | 37 | 243 | 0.118652 | 100101110 | | 6 | 3 | 10 | 238 | 0.116211 | 001010011 | | 7 | 4 | 17 | 236 | 0.115234 | 010001100 | The information displayed in the dataframe is: * `counts` shows the number of times each state was measured. * `bitstring` is the bitstring that represents each state measured. * `x` and `y` are the numerical representation of the states associated with the measurement. * `probability` is the probability associated with each measured state. By default, the number of executions of the quantum program is $2048$. This quantity, called the number of shots, can be modified inside `sample`. For instance, if we want to execute the same circuit with $10{,}000$ shots: ```python theme={null} results_more_shots = sample(qprog, num_shots=10000) ``` **Output:** ``` Submitting job to simulator Job: https://platform.classiq.io/jobs/5cd7b05e-2751-47a6-9d46-50a2e702244b ``` The number of counts for each state will grow proportionally with the number of shots: ```python theme={null} results_more_shots ``` | | x | y | counts | probability | bitstring | | - | - | -- | ------ | ----------- | --------- | | 0 | 3 | 10 | 1308 | 0.1308 | 001010011 | | 1 | 7 | 50 | 1271 | 0.1271 | 110010111 | | 2 | 4 | 17 | 1266 | 0.1266 | 010001100 | | 3 | 0 | 1 | 1256 | 0.1256 | 000001000 | | 4 | 2 | 5 | 1256 | 0.1256 | 000101010 | | 5 | 6 | 37 | 1229 | 0.1229 | 100101110 | | 6 | 1 | 2 | 1214 | 0.1214 | 000010001 | | 7 | 5 | 26 | 1200 | 0.1200 | 011010101 | ## Example 2: GHZ States and Noise Many simulators provide noise models that approximate the behavior of real quantum hardware. In this example, we create a GHZ state using the Classiq simulator while emulating the noise profile of IBM Pittsburgh, an IBM backend available through Classiq. We begin by defining the model for the GHZ state: ```python theme={null} from classiq import * @qfunc def main(x: Output[QArray[QBit, 3]]): allocate(x) H(x[0]) CX(x[0], x[1]) CX(x[1], x[2]) qprog = synthesize(main) ``` Next, we configure the simulator to use the noise model associated with the IBM Pittsburgh backend. This is done by passing a `noise_model` entry through the `config` argument: ```python theme={null} cfg = {"noise_model": "ibm_pittsburgh"} res = sample(qprog, backend="simulator", config=cfg) res ``` **Output:** ``` Submitting job to simulator Job: https://platform.classiq.io/jobs/6f56f8a5-1441-450f-b24c-2d4b7232dafc ``` | | x | counts | probability | bitstring | | - | ---------- | ------ | ----------- | --------- | | 0 | \[1, 1, 1] | 1014 | 0.495117 | 111 | | 1 | \[0, 0, 0] | 985 | 0.480957 | 000 | | 2 | \[0, 0, 1] | 10 | 0.004883 | 100 | | 3 | \[1, 1, 0] | 9 | 0.004395 | 011 | | 4 | \[0, 1, 1] | 9 | 0.004395 | 110 | | 5 | \[0, 1, 0] | 8 | 0.003906 | 010 | | 6 | \[1, 0, 1] | 7 | 0.003418 | 101 | | 7 | \[1, 0, 0] | 6 | 0.002930 | 001 | For an ideal, noiseless GHZ state, the only expected measurement outcomes are `[0, 0, 0]` and `[1, 1, 1]`, each occurring with approximately equal probability. All other basis states should have zero probability. Here, because the simulation includes a realistic noise model, a small fraction of the measurements appears in other states. The dominant outcomes are still `[0, 0, 0]` and `[1, 1, 1]`, but the presence of low-probability additional bitstrings reflects the effect of hardware noise on the quantum program execution. ## State Vector Simulation A state vector simulator returns the amplitudes of the quantum states produced by a quantum program. Unlike sampling, which estimates output probabilities from repeated measurements, state vector simulation gives direct access to the simulated quantum state. On real quantum hardware, these amplitudes are not directly observable. Reconstructing them requires quantum state tomography, which involves measuring the system in different bases to infer the output state. In this example, we calculate the state vector of the quantum program using `calculate_state_vector`: ```python theme={null} res_sv = calculate_state_vector(qprog) res_sv ``` **Output:** ``` Submitting job to simulator Job: https://platform.classiq.io/jobs/2eced471-369f-4048-b515-3a8d83988f80 ``` | | x | amplitude | magnitude | phase | probability | bitstring | | - | ---------- | ------------------ | --------- | ----- | ----------- | --------- | | 0 | \[0, 0, 0] | 0.707107+0.000000j | 0.71 | 0.00π | 0.5 | 000 | | 1 | \[1, 1, 1] | 0.707107+0.000000j | 0.71 | 0.00π | 0.5 | 111 | The information displayed in the dataframe is: * `amplitude` is the complex amplitude associated with each basis state. * `magnitude` is the absolute value of the amplitude. * `phase` is the phase of the amplitude. * `probability` is the probability of measuring the corresponding state. * `bitstring` is the bitstring representation of the basis state. * `x` is the value of the quantum array. It is displayed as a list of 0s and 1s, with each entry corresponding to one qubit in the array. In this case, the output corresponds to an ideal GHZ state. The only states with nonzero probability are `[0, 0, 0]` and `[1, 1, 1]`, each with probability 0.5 and amplitude approximately $1/\sqrt{2}$. ## Backend Selection The backend of an execution is the hardware or simulator where the quantum program is executed. To select a specific backend, it is necessary to know its correct name and provider. To do so, run `get_backend_details()` for a concise list of available backends. ```python theme={null} backend_list = get_backend_details() backend_list.head() ``` | | provider | backend | type | num\_qubits | is\_available | pending\_jobs | queue\_time | | - | ---------- | --------------------------------- | --------- | ----------- | ------------- | ------------- | ----------- | | 0 | classiq | nvidia\_simulator | simulator | 29 | True | NaN | NaT | | 1 | classiq | simulator | simulator | 28 | True | NaN | NaT | | 2 | classiq | simulator\_density\_matrix | simulator | 28 | True | NaN | NaT | | 3 | classiq | simulator\_matrix\_product\_state | simulator | 28 | True | NaN | NaT | | 4 | alice\&bob | LOGICAL\_EARLY | simulator | 15 | True | NaN | NaT | Now, to define a backend, set the backend name following the rule `"provider/backend"` under the execution function used. For example, you can use the [Classiq simulator](https://docs.classiq.io/user-guide/execution/cloud-providers/classiq-backends#supported-backends) to realize a state vector simulation of the GHZ state, or the [MPS simulator](https://docs.classiq.io/user-guide/execution/cloud-providers/classiq-backends#supported-backends) to sample over the same state: ```python theme={null} default_backend = "classiq/simulator" MPS_backend = "classiq/simulator_matrix_product_state" res_default = calculate_state_vector(qprog, backend=default_backend) res_MPS = sample(qprog, MPS_backend) ``` **Output:** ``` Submitting job to classiq/simulator Job: https://platform.classiq.io/jobs/301d33f4-97a4-46bf-8794-aef3e6da2c6e Submitting job to classiq/simulator_matrix_product_state Job: https://platform.classiq.io/jobs/b703a7ac-9e4d-4752-8166-4474c5eb8c66 ``` ```python theme={null} res_default ``` | | x | amplitude | magnitude | phase | probability | bitstring | | - | ---------- | ------------------ | --------- | ----- | ----------- | --------- | | 0 | \[0, 0, 0] | 0.707107+0.000000j | 0.71 | 0.00π | 0.5 | 000 | | 1 | \[1, 1, 1] | 0.707107+0.000000j | 0.71 | 0.00π | 0.5 | 111 | ```python theme={null} res_MPS ``` | | x | counts | probability | bitstring | | - | ---------- | ------ | ----------- | --------- | | 0 | \[0, 0, 0] | 1062 | 0.518555 | 000 | | 1 | \[1, 1, 1] | 986 | 0.481445 | 111 | # Execution Tutorial - Part 2 Source: https://docs.classiq.io/getting-started/classiq_tutorial/execution_tutorial_part2 Open this notebook in GitHub to run it yourself ## Expectation Values and Parameterized Quantum Programs This tutorial covers the basics of measuring observables expressed as linear combinations of Pauli strings and executing a parameterized quantum program using Classiq via Python SDK. Alternatively, you can use the [Classiq IDE web page](https://platform.classiq.io) to execute quantum algorithms. A parameterized quantum program is a quantum circuit with adjustable parameters, such as angles in rotation gates, that can be tuned to alter the circuit's behavior. Think of it like tuning a camera with adjustable settings: the camera (the circuit) stays the same, but adjusting the settings (parameters) changes the captured images (outputs). In quantum computing, tuning these parameters helps identify the configuration that yields the most useful results. These programs are particularly useful in quantum machine learning and optimization, where the goal is to find the best parameter set. First, we create a parameterized quantum program using two qubits. The program applies an X gate, a parameterized RY rotation, and a CX gate. The rotation angle is controlled by a variable called `angle`. ```python theme={null} from classiq import * @qfunc def main(angle: CReal, x: Output[QBit], y: Output[QBit]) -> None: allocate(x) allocate(y) X(x) RY(angle, x) CX(x, y) qprog = synthesize(main) show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3DuJkbhvAlRZokLgJCdNrEWdUqX ``` The first thing we can do is to sample the outputs of the quantum program for a given parameter. For example, $\pi / 2$. We can now execute the quantum program and obtain a sample of output states using [ExecutionSession](https://docs.classiq.io/latest/sdk-reference/execution/#classiq.execution.ExecutionSession). To do this, we define the parameter values using a dictionary. ```python theme={null} import numpy as np # Set angle parameter to pi/2 for sampling parameter = {"angle": np.pi / 2} ``` After generating the `ExecutionSession`, it is possible to show the counts for this particular parameter value: ```python theme={null} first_sample = sample(qprog, parameters=parameter) print("Counts for angle = pi/2: ") first_sample ``` **Output:** ``` Submitting job to simulator Job: https://platform.classiq.io/jobs/6577986e-df35-477e-921c-9ba673415632 ``` **Output:** ``` Counts for angle = pi/2: ``` | | x | y | counts | probability | bitstring | | - | - | - | ------ | ----------- | --------- | | 0 | 0 | 0 | 1031 | 0.503418 | 00 | | 1 | 1 | 1 | 1017 | 0.496582 | 11 | Running your circuit with different parameter values helps explore how the output changes, revealing trends or minima in a cost function. This is especially useful in quantum optimization. As an example, we'll evaluate the circuit over 50 values of `angle` from $0$ to $2\pi$. ```python theme={null} # Create a list of 50 angle values from 0 to 2π angles_list = np.linspace(0, 2 * np.pi, 50) parameters_list = [{"angle": angles} for angles in angles_list] # Execute batch sampling over all angles second_sample = sample(qprog, parameters=parameters_list) ``` **Output:** ``` Submitting job to simulator Job: https://platform.classiq.io/jobs/3b55e716-dc5f-44c0-a53c-cb542aaf65b6 ``` When the `parameters` argument is a list, the result of `sample` is also a list of results, one for each angle value. Therefore, we can analyze the data from each parameter on `angles_list`. For example, an interesting way of analyzing this data is to plot the number of counts of the states $|00\rangle$ and $|11\rangle$ as functions of `angle`: ```python theme={null} # extract counts of |11> and |00> for each result def get_counts(res, bitstring): pops = [] for results in res: matches = results[results["bitstring"] == bitstring] if matches.empty: pops.append(0) else: pops.append(matches["counts"].iloc[0]) return pops pops_00 = get_counts(second_sample, "00") pops_11 = get_counts(second_sample, "11") ``` ```python theme={null} import matplotlib.pyplot as plt plt.figure(figsize=(8, 5)) plt.plot(angles_list / np.pi, pops_00, label='Counts of "00"') plt.plot(angles_list / np.pi, pops_11, label='Counts of "11"', linestyle="-.") plt.xlabel(r"$\mathrm{Angle} \; (\pi)$") plt.ylabel("Counts") plt.legend() plt.show() ``` output ## Measuring Pauli Strings Measuring observables from a quantum program turns out to be necessary when you want to obtain information that can't be accessed only from the populations of states. For this end, you can measure Pauli Strings using Classiq. As an example, if we want to measure how close the output of our system is to the Bell state: $$ |\Phi^+ \rangle = \frac{1}{\sqrt{2}} \left( |00\rangle + |11\rangle \right), $$ it is possible measure the expected value of its projection: $$ P(\Phi^+) = |\Phi^+ \rangle \langle \Phi^+ | = \frac{1}{2} \left( |00\rangle + |11\rangle \right) \left( \langle00| + \langle 11| \right) = \frac{1}{2} \left ( |00\rangle \langle 00| + |00\rangle \langle 11 | + |11 \rangle \langle 00 | + |11\rangle \langle |11\right). $$ The projector operator, by its turn, can be represented as a Pauli string: $$ P(\Phi^+) = \frac{1}{4} \left( II + XX - YY + ZZ \right) $$ Therefore, if we want to measure the projector expected value for some output of the quantum circuit, say angle $= \pi/5$, it is possible using `estimate` and `ExecutionSession`. For this, first we need to define the Hamiltonian to be measured: ```python theme={null} projector_operator = 0.25 * ( Pauli.I(0) * Pauli.I(1) + Pauli.X(0) * Pauli.X(1) - Pauli.Y(0) * Pauli.Y(1) + Pauli.Z(0) * Pauli.Z(1) ) ``` Now, using `observe`, evaluate the expected value of the output: ```python theme={null} parameter = {"angle": np.pi / 5} expectation_value_1 = observe(qprog, projector_operator, parameters=parameter) print("Expected value for angle = pi/5: ", expectation_value_1) ``` **Output:** ``` Submitting job to simulator Job: https://platform.classiq.io/jobs/a599de9a-f855-493f-bc5d-21e12e106008 ``` **Output:** ``` Expected value for angle = pi/5: 0.212890625 ``` The same can be done in batches, for example, if we want to plot a graph of fidelity between the output of the quantum program and the $|\Phi^+\rangle$ state: ```python theme={null} expectation_value_2 = observe(qprog, projector_operator, parameters=parameters_list) ``` **Output:** ``` Submitting job to simulator Job: https://platform.classiq.io/jobs/62178e6a-1e58-4fac-817b-d9f99a3a5f4d ``` ```python theme={null} plt.figure(figsize=(8, 5)) plt.plot(angles_list / np.pi, expectation_value_2, label="Expectation value") plt.xlabel(r"$\mathrm{Angle} \; (\pi)$") plt.ylabel(r"$|\langle \Phi^+ | \psi \rangle |^2$") plt.legend() plt.show() ``` output ## Retrieving Jobs Executed Using Execution Session When executing a job that may take longer to complete, or when running on a hardware backend with a job queue, it is useful to have a way to retrieve the job results later. For this purpose, Classiq supports submitting an `ExecutionJob`, which is associated with a unique job ID. The job can then be retrieved later using this ID. To submit a job, use an `ExecutionSession` together with one of the execution functions that has the [`submit_` prefix](https://docs.classiq.io/sdk-reference/execution#submit_sample), such as `submit_sample`. As an example, we submit two different jobs and then retrieve their outputs using their job IDs. First, we submit the jobs: ```python theme={null} with ExecutionSession(qprog) as execution_session: # These are the sampling jobs sample_job = execution_session.submit_sample(parameter) # These are the estimate jobs estimate_job = execution_session.submit_estimate(projector_operator, parameter) # These are the job IDs for the respective jobs sample_job_ID = sample_job.id estimate_job_ID = estimate_job.id ``` Once you have the job ID, it is possible to retrieve its execution data using `ExecutionJob`. For example, here we retrieve a previously execution of `estimate_job` and compare it to the outputs of `first_sample` - they should be close: ```python theme={null} # Retrieving the job from its ID retrieved_estimate = ExecutionJob.from_id(estimate_job_ID) print("Retrieved job result:", retrieved_estimate.result_value().value) ``` **Output:** ``` Retrieved job result: (0.19482421875+0j) ``` As expected, the retrieved job result is very close to the first sample, since they execute the same quantum circuit. ## Application: Variational Quantum Circuit to Prepare a Bell State In this additional session, we create a simple parameterized quantum algorithm that prepares the Bell State $|\Phi^+\rangle$. For this, an ansatz with two parameters is constructed: ```python theme={null} # Note that now angles are declared as a CArray[CReal, 2], where 2 represents its length @qfunc def main(angles: CArray[CReal, 2], x: Output[QBit], y: Output[QBit]) -> None: allocate(x) allocate(y) RX(angles[0], x) RY(angles[1], x) CX(x, y) qprog_bell = synthesize(main) ``` Then define the function that is subject to classical optimization. In this case, we aim to maximize the expected value of the `projector_operator`. Therefore, we create a `negative_coeffs_projector_operator` to minimize: ```python theme={null} negative_coeffs_projector_operator = (-1) * projector_operator ``` The final step is to perform the optimization of the cost function defined by the quantum ansatz. In this tutorial, the `minimize` method from `ExecutionSession` will be employed. ```python theme={null} res = variational_minimize( qprog_bell, cost_function=negative_coeffs_projector_operator, initial_params={"angles": [0, 0]}, max_iteration=200, ) ``` **Output:** ``` Submitting job to simulator Job: https://platform.classiq.io/jobs/ccdb464b-a6b2-4c67-8638-074c21f82d36 ``` ```python theme={null} coefficients = res[-1][1] fidelity = -res[-1][0] print("Fidelity =", fidelity, "Coefficients: ", coefficients) ``` **Output:** ``` Fidelity = 1.0 Coefficients: {'angles': [0.025111834053147968, 1.5657679610396165]} ``` These values corresponds to the quantum circuit that generates this Bell State using RX, RY, and CX gates. ## Final Remarks In this tutorial, we built a simple parameterized quantum circuit, explored sampling it with specific parameter values, and visualized how output probabilities vary with those parameters. At the end, a simple Variational Quantum Algorithm is presented to prepare a Bell State. These techniques form the foundation for building and optimizing more complex quantum algorithms. # Onboarding Tutorial Source: https://docs.classiq.io/getting-started/classiq_tutorial/index Welcome to the Onboarding Tutorial! This is the place to gain hands-on experience in high-level quantum programming. The tutorial consists of the following sections: 1. [Overview Tutorial](/getting-started/classiq_tutorial/classiq_overview_tutorial) - a full workflow with classiq, from designing a simple quantum model to execution and post processing the results. 2. Qmod Tutorial - introducing the Qmod language and its accompanying library. These are the building blocks of every quantum model. 1. [Part 1](/getting-started/classiq_tutorial/Qmod_tutorial_part1) covers basic concepts and functions, which are relevant in most use-cases. 2. [Part 2](/getting-started/classiq_tutorial/Qmod_tutorial_part2) goes into more advanced topics. Grasping these tools and ideas opens the door to a whole world of quantum algorithms and applications. 3. [Synthesis tutorial](/getting-started/classiq_tutorial/synthesis_tutorial) - introducing the synthesis engine and explaining how to use it for optimization of the resulting quantum program. 4. Execution tutorial - How to execute quantum programs, and how to post-process their outputs. 1. [Part 1](/getting-started/classiq_tutorial/execution_tutorial) introduces how to execute a quantum program and process the sampling outputs. 2. [Part 2](/getting-started/classiq_tutorial/execution_tutorial_part2) covers measurement of observables, execution of parameterized quantum circuits, and a workflow for variational quantum algorithms. # Synthesis Tutorial Source: https://docs.classiq.io/getting-started/classiq_tutorial/synthesis_tutorial Open this notebook in GitHub to run it yourself Classiq's synthesis engine takes a high-level model written in the Qmod language, and compiles it into an executable gate-level circuit. When mapping high-level functionality to concrete circuits, there may be many different but equivalent possible implementations that reflect tradeoffs in the overall depth, width, gate counts, etc. For example, implementing a multi-controlled-not operation can be shallower in gates given more auxiliary qubits. Choosing the best implementation for a specific operation instance depends on the overall constraints and objectives, as well as the specific structure of the quantum program. Let's look at a simple model, and use Classiq's synthesis engine to compile it given different optimization objectives. ```python theme={null} from classiq import * @qfunc def main(x: Output[QNum[3]], y: Output[QNum]) -> None: allocate(x) hadamard_transform(x) y |= x**2 + 1 ``` First, let's synthesize to optimize on circuit depth, i.e. to minimize the longest path formed by gates in the circuit (and hence affects the required coherence time). ```python theme={null} qprog_opt_depth = synthesize( model=main, constraints=Constraints(optimization_parameter=OptimizationParameter.DEPTH), ) ``` We can inspect the resulting circuit using Classiq's web visualization: ```python theme={null} show(qprog_opt_depth) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/319KlN64C4oX3KLiMFXcQrhtRwr ``` On the left side menu, under 'Transpiled info', you should see the resulting depth, width and gate-count:
vis
The resulting depth and width are 171 and 16, respectively. This information can also be obtained using `data.width` and `transpiled_circuit.depth`: ```python theme={null} depth = qprog_opt_depth.transpiled_circuit.depth width = qprog_opt_depth.data.width print("Depth: ", depth, ". Width: ", width) ``` **Output:** ``` Depth: 171 . Width: 16 ``` Now, let's synthesize to optimize width, i.e to minimize the number of qubits used. ```python theme={null} qprog_opt_width = synthesize( model=main, constraints=Constraints(optimization_parameter=OptimizationParameter.WIDTH), ) ``` Inspect the resulting circuit: ```python theme={null} show(qprog_opt_width) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/319Km1ZWgqCcVR903Cn4Cd05egy ``` ```python theme={null} print( "Width =", qprog_opt_width.data.width, ", Depth =", qprog_opt_width.transpiled_circuit.depth, ) ``` **Output:** ``` Width = 9 , Depth = 199 ``` The new depth and width are 199 and 9, respectively. As expected, we "pay" with extra depth for an implementation that uses less qubits. ## Visualization When opening a quantum program using the `show` command, the visualization provides details regarding the quantum circuit's structure. Key characteristics, such as gate count, qubit usage, and circuit depth, are displayed. The visualization is organized according to the building blocks used during the quantum program construction. These blocks represent modular components or routines in your quantum algorithm. By clicking on the "+" sign on a block, you can expand it to reveal the underlying quantum gates, making it easier to inspect, debug, or understand the algorithm's logic at both high and low levels. This can be seen in the following figure: the left panel represents a high-level view showing the quantum blocks used in the algorithm - such as `hadamard_transform` and `assign x**2 + 1` - while the right panel shows an expanded view of the `assign` block, revealing the underlying quantum gate sequence. Gates include multiple `PHASE` operations and a quantum Fourier transform block (`qft6`), offering insight into the inner workings of this computation. ![Quantum Program Visualization](https://docs.classiq.io/resources/synthesis_tutorial.png) # ## Exporting and Sharing In addition to visualizing a quantum program, the tool provides convenient options to export the quantum circuit in multiple formats, enabling integration with other tools and workflows. Some of them are: * **QASM**: Allows interoperability with quantum simulators and hardware platforms. * **LaTeX**: Produces a high-quality, pictorial representation of the circuit, ready for inclusion in LaTeX files. * **JPEG**: Generates a graphical image of the circuit. To facilitate collaboration, there is a **Share** button that generates a unique link that you can send to anyone who wants to view your circuit directly in their browser - no login required. ## Exercise Create your own `main` function and synthesize it with different constraints. Note that `OptimizationParameter` is only one kind of configuration possible. Classiq synthesis engine also supports rigid constraints of `max_width`, `max_depth` and `max_gate_count`. # Hello World Source: https://docs.classiq.io/getting-started/index Create, synthesize, execute, and analyze your first quantum program with Classiq. Welcome to Classiq. This guide walks you through your first quantum program and introduces the core Classiq workflow: modeling, synthesis, execution, and result analysis. You can get started with Classiq in one of the following ways: * **Studio Python**: code in Python directly in the browser. No local installation required. * **Local Python SDK**: code in Python from your local environment, notebook, or preferred editor. * **Qmod in the Classiq Platform**: write Qmod directly in the browser-based Model Editor. To use Classiq, a Classiq account is required. Access is by invitation only; see the [registration guide](/getting-started/registration_installations/) to request access. Both paths use the same core workflow: ```mermaid theme={null} flowchart LR Model["Model"] --> Synthesis; Synthesis["Synthesis"] --> QuantumProgram; QuantumProgram["Quantum program"] --> Execution; Execution["Execution"] --> Results; Results["Measurement results"] ``` In this example, you create a small quantum program that introduces the basic Classiq workflow. The program allocates a one-qubit quantum number, applies a Hadamard gate to place it in superposition, assigns a classical value to a second quantum number, and adds the two values into a third quantum number. The model uses three output variables: * `x`: a one-qubit quantum number prepared in superposition. * `y`: a quantum number assigned the value `2`. * `z`: a quantum number that stores the expression `x + y`. Because `x` is placed in superposition, execution samples two possible arithmetic outcomes: * When `x = 0`, `z = 2`. * When `x = 1`, `z = 3`. By the end of this example, you will have: * Created a simple Qmod model * Synthesized it into a quantum program * Executed it * Verified that the results show the expected relation between `x`, `y`, and `z` Choose your path: * **Use Python in Classiq Studio** if you want to work with Python in a setup-free, cloud-based coding editor. * **Use the Python SDK locally** if you want to work in notebooks or scripts in your local development environment. * **Use Qmod-native syntax in the Classiq Platform** if you want to write Qmod directly in the browser-based Model Editor. All paths produce the same expected behavior. Use the Classiq Studio if you want to build, synthesize, execute, and inspect quantum programs directly in your browser while still coding in Python - no need to install anything. Open the [Classiq Studio](https://platform.classiq.io/studio/) and create the arithmetic model in a new file: ```python theme={null} from classiq import * @qfunc def main(x: Output[QNum], y: Output[QNum], z: Output[QNum]): allocate(1, x) H(x) y |= 2 z |= x + y qprog = synthesize(main) show(qprog) ``` Visualization of the quantum circuit of a Bell state in light mode. Visualization of the quantum circuit of a Bell state in dark mode. The program visualization displays the synthesized quantum program. A Hadamard gate is applied to the quantum number `x`, and an arithmetic block is applied between `x`, `y`, and `z` to perform the equation $z = x + y$. To execute the quantum program, run: [comment]: DO_NOT_TEST ```python theme={null} res = sample(qprog) print(res) ``` Example output: | x | y | z | counts | probability | bitstring | | - | - | - | ------ | ----------- | --------- | | 0 | 2 | 2 | 1049 | 0.512207 | 11 | | 1 | 2 | 3 | 999 | 0.487793 | 00 | Your exact counts may differ, but the measured results should contain only $(x, y, z) = (0, 2, 2)$ and $(x, y, z) = (1, 2, 3)$, with roughly equal probabilities. This is the result of the arithmetic operation executed by the quantum program. In this path, you used `synthesize` to compile the high-level model into a quantum program, `show` to visualize it, and `sample` to execute it and inspect the measurement results. Use the Python SDK if you want to work locally, in a notebook, or in your preferred code editor. You can follow the more detailed [installation page](/getting-started/sdk_installation) or follow the summarized installation: Install the Classiq Python package: ```bash theme={null} pip install classiq ``` Authenticate your account: ` from classiq import authenticate; authenticate()` Do not forget to import classiq: ```python theme={null} from classiq import * ``` After installing and authenticating, create the arithmetic model: ```python theme={null} from classiq import * @qfunc def main(x: Output[QNum], y: Output[QNum], z: Output[QNum]): allocate(1, x) H(x) y |= 2 z |= x + y qprog = synthesize(main) show(qprog) ``` Visualization of the quantum circuit of a Bell state in light mode. Visualization of the quantum circuit of a Bell state in dark mode. The program visualization displays the synthesized quantum program. A Hadamard gate is applied to the quantum number `x`, and an arithmetic block is applied between `x`, `y`, and `z` to perform the equation $z = x + y$. To execute the quantum program, run: [comment]: DO_NOT_TEST ```python theme={null} res = sample(qprog) print(res) ``` Example output: | x | y | z | counts | probability | bitstring | | - | - | - | ------ | ----------- | --------- | | 0 | 2 | 2 | 1049 | 0.512207 | 11 | | 1 | 2 | 3 | 999 | 0.487793 | 00 | Your exact counts may differ, but the measured results should contain only $(x, y, z) = (0, 2, 2)$ and $(x, y, z) = (1, 2, 3)$, with roughly equal probabilities. This is the result of the arithmetic operation executed by the quantum program. In this path, you used `synthesize` to compile the high-level model into a quantum program, `show` to visualize it, and `sample` to execute it and inspect the measurement results. Use the Classiq Platform if you want to build, synthesize, execute, and inspect quantum programs directly in your browser. Open the [Model Editor](https://platform.classiq.io/dsl-synthesis) and enter the arithmetic model in Qmod-native syntax: ```qmod theme={null} qfunc main(output x: qnum, output y: qnum, output z: qnum) { allocate(1, x); H(x); y = 2; z = x + y; } ``` Click **Synthesize** in the top-right corner. Model Editor displaying the synthesis button and arithmetic code in Qmod-native syntax. After synthesis, the quantum program can be visualized: Visualization of the quantum circuit of a Bell state in light mode. Visualization of the quantum circuit of a Bell state in dark mode. The program visualization displays the synthesized quantum program. A Hadamard gate is applied to the quantum number `x`, and an arithmetic block is applied between `x`, `y`, and `z` to perform the equation $z = x + y$. Click **Execute** in the top-right corner to open the [execution page](https://platform.classiq.io/execution), where you can configure execution settings such as the backend and number of shots. Execution page displaying hardware selection and execution settings, including backend selection and number of shots. Select **Run** in the top-right corner to execute the quantum program. After execution, the results page displays the measured probabilities and counts. You can also export the results as JSON or CSV files. Execution results for the Bell-state quantum program. A histogram of probabilities is displayed together with a table containing measured qubits, counts,
probabilities, and bitstrings. Your exact counts may differ, but the measured results should contain only $(x, y, z) = (0, 2, 2)$ and $(x, y, z) = (1, 2, 3)$, with roughly equal probabilities. This is the result of the arithmetic operation executed by the quantum program. In this path, you used the browser-based Model Editor to define the model, the **Synthesize** button to compile it into a quantum program, and the execution interface to run the program and inspect the results. ## What this example introduced This first program introduced the main ideas you will use throughout Classiq: * A quantum function defines reusable quantum logic. * `main` is the entry point of a Qmod model. * `allocate` initializes quantum variables. * Quantum gates such as `H` that manipulates quantum information. * Synthesis compiles a high-level model into a quantum program. * Execution runs the quantum program and returns measurement results. * Result analysis helps you verify that the program behaves as expected. ## Next steps Choose your next tutorial based on your goal: * **Learn Qmod fundamentals**: [Qmod Tutorial, Part 1](/getting-started/classiq_tutorial/classiq_overview_tutorial) * **Go deeper into Qmod**: [Qmod Tutorial, Part 2](/getting-started/classiq_tutorial/Qmod_tutorial_part2) * **Understand synthesis and optimization**: [Synthesis Tutorial](/getting-started/classiq_tutorial/Qmod_tutorial_part1) * **Learn execution workflows**: [Execution Tutorial, Part 1](/getting-started/classiq_tutorial/execution_tutorial) * **Work with parameterized and variational workflows**: [Execution Tutorial, Part 2](/getting-started/classiq_tutorial/execution_tutorial_part2) You can also join the [Classiq Community Slack](https://classiq-community.slack.com/) for support, questions, and discussions. # Registration Source: https://docs.classiq.io/getting-started/registration_installations Access to the [platform](https://platform.classiq.io/) is available by invitation only; free access for non-commercial purposes is not currently available. Once your account is set up, you can use our [coding Studio](/user-guide/studio/) (web-based IDE) or install the Python SDK package and authenticate your account. This page guides you through the steps. ## Getting Access If you are interested in using Classiq, reach out through the **Contact Us** form at [platform.classiq.io](https://platform.classiq.io/): Fill in the requested information, and a member of the Classiq team will get back to you. ## Joining the Classiq Community Classiq has a vibrant and active [Slack community](https://short.classiq.io/join-slack) that is helpful for technical questions and support, as well as for general quantum computing questions and discussions. To join, click [here](https://short.classiq.io/join-slack) and sign up to the Classiq Community Slack workspace with your Google/Apple account or a general email address: # Using Python and Classiq Source: https://docs.classiq.io/getting-started/sdk_installation ## AI-assisted setup The fastest way to set up Classiq is to copy the setup prompt into your AI coding assistant. Use this prompt if you want your assistant to install the SDK, check your Python environment, authenticate your account, and verify that Classiq is ready to use. Follow these instructions to install and authenticate the Classiq Python SDK on the user's machine. Important constraints: * Do not ask for, collect, view, store, or transmit the user's Classiq credentials, browser session, authentication token, or local credential files. * The customer must personally complete any browser-based login or confirmation step. * Use the Python environment the customer intends to use for Classiq work, such as their active virtual environment, conda environment, notebook kernel, or IDE interpreter. * If an action requires admin/root permissions, stop and ask the customer before proceeding. Steps: 1. Verify Python is available and that the active environment is correct. Run: ```bash theme={null} python --version python -m pip --version ``` Classiq's Python SDK documentation currently states support for Python 3.8 through 3.12. If the user's Python version is different, offer creating a virtual environment with the correct version. 2. Install or upgrade the Classiq package in the active Python environment. Prefer: ```bash theme={null} python -m pip install -U classiq ``` Do not use a different Python or pip executable unless the customer explicitly confirms that it is the intended environment. 3\. Verify installation. Run: ```bash theme={null} python -m pip show classiq ``` 4. Authenticate the SDK. Run the following in Python, a notebook cell, or the user's intended Python environment: `import classiq; classiq.authenticate()` 5. A browser window or authentication URL should appear. Instruct the user to personally open the link, sign in if needed, and click the confirmation button. Do not complete this browser step on behalf of the user. 6. After confirmation, verify that authentication completed successfully by rerunning or testing a minimal Classiq import/session check. 7. If any issue occurs, consult the Classiq Registration and Installation documentation: [https://docs.classiq.io/getting-started/registration\_installations](https://docs.classiq.io/getting-started/registration_installations) Use the troubleshooting section for common authentication issues, including cases where the device is not authenticated or the system is headless/no-browser. ## Manual Python SDK Installation **Once you are granted platform access**, install the SDK package using `pip`: The SDK is currently supported for Python versions `3.8` to `3.12`. ```bash theme={null} pip install -U classiq ``` Run `pip install -U classiq` in your command line, or use `!pip install -U classiq` from your Python IDE.\ Make sure you are within the appropriate Python environment. ## Authentication Authenticate the device with your Classiq account. **In Python**, run these lines: ```Python theme={null} import classiq classiq.authenticate() ``` A confirmation window opens in your web browser. Confirm the authentication: Once you receive the confirmation you are good to go! If you encounter an issue, look for a solution in the [troubleshooting section](#Python-sdk-installation-troubleshooting) or ask in the [community Slack](https://short.classiq.io/join-slack). ## Platform Version Updates Every few weeks a new version of the platform launches. The web-based IDE at [platform.classiq.io](https://platform.classiq.io/) automatically updates. Update the Python SDK package manually with this command: ```bash theme={null} pip install -U classiq ``` To update the Python SDK for a specific version, run this in the terminal: ```bash theme={null} pip install classiq=={DESIRED_VERSION} ``` and replace the `DESIRED_VERSION` with the number of your desired version, e.g. `0.40`. You can check what version of Classiq is installed with: ```bash theme={null} pip show classiq ``` **NOTE**: Only the last three versions of Classiq are supported. For example, if the current version is `0.41`, only versions `0.41`, `0.40`, and `0.39` are supported. \*Once version `0.52` is released - execution of quantum programs with older SDK versions might result in errors or unexpected behavior. To ensure proper execution, upgrade your SDK to the latest version. ## Python SDK Installation Troubleshooting Check your user profile page on the platform (when logged in, click your avatar on the top-right, then "Profile Settings"). Look for the "SDK Configuration" section. If it's not there, simply ignore this step. Otherwise, download the file from the profile and copy it to: * Mac & Linux: `~/.config/classiq/config.env` * Windows: `%APPDATA%\classiq\config.env` Try to use the following command in Python: ```Python theme={null} import classiq classiq.authenticate(overwrite=True) ``` The authentication procedure on headless Linux systems stores the tokens locally in a credentials file. While you must still run the authentication once, it can be done on another system with a browser. On some Apple computers, the following might pop up: Make sure to type your device password and click `Always Allow` **twice**. # Classiq Documentation Source: https://docs.classiq.io/index

Classiq Documentation

Your guide to high-level quantum programming. Code faster, implement better, run anywhere.

Getting Started

Learn the fundamentals and begin your quantum journey with Classiq.

Hello World

Onboarding Tutorial

Build Applications

Explore features, workflows, and advanced guides.

Classiq User Guide

Classiq Library

Reference Materials

Detailed technical documentation for quantum programming.

Qmod Reference

SDK Reference

# Qmod - the Quantum Modeling Language Source: https://docs.classiq.io/qmod-reference/index The Qmod language lets you describe quantum algorithms at a high level of abstraction. It supports unique quantum-computing concepts, as well as more conventional concepts available in high-level classical programming languages. With Qmod you can focus on the pure functional intent of your algorithm, and leave implementation details to be worked out by a powerful compiler and synthesis engine. Gate-level implementation and qubit management decisions made by the engine will be geared toward the required circuit properties. ## One Language, Three Input Formats Qmod can be coded in its own native syntax, and processed by a dedicated parser. This can be done in Classiq's platform web application, in the "Model" page. Qmod can also be coded in Python as part of the Classiq Python SDK, using the `classiq` package. These two input formats are equivalent - they expose the same set of constructs with the same semantics. A Qmod description in Python can be translated to an equivalent in the native syntax and vice versa. Each of these may appeal to different users in different situations. The Qmod native syntax is designed to express the concepts of the language in a pure and concise form. In addition, the platform features smart editor support for Qmod, which is tuned for the language syntax and semantics. Users who feel comfortable programming in the Python language and environment, may find the Python format easier to use. Being embedded in a strong general-purpose language such as Python has another key advantage. Users can utilize general Python computation and existing Python packages to construct parts of the Qmod descriptions. A third way to describe a quantum model is using the Qmod graphical syntax. Graphical editing of Qmod is available through the Classiq web application on the "Graphical Model" page. Only a subset of the Qmod constructs are currently supported in the graphical model editor, but it will grow to cover all of Qmod over the coming releases. ```mermaid theme={null} flowchart LR Native[["`Native Qmod IDE / Model Editor`"]] <--> ModelStructure; Python[["`Python Qmod Classiq SDK`"]] <--> ModelStructure; Graphical[["`Graphical Qmod IDE / Graphical Model page`"]] <-.-> ModelStructure; ModelStructure[("Model data-structure")] --> Synthesis; Synthesis(("Synthesis engine")) --> QuantumProgram["Quantum program"]; ``` # Classical Types Source: https://docs.classiq.io/qmod-reference/language-reference/classical-types Classical types in Qmod are not very different from classical types in conventional programming languages. There are scalar types like `int` and `bool`, and aggregate types, such as arrays and structs. Classical types are used to declare classical function arguments, and global constants. Variables and literal values of classical types can be used in expressions, and support the conventional set of operators commonly available in conventional programming languages. ## Scalar Types In Qmod, scalar types represent numeric values, Boolean values, and Pauli base elements. ### Syntax Python Classes are used to represent scalar types * `CInt` represents integers * `CReal` represents real numbers (using floating point encoding) * `CBool` represents the Boolean values `False` and `True` * `Pauli` represents the Pauli base elements using the symbols `Pauli.I`, `Pauli.X`, `Pauli.Y`, and `Pauli.Z` (with the integer values 0, 1, 2, 3 respectively) * `int` represents integers * `real` represents real numbers (using floating point encoding) * `bool` represents the Boolean values `false` and `true` * `Pauli` represents the Pauli base elements using the symbols `Pauli::I`, `Pauli::X`, `Pauli::Y`, and `Pauli::Z` (with the integer values 0, 1, 2, 3 respectively) ## Arrays Arrays are homogenous collections of scalars or structs with random access. ### Syntax Array types are represented with the generic class `CArray`. Arguments are declared with the type hint in the form: *name* **:** **CArray** \[ **\[** *element-type* \[ **,** *length\_expression* ] **]** ] Array types have the form - *element-type* **\[** \[ *length\_expression* ] **]** ### Semantics *element-type* is any scalar, array, or struct type. *length\_expression* is optional, determining the length of the array. When left out, the length is determined upon variable initialization. Expressions of array type support the following operations: * Subscript: *array-expression* **\[** *index-expression* **]** * Slice: *array-expression* **\[** *from-index-expression* : *to-index-expression* **]** * Length: *array-expression* **.** **len** Literal array values are expressed in the form - **\[** *values* **]**, where *values* is a list of zero or more comma-separated expressions of the same type. ### Example The following example demonstrates the use of classical arrays in Qmod. Function `foo` takes an array of reals, and uses the `.len` attribute and array subscripting to access the elements of the array. Note that index -1 signifies the last element in an array (similar to Python). ```python theme={null} from classiq import * @qfunc def foo(arr: CArray[CReal], qb: QBit): if_(arr.len > 2, lambda: RX(arr[-1], qb), lambda: RX(arr[0], qb)) @qfunc def main(q0: Output[QBit]): allocate(q0) foo([0.5, 1.0, 1.5], q0) ``` ``` qfunc foo(arr: real[], qb: qbit) { if (arr.len > 2) { RX(arr[-1], qb); } else { RX(arr[0], qb); } } qfunc main(output q0: qbit) { allocate(q0); foo([0.5, 1.0, 1.5], q0); } ``` ## Structs Structs are aggregates of variables, called *fields*, each with its own name and type. A Qmod classical struct is defined with a Python data class: A class decorated with `@dataclasses.dataclass`. Fields need to be declared with type-hints like classical arguments of functions. Fields are initialized and accessed like attributes of Python object. Structs are declared in the form - **struct** **\{** *field-declarations* **}**, where *field-declarations* is a list of one or more field declarations in the form - *name* **:** *classical-type* **;**. Expressions of struct type support the field-access operation in the form - *struct-expression* **.** *field-name*. Literal struct values are expressed in the form - *struct-name* **\{** *field-value-list* **}**. where *field-value-list* is a list of zero or more comma-separated field initializations in the form - *name* **=** *expression*. ### Example In the following example a struct type called `MyStruct` is defined. Function `foo` takes an argument of this type and accesses its fields. Function `main` instantiates and populates `MyStruct` in its call to `foo`. ```python theme={null} from classiq import * from dataclasses import dataclass @dataclass class MyStruct: loop_counts: CArray[CInt] angle: CReal @qfunc def foo(ms: MyStruct, qv: QArray[QBit, 2]): H(qv[0]) repeat( count=ms.loop_counts[1], iteration=lambda index: PHASE(ms.angle + 0.5, qv[1]), ) @qfunc def main(qba: Output[QArray[QBit]]): allocate(2, qba) foo(MyStruct(loop_counts=[1, 2], angle=0.1), qba) ``` ``` struct MyStruct { loop_counts: int[]; angle: real; } qfunc foo(ms: MyStruct, qv: qbit[2]) { H(qv[0]); repeat (index: ms.loop_counts[1]) { PHASE(ms.angle + 0.5, qv[1]); } } qfunc main(output qba: qbit[]) { allocate(2, qba); foo(MyStruct { loop_counts = [1, 2], angle = 0.1 }, qba); } ``` ## Hamiltonians Qmod's Python embedding offers a specialized syntax for creating [sparse Hamiltonian](/sdk-reference/qmod/classical-types#sparsepauliop) objects. Calling a [Pauli](/sdk-reference/qmod/classical-types#pauli) enum value (e.g., `Pauli.X`) with an index (e.g., `Pauli.X(3)`) creates a single-qubit Pauli operator. The multiplication of single-qubit Pauli operators (e.g., `Pauli.X(1) * Pauli.Y(2)`) constructs the tensor product of these operators on the respective qubits. These can be linearly combined in a sum, each optionally with a scalar coefficient (e.g., `0.5 * Pauli.X(2) + Pauli.Y(0)*Pauli.Z(2)`). ### Example The Hamiltonian specified by the Pauli strings `XYZ` and `IXI` with coefficients `0.5` and `0.8` respectively is specified in the standard struct literal syntax as follows: [comment]: DO_NOT_TEST ```python theme={null} H = SparsePauliOp( terms=[ SparsePauliTerm( paulis=[ IndexedPauli(pauli=Pauli.Z, index=0), IndexedPauli(pauli=Pauli.Y, index=1), IndexedPauli(pauli=Pauli.X, index=2), ], coefficient=0.5, ), SparsePauliTerm( paulis=[ IndexedPauli(pauli=Pauli.X, index=1), ], coefficient=0.8, ), ], num_qubits=3, ) ``` You can specify the same Hamiltonian using the specialized Hamiltonian syntax as follows: [comment]: DO_NOT_TEST ```python theme={null} H = 0.5 * Pauli.Z(0) * Pauli.Y(1) * Pauli.X(2) + 0.8 * Pauli.X(1) ``` # Classical Variables Source: https://docs.classiq.io/qmod-reference/language-reference/classical-variables Classical variables store classical values such as integers, real numbers, Boolean values, and lists and structs thereof (see [classical types](/qmod-reference/language-reference/classical-types)). In quantum circuits, classical values control rotation gates, like RX, and may be the results of quantum measurements. Qmod provides additional abstractions involving classical values, including classical control flow constructs such as `repeat`, `power`, and `if`. Qmod also supports the use of classical values in the context of quantum expressions. Classical variables can be declared as [function parameters](/qmod-reference/language-reference/functions) or local variables. ## Local Classical Variables Syntax Instead of explicitly declaring local Qmod classical variables, use local Python variables to store classical Qmod expressions. When run-time expressions, such as `measure`, are used, the corresponding Qmod variables are implicitly declared in the same scope. Variable declaration: *classical-var* **:** *classical-type* Variable assignment: *classical-var* **=** *classical-expression* Currently, only local classical variables of type `bool` are supported. ## Semantics The Qmod compiler classifies classical variables according to their evaluation time during the program's lifecycle: *Compile-time* variables are evaluated during compilation, *link-time* variables are evaluated after compilation but before execution, and *run-time* variables are evaluated during the program's execution. The following table describes how the compiler classifies classical variables that appear in each kind of Qmod expression. For instance, if a classical variable is used as an array index, the compiler classifies it as compile-time. | Expression | Compile-time | Link-time | Run-time | | ---------------------------- | ------------ | --------- | -------- | | `allocate` size | ✅ | ❌ | ❌ | | `control` condition | ✅ | ❌ | ❌ | | `if` condition | ✅ | ❌ | ✅ | | `phase` classical expression | ✅ | ✅ | ❌ | | `phase` quantum expression | ✅ | ❌ | ❌ | | `phase` theta | ✅ | ✅ | ❌ | | `power` count | ✅ | ✅ | ❌ | | `repeat` count | ✅ | ❌ | ❌ | | `foreach` values | ✅ | ❌ | ❌ | | Array index | ✅ | ❌ | ❌ | | Quantum arithmetics | ✅ | ❌ | ❌ | | Type attribute | ✅ | ❌ | ❌ | Function arguments are classified as either compile-time or link-time based on the use of the parameter inside the function (run-time function parameters are currently not supported). While the compiler classifies the parameters of user-defined functions, the classifications of atomic functions parameters are predefined. For example, the `evolution_coefficient` parameter of `suzuki_trotter` is link-time but the `reps` parameter is compile-time. [Execution parameters](/qmod-reference/language-reference/quantum-entry-point#model-execution-parameters) must be used as link-time variables. Currently, local classical variables are classified as run-time variables, and their use is restricted to *assignment* and *if* statements. ## Examples The following examples demonstrates how the Qmod compiler classifies different function parameters as either compile-time or link-time variables. [comment]: DO_NOT_TEST ```python theme={null} from classiq import * @qfunc def foo(qarr: QArray[QBit], index: CInt, angle: CReal): RX(angle * index, qarr[index]) ``` ``` qfunc foo(qarr: qbit[], index: int, angle: real) { RX(angle * index, qarr[index]); } ``` Since parameter `index` is used as an array subscript expression, it is classified as a compile-time variable and will be evaluated and eliminated from the program during compilation. On the other hand, parameter `angle` is only used in a rotation expression, so it is classified as a link-time parameter and will appear in the compiled program. Although `index` also appears in the rotation expression, this doesn't affect its classification as a compile-time variable due to its other more restrictive use. See the [mid-circuit measurement](/qmod-reference/language-reference/mid-circuit-measurement) documentation page for code examples of run-time variables. # Expressions Source: https://docs.classiq.io/qmod-reference/language-reference/expressions Expressions in Qmod have syntax, semantics, and use, similar to expressions in conventional programming languages. They comprise literal values, variables, and operators applied to them. However, Qmod is unique in that variables can be of either [classical](/sdk-reference/qmod/classical-types) or [quantum](/qmod-reference/language-reference/quantum-types) types, and quantum variables have states that can be a superposition of values, entangled with the states of other variables. Expressions over quantum variables evaluate to a superposition of correlated values. For example, if `x` is a [classical variable](/qmod-reference/language-reference/classical-variables) of type `CInt` (an integer), then `x + 1` is a classical expression of type `CInt` comprising the operator `+` (plus) applied to `x` and the literal `1`. Similarly, if `qarr` is a [quantum variable](/qmod-reference/language-reference/quantum-variables) of type `QArray[QNum[3]]`, then `qarr[0] > x` is a quantum expression of type `QBit`. *Unary operators* are applied to a single operand. For instance, you can apply the unary operator `~` (bitwise-invert) to variable `x` and get the expression `~x`. *Binary operators* are applied to two operands. For example, the operator `>` (greater-than) in the expression `qarr[0] > x` is applied to two operands, `qarr[0]` and variable `x`. The expression `qarr[0]` comprises the *subscript operator* `[]` applied to variable `qarr` and the literal `0`. Applications of subscript (`[]`) and field-access (`.`) operators to classical and quantum variables are called *path expressions*, since they point to a partial section of the variable along a certain access path. In our case, for instance, `qarr[0]` represents the first (0) element of the array `qarr`. ## Qmod Operators You can apply operators to operand expressions to create composite expressions. If at least one of the operands is quantum then the expression is quantum as well; Otherwise, it is classical. Qmod supports the following operators: ### Arithmetic operators You can apply arithmetic operators to classical numbers (`CInt` and `CReal`) and quantum scalars (`QBit` and `QNum`) to create numeric expressions. * Add: + * Subtract: - (binary) * Negate: - (unary) * Multiply: \* * Power \*\* (quantum base, positive classical integer exponent) * Modulo: % limited for power of 2 * Max: max (n>=2 arguments) * Min: min (n>=2 arguments) ### Bitwise operators You can apply bitwise operators to classical numbers (`CInt` and `CReal`) and quantum scalars (`QBit` and `QNum`) to create numeric expressions. * Bitwise Or: | * Bitwise And: & * Bitwise Xor: ^ * Bitwise Invert: \~ ### Relational operators You can apply relational operators to classical numbers (`CInt` and `CReal`) and quantum scalars (`QBit` and `QNum`) to create Boolean expressions (of types `CBool` and `QBit`). * Equal: == * Not Equal: != * Greater Than: > * Greater Or Equal: >= * Less Than: \< * Less Or Equal: \<= ### Logic operators You can apply logical operators to Boolean expressions (`CBool`, `QBit`, and `QNum[1]`) to create Boolean expressions (of types `CBool` and `QBit`). * Logical And: `logical_and()` (in Qmod Native: and) * Logical Or: `logical_or()` (in Qmod Native: or) * Logical Not: `logical_not()` (in Qmod Native: not) ### Path operators You can use path operators to access parts of classical and quantum variables of aggregate types, namely, structs and arrays. * Field Access: *struct* **.** *field-name* * Array Slice: *array* **\[** *start-index* **:** *stop-index* **]** * In Python, *start-index* and *stop-index* may be omitted. If *start-index* is omitted, a `0` will be placed in its stead. If *stop-index* is omitted, `array.len` will be placed in its stead. * In Python, if *array* is a Python list, use the alternative syntax: **slice\_(** *array* **,** *start-index* **,** *stop-index* **)** * Array Subscript: *array* **\[** *index* **]** * The index of a quantum subscript expression must be an [unsigned quantum integer](https://docs.classiq.io/latest/qmod-reference/language-reference/quantum-types/#quantum-scalar-types) variable. * In Python, if *array* is a Python list and *index* is a quantum variable, use the alternative syntax: **subscript(** *array* **,** *index* **)** * Currently, quantum subscript expressions are not supported in [phase statements](https://docs.classiq.io/latest/qmod-reference/language-reference/statements/phase/). ## Quantum Expressions Quantum expressions are expressions that involve one or more quantum variables. Quantum expressions can occur in the following contexts: * The right-value in [assignment](https://docs.classiq.io/latest/qmod-reference/language-reference/statements/assignment) statements * The condition in [control](https://docs.classiq.io/latest/qmod-reference/language-reference/statements/control) statements * The expression argument in [phase](https://docs.classiq.io/latest/qmod-reference/language-reference/statements/phase) statements During computation, the value(s) of an expression are coherently correlated to the evaluation of the operators over the computational-basis values of the quantum variables it comprises, which may be in any specific superpositions and entanglement. Quantum expressions may include any combination of operators on any classical and quantum variables and literals, with the following exceptions: * All classical variables must be [compile-time](https://docs.classiq.io/latest/qmod-reference/language-reference/classical-variables/#semantics). * The right-hand side of the division (`/`) and power (`**`) operators must be a classical expression. ### Examples The following model includes qubit `q` and quantum numeric `n` of size three. It uses a `control` statement with a quantum expression `n > 4` to apply `X` to `q` only when `n` is greater than four. ```python theme={null} from classiq import * @qfunc def main(n: Output[QNum[3]], q: Output[QBit]): allocate(n) hadamard_transform(n) allocate(q) control(n > 4, lambda: X(q)) ``` ``` qfunc main(output n: qnum<3>, output q: qbit) { allocate(n); hadamard_transform(n); allocate(q); control(n > 4) { X(q); } } ``` After executing this model, you get $q=0$ for $n\in\{0, 1, 2, 3, 4\}$ and $q=1$ for $n\in\{5, 6, 7\}$. See [additional examples](https://docs.classiq.io/latest/qmod-reference/language-reference/statements/assignment/#examples) on the Assignment documentation page. ## Classical Expressions Classical expressions are expressions that involve classical variables and constant literals, but no quantum variables. Classical variables may have known values at [compile time, link time, or runtime](https://docs.classiq.io/latest/qmod-reference/language-reference/classical-variables/#semantics). Classical expressions with only compile-time variables are evaluated and simplified during compilation. This applies sub-expressions of quantum expressions too. Qmod supports several built-in classical [constants and functions](/sdk-reference/qmod/symbolic-functions/), such as `pi` and `sin`. ### Example In the following model, function `foo` accepts quantum numeric `n` and a classical integer `x`, and perform the in-place xor operation `n ^= x + 1`. Function `foo` is called twice, once with `x=1` and once with `x=-1`. ```python theme={null} from classiq import * @qfunc def foo(n: QNum, x: CInt): n ^= x + 1 @qfunc def main(n: Output[QNum]): n |= 1 foo(n, 1) # n ^= 2 foo(n, -1) # n ^= 0 ``` ``` qfunc foo(n: qnum, x: int) { n ^= x + 1; } qfunc main(output n: qnum) { n = 1; foo(n, 1); // n ^= 2; foo(n, -1); // n ^= 0; } ``` On the first call to `foo`, the Qmod compiler assigns `x=1` and simplifies the expression `x + 1` into `2`. Therefore, the first `foo` call applies a constant-value xor `n ^= 2`. On the second call to `foo`, the expression `x + 1` is simplified to `0`. Since the assignment `n ^= 0` has no effect, the Qmod compiler removes it from the model. # Quantum Functions Source: https://docs.classiq.io/qmod-reference/language-reference/functions Quantum functions are operations that modify the state of quantum objects, which are passed to the function as arguments. In addition, quantum functions can take as arguments classical values and other functions. The following example demonstrates how to define a simple Qmod function. Function `rotate` applies a phase specified as a multiple of $\pi$ radians to a qubit. It declares and uses a classical real-number parameter `p` and a quantum single-qubit parameter `q`. ```python theme={null} from classiq import CReal, qfunc, QBit from classiq.qmod.symbolic import pi @qfunc def rotate(p: CReal, qv: QBit): PHASE(theta=p * pi, target=qv) ``` ``` qfunc rotate(p: real, q: qbit) { PHASE(p * pi, q); } ``` ## Syntax The signature of a function comprises the function's name and its parameters, that is, the arguments it expects when called. The function's body is the description of its implementation as a sequence of statements. A quantum function is defined with a regular Python function decorated with `@qfunc` or `@qperm`. The Qmod compiler extracts the signature of the quantum function from the Python type hints. Type hints must be specified for all parameters, and must be Qmod types or, in the case of classical types, their Python counterparts (see [Generative Descriptions](/qmod-reference/language-reference/generative-descriptions)). Direction modifiers for quantum arguments are represented with the generic classes `Input` and `Output`. The *const* modifier for quantum arguments is represented with the generic class `Const`. (**qfunc** | **qperm**) *name* **(** *parameters* **)** **\{** *statements* **}** *parameters* is a list of zero or more comma-separated declarations in one of the three forms: * \[ **output** | **input** ] \[ **const** ] *name* **:** *quantum-type* * *name* **:** *classical-type* * *name* **:** (**qfunc** | **qperm**) \[ **\[** **]** ] **(** *parameters* **)** ## Semantics * A function definition introduces a new function symbol into the global namespace. * The `qfunc` keyword designates a quantum function that modifies the quantum state arbitrarily, while the `qperm` keyword designates a quantum function that modifies the quantum state only as a permutation over computational-basis states (i.e., does not introduce or destroy superpositions). The `qperm` declaration provides the corresponding guarantees for the caller and restrictions on the function's implementation. See [Uncomputation](/qmod-reference/language-reference/uncomputation) for more details. * Parameters can be used as variables in the body of the function, based on their declared types. For more on Qmod types, see [Quantum Types](/qmod-reference/language-reference/quantum-types) and [Classical Types](/qmod-reference/language-reference/classical-types). * Classical parameters can be used as variables in the declaration of subsequent parameter types in the signature of the function. * The direction modifiers `input` and `output` may be used to specify whether a quantum parameter is input-only or output-only. Note that direction modifiers cannot be used with classical or function parameters. * The `const` modifier provides guarantees (and restrictions) on how the quantum state may change within the function. Specifialy, a *const* parameter is immutable up to a phase. See [Uncomputation](/qmod-reference/language-reference/uncomputation) for more details. * Qmod functions can also take functions as arguments. For details on this capability, see [Operators](/qmod-reference/language-reference/operators). Statements can do one of the following: * Call other quantum functions * Declare local quantum variables * Assign expressions to quantum variables * Apply quantum operations to quantum variables * Use classical control flow statements - `repeat` and `if` * Bind quantum variables to other quantum variables ## Examples ### Example 1 - Function Declarations The following example demonstrates function declarations: ```python theme={null} from classiq import CInt, QArray, QBit, QNum, Output, qfunc @qfunc def foo(n: CInt, qba: QArray[QBit, "2*n"]): pass @qfunc def bar(x: QNum, y: QNum, res: Output[QNum]): pass ``` ``` qfunc foo(n: int, qba: qbit[2*n]) { // ... } qfunc bar(x: qnum, y: qnum, output res: qnum) { // ... } ``` Note that when classical arguments are used to specify subsequent arguments, as in the case where `qba` is a qubit array of size 2\*n, the expression is specified as a string literal because the Python variable `n` is not in scope. ### Example 2 - Function Definitions The following example demonstrates a simple function definition. In its body it calls the built-in function `H()` and then iteratively function `PHASE()` using the *repeat* statement (for more on `repeat` see [Classical Control Flow](/qmod-reference/language-reference/statements/classical-control-flow)). A function decorated with `@qfunc` is executed by the Python interpreter to construct the body of the Qmod function. Python functions corresponding to Qmod statements inject the respective statements into the constructed function. ```python theme={null} from classiq import CInt, QBit, H, PHASE, allocate, repeat, qfunc from classiq.qmod.symbolic import pi @qfunc def foo(n: CInt, qv: QBit): H(qv) repeat(n, lambda i: PHASE(theta=(i / n) * pi, target=qv)) ``` ``` qfunc foo(n: int, qv: qbit) { H(qv); repeat (index: n) { PHASE((index / n) * pi, qv); } } ``` # Generative Descriptions Source: https://docs.classiq.io/qmod-reference/language-reference/generative-descriptions Qmod supports classical variables, expressions, and control-flow statements. In addition, in Qmod's Python embedding it is often useful to rely on Python itself to perform classical computations at model construction time. This lets you leverage Python's language features, libraries, and debugging tools. The example below illustrates how the Python `for` statement and `math.asin` function can be used to generate a Qmod description. ```python theme={null} from classiq import * import math @qfunc def foo(qa: QArray[QBit]): for i in range(qa.len): # 'qa.len' is a Python integer PHASE( math.asin(i / qa.len), qa[i] ) # the expression `i / qa.len` is a Python float ``` ## Python-Type Function Parameters In Python, Qmod functions may declare parameters of either Qmod classical types, or their Python built-in counterparts. Variables of Qmod types are represented symbolically in Python (see explanation below), while variables of standard Python types hold the actual Python value. The values of Python-type parameters are known at model construction time, while the value of Qmod variables are known only at a later compilation or execution stage. The table below lists the mappings between Qmod classical types and Python built-in types (for more on classical types, see [Classical Types](/qmod-reference/language-reference/classical-types)). Using Python types other than the ones listed below is flagged as an error. | Qmod | Python | | :----- | :----: | | CInt | int | | CReal | float | | CBool | bool | | CArray | list | Qmod functions can declare parameters of function type using the generic class `QCallable` (see [Operators](/qmod-reference/language-reference/operators)). In these cases too, parameters may be Python built-in types. When the actual function argument is a Python lambda expression, the use of lambda parameters as symbolic or Python values must conform to the declaration of the respective parameters in the receiving function. ## Symbolic and Python-Value Expression Semantics * A classical expression is symbolic if it contains any variable declared with a Qmod type. A (non-symbolic) Python expression may contain only Python variables. * Python literal values, variables, and expressions can be passed as arguments to functions expecting Qmod types, or be used in Qmod statements. However, Qmod variables and expressions cannot be passed as arguments to functions expecting Python types, nor be used in general Python expression contexts. * The attributes of quantum variables - `size`, `len`, `is_signed`, and `fraction_digits` - are treated as Python (non-symbolic) expressions (see [Quantum Types](/qmod-reference/language-reference/quantum-types)). * The `len` attribute of classical arrays is symbolic for type `CArray` and a Python value for type `list`. * The index variable in a Qmod `repeat` statement is a Qmod symbolic variable of type `CInt` (see [Classical Control Flow](/qmod-reference/language-reference/statements/classical-control-flow)). * Execution parameters (i.e., parameters of function `main`) must be symbolic (see [Quantum Entry Point](/qmod-reference/language-reference/quantum-entry-point)). Notes: * The body of functions with Python parameters will be evaluated for every different set of arguments, while the body of a function with symbolic parameters will be evaluated only once. The overall Qmod compilation process for symbolic logic may be more efficient. * The Classiq SDK includes the package `classiq.qmod.symbolic` with useful math functions operating on Qmod symbolic expressions. * Qmod symbolic expressions and control-flow statements are processed by the Qmod compiler. Therefore, they can be translated back to the Qmod native syntax and visualized as such in the Classiq IDE. In contrast, Python expressions and statements are evaluated by the Python interpreter prior to reaching the Qmod toolchain and leave no trace in translation or visualization. * Python expressions and statements have the advantage of being supported by conventional Python debugging tools. ## Examples ### Example 1: Generative Description of QFT The following example demonstrates the use of nested Python `for` loops to define the quantum Fourier transform (QFT). An equivalent description can be written with nested Qmod `repeat` statements, but the Python version is more readable and easier to debug. The printouts with the `print` statement are meaningful only in this style, that is, using non-symbolic Python expressions. ```python theme={null} from math import pi from classiq import * @qfunc def my_gen_qft(qa: QArray[QBit]): for i in range(qa.len): H(qa[i]) for j in range(qa.len - i - 1): phase = pi / (2 ** (j + 1)) print(f"phase on qubit {i}: {phase}") CPHASE(phase, qa[i + j + 1], qa[i]) ``` Note that calling `my_gen_qft` multiple times with qubit arrays of different lengths will evaluate the body multiple times, and the printouts will reflect these separate calls. ### Example 2: Symbolic and Python Expressions with Control-flow Statements The example below shows the use of symbolic and Python expressions in the context of classical *if* statements. Function `foo` takes 2 classical parameters - `p1` of Qmod type `CInt`, and `p2` of the corresponding Python-type `int`. The expression `p1 > 3` is a symbolic expression, while `p2 > 3` is a Python-value expression. Both can be used as the condition in a Qmod `if_` statement, as is shown in case *A* and *B*. However, only `p2 > 3` can be used in a Python `if` statement, as shown in case *C*. Case *D* is therefore illegal. ```python theme={null} from classiq import * @qfunc def foo(p1: CInt, p2: int, q: QBit): if_(p1 > 3, lambda: X(q)) # case A - OK if_(p2 > 3, lambda: X(q)) # case B - OK if p2 > 3: # case C - OK X(q) # if p1 > 3: # case D - Error: 'p1 > 3' is a symbolic expression # X(q) ``` ### Example 3: Python-Type Parameter in a Lambda Function The example below demonstrates the declaration and use of a function parameter with a Python type. The function `my_operator` takes a function parameter `my_operand`, which expects a Python `float` as parameter. In function `main`, `my_operator` is called and passed a lambda expression in which the corresponding `ratio` parameter is used in Python context expression, namely as the argument of `math.asin`. If `ratio*2` were a symbolic expression, it would be illegal to use it in this context. ```python theme={null} from classiq import * from math import asin @qfunc def my_operator(my_operand: QCallable[float, QBit], q: QBit): H(q) my_operand(0.5, q) H(q) @qfunc def main(q: Output[QBit]): allocate(q) my_operator(lambda ratio, target: RX(asin(ratio * 2), target), q) ``` ### Example 4: Numeric Attributes as Python-value Expressions In the following model, function `compute_arith` checks that the inferred number of fraction digits required to store the result of some quantum computation does not exceed some threshold. In function `main`, `compute_arith` is called twice, where the second time violates the requirement. Therefore, an exception is raised during model construction.There is no Qmod equivalent to raising an exception, so using Python logic is necessary in this case. ```python theme={null} from classiq import Output, QNum, allocate, qfunc @qfunc def compute_arith(x: QNum, y: QNum, z: Output[QNum]): z |= x * y if z.fraction_digits > 4: raise ValueError("Fraction digits exceed max") @qfunc def main(): x = QNum(size=4, is_signed=False, fraction_digits=2) y = QNum(size=4, is_signed=False, fraction_digits=2) allocate(x) allocate(y) tmp = QNum() res = QNum() compute_arith(x, y, tmp) compute_arith(tmp, y, res) ``` # Qmod Language Reference Source: https://docs.classiq.io/qmod-reference/language-reference/index The Qmod reference manual describes the language concepts and constructs, and demonstrates them through examples. The examples are present in both input formats - the Qmod native syntax and its Python embedding. ## Qmod Native Syntax Rules Qmod generally follows the C language lexical and syntactic conventions. Identifiers, literal values, and inline comments, are styled after the C family, as well as syntactic nesting and statement terminators. ## Python Embedding Design The embedding of Qmod in Python leverages Python mechanisms for language enhancements, such as type-hints, decorators, and "magic methods". The regular Python execution of decorated functions and the statements under them constructs a representation of the Qmod description. Expressions are generally evaluated symbolically, that is, construct a representation of the expression that retains the symbols whose values are unknown at that point. # Mid-Circuit Measurement Source: https://docs.classiq.io/qmod-reference/language-reference/mid-circuit-measurement This feature is under development. ## Syntax **measure** **(** *quantum-var* **)** **measure** **(** *quantum-var* **)** ## Semantics * A `measure` call receives a quantum variable of type `QBit` and returns a `CBool` value. * In Qmod Native, when `measure` is called, it must be immediately be assigned to a local classical variable, e.g., `x = measure(q);`. * Measurements are [run-time](/qmod-reference/language-reference/classical-variables#run-time-variables) values. Currently, `if` is the only control flow statements that supports run-time variables. * Following the `measure` operation, any superposition state of its operand collapses to a computational-basis state corresponding to the classical measured result. ## Example The following function implements the `RESET` gate using a mid-circuit measurement. [comment]: DO_NOT_TEST ```python theme={null} from classiq import * @qfunc def reset(q: QBit): val = measure(q) if_(val, lambda: X(q)) ``` ``` qfunc reset(q: qbit) { val: bool; val = measure(q); if (val) { X(q); } } ``` # Operators Source: https://docs.classiq.io/qmod-reference/language-reference/operators A function in Qmod can take other functions as arguments and call these functions in its body. Functions operating on other functions are often referred to as higher-order functions, or operators. This mechanism is used to define reusable quantum algorithm patterns, such as the Quantum Phase Estimation and the Grover operator. ## Function types Parameters of function types are declared like parameters of other type categories. The function type determines the list of arguments that must be accepted by the function passed in as argument. Array function types correspond to an indexable collection of function values with a common signature. ### Syntax The `QCallable` type hint is used to specify a function type. `QCallable` itself is a generic class, taking as parameters a list of type hints that declare the parameters of the function. The `QPerm` type hint is used to specify a *permutation* function type. The `QCallableList` type hint specifies a function array type, and `QPermList` specifies a *permutation* function array type. The name of a parameter in a function type can optionally be specified using the form - **Annotated** **\[** *type* **, "** *name* **"]**. Function type syntax has the following form - (**qfunc** | **qperm**) **(** *function-type-parameters* **)** *function-type-parameters* is a list of zero or more comma-separated declarations in the form - \[ *name* **:** ] *type*. If **\[]** follows the **qfunc**/**qperm** keyword, the parameter is interpreted as a function array - (**qfunc** | **qperm**) **\[** **]** **(** *function-type-parameters* **)** ### Semantics * The function passed as argument to an operator must agree in its signature with the number, types, and order of parameters declared in the respective function type. Note that names of parameters are optional in the function type, and where specified, are not required to match the argument. * A parameter of a function type can be called inside the function's body just like a regular function, passing arguments as per the declared signature. * An element of a function array type can be called with a subscript operator applied to the parameter, followed by the argument list. * The `qperm` keyword specifies guarantees (and restrictions) on how the quantum state may change within the function. See [Uncomputation](/qmod-reference/language-reference/uncomputation) for more details. ### Example In the following example, the function `my_operator` declares the parameter `my_operand` of a function type with one classical parameter and one quantum parameter. The function is called twice in its body, passing different argument values. In function `main`, `my_operator` is called twice, each time passing a different function as its argument. ```python theme={null} from classiq import * from classiq.qmod.symbolic import pi @qfunc def my_operator(my_operand: QCallable[CReal, QBit], q: QBit): my_operand(pi / 2, q) my_operand(pi / 4, q) @qfunc def main(q: Output[QBit]): allocate(q) my_operator(RX, q) my_operator(RY, q) ``` Notes: * Passing named functions in Python is currently not supported. This example uses Python lambda expressions - see more in the next section. * Function arguments in Python do not support specifying argument names. When translating Qmod Python description to native syntax, names `arg0`, `arg1`, etc. are associated with the arguments automatically. ``` qfunc my_operator(my_operand: qfunc (real, qbit), q: qbit) { my_operand(pi / 2, q); my_operand(pi / 4, q); } qfunc main(output q: qbit) { allocate(q); my_operator(RX, q); my_operator(RY, q); } ``` Synthesizing this model creates the quantum program shown below. You can see four rotations in the circuit with their respective angles. visualization of the first example bright visualization of the first example dark ## Lambda functions You can pass a function to an operator in one of two forms - a named function, and a lambda function. Lambda functions are anonymous functions defined in-line in the operator call site. Note that in Python only the lambda function form is supported. ### Syntax A Python `Callable` object is used as a Qmod lambda function. This can take one of two forms - a Python lambda expression, or a named *Python* function (not decorated with `@qfunc`). When a named function is used with type hints on its arguments, the names of the operands will be reflected in the Qmod description. Lambda function syntax is somewhat similar to a function definition. The keyword `qfunc` is replaced with `lambda`, the name of the function is omitted, and argument lists only specify only names, not types. **lambda** \[ **\<** *classical-arg-names* **>** ] **(** *quantum-arg-names* **)** **\{** *statements* **}** ### Example 1 Consider the following snippet, where `my_operator` is called twice from function `main`, once with a regular function and a second time with a lambda function. These two calls are equivalent. ```python theme={null} from typing import Annotated from classiq import * from classiq.qmod.symbolic import pi @qfunc def my_operator( my_operand: QCallable[Annotated[CReal, "angle"], Annotated[QBit, "target"]], q: QBit ): H(q) my_operand(pi / 2, q) @qfunc def my_operand(angle: CReal, target: QBit): RX(angle, target) @qfunc def main(q: Output[QBit]): allocate(q) my_operator(my_operand, q) my_operator(lambda angle, target: RX(angle, target), q) ``` ``` qfunc my_operator(my_operand: qfunc (angle: real, target: qbit), q: qbit) { H(q); my_operand(pi / 2, q); } qfunc my_operand(angle: real, target: qbit) { RX(angle, target); } qfunc main(output q: qbit) { allocate(q); my_operator(my_operand, q); my_operator(lambda(angle, target) { RX(angle, target); }, q); } ``` ### Example 2 An operator may pass expressions involving its own arguments to its operand. The following example demonstrates this. ```python theme={null} from classiq import * from classiq.qmod.symbolic import pi @qfunc def foo_operator( n: CInt, my_operand: QCallable[CReal, QBit], qba: QArray[QBit, 2], ): H(qba[0]) my_operand(pi / n, qba[1]) @qfunc def main(qba: Output[QArray[QBit]]): allocate(2, qba) foo_operator(4, lambda theta, target: RX(theta, target), qba) ``` ``` qfunc foo_operator(n: int, my_operand: qfunc (real, qbit), qba: qbit[2]) { H(qba[0]); my_operand(pi / n, qba[1]); } qfunc main(output qba: qbit[]) { allocate(2, qba); foo_operator(4, lambda(theta, target) { RX(theta, target); }, qba); } ``` Synthesizing this model creates the quantum program shown below. You can see that the call to `foo_operator` applies an X rotation on qubit 1, based on the value of `n` passed to it. visualization of the second example bright visualization of the second example dark ## Capturing context variables and parameters A lambda function that is passed as an argument to an operator may reference classical or quantum variables in its own lexical scope. The objects whose references are captured are available when the callable is invoked by the operator, even though the operator itself is oblivious to them. An operator must not implicitly change the initialized status of quantum variables captured inside lambda functions that are passed to it. Only initialized variables may be captured, and they remain initialized after the operator call. Hence, quantum variables cannot be captured as output-only or input-only arguments inside a lambda function. Note that an operator may actually invoke the operand once, multiple times, or not at all. ### Example The following example is similar to *Example 2* from the previous section. However, in this case, the quantum variable used inside the lambda function is captured directly from the scope rather than being passed to it indirectly through the operator. The resulting quantum program in this case is identical to that of the previous version in *Example 2* above. ```python theme={null} from classiq import * from classiq.qmod.symbolic import pi @qfunc def foo_operator( n: CInt, my_operand: QCallable[CReal], qb: QBit, ): H(qb) my_operand(pi / n) @qfunc def main(qb1: Output[QBit], qb2: Output[QBit]): allocate(qb1) allocate(qb2) foo_operator(4, lambda t: RX(t, qb1), qb2) ``` ``` qfunc foo_operator(n: int, my_operand: qfunc (real), qb: qbit) { H(qb); my_operand(pi / n); } qfunc main(output qb1: qbit, output qb2: qbit) { allocate(qb1); allocate(qb2); foo_operator(4, lambda(t) { RX(t, qb1); }, qb2); } ``` # Quantum Entry Point Source: https://docs.classiq.io/qmod-reference/language-reference/quantum-entry-point A quantum model in Qmod is compiled into a quantum program - a concrete executable description. You can execute a quantum program any number of times on quantum hardware or simulators, and specify execution preferences (such as number of shots). When executing a parametric quantum program you must assign values to its parameters. Both compilation and execution start from a user-defined quantum function called 'main', which is the quantum program entry point. Function `main` specifies the inputs and outputs of the quantum program, that is, its interface with the external classical execution logic. ## Model Outputs Function `main` can declare quantum output arguments. Upon invocation, the quantum program executes the specified number-of-shot times, and its outputs are measured each time. When using the `sample` operation, each output variable is measured in the computational (z) basis. Their names and values, along with the respective counts, are available in the returned result. The values are interpreted according to their specified types (see [Quantum Types](/qmod-reference/language-reference/quantum-types)). Function `main` cannot declare quantum arguments other than using the `output` modifier, since the classical execution logic cannot pass quantum states as arguments. For example, consider the following model: ```python theme={null} from classiq import * @qfunc def main(a: Output[QBit], b: Output[QNum[2, UNSIGNED, 2]]): allocate(a) H(a) allocate(b) control(a, lambda: apply_to_all(lambda target: X(target), b)) ``` ``` qfunc main(output a: qbit, output b: qnum<2, UNSIGNED, 2>) { allocate(a); H(a); allocate(b); control (a) { apply_to_all(lambda(target) { X(target); }, b); } } ``` This model can be synthesized and executed in the SDK with the following code - [comment]: DO_NOT_TEST ```python theme={null} qprog = synthesize(main) job = execute(qprog) result = job.result()[0] print(result.value.parsed_counts) ``` The printout will show the counts of the measured values of `a` and `b` thus - ``` [{'a': 1.0, 'b': 0.75}: 503, {'a': 0.0, 'b': 0.0}: 497] ``` Similarly, the results of the execution job in the Classiq web application show the values as tooltip on the histogram bars. parsed_results.png ## Model execution parameters Function `main` can declare classical parameters of scalar types (integers and reals) and arrays thereof with explicitly specified lengths. These are called execution parameters. They are assigned by the external classical execution logic using arguments to the execution operations `sample` and `estimate`. Execution parameters are left in their symbolic form in the quantum program, so that passing different sets of parameter values does not require re-synthesis of the model. Execution parameters can figure in restricted contexts only as rotation angles in gate-level functions and as the exponent value of the `power` operation. They cannot be used in classical control flow statements (`repeat` and `if`) or in subscript/slice expressions. Below is a full SDK example of a very simple model with an execution parameter. The quantum program is invoked in a loop using the method `sample` of `ExecutionSession`. ```Python theme={null} from math import pi from classiq import * @qfunc def main(angle: CReal, res: Output[QBit]): allocate(res) RX(angle, res) qprog = synthesize(main) with ExecutionSession(qprog, ExecutionPreferences(num_shots=10000)) as es: for i in range(4): result = es.sample({"angle": i * pi / 4}) print(result.parsed_counts) ``` Running this script will print, for example - ``` [{'res': 0.0}: 10000] [{'res': 0.0}: 8560, {'res': 1.0}: 1440] [{'res': 0.0}: 5019, {'res': 1.0}: 4981] [{'res': 1.0}: 8514, {'res': 0.0}: 1486] ``` # Quantum Types Source: https://docs.classiq.io/qmod-reference/language-reference/quantum-types Once initialized, Qmod variables reference a quantum object in some state. Quantum types determine the overall number of qubits used to store the object, as well as the interpretation of its state. For example, a quantum object stored on 4 qubits can represent an array of 4 bits, an integer number in the domain 0 to 15, or an array of two fixed-point numbers in the domain \[-1.0, -0.5, 0, -0.5]. The type determines which interpretation is the intended one, for example, when evaluating quantum operators. Qmod has two categories of quantum scalar types - bits and numbers. Qmod also supports quantum array and struct types, which can be arbitrarily nested. Certain quantum type attributes can be retrieved using field access: *var*.*attr-name*. For example, the total number of qubits referenced by variable `my_var` is obtained by writing `my_var.size`. The attributes associated with each quantum type are listed below. ## Quantum Scalar Types In Qmod, there are two kinds of scalar quantum types: * `qbit` represents the states $|0\rangle$, $|1\rangle$, or a superposition of the two * `qnum` represents numbers in some discrete domain - integers or fixed-point reals When declaring a `qnum` variable, you can optionally specify its numeric attributes - overall size in bits, whether it is signed, and the number of binary fraction digits. ### Syntax In Python the classes `QBit` and `QNum` are used as type hints in the declaration of arguments: *name* **:** **QBit** *name* **:** **QNum** \[ **\[** *size-int-expr* \[ **,** *sign-bool-expr* **,** *frac-digits-int-expr* ] **]** ] The same classes are used to declare local variables: *name* = **QBit** **( "** *local\_name* **" )** *name* = **QNum** **( "** *name* **" ,** \[ \[ **size =** ] *size-int-expr* **,** \[ \[ **is\_signed =** ] *sign-bool-expr* **,** \[ \[ **fraction\_digits =** ] *frac-digits-int-expr* ] **)** **qbit** **qnum** \[ **\<** *size-int-expr* \[ **,** *sign-bool-expr* **,** *frac-digits-int-expr* ] **>** ] It is recommended to use the `SIGNED` and `UNSIGNED` built-in constants instead of `True` and `False` respectively when specifying the *sign-bool-expr* `qnum` property. ### Semantics * Computational-basis encoding of numeric types is big-endian (the most significant bit has the highest index). * *size-int-expr* determines the overall number of qubits used to store the number, including sign and fraction where applicable. * If *sign-bool-expr* is `True` (`SIGNED`), two's complement is used to represent signed numbers, utilizing the most-significant bit for sign. * *frac-digits-int-expr* determines the number of least-significant bits representing binary fraction digits. * When only *size-int-expr* is specified and *sign-bool-expr* and *frac-digits-int-expr* are left out, the later two are set to `UNSIGNED` and `0` (integer) respectively. ### Attributes * `qbit`: * `size`: The total number of qubits (always 1). * `qnum`: * `size`: The total number of qubits (including the fraction digits and the sign bit). * `is_signed`: Whether the number is signed. * `fraction_digits`: The number of fraction digits. ### Examples In the following example, two 4-qubit numeric variables, `x` and `y`, are prepared to store the bit string `1101`. `x` is declared with no sign bit and no fraction digits, and therefore its state represents the number 13. `y` is declared to be signed and have one fraction-digit, and thus, the same bit-level state represents the number -1.5. ```python theme={null} from classiq import * @qfunc def prepare_1101(qba: Output[QArray[QBit]]): allocate(4, qba) X(qba[0]) X(qba[2]) X(qba[3]) @qfunc def main(x: Output[QNum[4]], y: Output[QNum[4, SIGNED, 1]]): prepare_1101(x) prepare_1101(y) ``` ``` qfunc prepare_1101(output qba: qbit[]) { allocate(4, qba); X(qba[0]); X(qba[2]); X(qba[3]); } qfunc main(output x: qnum<4>, output y: qnum<4, SIGNED, 1>) { prepare_1101(x); prepare_1101(y); } ``` ## Numeric Inference Rules Numeric representation modifiers are optional in the declaration. When left out, the representation attributes of a `qnum` variable are determined upon its first initialization. Following are the inference rules for these cases: * When the varialbe is initialized with `allocate`, the size is determined by the `num_qubits` argument, while sign and fraction-digits are either explicitly specified or default to `False` (`UNSIGNED`) and 0 respectively. * When the variable is passed to a function as its output argument with declared type `qbit[]`, the size is determined by the actual array size, while sign and fraction-digits default to `False` (`UNSIGNED`) and 0 respectively. * When the variable is passed to a function as its output argument with declared type `qnum`, the size is determined by the actual size, sign, and fraction-digits of the function's output. * When the variable is initialized on the left of an out-of-place assignment `=`, the domain of the expression determines its representation properties. * Variables retain their type, including the representation attributes, even after being un-initialized (for example, when occurring on the left side of a *bind* statement). Subsequent initializations must agree with the specific `qnum` type. * On the right side of a *bind* statement (`->`) the representation attributes of `qnum` variables must already be known either through declaration, or by previous initialization (and subsequent un-initialization). ### Examples The following example demonstrates the default and explicit numeric interpretation of quantum states. Two variables, `a` and `b`, are initialized to some quantum state. `a` is left with the default unsigned integer interpretation. `b` is initialized to a superposition of the bit strings 01 and 10 interpreted with a sign bit and one fraction digit. This implies that its domain is \[-1.0, -0.5, 0, 0.5] and its value is in a superposition of -1.0 and 0.5. `res` is accordingly uniformly distributed on the 8 possible addition values. ```python theme={null} from classiq import * @qfunc def main(a: Output[QNum], b: Output[QNum[2, SIGNED, 1]], res: Output[QNum]) -> None: allocate(2, a) # 'a' is a 2 qubit unsigned int in the domain [0, 1, 2, 3] hadamard_transform(a) # 'a' is in a superposition of all values in its domain prepare_state([0, 0.5, 0.5, 0], 0, b) # 'b' is in superposition of 01 and 10 res |= a + b ``` ``` qfunc main(output a: qnum, output b: qnum<2, SIGNED, 1>, output res: qnum) { allocate(2, a); hadamard_transform(a); prepare_state([0, 0.5, 0.5, 0], 0, b); res = a + b; } ``` ## Rounding a `qnum` in Qmod QNum variables may occasionally be declared with too few qubits to represent their intended values. This can occur, for example, when a variable is the result of an arithmetic operation. In such cases, Qmod automatically resolves the issue by adjusting the variable’s possible outcomes. Specifically, it rounds down the numeric values to fit within the allocated number of qubits. ### Examples The following example shows that when allocating a `qnum` and then performing some arithmetic operation, the values are rounded down: ```python theme={null} from classiq import * @qfunc def main(x: Output[QNum[3, False, 1]], y: Output[QNum[4, False, 2]]): allocate( x ) # Allocate x as a quantum number with 3 qubits, no sign and 1 fraction digit allocate( y ) # Allocate y as a quantum number with 4 qubits, no sign and 1 fraction digit hadamard_transform(x) # Create a superposition of all possible numbers of x y ^= 1.4 * x # Evaluate y = 1.4 * x ``` ``` qfunc main(output x: qnum<3, False, 1>, output y: qnum<4, False, 2>) { allocate(x); allocate(y); hadamard_transform(x); y ^= 1.4 * x; } ``` Output measurements: ``` state=[{'x': 1.0, 'y': 1.25}: 268, {'x': 1.5, 'y': 2.0}: 262, {'x': 2.5, 'y': 3.25}: 261, {'x': 0.0, 'y': 0.0}: 260, {'x': 2.0, 'y': 2.75}: 255, {'x': 3.5, 'y': 0.75}: 253, {'x': 0.5, 'y': 0.5}: 251, {'x': 3.0, 'y': 0.0}: 238] ``` Notice, for instance, when `x = 1.0`, the exact product 1.4 is rounded down to 1.25 to fit into `y`'s available qubits. ## Quantum arrays A quantum array is an object that supports indexed access to parts of its state - its elements. Elements are interpreted as values of the specified array element type. A quantum array is one object with respect to its lifetime. Elements of an array cannot be initialized separately or bound separately to other variables. Also, an array's length (the number of elements it represents) is fixed at the time of its initialization and remains constant throughout its lifetime. ### Syntax In Python the class `QArray` is used as type hints in the declaration of arguments: *name* **:** **QArray** \[ **\[** *element-type* \[ **,** *length-expr* ] **]** ] The same class is used to declare local variables: *name* = **QArray** **( "** *name* **"** \[ **,** *element-type* \[ **,** *length-expr* ] ] **)** *element-type* **\[** \[ *length-expr* ] **]** ### Semantics * *element\_type* optionally determines the type of the array elements. Arrays are homogenous, that is, all elements are of the same type. When left unspecified, the type defaults to `qbit`. * *length-expr* optionally determines the number of elements in the array. The overall size of the array is its length multiplied by the size of the element type. When the length is unspecified, it is determined upon initialization based on the element size. Similarly, when the size of the element type is not specified, it is inferred upon initialization based on the length. Either the length, or the size of the element type, must be specified in the declaration * The length cannot change throughout the lifetime of an array. Expressions of quantum array type support the following operations: * Subscript: *array-expression* **\[** *index-expression* **]** * Slice: *array-expression* **\[** *from-index-expression* : *to-index-expression* **]** * Length: *array-expression* **.** **len** ### Attributes * `size`: The total number of qubits (= length × element size). * `len`: The number of array elements. ### Examples In the following example, a Boolean expression of a 3-SAT formula is evaluated over the elements of a qubit array, which is prepared in the state of uniform superposition. Note that bitwise operators are used in this case, but equivalent logical operators `and`, `or`, and `not` (and their respective Python counterparts in package `qmod.symbolic`) are also supported. ```python theme={null} from classiq import * @qfunc def main(x: Output[QArray[QBit, 3]], res: Output[QBit]) -> None: allocate(x) hadamard_transform(x) res |= (x[0] | ~x[1] | ~x[2]) & (~x[0] | x[1] | ~x[2]) ``` ``` qfunc main(output x: qbit[3], output res: qbit) { allocate(x); hadamard_transform(x); res = (x[0] | ~x[1] | ~x[2]) & (~x[0] | x[1] | ~x[2]); } ``` The next example demonstrates the initialization of a numeric array using the *bind* statement (`->`). Two numeric variables are declared and initialized separately and subsequently bound together to initialize the array. The declared type of these variables is an unsigned integer, but the declared element type of the array is signed. Hence, the values 6 and 7 are interpreted as -2 and -1, respectively. When executing the resulting quantum program, `res` is sampled with the value -3 (with probability 1). ```python theme={null} from classiq import * @qfunc def main(res: Output[QNum]) -> None: n0 = QNum("n0", 3) n0 |= 6 n1 = QNum("n1", 3) n1 |= 7 n_arr = QArray("n_arr", QNum[3, SIGNED, 0]) bind([n0, n1], n_arr) res |= n_arr[0] + n_arr[1] ``` ``` qfunc main(output res: qnum) { n0: qnum<3>; n0 = 6; n1: qnum<3>; n1 = 7; n_arr: qnum<3, SIGNED, 0>[]; {n0, n1} -> n_arr; res = n_arr[0] + n_arr[1]; } ``` ## Quantum structs A quantum struct is an object that supports named access to parts of its state - its fields. Each field corresponds to a slice of the overall object, interpreted according to its declared type. A quantum struct is one object with respect to its lifetime. Fields of a struct cannot be initialized separately or bound separately to other variables. Quantum structs are typically used to pack and unpack multiple variables, that is, to switch between contexts that treat the object in a generic way (as a qubit array) and in a problem-specific way (to capture expressions over fields). ### Syntax The following syntax is used to define a quantum struct type - A quantum struct type in Python is defined using a Python class derived from the class `QStruct`. Fields are declared with type hints, similar to how member variables are declared in a Python `dataclass`. **qstruct** *name* **\{** *field\_declarations* **}** *field-declarations* is a list of one or more field declarations in the form - *name* **:** *quantum-type* **;**. ### Semantics * Only quantum types are allowed as field types in a quantum struct. * Quantum structs may be arbitrarily nested, that is, a field of a struct may itself be a struct or a struct array. However, recursive struct types are not allowed. * The overall size of a struct (the number of qubits used to store it) must be known upon declaration. This means that the size of all fields, except at most one, must be fully specified. Expressions of quantum struct type support field-access operation in the form - *struct-expression* **.** *field-name*. ### Attributes * `size`: The total number of qubits (= the sum of field sizes). In Qmod's Python embedding, the quantum struct's total number of qubits can be retrieved using the `num_qubits` class attribute. ### Examples In the following example, quantum struct `MyQStruct` is defined and subsequently initialized and prepared in a specific state in function `main`. ```python theme={null} from classiq import * class MyQStruct(QStruct): a: QBit b: QNum[3] @qfunc def main(s: Output[MyQStruct]) -> None: allocate(s) H(s.a) s.b ^= 6 ``` ``` qstruct MyQStruct { a: qbit; b: qnum<3>; } qfunc main(output s: MyQStruct) { allocate(s); H(s.a); s.b ^= 6; } ``` The example below demonstrates the common situation where an algorithm alternates between the two views of a quantum state - the structured view with partition into problem variables, and the unstructured view as an array of qubits. The example defines a constraint over two variables, `a` and `b`, of different numeric types. It uses Grover-search to find a solution. In the Grover-search algorithm, encapsulated by the function `grover_search`, the oracle application uses the structured view of the state to evaluate the constraint, while the diffuser is defined in a generic way and uses the qubit array view of the state. ```python theme={null} from classiq import * class MyProblem(QStruct): a: QNum[2, UNSIGNED, 2] b: QNum[3, UNSIGNED, 3] @qperm def my_problem_constraint(p: Const[MyProblem], res: QBit) -> None: res ^= p.a + p.b == 0.625 @qfunc def main(p: Output[MyProblem]) -> None: allocate(p) grover_search(2, lambda p: phase_oracle(my_problem_constraint, p), p) ``` ``` qstruct MyProblem { a: qnum<2, UNSIGNED, 2>; b: qnum<3, UNSIGNED, 3>; } qperm my_problem_constraint(const p: MyProblem, res: qbit) { res ^= (p.a + p.b) == 0.625; } qfunc main(output p: MyProblem) { allocate(p); grover_search(2, lambda(p) { phase_oracle(my_problem_constraint, p); }, p); } ``` Executing this model will sample a state representing a solution to the problem in very high probability. This is an example of an output. Here is an output example: ``` state={'p': {'a': 0.0, 'b': 0.625}} shots=350 state={'p': {'a': 0.25, 'b': 0.375}} shots=344 state={'p': {'a': 0.5, 'b': 0.125}} shots=306 ``` In Qmod's Python embedding, the size of `MyProblem` is given by `MyProblem.num_qubits`: [comment]: DO_NOT_TEST ```python theme={null} print(MyProblem.num_qubits) # 5 ``` # Quantum Variables Source: https://docs.classiq.io/qmod-reference/language-reference/quantum-variables A model operates on quantum objects, by modifying their states using different kinds of operations. Quantum objects represent values that are stored on one or more qubits. The simplest quantum object is a single qubit, representing the values 0 or 1 when measured. Other types of quantum objects are stored on multiple qubits and represent numeric values or arrays of qubits. Quantum objects are managed in Qmod using quantum variables. Variables are introduced into the scope of a quantum function through the declaration of arguments or the declaration of local variables. A quantum variable establishes its reference to some object by explicitly initializing it. This is often done by passing it as the output argument of a function, such as `allocate()`. Once initialized, the state of the object it references can be modified, but the variable's reference itself is immutable. A quantum variable is declared as a function argument using a Python class as a type hint. The same Python class is instantiated to declare a local variable, in which case the name of the variable is optionally specified as a constructor argument and otherwise inferred automatically. ```python theme={null} from classiq import * @qfunc def main(q1: Output[QBit]): q2 = QBit() # The variable name can be set explicitly: QBit("q") allocate(q1) allocate(q2) CX(q1, q2) ``` ``` qfunc main(output q1: qbit) { q2: qbit; allocate(q1); allocate(q2); CX(q1, q2); } ``` ## Managing Quantum Variables Here are the rules for managing quantum variables: * Local variables and output-only arguments (arguments declared with the `output` modifier) are uninitialized upon declaration. * Quantum arguments declared without a modifier or with the `input` modifier are guaranteed to be initialized. * A variable is initialized in one of the following ways: * It is passed as the output-only argument of a function * It is used as the left-value of an assignment * It occurs on the right side of a `->` (bind) statement * Once initialized, a variable can be used as an argument in any number of quantum function calls, as long as it is not an output only or input-only argument (an argument declared with the `output` or `input` modifier). * An initialized variable returns to its uninitialized state in one of the following ways: * It is passed as the input-only argument of a function * It occurs on the left side of a `->` (bind) statement The following diagram illustrates these rules: ```mermaid theme={null} flowchart LR StartUninit["local declaration output declaration"] --- Uninit Uninit((Uninitialized)) -- "allocate output-arg bind-RHS assign-LHS" --> Init Init((Initialized)) -- "free input-arg bind-LHS" --> Uninit Init -- arg --> Init Init --- StartInit["arg declaration input declaration"] ``` In the next example, the local variable `a` must be initialized prior to applying `X()` on it, since it is declared as an output-only argument of function `main`. Similarly, the local variable `b` is uninitialized upon declaration, and subsequently initialized through the call to `prepare_state`, to which it is passed as an output-only argument. Note that, since `b` is a local variable, it cannot be left initialized at the end of the function, but it cannot be automatically uncomputed because `prepare_state` initializes it in a superposition state. Therefore, it has to be explicitly dropped. ```python theme={null} from classiq import * @qfunc def main(a: Output[QBit]): allocate(a) X(a) b = QArray() prepare_state(probabilities=[0.25, 0.25, 0.25, 0.25], bound=0.01, out=b) drop(b) ``` ``` qfunc main(output a: qbit) { allocate(a); X(a); b: qbit[]; prepare_state([0.25, 0.25, 0.25, 0.25], 0.01, b); drop(b); } ``` ## Allocate The *allocate* statement is used to initialize quantum variables, allocating a sequence of qubits to store the new quantum object. The number of qubits allocated, and the numeric type attributes in the case of a numeric variable, are either explicitly specified, or derived from the variable's type. ### Syntax [comment]: DO_NOT_TEST ```python theme={null} @overload def allocate(out: Output[QVar]) -> None: pass @overload def allocate(num_qubits: Union[int, SymbolicExpr], out: Output[QVar]) -> None: pass @overload def allocate( num_qubits: Union[int, SymbolicExpr], is_signed: Union[bool, SymbolicExpr], fraction_digits: Union[int, SymbolicExpr], out: Output[QVar], ) -> None: pass ``` **allocate** **(** \[ *size-int-expr* **,** \[ *sign-bool-expr* **,** *frac-digits-int-expr* **,** ] ] *var* **)** It is recommended to use the `SIGNED` and `UNSIGNED` built-in constants instead of `True` and `False` respectively when specifying the *sign-bool-expr*. ### Semantics * Prior to an *allocate* statement *var* must be uninitialized, and subsequently it becomes initialized. * The *size-int-expr*, if specified, must agree with the declared size of the variable. If the variable declaration does not determine the size, the type of the variable is inferred to accommodate the specified size. See more under [Quantum Types](https://docs.classiq.io/latest/qmod-reference/language-reference/quantum-types/). * For variables of type `qnum`, *sign-bool-expr* and *frac-digits-int-expr*, if specified, must agree with the quantum type of the variable. Here too, if the variable declaration does not determine these numeric properties, they are inferred per the specified values. ### Example The following example demonstrates three uses of `allocate` on two local variables and one output parameter of `main`. Note how the overall size of the quantum object is used in the inference of its type. ```python theme={null} from classiq import * @qfunc def main( qnarr: Output[QArray[QNum, 2]], # quantum array of two numbers qn: Output[QNum], # quantum number with unspecified size qb: Output[QBit], ): allocate(qb) # allocates a single qubit allocate(3, SIGNED, 0, qn) # allocates a 3 bit signed integer (ranging in [-4, 3]) allocate(6, qnarr) # allocates an array of 2 elements, each with size 3 hadamard_transform([qb, qn, qnarr]) ``` ``` qfunc main( output qnarr: qnum[2], // quantum array of two numbers output qn: qnum, // quantum number with unspecified size output qb: qbit) { allocate(qb); // allocates a single qubit allocate(3, SIGNED, 0, qn); // allocates a 3 bit signed integer (ranging in [-4, 3]) allocate(6, qnarr); // allocates an array of 3 elements, each with size 2 hadamard_transform({qb, qn, qnarr}); } ``` ## Free The *free* statement is used to declare that a quantum variable is back to its initial $|0\rangle$ state, and no longer used. Subsequently, the variable becomes uninitialized and its qubits can are reclaimed by the compiler for subsequent use. ### Syntax [comment]: DO_NOT_TEST ```python theme={null} def free(in_: Input[QArray[QBit]]) -> None: pass ``` **free** **(** *var* **)** ### Semantics * Prior to a *free* statement *var* must be initialized, and subsequently it becomes uninitialized. * The quantum object referenced by *var* must be in the $|0\rangle$ state when it is freed. This property is not enforced by the compiler. * Local variables that are explicitly freed are not considered uncomputation candidates, and are not restricted to permutable use contexts. See more under [Uncomputation](https://docs.classiq.io/latest/qmod-reference/language-reference/uncomputation/). It is the programmer's responsibility to apply `free` only to quantum variables that are known to be in the $|0\rangle$ state. Failing to do so may lead to undefined behavior. ### Example Explicitly freeing a variable is typically not needed and is only used for specific purposes. The following example demonstrates the use of `free` in a phase-kickback pattern. It is used to release an auxiliary qubit that is known to be returned to state $|0\rangle$, despite having applied the Hadamard gate to it. ```python theme={null} from classiq import * @qfunc def flip_phase(val: CInt, state: Const[QNum]): aux = QBit() allocate(aux) within_apply( lambda: (X(aux), H(aux)), lambda: control(state == val, lambda: X(aux)), ) free(aux) # aux is known to be in the |0> state ``` ``` qfunc flip_phase(val: int, const state: qnum) { aux: qbit; allocate(aux); within { X(aux); H(aux); } apply { control (state == val) { X(aux); } } free(aux); // aux is known to be in the |0> state } ``` Note that an alternative approach to implementing a phase-kickback pattern, which does not require the use of `free`, is to encapsulate the calls to `H` in a function with an as unchecked *const* parameter. ## Drop The *drop* statement is used to declare that a quantum variable is no longer used and should be exluded from any future uncomputation. Subsequently, the variable becomes uninitialized, while its qubits retain their current state — which may be dirty and entangled with functional qubits — and cannot be reused. ### Syntax [comment]: DO_NOT_TEST ```python theme={null} def drop(in_: Input[QArray[QBit]]) -> None: pass ``` **drop** **(** *var* **)** ### Semantics * Prior to a *drop* statement *var* must be initialized, and subsequently it becomes uninitialized. * Local variables that are explicitly dropped are not considered uncomputation candidates, and are not restricted to permutable use contexts. See more under [Uncomputation](https://docs.classiq.io/latest/qmod-reference/language-reference/uncomputation/). ### Example Explicitly dropping a variable is typically not needed and is only used for specific purposes. The following example demonstrates the use of `drop` in a swap test algorithm, where the two quantum states cannot be uncomputed, yet we do not wish to measure them. ```python theme={null} from classiq import * @qfunc def main(test: Output[QBit]): state1 = QArray() state2 = QArray() prepare_state([0.1, 0.5, 0.3, 0.1], 0.0, state1) prepare_state([0.2, 0.1, 0.4, 0.3], 0.0, state2) swap_test(state1, state2, test) drop(state1) drop(state2) ``` ``` qfunc main(output test: qbit) { state1: qbit[]; state2: qbit[]; prepare_state([0.1, 0.5, 0.3, 0.1], 0.0, state1); prepare_state([0.2, 0.1, 0.4, 0.3], 0.0, state2); swap_test(state1, state2, test); drop(state1); drop(state2); } ``` ## Concatenation Operator The *concatenation operator* is used to combine a sequence of quantum objects (or their parts) into a [quantum array](https://docs.classiq.io/latest/qmod-reference/language-reference/quantum-types/#quantum-arrays). A concatenation is a Python list containing quantum objects. **\[** *path-expressions* **]** *path-expressions* is a comma-separated sequence of one or more quantum [path expressions](http://docs.classiq.io/latest/qmod-reference/language-reference/expressions). **\{** *path-expressions* **}** *path-expressions* is a comma-separated sequence of one or more quantum [path expressions](http://docs.classiq.io/latest/qmod-reference/language-reference/expressions). For example, the model below uses the concatenation operator to apply `hadamard_transform` to a specific set of qubits drawn from two quantum variables: ```python theme={null} from classiq import * @qfunc def main(v1: Output[QArray[QBit, 4]], v2: Output[QArray[QBit, 4]], v3: Output[QBit]): allocate(v1) allocate(v2) allocate(v3) hadamard_transform([v1[3], v3, v2[1:3], v1[0]]) ``` ``` qfunc main(output v1: qbit[4], output v2: qbit[4], output v3: qbit) { allocate(v1); allocate(v2); allocate(v3); hadamard_transform({v1[3], v3, v2[1:3], v1[0]}); } ``` This model allocates three quantum objects: quantum arrays `v1` and `v2` and a qubit `v3`. The model uses a concatenation operator to create a quantum array and apply `hadamard_transform` to it. The quantum array comprises the last bit of `v1`, the entirety of `v3`, the middle two qubits of `v2`, and the first qubit of `v1`. # Assignment Source: https://docs.classiq.io/qmod-reference/language-reference/statements/assignment ## Numeric Assignment Scalar quantum variables (`qnum` and `qbit`) can be assigned the result of arithmetic/logical over other scalar variables using computational basis arithmetic. Expressions comprise conventional arithmetic operators, numeric constants, and quantum scalar variables. Numeric assignment statements in the computational basis take two forms - out-of-place and in-place. When assigning the result of an expression out-of-place, a new quantum object is allocated to store the result. For in-place assignment, the result of the operation is stored back in the target variable. ### Syntax *target-var* **|=** *quantum-expression*
OR
**assign(***quantum-expression***,** *target-var*\*\*)\*\* *target-var* **^=** *quantum-expression*
OR
**inplace\_xor(***quantum-expression***,** *target-var*\*\*)\*\* *target-var* **+=** *quantum-expression*
OR
**inplace\_add(***quantum-expression***,** *target-var*\*\*)\*\* #### Notes * The operator `|=` is used to represent the native `=` since the operator `=` cannot be overloaded in Python. * The operator syntax and the function call syntax are equivalent. The operator syntax is typically easier to read, but it cannot be used directly in lambda expressions, where the function call syntax should be used.
*target-var* **=** *quantum-expression* *target-var* **^=** *quantum-expression* *target-var* **+=** *quantum-expression*
### Semantics * *quantum-expression* consists of quantum scalar variables, numeric constant literals, and classical scalar variables, composed using arithmetic operators. See below the set of supported operators. * The quantum variables occurring in the expression can subsequently be used, with their states unmodified. #### Out-of-place assignment (`=`/`|=`) * *target-var* must be uninitialized prior to the assignment and is subsequently initialized. * The size and numeric attributes of *target-var* are computed to tightly fit the range of possible result values of *quantum-expression*, based on variable sizes, constants, and operators. * The numeric attributes of *target-var* must be left unspecified in the declaration or otherwise be compatible with the computed numeric attributes of *quantum-expression*, that is, fit the entire range of possible expression values. #### In-place XOR (`^=`) * *target-var* must be initialized prior to the assignment. * Each bit in *target-var* is xor-ed with the respective bit in the result of *quantum-expression* if any, or otherwise left unchanged. * Bits in the result of *quantum-expression* with no counterpart in *target-var* are ignored. #### In-place add (`+=`) * *target-var* must be initialized prior to the assignment. * The result of *quantum-expression* is added to the numeric value of *target-var* according to the method. * Superfluous fraction digits in *quantum-expression* are ignored. Superfluous fraction digits in *target-var* remain untouched. * When *target-var* overflows or underflows, its value is wrapped-around the integer part (including the sign bit) without incurring additional qubits, following the two's complement method. ## Aggregate Type Assignment A struct or array quantum variable can be assigned to another variable of the same type. In addition, an array literal can be assigned to a `QArray[QBit]` variable. ### Syntax *target-var* **|=** *assigned-var*
OR
**assign(***assigned-var***,** *target-var*\*\*)\*\* *target-var* **^=** *assigned-var*
OR
**inplace\_xor(***assigned-var***,** *target-var*\*\*)\*\* *array-var* **|=** *array-literal*
OR
**assign(***array-literal***,** *array-var*\*\*)\*\* *target-var* **^=** *array-literal*
OR
**inplace\_xor(***array-literal***,** *array-var*\*\*)\*\* #### Notes * The operator `|=` is used to represent the native `=` since the operator `=` cannot be overloaded in Python. * The operator syntax and the function call syntax are equivalent. The operator syntax is typically easier to read, but it cannot be used directly in lambda expressions, where the function call syntax should be used.
*target-var* **=** *assigned-var* *target-var* **^=** *assigned-var* *array-var* **=** *array-literal* *array-var* **^=** *array-literal*
### Semantics * As with numeric assignments, *target-var* must be uninitialized in out-of-place assignments and initialized in in-place assignments. *assigned-var* must be initialized in both cases. * *target-var* and *assigned-var* must have the same type. For example, variable of type `QArray[QBit]` can be assigned into a `QArray[QBit]` variable, but a `QArray[QNum]` variable cannot. * *array-literal* is a classical array of 0-s and 1-s. *array-var* must be a quantum variable of type `QArray[QBit]`. ## Examples ### Example 1: Out-of-place assignment The following is a model that computes the result of the expression `a + 2 * b + 3`, with `a` initialized to 3 and `b` initialized to a superposition of 1 and 2. The output is a superposition of 8 and 10. ```python theme={null} from classiq import * @qfunc def main(a: Output[QNum], b: Output[QNum], res: Output[QNum]): a |= 3 prepare_state([0, 0.5, 0.5, 0], 0, b) # 'b' is in superposition of 1 and 2 res |= a + 2 * b + 3 # 'res' is in superposition of 8 and 10 ``` ``` qfunc main(output a: qnum, output b: qnum, output res: qnum) { a = 3; prepare_state([0, 0.5, 0.5, 0], 0, b); // 'b' is in superposition of 1 and 2 res = a + 2 * b + 3; // 'res' is in superposition of 8 and 10 } ``` Note that the output size is 4 qubits, since the maximum value of this expression is 15, given that `a` and `b` are two-qubit variables. Any other size declared for `res` will result in an error. ### Example 2: In-place XOR assignment In the next example, the relational expression `a + 2 * b + 3 == 8` is computed, with `a` initialized to 3 and `b` initialized to 1. Calling function `foo` will flip the single variable `res`, because the expression evaluates to 1, that is, true. ```python theme={null} from classiq import QNum, qfunc, QBit @qfunc def foo(res: QBit): a = QNum() b = QNum() a |= 3 b |= 1 res ^= a + 2 * b + 3 == 8 # expression is true so 'res' is flipped ``` ``` qfunc foo(res: qbit) { a: qnum; b: qnum; a = 3; b = 1; res ^= (a + 2 * b + 3 == 8); // expression is true so 'res' is flipped } ``` ### Example 3: In-place assignment of a logical expression In the example below, function `my_oracle` serves as a quantum oracle that marks all states satisfying the logical expression `(x0 and x1) or (x2 and x3)` with a minus phase. ```python theme={null} from classiq import QBit, qfunc, allocate, X, H, within_apply from classiq.qmod.symbolic import logical_or, logical_and @qfunc def my_oracle(x0: QBit, x1: QBit, x2: QBit, x3: QBit): aux = QBit() allocate(aux) def assignment_stmt(var: QBit): var ^= logical_or(logical_and(x0, x1), logical_and(x2, x3)) within_apply(lambda: (X(aux), H(aux)), lambda: assignment_stmt(aux)) ``` Note that in Python, assignment statements are not allowed directly as lambda expressions. Therefore, in this example the `^=` is factored out to an inner Python function. ``` qfunc my_oracle(x0: qbit, x1: qbit, x2: qbit, x3: qbit) { aux: qbit; allocate(aux); within { X(aux); H(aux); } apply { aux ^= (x0 and x1) or (x2 and x3); } } ``` ### Example 4: In-place add assignment The following model initializes two quantum numeric variables `n` and `m`. ```python theme={null} from classiq import * @qfunc def main(m: Output[QNum[3, SIGNED, 2]], n: Output[QNum[3, SIGNED, 1]]): allocate(m) apply_to_all(X, m) allocate(n) n += m ``` ``` qfunc main(output m: qnum<3, SIGNED, 2>, output n: qnum<3, SIGNED, 1>) { allocate(m); apply_to_all(X, m); allocate(n); n += m; } ``` Variable `m` has three qubits, of which one is a sign qubit and two are fraction digits. By applying `X` (not) to `m`'s qubit, we set its value to `-0.25`. When adding `m` to `n` (`n += m`), the variables do not align since variable `n` has one less fraction digit than `m`: ``` n = 00.0 m = 1.11 ``` First, we ignore the last fraction digit of `m`: ``` n = 00.0 m = 1.1 ``` Then, we extend `m` to the size of `n` (3) by duplicating the sign bit in accordance with the two's complement method: ``` n = 00.0 m = 11.1 ``` Finally, adding `m` to `n` sets `n`'s state to `111`, whose interpretation is the numeric value `-0.5`. ### Example 5: Overflowing in-place add assignment The following model demonstrates what happens to the target variable when its value overflows, i.e., extends beyond the variable domain. ```python theme={null} from classiq import * @qfunc def main(n: Output[QNum[3, UNSIGNED, 1]], m: Output[QNum[3, SIGNED, 1]]): allocate(n) # n = 0 n += 3.5 # n = 3.5 n += 1 # n = 0.5, n still has 3 qubits allocate(m) # m = 0 m += 1.5 # m = 1.5 m += 1 # m = -1.5, m still has 3 qubits ``` ``` qfunc main(output n: qnum<3, UNSIGNED, 1>, output m: qnum<3, SIGNED, 1>) { allocate(n); // n = 0 n += 3.5; // n = 3.5 n += 1; // n = 0.5, n still has 3 qubits allocate(m); // m = 0 m += 1.5; // m = 1.5 m += 1; // m = -1.5, m still has 3 qubits } ``` ### Example 6: Quantum subscript expression The following model demonstrate quantum subscript expression over a classical list `[7, 3, 6, 2]` and a quantum variable `index`. The quantum `index` is in superposition over the indices 0 (10%), 1 (20%), 2 (30%), and 3 (40%). The quantum subscript expression `[7, 3, 6, 2][index]` is in superposition over the items `[7, 3, 6, 2]` entangled to `index`. Overall, the output variable `n` evaluates to 7 (10%), 3 (20%), 6 (30%), or 2 (40%). ```python theme={null} from classiq import * from classiq.qmod.symbolic import subscript @qfunc def main(n: Output[QNum]): index = QNum() prepare_state([0.1, 0.2, 0.3, 0.4], 0, index) n |= subscript( [7, 3, 6, 2], index ) # n is 7, 3, 6, or 2 with increasing probability drop(index) ``` ``` qfunc main(output n: qnum) { index: qnum; prepare_state([0.1, 0.2, 0.3, 0.4], 0, index); n = [7, 3, 6, 2][index]; drop(index); } ``` ### Example 7: Aggregate type assignments The following model demonstrate different kinds of quantum array assignments. First, you assign the array literal `[0, 1, 1, 0]` into variable `qarr1` of type `QArray[QBit]`. This applies `X` to the second and third bits of the array. Next, you initialize `qarr2` by assigning `qarr1` to it. This applies `CX` to the qubits of `qarr1` and `qarr2` sequentially. ```python theme={null} from classiq import qfunc, Output, QArray, QBit, allocate @qfunc def main(qarr1: Output[QArray], qarr2: Output[QArray]): allocate(4, qarr1) qarr1 ^= [0, 1, 1, 0] qarr2 |= qarr1 ``` ``` qfunc main(output qarr1: qbit[], output qarr2: qbit[]) { allocate(4, qarr1); qarr1 ^= [0, 1, 1, 0]; qarr2 = qarr1; } ``` # Bind Source: https://docs.classiq.io/qmod-reference/language-reference/statements/bind The *bind* statement (operator `->`) is used to rewire the qubits referenced by one or more source variables to one or more destination variables. In accordance with the no-cloning principle, the source variables, which are initialized prior to the bind statement, become uninitialized subsequently. You can use the `bind` statement to split one quantum object into multiple objects and to join multiple objects into one. You can also use it to reinterpret a numeric object as a qubit array and vice versa. ## Syntax [comment]: DO_NOT_TEST ```python theme={null} def bind( source: Union[Input[QVar], List[Input[QVar]]], destination: Union[Output[QVar], List[Output[QVar]]], ) -> None: pass ``` *source-var-list* **->** *destination-var-list* *source-var-list* and *destination-var-list* are either a single quantum variable or a list of one or more comma-separated quantum variables enclosed in **\{** **}** ## Semantics * Prior to a `bind` statement variables in *source-var-list* must be initialized and variables in *destination-var-list* must be uninitialized. * Following a `bind` statement variables in *source-var-list* are uninitialized and variables in *destination-var-list* are initialized. * If more than one variable is listed in *destination-var-list*, the overall size in bits of each variable must be known. This is required to determine the partition of qubits between them. * `qnum` variables must have a declared or previously inferred size. * `qbit[]` variables must have a declared or previously inferred length. * The sum of sizes associated with variables in *destination-var-list* must agree with the overall number of qubits actually used by variables in *source-var-list*. * Following a `bind` statement qubits are rewired from the source variable(s) to the respective position in the destination variable(s). Note that the `bind` statement is only rewiring qubits across model variables and has no resource footprint (additional gates or auxiliary qubits) on the resulting circuit. ### Qubit layout stability A `bind` statement can change a variable's *qubit layout* -- the physical qubits it occupies and their order. Splitting a variable and rejoining its parts in a different order is a "shuffle": [comment]: DO_NOT_TEST ```python theme={null} bind(x, [a, b]) bind([b, a], x) # reordered -> a "shuffle" ``` A variable may not be shuffled inside a block (`power`, `repeat`, `foreach`, `if`, `control`, `invert`): it must have the same qubit layout when it leaves the block as when it entered. The same holds across a function body. ## Examples ### Example 1: Cast The following example demonstrates how to use the `bind` statement to cast a numeric variable to a qubit array and cast back. In it, all bits of some number are flipped. Accessing qubits cannot be performed directly on a `qnum` variable. Therefore, `x` is bound to a `qbit[]` variable to perform the operation and subsequently bound back. ```python theme={null} from classiq import * @qfunc def main(x: Output[QNum]): x |= 5 qba = QArray() bind(x, qba) repeat(qba.len, lambda i: X(qba[i])) bind(qba, x) ``` ``` qfunc main(output x: qnum) { x = 5; qba: qbit[]; x -> qba; repeat (i: qba.len) { X(qba[i]); } qba -> x; } ``` ### Example 2: Split and join The following example demonstrates how to apply an operation on a specific qubit of a numeric variable. In function `xor_lsb` the LSB (least significant bit) of argument `x` is the target of a `CX` operation. This is done by splitting it from `x` while keeping the rest of the qubits in `msbs` and subsequently joining `x` back. ```python theme={null} from classiq import * @qfunc def xor_lsb(x: QNum, xor_bit: QBit): lsb = QNum("lsb", 1, UNSIGNED, 1) msbs = QArray("msbs", QBit, x.size - 1) bind(x, [lsb, msbs]) CX(xor_bit, lsb) bind([lsb, msbs], x) @qfunc def main(x: Output[QNum]): x |= 5 xor_bit = QBit() allocate(xor_bit) H(xor_bit) xor_lsb(x, xor_bit) drop(xor_bit) ``` ``` qfunc xor_lsb(x: qnum, xor_bit: qbit) { lsb: qnum<1, UNSIGNED, 1>; msbs: qbit[x.size - 1]; x -> {lsb, msbs}; CX(xor_bit, lsb); {lsb, msbs} -> x; } qfunc main(output x: qnum) { x = 5; xor_bit: qbit; allocate(xor_bit); H(xor_bit); xor_lsb(x, xor_bit); drop(xor_bit); } ``` In the overall model, function `main` calls `xor_lsb` with the number 5. Its output is the uniform distribution of 5 and 4, correlative to the `xor_bit` being 0 and 1. Below is the visualization of the resulting quantum program. visualization of the bind example bright visualization of the bind example dark # Classical Control Flow Source: https://docs.classiq.io/qmod-reference/language-reference/statements/classical-control-flow Loops and conditionals on classical expressions are useful means to describe reusable building blocks. Qmod has two basic forms - the *repeat* statement and the *if* statement. ## Classical Repeat ### Syntax [comment]: DO_NOT_TEST ```python theme={null} def repeat(count: CInt, iteration: QCallable[CInt]) -> None: pass ``` **repeat** **(** *iteration\_variable* **:** *count* **)** **\{** *iteration-statements* **}** ### Semantics * Invoke the *iteration* block *count* times, binding the index variable to the respective iteration number - 0, 1,... *count*-1. * Inside the statement block, use of quantum variables declared outside it is restricted to contexts where the variable is initialized and remains initialized (see [Quantum Variables](/qmod-reference/language-reference/quantum-variables)) ### Example The following example defines a useful function - applying the Hadamard function across all qubits in a qubit array - using *repeat*. Note that a similar function is available in the Classiq open-library. ```python theme={null} from classiq import H, QArray, QBit, qfunc, repeat @qfunc def my_hadamard_transform(qba: QArray[QBit]): repeat( count=qba.len, iteration=lambda index: H(qba[index]), ) ``` ``` qfunc my_hadamard_transform(qba: qbit[]) { repeat (index: qba.len) { H(qba[index]); } } ``` ## Classical If ### Syntax [comment]: DO_NOT_TEST ```python theme={null} def if_(condition: CBool, then: QCallable, else_: Optional[QCallable] = None) -> None: pass ``` Note that identifiers in Qmod that happen to conflict with Python keywords have `_` suffix. This is the case with `if_` and `else_` in the second function. **if** **(** condition **)** **\{** *then-statements* **}** else **\{** *else-statements* **}** ### Semantics * Invoke the *then* block if *condition* evaluates to `true` and otherwise invoke the *else* block * Inside the statement block, use of quantum variables declared outside it is restricted to contexts where the variable is initialized and remains initialized (see [Quantum Variables](/qmod-reference/language-reference/quantum-variables)) ### Example ```python theme={null} from classiq import CBool, X, Y, QBit, qfunc, if_ @qfunc def my_conditional_gate(cond: CBool, qb: QBit): if_( condition=cond, then=lambda _: X(qb), else_=lambda _: Y(qb), ) ``` ``` qfunc my_conditional_gate(cond: bool, qb: qbit) { if (cond) { X(qb); } else { Y(qb); } } ``` ## Classical Foreach The *foreach* statement iterates through the elements of a classical array. The use of *foreach* iteration variables is more restrictive than that of the *repeat* and consequently its compilation and execution are more efficient. ### Syntax [comment]: DO_NOT_TEST ```python theme={null} def foreach(values: CArray | list, iteration: Callable) -> None: pass ``` The `iteration` callable accepts one or more iteration variables. **foreach** **(** *iteration\_variables* **:** *values* **)** **\{** *iteration-statements* **}** The *iteration\_variables* are identifiers separated by commas. ### Semantics * Invoke the *iteration* block once for every element of *values*. * *values* must be a value of type `CArray[CReal]` or `CArray[CArray[CReal]]`, and its values must be known at compile-time. * If the *iteration* block accepts a single iteration variable, the elements of *values* will be bound to it sequentially. * In the case that *values* is a nested array (`CArray[CArray[CReal]]`), the number of iteration variables must correspond to the length of the inner array. At each iteration, the inner array is unpacked into the iteration variables, with each variable assigned the corresponding scalar value in the specified order. * The iteration variables are [link-time](/qmod-reference/language-reference/classical-variables#semantics): They can be used, e.g., as angles in rotation gates, but not in quantum expressions or as array indices. * Inside the statement block, use of quantum variables declared outside it is restricted to contexts where the variable is initialized and remains initialized (see [Quantum Variables](/qmod-reference/language-reference/quantum-variables)). ### Examples In the following example, the `foreach` statement iterates through the elements of the classical list `[0.1, 0.2]` and assigns its elements into the iteration variable `i`. This is equivalent to calling `RX` and `Y` twice, once with angle `0.1` and once with `0.2`. ```python theme={null} from classiq import * @qfunc def main(q: Output[QBit]) -> None: allocate(q) foreach( [0.1, 0.2], lambda i: [ RX(i, q), Y(q), ], ) ``` ``` qfunc main(output q: qbit) { allocate(1, q); foreach (i: [0.1, 0.2]) { RX(i, q); Y(q); } } ``` In the following example, the `foreach` statement iterates through the elements of the classical list `[[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]]` and assigns its elements into the iteration variables `i` and `j`. In each iteration, the elements of the nested arrays are "unpacked" into the iteration variables. The first iteration assigns `0.1` to `i` and `0.2` into `j`, the second iteration assigns `0.3` into `i` and `0.4` to `j`, and the third iteration assigns `0.5` into `i` and `0.6` to `j`. ```python theme={null} from classiq import * @qfunc def main(q: Output[QBit]) -> None: allocate(q) foreach( [[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]], lambda i, j: [ RX(i, q), RY(j, q), ], ) ``` ``` qfunc main(output q: qbit) { allocate(1, q); foreach (i, j: [ [0.1, 0.2], [0.3, 0.4], [0.5, 0.6] ]) { RX(i, q); RY(j, q); } } ``` # Control Source: https://docs.classiq.io/qmod-reference/language-reference/statements/control The *control* statement applies a unitary operation conditionally, depending on a quantum state, and optionally a different one if the condition doesn't hold. The unitary operations are specified as nested statement blocks. The objects used in the statement blocks become entangled with the object used in the condition, so that any superposition in the state of the condition carries over to the operations in the statement blocks. The control statement could be viewed as the quantum equivalent of the classical *if* statement, with a *then* block and an optional *else* block. The condition can be specified in one of two forms: as a single quantum variable, and as a quantum logical expression. ## Syntax [comment]: DO_NOT_TEST ```python theme={null} def control( ctrl: Union[SymbolicExpr, QBit, QArray[QBit]], stmt_block: Union[QCallable, Callable[[], None]], else_block: Union[QCallable, Callable[[], None], None] = None, ) -> None: pass ``` **control** **(** *ctrl-var* **)** **\{** *statements* **}** \[**else** **\{** *else-statements* **}**] **control** **(** *ctrl-expression* **)** **\{** *statements* **}** \[**else** **\{** *else-statements* **}**] ## Semantics * *ctrl-var* (in the first variant) is a quantum variable of type `qbit` or `qbit[]`, and *ctrl-expression* (in the second variant) is a logical expression over a quantum variable. * The statement block is applied if all the qubits in *ctrl* are in state $|1\rangle$ (in the first variant), or if the *ctrl-expression* evaluates to `true` (in the second variant). * else-statements block is optional, and is applied if the negation of the condition holds, that is, if at least one of the qubits in ctrl is in state $|0\rangle$ (in the first variant), or if the ctrl-expression evaluates to False (in the second variant). * Currently, there exists a single limitation on the expression: if *ctrl-expression* is an equality between a single variable and a classical expression, it is restricted to integers, meaning: In a *ctrl-expression* of the form ` == `, `` should be a `qnum` with zero fraction places or a `qbit`, and `` should evaluate to an integer. * The same variable cannot occur both in the condition and inside the statement block. This restriction applies also to mutually exclusive slices of the same variable. * Quantum variables declared outside the *control* statement and used in its condition or inside its nested block must be initialized prior to it and remain initialized subsequently. * Quantum variables declared inside the *control* statement, including in nested function calls, must be uninitialized at the end of the statement. ## Examples ### Example 1: Single-qubit control In the following example, `control` statement applies function `X` on the variable `target` conditioned on a single qubit variable `qb`. Note that this is equivalent to using the built-in gate-level function `CX`. ```python theme={null} from classiq import * @qfunc def main(target: Output[QBit], ctrl: Output[QBit]): allocate(ctrl) H(ctrl) allocate(target) control(ctrl, lambda: X(target)) ``` ``` qfunc main(output target: qbit, output ctrl: qbit) { allocate(ctrl); H(ctrl); allocate(target); control (ctrl) { X(target); } } ``` Synthesizing this model creates the quantum program shown below. visualization of the single-qubit control example bright visualization of the single-qubit control example dark ### Example 2: Multi-qubit control The next example shows how `control` can be similarly used with multi-qubit control variable. In this case `target` is rotated when the state of `qba` is the bit string 111. ```python theme={null} from classiq.qmod.symbolic import pi from classiq import * @qfunc def main(target: Output[QBit], ctrl: Output[QArray[QBit]]): allocate(3, ctrl) hadamard_transform(ctrl) allocate(target) control(ctrl, lambda: RX(pi / 2, target)) ``` ``` qfunc main(output target: qbit, output ctrl: qbit[]) { allocate(3, ctrl); hadamard_transform(ctrl); allocate(target); control (ctrl) { RX(pi / 2, target); } } ``` Synthesizing this model creates the quantum program shown below. visualization of the multi-qubit control example bright visualization of the multi-qubit control example dark ### Example 3: Numeric equality condition The following example demonstrates the use of `control` to rotate the state of a qubit by an angle determined by another quantum variable. In this case the condition compares the quantum variable with the repeat index. ```python theme={null} from classiq import * from classiq.qmod.symbolic import pi @qfunc def switch_rx(x: QNum, target: QBit): repeat(4, lambda i: control(x == i, lambda: RX(pi / 2**i, target))) @qfunc def main(res: Output[QBit]): allocate(res) x = QNum() allocate(2, x) hadamard_transform(x) switch_rx(x, res) drop(x) ``` ``` qfunc switch_rx(x: qnum, target: qbit) { repeat (i: 4) { control (x == i) { RX(pi / (2 ** i), target); } } } qfunc main(output res: qbit) { allocate(res); x: qnum; allocate(2, x); hadamard_transform(x); switch_rx(x, res); drop(x); } ``` Synthesizing this model creates the following quantum program. Note how `control` is implemented as positive and negative controls in the respective numeric qubits. visualization of the control by numeric value example bright visualization of the control by numeric value example dark ### Example 4: Arithmetic condition The following example demonstrates the use of `control` to rotate the state of a qubit according to a condition imposed by another two quantum variables, `x` and `y`. Here, the condition filters quantum states such that `y <= x[0] + x[1] + x[2]`. ```python theme={null} from classiq import * from classiq.qmod.symbolic import pi @qfunc def conditional_rx(x: QArray[QBit], y: QNum, target: QBit): control(x[0] + x[1] + x[2] >= y, lambda: RX(pi / 3, target)) @qfunc def main(res: Output[QBit]): allocate(res) x: QArray = QArray("x", QBit, 3) y: QNum = QNum("y", 3, UNSIGNED, 0) allocate(x) allocate(y) hadamard_transform(x) hadamard_transform(y) conditional_rx(x, y, res) drop(x) drop(y) ``` ``` qfunc conditional_rx(x: qbit[], y: qnum, target: qbit) { control (y <= ((x[0] + x[1]) + x[2])) { RX(pi / 3, target); } } qfunc main(output res: qbit) { allocate(res); x: qbit[3]; y: qnum<3, UNSIGNED, 0>; allocate(x); allocate(y); hadamard_transform(x); hadamard_transform(y); conditional_rx(x, y, res); drop(x); drop(y); } ``` Synthesizing this model creates the following quantum program. Note how `control` is implemented as a result of an arithmetic computation. After applying the `control` operation, the arithmetic operation is uncomputed. visualization of the arithmetic condition example bright visualization of the arithmetic condition example dark ### Example 5: Control else The following example demonstrates the use of the `else` block in the `control` statement. In this case, the `else` block applies an 'H' gate on the target qubit when the control condition is not met, instead of the 'X' gate, so when each qubit of the control variable is in state $|1\rangle$, the bit is flipped, otherwise, the Hadamard gate is applied. ```python theme={null} from classiq import * @qfunc def main(x: Output[QBit], ctrl: Output[QArray[QBit]]): allocate(2, ctrl) hadamard_transform(ctrl) allocate(x) control(ctrl, lambda: X(x), lambda: H(x)) ``` ``` qfunc main(output x: qbit, output ctrl: qbit[]) { allocate(2, ctrl); hadamard_transform(ctrl); allocate(x); control (ctrl) { X(x); } else { H(x); } } ``` Synthesizing this model creates the following quantum program. Note how the `else` block is implemented as a negation of the control condition, and the negation is uncomputed after the `control` operation of the else block. visualization of the control else example bright visualization of the control else example dark # Invert Source: https://docs.classiq.io/qmod-reference/language-reference/statements/invert The *invert* statement applies the adjoint (conjugate transpose) of the unitary operation specified as a nested statement block. If the nested block specifies the unitary operation $U$, *invert* applies $U^{\dagger}$. ## Syntax [comment]: DO_NOT_TEST ```python theme={null} def invert(stmt_block: QCallable) -> None: pass ``` **invert** **\{** *statements* **}** ## Semantics * The *invert* statement applies the adjoint of the operations specified in the nested block, equivalent to the adjoint of each nested statement in reverse order. * Quantum variables declared outside the *invert* statement and used inside its nested block must be initialized prior to it and remain initialized subsequently. * Quantum variables declared inside the *invert* statement, including in nested function calls, must be uninitialized at the end of the statement. ## Example The following example demonstrates the use of `invert` applied to a single gate-level function call, and to a statement block in which a user-defined function `foo` is called twice. ```python theme={null} from classiq.qmod.symbolic import pi from classiq import * @qfunc def foo(target: QBit): H(target) X(target) @qfunc def main(qba: Output[QArray[QBit]]): allocate(2, qba) invert(lambda: RX(pi / 2, qba[0])) invert(lambda: [foo(qba[0]), foo(qba[1])]) ``` ``` qfunc foo(target: qbit) { H(target); X(target); } qfunc main(output qba: qbit[]) { allocate(2, qba); invert { RX(pi / 2, qba[0]); } invert { foo(qba[0]); foo(qba[1]); } } ``` Synthesizing this model creates the quantum program shown below. The inversion of `RX` is simply negating the rotation angle. In the second `invert` statement each call to `foo` is inverted, applying the gate-level functions in reverse but unchanged (as they are hermitian). visualization of the invert example bright visualization of the invert example dark # Phase Source: https://docs.classiq.io/qmod-reference/language-reference/statements/phase The *phase* statement is used to compute and encode the result of some arithmetic computation in the phase of the respective quantum states. It applies a relative phase to computational-basis states of quantum variables proportional to the value of a specified expression over these variables. The *phase* statement can also specify a fixed rotation angle, i.e., a "global" phase, using an expression with no quantum variables. When a fixed phase rotation occurs under a controlled context, it affects only the controlling states. Otherwise, a fixed *phase* statement is undetectable. *phase* statements with a quantum expression are often used to compute the cost of an optimization problem. Applying a fixed *phase* is useful in expressing phase oracles and reflections. ## Syntax [comment]: DO_NOT_TEST ```python theme={null} def phase(phase_expr: SymbolicExpr, coefficient: float = 1.0) -> None: pass ``` **phase** **(** *phase-expression* \[ **,** *coefficient* ] **)** ## Semantics * *phase-expression* may consist of quantum scalar variables, numeric constant literals, and classical scalar variables, composed using arithmetic operators. See below the set of supported operators. * The *coefficient* expression is optional, and may include an execution parameter. Note that an execution parameter cannot occur in *phase-expression* if it contains quantum variables. If not provided, the default coefficient value is 1.0. * The operation rotates each computational basis state about the Z axis by an angle equal to the value of *phase-expression*, multiplied by *coefficient* if specified. * For *phase-expression* over quantum variable $x_1, x_2, \ldots, x_n$ that computes the function $f(x_1, x_2, \ldots, x_n)$ and *coefficient* $=\theta$, the operation performed by the statement is $|x\rangle \rightarrow e^{i\theta f(x_1, x_2, \ldots, x_n)} |x\rangle$. * For *phase-expression* without quantum variables that evaluates to $\theta$, the operation performed by the statement is $|x\rangle \rightarrow e^{i\theta} |x\rangle$. * The expression must be a polynomial in the quantum variables. It is compiled into an Ising-model Hamiltonian, which is evolved per the specified coefficient. The following operators are supported: * Add: `+` * Subtract: `-` (binary) * Negate: `-` (unary) * Multiply: `*` * Divide: `/` (by a classical value) * Power: `**` (quantum base, positive classical integer exponent) The following operators are supported only when applied to `QBit`, `QNum[1]`, and the classical integers `0` and `1`: * Bitwise Or: `|` * Bitwise And: `&` * Bitwise Xor: `^` * Bitwise Not: `~` Note that when the expression consists of a single one-qubit variable, *phase* statement is equivalent to the core-library function `PHASE()`. ## Examples ### Example 1 In the following model phase $x^2$ is applied to variable $x$ with the coefficient $\frac{\pi}{4}$. $x$ is initialized to a superposition of the values 0, 1, 2, and 3. After the *phase* statement, state 1 is in phase $\frac{\pi}{4}$ relative to state 0, state 2 is rotated $\pi$ relative to state 0. State 3 is rotated $\frac{\pi}{4}$, which is a full $2\pi$ + $\frac{\pi}{4}$ rotation, that is, the same phase as state 1. ```python theme={null} from classiq import * from classiq.qmod.symbolic import pi @qfunc def main(x: Output[QNum]): allocate(2, x) hadamard_transform(x) phase(x**2, pi / 4) ``` ``` qfunc main(output x: qnum) { allocate(2, x); hadamard_transform(x); phase (x**2, pi/4); } ``` Visualizing the synthesized quantum program, you can see how Z-rotations and controlled Z-rotations are used to achieve the required rotation. visualization of the phase example bright visualization of the phase example dark When executing this model using a state-vector simulator, the relative phases of the different states can be observed. In simple_phase_exe_result.png ### Example 2 The following example demonstrates the use of *phase* statement to encode the cost of a max-cut problem, as the implementation of the cost-layer in a QAOA ansatz. The qubit array `v` represents the partition of the set of vertices in a graph into two, and the expression inside the *phase* statement computes the number of edges that cross the partition. Executing this model with the right set of parameter values will yield with high probability an optimal solution. ```python theme={null} from classiq import * @qfunc def main( gammas: CArray[CReal, 4], betas: CArray[CReal, 4], v: Output[QArray[QBit, 3]], ): allocate(v) hadamard_transform(v) for i in range(4): phase( (v[0] * (1 - v[1]) + v[1] * (1 - v[0])) # edge 0-1 + (v[0] * (1 - v[2]) + v[2] * (1 - v[0])), # edge 0-2 gammas[i], ) apply_to_all(lambda q: RX(betas[i], q), v) ``` ``` qfunc main(gammas: real[4], betas: real[4], output v: qbit[3]) { allocate(v); hadamard_transform(v); repeat (i: 4) { phase( (v[0] * (1 - v[1]) + v[1] * (1 - v[0])) // edge 0-1 + (v[0] * (1 - v[2]) + v[2] * (1 - v[0])), // edge 0-2 gammas[i] ); apply_to_all(lambda(q) { RX(betas[i], q); }, v); } } ``` ### Example 3 The following model applies `phase` with an angle specified as a classical expression, thus inserting a fixed phase under controlled contexts. In each case, the states that satisfy the control condition rotate by $\frac{\pi}{4}$ relative to those that do not. ```python theme={null} from classiq import * from classiq.qmod.symbolic import pi @qfunc def main(qarr: Output[QArray[QBit, 2]]): allocate(qarr) hadamard_transform(qarr) control(qarr[0], lambda: phase(pi / 4)) control(qarr, lambda: phase(pi / 4)) ``` ``` qfunc main(output qarr: qbit[2]) { allocate(qarr); hadamard_transform(qarr); control (qarr[0]) { phase (pi / 4); } control (qarr) { phase (pi / 4); } } ``` The cumulative result of both statements revealed by running a state-vector simulation is a uniform superposition of the four states with the following phases: $$ \begin{aligned} |[0,0]\rangle&: 0 \\ |[0,1]\rangle&: 0 \\ |[1,0]\rangle&: \frac{\pi}{4} \\ |[1,1]\rangle&: \frac{\pi}{2} \\ \end{aligned} $$ # Power Source: https://docs.classiq.io/qmod-reference/language-reference/statements/power The *power* statement applies the unitary operation raised to some integer power, where the unitary is specified as a nested statement block. ## Syntax [comment]: DO_NOT_TEST ```python theme={null} def power(exponent: CInt, stmt_block: QCallable) -> None: pass ``` **power** **(** *exponent* **)** **\{** *statements* **}** ## Semantics * If the statement block specifies the unitary operation $U$ on some quantum object, *power* applies $U^{exponent}$. * In the general case, the statement block is iterated over *exponent* times, but in some important special cases the operation is implemented more efficiently. * Quantum variables declared outside the *power* statement and used inside its nested block must be initialized prior to it and remain initialized subsequently. * Quantum variables declared inside the *power* statement, including in nested function calls, must be uninitialized at the end of the statement. ## Examples In the following example `power` is applied 3 times - to gate-level functions `H` and `RX`, and to a user-defined function `foo`. It demonstrates both special and general treatment of the *power* operation. ```python theme={null} from classiq import * from classiq.qmod.symbolic import pi @qfunc def foo(p: CInt, q: QBit): power(p, lambda: H(q)) power(p, lambda: PHASE(pi / 8, q)) @qfunc def main(q: Output[QBit]): allocate(q) power(2, lambda: foo(5, q)) ``` ``` qfunc foo(p: int, q: qbit) { power (p) { H(q); } power (p) { PHASE(pi / 8, q); } } qfunc main() { q: qbit; allocate(q); power (2) { foo(5, q); } } ``` Synthesizing this model creates the quantum program shown below. Because two consecutive applications of `H` cancel each other out, raising `H` to the power of 5 is equivalent to applying `H` once. Raising `RX` with rotation angle $\pi / 8$ to the power 5 is equivalent to applying `RX` with rotation angle $5 \times \pi / 8$. However, raising `foo` to the power 2 requires 2 consecutive applications of `foo`. visualization of the power example bright visualization of the power example dark # Skip Control Source: https://docs.classiq.io/qmod-reference/language-reference/statements/skip-control The *skip-control* statement designates a statement block that should be applied unconditionally, even when the enclosing function is subject directly or indirectly to a [quantum control operator](https://docs.classiq.io/latest/qmod-reference/language-reference/statements/control/). This construct is typically applied when the enclosing function involves intermediate computation and uncomputation blocks in ways that cannot be formulated using a simple conjugation (i.e., using the *within-apply* statement). ## Syntax [comment]: DO_NOT_TEST ```python theme={null} def skip_control(stmt_block: QCallable) -> None: pass ``` **skip\_control** **\{** *statements* **}** ## Semantics * All statements nested under a *skip-control* statement are applied unconditionally, including in states where a control condition from above is evaluated to `False`. It is the user's responsibility to ensure that the combined effect of the *skip-control* blocks in the enclosing function is functionally equivalent to NOP (identity). * Using *skip-control* inside the *within* block of a *within-apply* statement is redundant, since the *within* block is itself designated as a *skip-control* block. * Using *skip-control* directly inside the *apply* block of a *within-apply* statement is not allowed. ## Example The following example demonstrates the use of `skip_control`. Function `foo` rotates a qubit in steps, applying a controlled flip of other qubits at each step. The controlling qubit ultimately returns to its original state. The rotations are marked with `skip_control` because they can be safely applied even for states where the whole function is under a negative control condition, as demonstrated by function `main`. Note that the computation of `qarr[0]` within `foo` cannot be encapsulated in an *apply* block of a *within-apply* statement. ```python theme={null} from classiq import * from classiq.qmod.symbolic import pi @qfunc def foo(qarr: QArray[QBit, 5]): repeat( 4, lambda i: [ CX(qarr[0], qarr[i + 1]), skip_control(lambda: RX(pi / 2, qarr[0])), ], ) @qfunc def main(ctrl: Output[QBit], qarr: Output[QArray[QBit, 5]]): allocate(qarr) hadamard_transform(qarr[0]) allocate(ctrl) hadamard_transform(ctrl) control(ctrl, lambda: foo(qarr)) ``` ``` qfunc foo(qarr: qbit[5]) { repeat (i: 4) { CX(qarr[0], qarr[i+1]); skip_control { RX(pi/2, qarr[0]); } } } qfunc main(output ctrl: qbit, output qarr: qbit[5]) { allocate(qarr); hadamard_transform(qarr[0]); allocate(ctrl); hadamard_transform(ctrl); control(ctrl) { foo(qarr); } } ``` # Within-apply Source: https://docs.classiq.io/qmod-reference/language-reference/statements/within-apply The *within-apply* statement performs the common quantum pattern $U^{\dagger} V U$. It operates on two nested statement blocks, the *within* block and the *apply* blocks, and evaluates the sequence - *within*, *apply*, and *invert(within)*. Under conditions described below, quantum objects that are allocated and prepared by the *within* block are subsequently uncomputed and released. ## Syntax [comment]: DO_NOT_TEST ```python theme={null} def within_apply(within: Callable, apply: Callable) -> None: pass ``` **within** **\{** *within-statements* **}** **apply** **\{** *apply-statements* **}** ## Semantics * Unlike the case with other statements, the nested blocks of *within-apply* may initialize outer context variables. * Variables that are initialized inside the *within* block of a *within-apply* statement, are returned to their uninitialized state after the statement completes. * All quantum objects allocated directly or indirectly under the *within* block are uncomputed, and their qubits are reclaimed for subsequent use after the statement completes. * In addition to the general restriction of local variables to *permutable* use contexts, variables initialized inside the *within* block and their dependents can only be used in *const* contexts inside the *apply* block. See more on uncomputation rules under [Uncomputation](/qmod-reference/language-reference/uncomputation). * The application of the *within* block and its inverse are not subjected to redundant control logic in the case where the *within-apply* statement as a whole is subject to control. * The application of the *within* block and its inverse are guaranteed to be strictly equivalent, including in the case where *within* block involves non-deterministic implementation decisions by the synthesis engine. ## Examples ### Example 1 The following example demonstrates how auxiliary qubits get used, uncomputed, and reused at different steps of a computation, when scoped inside a *within-apply* statement. Actual reuse is a decision the synthesis engine takes to satisfy width constraints. ```python theme={null} from classiq import * @qfunc def main(res: Output[QBit]): allocate(res) ctrl = QNum() within_apply(lambda: assign(3, ctrl), lambda: CCX(ctrl, res)) within_apply(lambda: assign(2, ctrl), lambda: CCX(ctrl, res)) ``` ``` qfunc main(output res: qbit) { allocate(res); ctrl: qnum; within { ctrl = 3; } apply { CCX(ctrl, res); } within { ctrl = 2; } apply { CCX(ctrl, res); } } ``` Note how variable `ctrl` is initialized in the *within* block, prior to being used for the `CCX` in the *apply* block. Outside the *within-apply* statement the variable is reset to its uninitialized state, and used again in the same way. Visualizing the resulting quantum program, you can see how the same two auxiliary qubits are reused across the two steps of the circuit, because of width optimization. visualization of the within-apply example bright visualization of the within-apply example dark ### Example 2 The code snippet below demonstrates the implementation of the phase kickback pattern for an arbitrary quantum predicate. Function `my_phase_oracle` takes as parameter a function that flips a qubit on the states of interest. Variable `aux` is prepared in the $|1\rangle$ state and passed as the target to function `my_cond_phase_flip`. The cumulative effect of `my_cond_phase_flip` is a conditional $\pi$ phase on `target`, controlled on the states of interest. Since `aux` is subsequently uncomputed and released, a relative $\pi$ phase remains between the states of interest and all others in the superposition. ```python theme={null} from classiq import * @qperm(disable_perm_check=True, disable_const_checks=True) def my_cond_phase_flip(predicate: QPerm[QBit], target: Const[QBit]): H(target) predicate(target) H(target) @qperm def my_phase_oracle(predicate: QPerm[QBit]): aux = QBit() within_apply( lambda: (allocate(aux), X(aux)), lambda: my_cond_phase_flip(predicate, aux) ) ``` ``` @disable_perm_check @disable_const_checks qperm my_cond_phase_flip(predicate: qperm (qbit), const target: qbit) { H(target); predicate(target); H(target); } qperm my_phase_oracle(predicate: qperm (qbit)) { aux: qbit; within { allocate(aux); X(aux); } apply { my_cond_phase_flip(predicate, aux); } } ``` Note that `my_cond_phase_flip` declares parameter `target` as `const` because, taken as a whole, the function only applies phase changes to it. But because the implementation uses non-cost operations, we disable const checks. For more on enforcement of parameter restrictions see [Uncomputation](/qmod-reference/language-reference/uncomputation). ### Example 3 The code snippet below demonstrates the use of *within-apply* to define the Grover operator, avoiding redundant control logic when called in higher-level contexts (for example, when used as the unitary operand in a phase-estimation flow): ```python theme={null} from classiq import * @qfunc def my_grover_operator( oracle: QCallable[QArray[QBit]], space_transform: QCallable[QArray[QBit]], target: QArray[QBit], ): oracle(target) within_apply( lambda: invert(lambda: space_transform(target)), lambda: reflect_about_zero(target), ) ``` ``` qfunc my_grover_operator(oracle: qfunc (qbit[]), space_transform: qfunc (qbit[]), target: qbit[]) { oracle(target); within { invert { space_transform(target); } } apply { reflect_about_zero(target); } } ``` # Uncomputation Source: https://docs.classiq.io/qmod-reference/language-reference/uncomputation Uncomputation is the process of reversing the effects of quantum operations, restoring the state of qubits to their initial $|0\rangle$ state and disentangling them from other qubits. Failing to properly uncompute intermediate results can lead to incorrect final measurement results and wasted resources. In Qmod, intermediate results are typically stored in local variables, which are scoped inside a function and are inaccessible outside of it. Hence, local variables must be used in a way that enables subsequent uncomputation. Specifically, their interactions with other objects must be restrictive to be subsequently disentangled from them. In Qmod, local variables are **automatically uncomputed**, lifting the burden of manually uncomputing intermediate results. When finer-grained control is required, allocate the variable inside the *within* block of a [*within-apply*](/qmod-reference/language-reference/statements/within-apply) statement. Such variables are subject to strict rules that guarantee their correct uncomputation. In this way, Qmod abstracts away the implementation details of efficient quantum storage management and prevents a class of functional bugs that are very difficult to detect. A fully manual uncomputation can be achieved by using [free](/qmod-reference/language-reference/quantum-variables#free), as variables which are explicitly freed are not subject to these rules. To enable these capabilities, quantum operations are classified into arbitrary functions and *permutation*-only functions. In addition, each of an operation's parameters is classified as either *const* or *non-const*. * **permutation**: A quantum operation is a *permutation* if it maps computational-basis states to computational-basis states (with possible phase shifts). Such an operation neither introduces nor destroys quantum superpositions, and its computation can be described classically. * **const**: A parameter of a quantum operation is constant if it is immutable up to a phase. That is, the magnitudes of its computational-basis state components remain unchanged, while their phases may shift. *permutation* functions are explicity declared using the keyword `qperm`. *const* parameters are explicity declared using the keyword `const`. See more under [Function Declarations](/qmod-reference/language-reference/functions). * `Z` is a *permutation* and its parameter is *const* as it merely flips the phase of the $|1\rangle$ state. * `X` is a *permutation* but its parameter is not *const* as it flips between $|0\rangle$ and $|1\rangle$. * `H` is not a *permutation* and its parameter is not *const*, as it introduces superposition. * `SWAP` is a *permutation* but its two parameters are not *const*, as it swaps between $|01\rangle$ and $|10\rangle$. ## Quantum operations classification Each quantum operation is classified as either an arbitrary mutation of the quantum state or strictly a *permutation* of computational-basis states, according to the following rules: * A function call is a *permutation* if the callee is declared as `qperm`. * A numeric assignment (both out-of-place and in-place) is a *permutation*. * Amplitude-encoding assignment is *non-permutation*. * Phase statement is a *permutation*. * A compound statement is a *permutation* if its body contains only *permutation* operations. For example, a *control* statement is a *permutation* if all statements inside its *then* and *else* clauses are *permutations*. * Bind statement is considered a *permutation*, as are `allocate` and `free`. In addition, each use of a quantum variable is classified as either *const* or *non-const*: * An argument in a function call is classified according to the modifier used in the matching parameter declaration. * Use as right-value quantum expression is *const*. Right-value expressions occur in the following contexts: * On the right-hand side of a numeric assignment or amplitude-encoding assignment. * As the condition in a *control* statement. * As the argument of a *phase* statement. * Use as left-value expression in numeric assignments (both out-of-place and in-place) and amplitude-encoding assignments is *non-const*. * Arguments to a *bind* statement are treated specially: they are not immediately classified, but are instead bound together and assigned a joint classification of either *const* or *non-const*. ### Enforcement of function classification Generally, a `qperm` function is restricted to use only *permutation* operations, and a `const` parameter is restricted to *const* use contexts. Violation of these restrictions results in a compilation error. However, flexibility is often required in the implementation of lower-level building blocks. The cumulative effect of the function on the quantum parameters in these cases satisfies its declared restrictions, but individual operations violate it. Well known examples are the implementation of Toffoli gate, and arithmetic addition in the Fourier bases. Both are permutation-only operations taken as a whole, but internally use Hadamard gates and rotations. It is not scalable to validate the correct cumulative effect of a description automatically in the general case. However, you can suppress the fine-grained enforcement of function classification with the `disable_perm_check` and `disable_const_checks` specifiers using the following syntax: The decorators `@qfunc` and `@qperm` have the optional parameters `disable_perm_check` and `disable_const_checks` declared thus - disable\_perm\_check: bool = False disable\_const\_checks: Union\[list\[str], bool] = False `disable_const_checks` may contain a list of parameter names, or a boolean to disable the checks for all *const* parameters. Before a function definition you may specify the following decorators: **@disable\_perm\_check** **@disable\_const\_checks** \[ **(** *parameters* **)** ] Where *parameters* is an optional list of comma-separated parameter names (if the list is omitted, the checks are disabled for all *const* parameters). When `disable_perm_check` is used, the compiler does not enforce the usage of *non-permutation* operations. When `disable_const_checks` is used, the compiler does not enforce use context restrictions on the listed parameters (or all parameters if none specified). ### Examples #### Example 1 - Correct use of permutation and const parameters In the example below, function `foo` is declared `qperm`, and its first parameter `param1` is declared `const`. The definition of `foo` is consistent with these declarations. Note that the restriction on `param1` is carried over to its use in the lambda expression passed to `apply_to_all`. ```python theme={null} from classiq import * @qperm def foo(param1: Const[QNum], param2: Output[QNum]): param2 |= param1 + 1 # OK - assignment is a permutation and RHS is const apply_to_all(lambda qb: Z(qb), param1) # OK - the parameter of 'Z' is const ``` ``` qperm foo(const param1: qnum, output param2: qnum) { param2 = param1 + 1; // OK - assignment is a permutation and RHS is const apply_to_all(lambda(qb) { Z(qb); }, param1); // OK - the parameter of 'Z' is const } ``` #### Example 2 - Incorrect use of permutation and const parameters The example below demonstrates violations of the restrictions on the use of parameters and operations. As in the previous example, the function `foo` is declared `qperm` and its first parameter `param1` is declared `const`. However, the use of `param1` violates the restriction, and the function uses a *non-permutation* operation as well. ```python theme={null} from classiq import * @qperm def foo(param1: Const[QNum], param2: Output[QNum]): param1 += 2 # Error - LHS is non-const hadamard_transform(param2) # Error - 'hadamard_transform' is non-permutation ``` ``` qperm foo(const param1: qnum, output param2: qnum) { param1 += 2; // Error - LHS is non-const hadamard_transform(param2); // Error - 'hadamard_transform' is non-permutation ``` #### Example 3 - Disabling permutation check In the example below, function `my_cx` implements the CX operation using a simple equivalence - applying phase flip in the Hadamard basis. The cumulative operation on the quantum state is a *permutation*, but individual calls to `H` are not. The `disable_perm_check` is used to suppress compiler errors in this case. ```python theme={null} from classiq import * @qperm(disable_perm_check=True) def my_cx(ctrl: Const[QBit], tgt: QBit): H(tgt) CZ(ctrl, tgt) H(tgt) ``` ``` @disable_perm_check qperm my_cx(const ctrl: qbit, tgt: qbit) { H(tgt); CZ(ctrl, tgt); H(tgt); } ``` #### Example 4 - Disabling permutation check and const checks In the example below, function `my_z` implements the Z operation using a simple equivalence - applying bit flip in the Hadamard basis. The cumulative operation on the quantum state is a *permutation*, but individual calls to `H` are not. Also, the cumulative operation on the `tgt` is *const*, but individual calls are not. The `disable_perm_check` and `disable_const_checks` are used to suppress compiler errors in this case. ```python theme={null} from classiq import * @qperm(disable_perm_check=True, disable_const_checks=True) def my_z(tgt: Const[QBit]): H(tgt) X(tgt) H(tgt) ``` ``` @disable_perm_check @disable_const_checks qperm my_z(const tgt: qbit) { H(tgt); X(tgt); H(tgt); } ``` ## Semantics of uncomputation When a variable is initialized inside the *within* block of a [*within-apply*](/qmod-reference/language-reference/statements/within-apply) statement, it is returned to its uninitialized state after the statement completes. The newly allocated quantum object is uncomputed, and its qubits are reclaimed by the compiler for subsequent use. Likewise, a variable declared locally within a function is only accessible inside the function's scope. The quantum object allocated inside the function may be uncomputed at some later point and its qubits reclaimed. Local variables are considered *uncomputation candidates* if they remain initialized at the end of the function scope in which they were declared. Variables initialized inside a *within* block of a *within-apply* statement are also considered uncomputation candidates in the scope of the statement. The following rules guarantee that uncomputation candidates can be handled correctly: * An uncomputation candidate must not be used in a *non-permutation* operation. * A variable becomes a *dependency* of an uncomputation candidate when used in an operation together with the candidate variable, and the latter is used in a *non-const* context. From that point, the dependency variable is subject to the same rules as the candidate variable, while the latter is in scope. * An auto-uncomputation candidate must not be used in an operation together with a non-local variable if both use contexts are *non-const*. * An auto-uncomputation candidate must not become a *dependency* of any variable that already (directly or indirectly) depends on it — that is, circular dependencies are not allowed. * A variable initialized inside a *within* block of a *within-apply* statement can only be used in *const* contexts inside the *apply* block. Violating these rules will result in a compilation error. The operations [`free`](/qmod-reference/language-reference/quantum-variables#free) and [`drop`](/qmod-reference/language-reference/quantum-variables#drop) return the variable to its uninitialized state, so it is consequently not considered an uncomputation candidate and therefore does not undergo automatic uncomputation or any uncomputation validation. ### Examples #### Example 1 - Automatic uncomputation The example below demonstrates the use of a local variable and its automatic uncomputation. Variable `aux` is initialized as the left-value expression of an assignment statement, which is a *permutation* operation. Subsequently, it is used as the condition of a *control* statement, which is a *const* context. Both uses are valid, and the variable is automatically uncomputed and freed correctly at the end of the function. ```python theme={null} from classiq import * @qperm def foo(qn: QNum, res: QBit): aux = QBit() aux |= qn > 1 control(aux, lambda: X(res)) @qfunc def main(qn: Output[QNum], res: Output[QBit]): allocate(2, qn) hadamard_transform(qn) allocate(res) foo(qn, res) foo(qn, res) ``` ``` qperm foo(qn: qnum, res: qbit) { aux: qbit; aux = qn > 1; control(aux) { X(res); } } qfunc main(output qn: qnum, output res: qbit) { allocate(2, qn); hadamard_transform(qn); allocate(1, res); foo(qn, res); foo(qn, res); } ``` In the synthesized quantum program, variable `aux` is uncomputed and reused across the multiple calls to `foo`, as can be seen in the visualization: visualization of the automatic uncomputation example bright visualization of the automatic uncomputation example dark #### Example 2 - Illegal use of local variable The example below demonstrates illegal use of a local variable in a function which outputs only one qubit of a Bell pair, making it impossible to uncompute the other qubit. Specifically, the local variable `q2` undergoes a *non-permutation* operation, which is flagged as an error. ```python theme={null} from classiq import * @qfunc def main(q1: Output[QBit]): q2 = QBit() allocate(q1) allocate(q2) H( q2 ) # Error - The local variable 'q2' cannot be automatically uncomputed because it is mutated by a non-permutation operation. CX(q2, q1) ``` ``` qfunc main(output q1: qbit) { q2: qbit; allocate(q1); allocate(q2); H(q2); // Error - The local variable 'q2' cannot be automatically uncomputed because it is mutated by a non-permutation operation. CX(q2, q1); } ``` #### Example 3 - Illegal use of local variable due to parameter mutation The example below demonstrates an illegal use of a local variable `aux`, as it is used as an argument in a call to function `x_transform` together with the variable `p`, where both parameters of `x_transform` are declared *non-const*. ```python theme={null} from classiq import * @qperm def x_transform(q1: QBit, q2: QBit): X(q1) X(q2) @qfunc def main(p: Output[QBit]): aux = QBit() allocate(p) allocate(aux) x_transform( p, aux ) # Error - The local variable 'aux' cannot be automatically uncomputed because it is mutated by an operation which also mutates the parameter 'p'. ``` ``` qperm x_transform(q1: qbit, q2: qbit) { X(q1); X(q2); } qfunc main(output p: qbit) { aux: qbit; allocate(p); allocate(aux); x_transform(p, aux); // Error - The local variable 'aux' cannot be automatically uncomputed because it is mutated by an operation which also mutates the parameter 'p'. } ``` #### Example 4 - Illegal use of local variable due to circular dependency The example below demonstrates an illegal use of the local variable `aux`, as the two calls to the function `CX` create a circular dependency between it and the variable `p`. ```python theme={null} from classiq import * @qfunc def main(p: Output[QBit]): aux = QBit() allocate(p) allocate(aux) CX( p, aux ) # Error - The local variable 'aux' cannot be automatically uncomputed because it forms a circular dependency with the variable 'p'. CX(aux, p) ``` ``` qfunc main(output p: qbit) { aux: qbit; allocate(p); allocate(aux); CX(p, aux); // Error - The local variable 'aux' cannot be automatically uncomputed because it forms a circular dependency with the variable 'p'. CX(aux, p); } ``` #### Example 5 - Correct uncomputation in within-apply The example below demonstrates the use of a local variable initialized inside a *within* block of a *within-apply* statement. Variable `aux` is initialized as the left-value expression of an assignment statement, which is a *permutation* operation. In the *apply* block, it is used as the condition of a *control* statement, which is a *const* context. Both uses are valid, and the variable is uncomputed and freed correctly after the *within-apply* statement. ```python theme={null} from classiq import * @qfunc def main(qn: Output[QNum], res: Output[QBit]): allocate(2, qn) hadamard_transform(qn) allocate(res) aux = QBit() within_apply( within=lambda: assign(qn > 1, aux), apply=lambda: control(aux, lambda: X(res)) ) ``` ``` qfunc main(output qn: qnum, output res: qbit) { allocate(2, qn); hadamard_transform(qn); allocate(1, res); aux: qbit; within { aux = qn > 1; } apply { control (aux) { X(res); } } } ``` #### Example 6 - Illegal use of local variable in within-apply The code below is a modification of *Example 1* above, with a couple of lines added to demonstrate violations of the rules for correct use of a variable initialized inside the *within* block of a *within-apply* statement. Here, variable `aux` is also used as the argument of function `H` in the *within* block. This is an arbitrarily *non-permutation* operation (indeed `H` introduces superposition between computational-basis states). In addition, `aux` is used as the argument of function `X` in the *apply* block, which is *non-const* context. Both uses are illegal, and both are reported as errors by the compiler. ```python theme={null} from classiq import * @qfunc def main(qn: Output[QNum], res: Output[QBit]): allocate(2, qn) hadamard_transform(qn) allocate(res) aux = QBit() within_apply( within=lambda: ( assign(qn > 1, aux), H(aux), ), apply=lambda: ( control(aux, lambda: X(res)), X(aux), ), ) ``` ``` qfunc main(output qn: qnum, output res: qbit) { allocate(2, qn); hadamard_transform(qn); allocate(1, res); aux: qbit; within { aux = qn > 1; H(aux); } apply { control (aux) { X(res); } X(aux); } } ``` #### Example 7 - Illegal use of dependent variable in within-apply The following example demonstrates a violation of the rules for correct use of a dependent variable inside a *within-apply* statement. Here, variable `aux` is initialized inside a *within* block, and is subsequently entangled with `q1` which is not an uncomputation candidate. From that point, the same restrictions that hold for `aux` apply to `q1`. Therefore, using it as an argument to function `H` is illegal, and is reported as an error. Indeed, if `foo` would execute as specified, `aux` would not be uncomputed correctly. ```python theme={null} from classiq import * @qfunc def foo(q1: QBit, q2: QBit): aux = QBit() within_apply( within=lambda: ( allocate(aux), CX(q1, aux), H(q1), ), apply=lambda: (CX(aux, q2), Z(q1)), ) ``` ``` qfunc foo(q1: qbit, q2: qbit) { aux: qbit; within { allocate(1, aux); CX(q1, aux); H(q1); } apply { CX(aux, q2); Z(q1); } } ``` # qDrift Source: https://docs.classiq.io/qmod-reference/library-reference/core-library-functions/hamiltonian_evolution/qdrift/qdrift Open this notebook in GitHub to run it yourself The `qdrift` function implements the qDrift Trotter evolution of Ref.[ \[1\] ](#qdrift). Function: `qdrift` Arguments: * `pauli_operator`: `CArray[PauliTerm]`, * `evolution_coefficient`: `CReal`, * `num_qdrift`: `CInt`, * `qbv`: `QArray[QBit]`, ## Example ```python theme={null} from classiq import * @qfunc def main(qba: Output[QArray[QBit]]): allocate(2, qba) qdrift( Pauli.X(0) * Pauli.Y(1) + 0.5 * Pauli.Z(0), evolution_coefficient=2.0, num_qdrift=5, qbv=qba, ) qprog = synthesize(main) ``` ## References \[1] E. Campbell, Random Compiler for Fast Hamiltonian Simulation, (2019). [https://arxiv.org/abs/1811.08017](https://arxiv.org/abs/1811.08017) # Suzuki Trotter Source: https://docs.classiq.io/qmod-reference/library-reference/core-library-functions/hamiltonian_evolution/suzuki_trotter/suzuki_trotter Open this notebook in GitHub to run it yourself The `suzuki_trotter` function produces the Suzuki-Trotter product for a given order and repetitions. Given a Hamiltonian as a sum of Pauli strings $$ H = \sum^L_{k=1} \alpha_k H_k, $$ the Suzuki-Trotter formula of order $o$ and repetitions $r$ approximates the Hamiltonian simulation $U = e^{-iHt}$ according to the following: * Each order $ST^{(o)}(H,t)$ is defined recursively: * The first order is : $ST^{(1)}(H,t) = \Pi^L_{k=1} e^{-iH_k t}$ * The second order is : $ST^{(2)}(H,t) = \Pi^L_{k=1} e^{-iH_k t/2} \Pi^1_{k=L} e^{-iH_k t/2}$ * Recursion formula for order $2m$ with $m>1$ is given in Eq. (5) of Ref. \[[2](#childs)] * For a given order, repetitions refers to $$ ST^{(o,r)}(H,t) = [ST^{(o)}(H,t/r)]^r. $$ Function: `suzuki_trotter` Arguments: * `pauli_operator`: `CArray[PauliTerm]` * `evolution_coefficient`: `CReal` * `order`: `CInt`, * `repetitions`: `CInt`, * `qbv`: `QArray[QBit]` ## Example ```python theme={null} from classiq import * @qfunc def main(x: CReal, qba: Output[QArray[QBit]]): allocate(3, qba) suzuki_trotter( [ PauliTerm(pauli=[Pauli.X, Pauli.X, Pauli.Z], coefficient=1.5), PauliTerm(pauli=[Pauli.Y, Pauli.X, Pauli.Z], coefficient=0.5), ], evolution_coefficient=x, order=1, repetitions=1, qbv=qba, ) qprog = synthesize(main) ``` ## References \[1] N. Hatano and M. Suzuki, Finding Exponential Product Formulas of Higher Orders, (2005). [https://arxiv.org/abs/math-ph/0506007](https://arxiv.org/abs/math-ph/0506007) \[2] Childs, et al., Toward the first quantum simulation with quantum speedup, (2018). [https://arxiv.org/abs/1711.10980](https://arxiv.org/abs/1711.10980) # State Preparation Source: https://docs.classiq.io/qmod-reference/library-reference/core-library-functions/prepare_state_and_amplitudes/prepare_state_and_amplitudes Open this notebook in GitHub to run it yourself Most quantum applications start with preparing a state in a quantum register. For example, in finance the state may represent the price distribution of some assets. In chemistry, it may be an initial guess for the ground state of a molecule, and in a quantum machine learning, a feature vector to analyze. The state preparation functions creates a quantum program that outputs either a probability distribution $p_{i}$ or a real amplitudes vector $a_{i}$ in the computational basis, with $i$ denoting the corresponding basis state. The amplitudes take the form of list of float numbers. The probabilities are a list of positive numbers. This is the resulting wave function for probability: $$ \left|\psi\right\rangle = \sum_{i}\sqrt{p_{i}} \left|i\right\rangle, $$ and this is for amplitude: $$ \left|\psi\right\rangle = \sum_{i}a_{i} \left|i\right\rangle. $$ In general, state preparation is hard. Only a very small portion of the Hilbert space can be prepared efficiently (in $O(poly(n))$ steps) on a quantum program. Therefore, in practice, an approximation is often used to lower the complexity. The approximation is specified by an error bound, using the [$L_2$ norm](https://en.wikipedia.org/wiki/Lp_space). The higher the specified error tolerance, the smaller the output quantum program. For exact state preparation, specify an error bound of $0$. The state preparation algorithm can be tuned depending on whether the probability distribution is sparse or dense. The synthesis engine will automatically select the parameterization based on the given constraints and optimization level. Function: `prepare_state` Parameters: * `probabilities: CArray[CReal]` * Probabilities to load. Should be non-negative and sum to 1. * `bound: CReal` * Approximation Error Bound, in the $L_2$ metric (with respect to the given probabilies vector). * `out: Output[QArray[QBit]]` Function: `inplace_prepare_state` Parameters: * `probabilities: CArray[CReal]` * `bound: CReal` * `out: QArray[QBit]` * Should of size exactly $\log_2$(\`\`probabilities.len\`) The `inplace_prepare_state` works the same, but for a given allocated `QArray`. Function: `prepare_amplitudes` Parameters: * `amplitudes: CArray[CReal]` * Amplitudes of the loaded state. Each should be real and the vector norm should be equal to 1. * `bound: CReal` * Approximation Error Bound, in the $L_2$ metric (with respect to the given amplitudes vector). * `out: Output[QArray[QBit]]` Function: `inplace_prepare_amplitudes` Parameters: * `amplitudes: CArray[CReal]` * Amplitudes of the loaded state. Each should be real and the vector norm should be equal to 1. * `bound: CReal` * Approximation Error Bound, in the $L_2$ metric (with respect to the given amplitudes vector). * `out: QArray[QBit]` * Should of size exactly $\log_2$(`amplitudes.len`) The `inplace_prepare_amplitudes` works the same, but for a given allocated `QArray`. ## Example 1: Loading Point Mass (PMF) Function This example generates a quantum program whose output state probabilities are an approximation to the PMF given. That is, the probability of measuring the state $|000\rangle$ is $0.05$, $|001\rangle$ is $0.11$,... , and the probability to measure $|111\rangle$ is $0.06$. ```python theme={null} from classiq import * @qfunc def main(x: Output[QNum]): probabilities = [0.05, 0.11, 0.13, 0.23, 0.27, 0.12, 0.03, 0.06] prepare_state(probabilities=probabilities, bound=0.01, out=x) qmod = create_model(main) ``` ```python theme={null} qprog = synthesize(qmod) ``` Print the resulting probabilities: ```python theme={null} import numpy as np result = execute(qprog).result_value() probs = np.zeros(8) for sample in result.parsed_counts: probs[int(sample.state["x"])] = sample.shots / result.num_shots print("Resulting probabilities:", probs) ``` **Output:** ``` Resulting probabilities: [0.032 0.118 0.141 0.217 0.282 0.107 0.031 0.072] ``` ## Example 2 * Preparating Amplitudes This example loads a normalized linear space between -1 to 1. The load state has an accuracy of 99 present under the L2 norm. ```python theme={null} from classiq.execution import ClassiqBackendPreferences, ExecutionPreferences @qfunc def main(x: Output[QNum]): amps = np.linspace(-1, 1, 8) amps = amps / np.linalg.norm(amps) prepare_amplitudes(amplitudes=amps.tolist(), bound=0, out=x) qmod = create_model(main) qmod = set_execution_preferences( qmod, num_shots=1, backend_preferences=ClassiqBackendPreferences(backend_name="simulator_statevector"), ) ``` ```python theme={null} qprog = synthesize(qmod) ``` Print the resulting amplitudes: ```python theme={null} import numpy as np result = execute(qprog).result_value() amps = np.zeros(8, dtype=complex) for sample in result.parsed_state_vector: amps[int(sample.state["x"])] = sample.amplitude # remove global phase global_phase = np.angle(amps[0]) amps = np.real(amps / np.exp(1j * global_phase)) print("Resulting amplitudes:", amps) ``` **Output:** ``` Resulting amplitudes: [ 0.54006172 0.38575837 0.23145502 0.07715167 -0.07715167 -0.23145502 -0.38575837 -0.54006172] ``` # Standard Gates Source: https://docs.classiq.io/qmod-reference/library-reference/core-library-functions/standard_gates/standard_gates Open this notebook in GitHub to run it yourself The Classiq platform provides many standard gates. Some key standard gates are shown here in detail.
All gates are covered in the [reference manual](https://docs.classiq.io/latest/sdk-reference/#classiq.interface.generator.standard_gates). ```python theme={null} from classiq import * ``` ## Single Qubit Gates An example is given for $X$ gate. The gates $I$, $X$, $Y$, $Z$, $H$, $T$ are used in the same way. # ## For Example: X Function: `X` Arguments: * `target`: `QBit` ```python theme={null} @qfunc def main(q: Output[QBit]): allocate(q) X(q) qmod = create_model(main) qprog = synthesize(qmod) ``` ## Single Qubit Rotation Gates An example is given for $RZ$ gate. The gates $RX$, $RY$, $RZ$ are used in the same way except for parameter name. # ### Parameter Names for Different Rotation Gates * `RX`: `theta` * `RY`: `theta` * `RZ`: `phi` # ## For Example: RZ $$ \begin{split}RZ(\theta) = \begin{pmatrix} {e^{-i\frac{\theta}{2}}} & 0 \\ 0 & {e^{i\frac{\theta}{2}}} \\ \end{pmatrix}\end{split} $$ Function: `RZ` Arguments: * `theta`: `CReal` * `target`: `QBit` ```python theme={null} @qfunc def main(q: Output[QBit]): allocate(q) theta = 1.9 RZ(theta, q) qmod = create_model(main) qprog = synthesize(qmod) ``` # ## R Gate Rotation by $\theta$ around the $cos(\phi)X + sin(\phi)Y$ axis. $$ \begin{split}R(\theta, \phi) = \begin{pmatrix} cos(\frac{\theta}{2}) & -ie^{-i\phi}sin(\frac{\theta}{2}) \\ -ie^{i\phi}sin(\frac{\theta}{2}) & cos(\frac{\theta}{2}) \\ \end{pmatrix}\end{split} $$ Parameters: * `theta`: `CReal` * `phi`: `CReal` * `target`: `QBit` ```python theme={null} @qfunc def main(q: Output[QBit]): allocate(q) theta = 1 phi = 2 R(theta, phi, q) qmod = create_model(main) qprog = synthesize(qmod) ``` # ## Phase Gate Rotation about the Z axis by $\lambda$ with global phase of $\frac{\lambda}{2}$. $$ \begin{split}PHASE(\lambda) = \begin{pmatrix} 1 & 0 \\ 0 & e^{i\lambda} \end{pmatrix}\end{split} $$ Parameters: * `theta`: `CReal` * `target`: `QBit` ```python theme={null} @qfunc def main(q: Output[QBit]): allocate(q) theta = 1 PHASE(theta, q) qmod = create_model(main) qprog = synthesize(qmod) ``` ## Double Qubits Rotation Gates An example is given for $RZZ$ gate. The gates $RXX$, $RYY$, $RZZ$ are used in the same way. # ## RZZ Gate Rotation about ZZ. $$ \begin{split}RZZ(\theta) = \begin{pmatrix} {e^{-i\frac{\theta}{2}}} & 0 & 0 & 0 \\ 0 & {e^{i\frac{\theta}{2}}} & 0 & 0 \\ 0 & 0 & {e^{i\frac{\theta}{2}}} & 0 \\ 0 & 0 & 0 & {e^{-i\frac{\theta}{2}}} \\ \end{pmatrix}\end{split} $$ Parameters: * `theta`: `CReal` * `target`: `QArray[QBit]` ```python theme={null} @qfunc def main(q: Output[QArray]): allocate(2, q) theta = 1 RZZ(theta, q) qmod = create_model(main) qprog = synthesize(qmod) ``` ## Controlled Gates An example is given for $CX$ gate. The gates $CX$, $CY$, $CZ$, $CH$, $CSX$, $CCX$ are used in a similar way. In $CCX$ Gate the `ctrl_state` parameter receives a value suitable for 2 control qubits. for example: `"01"`. # ## CX Gate The Controlled $X$ gate. Applies $X$ Gate on the target qubit, based on the state of the control qubit (by default if the controlled state is $|1\rangle$). $$ \begin{split}CX = \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 1 \\ 0 & 0 & 1 & 0 \\ \end{pmatrix}\end{split} $$ Parameters: * `control`: `QBit` * `target`: `QBit` ```python theme={null} @qfunc def main(q_target: Output[QBit], q_control: Output[QBit]): allocate(q_target) allocate(q_control) CX(q_control, q_target) qmod = create_model(main) qprog = synthesize(qmod) ``` ## Controlled Rotations An example is given for $CRX$ gate. The gates $CRX$, $CRY$, $CRZ$, CPhase are used in the same way. # ## CRX Gate Controlled rotation around the X axis. $$ \begin{split}CRX(\theta) = \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & \cos(\frac{\theta}{2}) & -i\sin(\frac{\theta}{2}) \\ 0 & 0 & -i\sin(\frac{\theta}{2}) & \cos(\frac{\theta}{2}) \\ \end{pmatrix}\end{split} $$ Parameters: * `theta`: `CReal` * `control`: `QBit` * `target`: `QBit` ```python theme={null} @qfunc def main(q_target: Output[QBit], q_control: Output[QBit]): allocate(q_target) allocate(q_control) theta = 1 CRX(theta, q_control, q_target) qmod = create_model(main) qprog = synthesize(qmod) ``` ## Swap Gate Swaps between two qubit states. $$ \begin{split}SWAP = \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 1 \\ \end{pmatrix}\end{split} $$ Parameters: * `qbit0`: `QBit` * `qbit1`: `QBit` ```python theme={null} @qfunc def main(q1: Output[QBit], q2: Output[QBit]): allocate(q1) allocate(q2) SWAP(q1, q2) qmod = create_model(main) qprog = synthesize(qmod) ``` ## U Gate The single-qubit gate applies phase and rotation with three Euler angles. Matrix representation: $$ U(\gamma,\phi,\theta,\lambda) = e^{i\gamma}\begin{pmatrix} \cos(\frac{\theta}{2}) & -e^{i\lambda}\sin(\frac{\theta}{2}) \\ e^{i\phi}\sin(\frac{\theta}{2}) & e^{i(\phi+\lambda)}\cos(\frac{\theta}{2}) \\ \end{pmatrix} $$ Parameters: * `theta`: `CReal` * `phi`: `CReal` * `lam`: `CReal` * `gam`: `CReal` * `target`: `QBit` ```python theme={null} @qfunc def main(q: Output[QBit]): allocate(q) theta = 1 phi = 2 lam = 1.5 gam = 1.1 U(theta, phi, lam, gam, q) qmod = create_model(main) qprog = synthesize(qmod) ``` # Unitary Function Source: https://docs.classiq.io/qmod-reference/library-reference/core-library-functions/unitary/unitary Open this notebook in GitHub to run it yourself Given a $2^{n}\times2^{n}$ unitary matrix, the unitary-gate function constructs an equivalent unitary function that acts on $n$ qubits accordingly. For $n>2$, the synthesis process implementation is based on [\[1\]](#1). Function: `unitary` Arguments: * `elements: CArray[CArray[CReal]]` * A 2d array of complex numbers representing the unitary matrix. * `target: QArray[QBit]` * The quantum state to apply the unitary on. Should be of corresponding size. ## Example This example shows a $2$-qubit unitary function application in the formed $4$-dimensional space. ```python theme={null} from classiq import * UNITARY = [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, -1j, 0], [0, 0, 0, 1j]] @qfunc def main(x: Output[QArray[QBit]]): allocate(2, x) unitary(UNITARY, x) ``` ```python theme={null} qprog = synthesize(main) ``` ## References \[1] R. Iten et al, Quantum Circuits for Isometries, Phys. Rev. A 93 (2016). [https://link.aps.org/doi/10.1103/PhysRevA.93.032318](https://link.aps.org/doi/10.1103/PhysRevA.93.032318) # Exact Amplitude Amplification Source: https://docs.classiq.io/qmod-reference/library-reference/open-library-functions/amplitude_amplification/exact_amplitude_amplification Open this notebook in GitHub to run it yourself The following is a usage example for the `exact_amplitude_amplification` function. We will amplify the state $|1\rangle$ out of the state $\sqrt{0.07}|0\rangle + \sqrt{0.93}|1\rangle$. We provide the function with the following parameters: * `amplitude`: $\sqrt{0.07}$, the original amplitude of the wanted state. * `oracle`: $Z$, that will apply a $(-1)$ phase on the $|1\rangle$ state. * `space_transform`: the state preparation function of the original state. * `packed_vars`: `x`, the quantum variable to apply on. Notice that knowledge of the amplitude of the "good" state is need. ```python theme={null} import numpy as np from classiq import * GOOD_STATE_PROB = 0.07 @qfunc def prepare_initial_state(x: QBit): inplace_prepare_state([1 - GOOD_STATE_PROB, GOOD_STATE_PROB], 0, x) @qfunc def main(x: Output[QBit]): allocate(x) exact_amplitude_amplification( np.sqrt(GOOD_STATE_PROB), Z, prepare_initial_state, x # amplify the |1> state ) qprog = synthesize(main) show(qprog) res = execute(qprog).get_sample_result() res.dataframe ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/31bCC7kOsCvsDd0UpFNHlHQnXXb ``` | | x | count | probability | bitstring | | - | - | ----- | ----------- | --------- | | 0 | 1 | 2048 | 1.0 | 1 | ```python theme={null} assert sum(res.dataframe[res.dataframe.x == 1]["counts"]) == res.num_shots ``` The concept in the implementation is to reduce the initial angle between the `good` and `bad` states to be an exact division of 1 by a integer: ```python theme={null} import matplotlib.pyplot as plt import numpy as np from matplotlib.lines import Line2D # angles theta = np.arcsin(np.sqrt(GOOD_STATE_PROB)) k = int(np.ceil((np.pi / (4 * theta)) - 0.5)) theta_prime = np.pi / (4 * k + 2) # basis axes (Good on +x, Bad on +y) GOOD = np.array([1.0, 0.0]) BAD = np.array([0.0, 1.0]) # initial states measured from the +x (GOOD) axis psi = np.array([np.sin(theta), np.cos(theta)]) # standard psi_prime = np.array([np.sin(theta_prime), np.cos(theta_prime)]) # ancilla-tuned def R(a): return np.array([[np.cos(a), np.sin(a)], [-np.sin(a), np.cos(a)]]) # each Grover iterate rotates by +2*theta in the GOOD/BAD plane steps = range(k + 1) states = [R(2 * k * theta) @ psi for k in steps] states_prime = [R(2 * k * theta_prime) @ psi_prime for k in steps] # ---- plot ---- plt.figure(figsize=(10, 10)) ax = plt.gca() ax.set_aspect("equal") # axes plt.axhline(0, color="gray", lw=1) plt.axvline(0, color="gray", lw=1) plt.text(1.25, 0, "|Good>", ha="left", va="center", fontsize=12) plt.text(0, 1.25, "|Bad>", ha="center", va="bottom", fontsize=12) # standard Grover trajectory (blue) for k, v in enumerate(states): plt.arrow( 0, 0, v[0], v[1], head_width=0.05, length_includes_head=True, color="blue", alpha=0.7, ) plt.text(v[0] * 1.08, v[1] * 1.08, f"{k}", color="blue") # exact (ancilla-tuned) trajectory (red) for k, v in enumerate(states_prime): plt.arrow( 0, 0, v[0], v[1], head_width=0.05, length_includes_head=True, color="red", alpha=0.7, ) plt.text(v[0] * 1.08, v[1] * 1.08, f"{k}'", color="red") # legend handles legend_elements = [ Line2D([0], [0], color="blue", lw=2, label="amplitude amplification"), Line2D([0], [0], color="red", lw=2, label="exact amplitude amplification"), ] plt.legend(handles=legend_elements, loc="upper right") plt.xlim(-1, 1.6) plt.ylim(-1, 1.4) plt.show() ``` output # Grover Operator Source: https://docs.classiq.io/qmod-reference/library-reference/open-library-functions/grover_operator/grover_operator Open this notebook in GitHub to run it yourself The Grover operator is a unitary used in amplitude estimation and amplitude amplification algorithms [\[1\]](#1). The Grover operator is given by $$ Q = Q(A,\chi) = -AS_0A^{-1}S_\chi $$ where $A$ is a state preparation operator, $$ A|0 \rangle= |\psi \rangle $$ $S_\chi$ marks good states and is called an oracle, $$ S_\chi\lvert x \rangle = \begin{cases} -\lvert x \rangle & \text{if } \chi(x) = 1 \\ \phantom{-} \lvert x \rangle & \text{if } \chi(x) = 0 \end{cases} $$ and $S_0$ is a reflection about the zero state. $$ S_0 = I - 2|0\rangle\langle0| $$ Function: `grover_operator` Arguments: * `oracle: QCallable[QArray[QBit]]` * Oracle representing $S_{\chi}$, accepting quantum state to apply on. * `space_transform: QCallable[QArray[QBit]]` * State preparation operator $A$, accepting quantum state to apply on. * `packed_vars: QArray[QBit]` * Packed form of the variable to apply the grover operator on. # ## Example The following example implements a grover search algorithm using the grover operator for a specific oracle, with a uniform superposition over the search space. The circuit starts with a uniform superposition on the search space, followed by 2 applications of the grover operator. ```python theme={null} from classiq import * VAR_SIZE = 2 class GroverVars(QStruct): x: QNum[VAR_SIZE] y: QNum[VAR_SIZE] @qperm def my_predicate(vars: Const[GroverVars], res: QBit) -> None: res ^= (vars.x + vars.y < 4) & ((vars.x * vars.y) % 4 == 2) @qfunc def main(vars: Output[GroverVars]): allocate(vars) hadamard_transform(vars) power( 2, lambda: grover_operator( lambda vars: phase_oracle( predicate=my_predicate, target=vars, ), hadamard_transform, vars, ), ) qprog = synthesize( main, auto_show=True, constraints=Constraints(optimization_parameter="width") ) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3FmUIpJrkfaPVo4zCO3c6VT6jUL ``` And the next is a verification of the amplification of the solutions to the oracle: ```python theme={null} result = execute(qprog).result_value() df = result.dataframe df["predicate"] = (df["vars.x"] + df["vars.y"] < 4) & ( (df["vars.x"] * df["vars.y"]) % 4 == 2 ) df ``` | | vars.x | vars.y | counts | probability | bitstring | predicate | | -- | ------ | ------ | ------ | ----------- | --------- | --------- | | 0 | 1 | 2 | 985 | 0.480957 | 1001 | True | | 1 | 2 | 1 | 949 | 0.463379 | 0110 | True | | 2 | 2 | 2 | 13 | 0.006348 | 1010 | False | | 3 | 0 | 3 | 13 | 0.006348 | 1100 | False | | 4 | 3 | 3 | 13 | 0.006348 | 1111 | False | | 5 | 3 | 0 | 10 | 0.004883 | 0011 | False | | 6 | 2 | 3 | 9 | 0.004395 | 1110 | False | | 7 | 1 | 1 | 8 | 0.003906 | 0101 | False | | 8 | 0 | 2 | 8 | 0.003906 | 1000 | False | | 9 | 0 | 0 | 7 | 0.003418 | 0000 | False | | 10 | 1 | 0 | 7 | 0.003418 | 0001 | False | | 11 | 2 | 0 | 6 | 0.002930 | 0010 | False | | 12 | 0 | 1 | 6 | 0.002930 | 0100 | False | | 13 | 3 | 1 | 5 | 0.002441 | 0111 | False | | 14 | 3 | 2 | 5 | 0.002441 | 1011 | False | | 15 | 1 | 3 | 4 | 0.001953 | 1101 | False | ## References \[1] G. Brassard, P. Hoyer, M. Mosca, and A. Tapp, "Quantum Amplitude Amplification and Estimation," arXiv:quant-ph/0005055, vol. 305, pp. 53-74, 2002, doi: 10.1090/conm/305/05215. # Hadamard Transform Source: https://docs.classiq.io/qmod-reference/library-reference/open-library-functions/hadamard_transform/hadamard_transform Open this notebook in GitHub to run it yourself The Hadamard transform function applies an H gate on each qubit of the register inputted to the function. Function: `hadamard_transform` Arguments: * `target`: `QArray[QBit]` The `target` quantum argument is the quantum state on which we apply the Hadamard Transform. ## Example ```python theme={null} from classiq import * @qfunc def main(x: Output[QArray[QBit]]): allocate(3, x) hadamard_transform(x) ``` ```python theme={null} qprog = synthesize(main) ``` # Linear Pauli Rotations Source: https://docs.classiq.io/qmod-reference/library-reference/open-library-functions/linear_pauli_rotations/linear_pauli_rotations Open this notebook in GitHub to run it yourself This function performs a rotation on a series of $m$ target qubits, where the rotation angle is a linear function of an $n$-qubit control register, as follows: $$ \left|x\right\rangle _{n}\left|q\right\rangle _{m}\rightarrow\left|x\right\rangle _{n}\prod_{k=1}^{m}\left(\cos\left(\frac{a_{k}}{2}x+\frac{b_{k}}{2}\right)- i\sin\left(\frac{a_{k}}{2}x+\frac{b_{k}}{2}\right)P_{k}\right)\left|q_{k}\right\rangle $$ where $\left|x\right\rangle$ is the control register, $\left|q\right\rangle$ is the target register, each $P_{k}$ is one of the three Pauli matrices $X$, $Y$, or $Z$, and $a_{k}$, $b_{k}$ are the user given slopes and offsets, respectively. For example, the operation of a linear $Y$ rotation on a zero-input qubit is $$ \left|x\right\rangle _{n}\left|0\right\rangle \rightarrow\left|x\right\rangle _{n}\left( \cos\left(\frac{a}{2}x+\frac{b}{2}\right)\left|0\right\rangle +\sin\left(\frac{a}{2}x+\frac{b}{2}\right)\left|1\right\rangle \right) $$ Such a rotation can be realized as a series of controlled rotations as follows: $$ \left[R_{y}\left(2^{n-1}a\right)\right]^{x_{n-1}}\cdots \left[R_{y}\left(2^{1}a\right)\right]^{x_{1}} \left[R_{y}\left(2^{0}a\right)\right]^{x_{0}}R_{y}\left(b\right) $$ Function: `linear_pauli_rotations` Arguments: * `bases: CArray[int]` * List of Pauli Enums. * `slopes: CArray[float]` * Rotation slopes for each of the given Pauli bases. * `offsets: CArray[float]` * Rotation offsets for each of the given Pauli bases. * `x: QArray[QBit]` * Quantum state to apply the rotation based on its value. * `q: QArray[QBit]` * List of indicator qubits for each of the given Pauli bases. Notice that `bases`, `slopes`, `offset` and `q` should be of the same size. ## Example: Three Y Rotations Controlled by a 6-Qubit State This example generates a quantum program with a $6$-qubit control state and $3$ target qubits, acted upon by Y rotations with different slopes and offsets. ```python theme={null} from classiq import * NUM_STATE_QUBITS = 6 BASES = [Pauli.Y.value] * 3 OFFSETS = [0.1, 0.3, 0.33] SLOPES = [2.1, 1, 7.0] @qfunc def main(x: Output[QArray[QBit]], ind: Output[QArray[QBit]]): allocate(NUM_STATE_QUBITS, x) allocate(len(BASES), ind) linear_pauli_rotations(BASES, SLOPES, OFFSETS, x, ind) ``` ```python theme={null} qprog = synthesize(main) ``` # Quantum Sine and Cosine Transforms Source: https://docs.classiq.io/qmod-reference/library-reference/open-library-functions/qct_qst/qct_qst Open this notebook in GitHub to run it yourself The quantum Sine and Cosine transforms functions are the quantum analog for the discrete Sine and Cosine transforms. The **unitary** versions of the type I and type II transforms are defined as follows: $$ {\rm DCT}^{(1)}_{jk}(N) = \alpha_{jk}\sqrt{\frac{2}{N-1}} \cos\left(\frac{\pi j k}{N-1}\right), \qquad \alpha_{jk} = \left\{ \begin{array}{l l} \frac{1}{\sqrt{2}} & j = 0,N-1 ,\\ \frac{1}{\sqrt{2}} & k = 0,N-1 ,\\ 1 & \text{else} \end{array} \right., \qquad j,k = 0\dots,N-1 $$ $$ {\rm DST}^{(1)}_{jk}(N) = \sqrt{\frac{2}{N+1}} \sin\left(\frac{\pi j k}{N+1}\right), \qquad j,k = 0\dots,N-1 $$ $$ {\rm DCT}^{(2)}_{jk}(N) = \alpha_{jk}\sqrt{\frac{2}{N}} \cos\left(\frac{\pi (j+1/2) k }{N}\right), \qquad \alpha_{jk} = \left\{ \begin{array}{l l} \frac{1}{\sqrt{2}} & k = 0 ,\\ 1 & \text{else} \end{array} \right., \qquad j,k = 0\dots,N-1 $$ $$ {\rm DST}^{(2)}_{jk}(N) = \alpha_{jk} \sqrt{\frac{2}{N}} \sin\left(\frac{\pi (j+1/2) (k+1)}{N}\right), \qquad \alpha_{jk} = \left\{ \begin{array}{l l} \frac{1}{\sqrt{2}} & k = N-1 ,\\ 1 & \text{else} \end{array} \right., \qquad j,k = 0\dots,N-1 $$ The open library includes four functions, following the implementation in Ref. \[[1](#qcst)]: ## QCT and QST of Type I Function: `qct_qst_type1` Arguments: * `x`: `QArray[QBit]` The `x` quantum argument is the quantum state on which we apply the transforms, according to the following unitary on $n\equiv$`x.len` qubits: $$ \left( \begin{array}{ccc|c} {} &{} &{} \\ {}&{\rm DCT}^{(1)}(2^{n-1}+1) & {}& 0\\ {} &{} &{} \\ \hline {} & 0 & {} & i{\rm DST}^{(1)}(2^{n-1}-1) \end{array} \right) $$ # ## Example ```python theme={null} import numpy as np from classiq import * NUM_QUBITS = 4 execution_preferences = ExecutionPreferences( num_shots=1, backend_preferences=ClassiqBackendPreferences( backend_name=ClassiqSimulatorBackendNames.SIMULATOR_STATEVECTOR ), ) np.random.seed(123) cos_data = np.random.rand(2 ** (NUM_QUBITS - 1) + 1) cos_data = cos_data / np.linalg.norm(cos_data) sin_data = np.random.rand(2 ** (NUM_QUBITS - 1) - 1) sin_data = sin_data / np.linalg.norm(sin_data) combined_data = np.append(cos_data / np.sqrt(2), sin_data / np.sqrt(2)) ``` ```python theme={null} @qfunc def main(x: Output[QNum]): prepare_amplitudes(combined_data.tolist(), 0.0, x) qct_qst_type1(x) qmod = create_model(main, execution_preferences=execution_preferences) ``` ```python theme={null} qprog = synthesize(qmod) ``` ```python theme={null} result = execute(qprog).result_value() ``` ```python theme={null} qct_data = np.zeros(2 ** (NUM_QUBITS - 1) + 1).astype(complex) qst_data = np.zeros(2 ** (NUM_QUBITS - 1) - 1).astype(complex) for sample in result.parsed_state_vector: value = int(sample.state["x"]) if value < 2 ** (NUM_QUBITS - 1) + 1: qct_data[value] += sample.amplitude else: qst_data[int(value - 2 ** (NUM_QUBITS - 1) - 1)] += sample.amplitude ``` ```python theme={null} def dct1(n): dct = np.array( [ [ np.cos(np.pi * j * k / (n - 1)) * (np.sqrt(1 / 2) if j == 0 or j == n - 1 else 1) * (np.sqrt(1 / 2) if k == 0 or k == n - 1 else 1) for j in range(n) ] for k in range(n) ] ) / np.sqrt((n - 1) / 2) return dct def dst1(n): dst = np.array( [ [np.sin(np.pi * (j + 1) * (k + 1) / (n + 1)) for j in range(n)] for k in range(n) ] ) / np.sqrt((n + 1) / 2) return dst ``` ```python theme={null} global_phase = np.exp(1j * np.angle(qct_data[0])) measured_cos_res = np.real(qct_data / global_phase) expected_cos_res = (dct1(2 ** (NUM_QUBITS - 1) + 1) @ cos_data) / np.sqrt(2) print("measured result:", measured_cos_res) print("expected result:", expected_cos_res) assert np.allclose(measured_cos_res, expected_cos_res, atol=0.01) ``` **Output:** ``` measured result: [ 0.64936034 -0.13662084 0.02159456 0.08089956 0.06722088 0.18669631 0.02254746 -0.01198615 0.1123771 ] expected result: [ 0.64936034 -0.13662084 0.02159456 0.08089956 0.06722088 0.18669631 0.02254746 -0.01198615 0.1123771 ] ``` ```python theme={null} global_phase = np.exp(1j * np.angle(qst_data[0])) measured_sin_res = np.real(qst_data / global_phase) expected_sin_res = (dst1(2 ** (NUM_QUBITS - 1) - 1) @ sin_data) / np.sqrt(2) print("measured result:", measured_sin_res) print("expected result:", expected_sin_res) assert np.allclose(measured_sin_res, expected_sin_res, atol=0.01) ``` **Output:** ``` measured result: [ 0.57556991 0.04712138 0.22433696 -0.27513447 0.17796799 0.07685908 0.05378552] expected result: [ 0.57556991 0.04712138 0.22433696 -0.27513447 0.17796799 0.07685908 0.05378552] ``` ## QCT and QST of Type II Function: `qct_qst_type2` Arguments: * `x`: `QArray[QBit]`, * `q`: `QBit` The `x` quantum argument is the quantum state on which we apply the transforms, whereas the single `q` qubit indicates the block, according to the following unitary on $n+1\equiv$ `x.len` $+1$ qubits: $$ \left( \begin{array}{c|c} {\rm DCT}^{(2)}(2^{n-1}) & 0\\ \hline 0 & -{\rm DST}^{(2)}(2^{n-1}) \end{array} \right) $$ Function: `qct_type2` Arguments: * `x`: `QArray[QBit]`: the quantum state on which we apply ${\rm DCT}^{(2)}$. Function: `qst_type2` Arguments: * `x`: `QArray[QBit]`: the quantum state on which we apply ${\rm DST}^{(2)}$. # ## Example ```python theme={null} NUM_QUBITS = 4 cos_sin_data = np.random.rand(2 ** (NUM_QUBITS - 1)) cos_sin_data = cos_sin_data / np.linalg.norm(cos_sin_data) ``` ```python theme={null} @qfunc def main(x: Output[QNum], q: Output[QBit]): prepare_amplitudes(cos_sin_data.tolist(), 0.0, x) allocate(q) H(q) qct_qst_type2(x, q) qmod = create_model(main, execution_preferences=execution_preferences) ``` ```python theme={null} qprog = synthesize(qmod) ``` ```python theme={null} result = execute(qprog).result_value() ``` ```python theme={null} qct_data = np.zeros(2 ** (NUM_QUBITS - 1)).astype(complex) qst_data = np.zeros(2 ** (NUM_QUBITS - 1)).astype(complex) for sample in result.parsed_state_vector: if sample.state["q"] == 0: qct_data[int(sample.state["x"])] += sample.amplitude else: qst_data[int(sample.state["x"])] += sample.amplitude ``` ```python theme={null} def dct2(n): dct = np.array( [ [ np.cos(np.pi * j * (k + 1 / 2) / n) * (np.sqrt(1 / 2) if j == 0 else 1) for j in range(n) ] for k in range(n) ] ) / np.sqrt(n / 2) return dct.T def dst2(n): dst = np.array( [ [ np.sin(np.pi * (j + 1) * (k + 1 / 2) / n) * (np.sqrt(1 / 2) if j == n - 1 else 1) for j in range(n) ] for k in range(n) ] ) / np.sqrt(n / 2) return dst.T ``` ```python theme={null} global_phase = np.exp(1j * np.angle(qct_data[0])) measured_cos_res = np.real(qct_data / global_phase) expected_cos_res = (dct2(2 ** (NUM_QUBITS - 1)) @ cos_sin_data) / np.sqrt(2) print("measured result:", measured_cos_res) print("expected result:", expected_cos_res) assert np.allclose(measured_cos_res, expected_cos_res, atol=0.01) ``` **Output:** ``` measured result: [ 0.65104654 -0.23305325 -0.11473439 0.02595719 -0.04930425 0.03323495 0.06553173 0.01252819] expected result: [ 0.65104654 -0.23305325 -0.11473439 0.02595719 -0.04930425 0.03323495 0.06553173 0.01252819] ``` ```python theme={null} global_phase = np.exp(1j * np.angle(qst_data[0])) measured_sin_res = np.real(qst_data / global_phase) expected_sin_res = (dst2(2 ** (NUM_QUBITS - 1)) @ cos_sin_data) / np.sqrt(2) print("measured result:", measured_sin_res) print("expected result:", expected_sin_res) assert np.allclose(measured_sin_res, expected_sin_res, atol=0.01) ``` **Output:** ``` measured result: [ 0.63981132 -0.2180174 0.13530848 -0.08552637 0.02796936 -0.03450769 0.12370004 -0.01455965] expected result: [ 0.63981132 -0.2180174 0.13530848 -0.08552637 0.02796936 -0.03450769 0.12370004 -0.01455965] ``` ```python theme={null} @qfunc def main(x: Output[QNum]): prepare_amplitudes(cos_sin_data.tolist(), 0.0, x) qct_type2(x) qmod = create_model(main, execution_preferences=execution_preferences) qprog = synthesize(qmod) result = execute(qprog).result_value() qct_data = np.zeros(2 ** (NUM_QUBITS - 1)).astype(complex) for sample in result.parsed_state_vector: qct_data[int(sample.state["x"])] += sample.amplitude global_phase = np.exp(1j * np.angle(qct_data[0])) measured_cos_res = np.real(qct_data / global_phase) expected_cos_res = dct2(2 ** (NUM_QUBITS - 1)) @ cos_sin_data print("measured result:", measured_cos_res) print("expected result:", expected_cos_res) assert np.allclose(measured_cos_res, expected_cos_res, atol=0.01) ``` **Output:** ``` measured result: [ 0.92071884 -0.32958707 -0.16225893 0.03670901 -0.06972674 0.04700132 0.09267587 0.01771754] expected result: [ 0.92071884 -0.32958707 -0.16225893 0.03670901 -0.06972674 0.04700132 0.09267587 0.01771754] ``` ```python theme={null} @qfunc def main(x: Output[QNum]): prepare_amplitudes(cos_sin_data.tolist(), 0.0, x) qst_type2(x) qmod = create_model(main, execution_preferences=execution_preferences) qprog = synthesize(qmod) result = execute(qprog).result_value() qst_data = np.zeros(2 ** (NUM_QUBITS - 1)).astype(complex) for sample in result.parsed_state_vector: qst_data[int(sample.state["x"])] += sample.amplitude global_phase = np.exp(1j * np.angle(qst_data[0])) measured_sin_res = np.real(qst_data / global_phase) expected_sin_res = dst2(2 ** (NUM_QUBITS - 1)) @ cos_sin_data print("measured result:", measured_sin_res) print("expected result:", expected_sin_res) assert np.allclose(measured_sin_res, expected_sin_res, atol=0.01) ``` **Output:** ``` measured result: [ 0.90482984 -0.30832317 0.19135509 -0.12095255 0.03955464 -0.04880124 0.17493827 -0.02059046] expected result: [ 0.90482984 -0.30832317 0.19135509 -0.12095255 0.03955464 -0.04880124 0.17493827 -0.02059046] ``` ## References \[1]: [Klappenecker, A., & Rotteler M., "Discrete Cosine Transforms on Quantum Computers".](https://arxiv.org/abs/quant-ph/0111038) # Quantum Fourier Transform Source: https://docs.classiq.io/qmod-reference/library-reference/open-library-functions/qft/qft Open this notebook in GitHub to run it yourself The quantum Fourier transform (QFT) function is the quantum analog for discrete Fourier transform. It is applied on the quantum register state vector. The state vector `x` is transformed to `y` in the following manner: $$ y_{k} = \frac{1}{\sqrt{N}} \sum_{j=0}^{N-1} x_j e^{2\pi i \frac{jk}{N}} $$ Function: `qft` Arguments: * `target`: `QArray[QBit]` The `target` quantum argument is the quantum state on which we apply the QFT. ## Example ```python theme={null} from classiq import * @qfunc def main(x: Output[QArray[QBit]]): allocate(4, x) qft(x) ``` ```python theme={null} qprog = synthesize(main) ``` # Quantum Phase Estimation Source: https://docs.classiq.io/qmod-reference/library-reference/open-library-functions/qpe/qpe Open this notebook in GitHub to run it yourself The quantum phase estimation (QPE) function estimates the phase of an eigenvector of a unitary function. More precisely, given a unitary function $F$ and an input containing a quantum variable with a state $|\psi\rangle$ such that $F(|\psi\rangle)=e^{2\pi i\nu}|\psi\rangle$, the phase estimation function outputs an estimation of $\nu$ as a fixed-point binary number. Phase estimation is frequently used as a subroutine in other quantum algorithms such as Shor's algorithm and quantum algorithms for solving linear systems of equations (HHL algorithm). Theoretical details are in Ref. [\[1\]](#1). Function: `qpe` Arguments: * `unitary: QCallable` * The unitary operation for which the qpe estimation the eigenvalues * `phase: QNum` * The output of the qpe, holding the phase as a number in the range $[0, 1)$ Function: `qpe_flexible` The function is suitable when one wants to specialize the way the power of a unitary is defined, other than using the naive power. For example it can be used to obtain the time evolution of hamiltonians or for Shor's algorithm. Arguments: * `unitary_with_power: QCallable[CInt]` * Power of a unitary. Accepts as argument the power of the unitary to apply. * `phase: QNum` ## Examples # ## Example 1: QPE of a Function This example shows how to perform a simple phase estimation: 1. Initialize the state $|3\rangle$ over two qubits. 2. Apply a phase estimation on the the controlled-RZ gate, represeneted by the unitary matrix: $$ \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & e^{-i\frac{\lambda}{2}} & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & e^{i\frac{\lambda}{2}} \end{pmatrix} $$ The expected phase variable should encode $\frac{\lambda}{4\pi}$, the phase of the eigenvalue of the $|3\rangle$ state. Choosing $\lambda = \pi$, the expected result is $\frac{1}{4}$, represented in binary by `01`. ```python theme={null} from classiq import * from classiq.qmod.symbolic import pi QPE_RESOLUTION = 2 @qfunc def main( state: Output[QArray[QBit]], phase: Output[QNum[QPE_RESOLUTION, UNSIGNED, QPE_RESOLUTION]], ): allocate(phase) allocate(2, state) X(state[0]) X(state[1]) qpe(unitary=lambda: CRZ(pi, state[0], state[1]), phase=phase) qmod = create_model(main) ``` ```python theme={null} qprog = synthesize(qmod) ``` Show the actual results: ```python theme={null} result = execute(qprog).result_value() print("Results:", result.parsed_counts) ``` **Output:** ``` Results: [{'state': [1, 1], 'phase': 0.25}: 2048] ``` # ## Example 2: Flexible QPE The following examples will specifiy directly how to take powers in the QPE. The unitary function is `suzuki_trotter`, where the number of repetitions will be 1. In the case of diagonal hamiltonian it be exact exponentiation of the hamiltoian. Take the following matrix: $$ \begin{pmatrix} 0 & 0 & 0 & 0 \\ 0 & \tfrac{1}{4} & 0 & 0 \\ 0 & 0 & \tfrac{1}{2} & 0 \\ 0 & 0 & 0 & \tfrac{3}{4} \\ \end{pmatrix} $$ Represented by the hamiltonian: $H = -\frac{1}{8}Z_0I_1 - \frac{1}{4}I_0Z_1 + \frac{3}{8}I_0I_1$ ```python theme={null} QPE_RESOLUTION = 2 HAMILTONIAN = [ PauliTerm(pauli=[Pauli.I, Pauli.Z], coefficient=-0.125), PauliTerm(pauli=[Pauli.Z, Pauli.I], coefficient=-0.25), PauliTerm(pauli=[Pauli.I, Pauli.I], coefficient=0.375), ] @qfunc def main( state: Output[QArray[QBit]], phase: Output[QNum[QPE_RESOLUTION, UNSIGNED, QPE_RESOLUTION]], ): allocate(2, state) allocate(phase) hadamard_transform(state) qpe_flexible( lambda power: suzuki_trotter( HAMILTONIAN, evolution_coefficient=-2 * pi * (power), order=1, repetitions=1, qbv=state, ), phase, ) qmod = create_model(main) ``` ```python theme={null} qprog = synthesize(qmod) ``` Show the actual results: ```python theme={null} result = execute(qprog).result_value() print("Results:", result.parsed_counts) ``` **Output:** ``` Results: [{'state': [0, 0], 'phase': 0.0}: 554, {'state': [1, 1], 'phase': 0.75}: 517, {'state': [1, 0], 'phase': 0.25}: 516, {'state': [0, 1], 'phase': 0.5}: 461] ``` ## References \[1] A. Yu. Kitaev Barenco et al, Quantum Measurements and the Abelian Stabilizer Problem, (1995). [https://doi.org/10.48550/arXiv.quant-ph/9511026](https://doi.org/10.48550/arXiv.quant-ph/9511026) # QSVT Source: https://docs.classiq.io/qmod-reference/library-reference/open-library-functions/qsvt/qsvt Open this notebook in GitHub to run it yourself The Quantum Singlar Value Transformation (QSVT) [\[1\]](#qsvt) is an algorithmic framework, used to apply polynomial transformation on the singular values of a block encoded matrix. It has wide range of applications such as matrix inversion, amplitude amplification and hamiltonian simulation. Given a unitary $U$, a list of phase angles $\phi_1, \phi_2, ..., \phi_{d+1}$ and 2 projector-controlled-not operands $C_{\Pi}NOT,C_{\tilde{\Pi}}NOT$ , the QSVT sequence is as follows: $$ \tilde{\Pi}_{\phi_{d+1}}U \prod_{k=1}^{(d-1)/2} (\Pi_{\phi_{d-2k}} U^{\dagger}\tilde{\Pi}_{\phi_{d - (2k+1)}}U)\Pi_{\phi_{1}} $$ for odd $d$, and: $$ \prod_{k=1}^{d/2} (\Pi_{\phi_{d-(2k-1)}} U^{\dagger}\tilde{\Pi}_{\phi_{d-2k}}U)\Pi_{\phi_{1}} $$ for even $d$. Each of the projector-controlled-phase unitaries $\Pi$ consists of a $Z$ rotation of an auxilliary qubit wrapped by the $C_{\Pi}NOT$s, NOTing the auxilliary qubit: $$ \Pi_{\phi} = (C_{\Pi}NOT) e^{-i\frac{\phi}{2}Z}(C_{\Pi}NOT) $$ The transformation will result with a polynomial of order $d$. Function: `qsvt` Arguments: * `phase_seq: CArray[CReal]` - a $d+1$ sized sequence of phase angles. * `proj_cnot_1: QCallable[QArray[QBit], QBit]` - projector-controlled-not unitary that locates the encoded matrix columns within $U$. Accepts quantum variable of the size of `qvar`, and a qubit that is set to $|1\rangle$ when the state is in the block. * `proj_cnot_2: QCallable[QArray[QBit], QBit]` - projector-controlled-not unitary that locates the encoded matrix rows within $U$. Accepts quantum variable of the size of `qvar`, and a qubit that is set to $|1\rangle$ when the state is in the block. * `u: QCallable[QArray[QBit]]` - $U$ a block encoding unitary of a matrix $A$, such that $A = \tilde{\Pi}U\Pi$. * `qvar: QArray[QBit]` - the quantum variable on which $U$ applies, which resides in the entire block encoding space. * `aux: QBit` - a zero auxilliary qubit, used for the projector-controlled-phase rotations. Given as an input so that qsvt can be used as a building-block in a larger algorithm. ```python theme={null} !pip install -qq "classiq[qsp]" ``` # ## Example: Polynomial Transformation on a $\sqrt(x)$ Block Encoding The following example implements a random polynomial transformation on a given block, based on [\[2\]](#qsvt-derivative). The unitary $U$ here is a square-root transformation: $U|x\rangle_n|0\rangle_{n+1} = |x\rangle_n(\sqrt{x}|\psi_0\rangle_{n}|0\rangle + \sqrt{1-x}|\psi_1\rangle_{n}|1\rangle)$ where $x$ is a fixed-point variable in the range $[0, 1)$. The example samples a random odd-polynomial, calculates the necessary phase sequence, then applies the qsvt and verifies the results. There are 2 distinct projector-controlled-not unitaries - one is applying on the entire $(n+1)$ variable, and the second is on the 1-qubits auxilliary in the image. ```python theme={null} from typing import Dict, Tuple import numpy as np from numpy.polynomial import Polynomial from classiq import * from classiq.qmod.symbolic import logical_and NUM_QUBITS = 4 @qfunc def u_sqrt(state: QNum, ref: QNum, ind: QBit) -> None: hadamard_transform(ref) ind ^= state <= ref @qfunc def qsvt_sqrt_polynomial( qsvt_phases: list[float], state: QNum, ref: QNum, ind: QBit, qsvt_aux: QBit ) -> None: qsvt( qsvt_phases, lambda _aux: inplace_xor(ref == 0, _aux), lambda _aux: inplace_xor(ind == 0, _aux), lambda: u_sqrt(state, ref, ind), qsvt_aux, ) ``` ```python theme={null} import matplotlib.pyplot as plt from classiq.applications.qsp import qsvt_phases def sample_random_chebyshev_polynomial(degree): # Generate random coefficients coefficients = np.random.uniform(-1, 1, degree + 1) # take care for parity coefficients[int(degree + 1) % 2 :: 2] = 0 # Create the polynomial p = np.polynomial.Chebyshev(coefficients) # Evaluate the polynomial over the interval [-1, 1] x = np.linspace(-1, 1, 500) y = p(x) # Normalize the polynomial poly = p / (np.max(np.abs(y)) + 0.001) print(poly) return poly def parse_qsvt_results(result) -> Tuple[np.ndarray, np.ndarray]: parsed_state_vector = result.parsed_state_vector d: Dict = {x: [] for x in range(2**NUM_QUBITS)} for parsed_state in parsed_state_vector: if ( parsed_state["qsvt_aux"] == 0 and parsed_state["ind"] == 0 and np.linalg.norm(parsed_state.amplitude) > 1e-15 and (DEGREE % 2 == 1 or parsed_state["ref"] == 0) ): d[parsed_state["state"]].append(parsed_state.amplitude) d = {k: np.linalg.norm(v) for k, v in d.items()} values = [d[i] for i in range(len(d))] x = np.sqrt(np.linspace(0, 1 - 1 / (2**NUM_QUBITS), 2**NUM_QUBITS)) measured_poly_values = np.sqrt(2**NUM_QUBITS) * np.array(values) target_poly_values = np.abs(POLY(x)) plt.scatter(x, measured_poly_values, label="measured", c="g") plt.plot(x, target_poly_values, label="target") plt.xlabel(r"$\sqrt{x}$") plt.ylabel(r"$P(\sqrt{x})$") plt.legend() return measured_poly_values, target_poly_values ``` ```python theme={null} DEGREE = 5 np.random.seed(1) # choosing in purpose odd polynomial POLY = sample_random_chebyshev_polynomial(DEGREE) QSVT_PHASES = qsvt_phases(POLY.coef) ``` **Output:** ``` 0.0 + 0.33582085·T₁(x) + 0.0·T₂(x) - 0.30128672·T₃(x) + 0.0·T₄(x) - 0.62136169·T₅(x) ``` ```python theme={null} @qfunc def main( state: Output[QNum[NUM_QUBITS]], ref: Output[QNum[NUM_QUBITS]], ind: Output[QBit], qsvt_aux: Output[QBit], ) -> None: allocate(state) allocate(ref) allocate(ind) allocate(qsvt_aux) hadamard_transform(state) qsvt_sqrt_polynomial(QSVT_PHASES, state, ref, ind, qsvt_aux) qmod = create_model( main, constraints=Constraints(optimization_parameter="width"), execution_preferences=ExecutionPreferences( num_shots=1, backend_preferences=ClassiqBackendPreferences( backend_name="simulator_statevector" ), ), ) qprog = synthesize(qmod) ``` ```python theme={null} show(qprog) ``` **Output:** ``` Quantum program link: https://platform.classiq.io/circuit/3FmW1eMFbXPFRVnq1mtPbK1wBSc ``` ```python theme={null} result = execute(qprog).result_value() measured, target = parse_qsvt_results(result) assert np.allclose(measured, target, atol=0.02) ``` output ## References \[1]: András Gilyén, Yuan Su, Guang Hao Low, and Nathan Wiebe. 2019. Quantum singular value transformation and beyond: exponential improvements for quantum matrix arithmetics. In Proceedings of the 51st Annual ACM SIGACT Symposium on Theory of Computing (STOC 2019). Association for Computing Machinery, New York, NY, USA, 193-204 [https://doi.org/10.1145/3313276.3316366](https://doi.org/10.1145/3313276.3316366). \[2]: Stamatopoulos, Nikitas, and William J. Zeng. "Derivative pricing using quantum signal processing." arXiv preprint arXiv:2307.14310 (2023). # Bell State Preparation Source: https://docs.classiq.io/qmod-reference/library-reference/open-library-functions/special_state_preparations/prepare_bell_state Open this notebook in GitHub to run it yourself The `prepare_bell_state` function creates one of the four Bell states $$ \phi_{+} = \frac{1}{2} \left(|00\rangle + |11\rangle \right) $$ $$ \phi_{-} = \frac{1}{2} \left(|00\rangle - |11\rangle \right) $$ $$ \psi_{+} = \frac{1}{2} \left(|01\rangle + |10\rangle \right) $$ $$ \psi_{-} = \frac{1}{2} \left(|01\rangle - |10\rangle \right) $$ Function: `prepare_bell_state` Arguments: * `state_num: CInt` * `q: Output[QArray[QBit]]` ## Example The `prepare_bell_state` function creates $$ \psi_{+} = \frac{1}{2} \left(|01\rangle + |10\rangle \right) $$ `state_num` is set to 2 ```python theme={null} from classiq import * @qfunc def main(x: Output[QArray[QBit]]): prepare_bell_state(2, x) ``` ```python theme={null} qprog = synthesize(main) ``` # Exponential State Preparation Source: https://docs.classiq.io/qmod-reference/library-reference/open-library-functions/special_state_preparations/prepare_exponential_state Open this notebook in GitHub to run it yourself The `prepare_exponential_state` function creates a state with exponentially decreasing amplitudes. Namely, the probability for a state representing an integer $n$ is $$ P\left(n\right) = \frac{1}{Z} e^{-\lambda n} $$ where $\lambda$ is the rate, and $Z$ is a normalization factor. If $q$ in the number of qubits, then $$ Z = \sum_{n=0} ^{n = 2^q - 1} e^{-\lambda n} = \frac{1 - e^{-\lambda 2^q}}{1 - e^{-\lambda}} $$ Function: `prepare_exponential_state` Arguments: * `rate: CReal` * `q: QArray[QBit]` Notice that the function acts inplace on the qubits. ## Example Prepare a state with probabilities: $$ P\left(n\right) = \frac{1}{Z} e^{-0.1 n} $$ where $n$ is in the range $[0, 31]$. ```python theme={null} from classiq import * @qfunc def main(x: Output[QArray[QBit]]): allocate(5, x) prepare_exponential_state(0.1, x) ``` ```python theme={null} qprog = synthesize(main) ``` # GHZ State Preparation Source: https://docs.classiq.io/qmod-reference/library-reference/open-library-functions/special_state_preparations/prepare_ghz_state Open this notebook in GitHub to run it yourself Use the `prepare_ghz_state` function to create a Greenberger-Horne-Zeilinger (GHZ) state. i.e., a balanced superposition of all ones and all zeros, on an arbitrary number of qubits. ## Syntax Function: `prepare_ghz_state` Arguments: * `size: CInt` * `q: Output[QArray[QBit]]` ## Example `prepare_ghz_state` on 5 qubits ```python theme={null} from classiq import * @qfunc def main(x: Output[QArray[QBit]]): prepare_ghz_state(5, x) ``` ```python theme={null} qprog = synthesize(main) ``` # Partial Uniform State Preparations Source: https://docs.classiq.io/qmod-reference/library-reference/open-library-functions/special_state_preparations/prepare_partial_uniform_state Open this notebook in GitHub to run it yourself The functions `prepare_uniform_trimmed_state` and `prepare_uniform_interval_state` create states with uniform superposition over a discrete interval of the possible states. Both scale polynomially with the number of qubits. ## Uniform Trimmed State Function: `prepare_uniform_trimmed_state` Arguments: * `m: CInt` - number of states to load. * `q: QArray[QBit]` - quantum variable to load the state into. The function loads the following superposition: $$ |\psi\rangle = \frac{1}{\sqrt{m}}\sum_{i=0}^{m-1}{|i\rangle} $$ # ## Example Prepare the following state on a variable of size 4 qubits.: $$ |\psi\rangle = \frac{1}{\sqrt{3}}\sum_{i=0}^{2}{|i\rangle} $$ ```python theme={null} import matplotlib.pyplot as plt from classiq import * @qfunc def main(x: Output[QNum]): allocate(4, x) prepare_uniform_trimmed_state(3, x) qmod = create_model(main) qprog = synthesize(qmod) ``` ```python theme={null} result = execute(qprog).result_value() counts = result.parsed_counts ``` ```python theme={null} plt.figure(figsize=(4, 3)) plt.bar( [c.state["x"] for c in counts], [c.shots for c in counts], color="skyblue", edgecolor="black", ) plt.xlabel("state") plt.ylabel("shots") ``` **Output:** ``` Text(0, 0.5, 'shots') ``` output ## Uniform Interval State Function: `prepare_uniform_interval_state` Arguments: * `start: CInt` - first state to be loaded. * `end: CInt` - boundary of the loaded states (not including). * `q: QArray[QBit]` - quantum variable to load the state into. The function loads the following superposition: $$ |\psi\rangle = \frac{1}{\sqrt{end-start}}\sum_{i=start}^{end-1}{|i\rangle} $$ # ## Example Prepare the following state on a variable of size 5 qubits.: $$ |\psi\rangle = \frac{1}{\sqrt{6}}\sum_{i=2}^{7}{|i\rangle} $$ ```python theme={null} @qfunc def main(x: Output[QNum]): allocate(5, x) prepare_uniform_interval_state(2, 8, x) qmod = create_model(main) qprog = synthesize(qmod) ``` ```python theme={null} result = execute(qprog).result_value() counts = result.parsed_counts ``` ```python theme={null} plt.figure(figsize=(5, 3)) plt.bar( [c.state["x"] for c in counts], [c.shots for c in counts], color="skyblue", edgecolor="black", ) plt.xlabel("state") plt.ylabel("shots") ``` **Output:** ``` Text(0, 0.5, 'shots') ``` output # Variational Data Encoding Source: https://docs.classiq.io/qmod-reference/library-reference/open-library-functions/variational_data_encoding/variational_data_encoding Open this notebook in GitHub to run it yourself Encoding classical data on quantum states is an important subroutine in variational quantum circuits, such as Quantum Singular Vector Machine (QSVM) and Quantum Neural Networks (QNN). ## Encode in Angle This function encodes $n$ data points on $n$ qubits, mapping the data point $x_i$ to a RY rotation on the $i$-th qubit with a $\pi x_i$ angle. Function: `encode_in_angle` Arguments: * `data`: `CArray[Creal]` * `qba`: `Output[QArray[QBit]]` The `qba` quantum argument is the quantum state on which we encode the classical array `data`. ## Example ```python theme={null} from classiq import * @qfunc def main(data: CArray[CReal, 4], x: Output[QArray[QBit]]): encode_in_angle(data, x) qmod = create_model(main) ``` ```python theme={null} from classiq import synthesize qprog = synthesize(qmod) ``` png ## Encode on Bloch This function encodes $n$ data points on $\lceil n/2 \rceil$, mapping pairs of data points $(x_{2i}, x_{2i+1})$ to the bloch sphere via RX rotation with an angle $\pi x_{2i}$ followed by a RZ rotation with an angle $\pi x_{2i+1}$. If the number of data points is odd then a single RX rotation is applied to the last qubit, with an angle of $2\pi x_n$. Function: `encode_on_bloch` Arguments: * `data`: `CArray[Creal]` * `qba`: `Output[QArray[QBit]]` The `qba` quantum argument is the quantum state on which we encode the classical array `data`. ## Example ```python theme={null} from classiq import * @qfunc def main(data: CArray[CReal, 7], x: Output[QArray[QBit]]): encode_on_bloch(data, x) qmod = create_model(main) ``` ```python theme={null} from classiq import synthesize qprog = synthesize(qmod) ``` png # Release News: July 2026 Source: https://docs.classiq.io/release-news/recap-july-2026 Five versions released in July, led by two headline features: a new architecture that makes synthesis significantly faster, and predefined hardware benchmarking across the IDE and SDK. A faster synthesis architecture, and predefined quantum hardware benchmarks A new QNN layer, language ergonomics, and direct session configuration Benchmarking and QNN guides, plus three library notebooks Correctness fixes and API deprecations ## A Faster, Scalable Synthesis Architecture Synthesizing Qmod code (in the IDE or via the SDK's [`synthesize`](/sdk-reference/synthesis#synthesize)) now returns a **Qmod-based executable** representation rather than a QASM circuit. This representation is a subset of Qmod: it captures the concrete implementations and resource allocations chosen for the high-level constructs in the source, while preserving the function hierarchy and symbolic control flow. Compilation is therefore **significantly faster**, cutting synthesis runtime by up to 10x, and scales better to larger problems and algorithms. The quantum program visualization is generated directly from this representation. Transpilation no longer occurs at synthesis. To restore the old flow, in which transpilation occurs during synthesis, set `compatibility_mode=True` in the [synthesis preferences](/sdk-reference/synthesis#preferences). ### Exporting to Target Languages After synthesizing high-level Qmod code into a concrete gate-level quantum program, the [**`export`**](/sdk-reference/synthesis#export) function, or the export menu in the Classiq IDE, transpiles it into a circuit in a target language of your choice: OpenQASM 2.0, OpenQASM 3.0, QIR, Cirq JSON, or Q#. The export menu in the Classiq IDE: a dropdown from the download button in the top bar listing the available target languages By default no further transpilation is applied; pass `transpilation_config=True` in the SDK (see example below) to reuse the transpilation level, backend, gate set, and connectivity map defined in the [synthesis preferences](/sdk-reference/synthesis#preferences), or an explicit [`TranspilationConfig`](/sdk-reference/synthesis#transpilationconfig) object to override them. For a full explanation of transpilation and the available levels, see the [Quantum Program Transpilation](/user-guide/synthesis/quantum-program-transpilation) guide. **Exporting a synthesized program to OpenQASM 3 with a specific gate set and connectivity:** [comment]: DO_NOT_TEST ```python theme={null} # synthesis preferences: a target gate set and qubit connectivity synthesis_preferences = Preferences( custom_hardware_settings=CustomHardwareSettings( basis_gates=["cx", "rz", "sx", "x"], connectivity_map=[(0, 1), (1, 2), (2, 3)], ) ) qprog = synthesize(main, preferences=synthesis_preferences) # transpilation_config=True reuses the gate set and connectivity from the synthesis preferences qasm = export(qprog, target_language=TargetLanguage.QASM3, transpilation_config=True) ``` [**Execution**](/user-guide/execution/index), by contrast, transpiles the program for the backend you run on; the backend and transpilation level you set for that execution override the synthesis preferences, if there is a conflict. ### Fault-Tolerant Export Exporting a program to a fault-tolerant circuit with a Clifford+T gate set is now **faster and more accurate**, driven by the [gridsynth](https://github.com/quantum-programming/pygridsynth) algorithm rather than the Solovay-Kitaev method used previously. Passing a [`FaultTolerantTranspilationConfig`](/sdk-reference/synthesis#faulttoleranttranspilationconfig) object to [`export`](/sdk-reference/synthesis#export) (instead of a regular [`TranspilationConfig`](/sdk-reference/synthesis#transpilationconfig)) activates fault-tolerant transpilation. Its `clifford_t_approximation_error` parameter sets the error threshold for approximating the RZ rotations into Clifford+T gates, where a lower threshold gives a more accurate but deeper circuit. ### Circuit Metrics Two new functions now report a program's resource estimates: [`get_circuit_metrics`](/sdk-reference/synthesis#get_circuit_metrics) returns the logical width, depth, and gate counts of the Qmod-based executable, without transpilation. [`get_transpiled_circuit_metrics`](/sdk-reference/synthesis#get_transpiled_circuit_metrics) transpiles the program using the synthesis preferences first, then returns the same counts for the resulting hardware-targeted circuit. ## Hardware Benchmarking The new hardware benchmarking suite scores how accurately a backend runs standard quantum algorithms, and lets you compare across different hardware providers. To get started, open the Execution page within the Classiq IDE and switch to the Benchmark tab, then pick one of the available [predefined benchmarks](/user-guide/execution/benchmarking/predefined-benchmarks): GHZ, Adder, QFT, State Preparation, or Dynamical Localization. Configuring a GHZ benchmark on the Execution page: selecting simulators and hardware backends on the left, with problem-size range, shots, and per-backend Emulate and Run Via Classiq options on the right At the bottom of the page, a panel describes the benchmark you chose and explains how it is scored. Set the range of problem sizes to sweep as min / max / step, select the backends, and set the shots per job; each backend has the option to be [run via Classiq](/user-guide/execution/budget-management) on an allocated budget, so no provider credentials are needed, and to be emulated against the device's noise model instead of the physical QPU. Up to 10 backends can be selected, spanning QPUs, hardware emulators, and simulators, and a session can include up to 15 jobs, one per backend per problem size, so the number of backends times the number of problem sizes must not exceed 15. Click Run to submit, and the results come back as a table of backend scores by problem size and a chart plotting them all together. The benchmark Jobs page: a score-versus-problem-size chart comparing the Classiq simulator against two IBM devices, with a per-job table of scores, cost, and status below Each job receives a score between 0 and 1, where 1 means the measured outcomes match the noise-free ideal and lower values reflect hardware noise and error. For a full walkthrough, see the [Benchmarking in the Classiq IDE](/user-guide/execution/benchmarking/predefined-benchmarks#benchmarking-in-the-classiq-ide) section of the Predefined Benchmarks user guide. Benchmarks can also be run through the Python SDK, where [`run_benchmark`](/sdk-reference/execution#run_benchmark) takes a [`BenchmarkRequest`](/sdk-reference/execution#benchmarkrequest) and returns a [`BenchmarkSession`](/sdk-reference/execution#benchmarksession) to poll for results. A dedicated SDK benchmarking package is on the way, adding support for custom benchmarks alongside the predefined ones. It is expected in the upcoming months. ## Enhancements ### New QLayer Interface for QNN The new [`QLayerV2`](/user-guide/applications/qml/qnn/qlayerv2) is a cleaner, faster quantum layer for training [quantum neural networks](/user-guide/applications/qml/qnn/qnn). It accepts a synthesized quantum program and an optional list of [Pauli observables](/user-guide/modeling/observables-and-operators) (see example below), and produces a `torch` module with one output feature per observable, with no `execute` or `post_process` function to write. Its gradients use adjoint differentiation, so they are exact rather than finite-difference estimates, and training is much faster. [comment]: DO_NOT_TEST ```python theme={null} from classiq import Pauli from classiq.applications.qnn import QLayerV2 layer = QLayerV2( # a synthesized QuantumProgram prog, # optional observables to estimate; one output feature each (two in this case) observables=[Pauli.Z(0), Pauli.X(1) + Pauli.Z(2)], ) ``` `QLayerV2` is set to supersede the existing [`QLayer`](/user-guide/applications/qml/qnn/qlayer) (now exported as `QLayerV1`) as the canonical interface. For a comparison and guidance on when to use each, see [Choosing between the two layers](/user-guide/applications/qml/qnn/qlayerv2#choosing-between-the-two-layers). `QLayerV2` is Studio-only for now, with support outside Studio expected in the coming months. ### Language Ergonomics Several small changes cut friction when writing in Qmod: * New classical functions `range_` and `reversed_`, mirroring Python's `range` and `reversed`, are now available. They operate on Qmod classical variables within classical expressions. * The `subscript` and `slice_` [path operators](/sdk-reference/qmod/path-operators) can now be imported directly from the top-level `classiq` package (`from classiq import *`), and no longer need an explicit import. * Declaring a local quantum variable as a `QArray` or `QNum` now requires only its defining arguments, where a name was previously required as well. ### Direct Execution Session Configuration Continuing the move toward flatter configuration, [`ExecutionSession`](/user-guide/execution/ExecutionSession) now accepts the advanced constructor arguments `noise_properties`, `amplitude_threshold`, `include_zero_amplitude_outputs`, and `job_name` directly, matching fields previously reachable only through [execution preferences](/sdk-reference/execution#executionpreferences). ### Backend Modality Data The DataFrame returned by [`get_backend_details`](/user-guide/execution/index#step-2-—-inspect-available-backends) now includes a `modality` column, letting you filter and compare backends by modality when deciding where to execute your quantum program. The reported modalities are `superconducting`, `trapped-ion`, `simulator-cpu`, and `simulator-gpu`. ### Optional Execution Progress Logging A `verbose` parameter was added to the execution functions and `ExecutionSession` methods, including [`sample`](/sdk-reference/execution#sample), [`observe`](/sdk-reference/execution#observe), [`variational_minimize`](/sdk-reference/execution#variational_minimize), and [`calculate_state_vector`](/sdk-reference/execution#calculate_state_vector). It allows the progress logging to be turned off (the "Submitting job to..." and "Job: ..." logs) by setting `verbose=False` (defaults to `True`). This is useful for repeated calls within a loop. ## Documentation Additions * **[Hardware Benchmarking](/user-guide/execution/benchmarking) Guide:** Covering the new functional-level benchmarking suite, the different predefined benchmarks, how each is scored, and how to run them. For more information, see the [Hardware Benchmarking](#hardware-benchmarking) section above. * **[QLayerV2](/user-guide/applications/qml/qnn/qlayerv2) Guide:** Covering the streamlined `QLayerV2` layer for hybrid QNNs, its interface and observables, worked examples, and how to choose between it and the existing [`QLayer`](/user-guide/applications/qml/qnn/qlayer). For more information, see the [New QLayer Interface for QNN](#new-qlayer-interface-for-qnn) section above. ## Library Additions * **[Projection-Based Embedding](https://github.com/Classiq/classiq-library/blob/main/applications/chemistry/projection_based_embedding/projected_based_embedding_tutorial.ipynb):** A notebook demonstrating the [Wavefunction-in-DFT Embedding](/release-news/recap-june-2026#wavefunction-in-dft-embedding) capability added in June, putting the [`EmbeddingCalculator`](/sdk-reference/applications/chemistry#embeddingcalculator) to use on a water molecule. It treats a chemically active fragment with a quantum method such as VQE while describing the surrounding environment with DFT, then recombines the two to recover the total energy. * **[Numerical Gradient Estimation](https://github.com/Classiq/classiq-library/blob/main/algorithms/quantum_primitives/gradient_estimation/gradient_estimation.ipynb):** Implementing Jordan's algorithm to estimate the gradient of a d-dimensional scalar function with a single quantum query, in place of the d+1 evaluations a classical finite-difference approach would require. * **[Quantum Likelihood Estimation](https://github.com/Classiq/classiq-library/blob/main/algorithms/search_and_optimization/quantum_likelihood_estimation/quantum_likelihood_estimation.ipynb):** A hybrid algorithm for Hamiltonian learning that identifies an unknown Hamiltonian, accessible only through the time evolution it generates, from a candidate set. It implements the optimal variant, developed with the paper's authors, which jointly optimizes all five circuit parameters each iteration to make every measurement maximally informative. It converges in roughly an order of magnitude fewer experiments than standard QLE. ## Bug Fixes * **Synthesis now catches unsafe qubit-layout changes in control-flow blocks:** reordering a quantum variable's qubits inside a `power`, `repeat`, `foreach`, or `if` block is now rejected up front with a descriptive error. * **Classical path operators return correctly-typed results:** `subscript`, `slice_`, and `reversed_` in the Python SDK now correctly infer their result type from their argument. * **`observe` no longer emits a spurious deprecation warning:** calling the [`observe`](/sdk-reference/execution#observe) function now runs cleanly, after previously surfacing a misplaced `submit_estimate() is deprecated` warning. * **Quantum variable constructors reject non-string names cleanly:** passing a non-string name to `QArray`, `QNum`, `QBit`, or `QStruct` now raises a clear error, where it previously failed internally. ## Deprecations * **`QuantumProgram.qasm` and `transpiled_circuit` deprecated:** following the synthesis architecture update, these `QuantumProgram` attributes are replaced by [`export`](/sdk-reference/synthesis#export) for a QASM circuit and [`get_circuit_metrics`](/sdk-reference/synthesis#get_circuit_metrics) or [`get_transpiled_circuit_metrics`](/sdk-reference/synthesis#get_transpiled_circuit_metrics) for resource estimation, as covered in the [synthesis architecture](#a-faster-scalable-synthesis-architecture) section above. * **`execute_qnn` accepted observable type change:** `execute_qnn` now takes its observable as a `SparsePauliOp`; passing the deprecated `PauliOperator` is converted automatically, and emits a `DeprecationWarning`. * **`slice` Qmod operator renamed to `slice_`:** calling `slice` now emits a deprecation warning; use `slice_` instead. For the technical entries behind these changes, see the [Changelog](/release-notes). # Release News: June 2026 Source: https://docs.classiq.io/release-news/recap-june-2026 Five versions released in June. Two ways to build with AI headline the month: a Quantum Engineer plugin that brings Classiq into Claude, Cursor, and Codex, and a no-code Assistant on the platform. A Quantum Engineer plugin for AI coding agents, and a Classiq platform assistant GPU simulation, molecular embedding, streamlined remote execution, and more New user guides for the AI capabilities and M2M authentication Correctness fixes and one function removal ## AI with Classiq Two separate AI capabilities shipped this month; if you work locally, the Quantum Engineer plugin enables your coding agent to model, synthesize, execute, and analyze quantum programs with Classiq. If you work on the platform, the Classiq Assistant turns a description into a synthesized, executable program, and answers your questions along the way. For further reading, refer to the [Classiq blog](https://www.classiq.io/insights/classiqs-quantum-engineering-agents-now-in-your-ide-and-the-platform). ### Quantum Engineer Plugin The plugin draws its capability from a set of **Classiq skills** paired with the **Classiq MCP**: it writes in Qmod (through the Classiq SDK), builds on validated components from the Classiq Library, and synthesizes on Classiq's backend engine to produce optimized circuits. That grounding is what lets it pull ahead on hard work, such as reproducing paper implementations, research, and multi-step applications you refine over time. The plugin is available to Classiq's enterprise customers. For more information, including a full breakdown of the skills for each phase of quantum algorithm development, see the [Quantum Engineer Plugin](/user-guide/ai/quantum-engineer-plugin) user guide. To request access, [contact us](https://www.classiq.io/contact-us). ### Classiq Assistant (Beta) The new Classiq Assistant, built within the Classiq platform, takes you from an idea to a working quantum program. It accepts natural-language descriptions or circuit images, builds a Qmod-native model, synthesizes it, and opens the result on the Quantum Program page. You start from the new text input box at the center of the home page, with no prior installation or configuration needed: Classiq Assistant input box on the platform home page From there, the assistant follows you onto the Quantum Program page, opening as a side panel: Classiq Assistant panel on the Quantum Program page It keeps the context of your session, attaching the circuit you currently have open to the conversation, so you can ask it to explain what the program does, predict what results to expect, or refine the model, all in one continuous flow. It also answers conceptual questions about quantum computing, and a new chat starts fresh whenever you move to a different problem. When the program is ready, Execute takes it straight to a backend. For more information, see the [Classiq Assistant](/user-guide/ai/classiq-assistant) user guide. ## Enhancements ### NVIDIA DGX Statevector Simulator Statevector simulations can now run on a GPU-accelerated backend hosted on an NVIDIA DGX system. Selected by passing `backend="classiq/dgx_simulator"`, it handles up to **35 qubits** in single precision (float32) across both **sampling** and **statevector** modes. Exact simulation at this scale lets you validate larger algorithms, so you can catch errors in the logic before spending resources running on real hardware. Access requires specific license permissions; see [DGX Statevector Simulator](/user-guide/execution/cloud-providers/classiq-backends#dgx-statevector-simulator). ### Wavefunction-in-DFT Embedding Simulating a full molecule at quantum accuracy quickly outgrows the qubit budget of current hardware, yet most of the chemistry that matters happens in a small, chemically active region of it. The new [`EmbeddingCalculator`](/sdk-reference/applications/chemistry#embeddingcalculator) class runs projection-based embedding to exploit exactly that: it partitions a molecular system into an active fragment, treated with a quantum algorithm such as VQE, and a surrounding environment described by DFT. The calculator drives the full DFT-and-embedding pipeline server-side and returns Hamiltonians ready to hand to a quantum solver. A notebook walking through a complete example is coming in July. ### Streamlined Remote Execution Two changes cut the job and queueing overhead of running on remote cloud hardware: * **Hosted optimization on [IonQ](/user-guide/execution/cloud-providers/ionq-backends):** The [`variational_minimize`](/sdk-reference/execution#variational_minimize) variants (standalone function and `ExecutionSession` methods), used for variational algorithms such as VQE or QAOA, now accept `hosted=True` to run the entire optimization loop on IonQ Hosted Hybrid as a single Classiq execution job. Previously each optimizer iteration was submitted as its own Classiq sample job, so the job count grew with the optimization; now the SDK submits and polls that one job, while the Classiq backend handles all communication with IonQ. Code Example: [comment]: DO_NOT_TEST ```python theme={null} with ExecutionSession(qprog, backend="ionq/...") as es: result = es.variational_minimize(hamiltonian, initial_params, hosted=True) ``` Hosted mode has specific configuration requirements; see [`variational_minimize`](/sdk-reference/execution#variational_minimize) for details. * **Persistent IBM Runtime sessions:** When you run on a remote [IBM backend](/user-guide/execution/cloud-providers/ibm-backends) inside a single [`ExecutionSession`](/user-guide/execution/ExecutionSession), successive primitives (`sample`, `observe`, `estimate_cost`, `variational_minimize`, and non-blocking counterparts) now reuse one IBM Quantum Runtime session rather than each opening its own. On busy hardware, where each session waits in the queue, keeping the session open across a multi-step workflow means you queue once instead of at every step. No flag needed; the session reuse happens **automatically**: [comment]: DO_NOT_TEST ```python theme={null} with ExecutionSession(qprog, backend="ibm/...") as es: counts = es.sample() # opens one IBM Runtime session energy = es.observe(hamiltonian) # reuses the same session ``` ### Per-Call Shot and Routing Overrides Within an execution session, [`ExecutionSession.sample()`](/user-guide/execution/ExecutionSession#operations), `observe()`, and `variational_minimize()` (and their `submit_*` counterparts) now accept optional `num_shots` and `run_via_classiq` keyword arguments that override the session defaults for **that invocation only**. This lets the session hold your shared defaults while you vary shot counts or execution routing per primitive call. Code Example: [comment]: DO_NOT_TEST ```python theme={null} with ExecutionSession(qprog, num_shots=1000) as es: quick = es.sample() # uses the session default of 1000 shots precise = es.sample(num_shots=8000) # overrides for this call only ``` ### Visualizer Label Management The [quantum program visualizer](/user-guide/analysis/visualization-of-quantum-programs) now keeps large circuits readable with finer control over variable labels. Previously, the same variable name repeated down every wire added visual noise that could obscure the circuit's architecture. What has been updated: * Adjacent repeating labels are suppressed automatically, cutting the clutter. * A suppressed label is never lost: hover over the wire for a tooltip with the hidden name. * A right-click context menu on each variable wire lets you show or hide individual labels as needed. For example, a circuit with label repeats suppressed: Quantum program visualizer after label suppression, with repeated labels hidden for a cleaner view As opposed to the previous circuit visualization: Quantum program visualizer before label suppression, with repeated variable names cluttering every wire ### LaTeX Export for Large Circuits LaTeX export on the Quantum Program page now also handles very large circuits, rendering them as publication-quality diagrams. ## Documentation Additions * **[Quantum Engineer Plugin](/user-guide/ai/quantum-engineer-plugin):** Install and set up the plugin in your AI coding agent, with a breakdown of the skills for each development phase and worked examples to start from. * **[Classiq Assistant: Getting Started](/user-guide/ai/classiq-assistant):** Access the platform assistant, generate and refine a program from a prompt, and learn the best practices for prompting it well. * **[M2M Authentication](/user-guide/authentication/m2m-authentication):** Authenticate backend services to the Classiq platform with no human intervention required, using the non-interactive machine-to-machine flow built on the OAuth 2.0 client credentials grant. ## Bug Fixes * **Loops and controls that reuse variables now compile correctly:** [`foreach`/`power`](/qmod-reference/language-reference/statements/classical-control-flow) loops that modified quantum-numeric variables used in later comparisons, and [`control`](/qmod-reference/language-reference/statements/control) statements whose bodies reused their condition variables, could previously produce incorrect circuits (the latter by leaking a phase onto the controlled state). * **`subscript` now accepts a quantum expression as its index:** Indexing a [`subscript`](/qmod-reference/language-reference/expressions#path-operators) with a quantum expression (for example `res ^= subscript(table, a + b)`) previously raised an internal error; such expressions are now lowered and synthesized correctly. * **Classical expressions can now reuse a variable:** A classical expression that reuses a variable, such as the exponent `k + k` in `power(k + k, ...)`, no longer fails with "both sides of the operation are identical"; that restriction now applies only to in-place quantum arithmetic. * **Clearer error message when exporting parametric programs to QASM 2:** Exporting a parametric program with the default `target_language=TargetLanguage.QASM2` now raises an indicative error suggesting QASM 3, rather than an unclear internal one. * **The visualizer stays responsive on oversized function blocks:** Running Expand All on a function block with too many child operations to render could previously freeze or crash the browser tab. It now stops with a warning that the block is too large to visualize, and the same warning appears when expanding a single oversized block. ## Deprecations * **`exponentiation_with_depth_constraint` removed:** This function exponentiated a Pauli operator (Hamiltonian simulation) while automatically selecting Suzuki-Trotter parameters to fit a target maximum circuit depth. It has now been removed after a deprecation period. Use [`exponentiate`](/sdk-reference/qmod/functions/core_library/exponentiation) for Hamiltonian exponentiation, or the [`suzuki_trotter`](/qmod-reference/library-reference/core-library-functions/hamiltonian_evolution/suzuki_trotter/suzuki_trotter) family and [`qdrift`](/qmod-reference/library-reference/core-library-functions/hamiltonian_evolution/qdrift/qdrift) when you need explicit control over the decomposition order and repetitions. For the technical entries behind these changes, see the [Changelog](/release-notes). # Release News: May 2026 Source: https://docs.classiq.io/release-news/recap-may-2026 Five versions released in May, spanning improvements to the execution workflow, scalability, and quantum developer experience. New top-level execution functions and simplified execution session methods New capabilities and performance improvements New guides and notebooks Correctness fixes and API deprecations ## Simplified Execution The execution workflow has been refreshed across several areas: * Three new top-level execution functions for one-call execution and result retrieval. * New `ExecutionSession` methods for simplified execution. * `ExecutionSession` preferences can now be passed directly. * OpenQASM 2.0 and 3.0 strings now accepted in place of a `QuantumProgram`. ### New Top-Level Execution Functions Three new top-level functions now enable execution and result retrieval in a single call, each returning results directly: * `sample()` for shot-based measurement statistics, returned as a DataFrame. * `observe()` for the expectation value of a Hermitian observable, returned as a scalar. * `calculate_state_vector()` for the full quantum state including amplitudes and phases, returned as a DataFrame (simulators only). Previously, this required managing job objects manually: submitting, polling, and unpacking results through separate API calls. All three support single and batch execution and accept the same arguments, as well as `run_via_classiq`. See the [Execution](/user-guide/execution) user guide for the full workflow and function reference. Usage examples: ```python theme={null} from classiq import * # Example function: Applies a Hadamard gate on a single qubit @qfunc def main(res: Output[QBit]): allocate(res) H(res) # Synthesizing the model into a quantum program qprog = synthesize(main) # Shot-based measurement statistics df = sample(qprog, num_shots=1000) # Expectation value of an observable (Pauli Z matrix) value = observe(qprog, observable=Pauli.Z(0), num_shots=1000) # Full quantum state (simulators only) df = calculate_state_vector(qprog) ``` ### Updated Execution Session Methods New `ExecutionSession` methods `sample()` and `estimate()` replace earlier dedicated batch functions (see [Execution Session](/user-guide/execution/ExecutionSession#operations) user guide), supporting both single and batch execution by accepting a parameter dictionary or a list of dictionaries. The dedicated `batch_sample`, `submit_batch_sample`, `batch_estimate`, and `submit_batch_estimate` methods are deprecated as a result, with a removal date of **June 22nd, 2026**. Usage examples: [comment]: DO_NOT_TEST ```python theme={null} with ExecutionSession(qprog) as es: # Single execution result = es.sample({"t": 0.5}) # Batch execution (2 iterations) results = es.sample([{"t": 0.5}, {"t": 0.6}]) # Expectation value hamiltonian = Pauli.Z(0) value = es.estimate(hamiltonian, {"t": 0.5}) # Batch expectation values (2 iterations) values = es.estimate(hamiltonian, [{"t": 0.5}, {"t": 0.6}]) ``` In addition, the `ExecutionSession` method `minimize()` for variational optimization of a cost function over the quantum program's initial parameter values has been renamed to `variational_minimize`. `variational_minimize` is now also available as a standalone function, usable outside the scope of `ExecutionSession`. It supports Hamiltonian and classical cost functions, `run_via_classiq`, and includes improved input validation. For more details, see [SDK Reference](/sdk-reference/execution#variational_minimize). ### Direct Execution Session Preferences `ExecutionSession` now also accepts configuration parameters directly as keyword arguments, rather than through a separate `ExecutionPreferences` object. Usage example: [comment]: DO_NOT_TEST ```python theme={null} with ExecutionSession(qprog, num_shots=2000, random_seed=42) as es: results = es.sample() ``` Passing an `ExecutionPreferences` object via `execution_preferences=` can still be used, but is deprecated and will be removed on **June 22nd, 2026**. Parameters that `ExecutionSession` may now accept directly: `backend`, `num_shots`, `random_seed`, `transpilation_option`, `run_via_classiq`, `config`. ### OpenQASM Support OpenQASM 2.0 and 3.0 strings are now accepted directly by `sample()` and `ExecutionSession` as an alternative to a synthesized `QuantumProgram`. Results are returned in the same histogram DataFrame format. Note that the `parameters` argument is not supported for OpenQASM strings. For parametric circuits, use a `QuantumProgram`, or bind parameters inside the QASM circuit directly. See the [Execution](/user-guide/execution) and [Execution Session](/user-guide/execution/ExecutionSession#initializing) user guides for more details. ## Enhancements ### Seamless Support for Larger Models The SDK now handles significantly larger synthesis and execution workloads without hitting payload size limits. Upgraded backend data handling enables seamless processing of large-scale models and datasets, requiring no changes from users. ### Improved Visualizer Rendering The visualizer can now fully render large circuits, with all child operations visible. This lifts a previous limitation where a per-block child operation limit could leave some operations unrendered in large circuits. ### GitHub Copilot in Classiq Studio GitHub Copilot is now available within Classiq Studio, our web-based pre-configured IDE. It includes three main capabilities: * **Chat-based AI Assistant**: ask questions about the Qmod language, Classiq workflow, or circuit debugging. * **Coding agent**: let Copilot write or refactor your Qmod code. * **Autocomplete**: inline suggestions as you write your quantum algorithm within the Studio. The Copilot is available on Classiq Studio without any additional setup if you hold an active GitHub Copilot subscription. GitHub Copilot in Classiq Studio ### Faster Synthesis for Large Hamiltonians Synthesis for models containing large Hamiltonian objects is now significantly faster. If you have been working with molecule simulations or large combinatorial problems and synthesis felt slow, this directly cuts that wait time. See [Observables and Operators](/user-guide/modeling/observables-and-operators) for how Hamiltonians are defined and used in Qmod. ### Qmod-Variable Array Slice Operator `classiq.qmod.symbolic` now includes a `slice(array, start, stop)` operator that accepts unsigned integer Qmod variables as the `start` and `stop` arguments (see [Path Operators](/qmod-reference/language-reference/expressions#path-operators)). It is the slice counterpart to the existing `subscript` operator for single-element access. When `array` is a Qmod variable, the standard bracket syntax `array[start:stop]` can be used directly; the `slice()` syntax is required when `array` is a regular Python list and integer Qmod variables are used as indices. ### QSVM Application (QML) A new [Quantum Support Vector Machine](/sdk-reference/applications/QSVM) (QSVM) module is now available in the Classiq Python SDK. It combines quantum-computed kernel matrices with classical SVMs, enabling quantum-enhanced classification through a `QSVM` class with `train`, `test`, and `predict` methods. Users can select from built-in quantum feature maps or supply their own. The SDK handles circuit synthesis, execution, and kernel-matrix construction automatically. Relevant notebooks in the Classiq Library have been updated accordingly. ### New SDK Utilities Two new utility functions have been added to the SDK for diagnostics and backend exploration: * **`print_diagnostics()` function:** A new `classiq.print_diagnostics()` function prints a snapshot of the current SDK environment, including SDK version, Python version, backend host and version, authentication status, and user ID. Useful for quick inclusion in support tickets. * **`get_backend_details()` function:** A new function that returns a DataFrame of all supported quantum backends, with details on provider, backend name, type (hardware or simulator), qubit count, availability, pending jobs, and queue time. For details, see the [Execution](/user-guide/execution/index#step-2--inspect-available-backends) user guide. ## Documentation Additions * **[Hello World](/getting-started) Guide:** Build, synthesize, and run your first quantum algorithm, covering everything you need before diving deeper into the Classiq platform. * **Streamlined [Execution](/user-guide/execution) and [Execution Session](/user-guide/execution/ExecutionSession) Guides:** The Execution and Execution Session user guides have been updated to cover the three new execution functions, the updated `ExecutionSession` methods, direct preferences passing, and a step-by-step workflow guide. See the [Simplified Execution](#simplified-execution) section for more details. ## Library Additions * **1D Fermi-Hubbard Model Simulation:** A new [notebook](https://github.com/Classiq/classiq-library/blob/main/applications/physical_systems/fermi_hubbard_model_1D/fermi_hubbard_1D.ipynb) for simulating the 1D Fermi-Hubbard model has been added to the Classiq Library. The Fermi-Hubbard model is a key model in condensed matter physics; this notebook, inspired by Google Quantum AI's 2020 experiment, demonstrates state preparation, time evolution, and spin-charge separation using the Classiq SDK. The implementation is fully high-level, with optimized quantum circuits generated automatically. ## Bug Fixes * **Fixed unintended relative phase in controlled amplitude preparation:** `prepare_amplitudes` and `inplace_prepare_amplitudes` state preparation functions (see [state preparation routines](/qmod-reference/library-reference/core-library-functions/prepare_state_and_amplitudes/prepare_state_and_amplitudes)) could introduce an unintended global phase in certain scenarios. For the uncontrolled version this is unobservable, but when used inside a `control()` statement (see [control](/qmod-reference/language-reference/statements/control)), the global phase could become a relative phase conditional on the control qubit. This is now fixed. * **Combinatorial optimization now handles degenerate constraints correctly:** `CombinatorialProblem` converts classical optimization problems into quantum models for algorithms like QAOA (see [Learning Optimization](/explore/tutorials/basic_tutorials/optimization/learning_optimization#learning-optimization)). A `KeyError` that was raised when an inequality constraint became degenerate (collapsed to a single feasible value) after another constraint had already fixed one of its variables to a single value is now corrected. * **Inconsistency in gate arguments of QASM output resolved:** `QuantumProgram.qasm` could produce incorrect gate arguments in certain scenarios. While transpilation would correct this automatically during synthesis, the problem could carry through to simulation when manually disabling transpilation via `transpilation_option=TranspilationOption.NONE` (see [Quantum Program Transpilation](/user-guide/synthesis/quantum-program-transpilation)). This is now fixed. * **Models saved with older SDK versions can now be successfully synthesized:** A compatibility issue that prevented most models saved with earlier SDK versions from being synthesized is now resolved. * **Circuit visualizer now able to recover from GPU context loss:** The visualizer would go blank and fail to recover if the WebGPU/WebGL graphics context was lost. The visualizer now includes automatic recovery from context loss. * **NVIDIA GPU worker now initializes correctly:** An issue that caused the NVIDIA GPU worker to fail during initialization is now fixed. ## Deprecations * **`ExecutionSession` batch methods deprecated:** `batch_sample` and `batch_estimate` are replaced by passing parameters directly to `ExecutionSession.sample()` or `ExecutionSession.estimate()`, as covered in the [Simplified Execution](#updated-execution-session-methods) section above.
Removal date: **June 22nd, 2026**. * **`ExecutionPreferences` object deprecated:** Replaced with direct keyword arguments in `ExecutionSession`, as covered in the [Simplified Execution](#direct-execution-session-preferences) section above.
Removal date: **June 22nd, 2026**. * **`theta` parameter of `phase` renamed to `coefficient`:** The old `theta` parameter name has been deprecated and removed for the `phase` function specifically; it remains valid in other functions. * **`randomized_benchmarking` function deprecated and removed:** This function has been deprecated and removed from the SDK. * **`pretty_qasm` field in Synthesis Preferences removed:** OpenQASM 2 output is now always formatted with line breaks inside gate declarations (the previous default). If you were explicitly setting `pretty_qasm=False` to get unformatted output, that option no longer exists (see [synthesis preferences](/user-guide/synthesis/preferences) user guide). # Changelog Source: https://docs.classiq.io/release-notes/index A chronological record of changes and improvements for each release. ## Upgrade Instructions * [Python SDK](/getting-started/sdk_installation#platform-version-updates) * The IDE upgrades automatically. ## Releases by Version ## Bug Fixes * **Synthesis:** Fixed a bug where automatic uncomputation silently dropped a rolled `repeat` or `foreach` loop, or a `skip_control` body, from a function that had a variable to uncompute, producing an incorrect circuit. ## Interface Changes * **Synthesis:** * The compiler now rejects a program that changes a quantum variable's qubit layout across a compile-once boundary. Reordering the variable's qubits, or routing it onto different qubits, inside a `power`, `repeat`, `foreach`, `if`, `control`, or `invert` block, or across a function body, now raises an error; re-join the variable's parts in their original order with a `bind` statement instead. * Comparing a quantum number against a constant (`x < 5`, `x >= 2.5`, and the other relations, in either operand order) is now computed directly, without allocating a scratch register. Previously it computed `x` minus the constant in an operand-width register, where the sign bit gives the comparison result. The compiler picks the implementation automatically to fit the optimization objective and any `max_width` constraint; the narrowest needs no auxiliary qubits, so a comparison now fits even where there is no spare width. Signed and fixed-point operands work as before, and under a `control`, the control is built into the comparison rather than applied to each of its gates, so a conditional comparison costs almost the same as an unconditional one. * **Language:** The open library function `inplace_binary_to_one_hot` now takes separate input and output arguments (`source: Input[QArray]`, `target: Output[QArray]`) instead of a single in-out array argument. ## Announcements * **Platform access is now by invitation only.** Free access is not currently available. Existing users are not affected and retain full access to the platform. ## Enhancements * **New QNN layer [`QLayerV2`](../user-guide/applications/qml/qnn/qlayerv2):** A simpler, faster quantum layer, available only within Classiq Studio at the moment. It accepts a synthesized quantum program and an optional list of Pauli observables, and returns a `torch` module with one output feature per observable - no `execute` or `post_process` function to write. Gradients use adjoint differentiation, so they are exact rather than finite-difference estimates, and training is much faster. For sampling, custom post-processing, backend selection, or remote execution, keep using [`QLayer`](../user-guide/applications/qml/qnn/qlayer). Import it with `from classiq.applications.qnn import QLayerV2`. * **Synthesis:** An in-place increment or decrement of a quantum number by 1 (`x += 1`, `x += -1`), including inside a `control` block, now uses a flexible, fault-tolerant increment primitive, automatically optimized for either circuit width or CX count according to the `max_width` constraint and the optimization parameter. ## Interface Changes * **Execution:** Added a `verbose` parameter (default `True`) to the execution functions `sample`, `observe`, `variational_minimize`, and `calculate_state_vector`, and their `ExecutionSession` counterparts (including `submit_calculate_state_vector`). Set `verbose=False` to suppress the progress logging they would otherwise print (the `Submitting...` and `Job: ...` lines), useful when calling these functions repeatedly in a loop. * **`QLayerV1` alias:** `QLayer` is now also exported as `QLayerV1`, distinguishing it from the new `QLayerV2`. ## Bug Fixes * **Synthesis:** Fixed a bug where changing a quantum variable's qubit layout inside a `power`, `repeat`, `foreach`, or `if` block (for example, by reordering its qubits) silently produced an incorrect circuit; such programs are now rejected with a descriptive error. * **Classiq Studio:** `QLayer` and `execute_qnn` now work in Classiq Studio for programs synthesized with the new synthesis flow. The in-process Studio simulator obtained its circuit through an export path that raised `ClassiqInternalError` for such programs, which made local simulation there unusable. Circuits of up to 12 qubits (previously 10) are now simulated in-process. ## Enhancements * **IDE hardware benchmarking:** Predefined hardware benchmarks are now also available in the Classiq IDE, through the new Benchmark tab within the Execution page. This no-code interface sweeps a range of problem sizes across multiple backends and returns a per-backend score for the same set of well-established circuits (GHZ, Adder, QFT, State Preparation, and Dynamical Localization). See the [Predefined Benchmarks](../user-guide/execution/benchmarking/predefined-benchmarks#benchmarking-in-the-classiq-ide) user guide for a walkthrough of configuring and running a benchmark. * **Synthesis:** Improved the runtime performance of [`export`](../sdk-reference/synthesis#export) when targeting a fault-tolerant gate set via [`FaultTolerantTranspilationConfig`](../sdk-reference/synthesis#faulttoleranttranspilationconfig). * **Language:** The `subscript` and `slice_` [path operators](../sdk-reference/qmod/path-operators) can now be imported directly from the top-level `classiq` package and are picked up by `from classiq import *`, so they no longer need to be imported explicitly from `classiq.qmod.symbolic`. ## Bug Fixes * **`observe`:** Calling [`observe`](../sdk-reference/execution#observe) no longer emits a spurious `submit_estimate() is deprecated` warning. The function now routes internally through `submit_observe`. ## Interface Changes * Renamed the `slice` Qmod operator to `slice_`. The `slice` name is now deprecated and emits a deprecation warning when called; use `slice_` instead. ## Enhancements * **Synthesis:** The executable representation of a quantum program returned by [`synthesize`](../sdk-reference/synthesis#synthesize) is now Qmod-based, significantly improving compilation performance and enabling quantum programs to scale better. Execution, resource estimation, and quantum program visualization now operate directly on the Qmod-based representation. [`export`](../sdk-reference/synthesis#export) can be used to generate the corresponding description in an external format, such as OpenQASM 2.0, OpenQASM 3.0, or QIR. * **Language:** `QArray` and `QNum` now accept their arguments positionally when the variable name is omitted, so a call such as `QArray(QBit, 2)` or `QNum(4)` works as intended instead of raising an internal error. Passing a non-string name to any quantum variable constructor (`QArray`, `QNum`, `QBit`, or `QStruct`) now raises a clear error instead of failing internally. ## Bug Fixes * Fixed the `subscript`, `slice`, and `reversed_` classical functions in the Python SDK, which returned wrong-typed objects instead of inferring the result type from their argument (for example, subscripting a nested list returned a scalar that could not be indexed further). ## Interface Changes * **[`QuantumProgram`](../sdk-reference/synthesis#quantumprogram) circuit attributes:** Deprecated the `qasm` and `transpiled_circuit` attributes of `QuantumProgram`, following the move to a Qmod-based synthesis representation; they will no longer be supported in a future release. * Use [`export`](../sdk-reference/synthesis#export) to obtain a circuit in an external format such as OpenQASM 2.0. * Use [`get_circuit_metrics`](../sdk-reference/synthesis#get_circuit_metrics) for approximate circuit depth and gate counts, and [`get_transpiled_circuit_metrics`](../sdk-reference/synthesis#get_transpiled_circuit_metrics) for accurate depth and gate counts after transpilation. * **Legacy synthesis flow:** To continue using the legacy synthesis flow from version 1.21.0 and earlier, set the Boolean [`compatibility_mode`](../sdk-reference/synthesis#compatibility_mode) attribute under [`Preferences`](../sdk-reference/synthesis#preferences). * **Execution:** [`ExecutionSession`](../sdk-reference/execution#executionsession) now accepts the advanced flat constructor arguments `noise_properties`, `amplitude_threshold`, `include_zero_amplitude_outputs`, and `job_name`, matching the fields previously available only through `execution_preferences`. ## Enhancements * **Hardware benchmarking:** Added predefined hardware benchmarks that measure and compare how well quantum backends run a set of well-established quantum circuits (GHZ, Adder, QFT, State Preparation, and Dynamical Localization). Sweep a range of problem sizes across multiple backends and get a per-backend score from the SDK via [`run_benchmark`](../sdk-reference/execution#run_benchmark) (and soon in the new Benchmark mode on the IDE Execution page). See the new [Predefined Benchmarks](../user-guide/execution/benchmarking/predefined-benchmarks) user guide. * **Classiq Studio:** [`execute_qnn`](../user-guide/applications/qml/qnn/qlayer#execution) now runs locally via the Studio simulator. Sampling runs locally, and requests that pass an observable now compute exact (state vector, shot-free) expectation values locally instead of falling back to remote simulation. * **QLayer observables:** [`QLayer`](../user-guide/applications/qml/qnn/qlayer) now accepts a keyword-only `observable` (a `SparsePauliOp`, the same type accepted by `ExecutionSession.observe`) when constructed without a custom `execute` function. The layer estimates the observable's expectation value instead of sampling: exact state vector expectation values inside Classiq Studio, and the `observe` flow remotely. The `post_process` callback receives a `SavedResult` wrapping an `EstimationResult` (read the value via `result.value.value.real`), and gradients work unchanged. * **Language:** Added the `range_` and `reversed_` classical functions, available both in classical expressions and in the Python SDK. ## Bug Fixes * **Synthesis:** * Fixed `power` applied to a `foreach` loop, which distributed the exponent into each iteration instead of raising the whole loop to the power. * Fixed `invert` applied to a `foreach` loop, which kept the original iteration order instead of reversing it. ## Interface Changes * **execute\_qnn observable type:** `execute_qnn` now takes the observable as a `SparsePauliOp` (the same type accepted by `ExecutionSession.observe`). Passing the legacy `PauliOperator` is deprecated; it is converted automatically and emits a `DeprecationWarning`. ## Enhancements * **AI with Classiq:** * **Classiq Assistant (Beta):** Added an AI assistant to the platform home page and the Quantum Program page. Describe a quantum problem in natural language to the assistant (or upload a circuit image), and it will build a qmod-native model, synthesize it into a quantum program, and open it on the Quantum Program page. The assistant keeps conversation context within a session and supports follow-up messages, so you can ask it to explain or refine the circuit. For more information, see the new [Classiq Assistant](../user-guide/ai/classiq-assistant) user guide. * **Quantum Engineer Plugin:** Added a Quantum Engineer plugin that brings Classiq into local AI coding agents such as Claude, Cursor, and Codex. The plugin pairs a set of Classiq skills with the Classiq MCP, letting the agents model, synthesize, execute, and analyze quantum programs across the full Classiq workflow. For more information, see the new [Quantum Engineer Plugin](../user-guide/ai/quantum-engineer-plugin) user guide. ## Enhancements * **Execution:** Added the option to run on a GPU-accelerated statevector simulator hosted on an NVIDIA DGX system, selected by passing `backend="classiq/dgx_simulator"`. It supports both sampling and statevector modes and handles circuits of up to 35 qubits in single precision (float32). Access requires specific license permissions. See [DGX Statevector Simulator](../user-guide/execution/cloud-providers/classiq-backends#dgx-statevector-simulator). * **Synthesis:** * Added a new `export` function to the SDK that converts a synthesized quantum program into a circuit string in a chosen target language (QASM2, QASM3, QIR, Cirq JSON, or Q#), with an optional transpilation configuration. See [export](../sdk-reference/synthesis#export). * Improved the algorithm that approximates RZ rotations over the Clifford+T basis when exporting to a fault-tolerant gate set (via [`FaultTolerantTranspilationConfig`](../sdk-reference/synthesis#faulttoleranttranspilationconfig)), which now uses gridsynth to produce lower-error decompositions. * **Projection-based WF-in-DFT embedding:** Added a new `EmbeddingCalculator` API for running projection-based wavefunction-in-DFT (WF-in-DFT) embedding calculations. This method enables accurate quantum-chemistry simulations of large molecules by treating a small active fragment with a quantum solver while capturing the effect of the surrounding environment at the DFT level. Users define a molecule via `MoleculeSpec` (supporting inline geometry, `.pdb`, and `.xyz` files) and configure the embedding with `EmbeddingConfig` (fragment atom selection, DFT mean-field method, exchange-correlation functional, active-virtual selection, and optional core freezing). The calculator drives a three-stage pipeline, full-system DFT, fragment embedding, and Hamiltonian construction, entirely server-side as queued backend jobs, returning the embedded and physical Hamiltonians as `openfermion.FermionOperator` objects ready for a quantum solver. * Both blocking (`run_dft`, `run_dft_embedding`) and non-blocking (`submit_dft`, `submit_dft_embedding`) execution modes are supported; the non-blocking variants return a `ChemistryJob` handle that can be polled, cancelled, or reconnected from a later process via `ChemistryJob.from_id`. * Built-in validation diagnostics (`DFT-in-DFT`, `FCI active space`, `probability leak`, `trace conservation`, `geometry perturbation`) can run automatically at the end of the embedding pipeline or be requested post-hoc via `run_validations`. * Restricted and unrestricted spin treatments are supported, with an `AUTO` mode that resolves based on the number of unpaired electrons. * **Classiq Studio:** * The Studio image now installs the Classiq SDK with all optional dependencies (`analyzer_sdk`, `qml`, `chemistry`, `qsp`, and `cudaq`) automatically. * When executing inside Classiq Studio, `execute_qnn` now runs sampling locally via the Studio simulator; requests that pass an observable continue to use remote simulation. ## Bug Fixes * Fixed Latex export failures on large circuits. ## Enhancements * **Execution:** Benchmarking is now available, with built-in benchmark classes and an end-to-end flow for creating and running benchmark jobs. Supported benchmark classes are `GHZ`, `Adder` & `Dynamic Localization`. ## Enhancements * **IonQ Hosted Hybrid variational minimization:** On IonQ backends, `variational_minimize`, `ExecutionSession.variational_minimize`, and `ExecutionSession.submit_variational_minimize` now accept `hosted=True` to delegate the full optimization loop to IonQ's Hosted Hybrid service instead of executing a separate Classiq sample job on every iteration. Classiq submits and polls a single IonQ optimization job through the standard execution job API, so the SDK never contacts IonQ directly. Hosted mode requires a Hamiltonian cost function, a single execution parameter (`CReal` or `CArray`), and `quantile=1.0`; classical Qmod cost functions and CVaR (`quantile < 1.0`) are not supported. * **Sticky IBM Runtime sessions under `ExecutionSession`:** When you run on a remote IBM backend inside a single `ExecutionSession`, successive primitives (`sample`, `observe`, `estimate_cost`, `variational_minimize`, and their non-blocking `submit_*` counterparts) reuse the same IBM Quantum Runtime session instead of opening a new one for every job. This reduces queue overhead for multi-step workflows such as variational optimization followed by a validation sample. ## Interface Changes * **`ExecutionSession.close()`:** Closing a session now calls a server-side close API that releases provider resources, including an open IBM Runtime session. Using `ExecutionSession` as a context manager (recommended) invokes `close()` automatically when the block exits. ## Bug Fixes * **Incorrect circuits from `foreach`/`power` loops that modify a quantum-numeric variable:** When the body of a `foreach` or `power` loop modified a quantum-numeric variable, a comparison against that variable (for example `x == 0`) could be folded against its value at loop entry instead of its actual per-iteration value, producing an incorrect circuit. Such comparisons are no longer folded against stale bounds. * **Incorrect circuit for a control whose body reuses the condition variable:** A control such as `control(q == 0, lambda: Z(q))` whose controlled block operates on the same variable used in the condition could leak a phase onto the controlled state, producing an incorrect circuit. The condition is now computed out of place when the variable is reused, so such controls compile correctly. * **Internal error when indexing a `subscript` with a quantum expression:** Using a quantum expression as the index of a `subscript` (for example `res ^= subscript(table, a + b)`) raised an internal error instead of synthesizing. Such expressions are now lowered correctly by computing the index into a temporary quantum variable. * **Error when a classical expression repeats a variable (for example `k + k`):** Expressions that used the same classical variable on both sides of an operation, such as `power(k + k, ...)` or two consecutive `power(k, ...)` blocks, failed with "both sides of the operation are identical". This restriction only applies to in-place quantum arithmetic and is no longer enforced on classical expressions. * **Internal error when exporting a parametric program to QASM2:** Calling `export` on a parametric quantum program with the default `target_language=TargetLanguage.QASM2` failed with an opaque internal error, since QASM2 cannot represent parametric circuits. It now raises an indicative error suggesting `target_language=TargetLanguage.QASM3` instead. * **QP Visualization:** Fixed Expand All freezing or crashing the browser tab on very large function blocks. In the quantum program visualizer (Classiq Studio and the Classiq IDE), expanding all operations when a function block had too many child operations to render could freeze or crash the tab. Expand All is now blocked in these cases with a warning that the function block is too large to visualize, and expanding an individual block over the same limit shows the same warning instead of failing silently or crashing. ## Interface Changes * **`exponentiation_with_depth_constraint` removed:** The previously deprecated `exponentiation_with_depth_constraint` function has been removed. Use `exponentiate` instead. ## Enhancements * **Hardware modality in `get_backend_details`:** The DataFrame returned by `get_backend_details` now includes a `modality` column (for example, `superconducting`, `trapped-ion`, `simulator-cpu`, or `simulator-gpu`), exposing the modality already tracked in the hardware catalog. Backends without a recorded modality show `None`. * **Jobs page results display:** The measurement results and state vector histograms now show at most the 64 highest-probability states; circuits with 6 or fewer qubits still show every state, including zero-probability ones. The results table below the histogram lists only measured states, so a zero-probability state can appear as an empty bar in the histogram while being omitted from the table. ## Bug Fixes * Fixed an issue that prevented `classiq` from being imported in Jupyter notebooks. ## Interface Changes * **`ExecutionSession.calculate_state_vector`** (and its non-blocking `submit_calculate_state_vector` counterpart) is a new primitive for statevector calculation. Use this instead of `sample` on the state vector simulator. ## Enhancements * **QP Visualization:** Added visual label management. * Adjacent variables that repeat across wires are now automatically suppressed to reduce clutter and save visual space in quantum program diagrams. * Suppressed variable labels can still be viewed by hovering over the wire, which shows a tooltip with the hidden name(s). * A right-click context menu on each variable wire lets you show or hide individual labels as needed. ## Interface Changes * **`ExecutionSession.sample()`, `observe()`, and `variational_minimize()`:** Accept optional per-call `num_shots` and `run_via_classiq` keyword arguments that override the session defaults for that invocation only (including the matching `submit_*` methods). ## Enhancements * The Python SDK now collects telemetry to help us improve our product. This can be disabled by setting the environment variable `CLASSIQ_TELEMETRY_MODE=disabled`. ## Bug Fixes * Fixed incorrect gate arguments in `QuantumProgram.qasm`. This also affected simulation when `transpilation_option=TranspilationOption.NONE`. ## Bug Fixes * Fixed an issue where the nvidia-gpu worker would fail during initialization. ## Interface Changes * The **pretty\_qasm** field was removed from `Preferences`. OpenQASM 2 outputs are now always formatted with line breaks inside gate declarations (the previous default behavior). * **`ExecutionSession`** now accepts individual configuration parameters directly: `ExecutionSession(qprog, backend=..., num_shots=..., random_seed=..., transpilation_option=..., run_via_classiq=..., config=...)`. The `execution_preferences=` keyword form still works but is deprecated and will be removed on 2026-06-22. ## Enhancements * **QP Visualization:** Improved support for larger circuits and operations. Visualizations can now display much larger quantum programs, with the maximum number of child operations that can be expanded at once raised from 1,000 to 2,500, so large composite gates, functions, and generated code blocks expand and render fully. ## Enhancements * **GitHub Copilot Integration:** Added support for GitHub Copilot in Classiq Studio, including chat-based AI assistance, coding agent features, and code autocomplete. These features are now available in the browser-based IDE experience. ## Bug Fixes * **`CombinatorialProblem`:** Fixed a `KeyError` raised when an inequality constraint became degenerate (collapsed to a single feasible value) after another constraint had fixed one of its variables. ## Bug Fixes * Restored the Studio button to its original location in the left sidebar of the IDE. ## Enhancements * **Synthesis:** Faster synthesis for models containing very large [Hamiltonian](../qmod-reference/language-reference/classical-types#hamiltonians) objects. * Added support for the C12 provider when using `sample`, `observe`, and related functions with `backend='c12/'`. * Added support for large-scale models by uploading/downloading execution and synthesis inputs/outputs directly to S3 via pre-signed URLs, bypassing payload size limitations. ## Bug Fixes * **`prepare_amplitudes` / `inplace_prepare_amplitudes`:** Fixed a wrong global phase that affected the controlled version for some amplitude vectors. * **QP Visualization:** Improved robustness of the QP Visualization feature by adding webGPU/webGL context loss recovery, preventing blank screens when the graphics context is lost. ## Bug Fixes * Fixed a bug in the infrastructure. ## Enhancements * **Studio:** `latexmk` and a TeX Live subset are now pre-installed, enabling the benchmarking application from the Classiq library to generate its PDF report directly in the Studio. ## Bug Fixes * Fixed an issue where most models saved with older SDK versions could not be synthesized. ## Enhancements * **`ExecutionSession.sample` and `ExecutionSession.estimate`:** These methods now accept a list of parameter dictionaries for batch execution, returning a list of results. The dedicated `batch_sample`, `submit_batch_sample`, `batch_estimate`, and `submit_batch_estimate` methods are deprecated — pass a list to the standard methods instead. * **`variational_minimize` function:** Added a new public `variational_minimize` function for variational optimization of a cost function over the parameter values of a quantum program. Supports Hamiltonian and classical cost functions, `run_via_classiq`, and improved input validation. See the [SDK reference](/sdk-reference/execution/) for details. * Added `slice(array, start, stop)` in `classiq.qmod.symbolic` as the Python alternative for [array slice expressions](../qmod-reference/language-reference/expressions/#path-operators) on Python lists, analogous to `subscript`. * **`classiq.print_diagnostics()`:** New function that prints a snapshot of the SDK version, Python environment, backend host and version, authentication status, and user ID - for easy inclusion in support tickets. ## Bug Fixes * **Execution:** Sample jobs started with `execute()` now persist submitted-circuit metadata so `ExecutionJob.get_submitted_circuits()` matches the documented behavior. * **Classiq Studio:** Fixed light mode not being saved in user preferences. * **Classiq IDE:** Fixed the Synthesize button repeatedly appearing and disappearing. * Fixed synthesis of nested concatenations in control condition. ## Interface Changes * The `theta` parameter of function `phase` was renamed to `coefficient`. The old name is deprecated and will no longer be supported starting on 2026-05-04 at the earliest. * Function `randomized_benchmarking` is deprecated and will no longer be supported starting on 2026-05-11 at the earliest. ## Enhancements * **`sample` function:** Added a new top-level `sample` function for executing a quantum program and retrieving results as a DataFrame directly, without managing a job object. Supports single and batch execution — pass a list of parameter dictionaries to `parameters` to run multiple parameter sets and receive a list of DataFrames. Also supports `run_via_classiq=True` to run using Classiq's provider credentials against your allocated budget. See the [SDK reference](../sdk-reference/execution/) for details. * **`observe` function:** Added a new public `observe` function that computes the expectation value of a Hermitian observable with respect to a quantum program's output state. Supports exact statevector calculation or shot-based estimation, batch execution, and `run_via_classiq`. See the [SDK reference](../sdk-reference/execution/) for details. * **`get_backend_details` function:** Added a `get_backend_details` function that returns a DataFrame of all supported quantum backends, including provider, backend name, type (hardware or simulator), qubit count, availability, pending jobs, and queue time. * **`calculate_state_vector` function:** Added a new public `calculate_state_vector` function that returns the full state vector of a quantum program as a DataFrame. Supports batch execution by passing a list of parameter dictionaries. Available on Classiq simulators (e.g. `classiq/simulator`). * **`minimize` function:** Added a new public `minimize` function for variational optimization of a cost function over the parameter values of a quantum program. Supports Hamiltonian and classical cost functions, `run_via_classiq`, and improved input validation. See the [SDK reference](../sdk-reference/execution/) for details. * **OpenQASM in `sample` and `ExecutionSession`:** You can pass **OpenQASM 2.0 or 3.0** source as a string to `sample()` (first argument) or to `ExecutionSession` instead of a synthesized `QuantumProgram`. Results use the same histogram DataFrame shape (`bitstring`, `counts`, etc.). The `parameters` argument is not supported for OpenQASM strings (use a `QuantumProgram` for Qmod `main` parameters, or bind parameters inside the QASM circuit). See the [Execution](../user-guide/execution/#sampling-openqasm) section of the user guide. * Improved error messages related to qfunc arguments. * Added `emulate` on `AzureBackendPreferences` to enable IonQ hardware noise simulation on Azure Quantum when using an IonQ QPU target (`ionq.qpu.*`); ignored for other Azure targets. * Added a QSVM application with a `QSVM` class that provides `train`, `test`, and `predict` methods for easy implementation of Quantum Support Vector Machine training and data classification. The relevant notebooks in the classiq-library will be updated accordingly. ## Enhancements * Upgraded infrastructure to enable future support in AWS Marketplace. ## Enhancements * **Execution:** Added optional `emulate` on `IBMBackendPreferences` and `IBMConfig`. Set `emulate=True` (default `False`) to run on Classiq AerSimulator with an IBM noise model derived from the backend name (e.g. `ibm_pittsburgh`, `ibm_boston`). Only valid for real IBM hardware backends (not fake backends); backend name must be in `CLASSIQ_NOISE_MODELS`. See [IBM backends](../user-guide/execution/cloud-providers/ibm-backends) for details. * Added `ExecutionJob.get_submitted_circuits()` to return the final quantum circuits submitted to the provider (sample jobs only). The returned circuits reflect the actual QASM after transpilation and parameter assignment. Each circuit can be converted to QASM via `to_qasm()` or to a Qiskit `QuantumCircuit` via `to_qiskit()`. * **Amplitude threshold for state vector simulation:** Added `amplitude_threshold` to `ExecutionPreferences`. When running state vector simulation, only states with amplitude magnitude strictly greater than the threshold are included in the result. Defaults to `0` (filters exactly zero-amplitude states, same as before). Setting a higher threshold reduces the size of the result for circuits where most amplitudes are negligibly small. `include_zero_amplitude_outputs=True` overrides this and includes all states regardless of amplitude. See [State Vector Filtering](../user-guide/execution/state-vector-filtering) for details. * **QP Visualization:** Fixed a bug in the visualization of split operations. * Added `estimate_sample_cost` and `estimate_sample_batch_cost` to the Python SDK for user-facing cost estimation before executing quantum programs. These functions return a `CostEstimateResult` with `cost` and `currency` fields. See [SDK execution reference](../sdk-reference/execution) for details. ## Bug Fixes * Fixed error messages when calling built-in operations with the wrong number of arguments. ## Enhancements * **Predefined noise models for Classiq simulators:** Added optional `noise_model` on `ClassiqBackendPreferences` to run Classiq Aer, Nvidia, and Braket Nvidia simulators with a device-style noise model. The value is a predefined name in the form `_` (e.g. `ibm_pittsburgh`, `ibm_boston`). Supported names are listed in `CLASSIQ_NOISE_MODELS`; initially IBM backend names are supported. The noise model is built from the provider (e.g. via IBM Runtime) and passed to the underlying AerSimulator. * **IonQ execution:** Replaced explicit `noise_model` with an `emulate` flag on `IonqBackendPreferences` and `IonQConfig`. Set `emulate=True` (default `False`) to run on the IonQ simulator with a noise model derived from the backend name (e.g. `qpu.aria-1` → `aria-1`). Only valid when the backend is a QPU. See [IonQ backends](../user-guide/execution/cloud-providers/ionq-backends#usage) for details. * **QLayer check-pointing:** Improved save/load behavior for models containing a `QLayer`. Non-picklable `post_process` callables are now omitted when saving and can be restored via `layer.register_post_process()` after loading. Added `serializable_post_process=False` constructor option to suppress the related warning when `post_process` is intentionally non-picklable. ## Bug Fixes * **Execution:** When execution runs with no transpilation (`transpilation_level = None`), the circuit is now submitted to the provider without any backend transpilation when it already uses only the provider’s basis gates. If the circuit contains gates not supported by the provider, an error is raised that lists the provider’s supported gates, the invalid gates in the program, and suggests using `transpilation_level = "decompose"` (or `transpile_to_hardware = "decompose"` in ExecutionPreferences). * Re-enabled negative indices for classical arrays. * Fixed the problem where users were not redirected to the login page when attempting to synthesize while logged out. * **Classiq IDE:** Changed the exported CSV on the results page to match the displayed tables. * **QLayer:** Fixed saving models that contain a `QLayer` when `post_process` or the execution path use local functions or lambdas (e.g. `torch.save(model, path)` or saving an epoch state dict). Such callables are now omitted from the pickled state and re-created or restored after load so that save/load no longer raises "Can't pickle local object". * Added an indicative error message when calling a classical function with quantum arguments in an arithmetic expression. ## Enhancements * Added `use_double_precision` to `ClassiqBackendPreferences` to control numerical precision on Nvidia and Braket Nvidia simulators. Default is `False` (single precision). Set `use_double_precision=True` for double precision. See [Classiq backends](/user-guide/execution/cloud-providers/classiq-backends/#nvidia-simulator-usage) for details. ## Bug Fixes * Fixed an incorrect compilation when an arithmetic operation appears inside a `repeat` statement and the left-hand side uses the repeat index as a subscript. ## Interface Changes * Changed default vendor from Azure to Classiq. * Removed `max_depth` and `max_gate_count` from `Constraints`. These fields were deprecated since 0.52 and are no longer supported. ## Bug Fixes * Fixed a bug causing an internal error during synthesis when calling a function containing `foreach` twice or more. ## Enhancements * **Classiq Platform:** Updated the [platform home page](https://platform.classiq.io). ## Enhancements * The function `poly_inversion` now supports `error_type` (`"relative"` default or `"uniform"`) to choose between minimizing relative error |xp(x)-1| or absolute uniform error |p(x)-1/x| over x in \[1/kappa,1]. The same `error_type` option is also available in `poly_inversion_degree` and `poly_inversion_error`. * Added the [*foreach*](../qmod-reference/language-reference/statements/classical-control-flow#classical-foreach) statement to Qmod. *Foreach* iterates efficiently through the elements of a classical array. * Added support for `QBit`s in addition to `QNum`s in `lookup_table`. ## Interface Changes * Renamed `sample` to `cmain_sample` in legacy `cscope`. This does not affect any usage of ExecutionSession. ## Enhancements * Introduced [CUDA-Q Integration](/user-guide/execution/cudaq_integration), enabling translation of synthesized Qmod programs into Python CUDA-Q kernels, to leverage CUDA-Q’s high-performance simulation and hybrid quantum–classical workflows. ## Bug Fixes * Fixed compilation of symbolic values in concatenations (for example, `[q[i], q[j]]` where `i` and `j` are of type `CInt`). ## Interface Changes * Renamed parameter `run_through_classiq` of `BackendPreferences` to `run_via_classiq`. `run_through_classiq` is deprecated and will no longer be supported starting on 2026-03-09 at the earliest. ## Enhancements * **Classiq IDE:** * Adjusted the table shown in "State Vector" jobs. * Added a table to "Measurement Results" jobs. * Noise models are now available for IonQ simulators. ## Bug Fixes * **Classiq IDE:** Fixed the phase legend not matching the actual phase. * Fixed compilation of the `I` (identity) gate. * Fixed evaluation of constants in `main` function parameter types. ## Interface Changes * Removed previously deprecated `"count"` column from execution result dataframe. Use `"counts"` column instead. ## Enhancements * Reduced CX count and circuit depth for multi-controlled Pauli rotations (RX, RY, RZ). ## Enhancements * **Classiq IDE:** * Improved the appearance of the histograms and bar plots shown for results. * Improved native Qmod library usability. * **Classiq Studio:** Added support for light mode visualization in the Quantum Program (QP) visualizer. Users can now switch between light and dark themes for an improved viewing experience. * C12 Cloud support was added. See the [Cloud Providers](/user-guide/execution/cloud-providers/) section in the user guide. ## Bug Fixes * Fixed error message for a measurement under unitary context. ## Enhancements * Support array subscripts with missing slices (e.g., `arr[1:]` and `arr[:9]`) in Python Qmod. * Add qmod\_to\_qubit\_op function to convert from SparsePauliOp to OpenFermion's QubitOp data structure. ## Bug Fixes * IDE - Fix selection in Quantum Program Execution Jobs tab - Make the first job on the jobs list be selected * Fix synthesis of functions with `Output` quantum parameters called under `invert`. ## Bug Fixes * IDE - Fix expand / collapse of backend details in the HW Catalogue * In the dataframe, change the `count` field to `counts` so as not to override the built-in `count` method on pandas dataframes. The current `count` field is deprecated and will be removed in the next release. * Fix multi-value Boolean operations in Native Qmod (e.g., `a or b or c`). * Fix synthesis error when controlling `X` with >=13 control qubits. * Raise indicative errors when overriding internal functions (for example, when re-defining `prepare_state`). ## Enhancements * Support `QArray[QBit]` assignment statements, for example, `qarr |= [1, 0, 0, 1]`. * Improve implementation of in-place quantum subscript assignments (e.g., `x ^= subscript([1, 2, 3, 4], y)`). * IDE - Execution in the IDE now supports running through Classiq account for Amazon Braket, Microsoft Azure Quantum, and IonQ backends. Authorized users can enable this option using the "Run through Classiq" switch, which eliminates the need to provide their own credentials for these backends. This feature also includes spending tracking and budget management capabilities. * Support assignments of non-scalar variables, for example, `qarr1 |= qarr2`. Both variables must have the same type. * Improve error message when assigning a symbolic value to a generative parameter. ## Deprecations * IDE - Removed support for `max depth` and `max gate count` synthesis constraints in the model ## Interface Changes * Violations of [uncomputation rules](/qmod-reference/language-reference/uncomputation) are now flagged as errors instead of warnings. * The function `qubit_op_to_pauli_terms` in the `chemistry` module is deprecated due to incorrect order of qubits in its result. Please use `qubit_op_to_qmod` instead. ## Bug Fixes * Resolved an issue with labeling CX gate inputs in QP visualization, so that the correct qubit labels are now displayed. * Fixed `qfunc`s being expanded declaratively (symbolically) when passed as arguments to other `qfunc`s. * Improved the error message for type errors with function arrays. * Fixed error message for `nan` values. * Fix mapping from output registers to measured qubits on parametrized repeat circuits with HW aware synthesis. * Fix `phase` applied with an execution parameter under multiple controls. * Fix `unscheduled_suzuki_trotter` and `qdrift` implementations. * Fix SDK function get\_execution\_actions * Flattened the cost field. The result holds now cost, currency\_code * Add session\_id in the result * Fix compilation of unused variables defined in nested blocks. ## Classiq Studio * Classiq Studio now includes built-in AI integration, allowing users to generate, optimize, and execute quantum models directly within the Studio. This capability leverages Classiq's quantum resources and requires no external API tokens. ## Enhancements * Add quantum modular arithmetic functions to the open library, enabling modular quantum operations: modular addition, multiplication, squaring, negation, and inversion: `modular_add_inplace`, `modular_double_inplace`, `modular_negate_inplace`, `modular_multiply`, `modular_square`, `modular_multiply_constant`, `modular_multiply_constant_inplace`, `modular_to_montgomery_inplace`, `modular_montgomery_to_standard_inplace`, `modular_inverse_inplace`, `kaliski_iteration`, and `modular_rsub_inplace`. * Add functions `get_execution_actions` and `get_execution_actions_async` return a pandas DataFrame of execution actions. Filter by id, session\_id, status, name, provider, backend, program\_id, cost range (total\_cost\_min/max), and time ranges (start\_time\_min/max, end\_time\_min/max). All filters are combined with AND logic. * Add functions `get_synthesis_actions` and `get_synthesis_actions_async` return a pandas DataFrame of synthesis actions. Filter by id, status, backend, program\_id, backend\_name, optimization\_parameter, random\_seed, max\_width, max\_gate\_count, cost range (total\_cost\_min/max), and time ranges (start\_time\_min/max, end\_time\_min/max). All filters are combined with AND logic. ## Bug Fixes * Fix uncomputation of function calls with input or output concatenations. * Fix within-apply bug caused by variable declarations nested in the "within" block. ## Bug Fixes * Fix memory issue in Synthesis queue mechanism. ## Enhancements * Add functions for encoding conversions between binary, unary, and one-hot representations: `binary_to_one_hot`, `binary_to_unary`, `one_hot_to_unary`, `one_hot_to_binary`, `unary_to_one_hot`, `unary_to_binary`, `inplace_binary_to_one_hot`, and `inplace_one_hot_to_unary`. * Change precision of GPU simulators to single-precision to make better use of hardware. ## Bug Fixes * Fixed a bug in Hardware-Aware Quantum Program. * Fixed [concatenations](/qmod-reference/language-reference/quantum-variables/#concatenation-operator) arguments to input and output quantum parameters. ## Classiq IDE * Fixed a bug in Jobs page after Executing on several hardwares. * Added a better labeling in the QP visualization. Now on "Show label" user will see label as on the closed box. On hover user will see full label. * Fixed issue with duplicate error snackbar in the Jobs page. ## Interface Changes * Function `exponentiation_with_depth_constraint` is deprecated and will no longer be supported starting on 2025-12-10 at the earliest. Instead, use `exponentiate`. ## Enhancements * Add SX gate to Qmod core functions ## Classiq IDE * Basis Gates field under “Hardware Aware” now starts empty. If left empty, synthesis applies default basis gates automatically based on connectivity; select one or more gates to override. ## Interface Changes * Update the QSVT functions to use [Capturing](/qmod-reference/language-reference/operators/?h=captur#capturing-context-variables-and-parameters). See the following [example](/qmod-reference/library-reference/open-library-functions/qsvt/qsvt/). ## Enhancements * Add functions for getting polynomial approximations for common use cases - `poly_jacobi_anger_<>` for Hamiltonian Simulation and `poly_inversion` for matrix inversion. ## Bug Fixes * Fix an issue that caused certain execution runs to fail due to incompatible gates ## Bug Fixes * Fixed minor issues in registration flow ## Enhancements * Qmod now supports **automatic uncomputation** of local variables and enforces rules that guarantee their correct uncomputation. Similar rules are enforced on variables initialized inside a *within-apply* statement. For more details, see [Uncomputation](../qmod-reference/language-reference/uncomputation). Currently, violations of uncomputation rules are issued as warnings for backward compatibility. These will become compilation errors no earlier than 2025-12-03. * Add the `assign_amplitude_table` and `assign_amplitude_poly_sin` open-library functions, to replace the `*=` operator and the `assign_amplitude` function. * Add [`unscheduled_suzuki_trotter`](../sdk-reference/qmod/functions/core_library/exponentiation/#classiq.qmod.builtins.functions.exponentiation.unscheduled_suzuki_trotter), a variant of `multi_suzuki_trotter` that doesn't re-order the Pauli terms. * Improve synthesis of controlled phase with theta=pi. * This release includes initial support of classical local variables, assignment of mid-circuit measurements, and runtime if statements. Currently, only variables of type QBit can be measured, and only the classical bool type is supported for local variable declaration and assignment. This enables simple algorithms such as the quantum teleportation protocol. These constructs have the corresponding dedicated graphics in the quantum program visualization. See more details under [Classical variables](/qmod-reference/language-reference/classical-variables)) and [Mid-circuit measurement](../qmod-reference/language-reference/mid-circuit-measurement). ## Interface Changes * The `*=` operator and the `assign_amplitude` function are deprecated and will no longer be supported starting on 2025-12-03 at the earliest. Use `assign_amplitude_table` instead. * Function `qdrift` now receives a sparse Hamiltonian ([`SparsePauliOp`](../sdk-reference/qmod/classical-types/#classiq.qmod.builtins.structs.SparsePauliOp)) instead of a list of non-sparse Pauli terms (`CArray[PauliTerm]`). Non-sparse pauli terms in `qdrift` will no longer be supported starting on 2025-12-03 at the earliest. ## Bug Fixes * Fix `multi_suzuki_trotter` synthesis with symbolic evolution coefficients raising an internal error. * Fix a bug where an internal error is raised when synthesizing with a maximum width constraint a model that contains control on a function, or a statement block, that allocates and frees qubits. ## Security * Internal dependencies have been upgraded to address security vulnerabilities. ## Bug Fixes * Fix transpilation and execution of `unitary` functions when compiling to QASM2. ## Enhancements * We've changed our AWS Braket integration, now AWS credentials consist of an access key ID and a secret access key. For more information, see [AWS Credentials](https://aws.amazon.com/blogs/quantum-computing/setting-up-your-local-development-environment-in-amazon-braket/). * Added the function `prepare_select` for the definition of structured Linear Combination of Unitaries primitive (LCU) schemes. ## Bug Fixes * Report assignments into non-numeric variables. * Fix concatenation operator on single variable not casting to QArray. * Fix lambda list (`QCallableList`) scoping issue causing a bug when a lambda list item is invoked in a different lambda. * Fix error which caused execution jobs on IonQ to fail if they took more than 5 minutes. ## Interface Changes * The **debug\_info** field was removed from the QuantumProgram class ## Bug Fixes * Report quantum types instantiated with execution parameters. ## Deprecations 1. Python 3.9 is no longer supported in the Python SDK. The minimum supported version is now Python 3.10. ## Enhancements * Added the [`skip_control` statement](/qmod-reference/language-reference/statements/skip-control/) to the Qmod language. `skip_control` applies a quantum statements unconditionally. * Add `pauli_operator_to_matrix`, the sparse counterpart of `hamiltonian_to_matrix`. * Add new quantum functions for modular arithmetics: `modular_add_qft_space`, `modular_multiply`, and `inplace_modular_multiply`. Those functions use `skip_control` statements for specifying their efficient controlled version. ## Bug Fixes * Fix a bug where an error is raised when synthesizing with a maximum width constraint, even though a solution exists. * Fix `qasm_to_qmod` quantum argument size calculations (resulting in, for instance, illegal `control` statements generated from `mcx` gates). ## Deprecations * SDK versions below 0.92 will be deprecated as planned from October 13, 2025 (at the earliest). As a one-time exception, the version deprecation error message will not be as usual. Instead of “You are using an unsupported version of Classiq SDK—... “, users will experience a “504 Gateway Timeout ERROR” or a similar message. We apologize for the inconvenience. ## Bug Fixes * Fix a bug where an internal error occurred during hardware-aware synthesis with a basis gate set of Clifford + T. * Fix `allocate` with floating-point size when the `max_width` constraint is set. ## Classiq Studio * A progress bar was added to the classiq studio start up page. ## Deprecations * Python version 3.9 will no longer be supported starting on 2025-10-01 at the earliest. ## Enhancements * The new [`qasm_to_qmod` function](/sdk-reference/synthesis#qasm_to_qmod) de-compiles QASM 2 or 3 into Python/Native Qmod source code. * Added methods for [execution budget management](/user-guide/execution/budget-management). * Classiq's AI agent is now installed in Classiq Studio, providing seamless AI-powered quantum development. See the [AI documentation](/user-guide/ai/index) for setup instructions. * Remove negligibly small amplitudes from state vectors when the auxiliary (non-output) qubits are non-zero. These amplitudes are caused by numeric error during simulation, but they result in multiple states with the same assignment for the variables, which caused confusion. * Added several Quantum Signal Processing [(QSP)](/sdk-reference/applications/QSP) related functions to the SDK: `qsvt_phases` for obtaining QSVT phases, `qsp_approximate` for approximating QSP-compatible Chebyshev polynomials, `gqsp_phases` for calculating Generalized-QSP (GQSP) phases, and a quantum `gqsp` function that implements GQSP. To use these functions the user should `pip install classiq[qsp]`. The existing QSVT examples and a new GQSP example will be updated in the library towards the next version release. ## Bug Fixes * Fix a bug where the visualization generated unnecessary variable splits and assigned incorrect variable names in certain cases. * Support classical functions in the cost expression of `minimize`. * Prevent invisible blocks from being collapsed. * Adjust CZ gate boundaries to align with disconnected variable lines. * Fix Studio loading stuck until page reload. ## Bug Fixes * **Visualization tooltips are now supported and displayed in the Studio.** This enhancement improves the user experience by providing helpful information directly within visualizations, making it easier to understand and interact with your data. ## Enhancements * Add magnitude and phase to the dataframe for state vector simulations. * Increase the limit of the number of qubits that can be simulated on Classiq's Simulator from 25 to 28. Note that wider circuits take longer to simulate, so Classiq's Nvidia Simulator will likely have improved performance for wide/deep circuits. Also, increase the limit on Classiq's State Vector Simulator to 28 qubits, as long as [State Vector Filtering](/user-guide/execution/state-vector-filtering/) reduces the number of output (unfiltered) qubits to 18 or less, the previous limit. For example, it is now possible to get the state vector from a quantum program of 20 qubits with a 2-qubit QNum filtered out. * Add support for bitwise operators in [phase statements](/qmod-reference/language-reference/statements/phase/#semantics). ### Documentation Enhancements * A new tutorial on execution is now available under [The Classiq Tutorial](/getting-started/classiq_tutorial/) * New explanations on visualization features in the [Synthesis Tutorial](/explore/tutorials/basic_tutorials/the_classiq_tutorial/synthesis_tutorial/) * Search results have been improved to show the most relevant information at the top. ## Enhancements * Loading of large Pandas Dataframes is now available in the studio, up to 200MB files. * Use non-blocking flow for execution jobs on the Classiq Nvidia Simulator. This allows for longer-running jobs. * The 'phase' statement has been generalized to support a fixed (classically specified) rotation angle, that is, to insert a global phase. A global phase across an entire circuit is undetectable in quantum hardware, but when applied in a controlled context, it introduces a relative phase between positive and negative condition states. This variant of the 'phase' statement is useful to directly express key idioms in quantum algorithms such as phase oracles, reflections, and relative-phase computations. ### QP Visualization: Improved Handling of Long Function Blocks Sub-labels Long sub-labels in the quantum program visualizer are now automatically truncated to prevent overflow and maintain a clean layout. When a sub-label is truncated, the full text is accessible via a tooltip on hover, ensuring that all information remains available without cluttering the interface. This enhancement improves readability and usability, especially for circuits with verbose or complex expressions. ## Deprecations * The following functions and classes have been deprecated and will no longer be supported starting on 2025-09-18 at the earliest: `construct_chemistry_model`, `molecule_ucc`, `molecule_hva`, `molecule_hartree_fock`, `fock_hamiltonian_hva`, `fock_hamiltonian_hartree_fock`, `GroundStateProblem`, `MoleculeProblem` and `HamiltonianProblem`. For more information on Classiq's chemistry application, see [here](/explore/applications/chemistry/classiq_chemistry_application/classiq_chemistry_application). ## Bug Fixes * Fix a bug where allocating quantum variables in disallowed blocks was not always reported. * Fix a bug where controlled free operations could cause the visualization to crash. The visualization now handles these cases correctly and no longer fails when such operations are present. ## Interface Changes * Parameter `expr` of function 'phase' has been renamed to `phase_expr`. Parameter `expr` will no longer be supported starting on 2025-09-19 at the earliest. Change `phase(expr=..., theta=...)` to `phase(phase_expr=..., theta=...)` or `phase(..., ...)`. ## Classiq Studio * You can now install and import Torch, PyQSP, and CUDA-Q packages in the Classiq Studio environment. Previously, these imports failed due to environment limitations, but they are now supported for your workflows. * PyGLPK package is installed and usable in the Classiq Studio. * Uploading files up to 1 GB to the user persistent workspace in the Classiq Studio is now enabled. ## Enhancements * Added the functions `prepare_sparse_amplitudes` and `inplace_prepare_sparse_amplitudes` to the function library. * Add support for numpy 2.2.6 (Python >=3.10) and numpy 2.3.2 (Python >=3.11) * Shared QP links now come with the actual visualization image previews. ## Security * Improved web application security ## Enhancements * Added the function `prepare_linear_amplitudes` for preparing the state $|\psi\rangle = \frac\{1\}\{Z\}\sum_\{x=0\}^\{2^n-1\}\{x|x\rangle\}$. * Extended the visualization of controlled functions as transparent boxes displaying control lines to all Qmod statements. * Support [state vector filtering](/user-guide/execution/state-vector-filtering) for Classiq's `simulator_statevector`. * Added tolerance parameter to the minimize method of the execution session. ## Enhancements * Added the functions `lcu` and `lcu_pauli` for creating the Linear Combination of Unitaries primitive (LCU). * IBM Cloud is now available. See [Cloud Providers](/user-guide/execution/cloud-providers/) section in the user guide. * The default optimization level in the synthesis preferences has been changed from `OptimizationLevel.HIGH` (3) to `OptimizationLevel.LIGHT` (1). For more information about optimization level see the [optimization level](/user-guide/synthesis/preferences/#optimization-level) section in the user guide. ### Introducing “Variables View” A new compact visualization mode that displays quantum variable flow at a higher level perspective. Toggle between this streamlined view and the traditional qubit grid overlay using the Variables View switch in the visualization menu bar.” ## Classiq Studio ### Memory monitor A new memory monitoring feature that tracks and displays resource usage during Classiq Studio Usage, providing real-time insights into memory consumption patterns. plot ## Bug Fixes * Fix a synthesis bug that causes, in some cases, the function under power to have a separate power for each function, instead of sharing it as a whole. * Fix a failure in hardware-aware synthesis of parametric models for the `Azure Quantum` provider. * Fix a bug in `get_hf_state` when using qubit tapering. ## Enhancements * [Concatenations](/qmod-reference/language-reference/quantum-variables/#concatenation-operator) can now also be used as [control](/qmod-reference/language-reference/statements/control/) expressions. * Lower model creation and synthesis run times for large models. ## Bug Fixes * Classical `if` statements are now supported in QP visualization, where they previously caused a failure. * Fix synthesis bug which caused wrong results when using the bitwise invert (`~`) operator. * Fix bug relating to classical struct arguments (such as [Hamiltonians](/qmod-reference/language-reference/classical-types/#hamiltonians)). * Use the Solovay-Kitaev algorithm in more cases when transpiling. Previously, we used the algorithm when transpiling with respect to a particular fixed set of basis gates. Now, we use this algorithm whenever the basis gate set contains the Clifford gates `X`, `Z`, `H`, `T`, and `CX` but does not contain arbitrary-angle rotation gates such as `RX` or `CRZ`. ## Classiq IDE 1. Add mechanism to allow more models to be visualized in new visualization. 2. ### Visualization: Data Tab Removal The Data tab has been removed from the left panel in the visualization interface. This change is removing redundant functionality. 3. ### Deprecation: QP Visualization Basic View Mode Deprecated "Basic" mode quantum program visualization in favor of the new visualization scheme with enhanced designs, views, and analysis capabilities. 4. ### Visualization: Focused Search The search panel focuses only on user written functions and QMOD statements. ## Bug Fixes * Fix the value range analysis of numerical variables declared with specified size. ## Deprecations * The Qmod function `allocate_num` has been removed. ## Classiq IDE ### New Feature: QP Visualization Enhancements The Classiq IDE now includes an improved QP visualization feature. Controlled functions are displayed in transparent boxes blocks, where: * The transparent section represents the control mechanism. * The filled block highlights the controlled function. This enhancement provides a clearer and more intuitive representation of quantum programs, making it easier to understand and debug complex circuits. ## Enhancements * Added new [chemistry functions](/sdk-reference/applications/chemistry) to the Python SDK for using Hartree-Fock and UCC ansatz in Qmod. In order to use them, it is required to install the Python SDK with the extra `chemistry` dependency. * Qmod functions are represented with a single symbolic definition throughout the compilation process when their use of classical parameters allows it. This improves compilation time and output QASM code size in many cases. * Added a new function, [`multi_suzuki_trotter`](http://../sdk-reference/qmod/functions/core_library/exponentiation/#classiq.qmod.builtins.functions.exponentiation.multi_suzuki_trotter), that applies the Suzuki-Trotter decomposition jointly to a sum of Hamiltonians. * Added a new syntax for specifying sparse Hamiltonians ([`SparsePauliOp`](http://../sdk-reference/qmod/classical-types/#classiq.qmod.builtins.structs.SparsePauliOp)) in Qmod's Python embedding. For example: `0.5 * Pauli.Z(0) * Pauli.Y(1) * Pauli.X(2) + 0.8 * Pauli.X(1)`. Check out the [documentation](http:///qmod-reference/language-reference/classical-types/#hamiltonians) for more details. * Added a `dataframe` property to ExecutionDetails object. Example usage: ``` with ExecutionSession(qprog) as es: result = es.sample() df = result.dataframe ``` * Added a new execution primitive [`ExecutionSession.minimize`](/sdk-reference/execution#classiq.execution.ExecutionSession.minimize) that encapsulates classical optimization of ansatz parameters. The cost function to minimize is specified either as a quantum observable (Hamiltonian) or an arithmetic expression. This offers a significant performance advantage compared to executing the same logic on the client side, as it eliminates communication overhead. Note that it utilizes a fixed generic minimization scheme (`scipy-COBYLA`). In non-trivial cases, you may still need to implement your own optimization logic. * The total size of quantum structs can be retrieved using the new of `QStruct`. ## Interface Changes * Function `sparse_suzuki_trotter` will no longer be supported starting on 21/7/25 at the earliest. Instead, use `suzuki_trotter`. * Function `parametric_suzuki_trotter` will no longer be supported starting on 21/7/25 at the earliest. Instead, use `multi_suzuki_trotter`. * Function `suzuki_trotter` now receives a sparse Hamiltonian ([`SparsePauliOp`](http://../sdk-reference/qmod/classical-types/#classiq.qmod.builtins.structs.SparsePauliOp)) instead of a list of non-sparse Pauli terms (`CArray[PauliTerm]`). Non-sparse pauli terms in `suzuki_trotter` are deprecated, use `SparsePauliOp` instead. * [`ExecutionSession`](http:///sdk-reference/execution/#classiq.execution.ExecutionSession)'s `estimate` methods now accept a sparse Hamiltonian ([`SparsePauliOp`](http://../sdk-reference/qmod/classical-types/#classiq.qmod.builtins.structs.SparsePauliOp)) instead of a list of non-sparse Pauli terms (`CArray[PauliTerm]`). Non-sparse pauli terms in `ExecutionSession` will no longer be supported starting on 21/7/25 at the earliest. Use `SparsePauliOp` instead. ## Enhancements * Added the concatenation operator to Qmod. This operator packs a sequence of quantum variables (or their parts) into a qubit array. For example, `hadamard_transform([my_qnum, my_qarray[1:3]])` passes an array of the respective qubits to the function. See more under [concatenation operator](/qmod-reference/language-reference/quantum-variables/#concatenation-operator). * Width constraints are now treated more systematically for optimization levels 0-2 (partitioning the model into separate synthesis steps), yielding solutions in cases where previously no solution was found. See more under [Optimization Level](/user-guide/synthesis/preferences#optimization-level). ## Interface Changes * The type `SerailizedQuantumProgram` and method `QuantumProgram.get_qprog` are no longer available. ## Bug Fixes * Removed the "Login required" error message when not authenticated ## Enhancements * In Quantum Program visualization, engine-level (grey) boxes are flattened when they are single children. ## Deprecations 1. The Qmod function `allocate_num` is deprecated and will no longer be supported starting on 16/06/2025 at the earliest. Instead, use which supports the same parameters. ## Security * Updated dependencies to fix security vulnerabilities. ## Enhancements 1. Quantum functions inside a control statement get a `c-` prefix 2. Added the functions `prepare_dicke_state` and `prepare_dicke_state_unary_input`. 3. A new exponentiation function, [sparse\_suzuki\_trotter](../sdk-reference/qmod/functions/core_library/exponentiation#classiq.qmod.builtins.functions.exponentiation.sparse_suzuki_trotter), has been added to the core library, performing Suzuki-Trotter decomposition using a sparse representation of the Hamiltonian. It is recommended to use this function instead of `suzuki_trotter` when you handle a sparse Hamiltonian, as it is more efficient. 4. Include `classiq.execution` in the top level `classiq` package in the SDK. Old: ``` from classiq.execution import ExecutionPreferences ``` New: ``` from classiq import ExecutionPreferences ``` 5. [`allocate`](../sdk-reference/qmod/operations#classiq.qmod.builtins.operations.allocate) now supports specifying numeric attributes of the allocated variable. ## Bug Fixes 1. Fix a system bug that caused execution jobs to fail with an "insufficient resources" message. 2. Fix a bug related to execution parameters. ## Enhancements 1. Value range analysis of arithmetic expressions is now generalized to apply across statements, optimizing quantum variables sizes and arithmetic expressions implementation in more cases. ## Enhancements 1. A new `IQAE` application has been added to the SDK. It allows you to define `iqae` problems in terms of Qmod function, and directly estimate the amplitude of the state you prepared. It is recommended to use this application in `iqae` problems, rather than `cmain` with the primitive `iqae`. ## Bug Fixes 1. Fixed an issue where the “Basic” view was always empty; it now correctly renders the circuit as intended. 2. Fixed bugs causing several example models to not be supported in the "New" circuit visualization, including "Shor's Algorithm Modular Exponentiation". ## Enhancements 1. In Qmod's Python embedding, you can now declare quantum functions with classical parameters of Python builtin types such as `int`, `float`, and `list`. These variables can be used in expressions that require Python values, such as Python `for` loops and 3rd party library functions, as is shown in the example below. See more on this under the new reference page on [comment]: DO_NOT_TEST ```python theme={null} @qfunc def rotate(ratio: float, qa: QArray[QBit]): for i in range(qa.len): # 'qa.len' is a Python integer PHASE(math.asin(ratio * i), qa[i]) # 'ratio * i' evaluates to a Python float ``` ## Interface Changes 1. With *Enhancement 1* described above, Python-type parameters supersede classical Qmod-type parameters in generative functions. Hence, the use of the function decorator `@qfunc(generative=True)` is no longer required. Qmod-type parameters are treated symbolically in Python, and their use in Python expressions is deprecated. ## Classiq IDE 1. Improved UX in Quantum program visualization: collapsing a Quantum operation block will scroll the viewport into the parent operation block position ## Enhancements 1. It is now legal to [out-of-place assign](/qmod-reference/language-reference/statements/assignment#out-of-place-assignment) to a variable whose declared size is larger than the minimal size required to fit the range of possible expression values. ## Interface Changes 1. The function `synthesize` now returns an object of type `QuantumProgram`, and its fields can be accessed directly. The type `SerailizedQuantumProgram` and method `QuantumProgram.get_qprog` are no longer needed, they are deprecated and will be removed in a future version. 2. Email Service Update - We've replaced the Community button with a new dropdown menu. Users can now select "Contact Us", which opens a form. Once the form is submitted with user details, an email is sent directly to Classiq. 3. Layout Fixes - Resolved an issue with inconsistent padding across the layout to ensure a cleaner, more polished UI. 4. Enhanced Sharing Options - In addition to existing platforms, users can now share content to LinkedIn and Reddit with a single click. ## Classiq Studio Added a new command `reset-user-env` to reset the virtual environment to its default state helping resolve dependency issues and clean up installations. ## Classiq IDE Added a "User Terms" button inside the IDE website at the bottom of the screen, and clicking on it opens a modal displaying our user terms. ## Enhancements When estimating using the `ClassiqSimulatorBackendNames.SIMULATOR_STATEVECTOR` backend, whether using the `ExecutionSession` or classical main, compute the expectation value directly from the state vector instead of running shots. ## Enhancements * `QuantumProgram` objects have been optimized to be significantly leaner, improving performance across all actions that handle them. * Visualizing Quantum Programs is now significantly faster. * Added the functions `prepare_complex_amplitudes` and `inplace_prepare_complex_amplitudes`. ### Quantum Program Visualization * Auto-expand QMOD statements without expressions (e.g. `power`). plot * Statements with quantum expressions now display their corresponding expression directly on the block. plot ## Interface Changes 1. The functions `construct_qsvm_model`, `construct_finance_model`, and `construct_grover_model` have been removed from the SDK. Check out our Qmod implementations of the QSVM, finance, and Grover algorithms in the [Classiq library](https://github.com/Classiq/classiq-library). ## Enhancements 1. Improve `control` statement visualization to distinguish control qubits from target qubits. ## Enhancements 1. Add new execution backends `BRAKET_NVIDIA_SIMULATOR` and `BRAKET_NVIDIA_SIMULATOR_STATEVECTOR`. These simulators run on Amazon Braket's infrastructure and provide faster execution for single circuits. See [Execution on Classiq Backends](/user-guide/execution/cloud-providers/classiq-backends) for more information. 2. Improve `prepare_amplitudes` and `prepare_state` performance for `bound=0`. 3. Add [`RESET`](../sdk-reference/qmod/functions/core_library/mid_circuit_measurement/#classiq.qmod.builtins.functions.mid_circuit_measurement.RESET), an atomic function that resets a qubit to the `|0>` state. 4. Intel simulator is now available as a backend for execution. See [Cloud Providers](/user-guide/execution/cloud-providers/) section in the user guide. ## Classiq IDE 1. Models page is now open to non-signed-up users. ## Classiq Studio 1. Trust Classiq Library workspace by default. ## Enhancements 1. Improve depth and gate count for transpilation options "intensive" and "custom". 2. Improve the synthesis of the `suzuki_trotter` function for small Hamiltonians. 3. Updated the Quantum program icon in the drawer to a newer version. ## Bug Fixes 1. Fixed a bug where arithmetic expressions that classically evaluate to constant boolean values could not be used. 2. Fixed a bug where using the `show` function in the Python SDK would open the IDE with a "Not Authorized" error. 3. Fixed a bug where certain operations in the Quantum Program visualization are displayed with very long name that describes the operation's hierarchy. ## Classiq Studio ### **We’re launching Classiq Studio!** Classiq Studio is a web-based coding environment where you can write and run Python code in a pre-configured setup. Access it now via the [Classiq Platform](https://platform.classiq.io). For details, see the [Classiq Studio user guide](/user-guide/studio/index). ## Enhancements 1. Improved the synthesis of the `molecule_ucc` function for small molecules. 2. Social Sharing is now available. You can share circuits to various social platforms. 3. Added SLSQP optimizer for use in [VQE](/user-guide/execution/). ## Interface Changes 1. Introduced a new QMOD core-library function [`commuting_paulis_exponent`](../sdk-reference/qmod/functions/core_library/exponentiation#classiq.qmod.builtins.functions.exponentiation.commuting_paulis_exponent). ## Bug Fixes 1. Fixed unexpected resets when using Classiq Studio. ## Support 1. Error messages now include a link to our [support system](https://classiq-community.freshdesk.com/support/tickets/new) for reporting bugs or opening support tickets. You can also reach us on our [Slack community channel](https://short.classiq.io/join-slack). ## Enhancements 1. Arithmetic assignments and control conditions now support [quantum subscript expressions](/qmod-reference/language-reference/expressions/#path-operators). A quantum subscript expression comprises a classical list accessed by a quantum subscript, e.g., `x |= subscript([1, 2, 3, 4], y)` (in Native Qmod: `x = [1, 2, 3, 4][y];`). 2. Report an indicative error when not releasing local variables inside [control](/qmod-reference/language-reference/statements/control), [invert](/qmod-reference/language-reference/statements/invert) and [power](/qmod-reference/language-reference/statements/power) statements. 3. When running VQE using the `ClassiqSimulatorBackendNames.SIMULATOR_STATEVECTOR` backend, compute the expectation value directly from the state vector instead of running shots. 4. In the Python SDK, arguments of type `CArray` can now be NumPy arrays, tuples, and similar sequential objects. For example, the following statements are equivalent: `prepare_state([0.25, 0.25, 0.25, 0.25], 0, q)` and `prepare_state(np.ones(4) / 4, 0, q)`. ## Interface Changes 1. The `synthesize` and `write_qmod` functions now accept a [quantum entry point](/qmod-reference/language-reference/quantum-entry-point/?h=quantum+entry+point#model-outputs). Instead of `synthesize(create_model(main))`, write `synthesize(main)`. ## Enhancements 1. Unreleased local variables of a function are now un-computed and released when calling the function under a *compute* block of a statement. ## Bug Fixes 1. Fix control optimization for constant equality conditions (e.g., `control(x == 1, ...)`) producing wrong circuits under certain conditions. 2. Fix in-place XOR optimization for classical values (e.g., `x ^= -0.5`) producing wrong circuits under certain conditions. 3. Fix in-place XOR optimization for Boolean expressions (e.g., `x ^= (y > 0) & (z > 0)`) producing wrong circuits under certain conditions. 4. Fix a bug causing an internal error in arithmetic expression that use comparison, subtraction, or negation (e.g. `x > 0.3`, `0.2 - x`, `-x`) when synthesizing models with the machine precision set to higher than 8. ## Interface Changes 1. Add the functions `amplitude_amplification` and `exact_amplitude_amplification` to the function library. ## Enhancements 1. The `num_qubits` argument of function `allocate` is now optional. If it is not specified, it is inferred automatically according to the quantum type of the allocated variable. Example: [comment]: DO_NOT_TEST ```python theme={null} q = QBit() allocate(q) # allocates one qubit for variable 'q' ``` 2. The `execute` parameter of the `QLayer` object is now optional. Example: [comment]: DO_NOT_TEST ```python theme={null} QLayer(quantum_program, post_process) ``` ## Deprecations The simulator name "nvidia\_state\_vector\_simulator" has been removed. Please use ClassiqNvidiaBackendNames.SIMULATOR or "nvidia\_simulator" instead. ## Classiq IDE 1. The Application Configuration Panel used for editing examples in the Built-in Apps folder has been removed. 2. Patched dependencies: Katex. ## Enhancements 1. Increase jobs memory. ## Bug Fixes 1. Fix failed execution jobs which stay in "running" status forever instead of reporting an indicative error. ## Enhancements 1. Improve overall stability and performance. ## Enhancements 1. The name argument of local [quantum variables](/qmod-reference/language-reference/quantum-variables/) is now optional. If a name is not specified, it is inferred automatically from the Python code. For example, it is now possible to write `q = QBit()` instead of `q = QBit("q")`. 2. Improve the performance of executing multiple primitives inside one `ExecutionSession`, such as in `execute_qaoa`. ## Classiq IDE Share Quantum Programs with anyone. For more information, see [Sharing your Quantum Program Visualization](/user-guide/analysis/visualization-of-quantum-programs#sharing-your-quantum-program-visualization). ## Deprecations Function `prepare_int` and `inplace_prepare_int` are now deprecated. Use Qmod out-of-place and in-place numeric assignment statements instead. For example, instead of `prepare_int(5, my_qnum)`, write `my_qnum |= 5`. See more under [numeric assignments](/qmod-reference/language-reference/statements/assignment/). ## Enhancements 1. Improve error messages. ## Bug Fixes 1. Fix quantum bits raising an error when used in in-place assignments (`^=` and `+=`). 2. Fix quantum struct field access raising an error in lambdas. ## Classiq IDE 1. Classiq Thumbnail: Updated the Classiq thumbnail image! 2. Links on the IDE model page now directing to Classiq Library. 3. Modify the Quantum Programs URL so that the circuit ID is embedded in its path ### New Visualization: Compact View: * Clean up variable names in engine blocks. * Corrected the spacing miscalculation for junction dots. * Refined padding before allocation * Hide labels in open functional blocks to create a more space-efficient display. * Removed unnecessary dots and labels for low-level QC elements. ## Enhancements 1. [Generative functions](/qmod-reference/language-reference/generative-descriptions) have undergone a round of significant improvements. Check out our [DQI notebook](/explore/algorithms/search_and_optimization/dqi/dqi_max_xorsat/) to see how generative functions are used to implement advanced quantum algorithms. 2. A new filed `optimization_level` has been added the `Preferences` of the synthesis. This field determines the trade-off between synthesis speed and the quality of the results, In terms of the optimization parameter and the constraints. For more information, see [here](/user-guide/synthesis/preferences#optimization-level). 3. State vector filtering is available. This is an important step for simulating large circuits with a state vector simulator. For more information, see [this page](/user-guide/execution/state-vector-filtering) ## Bug Fixes 1. Fix usage of overlapping quantum array slices (e.g., `qbv[1:3]` and `qbv[2]`) in bodies of lambda expressions and control statements. ## Enhancements 1. Added a new simulator under the Classiq provider, under the name `ClassiqNvidiaBackendNames.SIMULATOR_STATEVECTOR`. This simulator runs on a GPU and returns a state vector, similar to `ClassiqSimulatorBackendNames.SIMULATOR_STATEVECTOR` (which runs on a CPU). Thus, it can handle larger circuits. The name `"nvidia_state_vector_simulator"` is deprecated in favor of `ClassiqNvidiaBackendNames.SIMULATOR`. See [here](/user-guide/execution/cloud-providers/classiq-backends#nvidia-simulator-usage) for more information. ## Bug Fixes 1. Fix the connectivity map of the `rigetti.qpu.ankaa-9q-3` backend in Azure Quantum. 2. Fix the connectivity map of Amazon Braket devices. 3. Fix a bug in the method `from_id` of `ExecutionJob`. ## Deprecations 1. Parameters `value` and `target` of functions `inplace_add` and `inplace_xor` have been renamed to `expression` and `target_var` respectively. * In Native Qmod, use `+=` and `^=` instead of `inplace_add` and `inplace_xor` respectively. ## Classiq IDE 1. Qmod examples on the Model page now contain direct links to their respective tutorials in the Classiq documentation. Simply hover over an algorithm list item from the QMODs list in the left panel and a tooltip will appear with link to the relevant tutorial. ### New Visualization: 1. Captured variables are now shown as uninitialized. ## Enhancements 1. Add a demonstration of the Decoded Quantum Interferometry (DQI) algorithm. See [notebook](/explore/algorithms/search_and_optimization/dqi/dqi_max_xorsat/). ## Bug Fixes 1. Operands' list elements can be used as operands (in expressions such as, e.g., `[op_list[0]]`). ## Interface Changes 1. Add the function `qsvt_lcu` for efficiently encoding QSVT polynomials with indefinite parity. 2. Subsequent invocations to execution primitives inside `ExecutionSession` now use different random seeds (depending on the initial seed) to avoid getting the exact same simulation results inside a session. ## Classiq IDE ### New Visualization: 1. Clean up variable names in engine blocks. ## Interface Changes 1. `ExecutionSession` needs now to be explicitly closed, and it is recommended to use it as a context manager. See [here](/user-guide/execution/ExecutionSession) for more information. ## Classiq IDE 1. Credentials for Alice & Bob hardware are now optional. Quantum programs run on Alice & Bob backends will use Classiq's credentials by default. ## Enhancements 1. Allow different parts of a quantum struct or array to be passed in different sections of a quantum statement: [comment]: DO_NOT_TEST ```python theme={null} @qfunc def main() -> None: qbv = QArray("qbv") allocate(2, qbv) # The following line previously raised an exception, but is now valid control(qbv[0], lambda: H(qbv[1])) ``` ## Deprecations 1. Using `control` as a keyword parameter for standard gates (such as `CX`) is no longer supported. Use `ctrl` instead. ## Bug Fixes 1. Fix package dependencies. ## Enhancements 1. This release introduces a new version to the Quantum Program (QP) visualization tool in parallel to the legacy visualization. 2. The new visualization version offers advanced visualization capabilities that bridge high-level algorithmic descriptions (Qmod) with gate-level implementations, incorporating interactive hierarchical views and data flow analysis. ### Feature Details #### Visualization Versions: Both the new version and the legacy version are available on the Quantum Program page. Users can toggle between two visualization versions: * 'New' version: Advanced visualization that includes quantum data flow views and new hierarchical block structures. * 'Basic' version: Legacy visualization. #### Documentation & Support: [Initial documentation](/user-guide/analysis/visualization-of-quantum-programs) : A basic guide is available to help users navigate and utilize the visualization tool’s key features. Tooltip: Integrated to the QP page to provide in-line initial guidance on key functions and elements. #### Known Issues and Limitations: Supported models: The 'New' visualization doesn't yet support all models. Unsupported models may not render - in these cases it is suggested to switch back to the 'Basic' visualization. In-Progress development: This is part of an initial release milestone, with major issues being actively addressed. #### Usage Recommendations Switching between versions: It’s recommended to switch between the 'New' and 'Basic' versions to evaluate visualization consistency and effectiveness for specific quantum programs. Feedback and bug reporting: Any issues, inconsistencies, or suggested improvements should be reported through the designated Slack channel for prompt review. #### Next Steps User feedback will inform ongoing improvements and prepare the tool for broader production release. Further enhancements and bug fixes are planned in alignment with Classiq’s high-level quantum design roadmap. ## Bug Fixes 1. Fix walkthrough bug in the IDE. 2. Change the default value of display\_url in show(circuit, display\_url) to True. ## Bug Fixes Fix a bug causing execution failures on IBM in version 0.56.0. ## Interface Changes 1. Added an optional `else` block to the `control` statement. ## Bug Fixes 1. Fix Pauli feature map circuit visualization. 2. Add missing `len` property to `QConstant`s of type `CArray` (Qmod/Python). ## Enhancements 1. Qmod/Python: Functions `assign`, `assign_amplitude`, `inplace_xor`, and `inplace_add` are equivalent to the operators `|=`, `*=`, `^=`, and `+=` respectively except that they can be used in operands (Python lambda functions): [comment]: DO_NOT_TEST ```python theme={null} within( lambda: assign(x, y), # y |= x lambda: inplace_xor(y, z), # z ^= y ) ``` 2. Support non-equation Boolean expressions as control conditions: [comment]: DO_NOT_TEST ```python theme={null} @qfunc def main(a: Output[QBit], b: Output[QBit], res: Output[QBit]) -> None: allocate(1, a) allocate(1, b) allocate(1, res) control(a & b, lambda: X(res)) ``` ``` qfunc main(output a: qbit, output b: qbit, output res: qbit) { allocate(1, a); allocate(1, b); allocate(1, res); control (a & b) { X(res); } } ``` ## Interface Changes 1. Parameters `value` and `target` of functions `inplace_add` and `inplace_xor` have been renamed to `expression` and `target_var` respectively. Parameters `value` and `target` will no longer be supported starting on 02/12/24 at the earliest. ## Bug Fixes 1. Solve a within-apply bug. ## Deprecations 1. Python 3.8 is no longer supported in the Python SDK. The minimum supported version is now Python 3.9. ## Enhancements 1. Optimize in-place XOR variable assignments (`x ^= y`). (The implementation no longer uses auxiliary qubits.) 2. Improve error messages in `CArray` (array) parameter declaration. 3. Support array subscripts and struct field access on the left-hand side of in-place arithmetic assignments (`qbv[0] ^= 1` and `my_Struct.field += 2.5`). 4. Optimize the controlled version of QFT arithmetic implementations by skipping controlling the QFT and QFT dagger. ## Enhancements 1. Introducing generative functions to the Python SDK. Generative functions are `@qfunc`s that support Python control flow, integration with third-party libraries, and debugging. 2. Execution using IBM devices is available again. 3. New method `estimation_cost` in `ExecutionSession` evaluates a quantum circuit given a classical cost function. 4. A new `+=` operator performs of quantum numerics.
Example: `z += x ** 2 - 0.5 * y` 5\. The state of `SampledState` supports dot-notation for field access when representing a quantum struct: [comment]: DO_NOT_TEST ```python theme={null} struct_sample = sample.state["my_qstruct"] field_sample = struct_sample.my_field ``` 6. Add a new example for hybrid classical-quantum neural network. See [notebook](/explore/algorithms/QML/hybrid_qnn/hybrid_qnn_for_subset_majority#example-hybrid-neural-network-for-the-subset-majority-function). ## Interface Changes 1. Parameter `control` of built-in functions such as `CX` has been renamed to `ctrl`. Parameter `control` will no longer be supported starting on 4/11/24 at the earliest. 2. Add two new functions for encoding classical data, `encode_in_angle` and `encode_on_bloch`. See [notebook](/qmod-reference/library-reference/open-library-functions/variational_data_encoding/variational_data_encoding/). ## Bug Fixes 1. Fix classical array slicing in the SDK (`my_list[1:3][0]`). 2. Fix synthesis of arithmetic operations nested in a within-apply statement when `machine_precision` is set. 3. Fix in-place arithmetic operations (`^=`/`+=`) when the value on the right-hand side is a signed variable that is not aligned with the target variable.
## Enhancements 1. Optimize synthesis of variable and constant assignments (`x ^= y`, `x += 3`). 2. The behavior of synthesis with [`debug_mode`](/user-guide/synthesis/preferences?h=debug_mo#toggling-quantum-program-debug-information) set to `False` has been changed, such that synthesis process is up to 50% faster. However, the resulting visualized quantum program may lose much of its hierarchical structure. Note that the default value for debug\_mode is still `True`, and this mode's behavior remains unchanged. 3. The maximum number of shots in a single execution on Nvidia simulators has been increased to 1,000,000. ## Bug Fixes 1. Improve circuit width estimation when `machine_precision` is set. 2. Removing the non-gate-based devices from the available AWS Bracket devices. 3. Fix `n ^= 1` assignments where `n` has a single qubit (used to raise an exception). ## Notice * With the release of this version (`0.52.0`), execution with older SDK versions may result in errors or unexpected behavior. To ensure proper execution of your quantum programs via the SDK, please upgrade to the latest version (See instructions guide above). * The `Pydantic` package dependency has been upgraded from version 1 to version 2. If you are using an older version of `Pydantic` in the same environment as our SDK, this may lead to compatibility issues. Note that installing or upgrading the SDK will also update your `Pydantic` version to V2. It is recommended to verify compatibility across your environment. ## Bug Fixes 1. Allow the sign qubit of a quantum numeric variable to overlap the fraction digits (e.g., `qnum[1, SIGNED, 1]`). ## Notice * With the release of version `0.52` (scheduled for the week of 06-12.10.2024), execution with older SDK versions might result in errors or unexpected behavior. In order to make sure executions of your quantum programs via the SDK work properly upgrade your SDK to the latest version (See instructions guide above). ## Enhancements 1. Improve qubit reuse in arithmetic operations when `machine_precision` is set. 2. Improve error messages when executing circuits on Amazon Braket. 3. Improve error messages when executing the VQE primitive. 4. Support constant assignments: `x |= 3`, `x ^= 3`, and `x += 3`. ## Bug Fixes 1. Fix in-place XOR assignments (`^=`) of 1-qubit expressions into multi-qubit variables (used to raise an error). ## Interface Changes 1. Add a new function, `quantum_program_from_qasm`, to convert a QASM string into a Quantum Program. ## Notice * With the release of version `0.52` (scheduled for the week of 06-12.10.2024), execution with older SDK versions might result in errors or unexpected behavior. In order to make sure executions of your quantum programs via the SDK work properly upgrade your SDK to the latest version (See instructions guide above). ## Notice * With the release of version `0.52` (scheduled for the week of 06-12.10.2024), execution with older SDK versions might result in errors or unexpected behavior. In order to make sure executions of your quantum programs via the SDK work properly upgrade your SDK to the latest version (See instructions guide above). ## Bug Fixes 1. Raise indicative error when circuit cannot be visualized. 2. Fixed synthesis of arithmetic operations nested in a within-apply statement when `machine_precision` is set. ## Enhancements 1. Improved error messages. 2. Added `SIGNED` and `UNSIGNED` built-in constants to improve readability of QNum types. SDK: `QNum[4, SIGNED, 1]`. Native: `qnum<4, SIGNED, 1>`. 3. `QNum` types can specify just the size property. SDK: `QNum[4]` and `QNum("n", 4)`. Native: `qnum<4>`. Such types are unsigned (`is_signed=False`) integers (`fraction_digits=0`) by default. 4. Execution on remote providers is no longer subject to any time limit when using `ExecutionSession` or executing models without classical execution code. Note: simulation on Classiq backends is still subject to time limit. 5. In-place add operations (`inplace_add`) now support signed variables. 6. Added a new notebook for solving the Differential equation using the HHL Algorithm, to simulate war games. ## Bug Fixes 1. Fixed an operand-related bug. Might occur when calling a function recursively in one of its operands (for example: `foo(lambda: foo(...))`). 2. Fixed an expression-related bug. Might occur when using the same variable in multiple expressions. 3. Fixed in-place XOR operations (`^=` / `inplace_xor`) in the presence of signed variables. The sign variable is now interpreted as part of the significand without special treatment. 4. Fixed synthesis of arithmetic operations nested in a within-apply statement. ## Interface Changes 1. SDK: Deprecated parameter names in built-in operations were removed. * `control(ctrl=..., operand=...)` => `control(ctrl=..., stmt_block=...)` * `within_apply(compute=..., action=...)` => `within_apply(within=..., apply=...)` * `power(power=..., operand=...)` => `power(exponent=..., stmt_block=...)` * `invert(operand=)` => `invert(stmt_block=...)` ## Enhancements 1. Cancelling an execution job will now result in cancellation of any ongoing jobs sent to the provider during the execution. For more information, see [Cancellation](/user-guide/execution/index#cancellation). 2. The Classiq SDK now supports Python 3.12. 3. Added a new tutorial on the Oblivious Amplitude Amplification algorithm. ## Enhancements 1. Improve synthesis performance in unconstrained large models by lowering circuit width via qubit recycling. ## Deprecations 1. The Classiq SDK will drop support for Python 3.8 near its end-of-life on October 2024. ## Bug Fixes 1. Fixed nested quantum struct bug. ## Enhancements 1. Add `size` attribute to quantum variables: [comment]: DO_NOT_TEST ```python theme={null} @qfunc def main(my_struct: Output[MyStruct]) -> None: allocate(my_struct.size, my_struct) ``` ``` qfunc main(output my_struct: MyStruct) { allocate(my_struct.size, my_struct); } ``` ## Bug Fixes 1. Fixed a bug related to boolean arithmetic expressions with invert. 2. Fixed a bug related to arithmetic expressions inside within-apply. ## Classiq IDE 1. Add back improved circuit nodes search. 2. Fix bug in Quantum Program page .qprog file extensions uploads. 3. Poll all active jobs in the job list, not just the selected job. 4. Add support for Alice & Bob hardware configurations in the IDE. ## Enhancements 1. Add support for arithmetic boolean expressions as conditionals for control statements; see [here](/qmod-reference/language-reference/statements/control/). 2. Add quantum structs. 3. The element type of quantum arrays can be any quantum type. N-dimensional quantum arrays are supported. 4. Operand parameter names are optional in both Native (`qfunc (indicator: qbit)` -> `qfunc (qbit)`) and Python (`QCallable[QBit]` -> `QCallable[Annotated[QBit, "indicator"]]`) Qmod. 5. Improve error messages. 6. Provide better circuits for certain boolean arithmetic expressions. 7. Improved qubit reuse and runtime performance for model without constraints. 8. Add `solovay_kitaev_max_iterations` field to the synthesis preferences, allowing for tuning the accuracy of the Solovay-Kitaev algorithm. 9. Built-in classical functions to decompose/compose a matrix into/from a Hamiltonian. Example of usage: [comment]: DO_NOT_TEST ```python theme={null} mat = np.array([[0, 1, 2, 3], [1, 4, 5, 6], [2, 5, 7, 8], [3, 6, 8, 9]]) hamiltonian = matrix_to_hamiltonian(mat) mat = hamiltonian_to_matrix(hamiltonian) ``` 10. parsed\_states, parsed\_counts, parsed\_state\_vector will contain the parsed execution details as lists if a quantum array was used in the model. [comment]: DO_NOT_TEST ```python theme={null} @qfunc def main(qna: Output[QArray[QNum[3, True, 0]]]) -> None: allocate(6, qna) hadamard_transform(qna) qp = synthesize(create_model(main)) res = execute(qp).result() print(res[0].value.parsed_counts[0]) ``` previously this would print -> state=\{\{'qna': 43}} shots=27 now it prints -> state=\{\{'qna': \[-2, 3]}} shots=27 11. Added a new tutorial on Hamiltonian simulation for block-encoded Hamiltonians, using QSVT and Qubitization. 12. Added a new tutorial on solving the discrete Poisson's equation using the HHL algorithm, combined with quantum sine and cosine transforms; see [here](/explore/algorithms/quantum_differential_equations_solvers/discrete_poisson_solver/discrete_poisson_solver/). 13. Enhanced the Discrete logarithm example to the case where the order is not a power of 2; see [here](/explore/algorithms/number_theory_and_cryptography/discrete_log/discrete_log/). 14. Updated the [Quantum Types](/qmod-reference/language-reference/quantum-types/) documentation page. ## Interface Changes 1. Some builtin operations parameters have been renamed: * `control(ctrl=..., operand=...)` => `control(ctrl=..., stmt_block=...)` * `within_apply(compute=..., action=...)` => `within_apply(within=..., apply=...)` * `power(power=..., operand=...)` => `power(exponent=..., stmt_block=...)` * `invert(operand=)` => `invert(stmt_block=...)` 2. Added new state preparation functions: `prepare_unifrom_trimmed_state` and `prepare_unifrom_interval_state`; see [here](/qmod-reference/library-reference/open-library-functions/special_state_preparations/prepare_partial_uniform_state/). ## Deprecations 1. SDK: The `@struct` decorator has been removed. Define classical structs using `@dataclass`. 2. The field `variance` in `EstimationResult` is deprecated, and will be populated with the value `-1` until removed. 3. The field `timeout_sec` in `ExecutionPreferences`, and the field `job_timeout` in `AwsBackendPreferences`, have been removed. 4. classical arguments in the native language are now inside the parentheses section (`(...)`) alongside quantum variables and not inside the angle brackets (`<...>`), the angle brackets section is now considered deprecated. For example, you should migrate: `qfunc main{...}` -> `qfunc main(a: real) {...}` for info refer to the language reference about functions. A Warning was added to the IDE in the case it is used. ## Enhancements * Error mitigation can now be enabled or disabled for IonQ through Azure in the SDK. Default is set to False. ## Classiq IDE 1. Fix infinite loop bug in Quantum Program page. 2. Fix Quantum Program deletion bug on Quantum Program page. ## Bug Fixes 1. Fixed a bug related to nested control operations. ## Classiq IDE 1. Fix bug in .qprog files upload on Quantum Program page. 2. New Quantum Program export option: Quantum programs can now be exported as .qprog files. ## Enhancements 1. Support signed quantum numerics in in-place-xor assignments (`^=`). 2. Support quantum array subscripts in quantum expressions. 3. Support quantum numeric arrays. 4. Improve synthesis performance. 5. Apply an automatic qubit reuse pass when the model is unconstrained. 6. Add user-defined enums. 7. Added a new technology demonstration notebook for a discrete quantum walk on a circle; see [here](/explore/tutorials/technology_demonstrations/discrete_quantum_walk_circle/discrete_quantum_walk_circle/). 8. Added new rainbow options pricing notebooks in the public repository research folder. [comment]: DO_NOT_TEST ```python theme={null} @qfunc def main(res: Output[QBit]) -> None: allocate(1, res) qnv: QArray = QArray("qnv", element_type=QNum[2, False, 0]) # quantum numeric array allocate(6, qnv) repeat(qnv.len, lambda i: inplace_prepare_int(i + 1, qnv[i])) res ^= qnv[0] + qnv[1] == qnv[2] # array subscripts in expressions ``` ``` qfunc main(output res: qbit) { allocate<1>(res); qnv: qnum<2, False, 0>[]; // quantum numeric array allocate<6>(qnv); repeat (i: qnv.len) { inplace_prepare_int(qnv[i]); } res ^= qnv[0] + qnv[1] == qnv[2]; // array subscripts in expressions } ``` ## Interface Changes 1. The classical scope in Qmod no longer supports function definitions. 2. The `aer_simulator`, `aer_simulator_statevector`, `aer_simulator_density_matrix`, and `aer_simulator_matrix_product_state` Classiq backends are no longer accessible in the SDK. Use `simulator`, `simulator_statevector`, `simulator_density_matrix`, and `simulator_matrix_product_state` instead. 3. The `@struct` decorator is deprecated and will be removed in a future release. Use `@dataclass` instead. ## Bug Fixes 1. Fixed a bug where multiple in-place assignment statements resulted in a `22102` error. 2. Fixed a bug where using the same variable in `control` operation for both the control operation and the body resulted in a non-indicative error. 3. Fix `invert` and `within-apply` variable initialization tracking in native Qmod. 4. Fix division with classical symbolic variables. 5. Fix rogue comma inserted to the chemistry model classical execution code after IDE form update. 6. Fix reporting uninitialized quantum variables in arithmetic expressions as undefined. 7. Fix double execution on devices requiring access tokens. 8. Fix execution configuration being applied only to the first selected device, when one of the selected devices requires an access token. 9. Fix a bug where Grover circuit was incorrect when reflecting about states different than uniform superposition. 10. Fix a synthesis bug that could appear in models that constrain the width and optimize depth or vice versa. ## Classiq IDE 1. Graphical Model tab redesign of nodes (Function call, Output, Assignment). 2. Restructure of node categories (Graphical Model). 3. Fix countries list not loading sometimes during registration. 4. Prevent Qmod editor from crashing when compiler crashes. 5. Redesigned the Accordion and Icon status for jobs. 6. Quantum Program tabs moved to the left drawer. 7. Uploading Quantum Program can now be done using the Upload button on the left drawer. 8. 3 newly introduced tabs of Quantum Program data: Transpiled Info, Program Info, Data. ## Enhancements * Add direct links of all examples from the documentation to Classiq's library and IDE. ## Bug Fixes * fixed broken links. ## Enhancements 1. HHL workshop was added to the public repository and to the User Guide. 2. It is now possible to use the execution primitives `estimate` and `vqe` with models with multiple output ports. The hamiltonian should match all the output ports by their order. 3. Amplitude encoding assignments (`ind *= foo(n)`) support all quantum numeric variables, not only unsigned fractions. 4. Add support for lookup tables in amplitude encoding assignments, e.g., `ind *= [0, 0.5, 0.5, 1][n]` where `n` is an unsigned quantum integer (`ind *= subscript([0, 0.5, 0.5, 1], n)` in the SDK). 5. A quantum COSINE and SINE transforms of types I and II were added to the open library, see [Quantum COSINE and SINE Transforms](/qmod-reference/library-reference/open-library-functions/qct_qst/qct_qst). 6. New algorithm was added to the library: "Variational Quantum Linear Solver (VQLS) with Linear Combination of Unitaries (LCU) Block Encoding" 7. `Alice & Bob` provider is now accessible. ## Interface Changes 1. `compute_qaoa_initial_point` is now exposed in the SDK directly from `classiq` package. `from classiq import compute_qaoa_initial_point` 2. The examples in IDE Flow page are replaced with a new arithmetics example. ## Bug Fixes 1. Fixed an error related to the translation of certain circuits between synthesis and execution. 2. Fixed an error when calling a function with local variables multiple times. 3. Properly evaluate type attributes in `main`. * Native Qmod: `qnum`'s sign field can be specified with lowercase `true`/`false` in `main`'s signature. 4. Fix execution results received from running on Amazon Braket to be correct when the measured outputs are not all of the variables of the model. 5. Improve language error reporting. ## Classiq IDE 1. Uniform drawer width for all pages 2. New way to trigger node options context menu on Graphical Model page: node options menu can now be triggered by right-clicking a selected node ## Enhancements * Performance of synthesis has been improved, most significantly for synthesis requests with no constraints. ## Enhancements * Add `execution request` and `execution results` to reference manual in the documentation ## Bug Fixes * Fix broken links in documentation ## Enhancements 1. [Hardware-aware synthesis](/user-guide/synthesis/hardware-aware-synthesis) will now use the Solovay-Kitaev algorithm to approximate single-qubit gates when the basis gate set is a specific variation of Clifford + T (`X`, `Z`, `H`, `T`, `CX`, and `CCX`). 2. `qsvt` function was added to the function library. See [Quantum Singular Value Transformation](/qmod-reference/library-reference/open-library-functions/qsvt/qsvt). 3. A tutorial on discrete quantum walks was added to the tutorials library. See [Discrete Quantum Walk](/explore/tutorials/advanced_tutorials/discrete_quantum_walk/discrete_quantum_walk.ipynb). 4. SDK: Quantum functions (`@qfunc`) can be recursive. 5. SDK: PauliTerm can be used to declare a hamiltonian. [comment]: DO_NOT_TEST ```python theme={null} hamiltonian = [ PauliTerm(pauli=[Pauli.I], coefficient=1), PauliTerm(pauli=[Pauli.Z, Pauli.X], coefficient=2), ] ``` 6. Introducing **ExecutionSession** which will allow choosing the execution primitive in the SDK without the need of changing/synthesizing the quantum program once again. [comment]: DO_NOT_TEST ```python theme={null} model = create_model(main) qprog = synthesize(model) preferences = ExecutionPreferences(num_shots=1200) execution_session = ExecutionSession(qprog, preferences) # if the quantum program does not need any execution paramters: execution_session.sample() # if the quantum program needs execution parameters: execution_session.sample({"phi": 1}) # if multiple samples are needed: execution_session.batch_sample([{"phi": 1}, {"phi": 2}, {"phi": 3}]) # if an estimation is needed without execution parameters: hamiltonian = [ PauliTerm(pauli=[Pauli.I], coefficient=1), PauliTerm(pauli=[Pauli.Z], coefficient=2), ] execution_parameters.estimate(hamiltonian) # if an estimation is needed with execution paramters: execution_parameters.estimate(hamiltonian, {"theta": 1}) # if multiple estimations are needed: execution_parameters.batch_estimate(hamiltonian, [{"theta": 1}, {"theta": 2}]) ``` 7. A Qmod library reference, with usage examples for built-in and open library functions, can now be found in [the function menu](/explore/functions/index). ## Interface Changes 1. SDK: In `execute_qnn`, the optional argument `observables` of type `PauliOperators` has been replaced with the optional argument `observable` of type `PauliOperator`. ## Deprecations 1. SDK: The `quantum_if` operation and the old `control` syntax have been removed. * `quantum_if` is removed. Use `control` instead. * `control(operand, ctrl)` is no longer supported. Use `control(ctrl, operand)` instead. * `control(n, operand)` does not support quantum numeric variables (`n`). Instead, use a `bind` operation to cast `n` into a quantum array, or compare `n` to an integer explicitly (e.g., `n == 7`). 2. SDK: The `QParam` type has been removed. * Instead of `QParam[int]`, use `CInt`. * Instead of `QParam[float]`, use `CReal`. * Instead of `QParam[bool]`, use `CBool`. * Instead of `QParam[List[...]]`, use `CArray[...]`. * Instead of `QParam[Array[..., size]]`, use `CArray[..., size]`. 3. Native Qmod: Accessing quantum variable properties (e.g., `qbv.len` or `n.is_signed`) via function call syntax (`len(qbv)`, `is_signed(qbv)`) is no longer supported. 4. The field `optimizer_preferences` of `ExecutionPreferences` has been removed. 5. The function `set_initial_values` has been removed. ## Classiq IDE 1. "Slack" and "Learn More" links were moved from the side drawer to the IDE header. New links were added as well: Community, User Guide. 2. Allow visualization of larger circuits and prolong the timeout for visualization 3. Users can now download a LaTeX format of their quantum programs directly from the IDE, allowing for easy sharing, publication, and presentation of their work. plot 4. Cookies settings are now available to adjust the given consent at any time. ## Bug Fixes 1. "selected example not found" error when opening the IDE 2. Resetting now clears preferences form instead of initializing from cache on the model page 3. Naming for exported files on the Graphical Editor 4. State vector measurement results ordering 5. Parameter names in lambda expressions don't have to match the parameter names in operand declarations: [comment]: DO_NOT_TEST ```python theme={null} @qfunc def my_H(qb: QBit) -> None: H(qb) ... apply_to_all(my_H, qba) # used to throw an error since "qb" != "target" ``` ``` qfunc my_H(qb: qbit) { H(qb); } ... apply_to_all(qba); // used to throw an error since "qb" != "target" ``` ## Interface Changes 1. The `control` and `quantum_if` statements have been unified into one statement in native Qmod and the SDK under the name `control`. * SDK: The unified `control` statement accepts a qbit (`q`), a q-array (`qbv`), or a comparison of a qnum and an integer (`n == 5`) as the first argument. * SDK: The old `control` syntax (with `ctrl` as the second argument) is deprecated and will be removed in the next release. * SDK: The `quantum_if` operator is deprecated and will be removed in the next release. * Native Qmod: `quantum_if` is no longer supported, use the new `control` statement instead. 2. SDK: The `QParam` and `Array` types are deprecated and will be removed in the next release. * Instead of `QParam[int]`, use `CInt`. * Instead of `QParam[float]`, use `CReal`. * Instead of `QParam[bool]`, use `CBool`. * Instead of `QParam[List[...]]`, use `CArray[...]`. * Instead of `QParam[Array[..., size]]`, use `CArray[..., size]`. 3. SDK: Using Python types (`int`, `bool`, etc.) in struct declarations (`@struct`) is deprecated and will not be supported by the next release. Instead, use classical data types (`CInt`, `CBool`, etc., see above). 4. Default timeout for sending jobs to AWS changed from 5 minutes to 4 hours. ## Deprecations 1. The error mitigation settings in the execution preferences are no longer available. 2. The `aer_simulator` backend has been removed. Use Classiq's simulator backend instead (`ClassiqSimulatorBackendNames.SIMULATOR`). ## Enhancements 1. Improved error messages. ## Bug Fixes 1. Fixed a bug preventing execution of circuits synthesized with hardware aware synthesis on Azure and IonQ. ## Enhancements 1. Improved the native Qmod syntax of the `repeat`, `if` statements. For details, see [Classical Control Flow](/qmod-reference/language-reference/statements/classical-control-flow). 2. Improved the native Qmod syntax of the `control`, `power`, and `invert` statements. For details, see [Quantum Operators](/qmod-reference/language-reference/operators). 3. Added the `classiq.execution.all_hardware_devices.get_all_hardware_devices` function to the Python SDK. ## Deprecations 1. In the SDK, the `classiq.analyzer.analyzer.Analyzer.get_available_devices` method is deprecated. Use `classiq.execution.all_hardware_devices.get_all_hardware_devices` instead. ## Interface Changes 1. The `len` method for `QArray`, `QCallableList`, and `QParam` of lists, is now a property. use `.len` instead of `.len()`. ## Overview Release 0.38 achieves a major milestone in the migration to the new Qmod language. The following general changes are now in effect: 1. In the IDE, Synthesis page is now removed. Writing and synthesizing models is now done exclusively in the Model page. 2. All pre-defined models previously available in the Synthesis page in Json format are now available in the Model page in native Qmod syntax. 3. Many Qmod language enhancements were introduced to enable the coding of all available models and applications, in both native Qmod and in its Python embedding (see detailed list below). 4. A new Qmod reference manual, covering all the language concepts and constructs in both input formats, is available [Qmod-language-reference](/qmod-reference/language-reference/index). 5. Documentation content covering old input formats - the Json input and the old SDK classes - has been removed ## Enhancements 1. Native Qmod now supports classical POD struct declaration, initialization, and member access. 2. Native Qmod now supports within-apply statements. 3. Native Qmod now supports generalized in-place quantum assignment. 4. Native Qmod now supports global constants. ## Interface Changes 1. The `QStruct` decorator was renamed to `struct` in the `qmod`-python integration. 2. The `qfunc` decorator is renamed to `quantum_function`. The newer `QFunc` and `ExternalQFunc` decorator are now available as `qfunc` and `qfunc(external=True)`. Using either classes as a decorator is deprecated, and this feature may be removed in a future release. 3. Similarly, a new `cfunc` decorator is now available, and using the class `CFunc` as a decorator is deprecated and may be removed in the future. 4. Renamed `GeneratedCircuit` to `QuantumProgram` in the SDK. 5. The `QNum` type does not accept size argument without sign and fraction digits. 6. `QNum` on the right-hand side of `bind` statements must have sign and fraction digits (inferred from initialization or declaration). 7. The Following async methods were removed from `Analyzer` class (The documented methods without the `async` suffix are still available and can be used instead): 1. `get_available_devices_async` 2. `analyzer_app_async` 3. `plot_hardware_connectivity_async` 4. `get_hardware_comparison_table_async` 8. The method `show_multiple_hardware_data` was removed from `RBAnalysis` class. The async equivalent `show_multiple_hardware_data_async` can be used instead. 9. Removed `reinterpret_num` operation (instead use `QNum`/`allocate_num` arguments). 10. Removed `split` and `join` operation (instead use `bind` operation). 11. Added method `parsed_counts_of_outputs` to sample's result in the Python SDK; see [sample](/user-guide/execution/sample). 12. Constants can now be used in the `qmod`-python integration with the `QConstant` class. 13. The quantum numeric type can now receive sign and fraction digits as type arguments in parameter declarations, in both the SDK and native Qmod. * Native Qmod example: `qnum<5, True, 2>` to indicate a 5-bit signed number with 2 fraction digits. * SDK examples: * `QNum[5, True, 2]` to indicate a 5-bit signed number with 2 fraction digits (as a function argument) * `QNum("x", 5, True, 2)` to indicate a 5-bit signed number with 2 fraction digits (as a local variable) 14. The amplitude encoding flavor of quantum arithmetic (`*=` operator) is now an in-place operation in both the SDK and native Qmod. # Iqae Source: https://docs.classiq.io/sdk-reference/applications/IQAE Members: | Name | Description | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `GenericIQAE` | The implementation is based on Algorithm 1 & Algorithm 2 in \[1], with the intent of demistifying variables names and simplifying the code flow. | | `IQAEIterationData` | Handles the data storage for a single iteration of the Iterative Quantum Amplitude Estimation algorithm. | | `IQAEResult` | Represents the result of an Iterative Quantum Amplitude Estimation (IQAE) process. | | `ExecutionPreferences` | Represents the execution settings for running a quantum program. | | `Constraints` | Constraints for the quantum circuit synthesis engine. | | `Preferences` | Preferences for synthesizing a quantum circuit. | | `ExecutionSession` | A session for executing a quantum program or OpenQASM source text. | | `QBit` | A type representing a single qubit. | | `Z` | \[Qmod core-library function]. | | `allocate` | Initialize a quantum variable to a new quantum object in the zero state:. | | `bind` | Reassign qubit or arrays of qubits by redirecting their logical identifiers. | | `within_apply` | Given two operations $U$ and $V$, performs the composition of operations $U^\{-1\} V U$. | | `drop` | \[Qmod core-library function]. | | `create_model` | Create a serialized model from a given Qmod entry function and additional parameters. | | `synthesize` | Synthesize a model with the Classiq engine to receive a quantum program. | | `IQAE` | Implementation of Iterative Quantum Amplitude Estimation \[1]. | ## GenericIQAE The implementation is based on Algorithm 1 & Algorithm 2 in \[1], with the intent of demistifying variables names and simplifying the code flow. Moreover, we separated the algorithm flow from quantum execution to allow migrating this code to any execution interface and to improve its testability. **Methods:** | Name | Description | | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | [run](#run) | Execute the estimation algorithm. | | [current\_estimation\_confidence\_interval](#current_estimation_confidence_interval) | | | [current\_estimation](#current_estimation) | | | [find\_next\_K](#find_next_K) | We want to find the largest K (with some lower and upper bounds) such that the K-scaled confidence interval lies completely in the upper or lower h... | **Attributes:** | Name | Type | Description | | ------------ | --------------------- | ----------- | | `iterations` | `list[IterationInfo]` | | ### run
run(
self:
) -> float
Execute the estimation algorithm. See Algorithm 1, \[1]. **Parameters:** | Name | Type | Description | Default | | ------ | ---- | ----------- | ---------- | | `self` | \`\` | | *required* | ### current\_estimation\_confidence\_interval
current\_estimation\_confidence\_interval(
self:
) -> np.ndarray
**Parameters:** | Name | Type | Description | Default | | ------ | ---- | ----------- | ---------- | | `self` | \`\` | | *required* | ### current\_estimation
current\_estimation(
self:
) -> float
**Parameters:** | Name | Type | Description | Default | | ------ | ---- | ----------- | ---------- | | `self` | \`\` | | *required* | ### find\_next\_K
find\_next\_K(
K: int,
is\_upper\_plane: bool,
confidence\_interval: np.ndarray,
r: int = 2
) -> tuple\[int, bool]
We want to find the largest K (with some lower and upper bounds) such that the K-scaled confidence interval lies completely in the upper or lower half planes. See Algorithm 2, \[1]. **Parameters:** | Name | Type | Description | Default | | --------------------- | ------------ | ----------- | ---------- | | `K` | `int` | | *required* | | `is_upper_plane` | `bool` | | *required* | | `confidence_interval` | `np.ndarray` | | *required* | | `r` | `int` | | 2 | ## IQAEIterationData Handles the data storage for a single iteration of the Iterative Quantum Amplitude Estimation algorithm. This class is intended to represent the results and state of a single Grover iteration of the IQAE process. **Attributes:** | Name | Type | Description | | ------------------- | ------------------ | ----------------------------------------------------------------- | | `grover_iterations` | `int` | The iteration number of Grover's algorithm. | | `sample_results` | `ExecutionDetails` | The `ExecutionDetails` of Grover iteration. See ExecutionDetails. | ## IQAEResult Represents the result of an Iterative Quantum Amplitude Estimation (IQAE) process. This class encapsulates the output of the IQAE algorithm, including the estimated value, confidence interval, intermediate iteration data, and any warnings generated during the computation. **Attributes:** | Name | Type | Description | | --------------------- | ------------------------- | ----------------------------------------------------------------------------------- | | `estimation` | `float` | Estimation of the amplitude. | | `confidence_interval` | `list[float]` | The interval in which the amplitude is within, with a probability equal to epsilon. | | `iterations_data` | `list[IQAEIterationData]` | List of `IQAEIterationData` of each Grover iteration. | | `warnings` | `list[str]` | List of warnings generated during the IQAE process of each Grover iteration. | ## ExecutionPreferences Represents the execution settings for running a quantum program. Execution preferences for running a quantum program. For more details, refer to: ExecutionPreferences example: [ExecutionPreferences](https://docs.classiq.io/latest/user-guide/execution/#execution-preferences).. **Attributes:** | Name | Type | Description | | -------------------------------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | `include_zero_amplitude_outputs` | `bool` | | | `amplitude_threshold` | `float` | | | `noise_properties` | `Optional[NoiseProperties]` | Properties defining the noise in the quantum circuit. Defaults to `None`. | | `random_seed` | `int` | The random seed used for the execution. Defaults to a randomly generated seed. | | `backend_preferences` | `BackendPreferencesTypes` | Preferences for the backend used to execute the circuit. Defaults to the Classiq Simulator. | | `num_shots` | `Optional[pydantic.PositiveInt]` | The number of shots (executions) to be performed. | | `transpile_to_hardware` | `TranspilationOption` | Option to transpile the circuit to the hardware's basis gates before execution. Defaults to `TranspilationOption.DECOMPOSE`. | | `job_name` | `Optional[str]` | The name of the job, with a minimum length of 1 character. | ## Constraints Constraints for the quantum circuit synthesis engine. This class is used to specify constraints such as maximum width, depth, gate count, and optimization parameters for the synthesis engine, guiding the generation of quantum circuits that satisfy these constraints. **Attributes:** | Name | Type | Description | | ------------------------ | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `max_width` | `int` | Maximum number of qubits allowed in the generated quantum circuit. Defaults to `None`. | | `optimization_parameter` | `OptimizationParameterType` | Determines if and how the synthesis engine should optimize the solution. Defaults to `NO_OPTIMIZATION`. See `OptimizationParameterType` | ## Preferences Preferences for synthesizing a quantum circuit. **Methods:** | Name | Description | | ------------------------------------------------------------------------------------------------------------ | ----------- | | [optimization\_timeout\_less\_than\_generation\_timeout](#optimization_timeout_less_than_generation_timeout) | | | [make\_output\_format\_list](#make_output_format_list) | | | [validate\_output\_format](#validate_output_format) | | | [validate\_backend\_name](#validate_backend_name) | | | [validate\_backend](#validate_backend) | | **Attributes:** | Name | Type | Description | | ------------------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `synthesize_all_separately` | `bool` | | | `symbolic_loops` | `bool` | | | `compatibility_mode` | `bool` | | | `standalone` | `bool` | | | `backend_preferences` | `BackendPreferences \| None` | | | `machine_precision` | `int` | Specifies the precision used for quantum operations. Defaults to `DEFAULT_MACHINE_PRECISION`. | | `backend_service_provider` | `str` | The provider company or cloud service for the requested backend. Defaults to `None`. | | `backend_name` | `str` | The name of the requested backend or target. Defaults to `None`. | | `custom_hardware_settings` | `CustomHardwareSettings` | Defines custom hardware settings for optimization. This field is ignored if backend preferences are specified. | | `debug_mode` | `bool` | If `True`, debug information is added to the synthesized result, potentially slowing down the synthesis. Useful for executing interactive algorithms. Defaults to `True`. | | `optimization_level` | `OptimizationLevel)` | The optimization level used during synthesis (0-3); | | `output_format` | `List[QuantumFormat]` | Lists the output format(s) for the quantum circuit. Defaults to `[QuantumFormat.QASM]`. `QuantumFormat` Options: - QASM = "qasm" - QSHARP = "qsharp" - QIR = "qir" - IONQ = "ionq" - CIRQ\_JSON = "cirq\_json" - QASM\_CIRQ\_COMPATIBLE = "qasm\_cirq\_compatible" | | `qasm3` | `Optional[bool]` | If `True`, outputs OpenQASM 3.0 in addition to 2.0, applicable to relevant attributes in `GeneratedCircuit`. Defaults to `None`. | | `transpilation_option` | `TranspilationOption` | Sets the transpilation option to optimize the circuit. Defaults to `AUTO_OPTIMIZE`. See `TranspilationOption` | | `solovay_kitaev_max_iterations` | `Optional[int]` | Specifies the maximum number of iterations for the Solovay-Kitaev algorithm, if used. Defaults to `None`. | | `timeout_seconds` | `int` | Timeout setting for circuit synthesis in seconds. Defaults to `300`. | | `optimization_timeout_seconds` | `Optional[int]` | Specifies the timeout for optimization in seconds, or `None` for no optimization timeout. This will still adhere to the overall synthesis timeout. Defaults to `None`. | | `random_seed` | `int` | Random seed for circuit synthesis. | ### optimization\_timeout\_less\_than\_generation\_timeout
optimization\_timeout\_less\_than\_generation\_timeout(
cls: ,
optimization\_timeout\_seconds: pydantic.PositiveInt | None,
info: ValidationInfo
) -> pydantic.PositiveInt | None
**Parameters:** | Name | Type | Description | Default | | ------------------------------ | ------------------------------ | ----------- | ---------- | | `cls` | \`\` | | *required* | | `optimization_timeout_seconds` | `pydantic.PositiveInt \| None` | | *required* | | `info` | `ValidationInfo` | | *required* | ### make\_output\_format\_list
make\_output\_format\_list(
cls: ,
output\_format: Any
) -> list
**Parameters:** | Name | Type | Description | Default | | --------------- | ----- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `output_format` | `Any` | | *required* | ### validate\_output\_format
validate\_output\_format(
cls: ,
output\_format: PydanticConstrainedQuantumFormatList,
info: ValidationInfo
) -> PydanticConstrainedQuantumFormatList
**Parameters:** | Name | Type | Description | Default | | --------------- | -------------------------------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `output_format` | `PydanticConstrainedQuantumFormatList` | | *required* | | `info` | `ValidationInfo` | | *required* | ### validate\_backend\_name
validate\_backend\_name(
cls: ,
backend\_name: str | None
) -> str | None
**Parameters:** | Name | Type | Description | Default | | -------------- | ------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `backend_name` | `str \| None` | | *required* | ### validate\_backend
validate\_backend(
self:
) -> Self
**Parameters:** | Name | Type | Description | Default | | ------ | ---- | ----------- | ---------- | | `self` | \`\` | | *required* | ## ExecutionSession A session for executing a quantum program or OpenQASM source text. `ExecutionSession` allows to execute the quantum program with different parameters and operations without the need to re-synthesize the model. The session must be closed in order to ensure resources are properly cleaned up. It's recommended to use `ExecutionSession` as a context manager for this purpose. Alternatively, you can directly use the `close` method. **Methods:** | Name | Description | | ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | [close](#close) | Close the session and clean up its resources. | | [get\_session\_id](#get_session_id) | | | [update\_execution\_preferences](#update_execution_preferences) | Update the execution preferences for the session. | | [sample](#sample) | Samples the quantum program with the given parameters, if any. | | [submit\_sample](#submit_sample) | Initiates an execution job with the `sample` primitive. | | [calculate\_state\_vector](#calculate_state_vector) | Calculate the state vector of the quantum program. | | [submit\_calculate\_state\_vector](#submit_calculate_state_vector) | Initiates an execution job with the `calculate_state_vector` primitive. | | [calculate\_unitary](#calculate_unitary) | Calculate the unitary matrix of the quantum program. | | [submit\_calculate\_unitary](#submit_calculate_unitary) | Initiates an execution job with the `calculate_unitary` primitive. | | [batch\_sample](#batch_sample) | Samples the quantum program multiple times with the given parameters for each iteration. | | [submit\_batch\_sample](#submit_batch_sample) | Initiates an execution job with the `batch_sample` primitive. | | [observe](#observe) | Estimates the expectation value of the given Hamiltonian using the quantum program. | | [submit\_observe](#submit_observe) | Initiates an execution job with the `observe` primitive. | | [estimate](#estimate) | Estimates the expectation value of the given Hamiltonian using the quantum program. | | [submit\_estimate](#submit_estimate) | Initiates an execution job with the `estimate` primitive. | | [batch\_estimate](#batch_estimate) | Estimates the expectation value of the given Hamiltonian multiple times using the quantum program, with the given parameters for each iteration. | | [submit\_batch\_estimate](#submit_batch_estimate) | Initiates an execution job with the `batch_estimate` primitive. | | [variational\_minimize](#variational_minimize) | Variationally minimizes the given cost function using the quantum program. | | [minimize](#minimize) | . | | [submit\_variational\_minimize](#submit_variational_minimize) | Initiates an execution job with the variational minimization primitive. | | [submit\_minimize](#submit_minimize) | . | | [estimate\_cost](#estimate_cost) | Estimates circuit cost using a classical cost function. | | [set\_measured\_state\_filter](#set_measured_state_filter) | When simulating on a statevector simulator, emulate the behavior of postprocessing by discarding amplitudes for which their states are "undesirable". | **Attributes:** | Name | Type | Description | | --------- | ---------------- | -------------------------------------------------------------------------------------------------------------- | | `program` | `QuantumProgram` | The quantum program to execute, or a placeholder when the first constructor argument was OpenQASM source text. | ### close
close(
self:
) -> None
Close the session and clean up its resources. **Parameters:** | Name | Type | Description | Default | | ------ | ---- | ----------- | ---------- | | `self` | \`\` | | *required* | ### get\_session\_id
get\_session\_id(
self:
) -> str
**Parameters:** | Name | Type | Description | Default | | ------ | ---- | ----------- | ---------- | | `self` | \`\` | | *required* | ### update\_execution\_preferences
update\_execution\_preferences(
self: ,
execution\_preferences: ExecutionPreferences | None
) -> None
Update the execution preferences for the session. **Parameters:** | Name | Type | Description | Default | | ----------------------- | ------------------------------ | ------------------------------------ | ---------- | | `self` | \`\` | | *required* | | `execution_preferences` | `ExecutionPreferences \| None` | The execution preferences to update. | *required* | **Returns:** * **Type:** `None` ### sample
sample(
self: ,
parameters: ExecutionParams | list\[ExecutionParams] | None = None,
num\_shots: int | None = None,
run\_via\_classiq: bool | None = None
) -> ExecutionDetails | list\[ExecutionDetails]
Samples the quantum program with the given parameters, if any. **Parameters:** | Name | Type | Description | Default | | ----------------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `self` | \`\` | | *required* | | `parameters` | `ExecutionParams \| list[ExecutionParams] \| None` | A dictionary of parameter values, or a list of dictionaries for batch execution. Each key should be the name of a parameter in the quantum program (parameters of the main function), and the value should be the value to set for that parameter. | None | | `num_shots` | `int \| None` | Number of shots for this call only; overrides the session default when set. | None | | `run_via_classiq` | `bool \| None` | Run via Classiq credentials for this call only; overrides the session default when set. | None | **Returns:** * **Type:** `ExecutionDetails \| list[ExecutionDetails]` * The result of the sampling, or a list of results when * `parameters` is a list. ### submit\_sample
submit\_sample(
self: ,
parameters: ExecutionParams | list\[ExecutionParams] | None = None,
num\_shots: int | None = None,
run\_via\_classiq: bool | None = None
) -> ExecutionJob
Initiates an execution job with the `sample` primitive. This is a non-blocking version of `sample`: it gets the same parameters and initiates the same execution job, but instead of waiting for the result, it returns the job object immediately. **Parameters:** | Name | Type | Description | Default | | ----------------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `self` | \`\` | | *required* | | `parameters` | `ExecutionParams \| list[ExecutionParams] \| None` | A dictionary of parameter values, or a list of dictionaries for batch execution. Each key should be the name of a parameter in the quantum program (parameters of the main function), and the value should be the value to set for that parameter. | None | | `num_shots` | `int \| None` | Number of shots for this call only; overrides the session default when set. | None | | `run_via_classiq` | `bool \| None` | Run via Classiq credentials for this call only; overrides the session default when set. | None | **Returns:** * **Type:** `ExecutionJob` * The execution job. ### calculate\_state\_vector
calculate\_state\_vector(
self: ,
parameters: ExecutionParams | list\[ExecutionParams] | None = None,
filters: dict\[str, Any] | None = None,
amplitude\_threshold: float = 0.0,
verbose: bool = True
) -> DataFrame | list\[DataFrame]
Calculate the state vector of the quantum program. The session must be configured with a Classiq simulator (`"classiq/simulator"`, `"classiq/nvidia_simulator"`) or `"google/cuquantum"`. The corresponding statevector variant is selected automatically; callers do not need to know about the `_statevector` backend names. **Parameters:** | Name | Type | Description | Default | | --------------------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `self` | \`\` | | *required* | | `parameters` | `ExecutionParams \| list[ExecutionParams] \| None` | A dictionary of parameter values, or a list of dictionaries for batch execution. | None | | `filters` | `dict[str, Any] \| None` | Only states where the variables match these values will be included in the state vector. | None | | `amplitude_threshold` | `float` | If provided, only states whose amplitude magnitude is strictly greater than this value will be included in the result. Defaults to 0 (filters exactly zero-amplitude states). | 0.0 | | `verbose` | `bool` | Whether to print the "Submitting state-vector job to..." progress line. Set to `False` to suppress it, for example when calling `calculate_state_vector` repeatedly in a loop. Defaults to `True`. | True | **Returns:** * **Type:** `DataFrame \| list[DataFrame]` * A dataframe containing the state vector, or a list of dataframes when * `parameters` is a list. ### submit\_calculate\_state\_vector
submit\_calculate\_state\_vector(
self: ,
parameters: ExecutionParams | list\[ExecutionParams] | None = None,
filters: dict\[str, Any] | None = None,
amplitude\_threshold: float = 0.0,
verbose: bool = True
) -> ExecutionJob
Initiates an execution job with the `calculate_state_vector` primitive. This is a non-blocking version of [calculate\_state\_vector()](#calculate_state_vector): it gets the same parameters and initiates the same execution job, but instead of waiting for the result, it returns the job object immediately. **Parameters:** | Name | Type | Description | Default | | --------------------- | -------------------------------------------------- | ---------------------------------------------------------- | ---------- | | `self` | \`\` | | *required* | | `parameters` | `ExecutionParams \| list[ExecutionParams] \| None` | See [calculate\_state\_vector()](#calculate_state_vector). | None | | `filters` | `dict[str, Any] \| None` | See [calculate\_state\_vector()](#calculate_state_vector). | None | | `amplitude_threshold` | `float` | See [calculate\_state\_vector()](#calculate_state_vector). | 0.0 | | `verbose` | `bool` | See [calculate\_state\_vector()](#calculate_state_vector). | True | **Returns:** * **Type:** `ExecutionJob` * The execution job. ### calculate\_unitary
calculate\_unitary(
self: ,
parameters: ExecutionParams | list\[ExecutionParams] | None = None,
amplitude\_threshold: float = 0.0
) -> DataFrame | list\[DataFrame]
Calculate the unitary matrix of the quantum program. The session must be configured with a Classiq simulator (`"classiq/simulator"`, `"classiq/nvidia_simulator"`, `"classiq/dgx_simulator"`) or `"google/cuquantum"`. **Parameters:** | Name | Type | Description | Default | | --------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ---------- | | `self` | \`\` | | *required* | | `parameters` | `ExecutionParams \| list[ExecutionParams] \| None` | A dictionary of parameter values, or a list of dictionaries for batch execution. | None | | `amplitude_threshold` | `float` | If provided, matrix elements whose magnitude is below this threshold are zeroed (same cutoff semantics as `calculate_state_vector`). | 0.0 | **Returns:** * **Type:** `DataFrame \| list[DataFrame]` * A dataframe containing the unitary matrix, or a list of dataframes when * `parameters` is a list. ### submit\_calculate\_unitary
submit\_calculate\_unitary(
self: ,
parameters: ExecutionParams | list\[ExecutionParams] | None = None,
amplitude\_threshold: float = 0.0
) -> ExecutionJob
Initiates an execution job with the `calculate_unitary` primitive. Promotes the session backend to a unitary simulator and runs the `sample` primitive on a dedicated unitary session. Program size limits are enforced server-side. **Parameters:** | Name | Type | Description | Default | | --------------------- | -------------------------------------------------- | ----------- | ---------- | | `self` | \`\` | | *required* | | `parameters` | `ExecutionParams \| list[ExecutionParams] \| None` | | None | | `amplitude_threshold` | `float` | | 0.0 | ### batch\_sample
batch\_sample(
self: ,
parameters: list\[ExecutionParams]
) -> list\[ExecutionDetails]
Samples the quantum program multiple times with the given parameters for each iteration. The number of samples is determined by the length of the parameters list. **Deprecated:** Pass a list of parameter dicts to [sample()](#sample) instead. **Parameters:** | Name | Type | Description | Default | | ------------ | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `self` | \`\` | | *required* | | `parameters` | `list[ExecutionParams]` | A list of the parameters for each iteration. Each item is a dictionary where each key should be the name of a parameter in the quantum program (parameters of the main function), and the value should be the value to set for that parameter. | *required* | **Returns:** * **Type:** `list[ExecutionDetails]` * List\[ExecutionDetails]: The results of all the sampling iterations. ### submit\_batch\_sample
submit\_batch\_sample(
self: ,
parameters: list\[ExecutionParams]
) -> ExecutionJob
Initiates an execution job with the `batch_sample` primitive. **Deprecated:** Pass a list of parameter dicts to [submit\_sample()](#submit_sample) instead. **Parameters:** | Name | Type | Description | Default | | ------------ | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `self` | \`\` | | *required* | | `parameters` | `list[ExecutionParams]` | A list of the parameters for each iteration. Each item is a dictionary where each key should be the name of a parameter in the quantum program (parameters of the main function), and the value should be the value to set for that parameter. | *required* | **Returns:** * **Type:** `ExecutionJob` * The execution job. ### observe
observe(
self: ,
hamiltonian: Hamiltonian,
parameters: ExecutionParams | list\[ExecutionParams] | None = None,
num\_shots: int | None = None,
run\_via\_classiq: bool | None = None
) -> EstimationResult | list\[EstimationResult]
Estimates the expectation value of the given Hamiltonian using the quantum program. **Parameters:** | Name | Type | Description | Default | | ----------------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `self` | \`\` | | *required* | | `hamiltonian` | `Hamiltonian` | The Hamiltonian to estimate the expectation value of. | *required* | | `parameters` | `ExecutionParams \| list[ExecutionParams] \| None` | A dictionary of parameter values, or a list of dictionaries for batch execution. Each key should be the name of a parameter in the quantum program (parameters of the main function), and the value should be the value to set for that parameter. | None | | `num_shots` | `int \| None` | Number of shots for this call only; overrides the session default when set. | None | | `run_via_classiq` | `bool \| None` | Run via Classiq credentials for this call only; overrides the session default when set. | None | **Returns:** * **Type:** `EstimationResult \| list[EstimationResult]` * The estimation result, or a list of results when `parameters` * is a list. ### submit\_observe
submit\_observe(
self: ,
hamiltonian: Hamiltonian,
parameters: ExecutionParams | list\[ExecutionParams] | None = None,
num\_shots: int | None = None,
run\_via\_classiq: bool | None = None
) -> ExecutionJob
Initiates an execution job with the `observe` primitive. This is a non-blocking version of [observe()](#observe): it gets the same parameters and initiates the same execution job, but instead of waiting for the result, it returns the job object immediately. **Parameters:** | Name | Type | Description | Default | | ----------------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `self` | \`\` | | *required* | | `hamiltonian` | `Hamiltonian` | The Hamiltonian to estimate the expectation value of. | *required* | | `parameters` | `ExecutionParams \| list[ExecutionParams] \| None` | A dictionary of parameter values, or a list of dictionaries for batch execution. Each key should be the name of a parameter in the quantum program (parameters of the main function), and the value should be the value to set for that parameter. | None | | `num_shots` | `int \| None` | Number of shots for this call only; overrides the session default when set. | None | | `run_via_classiq` | `bool \| None` | Run via Classiq credentials for this call only; overrides the session default when set. | None | **Returns:** * **Type:** `ExecutionJob` * The execution job. ### estimate
estimate(
self: ,
hamiltonian: Hamiltonian,
parameters: ExecutionParams | list\[ExecutionParams] | None = None,
num\_shots: int | None = None,
run\_via\_classiq: bool | None = None
) -> EstimationResult | list\[EstimationResult]
Estimates the expectation value of the given Hamiltonian using the quantum program. **Deprecated:** `estimate` is deprecated and will no longer be supported starting on 2026-06-22. Use [observe()](#observe) instead. **Parameters:** | Name | Type | Description | Default | | ----------------- | -------------------------------------------------- | ----------- | ---------- | | `self` | \`\` | | *required* | | `hamiltonian` | `Hamiltonian` | | *required* | | `parameters` | `ExecutionParams \| list[ExecutionParams] \| None` | | None | | `num_shots` | `int \| None` | | None | | `run_via_classiq` | `bool \| None` | | None | ### submit\_estimate
submit\_estimate(
self: ,
hamiltonian: Hamiltonian,
parameters: ExecutionParams | list\[ExecutionParams] | None = None,
num\_shots: int | None = None,
run\_via\_classiq: bool | None = None
) -> ExecutionJob
Initiates an execution job with the `estimate` primitive. **Deprecated:** `submit_estimate` is deprecated and will no longer be supported starting on 2026-06-22. Use [submit\_observe()](#submit_observe) instead. **Parameters:** | Name | Type | Description | Default | | ----------------- | -------------------------------------------------- | ----------- | ---------- | | `self` | \`\` | | *required* | | `hamiltonian` | `Hamiltonian` | | *required* | | `parameters` | `ExecutionParams \| list[ExecutionParams] \| None` | | None | | `num_shots` | `int \| None` | | None | | `run_via_classiq` | `bool \| None` | | None | ### batch\_estimate
batch\_estimate(
self: ,
hamiltonian: Hamiltonian,
parameters: list\[ExecutionParams]
) -> list\[EstimationResult]
Estimates the expectation value of the given Hamiltonian multiple times using the quantum program, with the given parameters for each iteration. The number of estimations is determined by the length of the parameters list. **Deprecated:** Pass a list of parameter dicts to [observe()](#observe) instead. **Parameters:** | Name | Type | Description | Default | | ------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `self` | \`\` | | *required* | | `hamiltonian` | `Hamiltonian` | The Hamiltonian to estimate the expectation value of. | *required* | | `parameters` | `list[ExecutionParams]` | A list of the parameters for each iteration. Each item is a dictionary where each key should be the name of a parameter in the quantum program (parameters of the main function), and the value should be the value to set for that parameter. | *required* | **Returns:** * **Type:** `list[EstimationResult]` * List\[EstimationResult]: The results of all the estimation iterations. ### submit\_batch\_estimate
submit\_batch\_estimate(
self: ,
hamiltonian: Hamiltonian,
parameters: list\[ExecutionParams]
) -> ExecutionJob
Initiates an execution job with the `batch_estimate` primitive. **Deprecated:** Pass a list of parameter dicts to [submit\_observe()](#submit_observe) instead. **Parameters:** | Name | Type | Description | Default | | ------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `self` | \`\` | | *required* | | `hamiltonian` | `Hamiltonian` | The Hamiltonian to estimate the expectation value of. | *required* | | `parameters` | `list[ExecutionParams]` | A list of the parameters for each iteration. Each item is a dictionary where each key should be the name of a parameter in the quantum program (parameters of the main function), and the value should be the value to set for that parameter. | *required* | **Returns:** * **Type:** `ExecutionJob` * The execution job. ### variational\_minimize
variational\_minimize(
self: ,
cost\_function: Hamiltonian | QmodExpressionCreator,
initial\_params: ExecutionParams,
max\_iteration: int,
quantile: float = 1.0,
tolerance: float | None = None,
hosted: bool = False,
num\_shots: int | None = None,
run\_via\_classiq: bool | None = None
) -> list\[tuple\[float, ExecutionParams]]
Variationally minimizes the given cost function using the quantum program. **Parameters:** | Name | Type | Description | Default | | ----------------- | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `self` | \`\` | | *required* | | `cost_function` | `Hamiltonian \| QmodExpressionCreator` | The cost function to minimize. It can be one of the following: - A quantum cost function defined by a Hamiltonian. - A classical cost function represented as a callable that returns a Qmod expression. The callable should accept `QVar`s as arguments and use names matching the Model outputs. | *required* | | `initial_params` | `ExecutionParams` | The initial parameters for the minimization. Only Models with exactly one execution parameter are supported. This parameter must be of type `CReal` or `CArray`. The dictionary must contain a single key-value pair, where: - The key is the name of the parameter. - The value is either a float or a list of floats. | *required* | | `max_iteration` | `int` | The maximum number of iterations for the minimization. | *required* | | `quantile` | `float` | The quantile to use for cost estimation. | 1.0 | | `tolerance` | `float \| None` | The tolerance for the minimization. | None | | `hosted` | `bool` | When `True` on an IonQ backend, submit a Classiq execution job that runs the optimization loop on IonQ Hosted Hybrid in the backend. Defaults to `False`. | False | | `num_shots` | `int \| None` | Number of shots for this call only; overrides the session default when set. | None | | `run_via_classiq` | `bool \| None` | Run via Classiq credentials for this call only; overrides the session default when set. | None | **Returns:** * **Type:** `list[tuple[float, ExecutionParams]]` * A list of tuples, each containing the estimated cost and the corresponding parameters for that iteration. `cost` is a float, and `parameters` is a dictionary matching the execution parameter format. ### minimize
minimize(
self: ,
cost\_function: Hamiltonian | QmodExpressionCreator,
initial\_params: ExecutionParams,
max\_iteration: int,
quantile: float = 1.0,
tolerance: float | None = None,
num\_shots: int | None = None,
run\_via\_classiq: bool | None = None
) -> list\[tuple\[float, ExecutionParams]]
**Deprecated:** Use [variational\_minimize()](#variational_minimize) instead. This name is kept for backward compatibility and will be removed in a future release. **Parameters:** | Name | Type | Description | Default | | ----------------- | -------------------------------------- | ----------- | ---------- | | `self` | \`\` | | *required* | | `cost_function` | `Hamiltonian \| QmodExpressionCreator` | | *required* | | `initial_params` | `ExecutionParams` | | *required* | | `max_iteration` | `int` | | *required* | | `quantile` | `float` | | 1.0 | | `tolerance` | `float \| None` | | None | | `num_shots` | `int \| None` | | None | | `run_via_classiq` | `bool \| None` | | None | ### submit\_variational\_minimize
submit\_variational\_minimize(
self: ,
cost\_function: Hamiltonian | QmodExpressionCreator,
initial\_params: ExecutionParams,
max\_iteration: int,
quantile: float = 1.0,
tolerance: float | None = None,
hosted: bool = False,
num\_shots: int | None = None,
run\_via\_classiq: bool | None = None
) -> ExecutionJob
Initiates an execution job with the variational minimization primitive. Non-blocking counterpart of [variational\_minimize()](#variational_minimize): same parameters and job, but returns the [ExecutionJob](#executionjob) immediately. **Parameters:** | Name | Type | Description | Default | | ----------------- | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `self` | \`\` | | *required* | | `cost_function` | `Hamiltonian \| QmodExpressionCreator` | The cost function to minimize. It can be one of the following: - A quantum cost function defined by a Hamiltonian. - A classical cost function represented as a callable that returns a Qmod expression. The callable should accept `QVar`s as arguments and use names matching the Model outputs. | *required* | | `initial_params` | `ExecutionParams` | The initial parameters for the minimization. Only Models with exactly one execution parameter are supported. This parameter must be of type `CReal` or `CArray`. The dictionary must contain a single key-value pair, where: - The key is the name of the parameter. - The value is either a float or a list of floats. | *required* | | `max_iteration` | `int` | The maximum number of iterations for the minimization. | *required* | | `quantile` | `float` | The quantile to use for cost estimation. | 1.0 | | `tolerance` | `float \| None` | The tolerance for the minimization. | None | | `hosted` | `bool` | When `True` on an IonQ backend, submit a Classiq execution job that runs the optimization loop on IonQ Hosted Hybrid in the backend. Defaults to `False`. | False | | `num_shots` | `int \| None` | Number of shots for this call only; overrides the session default when set. | None | | `run_via_classiq` | `bool \| None` | Run via Classiq credentials for this call only; overrides the session default when set. | None | **Returns:** * **Type:** `ExecutionJob` * The execution job. When `hosted=True` on an IonQ backend, the backend * worker submits and polls IonQ Hosted Hybrid while this job tracks * progress through the standard Classiq execution API. ### submit\_minimize
submit\_minimize(
self: ,
cost\_function: Hamiltonian | QmodExpressionCreator,
initial\_params: ExecutionParams,
max\_iteration: int,
quantile: float = 1.0,
tolerance: float | None = None,
num\_shots: int | None = None,
run\_via\_classiq: bool | None = None
) -> ExecutionJob
**Deprecated:** Use [submit\_variational\_minimize()](#submit_variational_minimize) instead. This name is kept for backward compatibility and will be removed in a future release. **Parameters:** | Name | Type | Description | Default | | ----------------- | -------------------------------------- | ----------- | ---------- | | `self` | \`\` | | *required* | | `cost_function` | `Hamiltonian \| QmodExpressionCreator` | | *required* | | `initial_params` | `ExecutionParams` | | *required* | | `max_iteration` | `int` | | *required* | | `quantile` | `float` | | 1.0 | | `tolerance` | `float \| None` | | None | | `num_shots` | `int \| None` | | None | | `run_via_classiq` | `bool \| None` | | None | ### estimate\_cost
estimate\_cost(
self: ,
cost\_func: Callable\[\[ParsedState], float],
parameters: ExecutionParams | None = None,
quantile: float = 1.0,
num\_shots: int | None = None,
run\_via\_classiq: bool | None = None
) -> float
Estimates circuit cost using a classical cost function. **Parameters:** | Name | Type | Description | Default | | ----------------- | -------------------------------- | --------------------------------------------------------------------------------------- | ---------- | | `self` | \`\` | | *required* | | `cost_func` | `Callable[[ParsedState], float]` | classical circuit sample cost function | *required* | | `parameters` | `ExecutionParams \| None` | execution parameters sent to 'sample' | None | | `quantile` | `float` | drop cost values outside the specified quantile | 1.0 | | `num_shots` | `int \| None` | Number of shots for this call only; overrides the session default when set. | None | | `run_via_classiq` | `bool \| None` | Run via Classiq credentials for this call only; overrides the session default when set. | None | **Returns:** * **Type:** `float` * cost estimation ### set\_measured\_state\_filter
set\_measured\_state\_filter(
self: ,
output\_name: str,
condition: Callable
) -> None
When simulating on a statevector simulator, emulate the behavior of postprocessing by discarding amplitudes for which their states are "undesirable". **Parameters:** | Name | Type | Description | Default | | ------------- | ---------- | --------------------------------------------------------------------- | ---------- | | `self` | \`\` | | *required* | | `output_name` | `str` | The name of the register to filter | *required* | | `condition` | `Callable` | Filter out values of the statevector for which this callable is False | *required* | ## QBit A type representing a single qubit. `QBit` serves both as a placeholder for a temporary, non-allocated qubit and as the type of an allocated physical or logical qubit. Conceptually, a qubit is a two-level quantum system, described by the superposition of the computational basis states: $$ |0\rangle = \begin{pmatrix} 1 \\ 0 \end{pmatrix}, \quad |1\rangle = \begin{pmatrix} 0 \\ 1 \end{pmatrix} $$ Therefore, a qubit state is a linear combination: $$ |\psi\rangle = \alpha |0\rangle + \beta |1\rangle, $$ where ( \alpha ) and ( \beta ) are complex numbers satisfying: $$ |\alpha|^2 + |\beta|^2 = 1. $$ Typical usage includes: * Representing an unallocated qubit before its allocation. * Acting as the output type for a qubit or an allocated qubit in the main function after calling an allocation function. Examples: **Methods:** | Name | Description | | --------------------------------- | ----------- | | [to\_qvar](#to_qvar) | | | [get\_qmod\_type](#get_qmod_type) | | ### to\_qvar
to\_qvar(
cls: ,
origin: str | Expr,
type\_hint: Any
) -> QBit
**Parameters:** | Name | Type | Description | Default | | ----------- | ------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `origin` | `str \| Expr` | | *required* | | `type_hint` | `Any` | | *required* | ### get\_qmod\_type
get\_qmod\_type(
self:
) -> ConcreteQuantumType
**Parameters:** | Name | Type | Description | Default | | ------ | ---- | ----------- | ---------- | | `self` | \`\` | | *required* | ### Z
Z(
target: Const\[QBit]
) -> None
\[Qmod core-library function] Performs the Pauli-Z gate on a qubit. This operation is represented by the following matrix: $$ Z = \begin{bmatrix} 1 & 0 \\ 0 & -1 \end{bmatrix} $$ **Parameters:** | Name | Type | Description | Default | | -------- | ------------- | --------------------------------------- | ---------- | | `target` | `Const[QBit]` | The qubit to apply the Pauli-Z gate to. | *required* | ### allocate
allocate(
args: Any = (),
kwargs: Any = 
) -> None
Initialize a quantum variable to a new quantum object in the zero state: $$ \left|\text{out}\right\rangle = \left|0\right\rangle^{\otimes \text{num_qubits}} $$ If 'num\_qubits' is not specified, it will be inferred according to the type of 'out'. In case the quantum variable is of type `QNum`, its numeric attributes can be specified as well. **Parameters:** | Name | Type | Description | Default | | -------- | ----- | ----------- | ------- | | `args` | `Any` | | () | | `kwargs` | `Any` | | | ### bind
bind(
source: Input\[QVar] | list\[Input\[QVar]],
destination: Output\[QVar] | list\[Output\[QVar]]
) -> None
Reassign qubit or arrays of qubits by redirecting their logical identifiers. This operation rewires the logical identity of the `source` qubits to new objects given in `destination`. For example, an array of two qubits `X` can be mapped to individual qubits `Y` and `Z`. **Parameters:** | Name | Type | Description | Default | | ------------- | ------------------------------------ | ----------------------------------------------------------------------------------------- | ---------- | | `source` | `Input[QVar] \| list[Input[QVar]]` | A qubit or list of initialized qubits to reassign. | *required* | | `destination` | `Output[QVar] \| list[Output[QVar]]` | A qubit or list of target qubits to bind to. Must match the number of qubits in `source`. | *required* | ### within\_apply
within\_apply(
within: Callable\[\[], Statements],
apply: Callable\[\[], Statements]
) -> None
Given two operations $U$ and $V$, performs the composition of operations $U^\{-1\} V U$. This operation is used to represent a sequence where the operation `U` is applied, followed by another operation `V`, and then `U^{-1}` is applied to uncompute. This pattern is common in reversible computation and quantum subroutines. **Parameters:** | Name | Type | Description | Default | | -------- | -------------------------- | ------------------------------------------------------------- | ---------- | | `within` | `Callable[[], Statements]` | The unitary operation `U` to be computed and then uncomputed. | *required* | | `apply` | `Callable[[], Statements]` | The operation `V` to be applied within the `U` block. | *required* | ### drop
drop(
in\_: Input\[QArray\[QBit]]
) -> None
\[Qmod core-library function] Discards the qubits allocated to a quantum variable which may be in any state, preventing their further use. **Parameters:** | Name | Type | Description | Default | | ----- | --------------------- | ---------------------------------------------------------------------- | ---------- | | `in_` | `Input[QArray[QBit]]` | The quantum variable that will be dropped. Must be initialized before. | *required* | ### create\_model
create\_model(
entry\_point: QFunc | GenerativeQFunc,
constraints: Constraints | None = None,
execution\_preferences: ExecutionPreferences | None = None,
preferences: Preferences | None = None,
classical\_execution\_function: CFunc | None = None,
out\_file: str | None = None
) -> SerializedModel
Create a serialized model from a given Qmod entry function and additional parameters. **Parameters:** | Name | Type | Description | Default | | ------------------------------ | ------------------------------ | -------------------------------------------------------------------------------- | ---------- | | `entry_point` | `QFunc \| GenerativeQFunc` | The entry point function for the model, which must be a QFunc named 'main'. | *required* | | `constraints` | `Constraints \| None` | Constraints for the synthesis of the model. See Constraints (Optional). | None | | `execution_preferences` | `ExecutionPreferences \| None` | Preferences for the execution of the model. See ExecutionPreferences (Optional). | None | | `preferences` | `Preferences \| None` | Preferences for the synthesis of the model. See Preferences (Optional). | None | | `classical_execution_function` | `CFunc \| None` | A function for the classical execution logic, which must be a CFunc (Optional). | None | | `out_file` | `str \| None` | File path to write the Qmod model in native Qmod representation to (Optional). | None | **Returns:** * **Type:** `SerializedModel` * A serialized model. ### synthesize
synthesize(
model: SerializedModel | BaseQFunc,
auto\_show: bool = False,
constraints: Constraints | None = None,
preferences: Preferences | None = None
) -> QuantumProgram
Synthesize a model with the Classiq engine to receive a quantum program. [More details](https://docs.classiq.io/latest/sdk-reference/synthesis/#classiq.synthesize) **Parameters:** | Name | Type | Description | Default | | ------------- | ------------------------------ | ---------------------------------------------------------------------------------------------------------- | ---------- | | `model` | `SerializedModel \| BaseQFunc` | The entry point of the Qmod model - a qfunc named 'main' (or alternatively the output of 'create\_model'). | *required* | | `auto_show` | `bool` | Whether to 'show' the synthesized model (False by default). | False | | `constraints` | `Constraints \| None` | Constraints for the synthesis of the model. See Constraints (Optional). | None | | `preferences` | `Preferences \| None` | Preferences for the synthesis of the model. See Preferences (Optional). | None | **Returns:** * **Type:** `QuantumProgram` * Quantum program. (See: QuantumProgram) ## IQAE Implementation of Iterative Quantum Amplitude Estimation \[1]. Given $A$ s.t. $A`|0>`_n`|0>` = \sqrt\{1-a\}|\psi_0>_n`|0>` + \sqrt\{a\}|\psi_1>_n`|1>`$, the algorithm estimates $a$ by iteratively sampling $Q^kA$, where $Q=AS_0A^\{\dagger\}S_\{\psi_0\}$, and $k$ is an integer variable. For estimating $a$, The algorithm estimates $\theta_a$ which is defined by $a = sin^2(\theta_a)$, so it starts with a confidence interval $(0, \pi/2)$ and narrows down this interval on each iteration according to the sample results. **Methods:** | Name | Description | | ------------------------ | -------------------------------------------------------------------------------------------- | | [get\_model](#get_model) | Implement the quantum part of IQAE in terms of the Qmod Model. | | [get\_qprog](#get_qprog) | Create an executable quantum Program for IQAE. | | [run](#run) | Executes IQAE's quantum program with the provided epsilon, alpha, and execution preferences. | ### get\_model
get\_model(
self:
) -> SerializedModel
Implement the quantum part of IQAE in terms of the Qmod Model **Parameters:** | Name | Type | Description | Default | | ------ | ---- | ----------- | ---------- | | `self` | \`\` | | *required* | **Returns:** * **Type:** `SerializedModel` * A serialized model. ### get\_qprog
get\_qprog(
self:
) -> QuantumProgram
Create an executable quantum Program for IQAE. **Parameters:** | Name | Type | Description | Default | | ------ | ---- | ----------- | ---------- | | `self` | \`\` | | *required* | **Returns:** * **Type:** `QuantumProgram` * Quantum program. See QuantumProgram. ### run
run(
self: ,
epsilon: float,
alpha: float,
execution\_preferences: ExecutionPreferences | None = None
) -> IQAEResult
Executes IQAE's quantum program with the provided epsilon, alpha, and execution preferences. If execution\_preferences has been proved, or if it does not contain num\_shot, then num\_shot is set to 2048. **Parameters:** | Name | Type | Description | Default | | ----------------------- | ------------------------------ | -------------------------------------------------------------------------------- | ---------- | | `self` | \`\` | | *required* | | `epsilon` | `float` | Target accuracy in therm of $\theta_a$ e.g $a = sin^2(\theta_a \pm \epsilon)$ . | *required* | | `alpha` | `float` | Specifies the confidence level (1 - alpha) | *required* | | `execution_preferences` | `ExecutionPreferences \| None` | Preferences for the execution of the model. See ExecutionPreferences (Optional). | None | | Members: | | | | | Name | Description | | ------------------- | -------------------------------------------------------------------------------------------------------- | | `IQAEIterationData` | Handles the data storage for a single iteration of the Iterative Quantum Amplitude Estimation algorithm. | | `IQAEResult` | Represents the result of an Iterative Quantum Amplitude Estimation (IQAE) process. | ## IQAEIterationData Handles the data storage for a single iteration of the Iterative Quantum Amplitude Estimation algorithm. This class is intended to represent the results and state of a single Grover iteration of the IQAE process. **Attributes:** | Name | Type | Description | | ------------------- | ------------------ | ----------------------------------------------------------------- | | `grover_iterations` | `int` | The iteration number of Grover's algorithm. | | `sample_results` | `ExecutionDetails` | The `ExecutionDetails` of Grover iteration. See ExecutionDetails. | ## IQAEResult Represents the result of an Iterative Quantum Amplitude Estimation (IQAE) process. This class encapsulates the output of the IQAE algorithm, including the estimated value, confidence interval, intermediate iteration data, and any warnings generated during the computation. **Attributes:** | Name | Type | Description | | --------------------- | ------------------------- | ----------------------------------------------------------------------------------- | | `estimation` | `float` | Estimation of the amplitude. | | `confidence_interval` | `list[float]` | The interval in which the amplitude is within, with a probability equal to epsilon. | | `iterations_data` | `list[IQAEIterationData]` | List of `IQAEIterationData` of each Grover iteration. | | `warnings` | `list[str]` | List of warnings generated during the IQAE process of each Grover iteration. | # Qsp Source: https://docs.classiq.io/sdk-reference/applications/QSP ## Functions ### qsvt\_phases
qsvt\_phases(
poly\_coeffs: np.ndarray,
cheb\_basis: bool = True
) -> np.ndarray
Get QSVT phases that will generate the given Chebyshev polynomial. The phases are ready to be used in `qsvt` and `qsvt_lcu` functions in the classiq library. The convention is the reflection signal operator, and the measurement basis is the hadamard basis (see [https://arxiv.org/abs/2105.02859](https://arxiv.org/abs/2105.02859) APPENDIX A.). The current implementation is using the nlft-qsp package. **Parameters:** | Name | Type | Description | Default | | ------------- | ------------ | ------------------------------------------------------------------------------------------------------ | ---------- | | `poly_coeffs` | `np.ndarray` | Array of polynomial coefficients (Chebyshev\Monomial, depending on cheb\_basis). | *required* | | `cheb_basis` | `bool` | Whether the poly coefficients are given in Chebyshev (True) or Monomial(False). Defaults to Chebyshev. | True | ### qsp\_approximate
qsp\_approximate(
f\_target: Callable\[\[float], complex],
degree: int,
parity: int | None = None,
interval: tuple\[float, float] = (-1, 1),
bound: float = 0.99,
num\_grid\_points: int | None = None,
plot: bool = False
) -> tuple\[np.ndarray, float]
Approximate the target function on the given (sub-)interval of \[-1,1], using QSP-compatible chebyshev polynomials. The approximating polynomial is enforced to |P(x)| \<= bound on all of \[-1,1]. Note: scaling f\_target by a factor \< 1 might help the convergence and also a later qsp phase factor finiding. **Parameters:** | Name | Type | Description | Default | | ----------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------ | ---------- | | `f_target` | `Callable[[float], complex]` | Real function to approximate within the given interval. Should be bounded by \[-1, 1] in the given interval. | *required* | | `degree` | `int` | Approximating polynomial degree. | *required* | | `parity` | `int \| None` | None - full polynomial, 0 - restrict to even polynomial, 1 - odd polynomial. | None | | `interval` | `tuple[float, float]` | sub interval of \[-1, 1] to approximate the function within. | (-1, 1) | | `bound` | `float` | global polynomial bound on \[-1,1] (defaults to 0.99). | 0.99 | | `num_grid_points` | `int \| None` | sets the number of grid points used for the polynomial approximation (defaults to `max(2 * degree, 1000)`). | None | | `plot` | `bool` | A flag for plotting the resulting approximation vs the target function. | False | **Returns:** * **Type:** `tuple[np.ndarray, float]` * Array of Chebyshev coefficients. In case of definite parity, still a full coefficients array is returned. * (Approximated) maximum error between the target function and the approximating polynomial within the interval. ### gqsp\_phases
gqsp\_phases(
poly\_coeffs: np.ndarray,
cheb\_basis: bool = False
) -> list\[tuple\[float, float, float]]
Compute GQSP phases for a polynomial in the monomial (power) basis. The returned phases are compatible with Classiq's `gqsp` function and use the Wz signal operator convention. The current implementation is using the nlft-qsp package, based on techniques in [https://arxiv.org/abs/2503.03026](https://arxiv.org/abs/2503.03026). Notes: * The polynomial must be bounded on the unit circle: $|P(e^\{i*theta\})|$ \<= 1 for all theta in $[0, 2*pi)$. * Laurent polynomials are supported by degree shifting. If $P(z) = sum_\{k=m\}^n c_k * z^k with m < 0, the phases correspond to the$ degree-shifted polynomial $z^\{-m\} * P(z)$ (so the minimal degree is zero). * The phase finiding works in the monomial basis. If a Chebyshev basis polynomial is provided, it will be converted to the monomial basis (and introduce an additional overhead). **Parameters:** | Name | Type | Description | Default | | ------------- | ------------ | ----------------------------------------------------------------------------------------------------- | ---------- | | `poly_coeffs` | `np.ndarray` | | *required* | | `cheb_basis` | `bool` | Whether the poly coefficients are given in Chebyshev (True) or Monomial(False). Defaults to Monomial. | False | **Returns:** * **Type:** `list[tuple[float, float, float]]` * list of (theta, phi, lambda) tuples of length d+1, ready to use with `gqsp`. ### poly\_jacobi\_anger\_cos
poly\_jacobi\_anger\_cos(
degree: int,
t: float
) -> np.ndarray
Gets the Chebyshev polynomial coefficients approximating cos(t\*x) using the Jacobi-Anger expansion. $\cos(xt) = J_0(t) + 2\sum_\{k=1\}^\{d/2\} (-1)^k J_\{2k\}(t)\, T_\{2k\}(x) $ **Parameters:** | Name | Type | Description | Default | | -------- | ------- | -------------------------------------------- | ---------- | | `degree` | `int` | the degree of the approximating polynomial. | *required* | | `t` | `float` | the parameter in cos(t\*x). Can be negative. | *required* | ### poly\_jacobi\_anger\_sin
poly\_jacobi\_anger\_sin(
degree: int,
t: float
) -> np.ndarray
Gets the Chebyshev polynomial coefficients approximating sin(t\*x) using the Jacobi-Anger expansion. $\sin(xt) = 2\sum_\{k=0\}^\{d/2\} (-1)^k J_\{2k+1\}(t)\, T_\{2k+1\}(x)$ **Parameters:** | Name | Type | Description | Default | | -------- | ------- | ------------------------------------------- | ---------- | | `degree` | `int` | the degree of the approximating polynomial. | *required* | | `t` | `float` | the parameter in sin(t\*x). | *required* | ### poly\_jacobi\_anger\_exp\_sin
poly\_jacobi\_anger\_exp\_sin(
degree: int,
t: float
) -> np.ndarray
Gets the Chebyshev polynomial coefficients approximating exp(i*t*sin(x)) using the Jacobi-Anger expansion: $e^\{it\sin(x)\} = \sum_\{k=-d\}^\{d\} J_\{k\}(t) e^\{ikx\}$ **Parameters:** | Name | Type | Description | Default | | -------- | ------- | --------------------------------------------------------------------------- | ---------- | | `degree` | `int` | the maximum degree of the approximating polynomial (negative and positive). | *required* | | `t` | `float` | the parameter in exp(i*t*sin(x)). | *required* | ### poly\_jacobi\_anger\_exp\_cos
poly\_jacobi\_anger\_exp\_cos(
degree: int,
t: float
) -> np.ndarray
Gets the Chebyshev polynomial coefficients approximating exp(i*t*cos(x)) using the Jacobi-Anger expansion: $e^\{it\cos(x)\} = \sum_\{k=-d\}^\{d\} i^k J_\{k\}(t) e^\{ikx\}$ **Parameters:** | Name | Type | Description | Default | | -------- | ------- | --------------------------------------------------------------------------- | ---------- | | `degree` | `int` | the maximum degree of the approximating polynomial (negative and positive). | *required* | | `t` | `float` | the parameter in exp(i*t*cos(x)). | *required* | ### poly\_inversion
poly\_inversion(
degree: int,
kappa: float,
error\_type: str | ErrorType = ErrorType.RELATIVE
) -> tuple\[np.ndarray, float]
Gets the Chebyshev odd polynomial p(x) coefficients approximating 1/x on \[1/kappa, 1]. Based on the papers: [https://dl.acm.org/doi/pdf/10.1145/3649320](https://dl.acm.org/doi/pdf/10.1145/3649320) - for optimal polynomial that minimizes the relative error |xp(x)-1| for x in \[1/kappa,1]; and [https://arxiv.org/pdf/2507.15537-](https://arxiv.org/pdf/2507.15537-) for optimal polynomial that minimizes the uniform error |p(x)-1/x| for x in \[1/kappa,1]. The relative error refers to |xp(x)-1|, whereas the uniform error refers to |p(x)-1/x|, both for x in \[1/kappa,1]. **Parameters:** | Name | Type | Description | Default | | ------------ | ------------------ | --------------------------------------------------------------------------------------------------------------------------- | ------------------ | | `degree` | `int` | The degree of the approximating polynomial. | *required* | | `kappa` | `float` | The number defining the interval \[1/kappa, 1], usually represents the condition number in the setting of matrix inversion. | *required* | | `error_type` | `str \| ErrorType` | A string specifying the error type to minimize, can be either "relative" (default) or "uniform". | ErrorType.RELATIVE | **Returns:** * **Type:** `tuple[np.ndarray, float]` * The Chebyshev polynomial coefficients approximating 1/x on \[1/kappa, 1] using the optimal polynomial of given degree. * An upper bound on the maximum absolute value of the polynomial on \[-1, 1]. The value can be used to scale down the polynomial for the usage within QSVT. # Qsvm Source: https://docs.classiq.io/sdk-reference/applications/QSVM ## QSVM Quantum support vector machine (QSVM) model. Classifies classical data into two categories. The model is first trained, and fitted. After pre-training, the model predicts the labels of new data points. **Methods:** | Name | Description | | --------------------------------- | ------------------------------------------------------------------------------------------------------------- | | [train](#train) | Trains an SVM model using a custom precomputed kernel from the training data. | | [predict](#predict) | Predicts labels for new data using a precomputed kernel with a trained SVM model. | | [test](#test) | Predicts the labels of the test dataset and evaluates the resulting test score using the ground-truth labels. | | [get\_svm\_model](#get_svm_model) | Returns the classical SVM model. | | [get\_qprog](#get_qprog) | Returns the quantum program for the kernel at `data_dim` feature width. | **Attributes:** | Name | Type | Description | | ----------------------- | -------------------- | ----------- | | `feature_map` | | | | `num_qubits` | | | | `execution_preferences` | | | | `kernel_eval` | | | | `model` | | | | `train_data` | `np.ndarray \| None` | | ### train
train(
self: ,
train\_data: np.ndarray,
train\_labels: np.ndarray
) -> None
Trains an SVM model using a custom precomputed kernel from the training data. **Parameters:** | Name | Type | Description | Default | | -------------- | ------------ | ------------------------------------- | ---------- | | `self` | \`\` | | *required* | | `train_data` | `np.ndarray` | Contains the data points (np.ndarray) | *required* | | `train_labels` | `np.ndarray` | Contains the labels (0,1). | *required* | ### predict
predict(
self: ,
data: np.ndarray
) -> np.ndarray
Predicts labels for new data using a precomputed kernel with a trained SVM model. Evaluates kernel matrix elements which are associated with the support vectors (those associated with non-vanishing coefficients in the prediction equation). **Parameters:** | Name | Type | Description | Default | | ------ | ------------ | ----------------------------------- | ---------- | | `self` | \`\` | | *required* | | `data` | `np.ndarray` | List of new data points to predict. | *required* | **Returns:** * **Type:** `np.ndarray` * np.ndarray: Predicted labels (0,1). ### test
test(
self: ,
data: np.ndarray,
data\_labels: np.ndarray
) -> tuple\[float, np.ndarray]
Predicts the labels of the test dataset and evaluates the resulting test score using the ground-truth labels. **Parameters:** | Name | Type | Description | Default | | ------------- | ------------ | ------------------------------------ | ---------- | | `self` | \`\` | | *required* | | `data` | `np.ndarray` | List of test data points to predict. | *required* | | `data_labels` | `np.ndarray` | Contains the test data labels. | *required* | **Returns:** * **Type:** `tuple[float, np.ndarray]` * containing test score (float) and test labels (np.ndarray\[int]). ### get\_svm\_model
get\_svm\_model(
self:
) -> SVC
Returns the classical SVM model. **Parameters:** | Name | Type | Description | Default | | ------ | ---- | ----------- | ---------- | | `self` | \`\` | | *required* | ### get\_qprog
get\_qprog(
self: ,
data\_dim: int = 0
) -> QuantumProgram
Returns the quantum program for the kernel at `data_dim` feature width. **Parameters:** | Name | Type | Description | Default | | ---------- | ----- | ----------- | ---------- | | `self` | \`\` | | *required* | | `data_dim` | `int` | | 0 | # Chemistry Source: https://docs.classiq.io/sdk-reference/applications/chemistry Members: | Name | Description | | --------------------------- | -------------------------------------------------------------------------------------- | | `FermionHamiltonianProblem` | Defines an electronic-structure problem using a Fermionic operator and electron count. | ## FermionHamiltonianProblem Defines an electronic-structure problem using a Fermionic operator and electron count. Can also be constructed from a `MolecularData` object using the `from_molecule` method. **Methods:** | Name | Description | | -------------------------------- | -------------------------------------------------------------- | | [from\_molecule](#from_molecule) | Constructs a `FermionHamiltonianProblem` from a molecule data. | **Attributes:** | Name | Type | Description | | --------------------- | ----------------- | ----------------------------------------------------------------------------------- | | `occupied_alpha` | `list[int]` | | | `virtual_alpha` | `list[int]` | | | `occupied_beta` | `list[int]` | | | `virtual_beta` | `list[int]` | | | `occupied` | `list[int]` | | | `virtual` | `list[int]` | | | `fermion_hamiltonian` | `FermionOperator` | The fermionic hamiltonian of the problem. Assumed to be in the block-spin labeling. | | `n_orbitals` | `int` | Number of spatial orbitals. | | `n_alpha` | `int` | Number of alpha particles. | | `n_beta` | `int` | Number of beta particles. | | `n_particles` | `tuple[int, int]` | Number of alpha and beta particles. | ### from\_molecule
from\_molecule(
cls: ,
molecule: MolecularData,
first\_active\_index: int = 0,
remove\_orbitals: Sequence\[int] | None = None,
op\_compression\_tol: float = 1e-13
) -> FermionHamiltonianProblem
Constructs a `FermionHamiltonianProblem` from a molecule data. **Parameters:** | Name | Type | Description | Default | | -------------------- | ----------------------- | ---------------------------------------------------------------- | ---------- | | `cls` | \`\` | | *required* | | `molecule` | `MolecularData` | The molecule data. | *required* | | `first_active_index` | `int` | The first active index, indicates all prior indices are freezed. | 0 | | `remove_orbitals` | `Sequence[int] \| None` | Active indices to be removed. | None | | `op_compression_tol` | `float` | Tolerance for trimming the fermion operator. | 1e-13 | **Returns:** * **Type:** `FermionHamiltonianProblem` * The fermion hamiltonian problem. Members: | Name | Description | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------- | | `FermionHamiltonianProblem` | Defines an electronic-structure problem using a Fermionic operator and electron count. | | `MappingMethod` | Mapping methods from fermionic operators to qubits operators. | | `FermionToQubitMapper` | Mapper between fermionic operators to qubits operators, using one of the supported mapping methods (see `MappingMethod`). | ## FermionHamiltonianProblem Defines an electronic-structure problem using a Fermionic operator and electron count. Can also be constructed from a `MolecularData` object using the `from_molecule` method. **Methods:** | Name | Description | | -------------------------------- | -------------------------------------------------------------- | | [from\_molecule](#from_molecule) | Constructs a `FermionHamiltonianProblem` from a molecule data. | **Attributes:** | Name | Type | Description | | --------------------- | ----------------- | ----------------------------------------------------------------------------------- | | `occupied_alpha` | `list[int]` | | | `virtual_alpha` | `list[int]` | | | `occupied_beta` | `list[int]` | | | `virtual_beta` | `list[int]` | | | `occupied` | `list[int]` | | | `virtual` | `list[int]` | | | `fermion_hamiltonian` | `FermionOperator` | The fermionic hamiltonian of the problem. Assumed to be in the block-spin labeling. | | `n_orbitals` | `int` | Number of spatial orbitals. | | `n_alpha` | `int` | Number of alpha particles. | | `n_beta` | `int` | Number of beta particles. | | `n_particles` | `tuple[int, int]` | Number of alpha and beta particles. | ### from\_molecule
from\_molecule(
cls: ,
molecule: MolecularData,
first\_active\_index: int = 0,
remove\_orbitals: Sequence\[int] | None = None,
op\_compression\_tol: float = 1e-13
) -> FermionHamiltonianProblem
Constructs a `FermionHamiltonianProblem` from a molecule data. **Parameters:** | Name | Type | Description | Default | | -------------------- | ----------------------- | ---------------------------------------------------------------- | ---------- | | `cls` | \`\` | | *required* | | `molecule` | `MolecularData` | The molecule data. | *required* | | `first_active_index` | `int` | The first active index, indicates all prior indices are freezed. | 0 | | `remove_orbitals` | `Sequence[int] \| None` | Active indices to be removed. | None | | `op_compression_tol` | `float` | Tolerance for trimming the fermion operator. | 1e-13 | **Returns:** * **Type:** `FermionHamiltonianProblem` * The fermion hamiltonian problem. ## MappingMethod Mapping methods from fermionic operators to qubits operators. **Attributes:** | Name | Type | Description | | --------------- | ------ | ----------- | | `JORDAN_WIGNER` | `'jw'` | | | `BRAVYI_KITAEV` | `'bk'` | | ## FermionToQubitMapper Mapper between fermionic operators to qubits operators, using one of the supported mapping methods (see `MappingMethod`). **Methods:** | Name | Description | | ----------------------------------- | ---------------------------------------------------------------------------------------- | | [map](#map) | Maps the given fermionic operator to a qubits operator using the mapper's configuration. | | [get\_num\_qubits](#get_num_qubits) | Gets the number of qubits after mapping the given problem into qubits space. | **Attributes:** | Name | Type | Description | | -------- | --------------- | ------------------- | | `method` | `MappingMethod` | The mapping method. | ### map
map(
self: ,
fermion\_op: FermionOperator,
args: Any = (),
kwargs: Any = 
) -> QubitOperator
Maps the given fermionic operator to a qubits operator using the mapper's configuration. **Parameters:** | Name | Type | Description | Default | | ------------ | ----------------- | --------------------- | ---------- | | `self` | \`\` | | *required* | | `fermion_op` | `FermionOperator` | A fermionic operator. | *required* | | `args` | `Any` | | () | | `kwargs` | `Any` | | | **Returns:** * **Type:** `QubitOperator` * The mapped qubits operator. ### get\_num\_qubits
get\_num\_qubits(
self: ,
problem: FermionHamiltonianProblem
) -> int
Gets the number of qubits after mapping the given problem into qubits space. **Parameters:** | Name | Type | Description | Default | | --------- | --------------------------- | -------------------- | ---------- | | `self` | \`\` | | *required* | | `problem` | `FermionHamiltonianProblem` | The fermion problem. | *required* | **Returns:** * **Type:** `int` * The number of qubits. Members: | Name | Description | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | `FermionToQubitMapper` | Mapper between fermionic operators to qubits operators, using one of the supported mapping methods (see `MappingMethod`). | | `MappingMethod` | Mapping methods from fermionic operators to qubits operators. | | `FermionHamiltonianProblem` | Defines an electronic-structure problem using a Fermionic operator and electron count. | | `Z2SymTaperMapper` | Mapper between fermionic operators to qubits operators, using one of the supported mapping methods (see `MappingMethod`), and taking advantage of Z... | ## FermionToQubitMapper Mapper between fermionic operators to qubits operators, using one of the supported mapping methods (see `MappingMethod`). **Methods:** | Name | Description | | ----------------------------------- | ---------------------------------------------------------------------------------------- | | [map](#map) | Maps the given fermionic operator to a qubits operator using the mapper's configuration. | | [get\_num\_qubits](#get_num_qubits) | Gets the number of qubits after mapping the given problem into qubits space. | **Attributes:** | Name | Type | Description | | -------- | --------------- | ------------------- | | `method` | `MappingMethod` | The mapping method. | ### map
map(
self: ,
fermion\_op: FermionOperator,
args: Any = (),
kwargs: Any = 
) -> QubitOperator
Maps the given fermionic operator to a qubits operator using the mapper's configuration. **Parameters:** | Name | Type | Description | Default | | ------------ | ----------------- | --------------------- | ---------- | | `self` | \`\` | | *required* | | `fermion_op` | `FermionOperator` | A fermionic operator. | *required* | | `args` | `Any` | | () | | `kwargs` | `Any` | | | **Returns:** * **Type:** `QubitOperator` * The mapped qubits operator. ### get\_num\_qubits
get\_num\_qubits(
self: ,
problem: FermionHamiltonianProblem
) -> int
Gets the number of qubits after mapping the given problem into qubits space. **Parameters:** | Name | Type | Description | Default | | --------- | --------------------------- | -------------------- | ---------- | | `self` | \`\` | | *required* | | `problem` | `FermionHamiltonianProblem` | The fermion problem. | *required* | **Returns:** * **Type:** `int` * The number of qubits. ## MappingMethod Mapping methods from fermionic operators to qubits operators. **Attributes:** | Name | Type | Description | | --------------- | ------ | ----------- | | `JORDAN_WIGNER` | `'jw'` | | | `BRAVYI_KITAEV` | `'bk'` | | ## FermionHamiltonianProblem Defines an electronic-structure problem using a Fermionic operator and electron count. Can also be constructed from a `MolecularData` object using the `from_molecule` method. **Methods:** | Name | Description | | -------------------------------- | -------------------------------------------------------------- | | [from\_molecule](#from_molecule) | Constructs a `FermionHamiltonianProblem` from a molecule data. | **Attributes:** | Name | Type | Description | | --------------------- | ----------------- | ----------------------------------------------------------------------------------- | | `occupied_alpha` | `list[int]` | | | `virtual_alpha` | `list[int]` | | | `occupied_beta` | `list[int]` | | | `virtual_beta` | `list[int]` | | | `occupied` | `list[int]` | | | `virtual` | `list[int]` | | | `fermion_hamiltonian` | `FermionOperator` | The fermionic hamiltonian of the problem. Assumed to be in the block-spin labeling. | | `n_orbitals` | `int` | Number of spatial orbitals. | | `n_alpha` | `int` | Number of alpha particles. | | `n_beta` | `int` | Number of beta particles. | | `n_particles` | `tuple[int, int]` | Number of alpha and beta particles. | ### from\_molecule
from\_molecule(
cls: ,
molecule: MolecularData,
first\_active\_index: int = 0,
remove\_orbitals: Sequence\[int] | None = None,
op\_compression\_tol: float = 1e-13
) -> FermionHamiltonianProblem
Constructs a `FermionHamiltonianProblem` from a molecule data. **Parameters:** | Name | Type | Description | Default | | -------------------- | ----------------------- | ---------------------------------------------------------------- | ---------- | | `cls` | \`\` | | *required* | | `molecule` | `MolecularData` | The molecule data. | *required* | | `first_active_index` | `int` | The first active index, indicates all prior indices are freezed. | 0 | | `remove_orbitals` | `Sequence[int] \| None` | Active indices to be removed. | None | | `op_compression_tol` | `float` | Tolerance for trimming the fermion operator. | 1e-13 | **Returns:** * **Type:** `FermionHamiltonianProblem` * The fermion hamiltonian problem. ## Z2SymTaperMapper Mapper between fermionic operators to qubits operators, using one of the supported mapping methods (see `MappingMethod`), and taking advantage of Z2 symmetries in order to taper off qubits. **Methods:** | Name | Description | | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | [set\_sector](#set_sector) | Sets the symmetry sector coefficients. | | [map](#map) | Maps the given fermionic operator to qubits operator by using the mapper's method, and subsequently by tapering off qubits according to Z2 symmetries. | | [get\_num\_qubits](#get_num_qubits) | Gets the number of qubits after mapping the given problem into qubits space. | | [from\_problem](#from_problem) | Initializes a `Z2SymTaperMapper` object from a fermion problem (i.e. | **Attributes:** | Name | Type | Description | | ------------ | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `method` | `MappingMethod` | The mapping method. | | `generators` | `tuple[QubitOperator, ...]` | Generators representing the Z2 symmetries. | | `x_ops` | `tuple[QubitOperator, ...]` | Single-qubit X operations, such that each operation anti-commutes with its matching generator and commutes with all other generators. | ### set\_sector
set\_sector(
self: ,
sector: Sequence\[int]
) -> None
Sets the symmetry sector coefficients. **Parameters:** | Name | Type | Description | Default | | -------- | --------------- | ---------------------------------------------------------------- | ---------- | | `self` | \`\` | | *required* | | `sector` | `Sequence[int]` | (Sequence\[int]): Symmetry sector coefficients, each is 1 or -1. | *required* | ### map
map(
self: ,
fermion\_op: FermionOperator,
args: Any = (),
is\_invariant: bool = False,
kwargs: Any = 
) -> QubitOperator
Maps the given fermionic operator to qubits operator by using the mapper's method, and subsequently by tapering off qubits according to Z2 symmetries. **Parameters:** | Name | Type | Description | Default | | -------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------- | ---------- | | `self` | \`\` | | *required* | | `fermion_op` | `FermionOperator` | A fermionic operator. | *required* | | `args` | `Any` | | () | | `is_invariant` | `bool` | If `False`, the operator is not necessarily in the symmetry subspace, and thus gets projected onto it before tapering. | False | | `kwargs` | `Any` | | | **Returns:** * **Type:** `QubitOperator` * The mapped qubits operator. ### get\_num\_qubits
get\_num\_qubits(
self: ,
problem: FermionHamiltonianProblem
) -> int
Gets the number of qubits after mapping the given problem into qubits space. **Parameters:** | Name | Type | Description | Default | | --------- | --------------------------- | -------------------- | ---------- | | `self` | \`\` | | *required* | | `problem` | `FermionHamiltonianProblem` | The fermion problem. | *required* | **Returns:** * **Type:** `int` * The number of qubits. ### from\_problem
from\_problem(
cls: ,
problem: FermionHamiltonianProblem,
method: MappingMethod = MappingMethod.JORDAN\_WIGNER,
sector\_from\_hartree\_fock: bool = True,
tol: float = 1e-14
) -> Z2SymTaperMapper
Initializes a `Z2SymTaperMapper` object from a fermion problem (i.e. computing the Z2 symmetries from the problem definition). **Parameters:** | Name | Type | Description | Default | | -------------------------- | --------------------------- | ---------------------------------------------------------------------------------------- | ---------------------------- | | `cls` | \`\` | | *required* | | `problem` | `FermionHamiltonianProblem` | The fermion problem. | *required* | | `method` | `MappingMethod` | The mapping method. | MappingMethod.JORDAN\_WIGNER | | `sector_from_hartree_fock` | `bool` | Whether to compute the symmetry sector coefficients according to the Hartree-Fock state. | True | | `tol` | `float` | Tolerance for trimming off terms. | 1e-14 | **Returns:** * **Type:** `Z2SymTaperMapper` * The Z2 symmetries taper mapper. Members: | Name | Description | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------- | | `FermionToQubitMapper` | Mapper between fermionic operators to qubits operators, using one of the supported mapping methods (see `MappingMethod`). | | `FermionHamiltonianProblem` | Defines an electronic-structure problem using a Fermionic operator and electron count. | | `get_hf_fermion_op` | Constructs a fermion operator that creates the Hartree-Fock reference state in block-spin ordering. | | `get_hf_state` | Computes the qubits state after applying the Hartree-Fock operator defined by the given problem and mapper. | ## FermionToQubitMapper Mapper between fermionic operators to qubits operators, using one of the supported mapping methods (see `MappingMethod`). **Methods:** | Name | Description | | ----------------------------------- | ---------------------------------------------------------------------------------------- | | [map](#map) | Maps the given fermionic operator to a qubits operator using the mapper's configuration. | | [get\_num\_qubits](#get_num_qubits) | Gets the number of qubits after mapping the given problem into qubits space. | **Attributes:** | Name | Type | Description | | -------- | --------------- | ------------------- | | `method` | `MappingMethod` | The mapping method. | ### map
map(
self: ,
fermion\_op: FermionOperator,
args: Any = (),
kwargs: Any = 
) -> QubitOperator
Maps the given fermionic operator to a qubits operator using the mapper's configuration. **Parameters:** | Name | Type | Description | Default | | ------------ | ----------------- | --------------------- | ---------- | | `self` | \`\` | | *required* | | `fermion_op` | `FermionOperator` | A fermionic operator. | *required* | | `args` | `Any` | | () | | `kwargs` | `Any` | | | **Returns:** * **Type:** `QubitOperator` * The mapped qubits operator. ### get\_num\_qubits
get\_num\_qubits(
self: ,
problem: FermionHamiltonianProblem
) -> int
Gets the number of qubits after mapping the given problem into qubits space. **Parameters:** | Name | Type | Description | Default | | --------- | --------------------------- | -------------------- | ---------- | | `self` | \`\` | | *required* | | `problem` | `FermionHamiltonianProblem` | The fermion problem. | *required* | **Returns:** * **Type:** `int` * The number of qubits. ## FermionHamiltonianProblem Defines an electronic-structure problem using a Fermionic operator and electron count. Can also be constructed from a `MolecularData` object using the `from_molecule` method. **Methods:** | Name | Description | | -------------------------------- | -------------------------------------------------------------- | | [from\_molecule](#from_molecule) | Constructs a `FermionHamiltonianProblem` from a molecule data. | **Attributes:** | Name | Type | Description | | --------------------- | ----------------- | ----------------------------------------------------------------------------------- | | `occupied_alpha` | `list[int]` | | | `virtual_alpha` | `list[int]` | | | `occupied_beta` | `list[int]` | | | `virtual_beta` | `list[int]` | | | `occupied` | `list[int]` | | | `virtual` | `list[int]` | | | `fermion_hamiltonian` | `FermionOperator` | The fermionic hamiltonian of the problem. Assumed to be in the block-spin labeling. | | `n_orbitals` | `int` | Number of spatial orbitals. | | `n_alpha` | `int` | Number of alpha particles. | | `n_beta` | `int` | Number of beta particles. | | `n_particles` | `tuple[int, int]` | Number of alpha and beta particles. | ### from\_molecule
from\_molecule(
cls: ,
molecule: MolecularData,
first\_active\_index: int = 0,
remove\_orbitals: Sequence\[int] | None = None,
op\_compression\_tol: float = 1e-13
) -> FermionHamiltonianProblem
Constructs a `FermionHamiltonianProblem` from a molecule data. **Parameters:** | Name | Type | Description | Default | | -------------------- | ----------------------- | ---------------------------------------------------------------- | ---------- | | `cls` | \`\` | | *required* | | `molecule` | `MolecularData` | The molecule data. | *required* | | `first_active_index` | `int` | The first active index, indicates all prior indices are freezed. | 0 | | `remove_orbitals` | `Sequence[int] \| None` | Active indices to be removed. | None | | `op_compression_tol` | `float` | Tolerance for trimming the fermion operator. | 1e-13 | **Returns:** * **Type:** `FermionHamiltonianProblem` * The fermion hamiltonian problem. ### get\_hf\_fermion\_op
get\_hf\_fermion\_op(
problem: FermionHamiltonianProblem
) -> FermionOperator
Constructs a fermion operator that creates the Hartree-Fock reference state in block-spin ordering. **Parameters:** | Name | Type | Description | Default | | --------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `problem` | `FermionHamiltonianProblem` | The fermion problem. The Hartree-Fock fermion operator depends only on the number of spatial orbitals and the number of alpha and beta particles. | *required* | **Returns:** * **Type:** `FermionOperator` * The Hartree-Fock fermion operator. ### get\_hf\_state
get\_hf\_state(
problem: FermionHamiltonianProblem,
mapper: FermionToQubitMapper
) -> list\[bool]
Computes the qubits state after applying the Hartree-Fock operator defined by the given problem and mapper. The Qmod function `prepare_basis_state` can be used on the returned value to allocate and initialize the qubits array. **Parameters:** | Name | Type | Description | Default | | --------- | --------------------------- | ---------------------------------------------------- | ---------- | | `problem` | `FermionHamiltonianProblem` | The fermion problem. | *required* | | `mapper` | `FermionToQubitMapper` | The mapper from fermion operator to qubits operator. | *required* | **Returns:** * **Type:** `list[bool]` * The qubits state, given as a list of boolean values for each qubit. Members: | Name | Description | | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `FermionToQubitMapper` | Mapper between fermionic operators to qubits operators, using one of the supported mapping methods (see `MappingMethod`). | | `FermionHamiltonianProblem` | Defines an electronic-structure problem using a Fermionic operator and electron count. | | `SparsePauliOp` | Represents a collection of sparse Pauli operators. | | `get_ucc_hamiltonians` | Computes the UCC hamiltonians of the given problem in the desired excitations, using the given mapper. | | `get_excitations` | Gets all the possible excitations of the given problem according to the given number of excitations, preserving the particles spin. | ## FermionToQubitMapper Mapper between fermionic operators to qubits operators, using one of the supported mapping methods (see `MappingMethod`). **Methods:** | Name | Description | | ----------------------------------- | ---------------------------------------------------------------------------------------- | | [map](#map) | Maps the given fermionic operator to a qubits operator using the mapper's configuration. | | [get\_num\_qubits](#get_num_qubits) | Gets the number of qubits after mapping the given problem into qubits space. | **Attributes:** | Name | Type | Description | | -------- | --------------- | ------------------- | | `method` | `MappingMethod` | The mapping method. | ### map
map(
self: ,
fermion\_op: FermionOperator,
args: Any = (),
kwargs: Any = 
) -> QubitOperator
Maps the given fermionic operator to a qubits operator using the mapper's configuration. **Parameters:** | Name | Type | Description | Default | | ------------ | ----------------- | --------------------- | ---------- | | `self` | \`\` | | *required* | | `fermion_op` | `FermionOperator` | A fermionic operator. | *required* | | `args` | `Any` | | () | | `kwargs` | `Any` | | | **Returns:** * **Type:** `QubitOperator` * The mapped qubits operator. ### get\_num\_qubits
get\_num\_qubits(
self: ,
problem: FermionHamiltonianProblem
) -> int
Gets the number of qubits after mapping the given problem into qubits space. **Parameters:** | Name | Type | Description | Default | | --------- | --------------------------- | -------------------- | ---------- | | `self` | \`\` | | *required* | | `problem` | `FermionHamiltonianProblem` | The fermion problem. | *required* | **Returns:** * **Type:** `int` * The number of qubits. ## FermionHamiltonianProblem Defines an electronic-structure problem using a Fermionic operator and electron count. Can also be constructed from a `MolecularData` object using the `from_molecule` method. **Methods:** | Name | Description | | -------------------------------- | -------------------------------------------------------------- | | [from\_molecule](#from_molecule) | Constructs a `FermionHamiltonianProblem` from a molecule data. | **Attributes:** | Name | Type | Description | | --------------------- | ----------------- | ----------------------------------------------------------------------------------- | | `occupied_alpha` | `list[int]` | | | `virtual_alpha` | `list[int]` | | | `occupied_beta` | `list[int]` | | | `virtual_beta` | `list[int]` | | | `occupied` | `list[int]` | | | `virtual` | `list[int]` | | | `fermion_hamiltonian` | `FermionOperator` | The fermionic hamiltonian of the problem. Assumed to be in the block-spin labeling. | | `n_orbitals` | `int` | Number of spatial orbitals. | | `n_alpha` | `int` | Number of alpha particles. | | `n_beta` | `int` | Number of beta particles. | | `n_particles` | `tuple[int, int]` | Number of alpha and beta particles. | ### from\_molecule
from\_molecule(
cls: ,
molecule: MolecularData,
first\_active\_index: int = 0,
remove\_orbitals: Sequence\[int] | None = None,
op\_compression\_tol: float = 1e-13
) -> FermionHamiltonianProblem
Constructs a `FermionHamiltonianProblem` from a molecule data. **Parameters:** | Name | Type | Description | Default | | -------------------- | ----------------------- | ---------------------------------------------------------------- | ---------- | | `cls` | \`\` | | *required* | | `molecule` | `MolecularData` | The molecule data. | *required* | | `first_active_index` | `int` | The first active index, indicates all prior indices are freezed. | 0 | | `remove_orbitals` | `Sequence[int] \| None` | Active indices to be removed. | None | | `op_compression_tol` | `float` | Tolerance for trimming the fermion operator. | 1e-13 | **Returns:** * **Type:** `FermionHamiltonianProblem` * The fermion hamiltonian problem. ## SparsePauliOp Represents a collection of sparse Pauli operators. **Methods:** | Name | Description | | --------------------------------------------------------- | ----------- | | [get\_tuples\_representation](#get_tuples_representation) | | **Attributes:** | Name | Type | Description | | ------------ | ------------------------- | ----------------------------------------------------------------------------------------------- | | `terms` | `CArray[SparsePauliTerm]` | The list of chosen sparse Pauli terms, corresponds to a product of them. (See: SparsePauliTerm) | | `num_qubits` | `CInt` | The number of qubits in the Hamiltonian. | ### get\_tuples\_representation
get\_tuples\_representation(
self: ,
reverse\_order: bool
) -> list\[tuple\[str, int | float | complex]]
**Parameters:** | Name | Type | Description | Default | | --------------- | ------ | ----------- | ---------- | | `self` | \`\` | | *required* | | `reverse_order` | `bool` | | *required* | ### get\_ucc\_hamiltonians
get\_ucc\_hamiltonians(
problem: FermionHamiltonianProblem,
mapper: FermionToQubitMapper,
excitations: int | Sequence\[int]
) -> list\[SparsePauliOp]
Computes the UCC hamiltonians of the given problem in the desired excitations, using the given mapper. **Parameters:** | Name | Type | Description | Default | | ------------- | --------------------------- | --------------------------------------------------- | ---------- | | `problem` | `FermionHamiltonianProblem` | The fermion problem. | *required* | | `mapper` | `FermionToQubitMapper` | The mapper from fermion to qubits operators. | *required* | | `excitations` | `int \| Sequence[int]` | A single desired excitation or an excitations list. | *required* | **Returns:** * **Type:** `list[SparsePauliOp]` * The UCC hamiltonians. ### get\_excitations
get\_excitations(
problem: FermionHamiltonianProblem,
num\_excitations: int
) -> set\[tuple\[tuple\[int, ...], tuple\[int, ...]]]
Gets all the possible excitations of the given problem according to the given number of excitations, preserving the particles spin. **Parameters:** | Name | Type | Description | Default | | ----------------- | --------------------------- | ---------------------- | ---------- | | `problem` | `FermionHamiltonianProblem` | The fermion problem. | *required* | | `num_excitations` | `int` | Number of excitations. | *required* | **Returns:** * **Type:** `set[tuple[tuple[int, ...], tuple[int, ...]]]` * A set of all possible excitations, specified as a pair of source and target indices. Members: | Name | Description | | ---------------------- | ----------------------------------------------------------------------------------------------- | | `DFTState` | Post-DFT handle returned to the user, recording the resolved spin mode, functional, and method. | | `EmbeddingCalculator` | User-facing embedding driver, backed by queued backend jobs. | | `EmbeddingConfig` | Embedding-method parameters. | | `MeanFieldData` | Fragment / environment densities, electron counts, and active-MO basis. | | `MoleculeSpec` | User-facing molecular description. | | `QuantumData` | Solver-facing bundle: embedded + physical Hamiltonians and scalars. | | `SpinMode` | Which mean-field treatment the backend should use. | | `ValidationCheck` | Validation diagnostics the embedding pipeline can run. | | `embedding_calculator` | User-facing API for projection-based WF-in-DFT embedding. | | `chemistry_job` | Detached handle for a long-running chemistry backend job. | ## DFTState Post-DFT handle returned to the user, recording the resolved spin mode, functional, and method. Threaded back into later stages. **Attributes:** | Name | Type | Description | | --------------- | ------------------- | ----------- | | `spin_mode` | `SpinMode` | | | `xc_functional` | `str` | | | `method` | `CalculationMethod` | | ## EmbeddingCalculator User-facing embedding driver, backed by queued backend jobs. **Methods:** | Name | Description | | ----------------------------------------------- | ------------------------------------------------------------- | | [run\_dft](#run_dft) | Run the full-system mean field and cache the result. | | [submit\_dft](#submit_dft) | Enqueue the full-system mean field and return a job handle. | | [run\_dft\_embedding](#run_dft_embedding) | Run the full embedding pipeline in one backend call. | | [submit\_dft\_embedding](#submit_dft_embedding) | Enqueue the full embedding pipeline and return a job handle. | | [run\_validations](#run_validations) | Run a batch of post-hoc validations in a single backend call. | **Attributes:** | Name | Type | Description | | --------------------- | ----------------------------- | ----------- | | `spec` | | | | `spin_mode` | | | | `auto_validations` | `tuple[ValidationCheck, ...]` | | | `validation_results` | `ValidationResults` | | | `config` | `EmbeddingConfig \| None` | | | `effective_spin_mode` | `SpinMode` | | ### run\_dft
run\_dft(
self: ,
xc\_functional: str = 'B3LYP',
method: CalculationMethod = CalculationMethod.DFT
) -> DFTState
Run the full-system mean field and cache the result. Blocks until the backend job finishes. For a long run prefer [submit\_dft()](#submit_dft), which returns a handle you can poll later. `method` selects DFT (default, using `xc_functional`) or Hartree-Fock (`xc_functional` is then ignored). **Parameters:** | Name | Type | Description | Default | | --------------- | ------------------- | ----------- | --------------------- | | `self` | \`\` | | *required* | | `xc_functional` | `str` | | 'B3LYP' | | `method` | `CalculationMethod` | | CalculationMethod.DFT | ### submit\_dft
submit\_dft(
self: ,
xc\_functional: str = 'B3LYP',
method: CalculationMethod = CalculationMethod.DFT
) -> ChemistryJob\[DFTState]
Enqueue the full-system mean field and return a job handle. The non-blocking counterpart to [run\_dft()](#run_dft): the DFT runs server-side (it can take hours) while the client is free to exit. Call `.result()` on the returned handle to fetch the [DFTState](#dftstate) once ready -- doing so also caches the state for [run\_dft\_embedding()](#run_dft_embedding), exactly as [run\_dft()](#run_dft) does. **Parameters:** | Name | Type | Description | Default | | --------------- | ------------------- | ----------- | --------------------- | | `self` | \`\` | | *required* | | `xc_functional` | `str` | | 'B3LYP' | | `method` | `CalculationMethod` | | CalculationMethod.DFT | ### run\_dft\_embedding
run\_dft\_embedding(
self: ,
config: EmbeddingConfig
) -> tuple\[MeanFieldData, QuantumData]
Run the full embedding pipeline in one backend call. Blocks until the backend job finishes. For a long run prefer [submit\_dft\_embedding()](#submit_dft_embedding), which returns a handle you can poll later. Reuses the cached `DFTState` from a prior `run_dft` call if present; otherwise the backend runs the full-system DFT first using `config.xc_functional`. Auto-validations registered at construction are computed in the same call and stored on `self.validation_results`. **Parameters:** | Name | Type | Description | Default | | -------- | ----------------- | ----------- | ---------- | | `self` | \`\` | | *required* | | `config` | `EmbeddingConfig` | | *required* | ### submit\_dft\_embedding
submit\_dft\_embedding(
self: ,
config: EmbeddingConfig
) -> ChemistryJob\[tuple\[MeanFieldData, QuantumData]]
Enqueue the full embedding pipeline and return a job handle. The non-blocking counterpart to [run\_dft\_embedding()](#run_dft_embedding). Call `.result()` on the returned handle to fetch the `(MeanFieldData, QuantumData)` tuple once ready; doing so caches the embedding state for subsequent [run\_validations()](#run_validations) calls and emits any `n_active_virtuals` warning, exactly as the blocking method does. **Parameters:** | Name | Type | Description | Default | | -------- | ----------------- | ----------- | ---------- | | `self` | \`\` | | *required* | | `config` | `EmbeddingConfig` | | *required* | ### run\_validations
run\_validations(
self: ,
checks: Sequence\[ValidationCheck]
) -> ValidationResults
Run a batch of post-hoc validations in a single backend call. **Parameters:** | Name | Type | Description | Default | | -------- | --------------------------- | ----------- | ---------- | | `self` | \`\` | | *required* | | `checks` | `Sequence[ValidationCheck]` | | *required* | ## EmbeddingConfig Embedding-method parameters. **Attributes:** | Name | Type | Description | | ------------------- | ------------------- | ----------- | | `fragment_atoms` | `tuple[int, ...]` | | | `method` | `CalculationMethod` | | | `xc_functional` | `str` | | | `mu` | `float` | | | `w_cut` | `float` | | | `sv_tol` | `float` | | | `n_active_virtuals` | `int \| None` | | | `freeze_core` | `bool` | | ## MeanFieldData Fragment / environment densities, electron counts, and active-MO basis. **Attributes:** | Name | Type | Description | | ---------------- | --------------------------------------------- | ----------- | | `D_A` | `np.ndarray` | | | `D_B` | `np.ndarray` | | | `atom_indices` | `tuple[int, ...]` | | | `n_electrons_A` | `int` | | | `C_active` | `np.ndarray \| tuple[np.ndarray, np.ndarray]` | | | `E_DFT_fragment` | `float` | | | `n_electrons_B` | `int` | | ## MoleculeSpec User-facing molecular description. **Methods:** | Name | Description | | --------------------------------- | -------------------------------------- | | [from\_pdb\_file](#from_pdb_file) | Build a spec from a local `.pdb` file. | | [from\_xyz\_file](#from_xyz_file) | Build a spec from a local `.xyz` file. | **Attributes:** | Name | Type | Description | | -------- | ----- | ----------- | | `atom` | `str` | | | `basis` | `str` | | | `charge` | `int` | | | `spin` | `int` | | | `unit` | `str` | | ### from\_pdb\_file
from\_pdb\_file(
cls: ,
path: str | Path,
basis: str = 'cc-pVDZ',
charge: int = 0,
spin: int = 0,
unit: str = 'Angstrom'
) -> MoleculeSpec
Build a spec from a local `.pdb` file. The file is read on the client; only its contents are sent to the backend (the backend never opens paths). The remaining arguments mirror the `MoleculeSpec` fields. **Parameters:** | Name | Type | Description | Default | | -------- | ------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `path` | `str \| Path` | | *required* | | `basis` | `str` | | 'cc-pVDZ' | | `charge` | `int` | | 0 | | `spin` | `int` | | 0 | | `unit` | `str` | | 'Angstrom' | ### from\_xyz\_file
from\_xyz\_file(
cls: ,
path: str | Path,
basis: str = 'cc-pVDZ',
charge: int = 0,
spin: int = 0,
unit: str = 'Angstrom'
) -> MoleculeSpec
Build a spec from a local `.xyz` file. The file is read on the client; only its contents are sent to the backend (the backend never opens paths). The remaining arguments mirror the `MoleculeSpec` fields. **Parameters:** | Name | Type | Description | Default | | -------- | ------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `path` | `str \| Path` | | *required* | | `basis` | `str` | | 'cc-pVDZ' | | `charge` | `int` | | 0 | | `spin` | `int` | | 0 | | `unit` | `str` | | 'Angstrom' | ## QuantumData Solver-facing bundle: embedded + physical Hamiltonians and scalars. **Attributes:** | Name | Type | Description | | ------------------ | ----------------- | ----------- | | `hamiltonian_emb` | `FermionOperator` | | | `hamiltonian_phys` | `FermionOperator` | | | `n_particles` | `tuple[int, int]` | | | `env_correction` | `float` | | ## SpinMode Which mean-field treatment the backend should use. **Attributes:** | Name | Type | Description | | -------------- | ---------------- | ----------- | | `AUTO` | `'auto'` | | | `RESTRICTED` | `'restricted'` | | | `UNRESTRICTED` | `'unrestricted'` | | ## ValidationCheck Validation diagnostics the embedding pipeline can run. **Attributes:** | Name | Type | Description | | ----------------------- | ------------------------- | ----------- | | `DFT_IN_DFT` | `'dft_in_dft'` | | | `FCI_ACTIVE_SPACE` | `'fci_active_space'` | | | `PROBABILITY_LEAK` | `'probability_leak'` | | | `TRACE_CONSERVATION` | `'trace_conservation'` | | | `GEOMETRY_PERTURBATION` | `'geometry_perturbation'` | | ### embedding\_calculator User-facing API for projection-based WF-in-DFT embedding. Mirrors the local prototype: users build a [MoleculeSpec](#moleculespec) and an [EmbeddingConfig](#embeddingconfig), then drive the pipeline through [EmbeddingCalculator](#embeddingcalculator). Under the hood every stage is a queued backend job (the heavy pyscf / openfermion chemistry runs server-side and never ships to the client). The calculator exchanges the serializable wire models defined in `classiq.interface.applications.chemistry.embedding` with the backend and rehydrates the results into the dataclasses below. The embedded / physical Hamiltonians come back as real `openfermion.FermionOperator` objects, ready to feed into a quantum solver. **Methods:** | Name | Description | | ----------- | ----------- | | [run](#run) | | **Attributes:** | Name | Type | Description | | ------------------- | ---- | ----------- | | `ValidationResults` | | | #### run
run(
coro: Awaitable\[T]
) -> T
**Parameters:** | Name | Type | Description | Default | | ------ | -------------- | ----------- | ---------- | | `coro` | `Awaitable[T]` | | *required* | #### embedding\_calculator.ArrayModel A numpy array flattened to `(shape, data)` for JSON transport. Real-valued only (every array crossing the embedding boundary -- densities, MO coefficients, overlaps -- is real). Rebuild with `np.asarray(data).reshape(shape)`. **Attributes:** | Name | Type | Description | | ------- | ---------------------- | ----------- | | `shape` | `list[NonNegativeInt]` | | | `data` | `list[float]` | | #### embedding\_calculator.CalculationMethod Electronic-structure method for the mean-field stage. `DFT` runs Kohn-Sham with the configured `xc_functional`; `HF` runs Hartree-Fock (realized by setting the PySCF SCF object's `xc` to `"HF"`, which makes `RKS`/`UKS` reproduce `RHF`/`UHF` exactly). `xc_functional` is ignored when `method` is `HF`. **Attributes:** | Name | Type | Description | | ----- | ------- | ----------- | | `DFT` | `'dft'` | | | `HF` | `'hf'` | | #### embedding\_calculator.DFTStateModel Serializable `DFTState` -- the serialized `Mole` + SCF bundle. **Attributes:** | Name | Type | Description | | --------------- | ------------------- | ----------- | | `mol_dumps` | `str` | | | `scf` | `ScfPayload` | | | `spin_mode` | `SpinMode` | | | `xc_functional` | `str` | | | `method` | `CalculationMethod` | | #### embedding\_calculator.EmbeddingConfigModel Serializable `EmbeddingConfig` (all primitives). **Attributes:** | Name | Type | Description | | ------------------- | ---------------------- | ----------- | | `fragment_atoms` | `list[NonNegativeInt]` | | | `method` | `CalculationMethod` | | | `xc_functional` | `str` | | | `mu` | `float` | | | `w_cut` | `float` | | | `sv_tol` | `float` | | | `n_active_virtuals` | `int \| None` | | | `freeze_core` | `bool` | | #### embedding\_calculator.FermionOperatorModel Serializable `openfermion.FermionOperator` (its term -> coefficient map). **Attributes:** | Name | Type | Description | | ------- | ------------------- | ----------- | | `terms` | `list[FermionTerm]` | | #### embedding\_calculator.MeanFieldDataModel Serializable `MeanFieldData`. `c_active` holds one `ArrayModel` for the restricted backend or two (alpha, beta) for the unrestricted backend; `c_active_is_tuple` records which so it can be rebuilt to the right shape. **Attributes:** | Name | Type | Description | | ------------------- | ------------------ | ----------- | | `atom_indices` | `list[int]` | | | `d_a` | `ArrayModel` | | | `n_electrons_a` | `int` | | | `c_active` | `list[ArrayModel]` | | | `c_active_is_tuple` | `bool` | | | `e_dft_fragment` | `float` | | | `d_b` | `ArrayModel` | | | `n_electrons_b` | `int` | | #### embedding\_calculator.MoleculeSpecModel Serializable `MoleculeSpec`. Exactly one of `atom` or `mol_dumps` is populated. `mol_dumps` carries a pre-built `pyscf.gto.Mole` serialized via `Mole.dumps()`. **Attributes:** | Name | Type | Description | | ----------- | ------------- | ----------- | | `atom` | `str \| None` | | | `basis` | `str` | | | `charge` | `int` | | | `spin` | `int` | | | `unit` | `str` | | | `mol_dumps` | `str \| None` | | #### embedding\_calculator.QuantumDataModel Serializable `QuantumData` -- the two fermion Hamiltonians + scalars. **Attributes:** | Name | Type | Description | | ------------------ | ---------------------- | ----------- | | `hamiltonian_emb` | `FermionOperatorModel` | | | `hamiltonian_phys` | `FermionOperatorModel` | | | `n_particles` | `tuple[int, int]` | | | `env_correction` | `float` | | #### embedding\_calculator.SpinMode Which mean-field treatment the backend should use. **Attributes:** | Name | Type | Description | | -------------- | ---------------- | ----------- | | `AUTO` | `'auto'` | | | `RESTRICTED` | `'restricted'` | | | `UNRESTRICTED` | `'unrestricted'` | | #### embedding\_calculator.ValidationCheck Validation diagnostics the embedding pipeline can run. **Attributes:** | Name | Type | Description | | ----------------------- | ------------------------- | ----------- | | `DFT_IN_DFT` | `'dft_in_dft'` | | | `FCI_ACTIVE_SPACE` | `'fci_active_space'` | | | `PROBABILITY_LEAK` | `'probability_leak'` | | | `TRACE_CONSERVATION` | `'trace_conservation'` | | | `GEOMETRY_PERTURBATION` | `'geometry_perturbation'` | | #### embedding\_calculator.ValidationResultModel One validation check's outcome: a pass/fail flag plus a JSON-safe info dict. **Attributes:** | Name | Type | Description | | -------- | ---------------- | ----------- | | `passed` | `bool` | | | `info` | `dict[str, Any]` | | #### embedding\_calculator.RunDftEmbeddingInput **Attributes:** | Name | Type | Description | | ------------------ | ----------------------- | ----------- | | `spec` | `MoleculeSpecModel` | | | `config` | `EmbeddingConfigModel` | | | `spin_mode` | `SpinMode` | | | `dft_state` | `DFTStateModel \| None` | | | `auto_validations` | `list[ValidationCheck]` | | #### embedding\_calculator.RunDftEmbeddingOutput **Attributes:** | Name | Type | Description | | --------------------------- | ---------------------------------------------- | ----------- | | `dft_state` | `DFTStateModel` | | | `mean_field_data` | `MeanFieldDataModel` | | | `quantum_data` | `QuantumDataModel` | | | `validation_results` | `dict[ValidationCheck, ValidationResultModel]` | | | `n_active_virtuals_warning` | `str \| None` | | #### embedding\_calculator.RunDftInput **Attributes:** | Name | Type | Description | | --------------- | ------------------- | ----------- | | `spec` | `MoleculeSpecModel` | | | `xc_functional` | `str` | | | `spin_mode` | `SpinMode` | | | `method` | `CalculationMethod` | | #### embedding\_calculator.RunDftOutput **Attributes:** | Name | Type | Description | | ----------- | --------------- | ----------- | | `dft_state` | `DFTStateModel` | | #### embedding\_calculator.RunValidationsInput **Attributes:** | Name | Type | Description | | ----------------- | ----------------------- | ----------- | | `dft_state` | `DFTStateModel` | | | `mean_field_data` | `MeanFieldDataModel` | | | `quantum_data` | `QuantumDataModel` | | | `config` | `EmbeddingConfigModel` | | | `checks` | `list[ValidationCheck]` | | #### embedding\_calculator.ApiWrapper **Methods:** | Name | Description | | ---------------------------------------------------------------------------------------- | ----------- | | [call\_get\_benchmark\_classes](#call_get_benchmark_classes) | | | [call\_submit\_benchmark](#call_submit_benchmark) | | | [call\_get\_benchmark\_results](#call_get_benchmark_results) | | | [call\_list\_benchmark\_sessions](#call_list_benchmark_sessions) | | | [call\_cancel\_benchmark\_session](#call_cancel_benchmark_session) | | | [call\_generation\_task](#call_generation_task) | | | [call\_transpilation\_task](#call_transpilation_task) | | | [call\_assign\_parameters\_task](#call_assign_parameters_task) | | | [call\_export\_task](#call_export_task) | | | [call\_transpiled\_circuit\_metrics\_task](#call_transpiled_circuit_metrics_task) | | | [call\_circuit\_metrics\_task](#call_circuit_metrics_task) | | | [call\_qasm\_to\_qmod\_task](#call_qasm_to_qmod_task) | | | [submit\_chemistry\_run\_dft\_task](#submit_chemistry_run_dft_task) | | | [submit\_chemistry\_run\_dft\_embedding\_task](#submit_chemistry_run_dft_embedding_task) | | | [call\_chemistry\_run\_dft\_task](#call_chemistry_run_dft_task) | | | [call\_chemistry\_run\_dft\_embedding\_task](#call_chemistry_run_dft_embedding_task) | | | [call\_chemistry\_run\_validations\_task](#call_chemistry_run_validations_task) | | | [call\_get\_visual\_model](#call_get_visual_model) | | | [call\_visualization\_task](#call_visualization_task) | | | [call\_create\_execution\_session](#call_create_execution_session) | | | [call\_create\_openqasm\_execution\_session](#call_create_openqasm_execution_session) | | | [call\_close\_execution\_session](#call_close_execution_session) | | | [call\_create\_session\_job](#call_create_session_job) | | | [call\_convert\_quantum\_program](#call_convert_quantum_program) | | | [call\_execute\_execution\_input](#call_execute_execution_input) | | | [call\_estimate\_sample\_cost](#call_estimate_sample_cost) | | | [call\_get\_execution\_job\_details](#call_get_execution_job_details) | | | [call\_get\_execution\_job\_result](#call_get_execution_job_result) | | | [call\_get\_submitted\_circuits](#call_get_submitted_circuits) | | | [call\_patch\_execution\_job](#call_patch_execution_job) | | | [call\_cancel\_execution\_job](#call_cancel_execution_job) | | | [call\_query\_execution\_jobs](#call_query_execution_jobs) | | | [call\_query\_synthesis\_actions](#call_query_synthesis_actions) | | | [call\_analysis\_task](#call_analysis_task) | | | [get\_generated\_circuit\_from\_qasm](#get_generated_circuit_from_qasm) | | | [get\_analyzer\_app\_data](#get_analyzer_app_data) | | | [call\_rb\_analysis\_task](#call_rb_analysis_task) | | | [call\_hardware\_connectivity\_task](#call_hardware_connectivity_task) | | | [call\_table\_graphs\_task](#call_table_graphs_task) | | | [call\_available\_devices\_task](#call_available_devices_task) | | | [call\_get\_all\_hardware\_devices](#call_get_all_hardware_devices) | | | [call\_get\_all\_budgets](#call_get_all_budgets) | | | [call\_set\_budget\_limit](#call_set_budget_limit) | | | [call\_clear\_budget\_limit](#call_clear_budget_limit) | | | [call\_initialize\_logical\_noise](#call_initialize_logical_noise) | | | [call\_get\_logical\_noise](#call_get_logical_noise) | | | [call\_remove\_logical\_noise](#call_remove_logical_noise) | | ##### call\_get\_benchmark\_classes
call\_get\_benchmark\_classes(
cls:
) -> dict\[BenchmarkClass, BenchmarkClassMetadata]
**Parameters:** | Name | Type | Description | Default | | ----- | ---- | ----------- | ---------- | | `cls` | \`\` | | *required* | ##### call\_submit\_benchmark
call\_submit\_benchmark(
cls: ,
request: BenchmarkRequest,
http\_client: httpx.AsyncClient | None = None
) -> BenchmarkSession
**Parameters:** | Name | Type | Description | Default | | ------------- | --------------------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `request` | `BenchmarkRequest` | | *required* | | `http_client` | `httpx.AsyncClient \| None` | | None | ##### call\_get\_benchmark\_results
call\_get\_benchmark\_results(
cls: ,
session: BenchmarkSession,
http\_client: httpx.AsyncClient | None = None
) -> BenchmarkSession
**Parameters:** | Name | Type | Description | Default | | ------------- | --------------------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `session` | `BenchmarkSession` | | *required* | | `http_client` | `httpx.AsyncClient \| None` | | None | ##### call\_list\_benchmark\_sessions
call\_list\_benchmark\_sessions(
cls: ,
http\_client: httpx.AsyncClient | None = None
) -> list\[BenchmarkSession]
**Parameters:** | Name | Type | Description | Default | | ------------- | --------------------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `http_client` | `httpx.AsyncClient \| None` | | None | ##### call\_cancel\_benchmark\_session
call\_cancel\_benchmark\_session(
cls: ,
session\_id: str,
http\_client: httpx.AsyncClient | None = None
) -> None
**Parameters:** | Name | Type | Description | Default | | ------------- | --------------------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `session_id` | `str` | | *required* | | `http_client` | `httpx.AsyncClient \| None` | | None | ##### call\_generation\_task
call\_generation\_task(
cls: ,
model: Model,
http\_client: httpx.AsyncClient | None = None
) -> generator\_result.QuantumProgram
**Parameters:** | Name | Type | Description | Default | | ------------- | --------------------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `model` | `Model` | | *required* | | `http_client` | `httpx.AsyncClient \| None` | | None | ##### call\_transpilation\_task
call\_transpilation\_task(
cls: ,
params: TranspilationParams,
http\_client: httpx.AsyncClient | None = None
) -> generator\_result.QuantumProgram
**Parameters:** | Name | Type | Description | Default | | ------------- | --------------------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `params` | `TranspilationParams` | | *required* | | `http_client` | `httpx.AsyncClient \| None` | | None | ##### call\_assign\_parameters\_task
call\_assign\_parameters\_task(
cls: ,
params: ParameterAssignmentsParams,
http\_client: httpx.AsyncClient | None = None
) -> generator\_result.QuantumProgram
**Parameters:** | Name | Type | Description | Default | | ------------- | ---------------------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `params` | `ParameterAssignmentsParams` | | *required* | | `http_client` | `httpx.AsyncClient \| None` | | None | ##### call\_export\_task
call\_export\_task(
cls: ,
params: ExportParams,
http\_client: httpx.AsyncClient | None = None
) -> TargetCode
**Parameters:** | Name | Type | Description | Default | | ------------- | --------------------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `params` | `ExportParams` | | *required* | | `http_client` | `httpx.AsyncClient \| None` | | None | ##### call\_transpiled\_circuit\_metrics\_task
call\_transpiled\_circuit\_metrics\_task(
cls: ,
program: generator\_result.QuantumProgram,
http\_client: httpx.AsyncClient | None = None
) -> ProgramData
**Parameters:** | Name | Type | Description | Default | | ------------- | --------------------------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `program` | `generator_result.QuantumProgram` | | *required* | | `http_client` | `httpx.AsyncClient \| None` | | None | ##### call\_circuit\_metrics\_task
call\_circuit\_metrics\_task(
cls: ,
program: generator\_result.QuantumProgram,
http\_client: httpx.AsyncClient | None = None
) -> CircuitMetrics
**Parameters:** | Name | Type | Description | Default | | ------------- | --------------------------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `program` | `generator_result.QuantumProgram` | | *required* | | `http_client` | `httpx.AsyncClient \| None` | | None | ##### call\_qasm\_to\_qmod\_task
call\_qasm\_to\_qmod\_task(
cls: ,
params: QasmToQmodParams,
http\_client: httpx.AsyncClient | None = None
) -> QmodCode
**Parameters:** | Name | Type | Description | Default | | ------------- | --------------------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `params` | `QasmToQmodParams` | | *required* | | `http_client` | `httpx.AsyncClient \| None` | | None | ##### submit\_chemistry\_run\_dft\_task
submit\_chemistry\_run\_dft\_task(
cls: ,
params: RunDftInput,
http\_client: httpx.AsyncClient | None = None
) -> JobID
**Parameters:** | Name | Type | Description | Default | | ------------- | --------------------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `params` | `RunDftInput` | | *required* | | `http_client` | `httpx.AsyncClient \| None` | | None | ##### submit\_chemistry\_run\_dft\_embedding\_task
submit\_chemistry\_run\_dft\_embedding\_task(
cls: ,
params: RunDftEmbeddingInput,
http\_client: httpx.AsyncClient | None = None
) -> JobID
**Parameters:** | Name | Type | Description | Default | | ------------- | --------------------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `params` | `RunDftEmbeddingInput` | | *required* | | `http_client` | `httpx.AsyncClient \| None` | | None | ##### call\_chemistry\_run\_dft\_task
call\_chemistry\_run\_dft\_task(
cls: ,
params: RunDftInput,
http\_client: httpx.AsyncClient | None = None
) -> RunDftOutput
**Parameters:** | Name | Type | Description | Default | | ------------- | --------------------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `params` | `RunDftInput` | | *required* | | `http_client` | `httpx.AsyncClient \| None` | | None | ##### call\_chemistry\_run\_dft\_embedding\_task
call\_chemistry\_run\_dft\_embedding\_task(
cls: ,
params: RunDftEmbeddingInput,
http\_client: httpx.AsyncClient | None = None
) -> RunDftEmbeddingOutput
**Parameters:** | Name | Type | Description | Default | | ------------- | --------------------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `params` | `RunDftEmbeddingInput` | | *required* | | `http_client` | `httpx.AsyncClient \| None` | | None | ##### call\_chemistry\_run\_validations\_task
call\_chemistry\_run\_validations\_task(
cls: ,
params: RunValidationsInput,
http\_client: httpx.AsyncClient | None = None
) -> RunValidationsOutput
**Parameters:** | Name | Type | Description | Default | | ------------- | --------------------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `params` | `RunValidationsInput` | | *required* | | `http_client` | `httpx.AsyncClient \| None` | | None | ##### call\_get\_visual\_model
call\_get\_visual\_model(
cls: ,
program\_id: str,
http\_client: httpx.AsyncClient | None = None
) -> ProgramVisualModel
**Parameters:** | Name | Type | Description | Default | | ------------- | --------------------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `program_id` | `str` | | *required* | | `http_client` | `httpx.AsyncClient \| None` | | None | ##### call\_visualization\_task
call\_visualization\_task(
cls: ,
circuit: generator\_result.QuantumProgram,
http\_client: httpx.AsyncClient | None = None
) -> ProgramVisualModel
**Parameters:** | Name | Type | Description | Default | | ------------- | --------------------------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `circuit` | `generator_result.QuantumProgram` | | *required* | | `http_client` | `httpx.AsyncClient \| None` | | None | ##### call\_create\_execution\_session
call\_create\_execution\_session(
cls: ,
circuit: generator\_result.QuantumProgram,
http\_client: httpx.AsyncClient | None = None
) -> str
**Parameters:** | Name | Type | Description | Default | | ------------- | --------------------------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `circuit` | `generator_result.QuantumProgram` | | *required* | | `http_client` | `httpx.AsyncClient \| None` | | None | ##### call\_create\_openqasm\_execution\_session
call\_create\_openqasm\_execution\_session(
cls: ,
qasm: str,
execution\_preferences: ExecutionPreferences,
http\_client: httpx.AsyncClient | None = None
) -> str
**Parameters:** | Name | Type | Description | Default | | ----------------------- | --------------------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `qasm` | `str` | | *required* | | `execution_preferences` | `ExecutionPreferences` | | *required* | | `http_client` | `httpx.AsyncClient \| None` | | None | ##### call\_close\_execution\_session
call\_close\_execution\_session(
cls: ,
session\_id: str,
http\_client: httpx.AsyncClient | None = None
) -> None
**Parameters:** | Name | Type | Description | Default | | ------------- | --------------------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `session_id` | `str` | | *required* | | `http_client` | `httpx.AsyncClient \| None` | | None | ##### call\_create\_session\_job
call\_create\_session\_job(
cls: ,
session\_id: str,
primitives\_input: PrimitivesInput,
http\_client: httpx.AsyncClient | None = None
) -> execution\_request.ExecutionJobDetails
**Parameters:** | Name | Type | Description | Default | | ------------------ | --------------------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `session_id` | `str` | | *required* | | `primitives_input` | `PrimitivesInput` | | *required* | | `http_client` | `httpx.AsyncClient \| None` | | None | ##### call\_convert\_quantum\_program
call\_convert\_quantum\_program(
cls: ,
circuit: generator\_result.QuantumProgram,
http\_client: httpx.AsyncClient | None = None
) -> dict
**Parameters:** | Name | Type | Description | Default | | ------------- | --------------------------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `circuit` | `generator_result.QuantumProgram` | | *required* | | `http_client` | `httpx.AsyncClient \| None` | | None | ##### call\_execute\_execution\_input
call\_execute\_execution\_input(
cls: ,
execution\_input: dict,
http\_client: httpx.AsyncClient | None = None
) -> execution\_request.ExecutionJobDetails
**Parameters:** | Name | Type | Description | Default | | ----------------- | --------------------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `execution_input` | `dict` | | *required* | | `http_client` | `httpx.AsyncClient \| None` | | None | ##### call\_estimate\_sample\_cost
call\_estimate\_sample\_cost(
cls: ,
execution\_input: dict,
batch\_params: list\[dict] | None = None,
http\_client: httpx.AsyncClient | None = None
) -> dict
**Parameters:** | Name | Type | Description | Default | | ----------------- | --------------------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `execution_input` | `dict` | | *required* | | `batch_params` | `list[dict] \| None` | | None | | `http_client` | `httpx.AsyncClient \| None` | | None | ##### call\_get\_execution\_job\_details
call\_get\_execution\_job\_details(
cls: ,
job\_id: JobID,
http\_client: httpx.AsyncClient | None = None
) -> execution\_request.ExecutionJobDetails
**Parameters:** | Name | Type | Description | Default | | ------------- | --------------------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `job_id` | `JobID` | | *required* | | `http_client` | `httpx.AsyncClient \| None` | | None | ##### call\_get\_execution\_job\_result
call\_get\_execution\_job\_result(
cls: ,
job\_id: JobID,
http\_client: httpx.AsyncClient | None = None
) -> classiq.interface.executor.execution\_result.ExecuteGeneratedCircuitResults
**Parameters:** | Name | Type | Description | Default | | ------------- | --------------------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `job_id` | `JobID` | | *required* | | `http_client` | `httpx.AsyncClient \| None` | | None | ##### call\_get\_submitted\_circuits
call\_get\_submitted\_circuits(
cls: ,
job\_id: JobID,
http\_client: httpx.AsyncClient | None = None
) -> execution\_request.SubmittedCircuitsResponse
**Parameters:** | Name | Type | Description | Default | | ------------- | --------------------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `job_id` | `JobID` | | *required* | | `http_client` | `httpx.AsyncClient \| None` | | None | ##### call\_patch\_execution\_job
call\_patch\_execution\_job(
cls: ,
job\_id: JobID,
name: str,
http\_client: httpx.AsyncClient | None = None
) -> execution\_request.ExecutionJobDetails
**Parameters:** | Name | Type | Description | Default | | ------------- | --------------------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `job_id` | `JobID` | | *required* | | `name` | `str` | | *required* | | `http_client` | `httpx.AsyncClient \| None` | | None | ##### call\_cancel\_execution\_job
call\_cancel\_execution\_job(
cls: ,
job\_id: JobID,
http\_client: httpx.AsyncClient | None = None
) -> None
**Parameters:** | Name | Type | Description | Default | | ------------- | --------------------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `job_id` | `JobID` | | *required* | | `http_client` | `httpx.AsyncClient \| None` | | None | ##### call\_query\_execution\_jobs
call\_query\_execution\_jobs(
cls: ,
offset: int,
limit: int,
http\_client: httpx.AsyncClient | None = None,
extra\_query\_params: Any = 
) -> execution\_request.ExecutionJobsQueryResults
**Parameters:** | Name | Type | Description | Default | | -------------------- | --------------------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `offset` | `int` | | *required* | | `limit` | `int` | | *required* | | `http_client` | `httpx.AsyncClient \| None` | | None | | `extra_query_params` | `Any` | | | ##### call\_query\_synthesis\_actions
call\_query\_synthesis\_actions(
cls: ,
offset: int,
limit: int,
http\_client: httpx.AsyncClient | None = None,
extra\_query\_params: Any | None = 
) -> SynthesisActionsQueryResults
**Parameters:** | Name | Type | Description | Default | | -------------------- | --------------------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `offset` | `int` | | *required* | | `limit` | `int` | | *required* | | `http_client` | `httpx.AsyncClient \| None` | | None | | `extra_query_params` | `Any \| None` | | | ##### call\_analysis\_task
call\_analysis\_task(
cls: ,
params: analysis\_params.AnalysisParams,
http\_client: httpx.AsyncClient | None = None
) -> analysis\_result.Analysis
**Parameters:** | Name | Type | Description | Default | | ------------- | -------------------------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `params` | `analysis_params.AnalysisParams` | | *required* | | `http_client` | `httpx.AsyncClient \| None` | | None | ##### get\_generated\_circuit\_from\_qasm
get\_generated\_circuit\_from\_qasm(
cls: ,
params: analysis\_result.QasmCode,
http\_client: httpx.AsyncClient | None = None
) -> generator\_result.QuantumProgram
**Parameters:** | Name | Type | Description | Default | | ------------- | --------------------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `params` | `analysis_result.QasmCode` | | *required* | | `http_client` | `httpx.AsyncClient \| None` | | None | ##### get\_analyzer\_app\_data
get\_analyzer\_app\_data(
cls: ,
params: analysis\_result.DataID,
http\_client: httpx.AsyncClient | None = None
) -> generator\_result.QuantumProgram
**Parameters:** | Name | Type | Description | Default | | ------------- | --------------------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `params` | `analysis_result.DataID` | | *required* | | `http_client` | `httpx.AsyncClient \| None` | | None | ##### call\_rb\_analysis\_task
call\_rb\_analysis\_task(
cls: ,
params: AnalysisRBParams,
http\_client: httpx.AsyncClient | None = None
) -> analysis\_result.RbResults
**Parameters:** | Name | Type | Description | Default | | ------------- | --------------------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `params` | `AnalysisRBParams` | | *required* | | `http_client` | `httpx.AsyncClient \| None` | | None | ##### call\_hardware\_connectivity\_task
call\_hardware\_connectivity\_task(
cls: ,
params: analysis\_params.AnalysisHardwareParams,
http\_client: httpx.AsyncClient | None = None
) -> analysis\_result.GraphResult
**Parameters:** | Name | Type | Description | Default | | ------------- | ---------------------------------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `params` | `analysis_params.AnalysisHardwareParams` | | *required* | | `http_client` | `httpx.AsyncClient \| None` | | None | ##### call\_table\_graphs\_task
call\_table\_graphs\_task(
cls: ,
params: analysis\_params.AnalysisHardwareListParams,
http\_client: httpx.AsyncClient | None = None
) -> analysis\_result.GraphResult
**Parameters:** | Name | Type | Description | Default | | ------------- | -------------------------------------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `params` | `analysis_params.AnalysisHardwareListParams` | | *required* | | `http_client` | `httpx.AsyncClient \| None` | | None | ##### call\_available\_devices\_task
call\_available\_devices\_task(
cls: ,
params: analysis\_params.AnalysisOptionalDevicesParams,
http\_client: httpx.AsyncClient | None = None
) -> analysis\_result.DevicesResult
**Parameters:** | Name | Type | Description | Default | | ------------- | ----------------------------------------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `params` | `analysis_params.AnalysisOptionalDevicesParams` | | *required* | | `http_client` | `httpx.AsyncClient \| None` | | None | ##### call\_get\_all\_hardware\_devices
call\_get\_all\_hardware\_devices(
cls: ,
http\_client: httpx.AsyncClient | None = None
) -> list\[HardwareInformation]
**Parameters:** | Name | Type | Description | Default | | ------------- | --------------------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `http_client` | `httpx.AsyncClient \| None` | | None | ##### call\_get\_all\_budgets
call\_get\_all\_budgets(
cls:
) -> list\[UserBudget]
**Parameters:** | Name | Type | Description | Default | | ----- | ---- | ----------- | ---------- | | `cls` | \`\` | | *required* | ##### call\_set\_budget\_limit
call\_set\_budget\_limit(
cls: ,
provider: str,
budget\_limit: float
) -> UserBudget
**Parameters:** | Name | Type | Description | Default | | -------------- | ------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `provider` | `str` | | *required* | | `budget_limit` | `float` | | *required* | ##### call\_clear\_budget\_limit
call\_clear\_budget\_limit(
cls: ,
provider: str
) -> UserBudget
**Parameters:** | Name | Type | Description | Default | | ---------- | ----- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `provider` | `str` | | *required* | ##### call\_initialize\_logical\_noise
call\_initialize\_logical\_noise(
cls: ,
params: InitializeLogicalNoiseParams
) -> None
**Parameters:** | Name | Type | Description | Default | | -------- | ------------------------------ | ----------- | ---------- | | `cls` | \`\` | | *required* | | `params` | `InitializeLogicalNoiseParams` | | *required* | ##### call\_get\_logical\_noise
call\_get\_logical\_noise(
cls: ,
name: str
) -> LogicalNoiseParameters
**Parameters:** | Name | Type | Description | Default | | ------ | ----- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `name` | `str` | | *required* | ##### call\_remove\_logical\_noise
call\_remove\_logical\_noise(
cls: ,
name: str
) -> None
**Parameters:** | Name | Type | Description | Default | | ------ | ----- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `name` | `str` | | *required* | #### embedding\_calculator.ChemistryJob **Methods:** | Name | Description | | ------------------------------ | --------------------------------------------------------------- | | [from\_id](#from_id) | Reconnect to a previously submitted job by its id. | | [status\_async](#status_async) | | | [result\_async](#result_async) | Wait for the job to finish and return its (transformed) result. | | [cancel\_async](#cancel_async) | | **Attributes:** | Name | Type | Description | | -------- | ----- | ----------- | | `id` | `str` | | | `status` | | | | `result` | | | | `cancel` | | | ##### from\_id
from\_id(
cls: ,
id: str,
job\_route: str,
result\_type: type\[pydantic.BaseModel]
) -> ChemistryJob
Reconnect to a previously submitted job by its id. The result is fetchable while the job's result is still retained by the backend (see `MAX_KEEP_RESULT`); reconnecting much later may fail. **Parameters:** | Name | Type | Description | Default | | ------------- | -------------------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `id` | `str` | | *required* | | `job_route` | `str` | | *required* | | `result_type` | `type[pydantic.BaseModel]` | | *required* | ##### status\_async
status\_async(
self: ,
\_http\_client: httpx.AsyncClient | None = None
) -> JobStatus
**Parameters:** | Name | Type | Description | Default | | -------------- | --------------------------- | ----------- | ---------- | | `self` | \`\` | | *required* | | `_http_client` | `httpx.AsyncClient \| None` | | None | ##### result\_async
result\_async(
self: ,
timeout\_sec: float | None = None,
\_http\_client: httpx.AsyncClient | None = None
) -> ResultT
Wait for the job to finish and return its (transformed) result. Raises `ClassiqAPIError` if the job failed or was cancelled, or if polling exceeds `timeout_sec` (`None` waits indefinitely). **Parameters:** | Name | Type | Description | Default | | -------------- | --------------------------- | ----------- | ---------- | | `self` | \`\` | | *required* | | `timeout_sec` | `float \| None` | | None | | `_http_client` | `httpx.AsyncClient \| None` | | None | ##### cancel\_async
cancel\_async(
self: ,
\_http\_client: httpx.AsyncClient | None = None
) -> None
**Parameters:** | Name | Type | Description | Default | | -------------- | --------------------------- | ----------- | ---------- | | `self` | \`\` | | *required* | | `_http_client` | `httpx.AsyncClient \| None` | | None | #### embedding\_calculator.MoleculeSpec User-facing molecular description. **Methods:** | Name | Description | | --------------------------------- | -------------------------------------- | | [from\_pdb\_file](#from_pdb_file) | Build a spec from a local `.pdb` file. | | [from\_xyz\_file](#from_xyz_file) | Build a spec from a local `.xyz` file. | **Attributes:** | Name | Type | Description | | -------- | ----- | ----------- | | `atom` | `str` | | | `basis` | `str` | | | `charge` | `int` | | | `spin` | `int` | | | `unit` | `str` | | ##### from\_pdb\_file
from\_pdb\_file(
cls: ,
path: str | Path,
basis: str = 'cc-pVDZ',
charge: int = 0,
spin: int = 0,
unit: str = 'Angstrom'
) -> MoleculeSpec
Build a spec from a local `.pdb` file. The file is read on the client; only its contents are sent to the backend (the backend never opens paths). The remaining arguments mirror the `MoleculeSpec` fields. **Parameters:** | Name | Type | Description | Default | | -------- | ------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `path` | `str \| Path` | | *required* | | `basis` | `str` | | 'cc-pVDZ' | | `charge` | `int` | | 0 | | `spin` | `int` | | 0 | | `unit` | `str` | | 'Angstrom' | ##### from\_xyz\_file
from\_xyz\_file(
cls: ,
path: str | Path,
basis: str = 'cc-pVDZ',
charge: int = 0,
spin: int = 0,
unit: str = 'Angstrom'
) -> MoleculeSpec
Build a spec from a local `.xyz` file. The file is read on the client; only its contents are sent to the backend (the backend never opens paths). The remaining arguments mirror the `MoleculeSpec` fields. **Parameters:** | Name | Type | Description | Default | | -------- | ------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `path` | `str \| Path` | | *required* | | `basis` | `str` | | 'cc-pVDZ' | | `charge` | `int` | | 0 | | `spin` | `int` | | 0 | | `unit` | `str` | | 'Angstrom' | #### embedding\_calculator.EmbeddingConfig Embedding-method parameters. **Attributes:** | Name | Type | Description | | ------------------- | ------------------- | ----------- | | `fragment_atoms` | `tuple[int, ...]` | | | `method` | `CalculationMethod` | | | `xc_functional` | `str` | | | `mu` | `float` | | | `w_cut` | `float` | | | `sv_tol` | `float` | | | `n_active_virtuals` | `int \| None` | | | `freeze_core` | `bool` | | #### embedding\_calculator.DFTState Post-DFT handle returned to the user, recording the resolved spin mode, functional, and method. Threaded back into later stages. **Attributes:** | Name | Type | Description | | --------------- | ------------------- | ----------- | | `spin_mode` | `SpinMode` | | | `xc_functional` | `str` | | | `method` | `CalculationMethod` | | #### embedding\_calculator.MeanFieldData Fragment / environment densities, electron counts, and active-MO basis. **Attributes:** | Name | Type | Description | | ---------------- | --------------------------------------------- | ----------- | | `D_A` | `np.ndarray` | | | `D_B` | `np.ndarray` | | | `atom_indices` | `tuple[int, ...]` | | | `n_electrons_A` | `int` | | | `C_active` | `np.ndarray \| tuple[np.ndarray, np.ndarray]` | | | `E_DFT_fragment` | `float` | | | `n_electrons_B` | `int` | | #### embedding\_calculator.QuantumData Solver-facing bundle: embedded + physical Hamiltonians and scalars. **Attributes:** | Name | Type | Description | | ------------------ | ----------------- | ----------- | | `hamiltonian_emb` | `FermionOperator` | | | `hamiltonian_phys` | `FermionOperator` | | | `n_particles` | `tuple[int, int]` | | | `env_correction` | `float` | | #### embedding\_calculator.EmbeddingCalculator User-facing embedding driver, backed by queued backend jobs. **Methods:** | Name | Description | | ----------------------------------------------- | ------------------------------------------------------------- | | [run\_dft](#run_dft) | Run the full-system mean field and cache the result. | | [submit\_dft](#submit_dft) | Enqueue the full-system mean field and return a job handle. | | [run\_dft\_embedding](#run_dft_embedding) | Run the full embedding pipeline in one backend call. | | [submit\_dft\_embedding](#submit_dft_embedding) | Enqueue the full embedding pipeline and return a job handle. | | [run\_validations](#run_validations) | Run a batch of post-hoc validations in a single backend call. | **Attributes:** | Name | Type | Description | | --------------------- | ----------------------------- | ----------- | | `spec` | | | | `spin_mode` | | | | `auto_validations` | `tuple[ValidationCheck, ...]` | | | `validation_results` | `ValidationResults` | | | `config` | `EmbeddingConfig \| None` | | | `effective_spin_mode` | `SpinMode` | | ##### run\_dft
run\_dft(
self: ,
xc\_functional: str = 'B3LYP',
method: CalculationMethod = CalculationMethod.DFT
) -> DFTState
Run the full-system mean field and cache the result. Blocks until the backend job finishes. For a long run prefer [submit\_dft()](#submit_dft), which returns a handle you can poll later. `method` selects DFT (default, using `xc_functional`) or Hartree-Fock (`xc_functional` is then ignored). **Parameters:** | Name | Type | Description | Default | | --------------- | ------------------- | ----------- | --------------------- | | `self` | \`\` | | *required* | | `xc_functional` | `str` | | 'B3LYP' | | `method` | `CalculationMethod` | | CalculationMethod.DFT | ##### submit\_dft
submit\_dft(
self: ,
xc\_functional: str = 'B3LYP',
method: CalculationMethod = CalculationMethod.DFT
) -> ChemistryJob\[DFTState]
Enqueue the full-system mean field and return a job handle. The non-blocking counterpart to [run\_dft()](#run_dft): the DFT runs server-side (it can take hours) while the client is free to exit. Call `.result()` on the returned handle to fetch the [DFTState](#dftstate) once ready -- doing so also caches the state for [run\_dft\_embedding()](#run_dft_embedding), exactly as [run\_dft()](#run_dft) does. **Parameters:** | Name | Type | Description | Default | | --------------- | ------------------- | ----------- | --------------------- | | `self` | \`\` | | *required* | | `xc_functional` | `str` | | 'B3LYP' | | `method` | `CalculationMethod` | | CalculationMethod.DFT | ##### run\_dft\_embedding
run\_dft\_embedding(
self: ,
config: EmbeddingConfig
) -> tuple\[MeanFieldData, QuantumData]
Run the full embedding pipeline in one backend call. Blocks until the backend job finishes. For a long run prefer [submit\_dft\_embedding()](#submit_dft_embedding), which returns a handle you can poll later. Reuses the cached `DFTState` from a prior `run_dft` call if present; otherwise the backend runs the full-system DFT first using `config.xc_functional`. Auto-validations registered at construction are computed in the same call and stored on `self.validation_results`. **Parameters:** | Name | Type | Description | Default | | -------- | ----------------- | ----------- | ---------- | | `self` | \`\` | | *required* | | `config` | `EmbeddingConfig` | | *required* | ##### submit\_dft\_embedding
submit\_dft\_embedding(
self: ,
config: EmbeddingConfig
) -> ChemistryJob\[tuple\[MeanFieldData, QuantumData]]
Enqueue the full embedding pipeline and return a job handle. The non-blocking counterpart to [run\_dft\_embedding()](#run_dft_embedding). Call `.result()` on the returned handle to fetch the `(MeanFieldData, QuantumData)` tuple once ready; doing so caches the embedding state for subsequent [run\_validations()](#run_validations) calls and emits any `n_active_virtuals` warning, exactly as the blocking method does. **Parameters:** | Name | Type | Description | Default | | -------- | ----------------- | ----------- | ---------- | | `self` | \`\` | | *required* | | `config` | `EmbeddingConfig` | | *required* | ##### run\_validations
run\_validations(
self: ,
checks: Sequence\[ValidationCheck]
) -> ValidationResults
Run a batch of post-hoc validations in a single backend call. **Parameters:** | Name | Type | Description | Default | | -------- | --------------------------- | ----------- | ---------- | | `self` | \`\` | | *required* | | `checks` | `Sequence[ValidationCheck]` | | *required* | ### chemistry\_job Detached handle for a long-running chemistry backend job. `EmbeddingCalculator.submit_*` returns one of these instead of blocking: a chemistry DFT run can take hours, so the SDK hands the user a job id and lets them fetch the result later (`result`) -- the job keeps running server-side even if the client process exits. `from_id` reconnects to a job submitted by an earlier process. The handle is generic over the user-facing return type `ResultT`: a `transform` callback maps the parsed wire output (`result_type`) to that type (and, for the calculator, threads the freshly computed state back into the calculator so a later stage can reuse it). Without a transform the parsed wire model is returned as-is, which is what `from_id` does. **Methods:** | Name | Description | | -------------------------------------- | ----------- | | [syncify\_function](#syncify_function) | | **Attributes:** | Name | Type | Description | | ------------ | ---- | ----------- | | `JSONObject` | | | | `WireT` | | | | `ResultT` | | | #### syncify\_function
syncify\_function(
async\_func: Callable\[..., Awaitable\[T]]
) -> Callable\[..., T]
**Parameters:** | Name | Type | Description | Default | | ------------ | ----------------------------- | ----------- | ---------- | | `async_func` | `Callable[..., Awaitable[T]]` | | *required* | #### chemistry\_job.ClassiqAPIError **Attributes:** | Name | Type | Description | | ------------- | ---- | ----------- | | `status_code` | | | #### chemistry\_job.JobDescription **Methods:** | Name | Description | | ------------------------------------------------------------ | ----------- | | [validate\_status\_and\_fields](#validate_status_and_fields) | | **Attributes:** | Name | Type | Description | | ----------------- | ------------- | ----------- | | `status` | `JobStatus` | | | `failure_details` | `str \| None` | | | `result` | `T \| None` | | ##### validate\_status\_and\_fields
validate\_status\_and\_fields(
self:
) -> Self
**Parameters:** | Name | Type | Description | Default | | ------ | ---- | ----------- | ---------- | | `self` | \`\` | | *required* | #### chemistry\_job.JobID **Attributes:** | Name | Type | Description | | -------- | ----- | ----------- | | `job_id` | `str` | | #### chemistry\_job.JobStatus **Methods:** | Name | Description | | ---------------------- | ----------- | | [is\_final](#is_final) | | **Attributes:** | Name | Type | Description | | ------------ | -------------- | ----------- | | `QUEUED` | `'QUEUED'` | | | `RUNNING` | `'RUNNING'` | | | `READY` | `'READY'` | | | `COMPLETED` | `'COMPLETED'` | | | `FAILED` | `'FAILED'` | | | `CANCELLING` | `'CANCELLING'` | | | `CANCELLED` | `'CANCELLED'` | | | `UNKNOWN` | `'UNKNOWN'` | | ##### is\_final
is\_final(
self:
) -> bool
**Parameters:** | Name | Type | Description | Default | | ------ | ---- | ----------- | ---------- | | `self` | \`\` | | *required* | #### chemistry\_job.JobPoller **Methods:** | Name | Description | | ---------------------------------------- | ---------------------------------------------------------------- | | [submit](#submit) | Enqueue a job and return its id without waiting for completion. | | [fetch\_description](#fetch_description) | Read a job's current description with a single GET (no polling). | | [cancel](#cancel) | | | [poll](#poll) | | | [run](#run) | | | [run\_pydantic](#run_pydantic) | | **Attributes:** | Name | Type | Description | | ---------------------- | -------- | ----------- | | `INITIAL_INTERVAL_SEC` | `'0.1'` | | | `INTERVAL_FACTOR` | `'1.5'` | | | `FINAL_INTERVAL_SEC` | `'25'` | | | `DEV_INTERVAL` | `'0.05'` | | ##### submit
submit(
self: ,
body: dict,
http\_client: httpx.AsyncClient | None = None
) -> JobID
Enqueue a job and return its id without waiting for completion. The detached counterpart to `run`: callers hold on to the returned `JobID` and poll for the result later (e.g. via `poll`), so a long-running job survives the client process exiting. **Parameters:** | Name | Type | Description | Default | | ------------- | --------------------------- | ----------- | ---------- | | `self` | \`\` | | *required* | | `body` | `dict` | | *required* | | `http_client` | `httpx.AsyncClient \| None` | | None | ##### fetch\_description
fetch\_description(
self: ,
job\_id: JobID,
http\_client: httpx.AsyncClient | None = None
) -> GeneralJobDescription
Read a job's current description with a single GET (no polling). **Parameters:** | Name | Type | Description | Default | | ------------- | --------------------------- | ----------- | ---------- | | `self` | \`\` | | *required* | | `job_id` | `JobID` | | *required* | | `http_client` | `httpx.AsyncClient \| None` | | None | ##### cancel
cancel(
self: ,
job\_id: JobID,
http\_client: httpx.AsyncClient | None = None
) -> None
**Parameters:** | Name | Type | Description | Default | | ------------- | --------------------------- | ----------- | ---------- | | `self` | \`\` | | *required* | | `job_id` | `JobID` | | *required* | | `http_client` | `httpx.AsyncClient \| None` | | None | ##### poll
poll(
self: ,
job\_id: JobID,
timeout\_sec: float | None,
response\_parser: Callable\[\[JSONObject], T | None] = \_general\_job\_description\_parser,
http\_client: httpx.AsyncClient | None = None
) -> T
**Parameters:** | Name | Type | Description | Default | | ----------------- | ----------------------------------- | ----------- | ----------------------------------- | | `self` | \`\` | | *required* | | `job_id` | `JobID` | | *required* | | `timeout_sec` | `float \| None` | | *required* | | `response_parser` | `Callable[[JSONObject], T \| None]` | | \_general\_job\_description\_parser | | `http_client` | `httpx.AsyncClient \| None` | | None | ##### run
run(
self: ,
body: dict,
timeout\_sec: float | None,
http\_client: httpx.AsyncClient | None = None
) -> GeneralJobDescription
**Parameters:** | Name | Type | Description | Default | | ------------- | --------------------------- | ----------- | ---------- | | `self` | \`\` | | *required* | | `body` | `dict` | | *required* | | `timeout_sec` | `float \| None` | | *required* | | `http_client` | `httpx.AsyncClient \| None` | | None | ##### run\_pydantic
run\_pydantic(
self: ,
model: pydantic.BaseModel,
timeout\_sec: float | None,
http\_client: httpx.AsyncClient | None = None
) -> GeneralJobDescription
**Parameters:** | Name | Type | Description | Default | | ------------- | --------------------------- | ----------- | ---------- | | `self` | \`\` | | *required* | | `model` | `pydantic.BaseModel` | | *required* | | `timeout_sec` | `float \| None` | | *required* | | `http_client` | `httpx.AsyncClient \| None` | | None | #### chemistry\_job.ChemistryJob **Methods:** | Name | Description | | ------------------------------ | --------------------------------------------------------------- | | [from\_id](#from_id) | Reconnect to a previously submitted job by its id. | | [status\_async](#status_async) | | | [result\_async](#result_async) | Wait for the job to finish and return its (transformed) result. | | [cancel\_async](#cancel_async) | | **Attributes:** | Name | Type | Description | | -------- | ----- | ----------- | | `id` | `str` | | | `status` | | | | `result` | | | | `cancel` | | | ##### from\_id
from\_id(
cls: ,
id: str,
job\_route: str,
result\_type: type\[pydantic.BaseModel]
) -> ChemistryJob
Reconnect to a previously submitted job by its id. The result is fetchable while the job's result is still retained by the backend (see `MAX_KEEP_RESULT`); reconnecting much later may fail. **Parameters:** | Name | Type | Description | Default | | ------------- | -------------------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `id` | `str` | | *required* | | `job_route` | `str` | | *required* | | `result_type` | `type[pydantic.BaseModel]` | | *required* | ##### status\_async
status\_async(
self: ,
\_http\_client: httpx.AsyncClient | None = None
) -> JobStatus
**Parameters:** | Name | Type | Description | Default | | -------------- | --------------------------- | ----------- | ---------- | | `self` | \`\` | | *required* | | `_http_client` | `httpx.AsyncClient \| None` | | None | ##### result\_async
result\_async(
self: ,
timeout\_sec: float | None = None,
\_http\_client: httpx.AsyncClient | None = None
) -> ResultT
Wait for the job to finish and return its (transformed) result. Raises `ClassiqAPIError` if the job failed or was cancelled, or if polling exceeds `timeout_sec` (`None` waits indefinitely). **Parameters:** | Name | Type | Description | Default | | -------------- | --------------------------- | ----------- | ---------- | | `self` | \`\` | | *required* | | `timeout_sec` | `float \| None` | | None | | `_http_client` | `httpx.AsyncClient \| None` | | None | ##### cancel\_async
cancel\_async(
self: ,
\_http\_client: httpx.AsyncClient | None = None
) -> None
**Parameters:** | Name | Type | Description | Default | | -------------- | --------------------------- | ----------- | ---------- | | `self` | \`\` | | *required* | | `_http_client` | `httpx.AsyncClient \| None` | | None | # Cudaq Source: https://docs.classiq.io/sdk-reference/cudaq \ Classiq to [CUDA-Q](https://developer.nvidia.com/cuda-q) translation. These functions require the `cudaq` extra (install `classiq[cudaq]`). Note that the `cudaq` extra is only available in Classiq Studio and on any Linux Machine. ## Functions ### qprog\_to\_cudaq\_kernel
qprog\_to\_cudaq\_kernel(
quantum\_program: QuantumProgram,
is\_main\_kernel: bool = True
) -> Union\[cudaq.PyKernel, tuple]
Translates a quantum program into a CUDA-Q kernel. The 'is\_main\_kernel' parameter controls the kind of the returned kernel. If 'is\_main\_kernel' is True, the returned kernel can be used with CUDA-Q functions such as 'cudaq.draw()' and 'cudaq.get\_state()', but it cannot be added to another kernel via \`apply\_call()'. If 'is\_main\_kernel' is False, the reverse holds: The returned kernel cannot be used with CUDA-Q functions but can be added to another kernel. **Parameters:** | Name | Type | Description | Default | | ----------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- | | `quantum_program` | `QuantumProgram` | The quantum program to translate into CUDA-Q kernel. This is the result of the function 'synthesize()'. | *required* | | `is_main_kernel` | `bool` | Whether the kernel is compatible with CUDA-Q functions (`is_main_kernel=True`, the default behavior) or 'apply\_call()' (`is_main_kernel=False`) | True | **Returns:** * **Type:** `Union[cudaq.PyKernel, tuple]` * A CUDA-Q kernel. If the quantum program includes foreach statements, a tuple * containing a CUDA-Q kernel and its foreach arguments will be returned. ### pauli\_operator\_to\_cudaq\_spin\_op
pauli\_operator\_to\_cudaq\_spin\_op(
operator: SparsePauliOp
) -> cudaq.SpinOperator
Transforms Qmod's SparsePauliOp data structure to CUDA-Q's SpinOperator. **Parameters:** | Name | Type | Description | Default | | ---------- | --------------- | ------------------------------ | ---------- | | `operator` | `SparsePauliOp` | The operator to be transformed | *required* | **Returns:** * **Type:** `cudaq.SpinOperator` * The equivalent operator in CUDA-Q's data structure # Execution Source: https://docs.classiq.io/sdk-reference/execution Members: | Name | Description | | ---------------------- | ----------------------------------------------------------------------------------------------------- | | `ProviderConfig` | Provider-specific configuration data for execution, such as API keys and machine-specific parameters. | | `ExecutionPreferences` | Represents the execution settings for running a quantum program. | | `TranspilationOption` | Transpilation optimization level for quantum circuits. | | `ExecutionSession` | A session for executing a quantum program or OpenQASM source text. | | `sample` | Sample a quantum program or OpenQASM circuit. | ## ProviderConfig Provider-specific configuration data for execution, such as API keys and machine-specific parameters. ## ExecutionPreferences Represents the execution settings for running a quantum program. Execution preferences for running a quantum program. For more details, refer to: ExecutionPreferences example: [ExecutionPreferences](https://docs.classiq.io/latest/user-guide/execution/#execution-preferences).. **Attributes:** | Name | Type | Description | | -------------------------------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | `include_zero_amplitude_outputs` | `bool` | | | `amplitude_threshold` | `float` | | | `noise_properties` | `Optional[NoiseProperties]` | Properties defining the noise in the quantum circuit. Defaults to `None`. | | `random_seed` | `int` | The random seed used for the execution. Defaults to a randomly generated seed. | | `backend_preferences` | `BackendPreferencesTypes` | Preferences for the backend used to execute the circuit. Defaults to the Classiq Simulator. | | `num_shots` | `Optional[pydantic.PositiveInt]` | The number of shots (executions) to be performed. | | `transpile_to_hardware` | `TranspilationOption` | Option to transpile the circuit to the hardware's basis gates before execution. Defaults to `TranspilationOption.DECOMPOSE`. | | `job_name` | `Optional[str]` | The name of the job, with a minimum length of 1 character. | ## TranspilationOption Transpilation optimization level for quantum circuits. **Attributes:** | Name | Type | Description | | --------------- | ----------------- | ----------- | | `NONE` | `'none'` | | | `DECOMPOSE` | `'decompose'` | | | `AUTO_OPTIMIZE` | `'auto optimize'` | | | `LIGHT` | `'light'` | | | `MEDIUM` | `'medium'` | | | `INTENSIVE` | `'intensive'` | | | `CUSTOM` | `'custom'` | | ## ExecutionSession A session for executing a quantum program or OpenQASM source text. `ExecutionSession` allows to execute the quantum program with different parameters and operations without the need to re-synthesize the model. The session must be closed in order to ensure resources are properly cleaned up. It's recommended to use `ExecutionSession` as a context manager for this purpose. Alternatively, you can directly use the `close` method. **Methods:** | Name | Description | | ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | [close](#close) | Close the session and clean up its resources. | | [get\_session\_id](#get_session_id) | | | [update\_execution\_preferences](#update_execution_preferences) | Update the execution preferences for the session. | | [sample](#sample) | Samples the quantum program with the given parameters, if any. | | [submit\_sample](#submit_sample) | Initiates an execution job with the `sample` primitive. | | [calculate\_state\_vector](#calculate_state_vector) | Calculate the state vector of the quantum program. | | [submit\_calculate\_state\_vector](#submit_calculate_state_vector) | Initiates an execution job with the `calculate_state_vector` primitive. | | [calculate\_unitary](#calculate_unitary) | Calculate the unitary matrix of the quantum program. | | [submit\_calculate\_unitary](#submit_calculate_unitary) | Initiates an execution job with the `calculate_unitary` primitive. | | [batch\_sample](#batch_sample) | Samples the quantum program multiple times with the given parameters for each iteration. | | [submit\_batch\_sample](#submit_batch_sample) | Initiates an execution job with the `batch_sample` primitive. | | [observe](#observe) | Estimates the expectation value of the given Hamiltonian using the quantum program. | | [submit\_observe](#submit_observe) | Initiates an execution job with the `observe` primitive. | | [estimate](#estimate) | Estimates the expectation value of the given Hamiltonian using the quantum program. | | [submit\_estimate](#submit_estimate) | Initiates an execution job with the `estimate` primitive. | | [batch\_estimate](#batch_estimate) | Estimates the expectation value of the given Hamiltonian multiple times using the quantum program, with the given parameters for each iteration. | | [submit\_batch\_estimate](#submit_batch_estimate) | Initiates an execution job with the `batch_estimate` primitive. | | [variational\_minimize](#variational_minimize) | Variationally minimizes the given cost function using the quantum program. | | [minimize](#minimize) | . | | [submit\_variational\_minimize](#submit_variational_minimize) | Initiates an execution job with the variational minimization primitive. | | [submit\_minimize](#submit_minimize) | . | | [estimate\_cost](#estimate_cost) | Estimates circuit cost using a classical cost function. | | [set\_measured\_state\_filter](#set_measured_state_filter) | When simulating on a statevector simulator, emulate the behavior of postprocessing by discarding amplitudes for which their states are "undesirable". | **Attributes:** | Name | Type | Description | | --------- | ---------------- | -------------------------------------------------------------------------------------------------------------- | | `program` | `QuantumProgram` | The quantum program to execute, or a placeholder when the first constructor argument was OpenQASM source text. | ### close
close(
self:
) -> None
Close the session and clean up its resources. **Parameters:** | Name | Type | Description | Default | | ------ | ---- | ----------- | ---------- | | `self` | \`\` | | *required* | ### get\_session\_id
get\_session\_id(
self:
) -> str
**Parameters:** | Name | Type | Description | Default | | ------ | ---- | ----------- | ---------- | | `self` | \`\` | | *required* | ### update\_execution\_preferences
update\_execution\_preferences(
self: ,
execution\_preferences: ExecutionPreferences | None
) -> None
Update the execution preferences for the session. **Parameters:** | Name | Type | Description | Default | | ----------------------- | ----------------------------------------------------- | ------------------------------------ | ---------- | | `self` | \`\` | | *required* | | `execution_preferences` | [ExecutionPreferences](#executionpreferences) \| None | The execution preferences to update. | *required* | **Returns:** * **Type:** `None` ### sample
sample(
self: ,
parameters: ExecutionParams | list\[ExecutionParams] | None = None,
num\_shots: int | None = None,
run\_via\_classiq: bool | None = None
) -> ExecutionDetails | list\[ExecutionDetails]
Samples the quantum program with the given parameters, if any. **Parameters:** | Name | Type | Description | Default | | ----------------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `self` | \`\` | | *required* | | `parameters` | `ExecutionParams \| list[ExecutionParams] \| None` | A dictionary of parameter values, or a list of dictionaries for batch execution. Each key should be the name of a parameter in the quantum program (parameters of the main function), and the value should be the value to set for that parameter. | None | | `num_shots` | `int \| None` | Number of shots for this call only; overrides the session default when set. | None | | `run_via_classiq` | `bool \| None` | Run via Classiq credentials for this call only; overrides the session default when set. | None | **Returns:** * **Type:** `ExecutionDetails \| list[ExecutionDetails]` * The result of the sampling, or a list of results when * `parameters` is a list. ### submit\_sample
submit\_sample(
self: ,
parameters: ExecutionParams | list\[ExecutionParams] | None = None,
num\_shots: int | None = None,
run\_via\_classiq: bool | None = None
) -> ExecutionJob
Initiates an execution job with the `sample` primitive. This is a non-blocking version of `sample`: it gets the same parameters and initiates the same execution job, but instead of waiting for the result, it returns the job object immediately. **Parameters:** | Name | Type | Description | Default | | ----------------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `self` | \`\` | | *required* | | `parameters` | `ExecutionParams \| list[ExecutionParams] \| None` | A dictionary of parameter values, or a list of dictionaries for batch execution. Each key should be the name of a parameter in the quantum program (parameters of the main function), and the value should be the value to set for that parameter. | None | | `num_shots` | `int \| None` | Number of shots for this call only; overrides the session default when set. | None | | `run_via_classiq` | `bool \| None` | Run via Classiq credentials for this call only; overrides the session default when set. | None | **Returns:** * **Type:** `ExecutionJob` * The execution job. ### calculate\_state\_vector
calculate\_state\_vector(
self: ,
parameters: ExecutionParams | list\[ExecutionParams] | None = None,
filters: dict\[str, Any] | None = None,
amplitude\_threshold: float = 0.0,
verbose: bool = True
) -> DataFrame | list\[DataFrame]
Calculate the state vector of the quantum program. The session must be configured with a Classiq simulator (`"classiq/simulator"`, `"classiq/nvidia_simulator"`) or `"google/cuquantum"`. The corresponding statevector variant is selected automatically; callers do not need to know about the `_statevector` backend names. **Parameters:** | Name | Type | Description | Default | | --------------------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `self` | \`\` | | *required* | | `parameters` | `ExecutionParams \| list[ExecutionParams] \| None` | A dictionary of parameter values, or a list of dictionaries for batch execution. | None | | `filters` | `dict[str, Any] \| None` | Only states where the variables match these values will be included in the state vector. | None | | `amplitude_threshold` | `float` | If provided, only states whose amplitude magnitude is strictly greater than this value will be included in the result. Defaults to 0 (filters exactly zero-amplitude states). | 0.0 | | `verbose` | `bool` | Whether to print the "Submitting state-vector job to..." progress line. Set to `False` to suppress it, for example when calling `calculate_state_vector` repeatedly in a loop. Defaults to `True`. | True | **Returns:** * **Type:** `DataFrame \| list[DataFrame]` * A dataframe containing the state vector, or a list of dataframes when * `parameters` is a list. ### submit\_calculate\_state\_vector
submit\_calculate\_state\_vector(
self: ,
parameters: ExecutionParams | list\[ExecutionParams] | None = None,
filters: dict\[str, Any] | None = None,
amplitude\_threshold: float = 0.0,
verbose: bool = True
) -> ExecutionJob
Initiates an execution job with the `calculate_state_vector` primitive. This is a non-blocking version of [calculate\_state\_vector()](#calculate_state_vector): it gets the same parameters and initiates the same execution job, but instead of waiting for the result, it returns the job object immediately. **Parameters:** | Name | Type | Description | Default | | --------------------- | -------------------------------------------------- | ---------------------------------------------------------- | ---------- | | `self` | \`\` | | *required* | | `parameters` | `ExecutionParams \| list[ExecutionParams] \| None` | See [calculate\_state\_vector()](#calculate_state_vector). | None | | `filters` | `dict[str, Any] \| None` | See [calculate\_state\_vector()](#calculate_state_vector). | None | | `amplitude_threshold` | `float` | See [calculate\_state\_vector()](#calculate_state_vector). | 0.0 | | `verbose` | `bool` | See [calculate\_state\_vector()](#calculate_state_vector). | True | **Returns:** * **Type:** `ExecutionJob` * The execution job. ### calculate\_unitary
calculate\_unitary(
self: ,
parameters: ExecutionParams | list\[ExecutionParams] | None = None,
amplitude\_threshold: float = 0.0
) -> DataFrame | list\[DataFrame]
Calculate the unitary matrix of the quantum program. The session must be configured with a Classiq simulator (`"classiq/simulator"`, `"classiq/nvidia_simulator"`, `"classiq/dgx_simulator"`) or `"google/cuquantum"`. **Parameters:** | Name | Type | Description | Default | | --------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ---------- | | `self` | \`\` | | *required* | | `parameters` | `ExecutionParams \| list[ExecutionParams] \| None` | A dictionary of parameter values, or a list of dictionaries for batch execution. | None | | `amplitude_threshold` | `float` | If provided, matrix elements whose magnitude is below this threshold are zeroed (same cutoff semantics as `calculate_state_vector`). | 0.0 | **Returns:** * **Type:** `DataFrame \| list[DataFrame]` * A dataframe containing the unitary matrix, or a list of dataframes when * `parameters` is a list. ### submit\_calculate\_unitary
submit\_calculate\_unitary(
self: ,
parameters: ExecutionParams | list\[ExecutionParams] | None = None,
amplitude\_threshold: float = 0.0
) -> ExecutionJob
Initiates an execution job with the `calculate_unitary` primitive. Promotes the session backend to a unitary simulator and runs the `sample` primitive on a dedicated unitary session. Program size limits are enforced server-side. **Parameters:** | Name | Type | Description | Default | | --------------------- | -------------------------------------------------- | ----------- | ---------- | | `self` | \`\` | | *required* | | `parameters` | `ExecutionParams \| list[ExecutionParams] \| None` | | None | | `amplitude_threshold` | `float` | | 0.0 | ### batch\_sample
batch\_sample(
self: ,
parameters: list\[ExecutionParams]
) -> list\[ExecutionDetails]
Samples the quantum program multiple times with the given parameters for each iteration. The number of samples is determined by the length of the parameters list. **Deprecated:** Pass a list of parameter dicts to [sample()](#sample) instead. **Parameters:** | Name | Type | Description | Default | | ------------ | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `self` | \`\` | | *required* | | `parameters` | `list[ExecutionParams]` | A list of the parameters for each iteration. Each item is a dictionary where each key should be the name of a parameter in the quantum program (parameters of the main function), and the value should be the value to set for that parameter. | *required* | **Returns:** * **Type:** `list[ExecutionDetails]` * List\[ExecutionDetails]: The results of all the sampling iterations. ### submit\_batch\_sample
submit\_batch\_sample(
self: ,
parameters: list\[ExecutionParams]
) -> ExecutionJob
Initiates an execution job with the `batch_sample` primitive. **Deprecated:** Pass a list of parameter dicts to [submit\_sample()](#submit_sample) instead. **Parameters:** | Name | Type | Description | Default | | ------------ | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `self` | \`\` | | *required* | | `parameters` | `list[ExecutionParams]` | A list of the parameters for each iteration. Each item is a dictionary where each key should be the name of a parameter in the quantum program (parameters of the main function), and the value should be the value to set for that parameter. | *required* | **Returns:** * **Type:** `ExecutionJob` * The execution job. ### observe
observe(
self: ,
hamiltonian: Hamiltonian,
parameters: ExecutionParams | list\[ExecutionParams] | None = None,
num\_shots: int | None = None,
run\_via\_classiq: bool | None = None
) -> EstimationResult | list\[EstimationResult]
Estimates the expectation value of the given Hamiltonian using the quantum program. **Parameters:** | Name | Type | Description | Default | | ----------------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `self` | \`\` | | *required* | | `hamiltonian` | `Hamiltonian` | The Hamiltonian to estimate the expectation value of. | *required* | | `parameters` | `ExecutionParams \| list[ExecutionParams] \| None` | A dictionary of parameter values, or a list of dictionaries for batch execution. Each key should be the name of a parameter in the quantum program (parameters of the main function), and the value should be the value to set for that parameter. | None | | `num_shots` | `int \| None` | Number of shots for this call only; overrides the session default when set. | None | | `run_via_classiq` | `bool \| None` | Run via Classiq credentials for this call only; overrides the session default when set. | None | **Returns:** * **Type:** `EstimationResult \| list[EstimationResult]` * The estimation result, or a list of results when `parameters` * is a list. ### submit\_observe
submit\_observe(
self: ,
hamiltonian: Hamiltonian,
parameters: ExecutionParams | list\[ExecutionParams] | None = None,
num\_shots: int | None = None,
run\_via\_classiq: bool | None = None
) -> ExecutionJob
Initiates an execution job with the `observe` primitive. This is a non-blocking version of [observe()](#observe): it gets the same parameters and initiates the same execution job, but instead of waiting for the result, it returns the job object immediately. **Parameters:** | Name | Type | Description | Default | | ----------------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `self` | \`\` | | *required* | | `hamiltonian` | `Hamiltonian` | The Hamiltonian to estimate the expectation value of. | *required* | | `parameters` | `ExecutionParams \| list[ExecutionParams] \| None` | A dictionary of parameter values, or a list of dictionaries for batch execution. Each key should be the name of a parameter in the quantum program (parameters of the main function), and the value should be the value to set for that parameter. | None | | `num_shots` | `int \| None` | Number of shots for this call only; overrides the session default when set. | None | | `run_via_classiq` | `bool \| None` | Run via Classiq credentials for this call only; overrides the session default when set. | None | **Returns:** * **Type:** `ExecutionJob` * The execution job. ### estimate
estimate(
self: ,
hamiltonian: Hamiltonian,
parameters: ExecutionParams | list\[ExecutionParams] | None = None,
num\_shots: int | None = None,
run\_via\_classiq: bool | None = None
) -> EstimationResult | list\[EstimationResult]
Estimates the expectation value of the given Hamiltonian using the quantum program. **Deprecated:** `estimate` is deprecated and will no longer be supported starting on 2026-06-22. Use [observe()](#observe) instead. **Parameters:** | Name | Type | Description | Default | | ----------------- | -------------------------------------------------- | ----------- | ---------- | | `self` | \`\` | | *required* | | `hamiltonian` | `Hamiltonian` | | *required* | | `parameters` | `ExecutionParams \| list[ExecutionParams] \| None` | | None | | `num_shots` | `int \| None` | | None | | `run_via_classiq` | `bool \| None` | | None | ### submit\_estimate
submit\_estimate(
self: ,
hamiltonian: Hamiltonian,
parameters: ExecutionParams | list\[ExecutionParams] | None = None,
num\_shots: int | None = None,
run\_via\_classiq: bool | None = None
) -> ExecutionJob
Initiates an execution job with the `estimate` primitive. **Deprecated:** `submit_estimate` is deprecated and will no longer be supported starting on 2026-06-22. Use [submit\_observe()](#submit_observe) instead. **Parameters:** | Name | Type | Description | Default | | ----------------- | -------------------------------------------------- | ----------- | ---------- | | `self` | \`\` | | *required* | | `hamiltonian` | `Hamiltonian` | | *required* | | `parameters` | `ExecutionParams \| list[ExecutionParams] \| None` | | None | | `num_shots` | `int \| None` | | None | | `run_via_classiq` | `bool \| None` | | None | ### batch\_estimate
batch\_estimate(
self: ,
hamiltonian: Hamiltonian,
parameters: list\[ExecutionParams]
) -> list\[EstimationResult]
Estimates the expectation value of the given Hamiltonian multiple times using the quantum program, with the given parameters for each iteration. The number of estimations is determined by the length of the parameters list. **Deprecated:** Pass a list of parameter dicts to [observe()](#observe) instead. **Parameters:** | Name | Type | Description | Default | | ------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `self` | \`\` | | *required* | | `hamiltonian` | `Hamiltonian` | The Hamiltonian to estimate the expectation value of. | *required* | | `parameters` | `list[ExecutionParams]` | A list of the parameters for each iteration. Each item is a dictionary where each key should be the name of a parameter in the quantum program (parameters of the main function), and the value should be the value to set for that parameter. | *required* | **Returns:** * **Type:** `list[EstimationResult]` * List\[EstimationResult]: The results of all the estimation iterations. ### submit\_batch\_estimate
submit\_batch\_estimate(
self: ,
hamiltonian: Hamiltonian,
parameters: list\[ExecutionParams]
) -> ExecutionJob
Initiates an execution job with the `batch_estimate` primitive. **Deprecated:** Pass a list of parameter dicts to [submit\_observe()](#submit_observe) instead. **Parameters:** | Name | Type | Description | Default | | ------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `self` | \`\` | | *required* | | `hamiltonian` | `Hamiltonian` | The Hamiltonian to estimate the expectation value of. | *required* | | `parameters` | `list[ExecutionParams]` | A list of the parameters for each iteration. Each item is a dictionary where each key should be the name of a parameter in the quantum program (parameters of the main function), and the value should be the value to set for that parameter. | *required* | **Returns:** * **Type:** `ExecutionJob` * The execution job. ### variational\_minimize
variational\_minimize(
self: ,
cost\_function: Hamiltonian | QmodExpressionCreator,
initial\_params: ExecutionParams,
max\_iteration: int,
quantile: float = 1.0,
tolerance: float | None = None,
hosted: bool = False,
num\_shots: int | None = None,
run\_via\_classiq: bool | None = None
) -> list\[tuple\[float, ExecutionParams]]
Variationally minimizes the given cost function using the quantum program. **Parameters:** | Name | Type | Description | Default | | ----------------- | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `self` | \`\` | | *required* | | `cost_function` | `Hamiltonian \| QmodExpressionCreator` | The cost function to minimize. It can be one of the following: - A quantum cost function defined by a Hamiltonian. - A classical cost function represented as a callable that returns a Qmod expression. The callable should accept `QVar`s as arguments and use names matching the Model outputs. | *required* | | `initial_params` | `ExecutionParams` | The initial parameters for the minimization. Only Models with exactly one execution parameter are supported. This parameter must be of type `CReal` or `CArray`. The dictionary must contain a single key-value pair, where: - The key is the name of the parameter. - The value is either a float or a list of floats. | *required* | | `max_iteration` | `int` | The maximum number of iterations for the minimization. | *required* | | `quantile` | `float` | The quantile to use for cost estimation. | 1.0 | | `tolerance` | `float \| None` | The tolerance for the minimization. | None | | `hosted` | `bool` | When `True` on an IonQ backend, submit a Classiq execution job that runs the optimization loop on IonQ Hosted Hybrid in the backend. Defaults to `False`. | False | | `num_shots` | `int \| None` | Number of shots for this call only; overrides the session default when set. | None | | `run_via_classiq` | `bool \| None` | Run via Classiq credentials for this call only; overrides the session default when set. | None | **Returns:** * **Type:** `list[tuple[float, ExecutionParams]]` * A list of tuples, each containing the estimated cost and the corresponding parameters for that iteration. `cost` is a float, and `parameters` is a dictionary matching the execution parameter format. ### minimize
minimize(
self: ,
cost\_function: Hamiltonian | QmodExpressionCreator,
initial\_params: ExecutionParams,
max\_iteration: int,
quantile: float = 1.0,
tolerance: float | None = None,
num\_shots: int | None = None,
run\_via\_classiq: bool | None = None
) -> list\[tuple\[float, ExecutionParams]]
**Deprecated:** Use [variational\_minimize()](#variational_minimize) instead. This name is kept for backward compatibility and will be removed in a future release. **Parameters:** | Name | Type | Description | Default | | ----------------- | -------------------------------------- | ----------- | ---------- | | `self` | \`\` | | *required* | | `cost_function` | `Hamiltonian \| QmodExpressionCreator` | | *required* | | `initial_params` | `ExecutionParams` | | *required* | | `max_iteration` | `int` | | *required* | | `quantile` | `float` | | 1.0 | | `tolerance` | `float \| None` | | None | | `num_shots` | `int \| None` | | None | | `run_via_classiq` | `bool \| None` | | None | ### submit\_variational\_minimize
submit\_variational\_minimize(
self: ,
cost\_function: Hamiltonian | QmodExpressionCreator,
initial\_params: ExecutionParams,
max\_iteration: int,
quantile: float = 1.0,
tolerance: float | None = None,
hosted: bool = False,
num\_shots: int | None = None,
run\_via\_classiq: bool | None = None
) -> ExecutionJob
Initiates an execution job with the variational minimization primitive. Non-blocking counterpart of [variational\_minimize()](#variational_minimize): same parameters and job, but returns the [ExecutionJob](#executionjob) immediately. **Parameters:** | Name | Type | Description | Default | | ----------------- | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `self` | \`\` | | *required* | | `cost_function` | `Hamiltonian \| QmodExpressionCreator` | The cost function to minimize. It can be one of the following: - A quantum cost function defined by a Hamiltonian. - A classical cost function represented as a callable that returns a Qmod expression. The callable should accept `QVar`s as arguments and use names matching the Model outputs. | *required* | | `initial_params` | `ExecutionParams` | The initial parameters for the minimization. Only Models with exactly one execution parameter are supported. This parameter must be of type `CReal` or `CArray`. The dictionary must contain a single key-value pair, where: - The key is the name of the parameter. - The value is either a float or a list of floats. | *required* | | `max_iteration` | `int` | The maximum number of iterations for the minimization. | *required* | | `quantile` | `float` | The quantile to use for cost estimation. | 1.0 | | `tolerance` | `float \| None` | The tolerance for the minimization. | None | | `hosted` | `bool` | When `True` on an IonQ backend, submit a Classiq execution job that runs the optimization loop on IonQ Hosted Hybrid in the backend. Defaults to `False`. | False | | `num_shots` | `int \| None` | Number of shots for this call only; overrides the session default when set. | None | | `run_via_classiq` | `bool \| None` | Run via Classiq credentials for this call only; overrides the session default when set. | None | **Returns:** * **Type:** `ExecutionJob` * The execution job. When `hosted=True` on an IonQ backend, the backend * worker submits and polls IonQ Hosted Hybrid while this job tracks * progress through the standard Classiq execution API. ### submit\_minimize
submit\_minimize(
self: ,
cost\_function: Hamiltonian | QmodExpressionCreator,
initial\_params: ExecutionParams,
max\_iteration: int,
quantile: float = 1.0,
tolerance: float | None = None,
num\_shots: int | None = None,
run\_via\_classiq: bool | None = None
) -> ExecutionJob
**Deprecated:** Use [submit\_variational\_minimize()](#submit_variational_minimize) instead. This name is kept for backward compatibility and will be removed in a future release. **Parameters:** | Name | Type | Description | Default | | ----------------- | -------------------------------------- | ----------- | ---------- | | `self` | \`\` | | *required* | | `cost_function` | `Hamiltonian \| QmodExpressionCreator` | | *required* | | `initial_params` | `ExecutionParams` | | *required* | | `max_iteration` | `int` | | *required* | | `quantile` | `float` | | 1.0 | | `tolerance` | `float \| None` | | None | | `num_shots` | `int \| None` | | None | | `run_via_classiq` | `bool \| None` | | None | ### estimate\_cost
estimate\_cost(
self: ,
cost\_func: Callable\[\[ParsedState], float],
parameters: ExecutionParams | None = None,
quantile: float = 1.0,
num\_shots: int | None = None,
run\_via\_classiq: bool | None = None
) -> float
Estimates circuit cost using a classical cost function. **Parameters:** | Name | Type | Description | Default | | ----------------- | -------------------------------- | --------------------------------------------------------------------------------------- | ---------- | | `self` | \`\` | | *required* | | `cost_func` | `Callable[[ParsedState], float]` | classical circuit sample cost function | *required* | | `parameters` | `ExecutionParams \| None` | execution parameters sent to 'sample' | None | | `quantile` | `float` | drop cost values outside the specified quantile | 1.0 | | `num_shots` | `int \| None` | Number of shots for this call only; overrides the session default when set. | None | | `run_via_classiq` | `bool \| None` | Run via Classiq credentials for this call only; overrides the session default when set. | None | **Returns:** * **Type:** `float` * cost estimation ### set\_measured\_state\_filter
set\_measured\_state\_filter(
self: ,
output\_name: str,
condition: Callable
) -> None
When simulating on a statevector simulator, emulate the behavior of postprocessing by discarding amplitudes for which their states are "undesirable". **Parameters:** | Name | Type | Description | Default | | ------------- | ---------- | --------------------------------------------------------------------- | ---------- | | `self` | \`\` | | *required* | | `output_name` | `str` | The name of the register to filter | *required* | | `condition` | `Callable` | Filter out values of the statevector for which this callable is False | *required* | ### sample
sample(
qprog: QuantumProgram | str,
backend: str | None = None,
parameters: ExecutionParams | list\[ExecutionParams] | None = None,
config: dict\[str, Any] | ProviderConfig | None = None,
num\_shots: int | None = None,
random\_seed: int | None = None,
transpilation\_option: TranspilationOption = TranspilationOption.DECOMPOSE,
run\_via\_classiq: bool = False,
verbose: bool = True
) -> DataFrame | list\[DataFrame]
Sample a quantum program or OpenQASM circuit. **Parameters:** | Name | Type | Description | Default | | ---------------------- | ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- | | `qprog` | `QuantumProgram \| str` | A synthesized `QuantumProgram`, or OpenQASM **2.0** / **3.0** source as a single string (for example output of `qiskit.qasm2.dumps` or `qiskit.qasm3.dumps`). | *required* | | `backend` | `str \| None` | The hardware or simulator on which to run the quantum program. Use `"simulator"` for Classiq's default simulator, or specify a backend as `"provider/device_id"`. Use the `get_backend_details` function to see supported devices. | None | | `parameters` | `ExecutionParams \| list[ExecutionParams] \| None` | A dictionary of parameter values, or a list of dictionaries for batch execution. Each key should be the name of a parameter in the quantum program (parameters of the `main` function), and the value should be the value to set for that parameter. **Not supported** when `qprog` is an OpenQASM string (use a `QuantumProgram` or bind values in QASM before calling `sample`). | None | | `config` | dict\[str, Any] \| [ProviderConfig](#providerconfig) \| None | Provider-specific configuration, such as API keys. For full details, see the SDK reference under Providers. | None | | `num_shots` | `int \| None` | The number of times to sample. | None | | `random_seed` | `int \| None` | The random seed used for transpilation and simulation. | None | | `transpilation_option` | `TranspilationOption` | Advanced configuration for hardware-specific transpilation. | TranspilationOption.DECOMPOSE | | `run_via_classiq` | `bool` | Run via Classiq's credentials while using your allocated budget. Defaults to `False`. | False | | `verbose` | `bool` | Whether to print the "Submitting job to..." and "Job: ..." progress lines. Set to `False` to suppress them, for example when calling `sample` repeatedly in a loop. Defaults to `True`. | True | **Returns:** * **Type:** `DataFrame \| list[DataFrame]` * A dataframe containing the histogram, or a list of dataframes when * `parameters` is a list. ## BraketConfig Configuration specific to Amazon Braket. **Attributes:** | Name | Type | Description | | -------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `braket_access_key_id` | `str \| None` | The access key id of user with full braket access | | `braket_secret_access_key` | `str \| None` | The secret key assigned to the access key id for the user with full braket access. | | `s3_bucket_name` | `str \| None` | The name of the S3 bucket where results and other related data will be stored. This field should contain a valid S3 bucket name under your AWS account. | | `s3_folder` | `pydantic_backend.PydanticS3BucketKey \| None` | The folder path within the specified S3 bucket. This allows for organizing results and data under a specific directory within the S3 bucket. | ## IBMConfig Configuration specific to IBM. **Attributes:** | Name | Type | Description | | -------------- | ------------- | ------------------------------------------------------------------------------------------------------------- | | `access_token` | `str \| None` | The IBM Cloud access token to be used with IBM Quantum hosted backends. Defaults to `None`. | | `channel` | `str` | Channel to use for IBM cloud backends. Defaults to `"ibm_cloud"`. | | `instance_crn` | `str \| None` | The IBM Cloud instance CRN (Cloud Resource Name) for the IBM Quantum service. | | `emulate` | `bool` | If True, run on a Classiq-hosted simulator with IBM noise derived from the backend name. Defaults to `False`. | ## IonQConfig Configuration specific to IonQ. Attributes: api\_key (PydanticIonQApiKeyType | None): Key to access IonQ API. error\_mitigation (bool): A configuration option to enable or disable error mitigation during execution. Defaults to `False`. emulate (bool): If True, run on IonQ simulator with noise model derived from the backend name. Defaults to `False`. **Attributes:** | Name | Type | Description | | ------------------ | ------------------------------------------------- | ----------- | | `api_key` | `pydantic_backend.PydanticIonQApiKeyType \| None` | | | `error_mitigation` | `bool` | | | `emulate` | `bool` | | ## AzureConfig Configuration specific to Azure. **Attributes:** | Name | Type | Description | | ----------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `location` | `str` | Azure region. Defaults to `"East US"`. | | `tenant_id` | `str \| None` | Azure Tenant ID used to identify the directory in which the application is registered. | | `client_id` | `str \| None` | Azure Client ID, also known as the application ID, which is used to authenticate the application. | | `client_secret` | `str \| None` | Azure Client Secret associated with the application, used for authentication. | | `resource_id` | `str \| None` | Azure Resource ID, including the subscription ID, resource group, and workspace, typically used for personal resources. | | `ionq_error_mitigation` | `bool` | Should use error mitigation when running on IonQ via Azure. Defaults to `False`. | | `emulate` | `bool` | When True, request IonQ hardware noise simulation on Azure Quantum for IonQ QPU targets (`ionq.qpu.*`). No effect for `ionq.simulator` or non-IonQ targets. Defaults to `False`. | ## AQTConfig Configuration specific to AQT (Alpine Quantum Technologies). **Attributes:** | Name | Type | Description | | ----------- | ----- | ---------------------------------------------------------------- | | `api_key` | `str` | The API key required to access AQT's quantum computing services. | | `workspace` | `str` | The AQT workspace where the simulator/hardware is located. | ## AliceBobConfig Configuration specific to Alice\&Bob. **Attributes:** | Name | Type | Description | | -------------------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `distance` | `int \| None` | The number of times information is duplicated in the repetition code. - **Tooltip**: Phase-flip probability decreases exponentially with this parameter, bit-flip probability increases linearly. - **Supported Values**: 3 to 300, though practical values are usually lower than 30. - **Default**: None. | | `kappa_1` | `float \| None` | The rate at which the cat qubit loses one photon, creating a bit-flip. - **Tooltip**: Lower values mean lower error rates. - **Supported Values**: 10 to 10^5. Current hardware is at \~10^3. - **Default**: None. | | `kappa_2` | `float \| None` | The rate at which the cat qubit is stabilized using two-photon dissipation. - **Tooltip**: Higher values mean lower error rates. - **Supported Values**: 100 to 10^9. Current hardware is at \~10^5. - **Default**: None. | | `average_nb_photons` | `float \| None` | The average number of photons. - **Tooltip**: Bit-flip probability decreases exponentially with this parameter, phase-flip probability increases linearly. - **Supported Values**: 4 to 10^5, though practical values are usually lower than 30. - **Default**: None. | ## ProviderConfig Provider-specific configuration data for execution, such as API keys and machine-specific parameters. ## ExecutionSession A session for executing a quantum program or OpenQASM source text. `ExecutionSession` allows to execute the quantum program with different parameters and operations without the need to re-synthesize the model. The session must be closed in order to ensure resources are properly cleaned up. It's recommended to use `ExecutionSession` as a context manager for this purpose. Alternatively, you can directly use the `close` method. **Methods:** | Name | Description | | ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | [close](#close) | Close the session and clean up its resources. | | [get\_session\_id](#get_session_id) | | | [update\_execution\_preferences](#update_execution_preferences) | Update the execution preferences for the session. | | [sample](#sample) | Samples the quantum program with the given parameters, if any. | | [submit\_sample](#submit_sample) | Initiates an execution job with the `sample` primitive. | | [calculate\_state\_vector](#calculate_state_vector) | Calculate the state vector of the quantum program. | | [submit\_calculate\_state\_vector](#submit_calculate_state_vector) | Initiates an execution job with the `calculate_state_vector` primitive. | | [calculate\_unitary](#calculate_unitary) | Calculate the unitary matrix of the quantum program. | | [submit\_calculate\_unitary](#submit_calculate_unitary) | Initiates an execution job with the `calculate_unitary` primitive. | | [batch\_sample](#batch_sample) | Samples the quantum program multiple times with the given parameters for each iteration. | | [submit\_batch\_sample](#submit_batch_sample) | Initiates an execution job with the `batch_sample` primitive. | | [observe](#observe) | Estimates the expectation value of the given Hamiltonian using the quantum program. | | [submit\_observe](#submit_observe) | Initiates an execution job with the `observe` primitive. | | [estimate](#estimate) | Estimates the expectation value of the given Hamiltonian using the quantum program. | | [submit\_estimate](#submit_estimate) | Initiates an execution job with the `estimate` primitive. | | [batch\_estimate](#batch_estimate) | Estimates the expectation value of the given Hamiltonian multiple times using the quantum program, with the given parameters for each iteration. | | [submit\_batch\_estimate](#submit_batch_estimate) | Initiates an execution job with the `batch_estimate` primitive. | | [variational\_minimize](#variational_minimize) | Variationally minimizes the given cost function using the quantum program. | | [minimize](#minimize) | . | | [submit\_variational\_minimize](#submit_variational_minimize) | Initiates an execution job with the variational minimization primitive. | | [submit\_minimize](#submit_minimize) | . | | [estimate\_cost](#estimate_cost) | Estimates circuit cost using a classical cost function. | | [set\_measured\_state\_filter](#set_measured_state_filter) | When simulating on a statevector simulator, emulate the behavior of postprocessing by discarding amplitudes for which their states are "undesirable". | **Attributes:** | Name | Type | Description | | --------- | ---------------- | -------------------------------------------------------------------------------------------------------------- | | `program` | `QuantumProgram` | The quantum program to execute, or a placeholder when the first constructor argument was OpenQASM source text. | ### close
close(
self:
) -> None
Close the session and clean up its resources. **Parameters:** | Name | Type | Description | Default | | ------ | ---- | ----------- | ---------- | | `self` | \`\` | | *required* | ### get\_session\_id
get\_session\_id(
self:
) -> str
**Parameters:** | Name | Type | Description | Default | | ------ | ---- | ----------- | ---------- | | `self` | \`\` | | *required* | ### update\_execution\_preferences
update\_execution\_preferences(
self: ,
execution\_preferences: ExecutionPreferences | None
) -> None
Update the execution preferences for the session. **Parameters:** | Name | Type | Description | Default | | ----------------------- | ----------------------------------------------------- | ------------------------------------ | ---------- | | `self` | \`\` | | *required* | | `execution_preferences` | [ExecutionPreferences](#executionpreferences) \| None | The execution preferences to update. | *required* | **Returns:** * **Type:** `None` ### sample
sample(
self: ,
parameters: ExecutionParams | list\[ExecutionParams] | None = None,
num\_shots: int | None = None,
run\_via\_classiq: bool | None = None
) -> ExecutionDetails | list\[ExecutionDetails]
Samples the quantum program with the given parameters, if any. **Parameters:** | Name | Type | Description | Default | | ----------------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `self` | \`\` | | *required* | | `parameters` | `ExecutionParams \| list[ExecutionParams] \| None` | A dictionary of parameter values, or a list of dictionaries for batch execution. Each key should be the name of a parameter in the quantum program (parameters of the main function), and the value should be the value to set for that parameter. | None | | `num_shots` | `int \| None` | Number of shots for this call only; overrides the session default when set. | None | | `run_via_classiq` | `bool \| None` | Run via Classiq credentials for this call only; overrides the session default when set. | None | **Returns:** * **Type:** `ExecutionDetails \| list[ExecutionDetails]` * The result of the sampling, or a list of results when * `parameters` is a list. ### submit\_sample
submit\_sample(
self: ,
parameters: ExecutionParams | list\[ExecutionParams] | None = None,
num\_shots: int | None = None,
run\_via\_classiq: bool | None = None
) -> ExecutionJob
Initiates an execution job with the `sample` primitive. This is a non-blocking version of `sample`: it gets the same parameters and initiates the same execution job, but instead of waiting for the result, it returns the job object immediately. **Parameters:** | Name | Type | Description | Default | | ----------------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `self` | \`\` | | *required* | | `parameters` | `ExecutionParams \| list[ExecutionParams] \| None` | A dictionary of parameter values, or a list of dictionaries for batch execution. Each key should be the name of a parameter in the quantum program (parameters of the main function), and the value should be the value to set for that parameter. | None | | `num_shots` | `int \| None` | Number of shots for this call only; overrides the session default when set. | None | | `run_via_classiq` | `bool \| None` | Run via Classiq credentials for this call only; overrides the session default when set. | None | **Returns:** * **Type:** `ExecutionJob` * The execution job. ### calculate\_state\_vector
calculate\_state\_vector(
self: ,
parameters: ExecutionParams | list\[ExecutionParams] | None = None,
filters: dict\[str, Any] | None = None,
amplitude\_threshold: float = 0.0,
verbose: bool = True
) -> DataFrame | list\[DataFrame]
Calculate the state vector of the quantum program. The session must be configured with a Classiq simulator (`"classiq/simulator"`, `"classiq/nvidia_simulator"`) or `"google/cuquantum"`. The corresponding statevector variant is selected automatically; callers do not need to know about the `_statevector` backend names. **Parameters:** | Name | Type | Description | Default | | --------------------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `self` | \`\` | | *required* | | `parameters` | `ExecutionParams \| list[ExecutionParams] \| None` | A dictionary of parameter values, or a list of dictionaries for batch execution. | None | | `filters` | `dict[str, Any] \| None` | Only states where the variables match these values will be included in the state vector. | None | | `amplitude_threshold` | `float` | If provided, only states whose amplitude magnitude is strictly greater than this value will be included in the result. Defaults to 0 (filters exactly zero-amplitude states). | 0.0 | | `verbose` | `bool` | Whether to print the "Submitting state-vector job to..." progress line. Set to `False` to suppress it, for example when calling `calculate_state_vector` repeatedly in a loop. Defaults to `True`. | True | **Returns:** * **Type:** `DataFrame \| list[DataFrame]` * A dataframe containing the state vector, or a list of dataframes when * `parameters` is a list. ### submit\_calculate\_state\_vector
submit\_calculate\_state\_vector(
self: ,
parameters: ExecutionParams | list\[ExecutionParams] | None = None,
filters: dict\[str, Any] | None = None,
amplitude\_threshold: float = 0.0,
verbose: bool = True
) -> ExecutionJob
Initiates an execution job with the `calculate_state_vector` primitive. This is a non-blocking version of [calculate\_state\_vector()](#calculate_state_vector): it gets the same parameters and initiates the same execution job, but instead of waiting for the result, it returns the job object immediately. **Parameters:** | Name | Type | Description | Default | | --------------------- | -------------------------------------------------- | ---------------------------------------------------------- | ---------- | | `self` | \`\` | | *required* | | `parameters` | `ExecutionParams \| list[ExecutionParams] \| None` | See [calculate\_state\_vector()](#calculate_state_vector). | None | | `filters` | `dict[str, Any] \| None` | See [calculate\_state\_vector()](#calculate_state_vector). | None | | `amplitude_threshold` | `float` | See [calculate\_state\_vector()](#calculate_state_vector). | 0.0 | | `verbose` | `bool` | See [calculate\_state\_vector()](#calculate_state_vector). | True | **Returns:** * **Type:** `ExecutionJob` * The execution job. ### calculate\_unitary
calculate\_unitary(
self: ,
parameters: ExecutionParams | list\[ExecutionParams] | None = None,
amplitude\_threshold: float = 0.0
) -> DataFrame | list\[DataFrame]
Calculate the unitary matrix of the quantum program. The session must be configured with a Classiq simulator (`"classiq/simulator"`, `"classiq/nvidia_simulator"`, `"classiq/dgx_simulator"`) or `"google/cuquantum"`. **Parameters:** | Name | Type | Description | Default | | --------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ---------- | | `self` | \`\` | | *required* | | `parameters` | `ExecutionParams \| list[ExecutionParams] \| None` | A dictionary of parameter values, or a list of dictionaries for batch execution. | None | | `amplitude_threshold` | `float` | If provided, matrix elements whose magnitude is below this threshold are zeroed (same cutoff semantics as `calculate_state_vector`). | 0.0 | **Returns:** * **Type:** `DataFrame \| list[DataFrame]` * A dataframe containing the unitary matrix, or a list of dataframes when * `parameters` is a list. ### submit\_calculate\_unitary
submit\_calculate\_unitary(
self: ,
parameters: ExecutionParams | list\[ExecutionParams] | None = None,
amplitude\_threshold: float = 0.0
) -> ExecutionJob
Initiates an execution job with the `calculate_unitary` primitive. Promotes the session backend to a unitary simulator and runs the `sample` primitive on a dedicated unitary session. Program size limits are enforced server-side. **Parameters:** | Name | Type | Description | Default | | --------------------- | -------------------------------------------------- | ----------- | ---------- | | `self` | \`\` | | *required* | | `parameters` | `ExecutionParams \| list[ExecutionParams] \| None` | | None | | `amplitude_threshold` | `float` | | 0.0 | ### batch\_sample
batch\_sample(
self: ,
parameters: list\[ExecutionParams]
) -> list\[ExecutionDetails]
Samples the quantum program multiple times with the given parameters for each iteration. The number of samples is determined by the length of the parameters list. **Deprecated:** Pass a list of parameter dicts to [sample()](#sample) instead. **Parameters:** | Name | Type | Description | Default | | ------------ | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `self` | \`\` | | *required* | | `parameters` | `list[ExecutionParams]` | A list of the parameters for each iteration. Each item is a dictionary where each key should be the name of a parameter in the quantum program (parameters of the main function), and the value should be the value to set for that parameter. | *required* | **Returns:** * **Type:** `list[ExecutionDetails]` * List\[ExecutionDetails]: The results of all the sampling iterations. ### submit\_batch\_sample
submit\_batch\_sample(
self: ,
parameters: list\[ExecutionParams]
) -> ExecutionJob
Initiates an execution job with the `batch_sample` primitive. **Deprecated:** Pass a list of parameter dicts to [submit\_sample()](#submit_sample) instead. **Parameters:** | Name | Type | Description | Default | | ------------ | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `self` | \`\` | | *required* | | `parameters` | `list[ExecutionParams]` | A list of the parameters for each iteration. Each item is a dictionary where each key should be the name of a parameter in the quantum program (parameters of the main function), and the value should be the value to set for that parameter. | *required* | **Returns:** * **Type:** `ExecutionJob` * The execution job. ### observe
observe(
self: ,
hamiltonian: Hamiltonian,
parameters: ExecutionParams | list\[ExecutionParams] | None = None,
num\_shots: int | None = None,
run\_via\_classiq: bool | None = None
) -> EstimationResult | list\[EstimationResult]
Estimates the expectation value of the given Hamiltonian using the quantum program. **Parameters:** | Name | Type | Description | Default | | ----------------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `self` | \`\` | | *required* | | `hamiltonian` | `Hamiltonian` | The Hamiltonian to estimate the expectation value of. | *required* | | `parameters` | `ExecutionParams \| list[ExecutionParams] \| None` | A dictionary of parameter values, or a list of dictionaries for batch execution. Each key should be the name of a parameter in the quantum program (parameters of the main function), and the value should be the value to set for that parameter. | None | | `num_shots` | `int \| None` | Number of shots for this call only; overrides the session default when set. | None | | `run_via_classiq` | `bool \| None` | Run via Classiq credentials for this call only; overrides the session default when set. | None | **Returns:** * **Type:** `EstimationResult \| list[EstimationResult]` * The estimation result, or a list of results when `parameters` * is a list. ### submit\_observe
submit\_observe(
self: ,
hamiltonian: Hamiltonian,
parameters: ExecutionParams | list\[ExecutionParams] | None = None,
num\_shots: int | None = None,
run\_via\_classiq: bool | None = None
) -> ExecutionJob
Initiates an execution job with the `observe` primitive. This is a non-blocking version of [observe()](#observe): it gets the same parameters and initiates the same execution job, but instead of waiting for the result, it returns the job object immediately. **Parameters:** | Name | Type | Description | Default | | ----------------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `self` | \`\` | | *required* | | `hamiltonian` | `Hamiltonian` | The Hamiltonian to estimate the expectation value of. | *required* | | `parameters` | `ExecutionParams \| list[ExecutionParams] \| None` | A dictionary of parameter values, or a list of dictionaries for batch execution. Each key should be the name of a parameter in the quantum program (parameters of the main function), and the value should be the value to set for that parameter. | None | | `num_shots` | `int \| None` | Number of shots for this call only; overrides the session default when set. | None | | `run_via_classiq` | `bool \| None` | Run via Classiq credentials for this call only; overrides the session default when set. | None | **Returns:** * **Type:** `ExecutionJob` * The execution job. ### estimate
estimate(
self: ,
hamiltonian: Hamiltonian,
parameters: ExecutionParams | list\[ExecutionParams] | None = None,
num\_shots: int | None = None,
run\_via\_classiq: bool | None = None
) -> EstimationResult | list\[EstimationResult]
Estimates the expectation value of the given Hamiltonian using the quantum program. **Deprecated:** `estimate` is deprecated and will no longer be supported starting on 2026-06-22. Use [observe()](#observe) instead. **Parameters:** | Name | Type | Description | Default | | ----------------- | -------------------------------------------------- | ----------- | ---------- | | `self` | \`\` | | *required* | | `hamiltonian` | `Hamiltonian` | | *required* | | `parameters` | `ExecutionParams \| list[ExecutionParams] \| None` | | None | | `num_shots` | `int \| None` | | None | | `run_via_classiq` | `bool \| None` | | None | ### submit\_estimate
submit\_estimate(
self: ,
hamiltonian: Hamiltonian,
parameters: ExecutionParams | list\[ExecutionParams] | None = None,
num\_shots: int | None = None,
run\_via\_classiq: bool | None = None
) -> ExecutionJob
Initiates an execution job with the `estimate` primitive. **Deprecated:** `submit_estimate` is deprecated and will no longer be supported starting on 2026-06-22. Use [submit\_observe()](#submit_observe) instead. **Parameters:** | Name | Type | Description | Default | | ----------------- | -------------------------------------------------- | ----------- | ---------- | | `self` | \`\` | | *required* | | `hamiltonian` | `Hamiltonian` | | *required* | | `parameters` | `ExecutionParams \| list[ExecutionParams] \| None` | | None | | `num_shots` | `int \| None` | | None | | `run_via_classiq` | `bool \| None` | | None | ### batch\_estimate
batch\_estimate(
self: ,
hamiltonian: Hamiltonian,
parameters: list\[ExecutionParams]
) -> list\[EstimationResult]
Estimates the expectation value of the given Hamiltonian multiple times using the quantum program, with the given parameters for each iteration. The number of estimations is determined by the length of the parameters list. **Deprecated:** Pass a list of parameter dicts to [observe()](#observe) instead. **Parameters:** | Name | Type | Description | Default | | ------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `self` | \`\` | | *required* | | `hamiltonian` | `Hamiltonian` | The Hamiltonian to estimate the expectation value of. | *required* | | `parameters` | `list[ExecutionParams]` | A list of the parameters for each iteration. Each item is a dictionary where each key should be the name of a parameter in the quantum program (parameters of the main function), and the value should be the value to set for that parameter. | *required* | **Returns:** * **Type:** `list[EstimationResult]` * List\[EstimationResult]: The results of all the estimation iterations. ### submit\_batch\_estimate
submit\_batch\_estimate(
self: ,
hamiltonian: Hamiltonian,
parameters: list\[ExecutionParams]
) -> ExecutionJob
Initiates an execution job with the `batch_estimate` primitive. **Deprecated:** Pass a list of parameter dicts to [submit\_observe()](#submit_observe) instead. **Parameters:** | Name | Type | Description | Default | | ------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `self` | \`\` | | *required* | | `hamiltonian` | `Hamiltonian` | The Hamiltonian to estimate the expectation value of. | *required* | | `parameters` | `list[ExecutionParams]` | A list of the parameters for each iteration. Each item is a dictionary where each key should be the name of a parameter in the quantum program (parameters of the main function), and the value should be the value to set for that parameter. | *required* | **Returns:** * **Type:** `ExecutionJob` * The execution job. ### variational\_minimize
variational\_minimize(
self: ,
cost\_function: Hamiltonian | QmodExpressionCreator,
initial\_params: ExecutionParams,
max\_iteration: int,
quantile: float = 1.0,
tolerance: float | None = None,
hosted: bool = False,
num\_shots: int | None = None,
run\_via\_classiq: bool | None = None
) -> list\[tuple\[float, ExecutionParams]]
Variationally minimizes the given cost function using the quantum program. **Parameters:** | Name | Type | Description | Default | | ----------------- | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `self` | \`\` | | *required* | | `cost_function` | `Hamiltonian \| QmodExpressionCreator` | The cost function to minimize. It can be one of the following: - A quantum cost function defined by a Hamiltonian. - A classical cost function represented as a callable that returns a Qmod expression. The callable should accept `QVar`s as arguments and use names matching the Model outputs. | *required* | | `initial_params` | `ExecutionParams` | The initial parameters for the minimization. Only Models with exactly one execution parameter are supported. This parameter must be of type `CReal` or `CArray`. The dictionary must contain a single key-value pair, where: - The key is the name of the parameter. - The value is either a float or a list of floats. | *required* | | `max_iteration` | `int` | The maximum number of iterations for the minimization. | *required* | | `quantile` | `float` | The quantile to use for cost estimation. | 1.0 | | `tolerance` | `float \| None` | The tolerance for the minimization. | None | | `hosted` | `bool` | When `True` on an IonQ backend, submit a Classiq execution job that runs the optimization loop on IonQ Hosted Hybrid in the backend. Defaults to `False`. | False | | `num_shots` | `int \| None` | Number of shots for this call only; overrides the session default when set. | None | | `run_via_classiq` | `bool \| None` | Run via Classiq credentials for this call only; overrides the session default when set. | None | **Returns:** * **Type:** `list[tuple[float, ExecutionParams]]` * A list of tuples, each containing the estimated cost and the corresponding parameters for that iteration. `cost` is a float, and `parameters` is a dictionary matching the execution parameter format. ### minimize
minimize(
self: ,
cost\_function: Hamiltonian | QmodExpressionCreator,
initial\_params: ExecutionParams,
max\_iteration: int,
quantile: float = 1.0,
tolerance: float | None = None,
num\_shots: int | None = None,
run\_via\_classiq: bool | None = None
) -> list\[tuple\[float, ExecutionParams]]
**Deprecated:** Use [variational\_minimize()](#variational_minimize) instead. This name is kept for backward compatibility and will be removed in a future release. **Parameters:** | Name | Type | Description | Default | | ----------------- | -------------------------------------- | ----------- | ---------- | | `self` | \`\` | | *required* | | `cost_function` | `Hamiltonian \| QmodExpressionCreator` | | *required* | | `initial_params` | `ExecutionParams` | | *required* | | `max_iteration` | `int` | | *required* | | `quantile` | `float` | | 1.0 | | `tolerance` | `float \| None` | | None | | `num_shots` | `int \| None` | | None | | `run_via_classiq` | `bool \| None` | | None | ### submit\_variational\_minimize
submit\_variational\_minimize(
self: ,
cost\_function: Hamiltonian | QmodExpressionCreator,
initial\_params: ExecutionParams,
max\_iteration: int,
quantile: float = 1.0,
tolerance: float | None = None,
hosted: bool = False,
num\_shots: int | None = None,
run\_via\_classiq: bool | None = None
) -> ExecutionJob
Initiates an execution job with the variational minimization primitive. Non-blocking counterpart of [variational\_minimize()](#variational_minimize): same parameters and job, but returns the [ExecutionJob](#executionjob) immediately. **Parameters:** | Name | Type | Description | Default | | ----------------- | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `self` | \`\` | | *required* | | `cost_function` | `Hamiltonian \| QmodExpressionCreator` | The cost function to minimize. It can be one of the following: - A quantum cost function defined by a Hamiltonian. - A classical cost function represented as a callable that returns a Qmod expression. The callable should accept `QVar`s as arguments and use names matching the Model outputs. | *required* | | `initial_params` | `ExecutionParams` | The initial parameters for the minimization. Only Models with exactly one execution parameter are supported. This parameter must be of type `CReal` or `CArray`. The dictionary must contain a single key-value pair, where: - The key is the name of the parameter. - The value is either a float or a list of floats. | *required* | | `max_iteration` | `int` | The maximum number of iterations for the minimization. | *required* | | `quantile` | `float` | The quantile to use for cost estimation. | 1.0 | | `tolerance` | `float \| None` | The tolerance for the minimization. | None | | `hosted` | `bool` | When `True` on an IonQ backend, submit a Classiq execution job that runs the optimization loop on IonQ Hosted Hybrid in the backend. Defaults to `False`. | False | | `num_shots` | `int \| None` | Number of shots for this call only; overrides the session default when set. | None | | `run_via_classiq` | `bool \| None` | Run via Classiq credentials for this call only; overrides the session default when set. | None | **Returns:** * **Type:** `ExecutionJob` * The execution job. When `hosted=True` on an IonQ backend, the backend * worker submits and polls IonQ Hosted Hybrid while this job tracks * progress through the standard Classiq execution API. ### submit\_minimize
submit\_minimize(
self: ,
cost\_function: Hamiltonian | QmodExpressionCreator,
initial\_params: ExecutionParams,
max\_iteration: int,
quantile: float = 1.0,
tolerance: float | None = None,
num\_shots: int | None = None,
run\_via\_classiq: bool | None = None
) -> ExecutionJob
**Deprecated:** Use [submit\_variational\_minimize()](#submit_variational_minimize) instead. This name is kept for backward compatibility and will be removed in a future release. **Parameters:** | Name | Type | Description | Default | | ----------------- | -------------------------------------- | ----------- | ---------- | | `self` | \`\` | | *required* | | `cost_function` | `Hamiltonian \| QmodExpressionCreator` | | *required* | | `initial_params` | `ExecutionParams` | | *required* | | `max_iteration` | `int` | | *required* | | `quantile` | `float` | | 1.0 | | `tolerance` | `float \| None` | | None | | `num_shots` | `int \| None` | | None | | `run_via_classiq` | `bool \| None` | | None | ### estimate\_cost
estimate\_cost(
self: ,
cost\_func: Callable\[\[ParsedState], float],
parameters: ExecutionParams | None = None,
quantile: float = 1.0,
num\_shots: int | None = None,
run\_via\_classiq: bool | None = None
) -> float
Estimates circuit cost using a classical cost function. **Parameters:** | Name | Type | Description | Default | | ----------------- | -------------------------------- | --------------------------------------------------------------------------------------- | ---------- | | `self` | \`\` | | *required* | | `cost_func` | `Callable[[ParsedState], float]` | classical circuit sample cost function | *required* | | `parameters` | `ExecutionParams \| None` | execution parameters sent to 'sample' | None | | `quantile` | `float` | drop cost values outside the specified quantile | 1.0 | | `num_shots` | `int \| None` | Number of shots for this call only; overrides the session default when set. | None | | `run_via_classiq` | `bool \| None` | Run via Classiq credentials for this call only; overrides the session default when set. | None | **Returns:** * **Type:** `float` * cost estimation ### set\_measured\_state\_filter
set\_measured\_state\_filter(
self: ,
output\_name: str,
condition: Callable
) -> None
When simulating on a statevector simulator, emulate the behavior of postprocessing by discarding amplitudes for which their states are "undesirable". **Parameters:** | Name | Type | Description | Default | | ------------- | ---------- | --------------------------------------------------------------------- | ---------- | | `self` | \`\` | | *required* | | `output_name` | `str` | The name of the register to filter | *required* | | `condition` | `Callable` | Filter out values of the statevector for which this callable is False | *required* | ## ExecutionPreferences Represents the execution settings for running a quantum program. Execution preferences for running a quantum program. For more details, refer to: ExecutionPreferences example: [ExecutionPreferences](https://docs.classiq.io/latest/user-guide/execution/#execution-preferences).. **Attributes:** | Name | Type | Description | | -------------------------------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | `include_zero_amplitude_outputs` | `bool` | | | `amplitude_threshold` | `float` | | | `noise_properties` | `Optional[NoiseProperties]` | Properties defining the noise in the quantum circuit. Defaults to `None`. | | `random_seed` | `int` | The random seed used for the execution. Defaults to a randomly generated seed. | | `backend_preferences` | `BackendPreferencesTypes` | Preferences for the backend used to execute the circuit. Defaults to the Classiq Simulator. | | `num_shots` | `Optional[pydantic.PositiveInt]` | The number of shots (executions) to be performed. | | `transpile_to_hardware` | `TranspilationOption` | Option to transpile the circuit to the hardware's basis gates before execution. Defaults to `TranspilationOption.DECOMPOSE`. | | `job_name` | `Optional[str]` | The name of the job, with a minimum length of 1 character. | ## CostEstimateResult Result of sample cost estimation. **Attributes:** | Name | Type | Description | | ---------- | ------- | ----------- | | `cost` | `float` | | | `currency` | `str` | | ## BackendPreferences Preferences for the execution of the quantum program. **Methods:** | Name | Description | | ----------------------------------------- | ----------- | | [batch\_preferences](#batch_preferences) | | | [is\_nvidia\_backend](#is_nvidia_backend) | | **Attributes:** | Name | Type | Description | | -------------------------- | ---------- | ---------------------------------------------------- | | `hw_provider` | `Provider` | | | `backend_service_provider` | `str` | Provider company or cloud for the requested backend. | | `backend_name` | `str` | Name of the requested backend or target. | ### batch\_preferences
batch\_preferences(
cls: ,
backend\_names: Iterable\[str],
kwargs: Any = 
) -> list\[BackendPreferences]
**Parameters:** | Name | Type | Description | Default | | --------------- | --------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `backend_names` | `Iterable[str]` | | *required* | | `kwargs` | `Any` | | | ### is\_nvidia\_backend
is\_nvidia\_backend(
self:
) -> bool
**Parameters:** | Name | Type | Description | Default | | -------- | ---- | ----------- | ---------- | | `self` | \`\` | | *required* | | Members: | | | | | Name | Description | | ----------------------- | ---------------------------------------------------------------------------------- | | `program_id_scope` | Within the scope, HTTP spans emitted by Client.request stamp `classiq.program.id`. | | `ExecutionJobResults` | Results from `ExecutionJob.result()`: list-like with job-level metadata. | | `SubmittedCircuit` | A quantum circuit that was submitted to the provider. | | `ExecutionJobFilters` | Filter parameters for querying execution jobs. | | `get_execution_jobs` | Query execution jobs. | | `get_execution_actions` | Query execution jobs with optional filters. | ### program\_id\_scope
program\_id\_scope(
program\_id: str | None
) -> Generator\[None, None, None]
Within the scope, HTTP spans emitted by Client.request stamp `classiq.program.id`. None = no-op, leaves any outer scope intact. **Parameters:** | Name | Type | Description | Default | | ------------ | ------------- | ----------- | ---------- | | `program_id` | `str \| None` | | *required* | ## ExecutionJobResults Results from `ExecutionJob.result()`: list-like with job-level metadata. **Attributes:** | Name | Type | Description | | -------------------------------- | ------------- | ----------- | | `hardware_execution_duration_ms` | `int \| None` | | ## SubmittedCircuit A quantum circuit that was submitted to the provider. Wraps the circuit in QASM format. Use to\_qasm() for the text representation or to\_qiskit() for a Qiskit QuantumCircuit (requires qiskit). **Methods:** | Name | Description | | ------------------------ | ---------------------------------------------------------- | | [to\_qasm](#to_qasm) | Return the circuit as a QASM string (OpenQASM 2.0 or 3.0). | | [to\_qiskit](#to_qiskit) | Return the circuit as a Qiskit QuantumCircuit. | ### to\_qasm
to\_qasm(
self:
) -> str
Return the circuit as a QASM string (OpenQASM 2.0 or 3.0). **Parameters:** | Name | Type | Description | Default | | ------ | ---- | ----------- | ---------- | | `self` | \`\` | | *required* | ### to\_qiskit
to\_qiskit(
self:
) -> Any
Return the circuit as a Qiskit QuantumCircuit. Requires qiskit. **Parameters:** | Name | Type | Description | Default | | ------ | ---- | ----------- | ---------- | | `self` | \`\` | | *required* | ## ExecutionJobFilters Filter parameters for querying execution jobs. All filters are combined using AND logic: only jobs matching all specified filters are returned. Range filters (with \_min/\_max suffixes) are inclusive. Datetime filters are compared against the job's timestamps. **Methods:** | Name | Description | | ---------------------------------- | ------------------------------------------------------------------------------------ | | [format\_filters](#format_filters) | Convert filter fields to API kwargs, excluding None values and converting datetimes. | **Attributes:** | Name | Type | Description | | ---------------- | ------------------- | ----------- | | `id` | `str \| None` | | | `session_id` | `str \| None` | | | `status` | `JobStatus \| None` | | | `name` | `str \| None` | | | `provider` | `str \| None` | | | `backend` | `str \| None` | | | `program_id` | `str \| None` | | | `total_cost_min` | `float \| None` | | | `total_cost_max` | `float \| None` | | | `start_time_min` | `datetime \| None` | | | `start_time_max` | `datetime \| None` | | | `end_time_min` | `datetime \| None` | | | `end_time_max` | `datetime \| None` | | ### format\_filters
format\_filters(
self:
) -> dict\[str, Any]
Convert filter fields to API kwargs, excluding None values and converting datetimes. **Parameters:** | Name | Type | Description | Default | | ------ | ---- | ----------- | ---------- | | `self` | \`\` | | *required* | ### get\_execution\_jobs
get\_execution\_jobs(
offset: int = 0,
limit: int = 50
) -> list\[ExecutionJob]
Query execution jobs. **Parameters:** | Name | Type | Description | Default | | -------- | ----- | ------------------------------------------------- | ------- | | `offset` | `int` | Number of results to skip (default: 0) | 0 | | `limit` | `int` | Maximum number of results to return (default: 50) | 50 | **Returns:** * **Type:** `list[ExecutionJob]` * List of ExecutionJob objects. ### get\_execution\_actions
get\_execution\_actions(
offset: int = 0,
limit: int = 50,
filters: ExecutionJobFilters | None = None
) -> pd.DataFrame
Query execution jobs with optional filters. **Parameters:** | Name | Type | Description | Default | | --------- | ----------------------------- | ----------------------------------------------------------------- | ------- | | `offset` | `int` | Number of results to skip (default: 0) | 0 | | `limit` | `int` | Maximum number of results to return (default: 50) | 50 | | `filters` | `ExecutionJobFilters \| None` | Optional ExecutionJobFilters object containing filter parameters. | None | **Returns:** * **Type:** `pd.DataFrame` * pandas.DataFrame containing execution job information with columns: * id, name, start\_time, end\_time, provider, backend\_name, status, * num\_shots, program\_id, error, total\_cost, currency\_code, runtime\_ms * (provider-reported hardware execution duration in milliseconds when available). ## BenchmarkClass Benchmark problem classes supported by the Classiq platform. **Attributes:** | Name | Type | Description | | ------------------- | --------------------- | ----------- | | `ADDER` | `'ADDER'` | | | `GHZ` | `'GHZ'` | | | `QFT` | `'QFT'` | | | `STATE_PREPARATION` | `'STATE_PREPARATION'` | | ## BenchmarkRequest Request body for `run_benchmark`. Submits one benchmark class across multiple `problem_sizes` and backend targets in a single asynchronous session. **Attributes:** | Name | Type | Description | | ----------------- | ------------------------------- | ----------- | | `benchmark_class` | `BenchmarkClass` | | | `backends` | `list[BackendExecutionDetails]` | | | `problem_sizes` | `list[int]` | | ## BackendExecutionDetails Execution configuration for a benchmark backend target. The object defines how all requested `problem_sizes` should run on a specific backend. **Attributes:** | Name | Type | Description | | ---------------------- | --------------------- | ----------- | | `backend_name` | `str` | | | `run_via_classiq` | `bool` | | | `transpilation_option` | `TranspilationOption` | | | `shots` | `int` | | | `provider_config` | `dict[str, Any]` | | ## BenchmarkSession A handle to a submitted benchmark run. This is the same serializable session object the API returns from the submit endpoint (so SDK users and other services share the contract), extended with convenience methods for fetching results from Python. `run_benchmark` returns the session immediately, without waiting for the executions to finish. Use `fetch_results` (or `fetch_results_async`) to retrieve the current status and result of every submitted job; results that reached a final status are cached and not re-fetched. **Methods:** | Name | Description | | --------------------------------------------- | ---------------------------------------------------------------------- | | [fetch\_results\_async](#fetch_results_async) | Fetch the current status and result of every submitted job. | | [fetch\_results](#fetch_results) | Synchronous wrapper for `fetch_results_async`. | | [cancel\_async](#cancel_async) | Request cancellation of all incomplete jobs in this benchmark session. | | [cancel](#cancel) | Synchronous wrapper for `cancel_async`. | | [to\_dataframe](#to_dataframe) | Per-target benchmark table built from the results fetched so far. | ### fetch\_results\_async
fetch\_results\_async(
self: ,
\_http\_client: httpx.AsyncClient | None = None
) -> list\[BackendBenchmarkResult]
Fetch the current status and result of every submitted job. **Parameters:** | Name | Type | Description | Default | | -------------- | --------------------------- | ----------- | ---------- | | `self` | \`\` | | *required* | | `_http_client` | `httpx.AsyncClient \| None` | | None | **Returns:** * **Type:** list\[[BackendBenchmarkResult](#backendbenchmarkresult)] * Latest per-job results for all successfully submitted targets in this session. ### fetch\_results
fetch\_results(
self: ,
\_http\_client: httpx.AsyncClient | None = None
) -> list\[BackendBenchmarkResult]
Synchronous wrapper for `fetch_results_async`. **Parameters:** | Name | Type | Description | Default | | -------------- | --------------------------- | ----------- | ---------- | | `self` | \`\` | | *required* | | `_http_client` | `httpx.AsyncClient \| None` | | None | **Returns:** * **Type:** list\[[BackendBenchmarkResult](#backendbenchmarkresult)] * Latest per-job results for all successfully submitted targets in this session. ### cancel\_async
cancel\_async(
self: ,
\_http\_client: httpx.AsyncClient | None = None
) -> None
Request cancellation of all incomplete jobs in this benchmark session. **Parameters:** | Name | Type | Description | Default | | -------------- | --------------------------- | ----------- | ---------- | | `self` | \`\` | | *required* | | `_http_client` | `httpx.AsyncClient \| None` | | None | ### cancel
cancel(
self: ,
\_http\_client: httpx.AsyncClient | None = None
) -> None
Synchronous wrapper for `cancel_async`. Request cancellation of all incomplete jobs in this benchmark session **Parameters:** | Name | Type | Description | Default | | -------------- | --------------------------- | ----------- | ---------- | | `self` | \`\` | | *required* | | `_http_client` | `httpx.AsyncClient \| None` | | None | ### to\_dataframe
to\_dataframe(
self:
) -> BenchmarkResponse
Per-target benchmark table built from the results fetched so far. Columns: problem\_size, success, error, run\_via\_classiq, transpilation\_option, score, cost, submission\_date. Targets whose result has not been fetched (or is still pending) appear as unsuccessful rows with a missing score. **Parameters:** | Name | Type | Description | Default | | ------ | ---- | ----------- | ---------- | | `self` | \`\` | | *required* | ## BenchmarkSessionTarget A single execution target within a benchmark session. `job_id` is the internal handle used to fetch this target's result; it is `None` when the submission itself failed (`submission_error` is then populated). `execution_job_id` is the actual execution job id when it is known. **Attributes:** | Name | Type | Description | | ---------------------- | ------------------------------ | ----------- | | `problem_size` | `int` | | | `backend_name` | `str` | | | `run_via_classiq` | `bool` | | | `transpilation_option` | `TranspilationOption` | | | `shots` | `int` | | | `job_id` | `str \| None` | | | `execution_job_id` | `str \| None` | | | `submission_error` | `BenchmarkTargetError \| None` | | ## BenchmarkSessionsQueryResults List response payload for benchmark session queries. **Attributes:** | Name | Type | Description | | ---------- | ------------------------ | ----------- | | `sessions` | `list[BenchmarkSession]` | | ## BenchmarkJobStatus Status of an asynchronously tracked benchmark job. `PENDING` means the job is still in progress. `COMPLETED` means execution and scoring succeeded. `FAILED` means execution or scoring failed. `CANCELLED` means the job was cancelled before completion. **Attributes:** | Name | Type | Description | | ----------- | ------------- | ----------- | | `PENDING` | `'PENDING'` | | | `COMPLETED` | `'COMPLETED'` | | | `FAILED` | `'FAILED'` | | | `CANCELLED` | `'CANCELLED'` | | ## BackendBenchmarkResult Asynchronously-fetched result for a previously submitted benchmark job. **Attributes:** | Name | Type | Description | | ------------------- | ------------------------------ | ----------- | | `job_id` | `str` | | | `execution_job_id` | `str \| None` | | | `problem_size` | `int` | | | `status` | `BenchmarkJobStatus` | | | `backend_benchmark` | `BackendBenchmark \| None` | | | `error` | `BenchmarkTargetError \| None` | | ## BackendBenchmarkResponse Benchmark execution outcome for a single backend target. **Attributes:** | Name | Type | Description | | ------------------- | ------------------------------ | ----------- | | `success` | `bool` | | | `backend_benchmark` | `BackendBenchmark \| None` | | | `error` | `BenchmarkTargetError \| None` | | ## BenchmarkResponse Tabular benchmark response wrapper returned by `BenchmarkSession.to_dataframe`. **Attributes:** | Name | Type | Description | | -------------- | -------------- | ----------- | | `model_config` | | | | `data` | `pd.DataFrame` | | ## BenchmarkTargetError Structured error details for a benchmark target failure. **Attributes:** | Name | Type | Description | | -------------- | ------------- | ----------- | | `backend_name` | `str \| None` | | | `message` | `str` | | ## BackendBenchmark Benchmark score payload for a completed backend run. **Attributes:** | Name | Type | Description | | ----------------- | ------------------- | ----------- | | `benchmark_class` | `BenchmarkClass` | | | `problem_size` | `int` | | | `score` | `float` | | | `metadata` | `BenchmarkMetadata` | | ## BenchmarkMetadata Metadata captured for a completed benchmark target run. **Attributes:** | Name | Type | Description | | --------------------------- | --------------------------------- | ----------- | | `benchmark_class_version` | `int` | | | `execution_duration_ms` | `int \| None` | | | `cost` | `float \| None` | | | `submission_date` | `str \| None` | | | `backend_execution_details` | `BackendExecutionDetailsResponse` | | ## BackendExecutionDetailsResponse Backend execution settings recorded in benchmark result metadata. **Attributes:** | Name | Type | Description | | ---------------------- | --------------------- | ----------- | | `backend_name` | `str` | | | `run_via_classiq` | `bool` | | | `transpilation_option` | `TranspilationOption` | | | `shots` | `int` | | ## BenchmarkClassMetadata Metadata associated with a benchmark class. **Attributes:** | Name | Type | Description | | -------- | --------------------------- | ------------------------------------------------------ | | `limits` | `ProblemSizeLimits \| None` | Optional problem-size bounds for this benchmark class. | ## ProblemSizeLimits Optional lower/upper bounds for valid problem sizes of a class. **Attributes:** | Name | Type | Description | | ----- | ------------- | ------------------------------------------------------- | | `min` | `int \| None` | Inclusive minimum supported problem size, when defined. | | `max` | `int \| None` | Inclusive maximum supported problem size, when defined. | ## Functions ### calculate\_state\_vector
calculate\_state\_vector(
qprog: QuantumProgram,
backend: str | None = None,
parameters: ExecutionParams | list\[ExecutionParams] | None = None,
filters: dict\[str, Any] | None = None,
random\_seed: int | None = None,
transpilation\_option: TranspilationOption = TranspilationOption.DECOMPOSE,
amplitude\_threshold: float = 0.0,
verbose: bool = True
) -> DataFrame | list\[DataFrame]
Calculate the state vector of a quantum program. This function is only available for Classiq simulators (e.g. `"classiq/simulator"`). **Parameters:** | Name | Type | Description | Default | | ---------------------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- | | `qprog` | `QuantumProgram` | The quantum program to be executed. | *required* | | `backend` | `str \| None` | The simulator on which to simulate the quantum program. Specified as `"provider/backend_name"`. Use the `get_backend_details` function to see supported backends. Only Classiq simulators are supported. | None | | `parameters` | `ExecutionParams \| list[ExecutionParams] \| None` | A dictionary of parameter values, or a list of dictionaries for batch execution. Each key should be the name of a parameter in the quantum program (parameters of the main function), and the value should be the value to set for that parameter. | None | | `filters` | `dict[str, Any] \| None` | Only states where the variables match these values will be included in the state vector. | None | | `random_seed` | `int \| None` | The random seed for reproducibility. | None | | `transpilation_option` | `TranspilationOption` | Advanced configuration for hardware-specific transpilation. | TranspilationOption.DECOMPOSE | | `amplitude_threshold` | `float` | If provided, only states whose amplitude magnitude is strictly greater than this value will be included in the result. Defaults to 0 (filters exactly zero-amplitude states). | 0.0 | | `verbose` | `bool` | Whether to print the "Submitting state-vector job to..." progress line. Set to `False` to suppress it, for example when calling `calculate_state_vector` repeatedly in a loop. Defaults to `True`. | True | **Returns:** * **Type:** `DataFrame \| list[DataFrame]` * A dataframe containing the state vector, or a list of dataframes when * `parameters` is a list. ### calculate\_unitary
calculate\_unitary(
qprog: QuantumProgram,
backend: str | None = None,
parameters: ExecutionParams | list\[ExecutionParams] | None = None,
amplitude\_threshold: float = 0.0,
random\_seed: int | None = None,
transpilation\_option: TranspilationOption = TranspilationOption.DECOMPOSE
) -> DataFrame | list\[DataFrame]
Calculate the unitary matrix of a quantum program. This function is only available for Classiq simulators (e.g. `"classiq/simulator"`, `"classiq/nvidia_simulator"`, `"classiq/dgx_simulator"`) and `"google/cuquantum"`. **Parameters:** | Name | Type | Description | Default | | ---------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------- | | `qprog` | `QuantumProgram` | The quantum program to be executed. | *required* | | `backend` | `str \| None` | The simulator on which to simulate the quantum program. Specified as `"provider/backend_name"`. | None | | `parameters` | `ExecutionParams \| list[ExecutionParams] \| None` | A dictionary of parameter values, or a list of dictionaries for batch execution. | None | | `amplitude_threshold` | `float` | If provided, matrix elements whose magnitude is below this threshold are zeroed (same cutoff semantics as `calculate_state_vector`). | 0.0 | | `random_seed` | `int \| None` | The random seed for reproducibility. | None | | `transpilation_option` | `TranspilationOption` | Advanced configuration for hardware-specific transpilation. | TranspilationOption.DECOMPOSE | **Returns:** * **Type:** `DataFrame \| list[DataFrame]` * A dataframe containing the unitary matrix, or a list of dataframes when * `parameters` is a list. ### observe
observe(
qprog: QuantumProgram,
observable: SparsePauliOp,
backend: str | None = None,
estimate: bool = True,
parameters: ExecutionParams | list\[ExecutionParams] | None = None,
config: dict\[str, Any] | ProviderConfig | None = None,
num\_shots: int | None = None,
random\_seed: int | None = None,
transpilation\_option: TranspilationOption = TranspilationOption.DECOMPOSE,
run\_via\_classiq: bool = False,
verbose: bool = True
) -> float | list\[float]
Get the expectation value of the observable O with respect to the state `\|psi>`, which is prepared by the provided quantum program. **Parameters:** | Name | Type | Description | Default | | ---------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- | | `qprog` | `QuantumProgram` | The quantum program that generates the state \`\|psi>\` to be observed. | *required* | | `observable` | `SparsePauliOp` | The observable O, a Hermitian operator defined as a `SparsePauliOp` (sum of Pauli terms). | *required* | | `backend` | `str \| None` | The hardware or simulator on which to run the quantum program. Use `"simulator"` for Classiq's default simulator, or specify a backend as `"provider/backend_name"`. Use the `get_backend_details` function to see supported devices. | None | | `estimate` | `bool` | Whether to estimate the expectation value by repeatedly measuring the circuit `num_shots` times, or calculate the exact expectation value using a simulated statevector. Note that the available options depend on the specified backend. Defaults to `True`. | True | | `parameters` | `ExecutionParams \| list[ExecutionParams] \| None` | A dictionary of parameter values, or a list of dictionaries for batch execution. Each key should be the name of a parameter in the quantum program (parameters of the main function), and the value should be the value to set for that parameter. | None | | `config` | dict\[str, Any] \| [ProviderConfig](#providerconfig) \| None | Provider-specific configuration, such as API keys. For full details, see the SDK reference under Providers. | None | | `num_shots` | `int \| None` | The number of measurement shots. Only relevant when `estimate=True`. | None | | `random_seed` | `int \| None` | The random seed for reproducibility. | None | | `transpilation_option` | `TranspilationOption` | Advanced configuration for hardware-specific transpilation. | TranspilationOption.DECOMPOSE | | `run_via_classiq` | `bool` | Run via Classiq's credentials while using your allocated budget. Defaults to `False`. | False | | `verbose` | `bool` | Whether to print the "Submitting job to..." and "Job: ..." progress lines. Set to `False` to suppress them, for example when calling `observe` repeatedly in a loop. Defaults to `True`. | True | **Returns:** * **Type:** `float \| list[float]` * The expectation value as a float, or a list of floats when * `parameters` is a list. ### variational\_minimize
variational\_minimize(
qprog: QuantumProgram,
cost\_function: SparsePauliOp | QmodExpressionCreator,
initial\_params: ExecutionParams,
max\_iteration: int,
backend: str | None = None,
quantile: float = 1.0,
tolerance: float | None = None,
hosted: bool = False,
config: dict\[str, Any] | ProviderConfig | None = None,
random\_seed: int | None = None,
transpilation\_option: TranspilationOption = TranspilationOption.DECOMPOSE,
run\_via\_classiq: bool = False,
verbose: bool = True
) -> list\[tuple\[float, ExecutionParams]]
Minimize the given cost function over the parameter values of the provided quantum program. **Parameters:** | Name | Type | Description | Default | | ---------------------- | ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- | | `qprog` | `QuantumProgram` | The parametric quantum program that generates the state (ansatz). Only quantum programs with exactly one execution parameter are supported. | *required* | | `cost_function` | `SparsePauliOp \| QmodExpressionCreator` | The cost function to minimize. It can be one of the following: - A quantum cost function defined by a Hamiltonian. - A classical cost function represented as a callable that returns a Qmod expression. The callable should accept `QVar`s as arguments and use names matching the Model outputs. | *required* | | `initial_params` | `ExecutionParams` | The initial parameter values for the minimization. This parameter must be of type `CReal` or `CArray`. The dictionary must contain a single key-value pair, where: - The key is the name of the parameter. - The value is either a float or a list of floats. | *required* | | `max_iteration` | `int` | The maximum number of iterations for the minimization. | *required* | | `backend` | `str \| None` | The hardware or simulator on which to run the quantum programs. Use `"simulator"` for Classiq's default simulator, or specify a backend as `"provider/backend_name"`. Use the `get_backend_details` function to see supported devices. | None | | `quantile` | `float` | The quantile to use for cost estimation. | 1.0 | | `tolerance` | `float \| None` | The tolerance for the minimization. | None | | `hosted` | `bool` | When `True` and the configured backend is IonQ, delegate the optimization loop to IonQ's Hosted Hybrid Service instead of running it locally. Defaults to `False`. | False | | `config` | dict\[str, Any] \| [ProviderConfig](#providerconfig) \| None | Provider-specific configuration, such as API keys. For full details, see the SDK reference under Providers. | None | | `random_seed` | `int \| None` | The random seed for reproducibility. | None | | `transpilation_option` | `TranspilationOption` | Advanced configuration for hardware-specific transpilation. | TranspilationOption.DECOMPOSE | | `run_via_classiq` | `bool` | Run via Classiq's credentials while using your allocated budget. Defaults to `False`. | False | | `verbose` | `bool` | Whether to print the "Submitting job to..." progress line. Set to `False` to suppress it, for example when calling `variational_minimize` repeatedly in a loop. Defaults to `True`. | True | **Returns:** * **Type:** `list[tuple[float, ExecutionParams]]` * A list of tuples, each containing the estimated cost and the * corresponding parameters for that iteration. `cost` is a float, * and `parameters` is a dictionary matching the execution parameter * format. ### execute
execute(
quantum\_program: QuantumProgram
) -> ExecutionJob
Execute a quantum program. The preferences for execution are set on the quantum program using the method `set_execution_preferences`. **Parameters:** | Name | Type | Description | Default | | ----------------- | ---------------- | ---------------------------------------------------------------------------- | ---------- | | `quantum_program` | `QuantumProgram` | The quantum program to execute. This is the result of the synthesize method. | *required* | **Returns:** * **Type:** `ExecutionJob` * The result of the execution. ### estimate\_sample\_cost
estimate\_sample\_cost(
quantum\_program: QuantumProgram,
execution\_options: ExecutionPreferences | str,
config: dict\[str, Any] | None = None,
num\_shots: int | None = None,
transpilation\_option: TranspilationOption = TranspilationOption.DECOMPOSE
) -> CostEstimateResult
Estimate the cost for sampling a quantum program. `execution_options` may be a full `ExecutionPreferences` object, or the same **backend specifier string** used by `sample()` (for example `"braket/SV1"` or `"azure/ionq.simulator"`). When it is a string, optional `config`, `num_shots`, and `transpilation_option` are applied like the `sample()` helpers. String backends are always resolved with **run via Classiq** when the provider supports it (no user cloud credentials required for cost estimation). **Parameters:** | Name | Type | Description | Default | | ---------------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------- | ----------------------------- | | `quantum_program` | `QuantumProgram` | The quantum program (output of synthesize). | *required* | | `execution_options` | [ExecutionPreferences](#executionpreferences) \| str | Execution preferences or a backend specifier string. | *required* | | `config` | `dict[str, Any] \| None` | Passed only for string backends (non-credential options, e.g. Azure `location`). | None | | `num_shots` | `int \| None` | Shots when using a string backend; ignored when using `ExecutionPreferences`. | None | | `transpilation_option` | `TranspilationOption` | Transpilation when using a string backend; ignored otherwise. | TranspilationOption.DECOMPOSE | **Returns:** * **Type:** [CostEstimateResult](#costestimateresult) * CostEstimateResult with cost and currency. ### estimate\_sample\_batch\_cost
estimate\_sample\_batch\_cost(
quantum\_program: QuantumProgram,
execution\_backend: BackendPreferencesTypes | str,
transpilation\_level: TranspilationOption = TranspilationOption.DECOMPOSE,
shots: int = 1000,
params: list\[dict] | None = None,
config: dict\[str, Any] | None = None
) -> CostEstimateResult
Estimate the cost for batch sampling a quantum program. `execution_backend` may be backend preferences or a `sample()`-style specifier string. With a string backend, pass non-credential options in `config`; resolution uses **run via Classiq** whenever the provider supports it (same rule as `estimate_sample_cost`). **Parameters:** | Name | Type | Description | Default | | --------------------- | -------------------------------- | --------------------------------------------------------------------- | ----------------------------- | | `quantum_program` | `QuantumProgram` | The quantum program (output of synthesize). | *required* | | `execution_backend` | `BackendPreferencesTypes \| str` | Backend preferences or specifier string. | *required* | | `transpilation_level` | `TranspilationOption` | Transpilation option for the circuit. | TranspilationOption.DECOMPOSE | | `shots` | `int` | Number of shots per sample. | 1000 | | `params` | `list[dict] \| None` | Optional list of parameter sets for batch. If None, single sample. | None | | `config` | `dict[str, Any] \| None` | Non-credential provider options when `execution_backend` is a string. | None | **Returns:** * **Type:** [CostEstimateResult](#costestimateresult) * CostEstimateResult with cost and currency. ### assign\_parameters
assign\_parameters(
quantum\_program: QuantumProgram,
parameters: ExecutionParams
) -> QuantumProgram
Assign parameters to a parametric quantum program. **Parameters:** | Name | Type | Description | Default | | ----------------- | ----------------- | -------------------------------------------------------------------------------- | ---------- | | `quantum_program` | `QuantumProgram` | The quantum program to be assigned. This is the result of the synthesize method. | *required* | | `parameters` | `ExecutionParams` | The parameter assignments. | *required* | **Returns:** * **Type:** `QuantumProgram` * The quantum program after assigning parameters. ### transpile
transpile(
quantum\_program: QuantumProgram,
preferences: Preferences | None = None
) -> QuantumProgram
Transpiles a quantum program. **Parameters:** | Name | Type | Description | Default | | ----------------- | --------------------- | ------------------------------------------------------------------------------ | ---------- | | `quantum_program` | `QuantumProgram` | The quantum program to transpile. This is the result of the synthesize method. | *required* | | `preferences` | `Preferences \| None` | The transpilation preferences. | None | **Returns:** * **Type:** `QuantumProgram` * The result of the transpilation (Optional). ### get\_budget
get\_budget(
provider: ProviderVendor | None = None
) -> UserBudgets
Retrieve the user's budget information for quantum computing resources. **Parameters:** | Name | Type | Description | Default | | ---------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------- | ------- | | `provider` | `ProviderVendor \| None` | (Optional) The quantum backend provider to filter budgets by. If not provided, budgets for all providers will be returned. | None | **Returns:** * **Type:** `UserBudgets` * An object containing the user's budget information. ### set\_budget\_limit
set\_budget\_limit(
provider: ProviderVendor,
limit: float
) -> UserBudgets
Set a budget limit for a specific quantum backend provider. **Parameters:** | Name | Type | Description | Default | | ---------- | ---------------- | --------------------------------------------------------------------------------------- | ---------- | | `provider` | `ProviderVendor` | The quantum backend provider for which to set the budget limit. | *required* | | `limit` | `float` | The budget limit to set. Must be greater than zero and not exceed the available budget. | *required* | **Returns:** * **Type:** `UserBudgets` * An object containing the updated budget information. ### clear\_budget\_limit
clear\_budget\_limit(
provider: ProviderVendor
) -> UserBudgets
Clear the budget limit for a specific quantum backend provider. **Parameters:** | Name | Type | Description | Default | | ---------- | ---------------- | ----------------------------------------------------------------- | ---------- | | `provider` | `ProviderVendor` | The quantum backend provider for which to clear the budget limit. | *required* | **Returns:** * **Type:** `UserBudgets` * An object containing the updated budget information. ### run\_benchmark
run\_benchmark(
request: BenchmarkRequest
) -> BenchmarkSession
Submit a benchmark class across problem sizes and execution targets. Every backend in `request.backends` is paired with every value in `request.problem_sizes` and submitted up front. The call returns immediately with a `BenchmarkSession` and does not wait for executions to complete. Use `BenchmarkSession.fetch_results` or `BenchmarkSession.fetch_results_async` to poll statuses and fetch results. Targets that fail during submission are still included in the session with `submission_error` populated and no `job_id`. **Parameters:** | Name | Type | Description | Default | | --------- | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `request` | [BenchmarkRequest](#benchmarkrequest) | Benchmark submission settings including benchmark class, requested problem sizes, and per-target backend execution configuration. | *required* | **Returns:** * **Type:** [BenchmarkSession](#benchmarksession) * A `BenchmarkSession` containing session metadata, per-target submission handles, and any fetched results. ### get\_benchmark\_sessions
get\_benchmark\_sessions() -> list\[BenchmarkSession]
Return all benchmark sessions stored for the user. **Returns:** * **Type:** list\[[BenchmarkSession](#benchmarksession)] * Benchmark sessions for the current user, including session ids and any * results fetched so far. Use this after reconnecting to resume in-flight runs. ### get\_benchmark\_classes
get\_benchmark\_classes() -> dict\[BenchmarkClass, BenchmarkClassMetadata]
Return benchmark classes supported by the platform. **Returns:** * **Type:** dict\[[BenchmarkClass](#benchmarkclass), [BenchmarkClassMetadata](#benchmarkclassmetadata)] * Mapping from each supported `BenchmarkClass` to `BenchmarkClassMetadata`, including any known problem-size limits. # Classiq SDK Reference Source: https://docs.classiq.io/sdk-reference/index This reference manual provides a comprehensive guide to the Classiq SDK, which facilitates working with quantum models created using the Qmod language. It serves as a resource for understanding the tools and APIs required to synthesize quantum models into executable programs, visualize their structure and behavior, execute them on quantum hardware or simulators, and retrieve execution results. See the [Qmod Reference](/qmod-reference/index) to learn how to design and define quantum algorithms using the Qmod language. Once your models are ready, use this SDK Reference to transform them into functional quantum applications and analyze their execution outcomes. # Aws Source: https://docs.classiq.io/sdk-reference/providers/AWS For **`emulate`** on Amazon Braket, see [Device emulation (emulate)](../../user-guide/execution/cloud-providers/amazon-backends#device-emulation-emulate) in the execution user guide. ## AwsBackendPreferences AWS-specific backend preferences for quantum computing tasks using Amazon Braket. This class contains configuration options specific to Amazon Braket, including the AWS role ARN, S3 bucket details, and the folder path within the S3 bucket. It extends the base `BackendPreferences` class to provide additional properties required for interaction with Amazon Braket. **Attributes:** | Name | Type | Description | | -------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `backend_service_provider` | `ProviderTypeVendor.AMAZON_BRAKET` | The service provider for the backend, which is Amazon Braket. | | `aws_access_key_id` | `str` | The access key id of AWS user with full braket access | | `aws_secret_access_key` | `str` | The secret key assigned to the access key id for the user with full braket access. | | `s3_bucket_name` | `str` | The name of the S3 bucket where results and other related data will be stored. This field should contain a valid S3 bucket name under your AWS account. | | `s3_folder` | `pydantic_backend.PydanticS3BucketKey` | The folder path within the specified S3 bucket. This allows for organizing results and data under a specific directory within the S3 bucket. | | `emulate` | `bool` | Whether to run the job on a simulator instead of a real quantum device. | # Alice And Bob Source: https://docs.classiq.io/sdk-reference/providers/Alice and Bob ## AliceBobBackendPreferences Backend preferences specific to Alice\&Bob for quantum computing tasks. This class includes configuration options for setting up a backend using Alice\&Bob's quantum hardware. It extends the base `BackendPreferences` class and provides additional parameters required for working with Alice\&Bob's cat qubits, including settings for photon dissipation rates, repetition code distance, and the average number of photons. **Attributes:** | Name | Type | Description | | -------------------------- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `parameters` | `dict[str, Any]` | | | `backend_service_provider` | `ProviderTypeVendor.ALICE_BOB` | The service provider for the backend, which is Alice\&Bob. | | `distance` | `Optional[int]` | The number of times information is duplicated in the repetition code. - **Tooltip**: Phase-flip probability decreases exponentially with this parameter, bit-flip probability increases linearly. - **Supported Values**: 3 to 300, though practical values are usually lower than 30. - **Default**: None. | | `kappa_1` | `Optional[float]` | The rate at which the cat qubit loses one photon, creating a bit-flip. - **Tooltip**: Lower values mean lower error rates. - **Supported Values**: 10 to 10^5. Current hardware is at \~10^3. - **Default**: None. | | `kappa_2` | `Optional[float]` | The rate at which the cat qubit is stabilized using two-photon dissipation. - **Tooltip**: Higher values mean lower error rates. - **Supported Values**: 100 to 10^9. Current hardware is at \~10^5. - **Default**: None. | | `average_nb_photons` | `Optional[float]` | The average number of photons. - **Tooltip**: Bit-flip probability decreases exponentially with this parameter, phase-flip probability increases linearly. - **Supported Values**: 4 to 10^5, though practical values are usually lower than 30. - **Default**: None. | | `api_key` | `str` | The API key required to access Alice\&Bob's quantum hardware. - **Required**: Yes. | ## AliceBobBackendNames Alice & Bob backend names which Classiq Supports running on. **Attributes:** | Name | Type | Description | | ---------------- | ------------------ | ----------- | | `PERFECT_QUBITS` | `'PERFECT_QUBITS'` | | | `LOGICAL_TARGET` | `'LOGICAL_TARGET'` | | | `LOGICAL_EARLY` | `'LOGICAL_EARLY'` | | | `TRANSMONS` | `'TRANSMONS'` | | # Azure Source: https://docs.classiq.io/sdk-reference/providers/Azure For **`emulate`** on Azure Quantum (IonQ `ionq.qpu.*` targets), see [IonQ hardware noise simulation on Azure Quantum (emulate)](../../user-guide/execution/cloud-providers/azure-backends#ionq-hardware-noise-simulation-on-azure-quantum-emulate) in the execution user guide. ## AzureBackendPreferences This class inherits from RunViaClassiqBackendPreferences. This is where you specify Azure Quantum preferences. See usage in the [Azure Backend Documentation](https://docs.classiq.io/latest/sdk-reference/providers/Azure/). **Attributes:** | Name | Type | Description | | ---------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `backend_service_provider` | `ProviderTypeVendor.AZURE_QUANTUM` | | | `location` | `str` | Azure personal resource region. Defaults to `"East US"`. | | `credentials` | `Optional[AzureCredential]` | The service principal credential to access personal quantum workspace. Defaults to `None`. | | `ionq_error_mitigation_flag` | `Optional[bool]` | Error mitigation configuration upon running on IonQ via Azure. Defaults to `False`. | | `emulate` | `bool` | If True, request IonQ hardware noise simulation on Azure Quantum when `backend_name` is an IonQ QPU (`ionq.qpu.*`). If the target is not an IonQ QPU (including `ionq.simulator` and non-IonQ providers), the flag has no effect. Defaults to `False`. | | `run_via_classiq` | `bool` | When omitted, derived from `credentials` (`True` when `credentials` is `None`). | ## AzureCredential Represents the credentials and configuration required to authenticate with Azure services. **Attributes:** | Name | Type | Description | | --------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | `model_config` | | | | `tenant_id` | `str` | Azure Tenant ID used to identify the directory in which the application is registered. | | `client_id` | `str` | Azure Client ID, also known as the application ID, which is used to authenticate the application. | | `client_secret` | `str` | Azure Client Secret associated with the application, used for authentication. | | `resource_id` | `pydantic_backend.PydanticAzureResourceIDType` | Azure Resource ID, including the subscription ID, resource group, and workspace, typically used for personal resources. | ## AzureQuantumBackendNames AzureQuantum backend names which Classiq Supports running on. **Attributes:** | Name | Type | Description | | ------------------------------- | --------------------------------- | ----------- | | `IONQ_ARIA_1` | `'ionq.qpu.aria-1'` | | | `IONQ_ARIA_2` | `'ionq.qpu.aria-2'` | | | `IONQ_QPU` | `'ionq.qpu'` | | | `IONQ_QPU_FORTE` | `'ionq.qpu.forte-1'` | | | `IONQ_SIMULATOR` | `'ionq.simulator'` | | | `MICROSOFT_ESTIMATOR` | `'microsoft.estimator'` | | | `MICROSOFT_FULLSTATE_SIMULATOR` | `'microsoft.simulator.fullstate'` | | | `RIGETTI_SIMULATOR` | `'rigetti.sim.qvm'` | | | `RIGETTI_ANKAA2` | `'rigetti.qpu.ankaa-2'` | | | `RIGETTI_ANKAA9` | `'rigetti.qpu.ankaa-9q-1'` | | | `QCI_MACHINE1` | `'qci.machine1'` | | | `QCI_NOISY_SIMULATOR` | `'qci.simulator.noisy'` | | | `QCI_SIMULATOR` | `'qci.simulator'` | | | `QUANTINUUM_API_VALIDATOR1_1` | `'quantinuum.sim.h1-1sc'` | | | `QUANTINUUM_API_VALIDATOR1_2` | `'quantinuum.sim.h1-2sc'` | | | `QUANTINUUM_API_VALIDATOR2_1` | `'quantinuum.sim.h2-1sc'` | | | `QUANTINUUM_QPU1_1` | `'quantinuum.qpu.h1-1'` | | | `QUANTINUUM_QPU1_2` | `'quantinuum.qpu.h1-2'` | | | `QUANTINUUM_SIMULATOR1_1` | `'quantinuum.sim.h1-1e'` | | | `QUANTINUUM_SIMULATOR1_2` | `'quantinuum.sim.h1-2e'` | | | `QUANTINUUM_QPU2` | `'quantinuum.qpu.h2-1'` | | | `QUANTINUUM_SIMULATOR2` | `'quantinuum.sim.h2-1e'` | | # Braket Source: https://docs.classiq.io/sdk-reference/providers/Braket ## AmazonBraketBackendNames Amazon Braket backend names which Classiq Supports running on. **Attributes:** | Name | Type | Description | | ------------------------ | --------------- | ----------- | | `AMAZON_BRAKET_SV1` | `'SV1'` | | | `AMAZON_BRAKET_TN1` | `'TN1'` | | | `AMAZON_BRAKET_DM1` | `'dm1'` | | | `AMAZON_BRAKET_ASPEN_11` | `'Aspen-11'` | | | `AMAZON_BRAKET_M_1` | `'Aspen-M-1'` | | | `AMAZON_BRAKET_IONQ` | `'IonQ Device'` | | | `AMAZON_BRAKET_LUCY` | `'Lucy'` | | # C12 Source: https://docs.classiq.io/sdk-reference/providers/C12 ## C12BackendPreferences Represents the backend preferences specific to C12. **Attributes:** | Name | Type | Description | | -------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- | | `backend_service_provider` | `ProviderTypeVendor.C12` | | | `parameters` | `dict` | | | `backend_name` | `str` | Name of the requested backend or target. | | `result_format` | `str` | Result format of the job; one of "counts", "state\_vector", or "density\_matrix". Defaults to "counts". | | `inilabel` | `str \| None` | Initial state specified using a binary label format (e.g. "00", "01"). Mutually exclusive with inistatevector. | | `inistatevector` | `str \| None` | Initial state vector as a comma-separated list of complex values (e.g. "1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j"). Mutually exclusive with inilabel. | | `ininoisy` | `bool \| None` | Whether to use noisy initialization of the circuit. | ## C12BackendNames **Attributes:** | Name | Type | Description | | ----------- | ----------------- | ----------- | | `SIMULATOR` | `'c12sim-iswap'` | | | `SQUARED` | `'squared-iswap'` | | # Classiq Source: https://docs.classiq.io/sdk-reference/providers/Classiq ## ClassiqBackendPreferences Represents backend preferences specific to Classiq quantum computing targets. This class is used to configure the backend options for executing quantum circuits on Classiq's platform. The relevant backend names for Classiq targets are specified in `ClassiqSimulatorBackendNames` & `ClassiqNvidiaBackendNames`. **Methods:** | Name | Description | | ----------------------------------------- | ----------- | | [is\_nvidia\_backend](#is_nvidia_backend) | | **Attributes:** | Name | Type | Description | | -------------------------- | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `backend_service_provider` | `ProviderTypeVendor.CLASSIQ` | The provider vendor (Classiq). | | `backend_name` | `str` | Name of the requested backend or target. | | `use_double_precision` | `bool` | When True, Nvidia and Braket Nvidia simulators use double precision; when False (default), they use single precision. Only applies to Nvidia backends; ignored for other Classiq simulators. | | `noise_model` | `str \| None` | Optional named preset simulator noise (see `CLASSIQ_NOISE_MODELS`). Mutually exclusive with `simulator_noise_spec`. | | `simulator_noise_spec` | `ClassiqSimulatorNoiseSpecification \| None` | Optional user-defined noise (see `ClassiqSimulatorNoiseSpecification` in `classiq.interface.backend.simulator_noise` for field semantics and usage). Mutually exclusive with `noise_model`. | ### is\_nvidia\_backend
is\_nvidia\_backend(
self:
) -> bool
**Parameters:** | Name | Type | Description | Default | | ------ | ---- | ----------- | ---------- | | `self` | \`\` | | *required* | ## ClassiqSimulatorBackendNames The simulator backends available in the Classiq provider. **Attributes:** | Name | Type | Description | | -------------------------------- | ---------------------------------- | ----------- | | `SIMULATOR` | `'simulator'` | | | `SIMULATOR_STATEVECTOR` | `'simulator_statevector'` | | | `SIMULATOR_UNITARY` | `'simulator_unitary'` | | | `SIMULATOR_DENSITY_MATRIX` | `'simulator_density_matrix'` | | | `SIMULATOR_MATRIX_PRODUCT_STATE` | `'simulator_matrix_product_state'` | | ## ClassiqNvidiaBackendNames Classiq's Nvidia simulator backend names. **Methods:** | Name | Description | | -------------------------------------------------------- | ----------- | | [is\_braket\_nvidia\_backend](#is_braket_nvidia_backend) | | **Attributes:** | Name | Type | Description | | ------------------------------------- | --------------------------------------- | ----------- | | `SIMULATOR` | `'nvidia_simulator'` | | | `SIMULATOR_STATEVECTOR` | `'nvidia_simulator_statevector'` | | | `SIMULATOR_UNITARY` | `'nvidia_simulator_unitary'` | | | `BRAKET_NVIDIA_SIMULATOR` | `'braket_nvidia_simulator'` | | | `BRAKET_NVIDIA_SIMULATOR_STATEVECTOR` | `'braket_nvidia_simulator_statevector'` | | ### is\_braket\_nvidia\_backend
is\_braket\_nvidia\_backend(
self:
) -> bool
**Parameters:** | Name | Type | Description | Default | | ------ | ---- | ----------- | ---------- | | `self` | \`\` | | *required* | # Gcp Source: https://docs.classiq.io/sdk-reference/providers/GCP ## GCPBackendPreferences Represents the backend preferences specific to Google Cloud Platform (GCP) services. Inherits from `BackendPreferences` and sets the backend service provider to Google. **Methods:** | Name | Description | | ----------------------------------------- | ----------- | | [is\_nvidia\_backend](#is_nvidia_backend) | | **Attributes:** | Name | Type | Description | | -------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | | `use_double_precision` | `bool` | | | `backend_service_provider` | `ProviderTypeVendor.GOOGLE` | Indicates the backend service provider as Google, | | `noise_model` | `str \| None` | Optional named preset simulator noise (see `CLASSIQ_NOISE_MODELS`). Mutually exclusive with `simulator_noise_spec`. | | `simulator_noise_spec` | `ClassiqSimulatorNoiseSpecification \| None` | Optional user-defined noise; same type as on `ClassiqBackendPreferences`. Mutually exclusive with `noise_model`. | ### is\_nvidia\_backend
is\_nvidia\_backend(
self:
) -> bool
**Parameters:** | Name | Type | Description | Default | | ------ | ---- | ----------- | ---------- | | `self` | \`\` | | *required* | # Ibm Source: https://docs.classiq.io/sdk-reference/providers/IBM For **`emulate`** on IBM Quantum, see [IBM hardware noise simulation (emulate)](../../user-guide/execution/cloud-providers/ibm-backends#ibm-hardware-noise-simulation-emulate) in the execution user guide. ## IBMBackendPreferences Represents the backend preferences specific to IBM Quantum services. Inherits from `BackendPreferences` and adds additional fields and validations specific to IBM Quantum backends. **Attributes:** | Name | Type | Description | | -------------------------- | ------------------------------ | --------------------------------------------------------------------------------------------------------------------- | | `backend_service_provider` | `ProviderTypeVendor.IBM_CLOUD` | Indicates the backend service provider as IBM Cloud. | | `access_token` | `Optional[str]` | The IBM Cloud access token to be used with IBM Quantum hosted backends. Defaults to `None`. | | `channel` | `str` | Channel to use for IBM cloud backends. Defaults to `"ibm_cloud"`. | | `instance_crn` | `str` | The IBM Cloud instance CRN (Cloud Resource Name) for the IBM Quantum service. | | `run_via_classiq` | `bool` | Run via Classiq's credentials. Defaults to `False`. | | `emulate` | `bool` | If True, run on a Classiq-hosted simulator with a noise model derived from the IBM backend name. Defaults to `False`. | # Ionq Source: https://docs.classiq.io/sdk-reference/providers/IonQ For **`emulate`** on IonQ (direct API), see [IonQ hardware noise simulation (emulate)](../../user-guide/execution/cloud-providers/ionq-backends#ionq-hardware-noise-simulation-emulate) in the execution user guide. ## IonqBackendPreferences Represents the backend preferences specific to IonQ services. Inherits from `BackendPreferences` and adds additional fields and configurations specific to IonQ backends **Attributes:** | Name | Type | Description | | -------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `parameters` | `dict[str, Any]` | | | `backend_service_provider` | `ProviderTypeVendor.IONQ` | Indicates the backend service provider as IonQ. | | `api_key` | `PydanticIonQApiKeyType` | The IonQ API key required for accessing IonQ's quantum computing services. | | `error_mitigation` | `bool` | A configuration option to enable or disable error mitigation during execution. Defaults to `False`. | | `run_via_classiq` | `bool` | Running via Classiq's credentials while using user's allocated budget. | | `emulate` | `bool` | If True, run on the IonQ simulator with a noise model derived from the backend name (e.g. qpu.aria-1 -> aria-1). Defaults to `False`. | ## IonqBackendNames IonQ backend names which Classiq Supports running on. **Attributes:** | Name | Type | Description | | ----------- | --------------- | ----------- | | `SIMULATOR` | `'simulator'` | | | `HARMONY` | `'qpu.harmony'` | | | `ARIA_1` | `'qpu.aria-1'` | | | `ARIA_2` | `'qpu.aria-2'` | | | `FORTE_1` | `'qpu.forte-1'` | | # Oqc Source: https://docs.classiq.io/sdk-reference/providers/OQC ## OQCBackendPreferences This class inherits from `BackendPreferences`. This is where you specify OQC preferences. **Attributes:** | Name | Type | Description | | -------------------------- | ------------------------ | ------------ | | `backend_service_provider` | `ProviderTypeVendor.OQC` | | | `username` | `str` | OQC username | | `password` | `str` | OQC password | ## OQCBackendNames OQC backend names which Classiq Supports running on. **Attributes:** | Name | Type | Description | | ------ | -------- | ----------- | | `LUCY` | `'Lucy'` | | # Index Source: https://docs.classiq.io/sdk-reference/providers/index # Providers ## Provider This class defines all Providers that Classiq supports. This is mainly used in backend\_preferences when specifying where do we want to execute the defined model. **Attributes:** | Name | Type | Description | | --------------- | ----------------- | ----------- | | `IBM_QUANTUM` | `'IBM Quantum'` | | | `AZURE_QUANTUM` | `'Azure Quantum'` | | | `AMAZON_BRAKET` | `'Amazon Braket'` | | | `IONQ` | `'IonQ'` | | | `CLASSIQ` | `'Classiq'` | | | `GOOGLE` | `'Google'` | | | `ALICE_AND_BOB` | `'Alice & Bob'` | | | `OQC` | `'OQC'` | | | `INTEL` | `'Intel'` | | | `AQT` | `'AQT'` | | | `CINECA` | `'CINECA'` | | | `JHPC` | `'JHPC'` | | | `C12` | `'C12'` | | | `id` | `ProviderIDEnum` | | # Simulator Noise Source: https://docs.classiq.io/sdk-reference/providers/simulator-noise Members: | Name | Description | | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | `DepolarizingNoiseOnGate` | Depolarizing noise applied uniformly to every occurrence of a named gate: the same error statistics on all qubit positions where that gate appears. | | `LocalDepolarizingNoiseOnGate` | Depolarizing noise on a named gate only when it acts on a specific ordered tuple of qubit indices (other placements of the same gate name are unaff... | | `PauliNoiseTerm` | One Pauli operator and its probability within a mixed Pauli noise channel. | | `PauliNoiseOnGate` | A mixed Pauli channel applied uniformly to every occurrence of a named gate (same statistics on all positions). | | `ThermalRelaxationNoiseOnGate` | Single-qubit amplitude-and-phase relaxation over the gate duration `time`. | | `LocalReadoutNoise` | Readout confusion for one simulator qubit using a full 2-by-2 assignment matrix (see module docstring). | | `ClassiqSimulatorNoiseSpecification` | Complete custom noise description for Classiq noisy simulators. | ## DepolarizingNoiseOnGate Depolarizing noise applied uniformly to every occurrence of a named gate: the same error statistics on all qubit positions where that gate appears. **Attributes:** | Name | Type | Description | | ------------- | -------------------------- | ----------- | | `gate` | `str` | | | `probability` | `PydanticProbabilityFloat` | | | `num_qubits` | `Literal[1, 2]` | | ## LocalDepolarizingNoiseOnGate Depolarizing noise on a named gate only when it acts on a specific ordered tuple of qubit indices (other placements of the same gate name are unaffected). **Attributes:** | Name | Type | Description | | -------- | ----------- | ----------- | | `qubits` | `list[int]` | | ## PauliNoiseTerm One Pauli operator and its probability within a mixed Pauli noise channel. **Attributes:** | Name | Type | Description | | ------------- | ------- | ----------- | | `pauli` | `str` | | | `probability` | `float` | | ## PauliNoiseOnGate A mixed Pauli channel applied uniformly to every occurrence of a named gate (same statistics on all positions). Include an identity Pauli term when you want a fraction of executions to stay error-free. **Attributes:** | Name | Type | Description | | ------------- | ---------------------- | ----------- | | `gate` | `str` | | | `num_qubits` | `Literal[1, 2]` | | | `pauli_terms` | `list[PauliNoiseTerm]` | | ## ThermalRelaxationNoiseOnGate Single-qubit amplitude-and-phase relaxation over the gate duration `time`. Use only with single-qubit `gate` names; the channel acts on one tensor factor. **Attributes:** | Name | Type | Description | | -------------------------- | ------- | ----------- | | `gate` | `str` | | | `t1` | `float` | | | `t2` | `float` | | | `time` | `float` | | | `excited_state_population` | `float` | | ## LocalReadoutNoise Readout confusion for one simulator qubit using a full 2-by-2 assignment matrix (see module docstring). **Attributes:** | Name | Type | Description | | -------------------------- | ------------------- | ----------- | | `qubit` | `int` | | | `assignment_probabilities` | `list[list[float]]` | | ## ClassiqSimulatorNoiseSpecification Complete custom noise description for Classiq noisy simulators. Combine any subset of the fields; an empty specification means an ideal (noise-free) simulation. Global readout settings (symmetric bit-flip probability or a shared 2-by-2 assignment matrix for every qubit) cannot both be set. Per-qubit readout in `local_readout_errors` uses the same matrix convention as in `LocalReadoutNoise`. **Attributes:** | Name | Type | Description | | ---------------------------------- | ------------------------------------ | ----------- | | `basis_gates` | `list[str] \| None` | | | `readout_bit_flip_probability` | `PydanticProbabilityFloat \| None` | | | `readout_assignment_probabilities` | `list[list[float]] \| None` | | | `local_readout_errors` | `list[LocalReadoutNoise]` | | | `gate_depolarizing_errors` | `list[DepolarizingNoiseOnGate]` | | | `local_depolarizing_errors` | `list[LocalDepolarizingNoiseOnGate]` | | | `gate_pauli_errors` | `list[PauliNoiseOnGate]` | | | `gate_thermal_relaxation_errors` | `list[ThermalRelaxationNoiseOnGate]` | | # Classical Types Source: https://docs.classiq.io/sdk-reference/qmod/classical-types This is a list of the classical types that are built-in in `Qmod`. For more information regarding classical types see: [classical types](/qmod-reference/language-reference/classical-types). Members: | Name | Description | | ----------------- | ------------------------------------------------------------------------------------------ | | `Pauli` | Enumeration for the Pauli matrices used in quantum computing. | | `PauliTerm` | A term in a Hamiltonian, represented as a product of single-qubit Pauli matrices. | | `IndexedPauli` | A single-qubit Pauli matrix on a specific qubit given by its index. | | `SparsePauliTerm` | A term in the Hamiltonian, represented as a sparse product of single-qubit Pauli matrices. | | `SparsePauliOp` | Represents a collection of sparse Pauli operators. | ## Pauli Enumeration for the Pauli matrices used in quantum computing. Represents the four Pauli matrices used in quantum mechanics: Identity (I), X, Y, and Z operators. The Pauli matrices are defined as: $$ I = \begin{pmatrix} 1 & 0 \\ 0 & 1 \end{pmatrix} $$ $$ X = \begin{pmatrix} 0 & 1 \\ 1 & 0 \end{pmatrix} $$ $$ Y = \begin{pmatrix} 0 & -i \\ i & 0 \end{pmatrix} $$ $$ Z = \begin{pmatrix} 1 & 0 \\ 0 & -1 \end{pmatrix} $$ **Attributes:** | Name | Type | Description | | ---- | ----- | -------------------------------- | | `I` | `'0'` | | | `X` | `'1'` | | | `Y` | `'2'` | | | `Z` | `'3'` | | | `I` | `int` | The identity operator (value 0). | | `X` | `int` | The Pauli-X operator (value 1). | | `Y` | `int` | The Pauli-Y operator (value 2). | | `Z` | `int` | The Pauli-Z operator (value 3). | ## PauliTerm A term in a Hamiltonian, represented as a product of single-qubit Pauli matrices. **Attributes:** | Name | Type | Description | | ------------- | --------------- | ------------------------------------------------------------------------------------- | | `pauli` | `CArray[Pauli]` | The list of the chosen Pauli operators in the term, corresponds to a product of them. | | `coefficient` | `CReal` | The coefficient of the term (floating number). | ## IndexedPauli A single-qubit Pauli matrix on a specific qubit given by its index. **Attributes:** | Name | Type | Description | | ------- | ------- | ----------------------------------------- | | `pauli` | `Pauli` | The Pauli operator. | | `index` | `CInt` | The index of the qubit being operated on. | ## SparsePauliTerm A term in the Hamiltonian, represented as a sparse product of single-qubit Pauli matrices. Attributes: paulis (CArray\[IndexedPauli]): The list of chosen sparse Pauli operators in the term corresponds to a product of them. (See IndexedPauli) coefficient (CReal): The coefficient of the term (floating number). **Methods:** | Name | Description | | -------------------------- | ----------- | | [cmp\_paulis](#cmp_paulis) | | **Attributes:** | Name | Type | Description | | ------------- | ---------------------- | ----------- | | `paulis` | `CArray[IndexedPauli]` | | | `coefficient` | `CReal` | | ### cmp\_paulis
cmp\_paulis(
a: SparsePauliTerm,
b: SparsePauliTerm
) -> int
**Parameters:** | Name | Type | Description | Default | | ---- | ----------------- | ----------- | ---------- | | `a` | `SparsePauliTerm` | | *required* | | `b` | `SparsePauliTerm` | | *required* | ## SparsePauliOp Represents a collection of sparse Pauli operators. **Methods:** | Name | Description | | --------------------------------------------------------- | ----------- | | [get\_tuples\_representation](#get_tuples_representation) | | **Attributes:** | Name | Type | Description | | ------------ | ------------------------- | ----------------------------------------------------------------------------------------------- | | `terms` | `CArray[SparsePauliTerm]` | The list of chosen sparse Pauli terms, corresponds to a product of them. (See: SparsePauliTerm) | | `num_qubits` | `CInt` | The number of qubits in the Hamiltonian. | ### get\_tuples\_representation
get\_tuples\_representation(
self: ,
reverse\_order: bool
) -> list\[tuple\[str, int | float | complex]]
**Parameters:** | Name | Type | Description | Default | | ------------------------------ | ------ | ----------- | ---------- | | `self` | \`\` | | *required* | | `reverse_order` | `bool` | | *required* | | options: | | | | | show\_source: false | | | | | show\_if\_no\_docstring: false | | | | options: show\_source: false show\_if\_no\_docstring: false Members: | Name | Description | | --------------- | ------------------------------------------------------------- | | `SparsePauliOp` | Represents a collection of sparse Pauli operators. | | `Pauli` | Enumeration for the Pauli matrices used in quantum computing. | ## SparsePauliOp Represents a collection of sparse Pauli operators. **Methods:** | Name | Description | | --------------------------------------------------------- | ----------- | | [get\_tuples\_representation](#get_tuples_representation) | | **Attributes:** | Name | Type | Description | | ------------ | ------------------------- | ----------------------------------------------------------------------------------------------- | | `terms` | `CArray[SparsePauliTerm]` | The list of chosen sparse Pauli terms, corresponds to a product of them. (See: SparsePauliTerm) | | `num_qubits` | `CInt` | The number of qubits in the Hamiltonian. | ### get\_tuples\_representation
get\_tuples\_representation(
self: ,
reverse\_order: bool
) -> list\[tuple\[str, int | float | complex]]
**Parameters:** | Name | Type | Description | Default | | --------------- | ------ | ----------- | ---------- | | `self` | \`\` | | *required* | | `reverse_order` | `bool` | | *required* | ## Pauli Enumeration for the Pauli matrices used in quantum computing. Represents the four Pauli matrices used in quantum mechanics: Identity (I), X, Y, and Z operators. The Pauli matrices are defined as: $$ I = \begin{pmatrix} 1 & 0 \\ 0 & 1 \end{pmatrix} $$ $$ X = \begin{pmatrix} 0 & 1 \\ 1 & 0 \end{pmatrix} $$ $$ Y = \begin{pmatrix} 0 & -i \\ i & 0 \end{pmatrix} $$ $$ Z = \begin{pmatrix} 1 & 0 \\ 0 & -1 \end{pmatrix} $$ **Attributes:** | Name | Type | Description | | ------------------------------ | ----- | -------------------------------- | | `I` | `'0'` | | | `X` | `'1'` | | | `Y` | `'2'` | | | `Z` | `'3'` | | | `I` | `int` | The identity operator (value 0). | | `X` | `int` | The Pauli-X operator (value 1). | | `Y` | `int` | The Pauli-Y operator (value 2). | | `Z` | `int` | The Pauli-Z operator (value 3). | | options: | | | | show\_source: false | | | | show\_if\_no\_docstring: false | | | # Allocation Source: https://docs.classiq.io/sdk-reference/qmod/functions/core_library/allocation Functions: | Name | Description | | ---------------------------- | ------------------------------ | | `free` | \[Qmod core-library function]. | | `prepare_state` | \[Qmod core-library function]. | | `prepare_amplitudes` | \[Qmod core-library function]. | | `inplace_prepare_state` | \[Qmod core-library function]. | | `inplace_prepare_amplitudes` | \[Qmod core-library function]. | | `assign_phase_table` | \[Qmod core-library function]. | ### free
free(
in\_: Input\[QArray\[QBit]]
) -> None
\[Qmod core-library function] Releases the qubits allocated to a quantum variable, allowing them to be reused. **Parameters:** | Name | Type | Description | Default | | ----- | --------------------- | -------------------------------------------------------------------- | ---------- | | `in_` | `Input[QArray[QBit]]` | The quantum variable that will be freed. Must be initialized before. | *required* | ### prepare\_state
prepare\_state(
probabilities: CArray\[CReal],
bound: CReal,
out: Output\[QArray\[QBit, Literal\['log(probabilities.len, 2)']]]
) -> None
\[Qmod core-library function] Initializes a quantum variable in a state corresponding to a given probability distribution: $$ \left|\text{out}\right\rangle = \sum_{i=0}^{\text{len(probabilities)}-1} \sqrt{\text{probabilities}[i]} \left|i\right\rangle $$ with $i = 0, 1, 2, ..., \text\{len(amplitudes)\}-1$ corresponding to computational basis states. **Parameters:** | Name | Type | Description | Default | | --------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- | | `probabilities` | `CArray[CReal]` | The probability distribution to initialize the quantum variable. Must be a valid probability distribution, i.e., a list of non-negative real numbers that sum to 1. Must have a valid length (a power of 2). | *required* | | `bound` | `CReal` | An error bound, expressed as the $L^\{2\}$ norm between the expected and actual distributions. A larger bound can reduce the circuit size at the expense of accuracy. Must be a positive real number. | *required* | | `out` | `Output[QArray[QBit, Literal['log(probabilities.len, 2)']]]` | The quantum variable that will receive the initialized state. Must be uninitialized. | *required* | ### prepare\_amplitudes
prepare\_amplitudes(
amplitudes: CArray\[CReal],
bound: CReal,
out: Output\[QArray\[QBit, Literal\['log(amplitudes.len, 2)']]]
) -> None
\[Qmod core-library function] Initializes a quantum variable in a state corresponding to the given amplitudes: $$ \left|\text{out}\right\rangle = \sum_{i=0}^{\text{len(amplitudes)}-1} \text{amplitudes}[i] \left|i\right\rangle $$ with $i = 0, 1, 2, ..., \text\{len(amplitudes)\}-1$ corresponding to computational basis states. **Parameters:** | Name | Type | Description | Default | | ------------ | --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `amplitudes` | `CArray[CReal]` | The amplitudes to initialize the quantum variable. Must be a valid real quantum state vector, i.e., the sum of squares should be 1. Must have a valid length (a power of 2). | *required* | | `bound` | `CReal` | An error bound, expressed as the $L^\{2\}$ norm between the expected and actual distributions. A larger bound can reduce the circuit size at the expense of accuracy. Must be a positive real number. | *required* | | `out` | `Output[QArray[QBit, Literal['log(amplitudes.len, 2)']]]` | The quantum variable that will receive the initialized state. Must be uninitialized. | *required* | ### inplace\_prepare\_state
inplace\_prepare\_state(
probabilities: CArray\[CReal],
bound: CReal,
target: QArray\[QBit, Literal\['log(probabilities.len, 2)']]
) -> None
\[Qmod core-library function] Transforms a given quantum variable in the state `|0>` to the state per the specified probability distribution (similar to `prepare_state` but preformed on an initialized variable). **Parameters:** | Name | Type | Description | Default | | --------------- | ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `probabilities` | `CArray[CReal]` | The probability distribution corresponding to the quantum variable state. Must be a valid probability distribution, i.e., a list of non-negative real numbers that sum to 1. Must have a valid length (a power of 2). | *required* | | `bound` | `CReal` | An error bound, expressed as the $L^\{2\}$ norm between the expected and actual distributions. A larger bound can reduce the circuit size at the expense of accuracy. Must be a positive real number. | *required* | | `target` | `QArray[QBit, Literal['log(probabilities.len, 2)']]` | The quantum variable to act upon. | *required* | ### inplace\_prepare\_amplitudes
inplace\_prepare\_amplitudes(
amplitudes: CArray\[CReal],
bound: CReal,
target: QArray\[QBit, Literal\['log(amplitudes.len, 2)']]
) -> None
\[Qmod core-library function] Transforms a given quantum variable in the state `|0>` to the state per the specified amplitudes (similar to `prepare_amplitudes` but preformed on an initialized variable). **Parameters:** | Name | Type | Description | Default | | ------------ | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `amplitudes` | `CArray[CReal]` | The amplitudes to initialize the quantum variable. Must be a valid real quantum state vector, i.e., the sum of squares should be 1. Must have a valid length (a power of 2). | *required* | | `bound` | `CReal` | An error bound, expressed as the $L^\{2\}$ norm between the expected and actual distributions. A larger bound can reduce the circuit size at the expense of accuracy. Must be a positive real number. | *required* | | `target` | `QArray[QBit, Literal['log(amplitudes.len, 2)']]` | The quantum variable to act upon. | *required* | ### assign\_phase\_table
assign\_phase\_table(
phases: CArray\[CReal],
target: Const\[QArray\[QBit, Literal\['log(phases.len, 2)']]]
) -> None
\[Qmod core-library function] Transforms a given quantum variable in the state `|n>` to $e^\{i phases[n]\}$ `|n>` for all n, where `|n>` are the computational basis states. **Parameters:** | Name | Type | Description | Default | | -------- | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `phases` | `CArray[CReal]` | The phases to rotate the quantum variable. Must be a real vector. Must have a valid length (2 to the power of the number of qubits in the target variable). | *required* | | `target` | `Const[QArray[QBit, Literal['log(phases.len, 2)']]]` | The quantum variable to act upon. | *required* | # Arithmetic Source: https://docs.classiq.io/sdk-reference/qmod/functions/core_library/arithmetic Functions: | Name | Description | | ----------------------------- | ------------------------------ | | `unitary` | \[Qmod core-library function]. | | `multiply` | \[Qmod core-library function]. | | `multiply_constant` | \[Qmod core-library function]. | | `canonical_add` | \[Qmod core-library function]. | | `canonical_add_constant` | \[Qmod core-library function]. | | `canonical_multiply` | \[Qmod core-library function]. | | `canonical_multiply_constant` | \[Qmod core-library function]. | | `canonical_square` | \[Qmod core-library function]. | ### unitary
unitary(
elements: CArray\[CArray\[CReal]],
target: QArray\[QBit, Literal\['log(elements\[0].len, 2)']]
) -> None
\[Qmod core-library function] Applies a unitary matrix on a quantum state. **Parameters:** | Name | Type | Description | Default | | ---------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------- | ---------- | | `elements` | `CArray[CArray[CReal]]` | A 2d array of complex numbers representing the unitary matrix. This matrix must be unitary. | *required* | | `target` | `QArray[QBit, Literal['log(elements[0].len, 2)']]` | The quantum state to apply the unitary on. Should be of corresponding size. | *required* | ### multiply
multiply(
left: Const\[QNum],
right: Const\[QNum],
result: Output\[QNum]
) -> None
\[Qmod core-library function] Multiplies two quantum numeric variables: $$ \left|\text{left}\right\rangle \left|\text{right}\right\rangle \mapsto \left|\text{left}\right\rangle \left|\text{right}\right\rangle \left|\text{left} \cdot \text{right} \right\rangle $$ **Parameters:** | Name | Type | Description | Default | | -------- | -------------- | ------------------------------------------------------- | ---------- | | `left` | `Const[QNum]` | The first argument for the multiplication. | *required* | | `right` | `Const[QNum]` | The second argument for the multiplication. | *required* | | `result` | `Output[QNum]` | The quantum variable to hold the multiplication result. | *required* | ### multiply\_constant
multiply\_constant(
left: CReal,
right: Const\[QNum],
result: Output\[QNum]
) -> None
\[Qmod core-library function] Multiplies a quantum numeric variable with a constant: $$ \left|\text{right}\right\rangle \mapsto \left|\text{right}\right\rangle \left|\text{left} \cdot \text{right} \right\rangle $$ **Parameters:** | Name | Type | Description | Default | | -------- | -------------- | ------------------------------------------------------- | ---------- | | `left` | `CReal` | The constant argument for the multiplication. | *required* | | `right` | `Const[QNum]` | The variable argument for the multiplication. | *required* | | `result` | `Output[QNum]` | The quantum variable to hold the multiplication result. | *required* | ### canonical\_add
canonical\_add(
left: Const\[QArray],
extend\_left: CBool,
right: QArray
) -> None
\[Qmod core-library function] Adds two quantum variables representing integers (signed or unsigned), storing the result in the second variable (in-place): $$ \left|\text{left}\right\rangle \left|\text{right}\right\rangle \mapsto \left|\text{left}\right\rangle \left|\left(\text{right} + \text{left}\right) \bmod 2^{\text{right.size}} \right\rangle $$ **Parameters:** | Name | Type | Description | Default | | ------------- | --------------- | --------------------------------------------------------------- | ---------- | | `left` | `Const[QArray]` | The out-of-place argument for the addition. | *required* | | `extend_left` | `CBool` | Whether to sign-extend the left argument. | *required* | | `right` | `QArray` | The in-place argument for the addition, holds the final result. | *required* | ### canonical\_add\_constant
canonical\_add\_constant(
left: CInt,
right: QArray
) -> None
\[Qmod core-library function] Adds an integer constant to a quantum variable representing an integer (signed or unsigned): $$ \left|\text{right}\right\rangle \mapsto \left|\left(\text{right} + \text{left}\right) \bmod 2^{\text{right.size}} \right\rangle $$ **Parameters:** | Name | Type | Description | Default | | ------- | -------- | -------------------------------------------------------------- | ---------- | | `left` | `CInt` | The constant argument for the addition. | *required* | | `right` | `QArray` | The quantum argument for the addition, holds the final result. | *required* | ### canonical\_multiply
canonical\_multiply(
left: Const\[QArray],
extend\_left: CBool,
right: Const\[QArray],
extend\_right: CBool,
result: QArray,
trim\_result\_lsb: CBool
) -> None
\[Qmod core-library function] Multiplies two quantum variables representing integers (signed or unsigned) into the result variable which is assumed to start in the $|0\rangle$ state. If `trim_result_lsb` is `False`, applies the transformation: $$ \left|\text{left}\right\rangle \left|\text{right}\right\rangle \left|0\right\rangle \mapsto \left|\text{left}\right\rangle \left|\text{right}\right\rangle \left|\left( \text{left} \cdot \text{right} \right) \bmod 2^{\text{result.size}} \right\rangle $$ If `trim_result_lsb` is `True`, the function avoids computing the result's LSB and applies the transformation: $$ \left|\text{left}\right\rangle \left|\text{right}\right\rangle \left|0\right\rangle \mapsto \left|\text{left}\right\rangle \left|\text{right}\right\rangle \left|\left( \text{left} \cdot \text{right} \right) \gg 1 \bmod 2^{\text{result.size}} \right\rangle $$ **Parameters:** | Name | Type | Description | Default | | ----------------- | --------------- | ------------------------------------------------------- | ---------- | | `left` | `Const[QArray]` | The first argument for the multiplication. | *required* | | `extend_left` | `CBool` | Whether to sign-extend the left argument. | *required* | | `right` | `Const[QArray]` | The second argument for the multiplication. | *required* | | `extend_right` | `CBool` | Whether to sign-extend the right argument. | *required* | | `result` | `QArray` | The quantum variable to hold the multiplication result. | *required* | | `trim_result_lsb` | `CBool` | Whether to avoid computing the result's LSB. | *required* | ### canonical\_multiply\_constant
canonical\_multiply\_constant(
left: CInt,
right: Const\[QArray],
extend\_right: CBool,
result: QArray,
trim\_result\_lsb: CBool
) -> None
\[Qmod core-library function] Multiplies a quantum variable representing an integer (signed or unsigned) with a constant, into the result variable which is assumed to start in the $|0\rangle$ state. If `trim_result_lsb` is `False`, applies the transformation: $$ \left|\text{right}\right\rangle \left|0\right\rangle \mapsto \left|\text{right}\right\rangle \left|\left( \text{left} \cdot \text{right} \right) \bmod 2^{\text{result.size}} \right\rangle $$ If `trim_result_lsb` is `True`, the function avoids computing the result's LSB and applies the transformation: $$ \left|\text{right}\right\rangle \left|0\right\rangle \mapsto \left|\text{right}\right\rangle \left|\left( \text{left} \cdot \text{right} \right) \gg 1 \bmod 2^{\text{result.size}} \right\rangle $$ **Parameters:** | Name | Type | Description | Default | | ----------------- | --------------- | ------------------------------------------------------- | ---------- | | `left` | `CInt` | The constant argument for the multiplication. | *required* | | `right` | `Const[QArray]` | The variable argument for the multiplication. | *required* | | `extend_right` | `CBool` | Whether to sign-extend the right argument. | *required* | | `result` | `QArray` | The quantum variable to hold the multiplication result. | *required* | | `trim_result_lsb` | `CBool` | Whether to avoid computing the result's LSB. | *required* | ### canonical\_square
canonical\_square(
arg: Const\[QArray],
extend\_arg: CBool,
result: QArray,
trim\_result\_lsb: CBool
) -> None
\[Qmod core-library function] Squares a quantum variable representing an integer (signed or unsigned), into the result variable which is assumed to start in the $|0\rangle$ state. If `trim_result_lsb` is `False`, applies the transformation: $$ \left|\text{arg}\right\rangle \left|0\right\rangle \mapsto \left|\text{arg}\right\rangle \left|\left( \text{arg}^{2}\right) \bmod 2^{\text{result.size}} \right\rangle $$ If `trim_result_lsb` is `True`, the function avoids computing the result's LSB and applies the transformation: $$ \left|\text{arg}\right\rangle \left|0\right\rangle \mapsto \left|\text{arg}\right\rangle \left|\left( \text{arg}^{2} \right) \gg 1 \bmod 2^{\text{result.size}} \right\rangle $$ **Parameters:** | Name | Type | Description | Default | | ----------------- | --------------- | ------------------------------------------------ | ---------- | | `arg` | `Const[QArray]` | The argument to square. | *required* | | `extend_arg` | `CBool` | Whether to sign-extend the argument. | *required* | | `result` | `QArray` | The quantum variable to hold the squared result. | *required* | | `trim_result_lsb` | `CBool` | Whether to avoid computing the result's LSB. | *required* | # Exponentiation Source: https://docs.classiq.io/sdk-reference/qmod/functions/core_library/exponentiation Functions: | Name | Description | | --------------------------- | ------------------------------ | | `single_pauli_exponent` | \[Qmod core-library function]. | | `commuting_paulis_exponent` | \[Qmod core-library function]. | | `suzuki_trotter` | \[Qmod core-library function]. | | `multi_suzuki_trotter` | \[Qmod core-library function]. | | `sequential_suzuki_trotter` | \[Qmod core-library function]. | | `qdrift` | \[Qmod core-library function]. | | `exponentiate` | \[Qmod core-library function]. | ### single\_pauli\_exponent
single\_pauli\_exponent(
pauli\_string: CArray\[Pauli],
coefficient: CReal,
qbv: QArray\[QBit, Literal\['pauli\_string.len']]
) -> None
\[Qmod core-library function] Exponentiates the specified single Pauli operator multiplied by some coefficient. **Parameters:** | Name | Type | Description | Default | | -------------- | ------------------------------------------- | -------------------------------------------------- | ---------- | | `pauli_string` | `CArray[Pauli]` | The Pauli operator to be exponentiated. | *required* | | `coefficient` | `CReal` | A coefficient multiplying the Pauli operator. | *required* | | `qbv` | `QArray[QBit, Literal['pauli_string.len']]` | The target quantum variable of the exponentiation. | *required* | ### commuting\_paulis\_exponent
commuting\_paulis\_exponent(
pauli\_operator: CArray\[PauliTerm],
evolution\_coefficient: CReal,
qbv: QArray\[QBit, Literal\['pauli\_operator\[0].pauli.len']]
) -> None
\[Qmod core-library function] Exponentiates the specified commutative Pauli operator. As all the Pauli operator's terms commute, the exponential of the whole operator is exactly the product of exponentials of each term. Calling this funciton with a non-commutative Pauli operator will issue an error. **Parameters:** | Name | Type | Description | Default | | ----------------------- | ------------------------------------------------------ | -------------------------------------------------------------- | ---------- | | `pauli_operator` | `CArray[PauliTerm]` | The Pauli operator to be exponentiated. | *required* | | `evolution_coefficient` | `CReal` | A global evolution coefficient multiplying the Pauli operator. | *required* | | `qbv` | `QArray[QBit, Literal['pauli_operator[0].pauli.len']]` | The target quantum variable of the exponentiation. | *required* | ### suzuki\_trotter
suzuki\_trotter(
pauli\_operator: SparsePauliOp,
evolution\_coefficient: CReal,
order: CInt,
repetitions: CInt,
qbv: QArray\[QBit]
) -> None
\[Qmod core-library function] Applies the Suzuki-Trotter decomposition to a Pauli operator. The Suzuki-Trotter decomposition is a method for approximating the exponential of a sum of operators by a product of exponentials of each operator. The Suzuki-Trotter decomposition of a given order nullifies the error of the Taylor series expansion of the product of exponentials up to that order. The error of a Suzuki-Trotter decomposition decreases as the order and number of repetitions increase. **Parameters:** | Name | Type | Description | Default | | ----------------------- | --------------- | -------------------------------------------------------------- | ---------- | | `pauli_operator` | `SparsePauliOp` | The Pauli operator to be exponentiated. | *required* | | `evolution_coefficient` | `CReal` | A global evolution coefficient multiplying the Pauli operator. | *required* | | `order` | `CInt` | The order of the Suzuki-Trotter decomposition. | *required* | | `repetitions` | `CInt` | The number of repetitions of the Suzuki-Trotter decomposition. | *required* | | `qbv` | `QArray[QBit]` | The target quantum variable of the exponentiation. | *required* | ### multi\_suzuki\_trotter
multi\_suzuki\_trotter(
hamiltonians: CArray\[SparsePauliOp],
evolution\_coefficients: CArray\[CReal, Literal\['hamiltonians.len']],
order: CInt,
repetitions: CInt,
qbv: QArray
) -> None
\[Qmod core-library function] Applies the Suzuki-Trotter decomposition jointly to a sum of Hamiltonians (represented as Pauli operators), each with its separate evolution coefficient, approximating $\exp\{-i(H_1t_1+H_2t_2+\dots)\}$ with a specified order and number of repetitions. The Suzuki-Trotter decomposition is a method for approximating the exponential of a sum of operators by a product of exponentials of each operator. The Suzuki-Trotter decomposition of a given order nullifies the error of the Taylor series expansion of the product of exponentials up to that order. The error of a Suzuki-Trotter decomposition decreases as the order and number of repetitions increase. **Parameters:** | Name | Type | Description | Default | | ------------------------ | -------------------------------------------- | --------------------------------------------------------------- | ---------- | | `hamiltonians` | `CArray[SparsePauliOp]` | The hamiltonians to be exponentiated, in sparse representation. | *required* | | `evolution_coefficients` | `CArray[CReal, Literal['hamiltonians.len']]` | The hamiltonian coefficients (can be link-time). | *required* | | `order` | `CInt` | The order of the Suzuki-Trotter decomposition. | *required* | | `repetitions` | `CInt` | The number of repetitions of the Suzuki-Trotter decomposition. | *required* | | `qbv` | `QArray` | The target quantum variable of the exponentiation. | *required* | ### sequential\_suzuki\_trotter
sequential\_suzuki\_trotter(
hamiltonians: CArray\[SparsePauliOp],
evolution\_coefficients: CArray\[CReal, Literal\['hamiltonians.len']],
order: CInt,
repetitions: CInt,
qbv: QArray
) -> None
\[Qmod core-library function] Applies the Suzuki-Trotter decomposition jointly to a sum of Hamiltonians (represented as Pauli operators), each with its separate evolution coefficient, approximating $\exp\{-i(H_1t_1+H_2t_2+\dots)\}$ with a specified order and number of repetitions. Does not reorder the Pauli terms. The Suzuki-Trotter decomposition is a method for approximating the exponential of a sum of operators by a product of exponentials of each operator. The Suzuki-Trotter decomposition of a given order nullifies the error of the Taylor series expansion of the product of exponentials up to that order. The error of a Suzuki-Trotter decomposition decreases as the order and number of repetitions increase. **Parameters:** | Name | Type | Description | Default | | ------------------------ | -------------------------------------------- | --------------------------------------------------------------- | ---------- | | `hamiltonians` | `CArray[SparsePauliOp]` | The hamiltonians to be exponentiated, in sparse representation. | *required* | | `evolution_coefficients` | `CArray[CReal, Literal['hamiltonians.len']]` | The hamiltonian coefficients (can be link-time). | *required* | | `order` | `CInt` | The order of the Suzuki-Trotter decomposition. | *required* | | `repetitions` | `CInt` | The number of repetitions of the Suzuki-Trotter decomposition. | *required* | | `qbv` | `QArray` | The target quantum variable of the exponentiation. | *required* | ### qdrift
qdrift(
pauli\_operator: SparsePauliOp,
evolution\_coefficient: CReal,
num\_qdrift: CInt,
qbv: QArray\[QBit, Literal\['pauli\_operator.num\_qubits']]
) -> None
\[Qmod core-library function] Exponentiates a Pauli operator using the QDrift method. The QDrift method is a stochastic method based on the Trotter decomposition for approximating the exponential of a sum of operators by a product of exponentials of each operator. The QDrift method randomizes the order of the operators in the product of exponentials to stochastically reduce the error of the approximation. The error of the QDrift method decreases as the number of QDrift steps increases. **Parameters:** | Name | Type | Description | Default | | ----------------------- | ---------------------------------------------------- | -------------------------------------------------------------- | ---------- | | `pauli_operator` | `SparsePauliOp` | The Pauli operator to be exponentiated. | *required* | | `evolution_coefficient` | `CReal` | A global evolution coefficient multiplying the Pauli operator. | *required* | | `num_qdrift` | `CInt` | | *required* | | `qbv` | `QArray[QBit, Literal['pauli_operator.num_qubits']]` | The target quantum variable of the exponentiation. | *required* | ### exponentiate
exponentiate(
hamiltonian: SparsePauliOp,
evolution\_coefficient: CReal,
qbv: QArray\[QBit]
) -> None
\[Qmod core-library function] Exponentiates a Pauli operator. **Parameters:** | Name | Type | Description | Default | | ----------------------- | --------------- | ---------------------------------------------------- | ---------- | | `hamiltonian` | `SparsePauliOp` | The Pauli operator to be exponentiated. | *required* | | `evolution_coefficient` | `CReal` | A global coefficient multiplying the Pauli operator. | *required* | | `qbv` | `QArray[QBit]` | The target quantum variable of the exponentiation. | *required* | # Gray Code Source: https://docs.classiq.io/sdk-reference/qmod/functions/core_library/gray_code Functions: | Name | Description | | ------------------- | ------------------------------ | | `select_rotation` | \[Qmod core-library function]. | | `select_z_rotation` | \[Qmod core-library function]. | ### select\_rotation
select\_rotation(
basis: Pauli,
angles: CArray\[CReal],
selector: Const\[QNum],
target: QBit
) -> None
\[Qmod core-library function] Applies a set of controlled single-qubit rotations to a target qubit, using a specified rotation axis and a list of rotation angles. **Parameters:** | Name | Type | Description | Default | | ---------- | --------------- | ------------------------------------------------------------------------------------- | ---------- | | `basis` | `Pauli` | The Pauli operator defining the rotation axis. | *required* | | `angles` | `CArray[CReal]` | The list of rotation angles in radians. The list must have length 2\*\*selector.size. | *required* | | `selector` | `Const[QNum]` | The qubits that act as the selection register. | *required* | | `target` | `QBit` | The qubit on which the selected rotation is applied. | *required* | ### select\_z\_rotation
select\_z\_rotation(
angles: CArray\[CReal],
selector: Const\[QNum],
target: Const\[QBit]
) -> None
\[Qmod core-library function] Applies a set of controlled single-qubit Z rotations to a target qubit, using a list of rotation angles. **Parameters:** | Name | Type | Description | Default | | ---------- | --------------- | ------------------------------------------------------------------------------------- | ---------- | | `angles` | `CArray[CReal]` | The list of rotation angles in radians. The list must have length 2\*\*selector.size. | *required* | | `selector` | `Const[QNum]` | The qubits that act as the selection register. | *required* | | `target` | `Const[QBit]` | The qubit on which the selected rotation is applied. | *required* | # Mid Circuit Measurement Source: https://docs.classiq.io/sdk-reference/qmod/functions/core_library/mid_circuit_measurement Functions: | Name | Description | | ------- | -------------------------------------------- | | `RESET` | Resets the target qubit to the `\|0>` state. | ### RESET
RESET(
target: QBit
) -> None
Resets the target qubit to the `|0>` state. Performed by measuring the qubit and applying an X gate if necessary. **Parameters:** | Name | Type | Description | Default | | -------- | ------ | ------------------ | ---------- | | `target` | `QBit` | the qubit to reset | *required* | # Standard Gates Source: https://docs.classiq.io/sdk-reference/qmod/functions/core_library/standard_gates Functions: | Name | Description | | ---------- | ------------------------------ | | `H` | \[Qmod core-library function]. | | `X` | \[Qmod core-library function]. | | `Y` | \[Qmod core-library function]. | | `Z` | \[Qmod core-library function]. | | `I` | \[Qmod core-library function]. | | `S` | \[Qmod core-library function]. | | `T` | \[Qmod core-library function]. | | `SDG` | \[Qmod core-library function]. | | `TDG` | \[Qmod core-library function]. | | `PHASE` | \[Qmod core-library function]. | | `RX` | \[Qmod core-library function]. | | `RY` | \[Qmod core-library function]. | | `RZ` | \[Qmod core-library function]. | | `R` | \[Qmod core-library function]. | | `RXX` | \[Qmod core-library function]. | | `RYY` | \[Qmod core-library function]. | | `RZZ` | \[Qmod core-library function]. | | `CH` | \[Qmod core-library function]. | | `CX` | \[Qmod core-library function]. | | `CY` | \[Qmod core-library function]. | | `CZ` | \[Qmod core-library function]. | | `CRX` | \[Qmod core-library function]. | | `CRY` | \[Qmod core-library function]. | | `CRZ` | \[Qmod core-library function]. | | `CPHASE` | \[Qmod core-library function]. | | `SWAP` | \[Qmod core-library function]. | | `IDENTITY` | \[Qmod core-library function]. | | `U` | \[Qmod core-library function]. | | `CCX` | \[Qmod core-library function]. | ### H
H(
target: QBit
) -> None
\[Qmod core-library function] Performs the Hadamard gate on a qubit. This operation is represented by the following matrix: $$ H = \frac{1}{\sqrt{2}} \begin{bmatrix} 1 & 1 \\ 1 & -1 \end{bmatrix} $$ **Parameters:** | Name | Type | Description | Default | | -------- | ------ | ---------------------------------------- | ---------- | | `target` | `QBit` | The qubit to apply the Hadamard gate to. | *required* | ### X
X(
target: QBit
) -> None
\[Qmod core-library function] Performs the Pauli-X gate on a qubit. This operation is represented by the following matrix: $$ X = \begin{bmatrix} 0 & 1 \\ 1 & 0 \end{bmatrix} $$ **Parameters:** | Name | Type | Description | Default | | -------- | ------ | --------------------------------------- | ---------- | | `target` | `QBit` | The qubit to apply the Pauli-X gate to. | *required* | ### Y
Y(
target: QBit
) -> None
\[Qmod core-library function] Performs the Pauli-Y gate on a qubit. This operation is represented by the following matrix: $$ Y = \begin{bmatrix} 0 & -i \\ i & 0 \end{bmatrix} $$ **Parameters:** | Name | Type | Description | Default | | -------- | ------ | --------------------------------------- | ---------- | | `target` | `QBit` | The qubit to apply the Pauli-Y gate to. | *required* | ### Z
Z(
target: Const\[QBit]
) -> None
\[Qmod core-library function] Performs the Pauli-Z gate on a qubit. This operation is represented by the following matrix: $$ Z = \begin{bmatrix} 1 & 0 \\ 0 & -1 \end{bmatrix} $$ **Parameters:** | Name | Type | Description | Default | | -------- | ------------- | --------------------------------------- | ---------- | | `target` | `Const[QBit]` | The qubit to apply the Pauli-Z gate to. | *required* | ### I
I(
target: Const\[QBit]
) -> None
\[Qmod core-library function] Performs the identity gate on a qubit. This operation is represented by the following matrix: $$ I = \begin{bmatrix} 1 & 0 \\ 0 & 1 \end{bmatrix} $$ **Parameters:** | Name | Type | Description | Default | | -------- | ------------- | ---------------------------------------- | ---------- | | `target` | `Const[QBit]` | The qubit to apply the identity gate to. | *required* | ### S
S(
target: Const\[QBit]
) -> None
\[Qmod core-library function] Performs the S gate on a qubit. This operation is represented by the following matrix: $$ S = \begin{bmatrix} 1 & 0 \\ 0 & i \end{bmatrix} $$ **Parameters:** | Name | Type | Description | Default | | -------- | ------------- | --------------------------------- | ---------- | | `target` | `Const[QBit]` | The qubit to apply the S gate to. | *required* | ### T
T(
target: Const\[QBit]
) -> None
\[Qmod core-library function] Performs the T gate on a qubit. This operation is represented by the following matrix: $$ T = \begin{bmatrix} 1 & 0 \\ 0 & e^{i\frac{\pi}{4}} \end{bmatrix} $$ **Parameters:** | Name | Type | Description | Default | | -------- | ------------- | --------------------------------- | ---------- | | `target` | `Const[QBit]` | The qubit to apply the T gate to. | *required* | ### SDG
SDG(
target: Const\[QBit]
) -> None
\[Qmod core-library function] Performs the S-dagger gate on a qubit. This operation is represented by the following matrix: $$ S^\dagger = \begin{bmatrix} 1 & 0 \\ 0 & -i \end{bmatrix} $$ **Parameters:** | Name | Type | Description | Default | | -------- | ------------- | ---------------------------------------- | ---------- | | `target` | `Const[QBit]` | The qubit to apply the S-dagger gate to. | *required* | ### TDG
TDG(
target: Const\[QBit]
) -> None
\[Qmod core-library function] Performs the T-dagger gate on a qubit. This operation is represented by the following matrix: $$ T^\dagger = \begin{bmatrix} 1 & 0 \\ 0 & e^{-i\frac{\pi}{4}} \end{bmatrix} $$ **Parameters:** | Name | Type | Description | Default | | -------- | ------------- | ---------------------------------------- | ---------- | | `target` | `Const[QBit]` | The qubit to apply the T-dagger gate to. | *required* | ### PHASE
PHASE(
theta: CReal,
target: Const\[QBit]
) -> None
\[Qmod core-library function] Performs the phase gate on a qubit. This operation is represented by the following matrix: $$ PHASE(\theta) = \begin{bmatrix} 1 & 0 \\ 0 & e^{i\theta} \end{bmatrix} $$ **Parameters:** | Name | Type | Description | Default | | -------- | ------------- | ------------------------------------- | ---------- | | `theta` | `CReal` | The phase angle in radians. | *required* | | `target` | `Const[QBit]` | The qubit to apply the phase gate to. | *required* | ### RX
RX(
theta: CReal,
target: QBit
) -> None
\[Qmod core-library function] Performs the Pauli-X rotation gate on a qubit. This operation is represented by the following matrix: $$ R_X(\theta) = e^{-i\frac{\theta}{2}X} = \begin{bmatrix} cos(\frac{\theta}{2}) & -i sin(\frac{\theta}{2}) \\ -i sin(\frac{\theta}{2}) & cos(\frac{\theta}{2}) \end{bmatrix} $$ **Parameters:** | Name | Type | Description | Default | | -------- | ------- | ------------------------------------------------ | ---------- | | `theta` | `CReal` | The rotation angle in radians. | *required* | | `target` | `QBit` | The qubit to apply the Pauli-X rotation gate to. | *required* | ### RY
RY(
theta: CReal,
target: QBit
) -> None
\[Qmod core-library function] Performs the Pauli-Y rotation gate on a qubit. This operation is represented by the following matrix: $$ R_Y(\theta) = e^{-i\frac{\theta}{2}Y} = \begin{bmatrix} cos(\frac{\theta}{2}) & -sin(\frac{\theta}{2}) \\ sin(\frac{\theta}{2}) & cos(\frac{\theta}{2}) \end{bmatrix} $$ **Parameters:** | Name | Type | Description | Default | | -------- | ------- | ------------------------------------------------ | ---------- | | `theta` | `CReal` | The rotation angle in radians. | *required* | | `target` | `QBit` | The qubit to apply the Pauli-Y rotation gate to. | *required* | ### RZ
RZ(
theta: CReal,
target: Const\[QBit]
) -> None
\[Qmod core-library function] Performs the Pauli-Z rotation gate on a qubit. This operation is represented by the following matrix: $$ R_Z(\theta) = e^{-i\frac{\theta}{2}Z} = \begin{bmatrix} e^{-i\frac{\theta}{2}} & 0 \\ 0 & e^{i\frac{\theta}{2}} \end{bmatrix} $$ **Parameters:** | Name | Type | Description | Default | | -------- | ------------- | ------------------------------------------------ | ---------- | | `theta` | `CReal` | The rotation angle in radians. | *required* | | `target` | `Const[QBit]` | The qubit to apply the Pauli-Z rotation gate to. | *required* | ### R
R(
theta: CReal,
phi: CReal,
target: QBit
) -> None
\[Qmod core-library function] Performs a rotation of $\theta$ around the $cos(\phi)\hat\{x\} + sin(\phi)\hat\{y\}$ axis on a qubit. This operation is represented by the following matrix: $$ R(\theta, \phi) = e^{-i \frac{\theta}{2} (cos(\phi)X + sin(\phi)Y)} = \begin{bmatrix} cos(\frac{\theta}{2}) & -i e^{-i\phi} sin(\frac{\theta}{2}) \\ -i e^{i\phi} sin(\frac{\theta}{2}) & cos(\frac{\theta}{2}) \end{bmatrix} $$ **Parameters:** | Name | Type | Description | Default | | -------- | ------- | ------------------------------------------------------------- | ---------- | | `theta` | `CReal` | The rotation angle in radians. | *required* | | `phi` | `CReal` | The phase angle in radians. | *required* | | `target` | `QBit` | The qubit to apply the general single-qubit rotation gate to. | *required* | ### RXX
RXX(
theta: CReal,
target: QArray\[QBit, Literal\[2]]
) -> None
\[Qmod core-library function] Performs the XX rotation gate on a pair of qubits. This operation is represented by the following matrix: $$ R_{XX}(\theta) = e^{-i\frac{\theta}{2}X \otimes X} = \begin{bmatrix} cos(\frac{\theta}{2}) & 0 & 0 & -i sin(\frac{\theta}{2}) \\ 0 & cos(\frac{\theta}{2}) & -i sin(\frac{\theta}{2}) & 0 \\ 0 & -i sin(\frac{\theta}{2}) & cos(\frac{\theta}{2}) & 0 \\ -i sin(\frac{\theta}{2}) & 0 & 0 & cos(\frac{\theta}{2}) \end{bmatrix} $$ **Parameters:** | Name | Type | Description | Default | | -------- | -------------------------- | ---------------------------------------------------- | ---------- | | `theta` | `CReal` | The rotation angle in radians. | *required* | | `target` | `QArray[QBit, Literal[2]]` | The pair of qubits to apply the XX rotation gate to. | *required* | ### RYY
RYY(
theta: CReal,
target: QArray\[QBit, Literal\[2]]
) -> None
\[Qmod core-library function] Performs the YY rotation gate on a pair of qubits. This operation is represented by the following matrix: $$ R_{YY}(\theta) = e^{-i\frac{\theta}{2}Y \otimes Y} = \begin{bmatrix} cos(\frac{\theta}{2}) & 0 & 0 & -sin(\frac{\theta}{2}) \\ 0 & cos(\frac{\theta}{2}) & sin(\frac{\theta}{2}) & 0 \\ 0 & sin(\frac{\theta}{2}) & cos(\frac{\theta}{2}) & 0 \\ -sin(\frac{\theta}{2}) & 0 & 0 & cos(\frac{\theta}{2}) \end{bmatrix} $$ **Parameters:** | Name | Type | Description | Default | | -------- | -------------------------- | ---------------------------------------------------- | ---------- | | `theta` | `CReal` | The rotation angle in radians. | *required* | | `target` | `QArray[QBit, Literal[2]]` | The pair of qubits to apply the YY rotation gate to. | *required* | ### RZZ
RZZ(
theta: CReal,
target: Const\[QArray\[QBit, Literal\[2]]]
) -> None
\[Qmod core-library function] Performs the ZZ rotation gate on a pair of qubits. This operation is represented by the following matrix: $$ R_{ZZ}(\theta) = e^{-i\frac{\theta}{2}Z \otimes Z} = \begin{bmatrix} e^{-i\frac{\theta}{2}} & 0 & 0 & 0 \\ 0 & e^{i\frac{\theta}{2}} & 0 & 0 \\ 0 & 0 & e^{i\frac{\theta}{2}} & 0 \\ 0 & 0 & 0 & e^{-i\frac{\theta}{2}} \end{bmatrix} $$ **Parameters:** | Name | Type | Description | Default | | -------- | --------------------------------- | ---------------------------------------------------- | ---------- | | `theta` | `CReal` | The rotation angle in radians. | *required* | | `target` | `Const[QArray[QBit, Literal[2]]]` | The pair of qubits to apply the ZZ rotation gate to. | *required* | ### CH
CH(
ctrl: Const\[QBit],
target: QBit
) -> None
\[Qmod core-library function] Applies the Hadamard gate to the target qubit, conditioned on the control qubit. This operation is represented by the following matrix: $$ CH = \frac{1}{\sqrt{2}} \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 1 \\ 0 & 0 & 1 & -1 \end{bmatrix} $$ **Parameters:** | Name | Type | Description | Default | | -------- | ------------- | ---------------------------------------- | ---------- | | `ctrl` | `Const[QBit]` | The control qubit. | *required* | | `target` | `QBit` | The qubit to apply the Hadamard gate on. | *required* | ### CX
CX(
ctrl: Const\[QBit],
target: QBit
) -> None
\[Qmod core-library function] Applies the Pauli-X gate to the target qubit, conditioned on the control qubit. This operation is represented by the following matrix: $$ CX = \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 1 \\ 0 & 0 & 1 & 0 \end{bmatrix} $$ **Parameters:** | Name | Type | Description | Default | | -------- | ------------- | --------------------------------------- | ---------- | | `ctrl` | `Const[QBit]` | The control qubit. | *required* | | `target` | `QBit` | The qubit to apply the Pauli-X gate on. | *required* | ### CY
CY(
ctrl: Const\[QBit],
target: QBit
) -> None
\[Qmod core-library function] Applies the Pauli-Y gate to the target qubit, conditioned on the control qubit. This operation is represented by the following matrix: $$ CY = \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & -i \\ 0 & 0 & i & 0 \end{bmatrix} $$ **Parameters:** | Name | Type | Description | Default | | -------- | ------------- | --------------------------------------- | ---------- | | `ctrl` | `Const[QBit]` | The control qubit. | *required* | | `target` | `QBit` | The qubit to apply the Pauli-Y gate on. | *required* | ### CZ
CZ(
ctrl: Const\[QBit],
target: Const\[QBit]
) -> None
\[Qmod core-library function] Applies the Pauli-Z gate to the target qubit, conditioned on the control qubit. This operation is represented by the following matrix: $$ CZ = \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & -1 \end{bmatrix} $$ **Parameters:** | Name | Type | Description | Default | | -------- | ------------- | --------------------------------------- | ---------- | | `ctrl` | `Const[QBit]` | The control qubit. | *required* | | `target` | `Const[QBit]` | The qubit to apply the Pauli-Z gate on. | *required* | ### CRX
CRX(
theta: CReal,
ctrl: Const\[QBit],
target: QBit
) -> None
\[Qmod core-library function] Applies the RX gate to the target qubit, conditioned on the control qubit. This operation is represented by the following matrix: $$ CRX = \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & cos(\frac{\theta}{2}) & -i*sin(\frac{\theta}{2}) \\ 0 & 0 & -i*sin(\frac{\theta}{2}) & cos(\frac{\theta}{2}) \end{bmatrix} $$ **Parameters:** | Name | Type | Description | Default | | -------- | ------------- | ---------------------------------- | ---------- | | `theta` | `CReal` | The rotation angle in radians. | *required* | | `ctrl` | `Const[QBit]` | The control qubit. | *required* | | `target` | `QBit` | The qubit to apply the RX gate on. | *required* | ### CRY
CRY(
theta: CReal,
ctrl: Const\[QBit],
target: QBit
) -> None
\[Qmod core-library function] Applies the RY gate to the target qubit, conditioned on the control qubit. This operation is represented by the following matrix: $$ CRY = \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & cos(\frac{\theta}{2}) & -sin(\frac{\theta}{2}) \\ 0 & 0 & sin(\frac{\theta}{2}) & cos(\frac{\theta}{2}) \end{bmatrix} $$ **Parameters:** | Name | Type | Description | Default | | -------- | ------------- | ---------------------------------- | ---------- | | `theta` | `CReal` | The rotation angle in radians. | *required* | | `ctrl` | `Const[QBit]` | The control qubit. | *required* | | `target` | `QBit` | The qubit to apply the RY gate on. | *required* | ### CRZ
CRZ(
theta: CReal,
ctrl: Const\[QBit],
target: Const\[QBit]
) -> None
\[Qmod core-library function] Applies the RZ gate to the target qubit, conditioned on the control qubit. This operation is represented by the following matrix: $$ CRZ = \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & e^{-i\frac{\theta}{2}} & 0 \\ 0 & 0 & 0 & e^{i\frac{\theta}{2}} \end{bmatrix} $$ **Parameters:** | Name | Type | Description | Default | | -------- | ------------- | ---------------------------------- | ---------- | | `theta` | `CReal` | The rotation angle in radians. | *required* | | `ctrl` | `Const[QBit]` | The control qubit. | *required* | | `target` | `Const[QBit]` | The qubit to apply the RZ gate on. | *required* | ### CPHASE
CPHASE(
theta: CReal,
ctrl: Const\[QBit],
target: Const\[QBit]
) -> None
\[Qmod core-library function] Applies the PHASE gate to the target qubit, conditioned on the control qubit. This operation is represented by the following matrix: $$ CPHASE = \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & e^{i\theta} \end{bmatrix} $$ **Parameters:** | Name | Type | Description | Default | | -------- | ------------- | ------------------------------------- | ---------- | | `theta` | `CReal` | The rotation angle in radians. | *required* | | `ctrl` | `Const[QBit]` | The control qubit. | *required* | | `target` | `Const[QBit]` | The qubit to apply the PHASE gate on. | *required* | ### SWAP
SWAP(
qbit0: QBit,
qbit1: QBit
) -> None
\[Qmod core-library function] Swaps the states of two qubits. This operation is represented by the following matrix: $$ SWAP = \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} $$ **Parameters:** | Name | Type | Description | Default | | ------- | ------ | ----------------- | ---------- | | `qbit0` | `QBit` | The first qubit. | *required* | | `qbit1` | `QBit` | The second qubit. | *required* | ### IDENTITY
IDENTITY(
target: Const\[QArray\[QBit]]
) -> None
\[Qmod core-library function] Does nothing. This operation is represented by the following matrix: $$ IDENTITY = {\begin{bmatrix} 1 & 0 \\ 0 & 1 \end{bmatrix}} ^{\otimes n} $$ **Parameters:** | Name | Type | Description | Default | | -------- | --------------------- | ----------------------------------------- | ---------- | | `target` | `Const[QArray[QBit]]` | The qubits to apply the IDENTITY gate on. | *required* | ### U
U(
theta: CReal,
phi: CReal,
lam: CReal,
gam: CReal,
target: QBit
) -> None
\[Qmod core-library function] Performs a general single-qubit unitary gate that applies phase and rotation with three Euler angles on a qubit. This operation is represented by the following matrix: $$ U(\theta, \phi, \lambda, \gamma) = e^{i \gamma} \begin{bmatrix} cos(\theta/2) & -e^{i(\lambda)} sin(\theta/2) \\ e^{i\phi} sin(\theta/2) & e^{i(\phi + \lambda)} cos(\theta/2) \end{bmatrix} $$ **Parameters:** | Name | Type | Description | Default | | -------- | ------- | ------------------------------------------------------------ | ---------- | | `theta` | `CReal` | The first Euler angle in radians. | *required* | | `phi` | `CReal` | The second Euler angle in radians. | *required* | | `lam` | `CReal` | The third Euler angle in radians. | *required* | | `gam` | `CReal` | The global phase angle in radians. | *required* | | `target` | `QBit` | The qubit to apply the general single-qubit unitary gate to. | *required* | ### CCX
CCX(
ctrl: Const\[QArray\[QBit, Literal\[2]]],
target: QBit
) -> None
\[Qmod core-library function] Applies the Pauli-X gate to the target qubit, conditioned on the two control qubits (Toffoli). This operation is represented by the following matrix: $$ CCX = \begin{bmatrix} 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 1 \\ 0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 \end{bmatrix} $$ **Parameters:** | Name | Type | Description | Default | | -------- | --------------------------------- | --------------------------------------------------- | ---------- | | `ctrl` | `Const[QArray[QBit, Literal[2]]]` | The control qubits. | *required* | | `target` | `QBit` | The qubit to apply the conditioned Pauli-X gate on. | *required* | # Amplitude Amplification Source: https://docs.classiq.io/sdk-reference/qmod/functions/open_library/amplitude_amplification Functions: | Name | Description | | ------------------------------- | --------------------------------- | | `amplitude_amplification` | \[Qmod Classiq-library function]. | | `exact_amplitude_amplification` | \[Qmod Classiq-library function]. | ### amplitude\_amplification
amplitude\_amplification(
reps: CInt,
oracle: QCallable\[QArray\[QBit]],
space\_transform: QCallable\[QArray\[QBit]],
packed\_qvars: QArray\[QBit]
) -> None
\[Qmod Classiq-library function] Applies the Amplitude Amplification algorithm; Prepares a state using the given `space_transform` function, and applies `reps` repetititions of the grover operator, using the given `oracle` functions which marks the "good" states. **Parameters:** | Name | Type | Description | Default | | ----------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `reps` | `CInt` | Number of repetitions to apply the grover operator on the initial state. Should be determined by the user, according to the calculated amplification. | *required* | | `oracle` | `QCallable[QArray[QBit]]` | The oracle operator that marks the "good" states. This operator should flip the sign of the amplitude of the "good" state. | *required* | | `space_transform` | `QCallable[QArray[QBit]]` | The space transform operator (which is known also the state preparation operator). First applied to prepare the state before the amplification, then used inside the Grover operator. | *required* | | `packed_qvars` | `QArray[QBit]` | | *required* | ### exact\_amplitude\_amplification
exact\_amplitude\_amplification(
amplitude: CReal,
oracle: QCallable\[QArray\[QBit]],
space\_transform: QCallable\[QArray\[QBit]],
packed\_qvars: QArray\[QBit]
) -> None
\[Qmod Classiq-library function] Applies an exact version of the Amplitude Amplification algorithm, assuming knowledge of the amplitude of the marked state. The function should be applied on the zero state, and it takes care for preparing the initial state before amplification using the `space_transform`. Based on the algorithm in [Quantum state preparation without coherent arithmetic](https://arxiv.org/abs/2210.14892). Assuming the `space_transform` creates a state $|\psi\rangle = a|\psi_\{good\}\rangle + \sqrt\{1-a\}|\psi_\{bad\}\rangle$, given `a` as the `amplitude` argument, the function will load exactly the state $|\psi_\{good\}\rangle$. Note: if the `amplitude` argument is not exact, the resulting state will not be exactly $|\psi_\{good\}\rangle$, and there will be additional internal auxilliary of the function that is not released correctly. **Parameters:** | Name | Type | Description | Default | | ----------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `amplitude` | `CReal` | The amplitude of the state $\|\psi_\{good\}\rangle$ with regards to the initial state prepared by `space_transform`. | *required* | | `oracle` | `QCallable[QArray[QBit]]` | The oracle operator that marks the "good" states. This operator should flip the sign of the amplitude of the "good" state. | *required* | | `space_transform` | `QCallable[QArray[QBit]]` | The space transform operator (which is known also the state preparation operator). First applied to prepare the state before the amplification, then used inside the Grover operator. | *required* | | `packed_qvars` | `QArray[QBit]` | | *required* | # Amplitude Estimation Source: https://docs.classiq.io/sdk-reference/qmod/functions/open_library/amplitude_estimation Functions: | Name | Description | | ---------------------- | --------------------------------- | | `amplitude_estimation` | \[Qmod Classiq-library function]. | ### amplitude\_estimation
amplitude\_estimation(
oracle: QCallable\[QArray\[QBit]],
space\_transform: QCallable\[QArray\[QBit]],
phase: QNum,
packed\_vars: QArray\[QBit]
) -> None
\[Qmod Classiq-library function] Estimate the probability of a state being marked by the operand `oracle` as a "good state." The algorithm prepares the state in the `packed_vars` register and estimates the probability of this state being marked by the oracle as a "good state." This is done using the Quantum Phase Estimation (QPE) algorithm, where the unitary for QPE is the Grover operator, which is composed of the `oracle` and `space_transform` operators. **Parameters:** | Name | Type | Description | Default | | ----------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- | | `oracle` | `QCallable[QArray[QBit]]` | The oracle operator that marks the "good" state. This operator should flip the sign of the amplitude of the "good" state. | *required* | | `space_transform` | `QCallable[QArray[QBit]]` | The space transform operator (which is known also the state preparation operator), which is first applied to prepare the state before the QPE, and then used inside the Grover operator. | *required* | | `phase` | `QNum` | Assuming this variable starts from the zero state -this variable output holds the $phase=\theta$ result in the \[0,1] domain, which relates to the estimated probability $a$ through $a=\sin^2(\pi \theta)$. | *required* | | `packed_vars` | `QArray[QBit]` | The variable that holds the state to be estimated. Assumed to be in the zero state at the beginning of the algorithm. | *required* | # Amplitude Loading Source: https://docs.classiq.io/sdk-reference/qmod/functions/open_library/amplitude_loading Functions: | Name | Description | | ------------------------ | --------------------------------- | | `assign_amplitude_table` | \[Qmod Classiq-library function]. | ### assign\_amplitude\_table
assign\_amplitude\_table(
amplitudes: list\[float],
index: Const\[QArray],
indicator: QBit
) -> None
\[Qmod Classiq-library function] Load a specified list of real amplitudes into a quantum variable using an extra indicator qubit: ( |i\rangle|0\rangle \rightarrow a(i)\ |i\rangle|1\rangle + \sqrt\ |i\rangle|0\rangle ). Here, (a(i)) is the i-th amplitude, determined by the QNum when the index is in state (i). A list extracted from a given classical function (f(x)), with indexing according to a given QNum, can be obtained via the utility SDK function `lookup_table`. This function expects the indicator qubit to be initialized to (|0\rangle). **Parameters:** | Name | Type | Description | Default | | ------------ | --------------- | -------------------------------------------------------- | ---------- | | `amplitudes` | `list[float]` | Real values for the amplitudes. Must be between -1 and 1 | *required* | | `index` | `Const[QArray]` | The quantum variable used for amplitude indexing | *required* | | `indicator` | `QBit` | The quantum indicator qubit | *required* | # Bit Operations Source: https://docs.classiq.io/sdk-reference/qmod/functions/open_library/bit_operations Functions: | Name | Description | | -------------------- | -------------------------------------------------------------------------------------------- | | `cyclic_shift_left` | Performs a left shift on the quantum register array `reg` using SWAP gates. | | `cyclic_shift_right` | Performs a right shift on the quantum register array `reg` by inverting cyclic\_shift\_left. | | `bitwise_negate` | Negates each bit of the input x. | ### cyclic\_shift\_left
cyclic\_shift\_left(
reg: QArray\[QBit]
) -> None
Performs a left shift on the quantum register array `reg` using SWAP gates. **Parameters:** | Name | Type | Description | Default | | ----- | -------------- | ----------- | ---------- | | `reg` | `QArray[QBit]` | | *required* | ### cyclic\_shift\_right
cyclic\_shift\_right(
reg: QArray\[QBit]
) -> None
Performs a right shift on the quantum register array `reg` by inverting cyclic\_shift\_left. **Parameters:** | Name | Type | Description | Default | | ----- | -------------- | ----------- | ---------- | | `reg` | `QArray[QBit]` | | *required* | ### bitwise\_negate
bitwise\_negate(
x: QArray\[QBit]
) -> None
Negates each bit of the input x. **Parameters:** | Name | Type | Description | Default | | ---- | -------------- | ----------- | ---------- | | `x` | `QArray[QBit]` | | *required* | # Discrete Sine Cosine Transform Source: https://docs.classiq.io/sdk-reference/qmod/functions/open_library/discrete_sine_cosine_transform Functions: | Name | Description | | --------------- | --------------------------------- | | `qct_qst_type1` | \[Qmod Classiq-library function]. | | `qct_qst_type2` | \[Qmod Classiq-library function]. | | `qct_type2` | \[Qmod Classiq-library function]. | | `qst_type2` | \[Qmod Classiq-library function]. | ### qct\_qst\_type1
qct\_qst\_type1(
x: QArray\[QBit]
) -> None
\[Qmod Classiq-library function] Applies the quantum discrete cosine (DCT) and sine (DST) transform of type 1 to the qubit array `x`. Corresponds to the matrix (with $n\equiv$`x.len`): $$ \left( egin{array}{ccc|c} {} &{} &{} \ {}&{ m DCT}^{(1)}(2^{n-1}+1) & {}& 0\ {} &{} &{} \ \hline {} & 0 & {} & i{ m DST}^{(1)}(2^{n-1}-1) \end{array} ight) $$ **Parameters:** | Name | Type | Description | Default | | ---- | -------------- | ------------------------------------------ | ---------- | | `x` | `QArray[QBit]` | The qubit array to apply the transform to. | *required* | ### qct\_qst\_type2
qct\_qst\_type2(
x: QArray\[QBit],
q: Const\[QBit]
) -> None
\[Qmod Classiq-library function] Applies the quantum discrete cosine (DCT) and sine (DST) transform of type 2 to the qubit array `x` concatenated with `q`, with `q` being the MSB. Corresponds to the matrix (with $n\equiv$`x.len`+1): $$ \left( egin{array}{c|c} { m DCT}^{(2)}(2^{n-1}) & 0\ \hline 0 & -{ m DST}^{(2)}(2^{n-1}) \end{array} ight) $$ **Parameters:** | Name | Type | Description | Default | | ---- | -------------- | ---------------------------------------------------------- | ---------- | | `x` | `QArray[QBit]` | The LSB part of the qubit array to apply the transform to. | *required* | | `q` | `Const[QBit]` | The MSB of the qubit array to apply the transform to. | *required* | ### qct\_type2
qct\_type2(
x: QArray\[QBit]
) -> None
\[Qmod Classiq-library function] Applies the quantum discrete cosine (DCT) transform of type 2, $\{ m DCT\}^\{(2)\}$, to the qubit array `x`. **Parameters:** | Name | Type | Description | Default | | ---- | -------------- | ------------------------------------------ | ---------- | | `x` | `QArray[QBit]` | The qubit array to apply the transform to. | *required* | ### qst\_type2
qst\_type2(
x: QArray\[QBit]
) -> None
\[Qmod Classiq-library function] Applies the quantum discrete sine (DST) transform of type 2, $\{ m DST\}^\{(2)\}$, to the qubit array `x`. **Parameters:** | Name | Type | Description | Default | | ---- | -------------- | ------------------------------------------ | ---------- | | `x` | `QArray[QBit]` | The qubit array to apply the transform to. | *required* | # Encodings Source: https://docs.classiq.io/sdk-reference/qmod/functions/open_library/encodings Functions: | Name | Description | | --------------------------- | ------------------------------------------------------------------------------ | | `binary_to_one_hot` | Conversion of binary encoded value to one-hot encoding. | | `binary_to_unary` | Conversion of binary encoded value to unary encoding. | | `one_hot_to_unary` | Conversion of one-hot encoded value to unary encoding. | | `one_hot_to_binary` | Conversion of one-hot encoded value to binary encoding. | | `unary_to_one_hot` | Conversion of unary encoded value to one-hot encoding. | | `unary_to_binary` | Conversion of unary encoded value to binary encoding. | | `inplace_binary_to_one_hot` | Conversion of binary encoded value to one-hot encoding. | | `inplace_one_hot_to_unary` | Inplace conversion of one-hot encoded value to unary encoding. | | `pad_zeros` | Pad the input qvar with additional qubits at the end to reach the total\_size. | ### binary\_to\_one\_hot
binary\_to\_one\_hot(
binary: Input\[QNum],
one\_hot: Output\[QArray]
) -> None
Conversion of binary encoded value to one-hot encoding. The output `one_hot` variable is of size 2^n, where n is the number of bits in the binary representation. For example, the state `|01>`=`|2>` will be converted to `|0010>` (one-hot for 2). **Parameters:** | Name | Type | Description | Default | | --------- | ---------------- | ---------------------------------------------------------- | ---------- | | `binary` | `Input[QNum]` | binary input variable to be converted to one-hot encoding. | *required* | | `one_hot` | `Output[QArray]` | one-hot output array. | *required* | ### binary\_to\_unary
binary\_to\_unary(
binary: Input\[QNum],
unary: Output\[QArray]
) -> None
Conversion of binary encoded value to unary encoding. The output `unary` variable is of size 2^n - 1, where n is the number of bits in the binary representation. For example, the state `|01>`=`|2>` will be converted to `|110>` (unary for 2). **Parameters:** | Name | Type | Description | Default | | -------- | ---------------- | -------------------------------------------------------- | ---------- | | `binary` | `Input[QNum]` | binary input variable to be converted to unary encoding. | *required* | | `unary` | `Output[QArray]` | unary output array. | *required* | ### one\_hot\_to\_unary
one\_hot\_to\_unary(
one\_hot: Input\[QArray],
unary: Output\[QArray]
) -> None
Conversion of one-hot encoded value to unary encoding. The output `unary` variable is smaller in 1 qubit than the input `one_hot` variable. For example, the state `|0010>` (one-hot for 2) will be converted to `|110>` (unary for 2). **Parameters:** | Name | Type | Description | Default | | --------- | ---------------- | ------------------------------------------------------ | ---------- | | `one_hot` | `Input[QArray]` | one-hot input array to be converted to unary encoding. | *required* | | `unary` | `Output[QArray]` | unary output array. | *required* | ### one\_hot\_to\_binary
one\_hot\_to\_binary(
one\_hot: Input\[QArray],
binary: Output\[QNum\[Literal\['ceiling(log(one\_hot.len, 2))']]]
) -> None
Conversion of one-hot encoded value to binary encoding. The output `binary` variable is of size log2(one\_hot.size) rounded up. For example, the state `|0010>` (one-hot for 2) will be converted to `|01>`=`|2>`. **Parameters:** | Name | Type | Description | Default | | --------- | ------------------------------------------------------- | ------------------------------------------------------- | ---------- | | `one_hot` | `Input[QArray]` | one-hot input array to be converted to binary encoding. | *required* | | `binary` | `Output[QNum[Literal['ceiling(log(one_hot.len, 2))']]]` | binary output variable. | *required* | ### unary\_to\_one\_hot
unary\_to\_one\_hot(
unary: Input\[QArray],
one\_hot: Output\[QArray]
) -> None
Conversion of unary encoded value to one-hot encoding. The output `one_hot` variable is larger in 1 qubit than the input `unary` variable. For example, the state `|110>` (unary for 2) will be converted to `|0010>` (one-hot for 2). **Parameters:** | Name | Type | Description | Default | | --------- | ---------------- | ------------------------------------------------------ | ---------- | | `unary` | `Input[QArray]` | unary input array to be converted to one-hot encoding. | *required* | | `one_hot` | `Output[QArray]` | one-hot output array. | *required* | ### unary\_to\_binary
unary\_to\_binary(
unary: Input\[QArray],
binary: Output\[QNum]
) -> None
Conversion of unary encoded value to binary encoding. The output `binary` variable is of size log2(unary.size + 1) rounded up. For example, the state `|110>` (unary for 2) will be converted to `|01>`=`|2>`. **Parameters:** | Name | Type | Description | Default | | -------- | --------------- | ----------------------------------------------------- | ---------- | | `unary` | `Input[QArray]` | unary input array to be converted to binary encoding. | *required* | | `binary` | `Output[QNum]` | binary output variable. | *required* | ### inplace\_binary\_to\_one\_hot
inplace\_binary\_to\_one\_hot(
source: Input\[QArray],
target: Output\[QArray]
) -> None
Conversion of binary encoded value to one-hot encoding. The implementation is based on [https://quantumcomputing.stackexchange.com/questions/5526/garbage-free-reversible-binary-to-unary-decoder-construction](https://quantumcomputing.stackexchange.com/questions/5526/garbage-free-reversible-binary-to-unary-decoder-construction). The input is assumed to be of size 2^n, where n is the number of bits in the binary representation. For example, the state `|01000>`=`|2>` will be converted to `|00100>` (one-hot for 2). **Parameters:** | Name | Type | Description | Default | | -------- | ---------------- | ----------------------------------------------------------------------- | ---------- | | `source` | `Input[QArray]` | binary input array padded with 0's to be converted to one-hot encoding. | *required* | | `target` | `Output[QArray]` | one-hot output array. | *required* | ### inplace\_one\_hot\_to\_unary
inplace\_one\_hot\_to\_unary(
qvar: QArray
) -> None
Inplace conversion of one-hot encoded value to unary encoding. The input is assumed to be of size n, where n is the number of bits in the one-hot representation. The remaining unary representation will at the higher n-1 bits, where the lsb is cleared to 0. For example, the state `|0010>` (one-hot for 2) will be converted to `|0>``|110>` (unary for 2). **Parameters:** | Name | Type | Description | Default | | ------ | -------- | ------------------------------------------------------ | ---------- | | `qvar` | `QArray` | one-hot input array to be converted to unary encoding. | *required* | ### pad\_zeros
pad\_zeros(
total\_size: int,
qvar: Input\[QArray],
padded: Output\[QArray]
) -> None
Pad the input qvar with additional qubits at the end to reach the total\_size. **Parameters:** | Name | Type | Description | Default | | ------------ | ---------------- | --------------------------------------- | ---------- | | `total_size` | `int` | The desired total size after padding. | *required* | | `qvar` | `Input[QArray]` | The input quantum array to be padded. | *required* | | `padded` | `Output[QArray]` | The output quantum array after padding. | *required* | # Grover Source: https://docs.classiq.io/sdk-reference/qmod/functions/open_library/grover Functions: | Name | Description | | -------------------- | --------------------------------- | | `phase_oracle` | \[Qmod Classiq-library function]. | | `reflect_about_zero` | \[Qmod Classiq-library function]. | | `grover_diffuser` | \[Qmod Classiq-library function]. | | `grover_operator` | \[Qmod Classiq-library function]. | | `grover_search` | \[Qmod Classiq-library function]. | ### phase\_oracle
phase\_oracle(
predicate: QPerm\[Const\[QArray\[QBit]], QBit],
target: Const\[QArray\[QBit]]
) -> None
\[Qmod Classiq-library function] Creates a phase oracle operator based on a predicate function. Applies a predicate function and marks "good" and "bad" states with a phase flip. If the predicate is marked as $\chi$, and the oracle is marked as $S_\{\chi\}$, then: $$ S_{\chi}\lvert x \rangle = \begin{cases} -\lvert x \rangle & \text{if } \chi(x) = 1 \\ \phantom{-} \lvert x \rangle & \text{if } \chi(x) = 0 \end{cases} $$ **Parameters:** | Name | Type | Description | Default | | ----------- | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `predicate` | `QPerm[Const[QArray[QBit]], QBit]` | A predicate function that takes a QArray of QBits and sets a single QBit \`\|1>` if the predicate is true, and \`\|0>\` otherwise. | *required* | | `target` | `Const[QArray[QBit]]` | The target QArray of QBits to apply the phase oracle to. | *required* | ### reflect\_about\_zero
reflect\_about\_zero(
qvar: Const\[QArray\[QBit]]
) -> None
\[Qmod Classiq-library function] Reflects the state about the `|0>` state (i.e. applies a (-1) phase to all states besides the `|0>` state). Implements the operator $S_0$: $$ \begin{equation} S_0|{x}\rangle = (-1)^{(x\ne0)}|{x}\rangle= (2|{0}\rangle\langle{0}|-I)|{x}\rangle \end{equation} $$ **Parameters:** | Name | Type | Description | Default | | ------ | --------------------- | ----------------------------- | ---------- | | `qvar` | `Const[QArray[QBit]]` | The quantum state to reflect. | *required* | ### grover\_diffuser
grover\_diffuser(
space\_transform: QCallable\[QArray\[QBit]],
packed\_vars: QArray\[QBit]
) -> None
\[Qmod Classiq-library function] Reflects the given state about the A`|0>` state, where A is the `space_transform` parameter. It is defined as: $$ \begin{equation} D = A S_0 A^{\dagger} \end{equation} $$ where $S_0$ is the reflection about the `|0>` state (see `reflect_about_zero`). **Parameters:** | Name | Type | Description | Default | | ----------------- | ------------------------- | -------------------------------------------------- | ---------- | | `space_transform` | `QCallable[QArray[QBit]]` | The operator which encodes the axis of reflection. | *required* | | `packed_vars` | `QArray[QBit]` | The state to which to apply the diffuser. | *required* | ### grover\_operator
grover\_operator(
oracle: QCallable\[QArray\[QBit]],
space\_transform: QCallable\[QArray\[QBit]],
packed\_vars: QArray\[QBit]
) -> None
\[Qmod Classiq-library function] Applies the grover operator, defined by: $$ Q=S_{\psi_0}S_{\psi_1} $$ where $S_\{\psi_1\}$ is a reflection about the non-marked states, and $S_\{\psi_0\}$ is a reflection about a given state defined by $|\psi_0\rangle = A|0\rangle$. **Parameters:** | Name | Type | Description | Default | | ----------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------- | ---------- | | `oracle` | `QCallable[QArray[QBit]]` | A unitary operator which adds a phase of (-1) to marked states. | *required* | | `space_transform` | `QCallable[QArray[QBit]]` | The operator which creates $\|\psi_0\rangle$, the initial state, used by the diffuser to reflect about it. | *required* | | `packed_vars` | `QArray[QBit]` | The state to which to apply the grover operator. | *required* | ### grover\_search
grover\_search(
reps: CInt,
oracle: QCallable\[QArray\[QBit]],
packed\_vars: QArray\[QBit]
) -> None
\[Qmod Classiq-library function] Applies Grover search algorithm. **Parameters:** | Name | Type | Description | Default | | ------------- | ------------------------- | ------------------------------------------------------------ | ---------- | | `reps` | `CInt` | Number of repetitions of the grover operator. | *required* | | `oracle` | `QCallable[QArray[QBit]]` | An oracle that marks the solution. | *required* | | `packed_vars` | `QArray[QBit]` | Packed form of the variable to apply the grover operator on. | *required* | # Hea Source: https://docs.classiq.io/sdk-reference/qmod/functions/open_library/hea Functions: | Name | Description | | ---------- | --------------------------------- | | `full_hea` | \[Qmod Classiq-library function]. | ### full\_hea
full\_hea(
num\_qubits: CInt,
is\_parametrized: CArray\[CInt],
angle\_params: CArray\[CReal],
connectivity\_map: CArray\[CArray\[CInt]],
reps: CInt,
operands\_1qubit: QCallableList\[Annotated\[CReal, angle], Annotated\[QBit, q]],
operands\_2qubit: QCallableList\[Annotated\[CReal, angle], Annotated\[QBit, q1], Annotated\[QBit, q2]],
x: QArray\[QBit, Literal\['num\_qubits']]
) -> None
\[Qmod Classiq-library function] Implements an ansatz on a qubit array `x` with the given 1-qubit and 2-qubit operations. The number of ansatz layers is given in argument `reps`. Each layer applies the 1-qubit operands in `operands_1qubit` to all the qubits in `x`. Next, it applies the 2-qubit operands in `operands_2qubit` to qubits (i, j) for each pair of indices (i, j) in `connectivity_map`. The list `is_parametrized` specifies whether the operands in `operands_1qubit` and `operands_2qubit` are parametric (expect a classical argument). `is_parametrized` is a list of flags (0 and 1 integers) of length `len(operands_1qubit) + len(operands_2qubit)`. The first `len(operands_1qubit)` flags refer to the `operands_1qubit` operands and the next `len(operands_2qubit)` flags refer to the `operands_2qubit` operands. The classical arguments to the parametric operands are given in argument `angle_params`. `angle_params` concatenates a set of arguments for each ansatz layer. Each set contains an argument for each qubit in `x` times the number of parametric operands in `operands_1qubit`. These are followed by an argument for each mapping pair in `connectivity_map` times the number of parametric operands in `operands_2qubit`. **Parameters:** | Name | Type | Description | Default | | ------------------ | ---------------------------------------------------------------------------------- | -------------------------------------------------- | ---------- | | `num_qubits` | `CInt` | The length of qubit array x | *required* | | `is_parametrized` | `CArray[CInt]` | A list of 0 and 1 flags | *required* | | `angle_params` | `CArray[CReal]` | | *required* | | `connectivity_map` | `CArray[CArray[CInt]]` | A list of pairs of qubit indices | *required* | | `reps` | `CInt` | The number of ansatz layers | *required* | | `operands_1qubit` | `QCallableList[Annotated[CReal, angle], Annotated[QBit, q]]` | A list of operations on a single qubit | *required* | | `operands_2qubit` | `QCallableList[Annotated[CReal, angle], Annotated[QBit, q1], Annotated[QBit, q2]]` | A list of operations on two qubits | *required* | | `x` | `QArray[QBit, Literal['num_qubits']]` | The quantum object to be transformed by the ansatz | *required* | # Linear Combination Of Unitaries Source: https://docs.classiq.io/sdk-reference/qmod/functions/open_library/linear_combination_of_unitaries Functions: | Name | Description | | ---------------- | --------------------------------- | | `lcu` | \[Qmod Classiq-library function]. | | `lcu_pauli` | \[Qmod Classiq-library function]. | | `prepare_select` | \[Qmod Classiq-library function]. | ### lcu
lcu(
coefficients: list\[float],
unitaries: QCallableList,
block: QNum\[Literal\['max(ceiling(log(coefficients.len, 2)), 1)']]
) -> None
\[Qmod Classiq-library function] Implements a general linear combination of unitaries (LCU) procedure. The algorithm prepares a superposition over the `unitaries` according to the given `coefficients`, and then conditionally applies each unitary controlled by the `block`. The operation is of the form: $\sum_j \alpha_j U_j$ where $U_j$ is a unitary operation applied to `data`. **Parameters:** | Name | Type | Description | Default | | -------------- | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ---------- | | `coefficients` | `list[float]` | L1-normalized array of $\\{ \alpha_j \\}$ of the LCU coefficients. | *required* | | `unitaries` | `QCallableList` | A list of quantum callable functions to be applied conditionally. | *required* | | `block` | `QNum[Literal['max(ceiling(log(coefficients.len, 2)), 1)']]` | Quantum variable that holds the superposition index used for conditional application of each unitary. | *required* | ### lcu\_pauli
lcu\_pauli(
operator: SparsePauliOp,
data: QArray\[QBit, Literal\['operator.num\_qubits']],
block: QNum\[Literal\['max(ceiling(log(operator.terms.len, 2)), 1)']]
) -> None
\[Qmod Classiq-library function] Applies a linear combination of unitaries (LCU) where each unitary is a Pauli term, represented as a tensor product of Pauli operators. The function prepares a superposition over the unitaries according to the given magnitudes and phases, and applies the corresponding Pauli operators conditionally. This is useful for implementing Hamiltonian terms of the form: $H=\sum_j \alpha_j P_j$ where $P_j$ is a tensor product of Pauli operators. The SELECT operator (`_select_paulis`) loads each data qubit's Pauli column from the `block` index using gray-code uniformly-controlled RZ/RY rotations; the resulting per-term phases are folded into the coefficients so that the single phase table inside `prepare_select` corrects them together with the coefficient signs/phases. **Parameters:** | Name | Type | Description | Default | | ---------- | -------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ---------- | | `operator` | `SparsePauliOp` | Operator consists of pauli strings with their coefficients, represented in a sparse format. | *required* | | `data` | `QArray[QBit, Literal['operator.num_qubits']]` | Quantum Variable on which the Pauli operators act. Its size must match the number of qubits required by the Pauli operator. | *required* | | `block` | `QNum[Literal['max(ceiling(log(operator.terms.len, 2)), 1)']]` | Quantum variable that holds the superposition index used for conditional application of each term. | *required* | ### prepare\_select
prepare\_select(
coefficients: list\[float],
select: QCallable\[QNum],
block: QNum\[Literal\['max(ceiling(log(coefficients.len, 2)), 1)']]
) -> None
\[Qmod Classiq-library function] Applies the 'Prepare-Select' scheme used for Linear Combination of Unitaries (LCU). Compared to the `lcu` function, here the Select operator should be provided directly, allowing to take advantage of some structure for the unitaries of the LCU. The select operator is defined by: $\mathrm\{SELECT\} = \sum_\{j=0\}^\{m-1\} |j\rangle\!\langle j|_\{block\} \otimes U_j$. **Parameters:** | Name | Type | Description | Default | | -------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- | | `coefficients` | `list[float]` | L1-normalized array of $\\{ \alpha_j \\}$ of the LCU coefficients. | *required* | | `select` | `QCallable[QNum]` | A quantum callable to be applied between the state preparation and its inverse. Its input is the `block` variable, labeling the index of the unitaries in the LCU. | *required* | | `block` | `QNum[Literal['max(ceiling(log(coefficients.len, 2)), 1)']]` | A Quantum variable that holds the index used as input for the 'select' operator. | *required* | # Linear Pauli Rotation Source: https://docs.classiq.io/sdk-reference/qmod/functions/open_library/linear_pauli_rotation Functions: | Name | Description | | ------------------------ | --------------------------------- | | `linear_pauli_rotations` | \[Qmod Classiq-library function]. | ### linear\_pauli\_rotations
linear\_pauli\_rotations(
bases: CArray\[Pauli],
slopes: CArray\[CReal],
offsets: CArray\[CReal],
x: QArray\[QBit],
q: QArray\[QBit]
) -> None
\[Qmod Classiq-library function] Performs a rotation on a series of $m$ target qubits, where the rotation angle is a linear function of an $n$-qubit control register. Corresponds to the braket notation: $$ \left|x\right\rangle _{n}\left|q\right\rangle _{m}\rightarrow\left|x\right\rangle _{n}\prod_{k=1}^{m}\left(\cos\left(\frac{a_{k}}{2}x+\frac{b_{k}}{2}\right)- i\sin\left(\frac{a_{k}}{2}x+\frac{b_{k}}{2}\right)P_{k}\right)\left|q_{k}\right\rangle $$ where $\left|x\right\rangle$ is the control register, $\left|q\right\rangle$ is the target register, each $P_\{k\}$ is one of the three Pauli matrices $X$, $Y$, or $Z$, and $a_\{k\}$, $b_\{k\}$ are the user given slopes and offsets, respectively. **Parameters:** | Name | Type | Description | Default | | --------- | --------------- | ----------------------------------------------------------- | ---------- | | `bases` | `CArray[Pauli]` | List of Pauli Enums. | *required* | | `slopes` | `CArray[CReal]` | Rotation slopes for each of the given Pauli bases. | *required* | | `offsets` | `CArray[CReal]` | Rotation offsets for each of the given Pauli bases. | *required* | | `x` | `QArray[QBit]` | Quantum state to apply the rotation based on its value. | *required* | | `q` | `QArray[QBit]` | List of indicator qubits for each of the given Pauli bases. | *required* | # Modular Arithmetics Source: https://docs.classiq.io/sdk-reference/qmod/functions/open_library/modular_arithmetics Functions: | Name | Description | | | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | | `modular_add_inplace` | \[Qmod Classiq-library function]. | | | `modular_negate_inplace` | \[Qmod Classiq-library function]. | | | `modular_subtract_inplace` | \[Qmod Classiq-library function]. | | | `modular_double_inplace` | \[Qmod Classiq-library function]. | | | `modular_add_constant_inplace` | \[Qmod Classiq-library function]. | | | `modular_multiply` | \[Qmod Classiq-library function] Performs the transformation `\|x>``\|y>``\|0>` -> `\|x>``\|y>` | (x\*y mod modulus)>. | | `modular_square` | \[Qmod Classiq-library function] Performs the transformation `\|x>``\|0>` -> `\|x>` | (x^2 mod modulus)>. | | `modular_multiply_constant` | \[Qmod Classiq-library function] Performs the transformation `\|x>``\|y>` -> `\|x>` | (x \* a mod modulus)>. | | `modular_multiply_constant_inplace` | \[Qmod Classiq-library function] In-place modular multiplication of x by a classical constant modulo a symbolic modulus. | | | `modular_to_montgomery_inplace` | \[Qmod Classiq-library function] Converts a quantum integer `\|x>` into its Montgomery representation modulo modulus in place. | | | `modular_montgomery_to_standard_inplace` | \[Qmod Classiq-library function] Converts quantum integer `\|x>` from Montgomery representation to standard form in place modulo modulus. | | | `modular_inverse_inplace` | \[Qmod Classiq-library function] Computes the modular inverse of a quantum number `\|v>` modulo modulus in place, using the Kaliski algorithm. | | | `kaliski_iteration` | Single iteration of the Kaliski modular inverse algorithm main loop. | | | `modular_rsub_inplace` | \[Qmod Classiq-library function] Performs the in-place modular right-subtraction `\|x>` -> | (a - x mod modulus)>. | ### modular\_add\_inplace
modular\_add\_inplace(
modulus: CInt,
x: Const\[QNum],
y: QNum
) -> None
\[Qmod Classiq-library function] Performs the transformation `|x>``|y>` -> `|x>`|(x + y mod modulus)>. Note: `\|x>` and `\|y>` should have values smaller than `modulus`. The modulus should satisfy 1 \< modulus \< 2\*\*n, where n is the size of `|x>` and `|y>`. Implementation based on: [https://arxiv.org/pdf/1706.06752](https://arxiv.org/pdf/1706.06752) Chapter 3.2 Fig 3 **Parameters:** | Name | Type | Description | Default | | --------- | ------------- | ------------------------------------------------------------------------------ | ---------- | | `modulus` | `CInt` | Classical number modulus (CInt) | *required* | | `x` | `Const[QNum]` | 1st quantum number input (unsigned). | *required* | | `y` | `QNum` | 2nd quantum number input (unsigned). Will hold the result after the operation. | *required* | ### modular\_negate\_inplace
modular\_negate\_inplace(
modulus: CInt,
x: QNum
) -> None
\[Qmod Classiq-library function] Performs the transformation `|x>` -> |(-x mod modulus)>. Note: `\|x>` should have values smaller than `modulus`. The modulus should satisfy 1 \< modulus \< 2\*\*n, where n is the size of `|x>`. **Parameters:** | Name | Type | Description | Default | | --------- | ------ | -------------------------------------------------------------------------- | ---------- | | `modulus` | `CInt` | Classical number modulus | *required* | | `x` | `QNum` | Quantum number input (unsigned). Will hold the result after the operation. | *required* | ### modular\_subtract\_inplace
modular\_subtract\_inplace(
modulus: CInt,
x: Const\[QNum],
y: QNum
) -> None
\[Qmod Classiq-library function] Performs the transformation `|x>``|y>` -> `|x>`|(x - y mod modulus)>. Note: `\|x>` and `\|y>` should have values smaller than `modulus`. The modulus should satisfy 1 \< modulus \< 2\*\*n, where n is the size of `|x>` and `|y>`. **Parameters:** | Name | Type | Description | Default | | --------- | ------------- | ----------------------------------------------------------------------------------------------- | ---------- | | `modulus` | `CInt` | Classical number modulus | *required* | | `x` | `Const[QNum]` | 1st quantum number input (unsigned). Const. | *required* | | `y` | `QNum` | 2nd quantum number input (unsigned). In-place target, will hold the result after the operation. | *required* | ### modular\_double\_inplace
modular\_double\_inplace(
modulus: CInt,
x: QNum
) -> None
\[Qmod Classiq-library function] Performs the transformation `|x>` -> |(2x mod modulus)>. Note: `\|x>` should have a value smaller than `modulus`. The modulus must be a constant odd integer. The modulus should satisfy 1 \< modulus \< 2\*\*n, where n is the size of `|x>`. Implementation based on: [https://arxiv.org/pdf/1706.06752](https://arxiv.org/pdf/1706.06752) Chapter 3.2 Fig 4 **Parameters:** | Name | Type | Description | Default | | --------- | ------ | -------------------------------------------------------------------------- | ---------- | | `modulus` | `CInt` | Classical number modulus | *required* | | `x` | `QNum` | Quantum number input (unsigned). Will hold the result after the operation. | *required* | ### modular\_add\_constant\_inplace
modular\_add\_constant\_inplace(
modulus: CInt,
a: CInt,
x: QNum
) -> None
\[Qmod Classiq-library function] Performs the transformation `|x>` -> |(x + a mod modulus)>. Note: `\|x>` and `a` should have values smaller than `modulus`. The modulus should satisfy 1 \< modulus \< 2\*\*n, where n is the size of `|x>`. Implementation is based on the logic in: [https://arxiv.org/pdf/1706.06752](https://arxiv.org/pdf/1706.06752) Chapter 3.2 Fig 3 **Parameters:** | Name | Type | Description | Default | | --------- | ------ | -------------------------------------------------------------------------- | ---------- | | `modulus` | `CInt` | Classical number modulus | *required* | | `a` | `CInt` | constant unsigned number input for the addition. | *required* | | `x` | `QNum` | Quantum number input (unsigned). Will hold the result after the operation. | *required* | ### modular\_multiply
modular\_multiply(
modulus: CInt,
x: Const\[QArray\[QBit]],
y: Const\[QArray\[QBit]],
z: QArray\[QBit]
) -> None
\[Qmod Classiq-library function] Performs the transformation `|x>``|y>``|0>` -> `|x>``|y>`|(x\*y mod modulus)> Note: `\|x>`, `\|y>` should have the same size and have values smaller than `modulus`. The modulus must be a constant odd integer. The modulus should satisfy 1 \< modulus \< 2\*\*n, where n is the size of `|x>` and `|y>`. The output register z must be pre-allocated with the same size as x and y. Implementation is based on the logic in: [https://arxiv.org/pdf/1706.06752](https://arxiv.org/pdf/1706.06752) Chapter 3.2 Fig 5 **Parameters:** | Name | Type | Description | Default | | --------- | --------------------- | ----------------------------------------------------------------------------------- | ---------- | | `modulus` | `CInt` | Classical number modulus | *required* | | `x` | `Const[QArray[QBit]]` | Quantum number input (unsigned), multiplicand. | *required* | | `y` | `Const[QArray[QBit]]` | Quantum number input (unsigned), multiplier. | *required* | | `z` | `QArray[QBit]` | Quantum number (unsigned), pre-allocated output variable that will hold the result. | *required* | ### modular\_square
modular\_square(
modulus: CInt,
x: Const\[QArray\[QBit]],
z: QArray\[QBit]
) -> None
\[Qmod Classiq-library function] Performs the transformation `|x>``|0>` -> `|x>`|(x^2 mod modulus)>. Note: `\|x>` should have the same size and have values smaller than `modulus`. The modulus must be a constant odd integer. The modulus should satisfy 1 \< modulus \< 2\*\*n, where n is the size of `|x>`. The output register z must be pre-allocated with the same size as x. Implementation is based on: [https://arxiv.org/pdf/1706.06752](https://arxiv.org/pdf/1706.06752) Chapter 3.2 Fig 6 **Parameters:** | Name | Type | Description | Default | | --------- | --------------------- | ---------------------------------------------------------------------------- | ---------- | | `modulus` | `CInt` | Classical number modulus | *required* | | `x` | `Const[QArray[QBit]]` | Quantum number input (unsigned), the input to square. | *required* | | `z` | `QArray[QBit]` | Quantum number (unsigned), pre-allocated output variable to hold the result. | *required* | ### modular\_multiply\_constant
modular\_multiply\_constant(
modulus: CInt,
x: Const\[QNum],
a: CInt,
y: QNum
) -> None
\[Qmod Classiq-library function] Performs the transformation `|x>``|y>` -> `|x>`|(x \* a mod modulus)>. Note: `\|x>` and `\|y>` should have values smaller than `modulus`. The modulus must be a constant odd integer. The modulus should satisfy 1 \< modulus \< 2\*\*n, where n is the size of `|x>` and `|y>`. **Parameters:** | Name | Type | Description | Default | | --------- | ------------- | --------------------------------------------------------------------- | ---------- | | `modulus` | `CInt` | Classical number modulus | *required* | | `x` | `Const[QNum]` | Quantum number (unsigned), input variable. | *required* | | `a` | `CInt` | Classical number constant | *required* | | `y` | `QNum` | Quantum number (unsigned), output variable that will hold the result. | *required* | ### modular\_multiply\_constant\_inplace
modular\_multiply\_constant\_inplace(
modulus: CInt,
a: CInt,
x: QNum
) -> None
\[Qmod Classiq-library function] In-place modular multiplication of x by a classical constant modulo a symbolic modulus. Performs `|x>` -> |(x \* a mod modulus)>. Note: `\|x>` should have values smaller than `modulus`. The modulus should satisfy 1 \< modulus \< 2\*\*n, where n is the size of `|x>`. The constant `a` should have an inverse modulo `modulus`, i.e. gcd(a, modulus) = 1. The constant `a` should satisfy 0 \<= a \< modulus. **Parameters:** | Name | Type | Description | Default | | --------- | ------ | ------------------------------------------------- | ---------- | | `modulus` | `CInt` | Classical number modulus | *required* | | `a` | `CInt` | Classical number constant | *required* | | `x` | `QNum` | Quantum number (unsigned), in-place input/output. | *required* | ### modular\_to\_montgomery\_inplace
modular\_to\_montgomery\_inplace(
modulus: CInt,
x: QNum
) -> None
\[Qmod Classiq-library function] Converts a quantum integer `|x>` into its Montgomery representation modulo modulus in place. The Montgomery factor is R = 2**n, where n = x.size (the number of qubits in `|x>`). This function performs the transformation `|x>` -> |(x \* R mod modulus)>. Note: `\|x>` should have values smaller than `modulus`. The modulus should satisfy 1 \< modulus \< 2**n, where n is the size of `|x>`. The modulus must be odd so that R = 2\*\*n is invertible modulo modulus (gcd(R, modulus) = 1). **Parameters:** | Name | Type | Description | Default | | --------- | ------ | --------------------------------------------------------------- | ---------- | | `modulus` | `CInt` | Classical number modulus | *required* | | `x` | `QNum` | Quantum number, in-place operand to convert to Montgomery form. | *required* | ### modular\_montgomery\_to\_standard\_inplace
modular\_montgomery\_to\_standard\_inplace(
modulus: CInt,
x: QNum
) -> None
\[Qmod Classiq-library function] Converts quantum integer `|x>` from Montgomery representation to standard form in place modulo modulus. The Montgomery factor is R = 2**n, where n = x.size (the number of qubits in `|x>`). This function performs the transformation `|x>` -> |(x \* R^-1 mod modulus)>. Note: `\|x>` should have values smaller than `modulus`. The modulus should satisfy 1 \< modulus \< 2**n, where n is the size of `|x>`. The modulus must be odd so that R = 2\*\*n is invertible modulo modulus (gcd(R, modulus) = 1). **Parameters:** | Name | Type | Description | Default | | --------- | ------ | ----------------------------------------------------------------- | ---------- | | `modulus` | `CInt` | Classical number modulus | *required* | | `x` | `QNum` | Quantum number, in-place operand to convert from Montgomery form. | *required* | ### modular\_inverse\_inplace
modular\_inverse\_inplace(
modulus: CInt,
v: QNum,
m: Output\[QArray\[QBit]]
) -> None
\[Qmod Classiq-library function] Computes the modular inverse of a quantum number `|v>` modulo modulus in place, using the Kaliski algorithm. Performs the transformation `|v>` -> |(v^-1 mod modulus)>. Based on: [https://arxiv.org/pdf/2302.06639](https://arxiv.org/pdf/2302.06639) Chapter 5 Note: `\|v>` should have values smaller than `modulus`. If `|v>` = 0, the output will be 0 (although 0 does not have an inverse modulo `modulus`). The modulus should be prime OR at least gcd(v, modulus) = 1. The modulus must be a constant odd integer. The modulus should satisfy 1 \< modulus \< 2\*\*n, where n is the size of `|v>`. The ancilla qubits m are provided as Output, will be allocated to length 2\*n. **Parameters:** | Name | Type | Description | Default | | --------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------- | ---------- | | `modulus` | `CInt` | Classical number modulus | *required* | | `v` | `QNum` | Quantum number, in-place operand to compute the modular inverse. | *required* | | `m` | `Output[QArray[QBit]]` | Output quantum array (QArray\[QBit]) allocated to length 2\*n (n = v.size) and used as ancilla during the algorithm. | *required* | ### kaliski\_iteration
kaliski\_iteration(
modulus: CInt,
i: CInt,
v: QNum,
m: QArray\[QBit],
u: QNum,
r: QNum,
s: QNum,
a: QBit,
b: QBit,
f: QBit
) -> None
Single iteration of the Kaliski modular inverse algorithm main loop. Based on: [https://arxiv.org/pdf/2302.06639](https://arxiv.org/pdf/2302.06639) Figure 15 Note: Assumes the global inversion constraints (odd modulus, 1 \< modulus \< 2\*\*n). Called with 0 \<= v \< modulus; per-iteration ancilla bit is m\[i]. **Parameters:** | Name | Type | Description | Default | | --------- | -------------- | ----------------------------------------------------- | ---------- | | `modulus` | `CInt` | Classical number modulus (CInt) | *required* | | `i` | `CInt` | Loop iteration index. | *required* | | `v` | `QNum` | The QNum to invert (quantum number, will be mutated). | *required* | | `m` | `QArray[QBit]` | Quantum array of ancilla qubits (QArray\[QBit]). | *required* | | `u` | `QNum` | QNum (quantum number, auxiliary for algorithm). | *required* | | `r` | `QNum` | QNum (quantum number, auxiliary). | *required* | | `s` | `QNum` | QNum (quantum number, auxiliary). | *required* | | `a` | `QBit` | QBit (ancilla qubit) | *required* | | `b` | `QBit` | QBit (ancilla qubit) | *required* | | `f` | `QBit` | QBit (ancilla qubit) | *required* | ### modular\_rsub\_inplace
modular\_rsub\_inplace(
modulus: CInt,
a: CInt,
x: QNum
) -> None
\[Qmod Classiq-library function] Performs the in-place modular right-subtraction `|x>` -> |(a - x mod modulus)>. Note: `\|x>` should have values smaller than `modulus`. The modulus should satisfy 1 \< modulus \< 2\*\*n, where n is the size of `|x>`. The classical constant `a` should be in the range 0 \<= a \< modulus. **Parameters:** | Name | Type | Description | Default | | --------- | ------ | -------------------------------------------------------------------------- | ---------- | | `modulus` | `CInt` | Classical number modulus | *required* | | `a` | `CInt` | Classical constant to subtract from | *required* | | `x` | `QNum` | Quantum number, in-place operand to perform the modular right-subtraction. | *required* | # Qaoa Penalty Source: https://docs.classiq.io/sdk-reference/qmod/functions/open_library/qaoa_penalty Functions: | Name | Description | | ------------------ | --------------------------------- | | `qaoa_mixer_layer` | \[Qmod Classiq-library function]. | | `qaoa_cost_layer` | \[Qmod Classiq-library function]. | | `qaoa_layer` | \[Qmod Classiq-library function]. | | `qaoa_init` | \[Qmod Classiq-library function]. | | `qaoa_penalty` | \[Qmod Classiq-library function]. | ### qaoa\_mixer\_layer
qaoa\_mixer\_layer(
b: CReal,
target: QArray\[QBit]
) -> None
\[Qmod Classiq-library function] Applies the mixer layer for the QAOA algorithm. The mixer layer is a sequence of `X` gates applied to each qubit in the target quantum array variable. **Parameters:** | Name | Type | Description | Default | | -------- | -------------- | ------------------------------------------- | ---------- | | `b` | `CReal` | The rotation parameter for the mixer layer. | *required* | | `target` | `QArray[QBit]` | The target quantum array. | *required* | ### qaoa\_cost\_layer
qaoa\_cost\_layer(
g: CReal,
hamiltonian: CArray\[PauliTerm],
target: QArray\[QBit]
) -> None
\[Qmod Classiq-library function] Applies the cost layer to the QAOA model. This function integrates the problem-specific cost function into the QAOA model's objective function. The cost layer represents the primary objective that the QAOA algorithm seeks to optimize, such as minimizing energy or maximizing profit, depending on the application. **Parameters:** | Name | Type | Description | Default | | ------------- | ------------------- | ------------------------------------------------------ | ---------- | | `g` | `CReal` | The rotation parameter for the cost layer (prefactor). | *required* | | `hamiltonian` | `CArray[PauliTerm]` | The Hamiltonian terms for the QAOA model. | *required* | | `target` | `QArray[QBit]` | The target quantum array variable. | *required* | ### qaoa\_layer
qaoa\_layer(
g: CReal,
b: CReal,
hamiltonian: CArray\[PauliTerm],
target: QArray\[QBit]
) -> None
\[Qmod Classiq-library function] Applies the QAOA layer, which concatenates the cost layer and the mixer layer. The `qaoa_layer` function integrates both the cost and mixer layers, essential components of the Quantum Approximate Optimization Algorithm (QAOA). The cost layer encodes the problem's objective, while the mixer layer introduces quantum superposition and drives the search across the solution space. **Parameters:** | Name | Type | Description | Default | | ------------- | ------------------- | ------------------------------------------- | ---------- | | `g` | `CReal` | The rotation parameter for the cost layer. | *required* | | `b` | `CReal` | The rotation parameter for the mixer layer. | *required* | | `hamiltonian` | `CArray[PauliTerm]` | The Hamiltonian terms for the QAOA model. | *required* | | `target` | `QArray[QBit]` | The target quantum array variable. | *required* | ### qaoa\_init
qaoa\_init(
target: QArray\[QBit]
) -> None
\[Qmod Classiq-library function] Initializes the QAOA circuit by applying the Hadamard gate to all qubits. In the Quantum Approximate Optimization Algorithm (QAOA), the initial state is a uniform superposition created by applying the Hadamard gate to each qubit. This function prepares the qubits for the subsequent application of the cost and mixer layers by preparing them in an equal superposition state. **Parameters:** | Name | Type | Description | Default | | -------- | -------------- | ---------------------------------- | ---------- | | `target` | `QArray[QBit]` | The target quantum array variable. | *required* | ### qaoa\_penalty
qaoa\_penalty(
num\_qubits: CInt,
params\_list: CArray\[CReal],
hamiltonian: CArray\[PauliTerm],
target: QArray\[QBit, Literal\['num\_qubits']]
) -> None
\[Qmod Classiq-library function] Applies the penalty layer to the QAOA model. This function adds a penalty term to the objective function of the QAOA model to enforce certain constraints (e.g., binary or integer variables) during the optimization process. **Parameters:** | Name | Type | Description | Default | | ------------- | ------------------------------------- | -------------------------------------------- | ---------- | | `num_qubits` | `CInt` | The number of qubits in the quantum circuit. | *required* | | `params_list` | `CArray[CReal]` | | *required* | | `hamiltonian` | `CArray[PauliTerm]` | The Hamiltonian terms for the QAOA model. | *required* | | `target` | `QArray[QBit, Literal['num_qubits']]` | The target quantum array variable. | *required* | # Qft Functions Source: https://docs.classiq.io/sdk-reference/qmod/functions/open_library/qft_functions Functions: | Name | Description | | ------------- | --------------------------------- | | `qft_no_swap` | \[Qmod Classiq-library function]. | | `qft` | \[Qmod Classiq-library function]. | ### qft\_no\_swap
qft\_no\_swap(
qbv: QArray\[QBit]
) -> None
\[Qmod Classiq-library function] Applies the Quantum Fourier Transform (QFT) without the swap gates. **Parameters:** | Name | Type | Description | Default | | ----- | -------------- | ----------------------------------------------- | ---------- | | `qbv` | `QArray[QBit]` | The quantum number to which the QFT is applied. | *required* | ### qft
qft(
target: QArray\[QBit]
) -> None
\[Qmod Classiq-library function] Performs the Quantum Fourier Transform (QFT) on `target` in-place. Implements the following transformation: $$ y_{k} = \frac{1}{\sqrt{N}} \sum_{j=0}^{N-1} x_j e^{2\pi i \frac{jk}{N}} $$ **Parameters:** | Name | Type | Description | Default | | -------- | -------------- | ------------------------------------ | ---------- | | `target` | `QArray[QBit]` | The quantum object to be transformed | *required* | # Qpe Source: https://docs.classiq.io/sdk-reference/qmod/functions/open_library/qpe Functions: | Name | Description | | -------------- | --------------------------------- | | `qpe_flexible` | \[Qmod Classiq-library function]. | | `qpe` | \[Qmod Classiq-library function]. | ### qpe\_flexible
qpe\_flexible(
unitary\_with\_power: QCallable\[CInt],
phase: QArray\[QBit]
) -> None
\[Qmod Classiq-library function] Implements the Quantum Phase Estimation (QPE) algorithm, which estimates the phase (eigenvalue) associated with an eigenstate of a given unitary operator $U$. This is a flexible version that allows the user to provide a callable that generates the unitary operator $U^k$ for a given integer $k$, offering greater flexibility in handling different quantum circuits using some powering rule. **Parameters:** | Name | Type | Description | Default | | -------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- | | `unitary_with_power` | `QCallable[CInt]` | A callable that returns the unitary operator $U^k$ given an integer $k$. This callable is used to control the application of powers of the unitary operator. | *required* | | `phase` | `QArray[QBit]` | The quantum variable that represents the estimated phase (eigenvalue), assuming initialized to zero. | *required* | ### qpe
qpe(
unitary: QCallable,
phase: QNum
) -> None
\[Qmod Classiq-library function] Implements the standard Quantum Phase Estimation (QPE) algorithm, which estimates the phase (eigenvalue) associated with an eigenstate of a given unitary operator $U$. **Parameters:** | Name | Type | Description | Default | | --------- | ----------- | ---------------------------------------------------------------------------------------------------- | ---------- | | `unitary` | `QCallable` | A callable representing the unitary operator $U$, whose eigenvalue is to be estimated. | *required* | | `phase` | `QNum` | The quantum variable that represents the estimated phase (eigenvalue), assuming initialized to zero. | *required* | # Qsvt Source: https://docs.classiq.io/sdk-reference/qmod/functions/open_library/qsvt Functions: | Name | Description | | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | `qsvt_step` | \[Qmod Classiq-library function]. | | `qsvt` | \[Qmod Classiq-library function]. | | `projector_controlled_phase` | \[Qmod Classiq-library function]. | | `qsvt_inversion` | \[Qmod Classiq-library function]. | | `projector_controlled_double_phase` | \[Qmod Classiq-library function]. | | `qsvt_lcu_step` | \[Qmod Classiq-library function]. | | `qsvt_lcu` | \[Qmod Classiq-library function]. | | `gqsp` | Implements Generalized Quantum Signal Processing (GQSP), which realizes a (Laurent) polynomial transformation of degree d on the eigenvalues of the... | ### qsvt\_step
qsvt\_step(
phase1: CReal,
phase2: CReal,
proj\_cnot\_1: QCallable\[QBit],
proj\_cnot\_2: QCallable\[QBit],
u: QCallable,
aux: QBit
) -> None
\[Qmod Classiq-library function] Applies a single QSVT step, composed of 2 projector-controlled-phase rotations, and applications of the block encoding unitary `u` and its inverse: $$ \Pi_{\phi_2}U^{\dagger}\tilde{\Pi}_{\phi_{1}}U $$ **Parameters:** | Name | Type | Description | Default | | ------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `phase1` | `CReal` | 1st rotation phase. | *required* | | `phase2` | `CReal` | 2nd rotation phase. | *required* | | `proj_cnot_1` | `QCallable[QBit]` | Projector-controlled-not unitary that locates the encoded matrix columns within U. Accepts a qubit that should be set to \`\|1>\` when the state is in the block. | *required* | | `proj_cnot_2` | `QCallable[QBit]` | Projector-controlled-not unitary that locates the encoded matrix rows within U. Accepts a qubit that should be set to \`\|1>\` when the state is in the block. | *required* | | `u` | `QCallable` | A block encoding unitary matrix. | *required* | | `aux` | `QBit` | A zero auxilliary qubit, used for the projector-controlled-phase rotations. Given as an inout so that qsvt can be used as a building-block in a larger algorithm. | *required* | ### qsvt
qsvt(
phase\_seq: CArray\[CReal],
proj\_cnot\_1: QCallable\[QBit],
proj\_cnot\_2: QCallable\[QBit],
u: QCallable,
aux: QBit
) -> None
\[Qmod Classiq-library function] Implements the Quantum Singular Value Transformation (QSVT) - an algorithmic framework, used to apply polynomial transformations of degree `d` on the singular values of a block encoded matrix, given as the unitary `u`. Given a unitary $U$, a list of phase angles $\phi_1, \phi_2, ..., \phi_\{d+1\}$ and 2 projector-controlled-not operands $C_\{\Pi\}NOT,C_\{\tilde\{\Pi\}\}NOT$, the QSVT sequence is as follows: Given a unitary $U$, a list of phase angles $\phi_1, \phi_2, ..., \phi_\{d+1\}$ and 2 projector-controlled-not operands $C_\{\Pi\}NOT,C_\{\tilde\{\Pi\}\}NOT$, the QSVT sequence is as follows: $$ \tilde{\Pi}_{\phi_{d+1}}U \prod_{k=1}^{(d-1)/2} (\Pi_{\phi_{d-2k}} U^{\dagger}\tilde{\Pi}_{\phi_{d - (2k+1)}}U)\Pi_{\phi_{1}} $$ for odd $d$, and: $$ \prod_{k=1}^{d/2} (\Pi_{\phi_{d-(2k-1)}} U^{\dagger}\tilde{\Pi}_{\phi_{d-2k}}U)\Pi_{\phi_{1}} $$ for even $d$. Each of the $\Pi$s is a projector-controlled-phase unitary, according to the given projectors. **Parameters:** | Name | Type | Description | Default | | ------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `phase_seq` | `CArray[CReal]` | A sequence of phase angles of length d+1. | *required* | | `proj_cnot_1` | `QCallable[QBit]` | Projector-controlled-not unitary that locates the encoded matrix columns within U. Accepts a qubit that should be set to \`\|1>\` when the state is in the block. | *required* | | `proj_cnot_2` | `QCallable[QBit]` | Projector-controlled-not unitary that locates the encoded matrix rows within U. Accepts a qubit that should be set to \`\|1>\` when the state is in the block. | *required* | | `u` | `QCallable` | A block encoding unitary matrix. | *required* | | `aux` | `QBit` | A zero auxilliary qubit, used for the projector-controlled-phase rotations. Given as an inout so that qsvt can be used as a building-block in a larger algorithm. | *required* | ### projector\_controlled\_phase
projector\_controlled\_phase(
phase: CReal,
proj\_cnot: QCallable\[QBit],
aux: QBit
) -> None
\[Qmod Classiq-library function] Assigns a phase to the entire subspace determined by the given projector. Corresponds to the operation: $$ \Pi_{\phi} = (C_{\Pi}NOT) e^{-i rac{\phi}{2}Z}(C_{\Pi}NOT) $$ **Parameters:** | Name | Type | Description | Default | | ----------- | ----------------- | --------------------------------------------------------------------------------------------------------------- | ---------- | | `phase` | `CReal` | A rotation phase. | *required* | | `proj_cnot` | `QCallable[QBit]` | Projector-controlled-not unitary that sets an auxilliary qubit to \`\|1>\` when the state is in the projection. | *required* | | `aux` | `QBit` | A zero auxilliary qubit, used for the projector-controlled-phase rotation. | *required* | ### qsvt\_inversion
qsvt\_inversion(
phase\_seq: CArray\[CReal],
block\_encoding\_cnot: QCallable\[QBit],
u: QCallable,
aux: QBit
) -> None
\[Qmod Classiq-library function] Implements matrix inversion on a given block-encoding of a square matrix, using the QSVT framework. Applies a polynomial approximation of the inverse of the singular values of the matrix encoded in `u`. The phases for the polynomial should be pre-calculated and passed into the function. **Parameters:** | Name | Type | Description | Default | | --------------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `phase_seq` | `CArray[CReal]` | A sequence of phase angles of length d+1, corresponding to an odd polynomial approximation of the scaled inverse function. | *required* | | `block_encoding_cnot` | `QCallable[QBit]` | Projector-controlled-not unitary that locates the encoded matrix columns within U. Accepts a quantum variable that should be set to \`\|1>\` when the state is in the block. | *required* | | `u` | `QCallable` | A block encoding unitary matrix. | *required* | | `aux` | `QBit` | A zero auxilliary qubit, used for the projector-controlled-phase rotations. Given as an inout so that qsvt can be used as a building-block in a larger algorithm. | *required* | ### projector\_controlled\_double\_phase
projector\_controlled\_double\_phase(
phase\_even: CReal,
phase\_odd: CReal,
proj\_cnot: QCallable\[QBit],
aux: QBit,
lcu: QBit
) -> None
\[Qmod Classiq-library function] Assigns 2 phases to the entire subspace determined by the given projector, each one is controlled differentely on a given `lcu` qvar. Used in the context of the `qsvt_lcu` function. Corresponds to the operation: $$ \Pi_{\phi_{odd}, \phi_{even}} = (C_{\Pi}NOT) (C_{lcu=1}e^{-i\frac{\phi_{even}}{2}Z}) (C_{lcu=0}e^{-i\frac{\phi_{odd}}{2}Z}) (C_{\Pi}NOT) $$ **Parameters:** | Name | Type | Description | Default | | ------------ | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `phase_even` | `CReal` | Rotation phase, corresponds to 'lcu'=0. | *required* | | `phase_odd` | `CReal` | Rotation phase, corresponds to 'lcu'=1. | *required* | | `proj_cnot` | `QCallable[QBit]` | Projector-controlled-not unitary that sets an auxilliary qubit to \`\|1>\` when the state is in the projection. | *required* | | `aux` | `QBit` | A zero auxilliary qubit, used for the projector-controlled-phase rotation. Given as an inout so that qsvt can be used as a building-block in a larger algorithm. | *required* | | `lcu` | `QBit` | The quantum variable used for controlling the phase assignment. | *required* | ### qsvt\_lcu\_step
qsvt\_lcu\_step(
phases\_even: CArray\[CReal],
phases\_odd: CArray\[CReal],
proj\_cnot\_1: QCallable\[QBit],
proj\_cnot\_2: QCallable\[QBit],
u: QCallable,
aux: QBit,
lcu: QBit
) -> None
\[Qmod Classiq-library function] Applies a single QSVT-lcu step, composed of 2 double phase projector-controlled-phase rotations, and applications of the block encoding unitary `u` and its inverse: $$ (C_{lcu=1}\Pi^{even}_{\phi_2})(C_{lcu=0}\Pi^{odd}_{\phi_2})U^{\dagger}(C_{lcu=1}\tilde{\Pi}^{even}_{\phi_1})(C_{lcu=0}\tilde{\Pi}^{odd}_{\phi_1})U $$ **Parameters:** | Name | Type | Description | Default | | ------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `phases_even` | `CArray[CReal]` | 2 rotation phases for the even polynomial | *required* | | `phases_odd` | `CArray[CReal]` | 2 rotation phases for the odd polynomial | *required* | | `proj_cnot_1` | `QCallable[QBit]` | Projector-controlled-not unitary that locates the encoded matrix columns within U. Accepts a qubit that should be set to \`\|1>\` when the state is in the block. | *required* | | `proj_cnot_2` | `QCallable[QBit]` | Projector-controlled-not unitary that locates the encoded matrix rows within U. Accepts a qubit that should be set to \`\|1>\` when the state is in the block. | *required* | | `u` | `QCallable` | A block encoding unitary matrix. | *required* | | `aux` | `QBit` | A zero auxilliary qubit, used for the projector-controlled-phase rotations. Given as an inout so that qsvt can be used as a building-block in a larger algorithm. | *required* | | `lcu` | `QBit` | A qubit used for the combination of 2 polynomials within a single qsvt application | *required* | ### qsvt\_lcu
qsvt\_lcu(
phase\_seq\_even: CArray\[CReal],
phase\_seq\_odd: CArray\[CReal],
proj\_cnot\_1: QCallable\[QBit],
proj\_cnot\_2: QCallable\[QBit],
u: QCallable,
aux: QBit,
lcu: QBit
) -> None
\[Qmod Classiq-library function] Implements the Quantum Singular Value Transformation (QSVT) for a linear combination of odd and even polynomials, so that it is possible to encode a polynomial of indefinite parity, such as approximation to exp(i\*A) or exp(A). Should work for Hermitian block encodings. The function is equivalent to applying the `qsvt` function for odd and even polynomials with a LCU function, but is more efficient as the two polynomials share the same applications of the given unitary. The function is intended to be called within a context of LCU, where it is called as the SELECT operator, and wrapped with initialization of the `lcu` qubit to get the desired combination coefficients. The even polynomial corresponds to the case where the $lcu=|0\rangle$, while the odd to $lcu=|1\rangle$. Note: the two polynomials should have the same degree up to a difference of 1. **Parameters:** | Name | Type | Description | Default | | ---------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `phase_seq_even` | `CArray[CReal]` | A sequence of phase angles of length d+(d+1)%2 for the even polynomial. | *required* | | `phase_seq_odd` | `CArray[CReal]` | A sequence of phase angles of length d+(d%2) for the odd polynomial. | *required* | | `proj_cnot_1` | `QCallable[QBit]` | Projector-controlled-not unitary that locates the encoded matrix columns within U. Accepts a qubit that should be set to \`\|1>\` when the state is in the block. | *required* | | `proj_cnot_2` | `QCallable[QBit]` | Projector-controlled-not unitary that locates the encoded matrix rows within U. Accepts a qubit that should be set to \`\|1>\` when the state is in the block. | *required* | | `u` | `QCallable` | A block encoding unitary matrix. | *required* | | `aux` | `QBit` | A zero auxilliary qubit, used for the projector-controlled-phase rotations. Given as an inout so that qsvt can be used as a building-block in a larger algorithm. | *required* | | `lcu` | `QBit` | A qubit used for the combination of 2 polynomials within a single qsvt application | *required* | ### gqsp
gqsp(
u: QCallable,
aux: QBit,
phases: CArray\[CArray\[CReal, Literal\[3]]],
negative\_power: CInt
) -> None
Implements Generalized Quantum Signal Processing (GQSP), which realizes a (Laurent) polynomial transformation of degree d on the eigenvalues of the given signal unitary `u`. The protocol is according to [https://arxiv.org/abs/2308.01501](https://arxiv.org/abs/2308.01501) Fig.2. Notes: * The user is encouraged to use the function `gqsp_phases` to find `phases` that correspond to the wanted polynomial transformation. * Feasibility: the target polynomial must satisfy $|P(e^\{i*theta\})|$ \<= 1 for all theta in $[0, 2*pi)$. This ensures a unitary completion exists. * Using `negative_power = m` (m >= 0) you can realize Laurent polynomials with negative exponents: the implemented transform is equivalent to applying $z^\{-m\} * P(z)$ (i.e., shift the minimal degree to -m). For ordinary (non-Laurent) polynomials, set `negative_power = 0`. **Parameters:** | Name | Type | Description | Default | | ---------------- | ----------------------------------- | ------------------------------------------------------------------------------------------ | ---------- | | `u` | `QCallable` | The signal unitary. | *required* | | `aux` | `QBit` | Auxiliary qubit used for the phase rotations. Should start in \`\|0>\`. | *required* | | `phases` | `CArray[CArray[CReal, Literal[3]]]` | (d+1) x 3 real array of angles: each element is (theta, phi, lambda). | *required* | | `negative_power` | `CInt` | Integer m in \[0, d]. Encodes the minimal Laurent power -m of the realized transformation. | *required* | # State Preparation Source: https://docs.classiq.io/sdk-reference/qmod/functions/open_library/state_preparation Functions: | Name | Description | | ------------------------------------ | --------------------------------- | | `prepare_uniform_trimmed_state` | \[Qmod Classiq-library function]. | | `prepare_uniform_interval_state` | \[Qmod Classiq-library function]. | | `prepare_ghz_state` | \[Qmod Classiq-library function]. | | `prepare_exponential_state` | \[Qmod Classiq-library function]. | | `prepare_bell_state` | \[Qmod Classiq-library function]. | | `inplace_prepare_int` | \[Qmod Classiq-library function]. | | `prepare_int` | \[Qmod Classiq-library function]. | | `prepare_complex_amplitudes` | \[Qmod Classiq-library function]. | | `inplace_prepare_complex_amplitudes` | \[Qmod Classiq-library function]. | | `prepare_dicke_state` | \[Qmod Classiq-library function]. | | `prepare_dicke_state_unary_input` | \[Qmod Classiq-library function]. | | `prepare_basis_state` | \[Qmod Classiq-library function]. | | `prepare_linear_amplitudes` | \[Qmod Classiq-library function]. | | `prepare_sparse_amplitudes` | \[Qmod Classiq-library function]. | | `inplace_prepare_sparse_amplitudes` | \[Qmod Classiq-library function]. | ### prepare\_uniform\_trimmed\_state
prepare\_uniform\_trimmed\_state(
m: CInt,
q: QArray\[QBit]
) -> None
\[Qmod Classiq-library function] Initializes a quantum variable in a uniform superposition of the first `m` computational basis states: $$ \left|\text{q}\right\rangle = \frac{1}{\sqrt{m}}\sum_{i=0}^{m-1}{|i\rangle} $$ The number of allocated qubits would be $\left\lceil\log_2\{m\}\right\rceil$. The function is especially useful when `m` is not a power of 2. **Parameters:** | Name | Type | Description | Default | | ---- | -------------- | ------------------------------------------------------------------------------------ | ---------- | | `m` | `CInt` | The number of states to load in the superposition. | *required* | | `q` | `QArray[QBit]` | The quantum variable that will receive the initialized state. Must be uninitialized. | *required* | ### prepare\_uniform\_interval\_state
prepare\_uniform\_interval\_state(
start: CInt,
end: CInt,
q: QNum
) -> None
\[Qmod Classiq-library function] Initializes a quantum variable in a uniform superposition of the specified interval in the computational basis states: $$ \left|\text{q}\right\rangle = \frac{1}{\sqrt{\text{end} - \text{start}}}\sum_{i=\text{start}}^{\text{end}-1}{|i\rangle} $$ The number of allocated qubits would be $\left\lceil\log_2\{\left(\text\{end\}\right)\}\right\rceil$. **Parameters:** | Name | Type | Description | Default | | ------- | ------ | ------------------------------------------------------------------------------------ | ---------- | | `start` | `CInt` | The lower bound of the interval to load (inclusive). | *required* | | `end` | `CInt` | The upper bound of the interval to load (exclusive). | *required* | | `q` | `QNum` | The quantum variable that will receive the initialized state. Must be uninitialized. | *required* | ### prepare\_ghz\_state
prepare\_ghz\_state(
size: CInt,
q: Output\[QArray\[QBit]]
) -> None
\[Qmod Classiq-library function] Initializes a quantum variable in a Greenberger-Horne-Zeilinger (GHZ) state. i.e., a balanced superposition of all ones and all zeros, on an arbitrary number of qubits.. **Parameters:** | Name | Type | Description | Default | | ------ | ---------------------- | ------------------------------------------------------------------------------------ | ---------- | | `size` | `CInt` | The number of qubits in the GHZ state. Must be a positive integer. | *required* | | `q` | `Output[QArray[QBit]]` | The quantum variable that will receive the initialized state. Must be uninitialized. | *required* | ### prepare\_exponential\_state
prepare\_exponential\_state(
rate: CInt,
q: QArray\[QBit]
) -> None
\[Qmod Classiq-library function] Prepares a quantum state with exponentially decreasing amplitudes. The state is prepared in the computational basis, with the amplitudes of the states decreasing exponentially with the index of the state: $$ P(n) = \frac{1}{Z} e^{- \text{rate} \cdot n} $$ **Parameters:** | Name | Type | Description | Default | | ------ | -------------- | ---------------------------------- | ---------- | | `rate` | `CInt` | The rate of the exponential decay. | *required* | | `q` | `QArray[QBit]` | The quantum register to prepare. | *required* | ### prepare\_bell\_state
prepare\_bell\_state(
state\_num: CInt,
qpair: Output\[QArray\[QBit, Literal\[2]]]
) -> None
\[Qmod Classiq-library function] Initializes a quantum array of size 2 in one of the four Bell states. **Parameters:** | Name | Type | Description | Default | | ----------- | ---------------------------------- | ------------------------------------------------------------------------------------ | ---------- | | `state_num` | `CInt` | The number of the Bell state to be prepared. Must be an integer between 0 and 3. | *required* | | `qpair` | `Output[QArray[QBit, Literal[2]]]` | The quantum variable that will receive the initialized state. Must be uninitialized. | *required* | ### inplace\_prepare\_int
inplace\_prepare\_int(
value: CInt,
target: QNum
) -> None
\[Qmod Classiq-library function] This function is **deprecated**. Use in-place-xor assignment statement in the form *target-var* **^=** *quantum-expression* or **inplace\_xor(***quantum-expression***,** *target-var*\*\*)\*\* instead. Transitions a quantum variable in the zero state $|0\rangle$ into the computational basis state $|\text\{value\}\rangle$. In the general case, the function performs a bitwise-XOR, i.e. transitions the state $|\psi\rangle$ into $|\psi \oplus \text\{value\}\rangle$. **Parameters:** | Name | Type | Description | Default | | -------- | ------ | -------------------------------------------- | ---------- | | `value` | `CInt` | The value to assign to the quantum variable. | *required* | | `target` | `QNum` | The quantum variable to act upon. | *required* | ### prepare\_int
prepare\_int(
value: CInt,
out: Output\[QNum\[Literal\['floor(log(value, 2)) + 1']]]
) -> None
\[Qmod Classiq-library function] This function is **deprecated**. Use assignment statement in the form *target-var* **|=** *quantum-expression* or **assign(***quantum-expression***,** *target-var*\*\*)\*\* instead. Initializes a quantum variable to the computational basis state $|\text\{value\}\rangle$. The number of allocated qubits is automatically computed from the value, and is the minimal number required for representation in the computational basis. **Parameters:** | Name | Type | Description | Default | | ------- | --------------------------------------------------- | ------------------------------------------------------ | ---------- | | `value` | `CInt` | The value to assign to the quantum variable. | *required* | | `out` | `Output[QNum[Literal['floor(log(value, 2)) + 1']]]` | The allocated quantum variable. Must be uninitialized. | *required* | ### prepare\_complex\_amplitudes
prepare\_complex\_amplitudes(
magnitudes: CArray\[CReal],
phases: list\[float],
out: Output\[QArray\[QBit, Literal\['log(magnitudes.len, 2)']]]
) -> None
\[Qmod Classiq-library function] Initializes and prepares a quantum state with amplitudes and phases for each state according to the given parameters, in polar representation. **Parameters:** | Name | Type | Description | Default | | ------------ | --------------------------------------------------------- | --------------------------------------------------------------------------- | ---------- | | `magnitudes` | `CArray[CReal]` | Absolute values of the state amplitudes. | *required* | | `phases` | `list[float]` | phases of the state amplitudes. should be of the same size as `amplitudes`. | *required* | | `out` | `Output[QArray[QBit, Literal['log(magnitudes.len, 2)']]]` | The allocated quantum variable. Must be uninitialized. | *required* | ### inplace\_prepare\_complex\_amplitudes
inplace\_prepare\_complex\_amplitudes(
magnitudes: CArray\[CReal],
phases: list\[float],
target: QArray\[QBit, Literal\['log(magnitudes.len, 2)']]
) -> None
\[Qmod Classiq-library function] Prepares a quantum state with amplitudes and phases for each state according to the given parameters, in polar representation. Expects to act on an initialized zero state $|0\rangle$. **Parameters:** | Name | Type | Description | Default | | ------------ | ------------------------------------------------- | --------------------------------------------------------------------------- | ---------- | | `magnitudes` | `CArray[CReal]` | Absolute values of the state amplitudes. | *required* | | `phases` | `list[float]` | phases of the state amplitudes. should be of the same size as `amplitudes`. | *required* | | `target` | `QArray[QBit, Literal['log(magnitudes.len, 2)']]` | The quantum variable to act upon. | *required* | ### prepare\_dicke\_state
prepare\_dicke\_state(
k: int,
qvar: QArray\[QBit]
) -> None
\[Qmod Classiq-library function] Prepares a Dicke state with k excitations over the provided quantum register. A Dicke state of n qubits with k excitations is an equal superposition of all basis states with exactly k qubits in the $|1\rangle$ state and $(n - k)$ qubits in the $|0\rangle$ state. For example, $\mathrm\{Dicke\}(2, 1) = (|01\rangle + |10\rangle) / \sqrt(2)$. In the general case it is defined to be: $\mathrm\{Dicke\}(n, k) = \frac\{1\}\{\sqrt\{\binom\{n\}\{k\}\}\} \sum_\{x \in \\{0,1\\}^n,\, |x| = k\} |x\rangle$ **Parameters:** | Name | Type | Description | Default | | ------ | -------------- | ------------------------------------------------------------------------------------------------- | ---------- | | `k` | `int` | The number of excitations (i.e., number of qubits in state $\|1\rangle$). | *required* | | `qvar` | `QArray[QBit]` | The quantum register (array of qubits) to initialize. Must be uninitialized and have length >= k. | *required* | ### prepare\_dicke\_state\_unary\_input
prepare\_dicke\_state\_unary\_input(
max\_k: int,
qvar: QArray\[QBit]
) -> None
\[Qmod Classiq-library function] Prepares a Dicke state with a variable number of excitations based on a unary-encoded input. The Dicke state is defined to be: $\mathrm\{Dicke\}(n, k) = \frac\{1\}\{\sqrt\{\binom\{n\}\{k\}\}\} \sum_\{x \in \\{0,1\\}^n,\, |x| = k\} |x\rangle$ The input register `qvar` is expected to already be initialized in a unary encoding: the value k is represented by a string of k ones followed by zeros, e.g., k = 3 -> |11100...0>. The function generates a Dicke state with k excitations over a new quantum register, where 0 \<= k \< max\_k. **Parameters:** | Name | Type | Description | Default | | ------- | -------------- | ---------------------------------------------------------------------------------- | ---------- | | `max_k` | `int` | The maximum number of allowed excitations (upper bound for k). | *required* | | `qvar` | `QArray[QBit]` | Unary-encoded quantum input register of length >= max\_k. Must be pre-initialized. | *required* | ### prepare\_basis\_state
prepare\_basis\_state(
state: list\[bool],
arr: Output\[QArray]
) -> None
\[Qmod Classiq-library function] Initializes a quantum array in the specified basis state. **Parameters:** | Name | Type | Description | Default | | ------- | ---------------- | ----------------------------- | ---------- | | `state` | `list[bool]` | | *required* | | `arr` | `Output[QArray]` | The quantum array to prepare. | *required* | ### prepare\_linear\_amplitudes
prepare\_linear\_amplitudes(
x: QArray
) -> None
\[Qmod Classiq-library function] Initializes a quantum variable in a state with linear amplitudes: $$$|\psi angle = rac\{1\}\{Z\}\sum_\{x=0\}^\{2^n-1\}\{x|x angle\}$$ Where $Z$ is a normalization constant. Based on the paper https://quantum-journal.org/papers/q-2024-03-21-1297/pdf/ **Parameters:** | Name | Type | Description | Default | | ---- | ---- | ----------- | ------- | | `x` | `QArray` | The quantum register to prepare. | *required* | ### prepare_sparse_amplitudes
prepare_sparse_amplitudes(
    states: list[int],
    amplitudes: list[complex],
    out: Output[QArray]
) -> None
[Qmod Classiq-library function] Initializes and prepares a quantum state with the given (complex) amplitudes. The input is given sparse format, as a list of non-zero states and their corresponding amplitudes. Notice that the function is only suitable sparse states. Inspired by https://arxiv.org/abs/2310.19309. For example, `prepare_sparse_amplitudes([1, 8], [np.sqrt(0.5), np.sqrt(0.5)], out)` will and allocate it to be of size 4 qubits, and prepare it in the state sqrt(0.5)`|1>` + sqrt(0.5)`|8>`. Complexity: Asymptotic gate complexity is $O(dn)$ where d is the number of states and n is the required number of qubits. **Parameters:** | Name | Type | Description | Default | | ---- | ---- | ----------- | ------- | | `states` | `list[int]` | A list of distinct computational basis indices to populate. Each integer corresponds to the basis state in the computational basis. | *required* | | `amplitudes` | `list[complex]` | A list of complex amplitudes for the corresponding entries in `states`. Must have the same length as `states`. | *required* | | `out` | `Output[QArray]` | The allocated quantum variable. | *required* | ### inplace_prepare_sparse_amplitudes
inplace_prepare_sparse_amplitudes(
    states: list[int],
    amplitudes: list[complex],
    target: QArray
) -> None
[Qmod Classiq-library function] Prepares a quantum state with the given (complex) amplitudes. The input is given sparse format, as a list of non-zero states and their corresponding amplitudes. Notice that the function is only suitable sparse states. Inspired by https://arxiv.org/abs/2310.19309. For example, `inplace_prepare_sparse_amplitudes([1, 8], [np.sqrt(0.5), np.sqrt(0.5)], target)` will prepare the state sqrt(0.5)`|1>` + sqrt(0.5)`|8>` on the target variable, assuming it starts in the `|0>` state. Complexity: Asymptotic gate complexity is $O(dn)$ where d is the number of states and n is the target number of qubits. **Parameters:** | Name | Type | Description | Default | | ---- | ---- | ----------- | ------- | | `states` | `list[int]` | A list of distinct computational basis indices to populate. Each integer corresponds to the basis state in the computational basis. | *required* | | `amplitudes` | `list[complex]` | A list of complex amplitudes for the corresponding entries in `states`. Must have the same length as `states`. | *required* | | `target` | `QArray` | The quantum variable on which the state is to be prepared. Its size must be sufficient to represent all states in `states`. | *required* | $$$ # Swap Test Source: https://docs.classiq.io/sdk-reference/qmod/functions/open_library/swap_test Functions: | Name | Description | | ----------- | --------------------------------- | | `swap_test` | \[Qmod Classiq-library function]. | ### swap\_test
swap\_test(
state1: QArray\[QBit],
state2: QArray\[QBit],
test: Output\[QBit]
) -> None
\[Qmod Classiq-library function] Tests the overlap (in terms of fidelity) of two quantum states. The fidelity of `state1` and `state2` is calculated from the probability of measuring `test` qubit in the state 0 as follows: $$ |\langle state1 | state2 \rangle |^2 = 2*Prob(test=0)-1 $$ **Parameters:** | Name | Type | Description | Default | | -------- | -------------- | ----------------------------------------------------------------------------------------------------- | ---------- | | `state1` | `QArray[QBit]` | A quantum state to check its overlap with state2. | *required* | | `state2` | `QArray[QBit]` | A quantum state to check its overlap with state1. | *required* | | `test` | `Output[QBit]` | A qubit for which the probability of measuring 0 is $0.5*(\|\langle state1 \| state2 \rangle \|^2+1)$ | *required* | # Utility Functions Source: https://docs.classiq.io/sdk-reference/qmod/functions/open_library/utility_functions Functions: | Name | Description | | -------------------- | --------------------------------- | | `apply_to_all` | \[Qmod Classiq-library function]. | | `hadamard_transform` | \[Qmod Classiq-library function]. | | `modular_increment` | \[Qmod Classiq-library function]. | ### apply\_to\_all
apply\_to\_all(
gate\_operand: QCallable\[Annotated\[QBit, target]],
target: QArray\[QBit]
) -> None
\[Qmod Classiq-library function] Applies the single-qubit operand `gate_operand` to each qubit in the qubit array `target`. **Parameters:** | Name | Type | Description | Default | | -------------- | ------------------------------------ | ---------------------------------------------------------- | ---------- | | `gate_operand` | `QCallable[Annotated[QBit, target]]` | The single-qubit gate to apply to each qubit in the array. | *required* | | `target` | `QArray[QBit]` | The qubit array to apply the gate to. | *required* | ### hadamard\_transform
hadamard\_transform(
target: QArray\[QBit]
) -> None
\[Qmod Classiq-library function] Applies Hadamard transform to the target qubits. Corresponds to the braket notation: $$ H^{\otimes n} |x\rangle = \frac{1}{\sqrt{2^n}} \sum_{y=0}^{2^n - 1} (-1)^{x \\cdot y} |y\rangle $$ **Parameters:** | Name | Type | Description | Default | | -------- | -------------- | ----------------------------------------- | ---------- | | `target` | `QArray[QBit]` | qubits to apply to Hadamard transform to. | *required* | ### modular\_increment
modular\_increment(
a: CInt,
x: QNum
) -> None
\[Qmod Classiq-library function] Adds $a$ to $x$ modulo the range of $x$, assumed that $x$ is a non-negative integer and $a$ is an integer. Mathematically it is described as: $$ x = (x+a)\ \mod \ 2^{x.size}-1 $$ **Parameters:** | Name | Type | Description | Default | | ---- | ------ | ------------------------------------------------------------ | ---------- | | `a` | `CInt` | A classical integer to be added to x. | *required* | | `x` | `QNum` | A quantum number that is assumed to be non-negative integer. | *required* | # Variational Source: https://docs.classiq.io/sdk-reference/qmod/functions/open_library/variational Functions: | Name | Description | | ----------------- | --------------------------------- | | `encode_in_angle` | \[Qmod Classiq-library function]. | | `encode_on_bloch` | \[Qmod Classiq-library function]. | ### encode\_in\_angle
encode\_in\_angle(
data: CArray\[CReal],
qba: Output\[QArray\[QBit]]
) -> None
\[Qmod Classiq-library function] Creates an angle encoding of n data points on n qubits. Applies RY($\pi$data\[i]) on qba\[i]. **Parameters:** | Name | Type | Description | Default | | ------ | ---------------------- | -------------------------------------------------- | ---------- | | `data` | `CArray[CReal]` | A classical array representing the data to encode. | *required* | | `qba` | `Output[QArray[QBit]]` | The array of qubits on which the data is encoded. | *required* | ### encode\_on\_bloch
encode\_on\_bloch(
data: CArray\[CReal],
qba: Output\[QArray]
) -> None
\[Qmod Classiq-library function] Creates a dense angle encoding of n data points on n//2 qubits. Encodes pairs of data points on a Bloch sphere, via RX($\pi$data\[2*i])RZ($\pi$data\[2*i+1]) on qba\[i]. If the length of the data is odd then RX($\pi$data\[i]) is applied on the last qubit. **Parameters:** | Name | Type | Description | Default | | ------ | ---------------- | -------------------------------------------------- | ---------- | | `data` | `CArray[CReal]` | A classical array representing the data to encode. | *required* | | `qba` | `Output[QArray]` | The QArray of QBits on which the data is encoded. | *required* | # Operations Source: https://docs.classiq.io/sdk-reference/qmod/operations This is a list of the operations that are built-in in `Qmod`. For more information regarding classical types see the `Statements` section in the [language reference](/qmod-reference/language-reference/index). Members: | Name | Description | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `QuantumLambdaFunction` | The definition of an anonymous function passed as operand to higher-level functions. | | `H` | \[Qmod core-library function]. | | `S` | \[Qmod core-library function]. | | `QBit` | A type representing a single qubit. | | `QNum` | QNum is a quantum variable that represents a numeric value, which can be either integer or fixed-point, encoded within a quantum register. | | `allocate` | Initialize a quantum variable to a new quantum object in the zero state:. | | `bind` | Reassign qubit or arrays of qubits by redirecting their logical identifiers. | | `if_` | Conditionally executes quantum operations based on a symbolic or boolean expression. | | `control` | Conditionally executes quantum operations based on the value of quantum variables or expressions. | | `skip_control` | Applies quantum statements unconditionally. | | `assign` | Initialize a scalar quantum variable using an arithmetic expression. | | `inplace_add` | Add an arithmetic expression to a quantum variable. | | `inplace_xor` | Bitwise-XOR a quantum variable with an arithmetic expression. | | `within_apply` | Given two operations $U$ and $V$, performs the composition of operations $U^\{-1\} V U$. | | `repeat` | Executes a quantum loop a specified number of times, applying a quantum operation on each iteration. | | `power` | Apply a quantum operation raised to a symbolic or integer power. | | `invert` | Apply the inverse of a quantum gate. | | `phase` | Applies a state-dependent or fixed phase shift (Z rotation) to the quantum state. | | `foreach` | Loops through the elements of a classical list, applying a quantum operation on each iteration. | | `assign_amplitude_poly_sin` | Encodes the value of the sine/cosine of a polynomial into the amplitude of the respective computational basis state:. | | `lookup_table` | Reduces a classical function into a lookup table over all the possible values of the quantum numbers. | ## QuantumLambdaFunction The definition of an anonymous function passed as operand to higher-level functions **Methods:** | Name | Description | | ------------------------------------------------- | ----------- | | [has\_generative\_blocks](#has_generative_blocks) | | | [set\_py\_callable](#set_py_callable) | | | [set\_op\_decl](#set_op_decl) | | **Attributes:** | Name | Type | Description | | ------------------- | ------------------------------- | ----------- | | `pos_rename_params` | `list[str]` | | | `body` | `StatementBlock` | | | `py_callable` | `Callable` | | | `func_decl` | `AnonQuantumOperandDeclaration` | | | `named_func_decl` | `AnonQuantumOperandDeclaration` | | ### has\_generative\_blocks
has\_generative\_blocks(
self:
) -> bool
**Parameters:** | Name | Type | Description | Default | | ------ | ---- | ----------- | ---------- | | `self` | \`\` | | *required* | ### set\_py\_callable
set\_py\_callable(
self: ,
py\_callable: Callable
) -> None
**Parameters:** | Name | Type | Description | Default | | ------------- | ---------- | ----------- | ---------- | | `self` | \`\` | | *required* | | `py_callable` | `Callable` | | *required* | ### set\_op\_decl
set\_op\_decl(
self: ,
fd: AnonQuantumOperandDeclaration
) -> None
**Parameters:** | Name | Type | Description | Default | | ------ | ------------------------------- | ----------- | ---------- | | `self` | \`\` | | *required* | | `fd` | `AnonQuantumOperandDeclaration` | | *required* | ### H
H(
target: QBit
) -> None
\[Qmod core-library function] Performs the Hadamard gate on a qubit. This operation is represented by the following matrix: $$ H = \frac{1}{\sqrt{2}} \begin{bmatrix} 1 & 1 \\ 1 & -1 \end{bmatrix} $$ **Parameters:** | Name | Type | Description | Default | | -------- | ------ | ---------------------------------------- | ---------- | | `target` | `QBit` | The qubit to apply the Hadamard gate to. | *required* | ### S
S(
target: Const\[QBit]
) -> None
\[Qmod core-library function] Performs the S gate on a qubit. This operation is represented by the following matrix: $$ S = \begin{bmatrix} 1 & 0 \\ 0 & i \end{bmatrix} $$ **Parameters:** | Name | Type | Description | Default | | -------- | ------------- | --------------------------------- | ---------- | | `target` | `Const[QBit]` | The qubit to apply the S gate to. | *required* | ## QBit A type representing a single qubit. `QBit` serves both as a placeholder for a temporary, non-allocated qubit and as the type of an allocated physical or logical qubit. Conceptually, a qubit is a two-level quantum system, described by the superposition of the computational basis states: $$ |0\rangle = \begin{pmatrix} 1 \\ 0 \end{pmatrix}, \quad |1\rangle = \begin{pmatrix} 0 \\ 1 \end{pmatrix} $$ Therefore, a qubit state is a linear combination: $$ |\psi\rangle = \alpha |0\rangle + \beta |1\rangle, $$ where ( \alpha ) and ( \beta ) are complex numbers satisfying: $$ |\alpha|^2 + |\beta|^2 = 1. $$ Typical usage includes: * Representing an unallocated qubit before its allocation. * Acting as the output type for a qubit or an allocated qubit in the main function after calling an allocation function. Examples: **Methods:** | Name | Description | | --------------------------------- | ----------- | | [to\_qvar](#to_qvar) | | | [get\_qmod\_type](#get_qmod_type) | | ### to\_qvar
to\_qvar(
cls: ,
origin: str | Expr,
type\_hint: Any
) -> QBit
**Parameters:** | Name | Type | Description | Default | | ----------- | ------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `origin` | `str \| Expr` | | *required* | | `type_hint` | `Any` | | *required* | ### get\_qmod\_type
get\_qmod\_type(
self:
) -> ConcreteQuantumType
**Parameters:** | Name | Type | Description | Default | | ------ | ---- | ----------- | ---------- | | `self` | \`\` | | *required* | ## QNum QNum is a quantum variable that represents a numeric value, which can be either integer or fixed-point, encoded within a quantum register. It consists of an array of qubits for quantum representation and classical metadata (number of fraction digits, sign) to define its numeric behavior. QNum enables numerical computation in quantum circuits, supporting both signed and unsigned formats, as well as configurable fixed-point precision. It is a parameterizable scalar type, meaning its behavior can depend on symbolic or compile-time values. The total number of qubits (`size`) determines the resolution and range of representable values. **Methods:** | Name | Description | | ------------------------------------------- | ----------- | | [to\_qvar](#to_qvar) | | | [get\_qmod\_type](#get_qmod_type) | | | [get\_maximal\_bounds](#get_maximal_bounds) | | **Attributes:** | Name | Type | Description | | ------------------- | ---------------------- | ----------- | | `CONSTRUCTOR_DEPTH` | `'3'` | | | `fraction_digits` | `CParamScalar \| int` | | | `is_signed` | `CParamScalar \| bool` | | ### to\_qvar
to\_qvar(
cls: ,
origin: str | Expr,
type\_hint: Any
) -> QNum
**Parameters:** | Name | Type | Description | Default | | ----------- | ------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `origin` | `str \| Expr` | | *required* | | `type_hint` | `Any` | | *required* | ### get\_qmod\_type
get\_qmod\_type(
self:
) -> ConcreteQuantumType
**Parameters:** | Name | Type | Description | Default | | ------ | ---- | ----------- | ---------- | | `self` | \`\` | | *required* | ### get\_maximal\_bounds
get\_maximal\_bounds(
self:
) -> tuple\[float, float]
**Parameters:** | Name | Type | Description | Default | | ------ | ---- | ----------- | ---------- | | `self` | \`\` | | *required* | ### allocate
allocate(
args: Any = (),
kwargs: Any = 
) -> None
Initialize a quantum variable to a new quantum object in the zero state: $$ \left|\text{out}\right\rangle = \left|0\right\rangle^{\otimes \text{num_qubits}} $$ If 'num\_qubits' is not specified, it will be inferred according to the type of 'out'. In case the quantum variable is of type `QNum`, its numeric attributes can be specified as well. **Parameters:** | Name | Type | Description | Default | | -------- | ----- | ----------- | ------- | | `args` | `Any` | | () | | `kwargs` | `Any` | | | ### bind
bind(
source: Input\[QVar] | list\[Input\[QVar]],
destination: Output\[QVar] | list\[Output\[QVar]]
) -> None
Reassign qubit or arrays of qubits by redirecting their logical identifiers. This operation rewires the logical identity of the `source` qubits to new objects given in `destination`. For example, an array of two qubits `X` can be mapped to individual qubits `Y` and `Z`. **Parameters:** | Name | Type | Description | Default | | ------------- | ------------------------------------ | ----------------------------------------------------------------------------------------- | ---------- | | `source` | `Input[QVar] \| list[Input[QVar]]` | A qubit or list of initialized qubits to reassign. | *required* | | `destination` | `Output[QVar] \| list[Output[QVar]]` | A qubit or list of target qubits to bind to. Must match the number of qubits in `source`. | *required* | ### if\_
if\_(
condition: SymbolicExpr | bool,
then: QCallable | Callable\[\[], Statements],
else\_: QCallable | Callable\[\[], Statements] | int = \_MISSING\_VALUE
) -> None
Conditionally executes quantum operations based on a symbolic or boolean expression. This function defines classical control flow within a quantum program. It allows quantum operations to be conditionally executed based on symbolic expressions - such as parameters used in variational algorithms, loop indices, or other classical variables affecting quantum control flow. **Parameters:** | Name | Type | Description | Default | | ----------- | ---------------------------------------------- | -------------------------------------------------------------------------------------- | ---------------- | | `condition` | `SymbolicExpr \| bool` | A symbolic or boolean expression evaluated at runtime to determine the execution path. | *required* | | `then` | `QCallable \| Callable[[], Statements]` | A quantum operation executed when `condition` evaluates to True. | *required* | | `else_` | `QCallable \| Callable[[], Statements] \| int` | (Optional) A quantum operation executed when `condition` evaluates to False. | \_MISSING\_VALUE | ### control
control(
ctrl: SymbolicExpr | QBit | QArray\[QBit] | list\[QVar],
stmt\_block: QCallable | Callable\[\[], Statements],
else\_block: QCallable | Callable\[\[], Statements] | None = None
) -> None
Conditionally executes quantum operations based on the value of quantum variables or expressions. This operation enables quantum control flow similar to classical `if` statements. It evaluates a quantum condition and executes one of the provided quantum code blocks accordingly. **Parameters:** | Name | Type | Description | Default | | ------------ | ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | `ctrl` | `SymbolicExpr \| QBit \| QArray[QBit] \| list[QVar]` | A quantum control expression, which can be a logical expression, a single `QBit`, or a `QArray[QBit]`. If `ctrl` is a logical expression, `stmt_block` is executed when it evaluates to `True`. If `ctrl` is a `QBit` or `QArray[QBit]`, `stmt_block` is executed if all qubits are in the \`\|1>\` state. | *required* | | `stmt_block` | `QCallable \| Callable[[], Statements]` | The quantum operations to execute when the condition holds. This can be a `QCallable` or a function returning a `Statements` block. | *required* | | `else_block` | `QCallable \| Callable[[], Statements] \| None` | (Optional) Quantum operations to execute when the condition does not hold. | None | ### skip\_control
skip\_control(
stmt\_block: QCallable | Callable\[\[], Statements]
) -> None
Applies quantum statements unconditionally. **Parameters:** | Name | Type | Description | Default | | ------------ | --------------------------------------- | --------------------------------------------- | ---------- | | `stmt_block` | `QCallable \| Callable[[], Statements]` | A callable that produces a quantum operation. | *required* | ### assign
assign(
expression: SymbolicExpr,
target\_var: QScalar
) -> None
Initialize a scalar quantum variable using an arithmetic expression. If specified, the variable numeric properties (size, signedness, and fraction digits) must match the expression properties. Equivalent to ` |= `. **Parameters:** | Name | Type | Description | Default | | ------------ | -------------- | -------------------------------------------- | ---------- | | `expression` | `SymbolicExpr` | A classical or quantum arithmetic expression | *required* | | `target_var` | `QScalar` | An uninitialized scalar quantum variable | *required* | ### inplace\_add
inplace\_add(
expression: SymbolicExpr,
target\_var: QScalar
) -> None
Add an arithmetic expression to a quantum variable. Equivalent to ` += `. **Parameters:** | Name | Type | Description | Default | | ------------ | -------------- | -------------------------------------------- | ---------- | | `expression` | `SymbolicExpr` | A classical or quantum arithmetic expression | *required* | | `target_var` | `QScalar` | A scalar quantum variable | *required* | ### inplace\_xor
inplace\_xor(
expression: SymbolicExpr,
target\_var: QScalar
) -> None
Bitwise-XOR a quantum variable with an arithmetic expression. Equivalent to ` ^= `. **Parameters:** | Name | Type | Description | Default | | ------------ | -------------- | -------------------------------------------- | ---------- | | `expression` | `SymbolicExpr` | A classical or quantum arithmetic expression | *required* | | `target_var` | `QScalar` | A scalar quantum variable | *required* | ### within\_apply
within\_apply(
within: Callable\[\[], Statements],
apply: Callable\[\[], Statements]
) -> None
Given two operations $U$ and $V$, performs the composition of operations $U^\{-1\} V U$. This operation is used to represent a sequence where the operation `U` is applied, followed by another operation `V`, and then `U^{-1}` is applied to uncompute. This pattern is common in reversible computation and quantum subroutines. **Parameters:** | Name | Type | Description | Default | | -------- | -------------------------- | ------------------------------------------------------------- | ---------- | | `within` | `Callable[[], Statements]` | The unitary operation `U` to be computed and then uncomputed. | *required* | | `apply` | `Callable[[], Statements]` | The operation `V` to be applied within the `U` block. | *required* | ### repeat
repeat(
count: SymbolicExpr | int,
iteration: Callable\[\[int], Statements]
) -> None
Executes a quantum loop a specified number of times, applying a quantum operation on each iteration. This operation provides quantum control flow similar to a classical `for` loop, enabling repeated application of quantum operations based on classical loop variables. **Parameters:** | Name | Type | Description | Default | | ----------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------ | ---------- | | `count` | `SymbolicExpr \| int` | An integer or symbolic expression specifying the number of loop iterations. | *required* | | `iteration` | `Callable[[int], Statements]` | A callable that takes a single integer index and returns the quantum operations to be performed at each iteration. | *required* | ### power
power(
exponent: SymbolicExpr | int,
stmt\_block: QCallable | Callable\[\[], Statements]
) -> None
Apply a quantum operation raised to a symbolic or integer power. This function enables exponentiation of a quantum gate, where the exponent can be a symbolic expression or an integer. It is typically used within a quantum program to repeat or scale quantum operations in a parameterized way. **Parameters:** | Name | Type | Description | Default | | ------------ | --------------------------------------- | ------------------------------------------------------------------- | ---------- | | `exponent` | `SymbolicExpr \| int` | The exponent value, either as an integer or a symbolic expression. | *required* | | `stmt_block` | `QCallable \| Callable[[], Statements]` | A callable that produces the quantum operation to be exponentiated. | *required* | ### invert
invert(
stmt\_block: QCallable | Callable\[\[], Statements]
) -> Any
Apply the inverse of a quantum gate. This function allows inversion of a quantum gate. It is typically used within a quantum program to invert a sequence of operations. **Parameters:** | Name | Type | Description | Default | | ------------ | --------------------------------------- | -------------------------------------------------------------- | ---------- | | `stmt_block` | `QCallable \| Callable[[], Statements]` | A callable that produces the quantum operation to be inverted. | *required* | ### phase
phase(
phase\_expr: SymbolicExpr | float | None = None,
coefficient: SymbolicExpr | float = 1.0
) -> None
Applies a state-dependent or fixed phase shift (Z rotation) to the quantum state. This operation multiplies each computational-basis state $|x_1,x_2,\ldots,x_n\rangle$ by a complex phase factor $\text\{coefficient\} * \text\{phase_expr\}(x_1,x_2,\ldots,x_n)$, where `phase_expr` is a symbolic expression that contains quantum variables $x_1,x_2,\ldots,x_n$, and `coefficient` is a scalar multiplier. If `phase_expr` contains no quantum variables, all states are rotated by the same fixed angle. **Parameters:** | Name | Type | Description | Default | | ------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | | `phase_expr` | `SymbolicExpr \| float \| None` | A symbolic expression that evaluates to an angle (in radians) as a function of the state of the quantum variables occurring in it, if any, or otherwise a fixed value. Execution parameters are only allowed if no quantum variables occur in the expression. | None | | `coefficient` | `SymbolicExpr \| float` | (Optional, allowed only together with quantum expressions) A scalar multiplier for the evaluated expression, optionally containing execution parameters. Defaults to 1.0. | 1.0 | ### foreach
foreach(
values: SymbolicExpr | Sequence\[SymbolicExpr | int | float] | Sequence\[Sequence\[SymbolicExpr | int | float]],
iteration: Callable\[..., Statements]
) -> None
Loops through the elements of a classical list, applying a quantum operation on each iteration. This operation provides quantum control flow similar to a classical `for ... in` loop, enabling repeated application of quantum operations based on classical loop variables. The iteration callable accepts one or more classical iteration variables. If the iteration callable takes a single iteration variable, it will be assigned with the elements of 'values'. If the iteration callable takes two or more variables, the elements of 'values' will be unpacked into them. **Parameters:** | Name | Type | Description | Default | | ----------- | ------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- | ---------- | | `values` | `SymbolicExpr \| Sequence[SymbolicExpr \| int \| float] \| Sequence[Sequence[SymbolicExpr \| int \| float]]` | A classical list. | *required* | | `iteration` | `Callable[..., Statements]` | A callable that takes one or more iteration variables and returns the quantum operations to be performed at each iteration. | *required* | ### assign\_amplitude\_poly\_sin
assign\_amplitude\_poly\_sin(
indicator: QBit,
expr: SymbolicExpr
) -> None
Encodes the value of the sine/cosine of a polynomial into the amplitude of the respective computational basis state: $$ \begin{aligned} |x_1, x_2, \ldots, x_n\rangle|0\rangle &\rightarrow \cos(\mathrm{poly}(x_1, x_2, \ldots, x_n))|x_1, x_2, \ldots, x_n\rangle|0\rangle \\ &\quad + \sin(\mathrm{poly}(x_1, x_2, \ldots, x_n))|x_1, x_2, \ldots, x_n\rangle|1\rangle \end{aligned} $$ **Parameters:** | Name | Type | Description | Default | | ----------- | -------------- | --------------------------------------------------------------------- | ---------- | | `indicator` | `QBit` | The quantum indicator qubit | *required* | | `expr` | `SymbolicExpr` | A polynomial expression over quantum scalars x\_1, x\_2, \ldots, x\_n | *required* | ### lookup\_table
lookup\_table(
func: RealFunction,
targets: QScalar | list\[QScalar]
) -> list\[float]
Reduces a classical function into a lookup table over all the possible values of the quantum numbers. **Parameters:** | Name | Type | Description | Default | | --------- | -------------------------- | --------------------------------------- | ---------- | | `func` | `RealFunction` | A Python function | *required* | | `targets` | `QScalar \| list[QScalar]` | One or more initialized quantum numbers | *required* | **Returns:** * **Type:** `list[float]` * The function's lookup table options: show\_source: false show\_if\_no\_docstring: false # Path Operators Source: https://docs.classiq.io/sdk-reference/qmod/path-operators This is a list of the path operators available in `Qmod` for accessing elements and sub-arrays of arrays inside quantum expressions. Functions: | Name | Description | | ----------------------------- | ------------------------------------------------------------------- | | `python_value_to_qmod_object` | Translate a Python classical value into a QMod object. | | `subscript` | Return the element of `array` at position `index` in an expression. | | `slice_` | Return the `array[start:stop]` sub-array in an expression. | ### python\_value\_to\_qmod\_object
python\_value\_to\_qmod\_object(
value: Any,
qmodule: ModelStateContainer
) -> Any
Translate a Python classical value into a QMod object. numpy values are normalized to native Python, and dataclasses (which are always classical structs in QMod) become `QmodStructInstance`s with their declaration registered on `qmodule`. The result is a QMod object from which both an expression string (via `qmod_val_to_expr_str`) and a classical type (via `infer_classical_type`) can be derived. **Parameters:** | Name | Type | Description | Default | | --------- | --------------------- | ----------- | ---------- | | `value` | `Any` | | *required* | | `qmodule` | `ModelStateContainer` | | *required* | ### subscript
subscript(
array: list | CArray | np.ndarray,
index: Any
) -> Any
Return the element of `array` at position `index` in an expression. This is the expression-level form of the `array[index]` indexing operator. **Parameters:** | Name | Type | Description | Default | | ------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | ---------- | | `array` | `list \| CArray \| np.ndarray` | The array to index into. May be a Python list, a quantum-variable concatenation, a `CArray` parameter, or a numpy array. | *required* | | `index` | `Any` | The position to read, as a classical expression. | *required* | ### slice\_
slice\_(
array: list | CArray | np.ndarray,
start: Any,
stop: Any
) -> Any
Return the `array[start:stop]` sub-array in an expression. This is the expression-level form of the `array[start:stop]` slicing operator. **Parameters:** | Name | Type | Description | Default | | ------------------------------ | ------------------------------ | ------------------------------------------------------------------------------------------------------------------- | ---------- | | `array` | `list \| CArray \| np.ndarray` | The array to slice. May be a Python list, a quantum-variable concatenation, a `CArray` parameter, or a numpy array. | *required* | | `start` | `Any` | The index of the first element of the slice, inclusive. | *required* | | `stop` | `Any` | The index one past the last element of the slice, exclusive. | *required* | | options: | | | | | show\_source: false | | | | | show\_if\_no\_docstring: false | | | | # Symbolic Functions Source: https://docs.classiq.io/sdk-reference/qmod/symbolic-functions This is a list of the symbolic functions available in `Qmod` for applying on classical parameters in expressions context. Functions: | Name | Description | | ----------- | ------------------------------------------------------------------- | | `subscript` | Return the element of `array` at position `index` in an expression. | ### subscript
subscript(
array: list | CArray | np.ndarray,
index: Any
) -> Any
Return the element of `array` at position `index` in an expression. This is the expression-level form of the `array[index]` indexing operator. **Parameters:** | Name | Type | Description | Default | | ------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | ---------- | | `array` | `list \| CArray \| np.ndarray` | The array to index into. May be a Python list, a quantum-variable concatenation, a `CArray` parameter, or a numpy array. | *required* | | `index` | `Any` | The position to read, as a classical expression. | *required* | # Synthesis Source: https://docs.classiq.io/sdk-reference/synthesis ## TargetLanguage Enum specifying the output format for `export`. **Attributes:** | Name | Type | Description | | ----------- | ------------- | ------------------------------------------ | | `QASM2` | `'qasm2'` | | | `QASM3` | `'qasm3'` | | | `QIR` | `'qir'` | | | `CIRQ_JSON` | `'cirq_json'` | | | `QSHARP` | `'qsharp'` | | | `QASM2` | \`\` | OpenQASM 2.0 (default). | | `QASM3` | \`\` | OpenQASM 3.0. | | `QIR` | \`\` | Quantum Intermediate Representation (QIR). | | `CIRQ_JSON` | \`\` | Cirq JSON format. | | `QSHARP` | \`\` | Q# (QSharp). | ## TranspilationConfig Configuration for transpiling a quantum program to a target basis gate set and qubit connectivity. Pass a `TranspilationConfig` to the `transpilation_config` parameter of `export` to apply hardware-aware transpilation during export. **Attributes:** | Name | Type | Description | | --------------------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `kind` | `Literal['transpilation']` | | | `transpilation_level` | `TranspilationOption` | The transpilation strategy to apply. Defaults to `TranspilationOption.AUTO_OPTIMIZE`. | | `basis_gates` | `list[str] \| None` | The target gate set to decompose the circuit into. If `None`, defaults to single-qubit gates + `cx`. When a `connectivity_map` is provided, defaults to single-qubit gates + `cx`. | | `connectivity_map` | `ConnectivityMap \| None` | Qubit connectivity constraints of the target hardware - a list of allowed qubit-pair connections. If `None`, no routing is applied. | | `is_symmetric_connectivity` | `bool` | Whether the connectivity map is symmetric (i.e., if `(i, j)` is allowed then `(j, i)` is also allowed). Defaults to `True`. | ## FaultTolerantTranspilationConfig Configuration for fault-tolerant transpilation using Clifford+T gate approximation. Pass a `FaultTolerantTranspilationConfig` to the `transpilation_config` parameter of `export` to perform fault-tolerant transpilation in two stages: 1. The circuit is first decomposed into the Clifford+T+RZ gate set (`h`, `t`, `s`, `tdg`, `cx`, `rz`). 2. The remaining RZ gates are then approximated using the [gridsynth](https://github.com/quantum-programming/pygridsynth) algorithm, which finds an efficient Clifford+T sequence achieving the target rotation within the specified error bound. **Attributes:** | Name | Type | Description | | -------------------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `kind` | `Literal['fault_tolerant']` | | | `transpilation_level` | `TranspilationOption` | The transpilation strategy to apply. Defaults to `TranspilationOption.AUTO_OPTIMIZE`. | | `clifford_t_approximation_error` | `float` | The maximum total approximation error for the whole circuit. This budget is divided equally across all RZ gates, so each gate is approximated with a per-gate error of `clifford_t_approximation_error / num_rz_gates`. Smaller values yield more accurate circuits at the cost of greater depth. | | `keep_hierarchy` | `bool` | When `True`, each unique RZ-angle decomposition is emitted as a named custom gate (`rz_approx_0`, `rz_approx_1`, ...) in the target language, and every occurrence of that angle references the gate definition instead of inlining the Clifford+T sequence. When `False` (the default), the decomposition is inlined to produce a flat circuit. | ## QuantumProgram **Methods:** | Name | Description | | ------------------------------------- | --------------------------------------------------------- | | [to\_base\_program](#to_base_program) | | | [to\_program](#to_program) | | | [save\_results](#save_results) | Saves quantum program results as json into a file. | | [get\_debug\_info](#get_debug_info) | | | [raise\_warnings](#raise_warnings) | Raises all warnings that were collected during synthesis. | **Attributes:** | Name | Type | Description | | ---------------------------- | ------------------------------- | ----------- | | `hardware_data` | `SynthesisHardwareData` | | | `data` | `GeneratedCircuitData` | | | `model` | `ExecutionModel` | | | `transpiled_circuit` | `TranspiledCircuitData \| None` | | | `creation_time` | `str` | | | `compressed_debug_info` | `bytes \| None` | | | `program_id` | `str` | | | `execution_primitives_input` | `PrimitivesInput \| None` | | | `synthesis_warnings` | `list[str] \| None` | | | `compressed_compiled_qmod` | `bytes \| None` | | | `program_circuit` | `CircuitCodeInterface` | | | `has_compiled_qmod` | `bool` | | | `compiled_qmod` | `Model \| None` | | | `qasm` | `Code \| None` | | ### to\_base\_program
to\_base\_program(
self:
) -> quantum\_code.QuantumBaseCode
**Parameters:** | Name | Type | Description | Default | | ------ | ---- | ----------- | ---------- | | `self` | \`\` | | *required* | ### to\_program
to\_program(
self: ,
instruction\_set: QuantumInstructionSet | None = None
) -> quantum\_code.QuantumCode
**Parameters:** | Name | Type | Description | Default | | ----------------- | ------------------------------- | ----------- | ---------- | | `self` | \`\` | | *required* | | `instruction_set` | `QuantumInstructionSet \| None` | | None | ### save\_results
save\_results(
self: ,
filename: str | Path | None = None
) -> None
Saves quantum program results as json into a file. Parameters: filename (Union\[str, Path]): Optional, path + filename of file. If filename supplied add `.json` suffix. Returns: None **Parameters:** | Name | Type | Description | Default | | ---------- | --------------------- | ----------- | ---------- | | `self` | \`\` | | *required* | | `filename` | `str \| Path \| None` | | None | ### get\_debug\_info
get\_debug\_info(
self:
) -> list\[FunctionDebugInfoInterface] | None
**Parameters:** | Name | Type | Description | Default | | ------ | ---- | ----------- | ---------- | | `self` | \`\` | | *required* | ### raise\_warnings
raise\_warnings(
self:
) -> None
Raises all warnings that were collected during synthesis. **Parameters:** | Name | Type | Description | Default | | ------ | ---- | ----------- | ---------- | | `self` | \`\` | | *required* | ## Preferences Preferences for synthesizing a quantum circuit. **Methods:** | Name | Description | | ------------------------------------------------------------------------------------------------------------ | ----------- | | [optimization\_timeout\_less\_than\_generation\_timeout](#optimization_timeout_less_than_generation_timeout) | | | [make\_output\_format\_list](#make_output_format_list) | | | [validate\_output\_format](#validate_output_format) | | | [validate\_backend\_name](#validate_backend_name) | | | [validate\_backend](#validate_backend) | | **Attributes:** | Name | Type | Description | | ------------------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `synthesize_all_separately` | `bool` | | | `symbolic_loops` | `bool` | | | `compatibility_mode` | `bool` | | | `standalone` | `bool` | | | `backend_preferences` | `BackendPreferences \| None` | | | `machine_precision` | `int` | Specifies the precision used for quantum operations. Defaults to `DEFAULT_MACHINE_PRECISION`. | | `backend_service_provider` | `str` | The provider company or cloud service for the requested backend. Defaults to `None`. | | `backend_name` | `str` | The name of the requested backend or target. Defaults to `None`. | | `custom_hardware_settings` | `CustomHardwareSettings` | Defines custom hardware settings for optimization. This field is ignored if backend preferences are specified. | | `debug_mode` | `bool` | If `True`, debug information is added to the synthesized result, potentially slowing down the synthesis. Useful for executing interactive algorithms. Defaults to `True`. | | `optimization_level` | `OptimizationLevel)` | The optimization level used during synthesis (0-3); | | `output_format` | `List[QuantumFormat]` | Lists the output format(s) for the quantum circuit. Defaults to `[QuantumFormat.QASM]`. `QuantumFormat` Options: - QASM = "qasm" - QSHARP = "qsharp" - QIR = "qir" - IONQ = "ionq" - CIRQ\_JSON = "cirq\_json" - QASM\_CIRQ\_COMPATIBLE = "qasm\_cirq\_compatible" | | `qasm3` | `Optional[bool]` | If `True`, outputs OpenQASM 3.0 in addition to 2.0, applicable to relevant attributes in `GeneratedCircuit`. Defaults to `None`. | | `transpilation_option` | `TranspilationOption` | Sets the transpilation option to optimize the circuit. Defaults to `AUTO_OPTIMIZE`. See `TranspilationOption` | | `solovay_kitaev_max_iterations` | `Optional[int]` | Specifies the maximum number of iterations for the Solovay-Kitaev algorithm, if used. Defaults to `None`. | | `timeout_seconds` | `int` | Timeout setting for circuit synthesis in seconds. Defaults to `300`. | | `optimization_timeout_seconds` | `Optional[int]` | Specifies the timeout for optimization in seconds, or `None` for no optimization timeout. This will still adhere to the overall synthesis timeout. Defaults to `None`. | | `random_seed` | `int` | Random seed for circuit synthesis. | ### optimization\_timeout\_less\_than\_generation\_timeout
optimization\_timeout\_less\_than\_generation\_timeout(
cls: ,
optimization\_timeout\_seconds: pydantic.PositiveInt | None,
info: ValidationInfo
) -> pydantic.PositiveInt | None
**Parameters:** | Name | Type | Description | Default | | ------------------------------ | ------------------------------ | ----------- | ---------- | | `cls` | \`\` | | *required* | | `optimization_timeout_seconds` | `pydantic.PositiveInt \| None` | | *required* | | `info` | `ValidationInfo` | | *required* | ### make\_output\_format\_list
make\_output\_format\_list(
cls: ,
output\_format: Any
) -> list
**Parameters:** | Name | Type | Description | Default | | --------------- | ----- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `output_format` | `Any` | | *required* | ### validate\_output\_format
validate\_output\_format(
cls: ,
output\_format: PydanticConstrainedQuantumFormatList,
info: ValidationInfo
) -> PydanticConstrainedQuantumFormatList
**Parameters:** | Name | Type | Description | Default | | --------------- | -------------------------------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `output_format` | `PydanticConstrainedQuantumFormatList` | | *required* | | `info` | `ValidationInfo` | | *required* | ### validate\_backend\_name
validate\_backend\_name(
cls: ,
backend\_name: str | None
) -> str | None
**Parameters:** | Name | Type | Description | Default | | -------------- | ------------- | ----------- | ---------- | | `cls` | \`\` | | *required* | | `backend_name` | `str \| None` | | *required* | ### validate\_backend
validate\_backend(
self:
) -> Self
**Parameters:** | Name | Type | Description | Default | | ------ | ---- | ----------- | ---------- | | `self` | \`\` | | *required* | ## Constraints Constraints for the quantum circuit synthesis engine. This class is used to specify constraints such as maximum width, depth, gate count, and optimization parameters for the synthesis engine, guiding the generation of quantum circuits that satisfy these constraints. **Attributes:** | Name | Type | Description | | ------------------------ | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `max_width` | `int` | Maximum number of qubits allowed in the generated quantum circuit. Defaults to `None`. | | `optimization_parameter` | `OptimizationParameterType` | Determines if and how the synthesis engine should optimize the solution. Defaults to `NO_OPTIMIZATION`. See `OptimizationParameterType` | Functions: | Name | Description | | ------------ | ----------------------------------------------------------------------------------------------------------------------------- | | `write_qmod` | Creates a native Qmod file from a serialized model and outputs the synthesis options (Preferences and Constraints) to a file. | ### write\_qmod
write\_qmod(
model: SerializedModel | QFunc | GenerativeQFunc,
name: str,
directory: Path | None = None,
decimal\_precision: int = DEFAULT\_DECIMAL\_PRECISION,
symbolic\_only: bool = True
) -> None
Creates a native Qmod file from a serialized model and outputs the synthesis options (Preferences and Constraints) to a file. The native Qmod file may be uploaded to the Classiq IDE. **Parameters:** | Name | Type | Description | Default | | ------------------- | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- | | `model` | `SerializedModel \| QFunc \| GenerativeQFunc` | The entry point of the Qmod model - a qfunc named 'main' (or alternatively the output of 'create\_model'). | *required* | | `name` | `str` | The name to save the file by. | *required* | | `directory` | `Path \| None` | The directory to save the files in. If None, the current working directory is used. | None | | `decimal_precision` | `int` | The number of decimal places to use for numbers, set to 4 by default. | DEFAULT\_DECIMAL\_PRECISION | | `symbolic_only` | `bool` | If True keep function definitions un-expanded and symbolic (note that Qmod functions with parameters of Python types are not supported in this mode) | True | **Returns:** * **Type:** `None` ## Functions ### synthesize
synthesize(
model: SerializedModel | BaseQFunc,
auto\_show: bool = False,
constraints: Constraints | None = None,
preferences: Preferences | None = None
) -> QuantumProgram
Synthesize a model with the Classiq engine to receive a quantum program. [More details](https://docs.classiq.io/latest/sdk-reference/synthesis/#classiq.synthesize) **Parameters:** | Name | Type | Description | Default | | ------------- | ----------------------------------- | ---------------------------------------------------------------------------------------------------------- | ---------- | | `model` | `SerializedModel \| BaseQFunc` | The entry point of the Qmod model - a qfunc named 'main' (or alternatively the output of 'create\_model'). | *required* | | `auto_show` | `bool` | Whether to 'show' the synthesized model (False by default). | False | | `constraints` | [Constraints](#constraints) \| None | Constraints for the synthesis of the model. See Constraints (Optional). | None | | `preferences` | [Preferences](#preferences) \| None | Preferences for the synthesis of the model. See Preferences (Optional). | None | **Returns:** * **Type:** [QuantumProgram](#quantumprogram) * Quantum program. (See: QuantumProgram) ### show
show(
quantum\_program: QuantumProgram,
display\_url: bool = True
) -> None
Displays the interactive representation of the quantum program in the Classiq IDE. **Parameters:** | Name | Type | Description | Default | | ----------------- | --------------------------------- | ------------------------------------ | ---------- | | `quantum_program` | [QuantumProgram](#quantumprogram) | The quantum program to be displayed. | *required* | | `display_url` | `bool` | Whether to print the url | True | ### assign\_parameters
assign\_parameters(
quantum\_program: QuantumProgram,
parameters: ExecutionParams
) -> QuantumProgram
Assign parameters to a parametric quantum program. **Parameters:** | Name | Type | Description | Default | | ----------------- | --------------------------------- | -------------------------------------------------------------------------------- | ---------- | | `quantum_program` | [QuantumProgram](#quantumprogram) | The quantum program to be assigned. This is the result of the synthesize method. | *required* | | `parameters` | `ExecutionParams` | The parameter assignments. | *required* | **Returns:** * **Type:** [QuantumProgram](#quantumprogram) * The quantum program after assigning parameters. ### export
export(
quantum\_program: QuantumProgram,
target\_language: TargetLanguage | None = TargetLanguage.QASM2,
transpilation\_config: TranspilationConfig | FaultTolerantTranspilationConfig | Literal\[True] | None = None
) -> str
Export a quantum program as a circuit string in the requested target language. Non-angle runtime parameters (e.g. integers that affect circuit structure such as repeat counts) must be assigned first using `assign_parameters`. **Parameters:** | Name | Type | Description | Default | | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------- | | `quantum_program` | [QuantumProgram](#quantumprogram) | The quantum program to export. This is the result of `synthesize`. | *required* | | `target_language` | [TargetLanguage](#targetlanguage) \| None | The target language to export to. Defaults to `TargetLanguage.QASM2`. | TargetLanguage.QASM2 | | `transpilation_config` | [TranspilationConfig](#transpilationconfig) \| [FaultTolerantTranspilationConfig](#faulttoleranttranspilationconfig) \| Literal\[True] \| None | Transpilation configuration. Pass a `TranspilationConfig` or `FaultTolerantTranspilationConfig` to apply transpilation during export. Pass `True` to reuse the transpilation settings from the quantum program. Pass `None` to skip transpilation and export the circuit as synthesized. Defaults to `None`. | None | **Returns:** * **Type:** `str` * The exported circuit code as a string. ### get\_circuit\_metrics
get\_circuit\_metrics(
quantum\_program: QuantumProgram
) -> CircuitMetrics
Get the logical resource estimation (width, depth and gate counts) of a quantum program. For parametric programs, depth and gate counts may be returned as symbolic string expressions (e.g. `"n + 1"`) instead of integers. **Parameters:** | Name | Type | Description | Default | | ----------------- | --------------------------------- | ----------------------------------------------------------------------------- | ---------- | | `quantum_program` | [QuantumProgram](#quantumprogram) | The quantum program to estimate. This is the result of the synthesize method. | *required* | **Returns:** * **Type:** `CircuitMetrics` * The logical width, depth and gate counts of the quantum program. ### get\_transpiled\_circuit\_metrics
get\_transpiled\_circuit\_metrics(
quantum\_program: QuantumProgram
) -> CircuitMetrics
Get the hardware resource estimation (width, depth and gate counts) of a transpiled quantum program. **Parameters:** | Name | Type | Description | Default | | ----------------- | --------------------------------- | ----------------------------------------------------------------------------- | ---------- | | `quantum_program` | [QuantumProgram](#quantumprogram) | The quantum program to estimate. This should be a transpiled quantum program. | *required* | **Returns:** * **Type:** `CircuitMetrics` * The width, depth and gate counts of the transpiled program. ### set\_preferences
set\_preferences(
serialized\_model: SerializedModel,
preferences: Preferences | None = None,
kwargs: Any = 
) -> SerializedModel
Overrides the preferences of a (serialized) model and returns the updated model. **Parameters:** | Name | Type | Description | Default | | ------------------ | ----------------------------------- | -------------------------------------------------------------------------------- | ---------- | | `serialized_model` | `SerializedModel` | The model in serialized form. | *required* | | `preferences` | [Preferences](#preferences) \| None | The new preferences to be set for the model. Can be passed as keyword arguments. | None | | `kwargs` | `Any` | | | **Returns:** * **Type:** `SerializedModel` * The updated model with the new preferences applied. ### set\_execution\_preferences
set\_execution\_preferences(
serialized\_model: SerializedModel,
execution\_preferences: ExecutionPreferences | None = None,
kwargs: Any = 
) -> SerializedModel
Overrides the execution preferences of a (serialized) model and returns the updated model. **Parameters:** | Name | Type | Description | Default | | ----------------------- | ------------------------------ | ------------------------------------------------------------------------------------------ | ---------- | | `serialized_model` | `SerializedModel` | A serialization of the defined model. | *required* | | `execution_preferences` | `ExecutionPreferences \| None` | The new execution preferences to be set for the model. Can be passed as keyword arguments. | None | | `kwargs` | `Any` | | | ### set\_constraints
set\_constraints(
serialized\_model: SerializedModel,
constraints: Constraints | None = None,
kwargs: Any = 
) -> SerializedModel
Overrides the constraints of a (serialized) model and returns the updated model. **Parameters:** | Name | Type | Description | Default | | ------------------ | ----------------------------------- | -------------------------------------------------------------------------------- | ---------- | | `serialized_model` | `SerializedModel` | The model in serialized form. | *required* | | `constraints` | [Constraints](#constraints) \| None | The new constraints to be set for the model. Can be passed as keyword arguments. | None | | `kwargs` | `Any` | | | **Returns:** * **Type:** `SerializedModel` * The updated model with the new constraints applied. ### create\_model
create\_model(
entry\_point: QFunc | GenerativeQFunc,
constraints: Constraints | None = None,
execution\_preferences: ExecutionPreferences | None = None,
preferences: Preferences | None = None,
classical\_execution\_function: CFunc | None = None,
out\_file: str | None = None
) -> SerializedModel
Create a serialized model from a given Qmod entry function and additional parameters. **Parameters:** | Name | Type | Description | Default | | ------------------------------ | ----------------------------------- | -------------------------------------------------------------------------------- | ---------- | | `entry_point` | `QFunc \| GenerativeQFunc` | The entry point function for the model, which must be a QFunc named 'main'. | *required* | | `constraints` | [Constraints](#constraints) \| None | Constraints for the synthesis of the model. See Constraints (Optional). | None | | `execution_preferences` | `ExecutionPreferences \| None` | Preferences for the execution of the model. See ExecutionPreferences (Optional). | None | | `preferences` | [Preferences](#preferences) \| None | Preferences for the synthesis of the model. See Preferences (Optional). | None | | `classical_execution_function` | `CFunc \| None` | A function for the classical execution logic, which must be a CFunc (Optional). | None | | `out_file` | `str \| None` | File path to write the Qmod model in native Qmod representation to (Optional). | None | **Returns:** * **Type:** `SerializedModel` * A serialized model. ### qasm\_to\_qmod
qasm\_to\_qmod(
qasm: str,
qmod\_format: QmodFormat
) -> str
Decompiles QASM to Native/Python Qmod. Returns Qmod code as a string. Native Qmod can be synthesized in the Classiq IDE, while Python Qmod can be copy-pasted to a Python file (`.py`) and synthesized by calling `synthesize(main)`. **Parameters:** | Name | Type | Description | Default | | ------------- | ------------ | --------------------------- | ---------- | | `qasm` | `str` | QASM 2 or QASM 3 code | *required* | | `qmod_format` | `QmodFormat` | The requested output format | *required* | **Returns:** * **Type:** `str` * The decompiled Qmod program # Support Source: https://docs.classiq.io/support/index Welcome to the Classiq Support Center. This page is designed to help you quickly resolve common issues and get additional support if needed. *** ## Frequently Asked Questions (FAQs) ### Getting Started Classiq is quantum-computing software that enables the design, optimization, analysis, and execution of quantum algorithms. Check out how to get started [here](../getting-started/). * Start with [Registration and Installation](../getting-started/registration_installations/) to get access to the platform. * [The Onboarding Tutorial](../getting-started/classiq_tutorial/) will help with your first steps in coding. * When you feel confident, try some of our [real-world applications](../explore/applications/). Yes! With Classiq, you can run quantum algorithms on the quantum computers you have access to. Check out our list of [quantum providers](../sdk-reference/providers/). ### Technical Questions This error arises when your authentication credentials are denied. Try overriding your authentication credentials by executing `authenticate(overwrite=True)`. If you’re experiencing failed attempts to reconnect to the Studio or long loading times, please try closing and reopening your web browser. Try authenticating again using: [comment]: DO_NOT_TEST ```python theme={null} import classiq classiq.authenticate() ``` To prevent such problems in the future, it is a good practice to begin code files in the Studio with `authenticate()`. Some example notebooks (e.g., chemistry) require additional packages in the Studio. First, try: ```bash theme={null} pip install "classiq[chemistry]" ``` If it still doesn’t work, reset the virtual environment by running: ```bash theme={null} reset-user-env ``` If none of these options work, reach out to us on the [Community Slack](https://short.classiq.io/join-slack). Use `quantum_program_from_qasm()`. ### Contributing to the Library You can find contribution guidelines on [this page](https://github.com/Classiq/classiq-library/blob/main/CONTRIBUTING.md). ### Designing Quantum Models In Classiq, you can define any observable as a linear combination of Pauli strings. To measure a set of observables from a quantum program, see [Execution Session](../user-guide/execution/ExecutionSession/) and the [Execution Tutorial](../explore/tutorials/basic_tutorials/the_classiq_tutorial/execution_tutorial_part2/). You can synthesize the quantum circuit using `Constraints`. See [Quantum Program Constraints](../user-guide/synthesis/constraints/). You can synthesize the quantum program using hardware-aware synthesis. See [Hardware-Aware Synthesis](../user-guide/synthesis/hardware-aware-synthesis/). Using `control`, you can define multi-qubit controls and gates. You can also use `if_` for classical control of quantum gates. See [Classical Control Flow](/qmod-reference/language-reference/statements/classical-control-flow/). There are two ways to work around this: either bind the qubits you want to a QArray (see [bind](/qmod-reference/language-reference/statements/bind/)) or slice the QArrays (see [Path Operators](/qmod-reference/language-reference/expressions/#relational-operators)). Use the `apply_to_all` function. See [Utility functions](/sdk-reference/qmod/functions/open_library/utility_functions). You can use the `free()` function. For more information, see [Uncomputation](/qmod-reference/language-reference/uncomputation/). Use `qprog.data.width` and `qprog.transpiled_circuit.depth`. ### Execution Use `ExecutionPreferences` and set the backend appropriately. The [Execution Tutorial](../explore/tutorials/basic_tutorials/the_classiq_tutorial/execution_tutorial/) shows this step by step. In Qmod, it is possible to use Pyomo to formulate the optimization problem. See [Problem Formulation](/user-guide/applications/optimization/problem-formulation/). Use `ExecutionPreferences` and set the number of shots. See the [Execution Tutorial](../explore/tutorials/basic_tutorials/the_classiq_tutorial/execution_tutorial/). After defining the quantum program, use `ExecutionSession.minimize()` or `ExecutionSession.estimate()` for VQAs. See [Execution Tutorial 2: Expectation Values and Parameterized Quantum Programs](../explore/tutorials/basic_tutorials/the_classiq_tutorial/execution_tutorial_part2/). *** ## Need More Help? If your question is not answered here, please: * Reach out in the `#support-and-questions` channel on [our community Slack](https://short.classiq.io/join-slack). * For additional assistance, to report a bug, or to request a feature, submit a ticket via our [Support Center](https://classiq-community.freshdesk.com/support/home). # Classiq Assistant: Getting Started Source: https://docs.classiq.io/user-guide/ai/classiq-assistant **Beta:** The Classiq Assistant is currently in beta. ## Overview The Classiq Assistant is an AI assistant built into the [Classiq Platform](https://platform.classiq.io/). It lets you generate a quantum program from a natural language description or an image — without learning a new programming language. Describe the quantum problem you want to solve, and the assistant generates a qmod-native model, synthesizes it into a quantum program, and opens it in the Quantum Program page. You can also ask the assistant questions about quantum computing concepts and have it explain the programs it generates. ## Accessing the Assistant The Classiq Assistant is available only to users with an active Classiq subscription. The assistant is available in two places: * **Home page:** Type your request directly in the input box at the center of the home page. Home page with the assistant input box at the center * **Quantum Program page:** Open the assistant panel from any circuit page to keep refining your program or start a new request. The panel travels with you, so you can move between generating, refining, and asking questions without losing your place. Quantum Program page with the assistant panel open on the right ### Conversation context The assistant keeps the context of your current work so its answers stay relevant: * **Referenced circuit:** When you have a circuit open, it is attached to the conversation as a chip, so the assistant knows which program you are asking about. Remove the chip to drop that context, or start a new conversation to ask about something else. * **Generated model:** Each program the assistant generates is available on the Model page as a new model. This model belongs to the current conversation context only — it is not saved permanently and disappears once you move on, so save it if you want to keep it. * **Starting a new conversation:** Use the **New chat** button in the assistant panel to start fresh, or return to the home page and enter a new request there. ## Generating a Quantum Program 1. Type a description of the quantum program you want to build, or click the **+** icon to upload an image of a circuit or diagram. 2. Click **Send**. The assistant generates a qmod-native model and synthesizes it into a quantum program. 3. The Quantum Program page opens automatically with the result. From here you can keep chatting with the assistant — ask it to explain the circuit, predict the expected results, or refine the model. Synthesis occasionally fails on the first attempt, and an error message may appear. When this happens, the assistant automatically retries, so the quantum program can take a little longer to load. Wait for it to finish before starting over. 4. To run the program, click **Execute**. This takes you to the Execution page, where you select a backend and run the program. The assistant runs one request at a time and cannot process multiple chats concurrently. Wait for the current generation to finish before sending a new request or starting another chat. Generated Grover search circuit with the assistant explaining the implementation and the Execute button available **Example prompts:** * "Implement a Grover search to find all pairs where x times y equals 6, with x and y as integers in \[0, 7]" — generates a Grover search circuit over a 6-qubit search space. * "Teleport a qubit using quantum entanglement and classical corrections" — generates a quantum teleportation circuit. * "Show me a small Monte Carlo example" — generates a compact amplitude-estimation-style circuit. You can also ask conceptual and follow-up questions instead of only requesting circuits: * "Explain what entanglement is" * "Explain what this circuit does" * "What results should I expect from this program?" ## What You Can Do * Generate quantum programs from natural language descriptions or from circuit images and diagrams * Refine the output through follow-up messages in the same conversation * Ask the assistant to explain what it generated or walk you through the logic * Ask the assistant to predict what the results of the program should be * Ask questions about quantum computing concepts and get explanations in context ## Prompts Best Practice | Do | Don't | | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | Use clear, direct descriptions of what you want to build | Over-specify technical parameters in your initial prompt — it over-constrains the assistant and makes generation more likely to fail | | Start simple and add detail through follow-up messages | Pack multiple complex requirements into a single prompt — the assistant handles focused requests more reliably | | Rephrase or simplify if the first attempt isn't right | Assume a failed prompt means the use case is not supported — try a simpler version first | | Ask the assistant to explain the output or predict results | Expect the assistant to configure backends, run circuits, or interpret execution output — those are handled on the Execution page | ## Limitations * **Not all prompts will succeed.** Problems requiring parameterized circuits, complex Hamiltonian simulation, or niche library functions may produce incomplete or incorrect output. * **Generated programs are not guaranteed to be correct.** Always review the output before executing. * **Execution is a separate step.** The assistant generates and synthesizes quantum programs but does not run them. Click **Execute** and run the program on the Execution page. * **Model configuration is done in the Classiq Platform.** Setting optimization parameters, backend constraints, and preferences is handled directly in the platform, not through the assistant. # AI with Classiq Source: https://docs.classiq.io/user-guide/ai/index Classiq supports AI-powered quantum development across IDEs and environments. AI agents can model, synthesize, execute, and analyze quantum programs — all in natural language. ## Features Enterprise plugin for Claude Code and Cursor. Includes skills for every development phase — modeling, synthesis, execution, analysis — plus live reference lookup. Built-in AI assistant in the Classiq Studio. No additional setup — configure your LLM API key and start coding. ## Local Development Setup For AI-assisted development outside the Studio, set up the Classiq Library locally so agents have access to examples and documentation. ### Prerequisites * Python >=3.9, \<3.13 * Git * An IDE with an AI assistant (VS Code + Claude Code, Cursor, etc.) ### Setup 1. **Clone the library** ```bash theme={null} git clone https://github.com/Classiq/classiq-library.git cd classiq-library ``` 2. **Install the SDK** ```bash theme={null} pip install classiq ``` 3. **Open in your IDE** and point it at the `classiq-library` directory. For the full AI-in-IDE experience — skills, live doc lookup, and Classiq conventions baked in — see the [Quantum Engineer Plugin](/user-guide/ai/quantum-engineer-plugin). # Quantum Engineer Plugin Source: https://docs.classiq.io/user-guide/ai/quantum-engineer-plugin ## Overview This is an enterprise-grade feature available exclusively to Classiq's enterprise customers. The **Quantum Engineer** plugin brings Classiq expertise directly into your AI-powered IDE. It teaches coding agents like Claude Code, Cursor, and Codex how to model, synthesize, execute, analyze, and post-process quantum programs with Classiq. Build, run, and analyze quantum programs in natural language — describe what you want and the agent handles every step, from writing the model to reading back results. Programs are expressed in **Qmod**, and draw on the open library of reusable quantum functions. Synthesis delegates to Classiq's backend engine, which compiles high-level models into hardware-optimized circuits automatically. The plugin includes skills for each phase of quantum development: * **Modeling** — write quantum algorithms in Python using Classiq. * **Synthesis** — compile a model into a quantum program with `synthesize()`, constraints, and hardware targets. * **Execution** — run circuits: `sample`, `observe`, `calculate_state_vector`, and variational loops. * **Post-processing** — interpret results: histograms, energy curves, and most-probable states. * **Analyzer** — inspect synthesized programs for gate counts, depth, and hardware fit. * **Qiskit migration** — translate existing Qiskit code into Classiq models. A built-in reference layer keeps the agent's knowledge current as it works. ## Installation The marketplace is access-controlled: reach out to Classiq to get onboarded, and you'll receive a personal GitHub invitation to the repository. Accept it first — the steps below assume you already have access. Add the marketplace once per machine, using the repository from your invitation: ``` /plugin marketplace add / ``` Then install the plugin: ``` /plugin install classiq-quantum-engineer@classiq ``` Restart Claude Code after the first install so the bundled MCP server comes up. You'll be prompted to approve the `classiq-mcp` server on first use. **Keeping it up to date:** ``` /plugin marketplace update classiq # pull the latest catalog /plugin update classiq-quantum-engineer@classiq # update the plugin /plugin list # see what you have installed ``` * Open Cursor Settings (Cmd+Shift+J) → Plugins. * Choose Add/import plugin. * Paste the GitHub URL from your Classiq invitation. * Select classiq-quantum-engineer and install it. * Confirm the plugin status shows Imported. * Reload the window — open the Command Palette and run Developer: Reload Window. * Enable the MCP server — go to Settings → Features → Model Context Protocol and turn on Classiq Docs. * Confirm skills loaded — go to Settings → Rules → Agent Decides. You should see skills such as classiq, classiq-modeling, classiq-synthesis, and classiq-execution. **Keeping it up to date:** To pick up the latest plugin version, remove and re-import the plugin from the GitHub URL in your invitation, then reload the window. After updating, verify that: * Settings → Plugins shows classiq-quantum-engineer with status Imported. * Settings → Features → Model Context Protocol shows Classiq Docs enabled. * Settings → Rules → Agent Decides includes the Classiq skills. The plugin requires the `classiq` Python package in your project's environment. If you don't have it yet, ask the agent to install it — see the **Install Classiq** example in the [Examples](#examples) section below, or [Use Python and Classiq](/getting-started/sdk_installation) for manual SDK setup. ## Usage Once installed, just describe what you want in natural language — no special syntax required. The agent picks the right skill for each phase of the work: | You ask for | Skill that responds | | ------------------------------------------------------------------------------------------------- | ------------------- | | Writing, editing, or debugging a quantum algorithm in Python (`@qfunc`, `QBit`, `QNum`, `QArray`) | Modeling | | Writing or debugging native Qmod (`.qmod` files) | Native Qmod | | Compiling a model — `synthesize()`, constraints, hardware targets | Synthesis | | Running a circuit — `sample`, `observe`, `calculate_state_vector`, variational loops | Execution | | Interpreting results — histograms, energy curves, most-probable states | Post-processing | | Inspecting a synthesized program — gate counts, depth, hardware fit | Analyzer | | Translating Qiskit code into Classiq models | Qiskit migration | For requests that span multiple phases — "model this and run it" — the agent organizes the full pipeline end-to-end. ## Examples **Prompt:** ``` Create a Bell state, synthesize it, run it on the simulator, and show the measurement counts. ``` **Generated code:** ```python theme={null} from classiq import * @qfunc def main(res: Output[QArray[QBit, 2]]): allocate(2, res) H(res[0]) CX(res[0], res[1]) qprog = synthesize(main) result = sample(qprog) print(result) ``` **Output:** ``` res counts probability bitstring 0 [1, 1] 1036 0.505859 11 1 [0, 0] 1012 0.494141 00 ``` **Prompt:** ``` Write a quantum program that puts a 1-qubit integer in superposition, assigns 2 to a second integer, computes their sum, and runs it. ``` **Generated code:** ```python theme={null} from classiq import * @qfunc def main(x: Output[QNum], y: Output[QNum], z: Output[QNum]): allocate(1, x) H(x) y |= 2 z |= x + y qprog = synthesize(main) res = sample(qprog) print(res) ``` **Output:** | x | y | z | counts | probability | | - | - | - | ------ | ----------- | | 0 | 2 | 2 | 1049 | 0.512 | | 1 | 2 | 3 | 999 | 0.488 | **Prompt:** ``` Use Grover's algorithm to find all pairs (x, y) of 2-bit integers where x + y < 4 and (x * y) % 4 == 2. Run it and show which states get amplified. ``` **Generated code:** ```python theme={null} from classiq import * VAR_SIZE = 2 class GroverVars(QStruct): x: QNum[VAR_SIZE] y: QNum[VAR_SIZE] @qperm def my_predicate(vars: Const[GroverVars], res: QBit) -> None: res ^= (vars.x + vars.y < 4) & ((vars.x * vars.y) % 4 == 2) @qfunc def main(vars: Output[GroverVars]): allocate(vars) hadamard_transform(vars) power( 2, lambda: grover_operator( lambda vars: phase_oracle(predicate=my_predicate, target=vars), hadamard_transform, vars, ), ) qprog = synthesize(main) result = sample(qprog) result ``` **Output:** | vars.x | vars.y | probability | | ------ | ------ | ----------- | | 1 | 2 | 0.481 | | 2 | 1 | 0.463 | | 2 | 2 | 0.006 | **Prompt:** ``` Install classiq and set it up for me. ``` The agent invokes the **Setup** skill, which walks through the full installation in your environment: it runs an environment diagnostic, installs the `classiq` package via pip, opens a browser window for authentication, and confirms everything is ready with a final status summary. claude code completing the classiq installation using quantum engineer plugin ## Per-project best practices A small amount of one-time setup per project pays off quickly: a project memory file gives the agent persistent context about your environment, sanity checks let you verify everything is wired up before you start, and a troubleshooting reference saves time when something unexpected happens. ### Memory file Both IDEs support a project-level file that the agent reads at the start of every conversation. Use it to record your Python environment and any project-specific conventions — so the agent never has to ask and never guesses wrong. Create `CLAUDE.md` at the root of your project and fill in the bracketed fields: ```markdown theme={null} # [Project Name] ## Environment - Python: [path/to/venv/bin/python] - Classiq SDK: installed in the environment above ## Backend Default: Classiq cloud simulator. [Optional: list hardware targets or execution preferences for this project.] ## Project overview [One or two sentences describing the quantum algorithms or application this project implements.] ## Conventions [Any project-specific patterns — naming, file layout, preferred synthesis constraints, etc.] ``` Claude Code picks up `CLAUDE.md` automatically — no additional configuration required. Create `.cursor/rules/classiq.mdc` at the root of your project. This file gives Cursor persistent Classiq-specific context for the project. ```markdown theme={null} --- description: Classiq project context alwaysApply: true --- # [Project Name] ## Environment - Python: [path/to/venv/bin/python] - Classiq SDK: installed in the environment above ## Backend Default: Classiq cloud simulator. [Optional: list hardware targets or execution preferences for this project.] ## Project overview [One or two sentences describing the quantum algorithms or application this project implements.] ## Conventions [Any project-specific patterns — naming, file layout, preferred synthesis constraints, etc.] ``` The `alwaysApply: true` frontmatter tells Cursor to load this rule for every conversation in the project. To verify the rule is active, open Settings → Rules and confirm the project rule appears. You can also ask the agent: "What Classiq project context do you see?" The response should reflect the environment and conventions from .cursor/rules/classiq.mdc. ### Sanity checks Run these checks after installation to confirm the plugin is wired up correctly. **Verify the plugin is installed:** ``` /plugin list ``` Look for `classiq-quantum-engineer@classiq` in the output. If it is missing, reinstall: ``` /plugin install classiq-quantum-engineer@classiq ``` **Confirm skills are active:** Run `/status` or ask the agent directly: `"Which Classiq skills do you have loaded?"` The response should name skills such as `classiq-modeling`, `classiq-synthesis`, and `classiq-execution`. **Confirm the MCP server is running:** Ask the agent: ``` "Can you look up the Classiq docs for synthesize?" ``` A successful lookup means the `classiq-mcp` server is up. A timeout or "no tools available" reply means the server did not start — see [Troubleshooting](#troubleshooting) below. **Verify the plugin is installed:** * Open Cursor Settings → Plugins. The classiq-quantum-engineer plugin should appear with status Imported. If it is missing, re-import it from the GitHub URL in your invitation. If import fails, confirm that you accepted the GitHub invitation and are signed into Cursor with an account that can access the repository. **Confirm skills are active:** * Go to Settings → Rules → Agent Decides. You should see skills such as classiq, classiq-modeling, classiq-synthesis, and classiq-execution. You can also ask the agent: ``` Which Classiq skills do you have loaded? ``` A successful response should name the Classiq skills available to the agent. **Confirm the MCP server is running:** * Go to Settings → Features → Model Context Protocol and confirm Classiq Docs is enabled. Then ask the agent: ``` Can you look up the Classiq docs for synthesize? ``` A successful lookup confirms the MCP server is running. A timeout, missing-tool message, or generic answer without a docs lookup usually means the MCP server did not start correctly. **Confirm the SDK is available in the project environment:** Ask the agent: ``` Check whether the classiq Python package is installed in this project environment. ``` If the package is missing, you can ask the agent to install it. ### Troubleshooting #### MCP server does not start The `classiq-mcp` server starts automatically on launch, but requires a one-time approval prompt on first use. 1. Run `/plugin list` — confirm `classiq-quantum-engineer@classiq` appears. 2. Go to **Settings → MCP Servers**. If `classiq-mcp` shows as disabled, enable it. 3. Restart Claude Code. The server starts fresh on each launch. If the server still does not appear after a restart, try reinstalling: ``` /plugin update classiq-quantum-engineer@classiq ``` 1. Go to **Settings → Features → Model Context Protocol**. 2. Confirm **Classiq Docs** is toggled on. 3. Reload the window — Command Palette → *Developer: Reload Window*. If the server still does not appear, remove and re-import the plugin from the GitHub URL in your invitation, then reload. #### Skills do not load Skills appear in **Settings → Rules**. If they are absent after installation: * Quit and relaunch Claude Code. * Confirm the plugin installed cleanly: `/plugin list` should show `classiq-quantum-engineer@classiq` without an error flag. Skills appear in **Settings → Rules → Agent Decides**. If they are absent after installation: * Reload the window — Command Palette → *Developer: Reload Window*. * Confirm the plugin status in **Settings → Plugins** shows **Imported** without an error. #### Plugin is installed but not updating If `synthesize` behavior or skill instructions seem outdated: ``` /plugin marketplace update classiq # pull the latest catalog /plugin update classiq-quantum-engineer@classiq ``` Restart Claude Code after updating. If skill instructions seem outdated, remove and re-import the plugin from the GitHub URL in your invitation to pick up the latest version, then reload the window. ## See also [Use AI with Classiq](/user-guide/ai/index) # AI Agent in Studio Source: https://docs.classiq.io/user-guide/ai/studio-agent The Classiq Studio has a built-in AI agent that assists with quantum algorithm development — no local installation or plugin required. ## Getting Started 1. Open [Classiq Studio](https://platform.classiq.io/studio/). 2. Set your API key for your preferred LLM provider under **Settings**. 3. Describe what you want to build. The agent writes, runs, and explains Classiq code directly in the editor. ## What the Agent Can Do * Write and edit quantum algorithms using the Classiq Python SDK * Synthesize models and inspect the resulting circuit * Execute programs on the simulator and read back results * Explain code and suggest improvements ## Supported LLM Providers The Studio agent works with any of the LLM providers configured in your account settings. Set your API key once and the agent will use it for all sessions. # Data Analysis and Graphs Source: https://docs.classiq.io/user-guide/analysis/data-analysis-and-graphs The input to the analyzer tool is a quantum program in OpenQasm or Cirq format. The analysis data and graphs can be accessed using Classiq's Python SDK. After synthesizing a quantum program, initialize the `Analyzer` class using the quantum program returned from the synthesis process. [comment]: DO_NOT_TEST ```python theme={null} from classiq import ( qfunc, Analyzer, Output, QBit, allocate, synthesize, QuantumProgram, allocate, ) @qfunc def main(res: Output[QBit]) -> None: allocate(1, res) qprog = synthesize(main) analyzer = Analyzer(circuit=qprog) ``` ## Graphs and Data Quantum programs are more than just a beautiful image; they are meant to run on real quantum hardware to solve and supply interesting and new answers. The hardware analysis supplies two main insights. ### Available Devices To perform hardware-aware analysis, you may want to know which devices are available using the platform. You can get a list of the devices that are both available and suit the quantum program (i.e., have a sufficient number of qubits). [comment]: DO_NOT_TEST ```python theme={null} analyzer.get_available_devices() ``` This command returns the available devices of all the providers, in dictionary format, where providers are the keys, and lists of available devices are the values: [comment]: DO_NOT_TEST ```python theme={null} { "IBM Quantum": [ "almaden", "boeblingen", "brooklyn", "cairo", "cambridge", "guadalupe", "hanoi", "johannesburg", "kolkata", "manhattan", "melbourne", "montreal", "mumbai", "paris", "poughkeepsie", "rochester", "rueschlikon", "singapore", "sydney", "tokyo", "toronto", "washington", ], "Azure Quantum": ["ionq", "quantinuum"], } ``` You can also request the devices of a specific provider: [comment]: DO_NOT_TEST ```python theme={null} analyzer.get_available_devices(["IBM Quantum"]) ``` ### Hardware-Circuit Connection The Hardware-Circuit Connection graph is a representation of a quantum program as implemented on a specific (physical) quantum device. You can interactively select hardware from hardware providers such as IBM Quantum, Amazon Braket, and Microsoft Azure. The analyzer compiles the quantum program for the selected hardware, allowing easy inspection of which physical qubits will be used for execution on the device and, in turn, modifying the quantum program if needed. This information is important if you want to execute the quantum program on real quantum hardware, so you can make modifications to the quantum program. This graph is accessible from the SDK only if you install the `analyzer_sdk` extension with the `pip install classiq[analyzer_sdk]` command and use Jupyter as your coding platform. Once the extension is installed, run this command: [comment]: DO_NOT_TEST ```python theme={null} # Run inside jupyter analyzer.plot_hardware_connectivity() ``` connectivity Alternatively, you can open the graph directly with a specific provider and device: [comment]: DO_NOT_TEST ```python theme={null} analyzer.plot_hardware_connectivity(provider="IBM Quantum", device="washington") ``` ### Hardware Comparison Table The hardware comparison table compares the transpiled quantum program on different hardware backends. The table includes information about the quantum program's depth, number of multi-qubit gates, and total number of gates. [comment]: DO_NOT_TEST ```python theme={null} providers = ["IBM Quantum", "Azure Quantum", "Amazon Braket"] analyzer = Analyzer(circuit=qprog) analyzer.get_hardware_comparison_table(providers=providers) analyzer.plot_hardware_comparison_table() ``` The `providers` variable is a list of providers ("IBM Quantum", "Azure Quantum", and "Amazon Braket"), where the table includes only the backends of providers that appear in the list, and the default is to use all the providers. The table has the following form: comparison_table Sort the table according to the table properties using the dropdown button on the upper left of the table. \`\`\` Using the default device/providers option (all) or comparing a large number of devices might take a long time, especially when analyzing large quantum programs. It is advised to compare a small number of devices when you are interested in analyzing large quantum programs. The difference between the transpilation in the synthesis process and the comparison table data originates from the fact that the comparison table data is aware of the specific hardware and considers information such as the basis gates and connectivity. \[1] M. Van den Nest, W. Dur, G. Vidal, H. J. Briegel, "Classical simulation versus universality in measurement-based quantum computation", Phys. Rev. A 75, 012337 (2007). \[2] S. Oum, "Rank-width: Algorithmic and structural results", Lect. Notes Comput. Sci. 3787, 49 (2005). \[3] S. Oum, "Rank-width is less than or equal to branch-width", J. Graph Theory, 57 (3), 239-244 (2008). \[4] Hans L. Bodlaender, "Discovering treewidth". Institute of Information and Computing Sciences, Utrecht University, Technical Report. UU-CS-2005-018. [http://www.cs.uu.nl](http://www.cs.uu.nl) # Analysis Source: https://docs.classiq.io/user-guide/analysis/index The Classiq product requires information from the synthesis so it can automatically generate a quantum program. This fact is even more important for large quantum programs, where simulation may not even be an option. The Classiq analyzer comprises a web application plus analysis data and graphs that provide information about a quantum program, primarily so there is no need to simulate it. These sections describe how to use the web application, data, and graphs, and give a comprehensive walkthrough of the components: * [Quantum Program Visualization Tool](/user-guide/analysis/visualization-of-quantum-programs) * [Data Analysis and Graphs](/user-guide/analysis/data-analysis-and-graphs) # Visualization of Quantum Programs Source: https://docs.classiq.io/user-guide/analysis/visualization-of-quantum-programs The Classiq analyzer application helps you visualize and analyze quantum programs. The input to the application is a quantum program synthesized by the Classiq [synthesis engine](/user-guide/synthesis/getting-started), or an OpenQASM quantum program. ## Accessing the Quantum Program Visualization Tool There are 3 ways to view a quantum program using the visualization tool: * Through the Classiq Python SDK * Direct access via the Classiq IDE * Direct access via the Studio ### Classiq Python SDK * Synthesize your model using `synthesize()` to obtain a quantum program, and pass it as a parameter to the function `show()`. Here is an example with a trivial model: [comment]: DO_NOT_TEST ```python theme={null} from classiq import qfunc, Output, QBit, synthesize, show, allocate @qfunc def main(res: Output[QBit]) -> None: allocate(1, res) qprog = synthesize(main) show(qprog) ``` ### Direct Access * Synthesizing a quantum program using the IDE [https://platform.classiq.io/synthesis](https://platform.classiq.io/synthesis) automatically redirects you to the visualization tool. * Upload (drag and drop) a file that contains a quantum program synthesized using the Classiq engine (either synthesized and downloaded from the IDE or obtained from the python SDK using `qprog.save_result("file-name")`) into [https://platform.classiq.io/circuit/](https://platform.classiq.io/circuit/). upload_qp ### Visualization in the Studio * Follow the same steps as in Classiq Python SDK. The visualization tool can be directly rendered in the notebook: studio_vis_bright studio_vis ## Using Visualization Tool The Quantum Program visualization bridges the high-level algorithm descriptions (as it is captured in Qmod) with their quantum implementations (as synthesized by the Classiq engine). The tool offers two key innovations: **functional block hierarchies** and **quantum data flow** representations. The **quantum data flow** view lets users track quantum variables, including higher-level types like numbers, arrays, and structs, along with their qubit allocations. This aids in understanding data flow and memory allocation within quantum algorithms. **Functional block hierarchies** enable navigation between different levels of abstraction, from high-level functional blocks to individual quantum gates, offering a more thorough exploration of the algorithm's structure. The QP visualization tool provides an interactive way to analyze quantum programs by focusing on the quantum data-flow between functional blocks. Here’s a breakdown of its key features and functionality: #### Purpose * Analyze the implementation of the quantum model (Qmod) as synthesized by the Classiq engine. * Verify the data flow within the quantum program, enabling debugging and deeper analysis #### Hierarchical Structure * Allows users to traverse different levels of the quantum model implementation, from high-level Qmod functions down to atomic functions. Double clicking a function block opens and collapses the internal hierarchies: hierarchies #### Color Coding The visualization uses colors to help distinguish different block categories: * **Yellow**: Represents Qmod function blocks (either written by the user or taken from the Classiq library). * **Turquoise**: Represents Qmod statement blocks (such as: control, invert, power, within-apply, assignment). * **Grey**: Represents built-in functional blocks (generated by the Classiq engine during synthesis). #### Data Flow Analysis Data flow between functional blocks is presented in terms of the Qmod variables: * **Purple lines** represent the flow of quantum variables throughout the program. * The **purple dot** marks where a variable is initialized. * When variables split or merge (via slicing or the bind operation), the purple lines update to show the changes. * The variable’s name and size are also displayed along the purple lines, making it easier to track their initialization, usage and transformations. This structured approach allows for a clear understanding of the program’s execution, providing users with a tool to validate the accuracy and efficiency of their quantum programs. #### Task Bar On the top right is the analyzer task bar: taskbar The taskbar contain several groups of buttons, arranged from left to right: 1. **Search tool**: Allows you to search through the building blocks of a quantum program to quickly locate specific elements. 2. **Hierarchical View**: Provides options to expand all functions and gates, revealing the most detailed level of analysis for the quantum gates used in your algorithm. You can also collapse everything to return to the original, simplified view displayed when the quantum program was first loaded, where a single bar represents the entire algorithm. 3. **Navigation and Zoom**: This group includes map controls for navigating across different parts of the quantum program chart, enabling you to focus on gates and functions that are currently outside the visible area. It also includes zoom controls to: * Reset the chart to its default scale (currently 90%), * Zoom out, * Adjust the zoom level manually, * Or zoom in for a closer view. ### Variables View Within the Quantum Program page you can also toggle the **Variables View** option: toggle_versions Variables View provides a simplified, high-level visualization of how quantum data flows through a program. Instead of showing individual qubits, it highlights logical quantum variables and their transformations, making it easier to understand complex algorithms and trace data flow across different parts of the program. As quantum algorithms increase in scale and sophistication, traditional visualization techniques become cluttered and difficult to interpret, making it difficult to see the bigger picture. Classiq Qmod language addresses this by allowing users to design algorithms using high-level elements organized into modular functional blocks. Building on this, Variables View focuses on the logical units, making complex workflows far easier to follow. In the example below, the Variables View shows a simplified implementation of the discrete logarithm algorithm [\[1\]](#nielsen). Notice how purple lines trace the flow of logical variables, with dots indicating initialization points. As the algorithm progresses, variables are split and merged, providing an intuitive understanding of the quantum algorithm. variables_view ### Sharing your Quantum Program Visualization You can easily share your Quantum Program visualization with anyone, even those who haven’t signed up for the platform. It’s simple: * Copy the Quantum Program URL directly from your browser and share it. * Alternatively, click the "Share" button on the Quantum Program page, choose a social media to share in or copy the generated link, and share it with anyone. qp_sharing_link \[1]: [Michael A. Nielsen and Isaac L. Chuang. 2011. Quantum Computation and Quantum Information: 10th Anniversary Edition, Cambridge University Press, New York, NY, USA. ](https://www.amazon.com/Quantum-Computation-Information-Michael-Nielsen/dp/1107002176) # Combinatorial Optimization Source: https://docs.classiq.io/user-guide/applications/optimization/index The Classiq combinatorial optimization platform is a quantum software engine for optimization problems that you define. The engine tackles your formulated real-world optimization challenges and generates customized quantum programs. You choose whether to run the programs on a quantum backend or a classical simulation, and receive the optimized solution. To gain further insights regarding the synthesis process, you can examine the synthesized program with the Classiq [analyzer](/user-guide/analysis/index) module. ## Formulating Problems You describe new optimization problems using the Python SDK package. The problem is formulated using [PYOMO](http://www.pyomo.org/), a Python-based, open-source optimization modeling language. The language supports a wide variety of problem types, such as integer linear programming, quadratic programming, graph theory problems, and SAT problems. Read in-depth reviews of the language’s capabilities in \[ \[1] ]\(#pyomo documentation) and \[ \[2] ]\(#pyomo cookbook) \[ \[3] ]\(#pyomo intro). The basics of problem modelling in PYOMO and a complete example are in the [problem formulation](/user-guide/applications/optimization/problem-formulation) section. The platform supports an extensive set of modeling configurations for your use ([supported modeling](/user-guide/applications/optimization/supported-modeling)). ## Solving Optimization Problems The core Classiq capabilities are generation of a designated quantum solution, and execution of the generated algorithm on a quantum backend. The platform relies on the QAOA penalty algorithm to solve optimization problems. ## References \[1] Pyomo Documentation 6.0.1, [https://pyomo.readthedocs.io/en/stable/](https://pyomo.readthedocs.io/en/stable/). \[2] Prof. Jeffrey Kantor’s Pyomo Cookbook [https://jckantor.github.io/ND-Pyomo-Cookbook/](https://jckantor.github.io/ND-Pyomo-Cookbook/). \[3] J. D. Siirola, Introduction to Pyomo: The optimization foundation for IDAES, [https://www.osti.gov/servlets/purl/1524963](https://www.osti.gov/servlets/purl/1524963) (2018). # Problem Formulation Source: https://docs.classiq.io/user-guide/applications/optimization/problem-formulation [comment]: SINGLE_FILE In the PYOMO language, like any other algebraic modeling languages (AMLs), the optimization problem consists of three main components: decision variables, constraints, and an objective function. This section briefly introduces these components and explains the modalities supported by the platform. ### Model As an object-oriented language, PYOMO organizes the optimization problem into a single object of type ConcreteModel. It contains all the other components as Python attributes. The model is declared in the following way: ```python theme={null} import pyomo.environ as pyo model = pyo.ConcreteModel() ``` ### Decision Variables The variables component is the “unknown part” of the model. The solver aims to assign values that constitute the best possible solution. Following is a description of the possible arguments for variable declaration in PYOMO. In many optimization problems, the variables are indexed. The index set is provided as the first argument to the PYOMO var object. ```python theme={null} index_set = [0, 1, 2, 3] ``` [comment]: DO_NOT_TEST ```python theme={null} model.x = pyo.Var(index_set) ``` Next, state the variable domain. It may be a binary variable, an integer variable, a real variable, etc. The platform currently supports the binary and integer domains: • Binary: [comment]: DO_NOT_TEST ```python theme={null} model.x = pyo.Var(index_set, domain=pyo.Binary) ``` • Integer: [comment]: DO_NOT_TEST ```python theme={null} model.x = pyo.Var(index_set, domain=pyo.NonNegativeIntegers, bounds=(0, 7)) ``` If you know the variable value, fix it now to save qubits: [comment]: DO_NOT_TEST ```python theme={null} model.x[0].fix(3) ``` See [PYOMO variables](https://pyomo.readthedocs.io/en/stable/pyomo_modeling_components/Variables.html). ### Constraints Constraints enrich the descriptive power of optimization problems. Different scenarios, rules, regulations, and variable relations may be phrased as conditions on decision variables that must be satisfied. In PYOMO, specify the constraints using equality or inequality expressions. There are several ways to integrate them inside the existing PYOMO model: * Using the `expr` argument: ```python theme={null} model = pyo.ConcreteModel() model.x = pyo.Var(index_set, domain=pyo.Binary) model.amount_constraint = pyo.Constraint(expr=sum(model.x[i] for i in model.x) == 3) ``` * Using a rule argument and a separate Python function: ```python theme={null} def amount_rule(model): return sum(model.x[i] for i in model.x) == 3 model.amount = pyo.Constraint(rule=amount_rule) ``` * Using a Python constraint decorator: ```python theme={null} @model.Constraint() def amount_rule(model): return sum(model.x[i] for i in model.x) == 3 ``` Index the constraints similarly to the variables. ```python theme={null} def size_rule(model, i): return model.x[i] <= model.x[i + 1] model.size_rule = pyo.Constraint(index_set[:-1], rule=size_rule) ``` See [PYOMO constraints](https://pyomo.readthedocs.io/en/stable/pyomo_modeling_components/Constraints.html). ### Objective Function The objective function encodes the essence of the best solution. It is a function of the decision variables, which return a real value that the optimization solver tries to minimize or maximize. It is incorporated into the PYOMO model similarly to the constraints: with an `expr` argument, with a rule argument, or with a decorator. An additional declaration argument is `sense`, which is set to `minimize` or `maximize`. ```python theme={null} model.cost = pyo.Objective(expr=sum(model.x[i] for i in index_set), sense=pyo.maximize) ``` See [PYOMO objective functions](https://pyomo.readthedocs.io/en/stable/pyomo_modeling_components/Objectives.html). ### Importing You can import all PYOMO components from the `pyomo.environ` sub-package: [comment]: DO_NOT_TEST ```python theme={null} import pyomo.environ as pyo Model = pyo.ConcreteModel() model.variable = pyo.Var() model.constraint = pyo.Constraint() ``` ### Complete Example ```python theme={null} from typing import Union, List import numpy as np import pyomo.core as pyo import networkx as nx def mis(graph: Union[nx.Graph, List[List[int]]]) -> pyo.ConcreteModel: if isinstance(graph, list): graph = nx.convert_matrix.from_numpy_matrix(np.array(graph)) model = pyo.ConcreteModel() model.Nodes = pyo.Set(initialize=list(graph.nodes)) model.Arcs = pyo.Set(initialize=list(graph.edges)) model.x = pyo.Var(model.Nodes, domain=pyo.Binary) @model.Constraint(model.Arcs) def independent_rule(model, node1, node2): return model.x[node1] + model.x[node2] <= 1 model.cost = pyo.Objective(expr=sum(list(model.x.values())), sense=pyo.maximize) return model ``` This example combines all components under a single Python function. It accepts user-defined inputs, and returns a complete PYOMO model with the assigned parameters. The function is saved in a file of the same name. The `graph` may be either an instance of the `networkx.Graph` class or the adjacency matrix in the form of `List[List[int]]`. # Supported Modeling Source: https://docs.classiq.io/user-guide/applications/optimization/supported-modeling Classiq supports a limited set of modeling configurations. The following tables describe them. ## Variables
Index Set List or set
Domain Binary(equivalent to \[0, 1] list) Bounded NonNegativeIntegers
(equivalent to \[0:bound] list)
## Constraints
Constraint type Equality constraints Inequality constraints
Constraint amount Multiple, non overlapping Multiple
Expression type Linear sum
Variable coefficient Positive integer
Constant term Positive integer
## Objective Functions
Objective amount Single
Variable type Binary Integer
Expression type Polynomial
Variable coefficient Integer or float
# Quantum Machine Learning Source: https://docs.classiq.io/user-guide/applications/qml/index Quantum machine learning (QML) is a field that seeks to combine the principles of quantum mechanics and machine learning to develop new algorithms and techniques that can process and analyze data more efficiently. The platform has a few QML built-in algorithms: * With [Quantum Neural Network (QNN)](/user-guide/applications/qml/qnn/index), you can combine classical and quantum neural network layers. # A Full Example Source: https://docs.classiq.io/user-guide/applications/qml/qnn/a-full-example [comment]: SINGLE_FILE_SLOW ## 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](#step-1-create-our-torchnnmodule) 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 post-process callable * 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: * [Creating Models](https://pytorch.org/tutorials/beginner/basics/quickstart_tutorial.html#creating-models) * [Build the Neural Network](https://pytorch.org/tutorials/beginner/basics/buildmodel_tutorial.html) * [Optimizing the Model Parameters](https://pytorch.org/tutorials/beginner/basics/quickstart_tutorial.html#optimizing-the-model-parameters) * [Tensors](https://pytorch.org/tutorials/beginner/basics/tensorqs_tutorial.html) * [Datasets & DataLoaders](https://pytorch.org/tutorials/beginner/basics/data_tutorial.html) ## 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: ```python theme={null} from classiq import ( synthesize, qfunc, QArray, QBit, RX, Output, CReal, allocate, ) @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) quantum_program = synthesize(main) ``` 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 Post-processing Post-process the result of executing the quantum program to obtain a single number (`float`) and a single dimension `Tensor`. ```python theme={null} import torch from classiq.applications.qnn.types import SavedResult # 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()) return torch.tensor(p_zero) ``` ### 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. ```python theme={null} 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()` post_process, # a callable that takes a single `SavedResult`, returning 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](/user-guide/applications/qml/qnn/datasets), as well as [`L1Loss`](https://pytorch.org/docs/stable/generated/torch.nn.L1Loss.html) and [SGD](https://pytorch.org/docs/stable/generated/torch.optim.SGD.html) ```python theme={null} 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 data_loader = DATALOADER_NOT # 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](https://pytorch.org/tutorials/beginner/blitz/neural_networks_tutorial.html#update-the-weights) ```python theme={null} import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader def train( model: nn.Module, data_loader: DataLoader, loss_func: nn.modules.loss._Loss, optimizer: optim.Optimizer, epoch: int = 20, ) -> None: for index in range(epoch): print(index, model.qlayer.weight) for data, label in data_loader: optimizer.zero_grad() output = model(data) loss = loss_func(output, label) loss.backward() optimizer.step() train(model, data_loader, loss_func, optimizer) ``` ### Step 4 - Test Lastly, we will test our network accuracy, using [the following answer](https://stackoverflow.com/questions/52176178/pytorch-model-accuracy-test#answer-64838681) ```python theme={null} def check_accuracy(model: nn.Module, data_loader: DataLoader, atol=1e-4) -> float: num_correct = 0 total = 0 model.eval() with torch.no_grad(): for data, labels in data_loader: # 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 check_accuracy(model, data_loader) ``` 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$. Finally, we safely teardown the `QLayer` instance. ```python theme={null} model.qlayer.teardown() ``` ## Summary In this example, we wrote a fully working Quantum Neural Network from scratch, trained it, and saw its success at learning the requested transformation. In section 1 we defined our parametric quantum program, as well as our post-processing function. Together, these two are sent as arguments to [the `QLayer` object](/user-guide/applications/qml/qnn/qlayer). 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. # Datasets Source: https://docs.classiq.io/user-guide/applications/qml/qnn/datasets Pytorch provides two classes: `DataSet` and `DataLoader`. If you're not familiar with them, please review them [here](https://pytorch.org/tutorials/beginner/basics/data_tutorial.html) Classiq provides two simple datasets, which are mainly used in our examples. They can be found in `classiq/applications/qnn/data_sets.py`. ## DatasetNot This dataset is used for training a network to learn the "NOT" operation. More specifically, learning the angle to a parametrized `Rx` gate (spoiler - the correct answer is `pi`). This dataset has 2 items: 0. the data is `|00...0>`, the label is `|11...1>`. 1. the data is `|11...1>`, the label is `|00...0>`. The amount of qubits is specified in the constructor (`def DatasetNot.__init__(self, n: int, ...)`). ### Transformers Additionally, we provide 2 `Transform`s for this class. (Their PyTorch documentation can be found [here](https://pytorch.org/tutorials/beginner/basics/transforms_tutorial.html)), which serve our example. Note that these transformers are automatically used. Feel free to keep reading about them, though this is not mandatory. In our example, we wish to A) encode the input state onto the quantum circle B) execute the PQC C) measure the PQC D) post-process the measurement results Our transformers help steps `A` and `D`. First, `state_to_weights` transforms the state, into parameters for the encoding `Rx` gates. This is used for step `A` In other words, the state `|0>` is transformed into the angle `0`, and the state `|1>` is transformed into the angle `pi`. Second, `state_to_label` transforms the expected output state into a single float number. More specifically, since we generate this data, we set the expected output state to be a pure state (in the `Z` basis). Additionally, we define the post-processing to return the probability of measuring `|00...0>` in the output state. Thus, for the output `|00...0>`, the post processed output is `1`, corresponding to `100%`, and for the output `|11...1>`, the post processed output is `0`. ### DatasetXor This dataset is used for training a network to learn the "XOR" operation. This may take, similar to `DatasetNot`, the amount of qubits in the constructor. The "XOR" operation on more than 2 inputs is defined as "a quantum program that outputs a 1 when the number of 1s at its inputs is odd, and a 0 when the number of incoming 1s is even" (credit: [wikipedia](https://en.wikipedia.org/wiki/XOR_gate#More_than_two_inputs)) ## Usage examples ### Using pre-configured `DataLoader`s ```python theme={null} from classiq.applications.qnn.datasets import DATALOADER_NOT for data, label in DATALOADER_NOT: print(f"Training the following data: {data}") print(f"with the following labels: {label}") ``` ### Using pre-configures `Dataset`s ```python theme={null} from classiq.applications.qnn.datasets import DATASET_NOT from torch.utils.data import DataLoader DATALOADER_NOT = DataLoader(DATASET_NOT, batch_size=2, shuffle=True) for data, label in DATALOADER_NOT: print(f"Training the following data: {data}") print(f"with the following labels: {label}") ``` ### Using the `DatasetNot` class #### without using our pre-defined transformers ```python theme={null} from classiq.applications.qnn.datasets import DatasetNot from torch.utils.data import DataLoader NUM_QUBITS = 1 DATASET_NOT = DatasetNot(NUM_QUBITS) DATALOADER_NOT = DataLoader(DATASET_NOT, batch_size=2, shuffle=True) for data, label in DATALOADER_NOT: print(f"Training the following data: {data}") print(f"with the following labels: {label}") ``` #### with our pre-defined transformers ```python theme={null} from classiq.applications.qnn.datasets import ( DatasetNot, state_to_weights, state_to_label, ) from torch.utils.data import DataLoader from torchvision.transforms import Lambda NUM_QUBITS = 1 DATASET_NOT = DatasetNot( 1, transform=Lambda(state_to_weights), target_transform=Lambda(state_to_label) ) DATALOADER_NOT = DataLoader(DATASET_NOT, batch_size=2, shuffle=True) for data, label in DATALOADER_NOT: print(f"Training the following data: {data}") print(f"with the following labels: {label}") ``` # Quantum Neural Networks Source: https://docs.classiq.io/user-guide/applications/qml/qnn/index * [QNN](/user-guide/applications/qml/qnn/qnn) * [A full example](/user-guide/applications/qml/qnn/a-full-example) * [The `QLayer` object](/user-guide/applications/qml/qnn/qlayer) * [Simplified quantum layer (`QLayerV2`)](/user-guide/applications/qml/qnn/qlayerv2) * [Available datasets](/user-guide/applications/qml/qnn/datasets) # Quantum Layer Source: https://docs.classiq.io/user-guide/applications/qml/qnn/qlayer The Classiq engine exports the `QLayer` object, which inherits from `torch.nn.Module` (like most objects in the `torch.nn` namespace), and it acts like one. The `QLayer` object is defined like this: [comment]: DO_NOT_TEST ```python theme={null} class QLayer(nn.Module): def __init__( self, quantum_program: QuantumProgram, execute: ExecuteFunction, post_process: PostProcessFunction, ) -> None: ... ``` Or, [comment]: DO_NOT_TEST ```python theme={null} class QLayer(nn.Module): def __init__( self, quantum_program: QuantumProgram, post_process: PostProcessFunction, ) -> None: ... ``` The first parameter, `quantum_program`, is the result of [synthesizing a quantum model](/user-guide/synthesis/index). Note that the parameters are assumed to follow the API stated in [qnn](/user-guide/applications/qml/qnn/qnn). The second parameter is a callable which is responsible for executing the quantum program, usually with [`execute_qnn`](#execution). It takes a `QuantumProgram` and `MultipleArguments` (a list of arguments sets to assign to the quantum program parameters) as inputs, and returns a `ResultsCollection`. Note that this argument can be left out, as demonstrated in the second code block. If it is not supplied, the layer will create an `ExecutionSession` and sample the quantum program automagically. In order to properly close the `ExecutionSession`, if it was created, call the `teardown` method of `QLayer`. The third parameter is a callable which is responsible for post-processing each execution result. It takes a `SavedResult` as input, process it and returns a `Tensor`. ## Saving and loading models (checkpointing) For hybrid QNN models that include a `QLayer`, prefer saving and loading only the weights: [comment]: DO_NOT_TEST ```python theme={null} # Save torch.save(model.state_dict(), path) # Load (recreate model structure first, then load weights) model = Net(...) # same structure as when saved model.load_state_dict(torch.load(path)) ``` Whole-model pickle (`torch.save(model, path)`) is supported, but `post_process` and the execution path cannot be pickled when they are local functions, lambdas, or closures (e.g. defined inside `__init__`). In that case they are omitted from the checkpoint. After loading: 1. Call `layer.register_post_process(your_post_process)` on each `QLayer` that had a non-serializable `post_process` before calling `forward()`. 2. If you know `post_process` is not serializable, you can pass `serializable_post_process=False` when constructing the `QLayer` to skip pickling it and avoid a warning on save. ## Example callables An example of such callables: [comment]: DO_NOT_TEST ```python theme={null} import torch from classiq.applications.qnn.types import ( MultipleArguments, SavedResult, ResultsCollection, ) from classiq import execute_qnn from classiq.synthesis import QuantumProgram def execute( quantum_program: QuantumProgram, arguments: MultipleArguments ) -> ResultsCollection: return execute_qnn(quantum_program, arguments) def post_process(result: SavedResult) -> torch.Tensor: # for example, post-processing can take some value out of `result.value.counts`, which is a `dict` value = _post_process_result(result) return torch.tensor(value) ``` ## Execution To facilitate the execution of your quantum layer, we supply the utility function `execute_qnn`. It enables you to easily execute a batch of input arguments, and instruct whether you want the sample results or the estimation results according to a specific observable. The inputs for `execute_qnn` are: * `quantum_program` of type `QuantumProgram` * `arguments` of type `MultipleArguments` * (optionally) `observable` of type `PauliOperator`. The function returns a `ResultsCollection`, which is a list of `SavedResult` objects (see [Execution Results](/user-guide/execution/index#results) for more information). The type of each `SavedResult` depends on the `observable` input: * If no `observable` were given, the type would be `ExecutionDetails`. * Otherwise, the type would be `EstimationResult`. If only one observable was given, `execute_qnn` will estimate the execution of all batched arguments with this observable. If more than one observable was given, their number should match the number of batched arguments, and each execution with a set of arguments will be estimated with the matching observable. ### Examples [comment]: DO_NOT_TEST ```python theme={null} # Execute and return the sample results def execute( quantum_program: QuantumProgram, arguments: MultipleArguments ) -> ResultsCollection: return execute_qnn(quantum_program, arguments) # Execute and return the estimation results according to a specific observable def execute( quantum_program: QuantumProgram, arguments: MultipleArguments ) -> ResultsCollection: return execute_qnn( quantum_program, arguments, observable=PauliOperator( pauli_list=[("II", 1 / 2), ("IZ", -1 / 2), ("ZI", -1 / 2)] ), ) ``` ## Behind the Scenes Behind the scenes, the `QLayer` handles the following actions: * Processing of the PQC * Initializing and tracking of parameters * Passing the inputs and weights (as multi-dimensional tensors) to the execution function * Passing the results from the execution function to the post-processing function * Gradient calculation on the PQC # Simplified Quantum Layer (QLayerV2) Source: https://docs.classiq.io/user-guide/applications/qml/qnn/qlayerv2 `QLayerV2` is a new, simplified quantum layer for hybrid QNNs. It is constructed from a synthesized quantum program and an optional set of observables, and produces a trainable `torch.nn.Module` whose outputs are the expectation values of those observables. The new interface is designed to be more intuitive than the classic `QLayer` and - just as importantly - it is **much faster** behind the scenes, so training loops run considerably quicker. Use it when your quantum layer's output is naturally a set of expectation values. For layers that need sampling, counts, or custom classical post-processing, keep using the classic [`QLayer`](/user-guide/applications/qml/qnn/qlayer) (see [Choosing between the two layers](#choosing-between-the-two-layers)). `QLayerV2` currently runs only inside **Classiq Studio** and is limited to circuits of **up to 26 qubits**. Constructing it in any other environment, or with a larger circuit, raises an error. Extending its scale and reach is planned. **`QLayerV2` is a temporary name.** It ships alongside the existing `QLayer` (also available under the alias `QLayerV1`), which is unchanged for now so existing code keeps working. `QLayerV2` is the interface we are moving toward: once it covers the full feature set, the classic interface will go through the standard deprecation process and `QLayerV2` will become the single, canonical `QLayer`. That deprecation has **not** started yet. ## Interface `QLayerV2` is imported from the same package as `QLayer`: [comment]: DO_NOT_TEST ```python theme={null} from classiq.applications.qnn import QLayerV2 QLayerV2( qprog, # a synthesized QuantumProgram observables=None, # optional; defaults to one per qubit ) ``` * `qprog` - a synthesized [`QuantumProgram`](/user-guide/synthesis/index). Its parameters are classified by name: the `input`/`i_` prefixes mark **inputs** (the encoded classical data) and the `weight`/`w_` prefixes mark **trainable weights** (for example `input_0`, `i_x`, `weight_0`, `w_theta`). See [Parameters: Inputs Versus Weights](/user-guide/applications/qml/qnn/qnn#parameters-inputs-versus-weights). * `observables` - one output feature per Pauli observable. Defaults to one `` per qubit, so the default output width equals the circuit width. Accepts a single `SparsePauliOp` or a sequence of them. An observable narrower than the program is padded with identities to the program's width. The layer trains like any other `torch` module. Its `forward` is rank-preserving: an input of shape `(batch, in_features)` produces `(batch, out_features)`, and an unbatched input of shape `(in_features,)` produces `(out_features,)`. ## Examples ### Minimal example With only a `qprog`, the layer measures one `` per qubit using the default observables: [comment]: DO_NOT_TEST ```python theme={null} from classiq import * from classiq.applications.qnn import QLayerV2 @qfunc def main(input_0: CReal, weight_0: CReal, res: Output[QArray]) -> None: allocate(2, res) RY(input_0, res[0]) # driven by the input feature RY(weight_0, res[1]) # trainable weight CX(res[0], res[1]) qprog = synthesize(main) # Defaults: one per qubit. layer = QLayerV2(qprog) ``` The result is a trainable `torch` layer that plugs into an ordinary network: [comment]: DO_NOT_TEST ```python theme={null} import torch.nn as nn class MyNet(nn.Module): def __init__(self) -> None: super().__init__() self.pre = nn.Linear(4, 1) self.quantum = QLayerV2(qprog) def forward(self, x): x = self.pre(x) return self.quantum(x) ``` ### Custom observables Pass one `SparsePauliOp` per output feature to control what the layer measures. Here the single output feature is ``: [comment]: DO_NOT_TEST ```python theme={null} from classiq.qmod.builtins.enums import Pauli # : Pauli-Z on qubit 0. Narrower observables are padded to the circuit width. z0 = Pauli.Z(0) layer = QLayerV2(qprog, observables=[z0]) ``` ## Choosing between the two layers The classic [`QLayer`](/user-guide/applications/qml/qnn/qlayer) is also available under the alias `QLayerV1`, so the two interfaces can be named side by side during the transition. | | [`QLayerV1`](/user-guide/applications/qml/qnn/qlayer) (alias of the classic `QLayer`) | `QLayerV2` (new) | | ----------- | ------------------------------------------------------------------------------------- | ----------------------------------------- | | Output | Any tensor from a custom `post_process` | Expectation values of Pauli observables | | You provide | `execute` and `post_process` callables | Just `qprog` (and optional `observables`) | | Measurement | Sampling derived values | Expectation values only | | Speed | Standard | Much faster | | Runs | Anywhere execution is available | Classiq Studio only, up to 26 qubits | `QLayerV1` remains available today for sampling, counts, and custom post-processing. `QLayerV2` is the interface we are building toward and will eventually become the single, canonical `QLayer`. Reach for `QLayerV2` when you want the simplest, fastest path to an expectation-value layer. # Quantum Neural Networks (QNN) Source: https://docs.classiq.io/user-guide/applications/qml/qnn/qnn The Classiq QNN package is integrated with PyTorch so you can define `torch` networks with the addition of quantum layers (which are quantum programs). This topic assumes basic knowledge of classical neural networks. A neural network can be described as a list of layers, where each layer takes in a vector and outputs a different vector. The vectors are treated as one-dimensional, and other dimensions are identical up to `reshape`. The input and output of each layer is a vector of classical data; for convenience, called a `list` of `float`s. This implies that any quantum layer, assuming the structure of the data transfer between the layers does not change, must take in classical data and return classical data. This is done by renaming the incoming data as `parameters` for the quantum program, measuring the quantum layer, and applying classical post-processing calculations. ## Examples One example for post-processing outputs of a quantum layer is returning a single number between `0` and `1`, indicating the confidence of a single choice. This is common in cases of binary classification where a single qubit is measured and the output of the quantum layer is `the amount of |0> measured` divided by `the total number of measurements`. Another example for post-processing is returning the probability (or amplitude) of each result. If the measurement result is this: ``` { "00": 10, "01": 20, "10": 30, "11": 40, } ``` Then the output of the quantum layer can be this: ``` [0.1 , 0.2 , 0.3 , 0.4] ``` which is normalized by the number of measurements, and the result is an ordered list of the probabilities of each result. See a [full working example](/user-guide/applications/qml/qnn/a-full-example). ## Parameters: Inputs Versus Weights A complete quantum layer takes two types of parameters: "inputs" and "weights". The "input" parameters handle the encoding of the data (the classical `list` of `float`s), whereas the "weight" parameters undergo gradient descent in the usual NN way. The "input" parameters are usually handled by the first sub-layer, while the "weight" parameters are usually handled by the rest of the sub-layers. The Classiq engine distinguishes between the two types of parameters by their initial name: * `input_something` or `i_something` for inputs * `weight_something` or `w_something` for weights ## Classiq Engine API ### `QLayer` Classiq exports the `QLayer` object, which inherits from `torch.nn.Module` (like most objects in the `torch.nn` namespace), and it acts accordingly. For example: [comment]: DO_NOT_TEST ```python theme={null} class MyNet(nn.Module): def __init__(self) -> None: self.linear_layer = nn.Linear(...) self.quantum_layer = classiq.QLayer(...) def forward(self, x: Tensor): x = self.linear_layer(x) x = self.quantum_layer(x) return x ``` The full declaration of the `QLayer` object, with explanations about the parameter it gets, are described [here](/user-guide/applications/qml/qnn/qlayer). There are two quantum-layer interfaces, both fully supported. Use the classic `QLayer` (this page) when your layer needs sampling, counts, or custom classical post-processing. Use the simpler [`QLayerV2`](/user-guide/applications/qml/qnn/qlayerv2) when the layer's outputs are expectation values of Pauli observables. ### Datasets Classiq provides two [datasets](/user-guide/applications/qml/qnn/datasets) for play-testing examples. 1. "NOT" takes in a single-qubit state (either |0> or |1>) and returns an $n$-qubit state of all-ones or all-zeros, respectively. For example, for $n=2$: `0 -> |11>`, `1 -> |00>`. 2. "XOR" takes in an $n$-qubit state and returns a single classical bit, equal to the bitwise-xor of all the bits from the input state. For example, `101 -> 0`, `10101 -> 1`, `10 -> 1`, `11 -> 0`. ### Gradients The Classiq engine automatically calculates the gradient of a PQC. And there are many more calculations on their way. Stay tuned! # Client Credentials (M2M) Authentication Source: https://docs.classiq.io/user-guide/authentication/m2m-authentication The Classiq SDK supports a non-interactive, machine-to-machine (M2M) authentication flow built on the OAuth 2.0 client credentials grant. Use this flow when a backend service needs to call the Classiq platform without a person signing in. ## Obtaining credentials M2M clients are provisioned by Classiq rather than created self-service. To obtain credentials, contact your Classiq account manager or sales representative and request an M2M client for your account. Once the client is created, Classiq delivers the credentials over a secured channel to avoid exposing the secret in transit: * You receive an encrypted link to the credential details. * Access is protected by a **one-time verification code** sent to your recipient email address when you open the link. Treat `CLASSIQ_CLIENT_SECRET` like any other production secret. Do not commit it to source control, embed it in code, or print it in logs. ## Configuring the environment First, make sure you have the latest Classiq SDK installed: ```bash theme={null} pip install -U classiq ``` If you have not yet installed the SDK, follow the [Python SDK installation guide](/getting-started/sdk_installation) first. The SDK reads the credentials from the process environment at authentication time. When both `CLASSIQ_CLIENT_ID` and `CLASSIQ_CLIENT_SECRET` are present, it authenticates silently and skips the interactive browser login entirely. | Variable | Required | Purpose | | ----------------------- | -------- | -------------------------------------------------------------- | | `CLASSIQ_CLIENT_ID` | Yes | The M2M client identifier. | | `CLASSIQ_CLIENT_SECRET` | Yes | The client secret paired with the ID. | | `CLASSIQ_ORG_ID` | No | Selects an organization when the client has access to several. | Set these values as environment variables wherever your code runs. On Linux and macOS, export them in your shell session: ```bash theme={null} export CLASSIQ_CLIENT_ID="m2m_client_id" export CLASSIQ_CLIENT_SECRET="your_high_entropy_secret" ``` On Windows, set them with PowerShell for the current session: ```powershell theme={null} $env:CLASSIQ_CLIENT_ID = "m2m_client_id" $env:CLASSIQ_CLIENT_SECRET = "your_high_entropy_secret" ``` Replace the placeholder values with the client ID and secret you received. Because the SDK reads these from the environment, any mechanism that sets them for the process works. In a CI/CD pipeline, keep the secret in the platform's encrypted secrets store and inject both variables into the job. In GitHub Actions: ```yaml theme={null} env: CLASSIQ_CLIENT_ID: ${{ secrets.CLASSIQ_CLIENT_ID }} CLASSIQ_CLIENT_SECRET: ${{ secrets.CLASSIQ_CLIENT_SECRET }} ``` When running in a container, pass the variables through from the host environment: ```bash theme={null} docker run \ -e CLASSIQ_CLIENT_ID \ -e CLASSIQ_CLIENT_SECRET \ your-image ``` ## Authenticating with M2M credentials You call `classiq.authenticate()` exactly as in the interactive flow. With the variables set, it detects the M2M credentials and uses them instead of opening a browser, so the same code runs unattended. The M2M flow takes priority over any credentials cached from a previous interactive login on the same machine. M2M tokens are held in memory for the lifetime of the process and are not written to disk, so each new process authenticates fresh. ## Troubleshooting If authentication fails, the SDK raises: ```text theme={null} classiq.interface.exceptions.ClassiqAuthenticationError: Request to Auth0 failed with error code 401: access_denied ``` A `401 access_denied` response means the credentials were rejected or the SDK version does not support the M2M flow. Work through these checks: 1. **Verify the version.** Confirm you are running an up-to-date Classiq SDK: ```bash theme={null} pip show classiq ``` 2. **Verify the variables.** Confirm the client ID is exported in the environment running your code: ```bash theme={null} echo $CLASSIQ_CLIENT_ID ``` On Windows PowerShell, use `echo $env:CLASSIQ_CLIENT_ID` instead. If it prints nothing, the variables are not set in the current session. Re-export them, and make sure your CI/CD job or scheduler passes them through to the process. If the problem persists, ask in the [community Slack](https://short.classiq.io/join-slack). # SDK Authentication Source: https://docs.classiq.io/user-guide/authentication/sdk-authentication The Classiq Python SDK authenticates your device with your Classiq account through an interactive, browser-based confirmation. For unattended, server-to-server contexts where no person can sign in, use the [Client Credentials (M2M) flow](/user-guide/authentication/m2m-authentication) instead. ## Authenticating your device Make sure the SDK is installed before authenticating; see the [Python SDK installation guide](/getting-started/sdk_installation). In Python, run: [comment]: DO_NOT_TEST ```python theme={null} import classiq classiq.authenticate() ``` A confirmation window opens in your web browser. Confirm the authentication: Once confirmed, you are ready to use Classiq. The credentials are cached locally and refreshed automatically, so you only need to re-authenticate if they are missing, expired, or revoked. ## Re-authenticating To replace cached credentials, for example after they expire or when switching accounts, overwrite them: [comment]: DO_NOT_TEST ```python theme={null} import classiq classiq.authenticate(overwrite=True) ``` ## Troubleshooting Overwrite the cached credentials and authenticate again, as described in [Re-authenticating](#re-authenticating). This error means your authentication credentials were denied. Overwrite them as described in [Re-authenticating](#re-authenticating). The authentication procedure on headless Linux systems stores the tokens locally in a credentials file. You must still run the authentication once, but it can be done on another system that has a browser, then the credentials file copied over. On some Apple computers, a system pop-up may appear during authentication: Type your device password and click **Always Allow** twice. If the problem persists, ask in the [community Slack](https://short.classiq.io/join-slack). # Execution Session Source: https://docs.classiq.io/user-guide/execution/ExecutionSession This section explains how to execute a quantum program using the `ExecutionSession` class. The class enables executing a quantum program with parameters and operations, eliminating the need to resynthesize the model. ## Initializing Initialize `ExecutionSession` with either: * A **quantum program** (the output of `synthesize`), or * **OpenQASM 2.0 or 3.0** source as a **string** (same semantics as the first argument to [`sample`](/user-guide/execution/index#sampling-openqasm)). It is recommended to use `ExecutionSession` as a context manager in order to ensure resources are cleaned up. Alternatively, you can call the `close` method directly. ### Quantum program [comment]: DO_NOT_TEST ```python theme={null} from classiq import qfunc, synthesize, ExecutionSession @qfunc def main(): pass qprog = synthesize(main) with ExecutionSession(qprog) as es: ... ``` ### OpenQASM string [comment]: DO_NOT_TEST ```python theme={null} from classiq import ExecutionSession openqasm = """ OPENQASM 2.0; include "qelib1.inc"; qreg q[1]; h q[0]; """ with ExecutionSession(openqasm) as es: details = es.sample() ``` When the first argument is OpenQASM text, do not use `parameters` on `sample` / `submit_sample` for Qmod-style `main` arguments; use a synthesized `QuantumProgram` instead. ## Operations `ExecutionSession` supports three types of operations: * `sample`: Executes the quantum program with the specified parameters. * `estimate`: Computes the expectation value of the specified Hamiltonian using the quantum program. * `minimize`: Runs a variational minimization loop, iteratively sampling the circuit to minimize a cost function using the COBYLA optimizer. When estimating using a simulator, the `ClassiqSimulatorBackendNames.SIMULATOR_STATEVECTOR` backend calculates the expectation value directly from the computed state vector, as opposed to computing it from shots. Each invocation mode can use one of these options: * `single` * `submit`: When using submit as a prefix, the job object returns immediately, and the results should be polled. See `ExecutionJob` in the [sdk reference](/sdk-reference/index). For the `sample` method, pass a single parameter dict or a list for batch execution: * `es.sample(parameters: Optional[ExecutionParams | List[ExecutionParams]])` * `es.submit_sample(parameters: Optional[ExecutionParams | List[ExecutionParams]])` The `estimate` method works the same way: * `es.estimate(hamiltonian: SparsePauliOp, parameters: Optional[ExecutionParams | List[ExecutionParams]])` * `es.submit_estimate(hamiltonian: SparsePauliOp, parameters: Optional[ExecutionParams | List[ExecutionParams]])` The `minimize` method accepts a `max_iteration` parameter: * `es.minimize(cost_function, initial_params, max_iteration)` `max_iteration` does not directly control the number of quantum jobs. It sets an upper bound on the number of classical optimizer (COBYLA) iterations; the optimizer may converge before reaching this limit. See the [scipy COBYLA documentation](https://docs.scipy.org/doc/scipy/reference/optimize.minimize-cobyla.html) for details on iteration and convergence behavior. ## Examples ### sample() A simple example of how to use `sample` and its variations: [comment]: DO_NOT_TEST ```python theme={null} from classiq import ( qfunc, Output, QBit, CReal, synthesize, RX, allocate, ExecutionSession, ) @qfunc def main(x: Output[QBit], t: CReal): allocate(1, x) RX(t, x) qprog = synthesize(main) with ExecutionSession(qprog) as execution_session: sample_result = execution_session.sample({"t": 0.5}) print(sample_result.dataframe) batch_sample_result = execution_session.sample([{"t": 0.5}, {"t": 0.6}]) sample_job = execution_session.submit_sample({"t": 0.5}) batch_sample_job = execution_session.submit_sample([{"t": 0.5}, {"t": 0.6}]) ``` ### estimate() An example that shows how to use `estimate` and its variations: ```python theme={null} from classiq import ( qfunc, Output, QBit, CReal, synthesize, Pauli, RX, allocate, ExecutionSession, ) @qfunc def main(x: Output[QBit], t: CReal): allocate(1, x) RX(t, x) qprog = synthesize(main) with ExecutionSession(qprog) as execution_session: hamiltonian = Pauli.I(0) + 2 * Pauli.Z(0) estimate_result = execution_session.estimate(hamiltonian, {"t": 0.5}) batch_estimate_result = execution_session.estimate( hamiltonian, [{"t": 0.5}, {"t": 0.6}] ) estimate_job = execution_session.submit_estimate(hamiltonian, {"t": 0.5}) batch_estimate_job = execution_session.submit_estimate( hamiltonian, [{"t": 0.5}, {"t": 0.6}] ) ``` ## Handling Long Jobs When handling long-running jobs (jobs that are submitted to HW providers with potentially very long queues) it is advisable to retrieve and save the job ID for future reference: [comment]: DO_NOT_TEST ```python theme={null} ... with ExecutionSession(qprog) as session: job = session.submit_sample() job_ID = job.id restored_job = ExecutionJob.from_id(job_ID) results = restored_job.get_sample_result() ``` Alternatively, for scenarios requiring result polling, e.g., iterative hybrid algorithms, consider the following partial code example. This example runs a series of quantum estimation jobs, step by step. It automatically submits each job, collects the results, checks they meet certain criteria (for example, does the cost exceed a certain threshold), and adjusts the parameters for the next job based on those results. [comment]: DO_NOT_TEST ```python theme={null} from classiq import ExecutionSession, ExecutionJob def get_data_from_file(): # read data from file return data def store_data_in_file(data): # save data in file return with ExecutionSession(qprog) as session: iterations_data = get_data_from_file() or [] for _ in range(max_iterations - len(iterations_data)): if iterations_data and "result" not in iterations_data[-1]: # continue the loop from the last iteration last_job = ExecutionJob.from_id(iterations_data[-1]["job_id"]) else: last_job = session.submit_estimate(hamiltonian, params) iterations_data.append({"job_id": last_job.id, "params": params}) store_data_in_file(iterations_data) result = last_job.get_estimate_result() iterations_data[-1]["result"] = result store_data_in_file(iterations_data) if result_is_good_enough(result): break params = compute_the_next_parameters(result) ``` ## `@cfunc` Note that Classiq does not support algorithms utilizing `@cfunc` (or `cscope` in Qmod native) for long job execution. # Hardware Benchmarking Source: https://docs.classiq.io/user-guide/execution/benchmarking/index Benchmarking quantum hardware is essential on the path toward practical quantum computing. As hardware advances, benchmarking must go beyond standard device-level tests such as randomized benchmarking or Quantum Volume to include **algorithmic- and application-level** evaluation, which measures what ultimately matters: the correctness and usefulness of the final computational outcome. ## Functional-level benchmarks Classiq provides a functional-level benchmarking suite for this purpose. Each benchmark is defined by a quantum model, a **score**, and a problem-size parameter, so you can study how performance scales with the task. Hardware constraints such as circuit width are handled at the synthesis stage rather than hard-coded into the benchmark, keeping the functional definition of a task separate from the capabilities of the target device. ## Running across backends Classiq exposes a wide variety of backends — QPUs, hardware emulators, and simulators across many providers — and lets you switch smoothly between them. The platform also handles the operational side of a run — managing execution budget (including running jobs against your Classiq-allocated budget, or emulating a device's noise model without consuming QPU time or credits) and tracking the time each job takes to complete, so backends are directly comparable on both accuracy and turnaround. ## Next steps Run a set of predefined benchmarks covering canonical algorithmic and application-level tasks, or define your own. See [Predefined Benchmarks](./predefined-benchmarks) to get started. # Predefined Benchmarks Source: https://docs.classiq.io/user-guide/execution/benchmarking/predefined-benchmarks The predefined benchmarks let you measure and compare how well quantum backends run a set of well-established quantum circuits. You pick a benchmark, a range of problem sizes, and a set of backends; Classiq runs every combination, scores each result, and returns a table and a scalability chart you can use to decide which hardware fits your workload. You run benchmarks from the **Classiq IDE**, a no-code interface on the Execution page. It's ideal for interactive hardware evaluation and for comparing devices without writing any code. The same benchmarks can also be run from the Classiq SDK via [`run_benchmark`](../../../sdk-reference/execution#run_benchmark). ## Supported benchmarks Each benchmark targets a different circuit family and scales with a single **problem size** parameter. Every benchmark returns a **score** between 0 and 1, where higher is better and lower values reflect hardware noise and errors. For most benchmarks 1 corresponds to ideal, noiseless behavior; for some (such as Dynamical Localization) the ideal noiseless value can itself be lower than 1. The score is defined per benchmark: | Benchmark | What it measures | Problem size | Score | | -------------------------- | -------------------------------------------------------------- | -------------------- | -------------------------------------------------------------- | | **GHZ** | Ability to create and preserve a maximally entangled GHZ state | Number of qubits | Fidelity of the prepared GHZ state | | **Adder** | Correctness of in-circuit modular addition | Register size (bits) | Probability of measuring the correct sum | | **QFT** | Accuracy of the Quantum Fourier Transform output distribution | Number of qubits | 1 − total-variation distance from the ideal distribution | | **State Preparation** | Accuracy of preparing a target (linear-amplitude) state | Number of qubits | 1 − total-variation distance from the ideal distribution | | **Dynamical Localization** | Preservation of localization dynamics under repeated kicks | System size | Normalized localization peak (geometric mean over kick counts) | Some benchmarks enforce a minimum problem size (for example, GHZ and Adder start at 3). The IDE enforces these limits in the configuration form. ## Benchmarking in the Classiq IDE Open the **Execution** page in the IDE and switch the mode toggle from **Quantum Program** to **Benchmark**. ### Configure the run Select a benchmark type. A short description and its problem-size parameter are shown. Then set: * **Problem sizes:** the range of sizes to sweep, as **min / max / step** (for example, GHZ at 4, 8, 16). Each size runs as its own job. * **Backends:** multi-select from the available backends (up to **10** per session). Includes QPUs, hardware emulators, and simulators. * **Shots:** number of shots per job (default: 1,000). * **Run via Classiq:** per-backend toggle. When on, the job runs against your Classiq-allocated budget using Classiq's provider credentials, so you don't need your own account with that provider. * **Emulate:** run against the target hardware's noise model without consuming real QPU time or credits. * **Name:** an optional name for the benchmark session. Click **Run Benchmark** to submit. ### Cancel, history, and the Jobs page **Cancel** stops the session: jobs that already finished (Done or Failed) stay in the table and chart; jobs still Pending or Running are set to Cancelled. Reopening a past session re-hydrates its full chart and table. # Managing Execution Budget Source: https://docs.classiq.io/user-guide/execution/budget-management Enrolled users can run quantum programs on multiple backends without needing to provide their own credentials, by using `run_via_classiq`. When a job is submitted using `run_via_classiq`, it will only proceed if the estimated cost is within the remaining budget allocated for the chosen provider. Additionally, we offer methods to monitor usage and set limits, helping you control and optimize your spending. For example, to set up execution using `run_via_classiq` on the Amazon Braket SV1 simulator: [comment]: DO_NOT_TEST ```python theme={null} from classiq import * @qfunc def main(x: Output[QBit]): allocate(x) qprog = synthesize(main) exec_pref = ExecutionPreferences( backend_preferences=AwsBackendPreferences( backend_name="SV1", run_via_classiq=True, ) ) # Assuming you have a pre-defined quantum program "qprog" with ExecutionSession(qprog, execution_preferences=exec_pref) as es: result = es.sample() ``` ## Checking the Remaining Budget To view your remaining execution budget, run the following command: [comment]: DO_NOT_TEST ```python theme={null} from classiq import * budget = get_budget() print(budget) ``` A table will be displayed: Budget ## Setting a Custom Budget Limit You can also define a custom spending limit to stay within a desired budget (lower than your total budget). For example: [comment]: DO_NOT_TEST ```python theme={null} from classiq import * budget = set_budget_limit( provider=ProviderVendor.AMAZON_BRAKET, limit=90, # Set a custom limit below the provider's remaining budget ) ``` To verify the updated limit, simply run `print(budget)` again. The output will now reflect the new budget cap: Budget_limited To clear user-defined budget limits, run `clear_budget_limit("Amazon Braket")`. # Calculate State Vector Source: https://docs.classiq.io/user-guide/execution/calculate-state-vector The `calculate_state_vector(...)` function is used when you want direct access to the quantum state itself, including amplitudes and phases. Related pages: * [Execution overview](./index) * [Sampling](./sample) * [Observe](./observe) ## When to use state-vector calculation Use `calculate_state_vector(...)` when you want the full quantum state rather than measurement statistics or a single expectation value. State-vector calculations are only available only on Classiq simulators. This is useful when you want to: * inspect amplitudes directly * understand the exact state prepared by the circuit * analyze phases and probabilities * debug or study a circuit without sampling noise Unlike sampling, noiseless state-vector calculation is deterministic. It does not rely on repeated measurements, so it does not introduce statistical fluctuations. ## Basic example ```python theme={null} from classiq import * backend_name = "simulator" @qfunc def main(res: Output[QBit]) -> None: allocate(res) H(res) qprog = synthesize(main) df = calculate_state_vector( qprog, backend=backend_name, ) df ``` **Example output** | x | amplitude | magnitude | phase | probability | bitstring | | - | ------------------ | --------- | ----- | ----------- | --------- | | 0 | 0.707107+0.000000j | 0.71 | 0.00π | 0.5 | 0 | | 1 | 0.707107+0.000000j | 0.71 | 0.00π | 0.5 | 1 | ### Understanding the result The returned object is a DataFrame describing the full state. Common columns include: * **quantum variables** - the quantum number values, quantum arrays, and qubits * **amplitude** — the complex amplitude of the basis state * **magnitude** — the absolute value of the amplitude * **phase** — the phase angle * **probability** — the squared magnitude * **bitstring** — the computational basis state Because the result is a DataFrame, you can inspect or manipulate it using standard operations. ### Filtering small amplitudes By default, very small amplitudes are filtered out below a threshold of $10^{-16}$. You can control this behavior using `amplitude_threshold`: [comment]: DO_NOT_TEST ```python theme={null} df = calculate_state_vector( qprog, backend=backend_name, amplitude_threshold=0.01 ) ``` This is useful for reducing numerical noise and simplifying the displayed state, especially for larger systems where many amplitudes may be negligible. ## Parameterized execution State-vector calculation also supports parameterized quantum programs. This means you can define symbolic parameters in your circuit and provide their numerical values at execution time. **Example** ```python theme={null} from classiq import * backend_name = "simulator" @qfunc def main(angle_rx: CReal, angle_ry: CReal, x: Output[QBit]): allocate(x) RX(angle_rx, x) H(x) RY(angle_ry, x) qprog = synthesize(main) exec_params = { "angle_rx": 0.5, "angle_ry": 0.3 } df = calculate_state_vector( qprog, backend=backend_name, parameters=exec_params, ) df ``` | x | amplitude | magnitude | phase | probability | bitstring | | - | ------------------ | --------- | ------ | ----------- | --------- | | 1 | 0.779815+0.146834j | 0.79 | 0.06π | 0.629672 | 1 | | 0 | 0.575048-0.199119j | 0.61 | -0.11π | 0.370328 | 0 | In this workflow: * the circuit structure stays the same * the parameter values are supplied at execution time * the returned DataFrame describes the exact state corresponding to those values ## Summary Use `calculate_state_vector(...)` when you need the full quantum state. State-vector calculation: * returns a DataFrame * provides amplitudes, phases, and probabilities * is deterministic * supports parameterized execution * only allowed on simulators # Execution on Alice and Bob Quantum Cloud Source: https://docs.classiq.io/user-guide/execution/cloud-providers/alice-and-bob-backends The Classiq executor supports execution on Alice and Bob logical hardwares. ## Usage Alice and Bob provide multiple logical targets that we can use. Execution on Alice and Bob requires a valid API key. Additionally, Alice and Bob introduces several customizable hardware parameters (Available through SDK only): ### average\_nb\_photons Bit-flip probability decreases exponentially with this parameter, phase-flip probability increases linearly. ### kappa\_1 The rate at which the cat qubit loses one photon, creating a bit-flip. Lower values mean lower error rates. ### kappa\_2 The rate at which the cat qubit is stabilized using two-photon dissipation. Higher values mean lower error rates. ### distance The number of times information is duplicated in the repetition code. Phase-flip probability decreases exponentially with this parameter, bit-flip probability increases linearly. ## Execution ### Setting up Backend Preferences [comment]: DO_NOT_TEST ```python theme={null} from classiq import AliceBobBackendNames, AliceBobBackendPreferences backend_preferences = AliceBobBackendPreferences( backend_service_provider="Alice & Bob", api_key="", backend_name=AliceBobBackendNames.LOGICAL_TARGET, kappa_1=10.0, # OR Remove it to keep default value kappa_2=1000, # OR Remove it to keep default value distance=3, # OR Remove it to keep default value average_nb_photons=7, # OR Remove it to keep default value ) ``` ### Setting up execution preferences and adding number of shots [comment]: DO_NOT_TEST ```python theme={null} from classiq import ExecutionPreferences execution_preferences = ExecutionPreferences( num_shots=1000, backend_preferences=backend_preferences, ) ``` ### Prepare for Hardware-aware synthesis [comment]: DO_NOT_TEST ```python theme={null} from classiq import Preferences preferences = Preferences( backend_service_provider=ProviderVendor.ALICE_AND_BOB, backend_name=AliceBobBackendNames.LOGICAL_TARGET, ) ``` ### Execute At this point you should just create your model, set execution preferences and perform Hardware-aware synthesize then execute. [comment]: DO_NOT_TEST ```python theme={null} quantum_program = synthesize(your_model, preferences=preferences) quantum_program = set_quantum_program_execution_preferences( quantum_program, execution_preferences ) result = execute(quantum_program).result() ``` Choose Model/Graphical Model tab in IDE, and go to ***"Synthesis Configuration"*** section HW aware synthesis ***After synthesizing***, you will be navigated to ***Execution*** page, or you can just navigate to it and choose ***Alice & Bob*** backend(s) Execute Alice & Bob ## Supported Backends * "LOGICAL\_EARLY" * "LOGICAL\_TARGET" * "LOGICAL\_NOISELESS" # Execution on Amazon Braket Cloud Source: https://docs.classiq.io/user-guide/execution/cloud-providers/amazon-backends The Classiq executor supports execution on Amazon Braket's cloud simulators and hardware. Backends may sometimes be unavailable. Check the availability windows with Amazon Braket. ## Usage Execution on Amazon Braket requires an AWS account, and a role that Classiq can assume for execution. [comment]: DO_NOT_TEST ```python theme={null} from classiq import AwsBackendPreferences preferences = AwsBackendPreferences( backend_name="Name of requested simulator or hardware", aws_access_key_id="Amazon access key ID for the user with Braket access", aws_secret_access_key="Secret access key for the user's key ID", s3_bucket_name="S3 bucket name to save the results", s3_folder="The folder path within the S3 bucket, where the results will be saved", job_timeout="Timeout for execution (Optional)", ) ``` Opening info tab ### Initial Account Setup Before first use, the platform needs your permission to connect to your AWS account. This is done by creating a cross-account role. Classiq provides with the attached CloudFormation `AssumeRole.cf.yaml` file. It only has the permissions needed for Braket. To create the cross-account role that only Classiq can use, deploy the CloudFormation file to your account: 1. Download the [AssumeRole.cf.yaml](./resources/AssumeRole.cf.yaml.txt) file. 2. Install [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html). 3. Contact [Classiq support](mailto:support@classiq.io) to obtain these parameters: 1. `CORRECT_TRUSTED_ACCOUNT` 2. `CORRECT_EXTERNAL_ID_VALUE` 4. Execute this command: ``` aws cloudformation create-stack --stack-name ClassiqBraketRole --template-body file://AssumeRole.cf.yaml --capabilities CAPABILITY_NAMED_IAM --parameters ParameterKey=TrustedAccount,ParameterValue=${CORRECT_TRUSTED_ACCOUNT} ParameterKey=ExternalId,ParameterValue=${CORRECT_EXTERNAL_ID_VALUE} ``` The required parameters may differ between users. Contacting Classiq support is required! To learn more about IAM roles, refer to the [AWS documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html). ### Required Credentials When executing via the platform using AWS Cloud, there are several required credentials: 1. `aws_access_key_id` 1. Create a user with Braket full access 2. Create AWS secret key for this user 3. Fill the access key id as you got in the earlier steps 2. `aws_secret_access_key` 1. Fill the Secret access key from the steps above 3. `s3_bucket_name` 1. Create a new bucket. Its name must start with `amazon-braket-`. 2. Use the bucket name as the `s3_bucket_name`. This is the bucket that saves the execution results. 4. `s3_folder` 1. Enter the path to the folder in the `S3 bucket`. This is the path in the bucket where the execution results are saved. For further support, contact [Classiq support](mailto:support@classiq.io). ## Device emulation (emulate) Set `emulate=True` on [`AwsBackendPreferences`](/sdk-reference/providers/AWS) to use Classiq’s **device-aware Braket circuit preparation** for the selected Amazon Braket **device**: the circuit is translated using constraints from that device before the job is submitted. This path differs from the default Qiskit–Braket conversion and can be required for some programs targeting specific hardware. * **Default** `emulate=False`: standard adapter from Qiskit to Braket. * **`emulate=True`**: use Classiq’s emulator-style translation for the chosen device. If compilation fails, try without `emulate` or another backend; see the error message from the executor. Behavior depends on the Braket device and circuit; refer to [Amazon Braket documentation](https://docs.aws.amazon.com/braket/) for device capabilities. [comment]: DO_NOT_TEST ```python theme={null} from classiq import AwsBackendPreferences preferences = AwsBackendPreferences( backend_name="Ankaa-3", aws_access_key_id="…", aws_secret_access_key="…", s3_bucket_name="amazon-braket-…", s3_folder="results", emulate=True, ) ``` ## Supported Backends The Classiq executor supports any available gate-based Amazon Braket simulator and quantum hardware. Included hardware: * "Forte 1" * "Emerald" * "Ankaa-3" * "Garnet" Included simulators: * "SV1" * "TN1" * "dm1" # Execution on Azure Quantum Cloud Source: https://docs.classiq.io/user-guide/execution/cloud-providers/azure-backends The Classiq executor supports execution on Azure Quantum cloud simulators and hardware. Backends may sometimes be unavailable. Check the availability windows with Azure Quantum. ## Usage Possible modes of operation: * Running via Classiq-Azure integration. In this mode you do not need to provide credentials or location. * Executing on your private Azure Quantum Workspace by providing credentials. For more details, see [Executing on Your Quantum Workspace](#executing-on-your-quantum-workspace). [comment]: DO_NOT_TEST ```python theme={null} from classiq import ( AzureCredential, AzureBackendPreferences, ) # Running via Classiq-Azure integration: preferences = AzureBackendPreferences( backend_name="Name of requsted simulator or hardware", ) # Running via a private Azure account: cred = AzureCredential( tenant_id="Azure Tenant ID (from Azure Active Directory)", client_id="Azure Application (client) ID", client_secret="Azure Client Secret", resource_id="Azure Quantum Workspace Resource ID", ) preferences = AzureBackendPreferences( backend_name="Name of requsted simulator or hardware", credentials=cred, location="Azure region of Quantum Workspace", ) ``` Opening info tab For academic users, backends run via Classiq-Azure integration by default. To override this configuration, switch on "Run with my own credentials" in the backends summary section on the Execution page: Config run with own credentials ## Executing on Your Quantum Workspace Execution on your private Azure Quantum Workspace requires an Azure account with an active subscription. To authenticate, provide these details: * `Resource ID`: Azure Quantum Workspace resource ID. * `Location`: Azure region of the Quantum Workspace. * `Tenant ID`: Azure Active Directory tenant ID. * `Client ID`: Azure client ID of a registered application. * `Client secret`: Azure client secret of a registered application. Following is a brief description of the steps to configure and acquire these details: 1. Create an Azure Quantum Workspace (see [Azure documentation](https://docs.microsoft.com/en-us/azure/quantum/how-to-create-workspace)). In the workspace overview are the `Location` and `Resource ID`. 2. Register a new application in Azure, including creating a client secret (see [Azure documentation](https://learn.microsoft.com/en-us/azure/developer/python/sdk/authentication-on-premises-apps?tabs=azure-portal#1---register-the-application-in-azure)). At the end of this step are the settings for `Client ID`, `Client secret`, and `Tenant ID`. 3. Assign the `Contributor` role to the registered application on the Quantum Workspace or a resource group containing the Quantum Workspace (see [Azure documentation](https://learn.microsoft.com/en-us/azure/developer/python/sdk/authentication-on-premises-apps?tabs=azure-portal#2---assign-roles-to-the-application-service-principal)). 4. Add the `Jobs.ReadWrite` permission (under `Azure Quantum`) to the application (see [Azure documentation](https://learn.microsoft.com/en-us/azure/active-directory/develop/howto-add-app-roles-in-azure-ad-apps#assign-app-roles-to-applications)). ## Supported Backends Included hardware: * "ionq.qpu.forte-1" * "ionq.qpu.forte-enterprise-1" Included simulators: * "ionq.simulator" * "rigetti.sim.qvm" * "quantinuum.sim.h2-1sc" * "quantinuum.sim.h2-1e" ## IonQ hardware noise simulation on Azure Quantum (emulate) For **IonQ QPU** targets on Azure Quantum (`backend_name` values that start with `ionq.qpu.`), set `emulate=True` on [`AzureBackendPreferences`](/sdk-reference/providers/Azure) to enable IonQ’s **hardware noise model simulation**. Classiq passes Azure’s `noise` job options using the matching noise profile for the selected backend. Behavior details, allowed profiles, shots, and qubit limits are defined by Azure and IonQ; see [Noise model simulation (Microsoft Learn)](https://learn.microsoft.com/en-us/azure/quantum/provider-ionq#noise-model-simulation). When `emulate` does **not** apply, it is **ignored** (no error): for example `ionq.simulator`, Quantinuum targets, Rigetti targets, or any backend that is not an IonQ QPU. That lets you reuse one preferences object across several Azure targets. `emulate` is separate from `ionq_error_mitigation_flag`, which controls debiasing-style error mitigation on IonQ hardware. [comment]: DO_NOT_TEST ```python theme={null} from classiq import AzureBackendPreferences preferences = AzureBackendPreferences( backend_name="ionq.qpu.aria-1", emulate=True, ) ``` # Execution on C12 Quantum Cloud Source: https://docs.classiq.io/user-guide/execution/cloud-providers/c12 [C12](https://www.c12qe.com/) is building scalable carbon-based quantum computers. Classiq supports execution on C12 QPU-emulators. C12 simulator Callisto imitates the physical parameters of noise from C12's hardware. The fidelity parameters of the simulator can be accessed from C12's [documentation](https://c12qe.github.io/c12-callisto-clients/0.%20README.html). ## Configuration C12 introduces several customizable hardware parameters: ### inilabel The initial state specified using a binary label format, such as "00", "01", etc. ### inistatevector The initial state vector for each qubit, provided as a comma-separated list of complex values. Example: "1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j" Note: Either `inilabel` or `inistatevector` can be provided but not both. ### ininoisy Indicates whether noisy initialization of the circuit should be used. ### ## Usage Example If you are using [Classiq IDE](https://platform.classiq.io/), simply choose C12 in the checkbox menu under Execution page. ### Setting up Backend Preferences [comment]: DO_NOT_TEST ```python theme={null} from classiq.execution import C12BackendPreferences from classiq import ExecutionPreferences, ExecutionSession, synthesize backend_preferences = C12BackendPreferences( backend_name="c12sim-iswap", inilabel="00", # Either inilabel or inistatevector must be provided # inistatevector="1.+0.j, 0.+0.j,", # Either inilabel or inistatevector must be provided ininoisy=True, ) execution_preferences = ExecutionPreferences( num_shots=1000, backend_preferences=backend_preferences, ) ``` ### Execute with ExecutionSession Create your model, set execution preferences, synthesize, then run with `ExecutionSession`: [comment]: DO_NOT_TEST ```python theme={null} quantum_program = synthesize(your_model, preferences=preferences) with ExecutionSession(quantum_program, execution_preferences) as session: result = session.sample() ``` ## Supported Backends * SIMULATOR = "Callisto Emulator" # Execution on Classiq simulators Source: https://docs.classiq.io/user-guide/execution/cloud-providers/classiq-backends Classiq offers execution on simulators that are located at the Classiq backend. These simulators don't require an account on a different cloud, and are usually fast to execute. ## Simulator Usage [comment]: DO_NOT_TEST ```python theme={null} from classiq import ClassiqBackendPreferences preferences = ClassiqBackendPreferences( backend_name="Name of requested quantum simulator" ) ``` Opening info tab Classiq supports following simulators: 1. `simulator`: A general-purpose quantum simulator capable of handling circuits with up to 28 qubits. 2. `simulator_statevector`: Returns the full state vector, including phase information of the output state produced by the quantum circuit. Due to the exponential growth of the state vector, this simulator is suitable only for circuits with up to 18 qubits. 3. `simulator_density_matrix`: Uses density matrices to simulate open quantum circuits and supports simulations for circuits containing up to 28 qubits. 4. `simulator_matrix_product_state`: Efficiently simulates quantum circuits of up to 28 qubits, especially suited for circuits exhibiting low entanglement. You can access these simulators through `ClassiqSimulatorBackendNames`. ## Custom noise models Classiq simulators support user-defined gate and readout noise through `ClassiqSimulatorNoiseSpecification`. This is separate from preset device noise (`noise_model` on `ClassiqBackendPreferences`). See [Custom noise models on Classiq simulators](./custom-noise-models) for channel types, validation rules, and code examples. ## Nvidia Simulator Usage Execution on Nvidia simulators requires specific license permissions. Before first use, contact [Classiq support](mailto:support@classiq.io). Classiq supports two types of Nvidia simulators, with the same inputs and outputs but different underlying infrastructure, capable of simulating circuits with up to 29 qubits: 1. The backends `ClassiqNvidiaBackendNames.SIMULATOR` and `ClassiqNvidiaBackendNames.SIMULATOR_STATEVECTOR` are better suited when multiple circuits need to be executed in sequence. 2. The backends `ClassiqNvidiaBackendNames.BRAKET_NVIDIA_SIMULATOR` and `ClassiqNvidiaBackendNames.BRAKET_NVIDIA_SIMULATOR_STATEVECTOR` are executed using Amazon Braket's infrastructure, and provide faster execution for single circuits. Credentials for AWS are not needed. Both `ClassiqNvidiaBackendNames.SIMULATOR_STATEVECTOR` and `ClassiqNvidiaBackendNames.braket_nvidia_simulator_statevector` return the state vector at the end of the circuit's execution (analogous to the above `simulator_statevector`). **Precision:** Nvidia and Braket Nvidia simulators use **double precision** (float64) by default. Set `use_single_precision=True` in `ClassiqBackendPreferences` to use single precision (float32), which can be faster and use less memory at the cost of numerical precision. ```python theme={null} from classiq import ClassiqBackendPreferences, ClassiqNvidiaBackendNames preferences = ClassiqBackendPreferences( backend_name=ClassiqNvidiaBackendNames.SIMULATOR ) # Optional: use single precision (float32) instead of default double (float64) preferences_single = ClassiqBackendPreferences( backend_name=ClassiqNvidiaBackendNames.BRAKET_NVIDIA_SIMULATOR, use_single_precision=True, ) ``` Opening info tab The number of execution requests to the NVIDIA simulator may be limited. If you encounter any problem, contact [Classiq support](mailto:support@classiq.io). ## DGX Statevector Simulator Classiq offers a GPU-accelerated statevector simulator running on an NVIDIA DGX system. It is designed for large circuits, supporting up to **35 qubits**, making it well suited for state-vector simulations that exceed the capacity of the standard Classiq simulators. Like the other GPU simulators, the DGX simulator requires specific license permissions. Before first use, contact [Classiq support](mailto:support@classiq.io). The DGX simulator supports both execution modes: 1. **Sampling**: returns measurement counts for a given number of shots. 2. **Statevector**: returns the full output state vector. Because the state vector grows exponentially with the number of qubits, large statevector requests can be very large; filtering the returned registers is recommended. The DGX simulator is selected by passing `backend="classiq/dgx_simulator"` to your execution call. For example: [comment]: DO_NOT_TEST ```python theme={null} sample(qprog, backend="classiq/dgx_simulator") ``` **Precision:** The DGX simulator runs in **single precision** (float32), which keeps the memory footprint low enough to reach its 35-qubit capacity. The number of execution requests to the DGX simulator may be limited, and large circuits can take a long time to run. If you encounter any problem, contact [Classiq support](mailto:support@classiq.io). ## Supported Backends Included simulators: * "nvidia\_simulator\_statevector" * "simulator" * "simulator\_statevector" * "simulator\_density\_matrix" * "nvidia\_simulator" * "braket\_nvidia\_simulator" * "simulator\_matrix\_product\_state" * "braket\_nvidia\_simulator\_statevector" * "dgx\_simulator" # Custom noise models on Classiq simulators Source: https://docs.classiq.io/user-guide/execution/cloud-providers/custom-noise-models Define depolarizing, Pauli, thermal relaxation, and readout noise for Classiq-hosted simulators. Classiq-hosted simulators support **user-defined noise** through [`ClassiqSimulatorNoiseSpecification`](/sdk-reference/providers/simulator-noise#classiqsimulatornoisespecification). Combine any subset of gate and readout channels; an empty specification runs an ideal (noise-free) simulation. Custom noise is available on Classiq Aer simulators (`simulator`, `simulator_density_matrix`, `simulator_matrix_product_state`, and related backends) and on Nvidia / Braket Nvidia simulators. See [Execution on Classiq simulators](./classiq-backends) for backend names. **Preset vs custom noise:** [`ClassiqBackendPreferences`](/sdk-reference/providers/Classiq) accepts either `noise_model` (a named preset from `CLASSIQ_NOISE_MODELS`, such as `ibm_pittsburgh`) **or** `simulator_noise_spec` (custom). Set at most one of them. ## Quick start Attach a noise specification through backend preferences when you execute a program. [comment]: DO_NOT_TEST ```python theme={null} from classiq import * @qfunc def main(res: Output[QBit]) -> None: allocate(res) X(res) qprog = synthesize(main) noise_spec = ClassiqSimulatorNoiseSpecification( readout_bit_flip_probability=0.02, ) df = sample( qprog, backend="simulator", config={"simulator_noise_spec": noise_spec}, num_shots=1000, ) ``` [comment]: DO_NOT_TEST ```python theme={null} from classiq import * @qfunc def main(res: Output[QBit]) -> None: allocate(res) X(res) qprog = synthesize(main) execution_preferences = ExecutionPreferences( backend_preferences=ClassiqBackendPreferences( backend_name=ClassiqSimulatorBackendNames.SIMULATOR, simulator_noise_spec=ClassiqSimulatorNoiseSpecification( readout_bit_flip_probability=0.02, ), ), num_shots=1000, ) with ExecutionSession(qprog, execution_preferences=execution_preferences) as session: df = session.sample().dataframe ``` ## Noise channels The sections below describe each building block. Every section includes a minimal code example you can copy into `ClassiqSimulatorNoiseSpecification(...)`. ### Depolarizing noise on gates Depolarizing noise mixes the state with the maximally mixed state on the support of the gate: with probability $(4^n-1)/4^n \cdot \lambda$ a uniform Pauli error is applied (excluding identity), and otherwise the channel leaves the state unchanged, where $n$ is the number of qubits the gate acts on and $\lambda$ is the `probability` parameter. Use [`DepolarizingNoiseOnGate`](/sdk-reference/providers/simulator-noise#depolarizingnoiseongate) to apply the same depolarizing channel on **every** occurrence of a named gate. [comment]: DO_NOT_TEST ```python theme={null} from classiq import ClassiqSimulatorNoiseSpecification, DepolarizingNoiseOnGate ClassiqSimulatorNoiseSpecification( gate_depolarizing_errors=[ DepolarizingNoiseOnGate(gate="x", probability=0.001, num_qubits=1), DepolarizingNoiseOnGate(gate="cx", probability=0.01, num_qubits=2), ], ) ``` ### Local depolarizing noise on gates [`LocalDepolarizingNoiseOnGate`](/sdk-reference/providers/simulator-noise#localdepolarizingnoiseongate) applies depolarizing noise only when the named gate acts on a **specific ordered tuple** of qubit indices. Other placements of the same gate name are unaffected. [comment]: DO_NOT_TEST ```python theme={null} from classiq import ClassiqSimulatorNoiseSpecification, LocalDepolarizingNoiseOnGate ClassiqSimulatorNoiseSpecification( local_depolarizing_errors=[ LocalDepolarizingNoiseOnGate( gate="cx", num_qubits=2, probability=0.02, qubits=[0, 1], ), ], ) ``` ### Pauli noise on gates Pauli noise is a probabilistic mixture: each term is a Pauli product (written as a string of `I`, `X`, `Y`, `Z` per qubit, e.g. `X` on one qubit, `IX` or `XY` on two) with an associated probability. All term probabilities must sum to 1. Use [`PauliNoiseTerm`](/sdk-reference/providers/simulator-noise#paulinoiseterm) and [`PauliNoiseOnGate`](/sdk-reference/providers/simulator-noise#paulinoiseongate) to attach a mixed Pauli channel to every occurrence of a gate. Include an identity (`I`) term when you want a fraction of executions to stay error-free. [comment]: DO_NOT_TEST ```python theme={null} from classiq import ( ClassiqSimulatorNoiseSpecification, PauliNoiseOnGate, PauliNoiseTerm, ) ClassiqSimulatorNoiseSpecification( gate_pauli_errors=[ PauliNoiseOnGate( gate="x", num_qubits=1, pauli_terms=[ PauliNoiseTerm(pauli="I", probability=0.99), PauliNoiseTerm(pauli="X", probability=0.01), ], ), ], ) ``` ### Thermal relaxation on single-qubit gates Thermal relaxation models energy decay and dephasing over a gate duration. Use coherent time constants `t1` (amplitude damping) and `t2` (dephasing) in any consistent unit, the same unit for `time` as the gate duration, and optional `excited_state_population` for the equilibrium $|1\rangle$ population. For physical consistency, require $T_2 \leq 2 T_1$. [`ThermalRelaxationNoiseOnGate`](/sdk-reference/providers/simulator-noise#thermalrelaxationnoiseongate) is defined for a single computational qubit; attach it only to single-qubit gate names. [comment]: DO_NOT_TEST ```python theme={null} from classiq import ClassiqSimulatorNoiseSpecification, ThermalRelaxationNoiseOnGate ClassiqSimulatorNoiseSpecification( gate_thermal_relaxation_errors=[ ThermalRelaxationNoiseOnGate( gate="sx", t1=60_000.0, t2=20_000.0, time=100.0, ), ], ) ``` ### Global readout bit-flip probability The symmetric one-parameter readout model swaps classical outcomes 0 and 1 with probability `p` on **each** qubit, for both $|0\rangle$ and $|1\rangle$ pre-measurement states. Set `readout_bit_flip_probability` on [`ClassiqSimulatorNoiseSpecification`](/sdk-reference/providers/simulator-noise#classiqsimulatornoisespecification). This field is mutually exclusive with `readout_assignment_probabilities`. [comment]: DO_NOT_TEST ```python theme={null} from classiq import ClassiqSimulatorNoiseSpecification ClassiqSimulatorNoiseSpecification( readout_bit_flip_probability=0.02, ) ``` ### Global readout assignment matrix Readout noise can also be specified with a 2-by-2 **assignment matrix**: row index is the true pre-measurement computational state (0 or 1), column index is the classical outcome (0 or 1); entry $(i,j)$ is the probability of recording outcome $j$ when the qubit was in state $|i\rangle$. Each row must be non-negative and sum to 1. Set `readout_assignment_probabilities` to apply one matrix to **every** qubit's measurement. Mutually exclusive with `readout_bit_flip_probability`. [comment]: DO_NOT_TEST ```python theme={null} from classiq import ClassiqSimulatorNoiseSpecification ClassiqSimulatorNoiseSpecification( readout_assignment_probabilities=[[0.98, 0.02], [0.03, 0.97]], ) ``` ### Local readout noise [`LocalReadoutNoise`](/sdk-reference/providers/simulator-noise#localreadoutnoise) assigns a full 2-by-2 assignment matrix to a **single** simulator qubit index. Use `local_readout_errors` when different qubits need different readout confusion. [comment]: DO_NOT_TEST ```python theme={null} from classiq import ClassiqSimulatorNoiseSpecification, LocalReadoutNoise ClassiqSimulatorNoiseSpecification( local_readout_errors=[ LocalReadoutNoise( qubit=0, assignment_probabilities=[[0.9, 0.1], [0.15, 0.85]], ), ], ) ``` ### Basis gates `basis_gates` lists optional primitive gate names used when the simulator decomposes circuits before attaching noise. If omitted, the backend uses its default primitive set (commonly single-qubit rotations and one entangling gate type). [comment]: DO_NOT_TEST ```python theme={null} from classiq import ClassiqSimulatorNoiseSpecification ClassiqSimulatorNoiseSpecification( basis_gates=["id", "rz", "sx", "cx", "x"], ) ``` ## Combining channels You can mix gate and readout channels in one specification. Global readout settings (`readout_bit_flip_probability` or `readout_assignment_probabilities`) cannot both be set; per-qubit readout in `local_readout_errors` uses the same matrix convention. [comment]: DO_NOT_TEST ```python theme={null} from classiq import ( ClassiqSimulatorNoiseSpecification, DepolarizingNoiseOnGate, LocalDepolarizingNoiseOnGate, LocalReadoutNoise, PauliNoiseOnGate, PauliNoiseTerm, ThermalRelaxationNoiseOnGate, ) ClassiqSimulatorNoiseSpecification( basis_gates=["id", "rz", "sx", "cx", "x"], gate_depolarizing_errors=[ DepolarizingNoiseOnGate(gate="x", probability=0.001, num_qubits=1), ], local_depolarizing_errors=[ LocalDepolarizingNoiseOnGate( gate="cx", num_qubits=2, probability=0.02, qubits=[0, 1] ), ], gate_pauli_errors=[ PauliNoiseOnGate( gate="h", num_qubits=1, pauli_terms=[ PauliNoiseTerm(pauli="I", probability=0.99), PauliNoiseTerm(pauli="Z", probability=0.01), ], ), ], gate_thermal_relaxation_errors=[ ThermalRelaxationNoiseOnGate(gate="sx", t1=60_000.0, t2=20_000.0, time=100.0), ], readout_assignment_probabilities=[[0.98, 0.02], [0.03, 0.97]], local_readout_errors=[ LocalReadoutNoise( qubit=0, assignment_probabilities=[[0.9, 0.1], [0.15, 0.85]], ), ], ) ``` ## Validation rules The SDK validates noise specifications before execution. Common constraints: | Rule | Applies to | | -------------------------------------------------------------------------------------------- | ------------------------------------------ | | `noise_model` and `simulator_noise_spec` are mutually exclusive | `ClassiqBackendPreferences` | | `readout_bit_flip_probability` and `readout_assignment_probabilities` are mutually exclusive | `ClassiqSimulatorNoiseSpecification` | | Pauli term probabilities must sum to 1 | `PauliNoiseOnGate` | | `len(qubits)` must equal `num_qubits` | `LocalDepolarizingNoiseOnGate` | | $T_2 \leq 2 T_1$ | `ThermalRelaxationNoiseOnGate` | | Assignment matrices must be 2-by-2, non-negative, row-stochastic | `LocalReadoutNoise`, global readout fields | For full field-level reference, see [Simulator noise (SDK reference)](/sdk-reference/providers/simulator-noise). # Execution on Google Cloud Platform Source: https://docs.classiq.io/user-guide/execution/cloud-providers/google-backends Classiq offers execution on a GPU based simulator that is located in the Google Cloud Platform. This simulator doesn't require an account on GCP. ## Simulator Usage Execution on this simulator requires specific license permissions. Before first use, contact [Classiq support](mailto:support@classiq.io). ```python theme={null} from classiq import GCPBackendPreferences preferences = GCPBackendPreferences(backend_name="cuquantum") ``` Opening info tab ## Supported Backends Included simulators: * "cuquantum" * "cuquantum\_statevector" # Execution on IBM Quantum Cloud Source: https://docs.classiq.io/user-guide/execution/cloud-providers/ibm-backends ## Usage ### Execution on IBM Hardware Execution on IBM hardware requires a valid IBM Quantum Cloud API access token, and access to the requested hardware with an IBM Quantum hub, group, and project name. The access token is the API token that appears at the top of the [IBM Quantum Cloud page](https://quantum.cloud.ibm.com/), when you are logged in. You must create an account with IBM quantum if you do not have one already. IBM Backend Preferences configuration options are described in the sections above and in [IBM hardware noise simulation (emulate)](#ibm-hardware-noise-simulation-emulate). [comment]: DO_NOT_TEST ```python theme={null} from classiq import ( IBMBackendPreferences, ) preferences = IBMBackendPreferences( backend_name="Name of requsted quantum hardware", access_token="A Valid API access token to IBM Quantum", channel="IBM Cloud Channel", instance_crn="IBM Cloud Instance CRN", emulate=False, ) ``` Opening info tab ## IBM hardware noise simulation (emulate) Set `emulate=True` on [`IBMBackendPreferences`](/sdk-reference/providers/IBM) to run a simulator with a **Qiskit noise model** inferred from a **real IBM Quantum device** name (for example `ibm_pittsburgh`, `ibm_boston`). You do not pass a separate noise model; `backend_name` must be one of the IBM backends supported for this path in the SDK (see validation errors if the name is not supported). **Requirements and limitations** * Use **real** IBM hardware backend names (not `fake_*` simulators); fake backends already run locally and cannot be combined with `emulate=True`. * With `emulate=True`, execution does not use IBM Quantum hardware; it uses IBM's hardware noise model profile on Classiq's resources. * Emulation does not require IBM Quantum credentials or an IBM Quantum account. * Emulation runs on Classiq's execution environment; it does not use IBM Quantum's job queue or wait for IBM hardware availability. [comment]: DO_NOT_TEST ```python theme={null} from classiq import IBMBackendPreferences preferences = IBMBackendPreferences( backend_name="ibm_pittsburgh", access_token="…", channel="ibm_cloud", instance_crn="…", emulate=True, ) ``` ## Supported Backends Included hardware: * "ibm\_kingston" * "ibm\_boston" * "ibm\_marrakesh" * "ibm\_torino" * "ibm\_fez" * "ibm\_pittsburg" # Supported Cloud Providers Source: https://docs.classiq.io/user-guide/execution/cloud-providers/index Classiq offers integration with multiple cloud providers, to allow seamless execution of quantum programs on multiple backends. Some hardware providers may require an appropriate account. To estimate execution cost before running, see [Cost Estimation](/user-guide/execution/cost-estimation). * [Classiq](/user-guide/execution/cloud-providers/classiq-backends) * [Custom noise models (Classiq simulators)](/user-guide/execution/cloud-providers/custom-noise-models) * [Google](/user-guide/execution/cloud-providers/google-backends) * [IBM](/user-guide/execution/cloud-providers/ibm-backends) * [Intel](/user-guide/execution/cloud-providers/intel-backends) * [Amazon Braket](/user-guide/execution/cloud-providers/amazon-backends) * [IonQ](/user-guide/execution/cloud-providers/ionq-backends) * [Azure Quantum](/user-guide/execution/cloud-providers/azure-backends) * [Alice & Bob](/user-guide/execution/cloud-providers/alice-and-bob-backends) * [C12](/user-guide/execution/cloud-providers/c12) ## `emulate` by provider The SDK exposes an `emulate` flag on several backend preference types; **semantics differ by provider**: | Provider | Doc section | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | IBM Quantum | [IBM hardware noise simulation (emulate)](/user-guide/execution/cloud-providers/ibm-backends#ibm-hardware-noise-simulation-emulate) | | IonQ (direct API) | [IonQ hardware noise simulation (emulate)](/user-guide/execution/cloud-providers/ionq-backends#ionq-hardware-noise-simulation-emulate) | | Azure Quantum | [IonQ hardware noise simulation on Azure Quantum (emulate)](/user-guide/execution/cloud-providers/azure-backends#ionq-hardware-noise-simulation-on-azure-quantum-emulate) | | Amazon Braket | [Device emulation (emulate)](/user-guide/execution/cloud-providers/amazon-backends#device-emulation-emulate) | # Execution on Intel Backends Source: https://docs.classiq.io/user-guide/execution/cloud-providers/intel-backends The Classiq executor supports execution on Intel® simulators. ## Usage In Classiq's [web platform](https://platform.classiq.io/) simply choose Intel in the checkbox menu under "Execution". In Classiq's Python SDK, use the following code: [comment]: DO_NOT_TEST ```python theme={null} from classiq import IntelBackendPreferences preferences = IntelBackendPreferences( backend_name="intel_qsdk_simulator", ) ``` ## Citing Intel® Quantum SDK To cite the Intel® Quantum SDK, please reference: Khalate, P., Wu, X.-C., Premaratne, S., Hogaboam, J., Holmes, A., Schmitz, A., Guerreschi, G. G., Zou, X. & Matsuura, A. Y., arXiv:2202.11142 (2022). ## Supported Backends Included simulators: * "intel\_qsdk\_simulator" # Execution on IonQ Quantum Cloud Source: https://docs.classiq.io/user-guide/execution/cloud-providers/ionq-backends The Classiq executor supports execution on IonQ hardware and simulator. ## Usage Execution on IonQ requires a valid IonQ API key. IonQ Backend Preferences configuration options are summarized below; see [IonQ hardware noise simulation (emulate)](#ionq-hardware-noise-simulation-emulate) for the `emulate` flag. * **Error mitigation:** `bool` — valid for IonQ hardware; defaults to `False`. [comment]: DO_NOT_TEST ```python theme={null} from classiq import IonqBackendPreferences preferences = IonqBackendPreferences( backend_name="qpu.forte-1", api_key="A Valid IonQ API key", error_mitigation=True, emulate=False, ) ``` Opening info tab ## IonQ hardware noise simulation (emulate) Set `emulate=True` on [`IonqBackendPreferences`](/sdk-reference/providers/IonQ) to run on the **IonQ cloud simulator** while applying a **hardware noise profile** derived from your **QPU** backend name (for example `qpu.forte-1` → noise model `forte-1`). Classiq resolves the simulator backend and passes the appropriate run options to the IonQ provider. **Requirements** * `backend_name` must be a **QPU** id (prefix `qpu.`, e.g. `qpu.aria-2`, `qpu.forte-1`). If `emulate=True` with a non-QPU backend name, preferences validation fails. **Usage** [comment]: DO_NOT_TEST ```python theme={null} from classiq import IonqBackendPreferences preferences = IonqBackendPreferences( backend_name="qpu.forte-1", api_key="…", error_mitigation=False, emulate=True, ) ``` ## Supported Backends Included hardware: * "qpu.aria-2" * "qpu.forte-1" * "qpu.forte-enterprise-1" * "qpu.forte-enterprise-2" Included simulators: * "simulator" # Cost Estimation Source: https://docs.classiq.io/user-guide/execution/cost-estimation Before executing a quantum program, you can estimate the cost using `estimate_sample_cost` and `estimate_sample_batch_cost`. These functions return a `CostEstimateResult` with `cost` (in USD) and `currency` fields. Cost estimation is supported for all quantum providers that Classiq integrates with. Each provider uses its own pricing model. ## Basic Usage ### Single Sample Cost Use `estimate_sample_cost` to estimate the cost of sampling a quantum program with given execution preferences: [comment]: DO_NOT_TEST ```python theme={null} from classiq import ( ClassiqBackendPreferences, ClassiqSimulatorBackendNames, ExecutionPreferences, create_model, estimate_sample_cost, synthesize, ) from classiq.qmod.builtins.operations import allocate from classiq.qmod.qfunc import qfunc from classiq.qmod.qmod_variable import Output, QBit @qfunc def main(res: Output[QBit]) -> None: allocate(1, res) qmod = create_model(main) qprog = synthesize(qmod) execution_preferences = ExecutionPreferences( num_shots=100, backend_preferences=ClassiqBackendPreferences( backend_name=ClassiqSimulatorBackendNames.SIMULATOR ), ) result = estimate_sample_cost(qprog, execution_preferences) print(f"Estimated cost: {result.cost} {result.currency}") ``` ### Batch Cost Estimation Use `estimate_sample_batch_cost` to estimate the cost of sampling with multiple parameter sets: [comment]: DO_NOT_TEST ```python theme={null} from classiq import ( ClassiqBackendPreferences, ClassiqSimulatorBackendNames, estimate_sample_batch_cost, synthesize, ) qprog = synthesize(qmod) result = estimate_sample_batch_cost( qprog, execution_backend=ClassiqBackendPreferences( backend_name=ClassiqSimulatorBackendNames.SIMULATOR ), shots=100, params=[{}, {}], # Two parameter sets ) print(f"Estimated batch cost: {result.cost} {result.currency}") ``` ## Provider-Specific Usage Cost estimation works with the same backend preferences you use for execution. Configure the backend for your target provider and pass it to the estimation functions. ### Classiq Simulators Classiq simulators support cost estimation without provider credentials. [comment]: DO_NOT_TEST ```python theme={null} from classiq import ( ClassiqBackendPreferences, ClassiqSimulatorBackendNames, ExecutionPreferences, estimate_sample_cost, ) # All Classiq simulators are supported backends = [ ClassiqSimulatorBackendNames.SIMULATOR, ClassiqSimulatorBackendNames.SIMULATOR_STATEVECTOR, ClassiqSimulatorBackendNames.SIMULATOR_DENSITY_MATRIX, ClassiqSimulatorBackendNames.SIMULATOR_MATRIX_PRODUCT_STATE, ] for backend_name in backends: prefs = ExecutionPreferences( num_shots=100, backend_preferences=ClassiqBackendPreferences(backend_name=backend_name), ) result = estimate_sample_cost(qprog, prefs) print(f"{backend_name}: {result.cost} {result.currency}") ``` ### Amazon Braket Cost estimation supports both Braket simulators (SV1, TN1, dm1) and QPUs (e.g., Aspen-11, Lucy). [comment]: DO_NOT_TEST ```python theme={null} from classiq import ( AwsBackendPreferences, ExecutionPreferences, estimate_sample_cost, ) # Braket simulators prefs = ExecutionPreferences( num_shots=100, backend_preferences=AwsBackendPreferences( backend_name="SV1", run_via_classiq=True, ), ) result = estimate_sample_cost(qprog, prefs) # Braket QPU (e.g., Ankaa-3) prefs_qpu = ExecutionPreferences( num_shots=1000, backend_preferences=AwsBackendPreferences( backend_name="Ankaa-3", run_via_classiq=True, ), ) result_qpu = estimate_sample_cost(qprog, prefs_qpu) ``` When using your own AWS credentials, omit `run_via_classiq` and provide `aws_access_key_id`, `aws_secret_access_key`, `s3_bucket_name`, and `s3_folder`. ### IonQ [comment]: DO_NOT_TEST ```python theme={null} from classiq import ( IonqBackendPreferences, ExecutionPreferences, estimate_sample_cost, ) # IonQ simulator prefs = ExecutionPreferences( num_shots=100, backend_preferences=IonqBackendPreferences( backend_name="simulator", run_via_classiq=True, ), ) result = estimate_sample_cost(qprog, prefs) # IonQ QPU (e.g., Forte-1) prefs_qpu = ExecutionPreferences( num_shots=1000, backend_preferences=IonqBackendPreferences( backend_name="qpu.Forte-1", run_via_classiq=True, ), ) result_qpu = estimate_sample_cost(qprog, prefs_qpu) ``` ### Azure Quantum [comment]: DO_NOT_TEST ```python theme={null} from classiq import ( AzureBackendPreferences, ExecutionPreferences, estimate_sample_cost, ) # Azure IonQ simulator prefs = ExecutionPreferences( num_shots=100, backend_preferences=AzureBackendPreferences( backend_name="ionq.simulator", ), ) result = estimate_sample_cost(qprog, prefs) # Azure Quantinuum simulator prefs_quantinuum = ExecutionPreferences( num_shots=100, backend_preferences=AzureBackendPreferences( backend_name="quantinuum.sim.h1-1e", ), ) result_quantinuum = estimate_sample_cost(qprog, prefs_quantinuum) ``` When using `run_via_classiq`, omit `credentials`. Otherwise, provide Azure credentials. ## Summary by Provider | Provider | Backend examples | | ----------------- | ------------------------------------------------------------------------------------------------ | | **Classiq** | simulator, simulator\_statevector, simulator\_density\_matrix, simulator\_matrix\_product\_state | | **Amazon Braket** | SV1, TN1, dm1 (simulators); Aspen-11, Lucy (QPUs) | | **IonQ** | simulator, qpu.aria-1, qpu.forte-1 | | **Azure** | ionq.simulator, quantinuum.sim.h1-1e, rigetti.sim.qvm | Use `run_via_classiq=True` when available to estimate costs without providing provider credentials. Your allocated budget is used for execution. ## API Reference See the [execution SDK reference](/sdk-reference/execution) for full details on `estimate_sample_cost`, `estimate_sample_batch_cost`, and `CostEstimateResult`. # CUDA-Q Integration Source: https://docs.classiq.io/user-guide/execution/cudaq_integration ## Overview The Classiq–CUDA-Q integration enables users to design quantum algorithms at a high level using Classiq, while leveraging CUDA-Q for execution, simulation, and hybrid quantum–classical workflows. This integration is particularly well suited for variational algorithms, such as QAOA, where circuit synthesis, parameterized execution, and classical optimization must work together efficiently. ## Prerequisites Before using the integration, ensure the following are installed in your python environment: * Python 3.9 or later * Classiq SDK * CUDA-Q Note: The integration is only supported on Linux. The simplest way to handle installations is to use [Classiq Studio](https://platform.classiq.io/studio/) and `pip install classiq[cudaq]`. ## Integration Core Functions The Integration is built around two Classiq SDK helper functions: * `qprog_to_cudaq_kernel()`\ Converts a synthesized Classiq quantum program (`qprog`) into a CUDA-Q kernel that can be executed using CUDA-Q SDK. The default kernel type is a main kernel, which is compatible with CUDA-Q execution functions and key capabilities. In case that the kernel is meant to be added as a component to another main kernel, use `is_main_kernel=False` so that it can be passed as a parameter to `apply_call` (for example: `other_kernel.apply_call(your_kernel)`). * `pauli_operator_to_cudaq_spin_op()`\ Transforms Qmod's `SparsePauliOp` data structure to CUDA-Q's `SpinOperator`. ## Usage Example Here is a minimal example: * Defining and synthesizing a parametric model using Classiq. * Converting to CUDA-Q kernel, assigning value to the parameter and sampling. [comment]: DO_NOT_TEST ```python theme={null} # ! pip install classiq[cudaq] from classiq import * import cudaq import math @qfunc def main(theta: CReal, q: Output[QBit]): allocate(q) RX(theta, q) qprog = synthesize(main) my_kernel = qprog_to_cudaq_kernel(qprog) counts = cudaq.sample(my_kernel, math.pi / 3) print(counts) # example outputs - # { 0:753 1:247 } ``` # Execution Source: https://docs.classiq.io/user-guide/execution/index Classiq lets you execute quantum programs on a variety of backends, including simulators and real quantum hardware. Execution operates on synthesized `QuantumProgram` (see [Quantum Program Synthesis](/user-guide/synthesis/index)) and, for sampling, also OpenQASM 2.0 or 3.0. Once you have defined the format of input for execution, you can run it using a set of high-level APIs designed to be consistent and easy to use. ## Quick workflow Execution typically consists of four steps: 1. Choose the execution function based on the desired result type: * `sample(...)` for shot-based measurement results * `calculate_state_vector(...)` for amplitudes and basis-state information * `observe(...)` for expectation values 2. Choose a backend Use `get_backend_details()` to inspect the available providers and devices. Not all result types are available on all backends. For example, state vector results cannot be obtained from QPUs. 3. Pass execution settings in one place Use `config=` for provider-specific configuration, together with common execution arguments such as `num_shots`, `random_seed`, and `transpilation_option`. 4. Inspect the returned result * `sample(...)` returns a DataFrame * `calculate_state_vector(...)` returns a DataFrame * `observe(...)` returns a scalar * If `parameters` is a list, the returned value is a list of results of the corresponding type ## Step 1 — Choose the type of result you want Before running your program, it is important to decide what kind of information you want to extract. There are three common execution functions: | Function | When to use | Output | | ------------------------ | ------------------------------- | --------- | | `sample` | You want measurement statistics | DataFrame | | `calculate_state_vector` | You want amplitudes and phases | DataFrame | | `observe` | You want an expectation value | float | Each function exposes a different level of information about your quantum program: * Use [sample](./sample) when you want to see the outcomes that would be obtained by measuring the circuit many times. * Use [calculate\_state\_vector](./calculate-state-vector) when you want the full quantum state, including amplitudes and phases. * Use [observe](./observe) when you want to evaluate an observable and obtain a single expectation value. For a detailed walkthrough of each function, continue to the relevant page: [Sampling](./sample) [State vectors](./calculate-state-vector) [Expectation values](./observe) ## Step 2 — Inspect available backends Before executing, you should inspect which backends are available. ```python theme={null} from classiq import * backends = get_backend_details() backends ``` This returns a table containing: * provider (e.g., classiq, azure, braket) * backend (device name) * type (simulator / hardware) * num\_qubits * is\_available * queue\_time ## Provider-specific configuration All execution functions ([sample](./sample), [observe](./observe), and [calculate\_state\_vector](./calculate-state-vector)) accept a config argument that allows you to pass provider-specific configuration. This is the main mechanism for customizing how your program is executed on a given backend. ### When to use config Use config when you need to: * Provide authentication details (e.g., API keys or credentials) * Configure execution settings specific to a provider * Enable features such as noise models on simulators * Control advanced backend behavior that is not exposed through the common execution arguments ### Basic usage The config argument can be passed as either: * A plain Python dictionary, or * a provider-specific configuration object (for example, IBMConfig, BraketConfig, etc.) In the following examples, we enable `emulate` on an Azure backend to enable IonQ hardware noise simulation in two different ways: First, using config as a dict, and then using config as a provider-specific object. **Example: Using config as a dict** [comment]: DO_NOT_TEST ```python theme={null} from classiq import * backend_name = "azure/ionq.simulator" azure_config = {"emulate": True} @qfunc def main(x: Output[QBit]): allocate(x) H(x) qprog = synthesize(main) res = sample(qprog, backend=backend_name, num_shots=1000, config = azure_config, run_via_classiq= True) ``` **Example: Using config as a provider-specific object** [comment]: DO_NOT_TEST ```python theme={null} from classiq import * backend_name = "azure/ionq.simulator" azure_preferences = AzureBackendPreferences( emulate = True ) @qfunc def main(x: Output[QBit]): allocate(x) H(x) qprog = synthesize(main) res = sample(qprog, backend=backend_name, num_shots=1000, config = azure_preferences, run_via_classiq= True) ``` ## Key takeaways * Execution always follows the same pattern: 1. Choose result type 2. Choose backend 3. Configure in one place 4. Analyze result * Data is returned in ready-to-use structures * DataFrames for sampling and state vector * Scalars for observables * Batch execution is automatic when passing a list of parameters # Observe Source: https://docs.classiq.io/user-guide/execution/observe The `observe(...)` workflow is used when you want the expectation value of an observable rather than a full measurement distribution or the complete state vector. Related pages: * [Execution overview](./index) * [Sampling](./sample) * [State vectors](./calculate-state-vector) ## When to use observe Use `observe(...)` when your goal is to compute the expectation value of an observable with respect to the executed quantum state. **This is especially useful in workflows such as:** * variational algorithms * optimization loops * cost-function evaluation * repeated evaluation of the same circuit under different parameter values Unlike sampling, which returns a distribution of measurement outcomes, `observe(...)` returns a single scalar. Unlike state-vector calculation, it does not expose the full quantum state. ## Basic example ```python theme={null} from classiq import * backend_name = "simulator" @qfunc def main(res: Output[QBit]) -> None: allocate(res) H(res) qprog = synthesize(main) #Define your observable using SparsePauliOp my_observable = Pauli.Z(0) - Pauli.Y(0) value = observe( qprog, observable=my_observable, backend=backend_name, num_shots=1000, ) value ``` **Output:** -0.013999999999999999 ### Understanding the result The returned value is a single scalar. It represents the expectation value of the observable with respect to the state produced by the circuit. In other words, it summarizes the behavior of the circuit relative to the operator you are interested in. This is useful when you care about one derived quantity rather than the full result space. ### Choosing execution settings As with the other execution functions, you can configure the execution using standard arguments such as: * backend * config * num\_shots * random\_seed * parameters For example: [comment]: DO_NOT_TEST ```python theme={null} value = observe( qprog, observable=my_observable, backend=backend_name, config={}, num_shots=2000, random_seed=42, ) ``` ## Parameterized execution Many practical uses of `observe(...)` involve parameterized circuits. Instead of fixing all values inside the circuit, you define symbolic parameters in the quantum function and provide their values when you execute the program. ### Defining a parameterized quantum program ```python theme={null} from classiq import * backend_name = "simulator" @qfunc def main(angle_rx: CReal, angle_ry: CReal, x: Output[QBit]): allocate(x) RX(angle_rx, x) H(x) RY(angle_ry, x) qprog = synthesize(main) ``` ### Supplying parameter values [comment]: DO_NOT_TEST ```python theme={null} exec_params = { "angle_rx": 0.5, "angle_ry": 0.3 } value = observe( qprog, observable=my_observable, backend=backend_name, parameters=exec_params, num_shots=1000, ) value ``` **Output:** -0.748 This allows you to evaluate how the expectation value changes as the circuit parameters change. # Batch of expectation values A common pattern is to evaluate the same observable for many parameter settings. Instead of calling `observe(...)` repeatedly, you can pass a list of parameter dictionaries. The function then returns a list of scalar values, one per parameter set. **Example** [comment]: DO_NOT_TEST ```python theme={null} rx_angles = [0.1, 0.2, 0.3, 0.4] exec_params = [ {"angle_rx": angle, "angle_ry": 0.3} for angle in rx_angles ] values = observe( qprog, observable=my_observable, backend=backend_name, parameters=exec_params, num_shots=1000, ) values ``` **Output:** \[-0.396, -0.526, -0.6100000000000001, -0.642] ### Understanding batch results When `parameters` is a list: * the output is a list of scalar values * each value corresponds to one execution * the order matches the order of the provided parameter dictionaries This makes `observe(...)` a natural fit for optimization and sweep-style workflows, where you want to evaluate the same quantity many times. ## Summary Use `observe(...)` when you care about the expectation value of an observable. Observe: * returns a scalar * is ideal for cost functions and iterative algorithms * supports both single and batch parameterized execution # Sample Source: https://docs.classiq.io/user-guide/execution/sample Sampling is the most common execution workflow. It is used when you want to simulate or run measurements and inspect the resulting bitstring distribution. Related pages: * [Execution overview](./index) * [Observe](./observe) * [State vectors](./calculate-state-vector) ## When to use sampling Use `sample(...)` when you want to perform measurements. This method is typically used to understand the probability distribution over measurement outcomes. **Sampling is a good choice when:** * you want to inspect measured bitstrings * you want counts and probabilities * you want to compare outputs across different parameter values * you want behavior that resembles repeated measurements on a quantum device Unlike state-vector calculation, sampling does not return the full internal quantum state. Instead, it returns the measurement statistics that result from executing the circuit multiple times. The `sample` function accepts these arguments: | Argument | Description | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `qprog` | A synthesized `QuantumProgram`, or a string containing **OpenQASM 2.0 or 3.0** source (see [Sampling OpenQASM](#sampling-openqasm)). | | `backend` | Backend specifier as `"provider/backend"`. Defaults to `"simulator"`. | | `parameters` | A dict of parameter values, or a list of dicts for batch execution. Keys are parameter names of the main function. **Not supported** when `qprog` is an OpenQASM string. | | `config` | Provider-specific configuration (API keys, etc.). Accepts a `dict` or a typed config object (e.g. `IBMConfig`, `BraketConfig`). | | `num_shots` | Number of shots. Must be ≥ 1 if specified. | | `random_seed` | Seed for transpilation and simulation. | | `transpilation_option` | Transpilation level. See [`TranspilationOption`](/user-guide/synthesis/quantum-program-transpilation#transpilation-options). | | `run_via_classiq` | Run using Classiq's provider credentials against your allocated budget. Defaults to `False`. | ## Basic examples ```python theme={null} from classiq import * backend_name = "simulator" @qfunc def main(res: Output[QBit]) -> None: allocate(res) H(res) qprog = synthesize(main) df = sample( qprog, backend=backend_name, num_shots=1000, ) df ``` | x | counts | probability | bitstring | | - | ------ | ----------- | --------- | | 1 | 502 | 0.502 | 1 | | 0 | 498 | 0.498 | 0 | [comment]: DO_NOT_TEST ```python theme={null} from classiq import * backend_name = "ibm/fez" @qfunc def main(x: Output[QNum[2, UNSIGNED, 0]], y:Output[QArray[QBit]], res: Output[QBit]) -> None: allocate(3, y) allocate(res) prepare_state([0.5, 0, 0, 0.5], 0.0, x) H(y[1]) H(res) qprog = synthesize(main) df = sample( qprog, backend=backend_name, num_shots=1000, run_via_classiq=True, ) df ``` | x | y | res | counts | probability | bitstring | | - | ---------- | --- | ------ | ----------- | --------- | | 0 | \[0, 0, 0] | 1 | 136 | 0.136 | 100000 | | 3 | \[0, 0, 0] | 0 | 132 | 0.132 | 000011 | | 0 | \[0, 0, 0] | 0 | 126 | 0.126 | 000000 | | 0 | \[0, 1, 0] | 1 | 126 | 0.126 | 101000 | | 3 | \[0, 1, 0] | 1 | 124 | 0.124 | 101011 | | 3 | \[0, 1, 0] | 0 | 120 | 0.120 | 001011 | | 0 | \[0, 1, 0] | 0 | 119 | 0.119 | 001000 | | 3 | \[0, 0, 0] | 1 | 117 | 0.117 | 100011 | ### Understanding the result The returned object is a DataFrame, which makes it easy to analyze using familiar tools. **Common columns are:** * **quantum variables** - the quantum number values, quantum arrays, and qubits values measured * **bitstring** — the measured computational basis state * **counts** — how many times this result was observed * **probability** — normalized frequency ## Choosing execution settings Sampling accepts the same common execution settings used by other high-level execution APIs. For example: [comment]: DO_NOT_TEST ```python theme={null} df = sample( qprog, backend=backend_name, config={}, num_shots=2000, random_seed=42, ) ``` **A few important arguments are:** * `backend` — the backend on which the program will run * `config` — provider-specific configuration * `num_shots` — how many times to execute the circuit * `random_seed` — useful for reproducibility * `parameters` — values for parameterized quantum programs In most introductory workflows, using the default simulator with a chosen number of shots is enough. ## Parameterized Execution Many quantum programs are parameterized. Instead of hardcoding values directly into the circuit, you define them as inputs to your quantum function and provide their values at execution time. **This is useful for:** * variational algorithms * parameter sweeps * repeated evaluation of the same circuit structure with different values Execution of parameterized OpenQASM is not supported. ### Defining a parameterized quantum program ```python theme={null} from classiq import * backend_name = "simulator" @qfunc def main(angle_rx: CReal, angle_ry: CReal, x: Output[QBit]): allocate(x) RX(angle_rx, x) H(x) RY(angle_ry, x) qprog = synthesize(main) ``` **In this example:** * `angle_rx` and `angle_ry` are symbolic parameters * the structure of the quantum program is fixed * the actual numerical values are supplied during execution ### Supplying parameter values To execute the parameterized program, pass a dictionary to `parameters`: [comment]: DO_NOT_TEST ```python theme={null} exec_params = { "angle_rx": 0.5, "angle_ry": 0.3 } df = sample( qprog, backend=backend_name, parameters=exec_params, num_shots=1000, ) df ``` Each key in the dictionary must match a parameter name in the quantum function. **Example output:** | x | counts | probability | bitstring | | - | ------ | ----------- | --------- | | 1 | 642 | 0.642 | 1 | | 0 | 358 | 0.358 | 0 | This means the parameter values are first bound to the quantum program, and the resulting instantiated circuit is then sampled. ## Batch sampling Often, you want to evaluate the same circuit for multiple parameter values. Instead of calling `sample(...)` repeatedly in a loop, you can pass a list of parameter dictionaries. In that case, the function performs one execution per parameter set and returns a list of DataFrames. ### Example [comment]: DO_NOT_TEST ```python theme={null} rx_angles = [0.1, 0.2, 0.3, 0.4] exec_params = [ {"angle_rx": angle, "angle_ry": 0.3} for angle in rx_angles ] results = sample( qprog, backend=backend_name, parameters=exec_params, num_shots=1000, ) results[1] ``` | x | counts | probability | bitstring | | - | ------ | ----------- | --------- | | 1 | 639 | 0.639 | 1 | | 0 | 361 | 0.361 | 0 | ### Understanding batch results **When parameters is a list:** * the output is a list of DataFrames * each DataFrame corresponds to one execution * the order of the outputs matches the order of the parameter dictionaries you passed in This is convenient when scanning over parameters and comparing how the sampled distribution changes. ## Sampling OpenQASM You can pass **OpenQASM 2.0 or 3.0** text as the first argument to `sample` instead of a `QuantumProgram`. The backend runs the circuit the same way as for a synthesized program; the returned DataFrame still includes `bitstring`, `counts`, and related columns. OpenQASM from anytool is supported as long as you pass a string, for example: [comment]: DO_NOT_TEST ```python theme={null} from qiskit import QuantumCircuit, qasm2 from classiq.execution import sample qc = QuantumCircuit(1) qc.h(0) openqasm = qasm2.dumps(qc) df = sample(openqasm, backend="simulator", num_shots=500) ``` Hand-written OpenQASM 3 is also valid: [comment]: DO_NOT_TEST ```python theme={null} from classiq.execution import sample openqasm = """ OPENQASM 3; include "stdgates.inc"; qubit[1] q; h q[0]; """ df = sample(openqasm, backend="simulator", num_shots=500) ``` Limitations when using a string: * Do not pass `parameters` for OpenQASM strings; use a `QuantumProgram` if you need Qmod `main` parameters, or express parameters inside the QASM (e.g. Qiskit `Parameter` and `assign_parameters` before dumping). * Batch sampling with a list of parameter dicts is only defined for `QuantumProgram` execution. ## Summary Use sampling when your goal is to understand measurement outcomes rather than the full state or a single expectation value. **Sampling:** * returns a DataFrame * is well suited for measured distributions * supports both single and batch parameterized execution # State Vector Filtering Source: https://docs.classiq.io/user-guide/execution/state-vector-filtering Before measurement, a quantum circuit creates a state which can be described as a vector of $2^n$ amplitudes, where $n$ is the number of qubits. Though this state vector cannot be directly accessed on a quantum computer (as measurement destroys the state), certain quantum simulators make this information available. Classiq supports two simulators which return the full statevector, both under the Classiq provider: `simulator_statevector` and `nvidia_simulator_statevector`. Since the data size grows exponentially, large circuits cannot be simulated. However, in certain applications such as those that use block encoding, not all of the $2^n$ amplitudes are of interest. For example, some methods of post-processing discard certain results wholesale, and thus the amplitudes corresponding to those measured states are irrelevant. In these instances, filtering out the amplitudes that are not of interest can greatly save memory. Use the method `ExecutionSession.set_measured_state_filter`, to specify the execution output values of interest. ## Example ```python theme={null} from classiq import * @qfunc def main(x: Output[QBit], y: Output[QNum], z: Output[QNum]) -> None: allocate(1, x) hadamard_transform(x) prepare_state(probabilities=[0.5, 0, 0.25, 0.25], bound=0.01, out=y) z |= y + 1 quantum_program = synthesize(main) execution_preferences = ExecutionPreferences( backend_preferences=ClassiqBackendPreferences( backend_name=ClassiqSimulatorBackendNames.SIMULATOR_STATEVECTOR ) ) with ExecutionSession( quantum_program, execution_preferences=execution_preferences ) as session: session.set_measured_state_filter("x", lambda state: state == 1) session.set_measured_state_filter("y", lambda state: state == 2) results = session.sample() ``` Filtering ensures that `results` will contain only the amplitudes that correspond to states where x is 1 and y is 2. ## Amplitude Threshold By default, state vector simulation filters out states with exactly zero amplitude from the result. You can tighten this filter by setting `amplitude_threshold` in `ExecutionPreferences` to exclude states whose amplitude magnitude is below a given threshold: ```python theme={null} from classiq import * @qfunc def main(x: Output[QNum]) -> None: prepare_state(probabilities=[0.5, 0, 0.25, 0.25], bound=0.01, out=x) quantum_program = synthesize(main) execution_preferences = ExecutionPreferences( backend_preferences=ClassiqBackendPreferences( backend_name=ClassiqSimulatorBackendNames.SIMULATOR_STATEVECTOR ), amplitude_threshold=1e-4, ) with ExecutionSession( quantum_program, execution_preferences=execution_preferences ) as session: results = session.sample() ``` States with `|amplitude| <= amplitude_threshold` are excluded from `results`. This reduces the size of the result for circuits where most amplitudes are negligibly small (e.g. block-encoded circuits). Setting `include_zero_amplitude_outputs=True` overrides `amplitude_threshold` and includes all states regardless of amplitude. Filtering removes states from the returned state vector. As a result, the remaining amplitudes will not sum to a norm of 1. Do not rely on the filtered state vector being normalized. ## Limitations Currently, filtering is only available on Classiq's `simulator_statevector` (`ClassiqSimulatorBackendNames.SIMULATOR_STATEVECTOR`) and `nvidia_simulator_statevector` (`ClassiqNvidiaBackendNames.SIMULATOR_STATEVECTOR`). Filtering is only available for quantum scalars (`QuantumBit`s and `QuantumNumeric`s). Additionally, only a single value per variable is supported. For example, `session.set_measured_state_filter("x", lambda x: x < 5)` is not allowed. # User Guide Source: https://docs.classiq.io/user-guide/index Welcome to the User Guide! This section guides you through all the key features and functionalities of the platform. # Controlled Operations Source: https://docs.classiq.io/user-guide/modeling/controlled-operations ## Introduction Controlled operations are one of the main ways quantum programs apply operations depending on the state of quantum variables. In classical programming, an `if` statement chooses which instructions run based on a classical Boolean value. In quantum programming, the analogous idea is more subtle: the condition may itself be quantum, meaning it may be in superposition. In Qmod, controlled quantum behavior is expressed using the `control` statement. The statement applies a unitary operation conditionally, depending on a quantum state, and may optionally apply a different operation if the condition does not hold. The condition can be given either as a single qubit or qubit array, or as a quantum Boolean expression over quantum variables. When the control variable is in superposition, the controlled operation entangles the objects used in the controlled block with the condition. In this guide, we cover: * **Controlled operations**: applying quantum operations only on states that satisfy a quantum condition. * **Else blocks**: applying one operation when the condition is true and another when it is false. * **Operations controlled by expressions**: using Boolean expressions over quantum variables as control conditions. ## Controlled operations A controlled operation applies a quantum operation subject to a condition satisfied by the quantum state. The simplest case is control by a single qubit. If the control qubit is in state $\vert 1 \rangle$, the operation is applied to the target. If the control qubit is in state $\vert 0 \rangle$, the operation is not applied. In Qmod, this is written with `control`: ```python theme={null} from classiq import * @qfunc def main(ctrl: Output[QBit], target: Output[QBit]): allocate(ctrl) allocate(target) H(ctrl) control(ctrl, lambda: Y(target)) ``` In the Python SDK, the controlled statement block is passed as a Python callable, commonly written using `lambda`. This quantum program: * Allocates a control qubit `ctrl` and a target qubit `target`. * Applies the Hadamard gate, $H$, to `ctrl`, creating the superposition $\vert \text{ctrl} \rangle = \frac{1}{\sqrt{2}}\left( \vert 0 \rangle + \vert 1 \rangle\right)$. * Applies the Pauli-Y gate to `target` conditioned on `ctrl == 1`. * The resulting state is $\frac{1}{\sqrt{2}}\left( \vert 00 \rangle + i\vert 11 \rangle\right),$ where the first qubit is `ctrl` and the second is `target`. This means that the target is flipped only in the part of the state where the control qubit is 1. Since the control qubit is in superposition, the result is an entangled state rather than a classical branch selection. This is the same control mechanism underlying gates such as CY, but Qmod generalizes it to arbitrary statement blocks and quantum expressions. The `control` statement applies quantum operations coherently. It should not be interpreted as measuring the condition and then choosing a branch. ### Multi-qubit control The control variable can also be a quantum array. In that case, the controlled block is applied only when all qubits in the control array are in state $\vert 1 \rangle$. ```python theme={null} from classiq import * from classiq.qmod.symbolic import pi @qfunc def main(ctrl: Output[QArray[QBit]], target: Output[QArray[QBit]]): allocate(3, ctrl) allocate(4, target) hadamard_transform(ctrl) control(ctrl, lambda: qft(target)) ``` This quantum program: * Allocates a 3-qubit control array. * Puts the control array into a uniform superposition over all 3-bit strings. * Applies a Quantum Fourier Transform to `target` only on the basis state `ctrl == 111`. Because the control array is in superposition, the QFT is applied only to the component corresponding to `ctrl == 111`. ## Else blocks A control statement may include an `else_block`. The main block is applied for states where the condition is true, and the else block is applied for states where the condition is false. When the condition is a qubit array, the condition holds only when all qubits in the control array are in state $\vert 1 \rangle$; otherwise, the else block applies. ```python theme={null} from classiq import * @qfunc def main(ctrl: Output[QBit], target: Output[QBit]): allocate(ctrl) allocate(target) H(ctrl) control( ctrl, lambda: X(target), lambda: H(target), ) ``` This quantum program: * Allocates `ctrl` and `target`. * Puts `ctrl` in superposition. * Applies the Pauli-X gate to `target` on the branch where `ctrl == 1`. * Applies Hadamard gate (H) to `target` on the branch where `ctrl == 0`. This is analogous to the following classical structure: [comment]: DO_NOT_TEST ```python theme={null} if ctrl: X(target) else: H(target) ``` However, the quantum version is not a classical branching. If `ctrl` is in superposition, both branches are applied coherently to the corresponding parts of the quantum state. ### Example: choosing between two rotations The next example uses an else block to apply one rotation when a control qubit is 1 and a different rotation when it is 0. ```python theme={null} from classiq import * from classiq.qmod.symbolic import pi @qfunc def main(ctrl: Output[QBit], target: Output[QBit]): allocate(ctrl) allocate(target) H(ctrl) control( ctrl, lambda: RX(pi / 2, target), lambda: RX(pi / 3, target), ) ``` Whose outputs are closer to: | ctrl | target | counts | probability | bitstring | | ---- | ------ | ------ | ----------- | --------- | | 1 | 0 | 749 | 0.245117 | 01 | | 0 | 0 | 749 | 0.365723 | 00 | | 0 | 1 | 502 | 0.128906 | 10 | | 1 | 1 | 533 | 0.260254 | 11 | This program creates a state in which the target qubit is rotated differently depending on the value of `ctrl`. Since `ctrl` is in superposition, the two rotations are applied to different components of the full quantum state. A `control` statement, like other control-flow statements such as `if_`, `repeat`, `foreach`, and `power`, must preserve the initialization status of variables declared outside its blocks. A variable declared outside the controlled block may be allocated inside the block only if it is also released within that same block. Similarly, variables declared inside a controlled block must be uninitialized by the end of that block. ## Operations controlled by expressions In Qmod, a control condition may also be a quantum logical expression over quantum variables. Expressions over quantum variables evaluate coherently over superpositions, producing correlated quantum values rather than a single classical value. Qmod supports arithmetic operators such as +, -, \*, \*\*, relational operators such as `==`, `!=`, `<`, `<=`, `>`, `>=`, and logical operators such as `logical_and`/`&`, `logical_or`/`|`, and `logical_not`/ `~` for Boolean expressions. ### Example: Uniformly controlled rotations The following example applies a sequence of multi-controlled rotations ```python theme={null} from classiq import * from classiq.qmod.symbolic import pi num_qubits = 2 max_iterations = 2**num_qubits min_angle = pi/max_iterations @qfunc def main(x: Output[QNum], target: Output[QBit]): allocate(num_qubits, x) allocate(target) hadamard_transform(x) repeat(max_iterations, lambda i: control( x == i, lambda: RX(min_angle * i, target))) ``` This quantum program: * Allocates `x` as a 2-qubit quantum number. * Places `x` in a superposition over the values $\{0, 1, 2, 3\}$. * Applies a RX rotation with angle dependent on the numeric value of x. This can be interpreted as: | x value | Operation on target | | ------- | ------------------- | | 0 | No rotation | | 1 | $RX(\pi / 4)$ | | 2 | $RX(\pi / 2)$ | | 3 | $RX(3\pi / 4)$ | The target qubit becomes entangled with `x`, because only different rotations occur depending on the values of `x`. ### Example: Arithmetic condition Control expressions can involve arithmetic over multiple quantum variables. This is useful in search, optimization, and oracle construction, where a state should be marked or modified only if it satisfies a predicate. ```python theme={null} from classiq import * from classiq.qmod.symbolic import pi @qfunc def main(x: Output[QNum[2]], y: Output[QNum[2]], target: Output[QBit]): allocate(x) allocate(y) allocate(target) hadamard_transform(x) hadamard_transform(y) control(x + y >= 5, lambda: X(target)) ``` This program: * Allocates two 2-qubit quantum numbers, `x` and `y`. * Places both in uniform superposition. * Flips `target` only for basis states satisfying `x + y >= 5` The satisfying assignments are: | x | y | | - | - | | 2 | 3 | | 3 | 2 | | 3 | 3 | After the controlled operation, the target qubit is entangled with the predicate `x + y >= 5`. If `target` is later measured as 1, the variables `x` and `y` collapse to the subspace of satisfying assignments. This style is useful for expressing predicates directly in terms of problem variables, without manually decomposing the condition into lower-level gates. ### Example: Controlled phase A common use of controlled operations is to apply a phase only to selected states. This is central in algorithms such as Grover search, phase oracles, QAOA, and Hamiltonian-inspired constructions. The `phase` statement applies a fixed or state-dependent phase shift to the quantum state. For example, phase(x**2, pi / 4) applies a phase proportional to the value of x**2. When `x` is initialized over the values 0, 1, 2, and 3, the phase statement rotates each basis state according to the expression value. A controlled phase applies such a phase only under a control condition. ```python theme={null} from classiq import * from classiq.qmod.symbolic import pi @qfunc def main(q: Output[QArray[QBit, 2]]): allocate(q) hadamard_transform(q) control(q[0], lambda: phase(pi / 4)) ``` This quantum program: * Allocates two qubits. * Places them in a uniform superposition. * Applies a fixed phase of $\pi/4$ only when `q[0] == 1`. The affected states are those whose first qubit is 1: | q state | Relative phase | | ------- | ------------------ | | \[0,0] | unchanged | | \[0,1] | unchanged | | \[1,0] | rotated by $\pi/4$ | | \[1,1] | rotated by $\pi/4$ | It is possible to apply `phase` under both single-qubit and multi-qubit controls; the resulting phases accumulate on states satisfying the respective control conditions. ### Example: Phase oracle from an expression A controlled phase can be used to mark states satisfying a Boolean expression. The following example treats `v` as a selection register over the list `S`. Each basis state of `v` represents a subset of the elements of `S`: if `v[i] == 1`, then `S[i]` is selected. The controlled phase marks states that select exactly three elements whose sum is at most 16. ```python theme={null} from classiq import * from classiq.qmod.symbolic import pi S = [1, 3, 5, 8, 11] @qfunc def main(v: Output[QArray]): allocate(5, v) hadamard_transform(v) selected_count = sum(v[i] for i in range(v.len)) selected_sum = sum(S[i] * v[i] for i in range(v.len)) condition = (selected_count == 3) & (selected_sum <= 16) control(condition, lambda: phase(pi)) ``` This program: * Prepares `v` in uniform superposition. * Applies a relative phase of $\pi$ to states representing three selected elements of `S` whose sum is at most 16. * Leaves all other states unchanged. Since a phase of $\pi$ corresponds to multiplying the selected amplitudes by -1, this implements a phase oracle for the satisfying assignments. This pattern is common in Grover-style algorithms: rather than writing a result into a separate flag qubit, the oracle marks satisfying states by changing their phase. ## The `control` statement and runtime `if_` Qmod provides two distinct mechanisms for conditional execution: the `control` statement for quantum conditions, and the `if_` statement for classical conditions. | Construct | Condition type | Evaluated | Effect | | --------- | -------------- | ----------------------- | ------------------------------------------------------------------------------ | | `control` | Quantum | Runtime | Applies a unitary coherently, conditioned on a quantum state | | `if_` | Classical | Compile time or runtime | Conditionally executes a branch based on a symbolic or runtime classical value | Use `control` when the condition depends on quantum variables such as qubits, quantum arrays, or expressions over quantum numbers. Use `if_` when the condition is a classical Qmod expression — whether symbolic or a runtime value such as a mid-circuit measurement result. Use a Python `if` when the condition is a concrete Python value known at model construction time, including Python-type parameters in [generative functions](/qmod-reference/language-reference/generative-descriptions). ## See also [Control statement](/qmod-reference/language-reference/statements/control/) [Phase statement](/qmod-reference/language-reference/statements/phase/) [Expressions](/qmod-reference/language-reference/expressions) [Within-apply](/qmod-reference/language-reference/statements/within-apply) [Quantum Numbers and Arithmetics](/user-guide/modeling/quantum-numbers-arithmetics) # Modeling Source: https://docs.classiq.io/user-guide/modeling/index This under construction section is where you can learn the concepts and syntax needed to design quantum models—simply put, to write quantum code using Classiq. If you prefer a faster, exercise-oriented path to learning, see the [Classiq Tutorial](/getting-started/classiq_tutorial/index). For a complete, formal specification of the language, refer to the [language reference](/qmod-reference/language-reference/index). # Measurements, Observables, and Hermitian Operators Source: https://docs.classiq.io/user-guide/modeling/observables-and-operators ## Introduction In quantum computing, measurement defines how information is extracted from a quantum program and made available for classical processing. Depending on the algorithm and execution mode, this information may take the form of sampled quantum states, interpreted output values, or expectation values of observables. These different notions of measurement serve different purposes and are applied through explicit measurement operations within the execution workflow. In many algorithms - especially variational ones - the relevant result is not an individual sample but an expectation value of an observable, for example a Hamiltonian expressed as a linear combination of Pauli strings. This stems from the realization that many quantum algorithms do not optimize over samples, but over classical quantities derived from quantum states, such as energies or cost functions. In quantum algorithms, measurement is therefore more than a final readout step: it defines the classical quantity that is optimized, compared, or post-processed. In Classiq, this includes sampling-based measurements through execution functions and methods, which return counts of bitstrings or parsed values, as well as expectation-value estimation workflows, which repeatedly evaluate an expectation value such as $\langle H \rangle$ for a given observable $H$ using the same synthesized program under different parameters. In addition, Qmod supports mid-circuit measurement, which collapses a qubit and returns its classical value, enabling limited run-time control flow within a quantum program when needed. ## Measurements in Qmod In this guide, we cover the concepts: * **Measurement**: extracting classical information from qubits. * **Observables**: Hermitian operators whose expectation values are estimated. * **Quantum operators**: unitaries applied to evolve the quantum state. Quantum variables declared as `Output[...]` in `main` specify what is measured in a quantum program. When the program is executed using sampling-based execution methods, these output variables are measured in the computational ($Z$) basis on every shot. The results can be accessed as raw bitstring samples and their counts or as parsed values, where the measured bits are interpreted according to the declared [quantum variable types](/user-guide/modeling/quantum-numbers-arithmetics). Only variables declared as outputs of `main` are reported as execution results. In many algorithms, the goal is not to analyze individual measurement outcomes but to compute an expectation value of an observable, $\langle H \rangle$, commonly expressed as a linear combination of Pauli strings. In Classiq, expectation values are computed using `estimate`. Rather than returning samples over the computational basis, this function evaluates the expectation value of a specified observable with respect to the quantum state prepared by the program. This is the primary measurement mode in variational and optimization-based algorithms. The following example illustrates sampling-based measurement in Qmod: ```python theme={null} from classiq import * from classiq.qmod.symbolic import pi @qfunc def prepare_pair(angle: CReal, a: QBit, b: QBit): RY(angle, a) CX(a, b) @qfunc def main(a: Output[QBit], b: Output[QBit]): allocate(a) allocate(b) prepare_pair(pi / 2, a, b) ``` This quantum program: 1. Allocate two different qubits, `a` and `b`. 2. Prepare the following state by applying a `prepare_pair`: $\vert a, b \rangle = \cos(\theta)\vert 0, 0\rangle + \sin(\theta/2) \vert 1,1\rangle,$ with $\theta = \pi/2$. A representative sample output may look like this (each outcome should be measured at a \~$50\%$ probability): | a | b | counts | probability | bitstring | | - | - | ------ | ----------- | --------- | | 0 | 0 | 1047 | 0.511230 | 00 | | 1 | 1 | 1001 | 0.488769 | 11 | Which represents the outcomes of a maximally entangled Bell state. ## Observables as a Pauli string In the context of quantum mechanics, an observable is represented by a Hermitian operator. In digital quantum computing, these operators are most commonly expressed as a linear combination of Pauli strings. A Pauli operator is a tensor product of single-qubit Pauli matrices $(I, X, Y, Z)$ acting on specific qubits. In Qmod, it is possible to define these observables using `SparsePauliOp`. This enables specifying the physical quantity to estimate — such as energy in a molecular Hamiltonian or a cost function in a combinatorial optimization problem. A Pauli operator, when represented using `SparsePauliOp`, consists of: * **A list of Pauli terms**: Each term corresponds to a Pauli tensor product (a Pauli “string”) acting on specific qubits. * **A coefficient for each term**: Each Pauli string is associated with a real or complex coefficient, and the full operator is given by the linear combination of these terms. Mathematically, this means that any observable $H$ can be represented as: $H = \sum_i c_i \, P_i,$ where $\{c_i\}_i$ are real coefficients and $\{P_i\}_i$ are Pauli strings, such as $X \otimes Y \otimes Z$. To construct these observables in Python, use `SparsePauliOp` objects (Check [Classical Types](/qmod-reference/language-reference/classical-types/#hamiltonians)). In particular, using a SparsePauliOp is an efficient way to define a sparse observable. Where “sparse” refers to the structure of each Pauli string—specifically, that many qubits are acted on by the identity operator $I$, and only a relatively small number have non-identity Pauli matrices. This representation compactly encodes such strings while storing only the terms that actually appear in the operator. [comment]: DO_NOT_TEST ```python theme={null} # Define the same observable, but using a SparsePauliOp: 0.5 * Z0 * Z1 + 0.8 * X0 my_sparse_observable = 0.5 * Pauli.Z(0) * Pauli.Z(1) + 0.8 * Pauli.X(0) ``` ## Expectation value estimation Once an observable is defined, it is possible to evaluate its expectation value from a quantum program. For this, use [`observe`](../execution/observe). [comment]: DO_NOT_TEST ```python theme={null} qprog = synthesize(main) observe( qprog, observable=my_sparse_observable, num_shots=1000 ) res.value ``` `(0.49531250000000004+0j)` When the quantum program is parameterized, the observable can be estimated as a function of the program parameters. Parameters correspond to classical arguments of main (for example `CReal`), and values are provided at execution time using a dictionary keyed by the parameter name. More information about it can be obtained from the [Execution Tutorial](/getting-started/classiq_tutorial/execution_tutorial_part2). ## Pauli operators as quantum operators Beyond their role in defining observables for expectation-value estimation, Pauli operators can also be interpreted as quantum operators that generate unitary evolution. In this context, a Pauli operator represents a Hermitian operator that can be exponentiated to form a unitary of the form $U = e^{-i t H },$ where $P$ is a Pauli operator and $t$ is a real-valued parameter. This interpretation is central when simulating time evolution under a Hamiltonian or when constructing circuits that implement operator exponentials derived from physical models. ### Hamiltonian evolution and operator decomposition Consider a Hamiltonian expressed as a linear combination of Pauli strings, $H = \sum_i c_i \, P_i$ In general, the Pauli terms $P_i$ do not commute, which means the unitary evolution $U(t) = e^{-i t H },$ cannot be implemented exactly as a single operation. Instead, it is approximated by decomposing the evolution into a sequence of exponentials of individual Pauli strings. This decomposition turns Pauli operators into building blocks of quantum dynamics, rather than merely objects to be measured. **Example: Suzuki–Trotter decomposition** One widely used approach for approximating Hamiltonian evolution is the Suzuki–Trotter decomposition. In its first-order form, the time evolution under $H$ is approximated as: $e^{-i H t} \approx \prod_i e^{-it c_i P_i}$ with the approximation improving as the evolution is split into more, smaller time steps. From a modeling perspective, the same Pauli operator structure used to define observables naturally extends to defining operator evolution, making Pauli operators a unifying representation for both measurement and dynamics. We can see an example of it when using the `suzuki_trotter` function, as in the example below. ```python theme={null} from classiq import * quantum_operator = ( Pauli.X(0) * Pauli.Y(1) + Pauli.Z(1) + Pauli.Z(2) + Pauli.Y(3) * Pauli.X(2) ) measured_observable = Pauli.X(0) + Pauli.Y(1) + Pauli.Z(2) + Pauli.X(3) @qfunc def main(x: Output[QArray]): allocate(4, x) suzuki_trotter( quantum_operator, evolution_coefficient=1.0, order=1, repetitions=1, qbv=x ) qprog = synthesize(main) res = observe( qprog, observable=measured_observable, num_shots=1000 ) res ``` `(-0.4814453125+0j)` This code: * Synthesizes the quantum program defined in main, which prepares the quantum state and applies the Suzuki–Trotter evolution generated by the specified Pauli operator Hamiltonian. * Uses `observe` to execute the synthesized program. * Estimates the expectation value of `measured_observable`, computing a classical quantity derived from the final quantum state rather than returning raw measurement samples. This demonstrates that the same Pauli operator formalism underlies both quantum dynamics and measurement in Qmod: Pauli operators define the operators that evolve the state, while observables define the quantities extracted from it. Understanding this distinction clarifies why Pauli operators play a dual role in quantum programs, motivating the discussion that follows on how observables and operators are treated differently despite sharing the same mathematical representation. Although Pauli operators appear in both operator evolution and observable definitions, their roles are distinct: * As **observables**, Pauli operators specify what quantity is measured or estimated from a quantum state. * As **operators**, Pauli operators define unitary transformations applied to the quantum state during execution. ## See also The [Execution page](../execution/index). Definitions for [SparsePauliOp](/sdk-reference/qmod/classical-types#sparsepauliop), [observe](../execution/observe), and [suzuki\_trotter](/sdk-reference/qmod/functions/core_library/exponentiation#suzuki_trotter). # Quantum Numbers and Arithmetics Source: https://docs.classiq.io/user-guide/modeling/quantum-numbers-arithmetics ## Introduction Qmod allows modeling quantum data using typed variables that behave much like variables in classical programming languages: you declare them, allocate them, and then use them in arithmetic and logical expressions over them. In this guide, we focus on quantum numeric variables (for example, integers or fixed-point values represented on qubits) and how to use them in arithmetic expressions, such as addition, comparisons, and bitwise logic, to build useful subroutines for quantum algorithms. A key motivation is that these expressions are not simply for “doing math”: they are a practical way to encode conditions over quantum states, which appear throughout quantum algorithms. For instance, many search and optimization-style algorithms require marking the states that satisfy a certain condition, often implemented as a phase oracle. In Qmod, such predicates can be written directly as a arithmetic or boolean expression, such as `x + y == target`. We therefore start with a minimal “a + b” example to build intuition about computation over values in superposition, and then expand toward expressing and combining conditions in a way that scales to real algorithmic use cases. In this guide, you will find: * An introduction to **quantum numeric variables (`QNum`)** in Qmod and how they represent **integers or fixed-point values** stored on qubits. * An explanation of how to **declare and allocate quantum numbers**, including the meaning of `QNum[size, is_signed, fraction_digits]` and when its type attributes can be **inferred automatically**. * A walkthrough showing how **superposition affects arithmetic results**, and how computed outputs (e.g., `z = x + y`) become **correlated/entangled** with the input variables. * A description of **assignment** (`|=`) as the main way to compute expressions and store results in a target variable. * A clear distinction between **out-of-place** (`z |= x + y`) and **in-place** updates (`x += y`, `z ^= condition`), including when each is useful. * Examples of how to specify richer **quantum arithmetic and predicate expressions** using arithmetic, comparison, and bitwise operators to **mark target states** (as used in Grover-like algorithms). ## Quantum numeric variables In Qmod, once a quantum variable is allocated, it references a quantum object stored on one or more qubits. Quantum variables are declared with a *quantum type* that defines characteristics such as how many qubits are used and how the state is interpreted. In the case of a quantum number (`QNum`), the state is interpreted as an integer or a fixed-point real number. When expressing algorithm logic, it is often useful to treat a quantum object as a number and write code that resembles classical numeric programming. However, the values stored in quantum variables may present quantum effects, such as entanglement and superposition. Before diving into how to use quantum numbers, consider the following quantum program: ```python theme={null} from classiq import * @qfunc def prepare_superposition(q: QArray[QBit]): H(q[1]) @qfunc def main( x: Output[QNum[3, UNSIGNED, 0]], y: Output[QNum[3, UNSIGNED, 0]], z: Output[QNum] ): allocate(x) allocate(y) prepare_superposition(x) prepare_superposition(y) z |= x + y ``` At a high level, this quantum program: 1. Allocate two different quantum numbers, `x` and `y`. 2. Prepare the following superposition states by applying a Hadamard gate on different qubits using `prepare_superposition`: $\vert x \rangle = \frac{1}{\sqrt{2}}\left(\vert 0 \rangle + \vert 2 \rangle\right), \quad \vert y \rangle = \frac{1}{\sqrt{2}}\left(\vert 0 \rangle + \vert 2 \rangle\right).$ 3. Evaluate the arithmetic operation `x + y` and assign its result to the variable `z`. A representative sample output may look like this (each outcome should be measured at a \~25% probability): | x | y | z | counts | probability | bitstring | | - | - | - | ------ | ----------- | ---------- | | 0 | 0 | 0 | 519 | 0.253417 | 0000000000 | | 2 | 2 | 4 | 497 | 0.242675 | 0100010010 | | 2 | 0 | 2 | 508 | 0.248046 | 0010000010 | | 0 | 2 | 2 | 524 | 0.255859 | 0010010000 | Since variables `x` and `y` are in superposition, the value of `z` will vary according to the values measured in `x` and `y`. Consequently, `z` is entangled with `x` and `y`. ### Declaring quantum numbers Now, let’s dive into the declaration of the quantum numbers `x` and `y`. In the example above, they are declared using the type hint `Output[QNum[3, UNSIGNED, 0]]`. A type hint is an annotation next to a variable that specifies what kind of value it represents. Here, `QNum` indicates that `x` and `y` represent numbers, and their type attributes specifies their numeric attributes: * 3: the total number of qubits used to store the number (size) * `UNSIGNED`: the number has no sign (i.e., non-negative). This field is optional, and set to `UNSIGNED` by default. * 0: the number has no fraction digits. This means the number is interpreted as an integer. If $\text{fraction_digits} > 0$, the value is interpreted as a fixed-point number, scaled by $2^{-\text{fraction_digits}}$. This field is optional, and set to 0 by default. This matches the definition of a quantum number type in Qmod as:
QNum\[size, is\_signed, fraction\_digits]
Here, `is_signed` is an optional field, set to `UNSIGNED` by default, and `fraction_digits` is an optional field set to 0. It is also possible to use the booleans `False`/`True` instead of `SIGNED`/`UNSIGNED`. A useful point when writing more general Qmod code is that the type attributes (such as `size`, `is_signed`, and `fraction_digits`) can also be used as Python expressions, which allows you to write code that adapts to the numeric configuration of a quantum variable. `size` is an attribute accessible by every quantum type, not only quantum numbers. ### Omitting type attributes and inference Note that `z` is declared as `Output[QNum]` without specifying numeric attributes. This is intentional: numeric attributes are optional, and when they are not provided, the Qmod compiler infers them upon the variable’s first initialization (including cases such as assignment). The use of this feature is restricted to when it is possible to infer the type attributes from the quantum number, otherwise the quantum program might raise errors. For instance, if `y` is not correctly declared in the above example, the following error will arise:
Could not infer the size of variable 'y'.
For a complete description of the numeric inference rules, check the [Language Reference](/qmod-reference/language-reference/quantum-types#numeric-inference-rules). ### Alternative: Providing type attributes in `allocate` Instead of specifying numeric attributes in the type hint, you can provide them in the `allocate` call. The following two declarations are equivalent according to the numeric inference rules and initialization behavior: ```python theme={null} from classiq import * @qfunc def main(x: Output[QNum[3, SIGNED, 1]]): allocate(x) ``` ```python theme={null} from classiq import * @qfunc def main(x: Output[QNum]): allocate(3, SIGNED, 1, x) ``` ## Numeric assignment Numeric assignment is the Qmod mechanism that lets you compute with quantum numbers using expressions that resemble classical code. Conceptually, you write an expression such as `x + y`, and the Qmod compiler synthesizes a gate-level description that computes the result in the computational basis, even when `x` and `y` are in superposition. This can be written using `|=`. For example, [comment]: DO_NOT_TEST ```python theme={null} z |= x + y ``` can be read as:
"Compute x + y and store the result in the variable z."
In the earlier example, `z` is declared as `Output[QNum]` without specifying how many qubits it uses. This works because Qmod can infer the right size when `z` is first initialized. In this case, `z` is a 4-qubit unsigned quantum number with 0 fraction digits. This is why the assignment line is doing more than “just storing a value”: it is also the moment where `z` gets its final numeric attributes if you did not specify it explicitly. ### In-place and out-of-place assignment When you compute something like x + y, you must determine where the result will be stored. There are two options: 1. **Out-of-place**: write the result into a new variable (leave the inputs unchanged) This is what happens with: [comment]: DO_NOT_TEST ```python theme={null} z |= x + y ``` * `x` and `y` remain the same variables, with the same qubits. * `z` is the output where the result is computed. * This typically creates or initializes storage for `z` (therefore it should not be initialized yet). * Quantum effect: if `x` and `y` are in superposition, `z` becomes correlated with them (often entangled), but `x` and `y` are not overwritten. This is analogous to performing the following classical operation: [comment]: DO_NOT_TEST ```python theme={null} z = x + y ``` This is called out-of-place because the result is written in an additional variable `z`. 2. **In-place**: write the result into the same variable (update an existing variable) This is what happens with operators like += and ^=: [comment]: DO_NOT_TEST ```python theme={null} x += y ``` * The result is stored back into x. * `x` must already be allocated (because it is being modified). * `x` keeps the same number of qubits, so if the operation would need more bits, the result may wrap around (modulo $2^{\rm{size}}$). This is called in-place because the computation updates the same storage in-place, instead of creating a new variable where the result is assigned. You can think of this exactly as classical code `x+=y`. ### Two common in-place operators 1. `+=` **(add into the same variable)** The in-place add can be used to increment a quantum variable's value. To better understand this, we can analyze the following example: ```python theme={null} from classiq import * @qfunc def main(x: Output[QNum[3]], y: Output[QNum[2]]): allocate(y) hadamard_transform(y) x |= 1 x += y ``` Whose outputs are close to | x | y | counts | probability | bitstring | | - | - | ------ | ----------- | --------- | | 4 | 3 | 508 | 0.258046 | 11100 | | 3 | 2 | 513 | 0.250488 | 10011 | | 2 | 1 | 522 | 0.254882 | 01010 | | 1 | 0 | 505 | 0.256582 | 00001 | This quantum program: * Initializes two quantum numbers, `x` and `y` * `x` is assigned the integer value 1, while `y` is in a superposition of the values in $[0, 1, 2, 3]$ * Performs the quantum operation `x = x + y`, where `y` is in a superposition and, consequently, `x` becomes superposed as well. This way, the variable `x` is always an increment of variable `y`. 2. `^=` **(in-place XOR)** The bitwise XOR operator is a binary operation that compares the bitstrings of two numeric values and produces a result bit of 1 when the bits differ and 0 when they are the same. In Qmod, it is implemented by applying this rule between corresponding qubits (or between a qubit and a classical bit) across the operands. The in-place form `^=` computes the XOR and writes the result back into the left-hand variable. In the example below, `x + y == 3` combines this value with `z` using XOR and writes the result back into `z`. This means that `z` is updated only when the condition evaluates to 1. ```python theme={null} from classiq import * @qfunc def main(x: Output[QNum[3]], y: Output[QNum[2]], z: Output[QBit]): allocate(y) allocate(z) hadamard_transform(y) x |= 1 z ^= x + y == 3 ``` Whose outputs are close to | x | y | z | counts | probability | bitstring | | - | - | - | ------ | ----------- | --------- | | 1 | 0 | 0 | 503 | 0.255605 | 000001 | | 1 | 1 | 0 | 517 | 0.252441 | 001001 | | 1 | 2 | 1 | 532 | 0.259765 | 110001 | | 1 | 3 | 0 | 496 | 0.242187 | 011001 | This quantum program: * Initializes two quantum numbers, `x` and `y`, and a qubit `z`. * Assign the numeric value 1 to `x` and generates a uniform superposition in `y`. * Flips the state of `z` whenever `x + y == 3`. This way, the in-place XOR operation serves as a powerful way of "marking" target states - a very important tool in search and optimization algorithms, such as [Simon's algorithm](/explore/algorithms/foundational/simon/simon). ## Quantum Arithmetics Qmod supports quantum expressions, including arithmetic expressions, in a manner analogous to classical programming languages. Both quantum and classical expressions may appear on the right-hand side of assignment statements, and quantum expressions may also be used in other contexts, such as the conditions of control statements. When a quantum expression is written, Qmod records the expression symbolically and translates it into a reversible quantum subroutine that implements the corresponding operation. This ensures that the resulting operation is unitary and can be applied to quantum variables. ### Writing Arithmetic Expressions Qmod supports a wide range of arithmetic and logical operators inside expressions used for numeric assignment. In practice, this means you can write expressions such as: * Arithmetic: `+`, `-`, `*` * Comparisons (produce a boolean / predicate): `==`, `!=`, `<`, `<=`, `>`, `>=` * Bitwise logic: `&`, `|`, `^`, `~` * Boolean composition of predicates: `logical_and`, `logical_or`, `logical_not` (depending on the expression context) A simple example using different arithmetics and boolean operations is: ```python theme={null} from classiq import * @qfunc def main(x: Output[QNum[2]], y: Output[QNum[2]], z: Output[QBit]): allocate(y) allocate(x) allocate(z) hadamard_transform(x) hadamard_transform(y) z ^= (x + y - 1) * (x - y + 1) == -1 ``` This quantum program: * Allocates two quantum numbers `x` and `y`, and a qubit `z` * Puts the quantum numbers in superposition over all possible values * Computes the predicate $(x + y - 1) \cdot (x - y + 1) = -1$ and conditionally modofies the state of `z` when the predicate is satisfied. In this context, we are correlating the auxiliary qubit `z` with the values of `x` and `y`. The quantum numbers `x` and `y` themselves are not modified. Instead, `z` becomes entangled with them such that: * For basis states where the expression evaluates to -1, the state of `z` is changed to $\vert 1 \rangle$ * For all other basis states, `z` remains $\vert 0 \rangle$ As a result, measuring `z` and obtaining 1 would collapse `x` and `y` into a superposition of only those value pairs that satisfy the equality. A technical description of numeric assignment is available at the [Language Reference](/qmod-reference/language-reference/statements/assignment). ## See also For more information regarding quantum types, see [Quantum Types](/qmod-reference/language-reference/quantum-types). # Getting Started with Studio Source: https://docs.classiq.io/user-guide/studio/Studio-Guide The Studio is Classiq's browser-based development environment, giving you a ready-to-use workspace for writing and running quantum programs without any local installation. ## **Setup and Get Started** To get started with the Studio, follow these steps: 1. Sign in to the platform: access is by invitation only, so if you do not yet have a Classiq account, first request access through the [Registration](/getting-started/registration_installations) guide. Once your account is set up, sign in at [platform.classiq.io](https://platform.classiq.io/) with your Google/Microsoft account or a general email address, and you'll be redirected to the platform home page. 2. Access the Studio from the platform home page using the sidebar button, as shown below. studio sidebar button 3. Your environment is loaded in a new browser tab (initial setup could take a couple of minutes). loading screen 4. Please trust the Classiq workspace author to open the Classiq extension and load your workspace. trusted-host Now your Studio is ready for use. Access your workspace via the "EXPLORER" icon on the top left. Your workspace consists of two sections: * Workspace: persistent storage; any edits you make are saved. * Classiq library: This is your place to explore the world of quantum algorithms. You can click on any notebook, modify it, and run it. But remember that this is non-persistent storage: any edits to these files are reverted when the Studio is shut down (after 1 hour of being idle). If you've made changes you want to save, copy-paste the file to the personal workspace section. workspace_bright workspace * You can also upload any file into your persistent storage by right-clicking on "user workspace" and selecting "upload". upload file at studio bright upload file at studio dark 5. When running a notebook for the first time, you will be asked to install the kernel. Click on the "Select Kernel" button and choose the recommended Python kernel (Python 3.12.8). select correct kernel in studio bright select correct kernel in studio dark 6. A drop-down menu appears; select "Python Environments" (or press "Enter"). 7. Select "classiq-venv". * More advanced users who are comfortable managing their own environment are welcome to set up and use their own virtual environment. 8. Start coding, experiment, and create quantum algorithms. 9. When calling the method `show()` you might see a pop-up that asks you to trust an external domain - click on "trust all domains from classiq.io" to see this pop-up only once. The circuit will open up in a separate tab. ## **Best Practices** In the Studio, each user has their own dedicated persistent storage (2 GB). This means that every time you reload, you can keep working right where you left off, just as you would locally on your computer. Keep the following considerations and best practices in mind: ### **Data Persistence** #### **Location** The explorer (file browser) is divided into two sections: Workspace and Classiq library. The Classiq library is non-persistent, and any edits to these files are reverted when the Studio is shut down (after 1 hour of being idle). Thus, we recommend copying the desired notebook to the Workspace section. #### **Retention Period** Please note that this feature is still under development, and we are working on improving it. Thus, we will do our best to keep your data and work safe, but we cannot guarantee that your data will be kept forever. We recommend backing up your work locally (by downloading the files) or in a remote repository. ### **Using Git** We highly recommend using Git and GitHub (or similar) to keep track of your work. You can use Git in the terminal or in the Studio. Check out [this section](/user-guide/studio/git-getting-started) to get started with Git. After committing locally, it's best to push your work into a remote repository for backup and collaborative work. ### **Using the Virtual Environment Wisely** You have a virtual environment installed in your Studio. You can install any pip package you need for your work (e.g., Qiskit, TensorFlow, etc.), but keep in mind that you have limited space. Also, environments are often hard to maintain, and you can get into a dependency rabbit hole. If you want to experiment with new packages, create a new venv with whatever packages you need and delete it when you're done. We guarantee that the "classiq-venv" is set and working. #### **Resetting the Virtual Environment** If you encounter issues or simply want a clean slate, you can reset your virtual environment by running the following command in the terminal: ```bash theme={null} reset-user-env ``` Running this command will remove any additional packages or modifications you've made in your virtual environment, restoring it to its default state. This is especially useful if you experience dependency issues or if the environment becomes cluttered from experimentation. > ***Pro Tip:*** To manually reset your virtual environment, you can run: > `rm -rf /classiq-venv/*` ## **Pre-Installed LaTeX (latexmk)** `latexmk` and a TeX Live subset (`texlive-latex-base`, `texlive-latex-recommended`, `texlive-latex-extra`, `texlive-fonts-recommended`, `texlive-science`) are pre-installed in the Studio. This means you can run the benchmarking application from the Classiq library and have it build its PDF report out of the box, without any additional setup. # Getting started with git Source: https://docs.classiq.io/user-guide/studio/git-getting-started This page contains a practical guide on how to get started with git and GitHub, so you can get the most out of working with the Studio and writing code in general ## **what is git?** Git is a version control system that tracks changes in your code. It lets you: Keep a history of modifications. Work on different versions of your code. Collaborate with others without overwriting changes. Restore previous versions if something breaks. It runs locally on your machine, but when combined with platforms like GitHub or GitLab, it enables seamless collaboration. ### **What is a Repository?** A repository (repo) is a storage space where your project’s files, code, and version history are managed by Git. A local repository exists inside your project folder and tracks changes. A remote repository is stored on a hosting service like GitHub, GitLab, or Bitbucket, allowing multiple people to work on the same project. We highly recommend to push your code to a remote repository. It will promise you that your work is backed up, and you can access it from anywhere. ## **Common commands and quick setup** The Studio comes with Git pre-installed, so you can start using it right away. ### **Initialize a Git Repository** Inside your project folder, run: ```bash theme={null} git init ``` This creates a local repository, allowing you to track changes in your project. ### **Clone an Existing Repository** You can always copy (clone) an existing project from a remote repository (e.g., GitHub) to your workspace: ```bash theme={null} git clone ``` ### **Useful commands:** #### **Check the status of your project:** ```bash theme={null} git status ``` #### **Add changes to the next commit:** ```bash theme={null} git add . ``` #### **Save your changes:** ```bash theme={null} git commit -m "Description of changes" ``` #### **Push and Pull** Push – Send your changes to a remote repository (called origin): ```bash theme={null} git push origin main ``` Pull – Get the latest updates from the remote repository: ```bash theme={null} git pull origin main ``` When you start feeling comfortable with git, you can start using branches, pull requests, and more advanced features. You can read more about it in attached linked. ## **How does it work with GitHub?** GitHub (or alternatives like GitLab and Bitbucket) acts as a remote repository, meaning it stores your code in the cloud and enables collaboration. ### **What is a Remote Repository?** A remote repository is a version of your project that is hosted online. It allows you to: * Store your code securely. * Access your work from any device. * Collaborate with team members in real-time. ### **Connect your local repository to a remote repository, use:** clone the repo: ```bash theme={null} git clone ``` add it to your local workspace ```bash theme={null} git remote add origin ``` Then, push your local changes to the remote repo: ```bash theme={null} git push -u origin main ``` Best Practices for Using Git with GitHub Use branches – Work on a separate branch before merging changes to the main project: ```bash theme={null} git checkout -b feature-branch ``` ### **Best practice to combine git and GitHub (or gitlab etc. - it's up to you :)** * Write clear commit messages – describe what changed and why. * Pull before pushing – always get the latest updates before sending your changes. * Use `.gitignore` – avoid committing unnecessary files. * Create pull requests – review changes before merging into main. ## **How to use git in the Studio** * Open the terminal in the Studio. * We have already installed git inside the studio, so you can start using it right away. ## **Read more** There are infinite number of guides available on using git, integration with GitHub or others. We have collected some of our favorites: * [Git official documentation](https://git-scm.com/doc) * [GitHub official documentation](https://docs.github.com/en/get-started/) * [Git cheat sheet](https://www.atlassian.com/git/tutorials/atlassian-git-cheatsheet) # Studio - Web-based IDE with Classiq inside Source: https://docs.classiq.io/user-guide/studio/index The Studio is a VSCode environment part of the platform. It bypasses the need to install the Python SDK package and its dependencies, and includes the complete and updated Classiq Library git repository of quantum functions, algorithms, applications, and tutorials. ## **Key features** **Code editing and running** – Write and modify your code using the latest Qmod and python SDK version in a fully featured editor with syntax highlighting, autocompletion, go-to definition, jupyterlab extension and more. Using your favorite code editing tools - vscode or jupyterlab **Work from anywhere** - We supply a persistent user workspace to save and manage code and projects. **Ready-to-use library** - The Classiq library contains tens of state-of-the-art quantum algorithms ready for use. **Integrated debugging** – Step through execution and inspect intermediate results. ## **Getting started** **Access the Studio** – Open the Studio from the [platform](https://platform.classiq.io/) (once you are granted access) and follow the instructions [here](/user-guide/studio/Studio-Guide) **Write Your First Program** – Use the editor to create your first quantum program or load an example of our applications from the build in Classiq library. ## **Next steps** * Explore the code editor guide to learn about the features, capabilities and best practice of the editor * Read the [Qmod reference](/qmod-reference/index), [SDK reference](/sdk-reference/index) and the [Execution section](/user-guide/execution/index) to get started with Classiq and start writing and running quantum algorithms and programs. # Quantum Program Constraints Source: https://docs.classiq.io/user-guide/synthesis/constraints When synthesizing a quantum program, you can pass a maximum width constraint to the generation; for example, requiring that no more than 20 qubits are used. Pass constraints as follows: ```python theme={null} from classiq import * constraints = Constraints(max_width=20) @qfunc def main(res: Output[QBit]) -> None: allocate(res) synthesize(main, constraints=constraints) ``` ## Optimization Parameter When synthesizing a quantum program, to optimize the quantum program according to a parameter, set the `optimization_parameter` field. The possible parameters are the same parameters that can be constrained. The following example shows how to remove the width constraint in the quantum program, setting it instead as the optimization parameter. ```python theme={null} from classiq import ( qfunc, Constraints, OptimizationParameter, Output, QBit, TranspilerBasisGates, allocate, set_constraints, synthesize, ) constraints = Constraints( max_width=20, optimization_parameter=OptimizationParameter.DEPTH, ) @qfunc def main(res: Output[QBit]) -> None: allocate(1, res) synthesize(main, constraints=constraints) ``` # Getting Started with Quantum Program Synthesis Source: https://docs.classiq.io/user-guide/synthesis/getting-started The first steps in quantum program synthesis are defining the algorithms, its constraints, and your preferences. These are described in detail in the following sections. Once your model is fully defined, it is time to synthesize it. In the IDE, describe the model in the [model](https://platform.classiq.io/dsl-synthesis) page. After writing down the model, click `Synthesize` on the bottom right of the page. After synthesis, you are automatically redirected to the quantum program page. Initiate the synthesis process by performing the `synthesize` method on a model. Alternatively, you can use the async `synthesize_async` method as part of an async code. ```python theme={null} from classiq import qfunc, Output, QBit, allocate, synthesize @qfunc def main(res: Output[QBit]) -> None: allocate(1, res) qprog = synthesize(main) ``` # Hardware-Aware Synthesis Source: https://docs.classiq.io/user-guide/synthesis/hardware-aware-synthesis Quantum computers differ from one other in many significant parameters, such as basis gates, connectivity, and error rates. The device specifications determine the possibility of executing the quantum program, and logically equivalent programs might require different implementations to optimize the probability of success. The platform allows you to provide information about the hardware you want to use to run your quantum program. The synthesis engine takes the parameters of this hardware into account. For example, the engine could choose the implementation of a function that requires the least number of swaps, given the connectivity of the hardware. If the hardware device's basis gate set contains the Clifford gates `X`, `Z`, `H`, `T`, and `CX` but does not contain arbitrary-angle rotation gates such as `RX`, the platform uses the Solovay-Kitaev algorithm to approximate single-qubit gates when necessary. You can set the maximum iterations of the Solovay-Kitaev algorithm in the preferences, thus tuning the algorithm target accuracy. (Larger values usually result in better and longer approximations, at the expense of longer running times.) ## Specifying a Backend To synthesize your quantum program for a specific backend, specify the backend provider and the name of the backend. The platform supports these backend providers: * Amazon Braket: All gate-based backends in [Amazon Braket](https://docs.aws.amazon.com/braket/latest/developerguide/braket-devices.html) including all Rigetti devices, `Lucy`, and `IonQ Device`. * Azure Quantum: `ionq` and `quantinuum`. * IBM Quantum: Those listed on [IBM Quantum's official website](https://quantum-computing.ibm.com/services/resources?tab=systems). Note that you should specify the name of the backend without the `ibmq_` prefix. ```python theme={null} from classiq import * @qfunc def main(res: Output[QBit]) -> None: allocate(res) preferences = Preferences( backend_service_provider="IBM Quantum", backend_name="ibm_boston" ) qprog = synthesize(main, preferences=preferences) ``` ## Customizing Hardware Settings To synthesize the quantum program for hardware that is not available in the platform, you can specify the custom settings of the hardware. This includes the basis gate set and the connectivity map of the hardware. Note that all hardware parameters are optional. ### Basis Gate Set These are the allowed gates: * Single-qubit gates: `u1`, `u2`, `u`, `p`, `x`, `y`, `z`, `t`, `tdg`, `s`, `sdg`, `sx`, `sxdg`, `rx`, `ry`, `rz`, `r`, `id`, `h` * Basic two-qubit gates: `cx`, `cy`, `cz` * Extra two-qubit gates: `swap`, `rxx`, `ryy`, `rzz`, `rzx`, `ecr`, `crx`, `cry`, `crz`, `csx`, `cu1`, `cu`, `cp`, `ch` * Three-qubit gates: `ccx`, `cswap` If you do not specify gates in the IDE, the field remains empty. During synthesis, default basis gates are applied automatically based on connectivity. To override, select one or more gates in this field. ### Connectivity Map The connectivity map is given by a list of pairs of qubit IDs. Each pair in the list means that a two-qubit gate (e.g., `cx`) can be performed on the pair of qubits. If the coupling map is symmetric, then both qubits can act as control. If the coupling map is asymmetric, then the first qubit can act only as control, and the second qubit can act only as target. To determine whether the provided map is symmetric, set the ` is_symmetric_connectivity` argument. If you do not specify the connectivity map, the engine assumes full connectivity. ### Example The following example specifies a backend with 6 qubits in a 2-by-3 grid, where each qubit connects to its immediate neighbors. The backend uses four basis gates: `cx`, `rz`, `sx`, and `x`. ```python theme={null} from classiq import ( qfunc, CustomHardwareSettings, Output, Preferences, QBit, allocate, synthesize, set_preferences, ) @qfunc def main(res: Output[QBit]) -> None: allocate(1, res) custom_hardware_settings = CustomHardwareSettings( basis_gates=["cx", "rz", "sx", "x"], connectivity_map=[(0, 1), (0, 3), (1, 4), (1, 2), (2, 5), (3, 4), (4, 5)], is_symmetric_connectivity=True, ) preferences = Preferences(custom_hardware_settings=custom_hardware_settings) synthesize(main, preferences=preferences) ``` # Quantum Program Synthesis Source: https://docs.classiq.io/user-guide/synthesis/index Quantum algorithm design is a complex task, and solutions based on the approach of designing at the gate level are not scalable. Likewise, solutions based on combining existing building blocks are very limited in their scope. The platform allows a high level description of quantum algorithms at the functional level and automatically synthesizes a corresponding quantum program. The synthesis process refines the functional requirements, then allocates and optimizes available resources such as the number of qubits available. The basic description of quantum algorithms in the platform is through quantum functions. A quantum function can be implemented in multiple ways, each with different properties such as number of qubits, number of auxiliary qubits, depth, and approximation level. You can define any level of refinement of the function, from the purely abstract to the fully defined. The synthesis engine fills in the details to provide a concrete implementation satisfying all your requirements. Moreover, a complete quantum algorithm can be realized in multiple ways, utilizing different design choices such as function implementations, placement, uncompute strategies, qubit management, and wirings. The platform sifts through the design space and finds a best realization given your requirements and resource constraints. This section describes how to define your algorithm and synthesize a quantum program that implements it: * [Getting Started](/user-guide/synthesis/getting-started) * [Constraints](/user-guide/synthesis/constraints) * [Preferences](/user-guide/synthesis/preferences) # Synthesis Preferences Source: https://docs.classiq.io/user-guide/synthesis/preferences You can modify these synthesis process preferences: * [Output formats](#output-formats) * [Hardware-aware settings](/user-guide/synthesis/hardware-aware-synthesis) * [Timeouts](#timeouts) * [Optimization level](#optimization-level) * [Toggling quantum program visualization support](#toggling-circuit-visualization-support) In this example, the chosen output format includes both Q# and OpenQASM. Specific basis gates are selected for the synthesis: controlled not, controlled phase, square root of not, Z-rotation, and not gates. [comment]: DO_NOT_TEST ```python theme={null} from classiq import ( qfunc, Output, QBit, allocate, synthesize, show, CustomHardwareSettings, Preferences, QuantumProgram, allocate, ) @qfunc def main(res: Output[QBit]) -> None: allocate(1, res) custom_hardware_settings = CustomHardwareSettings( basis_gates=["cx", "cp", "sx", "rz", "x"] ) preferences = Preferences( output_format=["qasm", "qsharp"], custom_hardware_settings=custom_hardware_settings ) qprog = synthesize(main, preferences=preferences) show(qprog) print(qprog.qsharp) ``` ## Output Formats The platform provides different ways to format the output of synthesized quantum programs. You can choose multiple output formats. * In the SDK, you can print or save the desired output format after synthesizing. * In the IDE, you can download the desired output format after synthesizing. The output options: * `"qasm"` - OpenQASM. The qasm circuit is in `qprog.qasm`. * By default, the platform uses OpenQASM 2.0. To use OpenQASM 3.0 instead, set the `qasm3` field of the preferences to `True`. * `"qsharp"` - Q#. The qsharp circuit is in `qprog.qsharp`. * `"qir"` - Microsoft's QIR. The QIR circuit is in `qprog.qir`. * `"ionq"` - IonQ Json format is in `qprog.ionq`. * `"cirq_json"` - Cirq Json format is in `qprog.cirq_json`. * `"qasm_cirq_compatible"` - OpenQASM 2.0 is compatible with Cirq, which is in `qprog.qasm_cirq_compatible`. ## Optimization Level Some optimization strategies employed by the synthesis engine are computationally heavy. You can control the tradeoff between synthesis time and the exhaustiveness of the search for optimal circuit. Use `optimization_level` with the following values: * `NONE` (0) - take the most time-efficient path * `LOW` (1) - perform only light-optimizations * `MEDIUM` (2) - skip the most time-consuming optimizations * `HIGH` (3) - employ the most aggressive and time-consuming optimizations Notes: * Lower optimization levels may fail to satisfy user-specified synthesis constraints (see [Quantum Program Constraints](/user-guide/synthesis/constraints#optimization-parameter)). In such cases you can retry with a higher optimization level. * Lower optimization levels may result in missing details in the quantum-program visualization in the IDE. This limitation will be lifted in future releases. * Higher optimization levels may take longer to complete, and may yield better results, but neither is guaranteed. ## Timeouts The platform offers two timeouts: * `timeout_seconds` – A timeout value for the end-to-end synthesis process. * `optimization_timeout_seconds` – A timeout value specifically controlling the search process when given constraints and optimization directives (see [Quantum Program Constraints](/user-guide/synthesis/constraints#optimization-parameter)). You can specify both timeouts. Just make sure that the optimization timeout is smaller than the generation timeout. Both timeouts are specified in a whole number of seconds. ## Toggling Quantum Program Debug Information The platform allows users to toggle quantum program debug information: * `debug_mode` - When the flag is set to `True` (default), the quantum program will contain debug information for enhanced visualization (See [Quantum Program Visualization Tool](/user-guide/analysis/visualization-of-quantum-programs)). Setting this flag to `False` can potentially decrease the quantum program's size and increase synthesis speeds. # Quantum Program Transpilation Source: https://docs.classiq.io/user-guide/synthesis/quantum-program-transpilation Transpilation is the process of optimizing an already-synthesized quantum program and matching it to the desired hardware. It includes optimizations, such as combining a sequence of gates into an equivalent single gate; and transformations, such as qubit routing (i.e., using swap gates to apply 2-qubit gates on partially connected hardware). Classiq synthesis returns an execution-ready quantum program. It contains a description, in a restricted subset of Qmod, that defines the concrete implementation of high-level constructs and folds in all resource-allocation decisions. This description preserves the function hierarchy and symbolic control flow of the Qmod source and can be displayed via the [QP visualizer](/user-guide/analysis/visualization-of-quantum-programs). However, it is not yet transpiled to a specific backend's gate set or connectivity. Transpilation happens later, only when a concrete circuit is needed: when you [`export`](/sdk-reference/synthesis#export) the program to a supported [target language](/sdk-reference/synthesis#targetlanguage), when you request its transpiled metrics with [`get_transpiled_circuit_metrics`](/sdk-reference/synthesis#get_transpiled_circuit_metrics), or when you execute it on a designated backend, where the Classiq execution service transpiles it for that hardware so that every gate is compatible. To restore the legacy flow, in which transpilation runs during synthesis, set `compatibility_mode=True` in [`Preferences`](/sdk-reference/synthesis#preferences). If you set preferences in the synthesis step, for example a transpilation option or a target backend in the synthesis [`Preferences`](/sdk-reference/synthesis#preferences), they are saved with the quantum program and applied when you export it (by setting `transpilation_config=True`) or read its transpiled metrics. To use a different transpilation option on export, pass an explicit [`TranspilationConfig`](/sdk-reference/synthesis#transpilationconfig) instead of `True`; this overrides the synthesis settings entirely. Execution, on the other hand, disregards the synthesis transpilation option and transpiles for whichever backend you run on, at the `decompose` transpilation option by default. See [execution preferences](/sdk-reference/execution#executionpreferences) for the options available at execution. ## Transpilation Options * `none`: no transpilation. This is the default transpilation option of [`export`](/sdk-reference/synthesis#export) if no [`transpilation_config`](/sdk-reference/synthesis#transpilationconfig) is passed. * `decompose`: decompose all of the functions according to the basis gates of the backend. * `light`: the lightest optimization pass; fast, and best suited for fully connected hardware. * `medium`: a heavier pass that optimizes the quantum program further, at the cost of longer runtime. A balanced choice for hardware with moderate connectivity. * `auto optimize`: chooses the transpilation automatically between `light` and `medium`, based on the chosen backend. This is the default transpilation option in [`get_transpiled_circuit_metrics`](/sdk-reference/synthesis#get_transpiled_circuit_metrics) and in [`export(..., transpilation_config=True)`](/sdk-reference/synthesis#export) when no transpilation option is set as a preference at synthesis. * `intensive`: the heaviest pass, for maximum optimization. Well-suited for hardware with complex connectivity. * `custom`: offers a personalized approach to optimizing quantum programs while weighing various factors. It takes specific optimization criteria into account, transpiling the quantum program to maximize efficiency and resource utilization for the chosen backend, with a primary focus on delivering the best possible circuit performance. The heavier the transpilation option, the better the quantum program is optimized, but transpilation takes longer.