diff --git a/.github/workflows/config/spelling_allowlist.txt b/.github/workflows/config/spelling_allowlist.txt index 456558c509b..b300464829b 100644 --- a/.github/workflows/config/spelling_allowlist.txt +++ b/.github/workflows/config/spelling_allowlist.txt @@ -136,6 +136,8 @@ canonicalization canonicalize canonicalizer canonicalizes +centroid +centroids codebase comparator comparators @@ -147,6 +149,7 @@ coprocessing coprocessor coprocessors copyable +coreset cortex-cli coupler couplers @@ -161,6 +164,7 @@ deallocation deallocations decrement decrementing +dendrogram deserialize destructor dimensionality diff --git a/.github/workflows/python_wheels.yml b/.github/workflows/python_wheels.yml index e52969f1024..f3f95a457ac 100644 --- a/.github/workflows/python_wheels.yml +++ b/.github/workflows/python_wheels.yml @@ -316,7 +316,7 @@ jobs: docker run --rm -dit --name wheel-validation-examples wheel_validation:local status_sum=0 - for ex in `find docs/sphinx/examples/python -name '*.py' -not -path '*/providers/*'`; do + for ex in `find docs/sphinx/examples/python -name '*.py' -not -path '*/providers/*' -not -path '*/divisive_clustering_src/*'`; do file="${ex#docs/sphinx/examples/python/}" echo "__Example ${file}:__" >> /tmp/validation.out (docker exec wheel-validation-examples bash -c "python${{ inputs.python_version }} /tmp/examples/$file" >> /tmp/validation.out) && success=true || success=false diff --git a/docs/notebook_validation.py b/docs/notebook_validation.py index fcc53ffa3ae..e0d464af932 100644 --- a/docs/notebook_validation.py +++ b/docs/notebook_validation.py @@ -31,6 +31,10 @@ def validate(notebook_filename, available_backends): match = re.search('set_target[\\\s\(]+"(.+)\\\\"[)]', notebook_content) if match and (match.group(1) not in available_backends): return False + for notebook_content in lines: + match = re.search('--target ([^ ]+)', notebook_content) + if match and (match.group(1) not in available_backends): + return False return True diff --git a/docs/sphinx/examples/python/tutorials/Divisive_clustering.ipynb b/docs/sphinx/examples/python/tutorials/Divisive_clustering.ipynb new file mode 100644 index 00000000000..b3730dcb438 --- /dev/null +++ b/docs/sphinx/examples/python/tutorials/Divisive_clustering.ipynb @@ -0,0 +1,1088 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Divisive Clustering With Coresets Using CUDA-Q" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This tutorial will explore a CUDA-Q implementation of recent research (ArXiv Paper: https://arxiv.org/pdf/2402.01529.pdf) performed by a team from the University of Edinburgh. This tutorial was jointly developed by NVIDIA and the authors so users can better understand their method and explore how CUDA-Q removed barriers to scaling. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The code for this tutorial is based off the MIT licensed code found here: https://github.com/Boniface316/bigdata_vqa" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Clustering is a common unsupervised learning technique aimed at grouping data with similar characteristics. The unique properties of quantum computers could allow for enhanced pattern finding in clustering applications and enable more reliable data analysis. However, quantum computers today are severely limited by qubit count and noise. Performing practical clustering applications would require far too many qubits. The Edinburgh team developed a new method (extending the work of Harrow) to leverage coresets for clustering applications on quantum computers and use far fewer qubits. This tutorial will walk through an example using this approach for divisive clustering and emphasize the utility of CUDA-Q for scaling quantum simulations.\n", + "\n", + "The goal of divisive clustering is to begin with all data points as one set, and iteratively bipartition the data until each point is its own cluster. The branching behavior of this process can be used to understand similarities in the data points.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "# If you are running outside of a CUDA-Q container or CUDA-Q directory tree, you may need to uncomment these lines to fetch the files.\n", + "# If you are running inside a CUDA-Q tree, then this step can be skipped.\n", + "# !mkdir divisive_clustering_src\n", + "# !wget -P divisive_clustering_src https://raw.githubusercontent.com/NVIDIA/cuda-quantum/main/docs/sphinx/examples/python/tutorials/divisive_clustering_src/divisive_clustering.py\n", + "# !wget -P divisive_clustering_src https://raw.githubusercontent.com/NVIDIA/cuda-quantum/main/docs/sphinx/examples/python/tutorials/divisive_clustering_src/main_divisive_clustering.py" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Install the relevant packages.\n", + "!pip install mpi4py==3.1.6\n", + "!pip install networkx==2.8.8\n", + "!pip install pandas==2.2.2\n", + "!pip install scikit-learn==1.4.2\n", + "!pip install tqdm==4.66.2" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "import cudaq\n", + "from cudaq import spin\n", + "\n", + "\n", + "# Auxillary Imports\n", + "import os\n", + "import numpy as np\n", + "import networkx as nx\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "import warnings\n", + "from typing import Tuple\n", + "from divisive_clustering_src.divisive_clustering import Coreset, DivisiveClustering, Dendrogram, Voironi_Tessalation\n", + "\n", + "warnings.filterwarnings(\"ignore\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The settings below are global parameters for the quantum simulation and can be toggled by the user. " + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "circuit_depth = 1\n", + "max_iterations = 75\n", + "max_shots = 1000" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "\n", + "Given a data set $X = (x_1, x_2, \\cdots, x_N)$, a coreset is weighted data set of much smaller size ($X', w$) that represents $X$ enough such that analysis of ($X', w$) can allow us to draw reasonable approximate conclusions about $X$. There are various approaches to build coresets. They can be found in Practical Coreset Construction for Machine Learning (https://arxiv.org/pdf/1703.06476.pdf) and New Streaming Algorithms for Coresets in Machine Learning (https://arxiv.org/pdf/1703.06476.pdf).\n", + "\n", + "\n", + "Essentially, coreset construction boils down to finding the optimal coreset size and weights given some error tolerance. Given the constraints of a quantum computer, in this work, a coreset size is selected $a$ $priori$, and the error is determined for each model.\n", + "\n", + "\n", + "The following is an example $M=10$ coreset constructed from a 1000-point data set and loaded into a pandas data frame. See the image below where the coreset is represented by the black stars, the size of which corresponds to the weights.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Using BFL2 method to generate coresets\n", + " X Y weights Name\n", + "0 7.028364 1.669787 234.230716 A\n", + "1 7.167441 0.354792 101.319288 B\n", + "2 1.022889 -0.921443 125.158339 C\n", + "3 2.706134 -2.636852 13.650774 D\n", + "4 6.998497 0.455847 116.758239 E\n", + "5 7.507918 0.630311 120.727176 F\n", + "6 -2.102508 2.297727 53.294127 G\n", + "7 5.722463 1.400433 77.415840 H\n", + "8 -1.425868 2.341136 42.847985 I\n", + "9 7.985373 -0.063209 240.116237 J\n" + ] + } + ], + "source": [ + "raw_data = Coreset.create_dataset(1000)\n", + "coreset = Coreset(\n", + " raw_data=raw_data,\n", + " number_of_sampling_for_centroids=10,\n", + " coreset_size=10,\n", + " number_of_coresets_to_evaluate=4,\n", + " coreset_method=\"BFL2\",\n", + ")\n", + "\n", + "\n", + "coreset_vectors, coreset_weights = coreset.get_best_coresets()\n", + "\n", + "coreset_df = pd.DataFrame(\n", + " {\"X\": coreset_vectors[:, 0], \"Y\": coreset_vectors[:, 1], \"weights\": coreset_weights}\n", + ")\n", + "coreset_df[\"Name\"] = [chr(i + 65) for i in coreset_df.index]\n", + "print(coreset_df)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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3nzzTbRo0QK//fYbxo4dq7MdbD788EMUFxfj9u3bWLRoEQDA29sbAPDtt9/inXfewdChQzFp0iQ8evQI586dw7FjxzB69Gi9x547dy4EAgGmT5+O/Px8LF68GD179sTZs2fh4eEBAPj777/Rt29fJCQkYNasWRAKhVi9ejWeeeYZpKeno1OnTgCAN998E5s3b8bEiRMRHx+PwsJCHDp0CJcuXUKHDh10/hymOnPmDLy8vDT+JpVtO3PmDLp27cr6/nPnziExMRGurq54/fXXERkZievXr+OPP/7A3LlzAQAXL15EYmIiJBIJ3n//fbi6umLlypVISkrCgQMH8OSTT6odc/z48QgMDMTMmTNRXl4OAPj5558xduxY9O7dG59//jkqKiqwfPlydO3aFWfOnFEFeUOGDMHFixfx9ttvIzIyEvn5+di7dy9u3bql2ofLsfRdN6aYPXs25s2bh9deew2dOnVCSUkJTp48idOnT+PZZ5/V+d5Dhw4hNTUV48ePh4+PD7766isMGTIEt27dQkBAAADF/7M+ffogNDQUc+bMgUwmwyeffILAwECD2nn//n0Aimv3xo0bmD59OgICAvD8889r7FtaWqraX8nf318jSDFUWVkZysrK0LBhQ5OO41QY4lRWr17NAGD++usvpqCggMnJyWE2b97MBAYGMmKxmMnJyVHtW1NTw0ilUrX3P3jwgAkODmZeeeUV1bZNmzYxAJiMjAyGYRhm27ZtjFgsZgYMGMCMGDFCtV+bNm2YF154QWf7Fi9ezABgNm7cqNpWXl7ONG3alAHA7N+/X7W9oqJC4/3z5s1jBAIBk52drdo2YcIEhu1Pvf4xqqqqmFatWjHPPPOMznYacv6xY8cyAJhPPvlEbd/27dszCQkJqu9///13BgAzf/581baamhomMTGRAcCsXr1aZ3v279+v8Tvq168fExERobHvwIEDmZYtW+r5CdnP0bhxY6akpES1fePGjQwAZsmSJQzDMIxcLmdiY2OZ3r17M3K5XLVfRUUFExUVxTz77LOqbb6+vsyECRN0npft5+BC13v79evHREdHa2wvLy9nADAzZszQeexu3boxPj4+av+/GYZR+5kHDRrEuLm5MdevX1dtu3PnDuPj48N069ZNtU15bXbt2pWpqalRbS8tLWUaNGjAjBs3Tu0c9+7dY3x9fVXbHzx4wABg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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plt.scatter(raw_data[:, 0], raw_data[:, 1], label=\"Raw Data\", c=\"#7eba00\")\n", + "plt.scatter(\n", + " coreset_df[\"X\"],\n", + " coreset_df[\"Y\"],\n", + " s=coreset_df[\"weights\"],\n", + " label=\"Coreset\",\n", + " color=\"black\",\n", + " marker=\"*\",\n", + ")\n", + "plt.xlabel(\"X\")\n", + "plt.ylabel(\"Y\")\n", + "plt.title(\"Raw data and its best 10 coreset using BFL2\")\n", + "plt.legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Data preprocessing" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In order to cluster data on a quantum computer, the task needs to be cast into the form of a binary optimization problem. Each qubit represents a coreset point, and the quantum algorithm determines how to bipartition the coreset points at each iteration of the divisive clustering routine. \n", + "\n", + "The first step is to convert coreset points into a fully connected graph. The edge weight is calculated by:\n", + "\n", + "$e_{ij} = w_iw_jd_{ij}$ where $d_{ij}$ is the Euclidean distance between points $i$ and $j$. \n", + "\n", + "This process is handled by `Coreset.coreset_to_graph()`. The function returns a fully connected graph $G$ with edge weights." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Quantum functions" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The divisive clustering problem will be implemented on a quantum computer using a variational quantum algorithm (VQA) approach. A VQA takes a Hamiltonian (encoded with the optimization problem) and a parameterized ansatz and evaluates expectation values (quantum computer) that inform updates to the ansatz parameters (classical computer). The graph $G$ (Code in the \"src\" file) is used to construct the Hamiltonian, derived specifically for the divisive clustering problem, and motivated by a max-cut Hamiltonian. The `spin.z(i)` method in CUDA-Q adds a Pauli Z operation that acts on qubit $i$ to the Hamiltonian." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "def get_K2_Hamiltonian(G: nx.Graph) -> cudaq.SpinOperator:\n", + " \"\"\"Returns the K2 Hamiltonian for the given graph G\n", + "\n", + " Args:\n", + " G (nx.Graph): Weighted graph\n", + " \"\"\"\n", + " H = 0\n", + "\n", + " for i, j in G.edges():\n", + " weight = G[i][j][\"weight\"]\n", + " H += weight * (spin.z(i) * spin.z(j))\n", + "\n", + " return H" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The code below constructs a quantum kernel, defining the circuit which will serve as an ansatz. The structure of the circuit is a hardware efficient ansatz consisting of layers of parameterized $R_Z$ and $R_Y$ gate acting on each qubit, followed by a linear cascade of CNOT gates, and two more rotation gates.\n", + "\n", + "The `@cudaq.kernel` decorator allows us to define a quantum circuit in the new kernel mode syntax which provides performance benefits to JIT compilation." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "def get_VQE_circuit(number_of_qubits: int, circuit_depth: int) -> cudaq.Kernel:\n", + " \"\"\"Returns the VQE circuit for the given number of qubits and circuit depth\n", + "\n", + " Args:\n", + " number_of_qubits (int): Number of qubits\n", + " circuit_depth (int): Circuit depth\n", + "\n", + " Returns:\n", + " cudaq.Kernel: VQE Circuit\n", + " \"\"\"\n", + "\n", + " @cudaq.kernel\n", + " def kernel(thetas: list[float], number_of_qubits: int, circuit_depth: int):\n", + " \"\"\"VQE Circuit\n", + "\n", + " Args:\n", + " thetas (list[float]): List of parameters\n", + " number_of_qubits (int): Number of qubits\n", + " circuit_depth (int): Circuit depth\n", + " \"\"\"\n", + " qubits = cudaq.qvector(number_of_qubits)\n", + "\n", + " theta_position = 0\n", + "\n", + " for i in range(circuit_depth):\n", + " for j in range(number_of_qubits):\n", + " ry(thetas[theta_position], qubits[j])\n", + " rz(thetas[theta_position + 1], qubits[j])\n", + "\n", + " theta_position += 2\n", + "\n", + " for j in range(number_of_qubits - 1):\n", + " cx(qubits[j], qubits[j + 1])\n", + "\n", + " for j in range(number_of_qubits):\n", + " ry(thetas[theta_position], qubits[j])\n", + " rz(thetas[theta_position + 1], qubits[j])\n", + "\n", + " theta_position += 2\n", + "\n", + " return kernel" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can visualize the circuit using the `cudaq.draw()` method. Below, we are drawing the circuit for 5 qubits." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " ╭────────────╮ ╭────────────╮ ╭────────────╮╭────────────╮»\n", + "q0 : ┤ ry(0.8904) ├─┤ rz(0.7335) ├───●──┤ ry(0.4343) ├┤ rz(0.2236) ├»\n", + " ├────────────┤ ├────────────┤ ╭─┴─╮╰────────────╯├────────────┤»\n", + "q1 : ┤ ry(0.7937) ├─┤ rz(0.9981) ├─┤ x ├──────●───────┤ ry(0.3945) ├»\n", + " ├───────────┬╯ ├────────────┤ ╰───╯ ╭─┴─╮ ╰────────────╯»\n", + "q2 : ┤ ry(0.696) ├──┤ rz(0.3352) ├──────────┤ x ├───────────●───────»\n", + " ├───────────┴╮╭┴────────────┤ ╰───╯ ╭─┴─╮ »\n", + "q3 : ┤ ry(0.6658) ├┤ rz(0.05277) ├────────────────────────┤ x ├─────»\n", + " ├───────────┬╯├─────────────┴╮ ╰───╯ »\n", + "q4 : ┤ ry(0.791) ├─┤ rz(0.003569) ├─────────────────────────────────»\n", + " ╰───────────╯ ╰──────────────╯ »\n", + "\n", + "################################################################################\n", + "\n", + " \n", + "─────────────────────────────────────────────\n", + "╭────────────╮ \n", + "┤ rz(0.4119) ├───────────────────────────────\n", + "├────────────┤╭────────────╮ \n", + "┤ ry(0.3205) ├┤ rz(0.3504) ├─────────────────\n", + "╰────────────╯├────────────┤ ╭────────────╮ \n", + "──────●───────┤ ry(0.3913) ├─┤ rz(0.7392) ├──\n", + " ╭─┴─╮ ├────────────┤╭┴────────────┴─╮\n", + "────┤ x ├─────┤ ry(0.3171) ├┤ rz(0.0008056) ├\n", + " ╰───╯ ╰────────────╯╰───────────────╯\n", + "\n" + ] + } + ], + "source": [ + "parameter_count = 4 * circuit_depth * 5\n", + "parameters = np.random.rand(parameter_count)\n", + "\n", + "circuit = get_VQE_circuit(5, circuit_depth)\n", + "print(cudaq.draw(circuit, parameters, 5, circuit_depth))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The next step is to select a classical optimizer. There are multiple [optimizers](https://nvidia.github.io/cuda-quantum/latest/api/languages/python_api.html#optimizers) built-in to CUDA-Q that can be selected. The code below returns the optimizer with the proper number of initial parameters. " + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "def get_optimizer(\n", + " optimizer: cudaq.optimizers.optimizer, max_iterations, **kwargs\n", + ") -> Tuple[cudaq.optimizers.optimizer, int]:\n", + " \"\"\"Returns the optimizer with the given parameters\n", + "\n", + " Args:\n", + " optimizer (cudaq.optimizers.optimizer): Optimizer\n", + " max_iterations (int): Maximum number of iterations\n", + " **kwargs: Additional arguments\n", + "\n", + " Returns:\n", + " tuple(cudaq.optimizers.optimizer, int): Optimizer and parameter count\n", + " \"\"\"\n", + " parameter_count = 4 * kwargs[\"circuit_depth\"] * kwargs[\"qubits\"]\n", + " initial_params = np.random.uniform(-np.pi / 8.0, np.pi / 8.0, parameter_count)\n", + " optimizer.initial_parameters = initial_params\n", + "\n", + " optimizer.max_iterations = max_iterations\n", + " return optimizer, parameter_count" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Divisive Clustering Function" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `DivisiveClusteringVQA` class enables the procedure to iteratively bipartition the coreset points until each is its own cluster. If you wish to develop on top of this or see how the underlying code works, please see the `divisive_clustering.py` file in the src directory. \n", + "\n", + "`run_divisive_clustering`, takes the current iteration's coreset points that will be bipartitioned as inputs, extracts the appropriate weights, and builds a graph $G$. The graph is then an input into the `get_counts_from_simulation` function. \n", + "\n", + "\n", + "`get_counts_from_simulation` handles preparation and execution of the quantum simulation. First, it takes $G$ and from it builds a spin Hamiltonian. Second, it defines a cost function, which in this case is a lambda function that returns the expectation value of our parameterized quantum circuit and the Hamiltonian. This value is obtained using the CUDA-Q `observe` command, accelerated by GPUs. After the expectation value is minimized, the quantum circuit corresponding to the optimal parameters is sampled using the CUDA-Q `sample` function. The bitstrings and their associated counts are returned by `get_counts_from_simulation`.\n", + "\n", + "A subset of these counts is evaluated to compute their exact cost. The best bitstring is returned and later used to assign the coreset points to one of two clusters.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "class DivisiveClusteringVQA(DivisiveClustering):\n", + " def __init__(\n", + " self,\n", + " **kwargs,\n", + " ):\n", + " super().__init__(**kwargs)\n", + "\n", + " def run_divisive_clustering(\n", + " self,\n", + " coreset_vectors_df_for_iteration: pd.DataFrame,\n", + " ):\n", + " \"\"\"Runs the Divisive Clustering algorithm\n", + "\n", + " Args:\n", + " coreset_vectors_df_for_iteration (pd.DataFrame): Coreset vectors for the iteration\n", + "\n", + " Returns:\n", + " str: Best bitstring\n", + "\n", + " \"\"\"\n", + " coreset_vectors_for_iteration_np, coreset_weights_for_iteration_np = (\n", + " self._get_iteration_coreset_vectors_and_weights(coreset_vectors_df_for_iteration)\n", + " )\n", + "\n", + " G = Coreset.coreset_to_graph(\n", + " coreset_vectors_for_iteration_np,\n", + " coreset_weights_for_iteration_np,\n", + " metric=self.coreset_to_graph_metric,\n", + " )\n", + "\n", + " counts = self.get_counts_from_simulation(\n", + " G,\n", + " self.circuit_depth,\n", + " self.max_iterations,\n", + " self.max_shots,\n", + " )\n", + "\n", + " return self._get_best_bitstring(counts, G)\n", + "\n", + " def get_counts_from_simulation(\n", + " self, G: nx.graph, circuit_depth: int, max_iterations: int, max_shots: int\n", + " ) -> cudaq.SampleResult:\n", + " \"\"\"\n", + " Runs the VQA simulation\n", + "\n", + " Args:\n", + " G (nx.graph): Graph\n", + " circuit_depth (int): Circuit depth\n", + " max_iterations (int): Maximum number of iterations\n", + " max_shots (int): Maximum number of shots\n", + "\n", + " Returns:\n", + " cudaq.SampleResult: Measurement from the experiment\n", + " \"\"\"\n", + "\n", + " qubits = len(G.nodes)\n", + " Hamiltonian = self.create_Hamiltonian(G)\n", + " optimizer, parameter_count = self.optimizer_function(\n", + " self.optimizer, max_iterations, qubits=qubits, circuit_depth=circuit_depth\n", + " )\n", + "\n", + " kernel = self.create_circuit(qubits, circuit_depth)\n", + "\n", + " def objective_function(\n", + " parameter_vector: list[float],\n", + " hamiltonian: cudaq.SpinOperator = Hamiltonian,\n", + " kernel: cudaq.Kernel = kernel,\n", + " ) -> float:\n", + " \"\"\"\n", + "\n", + " Objective function that returns the cost of the simulation\n", + "\n", + " Args:\n", + " parameter_vector (List[float]):\n", + " hamiltonian (cudaq.SpinOperator): Circuit parameter values as a vector\n", + " kernel (cudaq.Kernel) : Circuit configuration\n", + "\n", + " Returns:\n", + " float: Expectation value of the circuit\n", + "\n", + " \"\"\"\n", + "\n", + " get_result = lambda parameter_vector: cudaq.observe(\n", + " kernel, hamiltonian, parameter_vector, qubits, circuit_depth\n", + " ).expectation()\n", + "\n", + " cost = get_result(parameter_vector)\n", + "\n", + " return cost\n", + "\n", + " energy, optimal_parameters = optimizer.optimize(\n", + " dimensions=parameter_count, function=objective_function\n", + " )\n", + "\n", + " counts = cudaq.sample(\n", + " kernel, optimal_parameters, qubits, circuit_depth, shots_count=max_shots\n", + " )\n", + "\n", + " return counts" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "An instance of the `DivisiveClusteringVQA` class is mostly constructed from variables previously discussed like the functions for building the Hamiltonians and quantum circuits. Parameters related to the quantum simulation can also be specified here such as `circuit_depth` and `max_shots`. The ` threshold_for_max_cut` parameter specifies what percent of the sample results from the quantum computer are checked for the best bitstring value.\n", + "\n", + "The other options specify advanced features like if the data is normalized and how the graph weights are computed.\n", + "\n", + "\n", + "Finally, the `get_divisive_sequence` method performs the iterations and produces the clustering data which we will analyze below. Note that this postprocessing code is not exposed in this tutorial but can be found in the source code. \n" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 129/129 [00:00<00:00, 12075.19it/s]\n", + "100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 12/12 [00:00<00:00, 35025.50it/s]\n", + "100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 18/18 [00:00<00:00, 44254.09it/s]\n", + "100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 15827.56it/s]\n", + "100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 13617.87it/s]\n" + ] + } + ], + "source": [ + "optimizer = cudaq.optimizers.COBYLA()\n", + "\n", + "divisive_clustering = DivisiveClusteringVQA(\n", + " circuit_depth=circuit_depth,\n", + " max_iterations=max_iterations,\n", + " max_shots=max_shots,\n", + " threshold_for_max_cut=0.75,\n", + " create_Hamiltonian=get_K2_Hamiltonian,\n", + " optimizer=optimizer,\n", + " optimizer_function=get_optimizer,\n", + " create_circuit=get_VQE_circuit,\n", + " normalize_vectors=True,\n", + " sort_by_descending=True,\n", + " coreset_to_graph_metric=\"dist\",\n", + ")\n", + "\n", + "hierarchial_clustering_sequence = divisive_clustering.get_divisive_sequence(coreset_df)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The data can be nicely visualized with a Dendrogram which maps where the bipartitionings occurred. Early splits generally mark divisions between the least similar data." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAy0AAANICAYAAADKKQDvAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/NK7nSAAAACXBIWXMAAA9hAAAPYQGoP6dpAAA9MElEQVR4nO3deXzU9Z348Xc4EsItCFUEOcQbRSueaAFLtQhatIpFLIqVWkUrtehP2vVAu6Xq6oquUnVV3AqLYLXrsWhVFrteVUHXo2qBB1i0CooSrhCOfH9/uJklJCABwnwgz+fjMQ+Yme985z1DSPLK90hBlmVZAAAAJKpevgcAAADYFNECAAAkTbQAAABJEy0AAEDSRAsAAJA00QIAACRNtAAAAEkTLQAAQNJECwAAkDTRAtQJnTp1inPPPTffY+zQnnrqqTjkkEOiUaNGUVBQEEuWLMn3SHVWQUFBXHvttfkeA2C7ES3ANjFhwoQoKCjIXRo1ahTt2rWLE088MW677bZYtmxZvkdkKyxevDgGDRoUxcXFcccdd8Tvfve7aNKkySYfM3fu3LjggguiS5cu0ahRo2jevHn07Nkzxo0bF6Wlpdtp8tqxcuXKuPbaa2PGjBn5HmW7ueWWW6KgoCCeffbZjS5zzz33REFBQTz22GO527Isi9/97nfxrW99K1q2bBmNGzeOgw46KH71q1/FypUrq6yjd+/elT6XrH/Zb7/9auW1AelrkO8BgJ3LddddF507d441a9bEp59+GjNmzIiRI0fGLbfcEo899lgcfPDB+R6RLfDaa6/FsmXL4vrrr4++fft+7fJPPvlknHHGGVFUVBRDhw6Nbt26xerVq+OFF16Iyy+/PN599924++67t8PktWPlypUxZsyYiPjqm+ztrbS0NBo02L5fwn/wgx/E5ZdfHpMmTdrox8CkSZOidevW0a9fv4iIWLduXZx11lkxZcqUOO644+Laa6+Nxo0bx3//93/HNddcE1OmTIlnn3022rZtW2k97du3j7Fjx1ZZf4sWLbb9CwN2CKIF2Kb69esXPXr0yF0fPXp0TJ8+PQYMGBCnnHJKvPfee1FcXJzHCTduxYoVX7v1YFtZtWpVFBYWRr16O8YG70WLFkVERMuWLb922Xnz5sUPfvCD6NixY0yfPj1233333H0jRoyIOXPmxJNPPrnVM2VZFqtWrUr246k2NWrUaLs/Z7t27aJPnz7xyCOPxPjx46OoqKjS/R9//HH86U9/ih//+MfRsGHDiIi48cYbY8qUKTFq1Ki46aabcsv++Mc/jkGDBsXAgQNj2LBhVT4eWrRoEWeffXbtvyhgh7FjfLUEdmjHH398XHXVVfHhhx/Ggw8+WOm+999/P04//fRo1apVNGrUKHr06FFp15KI/9v17MUXX4zLLrss2rRpE02aNIlTTz01Pvvss0rLZlkWv/rVr6J9+/bRuHHj6NOnT7z77rtVZqpY5/PPPx8XXXRRtG3bNtq3b5+7/84774wDDzwwioqKol27djFixIhqj+G44447okuXLlFcXBxHHHFE/Pd//3f07t270k/fZ8yYEQUFBTF58uT4h3/4h9hjjz2icePGsXTp0vjiiy9i1KhRcdBBB0XTpk2jefPm0a9fv/if//mfSs9TsY4pU6bEmDFjYo899ohmzZrF6aefHiUlJVFWVhYjR46Mtm3bRtOmTWPYsGFRVla2Wf8+U6dOjcMOOyyKi4tj1113jbPPPjs+/vjj3P29e/eOc845JyIiDj/88CgoKNjk8UE33nhjLF++PO69995KwVKha9eucemll+aur127Nq6//vrYa6+9oqioKDp16hS/+MUvqszfqVOnGDBgQDz99NPRo0ePKC4ujrvuuisiIpYsWRIjR46MDh06RFFRUXTt2jVuuOGGKC8vr7SOyZMnx2GHHRbNmjWL5s2bx0EHHRTjxo2rtMzXrWv+/PnRpk2biIgYM2ZMbtelTR1jcu2110ZBQUGV2ys+DufPn5+77fXXX48TTzwxdt111yguLo7OnTvHeeedV+lxGz5fxfrnzJkT5557brRs2TJatGgRw4YNq7ILVmlpafz0pz+NXXfdNZo1axannHJKfPzxx5t1nMzZZ58dJSUl1Ubn5MmTo7y8PIYMGZJ7nptuuin22WefareanHzyyXHOOefEf/7nf8arr766yecFsKUF2C5++MMfxi9+8Yv44x//GMOHD4+IiHfffTd69uwZe+yxR1x55ZXRpEmTmDJlSgwcODB+//vfx6mnnlppHZdccknssssucc0118T8+fPj1ltvjYsvvjgeeuih3DJXX311/OpXv4qTTjopTjrppJg1a1accMIJsXr16mrnuuiii6JNmzZx9dVXx4oVKyLiq28Ax4wZE3379o0L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mDHxz//8z9G7d+8YOHBgnHXWWVUOfK/Opv6PNG/ePPd+b/g+bvj+b0q/fv2iVatWMWnSpNyxTf/+7/8e3bt3jwMPPDC33MaCZH0V92149rAmTZps1lnqgLpDtADbxUcffRQlJSW5b44qDmoeNWpUlZ+cV9jwG6n1t4isL8uyLZ5re555qrrn+vWvfx1XXXVVnHfeeXH99ddHq1atol69ejFy5MgqB5FHbPw9qI33Zks0b9482rVrF++8806NHlfdQerVqe49LC8vj+985ztxxRVXVPuYffbZJyK++sb4zTffjKeffjqmTZsW06ZNi/vvvz+GDh0aDzzwQI3WVVMbe33rR2rFcg8//HC88sor8fjjj8fTTz8d5513Xtx8883xyiuvRNOmTTf5PNvj46Bhw4YxaNCguOeee2LhwoXxt7/9LWbPnh033nhjpeUOOOCAiIh46623YuDAgdWu66233oqIiC5dumyz+YCdk2gBtovf/e53ERG5QKn4JqVhw4bb7CeqHTt2jIivflq+/jdBn332WW5rzOau44MPPqi0jtWrV8e8efNys1YsN2fOnOjTp09uubVr18b8+fM3+6Dshx9+OPr06RP33ntvpduXLFkSu+6662atY0ut/1qPP/74Svd98MEHuftrasCAAXH33XfHyy+/HEcfffTXzlBeXh6zZ8+O/fffP3f7woULY8mSJZs1w1577RXLly/frI+jwsLCOPnkk+Pkk0+O8vLyuOiii+Kuu+6Kq666Krp27brZ69rcyKpQscVjyZIllU4bveGWpApHHXVUHHXUUfGP//iPMWnSpBgyZEhMnjw5zj///Bo974Yq3u958+ZV2iI5Z86cGq1nyJAh8dvf/jYeeuihmDdvXhQUFMTgwYMrLdOzZ89o2bJlTJo0KX75y19WG1T/9m//FhERZ5xxxha8GqAucUwLUOumT58e119/fXTu3Dm3z3vbtm2jd+/ecdddd8Unn3xS5TEbnsp4c/Tt2zcaNmwYt99+e6WfLFecPWpz11FYWBi33XZbpXXce++9UVJSkjvLUo8ePaJ169Zxzz33VDpuZOLEiZsdSBFf/WR8w5+CT506tcrxPLWhR48e0bZt2/jtb39b6fTC06ZNi/fee2+zzyi1oSuuuCKaNGkS559/fixcuLDK/XPnzs2dZvikk06KiKr/RrfccktExGbNMGjQoHj55Zfj6aefrnLfkiVLcv8+ixcvrnRfvXr1cnFZ8fo3d12NGzfO3bY5Ks62tv4xMStWrMht4anw5ZdfVvl4OOSQQyrNuDUqfmhw5513Vrr99ttvr9F6evbsGZ06dYoHH3wwHnrooejVq1eVM+Y1btw4rrjiivjggw/il7/8ZZV1PPnkkzFhwoQ4+eST46CDDqrhKwHqGltagG1q2rRp8f7778fatWtj4cKFMX369HjmmWeiY8eO8dhjj1U64PuOO+6IY489Ng466KAYPnx4dOnSJRYuXBgvv/xyfPTRR1V+V8nXadOmTYwaNSrGjh0bAwYMiJNOOineeOONmDZt2mZvtWjTpk2MHj06xowZE9/97nfjlFNOiQ8++CDuvPPOOPzww3O/8K6wsDCuvfbauOSSS+L444+PQYMGxfz582PChAmx1157bfZP4gcMGBDXXXddDBs2LI455ph4++23Y+LEidtld5mGDRvGDTfcEMOGDYtevXrF4MGDc6c87tSpU/zsZz/bovXutddeMWnSpDjzzDNj//33j6FDh0a3bt1i9erV8dJLL8XUqVNzx0J07949zjnnnLj77rtjyZIl0atXr3j11VfjgQceiIEDB1bairUxl19+eTz22GMxYMCAOPfcc+Owww6LFStWxNtvvx0PP/xwzJ8/P3bdddc4//zz44svvojjjz8+2rdvHx9++GHcfvvtccghh+S28mzuuoqLi+OAAw6Ihx56KPbZZ59o1apVdOvWLbp161btjCeccELsueee8aMf/Sguv/zyqF+/ftx3333Rpk2b+Nvf/pZb7oEHHog777wzTj311Nhrr71i2bJlcc8990Tz5s1zgbc1DjvssPj+978ft956ayxevDh3yuO//vWvEbH5W5AKCgrirLPOil//+tcREXHddddVu9wVV1wRb775Ztxwww3x8ssvx/e///0oLi6OF154IR588ME48MADY8KECVUeV1JSUuV3OlXwSyehjsrbecuAnUrFqVsrLoWFhdluu+2Wfec738nGjRuXLV26tNrHzZ07Nxs6dGi22267ZQ0bNsz22GOPbMCAAdnDDz9cZd0bnga24jTA//Vf/5W7bd26ddmYMWOy3XffPSsuLs569+6dvfPOO1nHjh2rPeXxxk4t+y//8i/ZfvvtlzVs2DD7xje+kV144YXZl19+WWW52267LevYsWNWVFSUHXHEEdmLL76YHXbYYdl3v/vdKnNOnTq1yuNXrVqV/fznP8/N27Nnz+zll1/OevXqVe1pkzdcx8ZeR8UpcD/77LNqX9/6HnrooezQQw/NioqKslatWmVDhgzJPvroo816nk3561//mg0fPjzr1KlTVlhYmDVr1izr2bNndvvtt1c6zfWaNWuyMWPGZJ07d84aNmyYdejQIRs9enSlZbLsq1Me9+/fv9rnWrZsWTZ69Oisa9euWWFhYbbrrrtmxxxzTPZP//RPuVNEP/zww9kJJ5yQtW3bNissLMz23HPP7IILLsg++eSTGq8ry7LspZdeyg477LCssLBws04XPHPmzOzII4/MPfctt9xS5ZTHs2bNygYPHpztueeeWVFRUda2bdtswIAB2euvv15pXRs+38b+vTdcf5Zl2YoVK7IRI0ZkrVq1ypo2bZoNHDgw++CDD7KIyH7zm99s8jWs7913380iIisqKqr2/0aF8vLybMKECVnPnj2zZs2a5T5H9O3bNysrK6uy/KZOeezbFqi7CrJsOx+lCbATKy8vjzZt2sRpp50W99xzT77Hgc3y5ptvxqGHHhoPPvhgpdMW14Y1a9bEySefHM8991w8/vjjlc7eBrAxjmkB2EKrVq2qcvzBv/3bv8UXX3wRvXv3zs9Q8DVKS0ur3HbrrbdGvXr14lvf+latP3/Dhg3j97//fRxyyCFxxhlnVPv7iAA2ZEsLwBaaMWNG/OxnP4szzjgjWrduHbNmzYp777039t9//5g5c2YUFhbme0SoYsyYMTFz5szo06dPNGjQIHf65x//+Mdx11135Xs8gGqJFoAtNH/+/PjpT38ar776anzxxRfRqlWrOOmkk+I3v/lNlV+WB6l45plnYsyYMfGXv/wlli9fHnvuuWf88Ic/jF/+8pfRoIHz8wBpEi0AAEDSHNMCAAAkTbQAAABJ2+47r5aXl8ff//73aNas2Wb/EisAAGDnk2VZLFu2LNq1axf16m18e8p2j5a///3v0aFDh+39tAAAQKIWLFgQ7du33+j92z1amjVrFhFfDda8efPt/fQAAEAili5dGh06dMg1wsZs92ip2CWsefPmogUAAPjaw0YciA8AACRNtAAAAEkTLQAAQNJECwAAkDTRAgAAJE20AAAASRMtAABA0kQLAACQNNECAAAkTbQAAABJEy0AAEDSRAsAAJA00QIAACRNtAAAAEkTLQAAQNJECwAAkDTRAgAAJE20AAAASRMtAABA0kQLAACQNNECAAAkTbQAAABJEy0AAEDSRAsAAJA00QIAACRNtAAAAEkTLQAAQNJECwAAkDTRAgAAJE20AAAASRMtAABA0kQLAACQtAb5HgDyKcuyKF2zLt9jAMB2U9ywfhQUFOR7DKgR0UKdlWVZnP7bl2Pmh1/mexQA2G56dNwlpv7kaOHCDsXuYdRZpWvWCRYA6pzXP/zSXgbscGxpgYh4/R/6RuPC+vkeAwBqzcrV66LHr57N9xiwRUQLRETjwvrRuNB/BwCAFNk9DAAASJpoAQAAkiZaAACApIkWAAAgaaIFAABImmgBAACSJloAAICkiRYAACBpogUAAEiaaAEAAJImWgAAgKSJFgAAIGmiBQAASJpoAQAAkiZaAACApIkWAAAgaaIFAABImmgBAACSJloAAICkiRYAACBpogUAAEiaaAEAAJImWgAAgKSJFgAAIGmiBQAASJpoAQAAkiZaAACApIkWAAAgaaIFAABImmgBAACSJloAAICkiRYAACBpogUAAEiaaAEAAJImWgAAgKSJFgAAIGmiBQAASJpoAQAAkiZaAACApIkWAAAgaaIFAABImmgBAACSJloAAICkiRYAACBpogUAAEiaaAEAAJImWgAAgKSJFgAAIGmiBQAASJpoAQAAkiZaAACApIkWAAAgaaIFAABImmgBAACSJloAAICkiRYAACBpogUAAEiaaAEAAJImWgAAgKSJFgAAIGmiBQAASJpoAQAAkiZaAACApIkWAAAgaaIFAABImmgBAACSJloAAICkiRYAACBpogUAAEiaaAEAAJJWo2i59tpro6CgoNJlv/32q63ZAAAAokFNH3DggQfGs88++38raFDjVQAAAGy2GhdHgwYNYrfddquNWQAAAKqo8TEts2fPjnbt2kWXLl1iyJAh8be//a025gIAAIiIGm5pOfLII2PChAmx7777xieffBJjxoyJ4447Lt55551o1qxZtY8pKyuLsrKy3PWlS5du3cQAAECdUqNo6devX+7vBx98cBx55JHRsWPHmDJlSvzoRz+q9jFjx46NMWPGbN2UAABAnbVVpzxu2bJl7LPPPjFnzpyNLjN69OgoKSnJXRYsWLA1TwkAANQxWxUty5cvj7lz58buu+++0WWKioqiefPmlS4AAACbq0bRMmrUqHj++edj/vz58dJLL8Wpp54a9evXj8GDB9fWfAAAQB1Xo2NaPvrooxg8eHAsXrw42rRpE8cee2y88sor0aZNm9qaDwAAqONqFC2TJ0+urTkAAACqtVXHtAAAANQ20QIAACRNtAAAAEkTLQAAQNJECwAAkDTRAgAAJE20AAAASRMtAABA0kQLAACQNNECAAAkTbQAAABJEy0AAEDSRAsAAJA00QIAACRNtAAAAEkTLQAAQNJECwAAkDTRAgAAJE20AAAASRMtAABA0kQLAACQNNECAAAkTbQAAABJEy0AAEDSRAsAAJA00QIAACRNtAAAAEkTLQAAQNJECwAAkDTRAgAAJE20AAAASRMtAABA0kQLAACQNNECAAAkTbQAAABJEy0AAEDSRAsAAJA00QIAACRNtAAAAEkTLQAAQNJECwAAkDTRAgAAJE20AAAASRMtAABA0kQLAACQNNECAAAkTbQAAABJEy0AAEDSRAsAAJA00QIAACRNtAAAAEkTLQAAQNJECwAAkLQG+R6Ar5dlWZSuWZfvMXY6K1evrfbvbDvFDetHQUFBvscAAHZwoiVxWZbF6b99OWZ++GW+R9mp9fjVc/keYafUo+MuMfUnRwsXAGCr2D0scaVr1gkWdlivf/ilrYQAwFazpWUH8vo/9I3GhfXzPQZ8rZWr10WPXz2b7zEAgJ2EaNmBNC6sH40L/ZMBAFC32D0MAABImmgBAACSJloAAICkiRYAACBpogUAAEiaaAEAAJImWgAAgKSJFgAAIGmiBQAASJpoAQAAkiZaAACApIkWAAAgaaIFAABImmgBAACSJloAAICkiRYAACBpogUAAEiaaAEAAJImWgAAgKSJFgAAIGmiBQAASJpoAQAAkiZaAACApIkWAAAgaaIFAABImmgBAACSJloAAICkiRYAACBpogUAAEiaaAEAAJImWgAAgKSJFgAAIGmiBQAASJpoAQAAkiZaAACApIkWAAAgaaIFAABImmgBAACSJloAAICkiRYAACBpogUAAEiaaAEAAJImWgAAgKSJFgAAIGmiBQAASJpoAQAAkiZaAACApIkWAAAgaaIFAABImmgBAACSJloAAICkiRYAACBpogUAAEiaaAEAAJImWgAAgKSJFgAAIGmiBQAASJpoAQAAkiZaAACApIkWAAAgaaIFAABImmgBAACStlXR8pvf/CYKCgpi5MiR22gcAACAyrY4Wl577bW466674uCDD96W8wAAAFSyRdGyfPnyGDJkSNxzzz2xyy67bOuZAAAAcrYoWkaMGBH9+/ePvn37but5AAAAKmlQ0wdMnjw5Zs2aFa+99tpmLV9WVhZlZWW560uXLq3pUwIAAHVYjba0LFiwIC699NKYOHFiNGrUaLMeM3bs2GjRokXu0qFDhy0aFAAAqJtqFC0zZ86MRYsWxTe/+c1o0KBBNGjQIJ5//vm47bbbokGDBrFu3boqjxk9enSUlJTkLgsWLNhmwwMAADu/Gu0e9u1vfzvefvvtSrcNGzYs9ttvv/h//+//Rf369as8pqioKIqKirZuSgAAoM6qUbQ0a9YsunXrVum2Jk2aROvWravcDgAAsC1s1S+XBAAAqG01PnvYhmbMmLENxgAAAKieLS0AAEDSRAsAAJA00QIAACRNtAAAAEkTLQAAQNJECwAAkDTRAgAAJE20AAAASRMtAABA0kQLAACQNNECAAAkTbQAAABJEy0AAEDSRAsAAJA00QIAACRNtAAAAEkTLQAAQNJECwAAkDTRAgAAJE20AAAASRMtAABA0kQLAACQNNECAAAkTbQAAABJEy0AAEDSRAsAAJA00QIAACRNtAAAAEkTLQAAQNJECwAAkDTRAgAAJE20AAAASRMtAABA0kQLAACQNNECAAAkTbQAAABJEy0AAEDSRAsAAJA00QIAACRNtAAAAEkTLQAAQNJECwAAkDTRAgAAJE20AAAASRMtAABA0kQLAACQNNECAAAkTbQAAABJEy0AAEDSRAsAAJA00QIAACRNtAAAAEkTLQAAQNJECwAAkDTRAgAAJE20AAAASRMtAABA0kQLAACQNNECAAAkTbQAAABJEy0AAEDSRAsAAJA00QIAACRNtAAAAEkTLQAAQNJECwAAkDTRAgAAJE20AAAASRMtAABA0kQLAACQNNECAAAkTbQAAABJEy0AAEDSRAsAAJA00QIAACRNtAAAAEkTLQAAQNJECwAAkDTRAgAAJE20AAAASRMtAABA0kQLAACQNNECAAAkTbQAAABJEy0AAEDSRAsAAJA00QIAACRNtAAAAEkTLQAAQNJECwAAkDTRAgAAJE20AAAASRMtAABA0kQLAACQNNECAAAkTbQAAABJEy0AAEDSRAsAAJA00QIAACRNtAAAAEkTLQAAQNJECwAAkDTRAgAAJE20AAAASRMtAABA0kQLAACQNNECAAAkTbQAAABJEy0AAEDSRAsAAJA00QIAACRNtAAAAEkTLQAAQNJECwAAkDTRAgAAJE20AAAASRMtAABA0kQLAACQNNECAAAkTbQAAABJq1G0jB8/Pg4++OBo3rx5NG/ePI4++uiYNm1abc0GAABQs2hp3759/OY3v4mZM2fG66+/Hscff3x873vfi3fffbe25gMAAOq4BjVZ+OSTT650/R//8R9j/Pjx8corr8SBBx64TQcDAACIqGG0rG/dunUxderUWLFiRRx99NHbciYAAICcGkfL22+/HUcffXSsWrUqmjZtGo8++mgccMABG12+rKwsysrKcteXLl26ZZMCAAB1Uo3PHrbvvvvGm2++GX/+85/jwgsvjHPOOSf+8pe/bHT5sWPHRosWLXKXDh06bNXAAABA3VLjaCksLIyuXbvGYYcdFmPHjo3u3bvHuHHjNrr86NGjo6SkJHdZsGDBVg0MAADULVt8TEuF8vLySrt/baioqCiKioq29mkAAIA6qkbRMnr06OjXr1/sueeesWzZspg0aVLMmDEjnn766dqaDwAAqONqFC2LFi2KoUOHxieffBItWrSIgw8+OJ5++un4zne+U1vzAQAAdVyNouXee++trTkAAACqVeMD8QEAALYn0QIAACRtq88eBtRMlmVRurY032PUqpVr1q3399KIgvp5nGb7KG5QHAUFBfkeAwB2SqIFtqMsy2LotKHx5mdv5nuUWpWVN4yI6yMioveUXlFQb01+B9oODm17aDzw3QeECwDUAtEC21Hp2tKdPlgiIgrqrYlm+1+Z7zG2qzcWvRGla0ujccPG+R4FAHY6ogXyZMagGVHcoDjfY7CVSteWRu8pvfM9BgDs1EQL5Elxg2I/lQcA2AzOHgYAACRNtAAAAEkTLQAAQNJECwAAkDTRAgAAJE20AAAASRMtAABA0kQLAACQNNECAAAkTbQAAABJEy0AAEDSRAsAAJA00QIAACRNtAAAAEkTLQAAQNJECwAAkDTRAgAAJE20AAAASRMtAABA0kQLAACQNNECAAAkTbQAAABJEy0AAEDSRAsAAJA00QIAACRNtAAAAEkTLQAAQNJECwAAkDTRAgAAJE20AAAASRMtAABA0kQLAACQNNECAAAkTbQAAABJEy0AAEDSRAsAAJA00QIAACRNtAAAAEkTLQAAQNJECwAAkDTRAgAAJE20AAAASRMtAABA0kQLAACQNNECAAAkTbQAAABJEy0AAEDSRAsAAJA00QIAACRNtAAAAEkTLQAAQNJECwAAkDTRAgAAJE20AAAASRMtAABA0kQLAACQNNECAAAkTbQAAABJEy0AAEDSRAsAAJA00QIAACRNtAAAAEkTLQAAQNJECwAAkDTRAgAAJE20AAAASRMtAABA0kQLAACQNNECAAAkTbQAAABJEy0AAEDSRAsAAJA00QIAACRNtAAAAEkTLQAAQNJECwAAkDTRAgAAJE20AAAASRMtAABA0kQLAACQNNECAAAkTbQAAABJEy0AAEDSRAsAAJA00QIAACRNtAAAAEkTLQAAQNJECwAAkDTRAgAAJE20AAAASRMtAABA0kQLAACQNNECAAAkTbQAAABJEy0AAEDSRAsAAJA00QIAACRNtAAAAEkTLQAAQNJECwAAkDTRAgAAJE20AAAASRMtAABA0kQLAACQNNECAAAkTbQAAABJEy0AAEDSRAsAAJA00QIAACRNtAAAAEkTLQAAQNJECwAAkDTRAgAAJE20AAAASatRtIwdOzYOP/zwaNasWbRt2zYGDhwYH3zwQW3NBgAAULNoef7552PEiBHxyiuvxDPPPBNr1qyJE044IVasWFFb8wEAAHVcg5os/NRTT1W6PmHChGjbtm3MnDkzvvWtb23TwQAAACJqGC0bKikpiYiIVq1abXSZsrKyKCsry11funTp1jwlAABQx2zxgfjl5eUxcuTI6NmzZ3Tr1m2jy40dOzZatGiRu3To0GFLnxIAAKiDtjhaRowYEe+8805Mnjx5k8uNHj06SkpKcpcFCxZs6VMCAAB10BbtHnbxxRfHE088EX/605+iffv2m1y2qKgoioqKtmg4AACAGkVLlmVxySWXxKOPPhozZsyIzp0719ZcAAAAEVHDaBkxYkRMmjQp/uM//iOaNWsWn376aUREtGjRIoqLi2tlQAAAoG6r0TEt48ePj5KSkujdu3fsvvvuuctDDz1UW/MBAAB1XI13DwPIhyzLonRtab7HqGL9mVKcr7hBcRQUFOR7DADYKlv1e1oAtocsy2LotKHx5mdv5nuUTeo9pXe+R6ji0LaHxgPffUC4ALBD2+JTHgNsL6VrS5MPllS9seiNJLcAAUBN2NIC7FBmDJoRxQ2c+OPrlK4tTXLLDwBsCdEC7FCKGxRH44aN8z0GALAd2T0MAABImmgBAACSJloAAICkiRYAACBpogUAAEiaaAEAAJImWgAAgKSJFgAAIGmiBQAASJpoAQAAkiZaAACApIkWAAAgaaIFAABImmgBAACSJloAAICkiRYAACBpogUAAEiaaAEAAJImWgAAgKSJFgAAIGmiBQAASJpoAQAAkiZaAACApIkWAAAgaaIFAABImmgBAACSJloAAICkiRYAACBpogUAAEiaaAEAAJImWgAAgKSJFgAAIGmiBQAASJpoAQAAkiZaAACApIkWAAAgaaIFAABImmgBAACS1iDfAyQjyyLWrMz3FFWtXrfe31dGRP28jbJRDRtHFBTkewoAAHZSoiXiq2C578SIBX/O9yRVZUURcf9Xf7+pa0RBWV7HqVaHoyLOe0q4AABQK0RLxFdbWFIMlohoXFAW8xudle8xNm3BK1+9h4VN8j0JAAA7IdGyoVFzIgob53uKHcPqlRH/1DXfUwAAsJMTLRsqbGyLAQAAJMTZwwAAgKSJFgAAIGmiBQAASJpoAQAAkiZaAACApIkWAAAgaaIFAABImmgBAACSJloAAICkiRYAACBpogUAAEiaaAEAAJImWgAAgKSJFgAAIGmiBQAASJpoAQAAkiZaAACApIkWAAAgaaIFAABImmgBAACSJloAAICkiRYAACBpogUAAEiaaAEAAJImWgAAgKSJFgAAIGmiBQAASJpoAQAAkiZaAACApIkWAAAgaaIFAABImmgBAACSJloAAICkiRYAACBpogUAAEiaaAEAAJImWgAAgKSJFgAAIGmiBQAASJpoAQAAkiZaAACApIkWAAAgaaIFAABImmgBAACSJloAAICkiRYAACBpogUAAEiaaAEAAJImWgAAgKSJFgAAIGmiBQAASJpoAQAAkiZaAACApIkWAAAgaaIFAABImmgBAACSJloAAICkiRYAACBpogUAAEiaaAEAAJImWgAAgKSJFgAAIGmiBQAASJpoAQAAkiZaAACApIkWAAAgaaIFAABIWoN8DwAAsL4syyIrLc33GDud8tXr/u/vK0ujfG39PE6zcyooLo6CgoJ8j7FTEi0AQDKyLIsPzxoSpW+8ke9Rdjqr6hdGnPzriIiY3fPYaLRudZ4n2vkUf/Ob0XHig8KlFogWACAZWWmpYKkljdatjml/GJXvMXZqpbNmRVZaGgWNG+d7lJ2OaAEAkrT3iy9EveLifI8BX6u8tDRm9zw232Ps1EQLAJCkesXFUc9PrIFw9jAAACBxNY6WP/3pT3HyySdHu3btoqCgIP7whz/UwlgAAABfqXG0rFixIrp37x533HFHbcwDAABQSY2PaenXr1/069evNmYBAACootYPxC8rK4uysrLc9aVLl9b2UwIAADuRWj8Qf+zYsdGiRYvcpUOHDrX9lAAAwE6k1qNl9OjRUVJSkrssWLCgtp8SAADYidT67mFFRUVRVFRU208DAADspPyeFgAAIGk13tKyfPnymDNnTu76vHnz4s0334xWrVrFnnvuuU2HAwAAqHG0vP7669GnT5/c9csuuywiIs4555yYMGHCNhsMAAAgYguipXfv3pFlWW3MAgAAUIVjWgAAgKTV+tnDAABgW8iyLLLS0nyPUUX5ejOVJzhfRERBcXEUFBTke4wtJloAAEhelmXx4VlDovSNN/I9yibN7nlsvkeoVvE3vxkdJz64w4aL3cMAAEheVlqafLCkrHTWrCS3Um0uW1oAANih7P3iC1GvuDjfY+wQyktLk936UxOiBQCAHUq94uKo17hxvsdgO7J7GAAAkDTRAgAAJE20AAAASRMtAABA0kQLAACQNNECAAAkTbQAAABJEy0AAEDSRAsAAJA00QIAACRNtAAAAEkTLQAAQNJECwAAkDTRAgAAJE20AAAASRMtAABA0kQLAACQNNECAAAkTbQAAABJEy0AAEDSRAsAAJA00QIAACRNtAAAAEkTLQAAQNJECwAAkDTRAgAAJE20AAAASRMtAABA0kQLAACQNNECAAAkTbQAAABJEy0AAEDSRAsAAJA00QIAACRNtAAAAEkTLQAAQNJECwAAkDTRAgAAJE20AAAASRMtAABA0kQLAACQNNECAAAkTbQAAABJEy0AAEDSRAsAAJA00QIAACRNtAAAAEkTLQAAQNJECwAAkDTRAgAAJE20AAAASRMtAABA0kQLAACQNNECAAAkTbQAAABJEy0AAEDSRAsAAJA00QIAACRNtAAAAEkTLQAAQNJECwAAkDTRAgAAJE20AAAASRMtAABA0kQLAACQNNECAAAkTbQAAABJEy0AAEDSRAsAAJA00QIAACRNtAAAAEkTLQAAQNJECwAAkDTRAgAAJE20AAAASRMtAABA0kQLAACQNNECAAAkTbQAAABJEy0AAEDSRAsAAJA00QIAACRNtAAAAEkTLQAAQNJECwAAkDTRAgAAJE20AAAASRMtAABA0kQLAACQNNECAAAkTbQAAABJEy0AAEDSRAsAAJA00QIAACRNtAAAAEkTLQAAQNJECwAAkDTRAgAAJE20AAAASWuQ7wEAgM2TZVlkpaX5HqNWla/3+sp38tcaEVFQXBwFBQX5HgOSJ1oAYAeQZVl8eNaQKH3jjXyPst3M7nlsvkeodcXf/GZ0nPigcIGvYfcwANgBZKWldSpY6orSWbN2+q1nsC3Y0gIAO5i9X3wh6hUX53sMtkJ5aWmd2JIE24poAYAdTL3i4qjXuHG+xwDYbuweBgAAJE20AAAASRMtAABA0kQLAACQNNECAAAkTbQAAABJc8pjAFhPlmVJ/rK/8vVmKk9wvoLiYr/VHag1ogUA/leWZfHhWUOS/83zKf5SwuJvfjM6TnxQuAC1wu5hAPC/stLS5IMlVaWzZiW5hQrYOdjSAgDV2PvFF6JecXG+x0heeWlpklt+gJ3LFkXLHXfcETfddFN8+umn0b1797j99tvjiCOO2NazAUDe1CsujnqNG+d7DABiC3YPe+ihh+Kyyy6La665JmbNmhXdu3ePE088MRYtWlQb8wEAAHVcjaPllltuieHDh8ewYcPigAMOiN/+9rfRuHHjuO+++2pjPgAAoI6r0e5hq1evjpkzZ8bo0aNzt9WrVy/69u0bL7/8crWPKSsri7Kystz1kpKSiIhYunTplsxbO1aviCjLvvr70qURhevyO8+OwvtWYyvXrIx1pV+9T0uXLo21DdfmeaIdg/et5rxnW6Z85cpYvu7/3rd6a71vX8d7tmW8bzXnPdsyqb9vFU2QZdmmF8xq4OOPP84iInvppZcq3X755ZdnRxxxRLWPueaaa7KIcHFxcXFxcXFxcXFxqfayYMGCTXZIrZ89bPTo0XHZZZflrpeXl8cXX3wRrVu3di53AACow7Isi2XLlkW7du02uVyNomXXXXeN+vXrx8KFCyvdvnDhwthtt92qfUxRUVEUFRVVuq1ly5Y1eVoAAGAn1aJFi69dpkYH4hcWFsZhhx0Wzz33XO628vLyeO655+Loo4+u+YQAAABfo8a7h1122WVxzjnnRI8ePeKII46IW2+9NVasWBHDhg2rjfkAAIA6rsbRcuaZZ8Znn30WV199dXz66adxyCGHxFNPPRXf+MY3amM+AACgjivIvvb8YgAAAPlT418uCQAAsD2JFgAAIGmiBQAASJpoAQAAkiZa/tedd94ZBQUFceSRR+Z7lORNmDAhCgoKKl3atm0bffr0iWnTpuV7vKTNnTs3LrjggujSpUs0atQomjdvHj179oxx48ZFaWlpvsdLSsXH2euvv17t/b17945u3bpt56l2PF/3PvJ/qvvcVnG58sor8z1e0nyc1cy8efPi4osvjn322ScaN24cjRs3jgMOOCBGjBgRb731Vr7HS86G/zcbNWoU7dq1ixNPPDFuu+22WLZsWb5HTNKmPqcVFBTEK6+8ku8Ra6TGpzzeWU2cODE6deoUr776asyZMye6du2a75GSd91110Xnzp0jy7JYuHBhTJgwIU466aR4/PHHY8CAAfkeLzlPPvlknHHGGVFUVBRDhw6Nbt26xerVq+OFF16Iyy+/PN599924++678z0m1HkVn9vWJ5DZVp544ok488wzo0GDBjFkyJDo3r171KtXL95///145JFHYvz48TFv3rzo2LFjvkdNTsX/zTVr1sSnn34aM2bMiJEjR8Ytt9wSjz32WBx88MH5HjFJ1X1Oi4gd7ntd0RJf/cTjpZdeikceeSQuuOCCmDhxYlxzzTX5Hit5/fr1ix49euSu/+hHP4pvfOMb8e///u+iZQPz5s2LH/zgB9GxY8eYPn167L777rn7RowYEXPmzIknn3wyjxMCFTb83Abbyty5c3NfC5577rlKXwsiIm644Ya48847o149O8JUZ8P/m6NHj47p06fHgAED4pRTTon33nsviouL8zhhmnaWz2n+V8RXW1l22WWX6N+/f5x++ukxceLEfI+0Q2rZsmUUFxdHgwZaeEM33nhjLF++PO69994qX6Qivvppx6WXXpqHyQDYXm688cZYsWJF3H///dV+LWjQoEH89Kc/jQ4dOuRhuh3T8ccfH1dddVV8+OGH8eCDD+Z7HGqRaImvouW0006LwsLCGDx4cMyePTtee+21fI+VvJKSkvj888/js88+i3fffTcuvPDCWL58eZx99tn5Hi05jz/+eHTp0iWOOeaYfI+yw6n4ONvwsmbNmnyPxk6quo852BaeeOKJ6Nq1q+Nnt7Ef/vCHERHxxz/+Mc+TpKm6z2mLFy/O91g1Vud/JD5z5sx4//334/bbb4+IiGOPPTbat28fEydOjMMPPzzP06Wtb9++la4XFRXFfffdF9/5znfyNFGali5dGh9//HF873vfy/coO6QNP87Wd+CBB27HSagrqvuYy7IsD5OwM1m6dGn8/e9/j4EDB1a5b8mSJbF27drc9SZNmtjNqQbat28fLVq0iLlz5+Z7lCRV9zmtqKgoVq1alYdptlydj5aJEyfGN77xjejTp09ERBQUFMSZZ54ZDz74YNx8881Rv379PE+YrjvuuCP22WefiIhYuHBhPPjgg3H++edHs2bN4rTTTsvzdOlYunRpREQ0a9Ysz5PsmNb/OFvfz3/+81i3bl0eJmJnt7GPOdgaFV8LmjZtWuW+3r17x//8z//krt90000xatSo7TbbzqBp06bOIrYR1X1O2xG/v63T0bJu3bqYPHly9OnTJ+bNm5e7/cgjj4ybb745nnvuuTjhhBPyOGHajjjiiEoHdg0ePDgOPfTQuPjii2PAgAFRWFiYx+nS0bx584gIn0y30IYfZxV22WUXu+1QKzb2MQdbo+IHV8uXL69y31133RXLli2LhQsX2sV6Cy1fvjzatm2b7zGStLN8TqvT0TJ9+vT45JNPYvLkyTF58uQq90+cOFG01EC9evWiT58+MW7cuJg9e7Zdd/5X8+bNo127dvHOO+/kexQA8qRFixax++67V/u1oOIYl/nz52/nqXYOH330UZSUlOxwp/ClZur0gfgTJ06Mtm3bxtSpU6tcBg8eHI8++qhf+FdDFfvkVveTpLpswIABMXfu3Hj55ZfzPQoAedK/f/+YM2dOvPrqq/keZafyu9/9LiIiTjzxxDxPQm2qs9FSWloajzzySAwYMCBOP/30KpeLL744li1bFo899li+R91hrFmzJv74xz9GYWFh7L///vkeJylXXHFFNGnSJM4///xYuHBhlfvnzp0b48aNy8NkAGwvV1xxRTRu3DjOO++8ar8WOOFDzU2fPj2uv/766Ny5cwwZMiTf41CL6uzuYY899lgsW7YsTjnllGrvP+qoo6JNmzYxceLEOPPMM7fzdDuGadOmxfvvvx8REYsWLYpJkybF7Nmz48orr8wdx8FX9tprr5g0aVKceeaZsf/++8fQoUOjW7dusXr16njppZdi6tSpce655+Z7TABq0d577x2TJk2KwYMHx7777htDhgyJ7t27R5ZlMW/evJg0aVLUq1cv2rdvn+9Rk1TxfcfatWtj4cKFMX369HjmmWeiY8eO8dhjj0WjRo3yPWKS1v9+bX3HHHNMdOnSJQ8TbZk6Gy0TJ06MRo0abfT0vPXq1Yv+/fvHxIkTY/HixdG6devtPGH6rr766tzfGzVqFPvtt1+MHz8+LrjggjxOla5TTjkl3nrrrbjpppviP/7jP2L8+PFRVFQUBx98cNx8880xfPjwfI8IsEUqthDsiGck2t6+973vxdtvvx0333xz/PGPf4z77rsvCgoKomPHjtG/f//4yU9+Et27d8/3mEmq+L6jsLAwWrVqFQcddFDceuutMWzYMGfo3IT1v19b3/33379DRUtBZlskALAVbrvttrj00ktjzpw5sddee+V7HGAnVGePaQEAto3XXnstmjRpEh07dsz3KMBOqs7uHgYAbJ3f//73MWPGjJg4cWKcf/750aCBbyuA2mH3MABgi3Tu3DmWLVsWp556atx6663RpEmTfI8E7KRECwAAkDTHtAAAAEkTLQAAQNJECwAAkDTRAgAAJE20AAAASRMtAABA0kQLAACQNNECAAAkTbQAAABJ+/9NVsdwRnDnBwAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "dendo = Dendrogram(coreset_df, hierarchial_clustering_sequence)\n", + "dendo.plot_dendrogram(plot_title=\"Dendrogram of Coreset using VQE\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Each branch point in the dendrogram aboves corresponds to one of the plots below. Notice the first iterations are the most complicated, and the final iterations become trivial bipartitioning of two points. Occasionally, especially in the first iteration, the partitioning might be puzzling at first glance. The data might seem to naturally cluster into two groups. However, there are cases where a stray point seems to belong in the wrong cluster. There are two explanations for this. 1) The quantum sampling is approximate and stochastic. It is possible that too few shots were taken to sample the ground state of the problem. 2) It is important to remember that we are clustering coresets and not data points. There can be cases where it is optimal to pay a penalty by excluding a point based on proximity if the weights are small enough that the penalty has less impact. Usually, if a point looks unusually clustered and you go look at the original coresets plotted above, that point will have a small weight." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABKUAAASmCAYAAAD/KRjlAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/NK7nSAAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOzdeVwV9f7H8fcAcsAFXNkUFbXct1wIbdEi0cwr1TX12nVJ7Wbq1Uub9CuXLM1WM00zt7pm2mplXdQw9Zq4R2qppbnLwRUQVEDO/P7geurEIhqcA4fX8/GYh57vfGfOZ0aZL/OZ7/c7hmmapgAAAAAAAAAn8nB1AAAAAAAAACh/SEoBAAAAAADA6UhKAQAAAAAAwOlISgEAAAAAAMDpSEoBAAAAAADA6UhKAQAAAAAAwOlISgEAAAAAAMDpSEoBAAAAAADA6UhKAQAAAAAAwOlISqFY1a9fX4MHD3Z1GNdl0aJFMgxDhw4dcnUoxWbw4MGqX7++q8O4Jl26dFGXLl1cHQaAYkb7ULrQPgAoLWgfShfaBzgbSSkUyYEDB/SPf/xDDRo0kI+Pj/z8/NS5c2e98cYbunjxolNiuHDhgiZOnKi1a9c65ftKo40bN2rixIlKSUm5ru1Lyzn86aefNHHixFLXgNtsNr300ksKCwuTj4+PWrVqpQ8++MDVYQGlGu1D6UD7ULJeeOEF/eUvf1FgYKAMw9DEiRNdHRJQ6tE+lA60DyVn7969evLJJ9WmTRtVqVJFwcHB6tmzp7Zt2+bq0MoWE7iKFStWmL6+vmbVqlXNf/7zn+bcuXPNmTNnmv369TMrVKhgDh8+3F63Xr165qBBg0okjlOnTpmSzAkTJpTI/i9fvmxevHjRtNlsJbL/4vDyyy+bksyDBw8WqX5WVpZ56dIl++eSPodF9dFHH5mSzG+//TbPuszMTDMzM9P5QZmmOW7cOFOSOXz4cHPu3Llmz549TUnmBx984JJ4gNKO9qH0oH0oWZLMoKAgMyoqqlScJ6C0o30oPWgfSs5jjz1mVq1a1Rw6dKj59ttvmy+99JLZsGFD09PT01y9erXT4ymrvJyfBkNZcvDgQfXr10/16tXTmjVrFBwcbF83cuRI7d+/X1999ZULI/zzMjIyVKlSJXl6esrT09PV4RSrChUqOOV7rpzD4uDt7V0s+7lWx48f16uvvqqRI0dq5syZkqRhw4bp9ttv1xNPPKE+ffq43f8P4M+gfSjbaB+uzcGDB1W/fn2dPn1atWrVclkcQFlA+1C20T4UXf/+/TVx4kRVrlzZXvbQQw+padOmmjhxoiIjI10SV5nj6qwYSrdHHnnElGR+9913Rar/xycdEyZMMPP7b7Zw4cI8GfutW7ea3bp1M2vUqGH6+PiY9evXN4cMGWKapmkePHjQlJRn+X3Gfs+ePeb9999vVqtWzbRYLGa7du3Mzz//PN/vXbt2rTlixAizVq1aZtWqVQuMqV69embPnj3N//73v2aHDh1Mi8VihoWFme+++26eY/rhhx/M2267zfTx8TFr165tTp482VywYEGRnkz88MMP5qBBg8ywsDDTYrGYgYGB5pAhQ8zTp0/nOZd/XArb96BBg8x69eo57RweOnTIHDFihHnjjTeaPj4+ZvXq1c2//vWvDjFe2f6Py5WnHrfffrt5++23O3xncnKy+dBDD5kBAQGmxWIxW7VqZS5atMihzpXje/nll823337bbNCggent7W22b9/e3LJlS6Hn3zRNc9asWaYk88cff3QoX7JkiSnJ/O9//3vVfQDlCe0D7UN5aR9+r7T0GABKM9oH2ofy2D783n333WdWr179urcvb+gphUJ9+eWXatCggTp16lSi33Py5El169ZNtWrV0rhx41S1alUdOnRIn376qSSpVq1amj17tkaMGKF7771X9913nySpVatWkqQff/xRnTt3Vu3atTVu3DhVqlRJH374oaKjo/XJJ5/o3nvvdfi+Rx99VLVq1dL48eOVkZFRaGz79+/XX//6Vw0dOlSDBg3SggULNHjwYLVr107NmzeXlNvLpmvXrjIMQ7GxsapUqZLmzZsni8VSpONfvXq1fv31Vw0ZMkRBQUH68ccfNXfuXP3444/atGmTDMPQfffdp59//lkffPCBXn/9ddWsWdN+borCGedw69at2rhxo/r166c6dero0KFDmj17trp06aKffvpJFStW1G233aZ//vOfmjFjhp5++mk1bdpUkux//tHFixfVpUsX7d+/X6NGjVJYWJg++ugjDR48WCkpKRozZoxD/SVLluj8+fP6xz/+IcMw9NJLL+m+++7Tr7/+WuiTn++//16VKlXKE0fHjh3t62+55ZYinWugPKB9oH0oL+0DgGtD+0D7UN7bB6vVaj/XKAJXZ8VQeqWmppqSzN69exd5m+t90vHZZ5+ZksytW7cWuO/Cnk7eeeedZsuWLR3GP9tsNrNTp07mDTfckOd7b7nlFvPy5cuFxnTleCSZ69evt5edPHnStFgs5mOPPWYvGz16tGkYhvn999/by86cOWNWr169SE86Lly4kKfsgw8+yPPd1zom/PdPOkyz5M9hfseRkJBgSjLfe+89e1lhY8L/+KRj+vTppiRz8eLF9rKsrCwzIiLCrFy5spmWlmaa5m9POmrUqGGePXvWXvfzzz83JZlffvll3hP0Oz179jQbNGiQpzwjI8OUZI4bN67Q7YHyhPaB9qE8tQ+/R08poHC0D7QP5bV9uGL9+vWmYRjms88+e83blle8fQ8FSktLkyRVqVKlxL+ratWqkqQVK1YoOzv7mrY9e/as1qxZowceeEDnz5/X6dOndfr0aZ05c0ZRUVH65ZdfdPz4cYdthg8fXuTx382aNdOtt95q/1yrVi01btxYv/76q70sLi5OERERatOmjb2sevXqGjBgQJG+w9fX1/73S5cu6fTp07r55pslSTt27CjSPv6M4jqHvz+O7OxsnTlzRo0aNVLVqlWv+zi+/vprBQUFqX///vayChUq6J///KfS09O1bt06h/p9+/ZVtWrV7J+v/Nv9/t8rPxcvXsz3yZSPj499PYBctA+5aB/KR/sAoOhoH3LRPpTP9uHkyZP629/+prCwMD355JPXFXt5RFIKBfLz85MknT9/vsS/6/bbb9f999+vSZMmqWbNmurdu7cWLlyozMzMq267f/9+maapZ599VrVq1XJYJkyYICn3AvF7YWFhRY6tbt26ecqqVaumc+fO2T8fPnxYjRo1ylMvv7L8nD17VmPGjFFgYKB8fX1Vq1Yte4ypqalFjvV6Fdc5vHjxosaPH6/Q0FBZLBbVrFlTtWrVUkpKynUfx+HDh3XDDTfIw8PxcnWlu+7hw4cdyv/473Wlgfn9v1d+fH198/3/dunSJft6ALloH3LRPpSP9gFA0dE+5KJ9KH/tQ0ZGhu655x6dP39en3/+ucPk5ygcc0qhQH5+fgoJCdHu3buvex+GYeRbnpOTk6fexx9/rE2bNunLL7/UypUr9dBDD+nVV1/Vpk2bCv2httlskqTHH39cUVFR+db548X9WhIMBT0RMU2zyPu4mgceeEAbN27UE088oTZt2qhy5cqy2Wzq3r27/fhKUnGdw9GjR2vhwoUaO3asIiIi5O/vL8Mw1K9fP6cch3T9/17BwcH69ttvZZqmw//bpKQkSVJISEjxBQmUcbQPuWgfcrl7+wCg6GgfctE+5Cov7UNWVpbuu+8+7dy5UytXrlSLFi2KMzy3R1IKhbrnnns0d+5cJSQkKCIi4pq3v5JlTklJsXexlfJmp6+4+eabdfPNN+uFF17QkiVLNGDAAC1dulTDhg0rsIFq0KCBpNwuma567Wa9evW0f//+POX5lf3RuXPnFB8fr0mTJmn8+PH28l9++SVP3YLOQVGV9Dn8+OOPNWjQIL366qv2skuXLiklJaVIceSnXr162rlzp2w2m8PTjr1799rXF4c2bdpo3rx52rNnj5o1a2Yv37x5s309gN/QPhQN7UOustw+ALg2tA9FQ/uQq6y3DzabTQMHDlR8fLw+/PBD3X777cW27/KC4Xso1JNPPqlKlSpp2LBhSk5OzrP+wIEDeuONNwrcvmHDhpKk9evX28syMjL07rvvOtQ7d+5cnkz0lSTAlS64FStWlKQ8F6iAgAB16dJFb7/9tr1Xy++dOnWqwPiKS1RUlBISEpSYmGgvO3v2rN5///2rbnslM//H458+fXqeupUqVZKU9xwUVUmfQ09PzzzH8eabb+Z5snUtx3H33XfLarVq2bJl9rLLly/rzTffVOXKlYvtwt+7d29VqFBBb731lr3MNE3NmTNHtWvXLvE3yABlDe1D0dA+5CrL7QOAa0P7UDS0D7nKevswevRoLVu2TG+99Zb97YS4NvSUQqEaNmyoJUuWqG/fvmratKkGDhyoFi1aKCsrSxs3brS/WrMg3bp1U926dTV06FA98cQT8vT01IIFC1SrVi0dOXLEXu/dd9/VW2+9pXvvvVcNGzbU+fPn9c4778jPz0933323pNzuns2aNdOyZct04403qnr16mrRooVatGihWbNm6ZZbblHLli01fPhwNWjQQMnJyUpISNCxY8f0ww8/lOh5evLJJ7V48WLdddddGj16tP2VrnXr1tXZs2cLzez7+fnptttu00svvaTs7GzVrl1bq1at0sGDB/PUbdeunSTp//7v/9SvXz9VqFBBvXr1sl+kr6akz+E999yjf//73/L391ezZs2UkJCgb775RjVq1HCo16ZNG3l6emratGlKTU2VxWLRHXfc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vSw8PFyLFi1SXFycZs+erYMHD+rWW2/V+fPn891HZmam0tLSHJayzrzwkSTPAtdnZplKOrhIVqvVvpw+fdp5AQIoM9wqKTV79my1atVKfn5+9okJ//Of/xS6zUcffaQmTZrIx8dHLVu21Ndff+2kaIHSy3roZJ6yEzqk6gqQl1FBknT+XIYuZVySJN1///3atm2bdu7c6dQ4AXdgPXhSnhUcf7H/48+baTN16mjufB38vAGAcyxZskSTJk3Shx9+qICAAHt5jx491KdPH7Vq1UpRUVH6+uuvlZKSog8//DDf/UydOlX+/v72JTQ01FmHUHJyjkrKKXD1ym8vqHbL7xQcHGxfbrnlFufFB6DMcKukVJ06dfTiiy9q+/bt2rZtm+644w717t1bP/74Y771N27cqP79+2vo0KH6/vvvFR0drejoaO3evdvJkQOlS5VqlR3mtpGkNkZntTV++2XCy9tL3r7ekqSOHTvKNE21atXKqXEC7qBK9cqy5dgcyv7483alnsTPGwA4w9KlSzVs2DB9+OGHioyMLLRu1apVdeONN2r//v35ro+NjVVqaqp9OXr0aEmE7FweVVVQT6mFbwQpJ+kG5Vg7yDRN+7J3716nhgigbHCrpFSvXr10991364YbbtCNN96oF154QZUrV9amTZvyrf/GG2+oe/fueuKJJ9S0aVNNnjxZN910k2bOnOnkyIHSpWv/W5RzueCnXx5eHurar7M8PQvutg2gaG79682Frjc8DDXu0EhB9QMKrQcAKB4ffPCBhgwZog8++EA9e/a8av309HQdOHBAwcHB+a63WCz2kRxXlrLO8LlHhfWUkjwl32gnRQOgLHOrpNTv5eTkaOnSpcrIyFBERES+dRISEvI8+YiKilJCQkKh+3bHceHA7zXv1Fjto9rkmVdKkjw8PFTBu4L6PhXt/MCAfJTE0G3TNDV+/HgFBwfL19dXkZGR+uWXX0ok/poh1RU9uof+0DkxlyHJlB56oX+JfDcAuLv09HQlJiYqMTFRknTw4EElJibaJyaPjY3VwIED7fWXLFmigQMH6tVXX1V4eLh9PqTU1FR7nccff1zr1q3ToUOHtHHjRt17773y9PRU//7l6FptuV2q0Fr595bylIxKMioNdnJQAMoit0tK7dq1S5UrV5bFYtEjjzyizz77TM2aNcu3rtVqVWBgoENZYGCg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Zw4LANwKPaUAAG4lNTVVklS9evUib3PhwgVlZ2fn2Wbt2rUKCAhQ48aNNWLECJ05c6bAfWRmZiotLc1hAQCULRERERo4cKDatGmj22+/XZ9++qlq1aqlt99++7r3OXXqVPn7+9uX0NDQYowYAMo2klIAALdhs9k0duxYde7cWS1atCjydk899ZRCQkIchmN0795d7733nuLj4zVt2jStW7dOPXr0UE5OTr774KYDAEqXmjVrytPTU8nJyQ7lycnJRe4FW6FCBbVt21b79++XJPt217LP2NhYpaam2pejR49e66EAgNsiKQUAcBsjR47U7t27tXTp0iJv8+KLL2rp0qX67LPPHF773a9fP/3lL39Ry5YtFR0drRUrVmjr1q1au3ZtvvvhpgMAShdvb2+1a9dO8fHx9jKbzab4+HhFREQUaR85OTnatWuXgoODJUlhYWEKCgpy2GdaWpo2b95c4D4tFov8/PwcFgBALrdKSq1fv169evVSSEiIDMO46hsw1q5dm+fNSoZhFDgeHABQeo0aNUorVqzQt99+qzp16hRpm1deeUUvvviiVq1apVatWhVat0GDBqpZs6b9afkfcdMBAKVPTEyM3nnnHb377rvas2ePRowYoYyMDA0ZMkSSNHDgQMXGxtrrP/fcc1q1apV+/fVX7dixQw8++KAOHz6sYcOGScp9M9/YsWP1/PPP64svvtCuXbs0cOBAhYSEKDo62hWHCABlmltNdJ6RkaHWrVvroYce0n333Vfk7fbt2+dw8xAQEFAS4QEASoBpmho9erQ+++wzrV27VmFhYUXa7qWXXtILL7yglStXqn379letf+zYMZ05c8b+tBwAUPr17dtXp06d0vjx42W1WtWmTRvFxcXZJyo/cuSIPDx+e05/7tw5DR8+XFarVdWqVVO7du20ceNGNWvWzF7nySefVEZGhh5++GGlpKTolltuUVxcnENvWwBA0RimaZquDqIkGIahzz77rNAnFmvXrlXXrl117tw5Va1a9bq/Ky0tTf7+/kpNTeXJOIAyp6xfwx599FEtWbJEn3/+uRo3bmwv9/f3l6+vr6TcJ+G1a9fW1KlTJUnTpk3T+PHjtWTJEnXu3Nm+TeXKlVW5cmWlp6dr0qRJuv/++xUUFKQDBw7oySef1Pnz57Vr1y5ZLJarxlXWzysAcB0rGZxXAGVdcV7H3Gr4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iRO213/++afq16+fq19ebXk5fvy4RowYoZCQEJUvX15BQUG2GNPS0god69Uqrn14+vRpjRkzRuHh4bJarapataqCgoKUmpp61eP4888/1aBBA3l42B+uLp6u++eff9q1X/5+XUwwl75fBalVq5aioqLUr18/ffrpp6pbt66ioqIoTAGXID9cQH4oG/nhrbfe0tGjR21PnwKQP/LDBeSHspEf8hIYGKhBgwZp+/btOnDgQJGXL4u4pxTy5efnp7CwMG3ZsuWq12GxWPJsz87OztVv7ty5Wrt2rb755hstWbJEgwcP1htvvKG1a9eqUqVK+W7j4iNLn3jiCUVHR+fZ5/KDe1HuD5TfLyLGmEKv40ruvfderVmzRk8++aRatmypSpUqKScnR927d7d7JKujFNc+HD58uGbOnKmRI0cqIiJC/v7+slgs6tu3r1PGIRX/+3X33Xdr2rRpWrVqVb77BihryA8XkB8uKM35IS0tTS+++KIeffRRpaen284CycjIkDFGe/fuVYUKFRQcHOyQmAF3Q364gPxwQWnODwUJDw+XdKFwWKNGjauOq6ygKIUC3X777Zo6daoSExMVERFR5OUvVplTU1Ntp9hKuavTF91444268cYb9dJLLykhIUH9+/fX7Nmz9eCDD+aboOrWrSvpwimZUVFRRY6xONSqVUs7d+7M1Z5X2+VOnDihZcuWafz48RozZoytfceOHbn65rcPCsvR+3Du3LkaOHCg3njjDVvbmTNnlJqaWqg48lKrVi398ssvysnJsfu14/fff7fNd6SLZ0g54xcnwJ2QHwqH/HCBu+aHEydOKCMjQ6+99ppee+21XPPr1KmjO+64Q/Pnz7/mbQGlBfmhcMgPF7hrfijIxcv+CntD+bKOy/dQoKeeekoVK1bUgw8+qJSUlFzzd+3apbfffjvf5evVqydJWrVqla0tMzNTH330kV2/EydO5KpEX7y++uIpuBUqVJCkXAeo4OBgRUZG6oMPPtDhw4dzxfDXX3/lG19xiY6OVmJiopKSkmxtx48f16effnrFZS9W5i8f/8SJE3P1rVixoqTc+6CwHL0PPT09c43j3XffzfXLVlHGcdtttyk5OVmff/65re38+fN69913ValSJXXp0qVQsV1JfmOcPn26LBaLbrjhhmLZDlBakB8Kh/xwgbvmh+DgYM2bNy/X1LVrV/n4+GjevHkaPXr0NW8HKE3ID4VDfrjAXfODlPcYDx48qBkzZqh58+aqVq1asWyntONMKRSoXr16SkhIUJ8+fdS4cWMNGDBATZs21dmzZ7VmzRrbozXz061bN9WsWVNDhgzRk08+KU9PT82YMUNBQUHat2+frd9HH32k999/X71791a9evV08uRJTZs2TX5+frrtttskXTjds0mTJvr888913XXXKTAwUE2bNlXTpk01adIk3XTTTWrWrJmGDh2qunXrKiUlRYmJiTpw4IB+/vlnh+6np556Sp988oluvfVWDR8+3PZI15o1a+r48eMFVvb9/PzUuXNnvfbaazp37pyqV6+u7777Tnv27MnVt3Xr1pKkZ599Vn379lW5cuXUq1cv20H6Shy9D2+//Xb9+9//lr+/v5o0aaLExER9//33qlKlil2/li1bytPTU6+++qrS0tJktVp1880353n5w0MPPaQPPvhAsbGx2rRpk2rXrq25c+dq9erVmjhxYrHdSPOll17S6tWr1b17d9v79uWXX2rDhg0aPnx4oa/vB8oK8kPhkB8ucNf8UKFCBcXExORqnz9/vtavX5/nPKCsIz8UDvnhAnfND9KF93DXrl265ZZbFBYWpr179+qDDz5QZmZmgYVXXMaZj/qD+/rjjz/M0KFDTe3atY23t7fx9fU1HTt2NO+++67dI0Avf6SrMcZs2rTJtG/f3nh7e5uaNWuaN998M9fjU3/66SfTr18/U7NmTWO1Wk1wcLC5/fbbzcaNG+3WtWbNGtO6dWvj7e2d69Gku3btMgMGDDChoaGmXLlypnr16ub22283c+fOtfW5uN28Hh2b3yNde/bsmavv5Y8dNcaYzZs3m06dOhmr1Wpq1Khh4uPjzTvvvGMkmeTk5AL374EDB0zv3r1NQECA8ff3N/fcc485dOhQno9ffeGFF0z16tWNh4fHFR/vevkjXY1x7D48ceKEGTRokKlataqpVKmSiY6ONr///nue/y6mTZtm6tatazw9Pe0e75rXvk1JSbGt19vb2zRr1szMnDnTrs/FR7q+/vrrueLKaz9e7rvvvjO33367CQsLM+XKlbP9G585c6bJyckpcFmgLCM/2CM/lL78kJeBAweaihUrFnk5oCwhP9gjP5S+/JCQkGA6d+5sgoKCjJeXl6latarp3bu32bRpU4HLwZ7FmGK82xoAOyNHjtQHH3ygjIyMQj9CFgBQ+pEfAAB5IT+grOGeUkAxuXhD7IuOHTumf//737rppptIKABQhpEfAAB5IT8A3FMKKDYRERGKjIxU48aNlZKSounTpys9PV3PP/+8q0MDALgQ+QEAkBfyA0BRCig2t912m+bOnaupU6fantY2ffp0de7c2dWhAQBciPwAAMgL+QGQuKcUAAAAAAAAnI57SgEAAAAAAMDpKEoBAAAAAADA6binVDHIycnRoUOH5OvrK4vF4upwAKBIjDE6efKkwsLC5OHBbxXFhdwAwN2RHxyD/ADA3RVnfqAoVQwOHTqk8PBwV4cBANdk//79qlGjhqvDKDXIDQBKC/JD8SI/ACgtiiM/UJQqBr6+vpIuvCF+fn4ujgYAiiY9PV3h4eG2YxmKB7kBgLsjPzgG+QGAuyvO/EBRqhhcPO3Wz8+PxALAbXEJQfEiNwAoLcgPxYv8AKC0KI78wMXhAAAAAAAAcDqKUgAAAAAAAHA6ilIlSGJiojw9PdWzZ09XhwIAAOBSsbGxiomJydW+YsUKWSwWpaamOj0moCRJTk7WiBEjVL9+ffn4+CgkJEQdO3bU5MmTderUKVeHBwCFwj2lSpDp06dr+PDhmj59ug4dOqSwsDBXhwQAAACghNm9e7c6duyogIAAvfzyy2rWrJmsVqt+/fVXTZ06VdWrV9ff/vY3V4cJAFdEUaqEyMjI0Oeff66NGzcqOTlZs2bN0jPPPOPqsAAAAACUMI8++qi8vLy0ceNGVaxY0dZet25d3XHHHTLGuDA6ACg8Lt8rIb744gs1atRIDRs21P33368ZM2aQTAAAAADYOXbsmL777jvFxcXZFaQuxRMTAbgLilIucuCPQ9qWuF1HDx6TdOHSvfvvv1+S1L17d6WlpWnlypWuDBEAAMBpjMmWObdN5uzPMjlpkqSFCxeqUqVKdlOPHj1cHCngfMYYmXM7ZM4macf2TTLGqGHDhnZ9qlatavuc/POf/3RRpABQNFy+52QbliRp+uhPtStp74UGi1Q7Ikzr16/XvHnzJEleXl7q06ePpk+frsjISJfFCgAA4GjGGOl0gkzGFCkn5b+t5WTOeqhr5E2aPOVDu/7r1q2z/ZAHlAXm9EKZjIlS9r4Lr1OzLvw357hdv/Xr1ysnJ0f9+/dXVlaWs8MEgKtCUcqJfpy3Tv+6+w37RiN9v2aJzpvzdjc2N8bIarXqvffek7+/v5MjBQAAcA6T8aaU+cFlreek7BRVKJeienWDZfHws805cOCAcwMEXMhk/lvm5AuS/nc5Xv3aXrJYpO2b/yUT01MWz1BJF+4nJUnly5d3RagAcFW4fM9Jzp09p4l//0BGxu5eUTkmR4fMXl3n0UL3t35ISUlJSkpK0s8//6ywsDB99tlnLowaAADAccz53XkUpGxzJXNaJnOmU2MCSgqTc0LmZPzFV7b2KoGeiupcQZNmHFJG8uuuCQ4AiglFKSdZv2iz0o6evDSfSJKO6rDO6ZzCcmrr4Ia/VLVSsJo2baqmTZvqrrvu0vTp010TMAAAgIOZ019K8iyoh3T6Mx7+grLp9DeSsvOcNemVIJ0/b9Qu8j3N/uxj/fbbb9q+fbs++eQT/f777/L0LOhzBQAlB0UpJ0nec0Qenrl39yHtVaCC5WUpZ+t30V133aWNGzfql19+cVqcAAAATpN9QFJOwX1yjks664xogBLFZB9QfkXberW9tWlpTd3SubyeefY5tWjRQm3atNG7776rJ554Qi+88IJzgwWAq8Q9pZzEN7CScrJzf+lqaelo99qviq/t/9u1a8cvgwAAoPSy+OvCb6S5zwaZ+Xbof//PKsnb1h4ZGcn3I5QJFg9/mcsvs7hEtRAvvfNSsN4NXieLR2UnRgYAxYczpZwk4m9tVM6afw3QYpFqNAxTnWY1nRgVAACA61jK3678Lk+6wFPy6SWLxVJAH6CU8rlNBX8+PCTvjhSkALg1tylKTZ48Wc2bN5efn5/8/PwUERGh//znPwUuM2fOHDVq1Eg+Pj5q1qyZFi1aZDffGKMxY8aoWrVqKl++vKKiorRjxw6HxO9buZL6PBWT73xjpCEv38eXLgAAUHaUayt5d1DeX0k9JIu3LJWGOjsqoESweNWRfO7UpU/eu2SuJA9ZKj3m5KgAoHi5TVGqRo0aeuWVV7Rp0yZt3LhRN998s+644w5t3bo1z/5r1qxRv379NGTIEG3evFkxMTGKiYnRli1bbH1ee+01vfPOO5oyZYrWrVunihUrKjo6WmfOnHHIGB4Ye4/6P3uXvMp5ShbJ0+vC7q/gV15PfTRMN/Vu75DtAgAAlEQWi0WWgEmS9daLLbLdQ8ezmiyVP77whzlQRln8/yWVv1cX/my75PPhEShL5SmyeLdyYXQAcO0sxo0vyg8MDNTrr7+uIUOG5JrXp08fZWZmauHChba2G2+8US1bttSUKVNkjFFYWJgef/xxPfHEE5KktLQ0hYSEaNasWerbt2+h40hPT5e/v7/S0tLk5+d35f7HTurHr9Yp/dhJBdcKUseYtrKWtxZ6ewBQnIp6DEPhsF+BojHn/5SyVkgmSyrX+MJlSRa3+f20VOI45hhXs19NdrJ05nvJZEpedSVrpCz/fVASADhbceYHt7zReXZ2tubMmaPMzExFRETk2ScxMVGjRo2ya4uOjtb8+fMlSXv27FFycrKioqJs8/39/dW+fXslJiYWqShVVH5VfHXb0KgrdwQAACgjLF61JK+Brg4DKJEsnqFSxftdHQYAFDu3Kkr9+uuvioiI0JkzZ1SpUiXNmzdPTZo0ybNvcnKyQkJC7NpCQkKUnJxsm3+xLb8++cnKylJWVpbtdXp6epHHAgAAAAAAUJa51TnRDRs2VFJSktatW6dHHnlEAwcO1LZt25weR3x8vPz9/W1TeHi402MAAAAAAABwZ25VlPL29lb9+vXVunVrxcfHq0WLFnr77bfz7BsaGqqUlBS7tpSUFIWGhtrmX2zLr09+Ro8erbS0NNu0f//+qx0SAAAAAABAmeRWRanL5eTk2F1Gd6mIiAgtW7bMrm3p0qW2e1DVqVNHoaGhdn3S09O1bt26fO9TdZHVapWfn5/dBAAAAAAAgMJzm3tKjR49Wj169FDNmjV18uRJJSQkaMWKFVqyZIkkacCAAapevbri4+MlSSNGjFCXLl30xhtvqGfPnpo9e7Y2btyoqVOnSrrwCOKRI0fqxRdfVIMGDVSnTh09//zzCgsLU0xMjKuGCQAAAAAAUCa4TVHqyJEjGjBggA4fPix/f381b95cS5Ys0a233ipJ2rdvnzw8/nfiV4cOHZSQkKDnnntOzzzzjBo0aKD58+eradOmtj5PPfWUMjMz9dBDDyk1NVU33XSTFi9eLB8fH6ePDwAAAAAAoCyxGGOMq4Nwd+np6fL391daWhqX8gFwOxzDHIP9CsDdcRxzDPYrAHdXnMcxt76nFAAAAAAAANwTRSkAAAAAAAA4HUUpAAAAAAAAOB1FKQAAAAAAADgdRSkAQKkXHx+vtm3bytfXV8HBwYqJidH27duvuNycOXPUqFEj+fj4qFmzZlq0aJETogUAAADKBopSAIBSb+XKlYqLi9PatWu1dOlSnTt3Tt26dVNmZma+y6xZs0b9+vXTkCFDtHnzZsXExCgmJkZbtmxxYuQAAABA6WUxxhhXB+HueKwrAHdWFo9hf/31l4KDg7Vy5Up17tw5zz59+vRRZmamFi5caGu78cYb1bJlS02ZMuWK2yiL+xVA6cJxzDHYrwDcXXEexzhTCgBQ5qSlpUmSAgMD8+2TmJioqKgou7bo6GglJibm2T8rK0vp6el2EwAAAID8UZQCAJQpOTk5GjlypDp27KimTZvm2y85OVkhISF2bSEhIUpOTs6zf3x8vPz9/W1TeHh4scYNAAAAlDYUpQAAZUpcXJy2bNmi2bNnF+t6R48erbS0NNu0f//+Yl0/AAAAUNp4uToAAACcZdiwYVq4cKFWrVqlGjVqFNg3NDRUKSkpdm0pKSkKDQ3Ns7/VapXVai22WAEAAIDSjjOlAAClnjFGw4YN07x587R8+XLVqVPnistERERo2bJldm1Lly5VRESEo8IEAAAAyhTOlAIAlHpxcXFKSEjQggUL5Ovra7svlL+/v8qXLy9JGjBggKpXr674+HhJ0ogRI9SlSxe98cYb6tmzp2bPnq2NGzdq6tSpLhsHAAAAUJpwphQAoNSbPHmy0tLSFBkZqWrVqtmmzz//3NZn3759Onz4sO11hw4dlJCQoKlTp6pFixaaO3eu5s+fX+DN0QEAAAAUHmdKAQBKPWPMFfusWLEiV9s999yje+65xwERAQAAAOBMKQAAAAAAADgdRSkAAAAAAAA4HUUpAAAAAAAAOB1FKQAAAAAAUCrFxsYqJibG1WEgHxSlAAAAAAAA4HQUpQAAAAAAAOB0FKUAAAAAAADgdBSlAAAAAAAA4HRerg4AAAAAAACgOGRnZ+uXldt07NAJBYYGyOQYV4eEAlCUAgAAAAAAbu//vlqn90fM0NGDx21tO3x+Vo3rQ10YFQrC5XsAAAAAAMCt/Thvnf51zwS7gpQknT19Vn9s3KXln/3ooshQEIpSAAAAAADAbeXk5GjyqI8K7DPl8Y+UfT7bSRGhsChKAQAAAAAAt7VtzXYd+fMvKY/bRxkZWWTRieRUJf2wxfnBoUBuU5SKj49X27Zt5evrq+DgYMXExGj79u0FLhMZGSmLxZJr6tmzp61PbGxsrvndu3d39HAAAAAAlBCTJk1S7dq15ePjo/bt22v9+vX59t26davuuusu1a5dWxaLRRMnTnReoADydDw5Nd9555Qlb/lc6Hc4/35wDbcpSq1cuVJxcXFau3atli5dqnPnzqlbt27KzMzMd5mvvvpKhw8ftk1btmyRp6en7rnnHrt+3bt3t+v32WefOXo4AAAAAEqAzz//XKNGjdLYsWP1008/qUWLFoqOjtaRI0fy7H/q1CnVrVtXr7zyikJDuXkyUBJUCQvM1XbOnNVf5pBO6C8FKvhCv+q5+8G13Obpe4sXL7Z7PWvWLAUHB2vTpk3q3LlznssEBtr/g5s9e7YqVKiQqyhltVpJKAAAAEAZ9Oabb2ro0KEaNGiQJGnKlCn69ttvNWPGDD399NO5+rdt21Zt27aVpDznA3C+xjc2UGidYKXsPSLz30v4tmmj0nVCNXWdghSmKtUD1SKyiWsDRS5uc6bU5dLS0iTlLjwVZPr06erbt68qVqxo175ixQoFBwerYcOGeuSRR3Ts2LFijRUAAABAyXP27Flt2rRJUVFRtjYPDw9FRUUpMTHRhZEBKAoPDw/FvT1YkkUWy4W2FpYO6mTpqfqWprJYLHr0rVh5enq6NE7k5pZFqZycHI0cOVIdO3ZU06ZNC7XM+vXrtWXLFj344IN27d27d9fHH3+sZcuW6dVXX9XKlSvVo0cPZWfnf1f+rKwspaen200AAAAA3MvRo0eVnZ2tkJAQu/aQkBAlJycXyzb42wFwjhtvb63x859SSO1gu/bgmlU1Zu4T6nx3hIsiQ0Hc5vK9S8XFxWnLli368ccfC73M9OnT1axZM7Vr186uvW/fvrb/b9asmZo3b6569eppxYoVuuWWW/JcV3x8vMaPH391wQMAAAAoM/jbAXCeiF5t1L7nDdqW+IeOHTyuyqEBanpTI3l4uOX5OGWC270zw4YN08KFC/XDDz+oRo0ahVomMzNTs2fP1pAhQ67Yt27duqpatap27tyZb5/Ro0crLS3NNu3fv7/Q8QMAAAAoGapWrSpPT0+lpKTYtaekpBTbPWf52wFwLg8PDzXt2Ehd7u2g5p2bUJAq4dzm3THGaNiwYZo3b56WL1+uOnXqFHrZOXPmKCsrS/fff/8V+x44cEDHjh1TtWrV8u1jtVrl5+dnNwEAAABwL97e3mrdurWWLVtma8vJydGyZcsUEVE8l/rwtwMA5M9tLt+Li4tTQkKCFixYIF9fX9s13v7+/ipfvrwkacCAAapevbri4+Ptlp0+fbpiYmJUpUoVu/aMjAyNHz9ed911l0JDQ7Vr1y499dRTql+/vqKjo50zMAAAAAAuM2rUKA0cOFBt2rRRu3btNHHiRGVmZtqexnf53xhnz57Vtm3bbP9/8OBBJSUlqVKlSqpfv77LxgEA7shtilKTJ0+WJEVGRtq1z5w5U7GxsZKkffv25To1b/v27frxxx/13Xff5Vqnp6enfvnlF3300UdKTU1VWFiYunXrphdeeEFWq9Uh4wAAAABQcvTp00d//fWXxowZo+TkZLVs2VKLFy+23fz88r8xDh06pFatWtleT5gwQRMmTFCXLl20YsUKZ4cPAG7NYowxrg7C3aWnp8vf319paWmcjgvA7XAMcwz2KwB3x3HMMdivANxdcR7H3OaeUgAAAAAAACg9KEoBAAAAAADA6ShKAQAAAAAAwOkoSgEAAAAAAMDpKEoBAAAAAADA6ShKAQAAAAAAwOkoSgEAAAAAAMDpKEoBAAAAAADA6ShKAQAAAAAAwOkoSgEAAAAAAMDpKEoBAAAAAADA6ShKAQAAAAAAwOkoSgEAAAAAAMDpKEoBAAAAAADA6ShKAQAAAAAAwOkoSgEAAAAAAMDpKEoBAAAAAADA6ShKAQAAAAAAwOkoSgEAAAAAAMDpKEoBAAAAAADA6ShKAQBKvVWrVqlXr14KCwuTxWLR/Pnzr7jMp59+qhYtWqhChQqqVq2aBg8erGPHjjk+WAAAAKCMoCgFACj1MjMz1aJFC02aNKlQ/VevXq0BAwZoyJAh2rp1q+bMmaP169dr6NChDo4UAAAAKDu8XB0AAACO1qNHD/Xo0aPQ/RMTE1W7dm099thjkqQ6dero73//u1599VVHhQgAAACUOZwpBQDAZSIiIrR//34tWrRIxhilpKRo7ty5uu2221wdGgAAAFBqUJQCAOAyHTt21Keffqo+ffrI29tboaGh8vf3L/Dyv6ysLKWnp9tNAAAAAPJHUQoAgMts27ZNI0aM0JgxY7Rp0yYtXrxYe/fu1cMPP5zvMvHx8fL397dN4eHhTowYAAAAcD8UpQAAuEx8fLw6duyoJ598Us2bN1d0dLTef/99zZgxQ4cPH85zmdGjRystLc027d+/38lRAwAAAO6FG50DAHCZU6dOycvLPkV6enpKkowxeS5jtVpltVodHhsAAABQWrjNmVLx8fFq27atfH19FRwcrJiYGG3fvr3AZWbNmiWLxWI3+fj42PUxxmjMmDGqVq2aypcvr6ioKO3YscORQwEAOFlGRoaSkpKUlJQkSdqzZ4+SkpK0b98+SRfOchowYICtf69evfTVV19p8uTJ2r17t1avXq3HHntM7dq1U1hYmCuGAAAAAJQ6blOUWrlypeLi4rR27VotXbpU586dU7du3ZSZmVngcn5+fjp8+LBt+vPPP+3mv/baa3rnnXc0ZcoUrVu3ThUrVlR0dLTOnDnjyOEAAJxo48aNatWqlVq1aiVJGjVqlFq1aqUxY8ZIkg4fPmwrUElSbGys3nzzTb333ntq2rSp7rnnHjVs2FBfffWVS+IHAAAASiOLye86hBLur7/+UnBwsFauXKnOnTvn2WfWrFkaOXKkUlNT85xvjFFYWJgef/xxPfHEE5KktLQ0hYSEaNasWerbt2+hYklPT5e/v7/S0tLk5+d3VeMBAFfhGOYY7FcA7o7jmGOwXwG4u+I8jrnNmVKXS0tLkyQFBgYW2C8jI0O1atVSeHi47rjjDm3dutU2b8+ePUpOTlZUVJStzd/fX+3bt1diYmK+6+Sx3wAAAAAAANfGLYtSOTk5GjlypDp27KimTZvm269hw4aaMWOGFixYoE8++UQ5OTnq0KGDDhw4IElKTk6WJIWEhNgtFxISYpuXFx77DQAAAAAAcG3csigVFxenLVu2aPbs2QX2i4iI0IABA9SyZUt16dJFX331lYKCgvTBBx9c0/Z57DcAAAAAAMC18bpyl5Jl2LBhWrhwoVatWqUaNWoUadly5cqpVatW2rlzpyQpNDRUkpSSkqJq1arZ+qWkpKhly5b5rofHfgMAAAAAAFwbtzlTyhijYcOGad68eVq+fLnq1KlT5HVkZ2fr119/tRWg6tSpo9DQUC1btszWJz09XevWrVNERESxxQ4AAAAAAAB7bnOmVFxcnBISErRgwQL5+vra7vnk7++v8uXLS5IGDBig6tWrKz4+XpL0r3/9SzfeeKPq16+v1NRUvf766/rzzz/14IMPSpIsFotGjhypF198UQ0aNFCdOnX0/PPPKywsTDExMS4ZJwAAAAAAQFngNkWpyZMnS5IiIyPt2mfOnKnY2FhJ0r59++Th8b+Tv06cOKGhQ4cqOTlZlStXVuvWrbVmzRo1adLE1uepp55SZmamHnroIaWmpuqmm27S4sWL5ePj4/AxAQAAAAAAlFUWY4xxdRDuLj09Xf7+/kpLS5Ofn5+rwwGAIuEY5hjsVwDujuOYY7BfAbi74jyOuc09pQAAAAAAAFB6UJQCAAAAAACA01GUAgAAAAAAgNNRlAIAAAAAAIDTUZQCAAAAAACA01GUAgAAAAAAgNNRlAIAAAAAAIDTUZQCAAAAAACA01GUAgAAAAAAgNNRlAIAAAAAAIDTUZQCAAAAAACA01GUAgAAAAAAgNNRlAIAAAAAAIDTUZQCAAAAAACA01GUAgAAAAAAgNNRlAIAAAAAAIDTUZQCAAAAAACA01GUAgAAAFCmTZo0SbVr15aPj4/at2+v9evXF9h/zpw5atSokXx8fNSsWTMtWrTISZECQOlCUQoAAABAmfX5559r1KhRGjt2rH766Se1aNFC0dHROnLkSJ7916xZo379+mnIkCHavHmzYmJiFBMToy1btjg5cgBwfxSlAAAAAJRZb775poYOHapBgwapSZMmmjJliipUqKAZM2bk2f/tt99W9+7d9eSTT6px48Z64YUXdMMNN+i9995zcuQA4P4oSgEAAAAok86ePatNmzYpKirK1ubh4aGoqCglJibmuUxiYqJdf0mKjo7Ot39WVpbS09PtJgDABRSlAAAAAJRJR48eVXZ2tkJCQuzaQ0JClJycnOcyycnJReofHx8vf39/2xQeHl48wQNAKUBRCgAAAAAcZPTo0UpLS7NN+/fvd3VIAFBieLk6AAAAAABwhapVq8rT01MpKSl27SkpKQoNDc1zmdDQ0CL1t1qtslqtxRMwAJQynCkFAAAAoEzy9vZW69attWzZMltbTk6Oli1bpoiIiDyXiYiIsOsvSUuXLs23PwAgf5wpBQAAAKDMGjVqlAYOHKg2bdqoXbt2mjhxojIzMzVo0CBJ0oABA1S9enXFx8dLkkaMGKEuXbrojTfeUM+ePTV79mxt3LhRU6dOdeUwAMAtUZQCAAAAUGb16dNHf/31l8aMGaPk5GS1bNlSixcvtt3MfN++ffLw+N8FJh06dFBCQoKee+45PfPMM2rQoIHmz5+vpk2bumoIAOC2LMYY4+og3F16err8/f2VlpYmPz8/V4cDAEXCMcwx2K8A3B3HMcdgvwJwd8V5HHObe0rFx8erbdu28vX1VXBwsGJiYrR9+/YCl5k2bZo6deqkypUrq3LlyoqKitL69evt+sTGxspisdhN3bt3d+RQAAAAAAAAyjy3KUqtXLlScXFxWrt2rZYuXapz586pW7duyszMzHeZFStWqF+/fvrhhx+UmJio8PBwdevWTQcPHrTr1717dx0+fNg2ffbZZ44eDgAAAAAAQJnmNveUWrx4sd3rWbNmKTg4WJs2bVLnzp3zXObTTz+1e/3hhx/qyy+/1LJlyzRgwABbu9VqzfcRrgAAAAAAACh+bnOm1OXS0tIkSYGBgYVe5tSpUzp37lyuZVasWKHg4GA1bNhQjzzyiI4dO1bgerKyspSenm43AQAAAAAAoPDcsiiVk5OjkSNHqmPHjkV6ysU///lPhYWFKSoqytbWvXt3ffzxx1q2bJleffVVrVy5Uj169FB2dna+64mPj5e/v79tCg8Pv6bxAAAAAAAAlDVuc/nepeLi4rRlyxb9+OOPhV7mlVde0ezZs7VixQr5+PjY2vv27Wv7/2bNmql58+aqV6+eVqxYoVtuuSXPdY0ePVqjRo2yvU5PT6cwBQAAAAAAUARud6bUsGHDtHDhQv3www+qUaNGoZaZMGGCXnnlFX333Xdq3rx5gX3r1q2rqlWraufOnfn2sVqt8vPzs5sAAAAAAABQeG5zppQxRsOHD9e8efO0YsUK1alTp1DLvfbaa3rppZe0ZMkStWnT5or9Dxw4oGPHjqlatWrXGjIAAAAAAADy4TZnSsXFxemTTz5RQkKCfH19lZycrOTkZJ0+fdrWZ8CAARo9erTt9auvvqrnn39eM2bMUO3atW3LZGRkSJIyMjL05JNPau3atdq7d6+WLVumO+64Q/Xr11d0dLTTxwgAcIxVq1apV69eCgsLk8Vi0fz586+4TFZWlp599lnVqlVLVqtVtWvX1owZMxwfLAAAAFBGuM2ZUpMnT5YkRUZG2rXPnDlTsbGxkqR9+/bJw8PDbpmzZ8/q7rvvtltm7NixGjdunDw9PfXLL7/oo48+UmpqqsLCwtStWze98MILslqtDh0PAMB5MjMz1aJFCw0ePFh33nlnoZa59957lZKSounTp6t+/fo6fPiwcnJyHBwpAAAAUHa4TVHKGHPFPitWrLB7vXfv3gL7ly9fXkuWLLmGqAAA7qBHjx7q0aNHofsvXrxYK1eu1O7duxUYGChJql27toOiAwAAAMomt7l8DwAAZ/n666/Vpk0bvfbaa6pevbquu+46PfHEE3aXjAMAAAC4Nm5zphQAAM6ye/du/fjjj/Lx8dG8efN09OhRPfroozp27JhmzpyZ5zJZWVnKysqyvU5PT3dWuAAAAIBb4kwpAAAuk5OTI4vFok8//VTt2rXTbbfdpjfffFMfffRRvmdLxcfHy9/f3zaFh4c7OWoAAADAvVCUAgDgMtWqVVP16tXl7+9va2vcuLGMMTpw4ECey4wePVppaWm2af/+/c4KFwAAAHBLFKUAALhMx44ddejQIWVkZNja/vjjD3l4eKhGjRp5LmO1WuXn52c3AQAAAMgfRSkAQKmXkZGhpKQkJSUlSZL27NmjpKQk7du3T9KFs5wGDBhg63/fffepSpUqGjRokLZt26ZVq1bpySef1ODBg1W+fHlXDAEAAAAodShKAQBKvY0bN6pVq1Zq1aqVJGnUqFFq1aqVxowZI0k6fPiwrUAlSZUqVdLSpUuVmpqqNm3aqH///urVq5feeecdl8QPAAAAlEY8fQ8AUOpFRkbKGJPv/FmzZuVqa9SokZYuXerAqAAAAICyzSFnSh06dMgRqwUAlAHkEAAoGzjeAwAcUpS6/vrrlZCQ4IhVAwBKOXIIAJQNHO8BAA4pSr300kv6+9//rnvuuUfHjx93xCYAAKUUOQQAygaO9wAAhxSlHn30Uf3yyy86duyYmjRpom+++cYRmwEAlELkEAAoGzjeAwAcdqPzOnXqaPny5Xrvvfd05513qnHjxvLyst/cTz/95KjNAwDcGDkEAMoGjvcAULY59Ol7f/75p7766itVrlxZd9xxR64EAwBAfsghAFA2cLwHgLLLYUf8adOm6fHHH1dUVJS2bt2qoKAgR20KAFDKkEMAoGzgeA8AZZtDilLdu3fX+vXr9d5772nAgAGO2AQAoJQihwBA2cDxHgDgkKJUdna2fvnlF9WoUcMRqwcAlGLkEAAoGzjeAwAcUpRaunSpI1YLACgDyCEAUDZwvAcAeLg6AAAAAAAAAJQ9FKUAAAAAAADgdBSlAAAAAAAA4HQUpQAAAAAAAOB0FKUAAAAAAADgdBSlAAAAAAAA4HQUpQAAAAAAAOB0FKUAAAAAAADgdBSlAAAAAAAA4HQUpQAAAAAAAOB0blOUio+PV9u2beXr66vg4GDFxMRo+/btV1xuzpw5atSokXx8fNSsWTMtWrTIbr4xRmPGjFG1atVUvnx5RUVFaceOHY4aBgAAAAAAAORGRamVK1cqLi5Oa9eu1dKlS3Xu3Dl169ZNmZmZ+S6zZs0a9evXT0OGDNHmzZsVExOjmJgYbdmyxdbntdde0zvvvKMpU6Zo3bp1qlixoqKjo3XmzBlnDAsAAAAAAKBMshhjjKuDuBp//fWXgoODtXLlSnXu3DnPPn369FFmZqYWLlxoa7vxxhvVsmVLTZkyRcYYhYWF6fHHH9cTTzwhSUpLS1NISIhmzZqlvn37FiqW9PR0+fv7Ky0tTX5+ftc+OABwIo5hjsF+BeDuOI45BvsVgLsrzuOY25wpdbm0tDRJUmBgYL59EhMTFRUVZdcWHR2txMRESdKePXuUnJxs18ff31/t27e39clLVlaW0tPT7SYAAAAAAAAUnlsWpXJycjRy5Eh17NhRTZs2zbdfcnKyQkJC7NpCQkKUnJxsm3+xLb8+eYmPj5e/v79tCg8Pv9qhAAAAAAAAlEluWZSKi4vTli1bNHv2bJdsf/To0UpLS7NN+/fvd0kcAAAAAAAA7srL1QEU1bBhw7Rw4UKtWrVKNWrUKLBvaGioUlJS7NpSUlIUGhpqm3+xrVq1anZ9WrZsme96rVarrFbrVY4AAAAAAAAAbnOmlDFGw4YN07x587R8+XLVqVPnistERERo2bJldm1Lly5VRESEJKlOnToKDQ2165Oenq5169bZ+gAAAAAAAKD4uc2ZUnFxcUpISNCCBQvk6+tru+eTv7+/ypcvL0kaMGCAqlevrvj4eEnSiBEj1KVLF73xxhvq2bOnZs+erY0bN2rq1KmSJIvFopEjR+rFF19UgwYNVKdOHT3//PMKCwtTTEyMS8YJAAAAAABQFrhNUWry5MmSpMjISLv2mTNnKjY2VpK0b98+eXj87+SvDh06KCEhQc8995yeeeYZNWjQQPPnz7e7OfpTTz2lzMxMPfTQQ0pNTdVNN92kxYsXy8fHx+FjAgAAAAAAKKssxhjj6iDcXXp6uvz9/ZWWliY/Pz9XhwMARcIxzDHYrwDcHccxx2C/AnB3xXkcc5t7SgEAAAAAAKD0oCgFAAAAAAAAp6MoBQAAAAAAAKejKAUAAAAAAACnoygFAAAAAAAAp6MoBQAAAAAAAKejKAUAAACgTDp+/Lj69+8vPz8/BQQEaMiQIcrIyChwmalTpyoyMlJ+fn6yWCxKTU11TrAAUApRlAIAAABQJvXv319bt27V0qVLtXDhQq1atUoPPfRQgcucOnVK3bt31zPPPOOkKAGg9PJydQAAAAAA4Gy//fabFi9erA0bNqhNmzaSpHfffVe33XabJkyYoLCwsDyXGzlypCRpxYoVTooUAEovzpQCAAAAUOYkJiYqICDAVpCSpKioKHl4eGjdunXFtp2srCylp6fbTQCACyhKAQAAAChzkpOTFRwcbNfm5eWlwMBAJScnF9t24uPj5e/vb5vCw8OLbd0A4O4oSgEAAAAoNZ5++mlZLJYCp99//91p8YwePVppaWm2af/+/U7bNgCUdNxTCgAAAECp8fjjjys2NrbAPnXr1lVoaKiOHDli137+/HkdP35coaGhxRaP1WqV1WottvUBQGlCUQoAAABAqREUFKSgoKAr9ouIiFBqaqo2bdqk1q1bS5KWL1+unJwctW/f3tFhAgDE5XsAgDJg1apV6tWrl8LCwmSxWDR//vxCL7t69Wp5eXmpZcuWDosPAOB8jRs3Vvfu3TV06FCtX79eq1ev1rBhw9S3b1/bk/cOHjyoRo0aaf369bblkpOTlZSUpJ07d0qSfv31VyUlJen48eMuGQcAuDOKUgCAUi8zM1MtWrTQpEmTirRcamqqBgwYoFtuucVBkQEAXOnTTz9Vo0aNdMstt+i2227TTTfdpKlTp9rmnzt3Ttu3b9epU6dsbVOmTFGrVq00dOhQSVLnzp3VqlUrff31106PHwDcncUYY1wdhLtLT0+Xv7+/0tLS5Ofn5+pwAKBIytoxzGKxaN68eYqJibli3759+6pBgwby9PTU/PnzlZSUVOjtlLX9CqD04TjmGOxXAO6uOI9jnCkFAEAeZs6cqd27d2vs2LGF6p+VlaX09HS7CQAAAED+KEoBAHCZHTt26Omnn9Ynn3wiL6/CPRMkPj5e/v7+tik8PNzBUQIAAADujaIUAACXyM7O1n333afx48fruuuuK/Ryo0ePVlpamm3av3+/A6MEAAAA3F/hfv4FAKCMOHnypDZu3KjNmzdr2LBhkqScnBwZY+Tl5aXvvvtON998c67lrFarrFars8MFAAAA3BZFKQAALuHn56dff/3Vru3999/X8uXLNXfuXNWpU8dFkQEAAAClC0UpAECpl5GRoZ07d9pe79mzR0lJSQoMDFTNmjU1evRoHTx4UB9//LE8PDzUtGlTu+WDg4Pl4+OTqx0AAADA1aMoBQAo9TZu3KiuXbvaXo8aNUqSNHDgQM2aNUuHDx/Wvn37XBUeAAAAUCZZjDHG1UG4u/T0dPn7+ystLU1+fn6uDgcAioRjmGOwXwG4O45jjsF+BeDuivM4xtP3AAAAAAAA4HQUpQAAAAAAAOB0blWUWrVqlXr16qWwsDBZLBbNnz+/wP6xsbGyWCy5puuvv97WZ9y4cbnmN2rUyMEjAQAAAAAAKNvcqiiVmZmpFi1aaNKkSYXq//bbb+vw4cO2af/+/QoMDNQ999xj1+/666+36/fjjz86InwAAAAAAAD8l1s9fa9Hjx7q0aNHofv7+/vL39/f9nr+/Pk6ceKEBg0aZNfPy8tLoaGhxRYnAAAAAAAACuZWZ0pdq+nTpysqKkq1atWya9+xY4fCwsJUt25d9e/fn8eCAwAAAAAAOJhbnSl1LQ4dOqT//Oc/SkhIsGtv3769Zs2apYYNG+rw4cMaP368OnXqpC1btsjX1zfPdWVlZSkrK8v2Oj093aGxAwAAAAAAlDZlpij10UcfKSAgQDExMXbtl14O2Lx5c7Vv3161atXSF198oSFDhuS5rvj4eI0fP96R4QIAAAAAAJRqZeLyPWOMZsyYoQceeEDe3t4F9g0ICNB1112nnTt35ttn9OjRSktLs0379+8v7pABAAAAAABKtTJRlFq5cqV27tyZ75lPl8rIyNCuXbtUrVq1fPtYrVb5+fnZTQAAAAAAACg8typKZWRkKCkpSUlJSZKkPXv2KCkpyXZj8tGjR2vAgAG5lps+fbrat2+vpk2b5pr3xBNPaOXKldq7d6/WrFmj3r17y9PTU/369XPoWAAAAAAAAMoyt7qn1MaNG9W1a1fb61GjRkmSBg4cqFmzZunw4cO5npyXlpamL7/8Um+//Xae6zxw4ID69eunY8eOKSgoSDfddJPWrl2roKAgxw0EAAAAAACgjHOrolRkZKSMMfnOnzVrVq42f39/nTp1Kt9lZs+eXRyhAQAAAAAAoAjc6vI9AAAAAAAAlA4UpQAAAAAAAOB0FKUAAAAAAADgdBSlAAAAAAAA4HQUpQAAAAAAAOB0FKUAAAAAAADgdBSlAAAAAAAA4HQUpQAAAAAAAOB0FKUAAAAAAADgdBSlAAAAAAAA4HQUpQAAAAAAAOB0FKUAAAAAAADgdBSlAAAAAAAA4HQUpQAAAAAAAOB0FKUAAAAAAADgdBSlAAAAAAAA4HQUpQAAAAAAAOB0FKUAAAAAAADgdBSlAAAAAAAA4HQUpQAAAAAAAOB0FKUAAAAAAADgdBSlAAAAAAAA4HQUpQAAAAAAAOB0FKUAAAAAAADgdBSlAAAAAAAA4HQUpQAAAAAAAOB0FKUAAAAAAADgdBSlAAAAAAAA4HRuVZRatWqVevXqpbCwMFksFs2fP7/A/itWrJDFYsk1JScn2/WbNGmSateuLR8fH7Vv317r16934CgAAAAAAADgVkWpzMxMtWjRQpMmTSrSctu3b9fhw4dtU3BwsG3e559/rlGjRmns2LH66aef1KJFC0VHR+vIkSPFHT4AAAAAAAD+y8vVARRFjx491KNHjyIvFxwcrICAgDznvfnmmxo6dKgGDRokSZoyZYq+/fZbzZgxQ08//fS1hAsAAAAAAIB8uNWZUlerZcuWqlatmm699VatXr3a1n727Flt2rRJUVFRtjYPDw9FRUUpMTHRFaECABygqJd/f/XVV7r11lsVFBQkPz8/RUREaMmSJc4JFgAAACgjSnVRqlq1apoyZYq+/PJLffnllwoPD1dkZKR++uknSdLRo0eVnZ2tkJAQu+VCQkJy3XfqUllZWUpPT7ebAAAlV1Ev/161apVuvfVWLVq0SJs2bVLXrl3Vq1cvbd682cGRAgAAAGWHW12+V1QNGzZUw4YNba87dOigXbt26a233tK///3vq15vfHy8xo8fXxwhAgCcoKiXf0+cONHu9csvv6wFCxbom2++UatWrYo5OgAAAKBsKtVnSuWlXbt22rlzpySpatWq8vT0VEpKil2flJQUhYaG5ruO0aNHKy0tzTbt37/foTEDAFwrJydHJ0+eVGBgYL59OIsWANzP8ePH1b9/f/n5+SkgIEBDhgxRRkZGgf2HDx+uhg0bqnz58qpZs6Yee+wxpaWlOTFqACg9ylxRKikpSdWqVZMkeXt7q3Xr1lq2bJltfk5OjpYtW6aIiIh812G1WuXn52c3AQBKrwkTJigjI0P33ntvvn3i4+Pl7+9vm8LDw50YIQDgavTv319bt27V0qVLtXDhQq1atUoPPfRQvv0PHTqkQ4cOacKECdqyZYtmzZqlxYsXa8iQIU6MGgBKD7e6fC8jI8N2lpMk7dmzR0lJSQoMDFTNmjU1evRoHTx4UB9//LGkC5df1KlTR9dff73OnDmjDz/8UMuXL9d3331nW8eoUaM0cOBAtWnTRu3atdPEiROVmZlpexofAKBsS0hI0Pjx47VgwQIFBwfn22/06NEaNWqU7XV6ejqFKQAowX777TctXrxYGzZsUJs2bSRJ7777rm677TZNmDBBYWFhuZZp2rSpvvzyS9vrevXq6aWXXtL999+v8+fPy8vLrf68AgCXc6uj5saNG9W1a1fb64tf/gcOHKhZs2bp8OHD2rdvn23+2bNn9fjjj+vgwYOqUKGCmjdvru+//95uHX369NFff/2lMWPGKDk5WS1bttTixYtz3fwcAFD2zJ49Ww8++KDmzJlj96TWvFitVlmtVidFBgC4VomJiQoICLAVpCQpKipKHh4eWrdunXr37l2o9aSlpcnPz4+CFABcBbc6ckZGRsoYk+/8WbNm2b1+6qmn9NRTT11xvcOGDdOwYcOuNTwAQCny2WefafDgwZo9e7Z69uzp6nAAAMUsOTk51xmwXl5eCgwMLPBJ3Jc6evSoXnjhhQIv+cvKylJWVpbtNfccBID/KXP3lAIAlD0ZGRlKSkpSUlKSpP9d/n3x7NrRo0drwIABtv4JCQkaMGCA3njjDbVv317JyclKTk52+o1sY2NjFRMT49RtAoC7e/rpp2WxWAqcfv/992veTnp6unr27KkmTZpo3Lhx+fbjnoMAkD+3OlMKAICrUdTLv6dOnarz588rLi5OcXFxtvaL/QEAJdfjjz+u2NjYAvvUrVtXoaGhOnLkiF37+fPndfz48QKfxC1JJ0+eVPfu3eXr66t58+apXLly+fblnoMAkD+KUgCAUq+ol3+vWLHCsQEBABwmKChIQUFBV+wXERGh1NRUbdq0Sa1bt5YkLV++XDk5OWrfvn2+y6Wnpys6OlpWq1Vff/21fHx8CtwO9xwEgPxx+R4AAACAMqdx48bq3r27hg4dqvXr12v16tUaNmyY+vbta3vy3sGDB9WoUSOtX79e0oWCVLdu3ZSZmanp06crPT3ddol3dna2K4cDAG6JM6UAAChBLpzRdU4Wi7erQwGAUu/TTz/VsGHDdMstt8jDw0N33XWX3nnnHdv8c+fOafv27Tp16pQk6aefftK6deskSfXr17db1549e1S7dm2nxQ4ApQFFKQAASgBzfpdMxjTpzLeSsmQ8gqRz2ZKp6urQAKDUCgwMVEJCQr7za9eubXf595UuBwcAFA1FKQAAXMyc/UnmeKykc5L+e/lHzl8y51NkzpWXyUmXxcPPhRECAAAAxY97SgEA4ELGnJdJfUzSWdkKUv+bK+VkymS86YLIAAAAAMeiKAUAgCtlrZRyjkjKyaeDkU59JZOT6cyoAAAAAIejKAUAgCud/01Xvpr+jJT9pzOiAQAAAJyGe0oBAOBS3srvLKmZb4f+74XF6pxwAAAAACfhTCkAAFzJ2lX5X7onSRbJs4bkWcdZEQEAAABOQVEKAAAXspRrIHl3luSZTw8jS8WHZbGQsgEAAFC68A0XAAAXswS8JZVr+d9XnpIsshWpKsZJ5e9xTWAAAACAA3FPKQAAXMzi4SsFfiqdTZQ5s0jKOSl5hctS/h5ZvGq7OjwAAADAIShKAQBQAlgsHpK1oyzWjq4OBQAAAHAKLt8DAAAAAACA01GUAgAAAAAAgNNRlAIAAAAAAIDTUZQCAAAAAACA01GUAgAAAAAAgNNRlAIAAAAAAIDTUZQCAAAAAACA01GUAgAAAAAAgNNRlAIAAAAAAIDTUZQCAAAAAACA01GUAgAAAAAAgNNRlAIAAAAAAIDTuVVRatWqVerVq5fCwsJksVg0f/78Avt/9dVXuvXWWxUUFCQ/Pz9FRERoyZIldn3GjRsni8ViNzVq1MiBowAAAAAAAIBbFaUyMzPVokULTZo0qVD9V61apVtvvVWLFi3Spk2b1LVrV/Xq1UubN2+263f99dfr8OHDtunHH390RPgA4BSxsbF2hfYqVaqoe/fu+uWXX1wdGgCgBEhOTtbw4cNVt25dWa1WhYeHq1evXlq2bJmrQwMAuMDlfz9cnHbu3OnwbXs5fAvFqEePHurRo0eh+0+cONHu9csvv6wFCxbom2++UatWrWztXl5eCg0NLa4wAcDlunfvrpkzZ0q68MfHc889p9tvv1379u1zcWQAAFfau3evOnbsqICAAL3++utq1qyZzp07pyVLliguLk6///67q0MEALjApX8/XBQUFOTw7bpVUepa5eTk6OTJkwoMDLRr37Fjh8LCwuTj46OIiAjFx8erZs2a+a4nKytLWVlZttfp6ekOixkArobVarUV20NDQ/X000+rU6dO+uuvv5ySXAAAJdOjjz4qi8Wi9evXq2LFirb266+/XoMHD3ZhZAAAV7r07wdncqvL967VhAkTlJGRoXvvvdfW1r59e82aNUuLFy/W5MmTtWfPHnXq1EknT57Mdz3x8fHy9/e3TeHh4c4IHwCuSkZGhj755BPVr19fVapUcXU4AAAXOX78uBYvXqy4uDi7gtRFAQEBzg8KAFCmlZkzpRISEjR+/HgtWLBAwcHBtvZLLwds3ry52rdvr1q1aumLL77QkCFD8lzX6NGjNWrUKNvr9PR0ClMASpSFCxeqUqVKki7cj69atWpauHChPDzK1G8RAIBL7Ny5U8YYHuoDAMjl0r8fpAu1kjlz5jh8u2WiKDV79mw9+OCDmjNnjqKiogrsGxAQoOuuu67AG3pZrVZZrdbiDhMArpo5t106t1mSRcrJVNeuXTV58mRJ0okTJ/T++++rR48eWr9+vWrVquXaYAEATvPHpl3avmGXvMp5SlXPuTocAEAJYIzRb+t2aNfmPSrn460zp7Ls/n6QlOcZtY5Q6otSn332mQYPHqzZs2erZ8+eV+yfkZGhXbt26YEHHnBCdABwbUz2YZnUUdK5Tf9ry0pRBe9A1asbJIuHvyTpww8/lL+/v6ZNm6YXX3zRVeECAJzk4M7DeqnfRO3YtFuySDLSeZ2TRRb9+vOv6t27t6tDBAC4wJ4t+xTf/23t+XWfLT9s00ZVDvdTePVwWcs79wQct7qOIyMjQ0lJSUpKSpIk7dmzR0lJSbanSY0ePVoDBgyw9U9ISNCAAQP0xhtvqH379kpOTlZycrLS0tJsfZ544gmtXLlSe/fu1Zo1a9S7d295enqqX79+Th0bABSVyUmTOdZPOpd0+Rwp+6jM8VgZc1aSZLFY5OHhodOnTzs9TgCAcx1PPqF/dHpeu5L2XmgwF/7jpXIKtITo9fgJed4/NTU11WkxAgCcL3nvEY3qPEZ/bjtwoeG/+cEYoyP7jurFPm/JGOPUmNyqKLVx40a1atVKrVq1kiSNGjVKrVq10pgxYyRJhw8ftnvc+dSpU3X+/HnFxcWpWrVqtmnEiBG2PgcOHFC/fv3UsGFD3XvvvapSpYrWrl3L06kAlHynPpdykiVl55qVdTZHyYd+VvLez/Xbb79p+PDhysjIUK9evZwfJwDAqea98x+lHT2pnOycXPMampY6e/acWjRtqS+//FI7duzQb7/9pnfeeUcREREuiBYA4CxfvP61TmWczjM/SNLahZu0LfEPp8bkVpfvRUZGFli1mzVrlt3rFStWXHGds2fPvsaoAMA1zOkvJeWdUJb8cErVW+yRNEC+vr5q1KiR5syZo8jISGeGCABwge9m/ZDvHxwVLJUU4XGrzlhP6PHHH9fhw4cVFBSk1q1b291LBABQuhhjtPTjFco5n3d+kCRPL099/++Vur5DQ6fF5VZFKQDAJXJO5Nk88+1QzXw79MILr8byqLrAiUEBAFwt/XhGgfPL5VjVtmaUXv9+rJMiAgC42vlz53UmMyvPeddb2kqScrJzlHY03ZlhudflewCAS3hW04W7E+bbQfIMd1Y0AIASompYYIHzPb08FFqLW1UAQFniVc5LvoGVCuzj4WlRcHhVJ0X03206dWsAgGJjqdBHtrsT5ilblgr3OCscAEAJ0fOhKFk88v/RIvt8jroPucWJEQEAXM1isajn0Ch5eOZfBso+n6PowTc7MSqKUgDgvsrfKXk1Vd6Hcotk7Sp5d3J2VAAAF+v1aLTCG4bl+YeHxSLdfN9NahJxnQsiAwC40t2P91JQeJV8C1N/ezRadZrWdGpMFKUAwE1ZLD6yBH4k+fSW/S0Cy0sVBssS8K4sFg7zAFDWVPSroDdX/ktd7u0gT6//5YEKfuV13zN36alZw2SxFHT5NwCgNPKv6qe3V7+kDne0tTujtlLlihr80n2Ke2ew02OymIIeZ4dCSU9Pl7+/v9LS0uTn5+fqcACUQSbnhHRumyQPqVxzWTwqFnpZjmGOwX4FUBKcOJKm3T/vlVc5LzVsV18+FayFXpbjmGOwXwGUBEcPHdfeLfvl7VNOjdo3kLe1XKGXLc7jGE/fA4BSwOJRWbJ2dHUYAIASpnKwv1rf2sLVYQAASpiqYYFXfDCGM3BdBwAAAAAAAJyOohQAoNRbtWqVevXqpbCwMFksFs2fP/+Ky6xYsUI33HCDrFar6tevr1mzZjk8TgAAAKAsoSgFACj1MjMz1aJFC02aNKlQ/ffs2aOePXuqa9euSkpK0siRI/Xggw9qyZIlDo4UAAAAKDu4pxQAoNTr0aOHevToUej+U6ZMUZ06dfTGG29Ikho3bqwff/xRb731lqKjox0VJgAAAFCmcKYUAACXSUxMVFRUlF1bdHS0EhMT810mKytL6enpdhMAAACA/FGUAgDgMsnJyQoJCbFrCwkJUXp6uk6fPp3nMvHx8fL397dN4eHhzggVAAAAcFsUpQAAKAajR49WWlqabdq/f7+rQwIAAABKNO4pVQyMMZLEpRoA3NLFY9fFYxmk0NBQpaSk2LWlpKTIz89P5cuXz3MZq9Uqq9Vqe01uAODuyA+OQX4A4O6KMz9QlCoGJ0+elCQu1QDg1k6ePCl/f39Xh1EiREREaNGiRXZtS5cuVURERKHXQW4AUFqQH4oX+QFAaVEc+YGiVDEICwvT/v375evrK4vF4upw7KSnpys8PFz79++Xn5+fq8O5IuJ1LHeLV3K/mN013m3btiksLMzV4ThMRkaGdu7caXu9Z88eJSUlKTAwUDVr1tTo0aN18OBBffzxx5Kkhx9+WO+9956eeuopDR48WMuXL9cXX3yhb7/9ttDbdFZucLd/c4XBmNwDYyr5rnU8xhidPHmyVOcHVyjJfzsUVmn7rBRVWR5/WR67xPgvjn/fvn2yWCzFkh8oShUDDw8P1ahRw9VhFMjPz8+tPjTE61juFq/kfjG7W7zVq1eXh0fpvc3gxo0b1bVrV9vrUaNGSZIGDhyoWbNm6fDhw9q3b59tfp06dfTtt9/qH//4h95++23VqFFDH374oaKjowu9TWfnBnf7N1cYjMk9MKaS71rGwxlSxc8d/nYorNL2WSmqsjz+sjx2ifH7+/sX2/gpSgEASr3IyMgCr3mfNWtWnsts3rzZgVEBAAAAZVvp/VkcAAAAAAAAJRZFqVLOarVq7Nixdk+EKsmI17HcLV7J/WImXjhbaXwPGZN7YEwlX2kbD0qOsv5vqyyPvyyPXWL8jhi/xfCMVwAAAAAAADgZZ0oBAAAAAADA6ShKAQAAAAAAwOkoSgEAAAAAAMDpKEqVEq+88oosFotGjhxZYL/U1FTFxcWpWrVqslqtuu6667Ro0SLnBHmZwsY8ceJENWzYUOXLl1d4eLj+8Y9/6MyZMw6Pb9y4cbJYLHZTo0aNClxmzpw5atSokXx8fNSsWTOn79uixjxt2jR16tRJlStXVuXKlRUVFaX169eX2HgvNXv2bFksFsXExDg2yEtcTbyu/MxdTbyu+rwhfwcPHtT999+vKlWqqHz58mrWrJk2btxY4DKffvqpWrRooQoVKqhatWoaPHiwjh075qSIC1a7du1c/y4tFovi4uLyXcbVx9YrKeqYXH3sLYyreZ8ucsXxuTCuZkwl6XtTXq5mTBzncbWOHz+u/v37y8/PTwEBARoyZIgyMjIKXCY5OVkPPPCAQkNDVbFiRd1www368ssvnRRx8bmasUtSYmKibr75ZlWsWFF+fn7q3LmzTp8+7YSIi9fVjl+SjDHq0aOHLBaL5s+f79hAHaSo4z9+/LiGDx9uO9bWrFlTjz32mNLS0pwY9dWbNGmSateuLR8fH7Vv3/6K31Gu9Xua17UEi5Jhw4YN+uCDD9S8efMC+509e1a33nqrgoODNXfuXFWvXl1//vmnAgICnBPoJQobc0JCgp5++mnNmDFDHTp00B9//KHY2FhZLBa9+eabDo/z+uuv1/fff2977eWV/0dmzZo16tevn+Lj43X77bcrISFBMTEx+umnn9S0aVOHx3pRUWJesWKF+vXrpw4dOsjHx0evvvqqunXrpq1bt6p69erOCLdI8V60d+9ePfHEE+rUqZMjQ8tTUeItCZ+5osTr6s8bcjtx4oQ6duyorl276j//+Y+CgoK0Y8cOVa5cOd9lVq9erQEDBuitt95Sr169dPDgQT388MMaOnSovvrqKydGn7cNGzYoOzvb9nrLli269dZbdc899+TZv6QcWwtS1DGVhGPvlRR1TBe58vh8JUUdU0k4hl9JUcfEcR7Xon///jp8+LCWLl2qc+fOadCgQXrooYeUkJCQ7zIDBgxQamqqvv76a1WtWlUJCQm69957tXHjRrVq1cqJ0V+bqxl7YmKiunfvrtGjR+vdd9+Vl5eXfv75Z3l4uN95IVcz/osmTpwoi8XihCgdp6jjP3TokA4dOqQJEyaoSZMm+vPPP/Xwww/r0KFDmjt3rpOjL5rPP/9co0aN0pQpU9S+fXtNnDhR0dHR2r59u4KDg3P1L5bvaQZu7eTJk6ZBgwZm6dKlpkuXLmbEiBH59p08ebKpW7euOXv2rPMCzENRYo6LizM333yzXduoUaNMx44dHRylMWPHjjUtWrQodP97773X9OzZ066tffv25u9//3sxR5a/osZ8ufPnzxtfX1/z0UcfFV9QBbiaeM+fP286dOhgPvzwQzNw4EBzxx13OCS2vBQ1Xld/5ooarys/b8jbP//5T3PTTTcVaZnXX3/d1K1b167tnXfeMdWrVy/O0IrNiBEjTL169UxOTk6e80vCsbWorjSmyzn72Hs1CjMmVx6fr8aVxuTqY/jVuNKYOM7jam3bts1IMhs2bLC1/ec//zEWi8UcPHgw3+UqVqxoPv74Y7u2wMBAM23aNIfFWtyuduzt27c3zz33nDNCdKirHb8xxmzevNlUr17dHD582Egy8+bNc3C0xe9axn+pL774wnh7e5tz5845Isxi065dOxMXF2d7nZ2dbcLCwkx8fHye/Yvje5r7lWlhJy4uTj179lRUVNQV+3799deKiIhQXFycQkJC1LRpU7388st2v7A5Q1Fi7tChgzZt2mQ7ZXD37t1atGiRbrvtNkeHKUnasWOHwsLCVLduXfXv31/79u3Lt29iYmKuMUVHRysxMdHRYdopSsyXO3XqlM6dO6fAwEAHRmivqPH+61//UnBwsIYMGeKkCO0VJd6S8JkrSryu/rwht6+//lpt2rTRPffco+DgYLVq1UrTpk0rcJmIiAjt379fixYtkjFGKSkpmjt3bol8H8+ePatPPvlEgwcPzvdX1JJybC2swozpcq449hZFYcfk6uNzURRmTCXhGF4UhRkTx3lcrcTERAUEBKhNmza2tqioKHl4eGjdunX5LtehQwd9/vnnOn78uHJycjR79mydOXNGkZGRToi6eFzN2I8cOaJ169YpODhYHTp0UEhIiLp06aIff/zRWWEXm6t970+dOqX77rtPkyZNUmhoqDNCdYirHf/l0tLS5OfnV6irQlzl7Nmz2rRpk933Lg8PD0VFReX7vas4vqeV3D2CK5o9e7Z++uknbdiwoVD9d+/ereXLl6t///5atGiRdu7cqUcffVTnzp3T2LFjHRztBUWN+b777tPRo0d10003yRij8+fP6+GHH9Yzzzzj4Eil9u3ba9asWWrYsKEOHz6s8ePHq1OnTtqyZYt8fX1z9U9OTlZISIhdW0hIiJKTkx0e60VFjfly//znPxUWFlaogmFxKGq8P/74o6ZPn66kpCSnxHe5osbr6s9cUeN15ecNedu9e7cmT56sUaNG6ZlnntGGDRv02GOPydvbWwMHDsxzmY4dO+rTTz9Vnz59dObMGZ0/f169evXSpEmTnBz9lc2fP1+pqamKjY3Nt09JOLYWRWHGdDlnH3uLqjBjcvXxuagKMyZXH8OLqjBj4jiPq5WcnJzr0h0vLy8FBgYWeDz+4osv1KdPH1WpUkVeXl6qUKGC5s2bp/r16zs65GJzNWPfvXu3pAv395wwYYJatmypjz/+WLfccou2bNmiBg0aODzu4nK17/0//vEPdejQQXfccYejQ3Soqx3/pY4ePaoXXnhBDz30kCNCLDZHjx5VdnZ2nt+7fv/99zyXKZbvaUU/oQslwb59+0xwcLD5+eefbW1XuhSuQYMGJjw83Jw/f97W9sYbb5jQ0FBHhmpzNTH/8MMPJiQkxEybNs388ssv5quvvjLh4eHmX//6lxMitnfixAnj5+dnPvzwwzznlytXziQkJNi1TZo0yQQHBzsjvDxdKeZLxcfHm8qVK9u9P85WULzp6emmdu3aZtGiRbY2V18ecqX96+rP3OWuFG9J+rzhgnLlypmIiAi7tuHDh5sbb7wx32W2bt1qqlWrZl577TXz888/m8WLF5tmzZqZwYMHOzrcIuvWrZu5/fbbC+xTEo+tBSnMmC5VEo69V3KlMZXE4/OVFOZ9KmnH8CspzJg4zuNy//znP42kAqfffvvNvPTSS+a6667LtXxQUJB5//33813/sGHDTLt27cz3339vkpKSzLhx44y/v7/55ZdfHDmsQnHk2FevXm0kmdGjR9u1N2vWzDz99NMOGU9ROXL8CxYsMPXr1zcnT560tamEXb7n6H/7F6WlpZl27dqZ7t27l/jLwQ8ePGgkmTVr1ti1P/nkk6Zdu3Z5LlMc39M4U8pNbdq0SUeOHNENN9xga8vOztaqVav03nvvKSsrS56ennbLVKtWTeXKlbNrb9y4sZKTk3X27Fl5e3uXuJiff/55PfDAA3rwwQclSc2aNVNmZqYeeughPfvss069UWBAQICuu+467dy5M8/5oaGhSklJsWtLSUlx6emqV4r5ogkTJuiVV17R999/f8WbzztSQfHu2rVLe/fuVa9evWxtOTk5ki78WrF9+3bVq1fPabFKV96/rv7MXe5K8ZakzxsuqFatmpo0aWLX1rhx4wKfXBQfH6+OHTvqySeflCQ1b95cFStWVKdOnfTiiy+qWrVqDo25sP788099//33V7z5ekk8tuansGO6qKQcewtSmDGVxONzQQr7PpW0Y3hBCjsmjvO43OOPP37FMzvr1q2r0NBQHTlyxK79/PnzOn78eL7H4127dum9997Tli1bdP3110uSWrRoof/7v//TpEmTNGXKlGIZw9Vy5Ngv5tq8cnhRbq3hSI4c//Lly7Vr165cD4a466671KlTJ61YseIa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/LKzQgZchvzg3sgPRVPY/PC3v/1Nqamp8vX11YQJE5SUlOS8IN2Rq6tiKNkefvhhI8msXr26UP0v//Vj7NixJq9/ZjNnzsxVMd6wYYPp1q2bqVKlivHx8TG1a9c2gwYNMsYYs2fPHiMp13Rpdfq3334zd911l6lcubKxWq2mdevWZsGCBXlud8WKFeaRRx4xQUFBJiAgIN+YatWqZXr27Gn+7//+z7Rt29ZYrVZTp04d89FHH+Ua088//2w6d+5sfHx8TPXq1c0LL7xgZsyYUajK+M8//2wGDhxo6tSpY6xWqwkJCTGDBg2y+6X14r68fCpo3QMHDjS1atVy2j7cu3eveeSRR8x1111nfHx8TGBgoLn77rvtYry4/OXTxV9CunTpYrp06WK3zZSUFDN48GATHBxsrFarad68uZk1a5Zdn4vje/31180HH3xg6tata7y9vU2bNm3M+vXrC9z/xhjzn//8x0gyixYtMsYYk5mZac6fP3/F5YCygFxALigrueBSVzpjoG3btqZt27a52rt162bq1atXpG0B7or8QH4gPxSsqGevlUWcKYUCffPNN6pbt646dOjg0O0cOXJE3bp1U1BQkJ5++mkFBARo7969+uqrryRJQUFBmjx5sh555BH17t1bd955pySpefPmkqStW7eqY8eOql69up5++mlVrFhRX3zxhWJiYvTll1+qd+/edtt79NFHFRQUpDFjxigzM7PA2Hbu3Km7775bQ4YM0cCBAzVjxgzFxsaqdevWuv766yVduKdE165dZbFYNHr0aFWsWFEffvihrFZroca/dOlS7d69W4MGDVJoaKi2bt2qqVOnauvWrVq7dq0sFovuvPNO/fHHH/rss8/01ltvqWrVqrZ9UxjO2IcbNmzQmjVr1LdvX9WoUUN79+7V5MmTFRkZqW3btqlChQrq3LmzHnvsMb3zzjt65pln1LhxY0my/fdyp0+fVmRkpHbu3Klhw4apTp06mjNnjmJjY5WamqoRI0bY9U9ISNDJkyf197//XRaLRa+99pruvPNO7d69u8Bfg77//ntJktVqVZs2bbRp0yZ5e3urd+/eev/99xUYGFio/QyURuQCckFZyQWFlZOTo19++UWDBw/ONa9du3b67rvvdPLkSfn6+l7ztoCSjPxAfiA/4Jq5uiqGkistLc1IMnfccUehl7naXz/mzZtnJJkNGzbku+6CKtK33HKLadasmd010Tk5OaZDhw6mQYMGubZ700035ToLJr9fPySZVatW2dqOHDlirFarefzxx21tw4cPNxaLxWzevNnWduzYMRMYGFioyvipU6dytX322We5tl3USvulv34Y4/h9mNc4EhMTjSTz8ccf29oKuk788l8/Jk6caCSZTz75xNZ29uxZExERYSpVqmTS09ONMf/79aNKlSrm+PHjtr4LFiwwksw333yTewdd4m9/+5tt+f79+5u5c+ea559/3nh5eZkOHTqU6PsHAI5ELiAXlKVccKmC9tPFef/6179yzZs0aZKRZH7//fdCbwtwR+QH8gP5YewV+3Km1JXx9D3kKz09XZKc8itfQECAJGnhwoU6d+5ckZY9fvy4li9frnvvvVcnT57U0aNHdfToUR07dkzR0dHasWOHDh48aLfM0KFDC31NeJMmTdSpUyfb66CgIDVs2NDu6TqLFy9WRESEWrZsaWsLDAxU//79C7WNS+9fdObMGR09elQ33nijJOmnn34q1DquRXHtw0vHce7cOR07dkz169dXQEDAVY9j0aJFCg0NVb9+/Wxt5cqV02OPPaaMjAytXLnSrn+fPn1UuXJl2+uL792VnoaUkZEhSWrbtq0++eQT3XXXXfrXv/6lF154QWvWrNGyZcuuKn7A3ZELLiAXlI1cUFgXnyaW11kOPj4+dn2A0or8cAH5gfyAa0NRCvny8/OTJJ08edLh2+rSpYvuuusujR8/XlWrVtUdd9yhmTNnKisr64rL7ty5U8YYPf/88woKCrKbxo4dK+nCKb+XqlOnTqFjq1mzZq62ypUr68SJE7bXf/75p+rXr5+rX15teTl+/LhGjBihkJAQlS9fXkFBQbYY09LSCh3r1SqufXj69GmNGTNG4eHhslqtqlq1qoKCgpSamnrV4/jzzz/VoEEDeXjYH64unsL7559/2rVf/n5dTDqXvl95uZgkL01oknTfffdJuvCIXaAsIhdcQC4oG7mgsC7mjLz+bZ45c8auD1BakR8uID+QH3BtuKcU8uXn56ewsDBt2bLlqtdhsVjybM/Ozs7Vb+7cuVq7dq2++eYbLVmyRIMHD9Ybb7yhtWvXqlKlSvluIycnR5L0xBNPKDo6Os8+lx/wi/JFMb9fSYwxhV7Hldx7771as2aNnnzySbVs2VKVKlVSTk6OunfvbhufIxXXPhw+fLhmzpypkSNHKiIiQv7+/rJYLOrbt69TxiFd/fsVFhYmSQoJCbFrDw4OlkSiQtlFLriAXHBBac8FhRUYGCir1arDhw/nmnex7WJeAUor8sMF5IcLyA+4WhSlUKDbb79dU6dOVWJioiIiIoq8/MXKc2pqqu20Wyl3xfqiG2+8UTfeeKNeeuklJSQkqH///po9e7YefPDBfJNW3bp1JV04TTMqKqrIMRaHWrVqaefOnbna82q73IkTJ7Rs2TKNHz9eY8aMsbXv2LEjV9/89kFhOXofzp07VwMHDtQbb7xhaztz5oxSU1MLFUdeatWqpV9++UU5OTl2v4D8/vvvtvnFoXXr1po2bVquU48PHTokqfA3iQRKI3JB4ZALLnDnXFBYHh4eatasmTZu3Jhr3rp161S3bl1uco4ygfxQOOSHC8pCfkDRcfkeCvTUU0+pYsWKevDBB5WSkpJr/q5du/T222/nu3y9evUkSatWrbK1ZWZm6qOPPrLrd+LEiVzV6YvXXF88LbdChQqSlOugFRwcrMjISH3wwQd5/mL5119/5RtfcYmOjlZiYqKSkpJsbcePH9enn356xWUvVusvH//EiRNz9a1YsaKk3PugsBy9Dz09PXON49133831a1dRxnHbbbcpOTlZn3/+ua3t/Pnzevfdd1WpUiV16dKlULFdyR133CGr1aqZM2fa/VLz4YcfSpJuvfXWYtkO4I7IBYVDLrjAnXNBUdx9993asGGDXWFq+/btWr58ue655x6nxwO4AvmhcMgPF5SV/ICi4UwpFKhevXpKSEhQnz591LhxYw0YMEBNmzbV2bNntWbNGtvjNvPTrVs31axZU0OGDNGTTz4pT09PzZgxQ0FBQdq3b5+t30cffaT3339fvXv3Vr169XTy5ElNmzZNfn5+uu222yRdOAW0SZMm+vzzz3XdddcpMDBQTZs2VdOmTTVp0iTddNNNatasmYYOHaq6desqJSVFiYmJOnDggH7++WeH7qennnpKn3zyiW699VYNHz7c9pjXmjVr6vjx4wVW+/38/NS5c2e99tprOnfunKpXr67vvvtOe/bsydW3devWkqRnn31Wffv2Vbly5dSrVy/bgftKHL0Pb7/9dv373/+Wv7+/mjRposTERH3//feqUqWKXb+WLVvK09NTr776qtLS0mS1WnXzzTfbLpW71EMPPaQPPvhAsbGx2rRpk2rXrq25c+dq9erVmjhxYrH9Eh0aGqpnn31WY8aMUffu3RUTE6Off/5Z06ZNU79+/dS2bdti2Q7gjsgFhUMuuMCdc4Ek/fvf/9aff/6pU6dOSbrwx/KLL74oSXrggQdsv7o/+uijmjZtmnr27KknnnhC5cqV05tvvqmQkBA9/vjjxRYPUJKRHwqH/HBBWckPaWlpevfddyVJq1evliS99957CggIUEBAgIYNG1ZsMZUKTn3WH9zWH3/8YYYOHWpq165tvL29ja+vr+nYsaN599137R4LevljXo0xZtOmTaZ9+/bG29vb1KxZ07z55pu5Hqn6008/mX79+pmaNWsaq9VqgoODze233242btxot641a9aY1q1bG29v71yP4dy1a5cZMGCACQ0NNeXKlTPVq1c3t99+u5k7d66tz8Xt5vU42fwe89qzZ89cfS9/FKkxxmzevNl06tTJWK1WU6NGDRMfH2/eeecdI8kkJycXuH8PHDhgevfubQICAoy/v7+55557zKFDh/J81OgLL7xgqlevbjw8PK74eNHLH/NqjGP34YkTJ8ygQYNM1apVTaVKlUx0dLT5/fff8/x3MW3aNFO3bl3j6elp98jXvPZtSkqKbb3e3t6mWbNmZubMmXZ9Lj7m9fXXX88VV177MS85OTnm3XffNdddd50pV66cCQ8PN88995w5e/bsFZcFygJygT1yQenMBV26dDGS8pwufzz5/v37zd133238/PxMpUqVzO2332527NhxxW0ApQ35wR75oWznh4vbymu6fH/DGIsx3NELcJSRI0fqgw8+UEZGRqEfKwsAKF3IBQCAvJAfAO4pBRSb06dP270+duyY/v3vf+umm24iyQBAGUEuAADkhfwA5I17SgHFJCIiQpGRkWrcuLFSUlI0ffp0paen6/nnn3d1aAAAJyEXAADyQn4A8kZRCigmt912m+bOnaupU6fKYrHohhtu0PTp09W5c2dXhwYAcBJyAQAgL+QHIG/cUwoAAAAAAABOxz2lAAAAAAAA4HQUpQAAAAAAAOB03FOqGOTk5OjQoUPy9fWVxWJxdTgAUCTGGJ08eVJhYWHy8OC3iuJCbgDg7sgPjkF+AODuijM/UJQqBocOHVJ4eLirwwCAa7J//37VqFHD1WGUGuQGAKUF+aF4kR8AlBbFkR8o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L72kO++8U71791ZAgOsQe/bsadIEAQCtD8cbAGgZ2J8DAOrTqKKUJH311Vd666231LFjR91xxx01DioAADSFpjzebN++XfPnz1dBQYGKioq0YcMGjRo1qt4+DodDc+fO1WuvvSabzabOnTsrIyND99577yXnAQCtEX8/AADq0qgjwrJlyzRjxgwlJCRo//79CgsLc1deAIBWrKmPN+Xl5YqOjta9996rO++8s0F9fvWrX6m4uFjLly9Xjx49VFRUpOrq6svKAwBaG/5+AADUp8FFqaSkJOXn5+ull15ScnKyO3Oq15IlSzR//nzZbDZFR0dr8eLFGjRoUK2xy5Yt06uvvqp9+/ZJkmJjY/XMM8+4xE+YMEGrV6926ZeYmKjs7Gz3TQIAUCd3HG+GDx+u4cOHNzg+OztbH3zwgb788kuFhoZKkrp27dokuQBAa9Fc/n4AADRfDS5KVVVV6dNPP9VPfvITd+ZTr9dff11paWnKyspSXFycXnzxRSUmJurQoUMKDw+vEb9t2zbdfffduvHGGxUUFKTnnntOw4YN0/79+3XVVVc545KSkrRy5Urna7PZ7JH5AABqag7Hm02bNmngwIF6/vnn9f/+3//TFVdcof/6r//SvHnz1LZt21r7OBwOORwO52u73e6pdAGgWWoO+3MAQPPW4KJUTk6OO/NokIULFyolJUUTJ06UJGVlZelvf/ubVqxYoVmzZtWIX7NmjcvrP/3pT/rLX/6i3Nxcl29rzGazrFare5MHADRIczjefPnll/rwww8VFBSkDRs26PTp03rggQd05swZly8xfigzM1NPPvmkhzMFgOarOezPAQDNm5+3E2ioCxcuqKCgQAkJCc42Pz8/JSQkKC8vr0FjnD9/XhUVFc5LMb63bds2hYeHq2fPnrr//vt15syZesdxOByy2+0uCwCg5aiurpbJZNKaNWs0aNAgjRgxQgsXLtTq1av173//u9Y+6enpKi0tdS7Hjx/3cNYAAACAb/GZotTp06dVVVWliIgIl/aIiAjZbLYGjTFz5kxFRka6FLaSkpL06quvKjc3V88995w++OADDR8+XFVVVXWOk5mZqeDgYOcSFRV1aZMCADRLnTt31lVXXaXg4GBnW+/evWUYhr7++uta+5jNZlksFpcFAAAAQN1azfNYn332Wa1bt07btm1TUFCQs33s2LHOf/fr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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "Dendrogram.plot_hierarchial_split(hierarchial_clustering_sequence, coreset_df)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The hierarchical clustering can be converted to flat clustering by drawing a line perpendicular to the branches. Any data point that intersects the line is considered to be in the same cluster. The function below performs this task at threshold height of 1.5. If you want to use the number of clusters instead of height, you can use `dendo.get_clusters_using_k()` method. You pass the number of desired clusters as an argument. The figure below shows the clusters that are formed at threshold height of 1.5." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAy0AAANICAYAAADKKQDvAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/NK7nSAAAACXBIWXMAAA9hAAAPYQGoP6dpAAA+1UlEQVR4nO3deZhcZYH3729n6c5CFgKJGAghgIDsDrsRA4hg2AQFFFE2QQaQETH4En1ZAjpRdHhBBEQGiCNk2JQZwAFEERyBsA6joALhl2hETFg7G1lIzu+PtjvpdCekk3TqSXLf13UuuqqeOvV0pbroT5+l6qqqqgIAAFCoLrWeAAAAwLKIFgAAoGiiBQAAKJpoAQAAiiZaAACAookWAACgaKIFAAAommgBAACKJloAAICiiRZgnbDZZpvlhBNOqPU01mj33ntvdt555/To0SN1dXV56623aj2ldVZdXV0uvPDCWk8DYLURLcAqMW7cuNTV1bUsPXr0yODBg3PggQfme9/7XmbMmFHrKbISXn/99Rx99NHp2bNnrrzyyvz4xz9O7969l3mfl156Kaeeemo233zz9OjRI3379s3w4cNz+eWX5+23315NM+8cs2fPzoUXXpgHH3yw1lNZbS699NLU1dXlF7/4xVLHXHvttamrq8udd97Zcl1VVfnxj3+cD3/4w+nfv3969eqVHXbYId/4xjcye/bsNuvYZ599Wr2XLL5ss802nfK9AeXrVusJAGuXiy66KMOGDcv8+fPzt7/9LQ8++GDOOuusXHrppbnzzjuz44471nqKrIAnnngiM2bMyMUXX5z999//Xcf/7Gc/y1FHHZWGhoYcd9xx2X777TNv3rz85je/yTnnnJPnnnsuP/zhD1fDzDvH7NmzM2bMmCRNv2Svbm+//Xa6dVu9/wv/9Kc/nXPOOSfjx49f6mtg/Pjx2WCDDTJy5MgkyYIFC/KZz3wmt956a/bee+9ceOGF6dWrV/77v/87F1xwQW699db84he/yKBBg1qtZ5NNNsnYsWPbrL9fv36r/hsD1giiBVilRo4cmV133bXl8ujRo/PAAw/kkEMOyWGHHZY//OEP6dmzZw1nuHSzZs16160Hq8qcOXNSX1+fLl3WjA3e06ZNS5L079//XcdOmjQpn/70pzN06NA88MADee9739ty2xlnnJGJEyfmZz/72UrPqaqqzJkzp9jXU2fq0aPHan/MwYMHZ999981Pf/rTXH311WloaGh1+8svv5xf//rX+cIXvpDu3bsnSS655JLceuutGTVqVL7zne+0jP3CF76Qo48+OocffnhOPPHENq+Hfv365bOf/Wznf1PAGmPN+L8lsEbbb7/9ct555+VPf/pTbrzxxla3/fGPf8yRRx6ZAQMGpEePHtl1111b7VqSLNr17OGHH87ZZ5+dgQMHpnfv3jniiCPy6quvthpbVVW+8Y1vZJNNNkmvXr2y77775rnnnmszp+Z1PvTQQzn99NMzaNCgbLLJJi23X3XVVdluu+3S0NCQwYMH54wzzmj3GI4rr7wym2++eXr27Jndd989//3f/5199tmn1V/fH3zwwdTV1eXmm2/O//2//zcbb7xxevXqlenTp+eNN97IqFGjssMOO2S99dZL3759M3LkyPzv//5vq8dpXsett96aMWPGZOONN06fPn1y5JFHprGxMXPnzs1ZZ52VQYMGZb311suJJ56YuXPnLte/z2233ZZddtklPXv2zIYbbpjPfvazefnll1tu32effXL88ccnSXbbbbfU1dUt8/igSy65JDNnzsx1113XKliabbnllvnSl77Ucvmdd97JxRdfnC222CINDQ3ZbLPN8rWvfa3N/DfbbLMccsghue+++7LrrrumZ8+eueaaa5Ikb731Vs4666wMGTIkDQ0N2XLLLfPtb387CxcubLWOm2++Obvsskv69OmTvn37Zocddsjll1/easy7rWvy5MkZOHBgkmTMmDEtuy4t6xiTCy+8MHV1dW2ub34dTp48ueW6J598MgceeGA23HDD9OzZM8OGDctJJ53U6n5LPl7z+idOnJgTTjgh/fv3T79+/XLiiSe22QXr7bffzj/90z9lww03TJ8+fXLYYYfl5ZdfXq7jZD772c+msbGx3ei8+eabs3Dhwhx77LEtj/Od73wnW221VbtbTQ499NAcf/zx+a//+q88/vjjy3xcAFtagNXic5/7XL72ta/l5z//eU455ZQkyXPPPZfhw4dn4403zrnnnpvevXvn1ltvzeGHH56f/OQnOeKII1qt48wzz8z666+fCy64IJMnT85ll12WL37xi7nllltaxpx//vn5xje+kYMOOigHHXRQnn766RxwwAGZN29eu/M6/fTTM3DgwJx//vmZNWtWkqZfAMeMGZP9998/p512Wp5//vlcffXVeeKJJ/Lwww+3/BX56quvzhe/+MXsvffe+fKXv5zJkyfn8MMPz/rrr98qgJpdfPHFqa+vz6hRozJ37tzU19fn97//ff7jP/4jRx11VIYNG5apU6fmmmuuyYgRI/L73/8+gwcPbrWOsWPHpmfPnjn33HMzceLEXHHFFenevXu6dOmSN998MxdeeGEmTJiQcePGZdiwYTn//POX+e8ybty4nHjiidltt90yduzYTJ06NZdffnkefvjh/M///E/69++fr3/969l6663zwx/+sGX3vy222GKp67zrrruy+eab54Mf/OAyH7vZySefnB/96Ec58sgj85WvfCWPPfZYxo4dmz/84Q+54447Wo19/vnnc8wxx+TUU0/NKaeckq233jqzZ8/OiBEj8vLLL+fUU0/NpptumkceeSSjR4/OK6+8kssuuyxJcv/99+eYY47JRz7ykXz7299OkvzhD3/Iww8/3BJRy7OugQMH5uqrr85pp52WI444Ip/4xCeSZJXs+jht2rQccMABGThwYM4999z0798/kydPzk9/+tPluv/RRx+dYcOGZezYsXn66afzr//6rxk0aFDL95skJ5xwQm699dZ87nOfy5577pmHHnooBx988HKt/xOf+EROO+20jB8/vuX7bjZ+/PgMHTo0w4cPT5L85je/yZtvvpkvfelLS92V7bjjjssNN9yQu+66K7vvvnvL9QsWLMhrr73WZnzPnj1X29ZQoDAVwCpwww03VEmqJ554Yqlj+vXrV33gAx9oufyRj3yk2mGHHao5c+a0XLdw4cLqgx/8YPW+972vzbr333//auHChS3Xf/nLX666du1avfXWW1VVVdW0adOq+vr66uCDD2417mtf+1qVpDr++OPbrPNDH/pQ9c4777Rc37yOAw44oFqwYEHL9d///verJNX1119fVVVVzZ07t9pggw2q3XbbrZo/f37LuHHjxlVJqhEjRrRc96tf/apKUm2++ebV7NmzWz0nc+bMafU4VVVVkyZNqhoaGqqLLrqozTq23377at68eS3XH3PMMVVdXV01cuTIVuvYa6+9qqFDh1bLMm/evGrQoEHV9ttvX7399tst1999991Vkur8889v83wt69+3qqqqsbGxSlJ9/OMfX+a4Zs8880yVpDr55JNbXT9q1KgqSfXAAw+0XDd06NAqSXXvvfe2GnvxxRdXvXv3rl544YVW15977rlV165dqz//+c9VVVXVl770papv376t/r2XtLzrevXVV6sk1QUXXLBc3+cFF1xQtfe/3ObnddKkSVVVVdUdd9yxXM/zko/dvP6TTjqp1bgjjjii2mCDDVouP/XUU1WS6qyzzmo17oQTTlju7+eoo46qevToUTU2NrZc98c//rFKUo0ePbrlussuu6xKUt1xxx1LXdcbb7xRJak+8YlPtFw3YsSIKkm7y6mnnvqu8wPWTnYPA1ab9dZbr+UsYm+88UYeeOCBHH300ZkxY0Zee+21vPbaa3n99ddz4IEH5sUXX2y1i1LStB/84rvY7L333lmwYEH+9Kc/JUl+8YtfZN68eTnzzDNbjTvrrLOWOqdTTjklXbt2bbncvI6zzjqr1fEmp5xySvr27duyW8yTTz6Z119/PaecckqrvyIfe+yxWX/99dt9rOOPP77N8RcNDQ0tj7NgwYK8/vrrWW+99bL11lvn6aefbrOO4447rmVLT5Lsscceqaqqze5De+yxR6ZMmZJ33nlnqd/7k08+mWnTpuX0009vdYzEwQcfnG222WaFjjuZPn16kqRPnz7LNf6//uu/kiRnn312q+u/8pWvJEmbOQwbNiwHHnhgq+tuu+227L333ll//fVbXkevvfZa9t9//yxYsCC//vWvkzQdjzNr1qzcf//9S53P8q6rszQfM3T33Xdn/vz5Hb7/P/7jP7a6vPfee+f1119v+Xe59957kzRtYVzcmWeeudyP8dnPfjZz5sxptfVn/PjxSdKya1iSlp/1Zb0Wmm9b8uyCm222We6///42y7J+loG1m93DgNVm5syZLWcJmjhxYqqqynnnnZfzzjuv3fHTpk3Lxhtv3HJ50003bXV7cxy8+eabSdISL+973/tajRs4cOBSQ2LYsGGtLjevY+utt251fX19fTbffPOW25v/u+WWW7Ya161bt2y22WbL9VhJsnDhwlx++eW56qqrMmnSpCxYsKDltg022KDN+CWfg+azKQ0ZMqTN9QsXLkxjY2O761n8e1jye02SbbbZJr/5zW/avd+y9O3bN0nbX0KX5k9/+lO6dOnS5nncaKON0r9//5Y5NmvvOXzxxRfz29/+tuU4kyU1n0Tg9NNPz6233pqRI0dm4403zgEHHJCjjz46H/vYxzq8rs4yYsSIfPKTn8yYMWPy//7f/8s+++yTww8/PJ/5zGfaHPjenmX9jPTt27fl+V7yeVzy+V+WkSNHZsCAARk/fnzLsU3//u//np122inbbbddy7ilBcnimm9b8uxhvXv3Xq6z1AHrDtECrBZ/+ctf0tjY2PLLUfNBzaNGjWrzl/NmS/4itfgWkcVVVbXC81qdZ55q77H++Z//Oeedd15OOumkXHzxxRkwYEC6dOmSs846q81B5MnSn4POeG5WRN++fTN48OA8++yzHbpfewept6e953DhwoX56Ec/mq9+9avt3merrbZK0vSL8TPPPJP77rsv99xzT+65557ccMMNOe644/KjH/2oQ+vqqKV9f4tHavO422+/PRMmTMhdd92V++67LyeddFL+5V/+JRMmTMh66623zMdZHa+D7t275+ijj861116bqVOn5s9//nNefPHFXHLJJa3GbbvttkmS3/72tzn88MPbXddvf/vbJMnmm2++yuYHrJ1EC7Ba/PjHP06SlkBp/iWle/fuq+wvqkOHDk3S9NfyxX8JevXVV1u2xizvOp5//vlW65g3b14mTZrUMtfmcRMnTsy+++7bMu6dd97J5MmTl/ug7Ntvvz377rtvrrvuulbXv/XWW9lwww2Xax0ravHvdb/99mt12/PPP99ye0cdcsgh+eEPf5hHH300e+2117vOYeHChXnxxRfz/ve/v+X6qVOn5q233lquOWyxxRaZOXPmcr2O6uvrc+ihh+bQQw/NwoULc/rpp+eaa67Jeeedly233HK517W8kdWseYvHW2+91eq00UtuSWq25557Zs8998w3v/nNjB8/Pscee2xuvvnmnHzyyR163CU1P9+TJk1qtUVy4sSJHVrPsccemx/84Ae55ZZbMmnSpNTV1eWYY45pNWb48OHp379/xo8fn69//evtBtW//du/JUmOOuqoFfhugHWJY1qATvfAAw/k4osvzrBhw1r2eR80aFD22WefXHPNNXnllVfa3GfJUxkvj/333z/du3fPFVdc0eovy81nj1reddTX1+d73/teq3Vcd911aWxsbDnL0q677poNNtgg1157bavjRm666ablDqSk6S/jS/4V/LbbbmtzPE9n2HXXXTNo0KD84Ac/aHV64XvuuSd/+MMflvuMUkv66le/mt69e+fkk0/O1KlT29z+0ksvtZxm+KCDDkrS9t/o0ksvTZLlmsPRRx+dRx99NPfdd1+b2956662Wf5/XX3+91W1dunRpicvm739519WrV6+W65ZH89nWFj8mZtasWS1beJq9+eabbV4PO++8c6s5rozmPxpcddVVra6/4oorOrSe4cOHZ7PNNsuNN96YW265JSNGjGhzxrxevXrlq1/9ap5//vl8/etfb7OOn/3sZxk3blwOPfTQ7LDDDh38ToB1jS0twCp1zz335I9//GPeeeedTJ06NQ888EDuv//+DB06NHfeeWerA76vvPLKfOhDH8oOO+yQU045JZtvvnmmTp2aRx99NH/5y1/afFbJuxk4cGBGjRqVsWPH5pBDDslBBx2U//mf/8k999yz3FstBg4cmNGjR2fMmDH52Mc+lsMOOyzPP/98rrrqquy2224tH3hXX1+fCy+8MGeeeWb222+/HH300Zk8eXLGjRuXLbbYYrn/En/IIYfkoosuyoknnpgPfvCD+d3vfpebbrpptewu071793z729/OiSeemBEjRuSYY45pOeXxZpttli9/+csrtN4tttgi48ePz6c+9am8//3vz3HHHZftt98+8+bNyyOPPJLbbrut5ViInXbaKccff3x++MMf5q233sqIESPy+OOP50c/+lEOP/zwVluxluacc87JnXfemUMOOSQnnHBCdtlll8yaNSu/+93vcvvtt2fy5MnZcMMNc/LJJ+eNN97Ifvvtl0022SR/+tOfcsUVV2TnnXdu2cqzvOvq2bNntt1229xyyy3ZaqutMmDAgGy//fbZfvvt253jAQcckE033TSf//znc84556Rr1665/vrrM3DgwPz5z39uGfejH/0oV111VY444ohsscUWmTFjRq699tr07du3JfBWxi677JJPfvKTueyyy/L666+3nPL4hRdeSLL8W5Dq6urymc98Jv/8z/+cJLnooovaHffVr341zzzzTL797W/n0UcfzSc/+cn07Nkzv/nNb3LjjTdmu+22y7hx49rcr7Gxsc1nOjXzoZOwjqrZecuAtUrzqVubl/r6+mqjjTaqPvrRj1aXX355NX369Hbv99JLL1XHHXdctdFGG1Xdu3evNt544+qQQw6pbr/99jbrXvI0sM2nAf7Vr37Vct2CBQuqMWPGVO9973urnj17Vvvss0/17LPPVkOHDm33lMdLO7Xs97///WqbbbapunfvXr3n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M2fmpZdeynvf+96ljmloaEjfvn1bLQAAAMurQ9EyatSoPPTQQ5k8eXIeeeSRHHHEEenatWuOOeaYzpofAACwjuvQMS1/+ctfcswxx+T111/PwIED86EPfSgTJkzIwIEDO2t+AADAOq5D0XLzzTd31jwAAADatVLHtAAAAHQ20QIAABRNtAAAAEUTLQAAQNFECwAAUDTRAgAAFE20AAAARRMtAABA0UQLAABQNNECAAAUTbQAAABFEy0AAEDRRAsAAFA00QIAABRNtAAAAEUTLQAAQNFECwAAUDTRAgAAFE20AAAARRMtAABA0UQLAABQNNECAAAUTbQAAABFEy0AAEDRRAsAAFA00QIAABRNtAAAAEUTLQAAQNFECwAAUDTRAgAAFE20AAAARRMtAABA0UQLAABQNNECAAAUTbQAAABFEy0AAEDRRAsAAFA00QIAABRNtAAAAEUTLQAAQNFECwAAUDTRAgAAFE20AAAARRMtAABA0UQLAABQNNECAAAUTbQAAABFEy0AAEDRRAsAAFA00QIAABRNtAAAAEUTLQAAQNFECwAAULRutZ4Ay6Gqkvmzaz2Ltc+82e1/zarTvVdSV1frWQAAazjRUrqqSq4/MJnyWK1nsnb77pa1nsHaacieyUn3ChcAYKXYPax082cLFtZcUybYSggArDRbWtYkoyYm9b1qPQt4d/Nm23oFAKwyomVNUt8rqe9d61kAAMBqZfcwAACgaKIFAAAommgBAACKJloAAICiiRYAAKBoogUAACiaaAEAAIomWgAAgKKJFgAAoGiiBQAAKJpoAQAAiiZaAACAookWAACgaKIFAAAommgBAACKJloAAICiiRYAAKBoogUAACiaaAEAAIomWgAAgKKJFgAAoGiiBQAAKJpoAQAAiiZaAACAookWAACgaKIFAAAommgBAACKJloAAICiiRYAAKBoogUAACiaaAEAAIomWgAAgKKJFgAAoGiiBQAAKJpoAQAAiiZaAACAookWAACgaKIFAAAommgBAACKJloAAICiiRYAAKBoogUAACiaaAEAAIomWgAAgKKJFgAAoGiiBQAAKJpoAQAAiiZaAACAookWAACgaKIFAAAommgBAACKJloAAICiiRYAAKBoogUAACiaaAEAAIomWgAAgKKJFgAAoGiiBQAAKJpoAQAAiiZaAACAookWAACgaKIFAAAo2kpFy7e+9a3U1dXlrLPOWkXTAQAAaG2Fo+WJJ57INddckx133HFVzgcAAKCVFYqWmTNn5thjj821116b9ddff1XPCQAAoMUKRcsZZ5yRgw8+OPvvv/+qng8AAEAr3Tp6h5tvvjlPP/10nnjiieUaP3fu3MydO7fl8vTp0zv6kAAAwDqsQ1tapkyZki996Uu56aab0qNHj+W6z9ixY9OvX7+WZciQISs0UQAAYN3UoWh56qmnMm3atPzDP/xDunXrlm7duuWhhx7K9773vXTr1i0LFixoc5/Ro0ensbGxZZkyZcoqmzwAALD269DuYR/5yEfyu9/9rtV1J554YrbZZpv8n//zf9K1a9c292loaEhDQ8PKzRIAAFhndSha+vTpk+23377Vdb17984GG2zQ5noAAIBVYaU+XBIAAKCzdfjsYUt68MEHV8E0AAAA2mdLCwAAUDTRAgAAFE20AAAARRMtAABA0UQLAABQNNECAAAUTbQAAABFEy0AAEDRRAsAAFA00QIAABRNtAAAAEUTLQAAQNFECwAAUDTRAgAAFE20AAAARRMtAABA0UQLAABQNNECAAAUTbQAAABFEy0AAEDRRAsAAFA00QIAABRNtAAAAEUTLQAAQNFECwAAUDTRAgAAFE20AAAARRMtAABA0UQLAABQNNECAAAUTbQAAABFEy0AAEDRRAsAAFA00QIAABRNtAAAAEUTLQAAQNFECwAAUDTRAgAAFE20AAAARRMtAABA0UQLAABQNNECAAAUTbQAAABFEy0AAEDRRAsAAFA00QIAABRNtAAAAEUTLQAAQNFECwAAUDTRAgAAFE20AAAARRMtAABA0UQLAABQNNECAAAUTbQAAABFEy0AAEDRRAsAAFA00QIAABRNtAAAAEUTLQAAQNFECwAAUDTRAgAAFE20AAAARRMtAABA0UQLAABQNNECAAAUTbQAAABFEy0AAEDRRAsAAFA00QIAABRNtAAAAEUTLQAAQNFECwAAUDTRAgAAFE20AAAARRMtAABA0UQLAABQNNECAAAUTbQAAABFEy0AAEDRRAsAAFA00QIAABRNtAAAAEUTLQAAQNFECwAAUDTRAgAAFE20AAAARRMtAABA0UQLAABQNNECAAAUTbQAAABFEy0AAEDRRAsAAFA00QIAABRNtAAAAEUTLQAAQNFECwAAUDTRAgAAFE20AAAARRMtAABA0UQLAABQNNECAAAUTbQAAABFEy0AAEDRRAsAAFA00QIAABRNtAAAAEUTLQAAQNFECwAAUDTRAgAAFE20AAAARRMtAABA0UQLAABQNNECAAAUTbQAAABFEy0AAEDRRAsAAFA00QIAABRNtAAAAEUTLQAAQNE6FC1XX311dtxxx/Tt2zd9+/bNXnvtlXvuuaez5gYAANCxaNlkk03yrW99K0899VSefPLJ7Lfffvn4xz+e5557rrPmBwAArOO6dWTwoYce2uryN7/5zVx99dWZMGFCtttuu1U6MQAAgKSD0bK4BQsW5LbbbsusWbOy1157rco5AQAAtOhwtPzud7/LXnvtlTlz5mS99dbLHXfckW233Xap4+fOnZu5c+e2XJ4+ffqKzRQAAFgndfjsYVtvvXWeeeaZPPbYYznttNNy/PHH5/e///1Sx48dOzb9+vVrWYYMGbJSEwYAANYtHY6W+vr6bLnlltlll10yduzY7LTTTrn88suXOn706NFpbGxsWaZMmbJSEwYAANYtK3xMS7OFCxe22v1rSQ0NDWloaFjZhwEAANZRHYqW0aNHZ+TIkdl0000zY8aMjB8/Pg8++GDuu+++zpofAACwjutQtEybNi3HHXdcXnnllfTr1y877rhj7rvvvnz0ox/trPkBAADruA5Fy3XXXddZ8wAAAGhXhw/EBwAAWJ1ECwAAULSVPnsY0EFVlcyfXetZdK55s9v/em3WvVdSV1frWQDAWkm0wOpUVcn1ByZTHqv1TFaf725Z6xmsHkP2TE66V7gAQCewexisTvNnr1vBsi6ZMmHt34IGADViSwvUyqiJSX2vWs+ClTVv9rqzNQkAakS0QK3U90rqe9d6FgAAxbN7GAAAUDTRAgAAFE20AAAARRMtAABA0UQLAABQNNECAAAUTbQAAABFEy0AAEDRRAsAAFA00QIAABRNtAAAAEUTLQAAQNFECwAAUDTRAgAAFE20AAAARRMtAABA0UQLAABQNNECAAAUTbQAAABFEy0AAEDRRAsAAFA00QIAABRNtAAAAEUTLQAAQNFECwAAUDTRAgAAFE20AAAARRMtAABA0UQLAABQNNECAAAUTbQAAABFEy0AAEDRRAsAAFA00QIAABRNtAAAAEUTLQAAQNFECwAAUDTRAgAAFE20AAAARRMtAABA0UQLAABQNNECAAAUTbQAAABFEy0AAEDRRAsAAFA00QIAABRNtAAAAEUTLQAAQNFECwAAUDTRAgAAFE20AAAARRMtAABA0UQLAABQNNECAAAUTbQAAABFEy0AAEDRRAsAAFA00QIAABRNtAAAAEUTLQAAQNFECwAAUDTRAgAAFE20AAAARRMtAABA0UQLAABQNNECAAAUTbQAAABFEy0AAEDRRAsAAFA00QIAABRNtAAAAEUTLQAAQNFECwAAUDTRAgAAFE20AAAARRMtAABA0UQLAABQNNECAAAUTbQAAABFEy0AAEDRRAsAAFA00QIAABRNtAAAAEUTLQAAQNFECwAAUDTRAgAAFE20AAAARRMtAABA0UQLAABQNNECAAAUTbQAAABFEy0AAEDRRAsAAFA00QIAABRNtAAAAEUTLQAAQNFECwAAUDTRAgAAFE20AAAARRMtAABA0UQLAABQNNECAAAUTbQAAABFEy0AAEDRRAsAAFA00QIAABRNtAAAAEUTLQAAQNFECwAAUDTRAgAAFE20AAAARRMtAABA0UQLAABQNNECAAAUrUPRMnbs2Oy2227p06dPBg0alMMPPzzPP/98Z80NAACgY9Hy0EMP5YwzzsiECRNy//33Z/78+TnggAMya9aszpofAACwjuvWkcH33ntvq8vjxo3LoEGD8tRTT+XDH/7wKp0YAABA0sFoWVJjY2OSZMCAAUsdM3fu3MydO7fl8vTp01fmIQEAgHXMCh+Iv3Dhwpx11lkZPnx4tt9++6WOGzt2bPr169eyDBkyZEUfEgAAWAetcLScccYZefbZZ3PzzTcvc9zo0aPT2NjYskyZMmVFHxIAAFgHrdDuYV/84hdz991359e//nU22WSTZY5taGhIQ0PDCk0OAACgQ9FSVVXOPPPM3HHHHXnwwQczbNiwzpoXAABAkg5GyxlnnJHx48fnP//zP9OnT5/87W9/S5L069cvPXv27JQJAgAA67YOHdNy9dVXp7GxMfvss0/e+973tiy33HJLZ80PAABYx3V49zCAmqiqZP7sWs+irXmz2/+6FN17JXV1tZ4FAKyUlfqcFoDVoqqS6w9MpjxW65ks23e3rPUM2hqyZ3LSvcIFgDXaCp/yGGC1mT+7/GAp1ZQJZW6hAoAOsKUFWLOMmpjU96r1LMo3b3aZW34AYAWIFmDNUt8rqe9d61kAAKuR3cMAAICiiRYAAKBoogUAACiaaAEAAIomWgAAgKKJFgAAoGiiBQAAKJpoAQAAiiZaAACAookWAACgaKIFAAAommgBAACKJloAAICiiRYAAKBoogUAACiaaAEAAIomWgAAgKKJFgAAoGiiBQAAKJpoAQAAiiZaAACAookWAACgaKIFAAAommgBAACKJloAAICiiRYAAKBoogUAACiaaAEAAIomWgAAgKKJFgAAoGiiBQAAKJpoAQAAiiZaAACAookWAACgaKIFAAAommgBAACKJloAAICiiRYAAKBo3Wo9gWJUVTJ/dq1n0da82e1/XZLuvZK6ulrPAgCAtZRoSZqC5foDkymP1Xomy/bdLWs9g/YN2TM56V7hAgBAp7B7WNK0haX0YCnZlAllbqUCAGCtYEvLkkZNTOp71XoWa4Z5s8vd+gMAwFpDtCypvldS37vWswAAAP7O7mEAAEDRRAsAAFA00QIAABRNtAAAAEUTLQAAQNFECwAAUDTRAgAAFE20AAAARRMtAABA0UQLAABQNNECAAAUTbQAAABFEy0AAEDRRAsAAFA00QIAABRNtAAAAEUTLQAAQNFECwAAUDTRAgAAFE20AAAARRMtAABA0UQLAABQNNECAAAUTbQAAABFEy0AAEDRRAsAAFA00QIAABRNtAAAAEUTLQAAQNFECwAAUDTRAgAAFE20AAAARRMtAABA0UQLAABQNNECAAAUTbQAAABFEy0AAEDRRAsAAFA00QIAABRNtAAAAEUTLQAAQNFECwAAUDTRAgAAFE20AAAARRMtAABA0UQLAABQNNECAAAUTbQAAABFEy0AAEDRRAsAAFA00QIAABRNtAAAAEUTLQAAQNFECwAAUDTRAgAAFE20AAAARRMtAABA0UQLAABQNNECAAAUTbQAAABFEy0AAEDRRAsAAFA00QIAABRNtAAAAEUTLQAAQNFECwAAUDTRAgAAFK1brScAANBKVSXzZ9d6FmufebPb/5pVp3uvpK6u1rNYK4kWAKAcVZVcf2Ay5bFaz2Tt9t0taz2DtdOQPZOT7hUuncDuYQBAOebPFiysuaZMsJWwk9jSAgCUadTEpL5XrWcB727ebFuvOploAQDKVN8rqe9d61kABbB7GAAAULQOR8uvf/3rHHrooRk8eHDq6uryH//xH50wLQAAgCYdjpZZs2Zlp512ypVXXtkZ8wEAAGilw8e0jBw5MiNHjlz5R541K+nate31XbsmPXq0Hrc0XbokPXuu2NjZs5tOq5gk82Yl86pF63inLunVq/2xS6pbYuzbbycLFy59Hr17r9jYOXOSBQtWzdhei51DfO7c5J13Vmzsks9bt55Nz3OSzJuXzJ+/9PX27MDYHj0WvVY6Mnb+/KbxS9PQkHTr1vGx77zT9FwsTX190r17+2OXfM7qFhu7YEHTv93SdO/etO6Ojl24sOm1tirGduvW9FwkTT8Ts5dxhpSOjH23n/vFn7c5c1rv47463iOWtOTPfYnvEUu+1upr8B6xpI783K+r7xHLGruuvEcs+dqdn479blCL3yOWtCa8R6zMWO8RTdq8Ryzxul3aWO8RTV83/9wv6+ducdVKSFLdcccdyxwzZ86cqrGxsWWZMmVKlaRqbJpq2+Wgg1qvoFev9sclVTViROuxG2649LG77tp67NChSx+77batx2677dLHDh3aeuyuuy597IYbth47YsTSx/bq1XrsQQctfeyS/4xHHrnssTNnLhp7/PHLHjtt2qKxp5++7LGTJi0aO2rUssc+++yisRdcsOyxjz++aOwllyx77K9+tWjs97+/7LF3371o7A03LHvsrbcuGnvrrcsee8MNi8beffeyx37/+4vG/upXyx57ySWLxj7++LLHXnDBorHPPrvssaNGLRo7adKyx55++qKx06Yte+zxxy8aO3PmssceeWTVyrLGfuzA1mO9RzTxHtHEe0STdfk9wu8RTYv3iEXL6niPuPYHyx7rPaJpaec9ojGpklSNjY3VsnT6gfhjx45Nv379WpYhQ4Z09kMCAABrkbqqqqoVvnNdXe64444cfvjhSx0zd+7czF1ss9b06dMzZMiQNP71r+nbt2/bO9Rq97Dv/P3c2udMTBrWs1l3ecYu+bz123AN36y7mnYPW/w5W2/9NX+z7qoYuzy7hzU/b+dOSvpusPSxi1uXd/1Y8rW2/qClj12SXT+a2D2s42NX1e5hi79263vbPWxxa9PvEUtak98jZr2VfHPTpq+bX7dLG+s9ounrv//cT58+Pf0GD05jY2P7bdB816WvddVoaGhIQ/PkFte7d+sfkKVZnjErMnbxN4juSerrFq1jyQ+y6rXE5WVZ/A1tVY5d/A14VY5taFj04uno2CWfty6Lbbirr1/0An43nTW2e/dFP8ircmy3boveeDo6dsnnbPHH7Np1+V/DHRnbpUvnjK2r65yxSduxiz9vS76+V8d7xKocu7reI5Z8rS1r7LKszHvEsniP6PjYdeU9os3/k9tZR2f93K9L7xGraqz3iEVj3+1128x7RJPmn/tlBfLiq16+tQIAANRGh7e0zJw5MxMnTmy5PGnSpDzzzDMZMGBANt1001U6OQAAgA5Hy5NPPpl999235fLZZ5+dJDn++OMzbty4VTYxAACAZAWiZZ999slKHLsPAADQIY5pAQAAitbpZw8DAIBVoqqS+cs4lXatzJvd/tcl6b7Y6afXQKIFAIDyVVVy/YHJlMdqPZNl++6WtZ5B+4bsmZx07xobLnYPAwCgfPNnlx8sJZsyocytVMvJlhYAANYsoya2/TBw2jdvdrlbfzpAtAAAsGap77XsT51nrWP3MAAAoGiiBQAAKJpoAQAAiiZaAACAookWAACgaKIFAAAommgBAACKJloAAICiiRYAAKBoogUAACiaaAEAAIomWgAAgKKJFgAAoGiiBQAAKJpoAQAAiiZaAACAookWAACgaKIFAAAommgBAACKJloAAICiiRYAAKBoogUAACiaaAEAAIomWgAAgKKJFgAAoGiiBQAAKJpoAQAAiiZaAACAookWAACgaKIFAAAommgBAACKJloAAICiiRYAAKBoogUAACiaaAEAAIomWgAAgKKJFgAAoGiiBQAAKJpoAQAAiiZaAACAookWAACgaKIFAAAommgBAACKJloAAICiiRYAAKBoogUAACiaaAEAAIomWgAAgKKJFgAAoGiiBQAAKJpoAQAAiiZaAACAookWAACgaKIFAAAommgBAACKJloAAICiiRYAAKBoogUAACiaaAEAAIomWgAAgKKJFgAAoGiiBQAAKJpoAQAAiiZaAACAookWAACgaKIFAAAommgBAACKJloAAICiiRYAAKBoogUAACiaaAEAAIomWgAAgKKJFgAAoGiiBQAAKJpoAQAAiiZaAACAookWAACgaKIFAAAommgBAACKJloAAICiiRYAAKBoogUAACiaaAEAAIomWgAAgKKJFgAAoGiiBQAAKJpoAQAAiiZaAACAookWAACgaKIFAAAommgBAACKJloAAICiiRYAAKBoogUAACiaaAEAAIomWgAAgKKJFgAAoGiiBQAAKJpoAQAAitat1hMA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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "threshold_height = 1\n", + "clusters = dendo.get_clusters_using_height(threshold_height)\n", + "colors = [\"red\", \"blue\", \"green\", \"black\", \"purple\", \"orange\", \"yellow\"]\n", + "dendo.plot_dendrogram(\n", + " plot_title=\"Dendrogram of Coreset using VQE\",\n", + " colors=colors,\n", + " clusters=clusters,\n", + " color_threshold=threshold_height,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can visualize the flat clusters using `dendo.plot_clusters()` method. The function takes the clusters and colors as arguments. The clusters are represented by different colors." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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Z87YP7+SfE8lPZezWCwWJk2axMqVKwkODubee+/l73//e4mX96pdu7bD64JAWxCkCz7P+vXrO/SrVKnSNadUFLjtttvo1q0by5Yt49dffwXypzxUqlSJxx9/3N6vIPRe6+/C6dOnCQoKKtR+77330qVLF4etbdu2JapRRKS0NOdXbnl+fn7UqlWL77///rrPYRhGke02m61QvyVLlrBt2zb+85//sGrVKv7v//6PKVOmsG3btqvOj83LywPgpZdeolu3bkX2uTKoeHt7F+rz+OOP07FjR5YtW8bq1auZPHkykyZNIiEhgQcffPCq77OsNWrUiG+//ZZDhw5htVrL9NwWiwUPD8d/n+fl5REUFMSHH35Y5DGBgYFAyb6nkp6rtEr6dwlg2LBhdO/eneXLl7Nq1SrGjBlDfHw869ato2XLlle9zpW/EShgmmbpi76Kvn37smLFClasWMGjjz7K0qVL7fOpCzRo0IBKlSrx3XffFXue3Nxc9u7dS+vWrcu0PhGR0tLIr1QIjzzyCPv372fr1q3XdXzBqNmpU6cc2q8cjSxwzz33MHHiRL755hs+/PBDdu3axUcffQQUH34KphtUrly50ChXwVbSKQQhISG88MILLF++nAMHDnDbbbcxceLEYvsHBgZStWpV9u7dW2jfnj178PDwuK7w2r17dwA++OCDa/atU6cOQJE17N27177/aurVq8f//vc/2rdvX+Tn16JFC4f+V/ueSnOu4r7TopT271K9evUYOXIkq1ev5vvvv+f8+fNMmTKlxNcrTsHnmZaW5tB+8eJF0tPTS3yeRx99FF9fXxYuXMjKlSs5efKkw5QHgKpVq3L//fezadOmYt/n4sWLyc3N5bHHHivdGxERKWMKv1IhvPzyy/j4+PDss89y7NixQvv379/PtGnTij2+Xr16AGzatMnedvbsWRYsWODQ7+TJk4VG1u68804A+6/UC1YluDL8BAUF0blzZ+bMmUNmZmahGgrW170am81GdnZ2ofPWqlXrqlMvPD09eeCBB/j0008dgs+xY8dYuHAhHTp0sE8fKY3evXvTrFkzJk6cWOQ/PE6fPm1fIaFVq1YEBQUxe/Zsh1pXrlxJamoqDz/88DWv9/jjj2Oz2Rg/fnyhfRcvXrR/5iX5nkp6LgAfH59C32dx6tSpg6enp8PfJch/UMblzp07Z59KUKBe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DRtGrVq19N3ITUPhtwK7/fbb8fT05NixYw7tx44do2bNmi6qSq40ePBgVqxYwaZNmwgLC3N1OQLs2LGDn376ibvuusveZrPZ2LRpE2+//Ta5ubl4enq6sEL3FhISQuPGjR3aIiMjWbp0qYsqkgJ/+tOfGDVqFL///e8BaNasGQcPHiQ+Pl7hV24amvNbgXl5eREVFcXatWvtbXl5eaxdu5a2bdu6sDIBME2TwYMHs2zZMtatW0d4eLirS5JL7r//fnbu3ElKSop9a9WqFX369CElJUXB18Xat29faFnAH374gTp16rioIilw7tw5PDwco4Wnpyd5eXkuqkikMI38VnAjRoygf//+tGrVitatWzN16lTOnj3L008/7erS3F5cXBwLFy7k008/xdfXl6ysLAD8/f3x9vZ2cXXuzdfXt9Dcax8fH2677TbNyb4JDB8+nHbt2vH666/z+OOPs337dubOncvcuXNdXZrb6969OxMnTqR27do0adKEb7/9ln/84x/83//9n6tLE7HTUmdu4O2332by5MlkZWVx55138tZbb9GmTRtXl+X2DMMosv29995jwIAB5VuMXFPnzp211NlNZMWKFYwePZp9+/YRHh7OiBEjGDhwoKvLcnunT59mzJgxLFu2jJ9++olatWrx5JNP8sorr+Dl5eXq8kQAhV8RERERcSOa8ysiIiIibkPhV0RERETchsKviIiIiLgNhV8RERERcRsKvyIiIiLiNhR+RURERMRtKPyKiIiIiNtQ+BURERERt6HwKyIiIiJuQ+FXRERERNyGwq+IiIiIuI3/D41GRMsQk+8TAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "dendo.plot_clusters(\n", + " clusters, colors, plot_title=\"Clusters of Coreset using VQE\", show_annotation=True\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The function below uses the `dendo.get_voronoi_tessalation()` method to convert the clusters into regions. `coreset_df`, `clusters` and `colors` need to be passed as the arguments to create the regions. This function creates a region for each coreset point separately and then colors them according to the clusters with colors passed as arguments. Another option is to create regions using the centroids of the clusters. You need to pass `tesslation_by_cluster=True` to the function to perform this task.\n", + "\n", + "Once the region creation is complete, you can use `plot_voironi()` method to plot the regions. The function takes the clusters and colors as arguments. \n", + "\n", + "Remembering that these regions were based on coresets, they can overlay the original data set and be used to cluster the data based on the coreset analysis." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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3r17m02nfPTRR/jnP/+JG2+8ER6PB4899hguvvhi7N69G9nZ2XF7TKLRKC644AJ89NFHuO666zBgwABs3rwZjzzyCLZt29Y4l//ll1/ivPPOw5AhQ/DHP/4RDocD3377LT7++OPG2/rLX/6COXPm4JJLLsHNN98Mv9+Pzz//HJ9++imuuOIKAPXTDZ988gkuv/xylJSUYOfOnViwYAHOPPNMfPXVV3C73a3W+sorr8Dr9eKGG25AdnY21qxZg8cffxx79uzBK6+8AgD45S9/iX379mHp0qVYvHhxu7//l19+idNPPx1paWn4zW9+A5vNhmeeeQZnnnkmPvjgA4wePbrJ9W+66SZkZmbizjvvxM6dO/Hoo49i9uzZeOmll9q8n2XLlmHatGno2bMn7rrrLvh8Pjz++OMYN24cNmzYgO7du+OXv/wliouLcd9992HOnDkYOXIk8vPzW73Nf/3rX3C5XLjkkkva/T0B4K677sLdd9+NyZMn44YbbsDWrVuxYMECrF27Fh9//HGTKYqjR49i2rRpuPzyy3HVVVchPz8/rs+Vjt7W4sWL8fOf/xyjRo3CddddBwDo1atXh37ftgSDQUydOhWBQAA33XQTCgoKsHfvXrz11luoqqpCenp6mz/fkefBvHnz8OCDD+L888/H1KlTsWnTJkydOhV+v7/d+s444wyUlJRgyZIlmDt3bpPvLVmyBG63GxdddBEA4JtvvsHWrVsxc+ZMpKWltXh7V199Ne6880787//+L37yk580+d6RI0eaXd9ut7d6WxQj2amItCccDovCwkIxZsyYJpc//fTTAoB45513hBBC3HLLLQKA+PDDDxuvU1tbK3r06CG6d+8uIpGIEKL1kRIA4vbbb29yH2+88YYAIO65554ml19yySVCURTx7bffNl4GQNjt9iaXbdq0SQAQjz/+eONl8RgpWbx4sVBVtcnvevxj8vHHHwshhHjkkUcEAHH48OFWb/vCCy8UgwYNavP+jx/VaLBq1SoBQPztb39rvKylT7Yt/ez8+fOFoihi165djZe1NX2DE0ZKLrroImG328V3333XeNm+ffuEx+MRZ5xxRuNlDY/15MmTm4xs3XrrrcJisTT5dN2SYcOGiby8PHH06NHGyzZt2iRUVRVXX311s9+7I9M3mZmZYujQoe1eTwghDh06JOx2u5gyZUrj81cIIZ544gkBQPz1r39tvGz8+PECgHj66aeb3EY8nysdvS0hOjd909GRks8++6xDj3NrIyXtPQ8OHDggrFaruOiii5rc3l133dXuaE+D2267TQAQW7dubbysurpaOJ1OMX369MbLGo4tjzzySJu3l5aWJk455ZTG/7c2qgig1Wluih1331AzFosFl19+OVatWtVkt8mSJUuQn5+PSZMmAajfvTFq1CicdtppjddJTU3Fddddh507d+Krr75q975uuOGGJv//97//DYvFgjlz5jS5/Fe/+hWEEPjPf/7T5PLJkyc3+UQ4ZMgQpKWlYfv27R3+fTvilVdewYABA9C/f38cOXKk8WvixIkAgBUrVgAAMjIyAABvvvkmotFoi7eVkZGBPXv2YO3ata3en8vlavx3KBTC0aNH0bt3b2RkZGDDhg1t1nr8z9bV1eHIkSMYO3YshBD47LPPOvT7Hi8SieDdd9/FRRddhJ49ezZeXlhYiCuuuAIfffQRampqmvzMdddd12RR8emnn45IJIJdu3a1ej/79+/Hxo0bMXPmTGRlZTVePmTIEJx11ln497//3enaAaCmpgYej6dD1122bBmCwSBuueUWqOoPh8df/OIXSEtLw//93/81ub7D4cA111zT5LJ4Plc6eluJ0jAS8s4778Dr9Xb659t7HixfvhzhcBg33nhjk5+76aabOnwfV111FYD641OD1157DX6/H1deeWXjZQ07Ctt7Lng8nma7D51OJ5YuXdrs6/777+9wndQxDCXUooYXc8MLfc+ePfjwww9x+eWXN+6g2bVrF/r169fsZwcMGND4/bZYrVaUlJQ0uWzXrl0oKipqduBo7TZLS0ub3W5mZiYqKyvbvO/O+uabb/Dll18iNze3yVffvn0BAIcOHQIAXHbZZRg3bhx+/vOfIz8/H5dffjlefvnlJm86v/3tb5GamopRo0ahT58+mDVrVpMhewDw+Xy444470K1bNzgcDuTk5CA3NxdVVVWorq5us9bdu3c3vrGnpqYiNzcX48ePB4B2f7Ylhw8fhtfrbfVvHY1GUV5e3uTyE/8umZmZANDm36Xhb9va/Rw5cgR1dXWdrj8tLa3NLe4dqcFut6Nnz57Nnn/FxcWw2+1NLovnc6Wjt5UoPXr0wNy5c/E///M/yMnJwdSpU/Hkk092+HnU3vOg4fHs3bt3k+tlZWU1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20VRUZGYPn262LZtW+PtPPPMM+KMM84Q2dnZwuFwiF69eonbbrutSUOnyspKcc0114icnByRmpoqpk6dKrZs2dLsd2tpS/BXX30lJk+eLFJTU0VOTo74xS9+0bhV+vi/QTgcFjfddJPIzc0ViqJ0qHna1KlTRWpqqnC73WLChAnik08+aXKdhsd67dq1TS7vTOfZZcuWiXHjxgmXyyXS0tLE+eef36R52vG315EtwQ327dsnbr311saGdG63WwwfPlzce++9zZppPfHEE6J///7CZrOJ/Px8ccMNN7TaPK0l8XqudPS2hBBiy5Yt4owzzhAul6tD22m/++47MXnyZOFwOER+fr743e9+J5YuXdrk77R9+3bxs5/9TPTq1auxid+ECRPEsmXLmtxWa6+5jjwPwuGw+MMf/iAKCgqEy+USEydOFF9//bXIzs4W119/fZu/w/EqKiqEw+EQAJo9X070r3/9S0yePFlkZGQ0bu8dNGhQi03V2toS3NnjCrWP574hIiJNqaqqQmZmJu65555WRz3i6ec//zmee+65Dp9LiRKH0zdERCSNz+drtovp0UcfBYBOn2QyVs888wwOHjyIG264AUVFRTGvX6Ku40gJERFJs3DhQixcuBDnnHMOUlNT8dFHH+GFF17AlClT8M4778guj5KMIyVERCTNkCFDYLVa8eCDD6KmpqZx8WtXtxuTPnGkhIiIiDSBfUqIiIhIExhKiIiISBNMtaYkGo1i37598Hg8nWrKQ0RERLERQqC2thZFRUXtN9WU2COlS+bPny8AiJtvvrnDP1NeXt5mExx+8Ytf/OIXv/iVmK/y8vJ236d1OVKydu1aPPPMMxgyZEinfq6hhXd5+f1IS3MmojSiH2x1A594ZVdBZHj/HGxDtSUkuwxqhb/Oj9vPvb3FkyCeSHeh5NixY7jyyivxl7/8pdNbxhqmbNLSnEhLa37KcaK46m4HPhOyqyAytIiqIJQu4NLf25npdGTZhO4Wus6aNQvnnntuh04hHggEUFNT0+SLKGky+cmNKNGqU61g9DcOXUXLF198ERs2bMDatWs7dP358+fj7rvvTnBVRK2wRoFcG3CY4YQoUSo8VgB8jRmFbkZKysvLcfPNN+Mf//gHnM6OrQeZN28eqqurG7/Ky8sTXCXRCYp1lfuJdKeSM/GGopsj5vr163Ho0CGccsopjZdFIhGsXLkSTzzxBAKBACwWS5OfcTgccDgcyS6V6Af5sgsgMrZKBydvjEQ3oWTSpEnYvHlzk8uuueYa9O/fH7/97W+bBRIiTciKyK6AyNAqbXyNGYluQonH48FJJ53U5LKUlBRkZ2c3u5xIM1KDgFMF/FHZlRAZTkRVUMOtwIaimzUlRLqkACiyya6CyJCqPDbuvDEY3YyUtOT999+XXQJR+4oswHbZRRAZT2Uqp+2NhiMlRImWwzlvokSodPEcZkbDUEKUaGyiRpQQlQ6u1TIahhKiRLNFgRxdz5QSaRJ33hgPQwlRMhRzsStRPHHnjTExlBAlA5uoEcUVd94YE0MJUTJkh2VXQGQo3HljTAwlRMmQGgLs3ClAFC/ceWNMDCVEyaAAKLbLroLIMLjzxpgYSoiSpYjDzUTxUmHnlKgRMZQQJUsuP9kRxUPYoqBWZSgxIoYSomTJ4PZFonioSrVy541BMZQQJYs9AmSziRpRV1Wl8nVkVAwlRMnEJmpEXVbh5s4bo2IoIUomNlEj6jLuvDEuhhKiZGITNaIuq7TxdWRUDCVEyeQJATYOPRPFijtvjI2hhCiZFHBdCVEX8Jw3xsZQQpRsRdw5QBQrnvPG2BhKiJKNTdSIYsZz3hgbQwlRsmWyiRpRrLjzxtgYSoiSzR4BsjiFQxQL7rwxNoYSIhm42JWo07jzxvgYSohkYBM1ok7jzhvjYyghkiEnIrsCIt3hzhvjYyghkiE1yCZqRJ3EnTfGx1BCJIMKoIjrSog6o8LOEUajYyghkoVN1Ig6pYqhxPAYSohkYRM1og4LWxTUcOeN4TGUEMnCJmpEHVbl4XSnGTCUEMniiACZnMIh6ogK7rwxBYYSIpmKGUqIOoI7b8yBoYRIpny+BIk6gue8MQceEYlkyuHCPaKO4DlvzIGhhEgmTxCwcliaqC085415MJQQyaQCKLbLroJI0yrTuPPGLBhKiGQr5K4CorZUpvA1YhYMJUSysYkaUZu488Y8GEqIZMtiEzWitnDnjXkwlBDJ5ogAGexXQtQa7rwxD4YSIi1gEzWiFnHnjbkwlBBpQQFfikQt4c4bc+GRkEgLsvlJkKgl3HljLgwlRFqQxiZqRC3hzhtzYSgh0gIVQBGbqBGdiDtvzIWhhEgrCvlyJDpRBXfemAqPgkRakSe7ACJtCVkVHOPOG1NhKCHSikw2USM6XqWHW+XNhqGESCucYSCNOw2IGlSlMJSYDUMJkZZ042JXogYVbu68MRuGEiItyedBmKhBpZ07b8yGoYRIS3Iisisg0gye88Z8GEqItCQtyFclEbjzxqx4+CPSElWwiRoRgEoPz3ljRgwlRFpTxB0HRJWp3IlmRgwlRFqTJ2RXQCQdz3ljTgwlRFqTxSZqRNx5Y04MJURa4wwDHg5dk7lx5405MZQQaVEJF/mReXHnjXkxlBBpUQHn08m8uPPGvBhKiLQoh/PpZF7ceWNeDCVEWpTOJmpkXtx5Y1487BFpkSqAQjZRI3OqcHCk0KwYSoi0qphN1MicKq3cFm9WDCVEWpXLJmpkPkGbijqVJ6Y0K4YSIq1iEzUyoapUjhCaGUMJkVa5wgB3IZDJVPA5b2oMJURaxiZqprTqu+9guf56nPv447JLSTruvDE3hhIiLSvgS9SMnvv4Y9w0YQJWfvMN9lVVyS4nqSq588bUeMQj0rIcLvgzm2N+P15atw43jB+PcwcPxsJPPpFdUlJVWtle3swYSoi0jE3UTOfl9evRv6AA/QoKcNXo0fjrJ59ACHPsxKrfecNQYmY83BFpmUUABWyiZibPffwxrho9GgBw9qBBqPb58MG2bZKrSg6e84YYSoi0roi7Ecxi64EDWLNjB6aPHAkAsFosuGzECDz38ceSK0uOyhS+JZkdN4QTaV2eOYbuqX6UJByNoui3v228TAgBh9WKJ6ZPR7rLJbG6xOPOG2IoIdK6LM6xm0E4EsHfVq/GQ5dcgikDBzb53kULFuCFNWtw/fjxkqpLDu68IYYSIq1zhwG3Cnh5wDaytzZvRqXXi2tPO63ZiMjFJ5+M5z7+2PihhDtvTE83E3gLFizAkCFDkJaWhrS0NIwZMwb/+c9/ZJdFlBzduNjV6J776CNM7t+/xSmai085Bet27cLne/ZIqCw5uPOGAB2NlJSUlOD+++9Hnz59IITAokWLcOGFF+Kzzz7DoEGDZJdHlFiFKrBVdhGUSP87e3ar3xvVowfEM88ksZrkq995E5BdBkmmm1By/vnnN/n/vffeiwULFmD16tUMJWR82WyiRsbGnTcE6CiUHC8SieCVV15BXV0dxowZ0+r1AoEAAoEfkndNTU0yyiOKv4wgoADgRhwyqApjbyyiDtJVNN28eTNSU1PhcDhw/fXX4/XXX8fAE1apH2/+/PlIT09v/OrWrVsSqyWKIzZRI4OrcjBxk85CSb9+/bBx40Z8+umnuOGGGzBjxgx89dVXrV5/3rx5qK6ubvwqLy9PYrVEccYmamRgFdaQ7BJIA3Q1fWO329G7d28AwPDhw7F27Vr8+c9/xjOtLABzOBxwOBzJLJEocfJkF0CUGEGbCq/KdVOks5GSE0Wj0SZrRogMLZvbJcmYKtJ4zhuqp5uRknnz5mHatGkoLS1FbW0tlixZgvfffx/vvPOO7NKIksMdYhM1MqRKN9vLUz3dhJJDhw7h6quvxv79+5Geno4hQ4bgnXfewVlnnSW7NKLkKbED2/yyqyCKq0q3rgftKY50E0qee+452SUQyVeoAuY4iz2ZSKWdo39Uj/GUSE9yuBiQjKeSO2/oewwlRHqSHqpvokZkEAE7d97QDxhKiPTEGgXyuFOBjKP+nDdE9RhKiPSmWDdLwYjaxXPe0PH4bCDSm3zZBRDFT6WL85H0A4YSIr3JYhM1Mg7uvKHjMZQQ6U1KCHDxpUvGwJ03dDwe2Yj0qIRnDCb9484bOhFDCZEeFfKlS/rHnTd0Ih7ZiPQoh/PwpH/ceUMn4jOCSI8ygmyiRrrHnTd0IoYSIj2yRoFcDn2TvlXYuZ6EmmIoIdIrNlEjnau0cXs7NcVQQqRXbKJGOuazWeBTOFJCTTGUEOlVFvs7kH6tsk6FEuXWdmqKoYRIr1LDgJMvYdKfr1zD8a2vBI5gkexSSGN4RCPSMzZRI505Yi/CJ76TAQBqHUMJNcVQQqRnbKJGOhJUnVganYjo9289oYpiyRWR1vCIRqRnuWyiRvrxvn0KasPuxv+HajKhRl0SKyKtYSgh0rOMoOwKiDrkc9do7PQXNLvcEeBoCf2AoYRIz9hEjXTgoKMUa3yDW/yeqOG6EvoBQwmR3rGJGmmYX3VjWeTMxnUkJwpXcqSEfsBQQqR3bKJGGiUE8J5tKurCzlavE67zwBrxJLEq0jKGEiK9y2JXTNKmz9zjsCeQ2+71bH6OllA9hhIivUsNsokaac4+Rw+s9w3s0HW5roQa8EhGpHcKgGIudiXt8Fo8WB4eDwGlQ9cPHeVICdVjKCEygkKL7AqIAABRoWC5dSp8kY53G44EXLCFMxNYFekFQwmREbCJGmnEOvcZ2B/I6vTPcV0JAQwlRMbAJmqkAbudfbHR1y+mn41UcV0JMZQQGYONTdRIrmPWDKwIjov550MVRRCiY2tQyLgYSoiMoohN1EiOKFQsU6cgEI09GEdDdjjCOXGsivSIoYTIKNhEjSRZ7ZqAQ8GMLt+O1cd1JWbHUEJkFNlsokbJt905EF/4esXltriuhBhKiIwiNQjYOSdPyVNjzcIHgVPjdnuBowVQBLe3mxlDCZFRKACKO94bgqgrwrBiqToVIRHHtUxRK2yhvPjdHukOQwmRkRTxUyYlxyeuiTgajP+J9KxerisxM4YSIiNhEzVKgm9cQ7DF1z0htx2u5LoSM2MoITKSjJDsCsjgKm15+NA/MmG3H6zMgyrYc8esGEqIjMQeAXJ4QKfECMGGpWIywolcjCpU2IIFibt90jSGEiKj4boSSpAPXVNQFU5N+P1Y6riuxKwYSoiMJp/bgin+vnadgm+T1NwsXMFQYlYMJURGk8MmahRfR+xF+Nh3StLuL1idDYtwJu3+SDsYSoiMJjUI2DhaQvERVJ1YFp2IaJLfLuwB7sIxI4YSIqNRAJSwiRrFx/v2KagJu5N/x7UMJWZkzlAS5O4EMrhCLnalrtvsHo2dfjk7YbiuxJzMGUqqeYp3Mjg2UaMuOmjvhk+9g6Xdf+hYOizRFGn3T3KYM5TU+IAwP0mSgWWyiRrFzq+6sSwyIenrSE5k93O0xGzMGUoAoNIhuwKixLFHgCyOCFLnCQGssE9FXUQDu1+4rsR0zBtKanxAxLy/PplAMddOUedtTBmHcn+u7DIAAMGjHCkxG/O+KwsBVGngkwBRouTLLoD0Zp+jB9Z5B8ouo1HElwJrJF12GZRE5g0lAFDtB6Ls50AGxSZq1AleiwfLQ2dAQFvHRBvXlZiKuUNJNApUu2RXQZQYbKJGHRQVCpZbpsAX1d5aO1HNdSVmYu5QAgBVAUDILoIoAVQAxWyiRu1bn3IG9gezZZfRosCRIggeo02DoSQSAWq4toQMqpAvcWpbubMPPvP2k11Gq0TICXtEm4GJ4o9HLACoDHO0hIwpl09sat0xawbeC46TXUa7bEk6OzHJx1ACAOEwcIxrS8iAsthEjVoWhYpl6hQEotqf4otUcV2JWTCUNKjkTgUyIHsEyGQTNWruU/cEHApmyC6jQwJHC8G3K3PgX7lBMAjUcW0JGVAJm6hRUzucA7DZ20t2GR0XscEe0kZDN0oshpLjVXL+nQwoj9uC6Qc11ix8EBwju4xOsx3Nkl0CJQFDyfH8AcCnvX36RF2SE5ZdAWlERLFiqTIFwaj+pvQiR4NwHuNottExlJyokp8qyWA8QUB/70GUAJ84J+JoKE12GTGKILg9CJXnLDM0/nVP5PUDAe2vRifqMBVAMUcAze4b52B87esuu4wuiCIaisKxj89lI2MoaUmlRXYFRPFVyOe0mVVac/FhYJTsMrpEiPodkr5DPjiOMZgYFUNJS475gBDHu8lAcqOyKyBJwooNS3EWwkLfwbQhlABAeEcYSoRT7UbEUNKaSm6jJANhEzXT+tB5FqrCqbLLiIMfgnUkGIFzv3kWvS68ayGe+tVTzS7fum4rfjnil/DWeiVUlRgMJa2p8QFhfX+yIGrkiAAZHP0zm6+dJ+MbX4nsMuLi+JESAPAd9MHJ3lKGw1DSlirOW5KBsImaqRyxFeKTwHDZZcTNiaEEqJ/GUaN8GzMS/jXbUu0HuP2MjCJfdgGULEHViWViEiLCSMevFkJJIAz7Pu6WNBKO57ZFRIFqN5BlnPk6MrFsnt/JLD6wn4Uav1t2GXHV0kgJAPgP+uHIdCCQEkhyRcm1+aPNmHP6nCaXRaPGW8DOUNKeKj+QoQAqW9CTzqUFAasChPlcNrLNrlHY4SuUXUYCtP4GHN4RhjpQRVQ13pt0g37D++GKeVc0uWzHFzvw1z/8VVJFicFQ0p5oFKhxAxkcLSGdUwEU2YHdxv5EaWaH7CX41DdEdhkJEY22PtIXCUTg2u+Cr9iXxIqSy+6yI69bXpPLKg9WSqomcYw04Zg4lUGAHy7JCIq4o8yo/IoLy6ITETXoYb216ZsGvgM+OL3cjaN3xnz2xlskDNS4ZFdB1HW5TNdGJASwwj4Vx8LGfFMWQqCt6ZsGbKqmfwwlHVUV5mgJ6V8mm6gZ0Ub3WJQH8tq/ok4pSsfWioT9YTgPGjOYmQXXlHRUKATUuYBU485Zkgk4w0CaBajhThyj2GfvjnW+QbLLSChFiUB08EOhb78PjgwHAm7jrJ2aedfMFi/vN6Ifnln3THKLSTCOlHRGBQ/kZADd2NfBKLxqKpaHx0PA2FMWitK5Y294O6dx9Eo3oWT+/PkYOXIkPB4P8vLycNFFF2Hr1q3JLSIYBNjWmPQunwdrIxBQ8J5tKnxR43ee7mwoiQQinMbRKd2Ekg8++ACzZs3C6tWrsXTpUoRCIUyZMgV1dXXJLaTSuPvgySRyOOJnBOtcp2NfIFt2GUnR0TUlx/Pt98HhNX5gMxrdrCl5++23m/x/4cKFyMvLw/r163HGGWckrxB/EPA5AJdx5ivJZNKC9R9HmK91q9zZB5/5+ssuI4liC9KRHREoAxQINr/UDd2EkhNVV1cDALKyslq9TiAQQCDwQ3ioqamJz51XKgB3CJNeqaK+idqeoOxKKAbHLOlYERonu4yk6uz0TYOwPwz3ITe8BWx+qRe6mb45XjQaxS233IJx48bhpJNOavV68+fPR3p6euNXt27d4lOA1w8EeMZV0rFi3X4eMbUoVCyzTIU/Yq7FyrGGEgDw7vVyGkdHdBlKZs2ahS+++AIvvvhim9ebN28eqqurG7/Ky8vjV0QlD+qkY7mcu9GjT11n4lAwQ3YZEnRtHVRkZwRKlAu89UB376yzZ8/GW2+9hZUrV6KkpKTN6zocDjgcCUrIx3xAyArYwom5faJEyuLzVm92Ovtjs6+37DKkULt4or2wLwzXQRd8hewzpXW6GSkRQmD27Nl4/fXX8d5776FHjx6ySwIqOYVDOuUMAx6eB0cvaqxZeD84VnYZ0rR33puO8O71wuHjNI7W6SaUzJo1C3//+9+xZMkSeDweHDhwAAcOHIDPJzH51vqBMA/spFMlDNV6EFGsWKZOQTCqu4HtuOnKmpIfbkNBdEeU0zgap5tQsmDBAlRXV+PMM89EYWFh49dLL70kryghgComb9KpQt28/E3tE+dEHAmmyS5Dqlj6lLQk5AvBdZhbJ7VMN9FbdPTEB8lW7QcyVcDChYOkM9lsoqZ137oG42tfd9llSBeP6ZsGdeV1cKY5EWCvqaTxhD0dvq5uQolmiShQ7QayuA+edCadTdS0rMqWi5X+kbLL0IR4TN/8cFsKojujQF8AnH1PKJuwofRwKazbOh41OH4bD1UBgPOUpDeqAArN1e9CL8KKDUtxFsKCnxvrxXdUL+QNwX3EHdfbpKYK6wox6ItByCjP6NTP8RkfD9EIUOMCMrjdjHSm2ArsZWdXrfnQMRmV/lTZZWhI/KcavXu8sKfbEXTy+R9PaeE0lO4qhSPG9ZYMJfFSGQLSAYOfQZyMJleja7VMbIvrZHzji1P3aYOI10LXE4kdAko/nhsnHqzCirLDZUgvT4fShTdChpJ4iYSBWheQxtES0pGskOwK6DhH7YX42D9cdhmaE8+FrsdrmMbx5nFNYFcU1BWgYHsBLMGuL9JhKImnyjDgAUdLSD9c3zdRq+VOHNmCqhNLo5MQEVzq11zinp/eci/saZzGiYUn7EHp7lI4K51xu00+++MpFALquAeedKaYTdS04AP7WagJc/FlyxIbmsVOwV1onWAVVvQ83BN9NvWJayABOFISf5URgOvTSE8KFGCL7CLM7Qv3KOzwFsouQ8MSG0pCdZzG6ah8bz4KthfAGkhMfGAoibdAEPA6AbdfdiVEHZPDj4gyHbKXYLV3iOwyNC7xz9G68jo40h0IOjiN05LUSCpKy0vhOprY2QCGkkSoEABHYUkv2ERNGr/iwrLoREQ5k96mRC10PZ4CBdiJ+qZqXBfYyCqs6Ha0GzJ3ZXZpV02H7y/h92BG/gDgcwBsY0x6YBFAgR3Yx0+IySQE8L5zKo754zsnb0zJWYgdPBasn8bJ5TQOAOT58lC4vRBWf/KiAkNJolQqANe8kl4UWYB9soswl00pY7Hbmye7DF1IxkhJg7rddbB77Ag5zbtdPiWSgtI9pVK63jKUJIrXDwRsgMO8T2zSkXzZBZjLfkd3rPUOkl2GjiQvlChQoOxSTDmNYxEWlFaWInNnJhQh55dnKEmkSitQwFBCOpDJ52my+CypWB4eD2G2d7wuSe6Cp+CxIFxHXPDlmqcZZq4/F4XbC2HzyW0RwFCSSMd8QMgK2MKyKyFqmzsMuFXAy9WuiSSgYLl1KryB2M4LYlbJnL5p4Cv3wZZmQ8jgo92uiAvd93WH+5A2dmdwyXeiVbIxFelEN54xONHWu0/HvkC27DJ0R0YogUD9bhyDnhZHFSrKKsowYNMAzQQSgCMliVfrB7IsgJVtvEnjClVgq+wijGuPszc21PUz3TqFeJASSgCEjoUMOY2T489B8Y5iWL3aiwDaq8hohACqHEAOt5iRxmUzOCfKMUs63gudBihMJLGQFUoAY03juKIulO0rQ8rBFNmltIqhJBmq/UCmClg4X08alhGs/xRv0OFqWaJQsdwyBf4gp8diJ/HYKVC/G6cPdDvKpQgF3aq7IXtHNtSotldtMJQkg4gC1S4gy1hDgGQwFgEU2ID9+v9EqCVr3GfioDdTdhm6JnOkBACCtUG4jrrgy9HfMTw7kI3iHcWw1eljfSNDSbJUBYEMBVD5MZQ0rMjKUBJHO5398bm3t+wydE92KAEA3y4fbB79TOM4o06U7S9D6gF9nSGWoSRZohGgxg1kcG0JaVgeQ3O81Fgy8X5wrOwyDEEIbUx9K7sUiN4CiqrdeRxFKOhW8/1UTUTbUzUtYShJpqogkA7dzkuSCXCxa1xEFCuWWaYiGOQhtquEEFAUbYSSYG0Q7gq3ZqdxsoJZKN5RDPsx/a5f4ismmcJhoNYFpGnzCU0Ed4hN1OJglXMCjvjSZJdhCIqiraDs2+WD1WNF2KGdppjOqBOlB0rh2e+RXUqXMZQkW2UY8ICjJaRdJXZgm192Fbr1rWswvvL1kF2GYahqFFGNZWR1twr0hvTjuAIFJTUlyNmeo8upmpYwlCRbKATUOYFUHvRJowpVYJvsIvSpypaLlf6RssswGG2NlABAsCYId6Ub3ix5awQzghnotrMb7LX6nappCUOJDJVRQF8LoslMcrT3JqAHYcWGpZiMsOBhNZ60Nn3TwLfTB1uqDSF7cnfj2KN2lB0sQ9o+Y04P8tUjQyAIeJ2Am6MlpEHpITZRi8FHzsmo9Ol/Tl9rtBpKhBBQdyVvGkeBguLaYuRsz4ElbEn8HUrCUCJLpQC0cw4koh9Yo0CeDTioj34MWrDFOQzbfN1kl2FIWg0lABCoCcBV4YIvO7GbFzJCGSjZWQJHjfHPLs1QIosvAPjtgDMouxKi5oqtDCUddNRWgI/9w2WXYVha2Q7cmsbdOPb478axCRvKDpUhfU963G9bqxhKZKpUgULZRRC1IF92AfoQVBxYhsmIwLjD6fJpd6QEACAAy24Lwr3CcZ3GKaotQv6OfKghY+yq6SiGEpnq/EDQBiR5oRRRu7K004NBy1Y6p6Dax3nYxNJ4KAEQqA7AXeWGN7Pru3HSQmko3V0KR5Xxp2pawlAiW6UVyGcoIY1JCQEuFfBpe+hcpi9cI7Hdx6HORNPympLj+Xb6YE21ImyLLdDbhA2lh0uRUZ4R38J0xlzjQlpU6wNCzIakQSX6OKuoDIdsxVjtGyq7DJPQRygRUQHLbguE6Py2tcK6Qgz6YpDpAwnAkRJtqLQBeRwuJ40ptADfyC5CewKqC8uiExHlZ7qkUFX9jNYFquqncXyZHduN4wl7ULq7FM5KZ4Ir0w+GEi2o9QNZFsCqj08EZBJsotaMEMAK2xQcC7hkl2IaQujreejf6YclxYKIvfW6rcKKsiNlSN+dDkV2r3qNYSjRAiGAKgeQI69lMVEzGWyidqJN7jHY7ePWpOTSVygRUQFruRXhnmEoSvPAUVBXgILtBbAEuWOrJQwlWlHjBzJVwKKfoUoyODZRa2K/vQxrvYOkn4TNbPSy0PV4LU3jcKqmYxhKtCIaBardgMQTPBE1U8QmagDgs6RieeRMCIXrSJJPf6EE+H4aJ9UCxaqg9GgpMnZlcKqmAxhKtKQqAGQogMrxctIIzlRAQMF7tinw+s3ZN0I2rXd0bY2ICnjKPSjzlsEa4FttRzH2a0k0AtRwaI80JJu7wta7TsNef47sMkxLbwtdj2fxWRhIOomhRGuqQlxYSNqREgKc5j1M7HH2xgZvf9llmJx+Q4lVYSDpLPMebbQqHAZqud2QNKTELrsCKeosaXgvdBrQwg4KSh69Tt8AgIXnROo0hhItquSQOWlIofkOE1GoWG6dCn/EnIFMS3Q9faMwlHSW+Y42ehAKAce4toQ0Ile/n1RjtcY1HgcCmbLLIAB6nr7hSEnnMZRoVaX53ghIozKCsitIqp2Ofvjc10d2GfQ9PY+UqIJvsZ3FR0yrAkHAyy2IpAHWKJBrjgV7tdZMvB8cK7sMakLHoYRvsZ3GR0zLKrnAjjSi2PhnDI7AgqXKFASF8X9XfdHvqLEa5VtsZ/ER0zKfH/BzoR1pgAmaqK1yTcSRULrsMugEep6+4ZqSzjPHmKyeVapAoewi5Jg5cyEWLVrV7PKpUwfi7bdvllCRiWUZe0fYd66T8JWvh+wyqEX6DSUcKek8hhKtq/MDQRtgN+f5R84+exCef35Gk8scDj5tky71+yZqfv0OpbemypqDlYFRssugVuh5pEQRnILvLB7d9aDSCuSbM5Q4HFYUFHBIXToF9U3UvvXLriSuwooNy9SzEArzUKhVeg4lFsHpm87iK1EPan1AlhWwGXsInTSuQAW+lV1EfH3kmIQKv0d2GaaxcOFMrFq1qPH/KSlZKCsbiYsvfhAlJUNa/Bk9d3Tl9E3n8RHTiypz7gh4663NSE2d0+Trvvv+LbssczJYE7WtzqHY5i+VXYbpDBp0Nh58cD8efHA/br11OSwWK5544rxWrx+N6nekhH1KOo8jJXpR4wcyLYBVvy/QWEyY0A8LFlzR5LKsrBRJ1ZicgZqoVdjy8ZF/hOwyTMlqdSA9vQAAkJ5egLPPvh1/+tPpqK09DI8nt9n19Tp9Y1Wtet7NLA1DiV4IAVTZgRyf7EqSKiXFjt6982SXQQBgiwK5NuCwvtc3BRUHluIsRLhdUzq//xg+/fTvyMvrjZSU7Bavo9dQwvPexIahRE9qAkCmClgYv0mSIqvuQ8lKx1mo9rtll2Famze/hTlzUgEAgUAd0tMLMXv2W1DVlqc69BpKrKpVz7uZpWEo0ZNoFKhxA5le2ZUkTSAQxoED1U0us1otyMlJlVSRyem8idqXrhHY7iuSXYap9es3AVdcsQAA4PVW4v33n8Jjj03DvHlrkJ1d1sJPyP8QtnDhQni9Xtx4440d/hmOlMSGoURvqgJAugKoQnYlSfH221+isPA3TS7r1y8fW7b8UVJFJpet349+h+3FWOUbJrsM07PbU5CX17vx/1df/T+45ZZ0fPjhX3DRRfc0u340GoGiw3YfVpVvr7Hgo6Y3kQhQ6wLSjb+2ZOHCmVi4cKbsMuh4qUHArgBBfYXigOLE0shERLnhUIMUKIqKUKj5MU2IKBRFX8+1BjwZX2w6/ajNmDEDK1euTEQt1FGVIUCfr1PSu4YmajoiBPC+YyqORVyySyEA4XAA1dUHUF19APv3f40XX7wJgcAxDBlyfrPrqqr8qZtY8bw3sen0SEl1dTUmT56MsrIyXHPNNZgxYwaKi4sTURu1JhyuHy1JM/5oCWlQoQXYLruIjvvcfSp2+XS+GMZAvvzybfzmN/Un9HI6PSgo6I/rrnsF/fqd2cK19TtdyOmb2HT6UXvjjTdw+PBhLF68GIsWLcKdd96JyZMn49prr8WFF14Im82cTb6SrioMpMkugkxJR03UDjjKsMZ7Uv0ID0k3c+ZCzJy5sMPXV5QIhE5Hhdk4LTYxPWq5ubmYO3cuNm3ahE8//RS9e/fGT3/6UxQVFeHWW2/FN998E+866UTBEHDMKbsKMqMMfWwJ9qkpWBYeD6HwzUGvdD19w903MenSq3X//v1YunQpli5dCovFgnPOOQebN2/GwIED8cgjj8SrRmpNpX5fsKRj9giQo+0RUQEF79mmwhthcNc3/U7fcKQkNp1+1EKhEF577TWcd955KCsrwyuvvIJbbrkF+/btw6JFi7Bs2TK8/PLL+OMfuWUz4QJBwOuQXQWZUbG258s3uMZhbyBHdhnURYqi31DCha6x6fSRpbCwENFoFNOnT8eaNWswbNiwZteZMGECMjIy4lAetatSAdickpJNw53/9zh6Yb13ANeRGIJ+Q4mWRkqOVB/BX//9V3z8xcc4VHUIWZ4s9C3pi+mTpmPUgFGyy2ui06HkkUcewaWXXgqns/Vh0YyMDOzYsaNLhVEH+fyA3w44jXOyNNKBHG2+WdSpHrwXOh267LZFzWhlpGTmzJmd/hklqo3n4L4j+3Dtn66Fx+XBnIvnoHdxb4QjYaz6chUeeOEBvPbH12SX2ESnQ8lPf/rTRNRBXVGpAoWyiyBT0WATtShULLedDX9AX31UqHWKot91c1aN9Ca9/4X7oUDBonmL4HL80KunV1EvXDjuQomVtUw740sUuzo/ENT2wkMyGAVAsbbe/Ne6x+NAIFN2GRRX2hgpiYUWRkqq66qx6stVuPTMS5sEkgYet0dCVW1jKDGKSm2kcjKRQu0s5Nvl7IdN3j6yy6C4028o0cKakvJD5RBCoHtBd9mldJj8R43io9YHhBhMKIk00kSt1pqJFYGxssugBNDKmpJYqFG+vcaCj5qRVGlrOJ0MLlN+E7UILFimTkFQcPrSmHQaSoQ2Rkq65XWDoijYeWCn7FI6TP6jRvFT4wMi/JNSktgjQJbc0bnVrgk4HEyXWgMljl4XulpUbUxtpqekY8zAMXjl/VfgCzQ/V1qtt1ZCVW3jO5iRCAFUsYMlJZHEJmrfOQfhS19PafdPyaDPkRKbRTsjd7+Z/htEohHMmD8Dyzcsx+6Du7Fj/w68+N6LuOaBa2SX14yuQsnKlStx/vnno6ioCIqi4I033pBdkvZU+wENrPomk8iX81yrsuZgZXC0lPumZNJnKNHSeW9Kckvwj//6B4b3G45HX30Ul/3xMsx6dBbWbFmD26+4XXZ5zehqZWRdXR2GDh2Kn/3sZ/jxj38suxxtikaBajeQ6ZVdCZmBhCZqYcWGZepZCIV1dfiimOg0lGhk+qZBTnoOfjv9t/jt9N/KLqVdunpVT5s2DdOmTZNdhvZVBYB0BVC109iKDCo1CNgUIJS859rHzkmo8GmvvwIlgj5DiVXR1Vurphj6kQsEAggEAo3/r6mpkVhNEkUiQK0LSG++sIkorlTUN1HbGWj3qvGw1TkUW32lSbkvkk+3C101NH2jN7paU9JZ8+fPR3p6euNXt27dZJeUPJUhgAMllAxFyTkAV9jy8XFgRFLui7RBCH2OlKjGfmtNKEM/cvPmzUN1dXXjV3l5ueySkiccBo41bytMFHc5if80G1LsWComIyz4CdRc9BlKLODzNFaGnr5xOBxwOByyy5CnMgJw6p0SLSvxTdRWOqeg2peS8PshrdHn9A3XlMTO0CMlphcMAnXsW0IJZo8AmYk7CH/lGo7vfEUJu33SLt1O32igm6te6SrOHTt2DN9++23j/3fs2IGNGzciKysLpaVc/NaiSgHwAyYlWokNqAzH/WYP24rwie/kuN8u6YU+Qwmnb2Knq1Cybt06TJgwofH/c+fOBQDMmDEDCxculFSVxvkDgM8BuJKzO4JMKi/+TdQCihPLxCREOaBrWrodKeFzNma6CiVnnnkmhOCWkk6rUIBi2UWQoeXEf5TkffsU1Aa4WNvc9BlKLFyQHTPGOTPw+QE/15ZQAnmCcf2I87n7VOwKFMTvBkmn9LnQVRE81UesGErM4gAAr4l3IlFiqQCK4/P8OuAow6d1J8XltkjfdDt9E+Vba6z4yJlFJArsDwLHOGJCCVLY9cOJX3VjWXg8hMJDE+k3lHD6JnZ85ZtGFBACOOAHqjlPTwmQ27X1XgIK3rOfDW+EwZka6DOUKDxTe8wYSszi+AXCh33AUbe8WsiYuthE7TP3OOzx58SpGDICvY6UsE9J7PjImYU4YcFYpRc45OL5cSh+HBEgI7bVrnsdPbGubkCcCyL9099CVwUKQ0kX8JEzAwFAaSF91PiAA06AQ40ULyW2Tv+I1+LB8tAZgMLnITUVjepvpMSicj1JVzCUmEFbiwbr/MA+OxDhU4HiIL9zwSIqFCyzTIU/ak9QQaRnepy+sSgMJV3BdyIzaG8ngz8A7LUCYb6YqIuyO9dEbV3KeBwIZiWoGNI7PYYSm6Xzo4X0A4YSU+jAp9dgENijAEG+oKgL0oKAtWOjJbudfbHR2zfBBZGe6TGUsMV81/DRM4OO9nwIh4E9UcDPoXSKkQqgqP3nT60lAyuC4xJfD+mWEFFdLjOyqro6e4vmMJSYQide2dEIsDcMeNkrgmJU1PY0YAQWLLNMQSDKUTlqnaLob5QE4JqSrmIooeZEFNjnB2rZZI1i0E4TtU/dZ+JwMCM5tZBu6TaUgKGkKxhKzCDWMdCDPqCKTdaokzJbb6K23TUIX3h7JbEY0itV1V+PEoChpKsYSqhtR7zAEQYT6gRnGEhrfmCutuXgA/9oCQWRPulzpIQLXbuGj54ZdPU02lVe4CC7v1IndGu62DUMK5biLIQEFwFSx+h1+saq8DneFQwlZhCPFey1PmA/u79SB+U3/e8nrkmoCHnk1EK6pNdQokb5ttoVfPTMQMRpiMPrB/ba2P2V2pfzw3qAba6h2OIrk1gM6ZGi6HNNidrRFgzUIj561DmBILDHAoQ4REltSAsCKlBhy8NH/hGyqyFd0udIiUVwoWtXMJSYQpwXg4RCwB6w+yu1ThUIFadiGc5CmAdpioFup294huAu4aNHsYmEgT0RwOeQXQlp1Ja8QagKpcgug3SLocSM+OiZQoLmZqNRYF8QqGP3V2ouCI6kUez0uqZEiXAzQFcwlJhCAvfyCgHs9wM17P5KRPGkz5ESrinpGoYSM4jX7pu2HPIBFQwmRBQv+gwlCtsmdAlDiRmIJA2DVviAw+z+SvVEV5v2kcnpM5Swo2vX8NEzhSTOzVZ7gQPs/krJGaAj49Lj7hubheuouoqhxPAkfFo95gP2sfsr8e9PsdPjQleLwvUkXcVQYniS3hh833d/DfNFSkSdJ4T+RkoYSrqOocTwJP6JA0Fgj8LurybF6RvqGv2FEqvKY11XMZQYnezzMITDwB4BBOztX5cMhtM31BX6CyUWcKSkqxhKjE7RwBtDJALsDQNedn8loo7SYShRGUq6iqHE8DQQSoD67q/7g8Ax9jIxC07fUNfocKErR0q6jKHE6LQwUtJACOCAD6hmMDEHDT33SHf0uNDVqnBNSVcxlFDyHfYBR9lkjYjaor9QwpPxdR0fQaPTalfNSi9wiE3WjIzTN9Q1OgwlfEvtMj6CRqfRTAIAqPEBB9hkzbj4d6XY6XH6hmtKuo6hhOSq8wP77ECET0Wj4UgJdYUeO7py+qbr+Agang7eGfwBYK+V3V8NhyMlFLtolCMlZsRQQtoQ/L77a5AntCIiQJdrSjhS0mV8BA1PByMlDcJhYE8U8LP7qxFw+oa6RofTN1G+pXYVH0Gj09s7QzQC7A0BXqfsSqjLOH1DsdPjQleOlHQdH0Gj01soAepr3ucHatlkTc/0+NQj7dBjKFEiDOJdxVBieDp+ZzjoA6rYZE2/eICm2OkxlFgEF7p2FUOJ4elvXraJI17gCIMJkdnoLZQoigKFQbzLGEqMzghj6FVe4CC7v+qNEZ56JJO+PlBZFI6SxANDieHp64XdqlofcMDNLcM6IrR6igPSBb31KbGpPDbFA09paGRCARSjfFxVAG8YqAsBTgeQpgCpAUA1yu9HRMcTIqKpk5y3hyMl8cFQYmSKCj02IGqR6gCi/vp/+wOAH8ARFUh1AmkRwBmUWh4RxVNUV4EEYCiJF4YSI1NUQGeLxVpkcQMRb/PLo9H6k/rVAHDYAY8F8AQAi0GmrHSO0zcUO/0dt6wq307jgY+ikSmK/heHWlwtB5ITBYJAAMBRBUh1AWlRwBVIeHlEFH+KEtHdQmmVSzTjgqHE0HT+SVW1AxF/535GiPpFsbUAbDYgzVY/emLV3ycvvdPbmwpphx5DiVXh22k88FE0ND2HEgsgoujSUE8oBBwNAUcBpDiBNAG4A/p+WHSFDzTFRlH09yGCIyXxwVBiaDp+U1CtQDSO0y91fqAOgMUCpDmAtCBgC8fv9qkZvX3SJe1Q1SgiOsslHCmJDz6KRqbXTKK6gKgvMbcdiQCVXqASgMsBpKlAip9bixNCr09Akk9niQQ8GV+8MJQYmg7fFFrbaZMIvgDgA6CqgMcJpIUBRyg5901EbWAoMSs+iqQdqhMIJymQHC8aBap9QHkIKLcD1S4gwpdGV3H6hmKltzUlVquC0oE+2NP4oaarOFJiMAcOV2P+02/j/97fjD0HqpDucaJ3WR6uumA0Zvx4DNwuu+wSW6bYgGhQ/uBOIAgcBnCEW4u7TvYfk/RKT6EkI8OKKVMUZGTsgxi+D76jmTj6TTaObPEgGuKHm85iKDGQ7bsPY9zlDyLD48Z9v7oIg/v3hMMaweZte/Hsix+iuCADF0waKrvMFjS8cDXU9KzZ1mIr4Alya3EncKSEYqUoGjoWtKFXLyfOOMMP2/envVEUwJ1TCXdOJYpHWlCzLwdHtmSheifPdN5RDCUGcuNdS2C1WLDu9d8hxe1oXDDaszQXF04eBqHFdwkBwGL/oYW8FnFrcYz4AFGstB3+VRU49VQXTjqp9QX5qjWCjNKDyCg9iJDPiaqdeTj0RQb8lTxxX1sYSgziaOUxvPvR17jvVxfVBxIAJ/b4ULR4MglrEhe2xkPD1mKrFfDYubWYKCG0G0pSUqyYPFlFfn7HdwjaXH7kDtiN3AG74a3IQMW32Tj8lQfRIM+XcyKGEoP4dtdhCCHQr0f+cZcK5IyaC3+g/k1z1pXj8cBvLpZTYEvUDraQ16JwGKgMc2txG7Q4MEf6oNXpm+JiJyZODMLliv2DiDurCu5RVSgarqJ2fzaObslB5Q5X/VndiaHE0ITAmlfnISoErpz7HALB+H6in/mbhaiq9eKNBTd2/ocVh7anbDqjcWux5fvRE24tBnhCPoqd0OCJRE8+2YURI3xxO3uxaokiveQw0ksOI+x3oHp3Hg59mQHvYY1uRkgShhKD6F2WC0VRsHXHweMujaJnaS4AwOXU0BNdsQIiDP2fLfAE0Uj91uJq1J+1OM0CeDh6QtRZWtp9Y7crmDjRgdLSBDV0BGB1BpDdtxzZfcvhr0pDxXc5OPxlOsJ+8+3eMd9vbFDZmak4a9wAPLF4Beq8329h1eT4uQIoKrQ8ZxwXgSBw2AfsUICDTsDnaP9niOh72jg+5OTY8eMfqygtTd6orjOjBkXDt2PwVRvR9/zdyO5bByhaPJYnBkOJgTx11xUIR6IY8aP78NL/rcXX3+7F1u0H8Pc3V2PL9gOwWDTw57Y46/uRmIWIArV+YG8A2G0DKs3TmI3TNxQ7+aGkf38XLrwwhLQ0ObWoqoCn8DC6n7kFQ6/+Aj0mHkRKvvF7JnH6xkB6leXiszf/C/c9/W/M+39vYM/BSjjsVgzsVYhfX3sWbrzyTLkFJrOFvBYFj9tanOqq31rs8nPnLNEJZC50tViA005zol+/xE3XdJbVEURW7z3I6r0HgRoPKrfn4tAXaQh5jbd7h6HEYArz0vH4HdPx+B0qNNWMTHWaO5Cc6JgPOIb6rcVp9vrGbAbbWqzJ2UPSBVkLXdPSrDjrLBXZ2dpdhO9Iq0XBsFrkD1FQdygbR7dl4+i2FIioMT7dMJQYloaeoIodiBp/2DEm4TBQEQYqUL+1OF2p31qsoT9f7AzxS5AUyQ8lZWVOTJgQhN2ujw8HiiqQWnAEqQVHUHKqDTV7cnH4qyzU7tP3+jWGEqNSVEAT2+osqB+x4cfmdh2/tTjNAaSFALt+txZzpIRil7xjl6IAI0e6MXRonTYbTHaAxR5CZs99yOy5D4FjKajakYtDX6QjWKu/t3j9VUwdpJEXl2IFBEdJOiUaAaq8QBUA5/dbi1P1uLVYI89B0p1kTd+4XBZMmmRFUZEXRnm+OlLrkD+4DnknAd4jWd+fHDAVIqyPBfYMJUalKAkfnIgKAauljYVWFhcQ0c5iMV3yBwE/gMMq4HHWN2Zz6nf0hKgjkrHQNT/fjsmTw0hJMeaHJkUBUnIrkJJbgZJR1vrpnS2ZqNntkl1amxhKDCvxqf/Q0Rr0Lstr+ZsqA0lciShQ4wNqANjt35+12A9YNLSY+QScvqFYJXqkZPBgN0aP9kLVx+BBl6nWMDK670dG9/0Iel2o2pGHw19mwF+lvQigvYooPhI4N1pZXYeP13+H9z/dhuunj29+BdVZH0iMMRqqPcEgcCQIHFWanrVYc/gEoNgkKpTYbCrGj3egZ0/z7gS0u33IG7QLeYN2wXs0AxXf5tSfHDCkjYTGUGJUCfyU+rN5f8Paz3fiV9eehQsnD236TcVa3xyN70eJJwRwzH/c1mJb/VmLrVpY4MyREuqK+D+HMzNtOOssICODI7gN3NlVcGdXoWi4BbX7c3BkSxaqdril1sRQYliJSwWvP3VDK99Rv79f7U4pGNbxW4vdTiAN0rcWs6MrxSreIyW9e7tw+uk+2GxxvVnDUK0RpHc7iPRuBxHyOVG9Kw+HvsiAryL5DxhDiVEl+/1AALDYjXPmXz3z+gEv6ltTevS/tZjMKD4fbFQVGDPGhUGDODrSUTaXHzn9dyOn/274KtNR8W0OjnydlrSTA2pjEqkTnnzySXTv3h1OpxOjR4/GmjVrZJdEAGB1M5BoTeT7rcW7Q8AeO1DjApLY9ZHTNxSreIyUpKRYcMEFdgaSLnBlVqN45HcYfOUm9Dm3HFm9vQk/OaCuQslLL72EuXPn4s4778SGDRswdOhQTJ06FYcOHZJdmgYl8R1BdbGFvNb5g8AhH7BTAQ65AL89CXfK6RuKTVdDSXGxAxdfLJCXZ6KTfyaQaokirfgQekz8GkN/+iW6n3kI7tzEPLa6CiUPP/wwfvGLX+Caa67BwIED8fTTT8PtduOvf/2r7NLMS+WUja5Ev99avCcI7LYDVYk7azFHSih2sYeSU05x45xzAnA6ubYtEazOALL7lmPAjzZj0E++QeHwSlhd8VsDpJs1JcFgEOvXr8e8efMaL1NVFZMnT8aqVata/JlAIIBA4IetkjU1NQmvUzOS8Y6gWOu7j7KFvD4Fg8ARJHBrMUdKKDaxjJQ4HComTLCjtJSjtsnizKhB0fAaFJ6s4NjB+pMDVnzTtZMD6iaUHDlyBJFIBPn5+U0uz8/Px5YtW1r8mfnz5+Puu+9ORnnak/BQoqB+oI3Do7p34tbidDvgCWhmazGZUedGOXJy7DjrrCg8Ho7ayqCoAp7CI/AUHkHJGBtqyvNw+KtMHNvf+ZMD6iaUxGLevHmYO3du4/9ramrQrVs3iRUlU4JDieoEolxAZjjhMHA0DBxF/dbidABuo5y1mPQiGu14IO7f34Vx4/ywWDhiqwVWewhZvfYiq9deBGpTUbk9F9vXtnE6khN/PoG1xVVOTg4sFgsOHjzY5PKDBw+ioKCgxZ9xOBxwOPR9GufYJXA+1eLmwlYzOHFrcXoQsHX8tO5cU0Kx6sj0jcUCnH66C3378sORVjk8x1Aw9BhcZX7gFx37Gd0sdLXb7Rg+fDiWL1/eeFk0GsXy5csxZswYiZVpVKLeESxOBhKzadhavCsM7HUAtR3dWszhFYqNEG1/qEpLs+Kii+wMJDqhdOIM57oZKQGAuXPnYsaMGRgxYgRGjRqFRx99FHV1dbjmmmtkl6ZBCVgPoNiBcIDvNWbmCwA+1Hel8jiBtAjgaHldEUdKKDaRNk/dVVbmwIQJIdjtHR+1I/3QVSi57LLLcPjwYdxxxx04cOAAhg0bhrfffrvZ4lfTE0hAcLAAiCa8cQ7pRDQKVPuAagAOW/1Zi1MDTc5azDbzFAtFibQYaBUFGDnShaFDvVASeMJRkktXoQQAZs+ejdmzZ8suQ9sUFXFfU6JagagWz0RL0gVCwOEQcEQBUl1AWhRw8blCsVGUaLNQ4nJZMGmSFUVFPnCo1th0F0qoAxQVaGdOtlNUF3faUPuEAGp9QC0Amw2WKJtXUSyaTj0XFDgwaVIIKSkMumbAUGJIcfwkYXEBEQYS6qRQCGnOY9iHHNmVkM4oyg+hZPBgF0aP9kHVzZYM6iqGEiNS1Pi0KVEdQNjH0VKKSbrbRB2UKW4UJQKbTcX48Q707MkPRGbDUGJIcUgRihWIhhhIKGYeV63sEkiHPJ4gzjpLRUYGA4kZcVDMiLq8Ml39/ja4JoBil+pgKKHOO+20z5GRwe2+ZsVQYkRdmboR+P7Mv6F4VUMmlWLn9A11TmnpQRQW7pBdBknE6Rsj6spIicUNROPfsXXmbxZi0evNz+b8zbL/Ru+yvLjfH8nntHCkhDrn1FNXyy6BJGMooR9YXAltIX/2GYPw/P0zmlyWm+VJ2P2RXBY1Ao/bi1qvW3YppAMDB25HRsbB9q9IhsZQQvUUOxBJ7Gm/HXYrCnLTE3ofpC1ZqTUMJdQuVY3ilFPWyC6DNIBrSoyosycdUSyAiCA++4iJfpCewikcat+IEV/BzS3kBI6UkFDqQwlaPqlaPL21YjNSh85p/P+0Mwbhlcd/mfD7JXnSXHyjobY5nUEMHLhBdhmkEQwlhtSJEQ+rM2kdWyeM7ocFf7yi8f8pLkdS7pfk8Tg5UkJtGzNmA+z2xE4dk34wlJiZxZ3Qha0nSnHbudPGZFIcHCmh1mVk1KJXry9ll0EawjUlhtSBpmeqEwgnL5CQObltHCmh1o0btxaqGmn/imQaDCWG1M70jWoDokG2kKeEc1i8sPBNh1pQVHQYRUXfyi6DNIahxIja3H2jAlEBtpCnZFAUgcxUjpZQc2PGrO76GTHIcLimxIhEG4FDtQHRQPJq+d7CB2cm/T5JGzJSa3GkJkN2GaQh/frtQnb2ftllkAZxpMSIWgslFpeUQELmxl4ldDxFiWLEiE9ll0EaxVBiOErLa0UsrqRt/SU6XpqTO3DoB6ecsgUpKVWyyyCNYigxnBb+pKoDCDOQkByp7FVC37PbQxg8eL3sMkjDGEoM54RhEsUKREPcaUPSpNgZSqjemDEbYbfzAxK1jqHEaJTj/6Qq6tMId9qQPG4bp28ISEurQ+/em2WXQRrHUGI0x++xU+2ACMmrhQiAVQ3CaecCa7MbN24tLJaw7DJI4xhKDOf7UGJxA1GeT4K0IcvDKRwzKyg4ipKSb2SXQTrAUGI4yvc7bdhCnrQjM5VTOGY2duxqKEonThRKpsVQYjgKEOEICWlLmosjJWbVp085cnL2yi6DdIKhxHAE2j33DVGSedirxJQURbBRGnUKQ4nRCC4kI+1hrxJzGjZsKzyeCtllkI4wlBiJYuFuG9Ikbgs2H5stjCFD1skug3SGocRIVLvsCoha5LQcA6cVzWX06E1wOLjgnjqHoYSIEk5Vo8hIrZNdBiVJaqoXfftukl0G6RBDiZFEOHVD2pXBswWbxtix62C1cn0bdR5DiVEoVgA8CJB2padwXYkZ5OVVorR0q+wySKcYSoxCscmugKhN6exVYgpjx34KVeX6IYoNQ4lR8CzApHGp7FVieD177kVe3m7ZZZCOMZQYRZTrSUjbUh0cKTG6UaPYKI26hqHECBQrm6aR5rlsDCVGNmTINqSlHZFdBukcQ4kRqFxPQtrnsHhh5anrDclqjWDYMDZKo65jKDECwUVlpA+ZqRwtMaJRozbD6TwmuwwyAIYSI+B6EtIJhhLjcbv96N9/o+wyyCCssgugLlJsPN8N6Ua6mztwjGbs2PWwWoOyyzC0mTMXYtGiVQAAq1VFVlYKhgwpwfTpIzFz5hioqnHGFxhK9E61spMr6UYae5UYSk5ONbp3/1p2GaZw9tmD8PzzMxCJRHHwYC3efvsL3HzzS3j11Q34179uhNVqkV1iXDCU6B2Xk5COpDg4UmIk9Y3SorLLMAWHw4qCgnQAQHFxJk45pRSnntoTkyY9goULV+HnPz9NcoXxYZwxH7OKctiU9COFvUoMo6zsAAoKdsouw9QmTuyPoUNL8M9/fia7lLhhKNEz1QYgIrsKog5zWRlKjGL06NWySyAA/fsXYOfOo7LLiBuGEl3j7Bvpi1UNwe30yy6Duuikk75DRsYh2WUQACEEFAOdZoShRNe4oIT0J8vDdSV6ZrFEcPLJa2SXQd/7+usD6NEjR3YZccNQomdcT0I6lOHmFI6ejRz5JVzcRaUJ7723BZs378XFF58su5S44fi/Xql2hhLSpTT2KtEtlyuAAQOMs6hSTwKBMA4cqG6yJXj+/Ldx3nmDcfXVY2SXFzcMJXqlWAEwlJD+eJz8lK1XY8ZsgM0WkF2GKb399pcoLPwNrFYVmZkpGDq0BI89dhlmzGDzNNICwd4ApE+p7FWiS5mZNejZ80vZZZjSwoUzsXDhTNllJIVx4pWZCHDqhnTLbedIiR6NG7eGjdIo4RhK9Ei1A+DBgfTJYTkGReHzV09KSg6hqGi77DLIBBhK9EjlrBvpl6oIZKTwNPd6MmYMG6VRcjCU6BHXk5DOZaZyCkcvBgzYgczMA7LLIJNgKNGjCFe/k76lpzCU6IGqRjF8OBulUfIwlOiN4gAUdnIlfUtzcQeOHgwf/hXc7mrZZZCJMJTojWqRXQFRl7FXifY5nUEMGrRBdhlkMgwleiN4VmDSP7edIyVad+qpG2G38+SJlFwMJXrD/iRkAG4bR0q0LCPjGHr12iy7DDIhhhI9Ue3gmYHJCOwWP+y2kOwyqBVjx66BxcJRWUo+hhI9UdifhIyD24K1qajoCIqLv5VdBpkUQ4meRPnJhYwjM5XrSrRozJjVUBTZVZBZ8aO3XggFPCswGUm6myMlWtO3725kZ++TXQaZGEdK9MLC9SRkLB4nR0q0RFEERoxgO3mSi6FELxT+qchYUtmrRFNOPnkLUlOrZJdBJsd3Or3g+W7IYNw2jpRohc0WwuDB62SXQcRQog8Kz3dDhuOy8kzBWjFmzCY4HD7ZZRAxlOiC6gC4Gp4MxqKG4XF7ZZdheh6PF336fC67DCIADCU6wT8TGVNmCqdwZBs3bi0slrDsMogA8N1OHwQ7X5IxpadwsatM+fkV6NZtm+wyiBoxlGieCkQZSsiY2KtErnHjVkNR2GqAtIOhROtUO9eTkGGlsleJNL1770FOzh7ZZRA1wVCidexPQgaW6uBIiRwCI0eyURppD9/xtE5wARoZl9vGUCLDsGHb4PFUyC6DqBndhJJ7770XY8eOhdvtRkZGhuxykkQFIjzfDRmXw1IHVWVjwGSy2cIYMmSt7DKIWqSbUBIMBnHppZfihhtukF1K8nA9CRmcoghkpXK0JJlGj/4cTif7w5A26eYswXfffTcAYOHChXILSSauJyETyEitxZGadNllmEJqqg99+26SXQZRq3QTSmIRCAQQCPzQnr2mRmcr/bkVmEwgnQ3Ukmbs2HWwWnlcIe0y9Efx+fPnIz09vfGrW7duskvqBAubppEppPFswUmRm1uF0tItsssgapPUUHL77bdDUZQ2v7Zsif1FNG/ePFRXVzd+lZeXx7H6BLPYZVdAlBSpDo6UJMPYsauhqmyURtomdfrmV7/6FWbOnNnmdXr27Bnz7TscDjgcjph/XioeO8gkUtirJOF69NiH/PzdsssgapfUUJKbm4vc3FyZJWgX+5OQSbisHClJtNGj2SiN9EE3C113796NiooK7N69G5FIBBs3bgQA9O7dG6mpqXKLizeF60nIPGyWIJz2IPxBTlkmwuDB3yAt7YjsMog6RDeh5I477sCiRYsa/3/yyScDAFasWIEzzzxTUlUJotqBiE92FURJk+Wpwb6jObLLMByrNYKTT2ajNNIP3ey+WbhwIYQQzb4MF0iITCiD24ITYuTIzXA6j8kug6jDdBNKTCXCqRsylzQXF7vGm9vtx4ABG2WXQdQpDCVao1gBcJErmYuHoSTuxozZAKuV584ifWEo0RrFJrsCoqRjr5L4ys6uRo8eX8kug6jTGEq0hifgIxNKsXOkJJ7Gjl3Dsy+TLjGUaA3Pd0Mm5LTUgh0D46O09AAKC3fILoMoJgwlWqJY2TSNTElVo0hPqZNdhiGceiobpZF+MZRoCdeTkIllpnIKp6sGDdqOjIxDsssgihlDCRFpQnoKQ0lXqGoUJ5+8RnYZRF3CUKIlUW7fI/NKd3EHTleMHPkl3G4+hqRvDCVaodgARGRXQSRNqpMjJbFyOgMYMGCD7DKIuoyhRCtU3ZyGiCghUuz8lB+rsWM/g90ekF0GUZcxlBCRJrjZqyQmmZm16NnzS9llEMUFQ4lWRLiehMzNrnphtXBLfGfVN0rj1C8ZA0OJFih2cD0JmZ2iAJmpPKNtZxQXH0JR0XeyyyCKG4YSLeB6EiIAQGYq15V0xpgxq6Hw1BRkIAwlWiB4jgoiAEjnltYO699/J7KyDsgugyiuGEpkE4L9SYi+53FxsWtHKEoUw4d/KrsMorhjKJFNtQPgSAkRwF4lHTV8+NdISamWXQZR3DGUyMbz3RA1SrFx+qY9DkcQJ520XnYZRAnBUCIdR0mIGrhsHClpz6mnboTd7pddBlFCMJTIJMD1JETHsaohuJ18w21Nevox9O79hewyiBKGoUQmrichaobbgls3duw6WNhgjgyMoUQm1SK7AiLNyXBzCqclhYVHUFLyjewyiBKKoUQmIWRXQKQ56SkMJS0ZO3Y1FIXHDDI2hhKZojyrJ9GJPE5O35yoT5/dyM7eJ7sMooRjKJFFcaB+pSsRHS/FwZGS4ymKwIgRbJRG5sBQIgvXkxC1yM1eJU0MG7YVHk+l7DKIkoKhRBbBswITtcRprePaie/ZbGEMGbJOdhlEScNQIoNQ2J+EqBWqEkVGyjHZZWjCqadugsPhlV0GUdIwlMhgsYPrSYhal5HKdSUejxd9+mySXQZRUjGUyKBwPQlRWzJSuK5k7Nh1sFrZKI3MhaFEhijXkxC1Jc1l7pGSvLwKlJZulV0GUdIxlCSdwv4kRO1IdZh7pGTcuE+52JdMiaEk2VQHoMgugkjbzNyrpGfPvcjNLZddBpEUDCXJpvAhJ2qPeXuVCIwevVp2EUTS8B0y2difhKhddosfNmtIdhlJN3ToN/B4jsoug0gahpKkUoEI15MQdUSWx1xTOFZrGEOHrpVdBpFUDCXJpNq5noSog8y2LXj06M1wOutkl0EkFUNJMnE9CVGHpbvNM1KSkuJDv34bZZdBJB3fJZMpykZIRB3lMVGvkrFj18NqwjU0RCdiKEkalee7IeoEs/QqycmpQlnZFtllEGkCQ0myWLiehKgzUuzmGCkZO/ZTqGpUdhlEmsBQkiyCDzVRZ7isxg8lZWX7UVCwS3YZRJrBd8pkEVxPQtQZFjWMVJdXdhkJxUZpRE0xlCSFCgiuJyHqLCP3Khk8+FtkZByWXQaRpjCUJINql10BkS4ZdVuwxRLBsGFrZJdBpDkMJcmgcIUrUSzS3cbcgTNy5BdwuY7JLoNIcxhKkoHrSYhikuo03kiJyxXAgAGfyS6DSJMYShLOAkTZFIkoFil2442UjB27HjYb15gRtcQqu4BkEkIAAGqO+ZN3pxYXEPEl7/6IDCQSOQifzzjBJDOzBtnZ61FTw74kZB41NfXvuQ3vwW1RREeuZRB79uxBt27dZJdBRERkOuXl5SgpKWnzOqYKJdFoFPv27YPH44HCxadxUVNTg27duqG8vBxpaWmyyzEsPs6Jx8c4Ofg4J57WHmMhBGpra1FUVARVbXvViKmmb1RVbTelUWzS0tI08eQ3Oj7OicfHODn4OCeelh7j9PT0Dl2PC12JiIhIExhKiIiISBMYSqhLHA4H7rzzTjgcDtmlGBof58TjY5wcfJwTT8+PsakWuhIREZF2caSEiIiINIGhhIiIiDSBoYSIiIg0gaGEiIiINIGhhLrkySefRPfu3eF0OjF69GisWbNGdkmGMX/+fIwcORIejwd5eXm46KKLsHXrVtllGd79998PRVFwyy23yC7FUPbu3YurrroK2dnZcLlcGDx4MNatWye7LEOJRCL4wx/+gB49esDlcqFXr1747//+7w6dc0YrGEooZi+99BLmzp2LO++8Exs2bMDQoUMxdepUHDp0SHZphvDBBx9g1qxZWL16NZYuXYpQKIQpU6agrq5OdmmGtXbtWjzzzDMYMmSI7FIMpbKyEuPGjYPNZsN//vMffPXVV3jooYeQmZkpuzRDeeCBB7BgwQI88cQT+Prrr/HAAw/gwQcfxOOPPy67tA7jlmCK2ejRozFy5Eg88cQTAOrPLdStWzfcdNNNuP322yVXZzyHDx9GXl4ePvjgA5xxxhmyyzGcY8eO4ZRTTsFTTz2Fe+65B8OGDcOjjz4quyxDuP322/Hxxx/jww8/lF2KoZ133nnIz8/Hc88913jZxRdfDJfLhb///e8SK+s4jpRQTILBINavX4/Jkyc3XqaqKiZPnoxVq1ZJrMy4qqurAQBZWVmSKzGmWbNm4dxzz23ynKb4+Ne//oURI0bg0ksvRV5eHk4++WT85S9/kV2W4YwdOxbLly/Htm3bAACbNm3CRx99hGnTpkmurONMdUI+ip8jR44gEokgPz+/yeX5+fnYsmWLpKqMKxqN4pZbbsG4ceNw0kknyS7HcF588UVs2LABa9eulV2KIW3fvh0LFizA3Llz8bvf/Q5r167FnDlzYLfbMWPGDNnlGcbtt9+Ompoa9O/fHxaLBZFIBPfeey+uvPJK2aV1GEMJkQ7MmjULX3zxBT766CPZpRhOeXk5br75ZixduhROp1N2OYYUjUYxYsQI3HfffQCAk08+GV988QWefvpphpI4evnll/GPf/wDS5YswaBBg7Bx40bccsstKCoq0s3jzFBCMcnJyYHFYsHBgwebXH7w4EEUFBRIqsqYZs+ejbfeegsrV65ESUmJ7HIMZ/369Th06BBOOeWUxssikQhWrlyJJ554AoFAABaLRWKF+ldYWIiBAwc2uWzAgAF47bXXJFVkTLfddhtuv/12XH755QCAwYMHY9euXZg/f75uQgnXlFBM7HY7hg8fjuXLlzdeFo1GsXz5cowZM0ZiZcYhhMDs2bPx+uuv47333kOPHj1kl2RIkyZNwubNm7Fx48bGrxEjRuDKK6/Exo0bGUjiYNy4cc22s2/btg1lZWWSKjImr9cLVW36tm6xWBCNRiVV1HkcKaGYzZ07FzNmzMCIESMwatQoPProo6irq8M111wjuzRDmDVrFpYsWYI333wTHo8HBw4cAACkp6fD5XJJrs44PB5Ps3U6KSkpyM7O5vqdOLn11lsxduxY3HffffjJT36CNWvW4Nlnn8Wzzz4ruzRDOf/883HvvfeitLQUgwYNwmeffYaHH34YP/vZz2SX1nGCqAsef/xxUVpaKux2uxg1apRYvXq17JIMA0CLX88//7zs0gxv/Pjx4uabb5ZdhqH87//+rzjppJOEw+EQ/fv3F88++6zskgynpqZG3HzzzaK0tFQ4nU7Rs2dP8fvf/14EAgHZpXUY+5QQERGRJnBNCREREWkCQwkRERFpAkMJERERaQJDCREREWkCQwkRERFpAkMJERERaQJDCREREWkCQwkRERFpAkMJERERaQJDCREREWkCQwkRERFpAkMJEWnW4cOHUVBQgPvuu6/xsk8++QR2ux3Lly+XWBkRJQJPyEdEmvbvf/8bF110ET755BP069cPw4YNw4UXXoiHH35YdmlEFGcMJUSkebNmzcKyZcswYsQIbN68GWvXroXD4ZBdFhHFGUMJEWmez+fDSSedhPLycqxfvx6DBw+WXRIRJQDXlBCR5n333XfYt28fotEodu7cKbscIkoQjpQQkaYFg0GMGjUKw4YNQ79+/fDoo49i8+bNyMvLk10aEcUZQwkRadptt92GV199FZs2bUJqairGjx+P9PR0vPXWW7JLI6I44/QNEWnW+++/j0cffRSLFy9GWloaVFXF4sWL8eGHH2LBggWyyyOiOONICREREWkCR0qIiIhIExhKiIiISBMYSoiIiEgTGEqIiIhIExhKiIiISBMYSoiIiEgTGEqIiIhIExhKiIiISBMYSoiIiEgTGEqIiIhIExhKiIiISBP+P47pALgePnBfAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "vt = Voironi_Tessalation(coreset_df, clusters, colors, tesslation_by_cluster=False)\n", + "vt.plot_voironi(plot_title=\"Voironi Tessalation of Coreset using VQE\", show_annotation=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## QAOA Implementation\n", + "\n", + "CUDA-Q is designed to be a flexible tool for developers so they can test different implementations of the same code. For example, one can perform the same analysis, instead using a QAOA approach. Only the kernel needs to be changed as is done below." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " »\n", + "q0 : ──●──────────────────●──────────────────────────────────────────────────»\n", + " ╭─┴─╮╭────────────╮╭─┴─╮ »\n", + "q1 : ┤ x ├┤ rz(0.3527) ├┤ x ├──●──────────────────●──────────────────────────»\n", + " ╰───╯╰────────────╯╰───╯╭─┴─╮╭────────────╮╭─┴─╮ »\n", + "q2 : ────────────────────────┤ x ├┤ rz(0.3527) ├┤ x ├──●──────────────────●──»\n", + " ╰───╯╰────────────╯╰───╯╭─┴─╮╭────────────╮╭─┴─╮»\n", + "q3 : ────────────────────────────────────────────────┤ x ├┤ rz(0.3527) ├┤ x ├»\n", + " ╰───╯╰────────────╯╰───╯»\n", + "q4 : ────────────────────────────────────────────────────────────────────────»\n", + " »\n", + "\n", + "################################################################################\n", + "\n", + " ╭───╮╭────────────╮╭───╮╭───────────╮\n", + "────────────────────────┤ x ├┤ rz(0.3527) ├┤ x ├┤ rx(1.111) ├\n", + " ╰─┬─╯╰────────────╯╰─┬─╯├───────────┤\n", + "──────────────────────────┼──────────────────┼──┤ rx(1.111) ├\n", + " │ │ ├───────────┤\n", + "──────────────────────────┼──────────────────┼──┤ rx(1.111) ├\n", + " │ │ ├───────────┤\n", + "──●──────────────────●────┼──────────────────┼──┤ rx(1.111) ├\n", + "╭─┴─╮╭────────────╮╭─┴─╮ │ │ ├───────────┤\n", + "┤ x ├┤ rz(0.3527) ├┤ x ├──●──────────────────●──┤ rx(1.111) ├\n", + "╰───╯╰────────────╯╰───╯ ╰───────────╯\n", + "\n" + ] + } + ], + "source": [ + "def get_QAOA_circuit(number_of_qubits, circuit_depth) -> cudaq.Kernel:\n", + " \"\"\"Returns the QAOA circuit for the given number of qubits and circuit depth\n", + "\n", + "\n", + " Args:\n", + " number_of_qubits (int): Number of qubits\n", + " circuit_depth (int): Circuit depth\n", + "\n", + " Returns:\n", + " cudaq.Kernel: QAOA Circuit\n", + " \"\"\"\n", + "\n", + " @cudaq.kernel\n", + " def kernel(thetas: list[float], number_of_qubits: int, circuit_depth: int):\n", + " qubits = cudaq.qvector(number_of_qubits)\n", + "\n", + " layers = circuit_depth\n", + "\n", + " for layer in range(layers):\n", + " for qubit in range(number_of_qubits):\n", + " cx(qubits[qubit], qubits[(qubit + 1) % number_of_qubits])\n", + " rz(2.0 * thetas[layer], qubits[(qubit + 1) % number_of_qubits])\n", + " cx(qubits[qubit], qubits[(qubit + 1) % number_of_qubits])\n", + "\n", + " rx(2.0 * thetas[layer + layers], qubits)\n", + "\n", + " return kernel\n", + "\n", + "\n", + "circuit = get_QAOA_circuit(5, circuit_depth)\n", + "\n", + "print(cudaq.draw(circuit, np.random.rand(2 * circuit_depth), 5, circuit_depth))" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [], + "source": [ + "def get_optimizer(\n", + " optimizer: cudaq.optimizers.optimizer, max_iterations, **kwargs\n", + ") -> Tuple[cudaq.optimizers.optimizer, int]:\n", + " \"\"\"\n", + " Returns the optimizer with the given parameters\n", + "\n", + " Args:\n", + " optimizer (cudaq.optimizers.optimizer): Optimizer\n", + " max_iterations (int): Maximum number of iterations\n", + " **kwargs: Additional arguments\n", + "\n", + " Returns:\n", + " tuple(cudaq.optimizers.optimizer, int): Optimizer and parameter count\n", + " \"\"\"\n", + "\n", + " parameter_count = 2 * kwargs[\"circuit_depth\"]\n", + " optimizer.initial_parameters = np.random.uniform(-np.pi / 8.0, np.pi / 8.0, parameter_count)\n", + " optimizer.max_iterations = max_iterations\n", + " return optimizer, parameter_count" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 484/484 [00:00<00:00, 12163.89it/s]\n", + "100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 12/12 [00:00<00:00, 52703.30it/s]\n", + "100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 48/48 [00:00<00:00, 31987.07it/s]\n", + "100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 4/4 [00:00<00:00, 36393.09it/s]\n", + "100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 4/4 [00:00<00:00, 37957.50it/s]\n", + "100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 4/4 [00:00<00:00, 42473.96it/s]\n" + ] + } + ], + "source": [ + "optimizer = cudaq.optimizers.COBYLA()\n", + "\n", + "divisive_clustering = DivisiveClusteringVQA(\n", + " circuit_depth=circuit_depth,\n", + " max_iterations=max_iterations,\n", + " max_shots=max_shots,\n", + " threshold_for_max_cut=0.75,\n", + " create_Hamiltonian=get_K2_Hamiltonian,\n", + " optimizer=optimizer,\n", + " optimizer_function=get_optimizer,\n", + " create_circuit=get_QAOA_circuit,\n", + " normalize_vectors=True,\n", + " sort_by_descending=True,\n", + " coreset_to_graph_metric=\"dist\",\n", + ")\n", + "\n", + "hierarchial_clustering_sequence = divisive_clustering.get_divisive_sequence(coreset_df)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Scaling simulations with CUDA-Q\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The University of Edinburgh team quickly encountered scaling challenges when they were developing this method. By developing with CUDA-Q they were able to port their code to an HPC environment once GPUs became available. GPUs massively accelerated their development and testing and allowed them to produce the 25 qubit experiments presented in their publication. If you have a GPU available, run the following code examples and see how the times compare on your device. Each call executes the divisive clustering procedure using the QAOA method, 100000 simulated shots for each VQE loop, and maximum 75 VQE iterations." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "First, try a slightly larger N=18 problem using the CPU (`qpp-cpu`) backend." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Uncomment the following line if you want to explicitly execute this step on your system.\n", + "# !python3 divisive_clustering_src/main_divisive_clustering.py --target qpp-cpu --M 18" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now try the N=18 example on the GPU backend (`nvidia`)." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Using BFL2 method to generate coresets\n", + "100%|███████████████████████████████████████| 751/751 [00:00<00:00, 3460.26it/s]\n", + "100%|████████████████████████████████████████| 16/16 [00:00<00:00, 42771.74it/s]\n", + "100%|█████████████████████████████████████| 4064/4064 [00:00<00:00, 6862.37it/s]\n", + "100%|██████████████████████████████████████████| 4/4 [00:00<00:00, 56871.92it/s]\n", + "100%|████████████████████████████████████████| 16/16 [00:00<00:00, 44979.13it/s]\n", + "100%|██████████████████████████████████████| 128/128 [00:00<00:00, 19366.94it/s]\n", + "100%|██████████████████████████████████████████| 4/4 [00:00<00:00, 53773.13it/s]\n", + "100%|██████████████████████████████████████████| 8/8 [00:00<00:00, 54648.91it/s]\n", + "100%|██████████████████████████████████████████| 8/8 [00:00<00:00, 51941.85it/s]\n", + "100%|██████████████████████████████████████████| 4/4 [00:00<00:00, 56111.09it/s]\n", + "Total time for the execution: 461.866833317\n", + "Total time spent on CUDA-Q: 10.452308367999706\n" + ] + } + ], + "source": [ + "!python3 divisive_clustering_src/main_divisive_clustering.py --target nvidia --M 18" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Scaling up to N=25, the task becomes even more onerous on a CPU. Depending on your device, the simulation may not even run. Try it and feel free to interrupt after your patience has worn out." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# target = 'qpp-cpu'\n", + "# Uncomment the following line if you want to explicitly execute this step on your system.\n", + "# !python3 divisive_clustering_src/main_divisive_clustering.py --target qpp-cpu --M 25" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "N=25 can still easily be completed by a single GPU." + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Using BFL2 method to generate coresets\n", + "100%|█████████████████████████████████████| 7352/7352 [00:03<00:00, 2063.82it/s]\n", + "100%|███████████████████████████████████| 16492/16492 [00:03<00:00, 4739.44it/s]\n", + "100%|██████████████████████████████████████| 256/256 [00:00<00:00, 15185.58it/s]\n", + "100%|████████████████████████████████████████| 64/64 [00:00<00:00, 23728.05it/s]\n", + "100%|██████████████████████████████████████| 256/256 [00:00<00:00, 15437.97it/s]\n", + "100%|██████████████████████████████████████████| 4/4 [00:00<00:00, 50840.05it/s]\n", + "100%|████████████████████████████████████████| 32/32 [00:00<00:00, 33562.82it/s]\n", + "100%|██████████████████████████████████████████| 4/4 [00:00<00:00, 54120.05it/s]\n", + "100%|██████████████████████████████████████████| 8/8 [00:00<00:00, 54560.05it/s]\n", + "100%|██████████████████████████████████████████| 8/8 [00:00<00:00, 55924.05it/s]\n", + "100%|████████████████████████████████████████| 16/16 [00:00<00:00, 42717.29it/s]\n", + "100%|██████████████████████████████████████████| 4/4 [00:00<00:00, 55007.27it/s]\n", + "100%|██████████████████████████████████████████| 4/4 [00:00<00:00, 53601.33it/s]\n", + "100%|██████████████████████████████████████████| 4/4 [00:00<00:00, 47127.01it/s]\n", + "Total time for the execution: 67.61674502899999\n", + "Total time spent on CUDA-Q: 21.439895901\n" + ] + } + ], + "source": [ + "# target = 'nvidia'\n", + "!python3 divisive_clustering_src/main_divisive_clustering.py --target nvidia --M 25" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If we want to push the simulation to an $N=34$ coreset, a single GPU (assuming A100) will run out of memory. Run the code below to see for yourself. " + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Using BFL2 method to generate coresets\n", + "RuntimeError: NLOpt runtime error: nlopt failure\n" + ] + } + ], + "source": [ + "# target = 'nvidia'\n", + "!python3 divisive_clustering_src/main_divisive_clustering.py --target nvidia --M 34" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To compute a problem with 34 qubits, we need to pool the memory of multiple GPUs. If you have multiple GPUs available, try the code below to run the same computation on 4 GPUs. You do not need to wait for the code to finish, just note how it does not fail immediately due to memory issues." + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Using BFL2 method to generate coresets\n", + "Using BFL2 method to generate coresets\n", + "Using BFL2 method to generate coresets\n", + "Using BFL2 method to generate coresets\n", + "^C\n" + ] + } + ], + "source": [ + "# target = 'nvidia-mgpu'\n", + "gpu_count = !nvidia-smi -L | wc -l\n", + "try:\n", + " gpu_count = int(gpu_count[0])\n", + "except:\n", + " gpu_count = 0 \n", + "if gpu_count >= 4:\n", + " !mpirun -np 4 python3 divisive_clustering_src/main_divisive_clustering.py --target nvidia-mgpu --M 34\n", + "else:\n", + " print(f'Not enough GPUs found on this system ({gpu_count}) to run this step')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/sphinx/examples/python/tutorials/divisive_clustering_src/divisive_clustering.py b/docs/sphinx/examples/python/tutorials/divisive_clustering_src/divisive_clustering.py new file mode 100644 index 00000000000..e9e87638383 --- /dev/null +++ b/docs/sphinx/examples/python/tutorials/divisive_clustering_src/divisive_clustering.py @@ -0,0 +1,1270 @@ +from abc import ABC, abstractmethod +from typing import Callable, Dict, List, Optional, Tuple, Union + +import cudaq +import networkx as nx +import numpy as np +import pandas as pd +from matplotlib import pyplot as plt +from scipy.cluster.hierarchy import dendrogram, fcluster +from scipy.spatial import Voronoi +from scipy.stats import multivariate_normal +from sklearn.cluster import KMeans +from tqdm import tqdm + + +class Coreset: + + def __init__( + self, + raw_data: np.ndarray, + number_of_sampling_for_centroids: int, + coreset_size: int, + number_of_coresets_to_evaluate: Optional[int] = 10, + coreset_method: Optional[str] = "BFL2", + k_value_for_BLK2: Optional[int] = 2, + ) -> None: + self._raw_data = raw_data + self._coreset_size = coreset_size + self._number_of_coresets_to_evaluate = number_of_coresets_to_evaluate + self._number_of_sampling_for_centroids = number_of_sampling_for_centroids + self._k_value_for_BLK2 = k_value_for_BLK2 + + if coreset_method not in ["BFL2", "BLK2"]: + raise ValueError("Coreset method must be either BFL2 or BLK2.") + else: + self._coreset_method = coreset_method + + @property + def raw_data(self) -> np.ndarray: + return self._raw_data + + @property + def coreset_size(self) -> int: + return self._coreset_size + + @property + def number_of_coresets_to_evaluate(self) -> int: + return self._number_of_coresets_to_evaluate + + @property + def number_of_sampling_for_centroids(self) -> int: + return self._number_of_sampling_for_centroids + + @property + def coreset_method(self) -> str: + return self._coreset_method + + @property + def k_value_for_BLK2(self) -> int: + return self._k_value_for_BLK2 + + @raw_data.setter + def raw_data(self, raw_data: np.ndarray) -> None: + self._raw_data = raw_data + + @coreset_size.setter + def coreset_size(self, coreset_size: int) -> None: + self._coreset_size = coreset_size + + @number_of_coresets_to_evaluate.setter + def number_of_coresets_to_evaluate( + self, number_of_coresets_to_evaluate: int) -> None: + self._number_of_coresets_to_evaluate = number_of_coresets_to_evaluate + + @number_of_sampling_for_centroids.setter + def number_of_sampling_for_centroids( + self, number_of_sampling_for_centroids: int) -> None: + self._number_of_sampling_for_centroids = number_of_sampling_for_centroids + + @coreset_method.setter + def coreset_method(self, coreset_method: str) -> None: + self._coreset_method = coreset_method + + @k_value_for_BLK2.setter + def k_value_for_BLK2(self, k_value_for_BLK2: int) -> None: + self._k_value_for_BLK2 = k_value_for_BLK2 + + def get_best_coresets(self) -> Tuple[np.ndarray, np.ndarray]: + """ + Get the best coreset vectors and weights for a given data. + + Returns: + `Tuple[np.ndarray, np.ndarray]`: The coreset vectors and weights. + """ + + centroids = self.get_best_centroids() + + if self._coreset_method == "BFL2": + print("Using BFL2 method to generate coresets") + coreset_vectors, coreset_weights = self.get_coresets_using_BFL2( + centroids) + + elif self._coreset_method == "BLK2": + print("Using BLK2 method to generate coresets") + coreset_vectors, coreset_weights = self.get_coresets_using_BLK2( + centroids) + else: + raise ValueError("Coreset method must be either BFL2 or BLK2.") + + coreset_vectors, coreset_weights = self.best_coreset_using_kmeans_cost( + coreset_vectors, coreset_weights) + + self.coreset_vectors = coreset_vectors + self.coreset_weights = coreset_weights + + return (np.array(coreset_vectors), np.array(coreset_weights)) + + def get_coresets_using_BFL2( + self, centroids: List[np.ndarray] + ) -> Tuple[List[np.ndarray], List[np.ndarray]]: + """ + Generates coreset vectors and weights using the BFL2 algorithm. + + Args: + `centroids (List[np.ndarray])`: The centroids to use for the coreset generation. + + Returns: + `Tuple[List[np.ndarray], List[np.ndarray]]`: List of coreset vectors and weights. + """ + + coreset_vectors_list = [] + coreset_weights_list = [] + for i in range(self.number_of_coresets_to_evaluate): + coreset_vectors, coreset_weights = self.BFL2(centroids=centroids) + coreset_vectors_list.append(coreset_vectors) + coreset_weights_list.append(coreset_weights) + + return (coreset_vectors_list, coreset_weights_list) + + def get_best_centroids(self) -> List[np.ndarray]: + """ + Get the best centroids using the D2 sampling algorithm. + + Returns: + List[np.ndarray]: The best centroids. + + """ + + best_centroid_coordinates, best_centroid_cost = None, np.inf + + for _ in range(self.number_of_sampling_for_centroids): + centroids = self.D2_sampling() + cost = self.get_cost(centroids) + if cost < best_centroid_cost: + best_centroid_coordinates, best_centroid_cost = centroids, cost + + return best_centroid_coordinates + + def D2_sampling(self) -> List[np.ndarray]: + """ + Selects the centroids from the data points using the D2 sampling algorithm. + + Returns: + List[np.ndarray]: The selected centroids as a list. + """ + + centroids = [] + data_vectors = self.raw_data + + centroids.append(data_vectors[np.random.choice(len(data_vectors))]) + + for _ in range(self.coreset_size - 1): + p = np.zeros(len(data_vectors)) + for i, x in enumerate(data_vectors): + p[i] = self.distance_to_centroids(x, centroids)[0]**2 + p = p / sum(p) + centroids.append(data_vectors[np.random.choice(len(data_vectors), + p=p)]) + + return centroids + + def get_cost(self, centroids: Union[List[np.ndarray], np.ndarray]) -> float: + """ + Computes the sum of between each data points and each centroids. + + Args: + `centroids (Union[List[np.ndarray], np.ndarray])`: The centroids to evaluate. + + Returns: + float: The cost of the centroids. + + """ + + cost = 0.0 + for x in self.raw_data: + cost += self.distance_to_centroids(x, centroids)[0]**2 + return cost + + def distance_to_centroids( + self, data_instance: np.ndarray, + centroids: Union[List[np.ndarray], + np.ndarray]) -> Tuple[float, int]: + """ + Compute the distance between a data instance and the centroids. + + Args: + `data_instance (np.ndarray)`: The data instance. + `centroids (Union[List[np.ndarray], np.ndarray])`: The centroids as a list or `numpy` array. + + Returns: + Tuple[float, int]: The minimum distance and the index of the closest centroid. + """ + + minimum_distance = np.inf + closest_index = -1 + for i, centroid in enumerate(centroids): + distance_between_data_instance_and_centroid = np.linalg.norm( + data_instance - centroid) + if distance_between_data_instance_and_centroid < minimum_distance: + minimum_distance = distance_between_data_instance_and_centroid + closest_index = i + + return (minimum_distance, closest_index) + + def BFL2( + self, centroids: Union[List[np.ndarray], np.ndarray] + ) -> Tuple[List[np.ndarray], List[float]]: + """ + Performs Algorithm 2 from https://arxiv.org/pdf/1612.00889.pdf BFL2. This will pick the coreset vectors and its corresponding weights. + + Args: + centroids (List): The centroids to use for the coreset generation. + + Returns: + Tuple[List, List]: The coreset vectors and coreset weights. + """ + + number_of_data_points_close_to_a_cluster = { + i: 0 for i in range(len(centroids)) + } + sum_distance_to_closest_cluster = 0.0 + for data_instance in self.raw_data: + min_dist, closest_index = self.distance_to_centroids( + data_instance, centroids) + number_of_data_points_close_to_a_cluster[closest_index] += 1 + sum_distance_to_closest_cluster += min_dist**2 + + Prob = np.zeros(len(self._raw_data)) + for i, p in enumerate(self._raw_data): + min_dist, closest_index = self.distance_to_centroids(p, centroids) + Prob[i] += min_dist**2 / (2 * sum_distance_to_closest_cluster) + Prob[i] += 1 / ( + 2 * len(centroids) * + number_of_data_points_close_to_a_cluster[closest_index]) + + if not (0.999 <= sum(Prob) <= 1.001): + raise ValueError( + "sum(Prob) = %s; the algorithm should automatically " + "normalize Prob by construction" % sum(Prob)) + chosen_indices = np.random.choice(len(self._raw_data), + size=self._coreset_size, + p=Prob) + weights = [1 / (self._coreset_size * Prob[i]) for i in chosen_indices] + + return ([self._raw_data[i] for i in chosen_indices], weights) + + def kmeans_cost(self, + coreset_vectors: np.ndarray, + sample_weight: Optional[np.ndarray] = None) -> float: + """ + Compute the cost of coreset vectors using k-means clustering. + + Args: + `coreset_vectors (np.ndarray)`: The coreset vectors. + `sample_weight (np.ndarray)`: The sample weights. + + Returns: + float: The cost of the k-means clustering. + + """ + + kmeans = KMeans(n_clusters=2).fit(coreset_vectors, + sample_weight=sample_weight) + return self.get_cost(kmeans.cluster_centers_) + + def best_coreset_using_kmeans_cost( + self, coreset_vectors: List[np.ndarray], + coreset_weights: List[np.ndarray]) -> Tuple[np.ndarray, np.ndarray]: + """ + Get the best coreset using k-means cost. + + Args: + `coreset_vectors (List[np.ndarray])`: The coreset vectors. + `coreset_weights (List[np.ndarray])`: The coreset weights. + + Returns: + Tuple: The best coreset vectors and coreset weights. + """ + + cost_coreset = [ + self.kmeans_cost( + coreset_vectors=coreset_vectors[i], + sample_weight=coreset_weights[i], + ) for i in range(self._number_of_coresets_to_evaluate) + ] + + best_index = cost_coreset.index(np.min(cost_coreset)) + return (coreset_vectors[best_index], coreset_weights[best_index]) + + def get_coresets_using_BLK2( + self, centroids: Union[List[np.ndarray], np.ndarray] + ) -> Tuple[List[List[np.ndarray]], List[List[float]]]: + """ + Generates coreset vectors and weights using Algorithm 2. + + Args: + `centroids (List[np.ndarray])`: The centroids to use for the coreset generation. + + Returns: + `Tuple[List[List[np.ndarray]], List[List[float]]]`: The coreset vectors and coreset weights. + """ + + coreset_vectors_list = [] + coreset_weights_list = [] + for i in range(self.number_of_coresets_to_evaluate): + coreset_vectors, coreset_weights = self.BLK2(centroids=centroids) + coreset_vectors_list.append(coreset_vectors) + coreset_weights_list.append(coreset_weights) + + return (coreset_vectors_list, coreset_weights_list) + + def BLK2( + self, + centroids: Union[List[np.ndarray], np.ndarray], + ) -> Tuple[List[np.ndarray], List[float]]: + """ + Performs Algorithm 2 from https://arxiv.org/pdf/1703.06476.pdf. + + Args: + `centroids (List[np.ndarray])`: The centroids to use for the coreset generation. + + Returns: + `Tuple[List, List]`: The coreset vectors and coreset weights. + """ + + alpha = 16 * (np.log2(self._k_value_for_BLK2) + 2) + + B_i_totals = [0] * len(centroids) + B_i = [np.empty_like(self._raw_data) for _ in range(len(centroids))] + for data_instance in self._raw_data: + _, closest_index = self.distance_to_centroids( + data_instance, centroids) + B_i[closest_index][B_i_totals[closest_index]] = data_instance + B_i_totals[closest_index] += 1 + + c_phi = sum([ + self.distance_to_centroids(data_instance, centroids)[0]**2 + for data_instance in self._raw_data + ]) / len(self._raw_data) + + p = np.zeros(len(self._raw_data)) + + sum_dist = {i: 0.0 for i in range(len(centroids))} + for i, data_instance in enumerate(self._raw_data): + dist, closest_index = self.distance_to_centroids( + data_instance, centroids) + sum_dist[closest_index] += dist**2 + + for i, data_instance in enumerate(self._raw_data): + p[i] = 2 * alpha * self.distance_to_centroids( + data_instance, centroids)[0]**2 / c_phi + + closest_index = self.distance_to_centroids(data_instance, + centroids)[1] + p[i] += 4 * alpha * sum_dist[closest_index] / ( + B_i_totals[closest_index] * c_phi) + + p[i] += 4 * len(self._raw_data) / B_i_totals[closest_index] + p = p / sum(p) + + chosen_indices = np.random.choice(len(self._raw_data), + size=self._coreset_size, + p=p) + weights = [1 / (self._coreset_size * p[i]) for i in chosen_indices] + + return [self._raw_data[i] for i in chosen_indices], weights + + @staticmethod + def coreset_to_graph( + coreset_vectors: np.ndarray, + coreset_weights: np.ndarray, + metric: Optional[str] = "dot", + number_of_qubits_representing_data: Optional[int] = 1, + ) -> nx.Graph: + """ + Convert coreset vectors to a graph. + + Args: + `coreset_vectors (np.ndarray)`: The coreset vectors. + `coreset_weights (np.ndarray)`: The coreset weights. + `metric (str, optional)`: The metric to use. Defaults to "dot". + `number_of_qubits_representing_data (int, optional)`: The number of qubits representing the data. Defaults to 1. + + Returns: + `nx.Graph`: The graph. + """ + + coreset = [(w, v) for w, v in zip(coreset_weights, coreset_vectors)] + + vertices = len(coreset) + vertex_labels = [ + number_of_qubits_representing_data * int(i) for i in range(vertices) + ] + G = nx.Graph() + G.add_nodes_from(vertex_labels) + edges = [( + number_of_qubits_representing_data * i, + number_of_qubits_representing_data * j, + ) for i in range(vertices) for j in range(i + 1, vertices)] + + G.add_edges_from(edges) + + for edge in G.edges(): + v_i = edge[0] // number_of_qubits_representing_data + v_j = edge[1] // number_of_qubits_representing_data + w_i = coreset[v_i][0] + w_j = coreset[v_j][0] + if metric == "dot": + mval = np.dot( + coreset[v_i][1], + coreset[v_j][1], + ) + elif metric == "dist": + mval = np.linalg.norm(coreset[v_i][1] - coreset[v_j][1]) + else: + raise Exception("Unknown metric: {}".format(metric)) + + G[edge[0]][edge[1]]["weight"] = w_i * w_j * mval + + return G + + @staticmethod + def normalize_array(vectors: np.ndarray, + centralize: bool = False) -> np.ndarray: + """ + Normalize and centralize the array + + Args: + `vectors (np.ndarray)`: The vectors to normalize + `centralize (bool, optional)`: Centralize the array. Defaults to False. + + Returns: + `np.ndarray`: The normalized array + """ + + if centralize: + vectors = vectors - np.mean(vectors, axis=0) + + max_abs = np.max(np.abs(vectors), axis=0) + vectors_norm = vectors / max_abs + + return vectors_norm + + @staticmethod + def create_dataset( + n_samples: float, + covariance_values: List[float] = [-0.8, -0.8], + n_features: Optional[int] = 2, + number_of_samples_from_distribution: Optional[int] = 500, + mean_array: Optional[np.ndarray] = np.array([[0, 0], [7, 1]]), + random_seed: Optional[int] = 10, + ) -> np.ndarray: + """ + Create a data set with the given parameters. + + Args: + `n_samples (float)`: The number of samples. + `covariance_values (List[float], optional)`: The covariance values. Defaults to [-0.8, -0.8]. + `n_features (int, optional)`: The number of features. Defaults to 2. + `number_of_samples_from_distribution (int, optional)`: The number of samples from the distribution. Defaults to 500. + `mean_array (np.ndarray, optional)`: The mean array. Defaults to `np.array([[0, 0], [7, 1]])`. + `random_seed (int, optional)`: The random seed. Defaults to 10. + + Returns: + `np.ndarray`: The data set created + """ + + random_seed = random_seed + + X = np.zeros((n_samples, n_features)) + + for idx, val in enumerate(covariance_values): + covariance_matrix = np.array([[1, val], [val, 1]]) + + distr = multivariate_normal(cov=covariance_matrix, + mean=mean_array[idx], + seed=random_seed) + + data = distr.rvs(size=number_of_samples_from_distribution) + + X[number_of_samples_from_distribution * + idx:number_of_samples_from_distribution * (idx + 1)][:] = data + + return X + + +class DivisiveClustering(ABC): + + def __init__( + self, + circuit_depth: int, + max_iterations: int, + max_shots: int, + threshold_for_max_cut: float, + create_Hamiltonian: Callable, + optimizer: cudaq.optimizers.optimizer, + optimizer_function: Callable, + create_circuit: Callable, + normalize_vectors: Optional[bool] = True, + sort_by_descending: Optional[bool] = True, + coreset_to_graph_metric: Optional[str] = "dot", + ) -> None: + self.circuit_depth = circuit_depth + self.max_iterations = max_iterations + self.max_shots = max_shots + self.threshold_for_maxcut = threshold_for_max_cut + self.normalize_vectors = normalize_vectors + self.sort_by_descending = sort_by_descending + self.coreset_to_graph_metric = coreset_to_graph_metric + self.create_Hamiltonian = create_Hamiltonian + self.create_circuit = create_circuit + self.optimizer = optimizer + self.optimizer_function = optimizer_function + + @abstractmethod + def run_divisive_clustering( + self, coreset_vectors_df_for_iteration: pd.DataFrame + ) -> Union[List[str], List[int]]: + """ + Run the divisive clustering algorithm. + + Args: + `coreset_vectors_df_for_iteration (pd.DataFrame)`: The coreset vectors for the iteration. + + Returns: + `Union[List[str], List[int]]`: The bitstring or the cluster. The return will depend on the name of the data point given. + """ + + pass + + def get_hierarchical_clustering_sequence( + self, + coreset_vectors_df_for_iteration: np.ndarray, + hierarchial_sequence: List, + ) -> List: + """ + Get the hierarchical clustering sequence. + + Args: + `coreset_vectors_df_for_iteration (np.ndarray)`: The coreset vectors for the iteration. + `hierarchial_sequence (List)`: The hierarchical sequence. + + """ + + bitstring = self.run_divisive_clustering( + coreset_vectors_df_for_iteration) + return self._add_children_to_hierarchial_clustering( + coreset_vectors_df_for_iteration, hierarchial_sequence, bitstring) + + def _get_iteration_coreset_vectors_and_weights( + self, coreset_vectors_df_for_iteration: pd.DataFrame + ) -> Tuple[np.ndarray, np.ndarray]: + """ + Gets the iteration coreset vectors and weights. + + Args: + `coreset_vectors_df_for_iteration (pd.DataFrame)`: The coreset vectors for the iteration. + + Returns: + `Tuple[np.ndarray, np.ndarray]`: The coreset vectors and weights. + + """ + + coreset_vectors_for_iteration = coreset_vectors_df_for_iteration[[ + "X", "Y" + ]].to_numpy() + + coreset_weights_for_iteration = coreset_vectors_df_for_iteration[ + "weights"].to_numpy() + + if self.normalize_vectors: + coreset_vectors_for_iteration = Coreset.normalize_array( + coreset_vectors_for_iteration, True) + coreset_weights_for_iteration = Coreset.normalize_array( + coreset_weights_for_iteration) + + return (coreset_vectors_for_iteration, coreset_weights_for_iteration) + + def brute_force_cost_maxcut(self, bitstrings: list[Union[str, int]], + G: nx.graph) -> Dict[str, float]: + """ + Cost function for brute force method + + Args: + bitstrings: list of bit strings + G: The graph of the problem + + Returns: + Dict: Dictionary with bitstring and cost value + """ + + cost_value = {} + for bitstring in tqdm(bitstrings): + c = 0 + for i, j in G.edges(): + edge_weight = G[i][j]["weight"] + c += self._get_edge_cost(bitstring, i, j, edge_weight) + + cost_value.update({bitstring: c}) + + return cost_value + + def _get_edge_cost(self, bitstring: str, i: int, j: int, + edge_weight: float) -> float: + """ + Get the edge cost using MaxCut cost function. + + Args: + bitstring: The bitstring + i: The first node + j: The second node + edge_weight: The edge weight + + Returns: + float: The edge cost + """ + + ai = int(bitstring[i]) + aj = int(bitstring[j]) + + return -1 * edge_weight * (1 - ((-1)**ai) * ((-1)**aj)) + + def _add_children_to_hierarchial_clustering( + self, + iteration_dataframe: pd.DataFrame, + hierarchial_sequence: list, + bitstring: str, + ) -> List[Union[str, int]]: + """ + Add children to the hierarchical clustering sequence. + + Args: + `iteration_dataframe (pd.DataFrame)`: The iteration data frame. + `hierarchial_sequence (list)`: The hierarchical sequence. + `bitstring (str)`: The bitstring. + + Returns: + list: The hierarchical sequence. + """ + + iteration_dataframe["cluster"] = [int(bit) for bit in bitstring] + + for j in range(2): + idx = list( + iteration_dataframe[iteration_dataframe["cluster"] == j].index) + if len(idx) > 0: + hierarchial_sequence.append(idx) + + return hierarchial_sequence + + @staticmethod + def get_divisive_cluster_cost(hierarchical_clustering_sequence: List[Union[ + str, int]], coreset_data: pd.DataFrame) -> List[float]: + """ + Get the cost of the divisive clustering at each iteration. + + Args: + `hierarchical_clustering_sequence (List)`: The hierarchical clustering sequence. + `coreset_data (pd.DataFrame)`: The coreset data. + + Returns: + List[float]: The cost of the divisive clustering sequence. + """ + + coreset_data = coreset_data.drop(["Name", "weights"], axis=1) + cost_at_each_iteration = [] + for parent in hierarchical_clustering_sequence: + children_lst = Dendrogram.find_children( + parent, hierarchical_clustering_sequence) + + if not children_lst: + continue + else: + children_1, children_2 = children_lst + + parent_data_frame = coreset_data.iloc[parent] + + parent_data_frame["cluster"] = 0 + + parent_data_frame.loc[children_2, "cluster"] = 1 + + cost = 0 + + centroid_coords = parent_data_frame.groupby("cluster").mean()[[ + "X", "Y" + ]] + centroid_coords = centroid_coords.to_numpy() + + for idx, row in parent_data_frame.iterrows(): + if row.cluster == 0: + cost += np.linalg.norm(row[["X", "Y"]] - + centroid_coords[0])**2 + else: + cost += np.linalg.norm(row[["X", "Y"]] - + centroid_coords[1])**2 + + cost_at_each_iteration.append(cost) + + return cost_at_each_iteration + + def _get_best_bitstring(self, counts: cudaq.SampleResult, + G: nx.Graph) -> str: + """ + From the simulator output, extract the best bitstring. + + Args: + `counts (cudaq.SampleResult)`: The counts. + `G (nx.Graph)`: The graph. + + Returns: + `str`: The best bitstring. + """ + + counts_pd = pd.DataFrame(counts.items(), + columns=["bitstring", "counts"]) + counts_pd[ + "probability"] = counts_pd["counts"] / counts_pd["counts"].sum() + bitstring_probability_df = counts_pd.drop(columns=["counts"]) + bitstring_probability_df = bitstring_probability_df.sort_values( + "probability", ascending=self.sort_by_descending) + + unacceptable_bitstrings = [ + "".join("1" for _ in range(10)), + "".join("0" for _ in range(10)), + ] + + bitstring_probability_df = bitstring_probability_df[ + ~bitstring_probability_df["bitstring"].isin(unacceptable_bitstrings + )] + + if len(bitstring_probability_df) > 10: + selected_rows = int( + len(bitstring_probability_df) * self.threshold_for_maxcut) + else: + selected_rows = int(len(bitstring_probability_df) / 2) + + bitstring_probability_df = bitstring_probability_df.head(selected_rows) + + bitstrings = bitstring_probability_df["bitstring"].tolist() + + brute_force_cost_of_bitstrings = self.brute_force_cost_maxcut( + bitstrings, G) + + return min(brute_force_cost_of_bitstrings, + key=brute_force_cost_of_bitstrings.get) + + def get_divisive_sequence( + self, full_coreset_df: pd.DataFrame) -> List[Union[str, int]]: + """ + Perform divisive clustering on the coreset data. + + Args: + `full_coreset_df (pd.DataFrame)`: The full coreset data. + + Returns: + `List[Union[str, int]]`: The hierarchical clustering sequence. + """ + + index_iteration_counter = 0 + single_clusters = 0 + + index_values = list(range(len(full_coreset_df))) + hierarchical_clustering_sequence = [index_values] + + while single_clusters < len(index_values): + index_values_to_evaluate = hierarchical_clustering_sequence[ + index_iteration_counter] + if len(index_values_to_evaluate) == 1: + single_clusters += 1 + + elif len(index_values_to_evaluate) == 2: + hierarchical_clustering_sequence.append( + [index_values_to_evaluate[0]]) + hierarchical_clustering_sequence.append( + [index_values_to_evaluate[1]]) + + else: + coreset_vectors_df_for_iteration = full_coreset_df.iloc[ + index_values_to_evaluate] + + hierarchical_clustering_sequence = self.get_hierarchical_clustering_sequence( + coreset_vectors_df_for_iteration, + hierarchical_clustering_sequence, + ) + + index_iteration_counter += 1 + + return hierarchical_clustering_sequence + + +class Dendrogram: + + def __init__( + self, coreset_data: pd.DataFrame, + hierarchical_clustering_sequence: List[Union[str, int]]) -> None: + self._coreset_data = self.__create_coreset_data(coreset_data) + self._hierarchial_clustering_sequence = self.__convert_numbers_to_name( + hierarchical_clustering_sequence, coreset_data) + self.linkage_matrix = [] + + @property + def coreset_data(self) -> pd.DataFrame: + return self._coreset_data + + @coreset_data.setter + def coreset_data(self, coreset_data: pd.DataFrame) -> None: + self.linkage_matrix = [] + self._coreset_data = coreset_data + + @property + def hierarchical_clustering_sequence(self) -> List[Union[str, int]]: + return self._hierarchial_clustering_sequence + + @hierarchical_clustering_sequence.setter + def hierarchical_clustering_sequence( + self, hierarchical_clustering_sequence: List[Union[str, + int]]) -> None: + self.linkage_matrix = [] + self._hierarchial_clustering_sequence = hierarchical_clustering_sequence + + def __call__(self) -> List: + if not self.linkage_matrix: + self.get_linkage_matrix(self._hierarchial_clustering_sequence[0]) + + return self.linkage_matrix + + def __create_coreset_data(self, coreset_data: pd.DataFrame) -> pd.DataFrame: + """ + Creates coreset data that can be used for plotting. + + Args: + `coreset_data (pd.DataFrame)`: The coreset data. + + Returns: + `pd.DataFrame`: The coreset data. + """ + + _coreset_data = coreset_data.copy() + _coreset_data.index = _coreset_data.Name + + return _coreset_data.drop(columns=["Name", "weights"]) + + def __convert_numbers_to_name(self, + hierarchical_clustering_sequence: List[int], + coreset_data: pd.DataFrame) -> List[str]: + """ + Converts the int in the hierarchical sequence into the instance name. This would be used to plot the leaves of the dendrogram. + + Args: + `hierarchical_clustering_sequence (List[int])`: The hierarchical clustering sequence. + `coreset_data (pd.DataFrame)`: The coreset data. + + Returns: + List[str]: The converted hierarchical clustering sequence. + """ + + converted_hc = [] + for hc in hierarchical_clustering_sequence: + converted_hc.append([coreset_data.Name[num] for num in hc]) + + return converted_hc + + def plot_dendrogram( + self, + plot_title: Optional[str] = "DIANA", + orientation: Optional[str] = "top", + color_threshold: Optional[int] = None, + colors: Optional[List] = None, + clusters: Optional[np.ndarray] = None, + link_color_func: Optional[Callable] = None, + ): + """ + Plots the dendrogram. + + Args: + `plot_title (str, optional)`: The plot title. Defaults to "DIANA". + `orientation (str, optional)`: The orientation of the dendrogram. Defaults to "top". + `color_threshold (int, optional)`: The color threshold to convert hierarchical clustering into flat clustering. Defaults to None. + `colors (List, optional)`: The colors for the leaves. Defaults to None. + `clusters (np.ndarray, optional)`: Flat clustering results from applying threshold. Defaults to None. + `link_color_func (Callable, optional)`: Function to color the branches. Defaults to None. + """ + + if not self.linkage_matrix: + self.get_linkage_matrix(self._hierarchial_clustering_sequence[0]) + + if clusters is None: + clusters = np.array([0] * len(self._coreset_data)) + + fig = plt.figure(figsize=(10, 10), dpi=100) + plt.title(plot_title) + dn = dendrogram( + self.linkage_matrix, + labels=self._coreset_data.index, + orientation=orientation, + color_threshold=color_threshold * 100 if colors else None, + ) + + if color_threshold is not None: + plt.axhline(y=color_threshold, color="r", linestyle="--") + + if colors is not None: + if len(colors) < len(set(clusters)): + raise ValueError( + "Number of colors should be equal to number of clusters") + else: + colors_dict = { + self._coreset_data.index[i]: colors[j] + for i, j in enumerate(clusters) + } + + ax = plt.gca() + xlbls = ax.get_xmajorticklabels() + for lbl in xlbls: + lbl.set_color(colors_dict[lbl.get_text()]) + + plt.show() + + def get_clusters_using_height(self, threshold: float) -> np.ndarray: + """ + Get flat clusters from the hierarchical clustering using a threshold. + + Args: + threshold (float): The height threshold to convert. + + Returns: + `np.ndarray`: The flat cluster labels. + """ + + if not self.linkage_matrix: + self.get_linkage_matrix(self._hierarchial_clustering_sequence[0]) + + clusters = fcluster(self.linkage_matrix, + threshold, + criterion="distance") + + return np.array(clusters) - 1 + + def get_clusters_using_k(self, k: int) -> np.ndarray: + """ + Get flat clusters from the hierarchical cluster by defining the number of clusters. + + Args: + k (int): The number of clusters. + + Returns: + `np.ndarray`: The flat cluster labels. + + """ + if not self.linkage_matrix: + self.get_linkage_matrix(self._hierarchial_clustering_sequence[0]) + + clusters = fcluster(self.linkage_matrix, k, criterion="maxclust") + + return np.array(clusters) - 1 + + def plot_clusters( + self, + clusters: np.ndarray, + colors: List[str], + plot_title: str, + show_annotation: Optional[bool] = False, + ): + """ + Plot the flat clusters. + + Args: + `clusters (np.ndarray)`: The flat clusters. + `colors (List[str])`: The colors for the clusters. + `plot_title (str)`: The plot title. + `show_annotation (bool, optional)`: Show annotation. Defaults to False. + + """ + if len(colors) < len(set(clusters)): + raise ValueError( + "Number of colors should be equal to number of clusters") + coreset_data = self._coreset_data.copy() + coreset_data["clusters"] = clusters + for i in range(coreset_data.clusters.nunique()): + data = coreset_data[coreset_data.clusters == i] + plt.scatter(data.X, data.Y, c=colors[i], label=f"Cluster {i}") + if show_annotation: + for _, row in coreset_data.iterrows(): + plt.annotate(row.name, (row.X, row.Y)) + plt.legend(bbox_to_anchor=(1.05, 1), loc="upper left") + plt.title(plot_title) + plt.show() + + def get_linkage_matrix(self, parent: List[str]) -> int: + """ + Create the linkage matrix for the dendrogram and returns the index of the new branch. + + Args: + parent (`List[str]`): The parent cluster. + + Returns: + List: The linkage matrix. + """ + + if len(parent) < 2: + index_of_parent = np.argwhere(self._coreset_data.index == parent[0]) + return index_of_parent[0][0] + children_1, children_2 = self.find_children( + parent, self._hierarchial_clustering_sequence) + + index1 = self.get_linkage_matrix(children_1) + index2 = self.get_linkage_matrix(children_2) + self.linkage_matrix.append([ + index1, + index2, + self.distance(index1) + self.distance(index2), + self.cluster_len(index1) + self.cluster_len(index2), + ]) + + return len(self.linkage_matrix) - 1 + len(self.coreset_data) + + def distance(self, i: int) -> float: + """ + Get the distance between two clusters. + + Args: + i (int): The index of the cluster. + + Returns: + float: The distance of the cluster. + """ + + if i >= len(self._coreset_data): + distance = self.linkage_matrix[i - len(self._coreset_data)][2] + else: + distance = sum( + self._coreset_data.iloc[i]) / (len(self.coreset_data) - 1) + + return abs(distance) + + def cluster_len(self, i: int): + """ + Get the length of the cluster. + + Args: + i (int): The index of the cluster. + + Returns: + int: The length of the cluster. + """ + + if i >= len(self._coreset_data): + return self.linkage_matrix[i - len(self._coreset_data)][3] + else: + return 1 + + @staticmethod + def find_children( + parent: List[Union[str, int]], + hierarchical_clustering_sequence: List[Union[str, int]]) -> List: + """ + Find the children of a given parent cluster. + + Args: + parent (List): The parent cluster. + hierarchical_clustering_sequence (List): The hierarchical clustering sequence. + + Returns: + List: The children of the parent cluster. + """ + + parent_position = hierarchical_clustering_sequence.index(parent) + + found = 0 + children = [] + for i in range(parent_position + 1, + len(hierarchical_clustering_sequence)): + if any(item in hierarchical_clustering_sequence[i] + for item in parent): + children.append(hierarchical_clustering_sequence[i]) + found += 1 + if found == 2: + break + + return children + + @staticmethod + def plot_hierarchial_split( + hierarchical_clustering_sequence: List[Union[str, int]], + full_coreset_df: pd.DataFrame): + """ + Plots the flat clusters at each iteration of the hierarchical clustering. + + Args: + hierarchical_clustering_sequence (List): The hierarchical clustering sequence. + `full_coreset_df` (`pd.DataFrame`): The full coreset data. + """ + parent_clusters = [ + parent_cluster + for parent_cluster in hierarchical_clustering_sequence + if len(parent_cluster) > 1 + ] + x_grid = int(np.sqrt(len(parent_clusters))) + y_grid = int(np.ceil(len(parent_clusters) / x_grid)) + + fig, axs = plt.subplots(x_grid, y_grid, figsize=(12, 12)) + + for i, parent_cluster in enumerate(parent_clusters): + parent_position = hierarchical_clustering_sequence.index( + parent_cluster) + children = Dendrogram.find_children( + parent_cluster, hierarchical_clustering_sequence) + coreset_for_parent_cluster = full_coreset_df.loc[parent_cluster] + coreset_for_parent_cluster["cluster"] = 1 + coreset_for_parent_cluster.loc[children[0], "cluster"] = 0 + + ax = axs[i // 3, i % 3] + ax.scatter( + coreset_for_parent_cluster["X"], + coreset_for_parent_cluster["Y"], + c=coreset_for_parent_cluster["cluster"], + ) + for _, row in coreset_for_parent_cluster.iterrows(): + ax.annotate(row["Name"], (row["X"], row["Y"])) + + ax.set_xlabel("X") + ax.set_ylabel("Y") + ax.set_title(f"Clustering at iteration {parent_position}") + + plt.tight_layout() + plt.show() + + +class Voironi_Tessalation: + + def __init__( + self, + coreset_df: pd.DataFrame, + clusters: np.ndarray, + colors: List[str], + tesslation_by_cluster: Optional[bool] = False, + ) -> None: + coreset_df["cluster"] = clusters + + if tesslation_by_cluster: + cluster_means = coreset_df.groupby("cluster")[["X", "Y"]].mean() + coreset_df = cluster_means.reset_index() + coreset_df["cluster"] = [i for i in range(len(coreset_df))] + + coreset_df["color"] = [colors[i] for i in coreset_df.cluster] + + points = coreset_df[["X", "Y"]].to_numpy() + + self.coreset_df = coreset_df + + self.voronoi = Voronoi(points) + + def voronoi_finite_polygons_2d(self, + radius: Optional[float] = None + ) -> Tuple[List, np.ndarray]: + """ + Creates the Voronoi regions and vertices for 2D data. + + Args: + radius (Optional[None]): The radius from the data points to create the Voronoi regions. Defaults to None. + + Returns: + Tuple: The regions and vertices. + """ + + if self.voronoi.points.shape[1] != 2: + raise ValueError("Requires 2D input") + + new_regions = [] + new_vertices = self.voronoi.vertices.tolist() + + center = self.voronoi.points.mean(axis=0) + if radius is None: + radius = self.voronoi.points.ptp().max() + + all_ridges = {} + for (p1, p2), (v1, v2) in zip(self.voronoi.ridge_points, + self.voronoi.ridge_vertices): + all_ridges.setdefault(p1, []).append((p2, v1, v2)) + all_ridges.setdefault(p2, []).append((p1, v1, v2)) + + for p1, region in enumerate(self.voronoi.point_region): + vertices = self.voronoi.regions[region] + + if all(v >= 0 for v in vertices): + new_regions.append(vertices) + continue + + ridges = all_ridges[p1] + new_region = [v for v in vertices if v >= 0] + + for p2, v1, v2 in ridges: + if v2 < 0: + v1, v2 = v2, v1 + if v1 >= 0: + continue + + t = self.voronoi.points[p2] - self.voronoi.points[p1] + t /= np.linalg.norm(t) + n = np.array([-t[1], t[0]]) + + midpoint = self.voronoi.points[[p1, p2]].mean(axis=0) + direction = np.sign(np.dot(midpoint - center, n)) * n + + far_point = self.voronoi.vertices[v2] + direction * radius + + new_region.append(len(new_vertices)) + new_vertices.append(far_point.tolist()) + + vs = np.asarray([new_vertices[v] for v in new_region]) + c = vs.mean(axis=0) + + angles = np.arctan2(vs[:, 1] - c[1], vs[:, 0] - c[0]) + new_region = np.array(new_region)[np.argsort(angles)] + + new_regions.append(new_region.tolist()) + + return new_regions, np.asarray(new_vertices) + + def plot_voironi( + self, + plot_title: Optional[str] = "Voronoi Tessalation", + show_annotation: bool = False, + show_scatters: bool = False, + ): + regions, vertices = self.voronoi_finite_polygons_2d() + fig, ax = plt.subplots(figsize=(8, 8)) + fig.tight_layout(pad=10) + + for j, region in enumerate(regions): + polygon = vertices[region] + color = self.coreset_df.color[j] + breakpoint() + plt.fill(*zip(*polygon), alpha=0.4, color=color, linewidth=0) + if show_annotation: + plt.annotate( + self.coreset_df.Name[j], + (self.coreset_df.X[j] + 0.2, self.coreset_df.Y[j]), + fontsize=10, + ) + + if show_scatters: + plt.plot(self.coreset_df.X, self.coreset_df.Y, "ko") + + plt.xlim(min(self.coreset_df.X) - 1, max(self.coreset_df.X) + 1) + plt.ylim(min(self.coreset_df.Y) - 1, max(self.coreset_df.Y) + 1) + plt.xlabel("x") + plt.ylabel("y") + plt.title(plot_title) + plt.show() diff --git a/docs/sphinx/examples/python/tutorials/divisive_clustering_src/main_divisive_clustering.py b/docs/sphinx/examples/python/tutorials/divisive_clustering_src/main_divisive_clustering.py new file mode 100644 index 00000000000..e9869e0d1c5 --- /dev/null +++ b/docs/sphinx/examples/python/tutorials/divisive_clustering_src/main_divisive_clustering.py @@ -0,0 +1,259 @@ +import argparse +import os +import sys +import time +import warnings +from typing import Callable + +import cudaq +import networkx as nx +import numpy as np +import pandas as pd +from cudaq import spin +from mpi4py import MPI + +sys.path.append(os.path.join(os.path.dirname(__file__), "..")) + +from divisive_clustering import Coreset, DivisiveClustering + +warnings.filterwarnings("ignore") + +argparser = argparse.ArgumentParser() +argparser.add_argument( + "-t", + "--target", + type=str, + choices=["qpp-cpu", "nvidia", "nvidia-mgpu"], + help= + "Quantum simulator backend. Default is qpp-cpu. See https://nvidia.github.io/cuda-quantum/0.6.0/using/simulators.html for more options.", +) +argparser.add_argument( + "-d", + "--depth", + type=int, + default=1, + help="Depth of the QAOA circuit. Default is 1.", +) +argparser.add_argument("-i", + "--max_iterations", + type=int, + default=75, + help="Max iterations for the optimizer.") +argparser.add_argument("-s", + "--max_shots", + type=int, + default=100000, + help="Max shots for the simulation.") +argparser.add_argument("-m", + "--M", + type=int, + default=10, + help="Size of the coreset.") + +args = argparser.parse_args() + +target = args.target +coreset_size = args.M +circuit_depth = args.depth +max_iterations = args.max_iterations +max_shots = args.max_shots + + +class DivisiveClusteringVQA(DivisiveClustering): + + def __init__( + self, + circuit_depth: int, + max_iterations: int, + max_shots: int, + threshold_for_max_cut: float, + create_Hamiltonian: Callable, + optimizer: cudaq.optimizers.optimizer, + optimizer_function: Callable, + create_circuit: Callable, + normalize_vectors: bool = True, + sort_by_descending: bool = True, + coreset_to_graph_metric: str = "dist", + ) -> None: + self.circuit_depth = circuit_depth + self.max_iterations = max_iterations + self.max_shots = max_shots + self.threshold_for_maxcut = threshold_for_max_cut + self.normalize_vectors = normalize_vectors + self.sort_by_descending = sort_by_descending + self.coreset_to_graph_metric = coreset_to_graph_metric + self.create_Hamiltonian = create_Hamiltonian + self.create_circuit = create_circuit + self.optimizer = optimizer + self.optimizer_function = optimizer_function + self.time_consumed = 0 + + def run_divisive_clustering( + self, + coreset_vectors_df_for_iteration: pd.DataFrame, + ): + coreset_vectors_for_iteration_np, coreset_weights_for_iteration_np = ( + self._get_iteration_coreset_vectors_and_weights( + coreset_vectors_df_for_iteration)) + + G = Coreset.coreset_to_graph( + coreset_vectors_for_iteration_np, + coreset_weights_for_iteration_np, + metric=self.coreset_to_graph_metric, + ) + + counts = self.get_counts_from_simulation( + G, + self.circuit_depth, + self.max_iterations, + self.max_shots, + ) + + return self._get_best_bitstring(counts, G) + + def get_counts_from_simulation(self, G, circuit_depth, max_iterations, + max_shots): + qubits = len(G.nodes) + Hamiltonian = self.create_Hamiltonian(G) + optimizer, parameter_count, initial_params = self.optimizer_function( + self.optimizer, + max_iterations, + qubits=qubits, + circuit_depth=circuit_depth) + + kernel = self.create_circuit(qubits, circuit_depth) + + def objective_function(parameter_vector: list[float], + hamiltonian=Hamiltonian, + kernel=kernel) -> tuple[float, list[float]]: + get_result = lambda parameter_vector: cudaq.observe( + kernel, hamiltonian, parameter_vector, qubits, circuit_depth + ).expectation() + + cost = get_result(parameter_vector) + + return cost + + t0 = time.process_time() + + energy, optimal_parameters = optimizer.optimize( + dimensions=parameter_count, function=objective_function) + + counts = cudaq.sample( + kernel, + optimal_parameters, + qubits, + circuit_depth, + shots_count=max_shots, + ) + + tf = time.process_time() + self.time_consumed += tf - t0 + + return counts + + +def get_K2_Hamiltonian(G: nx.Graph) -> cudaq.SpinOperator: + H = 0 + + for i, j in G.edges(): + weight = G[i][j]["weight"] + H += weight * (spin.z(i) * spin.z(j)) + + return H + + +def get_QAOA_circuit(number_of_qubits, circuit_depth): + + @cudaq.kernel + def kernel(thetas: list[float], number_of_qubits: int, circuit_depth: int): + qubits = cudaq.qvector(number_of_qubits) + + layers = circuit_depth + + for layer in range(layers): + for qubit in range(number_of_qubits): + cx(qubits[qubit], qubits[(qubit + 1) % number_of_qubits]) + rz(2.0 * thetas[layer], qubits[(qubit + 1) % number_of_qubits]) + cx(qubits[qubit], qubits[(qubit + 1) % number_of_qubits]) + + rx(2.0 * thetas[layer + layers], qubits) + + return kernel + + +def get_optimizer(optimizer: cudaq.optimizers.optimizer, max_iterations, + **kwargs): + parameter_count = 4 * kwargs["circuit_depth"] * kwargs["qubits"] + initial_params = np.random.uniform(-np.pi / 8.0, np.pi / 8.0, + parameter_count) + optimizer.initial_parameters = initial_params + + optimizer.max_iterations = max_iterations + return optimizer, parameter_count, initial_params + + +def create_coreset_df( + raw_data_size: int = 1000, + number_of_sampling_for_centroids: int = 10, + coreset_size: int = 10, + number_of_coresets_to_evaluate: int = 4, + coreset_method: str = "BFL2", +): + raw_data = Coreset.create_dataset(raw_data_size) + coreset = Coreset( + raw_data=raw_data, + number_of_sampling_for_centroids=number_of_sampling_for_centroids, + coreset_size=coreset_size, + number_of_coresets_to_evaluate=number_of_coresets_to_evaluate, + coreset_method=coreset_method, + ) + + coreset_vectors, coreset_weights = coreset.get_best_coresets() + + coreset_df = pd.DataFrame({ + "X": coreset_vectors[:, 0], + "Y": coreset_vectors[:, 1], + "weights": coreset_weights, + }) + coreset_df["Name"] = [chr(i + 65) for i in coreset_df.index] + + return coreset_df + + +if __name__ == "__main__": + cudaq.set_target(target) + + coreset_df = create_coreset_df( + raw_data_size=1000, + number_of_sampling_for_centroids=10, + coreset_size=coreset_size, + number_of_coresets_to_evaluate=4, + coreset_method="BFL2", + ) + + optimizer = cudaq.optimizers.COBYLA() + + divisive_clustering = DivisiveClusteringVQA( + circuit_depth=circuit_depth, + max_iterations=max_iterations, + max_shots=max_shots, + threshold_for_max_cut=0.5, + create_Hamiltonian=get_K2_Hamiltonian, + optimizer=optimizer, + optimizer_function=get_optimizer, + create_circuit=get_QAOA_circuit, + normalize_vectors=True, + sort_by_descending=True, + coreset_to_graph_metric="dist", + ) + + t0 = time.process_time() + + hierarchial_clustering_sequence = divisive_clustering.get_divisive_sequence( + coreset_df) + tf = time.process_time() + + print(f"Total time for the execution: {tf - t0}") + + print(f"Total time spent on CUDA-Q: {divisive_clustering.time_consumed}") diff --git a/docs/sphinx/examples/python/tutorials/readout_error_mitigation.ipynb b/docs/sphinx/examples/python/tutorials/readout_error_mitigation.ipynb index cec9dea4135..1d1fee40950 100644 --- a/docs/sphinx/examples/python/tutorials/readout_error_mitigation.ipynb +++ b/docs/sphinx/examples/python/tutorials/readout_error_mitigation.ipynb @@ -7,7 +7,7 @@ "source": [ "# Readout Error Mitigation\n", "\n", - "Readout errors are caused by imperfect qubit measurement and are a common source of error in quantum computing. Properly modelling these errors in simulation can give the user tools to understand better how these errors are and how to mitigate them when running on actual quantum devices.\n", + "Readout errors are caused by imperfect qubit measurement and are a common source of error in quantum computing. Properly modelling these errors in simulation can give the user tools to understand better what these errors are and how to mitigate them when running on actual quantum devices.\n", "\n", "Readout errors can be mitigated with a confusion matrix $A$. It is a square matrix of size $2^n \\times 2^n$ and tells about the probability of observing the state $|y\\rangle$ given the true state $|x\\rangle$. The confusion matrix characterizes the readout error of the device and is circuit-independent. Once $A$ is estimated, we can compute its pseudoinverse $A^+$ which can be applied to the noisy probability distribution $p_{\\text{noisy}}$ to obtain an adjusted quasi-probability distribution \n", "\n", @@ -18,7 +18,7 @@ "In this tutorial, we show how to build a confusion matrix with the following approaches.\n", "\n", "- Using a single qubit model\n", - "- using $k$ local confusion matrices\n", + "- Using $k$ local confusion matrices\n", "- A full confusion matrix for each $2^n$ combination\n", "\n", "The last method works well for correcting correlated errors (which affect multiple qubits together). However, it becomes impractical for large numbers of qubits." @@ -73,7 +73,7 @@ "To model the readout error, we apply a bitflip channel on every qubit at the end of the circuit using an Identity gate. The probability of bitflip `probs` can be the same or different for all the qubits.\n", "\n", "
Note: \n", - "In principle, readout error is applied to the measurement gate but we use Identity gate as adding a quantum error on the measurement gate is not yet supported. Also, the Identity gate is not present in the add_channel method, so we model the Identity gate using a rotation gate with an angle of 0.0.\n", + "In principle, readout error is applied to the measurement gate but we use the Identity gate because adding a quantum error on the measurement gate is not yet supported. Also, the Identity gate is not present in the add_channel method, so we model the Identity gate using a rotation gate with an angle of 0.0.\n", "
" ] }, @@ -114,7 +114,7 @@ "id": "bb4c7a14-1c0c-4fab-a6aa-7174c23baa7f", "metadata": {}, "source": [ - "We define a cudaq kernel which will help create a set of quantum circuits to take measurements of the basis states for $n$ qubits. It takes the number of qubits and the basis state as arguments." + "We define a cudaq kernel which will help create a set of quantum circuits to take measurements of the basis states for $n$ qubits. The kernel takes the number of qubits and the basis state as arguments." ] }, { @@ -141,7 +141,7 @@ "id": "95411463-cd1e-499f-baef-d624acc0122c", "metadata": {}, "source": [ - "For the tutorial, we will use GHZ state on three qubits for testing readout error mitigation. The GHZ state on 3 qubits is described as:\n", + "For the tutorial, we will use the GHZ state on three qubits to test readout error mitigation. The GHZ state on 3 qubits is described as:\n", "\n", "$$\n", "\\frac{|000\\rangle + |111\\rangle}{\\sqrt{2}}\n", @@ -163,7 +163,7 @@ " for i in range(n_qubits - 1):\n", " cx(qvector[i], qvector[i + 1])\n", "\n", - " # Apply id gates for readout error mitigation\n", + " # Apply identity gates for readout error mitigation\n", " for i in range(n_qubits):\n", " rx(0.0, qvector[i])\n", "\n", @@ -199,7 +199,7 @@ "id": "032f6898-c953-4d30-a2bf-e8b46ee77c62", "metadata": {}, "source": [ - "It is possible that the adjusted quasi-probability distribution $p_{\\text{mitigated}}$ obtained by application of $A^+$ to $p_{\\text{noisy}}$ can be possibly non-positive. We need to find the closest positive probability distribution in such a case.\n", + "It is possible that the adjusted quasi-probability distribution $p_{\\text{mitigated}}$ obtained by application of $A^+$ to $p_{\\text{noisy}}$ may be non-positive. We need to find the closest positive probability distribution in such a case.\n", "\n", " $$ p'' = \\min_{p_{\\rm positive}} \\|p_{\\rm noisy} - p_{\\rm positive}\\|_1$$\n", "\n", @@ -393,7 +393,7 @@ "source": [ "## Inverse confusion matrix from single-qubit noise model\n", "\n", - "Here we assume that the readout error of each qubit is independent and with the same confusion probabilities. Then, the confusion matrix $A_1$ for a single qubit can be defined as\n", + "Here we assume that the readout error of each qubit is independent with the same confusion probabilities. Then, the confusion matrix $A_1$ for a single qubit can be defined as\n", "\n", "$$\n", "A_1 = \\begin{bmatrix}\n", @@ -402,7 +402,7 @@ "\\end{bmatrix}\n", "$$\n", "\n", - "where $P(y|x)$ is the probability of observing state $|y\\rangle$ when measuring true state $|x\\rangle$. Using $A_1$ the full $2^n \\times 2^n$ confusion matrix $A$ can be written as \n", + "where $P(y|x)$ is the probability of observing state $|y\\rangle$ when measuring the true state $|x\\rangle$. Using $A_1$, the full $2^n \\times 2^n$ confusion matrix $A$ can be written as \n", "\n", "$$\n", "A = A_1 \\otimes \\dots \\otimes A_1\n", @@ -719,7 +719,7 @@ "source": [ "## Inverse of full confusion matrix\n", "\n", - "Here we generate a quantum circuit for each of the basis states of $n$ qubits i.e. $2^n$ combinations. This gives more accurate readout error mitigation. Since it scales exponentially with the number of qubits, it is only practical for small devices." + "Here we generate a quantum circuit for each of the basis states of $n$ qubits (i.e., $2^n$ combinations). This gives more accurate readout error mitigation. Since it scales exponentially with the number of qubits, it is only practical for small devices." ] }, { diff --git a/docs/sphinx/releases.rst b/docs/sphinx/releases.rst index e9ac3adfad5..6a8a3534625 100644 --- a/docs/sphinx/releases.rst +++ b/docs/sphinx/releases.rst @@ -10,6 +10,37 @@ The latest version of CUDA-Q is on the main branch of our `GitHub repository `__ - `Examples `__ +**0.8.0** + +The 0.8.0 release adds a range of changes to improve the ease of use and performance with CUDA-Q. +The changes listed below highlight some of what we think will be the most useful features and changes +to know about. While the listed changes do not capture all of the great contributions, we would like +to extend many thanks for every contribution, in particular those from external contributors. + +- `Docker image `__ +- `Python wheel `__ +- `C++ installer `__ +- `Documentation `__ +- `Examples `__ + +The full change log can be found `here `__. + +**0.7.1** + +The 0.7.1 release adds simulator optimizations with significant performance improvements and +extends their functionalities. The `nvidia-mgpu` backend now supports user customization of the +gate fusion level as controlled by the `CUDAQ_MGPU_FUSE` environment variable documented +`here `__. +It furthermore adds a range of bug fixes and changes the Python wheel installation instructions. + +- `Docker image `__ +- `Python wheel `__ +- `C++ installer `__ +- `Documentation `__ +- `Examples `__ + +The full change log can be found `here `__. + **0.7.0** The 0.7.0 release adds support for using :doc:`NVIDIA Quantum Cloud `, @@ -19,13 +50,13 @@ Check out our `documentation `__ to learn more about the new setup and its performance benefits. -- `Docker image `__ -- `Python wheel `__ -- `C++ installer `__ +- `Docker image `__ +- `Python wheel `__ +- `C++ installer `__ - `Documentation `__ - `Examples `__ -The full change log can be found `here `__. +The full change log can be found `here `__. **0.6.0** diff --git a/docs/sphinx/using/backends/platform.rst b/docs/sphinx/using/backends/platform.rst index 91c582a0bf8..18753617c97 100644 --- a/docs/sphinx/using/backends/platform.rst +++ b/docs/sphinx/using/backends/platform.rst @@ -49,7 +49,8 @@ Here is a simple example demonstrating its usage. ./a.out CUDA-Q exposes asynchronous versions of the default :code:`cudaq` algorithmic -primitive functions like :code:`sample` and :code:`observe` (e.g., :code:`sample_async` function in the above code snippets). +primitive functions like :code:`sample`, :code:`observe`, and :code:`get_state` +(e.g., :code:`sample_async` function in the above code snippets). Depending on the number of GPUs available on the system, the :code:`nvidia` multi-QPU platform will create the same number of virtual QPU instances. For example, on a system with 4 GPUs, the above code will distribute the four sampling tasks among those :code:`GPUEmulatedQPU` instances. @@ -70,6 +71,30 @@ The results might look like the following 4 different random samplings: To specify the number QPUs to be instantiated, one can set the :code:`CUDAQ_MQPU_NGPUS` environment variable. For example, use :code:`export CUDAQ_MQPU_NGPUS=2` to specify that only 2 QPUs (GPUs) are needed. +Since the underlying :code:`GPUEmulatedQPU` is a simulator backend, we can also retrieve the state vector from each +QPU via the :code:`cudaq::get_state_async` (C++) or :code:`cudaq.get_state_async` (Python) as shown in the bellow code snippets. + +.. tab:: Python + + .. literalinclude:: ../../snippets/python/using/cudaq/platform/get_state_async.py + :language: python + :start-after: [Begin Documentation] + +.. tab:: C++ + + .. literalinclude:: ../../snippets/cpp/using/cudaq/platform/get_state_async.cpp + :language: cpp + :start-after: [Begin Documentation] + :end-before: [End Documentation] + + + One can specify the target multi-QPU architecture with the :code:`--target` flag: + + .. code-block:: console + + nvq++ get_state_async.cpp --target nvidia --target-option mqpu + ./a.out + .. deprecated:: 0.8 The :code:`nvidia-mqpu` and :code:`nvidia-mqpu-fp64` targets, which are equivalent to the multi-QPU options `mgpu,fp32` and `mgpu,fp64`, respectively, of the :code:`nvidia` target, are deprecated and will be removed in a future release. diff --git a/docs/sphinx/using/tutorials.rst b/docs/sphinx/using/tutorials.rst index a9fcfe3864f..e1ded4b2111 100644 --- a/docs/sphinx/using/tutorials.rst +++ b/docs/sphinx/using/tutorials.rst @@ -17,3 +17,4 @@ Tutorials that give an in depth view of CUDA-Q and its applications in Python. /examples/python/tutorials/noisy_simulations.ipynb /examples/python/tutorials/readout_error_mitigation.ipynb /examples/python/tutorials/vqe_water_active_space.ipynb + /examples/python/tutorials/Divisive_clustering.ipynb diff --git a/include/cudaq/Optimizer/Builder/Factory.h b/include/cudaq/Optimizer/Builder/Factory.h index 53d79fdff73..8971479b203 100644 --- a/include/cudaq/Optimizer/Builder/Factory.h +++ b/include/cudaq/Optimizer/Builder/Factory.h @@ -260,5 +260,9 @@ mlir::Value createCast(mlir::OpBuilder &builder, mlir::Location loc, bool signExtend = false, bool zeroExtend = false); } // namespace factory + +std::size_t getDataSize(llvm::DataLayout &dataLayout, mlir::Type ty); +std::size_t getDataOffset(llvm::DataLayout &dataLayout, mlir::Type ty, + std::size_t off); } // namespace opt } // namespace cudaq diff --git a/include/cudaq/Optimizer/Builder/Runtime.h b/include/cudaq/Optimizer/Builder/Runtime.h index 3b3117bc2c1..bf81843fd92 100644 --- a/include/cudaq/Optimizer/Builder/Runtime.h +++ b/include/cudaq/Optimizer/Builder/Runtime.h @@ -23,5 +23,7 @@ static constexpr unsigned cudaqGenPrefixLength = sizeof(cudaqGenPrefixName) - 1; /// compile time (see `cudaqGenPrefixName`) or it can be rewritten to call back /// to the runtime library (and be handled at runtime). static constexpr const char launchKernelFuncName[] = "altLaunchKernel"; +static constexpr const char launchKernelVersion2FuncName[] = + "altLaunchKernelUsingLocalJIT"; } // namespace cudaq::runtime diff --git a/include/cudaq/Optimizer/Dialect/CC/CCOps.td b/include/cudaq/Optimizer/Dialect/CC/CCOps.td index 1e6cc15243e..a5177140413 100644 --- a/include/cudaq/Optimizer/Dialect/CC/CCOps.td +++ b/include/cudaq/Optimizer/Dialect/CC/CCOps.td @@ -1604,4 +1604,52 @@ def cc_CreateStringLiteralOp : CCOp<"string_literal"> { $stringLiteral `:` qualified(type(results)) attr-dict }]; } + +def cc_ArgumentSubstitutionOp : CCOp<"arg_subst", + [IsolatedFromAbove, NoRegionArguments, NoTerminator, SingleBlock]> { + let summary = "An argument substition."; + let description = [{ + This operation is used to define computations to produce a particular value. + The last Op in the block is the result and specifies the result type. + The code in the block will be substituted into a FuncOp to replace an + argument. The argument is erased from the function's signature, specializing + the function into a new function of reduced arity. (Typically, all arguments + are erased turning the function into a nullary.) + + For example, given a function + ```mlir + func.func @foo(%arg0: i32, %arg1: f32) ... + ``` + and a set of argument substitutions for the scalar arguments + ```mlir + cc.arg_subst[0] { + %c42_i32 = arith.constant 42 : i32 + } + cc.arg_subst[1] { + %cst = arith.constant 3.100000e+00 : f32 + } + ``` + the argument synthesis pass can substitute the arguments and create a new + nullary function + ```mlir + func.func @foo() { + %arg0 = arith.constant 42 : i32 + %arg1 = arith.constant 3.1 : f32 + ... + ``` + + Each arg_subst can hold an arbitrary block of code, allowing for the + construction of non-trivial values. + + See also the `argument-synthesis` pass. + }]; + + let arguments = (ins + ConfinedAttr:$position + ); + let regions = (region SizedRegion<1>:$body); + + let assemblyFormat = "`[` $position `]` $body attr-dict"; +} + #endif // CUDAQ_OPTIMIZER_DIALECT_CC_OPS diff --git a/include/cudaq/Optimizer/Transforms/Passes.h b/include/cudaq/Optimizer/Transforms/Passes.h index 0d6b25838f9..650fd6f500f 100644 --- a/include/cudaq/Optimizer/Transforms/Passes.h +++ b/include/cudaq/Optimizer/Transforms/Passes.h @@ -56,6 +56,13 @@ inline std::unique_ptr createPySynthCallableBlockArgs() { return createPySynthCallableBlockArgs({}); } +/// Helper function to build an argument synthesis pass. The names of the +/// functions and the substitutions text can be built as an unzipped pair of +/// lists. +std::unique_ptr createArgumentSynthesisPass( + const mlir::ArrayRef &funcNames, + const mlir::ArrayRef &substitutions); + // declarative passes #define GEN_PASS_DECL #define GEN_PASS_REGISTRATION @@ -75,4 +82,7 @@ inline std::unique_ptr createQuantumMemToReg() { return createMemToReg(m2rOpt); } +/// Name of `quake.wire_set` generated prior to mapping +static constexpr const char topologyAgnosticWiresetName[] = "wires"; + } // namespace cudaq::opt diff --git a/include/cudaq/Optimizer/Transforms/Passes.td b/include/cudaq/Optimizer/Transforms/Passes.td index 37228195709..78509a13d7e 100644 --- a/include/cudaq/Optimizer/Transforms/Passes.td +++ b/include/cudaq/Optimizer/Transforms/Passes.td @@ -11,6 +11,23 @@ include "mlir/Pass/PassBase.td" +def AddWireset : Pass<"add-wireset", "mlir::ModuleOp"> { + let summary = "Adds a topology-less `quake.wireset` to the module"; + let description = [{ + Adds a `quake.wireset` operation without tological info to the module. + }]; +} + +def AssignWireIndices : Pass<"assign-wire-indices", "mlir::func::FuncOp"> { + let summary = "Replaces wires with wires from a `quake.wireset`"; + let description = [{ + Replaces all instances of `quake.null_wire_op` with `quake.borrow_wire_op`s + from a common `quake.wireset` without any topological information. + Each wire is assigned a unique identifier (the index into the + `quake.wireset`) through this process. + }]; +} + def ApplyControlNegations : Pass<"apply-control-negations", "mlir::func::FuncOp"> { let summary = @@ -47,6 +64,41 @@ def ApplySpecialization : Pass<"apply-op-specialization", "mlir::ModuleOp"> { ]; } +def ArgumentSynthesis : Pass<"argument-synthesis", "mlir::func::FuncOp"> { + let summary = "Specialize a function by replacing arguments with constants"; + let description = [{ + This pass takes a list of functions and argument substitutions. For each + function in the list, the arguments to the function in the substitutions + list will be erased and replaced with a computed value (e.g., a constant) + provided in the substitution list. All arguments or some subset of arguments + may be substituted in this way. + + To facilitate command-line testing, this pass can be run with the functions + suboption using filenames containing the argument substitutions. For + example, one might run + ```console + cudaq-opt input.qke \ + --argument-synthesis=functions="kernel1:subst1.qke,kernel2:subst2.qke" + ``` + where `kernel1`, `kernel2` are the names of functions and `subst1.qke` and + `subst2.qke` are quake source files contains arg_subst operations. + + For running this pass from code, one can build the substitution code in a + std::string and use a prefix character '*' to indicate the text is inline + and not in a file. + ``` + kernel1:*"cc.arg_subst [0] { ... }" + ``` + }]; + + let options = [ + ListOption<"funcList", "functions", "std::string", + "Function name and substitutions pairs (:)">, + ]; + let dependentDialects = ["cudaq::cc::CCDialect", "mlir::LLVM::LLVMDialect", + "mlir::cf::ControlFlowDialect"]; +} + def AssignIDs : Pass<"assign-ids", "mlir::func::FuncOp"> { let summary = "Generate and assign unique identifiers for virtual qubits."; let description = [{ @@ -58,7 +110,7 @@ def AssignIDs : Pass<"assign-ids", "mlir::func::FuncOp"> { let dependentDialects = ["quake::QuakeDialect"]; } -def BasisConversionPass: Pass<"basis-conversion", "mlir::ModuleOp"> { +def BasisConversionPass : Pass<"basis-conversion", "mlir::ModuleOp"> { let summary = "Converts kernels to a set of basis operations."; let description = [{ This pass takes as input a list of target (allowed) quantum operations. @@ -299,6 +351,11 @@ def GenerateKernelExecution : Pass<"kernel-execution", "mlir::ModuleOp"> { Generate the kernel execution thunks. The kernel execution thunks allow the control side (C++ code) to launch quantum kernels. This pass generates the required glue code. + + Specifying the alt-launch=2 option will generate different code that makes + use of library side argument conversion and the argument synthesis pass. + More generally, this option can be used when JIT compiling kernels on the + client/host/local processor. }]; let dependentDialects = ["cudaq::cc::CCDialect", "mlir::LLVM::LLVMDialect"]; @@ -308,11 +365,14 @@ def GenerateKernelExecution : Pass<"kernel-execution", "mlir::ModuleOp"> { /*default=*/"\"-\"", "Name of output file.">, Option<"startingArgIdx", "starting-arg-idx", "std::size_t", /*default=*/"0", "The starting argument index for the argsCreator.">, + Option<"altLaunchVersion", "alt-launch", "std::size_t", /*default=*/"1", + "Specify the version of altLaunchKernel to be used."> ]; } def GetConcreteMatrix : Pass<"get-concrete-matrix", "mlir::func::FuncOp"> { - let summary = "Replace the unitary matrix generator function with concrete matrix."; + let summary = + "Replace the unitary matrix generator function with concrete matrix."; let description = [{ Given a custom operation whose generator attribute is another function within the module, such that if `LiftArrayAlloc` pass has run, there will diff --git a/lib/Optimizer/Builder/Intrinsics.cpp b/lib/Optimizer/Builder/Intrinsics.cpp index 345e239b3da..12030de1996 100644 --- a/lib/Optimizer/Builder/Intrinsics.cpp +++ b/lib/Optimizer/Builder/Intrinsics.cpp @@ -298,6 +298,12 @@ static constexpr IntrinsicCode intrinsicTable[] = { R"#( func.func private @altLaunchKernel(!cc.ptr, !cc.ptr, !cc.ptr, i64, i64) -> ())#"}, + {cudaq::runtime:: + launchKernelVersion2FuncName, // altLaunchKernelUsingLocalJIT + {}, + R"#( + func.func private @altLaunchKernelUsingLocalJIT(!cc.ptr, !cc.ptr, !cc.ptr) -> ())#"}, + {"free", {}, "func.func private @free(!cc.ptr) -> ()"}, {cudaq::llvmMemCopyIntrinsic, // llvm.memcpy.p0i8.p0i8.i64 diff --git a/lib/Optimizer/CodeGen/ConvertCCToLLVM.cpp b/lib/Optimizer/CodeGen/ConvertCCToLLVM.cpp index dd0d62d1e09..735c981717c 100644 --- a/lib/Optimizer/CodeGen/ConvertCCToLLVM.cpp +++ b/lib/Optimizer/CodeGen/ConvertCCToLLVM.cpp @@ -24,6 +24,7 @@ #include "mlir/Conversion/MathToLLVM/MathToLLVM.h" #include "mlir/Conversion/SCFToControlFlow/SCFToControlFlow.h" #include "mlir/Dialect/Arith/Transforms/Passes.h" +#include "mlir/Target/LLVMIR/TypeToLLVM.h" namespace cudaq::opt { #define GEN_PASS_DEF_CCTOLLVM @@ -79,6 +80,27 @@ void cudaq::opt::populateCCTypeConversions(LLVMTypeConverter *converter) { }); } +std::size_t cudaq::opt::getDataSize(llvm::DataLayout &dataLayout, Type ty) { + LLVMTypeConverter converter(ty.getContext()); + cudaq::opt::populateCCTypeConversions(&converter); + auto llvmDialectTy = converter.convertType(ty); + llvm::LLVMContext context; + LLVM::TypeToLLVMIRTranslator translator(context); + auto llvmTy = translator.translateType(llvmDialectTy); + return dataLayout.getTypeAllocSize(llvmTy); +} + +std::size_t cudaq::opt::getDataOffset(llvm::DataLayout &dataLayout, Type ty, + std::size_t off) { + LLVMTypeConverter converter(ty.getContext()); + cudaq::opt::populateCCTypeConversions(&converter); + auto llvmDialectTy = converter.convertType(ty); + llvm::LLVMContext context; + LLVM::TypeToLLVMIRTranslator translator(context); + auto llvmTy = cast(translator.translateType(llvmDialectTy)); + return dataLayout.getStructLayout(llvmTy)->getElementOffset(off); +} + namespace { struct CCToLLVM : public cudaq::opt::impl::CCToLLVMBase { using CCToLLVMBase::CCToLLVMBase; diff --git a/lib/Optimizer/Transforms/ArgumentSynthesis.cpp b/lib/Optimizer/Transforms/ArgumentSynthesis.cpp new file mode 100644 index 00000000000..7281c0e21f7 --- /dev/null +++ b/lib/Optimizer/Transforms/ArgumentSynthesis.cpp @@ -0,0 +1,154 @@ +/******************************************************************************* + * Copyright (c) 2022 - 2024 NVIDIA Corporation & Affiliates. * + * All rights reserved. * + * * + * This source code and the accompanying materials are made available under * + * the terms of the Apache License 2.0 which accompanies this distribution. * + ******************************************************************************/ + +#include "PassDetails.h" +#include "cudaq/Optimizer/Dialect/CC/CCOps.h" +#include "cudaq/Optimizer/Transforms/Passes.h" +#include "llvm/Support/Debug.h" +#include "mlir/IR/IRMapping.h" +#include "mlir/Parser/Parser.h" +#include "mlir/Transforms/GreedyPatternRewriteDriver.h" +#include "mlir/Transforms/Passes.h" + +namespace cudaq::opt { +#define GEN_PASS_DEF_ARGUMENTSYNTHESIS +#include "cudaq/Optimizer/Transforms/Passes.h.inc" +} // namespace cudaq::opt + +#define DEBUG_TYPE "argument-synthesis" + +using namespace mlir; + +namespace { +class ArgumentSynthesisPass + : public cudaq::opt::impl::ArgumentSynthesisBase { +public: + using ArgumentSynthesisBase::ArgumentSynthesisBase; + + void runOnOperation() override { + func::FuncOp func = getOperation(); + StringRef funcName = func.getName(); + std::string text; + if (std::find_if(funcList.begin(), funcList.end(), + [&](const std::string &item) { + auto pos = item.find(':'); + if (pos == std::string::npos) + return false; + std::string itemName = item.substr(0, pos); + bool result = itemName == funcName; + if (result) + text = item.substr(pos + 1); + return result; + }) == funcList.end()) { + // If the function isn't on the list, do nothing. + LLVM_DEBUG(llvm::dbgs() << funcName << " not in list.\n"); + return; + } + + // If there are no substitutions, we're done. + if (text.empty()) { + LLVM_DEBUG(llvm::dbgs() << funcName << " has no substitutions."); + return; + } + + // If we're here, we have a FuncOp and we have substitutions that can be + // applied. + // + // 1. Create a Module with the substitutions that we'll be making. + auto *ctx = func.getContext(); + LLVM_DEBUG(llvm::dbgs() << "substitution pattern: '" << text << "'\n"); + auto substMod = [&]() -> OwningOpRef { + if (text.front() == '*') { + // Substitutions are a raw string after the '*' character. + return parseSourceString(text.substr(1), ctx); + } + // Substitutions are in a text file (command-line usage). + return parseSourceFile(text, ctx); + }(); + assert(*substMod && "module must have been created"); + + // 2. Go through the Module and process each substitution. + std::vector processedArgs(func.getFunctionType().getNumInputs()); + std::vector> replacements; + for (auto &op : *substMod) { + auto subst = dyn_cast(op); + if (!subst) { + if (auto symInterface = dyn_cast(op)) { + auto name = symInterface.getName(); + auto srcMod = func->getParentOfType(); + auto obj = srcMod.lookupSymbol(name); + if (!obj) + srcMod.getBody()->push_back(op.clone()); + } + continue; + } + auto pos = subst.getPosition(); + if (pos >= processedArgs.size()) { + func.emitError("Argument " + std::to_string(pos) + " is invalid."); + signalPassFailure(); + return; + } + if (processedArgs[pos]) { + func.emitError("Argument " + std::to_string(pos) + + " was already substituted."); + signalPassFailure(); + return; + } + + // OK, substitute the code for the argument. + Block &entry = func.getRegion().front(); + processedArgs[pos] = true; + OpBuilder builder{ctx}; + Block *splitBlock = entry.splitBlock(entry.begin()); + builder.setInsertionPointToEnd(&entry); + builder.create(func.getLoc(), &subst.getBody().front()); + Operation *lastOp = &subst.getBody().front().back(); + builder.setInsertionPointToEnd(&subst.getBody().front()); + builder.create(func.getLoc(), splitBlock); + func.getBlocks().splice(Region::iterator{splitBlock}, + subst.getBody().getBlocks()); + if (lastOp && + lastOp->getResult(0).getType() == entry.getArgument(pos).getType()) { + LLVM_DEBUG(llvm::dbgs() + << funcName << " argument " << std::to_string(pos) + << " was substituted.\n"); + replacements.emplace_back(pos, entry.getArgument(pos), + lastOp->getResult(0)); + } + } + + // Note: if we exited before here, any code that was cloned into the + // function is still dead and can be removed by a DCE. + + // 3. Replace the block argument values with the freshly inserted new code. + BitVector replacedArgs(processedArgs.size()); + for (auto [pos, fromVal, toVal] : replacements) { + replacedArgs.set(pos); + fromVal.replaceAllUsesWith(toVal); + } + + // 4. Finish specializing func and erase any of func's arguments that were + // substituted. + func.eraseArguments(replacedArgs); + } +}; +} // namespace + +// Helper function that takes an unzipped pair of lists of function names and +// substitution code strings. This is meant to make adding this pass to a +// pipeline easier from within a tool (such as the JIT compiler). +std::unique_ptr cudaq::opt::createArgumentSynthesisPass( + const ArrayRef &funcNames, + const ArrayRef &substitutions) { + SmallVector pairs; + if (funcNames.size() == substitutions.size()) + for (auto [name, text] : llvm::zip(funcNames, substitutions)) + pairs.emplace_back(name.str() + ":*" + text.str()); + return std::make_unique( + ArgumentSynthesisOptions{pairs}); +} diff --git a/lib/Optimizer/Transforms/CMakeLists.txt b/lib/Optimizer/Transforms/CMakeLists.txt index 10421b1ddcf..39a185d8957 100644 --- a/lib/Optimizer/Transforms/CMakeLists.txt +++ b/lib/Optimizer/Transforms/CMakeLists.txt @@ -15,6 +15,7 @@ add_cudaq_library(OptTransforms AggressiveEarlyInlining.cpp ApplyControlNegations.cpp ApplyOpSpecialization.cpp + ArgumentSynthesis.cpp AssignIDs.cpp BasisConversion.cpp CombineQuantumAlloc.cpp @@ -50,6 +51,7 @@ add_cudaq_library(OptTransforms RefToVeqAlloc.cpp RegToMem.cpp StatePreparation.cpp + WiresToWiresets.cpp DEPENDS OptTransformsPassIncGen diff --git a/lib/Optimizer/Transforms/GenKernelExecution.cpp b/lib/Optimizer/Transforms/GenKernelExecution.cpp index 6d954e7addb..78176e53877 100644 --- a/lib/Optimizer/Transforms/GenKernelExecution.cpp +++ b/lib/Optimizer/Transforms/GenKernelExecution.cpp @@ -1116,10 +1116,11 @@ class GenerateKernelExecution /// library. Pass along the thunk, so the runtime can call the quantum /// circuit. These entry points are `operator()` member functions in a class, /// so account for the `this` argument here. - void genNewHostEntryPoint(Location loc, OpBuilder &builder, - FunctionType funcTy, cudaq::cc::StructType structTy, - LLVM::GlobalOp kernelNameObj, func::FuncOp thunk, - func::FuncOp rewriteEntry, bool addThisPtr) { + void genNewHostEntryPoint1(Location loc, OpBuilder &builder, + FunctionType funcTy, + cudaq::cc::StructType structTy, + LLVM::GlobalOp kernelNameObj, func::FuncOp thunk, + func::FuncOp rewriteEntry, bool addThisPtr) { auto *ctx = builder.getContext(); auto i64Ty = builder.getI64Type(); auto offset = funcTy.getNumInputs(); @@ -1392,6 +1393,91 @@ class GenerateKernelExecution builder.create(loc, results); } + void genNewHostEntryPoint2(Location loc, OpBuilder &builder, + FunctionType devFuncTy, + LLVM::GlobalOp kernelNameObj, + func::FuncOp hostFunc, bool addThisPtr) { + const bool hiddenSRet = cudaq::opt::factory::hasHiddenSRet(devFuncTy); + const unsigned count = + cudaq::cc::numberOfHiddenArgs(addThisPtr, hiddenSRet); + auto *ctx = builder.getContext(); + auto i8PtrTy = cudaq::cc::PointerType::get(builder.getI8Type()); + + // 0) Pointer our builder into the entry block of the function. + Block *hostFuncEntryBlock = hostFunc.addEntryBlock(); + + OpBuilder::InsertionGuard guard(builder); + builder.setInsertionPointToStart(hostFuncEntryBlock); + + // 1) Allocate and initialize a std::vector object. + auto stdVec = builder.create( + loc, cudaq::opt::factory::stlVectorType(i8PtrTy)); + auto arrPtrTy = cudaq::cc::ArrayType::get(ctx, i8PtrTy, count); + Value buffer = builder.create(loc, arrPtrTy); + auto i64Ty = builder.getI64Type(); + auto buffSize = builder.create(loc, i64Ty, arrPtrTy); + auto ptrPtrTy = cudaq::cc::PointerType::get(i8PtrTy); + auto cast1 = builder.create(loc, ptrPtrTy, buffer); + auto ptr3Ty = cudaq::cc::PointerType::get(ptrPtrTy); + auto stdVec0 = builder.create(loc, ptr3Ty, stdVec); + builder.create(loc, cast1, stdVec0); + auto cast2 = builder.create(loc, i64Ty, buffer); + auto endBuff = builder.create(loc, cast2, buffSize); + auto cast3 = builder.create(loc, ptrPtrTy, endBuff); + auto stdVec1 = builder.create( + loc, ptr3Ty, stdVec, ArrayRef{1}); + builder.create(loc, cast3, stdVec1); + auto stdVec2 = builder.create( + loc, ptr3Ty, stdVec, ArrayRef{2}); + builder.create(loc, cast3, stdVec2); + auto zero = builder.create(loc, 0, 64); + auto nullPtr = builder.create(loc, i8PtrTy, zero); + + // 2) Iterate over the arguments passed in and populate the vector. + SmallVector blockArgs{dropAnyHiddenArguments( + hostFuncEntryBlock->getArguments(), devFuncTy, addThisPtr)}; + for (auto iter : llvm::enumerate(blockArgs)) { + std::int32_t i = iter.index(); + auto pos = builder.create( + loc, ptrPtrTy, buffer, ArrayRef{i}); + auto blkArg = iter.value(); + if (isa(blkArg.getType())) { + auto castArg = builder.create(loc, i8PtrTy, blkArg); + builder.create(loc, castArg, pos); + continue; + } + auto temp = builder.create(loc, blkArg.getType()); + builder.create(loc, blkArg, temp); + auto castTemp = builder.create(loc, i8PtrTy, temp); + builder.create(loc, castTemp, pos); + } + + auto resultBuffer = builder.create(loc, i8PtrTy); + builder.create(loc, nullPtr, resultBuffer); + auto castResultBuffer = + builder.create(loc, i8PtrTy, resultBuffer); + auto castStdvec = builder.create(loc, i8PtrTy, stdVec); + Value loadKernName = builder.create( + loc, cudaq::opt::factory::getPointerType(kernelNameObj.getType()), + kernelNameObj.getSymName()); + auto castKernelNameObj = + builder.create(loc, i8PtrTy, loadKernName); + builder.create( + loc, std::nullopt, cudaq::runtime::launchKernelVersion2FuncName, + ArrayRef{castKernelNameObj, castStdvec, castResultBuffer}); + + // FIXME: Drop any results on the floor for now and return random data left + // on the stack. (Maintains parity with existing kernel launch.) + if (hostFunc.getFunctionType().getResults().empty()) { + builder.create(loc); + return; + } + // There can only be 1 return type in C++, so this is safe. + Value garbage = builder.create( + loc, hostFunc.getFunctionType().getResult(0)); + builder.create(loc, garbage); + } + /// A kernel function that takes a quantum type argument (also known as a pure /// device kernel) cannot be called directly from C++ (classical) code. It /// must be called via other quantum code. @@ -1422,11 +1508,18 @@ class GenerateKernelExecution if (!mangledNameMap || mangledNameMap.empty()) return; auto irBuilder = cudaq::IRBuilder::atBlockEnd(module.getBody()); - if (failed(irBuilder.loadIntrinsic(module, - cudaq::runtime::launchKernelFuncName))) { - module.emitError("could not load altLaunchKernel intrinsic."); - return; - } + if (altLaunchVersion == 1) + if (failed(irBuilder.loadIntrinsic( + module, cudaq::runtime::launchKernelFuncName))) { + module.emitError("could not load altLaunchKernel intrinsic."); + return; + } + if (altLaunchVersion == 2) + if (failed(irBuilder.loadIntrinsic( + module, cudaq::runtime::launchKernelVersion2FuncName))) { + module.emitError("could not load altLaunchKernel intrinsic."); + return; + } auto loc = module.getLoc(); auto ptrType = cudaq::cc::PointerType::get(builder.getI8Type()); @@ -1526,37 +1619,47 @@ class GenerateKernelExecution cudaq::opt::factory::toHostSideFuncType(funcTy, hasThisPtr, module); } - // Generate the function that computes the return offset. - genReturnOffsetFunction(loc, builder, funcTy, structTy, classNameStr); + func::FuncOp thunk; + func::FuncOp argsCreatorFunc; - // Generate thunk, `.thunk`, to call back to the MLIR code. - auto thunk = genThunkFunction(loc, builder, classNameStr, structTy, - funcTy, funcOp); + if (altLaunchVersion == 1) { + // Generate the function that computes the return offset. + genReturnOffsetFunction(loc, builder, funcTy, structTy, classNameStr); - // Generate the argsCreator function used by synthesis. - mlir::func::FuncOp argsCreatorFunc; - if (startingArgIdx == 0) { - argsCreatorFunc = - genKernelArgsCreatorFunction(loc, builder, funcTy, structTy, - classNameStr, hostFuncTy, hasThisPtr); - } else { - // We are operating in a very special case where we want the argsCreator - // function to ignore the first `startingArgIdx` arguments. In this - // situation, the argsCreator function will not be compatible with the - // other helper functions created in this pass, so it is assumed that - // the caller is OK with that. - auto structTy_argsCreator = - cudaq::opt::factory::buildInvokeStructType(funcTy, startingArgIdx); - argsCreatorFunc = genKernelArgsCreatorFunction( - loc, builder, funcTy, structTy_argsCreator, classNameStr, - hostFuncTy, hasThisPtr); + // Generate thunk, `.thunk`, to call back to the MLIR code. + thunk = genThunkFunction(loc, builder, classNameStr, structTy, funcTy, + funcOp); + + // Generate the argsCreator function used by synthesis. + if (startingArgIdx == 0) { + argsCreatorFunc = genKernelArgsCreatorFunction( + loc, builder, funcTy, structTy, classNameStr, hostFuncTy, + hasThisPtr); + } else { + // We are operating in a very special case where we want the + // argsCreator function to ignore the first `startingArgIdx` + // arguments. In this situation, the argsCreator function will not be + // compatible with the other helper functions created in this pass, so + // it is assumed that the caller is OK with that. + auto structTy_argsCreator = + cudaq::opt::factory::buildInvokeStructType(funcTy, + startingArgIdx); + argsCreatorFunc = genKernelArgsCreatorFunction( + loc, builder, funcTy, structTy_argsCreator, classNameStr, + hostFuncTy, hasThisPtr); + } } // Generate a new mangled function on the host side to call the // callback function. - if (hostEntryNeeded) - genNewHostEntryPoint(loc, builder, funcTy, structTy, kernelNameObj, - thunk, hostFunc, hasThisPtr); + if (hostEntryNeeded) { + if (altLaunchVersion == 1) + genNewHostEntryPoint1(loc, builder, funcTy, structTy, kernelNameObj, + thunk, hostFunc, hasThisPtr); + else + genNewHostEntryPoint2(loc, builder, funcTy, kernelNameObj, hostFunc, + hasThisPtr); + } // Generate a function at startup to register this kernel as having // been processed for kernel execution. @@ -1576,17 +1679,19 @@ class GenerateKernelExecution builder.create(loc, std::nullopt, cudaqRegisterKernelName, ValueRange{castKernRef}); - // Register the argsCreator too - auto ptrPtrType = cudaq::cc::PointerType::get(ptrType); - auto argsCreatorFuncType = FunctionType::get( - ctx, {ptrPtrType, ptrPtrType}, {builder.getI64Type()}); - Value loadArgsCreator = builder.create( - loc, argsCreatorFuncType, argsCreatorFunc.getName()); - auto castLoadArgsCreator = builder.create( - loc, ptrType, loadArgsCreator); - builder.create( - loc, std::nullopt, cudaqRegisterArgsCreator, - ValueRange{castKernRef, castLoadArgsCreator}); + if (altLaunchVersion == 1) { + // Register the argsCreator too + auto ptrPtrType = cudaq::cc::PointerType::get(ptrType); + auto argsCreatorFuncType = FunctionType::get( + ctx, {ptrPtrType, ptrPtrType}, {builder.getI64Type()}); + Value loadArgsCreator = builder.create( + loc, argsCreatorFuncType, argsCreatorFunc.getName()); + auto castLoadArgsCreator = builder.create( + loc, ptrType, loadArgsCreator); + builder.create( + loc, std::nullopt, cudaqRegisterArgsCreator, + ValueRange{castKernRef, castLoadArgsCreator}); + } // Check if this is a lambda mangled name auto demangledPtr = abi::__cxa_demangle(mangledName.str().c_str(), diff --git a/lib/Optimizer/Transforms/QuakeSynthesizer.cpp b/lib/Optimizer/Transforms/QuakeSynthesizer.cpp index dee2729a88b..adbe9df29e0 100644 --- a/lib/Optimizer/Transforms/QuakeSynthesizer.cpp +++ b/lib/Optimizer/Transforms/QuakeSynthesizer.cpp @@ -418,7 +418,7 @@ class QuakeSynthesizer if (auto attr = getModule()->getAttr(cudaq::opt::factory::targetDataLayoutAttrName)) dataLayoutSpec = cast(attr); - auto dataLayout = llvm::DataLayout(dataLayoutSpec); + llvm::DataLayout dataLayout{dataLayoutSpec}; // Convert bufferTy to llvm. llvm::LLVMContext context; LLVMTypeConverter converter(funcTy.getContext()); diff --git a/lib/Optimizer/Transforms/WiresToWiresets.cpp b/lib/Optimizer/Transforms/WiresToWiresets.cpp new file mode 100644 index 00000000000..4fd887793dd --- /dev/null +++ b/lib/Optimizer/Transforms/WiresToWiresets.cpp @@ -0,0 +1,107 @@ +/******************************************************************************* + * Copyright (c) 2022 - 2024 NVIDIA Corporation & Affiliates. * + * All rights reserved. * + * * + * This source code and the accompanying materials are made available under * + * the terms of the Apache License 2.0 which accompanies this distribution. * + ******************************************************************************/ + +#include "cudaq/Frontend/nvqpp/AttributeNames.h" +#include "cudaq/Optimizer/Dialect/CC/CCDialect.h" +#include "cudaq/Optimizer/Dialect/Quake/QuakeDialect.h" +#include "cudaq/Optimizer/Dialect/Quake/QuakeOps.h" +#include "cudaq/Optimizer/Transforms/Passes.h" +#include "mlir/IR/PatternMatch.h" +#include "mlir/IR/Threading.h" +#include "mlir/InitAllDialects.h" +#include "mlir/Rewrite/FrozenRewritePatternSet.h" +#include "mlir/Transforms/DialectConversion.h" + +using namespace mlir; + +//===----------------------------------------------------------------------===// +// Generated logic +//===----------------------------------------------------------------------===// +namespace cudaq::opt { +#define GEN_PASS_DEF_ASSIGNWIREINDICES +#define GEN_PASS_DEF_ADDWIRESET +#include "cudaq/Optimizer/Transforms/Passes.h.inc" +} // namespace cudaq::opt + +namespace { +class NullWirePat : public OpRewritePattern { +public: + unsigned *counter; + StringRef setName; + + NullWirePat(MLIRContext *context, unsigned *c, StringRef name) + : OpRewritePattern(context), counter(c), + setName(name) {} + + LogicalResult matchAndRewrite(quake::NullWireOp alloc, + PatternRewriter &rewriter) const override { + + auto index = (*counter)++; + auto wirety = quake::WireType::get(rewriter.getContext()); + rewriter.replaceOpWithNewOp(alloc, wirety, setName, + index); + + return success(); + } +}; + +class SinkOpPat : public OpRewritePattern { + using OpRewritePattern::OpRewritePattern; + +public: + LogicalResult matchAndRewrite(quake::SinkOp release, + PatternRewriter &rewriter) const override { + rewriter.replaceOpWithNewOp(release, + release.getOperand()); + + return success(); + } +}; + +//===----------------------------------------------------------------------===// +// Pass implementation +//===----------------------------------------------------------------------===// + +struct AssignWireIndicesPass + : public cudaq::opt::impl::AssignWireIndicesBase { + using AssignWireIndicesBase::AssignWireIndicesBase; + + void runOnOperation() override { + func::FuncOp func = getOperation(); + + auto *ctx = &getContext(); + RewritePatternSet patterns(ctx); + unsigned x = 0; + patterns.insert(ctx, &x, + cudaq::opt::topologyAgnosticWiresetName); + patterns.insert(ctx); + ConversionTarget target(*ctx); + target.addLegalDialect(); + target.addIllegalOp(); + target.addIllegalOp(); + if (failed(applyPartialConversion(func, target, std::move(patterns)))) { + func->emitOpError("Converting individual wires to wireset wires failed"); + signalPassFailure(); + } + } +}; + +struct AddWiresetPass + : public cudaq::opt::impl::AddWiresetBase { + using AddWiresetBase::AddWiresetBase; + + void runOnOperation() override { + ModuleOp mod = getOperation(); + OpBuilder builder(mod.getBodyRegion()); + builder.create(builder.getUnknownLoc(), + cudaq::opt::topologyAgnosticWiresetName, + INT_MAX, ElementsAttr{}); + } +}; + +} // namespace diff --git a/pyproject.toml b/pyproject.toml index 2430e75ce40..015f5ea0388 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ chemistry = [ "scipy==1.10.1", "openfermionpyscf==0.5", "h5py<3.11" ] visualization = [ "qutip<5" , "matplotlib>=3.5" ] [build-system] -requires = ["scikit-build-core", "cmake>=3.26,<3.29", "numpy>=1.24", "pytest==8.2.0"] +requires = ["scikit-build-core==0.9.10", "cmake>=3.26,<3.29", "numpy>=1.24", "pytest==8.2.0"] build-backend = "scikit_build_core.build" [tool.scikit-build] diff --git a/python/cudaq/kernel/analysis.py b/python/cudaq/kernel/analysis.py index fd285a7cfe5..633c2d730bf 100644 --- a/python/cudaq/kernel/analysis.py +++ b/python/cudaq/kernel/analysis.py @@ -91,136 +91,6 @@ def visit_If(self, node): return -class RewriteMeasures(ast.NodeTransformer): - """ - This `NodeTransformer` will analyze the AST for measurement - nodes that do not provide a `register_name=` keyword. If found - it will replace that node with one that sets the register_name to - the variable name in the assignment. - """ - - def visit_FunctionDef(self, node): - node.decorator_list.clear() - self.generic_visit(node) - return node - - def visit_Assign(self, node): - # We only care about nodes with a Name target (`mz`,`my`,`mx`) - if len(node.targets) == 1 and isinstance(node.targets[0], ast.Name): - # The value has to be a Call node - if isinstance(node.value, ast.Call): - # The function we are calling should be Named - if not isinstance(node.value.func, ast.Name): - return node - - # Make sure we are only seeing measurements - if node.value.func.id not in ['mx', 'my', 'mz']: - return node - - # If we already have a register_name keyword - # then we don't have to do anything - if len(node.value.keywords): - for keyword in node.value.keywords: - if keyword.arg == 'register_name': - return node - - # If here, we have a measurement with no register name - # We'll add one here - newConstant = ast.Constant(value=node.targets[0].id) - newCall = ast.Call(func=node.value.func, - value=node.targets[0].id, - args=node.value.args, - keywords=[ - ast.keyword(arg='register_name', - value=newConstant) - ]) - ast.copy_location(newCall, node.value) - ast.copy_location(newConstant, node.value) - ast.fix_missing_locations(newCall) - node.value = newCall - return node - - return node - - -class MatrixToRowMajorList(ast.NodeTransformer): - """ - Convert 2D `np.array([[row0],[row1], ... ])` to a row-major - single list, `np.array([row0 row1 row2 ...])` in order to make it - easier to convert the data to a `stdvec` in our MLIR model. - """ - - def visit_Call(self, node): - - self.generic_visit(node) - - if not isinstance(node.func, ast.Attribute): - return node - - # is an attribute - if not node.func.value.id in ['numpy', 'np']: - return node - - if not node.func.attr == 'array': - return node - - # this is an NumPy array - args = node.args - - if len(args) != 1 and not isinstance(args[0], ast.List): - return node - - # [[this], [this], [this], [this], ...] - subLists = args[0].elts - # convert to [this, this, this, this, ...] - newElts = [] - for l in subLists: - for e in l.elts: - newElts.append(e) - - newList = ast.List(elts=newElts, ctx=ast.Load()) - newCall = ast.Call(func=node.func, args=[newList], keywords=[]) - ast.copy_location(newCall, node) - ast.fix_missing_locations(newCall) - return newCall - - -class LambdaOrLambdaAssignToFunctionDef(ast.NodeTransformer): - """ - Convert a parameterized lambda returning a NumPy array to a standard - Python function definition. - """ - - def visit_Assign(self, node): - if isinstance(node.value, ast.Call): - if isinstance(node.value.args[0], ast.Lambda): - n = ast.FunctionDef( - name=node.targets[0].id, - args=node.value.args[0].args, - body=[ast.Return(value=node.value.args[0].body)], - decorator_list=[]) - ast.copy_location(n, node.value) - ast.fix_missing_locations(n) - for a in n.args.args: - a.annotation = ast.Name(id='float') - return n - - -class CheckAndCorrectFunctionName(ast.NodeTransformer): - """ - It may be the case that the custom unitary has a specified function name - that is not equal to the desired operation name. Fix that here. - """ - - def __init__(self, desiredName): - self.desiredName = desiredName - - def visit_FunctionDef(self, node): - if node.name != self.desiredName: - node.name = self.desiredName - return node - - class FindDepKernelsVisitor(ast.NodeVisitor): def __init__(self, ctx): diff --git a/python/cudaq/kernel/ast_bridge.py b/python/cudaq/kernel/ast_bridge.py index 9461a8d5f8d..33d35a6b2f8 100644 --- a/python/cudaq/kernel/ast_bridge.py +++ b/python/cudaq/kernel/ast_bridge.py @@ -13,7 +13,7 @@ from collections import deque import numpy as np from .analysis import FindDepKernelsVisitor -from .utils import globalAstRegistry, globalKernelRegistry, globalRegisteredOperations, nvqppPrefix, mlirTypeFromAnnotation, mlirTypeFromPyType, Color, mlirTypeToPyType +from .utils import globalAstRegistry, globalKernelRegistry, globalRegisteredOperations, nvqppPrefix, mlirTypeFromAnnotation, mlirTypeFromPyType, Color, mlirTypeToPyType, globalRegisteredTypes from ..mlir.ir import * from ..mlir.passmanager import * from ..mlir.dialects import quake, cc @@ -520,6 +520,25 @@ def __createStdvecWithKnownValues(self, size, listElementValues): return cc.StdvecInitOp(cc.StdvecType.get(self.ctx, vecTy), alloca, arrSize).result + def getStructMemberIdx(self, memberName, structTy): + """ + For the given struct type and member variable name, return + the index of the variable in the struct and the specific + MLIR type for the variable. + """ + structName = cc.StructType.getName(structTy) + structIdx = None + _, userType = globalRegisteredTypes[structName] + for i, (k, _) in enumerate(userType.items()): + if k == memberName: + structIdx = i + break + if structIdx == None: + self.emitFatalError( + f'Invalid struct member: {structName}.{memberName} (members={[k for k,_ in userType.items()]})' + ) + return structIdx, mlirTypeFromPyType(userType[memberName], self.ctx) + # Create a new vector with source elements converted to the target element type if needed. def __copyVectorAndCastElements(self, source, targetEleType): if not cc.PointerType.isinstance(source.type): @@ -646,6 +665,19 @@ def convertArithmeticToSuperiorType(self, values, type): return retValues + def isQuantumStructType(self, structTy): + """ + Return True if the given struct type has one or more quantum member variables. + """ + if not cc.StructType.isinstance(structTy): + self.emitFatalError( + f'isQuantumStructType called on type that is not a struct ({structTy})' + ) + + return True in [ + self.isQuantumType(t) for t in cc.StructType.getTypes(structTy) + ] + def mlirTypeFromAnnotation(self, annotation): """ Return the MLIR Type corresponding to the given kernel function argument type annotation. @@ -811,8 +843,12 @@ def needsStackSlot(self, type): function. """ # FIXME add more as we need them + if cc.StructType.isinstance(type) and self.isQuantumStructType(type): + # If we have a quantum struct, we don't want to add a stack slot + return False return ComplexType.isinstance(type) or F64Type.isinstance( - type) or F32Type.isinstance(type) or IntegerType.isinstance(type) + type) or F32Type.isinstance(type) or IntegerType.isinstance( + type) or cc.StructType.isinstance(type) def generic_visit(self, node): for field, value in reversed(list(ast.iter_fields(node))): @@ -1117,7 +1153,11 @@ def visit_Assign(self, node): cc.StoreOp(value, self.symbolTable[varNames[i]]) elif cc.PointerType.isinstance(value.type): self.symbolTable[varNames[i]] = value - + elif cc.StructType.isinstance(value.type) and isinstance( + value.owner.opview, cc.InsertValueOp): + # If we have a new struct from `cc.undef` and `cc.insert_value`, we don't + # want to allocate new memory. + self.symbolTable[varNames[i]] = value else: # We should allocate and store alloca = cc.AllocaOp(cc.PointerType.get(self.ctx, value.type), @@ -1139,6 +1179,34 @@ def visit_Attribute(self, node): if isinstance(node.value, ast.Name) and node.value.id in self.symbolTable: value = self.symbolTable[node.value.id] + if cc.StructType.isinstance( + value.type) and self.isQuantumStructType(value.type): + # Here we have a quantum struct, need to use extract value instead + # of load from compute pointer. + structIdx, memberTy = self.getStructMemberIdx( + node.attr, value.type) + self.pushValue( + cc.ExtractValueOp( + memberTy, value, [], + DenseI32ArrayAttr.get([structIdx], + context=self.ctx)).result) + return + + if cc.PointerType.isinstance(value.type): + eleType = cc.PointerType.getElementType(value.type) + if cc.StructType.isinstance(eleType): + # Handle the case where we have a struct member extraction, memory semantics + structIdx, memberTy = self.getStructMemberIdx( + node.attr, eleType) + eleAddr = cc.ComputePtrOp( + cc.PointerType.get(self.ctx, memberTy), value, [], + DenseI32ArrayAttr.get([structIdx], + context=self.ctx)).result + # We'll always have a pointer, and we always want to load it. + eleAddr = cc.LoadOp(eleAddr).result + self.pushValue(eleAddr) + return + if node.attr == 'append': type = value.type if cc.PointerType.isinstance(type): @@ -1174,20 +1242,26 @@ def visit_Attribute(self, node): self.pushValue(complex.ImOp(value).result) return - if isinstance(node.value, ast.Name) and node.value.id in [ - 'np', 'numpy', 'math' - ] and node.attr == 'pi': - self.pushValue(self.getConstantFloat(np.pi)) - return - if isinstance(node.value, - ast.Name) and node.value.id in ['np', 'numpy']: + ast.Name) and node.value.id in ['np', 'numpy', 'math']: if node.attr == 'complex64': self.pushValue(self.getComplexType(width=32)) return if node.attr == 'complex128': self.pushValue(self.getComplexType(width=64)) return + if node.attr == 'pi': + self.pushValue(self.getConstantFloat(np.pi)) + return + if node.attr == 'e': + self.pushValue(self.getConstantFloat(np.e)) + return + if node.attr == 'euler_gamma': + self.pushValue(self.getConstantFloat(np.euler_gamma)) + return + raise RuntimeError( + "math expression {}.{} was not understood".format( + node.value.id, node.attr)) def visit_Call(self, node): """ @@ -1820,6 +1894,47 @@ def bodyBuilder(iterVal): self.__insertDbgStmt(self.popValue(), node.func.id) return + elif node.func.id in globalRegisteredTypes: + # Handle User-Custom Struct Constructor + cls, annotations = globalRegisteredTypes[node.func.id] + # Alloca the struct + structTys = [ + mlirTypeFromPyType(v, self.ctx) + for _, v in annotations.items() + ] + structTy = cc.StructType.getNamed(self.ctx, node.func.id, + structTys) + nArgs = len(self.valueStack) + ctorArgs = [self.popValue() for _ in range(nArgs)] + ctorArgs.reverse() + + if self.isQuantumStructType(structTy): + # If we have a struct with quantum types, we do not + # want to allocate struct memory and load / store pointers + # to quantum memory, so we'll instead use value semantics + # with InsertValue + undefOp = cc.UndefOp(structTy).result + for i, arg in enumerate(ctorArgs): + undefOp = cc.InsertValueOp( + structTy, undefOp, arg, + DenseI64ArrayAttr.get([i], context=self.ctx)).result + + self.pushValue(undefOp) + return + + stackSlot = cc.AllocaOp(cc.PointerType.get(self.ctx, structTy), + TypeAttr.get(structTy)).result + + # loop over each type and `compute_ptr` / store + + for i, ty in enumerate(structTys): + eleAddr = cc.ComputePtrOp( + cc.PointerType.get(self.ctx, ty), stackSlot, [], + DenseI32ArrayAttr.get([i], context=self.ctx)).result + cc.StoreOp(ctorArgs[i], eleAddr) + self.pushValue(stackSlot) + return + else: self.emitFatalError( "unhandled function call - {}, known kernels are {}".format( diff --git a/python/cudaq/kernel/kernel_decorator.py b/python/cudaq/kernel/kernel_decorator.py index 7addb00b99f..91c11c0fb6f 100644 --- a/python/cudaq/kernel/kernel_decorator.py +++ b/python/cudaq/kernel/kernel_decorator.py @@ -14,8 +14,8 @@ from ..mlir.passmanager import * from ..mlir.dialects import quake, cc from .ast_bridge import compile_to_mlir, PyASTBridge -from .utils import mlirTypeFromPyType, nvqppPrefix, mlirTypeToPyType, globalAstRegistry, emitFatalError, emitErrorIfInvalidPauli -from .analysis import MidCircuitMeasurementAnalyzer, RewriteMeasures, HasReturnNodeVisitor +from .utils import mlirTypeFromPyType, nvqppPrefix, mlirTypeToPyType, globalAstRegistry, emitFatalError, emitErrorIfInvalidPauli, globalRegisteredTypes +from .analysis import MidCircuitMeasurementAnalyzer, HasReturnNodeVisitor from ..mlir._mlir_libs._quakeDialects import cudaq_runtime from .captured_data import CapturedDataStorage @@ -88,6 +88,12 @@ def __init__(self, ['f_locals'].items() } + # Register any external class types that may be used + # in the kernel definition + for name, var in self.globalScopedVars.items(): + if isinstance(var, type): + globalRegisteredTypes[name] = (var, var.__annotations__) + # Once the kernel is compiled to MLIR, we # want to know what capture variables, if any, were # used in the kernel. We need to track these. diff --git a/python/cudaq/kernel/utils.py b/python/cudaq/kernel/utils.py index a26d56d055b..99d9705ea1a 100644 --- a/python/cudaq/kernel/utils.py +++ b/python/cudaq/kernel/utils.py @@ -14,9 +14,12 @@ import numpy as np from typing import Callable, List import ast, sys, traceback +import re +from typing import get_origin State = cudaq_runtime.State qvector = cudaq_runtime.qvector +qview = cudaq_runtime.qview qubit = cudaq_runtime.qubit pauli_word = cudaq_runtime.pauli_word qreg = qvector @@ -36,6 +39,9 @@ # Keep a global registry of all registered custom operations. globalRegisteredOperations = {} +# Keep a global registry of any custom data types +globalRegisteredTypes = {} + class Color: YELLOW = '\033[93m' @@ -110,7 +116,7 @@ def emitFatalErrorOverride(msg): localEmitFatalError( 'cudaq.kernel functions must have argument type annotations.') - if hasattr(annotation, 'attr'): + if hasattr(annotation, 'attr') and hasattr(annotation.value, 'id'): if annotation.value.id == 'cudaq': if annotation.attr in ['qview', 'qvector']: return quake.VeqType.get(ctx) @@ -200,6 +206,17 @@ def emitFatalErrorOverride(msg): if id == 'complex': return ComplexType.get(F64Type.get()) + if isinstance(annotation, ast.Attribute): + # in this case we might have `mod1.mod2...mod3.UserType` + # slurp up the path to the type + id = annotation.attr + + # One final check to see if this is a custom data type. + if id in globalRegisteredTypes: + _, memberTys = globalRegisteredTypes[id] + structTys = [mlirTypeFromPyType(v, ctx) for _, v in memberTys.items()] + return cc.StructType.getNamed(ctx, id, structTys) + localEmitFatalError( f"{ast.unparse(annotation) if hasattr(ast, 'unparse') else annotation} is not a supported type." ) @@ -225,6 +242,25 @@ def mlirTypeFromPyType(argType, ctx, **kwargs): if argType == State: return cc.PointerType.get(ctx, cc.StateType.get(ctx)) + if get_origin(argType) == list: + result = re.search(r'ist\[(.*)\]', str(argType)) + eleTyName = result.group(1) + argType = list + if eleTyName == 'int': + kwargs['argInstance'] = [int(0)] + elif eleTyName == 'float': + kwargs['argInstance'] = [float(0.0)] + elif eleTyName == 'bool': + kwargs['argInstance'] = [bool(False)] + elif eleTyName == 'complex': + kwargs['argInstance'] = [0j] + elif eleTyName == 'pauli_word': + kwargs['argInstance'] = [pauli_word('')] + elif eleTyName == 'numpy.complex128': + kwargs['argInstance'] = [np.complex128(0.0)] + elif eleTyName == 'numpy.complex64': + kwargs['argInstance'] = [np.complex64(0.0)] + if argType in [list, np.ndarray, List]: if 'argInstance' not in kwargs: return cc.StdvecType.get(ctx, mlirTypeFromPyType(float, ctx)) @@ -273,7 +309,7 @@ def mlirTypeFromPyType(argType, ctx, **kwargs): emitFatalError(f'Invalid list element type ({argType})') - if argType == qvector or argType == qreg: + if argType == qvector or argType == qreg or argType == qview: return quake.VeqType.get(ctx) if argType == qubit: return quake.RefType.get(ctx) @@ -284,6 +320,18 @@ def mlirTypeFromPyType(argType, ctx, **kwargs): argInstance = kwargs['argInstance'] if isinstance(argInstance, Callable): return cc.CallableType.get(ctx, argInstance.argTypes) + else: + if argType == list[int]: + return cc.StdvecType.get(ctx, mlirTypeFromPyType(int, ctx)) + if argType == list[float]: + return cc.StdvecType.get(ctx, mlirTypeFromPyType(float, ctx)) + + for name, (customTys, memberTys) in globalRegisteredTypes.items(): + if argType == customTys: + structTys = [ + mlirTypeFromPyType(v, ctx) for _, v in memberTys.items() + ] + return cc.StructType.getNamed(ctx, name, structTys) emitFatalError( f"Can not handle conversion of python type {argType} to MLIR type.") diff --git a/python/runtime/cudaq/platform/JITExecutionCache.cpp b/python/runtime/cudaq/platform/JITExecutionCache.cpp index 74b73a02cab..078c7111c74 100644 --- a/python/runtime/cudaq/platform/JITExecutionCache.cpp +++ b/python/runtime/cudaq/platform/JITExecutionCache.cpp @@ -11,10 +11,12 @@ using namespace mlir; namespace cudaq { +static constexpr int NUM_JIT_CACHE_ITEMS_TO_RETAIN = 100; + JITExecutionCache::~JITExecutionCache() { std::scoped_lock lock(mutex); for (auto &[k, v] : cacheMap) - delete v; + delete v.execEngine; cacheMap.clear(); } bool JITExecutionCache::hasJITEngine(std::size_t hashkey) { @@ -24,10 +26,29 @@ bool JITExecutionCache::hasJITEngine(std::size_t hashkey) { void JITExecutionCache::cache(std::size_t hash, ExecutionEngine *jit) { std::scoped_lock lock(mutex); - cacheMap.insert({hash, jit}); + + lruList.push_back(hash); + + // If adding a new item would exceed our cache limit, then remove the least + // recently used item (at the head of the list). + if (cacheMap.size() >= NUM_JIT_CACHE_ITEMS_TO_RETAIN) { + auto hashToRemove = lruList.begin(); + auto it = cacheMap.find(*hashToRemove); + delete it->second.execEngine; + lruList.erase(hashToRemove); + cacheMap.erase(it); + } + + cacheMap.insert({hash, {jit, std::prev(lruList.end())}}); } ExecutionEngine *JITExecutionCache::getJITEngine(std::size_t hash) { std::scoped_lock lock(mutex); - return cacheMap.at(hash); + auto &item = cacheMap.at(hash); + + // Move item.lruListIt to the end of the list to indicate that it is being + // used right now. + lruList.splice(lruList.end(), lruList, item.lruListIt); + + return item.execEngine; } } // namespace cudaq diff --git a/python/runtime/cudaq/platform/JITExecutionCache.h b/python/runtime/cudaq/platform/JITExecutionCache.h index e57ce370963..7589179f6d5 100644 --- a/python/runtime/cudaq/platform/JITExecutionCache.h +++ b/python/runtime/cudaq/platform/JITExecutionCache.h @@ -20,7 +20,18 @@ namespace cudaq { /// for the string representation of the original MLIR ModuleOp. class JITExecutionCache { protected: - std::unordered_map cacheMap; + // Implement a Least Recently Used cache based on the JIT hash. + std::list lruList; + + // A given JIT hash has an associated MapItemType, which contains pointers to + // the execution engine and to the LRU iterator that is used to track which + // engine is the least recently used. + struct MapItemType { + ExecutionEngine *execEngine = nullptr; + std::list::iterator lruListIt; + }; + std::unordered_map cacheMap; + std::mutex mutex; public: diff --git a/python/runtime/cudaq/platform/py_alt_launch_kernel.cpp b/python/runtime/cudaq/platform/py_alt_launch_kernel.cpp index 3878ec77180..31a983ae12a 100644 --- a/python/runtime/cudaq/platform/py_alt_launch_kernel.cpp +++ b/python/runtime/cudaq/platform/py_alt_launch_kernel.cpp @@ -71,8 +71,6 @@ jitAndCreateArgs(const std::string &name, MlirModule module, std::size_t startingArgIdx = 0) { ScopedTraceWithContext(cudaq::TIMING_JIT, "jitAndCreateArgs", name); auto mod = unwrap(module); - auto cloned = mod.clone(); - auto context = cloned.getContext(); // Do not cache the JIT if we are running with startingArgIdx > 0 because a) // we won't be executing right after JIT-ing, and b) we might get called later @@ -94,6 +92,8 @@ jitAndCreateArgs(const std::string &name, MlirModule module, ScopedTraceWithContext(cudaq::TIMING_JIT, "jitAndCreateArgs - execute passes", name); + auto cloned = mod.clone(); + auto context = cloned.getContext(); PassManager pm(context); pm.addNestedPass( cudaq::opt::createPySynthCallableBlockArgs(names)); diff --git a/python/runtime/mlir/py_register_dialects.cpp b/python/runtime/mlir/py_register_dialects.cpp index 8eca13638e2..2037239522e 100644 --- a/python/runtime/mlir/py_register_dialects.cpp +++ b/python/runtime/mlir/py_register_dialects.cpp @@ -181,16 +181,36 @@ void registerCCDialectAndTypes(py::module &m) { return wrap(cudaq::cc::StructType::get(unwrap(ctx), inTys)); }) - .def_classmethod("getTypes", [](py::object cls, MlirType structTy) { + .def_classmethod( + "getNamed", + [](py::object cls, MlirContext ctx, const std::string &name, + py::list aggregateTypes) { + SmallVector inTys; + for (auto &t : aggregateTypes) + inTys.push_back(unwrap(t.cast())); + + return wrap(cudaq::cc::StructType::get(unwrap(ctx), name, inTys)); + }) + .def_classmethod( + "getTypes", + [](py::object cls, MlirType structTy) { + auto ty = dyn_cast(unwrap(structTy)); + if (!ty) + throw std::runtime_error( + "invalid type passed to StructType.getTypes(), must be a " + "cc.struct"); + std::vector ret; + for (auto &t : ty.getMembers()) + ret.push_back(wrap(t)); + return ret; + }) + .def_classmethod("getName", [](py::object cls, MlirType structTy) { auto ty = dyn_cast(unwrap(structTy)); if (!ty) throw std::runtime_error( - "invalid type passed to StructType.getTypes(), must be a " + "invalid type passed to StructType.getName(), must be a " "cc.struct"); - std::vector ret; - for (auto &t : ty.getMembers()) - ret.push_back(wrap(t)); - return ret; + return ty.getName().getValue().str(); }); mlir_type_subclass( diff --git a/python/tests/kernel/mock/hello/__init__.py b/python/tests/kernel/mock/hello/__init__.py new file mode 100644 index 00000000000..4047c8a4acc --- /dev/null +++ b/python/tests/kernel/mock/hello/__init__.py @@ -0,0 +1,8 @@ +# ============================================================================ # +# Copyright (c) 2022 - 2024 NVIDIA Corporation & Affiliates. # +# All rights reserved. # +# # +# This source code and the accompanying materials are made available under # +# the terms of the Apache License 2.0 which accompanies this distribution. # +# ============================================================================ # +from .dtype import TestClass \ No newline at end of file diff --git a/python/tests/kernel/mock/hello/dtype.py b/python/tests/kernel/mock/hello/dtype.py new file mode 100644 index 00000000000..ddfdc0531ad --- /dev/null +++ b/python/tests/kernel/mock/hello/dtype.py @@ -0,0 +1,14 @@ +# ============================================================================ # +# Copyright (c) 2022 - 2024 NVIDIA Corporation & Affiliates. # +# All rights reserved. # +# # +# This source code and the accompanying materials are made available under # +# the terms of the Apache License 2.0 which accompanies this distribution. # +# ============================================================================ # +import cudaq +from dataclasses import dataclass + +@dataclass +class TestClass: + i : int + d : float diff --git a/python/tests/kernel/test_kernel_features.py b/python/tests/kernel/test_kernel_features.py index 4147a8af207..52624cf6ed2 100644 --- a/python/tests/kernel/test_kernel_features.py +++ b/python/tests/kernel/test_kernel_features.py @@ -1623,6 +1623,108 @@ def kd(): assert len(counts) == 2 and '0' in counts and '1' in counts +def test_custom_classical_kernel_type(): + from dataclasses import dataclass + + @dataclass + class CustomIntAndFloatType: + integer: int + floatingPoint: float + + instance = CustomIntAndFloatType(123, 123.123) + assert instance.integer == 123 and instance.floatingPoint == 123.123 + + @cudaq.kernel + def test(input: CustomIntAndFloatType): + qubits = cudaq.qvector(input.integer) + ry(input.floatingPoint, qubits[0]) + rx(input.floatingPoint * 2, qubits[0]) + x.ctrl(qubits[0], qubits[1]) + + instance = CustomIntAndFloatType(2, np.pi / 2.) + counts = cudaq.sample(test, instance) + counts.dump() + assert len(counts) == 2 and '00' in counts and '11' in counts + + @dataclass + class CustomIntAndListFloat: + integer: int + array: List[float] + + @cudaq.kernel + def test(input: CustomIntAndListFloat): + qubits = cudaq.qvector(input.integer) + ry(input.array[0], qubits[0]) + rx(input.array[1], qubits[0]) + x.ctrl(qubits[0], qubits[1]) + + print(test) + instance = CustomIntAndListFloat(2, [np.pi / 2., np.pi]) + counts = cudaq.sample(test, instance) + counts.dump() + assert len(counts) == 2 and '00' in counts and '11' in counts + + # Test that the class can be in a library + # and the paths all work out + from mock.hello import TestClass + + @cudaq.kernel + def test(input: TestClass): + q = cudaq.qvector(input.i) + + instance = TestClass(2, 2.2) + state = cudaq.get_state(test, instance) + state.dump() + + assert len(state) == 2**instance.i + + # Test invalid struct member + @cudaq.kernel + def test(input: TestClass): + local = input.helloBadMember + + with pytest.raises(RuntimeError) as e: + test.compile() + + +def test_custom_quantum_type(): + from dataclasses import dataclass + + @dataclass + class patch: + data: cudaq.qview + ancx: cudaq.qview + ancz: cudaq.qview + + @cudaq.kernel + def logicalH(p: patch): + h(p.data) + + # print(logicalH) + @cudaq.kernel + def logicalX(p: patch): + x(p.ancx) + + @cudaq.kernel + def logicalZ(p: patch): + z(p.ancz) + + @cudaq.kernel # (verbose=True) + def run(): + q = cudaq.qvector(2) + r = cudaq.qvector(2) + s = cudaq.qvector(2) + p = patch(q, r, s) + + logicalH(p) + logicalX(p) + logicalZ(p) + + # Test here is that it compiles and runs successfully + print(run) + run() + + @skipIfPythonLessThan39 def test_issue_9(): @@ -1697,6 +1799,38 @@ def kernel(theta: float): cudaq.sample(kernel, theta).dump() +def test_numpy_functions(): + + @cudaq.kernel + def kernel(): + q = cudaq.qvector(3) + h(q) + rx(np.pi, q[0]) + ry(np.e, q[1]) + rz(np.euler_gamma, q[2]) + + # test here is that this compiles and runs + cudaq.sample(kernel).dump() + + @cudaq.kernel + def valid_unsupported(): + q = cudaq.qubit() + h(q) + r1(np.inf, q) + + with pytest.raises(RuntimeError): + cudaq.sample(valid_unsupported) + + @cudaq.kernel + def invalid_unsupported(): + q = cudaq.qubit() + h(q) + r1(np.foo, q) + + with pytest.raises(RuntimeError): + cudaq.sample(invalid_unsupported) + + # leave for gdb debugging if __name__ == "__main__": loc = os.path.abspath(__file__) diff --git a/python/tests/mlir/quantum_type.py b/python/tests/mlir/quantum_type.py new file mode 100644 index 00000000000..b204c10f772 --- /dev/null +++ b/python/tests/mlir/quantum_type.py @@ -0,0 +1,87 @@ +# ============================================================================ # +# Copyright (c) 2022 - 2024 NVIDIA Corporation & Affiliates. # +# All rights reserved. # +# # +# This source code and the accompanying materials are made available under # +# the terms of the Apache License 2.0 which accompanies this distribution. # +# ============================================================================ # + +# RUN: PYTHONPATH=../../ pytest -rP %s | FileCheck %s + +import pytest + +import cudaq + +def test_custom_quantum_type(): + from dataclasses import dataclass + @dataclass + class patch: + data : cudaq.qview + ancx : cudaq.qview + ancz : cudaq.qview + + @cudaq.kernel + def logicalH(p : patch): + h(p.data) + print(logicalH) + + @cudaq.kernel + def logicalX(p : patch): + x(p.ancx) + + @cudaq.kernel + def logicalZ(p : patch): + z(p.ancz) + + @cudaq.kernel + def run(): + q = cudaq.qvector(2) + r = cudaq.qvector(2) + s = cudaq.qvector(2) + p = patch(q, r, s) + + logicalH(p) + logicalX(p) + logicalZ(p) + + # Test here is that it compiles and runs successfully + print(run) + +# CHECK-LABEL: func.func @__nvqpp__mlirgen__logicalH( +# CHECK-SAME: %[[VAL_0:.*]]: !cc.struct<"patch" {!quake.veq, !quake.veq, !quake.veq}>) attributes {"cudaq-entrypoint"} { +# CHECK: %[[VAL_1:.*]] = arith.constant 1 : i64 +# CHECK: %[[VAL_2:.*]] = arith.constant 0 : i64 +# CHECK: %[[VAL_3:.*]] = cc.extract_value %[[VAL_0]][0] : (!cc.struct<"patch" {!quake.veq, !quake.veq, !quake.veq}>) -> !quake.veq +# CHECK: %[[VAL_4:.*]] = quake.veq_size %[[VAL_3]] : (!quake.veq) -> i64 +# CHECK: %[[VAL_5:.*]] = cc.loop while ((%[[VAL_6:.*]] = %[[VAL_2]]) -> (i64)) { +# CHECK: %[[VAL_7:.*]] = arith.cmpi slt, %[[VAL_6]], %[[VAL_4]] : i64 +# CHECK: cc.condition %[[VAL_7]](%[[VAL_6]] : i64) +# CHECK: } do { +# CHECK: ^bb0(%[[VAL_8:.*]]: i64): +# CHECK: %[[VAL_9:.*]] = quake.extract_ref %[[VAL_3]]{{\[}}%[[VAL_8]]] : (!quake.veq, i64) -> !quake.ref +# CHECK: quake.h %[[VAL_9]] : (!quake.ref) -> () +# CHECK: cc.continue %[[VAL_8]] : i64 +# CHECK: } step { +# CHECK: ^bb0(%[[VAL_10:.*]]: i64): +# CHECK: %[[VAL_11:.*]] = arith.addi %[[VAL_10]], %[[VAL_1]] : i64 +# CHECK: cc.continue %[[VAL_11]] : i64 +# CHECK: } {invariant} +# CHECK: return +# CHECK: } + +# CHECK-LABEL: func.func @__nvqpp__mlirgen__run() attributes {"cudaq-entrypoint"} { +# CHECK: %[[VAL_0:.*]] = quake.alloca !quake.veq<2> +# CHECK: %[[VAL_1:.*]] = quake.relax_size %[[VAL_0]] : (!quake.veq<2>) -> !quake.veq +# CHECK: %[[VAL_2:.*]] = quake.alloca !quake.veq<2> +# CHECK: %[[VAL_3:.*]] = quake.relax_size %[[VAL_2]] : (!quake.veq<2>) -> !quake.veq +# CHECK: %[[VAL_4:.*]] = quake.alloca !quake.veq<2> +# CHECK: %[[VAL_5:.*]] = quake.relax_size %[[VAL_4]] : (!quake.veq<2>) -> !quake.veq +# CHECK: %[[VAL_6:.*]] = cc.undef !cc.struct<"patch" {!quake.veq, !quake.veq, !quake.veq}> +# CHECK: %[[VAL_7:.*]] = cc.insert_value %[[VAL_1]], %[[VAL_6]][0] : (!cc.struct<"patch" {!quake.veq, !quake.veq, !quake.veq}>, !quake.veq) -> !cc.struct<"patch" {!quake.veq, !quake.veq, !quake.veq}> +# CHECK: %[[VAL_8:.*]] = cc.insert_value %[[VAL_3]], %[[VAL_7]][1] : (!cc.struct<"patch" {!quake.veq, !quake.veq, !quake.veq}>, !quake.veq) -> !cc.struct<"patch" {!quake.veq, !quake.veq, !quake.veq}> +# CHECK: %[[VAL_9:.*]] = cc.insert_value %[[VAL_5]], %[[VAL_8]][2] : (!cc.struct<"patch" {!quake.veq, !quake.veq, !quake.veq}>, !quake.veq) -> !cc.struct<"patch" {!quake.veq, !quake.veq, !quake.veq}> +# CHECK: call @__nvqpp__mlirgen__logicalH(%[[VAL_9]]) : (!cc.struct<"patch" {!quake.veq, !quake.veq, !quake.veq}>) -> () +# CHECK: call @__nvqpp__mlirgen__logicalX(%[[VAL_9]]) : (!cc.struct<"patch" {!quake.veq, !quake.veq, !quake.veq}>) -> () +# CHECK: call @__nvqpp__mlirgen__logicalZ(%[[VAL_9]]) : (!cc.struct<"patch" {!quake.veq, !quake.veq, !quake.veq}>) -> () +# CHECK: return +# CHECK: } \ No newline at end of file diff --git a/python/tests/visualization/test_draw.py b/python/tests/visualization/test_draw.py index 976ed4eaee1..dbc35b9296e 100644 --- a/python/tests/visualization/test_draw.py +++ b/python/tests/visualization/test_draw.py @@ -24,13 +24,9 @@ def test_draw(): @cudaq.kernel def bar(qvec: cudaq.qview): - # FIXME https://github.com/NVIDIA/cuda-quantum/issues/1734 - # rx(np.e, qvec[0]) - rx(2.71828182845904523536028, qvec[0]) + rx(np.e, qvec[0]) ry(np.pi, qvec[1]) - # FIXME https://github.com/NVIDIA/cuda-quantum/issues/1734 - # cudaq.adjoint(rz, np.pi, qvec[2]) - rz(-np.pi, qvec[2]) + rz.adj(np.pi, qvec[2]) @cudaq.kernel def zaz(qub: cudaq.qubit): @@ -109,6 +105,33 @@ def kernel(): assert expected_str == produced_string +def test_draw_hw_target(): + + @cudaq.kernel + def hw_kernel(): + q = cudaq.qvector(3) + h(q[0]) + x.ctrl(q[0], q[1], q[2]) + + cudaq.set_target('ionq', emulate=True) + # fmt: on + expected_str = R""" + ╭───╮ ╭───╮╭─────╮╭───╮╭───╮ +q0 : ┤ h ├──●─────────────────────●──────────────┤ x ├┤ tdg ├┤ x ├┤ t ├ + ╰───╯ │ │ ╰─┬─╯╰─────╯╰─┬─╯├───┤ +q1 : ───────┼───────────●─────────┼───────────●────●───────────●──┤ t ├ + ╭───╮╭─┴─╮╭─────╮╭─┴─╮╭───╮╭─┴─╮╭─────╮╭─┴─╮╭───╮ ╭───╮ ╰───╯ +q2 : ┤ h ├┤ x ├┤ tdg ├┤ x ├┤ t ├┤ x ├┤ tdg ├┤ x ├┤ t ├─┤ h ├─────────── + ╰───╯╰───╯╰─────╯╰───╯╰───╯╰───╯╰─────╯╰───╯╰───╯ ╰───╯ +""" + # fmt: off + # Extra newline added for convenience to match the cleanly formatted expected_str above. + produced_string = '\n' + cudaq.draw(hw_kernel) + print(produced_string) + assert expected_str == produced_string + cudaq.reset_target() + + # leave for gdb debugging if __name__ == "__main__": loc = os.path.abspath(__file__) diff --git a/python/utils/OpaqueArguments.h b/python/utils/OpaqueArguments.h index 661f5efb5d1..71dd45002be 100644 --- a/python/utils/OpaqueArguments.h +++ b/python/utils/OpaqueArguments.h @@ -10,12 +10,16 @@ #include "PyTypes.h" #include "common/FmtCore.h" +#include "cudaq/Optimizer/Builder/Runtime.h" +#include "cudaq/Optimizer/CodeGen/QIRFunctionNames.h" #include "cudaq/Optimizer/Dialect/CC/CCTypes.h" #include "cudaq/builder/kernel_builder.h" #include "cudaq/qis/pauli_word.h" #include "llvm/ADT/TypeSwitch.h" #include "mlir/CAPI/IR.h" +#include "mlir/Conversion/LLVMCommon/TypeConverter.h" #include "mlir/Dialect/Func/IR/FuncOps.h" +#include "mlir/Target/LLVMIR/TypeToLLVM.h" #include #include #include @@ -185,6 +189,87 @@ inline std::string mlirTypeToString(mlir::Type ty) { return msg; } +/// @brief Return the size and member variable offsets for the input struct. +inline std::pair> +getTargetLayout(func::FuncOp func, cudaq::cc::StructType structTy) { + auto mod = func->getParentOfType(); + StringRef dataLayoutSpec = ""; + if (auto attr = mod->getAttr(cudaq::opt::factory::targetDataLayoutAttrName)) + dataLayoutSpec = cast(attr); + auto dataLayout = llvm::DataLayout(dataLayoutSpec); + // Convert bufferTy to llvm. + llvm::LLVMContext context; + LLVMTypeConverter converter(func.getContext()); + cudaq::opt::initializeTypeConversions(converter); + auto llvmDialectTy = converter.convertType(structTy); + LLVM::TypeToLLVMIRTranslator translator(context); + auto *llvmStructTy = + cast(translator.translateType(llvmDialectTy)); + auto *layout = dataLayout.getStructLayout(llvmStructTy); + auto strSize = layout->getSizeInBytes(); + std::vector fieldOffsets; + for (std::size_t i = 0, I = structTy.getMembers().size(); i != I; ++i) + fieldOffsets.emplace_back(layout->getElementOffset(i)); + return {strSize, fieldOffsets}; +} + +/// @brief For the current struct member variable type, insert the +/// value into the dynamically-constructed struct. +inline void handleStructMemberVariable(void *data, std::size_t offset, + Type memberType, py::object value) { + auto appendValue = [](void *data, auto &&value, std::size_t offset) { + std::memcpy(((char *)data) + offset, &value, + sizeof(std::remove_cvref_t)); + }; + llvm::TypeSwitch(memberType) + .Case([&](IntegerType ty) { + if (ty.isInteger(1)) { + appendValue(data, value.cast(), offset); + return; + } + appendValue(data, (std::size_t)value.cast(), offset); + }) + .Case([&](mlir::Float64Type ty) { + appendValue(data, (double)value.cast(), offset); + }) + .Case([&](cudaq::cc::StdvecType ty) { + auto appendVectorValue = [](py::object value, void *data, + std::size_t offset, T) { + auto asList = value.cast(); + std::vector *values = new std::vector(asList.size()); + for (std::size_t i = 0; auto &v : asList) + (*values)[i++] = v.cast(); + + std::memcpy(((char *)data) + offset, values, 16); + }; + + TypeSwitch(ty.getElementType()) + .Case([&](IntegerType type) { + if (type.isInteger(1)) { + appendVectorValue(value, data, offset, bool()); + return; + } + + appendVectorValue(value, data, offset, std::size_t()); + return; + }) + .Case([&](FloatType type) { + if (type.isF32()) { + appendVectorValue(value, data, offset, float()); + return; + } + + appendVectorValue(value, data, offset, double()); + return; + }); + }) + .Default([&](Type ty) { + ty.dump(); + throw std::runtime_error( + "Type not supported for custom struct in kernel."); + }); +} + inline void packArgs(OpaqueArguments &argData, py::args args, mlir::func::FuncOp kernelFuncOp, const std::function(); + for (std::size_t i = 0; + const auto &[attr_name, unused] : attributes) { + py::object attr_value = + arg.attr(attr_name.cast().c_str()); + handleStructMemberVariable(allocatedArg, offsets[i], memberTys[i], + attr_value); + i++; + } + + argData.emplace_back(allocatedArg, [](void *ptr) { std::free(ptr); }); + }) .Case([&](cudaq::cc::StdvecType ty) { checkArgumentType(arg, i); auto casted = py::cast(arg); diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index ddfcbe51309..bcd47a8cafb 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -11,6 +11,7 @@ set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) add_subdirectory(common) add_subdirectory(nvqir) add_subdirectory(cudaq) +add_subdirectory(test) # Install # ============================================================================== diff --git a/runtime/common/ArgumentConversion.cpp b/runtime/common/ArgumentConversion.cpp new file mode 100644 index 00000000000..7edb146c6fb --- /dev/null +++ b/runtime/common/ArgumentConversion.cpp @@ -0,0 +1,362 @@ +/******************************************************************************* + * Copyright (c) 2022 - 2024 NVIDIA Corporation & Affiliates. * + * All rights reserved. * + * * + * This source code and the accompanying materials are made available under * + * the terms of the Apache License 2.0 which accompanies this distribution. * + ******************************************************************************/ + +#include "ArgumentConversion.h" +#include "cudaq/Optimizer/Builder/Intrinsics.h" +#include "cudaq/Optimizer/Builder/Runtime.h" +#include "cudaq/Todo.h" +#include "llvm/ADT/TypeSwitch.h" +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Complex/IR/Complex.h" +#include "mlir/IR/BuiltinAttributes.h" + +using namespace mlir; + +template +Value genIntegerConstant(OpBuilder &builder, A v, unsigned bits) { + return builder.create(builder.getUnknownLoc(), v, bits); +} + +static Value genConstant(OpBuilder &builder, bool v) { + return genIntegerConstant(builder, v, 1); +} +static Value genConstant(OpBuilder &builder, char v) { + return genIntegerConstant(builder, v, 8); +} +static Value genConstant(OpBuilder &builder, std::int16_t v) { + return genIntegerConstant(builder, v, 16); +} +static Value genConstant(OpBuilder &builder, std::int32_t v) { + return genIntegerConstant(builder, v, 32); +} +static Value genConstant(OpBuilder &builder, std::int64_t v) { + return genIntegerConstant(builder, v, 64); +} + +static Value genConstant(OpBuilder &builder, float v) { + return builder.create( + builder.getUnknownLoc(), APFloat{v}, builder.getF32Type()); +} +static Value genConstant(OpBuilder &builder, double v) { + return builder.create( + builder.getUnknownLoc(), APFloat{v}, builder.getF64Type()); +} + +template +Value genComplexConstant(OpBuilder &builder, const std::complex &v, + FloatType fTy) { + auto rePart = builder.getFloatAttr(fTy, APFloat{v.real()}); + auto imPart = builder.getFloatAttr(fTy, APFloat{v.imag()}); + auto complexAttr = builder.getArrayAttr({rePart, imPart}); + auto loc = builder.getUnknownLoc(); + auto ty = ComplexType::get(fTy); + return builder.create(loc, ty, complexAttr).getResult(); +} + +static Value genConstant(OpBuilder &builder, std::complex v) { + return genComplexConstant(builder, v, builder.getF32Type()); +} +static Value genConstant(OpBuilder &builder, std::complex v) { + return genComplexConstant(builder, v, builder.getF64Type()); +} +static Value genConstant(OpBuilder &builder, FloatType fltTy, long double *v) { + return builder.create( + builder.getUnknownLoc(), + APFloat{fltTy.getFloatSemantics(), std::to_string(*v)}, fltTy); +} + +static Value genConstant(OpBuilder &builder, const std::string &v, + ModuleOp substMod) { + auto loc = builder.getUnknownLoc(); + cudaq::IRBuilder irBuilder(builder); + auto cString = irBuilder.genCStringLiteralAppendNul(loc, substMod, v); + auto addr = builder.create( + loc, cudaq::cc::PointerType::get(cString.getType()), cString.getName()); + auto i8PtrTy = cudaq::cc::PointerType::get(builder.getI8Type()); + auto cast = builder.create(loc, i8PtrTy, addr); + auto size = builder.create(loc, v.size(), 64); + auto chSpanTy = cudaq::cc::CharspanType::get(builder.getContext()); + return builder.create(loc, chSpanTy, cast, size); +} + +static Value genConstant(OpBuilder &builder, const cudaq::state *v, + ModuleOp substMod) { + // TODO: do we materialize the data here or just add a symbolic reference into + // the unknown? + TODO("cudaq::state* argument synthesis"); + return {}; +} + +// Forward declare aggregate type builder as they can be recursive. +static Value genConstant(OpBuilder &, cudaq::cc::StdvecType, void *, + ModuleOp substMod, llvm::DataLayout &); +static Value genConstant(OpBuilder &, cudaq::cc::StructType, void *, + ModuleOp substMod, llvm::DataLayout &); +static Value genConstant(OpBuilder &, TupleType, void *, ModuleOp substMod, + llvm::DataLayout &); +static Value genConstant(OpBuilder &, cudaq::cc::ArrayType, void *, + ModuleOp substMod, llvm::DataLayout &); + +// Recursive step processing of aggregates. +Value dispatchSubtype(OpBuilder &builder, Type ty, void *p, ModuleOp substMod, + llvm::DataLayout &layout) { + auto *ctx = builder.getContext(); + return TypeSwitch(ty) + .Case([&](IntegerType intTy) -> Value { + switch (intTy.getIntOrFloatBitWidth()) { + case 1: + return genConstant(builder, *static_cast(p)); + case 8: + return genConstant(builder, *static_cast(p)); + case 16: + return genConstant(builder, *static_cast(p)); + case 32: + return genConstant(builder, *static_cast(p)); + case 64: + return genConstant(builder, *static_cast(p)); + default: + return {}; + } + }) + .Case([&](Float32Type fltTy) { + return genConstant(builder, *static_cast(p)); + }) + .Case([&](Float64Type fltTy) { + return genConstant(builder, *static_cast(p)); + }) + .Case([&](FloatType fltTy) { + assert(fltTy.getIntOrFloatBitWidth() > 64); + return genConstant(builder, fltTy, static_cast(p)); + }) + .Case([&](ComplexType cmplxTy) -> Value { + if (cmplxTy.getElementType() == Float32Type::get(ctx)) + return genConstant(builder, *static_cast *>(p)); + if (cmplxTy.getElementType() == Float64Type::get(ctx)) + return genConstant(builder, *static_cast *>(p)); + return {}; + }) + .Case([&](cudaq::cc::CharspanType strTy) { + return genConstant(builder, *static_cast(p), + substMod); + }) + .Case([&](cudaq::cc::PointerType ptrTy) -> Value { + if (ptrTy.getElementType() == cudaq::cc::StateType::get(ctx)) + return genConstant(builder, static_cast(p), + substMod); + return {}; + }) + .Case([&](cudaq::cc::StdvecType ty) { + return genConstant(builder, ty, p, substMod, layout); + }) + .Case([&](cudaq::cc::StructType ty) { + return genConstant(builder, ty, p, substMod, layout); + }) + .Case([&](cudaq::cc::ArrayType ty) { + return genConstant(builder, ty, p, substMod, layout); + }) + .Case([&](TupleType ty) { + return genConstant(builder, ty, p, substMod, layout); + }) + .Default({}); +} + +// Clang++ lays std::tuples out in reverse order. +Value genConstant(OpBuilder &builder, TupleType tupTy, void *p, + ModuleOp substMod, llvm::DataLayout &layout) { + if (tupTy.getTypes().empty()) + return {}; + SmallVector members; + for (auto ty : llvm::reverse(tupTy.getTypes())) + members.emplace_back(ty); + auto *ctx = builder.getContext(); + auto strTy = cudaq::cc::StructType::get(ctx, members); + // FIXME: read out in reverse order, but build in forward order. + auto revCon = genConstant(builder, strTy, p, substMod, layout); + auto fwdTy = cudaq::cc::StructType::get(ctx, tupTy.getTypes()); + auto loc = builder.getUnknownLoc(); + Value aggie = builder.create(loc, fwdTy); + auto n = fwdTy.getMembers().size(); + for (auto iter : llvm::enumerate(fwdTy.getMembers())) { + auto i = iter.index(); + Value v = builder.create(loc, iter.value(), + revCon, n - i - 1); + aggie = builder.create(loc, fwdTy, aggie, v, i); + } + return aggie; +} + +Value genConstant(OpBuilder &builder, cudaq::cc::StdvecType vecTy, void *p, + ModuleOp substMod, llvm::DataLayout &layout) { + typedef const char *VectorType[3]; + VectorType *vecPtr = static_cast(p); + auto delta = (*vecPtr)[1] - (*vecPtr)[0]; + if (!delta) + return {}; + auto eleTy = vecTy.getElementType(); + auto elePtrTy = cudaq::cc::PointerType::get(eleTy); + auto eleSize = cudaq::opt::getDataSize(layout, eleTy); + assert(eleSize && "element must have a size"); + auto loc = builder.getUnknownLoc(); + std::int32_t vecSize = delta / eleSize; + auto eleArrTy = + cudaq::cc::ArrayType::get(builder.getContext(), eleTy, vecSize); + auto buffer = builder.create(loc, eleArrTy); + const char *cursor = (*vecPtr)[0]; + for (std::int32_t i = 0; i < vecSize; ++i) { + if (Value val = dispatchSubtype( + builder, eleTy, static_cast(const_cast(cursor)), + substMod, layout)) { + auto atLoc = builder.create( + loc, elePtrTy, buffer, ArrayRef{i}); + builder.create(loc, val, atLoc); + } + cursor += eleSize; + } + auto size = builder.create(loc, vecSize, 64); + return builder.create(loc, vecTy, buffer, size); +} + +Value genConstant(OpBuilder &builder, cudaq::cc::StructType strTy, void *p, + ModuleOp substMod, llvm::DataLayout &layout) { + if (strTy.getMembers().empty()) + return {}; + const char *cursor = static_cast(p); + auto loc = builder.getUnknownLoc(); + Value aggie = builder.create(loc, strTy); + for (auto iter : llvm::enumerate(strTy.getMembers())) { + auto i = iter.index(); + if (Value v = dispatchSubtype( + builder, iter.value(), + static_cast(const_cast( + cursor + cudaq::opt::getDataOffset(layout, strTy, i))), + substMod, layout)) + aggie = builder.create(loc, strTy, aggie, v, i); + } + return aggie; +} + +Value genConstant(OpBuilder &builder, cudaq::cc::ArrayType arrTy, void *p, + ModuleOp substMod, llvm::DataLayout &layout) { + if (arrTy.isUnknownSize()) + return {}; + auto eleTy = arrTy.getElementType(); + auto loc = builder.getUnknownLoc(); + auto eleSize = cudaq::opt::getDataSize(layout, eleTy); + Value aggie = builder.create(loc, arrTy); + std::size_t arrSize = arrTy.getSize(); + const char *cursor = static_cast(p); + for (std::size_t i = 0; i < arrSize; ++i) { + if (Value v = dispatchSubtype( + builder, eleTy, static_cast(const_cast(cursor)), + substMod, layout)) + aggie = builder.create(loc, arrTy, aggie, v, i); + cursor += eleSize; + } + return aggie; +} + +//===----------------------------------------------------------------------===// + +cudaq::opt::ArgumentConverter::ArgumentConverter(StringRef kernelName, + ModuleOp sourceModule) + : sourceModule(sourceModule), builder(sourceModule.getContext()), + kernelName(kernelName) { + substModule = builder.create(builder.getUnknownLoc()); +} + +void cudaq::opt::ArgumentConverter::gen(const std::vector &arguments) { + auto *ctx = builder.getContext(); + // We should look up the input type signature here. + + auto fun = sourceModule.lookupSymbol( + cudaq::runtime::cudaqGenPrefixName + kernelName.str()); + FunctionType fromFuncTy = fun.getFunctionType(); + for (auto iter : + llvm::enumerate(llvm::zip(fromFuncTy.getInputs(), arguments))) { + Type argTy = std::get<0>(iter.value()); + void *argPtr = std::get<1>(iter.value()); + unsigned i = iter.index(); + auto buildSubst = [&, i = i](Ts &&...ts) { + builder.setInsertionPointToEnd(substModule.getBody()); + auto loc = builder.getUnknownLoc(); + auto result = builder.create(loc, i); + auto *block = new Block(); + result.getBody().push_back(block); + builder.setInsertionPointToEnd(block); + [[maybe_unused]] auto val = genConstant(builder, std::forward(ts)...); + return result; + }; + + StringRef dataLayoutSpec = ""; + if (auto attr = sourceModule->getAttr( + cudaq::opt::factory::targetDataLayoutAttrName)) + dataLayoutSpec = cast(attr); + llvm::DataLayout dataLayout{dataLayoutSpec}; + + auto subst = + TypeSwitch(argTy) + .Case([&](IntegerType intTy) -> cc::ArgumentSubstitutionOp { + switch (intTy.getIntOrFloatBitWidth()) { + case 1: + return buildSubst(*static_cast(argPtr)); + case 8: + return buildSubst(*static_cast(argPtr)); + case 16: + return buildSubst(*static_cast(argPtr)); + case 32: + return buildSubst(*static_cast(argPtr)); + case 64: + return buildSubst(*static_cast(argPtr)); + default: + return {}; + } + }) + .Case([&](Float32Type fltTy) { + return buildSubst(*static_cast(argPtr)); + }) + .Case([&](Float64Type fltTy) { + return buildSubst(*static_cast(argPtr)); + }) + .Case([&](FloatType fltTy) { + assert(fltTy.getIntOrFloatBitWidth() > 64); + return buildSubst(fltTy, static_cast(argPtr)); + }) + .Case([&](ComplexType cmplxTy) -> cc::ArgumentSubstitutionOp { + if (cmplxTy.getElementType() == Float32Type::get(ctx)) + return buildSubst(*static_cast *>(argPtr)); + if (cmplxTy.getElementType() == Float64Type::get(ctx)) + return buildSubst(*static_cast *>(argPtr)); + return {}; + }) + .Case([&](cc::CharspanType strTy) { + return buildSubst(*static_cast(argPtr), + substModule); + }) + .Case([&](cc::PointerType ptrTy) -> cc::ArgumentSubstitutionOp { + if (ptrTy.getElementType() == cc::StateType::get(ctx)) + return buildSubst(static_cast(argPtr), + substModule); + return {}; + }) + .Case([&](cc::StdvecType ty) { + return buildSubst(ty, argPtr, substModule, dataLayout); + }) + .Case([&](cc::StructType ty) { + return buildSubst(ty, argPtr, substModule, dataLayout); + }) + .Case([&](cc::ArrayType ty) { + return buildSubst(ty, argPtr, substModule, dataLayout); + }) + .Case([&](TupleType ty) { + return buildSubst(ty, argPtr, substModule, dataLayout); + }) + .Default({}); + if (subst) + substitutions.emplace_back(std::move(subst)); + } +} diff --git a/runtime/common/ArgumentConversion.h b/runtime/common/ArgumentConversion.h new file mode 100644 index 00000000000..cefc27aed90 --- /dev/null +++ b/runtime/common/ArgumentConversion.h @@ -0,0 +1,50 @@ +/****************************************************************-*- C++ -*-**** + * Copyright (c) 2022 - 2024 NVIDIA Corporation & Affiliates. * + * All rights reserved. * + * * + * This source code and the accompanying materials are made available under * + * the terms of the Apache License 2.0 which accompanies this distribution. * + ******************************************************************************/ + +#pragma once + +#include "cudaq/Optimizer/Dialect/CC/CCOps.h" +#include "cudaq/Optimizer/Dialect/CC/CCTypes.h" +#include "mlir/IR/Builders.h" +#include "mlir/IR/Types.h" + +namespace cudaq { +class state; +} + +namespace cudaq::opt { + +class ArgumentConverter { +public: + /// Build an instance to create argument substitutions for a specified \p + /// kernelName in \p sourceModule. + ArgumentConverter(mlir::StringRef kernelName, mlir::ModuleOp sourceModule); + + /// Generate a substitution ModuleOp for the vector of arguments presented. + /// The arguments are those presented to the kernel, kernelName. + void gen(const std::vector &arguments); + + /// Get the list of substitutions that were generated by `gen()`. + mlir::SmallVector &getSubstitutions() { + return substitutions; + } + + /// Some substitutions may generate global constant information. Use this + /// interface to access both the substitutions and any global constants + /// created. + mlir::ModuleOp getSubstitutionModule() { return substModule; } + +private: + mlir::ModuleOp sourceModule; + mlir::ModuleOp substModule; + mlir::OpBuilder builder; + mlir::StringRef kernelName; + mlir::SmallVector substitutions; +}; + +} // namespace cudaq::opt diff --git a/runtime/common/CMakeLists.txt b/runtime/common/CMakeLists.txt index 1688063e48c..cf6373fc09a 100644 --- a/runtime/common/CMakeLists.txt +++ b/runtime/common/CMakeLists.txt @@ -11,15 +11,15 @@ set(LIBRARY_NAME cudaq-common) set(COMMON_EXTRA_DEPS "") set(COMMON_RUNTIME_SRC + Environment.cpp + Executor.cpp + Future.cpp Logger.cpp MeasureCounts.cpp NoiseModel.cpp - ServerHelper.cpp Resources.cpp + ServerHelper.cpp Trace.cpp - Future.cpp - Environment.cpp - Executor.cpp ) # Create the cudaq-common library @@ -85,9 +85,25 @@ endif() get_property(dialect_libs GLOBAL PROPERTY MLIR_DIALECT_LIBS) get_property(conversion_libs GLOBAL PROPERTY MLIR_CONVERSION_LIBS) -add_library(cudaq-mlir-runtime SHARED RuntimeMLIR.cpp Environment.cpp JIT.cpp Logger.cpp) +add_library(cudaq-mlir-runtime + SHARED + ArgumentConversion.cpp + Environment.cpp + JIT.cpp + Logger.cpp + RuntimeMLIR.cpp +) set_property(GLOBAL APPEND PROPERTY CUDAQ_RUNTIME_LIBS cudaq-mlir-runtime) -set_source_files_properties(JIT.cpp PROPERTIES COMPILE_FLAGS -fno-rtti) + +# Note: JIT.cpp contains throw statements. Should RTTI be enabled? +set_source_files_properties( + ArgumentConversion.cpp + Environment.cpp + JIT.cpp + Logger.cpp + RuntimeMLIR.cpp + PROPERTIES COMPILE_FLAGS -fno-rtti +) target_include_directories(cudaq-mlir-runtime PRIVATE . diff --git a/runtime/cudaq/builder/kernel_builder.cpp b/runtime/cudaq/builder/kernel_builder.cpp index ef22bbc5bc9..29333b96401 100644 --- a/runtime/cudaq/builder/kernel_builder.cpp +++ b/runtime/cudaq/builder/kernel_builder.cpp @@ -510,6 +510,27 @@ QuakeValue qalloc(ImplicitLocOpBuilder &builder, QuakeValue &sizeOrVec) { return QuakeValue(builder, qubits); } + if (auto statePtrTy = dyn_cast(type)) { + auto eleTy = statePtrTy.getElementType(); + if (auto stateTy = dyn_cast(eleTy)) { + // get the number of qubits + IRBuilder irBuilder(context); + auto mod = builder.getBlock()->getParentOp()->getParentOfType(); + auto result = irBuilder.loadIntrinsic(mod, getNumQubitsFromCudaqState); + assert(succeeded(result) && "loading intrinsic should never fail"); + auto numQubits = builder.create( + builder.getI64Type(), getNumQubitsFromCudaqState, ValueRange{value}); + // allocate the number of qubits we need + auto veqTy = quake::VeqType::getUnsized(context); + Value qubits = + builder.create(veqTy, numQubits.getResult(0)); + // Add the initialize state op + qubits = builder.create(qubits.getType(), + qubits, value); + return QuakeValue(builder, qubits); + } + } + if (!type.isIntOrIndex()) throw std::runtime_error( "Invalid parameter passed to qalloc (must be integer type)."); diff --git a/runtime/cudaq/builder/kernel_builder.h b/runtime/cudaq/builder/kernel_builder.h index 033f6c6cd69..d88c1971c64 100644 --- a/runtime/cudaq/builder/kernel_builder.h +++ b/runtime/cudaq/builder/kernel_builder.h @@ -349,6 +349,14 @@ struct ArgumentValidator> { } }; +/// @brief Return a pointer to store in argument array. +template +void *getArgPointer(T *arg) { + if constexpr (std::is_pointer_v) + return *arg; + return arg; +} + /// @brief The `kernel_builder_base` provides a base type for the templated /// kernel builder so that we can get a single handle on an instance within the /// runtime. @@ -889,7 +897,7 @@ class kernel_builder : public details::kernel_builder_base { [[maybe_unused]] std::size_t argCounter = 0; (details::ArgumentValidator::validate(argCounter, arguments, args), ...); - void *argsArr[sizeof...(Args)] = {&args...}; + void *argsArr[sizeof...(Args)] = {details::getArgPointer(&args)...}; return operator()(argsArr); } diff --git a/runtime/nvqir/custatevec/CuStateVecCircuitSimulator.cpp b/runtime/nvqir/custatevec/CuStateVecCircuitSimulator.cpp index 03044c436bb..207529a3038 100644 --- a/runtime/nvqir/custatevec/CuStateVecCircuitSimulator.cpp +++ b/runtime/nvqir/custatevec/CuStateVecCircuitSimulator.cpp @@ -189,9 +189,14 @@ class CuStateVecCircuitSimulator return; } - // User state provided... + // Check if the pointer is a device pointer + cudaPointerAttributes attributes; + HANDLE_CUDA_ERROR(cudaPointerGetAttributes(&attributes, state)); - // FIXME handle case where pointer is a device pointer + if (attributes.type == cudaMemoryTypeDevice) { + throw std::invalid_argument( + "[CuStateVecCircuitSimulator] Incompatible host pointer"); + } // First allocation, so just set the user provided data here ScopedTraceWithContext( @@ -200,6 +205,7 @@ class CuStateVecCircuitSimulator HANDLE_CUDA_ERROR(cudaMemcpy(deviceStateVector, state, stateDimension * sizeof(CudaDataType), cudaMemcpyHostToDevice)); + return; } @@ -221,8 +227,15 @@ class CuStateVecCircuitSimulator n_blocks, threads_per_block, otherState, (1UL << count)); HANDLE_CUDA_ERROR(cudaGetLastError()); } else { + // Check if the pointer is a device pointer + cudaPointerAttributes attributes; + HANDLE_CUDA_ERROR(cudaPointerGetAttributes(&attributes, state)); + + if (attributes.type == cudaMemoryTypeDevice) { + throw std::invalid_argument( + "[CuStateVecCircuitSimulator] Incompatible host pointer"); + } - // FIXME Handle case where data is already on GPU HANDLE_CUDA_ERROR(cudaMemcpy(otherState, state, (1UL << count) * sizeof(CudaDataType), cudaMemcpyHostToDevice)); @@ -692,6 +705,9 @@ class CuStateVecCircuitSimulator } counts.expectationValue = expVal; + + HANDLE_ERROR(custatevecSamplerDestroy(sampler)); + return counts; } diff --git a/runtime/test/CMakeLists.txt b/runtime/test/CMakeLists.txt new file mode 100644 index 00000000000..78f39497e0e --- /dev/null +++ b/runtime/test/CMakeLists.txt @@ -0,0 +1,30 @@ +# ============================================================================ # +# Copyright (c) 2022 - 2024 NVIDIA Corporation & Affiliates. # +# All rights reserved. # +# # +# This source code and the accompanying materials are made available under # +# the terms of the Apache License 2.0 which accompanies this distribution. # +# ============================================================================ # + +# Hybrid test. Build the executable like any of the other tools, but +# run it like it was a lit test. +set(TEST_NAME test_argument_conversion) + +add_llvm_executable(${TEST_NAME} test_argument_conversion.cpp) + +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-type-limits") +llvm_update_compile_flags(${TEST_NAME}) + +target_include_directories(${TEST_NAME} + PUBLIC + ${CMAKE_SOURCE_DIR}/runtime +) + +link_directories(${CMAKE_BINARY_DIR}/lib) + +target_link_libraries(${TEST_NAME} + PUBLIC + cudaq-mlir-runtime +) + +set_property(TARGET ${TEST_NAME} PROPERTY FOLDER test) diff --git a/runtime/test/test_argument_conversion.cpp b/runtime/test/test_argument_conversion.cpp new file mode 100644 index 00000000000..bb8c0ab34ca --- /dev/null +++ b/runtime/test/test_argument_conversion.cpp @@ -0,0 +1,303 @@ +/******************************************************************************* + * Copyright (c) 2022 - 2024 NVIDIA Corporation & Affiliates. * + * All rights reserved. * + * * + * This source code and the accompanying materials are made available under * + * the terms of the Apache License 2.0 which accompanies this distribution. * + ******************************************************************************/ + +// This test is compiled inside the runtime directory tree. We include it as a +// regression test and use FileCheck to verify the output. + +// RUN: test_argument_conversion | FileCheck %s + +#include "common/ArgumentConversion.h" +#include "cudaq/Optimizer/Dialect/CC/CCDialect.h" +#include "cudaq/Optimizer/Dialect/Quake/QuakeDialect.h" +#include "mlir/InitAllDialects.h" +#include "mlir/Parser/Parser.h" + +void doSimpleTest(mlir::MLIRContext *ctx, const std::string &typeName, + std::vector args) { + std::string code = R"#( +func.func private @callee(%0: )#" + + typeName + R"#() +func.func @__nvqpp__mlirgen__testy(%0: )#" + + typeName + R"#() { + call @callee(%0) : ()#" + + typeName + R"#() -> () + return +})#"; + + // Create the Module + auto mod = mlir::parseSourceString(code, ctx); + llvm::outs() << "Source module:\n" << *mod << '\n'; + cudaq::opt::ArgumentConverter ab{"testy", *mod}; + + // Create the argument conversions + ab.gen(args); + + // Dump the conversions + llvm::outs() << "========================================\n" + "Substitution module:\n" + << ab.getSubstitutionModule() << '\n'; +} + +void test_scalars(mlir::MLIRContext *ctx) { + { + bool x = true; + std::vector v = {static_cast(&x)}; + doSimpleTest(ctx, "i1", v); + } + // clang-format off +// CHECK-LABEL: Source module: +// CHECK: func.func private @callee(i1) +// CHECK: Substitution module: + +// CHECK-LABEL: cc.arg_subst[0] { +// CHECK: %[[VAL_0:.*]] = arith.constant true +// CHECK: } + // clang-format on + { + char x = 'X'; + std::vector v = {static_cast(&x)}; + doSimpleTest(ctx, "i8", v); + } + // clang-format off +// CHECK: Source module: +// CHECK: func.func private @callee(i8) +// CHECK: Substitution module: + +// CHECK-LABEL: cc.arg_subst[0] { +// CHECK: %[[VAL_0:.*]] = arith.constant 88 : i8 +// CHECK: } + // clang-format on + { + std::int16_t x = 103; + std::vector v = {static_cast(&x)}; + doSimpleTest(ctx, "i16", v); + } + // clang-format off +// CHECK: Source module: +// CHECK: func.func private @callee(i16) +// CHECK: Substitution module: + +// CHECK-LABEL: cc.arg_subst[0] { +// CHECK: %[[VAL_0:.*]] = arith.constant 103 : i16 +// CHECK: } + // clang-format on + { + std::int32_t x = 14581; + std::vector v = {static_cast(&x)}; + doSimpleTest(ctx, "i32", v); + } + // clang-format off +// CHECK: Source module: +// CHECK: func.func private @callee(i32) +// CHECK: Substitution module: + +// CHECK-LABEL: cc.arg_subst[0] { +// CHECK: %[[VAL_0:.*]] = arith.constant 14581 : i32 +// CHECK: } + // clang-format on + { + std::int64_t x = 78190214; + std::vector v = {static_cast(&x)}; + doSimpleTest(ctx, "i64", v); + } + // clang-format off +// CHECK: Source module: +// CHECK: func.func private @callee(i64) +// CHECK: Substitution module: + +// CHECK-LABEL: cc.arg_subst[0] { +// CHECK: %[[VAL_0:.*]] = arith.constant 78190214 : i64 +// CHECK: } + // clang-format on + + { + float x = 974.17244; + std::vector v = {static_cast(&x)}; + doSimpleTest(ctx, "f32", v); + } + // clang-format off +// CHECK: Source module: +// CHECK: func.func private @callee(f32) +// CHECK: Substitution module: + +// CHECK-LABEL: cc.arg_subst[0] { +// CHECK: %[[VAL_0:.*]] = arith.constant 974.172424 : f32 +// CHECK: } + // clang-format on + { + double x = 77.4782348; + std::vector v = {static_cast(&x)}; + doSimpleTest(ctx, "f64", v); + } + // clang-format off +// CHECK: Source module: +// CHECK: func.func private @callee(f64) +// CHECK: Substitution module: + +// CHECK-LABEL: cc.arg_subst[0] { +// CHECK: %[[VAL_0:.*]] = arith.constant 77.478234799999996 : f64 +// CHECK: } + // clang-format on + + { + std::string x = "Hi, there!"; + std::vector v = {static_cast(&x)}; + doSimpleTest(ctx, "!cc.charspan", v); + } + // clang-format off +// CHECK: Source module: +// CHECK: func.func private @callee(!cc.charspan) +// CHECK: Substitution module: + +// CHECK-LABEL: cc.arg_subst[0] { +// CHECK: %[[VAL_0:.*]] = cc.address_of @cstr.48692C2074686572652100 : !cc.ptr> +// CHECK: %[[VAL_1:.*]] = cc.cast %[[VAL_0]] : (!cc.ptr>) -> !cc.ptr +// CHECK: %[[VAL_2:.*]] = arith.constant 10 : i64 +// CHECK: %[[VAL_3:.*]] = cc.stdvec_init %[[VAL_1]], %[[VAL_2]] : (!cc.ptr, i64) -> !cc.charspan +// CHECK: } +// CHECK: llvm.mlir.global private constant @cstr.48692C2074686572652100("Hi, there!\00") {addr_space = 0 : i32} + // clang-format on +} + +void test_vectors(mlir::MLIRContext *ctx) { + { + std::vector x = {14581, 0xcafe, 42, 0xbeef}; + std::vector v = {static_cast(&x)}; + doSimpleTest(ctx, "!cc.stdvec", v); + } + // clang-format off +// CHECK: Source module: +// CHECK: func.func private @callee(!cc.stdvec) +// CHECK: Substitution module: + +// CHECK-LABEL: cc.arg_subst[0] { +// CHECK: %[[VAL_0:.*]] = cc.alloca !cc.array +// CHECK: %[[VAL_1:.*]] = arith.constant 14581 : i32 +// CHECK: %[[VAL_2:.*]] = cc.compute_ptr %[[VAL_0]][0] : (!cc.ptr>) -> !cc.ptr +// CHECK: cc.store %[[VAL_1]], %[[VAL_2]] : !cc.ptr +// CHECK: %[[VAL_3:.*]] = arith.constant 51966 : i32 +// CHECK: %[[VAL_4:.*]] = cc.compute_ptr %[[VAL_0]][1] : (!cc.ptr>) -> !cc.ptr +// CHECK: cc.store %[[VAL_3]], %[[VAL_4]] : !cc.ptr +// CHECK: %[[VAL_5:.*]] = arith.constant 42 : i32 +// CHECK: %[[VAL_6:.*]] = cc.compute_ptr %[[VAL_0]][2] : (!cc.ptr>) -> !cc.ptr +// CHECK: cc.store %[[VAL_5]], %[[VAL_6]] : !cc.ptr +// CHECK: %[[VAL_7:.*]] = arith.constant 48879 : i32 +// CHECK: %[[VAL_8:.*]] = cc.compute_ptr %[[VAL_0]][3] : (!cc.ptr>) -> !cc.ptr +// CHECK: cc.store %[[VAL_7]], %[[VAL_8]] : !cc.ptr +// CHECK: %[[VAL_9:.*]] = arith.constant 4 : i64 +// CHECK: %[[VAL_10:.*]] = cc.stdvec_init %[[VAL_0]], %[[VAL_9]] : (!cc.ptr>, i64) -> !cc.stdvec +// CHECK: } + // clang-format on +} + +void test_aggregates(mlir::MLIRContext *ctx) { + { + struct ure { + int _0; + double _1; + char _2; + short _3; + }; + ure x = {static_cast(0xcafebabe), 87.6545, 'A', + static_cast(0xfade)}; + std::vector v = {static_cast(&x)}; + doSimpleTest(ctx, "!cc.struct<{i32,f64,i8,i16}>", v); + } + // clang-format off +// CHECK: Source module: +// CHECK: func.func private @callee(!cc.struct<{i32, f64, i8, i16}>) +// CHECK: Substitution module: + +// CHECK-LABEL: cc.arg_subst[0] { +// CHECK: %[[VAL_0:.*]] = cc.undef !cc.struct<{i32, f64, i8, i16}> +// CHECK: %[[VAL_1:.*]] = arith.constant -889275714 : i32 +// CHECK: %[[VAL_2:.*]] = cc.insert_value %[[VAL_1]], %[[VAL_0]][0] : (!cc.struct<{i32, f64, i8, i16}>, i32) -> !cc.struct<{i32, f64, i8, i16}> +// CHECK: %[[VAL_3:.*]] = arith.constant 87.654499999999998 : f64 +// CHECK: %[[VAL_4:.*]] = cc.insert_value %[[VAL_3]], %[[VAL_2]][1] : (!cc.struct<{i32, f64, i8, i16}>, f64) -> !cc.struct<{i32, f64, i8, i16}> +// CHECK: %[[VAL_5:.*]] = arith.constant 65 : i8 +// CHECK: %[[VAL_6:.*]] = cc.insert_value %[[VAL_5]], %[[VAL_4]][2] : (!cc.struct<{i32, f64, i8, i16}>, i8) -> !cc.struct<{i32, f64, i8, i16}> +// CHECK: %[[VAL_7:.*]] = arith.constant -1314 : i16 +// CHECK: %[[VAL_8:.*]] = cc.insert_value %[[VAL_7]], %[[VAL_6]][3] : (!cc.struct<{i32, f64, i8, i16}>, i16) -> !cc.struct<{i32, f64, i8, i16}> +// CHECK: } + // clang-format on +} + +void test_recursive(mlir::MLIRContext *ctx) { + { + struct ure { + int _0; + double _1; + char _2; + short _3; + }; + ure x0 = {static_cast(0xcafebabe), 87.6545, 'A', + static_cast(0xfade)}; + ure x1 = {5412, 23894.5, 'B', 0xada}; + ure x2 = {90210, 782934.78923, 'C', 747}; + std::vector x = {x0, x1, x2}; + std::vector v = {static_cast(&x)}; + doSimpleTest(ctx, "!cc.stdvec>", v); + } + // clang-format off +// CHECK: Source module: +// CHECK: func.func private @callee(!cc.stdvec>) +// CHECK: Substitution module: + +// CHECK-LABEL: cc.arg_subst[0] { +// CHECK: %[[VAL_0:.*]] = cc.alloca !cc.array x 3> +// CHECK: %[[VAL_1:.*]] = cc.undef !cc.struct<{i32, f64, i8, i16}> +// CHECK: %[[VAL_2:.*]] = arith.constant -889275714 : i32 +// CHECK: %[[VAL_3:.*]] = cc.insert_value %[[VAL_2]], %[[VAL_1]][0] : (!cc.struct<{i32, f64, i8, i16}>, i32) -> !cc.struct<{i32, f64, i8, i16}> +// CHECK: %[[VAL_4:.*]] = arith.constant 87.654499999999998 : f64 +// CHECK: %[[VAL_5:.*]] = cc.insert_value %[[VAL_4]], %[[VAL_3]][1] : (!cc.struct<{i32, f64, i8, i16}>, f64) -> !cc.struct<{i32, f64, i8, i16}> +// CHECK: %[[VAL_6:.*]] = arith.constant 65 : i8 +// CHECK: %[[VAL_7:.*]] = cc.insert_value %[[VAL_6]], %[[VAL_5]][2] : (!cc.struct<{i32, f64, i8, i16}>, i8) -> !cc.struct<{i32, f64, i8, i16}> +// CHECK: %[[VAL_8:.*]] = arith.constant -1314 : i16 +// CHECK: %[[VAL_9:.*]] = cc.insert_value %[[VAL_8]], %[[VAL_7]][3] : (!cc.struct<{i32, f64, i8, i16}>, i16) -> !cc.struct<{i32, f64, i8, i16}> +// CHECK: %[[VAL_10:.*]] = cc.compute_ptr %[[VAL_0]][0] : (!cc.ptr x 3>>) -> !cc.ptr> +// CHECK: cc.store %[[VAL_9]], %[[VAL_10]] : !cc.ptr> +// CHECK: %[[VAL_11:.*]] = cc.undef !cc.struct<{i32, f64, i8, i16}> +// CHECK: %[[VAL_12:.*]] = arith.constant 5412 : i32 +// CHECK: %[[VAL_13:.*]] = cc.insert_value %[[VAL_12]], %[[VAL_11]][0] : (!cc.struct<{i32, f64, i8, i16}>, i32) -> !cc.struct<{i32, f64, i8, i16}> +// CHECK: %[[VAL_14:.*]] = arith.constant 2.389450e+04 : f64 +// CHECK: %[[VAL_15:.*]] = cc.insert_value %[[VAL_14]], %[[VAL_13]][1] : (!cc.struct<{i32, f64, i8, i16}>, f64) -> !cc.struct<{i32, f64, i8, i16}> +// CHECK: %[[VAL_16:.*]] = arith.constant 66 : i8 +// CHECK: %[[VAL_17:.*]] = cc.insert_value %[[VAL_16]], %[[VAL_15]][2] : (!cc.struct<{i32, f64, i8, i16}>, i8) -> !cc.struct<{i32, f64, i8, i16}> +// CHECK: %[[VAL_18:.*]] = arith.constant 2778 : i16 +// CHECK: %[[VAL_19:.*]] = cc.insert_value %[[VAL_18]], %[[VAL_17]][3] : (!cc.struct<{i32, f64, i8, i16}>, i16) -> !cc.struct<{i32, f64, i8, i16}> +// CHECK: %[[VAL_20:.*]] = cc.compute_ptr %[[VAL_0]][1] : (!cc.ptr x 3>>) -> !cc.ptr> +// CHECK: cc.store %[[VAL_19]], %[[VAL_20]] : !cc.ptr> +// CHECK: %[[VAL_21:.*]] = cc.undef !cc.struct<{i32, f64, i8, i16}> +// CHECK: %[[VAL_22:.*]] = arith.constant 90210 : i32 +// CHECK: %[[VAL_23:.*]] = cc.insert_value %[[VAL_22]], %[[VAL_21]][0] : (!cc.struct<{i32, f64, i8, i16}>, i32) -> !cc.struct<{i32, f64, i8, i16}> +// CHECK: %[[VAL_24:.*]] = arith.constant 782934.78922999999 : f64 +// CHECK: %[[VAL_25:.*]] = cc.insert_value %[[VAL_24]], %[[VAL_23]][1] : (!cc.struct<{i32, f64, i8, i16}>, f64) -> !cc.struct<{i32, f64, i8, i16}> +// CHECK: %[[VAL_26:.*]] = arith.constant 67 : i8 +// CHECK: %[[VAL_27:.*]] = cc.insert_value %[[VAL_26]], %[[VAL_25]][2] : (!cc.struct<{i32, f64, i8, i16}>, i8) -> !cc.struct<{i32, f64, i8, i16}> +// CHECK: %[[VAL_28:.*]] = arith.constant 747 : i16 +// CHECK: %[[VAL_29:.*]] = cc.insert_value %[[VAL_28]], %[[VAL_27]][3] : (!cc.struct<{i32, f64, i8, i16}>, i16) -> !cc.struct<{i32, f64, i8, i16}> +// CHECK: %[[VAL_30:.*]] = cc.compute_ptr %[[VAL_0]][2] : (!cc.ptr x 3>>) -> !cc.ptr> +// CHECK: cc.store %[[VAL_29]], %[[VAL_30]] : !cc.ptr> +// CHECK: %[[VAL_31:.*]] = arith.constant 3 : i64 +// CHECK: %[[VAL_32:.*]] = cc.stdvec_init %[[VAL_0]], %[[VAL_31]] : (!cc.ptr x 3>>, i64) -> !cc.stdvec> +// CHECK: } + // clang-format on +} + +int main() { + mlir::DialectRegistry registry; + mlir::registerAllDialects(registry); + registry.insert(); + mlir::MLIRContext context(registry); + context.loadAllAvailableDialects(); + test_scalars(&context); + test_vectors(&context); + test_aggregates(&context); + test_recursive(&context); + return 0; +} diff --git a/targettests/execution/mapping_test-1-cpp17.cpp b/targettests/execution/mapping_test-1-cpp17.cpp index 111e25a5994..1db7fe5eaf9 100644 --- a/targettests/execution/mapping_test-1-cpp17.cpp +++ b/targettests/execution/mapping_test-1-cpp17.cpp @@ -7,9 +7,10 @@ ******************************************************************************/ // REQUIRES: c++17 -// RUN: nvq++ %cpp_std %s -o %t --target oqc --emulate && CUDAQ_DUMP_JIT_IR=1 %t 2> %t.code | FileCheck %s && FileCheck --check-prefix=QUAKE %s < %t.code && rm %t.code -// RUN: nvq++ %cpp_std %s -o %t --target iqm --iqm-machine Adonis --mapping-file "%p/../Adonis Variant.txt" --emulate && %t +// RUN: nvq++ %cpp_std %s -o %t --target oqc --emulate && CUDAQ_DUMP_JIT_IR=1 %t 2> %t.txt | FileCheck %s && FileCheck --check-prefix=QUAKE %s < %t.txt +// RUN: mkdir -p %t.dir && cp "%iqm_test_src_dir/Adonis.txt" "%t.dir/Adonis Variant.txt" && nvq++ %cpp_std %s -o %t --target iqm --iqm-machine Adonis --mapping-file "%t.dir/Adonis Variant.txt" --emulate && %t // RUN: nvq++ %cpp_std --enable-mlir %s -o %t +// RUN: rm -rf %t.txt %t.dir #include #include diff --git a/targettests/execution/mapping_test-1.cpp b/targettests/execution/mapping_test-1.cpp index 74cfbe112d0..58dc4a7616c 100644 --- a/targettests/execution/mapping_test-1.cpp +++ b/targettests/execution/mapping_test-1.cpp @@ -7,9 +7,11 @@ ******************************************************************************/ // REQUIRES: c++20 -// RUN: nvq++ %cpp_std %s -o %t --target oqc --emulate && CUDAQ_DUMP_JIT_IR=1 %t 2> %t.code | FileCheck %s && FileCheck --check-prefix=QUAKE %s < %t.code && rm %t.code -// RUN: nvq++ %cpp_std %s -o %t --target iqm --iqm-machine Adonis --mapping-file "%p/../Adonis Variant.txt" --emulate && %t +// clang-format off +// RUN: nvq++ %cpp_std %s -o %t --target oqc --emulate && CUDAQ_DUMP_JIT_IR=1 %t 2> %t.txt | FileCheck %s && FileCheck --check-prefix=QUAKE %s < %t.txt +// RUN: mkdir -p %t.dir && cp "%iqm_test_src_dir/Adonis.txt" "%t.dir/Adonis Variant.txt" && nvq++ %cpp_std %s -o %t --target iqm --iqm-machine Adonis --mapping-file "%t.dir/Adonis Variant.txt" --emulate && %t // RUN: nvq++ %cpp_std --enable-mlir %s -o %t +// RUN: rm -rf %t.txt %t.dir #include #include diff --git a/targettests/lit.cfg.py b/targettests/lit.cfg.py index d16a0344444..a15438026b2 100644 --- a/targettests/lit.cfg.py +++ b/targettests/lit.cfg.py @@ -36,6 +36,7 @@ config.substitutions.append(('%cudaq_plugin_ext', config.cudaq_plugin_ext)) config.substitutions.append(('%cudaq_target_dir', config.cudaq_target_dir)) config.substitutions.append(('%cudaq_src_dir', config.cudaq_src_dir)) +config.substitutions.append(('%iqm_test_src_dir', config.cudaq_src_dir + "/runtime/cudaq/platform/default/rest/helpers/iqm")) llvm_config.use_default_substitutions() @@ -71,9 +72,3 @@ # Tweak the PATH to include the tools directory. llvm_config.with_environment('PATH', config.cudaq_tools_dir, append_path=True) llvm_config.with_environment('PATH', config.llvm_tools_dir, append_path=True) - -# Create some of the test inputs. -target_config_origin = os.path.join(config.cudaq_src_dir, "runtime/cudaq/platform/default/rest/helpers/iqm") -target_config_dest = os.path.join(config.cudaq_src_dir, "targettests") -shutil.copy(os.path.join(target_config_origin, "Adonis.txt"), os.path.join(target_config_dest, "Adonis Variant.txt")) -shutil.copy(os.path.join(target_config_origin, "Apollo.txt"), os.path.join(target_config_dest, "Apollo Variant.txt")) diff --git a/test/ArgumentConversion/test_argument_conversion.cpp b/test/ArgumentConversion/test_argument_conversion.cpp new file mode 120000 index 00000000000..aaad276e135 --- /dev/null +++ b/test/ArgumentConversion/test_argument_conversion.cpp @@ -0,0 +1 @@ +../../runtime/test/test_argument_conversion.cpp \ No newline at end of file diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 88d2f811ac2..8ab70728181 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -34,6 +34,9 @@ set(NVQPP_TEST_DEPENDS cudaq-translate CircuitCheck FileCheck + + test_argument_conversion + CustomPassPlugin ) if (NOT CUDAQ_DISABLE_CPP_FRONTEND) set(NVQPP_TEST_DEPENDS ${NVQPP_TEST_DEPENDS} diff --git a/test/Quake/arg_subst-1.txt b/test/Quake/arg_subst-1.txt new file mode 100644 index 00000000000..6d9fa5ec985 --- /dev/null +++ b/test/Quake/arg_subst-1.txt @@ -0,0 +1,16 @@ +// ========================================================================== // +// Copyright (c) 2022 - 2024 NVIDIA Corporation & Affiliates. // +// All rights reserved. // +// // +// This source code and the accompanying materials are made available under // +// the terms of the Apache License 2.0 which accompanies this distribution. // +// ========================================================================== // + +cc.arg_subst[0] { + %0 = cc.address_of @cstr.48692C2074686572652100 : !cc.ptr> + %1 = cc.cast %0 : (!cc.ptr>) -> !cc.ptr + %c10_i64 = arith.constant 10 : i64 + %2 = cc.stdvec_init %1, %c10_i64 : (!cc.ptr, i64) -> !cc.charspan +} + +llvm.mlir.global private constant @cstr.48692C2074686572652100("Hi, there!\00") {addr_space = 0 : i32} diff --git a/test/Quake/arg_subst-2.txt b/test/Quake/arg_subst-2.txt new file mode 100644 index 00000000000..2f3c5d39d0a --- /dev/null +++ b/test/Quake/arg_subst-2.txt @@ -0,0 +1,25 @@ +// ========================================================================== // +// Copyright (c) 2022 - 2024 NVIDIA Corporation & Affiliates. // +// All rights reserved. // +// // +// This source code and the accompanying materials are made available under // +// the terms of the Apache License 2.0 which accompanies this distribution. // +// ========================================================================== // + +cc.arg_subst[0] { + %0 = cc.alloca !cc.array + %c14581_i32 = arith.constant 14581 : i32 + %1 = cc.compute_ptr %0[0] : (!cc.ptr>) -> !cc.ptr + cc.store %c14581_i32, %1 : !cc.ptr + %c51966_i32 = arith.constant 51966 : i32 + %2 = cc.compute_ptr %0[1] : (!cc.ptr>) -> !cc.ptr + cc.store %c51966_i32, %2 : !cc.ptr + %c42_i32 = arith.constant 42 : i32 + %3 = cc.compute_ptr %0[2] : (!cc.ptr>) -> !cc.ptr + cc.store %c42_i32, %3 : !cc.ptr + %c48879_i32 = arith.constant 48879 : i32 + %4 = cc.compute_ptr %0[3] : (!cc.ptr>) -> !cc.ptr + cc.store %c48879_i32, %4 : !cc.ptr + %c4_i64 = arith.constant 4 : i64 + %5 = cc.stdvec_init %0, %c4_i64 : (!cc.ptr>, i64) -> !cc.stdvec +} diff --git a/test/Quake/arg_subst-3.txt b/test/Quake/arg_subst-3.txt new file mode 100644 index 00000000000..0aadcbdfad8 --- /dev/null +++ b/test/Quake/arg_subst-3.txt @@ -0,0 +1,19 @@ +// ========================================================================== // +// Copyright (c) 2022 - 2024 NVIDIA Corporation & Affiliates. // +// All rights reserved. // +// // +// This source code and the accompanying materials are made available under // +// the terms of the Apache License 2.0 which accompanies this distribution. // +// ========================================================================== // + +cc.arg_subst[0] { + %0 = cc.undef !cc.struct<{i32, f64, i8, i16}> + %c-889275714_i32 = arith.constant -889275714 : i32 + %1 = cc.insert_value %c-889275714_i32, %0[0] : (!cc.struct<{i32, f64, i8, i16}>, i32) -> !cc.struct<{i32, f64, i8, i16}> + %cst = arith.constant 87.654499999999998 : f64 + %2 = cc.insert_value %cst, %1[1] : (!cc.struct<{i32, f64, i8, i16}>, f64) -> !cc.struct<{i32, f64, i8, i16}> + %c65_i8 = arith.constant 65 : i8 + %3 = cc.insert_value %c65_i8, %2[2] : (!cc.struct<{i32, f64, i8, i16}>, i8) -> !cc.struct<{i32, f64, i8, i16}> + %c-1314_i16 = arith.constant -1314 : i16 + %4 = cc.insert_value %c-1314_i16, %3[3] : (!cc.struct<{i32, f64, i8, i16}>, i16) -> !cc.struct<{i32, f64, i8, i16}> +} diff --git a/test/Quake/arg_subst-4.txt b/test/Quake/arg_subst-4.txt new file mode 100644 index 00000000000..828a0aad305 --- /dev/null +++ b/test/Quake/arg_subst-4.txt @@ -0,0 +1,46 @@ +// ========================================================================== // +// Copyright (c) 2022 - 2024 NVIDIA Corporation & Affiliates. // +// All rights reserved. // +// // +// This source code and the accompanying materials are made available under // +// the terms of the Apache License 2.0 which accompanies this distribution. // +// ========================================================================== // + +cc.arg_subst[0] { + %0 = cc.alloca !cc.array x 3> + %1 = cc.undef !cc.struct<{i32, f64, i8, i16}> + %c-889275714_i32 = arith.constant -889275714 : i32 + %2 = cc.insert_value %c-889275714_i32, %1[0] : (!cc.struct<{i32, f64, i8, i16}>, i32) -> !cc.struct<{i32, f64, i8, i16}> + %cst = arith.constant 87.654499999999998 : f64 + %3 = cc.insert_value %cst, %2[1] : (!cc.struct<{i32, f64, i8, i16}>, f64) -> !cc.struct<{i32, f64, i8, i16}> + %c65_i8 = arith.constant 65 : i8 + %4 = cc.insert_value %c65_i8, %3[2] : (!cc.struct<{i32, f64, i8, i16}>, i8) -> !cc.struct<{i32, f64, i8, i16}> + %c-1314_i16 = arith.constant -1314 : i16 + %5 = cc.insert_value %c-1314_i16, %4[3] : (!cc.struct<{i32, f64, i8, i16}>, i16) -> !cc.struct<{i32, f64, i8, i16}> + %6 = cc.compute_ptr %0[0] : (!cc.ptr x 3>>) -> !cc.ptr> + cc.store %5, %6 : !cc.ptr> + %7 = cc.undef !cc.struct<{i32, f64, i8, i16}> + %c5412_i32 = arith.constant 5412 : i32 + %8 = cc.insert_value %c5412_i32, %7[0] : (!cc.struct<{i32, f64, i8, i16}>, i32) -> !cc.struct<{i32, f64, i8, i16}> + %cst_0 = arith.constant 2.389450e+04 : f64 + %9 = cc.insert_value %cst_0, %8[1] : (!cc.struct<{i32, f64, i8, i16}>, f64) -> !cc.struct<{i32, f64, i8, i16}> + %c66_i8 = arith.constant 66 : i8 + %10 = cc.insert_value %c66_i8, %9[2] : (!cc.struct<{i32, f64, i8, i16}>, i8) -> !cc.struct<{i32, f64, i8, i16}> + %c2778_i16 = arith.constant 2778 : i16 + %11 = cc.insert_value %c2778_i16, %10[3] : (!cc.struct<{i32, f64, i8, i16}>, i16) -> !cc.struct<{i32, f64, i8, i16}> + %12 = cc.compute_ptr %0[1] : (!cc.ptr x 3>>) -> !cc.ptr> + cc.store %11, %12 : !cc.ptr> + %13 = cc.undef !cc.struct<{i32, f64, i8, i16}> + %c90210_i32 = arith.constant 90210 : i32 + %14 = cc.insert_value %c90210_i32, %13[0] : (!cc.struct<{i32, f64, i8, i16}>, i32) -> !cc.struct<{i32, f64, i8, i16}> + %cst_1 = arith.constant 782934.78922999999 : f64 + %15 = cc.insert_value %cst_1, %14[1] : (!cc.struct<{i32, f64, i8, i16}>, f64) -> !cc.struct<{i32, f64, i8, i16}> + %c67_i8 = arith.constant 67 : i8 + %16 = cc.insert_value %c67_i8, %15[2] : (!cc.struct<{i32, f64, i8, i16}>, i8) -> !cc.struct<{i32, f64, i8, i16}> + %c747_i16 = arith.constant 747 : i16 + %17 = cc.insert_value %c747_i16, %16[3] : (!cc.struct<{i32, f64, i8, i16}>, i16) -> !cc.struct<{i32, f64, i8, i16}> + %18 = cc.compute_ptr %0[2] : (!cc.ptr x 3>>) -> !cc.ptr> + cc.store %17, %18 : !cc.ptr> + %c3_i64 = arith.constant 3 : i64 + %19 = cc.stdvec_init %0, %c3_i64 : (!cc.ptr x 3>>, i64) -> !cc.stdvec> +} diff --git a/test/Quake/arg_subst.txt b/test/Quake/arg_subst.txt new file mode 100644 index 00000000000..a29049b294d --- /dev/null +++ b/test/Quake/arg_subst.txt @@ -0,0 +1,15 @@ +// ========================================================================== // +// Copyright (c) 2022 - 2024 NVIDIA Corporation & Affiliates. // +// All rights reserved. // +// // +// This source code and the accompanying materials are made available under // +// the terms of the Apache License 2.0 which accompanies this distribution. // +// ========================================================================== // + +cc.arg_subst [0] { + %1 = arith.constant 42 : i32 +} + +cc.arg_subst [1] { + %1 = arith.constant 3.1 : f32 +} diff --git a/test/Quake/arg_subst_func.qke b/test/Quake/arg_subst_func.qke new file mode 100644 index 00000000000..e96e04b63af --- /dev/null +++ b/test/Quake/arg_subst_func.qke @@ -0,0 +1,148 @@ +// ========================================================================== // +// Copyright (c) 2022 - 2024 NVIDIA Corporation & Affiliates. // +// All rights reserved. // +// // +// This source code and the accompanying materials are made available under // +// the terms of the Apache License 2.0 which accompanies this distribution. // +// ========================================================================== // + +// RUN: cudaq-opt --argument-synthesis=functions=foo:%S/arg_subst.txt,blink:%S/arg_subst.txt,testy1:%S/arg_subst-1.txt,testy2:%S/arg_subst-2.txt,testy3:%S/arg_subst-3.txt,testy4:%S/arg_subst-4.txt --canonicalize %s | FileCheck %s + +func.func private @bar(i32) +func.func private @baz(f32) + +func.func @foo(%arg0: i32, %arg1: f32) { + call @bar(%arg0) : (i32) -> () + call @baz(%arg1) : (f32) -> () + return +} + +// CHECK-LABEL: func.func @foo() { +// CHECK-DAG: %[[VAL_0:.*]] = arith.constant 42 : i32 +// CHECK-DAG: %[[VAL_1:.*]] = arith.constant 3.100000e+00 : f32 +// CHECK: call @bar(%[[VAL_0]]) : (i32) -> () +// CHECK: call @baz(%[[VAL_1]]) : (f32) -> () +// CHECK: return +// CHECK: } + +func.func @blink(%arg0: i32, %arg1: i32) { + call @bar(%arg0) : (i32) -> () + call @bar(%arg1) : (i32) -> () + return +} + +// CHECK-LABEL: func.func @blink( +// CHECK-SAME: %[[VAL_0:.*]]: i32) { +// CHECK: %[[VAL_1:.*]] = arith.constant 42 : i32 +// CHECK: call @bar(%[[VAL_1]]) : (i32) -> () +// CHECK: call @bar(%[[VAL_0]]) : (i32) -> () +// CHECK: return +// CHECK: } + +func.func private @callee1(!cc.charspan) +func.func @testy1(%arg0: !cc.charspan) { + call @callee1(%arg0) : (!cc.charspan) -> () + return +} + +// CHECK-LABEL: func.func @testy1() { +// CHECK: %[[VAL_0:.*]] = arith.constant 10 : i64 +// CHECK: %[[VAL_1:.*]] = cc.address_of @cstr.48692C2074686572652100 : !cc.ptr> +// CHECK: %[[VAL_2:.*]] = cc.cast %[[VAL_1]] : (!cc.ptr>) -> !cc.ptr +// CHECK: %[[VAL_3:.*]] = cc.stdvec_init %[[VAL_2]], %[[VAL_0]] : (!cc.ptr, i64) -> !cc.charspan +// CHECK: call @callee1(%[[VAL_3]]) : (!cc.charspan) -> () +// CHECK: return +// CHECK: } + +func.func private @callee2(!cc.stdvec) +func.func @testy2(%arg0: !cc.stdvec) { + call @callee2(%arg0) : (!cc.stdvec) -> () + return +} + +// CHECK-LABEL: func.func @testy2() { +// CHECK: %[[VAL_0:.*]] = arith.constant 4 : i64 +// CHECK: %[[VAL_1:.*]] = arith.constant 48879 : i32 +// CHECK: %[[VAL_2:.*]] = arith.constant 42 : i32 +// CHECK: %[[VAL_3:.*]] = arith.constant 51966 : i32 +// CHECK: %[[VAL_4:.*]] = arith.constant 14581 : i32 +// CHECK: %[[VAL_5:.*]] = cc.alloca !cc.array +// CHECK: %[[VAL_6:.*]] = cc.cast %[[VAL_5]] : (!cc.ptr>) -> !cc.ptr +// CHECK: cc.store %[[VAL_4]], %[[VAL_6]] : !cc.ptr +// CHECK: %[[VAL_7:.*]] = cc.compute_ptr %[[VAL_5]][1] : (!cc.ptr>) -> !cc.ptr +// CHECK: cc.store %[[VAL_3]], %[[VAL_7]] : !cc.ptr +// CHECK: %[[VAL_8:.*]] = cc.compute_ptr %[[VAL_5]][2] : (!cc.ptr>) -> !cc.ptr +// CHECK: cc.store %[[VAL_2]], %[[VAL_8]] : !cc.ptr +// CHECK: %[[VAL_9:.*]] = cc.compute_ptr %[[VAL_5]][3] : (!cc.ptr>) -> !cc.ptr +// CHECK: cc.store %[[VAL_1]], %[[VAL_9]] : !cc.ptr +// CHECK: %[[VAL_10:.*]] = cc.stdvec_init %[[VAL_5]], %[[VAL_0]] : (!cc.ptr>, i64) -> !cc.stdvec +// CHECK: call @callee2(%[[VAL_10]]) : (!cc.stdvec) -> () +// CHECK: return +// CHECK: } + +func.func private @callee3(!cc.struct<{i32, f64, i8, i16}>) +func.func @testy3(%arg0: !cc.struct<{i32, f64, i8, i16}>) { + call @callee3(%arg0) : (!cc.struct<{i32, f64, i8, i16}>) -> () + return +} + +// CHECK-LABEL: func.func @testy3() { +// CHECK: %[[VAL_0:.*]] = arith.constant -1314 : i16 +// CHECK: %[[VAL_1:.*]] = arith.constant 65 : i8 +// CHECK: %[[VAL_2:.*]] = arith.constant 87.654499999999998 : f64 +// CHECK: %[[VAL_3:.*]] = arith.constant -889275714 : i32 +// CHECK: %[[VAL_4:.*]] = cc.undef !cc.struct<{i32, f64, i8, i16}> +// CHECK: %[[VAL_5:.*]] = cc.insert_value %[[VAL_3]], %[[VAL_4]][0] : (!cc.struct<{i32, f64, i8, i16}>, i32) -> !cc.struct<{i32, f64, i8, i16}> +// CHECK: %[[VAL_6:.*]] = cc.insert_value %[[VAL_2]], %[[VAL_5]][1] : (!cc.struct<{i32, f64, i8, i16}>, f64) -> !cc.struct<{i32, f64, i8, i16}> +// CHECK: %[[VAL_7:.*]] = cc.insert_value %[[VAL_1]], %[[VAL_6]][2] : (!cc.struct<{i32, f64, i8, i16}>, i8) -> !cc.struct<{i32, f64, i8, i16}> +// CHECK: %[[VAL_8:.*]] = cc.insert_value %[[VAL_0]], %[[VAL_7]][3] : (!cc.struct<{i32, f64, i8, i16}>, i16) -> !cc.struct<{i32, f64, i8, i16}> +// CHECK: call @callee3(%[[VAL_8]]) : (!cc.struct<{i32, f64, i8, i16}>) -> () +// CHECK: return +// CHECK: } + +func.func private @callee4(!cc.stdvec>) +func.func @testy4(%arg0: !cc.stdvec>) { + call @callee4(%arg0) : (!cc.stdvec>) -> () + return +} + +// CHECK-LABEL: func.func @testy4() { +// CHECK: %[[VAL_0:.*]] = arith.constant 3 : i64 +// CHECK: %[[VAL_1:.*]] = arith.constant 747 : i16 +// CHECK: %[[VAL_2:.*]] = arith.constant 67 : i8 +// CHECK: %[[VAL_3:.*]] = arith.constant 782934.78922999999 : f64 +// CHECK: %[[VAL_4:.*]] = arith.constant 90210 : i32 +// CHECK: %[[VAL_5:.*]] = arith.constant 2778 : i16 +// CHECK: %[[VAL_6:.*]] = arith.constant 66 : i8 +// CHECK: %[[VAL_7:.*]] = arith.constant 2.389450e+04 : f64 +// CHECK: %[[VAL_8:.*]] = arith.constant 5412 : i32 +// CHECK: %[[VAL_9:.*]] = arith.constant -1314 : i16 +// CHECK: %[[VAL_10:.*]] = arith.constant 65 : i8 +// CHECK: %[[VAL_11:.*]] = arith.constant 87.654499999999998 : f64 +// CHECK: %[[VAL_12:.*]] = arith.constant -889275714 : i32 +// CHECK: %[[VAL_13:.*]] = cc.alloca !cc.array x 3> +// CHECK: %[[VAL_14:.*]] = cc.undef !cc.struct<{i32, f64, i8, i16}> +// CHECK: %[[VAL_15:.*]] = cc.insert_value %[[VAL_12]], %[[VAL_14]][0] : (!cc.struct<{i32, f64, i8, i16}>, i32) -> !cc.struct<{i32, f64, i8, i16}> +// CHECK: %[[VAL_16:.*]] = cc.insert_value %[[VAL_11]], %[[VAL_15]][1] : (!cc.struct<{i32, f64, i8, i16}>, f64) -> !cc.struct<{i32, f64, i8, i16}> +// CHECK: %[[VAL_17:.*]] = cc.insert_value %[[VAL_10]], %[[VAL_16]][2] : (!cc.struct<{i32, f64, i8, i16}>, i8) -> !cc.struct<{i32, f64, i8, i16}> +// CHECK: %[[VAL_18:.*]] = cc.insert_value %[[VAL_9]], %[[VAL_17]][3] : (!cc.struct<{i32, f64, i8, i16}>, i16) -> !cc.struct<{i32, f64, i8, i16}> +// CHECK: %[[VAL_19:.*]] = cc.cast %[[VAL_13]] : (!cc.ptr x 3>>) -> !cc.ptr> +// CHECK: cc.store %[[VAL_18]], %[[VAL_19]] : !cc.ptr> +// CHECK: %[[VAL_20:.*]] = cc.undef !cc.struct<{i32, f64, i8, i16}> +// CHECK: %[[VAL_21:.*]] = cc.insert_value %[[VAL_8]], %[[VAL_20]][0] : (!cc.struct<{i32, f64, i8, i16}>, i32) -> !cc.struct<{i32, f64, i8, i16}> +// CHECK: %[[VAL_22:.*]] = cc.insert_value %[[VAL_7]], %[[VAL_21]][1] : (!cc.struct<{i32, f64, i8, i16}>, f64) -> !cc.struct<{i32, f64, i8, i16}> +// CHECK: %[[VAL_23:.*]] = cc.insert_value %[[VAL_6]], %[[VAL_22]][2] : (!cc.struct<{i32, f64, i8, i16}>, i8) -> !cc.struct<{i32, f64, i8, i16}> +// CHECK: %[[VAL_24:.*]] = cc.insert_value %[[VAL_5]], %[[VAL_23]][3] : (!cc.struct<{i32, f64, i8, i16}>, i16) -> !cc.struct<{i32, f64, i8, i16}> +// CHECK: %[[VAL_25:.*]] = cc.compute_ptr %[[VAL_13]][1] : (!cc.ptr x 3>>) -> !cc.ptr> +// CHECK: cc.store %[[VAL_24]], %[[VAL_25]] : !cc.ptr> +// CHECK: %[[VAL_26:.*]] = cc.undef !cc.struct<{i32, f64, i8, i16}> +// CHECK: %[[VAL_27:.*]] = cc.insert_value %[[VAL_4]], %[[VAL_26]][0] : (!cc.struct<{i32, f64, i8, i16}>, i32) -> !cc.struct<{i32, f64, i8, i16}> +// CHECK: %[[VAL_28:.*]] = cc.insert_value %[[VAL_3]], %[[VAL_27]][1] : (!cc.struct<{i32, f64, i8, i16}>, f64) -> !cc.struct<{i32, f64, i8, i16}> +// CHECK: %[[VAL_29:.*]] = cc.insert_value %[[VAL_2]], %[[VAL_28]][2] : (!cc.struct<{i32, f64, i8, i16}>, i8) -> !cc.struct<{i32, f64, i8, i16}> +// CHECK: %[[VAL_30:.*]] = cc.insert_value %[[VAL_1]], %[[VAL_29]][3] : (!cc.struct<{i32, f64, i8, i16}>, i16) -> !cc.struct<{i32, f64, i8, i16}> +// CHECK: %[[VAL_31:.*]] = cc.compute_ptr %[[VAL_13]][2] : (!cc.ptr x 3>>) -> !cc.ptr> +// CHECK: cc.store %[[VAL_30]], %[[VAL_31]] : !cc.ptr> +// CHECK: %[[VAL_32:.*]] = cc.stdvec_init %[[VAL_13]], %[[VAL_0]] : (!cc.ptr x 3>>, i64) -> !cc.stdvec> +// CHECK: call @callee4(%[[VAL_32]]) : (!cc.stdvec>) -> () +// CHECK: return +// CHECK: } diff --git a/test/Quake/kernel_exec-1.qke b/test/Quake/kernel_exec-1.qke index 4b0bbf3c279..b0287073a7f 100644 --- a/test/Quake/kernel_exec-1.qke +++ b/test/Quake/kernel_exec-1.qke @@ -7,6 +7,7 @@ // ========================================================================== // // RUN: cudaq-opt --kernel-execution %s | FileCheck %s +// RUN: cudaq-opt --kernel-execution=alt-launch=2 %s | FileCheck --check-prefix=ALT2 %s module attributes {quake.mangled_name_map = { __nvqpp__mlirgen__ghz = "_ZN3ghzclEi"}} { @@ -157,3 +158,37 @@ module attributes {quake.mangled_name_map = { // CHECK: func.call @cudaqRegisterArgsCreator(%[[VAL_1]], %[[VAL_3]]) : (!cc.ptr, !cc.ptr) -> () // CHECK: llvm.return // CHECK: } + + +// ALT2-LABEL: func.func @_ZN3ghzclEi( +// ALT2-SAME: %[[VAL_0:.*]]: !cc.ptr, %[[VAL_1:.*]]: i32) -> f64 { +// ALT2: %[[VAL_2:.*]] = cc.alloca !cc.struct<{!cc.ptr>, !cc.ptr>, !cc.ptr>}> +// ALT2: %[[VAL_3:.*]] = cc.alloca !cc.array x 1> +// ALT2: %[[VAL_4:.*]] = cc.sizeof !cc.array x 1> : i64 +// ALT2: %[[VAL_5:.*]] = cc.cast %[[VAL_3]] : (!cc.ptr x 1>>) -> !cc.ptr> +// ALT2: %[[VAL_6:.*]] = cc.cast %[[VAL_2]] : (!cc.ptr>, !cc.ptr>, !cc.ptr>}>>) -> !cc.ptr>> +// ALT2: cc.store %[[VAL_5]], %[[VAL_6]] : !cc.ptr>> +// ALT2: %[[VAL_7:.*]] = cc.cast %[[VAL_3]] : (!cc.ptr x 1>>) -> i64 +// ALT2: %[[VAL_8:.*]] = arith.addi %[[VAL_7]], %[[VAL_4]] : i64 +// ALT2: %[[VAL_9:.*]] = cc.cast %[[VAL_8]] : (i64) -> !cc.ptr> +// ALT2: %[[VAL_10:.*]] = cc.compute_ptr %[[VAL_2]][1] : (!cc.ptr>, !cc.ptr>, !cc.ptr>}>>) -> !cc.ptr>> +// ALT2: cc.store %[[VAL_9]], %[[VAL_10]] : !cc.ptr>> +// ALT2: %[[VAL_11:.*]] = cc.compute_ptr %[[VAL_2]][2] : (!cc.ptr>, !cc.ptr>, !cc.ptr>}>>) -> !cc.ptr>> +// ALT2: cc.store %[[VAL_9]], %[[VAL_11]] : !cc.ptr>> +// ALT2: %[[VAL_12:.*]] = arith.constant 0 : i64 +// ALT2: %[[VAL_13:.*]] = cc.cast %[[VAL_12]] : (i64) -> !cc.ptr +// ALT2: %[[VAL_14:.*]] = cc.compute_ptr %[[VAL_3]][0] : (!cc.ptr x 1>>) -> !cc.ptr> +// ALT2: %[[VAL_15:.*]] = cc.alloca i32 +// ALT2: cc.store %[[VAL_1]], %[[VAL_15]] : !cc.ptr +// ALT2: %[[VAL_16:.*]] = cc.cast %[[VAL_15]] : (!cc.ptr) -> !cc.ptr +// ALT2: cc.store %[[VAL_16]], %[[VAL_14]] : !cc.ptr> +// ALT2: %[[VAL_17:.*]] = cc.alloca !cc.ptr +// ALT2: cc.store %[[VAL_13]], %[[VAL_17]] : !cc.ptr> +// ALT2: %[[VAL_18:.*]] = cc.cast %[[VAL_17]] : (!cc.ptr>) -> !cc.ptr +// ALT2: %[[VAL_19:.*]] = cc.cast %[[VAL_2]] : (!cc.ptr>, !cc.ptr>, !cc.ptr>}>>) -> !cc.ptr +// ALT2: %[[VAL_20:.*]] = llvm.mlir.addressof @ghz.kernelName : !llvm.ptr> +// ALT2: %[[VAL_21:.*]] = cc.cast %[[VAL_20]] : (!llvm.ptr>) -> !cc.ptr +// ALT2: call @altLaunchKernelUsingLocalJIT(%[[VAL_21]], %[[VAL_19]], %[[VAL_18]]) : (!cc.ptr, !cc.ptr, !cc.ptr) -> () +// ALT2: %[[VAL_22:.*]] = cc.undef f64 +// ALT2: return %[[VAL_22]] : f64 +// ALT2: } diff --git a/test/Quake/wires_to_wireset.qke b/test/Quake/wires_to_wireset.qke new file mode 100644 index 00000000000..e3522436c2a --- /dev/null +++ b/test/Quake/wires_to_wireset.qke @@ -0,0 +1,42 @@ +// ========================================================================== // +// Copyright (c) 2022 - 2024 NVIDIA Corporation & Affiliates. // +// All rights reserved. // +// // +// This source code and the accompanying materials are made available under // +// the terms of the Apache License 2.0 which accompanies this distribution. // +// ========================================================================== // + +// RUN: cudaq-opt --add-wireset --assign-wire-indices %s | FileCheck %s + +func.func @__nvqpp__mlirgen__run_test() attributes {"cudaq-entrypoint", "cudaq-kernel"} { + %0 = quake.null_wire + %1 = quake.h %0 : (!quake.wire) -> !quake.wire + %measOut, %wires = quake.mz %1 : (!quake.wire) -> (!quake.measure, !quake.wire) + %2 = quake.discriminate %measOut : (!quake.measure) -> i1 + cc.if(%2) { + %3 = quake.null_wire + %4 = quake.h %3 : (!quake.wire) -> !quake.wire + %measOut_0, %wires_1 = quake.mz %4 : (!quake.wire) -> (!quake.measure, !quake.wire) + quake.sink %wires_1 : !quake.wire + } else { + } + quake.sink %wires : !quake.wire + return +} + +// CHECK-LABEL: quake.wire_set @wires[2147483647] + +// CHECK-LABEL: func.func @__nvqpp__mlirgen__run_test() attributes {"cudaq-entrypoint", "cudaq-kernel"} { +// CHECK: %[[VAL_0:.*]] = quake.borrow_wire @wires[0] : !quake.wire +// CHECK: %[[VAL_1:.*]] = quake.h %[[VAL_0]] : (!quake.wire) -> !quake.wire +// CHECK: %[[VAL_2:.*]], %[[VAL_3:.*]] = quake.mz %[[VAL_1]] : (!quake.wire) -> (!quake.measure, !quake.wire) +// CHECK: %[[VAL_4:.*]] = quake.discriminate %[[VAL_2]] : (!quake.measure) -> i1 +// CHECK: cc.if(%[[VAL_4]]) { +// CHECK: %[[VAL_5:.*]] = quake.borrow_wire @wires[1] : !quake.wire +// CHECK: %[[VAL_6:.*]] = quake.h %[[VAL_5]] : (!quake.wire) -> !quake.wire +// CHECK: %[[VAL_7:.*]], %[[VAL_8:.*]] = quake.mz %[[VAL_6]] : (!quake.wire) -> (!quake.measure, !quake.wire) +// CHECK: quake.return_wire %[[VAL_8]] : !quake.wire +// CHECK: } +// CHECK: quake.return_wire %[[VAL_3]] : !quake.wire +// CHECK: return +// CHECK: } diff --git a/unittests/integration/builder_tester.cpp b/unittests/integration/builder_tester.cpp index 9cde815c1b0..d15e02e8f29 100644 --- a/unittests/integration/builder_tester.cpp +++ b/unittests/integration/builder_tester.cpp @@ -1320,7 +1320,10 @@ TEST(BuilderTester, checkFromStateVector) { EXPECT_EQ(counts.size(), 1); EXPECT_EQ(counts.count("110"), 1000); } +} +TEST(BuilderTester, checkFromState) { + std::vector vec{M_SQRT1_2, 0., 0., M_SQRT1_2}; { // qalloc with state auto kernel = cudaq::make_kernel(); @@ -1340,6 +1343,26 @@ TEST(BuilderTester, checkFromStateVector) { } EXPECT_EQ(counter, 1000); } + + { + // qalloc with state passed by argument + auto kernel = cudaq::make_kernel(); + auto qubits = kernel.qalloc(2); + kernel.h(qubits[0]); + auto state = cudaq::get_state(kernel); + // Send on the state to the next kernel + auto [anotherOne, s] = cudaq::make_kernel(); + auto newQubits = anotherOne.qalloc(s); + anotherOne.x(newQubits[0], newQubits[1]); + std::cout << anotherOne << "\n"; + auto counts = cudaq::sample(anotherOne, &state); + std::size_t counter = 0; + for (auto &[k, v] : counts) { + counter += v; + EXPECT_TRUE(k == "00" || k == "11"); + } + EXPECT_EQ(counter, 1000); + } } CUDAQ_TEST(BuilderTester, checkCanProgressivelyBuild) {