├── .gitignore ├── .readthedocs.yaml ├── .vscode └── settings.json ├── LICENSE.txt ├── README.md ├── __init__.py ├── build └── lib │ └── quask │ ├── __init__.py │ ├── core │ ├── __init__.py │ ├── ansatz.py │ ├── kernel.py │ ├── kernel_factory.py │ ├── kernel_type.py │ └── operation.py │ ├── core_implementation │ ├── __init__.py │ ├── braket_kernel.py │ ├── pennylane_kernel.py │ ├── qibo_kernel.py │ └── qiskit_kernel.py │ ├── evaluator │ ├── __init__.py │ ├── centered_kernel_alignment_evaluator.py │ ├── covering_number_evaluator.py │ ├── ess_model_complexity_evaluator.py │ ├── geometric_difference_evaluator.py │ ├── haar_evaluator.py │ ├── kernel_alignment_evaluator.py │ ├── kernel_evaluator.py │ ├── lie_rank_evaluator.py │ ├── ridge_generalization_evaluator.py │ └── spectral_bias_evaluator.py │ ├── optimizer │ ├── __init__.py │ ├── base_kernel_optimizer.py │ ├── bayesian_optimizer.py │ ├── greedy_optimizer.py │ ├── metaheuristic_optimizer.py │ ├── reinforcement_learning_optimizer.py │ └── wide_kernel_environment.py │ └── tests │ ├── __init__.py │ └── kernel_test.py ├── dist ├── quask-1.0.2-py3-none-any.whl ├── quask-1.0.2.tar.gz ├── quask-2.0.0a1-py3-none-any.whl ├── quask-2.0.0a1.tar.gz ├── quask-2.0.1a2-py3-none-any.whl └── quask-2.0.1a2.tar.gz ├── docs └── source │ ├── notebooks │ ├── getting_started.ipynb │ ├── quantum_0_intro.ipynb │ ├── quantum_1_expressibility.ipynb │ ├── quantum_2_projected.ipynb │ ├── quantum_2_projected_1.ipynb │ ├── quask_0_backends.ipynb │ └── quask_1_evaluators.ipynb │ └── tutorials_quantum │ ├── quantum_0_intro.rst │ └── quantum_2_projected.rst ├── setup.cfg ├── setup.py ├── src ├── quask.egg-info │ ├── PKG-INFO │ ├── SOURCES.txt │ ├── dependency_links.txt │ ├── not-zip-safe │ ├── requires.txt │ └── top_level.txt └── quask │ ├── __init__.py │ ├── core │ ├── __init__.py │ ├── ansatz.py │ ├── kernel.py │ ├── kernel_factory.py │ ├── kernel_type.py │ └── operation.py │ ├── core_implementation │ ├── __init__.py │ ├── braket_kernel.py │ ├── pennylane_kernel.py │ ├── qibo_kernel.py │ └── qiskit_kernel.py │ ├── evaluator │ ├── __init__.py │ ├── centered_kernel_alignment_evaluator.py │ ├── covering_number_evaluator.py │ ├── ess_model_complexity_evaluator.py │ ├── geometric_difference_evaluator.py │ ├── haar_evaluator.py │ ├── kernel_alignment_evaluator.py │ ├── kernel_evaluator.py │ ├── lie_rank_evaluator.py │ ├── ridge_generalization_evaluator.py │ └── spectral_bias_evaluator.py │ ├── optimizer │ ├── __init__.py │ ├── base_kernel_optimizer.py │ ├── bayesian_optimizer.py │ ├── greedy_optimizer.py │ ├── metaheuristic_optimizer.py │ ├── reinforcement_learning_optimizer.py │ └── wide_kernel_environment.py │ └── tests │ ├── __init__.py │ └── kernel_test.py └── tests ├── __init__.py ├── kernel_test.py ├── test_example.py └── test_pennylane_kernel.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | dist/* 3 | build/* 4 | docs/build/* 5 | docs/source/notebooks/.ipynb_checkpoints/* 6 | src/quask/__pycache__/* 7 | src/quask/core/__pycache__/* 8 | src/quask/core_implementation/__pycache__/* 9 | src/quask/evaluator/__pycache__/* 10 | src/quask/optimizer/__pycache__/* 11 | .ipynb_checkpoints/* 12 | .vs_code/* 13 | .vscode/* -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file for Sphinx projects 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Set the OS, Python version and other tools you might need 8 | build: 9 | os: ubuntu-22.04 10 | tools: 11 | python: "3.10" 12 | # You can also specify other tool versions: 13 | # nodejs: "20" 14 | # rust: "1.70" 15 | # golang: "1.20" 16 | 17 | # Build documentation in the "docs/" directory with Sphinx 18 | sphinx: 19 | configuration: docs/source/conf.py 20 | # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs 21 | # builder: "dirhtml" 22 | # Fail on all warnings to avoid broken references 23 | # fail_on_warning: true 24 | 25 | # Optionally build your docs in additional formats such as PDF and ePub 26 | # formats: 27 | # - pdf 28 | # - epub 29 | 30 | # Optional but recommended, declare the Python requirements required 31 | # to build your documentation 32 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 33 | python: 34 | install: 35 | - requirements: docs/requirements.txt 36 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "esbonio.sphinx.confDir": "", 3 | "restructuredtext.syntaxHighlighting.disabled": true 4 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Quantum Advantage Seeker with Kernels (QuASK) 2 | 3 | QuASK is an actively maintained library for constructing, studying, and benchmarking quantum kernel methods. 4 | 5 | It is designed to simplify the process of choosing a quantum kernel, automate the machine learning pipeline at all its stages, and provide pedagogical guidance for early-stage researchers to utilize these tools to their full potential. 6 | 7 | QuASK promotes the use of reusable code and is available as a library that can be seamlessly integrated into existing code bases. It is written in Python 3, can be easily installed using pip, and is accessible on PyPI. 8 | 9 | 10 | *Homepage*: [quask.web.cern.ch](https://quask.web.cern.ch/) 11 | 12 | *Documentation*: [quask.readthedocs.io](https://quask.readthedocs.io/en/latest/) 13 | 14 | ## Installation 15 | 16 | The easiest way to use *quask* is by installing it in your Python3 17 | environment (version >= 3.10) via the *pip* packet manager, 18 | 19 | python3 -m pip install -U quask==2.0.0-alpha1 20 | 21 | You also need any quantum SDK installed on your system. For example, we can install Qiskit (but we can also work with Pennylane, Braket, Qibo, and the modular nature of the software allows the creation of your own custom backends). 22 | 23 | python3 -m pip install qiskit qiskit_ibm_runtime 24 | python3 -m pip install qiskit_ibm_runtime --upgrade 25 | python3 -m pip install qiskit-aer 26 | 27 | See the [Installation section](https://quask.readthedocs.io/en/latest/installation.html) 28 | of our documentation page for more information. 29 | 30 | ## Examples 31 | 32 | The fastest way to start developing using _quask_ is via our [Getting started](https://quask.readthedocs.io/en/latest/getting_started.html) guide. 33 | 34 | If you are not familiar with the concept of kernel methods in classical machine learning, we have developed a [series of introductory tutorials](https://quask.readthedocs.io/en/latest/tutorials_classical/index.html) on the topic. 35 | 36 | If you are not familiar with the concept of quantum kernels, we have developed a [series of introductory tutorials](https://quask.readthedocs.io/en/latest/tutorials_quantum/index.html) on the topic, which is also used to showcase the basic functionalities of _quask_. 37 | 38 | Then [advanced features of _quask_](https://quask.readthedocs.io/en/latest/tutorials_quask/index.html) are shown, including the use of different backends, the criteria to evaluate a quantum kernel, and the automatic optimization approach. 39 | 40 | Finally, [look here for some applications](https://quask.readthedocs.io/en/latest/tutorials_applications/index.html). 41 | 42 | 43 | ## Source 44 | 45 | 46 | ### Deployment to PyPI 47 | 48 | The software is uploaded to [PyPI](https://pypi.org/project/quask/). 49 | 50 | ### Test 51 | 52 | The suite of test for _quask_ is currently under development.To run the available tests, type 53 | 54 | pytest 55 | 56 | 57 | You can also specify specific test scripts. 58 | 59 | pytest tests/test_example.py 60 | 61 | _quask_ has been developed and tested with the following versions of the quantum frameworks: 62 | 63 | * PennyLane==0.32.0 64 | * PennyLane-Lightning==0.32.0 65 | * qiskit==0.44.1 66 | * qiskit-aer==0.12.2 67 | * qiskit-ibm-runtime==0.14.0 68 | 69 | 70 | ## Documentation 71 | 72 | The documentation is available at our [Read the Docs](https://quask.readthedocs.io/en/latest/) domain. 73 | 74 | ### Generate the documentation 75 | 76 | The documentation has been generated with Sphinx (v7.2.6) and uses the Furo theme. To install it, run 77 | 78 | python3 -m pip install -U sphinx 79 | python3 -m pip install furo 80 | 81 | To generate the documentation, run 82 | 83 | cd docs 84 | make clean && make html 85 | 86 | The Sphinx configuration file (`conf.py`) has the following, non-standard options: 87 | 88 | html_theme = 'furo' 89 | html_theme_options = { 90 | "sidebar_hide_name": True 91 | } 92 | autodoc_mock_imports = ["skopt", "skopt.space", "django", "mushroom_rl", "opytimizer", "pennylane", "qiskit", "qiskit_ibm_runtime", "qiskit_aer"] 93 | 94 | ### Generate the UML diagrams 95 | 96 | Currently, the pages generated from the Python notebooks has to be compiled to RST format manually. We could use in the future the [nbsphinx extension](https://docs.readthedocs.io/en/stable/guides/jupyter.html) to automatize this process. This has the advantage that the documentation is always up to date, the disadvantage is that the process is much slower. 97 | 98 | ### Generate the UML diagrams 99 | 100 | The UML diagrams in the [Platform overview](https://quask.readthedocs.io/en/latest/platform_overview.html) page of the documentation are generated using pyreverse and Graphviz. They can be installed via: 101 | 102 | sudo apt-get install graphviz 103 | python3 -m pip install pylint 104 | 105 | The UML diagrams are created via: 106 | 107 | cd src/quask 108 | pyreverse -o png -p QUASK . 109 | 110 | 111 | ## Acknowledgements 112 | 113 | The platform has been developed with the contribution of [Massimiliano Incudini](https://incud.github.io), Francesco Di Marcantonio, Davide Tezza, Roman Wixinger, Sofia Vallecorsa, and [Michele Grossi](https://scholar.google.com/citations?user=cnfcO7cAAAAJ&hl=en). 114 | 115 | If you have used _quask_ for your project, please consider citing us. 116 | 117 | @article{dimarcantonio2023quask, 118 | title={Quantum Advantage Seeker with Kernels (QuASK): a software framework to accelerate research in quantum machine learning}, 119 | author={Di Marcantonio, Francesco and Incudini, Massimiliano and Tezza, Davide and Grossi, Michele}, 120 | journal={Quantum Machine Intelligence}, 121 | volume={5}, 122 | number={1}, 123 | pages={20}, 124 | year={2023}, 125 | publisher={Springer} 126 | } 127 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CERN-IT-INNOVATION/QuASK/3ac5bef5aff678e2239af4207ed9e89b8a0668b9/__init__.py -------------------------------------------------------------------------------- /build/lib/quask/__init__.py: -------------------------------------------------------------------------------- 1 | from . import datasets 2 | from . import metrics 3 | from . import kernels 4 | -------------------------------------------------------------------------------- /build/lib/quask/core/__init__.py: -------------------------------------------------------------------------------- 1 | from .operation import Operation 2 | from .ansatz import Ansatz 3 | from .kernel_type import KernelType 4 | from .kernel_factory import KernelFactory 5 | from .kernel import Kernel 6 | -------------------------------------------------------------------------------- /build/lib/quask/core/ansatz.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from typing import List 3 | from . import Operation 4 | 5 | 6 | class Ansatz: 7 | """ 8 | Class representing the Ansatz as list of Operations 9 | """ 10 | 11 | def __init__(self, n_features: int, n_qubits: int, n_operations: int, allow_midcircuit_measurement=False): 12 | """ 13 | Initialization 14 | :param n_features: number of feature that can be used to parametrize the operation 15 | :param n_qubits: number of qubits of the circuit 16 | :param n_operations: number of operations 17 | :param allow_midcircuit_measurement: True if mid-circuit measurement are allowed 18 | """ 19 | assert n_qubits >= 2, "This ansatz is specified for >= 2 qubits" 20 | assert n_features > 0, "Cannot have zero or negative number of features" 21 | assert n_operations > 0, "Cannot have zero or negative number or operations" 22 | self.n_features: int = n_features 23 | self.n_qubits: int = n_qubits 24 | self.n_operations: int = n_operations 25 | self.operation_list: List[Operation] = [None] * n_operations 26 | self.allow_midcircuit_measurement: bool = allow_midcircuit_measurement 27 | 28 | def change_operation(self, operation_index: int, new_feature: int, new_wires: List[int], new_generator: str, new_bandwidth: float): 29 | """ 30 | Overwrite the operation at the given index with a whole new set of data 31 | :param operation_index: index of the operation 32 | :param new_feature: feature parameterizing the operation 33 | :param new_wires: wires on which the operation is applied 34 | :param new_generator: generator of the operation 35 | :param new_bandwidth: bandwidth of the operation 36 | :return: None 37 | """ 38 | self.change_feature(operation_index, new_feature) 39 | self.change_wires(operation_index, new_wires) 40 | self.change_generators(operation_index, new_generator) 41 | self.change_bandwidth(operation_index, new_bandwidth) 42 | 43 | def change_bandwidth(self, operation_index: int, new_bandwidth: float): 44 | """ 45 | Overwrite the operation at the given index with a new bandwidth 46 | :param operation_index: index of the operation 47 | :param new_bandwidth: bandwidth of the operation 48 | :return: None 49 | """ 50 | assert 0 <= operation_index < self.n_operations, "Operation index out of bounds" 51 | self.operation_list[operation_index].bandwidth = new_bandwidth 52 | 53 | def change_generators(self, operation_index: int, new_generator: str): 54 | """ 55 | Overwrite the operation at the given index with a new generator 56 | :param operation_index: index of the operation 57 | :param new_generator: generator of the operation 58 | :return: None 59 | """ 60 | assert 0 <= operation_index < self.n_operations, "Operation index out of bounds" 61 | assert new_generator in Operation.OPERATIONS, f"Unknown generator {new_generator}" 62 | if not self.allow_midcircuit_measurement: 63 | assert new_generator not in Operation.MEASUREMENT_OPERATIONS, "Mid-circuit measurement not allowed" 64 | self.operation_list[operation_index].generator = new_generator 65 | 66 | def change_feature(self, operation_index: int, new_feature: int): 67 | """ 68 | Overwrite the operation at the given index with a new feature 69 | :param operation_index: index of the operation 70 | :param new_feature: feature parameterizing the operation 71 | :return: None 72 | """ 73 | assert 0 <= operation_index < self.n_operations, "Operation index out of bounds" 74 | assert 0 <= new_feature <= self.n_features, f"Feature index out of bounds ({new_feature=})" 75 | self.operation_list[operation_index].feature = new_feature 76 | 77 | def change_wires(self, operation_index: int, new_wires: List[int]): 78 | """ 79 | Overwrite the operation at the given index with a new pair of wires 80 | :param operation_index: index of the operation 81 | :param new_wires: wires on which the operation is applied 82 | :return: None 83 | """ 84 | assert 0 <= operation_index < self.n_operations, "Operation index out of bounds" 85 | assert len(new_wires) == 2, "The location is a list of two integers, not less no more" 86 | assert 0 <= new_wires[0] < self.n_qubits, f"First wire index out of bounds ({new_wires=})" 87 | assert 0 <= new_wires[1] < self.n_qubits, f"Second wire index out of bounds ({new_wires=})" 88 | assert new_wires[0] != new_wires[1], f"Cannot specify the same wire twice ({new_wires=})" 89 | self.operation_list[operation_index].wires = new_wires 90 | 91 | def get_allowed_operations(self): 92 | """ 93 | Get the list of allowed operation for the ansatz, either only the PAULI_GENERATORS or any operation including measurements 94 | :return: list of allowed operations 95 | """ 96 | if self.allow_midcircuit_measurement: 97 | return Operation.OPERATIONS 98 | else: 99 | return Operation.PAULI_GENERATORS 100 | 101 | def initialize_to_identity(self): 102 | """ 103 | Initialize the ansatz to the identity circuit 104 | :return: None 105 | """ 106 | self.operation_list = [None] * self.n_operations 107 | for i in range(self.n_operations): 108 | self.operation_list[i] = Operation("II", [0, 1], -1, 1) 109 | 110 | def initialize_to_random_circuit(self): 111 | """ 112 | Initialize the ansatz to a random circuit 113 | :return: None 114 | """ 115 | for i in range(self.n_operations): 116 | generator = np.random.choice(self.get_allowed_operations()) 117 | wires = np.random.choice(list(range(self.n_qubits)), 2, replace=False) 118 | feature = np.random.choice(list(range(self.n_features + 1))) 119 | bandwidth = np.random.uniform(0.0, 1.0) 120 | self.operation_list[i] = Operation(generator, wires, feature, bandwidth) 121 | 122 | def initialize_to_known_ansatz(self, ansatz): 123 | """ 124 | Initialize the ansatz form an already given element 125 | :param ansatz: Given ansatz 126 | :return: None 127 | """ 128 | self.initialize_to_identity() 129 | for i in range(self.n_operations): 130 | op: Operation = ansatz.operation_list[i] 131 | self.change_operation(i, op.feature, op.wires, op.generator, op.bandwidth) 132 | 133 | def to_numpy(self): 134 | """ 135 | Serialize the ansatz to a numpy array 136 | :return: numpy array 137 | """ 138 | return np.array([op.to_numpy() for op in self.operation_list]).ravel() 139 | 140 | @staticmethod 141 | def from_numpy(array, n_features, n_qubits, n_operations, allow_midcircuit_measurement, shift_second_wire=False): 142 | """ 143 | Deserialize the ansatz from a numpy array 144 | :param array: numpy array 145 | :param n_features: number of feature that can be used to parametrize the operation 146 | :param n_qubits: number of qubits of the circuit 147 | :param n_operations: number of operations 148 | :param allow_midcircuit_measurement: True if mid-circuit measurement are allowed 149 | :return: Ansatz deserialized 150 | """ 151 | ans = Ansatz(n_features, n_qubits, n_operations, allow_midcircuit_measurement) 152 | ans.initialize_to_identity() 153 | for i in range(n_operations): 154 | # feature -> wires -> generator -> bandwidth 155 | generator = np.rint(array[i * 5]).astype(int) 156 | wires = [np.rint(array[i * 5 + 1]).astype(int), np.rint(array[i * 5 + 2]).astype(int)] 157 | feature = np.rint(array[i * 5 + 3]).astype(int) 158 | bandwidth = np.round(array[i * 5 + 4], decimals=4) 159 | if shift_second_wire and wires[1] >= wires[0]: 160 | wires[1] += 1 161 | ans.change_operation(i, feature, wires, Operation.OPERATIONS[generator], bandwidth) 162 | return ans 163 | 164 | def __str__(self): 165 | return str(self.operation_list) 166 | 167 | def __repr__(self): 168 | return self.__str__() -------------------------------------------------------------------------------- /build/lib/quask/core/kernel.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy.spatial.distance import cdist 3 | from abc import ABC, abstractmethod 4 | from . import Ansatz, KernelType, KernelFactory 5 | 6 | 7 | class Kernel(ABC): 8 | """ 9 | Abstract class representing a kernel object 10 | """ 11 | 12 | PAULIS = ['I', 'X', 'Y', 'Z'] 13 | 14 | def __init__(self, ansatz: Ansatz, measurement: str, type: KernelType): 15 | """ 16 | Initialization. 17 | 18 | :param ansatz: Ansatz object representing the unitary transformation 19 | :param measurement: Pauli string representing the measurement 20 | :param type: type of kernel, fidelity or observable 21 | """ 22 | assert ansatz.n_qubits == len(measurement), "Measurement qubits and number of ansatz qubits do not match" 23 | assert len(set(measurement).difference(Kernel.PAULIS)) == 0, "Unknown Pauli in measurement" 24 | self.ansatz = ansatz 25 | self.measurement = measurement 26 | self.type = type 27 | self.last_probabilities = None 28 | 29 | def get_last_probabilities(self): 30 | """ 31 | Get the last kernel value calculated. 32 | 33 | :return: last probability array 34 | """ 35 | return np.array(self.last_probabilities) 36 | 37 | @abstractmethod 38 | def kappa(self, x1, x2) -> float: 39 | """ 40 | Calculate the kernel given two datapoints. 41 | 42 | :param x1: first data point 43 | :param x2: second data point 44 | :return: Kernel similarity between the two data points 45 | """ 46 | pass 47 | 48 | @abstractmethod 49 | def phi(self, x) -> float: 50 | """ 51 | Calculate the feature map of a data point. 52 | 53 | :param x: data point 54 | :return: feature map of the datapoint as numpy array 55 | """ 56 | pass 57 | 58 | def get_allowed_operations(self): 59 | """ 60 | Get the list of allowed operations. 61 | 62 | :return: list of generators allowed (the information is saved in the ansatz) 63 | """ 64 | return self.ansatz.get_allowed_operations() 65 | 66 | def build_kernel(self, X1: np.ndarray, X2: np.ndarray) -> np.ndarray: 67 | """ 68 | Build a kernel. 69 | 70 | :param X1: a single datapoint or a list of datapoints 71 | :param X2: a single datapoint or a list of datapoints 72 | :return: a single or a matrix of kernel inner products 73 | """ 74 | # if you gave me only one sample 75 | if len(X1.shape) == 1 and len(X2.shape) == 1: 76 | if self.type in [KernelType.FIDELITY, KernelType.SWAP_TEST]: 77 | return self.kappa(X1, X2) 78 | else: 79 | return self.phi(X1) * self.phi(X2) 80 | 81 | # if you gave me multiple samples 82 | assert self.ansatz.n_features == X1.shape[1], "Number of features and X1.shape[1] do not match" 83 | assert self.ansatz.n_features == X2.shape[1], "Number of features and X2.shape[1] do not match" 84 | if self.type in [KernelType.FIDELITY, KernelType.SWAP_TEST]: 85 | return cdist(X1, X2, metric=self.kappa) 86 | else: 87 | n = X1.shape[0] 88 | m = X2.shape[0] 89 | Phi1 = np.array([self.phi(x) for x in X1]).reshape((n, 1)) 90 | Phi2 = np.array([self.phi(x) for x in X2]).reshape((m, 1)) 91 | return Phi1.dot(Phi2.T) 92 | 93 | def to_numpy(self): 94 | """ 95 | Serialize the kernel object as a numpy array. 96 | 97 | :return: numpy array 98 | """ 99 | ansatz_numpy = self.ansatz.to_numpy() 100 | measurement_numpy = np.array([Kernel.PAULIS.index(p) for p in self.measurement]) 101 | type_numpy = np.array([self.type.value]) 102 | return np.concatenate([ansatz_numpy, measurement_numpy, type_numpy], dtype=object).ravel() 103 | 104 | @staticmethod 105 | def from_numpy(array, n_features, n_qubits, n_operations, allow_midcircuit_measurement, shift_second_wire=False): 106 | """ 107 | Deserialize the object from a numpy array. 108 | 109 | :param array: numpy array 110 | :param n_features: number of feature that can be used to parametrize the operation 111 | :param n_qubits: number of qubits of the circuit 112 | :param n_operations: number of operations 113 | :param allow_midcircuit_measurement: True if mid-circuit measurement are allowed 114 | :return: Kernel object (created using default instance in KernelFactory) 115 | """ 116 | assert len(array) == 5 * n_operations + n_qubits + 1, f"Size of the array is {len(array)} instead of {5 * n_operations + n_qubits + 1}" 117 | ansatz_numpy = array[:n_operations*5] 118 | measurement_numpy = array[n_operations*5:-1] 119 | type_numpy = array[-1] 120 | ansatz = Ansatz.from_numpy(ansatz_numpy, n_features, n_qubits, n_operations, allow_midcircuit_measurement, shift_second_wire) 121 | measurement = "".join(Kernel.PAULIS[np.rint(i).astype(int)] for i in measurement_numpy) 122 | the_type = KernelType.convert(type_numpy) 123 | kernel = KernelFactory.create_kernel(ansatz, measurement, the_type) 124 | return kernel 125 | 126 | def __str__(self): 127 | return str(self.ansatz) + " -> " + self.measurement 128 | 129 | def __repr__(self): 130 | return self.__str__() 131 | 132 | def __copy__(self): 133 | return Kernel.from_numpy(self.to_numpy(), self.ansatz.n_features, self.ansatz.n_qubits, self.ansatz.n_operations, self.ansatz.allow_midcircuit_measurement) 134 | -------------------------------------------------------------------------------- /build/lib/quask/core/kernel_factory.py: -------------------------------------------------------------------------------- 1 | from . import Ansatz, KernelType 2 | 3 | 4 | class KernelFactory: 5 | """ 6 | Instantiate the concrete object from classes that inherit from (abstract class) Kernel. 7 | Implement the self-registering factory pattern 8 | """ 9 | 10 | __implementations = {} 11 | """Dictionary containing pairs (name, function to create the kernel).""" 12 | 13 | __current_implementation: str = "" 14 | """Name of the implementation to use right now to create the kernels""" 15 | 16 | @staticmethod 17 | def add_implementation(name, fn): 18 | """ 19 | Add the current closure function as one of the possible implementations available 20 | 21 | :param name: name of the implementation 22 | :param fn: function that creates the quantum kernel 23 | """ 24 | if name in KernelFactory.__implementations: 25 | raise ValueError("This name is already present in the register of available implementations") 26 | if fn.__code__.co_argcount != 3: 27 | raise ValueError("The function must have these three arguments, 'ansatz', 'measurement', and 'type': the number of argument does not match") 28 | if fn.__code__.co_varnames != ('ansatz', 'measurement', 'type'): 29 | raise ValueError("The function must have these three arguments, 'ansatz', 'measurement', and 'type': the name of some argument does not match") 30 | KernelFactory.__implementations[name] = fn 31 | 32 | 33 | @staticmethod 34 | def set_current_implementation(name): 35 | if name not in KernelFactory.__implementations: 36 | raise ValueError("This name is not present in the register of available implementations") 37 | KernelFactory.__current_implementation = name 38 | 39 | @staticmethod 40 | def create_kernel(ansatz: Ansatz, measurement: str, type: KernelType): 41 | """ 42 | Create a kernel object using the default class chosen. 43 | 44 | :param ansatz: Ansatz object representing the unitary transformation 45 | :param measurement: Pauli string representing the measurement 46 | :param type: type of kernel, fidelity, swap test or observable 47 | :return: kernel object of the default concrete class 48 | """ 49 | fn = KernelFactory.__implementations[KernelFactory.__current_implementation] 50 | return fn(ansatz, measurement, type) -------------------------------------------------------------------------------- /build/lib/quask/core/kernel_type.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class KernelType(Enum): 5 | """ 6 | Possible types of kernel 7 | """ 8 | FIDELITY = 0 9 | OBSERVABLE = 1 10 | SWAP_TEST = 2 11 | 12 | @staticmethod 13 | def convert(item): 14 | if isinstance(item, KernelType): 15 | return item 16 | elif item < 0.5: 17 | return KernelType.FIDELITY 18 | elif 0.5 <= item < 1.5: 19 | return KernelType.OBSERVABLE 20 | else: 21 | return KernelType.SWAP_TEST 22 | -------------------------------------------------------------------------------- /build/lib/quask/core/operation.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from typing import List 3 | import itertools 4 | 5 | 6 | class Operation: 7 | """ 8 | Class representing a 2-qubit rotational quantum gates $exp(-i \theta \sigma_1 \otimes \sigma_2)$ 9 | """ 10 | 11 | PAULI_GENERATORS = list(a + b for a, b in itertools.product(["I", "X", "Y", "Z"], repeat=2)) 12 | MEASUREMENT_OPERATIONS = ["IM", "MI"] 13 | OPERATIONS = PAULI_GENERATORS + MEASUREMENT_OPERATIONS 14 | 15 | def __init__(self, generator: str, wires: List[int], feature: int, bandwidth: float): 16 | """ 17 | Operation initializer 18 | :param generator: one of the elements of Operation.OPERATIONS 19 | :param wires: pair of integers 20 | :param feature: index of the feature parameterizing the element (can be -1 for constant feature '1') 21 | :param bandwidth: bandwidth parameter in range [0,1] 22 | """ 23 | self.generator: str = generator 24 | self.wires: List[int] = wires 25 | self.feature: int = feature 26 | self.bandwidth: float = bandwidth 27 | 28 | def to_numpy(self): 29 | """ 30 | Serialize the Operation object to a numpy array format 31 | :return: numpy array representing the operation 32 | """ 33 | return np.array([Operation.OPERATIONS.index(self.generator), self.wires[0], self.wires[1], self.feature, self.bandwidth]) 34 | 35 | @staticmethod 36 | def from_numpy(array): 37 | """ 38 | Deserialize the operation object given its numpy array description 39 | :param array: numpy array 40 | :return: Operation object 41 | """ 42 | op = Operation(None, None, None, None) 43 | op.generator = Operation.OPERATIONS[int(array[0])] 44 | op.wires = [int(array[1]), int(array[2])] 45 | op.feature = int(array[3]) 46 | op.bandwidth = float(array[4]) 47 | return op 48 | 49 | def __str__(self): 50 | return f"-i {self.bandwidth:0.2f} * x[{self.feature}] {self.generator}^({self.wires[0]},{self.wires[1]})" 51 | 52 | def __repr__(self): 53 | return self.__str__() -------------------------------------------------------------------------------- /build/lib/quask/core_implementation/__init__.py: -------------------------------------------------------------------------------- 1 | from .pennylane_kernel import PennylaneKernel 2 | from .qiskit_kernel import QiskitKernel 3 | from .braket_kernel import BraketKernel 4 | from .qibo_kernel import QiboKernel 5 | -------------------------------------------------------------------------------- /build/lib/quask/core_implementation/braket_kernel.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pennylane as qml 3 | from ..core import Ansatz, Kernel, KernelType 4 | from .pennylane_kernel import PennylaneKernel 5 | 6 | 7 | class BraketKernel(PennylaneKernel): 8 | 9 | def create_device(self, n_qubits): 10 | return qml.device( 11 | "braket.aws.qubit", 12 | device_arn=self.device_name, 13 | s3_destination_folder=(self.s3_bucket, self.s3_prefix), 14 | wires=n_qubits 15 | ) 16 | 17 | def __init__(self, ansatz: Ansatz, measurement: str, type: KernelType, 18 | device_name: str, s3_bucket: str, s3_prefix: str, n_shots: int = None): 19 | """ 20 | Initialization. 21 | 22 | :param ansatz: Ansatz object representing the unitary transformation 23 | :param measurement: Pauli string representing the measurement 24 | :param type: type of kernel, fidelity or observable 25 | :param device_name: name of the device, 'default.qubit' for noiseless simulation 26 | :param n_shots: number of shots when sampling the solution, None to have infinity 27 | """ 28 | 29 | super().__init__(ansatz, measurement, type, device_name, n_shots) 30 | self.s3_bucket = s3_bucket 31 | self.s3_prefix = s3_prefix 32 | 33 | -------------------------------------------------------------------------------- /build/lib/quask/core_implementation/pennylane_kernel.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pennylane as qml 3 | from ..core import Ansatz, Kernel, KernelType 4 | 5 | 6 | def AnsatzTemplate(ansatz: Ansatz, params: np.ndarray, wires: np.ndarray): 7 | for operation in ansatz.operation_list: 8 | if "M" not in operation.generator: 9 | feature = np.pi if operation.feature == ansatz.n_features else params[operation.feature] 10 | qml.PauliRot(operation.bandwidth * feature, operation.generator, wires=wires[operation.wires]) 11 | elif operation.generator[0] == "M": 12 | qml.measure(wires[operation.wires[0]]) 13 | else: 14 | qml.measure(wires[operation.wires[1]]) 15 | 16 | 17 | def ChangeBasis(measurement: str): 18 | for i, pauli in enumerate(measurement): 19 | if pauli == 'X': 20 | qml.Hadamard(wires=[i]) 21 | elif pauli == 'Y': 22 | qml.S(wires=[i]) 23 | qml.Hadamard(wires=[i]) 24 | 25 | 26 | class PennylaneKernel(Kernel): 27 | 28 | def create_device(self, n_qubits): 29 | return qml.device(self.device_name, wires=n_qubits, shots=self.n_shots) 30 | 31 | def __init__(self, ansatz: Ansatz, measurement: str, type: KernelType, device_name: str = "default.qubit", n_shots: int = None): 32 | """ 33 | Initialization. 34 | 35 | :param ansatz: Ansatz object representing the unitary transformation 36 | :param measurement: Pauli string representing the measurement 37 | :param type: type of kernel, fidelity or observable 38 | :param device_name: name of the device, 'default.qubit' for noiseless simulation 39 | :param n_shots: number of shots when sampling the solution, None to have infinity 40 | """ 41 | 42 | super().__init__(ansatz, measurement, type) 43 | self.device_name = device_name 44 | self.n_shots = n_shots 45 | 46 | dev = self.create_device(self.ansatz.n_qubits) 47 | wires = np.array(list(range(self.ansatz.n_qubits))) 48 | measurement_wires = np.array([i for i in wires if measurement[i] != 'I']) 49 | if len(measurement_wires) == 0: 50 | measurement_wires = range(self.ansatz.n_qubits) 51 | 52 | @qml.qnode(dev) 53 | def fidelity_kernel(x1, x2): 54 | AnsatzTemplate(self.ansatz, x1, wires=wires) 55 | qml.adjoint(AnsatzTemplate)(self.ansatz, x2, wires=wires) 56 | ChangeBasis(self.measurement) 57 | return qml.probs(wires=measurement_wires) 58 | 59 | self.fidelity_kernel = fidelity_kernel 60 | 61 | @qml.qnode(dev) 62 | def observable_phi(x): 63 | AnsatzTemplate(self.ansatz, x, wires=wires) 64 | ChangeBasis(self.measurement) 65 | return qml.probs(wires=measurement_wires) 66 | 67 | self.observable_phi = observable_phi 68 | 69 | dev_swap = self.create_device(1+2*self.ansatz.n_qubits) 70 | n = self.ansatz.n_qubits 71 | 72 | @qml.qnode(dev_swap) 73 | def swap_kernel(x1, x2): 74 | qml.Hadamard(wires=[0]) 75 | AnsatzTemplate(self.ansatz, x1, wires=1+wires) 76 | AnsatzTemplate(self.ansatz, x2, wires=1+n+wires) 77 | for j in measurement_wires: 78 | qml.CSWAP(wires=[0, 1+j, 1+n+j]) 79 | qml.Hadamard(wires=[0]) 80 | return qml.probs(wires=[0]) 81 | 82 | self.swap_kernel = swap_kernel 83 | 84 | def kappa(self, x1, x2) -> float: 85 | if self.type == KernelType.OBSERVABLE: 86 | return self.phi(x1) * self.phi(x2) 87 | 88 | elif self.type == KernelType.FIDELITY: 89 | probabilities = self.fidelity_kernel(x1, x2) 90 | self.last_probabilities = probabilities 91 | return probabilities[0] 92 | 93 | elif self.type == KernelType.SWAP_TEST: 94 | probabilities = self.swap_kernel(x1, x2) 95 | self.last_probabilities = probabilities 96 | return np.max([2 * probabilities[0] - 1, 0.0]) 97 | 98 | def phi(self, x) -> float: 99 | if self.type == KernelType.OBSERVABLE: 100 | probabilities = self.observable_phi(x) 101 | self.last_probabilities = probabilities 102 | parity = lambda i: 1 if bin(i).count('1') % 2 == 0 else -1 103 | probabilities = np.array([parity(i) * probabilities[i] for i in range(len(probabilities))]) 104 | # sum_probabilities = np.sum(probabilities) 105 | # print(f"{sum_probabilities=} {probabilities=}") 106 | return np.sum(probabilities) 107 | 108 | elif self.type in [KernelType.FIDELITY, KernelType.SWAP_TEST]: 109 | raise ValueError("phi not available for fidelity kernels") 110 | 111 | else: 112 | raise ValueError("Unknown type, possible erroneous loading from a numpy array") 113 | 114 | -------------------------------------------------------------------------------- /build/lib/quask/core_implementation/qibo_kernel.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from ..core import Ansatz, Kernel, KernelType 3 | 4 | class QiboKernel(Kernel): 5 | pass 6 | -------------------------------------------------------------------------------- /build/lib/quask/core_implementation/qiskit_kernel.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from ..core import Ansatz, Kernel, KernelType 3 | 4 | from qiskit import Aer, BasicAer, QuantumCircuit 5 | from qiskit.circuit import ParameterVector 6 | from qiskit.circuit.library import PauliEvolutionGate 7 | from qiskit_ibm_runtime import QiskitRuntimeService 8 | from qiskit.quantum_info import SparsePauliOp 9 | from qiskit_ibm_runtime import Sampler as IbmSampler 10 | from qiskit_ibm_runtime import Estimator as IbmEstimator 11 | from qiskit_ibm_runtime import Options 12 | from qiskit_aer.primitives import Sampler as AerSampler 13 | from qiskit_aer.primitives import Estimator as AerEstimator 14 | 15 | class QiskitKernel(Kernel): 16 | 17 | def get_estimator(self): 18 | if self.platform == "Aer": 19 | return AerEstimator( 20 | backend_options={"method": "statevector"}, 21 | run_options={"shots": self.n_shots}) 22 | else: 23 | options = Options() 24 | options.optimization_level = self.optimization_level 25 | options.resilience_level = self.resilience_level 26 | return IbmEstimator(backend=self.backend, options=self.options) 27 | 28 | def get_sampler(self): 29 | if self.platform == "Aer": 30 | return AerSampler( 31 | backend_options={"method": "statevector"}, 32 | run_options={"shots": self.n_shots}) 33 | else: 34 | options = Options() 35 | options.optimization_level = self.optimization_level 36 | options.resilience_level = self.resilience_level 37 | return IbmSampler(backend=self.backend, options=self.options) 38 | 39 | def get_qiskit_ansatz(self): 40 | n_params = self.ansatz.n_features + 1 41 | params = ParameterVector('p', n_params) 42 | qc = QuantumCircuit(self.ansatz.n_qubits) 43 | qc.rx(0.0*np.prod(params), 0) # fake instruction to include all parameters in the quantum circuit 44 | for operation in self.ansatz.operation_list: 45 | operator = SparsePauliOp(operation.generator) 46 | rotation = operation.bandwidth*params[operation.feature] 47 | evo = PauliEvolutionGate(operator, time=rotation) 48 | qc.append(evo, operation.wires) 49 | return qc 50 | 51 | def __init__(self, ansatz: Ansatz, measurement: str, type: KernelType, 52 | platform="Aer", backend="qasm_simulator", n_shots=2048, 53 | optimization_level=2, resilience_level=2): 54 | """ 55 | Initialization. 56 | 57 | :param ansatz: Ansatz object representing the unitary transformation 58 | :param measurement: Pauli string representing the measurement 59 | :param type: type of kernel, fidelity or observable 60 | :param device_name: name of the device, 'default.qubit' for noiseless simulation 61 | :param n_shots: number of shots when sampling the solution, None to have infinity 62 | """ 63 | super().__init__(ansatz, measurement, type) 64 | assert platform in ["Aer", "QiskitRuntimeService"] 65 | self.platform = platform 66 | self.backend_name = backend 67 | self.n_shots = n_shots 68 | self.optimization_level = optimization_level 69 | self.resilience_level = resilience_level 70 | 71 | 72 | def kappa(self, x1, x2) -> float: 73 | assert len(x1) == self.ansatz.n_features 74 | assert len(x2) == self.ansatz.n_features 75 | 76 | if self.type == KernelType.OBSERVABLE: 77 | return self.phi(x1) * self.phi(x2) 78 | 79 | elif self.type == KernelType.FIDELITY: 80 | qc = QuantumCircuit(self.ansatz.n_qubits, self.ansatz.n_qubits) 81 | qc.append(self.get_qiskit_ansatz().bind_parameters(x1.tolist() + [1.0]), range(self.ansatz.n_qubits)) 82 | qc.append(self.get_qiskit_ansatz().bind_parameters(x2.tolist() + [1.0]).inverse(), range(self.ansatz.n_qubits)) 83 | qc.measure(range(self.ansatz.n_qubits), range(self.ansatz.n_qubits)) 84 | job = self.get_sampler().run(qc) 85 | probabilities = job.result().quasi_dists[0] 86 | return probabilities.get(0, 0.0) 87 | 88 | elif self.type == KernelType.SWAP_TEST: 89 | qc = QuantumCircuit(1+2*self.ansatz.n_qubits, 1) 90 | qc.h(0) 91 | qc.append(self.get_qiskit_ansatz().bind_parameters(x1.tolist() + [1.0]), range(1, 1+self.ansatz.n_qubits)) 92 | qc.append(self.get_qiskit_ansatz().bind_parameters(x2.tolist() + [1.0]), range(self.ansatz.n_qubits)) 93 | for i in range(self.ansatz.n_qubits): 94 | qc.cswap(0, 1+i, 1+self.ansatz.n_qubits+i) 95 | qc.h(0) 96 | qc.measure(0, 0) 97 | job = self.get_sampler().run(qc) 98 | probabilities = job.result().quasi_dists[0] 99 | return probabilities.get(0, 0.0) 100 | 101 | 102 | def phi(self, x) -> float: 103 | if self.type == KernelType.OBSERVABLE: 104 | 105 | assert len(x) == self.ansatz.n_features 106 | complete_features = x.tolist() + [1.0] 107 | circuit = self.get_qiskit_ansatz().bind_parameters(complete_features) 108 | observable = SparsePauliOp(self.measurement) 109 | job = self.get_estimator().run(circuit, observable) 110 | exp_val = job.result().values[0] 111 | return exp_val 112 | 113 | elif self.type in [KernelType.FIDELITY, KernelType.SWAP_TEST]: 114 | raise ValueError("phi not available for fidelity kernels") 115 | 116 | else: 117 | raise ValueError("Unknown type, possible erroneous loading from a numpy array") 118 | 119 | -------------------------------------------------------------------------------- /build/lib/quask/evaluator/__init__.py: -------------------------------------------------------------------------------- 1 | from .kernel_evaluator import KernelEvaluator 2 | from .lie_rank_evaluator import LieRankEvaluator 3 | from .haar_evaluator import HaarEvaluator 4 | from .covering_number_evaluator import CoveringNumberEvaluator 5 | from .kernel_alignment_evaluator import KernelAlignmentEvaluator 6 | from .centered_kernel_alignment_evaluator import CenteredKernelAlignmentEvaluator 7 | from .spectral_bias_evaluator import SpectralBiasEvaluator 8 | from .ridge_generalization_evaluator import RidgeGeneralizationEvaluator 9 | from .geometric_difference_evaluator import GeometricDifferenceEvaluator 10 | from .ess_model_complexity_evaluator import EssModelComplexityEvaluator 11 | -------------------------------------------------------------------------------- /build/lib/quask/evaluator/centered_kernel_alignment_evaluator.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from ..core import Kernel 3 | from . import KernelEvaluator, KernelAlignmentEvaluator 4 | 5 | 6 | class CenteredKernelAlignmentEvaluator(KernelEvaluator): 7 | """ 8 | Kernel compatibility measure based on the centered kernel-target alignment 9 | See: Cortes, Corinna, Mehryar Mohri, and Afshin Rostamizadeh. "Algorithms for learning kernels based on centered alignment." 10 | The Journal of Machine Learning Research 13.1 (2012): 795-828. 11 | """ 12 | 13 | def evaluate(self, kernel: Kernel, K: np.ndarray, X: np.ndarray, y: np.ndarray): 14 | """ 15 | Evaluate the current kernel and return the corresponding cost. Lower cost values corresponds to better solutions 16 | :param kernel: kernel object 17 | :param K: optional kernel matrix \kappa(X, X) 18 | :param X: datapoints 19 | :param y: labels 20 | :return: cost of the kernel, the lower the better 21 | """ 22 | if K is None: 23 | K = kernel.build_kernel(X, X) 24 | Kc = CenteredKernelAlignmentEvaluator.center_kernel(K) 25 | kta = KernelAlignmentEvaluator.kta(Kc, y) 26 | return - np.abs(kta) 27 | 28 | @staticmethod 29 | def center_kernel(K): 30 | """ 31 | Center a kernel (subtract its mean value) 32 | :param K: kernel matrix 33 | :return: centered kernel 34 | """ 35 | m = K.shape[0] 36 | U = np.eye(m) - (1 / m) * np.outer([1] * m, [1] * m) 37 | Kc = U @ K @ U.T 38 | return Kc 39 | -------------------------------------------------------------------------------- /build/lib/quask/evaluator/covering_number_evaluator.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from ..core import Kernel 3 | from . import KernelEvaluator 4 | 5 | 6 | class CoveringNumberEvaluator(KernelEvaluator): 7 | """ 8 | Expressibility measure based on the covering number associated with the hypothesis class related to the current ansatz. 9 | See: Du, Yuxuan, et al. "Efficient measure for the expressivity of variational quantum algorithms." Physical Review Letters 10 | 128.8 (2022): 080506. 11 | """ 12 | 13 | def evaluate(self, kernel: Kernel, K: np.ndarray, X: np.ndarray, y: np.ndarray): 14 | """ 15 | Evaluate the current kernel and return the corresponding cost. Lower cost values corresponds to better solutions 16 | :param kernel: kernel object 17 | :param K: optional kernel matrix \kappa(X, X) 18 | :param X: datapoints 19 | :param y: labels 20 | :return: cost of the kernel, the lower the better 21 | """ 22 | operations = kernel.ansatz.operation_list 23 | trainable_operations = [op for op in operations if op.feature >= 0] 24 | return 2 ** trainable_operations 25 | -------------------------------------------------------------------------------- /build/lib/quask/evaluator/ess_model_complexity_evaluator.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy.linalg import sqrtm 3 | from ..core import Kernel 4 | from . import KernelEvaluator 5 | 6 | 7 | def EssModelComplexityEvaluator(KernelEvaluator): 8 | """ 9 | Calculate the model complexity s(K). 10 | See Equation F1 in "The power of data in quantum machine learning" (https://arxiv.org/abs/2011.01938) 11 | """ 12 | 13 | def evaluate(self, kernel: Kernel, K: np.ndarray, X: np.ndarray, y: np.ndarray): 14 | """ 15 | Evaluate the current kernel and return the corresponding cost. Lower cost values corresponds to better solutions 16 | 17 | :param kernel: kernel object 18 | :param K: optional kernel matrix \kappa(X, X) 19 | :param X: datapoints 20 | :param y: labels 21 | :return: cost of the kernel, the lower the better 22 | """ 23 | if K is None: 24 | K = kernel.build_kernel(X, X) 25 | 26 | return calculate_model_complexity(K, y) 27 | 28 | def calculate_model_complexity(k, y, normalization_lambda=0.001): 29 | """ 30 | Calculate the model complexity s(K), which is equation F1 in 31 | "The power of data in quantum machine learning" (https://arxiv.org/abs/2011.01938). 32 | 33 | :param k: Kernel gram matrix 34 | :param y: Labels 35 | :param normalization_lambda: Normalization factor 36 | :return: model complexity of the given kernel 37 | """ 38 | n = k.shape[0] 39 | k_inv = np.linalg.inv(k + normalization_lambda * np.eye(n)) 40 | k_body = k_inv @ k @ k_inv 41 | model_complexity = y.T @ k_body @ y 42 | return model_complexity 43 | 44 | def calculate_model_complexity_training(k, y, normalization_lambda=0.001): 45 | """ 46 | Subprocedure of the function 'calculate_model_complexity_generalized'. 47 | 48 | :param k: Kernel gram matrix 49 | :param y: Labels 50 | :param normalization_lambda: Normalization factor 51 | :return: model complexity of the given kernel 52 | """ 53 | n = k.shape[0] 54 | k_inv = np.linalg.inv(k + normalization_lambda * np.eye(n)) 55 | k_mid = k_inv @ k_inv # without k in the middle 56 | model_complexity = (normalization_lambda**2) * (y.T @ k_mid @ y) 57 | return model_complexity 58 | 59 | def calculate_model_complexity_generalized(k, y, normalization_lambda=0.001): 60 | """ 61 | Calculate the model complexity s(K), which is equation M1 in 62 | "The power of data in quantum machine learning" (https://arxiv.org/abs/2011.01938). 63 | 64 | :param k: Kernel gram matrix 65 | :param y: Labels 66 | :param normalization_lambda: Normalization factor 67 | :return: model complexity of the given kernel 68 | """ 69 | n = k.shape[0] 70 | a = np.sqrt(calculate_model_complexity_training(k, y, normalization_lambda) / n) 71 | b = np.sqrt(calculate_model_complexity(k, y, normalization_lambda) / n) 72 | return a + b 73 | -------------------------------------------------------------------------------- /build/lib/quask/evaluator/geometric_difference_evaluator.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy.linalg import sqrtm 3 | from ..core import Kernel 4 | from . import KernelEvaluator 5 | 6 | 7 | class GeometricDifferenceEvaluator(KernelEvaluator): 8 | """ 9 | Calculate the geometric difference g(K_1 || K_2), and characterize 10 | the separation between classical and quantum kernels. 11 | See Equation F9 in "The power of data in quantum machine learning" (https://arxiv.org/abs/2011.01938) 12 | """ 13 | 14 | def __init__(self, list_classical_kernel_matrices, lam): 15 | """ 16 | Initialization. 17 | 18 | :param list_classical_kernel_matrices: List of kernel matrices obtained with classical kernels 19 | :param lam: normalization constant lambda 20 | """ 21 | super().__init__() 22 | self.list_classical_kernel_matrices = list_classical_kernel_matrices 23 | self.lam = lam 24 | 25 | 26 | def evaluate(self, kernel: Kernel, K: np.ndarray, X: np.ndarray, y: np.ndarray): 27 | """ 28 | Evaluate the current kernel and return the corresponding cost. Lower cost values corresponds to better solutions 29 | 30 | :param kernel: kernel object 31 | :param K: optional kernel matrix \kappa(X, X) 32 | :param X: datapoints 33 | :param y: labels 34 | :return: cost of the kernel, the lower the better 35 | """ 36 | if K is None: 37 | K = kernel.build_kernel(X, X) 38 | 39 | geometric_differences = [GeometricDifferenceEvaluator.g(K, Kc, self.lam) 40 | for Kc in self.list_classical_kernel_matrices] 41 | 42 | return -1 * np.min(geometric_differences) 43 | 44 | @staticmethod 45 | def g(k_1, k_2, lam): 46 | """ 47 | Method to calculate the geometric difference 48 | 49 | :param k_1: first matrix (quantum usually) 50 | :param k_2: second matrix (classical usually) 51 | :param lam: normalization lambda 52 | :return: value of g(K_1, K_2) 53 | """ 54 | n = k_2.shape[0] 55 | assert k_2.shape == (n, n) 56 | assert k_1.shape == (n, n) 57 | # √K1 58 | k_1_sqrt = np.real(sqrtm(k_1)) 59 | # √K2 60 | k_2_sqrt = np.real(sqrtm(k_2)) 61 | # √(K2 + lambda I)^-2 62 | kc_inv = np.linalg.inv(k_2 + lam * np.eye(n)) 63 | kc_inv = kc_inv @ kc_inv 64 | # Equation F9 65 | f9_body = k_1_sqrt.dot(k_2_sqrt.dot(kc_inv.dot(k_2_sqrt.dot(k_1_sqrt)))) 66 | f9 = np.sqrt(np.linalg.norm(f9_body, np.inf)) 67 | return f9 68 | -------------------------------------------------------------------------------- /build/lib/quask/evaluator/haar_evaluator.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from ..core import Kernel 3 | from . import KernelEvaluator 4 | 5 | 6 | class HaarEvaluator(KernelEvaluator): 7 | """ 8 | Expressibility measure based on the comparison between the distribution of states obtained with an Haar random circuit and 9 | the one obtained with the current ansatz. 10 | See: Sim, Sukin, Peter D. Johnson, and Alán Aspuru-Guzik. "Expressibility and entangling capability of parameterized quantum 11 | circuits for hybrid quantum-classical algorithms." Advanced Quantum Technologies 2.12 (2019): 1900070. 12 | """ 13 | 14 | def __init__(self, n_bins: int, n_samples: int): 15 | """ 16 | Initialization 17 | :param n_bins: number of discretization buckets 18 | :param n_samples: number of samples approximating the distribution of values 19 | """ 20 | self.n_bins = n_bins 21 | self.n_samples = n_samples 22 | 23 | def evaluate(self, kernel: Kernel, K: np.ndarray, X: np.ndarray, y: np.ndarray): 24 | """ 25 | Evaluate the current kernel and return the corresponding cost. Lower cost values corresponds to better solutions 26 | :param kernel: kernel object 27 | :param K: optional kernel matrix \kappa(X, X) 28 | :param X: datapoints 29 | :param y: labels 30 | :return: cost of the kernel, the lower the better 31 | """ 32 | haar_histogram = HaarEvaluator.haar_histogram(kernel, self.n_bins) 33 | ansatz_histogram = HaarEvaluator.ansatz_histogram(kernel, self.n_bins, self.n_samples) 34 | self.last_result = (haar_histogram, ansatz_histogram) 35 | return np.linalg.norm(haar_histogram - ansatz_histogram) 36 | 37 | @staticmethod 38 | def ansatz_histogram(kernel, n_bins, n_samples): 39 | """ 40 | Create a histogram of the fidelities of the ansatz 41 | :param kernel: kernel object 42 | :param n_bins: number of discretization buckets 43 | :param n_samples: number of samples approximating the distribution of values 44 | :return: histogram of the given ansatz 45 | """ 46 | histogram = [0] * n_bins 47 | 48 | for _ in range(n_samples): 49 | theta_1 = np.random.normal(size=(kernel.ansatz.n_features,)) * np.pi 50 | theta_2 = np.random.normal(size=(kernel.ansatz.n_features,)) * np.pi 51 | fidelity = kernel.kappa(theta_1, theta_2) 52 | index = int(fidelity * n_bins) 53 | histogram[np.minimum(index, n_bins - 1)] += 1 54 | 55 | return np.array(histogram) / n_samples 56 | 57 | @staticmethod 58 | def haar_histogram(kernel, n_bins): 59 | """ 60 | Create a histogram of the Haar random fidelities 61 | :param n_bins: number of bins 62 | :return: histogram 63 | """ 64 | N = 2 ** kernel.ansatz.n_qubits 65 | 66 | def prob(low, high): 67 | return (1-low) ** (N - 1) - (1 - high) ** (N - 1) 68 | 69 | histogram = np.array([prob(i / n_bins, (i+1) / n_bins) for i in range(n_bins)]) 70 | return histogram 71 | 72 | def __str__(self): 73 | return "A = " + self.last_result[0] + " - " + self.last_result[1] 74 | -------------------------------------------------------------------------------- /build/lib/quask/evaluator/kernel_alignment_evaluator.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from ..core import Kernel 3 | from . import KernelEvaluator 4 | 5 | 6 | class KernelAlignmentEvaluator(KernelEvaluator): 7 | """ 8 | Kernel compatibility measure based on the kernel-target alignment 9 | See: Cristianini, Nello, et al. "On kernel-target alignment." Advances in neural information processing systems 14 (2001). 10 | """ 11 | 12 | def evaluate(self, kernel: Kernel, K: np.ndarray, X: np.ndarray, y: np.ndarray): 13 | """ 14 | Evaluate the current kernel and return the corresponding cost. Lower cost values corresponds to better solutions 15 | :param kernel: kernel object 16 | :param K: optional kernel matrix \kappa(X, X) 17 | :param X: datapoints 18 | :param y: labels 19 | :return: cost of the kernel, the lower the better 20 | """ 21 | if K is None: 22 | K = kernel.build_kernel(X, X) 23 | the_cost = -1 * np.abs(KernelAlignmentEvaluator.kta(K, y)) 24 | # assert not np.isnan(the_cost), f"{kernel=} {K=} {y=}" 25 | return the_cost if not np.isnan(the_cost) else 1000 26 | 27 | @staticmethod 28 | def kta(K, y): 29 | """ 30 | Calculates the kernel target alignment 31 | :param K: kernel matrix 32 | :param y: label vector 33 | :return: kernel target alignment 34 | """ 35 | Y = np.outer(y, y) 36 | return np.sum(K * Y) / (np.linalg.norm(K) * np.linalg.norm(Y)) 37 | -------------------------------------------------------------------------------- /build/lib/quask/evaluator/kernel_evaluator.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from abc import ABC, abstractmethod 3 | from ..core import Kernel 4 | 5 | 6 | class KernelEvaluator(ABC): 7 | 8 | def __init__(self): 9 | self.last_result = None 10 | 11 | @abstractmethod 12 | def evaluate(self, kernel: Kernel, K: np.ndarray, X: np.ndarray, y: np.ndarray): 13 | """ 14 | Evaluate the current kernel and return the corresponding cost. Lower cost values corresponds to better solutions 15 | :param kernel: kernel object 16 | :param K: optional kernel matrix \kappa(X, X) 17 | :param X: datapoints 18 | :param y: labels 19 | :return: cost of the kernel, the lower the better 20 | """ 21 | pass 22 | -------------------------------------------------------------------------------- /build/lib/quask/evaluator/lie_rank_evaluator.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import numpy as np 3 | from typing import Set 4 | from ..core import Kernel 5 | from . import KernelEvaluator 6 | 7 | 8 | class LieRankEvaluator(KernelEvaluator): 9 | """ 10 | Expressibility and 'Efficient classical simulability' measure based on the rank of the Lie algebra obtained by spanning 11 | the generators of the circuits. 12 | See: Larocca, Martin, et al. "Diagnosing barren plateaus with tools from quantum optimal control." Quantum 6 (2022): 824. 13 | """ 14 | 15 | def __init__(self, T): 16 | """ 17 | Initializer 18 | :param T: threshold T > 0 telling how is the minimum dimension of a 'hard-to-simulate' Lie algebra 19 | """ 20 | super().__init__() 21 | self.T = T 22 | 23 | def evaluate(self, kernel: Kernel, K: np.ndarray, X: np.ndarray, y: np.ndarray): 24 | """ 25 | Evaluate the current kernel and return the corresponding cost. Lower cost values corresponds to better solutions 26 | :param kernel: kernel object 27 | :param K: optional kernel matrix \kappa(X, X) 28 | :param X: datapoints 29 | :param y: labels 30 | :return: cost of the kernel, the lower the better 31 | """ 32 | self.last_result = self.braket_generators(kernel, self.T) 33 | return -len(self.last_result) 34 | 35 | def braket_pair(self, a: str, b: str): 36 | """ 37 | Calculate the commutator between two pauli matrices 38 | :param a: first Pauli (one of the strings 'I', 'X', 'Y', 'Z') 39 | :param b: second Pauli (one of the strings 'I', 'X', 'Y', 'Z') 40 | :return: [a, b] 41 | """ 42 | assert a in ['I', 'X', 'Y', 'Z'] and b in ['I', 'X', 'Y', 'Z'] 43 | if a == b: return 'I' 44 | if a == 'I': return b 45 | if b == 'I': return a 46 | return list(set(['X', 'Y', 'Z']).difference([a, b]))[0] 47 | 48 | def braket_strings(self, s1: str, s2: str): 49 | """ 50 | Calculate the communtator between two pauli strings 51 | :param s1: first Pauli string 52 | :param s2: second Pauli string 53 | :return: [s1, s2] 54 | """ 55 | assert len(s1) == len(s2), "Tha Pauli strings have different lengths" 56 | return [self.braket_pair(a, b) for (a, b) in zip(s1, s2)] 57 | 58 | def __braket_generators(self, initial_generators: Set[str], new_generators: Set[str]): 59 | """ 60 | Return the set of generators obtained by commutating pairwise the elements in the given set 61 | :param initial_generators: first set of generators 62 | :param new_generators: second set of generators 63 | :return: generators obtained with the pairwise commutation of the given elements (only new ones) 64 | """ 65 | out_generators = [] 66 | for gen_new in new_generators: 67 | for gen_old in initial_generators: 68 | braket = "".join(self.braket_strings(gen_new, gen_old)) 69 | if braket not in initial_generators and braket not in new_generators: 70 | out_generators.append(braket) 71 | return set(out_generators) 72 | 73 | def get_initial_generators(self, kernel): 74 | """ 75 | Create the initial generators of a kernel, i.e. for each operation apply the generator to the correct wires 76 | and identity everywhere else 77 | :param kernel: kernel object 78 | :return set of initial generators corresponding to the operations of the kernel 79 | """ 80 | # get the generators of each operation 81 | generators = [kernel.ansatz.operation_list[i].generator for i in range(kernel.ansatz.n_operations)] 82 | # get the wires on which each operation acts 83 | wires = [kernel.ansatz.operation_list[i].wires for i in range(kernel.ansatz.n_operations)] 84 | initial_generators = [] 85 | for i in range(kernel.ansatz.n_operations): 86 | # initialize each generator with identity everyone, as list of char and not as string (the latter is immutable) 87 | initial_generator = ['I'] * kernel.ansatz.n_qubits 88 | # assign the generator to each qubit 89 | q0, q1 = wires[i][0], wires[i][1] 90 | g0, g1 = generators[i][0], generators[i][1] 91 | initial_generator[q0] = g0 92 | initial_generator[q1] = g1 93 | # convert the list of char to string, now 94 | initial_generator = "".join(initial_generator) 95 | # print(f"{i=} {q0=} {q1=} {g0=} {g1=} {initial_generator=}") 96 | initial_generators.append(initial_generator) 97 | # print(f"{initial_generators}") 98 | return set(initial_generators) 99 | 100 | def braket_generators(self, kernel, T): 101 | """ 102 | Return the basis of the lie algebra of the circuit defined by the kernel. The number of elements is truncated at T 103 | :param kernel: kernel object 104 | :param T: threshold 105 | :return: basis of the lie algebra of the generators in kernel 106 | """ 107 | initial_generators = self.get_initial_generators(kernel) 108 | new_generators = copy.deepcopy(initial_generators) 109 | all_generators = copy.deepcopy(initial_generators) 110 | while len(all_generators) < T and len(new_generators) > 0: 111 | new_generators = self.__braket_generators(all_generators, new_generators) 112 | all_generators = all_generators.union(new_generators) 113 | return all_generators 114 | 115 | def __str__(self): 116 | return str(self.last_result) 117 | -------------------------------------------------------------------------------- /build/lib/quask/evaluator/ridge_generalization_evaluator.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from sklearn.kernel_ridge import KernelRidge 3 | from ..core import Kernel 4 | from . import KernelEvaluator 5 | 6 | 7 | class RidgeGeneralizationEvaluator(KernelEvaluator): 8 | """ 9 | Evaluates the generalization error of the given kernel 10 | """ 11 | 12 | def __init__(self): 13 | """ 14 | Initialization 15 | """ 16 | pass 17 | 18 | def evaluate(self, kernel: Kernel, K: np.ndarray, X: np.ndarray, y: np.ndarray): 19 | """ 20 | Evaluate the current kernel and return the corresponding cost. Lower cost values corresponds to better solutions 21 | :param kernel: kernel object 22 | :param K: optional kernel matrix \kappa(X, X) 23 | :param X: datapoints 24 | :param y: labels 25 | :return: cost of the kernel, the lower the better 26 | """ 27 | n_train = len(y) // 2 28 | n_test = len(y) - n_train 29 | krr = KernelRidge(kernel=lambda X1, X2: kernel.build_kernel(X1, X2)) 30 | krr.fit(X[:n_train], y[:n_train]) 31 | y_pred = np.array(krr.predict(X[n_train:])) 32 | mse = np.linalg.norm(y_pred - y[n_train:]) / n_test 33 | return mse 34 | 35 | -------------------------------------------------------------------------------- /build/lib/quask/evaluator/spectral_bias_evaluator.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy.linalg import eigh 3 | from ..core import Kernel 4 | from . import KernelEvaluator 5 | 6 | 7 | class SpectralBiasEvaluator(KernelEvaluator): 8 | """ 9 | Kernel compatibility measure based on the spectral bias framework. 10 | See: Canatar, Abdulkadir, Blake Bordelon, and Cengiz Pehlevan. "Spectral bias and task-model alignment explain generalization 11 | in kernel regression and infinitely wide neural networks." Nature communications 12.1 (2021): 2914. 12 | """ 13 | 14 | def __init__(self, n_eigenvalues_cut): 15 | """ 16 | Initialization 17 | :param n_eigenvalues_cut: number of eigenvalues contributing to the cumulative power 18 | """ 19 | self.n_eigenvalues_cut = n_eigenvalues_cut 20 | 21 | def evaluate(self, kernel: Kernel, K: np.ndarray, X: np.ndarray, y: np.ndarray): 22 | """ 23 | Evaluate the current kernel and return the corresponding cost. Lower cost values corresponds to better solutions 24 | :param kernel: kernel object 25 | :param K: optional kernel matrix \kappa(X, X) 26 | :param X: datapoints 27 | :param y: labels 28 | :return: cost of the kernel, the lower the better 29 | """ 30 | if K is None: 31 | K = kernel.build_kernel(X, X) 32 | Lambda, Phi = SpectralBiasEvaluator.decompose_kernel(K) 33 | w, a = SpectralBiasEvaluator.calculate_weights(Lambda, Phi, y) 34 | C, powers = SpectralBiasEvaluator.cumulative_power_distribution(w, Lambda, self.n_eigenvalues_cut) 35 | self.last_result = (Lambda, Phi, w, a, C, powers) 36 | return C 37 | 38 | 39 | @staticmethod 40 | def decompose_kernel(K, eigenvalue_descending_order=True, eigenvalue_removal_threshold=1e-12): 41 | """ 42 | Decompose the kernel matrix K in its eigenvalues Λ and eigenvectors Φ 43 | :param K: kernel matrix, real and symmetric 44 | :param eigenvalue_descending_order: if True, the biggest eigenvalue is the first one 45 | :return: Lambda vector (n elements) and Phi matrix (N*N matrix) 46 | """ 47 | Lambda, Phi = eigh(K) 48 | 49 | # set the desired order for the eigenvalues 50 | if eigenvalue_descending_order: 51 | Lambda = Lambda[::-1] 52 | Phi = Phi[:, ::-1] 53 | 54 | # kernel matrix is positive definite, any (small) negative eigenvalue is effectively a numerical error 55 | Lambda[Lambda < 0] = 0 56 | 57 | # remove the smallest positive eigenvalues, as they are useless 58 | Lambda[Lambda < eigenvalue_removal_threshold] = 0 59 | 60 | return Lambda, Phi 61 | 62 | @staticmethod 63 | def calculate_weights(Lambda, Phi, labels): 64 | """ 65 | Calculates the weights of a predictor given the labels and the kernel eigendecomposition, 66 | as shown in (Canatar et al 2021, inline formula below equation 18). 67 | :param Lambda: vectors of m nonnegative eigenvalues 'eta' 68 | :param Phi: vectors of m nonnegative eigenvectors 'phi' 69 | :param labels: vector of m labels corresponding to 'm' ground truth labels or predictor outputs 70 | :return: vector w of RKHS weights, vector a of out-of-RKHS weights 71 | """ 72 | # get the number of training elements 73 | m = Lambda.shape[0] 74 | 75 | # invert nonzero eigenvalues 76 | inv_eigenvalues = np.reciprocal(Lambda, where=Lambda > 0) 77 | 78 | # weight vectors are calculated by inverting formula: y = \sum_k=1^M w_k \sqrt{lambda_k} \phi_k(x) 79 | the_w = (1 / m) * np.diag(inv_eigenvalues ** 0.5) @ Phi.T @ labels 80 | the_w[Lambda == 0] = 0 81 | 82 | # weight vector for the components out-of-RKHS 83 | the_a = (1 / m) * Phi.T @ labels 84 | the_a[Lambda > 0] = 0 85 | return the_w, the_a 86 | 87 | @staticmethod 88 | def cumulative_power_distribution(w, Lambda, n_eigenvalues): 89 | """ 90 | 91 | :param w: vector of weights 92 | :param Lambda: vector of eigenvalues 93 | :param n_eigenvalues: number of eigenvalues contributing to the cumulative power 94 | :return: 95 | """ 96 | powers = np.diag(Lambda) @ (w ** 2) 97 | return np.sum(powers[:n_eigenvalues]) / np.sum(powers), powers 98 | 99 | def __str__(self): 100 | (Lambda, Phi, w, a, C, powers) = self.last_result 101 | return f"""{Lambda=} {Phi=} {w=} {a=} {C=} {powers=}""" 102 | 103 | -------------------------------------------------------------------------------- /build/lib/quask/optimizer/__init__.py: -------------------------------------------------------------------------------- 1 | from .base_kernel_optimizer import BaseKernelOptimizer 2 | from .wide_kernel_environment import WideKernelEnvironment 3 | from .metaheuristic_optimizer import MetaheuristicOptimizer 4 | from .reinforcement_learning_optimizer import ReinforcementLearningOptimizer 5 | from .greedy_optimizer import GreedyOptimizer 6 | from .bayesian_optimizer import BayesianOptimizer 7 | -------------------------------------------------------------------------------- /build/lib/quask/optimizer/base_kernel_optimizer.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | import numpy as np 3 | import copy 4 | from ..core import Operation, Ansatz, Kernel, KernelFactory 5 | from ..evaluator import KernelEvaluator 6 | 7 | 8 | class BaseKernelOptimizer(ABC): 9 | """ 10 | Abstract class implementing a procedure to optimize the kernel 11 | """ 12 | 13 | def __init__(self, initial_kernel: Kernel, X: np.ndarray, y: np.ndarray, ke: KernelEvaluator): 14 | """ 15 | Initialization 16 | :param initial_kernel: initial kernel object 17 | :param X: datapoints 18 | :param y: labels 19 | :param ke: kernel evaluator object 20 | """ 21 | self.initial_kernel = initial_kernel 22 | self.X = X 23 | self.y = y 24 | self.ke = ke 25 | 26 | @abstractmethod 27 | def optimize(self): 28 | """ 29 | Run the optimization 30 | :return: optimized kernel object 31 | """ 32 | pass 33 | -------------------------------------------------------------------------------- /build/lib/quask/optimizer/bayesian_optimizer.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from joblib import Parallel, delayed 3 | from skopt import Optimizer 4 | from skopt.space import Real, Categorical 5 | 6 | from ..core import Operation, Ansatz, Kernel, KernelFactory 7 | from ..evaluator import KernelEvaluator 8 | from . import BaseKernelOptimizer 9 | 10 | 11 | class BayesianOptimizer(BaseKernelOptimizer): 12 | 13 | def __init__(self, initial_kernel: Kernel, X: np.ndarray, y: np.ndarray, ke: KernelEvaluator): 14 | super().__init__(initial_kernel, X, y, ke) 15 | self.optimizer = None 16 | 17 | def get_sklearn_dimensions(self): 18 | n_features = self.initial_kernel.ansatz.n_features 19 | n_operations = self.initial_kernel.ansatz.n_operations 20 | n_qubits = self.initial_kernel.ansatz.n_qubits 21 | allowed_generators = self.initial_kernel.get_allowed_operations() 22 | ansatz_dimension = [ 23 | # generator 24 | Categorical(list(range(len(allowed_generators)))), 25 | # wires 26 | Categorical(list(range(n_qubits))), 27 | Categorical(list(range(n_qubits - 1))), 28 | # features 29 | Categorical(list(range(n_features))), 30 | # bandwidth 31 | Real(0.0, 1.0), 32 | ] * n_operations 33 | measurement_dimensions = [Categorical([0, 1, 2, 3])] * n_qubits 34 | return ansatz_dimension + measurement_dimensions 35 | 36 | def get_kernel(self, the_array): 37 | the_array = np.array(the_array, dtype=object) 38 | the_kernel = Kernel.from_numpy(np.concatenate([the_array.ravel(), np.array([self.initial_kernel.type])]), 39 | self.initial_kernel.ansatz.n_features, 40 | self.initial_kernel.ansatz.n_qubits, 41 | self.initial_kernel.ansatz.n_operations, 42 | self.initial_kernel.ansatz.allow_midcircuit_measurement, 43 | shift_second_wire=True) 44 | return the_kernel 45 | 46 | def get_cost(self, the_array): 47 | the_kernel = self.get_kernel(the_array) 48 | the_cost = self.ke.evaluate(the_kernel, None, self.X, self.y) 49 | return the_cost 50 | 51 | def optimize(self, n_epochs=20, n_points=4, n_jobs=4): 52 | 53 | self.optimizer = Optimizer( 54 | dimensions=self.get_sklearn_dimensions(), 55 | random_state=1, 56 | base_estimator='gp', 57 | acq_func="PI", 58 | acq_optimizer="sampling", 59 | acq_func_kwargs={"xi": 10000.0, "kappa": 10000.0} 60 | ) 61 | 62 | for i in range(n_epochs): 63 | x = self.optimizer.ask(n_points=n_points) # x is a list of n_points points 64 | y = Parallel(n_jobs=n_jobs)(delayed(lambda array: self.get_cost(array))(v) for v in x) # evaluate points in parallel 65 | self.optimizer.tell(x, y) 66 | print(f"Epoch of training {i=}") 67 | 68 | min_index = np.argmin(self.optimizer.yi) 69 | # min_cost = self.optimizer.yi[min_index] 70 | min_solution = self.optimizer.Xi[min_index] 71 | 72 | best_kernel = self.get_kernel(min_solution) 73 | return best_kernel 74 | -------------------------------------------------------------------------------- /build/lib/quask/optimizer/greedy_optimizer.py: -------------------------------------------------------------------------------- 1 | import copy 2 | 3 | import numpy as np 4 | from mushroom_rl.core import Environment 5 | 6 | from ..core import Kernel 7 | from ..evaluator import KernelEvaluator 8 | from . import BaseKernelOptimizer, WideKernelEnvironment 9 | 10 | 11 | class GreedyOptimizer(BaseKernelOptimizer): 12 | 13 | def __init__(self, initial_kernel: Kernel, X: np.ndarray, y: np.ndarray, ke: KernelEvaluator): 14 | super().__init__(initial_kernel, X, y, ke) 15 | self.mdp: WideKernelEnvironment = Environment.make('WideKernelEnvironment', initial_kernel=self.initial_kernel, X=X, y=y, ke=ke) 16 | self.rewards_history = [] 17 | self.actions_history = [] 18 | 19 | def optimize(self, verbose=False): 20 | 21 | self.mdp.reset() 22 | state = copy.deepcopy(self.mdp._state) 23 | 24 | terminated = False 25 | n_actions = self.mdp._mdp_info.action_space.size[0] 26 | rewards = np.zeros(shape=(n_actions,)) 27 | 28 | while not terminated: 29 | # list all actions at the first depth 30 | for action in range(n_actions): 31 | self.mdp.reset(state) 32 | new_state, reward, absorbed, _ = self.mdp.step((action,)) 33 | rewards[action] = reward 34 | _, kernel = self.mdp.deserialize_state(new_state) 35 | if absorbed: 36 | terminated = True 37 | print(f"{action=:4d} {reward=:0.6f} {kernel=}") 38 | # apply chosen action 39 | chosen_action = np.argmax(rewards) 40 | self.mdp.reset(state) 41 | state, _, _, _ = self.mdp.step((chosen_action,)) 42 | if verbose: 43 | print(f"Chosen action: {chosen_action}") 44 | print(f"{self.mdp.deserialize_state(state)=}") 45 | # additional information 46 | self.rewards_history.append(rewards) 47 | self.actions_history.append(chosen_action) 48 | 49 | _, kernel = self.mdp.deserialize_state(state) 50 | return kernel 51 | -------------------------------------------------------------------------------- /build/lib/quask/optimizer/metaheuristic_optimizer.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import numpy as np 3 | from enum import Enum 4 | 5 | from opytimizer import Opytimizer 6 | from opytimizer.core import Function 7 | from opytimizer.optimizers.swarm import PSO 8 | from opytimizer.spaces import GridSpace 9 | from opytimizer.utils.callback import Callback 10 | from ..core import Kernel 11 | from ..evaluator import KernelEvaluator 12 | from . import BaseKernelOptimizer 13 | 14 | 15 | class CustomCallback(Callback): 16 | """A CustomCallback can be created by override its parent `Callback` class 17 | and by implementing the desired logic in its available methods. 18 | """ 19 | 20 | def __init__(self): 21 | """Initialization method for the customized callback.""" 22 | 23 | # You only need to override its parent class 24 | super(CustomCallback).__init__() 25 | 26 | def on_task_begin(self, opt_model): 27 | """Called at the beginning of an task.""" 28 | print("Task begin") 29 | 30 | def on_task_end(self, opt_model): 31 | """Called at the end of an task.""" 32 | print("Task end") 33 | 34 | def on_iteration_begin(self, iteration, opt_model): 35 | """Called at the beginning of an iteration.""" 36 | print(f"Iteration {iteration} begin") 37 | 38 | def on_iteration_end(self, iteration, opt_model): 39 | """Called at the end of an iteration.""" 40 | print(f"Iteration {iteration} end") 41 | 42 | def on_evaluate_before(self, *evaluate_args): 43 | """Called before the `evaluate` method.""" 44 | print(f"Evaluate before {evaluate_args}") 45 | 46 | def on_evaluate_after(self, *evaluate_args): 47 | """Called after the `evaluate` method.""" 48 | print(f"Evaluate after {evaluate_args}") 49 | 50 | def on_update_before(self, *update_args): 51 | """Called before the `update` method.""" 52 | print(f"Update before {update_args}") 53 | 54 | def on_update_after(self, *update_args): 55 | """Called after the `update` method.""" 56 | print(f"Update after {update_args}") 57 | 58 | 59 | class MetaheuristicType(Enum): 60 | 61 | # evolutionary 62 | FOREST_OPTIMIZATION = 1 63 | GENERIC_ALGORITHM = 2 64 | # population 65 | EMPEROR_PENGUIN_OPTIMIZER = 3 66 | # swarm 67 | PARTICLE_SWARM_OPTIMIZATION = 0 68 | 69 | 70 | class MetaheuristicOptimizer(BaseKernelOptimizer): 71 | 72 | def __init__(self, initial_kernel: Kernel, X: np.ndarray, y: np.ndarray, ke: KernelEvaluator): 73 | super().__init__(initial_kernel, X, y, ke) 74 | 75 | def cost(array): 76 | array = array.ravel() 77 | the_array = np.concatenate([array, np.array([initial_kernel.type])]) 78 | # create kernel 79 | the_kernel = Kernel.from_numpy(the_array, 80 | initial_kernel.ansatz.n_features, 81 | initial_kernel.ansatz.n_qubits, 82 | initial_kernel.ansatz.n_operations, 83 | initial_kernel.ansatz.allow_midcircuit_measurement, 84 | shift_second_wire=True) 85 | the_cost = ke.evaluate(the_kernel, None, X, y) 86 | print(f"MetaheuristicOptimizer.cost -> {the_cost: 5.5f} -> {array}") 87 | return the_cost 88 | 89 | self.space = self.get_opytimize_space() 90 | self.optimizer = PSO() 91 | self.cost = cost 92 | self.function = Function(cost) 93 | self.opt = Opytimizer(self.space, self.optimizer, self.function, save_agents=True) 94 | self.history = None 95 | self.best_solution = None 96 | self.best_cost = None 97 | 98 | def optimize(self, n_iterations=1000, verbose=False): 99 | self.opt.start(n_iterations=n_iterations, callbacks=[CustomCallback()] if verbose else []) 100 | self.history = self.opt.history 101 | data_at_convergence = self.history.get_convergence("best_agent") 102 | self.best_solution = data_at_convergence[0].ravel() 103 | self.best_cost = data_at_convergence[1].ravel() 104 | the_array = np.concatenate([self.best_solution, np.array([self.initial_kernel.type])]) 105 | return Kernel.from_numpy(the_array, 106 | self.initial_kernel.ansatz.n_features, 107 | self.initial_kernel.ansatz.n_qubits, 108 | self.initial_kernel.ansatz.n_operations, 109 | self.initial_kernel.ansatz.allow_midcircuit_measurement, 110 | shift_second_wire=True) 111 | 112 | def get_opytimize_space(self): 113 | n_features = self.initial_kernel.ansatz.n_features 114 | n_operations = self.initial_kernel.ansatz.n_operations 115 | n_qubits = self.initial_kernel.ansatz.n_qubits 116 | allowed_generators = self.initial_kernel.get_allowed_operations() 117 | 118 | n_variables = 5 * n_operations + n_qubits 119 | step = [1, 1, 1, 1, 0.2] * n_operations + [1] * n_qubits 120 | lower_bound = [0, 0, 0, 0, 0.2] * n_operations + [0] * n_qubits 121 | upper_bound = [len(allowed_generators) - 1, n_qubits - 1, n_qubits - 2, n_features, 1.0] * n_operations + [3] * n_qubits 122 | return GridSpace(n_variables, step, lower_bound, upper_bound) 123 | -------------------------------------------------------------------------------- /build/lib/quask/optimizer/reinforcement_learning_optimizer.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import copy 3 | from ..core import Operation, Ansatz, Kernel, KernelFactory 4 | from ..evaluator import KernelEvaluator 5 | from . import BaseKernelOptimizer 6 | 7 | 8 | class ReinforcementLearningOptimizer(BaseKernelOptimizer): 9 | """ 10 | Reinforcement learning based technique for optimize a kernel function 11 | """ 12 | 13 | def __init__(self, initial_kernel: Kernel, X: np.ndarray, y: np.ndarray, ke: KernelEvaluator): 14 | """ 15 | Initialization 16 | :param initial_kernel: initial kernel object 17 | :param X: datapoints 18 | :param y: labels 19 | :param ke: kernel evaluator object 20 | """ 21 | from mushroom_rl.core import Environment 22 | from mushroom_rl.core import Core 23 | from mushroom_rl.algorithms.value import SARSALambda 24 | from mushroom_rl.policy import EpsGreedy 25 | from mushroom_rl.utils.parameters import Parameter 26 | from mushroom_rl.utils.dataset import compute_J 27 | self.initial_kernel = copy.deepcopy(initial_kernel) 28 | self.X = X 29 | self.y = y 30 | self.ke = ke 31 | self.mdp = Environment.make('WideKernelEnvironment', initial_kernel=self.initial_kernel, X=X, y=y, ke=ke) 32 | self.agent = None 33 | self.core = None 34 | 35 | def optimize(self, initial_episodes=3, n_episodes=100, n_steps_per_fit=1, final_episodes=3): 36 | """ 37 | Optimization routine 38 | :param initial_episodes: 39 | :param n_steps: 40 | :param n_steps_per_fit: 41 | :param final_episodes: 42 | :return: 43 | """ 44 | from mushroom_rl.core import Environment 45 | from mushroom_rl.core import Core 46 | from mushroom_rl.algorithms.value import SARSALambda 47 | from mushroom_rl.policy import EpsGreedy 48 | from mushroom_rl.utils.parameters import Parameter 49 | from mushroom_rl.utils.dataset import compute_J 50 | # Policy 51 | epsilon = Parameter(value=1.) 52 | pi = EpsGreedy(epsilon=epsilon) 53 | learning_rate = Parameter(.1) 54 | 55 | # Agent 56 | self.agent = SARSALambda(self.mdp.info, pi, 57 | learning_rate=learning_rate, 58 | lambda_coeff=.9) 59 | 60 | # Reinforcement learning experiment 61 | self.core = Core(self.agent, self.mdp) 62 | 63 | # Visualize initial policy for 3 episodes 64 | dataset = self.core.evaluate(n_episodes=initial_episodes, render=True) 65 | print(f"{dataset=}") 66 | 67 | # Print the average objective value before learning 68 | J = np.mean(compute_J(dataset, self.mdp.info.gamma)) 69 | print(f'Objective function before learning: {J}') 70 | 71 | # Train 72 | self.core.learn(n_episodes=n_episodes, n_steps_per_fit=n_steps_per_fit, render=True) 73 | 74 | # Visualize results for 3 episodes 75 | dataset = self.core.evaluate(n_episodes=final_episodes, render=True) 76 | 77 | # Print the average objective value after learning 78 | J = np.mean(compute_J(dataset, self.mdp.info.gamma)) 79 | print(f'Objective function after learning: {J}') 80 | 81 | kernel = Kernel.from_numpy(self.mdp._state[1:], self.mdp.n_features, self.mdp.n_qubits, self.mdp.n_operations, self.mdp.allow_midcircuit_measurement) 82 | return kernel 83 | -------------------------------------------------------------------------------- /build/lib/quask/optimizer/wide_kernel_environment.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from mushroom_rl.core import Environment, MDPInfo 3 | from mushroom_rl.utils.spaces import Discrete 4 | from ..core import Operation, Ansatz, Kernel, KernelFactory 5 | from ..evaluator import KernelEvaluator 6 | 7 | 8 | class WideKernelEnvironment(Environment): 9 | """ 10 | Implementation of a Mushroom-RL Environment for our problem 11 | """ 12 | 13 | @staticmethod 14 | def setup(): 15 | WideKernelEnvironment.register() 16 | 17 | def __init__(self, initial_kernel: Kernel, X: np.ndarray, y: np.ndarray, ke: KernelEvaluator): 18 | """ 19 | Initialization 20 | :param initial_kernel: initial kernel object 21 | :param X: datapoints 22 | :param y: labels 23 | :param ke: kernel evaluator object 24 | """ 25 | self.initial_kernel = initial_kernel 26 | self.n_operations = self.initial_kernel.ansatz.n_operations 27 | self.n_features = self.initial_kernel.ansatz.n_features 28 | self.n_qubits = self.initial_kernel.ansatz.n_qubits 29 | self.allow_midcircuit_measurement = self.initial_kernel.ansatz.allow_midcircuit_measurement 30 | self.X = X 31 | self.y = y 32 | self.ke = ke 33 | self.last_reward = None 34 | 35 | # Create the action space. 36 | action_space = Discrete( 37 | len(self.initial_kernel.get_allowed_operations()) 38 | * self.n_qubits 39 | * (self.n_qubits - 1) 40 | * (self.n_features + 1) 41 | ) 42 | 43 | # Create the observation space. 44 | observation_space = Discrete( 45 | len(self.initial_kernel.get_allowed_operations()) 46 | * self.n_qubits 47 | * (self.n_qubits - 1) 48 | * (self.n_features + 1) 49 | * self.n_operations 50 | ) 51 | 52 | # Create the MDPInfo structure, needed by the environment interface 53 | mdp_info = MDPInfo(observation_space, action_space, gamma=0.99, horizon=100) 54 | super().__init__(mdp_info) 55 | 56 | # Create a state class variable to store the current state 57 | self._state = self.serialize_state(0, initial_kernel) 58 | 59 | # Create the viewer 60 | self._viewer = None 61 | 62 | def serialize_state(self, n_operation, kernel): 63 | """ 64 | Pack the state of the optimization technique 65 | :param n_operation: number of operations currently performed 66 | :param kernel: kernel object 67 | :return: serialized state 68 | """ 69 | state = np.concatenate([np.array([n_operation], dtype=int), kernel.to_numpy()], dtype=object).ravel() 70 | return state.astype(int) 71 | 72 | def deserialize_state(self, array): 73 | """ 74 | Deserialized a previously packed state variable 75 | :param array: serialized state 76 | :return: tuple n_operations, kernel object 77 | """ 78 | kernel = Kernel.from_numpy(array[1:], self.n_features, self.n_qubits, self.n_operations, self.allow_midcircuit_measurement) 79 | n_operations = int(array[0]) 80 | return n_operations, kernel 81 | 82 | def render(self): 83 | """ 84 | Rendering function - we don't need that 85 | :return: None 86 | """ 87 | n_op, kernel = self.deserialize_state(self._state) 88 | print(f"{self.last_reward=:2.4f} {n_op=:2d} {kernel=}") 89 | 90 | def reset(self, state=None): 91 | """ 92 | Reset the state 93 | :param state: optional state 94 | :return: self._state variable 95 | """ 96 | if state is None: 97 | self.initial_kernel.ansatz.initialize_to_identity() 98 | self._state = self.serialize_state(0, self.initial_kernel) 99 | else: 100 | self._state = state 101 | return self._state 102 | 103 | def unpack_action(self, action): 104 | """ 105 | Unpack an action to a operation 106 | :param action: integer representing the action 107 | :return: dictionary of the operation 108 | """ 109 | generator_index = int(action % len(self.initial_kernel.get_allowed_operations())) 110 | action = action // len(self.initial_kernel.get_allowed_operations()) 111 | 112 | wires_0 = int(action % self.n_qubits) 113 | action = action // self.n_qubits 114 | 115 | wires_1 = int(action % (self.n_qubits - 1)) 116 | if wires_1 >= wires_0: 117 | wires_1 += 1 118 | action = action // (self.n_qubits - 1) 119 | 120 | feature = int(action % (self.n_features + 1)) 121 | action = action // (self.n_features + 1) 122 | assert action == 0 123 | 124 | return {'generator': self.initial_kernel.get_allowed_operations()[generator_index], 125 | 'wires': [wires_0, wires_1], 126 | 'feature': feature, 127 | 'bandwidth': 1.0} 128 | 129 | def step(self, action): 130 | 131 | the_action = self.unpack_action(action[0]) 132 | 133 | # Create kernel from state 134 | n_operations, kernel = self.deserialize_state(self._state) 135 | 136 | # Update kernel 137 | kernel.ansatz.change_operation(n_operations, the_action['feature'], the_action['wires'], the_action['generator'], the_action['bandwidth']) 138 | n_operations += 1 139 | 140 | # Update state 141 | self._state = self.serialize_state(n_operations, kernel) 142 | 143 | # Compute the reward as distance penalty from goal 144 | reward = -1 * self.ke.evaluate(kernel, None, self.X, self.y) 145 | self.last_reward = reward 146 | 147 | # Set the absorbing flag if goal is reached 148 | absorbing = self.n_operations == n_operations 149 | 150 | # Return all the information + empty dictionary (used to pass additional information) 151 | return self._state, reward, absorbing, {} 152 | 153 | 154 | -------------------------------------------------------------------------------- /build/lib/quask/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CERN-IT-INNOVATION/QuASK/3ac5bef5aff678e2239af4207ed9e89b8a0668b9/build/lib/quask/tests/__init__.py -------------------------------------------------------------------------------- /build/lib/quask/tests/kernel_test.py: -------------------------------------------------------------------------------- 1 | from quask import Operation, Ansatz, KernelType, PennylaneKernel 2 | import numpy as np 3 | 4 | 5 | def test_static_single_qubit(KernelClass): 6 | 7 | # circuit: |0> - RX(pi) - 8 | # |0> - ID - 9 | ansats = Ansatz(n_features=1, n_qubits=2, n_operations=1) 10 | ansats.initialize_to_identity() 11 | ansats.change_generators(0, "XI") 12 | ansats.change_feature(0, -1) 13 | ansats.change_wires(0, [0, 1]) 14 | ansats.change_bandwidth(0, 1) 15 | 16 | # measurement operation = <1|Z|1> 17 | # probabilities: [0.0, 1.0] 18 | # observable: 0.0 * (+1) + 1.0 * (-1) = -1.0 19 | kernel = KernelClass(ansats, "ZI", KernelType.OBSERVABLE) 20 | x = kernel.phi(np.array([np.inf])) 21 | assert np.allclose(kernel.get_last_probabilities(), np.array([0, 1])), f"Incorrect measurement: {kernel.get_last_probabilities()}" 22 | assert np.isclose(x, -1), "Incorrect observable" 23 | 24 | # measurement operation = <1|X|1> = <1H|Z|H1> = <+|Z|+> 25 | # probabilities: [0.5, 0.5] 26 | # observable: 0.5 * (+1) + 0.5 * (-1) = 0.0 27 | kernel = KernelClass(ansats, "XI", KernelType.OBSERVABLE) 28 | x = kernel.phi(np.array([np.inf])) 29 | assert np.allclose(kernel.get_last_probabilities(), np.array([0.5, 0.5])), f"Incorrect measurement: {kernel.get_last_probabilities()}" 30 | assert np.isclose(x, 0), "Incorrect observable" 31 | 32 | # measurement operation = <1|Y|1> = <1HSdag|Z|SdagH1> = <[1/sqrt(2), -i/sqrt(2)]|Z|[1/sqrt(2), -i/sqrt(2)]> 33 | # probabilities: [0.5, 0.5] 34 | # observable: 0.5 * (+1) + 0.5 * (-1) = 0.0 35 | kernel = KernelClass(ansats, "YI", KernelType.OBSERVABLE) 36 | x = kernel.phi(np.array([np.inf])) 37 | assert np.allclose(kernel.get_last_probabilities(), np.array([0.5, 0.5])), f"Incorrect measurement: {kernel.get_last_probabilities()}" 38 | assert np.isclose(x, 0), "Incorrect observable" 39 | 40 | 41 | def test_static_two_qubit(KernelClass): 42 | 43 | # circuit: |0> - XY(#0) - 44 | # |0> - XY(#0) - 45 | ansats = Ansatz(n_features=1, n_qubits=2, n_operations=1) 46 | ansats.initialize_to_identity() 47 | ansats.change_generators(0, "XY") 48 | ansats.change_feature(0, 0) 49 | ansats.change_wires(0, [0, 1]) 50 | ansats.change_bandwidth(0, 1) 51 | 52 | kernel = KernelClass(ansats, "ZZ", KernelType.OBSERVABLE) 53 | x = kernel.phi(np.array([np.pi / 2])) 54 | assert np.allclose(kernel.get_last_probabilities(), np.array([0.5, 0.0, 0.0, 0.5])), "Incorrect measurement" 55 | assert np.isclose(x, 0), "Incorrect observable" 56 | 57 | 58 | test_static_single_qubit(PennylaneKernel) 59 | test_static_two_qubit(PennylaneKernel) 60 | -------------------------------------------------------------------------------- /dist/quask-1.0.2-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CERN-IT-INNOVATION/QuASK/3ac5bef5aff678e2239af4207ed9e89b8a0668b9/dist/quask-1.0.2-py3-none-any.whl -------------------------------------------------------------------------------- /dist/quask-1.0.2.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CERN-IT-INNOVATION/QuASK/3ac5bef5aff678e2239af4207ed9e89b8a0668b9/dist/quask-1.0.2.tar.gz -------------------------------------------------------------------------------- /dist/quask-2.0.0a1-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CERN-IT-INNOVATION/QuASK/3ac5bef5aff678e2239af4207ed9e89b8a0668b9/dist/quask-2.0.0a1-py3-none-any.whl -------------------------------------------------------------------------------- /dist/quask-2.0.0a1.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CERN-IT-INNOVATION/QuASK/3ac5bef5aff678e2239af4207ed9e89b8a0668b9/dist/quask-2.0.0a1.tar.gz -------------------------------------------------------------------------------- /dist/quask-2.0.1a2-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CERN-IT-INNOVATION/QuASK/3ac5bef5aff678e2239af4207ed9e89b8a0668b9/dist/quask-2.0.1a2-py3-none-any.whl -------------------------------------------------------------------------------- /dist/quask-2.0.1a2.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CERN-IT-INNOVATION/QuASK/3ac5bef5aff678e2239af4207ed9e89b8a0668b9/dist/quask-2.0.1a2.tar.gz -------------------------------------------------------------------------------- /docs/source/notebooks/quask_1_evaluators.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "66c99cd8-418d-4be5-a573-e440824b52e5", 6 | "metadata": {}, 7 | "source": [ 8 | "# Criteria to evaluate a quantum kernel\n", 9 | "\n", 10 | "One of the main features of _quask_ is the opportunity to evaluate a quantum kernel according to the various criteria proposed in the literature. These criteria are especially important in the context of seeking a quantum advantage, a model that outperforms existing classical choices.\n", 11 | "\n", 12 | "All the criteria are available as classes that inherit the abstract class `KernelEvaluator`. This object has only one abstract method, `evaluate`, which takes four arguments:\n", 13 | "\n", 14 | "1. The `Kernel` object.\n", 15 | "2. The set of training data `X`, which might be used by some criteria.\n", 16 | "3. The set of labels `y`, which might be used by some criteria in conjunction with the training data.\n", 17 | "4. The kernel Gram matrix `K`, which is entirely optional and can be built from `kappa` and `X`.\n", 18 | "\n", 19 | "The argument `K` is provided in case such an object has been previously calculated and is kept for the purpose of speeding up the computation.\n" 20 | ] 21 | }, 22 | { 23 | "cell_type": "markdown", 24 | "id": "2d78c4e0-851f-49df-9966-999bf5e6976c", 25 | "metadata": {}, 26 | "source": [ 27 | "## Depending uniquely on the structure of the kernel \n", 28 | "\n", 29 | "We illustrate a set of criteria that measure the expressibility of the given ansatz, thus do not need any information about the data used." 30 | ] 31 | }, 32 | { 33 | "cell_type": "markdown", 34 | "id": "f1a083c2-59fe-428b-a067-c13265e4d4de", 35 | "metadata": {}, 36 | "source": [ 37 | "### Haar evaluator evaluator\n", 38 | "\n", 39 | "A criteria inspired by the definition of expressiblity given in \\[sim19\\]. A discretized, approximated version of this metric is given, and compares the histogram of inner products between Haar random vectors, with the inner product of vectors generated with the given kernel `kappa`. Note that, for $n \\to \\infty$, the Haar random histogram concentrates around zero.\n" 40 | ] 41 | }, 42 | { 43 | "cell_type": "code", 44 | "execution_count": null, 45 | "id": "938ac932-729f-484f-a914-23e3d88b6782", 46 | "metadata": {}, 47 | "outputs": [], 48 | "source": [ 49 | "from quask.evaluator import HaarEvaluator\n", 50 | "n = 100 # number of bins discretizing the histogram\n", 51 | "m = 10000 #number of randomly sampled data for creating the ansatz's ensemble of states\n", 52 | "h_eval = HaarEvaluator(n, m)" 53 | ] 54 | }, 55 | { 56 | "cell_type": "markdown", 57 | "id": "2f80a0ee-974d-4a58-a821-b44c6d05e1bc", 58 | "metadata": {}, 59 | "source": [ 60 | "### Lie Rank evaluator evaluator\n", 61 | "\n", 62 | "A criteria inspired by the work in \\[lar21\\]. The rank of the Lie algebra associated with the ansatz is computed, truncated to a maximum value $T$. In this case, the criteria can both be associated with the expressibility (higher rank leads to higher expressibility) or with the efficiency of simulation on a classical device (higher rank leads to harder to simulate unitaries). \n" 63 | ] 64 | }, 65 | { 66 | "cell_type": "code", 67 | "execution_count": null, 68 | "id": "36359d8b-8554-446b-91e1-eba97bfcf692", 69 | "metadata": {}, 70 | "outputs": [], 71 | "source": [ 72 | "from quask.evaluator import LieRankEvaluator\n", 73 | "t = 10_000 # threshold on the rank of the Lie algebra\n", 74 | "lr_eval = LieRankEvaluator(t)" 75 | ] 76 | }, 77 | { 78 | "cell_type": "markdown", 79 | "id": "e27bf3dd-73e9-4c29-ac2e-8e106074112f", 80 | "metadata": {}, 81 | "source": [ 82 | "### Covering numbers evaluator\n", 83 | "\n", 84 | "A criteria inspired by the work in \\[du22\\]. The expressibility is upper bounded by a quantity exponential in the number of trainable gates. In our context, it is quite a loose bound, but the original article allows to consider a more precise bounds in some cases." 85 | ] 86 | }, 87 | { 88 | "cell_type": "code", 89 | "execution_count": null, 90 | "id": "1dace440-d38d-4fcb-9f8c-832760f1760b", 91 | "metadata": {}, 92 | "outputs": [], 93 | "source": [ 94 | "from quask.evaluator import CoveringNumberEvaluator\n", 95 | "cn_eval = CoveringNumberEvaluator()" 96 | ] 97 | }, 98 | { 99 | "cell_type": "markdown", 100 | "id": "d6a29536-f1eb-4c1c-9b15-0456c7a4ef6a", 101 | "metadata": {}, 102 | "source": [ 103 | "## Depending on the kernel and on the training features, but not on the labels\n", 104 | "\n", 105 | "These criteria depends on the kernel itself and the training data. " 106 | ] 107 | }, 108 | { 109 | "cell_type": "markdown", 110 | "id": "bcda3c04-00d6-489c-bf55-1376ff7b5014", 111 | "metadata": {}, 112 | "source": [ 113 | "### Geometric difference evaluator\n", 114 | "\n", 115 | "A criteria inspired by the work in \\[hua21\\]. The geometric difference has been extensively studied in the [Projected quantum kernels tutorial](\"../tutorial_quantum/quantum_2_projected\"). It is used as follows:" 116 | ] 117 | }, 118 | { 119 | "cell_type": "code", 120 | "execution_count": null, 121 | "id": "70bfcef1-3853-401c-a9f7-cec61fe3f4d2", 122 | "metadata": {}, 123 | "outputs": [], 124 | "source": [ 125 | "from quask.evaluator import GeometricDifferenceEvaluator\n", 126 | "\n", 127 | "Kc1 = ... # first classical kernel\n", 128 | "Kc2 = ... # second classical kernel\n", 129 | "# ...\n", 130 | "Kc100 = ... # last classical kernel\n", 131 | "lam = 0.0001 # regularization \n", 132 | "\n", 133 | "gd_eval = GeometricDifferenceEvaluator([Kc1, Kc2, ..., Kc100], lam)" 134 | ] 135 | }, 136 | { 137 | "cell_type": "markdown", 138 | "id": "9094fa19-4c26-4321-8f9b-44e6488aeb59", 139 | "metadata": {}, 140 | "source": [ 141 | "## Depending on the kernel, the training features and training labels\n" 142 | ] 143 | }, 144 | { 145 | "cell_type": "markdown", 146 | "id": "8d9d7a20-3cea-47b4-9001-9328da896ca2", 147 | "metadata": {}, 148 | "source": [ 149 | "### Kernel alignment evaluator\n", 150 | "\n", 151 | "A criteria inspired by the work in \\[cri01\\]. " 152 | ] 153 | }, 154 | { 155 | "cell_type": "code", 156 | "execution_count": null, 157 | "id": "661b3276-602e-47a5-b390-f7e14ed019c3", 158 | "metadata": {}, 159 | "outputs": [], 160 | "source": [ 161 | "from quask.evaluator import KernelAlignmentEvaluator\n", 162 | "ka_eval = KernelAlignmentEvaluator()" 163 | ] 164 | }, 165 | { 166 | "cell_type": "markdown", 167 | "id": "92d4f4a8-fed1-44bd-afb5-82268bfa7476", 168 | "metadata": {}, 169 | "source": [ 170 | "### Centered Kernel alignment evaluator\n", 171 | "\n", 172 | "A criteria inspired by the work in \\[cor12\\]." 173 | ] 174 | }, 175 | { 176 | "cell_type": "code", 177 | "execution_count": null, 178 | "id": "4ab4991c-9ea9-41f7-ab22-683cb3b949cc", 179 | "metadata": {}, 180 | "outputs": [], 181 | "source": [ 182 | "from quask.evaluator import CenteredKernelAlignmentEvaluator\n", 183 | "cka_eval = CenteredKernelAlignmentEvaluator()" 184 | ] 185 | }, 186 | { 187 | "cell_type": "markdown", 188 | "id": "69e1a559-c4fe-456b-852b-c7e0b366b188", 189 | "metadata": {}, 190 | "source": [ 191 | "### Ridge generalization evaluator\n" 192 | ] 193 | }, 194 | { 195 | "cell_type": "code", 196 | "execution_count": null, 197 | "id": "b38103f8-ad3d-47ee-935a-5f8a5c04a4e5", 198 | "metadata": {}, 199 | "outputs": [], 200 | "source": [ 201 | "from quask.evaluator import RidgeGeneralizationEvaluator\n", 202 | "rg_eval = RidgeGeneralizationEvaluator()" 203 | ] 204 | }, 205 | { 206 | "cell_type": "markdown", 207 | "id": "9859de1f-9894-420a-a0db-71bbaf4db301", 208 | "metadata": {}, 209 | "source": [ 210 | "### 'S' model complexity evaluator\n", 211 | "\n", 212 | "A criteria inspired by the work in \\[hua21\\]. The 'S' model complexity has been extensively studied in the [Projected quantum kernels tutorial](\"../tutorial_quantum/quantum_2_projected.html\"). It is used as follows:" 213 | ] 214 | }, 215 | { 216 | "cell_type": "code", 217 | "execution_count": null, 218 | "id": "dcfd35cf-458d-48ce-b48d-cd5c8c5b4c9f", 219 | "metadata": {}, 220 | "outputs": [], 221 | "source": [ 222 | "from quask.evaluator import EssModelComplexityEvaluator\n", 223 | "smc_eval = EssModelComplexityEvaluator()" 224 | ] 225 | }, 226 | { 227 | "cell_type": "markdown", 228 | "id": "d31d3fd5-991c-4b26-a896-78ffeaad478a", 229 | "metadata": {}, 230 | "source": [ 231 | "### Spectral bias evaluator\n", 232 | "\n", 233 | "A criteria inspired by the work in \\[can21\\]. " 234 | ] 235 | }, 236 | { 237 | "cell_type": "code", 238 | "execution_count": null, 239 | "id": "4de53f91-2057-4dd9-94a9-ad9832fa21b6", 240 | "metadata": {}, 241 | "outputs": [], 242 | "source": [ 243 | "from quask.evaluator import SpectralBiasEvaluator\n", 244 | "sb_eval = SpectralBiasEvaluator(10)" 245 | ] 246 | }, 247 | { 248 | "cell_type": "markdown", 249 | "id": "2e08e7c5-2245-404d-b9f6-871fce5ff587", 250 | "metadata": {}, 251 | "source": [ 252 | "## Add your own criteria\n", 253 | "\n" 254 | ] 255 | }, 256 | { 257 | "cell_type": "markdown", 258 | "id": "9161c827-7e53-43df-a42e-5b97d5c84b83", 259 | "metadata": {}, 260 | "source": [ 261 | "## References \n", 262 | "\n", 263 | "\\[sim19\\] Sim, Sukin, Peter D. Johnson, and Alán Aspuru‐Guzik. \"Expressibility and entangling capability of parameterized quantum circuits for hybrid quantum‐classical algorithms.\" Advanced Quantum Technologies 2.12 (2019): 1900070.\n", 264 | "\n", 265 | "\\[lar21\\] Larocca, Martin, et al. \"Diagnosing barren plateaus with tools from quantum optimal control.\" Quantum 6 (2022): 824.\n", 266 | "\n", 267 | "\\[du22\\] Du, Yuxuan, et al. \"Efficient measure for the expressivity of variational quantum algorithms.\" Physical Review Letters 128.8 (2022): 080506.\n", 268 | "\n", 269 | "\\[cri01\\] Cristianini, Nello, et al. \"On kernel-target alignment.\" Advances in neural information processing systems 14 (2001).\n", 270 | "\n", 271 | "\\[cor12\\] Cortes, Corinna, Mehryar Mohri, and Afshin Rostamizadeh. \"Algorithms for learning kernels based on centered alignment.\" The Journal of Machine Learning Research 13.1 (2012): 795-828.\n", 272 | "\n", 273 | "\\[can21\\] Canatar, Abdulkadir, Blake Bordelon, and Cengiz Pehlevan. \"Spectral bias and task-model alignment explain generalization in kernel regression and infinitely wide neural networks.\" Nature communications 12.1 (2021): 2914.\n", 274 | "\n", 275 | "\\[hua21\\] Huang, HY., Broughton, M., Mohseni, M. et al. Power of data in quantum machine learning. Nat Commun 12, 2631 (2021). https://doi.org/10.1038/s41467-021-22539-9" 276 | ] 277 | }, 278 | { 279 | "cell_type": "raw", 280 | "id": "2c630cb4-56e4-4ef5-806e-c324b06bec81", 281 | "metadata": {}, 282 | "source": [ 283 | ".. note::\n", 284 | "\n", 285 | " Author's note." 286 | ] 287 | } 288 | ], 289 | "metadata": { 290 | "kernelspec": { 291 | "display_name": "Python 3 (ipykernel)", 292 | "language": "python", 293 | "name": "python3" 294 | }, 295 | "language_info": { 296 | "codemirror_mode": { 297 | "name": "ipython", 298 | "version": 3 299 | }, 300 | "file_extension": ".py", 301 | "mimetype": "text/x-python", 302 | "name": "python", 303 | "nbconvert_exporter": "python", 304 | "pygments_lexer": "ipython3", 305 | "version": "3.10.12" 306 | } 307 | }, 308 | "nbformat": 4, 309 | "nbformat_minor": 5 310 | } 311 | -------------------------------------------------------------------------------- /docs/source/tutorials_quantum/quantum_2_projected.rst: -------------------------------------------------------------------------------- 1 | Projected quantum kernels 2 | ========================= 3 | 4 | To understand Projected Quantum Kernels we should understand the limitations 5 | of "traditional" quantum kernels. These limitations have very deep implications 6 | in the understanding of kernel methods also on a classical perspective. 7 | 8 | Expressibility and curse of dimensionality in kernel methods 9 | ------------------------------------------------------------ 10 | 11 | When approaching a ML problem we could ask if it makes sense at all to use 12 | QML techniques, such as quantum kernel methods. We understood in the last years 13 | [kbs21],[Hua21] that having a large Hilbert space where we can compute 14 | classically intractable inner products does not guarantee an advantage. But, why? 15 | 16 | When dealing with kernel methods, whether classical or quantum, we must 17 | exercise caution when working in high-dimensional (or even 18 | infinite-dimensional) Hilbert spaces. This is due to the fact that in 19 | high dimensions, the problem of generalization becomes hard, *i.e.* the 20 | trained kernel is prone to overfitting. 21 | In turn, an exponential (in the number of features/qubits) number of datapoints 22 | are needed to learn the target function we aim to estimate. 23 | These phenomena are explored in the `upcoming tutorial `__. 24 | 25 | For instance, in the classical context, the Gaussian kernel maps any 26 | :math:`\mathbf{x} \in \mathbb{R}^d` to a multi-dimensional Gaussian 27 | distribution with an average of :math:`\mathbf{x}` and a covariance 28 | matrix of :math:`\sigma I`. When :math:`\sigma` is small, data points 29 | are mapped to different regions of this infinite-dimensional Hilbert 30 | space, and :math:`\kappa(\mathbf{x}, \mathbf{x}') \approx 0` for all 31 | :math:`\mathbf{x} \neq \mathbf{x}'`. This is known as the phenonenon of 32 | *curse of dimensionality*, or *orthogonality catastrophe*. To avoid this, a larger 33 | :math:`\sigma` is chosen to ensure that most data points relevant to our 34 | task have some nontrivial overlap. 35 | 36 | As the Hilbert space for quantum systems grows exponentially with the 37 | number of qubits :math:`n`, similar challenges can arise when using 38 | quantum kernels. This situation occurs with expressible 39 | :math:`U(\cdot)`, which allows access to various regions within the 40 | Hilbert space. In such cases, similar to classical kernels, techniques 41 | must be employed to control expressibility and, consequently, the 42 | model’s complexity. 43 | 44 | Projection as expressibility control 45 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 46 | 47 | The authors of [Hua21], who initially addressed the challenge of the 48 | exponential dimensionality of Hilbert space in the context of quantum 49 | kernels, have introduced the concept of *projected quantum kernels* to 50 | mitigate this issue. Then, in [kbs21] they proved as this projected kernel 51 | must intertwine with a correct inductive bias to obtain some good performance. 52 | 53 | The concept is straightforward: first, the unitary transformation 54 | :math:`U` maps classical data into the Hilbert space of the quantum 55 | system. Subsequently, a projection maps these elements back to a 56 | lower-dimensional Hilbert space. The overall transformation, thanks to 57 | the contribution of :math:`U`, remains beyond the capabilities of 58 | classical kernels. 59 | 60 | For a single data point encoded in the quantum system, denoted as 61 | :math:`\rho_x = U(x) \rho_0 U(x)`, projected quantum kernels can be 62 | implemented in two different ways: 63 | 64 | * We can implement the feature map 65 | :math:`\phi(x) = \mathrm{\tilde{Tr}}[\rho_x]`, with 66 | :math:`\mathrm{\tilde{Tr}}` representing partial trace. 67 | 68 | * Alternatively, we can implement the feature map 69 | :math:`\phi(x) = \{ \mathrm{Tr}[\rho_x O^{(j)}] \}_{j=1}^k`, where the 70 | observable :math:`O^{(j)}` is employed for the projections. 71 | 72 | Finally, the kernel :math:`\kappa(x, x')` is explicitly constructed as 73 | the inner product between :math:`\phi(x)` and :math:`\phi(x')`. 74 | 75 | Implementation of projected quantum kernel in *quask* 76 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 77 | 78 | We first create the parameterized quantum circuit :math:`U` as in the 79 | previous tutorials. 80 | 81 | .. code:: ipython3 82 | 83 | from quask.core import Ansatz, Kernel, KernelFactory, KernelType 84 | 85 | N_FEATURES = 2 86 | N_OPERATIONS = 3 87 | N_QUBITS = 2 88 | ansatz = Ansatz(n_features=N_FEATURES, n_qubits=N_QUBITS, n_operations=N_OPERATIONS) 89 | ansatz.initialize_to_identity() 90 | ansatz.change_operation(0, new_feature=0, new_wires=[0, 1], new_generator="ZZ", new_bandwidth=1.0) 91 | ansatz.change_operation(1, new_feature=1, new_wires=[0, 1], new_generator="XX", new_bandwidth=1.0) 92 | ansatz.change_operation(2, new_feature=2, new_wires=[0, 1], new_generator="IX", new_bandwidth=0.123) 93 | 94 | Now, by employing the SWAP test over a subset of the :math:`n` qubits, 95 | only a small and constant number of qubits are measured while the rest 96 | remain unmeasured. This calculation is equivalent to performing the 97 | inner product between partial traces of two quantum-encoded data points 98 | can be achieved. 99 | 100 | In the following example, the measurement is performed only on the first 101 | of two qubits. 102 | 103 | .. code:: ipython3 104 | 105 | kernel = KernelFactory.create_kernel(ansatz, "ZI", KernelType.SWAP_TEST) 106 | 107 | We can also obtain the kernel by projecting onto a single observable 108 | described by a Pauli string. 109 | 110 | .. code:: ipython3 111 | 112 | kernel = KernelFactory.create_kernel(ansatz, "XY", KernelType.OBSERVABLE) 113 | 114 | Multiple observable can be tested if we compose together kernel 115 | functions made of different observables. Due to the properties of 116 | positive semidefinite functions, the sum and product and tensor of 117 | positive semidefinite operators is again positive semidefinite. 118 | 119 | Learning of quantum processes 120 | ----------------------------- 121 | 122 | The projected quantum kernel finds application in the realm of learning 123 | a quantum process, described by a function: 124 | 125 | .. math:: f(x) = \mathrm{Tr}[U^\dagger(x) \rho_0 U(x) O] 126 | 127 | Here, :math:`U` represents a parameterized quantum circuit, 128 | :math:`\rho_0` is the initial state, and :math:`O` stands for the 129 | observable. This family of functions carries significant theoretical 130 | importance, as it has facilitated the formal demonstration of quantum 131 | advantages. It also holds practical significance, as certain use cases 132 | in physics and chemistry can be conceptualized as quantum processes. 133 | 134 | We are given a dataset, denoted as 135 | :math:`\{ (x^{(j)}, y^{(j)}) \}_{j=1}^m`. Additionally, we assume that 136 | each label in this dataset is noise-free, meaning that 137 | :math:`y^{(j)} = f(x^{(j)})`. 138 | 139 | S-value 140 | ~~~~~~~ 141 | 142 | We can train a kernel machine on a dataset using a kernel 143 | :math:`\kappa`. The resulting model takes the form 144 | :math:`h(x) = w^\top \phi(x)`. This representation is a kernel machine 145 | in its primal form, and the corresponding kernel Gram matrix is defined 146 | as :math:`K = [\kappa(x^{(i)}, x^{(j)})]_{i,j=1}^m`. Assuming that the 147 | kernel Gram matrix is normalized, i.e., :math:`\mathrm{Tr}[K]=m`, we can 148 | define the *s-value*, a quantity that depends on the process :math:`f`, 149 | the input data, and the kernel Gram matrix $K: 150 | 151 | .. math:: s_K = \sum_{i,j=1}^m (K_{i,j}^{-1}) \, f(x^{(i)}) \, f(x^{(j)}) 152 | 153 | This value quantifies how well the kernel function captures the behavior 154 | of the quantum process. The kernel is indeed able to capture the 155 | relationship within the data if: 156 | 157 | .. math:: \kappa(x^{(i)}, x^{(j)}) \approx f(x^{(i)}) \, f(x^{(j)}) 158 | 159 | It’s important to note that :math:`s_K = \lVert w \rVert`, making it a 160 | measure of the model’s complexity. Higher values of :math:`s_K` suggest 161 | that the kernel machine :math:`h` becomes a more complex function, which 162 | can lead to overfitting and poor generalization performance. 163 | 164 | Geometric difference 165 | ~~~~~~~~~~~~~~~~~~~~ 166 | 167 | While the quantity :math:`s_K` compare a kernel and the target function, 168 | the geometric difference quantifies the divergence between two kernels. 169 | 170 | Assume for the two kernel matrices :math:`K_1, K_2` that their trace is 171 | equal to :math:`m`. This is a valid assumption for quantum kernels, as 172 | the inner product between unitary vectors (or corresponding density 173 | matrices) is one, which then has to be multiplied for the :math:`m` 174 | elements. For classical kernels, the Gram matrix needs to be normalized. 175 | 176 | The geometric difference is defined by 177 | 178 | .. math:: g(K_1, K_2) = \sqrt{\lVert \sqrt{K_2} K_1^{-1} \sqrt{K_2} \rVert_{\infty}}, 179 | 180 | where :math:`\lVert \cdot \rVert_\infty` is the spectral norm, i.e. the 181 | largest singular value. 182 | 183 | One should use the geometric difference to compare the quantum kernel 184 | :math:`K_Q` with several classical kernels 185 | :math:`K_{C_1}, K_{C_2}, ...`. Then, :math:`\min g(K_C, K_Q)` has to be 186 | calculated: \* if this difference is small, 187 | :math:`g(K_C, K_Q) \ll \sqrt{m}`, then one of the classical kernels, the 188 | one with the smallest geometric difference, is guaranteed to provide 189 | similar performances; \* if the difference is high, 190 | :math:`g(K_C, K_Q) \approx \sqrt{m}`, the quantum kernel might 191 | outperform all the classical kernels tested. 192 | 193 | Geometry Test 194 | ~~~~~~~~~~~~~ 195 | 196 | The geometry test, introduced by [Hua21], serves as a means to assess 197 | whether a particular dataset holds the potential for a quantum advantage 198 | or if such an advantage is unlikely. The test operates as follows: 199 | 200 | - When :math:`g(K_C, K_Q) \ll \sqrt{m}`, a classical kernel exhibits 201 | behavior similar to the quantum kernel, rendering the use of the 202 | quantum kernel redundant. 203 | 204 | - When :math:`g(K_C, K_Q) \approx \sqrt{m}`, the quantum kernel 205 | significantly deviates from all tested classical kernels. The outcome 206 | depends on the complexity of classical kernel machines: 207 | 208 | - If the complexity of any classical kernel machine is low 209 | (:math:`s_{K_C} \ll m`), classical kernels perform well, and the 210 | quantum kernel’s divergence from classical :math:`K_C`, doesn’t 211 | yield superior performance. 212 | - When the complexity of all classical kernel machines is high 213 | (:math:`s_{K_C} \approx m`), classical models struggle to learn 214 | the function :math:`f`. In this scenario: 215 | 216 | - If the quantum model’s complexity is low 217 | (:math:`s_{K_Q} \ll m`), the quantum kernel successfully solves 218 | the task while the classical models do not. 219 | - If the quantum model’s complexity is high 220 | (:math:`s_{K_Q} \approx m`), even the quantum model struggles 221 | to solve the problem. 222 | 223 | 224 | 225 | .. code:: ipython3 226 | 227 | from quask.evaluator import EssEvaluator, GeometricDifferenceEvaluator, GeometryTestEvaluator 228 | 229 | 230 | .. parsed-literal:: 231 | 232 | 233 | KeyboardInterrupt 234 | 235 | 236 | 237 | References & acknowledgements 238 | ----------------------------- 239 | 240 | [Hua21] Huang, HY., Broughton, M., Mohseni, M. et al."Power of data in 241 | quantum machine learning." Nat Commun 12, 2631 (2021). 242 | https://doi.org/10.1038/s41467-021-22539-9 243 | 244 | [kbs21] Jonas M. Kübler, Simon Buchholz, Bernhard Schölkopf. "The 245 | Inductive Bias of Quantum Kernels." arXiv:2106.03747 (2021). 246 | https://doi.org/10.48550/arXiv.2106.03747 247 | 248 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = quask 3 | version = 2.0.0-alpha2 4 | author = Massimiliano Incudini, Francesco Di Marcantonio, Roman Wixinger, Sofia Vallecorsa, Michele Grossi 5 | author_email = massimiliano.incudini@univr.it 6 | description = Quantum Advantage Seeker with Kernels (QuASK): a software framework to speed up the research in quantum machine learning 7 | license_files = LICENSE.txt 8 | url = https://quask.web.cern.ch/ 9 | classifiers = 10 | Intended Audience :: Developers 11 | License :: OSI Approved :: Apache Software License 12 | Programming Language :: Python :: 3 13 | Programming Language :: Python :: 3.9 14 | Programming Language :: Python :: 3.10 15 | 16 | [options] 17 | python_requires = >=3.8 18 | install_requires = 19 | pennylane==0.33.1 20 | pytest>=7.4.3 21 | numpy>=1.23.5 22 | pandas>=2.1.2 23 | scikit-learn>=1.3.1 24 | scipy>=1.11.2 25 | 26 | [options.packages.find] 27 | where = src 28 | 29 | [options.extras_require] 30 | demo = 31 | jupyter 32 | docs = 33 | sphinx>=4.5.0 34 | sphinx-rtd-theme>=1.0.0 35 | sphinxcontrib-napoleon>=0.7 36 | sphinx-autoapi>=2.0.1 37 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | # Read the contents of the README.md file 4 | with open("README.md", "r", encoding="utf-8") as readme_file: 5 | long_description = readme_file.read() 6 | 7 | setup( 8 | long_description=long_description, 9 | long_description_content_type="text/markdown", 10 | ) 11 | -------------------------------------------------------------------------------- /src/quask.egg-info/PKG-INFO: -------------------------------------------------------------------------------- 1 | Metadata-Version: 2.1 2 | Name: quask 3 | Version: 2.0.0a1 4 | Summary: Quantum Advantage Seeker with Kernels (QuASK): a software framework to speed up the research in quantum machine learning 5 | Home-page: https://github.com/CERN-IT-INNOVATION/QuASK 6 | Author: Massimiliano Incudini, Francesco Di Marcantonio, Roman Wixinger, Sofia Vallecorsa, Michele Grossi 7 | Author-email: massimiliano.incudini@univr.it 8 | Classifier: Intended Audience :: Developers 9 | Classifier: License :: OSI Approved :: Apache Software License 10 | Classifier: Programming Language :: Python :: 3 11 | Classifier: Programming Language :: Python :: 3.9 12 | Classifier: Programming Language :: Python :: 3.10 13 | Requires-Python: >=3.8 14 | Description-Content-Type: text/markdown 15 | Provides-Extra: demo 16 | Provides-Extra: docs 17 | License-File: LICENSE.txt 18 | 19 | # Quantum Advantage Seeker with Kernels (QuASK) 20 | 21 | QuASK is an actively maintained library for constructing, studying, and benchmarking quantum kernel methods. 22 | 23 | It is designed to simplify the process of choosing a quantum kernel, automate the machine learning pipeline at all its stages, and provide pedagogical guidance for early-stage researchers to utilize these tools to their full potential. 24 | 25 | QuASK promotes the use of reusable code and is available as a library that can be seamlessly integrated into existing code bases. It is written in Python 3, can be easily installed using pip, and is accessible on PyPI. 26 | 27 | ## Installation 28 | 29 | The easiest way to use *quask* is by installing it in your Python3 30 | environment (version >= 3.10) via the *pip* packet manager, 31 | 32 | python3 -m pip install quask==2.0.0-alpha1 33 | 34 | You also need any quantum SDK installed on your system. For example, we can install Qiskit (but we can also work with Pennylane, Braket, Qibo, and the modular nature of the software allows the creation of your own custom backends). 35 | 36 | python3 -m pip install qiskit qiskit_ibm_runtime 37 | python3 -m pip install qiskit_ibm_runtime --upgrade 38 | python3 -m pip install qiskit-aer 39 | 40 | See the [Installation section](https://quask.readthedocs.io/en/latest/installation.html) 41 | of our documentation page for more information. 42 | 43 | ## Examples 44 | 45 | The fastest way to start developing using _quask_ is via our [Getting started](https://quask.readthedocs.io/en/latest/getting_started.html) guide. 46 | 47 | If you are not familiar with the concept of kernel methods in classical machine learning, we have developed a [series of introductory tutorials](https://quask.readthedocs.io/en/latest/tutorials_classical/index.html) on the topic. 48 | 49 | If you are not familiar with the concept of quantum kernels, we have developed a [series of introductory tutorials](https://quask.readthedocs.io/en/latest/tutorials_quantum/index.html) on the topic, which is also used to showcase the basic functionalities of _quask_. 50 | 51 | Then [advanced features of _quask_](https://quask.readthedocs.io/en/latest/tutorials_quask/index.html) are shown, including the use of different backends, the criteria to evaluate a quantum kernel, and the automatic optimization approach. 52 | 53 | Finally, [look here for some applications](https://quask.readthedocs.io/en/latest/tutorials_applications/index.html). 54 | 55 | 56 | ## Source 57 | 58 | 59 | ### Deployment to PyPI 60 | 61 | The software is uploaded to [PyPI](https://pypi.org/project/quask/). 62 | 63 | ### Test 64 | 65 | The suite of test for _quask_ is currently under development.To run the available tests, type 66 | 67 | pytest 68 | 69 | 70 | You can also specify specific test scripts. 71 | 72 | pytest tests/test_example.py 73 | 74 | _quask_ has been developed and tested with the following versions of the quantum frameworks: 75 | 76 | * PennyLane==0.32.0 77 | * PennyLane-Lightning==0.32.0 78 | * qiskit==0.44.1 79 | * qiskit-aer==0.12.2 80 | * qiskit-ibm-runtime==0.14.0 81 | 82 | 83 | ## Documentation 84 | 85 | The documentation is available at our [Read the Docs](https://quask.readthedocs.io/en/latest/) domain. 86 | 87 | ### Generate the documentation 88 | 89 | The documentation has been generated with Sphinx (v7.2.6) and uses the Furo theme. To install it, run 90 | 91 | python3 -m pip install -U sphinx 92 | python3 -m pip install furo 93 | 94 | To generate the documentation, run 95 | 96 | cd docs 97 | make clean && make html 98 | 99 | The Sphinx configuration file (`conf.py`) has the following, non-standard options: 100 | 101 | html_theme = 'furo' 102 | html_theme_options = { 103 | "sidebar_hide_name": True 104 | } 105 | autodoc_mock_imports = ["skopt", "skopt.space", "django", "mushroom_rl", "opytimizer", "pennylane", "qiskit", "qiskit_ibm_runtime", "qiskit_aer"] 106 | 107 | ### Generate the UML diagrams 108 | 109 | Currently, the pages generated from the Python notebooks has to be compiled to RST format manually. We could use in the future the [nbsphinx extension](https://docs.readthedocs.io/en/stable/guides/jupyter.html) to automatize this process. This has the advantage that the documentation is always up to date, the disadvantage is that the process is much slower. 110 | 111 | ### Generate the UML diagrams 112 | 113 | The UML diagrams in the [Platform overview](https://quask.readthedocs.io/en/latest/platform_overview.html) page of the documentation are generated using pyreverse and Graphviz. They can be installed via: 114 | 115 | sudo apt-get install graphviz 116 | python3 -m pip install pylint 117 | 118 | The UML diagrams are created via: 119 | 120 | cd src/quask 121 | pyreverse -o png -p QUASK . 122 | 123 | 124 | ## Acknowledgements 125 | 126 | The platform has been developed with the contribution of [Massimiliano Incudini](https://incud.github.io), Francesco Di Marcantonio, Davide Tezza, Roman Wixinger, Sofia Vallecorsa, and [Michele Grossi](https://scholar.google.com/citations?user=cnfcO7cAAAAJ&hl=en). 127 | 128 | If you have used _quask_ for your project, please consider citing us. 129 | 130 | @article{dimarcantonio2023quask, 131 | title={Quantum Advantage Seeker with Kernels (QuASK): a software framework to accelerate research in quantum machine learning}, 132 | author={Di Marcantonio, Francesco and Incudini, Massimiliano and Tezza, Davide and Grossi, Michele}, 133 | journal={Quantum Machine Intelligence}, 134 | volume={5}, 135 | number={1}, 136 | pages={20}, 137 | year={2023}, 138 | publisher={Springer} 139 | } 140 | -------------------------------------------------------------------------------- /src/quask.egg-info/SOURCES.txt: -------------------------------------------------------------------------------- 1 | LICENSE.txt 2 | README.md 3 | setup.cfg 4 | setup.py 5 | src/quask/__init__.py 6 | src/quask.egg-info/PKG-INFO 7 | src/quask.egg-info/SOURCES.txt 8 | src/quask.egg-info/dependency_links.txt 9 | src/quask.egg-info/requires.txt 10 | src/quask.egg-info/top_level.txt 11 | src/quask/core/__init__.py 12 | src/quask/core/ansatz.py 13 | src/quask/core/kernel.py 14 | src/quask/core/kernel_factory.py 15 | src/quask/core/kernel_type.py 16 | src/quask/core/operation.py 17 | src/quask/core_implementation/__init__.py 18 | src/quask/core_implementation/braket_kernel.py 19 | src/quask/core_implementation/pennylane_kernel.py 20 | src/quask/core_implementation/qibo_kernel.py 21 | src/quask/core_implementation/qiskit_kernel.py 22 | src/quask/evaluator/__init__.py 23 | src/quask/evaluator/centered_kernel_alignment_evaluator.py 24 | src/quask/evaluator/covering_number_evaluator.py 25 | src/quask/evaluator/ess_model_complexity_evaluator.py 26 | src/quask/evaluator/geometric_difference_evaluator.py 27 | src/quask/evaluator/haar_evaluator.py 28 | src/quask/evaluator/kernel_alignment_evaluator.py 29 | src/quask/evaluator/kernel_evaluator.py 30 | src/quask/evaluator/lie_rank_evaluator.py 31 | src/quask/evaluator/ridge_generalization_evaluator.py 32 | src/quask/evaluator/spectral_bias_evaluator.py 33 | src/quask/optimizer/__init__.py 34 | src/quask/optimizer/base_kernel_optimizer.py 35 | src/quask/optimizer/bayesian_optimizer.py 36 | src/quask/optimizer/greedy_optimizer.py 37 | src/quask/optimizer/metaheuristic_optimizer.py 38 | src/quask/optimizer/reinforcement_learning_optimizer.py 39 | src/quask/optimizer/wide_kernel_environment.py 40 | src/quask/tests/__init__.py 41 | src/quask/tests/kernel_test.py 42 | tests/test_example.py 43 | tests/test_pennylane_kernel.py -------------------------------------------------------------------------------- /src/quask.egg-info/dependency_links.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/quask.egg-info/not-zip-safe: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/quask.egg-info/requires.txt: -------------------------------------------------------------------------------- 1 | pennylane==0.33.1 2 | pytest>=7.4.3 3 | numpy>=1.23.5 4 | pandas>=2.1.2 5 | scikit-learn>=1.3.1 6 | scipy>=1.11.2 7 | 8 | [demo] 9 | jupyter 10 | 11 | [docs] 12 | sphinx>=4.5.0 13 | sphinx-rtd-theme>=1.0.0 14 | sphinxcontrib-napoleon>=0.7 15 | sphinx-autoapi>=2.0.1 16 | -------------------------------------------------------------------------------- /src/quask.egg-info/top_level.txt: -------------------------------------------------------------------------------- 1 | quask 2 | -------------------------------------------------------------------------------- /src/quask/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CERN-IT-INNOVATION/QuASK/3ac5bef5aff678e2239af4207ed9e89b8a0668b9/src/quask/__init__.py -------------------------------------------------------------------------------- /src/quask/core/__init__.py: -------------------------------------------------------------------------------- 1 | from .operation import Operation 2 | from .ansatz import Ansatz 3 | from .kernel_type import KernelType 4 | from .kernel_factory import KernelFactory 5 | from .kernel import Kernel 6 | -------------------------------------------------------------------------------- /src/quask/core/ansatz.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from typing import List 3 | from . import Operation 4 | 5 | 6 | class Ansatz: 7 | """ 8 | Class representing the Ansatz as list of Operations 9 | """ 10 | 11 | def __init__(self, n_features: int, n_qubits: int, n_operations: int, allow_midcircuit_measurement=False): 12 | """ 13 | Initialization 14 | :param n_features: number of feature that can be used to parametrize the operation 15 | :param n_qubits: number of qubits of the circuit 16 | :param n_operations: number of operations 17 | :param allow_midcircuit_measurement: True if mid-circuit measurement are allowed 18 | """ 19 | assert n_qubits >= 2, "This ansatz is specified for >= 2 qubits" 20 | assert n_features > 0, "Cannot have zero or negative number of features" 21 | assert n_operations > 0, "Cannot have zero or negative number or operations" 22 | self.n_features: int = n_features 23 | self.n_qubits: int = n_qubits 24 | self.n_operations: int = n_operations 25 | self.operation_list: List[Operation] = [None] * n_operations 26 | self.allow_midcircuit_measurement: bool = allow_midcircuit_measurement 27 | 28 | def change_operation(self, operation_index: int, new_feature: int, new_wires: List[int], new_generator: str, new_bandwidth: float): 29 | """ 30 | Overwrite the operation at the given index with a whole new set of data 31 | :param operation_index: index of the operation 32 | :param new_feature: feature parameterizing the operation 33 | :param new_wires: wires on which the operation is applied 34 | :param new_generator: generator of the operation 35 | :param new_bandwidth: bandwidth of the operation 36 | :return: None 37 | """ 38 | self.change_feature(operation_index, new_feature) 39 | self.change_wires(operation_index, new_wires) 40 | self.change_generators(operation_index, new_generator) 41 | self.change_bandwidth(operation_index, new_bandwidth) 42 | 43 | def change_bandwidth(self, operation_index: int, new_bandwidth: float): 44 | """ 45 | Overwrite the operation at the given index with a new bandwidth 46 | :param operation_index: index of the operation 47 | :param new_bandwidth: bandwidth of the operation 48 | :return: None 49 | """ 50 | assert 0 <= operation_index < self.n_operations, "Operation index out of bounds" 51 | self.operation_list[operation_index].bandwidth = new_bandwidth 52 | 53 | def change_generators(self, operation_index: int, new_generator: str): 54 | """ 55 | Overwrite the operation at the given index with a new generator 56 | :param operation_index: index of the operation 57 | :param new_generator: generator of the operation 58 | :return: None 59 | """ 60 | assert 0 <= operation_index < self.n_operations, "Operation index out of bounds" 61 | assert new_generator in Operation.OPERATIONS, f"Unknown generator {new_generator}" 62 | if not self.allow_midcircuit_measurement: 63 | assert new_generator not in Operation.MEASUREMENT_OPERATIONS, "Mid-circuit measurement not allowed" 64 | self.operation_list[operation_index].generator = new_generator 65 | 66 | def change_feature(self, operation_index: int, new_feature: int): 67 | """ 68 | Overwrite the operation at the given index with a new feature 69 | :param operation_index: index of the operation 70 | :param new_feature: feature parameterizing the operation 71 | :return: None 72 | """ 73 | assert 0 <= operation_index < self.n_operations, "Operation index out of bounds" 74 | assert 0 <= new_feature <= self.n_features, f"Feature index out of bounds ({new_feature=})" 75 | self.operation_list[operation_index].feature = new_feature 76 | 77 | def change_wires(self, operation_index: int, new_wires: List[int]): 78 | """ 79 | Overwrite the operation at the given index with a new pair of wires 80 | :param operation_index: index of the operation 81 | :param new_wires: wires on which the operation is applied 82 | :return: None 83 | """ 84 | assert 0 <= operation_index < self.n_operations, "Operation index out of bounds" 85 | assert len(new_wires) == 2, "The location is a list of two integers, not less no more" 86 | assert 0 <= new_wires[0] < self.n_qubits, f"First wire index out of bounds ({new_wires=})" 87 | assert 0 <= new_wires[1] < self.n_qubits, f"Second wire index out of bounds ({new_wires=})" 88 | assert new_wires[0] != new_wires[1], f"Cannot specify the same wire twice ({new_wires=})" 89 | self.operation_list[operation_index].wires = new_wires 90 | 91 | def get_allowed_operations(self): 92 | """ 93 | Get the list of allowed operation for the ansatz, either only the PAULI_GENERATORS or any operation including measurements 94 | :return: list of allowed operations 95 | """ 96 | if self.allow_midcircuit_measurement: 97 | return Operation.OPERATIONS 98 | else: 99 | return Operation.PAULI_GENERATORS 100 | 101 | def initialize_to_identity(self): 102 | """ 103 | Initialize the ansatz to the identity circuit 104 | :return: None 105 | """ 106 | self.operation_list = [None] * self.n_operations 107 | for i in range(self.n_operations): 108 | self.operation_list[i] = Operation("II", [0, 1], self.n_features, 1.0) 109 | 110 | def initialize_to_random_circuit(self): 111 | """ 112 | Initialize the ansatz to a random circuit 113 | :return: None 114 | """ 115 | for i in range(self.n_operations): 116 | generator = np.random.choice(self.get_allowed_operations()) 117 | wires = np.random.choice(list(range(self.n_qubits)), 2, replace=False) 118 | feature = np.random.choice(list(range(self.n_features + 1))) 119 | bandwidth = np.random.uniform(0.0, 1.0) 120 | self.operation_list[i] = Operation(generator, wires, feature, bandwidth) 121 | 122 | def initialize_to_known_ansatz(self, ansatz): 123 | """ 124 | Initialize the ansatz form an already given element 125 | :param ansatz: Given ansatz 126 | :return: None 127 | """ 128 | self.initialize_to_identity() 129 | for i in range(self.n_operations): 130 | op: Operation = ansatz.operation_list[i] 131 | self.change_operation(i, op.feature, op.wires, op.generator, op.bandwidth) 132 | 133 | def to_numpy(self): 134 | """ 135 | Serialize the ansatz to a numpy array 136 | :return: numpy array 137 | """ 138 | return np.array([op.to_numpy() for op in self.operation_list]).ravel() 139 | 140 | @staticmethod 141 | def from_numpy(array, n_features, n_qubits, n_operations, allow_midcircuit_measurement, shift_second_wire=False): 142 | """ 143 | Deserialize the ansatz from a numpy array 144 | :param array: numpy array 145 | :param n_features: number of feature that can be used to parametrize the operation 146 | :param n_qubits: number of qubits of the circuit 147 | :param n_operations: number of operations 148 | :param allow_midcircuit_measurement: True if mid-circuit measurement are allowed 149 | :return: Ansatz deserialized 150 | """ 151 | ans = Ansatz(n_features, n_qubits, n_operations, allow_midcircuit_measurement) 152 | ans.initialize_to_identity() 153 | for i in range(n_operations): 154 | # feature -> wires -> generator -> bandwidth 155 | generator = np.rint(array[i * 5]).astype(int) 156 | wires = [np.rint(array[i * 5 + 1]).astype(int), np.rint(array[i * 5 + 2]).astype(int)] 157 | feature = np.rint(array[i * 5 + 3]).astype(int) 158 | bandwidth = np.round(array[i * 5 + 4], decimals=4) 159 | if shift_second_wire and wires[1] >= wires[0]: 160 | wires[1] += 1 161 | ans.change_operation(i, feature, wires, Operation.OPERATIONS[generator], bandwidth) 162 | return ans 163 | 164 | def __str__(self): 165 | return str(self.operation_list) 166 | 167 | def __repr__(self): 168 | return self.__str__() -------------------------------------------------------------------------------- /src/quask/core/kernel.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy.spatial.distance import cdist 3 | from abc import ABC, abstractmethod 4 | from . import Ansatz, KernelType, KernelFactory 5 | 6 | 7 | class Kernel(ABC): 8 | """ 9 | Abstract class representing a kernel object 10 | """ 11 | 12 | PAULIS = ['I', 'X', 'Y', 'Z'] 13 | 14 | def __init__(self, ansatz: Ansatz, measurement: str, type: KernelType): 15 | """ 16 | Initialization. 17 | 18 | :param ansatz: Ansatz object representing the unitary transformation 19 | :param measurement: Pauli string representing the measurement 20 | :param type: type of kernel, fidelity or observable 21 | """ 22 | assert ansatz.n_qubits == len(measurement), "Measurement qubits and number of ansatz qubits do not match" 23 | assert len(set(measurement).difference(Kernel.PAULIS)) == 0, "Unknown Pauli in measurement" 24 | self.ansatz = ansatz 25 | self.measurement = measurement 26 | self.type = type 27 | self.last_probabilities = None 28 | 29 | def get_last_probabilities(self): 30 | """ 31 | Get the last kernel value calculated. 32 | 33 | :return: last probability array 34 | """ 35 | return np.array(self.last_probabilities) 36 | 37 | @abstractmethod 38 | def kappa(self, x1, x2) -> float: 39 | """ 40 | Calculate the kernel given two datapoints. 41 | 42 | :param x1: first data point 43 | :param x2: second data point 44 | :return: Kernel similarity between the two data points 45 | """ 46 | pass 47 | 48 | @abstractmethod 49 | def phi(self, x) -> float: 50 | """ 51 | Calculate the feature map of a data point. 52 | 53 | :param x: data point 54 | :return: feature map of the datapoint as numpy array 55 | """ 56 | pass 57 | 58 | def get_allowed_operations(self): 59 | """ 60 | Get the list of allowed operations. 61 | 62 | :return: list of generators allowed (the information is saved in the ansatz) 63 | """ 64 | return self.ansatz.get_allowed_operations() 65 | 66 | def build_kernel(self, X1: np.ndarray, X2: np.ndarray, matrix: str=None) -> np.ndarray: 67 | """ 68 | Build a kernel. 69 | 70 | :param X1: a single datapoint or a list of datapoints 71 | :param X2: a single datapoint or a list of datapoints 72 | :param matrix: training or testing matrix 73 | :return: a single or a matrix of kernel inner products 74 | """ 75 | # if you gave me only one sample 76 | if len(X1.shape) == 1 and len(X2.shape) == 1: 77 | if self.type in [KernelType.FIDELITY, KernelType.SWAP_TEST]: 78 | return self.kappa(X1, X2) 79 | else: 80 | return self.phi(X1) * self.phi(X2) 81 | 82 | # if you gave me multiple samples 83 | assert self.ansatz.n_features == X1.shape[1], "Number of features and X1.shape[1] do not match" 84 | assert self.ansatz.n_features == X2.shape[1], "Number of features and X2.shape[1] do not match" 85 | if self.type in [KernelType.FIDELITY, KernelType.SWAP_TEST]: 86 | if matrix == "train": 87 | return self.kernel_train_matrix(X1, X2) 88 | elif matrix == "test": 89 | return self.kernel_test_matrix(X1, X2) 90 | else: 91 | n = X1.shape[0] 92 | m = X2.shape[0] 93 | Phi1 = np.array([self.phi(x) for x in X1]).reshape((n, 1)) 94 | Phi2 = np.array([self.phi(x) for x in X2]).reshape((m, 1)) 95 | return Phi1.dot(Phi2.T) 96 | 97 | def kernel_train_matrix(self, X1, X2): 98 | N = X1.shape[0] 99 | kernel_matrix = np.full((N, N), np.nan) 100 | for i in range(kernel_matrix.shape[0]): 101 | for j in range(i,kernel_matrix.shape[1]): 102 | if i == j: 103 | kernel_matrix[i,j] = 1 104 | else: 105 | kernel_matrix[i,j] = self.kappa(X1[i], X2[j]) 106 | kernel_matrix[j,i] = kernel_matrix[i,j] 107 | return kernel_matrix 108 | 109 | def kernel_test_matrix(self, X1, X2): 110 | N_tr = X2.shape[0] 111 | N_te = X1.shape[0] 112 | kernel_matrix = np.full((N_te, N_tr), np.nan) 113 | for i in range(kernel_matrix.shape[0]): 114 | for j in range(kernel_matrix.shape[1]): 115 | kernel_matrix[i,j] = self.kappa(X1[i],X2[j]) 116 | return kernel_matrix 117 | 118 | def to_numpy(self): 119 | """ 120 | Serialize the kernel object as a numpy array. 121 | 122 | :return: numpy array 123 | """ 124 | ansatz_numpy = self.ansatz.to_numpy() 125 | measurement_numpy = np.array([Kernel.PAULIS.index(p) for p in self.measurement]) 126 | type_numpy = np.array([self.type.value]) 127 | return np.concatenate([ansatz_numpy, measurement_numpy, type_numpy], dtype=object).ravel() 128 | 129 | @staticmethod 130 | def from_numpy(array, n_features, n_qubits, n_operations, allow_midcircuit_measurement, shift_second_wire=False): 131 | """ 132 | Deserialize the object from a numpy array. 133 | 134 | :param array: numpy array 135 | :param n_features: number of feature that can be used to parametrize the operation 136 | :param n_qubits: number of qubits of the circuit 137 | :param n_operations: number of operations 138 | :param allow_midcircuit_measurement: True if mid-circuit measurement are allowed 139 | :return: Kernel object (created using default instance in KernelFactory) 140 | """ 141 | assert len(array) == 5 * n_operations + n_qubits + 1, f"Size of the array is {len(array)} instead of {5 * n_operations + n_qubits + 1}" 142 | ansatz_numpy = array[:n_operations*5] 143 | measurement_numpy = array[n_operations*5:-1] 144 | type_numpy = array[-1] 145 | ansatz = Ansatz.from_numpy(ansatz_numpy, n_features, n_qubits, n_operations, allow_midcircuit_measurement, shift_second_wire) 146 | measurement = "".join(Kernel.PAULIS[np.rint(i).astype(int)] for i in measurement_numpy) 147 | the_type = KernelType.convert(type_numpy) 148 | kernel = KernelFactory.create_kernel(ansatz, measurement, the_type) 149 | return kernel 150 | 151 | def __str__(self): 152 | return str(self.ansatz) + " -> " + self.measurement 153 | 154 | def __repr__(self): 155 | return self.__str__() 156 | 157 | def __copy__(self): 158 | return Kernel.from_numpy(self.to_numpy(), self.ansatz.n_features, self.ansatz.n_qubits, self.ansatz.n_operations, self.ansatz.allow_midcircuit_measurement) 159 | -------------------------------------------------------------------------------- /src/quask/core/kernel_factory.py: -------------------------------------------------------------------------------- 1 | from . import Ansatz, KernelType 2 | 3 | 4 | class KernelFactory: 5 | """ 6 | Instantiate the concrete object from classes that inherit from (abstract class) Kernel. 7 | Implement the self-registering factory pattern 8 | """ 9 | 10 | # to see the implementations and the current_implementation you can call it in this way: ._KernelFactory__implementations, ._KernelFactory__current_implementation 11 | __implementations = {} 12 | """Dictionary containing pairs (name, function to create the kernel).""" 13 | 14 | __current_implementation: str = "" 15 | """Name of the implementation to use right now to create the kernels""" 16 | 17 | @staticmethod 18 | def add_implementation(name, fn): 19 | """ 20 | Add the current closure function as one of the possible implementations available 21 | 22 | :param name: name of the implementation 23 | :param fn: function that creates the quantum kernel 24 | """ 25 | if name in KernelFactory.__implementations: 26 | raise ValueError("This name is already present in the register of available implementations") 27 | if fn.__code__.co_argcount != 3: 28 | raise ValueError("The function must have these three arguments, 'ansatz', 'measurement', and 'type': the number of argument does not match") 29 | if fn.__code__.co_varnames != ('ansatz', 'measurement', 'type'): 30 | raise ValueError("The function must have these three arguments, 'ansatz', 'measurement', and 'type': the name of some argument does not match") 31 | KernelFactory.__implementations[name] = fn 32 | 33 | 34 | @staticmethod 35 | def set_current_implementation(name): 36 | if name not in KernelFactory.__implementations: 37 | raise ValueError("This name is not present in the register of available implementations") 38 | KernelFactory.__current_implementation = name 39 | 40 | @staticmethod 41 | def create_kernel(ansatz: Ansatz, measurement: str, type: KernelType): 42 | """ 43 | Create a kernel object using the default class chosen. 44 | 45 | :param ansatz: Ansatz object representing the unitary transformation 46 | :param measurement: Pauli string representing the measurement 47 | :param type: type of kernel, fidelity, swap test or observable 48 | :return: kernel object of the default concrete class 49 | """ 50 | fn = KernelFactory.__implementations[KernelFactory.__current_implementation] 51 | return fn(ansatz, measurement, type) -------------------------------------------------------------------------------- /src/quask/core/kernel_type.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class KernelType(Enum): 5 | """ 6 | Possible types of kernel 7 | """ 8 | FIDELITY = 0 9 | OBSERVABLE = 1 10 | SWAP_TEST = 2 11 | 12 | @staticmethod 13 | def convert(item): 14 | if isinstance(item, KernelType): 15 | return item 16 | elif item < 0.5: 17 | return KernelType.FIDELITY 18 | elif 0.5 <= item < 1.5: 19 | return KernelType.OBSERVABLE 20 | else: 21 | return KernelType.SWAP_TEST 22 | -------------------------------------------------------------------------------- /src/quask/core/operation.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from typing import List 3 | import itertools 4 | 5 | 6 | class Operation: 7 | """ 8 | Class representing a 2-qubit rotational quantum gates $exp(-i \theta \sigma_1 \otimes \sigma_2)$ 9 | """ 10 | 11 | PAULI_GENERATORS = list(a + b for a, b in itertools.product(["I", "X", "Y", "Z"], repeat=2)) 12 | MEASUREMENT_OPERATIONS = ["IM", "MI"] 13 | OPERATIONS = PAULI_GENERATORS + MEASUREMENT_OPERATIONS 14 | 15 | def __init__(self, generator: str, wires: List[int], feature: int, bandwidth: float): 16 | """ 17 | Operation initializer 18 | :param generator: one of the elements of Operation.OPERATIONS 19 | :param wires: pair of integers 20 | :param feature: index of the feature parameterizing the element (can be -1 for constant feature '1') 21 | :param bandwidth: bandwidth parameter in range [0,1] 22 | """ 23 | self.generator: str = generator 24 | self.wires: List[int] = wires 25 | self.feature: int = feature 26 | self.bandwidth: float = bandwidth 27 | 28 | def to_numpy(self): 29 | """ 30 | Serialize the Operation object to a numpy array format 31 | :return: numpy array representing the operation 32 | """ 33 | return np.array([Operation.OPERATIONS.index(self.generator), self.wires[0], self.wires[1], self.feature, self.bandwidth]) 34 | 35 | @staticmethod 36 | def from_numpy(array): 37 | """ 38 | Deserialize the operation object given its numpy array description 39 | :param array: numpy array 40 | :return: Operation object 41 | """ 42 | op = Operation(None, None, None, None) 43 | op.generator = Operation.OPERATIONS[int(array[0])] 44 | op.wires = [int(array[1]), int(array[2])] 45 | op.feature = int(array[3]) 46 | op.bandwidth = float(array[4]) 47 | return op 48 | 49 | def __str__(self): 50 | return f"-i {self.bandwidth:0.2f} * x[{self.feature}] {self.generator}^({self.wires[0]},{self.wires[1]})" 51 | 52 | def __repr__(self): 53 | return self.__str__() -------------------------------------------------------------------------------- /src/quask/core_implementation/__init__.py: -------------------------------------------------------------------------------- 1 | from .pennylane_kernel import PennylaneKernel 2 | from .qiskit_kernel import QiskitKernel 3 | from .braket_kernel import BraketKernel 4 | from .qibo_kernel import QiboKernel 5 | -------------------------------------------------------------------------------- /src/quask/core_implementation/braket_kernel.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pennylane as qml 3 | from ..core import Ansatz, Kernel, KernelType 4 | from .pennylane_kernel import PennylaneKernel 5 | 6 | 7 | class BraketKernel(PennylaneKernel): 8 | 9 | def create_device(self, n_qubits): 10 | return qml.device( 11 | "braket.aws.qubit", 12 | device_arn=self.device_name, 13 | s3_destination_folder=(self.s3_bucket, self.s3_prefix), 14 | wires=n_qubits 15 | ) 16 | 17 | def __init__(self, ansatz: Ansatz, measurement: str, type: KernelType, 18 | device_name: str, s3_bucket: str, s3_prefix: str, n_shots: int = None): 19 | """ 20 | Initialization. 21 | 22 | :param ansatz: Ansatz object representing the unitary transformation 23 | :param measurement: Pauli string representing the measurement 24 | :param type: type of kernel, fidelity or observable 25 | :param device_name: name of the device, 'default.qubit' for noiseless simulation 26 | :param n_shots: number of shots when sampling the solution, None to have infinity 27 | """ 28 | 29 | super().__init__(ansatz, measurement, type, device_name, n_shots) 30 | self.s3_bucket = s3_bucket 31 | self.s3_prefix = s3_prefix 32 | 33 | -------------------------------------------------------------------------------- /src/quask/core_implementation/pennylane_kernel.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pennylane as qml 3 | from ..core import Ansatz, Kernel, KernelType 4 | 5 | 6 | def AnsatzTemplate(ansatz: Ansatz, params: np.ndarray, wires: np.ndarray): 7 | for operation in ansatz.operation_list: 8 | if "M" not in operation.generator: 9 | feature = np.pi if operation.feature == ansatz.n_features else params[operation.feature] 10 | qml.PauliRot(operation.bandwidth * feature, operation.generator, wires=wires[operation.wires]) 11 | elif operation.generator[0] == "M": 12 | qml.measure(wires[operation.wires[0]]) 13 | else: 14 | qml.measure(wires[operation.wires[1]]) 15 | 16 | 17 | def ChangeBasis(measurement: str): 18 | for i, pauli in enumerate(measurement): 19 | if pauli == 'X': 20 | qml.Hadamard(wires=[i]) 21 | elif pauli == 'Y': 22 | qml.S(wires=[i]) 23 | qml.Hadamard(wires=[i]) 24 | 25 | 26 | class PennylaneKernel(Kernel): 27 | 28 | def create_device(self, n_qubits): 29 | return qml.device(self.device_name, wires=n_qubits, shots=self.n_shots) 30 | 31 | def __init__(self, ansatz: Ansatz, measurement: str, type: KernelType, device_name: str = "default.qubit", n_shots: int = None): 32 | """ 33 | Initialization. 34 | 35 | :param ansatz: Ansatz object representing the unitary transformation 36 | :param measurement: Pauli string representing the measurement 37 | :param type: type of kernel, fidelity or observable 38 | :param device_name: name of the device, 'default.qubit' for noiseless simulation 39 | :param n_shots: number of shots when sampling the solution, None to have infinity 40 | """ 41 | 42 | super().__init__(ansatz, measurement, type) 43 | self.device_name = device_name 44 | self.n_shots = n_shots 45 | 46 | dev = self.create_device(self.ansatz.n_qubits) 47 | wires = np.array(list(range(self.ansatz.n_qubits))) 48 | measurement_wires = np.array([i for i in wires if measurement[i] != 'I']) 49 | if len(measurement_wires) == 0: 50 | measurement_wires = range(self.ansatz.n_qubits) 51 | 52 | @qml.qnode(dev) 53 | def fidelity_kernel(x1, x2): 54 | AnsatzTemplate(self.ansatz, x1, wires=wires) 55 | qml.adjoint(AnsatzTemplate)(self.ansatz, x2, wires=wires) 56 | ChangeBasis(self.measurement) 57 | return qml.probs(wires=measurement_wires) 58 | 59 | self.fidelity_kernel = fidelity_kernel 60 | 61 | @qml.qnode(dev) 62 | def observable_phi(x): 63 | AnsatzTemplate(self.ansatz, x, wires=wires) 64 | ChangeBasis(self.measurement) 65 | return qml.probs(wires=measurement_wires) 66 | 67 | self.observable_phi = observable_phi 68 | 69 | dev_swap = self.create_device(1+2*self.ansatz.n_qubits) 70 | n = self.ansatz.n_qubits 71 | 72 | @qml.qnode(dev_swap) 73 | def swap_kernel(x1, x2): 74 | qml.Hadamard(wires=[0]) 75 | AnsatzTemplate(self.ansatz, x1, wires=1+wires) 76 | AnsatzTemplate(self.ansatz, x2, wires=1+n+wires) 77 | for j in measurement_wires: 78 | qml.CSWAP(wires=[0, 1+j, 1+n+j]) 79 | qml.Hadamard(wires=[0]) 80 | return qml.probs(wires=[0]) 81 | 82 | self.swap_kernel = swap_kernel 83 | 84 | def kappa(self, x1, x2) -> float: 85 | if self.type == KernelType.OBSERVABLE: 86 | return self.phi(x1) * self.phi(x2) 87 | 88 | elif self.type == KernelType.FIDELITY: 89 | probabilities = self.fidelity_kernel(x1, x2) 90 | self.last_probabilities = probabilities 91 | return probabilities[0] 92 | 93 | elif self.type == KernelType.SWAP_TEST: 94 | probabilities = self.swap_kernel(x1, x2) 95 | self.last_probabilities = probabilities 96 | return np.max([2 * probabilities[0] - 1, 0.0]) 97 | 98 | def phi(self, x) -> float: 99 | if self.type == KernelType.OBSERVABLE: 100 | probabilities = self.observable_phi(x) 101 | self.last_probabilities = probabilities 102 | parity = lambda i: 1 if bin(i).count('1') % 2 == 0 else -1 103 | probabilities = np.array([parity(i) * probabilities[i] for i in range(len(probabilities))]) 104 | # sum_probabilities = np.sum(probabilities) 105 | # print(f"{sum_probabilities=} {probabilities=}") 106 | return np.sum(probabilities) 107 | 108 | elif self.type in [KernelType.FIDELITY, KernelType.SWAP_TEST]: 109 | raise ValueError("phi not available for fidelity kernels") 110 | 111 | else: 112 | raise ValueError("Unknown type, possible erroneous loading from a numpy array") 113 | 114 | -------------------------------------------------------------------------------- /src/quask/core_implementation/qibo_kernel.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from ..core import Ansatz, Kernel, KernelType 3 | 4 | class QiboKernel(Kernel): 5 | pass 6 | -------------------------------------------------------------------------------- /src/quask/core_implementation/qiskit_kernel.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from ..core import Ansatz, Kernel, KernelType 3 | 4 | # from qiskit import Aer, BasicAer, QuantumCircuit 5 | from qiskit.circuit import QuantumCircuit, ParameterVector 6 | from qiskit.circuit.library import PauliEvolutionGate 7 | from qiskit_ibm_runtime import QiskitRuntimeService, IBMBackend 8 | from qiskit.quantum_info import SparsePauliOp, Statevector 9 | from qiskit_ibm_runtime import SamplerV2 as IBMSampler 10 | from qiskit_ibm_runtime import EstimatorV2 as IBMEstimator 11 | from qiskit_ibm_runtime import RuntimeJobV2 12 | # from qiskit_ibm_runtime import Options 13 | from qiskit_ibm_runtime.options import SamplerOptions as sop 14 | from qiskit_ibm_runtime.options import EstimatorOptions as eop 15 | from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager 16 | from qiskit.result import QuasiDistribution 17 | from qiskit.primitives import BackendSamplerV2 as BackendSampler 18 | from qiskit.primitives import StatevectorSampler, StatevectorEstimator 19 | # from qiskit_aer.primitives import Sampler as AerSampler 20 | # from qiskit_aer.primitives import Estimator as AerEstimator 21 | 22 | class QiskitKernel(Kernel): 23 | 24 | def __init__(self, ansatz: Ansatz, measurement: str, type: KernelType, 25 | platform: str="finite_shots", backend: IBMBackend=None, n_shots: int=2048, 26 | options: dict=None, optimization_level: int=2, layout: list=None): 27 | """ 28 | Initialization. 29 | 30 | :param ansatz: Ansatz object representing the unitary transformation 31 | :param measurement: Pauli string representing the measurement 32 | :param type: type of kernel, fidelity or observable 33 | :param platform: name of the device, 'finite_shots' or 'infty_shots' for noiseless simulation 34 | :param backend: simulator or hardware backend 35 | :param n_shots: number of shots when sampling the solution, None to have infinity 36 | :param options: options of the sampler or estimator 37 | :param optimization_level: optimization level in case of transpilation 38 | :param layout: qubit layout of the physical circuit in case of transpilation 39 | """ 40 | super().__init__(ansatz, measurement, type) 41 | assert platform in ["infty_shots", "finite_shots", "ibm_quantum"] 42 | self.platform = platform 43 | self.backend = backend 44 | self.n_shots = n_shots 45 | self.options = options 46 | self.optimization_level = optimization_level 47 | self.layout = layout 48 | 49 | def get_backend(self, channel: str, ibm_token: str, group_instance: str, device: str=None): 50 | service = QiskitRuntimeService(channel=channel, token=ibm_token, instance=group_instance) 51 | if device == None: 52 | self.backend = service.least_busy(operational=True, simulator=False) 53 | else: 54 | self.backend = service.backend(device) 55 | print(f"{self.backend.name} selected") 56 | return self 57 | 58 | def get_sampler_options(self): 59 | sampler_options = sop( 60 | default_shots = self.n_shots, 61 | dynamical_decoupling = { 62 | "enable": bool(self.options["dynamical_decoupling"]["sequence_type"]), 63 | "sequence_type": self.options["dynamical_decoupling"]["sequence_type"] 64 | }, 65 | twirling = { 66 | "enable_gates": self.options["twirling"]["enable_gates"], 67 | "enable_measure": self.options["twirling"]["enable_measure"], 68 | "num_randomizations": "auto", 69 | "shots_per_randomization": "auto" 70 | } 71 | ) 72 | return sampler_options 73 | 74 | def get_estimator_options(self): 75 | estimator_options = eop( 76 | default_shots = self.n_shots, 77 | dynamical_decoupling = { 78 | "enable": bool(self.options["dynamical_decoupling"]["sequence_type"]), 79 | "sequence_type": self.options["dynamical_decoupling"]["sequence_type"] 80 | }, 81 | twirling = { 82 | "enable_gates": self.options["twirling"]["enable_gates"], 83 | "enable_measure": self.options["twirling"]["enable_measure"], 84 | "num_randomizations": "auto", 85 | "shots_per_randomization": "auto" 86 | } 87 | ) 88 | return estimator_options 89 | 90 | def get_sampler(self): 91 | if self.platform == "infty_shots": 92 | return Statevector 93 | 94 | elif self.platform == "finite_shots": 95 | return StatevectorSampler( 96 | default_shots=self.n_shots 97 | ) 98 | elif self.platform == "ibm_quantum": 99 | options = self.get_sampler_options() 100 | return IBMSampler(backend=self.backend, options=options) 101 | 102 | def get_estimator(self): 103 | if self.platform == "infty_shots": 104 | return StatevectorEstimator 105 | elif self.platform == "ibm_quantum": 106 | options = self.get_estimator_options() 107 | return IBMEstimator(backend=self.backend, options=options) 108 | 109 | def get_running_method(self, qc: QuantumCircuit): 110 | sampler = self.get_sampler() 111 | if self.platform == "infty_shots": 112 | res = Statevector.from_instruction(qc).data[0].real 113 | elif self.platform == "finite_shots": 114 | qc.measure_all() 115 | counts = ( 116 | sampler.run([qc]).result()[0].data.meas.get_int_counts() 117 | ) 118 | dist = QuasiDistribution( 119 | {meas: count / self.n_shots for meas, count in counts.items()}, shots=self.n_shots 120 | ) 121 | res = dist.get(0, 0.0) 122 | elif self.platform == "ibm_quantum": 123 | qc.measure_all() 124 | logical_circuit = qc 125 | pm = generate_preset_pass_manager(optimization_level=self.optimization_level, backend=self.backend, initial_layout=self.layout) 126 | physical_circuit = pm.run(logical_circuit) 127 | job = sampler.run([physical_circuit]) 128 | print(f"Job sent to hardware. Job ID: {job.job_id()}") 129 | res = job 130 | 131 | return res 132 | 133 | def get_job_results(self, job: RuntimeJobV2): 134 | counts = job.result()[0].data.meas.get_int_counts() 135 | dist = QuasiDistribution( 136 | {meas: count / self.n_shots for meas, count in counts.items()}, shots=self.n_shots 137 | ) 138 | res = dist.get(0, 0.0) 139 | return res 140 | 141 | def get_qiskit_ansatz(self): 142 | n_params = self.ansatz.n_features 143 | params = ParameterVector('p', n_params) 144 | qc = QuantumCircuit(self.ansatz.n_qubits) 145 | for operation in self.ansatz.operation_list: 146 | operator = SparsePauliOp(operation.generator) 147 | rotation = operation.bandwidth*params[operation.feature]/2 148 | evo = PauliEvolutionGate(operator, time=rotation) 149 | qc.append(evo, operation.wires) 150 | return qc 151 | 152 | def kappa(self, x1: np.ndarray, x2: np.ndarray) -> float: 153 | assert len(x1) == self.ansatz.n_features 154 | assert len(x2) == self.ansatz.n_features 155 | 156 | if self.type == KernelType.OBSERVABLE: 157 | return self.phi(x1) * self.phi(x2) 158 | 159 | elif self.type == KernelType.FIDELITY: 160 | qc = QuantumCircuit(self.ansatz.n_qubits, self.ansatz.n_qubits) 161 | qc.append(self.get_qiskit_ansatz().assign_parameters(x1.tolist()), range(self.ansatz.n_qubits)) 162 | qc.append(self.get_qiskit_ansatz().assign_parameters(x2.tolist()).inverse(), range(self.ansatz.n_qubits)) 163 | probabilities = self.get_running_method(qc) 164 | return probabilities 165 | 166 | elif self.type == KernelType.SWAP_TEST: 167 | qc = QuantumCircuit(1+2*self.ansatz.n_qubits, 1) 168 | qc.h(0) 169 | qc.append(self.get_qiskit_ansatz().assign_parameters(x1.tolist() + [1.0]), range(1, 1+self.ansatz.n_qubits)) 170 | qc.append(self.get_qiskit_ansatz().assign_parameters(x2.tolist() + [1.0]), range(self.ansatz.n_qubits)) 171 | for i in range(self.ansatz.n_qubits): 172 | qc.cswap(0, 1+i, 1+self.ansatz.n_qubits+i) 173 | qc.h(0) 174 | qc.measure(0, 0) 175 | job = self.get_sampler().run(qc) 176 | probabilities = job.result().quasi_dists[0] 177 | return probabilities.get(0, 0.0) 178 | 179 | def phi(self, x: np.ndarray) -> float: 180 | if self.type == KernelType.OBSERVABLE: 181 | 182 | assert len(x) == self.ansatz.n_features 183 | complete_features = x.tolist() + [1.0] 184 | circuit = self.get_qiskit_ansatz().bind_parameters(complete_features) 185 | observable = SparsePauliOp(self.measurement) 186 | job = self.get_estimator().run(circuit, observable) 187 | exp_val = job.result().values[0] 188 | return exp_val 189 | 190 | elif self.type in [KernelType.FIDELITY, KernelType.SWAP_TEST]: 191 | raise ValueError("phi not available for fidelity kernels") 192 | 193 | else: 194 | raise ValueError("Unknown type, possible erroneous loading from a numpy array") -------------------------------------------------------------------------------- /src/quask/evaluator/__init__.py: -------------------------------------------------------------------------------- 1 | from .kernel_evaluator import KernelEvaluator 2 | from .lie_rank_evaluator import LieRankEvaluator 3 | from .haar_evaluator import HaarEvaluator 4 | from .covering_number_evaluator import CoveringNumberEvaluator 5 | from .kernel_alignment_evaluator import KernelAlignmentEvaluator 6 | from .centered_kernel_alignment_evaluator import CenteredKernelAlignmentEvaluator 7 | from .spectral_bias_evaluator import SpectralBiasEvaluator 8 | from .ridge_generalization_evaluator import RidgeGeneralizationEvaluator 9 | from .geometric_difference_evaluator import GeometricDifferenceEvaluator 10 | from .ess_model_complexity_evaluator import EssModelComplexityEvaluator 11 | -------------------------------------------------------------------------------- /src/quask/evaluator/centered_kernel_alignment_evaluator.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from ..core import Kernel 3 | from . import KernelEvaluator, KernelAlignmentEvaluator 4 | 5 | 6 | class CenteredKernelAlignmentEvaluator(KernelEvaluator): 7 | """ 8 | Kernel compatibility measure based on the centered kernel-target alignment 9 | See: Cortes, Corinna, Mehryar Mohri, and Afshin Rostamizadeh. "Algorithms for learning kernels based on centered alignment." 10 | The Journal of Machine Learning Research 13.1 (2012): 795-828. 11 | """ 12 | 13 | def evaluate(self, kernel: Kernel, K: np.ndarray, X: np.ndarray, y: np.ndarray): 14 | """ 15 | Evaluate the current kernel and return the corresponding cost. Lower cost values corresponds to better solutions 16 | :param kernel: kernel object 17 | :param K: optional kernel matrix \kappa(X, X) 18 | :param X: datapoints 19 | :param y: labels 20 | :return: cost of the kernel, the lower the better 21 | """ 22 | if K is None: 23 | K = kernel.build_kernel(X, X) 24 | Kc = CenteredKernelAlignmentEvaluator.center_kernel(K) 25 | kta = KernelAlignmentEvaluator.kta(Kc, y) 26 | return - np.abs(kta) 27 | 28 | @staticmethod 29 | def center_kernel(K): 30 | """ 31 | Center a kernel (subtract its mean value) 32 | :param K: kernel matrix 33 | :return: centered kernel 34 | """ 35 | m = K.shape[0] 36 | U = np.eye(m) - (1 / m) * np.outer([1] * m, [1] * m) 37 | Kc = U @ K @ U.T 38 | return Kc 39 | -------------------------------------------------------------------------------- /src/quask/evaluator/covering_number_evaluator.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from ..core import Kernel 3 | from . import KernelEvaluator 4 | 5 | 6 | class CoveringNumberEvaluator(KernelEvaluator): 7 | """ 8 | Expressibility measure based on the covering number associated with the hypothesis class related to the current ansatz. 9 | See: Du, Yuxuan, et al. "Efficient measure for the expressivity of variational quantum algorithms." Physical Review Letters 10 | 128.8 (2022): 080506. 11 | """ 12 | 13 | def evaluate(self, kernel: Kernel, K: np.ndarray, X: np.ndarray, y: np.ndarray): 14 | """ 15 | Evaluate the current kernel and return the corresponding cost. Lower cost values corresponds to better solutions 16 | :param kernel: kernel object 17 | :param K: optional kernel matrix \kappa(X, X) 18 | :param X: datapoints 19 | :param y: labels 20 | :return: cost of the kernel, the lower the better 21 | """ 22 | operations = kernel.ansatz.operation_list 23 | trainable_operations = [op for op in operations if op.feature >= 0] 24 | return 2 ** trainable_operations 25 | -------------------------------------------------------------------------------- /src/quask/evaluator/ess_model_complexity_evaluator.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy.linalg import sqrtm 3 | from ..core import Kernel 4 | from . import KernelEvaluator 5 | 6 | 7 | class EssModelComplexityEvaluator(KernelEvaluator): 8 | """ 9 | Calculate the model complexity s(K). 10 | See Equation F1 in "The power of data in quantum machine learning" (https://arxiv.org/abs/2011.01938) 11 | """ 12 | 13 | def evaluate(self, kernel: Kernel, K: np.ndarray, X: np.ndarray, y: np.ndarray): 14 | """ 15 | Evaluate the current kernel and return the corresponding cost. Lower cost values corresponds to better solutions 16 | 17 | :param kernel: kernel object 18 | :param K: optional kernel matrix \kappa(X, X) 19 | :param X: datapoints 20 | :param y: labels 21 | :return: cost of the kernel, the lower the better 22 | """ 23 | if K is None: 24 | K = kernel.build_kernel(X, X) 25 | 26 | return EssModelComplexityEvaluator.calculate_model_complexity(K, y) 27 | 28 | @staticmethod 29 | def calculate_model_complexity(k, y, normalization_lambda=0.001): 30 | """ 31 | Calculate the model complexity s(K), which is equation F1 in 32 | "The power of data in quantum machine learning" (https://arxiv.org/abs/2011.01938). 33 | 34 | :param k: Kernel gram matrix 35 | :param y: Labels 36 | :param normalization_lambda: Normalization factor 37 | :return: model complexity of the given kernel 38 | """ 39 | n = k.shape[0] 40 | k_inv = np.linalg.inv(k + normalization_lambda * np.eye(n)) 41 | k_body = k_inv @ k @ k_inv 42 | model_complexity = y.T @ k_body @ y 43 | return model_complexity 44 | 45 | @staticmethod 46 | def calculate_model_complexity_training(k, y, normalization_lambda=0.001): 47 | """ 48 | Subprocedure of the function 'calculate_model_complexity_generalized'. 49 | 50 | :param k: Kernel gram matrix 51 | :param y: Labels 52 | :param normalization_lambda: Normalization factor 53 | :return: model complexity of the given kernel 54 | """ 55 | n = k.shape[0] 56 | k_inv = np.linalg.inv(k + normalization_lambda * np.eye(n)) 57 | k_mid = k_inv @ k_inv # without k in the middle 58 | model_complexity = (normalization_lambda**2) * (y.T @ k_mid @ y) 59 | return model_complexity 60 | 61 | @staticmethod 62 | def calculate_model_complexity_generalized(k, y, normalization_lambda=0.001): 63 | """ 64 | Calculate the model complexity s(K), which is equation M1 in 65 | "The power of data in quantum machine learning" (https://arxiv.org/abs/2011.01938). 66 | 67 | :param k: Kernel gram matrix 68 | :param y: Labels 69 | :param normalization_lambda: Normalization factor 70 | :return: model complexity of the given kernel 71 | """ 72 | n = k.shape[0] 73 | a = np.sqrt(EssModelComplexityEvaluator.calculate_model_complexity_training(k, y, normalization_lambda) / n) 74 | b = np.sqrt(EssModelComplexityEvaluator.calculate_model_complexity(k, y, normalization_lambda) / n) 75 | return a + b 76 | -------------------------------------------------------------------------------- /src/quask/evaluator/geometric_difference_evaluator.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy.linalg import sqrtm 3 | from ..core import Kernel 4 | from . import KernelEvaluator 5 | 6 | 7 | class GeometricDifferenceEvaluator(KernelEvaluator): 8 | """ 9 | Calculate the geometric difference g(K_1 || K_2), and characterize 10 | the separation between classical and quantum kernels. 11 | See Equation F9 in "The power of data in quantum machine learning" (https://arxiv.org/abs/2011.01938) 12 | """ 13 | 14 | def __init__(self, list_classical_kernel_matrices, lam): 15 | """ 16 | Initialization. 17 | 18 | :param list_classical_kernel_matrices: List of kernel matrices obtained with classical kernels 19 | :param lam: normalization constant lambda 20 | """ 21 | super().__init__() 22 | self.list_classical_kernel_matrices = list_classical_kernel_matrices 23 | self.lam = lam 24 | 25 | 26 | def evaluate(self, kernel: Kernel, K: np.ndarray, X: np.ndarray, y: np.ndarray): 27 | """ 28 | Evaluate the current kernel and return the corresponding cost. Lower cost values corresponds to better solutions 29 | 30 | :param kernel: kernel object 31 | :param K: optional kernel matrix \kappa(X, X) 32 | :param X: datapoints 33 | :param y: labels 34 | :return: cost of the kernel, the lower the better 35 | """ 36 | if K is None: 37 | K = kernel.build_kernel(X, X) 38 | 39 | geometric_differences = [GeometricDifferenceEvaluator.g(K, Kc, self.lam) 40 | for Kc in self.list_classical_kernel_matrices] 41 | 42 | # return -1 * np.min(geometric_differences) 43 | return geometric_differences 44 | 45 | @staticmethod 46 | def g(k_1, k_2, lam): 47 | """ 48 | Method to calculate the geometric difference 49 | 50 | :param k_1: first matrix (quantum usually) 51 | :param k_2: second matrix (classical usually) 52 | :param lam: normalization lambda 53 | :return: value of g(K_1, K_2) 54 | """ 55 | n = k_2.shape[0] 56 | assert k_2.shape == (n, n) 57 | assert k_1.shape == (n, n) 58 | # √K1 59 | k_1_sqrt = np.real(sqrtm(k_1)) 60 | # √K2 61 | k_2_sqrt = np.real(sqrtm(k_2)) 62 | # √(K2 + lambda I)^-2 63 | kc_inv = np.linalg.inv(k_2 + lam * np.eye(n)) 64 | kc_inv = kc_inv @ kc_inv 65 | # Equation F9 66 | f9_body = k_1_sqrt.dot(k_2_sqrt.dot(kc_inv.dot(k_2_sqrt.dot(k_1_sqrt)))) 67 | f9 = np.sqrt(np.linalg.norm(f9_body, np.inf)) 68 | return f9 69 | -------------------------------------------------------------------------------- /src/quask/evaluator/haar_evaluator.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from ..core import Kernel 3 | from . import KernelEvaluator 4 | 5 | 6 | class HaarEvaluator(KernelEvaluator): 7 | """ 8 | Expressibility measure based on the comparison between the distribution of states obtained with an Haar random circuit and 9 | the one obtained with the current ansatz. 10 | See: Sim, Sukin, Peter D. Johnson, and Alán Aspuru-Guzik. "Expressibility and entangling capability of parameterized quantum 11 | circuits for hybrid quantum-classical algorithms." Advanced Quantum Technologies 2.12 (2019): 1900070. 12 | """ 13 | 14 | def __init__(self, n_bins: int, n_samples: int): 15 | """ 16 | Initialization 17 | :param n_bins: number of discretization buckets 18 | :param n_samples: number of samples approximating the distribution of values 19 | """ 20 | self.n_bins = n_bins 21 | self.n_samples = n_samples 22 | 23 | def evaluate(self, kernel: Kernel, K: np.ndarray, X: np.ndarray, y: np.ndarray): 24 | """ 25 | Evaluate the current kernel and return the corresponding cost. Lower cost values corresponds to better solutions 26 | :param kernel: kernel object 27 | :param K: optional kernel matrix \kappa(X, X) 28 | :param X: datapoints 29 | :param y: labels 30 | :return: cost of the kernel, the lower the better 31 | """ 32 | haar_histogram = HaarEvaluator.haar_histogram(kernel, self.n_bins) 33 | ansatz_histogram = HaarEvaluator.ansatz_histogram(kernel, self.n_bins, self.n_samples, K) 34 | self.last_result = (haar_histogram, ansatz_histogram) 35 | return np.linalg.norm(haar_histogram - ansatz_histogram) 36 | 37 | @staticmethod 38 | def ansatz_histogram(kernel, n_bins, n_samples, K: None): 39 | """ 40 | Create a histogram of the fidelities of the ansatz 41 | :param kernel: kernel object 42 | :param n_bins: number of discretization buckets 43 | :param n_samples: number of samples approximating the distribution of values 44 | :return: histogram of the given ansatz 45 | """ 46 | histogram = [0] * n_bins 47 | 48 | if type(K) != type(None): 49 | for i in range(K.shape[0]): 50 | for j in range(K.shape[1]): 51 | index = int(K[i,j] * n_bins) 52 | histogram[np.minimum(index, n_bins - 1)] += 1 53 | else: 54 | for _ in range(n_samples): 55 | theta_1 = np.random.normal(size=(kernel.ansatz.n_features,)) * np.pi 56 | theta_2 = np.random.normal(size=(kernel.ansatz.n_features,)) * np.pi 57 | fidelity = kernel.kappa(theta_1, theta_2) 58 | index = int(fidelity * n_bins) 59 | histogram[np.minimum(index, n_bins - 1)] += 1 60 | 61 | return np.array(histogram) / n_samples 62 | 63 | @staticmethod 64 | def haar_histogram(kernel, n_bins): 65 | """ 66 | Create a histogram of the Haar random fidelities 67 | :param n_bins: number of bins 68 | :return: histogram 69 | """ 70 | N = 2 ** kernel.ansatz.n_qubits 71 | 72 | def prob(low, high): 73 | return (1-low) ** (N - 1) - (1 - high) ** (N - 1) 74 | 75 | histogram = np.array([prob(i / n_bins, (i+1) / n_bins) for i in range(n_bins)]) 76 | return histogram 77 | 78 | def __str__(self): 79 | return "A = " + self.last_result[0] + " - " + self.last_result[1] 80 | -------------------------------------------------------------------------------- /src/quask/evaluator/kernel_alignment_evaluator.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from ..core import Kernel 3 | from . import KernelEvaluator 4 | 5 | 6 | class KernelAlignmentEvaluator(KernelEvaluator): 7 | """ 8 | Kernel compatibility measure based on the kernel-target alignment 9 | See: Cristianini, Nello, et al. "On kernel-target alignment." Advances in neural information processing systems 14 (2001). 10 | """ 11 | 12 | def evaluate(self, kernel: Kernel, K: np.ndarray, X: np.ndarray, y: np.ndarray): 13 | """ 14 | Evaluate the current kernel and return the corresponding cost. Lower cost values corresponds to better solutions 15 | :param kernel: kernel object 16 | :param K: optional kernel matrix \kappa(X, X) 17 | :param X: datapoints 18 | :param y: labels 19 | :return: cost of the kernel, the lower the better 20 | """ 21 | if K is None: 22 | K = kernel.build_kernel(X, X) 23 | the_cost = -1 * np.abs(KernelAlignmentEvaluator.kta(K, y)) 24 | # assert not np.isnan(the_cost), f"{kernel=} {K=} {y=}" 25 | return the_cost if not np.isnan(the_cost) else 1000 26 | 27 | @staticmethod 28 | def kta(K, y): 29 | """ 30 | Calculates the kernel target alignment 31 | :param K: kernel matrix 32 | :param y: label vector 33 | :return: kernel target alignment 34 | """ 35 | Y = np.outer(y, y) 36 | return np.sum(K * Y) / (np.linalg.norm(K) * np.linalg.norm(Y)) 37 | -------------------------------------------------------------------------------- /src/quask/evaluator/kernel_evaluator.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from abc import ABC, abstractmethod 3 | from ..core import Kernel 4 | 5 | 6 | class KernelEvaluator(ABC): 7 | 8 | def __init__(self): 9 | self.last_result = None 10 | 11 | @abstractmethod 12 | def evaluate(self, kernel: Kernel, K: np.ndarray, X: np.ndarray, y: np.ndarray): 13 | """ 14 | Evaluate the current kernel and return the corresponding cost. Lower cost values corresponds to better solutions 15 | :param kernel: kernel object 16 | :param K: optional kernel matrix \kappa(X, X) 17 | :param X: datapoints 18 | :param y: labels 19 | :return: cost of the kernel, the lower the better 20 | """ 21 | pass 22 | -------------------------------------------------------------------------------- /src/quask/evaluator/lie_rank_evaluator.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import numpy as np 3 | from typing import Set 4 | from ..core import Kernel 5 | from . import KernelEvaluator 6 | 7 | 8 | class LieRankEvaluator(KernelEvaluator): 9 | """ 10 | Expressibility and 'Efficient classical simulability' measure based on the rank of the Lie algebra obtained by spanning 11 | the generators of the circuits. 12 | See: Larocca, Martin, et al. "Diagnosing barren plateaus with tools from quantum optimal control." Quantum 6 (2022): 824. 13 | """ 14 | 15 | def __init__(self, T): 16 | """ 17 | Initializer 18 | :param T: threshold T > 0 telling how is the minimum dimension of a 'hard-to-simulate' Lie algebra 19 | """ 20 | super().__init__() 21 | self.T = T 22 | 23 | def evaluate(self, kernel: Kernel, K: np.ndarray, X: np.ndarray, y: np.ndarray): 24 | """ 25 | Evaluate the current kernel and return the corresponding cost. Lower cost values corresponds to better solutions 26 | :param kernel: kernel object 27 | :param K: optional kernel matrix \kappa(X, X) 28 | :param X: datapoints 29 | :param y: labels 30 | :return: cost of the kernel, the lower the better 31 | """ 32 | self.last_result = self.braket_generators(kernel, self.T) 33 | return -len(self.last_result) 34 | 35 | def braket_pair(self, a: str, b: str): 36 | """ 37 | Calculate the commutator between two pauli matrices 38 | :param a: first Pauli (one of the strings 'I', 'X', 'Y', 'Z') 39 | :param b: second Pauli (one of the strings 'I', 'X', 'Y', 'Z') 40 | :return: [a, b] 41 | """ 42 | assert a in ['I', 'X', 'Y', 'Z'] and b in ['I', 'X', 'Y', 'Z'] 43 | if a == b: return 'I' 44 | if a == 'I': return b 45 | if b == 'I': return a 46 | return list(set(['X', 'Y', 'Z']).difference([a, b]))[0] 47 | 48 | def braket_strings(self, s1: str, s2: str): 49 | """ 50 | Calculate the communtator between two pauli strings 51 | :param s1: first Pauli string 52 | :param s2: second Pauli string 53 | :return: [s1, s2] 54 | """ 55 | assert len(s1) == len(s2), "Tha Pauli strings have different lengths" 56 | return [self.braket_pair(a, b) for (a, b) in zip(s1, s2)] 57 | 58 | def __braket_generators(self, initial_generators: Set[str], new_generators: Set[str]): 59 | """ 60 | Return the set of generators obtained by commutating pairwise the elements in the given set 61 | :param initial_generators: first set of generators 62 | :param new_generators: second set of generators 63 | :return: generators obtained with the pairwise commutation of the given elements (only new ones) 64 | """ 65 | out_generators = [] 66 | for gen_new in new_generators: 67 | for gen_old in initial_generators: 68 | braket = "".join(self.braket_strings(gen_new, gen_old)) 69 | if braket not in initial_generators and braket not in new_generators: 70 | out_generators.append(braket) 71 | return set(out_generators) 72 | 73 | def get_initial_generators(self, kernel): 74 | """ 75 | Create the initial generators of a kernel, i.e. for each operation apply the generator to the correct wires 76 | and identity everywhere else 77 | :param kernel: kernel object 78 | :return set of initial generators corresponding to the operations of the kernel 79 | """ 80 | # get the generators of each operation 81 | generators = [kernel.ansatz.operation_list[i].generator for i in range(kernel.ansatz.n_operations)] 82 | # get the wires on which each operation acts 83 | wires = [kernel.ansatz.operation_list[i].wires for i in range(kernel.ansatz.n_operations)] 84 | initial_generators = [] 85 | for i in range(kernel.ansatz.n_operations): 86 | # initialize each generator with identity everyone, as list of char and not as string (the latter is immutable) 87 | initial_generator = ['I'] * kernel.ansatz.n_qubits 88 | # assign the generator to each qubit 89 | q0, q1 = wires[i][0], wires[i][1] 90 | g0, g1 = generators[i][0], generators[i][1] 91 | initial_generator[q0] = g0 92 | initial_generator[q1] = g1 93 | # convert the list of char to string, now 94 | initial_generator = "".join(initial_generator) 95 | # print(f"{i=} {q0=} {q1=} {g0=} {g1=} {initial_generator=}") 96 | initial_generators.append(initial_generator) 97 | # print(f"{initial_generators}") 98 | return set(initial_generators) 99 | 100 | def braket_generators(self, kernel, T): 101 | """ 102 | Return the basis of the lie algebra of the circuit defined by the kernel. The number of elements is truncated at T 103 | :param kernel: kernel object 104 | :param T: threshold 105 | :return: basis of the lie algebra of the generators in kernel 106 | """ 107 | initial_generators = self.get_initial_generators(kernel) 108 | new_generators = copy.deepcopy(initial_generators) 109 | all_generators = copy.deepcopy(initial_generators) 110 | while len(all_generators) < T and len(new_generators) > 0: 111 | new_generators = self.__braket_generators(all_generators, new_generators) 112 | all_generators = all_generators.union(new_generators) 113 | return all_generators 114 | 115 | def __str__(self): 116 | return str(self.last_result) 117 | -------------------------------------------------------------------------------- /src/quask/evaluator/ridge_generalization_evaluator.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from sklearn.kernel_ridge import KernelRidge 3 | from ..core import Kernel 4 | from . import KernelEvaluator 5 | 6 | 7 | class RidgeGeneralizationEvaluator(KernelEvaluator): 8 | """ 9 | Evaluates the generalization error of the given kernel 10 | """ 11 | 12 | def __init__(self): 13 | """ 14 | Initialization 15 | """ 16 | pass 17 | 18 | def evaluate(self, kernel: Kernel, K: np.ndarray, X: np.ndarray, y: np.ndarray): 19 | """ 20 | Evaluate the current kernel and return the corresponding cost. Lower cost values corresponds to better solutions 21 | :param kernel: kernel object 22 | :param K: optional kernel matrix \kappa(X, X) 23 | :param X: datapoints 24 | :param y: labels 25 | :return: cost of the kernel, the lower the better 26 | """ 27 | n_train = len(y) // 2 28 | n_test = len(y) - n_train 29 | krr = KernelRidge(kernel=lambda X1, X2: kernel.build_kernel(X1, X2)) 30 | krr.fit(X[:n_train], y[:n_train]) 31 | y_pred = np.array(krr.predict(X[n_train:])) 32 | mse = np.linalg.norm(y_pred - y[n_train:]) / n_test 33 | return mse 34 | 35 | -------------------------------------------------------------------------------- /src/quask/evaluator/spectral_bias_evaluator.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy.linalg import eigh 3 | from ..core import Kernel 4 | from . import KernelEvaluator 5 | 6 | 7 | class SpectralBiasEvaluator(KernelEvaluator): 8 | """ 9 | Kernel compatibility measure based on the spectral bias framework. 10 | See: Canatar, Abdulkadir, Blake Bordelon, and Cengiz Pehlevan. "Spectral bias and task-model alignment explain generalization 11 | in kernel regression and infinitely wide neural networks." Nature communications 12.1 (2021): 2914. 12 | """ 13 | 14 | def __init__(self, n_eigenvalues_cut): 15 | """ 16 | Initialization 17 | :param n_eigenvalues_cut: number of eigenvalues contributing to the cumulative power 18 | """ 19 | self.n_eigenvalues_cut = n_eigenvalues_cut 20 | 21 | def evaluate(self, kernel: Kernel, K: np.ndarray, X: np.ndarray, y: np.ndarray): 22 | """ 23 | Evaluate the current kernel and return the corresponding cost. Lower cost values corresponds to better solutions 24 | :param kernel: kernel object 25 | :param K: optional kernel matrix \kappa(X, X) 26 | :param X: datapoints 27 | :param y: labels 28 | :return: cost of the kernel, the lower the better 29 | """ 30 | if K is None: 31 | K = kernel.build_kernel(X, X) 32 | Lambda, Phi = SpectralBiasEvaluator.decompose_kernel(K) 33 | w, a = SpectralBiasEvaluator.calculate_weights(Lambda, Phi, y) 34 | C, powers = SpectralBiasEvaluator.cumulative_power_distribution(w, Lambda, self.n_eigenvalues_cut) 35 | self.last_result = (Lambda, Phi, w, a, C, powers) 36 | return C 37 | 38 | 39 | @staticmethod 40 | def decompose_kernel(K, eigenvalue_descending_order=True, eigenvalue_removal_threshold=1e-12): 41 | """ 42 | Decompose the kernel matrix K in its eigenvalues Λ and eigenvectors Φ 43 | :param K: kernel matrix, real and symmetric 44 | :param eigenvalue_descending_order: if True, the biggest eigenvalue is the first one 45 | :return: Lambda vector (n elements) and Phi matrix (N*N matrix) 46 | """ 47 | Lambda, Phi = eigh(K) 48 | 49 | # set the desired order for the eigenvalues 50 | if eigenvalue_descending_order: 51 | Lambda = Lambda[::-1] 52 | Phi = Phi[:, ::-1] 53 | 54 | # kernel matrix is positive definite, any (small) negative eigenvalue is effectively a numerical error 55 | Lambda[Lambda < 0] = 0 56 | 57 | # remove the smallest positive eigenvalues, as they are useless 58 | Lambda[Lambda < eigenvalue_removal_threshold] = 0 59 | 60 | return Lambda, Phi 61 | 62 | @staticmethod 63 | def calculate_weights(Lambda, Phi, labels): 64 | """ 65 | Calculates the weights of a predictor given the labels and the kernel eigendecomposition, 66 | as shown in (Canatar et al 2021, inline formula below equation 18). 67 | :param Lambda: vectors of m nonnegative eigenvalues 'eta' 68 | :param Phi: vectors of m nonnegative eigenvectors 'phi' 69 | :param labels: vector of m labels corresponding to 'm' ground truth labels or predictor outputs 70 | :return: vector w of RKHS weights, vector a of out-of-RKHS weights 71 | """ 72 | # get the number of training elements 73 | m = Lambda.shape[0] 74 | 75 | # invert nonzero eigenvalues 76 | inv_eigenvalues = np.reciprocal(Lambda, where=Lambda > 0) 77 | 78 | # weight vectors are calculated by inverting formula: y = \sum_k=1^M w_k \sqrt{lambda_k} \phi_k(x) 79 | the_w = (1 / m) * np.diag(inv_eigenvalues ** 0.5) @ Phi.T @ labels 80 | the_w[Lambda == 0] = 0 81 | 82 | # weight vector for the components out-of-RKHS 83 | the_a = (1 / m) * Phi.T @ labels 84 | the_a[Lambda > 0] = 0 85 | return the_w, the_a 86 | 87 | @staticmethod 88 | def cumulative_power_distribution(w, Lambda, n_eigenvalues): 89 | """ 90 | 91 | :param w: vector of weights 92 | :param Lambda: vector of eigenvalues 93 | :param n_eigenvalues: number of eigenvalues contributing to the cumulative power 94 | :return: 95 | """ 96 | powers = np.diag(Lambda) @ (w ** 2) 97 | return np.sum(powers[:n_eigenvalues]) / np.sum(powers), powers 98 | 99 | def __str__(self): 100 | (Lambda, Phi, w, a, C, powers) = self.last_result 101 | return f"""{Lambda=} {Phi=} {w=} {a=} {C=} {powers=}""" 102 | 103 | -------------------------------------------------------------------------------- /src/quask/optimizer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CERN-IT-INNOVATION/QuASK/3ac5bef5aff678e2239af4207ed9e89b8a0668b9/src/quask/optimizer/__init__.py -------------------------------------------------------------------------------- /src/quask/optimizer/base_kernel_optimizer.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | import numpy as np 3 | import copy 4 | from ..core import Operation, Ansatz, Kernel, KernelFactory 5 | from ..evaluator import KernelEvaluator 6 | 7 | 8 | class BaseKernelOptimizer(ABC): 9 | """ 10 | Abstract class implementing a procedure to optimize the kernel 11 | """ 12 | 13 | def __init__(self, initial_kernel: Kernel, X: np.ndarray, y: np.ndarray, ke: KernelEvaluator): 14 | """ 15 | Initialization 16 | :param initial_kernel: initial kernel object 17 | :param X: datapoints 18 | :param y: labels 19 | :param ke: kernel evaluator object 20 | """ 21 | self.initial_kernel = initial_kernel 22 | self.X = X 23 | self.y = y 24 | self.ke = ke 25 | 26 | @abstractmethod 27 | def optimize(self): 28 | """ 29 | Run the optimization 30 | :return: optimized kernel object 31 | """ 32 | pass 33 | -------------------------------------------------------------------------------- /src/quask/optimizer/bayesian_optimizer.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from joblib import Parallel, delayed 3 | from skopt import Optimizer 4 | from skopt.space import Real, Categorical 5 | 6 | from ..core import Operation, Ansatz, Kernel, KernelFactory 7 | from ..evaluator import KernelEvaluator 8 | from .base_kernel_optimizer import BaseKernelOptimizer 9 | 10 | 11 | class BayesianOptimizer(BaseKernelOptimizer): 12 | 13 | def __init__(self, initial_kernel: Kernel, X: np.ndarray, y: np.ndarray, ke: KernelEvaluator): 14 | super().__init__(initial_kernel, X, y, ke) 15 | self.optimizer = None 16 | 17 | def get_sklearn_dimensions(self): 18 | n_features = self.initial_kernel.ansatz.n_features 19 | n_operations = self.initial_kernel.ansatz.n_operations 20 | n_qubits = self.initial_kernel.ansatz.n_qubits 21 | allowed_generators = self.initial_kernel.get_allowed_operations() 22 | ansatz_dimension = [ 23 | # generator 24 | Categorical(list(range(len(allowed_generators)))), 25 | # wires 26 | Categorical(list(range(n_qubits))), 27 | Categorical(list(range(n_qubits - 1))), 28 | # features 29 | Categorical(list(range(n_features))), 30 | # bandwidth 31 | Real(0.0, 1.0), 32 | ] * n_operations 33 | measurement_dimensions = [Categorical([0, 1, 2, 3])] * n_qubits 34 | return ansatz_dimension + measurement_dimensions 35 | 36 | def get_kernel(self, the_array): 37 | the_array = np.array(the_array, dtype=object) 38 | the_kernel = Kernel.from_numpy(np.concatenate([the_array.ravel(), np.array([self.initial_kernel.type])]), 39 | self.initial_kernel.ansatz.n_features, 40 | self.initial_kernel.ansatz.n_qubits, 41 | self.initial_kernel.ansatz.n_operations, 42 | self.initial_kernel.ansatz.allow_midcircuit_measurement, 43 | shift_second_wire=True) 44 | return the_kernel 45 | 46 | def get_cost(self, the_array): 47 | the_kernel = self.get_kernel(the_array) 48 | the_cost = self.ke.evaluate(the_kernel, None, self.X, self.y) 49 | return the_cost 50 | 51 | def optimize(self, n_epochs=20, n_points=4, n_jobs=4): 52 | 53 | self.optimizer = Optimizer( 54 | dimensions=self.get_sklearn_dimensions(), 55 | random_state=1, 56 | base_estimator='gp', 57 | acq_func="PI", 58 | acq_optimizer="sampling", 59 | acq_func_kwargs={"xi": 10000.0, "kappa": 10000.0} 60 | ) 61 | 62 | for i in range(n_epochs): 63 | x = self.optimizer.ask(n_points=n_points) # x is a list of n_points points 64 | y = Parallel(n_jobs=n_jobs)(delayed(lambda array: self.get_cost(array))(v) for v in x) # evaluate points in parallel 65 | self.optimizer.tell(x, y) 66 | print(f"Epoch of training {i=}") 67 | 68 | min_index = np.argmin(self.optimizer.yi) 69 | # min_cost = self.optimizer.yi[min_index] 70 | min_solution = self.optimizer.Xi[min_index] 71 | 72 | best_kernel = self.get_kernel(min_solution) 73 | return best_kernel 74 | -------------------------------------------------------------------------------- /src/quask/optimizer/greedy_optimizer.py: -------------------------------------------------------------------------------- 1 | import copy 2 | 3 | import numpy as np 4 | from mushroom_rl.core import Environment 5 | 6 | from ..core import Kernel 7 | from ..evaluator import KernelEvaluator 8 | from .base_kernel_optimizer import BaseKernelOptimizer 9 | from .wide_kernel_environment import WideKernelEnvironment 10 | 11 | 12 | class GreedyOptimizer(BaseKernelOptimizer): 13 | 14 | def __init__(self, initial_kernel: Kernel, X: np.ndarray, y: np.ndarray, ke: KernelEvaluator): 15 | super().__init__(initial_kernel, X, y, ke) 16 | self.mdp: WideKernelEnvironment = Environment.make('WideKernelEnvironment', initial_kernel=self.initial_kernel, X=X, y=y, ke=ke) 17 | self.rewards_history = [] 18 | self.actions_history = [] 19 | 20 | def optimize(self, verbose=False): 21 | 22 | self.mdp.reset() 23 | state = copy.deepcopy(self.mdp._state) 24 | 25 | terminated = False 26 | n_actions = self.mdp._mdp_info.action_space.size[0] 27 | rewards = np.zeros(shape=(n_actions,)) 28 | 29 | while not terminated: 30 | # list all actions at the first depth 31 | for action in range(n_actions): 32 | self.mdp.reset(state) 33 | new_state, reward, absorbed, _ = self.mdp.step((action,)) 34 | rewards[action] = reward 35 | _, kernel = self.mdp.deserialize_state(new_state) 36 | if absorbed: 37 | terminated = True 38 | print(f"{action=:4d} {reward=:0.6f} {kernel=}") 39 | # apply chosen action 40 | chosen_action = np.argmax(rewards) 41 | self.mdp.reset(state) 42 | state, _, _, _ = self.mdp.step((chosen_action,)) 43 | if verbose: 44 | print(f"Chosen action: {chosen_action}") 45 | print(f"{self.mdp.deserialize_state(state)=}") 46 | # additional information 47 | self.rewards_history.append(rewards) 48 | self.actions_history.append(chosen_action) 49 | 50 | _, kernel = self.mdp.deserialize_state(state) 51 | return kernel 52 | -------------------------------------------------------------------------------- /src/quask/optimizer/metaheuristic_optimizer.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import numpy as np 3 | from enum import Enum 4 | 5 | from opytimizer import Opytimizer 6 | from opytimizer.core import Function 7 | from opytimizer.optimizers.swarm import PSO 8 | from opytimizer.spaces import GridSpace 9 | from opytimizer.utils.callback import Callback 10 | from ..core import Kernel 11 | from ..evaluator import KernelEvaluator 12 | from .base_kernel_optimizer import BaseKernelOptimizer 13 | 14 | 15 | class CustomCallback(Callback): 16 | """A CustomCallback can be created by override its parent `Callback` class 17 | and by implementing the desired logic in its available methods. 18 | """ 19 | 20 | def __init__(self): 21 | """Initialization method for the customized callback.""" 22 | 23 | # You only need to override its parent class 24 | super(CustomCallback).__init__() 25 | 26 | def on_task_begin(self, opt_model): 27 | """Called at the beginning of an task.""" 28 | print("Task begin") 29 | 30 | def on_task_end(self, opt_model): 31 | """Called at the end of an task.""" 32 | print("Task end") 33 | 34 | def on_iteration_begin(self, iteration, opt_model): 35 | """Called at the beginning of an iteration.""" 36 | print(f"Iteration {iteration} begin") 37 | 38 | def on_iteration_end(self, iteration, opt_model): 39 | """Called at the end of an iteration.""" 40 | print(f"Iteration {iteration} end") 41 | 42 | def on_evaluate_before(self, *evaluate_args): 43 | """Called before the `evaluate` method.""" 44 | print(f"Evaluate before {evaluate_args}") 45 | 46 | def on_evaluate_after(self, *evaluate_args): 47 | """Called after the `evaluate` method.""" 48 | print(f"Evaluate after {evaluate_args}") 49 | 50 | def on_update_before(self, *update_args): 51 | """Called before the `update` method.""" 52 | print(f"Update before {update_args}") 53 | 54 | def on_update_after(self, *update_args): 55 | """Called after the `update` method.""" 56 | print(f"Update after {update_args}") 57 | 58 | 59 | class MetaheuristicType(Enum): 60 | 61 | # evolutionary 62 | FOREST_OPTIMIZATION = 1 63 | GENETIC_ALGORITHM = 2 64 | # population 65 | EMPEROR_PENGUIN_OPTIMIZER = 3 66 | # swarm 67 | PARTICLE_SWARM_OPTIMIZATION = 0 68 | 69 | 70 | class MetaheuristicOptimizer(BaseKernelOptimizer): 71 | 72 | def __init__(self, initial_kernel: Kernel, X: np.ndarray, y: np.ndarray, ke: KernelEvaluator): 73 | super().__init__(initial_kernel, X, y, ke) 74 | 75 | def cost(array): 76 | array = array.ravel() 77 | the_array = np.concatenate([array, np.array([initial_kernel.type])]) 78 | # create kernel 79 | the_kernel = Kernel.from_numpy(the_array, 80 | initial_kernel.ansatz.n_features, 81 | initial_kernel.ansatz.n_qubits, 82 | initial_kernel.ansatz.n_operations, 83 | initial_kernel.ansatz.allow_midcircuit_measurement, 84 | shift_second_wire=True) 85 | the_cost = ke.evaluate(the_kernel, None, X, y) 86 | print(f"MetaheuristicOptimizer.cost -> {the_cost: 5.5f} -> {array}") 87 | return the_cost 88 | 89 | self.space = self.get_opytimize_space() 90 | self.optimizer = PSO() 91 | self.cost = cost 92 | self.function = Function(cost) 93 | self.opt = Opytimizer(self.space, self.optimizer, self.function, save_agents=True) 94 | self.history = None 95 | self.best_solution = None 96 | self.best_cost = None 97 | 98 | def optimize(self, n_iterations=1000, verbose=False): 99 | self.opt.start(n_iterations=n_iterations, callbacks=[CustomCallback()] if verbose else []) 100 | self.history = self.opt.history 101 | data_at_convergence = self.history.get_convergence("best_agent") 102 | self.best_solution = data_at_convergence[0].ravel() 103 | self.best_cost = data_at_convergence[1].ravel() 104 | the_array = np.concatenate([self.best_solution, np.array([self.initial_kernel.type])]) 105 | return Kernel.from_numpy(the_array, 106 | self.initial_kernel.ansatz.n_features, 107 | self.initial_kernel.ansatz.n_qubits, 108 | self.initial_kernel.ansatz.n_operations, 109 | self.initial_kernel.ansatz.allow_midcircuit_measurement, 110 | shift_second_wire=True) 111 | 112 | def get_opytimize_space(self): 113 | n_features = self.initial_kernel.ansatz.n_features 114 | n_operations = self.initial_kernel.ansatz.n_operations 115 | n_qubits = self.initial_kernel.ansatz.n_qubits 116 | allowed_generators = self.initial_kernel.get_allowed_operations() 117 | 118 | n_variables = 5 * n_operations + n_qubits 119 | step = [1, 1, 1, 1, 0.2] * n_operations + [1] * n_qubits 120 | lower_bound = [0, 0, 0, 0, 0.2] * n_operations + [0] * n_qubits 121 | upper_bound = [len(allowed_generators) - 1, n_qubits - 1, n_qubits - 2, n_features, 1.0] * n_operations + [3] * n_qubits 122 | return GridSpace(n_variables, step, lower_bound, upper_bound) 123 | -------------------------------------------------------------------------------- /src/quask/optimizer/reinforcement_learning_optimizer.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import copy 3 | from ..core import Operation, Ansatz, Kernel, KernelFactory 4 | from ..evaluator import KernelEvaluator 5 | from .base_kernel_optimizer import BaseKernelOptimizer 6 | 7 | 8 | class ReinforcementLearningOptimizer(BaseKernelOptimizer): 9 | """ 10 | Reinforcement learning based technique for optimize a kernel function 11 | """ 12 | 13 | def __init__(self, initial_kernel: Kernel, X: np.ndarray, y: np.ndarray, ke: KernelEvaluator): 14 | """ 15 | Initialization 16 | :param initial_kernel: initial kernel object 17 | :param X: datapoints 18 | :param y: labels 19 | :param ke: kernel evaluator object 20 | """ 21 | from mushroom_rl.core import Environment 22 | from mushroom_rl.core import Core 23 | from mushroom_rl.algorithms.value import SARSALambda 24 | from mushroom_rl.policy import EpsGreedy 25 | from mushroom_rl.utils.parameters import Parameter 26 | from mushroom_rl.utils.dataset import compute_J 27 | self.initial_kernel = copy.deepcopy(initial_kernel) 28 | self.X = X 29 | self.y = y 30 | self.ke = ke 31 | self.mdp = Environment.make('WideKernelEnvironment', initial_kernel=self.initial_kernel, X=X, y=y, ke=ke) 32 | self.agent = None 33 | self.core = None 34 | 35 | def optimize(self, initial_episodes=3, n_episodes=100, n_steps_per_fit=1, final_episodes=3): 36 | """ 37 | Optimization routine 38 | :param initial_episodes: 39 | :param n_steps: 40 | :param n_steps_per_fit: 41 | :param final_episodes: 42 | :return: 43 | """ 44 | from mushroom_rl.core import Environment 45 | from mushroom_rl.core import Core 46 | from mushroom_rl.algorithms.value import SARSALambda 47 | from mushroom_rl.policy import EpsGreedy 48 | from mushroom_rl.utils.parameters import Parameter 49 | from mushroom_rl.utils.dataset import compute_J 50 | # Policy 51 | epsilon = Parameter(value=1.) 52 | pi = EpsGreedy(epsilon=epsilon) 53 | learning_rate = Parameter(.1) 54 | 55 | # Agent 56 | self.agent = SARSALambda(self.mdp.info, pi, 57 | learning_rate=learning_rate, 58 | lambda_coeff=.9) 59 | 60 | # Reinforcement learning experiment 61 | self.core = Core(self.agent, self.mdp) 62 | 63 | # Visualize initial policy for 3 episodes 64 | dataset = self.core.evaluate(n_episodes=initial_episodes, render=True) 65 | print(f"{dataset=}") 66 | 67 | # Print the average objective value before learning 68 | J = np.mean(compute_J(dataset, self.mdp.info.gamma)) 69 | print(f'Objective function before learning: {J}') 70 | 71 | # Train 72 | self.core.learn(n_episodes=n_episodes, n_steps_per_fit=n_steps_per_fit, render=True) 73 | 74 | # Visualize results for 3 episodes 75 | dataset = self.core.evaluate(n_episodes=final_episodes, render=True) 76 | 77 | # Print the average objective value after learning 78 | J = np.mean(compute_J(dataset, self.mdp.info.gamma)) 79 | print(f'Objective function after learning: {J}') 80 | 81 | kernel = Kernel.from_numpy(self.mdp._state[1:], self.mdp.n_features, self.mdp.n_qubits, self.mdp.n_operations, self.mdp.allow_midcircuit_measurement) 82 | return kernel 83 | -------------------------------------------------------------------------------- /src/quask/optimizer/wide_kernel_environment.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from mushroom_rl.core import Environment, MDPInfo 3 | from mushroom_rl.utils.spaces import Discrete 4 | from ..core import Operation, Ansatz, Kernel, KernelFactory 5 | from ..evaluator import KernelEvaluator 6 | 7 | 8 | class WideKernelEnvironment(Environment): 9 | """ 10 | Implementation of a Mushroom-RL Environment for our problem 11 | """ 12 | 13 | @staticmethod 14 | def setup(): 15 | WideKernelEnvironment.register() 16 | 17 | def __init__(self, initial_kernel: Kernel, X: np.ndarray, y: np.ndarray, ke: KernelEvaluator): 18 | """ 19 | Initialization 20 | :param initial_kernel: initial kernel object 21 | :param X: datapoints 22 | :param y: labels 23 | :param ke: kernel evaluator object 24 | """ 25 | self.initial_kernel = initial_kernel 26 | self.n_operations = self.initial_kernel.ansatz.n_operations 27 | self.n_features = self.initial_kernel.ansatz.n_features 28 | self.n_qubits = self.initial_kernel.ansatz.n_qubits 29 | self.allow_midcircuit_measurement = self.initial_kernel.ansatz.allow_midcircuit_measurement 30 | self.X = X 31 | self.y = y 32 | self.ke = ke 33 | self.last_reward = None 34 | 35 | # Create the action space. 36 | action_space = Discrete( 37 | len(self.initial_kernel.get_allowed_operations()) 38 | * self.n_qubits 39 | * (self.n_qubits - 1) 40 | * (self.n_features + 1) 41 | ) 42 | 43 | # Create the observation space. 44 | observation_space = Discrete( 45 | len(self.initial_kernel.get_allowed_operations()) 46 | * self.n_qubits 47 | * (self.n_qubits - 1) 48 | * (self.n_features + 1) 49 | * self.n_operations 50 | ) 51 | 52 | # Create the MDPInfo structure, needed by the environment interface 53 | mdp_info = MDPInfo(observation_space, action_space, gamma=0.99, horizon=100) 54 | super().__init__(mdp_info) 55 | 56 | # Create a state class variable to store the current state 57 | self._state = self.serialize_state(0, initial_kernel) 58 | 59 | # Create the viewer 60 | self._viewer = None 61 | 62 | def serialize_state(self, n_operation, kernel): 63 | """ 64 | Pack the state of the optimization technique 65 | :param n_operation: number of operations currently performed 66 | :param kernel: kernel object 67 | :return: serialized state 68 | """ 69 | state = np.concatenate([np.array([n_operation], dtype=int), kernel.to_numpy()], dtype=object).ravel() 70 | return state.astype(int) 71 | 72 | def deserialize_state(self, array): 73 | """ 74 | Deserialized a previously packed state variable 75 | :param array: serialized state 76 | :return: tuple n_operations, kernel object 77 | """ 78 | kernel = Kernel.from_numpy(array[1:], self.n_features, self.n_qubits, self.n_operations, self.allow_midcircuit_measurement) 79 | n_operations = int(array[0]) 80 | return n_operations, kernel 81 | 82 | def render(self): 83 | """ 84 | Rendering function - we don't need that 85 | :return: None 86 | """ 87 | n_op, kernel = self.deserialize_state(self._state) 88 | print(f"{self.last_reward=:2.4f} {n_op=:2d} {kernel=}") 89 | 90 | def reset(self, state=None): 91 | """ 92 | Reset the state 93 | :param state: optional state 94 | :return: self._state variable 95 | """ 96 | if state is None: 97 | self.initial_kernel.ansatz.initialize_to_identity() 98 | self._state = self.serialize_state(0, self.initial_kernel) 99 | else: 100 | self._state = state 101 | return self._state 102 | 103 | def unpack_action(self, action): 104 | """ 105 | Unpack an action to a operation 106 | :param action: integer representing the action 107 | :return: dictionary of the operation 108 | """ 109 | generator_index = int(action % len(self.initial_kernel.get_allowed_operations())) 110 | action = action // len(self.initial_kernel.get_allowed_operations()) 111 | 112 | wires_0 = int(action % self.n_qubits) 113 | action = action // self.n_qubits 114 | 115 | wires_1 = int(action % (self.n_qubits - 1)) 116 | if wires_1 >= wires_0: 117 | wires_1 += 1 118 | action = action // (self.n_qubits - 1) 119 | 120 | feature = int(action % (self.n_features + 1)) 121 | action = action // (self.n_features + 1) 122 | assert action == 0 123 | 124 | return {'generator': self.initial_kernel.get_allowed_operations()[generator_index], 125 | 'wires': [wires_0, wires_1], 126 | 'feature': feature, 127 | 'bandwidth': 1.0} 128 | 129 | def step(self, action): 130 | 131 | the_action = self.unpack_action(action[0]) 132 | 133 | # Create kernel from state 134 | n_operations, kernel = self.deserialize_state(self._state) 135 | 136 | # Update kernel 137 | kernel.ansatz.change_operation(n_operations, the_action['feature'], the_action['wires'], the_action['generator'], the_action['bandwidth']) 138 | n_operations += 1 139 | 140 | # Update state 141 | self._state = self.serialize_state(n_operations, kernel) 142 | 143 | # Compute the reward as distance penalty from goal 144 | reward = -1 * self.ke.evaluate(kernel, None, self.X, self.y) 145 | self.last_reward = reward 146 | 147 | # Set the absorbing flag if goal is reached 148 | absorbing = self.n_operations == n_operations 149 | 150 | # Return all the information + empty dictionary (used to pass additional information) 151 | return self._state, reward, absorbing, {} 152 | 153 | 154 | -------------------------------------------------------------------------------- /src/quask/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CERN-IT-INNOVATION/QuASK/3ac5bef5aff678e2239af4207ed9e89b8a0668b9/src/quask/tests/__init__.py -------------------------------------------------------------------------------- /src/quask/tests/kernel_test.py: -------------------------------------------------------------------------------- 1 | from quask import Operation, Ansatz, KernelType, PennylaneKernel 2 | import numpy as np 3 | 4 | 5 | def test_static_single_qubit(KernelClass): 6 | 7 | # circuit: |0> - RX(pi) - 8 | # |0> - ID - 9 | ansats = Ansatz(n_features=1, n_qubits=2, n_operations=1) 10 | ansats.initialize_to_identity() 11 | ansats.change_generators(0, "XI") 12 | ansats.change_feature(0, -1) 13 | ansats.change_wires(0, [0, 1]) 14 | ansats.change_bandwidth(0, 1) 15 | 16 | # measurement operation = <1|Z|1> 17 | # probabilities: [0.0, 1.0] 18 | # observable: 0.0 * (+1) + 1.0 * (-1) = -1.0 19 | kernel = KernelClass(ansats, "ZI", KernelType.OBSERVABLE) 20 | x = kernel.phi(np.array([np.inf])) 21 | assert np.allclose(kernel.get_last_probabilities(), np.array([0, 1])), f"Incorrect measurement: {kernel.get_last_probabilities()}" 22 | assert np.isclose(x, -1), "Incorrect observable" 23 | 24 | # measurement operation = <1|X|1> = <1H|Z|H1> = <+|Z|+> 25 | # probabilities: [0.5, 0.5] 26 | # observable: 0.5 * (+1) + 0.5 * (-1) = 0.0 27 | kernel = KernelClass(ansats, "XI", KernelType.OBSERVABLE) 28 | x = kernel.phi(np.array([np.inf])) 29 | assert np.allclose(kernel.get_last_probabilities(), np.array([0.5, 0.5])), f"Incorrect measurement: {kernel.get_last_probabilities()}" 30 | assert np.isclose(x, 0), "Incorrect observable" 31 | 32 | # measurement operation = <1|Y|1> = <1HSdag|Z|SdagH1> = <[1/sqrt(2), -i/sqrt(2)]|Z|[1/sqrt(2), -i/sqrt(2)]> 33 | # probabilities: [0.5, 0.5] 34 | # observable: 0.5 * (+1) + 0.5 * (-1) = 0.0 35 | kernel = KernelClass(ansats, "YI", KernelType.OBSERVABLE) 36 | x = kernel.phi(np.array([np.inf])) 37 | assert np.allclose(kernel.get_last_probabilities(), np.array([0.5, 0.5])), f"Incorrect measurement: {kernel.get_last_probabilities()}" 38 | assert np.isclose(x, 0), "Incorrect observable" 39 | 40 | 41 | def test_static_two_qubit(KernelClass): 42 | 43 | # circuit: |0> - XY(#0) - 44 | # |0> - XY(#0) - 45 | ansats = Ansatz(n_features=1, n_qubits=2, n_operations=1) 46 | ansats.initialize_to_identity() 47 | ansats.change_generators(0, "XY") 48 | ansats.change_feature(0, 0) 49 | ansats.change_wires(0, [0, 1]) 50 | ansats.change_bandwidth(0, 1) 51 | 52 | kernel = KernelClass(ansats, "ZZ", KernelType.OBSERVABLE) 53 | x = kernel.phi(np.array([np.pi / 2])) 54 | assert np.allclose(kernel.get_last_probabilities(), np.array([0.5, 0.0, 0.0, 0.5])), "Incorrect measurement" 55 | assert np.isclose(x, 0), "Incorrect observable" 56 | 57 | 58 | test_static_single_qubit(PennylaneKernel) 59 | test_static_two_qubit(PennylaneKernel) 60 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CERN-IT-INNOVATION/QuASK/3ac5bef5aff678e2239af4207ed9e89b8a0668b9/tests/__init__.py -------------------------------------------------------------------------------- /tests/kernel_test.py: -------------------------------------------------------------------------------- 1 | from quask import Operation, Ansatz, KernelType, PennylaneKernel 2 | import numpy as np 3 | 4 | 5 | def test_static_single_qubit(KernelClass): 6 | 7 | # circuit: |0> - RX(pi) - 8 | # |0> - ID - 9 | ansats = Ansatz(n_features=1, n_qubits=2, n_operations=1) 10 | ansats.initialize_to_identity() 11 | ansats.change_generators(0, "XI") 12 | ansats.change_feature(0, -1) 13 | ansats.change_wires(0, [0, 1]) 14 | ansats.change_bandwidth(0, 1) 15 | 16 | # measurement operation = <1|Z|1> 17 | # probabilities: [0.0, 1.0] 18 | # observable: 0.0 * (+1) + 1.0 * (-1) = -1.0 19 | kernel = KernelClass(ansats, "ZI", KernelType.OBSERVABLE) 20 | x = kernel.phi(np.array([np.inf])) 21 | assert np.allclose(kernel.get_last_probabilities(), np.array([0, 1])), f"Incorrect measurement: {kernel.get_last_probabilities()}" 22 | assert np.isclose(x, -1), "Incorrect observable" 23 | 24 | # measurement operation = <1|X|1> = <1H|Z|H1> = <+|Z|+> 25 | # probabilities: [0.5, 0.5] 26 | # observable: 0.5 * (+1) + 0.5 * (-1) = 0.0 27 | kernel = KernelClass(ansats, "XI", KernelType.OBSERVABLE) 28 | x = kernel.phi(np.array([np.inf])) 29 | assert np.allclose(kernel.get_last_probabilities(), np.array([0.5, 0.5])), f"Incorrect measurement: {kernel.get_last_probabilities()}" 30 | assert np.isclose(x, 0), "Incorrect observable" 31 | 32 | # measurement operation = <1|Y|1> = <1HSdag|Z|SdagH1> = <[1/sqrt(2), -i/sqrt(2)]|Z|[1/sqrt(2), -i/sqrt(2)]> 33 | # probabilities: [0.5, 0.5] 34 | # observable: 0.5 * (+1) + 0.5 * (-1) = 0.0 35 | kernel = KernelClass(ansats, "YI", KernelType.OBSERVABLE) 36 | x = kernel.phi(np.array([np.inf])) 37 | assert np.allclose(kernel.get_last_probabilities(), np.array([0.5, 0.5])), f"Incorrect measurement: {kernel.get_last_probabilities()}" 38 | assert np.isclose(x, 0), "Incorrect observable" 39 | 40 | 41 | def test_static_two_qubit(KernelClass): 42 | 43 | # circuit: |0> - XY(#0) - 44 | # |0> - XY(#0) - 45 | ansats = Ansatz(n_features=1, n_qubits=2, n_operations=1) 46 | ansats.initialize_to_identity() 47 | ansats.change_generators(0, "XY") 48 | ansats.change_feature(0, 0) 49 | ansats.change_wires(0, [0, 1]) 50 | ansats.change_bandwidth(0, 1) 51 | 52 | kernel = KernelClass(ansats, "ZZ", KernelType.OBSERVABLE) 53 | x = kernel.phi(np.array([np.pi / 2])) 54 | assert np.allclose(kernel.get_last_probabilities(), np.array([0.5, 0.0, 0.0, 0.5])), "Incorrect measurement" 55 | assert np.isclose(x, 0), "Incorrect observable" 56 | 57 | 58 | test_static_single_qubit(PennylaneKernel) 59 | test_static_two_qubit(PennylaneKernel) 60 | -------------------------------------------------------------------------------- /tests/test_example.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import sys 3 | 4 | sys.path.append("./src/") 5 | 6 | 7 | from quask.core import Ansatz 8 | 9 | 10 | def test_trivial(): 11 | """ Trivial test. """ 12 | print("Passed") 13 | 14 | 15 | def test_ansatz_init(): 16 | ansatz = Ansatz(n_features=2, n_qubits=2, n_operations=2, allow_midcircuit_measurement=False) 17 | assert ansatz is not None, "Could not create an Ansatz object." 18 | -------------------------------------------------------------------------------- /tests/test_pennylane_kernel.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.append("src/") 3 | import pytest 4 | import quask 5 | import numpy as np 6 | from quask.core import Ansatz, Kernel, KernelFactory, KernelType 7 | from quask.core_implementation import PennylaneKernel 8 | 9 | def check_kernel_value(kernel: Kernel, x1: float, x2: float, expected: float): 10 | similarity = kernel.kappa(x1, x2) 11 | print(similarity, expected) 12 | assert np.isclose(similarity, expected), f"Kernel value is {similarity:0.3f} while {expected:0.3f} was expected" 13 | 14 | def check_kernel_rx_value(kernel: Kernel, x1: float, x2: float): 15 | def rx(theta): 16 | return np.array([[np.cos(theta/2), -1j*np.sin(theta/2)], [-1j*np.sin(theta/2), np.cos(theta/2)]]) 17 | ket_zero = np.array([[1], [0]]) 18 | ket_phi = np.linalg.inv(rx(x2)) @ rx(x1) @ ket_zero 19 | expected_similarity = (np.abs(ket_phi[0])**2).real 20 | check_kernel_value(kernel, np.array([x1]), np.array([x2]), expected_similarity) 21 | 22 | def test_rx_kernel_fidelity(): 23 | 24 | ansatz = Ansatz(n_features=1, n_qubits=2, n_operations=1, allow_midcircuit_measurement=False) 25 | ansatz.initialize_to_identity() 26 | ansatz.change_operation(0, new_feature=0, new_wires=[0, 1], new_generator="XI", new_bandwidth=1.0) 27 | kernel = PennylaneKernel(ansatz, "ZZ", KernelType.FIDELITY, device_name="default.qubit", n_shots=None) 28 | 29 | check_kernel_value(kernel, np.array([0.33]), np.array([0.33]), 1.0) 30 | 31 | check_kernel_rx_value(kernel, 0.00, 0.00) 32 | check_kernel_rx_value(kernel, 0.33, 0.33) 33 | check_kernel_rx_value(kernel, np.pi/2, np.pi/2) 34 | check_kernel_rx_value(kernel, np.pi, np.pi) 35 | check_kernel_rx_value(kernel, 0, np.pi) 36 | check_kernel_rx_value(kernel, 0.33, np.pi) 37 | check_kernel_rx_value(kernel, np.pi/2, np.pi) 38 | check_kernel_rx_value(kernel, 0, 0.55) 39 | check_kernel_rx_value(kernel, 0.33, 0.55) 40 | check_kernel_rx_value(kernel, np.pi/2, 0.55) 41 | 42 | def test_rx_kernel_fidelity(): 43 | 44 | ansatz = Ansatz(n_features=1, n_qubits=2, n_operations=1, allow_midcircuit_measurement=False) 45 | ansatz.initialize_to_identity() 46 | ansatz.change_operation(0, new_feature=0, new_wires=[0, 1], new_generator="XI", new_bandwidth=1.0) 47 | kernel = PennylaneKernel(ansatz, "ZZ", KernelType.SWAP_TEST, device_name="default.qubit", n_shots=None) 48 | 49 | check_kernel_value(kernel, np.array([0.33]), np.array([0.33]), 1.0) 50 | 51 | check_kernel_rx_value(kernel, 0.00, 0.00) 52 | check_kernel_rx_value(kernel, 0.33, 0.33) 53 | check_kernel_rx_value(kernel, np.pi/2, np.pi/2) 54 | check_kernel_rx_value(kernel, np.pi, np.pi) 55 | check_kernel_rx_value(kernel, 0, np.pi) 56 | check_kernel_rx_value(kernel, 0.33, np.pi) 57 | check_kernel_rx_value(kernel, np.pi/2, np.pi) 58 | check_kernel_rx_value(kernel, 0, 0.55) 59 | check_kernel_rx_value(kernel, 0.33, 0.55) 60 | check_kernel_rx_value(kernel, np.pi/2, 0.55) 61 | 62 | --------------------------------------------------------------------------------