From 009c3998d7331e6bee3edbd246899abf2cd9890a Mon Sep 17 00:00:00 2001 From: FrancescaSchiav Date: Thu, 29 Feb 2024 16:57:15 +0000 Subject: [PATCH 01/85] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1938e68af..372787dd3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Qiskit Machine Learning -[![License](https://img.shields.io/github/license/Qiskit/qiskit-machine-learning.svg?style=popout-square)](https://opensource.org/licenses/Apache-2.0)[![Build Status](https://github.com/qiskit-community/qiskit-machine-learning/workflows/Machine%20Learning%20Unit%20Tests/badge.svg?branch=main)](https://github.com/qiskit-community/qiskit-machine-learning/actions?query=workflow%3A"Machine%20Learning%20Unit%20Tests"+branch%3Amain+event%3Apush)[![](https://img.shields.io/github/release/Qiskit/qiskit-machine-learning.svg?style=popout-square)](https://github.com/qiskit-community/qiskit-machine-learning/releases)[![](https://img.shields.io/pypi/dm/qiskit-machine-learning.svg?style=popout-square)](https://pypi.org/project/qiskit-machine-learning/)[![Coverage Status](https://coveralls.io/repos/github/Qiskit/qiskit-machine-learning/badge.svg?branch=main)](https://coveralls.io/github/Qiskit/qiskit-machine-learning?branch=main) +[![License](https://img.shields.io/github/license/Qiskit/qiskit-machine-learning.svg?style=popout-square)](https://opensource.org/licenses/Apache-2.0)[![Build Status](https://github.com/qiskit-community/qiskit-machine-learning/actions/workflows/main.yml/badge.svg)](https://github.com/qiskit-community/qiskit-machine-learning/actions?query=workflow%3A"Machine%20Learning%20Unit%20Tests"+branch%3Amain+event%3Apush)[![](https://img.shields.io/github/release/Qiskit/qiskit-machine-learning.svg?style=popout-square)](https://github.com/qiskit-community/qiskit-machine-learning/releases)[![](https://img.shields.io/pypi/dm/qiskit-machine-learning.svg?style=popout-square)](https://pypi.org/project/qiskit-machine-learning/)[![Coverage Status](https://coveralls.io/repos/github/Qiskit/qiskit-machine-learning/badge.svg?branch=main)](https://coveralls.io/github/Qiskit/qiskit-machine-learning?branch=main) Qiskit Machine Learning introduces fundamental computational building blocks - such as Quantum Kernels and Quantum Neural Networks - used in different applications, including classification and regression. From 32219fb7b2aea6437a090e65df0bc0cc685645a6 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Thu, 14 Mar 2024 17:45:35 +0000 Subject: [PATCH 02/85] Generalize the Einstein summation signature --- .../connectors/torch_connector.py | 65 +++++++++++++++++-- 1 file changed, 58 insertions(+), 7 deletions(-) diff --git a/qiskit_machine_learning/connectors/torch_connector.py b/qiskit_machine_learning/connectors/torch_connector.py index 537d97d76..f2817ffea 100644 --- a/qiskit_machine_learning/connectors/torch_connector.py +++ b/qiskit_machine_learning/connectors/torch_connector.py @@ -13,7 +13,7 @@ """A connector to use Qiskit (Quantum) Neural Networks as PyTorch modules.""" from __future__ import annotations -from typing import Tuple, Any, cast +from typing import Tuple, Any, cast, Literal import numpy as np @@ -52,12 +52,57 @@ class Module: # type: ignore pass +def get_einsum_signature(n_dimensions: int, return_type: Literal["input", "weight"]) -> str: + """ + Generate an Einstein summation signature for a given number of dimensions and return type. + + Args: + n_dimensions (int): The number of dimensions for the summation. + return_type (Literal["input", "weight"]): The type of the return signature. + - "input": Return signature includes all input indices except the last one. + - "weight": Return signature includes only the last index as the output. + + Returns: + str: The Einstein summation signature. + + Raises: + RuntimeError: If the number of dimensions exceeds the character limit. + ValueError: If an invalid return type is provided. + + Example: + Consider a scenario where n_dimensions is 3 and return_type is "input": + >>> get_einsum_signature(3, "input") + 'ab,abc->ac' + This returns the Einstein summation signature 'ab,abc->ac' for input with three dimensions. + """ + trace = "" + char_limit = 26 + for i in range(n_dimensions): + trace += chr(97 + i) # chr(97) == 'a' + if i >= char_limit: + raise RuntimeError( + f"Cannot define an Einstein summation with more tha {char_limit:d} dimensions." + ) + + if return_type == "input": + signature = f"{trace[:-1]},{trace:s}->{trace[0] + trace[2:]}" + elif return_type == "weight": + signature = f"{trace[:-1]},{trace:s}->{trace[-1]}" + else: + raise ValueError( + f'The only allowed return types are ["input", "weight"], got {return_type:s} instead.' + ) + + return signature + + @_optionals.HAS_TORCH.require_in_instance class TorchConnector(Module): """Connects a Qiskit (Quantum) Neural Network to PyTorch.""" # pylint: disable=abstract-method class _TorchNNFunction(Function): + # pylint: disable=arguments-differ @staticmethod def forward( # type: ignore @@ -187,7 +232,9 @@ def backward(ctx: Any, grad_output: Tensor) -> Tuple: # type: ignore # able to do back-prop in a batched manner. # Pytorch does not support sparse einsum, so we rely on Sparse. # pylint: disable=no-member - input_grad = sparse.einsum("ij,ijk->ik", grad_coo, input_grad) + n_dimension = max(grad_coo.ndim, input_grad.ndim) + signature = get_einsum_signature(n_dimension, return_type="input") + input_grad = sparse.einsum(signature, grad_coo, input_grad) # return sparse gradients input_grad = torch.sparse_coo_tensor(input_grad.coords, input_grad.data) @@ -205,7 +252,9 @@ def backward(ctx: Any, grad_output: Tensor) -> Tuple: # type: ignore input_grad = torch.as_tensor(input_grad, dtype=torch.float) # same as above - input_grad = torch.einsum("ij,ijk->ik", grad_output.detach().cpu(), input_grad) + n_dimension = max(grad_output.detach().cpu().ndim, input_grad.ndim) + signature = get_einsum_signature(n_dimension, return_type="input") + input_grad = torch.einsum(signature, grad_output.detach().cpu(), input_grad) # place the resulting tensor to the device where they were stored input_grad = input_grad.to(input_data.device) @@ -226,7 +275,9 @@ def backward(ctx: Any, grad_output: Tensor) -> Tuple: # type: ignore # w.r.t. each parameter k. The weights' dimension is independent of the # batch size. # pylint: disable=no-member - weights_grad = sparse.einsum("ij,ijk->k", grad_coo, weights_grad) + n_dimension = max(grad_coo.ndim, weights_grad.ndim) + signature = get_einsum_signature(n_dimension, return_type="weight") + weights_grad = sparse.einsum(signature, grad_coo, weights_grad) # return sparse gradients weights_grad = torch.sparse_coo_tensor( @@ -244,9 +295,9 @@ def backward(ctx: Any, grad_output: Tensor) -> Tuple: # type: ignore weights_grad = weights_grad.todense() weights_grad = torch.as_tensor(weights_grad, dtype=torch.float) # same as above - weights_grad = torch.einsum( - "ij,ijk->k", grad_output.detach().cpu(), weights_grad - ) + n_dimension = max(grad_output.detach().cpu().ndim, weights_grad.ndim) + signature = get_einsum_signature(n_dimension, return_type="weight") + weights_grad = torch.einsum(signature, grad_output.detach().cpu(), weights_grad) # place the resulting tensor to the device where they were stored weights_grad = weights_grad.to(weights.device) From 17d8c33595f247013ebd8578612940dbf4841f23 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Fri, 15 Mar 2024 14:47:56 +0000 Subject: [PATCH 03/85] Add reno --- ...fix-716-mismatch-dimension-pytorch-ba01bea90eba1435.yaml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 releasenotes/notes/fix-716-mismatch-dimension-pytorch-ba01bea90eba1435.yaml diff --git a/releasenotes/notes/fix-716-mismatch-dimension-pytorch-ba01bea90eba1435.yaml b/releasenotes/notes/fix-716-mismatch-dimension-pytorch-ba01bea90eba1435.yaml new file mode 100644 index 000000000..c64e30f1d --- /dev/null +++ b/releasenotes/notes/fix-716-mismatch-dimension-pytorch-ba01bea90eba1435.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Fixes the dimension mismatch error in the `torch_connector` raised when using other-than 3D datasets. + The updated implementation defines the Einstein summation signature dynamically based on the number of + dimensions `ndim` of the input data (up to 26 dimensions). From d6f688d1463dc0d1f57a9b50cefdd3d1faf815f8 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Fri, 15 Mar 2024 14:52:41 +0000 Subject: [PATCH 04/85] Update Copyright --- qiskit_machine_learning/connectors/torch_connector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_machine_learning/connectors/torch_connector.py b/qiskit_machine_learning/connectors/torch_connector.py index f2817ffea..b72b3f52e 100644 --- a/qiskit_machine_learning/connectors/torch_connector.py +++ b/qiskit_machine_learning/connectors/torch_connector.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2021, 2023. +# (C) Copyright IBM 2021, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory From 034785b1d3ff525633329445249a56abf704a2f3 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Wed, 20 Mar 2024 15:38:02 +0100 Subject: [PATCH 05/85] Rename and add test --- .../connectors/torch_connector.py | 12 ++++++------ test/connectors/test_torch_connector.py | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/qiskit_machine_learning/connectors/torch_connector.py b/qiskit_machine_learning/connectors/torch_connector.py index b72b3f52e..d975dc0bf 100644 --- a/qiskit_machine_learning/connectors/torch_connector.py +++ b/qiskit_machine_learning/connectors/torch_connector.py @@ -52,7 +52,7 @@ class Module: # type: ignore pass -def get_einsum_signature(n_dimensions: int, return_type: Literal["input", "weight"]) -> str: +def _get_einsum_signature(n_dimensions: int, return_type: Literal["input", "weight"]) -> str: """ Generate an Einstein summation signature for a given number of dimensions and return type. @@ -71,7 +71,7 @@ def get_einsum_signature(n_dimensions: int, return_type: Literal["input", "weigh Example: Consider a scenario where n_dimensions is 3 and return_type is "input": - >>> get_einsum_signature(3, "input") + >>> _get_einsum_signature(3, "input") 'ab,abc->ac' This returns the Einstein summation signature 'ab,abc->ac' for input with three dimensions. """ @@ -233,7 +233,7 @@ def backward(ctx: Any, grad_output: Tensor) -> Tuple: # type: ignore # Pytorch does not support sparse einsum, so we rely on Sparse. # pylint: disable=no-member n_dimension = max(grad_coo.ndim, input_grad.ndim) - signature = get_einsum_signature(n_dimension, return_type="input") + signature = _get_einsum_signature(n_dimension, return_type="input") input_grad = sparse.einsum(signature, grad_coo, input_grad) # return sparse gradients @@ -253,7 +253,7 @@ def backward(ctx: Any, grad_output: Tensor) -> Tuple: # type: ignore # same as above n_dimension = max(grad_output.detach().cpu().ndim, input_grad.ndim) - signature = get_einsum_signature(n_dimension, return_type="input") + signature = _get_einsum_signature(n_dimension, return_type="input") input_grad = torch.einsum(signature, grad_output.detach().cpu(), input_grad) # place the resulting tensor to the device where they were stored @@ -276,7 +276,7 @@ def backward(ctx: Any, grad_output: Tensor) -> Tuple: # type: ignore # batch size. # pylint: disable=no-member n_dimension = max(grad_coo.ndim, weights_grad.ndim) - signature = get_einsum_signature(n_dimension, return_type="weight") + signature = _get_einsum_signature(n_dimension, return_type="weight") weights_grad = sparse.einsum(signature, grad_coo, weights_grad) # return sparse gradients @@ -296,7 +296,7 @@ def backward(ctx: Any, grad_output: Tensor) -> Tuple: # type: ignore weights_grad = torch.as_tensor(weights_grad, dtype=torch.float) # same as above n_dimension = max(grad_output.detach().cpu().ndim, weights_grad.ndim) - signature = get_einsum_signature(n_dimension, return_type="weight") + signature = _get_einsum_signature(n_dimension, return_type="weight") weights_grad = torch.einsum(signature, grad_output.detach().cpu(), weights_grad) # place the resulting tensor to the device where they were stored diff --git a/test/connectors/test_torch_connector.py b/test/connectors/test_torch_connector.py index eeb526dc5..b439b5cb2 100644 --- a/test/connectors/test_torch_connector.py +++ b/test/connectors/test_torch_connector.py @@ -25,6 +25,7 @@ from qiskit_machine_learning import QiskitMachineLearningError from qiskit_machine_learning.connectors import TorchConnector from qiskit_machine_learning.neural_networks import SamplerQNN, EstimatorQNN +from qiskit_machine_learning.connectors.torch_connector import _get_einsum_signature @ddt @@ -44,6 +45,19 @@ def setup_test(self): torch.tensor([[[1.0], [2.0]], [[3.0], [4.0]]]), ] + def test_get_einsum_signature(self): + # Test valid inputs and outputs + self.assertEqual(_get_einsum_signature(3, "input"), "ab,abc->ac") + self.assertEqual(_get_einsum_signature(3, "weight"), "ab,abc->c") + + # Test raises for invalid return_type + with self.assertRaises(ValueError): + _get_einsum_signature(3, "invalid_type") + + # Test raises for exceeding character limit + with self.assertRaises(RuntimeError): + _get_einsum_signature(30, "input") + def _validate_backward_automatically(self, model: TorchConnector) -> None: """Uses PyTorch to validate the backward pass / autograd. From 04b886d1f869038f27a557f7ec7fe2efcac1fa6e Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Wed, 20 Mar 2024 15:39:49 +0100 Subject: [PATCH 06/85] Update Copyright --- test/connectors/test_torch_connector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/connectors/test_torch_connector.py b/test/connectors/test_torch_connector.py index b439b5cb2..57609e26b 100644 --- a/test/connectors/test_torch_connector.py +++ b/test/connectors/test_torch_connector.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2021, 2023. +# (C) Copyright IBM 2021, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory From 7b2e9be334ae9aa7dece85620aaf0b4cee6ff7b2 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Wed, 3 Apr 2024 20:01:39 +0100 Subject: [PATCH 07/85] Add docstring for `test_get_einsum_signature` --- test/connectors/test_torch_connector.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/connectors/test_torch_connector.py b/test/connectors/test_torch_connector.py index 57609e26b..0c486ea9c 100644 --- a/test/connectors/test_torch_connector.py +++ b/test/connectors/test_torch_connector.py @@ -46,6 +46,11 @@ def setup_test(self): ] def test_get_einsum_signature(self): + """ + Tests the functionality of `_get_einsum_signature` function by providing + valid inputs (`n_dimensions` = 3) and expected outputs. It also checks for error + handling scenarios where invalid input arguments are provided. + """ # Test valid inputs and outputs self.assertEqual(_get_einsum_signature(3, "input"), "ab,abc->ac") self.assertEqual(_get_einsum_signature(3, "weight"), "ab,abc->c") From 6a8a136bc5767ce94c79d2508e5c51f80a769424 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Wed, 3 Apr 2024 20:19:33 +0100 Subject: [PATCH 08/85] Correct spelling --- qiskit_machine_learning/connectors/torch_connector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_machine_learning/connectors/torch_connector.py b/qiskit_machine_learning/connectors/torch_connector.py index d975dc0bf..d93fc50a7 100644 --- a/qiskit_machine_learning/connectors/torch_connector.py +++ b/qiskit_machine_learning/connectors/torch_connector.py @@ -73,7 +73,7 @@ def _get_einsum_signature(n_dimensions: int, return_type: Literal["input", "weig Consider a scenario where n_dimensions is 3 and return_type is "input": >>> _get_einsum_signature(3, "input") 'ab,abc->ac' - This returns the Einstein summation signature 'ab,abc->ac' for input with three dimensions. + This returns the Einstein summation signature for an input with three dimensions. """ trace = "" char_limit = 26 From 31a826edb46b9969bc488f14d916adcc5f4d0e9b Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Wed, 3 Apr 2024 20:38:36 +0100 Subject: [PATCH 09/85] Disable spellcheck for comments --- qiskit_machine_learning/connectors/torch_connector.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/qiskit_machine_learning/connectors/torch_connector.py b/qiskit_machine_learning/connectors/torch_connector.py index d93fc50a7..7ea9bfa77 100644 --- a/qiskit_machine_learning/connectors/torch_connector.py +++ b/qiskit_machine_learning/connectors/torch_connector.py @@ -72,13 +72,15 @@ def _get_einsum_signature(n_dimensions: int, return_type: Literal["input", "weig Example: Consider a scenario where n_dimensions is 3 and return_type is "input": >>> _get_einsum_signature(3, "input") - 'ab,abc->ac' + 'ab,abc->ac' # pylint: disable=wrong-spelling-in-docstring This returns the Einstein summation signature for an input with three dimensions. """ trace = "" char_limit = 26 for i in range(n_dimensions): - trace += chr(97 + i) # chr(97) == 'a' + # pylint: disable=wrong-spelling-in-comment + # chr(97) == 'a' + trace += chr(97 + i) if i >= char_limit: raise RuntimeError( f"Cannot define an Einstein summation with more tha {char_limit:d} dimensions." From 5b4f617a5addae4f1417c050750f10b41859c7f5 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Wed, 3 Apr 2024 21:18:37 +0100 Subject: [PATCH 10/85] Add `docstring` in pylint dict --- .pylintdict | 1 + 1 file changed, 1 insertion(+) diff --git a/.pylintdict b/.pylintdict index cf55f403f..5c13d2348 100644 --- a/.pylintdict +++ b/.pylintdict @@ -66,6 +66,7 @@ discretize discretized discriminative distro +docstring dok dt eigenstate From b0d0590ac0f9faf20241784d9118de795668b772 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Wed, 3 Apr 2024 21:32:53 +0100 Subject: [PATCH 11/85] Delete example in docstring --- qiskit_machine_learning/connectors/torch_connector.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/qiskit_machine_learning/connectors/torch_connector.py b/qiskit_machine_learning/connectors/torch_connector.py index 7ea9bfa77..4027fb9f2 100644 --- a/qiskit_machine_learning/connectors/torch_connector.py +++ b/qiskit_machine_learning/connectors/torch_connector.py @@ -68,12 +68,6 @@ def _get_einsum_signature(n_dimensions: int, return_type: Literal["input", "weig Raises: RuntimeError: If the number of dimensions exceeds the character limit. ValueError: If an invalid return type is provided. - - Example: - Consider a scenario where n_dimensions is 3 and return_type is "input": - >>> _get_einsum_signature(3, "input") - 'ab,abc->ac' # pylint: disable=wrong-spelling-in-docstring - This returns the Einstein summation signature for an input with three dimensions. """ trace = "" char_limit = 26 From 240d02fb36b04260027276c3003b94d800aed352 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Wed, 3 Apr 2024 21:55:42 +0100 Subject: [PATCH 12/85] Add Einstein in pylint dict --- .pylintdict | 1 + 1 file changed, 1 insertion(+) diff --git a/.pylintdict b/.pylintdict index 5c13d2348..abcd3ddfd 100644 --- a/.pylintdict +++ b/.pylintdict @@ -72,6 +72,7 @@ dt eigenstate eigenstates einsum +einstein endian entangler estimatorqnn From f8c32dd318744aa08a90ed0bc64cafb14fdfd7e0 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Mon, 8 Apr 2024 15:50:36 +0100 Subject: [PATCH 13/85] Add full use case in einsum dict --- test/connectors/test_torch_connector.py | 209 +++++++++++++++++++++--- 1 file changed, 186 insertions(+), 23 deletions(-) diff --git a/test/connectors/test_torch_connector.py b/test/connectors/test_torch_connector.py index 0c486ea9c..b81f954dd 100644 --- a/test/connectors/test_torch_connector.py +++ b/test/connectors/test_torch_connector.py @@ -12,11 +12,12 @@ """Test Torch Connector.""" import itertools -from typing import cast +from typing import cast, Union, List, Tuple from test.connectors.test_torch import TestTorch import numpy as np +import torch from ddt import ddt, data, unpack, idata from qiskit import QuantumCircuit from qiskit.circuit.library import RealAmplitudes, ZFeatureMap @@ -34,7 +35,6 @@ class TestTorchConnector(TestTorch): def setup_test(self): super().setup_test() - import torch # pylint: disable=attribute-defined-outside-init self._test_data = [ @@ -45,31 +45,12 @@ def setup_test(self): torch.tensor([[[1.0], [2.0]], [[3.0], [4.0]]]), ] - def test_get_einsum_signature(self): - """ - Tests the functionality of `_get_einsum_signature` function by providing - valid inputs (`n_dimensions` = 3) and expected outputs. It also checks for error - handling scenarios where invalid input arguments are provided. - """ - # Test valid inputs and outputs - self.assertEqual(_get_einsum_signature(3, "input"), "ab,abc->ac") - self.assertEqual(_get_einsum_signature(3, "weight"), "ab,abc->c") - - # Test raises for invalid return_type - with self.assertRaises(ValueError): - _get_einsum_signature(3, "invalid_type") - - # Test raises for exceeding character limit - with self.assertRaises(RuntimeError): - _get_einsum_signature(30, "input") - def _validate_backward_automatically(self, model: TorchConnector) -> None: """Uses PyTorch to validate the backward pass / autograd. Args: model: The model to be tested. """ - import torch # test autograd func = TorchConnector._TorchNNFunction.apply # (input, weights, qnn) @@ -93,7 +74,6 @@ def _validate_backward_automatically(self, model: TorchConnector) -> None: self.assertTrue(test) def _validate_forward(self, model: TorchConnector): - import torch for batch_size in [1, 2]: input_data = torch.rand((batch_size, model.neural_network.num_inputs)) @@ -123,7 +103,6 @@ def _validate_forward(self, model: TorchConnector): model(wrong_input) def _validate_backward(self, model: TorchConnector): - import torch for batch_size in [1, 2]: input_data = torch.rand((batch_size, model.neural_network.num_inputs)) @@ -260,3 +239,187 @@ def test_estimator_qnn(self, num_qubits, observables): self._validate_forward(model) self._validate_backward(model) self._validate_backward_automatically(model) + + def _create_convolutional_layer( + self, input_channel: int, output_channel: int, num_qubits: int, num_weight: int + ): + from qiskit.circuit import Parameter + + class ConvolutionalLayer(torch.nn.Module): + """ + Quantum Convolutional Neural Network layer implemented using Qiskit and PyTorch. + + Args: + input_channel (int): Number of input channels. + output_channel (int): Number of output channels. + num_qubits (int): Number of qubits to represent weights. + num_weight (int): Number of weight parameters in the quantum circuit. + kernel_size (int, optional): Size of the convolutional kernel. Defaults to 3. + stride (int, optional): Stride of the convolution. Defaults to 1. + """ + + def __init__( + self, + input_channel: int, + output_channel: int, + num_qubits: int, + num_weight: int, + kernel_size: int = 3, + stride: int = 1, + ): + + super().__init__() + self.kernel_size = kernel_size + self.stride = stride + self.input_channel = input_channel + self.output_channel = output_channel + self.num_weight = num_weight + self.num_input = kernel_size * kernel_size * input_channel + self.num_qubits = num_qubits + self.qnn = TorchConnector(self.sampler()) + if 2**num_qubits < output_channel: + raise ValueError( + ( + f"The output channel must be >= 2**num_qubits. " + f"Got output_channel {output_channel:d} < 2 ** {num_qubits:d}" + ) + ) + + @staticmethod + def build_circuit( + num_weights: int, num_input: int, num_qubits: int = 3 + ) -> Tuple[QuantumCircuit, List[Parameter], List[Parameter]]: + """ + Build the quantum circuit for the convolutional layer. + + Args: + num_weights (int): Number of weight parameters. + num_input (int): Number of input parameters. + num_qubits (int, optional): Number of qubits for representing parameters. + Defaults to 3. + + Returns: + Tuple[QuantumCircuit, List[Parameter], List[Parameter]]: Quantum circuit, + list of weight parameters, list of input parameters. + """ + qc = QuantumCircuit(num_qubits) + weight_params = [Parameter(f"w{i}") for i in range(num_weights)] + input_params = [Parameter(f"x{i}") for i in range(num_input)] + + # Construct the quantum circuit with the parameters + for i in range(num_qubits): + qc.h(i) + for i in range(num_input): + qc.ry(input_params[i] * 2 * torch.pi, i % num_qubits) + for i in range(num_qubits - 1): + qc.cx(i, i + 1) + for i in range(num_weights): + qc.rx(weight_params[i] * 2 * torch.pi, i % num_qubits) + for i in range(num_qubits - 1): + qc.cx(i, i + 1) + + return qc, weight_params, input_params + + def sampler(self) -> SamplerQNN: + """ + Creates a SamplerQNN object representing the quantum neural network. + + Returns: + SamplerQNN: Quantum neural network. + """ + qc, weight_params, input_params = self.build_circuit( + self.num_weight, self.num_input, 3 + ) + + # Use SamplerQNN to convert the quantum circuit to a PyTorch module + return SamplerQNN( + circuit=qc, + weight_params=weight_params, + interpret=self.interpret, + input_params=input_params, + output_shape=self.output_channel, + ) + + def interpret(self, output: Union[List[int], int]) -> Union[int, List[int]]: + """ + Interprets the output from the quantum circuit. + + Args: + output (Union[List[int], int]): Output from the quantum circuit. + + Returns: + Union[int, List[int]]: Interpreted output. + """ + return output % self.output_channel + + def forward(self, input_tensor: torch.Tensor) -> torch.Tensor: + """ + Perform forward pass through the quantum convolutional layer. + + Args: + input_tensor (torch.Tensor): Input tensor. + + Returns: + torch.Tensor: Output tensor after convolution. + """ + height = len(range(0, input_tensor.shape[-2] - self.kernel_size + 1, self.stride)) + width = len(range(0, input_tensor.shape[-1] - self.kernel_size + 1, self.stride)) + output = torch.zeros((input_tensor.shape[0], self.output_channel, height, width)) + input_tensor = torch.nn.functional.unfold( + input_tensor[...], kernel_size=self.kernel_size, stride=self.stride + ) + qnn_output = self.qnn(input_tensor.permute(2, 0, 1)).permute(1, 2, 0) + qnn_output = torch.reshape( + qnn_output, shape=(input_tensor.shape[0], self.output_channel, height, width) + ) + output += qnn_output + return output + + return ConvolutionalLayer(input_channel, output_channel, num_qubits, num_weight, stride=1) + + def test_get_einsum_signature(self): + """ + Tests the functionality of `_get_einsum_signature` function. + + This function is tested with valid inputs (e.g., `n_dimensions` = 3) and expected outputs. + It also covers error handling scenarios for invalid input arguments. + + Valid Input and Output Tests: + - Tests with `n_dimensions` equal to 3 and different return types ('input' and 'weight') + to ensure correct signature generation for Einstein summation notation. + + Error Handling Tests: + - Raises a ValueError when an invalid return_type is provided. + - Raises a RuntimeError when the character limit for signature generation is exceeded. + + Additional Test Scenario: + - Tests `_get_einsum_signature` in the context of a convolutional layer based on Sampler QNN, + using a 4-D input tensor (batch, channel, height, width) to ensure compatibility. + This test mirrors Issue https://github.com/qiskit-community/qiskit-machine-learning/issues/716 + + Note: Backward test is run implicitly within the context of convolutional layer execution. + + pylint disable=SPELLING + """ + # Test valid inputs and outputs + self.assertEqual(_get_einsum_signature(3, "input"), "ab,abc->ac") + self.assertEqual(_get_einsum_signature(3, "weight"), "ab,abc->c") + + # Test raises for invalid return_type + with self.assertRaises(ValueError): + _get_einsum_signature(3, "invalid_type") + + # Test raises for exceeding character limit + with self.assertRaises(RuntimeError): + _get_einsum_signature(30, "input") + + # Test with a convolutional layer based on Sampler QNN and 4-D input + # Refer to issue https://github.com/qiskit-community/qiskit-machine-learning/issues/716 + model = self._create_convolutional_layer(3, 1, 3, 3) + input_tensor = torch.rand((2, 3, 6, 6)) + input_tensor.requires_grad = True + output_tensor = model.forward(input_tensor) + output_tensor = torch.sum(output_tensor) + + # Run `backward`, which calls `_get_einsum_signature` + output_tensor.backward() From 34322b20d4d92431ccd78afc5a67c44611d5adc4 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Mon, 8 Apr 2024 17:57:43 +0100 Subject: [PATCH 14/85] Spelling and type ignore --- .pylintdict | 1 + test/connectors/test_torch_connector.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.pylintdict b/.pylintdict index abcd3ddfd..357add023 100644 --- a/.pylintdict +++ b/.pylintdict @@ -199,6 +199,7 @@ qiskit's qnn qsvc qsvr +quantumcircuit qubit qubits rangle diff --git a/test/connectors/test_torch_connector.py b/test/connectors/test_torch_connector.py index b81f954dd..d95b25bf1 100644 --- a/test/connectors/test_torch_connector.py +++ b/test/connectors/test_torch_connector.py @@ -335,7 +335,7 @@ def sampler(self) -> SamplerQNN: return SamplerQNN( circuit=qc, weight_params=weight_params, - interpret=self.interpret, + interpret=self.interpret, # type: ignore input_params=input_params, output_shape=self.output_channel, ) From 94ec48ca8f77bf43922e1a1eb044a8142fbd3136 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Mon, 8 Apr 2024 18:14:55 +0100 Subject: [PATCH 15/85] Spelling and type ignore --- test/connectors/test_torch_connector.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/connectors/test_torch_connector.py b/test/connectors/test_torch_connector.py index d95b25bf1..3d626762e 100644 --- a/test/connectors/test_torch_connector.py +++ b/test/connectors/test_torch_connector.py @@ -17,7 +17,6 @@ from test.connectors.test_torch import TestTorch import numpy as np -import torch from ddt import ddt, data, unpack, idata from qiskit import QuantumCircuit from qiskit.circuit.library import RealAmplitudes, ZFeatureMap @@ -35,6 +34,7 @@ class TestTorchConnector(TestTorch): def setup_test(self): super().setup_test() + import torch # pylint: disable=attribute-defined-outside-init self._test_data = [ @@ -51,6 +51,7 @@ def _validate_backward_automatically(self, model: TorchConnector) -> None: Args: model: The model to be tested. """ + import torch # test autograd func = TorchConnector._TorchNNFunction.apply # (input, weights, qnn) @@ -74,6 +75,7 @@ def _validate_backward_automatically(self, model: TorchConnector) -> None: self.assertTrue(test) def _validate_forward(self, model: TorchConnector): + import torch for batch_size in [1, 2]: input_data = torch.rand((batch_size, model.neural_network.num_inputs)) @@ -103,6 +105,7 @@ def _validate_forward(self, model: TorchConnector): model(wrong_input) def _validate_backward(self, model: TorchConnector): + import torch for batch_size in [1, 2]: input_data = torch.rand((batch_size, model.neural_network.num_inputs)) @@ -243,6 +246,7 @@ def test_estimator_qnn(self, num_qubits, observables): def _create_convolutional_layer( self, input_channel: int, output_channel: int, num_qubits: int, num_weight: int ): + import torch from qiskit.circuit import Parameter class ConvolutionalLayer(torch.nn.Module): @@ -415,6 +419,8 @@ def test_get_einsum_signature(self): # Test with a convolutional layer based on Sampler QNN and 4-D input # Refer to issue https://github.com/qiskit-community/qiskit-machine-learning/issues/716 + import torch + model = self._create_convolutional_layer(3, 1, 3, 3) input_tensor = torch.rand((2, 3, 6, 6)) input_tensor.requires_grad = True From 16c84546e0bcd83253232839a0030bd2fa388f51 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Mon, 8 Apr 2024 18:49:48 +0100 Subject: [PATCH 16/85] Spelling and type ignore --- test/connectors/test_torch_connector.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/connectors/test_torch_connector.py b/test/connectors/test_torch_connector.py index 3d626762e..b69472dde 100644 --- a/test/connectors/test_torch_connector.py +++ b/test/connectors/test_torch_connector.py @@ -344,15 +344,16 @@ def sampler(self) -> SamplerQNN: output_shape=self.output_channel, ) - def interpret(self, output: Union[List[int], int]) -> Union[int, List[int]]: + def interpret(self, output: int) -> int: """ Interprets the output from the quantum circuit. Args: - output (Union[List[int], int]): Output from the quantum circuit. + output (int): Output from the quantum circuit. Returns: - Union[int, List[int]]: Interpreted output. + int: Remainder of the output divided by the + number of output channels. """ return output % self.output_channel From 00130f239b7fd318afe797f60db3000b116de15a Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Mon, 8 Apr 2024 19:52:24 +0100 Subject: [PATCH 17/85] Spelling and type ignore --- test/connectors/test_torch_connector.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/connectors/test_torch_connector.py b/test/connectors/test_torch_connector.py index b69472dde..a82a91179 100644 --- a/test/connectors/test_torch_connector.py +++ b/test/connectors/test_torch_connector.py @@ -12,7 +12,7 @@ """Test Torch Connector.""" import itertools -from typing import cast, Union, List, Tuple +from typing import cast, Union, List, Tuple, Any from test.connectors.test_torch import TestTorch @@ -344,7 +344,7 @@ def sampler(self) -> SamplerQNN: output_shape=self.output_channel, ) - def interpret(self, output: int) -> int: + def interpret(self, output: Union[List[int], int]) -> Any: """ Interprets the output from the quantum circuit. From e045c1642be428747d7fa7ca3f2b0a5ee01eebfd Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Mon, 8 Apr 2024 20:04:45 +0100 Subject: [PATCH 18/85] Spelling and type ignore --- test/connectors/test_torch_connector.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/connectors/test_torch_connector.py b/test/connectors/test_torch_connector.py index a82a91179..78491e483 100644 --- a/test/connectors/test_torch_connector.py +++ b/test/connectors/test_torch_connector.py @@ -344,15 +344,15 @@ def sampler(self) -> SamplerQNN: output_shape=self.output_channel, ) - def interpret(self, output: Union[List[int], int]) -> Any: + def interpret(self, output: Union[float, int]) -> Any: """ Interprets the output from the quantum circuit. Args: - output (int): Output from the quantum circuit. + output (Union[float, int]): Output from the quantum circuit. Returns: - int: Remainder of the output divided by the + Any: Remainder of the output divided by the number of output channels. """ return output % self.output_channel From 22d94ce347b8fced5d141d6d632e1e0906040a5a Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Wed, 24 Apr 2024 14:26:17 +0100 Subject: [PATCH 19/85] Remove for loop in einsum function and remove Literal arguments (1/2) --- .../connectors/torch_connector.py | 63 +++---------------- 1 file changed, 8 insertions(+), 55 deletions(-) diff --git a/qiskit_machine_learning/connectors/torch_connector.py b/qiskit_machine_learning/connectors/torch_connector.py index 4027fb9f2..537d97d76 100644 --- a/qiskit_machine_learning/connectors/torch_connector.py +++ b/qiskit_machine_learning/connectors/torch_connector.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2021, 2024. +# (C) Copyright IBM 2021, 2023. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -13,7 +13,7 @@ """A connector to use Qiskit (Quantum) Neural Networks as PyTorch modules.""" from __future__ import annotations -from typing import Tuple, Any, cast, Literal +from typing import Tuple, Any, cast import numpy as np @@ -52,53 +52,12 @@ class Module: # type: ignore pass -def _get_einsum_signature(n_dimensions: int, return_type: Literal["input", "weight"]) -> str: - """ - Generate an Einstein summation signature for a given number of dimensions and return type. - - Args: - n_dimensions (int): The number of dimensions for the summation. - return_type (Literal["input", "weight"]): The type of the return signature. - - "input": Return signature includes all input indices except the last one. - - "weight": Return signature includes only the last index as the output. - - Returns: - str: The Einstein summation signature. - - Raises: - RuntimeError: If the number of dimensions exceeds the character limit. - ValueError: If an invalid return type is provided. - """ - trace = "" - char_limit = 26 - for i in range(n_dimensions): - # pylint: disable=wrong-spelling-in-comment - # chr(97) == 'a' - trace += chr(97 + i) - if i >= char_limit: - raise RuntimeError( - f"Cannot define an Einstein summation with more tha {char_limit:d} dimensions." - ) - - if return_type == "input": - signature = f"{trace[:-1]},{trace:s}->{trace[0] + trace[2:]}" - elif return_type == "weight": - signature = f"{trace[:-1]},{trace:s}->{trace[-1]}" - else: - raise ValueError( - f'The only allowed return types are ["input", "weight"], got {return_type:s} instead.' - ) - - return signature - - @_optionals.HAS_TORCH.require_in_instance class TorchConnector(Module): """Connects a Qiskit (Quantum) Neural Network to PyTorch.""" # pylint: disable=abstract-method class _TorchNNFunction(Function): - # pylint: disable=arguments-differ @staticmethod def forward( # type: ignore @@ -228,9 +187,7 @@ def backward(ctx: Any, grad_output: Tensor) -> Tuple: # type: ignore # able to do back-prop in a batched manner. # Pytorch does not support sparse einsum, so we rely on Sparse. # pylint: disable=no-member - n_dimension = max(grad_coo.ndim, input_grad.ndim) - signature = _get_einsum_signature(n_dimension, return_type="input") - input_grad = sparse.einsum(signature, grad_coo, input_grad) + input_grad = sparse.einsum("ij,ijk->ik", grad_coo, input_grad) # return sparse gradients input_grad = torch.sparse_coo_tensor(input_grad.coords, input_grad.data) @@ -248,9 +205,7 @@ def backward(ctx: Any, grad_output: Tensor) -> Tuple: # type: ignore input_grad = torch.as_tensor(input_grad, dtype=torch.float) # same as above - n_dimension = max(grad_output.detach().cpu().ndim, input_grad.ndim) - signature = _get_einsum_signature(n_dimension, return_type="input") - input_grad = torch.einsum(signature, grad_output.detach().cpu(), input_grad) + input_grad = torch.einsum("ij,ijk->ik", grad_output.detach().cpu(), input_grad) # place the resulting tensor to the device where they were stored input_grad = input_grad.to(input_data.device) @@ -271,9 +226,7 @@ def backward(ctx: Any, grad_output: Tensor) -> Tuple: # type: ignore # w.r.t. each parameter k. The weights' dimension is independent of the # batch size. # pylint: disable=no-member - n_dimension = max(grad_coo.ndim, weights_grad.ndim) - signature = _get_einsum_signature(n_dimension, return_type="weight") - weights_grad = sparse.einsum(signature, grad_coo, weights_grad) + weights_grad = sparse.einsum("ij,ijk->k", grad_coo, weights_grad) # return sparse gradients weights_grad = torch.sparse_coo_tensor( @@ -291,9 +244,9 @@ def backward(ctx: Any, grad_output: Tensor) -> Tuple: # type: ignore weights_grad = weights_grad.todense() weights_grad = torch.as_tensor(weights_grad, dtype=torch.float) # same as above - n_dimension = max(grad_output.detach().cpu().ndim, weights_grad.ndim) - signature = _get_einsum_signature(n_dimension, return_type="weight") - weights_grad = torch.einsum(signature, grad_output.detach().cpu(), weights_grad) + weights_grad = torch.einsum( + "ij,ijk->k", grad_output.detach().cpu(), weights_grad + ) # place the resulting tensor to the device where they were stored weights_grad = weights_grad.to(weights.device) From 95dd9dfb14ce666580322e7732add2b11287a1a5 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Wed, 24 Apr 2024 14:28:04 +0100 Subject: [PATCH 20/85] Remove for loop in einsum function and remove Literal arguments (1/2) --- .../connectors/torch_connector.py | 61 ++++++++++++++++--- 1 file changed, 53 insertions(+), 8 deletions(-) diff --git a/qiskit_machine_learning/connectors/torch_connector.py b/qiskit_machine_learning/connectors/torch_connector.py index 537d97d76..106a19637 100644 --- a/qiskit_machine_learning/connectors/torch_connector.py +++ b/qiskit_machine_learning/connectors/torch_connector.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2021, 2023. +# (C) Copyright IBM 2021, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -15,6 +15,7 @@ from typing import Tuple, Any, cast +from string import ascii_lowercase import numpy as np import qiskit_machine_learning.optionals as _optionals @@ -52,12 +53,50 @@ class Module: # type: ignore pass +CHAR_LIMIT = 26 + + +def _get_einsum_signature(n_dimensions: int, for_weights: bool = False) -> str: + """ + Generate an Einstein summation signature for a given number of dimensions and return type. + + Args: + n_dimensions (int): The number of dimensions for the summation. + for_weights (bool): If True, the return signature includes only the + last index as the output. If False, the return signature includes + all input indices except the last one. Defaults to False. + + + Returns: + str: The Einstein summation signature. + + Raises: + RuntimeError: If the number of dimensions exceeds the character limit. + """ + if n_dimensions >= CHAR_LIMIT: + raise RuntimeError( + f"Cannot define an Einstein summation with more tha {CHAR_LIMIT:d} dimensions." + ) + + trace = ascii_lowercase[:n_dimensions] + + if for_weights: + return f"{trace[:-1]},{trace:s}->{trace[-1]}" + + return f"{trace[:-1]},{trace:s}->{trace[0] + trace[2:]}" + + @_optionals.HAS_TORCH.require_in_instance class TorchConnector(Module): - """Connects a Qiskit (Quantum) Neural Network to PyTorch.""" + """Connects a Qiskit (Quantum) Neural Network to PyTorch. + The dataset dimensionality is limited to 25. Datasets of 26 dimensions or higher + raise a `RuntimeError`. + + """ # pylint: disable=abstract-method class _TorchNNFunction(Function): + # pylint: disable=arguments-differ @staticmethod def forward( # type: ignore @@ -187,7 +226,9 @@ def backward(ctx: Any, grad_output: Tensor) -> Tuple: # type: ignore # able to do back-prop in a batched manner. # Pytorch does not support sparse einsum, so we rely on Sparse. # pylint: disable=no-member - input_grad = sparse.einsum("ij,ijk->ik", grad_coo, input_grad) + n_dimension = max(grad_coo.ndim, input_grad.ndim) + signature = _get_einsum_signature(n_dimension) + input_grad = sparse.einsum(signature, grad_coo, input_grad) # return sparse gradients input_grad = torch.sparse_coo_tensor(input_grad.coords, input_grad.data) @@ -205,7 +246,9 @@ def backward(ctx: Any, grad_output: Tensor) -> Tuple: # type: ignore input_grad = torch.as_tensor(input_grad, dtype=torch.float) # same as above - input_grad = torch.einsum("ij,ijk->ik", grad_output.detach().cpu(), input_grad) + n_dimension = max(grad_output.detach().cpu().ndim, input_grad.ndim) + signature = _get_einsum_signature(n_dimension) + input_grad = torch.einsum(signature, grad_output.detach().cpu(), input_grad) # place the resulting tensor to the device where they were stored input_grad = input_grad.to(input_data.device) @@ -226,7 +269,9 @@ def backward(ctx: Any, grad_output: Tensor) -> Tuple: # type: ignore # w.r.t. each parameter k. The weights' dimension is independent of the # batch size. # pylint: disable=no-member - weights_grad = sparse.einsum("ij,ijk->k", grad_coo, weights_grad) + n_dimension = max(grad_coo.ndim, weights_grad.ndim) + signature = _get_einsum_signature(n_dimension, for_weights=True) + weights_grad = sparse.einsum(signature, grad_coo, weights_grad) # return sparse gradients weights_grad = torch.sparse_coo_tensor( @@ -244,9 +289,9 @@ def backward(ctx: Any, grad_output: Tensor) -> Tuple: # type: ignore weights_grad = weights_grad.todense() weights_grad = torch.as_tensor(weights_grad, dtype=torch.float) # same as above - weights_grad = torch.einsum( - "ij,ijk->k", grad_output.detach().cpu(), weights_grad - ) + n_dimension = max(grad_output.detach().cpu().ndim, weights_grad.ndim) + signature = _get_einsum_signature(n_dimension, for_weights=True) + weights_grad = torch.einsum(signature, grad_output.detach().cpu(), weights_grad) # place the resulting tensor to the device where they were stored weights_grad = weights_grad.to(weights.device) From 4cbf0c3b195daf4def50975ac5e598e8a5a30882 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Wed, 24 Apr 2024 14:29:10 +0100 Subject: [PATCH 21/85] Remove for loop in einsum function and remove Literal arguments (2/2) --- test/connectors/test_torch_connector.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/test/connectors/test_torch_connector.py b/test/connectors/test_torch_connector.py index 78491e483..f0f67d3d1 100644 --- a/test/connectors/test_torch_connector.py +++ b/test/connectors/test_torch_connector.py @@ -389,12 +389,7 @@ def test_get_einsum_signature(self): This function is tested with valid inputs (e.g., `n_dimensions` = 3) and expected outputs. It also covers error handling scenarios for invalid input arguments. - Valid Input and Output Tests: - - Tests with `n_dimensions` equal to 3 and different return types ('input' and 'weight') - to ensure correct signature generation for Einstein summation notation. - Error Handling Tests: - - Raises a ValueError when an invalid return_type is provided. - Raises a RuntimeError when the character limit for signature generation is exceeded. Additional Test Scenario: @@ -406,17 +401,9 @@ def test_get_einsum_signature(self): pylint disable=SPELLING """ - # Test valid inputs and outputs - self.assertEqual(_get_einsum_signature(3, "input"), "ab,abc->ac") - self.assertEqual(_get_einsum_signature(3, "weight"), "ab,abc->c") - - # Test raises for invalid return_type - with self.assertRaises(ValueError): - _get_einsum_signature(3, "invalid_type") - # Test raises for exceeding character limit with self.assertRaises(RuntimeError): - _get_einsum_signature(30, "input") + _get_einsum_signature(30) # Test with a convolutional layer based on Sampler QNN and 4-D input # Refer to issue https://github.com/qiskit-community/qiskit-machine-learning/issues/716 From c4dca19a992f5170ae8367e9a2853edb97edf1c0 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Tue, 30 Apr 2024 13:29:11 +0200 Subject: [PATCH 22/85] Update RuntimeError msg --- qiskit_machine_learning/connectors/torch_connector.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit_machine_learning/connectors/torch_connector.py b/qiskit_machine_learning/connectors/torch_connector.py index 106a19637..b0fed4c5a 100644 --- a/qiskit_machine_learning/connectors/torch_connector.py +++ b/qiskit_machine_learning/connectors/torch_connector.py @@ -73,9 +73,9 @@ def _get_einsum_signature(n_dimensions: int, for_weights: bool = False) -> str: Raises: RuntimeError: If the number of dimensions exceeds the character limit. """ - if n_dimensions >= CHAR_LIMIT: + if n_dimensions > CHAR_LIMIT - 1: raise RuntimeError( - f"Cannot define an Einstein summation with more tha {CHAR_LIMIT:d} dimensions." + f"Cannot define an Einstein summation with more than {CHAR_LIMIT - 1:d} dimensions, got {n_dimensions:d}." ) trace = ascii_lowercase[:n_dimensions] From d5ed96b282e349720de6a7fe17dfa6c97605ddb0 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Tue, 30 Apr 2024 13:32:02 +0200 Subject: [PATCH 23/85] Update RuntimeError msg - line too long --- qiskit_machine_learning/connectors/torch_connector.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qiskit_machine_learning/connectors/torch_connector.py b/qiskit_machine_learning/connectors/torch_connector.py index b0fed4c5a..0d56db8d8 100644 --- a/qiskit_machine_learning/connectors/torch_connector.py +++ b/qiskit_machine_learning/connectors/torch_connector.py @@ -75,7 +75,8 @@ def _get_einsum_signature(n_dimensions: int, for_weights: bool = False) -> str: """ if n_dimensions > CHAR_LIMIT - 1: raise RuntimeError( - f"Cannot define an Einstein summation with more than {CHAR_LIMIT - 1:d} dimensions, got {n_dimensions:d}." + f"Cannot define an Einstein summation with more than {CHAR_LIMIT - 1:d} dimensions, " + f"got {n_dimensions:d}." ) trace = ascii_lowercase[:n_dimensions] From d6f3d47f5e482e6f7fc27d74397803ffa982fefa Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Thu, 2 May 2024 12:34:47 +0100 Subject: [PATCH 24/85] Trigger CI --- qiskit_machine_learning/connectors/torch_connector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_machine_learning/connectors/torch_connector.py b/qiskit_machine_learning/connectors/torch_connector.py index 0d56db8d8..5bfd2b2f9 100644 --- a/qiskit_machine_learning/connectors/torch_connector.py +++ b/qiskit_machine_learning/connectors/torch_connector.py @@ -25,7 +25,7 @@ if _optionals.HAS_TORCH: import torch - # imports for inheritance and type hints + # Imports for inheritance and type hints from torch import Tensor from torch.autograd import Function from torch.nn import Module From 3846d4d3a4a825d7325aab9923d0ed58c9d3db61 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Thu, 11 Jul 2024 15:42:52 +0200 Subject: [PATCH 25/85] Merge algos, globals.random to fix --- README.md | 4 +- qiskit_machine_learning/algorithm_job.py | 45 + qiskit_machine_learning/algorithm_result.py | 65 ++ .../classifiers/neural_network_classifier.py | 2 +- .../algorithms/classifiers/pegasos_qsvc.py | 4 +- .../algorithms/classifiers/qsvc.py | 3 +- .../algorithms/classifiers/vqc.py | 2 +- .../algorithms/objective_functions.py | 6 +- .../regressors/neural_network_regressor.py | 2 +- .../algorithms/regressors/qsvr.py | 6 +- .../algorithms/regressors/vqr.py | 2 +- .../algorithms/trainable_model.py | 10 +- .../circuit/library/qnn_circuit.py | 2 +- qiskit_machine_learning/datasets/ad_hoc.py | 8 +- qiskit_machine_learning/exceptions.py | 6 + qiskit_machine_learning/gradients/__init__.py | 109 +++ .../gradients/base/__init__.py | 11 + .../gradients/base/base_estimator_gradient.py | 363 +++++++++ .../gradients/base/base_qgt.py | 388 +++++++++ .../gradients/base/base_sampler_gradient.py | 300 +++++++ .../base/estimator_gradient_result.py | 35 + .../gradients/base/qgt_result.py | 39 + .../gradients/base/sampler_gradient_result.py | 33 + .../gradients/lin_comb/__init__.py | 11 + .../lin_comb/lin_comb_estimator_gradient.py | 194 +++++ .../gradients/lin_comb/lin_comb_qgt.py | 258 ++++++ .../lin_comb/lin_comb_sampler_gradient.py | 148 ++++ .../gradients/param_shift/__init__.py | 11 + .../param_shift_estimator_gradient.py | 122 +++ .../param_shift_sampler_gradient.py | 117 +++ qiskit_machine_learning/gradients/qfi.py | 171 ++++ .../gradients/qfi_result.py | 35 + .../gradients/spsa/__init__.py | 11 + .../gradients/spsa/spsa_estimator_gradient.py | 134 +++ .../gradients/spsa/spsa_sampler_gradient.py | 136 +++ qiskit_machine_learning/gradients/utils.py | 375 +++++++++ .../algorithms/quantum_kernel_trainer.py | 13 +- .../kernels/fidelity_quantum_kernel.py | 2 +- .../kernels/fidelity_statevector_kernel.py | 4 +- .../trainable_fidelity_quantum_kernel.py | 2 +- .../neural_networks/effective_dimension.py | 10 +- .../neural_networks/estimator_qnn.py | 6 +- .../neural_networks/sampler_qnn.py | 6 +- .../optimizers/__init__.py | 182 +++++ .../optimizers/adam_amsgrad.py | 252 ++++++ qiskit_machine_learning/optimizers/aqgd.py | 374 +++++++++ qiskit_machine_learning/optimizers/bobyqa.py | 84 ++ qiskit_machine_learning/optimizers/cg.py | 70 ++ qiskit_machine_learning/optimizers/cobyla.py | 59 ++ .../optimizers/gradient_descent.py | 398 +++++++++ qiskit_machine_learning/optimizers/gsls.py | 375 +++++++++ qiskit_machine_learning/optimizers/imfil.py | 86 ++ .../optimizers/l_bfgs_b.py | 88 ++ .../optimizers/nelder_mead.py | 73 ++ qiskit_machine_learning/optimizers/nft.py | 169 ++++ .../optimizers/nlopts/__init__.py | 13 + .../optimizers/nlopts/crs.py | 35 + .../optimizers/nlopts/direct_l.py | 34 + .../optimizers/nlopts/direct_l_rand.py | 32 + .../optimizers/nlopts/esch.py | 33 + .../optimizers/nlopts/isres.py | 39 + .../optimizers/nlopts/nloptimizer.py | 131 +++ .../optimizers/optimizer.py | 389 +++++++++ .../optimizers/optimizer_utils/__init__.py | 27 + .../optimizer_utils/learning_rate.py | 88 ++ qiskit_machine_learning/optimizers/p_bfgs.py | 183 +++++ qiskit_machine_learning/optimizers/powell.py | 64 ++ qiskit_machine_learning/optimizers/qnspsa.py | 273 +++++++ .../optimizers/scipy_optimizer.py | 177 ++++ qiskit_machine_learning/optimizers/slsqp.py | 73 ++ qiskit_machine_learning/optimizers/snobfit.py | 129 +++ qiskit_machine_learning/optimizers/spsa.py | 771 ++++++++++++++++++ .../optimizers/steppable_optimizer.py | 303 +++++++ qiskit_machine_learning/optimizers/tnc.py | 83 ++ qiskit_machine_learning/optimizers/umda.py | 348 ++++++++ .../state_fidelities/__init__.py | 44 + .../state_fidelities/base_state_fidelity.py | 315 +++++++ .../state_fidelities/compute_uncompute.py | 258 ++++++ .../state_fidelities/state_fidelity_result.py | 37 + qiskit_machine_learning/utils/__init__.py | 7 +- .../utils/adjust_num_qubits.py | 2 +- .../utils/algorithm_globals.py | 131 +++ qiskit_machine_learning/utils/optionals.py | 27 + qiskit_machine_learning/utils/set_batching.py | 27 + .../utils/validate_bounds.py | 44 + .../utils/validate_initial_point.py | 65 ++ qiskit_machine_learning/utils/validation.py | 138 ++++ .../variational_algorithm.py | 137 ++++ requirements.txt | 1 - ...st_fidelity_quantum_kernel_pegasos_qsvc.py | 2 +- .../test_fidelity_quantum_kernel_qsvc.py | 2 +- .../test_neural_network_classifier.py | 14 +- .../classifiers/test_pegasos_qsvc.py | 2 +- test/algorithms/classifiers/test_qsvc.py | 2 +- test/algorithms/classifiers/test_vqc.py | 13 +- test/algorithms/inference/test_qbayesian.py | 2 +- .../test_fidelity_quantum_kernel_qsvr.py | 2 +- .../test_neural_network_regressor.py | 4 +- test/algorithms/regressors/test_qsvr.py | 2 +- test/algorithms/regressors/test_vqr.py | 4 +- .../library/test_raw_feature_vector.py | 6 +- test/connectors/test_torch.py | 2 +- test/datasets/test_ad_hoc_data.py | 2 +- .../test_fidelity_qkernel_trainer.py | 2 +- test/kernels/test_fidelity_qkernel.py | 8 +- .../test_fidelity_statevector_kernel.py | 8 +- .../test_effective_dimension.py | 22 +- test/neural_networks/test_sampler_qnn.py | 2 +- 108 files changed, 9879 insertions(+), 101 deletions(-) create mode 100644 qiskit_machine_learning/algorithm_job.py create mode 100644 qiskit_machine_learning/algorithm_result.py create mode 100644 qiskit_machine_learning/gradients/__init__.py create mode 100644 qiskit_machine_learning/gradients/base/__init__.py create mode 100644 qiskit_machine_learning/gradients/base/base_estimator_gradient.py create mode 100644 qiskit_machine_learning/gradients/base/base_qgt.py create mode 100644 qiskit_machine_learning/gradients/base/base_sampler_gradient.py create mode 100644 qiskit_machine_learning/gradients/base/estimator_gradient_result.py create mode 100644 qiskit_machine_learning/gradients/base/qgt_result.py create mode 100644 qiskit_machine_learning/gradients/base/sampler_gradient_result.py create mode 100644 qiskit_machine_learning/gradients/lin_comb/__init__.py create mode 100644 qiskit_machine_learning/gradients/lin_comb/lin_comb_estimator_gradient.py create mode 100644 qiskit_machine_learning/gradients/lin_comb/lin_comb_qgt.py create mode 100644 qiskit_machine_learning/gradients/lin_comb/lin_comb_sampler_gradient.py create mode 100644 qiskit_machine_learning/gradients/param_shift/__init__.py create mode 100644 qiskit_machine_learning/gradients/param_shift/param_shift_estimator_gradient.py create mode 100644 qiskit_machine_learning/gradients/param_shift/param_shift_sampler_gradient.py create mode 100644 qiskit_machine_learning/gradients/qfi.py create mode 100644 qiskit_machine_learning/gradients/qfi_result.py create mode 100644 qiskit_machine_learning/gradients/spsa/__init__.py create mode 100644 qiskit_machine_learning/gradients/spsa/spsa_estimator_gradient.py create mode 100644 qiskit_machine_learning/gradients/spsa/spsa_sampler_gradient.py create mode 100644 qiskit_machine_learning/gradients/utils.py create mode 100644 qiskit_machine_learning/optimizers/__init__.py create mode 100644 qiskit_machine_learning/optimizers/adam_amsgrad.py create mode 100644 qiskit_machine_learning/optimizers/aqgd.py create mode 100644 qiskit_machine_learning/optimizers/bobyqa.py create mode 100644 qiskit_machine_learning/optimizers/cg.py create mode 100644 qiskit_machine_learning/optimizers/cobyla.py create mode 100644 qiskit_machine_learning/optimizers/gradient_descent.py create mode 100644 qiskit_machine_learning/optimizers/gsls.py create mode 100644 qiskit_machine_learning/optimizers/imfil.py create mode 100644 qiskit_machine_learning/optimizers/l_bfgs_b.py create mode 100644 qiskit_machine_learning/optimizers/nelder_mead.py create mode 100644 qiskit_machine_learning/optimizers/nft.py create mode 100644 qiskit_machine_learning/optimizers/nlopts/__init__.py create mode 100644 qiskit_machine_learning/optimizers/nlopts/crs.py create mode 100644 qiskit_machine_learning/optimizers/nlopts/direct_l.py create mode 100644 qiskit_machine_learning/optimizers/nlopts/direct_l_rand.py create mode 100644 qiskit_machine_learning/optimizers/nlopts/esch.py create mode 100644 qiskit_machine_learning/optimizers/nlopts/isres.py create mode 100644 qiskit_machine_learning/optimizers/nlopts/nloptimizer.py create mode 100644 qiskit_machine_learning/optimizers/optimizer.py create mode 100644 qiskit_machine_learning/optimizers/optimizer_utils/__init__.py create mode 100644 qiskit_machine_learning/optimizers/optimizer_utils/learning_rate.py create mode 100644 qiskit_machine_learning/optimizers/p_bfgs.py create mode 100644 qiskit_machine_learning/optimizers/powell.py create mode 100644 qiskit_machine_learning/optimizers/qnspsa.py create mode 100644 qiskit_machine_learning/optimizers/scipy_optimizer.py create mode 100644 qiskit_machine_learning/optimizers/slsqp.py create mode 100644 qiskit_machine_learning/optimizers/snobfit.py create mode 100644 qiskit_machine_learning/optimizers/spsa.py create mode 100644 qiskit_machine_learning/optimizers/steppable_optimizer.py create mode 100644 qiskit_machine_learning/optimizers/tnc.py create mode 100644 qiskit_machine_learning/optimizers/umda.py create mode 100644 qiskit_machine_learning/state_fidelities/__init__.py create mode 100644 qiskit_machine_learning/state_fidelities/base_state_fidelity.py create mode 100644 qiskit_machine_learning/state_fidelities/compute_uncompute.py create mode 100644 qiskit_machine_learning/state_fidelities/state_fidelity_result.py create mode 100644 qiskit_machine_learning/utils/algorithm_globals.py create mode 100644 qiskit_machine_learning/utils/optionals.py create mode 100644 qiskit_machine_learning/utils/set_batching.py create mode 100644 qiskit_machine_learning/utils/validate_bounds.py create mode 100644 qiskit_machine_learning/utils/validate_initial_point.py create mode 100644 qiskit_machine_learning/utils/validation.py create mode 100644 qiskit_machine_learning/variational_algorithm.py diff --git a/README.md b/README.md index e8d773477..3973737ad 100644 --- a/README.md +++ b/README.md @@ -112,8 +112,8 @@ be classified. ```python from qiskit.circuit.library import TwoLocal, ZZFeatureMap -from qiskit_algorithms.optimizers import COBYLA -from qiskit_algorithms.utils import algorithm_globals +from qiskit_machine_learning.optimizers import COBYLA +from qiskit_machine_learning.utils import algorithm_globals from qiskit_machine_learning.algorithms import VQC from qiskit_machine_learning.datasets import ad_hoc_data diff --git a/qiskit_machine_learning/algorithm_job.py b/qiskit_machine_learning/algorithm_job.py new file mode 100644 index 000000000..b12155746 --- /dev/null +++ b/qiskit_machine_learning/algorithm_job.py @@ -0,0 +1,45 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2024. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +AlgorithmJob class +""" +from qiskit.primitives.primitive_job import PrimitiveJob + + +class AlgorithmJob(PrimitiveJob): + """ + This class is introduced for typing purposes and provides no + additional function beyond that inherited from its parents. + + Update: :meth:`AlgorithmJob.submit()` method added. See its + documentation for more info. + """ + + def submit(self) -> None: + """ + Submit the job for execution. + + For V1 primitives, Qiskit ``PrimitiveJob`` subclassed JobV1 and defined ``submit()``. + ``PrimitiveJob`` was updated for V2 primitives, no longer subclasses ``JobV1``, and + now has a private ``_submit()`` method, with ``submit()`` being deprecated as of + Qiskit version 0.46. This maintains the ``submit()`` for ``AlgorithmJob`` here as + it's called in many places for such a job. An alternative could be to make + 0.46 the required minimum version and alter all algorithm's call sites to use + ``_submit()`` and make this an empty class again as it once was. For now this + way maintains compatibility with the current min version of 0.44. + """ + # TODO: Considering changing this in the future - see above docstring. + try: + super()._submit() + except AttributeError: + super().submit() diff --git a/qiskit_machine_learning/algorithm_result.py b/qiskit_machine_learning/algorithm_result.py new file mode 100644 index 000000000..695bab749 --- /dev/null +++ b/qiskit_machine_learning/algorithm_result.py @@ -0,0 +1,65 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2020, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +This module implements the abstract base class for algorithm results. +""" + +from abc import ABC +import inspect +import pprint + + +class AlgorithmResult(ABC): + """Abstract Base Class for algorithm results.""" + + def __str__(self) -> str: + result = {} + for name, value in inspect.getmembers(self): + if ( + not name.startswith("_") + and not inspect.ismethod(value) + and not inspect.isfunction(value) + and hasattr(self, name) + ): + + result[name] = value + + return pprint.pformat(result, indent=4) + + def combine(self, result: "AlgorithmResult") -> None: + """ + Any property from the argument that exists in the receiver is + updated. + Args: + result: Argument result with properties to be set. + Raises: + TypeError: Argument is None + """ + if result is None: + raise TypeError("Argument result expected.") + if result == self: + return + + # find any result public property that exists in the receiver + for name, value in inspect.getmembers(result): + if ( + not name.startswith("_") + and not inspect.ismethod(value) + and not inspect.isfunction(value) + and hasattr(self, name) + ): + try: + setattr(self, name, value) + except AttributeError: + # some attributes may be read only + pass diff --git a/qiskit_machine_learning/algorithms/classifiers/neural_network_classifier.py b/qiskit_machine_learning/algorithms/classifiers/neural_network_classifier.py index 096b64301..cc0edd7fa 100644 --- a/qiskit_machine_learning/algorithms/classifiers/neural_network_classifier.py +++ b/qiskit_machine_learning/algorithms/classifiers/neural_network_classifier.py @@ -17,7 +17,6 @@ import numpy as np import scipy.sparse -from qiskit_algorithms.optimizers import Optimizer, OptimizerResult, Minimizer from scipy.sparse import spmatrix from sklearn.base import ClassifierMixin from sklearn.exceptions import NotFittedError @@ -31,6 +30,7 @@ ObjectiveFunction, ) from ..trainable_model import TrainableModel +from ...optimizers import Optimizer, OptimizerResult, Minimizer from ...exceptions import QiskitMachineLearningError from ...neural_networks import NeuralNetwork from ...utils.loss_functions import Loss diff --git a/qiskit_machine_learning/algorithms/classifiers/pegasos_qsvc.py b/qiskit_machine_learning/algorithms/classifiers/pegasos_qsvc.py index 1b496d6f5..479be40b8 100644 --- a/qiskit_machine_learning/algorithms/classifiers/pegasos_qsvc.py +++ b/qiskit_machine_learning/algorithms/classifiers/pegasos_qsvc.py @@ -18,12 +18,12 @@ from typing import Dict import numpy as np -from qiskit_algorithms.utils import algorithm_globals from sklearn.base import ClassifierMixin from ...algorithms.serializable_model import SerializableModelMixin from ...exceptions import QiskitMachineLearningError from ...kernels import BaseKernel, FidelityQuantumKernel +from ...utils import algorithm_globals logger = logging.getLogger(__name__) @@ -188,7 +188,7 @@ def fit( # training loop for step in range(1, self._num_steps + 1): # for every step, a random index (determining a random datum) is fixed - i = algorithm_globals.random.integers(0, len(y)) + i = algorithm_globals.random().integers(0, len(y)) value = self._compute_weighted_kernel_sum(i, X, training=True) diff --git a/qiskit_machine_learning/algorithms/classifiers/qsvc.py b/qiskit_machine_learning/algorithms/classifiers/qsvc.py index 30cf30d29..2985176da 100644 --- a/qiskit_machine_learning/algorithms/classifiers/qsvc.py +++ b/qiskit_machine_learning/algorithms/classifiers/qsvc.py @@ -15,13 +15,14 @@ import warnings from typing import Optional -from qiskit_algorithms.utils import algorithm_globals from sklearn.svm import SVC from qiskit_machine_learning.algorithms.serializable_model import SerializableModelMixin from qiskit_machine_learning.exceptions import QiskitMachineLearningWarning from qiskit_machine_learning.kernels import BaseKernel, FidelityQuantumKernel +from ...utils import algorithm_globals + class QSVC(SVC, SerializableModelMixin): r"""Quantum Support Vector Classifier that extends the scikit-learn diff --git a/qiskit_machine_learning/algorithms/classifiers/vqc.py b/qiskit_machine_learning/algorithms/classifiers/vqc.py index d0ef96341..257464f8a 100644 --- a/qiskit_machine_learning/algorithms/classifiers/vqc.py +++ b/qiskit_machine_learning/algorithms/classifiers/vqc.py @@ -18,9 +18,9 @@ from qiskit import QuantumCircuit from qiskit.primitives import BaseSampler -from qiskit_algorithms.optimizers import Optimizer, OptimizerResult, Minimizer from ...neural_networks import SamplerQNN +from ...optimizers import Optimizer, OptimizerResult, Minimizer from ...utils import derive_num_qubits_feature_map_ansatz from ...utils.loss_functions import Loss diff --git a/qiskit_machine_learning/algorithms/objective_functions.py b/qiskit_machine_learning/algorithms/objective_functions.py index 252b53314..e8657731a 100644 --- a/qiskit_machine_learning/algorithms/objective_functions.py +++ b/qiskit_machine_learning/algorithms/objective_functions.py @@ -17,9 +17,9 @@ import numpy as np -import qiskit_machine_learning.optionals as _optionals -from qiskit_machine_learning.neural_networks import NeuralNetwork -from qiskit_machine_learning.utils.loss_functions import Loss +from .. import optionals as _optionals +from ..neural_networks import NeuralNetwork +from ..utils.loss_functions import Loss if _optionals.HAS_SPARSE: # pylint: disable=import-error diff --git a/qiskit_machine_learning/algorithms/regressors/neural_network_regressor.py b/qiskit_machine_learning/algorithms/regressors/neural_network_regressor.py index 428c0ed5f..667835e2b 100644 --- a/qiskit_machine_learning/algorithms/regressors/neural_network_regressor.py +++ b/qiskit_machine_learning/algorithms/regressors/neural_network_regressor.py @@ -14,7 +14,6 @@ from typing import Optional import numpy as np -from qiskit_algorithms.optimizers import OptimizerResult from sklearn.base import RegressorMixin from ..objective_functions import ( @@ -23,6 +22,7 @@ ObjectiveFunction, ) from ..trainable_model import TrainableModel +from ...optimizers import OptimizerResult class NeuralNetworkRegressor(TrainableModel, RegressorMixin): diff --git a/qiskit_machine_learning/algorithms/regressors/qsvr.py b/qiskit_machine_learning/algorithms/regressors/qsvr.py index ebb24b832..e5ed59090 100644 --- a/qiskit_machine_learning/algorithms/regressors/qsvr.py +++ b/qiskit_machine_learning/algorithms/regressors/qsvr.py @@ -17,9 +17,9 @@ from sklearn.svm import SVR -from qiskit_machine_learning.algorithms.serializable_model import SerializableModelMixin -from qiskit_machine_learning.exceptions import QiskitMachineLearningWarning -from qiskit_machine_learning.kernels import BaseKernel, FidelityQuantumKernel +from ...algorithms.serializable_model import SerializableModelMixin +from ...exceptions import QiskitMachineLearningWarning +from ...kernels import BaseKernel, FidelityQuantumKernel class QSVR(SVR, SerializableModelMixin): diff --git a/qiskit_machine_learning/algorithms/regressors/vqr.py b/qiskit_machine_learning/algorithms/regressors/vqr.py index b33d2930d..73fa9c98e 100644 --- a/qiskit_machine_learning/algorithms/regressors/vqr.py +++ b/qiskit_machine_learning/algorithms/regressors/vqr.py @@ -18,10 +18,10 @@ from qiskit import QuantumCircuit from qiskit.primitives import BaseEstimator from qiskit.quantum_info.operators.base_operator import BaseOperator -from qiskit_algorithms.optimizers import Optimizer, Minimizer from .neural_network_regressor import NeuralNetworkRegressor from ...neural_networks import EstimatorQNN +from ...optimizers import Optimizer, Minimizer from ...utils import derive_num_qubits_feature_map_ansatz from ...utils.loss_functions import Loss diff --git a/qiskit_machine_learning/algorithms/trainable_model.py b/qiskit_machine_learning/algorithms/trainable_model.py index d74eea8d9..4c2ebcf16 100644 --- a/qiskit_machine_learning/algorithms/trainable_model.py +++ b/qiskit_machine_learning/algorithms/trainable_model.py @@ -16,12 +16,12 @@ from typing import Callable import numpy as np -from qiskit_algorithms.optimizers import Optimizer, SLSQP, OptimizerResult, Minimizer -from qiskit_algorithms.utils import algorithm_globals +from ..optimizers import Optimizer, SLSQP, OptimizerResult, Minimizer +from ..utils import algorithm_globals from qiskit_machine_learning import QiskitMachineLearningError -from qiskit_machine_learning.neural_networks import NeuralNetwork -from qiskit_machine_learning.utils.loss_functions import ( +from ..neural_networks import NeuralNetwork +from ..utils.loss_functions import ( Loss, L1Loss, L2Loss, @@ -247,7 +247,7 @@ def _choose_initial_point(self) -> np.ndarray: if self._warm_start and self._fit_result is not None: self._initial_point = self._fit_result.x elif self._initial_point is None: - self._initial_point = algorithm_globals.random.random(self._neural_network.num_weights) + self._initial_point = algorithm_globals.random().random(self._neural_network.num_weights) return self._initial_point def _get_objective( diff --git a/qiskit_machine_learning/circuit/library/qnn_circuit.py b/qiskit_machine_learning/circuit/library/qnn_circuit.py index d3a99c2fa..3f4f3e01e 100644 --- a/qiskit_machine_learning/circuit/library/qnn_circuit.py +++ b/qiskit_machine_learning/circuit/library/qnn_circuit.py @@ -16,7 +16,7 @@ from qiskit.circuit import QuantumRegister, QuantumCircuit from qiskit.circuit.parametertable import ParameterView from qiskit.circuit.library import BlueprintCircuit -from qiskit_machine_learning.utils import derive_num_qubits_feature_map_ansatz +from ...utils import derive_num_qubits_feature_map_ansatz from qiskit_machine_learning import QiskitMachineLearningError diff --git a/qiskit_machine_learning/datasets/ad_hoc.py b/qiskit_machine_learning/datasets/ad_hoc.py index a5a39accc..d64eae298 100644 --- a/qiskit_machine_learning/datasets/ad_hoc.py +++ b/qiskit_machine_learning/datasets/ad_hoc.py @@ -21,7 +21,7 @@ import numpy as np from qiskit.utils import optionals -from qiskit_algorithms.utils import algorithm_globals +from ..utils import algorithm_globals from sklearn import preprocessing @@ -127,9 +127,9 @@ def ad_hoc_data( # Generate a random unitary operator by collecting eigenvectors of a # random hermitian operator - basis = algorithm_globals.random.random( + basis = algorithm_globals.random().random( (2**n, 2**n) - ) + 1j * algorithm_globals.random.random((2**n, 2**n)) + ) + 1j * algorithm_globals.random().random((2**n, 2**n)) basis = np.array(basis).conj().T @ np.array(basis) eigvals, eigvecs = np.linalg.eig(basis) idx = eigvals.argsort()[::-1] @@ -204,7 +204,7 @@ def _sample_ad_hoc_data(sample_total, xvals, num_samples, n): for i, sample_list in enumerate([sample_a, sample_b]): label = 1 if i == 0 else -1 while len(sample_list) < num_samples: - draws = tuple(algorithm_globals.random.choice(count) for i in range(n)) + draws = tuple(algorithm_globals.random().choice(count) for i in range(n)) if sample_total[draws] == label: sample_list.append([xvals[d] for d in draws]) diff --git a/qiskit_machine_learning/exceptions.py b/qiskit_machine_learning/exceptions.py index 65f687f15..1b8804d76 100644 --- a/qiskit_machine_learning/exceptions.py +++ b/qiskit_machine_learning/exceptions.py @@ -32,3 +32,9 @@ def __init__(self, *message): def __str__(self): """Return the message.""" return repr(self.message) + + +class AlgorithmError(QiskitError): + """For Algorithm specific errors.""" + + pass diff --git a/qiskit_machine_learning/gradients/__init__.py b/qiskit_machine_learning/gradients/__init__.py new file mode 100644 index 000000000..15f4a65a7 --- /dev/null +++ b/qiskit_machine_learning/gradients/__init__.py @@ -0,0 +1,109 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2023 +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +Gradients (:mod:`qiskit_algorithms.gradients`) +============================================== +Algorithms to calculate the gradient of a quantum circuit. + +.. currentmodule:: qiskit_algorithms.gradients + +Base Classes +------------ + +.. autosummary:: + :toctree: ../stubs/ + :nosignatures: + + BaseEstimatorGradient + BaseQGT + BaseSamplerGradient + EstimatorGradientResult + SamplerGradientResult + QGTResult + +Linear Combination of Unitaries +------------------------------- + +.. autosummary:: + :toctree: ../stubs/ + :nosignatures: + + LinCombEstimatorGradient + LinCombSamplerGradient + LinCombQGT + +Parameter Shift Rules +--------------------- + +.. autosummary:: + :toctree: ../stubs/ + :nosignatures: + + ParamShiftEstimatorGradient + ParamShiftSamplerGradient + +Quantum Fisher Information +-------------------------- + +.. autosummary:: + :toctree: ../stubs/ + :nosignatures: + + QFIResult + QFI + +Simultaneous Perturbation Stochastic Approximation +-------------------------------------------------- + +.. autosummary:: + :toctree: ../stubs/ + :nosignatures: + + SPSAEstimatorGradient + SPSASamplerGradient +""" + +from .base.base_estimator_gradient import BaseEstimatorGradient +from .base.base_qgt import BaseQGT +from .base.base_sampler_gradient import BaseSamplerGradient +from .base.estimator_gradient_result import EstimatorGradientResult +from .lin_comb.lin_comb_estimator_gradient import DerivativeType, LinCombEstimatorGradient +from .lin_comb.lin_comb_qgt import LinCombQGT +from .lin_comb.lin_comb_sampler_gradient import LinCombSamplerGradient +from .param_shift.param_shift_estimator_gradient import ParamShiftEstimatorGradient +from .param_shift.param_shift_sampler_gradient import ParamShiftSamplerGradient +from .qfi import QFI +from .qfi_result import QFIResult +from .base.qgt_result import QGTResult +from .base.sampler_gradient_result import SamplerGradientResult +from .spsa.spsa_estimator_gradient import SPSAEstimatorGradient +from .spsa.spsa_sampler_gradient import SPSASamplerGradient + +__all__ = [ + "BaseEstimatorGradient", + "BaseQGT", + "BaseSamplerGradient", + "DerivativeType", + "EstimatorGradientResult", + "LinCombEstimatorGradient", + "LinCombQGT", + "LinCombSamplerGradient", + "ParamShiftEstimatorGradient", + "ParamShiftSamplerGradient", + "QFI", + "QFIResult", + "QGTResult", + "SamplerGradientResult", + "SPSAEstimatorGradient", + "SPSASamplerGradient", +] diff --git a/qiskit_machine_learning/gradients/base/__init__.py b/qiskit_machine_learning/gradients/base/__init__.py new file mode 100644 index 000000000..adb31296c --- /dev/null +++ b/qiskit_machine_learning/gradients/base/__init__.py @@ -0,0 +1,11 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2023 +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. diff --git a/qiskit_machine_learning/gradients/base/base_estimator_gradient.py b/qiskit_machine_learning/gradients/base/base_estimator_gradient.py new file mode 100644 index 000000000..f7ea927b2 --- /dev/null +++ b/qiskit_machine_learning/gradients/base/base_estimator_gradient.py @@ -0,0 +1,363 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2023 +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +Abstract base class of gradient for ``Estimator``. +""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from collections.abc import Sequence +from copy import copy + +import numpy as np + +from qiskit.circuit import Parameter, ParameterExpression, QuantumCircuit +from qiskit.primitives import BaseEstimator +from qiskit.primitives.utils import _circuit_key +from qiskit.providers import Options +from qiskit.quantum_info.operators.base_operator import BaseOperator +from qiskit.transpiler.passes import TranslateParameterizedGates + +from .estimator_gradient_result import EstimatorGradientResult +from ..utils import ( + DerivativeType, + GradientCircuit, + _assign_unique_parameters, + _make_gradient_parameters, + _make_gradient_parameter_values, +) + +from ...algorithm_job import AlgorithmJob + + +class BaseEstimatorGradient(ABC): + """Base class for an ``EstimatorGradient`` to compute the gradients of the expectation value.""" + + def __init__( + self, + estimator: BaseEstimator, + options: Options | None = None, + derivative_type: DerivativeType = DerivativeType.REAL, + ): + r""" + Args: + estimator: The estimator used to compute the gradients. + options: Primitive backend runtime options used for circuit execution. + The order of priority is: options in ``run`` method > gradient's + default options > primitive's default setting. + Higher priority setting overrides lower priority setting + derivative_type: The type of derivative. Can be either ``DerivativeType.REAL`` + ``DerivativeType.IMAG``, or ``DerivativeType.COMPLEX``. + + - ``DerivativeType.REAL`` computes :math:`2 \mathrm{Re}[⟨ψ(ω)|O(θ)|dω ψ(ω)〉]`. + - ``DerivativeType.IMAG`` computes :math:`2 \mathrm{Im}[⟨ψ(ω)|O(θ)|dω ψ(ω)〉]`. + - ``DerivativeType.COMPLEX`` computes :math:`2 ⟨ψ(ω)|O(θ)|dω ψ(ω)〉`. + + Defaults to ``DerivativeType.REAL``, as this yields e.g. the commonly-used energy + gradient and this type is the only supported type for function-level schemes like + finite difference. + """ + self._estimator: BaseEstimator = estimator + self._default_options = Options() + if options is not None: + self._default_options.update_options(**options) + self._derivative_type = derivative_type + + self._gradient_circuit_cache: dict[ + tuple, + GradientCircuit, + ] = {} + + @property + def derivative_type(self) -> DerivativeType: + """Return the derivative type (real, imaginary or complex). + + Returns: + The derivative type. + """ + return self._derivative_type + + def run( + self, + circuits: Sequence[QuantumCircuit], + observables: Sequence[BaseOperator], + parameter_values: Sequence[Sequence[float]], + parameters: Sequence[Sequence[Parameter] | None] | None = None, + **options, + ) -> AlgorithmJob: + """Run the job of the estimator gradient on the given circuits. + + Args: + circuits: The list of quantum circuits to compute the gradients. + observables: The list of observables. + parameter_values: The list of parameter values to be bound to the circuit. + parameters: The sequence of parameters to calculate only the gradients of + the specified parameters. Each sequence of parameters corresponds to a circuit in + ``circuits``. Defaults to None, which means that the gradients of all parameters in + each circuit are calculated. None in the sequence means that the gradients of all + parameters in the corresponding circuit are calculated. + options: Primitive backend runtime options used for circuit execution. + The order of priority is: options in ``run`` method > gradient's + default options > primitive's default setting. + Higher priority setting overrides lower priority setting + + Returns: + The job object of the gradients of the expectation values. The i-th result corresponds to + ``circuits[i]`` evaluated with parameters bound as ``parameter_values[i]``. The j-th + element of the i-th result corresponds to the gradient of the i-th circuit with respect + to the j-th parameter. + + Raises: + ValueError: Invalid arguments are given. + """ + if isinstance(circuits, QuantumCircuit): + # Allow a single circuit to be passed in. + circuits = (circuits,) + if isinstance(observables, (BaseOperator)): + # Allow a single observable to be passed in. + observables = (observables,) + + if parameters is None: + # If parameters is None, we calculate the gradients of all parameters in each circuit. + parameters = [circuit.parameters for circuit in circuits] + else: + # If parameters is not None, we calculate the gradients of the specified parameters. + # None in parameters means that the gradients of all parameters in the corresponding + # circuit are calculated. + parameters = [ + params if params is not None else circuits[i].parameters + for i, params in enumerate(parameters) + ] + # Validate the arguments. + self._validate_arguments(circuits, observables, parameter_values, parameters) + # The priority of run option is as follows: + # options in ``run`` method > gradient's default options > primitive's default setting. + opts = copy(self._default_options) + opts.update_options(**options) + # Run the job. + job = AlgorithmJob( + self._run, circuits, observables, parameter_values, parameters, **opts.__dict__ + ) + job.submit() + return job + + @abstractmethod + def _run( + self, + circuits: Sequence[QuantumCircuit], + observables: Sequence[BaseOperator], + parameter_values: Sequence[Sequence[float]], + parameters: Sequence[Sequence[Parameter]], + **options, + ) -> EstimatorGradientResult: + """Compute the estimator gradients on the given circuits.""" + raise NotImplementedError() + + def _preprocess( + self, + circuits: Sequence[QuantumCircuit], + parameter_values: Sequence[Sequence[float]], + parameters: Sequence[Sequence[Parameter]], + supported_gates: Sequence[str], + ) -> tuple[Sequence[QuantumCircuit], Sequence[Sequence[float]], Sequence[Sequence[Parameter]]]: + """Preprocess the gradient. This makes a gradient circuit for each circuit. The gradient + circuit is a transpiled circuit by using the supported gates, and has unique parameters. + ``parameter_values`` and ``parameters`` are also updated to match the gradient circuit. + + Args: + circuits: The list of quantum circuits to compute the gradients. + parameter_values: The list of parameter values to be bound to the circuit. + parameters: The sequence of parameters to calculate only the gradients of the specified + parameters. + supported_gates: The supported gates used to transpile the circuit. + + Returns: + The list of gradient circuits, the list of parameter values, and the list of parameters. + parameter_values and parameters are updated to match the gradient circuit. + """ + translator = TranslateParameterizedGates(supported_gates) + g_circuits: list[QuantumCircuit] = [] + g_parameter_values: list[Sequence[float]] = [] + g_parameters: list[Sequence[Parameter]] = [] + for circuit, parameter_value_, parameters_ in zip(circuits, parameter_values, parameters): + circuit_key = _circuit_key(circuit) + if circuit_key not in self._gradient_circuit_cache: + unrolled = translator(circuit) + self._gradient_circuit_cache[circuit_key] = _assign_unique_parameters(unrolled) + gradient_circuit = self._gradient_circuit_cache[circuit_key] + g_circuits.append(gradient_circuit.gradient_circuit) + g_parameter_values.append( + _make_gradient_parameter_values( # type: ignore[arg-type] + circuit, gradient_circuit, parameter_value_ + ) + ) + g_parameters.append(_make_gradient_parameters(gradient_circuit, parameters_)) + return g_circuits, g_parameter_values, g_parameters + + def _postprocess( + self, + results: EstimatorGradientResult, + circuits: Sequence[QuantumCircuit], + parameter_values: Sequence[Sequence[float]], + parameters: Sequence[Sequence[Parameter]], + ) -> EstimatorGradientResult: + """Postprocess the gradients. This method computes the gradient of the original circuits + by applying the chain rule to the gradient of the circuits with unique parameters. + + Args: + results: The computed gradients for the circuits with unique parameters. + circuits: The list of original circuits submitted for gradient computation. + parameter_values: The list of parameter values to be bound to the circuits. + parameters: The sequence of parameters to calculate only the gradients of the specified + parameters. + + Returns: + The gradients of the original circuits. + """ + gradients, metadata = [], [] + for idx, (circuit, parameter_values_, parameters_) in enumerate( + zip(circuits, parameter_values, parameters) + ): + gradient = np.zeros(len(parameters_)) + if ( + "derivative_type" in results.metadata[idx] + and results.metadata[idx]["derivative_type"] == DerivativeType.COMPLEX + ): + # If the derivative type is complex, cast the gradient to complex. + gradient = gradient.astype("complex") + gradient_circuit = self._gradient_circuit_cache[_circuit_key(circuit)] + g_parameters = _make_gradient_parameters(gradient_circuit, parameters_) + # Make a map from the gradient parameter to the respective index in the gradient. + g_parameter_indices = {param: i for i, param in enumerate(g_parameters)} + # Compute the original gradient from the gradient of the gradient circuit + # by using the chain rule. + for i, parameter in enumerate(parameters_): + for g_parameter, coeff in gradient_circuit.parameter_map[parameter]: + # Compute the coefficient + if isinstance(coeff, ParameterExpression): + local_map = { + p: parameter_values_[circuit.parameters.data.index(p)] + for p in coeff.parameters + } + bound_coeff = coeff.bind(local_map) + else: + bound_coeff = coeff + # The original gradient is a sum of the gradients of the parameters in the + # gradient circuit multiplied by the coefficients. + gradient[i] += ( + float(bound_coeff) + * results.gradients[idx][g_parameter_indices[g_parameter]] + ) + gradients.append(gradient) + metadata.append({"parameters": parameters_}) + return EstimatorGradientResult( + gradients=gradients, metadata=metadata, options=results.options + ) + + @staticmethod + def _validate_arguments( + circuits: Sequence[QuantumCircuit], + observables: Sequence[BaseOperator], + parameter_values: Sequence[Sequence[float]], + parameters: Sequence[Sequence[Parameter]], + ) -> None: + """Validate the arguments of the ``run`` method. + + Args: + circuits: The list of quantum circuits to compute the gradients. + observables: The list of observables. + parameter_values: The list of parameter values to be bound to the circuit. + parameters: The sequence of parameters to calculate only the gradients of the specified + parameters. + + Raises: + ValueError: Invalid arguments are given. + """ + if len(circuits) != len(parameter_values): + raise ValueError( + f"The number of circuits ({len(circuits)}) does not match " + f"the number of parameter value sets ({len(parameter_values)})." + ) + + for i, (circuit, parameter_value) in enumerate(zip(circuits, parameter_values)): + if not circuit.num_parameters: + raise ValueError(f"The {i}-th circuit is not parameterised.") + if len(parameter_value) != circuit.num_parameters: + raise ValueError( + f"The number of values ({len(parameter_value)}) does not match " + f"the number of parameters ({circuit.num_parameters}) for the {i}-th circuit." + ) + + if len(circuits) != len(observables): + raise ValueError( + f"The number of circuits ({len(circuits)}) does not match " + f"the number of observables ({len(observables)})." + ) + + for i, (circuit, observable) in enumerate(zip(circuits, observables)): + if circuit.num_qubits != observable.num_qubits: + raise ValueError( + f"The number of qubits of the {i}-th circuit ({circuit.num_qubits}) does " + f"not match the number of qubits of the {i}-th observable " + f"({observable.num_qubits})." + ) + + if len(circuits) != len(parameters): + raise ValueError( + f"The number of circuits ({len(circuits)}) does not match " + f"the number of the list of specified parameters ({len(parameters)})." + ) + + for i, (circuit, parameters_) in enumerate(zip(circuits, parameters)): + if not set(parameters_).issubset(circuit.parameters): + raise ValueError( + f"The {i}-th parameters contains parameters not present in the " + f"{i}-th circuit." + ) + + @property + def options(self) -> Options: + """Return the union of estimator options setting and gradient default options, + where, if the same field is set in both, the gradient's default options override + the primitive's default setting. + + Returns: + The gradient default + estimator options. + """ + return self._get_local_options(self._default_options.__dict__) + + def update_default_options(self, **options): + """Update the gradient's default options setting. + + Args: + **options: The fields to update the default options. + """ + + self._default_options.update_options(**options) + + def _get_local_options(self, options: Options) -> Options: + """Return the union of the primitive's default setting, + the gradient default options, and the options in the ``run`` method. + The order of priority is: options in ``run`` method > gradient's + default options > primitive's default setting. + + Args: + options: The fields to update the options + + Returns: + The gradient default + estimator + run options. + """ + opts = copy(self._estimator.options) + opts.update_options(**options) + return opts diff --git a/qiskit_machine_learning/gradients/base/base_qgt.py b/qiskit_machine_learning/gradients/base/base_qgt.py new file mode 100644 index 000000000..2e254a8f0 --- /dev/null +++ b/qiskit_machine_learning/gradients/base/base_qgt.py @@ -0,0 +1,388 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +Abstract base class of the Quantum Geometric Tensor (QGT). +""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from collections.abc import Sequence +from copy import copy + +import numpy as np + +from qiskit.circuit import Parameter, ParameterExpression, QuantumCircuit +from qiskit.primitives import BaseEstimator +from qiskit.primitives.utils import _circuit_key +from qiskit.providers import Options +from qiskit.transpiler.passes import TranslateParameterizedGates + +from .qgt_result import QGTResult +from ..utils import ( + DerivativeType, + GradientCircuit, + _assign_unique_parameters, + _make_gradient_parameters, + _make_gradient_parameter_values, +) + +from ...algorithm_job import AlgorithmJob + + +class BaseQGT(ABC): + r"""Base class to computes the Quantum Geometric Tensor (QGT) given a pure, + parameterized quantum state. QGT is defined as: + + .. math:: + + \mathrm{QGT}_{ij}= \langle \partial_i \psi | \partial_j \psi \rangle + - \langle\partial_i \psi | \psi \rangle \langle\psi | \partial_j \psi \rangle. + """ + + def __init__( + self, + estimator: BaseEstimator, + phase_fix: bool = True, + derivative_type: DerivativeType = DerivativeType.COMPLEX, + options: Options | None = None, + ): + r""" + Args: + estimator: The estimator used to compute the QGT. + phase_fix: Whether to calculate the second term (phase fix) of the QGT, which is + :math:`\langle\partial_i \psi | \psi \rangle \langle\psi | \partial_j \psi \rangle`. + Defaults to ``True``. + derivative_type: The type of derivative. Can be either ``DerivativeType.REAL`` + ``DerivativeType.IMAG``, or ``DerivativeType.COMPLEX``. Defaults to + ``DerivativeType.REAL``. + + - ``DerivativeType.REAL`` computes + + .. math:: + + \mathrm{Re(QGT)}_{ij}= \mathrm{Re}[\langle \partial_i \psi | \partial_j \psi \rangle + - \langle\partial_i \psi | \psi \rangle \langle\psi | \partial_j \psi \rangle]. + + - ``DerivativeType.IMAG`` computes + + .. math:: + + \mathrm{Im(QGT)}_{ij}= \mathrm{Im}[\langle \partial_i \psi | \partial_j \psi \rangle + - \langle\partial_i \psi | \psi \rangle \langle\psi | \partial_j \psi \rangle]. + + - ``DerivativeType.COMPLEX`` computes + + .. math:: + + \mathrm{QGT}_{ij}= [\langle \partial_i \psi | \partial_j \psi \rangle + - \langle\partial_i \psi | \psi \rangle \langle\psi | \partial_j \psi \rangle]. + + options: Backend runtime options used for circuit execution. The order of priority is: + options in ``run`` method > QGT's default options > primitive's default + setting. Higher priority setting overrides lower priority setting. + """ + self._estimator: BaseEstimator = estimator + self._phase_fix: bool = phase_fix + self._derivative_type: DerivativeType = derivative_type + self._default_options = Options() + if options is not None: + self._default_options.update_options(**options) + self._qgt_circuit_cache: dict[tuple, GradientCircuit] = {} + self._gradient_circuit_cache: dict[tuple, GradientCircuit] = {} + + @property + def derivative_type(self) -> DerivativeType: + """The derivative type.""" + return self._derivative_type + + @derivative_type.setter + def derivative_type(self, derivative_type: DerivativeType) -> None: + """Set the derivative type.""" + self._derivative_type = derivative_type + + def run( + self, + circuits: Sequence[QuantumCircuit], + parameter_values: Sequence[Sequence[float]], + parameters: Sequence[Sequence[Parameter] | None] | None = None, + **options, + ) -> AlgorithmJob: + """Run the job of the QGTs on the given circuits. + + Args: + circuits: The list of quantum circuits to compute the QGTs. + parameter_values: The list of parameter values to be bound to the circuit. + parameters: The sequence of parameters to calculate only the QGTs of + the specified parameters. Each sequence of parameters corresponds to a circuit in + ``circuits``. Defaults to None, which means that the QGTs of all parameters in + each circuit are calculated. + options: Primitive backend runtime options used for circuit execution. + The order of priority is: options in ``run`` method > QGT's + default options > primitive's default setting. + Higher priority setting overrides lower priority setting. + + Returns: + The job object of the QGTs of the expectation values. The i-th result corresponds to + ``circuits[i]`` evaluated with parameters bound as ``parameter_values[i]``. + + Raises: + ValueError: Invalid arguments are given. + """ + if isinstance(circuits, QuantumCircuit): + # Allow a single circuit to be passed in. + circuits = (circuits,) + + if parameters is None: + # If parameters is None, we calculate the gradients of all parameters in each circuit. + parameters = [circuit.parameters for circuit in circuits] + else: + # If parameters is not None, we calculate the gradients of the specified parameters. + # None in parameters means that the gradients of all parameters in the corresponding + # circuit are calculated. + parameters = [ + params if params is not None else circuits[i].parameters + for i, params in enumerate(parameters) + ] + # Validate the arguments. + self._validate_arguments(circuits, parameter_values, parameters) + # The priority of run option is as follows: + # options in ``run`` method > QGT's default options > primitive's default setting. + opts = copy(self._default_options) + opts.update_options(**options) + job = AlgorithmJob(self._run, circuits, parameter_values, parameters, **opts.__dict__) + job.submit() + return job + + @abstractmethod + def _run( + self, + circuits: Sequence[QuantumCircuit], + parameter_values: Sequence[Sequence[float]], + parameters: Sequence[Sequence[Parameter]], + **options, + ) -> QGTResult: + """Compute the QGTs on the given circuits.""" + raise NotImplementedError() + + def _preprocess( + self, + circuits: Sequence[QuantumCircuit], + parameter_values: Sequence[Sequence[float]], + parameters: Sequence[Sequence[Parameter]], + supported_gates: Sequence[str], + ) -> tuple[Sequence[QuantumCircuit], Sequence[Sequence[float]], Sequence[Sequence[Parameter]]]: + """Preprocess the gradient. This makes a gradient circuit for each circuit. The gradient + circuit is a transpiled circuit by using the supported gates, and has unique parameters. + ``parameter_values`` and ``parameters`` are also updated to match the gradient circuit. + + Args: + circuits: The list of quantum circuits to compute the gradients. + parameter_values: The list of parameter values to be bound to the circuit. + parameters: The sequence of parameters to calculate only the gradients of the specified + parameters. + supported_gates: The supported gates used to transpile the circuit. + + Returns: + The list of gradient circuits, the list of parameter values, and the list of parameters. + parameter_values and parameters are updated to match the gradient circuit. + """ + translator = TranslateParameterizedGates(supported_gates) + g_circuits: list[QuantumCircuit] = [] + g_parameter_values: list[Sequence[float]] = [] + g_parameters: list[Sequence[Parameter]] = [] + for circuit, parameter_value_, parameters_ in zip(circuits, parameter_values, parameters): + circuit_key = _circuit_key(circuit) + if circuit_key not in self._gradient_circuit_cache: + unrolled = translator(circuit) + self._gradient_circuit_cache[circuit_key] = _assign_unique_parameters(unrolled) + gradient_circuit = self._gradient_circuit_cache[circuit_key] + g_circuits.append(gradient_circuit.gradient_circuit) + g_parameter_values.append( + _make_gradient_parameter_values( # type: ignore[arg-type] + circuit, gradient_circuit, parameter_value_ + ) + ) + g_parameters_ = [ + g_param + for g_param in gradient_circuit.gradient_circuit.parameters + if g_param in _make_gradient_parameters(gradient_circuit, parameters_) + ] + g_parameters.append(g_parameters_) + return g_circuits, g_parameter_values, g_parameters + + def _postprocess( + self, + results: QGTResult, + circuits: Sequence[QuantumCircuit], + parameter_values: Sequence[Sequence[float]], + parameters: Sequence[Sequence[Parameter]], + ) -> QGTResult: + """Postprocess the QGTs. This method computes the QGTs of the original circuits + by applying the chain rule to the QGTs of the circuits with unique parameters. + + Args: + results: The computed QGT for the circuits with unique parameters. + circuits: The list of original circuits submitted for gradient computation. + parameter_values: The list of parameter values to be bound to the circuits. + parameters: The sequence of parameters to calculate only the gradients of the specified + parameters. + + Returns: + The QGTs of the original circuits. + """ + qgts, metadata = [], [] + for idx, (circuit, parameter_values_, parameters_) in enumerate( + zip(circuits, parameter_values, parameters) + ): + dtype = complex if self.derivative_type == DerivativeType.COMPLEX else float + qgt: np.ndarray = np.zeros((len(parameters_), len(parameters_)), dtype=dtype) + + gradient_circuit = self._gradient_circuit_cache[_circuit_key(circuit)] + g_parameters = _make_gradient_parameters(gradient_circuit, parameters_) + # Make a map from the gradient parameter to the respective index in the gradient. + # parameters_ = [param for param in circuit.parameters if param in parameters_] + g_parameter_indices = [ + param + for param in gradient_circuit.gradient_circuit.parameters + if param in g_parameters + ] + g_parameter_indices_d = {param: i for i, param in enumerate(g_parameter_indices)} + rows, cols = np.triu_indices(len(parameters_)) + for row, col in zip(rows, cols): + for g_parameter1, coeff1 in gradient_circuit.parameter_map[parameters_[row]]: + for g_parameter2, coeff2 in gradient_circuit.parameter_map[parameters_[col]]: + if isinstance(coeff1, ParameterExpression): + local_map = { + p: parameter_values_[circuit.parameters.data.index(p)] + for p in coeff1.parameters + } + bound_coeff1 = coeff1.bind(local_map) + else: + bound_coeff1 = coeff1 + if isinstance(coeff2, ParameterExpression): + local_map = { + p: parameter_values_[circuit.parameters.data.index(p)] + for p in coeff2.parameters + } + bound_coeff2 = coeff2.bind(local_map) + else: + bound_coeff2 = coeff2 + qgt[row, col] += ( + float(bound_coeff1) + * float(bound_coeff2) + * results.qgts[idx][ + g_parameter_indices_d[g_parameter1], + g_parameter_indices_d[g_parameter2], + ] + ) + + if self.derivative_type == DerivativeType.IMAG: + qgt += -1 * np.triu(qgt, k=1).T + else: + qgt += np.triu(qgt, k=1).conjugate().T + qgts.append(qgt) + metadata.append([{"parameters": parameters_}]) + return QGTResult( + qgts=qgts, + derivative_type=self.derivative_type, + metadata=metadata, + options=results.options, + ) + + @staticmethod + def _validate_arguments( + circuits: Sequence[QuantumCircuit], + parameter_values: Sequence[Sequence[float]], + parameters: Sequence[Sequence[Parameter]], + ) -> None: + """Validate the arguments of the ``run`` method. + + Args: + circuits: The list of quantum circuits to compute the QGTs. + parameter_values: The list of parameter values to be bound to the circuits. + parameters: The sequence of parameters with respect to which the QGTs should be + computed. + + Raises: + ValueError: Invalid arguments are given. + """ + if len(circuits) != len(parameter_values): + raise ValueError( + f"The number of circuits ({len(circuits)}) does not match " + f"the number of parameter values ({len(parameter_values)})." + ) + + if len(circuits) != len(parameters): + raise ValueError( + f"The number of circuits ({len(circuits)}) does not match " + f"the number of the specified parameter sets ({len(parameters)})." + ) + + for i, (circuit, parameter_value) in enumerate(zip(circuits, parameter_values)): + if not circuit.num_parameters: + raise ValueError(f"The {i}-th circuit is not parameterised.") + if len(parameter_value) != circuit.num_parameters: + raise ValueError( + f"The number of values ({len(parameter_value)}) does not match " + f"the number of parameters ({circuit.num_parameters}) for the {i}-th circuit." + ) + + if len(circuits) != len(parameters): + raise ValueError( + f"The number of circuits ({len(circuits)}) does not match " + f"the number of the list of specified parameters ({len(parameters)})." + ) + + for i, (circuit, parameters_) in enumerate(zip(circuits, parameters)): + if not set(parameters_).issubset(circuit.parameters): + raise ValueError( + f"The {i}-th parameters contains parameters not present in the " + f"{i}-th circuit." + ) + + @property + def options(self) -> Options: + """Return the union of estimator options setting and QGT default options, + where, if the same field is set in both, the QGT's default options override + the primitive's default setting. + + Returns: + The QGT default + estimator options. + """ + return self._get_local_options(self._default_options.__dict__) + + def update_default_options(self, **options): + """Update the gradient's default options setting. + + Args: + **options: The fields to update the default options. + """ + + self._default_options.update_options(**options) + + def _get_local_options(self, options: Options) -> Options: + """Return the union of the primitive's default setting, + the QGT default options, and the options in the ``run`` method. + The order of priority is: options in ``run`` method > QGT's default options > primitive's + default setting. + + Args: + options: The fields to update the options + + Returns: + The QGT default + estimator + run options. + """ + opts = copy(self._estimator.options) + opts.update_options(**options) + return opts diff --git a/qiskit_machine_learning/gradients/base/base_sampler_gradient.py b/qiskit_machine_learning/gradients/base/base_sampler_gradient.py new file mode 100644 index 000000000..1114b5f02 --- /dev/null +++ b/qiskit_machine_learning/gradients/base/base_sampler_gradient.py @@ -0,0 +1,300 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2023 +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +Abstract base class of gradient for ``Sampler``. +""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from collections import defaultdict +from collections.abc import Sequence +from copy import copy + +from qiskit.circuit import Parameter, ParameterExpression, QuantumCircuit +from qiskit.primitives import BaseSampler +from qiskit.primitives.utils import _circuit_key +from qiskit.providers import Options +from qiskit.transpiler.passes import TranslateParameterizedGates + +from .sampler_gradient_result import SamplerGradientResult +from ..utils import ( + GradientCircuit, + _assign_unique_parameters, + _make_gradient_parameters, + _make_gradient_parameter_values, +) + +from ...algorithm_job import AlgorithmJob + + +class BaseSamplerGradient(ABC): + """Base class for a ``SamplerGradient`` to compute the gradients of the sampling probability.""" + + def __init__(self, sampler: BaseSampler, options: Options | None = None): + """ + Args: + sampler: The sampler used to compute the gradients. + options: Primitive backend runtime options used for circuit execution. + The order of priority is: options in ``run`` method > gradient's + default options > primitive's default setting. + Higher priority setting overrides lower priority setting + """ + self._sampler: BaseSampler = sampler + self._default_options = Options() + if options is not None: + self._default_options.update_options(**options) + self._gradient_circuit_cache: dict[tuple, GradientCircuit] = {} + + def run( + self, + circuits: Sequence[QuantumCircuit], + parameter_values: Sequence[Sequence[float]], + parameters: Sequence[Sequence[Parameter] | None] | None = None, + **options, + ) -> AlgorithmJob: + """Run the job of the sampler gradient on the given circuits. + + Args: + circuits: The list of quantum circuits to compute the gradients. + parameter_values: The list of parameter values to be bound to the circuit. + parameters: The sequence of parameters to calculate only the gradients of + the specified parameters. Each sequence of parameters corresponds to a circuit in + ``circuits``. Defaults to None, which means that the gradients of all parameters in + each circuit are calculated. None in the sequence means that the gradients of all + parameters in the corresponding circuit are calculated. + options: Primitive backend runtime options used for circuit execution. + The order of priority is: options in ``run`` method > gradient's + default options > primitive's default setting. + Higher priority setting overrides lower priority setting + Returns: + The job object of the gradients of the sampling probability. The i-th result + corresponds to ``circuits[i]`` evaluated with parameters bound as ``parameter_values[i]``. + The j-th quasi-probability distribution in the i-th result corresponds to the gradients of + the sampling probability for the j-th parameter in ``circuits[i]``. + + Raises: + ValueError: Invalid arguments are given. + """ + if isinstance(circuits, QuantumCircuit): + # Allow a single circuit to be passed in. + circuits = (circuits,) + if parameters is None: + # If parameters is None, we calculate the gradients of all parameters in each circuit. + parameters = [circuit.parameters for circuit in circuits] + else: + # If parameters is not None, we calculate the gradients of the specified parameters. + # None in parameters means that the gradients of all parameters in the corresponding + # circuit are calculated. + parameters = [ + params if params is not None else circuits[i].parameters + for i, params in enumerate(parameters) + ] + # Validate the arguments. + self._validate_arguments(circuits, parameter_values, parameters) + # The priority of run option is as follows: + # options in `run` method > gradient's default options > primitive's default options. + opts = copy(self._default_options) + opts.update_options(**options) + job = AlgorithmJob(self._run, circuits, parameter_values, parameters, **opts.__dict__) + job.submit() + return job + + @abstractmethod + def _run( + self, + circuits: Sequence[QuantumCircuit], + parameter_values: Sequence[Sequence[float]], + parameters: Sequence[Sequence[Parameter]], + **options, + ) -> SamplerGradientResult: + """Compute the sampler gradients on the given circuits.""" + raise NotImplementedError() + + def _preprocess( + self, + circuits: Sequence[QuantumCircuit], + parameter_values: Sequence[Sequence[float]], + parameters: Sequence[Sequence[Parameter]], + supported_gates: Sequence[str], + ) -> tuple[Sequence[QuantumCircuit], Sequence[Sequence[float]], Sequence[Sequence[Parameter]]]: + """Preprocess the gradient. This makes a gradient circuit for each circuit. The gradient + circuit is a transpiled circuit by using the supported gates, and has unique parameters. + ``parameter_values`` and ``parameters`` are also updated to match the gradient circuit. + + Args: + circuits: The list of quantum circuits to compute the gradients. + parameter_values: The list of parameter values to be bound to the circuit. + parameters: The sequence of parameters to calculate only the gradients of the specified + parameters. + supported_gates: The supported gates used to transpile the circuit. + + Returns: + The list of gradient circuits, the list of parameter values, and the list of parameters. + parameter_values and parameters are updated to match the gradient circuit. + """ + translator = TranslateParameterizedGates(supported_gates) + g_circuits: list[QuantumCircuit] = [] + g_parameter_values: list[Sequence[float]] = [] + g_parameters: list[Sequence[Parameter]] = [] + for circuit, parameter_value_, parameters_ in zip(circuits, parameter_values, parameters): + circuit_key = _circuit_key(circuit) + if circuit_key not in self._gradient_circuit_cache: + unrolled = translator(circuit) + self._gradient_circuit_cache[circuit_key] = _assign_unique_parameters(unrolled) + gradient_circuit = self._gradient_circuit_cache[circuit_key] + g_circuits.append(gradient_circuit.gradient_circuit) + g_parameter_values.append( + _make_gradient_parameter_values( # type: ignore[arg-type] + circuit, gradient_circuit, parameter_value_ + ) + ) + g_parameters.append(_make_gradient_parameters(gradient_circuit, parameters_)) + return g_circuits, g_parameter_values, g_parameters + + def _postprocess( + self, + results: SamplerGradientResult, + circuits: Sequence[QuantumCircuit], + parameter_values: Sequence[Sequence[float]], + parameters: Sequence[Sequence[Parameter] | None], + ) -> SamplerGradientResult: + """Postprocess the gradient. This computes the gradient of the original circuit from the + gradient of the gradient circuit by using the chain rule. + + Args: + results: The results of the gradient of the gradient circuits. + circuits: The list of quantum circuits to compute the gradients. + parameter_values: The list of parameter values to be bound to the circuit. + parameters: The sequence of parameters to calculate only the gradients of the specified + parameters. + + Returns: + The results of the gradient of the original circuits. + """ + gradients, metadata = [], [] + for idx, (circuit, parameter_values_, parameters_) in enumerate( + zip(circuits, parameter_values, parameters) + ): + gradient_circuit = self._gradient_circuit_cache[_circuit_key(circuit)] + g_parameters = _make_gradient_parameters(gradient_circuit, parameters_) + # Make a map from the gradient parameter to the respective index in the gradient. + g_parameter_indices = {param: i for i, param in enumerate(g_parameters)} + # Compute the original gradient from the gradient of the gradient circuit + # by using the chain rule. + gradient = [] + for parameter in parameters_: + grad_dist: dict[int, float] = defaultdict(float) + for g_parameter, coeff in gradient_circuit.parameter_map[parameter]: + # Compute the coefficient + if isinstance(coeff, ParameterExpression): + local_map = { + p: parameter_values_[circuit.parameters.data.index(p)] + for p in coeff.parameters + } + bound_coeff = coeff.bind(local_map) + else: + bound_coeff = coeff + # The original gradient is a sum of the gradients of the parameters in the + # gradient circuit multiplied by the coefficients. + unique_gradient = results.gradients[idx][g_parameter_indices[g_parameter]] + for key, value in unique_gradient.items(): + grad_dist[key] += float(bound_coeff) * value + gradient.append(dict(grad_dist)) + gradients.append(gradient) + metadata.append([{"parameters": parameters_}]) + return SamplerGradientResult( + gradients=gradients, metadata=metadata, options=results.options + ) + + @staticmethod + def _validate_arguments( + circuits: Sequence[QuantumCircuit], + parameter_values: Sequence[Sequence[float]], + parameters: Sequence[Sequence[Parameter]], + ) -> None: + """Validate the arguments of the ``run`` method. + + Args: + circuits: The list of quantum circuits to compute the gradients. + parameter_values: The list of parameter values to be bound to the circuit. + parameters: The sequence of parameters to calculate only the gradients of the specified + parameters. + + Raises: + ValueError: Invalid arguments are given. + """ + if len(circuits) != len(parameter_values): + raise ValueError( + f"The number of circuits ({len(circuits)}) does not match " + f"the number of parameter value sets ({len(parameter_values)})." + ) + + for i, (circuit, parameter_value) in enumerate(zip(circuits, parameter_values)): + if not circuit.num_parameters: + raise ValueError(f"The {i}-th circuit is not parameterised.") + + if len(parameter_value) != circuit.num_parameters: + raise ValueError( + f"The number of values ({len(parameter_value)}) does not match " + f"the number of parameters ({circuit.num_parameters}) for the {i}-th circuit." + ) + + if len(circuits) != len(parameters): + raise ValueError( + f"The number of circuits ({len(circuits)}) does not match " + f"the number of the specified parameter sets ({len(parameters)})." + ) + + for i, (circuit, parameters_) in enumerate(zip(circuits, parameters)): + if not set(parameters_).issubset(circuit.parameters): + raise ValueError( + f"The {i}-th parameter set contains parameters not present in the " + f"{i}-th circuit." + ) + + @property + def options(self) -> Options: + """Return the union of sampler options setting and gradient default options, + where, if the same field is set in both, the gradient's default options override + the primitive's default setting. + + Returns: + The gradient default + sampler options. + """ + return self._get_local_options(self._default_options.__dict__) + + def update_default_options(self, **options): + """Update the gradient's default options setting. + + Args: + **options: The fields to update the default options. + """ + + self._default_options.update_options(**options) + + def _get_local_options(self, options: Options) -> Options: + """Return the union of the primitive's default setting, + the gradient default options, and the options in the ``run`` method. + The order of priority is: options in ``run`` method > gradient's + default options > primitive's default setting. + + Args: + options: The fields to update the options + + Returns: + The gradient default + sampler + run options. + """ + opts = copy(self._sampler.options) + opts.update_options(**options) + return opts diff --git a/qiskit_machine_learning/gradients/base/estimator_gradient_result.py b/qiskit_machine_learning/gradients/base/estimator_gradient_result.py new file mode 100644 index 000000000..a01759f0d --- /dev/null +++ b/qiskit_machine_learning/gradients/base/estimator_gradient_result.py @@ -0,0 +1,35 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +""" +Estimator result class +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +import numpy as np + +from qiskit.providers import Options + + +@dataclass(frozen=True) +class EstimatorGradientResult: + """Result of EstimatorGradient.""" + + gradients: list[np.ndarray] + """The gradients of the expectation values.""" + metadata: list[dict[str, Any]] + """Additional information about the job.""" + options: Options + """Primitive runtime options for the execution of the job.""" diff --git a/qiskit_machine_learning/gradients/base/qgt_result.py b/qiskit_machine_learning/gradients/base/qgt_result.py new file mode 100644 index 000000000..f7a9d80b1 --- /dev/null +++ b/qiskit_machine_learning/gradients/base/qgt_result.py @@ -0,0 +1,39 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +""" +QGT result class +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +import numpy as np + +from qiskit.providers import Options + +from ..utils import DerivativeType + + +@dataclass(frozen=True) +class QGTResult: + """Result of QGT.""" + + qgts: list[np.ndarray] + """The QGT.""" + derivative_type: DerivativeType + """The type of derivative.""" + metadata: list[dict[str, Any]] | list[list[dict[str, Any]]] + """Additional information about the job.""" + options: Options + """Primitive runtime options for the execution of the job.""" diff --git a/qiskit_machine_learning/gradients/base/sampler_gradient_result.py b/qiskit_machine_learning/gradients/base/sampler_gradient_result.py new file mode 100644 index 000000000..393319ab0 --- /dev/null +++ b/qiskit_machine_learning/gradients/base/sampler_gradient_result.py @@ -0,0 +1,33 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +""" +Sampler result class +""" + +from __future__ import annotations + +from typing import Any +from dataclasses import dataclass + +from qiskit.providers import Options + + +@dataclass(frozen=True) +class SamplerGradientResult: + """Result of SamplerGradient.""" + + gradients: list[list[dict[int, float]]] + """The gradients of the sample probabilities.""" + metadata: list[dict[str, Any]] | list[list[dict[str, Any]]] + """Additional information about the job.""" + options: Options + """Primitive runtime options for the execution of the job.""" diff --git a/qiskit_machine_learning/gradients/lin_comb/__init__.py b/qiskit_machine_learning/gradients/lin_comb/__init__.py new file mode 100644 index 000000000..adb31296c --- /dev/null +++ b/qiskit_machine_learning/gradients/lin_comb/__init__.py @@ -0,0 +1,11 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2023 +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. diff --git a/qiskit_machine_learning/gradients/lin_comb/lin_comb_estimator_gradient.py b/qiskit_machine_learning/gradients/lin_comb/lin_comb_estimator_gradient.py new file mode 100644 index 000000000..1be059007 --- /dev/null +++ b/qiskit_machine_learning/gradients/lin_comb/lin_comb_estimator_gradient.py @@ -0,0 +1,194 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +""" +Gradient of probabilities with linear combination of unitaries (LCU) +""" + +from __future__ import annotations + +from collections.abc import Sequence + +import numpy as np + +from qiskit.circuit import Parameter, QuantumCircuit +from qiskit.primitives import BaseEstimator +from qiskit.primitives.utils import init_observable, _circuit_key +from qiskit.providers import Options +from qiskit.quantum_info.operators.base_operator import BaseOperator + +from ..base.base_estimator_gradient import BaseEstimatorGradient +from ..base.estimator_gradient_result import EstimatorGradientResult +from ..utils import DerivativeType, _make_lin_comb_gradient_circuit, _make_lin_comb_observables + +from ...exceptions import AlgorithmError + + +class LinCombEstimatorGradient(BaseEstimatorGradient): + """Compute the gradients of the expectation values. + This method employs a linear combination of unitaries [1]. + + **Reference:** + [1] Schuld et al., Evaluating analytic gradients on quantum hardware, 2018 + `arXiv:1811.11184 `_ + """ + + SUPPORTED_GATES = [ + "rx", + "ry", + "rz", + "rzx", + "rzz", + "ryy", + "rxx", + "cx", + "cy", + "cz", + "ccx", + "swap", + "iswap", + "h", + "t", + "s", + "sdg", + "x", + "y", + "z", + ] + + def __init__( + self, + estimator: BaseEstimator, + derivative_type: DerivativeType = DerivativeType.REAL, + options: Options | None = None, + ): + r""" + Args: + estimator: The estimator used to compute the gradients. + derivative_type: The type of derivative. Can be either ``DerivativeType.REAL`` + ``DerivativeType.IMAG``, or ``DerivativeType.COMPLEX``. Defaults to + ``DerivativeType.REAL``. + + - ``DerivativeType.REAL`` computes :math:`2 \mathrm{Re}[⟨ψ(ω)|O(θ)|dω ψ(ω)〉]`. + - ``DerivativeType.IMAG`` computes :math:`2 \mathrm{Im}[⟨ψ(ω)|O(θ)|dω ψ(ω)〉]`. + - ``DerivativeType.COMPLEX`` computes :math:`2 ⟨ψ(ω)|O(θ)|dω ψ(ω)〉`. + + options: Primitive backend runtime options used for circuit execution. + The order of priority is: options in ``run`` method > gradient's + default options > primitive's default setting. + Higher priority setting overrides lower priority setting. + """ + self._lin_comb_cache: dict[tuple, dict[Parameter, QuantumCircuit]] = {} + super().__init__(estimator, options, derivative_type=derivative_type) + + @BaseEstimatorGradient.derivative_type.setter # type: ignore[attr-defined] + def derivative_type(self, derivative_type: DerivativeType) -> None: + """Set the derivative type.""" + self._derivative_type = derivative_type + + def _run( + self, + circuits: Sequence[QuantumCircuit], + observables: Sequence[BaseOperator], + parameter_values: Sequence[Sequence[float]], + parameters: Sequence[Sequence[Parameter]], + **options, + ) -> EstimatorGradientResult: + """Compute the estimator gradients on the given circuits.""" + g_circuits, g_parameter_values, g_parameters = self._preprocess( + circuits, parameter_values, parameters, self.SUPPORTED_GATES + ) + results = self._run_unique( + g_circuits, observables, g_parameter_values, g_parameters, **options + ) + return self._postprocess(results, circuits, parameter_values, parameters) + + def _run_unique( + self, + circuits: Sequence[QuantumCircuit], + observables: Sequence[BaseOperator], + parameter_values: Sequence[Sequence[float]], + parameters: Sequence[Sequence[Parameter]], + **options, + ) -> EstimatorGradientResult: + """Compute the estimator gradients on the given circuits.""" + job_circuits, job_observables, job_param_values, metadata = [], [], [], [] + all_n = [] + for circuit, observable, parameter_values_, parameters_ in zip( + circuits, observables, parameter_values, parameters + ): + # Prepare circuits for the gradient of the specified parameters. + meta = {"parameters": parameters_} + circuit_key = _circuit_key(circuit) + if circuit_key not in self._lin_comb_cache: + # Cache the circuits for the linear combination of unitaries. + # We only cache the circuits for the specified parameters in the future. + self._lin_comb_cache[circuit_key] = _make_lin_comb_gradient_circuit( + circuit, add_measurement=False + ) + lin_comb_circuits = self._lin_comb_cache[circuit_key] + gradient_circuits = [] + for param in parameters_: + gradient_circuits.append(lin_comb_circuits[param]) + n = len(gradient_circuits) + # Make the observable as :class:`~qiskit.quantum_info.SparsePauliOp` and + # add an ancillary operator to compute the gradient. + observable = init_observable(observable) + observable_1, observable_2 = _make_lin_comb_observables( + observable, self._derivative_type + ) + # If its derivative type is `DerivativeType.COMPLEX`, calculate the gradient + # of the real and imaginary parts separately. + meta["derivative_type"] = self.derivative_type + metadata.append(meta) + # Combine inputs into a single job to reduce overhead. + if self._derivative_type == DerivativeType.COMPLEX: + job_circuits.extend(gradient_circuits * 2) + job_observables.extend([observable_1] * n + [observable_2] * n) + job_param_values.extend([parameter_values_] * 2 * n) + all_n.append(2 * n) + else: + job_circuits.extend(gradient_circuits) + job_observables.extend([observable_1] * n) + job_param_values.extend([parameter_values_] * n) + all_n.append(n) + + # Run the single job with all circuits. + job = self._estimator.run( + job_circuits, + job_observables, + job_param_values, + **options, + ) + try: + results = job.result() + except AlgorithmError as exc: + raise AlgorithmError("Estimator job failed.") from exc + + # Compute the gradients. + gradients = [] + partial_sum_n = 0 + for n in all_n: + # this disable is needed as Pylint does not understand derivative_type is a property if + # it is only defined in the base class and the getter is in the child + # pylint: disable=comparison-with-callable + if self.derivative_type == DerivativeType.COMPLEX: + gradient = np.zeros(n // 2, dtype="complex") + gradient.real = results.values[partial_sum_n : partial_sum_n + n // 2] + gradient.imag = results.values[partial_sum_n + n // 2 : partial_sum_n + n] + + else: + gradient = np.real(results.values[partial_sum_n : partial_sum_n + n]) + partial_sum_n += n + gradients.append(gradient) + + opt = self._get_local_options(options) + return EstimatorGradientResult(gradients=gradients, metadata=metadata, options=opt) diff --git a/qiskit_machine_learning/gradients/lin_comb/lin_comb_qgt.py b/qiskit_machine_learning/gradients/lin_comb/lin_comb_qgt.py new file mode 100644 index 000000000..a00c7b670 --- /dev/null +++ b/qiskit_machine_learning/gradients/lin_comb/lin_comb_qgt.py @@ -0,0 +1,258 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +""" +A class for the Linear Combination Quantum Gradient Tensor. +""" + +from __future__ import annotations + +from collections.abc import Sequence + +import numpy as np + +from qiskit.circuit import Parameter, QuantumCircuit +from qiskit.primitives import BaseEstimator +from qiskit.primitives.utils import _circuit_key +from qiskit.providers import Options +from qiskit.quantum_info import SparsePauliOp + +from ..base.base_qgt import BaseQGT +from .lin_comb_estimator_gradient import LinCombEstimatorGradient +from ..base.qgt_result import QGTResult +from ..utils import DerivativeType, _make_lin_comb_qgt_circuit, _make_lin_comb_observables + +from ...exceptions import AlgorithmError + + +class LinCombQGT(BaseQGT): + """Computes the Quantum Geometric Tensor (QGT) given a pure, parameterized quantum state. + + This method employs a linear combination of unitaries [1]. + + **Reference:** + + [1]: Schuld et al., "Evaluating analytic gradients on quantum hardware" (2018). + `arXiv:1811.11184 `_ + """ + + SUPPORTED_GATES = [ + "rx", + "ry", + "rz", + "rzx", + "rzz", + "ryy", + "rxx", + "cx", + "cy", + "cz", + "ccx", + "swap", + "iswap", + "h", + "t", + "s", + "sdg", + "x", + "y", + "z", + ] + + def __init__( + self, + estimator: BaseEstimator, + phase_fix: bool = True, + derivative_type: DerivativeType = DerivativeType.COMPLEX, + options: Options | None = None, + ): + r""" + Args: + estimator: The estimator used to compute the QGT. + phase_fix: Whether to calculate the second term (phase fix) of the QGT, which is + :math:`\langle\partial_i \psi | \psi \rangle \langle\psi | \partial_j \psi \rangle`. + Default to ``True``. + derivative_type: The type of derivative. Can be either ``DerivativeType.REAL`` + ``DerivativeType.IMAG``, or ``DerivativeType.COMPLEX``. Defaults to + ``DerivativeType.REAL``. + + - ``DerivativeType.REAL`` computes + + .. math:: + + \mathrm{Re(QGT)}_{ij}= \mathrm{Re}[\langle \partial_i \psi | \partial_j \psi \rangle + - \langle\partial_i \psi | \psi \rangle \langle\psi | \partial_j \psi \rangle]. + + - ``DerivativeType.IMAG`` computes + + .. math:: + + \mathrm{Re(QGT)}_{ij}= \mathrm{Im}[\langle \partial_i \psi | \partial_j \psi \rangle + - \langle\partial_i \psi | \psi \rangle \langle\psi | \partial_j \psi \rangle]. + + - ``DerivativeType.COMPLEX`` computes + + .. math:: + + \mathrm{QGT}_{ij}= [\langle \partial_i \psi | \partial_j \psi \rangle + - \langle\partial_i \psi | \psi \rangle \langle\psi | \partial_j \psi \rangle]. + + options: Backend runtime options used for circuit execution. The order of priority is: + options in ``run`` method > QGT's default options > primitive's default + setting. Higher priority setting overrides lower priority setting. + """ + super().__init__(estimator, phase_fix, derivative_type, options=options) + self._gradient = LinCombEstimatorGradient( + estimator, derivative_type=DerivativeType.COMPLEX, options=options + ) + self._lin_comb_qgt_circuit_cache: dict[ + tuple, dict[tuple[Parameter, Parameter], QuantumCircuit] + ] = {} + + def _run( + self, + circuits: Sequence[QuantumCircuit], + parameter_values: Sequence[Sequence[float]], + parameters: Sequence[Sequence[Parameter]], + **options, + ) -> QGTResult: + """Compute the QGT on the given circuits.""" + g_circuits, g_parameter_values, g_parameters = self._preprocess( + circuits, parameter_values, parameters, self.SUPPORTED_GATES + ) + results = self._run_unique(g_circuits, g_parameter_values, g_parameters, **options) + return self._postprocess(results, circuits, parameter_values, parameters) + + def _run_unique( + self, + circuits: Sequence[QuantumCircuit], + parameter_values: Sequence[Sequence[float]], + parameters: Sequence[Sequence[Parameter]], + **options, + ) -> QGTResult: + """Compute the QGTs on the given circuits.""" + job_circuits, job_observables, job_param_values, metadata = [], [], [], [] + all_n, all_m = [], [] + phase_fixes: list[int | np.ndarray] = [] + + for circuit, parameter_values_, parameters_ in zip(circuits, parameter_values, parameters): + # Prepare circuits for the gradient of the specified parameters. + parameters_ = [p for p in circuit.parameters if p in parameters_] + meta = {"parameters": parameters_} + metadata.append(meta) + + # Compute the first term in the QGT + circuit_key = _circuit_key(circuit) + if circuit_key not in self._lin_comb_qgt_circuit_cache: + # generate the all of the circuits for the first term in the QGT and cache them. + # Only the circuit related to specified parameters will be executed. + # In the future, we can generate the specified circuits on demand. + self._lin_comb_qgt_circuit_cache[circuit_key] = _make_lin_comb_qgt_circuit(circuit) + lin_comb_qgt_circuits = self._lin_comb_qgt_circuit_cache[circuit_key] + + qgt_circuits = [] + rows, cols = np.triu_indices(len(parameters_)) + for row, col in zip(rows, cols): + param_i = parameters_[row] + param_j = parameters_[col] + qgt_circuits.append(lin_comb_qgt_circuits[(param_i, param_j)]) + + observable = SparsePauliOp.from_list([("I" * circuit.num_qubits, 1)]) + observable_1, observable_2 = _make_lin_comb_observables( + observable, self._derivative_type + ) + + n = len(qgt_circuits) + if self._derivative_type == DerivativeType.COMPLEX: + job_circuits.extend(qgt_circuits * 2) + job_observables.extend([observable_1] * n + [observable_2] * n) + job_param_values.extend([parameter_values_] * 2 * n) + all_m.append(len(parameters_)) + all_n.append(2 * n) + else: + job_circuits.extend(qgt_circuits) + job_observables.extend([observable_1] * n) + job_param_values.extend([parameter_values_] * n) + all_m.append(len(parameters_)) + all_n.append(n) + + # Run the single job with all circuits. + job = self._estimator.run( + job_circuits, + job_observables, + job_param_values, + **options, + ) + + if self._phase_fix: + # Compute the second term in the QGT if phase fix is enabled. + phase_fix_obs = [ + SparsePauliOp.from_list([("I" * circuit.num_qubits, 1)]) for circuit in circuits + ] + phase_fix_job = self._gradient.run( + circuits=circuits, + observables=phase_fix_obs, + parameter_values=parameter_values, + parameters=parameters, + **options, + ) + + try: + results = job.result() + if self._phase_fix: + gradient_results = phase_fix_job.result() + except AlgorithmError as exc: + raise AlgorithmError("Estimator job or gradient job failed.") from exc + + # Compute the phase fix + if self._phase_fix: + for gradient in gradient_results.gradients: + phase_fix = np.outer(np.conjugate(gradient), gradient) + # Select the real or imaginary part of the phase fix if needed + if self.derivative_type == DerivativeType.REAL: + phase_fix = np.real(phase_fix) + elif self.derivative_type == DerivativeType.IMAG: + phase_fix = np.imag(phase_fix) + phase_fixes.append(phase_fix) + else: + phase_fixes = [0 for i in range(len(circuits))] + # Compute the QGT + qgts = [] + partial_sum_n = 0 + for i, (n, m) in enumerate(zip(all_n, all_m)): + qgt = np.zeros((m, m), dtype="complex") + # Compute the first term in the QGT + if self.derivative_type == DerivativeType.COMPLEX: + qgt[np.triu_indices(m)] = results.values[partial_sum_n : partial_sum_n + n // 2] + qgt[np.triu_indices(m)] += ( + 1j * results.values[partial_sum_n + n // 2 : partial_sum_n + n] + ) + elif self.derivative_type == DerivativeType.REAL: + qgt[np.triu_indices(m)] = results.values[partial_sum_n : partial_sum_n + n] + elif self.derivative_type == DerivativeType.IMAG: + qgt[np.triu_indices(m)] = 1j * results.values[partial_sum_n : partial_sum_n + n] + + # Add the conjugate of the upper triangle to the lower triangle + qgt += np.triu(qgt, k=1).conjugate().T + if self.derivative_type == DerivativeType.REAL: + qgt = np.real(qgt) + elif self.derivative_type == DerivativeType.IMAG: + qgt = np.imag(qgt) + + # Subtract the phase fix from the QGT + qgt = qgt - phase_fixes[i] + partial_sum_n += n + qgts.append(qgt / 4) + + opt = self._get_local_options(options) + return QGTResult( + qgts=qgts, derivative_type=self.derivative_type, metadata=metadata, options=opt + ) diff --git a/qiskit_machine_learning/gradients/lin_comb/lin_comb_sampler_gradient.py b/qiskit_machine_learning/gradients/lin_comb/lin_comb_sampler_gradient.py new file mode 100644 index 000000000..30083d538 --- /dev/null +++ b/qiskit_machine_learning/gradients/lin_comb/lin_comb_sampler_gradient.py @@ -0,0 +1,148 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +""" +Gradient of probabilities with linear combination of unitaries (LCU) +""" + +from __future__ import annotations + +from collections import defaultdict +from collections.abc import Sequence + +from qiskit.circuit import Parameter, QuantumCircuit +from qiskit.primitives import BaseSampler +from qiskit.primitives.utils import _circuit_key +from qiskit.providers import Options + +from ..base.base_sampler_gradient import BaseSamplerGradient +from ..base.sampler_gradient_result import SamplerGradientResult +from ..utils import _make_lin_comb_gradient_circuit + +from ...exceptions import AlgorithmError + + +class LinCombSamplerGradient(BaseSamplerGradient): + """Compute the gradients of the sampling probability. + This method employs a linear combination of unitaries [1]. + + **Reference:** + [1] Schuld et al., Evaluating analytic gradients on quantum hardware, 2018 + `arXiv:1811.11184 `_ + """ + + SUPPORTED_GATES = [ + "rx", + "ry", + "rz", + "rzx", + "rzz", + "ryy", + "rxx", + "cx", + "cy", + "cz", + "ccx", + "swap", + "iswap", + "h", + "t", + "s", + "sdg", + "x", + "y", + "z", + ] + + def __init__(self, sampler: BaseSampler, options: Options | None = None): + """ + Args: + sampler: The sampler used to compute the gradients. + options: Primitive backend runtime options used for circuit execution. + The order of priority is: options in ``run`` method > gradient's + default options > primitive's default setting. + Higher priority setting overrides lower priority setting + """ + self._lin_comb_cache: dict[tuple, dict[Parameter, QuantumCircuit]] = {} + super().__init__(sampler, options) + + def _run( + self, + circuits: Sequence[QuantumCircuit], + parameter_values: Sequence[Sequence[float]], + parameters: Sequence[Sequence[Parameter]], + **options, + ) -> SamplerGradientResult: + """Compute the estimator gradients on the given circuits.""" + g_circuits, g_parameter_values, g_parameters = self._preprocess( + circuits, parameter_values, parameters, self.SUPPORTED_GATES + ) + results = self._run_unique(g_circuits, g_parameter_values, g_parameters, **options) + return self._postprocess(results, circuits, parameter_values, parameters) + + def _run_unique( + self, + circuits: Sequence[QuantumCircuit], + parameter_values: Sequence[Sequence[float]], + parameters: Sequence[Sequence[Parameter]], + **options, + ) -> SamplerGradientResult: + """Compute the sampler gradients on the given circuits.""" + job_circuits, job_param_values, metadata = [], [], [] + all_n = [] + for circuit, parameter_values_, parameters_ in zip(circuits, parameter_values, parameters): + # Prepare circuits for the gradient of the specified parameters. + # TODO: why is this not wrapped into another list level like it is done elsewhere? + metadata.append({"parameters": parameters_}) + circuit_key = _circuit_key(circuit) + if circuit_key not in self._lin_comb_cache: + # Cache the circuits for the linear combination of unitaries. + # We only cache the circuits for the specified parameters in the future. + self._lin_comb_cache[circuit_key] = _make_lin_comb_gradient_circuit( + circuit, add_measurement=True + ) + lin_comb_circuits = self._lin_comb_cache[circuit_key] + gradient_circuits = [] + for param in parameters_: + gradient_circuits.append(lin_comb_circuits[param]) + # Combine inputs into a single job to reduce overhead. + n = len(gradient_circuits) + job_circuits.extend(gradient_circuits) + job_param_values.extend([parameter_values_] * n) + all_n.append(n) + + # Run the single job with all circuits. + job = self._sampler.run(job_circuits, job_param_values, **options) + try: + results = job.result() + except Exception as exc: + raise AlgorithmError("Sampler job failed.") from exc + + # Compute the gradients. + gradients = [] + partial_sum_n = 0 + for i, n in enumerate(all_n): + gradient = [] + result = results.quasi_dists[partial_sum_n : partial_sum_n + n] + m = 2 ** circuits[i].num_qubits + for dist in result: + grad_dist: dict[int, float] = defaultdict(float) + for key, value in dist.items(): + if key < m: + grad_dist[key] += value + else: + grad_dist[key - m] -= value + gradient.append(dict(grad_dist)) + gradients.append(gradient) + partial_sum_n += n + + opt = self._get_local_options(options) + return SamplerGradientResult(gradients=gradients, metadata=metadata, options=opt) diff --git a/qiskit_machine_learning/gradients/param_shift/__init__.py b/qiskit_machine_learning/gradients/param_shift/__init__.py new file mode 100644 index 000000000..adb31296c --- /dev/null +++ b/qiskit_machine_learning/gradients/param_shift/__init__.py @@ -0,0 +1,11 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2023 +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. diff --git a/qiskit_machine_learning/gradients/param_shift/param_shift_estimator_gradient.py b/qiskit_machine_learning/gradients/param_shift/param_shift_estimator_gradient.py new file mode 100644 index 000000000..cb3fcf908 --- /dev/null +++ b/qiskit_machine_learning/gradients/param_shift/param_shift_estimator_gradient.py @@ -0,0 +1,122 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +""" +Gradient of probabilities with parameter shift +""" + +from __future__ import annotations + +from collections.abc import Sequence + +from qiskit.circuit import Parameter, QuantumCircuit +from qiskit.quantum_info.operators.base_operator import BaseOperator + +from ..base.base_estimator_gradient import BaseEstimatorGradient +from ..base.estimator_gradient_result import EstimatorGradientResult +from ..utils import _make_param_shift_parameter_values + +from ...exceptions import AlgorithmError + + +class ParamShiftEstimatorGradient(BaseEstimatorGradient): + """ + Compute the gradients of the expectation values by the parameter shift rule [1]. + + **Reference:** + [1] Schuld, M., Bergholm, V., Gogolin, C., Izaac, J., and Killoran, N. Evaluating analytic + gradients on quantum hardware, `DOI `_ + """ + + SUPPORTED_GATES = [ + "x", + "y", + "z", + "h", + "rx", + "ry", + "rz", + "p", + "cx", + "cy", + "cz", + "ryy", + "rxx", + "rzz", + "rzx", + ] + + def _run( + self, + circuits: Sequence[QuantumCircuit], + observables: Sequence[BaseOperator], + parameter_values: Sequence[Sequence[float]], + parameters: Sequence[Sequence[Parameter]], + **options, + ) -> EstimatorGradientResult: + """Compute the gradients of the expectation values by the parameter shift rule.""" + g_circuits, g_parameter_values, g_parameters = self._preprocess( + circuits, parameter_values, parameters, self.SUPPORTED_GATES + ) + results = self._run_unique( + g_circuits, observables, g_parameter_values, g_parameters, **options + ) + return self._postprocess(results, circuits, parameter_values, parameters) + + def _run_unique( + self, + circuits: Sequence[QuantumCircuit], + observables: Sequence[BaseOperator], + parameter_values: Sequence[Sequence[float]], + parameters: Sequence[Sequence[Parameter]], + **options, + ) -> EstimatorGradientResult: + """Compute the estimator gradients on the given circuits.""" + job_circuits, job_observables, job_param_values, metadata = [], [], [], [] + all_n = [] + for circuit, observable, parameter_values_, parameters_ in zip( + circuits, observables, parameter_values, parameters + ): + metadata.append({"parameters": parameters_}) + # Make parameter values for the parameter shift rule. + param_shift_parameter_values = _make_param_shift_parameter_values( + circuit, parameter_values_, parameters_ + ) + # Combine inputs into a single job to reduce overhead. + n = len(param_shift_parameter_values) + job_circuits.extend([circuit] * n) + job_observables.extend([observable] * n) + job_param_values.extend(param_shift_parameter_values) + all_n.append(n) + + # Run the single job with all circuits. + job = self._estimator.run( + job_circuits, + job_observables, + job_param_values, + **options, + ) + try: + results = job.result() + except Exception as exc: + raise AlgorithmError("Estimator job failed.") from exc + + # Compute the gradients. + gradients = [] + partial_sum_n = 0 + for n in all_n: + result = results.values[partial_sum_n : partial_sum_n + n] + gradient_ = (result[: n // 2] - result[n // 2 :]) / 2 + gradients.append(gradient_) + partial_sum_n += n + + opt = self._get_local_options(options) + return EstimatorGradientResult(gradients=gradients, metadata=metadata, options=opt) diff --git a/qiskit_machine_learning/gradients/param_shift/param_shift_sampler_gradient.py b/qiskit_machine_learning/gradients/param_shift/param_shift_sampler_gradient.py new file mode 100644 index 000000000..b27d64873 --- /dev/null +++ b/qiskit_machine_learning/gradients/param_shift/param_shift_sampler_gradient.py @@ -0,0 +1,117 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +""" +Gradient of probabilities with parameter shift +""" + +from __future__ import annotations + +from collections import defaultdict +from collections.abc import Sequence + +from qiskit.circuit import Parameter, QuantumCircuit + +from ..base.base_sampler_gradient import BaseSamplerGradient +from ..base.sampler_gradient_result import SamplerGradientResult +from ..utils import _make_param_shift_parameter_values + +from ...exceptions import AlgorithmError + + +class ParamShiftSamplerGradient(BaseSamplerGradient): + """ + Compute the gradients of the sampling probability by the parameter shift rule [1]. + + **Reference:** + [1] Schuld, M., Bergholm, V., Gogolin, C., Izaac, J., and Killoran, N. Evaluating analytic + gradients on quantum hardware, `DOI `_ + """ + + SUPPORTED_GATES = [ + "x", + "y", + "z", + "h", + "rx", + "ry", + "rz", + "p", + "cx", + "cy", + "cz", + "ryy", + "rxx", + "rzz", + "rzx", + ] + + def _run( + self, + circuits: Sequence[QuantumCircuit], + parameter_values: Sequence[Sequence[float]], + parameters: Sequence[Sequence[Parameter]], + **options, + ) -> SamplerGradientResult: + """Compute the estimator gradients on the given circuits.""" + g_circuits, g_parameter_values, g_parameters = self._preprocess( + circuits, parameter_values, parameters, self.SUPPORTED_GATES + ) + results = self._run_unique(g_circuits, g_parameter_values, g_parameters, **options) + return self._postprocess(results, circuits, parameter_values, parameters) + + def _run_unique( + self, + circuits: Sequence[QuantumCircuit], + parameter_values: Sequence[Sequence[float]], + parameters: Sequence[Sequence[Parameter]], + **options, + ) -> SamplerGradientResult: + """Compute the sampler gradients on the given circuits.""" + job_circuits, job_param_values, metadata = [], [], [] + all_n = [] + for circuit, parameter_values_, parameters_ in zip(circuits, parameter_values, parameters): + metadata.append({"parameters": parameters_}) + # Make parameter values for the parameter shift rule. + param_shift_parameter_values = _make_param_shift_parameter_values( + circuit, parameter_values_, parameters_ + ) + # Combine inputs into a single job to reduce overhead. + n = len(param_shift_parameter_values) + job_circuits.extend([circuit] * n) + job_param_values.extend(param_shift_parameter_values) + all_n.append(n) + + # Run the single job with all circuits. + job = self._sampler.run(job_circuits, job_param_values, **options) + try: + results = job.result() + except Exception as exc: + raise AlgorithmError("Estimator job failed.") from exc + + # Compute the gradients. + gradients = [] + partial_sum_n = 0 + for n in all_n: + gradient = [] + result = results.quasi_dists[partial_sum_n : partial_sum_n + n] + for dist_plus, dist_minus in zip(result[: n // 2], result[n // 2 :]): + grad_dist: dict[int, float] = defaultdict(float) + for key, val in dist_plus.items(): + grad_dist[key] += val / 2 + for key, val in dist_minus.items(): + grad_dist[key] -= val / 2 + gradient.append(dict(grad_dist)) + gradients.append(gradient) + partial_sum_n += n + + opt = self._get_local_options(options) + return SamplerGradientResult(gradients=gradients, metadata=metadata, options=opt) diff --git a/qiskit_machine_learning/gradients/qfi.py b/qiskit_machine_learning/gradients/qfi.py new file mode 100644 index 000000000..4a53b20d9 --- /dev/null +++ b/qiskit_machine_learning/gradients/qfi.py @@ -0,0 +1,171 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2023 +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +""" +A class for the Quantum Fisher Information. +""" + +from __future__ import annotations + +from abc import ABC +from collections.abc import Sequence +from copy import copy + +from qiskit.circuit import Parameter, QuantumCircuit +from qiskit.providers import Options + +from .base.base_qgt import BaseQGT +from .lin_comb.lin_comb_estimator_gradient import DerivativeType +from .qfi_result import QFIResult + +from ..algorithm_job import AlgorithmJob +from ..exceptions import AlgorithmError + + +class QFI(ABC): + r"""Computes the Quantum Fisher Information (QFI) given a pure, + parameterized quantum state. QFI is defined as: + + .. math:: + + \mathrm{QFI}_{ij}= 4 \mathrm{Re}[\langle \partial_i \psi | \partial_j \psi \rangle + - \langle\partial_i \psi | \psi \rangle \langle\psi | \partial_j \psi \rangle]. + """ + + def __init__( + self, + qgt: BaseQGT, + options: Options | None = None, + ): + r""" + Args: + qgt: The quantum geometric tensor used to compute the QFI. + options: Backend runtime options used for circuit execution. The order of priority is: + options in ``run`` method > QFI's default options > primitive's default + setting. Higher priority setting overrides lower priority setting. + """ + self._qgt: BaseQGT = qgt + self._default_options = Options() + if options is not None: + self._default_options.update_options(**options) + + def run( + self, + circuits: Sequence[QuantumCircuit], + parameter_values: Sequence[Sequence[float]], + parameters: Sequence[Sequence[Parameter] | None] | None = None, + **options, + ) -> AlgorithmJob: + """Run the job of the QFIs on the given circuits. + + Args: + circuits: The list of quantum circuits to compute the QFIs. + parameter_values: The list of parameter values to be bound to the circuit. + parameters: The sequence of parameters to calculate only the QFIs of + the specified parameters. Each sequence of parameters corresponds to a circuit in + ``circuits``. Defaults to None, which means that the QFIs of all parameters in + each circuit are calculated. + options: Primitive backend runtime options used for circuit execution. + The order of priority is: options in ``run`` method > QFI's + default options > QGT's default setting. + Higher priority setting overrides lower priority setting. + + Returns: + The job object of the QFIs of the expectation values. The i-th result corresponds to + ``circuits[i]`` evaluated with parameters bound as ``parameter_values[i]``. + """ + + if isinstance(circuits, QuantumCircuit): + # Allow a single circuit to be passed in. + circuits = (circuits,) + + if parameters is None: + # If parameters is None, we calculate the gradients of all parameters in each circuit. + parameters = [circuit.parameters for circuit in circuits] + else: + # If parameters is not None, we calculate the gradients of the specified parameters. + # None in parameters means that the gradients of all parameters in the corresponding + # circuit are calculated. + parameters = [ + params if params is not None else circuits[i].parameters + for i, params in enumerate(parameters) + ] + # The priority of run option is as follows: + # options in ``run`` method > QFI's default options > QGT's default setting. + opts = copy(self._default_options) + opts.update_options(**options) + job = AlgorithmJob(self._run, circuits, parameter_values, parameters, **opts.__dict__) + job.submit() + return job + + def _run( + self, + circuits: Sequence[QuantumCircuit], + parameter_values: Sequence[Sequence[float]], + parameters: Sequence[Sequence[Parameter]], + **options, + ) -> QFIResult: + """Compute the QFI on the given circuits.""" + # Set the derivative type to real + temp_derivative_type, self._qgt.derivative_type = ( + self._qgt.derivative_type, + DerivativeType.REAL, + ) + job = self._qgt.run(circuits, parameter_values, parameters, **options) + + try: + result = job.result() + except AlgorithmError as exc: + raise AlgorithmError("Estimator job or gradient job failed.") from exc + + self._qgt.derivative_type = temp_derivative_type + + return QFIResult( + qfis=[4 * qgt.real for qgt in result.qgts], + metadata=result.metadata, + options=result.options, + ) + + @property + def options(self) -> Options: + """Return the union of QGT's options setting and QFI's default options, + where, if the same field is set in both, the QFI's default options override + the QGT's default setting. + + Returns: + The QFI default + QGT options. + """ + return self._get_local_options(self._default_options.__dict__) + + def update_default_options(self, **options): + """Update the gradient's default options setting. + + Args: + **options: The fields to update the default options. + """ + + self._default_options.update_options(**options) + + def _get_local_options(self, options: Options) -> Options: + """Return the union of the QFI default setting, + the QGT default options, and the options in the ``run`` method. + The order of priority is: options in ``run`` method > QFI's default options > QGT's + default setting. + + Args: + options: The fields to update the options + + Returns: + The QFI default + QGT default + run options. + """ + opts = copy(self._qgt.options) + opts.update_options(**options) + return opts diff --git a/qiskit_machine_learning/gradients/qfi_result.py b/qiskit_machine_learning/gradients/qfi_result.py new file mode 100644 index 000000000..9486c990c --- /dev/null +++ b/qiskit_machine_learning/gradients/qfi_result.py @@ -0,0 +1,35 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +""" +QFI result class +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +import numpy as np + +from qiskit.providers import Options + + +@dataclass(frozen=True) +class QFIResult: + """Result of QFI.""" + + qfis: list[np.ndarray] + """The QFI.""" + metadata: list[dict[str, Any]] + """Additional information about the job.""" + options: Options + """Primitive runtime options for the execution of the job.""" diff --git a/qiskit_machine_learning/gradients/spsa/__init__.py b/qiskit_machine_learning/gradients/spsa/__init__.py new file mode 100644 index 000000000..adb31296c --- /dev/null +++ b/qiskit_machine_learning/gradients/spsa/__init__.py @@ -0,0 +1,11 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2023 +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. diff --git a/qiskit_machine_learning/gradients/spsa/spsa_estimator_gradient.py b/qiskit_machine_learning/gradients/spsa/spsa_estimator_gradient.py new file mode 100644 index 000000000..9ace83afb --- /dev/null +++ b/qiskit_machine_learning/gradients/spsa/spsa_estimator_gradient.py @@ -0,0 +1,134 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Gradient of Sampler with Finite difference method.""" + +from __future__ import annotations + +from collections.abc import Sequence + +import numpy as np + +from qiskit.circuit import Parameter, QuantumCircuit +from qiskit.primitives import BaseEstimator +from qiskit.providers import Options +from qiskit.quantum_info.operators.base_operator import BaseOperator + +from ..base.base_estimator_gradient import BaseEstimatorGradient +from ..base.estimator_gradient_result import EstimatorGradientResult + +from ...exceptions import AlgorithmError + + +class SPSAEstimatorGradient(BaseEstimatorGradient): + """ + Compute the gradients of the expectation value by the Simultaneous Perturbation Stochastic + Approximation (SPSA) [1]. + + **Reference:** + [1] J. C. Spall, Adaptive stochastic approximation by the simultaneous perturbation method in + IEEE Transactions on Automatic Control, vol. 45, no. 10, pp. 1839-1853, Oct 2020, + `doi: 10.1109/TAC.2000.880982 `_ + """ + + def __init__( + self, + estimator: BaseEstimator, + epsilon: float, + batch_size: int = 1, + seed: int | None = None, + options: Options | None = None, + ): + """ + Args: + estimator: The estimator used to compute the gradients. + epsilon: The offset size for the SPSA gradients. + batch_size: The number of gradients to average. + seed: The seed for a random perturbation vector. + options: Primitive backend runtime options used for circuit execution. + The order of priority is: options in ``run`` method > gradient's + default options > primitive's default setting. + Higher priority setting overrides lower priority setting + + Raises: + ValueError: If ``epsilon`` is not positive. + """ + if epsilon <= 0: + raise ValueError(f"epsilon ({epsilon}) should be positive.") + self._epsilon = epsilon + self._batch_size = batch_size + self._seed = np.random.default_rng(seed) + + super().__init__(estimator, options) + + def _run( + self, + circuits: Sequence[QuantumCircuit], + observables: Sequence[BaseOperator], + parameter_values: Sequence[Sequence[float]], + parameters: Sequence[Sequence[Parameter]], + **options, + ) -> EstimatorGradientResult: + """Compute the estimator gradients on the given circuits.""" + job_circuits, job_observables, job_param_values, metadata, offsets = [], [], [], [], [] + all_n = [] + for circuit, observable, parameter_values_, parameters_ in zip( + circuits, observables, parameter_values, parameters + ): + # Indices of parameters to be differentiated. + indices = [circuit.parameters.data.index(p) for p in parameters_] + metadata.append({"parameters": parameters_}) + # Make random perturbation vectors. + offset = [ + (-1) ** (self._seed.integers(0, 2, len(circuit.parameters))) + for _ in range(self._batch_size) + ] + plus = [parameter_values_ + self._epsilon * offset_ for offset_ in offset] + minus = [parameter_values_ - self._epsilon * offset_ for offset_ in offset] + offsets.append(offset) + + # Combine inputs into a single job to reduce overhead. + job_circuits.extend([circuit] * 2 * self._batch_size) + job_observables.extend([observable] * 2 * self._batch_size) + job_param_values.extend(plus + minus) + all_n.append(2 * self._batch_size) + + # Run the single job with all circuits. + job = self._estimator.run( + job_circuits, + job_observables, + job_param_values, + **options, + ) + try: + results = job.result() + except Exception as exc: + raise AlgorithmError("Estimator job failed.") from exc + + # Compute the gradients. + gradients = [] + partial_sum_n = 0 + for i, n in enumerate(all_n): + result = results.values[partial_sum_n : partial_sum_n + n] + partial_sum_n += n + n = len(result) // 2 + diffs = (result[:n] - result[n:]) / (2 * self._epsilon) + # Calculate the gradient for each batch. Note that (``diff`` / ``offset``) is the gradient + # since ``offset`` is a perturbation vector of 1s and -1s. + batch_gradients = np.array([diff / offset for diff, offset in zip(diffs, offsets[i])]) + # Take the average of the batch gradients. + gradient = np.mean(batch_gradients, axis=0) + indices = [circuits[i].parameters.data.index(p) for p in metadata[i]["parameters"]] + gradients.append(gradient[indices]) + + opt = self._get_local_options(options) + return EstimatorGradientResult(gradients=gradients, metadata=metadata, options=opt) diff --git a/qiskit_machine_learning/gradients/spsa/spsa_sampler_gradient.py b/qiskit_machine_learning/gradients/spsa/spsa_sampler_gradient.py new file mode 100644 index 000000000..8a83a64a5 --- /dev/null +++ b/qiskit_machine_learning/gradients/spsa/spsa_sampler_gradient.py @@ -0,0 +1,136 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Gradient of Sampler with Finite difference method.""" + +from __future__ import annotations + +from collections import defaultdict +from collections.abc import Sequence + +import numpy as np + +from qiskit.circuit import Parameter, QuantumCircuit +from qiskit.primitives import BaseSampler +from qiskit.providers import Options + +from ..base.base_sampler_gradient import BaseSamplerGradient +from ..base.sampler_gradient_result import SamplerGradientResult + +from ...exceptions import AlgorithmError + + +class SPSASamplerGradient(BaseSamplerGradient): + """ + Compute the gradients of the sampling probability by the Simultaneous Perturbation Stochastic + Approximation (SPSA) [1]. + + **Reference:** + [1] J. C. Spall, Adaptive stochastic approximation by the simultaneous perturbation method in + IEEE Transactions on Automatic Control, vol. 45, no. 10, pp. 1839-1853, Oct 2020, + `doi: 10.1109/TAC.2000.880982 `_. + """ + + def __init__( + self, + sampler: BaseSampler, + epsilon: float, + batch_size: int = 1, + seed: int | None = None, + options: Options | None = None, + ): + """ + Args: + sampler: The sampler used to compute the gradients. + epsilon: The offset size for the SPSA gradients. + batch_size: number of gradients to average. + seed: The seed for a random perturbation vector. + options: Primitive backend runtime options used for circuit execution. + The order of priority is: options in ``run`` method > gradient's + default options > primitive's default setting. + Higher priority setting overrides lower priority setting + + Raises: + ValueError: If ``epsilon`` is not positive. + """ + if epsilon <= 0: + raise ValueError(f"epsilon ({epsilon}) should be positive.") + self._batch_size = batch_size + self._epsilon = epsilon + self._seed = np.random.default_rng(seed) + + super().__init__(sampler, options) + + def _run( + self, + circuits: Sequence[QuantumCircuit], + parameter_values: Sequence[Sequence[float]], + parameters: Sequence[Sequence[Parameter]], + **options, + ) -> SamplerGradientResult: + """Compute the sampler gradients on the given circuits.""" + job_circuits, job_param_values, metadata, offsets = [], [], [], [] + all_n = [] + for circuit, parameter_values_, parameters_ in zip(circuits, parameter_values, parameters): + # Indices of parameters to be differentiated. + indices = [circuit.parameters.data.index(p) for p in parameters_] + metadata.append({"parameters": parameters_}) + offset = np.array( + [ + (-1) ** (self._seed.integers(0, 2, len(circuit.parameters))) + for _ in range(self._batch_size) + ] + ) + plus = [parameter_values_ + self._epsilon * offset_ for offset_ in offset] + minus = [parameter_values_ - self._epsilon * offset_ for offset_ in offset] + offsets.append(offset) + + # Combine inputs into a single job to reduce overhead. + n = 2 * self._batch_size + job_circuits.extend([circuit] * n) + job_param_values.extend(plus + minus) + all_n.append(n) + + # Run the single job with all circuits. + job = self._sampler.run(job_circuits, job_param_values, **options) + try: + results = job.result() + except Exception as exc: + raise AlgorithmError("Sampler job failed.") from exc + + # Compute the gradients. + gradients = [] + partial_sum_n = 0 + for i, n in enumerate(all_n): + dist_diffs = {} + result = results.quasi_dists[partial_sum_n : partial_sum_n + n] + for j, (dist_plus, dist_minus) in enumerate(zip(result[: n // 2], result[n // 2 :])): + dist_diff: dict[int, float] = defaultdict(float) + for key, value in dist_plus.items(): + dist_diff[key] += value / (2 * self._epsilon) + for key, value in dist_minus.items(): + dist_diff[key] -= value / (2 * self._epsilon) + dist_diffs[j] = dist_diff + gradient = [] + indices = [circuits[i].parameters.data.index(p) for p in metadata[i]["parameters"]] + for j in indices: + gradient_j: dict[int, float] = defaultdict(float) + for k in range(self._batch_size): + for key, value in dist_diffs[k].items(): + gradient_j[key] += value * offsets[i][k][j] + gradient_j = {key: value / self._batch_size for key, value in gradient_j.items()} + gradient.append(gradient_j) + gradients.append(gradient) + partial_sum_n += n + + opt = self._get_local_options(options) + return SamplerGradientResult(gradients=gradients, metadata=metadata, options=opt) diff --git a/qiskit_machine_learning/gradients/utils.py b/qiskit_machine_learning/gradients/utils.py new file mode 100644 index 000000000..2ad9b0286 --- /dev/null +++ b/qiskit_machine_learning/gradients/utils.py @@ -0,0 +1,375 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +Utility functions for gradients +""" + +from __future__ import annotations + +from collections import defaultdict +from collections.abc import Sequence +from dataclasses import dataclass +from enum import Enum + +import numpy as np + +from qiskit.circuit import ( + ClassicalRegister, + Gate, + Instruction, + Parameter, + ParameterExpression, + QuantumCircuit, + QuantumRegister, +) +from qiskit.circuit.library.standard_gates import ( + CXGate, + CYGate, + CZGate, + RXGate, + RXXGate, + RYGate, + RYYGate, + RZGate, + RZXGate, + RZZGate, + XGate, +) +from qiskit.quantum_info import SparsePauliOp + + +################################################################################ +## Gradient circuits and Enum +################################################################################ +class DerivativeType(Enum): + """Types of derivative.""" + + REAL = "real" + IMAG = "imag" + COMPLEX = "complex" + + +@dataclass +class GradientCircuit: + """Gradient circuit with unique parameters and mapping information.""" + + gradient_circuit: QuantumCircuit + """An internal quantum circuit with unique parameters used to calculate the gradient""" + parameter_map: dict[Parameter, list[tuple[Parameter, float | ParameterExpression]]] + """A dictionary maps the parameters of ``circuit`` to the parameters of ``gradient_circuit`` with + coefficients""" + gradient_parameter_map: dict[Parameter, ParameterExpression] + """A dictionary maps the parameters of ``gradient_circuit`` to the parameter expressions of + ``circuit``""" + + +@dataclass +class LinearCombGradientCircuit: + """Gradient circuit for the linear combination of unitaries method.""" + + gradient_circuit: QuantumCircuit + """A gradient circuit for the linear combination of unitaries method.""" + coeff: float | ParameterExpression + """A coefficient corresponds to the gradient circuit.""" + + +################################################################################ +## Parameter shift gradient +################################################################################ +def _make_param_shift_parameter_values( # pylint: disable=invalid-name + circuit: QuantumCircuit, + parameter_values: np.ndarray | Sequence[float], + parameters: Sequence[Parameter], +) -> list[np.ndarray]: + """Returns a list of parameter values with offsets for parameter shift rule. + + Args: + circuit: The original quantum circuit + parameter_values: parameter values to be added to the base parameter values. + parameters: The parameters to be shifted. + + Returns: + A list of parameter values with offsets for parameter shift rule. + """ + indices = [circuit.parameters.data.index(p) for p in parameters] + offset = np.identity(circuit.num_parameters)[indices, :] + plus_offsets = parameter_values + offset * np.pi / 2 + minus_offsets = parameter_values - offset * np.pi / 2 + return plus_offsets.tolist() + minus_offsets.tolist() + + +################################################################################ +## Linear combination gradient and Linear combination QGT +################################################################################ +def _make_lin_comb_gradient_circuit( + circuit: QuantumCircuit, add_measurement: bool = False +) -> dict[Parameter, QuantumCircuit]: + """Makes a circuit that computes the linear combination of the gradient circuits.""" + circuit_temp = circuit.copy() + qr_aux = QuantumRegister(1, "qr_aux") + cr_aux = ClassicalRegister(1, "cr_aux") + circuit_temp.add_register(qr_aux) + circuit_temp.add_register(cr_aux) + circuit_temp.h(qr_aux) + circuit_temp.data.insert(0, circuit_temp.data.pop()) + circuit_temp.sdg(qr_aux) + circuit_temp.data.insert(1, circuit_temp.data.pop()) + + lin_comb_circuits = {} + for i, instruction in enumerate(circuit_temp.data): + if instruction.operation.is_parameterized(): + for p in instruction.operation.params[0].parameters: # pylint: disable=invalid-name + gate = _gate_gradient(instruction.operation) + lin_comb_circuit = circuit_temp.copy() + # insert `gate` to i-th position + lin_comb_circuit.append(gate, [qr_aux[0]] + list(instruction.qubits), []) + lin_comb_circuit.data.insert(i, lin_comb_circuit.data.pop()) + lin_comb_circuit.h(qr_aux) + if add_measurement: + lin_comb_circuit.measure(qr_aux, cr_aux) + lin_comb_circuits[p] = lin_comb_circuit + + return lin_comb_circuits + + +def _gate_gradient(gate: Gate) -> Instruction: + """Returns the derivative of the gate""" + # pylint: disable=too-many-return-statements + if isinstance(gate, RXGate): + return CXGate() + if isinstance(gate, RYGate): + return CYGate() + if isinstance(gate, RZGate): + return CZGate() + if isinstance(gate, RXXGate): + cxx_circ = QuantumCircuit(3) + cxx_circ.cx(0, 1) + cxx_circ.cx(0, 2) + cxx = cxx_circ.to_instruction() + return cxx + if isinstance(gate, RYYGate): + cyy_circ = QuantumCircuit(3) + cyy_circ.cy(0, 1) + cyy_circ.cy(0, 2) + cyy = cyy_circ.to_instruction() + return cyy + if isinstance(gate, RZZGate): + czz_circ = QuantumCircuit(3) + czz_circ.cz(0, 1) + czz_circ.cz(0, 2) + czz = czz_circ.to_instruction() + return czz + if isinstance(gate, RZXGate): + czx_circ = QuantumCircuit(3) + czx_circ.cx(0, 2) + czx_circ.cz(0, 1) + czx = czx_circ.to_instruction() + return czx + raise TypeError(f"Unrecognized parameterized gate, {gate}") + + +def _make_lin_comb_qgt_circuit( + circuit: QuantumCircuit, add_measurement: bool = False +) -> dict[tuple[Parameter, Parameter], QuantumCircuit]: + """Makes a circuit that computes the linear combination of the QGT circuits.""" + circuit_temp = circuit.copy() + qr_aux = QuantumRegister(1, "aux") + circuit_temp.add_register(qr_aux) + if add_measurement: + cr_aux = ClassicalRegister(1, "aux") + circuit_temp.add_bits(cr_aux) + circuit_temp.h(qr_aux) + circuit_temp.data.insert(0, circuit_temp.data.pop()) + + lin_comb_qgt_circuits = {} + for i, instruction_i in enumerate(circuit_temp.data): + if not instruction_i.operation.is_parameterized(): + continue + for j, instruction_j in enumerate(circuit_temp.data): + if not instruction_j.operation.is_parameterized(): + continue + # Calculate the QGT of the i-th gate with respect to the j-th gate. + param_i = instruction_i.operation.params[0] + param_j = instruction_j.operation.params[0] + + for p_i in param_i.parameters: + for p_j in param_j.parameters: + if circuit_temp.parameters.data.index(p_i) > circuit_temp.parameters.data.index( + p_j + ): + continue + gate_i = _gate_gradient(instruction_i.operation) + gate_j = _gate_gradient(instruction_j.operation) + lin_comb_qgt_circuit = circuit_temp.copy() + if i < j: + # insert gate_j to j-th position + lin_comb_qgt_circuit.append( + gate_j, [qr_aux[0]] + list(instruction_j.qubits), [] + ) + lin_comb_qgt_circuit.data.insert(j, lin_comb_qgt_circuit.data.pop()) + # insert gate_i to i-th position with two X gates at its sides + lin_comb_qgt_circuit.append(XGate(), [qr_aux[0]], []) + lin_comb_qgt_circuit.data.insert(i, lin_comb_qgt_circuit.data.pop()) + lin_comb_qgt_circuit.append( + gate_i, [qr_aux[0]] + list(instruction_i.qubits), [] + ) + lin_comb_qgt_circuit.data.insert(i, lin_comb_qgt_circuit.data.pop()) + lin_comb_qgt_circuit.append(XGate(), [qr_aux[0]], []) + lin_comb_qgt_circuit.data.insert(i, lin_comb_qgt_circuit.data.pop()) + else: + # insert gate_i to i-th position + lin_comb_qgt_circuit.append( + gate_i, [qr_aux[0]] + list(instruction_i.qubits), [] + ) + lin_comb_qgt_circuit.data.insert(i, lin_comb_qgt_circuit.data.pop()) + # insert gate_j to j-th position with two X gates at its sides + lin_comb_qgt_circuit.append(XGate(), [qr_aux[0]], []) + lin_comb_qgt_circuit.data.insert(j, lin_comb_qgt_circuit.data.pop()) + lin_comb_qgt_circuit.append( + gate_j, [qr_aux[0]] + list(instruction_j.qubits), [] + ) + lin_comb_qgt_circuit.data.insert(j, lin_comb_qgt_circuit.data.pop()) + lin_comb_qgt_circuit.append(XGate(), [qr_aux[0]], []) + lin_comb_qgt_circuit.data.insert(j, lin_comb_qgt_circuit.data.pop()) + + lin_comb_qgt_circuit.h(qr_aux) + if add_measurement: + lin_comb_qgt_circuit.measure(qr_aux, cr_aux) + lin_comb_qgt_circuits[(p_i, p_j)] = lin_comb_qgt_circuit + + return lin_comb_qgt_circuits + + +def _make_lin_comb_observables( + observable: SparsePauliOp, + derivative_type: DerivativeType, +) -> tuple[SparsePauliOp, SparsePauliOp | None]: + """Make the observable with an ancillary operator for the linear combination gradient. + + Args: + observable: The observable. + derivative_type: The type of derivative. Can be either ``DerivativeType.REAL`` + ``DerivativeType.IMAG``, or ``DerivativeType.COMPLEX``. + + Returns: + The observable with an ancillary operator for the linear combination gradient. + + Raises: + ValueError: If the derivative type is not supported. + """ + if derivative_type == DerivativeType.REAL: + return observable.expand(SparsePauliOp.from_list([("Z", 1)])), None + elif derivative_type == DerivativeType.IMAG: + return observable.expand(SparsePauliOp.from_list([("Y", -1)])), None + elif derivative_type == DerivativeType.COMPLEX: + return observable.expand(SparsePauliOp.from_list([("Z", 1)])), observable.expand( + SparsePauliOp.from_list([("Y", -1)]) + ) + else: + raise ValueError(f"Derivative type {derivative_type} is not supported.") + + +################################################################################ +## Preprocess +################################################################################ +def _assign_unique_parameters( + circuit: QuantumCircuit, +) -> GradientCircuit: + """Assign unique parameters to the circuit. + + Args: + circuit: The circuit to assign unique parameters. + + Returns: + The circuit with unique parameters and the mapping from the original parameters to the + unique parameters. + """ + gradient_circuit = circuit.copy_empty_like(f"{circuit.name}_gradient") + parameter_map = defaultdict(list) + gradient_parameter_map = {} + num_gradient_parameters = 0 + for instruction in circuit.data: + if instruction.operation.is_parameterized(): + new_op_params = [] + for angle in instruction.operation.params: + new_parameter = Parameter(f"__gθ{num_gradient_parameters}") + new_op_params.append(new_parameter) + num_gradient_parameters += 1 + for parameter in angle.parameters: + parameter_map[parameter].append((new_parameter, angle.gradient(parameter))) + gradient_parameter_map[new_parameter] = angle + instruction.operation.params = new_op_params + gradient_circuit.append(instruction.operation, instruction.qubits, instruction.clbits) + # For the global phase + gradient_circuit.global_phase = circuit.global_phase + if isinstance(gradient_circuit.global_phase, ParameterExpression): + substitution_map = {} + for parameter in gradient_circuit.global_phase.parameters: + if parameter in parameter_map: + substitution_map[parameter] = parameter_map[parameter][0][0] + else: + new_parameter = Parameter(f"__gθ{num_gradient_parameters}") + substitution_map[parameter] = new_parameter + parameter_map[parameter].append((new_parameter, 1)) + num_gradient_parameters += 1 + gradient_circuit.global_phase = gradient_circuit.global_phase.subs(substitution_map) + return GradientCircuit(gradient_circuit, parameter_map, gradient_parameter_map) + + +def _make_gradient_parameter_values( + circuit: QuantumCircuit, + gradient_circuit: GradientCircuit, + parameter_values: np.ndarray | Sequence[float], +) -> np.ndarray: + """Makes parameter values for the gradient circuit. + + Args: + circuit: The original quantum circuit + gradient_circuit: The gradient circuit + parameter_values: The parameter values for the original circuit + parameter_set: The parameter set to calculate gradients + + Returns: + The parameter values for the gradient circuit. + """ + g_circuit = gradient_circuit.gradient_circuit + g_parameter_values = np.empty(len(g_circuit.parameters)) + for i, g_parameter in enumerate(g_circuit.parameters): + expr = gradient_circuit.gradient_parameter_map[g_parameter] + bound_expr = expr.bind( + {p: parameter_values[circuit.parameters.data.index(p)] for p in expr.parameters} + ) + g_parameter_values[i] = float(bound_expr) + return g_parameter_values + + +def _make_gradient_parameters( + gradient_circuit: GradientCircuit, + parameters: Sequence[Parameter], +) -> Sequence[Parameter]: + """Makes parameter set for the gradient circuit. + + Args: + gradient_circuit: The gradient circuit + parameters: The parameters in the original circuit to calculate gradients + + Returns: + The parameters in the gradient circuit to calculate gradients. + """ + g_parameters = [ + g_parameter + for parameter in parameters + for g_parameter, _ in gradient_circuit.parameter_map[parameter] + ] + # make g_parameters unique and return it. + return list(dict.fromkeys(g_parameters)) diff --git a/qiskit_machine_learning/kernels/algorithms/quantum_kernel_trainer.py b/qiskit_machine_learning/kernels/algorithms/quantum_kernel_trainer.py index 4d272b2ac..897459fa6 100644 --- a/qiskit_machine_learning/kernels/algorithms/quantum_kernel_trainer.py +++ b/qiskit_machine_learning/kernels/algorithms/quantum_kernel_trainer.py @@ -18,12 +18,11 @@ import numpy as np -from qiskit_algorithms.optimizers import Optimizer, SPSA, Minimizer -from qiskit_algorithms.utils import algorithm_globals -from qiskit_algorithms.variational_algorithm import VariationalResult -from qiskit_machine_learning.utils.loss_functions import KernelLoss, SVCLoss - -from qiskit_machine_learning.kernels import TrainableKernel +from ...optimizers import Optimizer, SPSA, Minimizer +from ...utils import algorithm_globals +from ...variational_algorithm import VariationalResult +from ...utils.loss_functions import KernelLoss, SVCLoss +from ...kernels import TrainableKernel class QuantumKernelTrainerResult(VariationalResult): @@ -200,7 +199,7 @@ def fit( # Randomly initialize the initial point if one was not passed if self._initial_point is None: - self._initial_point = algorithm_globals.random.random(num_params) + self._initial_point = algorithm_globals.random().random(num_params) # Perform kernel optimization loss_function = partial( diff --git a/qiskit_machine_learning/kernels/fidelity_quantum_kernel.py b/qiskit_machine_learning/kernels/fidelity_quantum_kernel.py index fc72b9635..31a7cf426 100644 --- a/qiskit_machine_learning/kernels/fidelity_quantum_kernel.py +++ b/qiskit_machine_learning/kernels/fidelity_quantum_kernel.py @@ -19,7 +19,7 @@ import numpy as np from qiskit import QuantumCircuit from qiskit.primitives import Sampler -from qiskit_algorithms.state_fidelities import BaseStateFidelity, ComputeUncompute +from ..state_fidelities import BaseStateFidelity, ComputeUncompute from .base_kernel import BaseKernel diff --git a/qiskit_machine_learning/kernels/fidelity_statevector_kernel.py b/qiskit_machine_learning/kernels/fidelity_statevector_kernel.py index 1a15feef3..9da23ca70 100644 --- a/qiskit_machine_learning/kernels/fidelity_statevector_kernel.py +++ b/qiskit_machine_learning/kernels/fidelity_statevector_kernel.py @@ -20,7 +20,7 @@ from qiskit import QuantumCircuit from qiskit.quantum_info import Statevector -from qiskit_algorithms.utils import algorithm_globals +from ..utils import algorithm_globals from .base_kernel import BaseKernel @@ -154,7 +154,7 @@ def _compute_fidelity(x: np.ndarray, y: np.ndarray) -> float: return np.abs(np.conj(x) @ y) ** 2 def _add_shot_noise(self, fidelity: float) -> float: - return algorithm_globals.random.binomial(n=self._shots, p=fidelity) / self._shots + return algorithm_globals.random().binomial(n=self._shots, p=fidelity) / self._shots def clear_cache(self): """Clear the statevector cache.""" diff --git a/qiskit_machine_learning/kernels/trainable_fidelity_quantum_kernel.py b/qiskit_machine_learning/kernels/trainable_fidelity_quantum_kernel.py index 92d75c5f3..c84b1ce8e 100644 --- a/qiskit_machine_learning/kernels/trainable_fidelity_quantum_kernel.py +++ b/qiskit_machine_learning/kernels/trainable_fidelity_quantum_kernel.py @@ -19,7 +19,7 @@ import numpy as np from qiskit import QuantumCircuit from qiskit.circuit import Parameter, ParameterVector -from qiskit_algorithms.state_fidelities import BaseStateFidelity +from ..state_fidelities import BaseStateFidelity from .fidelity_quantum_kernel import FidelityQuantumKernel, KernelIndices from .trainable_kernel import TrainableKernel diff --git a/qiskit_machine_learning/neural_networks/effective_dimension.py b/qiskit_machine_learning/neural_networks/effective_dimension.py index 6a6feea12..4c32a92f2 100644 --- a/qiskit_machine_learning/neural_networks/effective_dimension.py +++ b/qiskit_machine_learning/neural_networks/effective_dimension.py @@ -18,8 +18,8 @@ import numpy as np from scipy.special import logsumexp -from qiskit_algorithms.utils import algorithm_globals -from qiskit_machine_learning import QiskitMachineLearningError +from ..utils import algorithm_globals +from ..exceptions import QiskitMachineLearningError from .estimator_qnn import EstimatorQNN from .neural_network import NeuralNetwork @@ -83,7 +83,7 @@ def weight_samples(self, weight_samples: Union[np.ndarray, int]) -> None: """Sets network weight samples.""" if isinstance(weight_samples, int): # random sampling from uniform distribution - self._weight_samples = algorithm_globals.random.uniform( + self._weight_samples = algorithm_globals.random().uniform( 0, 1, size=(weight_samples, self._model.num_weights) ) else: @@ -109,7 +109,7 @@ def input_samples(self, input_samples: Union[np.ndarray, int]) -> None: """Sets network input samples.""" if isinstance(input_samples, int): # random sampling from normal distribution - self._input_samples = algorithm_globals.random.normal( + self._input_samples = algorithm_globals.random().normal( 0, 1, size=(input_samples, self._model.num_inputs) ) else: @@ -332,7 +332,7 @@ def weight_samples(self, weight_samples: Union[np.ndarray, int]) -> None: """Sets network parameters.""" if isinstance(weight_samples, int): # random sampling from uniform distribution - self._weight_samples = algorithm_globals.random.uniform( + self._weight_samples = algorithm_globals.random().uniform( 0, 1, size=(1, self._model.num_weights) ) else: diff --git a/qiskit_machine_learning/neural_networks/estimator_qnn.py b/qiskit_machine_learning/neural_networks/estimator_qnn.py index d0c8d2d7d..7d4223924 100644 --- a/qiskit_machine_learning/neural_networks/estimator_qnn.py +++ b/qiskit_machine_learning/neural_networks/estimator_qnn.py @@ -23,14 +23,14 @@ from qiskit.primitives import BaseEstimator, Estimator, EstimatorResult from qiskit.quantum_info import SparsePauliOp from qiskit.quantum_info.operators.base_operator import BaseOperator -from qiskit_algorithms.gradients import ( +from ..gradients import ( BaseEstimatorGradient, EstimatorGradientResult, ParamShiftEstimatorGradient, ) -from qiskit_machine_learning.circuit.library import QNNCircuit -from qiskit_machine_learning.exceptions import QiskitMachineLearningError +from ..circuit.library import QNNCircuit +from ..exceptions import QiskitMachineLearningError from .neural_network import NeuralNetwork diff --git a/qiskit_machine_learning/neural_networks/sampler_qnn.py b/qiskit_machine_learning/neural_networks/sampler_qnn.py index 61930e2fa..4254a8881 100644 --- a/qiskit_machine_learning/neural_networks/sampler_qnn.py +++ b/qiskit_machine_learning/neural_networks/sampler_qnn.py @@ -21,14 +21,14 @@ import numpy as np from qiskit.circuit import Parameter, QuantumCircuit from qiskit.primitives import BaseSampler, SamplerResult, Sampler -from qiskit_algorithms.gradients import ( +from ..gradients import ( BaseSamplerGradient, ParamShiftSamplerGradient, SamplerGradientResult, ) -from qiskit_machine_learning.circuit.library import QNNCircuit -from qiskit_machine_learning.exceptions import QiskitMachineLearningError +from ..circuit.library import QNNCircuit +from ..exceptions import QiskitMachineLearningError import qiskit_machine_learning.optionals as _optionals from .neural_network import NeuralNetwork diff --git a/qiskit_machine_learning/optimizers/__init__.py b/qiskit_machine_learning/optimizers/__init__.py new file mode 100644 index 000000000..c196c85bf --- /dev/null +++ b/qiskit_machine_learning/optimizers/__init__.py @@ -0,0 +1,182 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2018, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +Optimizers (:mod:`qiskit_algorithms.optimizers`) +================================================ +Classical Optimizers. + +This package contains a variety of classical optimizers and were designed for use by +qiskit_algorithm's quantum variational algorithms, such as :class:`~qiskit_algorithms.VQE`. +Logically, these optimizers can be divided into two categories: + +`Local Optimizers`_ + Given an optimization problem, a **local optimizer** is a function + that attempts to find an optimal value within the neighboring set of a candidate solution. + +`Global Optimizers`_ + Given an optimization problem, a **global optimizer** is a function + that attempts to find an optimal value among all possible solutions. + +.. currentmodule:: qiskit_algorithms.optimizers + +Optimizer Base Classes +---------------------- + +.. autosummary:: + :toctree: ../stubs/ + :nosignatures: + + OptimizerResult + Optimizer + Minimizer + +Steppable Optimization +---------------------- + +.. autosummary:: + :toctree: ../stubs/ + + optimizer_utils + +.. autosummary:: + :toctree: ../stubs/ + :nosignatures: + + SteppableOptimizer + AskData + TellData + OptimizerState + + +Local Optimizers +---------------- + +.. autosummary:: + :toctree: ../stubs/ + :nosignatures: + + ADAM + AQGD + CG + COBYLA + L_BFGS_B + GSLS + GradientDescent + GradientDescentState + NELDER_MEAD + NFT + P_BFGS + POWELL + SLSQP + SPSA + QNSPSA + TNC + SciPyOptimizer + UMDA + +Qiskit also provides the following optimizers, which are built-out using the optimizers from +`scikit-quant `_. The ``scikit-quant`` package +is not installed by default but must be explicitly installed, if desired, by the user. The +optimizers therein are provided under various licenses, hence it has been made an optional install. +To install the ``scikit-quant`` dependent package you can use ``pip install scikit-quant``. + +.. autosummary:: + :toctree: ../stubs/ + :nosignatures: + + BOBYQA + IMFIL + SNOBFIT + +Global Optimizers +----------------- +The global optimizers here all use `NLOpt `_ for their +core function and can only be used if the optional dependent ``NLOpt`` package is installed. +To install the ``NLOpt`` dependent package you can use ``pip install nlopt``. + +.. autosummary:: + :toctree: ../stubs/ + :nosignatures: + + CRS + DIRECT_L + DIRECT_L_RAND + ESCH + ISRES + +""" + +from .adam_amsgrad import ADAM +from .aqgd import AQGD +from .bobyqa import BOBYQA +from .cg import CG +from .cobyla import COBYLA +from .gsls import GSLS +from .gradient_descent import GradientDescent, GradientDescentState +from .imfil import IMFIL +from .l_bfgs_b import L_BFGS_B +from .nelder_mead import NELDER_MEAD +from .nft import NFT +from .nlopts.crs import CRS +from .nlopts.direct_l import DIRECT_L +from .nlopts.direct_l_rand import DIRECT_L_RAND +from .nlopts.esch import ESCH +from .nlopts.isres import ISRES +from .steppable_optimizer import SteppableOptimizer, AskData, TellData, OptimizerState +from .optimizer import Minimizer, Optimizer, OptimizerResult, OptimizerSupportLevel +from .p_bfgs import P_BFGS +from .powell import POWELL +from .qnspsa import QNSPSA +from .scipy_optimizer import SciPyOptimizer +from .slsqp import SLSQP +from .snobfit import SNOBFIT +from .spsa import SPSA +from .tnc import TNC +from .umda import UMDA + +__all__ = [ + "Optimizer", + "OptimizerSupportLevel", + "SteppableOptimizer", + "AskData", + "TellData", + "OptimizerState", + "OptimizerResult", + "Minimizer", + "ADAM", + "AQGD", + "CG", + "COBYLA", + "GSLS", + "GradientDescent", + "GradientDescentState", + "L_BFGS_B", + "NELDER_MEAD", + "NFT", + "P_BFGS", + "POWELL", + "SciPyOptimizer", + "SLSQP", + "SPSA", + "QNSPSA", + "TNC", + "CRS", + "DIRECT_L", + "DIRECT_L_RAND", + "ESCH", + "ISRES", + "SNOBFIT", + "BOBYQA", + "IMFIL", + "UMDA", +] diff --git a/qiskit_machine_learning/optimizers/adam_amsgrad.py b/qiskit_machine_learning/optimizers/adam_amsgrad.py new file mode 100644 index 000000000..6deea91b9 --- /dev/null +++ b/qiskit_machine_learning/optimizers/adam_amsgrad.py @@ -0,0 +1,252 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2019, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""The Adam and AMSGRAD optimizers.""" +from __future__ import annotations + +from collections.abc import Callable +from typing import Any +import os + +import csv +import numpy as np +from .optimizer import Optimizer, OptimizerSupportLevel, OptimizerResult, POINT + +# pylint: disable=invalid-name + + +class ADAM(Optimizer): + """Adam and AMSGRAD optimizers. + + Adam [1] is a gradient-based optimization algorithm that is relies on adaptive estimates of + lower-order moments. The algorithm requires little memory and is invariant to diagonal + rescaling of the gradients. Furthermore, it is able to cope with non-stationary objective + functions and noisy and/or sparse gradients. + + AMSGRAD [2] (a variant of Adam) uses a 'long-term memory' of past gradients and, thereby, + improves convergence properties. + + References: + + [1]: Kingma, Diederik & Ba, Jimmy (2014), Adam: A Method for Stochastic Optimization. + `arXiv:1412.6980 `_ + + [2]: Sashank J. Reddi and Satyen Kale and Sanjiv Kumar (2018), + On the Convergence of Adam and Beyond. + `arXiv:1904.09237 `_ + """ + + _OPTIONS = [ + "maxiter", + "tol", + "lr", + "beta_1", + "beta_2", + "noise_factor", + "eps", + "amsgrad", + "snapshot_dir", + ] + + def __init__( + self, + maxiter: int = 10000, + tol: float = 1e-6, + lr: float = 1e-3, + beta_1: float = 0.9, + beta_2: float = 0.99, + noise_factor: float = 1e-8, + eps: float = 1e-10, + amsgrad: bool = False, + snapshot_dir: str | None = None, + ) -> None: + """ + Args: + maxiter: Maximum number of iterations + tol: Tolerance for termination + lr: Value >= 0, Learning rate. + beta_1: Value in range 0 to 1, Generally close to 1. + beta_2: Value in range 0 to 1, Generally close to 1. + noise_factor: Value >= 0, Noise factor + eps : Value >=0, Epsilon to be used for finite differences if no analytic + gradient method is given. + amsgrad: True to use AMSGRAD, False if not + snapshot_dir: If not None save the optimizer's parameter + after every step to the given directory + """ + super().__init__() + for k, v in list(locals().items()): + if k in self._OPTIONS: + self._options[k] = v + self._maxiter = maxiter + self._snapshot_dir = snapshot_dir + self._tol = tol + self._lr = lr + self._beta_1 = beta_1 + self._beta_2 = beta_2 + self._noise_factor = noise_factor + self._eps = eps + self._amsgrad = amsgrad + + # runtime variables + self._t = 0 # time steps + self._m = np.zeros(1) + self._v = np.zeros(1) + if self._amsgrad: + self._v_eff = np.zeros(1) + + if self._snapshot_dir: + # pylint: disable=unspecified-encoding + with open(os.path.join(self._snapshot_dir, "adam_params.csv"), mode="w") as csv_file: + if self._amsgrad: + fieldnames = ["v", "v_eff", "m", "t"] + else: + fieldnames = ["v", "m", "t"] + writer = csv.DictWriter(csv_file, fieldnames=fieldnames) + writer.writeheader() + + @property + def settings(self) -> dict[str, Any]: + return { + "maxiter": self._maxiter, + "tol": self._tol, + "lr": self._lr, + "beta_1": self._beta_1, + "beta_2": self._beta_2, + "noise_factor": self._noise_factor, + "eps": self._eps, + "amsgrad": self._amsgrad, + "snapshot_dir": self._snapshot_dir, + } + + def get_support_level(self): + """Return support level dictionary""" + return { + "gradient": OptimizerSupportLevel.supported, + "bounds": OptimizerSupportLevel.ignored, + "initial_point": OptimizerSupportLevel.supported, + } + + def save_params(self, snapshot_dir: str) -> None: + """Save the current iteration parameters to a file called ``adam_params.csv``. + + Note: + + The current parameters are appended to the file, if it exists already. + The file is not overwritten. + + Args: + snapshot_dir: The directory to store the file in. + """ + if self._amsgrad: + # pylint: disable=unspecified-encoding + with open(os.path.join(snapshot_dir, "adam_params.csv"), mode="a") as csv_file: + fieldnames = ["v", "v_eff", "m", "t"] + writer = csv.DictWriter(csv_file, fieldnames=fieldnames) + writer.writerow({"v": self._v, "v_eff": self._v_eff, "m": self._m, "t": self._t}) + else: + # pylint: disable=unspecified-encoding + with open(os.path.join(snapshot_dir, "adam_params.csv"), mode="a") as csv_file: + fieldnames = ["v", "m", "t"] + writer = csv.DictWriter(csv_file, fieldnames=fieldnames) + writer.writerow({"v": self._v, "m": self._m, "t": self._t}) + + def load_params(self, load_dir: str) -> None: + """Load iteration parameters for a file called ``adam_params.csv``. + + Args: + load_dir: The directory containing ``adam_params.csv``. + """ + # pylint: disable=unspecified-encoding + with open(os.path.join(load_dir, "adam_params.csv")) as csv_file: + if self._amsgrad: + fieldnames = ["v", "v_eff", "m", "t"] + else: + fieldnames = ["v", "m", "t"] + reader = csv.DictReader(csv_file, fieldnames=fieldnames) + for line in reader: + v = line["v"] + if self._amsgrad: + v_eff = line["v_eff"] + m = line["m"] + t = line["t"] + + v = v[1:-1] + self._v = np.fromstring(v, dtype=float, sep=" ") + if self._amsgrad: + v_eff = v_eff[1:-1] + self._v_eff = np.fromstring(v_eff, dtype=float, sep=" ") + m = m[1:-1] + self._m = np.fromstring(m, dtype=float, sep=" ") + t = t[1:-1] + self._t = int(np.fromstring(t, dtype=int, sep=" ")) + + def minimize( + self, + fun: Callable[[POINT], float], + x0: POINT, + jac: Callable[[POINT], POINT] | None = None, + bounds: list[tuple[float, float]] | None = None, + ) -> OptimizerResult: + """Minimize the scalar function. + + Args: + fun: The scalar function to minimize. + x0: The initial point for the minimization. + jac: The gradient of the scalar function ``fun``. + bounds: Bounds for the variables of ``fun``. This argument might be ignored if the + optimizer does not support bounds. + Returns: + The result of the optimization, containing e.g. the result as attribute ``x``. + """ + if jac is None: + jac = Optimizer.wrap_function(Optimizer.gradient_num_diff, (fun, self._eps)) + + derivative = jac(x0) + self._t = 0 + self._m = np.zeros(np.shape(derivative)) + self._v = np.zeros(np.shape(derivative)) + if self._amsgrad: + self._v_eff = np.zeros(np.shape(derivative)) + + params = params_new = x0 + while self._t < self._maxiter: + if self._t > 0: + derivative = jac(params) + self._t += 1 + self._m = self._beta_1 * self._m + (1 - self._beta_1) * derivative + self._v = self._beta_2 * self._v + (1 - self._beta_2) * derivative * derivative + lr_eff = self._lr * np.sqrt(1 - self._beta_2**self._t) / (1 - self._beta_1**self._t) + if not self._amsgrad: + params_new = params - lr_eff * self._m.flatten() / ( + np.sqrt(self._v.flatten()) + self._noise_factor + ) + else: + self._v_eff = np.maximum(self._v_eff, self._v) + params_new = params - lr_eff * self._m.flatten() / ( + np.sqrt(self._v_eff.flatten()) + self._noise_factor + ) + + if self._snapshot_dir: + self.save_params(self._snapshot_dir) + + # check termination + if np.linalg.norm(params - params_new) < self._tol: + break + + params = params_new + + result = OptimizerResult() + result.x = params_new + result.fun = fun(params_new) + result.nfev = self._t + return result diff --git a/qiskit_machine_learning/optimizers/aqgd.py b/qiskit_machine_learning/optimizers/aqgd.py new file mode 100644 index 000000000..2e49c83d8 --- /dev/null +++ b/qiskit_machine_learning/optimizers/aqgd.py @@ -0,0 +1,374 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2019, 2024. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Analytical Quantum Gradient Descent (AQGD) optimizer.""" + +from __future__ import annotations +import logging +from collections.abc import Callable +from typing import Any + +import numpy as np + +from ..utils.validation import validate_range_exclusive_max +from .optimizer import Optimizer, OptimizerSupportLevel, OptimizerResult, POINT +from ..exceptions import AlgorithmError + +logger = logging.getLogger(__name__) + + +class AQGD(Optimizer): + """Analytic Quantum Gradient Descent (AQGD) with Epochs optimizer. + Performs gradient descent optimization with a momentum term, analytic gradients, + and customized step length schedule for parameterized quantum gates, i.e. + Pauli Rotations. See, for example: + + * K. Mitarai, M. Negoro, M. Kitagawa, and K. Fujii. (2018). + Quantum circuit learning. Phys. Rev. A 98, 032309. + https://arxiv.org/abs/1803.00745 + + * Maria Schuld, Ville Bergholm, Christian Gogolin, Josh Izaac, Nathan Killoran. (2019). + Evaluating analytic gradients on quantum hardware. Phys. Rev. A 99, 032331. + https://arxiv.org/abs/1811.11184 + + for further details on analytic gradients of parameterized quantum gates. + + Gradients are computed "analytically" using the quantum circuit when evaluating + the objective function. + + """ + + _OPTIONS = ["maxiter", "eta", "tol", "disp", "momentum", "param_tol", "averaging"] + + def __init__( + self, + maxiter: int | list[int] = 1000, + eta: float | list[float] = 1.0, + tol: float = 1e-6, # this is tol + momentum: float | list[float] = 0.25, + param_tol: float = 1e-6, + averaging: int = 10, + max_evals_grouped: int = 1, + ) -> None: + """ + Performs Analytical Quantum Gradient Descent (AQGD) with Epochs. + + Args: + maxiter: Maximum number of iterations (full gradient steps) + eta: The coefficient of the gradient update. Increasing this value + results in larger step sizes: param = previous_param - eta * deriv + tol: Tolerance for change in windowed average of objective values. + Convergence occurs when either objective tolerance is met OR parameter + tolerance is met. + momentum: Bias towards the previous gradient momentum in current + update. Must be within the bounds: [0,1) + param_tol: Tolerance for change in norm of parameters. + averaging: Length of window over which to average objective values for objective + convergence criterion + max_evals_grouped: Max number of default gradient evaluations performed simultaneously. + + Raises: + AlgorithmError: If the length of ``maxiter``, `momentum``, and ``eta`` is not the same. + """ + super().__init__() + if isinstance(maxiter, int): + maxiter = [maxiter] + if isinstance(eta, (int, float)): + eta = [eta] + if isinstance(momentum, (int, float)): + momentum = [momentum] + if len(maxiter) != len(eta) or len(maxiter) != len(momentum): + raise AlgorithmError( + "AQGD input parameter length mismatch. Parameters `maxiter`, " + "`eta`, and `momentum` must have the same length." + ) + for m in momentum: + validate_range_exclusive_max("momentum", m, 0, 1) + + self._eta = eta + self._maxiter = maxiter + self._momenta_coeff = momentum + self._param_tol = param_tol + self._tol = tol + self._averaging = averaging + self.set_max_evals_grouped(max_evals_grouped) + + # state + self._avg_objval: float | None = None + self._prev_param: np.ndarray | None = None + self._eval_count = 0 # function evaluations + self._prev_loss: list[float] = [] + self._prev_grad: list[list[float]] = [] + + def get_support_level(self) -> dict[str, OptimizerSupportLevel]: + """Support level dictionary + + Returns: + Dict[str, int]: gradient, bounds and initial point + support information that is ignored/required. + """ + return { + "gradient": OptimizerSupportLevel.ignored, + "bounds": OptimizerSupportLevel.ignored, + "initial_point": OptimizerSupportLevel.required, + } + + @property + def settings(self) -> dict[str, Any]: + return { + "maxiter": self._maxiter, + "eta": self._eta, + "momentum": self._momenta_coeff, + "param_tol": self._param_tol, + "tol": self._tol, + "averaging": self._averaging, + } + + def _compute_objective_fn_and_gradient( + self, params: np.ndarray | list[float], obj: Callable + ) -> tuple[float, np.ndarray]: + """ + Obtains the objective function value for params and the analytical quantum derivatives of + the objective function with respect to each parameter. Requires + 2*(number parameters) + 1 objective evaluations + + Args: + params: Current value of the parameters to evaluate the objective function + obj: Objective function of interest + + Returns: + Tuple containing the objective value and array of gradients for the given parameter set. + """ + num_params = len(params) + param_sets_to_eval = params + np.concatenate( + ( + np.zeros((1, num_params)), # copy of the parameters as is + np.eye(num_params) * np.pi / 2, # copy of the parameters with the positive shift + -np.eye(num_params) * np.pi / 2, + ), # copy of the parameters with the negative shift + axis=0, + ) + # Evaluate, + # reshaping to flatten, as expected by objective function + if self._max_evals_grouped > 1: + batches = [ + param_sets_to_eval[i : i + self._max_evals_grouped] + for i in range(0, len(param_sets_to_eval), self._max_evals_grouped) + ] + values = np.array(np.concatenate([obj(b) for b in batches])) + else: + batches = param_sets_to_eval + values = np.array([obj(b) for b in batches]) + + # Update number of objective function evaluations + self._eval_count += 2 * num_params + 1 + + # return the objective function value + obj_value = values[0] + + # return the gradient values + gradient = 0.5 * (values[1 : num_params + 1] - values[1 + num_params :]) + return obj_value, gradient + + def _update( + self, + params: np.ndarray, + gradient: np.ndarray, + mprev: np.ndarray, + step_size: float, + momentum_coeff: float, + ) -> tuple[np.ndarray, np.ndarray]: + """ + Updates full parameter array based on a step that is a convex + combination of the gradient and previous momentum + + Args: + params: Current value of the parameters to evaluate the objective function at + gradient: Gradient of objective wrt parameters + mprev: Momentum vector for each parameter + step_size: The scaling of step to take + momentum_coeff: Bias towards previous momentum vector when updating current + momentum/step vector + + Returns: + Tuple of the updated parameter and momentum vectors respectively. + """ + # Momentum update: + # Convex combination of previous momentum and current gradient estimate + mnew = (1 - momentum_coeff) * gradient + momentum_coeff * mprev + params -= step_size * mnew + return params, mnew + + def _converged_objective(self, objval: float, tol: float, window_size: int) -> bool: + """ + Tests convergence based on the change in a moving windowed average of past objective values + + Args: + objval: Current value of the objective function + tol: tolerance below which (average) objective function change must be + window_size: size of averaging window + + Returns: + Bool indicating whether or not the optimization has converged. + """ + # If we haven't reached the required window length, + # append the current value, but we haven't converged + if len(self._prev_loss) < window_size: + self._prev_loss.append(objval) + return False + + # Update last value in list with current value + self._prev_loss.append(objval) + # (length now = n+1) + + # Calculate previous windowed average + # and current windowed average of objective values + prev_avg = np.mean(self._prev_loss[:window_size]) + curr_avg = np.mean(self._prev_loss[1 : window_size + 1]) + self._avg_objval = curr_avg # type: ignore[assignment] + + # Update window of objective values + # (Remove earliest value) + self._prev_loss.pop(0) + + if np.absolute(prev_avg - curr_avg) < tol: + # converged + logger.info("Previous obj avg: %f\nCurr obj avg: %f", prev_avg, curr_avg) + return True + return False + + def _converged_parameter(self, parameter: np.ndarray, tol: float) -> bool: + """ + Tests convergence based on change in parameter + + Args: + parameter: current parameter values + tol: tolerance for change in norm of parameters + + Returns: + Bool indicating whether or not the optimization has converged + """ + if self._prev_param is None: + self._prev_param = np.copy(parameter) + return False + + order = np.inf + p_change = np.linalg.norm(self._prev_param - parameter, ord=order) + if p_change < tol: + # converged + logger.info("Change in parameters (%f norm): %f", order, p_change) + return True + return False + + def _converged_alt(self, gradient: list[float], tol: float, window_size: int) -> bool: + """ + Tests convergence from norm of windowed average of gradients + + Args: + gradient: current gradient + tol: tolerance for average gradient norm + window_size: size of averaging window + + Returns: + Bool indicating whether or not the optimization has converged + """ + # If we haven't reached the required window length, + # append the current value, but we haven't converged + if len(self._prev_grad) < window_size - 1: + self._prev_grad.append(gradient) + return False + + # Update last value in list with current value + self._prev_grad.append(gradient) + # (length now = n) + + # Calculate previous windowed average + # and current windowed average of objective values + avg_grad = np.mean(self._prev_grad, axis=0) + + # Update window of values + # (Remove earliest value) + self._prev_grad.pop(0) + + if np.linalg.norm(avg_grad, ord=np.inf) < tol: + # converged + logger.info("Avg. grad. norm: %f", np.linalg.norm(avg_grad, ord=np.inf)) + return True + return False + + def minimize( + self, + fun: Callable[[POINT], float], + x0: POINT, + jac: Callable[[POINT], POINT] | None = None, + bounds: list[tuple[float, float]] | None = None, + ) -> OptimizerResult: + params = np.asarray(x0) + momentum = np.zeros(shape=(params.size,)) + # empty out history of previous objectives/gradients/parameters + # (in case this object is re-used) + self._prev_loss = [] + self._prev_grad = [] + self._prev_param = None + self._eval_count = 0 # function evaluations + + iter_count = 0 + logger.info("Initial Params: %s", params) + epoch = 0 + converged = False + for (eta, mom_coeff) in zip(self._eta, self._momenta_coeff): + logger.info("Epoch: %4d | Stepsize: %6.4f | Momentum: %6.4f", epoch, eta, mom_coeff) + + sum_max_iters = sum(self._maxiter[0 : epoch + 1]) + while iter_count < sum_max_iters: + # update the iteration count + iter_count += 1 + + # Check for parameter convergence before potentially costly function evaluation + converged = self._converged_parameter(params, self._param_tol) + if converged: + break + # Calculate objective function and estimate of analytical gradient + if jac is None: + objval, gradient = self._compute_objective_fn_and_gradient(params, fun) + else: + objval = fun(params) + gradient = jac(params) # type: ignore[assignment] + + logger.info( + " Iter: %4d | Obj: %11.6f | Grad Norm: %f", + iter_count, + objval, + np.linalg.norm(gradient, ord=np.inf), + ) + + # Check for objective convergence + converged = self._converged_objective(objval, self._tol, self._averaging) + if converged: + break + + # Update parameters and momentum + params, momentum = self._update(params, gradient, momentum, eta, mom_coeff) + # end inner iteration + # if converged, end iterating over epochs + if converged: + break + epoch += 1 + # end epoch iteration + + result = OptimizerResult() + result.x = params + result.fun = objval + result.nfev = self._eval_count + result.nit = iter_count + + return result diff --git a/qiskit_machine_learning/optimizers/bobyqa.py b/qiskit_machine_learning/optimizers/bobyqa.py new file mode 100644 index 000000000..e990366cc --- /dev/null +++ b/qiskit_machine_learning/optimizers/bobyqa.py @@ -0,0 +1,84 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2019, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Bound Optimization BY Quadratic Approximation (BOBYQA) optimizer.""" + +from __future__ import annotations + +from collections.abc import Callable +from typing import Any + +import numpy as np +from ..utils import optionals as _optionals +from .optimizer import Optimizer, OptimizerSupportLevel, OptimizerResult, POINT + + +@_optionals.HAS_SKQUANT.require_in_instance +class BOBYQA(Optimizer): + """Bound Optimization BY Quadratic Approximation algorithm. + + BOBYQA finds local solutions to nonlinear, non-convex minimization problems with optional + bound constraints, without requirement of derivatives of the objective function. + + Uses skquant.opt installed with pip install scikit-quant. + For further detail, please refer to + https://github.com/scikit-quant/scikit-quant and https://qat4chem.lbl.gov/software. + """ + + def __init__( + self, + maxiter: int = 1000, + ) -> None: + """ + Args: + maxiter: Maximum number of function evaluations. + + Raises: + MissingOptionalLibraryError: scikit-quant not installed + """ + super().__init__() + self._maxiter = maxiter + + def get_support_level(self): + """Returns support level dictionary.""" + return { + "gradient": OptimizerSupportLevel.ignored, + "bounds": OptimizerSupportLevel.required, + "initial_point": OptimizerSupportLevel.required, + } + + @property + def settings(self) -> dict[str, Any]: + return {"maxiter": self._maxiter} + + def minimize( + self, + fun: Callable[[POINT], float], + x0: POINT, + jac: Callable[[POINT], POINT] | None = None, + bounds: list[tuple[float, float]] | None = None, + ) -> OptimizerResult: + from skquant import opt as skq + + res, history = skq.minimize( + func=fun, + x0=np.asarray(x0), + bounds=np.array(bounds), + budget=self._maxiter, + method="bobyqa", + ) + + optimizer_result = OptimizerResult() + optimizer_result.x = res.optpar + optimizer_result.fun = res.optval + optimizer_result.nfev = len(history) + return optimizer_result diff --git a/qiskit_machine_learning/optimizers/cg.py b/qiskit_machine_learning/optimizers/cg.py new file mode 100644 index 000000000..e69761663 --- /dev/null +++ b/qiskit_machine_learning/optimizers/cg.py @@ -0,0 +1,70 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2018, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Conjugate Gradient optimizer.""" + +from __future__ import annotations + +from .scipy_optimizer import SciPyOptimizer + + +class CG(SciPyOptimizer): + """Conjugate Gradient optimizer. + + CG is an algorithm for the numerical solution of systems of linear equations whose matrices are + symmetric and positive-definite. It is an *iterative algorithm* in that it uses an initial + guess to generate a sequence of improving approximate solutions for a problem, + in which each approximation is derived from the previous ones. It is often used to solve + unconstrained optimization problems, such as energy minimization. + + Uses scipy.optimize.minimize CG. + For further detail, please refer to + https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html + """ + + _OPTIONS = ["maxiter", "disp", "gtol", "eps"] + + # pylint: disable=unused-argument + def __init__( + self, + maxiter: int = 20, + disp: bool = False, + gtol: float = 1e-5, + tol: float | None = None, + eps: float = 1.4901161193847656e-08, + options: dict | None = None, + max_evals_grouped: int = 1, + **kwargs, + ) -> None: + """ + Args: + maxiter: Maximum number of iterations to perform. + disp: Set to True to print convergence messages. + gtol: Gradient norm must be less than gtol before successful termination. + tol: Tolerance for termination. + eps: If jac is approximated, use this value for the step size. + options: A dictionary of solver options. + max_evals_grouped: Max number of default gradient evaluations performed simultaneously. + kwargs: additional kwargs for scipy.optimize.minimize. + """ + if options is None: + options = {} + for k, v in list(locals().items()): + if k in self._OPTIONS: + options[k] = v + super().__init__( + method="CG", + options=options, + tol=tol, + max_evals_grouped=max_evals_grouped, + **kwargs, + ) diff --git a/qiskit_machine_learning/optimizers/cobyla.py b/qiskit_machine_learning/optimizers/cobyla.py new file mode 100644 index 000000000..f5eaa0407 --- /dev/null +++ b/qiskit_machine_learning/optimizers/cobyla.py @@ -0,0 +1,59 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2018, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Constrained Optimization By Linear Approximation optimizer.""" + +from __future__ import annotations + +from .scipy_optimizer import SciPyOptimizer + + +class COBYLA(SciPyOptimizer): + """ + Constrained Optimization By Linear Approximation optimizer. + + COBYLA is a numerical optimization method for constrained problems + where the derivative of the objective function is not known. + + Uses scipy.optimize.minimize COBYLA. + For further detail, please refer to + https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html + """ + + _OPTIONS = ["maxiter", "disp", "rhobeg"] + + # pylint: disable=unused-argument + def __init__( + self, + maxiter: int = 1000, + disp: bool = False, + rhobeg: float = 1.0, + tol: float | None = None, + options: dict | None = None, + **kwargs, + ) -> None: + """ + Args: + maxiter: Maximum number of function evaluations. + disp: Set to True to print convergence messages. + rhobeg: Reasonable initial changes to the variables. + tol: Final accuracy in the optimization (not precisely guaranteed). + This is a lower bound on the size of the trust region. + options: A dictionary of solver options. + kwargs: additional kwargs for scipy.optimize.minimize. + """ + if options is None: + options = {} + for k, v in list(locals().items()): + if k in self._OPTIONS: + options[k] = v + super().__init__(method="COBYLA", options=options, tol=tol, **kwargs) diff --git a/qiskit_machine_learning/optimizers/gradient_descent.py b/qiskit_machine_learning/optimizers/gradient_descent.py new file mode 100644 index 000000000..d9561b19a --- /dev/null +++ b/qiskit_machine_learning/optimizers/gradient_descent.py @@ -0,0 +1,398 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2021, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""A standard gradient descent optimizer.""" +from __future__ import annotations + +from collections.abc import Generator +from dataclasses import dataclass, field +from typing import Any, Callable, SupportsFloat +import numpy as np +from .optimizer import Optimizer, OptimizerSupportLevel, OptimizerResult, POINT +from .steppable_optimizer import AskData, TellData, OptimizerState, SteppableOptimizer +from .optimizer_utils import LearningRate + +CALLBACK = Callable[[int, np.ndarray, float, SupportsFloat], None] + + +@dataclass +class GradientDescentState(OptimizerState): + """State of :class:`~.GradientDescent`. + + Dataclass with all the information of an optimizer plus the learning_rate and the stepsize. + """ + + stepsize: float | None + """Norm of the gradient on the last step.""" + + learning_rate: LearningRate = field(compare=False) + """Learning rate at the current step of the optimization process. + + It behaves like a generator, (use ``next(learning_rate)`` to get the learning rate for the + next step) but it can also return the current learning rate with ``learning_rate.current``. + + """ + + +class GradientDescent(SteppableOptimizer): + r"""The gradient descent minimization routine. + + For a function :math:`f` and an initial point :math:`\vec\theta_0`, the standard (or "vanilla") + gradient descent method is an iterative scheme to find the minimum :math:`\vec\theta^*` of + :math:`f` by updating the parameters in the direction of the negative gradient of :math:`f` + + .. math:: + + \vec\theta_{n+1} = \vec\theta_{n} - \eta_n \vec\nabla f(\vec\theta_{n}), + + for a small learning rate :math:`\eta_n > 0`. + + You can either provide the analytic gradient :math:`\vec\nabla f` as ``jac`` + in the :meth:`~.minimize` method, or, if you do not provide it, use a finite difference + approximation of the gradient. To adapt the size of the perturbation in the finite difference + gradients, set the ``perturbation`` property in the initializer. + + This optimizer supports a callback function. If provided in the initializer, the optimizer + will call the callback in each iteration with the following information in this order: + current number of function values, current parameters, current function value, norm of current + gradient. + + Examples: + + A minimum example that will use finite difference gradients with a default perturbation + of 0.01 and a default learning rate of 0.01. + + .. code-block:: python + + from qiskit_algorithms.optimizers import GradientDescent + + def f(x): + return (np.linalg.norm(x) - 1) ** 2 + + initial_point = np.array([1, 0.5, -0.2]) + + optimizer = GradientDescent(maxiter=100) + + result = optimizer.minimize(fun=fun, x0=initial_point) + + print(f"Found minimum {result.x} at a value" + "of {result.fun} using {result.nfev} evaluations.") + + An example where the learning rate is an iterator and we supply the analytic gradient. + Note how much faster this convergences (i.e. less ``nfev``) compared to the previous + example. + + .. code-block:: python + + from qiskit_algorithms.optimizers import GradientDescent + + def learning_rate(): + power = 0.6 + constant_coeff = 0.1 + def power_law(): + n = 0 + while True: + yield constant_coeff * (n ** power) + n += 1 + + return power_law() + + def f(x): + return (np.linalg.norm(x) - 1) ** 2 + + def grad_f(x): + return 2 * (np.linalg.norm(x) - 1) * x / np.linalg.norm(x) + + initial_point = np.array([1, 0.5, -0.2]) + + optimizer = GradientDescent(maxiter=100, learning_rate=learning_rate) + result = optimizer.minimize(fun=fun, jac=grad_f, x0=initial_point) + + print(f"Found minimum {result.x} at a value" + "of {result.fun} using {result.nfev} evaluations.") + + + An other example where the evaluation of the function has a chance of failing. The user, with + specific knowledge about his function can catch this errors and handle them before passing the + result to the optimizer. + + .. code-block:: python + + import random + import numpy as np + from qiskit_algorithms.optimizers import GradientDescent + + def objective(x): + if random.choice([True, False]): + return None + else: + return (np.linalg.norm(x) - 1) ** 2 + + def grad(x): + if random.choice([True, False]): + return None + else: + return 2 * (np.linalg.norm(x) - 1) * x / np.linalg.norm(x) + + + initial_point = np.random.normal(0, 1, size=(100,)) + + optimizer = GradientDescent(maxiter=20) + optimizer.start(x0=initial_point, fun=objective, jac=grad) + + while optimizer.continue_condition(): + ask_data = optimizer.ask() + evaluated_gradient = None + + while evaluated_gradient is None: + evaluated_gradient = grad(ask_data.x_center) + optimizer.state.njev += 1 + + optimizer.state.nit += 1 + + tell_data = TellData(eval_jac=evaluated_gradient) + optimizer.tell(ask_data=ask_data, tell_data=tell_data) + + result = optimizer.create_result() + + Users that aren't dealing with complicated functions and who are more familiar with step by step + optimization algorithms can use the :meth:`~.step` method which wraps the :meth:`~.ask` + and :meth:`~.tell` methods. In the same spirit the method :meth:`~.minimize` will optimize the + function and return the result. + + To see other libraries that use this interface one can visit: + https://optuna.readthedocs.io/en/stable/tutorial/20_recipes/009_ask_and_tell.html + + """ + + def __init__( + self, + maxiter: int = 100, + learning_rate: float + | list[float] + | np.ndarray + | Callable[[], Generator[float, None, None]] = 0.01, + tol: float = 1e-7, + callback: CALLBACK | None = None, + perturbation: float | None = None, + ) -> None: + """ + Args: + maxiter: The maximum number of iterations. + learning_rate: A constant, list, array or factory of generators yielding learning rates + for the parameter updates. See the docstring for an example. + tol: If the norm of the parameter update is smaller than this threshold, the + optimizer has converged. + perturbation: If no gradient is passed to :meth:`~.minimize` the gradient is + approximated with a forward finite difference scheme with ``perturbation`` + perturbation in both directions (defaults to 1e-2 if required). + Ignored when we have an explicit function for the gradient. + Raises: + ValueError: If ``learning_rate`` is an array and its length is less than ``maxiter``. + """ + super().__init__(maxiter=maxiter) + self.callback = callback + self._state: GradientDescentState | None = None + self._perturbation = perturbation + self._tol = tol + # if learning rate is an array, check it is sufficiently long. + if isinstance(learning_rate, (list, np.ndarray)): + if len(learning_rate) < maxiter: + raise ValueError( + f"Length of learning_rate ({len(learning_rate)}) " + f"is smaller than maxiter ({maxiter})." + ) + self.learning_rate = learning_rate + + @property # type: ignore[override] + def state(self) -> GradientDescentState: + """Return the current state of the optimizer.""" + return self._state + + @state.setter + def state(self, state: GradientDescentState) -> None: + """Set the current state of the optimizer.""" + self._state = state + + @property + def tol(self) -> float: + """Returns the tolerance of the optimizer. + + Any step with smaller stepsize than this value will stop the optimization.""" + return self._tol + + @tol.setter + def tol(self, tol: float) -> None: + """Set the tolerance.""" + self._tol = tol + + @property + def perturbation(self) -> float | None: + """Returns the perturbation. + + This is the perturbation used in the finite difference gradient approximation. + """ + return self._perturbation + + @perturbation.setter + def perturbation(self, perturbation: float | None) -> None: + """Set the perturbation.""" + self._perturbation = perturbation + + def _callback_wrapper(self) -> None: + """ + Wraps the callback function to accommodate GradientDescent. + + Will call :attr:`~.callback` and pass the following arguments: + current number of function values, current parameters, current function value, + norm of current gradient. + """ + if self.callback is not None: + self.callback( + self.state.nfev, + self.state.x, # type: ignore[arg-type] + self.state.fun(self.state.x), + self.state.stepsize, + ) + + @property + def settings(self) -> dict[str, Any]: + # if learning rate or perturbation are custom iterators expand them + learning_rate = self.learning_rate + if callable(self.learning_rate): + iterator = self.learning_rate() + learning_rate = np.array([next(iterator) for _ in range(self.maxiter)]) + + return { + "maxiter": self.maxiter, + "tol": self.tol, + "learning_rate": learning_rate, + "perturbation": self.perturbation, + "callback": self.callback, + } + + def ask(self) -> AskData: + """Returns an object with the data needed to evaluate the gradient. + + If this object contains a gradient function the gradient can be evaluated directly. Otherwise + approximate it with a finite difference scheme. + """ + return AskData( + x_jac=self.state.x, + ) + + def tell(self, ask_data: AskData, tell_data: TellData) -> None: + """ + Updates :attr:`.~GradientDescentState.x` by an amount proportional to the learning + rate and value of the gradient at that point. + + Args: + ask_data: The data used to evaluate the function. + tell_data: The data from the function evaluation. + + Raises: + ValueError: If the gradient passed doesn't have the right dimension. + """ + if np.shape(self.state.x) != np.shape(tell_data.eval_jac): # type: ignore[arg-type] + raise ValueError("The gradient does not have the correct dimension") + self.state.x = self.state.x - next(self.state.learning_rate) * tell_data.eval_jac + self.state.stepsize = np.linalg.norm(tell_data.eval_jac) # type: ignore[arg-type,assignment] + self.state.nit += 1 + + def evaluate(self, ask_data: AskData) -> TellData: + """Evaluates the gradient. + + It does so either by evaluating an analytic gradient or by approximating it with a + finite difference scheme. It will either add ``1`` to the number of gradient evaluations or add + ``N+1`` to the number of function evaluations (Where N is the dimension of the gradient). + + Args: + ask_data: It contains the point where the gradient is to be evaluated and the gradient + function or, in its absence, the objective function to perform a finite difference + approximation. + + Returns: + The data containing the gradient evaluation. + """ + if self.state.jac is None: + eps = 0.01 if (self.perturbation is None) else self.perturbation + grad = Optimizer.gradient_num_diff( + x_center=ask_data.x_jac, + f=self.state.fun, + epsilon=eps, + max_evals_grouped=self._max_evals_grouped, + ) + self.state.nfev += 1 + len(ask_data.x_jac) # type: ignore[arg-type] + else: + grad = self.state.jac(ask_data.x_jac) # type: ignore[arg-type] + self.state.njev += 1 + + return TellData(eval_jac=grad) + + def create_result(self) -> OptimizerResult: + """Creates a result of the optimization process. + + This result contains the best point, the best function value, the number of function/gradient + evaluations and the number of iterations. + + Returns: + The result of the optimization process. + """ + result = OptimizerResult() + result.x = self.state.x + result.fun = self.state.fun(self.state.x) + result.nfev = self.state.nfev + result.njev = self.state.njev + result.nit = self.state.nit + return result + + def start( + self, + fun: Callable[[POINT], float], + x0: POINT, + jac: Callable[[POINT], POINT] | None = None, + bounds: list[tuple[float, float]] | None = None, + ) -> None: + + self.state = GradientDescentState( + fun=fun, + jac=jac, + x=np.asarray(x0), + nit=0, + nfev=0, + njev=0, + learning_rate=LearningRate(learning_rate=self.learning_rate), + stepsize=None, + ) + + def continue_condition(self) -> bool: + """ + Condition that indicates the optimization process should come to an end. + + When the stepsize is smaller than the tolerance, the optimization process is considered + finished. + + Returns: + ``True`` if the optimization process should continue, ``False`` otherwise. + """ + if self.state.stepsize is None: + return True + else: + return (self.state.stepsize > self.tol) and super().continue_condition() + + def get_support_level(self): + """Get the support level dictionary.""" + return { + "gradient": OptimizerSupportLevel.supported, + "bounds": OptimizerSupportLevel.ignored, + "initial_point": OptimizerSupportLevel.required, + } diff --git a/qiskit_machine_learning/optimizers/gsls.py b/qiskit_machine_learning/optimizers/gsls.py new file mode 100644 index 000000000..b92a8c8cd --- /dev/null +++ b/qiskit_machine_learning/optimizers/gsls.py @@ -0,0 +1,375 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2018, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Line search with Gaussian-smoothed samples on a sphere.""" + +from __future__ import annotations + +from collections.abc import Callable +from typing import Any + +import numpy as np + +from ..utils import algorithm_globals +from .optimizer import Optimizer, OptimizerSupportLevel, OptimizerResult, POINT + + +class GSLS(Optimizer): + """Gaussian-smoothed Line Search. + + An implementation of the line search algorithm described in + https://arxiv.org/pdf/1905.01332.pdf, using gradient approximation + based on Gaussian-smoothed samples on a sphere. + + .. note:: + + This component has some function that is normally random. If you want to reproduce behavior + then you should set the random number generator seed in the algorithm_globals + (``qiskit_algorithms.utils.algorithm_globals.random_seed = seed``). + """ + + _OPTIONS = [ + "maxiter", + "max_eval", + "disp", + "sampling_radius", + "sample_size_factor", + "initial_step_size", + "min_step_size", + "step_size_multiplier", + "armijo_parameter", + "min_gradient_norm", + "max_failed_rejection_sampling", + ] + + # pylint: disable=unused-argument + def __init__( + self, + maxiter: int = 10000, + max_eval: int = 10000, + disp: bool = False, + sampling_radius: float = 1.0e-6, + sample_size_factor: int = 1, + initial_step_size: float = 1.0e-2, + min_step_size: float = 1.0e-10, + step_size_multiplier: float = 0.4, + armijo_parameter: float = 1.0e-1, + min_gradient_norm: float = 1e-8, + max_failed_rejection_sampling: int = 50, + ) -> None: + """ + Args: + maxiter: Maximum number of iterations. + max_eval: Maximum number of evaluations. + disp: Set to True to display convergence messages. + sampling_radius: Sampling radius to determine gradient estimate. + sample_size_factor: The size of the sample set at each iteration is this number + multiplied by the dimension of the problem, rounded to the nearest integer. + initial_step_size: Initial step size for the descent algorithm. + min_step_size: Minimum step size for the descent algorithm. + step_size_multiplier: Step size reduction after unsuccessful steps, in the + interval (0, 1). + armijo_parameter: Armijo parameter for sufficient decrease criterion, in the + interval (0, 1). + min_gradient_norm: If the gradient norm is below this threshold, the algorithm stops. + max_failed_rejection_sampling: Maximum number of attempts to sample points within + bounds. + """ + super().__init__() + for k, v in list(locals().items()): + if k in self._OPTIONS: + self._options[k] = v + + def get_support_level(self) -> dict[str, int]: + """Return support level dictionary. + + Returns: + A dictionary containing the support levels for different options. + """ + return { + "gradient": OptimizerSupportLevel.ignored, + "bounds": OptimizerSupportLevel.supported, + "initial_point": OptimizerSupportLevel.required, + } + + @property + def settings(self) -> dict[str, Any]: + return {key: self._options.get(key, None) for key in self._OPTIONS} + + def minimize( + self, + fun: Callable[[POINT], float], + x0: POINT, + jac: Callable[[POINT], POINT] | None = None, + bounds: list[tuple[float, float]] | None = None, + ) -> OptimizerResult: + if not isinstance(x0, np.ndarray): + x0 = np.asarray(x0) + + if bounds is None: + var_lb = np.array([-np.inf] * x0.size) + var_ub = np.array([np.inf] * x0.size) + else: + var_lb = np.array([l if l is not None else -np.inf for (l, _) in bounds]) + var_ub = np.array([u if u is not None else np.inf for (_, u) in bounds]) + + x, fun_, nfev, _ = self.ls_optimize(x0.size, fun, x0, var_lb, var_ub) + + result = OptimizerResult() + result.x = x + result.fun = fun_ + result.nfev = nfev + + return result + + def ls_optimize( + self, + n: int, + obj_fun: Callable[[POINT], float], + initial_point: np.ndarray, + var_lb: np.ndarray, + var_ub: np.ndarray, + ) -> tuple[np.ndarray, float, int, float]: + """Run the line search optimization. + + Args: + n: Dimension of the problem. + obj_fun: Objective function. + initial_point: Initial point. + var_lb: Vector of lower bounds on the decision variables. Vector elements can be -np.inf + if the corresponding variable is unbounded from below. + var_ub: Vector of upper bounds on the decision variables. Vector elements can be np.inf + if the corresponding variable is unbounded from below. + + Returns: + Final iterate as a vector, corresponding objective function value, + number of evaluations, and norm of the gradient estimate. + + Raises: + ValueError: If the number of dimensions mismatches the size of the initial point or + the length of the lower or upper bound. + """ + if len(initial_point) != n: + raise ValueError("Size of the initial point mismatches the number of dimensions.") + if len(var_lb) != n: + raise ValueError("Length of the lower bound mismatches the number of dimensions.") + if len(var_ub) != n: + raise ValueError("Length of the upper bound mismatches the number of dimensions.") + + # Initialize counters and data + iter_count = 0 + n_evals = 0 + prev_iter_successful = True + prev_directions, prev_sample_set_x, prev_sample_set_y = None, None, None + consecutive_fail_iter = 0 + alpha = self._options["initial_step_size"] + grad_norm: float = np.inf + sample_set_size = int(round(self._options["sample_size_factor"] * n)) + + # Initial point + x = initial_point + x_value = obj_fun(x) + n_evals += 1 + while iter_count < self._options["maxiter"] and n_evals < self._options["max_eval"]: + + # Determine set of sample points + directions, sample_set_x = self.sample_set(n, x, var_lb, var_ub, sample_set_size) + + if n_evals + len(sample_set_x) + 1 >= self._options["max_eval"]: + # The evaluation budget is too small to allow for + # another full iteration; we therefore exit now + break + + sample_set_y = np.array([obj_fun(point) for point in sample_set_x]) + n_evals += len(sample_set_x) + + # Expand sample set if we could not improve + if not prev_iter_successful: + directions = np.vstack((prev_directions, directions)) + sample_set_x = np.vstack((prev_sample_set_x, sample_set_x)) + sample_set_y = np.hstack((prev_sample_set_y, sample_set_y)) + + # Find gradient approximation and candidate point + grad = self.gradient_approximation( + n, x, x_value, directions, sample_set_x, sample_set_y + ) + grad_norm = float(np.linalg.norm(grad)) + new_x = np.clip(x - alpha * grad, var_lb, var_ub) + new_x_value = obj_fun(new_x) + n_evals += 1 + + # Print information + if self._options["disp"]: + print(f"Iter {iter_count:d}") + print(f"Point {x} obj {x_value}") + print(f"Gradient {grad}") + print(f"Grad norm {grad_norm} new_x_value {new_x_value} step_size {alpha}") + print(f"Direction {directions}") + + # Test Armijo condition for sufficient decrease + if new_x_value <= x_value - self._options["armijo_parameter"] * alpha * grad_norm: + # Accept point + x, x_value = new_x, new_x_value + alpha /= 2 * self._options["step_size_multiplier"] + prev_iter_successful = True + consecutive_fail_iter = 0 + + # Reset sample set + prev_directions = None + prev_sample_set_x = None + prev_sample_set_y = None + else: + # Do not accept point + alpha *= self._options["step_size_multiplier"] + prev_iter_successful = False + consecutive_fail_iter += 1 + + # Store sample set to enlarge it + prev_directions = directions + prev_sample_set_x, prev_sample_set_y = sample_set_x, sample_set_y + + iter_count += 1 + + # Check termination criterion + if ( + grad_norm <= self._options["min_gradient_norm"] + or alpha <= self._options["min_step_size"] + ): + break + + return x, x_value, n_evals, grad_norm + + def sample_points( + self, n: int, x: np.ndarray, num_points: int + ) -> tuple[np.ndarray, np.ndarray]: + """Sample ``num_points`` points around ``x`` on the ``n``-sphere of specified radius. + + The radius of the sphere is ``self._options['sampling_radius']``. + + Args: + n: Dimension of the problem. + x: Point around which the sample set is constructed. + num_points: Number of points in the sample set. + + Returns: + A tuple containing the sampling points and the directions. + """ + normal_samples = algorithm_globals.random().normal(size=(num_points, n)) + row_norms = np.linalg.norm(normal_samples, axis=1, keepdims=True) + directions = normal_samples / row_norms + points = x + self._options["sampling_radius"] * directions + + return points, directions + + def sample_set( + self, n: int, x: np.ndarray, var_lb: np.ndarray, var_ub: np.ndarray, num_points: int + ) -> tuple[np.ndarray, np.ndarray]: + """Construct sample set of given size. + + Args: + n: Dimension of the problem. + x: Point around which the sample set is constructed. + var_lb: Vector of lower bounds on the decision variables. Vector elements can be -np.inf + if the corresponding variable is unbounded from below. + var_ub: Vector of lower bounds on the decision variables. Vector elements can be np.inf + if the corresponding variable is unbounded from above. + num_points: Number of points in the sample set. + + Returns: + Matrices of (unit-norm) sample directions and sample points, one per row. + Both matrices are 2D arrays of floats. + + Raises: + RuntimeError: If not enough samples could be generated within the bounds. + """ + # Generate points uniformly on the sphere + points, directions = self.sample_points(n, x, num_points) + + # Check bounds + if (points >= var_lb).all() and (points <= var_ub).all(): + # If all points are within bounds, return them + return directions, (x + self._options["sampling_radius"] * directions) + else: + # Otherwise we perform rejection sampling until we have + # enough points that satisfy the bounds + indices = np.where((points >= var_lb).all(axis=1) & (points <= var_ub).all(axis=1))[0] + accepted = directions[indices] + num_trials = 0 + + while ( + len(accepted) < num_points + and num_trials < self._options["max_failed_rejection_sampling"] + ): + # Generate points uniformly on the sphere + points, directions = self.sample_points(n, x, num_points) + indices = np.where((points >= var_lb).all(axis=1) & (points <= var_ub).all(axis=1))[ + 0 + ] + accepted = np.vstack((accepted, directions[indices])) + num_trials += 1 + + # When we are at a corner point, the expected fraction of acceptable points may be + # exponential small in the dimension of the problem. Thus, if we keep failing and + # do not have enough points by now, we switch to a different method that guarantees + # finding enough points, but they may not be uniformly distributed. + if len(accepted) < num_points: + points, directions = self.sample_points(n, x, num_points) + to_be_flipped = (points < var_lb) | (points > var_ub) + directions *= np.where(to_be_flipped, -1, 1) + points = x + self._options["sampling_radius"] * directions + indices = np.where((points >= var_lb).all(axis=1) & (points <= var_ub).all(axis=1))[ + 0 + ] + accepted = np.vstack((accepted, directions[indices])) + + # If we still do not have enough sampling points, we have failed. + if len(accepted) < num_points: + raise RuntimeError( + "Could not generate enough samples within bounds; try smaller radius." + ) + + return ( + accepted[:num_points], + x + self._options["sampling_radius"] * accepted[:num_points], + ) + + def gradient_approximation( + self, + n: int, + x: np.ndarray, + x_value: float, + directions: np.ndarray, + sample_set_x: np.ndarray, + sample_set_y: np.ndarray, + ) -> np.ndarray: + """Construct gradient approximation from given sample. + + Args: + n: Dimension of the problem. + x: Point around which the sample set was constructed. + x_value: Objective function value at x. + directions: Directions of the sample points wrt the central point x, as a 2D array. + sample_set_x: x-coordinates of the sample set, one point per row, as a 2D array. + sample_set_y: Objective function values of the points in sample_set_x, as a 1D array. + + Returns: + Gradient approximation at x, as a 1D array. + """ + ffd = sample_set_y - x_value + gradient = ( + float(n) + / len(sample_set_y) + * np.sum( + ffd.reshape(len(sample_set_y), 1) / self._options["sampling_radius"] * directions, 0 + ) + ) + return gradient diff --git a/qiskit_machine_learning/optimizers/imfil.py b/qiskit_machine_learning/optimizers/imfil.py new file mode 100644 index 000000000..15d73948e --- /dev/null +++ b/qiskit_machine_learning/optimizers/imfil.py @@ -0,0 +1,86 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2019, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""IMplicit FILtering (IMFIL) optimizer.""" +from __future__ import annotations + +from collections.abc import Callable +from typing import Any + +from ..utils import optionals as _optionals +from .optimizer import Optimizer, OptimizerSupportLevel, OptimizerResult, POINT + + +@_optionals.HAS_SKQUANT.require_in_instance +class IMFIL(Optimizer): + """IMplicit FILtering algorithm. + + Implicit filtering is a way to solve bound-constrained optimization problems for + which derivatives are not available. In comparison to methods that use interpolation to + reconstruct the function and its higher derivatives, implicit filtering builds upon + coordinate search followed by interpolation to get an approximate gradient. + + Uses skquant.opt installed with pip install scikit-quant. + For further detail, please refer to + https://github.com/scikit-quant/scikit-quant and https://qat4chem.lbl.gov/software. + """ + + def __init__( + self, + maxiter: int = 1000, + ) -> None: + """ + Args: + maxiter: Maximum number of function evaluations. + + Raises: + MissingOptionalLibraryError: scikit-quant not installed + """ + super().__init__() + self._maxiter = maxiter + + def get_support_level(self): + """Returns support level dictionary.""" + return { + "gradient": OptimizerSupportLevel.ignored, + "bounds": OptimizerSupportLevel.required, + "initial_point": OptimizerSupportLevel.required, + } + + @property + def settings(self) -> dict[str, Any]: + return { + "maxiter": self._maxiter, + } + + def minimize( + self, + fun: Callable[[POINT], float], + x0: POINT, + jac: Callable[[POINT], POINT] | None = None, + bounds: list[tuple[float, float]] | None = None, + ) -> OptimizerResult: + from skquant import opt as skq + + res, history = skq.minimize( + func=fun, + x0=x0, + bounds=bounds, + budget=self._maxiter, + method="imfil", + ) + + optimizer_result = OptimizerResult() + optimizer_result.x = res.optpar + optimizer_result.fun = res.optval + optimizer_result.nfev = len(history) + return optimizer_result diff --git a/qiskit_machine_learning/optimizers/l_bfgs_b.py b/qiskit_machine_learning/optimizers/l_bfgs_b.py new file mode 100644 index 000000000..1c529e0a3 --- /dev/null +++ b/qiskit_machine_learning/optimizers/l_bfgs_b.py @@ -0,0 +1,88 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2018, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Limited-memory BFGS Bound optimizer.""" + +from __future__ import annotations +from typing import SupportsFloat + +import numpy as np + +from .scipy_optimizer import SciPyOptimizer + + +class L_BFGS_B(SciPyOptimizer): # pylint: disable=invalid-name + """ + Limited-memory BFGS Bound optimizer. + + The target goal of Limited-memory Broyden-Fletcher-Goldfarb-Shanno Bound (L-BFGS-B) + is to minimize the value of a differentiable scalar function :math:`f`. + This optimizer is a quasi-Newton method, meaning that, in contrast to Newtons's method, + it does not require :math:`f`'s Hessian (the matrix of :math:`f`'s second derivatives) + when attempting to compute :math:`f`'s minimum value. + + Like BFGS, L-BFGS is an iterative method for solving unconstrained, non-linear optimization + problems, but approximates BFGS using a limited amount of computer memory. + L-BFGS starts with an initial estimate of the optimal value, and proceeds iteratively + to refine that estimate with a sequence of better estimates. + + The derivatives of :math:`f` are used to identify the direction of steepest descent, + and also to form an estimate of the Hessian matrix (second derivative) of :math:`f`. + L-BFGS-B extends L-BFGS to handle simple, per-variable bound constraints. + + Uses ``scipy.optimize.fmin_l_bfgs_b``. + For further detail, please refer to + https://docs.scipy.org/doc/scipy/reference/optimize.minimize-lbfgsb.html + """ + + _OPTIONS = ["maxfun", "maxiter", "ftol", "iprint", "eps"] + + # pylint: disable=unused-argument + def __init__( + self, + maxfun: int = 15000, + maxiter: int = 15000, + ftol: SupportsFloat = 10 * np.finfo(float).eps, + iprint: int = -1, + eps: float = 1e-08, + options: dict | None = None, + max_evals_grouped: int = 1, + **kwargs, + ): + r""" + Args: + maxfun: Maximum number of function evaluations. + maxiter: Maximum number of iterations. + ftol: The iteration stops when + :math:`(f^k - f^{k+1}) / \max\{|f^k|, |f^{k+1}|,1\} \leq \text{ftol}`. + iprint: Controls the frequency of output. ``iprint < 0`` means no output; + ``iprint = 0`` print only one line at the last iteration; ``0 < iprint < 99`` + print also :math:`f` and :math:`|\text{proj} g|` every iprint iterations; + ``iprint = 99`` print details of every iteration except n-vectors; ``iprint = 100`` + print also the changes of active set and final :math:`x`; ``iprint > 100`` print + details of every iteration including :math:`x` and :math:`g`. + eps: If jac is approximated, use this value for the step size. + options: A dictionary of solver options. + max_evals_grouped: Max number of default gradient evaluations performed simultaneously. + kwargs: additional kwargs for ``scipy.optimize.minimize``. + """ + if options is None: + options = {} + for k, v in list(locals().items()): + if k in self._OPTIONS: + options[k] = v + super().__init__( + method="L-BFGS-B", + options=options, + max_evals_grouped=max_evals_grouped, + **kwargs, + ) diff --git a/qiskit_machine_learning/optimizers/nelder_mead.py b/qiskit_machine_learning/optimizers/nelder_mead.py new file mode 100644 index 000000000..a8c3a264b --- /dev/null +++ b/qiskit_machine_learning/optimizers/nelder_mead.py @@ -0,0 +1,73 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2018, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Nelder-Mead optimizer.""" +from __future__ import annotations + + +from .scipy_optimizer import SciPyOptimizer + + +class NELDER_MEAD(SciPyOptimizer): # pylint: disable=invalid-name + """ + Nelder-Mead optimizer. + + The Nelder-Mead algorithm performs unconstrained optimization; it ignores bounds + or constraints. It is used to find the minimum or maximum of an objective function + in a multidimensional space. It is based on the Simplex algorithm. Nelder-Mead + is robust in many applications, especially when the first and second derivatives of the + objective function are not known. + + However, if the numerical computation of the derivatives can be trusted to be accurate, + other algorithms using the first and/or second derivatives information might be preferred to + Nelder-Mead for their better performance in the general case, especially in consideration of + the fact that the Nelder–Mead technique is a heuristic search method that can converge to + non-stationary points. + + Uses scipy.optimize.minimize Nelder-Mead. + For further detail, please refer to + See https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html + """ + + _OPTIONS = ["maxiter", "maxfev", "disp", "xatol", "adaptive"] + + # pylint: disable=unused-argument + def __init__( + self, + maxiter: int | None = None, + maxfev: int = 1000, + disp: bool = False, + xatol: float = 0.0001, + tol: float | None = None, + adaptive: bool = False, + options: dict | None = None, + **kwargs, + ) -> None: + """ + Args: + maxiter: Maximum allowed number of iterations. If both maxiter and maxfev are set, + minimization will stop at the first reached. + maxfev: Maximum allowed number of function evaluations. If both maxiter and + maxfev are set, minimization will stop at the first reached. + disp: Set to True to print convergence messages. + xatol: Absolute error in xopt between iterations that is acceptable for convergence. + tol: Tolerance for termination. + adaptive: Adapt algorithm parameters to dimensionality of problem. + options: A dictionary of solver options. + kwargs: additional kwargs for scipy.optimize.minimize. + """ + if options is None: + options = {} + for k, v in list(locals().items()): + if k in self._OPTIONS: + options[k] = v + super().__init__(method="Nelder-Mead", options=options, tol=tol, **kwargs) diff --git a/qiskit_machine_learning/optimizers/nft.py b/qiskit_machine_learning/optimizers/nft.py new file mode 100644 index 000000000..504e5f730 --- /dev/null +++ b/qiskit_machine_learning/optimizers/nft.py @@ -0,0 +1,169 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2019, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Nakanishi-Fujii-Todo algorithm.""" +from __future__ import annotations + + +import numpy as np +from scipy.optimize import OptimizeResult + +from .scipy_optimizer import SciPyOptimizer + + +class NFT(SciPyOptimizer): + """ + Nakanishi-Fujii-Todo algorithm. + + See https://arxiv.org/abs/1903.12166 + """ + + _OPTIONS = ["maxiter", "maxfev", "disp", "reset_interval"] + + # pylint: disable=unused-argument + def __init__( + self, + maxiter: int | None = None, + maxfev: int = 1024, + disp: bool = False, + reset_interval: int = 32, + options: dict | None = None, + **kwargs, + ) -> None: + """ + Built out using scipy framework, for details, please refer to + https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html. + + Args: + maxiter: Maximum number of iterations to perform. + maxfev: Maximum number of function evaluations to perform. + disp: disp + reset_interval: The minimum estimates directly once + in ``reset_interval`` times. + options: A dictionary of solver options. + kwargs: additional kwargs for scipy.optimize.minimize. + + Notes: + In this optimization method, the optimization function have to satisfy + three conditions written in [1]_. + + References: + .. [1] K. M. Nakanishi, K. Fujii, and S. Todo. 2019. + Sequential minimal optimization for quantum-classical hybrid algorithms. + arXiv preprint arXiv:1903.12166. + """ + if options is None: + options = {} + for k, v in list(locals().items()): + if k in self._OPTIONS: + options[k] = v + super().__init__(method=nakanishi_fujii_todo, options=options, **kwargs) + + +# pylint: disable=invalid-name +def nakanishi_fujii_todo( + fun, x0, args=(), maxiter=None, maxfev=1024, reset_interval=32, eps=1e-32, callback=None, **_ +): + """ + Find the global minimum of a function using the nakanishi_fujii_todo + algorithm [1]. + Args: + fun (callable): ``f(x, *args)`` + Function to be optimized. ``args`` can be passed as an optional item + in the dict ``minimizer_kwargs``. + This function must satisfy the three condition written in Ref. [1]. + x0 (ndarray): shape (n,) + Initial guess. Array of real elements of size (n,), + where 'n' is the number of independent variables. + args (tuple, optional): + Extra arguments passed to the objective function. + maxiter (int): + Maximum number of iterations to perform. + Default: None. + maxfev (int): + Maximum number of function evaluations to perform. + Default: 1024. + reset_interval (int): + The minimum estimates directly once in ``reset_interval`` times. + Default: 32. + eps (float): eps + **_ : additional options + callback (callable, optional): + Called after each iteration. + Returns: + OptimizeResult: + The optimization result represented as a ``OptimizeResult`` object. + Important attributes are: ``x`` the solution array. See + `OptimizeResult` for a description of other attributes. + Notes: + In this optimization method, the optimization function have to satisfy + three conditions written in [1]. + References: + .. [1] K. M. Nakanishi, K. Fujii, and S. Todo. 2019. + Sequential minimal optimization for quantum-classical hybrid algorithms. + arXiv preprint arXiv:1903.12166. + """ + + x0 = np.asarray(x0) + recycle_z0 = None + niter = 0 + funcalls = 0 + + while True: + + idx = niter % x0.size + + if reset_interval > 0: + if niter % reset_interval == 0: + recycle_z0 = None + + if recycle_z0 is None: + z0 = fun(np.copy(x0), *args) + funcalls += 1 + else: + z0 = recycle_z0 + + p = np.copy(x0) + p[idx] = x0[idx] + np.pi / 2 + z1 = fun(p, *args) + funcalls += 1 + + p = np.copy(x0) + p[idx] = x0[idx] - np.pi / 2 + z3 = fun(p, *args) + funcalls += 1 + + z2 = z1 + z3 - z0 + c = (z1 + z3) / 2 + a = np.sqrt((z0 - z2) ** 2 + (z1 - z3) ** 2) / 2 + b = np.arctan((z1 - z3) / ((z0 - z2) + eps * (z0 == z2))) + x0[idx] + b += 0.5 * np.pi + 0.5 * np.pi * np.sign((z0 - z2) + eps * (z0 == z2)) + + x0[idx] = b + recycle_z0 = c - a + + niter += 1 + + if callback is not None: + callback(np.copy(x0)) + + if maxfev is not None: + if funcalls >= maxfev: + break + + if maxiter is not None: + if niter >= maxiter: + break + + return OptimizeResult( + fun=fun(np.copy(x0), *args), x=x0, nit=niter, nfev=funcalls, success=(niter > 1) + ) diff --git a/qiskit_machine_learning/optimizers/nlopts/__init__.py b/qiskit_machine_learning/optimizers/nlopts/__init__.py new file mode 100644 index 000000000..8aea0aea1 --- /dev/null +++ b/qiskit_machine_learning/optimizers/nlopts/__init__.py @@ -0,0 +1,13 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2018, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""NLopt based global optimizers""" diff --git a/qiskit_machine_learning/optimizers/nlopts/crs.py b/qiskit_machine_learning/optimizers/nlopts/crs.py new file mode 100644 index 000000000..ec30e236e --- /dev/null +++ b/qiskit_machine_learning/optimizers/nlopts/crs.py @@ -0,0 +1,35 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2018, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Controlled Random Search (CRS) with local mutation optimizer.""" + +from .nloptimizer import NLoptOptimizer, NLoptOptimizerType + + +class CRS(NLoptOptimizer): + """ + Controlled Random Search (CRS) with local mutation optimizer. + + Controlled Random Search (CRS) with local mutation is part of the family of the CRS optimizers. + The CRS optimizers start with a random population of points, and randomly evolve these points + by heuristic rules. In the case of CRS with local mutation, the evolution is a randomized + version of the :class:`NELDER_MEAD` local optimizer. + + + NLopt global optimizer, derivative-free. + For further detail, please refer to + https://nlopt.readthedocs.io/en/latest/NLopt_Algorithms/#controlled-random-search-crs-with-local-mutation + """ + + def get_nlopt_optimizer(self) -> NLoptOptimizerType: + """Return NLopt optimizer type""" + return NLoptOptimizerType.GN_CRS2_LM diff --git a/qiskit_machine_learning/optimizers/nlopts/direct_l.py b/qiskit_machine_learning/optimizers/nlopts/direct_l.py new file mode 100644 index 000000000..9ee350675 --- /dev/null +++ b/qiskit_machine_learning/optimizers/nlopts/direct_l.py @@ -0,0 +1,34 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2018, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""DIviding RECTangles Locally-biased optimizer.""" + +from .nloptimizer import NLoptOptimizer, NLoptOptimizerType + + +class DIRECT_L(NLoptOptimizer): # pylint: disable=invalid-name + """ + DIviding RECTangles Locally-biased optimizer. + + DIviding RECTangles (DIRECT) is a deterministic-search algorithms based on systematic division + of the search domain into increasingly smaller hyper-rectangles. + The DIRECT-L version is a "locally biased" variant of DIRECT that makes the algorithm more + biased towards local search, so that it is more efficient for functions with few local minima. + + NLopt global optimizer, derivative-free. + For further detail, please refer to + http://nlopt.readthedocs.io/en/latest/NLopt_Algorithms/#direct-and-direct-l + """ + + def get_nlopt_optimizer(self) -> NLoptOptimizerType: + """Return NLopt optimizer type""" + return NLoptOptimizerType.GN_DIRECT_L diff --git a/qiskit_machine_learning/optimizers/nlopts/direct_l_rand.py b/qiskit_machine_learning/optimizers/nlopts/direct_l_rand.py new file mode 100644 index 000000000..3c4a90da0 --- /dev/null +++ b/qiskit_machine_learning/optimizers/nlopts/direct_l_rand.py @@ -0,0 +1,32 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2018, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""DIviding RECTangles Locally-biased Randomized optimizer.""" + +from .nloptimizer import NLoptOptimizer, NLoptOptimizerType + + +class DIRECT_L_RAND(NLoptOptimizer): # pylint: disable=invalid-name + """ + DIviding RECTangles Locally-biased Randomized optimizer. + + DIRECT-L RAND is the "locally biased" variant with some randomization in near-tie decisions. + See also :class:`DIRECT_L` + + NLopt global optimizer, derivative-free. + For further detail, please refer to + http://nlopt.readthedocs.io/en/latest/NLopt_Algorithms/#direct-and-direct-l + """ + + def get_nlopt_optimizer(self) -> NLoptOptimizerType: + """Return NLopt optimizer type""" + return NLoptOptimizerType.GN_DIRECT_L_RAND diff --git a/qiskit_machine_learning/optimizers/nlopts/esch.py b/qiskit_machine_learning/optimizers/nlopts/esch.py new file mode 100644 index 000000000..f2687902b --- /dev/null +++ b/qiskit_machine_learning/optimizers/nlopts/esch.py @@ -0,0 +1,33 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2018, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""ESCH evolutionary optimizer.""" + +from .nloptimizer import NLoptOptimizer, NLoptOptimizerType + + +class ESCH(NLoptOptimizer): + """ + ESCH evolutionary optimizer. + + ESCH is an evolutionary algorithm for global optimization that supports bound constraints only. + Specifically, it does not support nonlinear constraints. + + NLopt global optimizer, derivative-free. + For further detail, please refer to + + http://nlopt.readthedocs.io/en/latest/NLopt_Algorithms/#esch-evolutionary-algorithm + """ + + def get_nlopt_optimizer(self) -> NLoptOptimizerType: + """Return NLopt optimizer type""" + return NLoptOptimizerType.GN_ESCH diff --git a/qiskit_machine_learning/optimizers/nlopts/isres.py b/qiskit_machine_learning/optimizers/nlopts/isres.py new file mode 100644 index 000000000..1af1f501e --- /dev/null +++ b/qiskit_machine_learning/optimizers/nlopts/isres.py @@ -0,0 +1,39 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2018, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Improved Stochastic Ranking Evolution Strategy optimizer.""" + +from .nloptimizer import NLoptOptimizer, NLoptOptimizerType + + +class ISRES(NLoptOptimizer): + """ + Improved Stochastic Ranking Evolution Strategy optimizer. + + Improved Stochastic Ranking Evolution Strategy (ISRES) is an algorithm for + non-linearly constrained global optimization. It has heuristics to escape local optima, + even though convergence to a global optima is not guaranteed. The evolution strategy is based + on a combination of a mutation rule and differential variation. The fitness ranking is simply + via the objective function for problems without nonlinear constraints. When nonlinear + constraints are included, the `stochastic ranking proposed by Runarsson and Yao + `__ + is employed. This method supports arbitrary nonlinear inequality and equality constraints, in + addition to the bound constraints. + + NLopt global optimizer, derivative-free. + For further detail, please refer to + http://nlopt.readthedocs.io/en/latest/NLopt_Algorithms/#isres-improved-stochastic-ranking-evolution-strategy + """ + + def get_nlopt_optimizer(self) -> NLoptOptimizerType: + """Return NLopt optimizer type""" + return NLoptOptimizerType.GN_ISRES diff --git a/qiskit_machine_learning/optimizers/nlopts/nloptimizer.py b/qiskit_machine_learning/optimizers/nlopts/nloptimizer.py new file mode 100644 index 000000000..51ce5b552 --- /dev/null +++ b/qiskit_machine_learning/optimizers/nlopts/nloptimizer.py @@ -0,0 +1,131 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2018, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Minimize using objective function""" +from __future__ import annotations + +from collections.abc import Callable +from enum import Enum +from abc import abstractmethod +import logging +import numpy as np + +from ...utils import optionals as _optionals +from ..optimizer import Optimizer, OptimizerSupportLevel, OptimizerResult, POINT + +logger = logging.getLogger(__name__) + + +class NLoptOptimizerType(Enum): + """NLopt Valid Optimizer""" + + GN_CRS2_LM = 1 + GN_DIRECT_L_RAND = 2 + GN_DIRECT_L = 3 + GN_ESCH = 4 + GN_ISRES = 5 + + +@_optionals.HAS_NLOPT.require_in_instance +class NLoptOptimizer(Optimizer): + """ + NLopt global optimizer base class + """ + + _OPTIONS = ["max_evals"] + + def __init__(self, max_evals: int = 1000) -> None: # pylint: disable=unused-argument + """ + Args: + max_evals: Maximum allowed number of function evaluations. + + Raises: + MissingOptionalLibraryError: NLopt library not installed. + """ + import nlopt + + super().__init__() + for k, v in list(locals().items()): + if k in self._OPTIONS: + self._options[k] = v + + self._optimizer_names = { + NLoptOptimizerType.GN_CRS2_LM: nlopt.GN_CRS2_LM, + NLoptOptimizerType.GN_DIRECT_L_RAND: nlopt.GN_DIRECT_L_RAND, + NLoptOptimizerType.GN_DIRECT_L: nlopt.GN_DIRECT_L, + NLoptOptimizerType.GN_ESCH: nlopt.GN_ESCH, + NLoptOptimizerType.GN_ISRES: nlopt.GN_ISRES, + } + + @abstractmethod + def get_nlopt_optimizer(self) -> NLoptOptimizerType: + """return NLopt optimizer enum type""" + raise NotImplementedError + + def get_support_level(self): + """return support level dictionary""" + return { + "gradient": OptimizerSupportLevel.ignored, + "bounds": OptimizerSupportLevel.supported, + "initial_point": OptimizerSupportLevel.required, + } + + @property + def settings(self): + return {"max_evals": self._options.get("max_evals", 1000)} + + def minimize( + self, + fun: Callable[[POINT], float], + x0: POINT, + jac: Callable[[POINT], POINT] | None = None, + bounds: list[tuple[float, float]] | None = None, + ) -> OptimizerResult: + import nlopt + + x0 = np.asarray(x0) + + if bounds is None: + bounds = [(None, None)] * x0.size + + threshold = 3 * np.pi + low = [(l if l is not None else -threshold) for (l, u) in bounds] + high = [(u if u is not None else threshold) for (l, u) in bounds] + + name = self._optimizer_names[self.get_nlopt_optimizer()] + opt = nlopt.opt(name, len(low)) + logger.debug(opt.get_algorithm_name()) + + opt.set_lower_bounds(low) + opt.set_upper_bounds(high) + + eval_count = 0 + + def wrap_objfunc_global(x, _grad): + nonlocal eval_count + eval_count += 1 + return fun(x) + + opt.set_min_objective(wrap_objfunc_global) + opt.set_maxeval(self._options.get("max_evals", 1000)) + + xopt = opt.optimize(x0) + minf = opt.last_optimum_value() + + logger.debug("Global minimize found %s eval count %s", minf, eval_count) + + result = OptimizerResult() + result.x = xopt + result.fun = minf + result.nfev = eval_count + + return result diff --git a/qiskit_machine_learning/optimizers/optimizer.py b/qiskit_machine_learning/optimizers/optimizer.py new file mode 100644 index 000000000..9ad8fe668 --- /dev/null +++ b/qiskit_machine_learning/optimizers/optimizer.py @@ -0,0 +1,389 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2018, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Optimizer interface""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from collections.abc import Callable +from enum import IntEnum +import logging +from typing import Any, Union, Protocol + +import numpy as np +import scipy + +from ..algorithm_result import AlgorithmResult + +logger = logging.getLogger(__name__) + +POINT = Union[float, np.ndarray] # pylint: disable=invalid-name + + +class OptimizerResult(AlgorithmResult): + """The result of an optimization routine.""" + + def __init__(self) -> None: + super().__init__() + self._x: POINT | None = None # pylint: disable=invalid-name + self._fun: float | None = None + self._jac: POINT | None = None + self._nfev: int | None = None + self._njev: int | None = None + self._nit: int | None = None + + @property + def x(self) -> POINT | None: + """The final point of the minimization.""" + return self._x + + @x.setter + def x(self, x: POINT | None) -> None: + """Set the final point of the minimization.""" + self._x = x + + @property + def fun(self) -> float | None: + """The final value of the minimization.""" + return self._fun + + @fun.setter + def fun(self, fun: float | None) -> None: + """Set the final value of the minimization.""" + self._fun = fun + + @property + def jac(self) -> POINT | None: + """The final gradient of the minimization.""" + return self._jac + + @jac.setter + def jac(self, jac: POINT | None) -> None: + """Set the final gradient of the minimization.""" + self._jac = jac + + @property + def nfev(self) -> int | None: + """The total number of function evaluations.""" + return self._nfev + + @nfev.setter + def nfev(self, nfev: int | None) -> None: + """Set the total number of function evaluations.""" + self._nfev = nfev + + @property + def njev(self) -> int | None: + """The total number of gradient evaluations.""" + return self._njev + + @njev.setter + def njev(self, njev: int | None) -> None: + """Set the total number of gradient evaluations.""" + self._njev = njev + + @property + def nit(self) -> int | None: + """The total number of iterations.""" + return self._nit + + @nit.setter + def nit(self, nit: int | None) -> None: + """Set the total number of iterations.""" + self._nit = nit + + +class Minimizer(Protocol): + """Callable Protocol for minimizer. + + This interface is based on `SciPy's optimize module + `__. + + This protocol defines a callable taking the following parameters: + + fun + The objective function to minimize (for example the energy in the case of the VQE). + x0 + The initial point for the optimization. + jac + The gradient of the objective function. + bounds + Parameters bounds for the optimization. Note that these might not be supported + by all optimizers. + + and which returns a minimization result object (either SciPy's or Qiskit's). + """ + + # pylint: disable=invalid-name + def __call__( + self, + fun: Callable[[np.ndarray], float], + x0: np.ndarray, + jac: Callable[[np.ndarray], np.ndarray] | None, + bounds: list[tuple[float, float]] | None, + ) -> scipy.optimize.OptimizeResult | OptimizerResult: + """Minimize the objective function. + + This interface is based on `SciPy's optimize module `__. + + Args: + fun: The objective function to minimize (for example the energy in the case of the VQE). + x0: The initial point for the optimization. + jac: The gradient of the objective function. + bounds: Parameters bounds for the optimization. Note that these might not be supported + by all optimizers. + + Returns: + The minimization result object (either SciPy's or Qiskit's). + """ + ... # pylint: disable=unnecessary-ellipsis + + +class OptimizerSupportLevel(IntEnum): + """Support Level enum for features such as bounds, gradient and initial point""" + + # pylint: disable=invalid-name + not_supported = 0 # Does not support the corresponding parameter in optimize() + ignored = 1 # Feature can be passed as non None but will be ignored + supported = 2 # Feature is supported + required = 3 # Feature is required and must be given, None is invalid + + +class Optimizer(ABC): + """Base class for optimization algorithm.""" + + @abstractmethod + def __init__(self): + """ + Initialize the optimization algorithm, setting the support + level for _gradient_support_level, _bound_support_level, + _initial_point_support_level, and empty options. + """ + self._gradient_support_level = self.get_support_level()["gradient"] + self._bounds_support_level = self.get_support_level()["bounds"] + self._initial_point_support_level = self.get_support_level()["initial_point"] + self._options = {} + self._max_evals_grouped = None + + @abstractmethod + def get_support_level(self): + """Return support level dictionary""" + raise NotImplementedError + + def set_options(self, **kwargs): + """ + Sets or updates values in the options dictionary. + + The options dictionary may be used internally by a given optimizer to + pass additional optional values for the underlying optimizer/optimization + function used. The options dictionary may be initially populated with + a set of key/values when the given optimizer is constructed. + + Args: + kwargs (dict): options, given as name=value. + """ + for name, value in kwargs.items(): + self._options[name] = value + logger.debug("options: %s", self._options) + + # pylint: disable=invalid-name + @staticmethod + def gradient_num_diff(x_center, f, epsilon, max_evals_grouped=None): + """ + We compute the gradient with the numeric differentiation in the parallel way, + around the point x_center. + + Args: + x_center (ndarray): point around which we compute the gradient + f (func): the function of which the gradient is to be computed. + epsilon (float): the epsilon used in the numeric differentiation. + max_evals_grouped (int): max evals grouped, defaults to 1 (i.e. no batching). + Returns: + grad: the gradient computed + + """ + if max_evals_grouped is None: # no batching by default + max_evals_grouped = 1 + + forig = f(*((x_center,))) + grad = [] + ei = np.zeros((len(x_center),), float) + todos = [] + for k in range(len(x_center)): + ei[k] = 1.0 + d = epsilon * ei + todos.append(x_center + d) + ei[k] = 0.0 + + counter = 0 + chunk = [] + chunks = [] + length = len(todos) + # split all points to chunks, where each chunk has batch_size points + for i in range(length): + x = todos[i] + chunk.append(x) + counter += 1 + # the last one does not have to reach batch_size + if counter == max_evals_grouped or i == length - 1: + chunks.append(chunk) + chunk = [] + counter = 0 + + for chunk in chunks: # eval the chunks in order + parallel_parameters = np.concatenate(chunk) + todos_results = f(parallel_parameters) # eval the points in a chunk (order preserved) + if isinstance(todos_results, float): + grad.append((todos_results - forig) / epsilon) + else: + for todor in todos_results: + grad.append((todor - forig) / epsilon) + + return np.array(grad) + + @staticmethod + def wrap_function(function, args): + """ + Wrap the function to implicitly inject the args at the call of the function. + + Args: + function (func): the target function + args (tuple): the args to be injected + Returns: + function_wrapper: wrapper + """ + + def function_wrapper(*wrapper_args): + return function(*(wrapper_args + args)) + + return function_wrapper + + @property + def setting(self): + """Return setting""" + ret = f"Optimizer: {self.__class__.__name__}\n" + params = "" + for key, value in self.__dict__.items(): + if key[0] == "_": + params += f"-- {key[1:]}: {value}\n" + ret += f"{params}" + return ret + + @property + def settings(self) -> dict[str, Any]: + """The optimizer settings in a dictionary format. + + The settings can for instance be used for JSON-serialization (if all settings are + serializable, which e.g. doesn't hold per default for callables), such that the + optimizer object can be reconstructed as + + .. code-block:: + + settings = optimizer.settings + # JSON serialize and send to another server + optimizer = OptimizerClass(**settings) + + """ + raise NotImplementedError("The settings method is not implemented per default.") + + @abstractmethod + def minimize( + self, + fun: Callable[[POINT], float], + x0: POINT, + jac: Callable[[POINT], POINT] | None = None, + bounds: list[tuple[float, float]] | None = None, + ) -> OptimizerResult: + """Minimize the scalar function. + + Args: + fun: The scalar function to minimize. + x0: The initial point for the minimization. + jac: The gradient of the scalar function ``fun``. + bounds: Bounds for the variables of ``fun``. This argument might be ignored if the + optimizer does not support bounds. + + Returns: + The result of the optimization, containing e.g. the result as attribute ``x``. + """ + raise NotImplementedError() + + @property + def gradient_support_level(self): + """Returns gradient support level""" + return self._gradient_support_level + + @property + def is_gradient_ignored(self): + """Returns is gradient ignored""" + return self._gradient_support_level == OptimizerSupportLevel.ignored + + @property + def is_gradient_supported(self): + """Returns is gradient supported""" + return self._gradient_support_level != OptimizerSupportLevel.not_supported + + @property + def is_gradient_required(self): + """Returns is gradient required""" + return self._gradient_support_level == OptimizerSupportLevel.required + + @property + def bounds_support_level(self): + """Returns bounds support level""" + return self._bounds_support_level + + @property + def is_bounds_ignored(self): + """Returns is bounds ignored""" + return self._bounds_support_level == OptimizerSupportLevel.ignored + + @property + def is_bounds_supported(self): + """Returns is bounds supported""" + return self._bounds_support_level != OptimizerSupportLevel.not_supported + + @property + def is_bounds_required(self): + """Returns is bounds required""" + return self._bounds_support_level == OptimizerSupportLevel.required + + @property + def initial_point_support_level(self): + """Returns initial point support level""" + return self._initial_point_support_level + + @property + def is_initial_point_ignored(self): + """Returns is initial point ignored""" + return self._initial_point_support_level == OptimizerSupportLevel.ignored + + @property + def is_initial_point_supported(self): + """Returns is initial point supported""" + return self._initial_point_support_level != OptimizerSupportLevel.not_supported + + @property + def is_initial_point_required(self): + """Returns is initial point required""" + return self._initial_point_support_level == OptimizerSupportLevel.required + + def print_options(self): + """Print algorithm-specific options.""" + for name in sorted(self._options): + logger.debug("%s = %s", name, str(self._options[name])) + + def set_max_evals_grouped(self, limit): + """Set max evals grouped""" + self._max_evals_grouped = limit diff --git a/qiskit_machine_learning/optimizers/optimizer_utils/__init__.py b/qiskit_machine_learning/optimizers/optimizer_utils/__init__.py new file mode 100644 index 000000000..faa3f6d53 --- /dev/null +++ b/qiskit_machine_learning/optimizers/optimizer_utils/__init__.py @@ -0,0 +1,27 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +"""Utils for optimizers + +Optimizer Utils (:mod:`qiskit_algorithms.optimizers.optimizer_utils`) +===================================================================== + +.. autosummary:: + :toctree: ../stubs/ + :nosignatures: + + LearningRate + +""" + +from .learning_rate import LearningRate + +__all__ = ["LearningRate"] diff --git a/qiskit_machine_learning/optimizers/optimizer_utils/learning_rate.py b/qiskit_machine_learning/optimizers/optimizer_utils/learning_rate.py new file mode 100644 index 000000000..3cb070773 --- /dev/null +++ b/qiskit_machine_learning/optimizers/optimizer_utils/learning_rate.py @@ -0,0 +1,88 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2021, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""A class to represent the Learning Rate.""" +from __future__ import annotations + +from collections.abc import Generator, Callable +from itertools import tee +import numpy as np + + +class LearningRate(Generator): + """Represents a Learning Rate. + Will be an attribute of :class:`~.GradientDescentState`. Note that :class:`~.GradientDescent` also + has a learning rate. That learning rate can be a float, a list, an array, a function returning + a generator and will be used to create a generator to be used during the + optimization process. + This class wraps ``Generator`` so that we can also access the last yielded value. + """ + + def __init__( + self, + learning_rate: float + | list[float] + | np.ndarray + | Callable[[], Generator[float, None, None]], + ): + """ + Args: + learning_rate: Used to create a generator to iterate on. + """ + if isinstance(learning_rate, (float, int)): + self._gen = constant(learning_rate) + elif isinstance(learning_rate, Generator): + learning_rate, self._gen = tee(learning_rate) + elif isinstance(learning_rate, (list, np.ndarray)): + self._gen = (eta for eta in learning_rate) + else: + self._gen = learning_rate() + + self._current: float | None = None + + def send(self, value): + """Send a value into the generator. + Return next yielded value or raise StopIteration. + """ + self._current = next(self._gen) + return self.current + + def throw(self, typ, val=None, tb=None): + """Raise an exception in the generator. + Return next yielded value or raise StopIteration. + """ + if val is None: + if tb is None: + raise typ + val = typ() + if tb is not None: + val = val.with_traceback(tb) + raise val + + @property + def current(self): + """Returns the current value of the learning rate.""" + return self._current + + +def constant(learning_rate: float = 0.01) -> Generator[float, None, None]: + """Returns a python generator that always yields the same value. + + Args: + learning_rate: The value to yield. + + Yields: + The learning rate for the next iteration. + """ + + while True: + yield learning_rate diff --git a/qiskit_machine_learning/optimizers/p_bfgs.py b/qiskit_machine_learning/optimizers/p_bfgs.py new file mode 100644 index 000000000..ceb9c574d --- /dev/null +++ b/qiskit_machine_learning/optimizers/p_bfgs.py @@ -0,0 +1,183 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2018, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Parallelized Limited-memory BFGS optimizer""" +from __future__ import annotations + +import logging +import multiprocessing +import platform +from collections.abc import Callable +from typing import SupportsFloat + +import numpy as np + +from ..utils import algorithm_globals +from ..utils.validation import validate_min + +from .optimizer import OptimizerResult, POINT +from .scipy_optimizer import SciPyOptimizer + +logger = logging.getLogger(__name__) + + +class P_BFGS(SciPyOptimizer): # pylint: disable=invalid-name + """ + Parallelized Limited-memory BFGS optimizer. + + P-BFGS is a parallelized version of :class:`L_BFGS_B` with which it shares the same parameters. + P-BFGS can be useful when the target hardware is a quantum simulator running on a classical + machine. This allows the multiple processes to use simulation to potentially reach a minimum + faster. The parallelization may also help the optimizer avoid getting stuck at local optima. + + Uses scipy.optimize.fmin_l_bfgs_b. + For further detail, please refer to + https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.fmin_l_bfgs_b.html + + .. note:: + + This component has some function that is normally random. If you want to reproduce behavior + then you should set the random number generator seed in the algorithm_globals + (``qiskit_algorithms.utils.algorithm_globals.random_seed = seed``). + """ + + _OPTIONS = ["maxfun", "ftol", "iprint"] + + # pylint: disable=unused-argument + def __init__( + self, + maxfun: int = 1000, + ftol: SupportsFloat = 10 * np.finfo(float).eps, + iprint: int = -1, + max_processes: int | None = None, + options: dict | None = None, + max_evals_grouped: int = 1, + **kwargs, + ) -> None: + r""" + Args: + maxfun: Maximum number of function evaluations. + ftol: The iteration stops when (f\^k - f\^{k+1})/max{\|f\^k\|,\|f\^{k+1}\|,1} <= ftol. + iprint: Controls the frequency of output. iprint < 0 means no output; + iprint = 0 print only one line at the last iteration; 0 < iprint < 99 + print also f and \|proj g\| every iprint iterations; iprint = 99 print + details of every iteration except n-vectors; iprint = 100 print also the + changes of active set and final x; iprint > 100 print details of + every iteration including x and g. + max_processes: maximum number of processes allowed, has a min. value of 1 if not None. + options: A dictionary of solver options. + max_evals_grouped: Max number of default gradient evaluations performed simultaneously. + kwargs: additional kwargs for scipy.optimize.minimize. + """ + if max_processes: + validate_min("max_processes", max_processes, 1) + + if options is None: + options = {} + for k, v in list(locals().items()): + if k in self._OPTIONS: + options[k] = v + super().__init__( + method="L-BFGS-B", + options=options, + max_evals_grouped=max_evals_grouped, + **kwargs, + ) + self._max_processes = max_processes + + def minimize( + self, + fun: Callable[[POINT], float], + x0: POINT, + jac: Callable[[POINT], POINT] | None = None, + bounds: list[tuple[float, float]] | None = None, + ) -> OptimizerResult: + x0 = np.asarray(x0) + + num_procs = multiprocessing.cpu_count() - 1 + num_procs = ( + num_procs if self._max_processes is None else min(num_procs, self._max_processes) + ) + num_procs = num_procs if num_procs >= 0 else 0 + + if platform.system() == "Darwin": + # Changed in version 3.8: On macOS, the spawn start method is now the + # default. The fork start method should be considered unsafe as it can + # lead to crashes. + # However P_BFGS doesn't support spawn, so we revert to single process. + num_procs = 0 + logger.warning( + "For MacOS, python >= 3.8, using only current process. " + "Multiple core use not supported." + ) + elif platform.system() == "Windows": + num_procs = 0 + logger.warning( + "For Windows, using only current process. Multiple core use not supported." + ) + + queue: multiprocessing.queues.Queue[tuple[POINT, float, int]] = multiprocessing.Queue() + + # TODO: are automatic bounds a good idea? What if the circuit parameters are not + # just from plain Pauli rotations but have a coefficient? + + # bounds for additional initial points in case bounds has any None values + threshold = 2 * np.pi + if bounds is None: + bounds = [(-threshold, threshold)] * x0.size + low = [(l if l is not None else -threshold) for (l, u) in bounds] + high = [(u if u is not None else threshold) for (l, u) in bounds] + + def optimize_runner(_queue, _i_pt): # Multi-process sampling + _sol, _opt, _nfev = self._optimize(fun, _i_pt, jac, bounds) + _queue.put((_sol, _opt, _nfev)) + + # Start off as many other processes running the optimize (can be 0) + processes = [] + for _ in range(num_procs): + i_pt = algorithm_globals.random().uniform(low, high) # Another random point in bounds + proc = multiprocessing.Process(target=optimize_runner, args=(queue, i_pt)) + processes.append(proc) + proc.start() + + # While the one optimize in this process below runs the other processes will + # be running too. This one runs + # with the supplied initial point. The process ones have their own random one + sol, opt, nfev = self._optimize(fun, x0, jac, bounds) + + for proc in processes: + # For each other process we wait now for it to finish and see if it has + # a better result than above + proc.join() + p_sol, p_opt, p_nfev = queue.get() + if p_opt < opt: + sol, opt = p_sol, p_opt + nfev += p_nfev + + result = OptimizerResult() + result.x = sol + result.fun = opt + result.nfev = nfev + + return result + + def _optimize( + self, + objective_function, + initial_point, + gradient_function=None, + variable_bounds=None, + ) -> tuple[POINT, float, int]: + result = super().minimize( + objective_function, initial_point, gradient_function, variable_bounds + ) + return result.x, result.fun, result.nfev diff --git a/qiskit_machine_learning/optimizers/powell.py b/qiskit_machine_learning/optimizers/powell.py new file mode 100644 index 000000000..1b53131d4 --- /dev/null +++ b/qiskit_machine_learning/optimizers/powell.py @@ -0,0 +1,64 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2018, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Powell optimizer.""" +from __future__ import annotations + +from .scipy_optimizer import SciPyOptimizer + + +class POWELL(SciPyOptimizer): + """ + Powell optimizer. + + The Powell algorithm performs unconstrained optimization; it ignores bounds or + constraints. Powell is a *conjugate direction method*: it performs sequential one-dimensional + minimization along each directional vector, which is updated at + each iteration of the main minimization loop. The function being minimized need not be + differentiable, and no derivatives are taken. + + Uses scipy.optimize.minimize Powell. + For further detail, please refer to + See https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html + """ + + _OPTIONS = ["maxiter", "maxfev", "disp", "xtol"] + + # pylint: disable=unused-argument + def __init__( + self, + maxiter: int | None = None, + maxfev: int = 1000, + disp: bool = False, + xtol: float = 0.0001, + tol: float | None = None, + options: dict | None = None, + **kwargs, + ) -> None: + """ + Args: + maxiter: Maximum allowed number of iterations. If both maxiter and maxfev + are set, minimization will stop at the first reached. + maxfev: Maximum allowed number of function evaluations. If both maxiter and + maxfev are set, minimization will stop at the first reached. + disp: Set to True to print convergence messages. + xtol: Relative error in solution xopt acceptable for convergence. + tol: Tolerance for termination. + options: A dictionary of solver options. + kwargs: additional kwargs for scipy.optimize.minimize. + """ + if options is None: + options = {} + for k, v in list(locals().items()): + if k in self._OPTIONS: + options[k] = v + super().__init__("Powell", options=options, tol=tol, **kwargs) diff --git a/qiskit_machine_learning/optimizers/qnspsa.py b/qiskit_machine_learning/optimizers/qnspsa.py new file mode 100644 index 000000000..aa84f5ab4 --- /dev/null +++ b/qiskit_machine_learning/optimizers/qnspsa.py @@ -0,0 +1,273 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2021, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""The QN-SPSA optimizer.""" + +from __future__ import annotations + +from collections.abc import Iterator +from typing import Any, Callable + +import numpy as np +from qiskit.circuit import QuantumCircuit + +from qiskit.primitives import BaseSampler +from ..state_fidelities import ComputeUncompute + +from .spsa import SPSA, CALLBACK, TERMINATIONCHECKER, _batch_evaluate + +# the function to compute the fidelity +FIDELITY = Callable[[np.ndarray, np.ndarray], float] + + +class QNSPSA(SPSA): + r"""The Quantum Natural SPSA (QN-SPSA) optimizer. + + The QN-SPSA optimizer [1] is a stochastic optimizer that belongs to the family of gradient + descent methods. This optimizer is based on SPSA but attempts to improve the convergence by + sampling the **natural gradient** instead of the vanilla, first-order gradient. It achieves + this by approximating Hessian of the ``fidelity`` of the ansatz circuit. + + Compared to natural gradients, which require :math:`\mathcal{O}(d^2)` expectation value + evaluations for a circuit with :math:`d` parameters, QN-SPSA only requires + :math:`\mathcal{O}(1)` and can therefore significantly speed up the natural gradient calculation + by sacrificing some accuracy. Compared to SPSA, QN-SPSA requires 4 additional function + evaluations of the fidelity. + + The stochastic approximation of the natural gradient can be systematically improved by + increasing the number of ``resamplings``. This leads to a Monte Carlo-style convergence to + the exact, analytic value. + + .. note:: + + This component has some function that is normally random. If you want to reproduce behavior + then you should set the random number generator seed in the algorithm_globals + (``qiskit_algorithms.utils.algorithm_globals.random_seed = seed``). + + Examples: + + This short example runs QN-SPSA for the ground state calculation of the ``Z ^ Z`` + observable where the ansatz is a ``PauliTwoDesign`` circuit. + + .. code-block:: python + + import numpy as np + from qiskit_algorithms.optimizers import QNSPSA + from qiskit.circuit.library import PauliTwoDesign + from qiskit.primitives import Estimator, Sampler + from qiskit.quantum_info import Pauli + + # problem setup + ansatz = PauliTwoDesign(2, reps=1, seed=2) + observable = Pauli("ZZ") + initial_point = np.random.random(ansatz.num_parameters) + + # loss function + estimator = Estimator() + + def loss(x): + result = estimator.run([ansatz], [observable], [x]).result() + return np.real(result.values[0]) + + # fidelity for estimation of the geometric tensor + sampler = Sampler() + fidelity = QNSPSA.get_fidelity(ansatz, sampler) + + # run QN-SPSA + qnspsa = QNSPSA(fidelity, maxiter=300) + result = qnspsa.optimize(ansatz.num_parameters, loss, initial_point=initial_point) + + References: + + [1] J. Gacon et al, "Simultaneous Perturbation Stochastic Approximation of the Quantum + Fisher Information", `arXiv:2103.09232 `_ + + """ + + def __init__( + self, + fidelity: FIDELITY, + maxiter: int = 100, + blocking: bool = True, + allowed_increase: float | None = None, + learning_rate: float | Callable[[], Iterator] | None = None, + perturbation: float | Callable[[], Iterator] | None = None, + resamplings: int | dict[int, int] = 1, + perturbation_dims: int | None = None, + regularization: float | None = None, + hessian_delay: int = 0, + lse_solver: Callable[[np.ndarray, np.ndarray], np.ndarray] | None = None, + initial_hessian: np.ndarray | None = None, + callback: CALLBACK | None = None, + termination_checker: TERMINATIONCHECKER | None = None, + ) -> None: + r""" + Args: + fidelity: A function to compute the fidelity of the ansatz state with itself for + two different sets of parameters. + maxiter: The maximum number of iterations. Note that this is not the maximal number + of function evaluations. + blocking: If True, only accepts updates that improve the loss (up to some allowed + increase, see next argument). + allowed_increase: If ``blocking`` is ``True``, this argument determines by how much + the loss can increase with the proposed parameters and still be accepted. + If ``None``, the allowed increases is calibrated automatically to be twice the + approximated standard deviation of the loss function. + learning_rate: The update step is the learning rate is multiplied with the gradient. + If the learning rate is a float, it remains constant over the course of the + optimization. It can also be a callable returning an iterator which yields the + learning rates for each optimization step. + If ``learning_rate`` is set ``perturbation`` must also be provided. + perturbation: Specifies the magnitude of the perturbation for the finite difference + approximation of the gradients. Can be either a float or a generator yielding + the perturbation magnitudes per step. + If ``perturbation`` is set ``learning_rate`` must also be provided. + resamplings: The number of times the gradient (and Hessian) is sampled using a random + direction to construct a gradient estimate. Per default the gradient is estimated + using only one random direction. If an integer, all iterations use the same number + of resamplings. If a dictionary, this is interpreted as + ``{iteration: number of resamplings per iteration}``. + perturbation_dims: The number of perturbed dimensions. Per default, all dimensions + are perturbed, but a smaller, fixed number can be perturbed. If set, the perturbed + dimensions are chosen uniformly at random. + regularization: To ensure the preconditioner is symmetric and positive definite, the + identity times a small coefficient is added to it. This generator yields that + coefficient. + hessian_delay: Start multiplying the gradient with the inverse Hessian only after a + certain number of iterations. The Hessian is still evaluated and therefore this + argument can be useful to first get a stable average over the last iterations before + using it as preconditioner. + lse_solver: The method to solve for the inverse of the Hessian. Per default an + exact LSE solver is used, but can e.g. be overwritten by a minimization routine. + initial_hessian: The initial guess for the Hessian. By default the identity matrix + is used. + callback: A callback function passed information in each iteration step. The + information is, in this order: the parameters, the function value, the number + of function evaluations, the stepsize, whether the step was accepted. + termination_checker: A callback function executed at the end of each iteration step. The + arguments are, in this order: the parameters, the function value, the number + of function evaluations, the stepsize, whether the step was accepted. If the callback + returns True, the optimization is terminated. + To prevent additional evaluations of the objective method, if the objective has not yet + been evaluated, the objective is estimated by taking the mean of the objective + evaluations used in the estimate of the gradient. + + + """ + super().__init__( + maxiter, + blocking, + allowed_increase, + # trust region *must* be false for natural gradients to work + trust_region=False, + learning_rate=learning_rate, + perturbation=perturbation, + resamplings=resamplings, + callback=callback, + second_order=True, + hessian_delay=hessian_delay, + lse_solver=lse_solver, + regularization=regularization, + perturbation_dims=perturbation_dims, + initial_hessian=initial_hessian, + termination_checker=termination_checker, + ) + + self.fidelity = fidelity + + def _point_sample(self, loss, x, eps, delta1, delta2): + loss_points = [x + eps * delta1, x - eps * delta1] + fidelity_points = [ + (x, x + eps * delta1), + (x, x - eps * delta1), + (x, x + eps * (delta1 + delta2)), + (x, x + eps * (-delta1 + delta2)), + ] + self._nfev += 6 + + loss_values = _batch_evaluate(loss, loss_points, self._max_evals_grouped) + fidelity_values = _batch_evaluate( + self.fidelity, fidelity_points, self._max_evals_grouped, unpack_points=True + ) + + # compute the gradient approximation and additionally return the loss function evaluations + gradient_estimate = (loss_values[0] - loss_values[1]) / (2 * eps) * delta1 + + # compute the preconditioner point estimate + fidelity_values = np.asarray(fidelity_values, dtype=float) + diff = fidelity_values[2] - fidelity_values[0] + diff = diff - (fidelity_values[3] - fidelity_values[1]) + diff = diff / (2 * eps**2) + + rank_one = np.outer(delta1, delta2) + # -0.5 factor comes from the fact that we need -0.5 * fidelity + hessian_estimate = -0.5 * diff * (rank_one + rank_one.T) / 2 + + return np.mean(loss_values), gradient_estimate, hessian_estimate + + @property + def settings(self) -> dict[str, Any]: + """The optimizer settings in a dictionary format.""" + # re-use serialization from SPSA + settings = super().settings + settings.update({"fidelity": self.fidelity}) + + # remove SPSA-specific arguments not in QNSPSA + settings.pop("trust_region") + settings.pop("second_order") + + return settings + + @staticmethod + def get_fidelity( + circuit: QuantumCircuit, + *, + sampler: BaseSampler | None = None, + ) -> Callable[[np.ndarray, np.ndarray], float]: + r"""Get a function to compute the fidelity of ``circuit`` with itself. + + Let ``circuit`` be a parameterized quantum circuit performing the operation + :math:`U(\theta)` given a set of parameters :math:`\theta`. Then this method returns + a function to evaluate + + .. math:: + + F(\theta, \phi) = \big|\langle 0 | U^\dagger(\theta) U(\phi) |0\rangle \big|^2. + + The output of this function can be used as input for the ``fidelity`` to the + :class:`~.QNSPSA` optimizer. + + Args: + circuit: The circuit preparing the parameterized ansatz. + sampler: A sampler primitive to sample from a quantum state. + + Returns: + A handle to the function :math:`F`. + + """ + fid = ComputeUncompute(sampler) + + num_parameters = circuit.num_parameters + + def fidelity(values_x, values_y): + values_x = np.reshape(values_x, (-1, num_parameters)).tolist() + batch_size_x = len(values_x) + + values_y = np.reshape(values_y, (-1, num_parameters)).tolist() + batch_size_y = len(values_y) + + result = fid.run( + batch_size_x * [circuit], batch_size_y * [circuit], values_x, values_y + ).result() + return np.asarray(result.fidelities) + + return fidelity diff --git a/qiskit_machine_learning/optimizers/scipy_optimizer.py b/qiskit_machine_learning/optimizers/scipy_optimizer.py new file mode 100644 index 000000000..789859e85 --- /dev/null +++ b/qiskit_machine_learning/optimizers/scipy_optimizer.py @@ -0,0 +1,177 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2018, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Wrapper class of scipy.optimize.minimize.""" +from __future__ import annotations + +from collections.abc import Callable +from typing import Any + +import numpy as np +from scipy.optimize import minimize + +from ..utils.validation import validate_min +from .optimizer import Optimizer, OptimizerSupportLevel, OptimizerResult, POINT + + +class SciPyOptimizer(Optimizer): + """A general Qiskit Optimizer wrapping scipy.optimize.minimize. + + For further detail, please refer to + https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html + """ + + _bounds_support_methods = {"l-bfgs-b", "tnc", "slsqp", "powell", "trust-constr"} + _gradient_support_methods = { + "cg", + "bfgs", + "newton-cg", + "l-bfgs-b", + "tnc", + "slsqp", + "dogleg", + "trust-ncg", + "trust-krylov", + "trust-exact", + "trust-constr", + } + + def __init__( + self, + method: str | Callable, + options: dict[str, Any] | None = None, + max_evals_grouped: int = 1, + **kwargs, + ): + """ + Args: + method: Type of solver. + options: A dictionary of solver options. + kwargs: additional kwargs for scipy.optimize.minimize. + max_evals_grouped: Max number of default gradient evaluations performed simultaneously. + """ + self._method = method.lower() if isinstance(method, str) else method + # Set support level + if self._method in self._bounds_support_methods: + self._bounds_support_level = OptimizerSupportLevel.supported + else: + self._bounds_support_level = OptimizerSupportLevel.ignored + if self._method in self._gradient_support_methods: + self._gradient_support_level = OptimizerSupportLevel.supported + else: + self._gradient_support_level = OptimizerSupportLevel.ignored + self._initial_point_support_level = OptimizerSupportLevel.required + + self._options = options if options is not None else {} + validate_min("max_evals_grouped", max_evals_grouped, 1) + self._max_evals_grouped = max_evals_grouped + self._kwargs = kwargs + + def get_support_level(self): + """Return support level dictionary""" + return { + "gradient": self._gradient_support_level, + "bounds": self._bounds_support_level, + "initial_point": self._initial_point_support_level, + } + + @property + def settings(self) -> dict[str, Any]: + options = self._options.copy() + if hasattr(self, "_OPTIONS"): + # all _OPTIONS should be keys in self._options, but add a failsafe here + attributes = [ + option + for option in self._OPTIONS # pylint: disable=no-member + if option in options.keys() + ] + + settings = {attr: options.pop(attr) for attr in attributes} + else: + settings = {} + + settings["max_evals_grouped"] = self._max_evals_grouped + settings["options"] = options + settings.update(self._kwargs) + + # the subclasses don't need the "method" key as the class type specifies the method + if self.__class__ == SciPyOptimizer: + settings["method"] = self._method + + return settings + + def minimize( + self, + fun: Callable[[POINT], float], + x0: POINT, + jac: Callable[[POINT], POINT] | None = None, + bounds: list[tuple[float, float]] | None = None, + ) -> OptimizerResult: + # Remove ignored parameters to suppress the warning of scipy.optimize.minimize + if self.is_bounds_ignored: + bounds = None + if self.is_gradient_ignored: + jac = None + + if self.is_gradient_supported and jac is None and self._max_evals_grouped > 1: + if "eps" in self._options: + epsilon = self._options["eps"] + else: + epsilon = ( + 1e-8 if self._method in {"l-bfgs-b", "tnc"} else np.sqrt(np.finfo(float).eps) + ) + jac = Optimizer.wrap_function( + Optimizer.gradient_num_diff, (fun, epsilon, self._max_evals_grouped) + ) + + # Workaround for L_BFGS_B because it does not accept np.ndarray. + # See https://github.com/Qiskit/qiskit/pull/6373. + if jac is not None and self._method == "l-bfgs-b": + jac = self._wrap_gradient(jac) + + # Starting in scipy 1.9.0 maxiter is deprecated and maxfun (added in 1.5.0) + # should be used instead + swapped_deprecated_args = False + if self._method == "tnc" and "maxiter" in self._options: + swapped_deprecated_args = True + self._options["maxfun"] = self._options.pop("maxiter") + + raw_result = minimize( + fun=fun, + x0=x0, + method=self._method, + jac=jac, + bounds=bounds, + options=self._options, + **self._kwargs, + ) + if swapped_deprecated_args: + self._options["maxiter"] = self._options.pop("maxfun") + + result = OptimizerResult() + result.x = raw_result.x + result.fun = raw_result.fun + result.nfev = raw_result.nfev + result.njev = raw_result.get("njev", None) + result.nit = raw_result.get("nit", None) + + return result + + @staticmethod + def _wrap_gradient(gradient_function): + def wrapped_gradient(x): + gradient = gradient_function(x) + if isinstance(gradient, np.ndarray): + return gradient.tolist() + return gradient + + return wrapped_gradient diff --git a/qiskit_machine_learning/optimizers/slsqp.py b/qiskit_machine_learning/optimizers/slsqp.py new file mode 100644 index 000000000..20eb49ef2 --- /dev/null +++ b/qiskit_machine_learning/optimizers/slsqp.py @@ -0,0 +1,73 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2018, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Sequential Least SQuares Programming optimizer""" +from __future__ import annotations + + +from .scipy_optimizer import SciPyOptimizer + + +class SLSQP(SciPyOptimizer): + """ + Sequential Least SQuares Programming optimizer. + + SLSQP minimizes a function of several variables with any combination of bounds, equality + and inequality constraints. The method wraps the SLSQP Optimization subroutine originally + implemented by Dieter Kraft. + + SLSQP is ideal for mathematical problems for which the objective function and the constraints + are twice continuously differentiable. Note that the wrapper handles infinite values in bounds + by converting them into large floating values. + + Uses scipy.optimize.minimize SLSQP. + For further detail, please refer to + See https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html + """ + + _OPTIONS = ["maxiter", "disp", "ftol", "eps"] + + # pylint: disable=unused-argument + def __init__( + self, + maxiter: int = 100, + disp: bool = False, + ftol: float = 1e-06, + tol: float | None = None, + eps: float = 1.4901161193847656e-08, + options: dict | None = None, + max_evals_grouped: int = 1, + **kwargs, + ) -> None: + """ + Args: + maxiter: Maximum number of iterations. + disp: Set to True to print convergence messages. + ftol: Precision goal for the value of f in the stopping criterion. + tol: Tolerance for termination. + eps: Step size used for numerical approximation of the Jacobian. + options: A dictionary of solver options. + max_evals_grouped: Max number of default gradient evaluations performed simultaneously. + kwargs: additional kwargs for scipy.optimize.minimize. + """ + if options is None: + options = {} + for k, v in list(locals().items()): + if k in self._OPTIONS: + options[k] = v + super().__init__( + "SLSQP", + options=options, + tol=tol, + max_evals_grouped=max_evals_grouped, + **kwargs, + ) diff --git a/qiskit_machine_learning/optimizers/snobfit.py b/qiskit_machine_learning/optimizers/snobfit.py new file mode 100644 index 000000000..7596c28f0 --- /dev/null +++ b/qiskit_machine_learning/optimizers/snobfit.py @@ -0,0 +1,129 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2019, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Stable Noisy Optimization by Branch and FIT algorithm (SNOBFIT) optimizer.""" +from __future__ import annotations + +from collections.abc import Callable +from typing import Any + +import numpy as np +from ..exceptions import AlgorithmError +from ..utils import optionals as _optionals +from .optimizer import Optimizer, OptimizerSupportLevel, OptimizerResult, POINT + + +@_optionals.HAS_SKQUANT.require_in_instance +@_optionals.HAS_SQSNOBFIT.require_in_instance +class SNOBFIT(Optimizer): + """Stable Noisy Optimization by Branch and FIT algorithm. + + SnobFit is used for the optimization of derivative-free, noisy objective functions providing + robust and fast solutions of problems with continuous variables varying within bound. + + Uses skquant.opt installed with pip install scikit-quant. + For further detail, please refer to + https://github.com/scikit-quant/scikit-quant and https://qat4chem.lbl.gov/software. + """ + + def __init__( + self, + maxiter: int = 1000, + maxfail: int = 10, + maxmp: int = None, + verbose: bool = False, + ) -> None: + """ + Args: + maxiter: Maximum number of function evaluations. + maxmp: Maximum number of model points requested for the local fit. + Default = 2 * number of parameters + 6 set to this value when None. + maxfail: Maximum number of failures to improve the solution. Stops the algorithm + after maxfail is reached. + verbose: Provide verbose (debugging) output. + + Raises: + MissingOptionalLibraryError: scikit-quant or SQSnobFit not installed + AlgorithmError: If NumPy 1.24.0 or above is installed. + See https://github.com/scikit-quant/scikit-quant/issues/24 for more details. + """ + # check version + if tuple(map(int, np.__version__.split(".")[:2])) >= (1, 24): + raise AlgorithmError( + "SnobFit is incompatible with NumPy 1.24.0 or above, please " + "install a previous version. See also scikit-quant/scikit-quant#24." + ) + + super().__init__() + self._maxiter = maxiter + self._maxfail = maxfail + self._maxmp = maxmp + self._verbose = verbose + + def get_support_level(self): + """Returns support level dictionary.""" + return { + "gradient": OptimizerSupportLevel.ignored, + "bounds": OptimizerSupportLevel.required, + "initial_point": OptimizerSupportLevel.required, + } + + @property + def settings(self) -> dict[str, Any]: + return { + "maxiter": self._maxiter, + "maxfail": self._maxfail, + "maxmp": self._maxmp, + "verbose": self._verbose, + } + + def minimize( + self, + fun: Callable[[POINT], float], + x0: POINT, + jac: Callable[[POINT], POINT] | None = None, + bounds: list[tuple[float, float]] | None = None, + ) -> OptimizerResult: + import skquant.opt as skq + from SQSnobFit import optset + + if bounds is None or any(None in bound_tuple for bound_tuple in bounds): + raise ValueError("Optimizer SNOBFIT requires bounds for all parameters.") + + snobfit_settings = { + "maxmp": self._maxmp, + "maxfail": self._maxfail, + "verbose": self._verbose, + } + options = optset(optin=snobfit_settings) + # counters the error when initial point is outside the acceptable bounds + x0 = np.asarray(x0) + for idx, theta in enumerate(x0): + if abs(theta) > bounds[idx][0]: + x0[idx] = x0[idx] % bounds[idx][0] + elif abs(theta) > bounds[idx][1]: + x0[idx] = x0[idx] % bounds[idx][1] + + res, history = skq.minimize( + fun, + x0, + bounds=bounds, + budget=self._maxiter, + method="snobfit", + options=options, + ) + + optimizer_result = OptimizerResult() + optimizer_result.x = res.optpar + optimizer_result.fun = res.optval + optimizer_result.nfev = len(history) + return optimizer_result diff --git a/qiskit_machine_learning/optimizers/spsa.py b/qiskit_machine_learning/optimizers/spsa.py new file mode 100644 index 000000000..a3d00a12f --- /dev/null +++ b/qiskit_machine_learning/optimizers/spsa.py @@ -0,0 +1,771 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2018, 2024. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Simultaneous Perturbation Stochastic Approximation (SPSA) optimizer. + +This implementation allows both standard first-order and second-order SPSA. +""" +from __future__ import annotations + +from collections import deque +from collections.abc import Iterator +from typing import Callable, Any, SupportsFloat +import logging +import warnings +from time import time + +import scipy +import numpy as np + +from ..utils import algorithm_globals + +from .optimizer import Optimizer, OptimizerSupportLevel, OptimizerResult, POINT + +# number of function evaluations, parameters, loss, stepsize, accepted +CALLBACK = Callable[[int, np.ndarray, float, SupportsFloat, bool], None] +TERMINATIONCHECKER = Callable[[int, np.ndarray, float, SupportsFloat, bool], bool] + +logger = logging.getLogger(__name__) + + +class SPSA(Optimizer): + """Simultaneous Perturbation Stochastic Approximation (SPSA) optimizer. + + SPSA [1] is an gradient descent method for optimizing systems with multiple unknown parameters. + As an optimization method, it is appropriately suited to large-scale population models, + adaptive modeling, and simulation optimization. + + .. seealso:: + + Many examples are presented at the `SPSA Web site `__. + + The main feature of SPSA is the stochastic gradient approximation, which requires only two + measurements of the objective function, regardless of the dimension of the optimization + problem. + + Additionally, to standard first-order SPSA, where only gradient information is used, this + implementation also allows second-order SPSA (2-SPSA) [2]. In 2-SPSA we also estimate the + Hessian of the loss with a stochastic approximation and multiply the gradient with the + inverse Hessian to take local curvature into account and improve convergence. + Notably this Hessian estimate requires only a constant number of function evaluations + unlike an exact evaluation of the Hessian, which scales quadratically in the number of + function evaluations. + + .. note:: + + SPSA can be used in the presence of noise, and it is therefore indicated in situations + involving measurement uncertainty on a quantum computation when finding a minimum. + If you are executing a variational algorithm using a Quantum ASseMbly Language (QASM) + simulator or a real device, SPSA would be the most recommended choice among the optimizers + provided here. + + The optimization process can include a calibration phase if neither the ``learning_rate`` nor + ``perturbation`` is provided, which requires additional functional evaluations. + (Note that either both or none must be set.) For further details on the automatic calibration, + please refer to the supplementary information section IV. of [3]. + + .. note:: + + This component has some function that is normally random. If you want to reproduce behavior + then you should set the random number generator seed in the algorithm_globals + (``qiskit_algorithms.utils.algorithm_globals.random_seed = seed``). + + + Examples: + + This short example runs SPSA for the ground state calculation of the ``Z ^ Z`` + observable where the ansatz is a ``PauliTwoDesign`` circuit. + + .. code-block:: python + + import numpy as np + from qiskit_algorithms.optimizers import SPSA + from qiskit.circuit.library import PauliTwoDesign + from qiskit.primitives import Estimator + from qiskit.quantum_info import SparsePauliOp + + ansatz = PauliTwoDesign(2, reps=1, seed=2) + observable = SparsePauliOp("ZZ") + initial_point = np.random.random(ansatz.num_parameters) + estimator = Estimator() + + def loss(x): + job = estimator.run([ansatz], [observable], [x]) + return job.result().values[0] + + spsa = SPSA(maxiter=300) + result = spsa.minimize(loss, x0=initial_point) + + To use the Hessian information, i.e. 2-SPSA, you can add `second_order=True` to the + initializer of the `SPSA` class, the rest of the code remains the same. + + .. code-block:: python + + two_spsa = SPSA(maxiter=300, second_order=True) + result = two_spsa.minimize(loss, x0=initial_point) + + The `termination_checker` can be used to implement a custom termination criterion. + + .. code-block:: python + + import numpy as np + from qiskit_algorithms.optimizers import SPSA + + def objective(x): + return np.linalg.norm(x) + .04*np.random.rand(1) + + class TerminationChecker: + + def __init__(self, N : int): + self.N = N + self.values = [] + + def __call__(self, nfev, parameters, value, stepsize, accepted) -> bool: + self.values.append(value) + + if len(self.values) > self.N: + last_values = self.values[-self.N:] + pp = np.polyfit(range(self.N), last_values, 1) + slope = pp[0] / self.N + + if slope > 0: + return True + return False + + spsa = SPSA(maxiter=200, termination_checker=TerminationChecker(10)) + result = spsa.minimize(objective, x0=[0.5, 0.5]) + print(f'SPSA completed after {result.nit} iterations') + + References: + + [1]: J. C. Spall (1998). An Overview of the Simultaneous Perturbation Method for Efficient + Optimization, Johns Hopkins APL Technical Digest, 19(4), 482–492. + `Online at jhuapl.edu. `_ + + [2]: J. C. Spall (1997). Accelerated second-order stochastic optimization using only + function measurements, Proceedings of the 36th IEEE Conference on Decision and Control, + 1417-1424 vol.2. `Online at IEEE.org. `_ + + [3]: A. Kandala et al. (2017). Hardware-efficient Variational Quantum Eigensolver for + Small Molecules and Quantum Magnets. Nature 549, pages242–246(2017). + `arXiv:1704.05018v2 `_ + + """ + + def __init__( + self, + maxiter: int = 100, + blocking: bool = False, + allowed_increase: float | None = None, + trust_region: bool = False, + learning_rate: float | np.ndarray | Callable[[], Iterator] | None = None, + perturbation: float | np.ndarray | Callable[[], Iterator] | None = None, + last_avg: int = 1, + resamplings: int | dict[int, int] = 1, + perturbation_dims: int | None = None, + second_order: bool = False, + regularization: float | None = None, + hessian_delay: int = 0, + lse_solver: Callable[[np.ndarray, np.ndarray], np.ndarray] | None = None, + initial_hessian: np.ndarray | None = None, + callback: CALLBACK | None = None, + termination_checker: TERMINATIONCHECKER | None = None, + ) -> None: + r""" + Args: + maxiter: The maximum number of iterations. Note that this is not the maximal number + of function evaluations. + blocking: If True, only accepts updates that improve the loss (up to some allowed + increase, see next argument). + allowed_increase: If ``blocking`` is ``True``, this argument determines by how much + the loss can increase with the proposed parameters and still be accepted. + If ``None``, the allowed increases is calibrated automatically to be twice the + approximated standard deviation of the loss function. + trust_region: If ``True``, restricts the norm of the update step to be :math:`\leq 1`. + learning_rate: The update step is the learning rate is multiplied with the gradient. + If the learning rate is a float, it remains constant over the course of the + optimization. If a NumPy array, the :math:`i`-th element is the learning rate for + the :math:`i`-th iteration. It can also be a callable returning an iterator which + yields the learning rates for each optimization step. + If ``learning_rate`` is set ``perturbation`` must also be provided. + perturbation: Specifies the magnitude of the perturbation for the finite difference + approximation of the gradients. See ``learning_rate`` for the supported types. + If ``perturbation`` is set ``learning_rate`` must also be provided. + last_avg: Return the average of the ``last_avg`` parameters instead of just the + last parameter values. + resamplings: The number of times the gradient (and Hessian) is sampled using a random + direction to construct a gradient estimate. Per default the gradient is estimated + using only one random direction. If an integer, all iterations use the same number + of resamplings. If a dictionary, this is interpreted as + ``{iteration: number of resamplings per iteration}``. + perturbation_dims: The number of perturbed dimensions. Per default, all dimensions + are perturbed, but a smaller, fixed number can be perturbed. If set, the perturbed + dimensions are chosen uniformly at random. + second_order: If True, use 2-SPSA instead of SPSA. In 2-SPSA, the Hessian is estimated + additionally to the gradient, and the gradient is preconditioned with the inverse + of the Hessian to improve convergence. + regularization: To ensure the preconditioner is symmetric and positive definite, the + identity times a small coefficient is added to it. This generator yields that + coefficient. + hessian_delay: Start multiplying the gradient with the inverse Hessian only after a + certain number of iterations. The Hessian is still evaluated and therefore this + argument can be useful to first get a stable average over the last iterations before + using it as preconditioner. + lse_solver: The method to solve for the inverse of the Hessian. Per default an + exact LSE solver is used, but can e.g. be overwritten by a minimization routine. + initial_hessian: The initial guess for the Hessian. By default the identity matrix + is used. + callback: A callback function passed information in each iteration step. The + information is, in this order: the number of function evaluations, the parameters, + the function value, the stepsize, whether the step was accepted. + termination_checker: A callback function executed at the end of each iteration step. The + arguments are, in this order: the parameters, the function value, the number + of function evaluations, the stepsize, whether the step was accepted. If the callback + returns True, the optimization is terminated. + To prevent additional evaluations of the objective method, if the objective has not yet + been evaluated, the objective is estimated by taking the mean of the objective + evaluations used in the estimate of the gradient. + + + Raises: + ValueError: If ``learning_rate`` or ``perturbation`` is an array with less elements + than the number of iterations. + + + """ + super().__init__() + + # general optimizer arguments + self.maxiter = maxiter + self.trust_region = trust_region + self.callback = callback + self.termination_checker = termination_checker + + # if learning rate and perturbation are arrays, check they are sufficiently long + for attr, name in zip([learning_rate, perturbation], ["learning_rate", "perturbation"]): + if isinstance(attr, (list, np.ndarray)): + if len(attr) < maxiter: + raise ValueError(f"Length of {name} is smaller than maxiter ({maxiter}).") + + self.learning_rate = learning_rate + self.perturbation = perturbation + + # SPSA specific arguments + self.blocking = blocking + self.allowed_increase = allowed_increase + self.last_avg = last_avg + self.resamplings = resamplings + self.perturbation_dims = perturbation_dims + + # 2-SPSA specific arguments + if regularization is None: + regularization = 0.01 + + self.second_order = second_order + self.hessian_delay = hessian_delay + self.lse_solver = lse_solver + self.regularization = regularization + self.initial_hessian = initial_hessian + + # runtime arguments + self._nfev: int | None = None # the number of function evaluations + self._smoothed_hessian: np.ndarray | None = None # smoothed average of the Hessians + + @staticmethod + def calibrate( + loss: Callable[[np.ndarray], float], + initial_point: np.ndarray, + c: float = 0.2, + stability_constant: float = 0, + target_magnitude: float | None = None, # 2 pi / 10 + alpha: float = 0.602, + gamma: float = 0.101, + modelspace: bool = False, + max_evals_grouped: int = 1, + ) -> tuple[Callable, Callable]: + r"""Calibrate SPSA parameters with a power series as learning rate and perturbation coeffs. + + The power series are: + + .. math:: + + a_k = \frac{a}{(A + k + 1)^\alpha}, c_k = \frac{c}{(k + 1)^\gamma} + + Args: + loss: The loss function. + initial_point: The initial guess of the iteration. + c: The initial perturbation magnitude. + stability_constant: The value of `A`. + target_magnitude: The target magnitude for the first update step, defaults to + :math:`2\pi / 10`. + alpha: The exponent of the learning rate power series. + gamma: The exponent of the perturbation power series. + modelspace: Whether the target magnitude is the difference of parameter values + or function values (= model space). + max_evals_grouped: The number of grouped evaluations supported by the loss function. + Defaults to 1, i.e. no grouping. + + Returns: + tuple(generator, generator): A tuple of power series generators, the first one for the + learning rate and the second one for the perturbation. + """ + logger.info("SPSA: Starting calibration of learning rate and perturbation.") + if target_magnitude is None: + target_magnitude = 2 * np.pi / 10 + + dim = len(initial_point) + + # compute the average magnitude of the first step + steps = 25 + points = [] + for _ in range(steps): + # compute the random direction + pert = bernoulli_perturbation(dim) + points += [initial_point + c * pert, initial_point - c * pert] + + losses = _batch_evaluate(loss, points, max_evals_grouped) + + avg_magnitudes = 0.0 + for i in range(steps): + delta = losses[2 * i] - losses[2 * i + 1] + avg_magnitudes += np.abs(delta / (2 * c)) + + avg_magnitudes /= steps + + if modelspace: + a = target_magnitude / (avg_magnitudes**2) + else: + a = target_magnitude / avg_magnitudes + + # compute the rescaling factor for correct first learning rate + if a < 1e-10: + warnings.warn(f"Calibration failed, using {target_magnitude} for `a`") + a = target_magnitude + + logger.info("Finished calibration:") + logger.info( + " -- Learning rate: a / ((A + n) ^ alpha) with a = %s, A = %s, alpha = %s", + a, + stability_constant, + alpha, + ) + logger.info(" -- Perturbation: c / (n ^ gamma) with c = %s, gamma = %s", c, gamma) + + # set up the power series + def learning_rate(): + return powerseries(a, alpha, stability_constant) + + def perturbation(): + return powerseries(c, gamma) + + return learning_rate, perturbation + + @staticmethod + def estimate_stddev( + loss: Callable[[np.ndarray], float], + initial_point: np.ndarray, + avg: int = 25, + max_evals_grouped: int = 1, + ) -> float: + """Estimate the standard deviation of the loss function.""" + losses = _batch_evaluate(loss, avg * [initial_point], max_evals_grouped) + return np.std(losses) + + @property + def settings(self) -> dict[str, Any]: + # if learning rate or perturbation are custom iterators expand them + if callable(self.learning_rate): + iterator = self.learning_rate() + learning_rate = np.array([next(iterator) for _ in range(self.maxiter)]) + else: + learning_rate = self.learning_rate # type: ignore[assignment] + + if callable(self.perturbation): + iterator = self.perturbation() + perturbation = np.array([next(iterator) for _ in range(self.maxiter)]) + else: + perturbation = self.perturbation # type: ignore[assignment] + + return { + "maxiter": self.maxiter, + "learning_rate": learning_rate, + "perturbation": perturbation, + "trust_region": self.trust_region, + "blocking": self.blocking, + "allowed_increase": self.allowed_increase, + "resamplings": self.resamplings, + "perturbation_dims": self.perturbation_dims, + "second_order": self.second_order, + "hessian_delay": self.hessian_delay, + "regularization": self.regularization, + "lse_solver": self.lse_solver, + "initial_hessian": self.initial_hessian, + "callback": self.callback, + "termination_checker": self.termination_checker, + } + + def _point_sample(self, loss, x, eps, delta1, delta2): + """A single sample of the gradient at position ``x`` in direction ``delta``.""" + # points to evaluate + points = [x + eps * delta1, x - eps * delta1] + self._nfev += 2 + + if self.second_order: + points += [x + eps * (delta1 + delta2), x + eps * (-delta1 + delta2)] + self._nfev += 2 + + # batch evaluate the points (if possible) + values = _batch_evaluate(loss, points, self._max_evals_grouped) + + plus = values[0] + minus = values[1] + gradient_sample = (plus - minus) / (2 * eps) * delta1 + + hessian_sample = None + if self.second_order: + diff = (values[2] - plus) - (values[3] - minus) + diff /= 2 * eps**2 + + rank_one = np.outer(delta1, delta2) + hessian_sample = diff * (rank_one + rank_one.T) / 2 + + return np.mean(values), gradient_sample, hessian_sample + + def _point_estimate(self, loss, x, eps, num_samples): + """The gradient estimate at point x.""" + # set up variables to store averages + value_estimate = 0 + gradient_estimate = np.zeros(x.size) + hessian_estimate = np.zeros((x.size, x.size)) + + # iterate over the directions + deltas1 = [ + bernoulli_perturbation(x.size, self.perturbation_dims) for _ in range(num_samples) + ] + + if self.second_order: + deltas2 = [ + bernoulli_perturbation(x.size, self.perturbation_dims) for _ in range(num_samples) + ] + else: + deltas2 = None + + for i in range(num_samples): + delta1 = deltas1[i] + delta2 = deltas2[i] if self.second_order else None + + value_sample, gradient_sample, hessian_sample = self._point_sample( + loss, x, eps, delta1, delta2 + ) + value_estimate += value_sample + gradient_estimate += gradient_sample + + if self.second_order: + hessian_estimate += hessian_sample + + return ( + value_estimate / num_samples, + gradient_estimate / num_samples, + hessian_estimate / num_samples, + ) + + def _compute_update(self, loss, x, k, eps, lse_solver): + # compute the perturbations + if isinstance(self.resamplings, dict): + num_samples = self.resamplings.get(k, 1) + else: + num_samples = self.resamplings + + # accumulate the number of samples + value, gradient, hessian = self._point_estimate(loss, x, eps, num_samples) + + # precondition gradient with inverse Hessian, if specified + if self.second_order: + smoothed = k / (k + 1) * self._smoothed_hessian + 1 / (k + 1) * hessian + self._smoothed_hessian = smoothed + + if k > self.hessian_delay: + spd_hessian = _make_spd(smoothed, self.regularization) + + # solve for the gradient update + gradient = np.real(lse_solver(spd_hessian, gradient)) + + return value, gradient + + def minimize( + self, + fun: Callable[[POINT], float], + x0: POINT, + jac: Callable[[POINT], POINT] | None = None, + bounds: list[tuple[float, float]] | None = None, + ) -> OptimizerResult: + # ensure learning rate and perturbation are correctly set: either none or both + # this happens only here because for the calibration the loss function is required + x0 = np.asarray(x0) + if self.learning_rate is None and self.perturbation is None: + get_eta, get_eps = self.calibrate(fun, x0, max_evals_grouped=self._max_evals_grouped) + else: + get_eta, get_eps = _validate_pert_and_learningrate( + self.perturbation, self.learning_rate + ) + eta, eps = get_eta(), get_eps() + + lse_solver = self.lse_solver + if self.lse_solver is None: + lse_solver = np.linalg.solve + + # prepare some initials + x = np.asarray(x0) + if self.initial_hessian is None: + self._smoothed_hessian = np.identity(x.size) + else: + self._smoothed_hessian = self.initial_hessian + + self._nfev = 0 + + # if blocking is enabled we need to keep track of the function values + if self.blocking: + fx = fun(x) # pylint: disable=invalid-name + + self._nfev += 1 + if self.allowed_increase is None: + self.allowed_increase = 2 * self.estimate_stddev( + fun, x, max_evals_grouped=self._max_evals_grouped + ) + + logger.info("SPSA: Starting optimization.") + start = time() + + # keep track of the last few steps to return their average + last_steps = deque([x]) + + # use a local variable and while loop to keep track of the number of iterations + # if the termination checker terminates early + k = 0 + while k < self.maxiter: + k += 1 + iteration_start = time() + # compute update + fx_estimate, update = self._compute_update(fun, x, k, next(eps), lse_solver) + + # trust region + if self.trust_region: + norm = np.linalg.norm(update) + if norm > 1: # stop from dividing by 0 + update = update / norm + + # compute next parameter value + update = update * next(eta) + x_next = x - update + fx_next = None + + # blocking + if self.blocking: + self._nfev += 1 + fx_next = fun(x_next) + + if fx + self.allowed_increase <= fx_next: # accept only if loss improved + if self.callback is not None: + self.callback( + self._nfev, # number of function evals + x_next, # next parameters + fx_next, # loss at next parameters + np.linalg.norm(update), # size of the update step + False, + ) # not accepted + + logger.info( + "Iteration %s/%s rejected in %s.", + k, + self.maxiter + 1, + time() - iteration_start, + ) + continue + fx = fx_next # pylint: disable=invalid-name + + logger.info( + "Iteration %s/%s done in %s.", k, self.maxiter + 1, time() - iteration_start + ) + + if self.callback is not None: + # if we didn't evaluate the function yet, do it now + if not self.blocking: + self._nfev += 1 + fx_next = fun(x_next) + + self.callback( + self._nfev, # number of function evals + x_next, # next parameters + fx_next, # loss at next parameters + np.linalg.norm(update), # size of the update step + True, + ) # accepted + + # update parameters + x = x_next + + # update the list of the last ``last_avg`` parameters + if self.last_avg > 1: + last_steps.append(x_next) + if len(last_steps) > self.last_avg: + last_steps.popleft() + + if self.termination_checker is not None: + fx_check = fx_estimate if fx_next is None else fx_next + if self.termination_checker( + self._nfev, x_next, fx_check, np.linalg.norm(update), True + ): + logger.info("terminated optimization at {k}/{self.maxiter} iterations") + break + + logger.info("SPSA: Finished in %s", time() - start) + + if self.last_avg > 1: + x = np.mean(np.asarray(last_steps), axis=0) + + result = OptimizerResult() + result.x = x + result.fun = fun(x) + result.nfev = self._nfev + result.nit = k + + return result + + def get_support_level(self): + """Get the support level dictionary.""" + return { + "gradient": OptimizerSupportLevel.ignored, + "bounds": OptimizerSupportLevel.ignored, + "initial_point": OptimizerSupportLevel.required, + } + + +def bernoulli_perturbation(dim, perturbation_dims=None): + """Get a Bernoulli random perturbation.""" + if perturbation_dims is None: + return 1 - 2 * algorithm_globals.random().binomial(1, 0.5, size=dim) + + pert = 1 - 2 * algorithm_globals.random().binomial(1, 0.5, size=perturbation_dims) + indices = algorithm_globals.random().choice( + list(range(dim)), size=perturbation_dims, replace=False + ) + result = np.zeros(dim) + result[indices] = pert + + return result + + +def powerseries(eta=0.01, power=2, offset=0): + """Yield a series decreasing by a power law.""" + + n = 1 + while True: + yield eta / ((n + offset) ** power) + n += 1 + + +def constant(eta=0.01): + """Yield a constant series.""" + + while True: + yield eta + + +def _batch_evaluate(function, points, max_evals_grouped, unpack_points=False): + """Evaluate a function on all points with batches of max_evals_grouped. + + The points are a list of inputs, as ``[in1, in2, in3, ...]``. If the individual + inputs are tuples (because the function takes multiple inputs), set ``unpack_points`` to ``True``. + """ + + # if the function cannot handle lists of points as input, cover this case immediately + if max_evals_grouped is None or max_evals_grouped == 1: + # support functions with multiple arguments where the points are given in a tuple + return [ + function(*point) if isinstance(point, tuple) else function(point) for point in points + ] + + num_points = len(points) + + # get the number of batches + num_batches = num_points // max_evals_grouped + if num_points % max_evals_grouped != 0: + num_batches += 1 + + # split the points + batched_points = np.array_split(np.asarray(points), num_batches) + + results = [] + for batch in batched_points: + if unpack_points: + batch = _repack_points(batch) + results += _as_list(function(*batch)) + else: + results += _as_list(function(batch)) + + return results + + +def _as_list(obj): + """Convert a list or numpy array into a list.""" + return obj.tolist() if isinstance(obj, np.ndarray) else obj + + +def _repack_points(points): + """Turn a list of tuples of points into a tuple of lists of points. + E.g. turns + [(a1, a2, a3), (b1, b2, b3)] + into + ([a1, b1], [a2, b2], [a3, b3]) + where all elements are np.ndarray. + """ + num_sets = len(points[0]) # length of (a1, a2, a3) + return ([x[i] for x in points] for i in range(num_sets)) + + +def _make_spd(matrix, bias=0.01): + identity = np.identity(matrix.shape[0]) + psd = scipy.linalg.sqrtm(matrix.dot(matrix)) + return psd + bias * identity + + +def _validate_pert_and_learningrate(perturbation, learning_rate): + if learning_rate is None or perturbation is None: + raise ValueError("If one of learning rate or perturbation is set, both must be set.") + + if isinstance(perturbation, float): + + def get_eps(): + return constant(perturbation) + + elif isinstance(perturbation, (list, np.ndarray)): + + def get_eps(): + return iter(perturbation) + + else: + get_eps = perturbation + + if isinstance(learning_rate, float): + + def get_eta(): + return constant(learning_rate) + + elif isinstance(learning_rate, (list, np.ndarray)): + + def get_eta(): + return iter(learning_rate) + + else: + get_eta = learning_rate + + return get_eta, get_eps diff --git a/qiskit_machine_learning/optimizers/steppable_optimizer.py b/qiskit_machine_learning/optimizers/steppable_optimizer.py new file mode 100644 index 000000000..e9ffda9be --- /dev/null +++ b/qiskit_machine_learning/optimizers/steppable_optimizer.py @@ -0,0 +1,303 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""SteppableOptimizer interface""" +from __future__ import annotations + +from abc import abstractmethod, ABC +from collections.abc import Callable +from dataclasses import dataclass +from .optimizer import Optimizer, POINT, OptimizerResult + + +@dataclass +class AskData(ABC): + """Base class for return type of :meth:`~.SteppableOptimizer.ask`. + + Args: + x_fun: Point or list of points where the function needs to be evaluated to compute the next + state of the optimizer. + x_jac: Point or list of points where the gradient/jacobian needs to be evaluated to compute + the next state of the optimizer. + + """ + + x_fun: POINT | list[POINT] | None = None + x_jac: POINT | list[POINT] | None = None + + +@dataclass +class TellData(ABC): + """Base class for argument type of :meth:`~.SteppableOptimizer.tell`. + + Args: + eval_fun: Image of the function at :attr:`~.ask_data.x_fun`. + eval_jac: Image of the gradient-jacobian at :attr:`~.ask_data.x_jac`. + + """ + + eval_fun: float | list[float] | None = None + eval_jac: POINT | list[POINT] | None = None + + +@dataclass +class OptimizerState: + """Base class representing the state of the optimizer. + + This class stores the current state of the optimizer, given by the current point and + (optionally) information like the function value, the gradient or the number of + function evaluations. This dataclass can also store any other individual variables that + change during the optimization. + + """ + + x: POINT + """Current optimization parameters.""" + fun: Callable[[POINT], float] | None + """Function being optimized.""" + jac: Callable[[POINT], POINT] | None + """Jacobian of the function being optimized.""" + nfev: int | None + """Number of function evaluations so far in the optimization.""" + njev: int | None + """Number of jacobian evaluations so far in the optimization.""" + nit: int | None + """Number of optimization steps performed so far in the optimization.""" + + +class SteppableOptimizer(Optimizer): + """ + Base class for a steppable optimizer. + + This family of optimizers uses the `ask and tell interface + `_. + When using this interface the user has to call :meth:`~.ask` to get information about + how to evaluate the function (we are asking the optimizer about how to do the evaluation). + This information is typically the next points at which the function is evaluated, but depending + on the optimizer it can also determine whether to evaluate the function or its gradient. + Once the function has been evaluated, the user calls the method :meth:`~..tell` + to tell the optimizer what the result of the function evaluation(s) is. The optimizer then + updates its state accordingly and the user can decide whether to stop the optimization process + or to repeat a step. + + This interface is more customizable, and allows the user to have full control over the evaluation + of the function. + + Examples: + + An example where the evaluation of the function has a chance of failing. The user, with + specific knowledge about his function can catch this errors and handle them before passing + the result to the optimizer. + + .. code-block:: python + + import random + import numpy as np + from qiskit_algorithms.optimizers import GradientDescent + + def objective(x): + if random.choice([True, False]): + return None + else: + return (np.linalg.norm(x) - 1) ** 2 + + def grad(x): + if random.choice([True, False]): + return None + else: + return 2 * (np.linalg.norm(x) - 1) * x / np.linalg.norm(x) + + + initial_point = np.random.normal(0, 1, size=(100,)) + + optimizer = GradientDescent(maxiter=20) + optimizer.start(x0=initial_point, fun=objective, jac=grad) + + while optimizer.continue_condition(): + ask_data = optimizer.ask() + evaluated_gradient = None + + while evaluated_gradient is None: + evaluated_gradient = grad(ask_data.x_center) + optimizer.state.njev += 1 + + optimizer.state.nit += 1 + + cf = TellData(eval_jac=evaluated_gradient) + optimizer.tell(ask_data=ask_data, tell_data=tell_data) + + result = optimizer.create_result() + + + Users that aren't dealing with complicated functions and who are more familiar with step by step + optimization algorithms can use the :meth:`~.step` method which wraps the :meth:`~.ask` + and :meth:`~.tell` methods. In the same spirit the method :meth:`~.minimize` will optimize the + function and return the result. + + To see other libraries that use this interface one can visit: + https://optuna.readthedocs.io/en/stable/tutorial/20_recipes/009_ask_and_tell.html + + + """ + + def __init__( + self, + maxiter: int = 100, + ): + """ + Args: + maxiter: Number of steps in the optimization process before ending the loop. + """ + super().__init__() + self._state: OptimizerState | None = None + self.maxiter = maxiter + + @property + def state(self) -> OptimizerState: + """Return the current state of the optimizer.""" + return self._state + + @state.setter + def state(self, state: OptimizerState) -> None: + """Set the current state of the optimizer.""" + self._state = state + + def ask(self) -> AskData: + """Ask the optimizer for a set of points to evaluate. + + This method asks the optimizer which are the next points to evaluate. + These points can, e.g., correspond to function values and/or its derivative. + It may also correspond to variables that let the user infer which points to evaluate. + It is the first method inside of a :meth:`~.step` in the optimization process. + + Returns: + An object containing the data needed to make the function evaluation to advance the + optimization process. + + """ + raise NotImplementedError + + def tell(self, ask_data: AskData, tell_data: TellData) -> None: + """Updates the optimization state using the results of the function evaluation. + + A canonical optimization example using :meth:`~.ask` and :meth:`~.tell` can be seen + in :meth:`~.step`. + + Args: + ask_data: Contains the information on how the evaluation was done. + tell_data: Contains all relevant information about the evaluation of the objective + function. + """ + raise NotImplementedError + + @abstractmethod + def evaluate(self, ask_data: AskData) -> TellData: + """Evaluates the function according to the instructions contained in :attr:`~.ask_data`. + + If the user decides to use :meth:`~.step` instead of :meth:`~.ask` and :meth:`~.tell` + this function will contain the logic on how to evaluate the function. + + Args: + ask_data: Contains the information on how to do the evaluation. + + Returns: + Data of all relevant information about the function evaluation. + + """ + raise NotImplementedError + + def _callback_wrapper(self) -> None: + """ + Wraps the callback function to accommodate each optimizer. + """ + pass + + def step(self) -> None: + """Performs one step in the optimization process. + + This method composes :meth:`~.ask`, :meth:`~.evaluate`, and :meth:`~.tell` to make a "step" + in the optimization process. + """ + ask_data = self.ask() + tell_data = self.evaluate(ask_data=ask_data) + self.tell(ask_data=ask_data, tell_data=tell_data) + + # pylint: disable=invalid-name + @abstractmethod + def start( + self, + fun: Callable[[POINT], float], + x0: POINT, + jac: Callable[[POINT], POINT] | None = None, + bounds: list[tuple[float, float]] | None = None, + ) -> None: + """Populates the state of the optimizer with the data provided and sets all the counters to 0. + + Args: + fun: Function to minimize. + x0: Initial point. + jac: Function to compute the gradient. + bounds: Bounds of the search space. + + """ + raise NotImplementedError + + def minimize( + self, + fun: Callable[[POINT], float], + x0: POINT, + jac: Callable[[POINT], POINT] | None = None, + bounds: list[tuple[float, float]] | None = None, + ) -> OptimizerResult: + """Minimizes the function. + + For well behaved functions the user can call this method to minimize a function. + If the user wants more control on how to evaluate the function a custom loop can be + created using :meth:`~.ask` and :meth:`~.tell` and evaluating the function manually. + + Args: + fun: Function to minimize. + x0: Initial point. + jac: Function to compute the gradient. + bounds: Bounds of the search space. + + Returns: + Object containing the result of the optimization. + + """ + self.start(x0=x0, fun=fun, jac=jac, bounds=bounds) + while self.continue_condition(): + self.step() + self._callback_wrapper() + return self.create_result() + + @abstractmethod + def create_result(self) -> OptimizerResult: + """Returns the result of the optimization. + + All the information needed to create such a result should be stored in the optimizer state + and will typically contain the best point found, the function value and gradient at that point, + the number of function and gradient evaluation and the number of iterations in the optimization. + + Returns: + The result of the optimization process. + + """ + raise NotImplementedError + + def continue_condition(self) -> bool: + """Condition that indicates the optimization process should continue. + + Returns: + ``True`` if the optimization process should continue, ``False`` otherwise. + """ + return self.state.nit < self.maxiter diff --git a/qiskit_machine_learning/optimizers/tnc.py b/qiskit_machine_learning/optimizers/tnc.py new file mode 100644 index 000000000..1d9665b5f --- /dev/null +++ b/qiskit_machine_learning/optimizers/tnc.py @@ -0,0 +1,83 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2018, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Truncated Newton (TNC) optimizer.""" +from __future__ import annotations + + +from .scipy_optimizer import SciPyOptimizer + + +class TNC(SciPyOptimizer): + """ + Truncated Newton (TNC) optimizer. + + TNC uses a truncated Newton algorithm to minimize a function with variables subject to bounds. + This algorithm uses gradient information; it is also called Newton Conjugate-Gradient. + It differs from the :class:`CG` method as it wraps a C implementation and allows each variable + to be given upper and lower bounds. + + Uses scipy.optimize.minimize TNC + For further detail, please refer to + See https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html + """ + + _OPTIONS = ["maxiter", "disp", "accuracy", "ftol", "xtol", "gtol", "eps"] + + # pylint: disable=unused-argument + def __init__( + self, + maxiter: int = 100, + disp: bool = False, + accuracy: float = 0, + ftol: float = -1, + xtol: float = -1, + gtol: float = -1, + tol: float | None = None, + eps: float = 1e-08, + options: dict | None = None, + max_evals_grouped: int = 1, + **kwargs, + ) -> None: + """ + Args: + maxiter: Maximum number of function evaluation. + disp: Set to True to print convergence messages. + accuracy: Relative precision for finite difference calculations. + If <= machine_precision, set to sqrt(machine_precision). Defaults to 0. + ftol: Precision goal for the value of f in the stopping criterion. + If ftol < 0.0, ftol is set to 0.0 defaults to -1. + xtol: Precision goal for the value of x in the stopping criterion + (after applying x scaling factors). + If xtol < 0.0, xtol is set to sqrt(machine_precision). Defaults to -1. + gtol: Precision goal for the value of the projected gradient in + the stopping criterion (after applying x scaling factors). + If gtol < 0.0, gtol is set to 1e-2 * sqrt(accuracy). + Setting it to 0.0 is not recommended. Defaults to -1. + tol: Tolerance for termination. + eps: Step size used for numerical approximation of the Jacobian. + options: A dictionary of solver options. + max_evals_grouped: Max number of default gradient evaluations performed simultaneously. + kwargs: additional kwargs for scipy.optimize.minimize. + """ + if options is None: + options = {} + for k, v in list(locals().items()): + if k in self._OPTIONS: + options[k] = v + super().__init__( + "TNC", + options=options, + tol=tol, + max_evals_grouped=max_evals_grouped, + **kwargs, + ) diff --git a/qiskit_machine_learning/optimizers/umda.py b/qiskit_machine_learning/optimizers/umda.py new file mode 100644 index 000000000..bc708b352 --- /dev/null +++ b/qiskit_machine_learning/optimizers/umda.py @@ -0,0 +1,348 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2018, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Univariate Marginal Distribution Algorithm (Estimation-of-Distribution-Algorithm).""" + +from __future__ import annotations + +from collections.abc import Callable +from typing import Any + +import numpy as np +from scipy.stats import norm + +from ..utils import algorithm_globals +from .optimizer import OptimizerResult, POINT +from .scipy_optimizer import Optimizer, OptimizerSupportLevel + + +class UMDA(Optimizer): + """Continuous Univariate Marginal Distribution Algorithm (UMDA). + + UMDA [1] is a specific type of Estimation of Distribution Algorithm (EDA) where new individuals + are sampled from univariate normal distributions and are updated in each iteration of the + algorithm by the best individuals found in the previous iteration. + + .. seealso:: + + This original implementation of the UDMA optimizer for Qiskit was inspired by my + (Vicente P. Soloviev) work on the EDAspy Python package [2]. + + EDAs are stochastic search algorithms and belong to the family of the evolutionary algorithms. + The main difference is that EDAs have a probabilistic model which is updated in each iteration + from the best individuals of previous generations (elite selection). Depending on the complexity + of the probabilistic model, EDAs can be classified in different ways. In this case, UMDA is a + univariate EDA as the embedded probabilistic model is univariate. + + UMDA has been compared to some of the already implemented algorithms in Qiskit library to + optimize the parameters of variational algorithms such as QAOA or VQE and competitive results + have been obtained [1]. UMDA seems to provide very good solutions for those circuits in which + the number of layers is not big. + + The optimization process can be personalized depending on the parameters chosen in the + initialization. The main parameter is the population size. The bigger it is, the final result + will be better. However, this increases the complexity of the algorithm and the runtime will + be much heavier. In the work [1] different experiments have been performed where population + size has been set to 20 - 30. + + .. note:: + + The UMDA implementation has more parameters but these have default values for the + initialization for better understanding of the user. For example, ``\alpha`` parameter has + been set to 0.5 and is the percentage of the population which is selected in each iteration + to update the probabilistic model. + + + Example: + + This short example runs UMDA to optimize the parameters of a variational algorithm. Here we + will use the same operator as used in the algorithms introduction, which was originally + computed by Qiskit Nature for an H2 molecule. The minimum energy of the H2 Hamiltonian can + be found quite easily so we are able to set maxiters to a small value. + + .. code-block:: python + + from qiskit_algorithms.optimizers import UMDA + from qiskit_algorithms import QAOA + from qiskit.quantum_info import Pauli + from qiskit.primitives import Sampler + + X = Pauli("X") + I = Pauli("I") + Z = Pauli("Z") + + H2_op = (-1.052373245772859 * I ^ I) + \ + (0.39793742484318045 * I ^ Z) + \ + (-0.39793742484318045 * Z ^ I) + \ + (-0.01128010425623538 * Z ^ Z) + \ + (0.18093119978423156 * X ^ X) + + p = 2 # Toy example: 2 layers with 2 parameters in each layer: 4 variables + + opt = UMDA(maxiter=100, size_gen=20) + qaoa = QAOA(Sampler(), opt,reps=p) + result = qaoa.compute_minimum_eigenvalue(operator=H2_op) + + If it is desired to modify the percentage of individuals considered to update the + probabilistic model, then this code can be used. Here for example we set the 60% instead + of the 50% predefined. + + .. code-block:: python + + opt = UMDA(maxiter=100, size_gen=20, alpha = 0.6) + qaoa = QAOA(Sampler(), opt,reps=p) + result = qaoa.compute_minimum_eigenvalue(operator=H2_op) + + .. note:: + + This component has some function that is normally random. If you want to reproduce behavior + then you should set the random number generator seed in the algorithm_globals + (``qiskit_algorithms.utils.algorithm_globals.random_seed = seed``). + + References: + + [1]: Vicente P. Soloviev, Pedro Larrañaga and Concha Bielza (2022, July). Quantum Parametric + Circuit Optimization with Estimation of Distribution Algorithms. In 2022 The Genetic and + Evolutionary Computation Conference (GECCO). DOI: https://doi.org/10.1145/3520304.3533963 + + [2]: Vicente P. Soloviev. Python package EDAspy. + https://github.com/VicentePerezSoloviev/EDAspy. + """ + + ELITE_FACTOR = 0.4 + STD_BOUND = 0.3 + + def __init__( + self, + maxiter: int = 100, + size_gen: int = 20, + alpha: float = 0.5, + callback: Callable[[int, np.ndarray, float], None] | None = None, + ) -> None: + r""" + Args: + maxiter: Maximum number of iterations. + size_gen: Population size of each generation. + alpha: Percentage (0, 1] of the population to be selected as elite selection. + callback: A callback function passed information in each iteration step. The + information is, in this order: the number of function evaluations, the parameters, + the best function value in this iteration. + """ + + self.size_gen = size_gen + self.maxiter = maxiter + self.alpha = alpha + self._vector: np.ndarray | None = None + # initialization of generation + self._generation: np.ndarray | None = None + self._dead_iter = int(self._maxiter / 5) + + self._truncation_length = int(size_gen * alpha) + + super().__init__() + + self._best_cost_global: float | None = None + self._best_ind_global: np.ndarray | None = None + self._evaluations: np.ndarray | None = None + + self._n_variables: int | None = None + + self.callback = callback + + def _initialization(self) -> np.ndarray: + vector = np.zeros((4, self._n_variables)) + + vector[0, :] = np.pi # mu + vector[1, :] = 0.5 # std + + return vector + + # build a generation of size SIZE_GEN from prob vector + def _new_generation(self): + """Build a new generation sampled from the vector of probabilities. + Updates the generation pandas dataframe + """ + + gen = algorithm_globals.random().normal( + self._vector[0, :], self._vector[1, :], [self._size_gen, self._n_variables] + ) + + self._generation = self._generation[: int(self.ELITE_FACTOR * len(self._generation))] + self._generation = np.vstack((self._generation, gen)) + + # truncate the generation at alpha percent + def _truncation(self): + """Selection of the best individuals of the actual generation. + Updates the generation by selecting the best individuals. + """ + best_indices = self._evaluations.argsort()[: self._truncation_length] + self._generation = self._generation[best_indices, :] + self._evaluations = np.take(self._evaluations, best_indices) + + # check each individual of the generation + def _check_generation(self, objective_function): + """Check the cost of each individual in the cost function implemented by the user.""" + self._evaluations = np.apply_along_axis(objective_function, 1, self._generation) + + # update the probability vector + def _update_vector(self): + """From the best individuals update the vector of normal distributions in order to the next + generation can sample from it. Update the vector of normal distributions + """ + + for i in range(self._n_variables): + self._vector[0, i], self._vector[1, i] = norm.fit(self._generation[:, i]) + if self._vector[1, i] < self.STD_BOUND: + self._vector[1, i] = self.STD_BOUND + + def minimize( + self, + fun: Callable[[POINT], float], + x0: POINT, + jac: Callable[[POINT], POINT] | None = None, + bounds: list[tuple[float, float]] | None = None, + ) -> OptimizerResult: + + not_better_count = 0 + result = OptimizerResult() + + if isinstance(x0, float): + x0 = np.asarray([x0]) + self._n_variables = len(x0) + + self._best_cost_global = 999999999999 + self._best_ind_global = None + history = [] + self._evaluations = np.array(0) + + self._vector = self._initialization() + + # initialization of generation + self._generation = algorithm_globals.random().normal( + self._vector[0, :], self._vector[1, :], [self._size_gen, self._n_variables] + ) + + for _ in range(self._maxiter): + self._check_generation(fun) + self._truncation() + self._update_vector() + + best_mae_local: float = min(self._evaluations) + + history.append(best_mae_local) + best_ind_local = np.where(self._evaluations == best_mae_local)[0][0] + best_ind_local = self._generation[best_ind_local] + + # update the best values ever + if best_mae_local < self._best_cost_global: + self._best_cost_global = best_mae_local + self._best_ind_global = best_ind_local + not_better_count = 0 + + else: + not_better_count += 1 + if not_better_count >= self._dead_iter: + break + + if self.callback is not None: + self.callback( + len(history) * self._size_gen, self._best_ind_global, self._best_cost_global + ) + + self._new_generation() + + result.x = self._best_ind_global + result.fun = self._best_cost_global + result.nfev = len(history) * self._size_gen + + return result + + @property + def size_gen(self) -> int: + """Returns the size of the generations (number of individuals per generation)""" + return self._size_gen + + @size_gen.setter + def size_gen(self, value: int): + """ + Sets the size of the generations of the algorithm. + + Args: + value: Size of the generations (number of individuals per generation). + + Raises: + ValueError: If `value` is lower than 1. + """ + if value <= 0: + raise ValueError("The size of the generation should be greater than 0.") + self._size_gen = value + + @property + def maxiter(self) -> int: + """Returns the maximum number of iterations""" + return self._maxiter + + @maxiter.setter + def maxiter(self, value: int): + """ + Sets the maximum number of iterations of the algorithm. + + Args: + value: Maximum number of iterations of the algorithm. + + Raises: + ValueError: If `value` is lower than 1. + """ + if value <= 0: + raise ValueError("The maximum number of iterations should be greater than 0.") + + self._maxiter = value + + @property + def alpha(self) -> float: + """Returns the alpha parameter value (percentage of population selected to update + probabilistic model)""" + return self._alpha + + @alpha.setter + def alpha(self, value: float): + """ + Sets the alpha parameter (percentage of individuals selected to update the probabilistic + model) + + Args: + value: Percentage (0,1] of generation selected to update the probabilistic model. + + Raises: + ValueError: If `value` is lower than 0 or greater than 1. + """ + if (value <= 0) or (value > 1): + raise ValueError(f"alpha must be in the range (0, 1], value given was {value}") + + self._alpha = value + + @property + def settings(self) -> dict[str, Any]: + return { + "maxiter": self.maxiter, + "alpha": self.alpha, + "size_gen": self.size_gen, + "callback": self.callback, + } + + def get_support_level(self): + """Get the support level dictionary.""" + return { + "gradient": OptimizerSupportLevel.ignored, + "bounds": OptimizerSupportLevel.ignored, + "initial_point": OptimizerSupportLevel.required, + } diff --git a/qiskit_machine_learning/state_fidelities/__init__.py b/qiskit_machine_learning/state_fidelities/__init__.py new file mode 100644 index 000000000..47e89f31f --- /dev/null +++ b/qiskit_machine_learning/state_fidelities/__init__.py @@ -0,0 +1,44 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +""" +State Fidelities (:mod:`qiskit_algorithms.state_fidelities`) +============================================================ +Algorithms that compute the fidelity of pairs of quantum states. + +.. currentmodule:: qiskit_algorithms.state_fidelities + +State Fidelities +---------------- + +.. autosummary:: + :toctree: ../stubs/ + :nosignatures: + + BaseStateFidelity + ComputeUncompute + +Results +------- + +.. autosummary:: + :toctree: ../stubs/ + :nosignatures: + + StateFidelityResult + +""" + +from .base_state_fidelity import BaseStateFidelity +from .compute_uncompute import ComputeUncompute +from .state_fidelity_result import StateFidelityResult + +__all__ = ["BaseStateFidelity", "ComputeUncompute", "StateFidelityResult"] diff --git a/qiskit_machine_learning/state_fidelities/base_state_fidelity.py b/qiskit_machine_learning/state_fidelities/base_state_fidelity.py new file mode 100644 index 000000000..4ca041edf --- /dev/null +++ b/qiskit_machine_learning/state_fidelities/base_state_fidelity.py @@ -0,0 +1,315 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +""" +Base state fidelity interface +""" + +from __future__ import annotations +from abc import ABC, abstractmethod +from collections.abc import MutableMapping +from typing import cast, Sequence, List +import numpy as np + +from qiskit import QuantumCircuit +from qiskit.circuit import ParameterVector +from qiskit.primitives.utils import _circuit_key + +from ..algorithm_job import AlgorithmJob + + +class BaseStateFidelity(ABC): + r""" + An interface to calculate state fidelities (state overlaps) for pairs of + (parametrized) quantum circuits. The calculation depends on the particular + fidelity method implementation, but can be always defined as the state overlap: + + .. math:: + + |\langle\psi(x)|\phi(y)\rangle|^2 + + where :math:`x` and :math:`y` are optional parametrizations of the + states :math:`\psi` and :math:`\phi` prepared by the circuits + ``circuit_1`` and ``circuit_2``, respectively. + + """ + + def __init__(self) -> None: + + # use cache for preventing unnecessary circuit compositions + self._circuit_cache: MutableMapping[tuple[int, int], QuantumCircuit] = {} + + @staticmethod + def _preprocess_values( + circuits: QuantumCircuit | Sequence[QuantumCircuit], + values: Sequence[float] | Sequence[Sequence[float]] | None = None, + ) -> Sequence[list[float]]: + """ + Checks whether the passed values match the shape of the parameters + of the corresponding circuits and formats values to 2D list. + + Args: + circuits: List of circuits to be checked. + values: Parameter values corresponding to the circuits to be checked. + + Returns: + A 2D value list if the values match the circuits, or an empty 2D list + if values is None. + + Raises: + ValueError: if the number of parameter values doesn't match the number of + circuit parameters + TypeError: if the input values are not a sequence. + """ + + if isinstance(circuits, QuantumCircuit): + circuits = [circuits] + + if values is None: + for circuit in circuits: + if circuit.num_parameters != 0: + raise ValueError( + f"`values` cannot be `None` because circuit <{circuit.name}> has " + f"{circuit.num_parameters} free parameters." + ) + return [[]] + else: + + # Support ndarray + if isinstance(values, np.ndarray): + values = values.tolist() + if len(values) > 0 and isinstance(values[0], np.ndarray): + values = [v.tolist() for v in values] + + if not isinstance(values, Sequence): + raise TypeError( + f"Expected a sequence of numerical parameter values, " + f"but got input type {type(values)} instead." + ) + + # ensure 2d + if len(values) > 0 and not isinstance(values[0], Sequence) or len(values) == 0: + values = [cast(List[float], values)] + + # we explicitly cast the type here because mypy appears to be unable to understand the + # above few lines where we ensure that values are 2d + return cast(Sequence[List[float]], values) + + def _check_qubits_match(self, circuit_1: QuantumCircuit, circuit_2: QuantumCircuit) -> None: + """ + Checks that the number of qubits of 2 circuits matches. + Args: + circuit_1: (Parametrized) quantum circuit. + circuit_2: (Parametrized) quantum circuit. + + Raises: + ValueError: when ``circuit_1`` and ``circuit_2`` don't have the + same number of qubits. + """ + + if circuit_1.num_qubits != circuit_2.num_qubits: + raise ValueError( + f"The number of qubits for the first circuit ({circuit_1.num_qubits}) " + f"and second circuit ({circuit_2.num_qubits}) are not the same." + ) + + @abstractmethod + def create_fidelity_circuit( + self, circuit_1: QuantumCircuit, circuit_2: QuantumCircuit + ) -> QuantumCircuit: + """ + Implementation-dependent method to create a fidelity circuit + from 2 circuit inputs. + + Args: + circuit_1: (Parametrized) quantum circuit. + circuit_2: (Parametrized) quantum circuit. + + Returns: + The fidelity quantum circuit corresponding to ``circuit_1`` and ``circuit_2``. + """ + raise NotImplementedError + + def _construct_circuits( + self, + circuits_1: QuantumCircuit | Sequence[QuantumCircuit], + circuits_2: QuantumCircuit | Sequence[QuantumCircuit], + ) -> Sequence[QuantumCircuit]: + """ + Constructs the list of fidelity circuits to be evaluated. + These circuits represent the state overlap between pairs of input circuits, + and their construction depends on the fidelity method implementations. + + Args: + circuits_1: (Parametrized) quantum circuits. + circuits_2: (Parametrized) quantum circuits. + + Returns: + List of constructed fidelity circuits. + + Raises: + ValueError: if the length of the input circuit lists doesn't match. + """ + + if isinstance(circuits_1, QuantumCircuit): + circuits_1 = [circuits_1] + if isinstance(circuits_2, QuantumCircuit): + circuits_2 = [circuits_2] + + if len(circuits_1) != len(circuits_2): + raise ValueError( + f"The length of the first circuit list({len(circuits_1)}) " + f"and second circuit list ({len(circuits_2)}) is not the same." + ) + + circuits = [] + for (circuit_1, circuit_2) in zip(circuits_1, circuits_2): + + # Use the same key for circuits as qiskit.primitives use. + circuit = self._circuit_cache.get((_circuit_key(circuit_1), _circuit_key(circuit_2))) + + if circuit is not None: + circuits.append(circuit) + else: + self._check_qubits_match(circuit_1, circuit_2) + + # re-parametrize input circuits + # TODO: make smarter checks to avoid unnecessary re-parametrizations + parameters_1 = ParameterVector("x", circuit_1.num_parameters) + parametrized_circuit_1 = circuit_1.assign_parameters(parameters_1) + parameters_2 = ParameterVector("y", circuit_2.num_parameters) + parametrized_circuit_2 = circuit_2.assign_parameters(parameters_2) + + circuit = self.create_fidelity_circuit( + parametrized_circuit_1, parametrized_circuit_2 + ) + circuits.append(circuit) + # update cache + self._circuit_cache[_circuit_key(circuit_1), _circuit_key(circuit_2)] = circuit + + return circuits + + def _construct_value_list( + self, + circuits_1: Sequence[QuantumCircuit], + circuits_2: Sequence[QuantumCircuit], + values_1: Sequence[float] | Sequence[Sequence[float]] | None = None, + values_2: Sequence[float] | Sequence[Sequence[float]] | None = None, + ) -> list[list[float]]: + """ + Preprocesses input parameter values to match the fidelity + circuit parametrization, and return in list format. + + Args: + circuits_1: (Parametrized) quantum circuits preparing the + first list of quantum states. + circuits_2: (Parametrized) quantum circuits preparing the + second list of quantum states. + values_1: Numerical parameters to be bound to the first circuits. + values_2: Numerical parameters to be bound to the second circuits. + + Returns: + List of lists of parameter values for fidelity circuit. + + """ + values_1 = self._preprocess_values(circuits_1, values_1) + values_2 = self._preprocess_values(circuits_2, values_2) + # now, values_1 and values_2 are explicitly made 2d lists + + values = [] + if len(values_2[0]) == 0: + values = list(values_1) + elif len(values_1[0]) == 0: + values = list(values_2) + else: + for (val_1, val_2) in zip(values_1, values_2): + # the `+` operation concatenates the lists + # and then this new list gets appended to the values list + values.append(val_1 + val_2) + + # values is guaranteed to be 2d + return values + + @abstractmethod + def _run( + self, + circuits_1: QuantumCircuit | Sequence[QuantumCircuit], + circuits_2: QuantumCircuit | Sequence[QuantumCircuit], + values_1: Sequence[float] | Sequence[Sequence[float]] | None = None, + values_2: Sequence[float] | Sequence[Sequence[float]] | None = None, + **options, + ) -> AlgorithmJob: + r""" + Computes the state overlap (fidelity) calculation between two + (parametrized) circuits (first and second) for a specific set of parameter + values (first and second). + + Args: + circuits_1: (Parametrized) quantum circuits preparing :math:`|\psi\rangle`. + circuits_2: (Parametrized) quantum circuits preparing :math:`|\phi\rangle`. + values_1: Numerical parameters to be bound to the first set of circuits + values_2: Numerical parameters to be bound to the second set of circuits. + options: Primitive backend runtime options used for circuit execution. The order + of priority is\: options in ``run`` method > fidelity's default + options > primitive's default setting. + Higher priority setting overrides lower priority setting. + + Returns: + A newly constructed algorithm job instance to get the fidelity result. + """ + raise NotImplementedError + + def run( + self, + circuits_1: QuantumCircuit | Sequence[QuantumCircuit], + circuits_2: QuantumCircuit | Sequence[QuantumCircuit], + values_1: Sequence[float] | Sequence[Sequence[float]] | None = None, + values_2: Sequence[float] | Sequence[Sequence[float]] | None = None, + **options, + ) -> AlgorithmJob: + r""" + Runs asynchronously the state overlap (fidelity) calculation between two + (parametrized) circuits (first and second) for a specific set of parameter + values (first and second). This calculation depends on the particular + fidelity method implementation. + + Args: + circuits_1: (Parametrized) quantum circuits preparing :math:`|\psi\rangle`. + circuits_2: (Parametrized) quantum circuits preparing :math:`|\phi\rangle`. + values_1: Numerical parameters to be bound to the first set of circuits. + values_2: Numerical parameters to be bound to the second set of circuits. + options: Primitive backend runtime options used for circuit execution. The order + of priority is\: options in ``run`` method > fidelity's default + options > primitive's default setting. + Higher priority setting overrides lower priority setting. + + Returns: + Primitive job for the fidelity calculation. + The job's result is an instance of :class:`.StateFidelityResult`. + """ + job = self._run(circuits_1, circuits_2, values_1, values_2, **options) + + job.submit() + return job + + @staticmethod + def _truncate_fidelities(fidelities: Sequence[float]) -> Sequence[float]: + """ + Ensures fidelity result in [0,1]. + + Args: + fidelities: Sequence of raw fidelity results. + + Returns: + List of truncated fidelities. + + """ + return np.clip(fidelities, 0, 1).tolist() diff --git a/qiskit_machine_learning/state_fidelities/compute_uncompute.py b/qiskit_machine_learning/state_fidelities/compute_uncompute.py new file mode 100644 index 000000000..b16e40115 --- /dev/null +++ b/qiskit_machine_learning/state_fidelities/compute_uncompute.py @@ -0,0 +1,258 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +""" +Compute-uncompute fidelity interface using primitives +""" + +from __future__ import annotations +from collections.abc import Sequence +from copy import copy + +from qiskit import QuantumCircuit +from qiskit.primitives import BaseSampler +from qiskit.primitives.primitive_job import PrimitiveJob +from qiskit.providers import Options + +from ..exceptions import AlgorithmError +from .base_state_fidelity import BaseStateFidelity +from .state_fidelity_result import StateFidelityResult +from ..algorithm_job import AlgorithmJob + + +class ComputeUncompute(BaseStateFidelity): + r""" + This class leverages the sampler primitive to calculate the state + fidelity of two quantum circuits following the compute-uncompute + method (see [1] for further reference). + The fidelity can be defined as the state overlap. + + .. math:: + + |\langle\psi(x)|\phi(y)\rangle|^2 + + where :math:`x` and :math:`y` are optional parametrizations of the + states :math:`\psi` and :math:`\phi` prepared by the circuits + ``circuit_1`` and ``circuit_2``, respectively. + + **Reference:** + [1] Havlíček, V., Córcoles, A. D., Temme, K., Harrow, A. W., Kandala, + A., Chow, J. M., & Gambetta, J. M. (2019). Supervised learning + with quantum-enhanced feature spaces. Nature, 567(7747), 209-212. + `arXiv:1804.11326v2 [quant-ph] `_ + + """ + + def __init__( + self, + sampler: BaseSampler, + options: Options | None = None, + local: bool = False, + ) -> None: + r""" + Args: + sampler: Sampler primitive instance. + options: Primitive backend runtime options used for circuit execution. + The order of priority is: options in ``run`` method > fidelity's + default options > primitive's default setting. + Higher priority setting overrides lower priority setting. + local: If set to ``True``, the fidelity is averaged over + single-qubit projectors + + .. math:: + + \hat{O} = \frac{1}{N}\sum_{i=1}^N|0_i\rangle\langle 0_i|, + + instead of the global projector :math:`|0\rangle\langle 0|^{\otimes n}`. + This coincides with the standard (global) fidelity in the limit of + the fidelity approaching 1. Might be used to increase the variance + to improve trainability in algorithms such as :class:`~.time_evolvers.PVQD`. + + Raises: + ValueError: If the sampler is not an instance of ``BaseSampler``. + """ + if not isinstance(sampler, BaseSampler): + raise ValueError( + f"The sampler should be an instance of BaseSampler, " f"but got {type(sampler)}" + ) + self._sampler: BaseSampler = sampler + self._local = local + self._default_options = Options() + if options is not None: + self._default_options.update_options(**options) + super().__init__() + + def create_fidelity_circuit( + self, circuit_1: QuantumCircuit, circuit_2: QuantumCircuit + ) -> QuantumCircuit: + """ + Combines ``circuit_1`` and ``circuit_2`` to create the + fidelity circuit following the compute-uncompute method. + + Args: + circuit_1: (Parametrized) quantum circuit. + circuit_2: (Parametrized) quantum circuit. + + Returns: + The fidelity quantum circuit corresponding to circuit_1 and circuit_2. + """ + if len(circuit_1.clbits) > 0: + circuit_1.remove_final_measurements() + if len(circuit_2.clbits) > 0: + circuit_2.remove_final_measurements() + + circuit = circuit_1.compose(circuit_2.inverse()) + circuit.measure_all() + return circuit + + def _run( + self, + circuits_1: QuantumCircuit | Sequence[QuantumCircuit], + circuits_2: QuantumCircuit | Sequence[QuantumCircuit], + values_1: Sequence[float] | Sequence[Sequence[float]] | None = None, + values_2: Sequence[float] | Sequence[Sequence[float]] | None = None, + **options, + ) -> AlgorithmJob: + r""" + Computes the state overlap (fidelity) calculation between two + (parametrized) circuits (first and second) for a specific set of parameter + values (first and second) following the compute-uncompute method. + + Args: + circuits_1: (Parametrized) quantum circuits preparing :math:`|\psi\rangle`. + circuits_2: (Parametrized) quantum circuits preparing :math:`|\phi\rangle`. + values_1: Numerical parameters to be bound to the first circuits. + values_2: Numerical parameters to be bound to the second circuits. + options: Primitive backend runtime options used for circuit execution. + The order of priority is: options in ``run`` method > fidelity's + default options > primitive's default setting. + Higher priority setting overrides lower priority setting. + + Returns: + An AlgorithmJob for the fidelity calculation. + + Raises: + ValueError: At least one pair of circuits must be defined. + AlgorithmError: If the sampler job is not completed successfully. + """ + + circuits = self._construct_circuits(circuits_1, circuits_2) + if len(circuits) == 0: + raise ValueError( + "At least one pair of circuits must be defined to calculate the state overlap." + ) + values = self._construct_value_list(circuits_1, circuits_2, values_1, values_2) + + # The priority of run options is as follows: + # options in `evaluate` method > fidelity's default options > + # primitive's default options. + opts = copy(self._default_options) + opts.update_options(**options) + + sampler_job = self._sampler.run(circuits=circuits, parameter_values=values, **opts.__dict__) + + local_opts = self._get_local_options(opts.__dict__) + return AlgorithmJob(ComputeUncompute._call, sampler_job, circuits, self._local, local_opts) + + @staticmethod + def _call( + job: PrimitiveJob, circuits: Sequence[QuantumCircuit], local: bool, local_opts: Options + ) -> StateFidelityResult: + try: + result = job.result() + except Exception as exc: + raise AlgorithmError("Sampler job failed!") from exc + + if local: + raw_fidelities = [ + ComputeUncompute._get_local_fidelity(prob_dist, circuit.num_qubits) + for prob_dist, circuit in zip(result.quasi_dists, circuits) + ] + else: + raw_fidelities = [ + ComputeUncompute._get_global_fidelity(prob_dist) for prob_dist in result.quasi_dists + ] + fidelities = ComputeUncompute._truncate_fidelities(raw_fidelities) + + return StateFidelityResult( + fidelities=fidelities, + raw_fidelities=raw_fidelities, + metadata=result.metadata, + options=local_opts, + ) + + @property + def options(self) -> Options: + """Return the union of estimator options setting and fidelity default options, + where, if the same field is set in both, the fidelity's default options override + the primitive's default setting. + + Returns: + The fidelity default + estimator options. + """ + return self._get_local_options(self._default_options.__dict__) + + def update_default_options(self, **options): + """Update the fidelity's default options setting. + + Args: + **options: The fields to update the default options. + """ + + self._default_options.update_options(**options) + + def _get_local_options(self, options: Options) -> Options: + """Return the union of the primitive's default setting, + the fidelity default options, and the options in the ``run`` method. + The order of priority is: options in ``run`` method > fidelity's + default options > primitive's default setting. + + Args: + options: The fields to update the options + + Returns: + The fidelity default + estimator + run options. + """ + opts = copy(self._sampler.options) + opts.update_options(**options) + return opts + + @staticmethod + def _get_global_fidelity(probability_distribution: dict[int, float]) -> float: + """Process the probability distribution of a measurement to determine the + global fidelity. + + Args: + probability_distribution: Obtained from the measurement result + + Returns: + The global fidelity. + """ + return probability_distribution.get(0, 0) + + @staticmethod + def _get_local_fidelity(probability_distribution: dict[int, float], num_qubits: int) -> float: + """Process the probability distribution of a measurement to determine the + local fidelity by averaging over single-qubit projectors. + + Args: + probability_distribution: Obtained from the measurement result + + Returns: + The local fidelity. + """ + fidelity = 0.0 + for qubit in range(num_qubits): + for bitstring, prob in probability_distribution.items(): + # Check whether the bit representing the current qubit is 0 + if not bitstring >> qubit & 1: + fidelity += prob / num_qubits + return fidelity diff --git a/qiskit_machine_learning/state_fidelities/state_fidelity_result.py b/qiskit_machine_learning/state_fidelities/state_fidelity_result.py new file mode 100644 index 000000000..6dc26dbf3 --- /dev/null +++ b/qiskit_machine_learning/state_fidelities/state_fidelity_result.py @@ -0,0 +1,37 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +""" +Fidelity result class +""" + +from __future__ import annotations + +from collections.abc import Sequence, Mapping +from typing import Any +from dataclasses import dataclass + +from qiskit.providers import Options + + +@dataclass(frozen=True) +class StateFidelityResult: + """This class stores the result of StateFidelity computations.""" + + fidelities: Sequence[float] + """List of truncated fidelity values for each pair of input circuits, ensured to be in [0,1].""" + raw_fidelities: Sequence[float] + """List of raw fidelity values for each pair of input circuits, which might not be in [0,1] + depending on the error mitigation method used.""" + metadata: Sequence[Mapping[str, Any]] + """Additional information about the fidelity calculation.""" + options: Options + """Primitive runtime options for the execution of the fidelity job.""" diff --git a/qiskit_machine_learning/utils/__init__.py b/qiskit_machine_learning/utils/__init__.py index bba7f030a..7b5a898ca 100644 --- a/qiskit_machine_learning/utils/__init__.py +++ b/qiskit_machine_learning/utils/__init__.py @@ -29,4 +29,9 @@ """ from .adjust_num_qubits import derive_num_qubits_feature_map_ansatz -__all__ = ["derive_num_qubits_feature_map_ansatz"] +__all__ = [ + "derive_num_qubits_feature_map_ansatz", + "algorithm_globals", + "validate_initial_point", + "validate_bounds", +] diff --git a/qiskit_machine_learning/utils/adjust_num_qubits.py b/qiskit_machine_learning/utils/adjust_num_qubits.py index f146d1513..69cd17d43 100644 --- a/qiskit_machine_learning/utils/adjust_num_qubits.py +++ b/qiskit_machine_learning/utils/adjust_num_qubits.py @@ -17,7 +17,7 @@ from qiskit import QuantumCircuit from qiskit.circuit.library import RealAmplitudes, ZFeatureMap, ZZFeatureMap -from qiskit_machine_learning import QiskitMachineLearningError +from ..exceptions import QiskitMachineLearningError # pylint: disable=invalid-name diff --git a/qiskit_machine_learning/utils/algorithm_globals.py b/qiskit_machine_learning/utils/algorithm_globals.py new file mode 100644 index 000000000..2fcb74e1a --- /dev/null +++ b/qiskit_machine_learning/utils/algorithm_globals.py @@ -0,0 +1,131 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2019, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +utils.algorithm_globals +======================= +Common (global) properties used across qiskit_algorithms. + +.. currentmodule:: qiskit_algorithms.utils.algorithm_globals + +Includes: + + * Random number generator and random seed. + + Algorithms can use the generator for random values, as needed, and it + can be seeded here for reproducible results when using such an algorithm. + This is often important, for example in unit tests, where the same + outcome is desired each time (reproducible) and not have it be variable + due to randomness. + +Attributes: + random_seed (int | None): Random generator seed (read/write). + random (np.random.Generator): Random generator (read-only) +""" + +from __future__ import annotations + +import warnings + +import numpy as np + + +class QiskitAlgorithmGlobals: + """Global properties for algorithms.""" + + # The code is done to work even after some future removal of algorithm_globals + # from Qiskit (qiskit.utils). All that is needed in the future, after that, if + # this is updated, is just the logic in the except blocks. + # + # If the Qiskit version exists this acts a redirect to that (it delegates the + # calls off to it). In the future when that does not exist this has similar code + # in the except blocks here, as noted above, that will take over. By delegating + # to the Qiskit instance it means that any existing code that uses that continues + # to work. Logic here in qiskit_algorithms though uses this instance and the + # random check here has logic to warn if the seed here is not the same as the Qiskit + # version so we can detect direct usage of the Qiskit version and alert the user to + # change their code to use this. So simply changing from: + # from qiskit.utils import algorithm_globals + # to + # from qiskit_algorithm.utils import algorithm_globals + + def __init__(self) -> None: + self._random_seed: int | None = None + self._random: np.random.Generator | None = None + + @property + def random_seed(self) -> int | None: + """Random seed property (getter/setter).""" + try: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", category=DeprecationWarning) + + from qiskit.utils import algorithm_globals as qiskit_globals + + return qiskit_globals.random_seed + + except ImportError: + return self._random_seed + + @random_seed.setter + def random_seed(self, seed: int | None) -> None: + """Set the random generator seed. + + Args: + seed: If ``None`` then internally a random value is used as a seed + """ + try: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", category=DeprecationWarning) + + from qiskit.utils import algorithm_globals as qiskit_globals + + qiskit_globals.random_seed = seed + # Mirror the seed here when set via this random_seed. If the seed is + # set on the qiskit.utils instance then we can detect it's different + self._random_seed = seed + + except ImportError: + self._random_seed = seed + self._random = None + + @property + def random(self) -> np.random.Generator: + """Return a numpy np.random.Generator (default_rng) using random_seed.""" + try: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", category=DeprecationWarning) + + from qiskit.utils import algorithm_globals as qiskit_globals + + if self._random_seed != qiskit_globals.random_seed: + # If the seeds are different - likely this local is None and the qiskit.utils + # algorithms global was seeded directly then we will warn to use this here as + # the Qiskit version is planned to be removed in a future version of Qiskit. + warnings.warn( + "Using random that is seeded via qiskit.utils algorithm_globals is deprecated " + "since version 0.2.0. Instead set random_seed directly to " + "qiskit_algorithms.utils algorithm_globals.", + category=DeprecationWarning, + stacklevel=2, + ) + + return qiskit_globals.random + + except ImportError: + if self._random is None: + self._random = np.random.default_rng(self._random_seed) + return self._random + + +# Global instance to be used as the entry point for globals. +algorithm_globals = QiskitAlgorithmGlobals() diff --git a/qiskit_machine_learning/utils/optionals.py b/qiskit_machine_learning/utils/optionals.py new file mode 100644 index 000000000..472157bea --- /dev/null +++ b/qiskit_machine_learning/utils/optionals.py @@ -0,0 +1,27 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +Additional optional constants. +""" + +from qiskit.utils import LazyImportTester + + +HAS_NLOPT = LazyImportTester("nlopt", name="NLopt Optimizer", install="pip install nlopt") +HAS_SKQUANT = LazyImportTester( + "skquant.opt", + name="scikit-quant", + install="pip install scikit-quant", +) +HAS_SQSNOBFIT = LazyImportTester("SQSnobFit", install="pip install SQSnobFit") +HAS_TWEEDLEDUM = LazyImportTester("tweedledum", install="pip install tweedledum") diff --git a/qiskit_machine_learning/utils/set_batching.py b/qiskit_machine_learning/utils/set_batching.py new file mode 100644 index 000000000..f0ec5b5f1 --- /dev/null +++ b/qiskit_machine_learning/utils/set_batching.py @@ -0,0 +1,27 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Set default batch sizes for the optimizers.""" + +from ..optimizers import Optimizer, SPSA + + +def _set_default_batchsize(optimizer: Optimizer) -> bool: + """Set the default batchsize, if None is set and return whether it was updated or not.""" + if isinstance(optimizer, SPSA): + updated = optimizer._max_evals_grouped is None + if updated: + optimizer.set_max_evals_grouped(50) + else: # we only set a batchsize for SPSA + updated = False + + return updated diff --git a/qiskit_machine_learning/utils/validate_bounds.py b/qiskit_machine_learning/utils/validate_bounds.py new file mode 100644 index 000000000..f0a801213 --- /dev/null +++ b/qiskit_machine_learning/utils/validate_bounds.py @@ -0,0 +1,44 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Validate parameter bounds.""" + +from __future__ import annotations + +from qiskit.circuit import QuantumCircuit + + +def validate_bounds(circuit: QuantumCircuit) -> list[tuple[float | None, float | None]]: + """ + Validate the bounds provided by a quantum circuit against its number of parameters. + If no bounds are obtained, return ``None`` for all lower and upper bounds. + + Args: + circuit: A parameterized quantum circuit. + + Returns: + A list of tuples (lower_bound, upper_bound)). + + Raises: + ValueError: If the number of bounds does not the match the number of circuit parameters. + """ + if hasattr(circuit, "parameter_bounds") and circuit.parameter_bounds is not None: + bounds = circuit.parameter_bounds + if len(bounds) != circuit.num_parameters: + raise ValueError( + f"The number of bounds ({len(bounds)}) does not match the number of " + f"parameters in the circuit ({circuit.num_parameters})." + ) + else: + bounds = [(None, None)] * circuit.num_parameters + + return bounds diff --git a/qiskit_machine_learning/utils/validate_initial_point.py b/qiskit_machine_learning/utils/validate_initial_point.py new file mode 100644 index 000000000..bb849a77a --- /dev/null +++ b/qiskit_machine_learning/utils/validate_initial_point.py @@ -0,0 +1,65 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Validate an initial point.""" + +from __future__ import annotations + +import numpy as np + +from qiskit.circuit import QuantumCircuit +from .algorithm_globals import algorithm_globals + + +def validate_initial_point(point: np.ndarray | None | None, circuit: QuantumCircuit) -> np.ndarray: + r""" + Validate a choice of initial point against a choice of circuit. If no point is provided, a + random point will be generated within certain parameter bounds. It will first look to the + circuit for these bounds. If the circuit does not specify bounds, bounds of :math:`-2\pi`, + :math:`2\pi` will be used. + + Args: + point: An initial point. + circuit: A parameterized quantum circuit. + + Returns: + A validated initial point. + + Raises: + ValueError: If the dimension of the initial point does not match the number of circuit + parameters. + """ + expected_size = circuit.num_parameters + + if point is None: + # get bounds if circuit has them set, otherwise use [-2pi, 2pi] for each parameter + bounds = getattr(circuit, "parameter_bounds", None) + if bounds is None: + bounds = [(-2 * np.pi, 2 * np.pi)] * expected_size + + # replace all Nones by [-2pi, 2pi] + lower_bounds = [] + upper_bounds = [] + for lower, upper in bounds: + lower_bounds.append(lower if lower is not None else -2 * np.pi) + upper_bounds.append(upper if upper is not None else 2 * np.pi) + + # sample from within bounds + point = algorithm_globals.random().uniform(lower_bounds, upper_bounds) + + elif len(point) != expected_size: + raise ValueError( + f"The dimension of the initial point ({len(point)}) does not match the " + f"number of parameters in the circuit ({expected_size})." + ) + + return point diff --git a/qiskit_machine_learning/utils/validation.py b/qiskit_machine_learning/utils/validation.py new file mode 100644 index 000000000..ae838d8d5 --- /dev/null +++ b/qiskit_machine_learning/utils/validation.py @@ -0,0 +1,138 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2019, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +Validation module +""" + +from typing import Set + + +def validate_in_set(name: str, value: object, values: Set[object]) -> None: + """ + Args: + name: value name. + value: value to check. + values: set that should contain value. + Raises: + ValueError: invalid value + """ + if value not in values: + raise ValueError(f"{name} must be one of '{values}', was '{value}'.") + + +def validate_min(name: str, value: float, minimum: float) -> None: + """ + Args: + name: value name. + value: value to check. + minimum: minimum value allowed. + Raises: + ValueError: invalid value + """ + if value < minimum: + raise ValueError(f"{name} must have value >= {minimum}, was {value}") + + +def validate_min_exclusive(name: str, value: float, minimum: float) -> None: + """ + Args: + name: value name. + value: value to check. + minimum: minimum value allowed. + Raises: + ValueError: invalid value + """ + if value <= minimum: + raise ValueError(f"{name} must have value > {minimum}, was {value}") + + +def validate_max(name: str, value: float, maximum: float) -> None: + """ + Args: + name: value name. + value: value to check. + maximum: maximum value allowed. + Raises: + ValueError: invalid value + """ + if value > maximum: + raise ValueError(f"{name} must have value <= {maximum}, was {value}") + + +def validate_max_exclusive(name: str, value: float, maximum: float) -> None: + """ + Args: + name: value name. + value: value to check. + maximum: maximum value allowed. + Raises: + ValueError: invalid value + """ + if value >= maximum: + raise ValueError(f"{name} must have value < {maximum}, was {value}") + + +def validate_range(name: str, value: float, minimum: float, maximum: float) -> None: + """ + Args: + name: value name. + value: value to check. + minimum: minimum value allowed. + maximum: maximum value allowed. + Raises: + ValueError: invalid value + """ + if value < minimum or value > maximum: + raise ValueError(f"{name} must have value >= {minimum} and <= {maximum}, was {value}") + + +def validate_range_exclusive(name: str, value: float, minimum: float, maximum: float) -> None: + """ + Args: + name: value name. + value: value to check. + minimum: minimum value allowed. + maximum: maximum value allowed. + Raises: + ValueError: invalid value + """ + if value <= minimum or value >= maximum: + raise ValueError(f"{name} must have value > {minimum} and < {maximum}, was {value}") + + +def validate_range_exclusive_min(name: str, value: float, minimum: float, maximum: float) -> None: + """ + Args: + name: value name. + value: value to check. + minimum: minimum value allowed. + maximum: maximum value allowed. + Raises: + ValueError: invalid value + """ + if value <= minimum or value > maximum: + raise ValueError(f"{name} must have value > {minimum} and <= {maximum}, was {value}") + + +def validate_range_exclusive_max(name: str, value: float, minimum: float, maximum: float) -> None: + """ + Args: + name: value name. + value: value to check. + minimum: minimum value allowed. + maximum: maximum value allowed. + Raises: + ValueError: invalid value + """ + if value < minimum or value >= maximum: + raise ValueError(f"{name} must have value >= {minimum} and < {maximum}, was {value}") diff --git a/qiskit_machine_learning/variational_algorithm.py b/qiskit_machine_learning/variational_algorithm.py new file mode 100644 index 000000000..aa295616e --- /dev/null +++ b/qiskit_machine_learning/variational_algorithm.py @@ -0,0 +1,137 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2019, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""The Variational Algorithm Base Class. + +This class can be used an interface for working with Variation Algorithms, such as VQE, +QAOA, or QSVM, and also provides helper utilities for implementing new variational algorithms. +Writing a new variational algorithm is a simple as extending this class, implementing a cost +function for the new algorithm to pass to the optimizer, and running :meth:`find_minimum` method +of this class to carry out the optimization. Alternatively, all of the functions below can be +overridden to opt-out of this infrastructure but still meet the interface requirements. + +.. note:: + + This component has some function that is normally random. If you want to reproduce behavior + then you should set the random number generator seed in the algorithm_globals + (``qiskit_algorithms.utils.algorithm_globals.random_seed = seed``). +""" + +from __future__ import annotations +from abc import ABC, abstractmethod +import numpy as np + +from qiskit.circuit import QuantumCircuit + +from .algorithm_result import AlgorithmResult +from .optimizers import OptimizerResult + + +class VariationalAlgorithm(ABC): + """The Variational Algorithm Base Class.""" + + @property + @abstractmethod + def initial_point(self) -> np.ndarray | None: + """Returns initial point.""" + pass + + @initial_point.setter + @abstractmethod + def initial_point(self, initial_point: np.ndarray | None) -> None: + """Sets initial point.""" + pass + + +class VariationalResult(AlgorithmResult): + """Variation Algorithm Result.""" + + def __init__(self) -> None: + super().__init__() + self._optimizer_evals: int | None = None + self._optimizer_time: float | None = None + self._optimal_value: float | None = None + self._optimal_point: np.ndarray | None = None + self._optimal_parameters: dict | None = None + self._optimizer_result: OptimizerResult | None = None + self._optimal_circuit: QuantumCircuit | None = None + + @property + def optimizer_evals(self) -> int | None: + """Returns number of optimizer evaluations""" + return self._optimizer_evals + + @optimizer_evals.setter + def optimizer_evals(self, value: int) -> None: + """Sets number of optimizer evaluations""" + self._optimizer_evals = value + + @property + def optimizer_time(self) -> float | None: + """Returns time taken for optimization""" + return self._optimizer_time + + @optimizer_time.setter + def optimizer_time(self, value: float) -> None: + """Sets time taken for optimization""" + self._optimizer_time = value + + @property + def optimal_value(self) -> float | None: + """Returns optimal value""" + return self._optimal_value + + @optimal_value.setter + def optimal_value(self, value: int) -> None: + """Sets optimal value""" + self._optimal_value = value + + @property + def optimal_point(self) -> np.ndarray | None: + """Returns optimal point""" + return self._optimal_point + + @optimal_point.setter + def optimal_point(self, value: np.ndarray) -> None: + """Sets optimal point""" + self._optimal_point = value + + @property + def optimal_parameters(self) -> dict | None: + """Returns the optimal parameters in a dictionary""" + return self._optimal_parameters + + @optimal_parameters.setter + def optimal_parameters(self, value: dict) -> None: + """Sets optimal parameters""" + self._optimal_parameters = value + + @property + def optimizer_result(self) -> OptimizerResult | None: + """Returns the optimizer result""" + return self._optimizer_result + + @optimizer_result.setter + def optimizer_result(self, value: OptimizerResult) -> None: + """Sets optimizer result""" + self._optimizer_result = value + + @property + def optimal_circuit(self) -> QuantumCircuit: + """The optimal circuits. Along with the optimal parameters, + these can be used to retrieve the minimum eigenstate. + """ + return self._optimal_circuit + + @optimal_circuit.setter + def optimal_circuit(self, optimal_circuit: QuantumCircuit) -> None: + self._optimal_circuit = optimal_circuit diff --git a/requirements.txt b/requirements.txt index bf8f50e3b..f1b058ff8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ qiskit>=0.44 -qiskit-algorithms>=0.2.0 scipy>=1.4 numpy>=1.17 psutil>=5 diff --git a/test/algorithms/classifiers/test_fidelity_quantum_kernel_pegasos_qsvc.py b/test/algorithms/classifiers/test_fidelity_quantum_kernel_pegasos_qsvc.py index 9f4ef4d8d..11f1812f2 100644 --- a/test/algorithms/classifiers/test_fidelity_quantum_kernel_pegasos_qsvc.py +++ b/test/algorithms/classifiers/test_fidelity_quantum_kernel_pegasos_qsvc.py @@ -19,10 +19,10 @@ import numpy as np from qiskit.circuit.library import ZFeatureMap -from qiskit_algorithms.utils import algorithm_globals from sklearn.datasets import make_blobs from sklearn.preprocessing import MinMaxScaler +from qiskit_machine_learning.utils import algorithm_globals from qiskit_machine_learning.algorithms import PegasosQSVC, SerializableModelMixin from qiskit_machine_learning.kernels import FidelityQuantumKernel diff --git a/test/algorithms/classifiers/test_fidelity_quantum_kernel_qsvc.py b/test/algorithms/classifiers/test_fidelity_quantum_kernel_qsvc.py index 7d4003799..643b27c0e 100644 --- a/test/algorithms/classifiers/test_fidelity_quantum_kernel_qsvc.py +++ b/test/algorithms/classifiers/test_fidelity_quantum_kernel_qsvc.py @@ -19,8 +19,8 @@ import numpy as np from qiskit.circuit.library import ZZFeatureMap -from qiskit_algorithms.utils import algorithm_globals +from qiskit_machine_learning.utils import algorithm_globals from qiskit_machine_learning.algorithms import QSVC, SerializableModelMixin from qiskit_machine_learning.exceptions import QiskitMachineLearningWarning from qiskit_machine_learning.kernels import FidelityQuantumKernel diff --git a/test/algorithms/classifiers/test_neural_network_classifier.py b/test/algorithms/classifiers/test_neural_network_classifier.py index 532d35421..44d346dc1 100644 --- a/test/algorithms/classifiers/test_neural_network_classifier.py +++ b/test/algorithms/classifiers/test_neural_network_classifier.py @@ -26,10 +26,10 @@ from ddt import ddt, data, idata, unpack from qiskit.circuit import QuantumCircuit from qiskit.circuit.library import RealAmplitudes, ZZFeatureMap -from qiskit_algorithms.optimizers import COBYLA, L_BFGS_B, SPSA, Optimizer -from qiskit_algorithms.utils import algorithm_globals from scipy.optimize import minimize +from qiskit_machine_learning.optimizers import COBYLA, L_BFGS_B, SPSA, Optimizer +from qiskit_machine_learning.utils import algorithm_globals from qiskit_machine_learning.algorithms import SerializableModelMixin from qiskit_machine_learning.algorithms.classifiers import NeuralNetworkClassifier from qiskit_machine_learning.exceptions import QiskitMachineLearningError @@ -106,7 +106,7 @@ def test_classifier_with_estimator_qnn(self, opt, loss, cb_flag): # construct data num_samples = 6 - X = algorithm_globals.random.random( # pylint: disable=invalid-name + X = algorithm_globals.random().random( # pylint: disable=invalid-name (num_samples, num_inputs) ) y = 2.0 * (np.sum(X, axis=1) <= 1) - 1.0 @@ -160,7 +160,7 @@ def parity(x): def _generate_data(self, num_inputs: int) -> tuple[np.ndarray, np.ndarray]: # construct data num_samples = 6 - features = algorithm_globals.random.random((num_samples, num_inputs)) + features = algorithm_globals.random().random((num_samples, num_inputs)) labels = 1.0 * (np.sum(features, axis=1) <= 1) return features, labels @@ -382,7 +382,7 @@ def test_binary_classification_with_multiclass_data(self): # construct data num_samples = 3 - x = algorithm_globals.random.random((num_samples, num_inputs)) + x = algorithm_globals.random().random((num_samples, num_inputs)) y = np.asarray([0, 1, 2]) with self.assertRaises(QiskitMachineLearningError): @@ -402,7 +402,7 @@ def test_bad_binary_shape(self): # construct data num_samples = 2 - x = algorithm_globals.random.random((num_samples, num_inputs)) + x = algorithm_globals.random().random((num_samples, num_inputs)) y = np.array([[0, 1], [1, 0]]) with self.assertRaises(QiskitMachineLearningError): @@ -419,7 +419,7 @@ def test_bad_one_hot_data(self): # construct data num_samples = 2 - x = algorithm_globals.random.random((num_samples, num_inputs)) + x = algorithm_globals.random().random((num_samples, num_inputs)) y = np.array([[0, 1], [2, 0]]) with self.assertRaises(QiskitMachineLearningError): diff --git a/test/algorithms/classifiers/test_pegasos_qsvc.py b/test/algorithms/classifiers/test_pegasos_qsvc.py index 622095b73..78725e40e 100644 --- a/test/algorithms/classifiers/test_pegasos_qsvc.py +++ b/test/algorithms/classifiers/test_pegasos_qsvc.py @@ -19,7 +19,7 @@ import numpy as np from qiskit.circuit.library import ZFeatureMap -from qiskit_algorithms.utils import algorithm_globals +from qiskit_machine_learning.utils import algorithm_globals from sklearn.datasets import make_blobs from sklearn.preprocessing import MinMaxScaler diff --git a/test/algorithms/classifiers/test_qsvc.py b/test/algorithms/classifiers/test_qsvc.py index 73fb83195..808765a56 100644 --- a/test/algorithms/classifiers/test_qsvc.py +++ b/test/algorithms/classifiers/test_qsvc.py @@ -20,7 +20,7 @@ import numpy as np from qiskit.circuit.library import ZZFeatureMap -from qiskit_algorithms.utils import algorithm_globals +from qiskit_machine_learning.utils import algorithm_globals from qiskit_machine_learning.algorithms import QSVC, SerializableModelMixin from qiskit_machine_learning.kernels import FidelityQuantumKernel from qiskit_machine_learning.exceptions import ( diff --git a/test/algorithms/classifiers/test_vqc.py b/test/algorithms/classifiers/test_vqc.py index 4c80bd216..4af50a576 100644 --- a/test/algorithms/classifiers/test_vqc.py +++ b/test/algorithms/classifiers/test_vqc.py @@ -29,9 +29,8 @@ from qiskit import QuantumCircuit from qiskit.circuit.library import RealAmplitudes, ZZFeatureMap, ZFeatureMap -from qiskit_algorithms.optimizers import COBYLA, L_BFGS_B -from qiskit_algorithms.utils import algorithm_globals - +from qiskit_machine_learning.optimizers import COBYLA, L_BFGS_B +from qiskit_machine_learning.utils import algorithm_globals from qiskit_machine_learning.algorithms import VQC from qiskit_machine_learning.exceptions import QiskitMachineLearningError @@ -175,7 +174,7 @@ def test_batches_with_incomplete_labels(self): """Test VQC when targets are one-hot and some batches don't have all possible labels.""" # Generate data with batches that have incomplete labels. - x = algorithm_globals.random.random((6, 2)) + x = algorithm_globals.random().random((6, 2)) y = np.asarray([0, 0, 1, 1, 2, 2]) y_one_hot = OneHotEncoder().fit_transform(y.reshape(-1, 1)) @@ -204,7 +203,7 @@ def test_batches_with_incomplete_labels(self): def test_multilabel_targets_raise_an_error(self): """Tests VQC multi-label input raises an error.""" # Generate multi-label data. - x = algorithm_globals.random.random((3, 2)) + x = algorithm_globals.random().random((3, 2)) y = np.asarray([[1, 1, 0], [1, 0, 1], [0, 1, 1]]) classifier = VQC(num_qubits=2) @@ -216,8 +215,8 @@ def test_changing_classes_raises_error(self): targets1 = np.asarray([[0, 0, 1], [0, 1, 0]]) targets2 = np.asarray([[0, 1], [1, 0]]) - features1 = algorithm_globals.random.random((len(targets1), 2)) - features2 = algorithm_globals.random.random((len(targets2), 2)) + features1 = algorithm_globals.random().random((len(targets1), 2)) + features2 = algorithm_globals.random().random((len(targets2), 2)) classifier = VQC(num_qubits=2, warm_start=True) classifier.fit(features1, targets1) diff --git a/test/algorithms/inference/test_qbayesian.py b/test/algorithms/inference/test_qbayesian.py index a4f5a2693..44976c8e3 100644 --- a/test/algorithms/inference/test_qbayesian.py +++ b/test/algorithms/inference/test_qbayesian.py @@ -16,7 +16,7 @@ from test import QiskitMachineLearningTestCase import numpy as np -from qiskit_algorithms.utils import algorithm_globals +from qiskit_machine_learning.utils import algorithm_globals from qiskit import QuantumCircuit from qiskit.circuit import QuantumRegister from qiskit.primitives import Sampler diff --git a/test/algorithms/regressors/test_fidelity_quantum_kernel_qsvr.py b/test/algorithms/regressors/test_fidelity_quantum_kernel_qsvr.py index 9d56532fd..d5dcc9cb9 100644 --- a/test/algorithms/regressors/test_fidelity_quantum_kernel_qsvr.py +++ b/test/algorithms/regressors/test_fidelity_quantum_kernel_qsvr.py @@ -20,7 +20,7 @@ import numpy as np from qiskit.circuit.library import ZZFeatureMap from qiskit.primitives import Sampler -from qiskit_algorithms.utils import algorithm_globals +from qiskit_machine_learning.utils import algorithm_globals from qiskit_machine_learning.algorithms import QSVR, SerializableModelMixin from qiskit_machine_learning.exceptions import QiskitMachineLearningWarning diff --git a/test/algorithms/regressors/test_neural_network_regressor.py b/test/algorithms/regressors/test_neural_network_regressor.py index 75eb1dfcc..3bbb46ea7 100644 --- a/test/algorithms/regressors/test_neural_network_regressor.py +++ b/test/algorithms/regressors/test_neural_network_regressor.py @@ -24,8 +24,8 @@ from ddt import ddt, unpack, idata from qiskit.circuit import Parameter, QuantumCircuit from qiskit.circuit.library import ZZFeatureMap, RealAmplitudes -from qiskit_algorithms.optimizers import COBYLA, L_BFGS_B, SPSA -from qiskit_algorithms.utils import algorithm_globals +from qiskit_machine_learning.optimizers import COBYLA, L_BFGS_B, SPSA +from qiskit_machine_learning.utils import algorithm_globals from scipy.optimize import minimize from qiskit_machine_learning import QiskitMachineLearningError diff --git a/test/algorithms/regressors/test_qsvr.py b/test/algorithms/regressors/test_qsvr.py index 2f3a6ef21..5510ede5a 100644 --- a/test/algorithms/regressors/test_qsvr.py +++ b/test/algorithms/regressors/test_qsvr.py @@ -20,7 +20,7 @@ import numpy as np from qiskit.circuit.library import ZZFeatureMap -from qiskit_algorithms.utils import algorithm_globals +from qiskit_machine_learning.utils import algorithm_globals from qiskit_machine_learning.algorithms import QSVR, SerializableModelMixin from qiskit_machine_learning.exceptions import QiskitMachineLearningWarning from qiskit_machine_learning.kernels import FidelityQuantumKernel diff --git a/test/algorithms/regressors/test_vqr.py b/test/algorithms/regressors/test_vqr.py index 3fe16683b..c2570598d 100644 --- a/test/algorithms/regressors/test_vqr.py +++ b/test/algorithms/regressors/test_vqr.py @@ -19,8 +19,8 @@ from qiskit.circuit import Parameter, QuantumCircuit from qiskit.circuit.library import ZZFeatureMap, RealAmplitudes from qiskit.primitives import Estimator -from qiskit_algorithms.optimizers import COBYLA, L_BFGS_B -from qiskit_algorithms.utils import algorithm_globals +from qiskit_machine_learning.optimizers import COBYLA, L_BFGS_B +from qiskit_machine_learning.utils import algorithm_globals from qiskit_machine_learning.algorithms import VQR diff --git a/test/circuit/library/test_raw_feature_vector.py b/test/circuit/library/test_raw_feature_vector.py index 6763067e0..1d7155a0c 100644 --- a/test/circuit/library/test_raw_feature_vector.py +++ b/test/circuit/library/test_raw_feature_vector.py @@ -22,8 +22,8 @@ from qiskit.circuit.library import RealAmplitudes from qiskit.exceptions import QiskitError from qiskit.quantum_info import Statevector -from qiskit_algorithms.optimizers import COBYLA -from qiskit_algorithms.utils import algorithm_globals +from qiskit_machine_learning.optimizers import COBYLA +from qiskit_machine_learning.utils import algorithm_globals from qiskit_machine_learning.algorithms import VQC from qiskit_machine_learning.circuit.library import RawFeatureVector @@ -99,7 +99,7 @@ def test_usage_in_vqc(self): # construct data num_samples = 10 num_inputs = 4 - X = algorithm_globals.random.random( # pylint: disable=invalid-name + X = algorithm_globals.random().random( # pylint: disable=invalid-name (num_samples, num_inputs) ) y = 1.0 * (np.sum(X, axis=1) <= 2) diff --git a/test/connectors/test_torch.py b/test/connectors/test_torch.py index f82558445..fd772f117 100644 --- a/test/connectors/test_torch.py +++ b/test/connectors/test_torch.py @@ -15,7 +15,7 @@ import unittest import builtins from abc import ABC, abstractmethod -from qiskit_algorithms.utils import algorithm_globals +from qiskit_machine_learning.utils import algorithm_globals import qiskit_machine_learning.optionals as _optionals diff --git a/test/datasets/test_ad_hoc_data.py b/test/datasets/test_ad_hoc_data.py index cb6392039..f044aff39 100644 --- a/test/datasets/test_ad_hoc_data.py +++ b/test/datasets/test_ad_hoc_data.py @@ -20,7 +20,7 @@ import numpy as np from ddt import ddt, unpack, idata -from qiskit_algorithms.utils import algorithm_globals +from qiskit_machine_learning.utils import algorithm_globals from qiskit_machine_learning.datasets import ad_hoc_data diff --git a/test/kernels/algorithms/test_fidelity_qkernel_trainer.py b/test/kernels/algorithms/test_fidelity_qkernel_trainer.py index f324da78c..f0b626baa 100644 --- a/test/kernels/algorithms/test_fidelity_qkernel_trainer.py +++ b/test/kernels/algorithms/test_fidelity_qkernel_trainer.py @@ -23,7 +23,7 @@ from qiskit import QuantumCircuit from qiskit.circuit import Parameter, ParameterVector from qiskit.circuit.library import ZZFeatureMap -from qiskit_algorithms.utils import algorithm_globals +from qiskit_machine_learning.utils import algorithm_globals from scipy.optimize import minimize from qiskit_machine_learning.algorithms.classifiers import QSVC diff --git a/test/kernels/test_fidelity_qkernel.py b/test/kernels/test_fidelity_qkernel.py index 6a132f14e..9d1b273e1 100644 --- a/test/kernels/test_fidelity_qkernel.py +++ b/test/kernels/test_fidelity_qkernel.py @@ -26,9 +26,9 @@ from qiskit.circuit import Parameter from qiskit.circuit.library import ZFeatureMap from qiskit.primitives import Sampler -from qiskit_algorithms import AlgorithmJob -from qiskit_algorithms.utils import algorithm_globals -from qiskit_algorithms.state_fidelities import ( +from qiskit_machine_learning.algorithm_job import AlgorithmJob +from qiskit_machine_learning.utils import algorithm_globals +from qiskit_machine_learning.state_fidelities import ( ComputeUncompute, BaseStateFidelity, StateFidelityResult, @@ -96,7 +96,7 @@ def test_svc_precomputed(self): def test_defaults(self): """Test quantum kernel with all default values.""" - features = algorithm_globals.random.random((10, 2)) - 0.5 + features = algorithm_globals.random().random((10, 2)) - 0.5 labels = np.sign(features[:, 0]) kernel = FidelityQuantumKernel() diff --git a/test/kernels/test_fidelity_statevector_kernel.py b/test/kernels/test_fidelity_statevector_kernel.py index 5366aed91..097c4cfb6 100644 --- a/test/kernels/test_fidelity_statevector_kernel.py +++ b/test/kernels/test_fidelity_statevector_kernel.py @@ -29,7 +29,7 @@ from qiskit.circuit import Parameter from qiskit.circuit.library import ZFeatureMap from qiskit.utils import optionals -from qiskit_algorithms.utils import algorithm_globals +from qiskit_machine_learning.utils import algorithm_globals from qiskit_machine_learning.kernels import FidelityStatevectorKernel @@ -89,7 +89,7 @@ def test_svc_precomputed(self): def test_defaults(self): """Test statevector kernel with all default values.""" - features = algorithm_globals.random.random((10, 2)) - 0.5 + features = algorithm_globals.random().random((10, 2)) - 0.5 labels = np.sign(features[:, 0]) kernel = FidelityStatevectorKernel() @@ -101,7 +101,7 @@ def test_defaults(self): def test_with_shot_noise(self): """Test statevector kernel with shot noise emulation.""" - features = algorithm_globals.random.random((3, 2)) - 0.5 + features = algorithm_globals.random().random((3, 2)) - 0.5 kernel = FidelityStatevectorKernel( feature_map=self.feature_map, shots=10, enforce_psd=False ) @@ -136,7 +136,7 @@ def test_aer_statevector(self): """Test statevector kernel when using AerStatevector type statevectors.""" from qiskit_aer.quantum_info import AerStatevector - features = algorithm_globals.random.random((10, 2)) - 0.5 + features = algorithm_globals.random().random((10, 2)) - 0.5 labels = np.sign(features[:, 0]) kernel = FidelityStatevectorKernel(statevector_type=AerStatevector) diff --git a/test/neural_networks/test_effective_dimension.py b/test/neural_networks/test_effective_dimension.py index c304df0b3..cda09018e 100644 --- a/test/neural_networks/test_effective_dimension.py +++ b/test/neural_networks/test_effective_dimension.py @@ -20,7 +20,7 @@ from qiskit.circuit import QuantumCircuit from qiskit.circuit.library import ZFeatureMap, RealAmplitudes -from qiskit_algorithms.utils import algorithm_globals +from qiskit_machine_learning.utils import algorithm_globals from qiskit_machine_learning.neural_networks import ( EffectiveDimension, @@ -149,8 +149,8 @@ def test_inputs(self): qnn = self.qnns["sampler_qnn_1"] num_input_samples, num_weight_samples = 10, 10 - inputs = algorithm_globals.random.uniform(0, 1, size=(num_input_samples, qnn.num_inputs)) - weights = algorithm_globals.random.uniform(0, 1, size=(num_weight_samples, qnn.num_weights)) + inputs = algorithm_globals.random().uniform(0, 1, size=(num_input_samples, qnn.num_inputs)) + weights = algorithm_globals.random().uniform(0, 1, size=(num_weight_samples, qnn.num_weights)) global_ed1 = EffectiveDimension( qnn=qnn, @@ -175,11 +175,11 @@ def test_inputs_shapes(self): qnn = self.qnns["sampler_qnn_1"] num_inputs, num_params = 10, 10 - inputs_ok = algorithm_globals.random.uniform(0, 1, size=(num_inputs, qnn.num_inputs)) - weights_ok = algorithm_globals.random.uniform(0, 1, size=(num_params, qnn.num_weights)) + inputs_ok = algorithm_globals.random().uniform(0, 1, size=(num_inputs, qnn.num_inputs)) + weights_ok = algorithm_globals.random().uniform(0, 1, size=(num_params, qnn.num_weights)) - inputs_wrong = algorithm_globals.random.uniform(0, 1, size=(num_inputs, 1)) - weights_wrong = algorithm_globals.random.uniform(0, 1, size=(num_params, 1)) + inputs_wrong = algorithm_globals.random().uniform(0, 1, size=(num_inputs, 1)) + weights_wrong = algorithm_globals.random().uniform(0, 1, size=(num_params, 1)) with self.assertRaises(QiskitMachineLearningError): EffectiveDimension( @@ -201,10 +201,10 @@ def test_local_ed_params(self): qnn = self.qnns["sampler_qnn_1"] num_inputs, num_params = 10, 10 - inputs_ok = algorithm_globals.random.uniform(0, 1, size=(num_inputs, qnn.num_inputs)) - weights_ok = algorithm_globals.random.uniform(0, 1, size=(1, qnn.num_weights)) - weights_ok2 = algorithm_globals.random.uniform(0, 1, size=qnn.num_weights) - weights_wrong = algorithm_globals.random.uniform(0, 1, size=(num_params, qnn.num_weights)) + inputs_ok = algorithm_globals.random().uniform(0, 1, size=(num_inputs, qnn.num_inputs)) + weights_ok = algorithm_globals.random().uniform(0, 1, size=(1, qnn.num_weights)) + weights_ok2 = algorithm_globals.random().uniform(0, 1, size=qnn.num_weights) + weights_wrong = algorithm_globals.random().uniform(0, 1, size=(num_params, qnn.num_weights)) LocalEffectiveDimension( qnn=qnn, diff --git a/test/neural_networks/test_sampler_qnn.py b/test/neural_networks/test_sampler_qnn.py index 219b6c567..744c3f6f9 100644 --- a/test/neural_networks/test_sampler_qnn.py +++ b/test/neural_networks/test_sampler_qnn.py @@ -24,7 +24,7 @@ from qiskit.circuit import Parameter, QuantumCircuit from qiskit.primitives import Sampler from qiskit.circuit.library import RealAmplitudes, ZZFeatureMap -from qiskit_algorithms.utils import algorithm_globals +from qiskit_machine_learning.utils import algorithm_globals from qiskit_machine_learning.circuit.library import QNNCircuit from qiskit_machine_learning.neural_networks.sampler_qnn import SamplerQNN From 070aa8163b1dde16dca667cd21e26a31705f3450 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Thu, 11 Jul 2024 16:03:10 +0200 Subject: [PATCH 26/85] Fixed `algorithms_globals` --- qiskit_machine_learning/__init__.py | 4 ++-- .../algorithms/classifiers/pegasos_qsvc.py | 2 +- .../algorithms/trainable_model.py | 2 +- qiskit_machine_learning/datasets/ad_hoc.py | 6 +++--- .../algorithms/quantum_kernel_trainer.py | 2 +- .../kernels/fidelity_statevector_kernel.py | 2 +- .../neural_networks/effective_dimension.py | 6 +++--- .../optimizers/gradient_descent.py | 6 +++--- qiskit_machine_learning/optimizers/gsls.py | 2 +- qiskit_machine_learning/optimizers/p_bfgs.py | 2 +- qiskit_machine_learning/optimizers/spsa.py | 6 +++--- qiskit_machine_learning/optimizers/umda.py | 4 ++-- qiskit_machine_learning/utils/__init__.py | 3 +++ .../utils/validate_initial_point.py | 2 +- .../test_neural_network_classifier.py | 10 +++++----- test/algorithms/classifiers/test_vqc.py | 8 ++++---- .../library/test_raw_feature_vector.py | 2 +- test/kernels/test_fidelity_qkernel.py | 17 ++-------------- .../test_fidelity_statevector_kernel.py | 6 +++--- .../test_effective_dimension.py | 20 +++++++++---------- 20 files changed, 51 insertions(+), 61 deletions(-) diff --git a/qiskit_machine_learning/__init__.py b/qiskit_machine_learning/__init__.py index ad23f2a8b..67579e4d1 100644 --- a/qiskit_machine_learning/__init__.py +++ b/qiskit_machine_learning/__init__.py @@ -44,6 +44,6 @@ """ from .version import __version__ -from .exceptions import QiskitMachineLearningError +from .exceptions import QiskitMachineLearningError, AlgorithmError -__all__ = ["__version__", "QiskitMachineLearningError"] +__all__ = ["__version__", "QiskitMachineLearningError", "AlgorithmError"] diff --git a/qiskit_machine_learning/algorithms/classifiers/pegasos_qsvc.py b/qiskit_machine_learning/algorithms/classifiers/pegasos_qsvc.py index 479be40b8..bd6e89baf 100644 --- a/qiskit_machine_learning/algorithms/classifiers/pegasos_qsvc.py +++ b/qiskit_machine_learning/algorithms/classifiers/pegasos_qsvc.py @@ -188,7 +188,7 @@ def fit( # training loop for step in range(1, self._num_steps + 1): # for every step, a random index (determining a random datum) is fixed - i = algorithm_globals.random().integers(0, len(y)) + i = algorithm_globals.random.integers(0, len(y)) value = self._compute_weighted_kernel_sum(i, X, training=True) diff --git a/qiskit_machine_learning/algorithms/trainable_model.py b/qiskit_machine_learning/algorithms/trainable_model.py index 4c2ebcf16..1b67e5c69 100644 --- a/qiskit_machine_learning/algorithms/trainable_model.py +++ b/qiskit_machine_learning/algorithms/trainable_model.py @@ -247,7 +247,7 @@ def _choose_initial_point(self) -> np.ndarray: if self._warm_start and self._fit_result is not None: self._initial_point = self._fit_result.x elif self._initial_point is None: - self._initial_point = algorithm_globals.random().random(self._neural_network.num_weights) + self._initial_point = algorithm_globals.random.random(self._neural_network.num_weights) return self._initial_point def _get_objective( diff --git a/qiskit_machine_learning/datasets/ad_hoc.py b/qiskit_machine_learning/datasets/ad_hoc.py index d64eae298..e1dd96428 100644 --- a/qiskit_machine_learning/datasets/ad_hoc.py +++ b/qiskit_machine_learning/datasets/ad_hoc.py @@ -127,9 +127,9 @@ def ad_hoc_data( # Generate a random unitary operator by collecting eigenvectors of a # random hermitian operator - basis = algorithm_globals.random().random( + basis = algorithm_globals.random.random( (2**n, 2**n) - ) + 1j * algorithm_globals.random().random((2**n, 2**n)) + ) + 1j * algorithm_globals.random.random((2**n, 2**n)) basis = np.array(basis).conj().T @ np.array(basis) eigvals, eigvecs = np.linalg.eig(basis) idx = eigvals.argsort()[::-1] @@ -204,7 +204,7 @@ def _sample_ad_hoc_data(sample_total, xvals, num_samples, n): for i, sample_list in enumerate([sample_a, sample_b]): label = 1 if i == 0 else -1 while len(sample_list) < num_samples: - draws = tuple(algorithm_globals.random().choice(count) for i in range(n)) + draws = tuple(algorithm_globals.random.choice(count) for i in range(n)) if sample_total[draws] == label: sample_list.append([xvals[d] for d in draws]) diff --git a/qiskit_machine_learning/kernels/algorithms/quantum_kernel_trainer.py b/qiskit_machine_learning/kernels/algorithms/quantum_kernel_trainer.py index 897459fa6..889a466ba 100644 --- a/qiskit_machine_learning/kernels/algorithms/quantum_kernel_trainer.py +++ b/qiskit_machine_learning/kernels/algorithms/quantum_kernel_trainer.py @@ -199,7 +199,7 @@ def fit( # Randomly initialize the initial point if one was not passed if self._initial_point is None: - self._initial_point = algorithm_globals.random().random(num_params) + self._initial_point = algorithm_globals.random.random(num_params) # Perform kernel optimization loss_function = partial( diff --git a/qiskit_machine_learning/kernels/fidelity_statevector_kernel.py b/qiskit_machine_learning/kernels/fidelity_statevector_kernel.py index 9da23ca70..8fa70d381 100644 --- a/qiskit_machine_learning/kernels/fidelity_statevector_kernel.py +++ b/qiskit_machine_learning/kernels/fidelity_statevector_kernel.py @@ -154,7 +154,7 @@ def _compute_fidelity(x: np.ndarray, y: np.ndarray) -> float: return np.abs(np.conj(x) @ y) ** 2 def _add_shot_noise(self, fidelity: float) -> float: - return algorithm_globals.random().binomial(n=self._shots, p=fidelity) / self._shots + return algorithm_globals.random.binomial(n=self._shots, p=fidelity) / self._shots def clear_cache(self): """Clear the statevector cache.""" diff --git a/qiskit_machine_learning/neural_networks/effective_dimension.py b/qiskit_machine_learning/neural_networks/effective_dimension.py index 4c32a92f2..275382607 100644 --- a/qiskit_machine_learning/neural_networks/effective_dimension.py +++ b/qiskit_machine_learning/neural_networks/effective_dimension.py @@ -83,7 +83,7 @@ def weight_samples(self, weight_samples: Union[np.ndarray, int]) -> None: """Sets network weight samples.""" if isinstance(weight_samples, int): # random sampling from uniform distribution - self._weight_samples = algorithm_globals.random().uniform( + self._weight_samples = algorithm_globals.random.uniform( 0, 1, size=(weight_samples, self._model.num_weights) ) else: @@ -109,7 +109,7 @@ def input_samples(self, input_samples: Union[np.ndarray, int]) -> None: """Sets network input samples.""" if isinstance(input_samples, int): # random sampling from normal distribution - self._input_samples = algorithm_globals.random().normal( + self._input_samples = algorithm_globals.random.normal( 0, 1, size=(input_samples, self._model.num_inputs) ) else: @@ -332,7 +332,7 @@ def weight_samples(self, weight_samples: Union[np.ndarray, int]) -> None: """Sets network parameters.""" if isinstance(weight_samples, int): # random sampling from uniform distribution - self._weight_samples = algorithm_globals.random().uniform( + self._weight_samples = algorithm_globals.random.uniform( 0, 1, size=(1, self._model.num_weights) ) else: diff --git a/qiskit_machine_learning/optimizers/gradient_descent.py b/qiskit_machine_learning/optimizers/gradient_descent.py index d9561b19a..a27ac36b8 100644 --- a/qiskit_machine_learning/optimizers/gradient_descent.py +++ b/qiskit_machine_learning/optimizers/gradient_descent.py @@ -73,7 +73,7 @@ class GradientDescent(SteppableOptimizer): .. code-block:: python - from qiskit_algorithms.optimizers import GradientDescent + from qiskit_machine_learning.optimizers import GradientDescent def f(x): return (np.linalg.norm(x) - 1) ** 2 @@ -93,7 +93,7 @@ def f(x): .. code-block:: python - from qiskit_algorithms.optimizers import GradientDescent + from qiskit_machine_learning.optimizers import GradientDescent def learning_rate(): power = 0.6 @@ -129,7 +129,7 @@ def grad_f(x): import random import numpy as np - from qiskit_algorithms.optimizers import GradientDescent + from qiskit_machine_learning.optimizers import GradientDescent def objective(x): if random.choice([True, False]): diff --git a/qiskit_machine_learning/optimizers/gsls.py b/qiskit_machine_learning/optimizers/gsls.py index b92a8c8cd..07645df8e 100644 --- a/qiskit_machine_learning/optimizers/gsls.py +++ b/qiskit_machine_learning/optimizers/gsls.py @@ -263,7 +263,7 @@ def sample_points( Returns: A tuple containing the sampling points and the directions. """ - normal_samples = algorithm_globals.random().normal(size=(num_points, n)) + normal_samples = algorithm_globals.random.normal(size=(num_points, n)) row_norms = np.linalg.norm(normal_samples, axis=1, keepdims=True) directions = normal_samples / row_norms points = x + self._options["sampling_radius"] * directions diff --git a/qiskit_machine_learning/optimizers/p_bfgs.py b/qiskit_machine_learning/optimizers/p_bfgs.py index ceb9c574d..7ed42dff5 100644 --- a/qiskit_machine_learning/optimizers/p_bfgs.py +++ b/qiskit_machine_learning/optimizers/p_bfgs.py @@ -144,7 +144,7 @@ def optimize_runner(_queue, _i_pt): # Multi-process sampling # Start off as many other processes running the optimize (can be 0) processes = [] for _ in range(num_procs): - i_pt = algorithm_globals.random().uniform(low, high) # Another random point in bounds + i_pt = algorithm_globals.random.uniform(low, high) # Another random point in bounds proc = multiprocessing.Process(target=optimize_runner, args=(queue, i_pt)) processes.append(proc) proc.start() diff --git a/qiskit_machine_learning/optimizers/spsa.py b/qiskit_machine_learning/optimizers/spsa.py index a3d00a12f..70a49f20b 100644 --- a/qiskit_machine_learning/optimizers/spsa.py +++ b/qiskit_machine_learning/optimizers/spsa.py @@ -652,10 +652,10 @@ def get_support_level(self): def bernoulli_perturbation(dim, perturbation_dims=None): """Get a Bernoulli random perturbation.""" if perturbation_dims is None: - return 1 - 2 * algorithm_globals.random().binomial(1, 0.5, size=dim) + return 1 - 2 * algorithm_globals.random.binomial(1, 0.5, size=dim) - pert = 1 - 2 * algorithm_globals.random().binomial(1, 0.5, size=perturbation_dims) - indices = algorithm_globals.random().choice( + pert = 1 - 2 * algorithm_globals.random.binomial(1, 0.5, size=perturbation_dims) + indices = algorithm_globals.random.choice( list(range(dim)), size=perturbation_dims, replace=False ) result = np.zeros(dim) diff --git a/qiskit_machine_learning/optimizers/umda.py b/qiskit_machine_learning/optimizers/umda.py index bc708b352..2b9eda872 100644 --- a/qiskit_machine_learning/optimizers/umda.py +++ b/qiskit_machine_learning/optimizers/umda.py @@ -172,7 +172,7 @@ def _new_generation(self): Updates the generation pandas dataframe """ - gen = algorithm_globals.random().normal( + gen = algorithm_globals.random.normal( self._vector[0, :], self._vector[1, :], [self._size_gen, self._n_variables] ) @@ -227,7 +227,7 @@ def minimize( self._vector = self._initialization() # initialization of generation - self._generation = algorithm_globals.random().normal( + self._generation = algorithm_globals.random.normal( self._vector[0, :], self._vector[1, :], [self._size_gen, self._n_variables] ) diff --git a/qiskit_machine_learning/utils/__init__.py b/qiskit_machine_learning/utils/__init__.py index 7b5a898ca..f56c0936b 100644 --- a/qiskit_machine_learning/utils/__init__.py +++ b/qiskit_machine_learning/utils/__init__.py @@ -28,6 +28,9 @@ """ from .adjust_num_qubits import derive_num_qubits_feature_map_ansatz +from .algorithm_globals import algorithm_globals +from .validate_initial_point import validate_initial_point +from .validate_bounds import validate_bounds __all__ = [ "derive_num_qubits_feature_map_ansatz", diff --git a/qiskit_machine_learning/utils/validate_initial_point.py b/qiskit_machine_learning/utils/validate_initial_point.py index bb849a77a..0bdd9eeb0 100644 --- a/qiskit_machine_learning/utils/validate_initial_point.py +++ b/qiskit_machine_learning/utils/validate_initial_point.py @@ -54,7 +54,7 @@ def validate_initial_point(point: np.ndarray | None | None, circuit: QuantumCirc upper_bounds.append(upper if upper is not None else 2 * np.pi) # sample from within bounds - point = algorithm_globals.random().uniform(lower_bounds, upper_bounds) + point = algorithm_globals.random.uniform(lower_bounds, upper_bounds) elif len(point) != expected_size: raise ValueError( diff --git a/test/algorithms/classifiers/test_neural_network_classifier.py b/test/algorithms/classifiers/test_neural_network_classifier.py index 44d346dc1..5f1613b00 100644 --- a/test/algorithms/classifiers/test_neural_network_classifier.py +++ b/test/algorithms/classifiers/test_neural_network_classifier.py @@ -106,7 +106,7 @@ def test_classifier_with_estimator_qnn(self, opt, loss, cb_flag): # construct data num_samples = 6 - X = algorithm_globals.random().random( # pylint: disable=invalid-name + X = algorithm_globals.random.random( # pylint: disable=invalid-name (num_samples, num_inputs) ) y = 2.0 * (np.sum(X, axis=1) <= 1) - 1.0 @@ -160,7 +160,7 @@ def parity(x): def _generate_data(self, num_inputs: int) -> tuple[np.ndarray, np.ndarray]: # construct data num_samples = 6 - features = algorithm_globals.random().random((num_samples, num_inputs)) + features = algorithm_globals.random.random((num_samples, num_inputs)) labels = 1.0 * (np.sum(features, axis=1) <= 1) return features, labels @@ -382,7 +382,7 @@ def test_binary_classification_with_multiclass_data(self): # construct data num_samples = 3 - x = algorithm_globals.random().random((num_samples, num_inputs)) + x = algorithm_globals.random.random((num_samples, num_inputs)) y = np.asarray([0, 1, 2]) with self.assertRaises(QiskitMachineLearningError): @@ -402,7 +402,7 @@ def test_bad_binary_shape(self): # construct data num_samples = 2 - x = algorithm_globals.random().random((num_samples, num_inputs)) + x = algorithm_globals.random.random((num_samples, num_inputs)) y = np.array([[0, 1], [1, 0]]) with self.assertRaises(QiskitMachineLearningError): @@ -419,7 +419,7 @@ def test_bad_one_hot_data(self): # construct data num_samples = 2 - x = algorithm_globals.random().random((num_samples, num_inputs)) + x = algorithm_globals.random.random((num_samples, num_inputs)) y = np.array([[0, 1], [2, 0]]) with self.assertRaises(QiskitMachineLearningError): diff --git a/test/algorithms/classifiers/test_vqc.py b/test/algorithms/classifiers/test_vqc.py index 4af50a576..4a2aca7cd 100644 --- a/test/algorithms/classifiers/test_vqc.py +++ b/test/algorithms/classifiers/test_vqc.py @@ -174,7 +174,7 @@ def test_batches_with_incomplete_labels(self): """Test VQC when targets are one-hot and some batches don't have all possible labels.""" # Generate data with batches that have incomplete labels. - x = algorithm_globals.random().random((6, 2)) + x = algorithm_globals.random.random((6, 2)) y = np.asarray([0, 0, 1, 1, 2, 2]) y_one_hot = OneHotEncoder().fit_transform(y.reshape(-1, 1)) @@ -203,7 +203,7 @@ def test_batches_with_incomplete_labels(self): def test_multilabel_targets_raise_an_error(self): """Tests VQC multi-label input raises an error.""" # Generate multi-label data. - x = algorithm_globals.random().random((3, 2)) + x = algorithm_globals.random.random((3, 2)) y = np.asarray([[1, 1, 0], [1, 0, 1], [0, 1, 1]]) classifier = VQC(num_qubits=2) @@ -215,8 +215,8 @@ def test_changing_classes_raises_error(self): targets1 = np.asarray([[0, 0, 1], [0, 1, 0]]) targets2 = np.asarray([[0, 1], [1, 0]]) - features1 = algorithm_globals.random().random((len(targets1), 2)) - features2 = algorithm_globals.random().random((len(targets2), 2)) + features1 = algorithm_globals.random.random((len(targets1), 2)) + features2 = algorithm_globals.random.random((len(targets2), 2)) classifier = VQC(num_qubits=2, warm_start=True) classifier.fit(features1, targets1) diff --git a/test/circuit/library/test_raw_feature_vector.py b/test/circuit/library/test_raw_feature_vector.py index 1d7155a0c..87643d311 100644 --- a/test/circuit/library/test_raw_feature_vector.py +++ b/test/circuit/library/test_raw_feature_vector.py @@ -99,7 +99,7 @@ def test_usage_in_vqc(self): # construct data num_samples = 10 num_inputs = 4 - X = algorithm_globals.random().random( # pylint: disable=invalid-name + X = algorithm_globals.random.random( # pylint: disable=invalid-name (num_samples, num_inputs) ) y = 1.0 * (np.sum(X, axis=1) <= 2) diff --git a/test/kernels/test_fidelity_qkernel.py b/test/kernels/test_fidelity_qkernel.py index 9d1b273e1..12c762c78 100644 --- a/test/kernels/test_fidelity_qkernel.py +++ b/test/kernels/test_fidelity_qkernel.py @@ -96,7 +96,7 @@ def test_svc_precomputed(self): def test_defaults(self): """Test quantum kernel with all default values.""" - features = algorithm_globals.random().random((10, 2)) - 0.5 + features = algorithm_globals.random.random((10, 2)) - 0.5 labels = np.sign(features[:, 0]) kernel = FidelityQuantumKernel() @@ -289,20 +289,7 @@ def _run( values = np.asarray(values_1) fidelities = np.full(values.shape[0], -0.5) - # Qiskit algorithms changed the internals of the base state fidelity - # class and what this method returns to avoid a threading issue. See - # https://github.com/qiskit-community/qiskit-algorithms/pull/92 for - # more information. That pull request will land in 0.3.0. I made - # this test work with the current released version 0.2.2, so tests - # pass at present, but in the future this logic can be reduced to - # just that needed for 0.3.0 and above if desired when testing against - # earlier algorithm versions is no longer needed or wanted. - from qiskit_algorithms import __version__ as algs_version - - if algs_version < "0.3.0": - return StateFidelityResult(fidelities, [], {}, options) - else: - return AlgorithmJob(MockFidelity._call, fidelities, options) + return AlgorithmJob(MockFidelity._call, fidelities, options) @staticmethod def _call(fidelities, options) -> StateFidelityResult: diff --git a/test/kernels/test_fidelity_statevector_kernel.py b/test/kernels/test_fidelity_statevector_kernel.py index 097c4cfb6..cd92b8c23 100644 --- a/test/kernels/test_fidelity_statevector_kernel.py +++ b/test/kernels/test_fidelity_statevector_kernel.py @@ -89,7 +89,7 @@ def test_svc_precomputed(self): def test_defaults(self): """Test statevector kernel with all default values.""" - features = algorithm_globals.random().random((10, 2)) - 0.5 + features = algorithm_globals.random.random((10, 2)) - 0.5 labels = np.sign(features[:, 0]) kernel = FidelityStatevectorKernel() @@ -101,7 +101,7 @@ def test_defaults(self): def test_with_shot_noise(self): """Test statevector kernel with shot noise emulation.""" - features = algorithm_globals.random().random((3, 2)) - 0.5 + features = algorithm_globals.random.random((3, 2)) - 0.5 kernel = FidelityStatevectorKernel( feature_map=self.feature_map, shots=10, enforce_psd=False ) @@ -136,7 +136,7 @@ def test_aer_statevector(self): """Test statevector kernel when using AerStatevector type statevectors.""" from qiskit_aer.quantum_info import AerStatevector - features = algorithm_globals.random().random((10, 2)) - 0.5 + features = algorithm_globals.random.random((10, 2)) - 0.5 labels = np.sign(features[:, 0]) kernel = FidelityStatevectorKernel(statevector_type=AerStatevector) diff --git a/test/neural_networks/test_effective_dimension.py b/test/neural_networks/test_effective_dimension.py index cda09018e..149b2428b 100644 --- a/test/neural_networks/test_effective_dimension.py +++ b/test/neural_networks/test_effective_dimension.py @@ -149,8 +149,8 @@ def test_inputs(self): qnn = self.qnns["sampler_qnn_1"] num_input_samples, num_weight_samples = 10, 10 - inputs = algorithm_globals.random().uniform(0, 1, size=(num_input_samples, qnn.num_inputs)) - weights = algorithm_globals.random().uniform(0, 1, size=(num_weight_samples, qnn.num_weights)) + inputs = algorithm_globals.random.uniform(0, 1, size=(num_input_samples, qnn.num_inputs)) + weights = algorithm_globals.random.uniform(0, 1, size=(num_weight_samples, qnn.num_weights)) global_ed1 = EffectiveDimension( qnn=qnn, @@ -175,11 +175,11 @@ def test_inputs_shapes(self): qnn = self.qnns["sampler_qnn_1"] num_inputs, num_params = 10, 10 - inputs_ok = algorithm_globals.random().uniform(0, 1, size=(num_inputs, qnn.num_inputs)) - weights_ok = algorithm_globals.random().uniform(0, 1, size=(num_params, qnn.num_weights)) + inputs_ok = algorithm_globals.random.uniform(0, 1, size=(num_inputs, qnn.num_inputs)) + weights_ok = algorithm_globals.random.uniform(0, 1, size=(num_params, qnn.num_weights)) - inputs_wrong = algorithm_globals.random().uniform(0, 1, size=(num_inputs, 1)) - weights_wrong = algorithm_globals.random().uniform(0, 1, size=(num_params, 1)) + inputs_wrong = algorithm_globals.random.uniform(0, 1, size=(num_inputs, 1)) + weights_wrong = algorithm_globals.random.uniform(0, 1, size=(num_params, 1)) with self.assertRaises(QiskitMachineLearningError): EffectiveDimension( @@ -201,10 +201,10 @@ def test_local_ed_params(self): qnn = self.qnns["sampler_qnn_1"] num_inputs, num_params = 10, 10 - inputs_ok = algorithm_globals.random().uniform(0, 1, size=(num_inputs, qnn.num_inputs)) - weights_ok = algorithm_globals.random().uniform(0, 1, size=(1, qnn.num_weights)) - weights_ok2 = algorithm_globals.random().uniform(0, 1, size=qnn.num_weights) - weights_wrong = algorithm_globals.random().uniform(0, 1, size=(num_params, qnn.num_weights)) + inputs_ok = algorithm_globals.random.uniform(0, 1, size=(num_inputs, qnn.num_inputs)) + weights_ok = algorithm_globals.random.uniform(0, 1, size=(1, qnn.num_weights)) + weights_ok2 = algorithm_globals.random.uniform(0, 1, size=qnn.num_weights) + weights_wrong = algorithm_globals.random.uniform(0, 1, size=(num_params, qnn.num_weights)) LocalEffectiveDimension( qnn=qnn, From ddc160f91151ab3b75416ac13e0e1f5e8b249a71 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Thu, 11 Jul 2024 17:18:38 +0200 Subject: [PATCH 27/85] Import /tests and run CI locally --- qiskit_machine_learning/algorithm_job.py | 2 +- .../algorithms/trainable_model.py | 11 +- .../circuit/library/qnn_circuit.py | 5 +- .../connectors/torch_connector.py | 1 + qiskit_machine_learning/datasets/ad_hoc.py | 7 +- .../neural_networks/sampler_qnn.py | 7 +- .../optimizers/adam_amsgrad.py | 3 +- qiskit_machine_learning/optimizers/bobyqa.py | 2 +- qiskit_machine_learning/optimizers/imfil.py | 2 +- .../optimizers/nlopts/nloptimizer.py | 4 +- qiskit_machine_learning/optimizers/snobfit.py | 4 +- test/__init__.py | 4 +- .../classifiers/test_pegasos_qsvc.py | 5 +- test/algorithms/inference/test_qbayesian.py | 4 +- .../test_neural_network_regressor.py | 5 +- test/algorithms_test_case.py | 88 +++ test/decorators.py | 37 + test/gradients/__init__.py | 13 + test/gradients/logging_primitives.py | 42 ++ test/gradients/test_estimator_gradient.py | 451 ++++++++++++ test/gradients/test_qfi.py | 151 ++++ test/gradients/test_qgt.py | 310 +++++++++ test/gradients/test_sampler_gradient.py | 644 ++++++++++++++++++ .../test_fidelity_qkernel_trainer.py | 5 +- test/kernels/test_fidelity_qkernel.py | 5 +- test/optimizers/__init__.py | 13 + test/optimizers/test_gradient_descent.py | 195 ++++++ test/optimizers/test_optimizer_aqgd.py | 75 ++ test/optimizers/test_optimizers.py | 441 ++++++++++++ .../optimizers/test_optimizers_scikitquant.py | 68 ++ test/optimizers/test_spsa.py | 266 ++++++++ test/optimizers/test_umda.py | 94 +++ test/optimizers/utils/__init__.py | 12 + test/optimizers/utils/test_learning_rate.py | 56 ++ test/state_fidelities/__init__.py | 13 + .../test_compute_uncompute.py | 265 +++++++ test/utils/test_validate_bounds.py | 52 ++ test/utils/test_validate_initial_point.py | 49 ++ 38 files changed, 3379 insertions(+), 32 deletions(-) create mode 100644 test/algorithms_test_case.py create mode 100644 test/decorators.py create mode 100644 test/gradients/__init__.py create mode 100644 test/gradients/logging_primitives.py create mode 100644 test/gradients/test_estimator_gradient.py create mode 100644 test/gradients/test_qfi.py create mode 100644 test/gradients/test_qgt.py create mode 100644 test/gradients/test_sampler_gradient.py create mode 100644 test/optimizers/__init__.py create mode 100644 test/optimizers/test_gradient_descent.py create mode 100644 test/optimizers/test_optimizer_aqgd.py create mode 100644 test/optimizers/test_optimizers.py create mode 100644 test/optimizers/test_optimizers_scikitquant.py create mode 100644 test/optimizers/test_spsa.py create mode 100644 test/optimizers/test_umda.py create mode 100644 test/optimizers/utils/__init__.py create mode 100644 test/optimizers/utils/test_learning_rate.py create mode 100644 test/state_fidelities/__init__.py create mode 100644 test/state_fidelities/test_compute_uncompute.py create mode 100644 test/utils/test_validate_bounds.py create mode 100644 test/utils/test_validate_initial_point.py diff --git a/qiskit_machine_learning/algorithm_job.py b/qiskit_machine_learning/algorithm_job.py index b12155746..abd6def46 100644 --- a/qiskit_machine_learning/algorithm_job.py +++ b/qiskit_machine_learning/algorithm_job.py @@ -42,4 +42,4 @@ def submit(self) -> None: try: super()._submit() except AttributeError: - super().submit() + super().submit() # pylint: disable=no-member diff --git a/qiskit_machine_learning/algorithms/trainable_model.py b/qiskit_machine_learning/algorithms/trainable_model.py index 1b67e5c69..0226d925f 100644 --- a/qiskit_machine_learning/algorithms/trainable_model.py +++ b/qiskit_machine_learning/algorithms/trainable_model.py @@ -14,12 +14,14 @@ from abc import abstractmethod from typing import Callable - import numpy as np -from ..optimizers import Optimizer, SLSQP, OptimizerResult, Minimizer -from ..utils import algorithm_globals from qiskit_machine_learning import QiskitMachineLearningError + +from .objective_functions import ObjectiveFunction +from .serializable_model import SerializableModelMixin +from ..optimizers import Optimizer, SLSQP, OptimizerResult, Minimizer +from ..utils import algorithm_globals from ..neural_networks import NeuralNetwork from ..utils.loss_functions import ( Loss, @@ -28,9 +30,6 @@ CrossEntropyLoss, ) -from .objective_functions import ObjectiveFunction -from .serializable_model import SerializableModelMixin - class TrainableModel(SerializableModelMixin): """Base class for ML model that defines a scikit-learn like interface for Estimators.""" diff --git a/qiskit_machine_learning/circuit/library/qnn_circuit.py b/qiskit_machine_learning/circuit/library/qnn_circuit.py index 3f4f3e01e..974ba8c16 100644 --- a/qiskit_machine_learning/circuit/library/qnn_circuit.py +++ b/qiskit_machine_learning/circuit/library/qnn_circuit.py @@ -13,12 +13,15 @@ """The QNN circuit.""" from __future__ import annotations from typing import List + from qiskit.circuit import QuantumRegister, QuantumCircuit from qiskit.circuit.parametertable import ParameterView from qiskit.circuit.library import BlueprintCircuit -from ...utils import derive_num_qubits_feature_map_ansatz + from qiskit_machine_learning import QiskitMachineLearningError +from ...utils import derive_num_qubits_feature_map_ansatz + class QNNCircuit(BlueprintCircuit): """ diff --git a/qiskit_machine_learning/connectors/torch_connector.py b/qiskit_machine_learning/connectors/torch_connector.py index 321ff5810..cc120a514 100644 --- a/qiskit_machine_learning/connectors/torch_connector.py +++ b/qiskit_machine_learning/connectors/torch_connector.py @@ -59,6 +59,7 @@ class Module: # type: ignore CHAR_LIMIT = 26 +# pylint: disable=no-member def _get_einsum_signature(n_dimensions: int, for_weights: bool = False) -> str: diff --git a/qiskit_machine_learning/datasets/ad_hoc.py b/qiskit_machine_learning/datasets/ad_hoc.py index e1dd96428..f553f74f4 100644 --- a/qiskit_machine_learning/datasets/ad_hoc.py +++ b/qiskit_machine_learning/datasets/ad_hoc.py @@ -15,14 +15,15 @@ """ from __future__ import annotations -import itertools as it from functools import reduce +import itertools as it from typing import Tuple, Dict, List - import numpy as np +from sklearn import preprocessing + from qiskit.utils import optionals + from ..utils import algorithm_globals -from sklearn import preprocessing def ad_hoc_data( diff --git a/qiskit_machine_learning/neural_networks/sampler_qnn.py b/qiskit_machine_learning/neural_networks/sampler_qnn.py index 4254a8881..81d5f615d 100644 --- a/qiskit_machine_learning/neural_networks/sampler_qnn.py +++ b/qiskit_machine_learning/neural_networks/sampler_qnn.py @@ -19,19 +19,20 @@ from typing import Callable, cast, Iterable, Sequence import numpy as np + from qiskit.circuit import Parameter, QuantumCircuit from qiskit.primitives import BaseSampler, SamplerResult, Sampler + +import qiskit_machine_learning.optionals as _optionals +from .neural_network import NeuralNetwork from ..gradients import ( BaseSamplerGradient, ParamShiftSamplerGradient, SamplerGradientResult, ) - from ..circuit.library import QNNCircuit from ..exceptions import QiskitMachineLearningError -import qiskit_machine_learning.optionals as _optionals -from .neural_network import NeuralNetwork if _optionals.HAS_SPARSE: # pylint: disable=import-error diff --git a/qiskit_machine_learning/optimizers/adam_amsgrad.py b/qiskit_machine_learning/optimizers/adam_amsgrad.py index 6deea91b9..b1639450e 100644 --- a/qiskit_machine_learning/optimizers/adam_amsgrad.py +++ b/qiskit_machine_learning/optimizers/adam_amsgrad.py @@ -147,14 +147,13 @@ def save_params(self, snapshot_dir: str) -> None: Args: snapshot_dir: The directory to store the file in. """ + # pylint: disable=unspecified-encoding if self._amsgrad: - # pylint: disable=unspecified-encoding with open(os.path.join(snapshot_dir, "adam_params.csv"), mode="a") as csv_file: fieldnames = ["v", "v_eff", "m", "t"] writer = csv.DictWriter(csv_file, fieldnames=fieldnames) writer.writerow({"v": self._v, "v_eff": self._v_eff, "m": self._m, "t": self._t}) else: - # pylint: disable=unspecified-encoding with open(os.path.join(snapshot_dir, "adam_params.csv"), mode="a") as csv_file: fieldnames = ["v", "m", "t"] writer = csv.DictWriter(csv_file, fieldnames=fieldnames) diff --git a/qiskit_machine_learning/optimizers/bobyqa.py b/qiskit_machine_learning/optimizers/bobyqa.py index e990366cc..6f21d6603 100644 --- a/qiskit_machine_learning/optimizers/bobyqa.py +++ b/qiskit_machine_learning/optimizers/bobyqa.py @@ -67,7 +67,7 @@ def minimize( jac: Callable[[POINT], POINT] | None = None, bounds: list[tuple[float, float]] | None = None, ) -> OptimizerResult: - from skquant import opt as skq + from skquant import opt as skq # pylint: disable=import-error res, history = skq.minimize( func=fun, diff --git a/qiskit_machine_learning/optimizers/imfil.py b/qiskit_machine_learning/optimizers/imfil.py index 15d73948e..604c65efb 100644 --- a/qiskit_machine_learning/optimizers/imfil.py +++ b/qiskit_machine_learning/optimizers/imfil.py @@ -69,7 +69,7 @@ def minimize( jac: Callable[[POINT], POINT] | None = None, bounds: list[tuple[float, float]] | None = None, ) -> OptimizerResult: - from skquant import opt as skq + from skquant import opt as skq # pylint: disable=import-error res, history = skq.minimize( func=fun, diff --git a/qiskit_machine_learning/optimizers/nlopts/nloptimizer.py b/qiskit_machine_learning/optimizers/nlopts/nloptimizer.py index 51ce5b552..b88584401 100644 --- a/qiskit_machine_learning/optimizers/nlopts/nloptimizer.py +++ b/qiskit_machine_learning/optimizers/nlopts/nloptimizer.py @@ -51,7 +51,7 @@ def __init__(self, max_evals: int = 1000) -> None: # pylint: disable=unused-arg Raises: MissingOptionalLibraryError: NLopt library not installed. """ - import nlopt + import nlopt # pylint: disable=import-error super().__init__() for k, v in list(locals().items()): @@ -90,7 +90,7 @@ def minimize( jac: Callable[[POINT], POINT] | None = None, bounds: list[tuple[float, float]] | None = None, ) -> OptimizerResult: - import nlopt + import nlopt # pylint: disable=import-error x0 = np.asarray(x0) diff --git a/qiskit_machine_learning/optimizers/snobfit.py b/qiskit_machine_learning/optimizers/snobfit.py index 7596c28f0..0e48e2229 100644 --- a/qiskit_machine_learning/optimizers/snobfit.py +++ b/qiskit_machine_learning/optimizers/snobfit.py @@ -93,8 +93,8 @@ def minimize( jac: Callable[[POINT], POINT] | None = None, bounds: list[tuple[float, float]] | None = None, ) -> OptimizerResult: - import skquant.opt as skq - from SQSnobFit import optset + import skquant.opt as skq # pylint: disable=import-error + from SQSnobFit import optset # pylint: disable=import-error if bounds is None or any(None in bound_tuple for bound_tuple in bounds): raise ValueError("Optimizer SNOBFIT requires bounds for all parameters.") diff --git a/test/__init__.py b/test/__init__.py index f3216fbbf..7b29e8d6d 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -13,5 +13,7 @@ """ ML test packages """ from .machine_learning_test_case import QiskitMachineLearningTestCase, gpu +from .algorithms_test_case import QiskitAlgorithmsTestCase +from .decorators import slow_test -__all__ = ["QiskitMachineLearningTestCase", "gpu"] +__all__ = ["QiskitMachineLearningTestCase", "gpu", "QiskitAlgorithmsTestCase", "slow_test"] diff --git a/test/algorithms/classifiers/test_pegasos_qsvc.py b/test/algorithms/classifiers/test_pegasos_qsvc.py index 78725e40e..b5a16415e 100644 --- a/test/algorithms/classifiers/test_pegasos_qsvc.py +++ b/test/algorithms/classifiers/test_pegasos_qsvc.py @@ -18,11 +18,12 @@ from test import QiskitMachineLearningTestCase import numpy as np -from qiskit.circuit.library import ZFeatureMap -from qiskit_machine_learning.utils import algorithm_globals from sklearn.datasets import make_blobs from sklearn.preprocessing import MinMaxScaler +from qiskit.circuit.library import ZFeatureMap + +from qiskit_machine_learning.utils import algorithm_globals from qiskit_machine_learning.algorithms import PegasosQSVC, SerializableModelMixin from qiskit_machine_learning.kernels import FidelityQuantumKernel diff --git a/test/algorithms/inference/test_qbayesian.py b/test/algorithms/inference/test_qbayesian.py index 44976c8e3..d0b114b8d 100644 --- a/test/algorithms/inference/test_qbayesian.py +++ b/test/algorithms/inference/test_qbayesian.py @@ -16,10 +16,12 @@ from test import QiskitMachineLearningTestCase import numpy as np -from qiskit_machine_learning.utils import algorithm_globals + from qiskit import QuantumCircuit from qiskit.circuit import QuantumRegister from qiskit.primitives import Sampler + +from qiskit_machine_learning.utils import algorithm_globals from qiskit_machine_learning.algorithms import QBayesian diff --git a/test/algorithms/regressors/test_neural_network_regressor.py b/test/algorithms/regressors/test_neural_network_regressor.py index 3bbb46ea7..da5db70c6 100644 --- a/test/algorithms/regressors/test_neural_network_regressor.py +++ b/test/algorithms/regressors/test_neural_network_regressor.py @@ -22,12 +22,13 @@ import numpy as np from ddt import ddt, unpack, idata +from scipy.optimize import minimize + from qiskit.circuit import Parameter, QuantumCircuit from qiskit.circuit.library import ZZFeatureMap, RealAmplitudes + from qiskit_machine_learning.optimizers import COBYLA, L_BFGS_B, SPSA from qiskit_machine_learning.utils import algorithm_globals -from scipy.optimize import minimize - from qiskit_machine_learning import QiskitMachineLearningError from qiskit_machine_learning.algorithms import SerializableModelMixin from qiskit_machine_learning.algorithms.regressors import NeuralNetworkRegressor diff --git a/test/algorithms_test_case.py b/test/algorithms_test_case.py new file mode 100644 index 000000000..d6e29b252 --- /dev/null +++ b/test/algorithms_test_case.py @@ -0,0 +1,88 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2018, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Algorithms Test Case""" + +from typing import Optional +from abc import ABC +import warnings +import inspect +import logging +import os +import unittest +import time + +from qiskit_machine_learning.utils import algorithm_globals + +# disable deprecation warnings that can cause log output overflow +# pylint: disable=unused-argument + + +def _noop(*args, **kargs): + pass + + +# disable warning messages +# warnings.warn = _noop + + +class QiskitAlgorithmsTestCase(unittest.TestCase, ABC): + """Optimization Test Case""" + + moduleName = None + log = None + + def setUp(self) -> None: + warnings.filterwarnings("default", category=DeprecationWarning) + self._started_at = time.time() + self._class_location = __file__ + + def tearDown(self) -> None: + algorithm_globals.random_seed = None + elapsed = time.time() - self._started_at + if elapsed > 5.0: + print(f"({round(elapsed, 2):.2f}s)", flush=True) + + @classmethod + def setUpClass(cls) -> None: + cls.moduleName = os.path.splitext(inspect.getfile(cls))[0] + cls.log = logging.getLogger(cls.__name__) + + # Set logging to file and stdout if the LOG_LEVEL environment variable + # is set. + if os.getenv("LOG_LEVEL"): + # Set up formatter. + log_fmt = f"{cls.__name__}.%(funcName)s:%(levelname)s:%(asctime)s:" " %(message)s" + formatter = logging.Formatter(log_fmt) + + # Set up the file handler. + log_file_name = f"{cls.moduleName}.log" + file_handler = logging.FileHandler(log_file_name) + file_handler.setFormatter(formatter) + cls.log.addHandler(file_handler) + + # Set the logging level from the environment variable, defaulting + # to INFO if it is not a valid level. + level = logging._nameToLevel.get(os.getenv("LOG_LEVEL"), logging.INFO) + cls.log.setLevel(level) + + def get_resource_path(self, filename: str, path: Optional[str] = None) -> str: + """Get the absolute path to a resource. + Args: + filename: filename or relative path to the resource. + path: path used as relative to the filename. + Returns: + str: the absolute path to the resource. + """ + root = os.path.dirname(self._class_location) + path = root if path is None else os.path.join(root, path) + return os.path.normpath(os.path.join(path, filename)) diff --git a/test/decorators.py b/test/decorators.py new file mode 100644 index 000000000..e14a3ba23 --- /dev/null +++ b/test/decorators.py @@ -0,0 +1,37 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2017, 2024. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + + +"""Decorator for using with unit tests.""" + +import functools +import os +import unittest + + +def slow_test(func): + """Decorator that signals that the test takes minutes to run. + + Args: + func (callable): test function to be decorated. + + Returns: + callable: the decorated function. + """ + + @functools.wraps(func) + def _wrapper(*args, **kwargs): + if "run_slow" in os.environ.get("QISKIT_TESTS", ""): + raise unittest.SkipTest("Skipping slow tests") + return func(*args, **kwargs) + + return _wrapper diff --git a/test/gradients/__init__.py b/test/gradients/__init__.py new file mode 100644 index 000000000..3f5bc2144 --- /dev/null +++ b/test/gradients/__init__.py @@ -0,0 +1,13 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Tests for the primitive-based gradients""" diff --git a/test/gradients/logging_primitives.py b/test/gradients/logging_primitives.py new file mode 100644 index 000000000..e9a9f21c5 --- /dev/null +++ b/test/gradients/logging_primitives.py @@ -0,0 +1,42 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Test primitives that check what kind of operations are in the circuits they execute.""" + +from qiskit.primitives import Estimator, Sampler + + +class LoggingEstimator(Estimator): + """An estimator checking what operations were in the circuits it executed.""" + + def __init__(self, options=None, operations_callback=None): + super().__init__(options=options) + self.operations_callback = operations_callback + + def _run(self, circuits, observables, parameter_values, **run_options): + if self.operations_callback is not None: + ops = [circuit.count_ops() for circuit in circuits] + self.operations_callback(ops) + return super()._run(circuits, observables, parameter_values, **run_options) + + +class LoggingSampler(Sampler): + """A sampler checking what operations were in the circuits it executed.""" + + def __init__(self, operations_callback): + super().__init__() + self.operations_callback = operations_callback + + def _run(self, circuits, parameter_values, **run_options): + ops = [circuit.count_ops() for circuit in circuits] + self.operations_callback(ops) + return super()._run(circuits, parameter_values, **run_options) diff --git a/test/gradients/test_estimator_gradient.py b/test/gradients/test_estimator_gradient.py new file mode 100644 index 000000000..ab2a97fac --- /dev/null +++ b/test/gradients/test_estimator_gradient.py @@ -0,0 +1,451 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2019, 2024. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +# ============================================================================= + +"""Test Estimator Gradients""" + +import unittest +from test import QiskitAlgorithmsTestCase + +import numpy as np +from ddt import ddt, data + +from qiskit import QuantumCircuit +from qiskit.circuit import Parameter +from qiskit.circuit.library import EfficientSU2, RealAmplitudes +from qiskit.circuit.library.standard_gates import RXXGate, RYYGate, RZXGate, RZZGate +from qiskit.primitives import Estimator +from qiskit.quantum_info import SparsePauliOp + +from qiskit_machine_learning.gradients import ( + LinCombEstimatorGradient, + ParamShiftEstimatorGradient, + SPSAEstimatorGradient, +) + +from .logging_primitives import LoggingEstimator + +gradient_factories = [ + ParamShiftEstimatorGradient, + LinCombEstimatorGradient, +] + + +@ddt +class TestEstimatorGradient(QiskitAlgorithmsTestCase): + """Test Estimator Gradient""" + + @data(*gradient_factories) + def test_gradient_operators(self, grad): + """Test the estimator gradient for different operators""" + estimator = Estimator() + a = Parameter("a") + qc = QuantumCircuit(1) + qc.h(0) + qc.p(a, 0) + qc.h(0) + gradient = grad(estimator) + op = SparsePauliOp.from_list([("Z", 1)]) + correct_result = -1 / np.sqrt(2) + param = [np.pi / 4] + value = gradient.run([qc], [op], [param]).result().gradients[0] + self.assertAlmostEqual(value[0], correct_result, 3) + op = SparsePauliOp.from_list([("Z", 1)]) + value = gradient.run([qc], [op], [param]).result().gradients[0] + self.assertAlmostEqual(value[0], correct_result, 3) + + @data(*gradient_factories) + def test_single_circuit_observable(self, grad): + """Test the estimator gradient for a single circuit and observable""" + estimator = Estimator() + a = Parameter("a") + qc = QuantumCircuit(1) + qc.h(0) + qc.p(a, 0) + qc.h(0) + gradient = grad(estimator) + op = SparsePauliOp.from_list([("Z", 1)]) + correct_result = -1 / np.sqrt(2) + param = [np.pi / 4] + value = gradient.run(qc, op, [param]).result().gradients[0] + self.assertAlmostEqual(value[0], correct_result, 3) + + @data(*gradient_factories) + def test_gradient_p(self, grad): + """Test the estimator gradient for p""" + estimator = Estimator() + a = Parameter("a") + qc = QuantumCircuit(1) + qc.h(0) + qc.p(a, 0) + qc.h(0) + gradient = grad(estimator) + op = SparsePauliOp.from_list([("Z", 1)]) + param_list = [[np.pi / 4], [0], [np.pi / 2]] + correct_results = [[-1 / np.sqrt(2)], [0], [-1]] + for i, param in enumerate(param_list): + gradients = gradient.run([qc], [op], [param]).result().gradients[0] + for j, value in enumerate(gradients): + self.assertAlmostEqual(value, correct_results[i][j], 3) + + @data(*gradient_factories) + def test_gradient_u(self, grad): + """Test the estimator gradient for u""" + estimator = Estimator() + a = Parameter("a") + b = Parameter("b") + c = Parameter("c") + qc = QuantumCircuit(1) + qc.h(0) + qc.u(a, b, c, 0) + qc.h(0) + gradient = grad(estimator) + op = SparsePauliOp.from_list([("Z", 1)]) + + param_list = [[np.pi / 4, 0, 0], [np.pi / 4, np.pi / 4, np.pi / 4]] + correct_results = [[-0.70710678, 0.0, 0.0], [-0.35355339, -0.85355339, -0.85355339]] + for i, param in enumerate(param_list): + gradients = gradient.run([qc], [op], [param]).result().gradients[0] + for j, value in enumerate(gradients): + self.assertAlmostEqual(value, correct_results[i][j], 3) + + @data(*gradient_factories) + def test_gradient_efficient_su2(self, grad): + """Test the estimator gradient for EfficientSU2""" + estimator = Estimator() + qc = EfficientSU2(2, reps=1) + op = SparsePauliOp.from_list([("ZI", 1)]) + gradient = grad(estimator) + param_list = [ + [np.pi / 4 for param in qc.parameters], + [np.pi / 2 for param in qc.parameters], + ] + correct_results = [ + [ + -0.35355339, + -0.70710678, + 0, + 0.35355339, + 0, + -0.70710678, + 0, + 0, + ], + [0, 0, 0, 1, 0, 0, 0, 0], + ] + for i, param in enumerate(param_list): + gradients = gradient.run([qc], [op], [param]).result().gradients[0] + np.testing.assert_allclose(gradients, correct_results[i], atol=1e-3) + + @data(*gradient_factories) + def test_gradient_2qubit_gate(self, grad): + """Test the estimator gradient for 2 qubit gates""" + estimator = Estimator() + for gate in [RXXGate, RYYGate, RZZGate, RZXGate]: + param_list = [[np.pi / 4], [np.pi / 2]] + correct_results = [ + [-0.70710678], + [-1], + ] + op = SparsePauliOp.from_list([("ZI", 1)]) + for i, param in enumerate(param_list): + a = Parameter("a") + qc = QuantumCircuit(2) + gradient = grad(estimator) + + if gate is RZZGate: + qc.h([0, 1]) + qc.append(gate(a), [qc.qubits[0], qc.qubits[1]], []) + qc.h([0, 1]) + else: + qc.append(gate(a), [qc.qubits[0], qc.qubits[1]], []) + gradients = gradient.run([qc], [op], [param]).result().gradients[0] + np.testing.assert_allclose(gradients, correct_results[i], atol=1e-3) + + @data(*gradient_factories) + def test_gradient_parameter_coefficient(self, grad): + """Test the estimator gradient for parameter variables with coefficients""" + estimator = Estimator() + qc = RealAmplitudes(num_qubits=2, reps=1) + qc.rz(qc.parameters[0].exp() + 2 * qc.parameters[1], 0) + qc.rx(3.0 * qc.parameters[0] + qc.parameters[1].sin(), 1) + qc.u(qc.parameters[0], qc.parameters[1], qc.parameters[3], 1) + qc.p(2 * qc.parameters[0] + 1, 0) + qc.rxx(qc.parameters[0] + 2, 0, 1) + gradient = grad(estimator) + param_list = [[np.pi / 4 for _ in qc.parameters], [np.pi / 2 for _ in qc.parameters]] + correct_results = [ + [-0.7266653, -0.4905135, -0.0068606, -0.9228880], + [-3.5972095, 0.10237173, -0.3117748, 0], + ] + op = SparsePauliOp.from_list([("ZI", 1)]) + for i, param in enumerate(param_list): + gradients = gradient.run([qc], [op], [param]).result().gradients[0] + np.testing.assert_allclose(gradients, correct_results[i], atol=1e-3) + + @data(*gradient_factories) + def test_gradient_parameters(self, grad): + """Test the estimator gradient for parameters""" + estimator = Estimator() + a = Parameter("a") + b = Parameter("b") + qc = QuantumCircuit(1) + qc.rx(a, 0) + qc.rx(b, 0) + gradient = grad(estimator) + param_list = [[np.pi / 4, np.pi / 2]] + correct_results = [ + [-0.70710678], + ] + op = SparsePauliOp.from_list([("Z", 1)]) + for i, param in enumerate(param_list): + gradients = gradient.run([qc], [op], [param], parameters=[[a]]).result().gradients[0] + np.testing.assert_allclose(gradients, correct_results[i], atol=1e-3) + + # parameter order + with self.subTest(msg="The order of gradients"): + c = Parameter("c") + qc = QuantumCircuit(1) + qc.rx(a, 0) + qc.rz(b, 0) + qc.rx(c, 0) + + param_list = [[np.pi / 4, np.pi / 2, np.pi / 3]] + correct_results = [ + [-0.35355339, 0.61237244, -0.61237244], + [-0.61237244, 0.61237244, -0.35355339], + [-0.35355339, -0.61237244], + [-0.61237244, -0.35355339], + ] + param = [[a, b, c], [c, b, a], [a, c], [c, a]] + op = SparsePauliOp.from_list([("Z", 1)]) + for i, p in enumerate(param): # pylint: disable=invalid-name + gradient = grad(estimator) + gradients = ( + gradient.run([qc], [op], param_list, parameters=[p]).result().gradients[0] + ) + np.testing.assert_allclose(gradients, correct_results[i], atol=1e-3) + + @data(*gradient_factories) + def test_gradient_multi_arguments(self, grad): + """Test the estimator gradient for multiple arguments""" + estimator = Estimator() + a = Parameter("a") + b = Parameter("b") + qc = QuantumCircuit(1) + qc.rx(a, 0) + qc2 = QuantumCircuit(1) + qc2.rx(b, 0) + gradient = grad(estimator) + param_list = [[np.pi / 4], [np.pi / 2]] + correct_results = [ + [-0.70710678], + [-1], + ] + op = SparsePauliOp.from_list([("Z", 1)]) + gradients = gradient.run([qc, qc2], [op] * 2, param_list).result().gradients + np.testing.assert_allclose(gradients, correct_results, atol=1e-3) + + c = Parameter("c") + qc3 = QuantumCircuit(1) + qc3.rx(c, 0) + qc3.ry(a, 0) + param_list2 = [[np.pi / 4], [np.pi / 4, np.pi / 4], [np.pi / 4, np.pi / 4]] + correct_results2 = [ + [-0.70710678], + [-0.5], + [-0.5, -0.5], + ] + gradients2 = ( + gradient.run([qc, qc3, qc3], [op] * 3, param_list2, parameters=[[a], [c], None]) + .result() + .gradients + ) + np.testing.assert_allclose(gradients2[0], correct_results2[0], atol=1e-3) + np.testing.assert_allclose(gradients2[1], correct_results2[1], atol=1e-3) + np.testing.assert_allclose(gradients2[2], correct_results2[2], atol=1e-3) + + @data(*gradient_factories) + def test_gradient_validation(self, grad): + """Test estimator gradient's validation""" + estimator = Estimator() + a = Parameter("a") + qc = QuantumCircuit(1) + qc.rx(a, 0) + gradient = grad(estimator) + param_list = [[np.pi / 4], [np.pi / 2]] + op = SparsePauliOp.from_list([("Z", 1)]) + with self.assertRaises(ValueError): + gradient.run([qc], [op], param_list) + with self.assertRaises(ValueError): + gradient.run([qc, qc], [op, op], param_list, parameters=[[a]]) + with self.assertRaises(ValueError): + gradient.run([qc, qc], [op], param_list, parameters=[[a]]) + with self.assertRaises(ValueError): + gradient.run([qc], [op], [[np.pi / 4, np.pi / 4]]) + + def test_spsa_gradient(self): + """Test the SPSA estimator gradient""" + estimator = Estimator() + with self.assertRaises(ValueError): + _ = SPSAEstimatorGradient(estimator, epsilon=-0.1) + a = Parameter("a") + b = Parameter("b") + qc = QuantumCircuit(2) + qc.rx(b, 0) + qc.rx(a, 1) + param_list = [[1, 1]] + correct_results = [[-0.84147098, 0.84147098]] + op = SparsePauliOp.from_list([("ZI", 1)]) + gradient = SPSAEstimatorGradient(estimator, epsilon=1e-6, seed=123) + gradients = gradient.run([qc], [op], param_list).result().gradients + np.testing.assert_allclose(gradients, correct_results, atol=1e-3) + + # multi parameters + with self.subTest(msg="Multiple parameters"): + gradient = SPSAEstimatorGradient(estimator, epsilon=1e-6, seed=123) + param_list2 = [[1, 1], [1, 1], [3, 3]] + gradients2 = ( + gradient.run([qc] * 3, [op] * 3, param_list2, parameters=[None, [b], None]) + .result() + .gradients + ) + correct_results2 = [[-0.84147098, 0.84147098], [0.84147098], [-0.14112001, 0.14112001]] + for grad, correct in zip(gradients2, correct_results2): + np.testing.assert_allclose(grad, correct, atol=1e-3) + + # batch size + with self.subTest(msg="Batch size"): + correct_results = [[-0.84147098, 0.1682942]] + gradient = SPSAEstimatorGradient(estimator, epsilon=1e-6, batch_size=5, seed=123) + gradients = gradient.run([qc], [op], param_list).result().gradients + np.testing.assert_allclose(gradients, correct_results, atol=1e-3) + + # parameter order + with self.subTest(msg="The order of gradients"): + gradient = SPSAEstimatorGradient(estimator, epsilon=1e-6, seed=123) + c = Parameter("c") + qc = QuantumCircuit(1) + qc.rx(a, 0) + qc.rz(b, 0) + qc.rx(c, 0) + op = SparsePauliOp.from_list([("Z", 1)]) + param_list3 = [[np.pi / 4, np.pi / 2, np.pi / 3]] + param = [[a, b, c], [c, b, a], [a, c], [c, a]] + expected = [ + [-0.3535525, 0.3535525, 0.3535525], + [0.3535525, 0.3535525, -0.3535525], + [-0.3535525, 0.3535525], + [0.3535525, -0.3535525], + ] + for i, p in enumerate(param): # pylint: disable=invalid-name + gradient = SPSAEstimatorGradient(estimator, epsilon=1e-6, seed=123) + gradients = ( + gradient.run([qc], [op], param_list3, parameters=[p]).result().gradients[0] + ) + np.testing.assert_allclose(gradients, expected[i], atol=1e-3) + + @data( + ParamShiftEstimatorGradient, + LinCombEstimatorGradient, + SPSAEstimatorGradient, + ) + def test_options(self, grad): + """Test estimator gradient's run options""" + a = Parameter("a") + qc = QuantumCircuit(1) + qc.rx(a, 0) + op = SparsePauliOp.from_list([("Z", 1)]) + estimator = Estimator(options={"shots": 100}) + with self.subTest("estimator"): + if grad is SPSAEstimatorGradient: + gradient = grad(estimator, epsilon=1e-6) + else: + gradient = grad(estimator) + options = gradient.options + result = gradient.run([qc], [op], [[1]]).result() + self.assertEqual(result.options.get("shots"), 100) + self.assertEqual(options.get("shots"), 100) + + with self.subTest("gradient init"): + if grad is SPSAEstimatorGradient: + gradient = grad(estimator, epsilon=1e-6, options={"shots": 200}) + else: + gradient = grad(estimator, options={"shots": 200}) + options = gradient.options + result = gradient.run([qc], [op], [[1]]).result() + self.assertEqual(result.options.get("shots"), 200) + self.assertEqual(options.get("shots"), 200) + + with self.subTest("gradient update"): + if grad is SPSAEstimatorGradient: + gradient = grad(estimator, epsilon=1e-6, options={"shots": 200}) + else: + gradient = grad(estimator, options={"shots": 200}) + gradient.update_default_options(shots=100) + options = gradient.options + result = gradient.run([qc], [op], [[1]]).result() + self.assertEqual(result.options.get("shots"), 100) + self.assertEqual(options.get("shots"), 100) + + with self.subTest("gradient run"): + if grad is SPSAEstimatorGradient: + gradient = grad(estimator, epsilon=1e-6, options={"shots": 200}) + else: + gradient = grad(estimator, options={"shots": 200}) + options = gradient.options + result = gradient.run([qc], [op], [[1]], shots=300).result() + self.assertEqual(result.options.get("shots"), 300) + # Only default + estimator options. Not run. + self.assertEqual(options.get("shots"), 200) + + @data( + ParamShiftEstimatorGradient, + LinCombEstimatorGradient, + SPSAEstimatorGradient, + ) + def test_operations_preserved(self, gradient_cls): + """Test non-parameterized instructions are preserved and not unrolled.""" + x = Parameter("x") + circuit = QuantumCircuit(2) + circuit.initialize([0.5, 0.5, 0.5, 0.5]) # this should remain as initialize + circuit.crx(x, 0, 1) # this should get unrolled + + values = [np.pi / 2] + expect = -1 / (2 * np.sqrt(2)) + + observable = SparsePauliOp(["XX"]) + + ops = [] + + def operations_callback(op): + ops.append(op) + + estimator = LoggingEstimator(operations_callback=operations_callback) + + if gradient_cls in [SPSAEstimatorGradient]: + gradient = gradient_cls(estimator, epsilon=0.01) + else: + gradient = gradient_cls(estimator) + + job = gradient.run([circuit], [observable], [values]) + result = job.result() + + with self.subTest(msg="assert initialize is preserved"): + self.assertTrue(all("initialize" in ops_i[0].keys() for ops_i in ops)) + + with self.subTest(msg="assert result is correct"): + self.assertAlmostEqual(result.gradients[0].item(), expect, places=5) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/gradients/test_qfi.py b/test/gradients/test_qfi.py new file mode 100644 index 000000000..b34fa59f5 --- /dev/null +++ b/test/gradients/test_qfi.py @@ -0,0 +1,151 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +# ============================================================================= + +"""Test QFI.""" + +import unittest +from test import QiskitAlgorithmsTestCase + +from ddt import ddt, data +import numpy as np + +from qiskit import QuantumCircuit +from qiskit.circuit import Parameter +from qiskit.circuit.parametervector import ParameterVector +from qiskit.primitives import Estimator + +from qiskit_machine_learning.gradients import LinCombQGT, QFI, DerivativeType + + +@ddt +class TestQFI(QiskitAlgorithmsTestCase): + """Test QFI""" + + def setUp(self): + super().setUp() + self.estimator = Estimator() + self.lcu_qgt = LinCombQGT(self.estimator, derivative_type=DerivativeType.REAL) + + def test_qfi(self): + """Test if the quantum fisher information calculation is correct for a simple test case. + QFI = [[1, 0], [0, 1]] - [[0, 0], [0, cos^2(a)]] + """ + # create the circuit + a, b = Parameter("a"), Parameter("b") + qc = QuantumCircuit(1) + qc.h(0) + qc.rz(a, 0) + qc.rx(b, 0) + + param_list = [[np.pi / 4, 0.1], [np.pi, 0.1], [np.pi / 2, 0.1]] + correct_values = [[[1, 0], [0, 0.5]], [[1, 0], [0, 0]], [[1, 0], [0, 1]]] + + qfi = QFI(self.lcu_qgt) + for i, param in enumerate(param_list): + qfis = qfi.run([qc], [param]).result().qfis + np.testing.assert_allclose(qfis[0], correct_values[i], atol=1e-3) + + def test_qfi_phase_fix(self): + """Test the phase-fix argument in the QFI calculation""" + # create the circuit + a, b = Parameter("a"), Parameter("b") + qc = QuantumCircuit(1) + qc.h(0) + qc.rz(a, 0) + qc.rx(b, 0) + + param = [np.pi / 4, 0.1] + # test for different values + correct_values = [[1, 0], [0, 1]] + qgt = LinCombQGT(self.estimator, phase_fix=False) + qfi = QFI(qgt) + qfis = qfi.run([qc], [param]).result().qfis + np.testing.assert_allclose(qfis[0], correct_values, atol=1e-3) + + @data("lcu") + def test_qfi_maxcut(self, qgt_kind): # pylint: disable=unused-argument + """Test the QFI for a simple MaxCut problem. + + This is interesting because it contains the same parameters in different gates. + """ + # create maxcut circuit for the hamiltonian + # H = (I ^ I ^ Z ^ Z) + (I ^ Z ^ I ^ Z) + (Z ^ I ^ I ^ Z) + (I ^ Z ^ Z ^ I) + + x = ParameterVector("x", 2) + ansatz = QuantumCircuit(4) + + # initial hadamard layer + ansatz.h(ansatz.qubits) + + # e^{iZZ} layers + def expiz(qubit0, qubit1): + ansatz.cx(qubit0, qubit1) + ansatz.rz(2 * x[0], qubit1) + ansatz.cx(qubit0, qubit1) + + expiz(2, 1) + expiz(3, 0) + expiz(2, 0) + expiz(1, 0) + + # mixer layer with RX gates + for i in range(ansatz.num_qubits): + ansatz.rx(2 * x[1], i) + + reference = np.array([[16.0, -5.551], [-5.551, 18.497]]) + param = [0.4, 0.69] + + qgt = self.lcu_qgt + qfi = QFI(qgt) + qfi_result = qfi.run([ansatz], [param]).result().qfis + np.testing.assert_array_almost_equal(qfi_result[0], reference, decimal=3) + + def test_options(self): + """Test QFI's options""" + a = Parameter("a") + qc = QuantumCircuit(1) + qc.rx(a, 0) + qgt = LinCombQGT(estimator=self.estimator, options={"shots": 100}) + + with self.subTest("QGT"): + qfi = QFI(qgt=qgt) + options = qfi.options + result = qfi.run([qc], [[1]]).result() + self.assertEqual(result.options.get("shots"), 100) + self.assertEqual(options.get("shots"), 100) + + with self.subTest("QFI init"): + qfi = QFI(qgt=qgt, options={"shots": 200}) + result = qfi.run([qc], [[1]]).result() + options = qfi.options + self.assertEqual(result.options.get("shots"), 200) + self.assertEqual(options.get("shots"), 200) + + with self.subTest("QFI update"): + qfi = QFI(qgt, options={"shots": 200}) + qfi.update_default_options(shots=100) + options = qfi.options + result = qfi.run([qc], [[1]]).result() + self.assertEqual(result.options.get("shots"), 100) + self.assertEqual(options.get("shots"), 100) + + with self.subTest("QFI run"): + qfi = QFI(qgt=qgt, options={"shots": 200}) + result = qfi.run([qc], [[0]], shots=300).result() + options = qfi.options + self.assertEqual(result.options.get("shots"), 300) + self.assertEqual(options.get("shots"), 200) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/gradients/test_qgt.py b/test/gradients/test_qgt.py new file mode 100644 index 000000000..7fa6c48c3 --- /dev/null +++ b/test/gradients/test_qgt.py @@ -0,0 +1,310 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +# ============================================================================= + +"""Test QGT.""" + +import unittest +from test import QiskitAlgorithmsTestCase + +from ddt import ddt, data +import numpy as np + +from qiskit import QuantumCircuit +from qiskit.circuit import Parameter +from qiskit.circuit.library import RealAmplitudes +from qiskit.primitives import Estimator + +from qiskit_machine_learning.gradients import DerivativeType, LinCombQGT + +from .logging_primitives import LoggingEstimator + + +@ddt +class TestQGT(QiskitAlgorithmsTestCase): + """Test QGT""" + + def setUp(self): + super().setUp() + self.estimator = Estimator() + + @data(LinCombQGT) + def test_qgt_derivative_type(self, qgt_type): + """Test QGT derivative_type""" + args = (self.estimator,) + qgt = qgt_type(*args, derivative_type=DerivativeType.REAL) + + a, b = Parameter("a"), Parameter("b") + qc = QuantumCircuit(1) + qc.h(0) + qc.rz(a, 0) + qc.rx(b, 0) + + param_list = [[np.pi / 4, 0], [np.pi / 2, np.pi / 4]] + correct_values = [ + np.array([[1, 0.707106781j], [-0.707106781j, 0.5]]) / 4, + np.array([[1, 1j], [-1j, 1]]) / 4, + ] + + # test real derivative + with self.subTest("Test with DerivativeType.REAL"): + qgt.derivative_type = DerivativeType.REAL + for i, param in enumerate(param_list): + qgt_result = qgt.run([qc], [param]).result().qgts + np.testing.assert_allclose(qgt_result[0], correct_values[i].real, atol=1e-3) + + # test imaginary derivative + with self.subTest("Test with DerivativeType.IMAG"): + qgt.derivative_type = DerivativeType.IMAG + for i, param in enumerate(param_list): + qgt_result = qgt.run([qc], [param]).result().qgts + np.testing.assert_allclose(qgt_result[0], correct_values[i].imag, atol=1e-3) + + # test real + imaginary derivative + with self.subTest("Test with DerivativeType.COMPLEX"): + qgt.derivative_type = DerivativeType.COMPLEX + for i, param in enumerate(param_list): + qgt_result = qgt.run([qc], [param]).result().qgts + np.testing.assert_allclose(qgt_result[0], correct_values[i], atol=1e-3) + + @data(LinCombQGT) + def test_qgt_phase_fix(self, qgt_type): + """Test the phase-fix argument in a QGT calculation""" + args = (self.estimator,) + qgt = qgt_type(*args, phase_fix=False) + + # create the circuit + a, b = Parameter("a"), Parameter("b") + qc = QuantumCircuit(1) + qc.h(0) + qc.rz(a, 0) + qc.rx(b, 0) + + param_list = [[np.pi / 4, 0], [np.pi / 2, np.pi / 4]] + correct_values = [ + np.array([[1, 0.707106781j], [-0.707106781j, 1]]) / 4, + np.array([[1, 1j], [-1j, 1]]) / 4, + ] + + # test real derivative + with self.subTest("Test phase fix with DerivativeType.REAL"): + qgt.derivative_type = DerivativeType.REAL + for i, param in enumerate(param_list): + qgt_result = qgt.run([qc], [param]).result().qgts + np.testing.assert_allclose(qgt_result[0], correct_values[i].real, atol=1e-3) + + # test imaginary derivative + with self.subTest("Test phase fix with DerivativeType.IMAG"): + qgt.derivative_type = DerivativeType.IMAG + for i, param in enumerate(param_list): + qgt_result = qgt.run([qc], [param]).result().qgts + np.testing.assert_allclose(qgt_result[0], correct_values[i].imag, atol=1e-3) + + # test real + imaginary derivative + with self.subTest("Test phase fix with DerivativeType.COMPLEX"): + qgt.derivative_type = DerivativeType.COMPLEX + for i, param in enumerate(param_list): + qgt_result = qgt.run([qc], [param]).result().qgts + np.testing.assert_allclose(qgt_result[0], correct_values[i], atol=1e-3) + + @data(LinCombQGT) + def test_qgt_coefficients(self, qgt_type): + """Test the derivative option of QGT""" + args = (self.estimator,) + qgt = qgt_type(*args, derivative_type=DerivativeType.REAL) + + qc = RealAmplitudes(num_qubits=2, reps=1) + qc.rz(qc.parameters[0].exp() + 2 * qc.parameters[1], 0) + qc.rx(3.0 * qc.parameters[2] + qc.parameters[3].sin(), 1) + + # test imaginary derivative + param_list = [ + [np.pi / 4 for param in qc.parameters], + [np.pi / 2 for param in qc.parameters], + ] + correct_values = ( + np.array( + [ + [ + [5.707309, 4.2924833, 1.5295868, 0.1938604], + [4.2924833, 4.9142136, 0.75, 0.8838835], + [1.5295868, 0.75, 3.4430195, 0.0758252], + [0.1938604, 0.8838835, 0.0758252, 1.1357233], + ], + [ + [1.0, 0.0, 1.0, 0.0], + [0.0, 1.0, 0.0, 0.0], + [1.0, 0.0, 10.0, -0.0], + [0.0, 0.0, -0.0, 1.0], + ], + ] + ) + / 4 + ) + for i, param in enumerate(param_list): + qgt_result = qgt.run([qc], [param]).result().qgts + np.testing.assert_allclose(qgt_result[0], correct_values[i], atol=1e-3) + + @data(LinCombQGT) + def test_qgt_parameters(self, qgt_type): + """Test the QGT with specified parameters""" + args = (self.estimator,) + qgt = qgt_type(*args, derivative_type=DerivativeType.REAL) + + a = Parameter("a") + b = Parameter("b") + qc = QuantumCircuit(1) + qc.rx(a, 0) + qc.ry(b, 0) + param_values = [np.pi / 4, np.pi / 4] + qgt_result = qgt.run([qc], [param_values], [[a]]).result().qgts + np.testing.assert_allclose(qgt_result[0], [[1 / 4]], atol=1e-3) + + with self.subTest("Test with different parameter orders"): + c = Parameter("c") + qc2 = QuantumCircuit(1) + qc2.rx(a, 0) + qc2.rz(b, 0) + qc2.rx(c, 0) + param_values = [np.pi / 4, np.pi / 4, np.pi / 4] + params = [[a, b, c], [c, b, a], [a, c], [b, a]] + expected = [ + np.array( + [ + [0.25, 0.0, 0.1767767], + [0.0, 0.125, -0.08838835], + [0.1767767, -0.08838835, 0.1875], + ] + ), + np.array( + [ + [0.1875, -0.08838835, 0.1767767], + [-0.08838835, 0.125, 0.0], + [0.1767767, 0.0, 0.25], + ] + ), + np.array([[0.25, 0.1767767], [0.1767767, 0.1875]]), + np.array([[0.125, 0.0], [0.0, 0.25]]), + ] + for i, param in enumerate(params): + qgt_result = qgt.run([qc2], [param_values], [param]).result().qgts + np.testing.assert_allclose(qgt_result[0], expected[i], atol=1e-3) + + @data(LinCombQGT) + def test_qgt_multi_arguments(self, qgt_type): + """Test the QGT for multiple arguments""" + args = (self.estimator,) + qgt = qgt_type(*args, derivative_type=DerivativeType.REAL) + + a = Parameter("a") + b = Parameter("b") + qc = QuantumCircuit(1) + qc.rx(a, 0) + qc.ry(b, 0) + qc2 = QuantumCircuit(1) + qc2.rx(a, 0) + qc2.ry(b, 0) + + param_list = [[np.pi / 4], [np.pi / 2]] + correct_values = [[[1 / 4]], [[1 / 4, 0], [0, 0]]] + param_list = [[np.pi / 4, np.pi / 4], [np.pi / 2, np.pi / 2]] + qgt_results = qgt.run([qc, qc2], param_list, [[a], None]).result().qgts + for i, _ in enumerate(param_list): + np.testing.assert_allclose(qgt_results[i], correct_values[i], atol=1e-3) + + @data(LinCombQGT) + def test_qgt_validation(self, qgt_type): + """Test estimator QGT's validation""" + args = (self.estimator,) + qgt = qgt_type(*args) + + a = Parameter("a") + qc = QuantumCircuit(1) + qc.rx(a, 0) + parameter_values = [[np.pi / 4]] + with self.subTest("assert number of circuits does not match"): + with self.assertRaises(ValueError): + qgt.run([qc, qc], parameter_values) + with self.subTest("assert number of parameter values does not match"): + with self.assertRaises(ValueError): + qgt.run([qc], [[np.pi / 4], [np.pi / 2]]) + with self.subTest("assert number of parameters does not match"): + with self.assertRaises(ValueError): + qgt.run([qc], parameter_values, parameters=[[a], [a]]) + + def test_options(self): + """Test QGT's options""" + a = Parameter("a") + qc = QuantumCircuit(1) + qc.rx(a, 0) + estimator = Estimator(options={"shots": 100}) + + with self.subTest("estimator"): + qgt = LinCombQGT(estimator) + options = qgt.options + result = qgt.run([qc], [[1]]).result() + self.assertEqual(result.options.get("shots"), 100) + self.assertEqual(options.get("shots"), 100) + + with self.subTest("QGT init"): + qgt = LinCombQGT(estimator, options={"shots": 200}) + result = qgt.run([qc], [[1]]).result() + options = qgt.options + self.assertEqual(result.options.get("shots"), 200) + self.assertEqual(options.get("shots"), 200) + + with self.subTest("QGT update"): + qgt = LinCombQGT(estimator, options={"shots": 200}) + qgt.update_default_options(shots=100) + options = qgt.options + result = qgt.run([qc], [[1]]).result() + self.assertEqual(result.options.get("shots"), 100) + self.assertEqual(options.get("shots"), 100) + + with self.subTest("QGT run"): + qgt = LinCombQGT(estimator, options={"shots": 200}) + result = qgt.run([qc], [[0]], shots=300).result() + options = qgt.options + self.assertEqual(result.options.get("shots"), 300) + self.assertEqual(options.get("shots"), 200) + + def test_operations_preserved(self): + """Test non-parameterized instructions are preserved and not unrolled.""" + x, y = Parameter("x"), Parameter("y") + circuit = QuantumCircuit(2) + circuit.initialize([0.5, 0.5, 0.5, 0.5]) # this should remain as initialize + circuit.crx(x, 0, 1) # this should get unrolled + circuit.ry(y, 0) + + values = [np.pi / 2, np.pi] + expect = np.diag([0.25, 0.5]) / 4 + + ops = [] + + def operations_callback(op): + ops.append(op) + + estimator = LoggingEstimator(operations_callback=operations_callback) + qgt = LinCombQGT(estimator, derivative_type=DerivativeType.REAL) + + job = qgt.run([circuit], [values]) + result = job.result() + + with self.subTest(msg="assert initialize is preserved"): + self.assertTrue(all("initialize" in ops_i[0].keys() for ops_i in ops)) + + with self.subTest(msg="assert result is correct"): + np.testing.assert_allclose(result.qgts[0], expect, atol=1e-5) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/gradients/test_sampler_gradient.py b/test/gradients/test_sampler_gradient.py new file mode 100644 index 000000000..d586dbe4b --- /dev/null +++ b/test/gradients/test_sampler_gradient.py @@ -0,0 +1,644 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2019, 2024. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +# ============================================================================= + +"""Test Sampler Gradients""" + +import unittest +from test import QiskitAlgorithmsTestCase +from typing import List + +import numpy as np +from ddt import ddt, data + +from qiskit import QuantumCircuit +from qiskit.circuit import Parameter +from qiskit.circuit.library import EfficientSU2, RealAmplitudes +from qiskit.circuit.library.standard_gates import RXXGate +from qiskit.primitives import Sampler +from qiskit.result import QuasiDistribution + +from qiskit_machine_learning.gradients import ( + LinCombSamplerGradient, + ParamShiftSamplerGradient, + SPSASamplerGradient, +) + +from .logging_primitives import LoggingSampler + +gradient_factories = [ + ParamShiftSamplerGradient, + LinCombSamplerGradient, +] + + +@ddt +class TestSamplerGradient(QiskitAlgorithmsTestCase): + """Test Sampler Gradient""" + + @data(*gradient_factories) + def test_single_circuit(self, grad): + """Test the sampler gradient for a single circuit""" + sampler = Sampler() + a = Parameter("a") + qc = QuantumCircuit(1) + qc.h(0) + qc.p(a, 0) + qc.h(0) + qc.measure_all() + gradient = grad(sampler) + param_list = [[np.pi / 4], [0], [np.pi / 2]] + expected = [ + [{0: -0.5 / np.sqrt(2), 1: 0.5 / np.sqrt(2)}], + [{0: 0, 1: 0}], + [{0: -0.499999, 1: 0.499999}], + ] + for i, param in enumerate(param_list): + gradients = gradient.run([qc], [param]).result().gradients[0] + array1 = _quasi2array(gradients, num_qubits=1) + array2 = _quasi2array(expected[i], num_qubits=1) + np.testing.assert_allclose(array1, array2, atol=1e-3) + + @data(*gradient_factories) + def test_gradient_p(self, grad): + """Test the sampler gradient for p""" + sampler = Sampler() + a = Parameter("a") + qc = QuantumCircuit(1) + qc.h(0) + qc.p(a, 0) + qc.h(0) + qc.measure_all() + gradient = grad(sampler) + param_list = [[np.pi / 4], [0], [np.pi / 2]] + expected = [ + [{0: -0.5 / np.sqrt(2), 1: 0.5 / np.sqrt(2)}], + [{0: 0, 1: 0}], + [{0: -0.499999, 1: 0.499999}], + ] + for i, param in enumerate(param_list): + gradients = gradient.run([qc], [param]).result().gradients[0] + array1 = _quasi2array(gradients, num_qubits=1) + array2 = _quasi2array(expected[i], num_qubits=1) + np.testing.assert_allclose(array1, array2, atol=1e-3) + + @data(*gradient_factories) + def test_gradient_u(self, grad): + """Test the sampler gradient for u""" + sampler = Sampler() + a = Parameter("a") + b = Parameter("b") + c = Parameter("c") + qc = QuantumCircuit(1) + qc.h(0) + qc.u(a, b, c, 0) + qc.h(0) + qc.measure_all() + gradient = grad(sampler) + param_list = [[np.pi / 4, 0, 0], [np.pi / 4, np.pi / 4, np.pi / 4]] + expected = [ + [{0: -0.5 / np.sqrt(2), 1: 0.5 / np.sqrt(2)}, {0: 0, 1: 0}, {0: 0, 1: 0}], + [{0: -0.176777, 1: 0.176777}, {0: -0.426777, 1: 0.426777}, {0: -0.426777, 1: 0.426777}], + ] + for i, param in enumerate(param_list): + gradients = gradient.run([qc], [param]).result().gradients[0] + array1 = _quasi2array(gradients, num_qubits=1) + array2 = _quasi2array(expected[i], num_qubits=1) + np.testing.assert_allclose(array1, array2, atol=1e-3) + + @data(*gradient_factories) + def test_gradient_efficient_su2(self, grad): + """Test the sampler gradient for EfficientSU2""" + sampler = Sampler() + qc = EfficientSU2(2, reps=1) + qc.measure_all() + gradient = grad(sampler) + param_list = [ + [np.pi / 4 for param in qc.parameters], + [np.pi / 2 for param in qc.parameters], + ] + expected = [ + [ + { + 0: -0.11963834764831836, + 1: -0.05713834764831845, + 2: -0.21875000000000003, + 3: 0.39552669529663675, + }, + { + 0: -0.32230339059327373, + 1: -0.031250000000000014, + 2: 0.2339150429449554, + 3: 0.11963834764831843, + }, + { + 0: 0.012944173824159189, + 1: -0.01294417382415923, + 2: 0.07544417382415919, + 3: -0.07544417382415919, + }, + { + 0: 0.2080266952966367, + 1: -0.03125000000000002, + 2: -0.11963834764831842, + 3: -0.057138347648318405, + }, + { + 0: -0.11963834764831838, + 1: 0.11963834764831838, + 2: -0.21875000000000003, + 3: 0.21875, + }, + { + 0: -0.2781092167691146, + 1: -0.0754441738241592, + 2: 0.27810921676911443, + 3: 0.07544417382415924, + }, + {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0}, + {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0}, + ], + [ + { + 0: -4.163336342344337e-17, + 1: 2.7755575615628914e-17, + 2: -4.163336342344337e-17, + 3: 0.0, + }, + {0: 0.0, 1: -1.3877787807814457e-17, 2: 4.163336342344337e-17, 3: 0.0}, + { + 0: -0.24999999999999994, + 1: 0.24999999999999994, + 2: 0.24999999999999994, + 3: -0.24999999999999994, + }, + { + 0: 0.24999999999999994, + 1: 0.24999999999999994, + 2: -0.24999999999999994, + 3: -0.24999999999999994, + }, + { + 0: -4.163336342344337e-17, + 1: 4.163336342344337e-17, + 2: -4.163336342344337e-17, + 3: 5.551115123125783e-17, + }, + { + 0: -0.24999999999999994, + 1: 0.24999999999999994, + 2: 0.24999999999999994, + 3: -0.24999999999999994, + }, + {0: 0.0, 1: 2.7755575615628914e-17, 2: 0.0, 3: 2.7755575615628914e-17}, + {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0}, + ], + ] + for i, param in enumerate(param_list): + gradients = gradient.run([qc], [param]).result().gradients[0] + array1 = _quasi2array(gradients, num_qubits=2) + array2 = _quasi2array(expected[i], num_qubits=2) + np.testing.assert_allclose(array1, array2, atol=1e-3) + + @data(*gradient_factories) + def test_gradient_2qubit_gate(self, grad): + """Test the sampler gradient for 2 qubit gates""" + sampler = Sampler() + for gate in [RXXGate]: + param_list = [[np.pi / 4], [np.pi / 2]] + correct_results = [ + [{0: -0.5 / np.sqrt(2), 1: 0, 2: 0, 3: 0.5 / np.sqrt(2)}], + [{0: -0.5, 1: 0, 2: 0, 3: 0.5}], + ] + for i, param in enumerate(param_list): + a = Parameter("a") + qc = QuantumCircuit(2) + qc.append(gate(a), [qc.qubits[0], qc.qubits[1]], []) + qc.measure_all() + gradient = grad(sampler) + gradients = gradient.run([qc], [param]).result().gradients[0] + array1 = _quasi2array(gradients, num_qubits=2) + array2 = _quasi2array(correct_results[i], num_qubits=2) + np.testing.assert_allclose(array1, array2, atol=1e-3) + + @data(*gradient_factories) + def test_gradient_parameter_coefficient(self, grad): + """Test the sampler gradient for parameter variables with coefficients""" + sampler = Sampler() + qc = RealAmplitudes(num_qubits=2, reps=1) + qc.rz(qc.parameters[0].exp() + 2 * qc.parameters[1], 0) + qc.rx(3.0 * qc.parameters[0] + qc.parameters[1].sin(), 1) + qc.u(qc.parameters[0], qc.parameters[1], qc.parameters[3], 1) + qc.p(2 * qc.parameters[0] + 1, 0) + qc.rxx(qc.parameters[0] + 2, 0, 1) + qc.measure_all() + gradient = grad(sampler) + param_list = [[np.pi / 4 for _ in qc.parameters], [np.pi / 2 for _ in qc.parameters]] + correct_results = [ + [ + { + 0: 0.30014831912265927, + 1: -0.6634809704357856, + 2: 0.343589357193753, + 3: 0.019743294119373426, + }, + { + 0: 0.16470607453981906, + 1: -0.40996282450610577, + 2: 0.08791803062881773, + 3: 0.15733871933746948, + }, + { + 0: 0.27036068339663866, + 1: -0.273790986018701, + 2: 0.12752010079553433, + 3: -0.12408979817347202, + }, + { + 0: -0.2098616294167757, + 1: -0.2515823946449894, + 2: 0.21929102305386305, + 3: 0.24215300100790207, + }, + ], + [ + { + 0: -1.844810060881004, + 1: 0.04620532700836027, + 2: 1.6367366426074323, + 3: 0.16186809126521057, + }, + { + 0: 0.07296073407769421, + 1: -0.021774869186331716, + 2: 0.02177486918633173, + 3: -0.07296073407769456, + }, + { + 0: -0.07794369186049102, + 1: -0.07794369186049122, + 2: 0.07794369186049117, + 3: 0.07794369186049112, + }, + { + 0: 0.0, + 1: 0.0, + 2: 0.0, + 3: 0.0, + }, + ], + ] + + for i, param in enumerate(param_list): + gradients = gradient.run([qc], [param]).result().gradients[0] + array1 = _quasi2array(gradients, num_qubits=2) + array2 = _quasi2array(correct_results[i], num_qubits=2) + np.testing.assert_allclose(array1, array2, atol=1e-3) + + @data(*gradient_factories) + def test_gradient_parameters(self, grad): + """Test the sampler gradient for parameters""" + sampler = Sampler() + a = Parameter("a") + b = Parameter("b") + qc = QuantumCircuit(1) + qc.rx(a, 0) + qc.rz(b, 0) + qc.measure_all() + gradient = grad(sampler) + param_list = [[np.pi / 4, np.pi / 2]] + expected = [ + [{0: -0.5 / np.sqrt(2), 1: 0.5 / np.sqrt(2)}], + ] + for i, param in enumerate(param_list): + gradients = gradient.run([qc], [param], parameters=[[a]]).result().gradients[0] + array1 = _quasi2array(gradients, num_qubits=1) + array2 = _quasi2array(expected[i], num_qubits=1) + np.testing.assert_allclose(array1, array2, atol=1e-3) + + # parameter order + with self.subTest(msg="The order of gradients"): + c = Parameter("c") + qc = QuantumCircuit(1) + qc.rx(a, 0) + qc.rz(b, 0) + qc.rx(c, 0) + qc.measure_all() + param_values = [[np.pi / 4, np.pi / 2, np.pi / 3]] + params = [[a, b, c], [c, b, a], [a, c], [c, a]] + expected = [ + [ + {0: -0.17677666583387008, 1: 0.17677666583378482}, + {0: 0.3061861668168149, 1: -0.3061861668167012}, + {0: -0.3061861668168149, 1: 0.30618616681678645}, + ], + [ + {0: -0.3061861668168149, 1: 0.30618616681678645}, + {0: 0.3061861668168149, 1: -0.3061861668167012}, + {0: -0.17677666583387008, 1: 0.17677666583378482}, + ], + [ + {0: -0.17677666583387008, 1: 0.17677666583378482}, + {0: -0.3061861668168149, 1: 0.30618616681678645}, + ], + [ + {0: -0.3061861668168149, 1: 0.30618616681678645}, + {0: -0.17677666583387008, 1: 0.17677666583378482}, + ], + ] + for i, p in enumerate(params): # pylint: disable=invalid-name + gradients = gradient.run([qc], param_values, parameters=[p]).result().gradients[0] + array1 = _quasi2array(gradients, num_qubits=1) + array2 = _quasi2array(expected[i], num_qubits=1) + np.testing.assert_allclose(array1, array2, atol=1e-3) + + @data(*gradient_factories) + def test_gradient_multi_arguments(self, grad): + """Test the sampler gradient for multiple arguments""" + sampler = Sampler() + a = Parameter("a") + b = Parameter("b") + qc = QuantumCircuit(1) + qc.rx(a, 0) + qc.measure_all() + qc2 = QuantumCircuit(1) + qc2.rx(b, 0) + qc2.measure_all() + gradient = grad(sampler) + param_list = [[np.pi / 4], [np.pi / 2]] + correct_results = [ + [{0: -0.5 / np.sqrt(2), 1: 0.5 / np.sqrt(2)}], + [{0: -0.499999, 1: 0.499999}], + ] + gradients = gradient.run([qc, qc2], param_list).result().gradients + for i, q_dists in enumerate(gradients): + array1 = _quasi2array(q_dists, num_qubits=1) + array2 = _quasi2array(correct_results[i], num_qubits=1) + np.testing.assert_allclose(array1, array2, atol=1e-3) + + # parameters + with self.subTest(msg="Different parameters"): + c = Parameter("c") + qc3 = QuantumCircuit(1) + qc3.rx(c, 0) + qc3.ry(a, 0) + qc3.measure_all() + param_list2 = [[np.pi / 4], [np.pi / 4, np.pi / 4], [np.pi / 4, np.pi / 4]] + gradients = ( + gradient.run([qc, qc3, qc3], param_list2, parameters=[[a], [c], None]) + .result() + .gradients + ) + correct_results = [ + [{0: -0.5 / np.sqrt(2), 1: 0.5 / np.sqrt(2)}], + [{0: -0.25, 1: 0.25}], + [{0: -0.25, 1: 0.25}, {0: -0.25, 1: 0.25}], + ] + for i, q_dists in enumerate(gradients): + array1 = _quasi2array(q_dists, num_qubits=1) + array2 = _quasi2array(correct_results[i], num_qubits=1) + np.testing.assert_allclose(array1, array2, atol=1e-3) + + @data(*gradient_factories) + def test_gradient_validation(self, grad): + """Test sampler gradient's validation""" + sampler = Sampler() + a = Parameter("a") + qc = QuantumCircuit(1) + qc.rx(a, 0) + qc.measure_all() + gradient = grad(sampler) + param_list = [[np.pi / 4], [np.pi / 2]] + with self.assertRaises(ValueError): + gradient.run([qc], param_list) + with self.assertRaises(ValueError): + gradient.run([qc, qc], param_list, parameters=[[a]]) + with self.assertRaises(ValueError): + gradient.run([qc], [[np.pi / 4, np.pi / 4]]) + + def test_spsa_gradient(self): + """Test the SPSA sampler gradient""" + sampler = Sampler() + with self.assertRaises(ValueError): + _ = SPSASamplerGradient(sampler, epsilon=-0.1) + + a = Parameter("a") + b = Parameter("b") + c = Parameter("c") + qc = QuantumCircuit(2) + qc.rx(b, 0) + qc.rx(a, 1) + qc.measure_all() + param_list = [[1, 2]] + correct_results = [ + [ + {0: 0.2273244, 1: -0.6480598, 2: 0.2273244, 3: 0.1934111}, + {0: -0.2273244, 1: 0.6480598, 2: -0.2273244, 3: -0.1934111}, + ], + ] + gradient = SPSASamplerGradient(sampler, epsilon=1e-6, seed=123) + for i, param in enumerate(param_list): + gradients = gradient.run([qc], [param]).result().gradients[0] + array1 = _quasi2array(gradients, num_qubits=2) + array2 = _quasi2array(correct_results[i], num_qubits=2) + np.testing.assert_allclose(array1, array2, atol=1e-3) + + # multi parameters + with self.subTest(msg="Multiple parameters"): + param_list2 = [[1, 2], [1, 2], [3, 4]] + correct_results2 = [ + [ + {0: 0.2273244, 1: -0.6480598, 2: 0.2273244, 3: 0.1934111}, + {0: -0.2273244, 1: 0.6480598, 2: -0.2273244, 3: -0.1934111}, + ], + [ + {0: -0.2273244, 1: 0.6480598, 2: -0.2273244, 3: -0.1934111}, + ], + [ + {0: -0.0141129, 1: -0.0564471, 2: -0.3642884, 3: 0.4348484}, + {0: 0.0141129, 1: 0.0564471, 2: 0.3642884, 3: -0.4348484}, + ], + ] + gradient = SPSASamplerGradient(sampler, epsilon=1e-6, seed=123) + gradients = ( + gradient.run([qc] * 3, param_list2, parameters=[None, [b], None]).result().gradients + ) + for i, result in enumerate(gradients): + array1 = _quasi2array(result, num_qubits=2) + array2 = _quasi2array(correct_results2[i], num_qubits=2) + np.testing.assert_allclose(array1, array2, atol=1e-3) + + # batch size + with self.subTest(msg="Batch size"): + param_list = [[1, 1]] + gradient = SPSASamplerGradient(sampler, epsilon=1e-6, batch_size=4, seed=123) + gradients = gradient.run([qc], param_list).result().gradients + correct_results3 = [ + [ + { + 0: -0.1620149622932887, + 1: -0.25872053011771756, + 2: 0.3723827084675668, + 3: 0.04835278392088804, + }, + { + 0: -0.1620149622932887, + 1: 0.3723827084675668, + 2: -0.25872053011771756, + 3: 0.04835278392088804, + }, + ] + ] + for i, q_dists in enumerate(gradients): + array1 = _quasi2array(q_dists, num_qubits=2) + array2 = _quasi2array(correct_results3[i], num_qubits=2) + np.testing.assert_allclose(array1, array2, atol=1e-3) + + # parameter order + with self.subTest(msg="The order of gradients"): + qc = QuantumCircuit(1) + qc.rx(a, 0) + qc.rz(b, 0) + qc.rx(c, 0) + qc.measure_all() + param_list = [[np.pi / 4, np.pi / 2, np.pi / 3]] + param = [[a, b, c], [c, b, a], [a, c], [c, a]] + correct_results = [ + [ + {0: -0.17677624757590138, 1: 0.17677624757590138}, + {0: 0.17677624757590138, 1: -0.17677624757590138}, + {0: 0.17677624757590138, 1: -0.17677624757590138}, + ], + [ + {0: 0.17677624757590138, 1: -0.17677624757590138}, + {0: 0.17677624757590138, 1: -0.17677624757590138}, + {0: -0.17677624757590138, 1: 0.17677624757590138}, + ], + [ + {0: -0.17677624757590138, 1: 0.17677624757590138}, + {0: 0.17677624757590138, 1: -0.17677624757590138}, + ], + [ + {0: 0.17677624757590138, 1: -0.17677624757590138}, + {0: -0.17677624757590138, 1: 0.17677624757590138}, + ], + ] + for i, p in enumerate(param): # pylint: disable=invalid-name + gradient = SPSASamplerGradient(sampler, epsilon=1e-6, seed=123) + gradients = gradient.run([qc], param_list, parameters=[p]).result().gradients[0] + array1 = _quasi2array(gradients, num_qubits=1) + array2 = _quasi2array(correct_results[i], num_qubits=1) + np.testing.assert_allclose(array1, array2, atol=1e-3) + + @data( + ParamShiftSamplerGradient, + LinCombSamplerGradient, + SPSASamplerGradient, + ) + def test_options(self, grad): + """Test sampler gradient's run options""" + a = Parameter("a") + qc = QuantumCircuit(1) + qc.rx(a, 0) + qc.measure_all() + sampler = Sampler(options={"shots": 100}) + with self.subTest("sampler"): + if grad is SPSASamplerGradient: + gradient = grad(sampler, epsilon=1e-6) + else: + gradient = grad(sampler) + options = gradient.options + result = gradient.run([qc], [[1]]).result() + self.assertEqual(result.options.get("shots"), 100) + self.assertEqual(options.get("shots"), 100) + + with self.subTest("gradient init"): + if grad is SPSASamplerGradient: + gradient = grad(sampler, epsilon=1e-6, options={"shots": 200}) + else: + gradient = grad(sampler, options={"shots": 200}) + options = gradient.options + result = gradient.run([qc], [[1]]).result() + self.assertEqual(result.options.get("shots"), 200) + self.assertEqual(options.get("shots"), 200) + + with self.subTest("gradient update"): + if grad is SPSASamplerGradient: + gradient = grad(sampler, epsilon=1e-6, options={"shots": 200}) + else: + gradient = grad(sampler, options={"shots": 200}) + gradient.update_default_options(shots=100) + options = gradient.options + result = gradient.run([qc], [[1]]).result() + self.assertEqual(result.options.get("shots"), 100) + self.assertEqual(options.get("shots"), 100) + + with self.subTest("gradient run"): + if grad is SPSASamplerGradient: + gradient = grad(sampler, epsilon=1e-6, options={"shots": 200}) + else: + gradient = grad(sampler, options={"shots": 200}) + options = gradient.options + result = gradient.run([qc], [[1]], shots=300).result() + self.assertEqual(result.options.get("shots"), 300) + # Only default + sampler options. Not run. + self.assertEqual(options.get("shots"), 200) + + @data( + ParamShiftSamplerGradient, + LinCombSamplerGradient, + SPSASamplerGradient, + ) + def test_operations_preserved(self, gradient_cls): + """Test non-parameterized instructions are preserved and not unrolled.""" + x = Parameter("x") + circuit = QuantumCircuit(2) + circuit.initialize(np.array([1, 1, 0, 0]) / np.sqrt(2)) # this should remain as initialize + circuit.crx(x, 0, 1) # this should get unrolled + circuit.measure_all() + + values = [np.pi / 2] + expect = [{0: 0, 1: -0.25, 2: 0, 3: 0.25}] + + ops = [] + + def operations_callback(op): + ops.append(op) + + sampler = LoggingSampler(operations_callback=operations_callback) + + if gradient_cls in [SPSASamplerGradient]: + gradient = gradient_cls(sampler, epsilon=0.01) + else: + gradient = gradient_cls(sampler) + + job = gradient.run([circuit], [values]) + result = job.result() + + with self.subTest(msg="assert initialize is preserved"): + self.assertTrue(all("initialize" in ops_i[0].keys() for ops_i in ops)) + + with self.subTest(msg="assert result is correct"): + array1 = _quasi2array(result.gradients[0], num_qubits=2) + array2 = _quasi2array(expect, num_qubits=2) + np.testing.assert_allclose(array1, array2, atol=1e-5) + + +def _quasi2array(quasis: List[QuasiDistribution], num_qubits: int) -> np.ndarray: + ret = np.zeros((len(quasis), 2**num_qubits)) + for i, quasi in enumerate(quasis): + ret[i, list(quasi.keys())] = list(quasi.values()) + return ret + + +if __name__ == "__main__": + unittest.main() diff --git a/test/kernels/algorithms/test_fidelity_qkernel_trainer.py b/test/kernels/algorithms/test_fidelity_qkernel_trainer.py index f0b626baa..dad548a03 100644 --- a/test/kernels/algorithms/test_fidelity_qkernel_trainer.py +++ b/test/kernels/algorithms/test_fidelity_qkernel_trainer.py @@ -20,12 +20,13 @@ from ddt import ddt, data import numpy as np +from scipy.optimize import minimize + from qiskit import QuantumCircuit from qiskit.circuit import Parameter, ParameterVector from qiskit.circuit.library import ZZFeatureMap -from qiskit_machine_learning.utils import algorithm_globals -from scipy.optimize import minimize +from qiskit_machine_learning.utils import algorithm_globals from qiskit_machine_learning.algorithms.classifiers import QSVC from qiskit_machine_learning.kernels import ( TrainableFidelityQuantumKernel, diff --git a/test/kernels/test_fidelity_qkernel.py b/test/kernels/test_fidelity_qkernel.py index 12c762c78..173f4a01f 100644 --- a/test/kernels/test_fidelity_qkernel.py +++ b/test/kernels/test_fidelity_qkernel.py @@ -22,10 +22,13 @@ import numpy as np from ddt import ddt, idata, unpack +from sklearn.svm import SVC + from qiskit import QuantumCircuit from qiskit.circuit import Parameter from qiskit.circuit.library import ZFeatureMap from qiskit.primitives import Sampler + from qiskit_machine_learning.algorithm_job import AlgorithmJob from qiskit_machine_learning.utils import algorithm_globals from qiskit_machine_learning.state_fidelities import ( @@ -33,8 +36,6 @@ BaseStateFidelity, StateFidelityResult, ) -from sklearn.svm import SVC - from qiskit_machine_learning.kernels import FidelityQuantumKernel diff --git a/test/optimizers/__init__.py b/test/optimizers/__init__.py new file mode 100644 index 000000000..d68c98527 --- /dev/null +++ b/test/optimizers/__init__.py @@ -0,0 +1,13 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2021, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Qiskit's algorithm optimizer tests.""" diff --git a/test/optimizers/test_gradient_descent.py b/test/optimizers/test_gradient_descent.py new file mode 100644 index 000000000..eada59d8a --- /dev/null +++ b/test/optimizers/test_gradient_descent.py @@ -0,0 +1,195 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2021, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Tests for the Gradient Descent optimizer.""" + +from test import QiskitAlgorithmsTestCase +import numpy as np +from qiskit.circuit.library import PauliTwoDesign +from qiskit.quantum_info import SparsePauliOp, Statevector + +from qiskit_machine_learning.optimizers import GradientDescent, GradientDescentState +from qiskit_machine_learning.optimizers.steppable_optimizer import TellData, AskData + + +class TestGradientDescent(QiskitAlgorithmsTestCase): + """Tests for the gradient descent optimizer.""" + + def setUp(self): + super().setUp() + np.random.seed(12) + self.initial_point = np.array([1, 1, 1, 1, 0]) + + def objective(self, x): + """Objective Function for the tests""" + return (np.linalg.norm(x) - 1) ** 2 + + def grad(self, x): + """Gradient of the objective function""" + return 2 * (np.linalg.norm(x) - 1) * x / np.linalg.norm(x) + + def test_pauli_two_design(self): + """Test standard gradient descent on the Pauli two-design example.""" + circuit = PauliTwoDesign(3, reps=3, seed=2) + parameters = list(circuit.parameters) + obs = SparsePauliOp("ZZI") # Z^Z^I + + initial_point = np.array( + [ + 0.1822308, + -0.27254251, + 0.83684425, + 0.86153976, + -0.7111668, + 0.82766631, + 0.97867993, + 0.46136964, + 2.27079901, + 0.13382699, + 0.29589915, + 0.64883193, + ] + ) + + def objective_pauli(x): + bound_circ = circuit.assign_parameters(dict(zip(parameters, x))) + return Statevector(bound_circ).expectation_value(obs).real + + optimizer = GradientDescent(maxiter=100, learning_rate=0.1, perturbation=0.1) + + result = optimizer.minimize(objective_pauli, x0=initial_point) + self.assertLess(result.fun, -0.95) # final loss + self.assertEqual(result.nfev, 1300) # function evaluations + + def test_callback(self): + """Test the callback.""" + + history = [] + + def callback(*args): + history.append(args) + + optimizer = GradientDescent(maxiter=1, callback=callback) + + _ = optimizer.minimize(self.objective, np.array([1, -1])) + + self.assertEqual(len(history), 1) + self.assertIsInstance(history[0][0], int) # nfevs + self.assertIsInstance(history[0][1], np.ndarray) # parameters + self.assertIsInstance(history[0][2], float) # function value + self.assertIsInstance(history[0][3], float) # norm of the gradient + + def test_minimize(self): + """Test setting the learning rate as iterator and minimizing the function.""" + + def learning_rate(): + power = 0.6 + constant_coeff = 0.1 + + def powerlaw(): + n = 0 + while True: + yield constant_coeff * (n**power) + n += 1 + + return powerlaw() + + optimizer = GradientDescent(maxiter=20, learning_rate=learning_rate) + result = optimizer.minimize(self.objective, self.initial_point, self.grad) + + self.assertLess(result.fun, 1e-5) + + def test_no_start(self): + """Tests that making a step without having started the optimizer raises an error.""" + optimizer = GradientDescent() + with self.assertRaises(AttributeError): + optimizer.step() + + def test_start(self): + """Tests if the start method initializes the state properly.""" + optimizer = GradientDescent() + self.assertIsNone(optimizer.state) + self.assertIsNone(optimizer.perturbation) + optimizer.start(x0=self.initial_point, fun=self.objective) + + test_state = GradientDescentState( + x=self.initial_point, + fun=self.objective, + jac=None, + nfev=0, + njev=0, + nit=0, + learning_rate=1, + stepsize=None, + ) + + self.assertEqual(test_state, optimizer.state) + + def test_ask(self): + """Test the ask method.""" + optimizer = GradientDescent() + optimizer.start(fun=self.objective, x0=self.initial_point) + + ask_data = optimizer.ask() + np.testing.assert_equal(ask_data.x_jac, self.initial_point) + self.assertIsNone(ask_data.x_fun) + + def test_evaluate(self): + """Test the evaluate method.""" + optimizer = GradientDescent(perturbation=1e-10) + optimizer.start(fun=self.objective, x0=self.initial_point) + ask_data = AskData(x_jac=self.initial_point) + tell_data = optimizer.evaluate(ask_data=ask_data) + np.testing.assert_almost_equal(tell_data.eval_jac, self.grad(self.initial_point), decimal=2) + + def test_tell(self): + """Test the tell method.""" + optimizer = GradientDescent(learning_rate=1.0) + optimizer.start(fun=self.objective, x0=self.initial_point) + ask_data = AskData(x_jac=self.initial_point) + tell_data = TellData(eval_jac=self.initial_point) + optimizer.tell(ask_data=ask_data, tell_data=tell_data) + np.testing.assert_equal(optimizer.state.x, np.zeros(optimizer.state.x.shape)) + + def test_continue_condition(self): + """Test if the continue condition is working properly.""" + optimizer = GradientDescent(tol=1) + optimizer.start(fun=self.objective, x0=self.initial_point) + self.assertTrue(optimizer.continue_condition()) + optimizer.state.stepsize = 0.1 + self.assertFalse(optimizer.continue_condition()) + optimizer.state.stepsize = 10 + optimizer.state.nit = 1000 + self.assertFalse(optimizer.continue_condition()) + + def test_step(self): + """Tests if performing one step yields the desired result.""" + optimizer = GradientDescent(learning_rate=1.0) + optimizer.start(fun=self.objective, jac=self.grad, x0=self.initial_point) + optimizer.step() + np.testing.assert_almost_equal( + optimizer.state.x, self.initial_point - self.grad(self.initial_point), 6 + ) + + def test_wrong_dimension_gradient(self): + """Tests if an error is raised when a gradient of the wrong dimension is passed.""" + + optimizer = GradientDescent(learning_rate=1.0) + optimizer.start(fun=self.objective, x0=self.initial_point) + ask_data = AskData(x_jac=self.initial_point) + tell_data = TellData(eval_jac=np.array([1.0, 5])) + with self.assertRaises(ValueError): + optimizer.tell(ask_data=ask_data, tell_data=tell_data) + + tell_data = TellData(eval_jac=np.array(1)) + with self.assertRaises(ValueError): + optimizer.tell(ask_data=ask_data, tell_data=tell_data) diff --git a/test/optimizers/test_optimizer_aqgd.py b/test/optimizers/test_optimizer_aqgd.py new file mode 100644 index 000000000..00cd67616 --- /dev/null +++ b/test/optimizers/test_optimizer_aqgd.py @@ -0,0 +1,75 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2019, 2024. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Test of AQGD optimizer""" + +import unittest +from test import QiskitAlgorithmsTestCase +import numpy as np +from ddt import ddt +from qiskit.primitives import Estimator +from qiskit.quantum_info import SparsePauliOp + +from qiskit_machine_learning import AlgorithmError +from qiskit_machine_learning.gradients import LinCombEstimatorGradient +from qiskit_machine_learning.optimizers import AQGD +from qiskit_machine_learning.utils import algorithm_globals + + +@ddt +class TestOptimizerAQGD(QiskitAlgorithmsTestCase): + """Test AQGD optimizer using RY for analytic gradient with VQE""" + + def setUp(self): + super().setUp() + algorithm_globals.random_seed = 50 + self.qubit_op = SparsePauliOp.from_list( + [ + ("II", -1.052373245772859), + ("IZ", 0.39793742484318045), + ("ZI", -0.39793742484318045), + ("ZZ", -0.01128010425623538), + ("XX", 0.18093119978423156), + ] + ) + self.estimator = Estimator() + self.gradient = LinCombEstimatorGradient(self.estimator) + + def test_raises_exception(self): + """tests that AQGD raises an exception when incorrect values are passed.""" + self.assertRaises(AlgorithmError, AQGD, maxiter=[1000], eta=[1.0, 0.5], momentum=[0.0, 0.5]) + + def test_max_grouped_evals_non_parallelizable(self): + """Tests max_grouped_evals for an objective function that cannot be parallelized""" + + # Define the objective function (toy example for functionality) + def quadratic_objective(x: np.ndarray) -> float: + # Check if only a single point as parameters is passed + if np.array(x).ndim != 1: + raise ValueError("The function expects a vector.") + + return x[0] ** 2 + x[1] ** 2 - 2 * x[0] * x[1] + + # Define initial point + initial_point = np.array([1, 2.23]) + # Test max_evals_grouped raises no error for max_evals_grouped=1 + aqgd = AQGD(maxiter=100, max_evals_grouped=1) + x_new = aqgd.minimize(quadratic_objective, initial_point).x + self.assertAlmostEqual(sum(np.round(x_new / max(x_new), 7)), 0) + # Test max_evals_grouped raises an error for max_evals_grouped=2 + aqgd.set_max_evals_grouped(2) + with self.assertRaises(ValueError): + aqgd.minimize(quadratic_objective, initial_point) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/optimizers/test_optimizers.py b/test/optimizers/test_optimizers.py new file mode 100644 index 000000000..344c3a4ef --- /dev/null +++ b/test/optimizers/test_optimizers.py @@ -0,0 +1,441 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2018, 2024. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Test Optimizers""" + +import unittest +from test import QiskitAlgorithmsTestCase + +from typing import Optional, List, Tuple +from ddt import ddt, data, unpack +import numpy as np +from scipy.optimize import rosen, rosen_der + +from qiskit.circuit.library import RealAmplitudes +from qiskit.exceptions import MissingOptionalLibraryError +from qiskit.utils import optionals +from qiskit.primitives import Sampler + +from qiskit_machine_learning.optimizers import ( + ADAM, + AQGD, + BOBYQA, + IMFIL, + CG, + CRS, + COBYLA, + DIRECT_L, + DIRECT_L_RAND, + GSLS, + GradientDescent, + L_BFGS_B, + NELDER_MEAD, + Optimizer, + P_BFGS, + POWELL, + SLSQP, + SPSA, + QNSPSA, + TNC, + SciPyOptimizer, +) +from qiskit_machine_learning.utils import algorithm_globals + + +@ddt +class TestOptimizers(QiskitAlgorithmsTestCase): + """Test Optimizers""" + + def setUp(self): + super().setUp() + algorithm_globals.random_seed = 52 + + def run_optimizer( + self, + optimizer: Optimizer, + max_nfev: int, + grad: bool = False, + bounds: Optional[List[Tuple[float, float]]] = None, + ): + """Test the optimizer. + + Args: + optimizer: The optimizer instance to test. + max_nfev: The maximal allowed number of function evaluations. + grad: Whether to pass the gradient function as input. + bounds: Optimizer bounds. + """ + x_0 = np.asarray([1.3, 0.7, 0.8, 1.9, 1.2]) + jac = rosen_der if grad else None + + res = optimizer.minimize(rosen, x_0, jac, bounds) + x_opt = res.x + nfev = res.nfev + + np.testing.assert_array_almost_equal(x_opt, [1.0] * len(x_0), decimal=2) + self.assertLessEqual(nfev, max_nfev) + + def test_adam(self): + """adam test""" + optimizer = ADAM(maxiter=10000, tol=1e-06) + self.run_optimizer(optimizer, max_nfev=10000) + + def test_cg(self): + """cg test""" + optimizer = CG(maxiter=1000, tol=1e-06) + self.run_optimizer(optimizer, max_nfev=10000) + + def test_gradient_descent(self): + """cg test""" + optimizer = GradientDescent(maxiter=100000, tol=1e-06, learning_rate=1e-3) + self.run_optimizer(optimizer, grad=True, max_nfev=100000) + + def test_cobyla(self): + """cobyla test""" + optimizer = COBYLA(maxiter=100000, tol=1e-06) + self.run_optimizer(optimizer, max_nfev=100000) + + def test_l_bfgs_b(self): + """l_bfgs_b test""" + optimizer = L_BFGS_B(maxfun=1000) + self.run_optimizer(optimizer, max_nfev=10000) + + def test_p_bfgs(self): + """parallel l_bfgs_b test""" + optimizer = P_BFGS(maxfun=1000, max_processes=4) + self.run_optimizer(optimizer, max_nfev=10000) + + def test_nelder_mead(self): + """nelder mead test""" + optimizer = NELDER_MEAD(maxfev=10000, tol=1e-06) + self.run_optimizer(optimizer, max_nfev=10000) + + def test_powell(self): + """powell test""" + optimizer = POWELL(maxfev=10000, tol=1e-06) + self.run_optimizer(optimizer, max_nfev=10000) + + def test_slsqp(self): + """slsqp test""" + optimizer = SLSQP(maxiter=1000, tol=1e-06) + self.run_optimizer(optimizer, max_nfev=10000) + + @unittest.skip("Skipping SPSA as it does not do well on non-convex rozen") + def test_spsa(self): + """spsa test""" + optimizer = SPSA(maxiter=10000) + self.run_optimizer(optimizer, max_nfev=100000) + + def test_tnc(self): + """tnc test""" + optimizer = TNC(maxiter=1000, tol=1e-06) + self.run_optimizer(optimizer, max_nfev=10000) + + def test_gsls(self): + """gsls test""" + optimizer = GSLS( + sample_size_factor=40, + sampling_radius=1.0e-12, + maxiter=10000, + max_eval=10000, + min_step_size=1.0e-12, + ) + x_0 = np.asarray([1.3, 0.7, 0.8, 1.9, 1.2]) + + algorithm_globals.random_seed = 1 + res = optimizer.minimize(rosen, x_0) + x_value = res.fun + n_evals = res.nfev + + # Ensure value is near-optimal + self.assertLessEqual(x_value, 0.01) + self.assertLessEqual(n_evals, 10000) + + with self.subTest("Bounds (None, None)"): + algorithm_globals.random_seed = 1 + res = optimizer.minimize(rosen, x_0, bounds=[(None, None)] * len(x_0)) + + self.assertLessEqual(res.fun, 0.01) + self.assertLessEqual(res.nfev, 10000) + + def test_scipy_optimizer(self): + """scipy_optimizer test""" + optimizer = SciPyOptimizer("BFGS", options={"maxiter": 1000}) + self.run_optimizer(optimizer, max_nfev=10000) + + def test_scipy_optimizer_callback(self): + """scipy_optimizer callback test""" + values = [] + + def callback(x): + values.append(x) + + optimizer = SciPyOptimizer("BFGS", options={"maxiter": 1000}, callback=callback) + self.run_optimizer(optimizer, max_nfev=10000) + self.assertTrue(values) # Check the list is nonempty. + + def test_scipy_optimizer_parse_bounds(self): + """ + Test the parsing of bounds in SciPyOptimizer.minimize method. Verifies that the bounds are + correctly parsed and set within the optimizer object. + + Raises: + AssertionError: If any of the assertions fail. + AssertionError: If a TypeError is raised unexpectedly while parsing bounds. + + """ + try: + # Initialize SciPyOptimizer instance with SLSQP method + optimizer = SciPyOptimizer("SLSQP") + + # Call minimize method with a simple lambda function and bounds + optimizer.minimize(lambda x: -x, 1.0, bounds=[(0.0, 1.0)]) + + # Assert that "bounds" is not present in optimizer options and kwargs + self.assertFalse("bounds" in optimizer._options) + self.assertFalse("bounds" in optimizer._kwargs) + + except TypeError: + # This would give: https://github.com/qiskit-community/qiskit-machine-learning/issues/570 + self.fail( + "TypeError was raised unexpectedly when parsing bounds in SciPyOptimizer.minimize(...)." + ) + + # Finally, expect exceptions if bounds are parsed incorrectly, i.e. differently than as in Scipy + # with self.assertRaises(RuntimeError): + # _ = SciPyOptimizer("SLSQP", bounds=[(0.0, 1.0)]) + # with self.assertRaises(RuntimeError): + # _ = SciPyOptimizer("SLSQP", options={"bounds": [(0.0, 1.0)]}) + + # ESCH and ISRES do not do well with rosen + @data( + (CRS, True), + (DIRECT_L, True), + (DIRECT_L_RAND, True), + (CRS, False), + (DIRECT_L, False), + (DIRECT_L_RAND, False), + ) + @unpack + def test_nlopt(self, optimizer_cls, use_bound): + """NLopt test""" + bounds = [(-6, 6)] * 5 if use_bound else None + try: + optimizer = optimizer_cls() + optimizer.set_options(**{"max_evals": 50000}) + self.run_optimizer(optimizer, max_nfev=50000, bounds=bounds) + except MissingOptionalLibraryError as ex: + self.skipTest(str(ex)) + + +@ddt +class TestOptimizerSerialization(QiskitAlgorithmsTestCase): + """Tests concerning the serialization of optimizers.""" + + @data( + ("BFGS", {"maxiter": 100, "eps": np.array([0.1])}), + ("CG", {"maxiter": 200, "gtol": 1e-8}), + ("COBYLA", {"maxiter": 10}), + ("L_BFGS_B", {"maxiter": 30}), + ("NELDER_MEAD", {"maxiter": 0}), + ("NFT", {"maxiter": 100}), + ("P_BFGS", {"maxiter": 5}), + ("POWELL", {"maxiter": 1}), + ("SLSQP", {"maxiter": 400}), + ("TNC", {"maxiter": 20}), + ("dogleg", {"maxiter": 100}), + ("trust-constr", {"maxiter": 10}), + ("trust-ncg", {"maxiter": 100}), + ("trust-exact", {"maxiter": 120}), + ("trust-krylov", {"maxiter": 150}), + ) + @unpack + def test_scipy(self, method, options): + """Test the SciPyOptimizer is serializable.""" + + optimizer = SciPyOptimizer(method, options=options) + serialized = optimizer.settings + from_dict = SciPyOptimizer(**serialized) + + self.assertEqual(from_dict._method, method.lower()) + self.assertEqual(from_dict._options, options) + + def test_independent_reconstruction(self): + """Test the SciPyOptimizers don't reset all settings upon creating a new instance. + + COBYLA is used as representative example here.""" + + kwargs = {"coffee": "without sugar"} + options = {"tea": "with milk"} + optimizer = COBYLA(maxiter=1, options=options, **kwargs) + serialized = optimizer.settings + from_dict = COBYLA(**serialized) + + with self.subTest(msg="test attributes"): + self.assertEqual(from_dict.settings["maxiter"], 1) + + with self.subTest(msg="test options"): + # options should only contain values that are *not* already in the initializer + # (e.g. should not contain maxiter) + self.assertEqual(from_dict.settings["options"], {"tea": "with milk"}) + + with self.subTest(msg="test kwargs"): + self.assertEqual(from_dict.settings["coffee"], "without sugar") + + with self.subTest(msg="option ids differ"): + self.assertNotEqual(id(serialized["options"]), id(from_dict.settings["options"])) + + def test_adam(self): + """Test ADAM is serializable.""" + + adam = ADAM(maxiter=100, amsgrad=True) + settings = adam.settings + + self.assertEqual(settings["maxiter"], 100) + self.assertTrue(settings["amsgrad"]) + + def test_aqgd(self): + """Test AQGD is serializable.""" + + opt = AQGD(maxiter=[200, 100], eta=[0.2, 0.1], momentum=[0.25, 0.1]) + settings = opt.settings + + self.assertListEqual(settings["maxiter"], [200, 100]) + self.assertListEqual(settings["eta"], [0.2, 0.1]) + self.assertListEqual(settings["momentum"], [0.25, 0.1]) + + @unittest.skipIf(not optionals.HAS_SKQUANT, "Install scikit-quant to run this test.") + def test_bobyqa(self): + """Test BOBYQA is serializable.""" + + opt = BOBYQA(maxiter=200) + settings = opt.settings + + self.assertEqual(settings["maxiter"], 200) + + @unittest.skipIf(not optionals.HAS_SKQUANT, "Install scikit-quant to run this test.") + def test_imfil(self): + """Test IMFIL is serializable.""" + + opt = IMFIL(maxiter=200) + settings = opt.settings + + self.assertEqual(settings["maxiter"], 200) + + def test_gradient_descent(self): + """Test GradientDescent is serializable.""" + + opt = GradientDescent(maxiter=10, learning_rate=0.01) + settings = opt.settings + + self.assertEqual(settings["maxiter"], 10) + self.assertEqual(settings["learning_rate"], 0.01) + + def test_gsls(self): + """Test GSLS is serializable.""" + + opt = GSLS(maxiter=100, sampling_radius=1e-3) + settings = opt.settings + + self.assertEqual(settings["maxiter"], 100) + self.assertEqual(settings["sampling_radius"], 1e-3) + + def test_spsa(self): + """Test SPSA optimizer is serializable.""" + options = { + "maxiter": 100, + "blocking": True, + "allowed_increase": 0.1, + "second_order": True, + "learning_rate": 0.02, + "perturbation": 0.05, + "regularization": 0.1, + "resamplings": 2, + "perturbation_dims": 5, + "trust_region": False, + "initial_hessian": None, + "lse_solver": None, + "hessian_delay": 0, + "callback": None, + "termination_checker": None, + } + spsa = SPSA(**options) + + self.assertDictEqual(spsa.settings, options) + + def test_spsa_custom_iterators(self): + """Test serialization works with custom iterators for learning rate and perturbation.""" + rate = 0.99 + + def powerlaw(): + n = 0 + while True: + yield rate**n + n += 1 + + def steps(): + n = 1 + divide_after = 20 + epsilon = 0.5 + while True: + yield epsilon + n += 1 + if n % divide_after == 0: + epsilon /= 2 + + learning_rate = powerlaw() + expected_learning_rate = np.array([next(learning_rate) for _ in range(200)]) + + perturbation = steps() + expected_perturbation = np.array([next(perturbation) for _ in range(200)]) + + spsa = SPSA(maxiter=200, learning_rate=powerlaw, perturbation=steps) + settings = spsa.settings + + self.assertTrue(np.allclose(settings["learning_rate"], expected_learning_rate)) + self.assertTrue(np.allclose(settings["perturbation"], expected_perturbation)) + + def test_qnspsa(self): + """Test QN-SPSA optimizer is serializable.""" + ansatz = RealAmplitudes(1) + fidelity = QNSPSA.get_fidelity(ansatz, sampler=Sampler()) + options = { + "fidelity": fidelity, + "maxiter": 100, + "blocking": True, + "allowed_increase": 0.1, + "learning_rate": 0.02, + "perturbation": 0.05, + "regularization": 0.1, + "resamplings": 2, + "perturbation_dims": 5, + "lse_solver": None, + "initial_hessian": None, + "callback": None, + "termination_checker": None, + "hessian_delay": 0, + } + spsa = QNSPSA(**options) + + settings = spsa.settings + expected = options.copy() + + with self.subTest(msg="check constructed dictionary"): + self.assertDictEqual(settings, expected) + + reconstructed = QNSPSA(**settings) # pylint: disable=unexpected-keyword-arg + with self.subTest(msg="test reconstructed optimizer"): + self.assertDictEqual(reconstructed.settings, expected) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/optimizers/test_optimizers_scikitquant.py b/test/optimizers/test_optimizers_scikitquant.py new file mode 100644 index 000000000..5b797a226 --- /dev/null +++ b/test/optimizers/test_optimizers_scikitquant.py @@ -0,0 +1,68 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2020, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Test of scikit-quant optimizers.""" + +import unittest +from test import QiskitAlgorithmsTestCase + +from ddt import ddt, data, unpack + +import numpy +from qiskit.exceptions import MissingOptionalLibraryError +from qiskit.quantum_info import SparsePauliOp + +from qiskit_machine_learning.optimizers import SNOBFIT +from qiskit_machine_learning.utils import algorithm_globals + + +@ddt +class TestOptimizers(QiskitAlgorithmsTestCase): + """Test scikit-quant optimizers.""" + + def setUp(self): + """Set the problem.""" + super().setUp() + algorithm_globals.random_seed = 50 + self.qubit_op = SparsePauliOp.from_list( + [ + ("II", -1.052373245772859), + ("IZ", 0.39793742484318045), + ("ZI", -0.39793742484318045), + ("ZZ", -0.01128010425623538), + ("XX", 0.18093119978423156), + ] + ) + + @unittest.skipIf( + # NB: numpy.__version__ may contain letters, e.g. "1.26.0b1" + tuple(map(int, numpy.__version__.split(".")[:2])) >= (1, 24), + "scikit's SnobFit currently incompatible with NumPy 1.24.0.", + ) + @data((None,), ([(-1, 1), (None, None)],)) + @unpack + def test_snobfit_missing_bounds(self, bounds): + """SNOBFIT optimizer test with missing bounds.""" + try: + optimizer = SNOBFIT() + with self.assertRaises(ValueError): + optimizer.minimize( + fun=lambda _: 1, # using dummy function (never called) + x0=numpy.array([0.1, 0.1]), # dummy initial point + bounds=bounds, + ) + except MissingOptionalLibraryError as ex: + self.skipTest(str(ex)) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/optimizers/test_spsa.py b/test/optimizers/test_spsa.py new file mode 100644 index 000000000..5ce5d69e9 --- /dev/null +++ b/test/optimizers/test_spsa.py @@ -0,0 +1,266 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2021, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Tests for the SPSA optimizer.""" + +from test import QiskitAlgorithmsTestCase +from ddt import ddt, data + +import numpy as np + +from qiskit.circuit.library import PauliTwoDesign +from qiskit.primitives import Estimator, Sampler +from qiskit.quantum_info import SparsePauliOp, Statevector + +from qiskit_machine_learning.optimizers import SPSA, QNSPSA +from qiskit_machine_learning.utils import algorithm_globals + + +@ddt +class TestSPSA(QiskitAlgorithmsTestCase): + """Tests for the SPSA optimizer.""" + + def setUp(self): + super().setUp() + np.random.seed(12) + algorithm_globals.random_seed = 12 + + # @slow_test + @data("spsa", "2spsa", "qnspsa") + def test_pauli_two_design(self, method): + """Test SPSA on the Pauli two-design example.""" + circuit = PauliTwoDesign(3, reps=1, seed=1) + parameters = list(circuit.parameters) + obs = SparsePauliOp("ZZI") # Z^Z^I + + initial_point = np.array( + [0.82311034, 0.02611798, 0.21077064, 0.61842177, 0.09828447, 0.62013131] + ) + + def objective(x): + bound_circ = circuit.assign_parameters(dict(zip(parameters, x))) + return Statevector(bound_circ).expectation_value(obs).real + + settings = {"maxiter": 100, "blocking": True, "allowed_increase": 0} + + if method == "2spsa": + settings["second_order"] = True + settings["regularization"] = 0.01 + expected_nfev = settings["maxiter"] * 5 + 1 + elif method == "qnspsa": + settings["fidelity"] = QNSPSA.get_fidelity(circuit, sampler=Sampler()) + settings["regularization"] = 0.001 + settings["learning_rate"] = 0.05 + settings["perturbation"] = 0.05 + + expected_nfev = settings["maxiter"] * 7 + 1 + else: + expected_nfev = settings["maxiter"] * 3 + 1 + + if method == "qnspsa": + spsa = QNSPSA(**settings) + else: + spsa = SPSA(**settings) + + result = spsa.minimize(objective, x0=initial_point) + + with self.subTest("check final accuracy"): + self.assertLess(result.fun, -0.95) # final loss + + with self.subTest("check number of function calls"): + self.assertEqual(result.nfev, expected_nfev) # function evaluations + + def test_recalibrate_at_optimize(self): + """Test SPSA calibrates anew upon each optimization run, if no auto-calibration is set.""" + + def objective(x): + return -(x**2) + + spsa = SPSA(maxiter=1) + _ = spsa.minimize(objective, x0=np.array([0.5])) + + self.assertIsNone(spsa.learning_rate) + self.assertIsNone(spsa.perturbation) + + def test_learning_rate_perturbation_as_iterators(self): + """Test the learning rate and perturbation can be callables returning iterators.""" + + def get_learning_rate(): + def learning_rate(): + x = 0.99 + while True: + x *= x + yield x + + return learning_rate + + def get_perturbation(): + def perturbation(): + x = 0.99 + while True: + x *= x + yield max(x, 0.01) + + return perturbation + + def objective(x): + return (np.linalg.norm(x) - 2) ** 2 + + spsa = SPSA(learning_rate=get_learning_rate(), perturbation=get_perturbation()) + result = spsa.minimize(objective, np.array([0.5, 0.5])) + + self.assertAlmostEqual(np.linalg.norm(result.x), 2, places=2) + + def test_learning_rate_perturbation_as_arrays(self): + """Test the learning rate and perturbation can be arrays.""" + + learning_rate = np.linspace(1, 0, num=100, endpoint=False) ** 2 + perturbation = 0.01 * np.ones(100) + + def objective(x): + return (np.linalg.norm(x) - 2) ** 2 + + spsa = SPSA(learning_rate=learning_rate, perturbation=perturbation) + result = spsa.minimize(objective, x0=np.array([0.5, 0.5])) + + self.assertAlmostEqual(np.linalg.norm(result.x), 2, places=2) + + def test_termination_checker(self): + """Test the termination_callback""" + + def objective(x): + return np.linalg.norm(x) + np.random.rand(1) + + class TerminationChecker: + """Example termination checker""" + + def __init__(self): + self.values = [] + + def __call__(self, nfev, point, fvalue, stepsize, accepted) -> bool: + self.values.append(fvalue) + + if len(self.values) > 10: + return True + return False + + maxiter = 400 + spsa = SPSA(maxiter=maxiter, termination_checker=TerminationChecker()) + result = spsa.minimize(objective, x0=[0.5, 0.5]) + + self.assertLess(result.nit, maxiter) + + def test_callback(self): + """Test using the callback.""" + + def objective(x): + return (np.linalg.norm(x) - 2) ** 2 + + history = {"nfevs": [], "points": [], "fvals": [], "updates": [], "accepted": []} + + def callback(nfev, point, fval, update, accepted): + history["nfevs"].append(nfev) + history["points"].append(point) + history["fvals"].append(fval) + history["updates"].append(update) + history["accepted"].append(accepted) + + maxiter = 10 + spsa = SPSA(maxiter=maxiter, learning_rate=0.01, perturbation=0.01, callback=callback) + _ = spsa.minimize(objective, x0=np.array([0.5, 0.5])) + + expected_types = [int, np.ndarray, float, float, bool] + for i, (key, values) in enumerate(history.items()): + self.assertTrue(all(isinstance(value, expected_types[i]) for value in values)) + self.assertEqual(len(history[key]), maxiter) + + @data(1, 2, 3, 4) + def test_estimate_stddev(self, max_evals_grouped): + """Test the estimate_stddev + See https://github.com/Qiskit/qiskit-nature/issues/797""" + + def objective(x): + if len(x.shape) == 2: + return np.array([sum(x_i) for x_i in x]) + return sum(x) + + point = np.ones(5) + result = SPSA.estimate_stddev(objective, point, avg=10, max_evals_grouped=max_evals_grouped) + self.assertAlmostEqual(result, 0) + + def test_qnspsa_fidelity_primitives(self): + """Test the primitives can be used in get_fidelity.""" + ansatz = PauliTwoDesign(2, reps=1, seed=2) + initial_point = np.random.random(ansatz.num_parameters) + + with self.subTest(msg="pass as kwarg"): + fidelity = QNSPSA.get_fidelity(ansatz, sampler=Sampler()) + result = fidelity(initial_point, initial_point) + + self.assertAlmostEqual(result[0], 1) + + def test_qnspsa_max_evals_grouped(self): + """Test using max_evals_grouped with QNSPSA.""" + circuit = PauliTwoDesign(3, reps=1, seed=1) + num_parameters = circuit.num_parameters + + obs = SparsePauliOp("ZZI") # Z^Z^I + estimator = Estimator(options={"seed": 12}) + + initial_point = np.array( + [0.82311034, 0.02611798, 0.21077064, 0.61842177, 0.09828447, 0.62013131] + ) + + def objective(x): + x = np.reshape(x, (-1, num_parameters)).tolist() + n = len(x) + return estimator.run(n * [circuit], n * [obs], x).result().values.real + + fidelity = QNSPSA.get_fidelity(circuit, sampler=Sampler()) + optimizer = QNSPSA(fidelity) + optimizer.maxiter = 1 + optimizer.learning_rate = 0.05 + optimizer.perturbation = 0.05 + optimizer.set_max_evals_grouped(50) # greater than 1 + + result = optimizer.minimize(objective, initial_point) + + with self.subTest("check final accuracy"): + self.assertAlmostEqual(result.fun[0], 0.473, places=3) + + with self.subTest("check number of function calls"): + expected_nfev = 8 # 7 * maxiter + 1 + self.assertEqual(result.nfev, expected_nfev) + + def test_point_sample(self): + """Test point sample function in QNSPSA""" + + def fidelity(x, _y): # pylint: disable=invalid-name + x = np.asarray(x) + return np.ones_like(x, dtype=float) # some float + + def objective(x): + return x + + def get_perturbation(): + def perturbation(): + while True: + yield 1 + + return perturbation + + qnspsa = QNSPSA(fidelity, maxiter=1, learning_rate=0.1, perturbation=get_perturbation()) + initial_point = 1.0 + result = qnspsa.minimize(objective, initial_point) + + expected_nfev = 8 # 7 * maxiter + 1 + self.assertEqual(result.nfev, expected_nfev) diff --git a/test/optimizers/test_umda.py b/test/optimizers/test_umda.py new file mode 100644 index 000000000..fb085014b --- /dev/null +++ b/test/optimizers/test_umda.py @@ -0,0 +1,94 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2021, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Tests for the UMDA optimizer.""" + +from test import QiskitAlgorithmsTestCase + +import numpy as np +from scipy.optimize import rosen + +from qiskit_machine_learning.optimizers.umda import UMDA +from qiskit_machine_learning.utils import algorithm_globals + + +class TestUMDA(QiskitAlgorithmsTestCase): + """Tests for the UMDA optimizer.""" + + def test_get_set(self): + """Test if getters and setters work as expected""" + umda = UMDA(maxiter=1, size_gen=20) + umda.disp = True + umda.size_gen = 30 + umda.alpha = 0.6 + umda.maxiter = 100 + + self.assertTrue(umda.disp) + self.assertEqual(umda.size_gen, 30) + self.assertEqual(umda.alpha, 0.6) + self.assertEqual(umda.maxiter, 100) + + def test_settings(self): + """Test if the settings display works well""" + umda = UMDA(maxiter=1, size_gen=20) + umda.disp = True + umda.size_gen = 30 + umda.alpha = 0.6 + umda.maxiter = 100 + + set_ = { + "maxiter": 100, + "alpha": 0.6, + "size_gen": 30, + "callback": None, + } + + self.assertEqual(umda.settings, set_) + + def test_minimize(self): + """optimize function test""" + # UMDA is volatile so we need to set the seeds for the execution + algorithm_globals.random_seed = 52 + + optimizer = UMDA(maxiter=1000, size_gen=100) + x_0 = [1.3, 0.7, 1.5] + res = optimizer.minimize(rosen, x_0) + + self.assertIsNotNone(res.fun) + self.assertEqual(len(res.x), len(x_0)) + np.testing.assert_array_almost_equal(res.x, [1.0] * len(x_0), decimal=2) + + def test_callback(self): + """Test the callback.""" + + def objective(x): + return np.linalg.norm(x) - 1 + + nfevs, parameters, fvals = [], [], [] + + def store_history(*args): + nfevs.append(args[0]) + parameters.append(args[1]) + fvals.append(args[2]) + + optimizer = UMDA(maxiter=1, callback=store_history) + _ = optimizer.minimize(objective, x0=np.arange(5)) + + self.assertEqual(len(nfevs), 1) + self.assertIsInstance(nfevs[0], int) + + self.assertEqual(len(parameters), 1) + self.assertIsInstance(parameters[0], np.ndarray) + self.assertEqual(parameters[0].size, 5) + + self.assertEqual(len(fvals), 1) + self.assertIsInstance(fvals[0], float) diff --git a/test/optimizers/utils/__init__.py b/test/optimizers/utils/__init__.py new file mode 100644 index 000000000..a8e8b1a03 --- /dev/null +++ b/test/optimizers/utils/__init__.py @@ -0,0 +1,12 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +"""Tests for Optimizer Utils.""" diff --git a/test/optimizers/utils/test_learning_rate.py b/test/optimizers/utils/test_learning_rate.py new file mode 100644 index 000000000..418cd4a75 --- /dev/null +++ b/test/optimizers/utils/test_learning_rate.py @@ -0,0 +1,56 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Tests for LearningRate.""" + +from test import QiskitAlgorithmsTestCase +import numpy as np + +from qiskit_machine_learning.optimizers.optimizer_utils import LearningRate + + +class TestLearningRate(QiskitAlgorithmsTestCase): + """Tests for the LearningRate class.""" + + def setUp(self): + super().setUp() + np.random.seed(12) + self.initial_point = np.array([1, 1, 1, 1, 0]) + + def objective(self, x): + """Objective Function for the tests""" + return (np.linalg.norm(x) - 1) ** 2 + + def test_learning_rate(self): + """ + Tests if the learning rate is initialized properly for each kind of input: + float, list and iterator. + """ + constant_learning_rate_input = 0.01 + list_learning_rate_input = [0.01 * n for n in range(10)] + # pylint: disable=unnecessary-lambda-assignment + generator_learning_rate_input = lambda: (el for el in list_learning_rate_input) + + with self.subTest("Check constant learning rate."): + constant_learning_rate = LearningRate(learning_rate=constant_learning_rate_input) + for _ in range(5): + self.assertEqual(constant_learning_rate_input, next(constant_learning_rate)) + + with self.subTest("Check learning rate list."): + list_learning_rate = LearningRate(learning_rate=list_learning_rate_input) + for i in range(5): + self.assertEqual(list_learning_rate_input[i], next(list_learning_rate)) + + with self.subTest("Check learning rate generator."): + generator_learning_rate = LearningRate(generator_learning_rate_input) + for i in range(5): + self.assertEqual(list_learning_rate_input[i], next(generator_learning_rate)) diff --git a/test/state_fidelities/__init__.py b/test/state_fidelities/__init__.py new file mode 100644 index 000000000..3e84d2eaa --- /dev/null +++ b/test/state_fidelities/__init__.py @@ -0,0 +1,13 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Tests for the primitive-based fidelity interfaces.""" diff --git a/test/state_fidelities/test_compute_uncompute.py b/test/state_fidelities/test_compute_uncompute.py new file mode 100644 index 000000000..cd209343b --- /dev/null +++ b/test/state_fidelities/test_compute_uncompute.py @@ -0,0 +1,265 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Tests for Fidelity.""" + +import unittest +from test import QiskitAlgorithmsTestCase + +import numpy as np + +from qiskit.circuit import QuantumCircuit, ParameterVector +from qiskit.circuit.library import RealAmplitudes +from qiskit.primitives import Sampler + +from qiskit_machine_learning.state_fidelities import ComputeUncompute + + +class TestComputeUncompute(QiskitAlgorithmsTestCase): + """Test Compute-Uncompute Fidelity class""" + + def setUp(self): + super().setUp() + parameters = ParameterVector("x", 2) + + rx_rotations = QuantumCircuit(2) + rx_rotations.rx(parameters[0], 0) + rx_rotations.rx(parameters[1], 1) + + ry_rotations = QuantumCircuit(2) + ry_rotations.ry(parameters[0], 0) + ry_rotations.ry(parameters[1], 1) + + plus = QuantumCircuit(2) + plus.h([0, 1]) + + zero = QuantumCircuit(2) + + rx_rotation = QuantumCircuit(2) + rx_rotation.rx(parameters[0], 0) + rx_rotation.h(1) + + self._circuit = [rx_rotations, ry_rotations, plus, zero, rx_rotation] + self._sampler = Sampler() + self._left_params = np.array([[0, 0], [np.pi / 2, 0], [0, np.pi / 2], [np.pi, np.pi]]) + self._right_params = np.array([[0, 0], [0, 0], [np.pi / 2, 0], [0, 0]]) + + def test_1param_pair(self): + """test for fidelity with one pair of parameters""" + fidelity = ComputeUncompute(self._sampler) + job = fidelity.run( + self._circuit[0], self._circuit[1], self._left_params[0], self._right_params[0] + ) + result = job.result() + np.testing.assert_allclose(result.fidelities, np.array([1.0])) + + def test_1param_pair_local(self): + """test for fidelity with one pair of parameters""" + fidelity = ComputeUncompute(self._sampler, local=True) + job = fidelity.run( + self._circuit[0], self._circuit[1], self._left_params[0], self._right_params[0] + ) + result = job.result() + np.testing.assert_allclose(result.fidelities, np.array([1.0])) + + def test_local(self): + """test difference between local and global fidelity""" + fidelity_global = ComputeUncompute(self._sampler, local=False) + fidelity_local = ComputeUncompute(self._sampler, local=True) + fidelities = [] + for fidelity in [fidelity_global, fidelity_local]: + job = fidelity.run(self._circuit[2], self._circuit[3]) + result = job.result() + fidelities.append(result.fidelities[0]) + np.testing.assert_allclose(fidelities, np.array([0.25, 0.5]), atol=1e-16) + + def test_4param_pairs(self): + """test for fidelity with four pairs of parameters""" + fidelity = ComputeUncompute(self._sampler) + n = len(self._left_params) + job = fidelity.run( + [self._circuit[0]] * n, [self._circuit[1]] * n, self._left_params, self._right_params + ) + results = job.result() + np.testing.assert_allclose(results.fidelities, np.array([1.0, 0.5, 0.25, 0.0]), atol=1e-16) + + def test_symmetry(self): + """test for fidelity with the same circuit""" + fidelity = ComputeUncompute(self._sampler) + n = len(self._left_params) + job_1 = fidelity.run( + [self._circuit[0]] * n, [self._circuit[0]] * n, self._left_params, self._right_params + ) + job_2 = fidelity.run( + [self._circuit[0]] * n, [self._circuit[0]] * n, self._right_params, self._left_params + ) + results_1 = job_1.result() + results_2 = job_2.result() + np.testing.assert_allclose(results_1.fidelities, results_2.fidelities, atol=1e-16) + + def test_no_params(self): + """test for fidelity without parameters""" + fidelity = ComputeUncompute(self._sampler) + job = fidelity.run([self._circuit[2]], [self._circuit[3]]) + results = job.result() + np.testing.assert_allclose(results.fidelities, np.array([0.25]), atol=1e-16) + + job = fidelity.run([self._circuit[2]], [self._circuit[3]], [], []) + results = job.result() + np.testing.assert_allclose(results.fidelities, np.array([0.25]), atol=1e-16) + + def test_left_param(self): + """test for fidelity with only left parameters""" + fidelity = ComputeUncompute(self._sampler) + n = len(self._left_params) + job = fidelity.run( + [self._circuit[1]] * n, [self._circuit[3]] * n, values_1=self._left_params + ) + results = job.result() + np.testing.assert_allclose(results.fidelities, np.array([1.0, 0.5, 0.5, 0.0]), atol=1e-16) + + def test_right_param(self): + """test for fidelity with only right parameters""" + fidelity = ComputeUncompute(self._sampler) + n = len(self._left_params) + job = fidelity.run( + [self._circuit[3]] * n, [self._circuit[1]] * n, values_2=self._left_params + ) + results = job.result() + np.testing.assert_allclose(results.fidelities, np.array([1.0, 0.5, 0.5, 0.0]), atol=1e-16) + + def test_not_set_circuits(self): + """test for fidelity with no circuits.""" + fidelity = ComputeUncompute(self._sampler) + with self.assertRaises(TypeError): + job = fidelity.run( + circuits_1=None, + circuits_2=None, + values_1=self._left_params, + values_2=self._right_params, + ) + job.result() + + def test_circuit_mismatch(self): + """test for fidelity with different number of left/right circuits.""" + fidelity = ComputeUncompute(self._sampler) + n = len(self._left_params) + with self.assertRaises(ValueError): + job = fidelity.run( + [self._circuit[0]] * n, + [self._circuit[1]] * (n + 1), + self._left_params, + self._right_params, + ) + job.result() + + def test_asymmetric_params(self): + """test for fidelity when the 2 circuits have different number of + left/right parameters.""" + + fidelity = ComputeUncompute(self._sampler) + n = len(self._left_params) + right_params = [[p] for p in self._right_params[:, 0]] + job = fidelity.run( + [self._circuit[0]] * n, [self._circuit[4]] * n, self._left_params, right_params + ) + result = job.result() + np.testing.assert_allclose(result.fidelities, np.array([0.5, 0.25, 0.25, 0.0]), atol=1e-16) + + def test_input_format(self): + """test for different input format variations""" + + fidelity = ComputeUncompute(self._sampler) + circuit = RealAmplitudes(2) + values = np.random.random(circuit.num_parameters) + shift = np.ones_like(values) * 0.01 + + # lists of circuits, lists of numpy arrays + job = fidelity.run([circuit], [circuit], [values], [values + shift]) + result_1 = job.result() + + # lists of circuits, lists of lists + shift_val = values + shift + job = fidelity.run([circuit], [circuit], [values.tolist()], [shift_val.tolist()]) + result_2 = job.result() + + # circuits, lists + shift_val = values + shift + job = fidelity.run(circuit, circuit, values.tolist(), shift_val.tolist()) + result_3 = job.result() + + # circuits, np.arrays + job = fidelity.run(circuit, circuit, values, values + shift) + result_4 = job.result() + + np.testing.assert_allclose(result_1.fidelities, result_2.fidelities, atol=1e-16) + np.testing.assert_allclose(result_1.fidelities, result_3.fidelities, atol=1e-16) + np.testing.assert_allclose(result_1.fidelities, result_4.fidelities, atol=1e-16) + + def test_input_measurements(self): + """test for fidelity with measurements on input circuits""" + fidelity = ComputeUncompute(self._sampler) + circuit_1 = self._circuit[0] + circuit_1.measure_all() + circuit_2 = self._circuit[1] + circuit_2.measure_all() + + job = fidelity.run(circuit_1, circuit_2, self._left_params[0], self._right_params[0]) + result = job.result() + np.testing.assert_allclose(result.fidelities, np.array([1.0])) + + def test_options(self): + """Test fidelity's run options""" + sampler_shots = Sampler(options={"shots": 1024}) + + with self.subTest("sampler"): + # Only options in sampler + fidelity = ComputeUncompute(sampler_shots) + options = fidelity.options + job = fidelity.run(self._circuit[2], self._circuit[3]) + result = job.result() + self.assertEqual(options.__dict__, {"shots": 1024}) + self.assertEqual(result.options.__dict__, {"shots": 1024}) + + with self.subTest("fidelity init"): + # Fidelity default options override sampler + # options and add new fields + fidelity = ComputeUncompute(sampler_shots, options={"shots": 2048, "dummy": 100}) + options = fidelity.options + job = fidelity.run(self._circuit[2], self._circuit[3]) + result = job.result() + self.assertEqual(options.__dict__, {"shots": 2048, "dummy": 100}) + self.assertEqual(result.options.__dict__, {"shots": 2048, "dummy": 100}) + + with self.subTest("fidelity update"): + # Update fidelity options + fidelity = ComputeUncompute(sampler_shots, options={"shots": 2048, "dummy": 100}) + fidelity.update_default_options(shots=100) + options = fidelity.options + job = fidelity.run(self._circuit[2], self._circuit[3]) + result = job.result() + self.assertEqual(options.__dict__, {"shots": 100, "dummy": 100}) + self.assertEqual(result.options.__dict__, {"shots": 100, "dummy": 100}) + + with self.subTest("fidelity run"): + # Run options override fidelity options + fidelity = ComputeUncompute(sampler_shots, options={"shots": 2048, "dummy": 100}) + job = fidelity.run(self._circuit[2], self._circuit[3], shots=50, dummy=None) + options = fidelity.options + result = job.result() + # Only default + sampler options. Not run. + self.assertEqual(options.__dict__, {"shots": 2048, "dummy": 100}) + self.assertEqual(result.options.__dict__, {"shots": 50, "dummy": None}) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/utils/test_validate_bounds.py b/test/utils/test_validate_bounds.py new file mode 100644 index 000000000..bfdb8e6bc --- /dev/null +++ b/test/utils/test_validate_bounds.py @@ -0,0 +1,52 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Test validate bounds.""" + +from test import QiskitAlgorithmsTestCase + +from unittest.mock import Mock + +import numpy as np + +from qiskit_machine_learning.utils import algorithm_globals, validate_bounds + + +class TestValidateBounds(QiskitAlgorithmsTestCase): + """Test the ``validate_bounds`` utility function.""" + + def setUp(self): + super().setUp() + algorithm_globals.random_seed = 0 + self.bounds = [(-np.pi / 2, np.pi / 2)] + self.ansatz = Mock() + + def test_with_no_ansatz_bounds(self): + """Test with no ansatz bounds.""" + self.ansatz.num_parameters = 1 + self.ansatz.parameter_bounds = None + bounds = validate_bounds(self.ansatz) + self.assertEqual(bounds, [(None, None)]) + + def test_with_ansatz_bounds(self): + """Test with ansatz bounds.""" + self.ansatz.num_parameters = 1 + self.ansatz.parameter_bounds = self.bounds + bounds = validate_bounds(self.ansatz) + self.assertEqual(bounds, self.bounds) + + def test_with_mismatched_num_params(self): + """Test with a mismatched number of parameters and bounds""" + self.ansatz.num_parameters = 2 + self.ansatz.parameter_bounds = self.bounds + with self.assertRaises(ValueError): + _ = validate_bounds(self.ansatz) diff --git a/test/utils/test_validate_initial_point.py b/test/utils/test_validate_initial_point.py new file mode 100644 index 000000000..bc0ac7368 --- /dev/null +++ b/test/utils/test_validate_initial_point.py @@ -0,0 +1,49 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Test validate initial point.""" + +from test import QiskitAlgorithmsTestCase + +from unittest.mock import Mock + +import numpy as np + +from qiskit_machine_learning.utils import algorithm_globals, validate_initial_point + + +class TestValidateInitialPoint(QiskitAlgorithmsTestCase): + """Test the ``validate_initial_point`` utility function.""" + + def setUp(self): + super().setUp() + algorithm_globals.random_seed = 0 + self.ansatz = Mock() + self.ansatz.num_parameters = 1 + + def test_with_no_initial_point_or_bounds(self): + """Test with no user-defined initial point and no ansatz bounds.""" + self.ansatz.parameter_bounds = None + initial_point = validate_initial_point(None, self.ansatz) + np.testing.assert_array_almost_equal(initial_point, [1.721111]) + + def test_with_no_initial_point(self): + """Test with no user-defined initial point with ansatz bounds.""" + self.ansatz.parameter_bounds = [(-np.pi / 2, np.pi / 2)] + initial_point = validate_initial_point(None, self.ansatz) + np.testing.assert_array_almost_equal(initial_point, [0.430278]) + + def test_with_mismatched_params(self): + """Test with mismatched parameters and bounds..""" + self.ansatz.parameter_bounds = None + with self.assertRaises(ValueError): + _ = validate_initial_point([1.0, 2.0], self.ansatz) From c5a55adc159dc772db528fd27b1ec62a6ed95247 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Thu, 11 Jul 2024 18:04:39 +0200 Subject: [PATCH 28/85] Fix copyrights and some spellings --- .pylintdict | 20 +++++++++++++++++++ qiskit_machine_learning/__init__.py | 2 +- qiskit_machine_learning/algorithm_result.py | 2 +- .../classifiers/neural_network_classifier.py | 2 +- .../algorithms/classifiers/pegasos_qsvc.py | 2 +- .../algorithms/classifiers/qsvc.py | 2 +- .../algorithms/classifiers/vqc.py | 2 +- .../algorithms/objective_functions.py | 2 +- .../regressors/neural_network_regressor.py | 2 +- .../algorithms/regressors/qsvr.py | 2 +- .../algorithms/regressors/vqr.py | 2 +- .../circuit/library/qnn_circuit.py | 2 +- qiskit_machine_learning/exceptions.py | 2 +- qiskit_machine_learning/gradients/__init__.py | 2 +- .../gradients/base/__init__.py | 2 +- .../gradients/base/base_estimator_gradient.py | 2 +- .../gradients/base/base_qgt.py | 2 +- .../gradients/base/base_sampler_gradient.py | 2 +- .../base/estimator_gradient_result.py | 2 +- .../gradients/base/qgt_result.py | 2 +- .../gradients/base/sampler_gradient_result.py | 2 +- .../gradients/lin_comb/__init__.py | 2 +- .../lin_comb/lin_comb_estimator_gradient.py | 2 +- .../gradients/lin_comb/lin_comb_qgt.py | 2 +- .../lin_comb/lin_comb_sampler_gradient.py | 2 +- .../gradients/param_shift/__init__.py | 2 +- .../param_shift_estimator_gradient.py | 2 +- .../param_shift_sampler_gradient.py | 2 +- qiskit_machine_learning/gradients/qfi.py | 2 +- .../gradients/qfi_result.py | 2 +- .../gradients/spsa/__init__.py | 2 +- .../gradients/spsa/spsa_estimator_gradient.py | 2 +- .../gradients/spsa/spsa_sampler_gradient.py | 2 +- qiskit_machine_learning/gradients/utils.py | 2 +- .../trainable_fidelity_quantum_kernel.py | 2 +- .../optimizers/__init__.py | 2 +- .../optimizers/adam_amsgrad.py | 2 +- qiskit_machine_learning/optimizers/bobyqa.py | 2 +- qiskit_machine_learning/optimizers/cg.py | 2 +- qiskit_machine_learning/optimizers/cobyla.py | 2 +- .../optimizers/gradient_descent.py | 2 +- qiskit_machine_learning/optimizers/gsls.py | 2 +- qiskit_machine_learning/optimizers/imfil.py | 2 +- .../optimizers/l_bfgs_b.py | 2 +- .../optimizers/nelder_mead.py | 2 +- qiskit_machine_learning/optimizers/nft.py | 2 +- .../optimizers/nlopts/__init__.py | 2 +- .../optimizers/nlopts/crs.py | 2 +- .../optimizers/nlopts/direct_l.py | 2 +- .../optimizers/nlopts/direct_l_rand.py | 2 +- .../optimizers/nlopts/esch.py | 2 +- .../optimizers/nlopts/isres.py | 2 +- .../optimizers/nlopts/nloptimizer.py | 2 +- .../optimizers/optimizer.py | 2 +- .../optimizers/optimizer_utils/__init__.py | 2 +- .../optimizer_utils/learning_rate.py | 2 +- qiskit_machine_learning/optimizers/p_bfgs.py | 2 +- qiskit_machine_learning/optimizers/powell.py | 2 +- qiskit_machine_learning/optimizers/qnspsa.py | 2 +- .../optimizers/scipy_optimizer.py | 2 +- qiskit_machine_learning/optimizers/slsqp.py | 2 +- qiskit_machine_learning/optimizers/snobfit.py | 2 +- .../optimizers/steppable_optimizer.py | 2 +- qiskit_machine_learning/optimizers/tnc.py | 2 +- qiskit_machine_learning/optimizers/umda.py | 2 +- .../state_fidelities/__init__.py | 2 +- .../state_fidelities/base_state_fidelity.py | 2 +- .../state_fidelities/compute_uncompute.py | 2 +- .../state_fidelities/state_fidelity_result.py | 2 +- qiskit_machine_learning/utils/__init__.py | 2 +- .../utils/adjust_num_qubits.py | 2 +- .../utils/algorithm_globals.py | 2 +- qiskit_machine_learning/utils/optionals.py | 2 +- qiskit_machine_learning/utils/set_batching.py | 2 +- .../utils/validate_bounds.py | 2 +- .../utils/validate_initial_point.py | 2 +- qiskit_machine_learning/utils/validation.py | 2 +- .../variational_algorithm.py | 2 +- test/__init__.py | 2 +- ...st_fidelity_quantum_kernel_pegasos_qsvc.py | 2 +- .../test_fidelity_quantum_kernel_qsvc.py | 2 +- .../test_neural_network_classifier.py | 2 +- .../classifiers/test_pegasos_qsvc.py | 2 +- test/algorithms/classifiers/test_qsvc.py | 2 +- test/algorithms/classifiers/test_vqc.py | 2 +- .../test_fidelity_quantum_kernel_qsvr.py | 2 +- .../test_neural_network_regressor.py | 2 +- test/algorithms/regressors/test_vqr.py | 2 +- test/algorithms_test_case.py | 2 +- .../library/test_raw_feature_vector.py | 2 +- test/connectors/test_torch.py | 2 +- test/datasets/test_ad_hoc_data.py | 2 +- test/gradients/__init__.py | 2 +- test/gradients/logging_primitives.py | 2 +- test/gradients/test_qfi.py | 2 +- test/gradients/test_qgt.py | 2 +- .../test_fidelity_qkernel_trainer.py | 2 +- .../test_effective_dimension.py | 2 +- test/neural_networks/test_sampler_qnn.py | 2 +- test/optimizers/__init__.py | 2 +- test/optimizers/test_gradient_descent.py | 2 +- .../optimizers/test_optimizers_scikitquant.py | 2 +- test/optimizers/test_spsa.py | 2 +- test/optimizers/test_umda.py | 2 +- test/optimizers/utils/__init__.py | 2 +- test/optimizers/utils/test_learning_rate.py | 2 +- test/state_fidelities/__init__.py | 2 +- .../test_compute_uncompute.py | 2 +- test/utils/test_validate_bounds.py | 2 +- test/utils/test_validate_initial_point.py | 2 +- 110 files changed, 129 insertions(+), 109 deletions(-) diff --git a/.pylintdict b/.pylintdict index 51c6a9431..7aff18f67 100644 --- a/.pylintdict +++ b/.pylintdict @@ -9,6 +9,7 @@ ancillas ansatz ansatzes args +Armijo asmatrix aspuru async @@ -80,6 +81,7 @@ einsum einstein endian entangler +eps estimatorqnn et eval @@ -91,10 +93,12 @@ formatter frac frontend func +Gacon gambetta gaussian gellmann getter +Gogolin gpu grover guang @@ -110,6 +114,7 @@ hubbard hyperparameter hyperparameters hyperplanes +im init inlier inplace @@ -121,23 +126,27 @@ isometry iten iterable iteratively +Izaac jacobian jm jonathan jupyter kandala kernelized +Killoran kwarg kwargs labelled lagrange langle +lcu lukin macos makefile mary matmul matplotlib +mathrm maxiter mcrx mcry @@ -194,6 +203,7 @@ precompute precomputed precomputes precomputing +preconditioner preprint priori ps @@ -208,10 +218,13 @@ qb qbayesian qbi qc +qfi qgan qgans +qgt qiskit qiskit's +qn qnn qsvc qsvr @@ -226,6 +239,7 @@ regressor regressors regs reproducibility +resamplings rescale rhs romero @@ -236,6 +250,7 @@ ry rz samplerqnn scalability +Schuld scikit scipy semidefinite @@ -247,11 +262,14 @@ sima sklearn softmax sparsearray +spsa +sqrt statevector statevectors stdlib stdlib stdout +stepsize str subclasses subcircuits @@ -270,6 +288,7 @@ th theodore toctree todo +tol traceback trainability trainablefidelityquantumkernel @@ -303,4 +322,5 @@ wrt yoder zoufal zsh +zz θ diff --git a/qiskit_machine_learning/__init__.py b/qiskit_machine_learning/__init__.py index 67579e4d1..3d9dc498a 100644 --- a/qiskit_machine_learning/__init__.py +++ b/qiskit_machine_learning/__init__.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2019, 2023. +# (C) Copyright IBM 2019, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/algorithm_result.py b/qiskit_machine_learning/algorithm_result.py index 695bab749..95b45d829 100644 --- a/qiskit_machine_learning/algorithm_result.py +++ b/qiskit_machine_learning/algorithm_result.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2020, 2023. +# (C) Copyright IBM 2020, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/algorithms/classifiers/neural_network_classifier.py b/qiskit_machine_learning/algorithms/classifiers/neural_network_classifier.py index cc0edd7fa..7ad9d4bf7 100644 --- a/qiskit_machine_learning/algorithms/classifiers/neural_network_classifier.py +++ b/qiskit_machine_learning/algorithms/classifiers/neural_network_classifier.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2021, 2023. +# (C) Copyright IBM 2021, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/algorithms/classifiers/pegasos_qsvc.py b/qiskit_machine_learning/algorithms/classifiers/pegasos_qsvc.py index bd6e89baf..cae4c0e74 100644 --- a/qiskit_machine_learning/algorithms/classifiers/pegasos_qsvc.py +++ b/qiskit_machine_learning/algorithms/classifiers/pegasos_qsvc.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023. +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/algorithms/classifiers/qsvc.py b/qiskit_machine_learning/algorithms/classifiers/qsvc.py index 2985176da..a67cfafd0 100644 --- a/qiskit_machine_learning/algorithms/classifiers/qsvc.py +++ b/qiskit_machine_learning/algorithms/classifiers/qsvc.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2021, 2023. +# (C) Copyright IBM 2021, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/algorithms/classifiers/vqc.py b/qiskit_machine_learning/algorithms/classifiers/vqc.py index 257464f8a..fbeb67f7e 100644 --- a/qiskit_machine_learning/algorithms/classifiers/vqc.py +++ b/qiskit_machine_learning/algorithms/classifiers/vqc.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2021, 2023. +# (C) Copyright IBM 2021, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/algorithms/objective_functions.py b/qiskit_machine_learning/algorithms/objective_functions.py index e8657731a..86d1bfc50 100644 --- a/qiskit_machine_learning/algorithms/objective_functions.py +++ b/qiskit_machine_learning/algorithms/objective_functions.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2021, 2023. +# (C) Copyright IBM 2021, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/algorithms/regressors/neural_network_regressor.py b/qiskit_machine_learning/algorithms/regressors/neural_network_regressor.py index 667835e2b..94a77f620 100644 --- a/qiskit_machine_learning/algorithms/regressors/neural_network_regressor.py +++ b/qiskit_machine_learning/algorithms/regressors/neural_network_regressor.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2021, 2023. +# (C) Copyright IBM 2021, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/algorithms/regressors/qsvr.py b/qiskit_machine_learning/algorithms/regressors/qsvr.py index e5ed59090..eb059a149 100644 --- a/qiskit_machine_learning/algorithms/regressors/qsvr.py +++ b/qiskit_machine_learning/algorithms/regressors/qsvr.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2021, 2023. +# (C) Copyright IBM 2021, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/algorithms/regressors/vqr.py b/qiskit_machine_learning/algorithms/regressors/vqr.py index 73fa9c98e..addf34667 100644 --- a/qiskit_machine_learning/algorithms/regressors/vqr.py +++ b/qiskit_machine_learning/algorithms/regressors/vqr.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2021, 2023. +# (C) Copyright IBM 2021, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/circuit/library/qnn_circuit.py b/qiskit_machine_learning/circuit/library/qnn_circuit.py index 974ba8c16..7385a8f46 100644 --- a/qiskit_machine_learning/circuit/library/qnn_circuit.py +++ b/qiskit_machine_learning/circuit/library/qnn_circuit.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2023. +# (C) Copyright IBM 2023, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/exceptions.py b/qiskit_machine_learning/exceptions.py index 1b8804d76..ac2befee7 100644 --- a/qiskit_machine_learning/exceptions.py +++ b/qiskit_machine_learning/exceptions.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2021, 2023. +# (C) Copyright IBM 2021, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/gradients/__init__.py b/qiskit_machine_learning/gradients/__init__.py index 15f4a65a7..700ebe7ee 100644 --- a/qiskit_machine_learning/gradients/__init__.py +++ b/qiskit_machine_learning/gradients/__init__.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023 +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/gradients/base/__init__.py b/qiskit_machine_learning/gradients/base/__init__.py index adb31296c..c870af8ba 100644 --- a/qiskit_machine_learning/gradients/base/__init__.py +++ b/qiskit_machine_learning/gradients/base/__init__.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023 +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/gradients/base/base_estimator_gradient.py b/qiskit_machine_learning/gradients/base/base_estimator_gradient.py index f7ea927b2..edfe80fd0 100644 --- a/qiskit_machine_learning/gradients/base/base_estimator_gradient.py +++ b/qiskit_machine_learning/gradients/base/base_estimator_gradient.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023 +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/gradients/base/base_qgt.py b/qiskit_machine_learning/gradients/base/base_qgt.py index 2e254a8f0..9094a26a5 100644 --- a/qiskit_machine_learning/gradients/base/base_qgt.py +++ b/qiskit_machine_learning/gradients/base/base_qgt.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023. +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/gradients/base/base_sampler_gradient.py b/qiskit_machine_learning/gradients/base/base_sampler_gradient.py index 1114b5f02..9e29b47ab 100644 --- a/qiskit_machine_learning/gradients/base/base_sampler_gradient.py +++ b/qiskit_machine_learning/gradients/base/base_sampler_gradient.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023 +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/gradients/base/estimator_gradient_result.py b/qiskit_machine_learning/gradients/base/estimator_gradient_result.py index a01759f0d..3c4ecabf6 100644 --- a/qiskit_machine_learning/gradients/base/estimator_gradient_result.py +++ b/qiskit_machine_learning/gradients/base/estimator_gradient_result.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023. +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/gradients/base/qgt_result.py b/qiskit_machine_learning/gradients/base/qgt_result.py index f7a9d80b1..acdb6710e 100644 --- a/qiskit_machine_learning/gradients/base/qgt_result.py +++ b/qiskit_machine_learning/gradients/base/qgt_result.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023. +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/gradients/base/sampler_gradient_result.py b/qiskit_machine_learning/gradients/base/sampler_gradient_result.py index 393319ab0..2c27fddc3 100644 --- a/qiskit_machine_learning/gradients/base/sampler_gradient_result.py +++ b/qiskit_machine_learning/gradients/base/sampler_gradient_result.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023. +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/gradients/lin_comb/__init__.py b/qiskit_machine_learning/gradients/lin_comb/__init__.py index adb31296c..c870af8ba 100644 --- a/qiskit_machine_learning/gradients/lin_comb/__init__.py +++ b/qiskit_machine_learning/gradients/lin_comb/__init__.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023 +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/gradients/lin_comb/lin_comb_estimator_gradient.py b/qiskit_machine_learning/gradients/lin_comb/lin_comb_estimator_gradient.py index 1be059007..f7787f7e3 100644 --- a/qiskit_machine_learning/gradients/lin_comb/lin_comb_estimator_gradient.py +++ b/qiskit_machine_learning/gradients/lin_comb/lin_comb_estimator_gradient.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023. +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/gradients/lin_comb/lin_comb_qgt.py b/qiskit_machine_learning/gradients/lin_comb/lin_comb_qgt.py index a00c7b670..afa452ae5 100644 --- a/qiskit_machine_learning/gradients/lin_comb/lin_comb_qgt.py +++ b/qiskit_machine_learning/gradients/lin_comb/lin_comb_qgt.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023. +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/gradients/lin_comb/lin_comb_sampler_gradient.py b/qiskit_machine_learning/gradients/lin_comb/lin_comb_sampler_gradient.py index 30083d538..cbbe8bf45 100644 --- a/qiskit_machine_learning/gradients/lin_comb/lin_comb_sampler_gradient.py +++ b/qiskit_machine_learning/gradients/lin_comb/lin_comb_sampler_gradient.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023. +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/gradients/param_shift/__init__.py b/qiskit_machine_learning/gradients/param_shift/__init__.py index adb31296c..c870af8ba 100644 --- a/qiskit_machine_learning/gradients/param_shift/__init__.py +++ b/qiskit_machine_learning/gradients/param_shift/__init__.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023 +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/gradients/param_shift/param_shift_estimator_gradient.py b/qiskit_machine_learning/gradients/param_shift/param_shift_estimator_gradient.py index cb3fcf908..cde25a0fd 100644 --- a/qiskit_machine_learning/gradients/param_shift/param_shift_estimator_gradient.py +++ b/qiskit_machine_learning/gradients/param_shift/param_shift_estimator_gradient.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023. +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/gradients/param_shift/param_shift_sampler_gradient.py b/qiskit_machine_learning/gradients/param_shift/param_shift_sampler_gradient.py index b27d64873..0d7f384a8 100644 --- a/qiskit_machine_learning/gradients/param_shift/param_shift_sampler_gradient.py +++ b/qiskit_machine_learning/gradients/param_shift/param_shift_sampler_gradient.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023. +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/gradients/qfi.py b/qiskit_machine_learning/gradients/qfi.py index 4a53b20d9..ad0c83e85 100644 --- a/qiskit_machine_learning/gradients/qfi.py +++ b/qiskit_machine_learning/gradients/qfi.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023 +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/gradients/qfi_result.py b/qiskit_machine_learning/gradients/qfi_result.py index 9486c990c..57aeeb932 100644 --- a/qiskit_machine_learning/gradients/qfi_result.py +++ b/qiskit_machine_learning/gradients/qfi_result.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023. +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/gradients/spsa/__init__.py b/qiskit_machine_learning/gradients/spsa/__init__.py index adb31296c..c870af8ba 100644 --- a/qiskit_machine_learning/gradients/spsa/__init__.py +++ b/qiskit_machine_learning/gradients/spsa/__init__.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023 +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/gradients/spsa/spsa_estimator_gradient.py b/qiskit_machine_learning/gradients/spsa/spsa_estimator_gradient.py index 9ace83afb..1f9bfa0b2 100644 --- a/qiskit_machine_learning/gradients/spsa/spsa_estimator_gradient.py +++ b/qiskit_machine_learning/gradients/spsa/spsa_estimator_gradient.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023. +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/gradients/spsa/spsa_sampler_gradient.py b/qiskit_machine_learning/gradients/spsa/spsa_sampler_gradient.py index 8a83a64a5..c3de7c4da 100644 --- a/qiskit_machine_learning/gradients/spsa/spsa_sampler_gradient.py +++ b/qiskit_machine_learning/gradients/spsa/spsa_sampler_gradient.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023. +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/gradients/utils.py b/qiskit_machine_learning/gradients/utils.py index 2ad9b0286..53ef7fcc2 100644 --- a/qiskit_machine_learning/gradients/utils.py +++ b/qiskit_machine_learning/gradients/utils.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023. +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/kernels/trainable_fidelity_quantum_kernel.py b/qiskit_machine_learning/kernels/trainable_fidelity_quantum_kernel.py index c84b1ce8e..a813b97e0 100644 --- a/qiskit_machine_learning/kernels/trainable_fidelity_quantum_kernel.py +++ b/qiskit_machine_learning/kernels/trainable_fidelity_quantum_kernel.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023. +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/optimizers/__init__.py b/qiskit_machine_learning/optimizers/__init__.py index c196c85bf..8ef6f5a8f 100644 --- a/qiskit_machine_learning/optimizers/__init__.py +++ b/qiskit_machine_learning/optimizers/__init__.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2018, 2023. +# (C) Copyright IBM 2018, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/optimizers/adam_amsgrad.py b/qiskit_machine_learning/optimizers/adam_amsgrad.py index b1639450e..5ce663212 100644 --- a/qiskit_machine_learning/optimizers/adam_amsgrad.py +++ b/qiskit_machine_learning/optimizers/adam_amsgrad.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2019, 2023. +# (C) Copyright IBM 2019, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/optimizers/bobyqa.py b/qiskit_machine_learning/optimizers/bobyqa.py index 6f21d6603..efd4faeb6 100644 --- a/qiskit_machine_learning/optimizers/bobyqa.py +++ b/qiskit_machine_learning/optimizers/bobyqa.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2019, 2023. +# (C) Copyright IBM 2019, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/optimizers/cg.py b/qiskit_machine_learning/optimizers/cg.py index e69761663..bb060389a 100644 --- a/qiskit_machine_learning/optimizers/cg.py +++ b/qiskit_machine_learning/optimizers/cg.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2018, 2023. +# (C) Copyright IBM 2018, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/optimizers/cobyla.py b/qiskit_machine_learning/optimizers/cobyla.py index f5eaa0407..d7710b1e3 100644 --- a/qiskit_machine_learning/optimizers/cobyla.py +++ b/qiskit_machine_learning/optimizers/cobyla.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2018, 2023. +# (C) Copyright IBM 2018, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/optimizers/gradient_descent.py b/qiskit_machine_learning/optimizers/gradient_descent.py index a27ac36b8..65b0a43ec 100644 --- a/qiskit_machine_learning/optimizers/gradient_descent.py +++ b/qiskit_machine_learning/optimizers/gradient_descent.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2021, 2023. +# (C) Copyright IBM 2021, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/optimizers/gsls.py b/qiskit_machine_learning/optimizers/gsls.py index 07645df8e..dd598ee8c 100644 --- a/qiskit_machine_learning/optimizers/gsls.py +++ b/qiskit_machine_learning/optimizers/gsls.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2018, 2023. +# (C) Copyright IBM 2018, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/optimizers/imfil.py b/qiskit_machine_learning/optimizers/imfil.py index 604c65efb..7273a4bac 100644 --- a/qiskit_machine_learning/optimizers/imfil.py +++ b/qiskit_machine_learning/optimizers/imfil.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2019, 2023. +# (C) Copyright IBM 2019, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/optimizers/l_bfgs_b.py b/qiskit_machine_learning/optimizers/l_bfgs_b.py index 1c529e0a3..0560e454d 100644 --- a/qiskit_machine_learning/optimizers/l_bfgs_b.py +++ b/qiskit_machine_learning/optimizers/l_bfgs_b.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2018, 2023. +# (C) Copyright IBM 2018, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/optimizers/nelder_mead.py b/qiskit_machine_learning/optimizers/nelder_mead.py index a8c3a264b..8109b3f48 100644 --- a/qiskit_machine_learning/optimizers/nelder_mead.py +++ b/qiskit_machine_learning/optimizers/nelder_mead.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2018, 2023. +# (C) Copyright IBM 2018, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/optimizers/nft.py b/qiskit_machine_learning/optimizers/nft.py index 504e5f730..80410d7f8 100644 --- a/qiskit_machine_learning/optimizers/nft.py +++ b/qiskit_machine_learning/optimizers/nft.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2019, 2023. +# (C) Copyright IBM 2019, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/optimizers/nlopts/__init__.py b/qiskit_machine_learning/optimizers/nlopts/__init__.py index 8aea0aea1..b5fcd3d6c 100644 --- a/qiskit_machine_learning/optimizers/nlopts/__init__.py +++ b/qiskit_machine_learning/optimizers/nlopts/__init__.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2018, 2023. +# (C) Copyright IBM 2018, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/optimizers/nlopts/crs.py b/qiskit_machine_learning/optimizers/nlopts/crs.py index ec30e236e..c28209d89 100644 --- a/qiskit_machine_learning/optimizers/nlopts/crs.py +++ b/qiskit_machine_learning/optimizers/nlopts/crs.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2018, 2023. +# (C) Copyright IBM 2018, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/optimizers/nlopts/direct_l.py b/qiskit_machine_learning/optimizers/nlopts/direct_l.py index 9ee350675..35e595973 100644 --- a/qiskit_machine_learning/optimizers/nlopts/direct_l.py +++ b/qiskit_machine_learning/optimizers/nlopts/direct_l.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2018, 2023. +# (C) Copyright IBM 2018, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/optimizers/nlopts/direct_l_rand.py b/qiskit_machine_learning/optimizers/nlopts/direct_l_rand.py index 3c4a90da0..0d0d0fd9d 100644 --- a/qiskit_machine_learning/optimizers/nlopts/direct_l_rand.py +++ b/qiskit_machine_learning/optimizers/nlopts/direct_l_rand.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2018, 2023. +# (C) Copyright IBM 2018, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/optimizers/nlopts/esch.py b/qiskit_machine_learning/optimizers/nlopts/esch.py index f2687902b..28a619101 100644 --- a/qiskit_machine_learning/optimizers/nlopts/esch.py +++ b/qiskit_machine_learning/optimizers/nlopts/esch.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2018, 2023. +# (C) Copyright IBM 2018, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/optimizers/nlopts/isres.py b/qiskit_machine_learning/optimizers/nlopts/isres.py index 1af1f501e..5353a4b67 100644 --- a/qiskit_machine_learning/optimizers/nlopts/isres.py +++ b/qiskit_machine_learning/optimizers/nlopts/isres.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2018, 2023. +# (C) Copyright IBM 2018, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/optimizers/nlopts/nloptimizer.py b/qiskit_machine_learning/optimizers/nlopts/nloptimizer.py index b88584401..d04e1d4d1 100644 --- a/qiskit_machine_learning/optimizers/nlopts/nloptimizer.py +++ b/qiskit_machine_learning/optimizers/nlopts/nloptimizer.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2018, 2023. +# (C) Copyright IBM 2018, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/optimizers/optimizer.py b/qiskit_machine_learning/optimizers/optimizer.py index 9ad8fe668..b9effe9b8 100644 --- a/qiskit_machine_learning/optimizers/optimizer.py +++ b/qiskit_machine_learning/optimizers/optimizer.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2018, 2023. +# (C) Copyright IBM 2018, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/optimizers/optimizer_utils/__init__.py b/qiskit_machine_learning/optimizers/optimizer_utils/__init__.py index faa3f6d53..bf77c4120 100644 --- a/qiskit_machine_learning/optimizers/optimizer_utils/__init__.py +++ b/qiskit_machine_learning/optimizers/optimizer_utils/__init__.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023. +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/optimizers/optimizer_utils/learning_rate.py b/qiskit_machine_learning/optimizers/optimizer_utils/learning_rate.py index 3cb070773..136fbd4c1 100644 --- a/qiskit_machine_learning/optimizers/optimizer_utils/learning_rate.py +++ b/qiskit_machine_learning/optimizers/optimizer_utils/learning_rate.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2021, 2023. +# (C) Copyright IBM 2021, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/optimizers/p_bfgs.py b/qiskit_machine_learning/optimizers/p_bfgs.py index 7ed42dff5..a18d1f9d0 100644 --- a/qiskit_machine_learning/optimizers/p_bfgs.py +++ b/qiskit_machine_learning/optimizers/p_bfgs.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2018, 2023. +# (C) Copyright IBM 2018, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/optimizers/powell.py b/qiskit_machine_learning/optimizers/powell.py index 1b53131d4..96842db36 100644 --- a/qiskit_machine_learning/optimizers/powell.py +++ b/qiskit_machine_learning/optimizers/powell.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2018, 2023. +# (C) Copyright IBM 2018, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/optimizers/qnspsa.py b/qiskit_machine_learning/optimizers/qnspsa.py index aa84f5ab4..1f7db8d84 100644 --- a/qiskit_machine_learning/optimizers/qnspsa.py +++ b/qiskit_machine_learning/optimizers/qnspsa.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2021, 2023. +# (C) Copyright IBM 2021, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/optimizers/scipy_optimizer.py b/qiskit_machine_learning/optimizers/scipy_optimizer.py index 789859e85..631649863 100644 --- a/qiskit_machine_learning/optimizers/scipy_optimizer.py +++ b/qiskit_machine_learning/optimizers/scipy_optimizer.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2018, 2023. +# (C) Copyright IBM 2018, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/optimizers/slsqp.py b/qiskit_machine_learning/optimizers/slsqp.py index 20eb49ef2..facbfdbce 100644 --- a/qiskit_machine_learning/optimizers/slsqp.py +++ b/qiskit_machine_learning/optimizers/slsqp.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2018, 2023. +# (C) Copyright IBM 2018, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/optimizers/snobfit.py b/qiskit_machine_learning/optimizers/snobfit.py index 0e48e2229..5f36b7f5e 100644 --- a/qiskit_machine_learning/optimizers/snobfit.py +++ b/qiskit_machine_learning/optimizers/snobfit.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2019, 2023. +# (C) Copyright IBM 2019, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/optimizers/steppable_optimizer.py b/qiskit_machine_learning/optimizers/steppable_optimizer.py index e9ffda9be..f50bbc874 100644 --- a/qiskit_machine_learning/optimizers/steppable_optimizer.py +++ b/qiskit_machine_learning/optimizers/steppable_optimizer.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023. +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/optimizers/tnc.py b/qiskit_machine_learning/optimizers/tnc.py index 1d9665b5f..13d50d29b 100644 --- a/qiskit_machine_learning/optimizers/tnc.py +++ b/qiskit_machine_learning/optimizers/tnc.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2018, 2023. +# (C) Copyright IBM 2018, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/optimizers/umda.py b/qiskit_machine_learning/optimizers/umda.py index 2b9eda872..375a7711f 100644 --- a/qiskit_machine_learning/optimizers/umda.py +++ b/qiskit_machine_learning/optimizers/umda.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2018, 2023. +# (C) Copyright IBM 2018, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/state_fidelities/__init__.py b/qiskit_machine_learning/state_fidelities/__init__.py index 47e89f31f..231e23242 100644 --- a/qiskit_machine_learning/state_fidelities/__init__.py +++ b/qiskit_machine_learning/state_fidelities/__init__.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023. +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/state_fidelities/base_state_fidelity.py b/qiskit_machine_learning/state_fidelities/base_state_fidelity.py index 4ca041edf..c7ec26391 100644 --- a/qiskit_machine_learning/state_fidelities/base_state_fidelity.py +++ b/qiskit_machine_learning/state_fidelities/base_state_fidelity.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023. +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/state_fidelities/compute_uncompute.py b/qiskit_machine_learning/state_fidelities/compute_uncompute.py index b16e40115..3453b2081 100644 --- a/qiskit_machine_learning/state_fidelities/compute_uncompute.py +++ b/qiskit_machine_learning/state_fidelities/compute_uncompute.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023. +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/state_fidelities/state_fidelity_result.py b/qiskit_machine_learning/state_fidelities/state_fidelity_result.py index 6dc26dbf3..683167ee7 100644 --- a/qiskit_machine_learning/state_fidelities/state_fidelity_result.py +++ b/qiskit_machine_learning/state_fidelities/state_fidelity_result.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023. +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/utils/__init__.py b/qiskit_machine_learning/utils/__init__.py index f56c0936b..70873e3e7 100644 --- a/qiskit_machine_learning/utils/__init__.py +++ b/qiskit_machine_learning/utils/__init__.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2021, 2023. +# (C) Copyright IBM 2021, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/utils/adjust_num_qubits.py b/qiskit_machine_learning/utils/adjust_num_qubits.py index 69cd17d43..84ae8eb85 100644 --- a/qiskit_machine_learning/utils/adjust_num_qubits.py +++ b/qiskit_machine_learning/utils/adjust_num_qubits.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023. +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/utils/algorithm_globals.py b/qiskit_machine_learning/utils/algorithm_globals.py index 2fcb74e1a..c762dba6e 100644 --- a/qiskit_machine_learning/utils/algorithm_globals.py +++ b/qiskit_machine_learning/utils/algorithm_globals.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2019, 2023. +# (C) Copyright IBM 2019, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/utils/optionals.py b/qiskit_machine_learning/utils/optionals.py index 472157bea..30e9b517e 100644 --- a/qiskit_machine_learning/utils/optionals.py +++ b/qiskit_machine_learning/utils/optionals.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023. +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/utils/set_batching.py b/qiskit_machine_learning/utils/set_batching.py index f0ec5b5f1..94d2624d0 100644 --- a/qiskit_machine_learning/utils/set_batching.py +++ b/qiskit_machine_learning/utils/set_batching.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023. +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/utils/validate_bounds.py b/qiskit_machine_learning/utils/validate_bounds.py index f0a801213..2affd7b8c 100644 --- a/qiskit_machine_learning/utils/validate_bounds.py +++ b/qiskit_machine_learning/utils/validate_bounds.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023. +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/utils/validate_initial_point.py b/qiskit_machine_learning/utils/validate_initial_point.py index 0bdd9eeb0..ad7f8ba5b 100644 --- a/qiskit_machine_learning/utils/validate_initial_point.py +++ b/qiskit_machine_learning/utils/validate_initial_point.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023. +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/utils/validation.py b/qiskit_machine_learning/utils/validation.py index ae838d8d5..edd8e06e1 100644 --- a/qiskit_machine_learning/utils/validation.py +++ b/qiskit_machine_learning/utils/validation.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2019, 2023. +# (C) Copyright IBM 2019, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/qiskit_machine_learning/variational_algorithm.py b/qiskit_machine_learning/variational_algorithm.py index aa295616e..ad5ddd30a 100644 --- a/qiskit_machine_learning/variational_algorithm.py +++ b/qiskit_machine_learning/variational_algorithm.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2019, 2023. +# (C) Copyright IBM 2019, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/test/__init__.py b/test/__init__.py index 7b29e8d6d..3d89bce89 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2020, 2023. +# (C) Copyright IBM 2020, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/test/algorithms/classifiers/test_fidelity_quantum_kernel_pegasos_qsvc.py b/test/algorithms/classifiers/test_fidelity_quantum_kernel_pegasos_qsvc.py index 11f1812f2..123bf3aa6 100644 --- a/test/algorithms/classifiers/test_fidelity_quantum_kernel_pegasos_qsvc.py +++ b/test/algorithms/classifiers/test_fidelity_quantum_kernel_pegasos_qsvc.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2021, 2023. +# (C) Copyright IBM 2021, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/test/algorithms/classifiers/test_fidelity_quantum_kernel_qsvc.py b/test/algorithms/classifiers/test_fidelity_quantum_kernel_qsvc.py index 643b27c0e..c56ad53e5 100644 --- a/test/algorithms/classifiers/test_fidelity_quantum_kernel_qsvc.py +++ b/test/algorithms/classifiers/test_fidelity_quantum_kernel_qsvc.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2021, 2023. +# (C) Copyright IBM 2021, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/test/algorithms/classifiers/test_neural_network_classifier.py b/test/algorithms/classifiers/test_neural_network_classifier.py index 5f1613b00..1bdd64cd5 100644 --- a/test/algorithms/classifiers/test_neural_network_classifier.py +++ b/test/algorithms/classifiers/test_neural_network_classifier.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023. +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/test/algorithms/classifiers/test_pegasos_qsvc.py b/test/algorithms/classifiers/test_pegasos_qsvc.py index b5a16415e..a6f888f4d 100644 --- a/test/algorithms/classifiers/test_pegasos_qsvc.py +++ b/test/algorithms/classifiers/test_pegasos_qsvc.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2021, 2023. +# (C) Copyright IBM 2021, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/test/algorithms/classifiers/test_qsvc.py b/test/algorithms/classifiers/test_qsvc.py index 808765a56..5b543c783 100644 --- a/test/algorithms/classifiers/test_qsvc.py +++ b/test/algorithms/classifiers/test_qsvc.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2021, 2023. +# (C) Copyright IBM 2021, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/test/algorithms/classifiers/test_vqc.py b/test/algorithms/classifiers/test_vqc.py index 4a2aca7cd..9d252e8c8 100644 --- a/test/algorithms/classifiers/test_vqc.py +++ b/test/algorithms/classifiers/test_vqc.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2018, 2023. +# (C) Copyright IBM 2018, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/test/algorithms/regressors/test_fidelity_quantum_kernel_qsvr.py b/test/algorithms/regressors/test_fidelity_quantum_kernel_qsvr.py index d5dcc9cb9..50130d756 100644 --- a/test/algorithms/regressors/test_fidelity_quantum_kernel_qsvr.py +++ b/test/algorithms/regressors/test_fidelity_quantum_kernel_qsvr.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2021, 2023. +# (C) Copyright IBM 2021, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/test/algorithms/regressors/test_neural_network_regressor.py b/test/algorithms/regressors/test_neural_network_regressor.py index da5db70c6..6ff26d543 100644 --- a/test/algorithms/regressors/test_neural_network_regressor.py +++ b/test/algorithms/regressors/test_neural_network_regressor.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023. +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/test/algorithms/regressors/test_vqr.py b/test/algorithms/regressors/test_vqr.py index c2570598d..67dde8c03 100644 --- a/test/algorithms/regressors/test_vqr.py +++ b/test/algorithms/regressors/test_vqr.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023. +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/test/algorithms_test_case.py b/test/algorithms_test_case.py index d6e29b252..c8e5abe07 100644 --- a/test/algorithms_test_case.py +++ b/test/algorithms_test_case.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2018, 2023. +# (C) Copyright IBM 2018, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/test/circuit/library/test_raw_feature_vector.py b/test/circuit/library/test_raw_feature_vector.py index 87643d311..5b8ed9363 100644 --- a/test/circuit/library/test_raw_feature_vector.py +++ b/test/circuit/library/test_raw_feature_vector.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2020, 2023. +# (C) Copyright IBM 2020, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/test/connectors/test_torch.py b/test/connectors/test_torch.py index fd772f117..a9d4dde22 100644 --- a/test/connectors/test_torch.py +++ b/test/connectors/test_torch.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023. +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/test/datasets/test_ad_hoc_data.py b/test/datasets/test_ad_hoc_data.py index f044aff39..5f5376925 100644 --- a/test/datasets/test_ad_hoc_data.py +++ b/test/datasets/test_ad_hoc_data.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2020, 2023. +# (C) Copyright IBM 2020, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/test/gradients/__init__.py b/test/gradients/__init__.py index 3f5bc2144..e448b5e28 100644 --- a/test/gradients/__init__.py +++ b/test/gradients/__init__.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023. +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/test/gradients/logging_primitives.py b/test/gradients/logging_primitives.py index e9a9f21c5..f5ffeae0e 100644 --- a/test/gradients/logging_primitives.py +++ b/test/gradients/logging_primitives.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023. +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/test/gradients/test_qfi.py b/test/gradients/test_qfi.py index b34fa59f5..8acbcaf09 100644 --- a/test/gradients/test_qfi.py +++ b/test/gradients/test_qfi.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023. +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/test/gradients/test_qgt.py b/test/gradients/test_qgt.py index 7fa6c48c3..cdab18353 100644 --- a/test/gradients/test_qgt.py +++ b/test/gradients/test_qgt.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023. +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/test/kernels/algorithms/test_fidelity_qkernel_trainer.py b/test/kernels/algorithms/test_fidelity_qkernel_trainer.py index dad548a03..d6e08ed93 100644 --- a/test/kernels/algorithms/test_fidelity_qkernel_trainer.py +++ b/test/kernels/algorithms/test_fidelity_qkernel_trainer.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2021, 2023. +# (C) Copyright IBM 2021, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/test/neural_networks/test_effective_dimension.py b/test/neural_networks/test_effective_dimension.py index 149b2428b..ce3944548 100644 --- a/test/neural_networks/test_effective_dimension.py +++ b/test/neural_networks/test_effective_dimension.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023. +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/test/neural_networks/test_sampler_qnn.py b/test/neural_networks/test_sampler_qnn.py index 744c3f6f9..8084b5109 100644 --- a/test/neural_networks/test_sampler_qnn.py +++ b/test/neural_networks/test_sampler_qnn.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023. +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/test/optimizers/__init__.py b/test/optimizers/__init__.py index d68c98527..c9d7aa94a 100644 --- a/test/optimizers/__init__.py +++ b/test/optimizers/__init__.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2021, 2023. +# (C) Copyright IBM 2021, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/test/optimizers/test_gradient_descent.py b/test/optimizers/test_gradient_descent.py index eada59d8a..5dcc05223 100644 --- a/test/optimizers/test_gradient_descent.py +++ b/test/optimizers/test_gradient_descent.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2021, 2023. +# (C) Copyright IBM 2021, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/test/optimizers/test_optimizers_scikitquant.py b/test/optimizers/test_optimizers_scikitquant.py index 5b797a226..f4ee39315 100644 --- a/test/optimizers/test_optimizers_scikitquant.py +++ b/test/optimizers/test_optimizers_scikitquant.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2020, 2023. +# (C) Copyright IBM 2020, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/test/optimizers/test_spsa.py b/test/optimizers/test_spsa.py index 5ce5d69e9..80afce43e 100644 --- a/test/optimizers/test_spsa.py +++ b/test/optimizers/test_spsa.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2021, 2023. +# (C) Copyright IBM 2021, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/test/optimizers/test_umda.py b/test/optimizers/test_umda.py index fb085014b..571cf9fb7 100644 --- a/test/optimizers/test_umda.py +++ b/test/optimizers/test_umda.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2021, 2023. +# (C) Copyright IBM 2021, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/test/optimizers/utils/__init__.py b/test/optimizers/utils/__init__.py index a8e8b1a03..d8993a914 100644 --- a/test/optimizers/utils/__init__.py +++ b/test/optimizers/utils/__init__.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023. +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/test/optimizers/utils/test_learning_rate.py b/test/optimizers/utils/test_learning_rate.py index 418cd4a75..a14ff3277 100644 --- a/test/optimizers/utils/test_learning_rate.py +++ b/test/optimizers/utils/test_learning_rate.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023. +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/test/state_fidelities/__init__.py b/test/state_fidelities/__init__.py index 3e84d2eaa..ded433ae5 100644 --- a/test/state_fidelities/__init__.py +++ b/test/state_fidelities/__init__.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023. +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/test/state_fidelities/test_compute_uncompute.py b/test/state_fidelities/test_compute_uncompute.py index cd209343b..b498e29fa 100644 --- a/test/state_fidelities/test_compute_uncompute.py +++ b/test/state_fidelities/test_compute_uncompute.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023. +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/test/utils/test_validate_bounds.py b/test/utils/test_validate_bounds.py index bfdb8e6bc..6dd723599 100644 --- a/test/utils/test_validate_bounds.py +++ b/test/utils/test_validate_bounds.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023. +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory diff --git a/test/utils/test_validate_initial_point.py b/test/utils/test_validate_initial_point.py index bc0ac7368..d0fc9d309 100644 --- a/test/utils/test_validate_initial_point.py +++ b/test/utils/test_validate_initial_point.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2022, 2023. +# (C) Copyright IBM 2022, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory From f4d49eb089a91344bd7e598181c7d9a76b448b8e Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Thu, 18 Jul 2024 13:45:48 +0100 Subject: [PATCH 29/85] Ignore mypy in 8 instances --- qiskit_machine_learning/algorithms/trainable_model.py | 6 +++--- .../kernels/algorithms/quantum_kernel_trainer.py | 10 +++++----- .../kernels/fidelity_quantum_kernel.py | 8 ++++---- .../neural_networks/estimator_qnn.py | 4 ++-- qiskit_machine_learning/neural_networks/sampler_qnn.py | 4 ++-- .../classifiers/test_neural_network_classifier.py | 4 ++-- .../regressors/test_neural_network_regressor.py | 4 ++-- test/kernels/test_fidelity_qkernel.py | 4 ++-- 8 files changed, 22 insertions(+), 22 deletions(-) diff --git a/qiskit_machine_learning/algorithms/trainable_model.py b/qiskit_machine_learning/algorithms/trainable_model.py index 0226d925f..ccc866d35 100644 --- a/qiskit_machine_learning/algorithms/trainable_model.py +++ b/qiskit_machine_learning/algorithms/trainable_model.py @@ -244,7 +244,7 @@ def _choose_initial_point(self) -> np.ndarray: An array as an initial point """ if self._warm_start and self._fit_result is not None: - self._initial_point = self._fit_result.x + self._initial_point = self._fit_result.x # type: ignore[assignment] elif self._initial_point is None: self._initial_point = algorithm_globals.random.random(self._neural_network.num_weights) return self._initial_point @@ -287,13 +287,13 @@ def _minimize(self, function: ObjectiveFunction) -> OptimizerResult: initial_point = self._choose_initial_point() if callable(self._optimizer): - optimizer_result = self._optimizer( + optimizer_result = self._optimizer( # type: ignore[call-arg] fun=objective, x0=initial_point, jac=function.gradient ) else: optimizer_result = self._optimizer.minimize( fun=objective, x0=initial_point, - jac=function.gradient, + jac=function.gradient, # type: ignore[arg-type] ) return optimizer_result diff --git a/qiskit_machine_learning/kernels/algorithms/quantum_kernel_trainer.py b/qiskit_machine_learning/kernels/algorithms/quantum_kernel_trainer.py index 889a466ba..3bbac7d0e 100644 --- a/qiskit_machine_learning/kernels/algorithms/quantum_kernel_trainer.py +++ b/qiskit_machine_learning/kernels/algorithms/quantum_kernel_trainer.py @@ -199,27 +199,27 @@ def fit( # Randomly initialize the initial point if one was not passed if self._initial_point is None: - self._initial_point = algorithm_globals.random.random(num_params) + self._initial_point = algorithm_globals.random.random(num_params) # type: ignore[assignment] # Perform kernel optimization loss_function = partial( self._loss.evaluate, quantum_kernel=self.quantum_kernel, data=data, labels=labels ) if callable(self._optimizer): - opt_results = self._optimizer(fun=loss_function, x0=self._initial_point) + opt_results = self._optimizer(fun=loss_function, x0=self._initial_point) # type: ignore[call-arg, arg-type] else: opt_results = self._optimizer.minimize( fun=loss_function, - x0=self._initial_point, + x0=self._initial_point, # type: ignore[arg-type] ) # Return kernel training results result = QuantumKernelTrainerResult() result.optimizer_evals = opt_results.nfev result.optimal_value = opt_results.fun - result.optimal_point = opt_results.x + result.optimal_point = opt_results.x # type: ignore[assignment] result.optimal_parameters = dict( - zip(self.quantum_kernel.training_parameters, opt_results.x) + zip(self.quantum_kernel.training_parameters, opt_results.x) # type: ignore[arg-type] ) # Return the QuantumKernel in optimized state diff --git a/qiskit_machine_learning/kernels/fidelity_quantum_kernel.py b/qiskit_machine_learning/kernels/fidelity_quantum_kernel.py index 31a7cf426..5de704e53 100644 --- a/qiskit_machine_learning/kernels/fidelity_quantum_kernel.py +++ b/qiskit_machine_learning/kernels/fidelity_quantum_kernel.py @@ -229,8 +229,8 @@ def _get_kernel_entries( job = self._fidelity.run( [self._feature_map] * num_circuits, [self._feature_map] * num_circuits, - left_parameters, - right_parameters, + left_parameters, # type: ignore[arg-type] + right_parameters, # type: ignore[arg-type] ) kernel_entries = job.result().fidelities else: @@ -249,8 +249,8 @@ def _get_kernel_entries( job = self._fidelity.run( [self._feature_map] * (end_idx - start_idx), [self._feature_map] * (end_idx - start_idx), - chunk_left_parameters, - chunk_right_parameters, + chunk_left_parameters, # type: ignore[arg-type] + chunk_right_parameters, # type: ignore[arg-type] ) # Extend the kernel_entries list with the results from this chunk kernel_entries.extend(job.result().fidelities) diff --git a/qiskit_machine_learning/neural_networks/estimator_qnn.py b/qiskit_machine_learning/neural_networks/estimator_qnn.py index 7d4223924..6aff892a5 100644 --- a/qiskit_machine_learning/neural_networks/estimator_qnn.py +++ b/qiskit_machine_learning/neural_networks/estimator_qnn.py @@ -270,10 +270,10 @@ def _backward( job = None if self._input_gradients: - job = self.gradient.run(circuits, observables, param_values) + job = self.gradient.run(circuits, observables, param_values) # type: ignore[arg-type] elif len(parameter_values[0]) > self._num_inputs: params = [self._circuit.parameters[self._num_inputs :]] * num_circuits - job = self.gradient.run(circuits, observables, param_values, parameters=params) + job = self.gradient.run(circuits, observables, param_values, parameters=params) # type: ignore[arg-type] if job is not None: try: diff --git a/qiskit_machine_learning/neural_networks/sampler_qnn.py b/qiskit_machine_learning/neural_networks/sampler_qnn.py index 81d5f615d..0180afad9 100644 --- a/qiskit_machine_learning/neural_networks/sampler_qnn.py +++ b/qiskit_machine_learning/neural_networks/sampler_qnn.py @@ -414,10 +414,10 @@ def _backward( job = None if self._input_gradients: - job = self.gradient.run(circuits, parameter_values) + job = self.gradient.run(circuits, parameter_values) # type: ignore[arg-type] elif len(parameter_values[0]) > self._num_inputs: params = [self._circuit.parameters[self._num_inputs :]] * num_samples - job = self.gradient.run(circuits, parameter_values, parameters=params) + job = self.gradient.run(circuits, parameter_values, parameters=params) # type: ignore[arg-type] if job is not None: try: diff --git a/test/algorithms/classifiers/test_neural_network_classifier.py b/test/algorithms/classifiers/test_neural_network_classifier.py index 1bdd64cd5..251982bd2 100644 --- a/test/algorithms/classifiers/test_neural_network_classifier.py +++ b/test/algorithms/classifiers/test_neural_network_classifier.py @@ -61,9 +61,9 @@ def _create_optimizer(self, opt: str) -> Optimizer | None: if opt == "bfgs": optimizer = L_BFGS_B(maxiter=5) elif opt == "cobyla": - optimizer = COBYLA(maxiter=25) + optimizer = COBYLA(maxiter=25) # type: ignore[assignment] elif opt == "callable": - optimizer = partial(minimize, method="COBYLA", options={"maxiter": 25}) + optimizer = partial(minimize, method="COBYLA", options={"maxiter": 25}) # type: ignore[assignment] else: optimizer = None diff --git a/test/algorithms/regressors/test_neural_network_regressor.py b/test/algorithms/regressors/test_neural_network_regressor.py index 6ff26d543..57798049c 100644 --- a/test/algorithms/regressors/test_neural_network_regressor.py +++ b/test/algorithms/regressors/test_neural_network_regressor.py @@ -80,9 +80,9 @@ def _create_regressor( if opt == "bfgs": optimizer = L_BFGS_B(maxiter=5) elif opt == "cobyla": - optimizer = COBYLA(maxiter=25) + optimizer = COBYLA(maxiter=25) # type: ignore[assignment] elif opt == "callable": - optimizer = partial(minimize, method="COBYLA", options={"maxiter": 25}) + optimizer = partial(minimize, method="COBYLA", options={"maxiter": 25}) # type: ignore[assignment] else: optimizer = None diff --git a/test/kernels/test_fidelity_qkernel.py b/test/kernels/test_fidelity_qkernel.py index 173f4a01f..51b9a5b45 100644 --- a/test/kernels/test_fidelity_qkernel.py +++ b/test/kernels/test_fidelity_qkernel.py @@ -279,7 +279,7 @@ def create_fidelity_circuit( ) -> QuantumCircuit: raise NotImplementedError() - def _run( + def _run( # type: ignore[override] self, circuits_1: QuantumCircuit | Sequence[QuantumCircuit], circuits_2: QuantumCircuit | Sequence[QuantumCircuit], @@ -294,7 +294,7 @@ def _run( @staticmethod def _call(fidelities, options) -> StateFidelityResult: - return StateFidelityResult(fidelities, [], {}, options) + return StateFidelityResult(fidelities, [], {}, options) # type: ignore[arg-type] with self.subTest("No PSD enforcement"): kernel = FidelityQuantumKernel(fidelity=MockFidelity(), enforce_psd=False) From 2d1209f39889dfb9c095a90d86ab3f572eaf7911 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Mon, 29 Jul 2024 17:12:18 +0200 Subject: [PATCH 30/85] Merge spell dicts --- .pylintdict | 290 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 280 insertions(+), 10 deletions(-) diff --git a/.pylintdict b/.pylintdict index 7aff18f67..be1f3b986 100644 --- a/.pylintdict +++ b/.pylintdict @@ -1,15 +1,26 @@ acyclic adam adjoint +ae aer al alan +algo +algorithmerror +allclose +amsgrad ancilla ancillas ansatz +ansatz's ansatzes +ap +apl +arg +argmax args -Armijo +armijo +arxiv asmatrix aspuru async @@ -17,156 +28,313 @@ autoencoder autoencoders autograd autosummary +ba backend backends backpropagation +barison +barkoutsos +batchsize bayes bayesian benchmarking bergholm +bfgs +bielza bitstring bitstrings bivariate bloch +boltzmann bool boolean borujeni +boyer +brassard +broyden +callables +cambridge +cancelled cargs +carleo carlo cbit centroid +chernoff choi chuang clbit clbits +clopper +cobyla +codebase codec +coeffs colin +combinatorial +concha config +configs +confint cong contravariance convolutional cpu creg crossentropyloss +crs csr ctrl ctx currentmodule +customizable +cvar cvs cx córcoles data's +dataclass +dataframe dataloader datapoints dataset datasets deepcopy denoising +deriv deterministically diag dicts +diederik dimensionality dir discretization discretize discretized discriminative +disp distro +dobsicek docstring +doi dok +dp dt +dω +eda +egger +eigen +eigenphase +eigenproblem +eigensolver +eigensolvers eigenstate eigenstates -einsum einstein +einsum +elif endian entangler +enum eps estimatorqnn et +euler eval +evals +evolutions +evolutionsynthesis +evolver +evolvers +excitations +exponentials +exponentiated +exponentiating expressibility +f'spsa +fae +failsafe +farhi farrokh +fi fidelities fidelityquantumkernel +filippo +fletcher +fm +fmin formatter +fourier frac +frobenius frontend +ftol +fujii func -Gacon +functools +gacon gambetta gaussian gellmann +generalised getter -Gogolin +getters +gilliam +giuseppe +globals +gogolin +goldfarb +goldstone +gonciulea gpu +gqt +grinko grover +gsls +gtol guang +gutmann guzik +hadamard +hadamards +hadfield hamiltonian +hamiltonians hao hashable +hatano havlíček +heidelberg +hessians hilbert hoc +hopkins +hoyer html +https hubbard hyperparameter hyperparameters hyperplanes +idx im +imag init +initializer inlier inplace instantiation instantiations +interatomic interdependencies +ints +iprint +iqft isaac +ising isometry iten iterable iteratively -Izaac +iz +izaac +izz +jac jacobian jm jonathan +jones +july jupyter kandala kernelized -Killoran +killoran +kingma +kitagawa +kraft +kth +kumar kwarg kwargs labelled lagrange langle +larrañaga lcu +len +leq +lin +linalg +loglik +loglikelihood +lov +lr +lsb +lse lukin macos +magnus makefile +marecek mary +masuo +mathcal +mathrm matmul matplotlib -mathrm +maxcut +maxfail +maxfev +maxfun maxiter +maxiters +maxmp mcrx mcry mcrz +mcz +michael +minimised minimizer +minwidth misclassified +mitarai mixin +mle +modelspace monte +mosca mpl +mprev multiclass multinomial multioutput mxd mypy +nabla +nakaji +nakanishi +nan +nannicini +naomichi nat +nathan nbsphinx +nd ndarray +negoro +nelder +nevals +newtons's +nfev +nfevs +nft nielsen +njev +nlopt nn noancilla +nones nonlocal nosignatures np @@ -175,9 +343,13 @@ num numpy nxd nxm +o'brien +objval observables +oct olson onboarding +onodera opflow optim optimizer's @@ -189,28 +361,46 @@ overfitted overfitting ovo ovr +parallelization +parallelized param parameterization +parameterizations +parametrization +parametrizations parametrize parametrized params pauli +paulis +pearson +pedro pegasos +peruzzo pixelated platt +polyfit +postprocess +powell pre precompute precomputed precomputes precomputing preconditioner +prepend preprint +preprocess +preprocesses priori +proj ps +pvqd pxd py pytorch qae +qaoa qarg qargs qasm @@ -218,74 +408,128 @@ qb qbayesian qbi qc +qdrift qfi +qfi's +qfis qgan qgans qgt +qgt's +qgts qiskit qiskit's qn qnn +qnspsa +qpe qsvc qsvr +quadratically +quant +quantile quantumcircuit qubit qubits rangle +raymond rbf readme recalibration +reddi regressor regressors regs +representable reproducibility resamplings rescale +rescaled +rescaling +retval +rhobeg rhs +rightarrow +robert romero +rosen +runarsson runtime runtimes rx ry rz samplerqnn +sanjiv +sashank +satisfiability +satyen scalability -Schuld +schroediger +schroedinger +schrödinger +schuld scikit scipy +sdg +seealso semidefinite +serializable shalev +shanno shende shwartz sigmoid sima sklearn +skquant +sle +slsqp softmax +soloviev +spall sparsearray +spedalieri spsa sqrt +statefn statevector statevectors -stdlib +stddev stdlib stdout +stefano +steppable stepsize str -subclasses subcircuits +subclassed +subclasses +subcomponents submodules subobjects subseteq +subspaces +suzuki svc svm svr +swappable sx sy +tanaka +tapp +tarasinski +tavernelli temme tensored +terhal terra th theodore +timestep +timesteps +tnc toctree todo tol @@ -298,29 +542,55 @@ transpiled transpiler transpiles transpiling +trotterization +trotterized tunable uncompiled uncompress uncompute unitaries univariate +uno +unscaled unsymmetric utf utils +varadarajan variational vatan vec +vectorized +veeravalli +vicentini +vigo +ville +voilà vqc +vqd vqe vqr vx vy vz +wavefunction wikipedia +wilhelm williams +woerner wrt +xatol +xopt +xtol +yamamoto +yao yoder +york +yy +yz +zi zoufal zsh zz θ +ψ +ω From 2735810808db765a18482d63ea1e721e17f55a6a Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Mon, 29 Jul 2024 17:19:17 +0200 Subject: [PATCH 31/85] Black reformatting --- qiskit_machine_learning/algorithm_result.py | 1 - qiskit_machine_learning/connectors/torch_connector.py | 1 - .../neural_networks/effective_dimension.py | 1 - qiskit_machine_learning/neural_networks/sampler_qnn.py | 1 - qiskit_machine_learning/optimizers/aqgd.py | 2 +- qiskit_machine_learning/optimizers/gradient_descent.py | 1 - qiskit_machine_learning/optimizers/gsls.py | 1 - qiskit_machine_learning/optimizers/nft.py | 1 - qiskit_machine_learning/optimizers/umda.py | 1 - .../state_fidelities/base_state_fidelity.py | 7 ++----- test/connectors/test_torch_connector.py | 1 - 11 files changed, 3 insertions(+), 15 deletions(-) diff --git a/qiskit_machine_learning/algorithm_result.py b/qiskit_machine_learning/algorithm_result.py index 95b45d829..0d1956f01 100644 --- a/qiskit_machine_learning/algorithm_result.py +++ b/qiskit_machine_learning/algorithm_result.py @@ -31,7 +31,6 @@ def __str__(self) -> str: and not inspect.isfunction(value) and hasattr(self, name) ): - result[name] = value return pprint.pformat(result, indent=4) diff --git a/qiskit_machine_learning/connectors/torch_connector.py b/qiskit_machine_learning/connectors/torch_connector.py index cc120a514..c1439defc 100644 --- a/qiskit_machine_learning/connectors/torch_connector.py +++ b/qiskit_machine_learning/connectors/torch_connector.py @@ -103,7 +103,6 @@ class TorchConnector(Module): # pylint: disable=abstract-method class _TorchNNFunction(Function): - # pylint: disable=arguments-differ @staticmethod def forward( # type: ignore diff --git a/qiskit_machine_learning/neural_networks/effective_dimension.py b/qiskit_machine_learning/neural_networks/effective_dimension.py index 275382607..1411b9bf8 100644 --- a/qiskit_machine_learning/neural_networks/effective_dimension.py +++ b/qiskit_machine_learning/neural_networks/effective_dimension.py @@ -252,7 +252,6 @@ def _get_effective_dimension( normalized_fisher: np.ndarray, dataset_size: Union[List[int], np.ndarray, int], ) -> Union[np.ndarray, int]: - if not isinstance(dataset_size, int) and len(dataset_size) > 1: # expand dims for broadcasting normalized_fisher = np.expand_dims(normalized_fisher, axis=0) diff --git a/qiskit_machine_learning/neural_networks/sampler_qnn.py b/qiskit_machine_learning/neural_networks/sampler_qnn.py index 0180afad9..188bef764 100644 --- a/qiskit_machine_learning/neural_networks/sampler_qnn.py +++ b/qiskit_machine_learning/neural_networks/sampler_qnn.py @@ -329,7 +329,6 @@ def _postprocess_gradient( ) weights_grad = DOK((num_samples, *self._output_shape, self._num_weights)) else: - input_grad = ( np.zeros((num_samples, *self._output_shape, self._num_inputs)) if self._input_gradients diff --git a/qiskit_machine_learning/optimizers/aqgd.py b/qiskit_machine_learning/optimizers/aqgd.py index 2e49c83d8..ef5d0d703 100644 --- a/qiskit_machine_learning/optimizers/aqgd.py +++ b/qiskit_machine_learning/optimizers/aqgd.py @@ -325,7 +325,7 @@ def minimize( logger.info("Initial Params: %s", params) epoch = 0 converged = False - for (eta, mom_coeff) in zip(self._eta, self._momenta_coeff): + for eta, mom_coeff in zip(self._eta, self._momenta_coeff): logger.info("Epoch: %4d | Stepsize: %6.4f | Momentum: %6.4f", epoch, eta, mom_coeff) sum_max_iters = sum(self._maxiter[0 : epoch + 1]) diff --git a/qiskit_machine_learning/optimizers/gradient_descent.py b/qiskit_machine_learning/optimizers/gradient_descent.py index 65b0a43ec..4eafe5ebd 100644 --- a/qiskit_machine_learning/optimizers/gradient_descent.py +++ b/qiskit_machine_learning/optimizers/gradient_descent.py @@ -362,7 +362,6 @@ def start( jac: Callable[[POINT], POINT] | None = None, bounds: list[tuple[float, float]] | None = None, ) -> None: - self.state = GradientDescentState( fun=fun, jac=jac, diff --git a/qiskit_machine_learning/optimizers/gsls.py b/qiskit_machine_learning/optimizers/gsls.py index dd598ee8c..c6747c442 100644 --- a/qiskit_machine_learning/optimizers/gsls.py +++ b/qiskit_machine_learning/optimizers/gsls.py @@ -180,7 +180,6 @@ def ls_optimize( x_value = obj_fun(x) n_evals += 1 while iter_count < self._options["maxiter"] and n_evals < self._options["max_eval"]: - # Determine set of sample points directions, sample_set_x = self.sample_set(n, x, var_lb, var_ub, sample_set_size) diff --git a/qiskit_machine_learning/optimizers/nft.py b/qiskit_machine_learning/optimizers/nft.py index 80410d7f8..b76bfc983 100644 --- a/qiskit_machine_learning/optimizers/nft.py +++ b/qiskit_machine_learning/optimizers/nft.py @@ -119,7 +119,6 @@ def nakanishi_fujii_todo( funcalls = 0 while True: - idx = niter % x0.size if reset_interval > 0: diff --git a/qiskit_machine_learning/optimizers/umda.py b/qiskit_machine_learning/optimizers/umda.py index 375a7711f..e7cc84a15 100644 --- a/qiskit_machine_learning/optimizers/umda.py +++ b/qiskit_machine_learning/optimizers/umda.py @@ -211,7 +211,6 @@ def minimize( jac: Callable[[POINT], POINT] | None = None, bounds: list[tuple[float, float]] | None = None, ) -> OptimizerResult: - not_better_count = 0 result = OptimizerResult() diff --git a/qiskit_machine_learning/state_fidelities/base_state_fidelity.py b/qiskit_machine_learning/state_fidelities/base_state_fidelity.py index c7ec26391..7355ce2b8 100644 --- a/qiskit_machine_learning/state_fidelities/base_state_fidelity.py +++ b/qiskit_machine_learning/state_fidelities/base_state_fidelity.py @@ -43,7 +43,6 @@ class BaseStateFidelity(ABC): """ def __init__(self) -> None: - # use cache for preventing unnecessary circuit compositions self._circuit_cache: MutableMapping[tuple[int, int], QuantumCircuit] = {} @@ -82,7 +81,6 @@ def _preprocess_values( ) return [[]] else: - # Support ndarray if isinstance(values, np.ndarray): values = values.tolist() @@ -171,8 +169,7 @@ def _construct_circuits( ) circuits = [] - for (circuit_1, circuit_2) in zip(circuits_1, circuits_2): - + for circuit_1, circuit_2 in zip(circuits_1, circuits_2): # Use the same key for circuits as qiskit.primitives use. circuit = self._circuit_cache.get((_circuit_key(circuit_1), _circuit_key(circuit_2))) @@ -230,7 +227,7 @@ def _construct_value_list( elif len(values_1[0]) == 0: values = list(values_2) else: - for (val_1, val_2) in zip(values_1, values_2): + for val_1, val_2 in zip(values_1, values_2): # the `+` operation concatenates the lists # and then this new list gets appended to the values list values.append(val_1 + val_2) diff --git a/test/connectors/test_torch_connector.py b/test/connectors/test_torch_connector.py index f0f67d3d1..4da8b72b8 100644 --- a/test/connectors/test_torch_connector.py +++ b/test/connectors/test_torch_connector.py @@ -271,7 +271,6 @@ def __init__( kernel_size: int = 3, stride: int = 1, ): - super().__init__() self.kernel_size = kernel_size self.stride = stride From 840c2706e523cd33b1607d03090e22d9cdf46ba4 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Wed, 31 Jul 2024 10:23:04 +0200 Subject: [PATCH 32/85] Black reformatting --- .github/actions/install-machine-learning/action.yml | 1 - .github/workflows/main.yml | 2 +- constraints.txt | 2 +- .../kernels/algorithms/quantum_kernel_trainer.py | 4 +++- qiskit_machine_learning/neural_networks/estimator_qnn.py | 4 +++- qiskit_machine_learning/neural_networks/sampler_qnn.py | 4 +++- test/algorithms/classifiers/test_neural_network_classifier.py | 4 +++- test/algorithms/regressors/test_neural_network_regressor.py | 4 +++- 8 files changed, 17 insertions(+), 8 deletions(-) diff --git a/.github/actions/install-machine-learning/action.yml b/.github/actions/install-machine-learning/action.yml index 17dbee841..51f37f443 100644 --- a/.github/actions/install-machine-learning/action.yml +++ b/.github/actions/install-machine-learning/action.yml @@ -17,7 +17,6 @@ runs: using: "composite" steps: - run : | - pip install torch==2.2.2 pip install -e .[torch,sparse] pip install -U -c constraints.txt -r requirements-dev.txt shell: bash diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9d5869b2d..49c3a6b0f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -146,7 +146,7 @@ jobs: - run: make lint shell: bash - run: make mypy - if: ${{ !cancelled() }} + if: ${{ !cancelled() && matrix.os != 'windows-latest' }} shell: bash - name: Machine Learning Unit Tests under Python ${{ matrix.python-version }} uses: ./.github/actions/run-tests diff --git a/constraints.txt b/constraints.txt index 2ad03450b..cfe92e0b9 100644 --- a/constraints.txt +++ b/constraints.txt @@ -1,4 +1,4 @@ -numpy>=1.20.0 +numpy>=1.20,<2.0 ipython<8.13;python_version<'3.9' nbconvert<7.14 # workaround https://github.com/jupyter/nbconvert/issues/2092 diff --git a/qiskit_machine_learning/kernels/algorithms/quantum_kernel_trainer.py b/qiskit_machine_learning/kernels/algorithms/quantum_kernel_trainer.py index 3bbac7d0e..2313f11e4 100644 --- a/qiskit_machine_learning/kernels/algorithms/quantum_kernel_trainer.py +++ b/qiskit_machine_learning/kernels/algorithms/quantum_kernel_trainer.py @@ -206,7 +206,9 @@ def fit( self._loss.evaluate, quantum_kernel=self.quantum_kernel, data=data, labels=labels ) if callable(self._optimizer): - opt_results = self._optimizer(fun=loss_function, x0=self._initial_point) # type: ignore[call-arg, arg-type] + opt_results = self._optimizer( + fun=loss_function, x0=self._initial_point # type: ignore[call-arg, arg-type] + ) else: opt_results = self._optimizer.minimize( fun=loss_function, diff --git a/qiskit_machine_learning/neural_networks/estimator_qnn.py b/qiskit_machine_learning/neural_networks/estimator_qnn.py index 6aff892a5..da3c0facd 100644 --- a/qiskit_machine_learning/neural_networks/estimator_qnn.py +++ b/qiskit_machine_learning/neural_networks/estimator_qnn.py @@ -273,7 +273,9 @@ def _backward( job = self.gradient.run(circuits, observables, param_values) # type: ignore[arg-type] elif len(parameter_values[0]) > self._num_inputs: params = [self._circuit.parameters[self._num_inputs :]] * num_circuits - job = self.gradient.run(circuits, observables, param_values, parameters=params) # type: ignore[arg-type] + job = self.gradient.run( + circuits, observables, param_values, parameters=params # type: ignore[arg-type] + ) if job is not None: try: diff --git a/qiskit_machine_learning/neural_networks/sampler_qnn.py b/qiskit_machine_learning/neural_networks/sampler_qnn.py index 188bef764..28cb0acff 100644 --- a/qiskit_machine_learning/neural_networks/sampler_qnn.py +++ b/qiskit_machine_learning/neural_networks/sampler_qnn.py @@ -416,7 +416,9 @@ def _backward( job = self.gradient.run(circuits, parameter_values) # type: ignore[arg-type] elif len(parameter_values[0]) > self._num_inputs: params = [self._circuit.parameters[self._num_inputs :]] * num_samples - job = self.gradient.run(circuits, parameter_values, parameters=params) # type: ignore[arg-type] + job = self.gradient.run( + circuits, parameter_values, parameters=params # type: ignore[arg-type] + ) if job is not None: try: diff --git a/test/algorithms/classifiers/test_neural_network_classifier.py b/test/algorithms/classifiers/test_neural_network_classifier.py index 251982bd2..30930a9fe 100644 --- a/test/algorithms/classifiers/test_neural_network_classifier.py +++ b/test/algorithms/classifiers/test_neural_network_classifier.py @@ -63,7 +63,9 @@ def _create_optimizer(self, opt: str) -> Optimizer | None: elif opt == "cobyla": optimizer = COBYLA(maxiter=25) # type: ignore[assignment] elif opt == "callable": - optimizer = partial(minimize, method="COBYLA", options={"maxiter": 25}) # type: ignore[assignment] + optimizer = partial( + minimize, method="COBYLA", options={"maxiter": 25} # type: ignore[assignment] + ) else: optimizer = None diff --git a/test/algorithms/regressors/test_neural_network_regressor.py b/test/algorithms/regressors/test_neural_network_regressor.py index 57798049c..a7c9cb225 100644 --- a/test/algorithms/regressors/test_neural_network_regressor.py +++ b/test/algorithms/regressors/test_neural_network_regressor.py @@ -82,7 +82,9 @@ def _create_regressor( elif opt == "cobyla": optimizer = COBYLA(maxiter=25) # type: ignore[assignment] elif opt == "callable": - optimizer = partial(minimize, method="COBYLA", options={"maxiter": 25}) # type: ignore[assignment] + optimizer = partial( + minimize, method="COBYLA", options={"maxiter": 25} # type: ignore[assignment] + ) else: optimizer = None From c2f726a7ce753c548aa01b7f1b6d3de14690318c Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Wed, 31 Jul 2024 13:59:35 +0200 Subject: [PATCH 33/85] Add reno --- ...orithms-incorporated-421554a4ff547d0d.yaml | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 releasenotes/notes/qiskit-algorithms-incorporated-421554a4ff547d0d.yaml diff --git a/releasenotes/notes/qiskit-algorithms-incorporated-421554a4ff547d0d.yaml b/releasenotes/notes/qiskit-algorithms-incorporated-421554a4ff547d0d.yaml new file mode 100644 index 000000000..7aebe5b7a --- /dev/null +++ b/releasenotes/notes/qiskit-algorithms-incorporated-421554a4ff547d0d.yaml @@ -0,0 +1,59 @@ +--- +prelude: > + This release includes the migration of a subset of Qiskit Algorithms features to Qiskit Machine Learning. + This ensures continued enhancement of essential features for Qiskit Machine Learning following the end + of official support for Qiskit Algorithms. Therefore, Qiskit Machine Learning will no longer depend on + Qiskit Algorithms, possibly introducing breaking changes in import structures. Some quick-fixes are + described below. + +features: + - | + Migrated essential Qiskit Algorithms features to Qiskit Machine Learning: + - `qiskit_algorithms/gradients` -> `qiskit_machine_learning/gradients` + - `qiskit_algorithms/optimizers` -> `qiskit_machine_learning/optimizers` + - `qiskit_algorithms/state_fidelities` -> `qiskit_machine_learning/state_fidelities` + - Partial merge of `qiskit_algorithms/utils` with `qiskit_machine_learning/utils` + - | + Unit tests migrated accordingly: + - `qiskit_algorithms/tests/gradients` -> `qiskit_machine_learning/tests/gradients` + - `qiskit_algorithms/tests/optimizers` -> `qiskit_machine_learning/tests/optimizers` + - `qiskit_algorithms/tests/state_fidelities` -> `qiskit_machine_learning/tests/state_fidelities` + - Partial merge of `qiskit_algorithms/tests/utils` with `qiskit_machine_learning/tests/utils` + +issues: + - | + Incorporating Qiskit Algorithms will facilitate the upgrade to newer versions of Qiskit and + Qiskit Runtime. In particular, the following issues, still outstanding, will be tackled in the + next release: + - Support V2 primitives (#742) + - Add support for EstimatorV2 to run circuits over hardware (#810) + - | + Compliance with ISA standards: + - ISA circuit support for latest Runtime (#786) + - Sampler fails to run FidelityKernel even if circuits are transpiled (#165) + +upgrade: + - | + The merge of some of the features of Qiskit Algorithms into Qiskit Machine Learning might lead to breaking changes. + For this reason, caution is advised when updating to version 0.8 during critical production stages in a project. + Users must update their imports and code references in code that uses Qiskit Machine Leaning and Algorithms: + - Change `qiskit_algorithms/gradients` to `qiskit_machine_learning/gradients` + - Change `qiskit_algorithms/optimizers` to `qiskit_machine_learning/optimizers` + - Change `qiskit_algorithms/state_fidelities` to `qiskit_machine_learning/state_fidelities` + - Update utilities as needed due to partial merge. + To continue using sub-modules and functionalities of Qiskit Algorithms that **have not been transferred**, + you may continue using them as before by importing from Qiskit Algorithms. However, be aware that Qiskit Algorithms + is no longer officially supported and some of its functionalities may not work in your use case. For any issues + directly related to Qiskit Algorithms, please open a GitHub issue at https://github.com/qiskit-community/qiskit-algorithms. + Should you want to include a Qiskit Algorithms functionality that has not been incorporated in Qiskit Machine Learning, + please open a feature-request issue at https://github.com/qiskit-community/qiskit-machine-learning, explaining why + this change would be useful for you and other users. + +deprecations: + - | + The following imports of Qiskit Algorithms modules into Qiskit Machine Learning are deprecated and their + functionalities are now part of Qiskit Machine Learning itself: + - `qiskit_algorithms/gradients` + - `qiskit_algorithms/optimizers` + - `qiskit_algorithms/state_fidelities` + - Portions of `qiskit_algorithms/utils` From 5976830439ef799c2004178534c4f591781457bb Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Wed, 31 Jul 2024 15:01:02 +0200 Subject: [PATCH 34/85] Lint sanitize --- qiskit_machine_learning/optimizers/gradient_descent.py | 4 ++-- qiskit_machine_learning/optimizers/steppable_optimizer.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit_machine_learning/optimizers/gradient_descent.py b/qiskit_machine_learning/optimizers/gradient_descent.py index 4eafe5ebd..f3145942d 100644 --- a/qiskit_machine_learning/optimizers/gradient_descent.py +++ b/qiskit_machine_learning/optimizers/gradient_descent.py @@ -304,8 +304,8 @@ def tell(self, ask_data: AskData, tell_data: TellData) -> None: """ if np.shape(self.state.x) != np.shape(tell_data.eval_jac): # type: ignore[arg-type] raise ValueError("The gradient does not have the correct dimension") - self.state.x = self.state.x - next(self.state.learning_rate) * tell_data.eval_jac - self.state.stepsize = np.linalg.norm(tell_data.eval_jac) # type: ignore[arg-type,assignment] + self.state.x -= next(self.state.learning_rate) * tell_data.eval_jac # type: ignore + self.state.stepsize = np.linalg.norm(tell_data.eval_jac) # type: ignore self.state.nit += 1 def evaluate(self, ask_data: AskData) -> TellData: diff --git a/qiskit_machine_learning/optimizers/steppable_optimizer.py b/qiskit_machine_learning/optimizers/steppable_optimizer.py index f50bbc874..2f2a3b06a 100644 --- a/qiskit_machine_learning/optimizers/steppable_optimizer.py +++ b/qiskit_machine_learning/optimizers/steppable_optimizer.py @@ -300,4 +300,4 @@ def continue_condition(self) -> bool: Returns: ``True`` if the optimization process should continue, ``False`` otherwise. """ - return self.state.nit < self.maxiter + return self.state.nit < self.maxiter # type: ignore[no-member] From 5e07acccfe508633d7c265c9ac7016d084fa70c8 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Wed, 31 Jul 2024 15:27:06 +0200 Subject: [PATCH 35/85] Pylint --- .pylintdict | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.pylintdict b/.pylintdict index be1f3b986..5dcc38c50 100644 --- a/.pylintdict +++ b/.pylintdict @@ -594,3 +594,5 @@ zz θ ψ ω +assertRaises +RuntimeError From b997bb047c92a74a6bb5808545dd4976997d5d85 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Wed, 31 Jul 2024 15:31:14 +0200 Subject: [PATCH 36/85] Pylint --- qiskit_machine_learning/optimizers/gradient_descent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_machine_learning/optimizers/gradient_descent.py b/qiskit_machine_learning/optimizers/gradient_descent.py index f3145942d..5bd7abcd7 100644 --- a/qiskit_machine_learning/optimizers/gradient_descent.py +++ b/qiskit_machine_learning/optimizers/gradient_descent.py @@ -304,7 +304,7 @@ def tell(self, ask_data: AskData, tell_data: TellData) -> None: """ if np.shape(self.state.x) != np.shape(tell_data.eval_jac): # type: ignore[arg-type] raise ValueError("The gradient does not have the correct dimension") - self.state.x -= next(self.state.learning_rate) * tell_data.eval_jac # type: ignore + self.state.x = self.state.x - next(self.state.learning_rate) * tell_data.eval_jac # type: ignore self.state.stepsize = np.linalg.norm(tell_data.eval_jac) # type: ignore self.state.nit += 1 From c464459729085869d0df91eda8e2b3fd42e67f88 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Wed, 31 Jul 2024 15:38:47 +0200 Subject: [PATCH 37/85] Pylint --- qiskit_machine_learning/optimizers/gradient_descent.py | 4 ++-- qiskit_machine_learning/optimizers/steppable_optimizer.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit_machine_learning/optimizers/gradient_descent.py b/qiskit_machine_learning/optimizers/gradient_descent.py index 5bd7abcd7..e1b12955f 100644 --- a/qiskit_machine_learning/optimizers/gradient_descent.py +++ b/qiskit_machine_learning/optimizers/gradient_descent.py @@ -304,8 +304,8 @@ def tell(self, ask_data: AskData, tell_data: TellData) -> None: """ if np.shape(self.state.x) != np.shape(tell_data.eval_jac): # type: ignore[arg-type] raise ValueError("The gradient does not have the correct dimension") - self.state.x = self.state.x - next(self.state.learning_rate) * tell_data.eval_jac # type: ignore - self.state.stepsize = np.linalg.norm(tell_data.eval_jac) # type: ignore + self.state.x = self.state.x - next(self.state.learning_rate) * tell_data.eval_jac # pylint: disable + self.state.stepsize = np.linalg.norm(tell_data.eval_jac) # pylint: disable=attribute-defined-outside-init self.state.nit += 1 def evaluate(self, ask_data: AskData) -> TellData: diff --git a/qiskit_machine_learning/optimizers/steppable_optimizer.py b/qiskit_machine_learning/optimizers/steppable_optimizer.py index 2f2a3b06a..bb436513b 100644 --- a/qiskit_machine_learning/optimizers/steppable_optimizer.py +++ b/qiskit_machine_learning/optimizers/steppable_optimizer.py @@ -300,4 +300,4 @@ def continue_condition(self) -> bool: Returns: ``True`` if the optimization process should continue, ``False`` otherwise. """ - return self.state.nit < self.maxiter # type: ignore[no-member] + return self.state.nit < self.maxiter # pylint: disable=no-member From 51610a1f49f6fede4b10a8283fbb1889e62f7beb Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Wed, 31 Jul 2024 15:57:30 +0200 Subject: [PATCH 38/85] Pylint --- qiskit_machine_learning/optimizers/gradient_descent.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qiskit_machine_learning/optimizers/gradient_descent.py b/qiskit_machine_learning/optimizers/gradient_descent.py index e1b12955f..09a950770 100644 --- a/qiskit_machine_learning/optimizers/gradient_descent.py +++ b/qiskit_machine_learning/optimizers/gradient_descent.py @@ -304,8 +304,9 @@ def tell(self, ask_data: AskData, tell_data: TellData) -> None: """ if np.shape(self.state.x) != np.shape(tell_data.eval_jac): # type: ignore[arg-type] raise ValueError("The gradient does not have the correct dimension") - self.state.x = self.state.x - next(self.state.learning_rate) * tell_data.eval_jac # pylint: disable - self.state.stepsize = np.linalg.norm(tell_data.eval_jac) # pylint: disable=attribute-defined-outside-init + # pylint: disable=attribute-defined-outside-init + self.state.x = self.state.x - next(self.state.learning_rate) * tell_data.eval_jac + self.state.stepsize = np.linalg.norm(tell_data.eval_jac) # type: ignore[arg-type, assignment] self.state.nit += 1 def evaluate(self, ask_data: AskData) -> TellData: From c42688c4c2746fee45e72e325968fde92706793d Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Wed, 31 Jul 2024 16:11:51 +0200 Subject: [PATCH 39/85] Fix relative imports in tutorials --- docs/tutorials/01_neural_networks.ipynb | 2 +- .../02_neural_network_classifier_and_regressor.ipynb | 4 ++-- .../02a_training_a_quantum_model_on_a_real_dataset.ipynb | 7 +++---- docs/tutorials/03_quantum_kernel.ipynb | 2 +- docs/tutorials/04_torch_qgan.ipynb | 2 +- docs/tutorials/05_torch_connector.ipynb | 5 ++--- docs/tutorials/07_pegasos_qsvc.ipynb | 2 +- docs/tutorials/08_quantum_kernel_trainer.ipynb | 2 +- docs/tutorials/09_saving_and_loading_models.ipynb | 4 ++-- docs/tutorials/10_effective_dimension.ipynb | 4 ++-- .../11_quantum_convolutional_neural_networks.ipynb | 4 ++-- docs/tutorials/12_quantum_autoencoder.ipynb | 4 ++-- 12 files changed, 20 insertions(+), 22 deletions(-) diff --git a/docs/tutorials/01_neural_networks.ipynb b/docs/tutorials/01_neural_networks.ipynb index 30bdfde5b..255aa8c3c 100644 --- a/docs/tutorials/01_neural_networks.ipynb +++ b/docs/tutorials/01_neural_networks.ipynb @@ -92,7 +92,7 @@ "metadata": {}, "outputs": [], "source": [ - "from qiskit_algorithms.utils import algorithm_globals\n", + "from qiskit_machine_learning.utils import algorithm_globals\n", "\n", "algorithm_globals.random_seed = 42" ] diff --git a/docs/tutorials/02_neural_network_classifier_and_regressor.ipynb b/docs/tutorials/02_neural_network_classifier_and_regressor.ipynb index 63e41d8e1..c15a6b5c6 100644 --- a/docs/tutorials/02_neural_network_classifier_and_regressor.ipynb +++ b/docs/tutorials/02_neural_network_classifier_and_regressor.ipynb @@ -36,8 +36,8 @@ "from qiskit import QuantumCircuit\n", "from qiskit.circuit import Parameter\n", "from qiskit.circuit.library import RealAmplitudes, ZZFeatureMap\n", - "from qiskit_algorithms.optimizers import COBYLA, L_BFGS_B\n", - "from qiskit_algorithms.utils import algorithm_globals\n", + "from qiskit_machine_learning.optimizers import COBYLA, L_BFGS_B\n", + "from qiskit_machine_learning.utils import algorithm_globals\n", "\n", "from qiskit_machine_learning.algorithms.classifiers import NeuralNetworkClassifier, VQC\n", "from qiskit_machine_learning.algorithms.regressors import NeuralNetworkRegressor, VQR\n", diff --git a/docs/tutorials/02a_training_a_quantum_model_on_a_real_dataset.ipynb b/docs/tutorials/02a_training_a_quantum_model_on_a_real_dataset.ipynb index 53c6ee7ef..63bf39dbb 100644 --- a/docs/tutorials/02a_training_a_quantum_model_on_a_real_dataset.ipynb +++ b/docs/tutorials/02a_training_a_quantum_model_on_a_real_dataset.ipynb @@ -117,8 +117,7 @@ " conceptual clustering system finds 3 classes in the data.\n", "- Many, many more ...\n", "\n", - "|details-end|\n", - "\n" + "|details-end|\n" ] } ], @@ -249,7 +248,7 @@ "outputs": [], "source": [ "from sklearn.model_selection import train_test_split\n", - "from qiskit_algorithms.utils import algorithm_globals\n", + "from qiskit_machine_learning.utils import algorithm_globals\n", "\n", "algorithm_globals.random_seed = 123\n", "train_features, test_features, train_labels, test_labels = train_test_split(\n", @@ -412,7 +411,7 @@ "metadata": {}, "outputs": [], "source": [ - "from qiskit_algorithms.optimizers import COBYLA\n", + "from qiskit_machine_learning.optimizers import COBYLA\n", "\n", "optimizer = COBYLA(maxiter=100)" ] diff --git a/docs/tutorials/03_quantum_kernel.ipynb b/docs/tutorials/03_quantum_kernel.ipynb index 7c314fa68..9de29a222 100644 --- a/docs/tutorials/03_quantum_kernel.ipynb +++ b/docs/tutorials/03_quantum_kernel.ipynb @@ -78,7 +78,7 @@ "metadata": {}, "outputs": [], "source": [ - "from qiskit_algorithms.utils import algorithm_globals\n", + "from qiskit_machine_learning.utils import algorithm_globals\n", "\n", "algorithm_globals.random_seed = 12345" ] diff --git a/docs/tutorials/04_torch_qgan.ipynb b/docs/tutorials/04_torch_qgan.ipynb index fbf8949f1..eb93decb9 100644 --- a/docs/tutorials/04_torch_qgan.ipynb +++ b/docs/tutorials/04_torch_qgan.ipynb @@ -85,7 +85,7 @@ "outputs": [], "source": [ "import torch\n", - "from qiskit_algorithms.utils import algorithm_globals\n", + "from qiskit_machine_learning.utils import algorithm_globals\n", "\n", "algorithm_globals.random_seed = 123456\n", "_ = torch.manual_seed(123456) # suppress output" diff --git a/docs/tutorials/05_torch_connector.ipynb b/docs/tutorials/05_torch_connector.ipynb index 5f9efb2dd..ff419093a 100644 --- a/docs/tutorials/05_torch_connector.ipynb +++ b/docs/tutorials/05_torch_connector.ipynb @@ -47,7 +47,7 @@ "from qiskit import QuantumCircuit\n", "from qiskit.circuit import Parameter\n", "from qiskit.circuit.library import RealAmplitudes, ZZFeatureMap\n", - "from qiskit_algorithms.utils import algorithm_globals\n", + "from qiskit_machine_learning.utils import algorithm_globals\n", "from qiskit_machine_learning.neural_networks import SamplerQNN, EstimatorQNN\n", "from qiskit_machine_learning.connectors import TorchConnector\n", "\n", @@ -836,8 +836,7 @@ "\n", "Downloading http://yann.lecun.com/exdb/mnist/t10k-labels-idx1-ubyte.gz\n", "Downloading http://yann.lecun.com/exdb/mnist/t10k-labels-idx1-ubyte.gz to ./data/MNIST/raw/t10k-labels-idx1-ubyte.gz\n", - "Extracting ./data/MNIST/raw/t10k-labels-idx1-ubyte.gz to ./data/MNIST/raw\n", - "\n" + "Extracting ./data/MNIST/raw/t10k-labels-idx1-ubyte.gz to ./data/MNIST/raw\n" ] } ], diff --git a/docs/tutorials/07_pegasos_qsvc.ipynb b/docs/tutorials/07_pegasos_qsvc.ipynb index da91dd383..27d91665b 100644 --- a/docs/tutorials/07_pegasos_qsvc.ipynb +++ b/docs/tutorials/07_pegasos_qsvc.ipynb @@ -110,7 +110,7 @@ "outputs": [], "source": [ "from qiskit.circuit.library import ZFeatureMap\n", - "from qiskit_algorithms.utils import algorithm_globals\n", + "from qiskit_machine_learning.utils import algorithm_globals\n", "\n", "from qiskit_machine_learning.kernels import FidelityQuantumKernel\n", "\n", diff --git a/docs/tutorials/08_quantum_kernel_trainer.ipynb b/docs/tutorials/08_quantum_kernel_trainer.ipynb index 7f69c3f17..2a7a01435 100644 --- a/docs/tutorials/08_quantum_kernel_trainer.ipynb +++ b/docs/tutorials/08_quantum_kernel_trainer.ipynb @@ -46,7 +46,7 @@ "from qiskit.circuit import ParameterVector\n", "from qiskit.visualization import circuit_drawer\n", "from qiskit.circuit.library import ZZFeatureMap\n", - "from qiskit_algorithms.optimizers import SPSA\n", + "from qiskit_machine_learning.optimizers import SPSA\n", "from qiskit_machine_learning.kernels import TrainableFidelityQuantumKernel\n", "from qiskit_machine_learning.kernels.algorithms import QuantumKernelTrainer\n", "from qiskit_machine_learning.algorithms import QSVC\n", diff --git a/docs/tutorials/09_saving_and_loading_models.ipynb b/docs/tutorials/09_saving_and_loading_models.ipynb index 08a45f8ed..0386d6c13 100644 --- a/docs/tutorials/09_saving_and_loading_models.ipynb +++ b/docs/tutorials/09_saving_and_loading_models.ipynb @@ -37,8 +37,8 @@ "import numpy as np\n", "from qiskit.circuit.library import RealAmplitudes\n", "from qiskit.primitives import Sampler\n", - "from qiskit_algorithms.optimizers import COBYLA\n", - "from qiskit_algorithms.utils import algorithm_globals\n", + "from qiskit_machine_learning.optimizers import COBYLA\n", + "from qiskit_machine_learning.utils import algorithm_globals\n", "from sklearn.model_selection import train_test_split\n", "from sklearn.preprocessing import OneHotEncoder, MinMaxScaler\n", "\n", diff --git a/docs/tutorials/10_effective_dimension.ipynb b/docs/tutorials/10_effective_dimension.ipynb index 21e656f67..f3357aa12 100644 --- a/docs/tutorials/10_effective_dimension.ipynb +++ b/docs/tutorials/10_effective_dimension.ipynb @@ -73,8 +73,8 @@ "from IPython.display import clear_output\n", "from qiskit import QuantumCircuit\n", "from qiskit.circuit.library import ZFeatureMap, RealAmplitudes\n", - "from qiskit_algorithms.optimizers import COBYLA\n", - "from qiskit_algorithms.utils import algorithm_globals\n", + "from qiskit_machine_learning.optimizers import COBYLA\n", + "from qiskit_machine_learning.utils import algorithm_globals\n", "from sklearn.datasets import make_classification\n", "from sklearn.preprocessing import MinMaxScaler\n", "\n", diff --git a/docs/tutorials/11_quantum_convolutional_neural_networks.ipynb b/docs/tutorials/11_quantum_convolutional_neural_networks.ipynb index b9d297273..c57cf3b9f 100644 --- a/docs/tutorials/11_quantum_convolutional_neural_networks.ipynb +++ b/docs/tutorials/11_quantum_convolutional_neural_networks.ipynb @@ -51,8 +51,8 @@ "from qiskit.circuit import ParameterVector\n", "from qiskit.circuit.library import ZFeatureMap\n", "from qiskit.quantum_info import SparsePauliOp\n", - "from qiskit_algorithms.optimizers import COBYLA\n", - "from qiskit_algorithms.utils import algorithm_globals\n", + "from qiskit_machine_learning.optimizers import COBYLA\n", + "from qiskit_machine_learning.utils import algorithm_globals\n", "from qiskit_machine_learning.algorithms.classifiers import NeuralNetworkClassifier\n", "from qiskit_machine_learning.neural_networks import EstimatorQNN\n", "from sklearn.model_selection import train_test_split\n", diff --git a/docs/tutorials/12_quantum_autoencoder.ipynb b/docs/tutorials/12_quantum_autoencoder.ipynb index 139f1c14e..e13425c36 100644 --- a/docs/tutorials/12_quantum_autoencoder.ipynb +++ b/docs/tutorials/12_quantum_autoencoder.ipynb @@ -270,8 +270,8 @@ "from qiskit import QuantumCircuit\n", "from qiskit.circuit.library import RealAmplitudes\n", "from qiskit.quantum_info import Statevector\n", - "from qiskit_algorithms.optimizers import COBYLA\n", - "from qiskit_algorithms.utils import algorithm_globals\n", + "from qiskit_machine_learning.optimizers import COBYLA\n", + "from qiskit_machine_learning.utils import algorithm_globals\n", "\n", "from qiskit_machine_learning.circuit.library import RawFeatureVector\n", "from qiskit_machine_learning.neural_networks import SamplerQNN\n", From db9b03f0b7fc4eef31b398fce5a2e3b6fe06a069 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Wed, 31 Jul 2024 16:27:04 +0200 Subject: [PATCH 40/85] Fix relative imports in tutorials --- docs/tutorials/03_quantum_kernel.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/03_quantum_kernel.ipynb b/docs/tutorials/03_quantum_kernel.ipynb index 9de29a222..1f5629ce4 100644 --- a/docs/tutorials/03_quantum_kernel.ipynb +++ b/docs/tutorials/03_quantum_kernel.ipynb @@ -246,7 +246,7 @@ "source": [ "from qiskit.circuit.library import ZZFeatureMap\n", "from qiskit.primitives import Sampler\n", - "from qiskit_algorithms.state_fidelities import ComputeUncompute\n", + "from qiskit_machine_learning.state_fidelities import ComputeUncompute\n", "from qiskit_machine_learning.kernels import FidelityQuantumKernel\n", "\n", "adhoc_feature_map = ZZFeatureMap(feature_dimension=adhoc_dimension, reps=2, entanglement=\"linear\")\n", From 21badc4da5893ae0546e49fd36f8a8e1b5118363 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Wed, 31 Jul 2024 16:53:01 +0200 Subject: [PATCH 41/85] Remove algorithms from Jupyter magic methods --- docs/conf.py | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 4c239b619..9eb0e42d7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -177,7 +177,6 @@ "scipy": ("https://docs.scipy.org/doc/scipy", None), "sklearn": ("https://scikit-learn.org/stable", None), "qiskit": ("https://docs.quantum.ibm.com/api/qiskit", None), - "qiskit-algorithms": ("https://qiskit-community.github.io/qiskit-algorithms", None), } html_context = {"analytics_enabled": True} From e8628cc16a8b80ea39dd965d753002d47c44d2ac Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Thu, 1 Aug 2024 16:48:59 +0200 Subject: [PATCH 42/85] Temporarily disable "Run stable tutorials" tests --- .github/workflows/main.yml | 60 +++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 49c3a6b0f..828f48b76 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -229,36 +229,36 @@ jobs: with: name: tutorials${{ matrix.python-version }} path: docs/_build/html/artifacts/tutorials.tar.gz - - name: Run stable tutorials - env: - QISKIT_PARALLEL: False - QISKIT_DOCS_BUILD_TUTORIALS: 'always' - run: | - # clean last sphinx output - make clean_sphinx - # get current version - version=$(pip show qiskit-machine-learning | awk -F. '/^Version:/ { print substr($1,10), $2-1 }' OFS=.) - # download stable version - wget https://codeload.github.com/qiskit-community/qiskit-machine-learning/zip/stable/$version -O /tmp/repo.zip - unzip /tmp/repo.zip -d /tmp/ - # copy stable tutorials to main tutorials - cp -R /tmp/qiskit-machine-learning-stable-$version/docs/tutorials/* docs/tutorials - # run tutorials and zip results - echo "earliest_version: 0.1.0" >> releasenotes/config.yaml - # ignore unreleased/untagged notes - tools/ignore_untagged_notes.sh - make html - cd docs/_build/html - mkdir artifacts - tar -zcvf artifacts/tutorials.tar.gz --exclude=./artifacts . - if: ${{ matrix.python-version == 3.8 && !startsWith(github.ref, 'refs/heads/stable') && !startsWith(github.base_ref, 'stable/') }} - shell: bash - - name: Run upload stable tutorials - uses: actions/upload-artifact@v4 - with: - name: tutorials-stable${{ matrix.python-version }} - path: docs/_build/html/artifacts/tutorials.tar.gz - if: ${{ matrix.python-version == 3.8 && !startsWith(github.ref, 'refs/heads/stable') && !startsWith(github.base_ref, 'stable/') }} +# - name: Run stable tutorials +# env: +# QISKIT_PARALLEL: False +# QISKIT_DOCS_BUILD_TUTORIALS: 'always' +# run: | +# # clean last sphinx output +# make clean_sphinx +# # get current version +# version=$(pip show qiskit-machine-learning | awk -F. '/^Version:/ { print substr($1,10), $2-1 }' OFS=.) +# # download stable version +# wget https://codeload.github.com/qiskit-community/qiskit-machine-learning/zip/stable/$version -O /tmp/repo.zip +# unzip /tmp/repo.zip -d /tmp/ +# # copy stable tutorials to main tutorials +# cp -R /tmp/qiskit-machine-learning-stable-$version/docs/tutorials/* docs/tutorials +# # run tutorials and zip results +# echo "earliest_version: 0.1.0" >> releasenotes/config.yaml +# # ignore unreleased/untagged notes +# tools/ignore_untagged_notes.sh +# make html +# cd docs/_build/html +# mkdir artifacts +# tar -zcvf artifacts/tutorials.tar.gz --exclude=./artifacts . +# if: ${{ matrix.python-version == 3.8 && !startsWith(github.ref, 'refs/heads/stable') && !startsWith(github.base_ref, 'stable/') }} +# shell: bash +# - name: Run upload stable tutorials +# uses: actions/upload-artifact@v4 +# with: +# name: tutorials-stable${{ matrix.python-version }} +# path: docs/_build/html/artifacts/tutorials.tar.gz +# if: ${{ matrix.python-version == 3.8 && !startsWith(github.ref, 'refs/heads/stable') && !startsWith(github.base_ref, 'stable/') }} Deprecation_Messages_and_Coverage: needs: [Checks, MachineLearning, Tutorials] runs-on: ubuntu-latest From da63c5b980e6d718f0ac2b50f5d84c2fff8b105b Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Thu, 1 Aug 2024 18:19:22 +0200 Subject: [PATCH 43/85] Change the docstrings with imports from qiskit_algorithms --- .../algorithms/classifiers/neural_network_classifier.py | 4 ++-- qiskit_machine_learning/algorithms/classifiers/vqc.py | 4 ++-- qiskit_machine_learning/algorithms/regressors/vqr.py | 4 ++-- qiskit_machine_learning/algorithms/trainable_model.py | 6 +++--- qiskit_machine_learning/gradients/__init__.py | 4 ++-- .../kernels/algorithms/quantum_kernel_trainer.py | 6 +++--- .../kernels/fidelity_quantum_kernel.py | 6 +++--- .../kernels/trainable_fidelity_quantum_kernel.py | 6 +++--- qiskit_machine_learning/neural_networks/estimator_qnn.py | 2 +- qiskit_machine_learning/neural_networks/sampler_qnn.py | 2 +- qiskit_machine_learning/optimizers/__init__.py | 6 +++--- qiskit_machine_learning/optimizers/gsls.py | 2 +- .../optimizers/optimizer_utils/__init__.py | 2 +- qiskit_machine_learning/optimizers/p_bfgs.py | 2 +- qiskit_machine_learning/optimizers/qnspsa.py | 4 ++-- qiskit_machine_learning/optimizers/spsa.py | 6 +++--- qiskit_machine_learning/optimizers/steppable_optimizer.py | 2 +- qiskit_machine_learning/optimizers/umda.py | 6 +++--- qiskit_machine_learning/state_fidelities/__init__.py | 4 ++-- qiskit_machine_learning/utils/algorithm_globals.py | 8 ++++---- qiskit_machine_learning/variational_algorithm.py | 2 +- 21 files changed, 44 insertions(+), 44 deletions(-) diff --git a/qiskit_machine_learning/algorithms/classifiers/neural_network_classifier.py b/qiskit_machine_learning/algorithms/classifiers/neural_network_classifier.py index 7ad9d4bf7..37595caa1 100644 --- a/qiskit_machine_learning/algorithms/classifiers/neural_network_classifier.py +++ b/qiskit_machine_learning/algorithms/classifiers/neural_network_classifier.py @@ -76,9 +76,9 @@ def __init__( loss). In case of a one-dimensional categorical output, this option determines how to encode the target data (i.e. one-hot or integer encoding). optimizer: An instance of an optimizer or a callable to be used in training. - Refer to :class:`~qiskit_algorithms.optimizers.Minimizer` for more information on + Refer to :class:`~qiskit_machine_learning.optimizers.Minimizer` for more information on the callable protocol. When `None` defaults to - :class:`~qiskit_algorithms.optimizers.SLSQP`. + :class:`~qiskit_machine_learning.optimizers.SLSQP`. warm_start: Use weights from previous fit to start next fit. initial_point: Initial point for the optimizer to start from. callback: a reference to a user's callback function that has two parameters and diff --git a/qiskit_machine_learning/algorithms/classifiers/vqc.py b/qiskit_machine_learning/algorithms/classifiers/vqc.py index fbeb67f7e..1f3c4238b 100644 --- a/qiskit_machine_learning/algorithms/classifiers/vqc.py +++ b/qiskit_machine_learning/algorithms/classifiers/vqc.py @@ -74,9 +74,9 @@ def __init__( circuit is used. loss: A target loss function to be used in training. Default value is ``cross_entropy``. optimizer: An instance of an optimizer or a callable to be used in training. - Refer to :class:`~qiskit_algorithms.optimizers.Minimizer` for more information on + Refer to :class:`~qiskit_machine_learning.optimizers.Minimizer` for more information on the callable protocol. When `None` defaults to - :class:`~qiskit_algorithms.optimizers.SLSQP`. + :class:`~qiskit_machine_learning.optimizers.SLSQP`. warm_start: Use weights from previous fit to start next fit. initial_point: Initial point for the optimizer to start from. callback: a reference to a user's callback function that has two parameters and diff --git a/qiskit_machine_learning/algorithms/regressors/vqr.py b/qiskit_machine_learning/algorithms/regressors/vqr.py index addf34667..a26499c87 100644 --- a/qiskit_machine_learning/algorithms/regressors/vqr.py +++ b/qiskit_machine_learning/algorithms/regressors/vqr.py @@ -61,9 +61,9 @@ def __init__( use the default :math:`Z^{\otimes num\_qubits}` observable. loss: A target loss function to be used in training. Default is squared error. optimizer: An instance of an optimizer or a callable to be used in training. - Refer to :class:`~qiskit_algorithms.optimizers.Minimizer` for more information on + Refer to :class:`~qiskit_machine_learning.optimizers.Minimizer` for more information on the callable protocol. When `None` defaults to - :class:`~qiskit_algorithms.optimizers.SLSQP`. + :class:`~qiskit_machine_learning.optimizers.SLSQP`. warm_start: Use weights from previous fit to start next fit. initial_point: Initial point for the optimizer to start from. callback: A reference to a user's callback function that has two parameters and diff --git a/qiskit_machine_learning/algorithms/trainable_model.py b/qiskit_machine_learning/algorithms/trainable_model.py index ccc866d35..efd7b1796 100644 --- a/qiskit_machine_learning/algorithms/trainable_model.py +++ b/qiskit_machine_learning/algorithms/trainable_model.py @@ -60,9 +60,9 @@ def __init__( 'squared_error', 'cross_entropy', or as a loss function implementing the Loss interface. optimizer: An instance of an optimizer or a callable to be used in training. - Refer to :class:`~qiskit_algorithms.optimizers.Minimizer` for more information on + Refer to :class:`~qiskit_machine_learning.optimizers.Minimizer` for more information on the callable protocol. When `None` defaults to - :class:`~qiskit_algorithms.optimizers.SLSQP`. + :class:`~qiskit_machine_learning.optimizers.SLSQP`. warm_start: Use weights from previous fit to start next fit. initial_point: Initial point for the optimizer to start from. callback: A reference to a user's callback function that has two parameters and @@ -154,7 +154,7 @@ def weights(self) -> np.ndarray: def fit_result(self) -> OptimizerResult: """Returns a resulting object from the optimization procedure. Please refer to the documentation of the `OptimizerResult - `_ + `_ class for more details. Raises: diff --git a/qiskit_machine_learning/gradients/__init__.py b/qiskit_machine_learning/gradients/__init__.py index 700ebe7ee..9f24daa53 100644 --- a/qiskit_machine_learning/gradients/__init__.py +++ b/qiskit_machine_learning/gradients/__init__.py @@ -11,11 +11,11 @@ # that they have been altered from the originals. """ -Gradients (:mod:`qiskit_algorithms.gradients`) +Gradients (:mod:`qiskit_machine_learning.gradients`) ============================================== Algorithms to calculate the gradient of a quantum circuit. -.. currentmodule:: qiskit_algorithms.gradients +.. currentmodule:: qiskit_machine_learning.gradients Base Classes ------------ diff --git a/qiskit_machine_learning/kernels/algorithms/quantum_kernel_trainer.py b/qiskit_machine_learning/kernels/algorithms/quantum_kernel_trainer.py index 2313f11e4..97dbaf014 100644 --- a/qiskit_machine_learning/kernels/algorithms/quantum_kernel_trainer.py +++ b/qiskit_machine_learning/kernels/algorithms/quantum_kernel_trainer.py @@ -101,12 +101,12 @@ def __init__( passed as the loss function, then the underlying :class:`~qiskit_machine_learning.utils.loss_functions.SVCLoss` object will exhibit default behavior. - optimizer: An instance of :class:`~qiskit_algorithms.optimizers.Optimizer` or a + optimizer: An instance of :class:`~qiskit_machine_learning.optimizers.Optimizer` or a callable to be used in training. Refer to - :class:`~qiskit_algorithms.optimizers.Minimizer` for more information on the + :class:`~qiskit_machine_learning.optimizers.Minimizer` for more information on the callable protocol. Since no analytical gradient is defined for kernel loss functions, gradient-based optimizers are not recommended for training kernels. When - `None` defaults to :class:`~qiskit_algorithms.optimizers.SPSA`. + `None` defaults to :class:`~qiskit_machine_learning.optimizers.SPSA`. initial_point: Initial point from which the optimizer will begin. Raises: diff --git a/qiskit_machine_learning/kernels/fidelity_quantum_kernel.py b/qiskit_machine_learning/kernels/fidelity_quantum_kernel.py index 5de704e53..e9769addc 100644 --- a/qiskit_machine_learning/kernels/fidelity_quantum_kernel.py +++ b/qiskit_machine_learning/kernels/fidelity_quantum_kernel.py @@ -29,7 +29,7 @@ class FidelityQuantumKernel(BaseKernel): r""" An implementation of the quantum kernel interface based on the - :class:`~qiskit_algorithms.state_fidelities.BaseStateFidelity` algorithm. + :class:`~qiskit_machine_learning.state_fidelities.BaseStateFidelity` algorithm. Here, the kernel function is defined as the overlap of two quantum states defined by a parametrized quantum circuit (called feature map): @@ -56,9 +56,9 @@ def __init__( in the dataset, then the kernel will try to adjust the feature map to reflect the number of features. fidelity: An instance of the - :class:`~qiskit_algorithms.state_fidelities.BaseStateFidelity` primitive to be used + :class:`~qiskit_machine_learning.state_fidelities.BaseStateFidelity` primitive to be used to compute fidelity between states. Default is - :class:`~qiskit_algorithms.state_fidelities.ComputeUncompute` which is created on + :class:`~qiskit_machine_learning.state_fidelities.ComputeUncompute` which is created on top of the reference sampler defined by :class:`~qiskit.primitives.Sampler`. enforce_psd: Project to the closest positive semidefinite matrix if ``x = y``. Default ``True``. diff --git a/qiskit_machine_learning/kernels/trainable_fidelity_quantum_kernel.py b/qiskit_machine_learning/kernels/trainable_fidelity_quantum_kernel.py index a813b97e0..0b976c440 100644 --- a/qiskit_machine_learning/kernels/trainable_fidelity_quantum_kernel.py +++ b/qiskit_machine_learning/kernels/trainable_fidelity_quantum_kernel.py @@ -28,7 +28,7 @@ class TrainableFidelityQuantumKernel(TrainableKernel, FidelityQuantumKernel): r""" An implementation of the quantum kernel that is based on the - :class:`~qiskit_algorithms.state_fidelities.BaseStateFidelity` algorithm and provides ability to + :class:`~qiskit_machine_learning.state_fidelities.BaseStateFidelity` algorithm and provides ability to train it. Finding good quantum kernels for a specific machine learning task is a big challenge in quantum @@ -60,9 +60,9 @@ def __init__( in the dataset, then the kernel will try to adjust the feature map to reflect the number of features. fidelity: An instance of the - :class:`~qiskit_algorithms.state_fidelities.BaseStateFidelity` primitive to be used + :class:`~qiskit_machine_learning.state_fidelities.BaseStateFidelity` primitive to be used to compute fidelity between states. Default is - :class:`~qiskit_algorithms.state_fidelities.ComputeUncompute` which is created on + :class:`~qiskit_machine_learning.state_fidelities.ComputeUncompute` which is created on top of the reference sampler defined by :class:`~qiskit.primitives.Sampler`. training_parameters: Iterable containing :class:`~qiskit.circuit.Parameter` objects which correspond to quantum gates on the feature map circuit which may be tuned. diff --git a/qiskit_machine_learning/neural_networks/estimator_qnn.py b/qiskit_machine_learning/neural_networks/estimator_qnn.py index da3c0facd..f55d82224 100644 --- a/qiskit_machine_learning/neural_networks/estimator_qnn.py +++ b/qiskit_machine_learning/neural_networks/estimator_qnn.py @@ -136,7 +136,7 @@ def __init__( :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` weight_parameters. gradient: The estimator gradient to be used for the backward pass. If None, a default instance of the estimator gradient, - :class:`~qiskit_algorithms.gradients.ParamShiftEstimatorGradient`, will be used. + :class:`~qiskit_machine_learning.gradients.ParamShiftEstimatorGradient`, will be used. input_gradients: Determines whether to compute gradients with respect to input data. Note that this parameter is ``False`` by default, and must be explicitly set to ``True`` for a proper gradient computation when using diff --git a/qiskit_machine_learning/neural_networks/sampler_qnn.py b/qiskit_machine_learning/neural_networks/sampler_qnn.py index 28cb0acff..6982d2e87 100644 --- a/qiskit_machine_learning/neural_networks/sampler_qnn.py +++ b/qiskit_machine_learning/neural_networks/sampler_qnn.py @@ -165,7 +165,7 @@ def __init__( ``2^circuit.num_qubits``. gradient: An optional sampler gradient to be used for the backward pass. If ``None`` is given, a default instance of - :class:`~qiskit_algorithms.gradients.ParamShiftSamplerGradient` will be used. + :class:`~qiskit_machine_learning.gradients.ParamShiftSamplerGradient` will be used. input_gradients: Determines whether to compute gradients with respect to input data. Note that this parameter is ``False`` by default, and must be explicitly set to ``True`` for a proper gradient computation when using diff --git a/qiskit_machine_learning/optimizers/__init__.py b/qiskit_machine_learning/optimizers/__init__.py index 8ef6f5a8f..891bb1d3a 100644 --- a/qiskit_machine_learning/optimizers/__init__.py +++ b/qiskit_machine_learning/optimizers/__init__.py @@ -11,12 +11,12 @@ # that they have been altered from the originals. """ -Optimizers (:mod:`qiskit_algorithms.optimizers`) +Optimizers (:mod:`qiskit_machine_learning.optimizers`) ================================================ Classical Optimizers. This package contains a variety of classical optimizers and were designed for use by -qiskit_algorithm's quantum variational algorithms, such as :class:`~qiskit_algorithms.VQE`. +qiskit_algorithm's quantum variational algorithms, such as :class:`~qiskit_machine_learning.VQE`. Logically, these optimizers can be divided into two categories: `Local Optimizers`_ @@ -27,7 +27,7 @@ Given an optimization problem, a **global optimizer** is a function that attempts to find an optimal value among all possible solutions. -.. currentmodule:: qiskit_algorithms.optimizers +.. currentmodule:: qiskit_machine_learning.optimizers Optimizer Base Classes ---------------------- diff --git a/qiskit_machine_learning/optimizers/gsls.py b/qiskit_machine_learning/optimizers/gsls.py index c6747c442..6f2a36e30 100644 --- a/qiskit_machine_learning/optimizers/gsls.py +++ b/qiskit_machine_learning/optimizers/gsls.py @@ -34,7 +34,7 @@ class GSLS(Optimizer): This component has some function that is normally random. If you want to reproduce behavior then you should set the random number generator seed in the algorithm_globals - (``qiskit_algorithms.utils.algorithm_globals.random_seed = seed``). + (``qiskit_machine_learning.utils.algorithm_globals.random_seed = seed``). """ _OPTIONS = [ diff --git a/qiskit_machine_learning/optimizers/optimizer_utils/__init__.py b/qiskit_machine_learning/optimizers/optimizer_utils/__init__.py index bf77c4120..fb251fe03 100644 --- a/qiskit_machine_learning/optimizers/optimizer_utils/__init__.py +++ b/qiskit_machine_learning/optimizers/optimizer_utils/__init__.py @@ -11,7 +11,7 @@ # that they have been altered from the originals. """Utils for optimizers -Optimizer Utils (:mod:`qiskit_algorithms.optimizers.optimizer_utils`) +Optimizer Utils (:mod:`qiskit_machine_learning.optimizers.optimizer_utils`) ===================================================================== .. autosummary:: diff --git a/qiskit_machine_learning/optimizers/p_bfgs.py b/qiskit_machine_learning/optimizers/p_bfgs.py index a18d1f9d0..c70d7697d 100644 --- a/qiskit_machine_learning/optimizers/p_bfgs.py +++ b/qiskit_machine_learning/optimizers/p_bfgs.py @@ -47,7 +47,7 @@ class P_BFGS(SciPyOptimizer): # pylint: disable=invalid-name This component has some function that is normally random. If you want to reproduce behavior then you should set the random number generator seed in the algorithm_globals - (``qiskit_algorithms.utils.algorithm_globals.random_seed = seed``). + (``qiskit_machine_learning.utils.algorithm_globals.random_seed = seed``). """ _OPTIONS = ["maxfun", "ftol", "iprint"] diff --git a/qiskit_machine_learning/optimizers/qnspsa.py b/qiskit_machine_learning/optimizers/qnspsa.py index 1f7db8d84..408bcd8a4 100644 --- a/qiskit_machine_learning/optimizers/qnspsa.py +++ b/qiskit_machine_learning/optimizers/qnspsa.py @@ -51,7 +51,7 @@ class QNSPSA(SPSA): This component has some function that is normally random. If you want to reproduce behavior then you should set the random number generator seed in the algorithm_globals - (``qiskit_algorithms.utils.algorithm_globals.random_seed = seed``). + (``qiskit_machine_learning.utils.algorithm_globals.random_seed = seed``). Examples: @@ -61,7 +61,7 @@ class QNSPSA(SPSA): .. code-block:: python import numpy as np - from qiskit_algorithms.optimizers import QNSPSA + from qiskit_machine_learning.optimizers import QNSPSA from qiskit.circuit.library import PauliTwoDesign from qiskit.primitives import Estimator, Sampler from qiskit.quantum_info import Pauli diff --git a/qiskit_machine_learning/optimizers/spsa.py b/qiskit_machine_learning/optimizers/spsa.py index 70a49f20b..c6579811e 100644 --- a/qiskit_machine_learning/optimizers/spsa.py +++ b/qiskit_machine_learning/optimizers/spsa.py @@ -77,7 +77,7 @@ class SPSA(Optimizer): This component has some function that is normally random. If you want to reproduce behavior then you should set the random number generator seed in the algorithm_globals - (``qiskit_algorithms.utils.algorithm_globals.random_seed = seed``). + (``qiskit_machine_learning.utils.algorithm_globals.random_seed = seed``). Examples: @@ -88,7 +88,7 @@ class SPSA(Optimizer): .. code-block:: python import numpy as np - from qiskit_algorithms.optimizers import SPSA + from qiskit_machine_learning.optimizers import SPSA from qiskit.circuit.library import PauliTwoDesign from qiskit.primitives import Estimator from qiskit.quantum_info import SparsePauliOp @@ -118,7 +118,7 @@ def loss(x): .. code-block:: python import numpy as np - from qiskit_algorithms.optimizers import SPSA + from qiskit_machine_learning.optimizers import SPSA def objective(x): return np.linalg.norm(x) + .04*np.random.rand(1) diff --git a/qiskit_machine_learning/optimizers/steppable_optimizer.py b/qiskit_machine_learning/optimizers/steppable_optimizer.py index bb436513b..bab6ec4a4 100644 --- a/qiskit_machine_learning/optimizers/steppable_optimizer.py +++ b/qiskit_machine_learning/optimizers/steppable_optimizer.py @@ -102,7 +102,7 @@ class SteppableOptimizer(Optimizer): import random import numpy as np - from qiskit_algorithms.optimizers import GradientDescent + from qiskit_machine_learning.optimizers import GradientDescent def objective(x): if random.choice([True, False]): diff --git a/qiskit_machine_learning/optimizers/umda.py b/qiskit_machine_learning/optimizers/umda.py index e7cc84a15..55af590cb 100644 --- a/qiskit_machine_learning/optimizers/umda.py +++ b/qiskit_machine_learning/optimizers/umda.py @@ -71,8 +71,8 @@ class UMDA(Optimizer): .. code-block:: python - from qiskit_algorithms.optimizers import UMDA - from qiskit_algorithms import QAOA + from qiskit_machine_learning.optimizers import UMDA + from qiskit_machine_learning import QAOA from qiskit.quantum_info import Pauli from qiskit.primitives import Sampler @@ -106,7 +106,7 @@ class UMDA(Optimizer): This component has some function that is normally random. If you want to reproduce behavior then you should set the random number generator seed in the algorithm_globals - (``qiskit_algorithms.utils.algorithm_globals.random_seed = seed``). + (``qiskit_machine_learning.utils.algorithm_globals.random_seed = seed``). References: diff --git a/qiskit_machine_learning/state_fidelities/__init__.py b/qiskit_machine_learning/state_fidelities/__init__.py index 231e23242..19a49ba5a 100644 --- a/qiskit_machine_learning/state_fidelities/__init__.py +++ b/qiskit_machine_learning/state_fidelities/__init__.py @@ -10,11 +10,11 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. """ -State Fidelities (:mod:`qiskit_algorithms.state_fidelities`) +State Fidelities (:mod:`qiskit_machine_learning.state_fidelities`) ============================================================ Algorithms that compute the fidelity of pairs of quantum states. -.. currentmodule:: qiskit_algorithms.state_fidelities +.. currentmodule:: qiskit_machine_learning.state_fidelities State Fidelities ---------------- diff --git a/qiskit_machine_learning/utils/algorithm_globals.py b/qiskit_machine_learning/utils/algorithm_globals.py index c762dba6e..5d85af3c1 100644 --- a/qiskit_machine_learning/utils/algorithm_globals.py +++ b/qiskit_machine_learning/utils/algorithm_globals.py @@ -13,9 +13,9 @@ """ utils.algorithm_globals ======================= -Common (global) properties used across qiskit_algorithms. +Common (global) properties used across qiskit_machine_learning. -.. currentmodule:: qiskit_algorithms.utils.algorithm_globals +.. currentmodule:: qiskit_machine_learning.utils.algorithm_globals Includes: @@ -50,7 +50,7 @@ class QiskitAlgorithmGlobals: # calls off to it). In the future when that does not exist this has similar code # in the except blocks here, as noted above, that will take over. By delegating # to the Qiskit instance it means that any existing code that uses that continues - # to work. Logic here in qiskit_algorithms though uses this instance and the + # to work. Logic here in qiskit_machine_learning though uses this instance and the # random check here has logic to warn if the seed here is not the same as the Qiskit # version so we can detect direct usage of the Qiskit version and alert the user to # change their code to use this. So simply changing from: @@ -114,7 +114,7 @@ def random(self) -> np.random.Generator: warnings.warn( "Using random that is seeded via qiskit.utils algorithm_globals is deprecated " "since version 0.2.0. Instead set random_seed directly to " - "qiskit_algorithms.utils algorithm_globals.", + "qiskit_machine_learning.utils algorithm_globals.", category=DeprecationWarning, stacklevel=2, ) diff --git a/qiskit_machine_learning/variational_algorithm.py b/qiskit_machine_learning/variational_algorithm.py index ad5ddd30a..2b863a8ca 100644 --- a/qiskit_machine_learning/variational_algorithm.py +++ b/qiskit_machine_learning/variational_algorithm.py @@ -23,7 +23,7 @@ This component has some function that is normally random. If you want to reproduce behavior then you should set the random number generator seed in the algorithm_globals - (``qiskit_algorithms.utils.algorithm_globals.random_seed = seed``). + (``qiskit_machine_learning.utils.algorithm_globals.random_seed = seed``). """ from __future__ import annotations From 0c5825c2b148e4fdbd327a51353fd0b08147981a Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Thu, 1 Aug 2024 18:26:54 +0200 Subject: [PATCH 44/85] Styling --- .../kernels/trainable_fidelity_quantum_kernel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit_machine_learning/kernels/trainable_fidelity_quantum_kernel.py b/qiskit_machine_learning/kernels/trainable_fidelity_quantum_kernel.py index 0b976c440..196113781 100644 --- a/qiskit_machine_learning/kernels/trainable_fidelity_quantum_kernel.py +++ b/qiskit_machine_learning/kernels/trainable_fidelity_quantum_kernel.py @@ -28,8 +28,8 @@ class TrainableFidelityQuantumKernel(TrainableKernel, FidelityQuantumKernel): r""" An implementation of the quantum kernel that is based on the - :class:`~qiskit_machine_learning.state_fidelities.BaseStateFidelity` algorithm and provides ability to - train it. + :class:`~qiskit_machine_learning.state_fidelities.BaseStateFidelity` algorithm + and provides ability to train it. Finding good quantum kernels for a specific machine learning task is a big challenge in quantum machine learning. One way to choose the kernel is to add trainable parameters to the feature From c0974f9df6e17c0ee492a6b092d257b03cf0a39c Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Thu, 1 Aug 2024 18:27:37 +0200 Subject: [PATCH 45/85] Update qiskit_machine_learning/optimizers/gradient_descent.py Co-authored-by: Declan Millar --- qiskit_machine_learning/optimizers/gradient_descent.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/qiskit_machine_learning/optimizers/gradient_descent.py b/qiskit_machine_learning/optimizers/gradient_descent.py index 09a950770..e33aacec0 100644 --- a/qiskit_machine_learning/optimizers/gradient_descent.py +++ b/qiskit_machine_learning/optimizers/gradient_descent.py @@ -177,10 +177,9 @@ def grad(x): def __init__( self, maxiter: int = 100, - learning_rate: float - | list[float] - | np.ndarray - | Callable[[], Generator[float, None, None]] = 0.01, + learning_rate: ( + float | list[float] | np.ndarray | Callable[[], Generator[float, None, None]] + ) = 0.01, tol: float = 1e-7, callback: CALLBACK | None = None, perturbation: float | None = None, From 7490b3852ac340956daf2c0f741fa21786503b91 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Thu, 1 Aug 2024 18:27:46 +0200 Subject: [PATCH 46/85] Update qiskit_machine_learning/optimizers/optimizer_utils/learning_rate.py Co-authored-by: Declan Millar --- .../optimizers/optimizer_utils/learning_rate.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/qiskit_machine_learning/optimizers/optimizer_utils/learning_rate.py b/qiskit_machine_learning/optimizers/optimizer_utils/learning_rate.py index 136fbd4c1..a9c159492 100644 --- a/qiskit_machine_learning/optimizers/optimizer_utils/learning_rate.py +++ b/qiskit_machine_learning/optimizers/optimizer_utils/learning_rate.py @@ -29,10 +29,9 @@ class LearningRate(Generator): def __init__( self, - learning_rate: float - | list[float] - | np.ndarray - | Callable[[], Generator[float, None, None]], + learning_rate: ( + float | list[float] | np.ndarray | Callable[[], Generator[float, None, None]] + ), ): """ Args: From d38154beef0342721cade56f0efb66d86e3f35b7 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Fri, 2 Aug 2024 11:45:04 +0200 Subject: [PATCH 47/85] Add more tests for utils --- test/utils/test_algorithm_globals.py | 80 ++++++++++++++++++ test/utils/test_set_batching.py | 53 ++++++++++++ test/utils/test_validation.py | 119 +++++++++++++++++++++++++++ 3 files changed, 252 insertions(+) create mode 100644 test/utils/test_algorithm_globals.py create mode 100644 test/utils/test_set_batching.py create mode 100644 test/utils/test_validation.py diff --git a/test/utils/test_algorithm_globals.py b/test/utils/test_algorithm_globals.py new file mode 100644 index 000000000..44a170184 --- /dev/null +++ b/test/utils/test_algorithm_globals.py @@ -0,0 +1,80 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2019, 2024. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Test QiskitAlgorithmGlobals.""" + +from test import QiskitAlgorithmsTestCase +from unittest.mock import patch +import numpy as np + +from qiskit_machine_learning.utils.algorithm_globals import QiskitAlgorithmGlobals + + +class TestQiskitAlgorithmGlobals(QiskitAlgorithmsTestCase): + """Test the QiskitAlgorithmGlobals class.""" + + def setUp(self): + super().setUp() + self.algorithm_globals = QiskitAlgorithmGlobals() + + @patch("qiskit.utils.algorithm_globals", create=True) + def test_random_seed_getter_qiskit(self, mock_qiskit_globals): + """Test random_seed getter when qiskit_machine_learning.utils.algorithm_globals + is available.""" + mock_qiskit_globals.random_seed = 42 + + seed = self.algorithm_globals.random_seed + + self.assertEqual(seed, 42) + + def test_random_seed_getter_local(self): + """Test random_seed getter when qiskit_machine_learning.utils.algorithm_globals + is not available.""" + self.algorithm_globals._random_seed = 24 + + seed = self.algorithm_globals.random_seed + + self.assertEqual(seed, 24) + + @patch("qiskit.utils.algorithm_globals", create=True) + def test_random_seed_setter_qiskit(self, mock_qiskit_globals): + """Test random_seed setter when qiskit_machine_learning.utils.algorithm_globals + is available.""" + self.algorithm_globals.random_seed = 15 + + self.assertEqual(mock_qiskit_globals.random_seed, 15) + self.assertEqual(self.algorithm_globals._random_seed, 15) + + def test_random_seed_setter_local(self): + """Test random_seed setter when qiskit_machine_learning.utils.algorithm_globals + is not available.""" + self.algorithm_globals.random_seed = 7 + + self.assertEqual(self.algorithm_globals._random_seed, 7) + self.assertIsNone(self.algorithm_globals._random) + + def test_random_property_local(self): + """Test random property when qiskit_machine_learning.utils.algorithm_globals is not available.""" + self.algorithm_globals.random_seed = 5 + rng = self.algorithm_globals.random + + self.assertEqual(self.algorithm_globals._random_seed, 5) + self.assertIsInstance(rng, np.random.Generator) + self.assertEqual(rng.bit_generator._seed_seq.entropy, 5) + + def test_random_property_local_no_seed(self): + """Test random property when qiskit_machine_learning.utils.algorithm_globals + is not available and seed is None.""" + rng = self.algorithm_globals.random + + self.assertIsNone(self.algorithm_globals._random_seed) + self.assertIsInstance(rng, np.random.Generator) diff --git a/test/utils/test_set_batching.py b/test/utils/test_set_batching.py new file mode 100644 index 000000000..da5d029f0 --- /dev/null +++ b/test/utils/test_set_batching.py @@ -0,0 +1,53 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2024. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Test setting default batch sizes for the optimizers.""" + +from test import QiskitAlgorithmsTestCase + +from unittest.mock import Mock +from qiskit_machine_learning.optimizers import Optimizer, SPSA +from qiskit_machine_learning.utils.set_batching import _set_default_batchsize + + +class TestSetDefaultBatchsize(QiskitAlgorithmsTestCase): + """Test the ``_set_default_batchsize`` utility function.""" + + def setUp(self): + super().setUp() + self.spsa_optimizer = Mock(spec=SPSA) + self.generic_optimizer = Mock(spec=Optimizer) + + def test_set_default_batchsize_for_spsa_with_none(self): + """Test setting default batchsize for SPSA when _max_evals_grouped is None.""" + self.spsa_optimizer._max_evals_grouped = None + self.spsa_optimizer.set_max_evals_grouped = Mock() + + updated = _set_default_batchsize(self.spsa_optimizer) + + self.spsa_optimizer.set_max_evals_grouped.assert_called_once_with(50) + self.assertTrue(updated) + + def test_set_default_batchsize_for_spsa_with_value(self): + """Test setting default batchsize for SPSA when _max_evals_grouped is already set.""" + self.spsa_optimizer._max_evals_grouped = 10 + + updated = _set_default_batchsize(self.spsa_optimizer) + + self.spsa_optimizer.set_max_evals_grouped.assert_not_called() + self.assertFalse(updated) + + def test_set_default_batchsize_for_generic_optimizer(self): + """Test setting default batchsize for a non-SPSA optimizer.""" + updated = _set_default_batchsize(self.generic_optimizer) + + self.assertFalse(updated) diff --git a/test/utils/test_validation.py b/test/utils/test_validation.py new file mode 100644 index 000000000..845a79d6f --- /dev/null +++ b/test/utils/test_validation.py @@ -0,0 +1,119 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2019, 2024. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Test validation functions.""" + +from test import QiskitAlgorithmsTestCase +from qiskit_machine_learning.utils.validation import ( + validate_in_set, + validate_min, + validate_min_exclusive, + validate_max, + validate_max_exclusive, + validate_range, + validate_range_exclusive, + validate_range_exclusive_min, + validate_range_exclusive_max, +) + + +class TestValidationFunctions(QiskitAlgorithmsTestCase): + """Test the validation functions.""" + + def test_validate_in_set_valid(self): + """Test validate_in_set with valid value.""" + validate_in_set("param", "a", {"a", "b", "c"}) # Should not raise + + def test_validate_in_set_invalid(self): + """Test validate_in_set with invalid value.""" + with self.assertRaises(ValueError): + validate_in_set("param", "d", {"a", "b", "c"}) + + def test_validate_min_valid(self): + """Test validate_min with valid value.""" + validate_min("param", 5, 1) # Should not raise + + def test_validate_min_invalid(self): + """Test validate_min with invalid value.""" + with self.assertRaises(ValueError): + validate_min("param", 0, 1) + + def test_validate_min_exclusive_valid(self): + """Test validate_min_exclusive with valid value.""" + validate_min_exclusive("param", 5, 1) # Should not raise + + def test_validate_min_exclusive_invalid(self): + """Test validate_min_exclusive with invalid value.""" + with self.assertRaises(ValueError): + validate_min_exclusive("param", 1, 1) + + def test_validate_max_valid(self): + """Test validate_max with valid value.""" + validate_max("param", 5, 10) # Should not raise + + def test_validate_max_invalid(self): + """Test validate_max with invalid value.""" + with self.assertRaises(ValueError): + validate_max("param", 15, 10) + + def test_validate_max_exclusive_valid(self): + """Test validate_max_exclusive with valid value.""" + validate_max_exclusive("param", 5, 10) # Should not raise + + def test_validate_max_exclusive_invalid(self): + """Test validate_max_exclusive with invalid value.""" + with self.assertRaises(ValueError): + validate_max_exclusive("param", 10, 10) + + def test_validate_range_valid(self): + """Test validate_range with valid value.""" + validate_range("param", 5, 1, 10) # Should not raise + + def test_validate_range_invalid(self): + """Test validate_range with invalid value.""" + with self.assertRaises(ValueError): + validate_range("param", 0, 1, 10) + with self.assertRaises(ValueError): + validate_range("param", 15, 1, 10) + + def test_validate_range_exclusive_valid(self): + """Test validate_range_exclusive with valid value.""" + validate_range_exclusive("param", 5, 1, 10) # Should not raise + + def test_validate_range_exclusive_invalid(self): + """Test validate_range_exclusive with invalid value.""" + with self.assertRaises(ValueError): + validate_range_exclusive("param", 1, 1, 10) + with self.assertRaises(ValueError): + validate_range_exclusive("param", 10, 1, 10) + + def test_validate_range_exclusive_min_valid(self): + """Test validate_range_exclusive_min with valid value.""" + validate_range_exclusive_min("param", 5, 1, 10) # Should not raise + + def test_validate_range_exclusive_min_invalid(self): + """Test validate_range_exclusive_min with invalid value.""" + with self.assertRaises(ValueError): + validate_range_exclusive_min("param", 1, 1, 10) + with self.assertRaises(ValueError): + validate_range_exclusive_min("param", 11, 1, 10) + + def test_validate_range_exclusive_max_valid(self): + """Test validate_range_exclusive_max with valid value.""" + validate_range_exclusive_max("param", 5, 1, 10) # Should not raise + + def test_validate_range_exclusive_max_invalid(self): + """Test validate_range_exclusive_max with invalid value.""" + with self.assertRaises(ValueError): + validate_range_exclusive_max("param", 0, 1, 10) + with self.assertRaises(ValueError): + validate_range_exclusive_max("param", 10, 1, 10) From fe021e9156d22704e41da3bff181deb419753d43 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Fri, 2 Aug 2024 14:06:55 +0200 Subject: [PATCH 48/85] Add more tests for optimizers: adam, bobyqa, gsls and imfil --- .../optimizers/adam_amsgrad.py | 74 ++++++++--------- test/optimizers/test_adam_amsgrad.py | 82 +++++++++++++++++++ test/optimizers/test_bobyqa.py | 76 +++++++++++++++++ test/optimizers/test_gsls.py | 74 +++++++++++++++++ test/optimizers/test_imfil.py | 71 ++++++++++++++++ 5 files changed, 337 insertions(+), 40 deletions(-) create mode 100644 test/optimizers/test_adam_amsgrad.py create mode 100644 test/optimizers/test_bobyqa.py create mode 100644 test/optimizers/test_gsls.py create mode 100644 test/optimizers/test_imfil.py diff --git a/qiskit_machine_learning/optimizers/adam_amsgrad.py b/qiskit_machine_learning/optimizers/adam_amsgrad.py index 5ce663212..74e332c0b 100644 --- a/qiskit_machine_learning/optimizers/adam_amsgrad.py +++ b/qiskit_machine_learning/optimizers/adam_amsgrad.py @@ -104,15 +104,14 @@ def __init__( if self._amsgrad: self._v_eff = np.zeros(1) - if self._snapshot_dir: - # pylint: disable=unspecified-encoding - with open(os.path.join(self._snapshot_dir, "adam_params.csv"), mode="w") as csv_file: - if self._amsgrad: - fieldnames = ["v", "v_eff", "m", "t"] - else: - fieldnames = ["v", "m", "t"] - writer = csv.DictWriter(csv_file, fieldnames=fieldnames) - writer.writeheader() + if self._snapshot_dir is not None: + file_path = os.path.join(self._snapshot_dir, "adam_params.csv") + if not os.path.isfile(file_path): + # pylint: disable=unspecified-encoding + with open(file_path, mode="w") as csv_file: + fieldnames = ["v", "v_eff", "m", "t"] if self._amsgrad else ["v", "m", "t"] + writer = csv.DictWriter(csv_file, fieldnames=fieldnames) + writer.writeheader() @property def settings(self) -> dict[str, Any]: @@ -148,16 +147,19 @@ def save_params(self, snapshot_dir: str) -> None: snapshot_dir: The directory to store the file in. """ # pylint: disable=unspecified-encoding - if self._amsgrad: - with open(os.path.join(snapshot_dir, "adam_params.csv"), mode="a") as csv_file: - fieldnames = ["v", "v_eff", "m", "t"] - writer = csv.DictWriter(csv_file, fieldnames=fieldnames) - writer.writerow({"v": self._v, "v_eff": self._v_eff, "m": self._m, "t": self._t}) - else: - with open(os.path.join(snapshot_dir, "adam_params.csv"), mode="a") as csv_file: - fieldnames = ["v", "m", "t"] - writer = csv.DictWriter(csv_file, fieldnames=fieldnames) - writer.writerow({"v": self._v, "m": self._m, "t": self._t}) + file_path = os.path.join(snapshot_dir, "adam_params.csv") + + if not os.path.isfile(file_path): + raise FileNotFoundError(f"The file {file_path} does not exist.") + + fieldnames = ["v", "v_eff", "m", "t"] if self._amsgrad else ["v", "m", "t"] + + with open(file_path, mode="a", newline="") as csv_file: + writer = csv.DictWriter(csv_file, fieldnames=fieldnames) + row = {"v": self._v, "m": self._m, "t": self._t} + if self._amsgrad: + row["v_eff"] = self._v_eff + writer.writerow(row) def load_params(self, load_dir: str) -> None: """Load iteration parameters for a file called ``adam_params.csv``. @@ -166,28 +168,20 @@ def load_params(self, load_dir: str) -> None: load_dir: The directory containing ``adam_params.csv``. """ # pylint: disable=unspecified-encoding - with open(os.path.join(load_dir, "adam_params.csv")) as csv_file: - if self._amsgrad: - fieldnames = ["v", "v_eff", "m", "t"] - else: - fieldnames = ["v", "m", "t"] - reader = csv.DictReader(csv_file, fieldnames=fieldnames) + file_path = os.path.join(load_dir, "adam_params.csv") + + if not os.path.isfile(file_path): + raise FileNotFoundError(f"The file {file_path} does not exist.") + + with open(file_path, mode="r", newline="") as csv_file: + reader = csv.DictReader(csv_file) + for line in reader: - v = line["v"] + self._v = np.fromstring(line["v"].strip("[]"), dtype=float, sep=" ") if self._amsgrad: - v_eff = line["v_eff"] - m = line["m"] - t = line["t"] - - v = v[1:-1] - self._v = np.fromstring(v, dtype=float, sep=" ") - if self._amsgrad: - v_eff = v_eff[1:-1] - self._v_eff = np.fromstring(v_eff, dtype=float, sep=" ") - m = m[1:-1] - self._m = np.fromstring(m, dtype=float, sep=" ") - t = t[1:-1] - self._t = int(np.fromstring(t, dtype=int, sep=" ")) + self._v_eff = np.fromstring(line["v_eff"].strip("[]"), dtype=float, sep=" ") + self._m = np.fromstring(line["m"].strip("[]"), dtype=float, sep=" ") + self._t = int(line["t"].strip("[]")) def minimize( self, @@ -235,7 +229,7 @@ def minimize( np.sqrt(self._v_eff.flatten()) + self._noise_factor ) - if self._snapshot_dir: + if self._snapshot_dir is not None: self.save_params(self._snapshot_dir) # check termination diff --git a/test/optimizers/test_adam_amsgrad.py b/test/optimizers/test_adam_amsgrad.py new file mode 100644 index 000000000..c14d8cf46 --- /dev/null +++ b/test/optimizers/test_adam_amsgrad.py @@ -0,0 +1,82 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2019, 2024. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Test of ADAM optimizer""" + +import unittest +import tempfile +from test import QiskitAlgorithmsTestCase +import numpy as np +from ddt import ddt + +from qiskit_machine_learning.optimizers import ADAM + + +@ddt +class TestOptimizerADAM(QiskitAlgorithmsTestCase): + """Test ADAM optimizer""" + + def setUp(self): + super().setUp() + self.quadratic_objective = lambda x: x[0] ** 2 + x[1] ** 2 + self.initial_point = np.array([1.0, 1.0]) + + def test_optimizer_minimize(self): + """Test ADAM optimizer minimize method""" + adam = ADAM(maxiter=150, tol=1e-6, lr=1e-1) + result = adam.minimize(self.quadratic_objective, self.initial_point) + self.assertAlmostEqual(result.fun, 0.0, places=6) + self.assertTrue(np.allclose(result.x, np.zeros_like(self.initial_point), atol=1e-2)) + + def test_optimizer_with_noise(self): + """Test ADAM optimizer with noise factor""" + adam = ADAM(maxiter=100, tol=1e-6, lr=1e-1, noise_factor=1e-2) + result = adam.minimize(self.quadratic_objective, self.initial_point) + self.assertAlmostEqual(result.fun, 0.0, places=4) + self.assertTrue(np.allclose(result.x, np.zeros_like(self.initial_point), atol=1e-2)) + + def test_amsgrad(self): + """Test ADAM optimizer with AMSGRAD variant""" + adam = ADAM(maxiter=150, tol=1e-6, lr=1e-1, amsgrad=True) + result = adam.minimize(self.quadratic_objective, self.initial_point) + self.assertAlmostEqual(result.fun, 0.0, places=6) + self.assertTrue(np.allclose(result.x, np.zeros_like(self.initial_point), atol=1e-2)) + + def test_save_load_params(self): + """Test save and load optimizer parameters""" + with tempfile.TemporaryDirectory() as tmpdir: + adam = ADAM(maxiter=100, tol=1e-6, lr=1e-1, snapshot_dir=tmpdir) + adam.minimize(self.quadratic_objective, self.initial_point) + new_adam = ADAM(snapshot_dir=tmpdir) + new_adam.load_params(tmpdir) + + self.assertTrue(np.allclose(adam._m, new_adam._m)) + self.assertTrue(np.allclose(adam._v, new_adam._v)) + self.assertEqual(adam._t, new_adam._t) + + def test_settings(self): + """Test settings property""" + adam = ADAM(maxiter=100, tol=1e-6, lr=1e-1) + settings = adam.settings + self.assertEqual(settings["maxiter"], 100) + self.assertEqual(settings["tol"], 1e-6) + self.assertEqual(settings["lr"], 1e-1) + self.assertEqual(settings["beta_1"], 0.9) + self.assertEqual(settings["beta_2"], 0.99) + self.assertEqual(settings["noise_factor"], 1e-8) + self.assertEqual(settings["eps"], 1e-10) + self.assertEqual(settings["amsgrad"], False) + self.assertEqual(settings["snapshot_dir"], None) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/optimizers/test_bobyqa.py b/test/optimizers/test_bobyqa.py new file mode 100644 index 000000000..8d3f90684 --- /dev/null +++ b/test/optimizers/test_bobyqa.py @@ -0,0 +1,76 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2019, 2024. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Test of BOBYQA optimizer""" + +import unittest +from test import QiskitAlgorithmsTestCase +import numpy as np +from ddt import ddt +from qiskit.exceptions import MissingOptionalLibraryError +from qiskit_machine_learning.optimizers import BOBYQA + + +@ddt +class TestOptimizerBOBYQA(QiskitAlgorithmsTestCase): + """Test BOBYQA optimizer""" + + def setUp(self): + super().setUp() + self.quadratic_objective = lambda x: x[0] ** 2 + x[1] ** 2 + self.initial_point = np.array([1.0, 1.0]) + self.bounds = [(-2.0, 2.0), (-2.0, 2.0)] + + def test_optimizer_minimize(self): + """Test BOBYQA optimizer minimize method""" + try: + bobyqa = BOBYQA(maxiter=100) + result = bobyqa.minimize( + self.quadratic_objective, self.initial_point, bounds=self.bounds + ) + self.assertAlmostEqual(result.fun, 0.0, places=6) + self.assertTrue(np.allclose(result.x, np.zeros_like(self.initial_point), atol=1e-2)) + except MissingOptionalLibraryError as error: + self.skipTest(str(error)) + + def test_optimizer_without_bounds(self): + """Test BOBYQA optimizer without bounds (should raise an error)""" + try: + bobyqa = BOBYQA(maxiter=100) + with self.assertRaises(ValueError): + bobyqa.minimize(self.quadratic_objective, self.initial_point) + except MissingOptionalLibraryError as error: + self.skipTest(str(error)) + + def test_settings(self): + """Test settings property""" + try: + bobyqa = BOBYQA(maxiter=100) + settings = bobyqa.settings + self.assertEqual(settings["maxiter"], 100) + except MissingOptionalLibraryError as error: + self.skipTest(str(error)) + + def test_support_level(self): + """Test support level""" + try: + bobyqa = BOBYQA(maxiter=100) + support_level = bobyqa.get_support_level() + self.assertEqual(support_level["gradient"], "ignored") + self.assertEqual(support_level["bounds"], "required") + self.assertEqual(support_level["initial_point"], "required") + except MissingOptionalLibraryError as error: + self.skipTest(str(error)) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/optimizers/test_gsls.py b/test/optimizers/test_gsls.py new file mode 100644 index 000000000..fe36699e4 --- /dev/null +++ b/test/optimizers/test_gsls.py @@ -0,0 +1,74 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2018, 2024. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""TestGSLS""" + +import unittest +import numpy as np +from qiskit_machine_learning.optimizers.gsls import GSLS +from qiskit_machine_learning.optimizers.optimizer import OptimizerResult + + +class TestGSLS(unittest.TestCase): + """TestGSLS""" + + def setUp(self): + self.optimizer = GSLS(maxiter=200, sampling_radius=0.1, initial_step_size=0.01) + + def test_minimize_rosenbrock(self): + """Tests minimize.""" + + def rosenbrock(x): + """Defines the calculation strategy.""" + return sum(100.0 * (x[1:] - x[:-1] ** 2.0) ** 2.0 + (1 - x[:-1]) ** 2.0) + + starting_point = np.array([0.0, 0.0]) + result = self.optimizer.minimize(fun=rosenbrock, x0=starting_point) + self.assertIsInstance(result, OptimizerResult) + self.assertLess(result.fun, 1.0) + self.assertLess(result.nfev, 2000) + + def test_minimize_bounds(self): + """Testing the minimize bounds.""" + + def objective(x): + """Defines the objective.""" + return np.sum(x**2) + + starting_point = np.array([0.1, 0.1]) + bounds = [(-0.05, 0.05), (-0.05, 0.05)] + result = self.optimizer.minimize(fun=objective, x0=starting_point, bounds=bounds) + self.assertIsInstance(result, OptimizerResult) + self.assertTrue(np.all(result.x >= -0.05) and np.all(result.x <= 0.05)) + + def test_settings(self): + """Testing the settings.""" + settings = self.optimizer.settings + self.assertEqual(settings["maxiter"], 200) + self.assertEqual(settings["sampling_radius"], 0.1) + self.assertEqual(settings["initial_step_size"], 0.01) + + def test_sample_set(self): + """Testing the sample set.""" + n = 2 + x = np.array([0.0, 0.0]) + num_points = 10 + var_lb = np.array([-1.0, -1.0]) + var_ub = np.array([1.0, 1.0]) + directions, points = self.optimizer.sample_set(n, x, var_lb, var_ub, num_points) + self.assertEqual(directions.shape, (num_points, n)) + self.assertEqual(points.shape, (num_points, n)) + self.assertTrue(np.all(points >= var_lb) and np.all(points <= var_ub)) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/optimizers/test_imfil.py b/test/optimizers/test_imfil.py new file mode 100644 index 000000000..0181f95ef --- /dev/null +++ b/test/optimizers/test_imfil.py @@ -0,0 +1,71 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2019, 2024. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""TestIMFIL""" + +import unittest +import numpy as np +from qiskit_machine_learning.optimizers.imfil import IMFIL +from qiskit_machine_learning.optimizers.optimizer import OptimizerResult +from qiskit_machine_learning.utils import optionals + + +class TestIMFIL(unittest.TestCase): + """TestIMFIL""" + + def setUp(self): + if not optionals.HAS_SKQUANT: + self.skipTest("skquant is required for IMFIL optimizer") + self.optimizer = IMFIL(maxiter=500) + + def test_support_level(self): + """Test support level.""" + support_levels = self.optimizer.get_support_level() + self.assertEqual(support_levels["gradient"], 0) + self.assertEqual(support_levels["bounds"], 2) + self.assertEqual(support_levels["initial_point"], 2) + + def test_minimize_rosenbrock(self): + """Testing minimize.""" + + def rosenbrock(x): + """Calculation strategy.""" + return sum(100.0 * (x[1:] - x[:-1] ** 2.0) ** 2.0 + (1 - x[:-1]) ** 2.0) + + starting_point = np.array([-1.2, 1.0]) + bounds = [(-2.0, 2.0), (-1.0, 3.0)] + result = self.optimizer.minimize(fun=rosenbrock, x0=starting_point, bounds=bounds) + self.assertIsInstance(result, OptimizerResult) + self.assertLess(result.fun, 1e-4) + self.assertLess(result.nfev, 2000) + + def test_minimize_bounds(self): + """Testing minimize.""" + + def objective(x): + """Defining the objective""" + return np.sum(x**2) + + starting_point = np.array([1.0, 1.0]) + bounds = [(-0.5, 0.5), (-0.5, 0.5)] + result = self.optimizer.minimize(fun=objective, x0=starting_point, bounds=bounds) + self.assertIsInstance(result, OptimizerResult) + self.assertTrue(np.all(result.x >= -0.5) and np.all(result.x <= 0.5)) + + def test_settings(self): + """Test settings.""" + settings = self.optimizer.settings + self.assertEqual(settings["maxiter"], 500) + + +if __name__ == "__main__": + unittest.main() From 51e3ea7f61eedccef50b3c3356b31b018fe16ff3 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Fri, 2 Aug 2024 14:26:17 +0200 Subject: [PATCH 49/85] Fix random seed for volatile optimizers --- test/optimizers/test_adam_amsgrad.py | 2 ++ test/optimizers/test_bobyqa.py | 2 ++ test/optimizers/test_gradient_descent.py | 3 ++- test/optimizers/test_gsls.py | 7 ++++++- test/optimizers/test_imfil.py | 8 ++++++-- test/optimizers/test_umda.py | 5 +++++ 6 files changed, 23 insertions(+), 4 deletions(-) diff --git a/test/optimizers/test_adam_amsgrad.py b/test/optimizers/test_adam_amsgrad.py index c14d8cf46..060accb34 100644 --- a/test/optimizers/test_adam_amsgrad.py +++ b/test/optimizers/test_adam_amsgrad.py @@ -19,6 +19,7 @@ from ddt import ddt from qiskit_machine_learning.optimizers import ADAM +from qiskit_machine_learning.utils import algorithm_globals @ddt @@ -27,6 +28,7 @@ class TestOptimizerADAM(QiskitAlgorithmsTestCase): def setUp(self): super().setUp() + algorithm_globals.random_seed = 50 self.quadratic_objective = lambda x: x[0] ** 2 + x[1] ** 2 self.initial_point = np.array([1.0, 1.0]) diff --git a/test/optimizers/test_bobyqa.py b/test/optimizers/test_bobyqa.py index 8d3f90684..5989841f9 100644 --- a/test/optimizers/test_bobyqa.py +++ b/test/optimizers/test_bobyqa.py @@ -18,6 +18,7 @@ from ddt import ddt from qiskit.exceptions import MissingOptionalLibraryError from qiskit_machine_learning.optimizers import BOBYQA +from qiskit_machine_learning.utils import algorithm_globals @ddt @@ -26,6 +27,7 @@ class TestOptimizerBOBYQA(QiskitAlgorithmsTestCase): def setUp(self): super().setUp() + algorithm_globals.random_seed = 50 self.quadratic_objective = lambda x: x[0] ** 2 + x[1] ** 2 self.initial_point = np.array([1.0, 1.0]) self.bounds = [(-2.0, 2.0), (-2.0, 2.0)] diff --git a/test/optimizers/test_gradient_descent.py b/test/optimizers/test_gradient_descent.py index 5dcc05223..4091ad113 100644 --- a/test/optimizers/test_gradient_descent.py +++ b/test/optimizers/test_gradient_descent.py @@ -19,6 +19,7 @@ from qiskit_machine_learning.optimizers import GradientDescent, GradientDescentState from qiskit_machine_learning.optimizers.steppable_optimizer import TellData, AskData +from qiskit_machine_learning.utils import algorithm_globals class TestGradientDescent(QiskitAlgorithmsTestCase): @@ -26,7 +27,7 @@ class TestGradientDescent(QiskitAlgorithmsTestCase): def setUp(self): super().setUp() - np.random.seed(12) + algorithm_globals.random_seed = 50 self.initial_point = np.array([1, 1, 1, 1, 0]) def objective(self, x): diff --git a/test/optimizers/test_gsls.py b/test/optimizers/test_gsls.py index fe36699e4..967390ebd 100644 --- a/test/optimizers/test_gsls.py +++ b/test/optimizers/test_gsls.py @@ -14,14 +14,19 @@ import unittest import numpy as np +from test import QiskitAlgorithmsTestCase + from qiskit_machine_learning.optimizers.gsls import GSLS from qiskit_machine_learning.optimizers.optimizer import OptimizerResult +from qiskit_machine_learning.utils import algorithm_globals -class TestGSLS(unittest.TestCase): +class TestGSLS(QiskitAlgorithmsTestCase): """TestGSLS""" def setUp(self): + super().setUp() + algorithm_globals.random_seed = 50 self.optimizer = GSLS(maxiter=200, sampling_radius=0.1, initial_step_size=0.01) def test_minimize_rosenbrock(self): diff --git a/test/optimizers/test_imfil.py b/test/optimizers/test_imfil.py index 0181f95ef..136e1d24b 100644 --- a/test/optimizers/test_imfil.py +++ b/test/optimizers/test_imfil.py @@ -14,15 +14,19 @@ import unittest import numpy as np +from test import QiskitAlgorithmsTestCase + from qiskit_machine_learning.optimizers.imfil import IMFIL from qiskit_machine_learning.optimizers.optimizer import OptimizerResult -from qiskit_machine_learning.utils import optionals +from qiskit_machine_learning.utils import optionals, algorithm_globals -class TestIMFIL(unittest.TestCase): +class TestIMFIL(QiskitAlgorithmsTestCase): """TestIMFIL""" def setUp(self): + super().setUp() + algorithm_globals.random_seed = 50 if not optionals.HAS_SKQUANT: self.skipTest("skquant is required for IMFIL optimizer") self.optimizer = IMFIL(maxiter=500) diff --git a/test/optimizers/test_umda.py b/test/optimizers/test_umda.py index 571cf9fb7..6a57cd0c5 100644 --- a/test/optimizers/test_umda.py +++ b/test/optimizers/test_umda.py @@ -24,6 +24,11 @@ class TestUMDA(QiskitAlgorithmsTestCase): """Tests for the UMDA optimizer.""" + def setUp(self): + """Set the problem.""" + super().setUp() + algorithm_globals.random_seed = 50 + def test_get_set(self): """Test if getters and setters work as expected""" umda = UMDA(maxiter=1, size_gen=20) From fb4fc39e2f28a26734a321b287385b0eb6dd9399 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Fri, 2 Aug 2024 14:34:22 +0200 Subject: [PATCH 50/85] Fix random seed for volatile optimizers --- test/optimizers/test_gsls.py | 2 +- test/optimizers/test_imfil.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/optimizers/test_gsls.py b/test/optimizers/test_gsls.py index 967390ebd..86206e447 100644 --- a/test/optimizers/test_gsls.py +++ b/test/optimizers/test_gsls.py @@ -13,8 +13,8 @@ """TestGSLS""" import unittest -import numpy as np from test import QiskitAlgorithmsTestCase +import numpy as np from qiskit_machine_learning.optimizers.gsls import GSLS from qiskit_machine_learning.optimizers.optimizer import OptimizerResult diff --git a/test/optimizers/test_imfil.py b/test/optimizers/test_imfil.py index 136e1d24b..53b7f0dc8 100644 --- a/test/optimizers/test_imfil.py +++ b/test/optimizers/test_imfil.py @@ -13,8 +13,8 @@ """TestIMFIL""" import unittest -import numpy as np from test import QiskitAlgorithmsTestCase +import numpy as np from qiskit_machine_learning.optimizers.imfil import IMFIL from qiskit_machine_learning.optimizers.optimizer import OptimizerResult From 3cb3850b948a62c8278cd5ff10acf647e21606c0 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Fri, 2 Aug 2024 16:22:37 +0200 Subject: [PATCH 51/85] Add more tests --- .../optimizers/nlopts/__init__.py | 7 + test/optimizers/test_nft.py | 58 ++++++ test/optimizers/test_nlopts.py | 175 ++++++++++++++++++ test/optimizers/test_snobfit.py | 102 ++++++++++ 4 files changed, 342 insertions(+) create mode 100644 test/optimizers/test_nft.py create mode 100644 test/optimizers/test_nlopts.py create mode 100644 test/optimizers/test_snobfit.py diff --git a/qiskit_machine_learning/optimizers/nlopts/__init__.py b/qiskit_machine_learning/optimizers/nlopts/__init__.py index b5fcd3d6c..59bf5f86c 100644 --- a/qiskit_machine_learning/optimizers/nlopts/__init__.py +++ b/qiskit_machine_learning/optimizers/nlopts/__init__.py @@ -11,3 +11,10 @@ # that they have been altered from the originals. """NLopt based global optimizers""" + +from .crs import CRS +from .direct_l import DIRECT_L +from .direct_l_rand import DIRECT_L_RAND +from .esch import ESCH +from .isres import ISRES +from .nloptimizer import NLoptOptimizer diff --git a/test/optimizers/test_nft.py b/test/optimizers/test_nft.py new file mode 100644 index 000000000..3ddde5d4d --- /dev/null +++ b/test/optimizers/test_nft.py @@ -0,0 +1,58 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2019, 2024. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Unit tests for the Nakanishi-Fujii-Todo (NFT) optimizer.""" + +import unittest +from test import QiskitAlgorithmsTestCase +import numpy as np + +from qiskit_machine_learning.optimizers.nft import NFT +from qiskit_machine_learning.utils import algorithm_globals + + +class TestNFTOptimizer(QiskitAlgorithmsTestCase): + """Test cases for the NFT optimizer.""" + + def setUp(self): + """Set up the optimizer for testing.""" + super().setUp() + algorithm_globals.random_seed = 50 + self.optimizer = NFT(maxiter=400, maxfev=1000) + + def test_optimizer_support(self): + """Test the optimizer support levels.""" + support_levels = self.optimizer.get_support_level() + self.assertIn("gradient", support_levels) + self.assertIn("bounds", support_levels) + self.assertIn("initial_point", support_levels) + + def test_minimize_simple_quadratic(self): + """Test the optimizer on a simple quadratic function.""" + + def quadratic_function(x): + """Test function.""" + return np.sum((x - 3) ** 2) + + initial_point = np.array([0.0, 0.0]) + result = self.optimizer.minimize(fun=quadratic_function, x0=initial_point) + self.assertTrue(np.allclose(result.x, np.array([3.0, 3.0]), atol=1e-2)) + + def test_optimizer_settings(self): + """Test the optimizer settings.""" + settings = self.optimizer.settings + self.assertEqual(settings["maxiter"], 400) + self.assertEqual(settings["maxfev"], 1000) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/optimizers/test_nlopts.py b/test/optimizers/test_nlopts.py new file mode 100644 index 000000000..474837e70 --- /dev/null +++ b/test/optimizers/test_nlopts.py @@ -0,0 +1,175 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2018, 2024. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Unit tests for NLopt optimizers.""" + +import unittest +from test import QiskitAlgorithmsTestCase +import numpy as np +from qiskit.exceptions import MissingOptionalLibraryError +from qiskit_machine_learning.optimizers.nlopts import CRS, DIRECT_L, DIRECT_L_RAND, ESCH, ISRES +from qiskit_machine_learning.utils import algorithm_globals + + +class TestNLoptOptimizer(QiskitAlgorithmsTestCase): + """Test cases for NLoptOptimizer and its derived classes.""" + + def setUp(self): + """Set up optimizers for testing.""" + super().setUp() + algorithm_globals.random_seed = 50 + self.max_evals = 200 + self.bounds = [(-5, 5), (-5, 5)] + + def test_optimizer_support(self): + """Test the support levels of NLopt optimizers.""" + try: + optimizers = [ + CRS(max_evals=self.max_evals), + DIRECT_L(max_evals=self.max_evals), + DIRECT_L_RAND(max_evals=self.max_evals), + ESCH(max_evals=self.max_evals), + ISRES(max_evals=self.max_evals), + ] + except MissingOptionalLibraryError as error: + self.skipTest(str(error)) + + for optimizer in optimizers: + support_levels = optimizer.get_support_level() + self.assertIn("gradient", support_levels) + self.assertIn("bounds", support_levels) + self.assertIn("initial_point", support_levels) + + def test_optimizer_settings(self): + """Test the optimizer settings.""" + try: + optimizers = [ + CRS(max_evals=self.max_evals), + DIRECT_L(max_evals=self.max_evals), + DIRECT_L_RAND(max_evals=self.max_evals), + ESCH(max_evals=self.max_evals), + ISRES(max_evals=self.max_evals), + ] + except MissingOptionalLibraryError as error: + self.skipTest(str(error)) + + for optimizer in optimizers: + settings = optimizer.settings + self.assertEqual(settings["max_evals"], self.max_evals) + + def test_minimize_simple_quadratic(self): + """Test optimizers on a simple quadratic function.""" + + def quadratic_function(params): + """Test function.""" + return np.sum((params - 4) ** 2) + + initial_point = np.array([0.0, 0.0]) + + try: + optimizers = [ + CRS(max_evals=self.max_evals), + DIRECT_L(max_evals=self.max_evals), + DIRECT_L_RAND(max_evals=self.max_evals), + ESCH(max_evals=self.max_evals), + ISRES(max_evals=self.max_evals), + ] + except MissingOptionalLibraryError as error: + self.skipTest(str(error)) + + for optimizer in optimizers: + result = optimizer.minimize( + fun=quadratic_function, x0=initial_point, bounds=self.bounds + ) + self.assertTrue(np.allclose(result.x, np.array([4.0, 4.0]), atol=1e-2)) + + def test_minimize_rosenbrock(self): + """Test optimizers on the Rosenbrock function.""" + + def rosenbrock_function(params): + """Test function.""" + return sum(100.0 * (params[1:] - params[:-1] ** 2.0) ** 2.0 + (1 - params[:-1]) ** 2.0) + + initial_point = np.array([1.5, 1.5]) + + try: + optimizers = [ + CRS(max_evals=self.max_evals), + DIRECT_L(max_evals=self.max_evals), + DIRECT_L_RAND(max_evals=self.max_evals), + ESCH(max_evals=self.max_evals), + ISRES(max_evals=self.max_evals), + ] + except MissingOptionalLibraryError as error: + self.skipTest(str(error)) + + for optimizer in optimizers: + result = optimizer.minimize( + fun=rosenbrock_function, x0=initial_point, bounds=self.bounds + ) + self.assertLess(result.fun, 1e-4) + self.assertLess(result.nfev, self.max_evals) + + def test_minimize_with_invalid_bounds(self): + """Test optimizers with invalid bounds.""" + + def quadratic_function(params): + """Test function.""" + return np.sum((params - 2) ** 2) + + initial_point = np.array([1.0, 1.0]) + invalid_bounds = [(None, 5), (None, 5)] # Invalid bounds with None + + try: + optimizers = [ + CRS(max_evals=self.max_evals), + DIRECT_L(max_evals=self.max_evals), + DIRECT_L_RAND(max_evals=self.max_evals), + ESCH(max_evals=self.max_evals), + ISRES(max_evals=self.max_evals), + ] + except MissingOptionalLibraryError as error: + self.skipTest(str(error)) + + for optimizer in optimizers: + with self.assertRaises(ValueError): + optimizer.minimize(fun=quadratic_function, x0=initial_point, bounds=invalid_bounds) + + def test_minimize_with_clamped_initial_point(self): + """Test optimizers with an initial point that is clamped to bounds.""" + + def quadratic_function(params): + """Test function.""" + return np.sum((params - 2) ** 2) + + initial_point = np.array([10.0, -10.0]) + + try: + optimizers = [ + CRS(max_evals=self.max_evals), + DIRECT_L(max_evals=self.max_evals), + DIRECT_L_RAND(max_evals=self.max_evals), + ESCH(max_evals=self.max_evals), + ISRES(max_evals=self.max_evals), + ] + except MissingOptionalLibraryError as error: + self.skipTest(str(error)) + + for optimizer in optimizers: + result = optimizer.minimize( + fun=quadratic_function, x0=initial_point, bounds=self.bounds + ) + self.assertTrue(np.allclose(result.x, np.array([5.0, 0.0]), atol=1e-2)) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/optimizers/test_snobfit.py b/test/optimizers/test_snobfit.py new file mode 100644 index 000000000..113487a44 --- /dev/null +++ b/test/optimizers/test_snobfit.py @@ -0,0 +1,102 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2019, 2024. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Unit tests for the SNOBFIT optimizer.""" + +import unittest +from test import QiskitAlgorithmsTestCase +import numpy as np +from qiskit.exceptions import MissingOptionalLibraryError +from qiskit_machine_learning.optimizers.snobfit import SNOBFIT +from qiskit_machine_learning.utils import algorithm_globals + + +class TestSNOBFITOptimizer(QiskitAlgorithmsTestCase): + """Test cases for the SNOBFIT optimizer.""" + + def setUp(self): + """Set up the optimizer for testing.""" + super().setUp() + algorithm_globals.random_seed = 50 + try: + self.optimizer = SNOBFIT(maxiter=200, maxfail=5, verbose=True) + + except MissingOptionalLibraryError as error: + self.skipTest(str(error)) + + def test_optimizer_support(self): + """Test the optimizer support levels.""" + support_levels = self.optimizer.get_support_level() + self.assertIn("gradient", support_levels) + self.assertIn("bounds", support_levels) + self.assertIn("initial_point", support_levels) + + def test_minimize_simple_quadratic(self): + """Test the optimizer on a simple quadratic function.""" + + def quadratic_function(params): + """Test function.""" + return np.sum((params - 4) ** 2) + + initial_point = np.array([0.0, 0.0]) + bounds = [(0, 10), (0, 10)] + result = self.optimizer.minimize(fun=quadratic_function, x0=initial_point, bounds=bounds) + self.assertTrue(np.allclose(result.x, np.array([4.0, 4.0]), atol=1e-2)) + + def test_minimize_rosenbrock(self): + """Test the optimizer on the Rosenbrock function.""" + + def rosenbrock_function(params): + """Test function.""" + return sum(100.0 * (params[1:] - params[:-1] ** 2.0) ** 2.0 + (1 - params[:-1]) ** 2.0) + + initial_point = np.array([1.5, 1.5]) + bounds = [(-5, 5), (-5, 5)] + result = self.optimizer.minimize(fun=rosenbrock_function, x0=initial_point, bounds=bounds) + self.assertLess(result.fun, 1e-4) + self.assertLess(result.nfev, 200) + + def test_optimizer_settings(self): + """Test the optimizer settings.""" + settings = self.optimizer.settings + self.assertEqual(settings["maxiter"], 200) + self.assertEqual(settings["maxfail"], 5) + self.assertIsInstance(settings["maxmp"], int) + self.assertTrue(settings["verbose"]) + + def test_minimize_with_invalid_bounds(self): + """Test the optimizer with invalid bounds.""" + + def quadratic_function(params): + """Test function.""" + return np.sum((params - 2) ** 2) + + initial_point = np.array([1.0, 1.0]) + bounds = [(None, 5), (None, 5)] # Invalid bounds with None + with self.assertRaises(ValueError): + self.optimizer.minimize(fun=quadratic_function, x0=initial_point, bounds=bounds) + + def test_minimize_with_clamped_initial_point(self): + """Test the optimizer with an initial point that is clamped to bounds.""" + + def quadratic_function(params): + """Test function.""" + return np.sum((params - 2) ** 2) + + initial_point = np.array([10.0, -10.0]) + bounds = [(0, 5), (0, 5)] + result = self.optimizer.minimize(fun=quadratic_function, x0=initial_point, bounds=bounds) + self.assertTrue(np.allclose(result.x, np.array([5.0, 0.0]), atol=1e-2)) + + +if __name__ == "__main__": + unittest.main() From 3da9109b0a8983a341a2cc69677bcf3f6ed9b2b0 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Fri, 2 Aug 2024 16:33:12 +0200 Subject: [PATCH 52/85] Pylint dict --- .pylintdict | 1 + 1 file changed, 1 insertion(+) diff --git a/.pylintdict b/.pylintdict index 5dcc38c50..40d21df3c 100644 --- a/.pylintdict +++ b/.pylintdict @@ -596,3 +596,4 @@ zz ω assertRaises RuntimeError +Rosenbrock From d34c4c9a8b3231e98fadda9219df62795bfa5e4c Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Fri, 2 Aug 2024 17:03:53 +0200 Subject: [PATCH 53/85] Activate scikit-quant-0.8.2 --- .../install-machine-learning/action.yml | 2 +- test/optimizers/test_bobyqa.py | 13 +---------- test/optimizers/test_imfil.py | 23 +------------------ 3 files changed, 3 insertions(+), 35 deletions(-) diff --git a/.github/actions/install-machine-learning/action.yml b/.github/actions/install-machine-learning/action.yml index 51f37f443..f4784b2fe 100644 --- a/.github/actions/install-machine-learning/action.yml +++ b/.github/actions/install-machine-learning/action.yml @@ -17,6 +17,6 @@ runs: using: "composite" steps: - run : | - pip install -e .[torch,sparse] + pip install -e .[torch,sparse,scikit-quant] pip install -U -c constraints.txt -r requirements-dev.txt shell: bash diff --git a/test/optimizers/test_bobyqa.py b/test/optimizers/test_bobyqa.py index 5989841f9..360c82e77 100644 --- a/test/optimizers/test_bobyqa.py +++ b/test/optimizers/test_bobyqa.py @@ -48,7 +48,7 @@ def test_optimizer_without_bounds(self): """Test BOBYQA optimizer without bounds (should raise an error)""" try: bobyqa = BOBYQA(maxiter=100) - with self.assertRaises(ValueError): + with self.assertRaises(IndexError): bobyqa.minimize(self.quadratic_objective, self.initial_point) except MissingOptionalLibraryError as error: self.skipTest(str(error)) @@ -62,17 +62,6 @@ def test_settings(self): except MissingOptionalLibraryError as error: self.skipTest(str(error)) - def test_support_level(self): - """Test support level""" - try: - bobyqa = BOBYQA(maxiter=100) - support_level = bobyqa.get_support_level() - self.assertEqual(support_level["gradient"], "ignored") - self.assertEqual(support_level["bounds"], "required") - self.assertEqual(support_level["initial_point"], "required") - except MissingOptionalLibraryError as error: - self.skipTest(str(error)) - if __name__ == "__main__": unittest.main() diff --git a/test/optimizers/test_imfil.py b/test/optimizers/test_imfil.py index 53b7f0dc8..7a9417bce 100644 --- a/test/optimizers/test_imfil.py +++ b/test/optimizers/test_imfil.py @@ -31,27 +31,6 @@ def setUp(self): self.skipTest("skquant is required for IMFIL optimizer") self.optimizer = IMFIL(maxiter=500) - def test_support_level(self): - """Test support level.""" - support_levels = self.optimizer.get_support_level() - self.assertEqual(support_levels["gradient"], 0) - self.assertEqual(support_levels["bounds"], 2) - self.assertEqual(support_levels["initial_point"], 2) - - def test_minimize_rosenbrock(self): - """Testing minimize.""" - - def rosenbrock(x): - """Calculation strategy.""" - return sum(100.0 * (x[1:] - x[:-1] ** 2.0) ** 2.0 + (1 - x[:-1]) ** 2.0) - - starting_point = np.array([-1.2, 1.0]) - bounds = [(-2.0, 2.0), (-1.0, 3.0)] - result = self.optimizer.minimize(fun=rosenbrock, x0=starting_point, bounds=bounds) - self.assertIsInstance(result, OptimizerResult) - self.assertLess(result.fun, 1e-4) - self.assertLess(result.nfev, 2000) - def test_minimize_bounds(self): """Testing minimize.""" @@ -59,7 +38,7 @@ def objective(x): """Defining the objective""" return np.sum(x**2) - starting_point = np.array([1.0, 1.0]) + starting_point = np.array([0.5, 0.5]) bounds = [(-0.5, 0.5), (-0.5, 0.5)] result = self.optimizer.minimize(fun=objective, x0=starting_point, bounds=bounds) self.assertIsInstance(result, OptimizerResult) From 1f6ca7ae938d30280e5f5c2cf3deaa0f487eeebc Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Fri, 2 Aug 2024 17:39:53 +0200 Subject: [PATCH 54/85] Remove scikit-quant methods --- .../install-machine-learning/action.yml | 2 +- .../optimizers/__init__.py | 24 +--- qiskit_machine_learning/optimizers/bobyqa.py | 84 ------------ qiskit_machine_learning/optimizers/imfil.py | 86 ------------ qiskit_machine_learning/optimizers/snobfit.py | 129 ------------------ test/optimizers/test_bobyqa.py | 67 --------- test/optimizers/test_imfil.py | 54 -------- test/optimizers/test_snobfit.py | 102 -------------- 8 files changed, 7 insertions(+), 541 deletions(-) delete mode 100644 qiskit_machine_learning/optimizers/bobyqa.py delete mode 100644 qiskit_machine_learning/optimizers/imfil.py delete mode 100644 qiskit_machine_learning/optimizers/snobfit.py delete mode 100644 test/optimizers/test_bobyqa.py delete mode 100644 test/optimizers/test_imfil.py delete mode 100644 test/optimizers/test_snobfit.py diff --git a/.github/actions/install-machine-learning/action.yml b/.github/actions/install-machine-learning/action.yml index f4784b2fe..51f37f443 100644 --- a/.github/actions/install-machine-learning/action.yml +++ b/.github/actions/install-machine-learning/action.yml @@ -17,6 +17,6 @@ runs: using: "composite" steps: - run : | - pip install -e .[torch,sparse,scikit-quant] + pip install -e .[torch,sparse] pip install -U -c constraints.txt -r requirements-dev.txt shell: bash diff --git a/qiskit_machine_learning/optimizers/__init__.py b/qiskit_machine_learning/optimizers/__init__.py index 891bb1d3a..6b3f0309e 100644 --- a/qiskit_machine_learning/optimizers/__init__.py +++ b/qiskit_machine_learning/optimizers/__init__.py @@ -84,19 +84,13 @@ SciPyOptimizer UMDA -Qiskit also provides the following optimizers, which are built-out using the optimizers from -`scikit-quant `_. The ``scikit-quant`` package -is not installed by default but must be explicitly installed, if desired, by the user. The -optimizers therein are provided under various licenses, hence it has been made an optional install. -To install the ``scikit-quant`` dependent package you can use ``pip install scikit-quant``. +The optimizers from +`scikit-quant `_ are not included in the +Qiskit Machine Learning library. +To continue using them, please import them from Qiskit Algorithms. Be aware that and a +deprecation of the methods `snobfit`, `imfil` and `bobyqa` the was considered: +https://github.com/qiskit-community/qiskit-algorithms/issues/84. -.. autosummary:: - :toctree: ../stubs/ - :nosignatures: - - BOBYQA - IMFIL - SNOBFIT Global Optimizers ----------------- @@ -118,12 +112,10 @@ from .adam_amsgrad import ADAM from .aqgd import AQGD -from .bobyqa import BOBYQA from .cg import CG from .cobyla import COBYLA from .gsls import GSLS from .gradient_descent import GradientDescent, GradientDescentState -from .imfil import IMFIL from .l_bfgs_b import L_BFGS_B from .nelder_mead import NELDER_MEAD from .nft import NFT @@ -139,7 +131,6 @@ from .qnspsa import QNSPSA from .scipy_optimizer import SciPyOptimizer from .slsqp import SLSQP -from .snobfit import SNOBFIT from .spsa import SPSA from .tnc import TNC from .umda import UMDA @@ -175,8 +166,5 @@ "DIRECT_L_RAND", "ESCH", "ISRES", - "SNOBFIT", - "BOBYQA", - "IMFIL", "UMDA", ] diff --git a/qiskit_machine_learning/optimizers/bobyqa.py b/qiskit_machine_learning/optimizers/bobyqa.py deleted file mode 100644 index efd4faeb6..000000000 --- a/qiskit_machine_learning/optimizers/bobyqa.py +++ /dev/null @@ -1,84 +0,0 @@ -# This code is part of a Qiskit project. -# -# (C) Copyright IBM 2019, 2024. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Bound Optimization BY Quadratic Approximation (BOBYQA) optimizer.""" - -from __future__ import annotations - -from collections.abc import Callable -from typing import Any - -import numpy as np -from ..utils import optionals as _optionals -from .optimizer import Optimizer, OptimizerSupportLevel, OptimizerResult, POINT - - -@_optionals.HAS_SKQUANT.require_in_instance -class BOBYQA(Optimizer): - """Bound Optimization BY Quadratic Approximation algorithm. - - BOBYQA finds local solutions to nonlinear, non-convex minimization problems with optional - bound constraints, without requirement of derivatives of the objective function. - - Uses skquant.opt installed with pip install scikit-quant. - For further detail, please refer to - https://github.com/scikit-quant/scikit-quant and https://qat4chem.lbl.gov/software. - """ - - def __init__( - self, - maxiter: int = 1000, - ) -> None: - """ - Args: - maxiter: Maximum number of function evaluations. - - Raises: - MissingOptionalLibraryError: scikit-quant not installed - """ - super().__init__() - self._maxiter = maxiter - - def get_support_level(self): - """Returns support level dictionary.""" - return { - "gradient": OptimizerSupportLevel.ignored, - "bounds": OptimizerSupportLevel.required, - "initial_point": OptimizerSupportLevel.required, - } - - @property - def settings(self) -> dict[str, Any]: - return {"maxiter": self._maxiter} - - def minimize( - self, - fun: Callable[[POINT], float], - x0: POINT, - jac: Callable[[POINT], POINT] | None = None, - bounds: list[tuple[float, float]] | None = None, - ) -> OptimizerResult: - from skquant import opt as skq # pylint: disable=import-error - - res, history = skq.minimize( - func=fun, - x0=np.asarray(x0), - bounds=np.array(bounds), - budget=self._maxiter, - method="bobyqa", - ) - - optimizer_result = OptimizerResult() - optimizer_result.x = res.optpar - optimizer_result.fun = res.optval - optimizer_result.nfev = len(history) - return optimizer_result diff --git a/qiskit_machine_learning/optimizers/imfil.py b/qiskit_machine_learning/optimizers/imfil.py deleted file mode 100644 index 7273a4bac..000000000 --- a/qiskit_machine_learning/optimizers/imfil.py +++ /dev/null @@ -1,86 +0,0 @@ -# This code is part of a Qiskit project. -# -# (C) Copyright IBM 2019, 2024. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""IMplicit FILtering (IMFIL) optimizer.""" -from __future__ import annotations - -from collections.abc import Callable -from typing import Any - -from ..utils import optionals as _optionals -from .optimizer import Optimizer, OptimizerSupportLevel, OptimizerResult, POINT - - -@_optionals.HAS_SKQUANT.require_in_instance -class IMFIL(Optimizer): - """IMplicit FILtering algorithm. - - Implicit filtering is a way to solve bound-constrained optimization problems for - which derivatives are not available. In comparison to methods that use interpolation to - reconstruct the function and its higher derivatives, implicit filtering builds upon - coordinate search followed by interpolation to get an approximate gradient. - - Uses skquant.opt installed with pip install scikit-quant. - For further detail, please refer to - https://github.com/scikit-quant/scikit-quant and https://qat4chem.lbl.gov/software. - """ - - def __init__( - self, - maxiter: int = 1000, - ) -> None: - """ - Args: - maxiter: Maximum number of function evaluations. - - Raises: - MissingOptionalLibraryError: scikit-quant not installed - """ - super().__init__() - self._maxiter = maxiter - - def get_support_level(self): - """Returns support level dictionary.""" - return { - "gradient": OptimizerSupportLevel.ignored, - "bounds": OptimizerSupportLevel.required, - "initial_point": OptimizerSupportLevel.required, - } - - @property - def settings(self) -> dict[str, Any]: - return { - "maxiter": self._maxiter, - } - - def minimize( - self, - fun: Callable[[POINT], float], - x0: POINT, - jac: Callable[[POINT], POINT] | None = None, - bounds: list[tuple[float, float]] | None = None, - ) -> OptimizerResult: - from skquant import opt as skq # pylint: disable=import-error - - res, history = skq.minimize( - func=fun, - x0=x0, - bounds=bounds, - budget=self._maxiter, - method="imfil", - ) - - optimizer_result = OptimizerResult() - optimizer_result.x = res.optpar - optimizer_result.fun = res.optval - optimizer_result.nfev = len(history) - return optimizer_result diff --git a/qiskit_machine_learning/optimizers/snobfit.py b/qiskit_machine_learning/optimizers/snobfit.py deleted file mode 100644 index 5f36b7f5e..000000000 --- a/qiskit_machine_learning/optimizers/snobfit.py +++ /dev/null @@ -1,129 +0,0 @@ -# This code is part of a Qiskit project. -# -# (C) Copyright IBM 2019, 2024. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Stable Noisy Optimization by Branch and FIT algorithm (SNOBFIT) optimizer.""" -from __future__ import annotations - -from collections.abc import Callable -from typing import Any - -import numpy as np -from ..exceptions import AlgorithmError -from ..utils import optionals as _optionals -from .optimizer import Optimizer, OptimizerSupportLevel, OptimizerResult, POINT - - -@_optionals.HAS_SKQUANT.require_in_instance -@_optionals.HAS_SQSNOBFIT.require_in_instance -class SNOBFIT(Optimizer): - """Stable Noisy Optimization by Branch and FIT algorithm. - - SnobFit is used for the optimization of derivative-free, noisy objective functions providing - robust and fast solutions of problems with continuous variables varying within bound. - - Uses skquant.opt installed with pip install scikit-quant. - For further detail, please refer to - https://github.com/scikit-quant/scikit-quant and https://qat4chem.lbl.gov/software. - """ - - def __init__( - self, - maxiter: int = 1000, - maxfail: int = 10, - maxmp: int = None, - verbose: bool = False, - ) -> None: - """ - Args: - maxiter: Maximum number of function evaluations. - maxmp: Maximum number of model points requested for the local fit. - Default = 2 * number of parameters + 6 set to this value when None. - maxfail: Maximum number of failures to improve the solution. Stops the algorithm - after maxfail is reached. - verbose: Provide verbose (debugging) output. - - Raises: - MissingOptionalLibraryError: scikit-quant or SQSnobFit not installed - AlgorithmError: If NumPy 1.24.0 or above is installed. - See https://github.com/scikit-quant/scikit-quant/issues/24 for more details. - """ - # check version - if tuple(map(int, np.__version__.split(".")[:2])) >= (1, 24): - raise AlgorithmError( - "SnobFit is incompatible with NumPy 1.24.0 or above, please " - "install a previous version. See also scikit-quant/scikit-quant#24." - ) - - super().__init__() - self._maxiter = maxiter - self._maxfail = maxfail - self._maxmp = maxmp - self._verbose = verbose - - def get_support_level(self): - """Returns support level dictionary.""" - return { - "gradient": OptimizerSupportLevel.ignored, - "bounds": OptimizerSupportLevel.required, - "initial_point": OptimizerSupportLevel.required, - } - - @property - def settings(self) -> dict[str, Any]: - return { - "maxiter": self._maxiter, - "maxfail": self._maxfail, - "maxmp": self._maxmp, - "verbose": self._verbose, - } - - def minimize( - self, - fun: Callable[[POINT], float], - x0: POINT, - jac: Callable[[POINT], POINT] | None = None, - bounds: list[tuple[float, float]] | None = None, - ) -> OptimizerResult: - import skquant.opt as skq # pylint: disable=import-error - from SQSnobFit import optset # pylint: disable=import-error - - if bounds is None or any(None in bound_tuple for bound_tuple in bounds): - raise ValueError("Optimizer SNOBFIT requires bounds for all parameters.") - - snobfit_settings = { - "maxmp": self._maxmp, - "maxfail": self._maxfail, - "verbose": self._verbose, - } - options = optset(optin=snobfit_settings) - # counters the error when initial point is outside the acceptable bounds - x0 = np.asarray(x0) - for idx, theta in enumerate(x0): - if abs(theta) > bounds[idx][0]: - x0[idx] = x0[idx] % bounds[idx][0] - elif abs(theta) > bounds[idx][1]: - x0[idx] = x0[idx] % bounds[idx][1] - - res, history = skq.minimize( - fun, - x0, - bounds=bounds, - budget=self._maxiter, - method="snobfit", - options=options, - ) - - optimizer_result = OptimizerResult() - optimizer_result.x = res.optpar - optimizer_result.fun = res.optval - optimizer_result.nfev = len(history) - return optimizer_result diff --git a/test/optimizers/test_bobyqa.py b/test/optimizers/test_bobyqa.py deleted file mode 100644 index 360c82e77..000000000 --- a/test/optimizers/test_bobyqa.py +++ /dev/null @@ -1,67 +0,0 @@ -# This code is part of a Qiskit project. -# -# (C) Copyright IBM 2019, 2024. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Test of BOBYQA optimizer""" - -import unittest -from test import QiskitAlgorithmsTestCase -import numpy as np -from ddt import ddt -from qiskit.exceptions import MissingOptionalLibraryError -from qiskit_machine_learning.optimizers import BOBYQA -from qiskit_machine_learning.utils import algorithm_globals - - -@ddt -class TestOptimizerBOBYQA(QiskitAlgorithmsTestCase): - """Test BOBYQA optimizer""" - - def setUp(self): - super().setUp() - algorithm_globals.random_seed = 50 - self.quadratic_objective = lambda x: x[0] ** 2 + x[1] ** 2 - self.initial_point = np.array([1.0, 1.0]) - self.bounds = [(-2.0, 2.0), (-2.0, 2.0)] - - def test_optimizer_minimize(self): - """Test BOBYQA optimizer minimize method""" - try: - bobyqa = BOBYQA(maxiter=100) - result = bobyqa.minimize( - self.quadratic_objective, self.initial_point, bounds=self.bounds - ) - self.assertAlmostEqual(result.fun, 0.0, places=6) - self.assertTrue(np.allclose(result.x, np.zeros_like(self.initial_point), atol=1e-2)) - except MissingOptionalLibraryError as error: - self.skipTest(str(error)) - - def test_optimizer_without_bounds(self): - """Test BOBYQA optimizer without bounds (should raise an error)""" - try: - bobyqa = BOBYQA(maxiter=100) - with self.assertRaises(IndexError): - bobyqa.minimize(self.quadratic_objective, self.initial_point) - except MissingOptionalLibraryError as error: - self.skipTest(str(error)) - - def test_settings(self): - """Test settings property""" - try: - bobyqa = BOBYQA(maxiter=100) - settings = bobyqa.settings - self.assertEqual(settings["maxiter"], 100) - except MissingOptionalLibraryError as error: - self.skipTest(str(error)) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/optimizers/test_imfil.py b/test/optimizers/test_imfil.py deleted file mode 100644 index 7a9417bce..000000000 --- a/test/optimizers/test_imfil.py +++ /dev/null @@ -1,54 +0,0 @@ -# This code is part of a Qiskit project. -# -# (C) Copyright IBM 2019, 2024. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""TestIMFIL""" - -import unittest -from test import QiskitAlgorithmsTestCase -import numpy as np - -from qiskit_machine_learning.optimizers.imfil import IMFIL -from qiskit_machine_learning.optimizers.optimizer import OptimizerResult -from qiskit_machine_learning.utils import optionals, algorithm_globals - - -class TestIMFIL(QiskitAlgorithmsTestCase): - """TestIMFIL""" - - def setUp(self): - super().setUp() - algorithm_globals.random_seed = 50 - if not optionals.HAS_SKQUANT: - self.skipTest("skquant is required for IMFIL optimizer") - self.optimizer = IMFIL(maxiter=500) - - def test_minimize_bounds(self): - """Testing minimize.""" - - def objective(x): - """Defining the objective""" - return np.sum(x**2) - - starting_point = np.array([0.5, 0.5]) - bounds = [(-0.5, 0.5), (-0.5, 0.5)] - result = self.optimizer.minimize(fun=objective, x0=starting_point, bounds=bounds) - self.assertIsInstance(result, OptimizerResult) - self.assertTrue(np.all(result.x >= -0.5) and np.all(result.x <= 0.5)) - - def test_settings(self): - """Test settings.""" - settings = self.optimizer.settings - self.assertEqual(settings["maxiter"], 500) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/optimizers/test_snobfit.py b/test/optimizers/test_snobfit.py deleted file mode 100644 index 113487a44..000000000 --- a/test/optimizers/test_snobfit.py +++ /dev/null @@ -1,102 +0,0 @@ -# This code is part of a Qiskit project. -# -# (C) Copyright IBM 2019, 2024. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Unit tests for the SNOBFIT optimizer.""" - -import unittest -from test import QiskitAlgorithmsTestCase -import numpy as np -from qiskit.exceptions import MissingOptionalLibraryError -from qiskit_machine_learning.optimizers.snobfit import SNOBFIT -from qiskit_machine_learning.utils import algorithm_globals - - -class TestSNOBFITOptimizer(QiskitAlgorithmsTestCase): - """Test cases for the SNOBFIT optimizer.""" - - def setUp(self): - """Set up the optimizer for testing.""" - super().setUp() - algorithm_globals.random_seed = 50 - try: - self.optimizer = SNOBFIT(maxiter=200, maxfail=5, verbose=True) - - except MissingOptionalLibraryError as error: - self.skipTest(str(error)) - - def test_optimizer_support(self): - """Test the optimizer support levels.""" - support_levels = self.optimizer.get_support_level() - self.assertIn("gradient", support_levels) - self.assertIn("bounds", support_levels) - self.assertIn("initial_point", support_levels) - - def test_minimize_simple_quadratic(self): - """Test the optimizer on a simple quadratic function.""" - - def quadratic_function(params): - """Test function.""" - return np.sum((params - 4) ** 2) - - initial_point = np.array([0.0, 0.0]) - bounds = [(0, 10), (0, 10)] - result = self.optimizer.minimize(fun=quadratic_function, x0=initial_point, bounds=bounds) - self.assertTrue(np.allclose(result.x, np.array([4.0, 4.0]), atol=1e-2)) - - def test_minimize_rosenbrock(self): - """Test the optimizer on the Rosenbrock function.""" - - def rosenbrock_function(params): - """Test function.""" - return sum(100.0 * (params[1:] - params[:-1] ** 2.0) ** 2.0 + (1 - params[:-1]) ** 2.0) - - initial_point = np.array([1.5, 1.5]) - bounds = [(-5, 5), (-5, 5)] - result = self.optimizer.minimize(fun=rosenbrock_function, x0=initial_point, bounds=bounds) - self.assertLess(result.fun, 1e-4) - self.assertLess(result.nfev, 200) - - def test_optimizer_settings(self): - """Test the optimizer settings.""" - settings = self.optimizer.settings - self.assertEqual(settings["maxiter"], 200) - self.assertEqual(settings["maxfail"], 5) - self.assertIsInstance(settings["maxmp"], int) - self.assertTrue(settings["verbose"]) - - def test_minimize_with_invalid_bounds(self): - """Test the optimizer with invalid bounds.""" - - def quadratic_function(params): - """Test function.""" - return np.sum((params - 2) ** 2) - - initial_point = np.array([1.0, 1.0]) - bounds = [(None, 5), (None, 5)] # Invalid bounds with None - with self.assertRaises(ValueError): - self.optimizer.minimize(fun=quadratic_function, x0=initial_point, bounds=bounds) - - def test_minimize_with_clamped_initial_point(self): - """Test the optimizer with an initial point that is clamped to bounds.""" - - def quadratic_function(params): - """Test function.""" - return np.sum((params - 2) ** 2) - - initial_point = np.array([10.0, -10.0]) - bounds = [(0, 5), (0, 5)] - result = self.optimizer.minimize(fun=quadratic_function, x0=initial_point, bounds=bounds) - self.assertTrue(np.allclose(result.x, np.array([5.0, 0.0]), atol=1e-2)) - - -if __name__ == "__main__": - unittest.main() From b5875a305a93616f8e72e481c38287675d9f6322 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Fri, 2 Aug 2024 17:44:32 +0200 Subject: [PATCH 55/85] Remove scikit-quant methods (2) --- test/optimizers/test_optimizers.py | 21 ------ .../optimizers/test_optimizers_scikitquant.py | 68 ------------------- 2 files changed, 89 deletions(-) delete mode 100644 test/optimizers/test_optimizers_scikitquant.py diff --git a/test/optimizers/test_optimizers.py b/test/optimizers/test_optimizers.py index 344c3a4ef..0ad5975f4 100644 --- a/test/optimizers/test_optimizers.py +++ b/test/optimizers/test_optimizers.py @@ -22,14 +22,11 @@ from qiskit.circuit.library import RealAmplitudes from qiskit.exceptions import MissingOptionalLibraryError -from qiskit.utils import optionals from qiskit.primitives import Sampler from qiskit_machine_learning.optimizers import ( ADAM, AQGD, - BOBYQA, - IMFIL, CG, CRS, COBYLA, @@ -313,24 +310,6 @@ def test_aqgd(self): self.assertListEqual(settings["eta"], [0.2, 0.1]) self.assertListEqual(settings["momentum"], [0.25, 0.1]) - @unittest.skipIf(not optionals.HAS_SKQUANT, "Install scikit-quant to run this test.") - def test_bobyqa(self): - """Test BOBYQA is serializable.""" - - opt = BOBYQA(maxiter=200) - settings = opt.settings - - self.assertEqual(settings["maxiter"], 200) - - @unittest.skipIf(not optionals.HAS_SKQUANT, "Install scikit-quant to run this test.") - def test_imfil(self): - """Test IMFIL is serializable.""" - - opt = IMFIL(maxiter=200) - settings = opt.settings - - self.assertEqual(settings["maxiter"], 200) - def test_gradient_descent(self): """Test GradientDescent is serializable.""" diff --git a/test/optimizers/test_optimizers_scikitquant.py b/test/optimizers/test_optimizers_scikitquant.py deleted file mode 100644 index f4ee39315..000000000 --- a/test/optimizers/test_optimizers_scikitquant.py +++ /dev/null @@ -1,68 +0,0 @@ -# This code is part of a Qiskit project. -# -# (C) Copyright IBM 2020, 2024. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Test of scikit-quant optimizers.""" - -import unittest -from test import QiskitAlgorithmsTestCase - -from ddt import ddt, data, unpack - -import numpy -from qiskit.exceptions import MissingOptionalLibraryError -from qiskit.quantum_info import SparsePauliOp - -from qiskit_machine_learning.optimizers import SNOBFIT -from qiskit_machine_learning.utils import algorithm_globals - - -@ddt -class TestOptimizers(QiskitAlgorithmsTestCase): - """Test scikit-quant optimizers.""" - - def setUp(self): - """Set the problem.""" - super().setUp() - algorithm_globals.random_seed = 50 - self.qubit_op = SparsePauliOp.from_list( - [ - ("II", -1.052373245772859), - ("IZ", 0.39793742484318045), - ("ZI", -0.39793742484318045), - ("ZZ", -0.01128010425623538), - ("XX", 0.18093119978423156), - ] - ) - - @unittest.skipIf( - # NB: numpy.__version__ may contain letters, e.g. "1.26.0b1" - tuple(map(int, numpy.__version__.split(".")[:2])) >= (1, 24), - "scikit's SnobFit currently incompatible with NumPy 1.24.0.", - ) - @data((None,), ([(-1, 1), (None, None)],)) - @unpack - def test_snobfit_missing_bounds(self, bounds): - """SNOBFIT optimizer test with missing bounds.""" - try: - optimizer = SNOBFIT() - with self.assertRaises(ValueError): - optimizer.minimize( - fun=lambda _: 1, # using dummy function (never called) - x0=numpy.array([0.1, 0.1]), # dummy initial point - bounds=bounds, - ) - except MissingOptionalLibraryError as ex: - self.skipTest(str(ex)) - - -if __name__ == "__main__": - unittest.main() From 800cca421a617fc0c65a8c1d38de5336f0a2fc5e Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Mon, 5 Aug 2024 17:15:37 +0100 Subject: [PATCH 56/85] Edit the release notes and Qiskit version 1+ --- ...orithms-incorporated-421554a4ff547d0d.yaml | 83 ++++++++++++------- requirements.txt | 2 +- 2 files changed, 52 insertions(+), 33 deletions(-) diff --git a/releasenotes/notes/qiskit-algorithms-incorporated-421554a4ff547d0d.yaml b/releasenotes/notes/qiskit-algorithms-incorporated-421554a4ff547d0d.yaml index 7aebe5b7a..52e9b3c00 100644 --- a/releasenotes/notes/qiskit-algorithms-incorporated-421554a4ff547d0d.yaml +++ b/releasenotes/notes/qiskit-algorithms-incorporated-421554a4ff547d0d.yaml @@ -9,51 +9,70 @@ prelude: > features: - | Migrated essential Qiskit Algorithms features to Qiskit Machine Learning: - - `qiskit_algorithms/gradients` -> `qiskit_machine_learning/gradients` - - `qiskit_algorithms/optimizers` -> `qiskit_machine_learning/optimizers` + - `qiskit_algorithms/gradients` -> `qiskit_machine_learning/gradients`. Note: only the SPSA, parameter-shift and linear-combination-of-unitaries gradients are retained. Other gradient strategies, such as reverse and finite-diff are not incorporated. + - `qiskit_algorithms/optimizers` -> `qiskit_machine_learning/optimizers`. Note: optimizers from `scikit-quant `_ are not incorporated. - `qiskit_algorithms/state_fidelities` -> `qiskit_machine_learning/state_fidelities` - Partial merge of `qiskit_algorithms/utils` with `qiskit_machine_learning/utils` - - | - Unit tests migrated accordingly: - - `qiskit_algorithms/tests/gradients` -> `qiskit_machine_learning/tests/gradients` - - `qiskit_algorithms/tests/optimizers` -> `qiskit_machine_learning/tests/optimizers` - - `qiskit_algorithms/tests/state_fidelities` -> `qiskit_machine_learning/tests/state_fidelities` - - Partial merge of `qiskit_algorithms/tests/utils` with `qiskit_machine_learning/tests/utils` - -issues: - - | - Incorporating Qiskit Algorithms will facilitate the upgrade to newer versions of Qiskit and - Qiskit Runtime. In particular, the following issues, still outstanding, will be tackled in the - next release: - - Support V2 primitives (#742) - - Add support for EstimatorV2 to run circuits over hardware (#810) - - | - Compliance with ISA standards: - - ISA circuit support for latest Runtime (#786) - - Sampler fails to run FidelityKernel even if circuits are transpiled (#165) upgrade: - | The merge of some of the features of Qiskit Algorithms into Qiskit Machine Learning might lead to breaking changes. For this reason, caution is advised when updating to version 0.8 during critical production stages in a project. Users must update their imports and code references in code that uses Qiskit Machine Leaning and Algorithms: - - Change `qiskit_algorithms/gradients` to `qiskit_machine_learning/gradients` - - Change `qiskit_algorithms/optimizers` to `qiskit_machine_learning/optimizers` - - Change `qiskit_algorithms/state_fidelities` to `qiskit_machine_learning/state_fidelities` + - Change `qiskit_algorithms.gradients` to `qiskit_machine_learning.gradients` + - Change `qiskit_algorithms.optimizers` to `qiskit_machine_learning.optimizers` + - Change `qiskit_algorithms.state_fidelities` to `qiskit_machine_learning.state_fidelities` - Update utilities as needed due to partial merge. To continue using sub-modules and functionalities of Qiskit Algorithms that **have not been transferred**, you may continue using them as before by importing from Qiskit Algorithms. However, be aware that Qiskit Algorithms - is no longer officially supported and some of its functionalities may not work in your use case. For any issues + is no longer officially supported and some of its functionalities may not work in your use case. For any problems directly related to Qiskit Algorithms, please open a GitHub issue at https://github.com/qiskit-community/qiskit-algorithms. Should you want to include a Qiskit Algorithms functionality that has not been incorporated in Qiskit Machine Learning, please open a feature-request issue at https://github.com/qiskit-community/qiskit-machine-learning, explaining why this change would be useful for you and other users. + Four examples of upgrading the code can be found below. -deprecations: - - | - The following imports of Qiskit Algorithms modules into Qiskit Machine Learning are deprecated and their - functionalities are now part of Qiskit Machine Learning itself: - - `qiskit_algorithms/gradients` - - `qiskit_algorithms/optimizers` - - `qiskit_algorithms/state_fidelities` - - Portions of `qiskit_algorithms/utils` + - Gradients + ```python + # Before: + from qiskit_algorithms.gradients import SPSA, ParameterShift + # After: + from qiskit_machine_learning.gradients import SPSA, ParameterShift + + # Usage + spsa = SPSA() + param_shift = ParameterShift() + ``` + + - Optimizers + ```python + # Before: + from qiskit_algorithms.optimizers import COBYLA, ADAM + # After: + from qiskit_machine_learning.optimizers import COBYLA, ADAM + + # Usage + cobyla = COBYLA() + adam = ADAM() + ``` + + - Quantum state fidelities + ```python + # Before: + from qiskit_algorithms.state_fidelities import ComputeFidelity + # After: + from qiskit_machine_learning.state_fidelities import ComputeFidelity + + # Usage + fidelity = ComputeFidelity() + ``` + + - Algorithm globals (used to fix the random seed) + ```python + # Before: + from qiskit_algorithms.utils import algorithm_globals + # After: + from qiskit_machine_learning.utils import algorithm_globals + + algorithm_globals.random_seed = 1234 + ``` diff --git a/requirements.txt b/requirements.txt index f1b058ff8..6436d0e69 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -qiskit>=0.44 +qiskit>=1.0 scipy>=1.4 numpy>=1.17 psutil>=5 From e98200a64714461655d7ac6193b8052dba2b6fa2 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Mon, 5 Aug 2024 17:58:57 +0100 Subject: [PATCH 57/85] Edit the release notes and Qiskit version 1+ --- .../qiskit-algorithms-incorporated-421554a4ff547d0d.yaml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/releasenotes/notes/qiskit-algorithms-incorporated-421554a4ff547d0d.yaml b/releasenotes/notes/qiskit-algorithms-incorporated-421554a4ff547d0d.yaml index 52e9b3c00..49199b7a8 100644 --- a/releasenotes/notes/qiskit-algorithms-incorporated-421554a4ff547d0d.yaml +++ b/releasenotes/notes/qiskit-algorithms-incorporated-421554a4ff547d0d.yaml @@ -9,8 +9,11 @@ prelude: > features: - | Migrated essential Qiskit Algorithms features to Qiskit Machine Learning: - - `qiskit_algorithms/gradients` -> `qiskit_machine_learning/gradients`. Note: only the SPSA, parameter-shift and linear-combination-of-unitaries gradients are retained. Other gradient strategies, such as reverse and finite-diff are not incorporated. - - `qiskit_algorithms/optimizers` -> `qiskit_machine_learning/optimizers`. Note: optimizers from `scikit-quant `_ are not incorporated. + - `qiskit_algorithms/gradients` -> `qiskit_machine_learning/gradients`. Note: only the SPSA, + parameter-shift and linear-combination-of-unitaries gradients are retained. Other gradient + strategies, such as reverse and finite-diff are not incorporated. + - `qiskit_algorithms/optimizers` -> `qiskit_machine_learning/optimizers`. Note: optimizers + from `scikit-quant `_ are not incorporated. - `qiskit_algorithms/state_fidelities` -> `qiskit_machine_learning/state_fidelities` - Partial merge of `qiskit_algorithms/utils` with `qiskit_machine_learning/utils` From f349f7cbbbdfdb3daf136046c8bfbd46d7150bf9 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Mon, 5 Aug 2024 18:07:15 +0100 Subject: [PATCH 58/85] Add Qiskit 1.0 upgrade in reno --- ...iskit-algorithms-incorporated-421554a4ff547d0d.yaml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/releasenotes/notes/qiskit-algorithms-incorporated-421554a4ff547d0d.yaml b/releasenotes/notes/qiskit-algorithms-incorporated-421554a4ff547d0d.yaml index 49199b7a8..ece3e8977 100644 --- a/releasenotes/notes/qiskit-algorithms-incorporated-421554a4ff547d0d.yaml +++ b/releasenotes/notes/qiskit-algorithms-incorporated-421554a4ff547d0d.yaml @@ -16,6 +16,8 @@ features: from `scikit-quant `_ are not incorporated. - `qiskit_algorithms/state_fidelities` -> `qiskit_machine_learning/state_fidelities` - Partial merge of `qiskit_algorithms/utils` with `qiskit_machine_learning/utils` + - From the next release, Qiskit Machine Learning will require Qiskit 1.0 or higher. + You may be required to upgrade Qiskit Aer accordingly, depending on your set-up. upgrade: - | @@ -34,48 +36,40 @@ upgrade: please open a feature-request issue at https://github.com/qiskit-community/qiskit-machine-learning, explaining why this change would be useful for you and other users. Four examples of upgrading the code can be found below. - - Gradients ```python # Before: from qiskit_algorithms.gradients import SPSA, ParameterShift # After: from qiskit_machine_learning.gradients import SPSA, ParameterShift - # Usage spsa = SPSA() param_shift = ParameterShift() ``` - - Optimizers ```python # Before: from qiskit_algorithms.optimizers import COBYLA, ADAM # After: from qiskit_machine_learning.optimizers import COBYLA, ADAM - # Usage cobyla = COBYLA() adam = ADAM() ``` - - Quantum state fidelities ```python # Before: from qiskit_algorithms.state_fidelities import ComputeFidelity # After: from qiskit_machine_learning.state_fidelities import ComputeFidelity - # Usage fidelity = ComputeFidelity() ``` - - Algorithm globals (used to fix the random seed) ```python # Before: from qiskit_algorithms.utils import algorithm_globals # After: from qiskit_machine_learning.utils import algorithm_globals - algorithm_globals.random_seed = 1234 ``` From 154d6a7752b9c77a8acf81b6219c0df464cc9c49 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Mon, 5 Aug 2024 18:13:26 +0100 Subject: [PATCH 59/85] Add Qiskit 1.0 upgrade in reno --- ...orithms-incorporated-421554a4ff547d0d.yaml | 70 ++++++++++--------- 1 file changed, 37 insertions(+), 33 deletions(-) diff --git a/releasenotes/notes/qiskit-algorithms-incorporated-421554a4ff547d0d.yaml b/releasenotes/notes/qiskit-algorithms-incorporated-421554a4ff547d0d.yaml index ece3e8977..da0c0ab3f 100644 --- a/releasenotes/notes/qiskit-algorithms-incorporated-421554a4ff547d0d.yaml +++ b/releasenotes/notes/qiskit-algorithms-incorporated-421554a4ff547d0d.yaml @@ -36,40 +36,44 @@ upgrade: please open a feature-request issue at https://github.com/qiskit-community/qiskit-machine-learning, explaining why this change would be useful for you and other users. Four examples of upgrading the code can be found below. + - Gradients - ```python - # Before: - from qiskit_algorithms.gradients import SPSA, ParameterShift - # After: - from qiskit_machine_learning.gradients import SPSA, ParameterShift - # Usage - spsa = SPSA() - param_shift = ParameterShift() - ``` + .. code:: python + + # Before: + from qiskit_algorithms.gradients import SPSA, ParameterShift + # After: + from qiskit_machine_learning.gradients import SPSA, ParameterShift + # Usage + spsa = SPSA() + param_shift = ParameterShift() + - Optimizers - ```python - # Before: - from qiskit_algorithms.optimizers import COBYLA, ADAM - # After: - from qiskit_machine_learning.optimizers import COBYLA, ADAM - # Usage - cobyla = COBYLA() - adam = ADAM() - ``` + .. code:: python + + # Before: + from qiskit_algorithms.optimizers import COBYLA, ADAM + # After: + from qiskit_machine_learning.optimizers import COBYLA, ADAM + # Usage + cobyla = COBYLA() + adam = ADAM() + - Quantum state fidelities - ```python - # Before: - from qiskit_algorithms.state_fidelities import ComputeFidelity - # After: - from qiskit_machine_learning.state_fidelities import ComputeFidelity - # Usage - fidelity = ComputeFidelity() - ``` + .. code:: python + + # Before: + from qiskit_algorithms.state_fidelities import ComputeFidelity + # After: + from qiskit_machine_learning.state_fidelities import ComputeFidelity + # Usage + fidelity = ComputeFidelity() + - Algorithm globals (used to fix the random seed) - ```python - # Before: - from qiskit_algorithms.utils import algorithm_globals - # After: - from qiskit_machine_learning.utils import algorithm_globals - algorithm_globals.random_seed = 1234 - ``` + .. code:: python + + # Before: + from qiskit_algorithms.utils import algorithm_globals + # After: + from qiskit_machine_learning.utils import algorithm_globals + algorithm_globals.random_seed = 1234 From c72840057d15e311a486cb05e47f3db4574d6c2b Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Mon, 5 Aug 2024 19:08:06 +0100 Subject: [PATCH 60/85] Add Qiskit 1.0 upgrade in reno --- .pylintdict | 1 + ...orithms-incorporated-421554a4ff547d0d.yaml | 107 +++++++++++------- 2 files changed, 66 insertions(+), 42 deletions(-) diff --git a/.pylintdict b/.pylintdict index 40d21df3c..375963119 100644 --- a/.pylintdict +++ b/.pylintdict @@ -597,3 +597,4 @@ zz assertRaises RuntimeError Rosenbrock +fidelities diff --git a/releasenotes/notes/qiskit-algorithms-incorporated-421554a4ff547d0d.yaml b/releasenotes/notes/qiskit-algorithms-incorporated-421554a4ff547d0d.yaml index da0c0ab3f..07fc2b775 100644 --- a/releasenotes/notes/qiskit-algorithms-incorporated-421554a4ff547d0d.yaml +++ b/releasenotes/notes/qiskit-algorithms-incorporated-421554a4ff547d0d.yaml @@ -9,13 +9,22 @@ prelude: > features: - | Migrated essential Qiskit Algorithms features to Qiskit Machine Learning: - - `qiskit_algorithms/gradients` -> `qiskit_machine_learning/gradients`. Note: only the SPSA, + + - `qiskit_algorithms/gradients` -> `qiskit_machine_learning/gradients`. + Note: + only the SPSA, parameter-shift and linear-combination-of-unitaries gradients are retained. Other gradient strategies, such as reverse and finite-diff are not incorporated. - - `qiskit_algorithms/optimizers` -> `qiskit_machine_learning/optimizers`. Note: optimizers + + - `qiskit_algorithms/optimizers` -> `qiskit_machine_learning/optimizers`. + Note: + optimizers from `scikit-quant `_ are not incorporated. + - `qiskit_algorithms/state_fidelities` -> `qiskit_machine_learning/state_fidelities` + - Partial merge of `qiskit_algorithms/utils` with `qiskit_machine_learning/utils` + - From the next release, Qiskit Machine Learning will require Qiskit 1.0 or higher. You may be required to upgrade Qiskit Aer accordingly, depending on your set-up. @@ -23,11 +32,19 @@ upgrade: - | The merge of some of the features of Qiskit Algorithms into Qiskit Machine Learning might lead to breaking changes. For this reason, caution is advised when updating to version 0.8 during critical production stages in a project. + + - | Users must update their imports and code references in code that uses Qiskit Machine Leaning and Algorithms: + - Change `qiskit_algorithms.gradients` to `qiskit_machine_learning.gradients` + - Change `qiskit_algorithms.optimizers` to `qiskit_machine_learning.optimizers` + - Change `qiskit_algorithms.state_fidelities` to `qiskit_machine_learning.state_fidelities` + - Update utilities as needed due to partial merge. + + - | To continue using sub-modules and functionalities of Qiskit Algorithms that **have not been transferred**, you may continue using them as before by importing from Qiskit Algorithms. However, be aware that Qiskit Algorithms is no longer officially supported and some of its functionalities may not work in your use case. For any problems @@ -35,45 +52,51 @@ upgrade: Should you want to include a Qiskit Algorithms functionality that has not been incorporated in Qiskit Machine Learning, please open a feature-request issue at https://github.com/qiskit-community/qiskit-machine-learning, explaining why this change would be useful for you and other users. + + - | Four examples of upgrading the code can be found below. - - Gradients - .. code:: python - - # Before: - from qiskit_algorithms.gradients import SPSA, ParameterShift - # After: - from qiskit_machine_learning.gradients import SPSA, ParameterShift - # Usage - spsa = SPSA() - param_shift = ParameterShift() - - - Optimizers - .. code:: python - - # Before: - from qiskit_algorithms.optimizers import COBYLA, ADAM - # After: - from qiskit_machine_learning.optimizers import COBYLA, ADAM - # Usage - cobyla = COBYLA() - adam = ADAM() - - - Quantum state fidelities - .. code:: python - - # Before: - from qiskit_algorithms.state_fidelities import ComputeFidelity - # After: - from qiskit_machine_learning.state_fidelities import ComputeFidelity - # Usage - fidelity = ComputeFidelity() - - - Algorithm globals (used to fix the random seed) - .. code:: python - - # Before: - from qiskit_algorithms.utils import algorithm_globals - # After: - from qiskit_machine_learning.utils import algorithm_globals - algorithm_globals.random_seed = 1234 + Gradients: + + .. code:: python + + # Before: + from qiskit_algorithms.gradients import SPSA, ParameterShift + # After: + from qiskit_machine_learning.gradients import SPSA, ParameterShift + # Usage + spsa = SPSA() + param_shift = ParameterShift() + + Optimizers + + .. code:: python + + # Before: + from qiskit_algorithms.optimizers import COBYLA, ADAM + # After: + from qiskit_machine_learning.optimizers import COBYLA, ADAM + # Usage + cobyla = COBYLA() + adam = ADAM() + + Quantum state fidelities + + .. code:: python + + # Before: + from qiskit_algorithms.state_fidelities import ComputeFidelity + # After: + from qiskit_machine_learning.state_fidelities import ComputeFidelity + # Usage + fidelity = ComputeFidelity() + + Algorithm globals (used to fix the random seed) + + .. code:: python + + # Before: + from qiskit_algorithms.utils import algorithm_globals + # After: + from qiskit_machine_learning.utils import algorithm_globals + algorithm_globals.random_seed = 1234 From 32947314376178858e7d6b2208fe3926c4519be9 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Tue, 6 Aug 2024 08:45:49 +0100 Subject: [PATCH 61/85] Apply line breaks --- ...orithms-incorporated-421554a4ff547d0d.yaml | 38 ++++++++++++------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/releasenotes/notes/qiskit-algorithms-incorporated-421554a4ff547d0d.yaml b/releasenotes/notes/qiskit-algorithms-incorporated-421554a4ff547d0d.yaml index 07fc2b775..9ee003981 100644 --- a/releasenotes/notes/qiskit-algorithms-incorporated-421554a4ff547d0d.yaml +++ b/releasenotes/notes/qiskit-algorithms-incorporated-421554a4ff547d0d.yaml @@ -1,9 +1,12 @@ --- prelude: > - This release includes the migration of a subset of Qiskit Algorithms features to Qiskit Machine Learning. - This ensures continued enhancement of essential features for Qiskit Machine Learning following the end - of official support for Qiskit Algorithms. Therefore, Qiskit Machine Learning will no longer depend on - Qiskit Algorithms, possibly introducing breaking changes in import structures. Some quick-fixes are + This release includes the migration of a subset of Qiskit Algorithms features to Qiskit Machine + Learning. + This ensures continued enhancement of essential features for Qiskit Machine Learning following + the end of official support for Qiskit Algorithms. Therefore, Qiskit Machine Learning + will no longer depend on + Qiskit Algorithms, possibly introducing breaking changes in import structures. Some quick- + fixes are described below. features: @@ -30,11 +33,14 @@ features: upgrade: - | - The merge of some of the features of Qiskit Algorithms into Qiskit Machine Learning might lead to breaking changes. - For this reason, caution is advised when updating to version 0.8 during critical production stages in a project. + The merge of some of the features of Qiskit Algorithms into Qiskit Machine Learning might lead + to breaking changes. + For this reason, caution is advised when updating to version 0.8 during critical production + stages in a project. - | - Users must update their imports and code references in code that uses Qiskit Machine Leaning and Algorithms: + Users must update their imports and code references in code that uses Qiskit Machine Leaning + and Algorithms: - Change `qiskit_algorithms.gradients` to `qiskit_machine_learning.gradients` @@ -45,12 +51,18 @@ upgrade: - Update utilities as needed due to partial merge. - | - To continue using sub-modules and functionalities of Qiskit Algorithms that **have not been transferred**, - you may continue using them as before by importing from Qiskit Algorithms. However, be aware that Qiskit Algorithms - is no longer officially supported and some of its functionalities may not work in your use case. For any problems - directly related to Qiskit Algorithms, please open a GitHub issue at https://github.com/qiskit-community/qiskit-algorithms. - Should you want to include a Qiskit Algorithms functionality that has not been incorporated in Qiskit Machine Learning, - please open a feature-request issue at https://github.com/qiskit-community/qiskit-machine-learning, explaining why + To continue using sub-modules and functionalities of Qiskit Algorithms that **have not been + transferred**, + you may continue using them as before by importing from Qiskit Algorithms. However, be aware + that Qiskit Algorithms + is no longer officially supported and some of its functionalities may not work in your use case. + For any problems + directly related to Qiskit Algorithms, please open a GitHub issue at + https://github.com/qiskit-community/qiskit-algorithms. + Should you want to include a Qiskit Algorithms functionality that has not been incorporated in + Qiskit Machine Learning, + please open a feature-request issue at + https://github.com/qiskit-community/qiskit-machine-learning, explaining why this change would be useful for you and other users. - | From 9e53371173b58daa9c8376b2e1b43a093abd34f8 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Tue, 6 Aug 2024 12:03:13 +0100 Subject: [PATCH 62/85] Restructure line breaks --- ...orithms-incorporated-421554a4ff547d0d.yaml | 45 ++++++++----------- 1 file changed, 18 insertions(+), 27 deletions(-) diff --git a/releasenotes/notes/qiskit-algorithms-incorporated-421554a4ff547d0d.yaml b/releasenotes/notes/qiskit-algorithms-incorporated-421554a4ff547d0d.yaml index 9ee003981..3f284d6db 100644 --- a/releasenotes/notes/qiskit-algorithms-incorporated-421554a4ff547d0d.yaml +++ b/releasenotes/notes/qiskit-algorithms-incorporated-421554a4ff547d0d.yaml @@ -1,13 +1,10 @@ --- prelude: > This release includes the migration of a subset of Qiskit Algorithms features to Qiskit Machine - Learning. - This ensures continued enhancement of essential features for Qiskit Machine Learning following - the end of official support for Qiskit Algorithms. Therefore, Qiskit Machine Learning - will no longer depend on - Qiskit Algorithms, possibly introducing breaking changes in import structures. Some quick- - fixes are - described below. + Learning. This ensures continued enhancement of essential features for Qiskit Machine + Learning following the end of official support for Qiskit Algorithms. Therefore, Qiskit + Machine Learning will no longer depend on Qiskit Algorithms, possibly introducing breaking + changes in import structures. Some quick-fixes are described below. features: - | @@ -15,14 +12,13 @@ features: - `qiskit_algorithms/gradients` -> `qiskit_machine_learning/gradients`. Note: - only the SPSA, - parameter-shift and linear-combination-of-unitaries gradients are retained. Other gradient - strategies, such as reverse and finite-diff are not incorporated. + only the SPSA, parameter-shift and linear-combination-of-unitaries gradients are retained. + Other gradient strategies, such as reverse and finite-diff are not incorporated. - `qiskit_algorithms/optimizers` -> `qiskit_machine_learning/optimizers`. Note: - optimizers - from `scikit-quant `_ are not incorporated. + optimizers from `scikit-quant `_ are not + incorporated. - `qiskit_algorithms/state_fidelities` -> `qiskit_machine_learning/state_fidelities` @@ -34,9 +30,8 @@ features: upgrade: - | The merge of some of the features of Qiskit Algorithms into Qiskit Machine Learning might lead - to breaking changes. - For this reason, caution is advised when updating to version 0.8 during critical production - stages in a project. + to breaking changes. For this reason, caution is advised when updating to version 0.8 during + critical production stages in a project. - | Users must update their imports and code references in code that uses Qiskit Machine Leaning @@ -52,18 +47,14 @@ upgrade: - | To continue using sub-modules and functionalities of Qiskit Algorithms that **have not been - transferred**, - you may continue using them as before by importing from Qiskit Algorithms. However, be aware - that Qiskit Algorithms - is no longer officially supported and some of its functionalities may not work in your use case. - For any problems - directly related to Qiskit Algorithms, please open a GitHub issue at - https://github.com/qiskit-community/qiskit-algorithms. - Should you want to include a Qiskit Algorithms functionality that has not been incorporated in - Qiskit Machine Learning, - please open a feature-request issue at - https://github.com/qiskit-community/qiskit-machine-learning, explaining why - this change would be useful for you and other users. + transferred**, you may continue using them as before by importing from Qiskit Algorithms. + However, be aware that Qiskit Algorithms is no longer officially supported and some of its + functionalities may not work in your use case. For any problems directly related to Qiskit + Algorithms, please open a GitHub issue at https://github + .com/qiskit-community/qiskit-algorithms. Should you want to include a Qiskit Algorithms + functionality that has not been incorporated in Qiskit Machine Learning, please open a + feature-request issue at https://github.com/qiskit-community/qiskit-machine-learning, + explaining why this change would be useful for you and other users. - | Four examples of upgrading the code can be found below. From 2bbb57cd8d7c791d48f0e6786413f71d295d0cfe Mon Sep 17 00:00:00 2001 From: "M. Emre Sahin" <40424147+OkuyanBoga@users.noreply.github.com> Date: Thu, 7 Nov 2024 15:43:32 +0000 Subject: [PATCH 63/85] Added support for SamplerV2 primitives (#49) * Migrating `qiskit_algorithms` (#817) * Update README.md * Generalize the Einstein summation signature * Add reno * Update Copyright * Rename and add test * Update Copyright * Add docstring for `test_get_einsum_signature` * Correct spelling * Disable spellcheck for comments * Add `docstring` in pylint dict * Delete example in docstring * Add Einstein in pylint dict * Add full use case in einsum dict * Spelling and type ignore * Spelling and type ignore * Spelling and type ignore * Spelling and type ignore * Spelling and type ignore * Remove for loop in einsum function and remove Literal arguments (1/2) * Remove for loop in einsum function and remove Literal arguments (1/2) * Remove for loop in einsum function and remove Literal arguments (2/2) * Update RuntimeError msg * Update RuntimeError msg - line too long * Trigger CI * Merge algos, globals.random to fix * Fixed `algorithms_globals` * Import /tests and run CI locally * Fix copyrights and some spellings * Ignore mypy in 8 instances * Merge spell dicts * Black reformatting * Black reformatting * Add reno * Lint sanitize * Pylint * Pylint * Pylint * Pylint * Fix relative imports in tutorials * Fix relative imports in tutorials * Remove algorithms from Jupyter magic methods * Temporarily disable "Run stable tutorials" tests * Change the docstrings with imports from qiskit_algorithms * Styling * Update qiskit_machine_learning/optimizers/gradient_descent.py Co-authored-by: Declan Millar * Update qiskit_machine_learning/optimizers/optimizer_utils/learning_rate.py Co-authored-by: Declan Millar * Add more tests for utils * Add more tests for optimizers: adam, bobyqa, gsls and imfil * Fix random seed for volatile optimizers * Fix random seed for volatile optimizers * Add more tests * Pylint dict * Activate scikit-quant-0.8.2 * Remove scikit-quant methods * Remove scikit-quant methods (2) * Edit the release notes and Qiskit version 1+ * Edit the release notes and Qiskit version 1+ * Add Qiskit 1.0 upgrade in reno * Add Qiskit 1.0 upgrade in reno * Add Qiskit 1.0 upgrade in reno * Apply line breaks * Restructure line breaks --------- Co-authored-by: FrancescaSchiav Co-authored-by: M. Emre Sahin <40424147+OkuyanBoga@users.noreply.github.com> Co-authored-by: Declan Millar * Revamp readme pt2 (#822) * Restructure README.md --------- Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com> * V2 Primitive Support for SamplerQNN and Gradients * Update base_sampler_gradient.py * Update qiskit_machine_learning/gradients/param_shift/param_shift_sampler_gradient.py Co-authored-by: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> * Update qiskit_machine_learning/gradients/spsa/spsa_sampler_gradient.py Co-authored-by: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> * Update qiskit_machine_learning/gradients/spsa/spsa_sampler_gradient.py Co-authored-by: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> * Update qiskit_machine_learning/neural_networks/sampler_qnn.py Co-authored-by: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> * Update qiskit_machine_learning/neural_networks/sampler_qnn.py Co-authored-by: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> * Update qiskit_machine_learning/neural_networks/sampler_qnn.py Co-authored-by: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> * Update qiskit_machine_learning/neural_networks/sampler_qnn.py Co-authored-by: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> * Update qiskit_machine_learning/neural_networks/sampler_qnn.py Co-authored-by: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> * Update qiskit_machine_learning/neural_networks/sampler_qnn.py Co-authored-by: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> * Fix lint errors due to Pylint 3.3.0 update in CI (#833) * disable=too-many-positional-arguments * Transfer pylint rc to toml * Transfer pylint rc to toml * Minor fixes * Remove Python 3.8 from CI (#824) (#826) * Remove Python 3.8 in CI (#824) * Correct `tmp` dirs (#818) * Correct unit py version (#818) * Add reno (#818) * Finalze removal of py38 (#818) * Spelling * Remove duplicate tmp folder * Updated the release note * Bump min pyversion in toml * Remove ipython constraints * Update reno * Updated test for test_sampler_qnn * Fix: output_shape * Adding optimisation level to TestSamplerQNN SamplerV2 option * Correcting the PUB prep for SamplerV2 by changing max iterator from n to len(job_param_values). Added a load of print statements to investigate behaviour when self._output_shape = (2, 3) - a tuple as this was failing tests due to a comparison in line 166. This has lead me to think that the way we are calculating QuasiDistribution is wrong as we need to know which real qubits the virtual qubits have been transpiled too to calculate the correct dist for SamplerV2. Following this up with IBM runtime. * Update sampler_qnn.py for correcting tuple output_shape when interpret is provided. * Adding ISA capabilities to gradients * Fix output shape and its default for V2 * Implement SamplerV2 for bayesian inference * Implement SamplerV2 for bayesian inference * Adding ISA capabilities to SamplerQNN and ParamShiftSamplerGradient * Removing unused backend * Removing failed merge conflicts * Removing residual merge conflicts * added SamplerV2 support for ComputeUncompute * Removing multiple tranpilations within same test * Formatting * Linting * Adding measure_all to setUp * removing default pm --------- Co-authored-by: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Co-authored-by: FrancescaSchiav Co-authored-by: Declan Millar Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com> Co-authored-by: oscar-wallis --- .github/actions/run-tests/action.yml | 4 +- .github/workflows/deploy-code.yml | 4 +- .github/workflows/deploy-docs.yml | 4 +- .github/workflows/main.yml | 34 +- .mergify.yml | 4 +- .pylintdict | 16 +- .pylintrc | 381 ------------------ README.md | 154 +++---- constraints.txt | 1 - docs/getting_started.rst | 23 +- docs/index.rst | 109 ++--- pyproject.toml | 74 +++- .../classifiers/neural_network_classifier.py | 1 + .../algorithms/classifiers/pegasos_qsvc.py | 1 + .../algorithms/classifiers/vqc.py | 1 + .../algorithms/inference/qbayesian.py | 56 ++- .../algorithms/regressors/vqr.py | 1 + .../algorithms/trainable_model.py | 1 + qiskit_machine_learning/datasets/ad_hoc.py | 1 + .../gradients/base/base_sampler_gradient.py | 11 +- .../param_shift_sampler_gradient.py | 47 ++- .../gradients/spsa/spsa_estimator_gradient.py | 1 + .../gradients/spsa/spsa_sampler_gradient.py | 1 + .../kernels/fidelity_quantum_kernel.py | 1 + .../neural_networks/neural_network.py | 1 + .../neural_networks/sampler_qnn.py | 136 ++++--- .../optimizers/adam_amsgrad.py | 1 + qiskit_machine_learning/optimizers/aqgd.py | 2 + qiskit_machine_learning/optimizers/cg.py | 1 + qiskit_machine_learning/optimizers/cobyla.py | 1 + .../optimizers/gradient_descent.py | 1 + qiskit_machine_learning/optimizers/gsls.py | 3 + .../optimizers/l_bfgs_b.py | 1 + .../optimizers/nelder_mead.py | 1 + qiskit_machine_learning/optimizers/nft.py | 2 + qiskit_machine_learning/optimizers/p_bfgs.py | 1 + qiskit_machine_learning/optimizers/powell.py | 1 + qiskit_machine_learning/optimizers/qnspsa.py | 2 + qiskit_machine_learning/optimizers/slsqp.py | 1 + qiskit_machine_learning/optimizers/spsa.py | 4 + qiskit_machine_learning/optimizers/tnc.py | 1 + .../state_fidelities/compute_uncompute.py | 98 ++++- .../py38_end_of_support-fa1fdea6ea02b502.yaml | 6 + setup.py | 3 +- .../test_neural_network_classifier.py | 1 + test/algorithms/classifiers/test_vqc.py | 1 + test/algorithms/inference/test_qbayesian.py | 191 +++++++++ test/connectors/test_torch.py | 1 + test/connectors/test_torch_connector.py | 1 + test/kernels/test_fidelity_qkernel.py | 2 + test/neural_networks/test_sampler_qnn.py | 43 +- test/optimizers/test_spsa.py | 1 + .../test_compute_uncompute_v2.py | 343 ++++++++++++++++ tox.ini | 2 +- 54 files changed, 1150 insertions(+), 634 deletions(-) delete mode 100644 .pylintrc create mode 100644 releasenotes/notes/py38_end_of_support-fa1fdea6ea02b502.yaml create mode 100644 test/state_fidelities/test_compute_uncompute_v2.py diff --git a/.github/actions/run-tests/action.yml b/.github/actions/run-tests/action.yml index bb48e9e4c..9ade249ac 100644 --- a/.github/actions/run-tests/action.yml +++ b/.github/actions/run-tests/action.yml @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2021, 2023. +# (C) Copyright IBM 2021, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -36,7 +36,7 @@ runs: if [ "${{ inputs.event-name }}" == "schedule" ] || [ "${{ inputs.run-slow }}" == "true" ]; then export QISKIT_TESTS="run_slow" fi - if [ "${{ inputs.os }}" == "ubuntu-latest" ] && [ "${{ inputs.python-version }}" == "3.8" ]; then + if [ "${{ inputs.os }}" == "ubuntu-latest" ] && [ "${{ inputs.python-version }}" == "3.9" ]; then export PYTHON="coverage3 run --source qiskit_machine_learning --parallel-mode" fi stestr --test-path test run 2> >(tee /dev/stderr out.txt > /dev/null) diff --git a/.github/workflows/deploy-code.yml b/.github/workflows/deploy-code.yml index ad80409ba..163e75b87 100644 --- a/.github/workflows/deploy-code.yml +++ b/.github/workflows/deploy-code.yml @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2021, 2023. +# (C) Copyright IBM 2021, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -25,7 +25,7 @@ jobs: id-token: write strategy: matrix: - python-version: [3.8] + python-version: [3.9] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 9e01421ee..b26b9f8da 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2021, 2023. +# (C) Copyright IBM 2021, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.8] + python-version: [3.9] steps: - uses: actions/checkout@v4 with: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 828f48b76..2bdd419ad 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -35,7 +35,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - python-version: [3.8] + python-version: [3.9] steps: - name: Print Concurrency Group env: @@ -112,14 +112,14 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - python-version: [3.8, 3.9, '3.10', 3.11, 3.12] + python-version: [3.9, '3.10', 3.11, 3.12] include: - os: macos-latest - python-version: 3.8 + python-version: 3.9 - os: macos-latest python-version: 3.12 - os: windows-latest - python-version: 3.8 + python-version: 3.9 - os: windows-latest python-version: 3.12 # macos-14 is an Arm64 image @@ -165,7 +165,7 @@ jobs: run: | coverage3 combine mv .coverage ./ci-artifact-data/ml.dat - if: ${{ matrix.os == 'ubuntu-latest' && matrix.python-version == 3.8 }} + if: ${{ matrix.os == 'ubuntu-latest' && matrix.python-version == 3.9 }} shell: bash - uses: actions/upload-artifact@v4 with: @@ -188,7 +188,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - python-version: [3.8, 3.12] + python-version: [3.9, 3.12] steps: - uses: actions/checkout@v4 with: @@ -251,29 +251,25 @@ jobs: # cd docs/_build/html # mkdir artifacts # tar -zcvf artifacts/tutorials.tar.gz --exclude=./artifacts . -# if: ${{ matrix.python-version == 3.8 && !startsWith(github.ref, 'refs/heads/stable') && !startsWith(github.base_ref, 'stable/') }} +# if: ${{ matrix.python-version == 3.9 && !startsWith(github.ref, 'refs/heads/stable') && !startsWith(github.base_ref, 'stable/') }} # shell: bash # - name: Run upload stable tutorials # uses: actions/upload-artifact@v4 # with: # name: tutorials-stable${{ matrix.python-version }} # path: docs/_build/html/artifacts/tutorials.tar.gz -# if: ${{ matrix.python-version == 3.8 && !startsWith(github.ref, 'refs/heads/stable') && !startsWith(github.base_ref, 'stable/') }} +# if: ${{ matrix.python-version == 3.9 && !startsWith(github.ref, 'refs/heads/stable') && !startsWith(github.base_ref, 'stable/') }} Deprecation_Messages_and_Coverage: needs: [Checks, MachineLearning, Tutorials] runs-on: ubuntu-latest strategy: matrix: - python-version: [3.8] + python-version: [3.9] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - uses: actions/download-artifact@v4 - with: - name: ubuntu-latest-3.8 - path: /tmp/u38 - uses: actions/download-artifact@v4 with: name: ubuntu-latest-3.9 @@ -292,16 +288,16 @@ jobs: path: /tmp/u312 - uses: actions/download-artifact@v4 with: - name: macos-latest-3.8 - path: /tmp/m38 + name: macos-latest-3.9 + path: /tmp/m39 - uses: actions/download-artifact@v4 with: name: macos-latest-3.12 path: /tmp/m312 - uses: actions/download-artifact@v4 with: - name: windows-latest-3.8 - path: /tmp/w38 + name: windows-latest-3.9 + path: /tmp/w39 - uses: actions/download-artifact@v4 with: name: windows-latest-3.12 @@ -319,10 +315,10 @@ jobs: shell: bash - name: Combined Deprecation Messages run: | - sort -f -u /tmp/u38/ml.dep /tmp/u39/ml.dep /tmp/u310/ml.dep /tmp/u311/ml.dep /tmp/u312/ml.dep /tmp/m38/ml.dep /tmp/m312/ml.dep /tmp/w38/ml.dep /tmp/w312/ml.dep /tmp/a310/ml.dep /tmp/a312/ml.dep || true + sort -f -u /tmp/u39/ml.dep /tmp/u310/ml.dep /tmp/u311/ml.dep /tmp/u312/ml.dep /tmp/m39/ml.dep /tmp/m312/ml.dep /tmp/w39/ml.dep /tmp/w312/ml.dep /tmp/a310/ml.dep /tmp/a312/ml.dep || true shell: bash - name: Coverage combine - run: coverage3 combine /tmp/u38/ml.dat + run: coverage3 combine /tmp/u39/ml.dat shell: bash - name: Upload to Coveralls env: diff --git a/.mergify.yml b/.mergify.yml index d9a318886..ca27e5964 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -1,12 +1,12 @@ queue_rules: - name: automerge conditions: - - check-success=Deprecation_Messages_and_Coverage (3.8) + - check-success=Deprecation_Messages_and_Coverage (3.9) pull_request_rules: - name: automatic merge on CI success and review conditions: - - check-success=Deprecation_Messages_and_Coverage (3.8) + - check-success=Deprecation_Messages_and_Coverage (3.9) - "#approved-reviews-by>=1" - label=automerge - label!=on hold diff --git a/.pylintdict b/.pylintdict index 375963119..93892d47d 100644 --- a/.pylintdict +++ b/.pylintdict @@ -23,6 +23,7 @@ armijo arxiv asmatrix aspuru +assertraises async autoencoder autoencoders @@ -136,6 +137,7 @@ elif endian entangler enum +eol eps estimatorqnn et @@ -158,6 +160,7 @@ farhi farrokh fi fidelities +fidelity fidelityquantumkernel filippo fletcher @@ -201,6 +204,7 @@ hadfield hamiltonian hamiltonians hao +hartree hashable hatano havlíček @@ -208,6 +212,7 @@ heidelberg hessians hilbert hoc +homebrew hopkins hoyer html @@ -259,6 +264,7 @@ kwargs labelled lagrange langle +linux larrañaga lcu len @@ -347,6 +353,7 @@ o'brien objval observables oct +october olson onboarding onodera @@ -452,10 +459,12 @@ rhs rightarrow robert romero +rosenbrock rosen runarsson runtime runtimes +RuntimeError rx ry rz @@ -495,6 +504,9 @@ sqrt statefn statevector statevectors +stdlib +stdout +stfc stddev stdlib stdout @@ -594,7 +606,3 @@ zz θ ψ ω -assertRaises -RuntimeError -Rosenbrock -fidelities diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index 43b37e7c4..000000000 --- a/.pylintrc +++ /dev/null @@ -1,381 +0,0 @@ -[MASTER] - -# Specify a configuration file. -#rcfile= - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -# Changed to fix recursion crash since pandas 1.1.5 -init-hook='import sys; sys.setrecursionlimit(8 * sys.getrecursionlimit())' - -# Add files or directories to the blacklist. They should be base names, not -# paths. -ignore=CVS - -# Add files or directories matching the regex patterns to the blacklist. The -# regex matches against base names, not paths. -ignore-patterns= - -# Pickle collected data for later comparisons. -persistent=yes - -# List of plugins (as comma separated values of python modules names) to load, -# usually to register additional checkers. -load-plugins=pylint.extensions.docparams, # enable checking of docstring args - pylint.extensions.docstyle, # basic docstring style checks - -# Use multiple processes to speed up Pylint. -jobs=1 - -# Allow loading of arbitrary C extensions. Extensions are imported into the -# active Python interpreter and may run arbitrary code. -unsafe-load-any-extension=no - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code -extension-pkg-whitelist= - - -[MESSAGES CONTROL] - -# Only show warnings with the listed confidence levels. Leave empty to show -# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED -confidence= - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where -# it should appear only once). See also the "--disable" option for examples. -#enable= - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifiers separated by comma (,) or put this -# option multiple times (only on the command line, not in the configuration -# file where it should appear only once).You can also use "--disable=all" to -# disable everything first and then reenable specific checks. For example, if -# you want to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use"--disable=all --enable=classes -# --disable=W" -disable=fixme, # disabled as TODOs would show up as warnings - protected-access, # disabled as we don't follow the public vs private - # convention strictly - duplicate-code, # disabled as it is too verbose - redundant-returns-doc, # for @abstractmethod, it cannot interpret "pass" - # disable the "too-many/few-..." refactoring hints - too-many-lines, too-many-branches, too-many-locals, too-many-nested-blocks, - too-many-statements, too-many-instance-attributes, too-many-arguments, - too-many-public-methods, too-few-public-methods, too-many-ancestors, - unnecessary-pass, # allow for methods with just "pass", for clarity - no-else-return, # relax "elif" after a clause with a return - docstring-first-line-empty, # relax docstring style - import-outside-toplevel, - - -[REPORTS] - -# Set the output format. Available formats are text, parseable, colorized, msvs -# (visual studio) and html. You can also give a reporter class, eg -# mypackage.mymodule.MyReporterClass. -output-format=text - -# Tells whether to display a full report or only the messages -reports=yes - -# Python expression which should return a note less than 10 (10 is the highest -# note). You have access to the variables errors warning, statement which -# respectively contain the number of errors / warnings messages and the total -# number of statements analyzed. This is used by the global evaluation report -# (RP0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details -#msg-template= - - -[BASIC] - -# Good variable names which should always be accepted, separated by a comma -# i,j,k = typical indices -# n,m = typical numbers -# ex = for exceptions and errors -# v,w = typical vectors -# x,y,z = typical axes -# _ = placeholder name -# q,r,qr,cr,qc = quantum and classical registers, and quantum circuit -# pi = the PI constant -# op = operation iterator -# b = basis iterator -good-names=i,j,k,n,m,ex,v,w,x,y,z,Run,_,logger,q,c,r,qr,cr,qc,nd,pi,op,b,ar,br,a,mu, - __unittest,iSwapGate - -# Bad variable names which should always be refused, separated by a comma -bad-names=foo,bar,toto,tutu,tata - -# Colon-delimited sets of names that determine each other's naming style when -# the name regexes allow several styles. -name-group= - -# Include a hint for the correct naming format with invalid-name -include-naming-hint=no - -# List of decorators that produce properties, such as abc.abstractproperty. Add -# to this list to register other decorators that produce valid properties. -property-classes=abc.abstractproperty - -# Regular expression matching correct module names -module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ - -# Regular expression matching correct constant names -const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ - -# Regular expression matching correct class names -class-rgx=[A-Z_][a-zA-Z0-9]+$ - -# Regular expression matching correct function names -function-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression matching correct method names -method-rgx=(([a-z_][a-z0-9_]{2,49})|(assert[A-Z][a-zA-Z0-9]{2,43})|(test_[_a-zA-Z0-9]{2,}))$ - -# Regular expression matching correct attribute names -attr-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression matching correct argument names -argument-rgx=[a-z_][a-z0-9_]{2,30}|ax|dt$ - -# Regular expression matching correct variable names -variable-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression matching correct class attribute names -class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ - -# Regular expression matching correct inline iteration names -inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ - -# Regular expression which should only match function or class names that do -# not require a docstring. -no-docstring-rgx=^_ - -# Minimum line length for functions/classes that require docstrings, shorter -# ones are exempt. -docstring-min-length=-1 - - -[ELIF] - -# Maximum number of nested blocks for function / method body -max-nested-blocks=5 - - -[FORMAT] - -# Maximum number of characters on a single line. -max-line-length=105 - -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=^\s*(# )??$ - -# Allow the body of an if to be on the same line as the test if there is no -# else. -single-line-if-stmt=no - -# Maximum number of lines in a module -max-module-lines=1000 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' - -# Number of spaces of indent required inside a hanging or continued line. -indent-after-paren=4 - -# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -expected-line-ending-format= - - -[LOGGING] - -# Logging modules to check that the string format arguments are in logging -# function parameter format -logging-modules=logging - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME,XXX,TODO - - -[SIMILARITIES] - -# Minimum lines number of a similarity. -min-similarity-lines=4 - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - -# Ignore imports when computing similarities. -ignore-imports=no - - -[SPELLING] - -# Spelling dictionary name. Available dictionaries: none. To make it working -# install python-enchant package. -spelling-dict= - -# List of comma separated words that should not be checked. -spelling-ignore-words= - -# A path to a file that contains private dictionary; one word per line. -spelling-private-dict-file= - -# Tells whether to store unknown words to indicated private dictionary in -# --spelling-private-dict-file option instead of raising a message. -spelling-store-unknown-words=no - - -[TYPECHECK] - -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis. It -# supports qualified module names, as well as Unix pattern matching. -ignored-modules=matplotlib.cm,numpy.random,retworkx,torch - -# List of class names for which member attributes should not be checked (useful -# for classes with dynamically set attributes). This supports the use of -# qualified names. -ignored-classes=optparse.Values,thread._local,_thread._local,QuantumCircuit,torch - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular -# expressions are accepted. -generated-members= - -# List of decorators that produce context managers, such as -# contextlib.contextmanager. Add to this list to register other decorators that -# produce valid context managers. -contextmanager-decorators=contextlib.contextmanager - - -[VARIABLES] - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# A regular expression matching the name of dummy variables (i.e. expectedly -# not used). -dummy-variables-rgx=(_+[a-zA-Z0-9]*?$)|dummy - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid to define new builtins when possible. -additional-builtins= - -# List of strings which can identify a callback function by name. A callback -# name must start or end with one of those strings. -callbacks=cb_,_cb - -# List of qualified module names which can have objects that can redefine -# builtins. -redefining-builtins-modules=six.moves,future.builtins - - -[CLASSES] - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__,__new__,setUp - -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg=cls - -# List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=mcs - -# List of member names, which should be excluded from the protected access -# warning. -exclude-protected=_asdict,_fields,_replace,_source,_make - - -[DESIGN] - -# Maximum number of arguments for function / method -max-args=8 - -# Argument names that match this expression will be ignored. Default to name -# with leading underscore -ignored-argument-names=_.* - -# Maximum number of locals for function / method body -max-locals=15 - -# Maximum number of return / yield for function / method body -max-returns=6 - -# Maximum number of branch for function / method body -max-branches=12 - -# Maximum number of statements in function / method body -max-statements=50 - -# Maximum number of parents for a class (see R0901). -max-parents=7 - -# Maximum number of attributes for a class (see R0902). -max-attributes=10 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=2 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=35 - -# Maximum number of boolean expressions in a if statement -max-bool-expr=5 - - -[IMPORTS] - -# Deprecated modules which should not be used, separated by a comma -deprecated-modules=optparse - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled) -import-graph= - -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled) -ext-import-graph= - -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled) -int-import-graph= - -# Force import order to recognize a module as part of the standard -# compatibility libraries. -known-standard-library= - -# Force import order to recognize a module as part of a third party library. -known-third-party=enchant - -# Analyse import fallback blocks. This can be used to support both Python 2 and -# 3 compatible code, which means that the block might have code that exists -# only in one or another interpreter, leading to false positives when analysed. -analyse-fallback-blocks=no - - -[EXCEPTIONS] - -# Exceptions that will emit a warning when being caught. Defaults to -# "Exception" -overgeneral-exceptions=builtins.Exception diff --git a/README.md b/README.md index 3973737ad..35f3b6260 100644 --- a/README.md +++ b/README.md @@ -12,103 +12,105 @@ ## What is Qiskit Machine Learning? -Qiskit Machine Learning introduces fundamental computational building blocks, such as Quantum Kernels -and Quantum Neural Networks, used in different applications, including classification and regression. -On the one hand, this design is very easy to use and allows users to rapidly prototype a first model -without deep quantum computing knowledge. On the other hand, Qiskit Machine Learning is very flexible, -and users can easily extend it to support cutting-edge quantum machine learning research. +Qiskit Machine Learning introduces fundamental computational building blocks, such as Quantum +Kernels and Quantum Neural Networks, used in various applications including classification +and regression. + +This library is part of the Qiskit Community ecosystem, a collection of high-level codes that are based +on the Qiskit software development kit. As of version `0.7.0`, Qiskit Machine Learning is co-maintained +by IBM and the Hartree Center, part of the UK Science and Technologies Facilities Council (STFC). + +The Qiskit Machine Learning framework aims to be: + +* **User-friendly**, allowing users to quickly and easily prototype quantum machine learning models without + the need of extensive quantum computing knowledge. +* **Flexible**, providing tools and functionalities to conduct proof-of-concepts and innovative research + in quantum machine learning for both beginners and experts. +* **Extensible**, facilitating the integration of new cutting-edge features leveraging Qiskit's + architectures, patterns and related services. + ## What are the main features of Qiskit Machine Learning? -Qiskit Machine Learning provides the -[FidelityQuantumKernel](https://qiskit-community.github.io/qiskit-machine-learning/stubs/qiskit_machine_learning.kernels.QuantumKernel.html#qiskit_machine_learning.kernels.FidelityQuantumKernel) -class that makes use of the [Fidelity](https://qiskit-community.github.io/qiskit-algorithms/stubs/qiskit_algorithms.state_fidelities.BaseStateFidelity.html) algorithm introduced in Qiskit Algorithms and can be easily used -to directly compute kernel matrices for given datasets or can be passed to a Quantum Support Vector Classifier -[QSVC](https://qiskit-community.github.io/qiskit-machine-learning/stubs/qiskit_machine_learning.algorithms.QSVC.html#qiskit_machine_learning.algorithms.QSVC) or -Quantum Support Vector Regressor -[QSVR](https://qiskit-community.github.io/qiskit-machine-learning/stubs/qiskit_machine_learning.algorithms.QSVR.html#qiskit_machine_learning.algorithms.QSVR) -to quickly start solving classification or regression problems. -It also can be used with many other existing kernel-based machine learning algorithms from established -classical frameworks. - -Qiskit Machine Learning defines a generic interface for neural networks that is implemented by different -quantum neural networks. Two core implementations are readily provided, such as the -[EstimatorQNN](https://qiskit-community.github.io/qiskit-machine-learning/stubs/qiskit_machine_learning.neural_networks.EstimatorQNN.html), -and the [SamplerQNN](https://qiskit-community.github.io/qiskit-machine-learning/stubs/qiskit_machine_learning.neural_networks.SamplerQNN.html). -The [EstimatorQNN](https://qiskit-community.github.io/qiskit-machine-learning/stubs/qiskit_machine_learning.neural_networks.EstimatorQNN.html) -leverages the [Estimator](https://docs.quantum.ibm.com/api/qiskit/qiskit.primitives.BaseEstimator) primitive from Qiskit and -allows users to combine parametrized quantum circuits with quantum mechanical observables. The circuits can be constructed using, for example, building blocks -from Qiskit’s circuit library, and the QNN’s output is given by the expected value of the observable. -The [SamplerQNN](https://qiskit-community.github.io/qiskit-machine-learning/stubs/qiskit_machine_learning.neural_networks.SamplerQNN.html) -leverages another primitive introduced in Qiskit, the [Sampler](https://docs.quantum.ibm.com/api/qiskit/qiskit.primitives.BaseSampler) primitive. -This neural network translates quasi-probabilities of bitstrings estimated by the primitive into a desired output. This -translation step can be used to interpret a given bitstring in a particular context, e.g. translating it into a set of classes. - -The neural networks include the functionality to evaluate them for a given input as well as to compute the -corresponding gradients, which is important for efficient training. To train and use neural networks, -Qiskit Machine Learning provides a variety of learning algorithms such as the -[NeuralNetworkClassifier](https://qiskit-community.github.io/qiskit-machine-learning/stubs/qiskit_machine_learning.algorithms.NeuralNetworkClassifier.html#qiskit_machine_learning.algorithms.NeuralNetworkClassifier) -and -[NeuralNetworkRegressor](https://qiskit-community.github.io/qiskit-machine-learning/stubs/qiskit_machine_learning.algorithms.NeuralNetworkRegressor.html#qiskit_machine_learning.algorithms.NeuralNetworkRegressor). -Both take a QNN as input and then use it in a classification or regression context. -To allow an easy start, two convenience implementations are provided - the Variational Quantum Classifier -[VQC](https://qiskit-community.github.io/qiskit-machine-learning/stubs/qiskit_machine_learning.algorithms.VQC.html#qiskit_machine_learning.algorithms.VQC) -as well as the Variational Quantum Regressor -[VQR](https://qiskit-community.github.io/qiskit-machine-learning/stubs/qiskit_machine_learning.algorithms.VQR.html#qiskit_machine_learning.algorithms.VQR). -Both take just a feature map and an ansatz and construct the underlying QNN automatically. - -In addition to the models provided directly in Qiskit Machine Learning, it has the -[TorchConnector](https://qiskit-community.github.io/qiskit-machine-learning/stubs/qiskit_machine_learning.connectors.TorchConnector.html#qiskit_machine_learning.connectors.TorchConnector), -which allows users to integrate all of our quantum neural networks directly into the -[PyTorch](https://pytorch.org) -open source machine learning library. Thanks to Qiskit’s gradient algorithms, this includes automatic -differentiation - the overall gradients computed by [PyTorch](https://pytorch.org) -during the backpropagation take into -account quantum neural networks, too. The flexible design also allows the building of connectors -to other packages in the future. - -## Installation - -We encourage installing Qiskit Machine Learning via the pip tool (a python package manager). +### Kernel-based methods + +The [`FidelityQuantumKernel`](https://qiskit-community.github.io/qiskit-machine-learning/stubs/qiskit_machine_learning.kernels.QuantumKernel.html#qiskit_machine_learning.kernels.FidelityQuantumKernel) +class uses the [`Fidelity`](https://qiskit-community.github.io/qiskit-machine-learning/stubs/qiskit_machine_learning.state_fidelities.BaseStateFidelity.html)) +algorithm. It computes kernel matrices for datasets and can be combined with a Quantum Support Vector Classifier ([`QSVC`](https://qiskit-community.github.io/qiskit-machine-learning/stubs/qiskit_machine_learning.algorithms.QSVC.html#qiskit_machine_learning.algorithms.QSVC)) +or a Quantum Support Vector Regressor ([`QSVR`](https://qiskit-community.github.io/qiskit-machine-learning/stubs/qiskit_machine_learning.algorithms.QSVR.html#qiskit_machine_learning.algorithms.QSVR)) +to solve classification or regression problems respectively. It is also compatible with classical kernel-based machine learning algorithms. + + +### Quantum Neural Networks (QNNs) + +Qiskit Machine Learning defines a generic interface for neural networks, implemented by two core (derived) primitives: + +- **[`EstimatorQNN`](https://qiskit-community.github.io/qiskit-machine-learning/stubs/qiskit_machine_learning.neural_networks.EstimatorQNN.html):** Leverages the [`Estimator`](https://docs.quantum.ibm.com/api/qiskit/qiskit.primitives.BaseEstimator) primitive, combining parametrized quantum circuits with quantum mechanical observables. The output is the expected value of the observable. + +- **[`SamplerQNN`](https://qiskit-community.github.io/qiskit-machine-learning/stubs/qiskit_machine_learning.neural_networks.SamplerQNN.html):** Leverages the [`Sampler`](https://docs.quantum.ibm.com/api/qiskit/qiskit.primitives.BaseSampler) primitive, translating bit-string counts into the desired outputs. + +To train and use neural networks, Qiskit Machine Learning provides learning algorithms such as the [`NeuralNetworkClassifier`](https://qiskit-community.github.io/qiskit-machine-learning/stubs/qiskit_machine_learning.algorithms.NeuralNetworkClassifier.html#qiskit_machine_learning.algorithms.NeuralNetworkClassifier) +and [`NeuralNetworkRegressor`](https://qiskit-community.github.io/qiskit-machine-learning/stubs/qiskit_machine_learning.algorithms.NeuralNetworkRegressor.html#qiskit_machine_learning.algorithms.NeuralNetworkRegressor). +Finally, built on these, the Variational Quantum Classifier ([`VQC`](https://qiskit-community.github.io/qiskit-machine-learning/stubs/qiskit_machine_learning.algorithms.VQC.html#qiskit_machine_learning.algorithms.VQC)) +and the Variational Quantum Regressor ([`VQR`](https://qiskit-community.github.io/qiskit-machine-learning/stubs/qiskit_machine_learning.algorithms.VQR.html#qiskit_machine_learning.algorithms.VQR)) +take a _feature map_ and an _ansatz_ to construct the underlying QNN automatically using high-level syntax. + +### Integration with PyTorch + +The [`TorchConnector`](https://qiskit-community.github.io/qiskit-machine-learning/stubs/qiskit_machine_learning.connectors.TorchConnector.html#qiskit_machine_learning.connectors.TorchConnector) +integrates QNNs with [PyTorch](https://pytorch.org). +Thanks to the gradient algorithms in Qiskit Machine Learning, this includes automatic differentiation. +The overall gradients computed by PyTorch during the backpropagation take into account quantum neural +networks, too. The flexible design also allows the building of connectors to other packages in the future. + +## Installation and documentation + +We encourage installing Qiskit Machine Learning via the `pip` tool, a `Python` package manager. ```bash pip install qiskit-machine-learning ``` -**pip** will handle all dependencies automatically and you will always install the latest -(and well-tested) version. +`pip` will install all dependencies automatically, so that you will always have the most recent +stable version. -If you want to work on the very latest work-in-progress versions, either to try features ahead of -their official release or if you want to contribute to Machine Learning, then you can install from source. -To do this follow the instructions in the +If you want to work instead on the very latest _work-in-progress_ versions of Qiskit Machine Learning, +either to try features ahead of +their official release or if you want to contribute to the library, then you can install from source. +For more details on how to do so and much more, follow the instructions in the [documentation](https://qiskit-community.github.io/qiskit-machine-learning/getting_started.html#installation). ### Optional Installs -* **PyTorch**, may be installed either using command `pip install 'qiskit-machine-learning[torch]'` to install the +* **PyTorch** may be installed either using command `pip install 'qiskit-machine-learning[torch]'` to install the package or refer to PyTorch [getting started](https://pytorch.org/get-started/locally/). When PyTorch is installed, the `TorchConnector` facilitates its use of quantum computed networks. -* **Sparse**, may be installed using command `pip install 'qiskit-machine-learning[sparse]'` to install the - package. Sparse being installed will enable the usage of sparse arrays/tensors. +* **Sparse** may be installed using command `pip install 'qiskit-machine-learning[sparse]'` to install the + package. Sparse being installed will enable the usage of sparse arrays and tensors. + +* **NLopt** is required for the global optimizers. [`NLopt`](https://nlopt.readthedocs.io/en/latest/) + can be installed manually with `pip install nlopt` on Windows and Linux platforms, or with `brew + install nlopt` on MacOS using the Homebrew package manager. For more information, + refer to the [installation guide](https://nlopt.readthedocs.io/en/latest/NLopt_Installation/). ## Migration to Qiskit 1.x > [!NOTE] -> Qiskit Machine Learning learning depends on Qiskit, which will be automatically installed as a -> dependency when you install Qiskit Machine Learning. If you have a pre-`1.0` version of Qiskit -> installed in your environment (however it was installed), and wish to upgrade to `1.0`, you -> should take note of the +> Qiskit Machine Learning depends on Qiskit, which will be automatically installed as a +> dependency when you install Qiskit Machine Learning. From version `0.8.0` of Qiskit Machine +> Learning, Qiskit `1.0` or above will be required. If you have a pre-`1.0` version of Qiskit +> installed in your environment (however it was installed), you should upgrade to `1.x` to +> continue using the latest features. You may refer to the > official [Qiskit 1.0 Migration Guide](https://docs.quantum.ibm.com/api/migration-guides/qiskit-1.0) -> for detailed instructions and examples on how to upgrade. +> for detailed instructions and examples on how to upgrade Qiskit. ---------------------------------------------------------------------------------------------------- ### Creating Your First Machine Learning Programming Experiment in Qiskit -Now that Qiskit Machine Learning is installed, it's time to begin working with the Machine Learning module. -Let's try an experiment using VQC (Variational Quantum Classifier) algorithm to -train and test samples from a data set to see how accurately the test set can -be classified. +Now that Qiskit Machine Learning is installed, it's time to begin working with the Machine +Learning module. Let's try an experiment using VQC (Variational Quantum Classifier) algorithm to +train and test samples from a data set to see how accurately the test set can be classified. ```python from qiskit.circuit.library import TwoLocal, ZZFeatureMap @@ -177,9 +179,11 @@ For questions that are more suited for a forum, we use the **Qiskit** tag in [St ## Humans behind Qiskit Machine Learning -Qiskit Machine Learning was inspired, authored and brought about by the collective work of a team of researchers -and software engineers. This library continues to grow with the help and work of -[many people](https://github.com/qiskit-community/qiskit-machine-learning/graphs/contributors), who contribute to the project at different levels. +Qiskit Machine Learning was inspired, authored and brought about by the collective work of a +team of researchers and software engineers. This library continues to grow with the help and +work of +[many people](https://github.com/qiskit-community/qiskit-machine-learning/graphs/contributors), +who contribute to the project at different levels. ## How can I cite Qiskit Machine Learning? If you use Qiskit, please cite as per the provided diff --git a/constraints.txt b/constraints.txt index cfe92e0b9..4cf890b52 100644 --- a/constraints.txt +++ b/constraints.txt @@ -1,4 +1,3 @@ numpy>=1.20,<2.0 -ipython<8.13;python_version<'3.9' nbconvert<7.14 # workaround https://github.com/jupyter/nbconvert/issues/2092 diff --git a/docs/getting_started.rst b/docs/getting_started.rst index b64772460..5ef8a3c99 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -14,7 +14,7 @@ that first. Then the information here can be followed which focuses on the addit specific to Qiskit Machine Learning. Qiskit Machine Learning has some functions that have been made optional where the dependent code and/or -support program(s) are not (or cannot be) installed by default. Those are PyTorch and Sparse. +support program(s) are not (or cannot be) installed by default. Those are PyTorch, Sparse and NLopt. See :ref:`optional_installs` for more information. .. tab-set:: @@ -97,6 +97,27 @@ Optional installs * **Sparse**, may be installed using command ``pip install 'qiskit-machine-learning[sparse]'`` to install the package. Sparse being installed will enable the usage of sparse arrays/tensors. +* **NLopt** is required for the global optimizers. `NLOpt `__ + can be installed manually with ``pip install nlopt`` on Windows and Linux platforms, or with + ``brew install nlopt`` on MacOS using the Homebrew package manager. For more information, refer + to the `installation guide `__. + +.. _migration-to-qiskit-1x: + +Migration to Qiskit 1.x +======================== + +.. note:: + + Qiskit Machine Learning depends on Qiskit, which will be automatically installed as a + dependency when you install Qiskit Machine Learning. From version ``0.8.0`` of Qiskit Machine + Learning, Qiskit ``1.0`` or above will be required. If you have a pre-``1.0`` version of Qiskit + installed in your environment (however it was installed), you should upgrade to ``1.x`` to + continue using the latest features. You may refer to the + official `Qiskit 1.0 Migration Guide `_ + for detailed instructions and examples on how to upgrade Qiskit. + + ---- Ready to get going?... diff --git a/docs/index.rst b/docs/index.rst index ee045ff58..767c75bce 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -5,58 +5,63 @@ Qiskit Machine Learning overview Overview ============== -Qiskit Machine Learning introduces fundamental computational building blocks - such as Quantum Kernels -and Quantum Neural Networks - used in different applications, including classification and regression. -On the one hand, this design is very easy to use and allows users to rapidly prototype a first model -without deep quantum computing knowledge. On the other hand, Qiskit Machine Learning is very flexible, -and users can easily extend it to support cutting-edge quantum machine learning research. - -Qiskit Machine Learning provides the :class:`~qiskit_machine_learning.kernels.FidelityQuantumKernel` -class class that makes use of the :class:`~qiskit_algorithms.state_fidelities.BaseStateFidelity` algorithm -introduced in Qiskit and can be easily used to directly compute kernel matrices for given datasets -or can be passed to a Quantum Support Vector Classifier -(:class:`~qiskit_machine_learning.algorithms.QSVC`) or -Quantum Support Vector Regressor (:class:`~qiskit_machine_learning.algorithms.QSVR`) -to quickly start solving classification or regression problems. -It also can be used with many other existing kernel-based machine learning algorithms from established -classical frameworks. - -Qiskit Machine Learning defines a generic interface for neural networks that is implemented by different -quantum neural networks. Two core implementations are readily provided, such as the -:class:`~qiskit_machine_learning.neural_networks.EstimatorQNN` -and the :class:`~qiskit_machine_learning.neural_networks.SamplerQNN`. -The :class:`~qiskit_machine_learning.neural_networks.EstimatorQNN` leverages -the :class:`~qiskit.primitives.BaseEstimator` primitive from Qiskit and allows users to combine -parametrized quantum circuits with quantum mechanical observables. The circuits can be constructed -using, for example, building blocks from Qiskit's circuit library, and the QNN's output is given -by the expected value of the observable. -The :class:`~qiskit_machine_learning.neural_networks.SamplerQNN` leverages another primitive -introduced in Qiskit, the :class:`~qiskit.primitives.BaseSampler` primitive. This neural network -translates quasi-probabilities of bitstrings estimated by the primitive into a desired output. This -translation step can be used to interpret a given bitstring in a particular context, e.g. -translating it into a set of classes. - -The neural networks include the functionality to evaluate them for a given input as well as to compute the -corresponding gradients, which is important for efficient training. To train and use neural networks, -Qiskit Machine Learning provides a variety of learning algorithms such as the -:class:`~qiskit_machine_learning.algorithms.NeuralNetworkClassifier` and -:class:`~qiskit_machine_learning.algorithms.NeuralNetworkRegressor`. -Both take a QNN as input and then use it in a classification or regression context. -To allow an easy start, two convenience implementations are provided - the Variational Quantum Classifier -(:class:`~qiskit_machine_learning.algorithms.VQC`) -as well as the Variational Quantum Regressor (:class:`~qiskit_machine_learning.algorithms.VQR`). -Both take just a feature map and an ansatz and construct the underlying QNN automatically. - -In addition to the models provided directly in Qiskit Machine Learning, it has the -:class:`~qiskit_machine_learning.connectors.TorchConnector`, -which allows users to integrate all of our quantum neural networks directly into the -`PyTorch `__ -open source machine learning library. Thanks to Qiskit Algorithm's gradient algorithms, -this includes automatic -differentiation - the overall gradients computed by `PyTorch `__ -during the backpropagation take into -account quantum neural networks, too. The flexible design also allows the building of connectors -to other packages in the future. +Qiskit Machine Learning introduces fundamental computational building blocks, such as Quantum +Kernels and Quantum Neural Networks, used in various applications including classification +and regression. + +This library is part of the Qiskit Community ecosystem, a collection of high-level codes that are based +on the Qiskit software development kit. As of version ``0.7.0``, Qiskit Machine Learning is co-maintained +by IBM and the Hartree Center, part of the UK Science and Technologies Facilities Council (STFC). + +The Qiskit Machine Learning framework aims to be: + +* **User-friendly**, allowing users to quickly and easily prototype quantum machine learning models without + the need of extensive quantum computing knowledge. +* **Flexible**, providing tools and functionalities to conduct proof-of-concepts and innovative research + in quantum machine learning for both beginners and experts. +* **Extensible**, facilitating the integration of new cutting-edge features leveraging Qiskit's + architectures, patterns and related services. + +What are the main features of Qiskit Machine Learning? +====================================================== + +Kernel-based methods +--------------------- + +The :class:`~qiskit_machine_learning.kernels.FidelityQuantumKernel` +class uses the :class:`~qiskit_algorithms.state_fidelities.BaseStateFidelity` +algorithm. It computes kernel matrices for datasets and can be combined with a Quantum Support Vector Classifier (:class:`~qiskit_machine_learning.algorithms.QSVC`) +or a Quantum Support Vector Regressor (:class:`~qiskit_machine_learning.algorithms.QSVR`) +to solve classification or regression problems respectively. It is also compatible with classical kernel-based machine learning algorithms. + +Quantum Neural Networks (QNNs) +------------------------------ + +Qiskit Machine Learning defines a generic interface for neural networks, implemented by two core (derived) primitives: + +- :class:`~qiskit_machine_learning.neural_networks.EstimatorQNN` leverages the Qiskit + `Estimator `__ primitive, combining parametrized quantum circuits + with quantum mechanical observables. The output is the expected value of the observable. + +- :class:`~qiskit_machine_learning.neural_networks.SamplerQNN` leverages the Qiskit + `Sampler `__ primitive, + translating bit-string counts into the desired outputs. + +To train and use neural networks, Qiskit Machine Learning provides learning algorithms such as the :class:`~qiskit_machine_learning.algorithms.NeuralNetworkClassifier` +and :class:`~qiskit_machine_learning.algorithms.NeuralNetworkRegressor`. +Finally, built on these, the Variational Quantum Classifier (:class:`~qiskit_machine_learning.algorithms.VQC`) +and the Variational Quantum Regressor (:class:`~qiskit_machine_learning.algorithms.VQR`) +take a *feature map* and an *ansatz* to construct the underlying QNN automatically using high-level syntax. + +Integration with PyTorch +------------------------ + +The :class:`~qiskit_machine_learning.connectors.TorchConnector` +integrates QNNs with `PyTorch `_. +Thanks to the gradient algorithms in Qiskit Machine Learning, this includes automatic differentiation. +The overall gradients computed by PyTorch during the backpropagation take into account quantum neural +networks, too. The flexible design also allows the building of connectors to other packages in the future. + diff --git a/pyproject.toml b/pyproject.toml index f9728a2be..dd917cc01 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,4 +4,76 @@ build-backend = "setuptools.build_meta" [tool.black] line-length = 100 -target-version = ['py38', 'py39', 'py310', 'py311'] + +target-version = ['py39', 'py310', 'py311', 'py312'] + +[tool.pylint.main] +extension-pkg-allow-list = [ + "numpy", + "rustworkx", +] +load-plugins = ["pylint.extensions.docparams", "pylint.extensions.docstyle"] +py-version = "3.9" # update it when bumping minimum supported python version + +[tool.pylint.basic] +good-names = ["a", "b", "i", "j", "k", "d", "n", "m", "ex", "v", "w", "x", "y", "z", "Run", "_", "logger", "q", "c", "r", "qr", "cr", "qc", "nd", "pi", "op", "b", "ar", "br", "p", "cp", "ax", "dt", "__unittest", "iSwapGate", "mu"] +method-rgx = "(([a-z_][a-z0-9_]{2,49})|(assert[A-Z][a-zA-Z0-9]{2,43})|(test_[_a-zA-Z0-9]{2,}))$" +variable-rgx = "[a-z_][a-z0-9_]{1,30}$" + +[tool.pylint.format] +max-line-length = 105 # default 100 + +[tool.pylint."messages control"] +disable = [ +# intentionally disabled: + "spelling", # too noisy + "fixme", # disabled as TODOs would show up as warnings + "protected-access", # disabled as we don't follow the public vs private convention strictly + "duplicate-code", # disabled as it is too verbose + "redundant-returns-doc", # for @abstractmethod, it cannot interpret "pass" + "too-many-lines", "too-many-branches", "too-many-locals", "too-many-nested-blocks", "too-many-statements", + "too-many-instance-attributes", "too-many-arguments", "too-many-public-methods", "too-few-public-methods", "too-many-ancestors", + "unnecessary-pass", # allow for methods with just "pass", for clarity + "no-else-return", # relax "elif" after a clause with a return + "docstring-first-line-empty", # relax docstring style + "import-outside-toplevel", "import-error", # overzealous with our optionals/dynamic packages +# TODO(#9614): these were added in modern Pylint. Decide if we want to enable them. If so, +# remove from here and fix the issues. Else, move it above this section and add a comment +# with the rationale + "arguments-renamed", + "broad-exception-raised", + "consider-iterating-dictionary", + "consider-using-dict-items", + "consider-using-enumerate", + "consider-using-f-string", + "modified-iterating-list", + "nested-min-max", + "no-member", + "no-name-in-module", + "no-value-for-parameter", + "non-ascii-name", + "not-context-manager", + "superfluous-parens", + "unknown-option-value", + "unexpected-keyword-arg", + "unnecessary-dict-index-lookup", + "unnecessary-direct-lambda-call", + "unnecessary-dunder-call", + "unnecessary-ellipsis", + "unnecessary-lambda-assignment", + "unnecessary-list-index-lookup", + "unspecified-encoding", + "unsupported-assignment-operation", + "use-dict-literal", + "use-list-literal", + "use-implicit-booleaness-not-comparison", + "use-maxsplit-arg", +] + +enable = [ + "use-symbolic-message-instead" +] + +[tool.pylint.spelling] +spelling-private-dict-file = ".pylintdict" +spelling-store-unknown-words = "n" diff --git a/qiskit_machine_learning/algorithms/classifiers/neural_network_classifier.py b/qiskit_machine_learning/algorithms/classifiers/neural_network_classifier.py index 37595caa1..76f25ad2a 100644 --- a/qiskit_machine_learning/algorithms/classifiers/neural_network_classifier.py +++ b/qiskit_machine_learning/algorithms/classifiers/neural_network_classifier.py @@ -42,6 +42,7 @@ class NeuralNetworkClassifier(TrainableModel, ClassifierMixin): See `Scikit-Learn `__ for more details. """ + # pylint: disable=too-many-positional-arguments def __init__( self, neural_network: NeuralNetwork, diff --git a/qiskit_machine_learning/algorithms/classifiers/pegasos_qsvc.py b/qiskit_machine_learning/algorithms/classifiers/pegasos_qsvc.py index cae4c0e74..1b007078f 100644 --- a/qiskit_machine_learning/algorithms/classifiers/pegasos_qsvc.py +++ b/qiskit_machine_learning/algorithms/classifiers/pegasos_qsvc.py @@ -56,6 +56,7 @@ class PegasosQSVC(ClassifierMixin, SerializableModelMixin): FITTED = 0 UNFITTED = 1 + # pylint: disable=too-many-positional-arguments # pylint: disable=invalid-name def __init__( self, diff --git a/qiskit_machine_learning/algorithms/classifiers/vqc.py b/qiskit_machine_learning/algorithms/classifiers/vqc.py index 1f3c4238b..7b1926a70 100644 --- a/qiskit_machine_learning/algorithms/classifiers/vqc.py +++ b/qiskit_machine_learning/algorithms/classifiers/vqc.py @@ -44,6 +44,7 @@ class VQC(NeuralNetworkClassifier): Multi-label classification is not supported. E.g., :math:`[[1, 1, 0], [0, 1, 1], [1, 0, 1]]`. """ + # pylint: disable=too-many-positional-arguments def __init__( self, num_qubits: int | None = None, diff --git a/qiskit_machine_learning/algorithms/inference/qbayesian.py b/qiskit_machine_learning/algorithms/inference/qbayesian.py index 9621ba5e4..164cbe1ac 100644 --- a/qiskit_machine_learning/algorithms/inference/qbayesian.py +++ b/qiskit_machine_learning/algorithms/inference/qbayesian.py @@ -15,11 +15,15 @@ import copy from typing import Tuple, Dict, Set, List + from qiskit import QuantumCircuit, ClassicalRegister from qiskit.quantum_info import Statevector -from qiskit.circuit.library import GroverOperator -from qiskit.primitives import BaseSampler, Sampler from qiskit.circuit import Qubit +from qiskit.circuit.library import GroverOperator +from qiskit.primitives import BaseSampler, Sampler, BaseSamplerV2 +from qiskit.transpiler.passmanager import BasePassManager +from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager +from qiskit.providers.fake_provider import GenericBackendV2 class QBayesian: @@ -62,7 +66,8 @@ def __init__( *, limit: int = 10, threshold: float = 0.9, - sampler: BaseSampler | None = None, + sampler: BaseSampler | BaseSamplerV2 | None = None, + pass_manager: BasePassManager | None = None, ): """ Args: @@ -83,7 +88,8 @@ def __init__( # Test valid input for qrg in circuit.qregs: if qrg.size > 1: - raise ValueError("Every register needs to be mapped to exactly one unique qubit") + raise ValueError("Every register needs to be mapped to exactly one unique qubit.") + # Initialize parameter self._circ = circuit self._limit = limit @@ -92,6 +98,11 @@ def __init__( sampler = Sampler() self._sampler = sampler + if pass_manager is None: + _backend = GenericBackendV2(num_qubits=max(circuit.num_qubits, 2)) + pass_manager = generate_preset_pass_manager(optimization_level=1, backend=_backend) + self._pass_manager = pass_manager + # Label of register mapped to its qubit self._label2qubit = {qrg.name: qrg[0] for qrg in self._circ.qregs} # Label of register mapped to its qubit index bottom up in significance @@ -139,11 +150,34 @@ def _get_grover_op(self, evidence: Dict[str, int]) -> GroverOperator: def _run_circuit(self, circuit: QuantumCircuit) -> Dict[str, float]: """Run the quantum circuit with the sampler.""" - # Sample from circuit - job = self._sampler.run(circuit) - result = job.result() - # Get the counts of quantum state results - counts = result.quasi_dists[0].nearest_probability_distribution().binary_probabilities() + counts = {} + + if isinstance(self._sampler, BaseSampler): + # Sample from circuit + job = self._sampler.run(circuit) + result = job.result() + + # Get the counts of quantum state results + counts = result.quasi_dists[0].nearest_probability_distribution().binary_probabilities() + + elif isinstance(self._sampler, BaseSamplerV2): + + # Sample from circuit + circuit_isa = self._pass_manager.run(circuit) + job = self._sampler.run([circuit_isa]) + result = job.result() + + bit_array = list(result[0].data.values())[0] + bitstring_counts = bit_array.get_counts() + + # Normalize the counts to probabilities + total_shots = result[0].metadata["shots"] + counts = {k: v / total_shots for k, v in bitstring_counts.items()} + + # Convert to quasi-probabilities + # counts = QuasiDistribution(probabilities) + # counts = {k: v for k, v in counts.items()} + return counts def __power_grover( @@ -360,12 +394,12 @@ def limit(self, limit: int): self._limit = limit @property - def sampler(self) -> BaseSampler: + def sampler(self) -> BaseSampler | BaseSamplerV2: """Returns the sampler primitive used to compute the samples.""" return self._sampler @sampler.setter - def sampler(self, sampler: BaseSampler): + def sampler(self, sampler: BaseSampler | BaseSamplerV2): """Set the sampler primitive used to compute the samples.""" self._sampler = sampler diff --git a/qiskit_machine_learning/algorithms/regressors/vqr.py b/qiskit_machine_learning/algorithms/regressors/vqr.py index a26499c87..3ece2ca2f 100644 --- a/qiskit_machine_learning/algorithms/regressors/vqr.py +++ b/qiskit_machine_learning/algorithms/regressors/vqr.py @@ -29,6 +29,7 @@ class VQR(NeuralNetworkRegressor): """A convenient Variational Quantum Regressor implementation.""" + # pylint: disable=too-many-positional-arguments def __init__( self, num_qubits: int | None = None, diff --git a/qiskit_machine_learning/algorithms/trainable_model.py b/qiskit_machine_learning/algorithms/trainable_model.py index efd7b1796..31af78056 100644 --- a/qiskit_machine_learning/algorithms/trainable_model.py +++ b/qiskit_machine_learning/algorithms/trainable_model.py @@ -34,6 +34,7 @@ class TrainableModel(SerializableModelMixin): """Base class for ML model that defines a scikit-learn like interface for Estimators.""" + # pylint: disable=too-many-positional-arguments def __init__( self, neural_network: NeuralNetwork, diff --git a/qiskit_machine_learning/datasets/ad_hoc.py b/qiskit_machine_learning/datasets/ad_hoc.py index f553f74f4..ee66570d4 100644 --- a/qiskit_machine_learning/datasets/ad_hoc.py +++ b/qiskit_machine_learning/datasets/ad_hoc.py @@ -26,6 +26,7 @@ from ..utils import algorithm_globals +# pylint: disable=too-many-positional-arguments def ad_hoc_data( training_size: int, test_size: int, diff --git a/qiskit_machine_learning/gradients/base/base_sampler_gradient.py b/qiskit_machine_learning/gradients/base/base_sampler_gradient.py index 9e29b47ab..eaee27945 100644 --- a/qiskit_machine_learning/gradients/base/base_sampler_gradient.py +++ b/qiskit_machine_learning/gradients/base/base_sampler_gradient.py @@ -26,6 +26,7 @@ from qiskit.primitives.utils import _circuit_key from qiskit.providers import Options from qiskit.transpiler.passes import TranslateParameterizedGates +from qiskit.transpiler.passmanager import BasePassManager from .sampler_gradient_result import SamplerGradientResult from ..utils import ( @@ -41,7 +42,13 @@ class BaseSamplerGradient(ABC): """Base class for a ``SamplerGradient`` to compute the gradients of the sampling probability.""" - def __init__(self, sampler: BaseSampler, options: Options | None = None): + def __init__( + self, + sampler: BaseSampler, + len_quasi_dist: int | None = None, + options: Options | None = None, + pass_manager: BasePassManager | None = None, + ): """ Args: sampler: The sampler used to compute the gradients. @@ -52,6 +59,8 @@ def __init__(self, sampler: BaseSampler, options: Options | None = None): """ self._sampler: BaseSampler = sampler self._default_options = Options() + self._pass_manager = pass_manager + self._len_quasi_dist = len_quasi_dist if options is not None: self._default_options.update_options(**options) self._gradient_circuit_cache: dict[tuple, GradientCircuit] = {} diff --git a/qiskit_machine_learning/gradients/param_shift/param_shift_sampler_gradient.py b/qiskit_machine_learning/gradients/param_shift/param_shift_sampler_gradient.py index 0d7f384a8..e376755fa 100644 --- a/qiskit_machine_learning/gradients/param_shift/param_shift_sampler_gradient.py +++ b/qiskit_machine_learning/gradients/param_shift/param_shift_sampler_gradient.py @@ -20,11 +20,15 @@ from qiskit.circuit import Parameter, QuantumCircuit +from qiskit.primitives import BaseSamplerV1 +from qiskit.primitives.base import BaseSamplerV2 +from qiskit.result import QuasiDistribution + from ..base.base_sampler_gradient import BaseSamplerGradient from ..base.sampler_gradient_result import SamplerGradientResult from ..utils import _make_param_shift_parameter_values -from ...exceptions import AlgorithmError +from ...exceptions import AlgorithmError, QiskitMachineLearningError class ParamShiftSamplerGradient(BaseSamplerGradient): @@ -91,18 +95,52 @@ def _run_unique( all_n.append(n) # Run the single job with all circuits. - job = self._sampler.run(job_circuits, job_param_values, **options) + if isinstance(self._sampler, BaseSamplerV1): + job = self._sampler.run(job_circuits, job_param_values, **options) + elif isinstance(self._sampler, BaseSamplerV2): + if self._pass_manager is None: + raise QiskitMachineLearningError( + "To use ParameterShifSamplerGradient with SamplerV2 you " + + "must pass a gradient with a pass manager" + ) + isa_g_circs = self._pass_manager.run(job_circuits) + circ_params = [ + (isa_g_circs[i], job_param_values[i]) for i in range(len(job_param_values)) + ] + job = self._sampler.run(circ_params) + else: + raise AlgorithmError( + "The accepted estimators are BaseSamplerV1 (deprecated) and BaseSamplerV2; got " + + f"{type(self._sampler)} instead." + ) + try: results = job.result() except Exception as exc: - raise AlgorithmError("Estimator job failed.") from exc + raise AlgorithmError("Sampler job failed.") from exc # Compute the gradients. gradients = [] partial_sum_n = 0 + opt = None # Required by PyLint: possibly-used-before-assignment for n in all_n: gradient = [] - result = results.quasi_dists[partial_sum_n : partial_sum_n + n] + + if isinstance(self._sampler, BaseSamplerV1): + result = results.quasi_dists[partial_sum_n : partial_sum_n + n] + opt = self._get_local_options(options) + elif isinstance(self._sampler, BaseSamplerV2): + result = [] + for i in range(partial_sum_n, partial_sum_n + n): + bitstring_counts = results[i].data.meas.get_counts() + # Normalize the counts to probabilities + total_shots = sum(bitstring_counts.values()) + probabilities = {k: v / total_shots for k, v in bitstring_counts.items()} + # Convert to quasi-probabilities + counts = QuasiDistribution(probabilities) + result.append({k: v for k, v in counts.items() if int(k) < self.len_quasi_dist}) + opt = options + for dist_plus, dist_minus in zip(result[: n // 2], result[n // 2 :]): grad_dist: dict[int, float] = defaultdict(float) for key, val in dist_plus.items(): @@ -113,5 +151,4 @@ def _run_unique( gradients.append(gradient) partial_sum_n += n - opt = self._get_local_options(options) return SamplerGradientResult(gradients=gradients, metadata=metadata, options=opt) diff --git a/qiskit_machine_learning/gradients/spsa/spsa_estimator_gradient.py b/qiskit_machine_learning/gradients/spsa/spsa_estimator_gradient.py index 1f9bfa0b2..c0387a201 100644 --- a/qiskit_machine_learning/gradients/spsa/spsa_estimator_gradient.py +++ b/qiskit_machine_learning/gradients/spsa/spsa_estimator_gradient.py @@ -40,6 +40,7 @@ class SPSAEstimatorGradient(BaseEstimatorGradient): `doi: 10.1109/TAC.2000.880982 `_ """ + # pylint: disable=too-many-positional-arguments def __init__( self, estimator: BaseEstimator, diff --git a/qiskit_machine_learning/gradients/spsa/spsa_sampler_gradient.py b/qiskit_machine_learning/gradients/spsa/spsa_sampler_gradient.py index c3de7c4da..1c25b8aaa 100644 --- a/qiskit_machine_learning/gradients/spsa/spsa_sampler_gradient.py +++ b/qiskit_machine_learning/gradients/spsa/spsa_sampler_gradient.py @@ -40,6 +40,7 @@ class SPSASamplerGradient(BaseSamplerGradient): `doi: 10.1109/TAC.2000.880982 `_. """ + # pylint: disable=too-many-positional-arguments def __init__( self, sampler: BaseSampler, diff --git a/qiskit_machine_learning/kernels/fidelity_quantum_kernel.py b/qiskit_machine_learning/kernels/fidelity_quantum_kernel.py index e9769addc..212c32acd 100644 --- a/qiskit_machine_learning/kernels/fidelity_quantum_kernel.py +++ b/qiskit_machine_learning/kernels/fidelity_quantum_kernel.py @@ -256,6 +256,7 @@ def _get_kernel_entries( kernel_entries.extend(job.result().fidelities) return kernel_entries + # pylint: disable=too-many-positional-arguments def _is_trivial( self, i: int, j: int, x_i: np.ndarray, y_j: np.ndarray, symmetric: bool ) -> bool: diff --git a/qiskit_machine_learning/neural_networks/neural_network.py b/qiskit_machine_learning/neural_networks/neural_network.py index 651abf4ae..e75858d38 100644 --- a/qiskit_machine_learning/neural_networks/neural_network.py +++ b/qiskit_machine_learning/neural_networks/neural_network.py @@ -42,6 +42,7 @@ class NeuralNetwork(ABC): batched inputs. This is to be implemented by other (quantum) neural networks. """ + # pylint: disable=too-many-positional-arguments def __init__( self, num_inputs: int, diff --git a/qiskit_machine_learning/neural_networks/sampler_qnn.py b/qiskit_machine_learning/neural_networks/sampler_qnn.py index 6982d2e87..28fee16d8 100644 --- a/qiskit_machine_learning/neural_networks/sampler_qnn.py +++ b/qiskit_machine_learning/neural_networks/sampler_qnn.py @@ -14,17 +14,19 @@ from __future__ import annotations import logging - from numbers import Integral from typing import Callable, cast, Iterable, Sequence - import numpy as np +from qiskit.primitives import BaseSamplerV1 +from qiskit.primitives.base import BaseSamplerV2 + from qiskit.circuit import Parameter, QuantumCircuit from qiskit.primitives import BaseSampler, SamplerResult, Sampler +from qiskit.result import QuasiDistribution import qiskit_machine_learning.optionals as _optionals -from .neural_network import NeuralNetwork + from ..gradients import ( BaseSamplerGradient, ParamShiftSamplerGradient, @@ -33,6 +35,7 @@ from ..circuit.library import QNNCircuit from ..exceptions import QiskitMachineLearningError +from .neural_network import NeuralNetwork if _optionals.HAS_SPARSE: # pylint: disable=import-error @@ -128,6 +131,7 @@ def __init__( self, *, circuit: QuantumCircuit, + num_virtual_qubits: int | None = None, sampler: BaseSampler | None = None, input_params: Sequence[Parameter] | None = None, weight_params: Sequence[Parameter] | None = None, @@ -138,50 +142,45 @@ def __init__( input_gradients: bool = False, ): """ - Args: - sampler: The sampler primitive used to compute the neural network's results. - If ``None`` is given, a default instance of the reference sampler defined - by :class:`~qiskit.primitives.Sampler` will be used. - circuit: The parametrized quantum circuit that generates the samples of this network. - If a :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` is passed, the - `input_params` and `weight_params` do not have to be provided, because these two - properties are taken from the - :class:`~qiskit_machine_learning.circuit.library.QNNCircuit`. - input_params: The parameters of the circuit corresponding to the input. If a - :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` is provided the - `input_params` value here is ignored. Instead the value is taken from the - :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` input_parameters. - weight_params: The parameters of the circuit corresponding to the trainable weights. If - a :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` is provided the - `weight_params` value here is ignored. Instead the value is taken from the - :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` weight_parameters. - sparse: Returns whether the output is sparse or not. - interpret: A callable that maps the measured integer to another unsigned integer or - tuple of unsigned integers. These are used as new indices for the (potentially - sparse) output array. If no interpret function is - passed, then an identity function will be used by this neural network. - output_shape: The output shape of the custom interpretation. It is ignored if no custom - interpret method is provided where the shape is taken to be - ``2^circuit.num_qubits``. - gradient: An optional sampler gradient to be used for the backward pass. - If ``None`` is given, a default instance of - :class:`~qiskit_machine_learning.gradients.ParamShiftSamplerGradient` will be used. - input_gradients: Determines whether to compute gradients with respect to input data. - Note that this parameter is ``False`` by default, and must be explicitly set to - ``True`` for a proper gradient computation when using - :class:`~qiskit_machine_learning.connectors.TorchConnector`. - Raises: - QiskitMachineLearningError: Invalid parameter values. + Args: sampler: The sampler primitive used to compute the neural network's results. If + ``None`` is given, a default instance of the reference sampler defined by + :class:`~qiskit.primitives.Sampler` will be used. circuit: The parametrized quantum + circuit that generates the samples of this network. If a + :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` is passed, + the `input_params` and `weight_params` do not have to be provided, because these two + properties are taken from the :class:`~qiskit_machine_learning.circuit.library.QNNCircuit + `. input_params: The parameters of the circuit corresponding to the input. If a + :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` is provided the + `input_params` value here is ignored. Instead, the value is taken from the + :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` input_parameters. + weight_params: The parameters of the circuit corresponding to the trainable weights. If a + :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` is provided the + `weight_params` value here is ignored. Instead, the value is taken from the + :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` weight_parameters. sparse: + Returns whether the output is sparse or not. interpret: A callable that maps the measured + integer to another unsigned integer or tuple of unsigned integers. These are used as new + indices for the (potentially sparse) output array. If no interpret function is passed, + then an identity function will be used by this neural network. output_shape: The output + shape of the custom interpretation. For SamplerV1, it is ignored if no custom interpret + method is provided where the shape is taken to be ``2^circuit.num_qubits``. gradient: An + optional sampler gradient to be used for the backward pass. If ``None`` is given, + a default instance of + :class:`~qiskit_machine_learning.gradients.ParamShiftSamplerGradient` will be used. + input_gradients: Determines whether to compute gradients with respect to input data. Note + that this parameter is ``False`` by default, and must be explicitly set to ``True`` for a + proper gradient computation when using + :class:`~qiskit_machine_learning.connectors.TorchConnector`. Raises: + QiskitMachineLearningError: Invalid parameter values. """ # set primitive, provide default if sampler is None: sampler = Sampler() self.sampler = sampler - # set gradient - if gradient is None: - gradient = ParamShiftSamplerGradient(self.sampler) - self.gradient = gradient + if num_virtual_qubits is None: + # print statement + num_virtual_qubits = circuit.num_qubits + self.num_virtual_qubits = num_virtual_qubits self._org_circuit = circuit @@ -196,6 +195,12 @@ def __init__( _optionals.HAS_SPARSE.require_now("DOK") self.set_interpret(interpret, output_shape) + + # set gradient + if gradient is None: + gradient = ParamShiftSamplerGradient(sampler=self.sampler) + self.gradient = gradient + self._input_gradients = input_gradients super().__init__( @@ -276,10 +281,9 @@ def _compute_output_shape( # Warn user that output_shape parameter will be ignored logger.warning( "No interpret function given, output_shape will be automatically " - "determined as 2^num_qubits." + "determined as 2^num_virtual_qubits." ) - output_shape_ = (2**self.circuit.num_qubits,) - + output_shape_ = (2**self.num_virtual_qubits,) return output_shape_ def _postprocess(self, num_samples: int, result: SamplerResult) -> np.ndarray | SparseArray: @@ -296,8 +300,24 @@ def _postprocess(self, num_samples: int, result: SamplerResult) -> np.ndarray | prob = np.zeros((num_samples, *self._output_shape)) for i in range(num_samples): - counts = result.quasi_dists[i] + if isinstance(self.sampler, BaseSamplerV1): + counts = result.quasi_dists[i] + elif isinstance(self.sampler, BaseSamplerV2): + bitstring_counts = result[i].data.meas.get_counts() + + # Normalize the counts to probabilities + total_shots = sum(bitstring_counts.values()) + probabilities = {k: v / total_shots for k, v in bitstring_counts.items()} + + # Convert to quasi-probabilities + counts = QuasiDistribution(probabilities) + counts = {k: v for k, v in counts.items() if int(k) < 2**self.num_virtual_qubits} + else: + raise QiskitMachineLearningError( + "The accepted estimators are BaseSamplerV1 (deprecated) and BaseSamplerV2; " + + f"got {type(self.sampler)} instead." + ) # evaluate probabilities for b, v in counts.items(): key = self._interpret(b) @@ -329,6 +349,7 @@ def _postprocess_gradient( ) weights_grad = DOK((num_samples, *self._output_shape, self._num_weights)) else: + input_grad = ( np.zeros((num_samples, *self._output_shape, self._num_inputs)) if self._input_gradients @@ -387,14 +408,22 @@ def _forward( """ parameter_values, num_samples = self._preprocess_forward(input_data, weights) - # sampler allows batching - job = self.sampler.run([self._circuit] * num_samples, parameter_values) + if isinstance(self.sampler, BaseSamplerV1): + job = self.sampler.run([self._circuit] * num_samples, parameter_values) + elif isinstance(self.sampler, BaseSamplerV2): + job = self.sampler.run( + [(self._circuit, parameter_values[i]) for i in range(num_samples)] + ) + else: + raise QiskitMachineLearningError( + "The accepted estimators are BaseSamplerV1 (deprecated) and BaseSamplerV2; " + + f"got {type(self.sampler)} instead." + ) try: results = job.result() except Exception as exc: - raise QiskitMachineLearningError("Sampler job failed.") from exc + raise QiskitMachineLearningError(f"Sampler job failed: {exc}") from exc result = self._postprocess(num_samples, results) - return result def _backward( @@ -410,21 +439,18 @@ def _backward( if np.prod(parameter_values.shape) > 0: circuits = [self._circuit] * num_samples - job = None if self._input_gradients: - job = self.gradient.run(circuits, parameter_values) # type: ignore[arg-type] + job = self.gradient.run(circuits, parameter_values) elif len(parameter_values[0]) > self._num_inputs: params = [self._circuit.parameters[self._num_inputs :]] * num_samples - job = self.gradient.run( - circuits, parameter_values, parameters=params # type: ignore[arg-type] - ) + job = self.gradient.run(circuits, parameter_values, parameters=params) if job is not None: try: results = job.result() except Exception as exc: - raise QiskitMachineLearningError("Sampler job failed.") from exc + raise QiskitMachineLearningError(f"Sampler job failed: {exc}") from exc input_grad, weights_grad = self._postprocess_gradient(num_samples, results) diff --git a/qiskit_machine_learning/optimizers/adam_amsgrad.py b/qiskit_machine_learning/optimizers/adam_amsgrad.py index 74e332c0b..fe0aeb910 100644 --- a/qiskit_machine_learning/optimizers/adam_amsgrad.py +++ b/qiskit_machine_learning/optimizers/adam_amsgrad.py @@ -57,6 +57,7 @@ class ADAM(Optimizer): "snapshot_dir", ] + # pylint: disable=too-many-positional-arguments def __init__( self, maxiter: int = 10000, diff --git a/qiskit_machine_learning/optimizers/aqgd.py b/qiskit_machine_learning/optimizers/aqgd.py index ef5d0d703..4de3fdfd7 100644 --- a/qiskit_machine_learning/optimizers/aqgd.py +++ b/qiskit_machine_learning/optimizers/aqgd.py @@ -49,6 +49,7 @@ class AQGD(Optimizer): _OPTIONS = ["maxiter", "eta", "tol", "disp", "momentum", "param_tol", "averaging"] + # pylint: disable=too-many-positional-arguments def __init__( self, maxiter: int | list[int] = 1000, @@ -179,6 +180,7 @@ def _compute_objective_fn_and_gradient( gradient = 0.5 * (values[1 : num_params + 1] - values[1 + num_params :]) return obj_value, gradient + # pylint: disable=too-many-positional-arguments def _update( self, params: np.ndarray, diff --git a/qiskit_machine_learning/optimizers/cg.py b/qiskit_machine_learning/optimizers/cg.py index bb060389a..f7005f36f 100644 --- a/qiskit_machine_learning/optimizers/cg.py +++ b/qiskit_machine_learning/optimizers/cg.py @@ -33,6 +33,7 @@ class CG(SciPyOptimizer): _OPTIONS = ["maxiter", "disp", "gtol", "eps"] + # pylint: disable=too-many-positional-arguments # pylint: disable=unused-argument def __init__( self, diff --git a/qiskit_machine_learning/optimizers/cobyla.py b/qiskit_machine_learning/optimizers/cobyla.py index d7710b1e3..eef13ab55 100644 --- a/qiskit_machine_learning/optimizers/cobyla.py +++ b/qiskit_machine_learning/optimizers/cobyla.py @@ -31,6 +31,7 @@ class COBYLA(SciPyOptimizer): _OPTIONS = ["maxiter", "disp", "rhobeg"] + # pylint: disable=too-many-positional-arguments # pylint: disable=unused-argument def __init__( self, diff --git a/qiskit_machine_learning/optimizers/gradient_descent.py b/qiskit_machine_learning/optimizers/gradient_descent.py index e33aacec0..832e44b47 100644 --- a/qiskit_machine_learning/optimizers/gradient_descent.py +++ b/qiskit_machine_learning/optimizers/gradient_descent.py @@ -174,6 +174,7 @@ def grad(x): """ + # pylint: disable=too-many-positional-arguments def __init__( self, maxiter: int = 100, diff --git a/qiskit_machine_learning/optimizers/gsls.py b/qiskit_machine_learning/optimizers/gsls.py index 6f2a36e30..7f7ab2966 100644 --- a/qiskit_machine_learning/optimizers/gsls.py +++ b/qiskit_machine_learning/optimizers/gsls.py @@ -51,6 +51,7 @@ class GSLS(Optimizer): "max_failed_rejection_sampling", ] + # pylint: disable=too-many-positional-arguments # pylint: disable=unused-argument def __init__( self, @@ -131,6 +132,7 @@ def minimize( return result + # pylint: disable=too-many-positional-arguments def ls_optimize( self, n: int, @@ -269,6 +271,7 @@ def sample_points( return points, directions + # pylint: disable=too-many-positional-arguments def sample_set( self, n: int, x: np.ndarray, var_lb: np.ndarray, var_ub: np.ndarray, num_points: int ) -> tuple[np.ndarray, np.ndarray]: diff --git a/qiskit_machine_learning/optimizers/l_bfgs_b.py b/qiskit_machine_learning/optimizers/l_bfgs_b.py index 0560e454d..4e355a6c7 100644 --- a/qiskit_machine_learning/optimizers/l_bfgs_b.py +++ b/qiskit_machine_learning/optimizers/l_bfgs_b.py @@ -46,6 +46,7 @@ class L_BFGS_B(SciPyOptimizer): # pylint: disable=invalid-name _OPTIONS = ["maxfun", "maxiter", "ftol", "iprint", "eps"] + # pylint: disable=too-many-positional-arguments # pylint: disable=unused-argument def __init__( self, diff --git a/qiskit_machine_learning/optimizers/nelder_mead.py b/qiskit_machine_learning/optimizers/nelder_mead.py index 8109b3f48..8fabce5eb 100644 --- a/qiskit_machine_learning/optimizers/nelder_mead.py +++ b/qiskit_machine_learning/optimizers/nelder_mead.py @@ -40,6 +40,7 @@ class NELDER_MEAD(SciPyOptimizer): # pylint: disable=invalid-name _OPTIONS = ["maxiter", "maxfev", "disp", "xatol", "adaptive"] + # pylint: disable=too-many-positional-arguments # pylint: disable=unused-argument def __init__( self, diff --git a/qiskit_machine_learning/optimizers/nft.py b/qiskit_machine_learning/optimizers/nft.py index b76bfc983..5dffc47c5 100644 --- a/qiskit_machine_learning/optimizers/nft.py +++ b/qiskit_machine_learning/optimizers/nft.py @@ -29,6 +29,7 @@ class NFT(SciPyOptimizer): _OPTIONS = ["maxiter", "maxfev", "disp", "reset_interval"] + # pylint: disable=too-many-positional-arguments # pylint: disable=unused-argument def __init__( self, @@ -69,6 +70,7 @@ def __init__( super().__init__(method=nakanishi_fujii_todo, options=options, **kwargs) +# pylint: disable=too-many-positional-arguments # pylint: disable=invalid-name def nakanishi_fujii_todo( fun, x0, args=(), maxiter=None, maxfev=1024, reset_interval=32, eps=1e-32, callback=None, **_ diff --git a/qiskit_machine_learning/optimizers/p_bfgs.py b/qiskit_machine_learning/optimizers/p_bfgs.py index c70d7697d..32287c4df 100644 --- a/qiskit_machine_learning/optimizers/p_bfgs.py +++ b/qiskit_machine_learning/optimizers/p_bfgs.py @@ -52,6 +52,7 @@ class P_BFGS(SciPyOptimizer): # pylint: disable=invalid-name _OPTIONS = ["maxfun", "ftol", "iprint"] + # pylint: disable=too-many-positional-arguments # pylint: disable=unused-argument def __init__( self, diff --git a/qiskit_machine_learning/optimizers/powell.py b/qiskit_machine_learning/optimizers/powell.py index 96842db36..e2e875a6e 100644 --- a/qiskit_machine_learning/optimizers/powell.py +++ b/qiskit_machine_learning/optimizers/powell.py @@ -33,6 +33,7 @@ class POWELL(SciPyOptimizer): _OPTIONS = ["maxiter", "maxfev", "disp", "xtol"] + # pylint: disable=too-many-positional-arguments # pylint: disable=unused-argument def __init__( self, diff --git a/qiskit_machine_learning/optimizers/qnspsa.py b/qiskit_machine_learning/optimizers/qnspsa.py index 408bcd8a4..3d2b91381 100644 --- a/qiskit_machine_learning/optimizers/qnspsa.py +++ b/qiskit_machine_learning/optimizers/qnspsa.py @@ -93,6 +93,7 @@ def loss(x): """ + # pylint: disable=too-many-positional-arguments def __init__( self, fidelity: FIDELITY, @@ -184,6 +185,7 @@ def __init__( self.fidelity = fidelity + # pylint: disable=too-many-positional-arguments def _point_sample(self, loss, x, eps, delta1, delta2): loss_points = [x + eps * delta1, x - eps * delta1] fidelity_points = [ diff --git a/qiskit_machine_learning/optimizers/slsqp.py b/qiskit_machine_learning/optimizers/slsqp.py index facbfdbce..2a32c7c08 100644 --- a/qiskit_machine_learning/optimizers/slsqp.py +++ b/qiskit_machine_learning/optimizers/slsqp.py @@ -36,6 +36,7 @@ class SLSQP(SciPyOptimizer): _OPTIONS = ["maxiter", "disp", "ftol", "eps"] + # pylint: disable=too-many-positional-arguments # pylint: disable=unused-argument def __init__( self, diff --git a/qiskit_machine_learning/optimizers/spsa.py b/qiskit_machine_learning/optimizers/spsa.py index c6579811e..03ed45017 100644 --- a/qiskit_machine_learning/optimizers/spsa.py +++ b/qiskit_machine_learning/optimizers/spsa.py @@ -161,6 +161,7 @@ def __call__(self, nfev, parameters, value, stepsize, accepted) -> bool: """ + # pylint: disable=too-many-positional-arguments def __init__( self, maxiter: int = 100, @@ -280,6 +281,7 @@ def __init__( self._nfev: int | None = None # the number of function evaluations self._smoothed_hessian: np.ndarray | None = None # smoothed average of the Hessians + # pylint: disable=too-many-positional-arguments @staticmethod def calibrate( loss: Callable[[np.ndarray], float], @@ -413,6 +415,7 @@ def settings(self) -> dict[str, Any]: "termination_checker": self.termination_checker, } + # pylint: disable=too-many-positional-arguments def _point_sample(self, loss, x, eps, delta1, delta2): """A single sample of the gradient at position ``x`` in direction ``delta``.""" # points to evaluate @@ -478,6 +481,7 @@ def _point_estimate(self, loss, x, eps, num_samples): hessian_estimate / num_samples, ) + # pylint: disable=too-many-positional-arguments def _compute_update(self, loss, x, k, eps, lse_solver): # compute the perturbations if isinstance(self.resamplings, dict): diff --git a/qiskit_machine_learning/optimizers/tnc.py b/qiskit_machine_learning/optimizers/tnc.py index 13d50d29b..b16af35a3 100644 --- a/qiskit_machine_learning/optimizers/tnc.py +++ b/qiskit_machine_learning/optimizers/tnc.py @@ -33,6 +33,7 @@ class TNC(SciPyOptimizer): _OPTIONS = ["maxiter", "disp", "accuracy", "ftol", "xtol", "gtol", "eps"] + # pylint: disable=too-many-positional-arguments # pylint: disable=unused-argument def __init__( self, diff --git a/qiskit_machine_learning/state_fidelities/compute_uncompute.py b/qiskit_machine_learning/state_fidelities/compute_uncompute.py index 3453b2081..a1f745f67 100644 --- a/qiskit_machine_learning/state_fidelities/compute_uncompute.py +++ b/qiskit_machine_learning/state_fidelities/compute_uncompute.py @@ -18,11 +18,17 @@ from copy import copy from qiskit import QuantumCircuit -from qiskit.primitives import BaseSampler + +from qiskit.primitives import BaseSampler, BaseSamplerV1, SamplerResult, StatevectorSampler +from qiskit.primitives.base import BaseSamplerV2 + +from qiskit.transpiler.passmanager import PassManager +from qiskit.result import QuasiDistribution + from qiskit.primitives.primitive_job import PrimitiveJob from qiskit.providers import Options -from ..exceptions import AlgorithmError +from ..exceptions import AlgorithmError, QiskitMachineLearningError from .base_state_fidelity import BaseStateFidelity from .state_fidelity_result import StateFidelityResult from ..algorithm_job import AlgorithmJob @@ -53,7 +59,10 @@ class ComputeUncompute(BaseStateFidelity): def __init__( self, - sampler: BaseSampler, + sampler: BaseSampler | BaseSamplerV2, + *, + num_virtual_qubits: int | None = None, + pass_manager: PassManager | None = None, options: Options | None = None, local: bool = False, ) -> None: @@ -79,11 +88,24 @@ def __init__( Raises: ValueError: If the sampler is not an instance of ``BaseSampler``. """ - if not isinstance(sampler, BaseSampler): + if (not isinstance(sampler, BaseSampler)) and (not isinstance(sampler, BaseSamplerV2)): + raise ValueError( + f"The sampler should be an instance of BaseSampler or BaseSamplerV2, " + f"but got {type(sampler)}" + ) + if ( + isinstance(sampler, BaseSamplerV2) + and (pass_manager is None) + and not isinstance(sampler, StatevectorSampler) + ): + raise ValueError(f"A pass_manager should be provided for {type(sampler)}.") + if (pass_manager is not None) and (num_virtual_qubits is None): raise ValueError( - f"The sampler should be an instance of BaseSampler, " f"but got {type(sampler)}" + f"Number of virtual qubits should be provided for {type(pass_manager)}." ) self._sampler: BaseSampler = sampler + self.num_virtual_qubits = num_virtual_qubits + self.pass_manager = pass_manager self._local = local self._default_options = Options() if options is not None: @@ -111,6 +133,8 @@ def create_fidelity_circuit( circuit = circuit_1.compose(circuit_2.inverse()) circuit.measure_all() + if self.pass_manager is not None: + circuit = self.pass_manager.run(circuit) return circuit def _run( @@ -156,29 +180,60 @@ def _run( # primitive's default options. opts = copy(self._default_options) opts.update_options(**options) - - sampler_job = self._sampler.run(circuits=circuits, parameter_values=values, **opts.__dict__) - - local_opts = self._get_local_options(opts.__dict__) - return AlgorithmJob(ComputeUncompute._call, sampler_job, circuits, self._local, local_opts) + if isinstance(self._sampler, BaseSamplerV1): + sampler_job = self._sampler.run( + circuits=circuits, parameter_values=values, **opts.__dict__ + ) + local_opts = self._get_local_options(opts.__dict__) + elif isinstance(self._sampler, BaseSamplerV2): + sampler_job = self._sampler.run( + [(circuits[i], values[i]) for i in range(len(circuits))], **opts.__dict__ + ) + local_opts = opts.__dict__ + else: + raise QiskitMachineLearningError( + "The accepted estimators are BaseSamplerV1 (deprecated) and BaseSamplerV2; got" + + f" {type(self.sampler)} instead." + ) + return AlgorithmJob( + ComputeUncompute._call, + sampler_job, + circuits, + self._local, + local_opts, + self._sampler, + self._post_process_v2, + self.num_virtual_qubits, + ) @staticmethod def _call( - job: PrimitiveJob, circuits: Sequence[QuantumCircuit], local: bool, local_opts: Options + job: PrimitiveJob, + circuits: Sequence[QuantumCircuit], + local: bool, + local_opts: Options = None, + _sampler=None, + _post_process_v2=None, + num_virtual_qubits=None, ) -> StateFidelityResult: try: result = job.result() except Exception as exc: raise AlgorithmError("Sampler job failed!") from exc + if isinstance(_sampler, BaseSamplerV1): + quasi_dists = result.quasi_dists + elif isinstance(_sampler, BaseSamplerV2): + quasi_dists = _post_process_v2(result) + if local: raw_fidelities = [ - ComputeUncompute._get_local_fidelity(prob_dist, circuit.num_qubits) - for prob_dist, circuit in zip(result.quasi_dists, circuits) + ComputeUncompute._get_local_fidelity(prob_dist, num_virtual_qubits) + for prob_dist, circuit in zip(quasi_dists, circuits) ] else: raw_fidelities = [ - ComputeUncompute._get_global_fidelity(prob_dist) for prob_dist in result.quasi_dists + ComputeUncompute._get_global_fidelity(prob_dist) for prob_dist in quasi_dists ] fidelities = ComputeUncompute._truncate_fidelities(raw_fidelities) @@ -225,6 +280,21 @@ def _get_local_options(self, options: Options) -> Options: opts.update_options(**options) return opts + def _post_process_v2(self, result: SamplerResult): + quasis = [] + for i in range(len(result)): + bitstring_counts = result[i].data.meas.get_counts() + + # Normalize the counts to probabilities + total_shots = sum(bitstring_counts.values()) + probabilities = {k: v / total_shots for k, v in bitstring_counts.items()} + + # Convert to quasi-probabilities + counts = QuasiDistribution(probabilities) + quasi_probs = {k: v for k, v in counts.items() if int(k) < 2**self.num_virtual_qubits} + quasis.append(quasi_probs) + return quasis + @staticmethod def _get_global_fidelity(probability_distribution: dict[int, float]) -> float: """Process the probability distribution of a measurement to determine the diff --git a/releasenotes/notes/py38_end_of_support-fa1fdea6ea02b502.yaml b/releasenotes/notes/py38_end_of_support-fa1fdea6ea02b502.yaml new file mode 100644 index 000000000..f039fa74e --- /dev/null +++ b/releasenotes/notes/py38_end_of_support-fa1fdea6ea02b502.yaml @@ -0,0 +1,6 @@ +--- +upgrade: + - | + Removed support for using Qiskit Machine Learning with Python 3.8 to reflect + the EOL of Python 3.8 in October 2024 (PEP 569). To continue using Qiskit Machine Learning, you + must upgrade to a Python: 3.9 or above if you are using older versions of Python. diff --git a/setup.py b/setup.py index 0d654e889..d7caaa4be 100644 --- a/setup.py +++ b/setup.py @@ -57,7 +57,6 @@ "Operating System :: MacOS", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -68,7 +67,7 @@ packages=setuptools.find_packages(include=['qiskit_machine_learning','qiskit_machine_learning.*']), install_requires=REQUIREMENTS, include_package_data=True, - python_requires=">=3.8", + python_requires=">=3.9", extras_require={ 'torch': ["torch"], 'sparse': ["sparse"], diff --git a/test/algorithms/classifiers/test_neural_network_classifier.py b/test/algorithms/classifiers/test_neural_network_classifier.py index 30930a9fe..52fcc8271 100644 --- a/test/algorithms/classifiers/test_neural_network_classifier.py +++ b/test/algorithms/classifiers/test_neural_network_classifier.py @@ -167,6 +167,7 @@ def _generate_data(self, num_inputs: int) -> tuple[np.ndarray, np.ndarray]: return features, labels + # pylint: disable=too-many-positional-arguments def _create_classifier( self, qnn: NeuralNetwork, diff --git a/test/algorithms/classifiers/test_vqc.py b/test/algorithms/classifiers/test_vqc.py index 9d252e8c8..15beeb049 100644 --- a/test/algorithms/classifiers/test_vqc.py +++ b/test/algorithms/classifiers/test_vqc.py @@ -84,6 +84,7 @@ def setUp(self): "no_one_hot": _create_dataset(6, 2, one_hot=False), } + # pylint: disable=too-many-positional-arguments @idata(itertools.product(NUM_QUBITS_LIST, FEATURE_MAPS, ANSATZES, OPTIMIZERS, DATASETS)) @unpack def test_VQC(self, num_qubits, f_m, ans, opt, d_s): diff --git a/test/algorithms/inference/test_qbayesian.py b/test/algorithms/inference/test_qbayesian.py index d0b114b8d..5c68e17ab 100644 --- a/test/algorithms/inference/test_qbayesian.py +++ b/test/algorithms/inference/test_qbayesian.py @@ -15,11 +15,14 @@ import unittest from test import QiskitMachineLearningTestCase +import copy import numpy as np from qiskit import QuantumCircuit from qiskit.circuit import QuantumRegister from qiskit.primitives import Sampler +from qiskit.providers.fake_provider import GenericBackendV2 +from qiskit_ibm_runtime import Session, SamplerV2 from qiskit_machine_learning.utils import algorithm_globals from qiskit_machine_learning.algorithms import QBayesian @@ -208,5 +211,193 @@ def test_trivial_circuit(self): ) +class TestQBayesianInferenceV2(QiskitMachineLearningTestCase): + """Test QBayesianInference Algorithm V2""" + + backend = GenericBackendV2(num_qubits=3) + session = Session(backend=backend) + _sampler = SamplerV2(mode=session) + _sampler.options.default_shots = 2**7 + + def setUp(self): + super().setUp() + algorithm_globals.random_seed = 10598 + # Quantum Bayesian inference + qc = self._create_bayes_net() + self.qbayesian = QBayesian(qc, sampler=self._sampler) + + def _create_bayes_net(self): + # Probabilities + theta_a = 2 * np.arcsin(np.sqrt(0.25)) + theta_b_na = 2 * np.arcsin(np.sqrt(0.6)) + theta_b_a = 2 * np.arcsin(np.sqrt(0.7)) + theta_c_nbna = 2 * np.arcsin(np.sqrt(0.1)) + theta_c_nba = 2 * np.arcsin(np.sqrt(0.55)) + theta_c_bna = 2 * np.arcsin(np.sqrt(0.7)) + theta_c_ba = 2 * np.arcsin(np.sqrt(0.9)) + # Random variables + qr_a = QuantumRegister(1, name="A") + qr_b = QuantumRegister(1, name="B") + qr_c = QuantumRegister(1, name="C") + # Define a 3-qubit quantum circuit + qc = QuantumCircuit(qr_a, qr_b, qr_c, name="Bayes net") + # P(A) + qc.ry(theta_a, 0) + # P(B|-A) + qc.x(0) + qc.cry(theta_b_na, qr_a, qr_b) + qc.x(0) + # P(B|A) + qc.cry(theta_b_a, qr_a, qr_b) + # P(C|-B,-A) + qc.x(0) + qc.x(1) + qc.mcry(theta_c_nbna, [qr_a[0], qr_b[0]], qr_c[0]) + qc.x(0) + qc.x(1) + # P(C|-B,A) + qc.x(1) + qc.mcry(theta_c_nba, [qr_a[0], qr_b[0]], qr_c[0]) + qc.x(1) + # P(C|B,-A) + qc.x(0) + qc.mcry(theta_c_bna, [qr_a[0], qr_b[0]], qr_c[0]) + qc.x(0) + # P(C|B,A) + qc.mcry(theta_c_ba, [qr_a[0], qr_b[0]], qr_c[0]) + return qc + + def test_rejection_sampling(self): + """Test rejection sampling with different amount of evidence""" + test_cases = [{"A": 0, "B": 0}, {"A": 0}, {}] + true_res = [ + {"000": 0.9, "100": 0.1}, + {"000": 0.36, "100": 0.04, "010": 0.18, "110": 0.42}, + { + "000": 0.27, + "001": 0.03375, + "010": 0.135, + "011": 0.0175, + "100": 0.03, + "101": 0.04125, + "110": 0.315, + "111": 0.1575, + }, + ] + for evd, res in zip(test_cases, true_res): + samples = self.qbayesian.rejection_sampling(evidence=evd) + self.assertTrue( + np.all( + [ + np.isclose(res[sample_key], sample_val, atol=0.08) + for sample_key, sample_val in samples.items() + ] + ) + ) + + def test_rejection_sampling_format_res(self): + """Test rejection sampling with different result format""" + test_cases = [{"A": 0, "C": 1}, {"C": 1}, {}] + true_res = [ + {"P(B=0|A=0,C=1)", "P(B=1|A=0,C=1)"}, + {"P(A=0,B=0|C=1)", "P(A=1,B=0|C=1)", "P(A=0,B=1|C=1)", "P(A=1,B=1|C=1)"}, + { + "P(A=0,B=0,C=0)", + "P(A=1,B=0,C=0)", + "P(A=0,B=1,C=0)", + "P(A=1,B=1,C=0)", + "P(A=0,B=0,C=1)", + "P(A=1,B=0,C=1)", + "P(A=0,B=1,C=1)", + "P(A=1,B=1,C=1)", + }, + ] + for evd, res in zip(test_cases, true_res): + self.assertTrue( + res == set(self.qbayesian.rejection_sampling(evidence=evd, format_res=True).keys()) + ) + + def test_inference(self): + """Test inference with different amount of evidence""" + test_q_1, test_e_1 = ({"B": 1}, {"A": 1, "C": 1}) + test_q_2 = {"B": 0} + test_q_3 = {} + test_q_4, test_e_4 = ({"B": 1}, {"A": 0}) + true_res = [0.79, 0.21, 1, 0.6] + res = [] + samples = [] + # 1. Query basic inference + res.append(self.qbayesian.inference(query=test_q_1, evidence=test_e_1)) + samples.append(self.qbayesian.samples) + # 2. Query basic inference + res.append(self.qbayesian.inference(query=test_q_2)) + samples.append(self.qbayesian.samples) + # 3. Query marginalized inference + res.append(self.qbayesian.inference(query=test_q_3)) + samples.append(self.qbayesian.samples) + # 4. Query marginalized inference + res.append(self.qbayesian.inference(query=test_q_4, evidence=test_e_4)) + # Correct inference + np.testing.assert_allclose(true_res, res, atol=0.04) + # No change in samples + self.assertTrue(samples[0] == samples[1]) + + def test_parameter(self): + """Tests parameter of methods""" + # Test set threshold + self.qbayesian.threshold = 0.9 + self.qbayesian.rejection_sampling(evidence={"A": 1}) + self.assertTrue(self.qbayesian.threshold == 0.9) + # Test set limit + # Not converged + self.qbayesian.limit = 0 + self.qbayesian.rejection_sampling(evidence={"B": 1}) + self.assertFalse(self.qbayesian.converged) + self.assertTrue(self.qbayesian.limit == 0) + # Converged + self.qbayesian.limit = 1 + self.qbayesian.rejection_sampling(evidence={"B": 1}) + self.assertTrue(self.qbayesian.converged) + self.assertTrue(self.qbayesian.limit == 1) + # Test sampler + sampler = copy.deepcopy(self._sampler) + self.qbayesian.sampler = sampler + self.qbayesian.inference(query={"B": 1}, evidence={"A": 0, "C": 0}) + self.assertTrue(self.qbayesian.sampler == sampler) + # Create a quantum circuit with a register that has more than one qubit + with self.assertRaises(ValueError, msg="No ValueError in constructor with invalid input."): + QBayesian(QuantumCircuit(QuantumRegister(2, "qr"))) + # Test invalid inference without evidence or generated samples + with self.assertRaises(ValueError, msg="No ValueError in inference with invalid input."): + QBayesian(QuantumCircuit(QuantumRegister(1, "qr"))).inference({"A": 0}) + + def test_trivial_circuit(self): + """Tests trivial quantum circuit""" + # Define rotation angles + theta_a = 2 * np.arcsin(np.sqrt(0.2)) + theta_b_a = 2 * np.arcsin(np.sqrt(0.9)) + theta_b_na = 2 * np.arcsin(np.sqrt(0.3)) + # Define quantum registers + qr_a = QuantumRegister(1, name="A") + qr_b = QuantumRegister(1, name="B") + # Define a 2-qubit quantum circuit + qc = QuantumCircuit(qr_a, qr_b, name="Bayes net small") + qc.ry(theta_a, 0) + qc.cry(theta_b_a, control_qubit=qr_a, target_qubit=qr_b) + qc.x(0) + qc.cry(theta_b_na, control_qubit=qr_a, target_qubit=qr_b) + qc.x(0) + # Inference + self.assertTrue( + np.all( + np.isclose( + 0.1, + QBayesian(circuit=qc).inference(query={"B": 0}, evidence={"A": 1}), + atol=0.04, + ) + ) + ) + + if __name__ == "__main__": unittest.main() diff --git a/test/connectors/test_torch.py b/test/connectors/test_torch.py index a9d4dde22..3979ed5d1 100644 --- a/test/connectors/test_torch.py +++ b/test/connectors/test_torch.py @@ -42,6 +42,7 @@ def subTest(self, msg, **kwargs): """Sub test.""" raise builtins.Exception("Abstract method") + # pylint: disable=too-many-positional-arguments @abstractmethod def assertAlmostEqual(self, first, second, places=None, msg=None, delta=None): """Assert almost equal.""" diff --git a/test/connectors/test_torch_connector.py b/test/connectors/test_torch_connector.py index 385af7803..68cb77c49 100644 --- a/test/connectors/test_torch_connector.py +++ b/test/connectors/test_torch_connector.py @@ -263,6 +263,7 @@ class ConvolutionalLayer(torch.nn.Module): stride (int, optional): Stride of the convolution. Defaults to 1. """ + # pylint: disable=too-many-positional-arguments def __init__( self, input_channel: int, diff --git a/test/kernels/test_fidelity_qkernel.py b/test/kernels/test_fidelity_qkernel.py index 51b9a5b45..d21923510 100644 --- a/test/kernels/test_fidelity_qkernel.py +++ b/test/kernels/test_fidelity_qkernel.py @@ -129,6 +129,7 @@ def test_exceptions(self): with self.assertRaises(ValueError, msg="Unsupported value of 'max_circuits_per_job'."): _ = FidelityQuantumKernel(max_circuits_per_job=-1) + # pylint: disable=too-many-positional-arguments @idata( # params, fidelity, feature map, enforce_psd, duplicate itertools.product( @@ -152,6 +153,7 @@ def test_evaluate_symmetric(self, params, fidelity, feature_map, enforce_psd, du np.testing.assert_allclose(kernel_matrix, solution, rtol=1e-4, atol=1e-10) + # pylint: disable=too-many-positional-arguments @idata( itertools.product( ["samples_1", "samples_4"], diff --git a/test/neural_networks/test_sampler_qnn.py b/test/neural_networks/test_sampler_qnn.py index 8084b5109..09c0982c0 100644 --- a/test/neural_networks/test_sampler_qnn.py +++ b/test/neural_networks/test_sampler_qnn.py @@ -23,11 +23,18 @@ from qiskit.circuit import Parameter, QuantumCircuit from qiskit.primitives import Sampler +from qiskit.providers.fake_provider import GenericBackendV2 +from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager from qiskit.circuit.library import RealAmplitudes, ZZFeatureMap -from qiskit_machine_learning.utils import algorithm_globals +from qiskit_ibm_runtime import Session, SamplerV2 + +from qiskit_machine_learning.utils import algorithm_globals from qiskit_machine_learning.circuit.library import QNNCircuit from qiskit_machine_learning.neural_networks.sampler_qnn import SamplerQNN +from qiskit_machine_learning.gradients.param_shift.param_shift_sampler_gradient import ( + ParamShiftSamplerGradient, +) import qiskit_machine_learning.optionals as _optionals if _optionals.HAS_SPARSE: @@ -45,8 +52,9 @@ class SparseArray: # type: ignore DEFAULT = "default" SHOTS = "shots" +V2 = "v2" SPARSE = [True, False] -SAMPLERS = [DEFAULT, SHOTS] +SAMPLERS = [DEFAULT, SHOTS, V2] INTERPRET_TYPES = [0, 1, 2] BATCH_SIZES = [2] INPUT_GRADS = [True, False] @@ -69,6 +77,8 @@ def setUp(self): self.qc = QuantumCircuit(num_qubits) self.qc.append(feature_map, range(2)) self.qc.append(var_form, range(2)) + self.qc.measure_all() + self.num_virtual_qubits = num_qubits # store params self.input_params = list(feature_map.parameters) @@ -93,19 +103,44 @@ def interpret_2d(x): # define sampler primitives self.sampler = Sampler() self.sampler_shots = Sampler(options={"shots": 100, "seed": 42}) + self.backend = GenericBackendV2(num_qubits=8) + self.session = Session(backend=self.backend) + self.sampler_v2 = SamplerV2(mode=self.session) self.array_type = {True: SparseArray, False: np.ndarray} + # pylint: disable=too-many-positional-arguments def _get_qnn( self, sparse, sampler_type, interpret_id, input_params, weight_params, input_grads ): """Construct QNN from configuration.""" + # get interpret setting + interpret = None + output_shape = None + if interpret_id == 1: + interpret = self.interpret_1d + output_shape = self.output_shape_1d + elif interpret_id == 2: + interpret = self.interpret_2d + output_shape = self.output_shape_2d # get quantum instance + gradient = None if sampler_type == SHOTS: sampler = self.sampler_shots elif sampler_type == DEFAULT: sampler = self.sampler + elif sampler_type == V2: + sampler = self.sampler_v2 + + if self.qc.layout is None: + self.pm = generate_preset_pass_manager(optimization_level=1, backend=self.backend) + self.qc = self.pm.run(self.qc) + gradient = ParamShiftSamplerGradient( + sampler=self.sampler, + len_quasi_dist=2**self.num_virtual_qubits, + pass_manager=self.pm, + ) else: sampler = None @@ -123,11 +158,13 @@ def _get_qnn( qnn = SamplerQNN( sampler=sampler, circuit=self.qc, + num_virtual_qubits=self.num_virtual_qubits, input_params=input_params, weight_params=weight_params, sparse=sparse, interpret=interpret, output_shape=output_shape, + gradient=gradient, input_gradients=input_grads, ) return qnn @@ -344,7 +381,7 @@ def test_no_parameters(self): sampler_qnn.input_gradients = True self._verify_qnn(sampler_qnn, 1, input_data=None, weights=None) - def test_qnn_qc_circui_construction(self): + def test_qnn_qc_circuit_construction(self): """Test Sampler QNN properties and forward/backward pass for QNNCircuit construction""" num_qubits = 2 feature_map = ZZFeatureMap(feature_dimension=num_qubits) diff --git a/test/optimizers/test_spsa.py b/test/optimizers/test_spsa.py index 80afce43e..ec3af13ee 100644 --- a/test/optimizers/test_spsa.py +++ b/test/optimizers/test_spsa.py @@ -146,6 +146,7 @@ class TerminationChecker: def __init__(self): self.values = [] + # pylint: disable=too-many-positional-arguments def __call__(self, nfev, point, fvalue, stepsize, accepted) -> bool: self.values.append(fvalue) diff --git a/test/state_fidelities/test_compute_uncompute_v2.py b/test/state_fidelities/test_compute_uncompute_v2.py new file mode 100644 index 000000000..819b206fc --- /dev/null +++ b/test/state_fidelities/test_compute_uncompute_v2.py @@ -0,0 +1,343 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2024. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Tests for Fidelity.""" + +import unittest +from test import QiskitMachineLearningTestCase + +import numpy as np + +from qiskit.circuit import QuantumCircuit, ParameterVector +from qiskit.circuit.library import RealAmplitudes +from qiskit.primitives import Sampler +from qiskit.providers.fake_provider import GenericBackendV2 +from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager + +from qiskit_ibm_runtime import Session, SamplerV2 + +from qiskit_machine_learning.state_fidelities import ComputeUncompute + + +class TestComputeUncompute(QiskitMachineLearningTestCase): + """Test Compute-Uncompute Fidelity class""" + + def setUp(self): + super().setUp() + parameters = ParameterVector("x", 2) + + rx_rotations = QuantumCircuit(2) + rx_rotations.rx(parameters[0], 0) + rx_rotations.rx(parameters[1], 1) + + ry_rotations = QuantumCircuit(2) + ry_rotations.ry(parameters[0], 0) + ry_rotations.ry(parameters[1], 1) + + plus = QuantumCircuit(2) + plus.h([0, 1]) + + zero = QuantumCircuit(2) + + rx_rotation = QuantumCircuit(2) + rx_rotation.rx(parameters[0], 0) + rx_rotation.h(1) + + self._circuit = [rx_rotations, ry_rotations, plus, zero, rx_rotation] + + self.backend = GenericBackendV2( + num_qubits=4, + calibrate_instructions=None, + pulse_channels=False, + noise_info=False, + seed=123, + ) + self.session = Session(backend=self.backend) + self._sampler = SamplerV2(mode=self.session) + self.pm = generate_preset_pass_manager(optimization_level=0, backend=self.backend) + + self._left_params = np.array([[0, 0], [np.pi / 2, 0], [0, np.pi / 2], [np.pi, np.pi]]) + self._right_params = np.array([[0, 0], [0, 0], [np.pi / 2, 0], [0, 0]]) + + def test_1param_pair(self): + """test for fidelity with one pair of parameters""" + fidelity = ComputeUncompute( + self._sampler, pass_manager=self.pm, num_virtual_qubits=self._circuit[0].num_qubits + ) + job = fidelity.run( + self._circuit[0], self._circuit[1], self._left_params[0], self._right_params[0] + ) + result = job.result() + np.testing.assert_allclose(result.fidelities, np.array([1.0])) + + def test_1param_pair_local(self): + """test for fidelity with one pair of parameters""" + fidelity = ComputeUncompute( + self._sampler, + local=True, + pass_manager=self.pm, + num_virtual_qubits=self._circuit[0].num_qubits, + ) + job = fidelity.run( + self._circuit[0], self._circuit[1], self._left_params[0], self._right_params[0] + ) + result = job.result() + np.testing.assert_allclose(result.fidelities, np.array([1.0])) + + def test_local(self): + """test difference between local and global fidelity""" + fidelity_global = ComputeUncompute( + self._sampler, + local=False, + pass_manager=self.pm, + num_virtual_qubits=self._circuit[2].num_qubits, + ) + fidelity_local = ComputeUncompute( + self._sampler, + local=True, + pass_manager=self.pm, + num_virtual_qubits=self._circuit[2].num_qubits, + ) + fidelities = [] + for fidelity in [fidelity_global, fidelity_local]: + job = fidelity.run(self._circuit[2], self._circuit[3]) + result = job.result() + fidelities.append(result.fidelities[0]) + np.testing.assert_allclose(fidelities, np.array([0.25, 0.5]), atol=1e-1, rtol=1e-1) + + def test_4param_pairs(self): + """test for fidelity with four pairs of parameters""" + fidelity = ComputeUncompute( + self._sampler, pass_manager=self.pm, num_virtual_qubits=self._circuit[0].num_qubits + ) + n = len(self._left_params) + job = fidelity.run( + [self._circuit[0]] * n, [self._circuit[1]] * n, self._left_params, self._right_params + ) + results = job.result() + np.testing.assert_allclose( + results.fidelities, np.array([1.0, 0.5, 0.25, 0.0]), atol=1e-1, rtol=1e-1 + ) + + def test_symmetry(self): + """test for fidelity with the same circuit""" + fidelity = ComputeUncompute( + self._sampler, pass_manager=self.pm, num_virtual_qubits=self._circuit[0].num_qubits + ) + n = len(self._left_params) + job_1 = fidelity.run( + [self._circuit[0]] * n, [self._circuit[0]] * n, self._left_params, self._right_params + ) + job_2 = fidelity.run( + [self._circuit[0]] * n, [self._circuit[0]] * n, self._right_params, self._left_params + ) + print(job_1) + results_1 = job_1.result() + results_2 = job_2.result() + np.testing.assert_allclose(results_1.fidelities, results_2.fidelities, atol=1e-1, rtol=1e-1) + + def test_no_params(self): + """test for fidelity without parameters""" + fidelity = ComputeUncompute( + self._sampler, pass_manager=self.pm, num_virtual_qubits=self._circuit[2].num_qubits + ) + job = fidelity.run([self._circuit[2]], [self._circuit[3]]) + results = job.result() + np.testing.assert_allclose(results.fidelities, np.array([0.25]), atol=1e-1, rtol=1e-1) + + job = fidelity.run([self._circuit[2]], [self._circuit[3]], [], []) + results = job.result() + np.testing.assert_allclose(results.fidelities, np.array([0.25]), atol=1e-1, rtol=1e-1) + + def test_left_param(self): + """test for fidelity with only left parameters""" + fidelity = ComputeUncompute( + self._sampler, pass_manager=self.pm, num_virtual_qubits=self._circuit[1].num_qubits + ) + n = len(self._left_params) + job = fidelity.run( + [self._circuit[1]] * n, [self._circuit[3]] * n, values_1=self._left_params + ) + results = job.result() + np.testing.assert_allclose( + results.fidelities, np.array([1.0, 0.5, 0.5, 0.0]), atol=1e-1, rtol=1e-1 + ) + + def test_right_param(self): + """test for fidelity with only right parameters""" + fidelity = ComputeUncompute( + self._sampler, pass_manager=self.pm, num_virtual_qubits=self._circuit[1].num_qubits + ) + n = len(self._left_params) + job = fidelity.run( + [self._circuit[3]] * n, [self._circuit[1]] * n, values_2=self._left_params + ) + results = job.result() + np.testing.assert_allclose( + results.fidelities, np.array([1.0, 0.5, 0.5, 0.0]), atol=1e-1, rtol=1e-1 + ) + + def test_not_set_circuits(self): + """test for fidelity with no circuits.""" + fidelity = ComputeUncompute( + self._sampler, pass_manager=self.pm, num_virtual_qubits=self._circuit[0].num_qubits + ) + with self.assertRaises(TypeError): + job = fidelity.run( + circuits_1=None, + circuits_2=None, + values_1=self._left_params, + values_2=self._right_params, + ) + job.result() + + def test_circuit_mismatch(self): + """test for fidelity with different number of left/right circuits.""" + fidelity = ComputeUncompute( + self._sampler, pass_manager=self.pm, num_virtual_qubits=self._circuit[0].num_qubits + ) + n = len(self._left_params) + with self.assertRaises(ValueError): + job = fidelity.run( + [self._circuit[0]] * n, + [self._circuit[1]] * (n + 1), + self._left_params, + self._right_params, + ) + job.result() + + def test_asymmetric_params(self): + """test for fidelity when the 2 circuits have different number of + left/right parameters.""" + + fidelity = ComputeUncompute( + self._sampler, pass_manager=self.pm, num_virtual_qubits=self._circuit[0].num_qubits + ) + n = len(self._left_params) + right_params = [[p] for p in self._right_params[:, 0]] + job = fidelity.run( + [self._circuit[0]] * n, [self._circuit[4]] * n, self._left_params, right_params + ) + result = job.result() + np.testing.assert_allclose( + result.fidelities, np.array([0.5, 0.25, 0.25, 0.0]), atol=1e-1, rtol=1e-1 + ) + + def test_input_format(self): + """test for different input format variations""" + + circuit = RealAmplitudes(2) + fidelity = ComputeUncompute( + self._sampler, pass_manager=self.pm, num_virtual_qubits=circuit.num_qubits + ) + values = np.random.random(circuit.num_parameters) + shift = np.ones_like(values) * 0.01 + + # lists of circuits, lists of numpy arrays + job = fidelity.run([circuit], [circuit], [values], [values + shift]) + result_1 = job.result() + + # lists of circuits, lists of lists + shift_val = values + shift + job = fidelity.run([circuit], [circuit], [values.tolist()], [shift_val.tolist()]) + result_2 = job.result() + + # circuits, lists + shift_val = values + shift + job = fidelity.run(circuit, circuit, values.tolist(), shift_val.tolist()) + result_3 = job.result() + + # circuits, np.arrays + job = fidelity.run(circuit, circuit, values, values + shift) + result_4 = job.result() + + np.testing.assert_allclose(result_1.fidelities, result_2.fidelities, atol=1e-1, rtol=1e-1) + np.testing.assert_allclose(result_1.fidelities, result_3.fidelities, atol=1e-1, rtol=1e-1) + np.testing.assert_allclose(result_1.fidelities, result_4.fidelities, atol=1e-1, rtol=1e-1) + + def test_input_measurements(self): + """test for fidelity with measurements on input circuits""" + fidelity = ComputeUncompute( + self._sampler, pass_manager=self.pm, num_virtual_qubits=self._circuit[0].num_qubits + ) + circuit_1 = self._circuit[0] + circuit_1.measure_all() + circuit_2 = self._circuit[1] + circuit_2.measure_all() + + job = fidelity.run(circuit_1, circuit_2, self._left_params[0], self._right_params[0]) + result = job.result() + np.testing.assert_allclose(result.fidelities, np.array([1.0])) + + def test_options(self): + """Test fidelity's run options""" + sampler_shots = Sampler(options={"shots": 1024}) + + with self.subTest("sampler"): + # Only options in sampler + fidelity = ComputeUncompute( + sampler_shots, pass_manager=self.pm, num_virtual_qubits=self._circuit[2].num_qubits + ) + options = fidelity.options + job = fidelity.run(self._circuit[2], self._circuit[3]) + result = job.result() + self.assertEqual(options.__dict__, {"shots": 1024}) + self.assertEqual(result.options.__dict__, {"shots": 1024}) + + with self.subTest("fidelity init"): + # Fidelity default options override sampler + # options and add new fields + fidelity = ComputeUncompute( + sampler_shots, + options={"shots": 2048, "dummy": 100}, + pass_manager=self.pm, + num_virtual_qubits=self._circuit[2].num_qubits, + ) + options = fidelity.options + job = fidelity.run(self._circuit[2], self._circuit[3]) + result = job.result() + self.assertEqual(options.__dict__, {"shots": 2048, "dummy": 100}) + self.assertEqual(result.options.__dict__, {"shots": 2048, "dummy": 100}) + + with self.subTest("fidelity update"): + # Update fidelity options + fidelity = ComputeUncompute( + sampler_shots, + options={"shots": 2048, "dummy": 100}, + pass_manager=self.pm, + num_virtual_qubits=self._circuit[2].num_qubits, + ) + fidelity.update_default_options(shots=100) + options = fidelity.options + job = fidelity.run(self._circuit[2], self._circuit[3]) + result = job.result() + self.assertEqual(options.__dict__, {"shots": 100, "dummy": 100}) + self.assertEqual(result.options.__dict__, {"shots": 100, "dummy": 100}) + + with self.subTest("fidelity run"): + # Run options override fidelity options + fidelity = ComputeUncompute( + sampler_shots, + options={"shots": 2048, "dummy": 100}, + pass_manager=self.pm, + num_virtual_qubits=self._circuit[2].num_qubits, + ) + job = fidelity.run(self._circuit[2], self._circuit[3], shots=50, dummy=None) + options = fidelity.options + result = job.result() + # Only default + sampler options. Not run. + self.assertEqual(options.__dict__, {"shots": 2048, "dummy": 100}) + self.assertEqual(result.options.__dict__, {"shots": 50, "dummy": None}) + + +if __name__ == "__main__": + unittest.main() diff --git a/tox.ini b/tox.ini index 0f06e2637..f8f98c294 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] # Sets this min.version because of differences with env_tmp_dir env. minversion = 4.0.2 -envlist = py38, py39, py310, py311, py312, lint, gpu, gpu-amd +envlist = py39, py310, py311, py312, lint, gpu, gpu-amd skipsdist = True [testenv] From 1712ebec18799ba73484c6fd7f74b7351a06b960 Mon Sep 17 00:00:00 2001 From: "M. Emre Sahin" <40424147+OkuyanBoga@users.noreply.github.com> Date: Thu, 7 Nov 2024 16:36:21 +0000 Subject: [PATCH 64/85] Added support for EstimatorV2 primitives (#48) * Migrating `qiskit_algorithms` (#817) * Update README.md * Generalize the Einstein summation signature * Add reno * Update Copyright * Rename and add test * Update Copyright * Add docstring for `test_get_einsum_signature` * Correct spelling * Disable spellcheck for comments * Add `docstring` in pylint dict * Delete example in docstring * Add Einstein in pylint dict * Add full use case in einsum dict * Spelling and type ignore * Spelling and type ignore * Spelling and type ignore * Spelling and type ignore * Spelling and type ignore * Remove for loop in einsum function and remove Literal arguments (1/2) * Remove for loop in einsum function and remove Literal arguments (1/2) * Remove for loop in einsum function and remove Literal arguments (2/2) * Update RuntimeError msg * Update RuntimeError msg - line too long * Trigger CI * Merge algos, globals.random to fix * Fixed `algorithms_globals` * Import /tests and run CI locally * Fix copyrights and some spellings * Ignore mypy in 8 instances * Merge spell dicts * Black reformatting * Black reformatting * Add reno * Lint sanitize * Pylint * Pylint * Pylint * Pylint * Fix relative imports in tutorials * Fix relative imports in tutorials * Remove algorithms from Jupyter magic methods * Temporarily disable "Run stable tutorials" tests * Change the docstrings with imports from qiskit_algorithms * Styling * Update qiskit_machine_learning/optimizers/gradient_descent.py Co-authored-by: Declan Millar * Update qiskit_machine_learning/optimizers/optimizer_utils/learning_rate.py Co-authored-by: Declan Millar * Add more tests for utils * Add more tests for optimizers: adam, bobyqa, gsls and imfil * Fix random seed for volatile optimizers * Fix random seed for volatile optimizers * Add more tests * Pylint dict * Activate scikit-quant-0.8.2 * Remove scikit-quant methods * Remove scikit-quant methods (2) * Edit the release notes and Qiskit version 1+ * Edit the release notes and Qiskit version 1+ * Add Qiskit 1.0 upgrade in reno * Add Qiskit 1.0 upgrade in reno * Add Qiskit 1.0 upgrade in reno * Apply line breaks * Restructure line breaks --------- Co-authored-by: FrancescaSchiav Co-authored-by: M. Emre Sahin <40424147+OkuyanBoga@users.noreply.github.com> Co-authored-by: Declan Millar * Revamp readme pt2 (#822) * Restructure README.md --------- Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com> * Added support for EstimatorV2 primitives * Update qiskit_machine_learning/neural_networks/estimator_qnn.py Co-authored-by: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> * Update qiskit_machine_learning/neural_networks/estimator_qnn.py Co-authored-by: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> * Update qiskit_machine_learning/neural_networks/estimator_qnn.py Co-authored-by: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> * Update qiskit_machine_learning/neural_networks/estimator_qnn.py Co-authored-by: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> * Update qiskit_machine_learning/gradients/param_shift/param_shift_estimator_gradient.py Co-authored-by: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> * Update qiskit_machine_learning/gradients/spsa/spsa_estimator_gradient.py Co-authored-by: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> * Update qiskit_machine_learning/gradients/param_shift/param_shift_estimator_gradient.py Co-authored-by: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> * Update qiskit_machine_learning/neural_networks/estimator_qnn.py Co-authored-by: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> * Update qiskit_machine_learning/neural_networks/estimator_qnn.py Co-authored-by: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> * Update qiskit_machine_learning/gradients/param_shift/param_shift_estimator_gradient.py Co-authored-by: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> * Fix lint errors due to Pylint 3.3.0 update in CI (#833) * disable=too-many-positional-arguments * Transfer pylint rc to toml * Transfer pylint rc to toml * Cleaner statements * Remove Python 3.8 from CI (#824) (#826) * Remove Python 3.8 in CI (#824) * Correct `tmp` dirs (#818) * Correct unit py version (#818) * Add reno (#818) * Finalze removal of py38 (#818) * Spelling * Remove duplicate tmp folder * Updated the release note * Bump min pyversion in toml * Remove ipython constraints * Update reno * Added unit tests for estimatorqnnV2 and minor fixes * Make black * Make lint and changes to V1/2 choice logics * Update requirements * Add default precision * Update estimator tests * Change num qubits in backend * Allow for num_qubits=None * Fix shape in parameter shift * Fix shape in parameter shift * Fix shape observables * Fix shape observables * Change default precision to match base estimator * Fix remaining shape issues * Estimator seed has no effect in local testing * fix argnames and supress error tolerance for test_estimator_qnn_v2 * Added pass manager the gradients. * quick bugfix for isa_circuits * Updating PUBs for estimatorqnn, updating test_estimator_qnn_v2 for ISA circs and relaxing tolerances * Lint and formatting * Tranpiling observables for isa g circs * fixing apply_layout --------- Co-authored-by: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Co-authored-by: FrancescaSchiav Co-authored-by: Declan Millar Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com> Co-authored-by: oscar-wallis --- .../gradients/base/base_estimator_gradient.py | 9 +- .../param_shift_estimator_gradient.py | 80 ++- .../neural_networks/estimator_qnn.py | 97 ++- .../neural_networks/neural_network.py | 6 +- requirements-dev.txt | 1 + ...imator_qnn.py => test_estimator_qnn_v1.py} | 6 +- test/neural_networks/test_estimator_qnn_v2.py | 569 ++++++++++++++++++ 7 files changed, 718 insertions(+), 50 deletions(-) rename test/neural_networks/{test_estimator_qnn.py => test_estimator_qnn_v1.py} (99%) create mode 100644 test/neural_networks/test_estimator_qnn_v2.py diff --git a/qiskit_machine_learning/gradients/base/base_estimator_gradient.py b/qiskit_machine_learning/gradients/base/base_estimator_gradient.py index edfe80fd0..74dc96ffd 100644 --- a/qiskit_machine_learning/gradients/base/base_estimator_gradient.py +++ b/qiskit_machine_learning/gradients/base/base_estimator_gradient.py @@ -24,10 +24,12 @@ from qiskit.circuit import Parameter, ParameterExpression, QuantumCircuit from qiskit.primitives import BaseEstimator +from qiskit.primitives.base import BaseEstimatorV2 from qiskit.primitives.utils import _circuit_key from qiskit.providers import Options from qiskit.quantum_info.operators.base_operator import BaseOperator from qiskit.transpiler.passes import TranslateParameterizedGates +from qiskit.transpiler.passmanager import BasePassManager from .estimator_gradient_result import EstimatorGradientResult from ..utils import ( @@ -46,13 +48,15 @@ class BaseEstimatorGradient(ABC): def __init__( self, - estimator: BaseEstimator, + estimator: BaseEstimator | BaseEstimatorV2, + pass_manager: BasePassManager | None = None, options: Options | None = None, derivative_type: DerivativeType = DerivativeType.REAL, ): r""" Args: estimator: The estimator used to compute the gradients. + pass_manager: pass manager for isa_circuit transpilation. options: Primitive backend runtime options used for circuit execution. The order of priority is: options in ``run`` method > gradient's default options > primitive's default setting. @@ -68,7 +72,8 @@ def __init__( gradient and this type is the only supported type for function-level schemes like finite difference. """ - self._estimator: BaseEstimator = estimator + self._estimator = estimator + self._pass_manager = pass_manager self._default_options = Options() if options is not None: self._default_options.update_options(**options) diff --git a/qiskit_machine_learning/gradients/param_shift/param_shift_estimator_gradient.py b/qiskit_machine_learning/gradients/param_shift/param_shift_estimator_gradient.py index cde25a0fd..3cecf904f 100644 --- a/qiskit_machine_learning/gradients/param_shift/param_shift_estimator_gradient.py +++ b/qiskit_machine_learning/gradients/param_shift/param_shift_estimator_gradient.py @@ -17,14 +17,19 @@ from collections.abc import Sequence +import numpy as np + from qiskit.circuit import Parameter, QuantumCircuit from qiskit.quantum_info.operators.base_operator import BaseOperator +from qiskit.primitives.base import BaseEstimatorV2 +from qiskit.primitives import BaseEstimatorV1 +from qiskit.providers.options import Options from ..base.base_estimator_gradient import BaseEstimatorGradient from ..base.estimator_gradient_result import EstimatorGradientResult from ..utils import _make_param_shift_parameter_values -from ...exceptions import AlgorithmError +from ...exceptions import QiskitMachineLearningError class ParamShiftEstimatorGradient(BaseEstimatorGradient): @@ -97,26 +102,57 @@ def _run_unique( job_param_values.extend(param_shift_parameter_values) all_n.append(n) - # Run the single job with all circuits. - job = self._estimator.run( - job_circuits, - job_observables, - job_param_values, - **options, - ) - try: + # Determine how to run the estimator based on its version + if isinstance(self._estimator, BaseEstimatorV1): + # Run the single job with all circuits. + job = self._estimator.run( + job_circuits, + job_observables, + job_param_values, + **options, + ) results = job.result() - except Exception as exc: - raise AlgorithmError("Estimator job failed.") from exc - - # Compute the gradients. - gradients = [] - partial_sum_n = 0 - for n in all_n: - result = results.values[partial_sum_n : partial_sum_n + n] - gradient_ = (result[: n // 2] - result[n // 2 :]) / 2 - gradients.append(gradient_) - partial_sum_n += n - - opt = self._get_local_options(options) + + # Compute the gradients. + gradients = [] + partial_sum_n = 0 + for n in all_n: + result = results.values[partial_sum_n : partial_sum_n + n] + gradient_ = (result[: n // 2] - result[n // 2 :]) / 2 + gradients.append(gradient_) + partial_sum_n += n + + opt = self._get_local_options(options) + + elif isinstance(self._estimator, BaseEstimatorV2): + isa_g_circs = self._pass_manager.run(job_circuits) + isa_g_observables = [op.apply_layout(isa_g_circs[i].layout) for i, op in enumerate(job_observables)] + # Prepare circuit-observable-parameter tuples (PUBs) + circuit_observable_params = [] + for pub in zip(isa_g_circs, isa_g_observables, job_param_values): + circuit_observable_params.append(pub) + + # For BaseEstimatorV2, run the estimator using PUBs and specified precision + job = self._estimator.run(circuit_observable_params) + results = job.result() + results = np.array([float(r.data.evs) for r in results]) + + # Compute the gradients. + gradients = [] + partial_sum_n = 0 + for n in all_n: + result = results[partial_sum_n : partial_sum_n + n] + gradient_ = (result[: n // 2] - result[n // 2 :]) / 2 + gradients.append(gradient_) + partial_sum_n += n + + opt = Options(**options) + + else: + raise QiskitMachineLearningError( + "The accepted estimators are BaseEstimatorV1 and BaseEstimatorV2; got " + + f"{type(self._estimator)} instead. Note that BaseEstimatorV1 is deprecated in" + + "Qiskit and removed in Qiskit IBM Runtime." + ) + return EstimatorGradientResult(gradients=gradients, metadata=metadata, options=opt) diff --git a/qiskit_machine_learning/neural_networks/estimator_qnn.py b/qiskit_machine_learning/neural_networks/estimator_qnn.py index f55d82224..63c094ce7 100644 --- a/qiskit_machine_learning/neural_networks/estimator_qnn.py +++ b/qiskit_machine_learning/neural_networks/estimator_qnn.py @@ -15,20 +15,22 @@ from __future__ import annotations import logging +import warnings from copy import copy from typing import Sequence - import numpy as np + from qiskit.circuit import Parameter, QuantumCircuit -from qiskit.primitives import BaseEstimator, Estimator, EstimatorResult +from qiskit.primitives.base import BaseEstimatorV2 +from qiskit.primitives import BaseEstimator, BaseEstimatorV1, Estimator, EstimatorResult from qiskit.quantum_info import SparsePauliOp from qiskit.quantum_info.operators.base_operator import BaseOperator + from ..gradients import ( BaseEstimatorGradient, EstimatorGradientResult, ParamShiftEstimatorGradient, ) - from ..circuit.library import QNNCircuit from ..exceptions import QiskitMachineLearningError @@ -64,7 +66,7 @@ class EstimatorQNN(NeuralNetwork): num_qubits = 2 # Using the QNNCircuit: - # Create a parameterized 2 qubit circuit composed of the default ZZFeatureMap feature map + # Create a parametrrized 2 qubit circuit composed of the default ZZFeatureMap feature map # and RealAmplitudes ansatz. qnn_qc = QNNCircuit(num_qubits) @@ -105,12 +107,14 @@ def __init__( self, *, circuit: QuantumCircuit, - estimator: BaseEstimator | None = None, + estimator: BaseEstimator | BaseEstimatorV2 | None = None, observables: Sequence[BaseOperator] | BaseOperator | None = None, input_params: Sequence[Parameter] | None = None, weight_params: Sequence[Parameter] | None = None, gradient: BaseEstimatorGradient | None = None, input_gradients: bool = False, + num_virtual_qubits: int | None = None, + default_precision: float = 0.015625, ): r""" Args: @@ -127,12 +131,12 @@ def __init__( input_params: The parameters that correspond to the input data of the network. If ``None``, the input data is not bound to any parameters. If a :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` is provided the - `input_params` value here is ignored. Instead the value is taken from the + `input_params` value here is ignored. Instead, the value is taken from the :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` input_parameters. weight_params: The parameters that correspond to the trainable weights. If ``None``, the weights are not bound to any parameters. If a :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` is provided the - `weight_params` value here is ignored. Instead the value is taken from the + `weight_params` value here is ignored. Instead, the value is taken from the :class:`~qiskit_machine_learning.circuit.library.QNNCircuit` weight_parameters. gradient: The estimator gradient to be used for the backward pass. If None, a default instance of the estimator gradient, @@ -141,6 +145,8 @@ def __init__( Note that this parameter is ``False`` by default, and must be explicitly set to ``True`` for a proper gradient computation when using :class:`~qiskit_machine_learning.connectors.TorchConnector`. + num_virtual_qubits: Number of virtual qubits. + default_precision: The default precision for the estimator if not specified during run. Raises: QiskitMachineLearningError: Invalid parameter values. @@ -149,19 +155,46 @@ def __init__( estimator = Estimator() self.estimator = estimator self._org_circuit = circuit + + if num_virtual_qubits is None: + self.num_virtual_qubits = circuit.num_qubits + warnings.warn( + f"No number of qubits was not specified ({num_virtual_qubits}) and was retrieved from " + + f"`circuit` ({self.num_virtual_qubits:d}). If `circuit` is transpiled, this may cause " + + "unstable behaviour.", + UserWarning, + stacklevel=2, + ) + else: + self.num_virtual_qubits = num_virtual_qubits + if observables is None: - observables = SparsePauliOp.from_list([("Z" * circuit.num_qubits, 1)]) + observables = SparsePauliOp.from_sparse_list( + [("Z" * self.num_virtual_qubits, range(self.num_virtual_qubits), 1)], + num_qubits=self.circuit.num_qubits, + ) + if isinstance(observables, BaseOperator): observables = (observables,) + self._observables = observables + if isinstance(circuit, QNNCircuit): self._input_params = list(circuit.input_parameters) self._weight_params = list(circuit.weight_parameters) else: self._input_params = list(input_params) if input_params is not None else [] self._weight_params = list(weight_params) if weight_params is not None else [] + if gradient is None: + if isinstance(self.estimator, BaseEstimatorV2): + raise QiskitMachineLearningError( + "Please provide a gradient with pass manager initialised." + ) + gradient = ParamShiftEstimatorGradient(self.estimator) + + self._default_precision = default_precision self.gradient = gradient self._input_gradients = input_gradients @@ -198,7 +231,7 @@ def weight_params(self) -> Sequence[Parameter] | None: @property def input_gradients(self) -> bool: """Returns whether gradients with respect to input data are computed by this neural network - in the ``backward`` method or not. By default such gradients are not computed.""" + in the ``backward`` method or not. By default, such gradients are not computed.""" return self._input_gradients @input_gradients.setter @@ -206,25 +239,44 @@ def input_gradients(self, input_gradients: bool) -> None: """Turn on/off computation of gradients with respect to input data.""" self._input_gradients = input_gradients + @property + def default_precision(self) -> float: + """Return the default precision""" + return self._default_precision + def _forward_postprocess(self, num_samples: int, result: EstimatorResult) -> np.ndarray: """Post-processing during forward pass of the network.""" - return np.reshape(result.values, (-1, num_samples)).T + return np.reshape(result, (-1, num_samples)).T def _forward( self, input_data: np.ndarray | None, weights: np.ndarray | None ) -> np.ndarray | None: """Forward pass of the neural network.""" parameter_values_, num_samples = self._preprocess_forward(input_data, weights) - job = self.estimator.run( - [self._circuit] * num_samples * self.output_shape[0], - [op for op in self._observables for _ in range(num_samples)], - np.tile(parameter_values_, (self.output_shape[0], 1)), - ) - try: - results = job.result() - except Exception as exc: - raise QiskitMachineLearningError("Estimator job failed.") from exc + # Determine how to run the estimator based on its version + if isinstance(self.estimator, BaseEstimatorV1): + job = self.estimator.run( + [self._circuit] * num_samples * self.output_shape[0], + [op for op in self._observables for _ in range(num_samples)], + np.tile(parameter_values_, (self.output_shape[0], 1)), + ) + results = job.result().values + + elif isinstance(self.estimator, BaseEstimatorV2): + # Prepare circuit-observable-parameter tuples (PUBs) + circuit_observable_params = [] + for observable in self._observables: + circuit_observable_params.append((self._circuit, observable, parameter_values_)) + # For BaseEstimatorV2, run the estimator using PUBs and specified precision + job = self.estimator.run(circuit_observable_params, precision=self._default_precision) + results = [result.data.evs for result in job.result()] + else: + raise QiskitMachineLearningError( + "The accepted estimators are BaseEstimatorV1 and BaseEstimatorV2; got " + + f"{type(self.estimator)} instead. Note that BaseEstimatorV1 is deprecated in" + + "Qiskit and removed in Qiskit IBM Runtime." + ) return self._forward_postprocess(num_samples, results) def _backward_postprocess( @@ -269,8 +321,11 @@ def _backward( param_values = np.tile(parameter_values, (num_observables, 1)) job = None + if self._input_gradients: - job = self.gradient.run(circuits, observables, param_values) # type: ignore[arg-type] + job = self.gradient.run( + circuits, observables, param_values + ) # type: ignore[arg-type] elif len(parameter_values[0]) > self._num_inputs: params = [self._circuit.parameters[self._num_inputs :]] * num_circuits job = self.gradient.run( @@ -281,7 +336,7 @@ def _backward( try: results = job.result() except Exception as exc: - raise QiskitMachineLearningError("Estimator job failed.") from exc + raise QiskitMachineLearningError(f"Estimator job failed. {exc}") from exc input_grad, weights_grad = self._backward_postprocess(num_samples, results) diff --git a/qiskit_machine_learning/neural_networks/neural_network.py b/qiskit_machine_learning/neural_networks/neural_network.py index e75858d38..3f0e14c9c 100644 --- a/qiskit_machine_learning/neural_networks/neural_network.py +++ b/qiskit_machine_learning/neural_networks/neural_network.py @@ -293,9 +293,9 @@ def _reparameterize_circuit( if len(parameters) != (self.num_inputs + self.num_weights): raise ValueError( - f"Number of circuit parameters {len(parameters)}" - f" mismatch with sum of num inputs and weights" - f" {self.num_inputs + self.num_weights}" + f"Number of circuit parameters ({len(parameters)})" + f" does not match the sum of number of inputs and weights" + f" ({self.num_inputs + self.num_weights})." ) new_input_params = ParameterVector("inputs", self.num_inputs) diff --git a/requirements-dev.txt b/requirements-dev.txt index bdfa45cba..6a56691fc 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -17,3 +17,4 @@ mypy>=0.981 mypy-extensions>=0.4.3 nbsphinx qiskit_sphinx_theme~=1.16.0 +qiskit-ibm-runtime>=0.21 diff --git a/test/neural_networks/test_estimator_qnn.py b/test/neural_networks/test_estimator_qnn_v1.py similarity index 99% rename from test/neural_networks/test_estimator_qnn.py rename to test/neural_networks/test_estimator_qnn_v1.py index 566329f27..483eaf0c1 100644 --- a/test/neural_networks/test_estimator_qnn.py +++ b/test/neural_networks/test_estimator_qnn_v1.py @@ -20,9 +20,10 @@ from qiskit.circuit import Parameter, QuantumCircuit from qiskit.circuit.library import ZZFeatureMap, RealAmplitudes, ZFeatureMap from qiskit.quantum_info import SparsePauliOp -from qiskit_machine_learning.circuit.library import QNNCircuit +from qiskit_machine_learning.circuit.library import QNNCircuit from qiskit_machine_learning.neural_networks.estimator_qnn import EstimatorQNN +from qiskit_machine_learning.utils import algorithm_globals CASE_DATA = { "shape_1_1": { @@ -178,6 +179,7 @@ def _test_network_passes( estimator_qnn, case_data, ): + algorithm_globals.random_seed = 52 test_data = case_data["test_data"] weights = case_data["weights"] correct_forwards = case_data["correct_forwards"] @@ -407,7 +409,7 @@ def test_setters_getters(self): estimator_qnn.input_gradients = True self.assertTrue(estimator_qnn.input_gradients) - def test_qnn_qc_circui_construction(self): + def test_qnn_qc_circuit_construction(self): """Test Estimator QNN properties and forward/backward pass for QNNCircuit construction""" num_qubits = 2 feature_map = ZZFeatureMap(feature_dimension=num_qubits) diff --git a/test/neural_networks/test_estimator_qnn_v2.py b/test/neural_networks/test_estimator_qnn_v2.py new file mode 100644 index 000000000..773f3fe33 --- /dev/null +++ b/test/neural_networks/test_estimator_qnn_v2.py @@ -0,0 +1,569 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2022, 2024. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" Test EstimatorQNN """ + +import unittest + +from test import QiskitMachineLearningTestCase + +import numpy as np + +from qiskit.circuit import Parameter, QuantumCircuit +from qiskit.circuit.library import ZZFeatureMap, RealAmplitudes, ZFeatureMap +from qiskit.quantum_info import SparsePauliOp +from qiskit.providers.fake_provider import GenericBackendV2 +from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager +from qiskit_ibm_runtime import Session, EstimatorV2 + +from qiskit_machine_learning.circuit.library import QNNCircuit +from qiskit_machine_learning.neural_networks.estimator_qnn import EstimatorQNN +from qiskit_machine_learning.utils import algorithm_globals + +from qiskit_machine_learning.gradients import ParamShiftEstimatorGradient + +algorithm_globals.random_seed = 52 + +CASE_DATA = { + "shape_1_1": { + "test_data": [1, [1], [[1], [2]], [[[1], [2]], [[3], [4]]]], + "weights": [1], + "correct_forwards": [ + [[0.08565359]], + [[0.08565359]], + [[0.08565359], [-0.90744233]], + [[[0.08565359], [-0.90744233]], [[-1.06623996], [-0.24474149]]], + ], + "correct_weight_backwards": [ + [[[0.70807342]]], + [[[0.70807342]]], + [[[0.70807342]], [[0.7651474]]], + [[[[0.70807342]], [[0.7651474]]], [[[0.11874839]], [[-0.63682734]]]], + ], + "correct_input_backwards": [ + [[[-1.13339757]]], + [[[-1.13339757]]], + [[[-1.13339757]], [[-0.68445233]]], + [[[[-1.13339757]], [[-0.68445233]]], [[[0.39377522]], [[1.10996765]]]], + ], + }, + "shape_2_1": { + "test_data": [[1, 2], [[1, 2]], [[1, 2], [3, 4]]], + "weights": [1, 2], + "correct_forwards": [ + [[0.41256026]], + [[0.41256026]], + [[0.41256026], [0.72848859]], + ], + "correct_weight_backwards": [ + [[[0.12262287, -0.17203964]]], + [[[0.12262287, -0.17203964]]], + [[[0.12262287, -0.17203964]], [[0.03230095, -0.04531817]]], + ], + "correct_input_backwards": [ + [[[-0.81570272, -0.39688474]]], + [[[-0.81570272, -0.39688474]]], + [[[-0.81570272, -0.39688474]], [[0.25229775, 0.67111573]]], + ], + }, + "shape_1_2": { + "test_data": [ + [1], + [[1], [2]], + [[[1], [2]], [[3], [4]]], + ], + "weights": [1], + "correct_forwards": [ + [[0.08565359, 0.17130718]], + [[0.08565359, 0.17130718], [-0.90744233, -1.81488467]], + [ + [[0.08565359, 0.17130718], [-0.90744233, -1.81488467]], + [[-1.06623996, -2.13247992], [-0.24474149, -0.48948298]], + ], + ], + "correct_weight_backwards": [ + [[[0.70807342], [1.41614684]]], + [[[0.70807342], [1.41614684]], [[0.7651474], [1.5302948]]], + [ + [[[0.70807342], [1.41614684]], [[0.7651474], [1.5302948]]], + [[[0.11874839], [0.23749678]], [[-0.63682734], [-1.27365468]]], + ], + ], + "correct_input_backwards": [ + [[[-1.13339757], [-2.26679513]]], + [[[-1.13339757], [-2.26679513]], [[-0.68445233], [-1.36890466]]], + [ + [[[-1.13339757], [-2.26679513]], [[-0.68445233], [-1.36890466]]], + [[[0.39377522], [0.78755044]], [[1.10996765], [2.2199353]]], + ], + ], + }, + "shape_2_2": { + "test_data": [[1, 2], [[1, 2], [3, 4]]], + "weights": [1, 2], + "correct_forwards": [ + [[-0.07873524, 0.4912955]], + [[-0.07873524, 0.4912955], [-0.0207402, 0.74922879]], + ], + "correct_weight_backwards": [ + [[[0.12262287, -0.17203964], [0, 0]]], + [[[0.12262287, -0.17203964], [0, 0]], [[0.03230095, -0.04531817], [0, 0]]], + ], + "correct_input_backwards": [ + [[[-0.05055532, -0.17203964], [-0.7651474, -0.2248451]]], + [ + [[-0.05055532, -0.17203964], [-0.7651474, -0.2248451]], + [[0.14549777, 0.02401345], [0.10679997, 0.64710228]], + ], + ], + }, + "no_input_parameters": { + "test_data": [None], + "weights": [1, 1], + "correct_forwards": [[[0.08565359]]], + "correct_weight_backwards": [[[[-1.13339757, 0.70807342]]]], + "correct_input_backwards": [None], + }, + "no_weight_parameters": { + "test_data": [[1, 1]], + "weights": None, + "correct_forwards": [[[0.08565359]]], + "correct_weight_backwards": [None], + "correct_input_backwards": [[[[-1.13339757, 0.70807342]]]], + }, + "no_parameters": { + "test_data": [None], + "weights": None, + "correct_forwards": [[[1]]], + "correct_weight_backwards": [None], + "correct_input_backwards": [None], + }, + "default_observables": { + "test_data": [[[1], [2]]], + "weights": [1], + "correct_forwards": [[[-0.45464871], [-0.4912955]]], + "correct_weight_backwards": [[[[0.70807342]], [[0.7651474]]]], + "correct_input_backwards": [[[[-0.29192658]], [[0.2248451]]]], + }, + "single_observable": { + "test_data": [1, [1], [[1], [2]], [[[1], [2]], [[3], [4]]]], + "weights": [1], + "correct_forwards": [ + [[0.08565359]], + [[0.08565359]], + [[0.08565359], [-0.90744233]], + [[[0.08565359], [-0.90744233]], [[-1.06623996], [-0.24474149]]], + ], + "correct_weight_backwards": [ + [[[0.70807342]]], + [[[0.70807342]]], + [[[0.70807342]], [[0.7651474]]], + [[[[0.70807342]], [[0.7651474]]], [[[0.11874839]], [[-0.63682734]]]], + ], + "correct_input_backwards": [ + [[[-1.13339757]]], + [[[-1.13339757]]], + [[[-1.13339757]], [[-0.68445233]]], + [[[[-1.13339757]], [[-0.68445233]]], [[[0.39377522]], [[1.10996765]]]], + ], + }, +} + + +class TestEstimatorQNNV2(QiskitMachineLearningTestCase): + """EstimatorQNN Tests for estimator_v2. The correct references is obtained from EstimatorQNN""" + + tolerance: dict[str:float] = dict(atol=3 * 1.0e-1, rtol=3 * 1.0e-1) + backend = GenericBackendV2(num_qubits=2, seed=123) + session = Session(backend=backend) + + def __init__( + self, + TestCase, + ): + self.estimator = EstimatorV2(mode=self.session, options={"default_shots": 1e3}) + self.pm = generate_preset_pass_manager(backend=self.backend, optimization_level=0) + self.gradient = ParamShiftEstimatorGradient(estimator=self.estimator, pass_manager=self.pm) + super().__init__(TestCase) + + def _test_network_passes( + self, + estimator_qnn, + case_data, + ): + test_data = case_data["test_data"] + weights = case_data["weights"] + correct_forwards = case_data["correct_forwards"] + correct_weight_backwards = case_data["correct_weight_backwards"] + correct_input_backwards = case_data["correct_input_backwards"] + + # test forward pass + with self.subTest("forward pass"): + for i, inputs in enumerate(test_data): + forward = estimator_qnn.forward(inputs, weights) + np.testing.assert_allclose(forward, correct_forwards[i], **self.tolerance) + # test backward pass without input_gradients + with self.subTest("backward pass without input gradients"): + for i, inputs in enumerate(test_data): + input_backward, weight_backward = estimator_qnn.backward(inputs, weights) + if correct_weight_backwards[i] is None: + self.assertIsNone(weight_backward) + else: + np.testing.assert_allclose( + weight_backward, correct_weight_backwards[i], **self.tolerance + ) + self.assertIsNone(input_backward) + # test backward pass with input_gradients + with self.subTest("backward pass with input gradients"): + estimator_qnn.input_gradients = True + for i, inputs in enumerate(test_data): + input_backward, weight_backward = estimator_qnn.backward(inputs, weights) + if correct_weight_backwards[i] is None: + self.assertIsNone(weight_backward) + else: + np.testing.assert_allclose( + weight_backward, correct_weight_backwards[i], **self.tolerance + ) + if correct_input_backwards[i] is None: + self.assertIsNone(input_backward) + else: + np.testing.assert_allclose( + input_backward, correct_input_backwards[i], **self.tolerance + ) + + def test_estimator_qnn_1_1(self): + """Test Estimator QNN with input/output dimension 1/1.""" + params = [Parameter("input1"), Parameter("weight1")] + qc = QuantumCircuit(1) + qc.h(0) + qc.ry(params[0], 0) + qc.rx(params[1], 0) + isa_qc = self.pm.run(qc) + op = SparsePauliOp.from_list([("Z", 1), ("X", 1)]) + isa_ob = op.apply_layout(isa_qc.layout) + estimator_qnn = EstimatorQNN( + circuit=isa_qc, + observables=[isa_ob], + input_params=[params[0]], + weight_params=[params[1]], + estimator=self.estimator, + gradient=self.gradient, + num_virtual_qubits=qc.num_qubits, + ) + + self._test_network_passes(estimator_qnn, CASE_DATA["shape_1_1"]) + + def test_estimator_qnn_2_1(self): + """Test Estimator QNN with input/output dimension 2/1.""" + params = [ + Parameter("input1"), + Parameter("input2"), + Parameter("weight1"), + Parameter("weight2"), + ] + qc = QuantumCircuit(2) + qc.h(0) + qc.ry(params[0], 0) + qc.ry(params[1], 1) + qc.rx(params[2], 0) + qc.rx(params[3], 1) + isa_qc = self.pm.run(qc) + op = SparsePauliOp.from_list([("ZZ", 1), ("XX", 1)]) + op = op.apply_layout(isa_qc.layout) + estimator_qnn = EstimatorQNN( + circuit=isa_qc, + observables=[op], + input_params=params[:2], + weight_params=params[2:], + estimator=self.estimator, + gradient=self.gradient, + num_virtual_qubits=qc.num_qubits, + ) + + self._test_network_passes(estimator_qnn, CASE_DATA["shape_2_1"]) + + def test_estimator_qnn_1_2(self): + """Test Estimator QNN with input/output dimension 1/2.""" + params = [Parameter("input1"), Parameter("weight1")] + qc = QuantumCircuit(1) + qc.h(0) + qc.ry(params[0], 0) + qc.rx(params[1], 0) + + isa_qc = self.pm.run(qc) + op1 = SparsePauliOp.from_list([("Z", 1), ("X", 1)]) + op1 = op1.apply_layout(isa_qc.layout) + op2 = SparsePauliOp.from_list([("Z", 2), ("X", 2)]) + op2 = op2.apply_layout(isa_qc.layout) + + # construct QNN + estimator_qnn = EstimatorQNN( + circuit=isa_qc, + observables=[op1, op2], + input_params=[params[0]], + weight_params=[params[1]], + estimator=self.estimator, + gradient=self.gradient, + num_virtual_qubits=qc.num_qubits, + ) + + self._test_network_passes(estimator_qnn, CASE_DATA["shape_1_2"]) + + def test_estimator_qnn_2_2(self): + """Test Estimator QNN with input/output dimension 2/2.""" + params = [ + Parameter("input1"), + Parameter("input2"), + Parameter("weight1"), + Parameter("weight2"), + ] + qc = QuantumCircuit(2) + qc.h(0) + qc.ry(params[0], 0) + qc.ry(params[1], 1) + qc.rx(params[2], 0) + qc.rx(params[3], 1) + isa_qc = self.pm.run(qc) + op1 = SparsePauliOp.from_list([("ZZ", 1)]) + op1 = op1.apply_layout(isa_qc.layout) + op2 = SparsePauliOp.from_list([("XX", 1)]) + op2 = op2.apply_layout(isa_qc.layout) + + estimator_qnn = EstimatorQNN( + circuit=isa_qc, + observables=[op1, op2], + input_params=params[:2], + weight_params=params[2:], + estimator=self.estimator, + gradient=self.gradient, + num_virtual_qubits=qc.num_qubits, + ) + + self._test_network_passes(estimator_qnn, CASE_DATA["shape_2_2"]) + + def test_no_input_parameters(self): + """Test Estimator QNN with no input parameters.""" + params = [Parameter("weight0"), Parameter("weight1")] + qc = QuantumCircuit(1) + qc.h(0) + qc.ry(params[0], 0) + qc.rx(params[1], 0) + isa_qc = self.pm.run(qc) + op = SparsePauliOp.from_list([("Z", 1), ("X", 1)]) + op = op.apply_layout(isa_qc.layout) + estimator_qnn = EstimatorQNN( + circuit=isa_qc, + observables=[op], + input_params=None, + weight_params=params, + estimator=self.estimator, + gradient=self.gradient, + num_virtual_qubits=qc.num_qubits, + ) + self._test_network_passes(estimator_qnn, CASE_DATA["no_input_parameters"]) + + def test_no_weight_parameters(self): + """Test Estimator QNN with no weight parameters.""" + params = [Parameter("input0"), Parameter("input1")] + qc = QuantumCircuit(1) + qc.h(0) + qc.ry(params[0], 0) + qc.rx(params[1], 0) + isa_qc = self.pm.run(qc) + op = SparsePauliOp.from_list([("Z", 1), ("X", 1)]) + op = op.apply_layout(isa_qc.layout) + estimator_qnn = EstimatorQNN( + circuit=isa_qc, + observables=[op], + input_params=params, + weight_params=None, + estimator=self.estimator, + gradient=self.gradient, + num_virtual_qubits=qc.num_qubits, + ) + self._test_network_passes(estimator_qnn, CASE_DATA["no_weight_parameters"]) + + def test_no_parameters(self): + """Test Estimator QNN with no parameters.""" + qc = QuantumCircuit(1) + qc.h(0) + isa_qc = self.pm.run(qc) + op = SparsePauliOp.from_list([("Z", 1), ("X", 1)]) + op = op.apply_layout(isa_qc.layout) + estimator_qnn = EstimatorQNN( + circuit=isa_qc, + observables=[op], + input_params=None, + weight_params=None, + estimator=self.estimator, + gradient=self.gradient, + num_virtual_qubits=qc.num_qubits, + ) + self._test_network_passes(estimator_qnn, CASE_DATA["no_parameters"]) + + def test_default_observables(self): + """Test Estimator QNN with default observables.""" + params = [Parameter("input1"), Parameter("weight1")] + qc = QuantumCircuit(1) + qc.h(0) + qc.ry(params[0], 0) + qc.rx(params[1], 0) + isa_qc = self.pm.run(qc) + estimator_qnn = EstimatorQNN( + circuit=isa_qc, + input_params=[params[0]], + weight_params=[params[1]], + estimator=self.estimator, + gradient=self.gradient, + num_virtual_qubits=qc.num_qubits, + ) + self._test_network_passes(estimator_qnn, CASE_DATA["default_observables"]) + + def test_single_observable(self): + """Test Estimator QNN with single observable.""" + params = [Parameter("input1"), Parameter("weight1")] + qc = QuantumCircuit(1) + qc.h(0) + qc.ry(params[0], 0) + qc.rx(params[1], 0) + isa_qc = self.pm.run(qc) + op = SparsePauliOp.from_list([("Z", 1), ("X", 1)]) + op = op.apply_layout(isa_qc.layout) + estimator_qnn = EstimatorQNN( + circuit=isa_qc, + observables=op, + input_params=[params[0]], + weight_params=[params[1]], + estimator=self.estimator, + gradient=self.gradient, + num_virtual_qubits=isa_qc.num_qubits, + ) + self._test_network_passes(estimator_qnn, CASE_DATA["single_observable"]) + + def test_setters_getters(self): + """Test Estimator QNN properties.""" + params = [Parameter("input1"), Parameter("weight1")] + qc = QuantumCircuit(1) + qc.h(0) + qc.ry(params[0], 0) + qc.rx(params[1], 0) + isa_qc = self.pm.run(qc) + op = SparsePauliOp.from_list([("Z", 1), ("X", 1)]) + op = op.apply_layout(isa_qc.layout) + estimator_qnn = EstimatorQNN( + circuit=isa_qc, + observables=[op], + input_params=[params[0]], + weight_params=[params[1]], + estimator=self.estimator, + gradient=self.gradient, + num_virtual_qubits=qc.num_qubits, + ) + with self.subTest("Test circuit getter."): + self.assertEqual(estimator_qnn.circuit, isa_qc) + with self.subTest("Test observables getter."): + self.assertEqual(estimator_qnn.observables, [op]) + with self.subTest("Test input_params getter."): + self.assertEqual(estimator_qnn.input_params, [params[0]]) + with self.subTest("Test weight_params getter."): + self.assertEqual(estimator_qnn.weight_params, [params[1]]) + with self.subTest("Test input_gradients setter and getter."): + self.assertFalse(estimator_qnn.input_gradients) + estimator_qnn.input_gradients = True + self.assertTrue(estimator_qnn.input_gradients) + + def test_qnn_qc_circuit_construction(self): + """Test Estimator QNN properties and forward/backward pass for QNNCircuit construction""" + num_qubits = 2 + feature_map = ZZFeatureMap(feature_dimension=num_qubits) + ansatz = RealAmplitudes(num_qubits=num_qubits, reps=1) + + qc = QuantumCircuit(num_qubits) + qc.compose(feature_map, inplace=True) + qc.compose(ansatz, inplace=True) + isa_qc = self.pm.run(qc) + + estimator_qc = EstimatorQNN( + circuit=isa_qc, + input_params=feature_map.parameters, + weight_params=ansatz.parameters, + input_gradients=True, + estimator=self.estimator, + gradient=self.gradient, + num_virtual_qubits=qc.num_qubits, + ) + + qnn_qc = QNNCircuit(num_qubits=num_qubits, feature_map=feature_map, ansatz=ansatz) + isa_qnn_qc = self.pm.run(qnn_qc) + estimator_qnn_qc = EstimatorQNN( + circuit=isa_qnn_qc, + input_params=qnn_qc.feature_map.parameters, + weight_params=qnn_qc.ansatz.parameters, + input_gradients=True, + estimator=self.estimator, + gradient=self.gradient, + num_virtual_qubits=qc.num_qubits, + ) + + input_data = [1, 2] + weights = [1, 2, 3, 4] + + with self.subTest("Test if Estimator QNN properties are equal."): + self.assertEqual(estimator_qnn_qc.input_params, estimator_qc.input_params) + self.assertEqual(estimator_qnn_qc.weight_params, estimator_qc.weight_params) + self.assertEqual(estimator_qnn_qc.observables, estimator_qc.observables) + + with self.subTest("Test if forward pass yields equal results."): + forward_qc = estimator_qc.forward(input_data=input_data, weights=weights) + forward_qnn_qc = estimator_qnn_qc.forward(input_data=input_data, weights=weights) + np.testing.assert_allclose(forward_qc, forward_qnn_qc, **self.tolerance) + + with self.subTest("Test if backward pass yields equal results."): + backward_qc = estimator_qc.backward(input_data=input_data, weights=weights) + backward_qnn_qc = estimator_qnn_qc.backward(input_data=input_data, weights=weights) + + # Test if input grad is close (difference due to shots) + np.testing.assert_allclose(backward_qc[0], backward_qnn_qc[0], **self.tolerance) + # Test if weights grad is close (difference due to shots) + np.testing.assert_allclose(backward_qc[1], backward_qnn_qc[1], **self.tolerance) + + def test_binding_order(self): + """Test parameter binding order gives result as expected""" + qc = ZFeatureMap(feature_dimension=2, reps=1) + input_params = qc.parameters + weight = Parameter("weight") + for i in range(qc.num_qubits): + qc.rx(weight, i) + isa_qc = self.pm.run(qc) + print(isa_qc) + op = SparsePauliOp.from_list([("Z" * isa_qc.num_qubits, 1)]) + op = op.apply_layout(isa_qc.layout) + estimator_qnn = EstimatorQNN( + circuit=isa_qc, + observables=op, + input_params=input_params, + weight_params=[weight], + estimator=self.estimator, + gradient=self.gradient, + num_virtual_qubits=qc.num_qubits, + ) + + estimator_qnn_weights = [3] + estimator_qnn_input = [2, 33] + res = estimator_qnn.forward(estimator_qnn_input, estimator_qnn_weights) + # When parameters were used in circuit order, before being assigned correctly, so inputs + # went to input params, weights to weight params, this gave 0.00613403 + self.assertAlmostEqual(res[0][0], 0.00040017, delta=0.05) + + +if __name__ == "__main__": + unittest.main() From 2bf26689d802e5e573263f532ea8c72b07f36ca0 Mon Sep 17 00:00:00 2001 From: Emre Date: Fri, 8 Nov 2024 11:15:45 +0000 Subject: [PATCH 65/85] Pulled changes from main --- .github/workflows/main.yml | 20 ++++---------------- constraints.txt | 4 +--- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2bdd419ad..b4cc1e962 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -114,6 +114,7 @@ jobs: os: [ubuntu-latest] python-version: [3.9, '3.10', 3.11, 3.12] include: + # macos-latest is an Arm64 image - os: macos-latest python-version: 3.9 - os: macos-latest @@ -122,11 +123,6 @@ jobs: python-version: 3.9 - os: windows-latest python-version: 3.12 - # macos-14 is an Arm64 image - - os: macos-14 - python-version: '3.10' - - os: macos-14 - python-version: 3.12 steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 @@ -146,7 +142,7 @@ jobs: - run: make lint shell: bash - run: make mypy - if: ${{ !cancelled() && matrix.os != 'windows-latest' }} + if: ${{ !cancelled() }} shell: bash - name: Machine Learning Unit Tests under Python ${{ matrix.python-version }} uses: ./.github/actions/run-tests @@ -302,20 +298,12 @@ jobs: with: name: windows-latest-3.12 path: /tmp/w312 - - uses: actions/download-artifact@v4 - with: - name: macos-14-3.10 - path: /tmp/a310 - - uses: actions/download-artifact@v4 - with: - name: macos-14-3.12 - path: /tmp/a312 - name: Install Dependencies run: pip install -U coverage coveralls diff-cover shell: bash - name: Combined Deprecation Messages run: | - sort -f -u /tmp/u39/ml.dep /tmp/u310/ml.dep /tmp/u311/ml.dep /tmp/u312/ml.dep /tmp/m39/ml.dep /tmp/m312/ml.dep /tmp/w39/ml.dep /tmp/w312/ml.dep /tmp/a310/ml.dep /tmp/a312/ml.dep || true + sort -f -u /tmp/u39/ml.dep /tmp/u310/ml.dep /tmp/u311/ml.dep /tmp/u312/ml.dep /tmp/m39/ml.dep /tmp/m312/ml.dep /tmp/w39/ml.dep /tmp/w312/ml.dep || true shell: bash - name: Coverage combine run: coverage3 combine /tmp/u39/ml.dat @@ -324,4 +312,4 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: coveralls --service=github - shell: bash + shell: bash \ No newline at end of file diff --git a/constraints.txt b/constraints.txt index 4cf890b52..8c5efaa52 100644 --- a/constraints.txt +++ b/constraints.txt @@ -1,3 +1 @@ -numpy>=1.20,<2.0 -nbconvert<7.14 # workaround https://github.com/jupyter/nbconvert/issues/2092 - +numpy>=1.20 \ No newline at end of file From e52575b2ee82b4a669bc790751282b71b3875f54 Mon Sep 17 00:00:00 2001 From: Emre Date: Fri, 8 Nov 2024 11:17:06 +0000 Subject: [PATCH 66/85] Quick fix --- constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/constraints.txt b/constraints.txt index 8c5efaa52..2496c6ccb 100644 --- a/constraints.txt +++ b/constraints.txt @@ -1 +1 @@ -numpy>=1.20 \ No newline at end of file +numpy>=1.20 \ No newline at end of file From 805a6b108984b7ea48f1c6a1b739156367cc0d1d Mon Sep 17 00:00:00 2001 From: Emre Date: Fri, 8 Nov 2024 12:41:52 +0000 Subject: [PATCH 67/85] bugfix for V1 --- qiskit_machine_learning/state_fidelities/compute_uncompute.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_machine_learning/state_fidelities/compute_uncompute.py b/qiskit_machine_learning/state_fidelities/compute_uncompute.py index a1f745f67..60b981160 100644 --- a/qiskit_machine_learning/state_fidelities/compute_uncompute.py +++ b/qiskit_machine_learning/state_fidelities/compute_uncompute.py @@ -228,7 +228,7 @@ def _call( if local: raw_fidelities = [ - ComputeUncompute._get_local_fidelity(prob_dist, num_virtual_qubits) + ComputeUncompute._get_local_fidelity(prob_dist, num_virtual_qubits if isinstance(_sampler, BaseSamplerV2) else circuit.num_qubits) for prob_dist, circuit in zip(quasi_dists, circuits) ] else: From 9a6574b9fe6146489a7997ac56aaeda2086f9322 Mon Sep 17 00:00:00 2001 From: oscar-wallis Date: Fri, 8 Nov 2024 13:44:06 +0000 Subject: [PATCH 68/85] formatting --- .../param_shift_estimator_gradient.py | 4 +- setup.py | 38 +++++++++++-------- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/qiskit_machine_learning/gradients/param_shift/param_shift_estimator_gradient.py b/qiskit_machine_learning/gradients/param_shift/param_shift_estimator_gradient.py index 3cecf904f..827a8911c 100644 --- a/qiskit_machine_learning/gradients/param_shift/param_shift_estimator_gradient.py +++ b/qiskit_machine_learning/gradients/param_shift/param_shift_estimator_gradient.py @@ -126,7 +126,9 @@ def _run_unique( elif isinstance(self._estimator, BaseEstimatorV2): isa_g_circs = self._pass_manager.run(job_circuits) - isa_g_observables = [op.apply_layout(isa_g_circs[i].layout) for i, op in enumerate(job_observables)] + isa_g_observables = [ + op.apply_layout(isa_g_circs[i].layout) for i, op in enumerate(job_observables) + ] # Prepare circuit-observable-parameter tuples (PUBs) circuit_observable_params = [] for pub in zip(isa_g_circs, isa_g_observables, job_param_values): diff --git a/setup.py b/setup.py index d7caaa4be..ea5e885e4 100644 --- a/setup.py +++ b/setup.py @@ -16,12 +16,16 @@ import os import re -with open('requirements.txt') as f: +with open("requirements.txt") as f: REQUIREMENTS = f.read().splitlines() -if not hasattr(setuptools, 'find_namespace_packages') or not inspect.ismethod(setuptools.find_namespace_packages): - print("Your setuptools version:'{}' does not support PEP 420 (find_namespace_packages). " - "Upgrade it to version >='40.1.0' and repeat install.".format(setuptools.__version__)) +if not hasattr(setuptools, "find_namespace_packages") or not inspect.ismethod( + setuptools.find_namespace_packages +): + print( + "Your setuptools version:'{}' does not support PEP 420 (find_namespace_packages). " + "Upgrade it to version >='40.1.0' and repeat install.".format(setuptools.__version__) + ) sys.exit(1) VERSION_PATH = os.path.join(os.path.dirname(__file__), "qiskit_machine_learning", "VERSION.txt") @@ -39,15 +43,15 @@ ) setuptools.setup( - name='qiskit-machine-learning', + name="qiskit-machine-learning", version=VERSION, - description='Qiskit Machine Learning: A library of quantum computing machine learning experiments', + description="Qiskit Machine Learning: A library of quantum computing machine learning experiments", long_description=README, long_description_content_type="text/markdown", - url='https://github.com/qiskit-community/qiskit-machine-learning', - author='Qiskit Machine Learning Development Team', - author_email='qiskit@us.ibm.com', - license='Apache-2.0', + url="https://github.com/qiskit-community/qiskit-machine-learning", + author="Qiskit Machine Learning Development Team", + author_email="qiskit@us.ibm.com", + license="Apache-2.0", classifiers=[ "Environment :: Console", "License :: OSI Approved :: Apache Software License", @@ -61,21 +65,23 @@ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", - "Topic :: Scientific/Engineering" + "Topic :: Scientific/Engineering", ], - keywords='qiskit sdk quantum machine learning ml', - packages=setuptools.find_packages(include=['qiskit_machine_learning','qiskit_machine_learning.*']), + keywords="qiskit sdk quantum machine learning ml", + packages=setuptools.find_packages( + include=["qiskit_machine_learning", "qiskit_machine_learning.*"] + ), install_requires=REQUIREMENTS, include_package_data=True, python_requires=">=3.9", extras_require={ - 'torch': ["torch"], - 'sparse': ["sparse"], + "torch": ["torch"], + "sparse": ["sparse"], }, project_urls={ "Bug Tracker": "https://github.com/qiskit-community/qiskit-machine-learning/issues", "Documentation": "https://qiskit-community.github.io/qiskit-machine-learning/", "Source Code": "https://github.com/qiskit-community/qiskit-machine-learning", }, - zip_safe=False + zip_safe=False, ) From 1d03d4f31fab835e5197acef89561323031480de Mon Sep 17 00:00:00 2001 From: Oscar <108736468+oscar-wallis@users.noreply.github.com> Date: Fri, 8 Nov 2024 14:08:39 +0000 Subject: [PATCH 69/85] Prep-ing for 0.8 (#53) * Migrating `qiskit_algorithms` (#817) * Update README.md * Generalize the Einstein summation signature * Add reno * Update Copyright * Rename and add test * Update Copyright * Add docstring for `test_get_einsum_signature` * Correct spelling * Disable spellcheck for comments * Add `docstring` in pylint dict * Delete example in docstring * Add Einstein in pylint dict * Add full use case in einsum dict * Spelling and type ignore * Spelling and type ignore * Spelling and type ignore * Spelling and type ignore * Spelling and type ignore * Remove for loop in einsum function and remove Literal arguments (1/2) * Remove for loop in einsum function and remove Literal arguments (1/2) * Remove for loop in einsum function and remove Literal arguments (2/2) * Update RuntimeError msg * Update RuntimeError msg - line too long * Trigger CI * Merge algos, globals.random to fix * Fixed `algorithms_globals` * Import /tests and run CI locally * Fix copyrights and some spellings * Ignore mypy in 8 instances * Merge spell dicts * Black reformatting * Black reformatting * Add reno * Lint sanitize * Pylint * Pylint * Pylint * Pylint * Fix relative imports in tutorials * Fix relative imports in tutorials * Remove algorithms from Jupyter magic methods * Temporarily disable "Run stable tutorials" tests * Change the docstrings with imports from qiskit_algorithms * Styling * Update qiskit_machine_learning/optimizers/gradient_descent.py Co-authored-by: Declan Millar * Update qiskit_machine_learning/optimizers/optimizer_utils/learning_rate.py Co-authored-by: Declan Millar * Add more tests for utils * Add more tests for optimizers: adam, bobyqa, gsls and imfil * Fix random seed for volatile optimizers * Fix random seed for volatile optimizers * Add more tests * Pylint dict * Activate scikit-quant-0.8.2 * Remove scikit-quant methods * Remove scikit-quant methods (2) * Edit the release notes and Qiskit version 1+ * Edit the release notes and Qiskit version 1+ * Add Qiskit 1.0 upgrade in reno * Add Qiskit 1.0 upgrade in reno * Add Qiskit 1.0 upgrade in reno * Apply line breaks * Restructure line breaks --------- Co-authored-by: FrancescaSchiav Co-authored-by: M. Emre Sahin <40424147+OkuyanBoga@users.noreply.github.com> Co-authored-by: Declan Millar * Revamp readme pt2 (#822) * Restructure README.md --------- Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com> * Fix lint errors due to Pylint 3.3.0 update in CI (#833) * disable=too-many-positional-arguments * Transfer pylint rc to toml * Transfer pylint rc to toml * Remove Python 3.8 from CI (#824) (#826) * Remove Python 3.8 in CI (#824) * Correct `tmp` dirs (#818) * Correct unit py version (#818) * Add reno (#818) * Finalze removal of py38 (#818) * Spelling * Remove duplicate tmp folder * Updated the release note * Bump min pyversion in toml * Remove ipython constraints * Update reno * Reestablish latest Pytorch and Numpy (#818) (#827) * Reestablish latest Pytorch and Numpy (#818) * Keep pinned Numpy * Keep pinned Numpy * Fix numpy min version * Fix RawFeatureVector for failing test case (#838) * Alter RawFeatureVector normalization * Alter RawFeatureVector normalization * Release nbconvert constraints (#842) * Remove redundant MacOS 14 image in CI (#841) * Remove redundant MacOS 14 image * Update with macos-latest-large * Revert "Update with macos-latest-large" This reverts commit 14f945e3f0c8eaf39195fed81bc6e55ce077735f. * Update with macos-latest-large * Update v2 (#54) * bugfix for V1 * formatting --------- Co-authored-by: oscar-wallis --------- Co-authored-by: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Co-authored-by: FrancescaSchiav Co-authored-by: M. Emre Sahin <40424147+OkuyanBoga@users.noreply.github.com> Co-authored-by: Declan Millar Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com> --- constraints.txt | 2 +- .../circuit/library/raw_feature_vector.py | 5 +++-- .../gradients/base/base_sampler_gradient.py | 1 + qiskit_machine_learning/neural_networks/estimator_qnn.py | 3 +++ qiskit_machine_learning/neural_networks/sampler_qnn.py | 8 +++++--- .../state_fidelities/compute_uncompute.py | 3 ++- 6 files changed, 15 insertions(+), 7 deletions(-) diff --git a/constraints.txt b/constraints.txt index 2496c6ccb..cc3e94d2c 100644 --- a/constraints.txt +++ b/constraints.txt @@ -1 +1 @@ -numpy>=1.20 \ No newline at end of file +numpy>=1.20 diff --git a/qiskit_machine_learning/circuit/library/raw_feature_vector.py b/qiskit_machine_learning/circuit/library/raw_feature_vector.py index be9cedf7b..bdb0cd460 100644 --- a/qiskit_machine_learning/circuit/library/raw_feature_vector.py +++ b/qiskit_machine_learning/circuit/library/raw_feature_vector.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2020, 2023. +# (C) Copyright IBM 2020, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -176,7 +176,8 @@ def _define(self): raise QiskitError("Cannot define a ParameterizedInitialize with unbound parameters") # normalize - normalized = np.array(cleaned_params) / np.linalg.norm(cleaned_params) + norm = np.linalg.norm(cleaned_params) + normalized = cleaned_params if np.isclose(norm, 1) else cleaned_params / norm circuit = QuantumCircuit(self.num_qubits) circuit.initialize(normalized, range(self.num_qubits)) diff --git a/qiskit_machine_learning/gradients/base/base_sampler_gradient.py b/qiskit_machine_learning/gradients/base/base_sampler_gradient.py index eaee27945..f27d1c45c 100644 --- a/qiskit_machine_learning/gradients/base/base_sampler_gradient.py +++ b/qiskit_machine_learning/gradients/base/base_sampler_gradient.py @@ -61,6 +61,7 @@ def __init__( self._default_options = Options() self._pass_manager = pass_manager self._len_quasi_dist = len_quasi_dist + if options is not None: self._default_options.update_options(**options) self._gradient_circuit_cache: dict[tuple, GradientCircuit] = {} diff --git a/qiskit_machine_learning/neural_networks/estimator_qnn.py b/qiskit_machine_learning/neural_networks/estimator_qnn.py index 63c094ce7..9aa75afe4 100644 --- a/qiskit_machine_learning/neural_networks/estimator_qnn.py +++ b/qiskit_machine_learning/neural_networks/estimator_qnn.py @@ -31,6 +31,7 @@ EstimatorGradientResult, ParamShiftEstimatorGradient, ) + from ..circuit.library import QNNCircuit from ..exceptions import QiskitMachineLearningError @@ -323,9 +324,11 @@ def _backward( job = None if self._input_gradients: + job = self.gradient.run( circuits, observables, param_values ) # type: ignore[arg-type] + elif len(parameter_values[0]) > self._num_inputs: params = [self._circuit.parameters[self._num_inputs :]] * num_circuits job = self.gradient.run( diff --git a/qiskit_machine_learning/neural_networks/sampler_qnn.py b/qiskit_machine_learning/neural_networks/sampler_qnn.py index 28fee16d8..6c2e81ea6 100644 --- a/qiskit_machine_learning/neural_networks/sampler_qnn.py +++ b/qiskit_machine_learning/neural_networks/sampler_qnn.py @@ -37,6 +37,7 @@ from .neural_network import NeuralNetwork + if _optionals.HAS_SPARSE: # pylint: disable=import-error from sparse import SparseArray @@ -349,7 +350,6 @@ def _postprocess_gradient( ) weights_grad = DOK((num_samples, *self._output_shape, self._num_weights)) else: - input_grad = ( np.zeros((num_samples, *self._output_shape, self._num_inputs)) if self._input_gradients @@ -441,10 +441,12 @@ def _backward( circuits = [self._circuit] * num_samples job = None if self._input_gradients: - job = self.gradient.run(circuits, parameter_values) + job = self.gradient.run(circuits, parameter_values) # type: ignore[arg-type] elif len(parameter_values[0]) > self._num_inputs: params = [self._circuit.parameters[self._num_inputs :]] * num_samples - job = self.gradient.run(circuits, parameter_values, parameters=params) + job = self.gradient.run( + circuits, parameter_values, parameters=params # type: ignore[arg-type] + ) if job is not None: try: diff --git a/qiskit_machine_learning/state_fidelities/compute_uncompute.py b/qiskit_machine_learning/state_fidelities/compute_uncompute.py index 60b981160..77dbaf5ca 100644 --- a/qiskit_machine_learning/state_fidelities/compute_uncompute.py +++ b/qiskit_machine_learning/state_fidelities/compute_uncompute.py @@ -18,7 +18,6 @@ from copy import copy from qiskit import QuantumCircuit - from qiskit.primitives import BaseSampler, BaseSamplerV1, SamplerResult, StatevectorSampler from qiskit.primitives.base import BaseSamplerV2 @@ -29,6 +28,7 @@ from qiskit.providers import Options from ..exceptions import AlgorithmError, QiskitMachineLearningError + from .base_state_fidelity import BaseStateFidelity from .state_fidelity_result import StateFidelityResult from ..algorithm_job import AlgorithmJob @@ -180,6 +180,7 @@ def _run( # primitive's default options. opts = copy(self._default_options) opts.update_options(**options) + if isinstance(self._sampler, BaseSamplerV1): sampler_job = self._sampler.run( circuits=circuits, parameter_values=values, **opts.__dict__ From 5606dd6c18bb518eace890797b37f59169ca2b88 Mon Sep 17 00:00:00 2001 From: Emre Date: Fri, 8 Nov 2024 14:31:50 +0000 Subject: [PATCH 70/85] Update test_qbayesian --- test/algorithms/inference/test_qbayesian.py | 191 -------------------- 1 file changed, 191 deletions(-) diff --git a/test/algorithms/inference/test_qbayesian.py b/test/algorithms/inference/test_qbayesian.py index 5c68e17ab..cae2a1b6d 100644 --- a/test/algorithms/inference/test_qbayesian.py +++ b/test/algorithms/inference/test_qbayesian.py @@ -21,8 +21,6 @@ from qiskit import QuantumCircuit from qiskit.circuit import QuantumRegister from qiskit.primitives import Sampler -from qiskit.providers.fake_provider import GenericBackendV2 -from qiskit_ibm_runtime import Session, SamplerV2 from qiskit_machine_learning.utils import algorithm_globals from qiskit_machine_learning.algorithms import QBayesian @@ -210,194 +208,5 @@ def test_trivial_circuit(self): ) ) - -class TestQBayesianInferenceV2(QiskitMachineLearningTestCase): - """Test QBayesianInference Algorithm V2""" - - backend = GenericBackendV2(num_qubits=3) - session = Session(backend=backend) - _sampler = SamplerV2(mode=session) - _sampler.options.default_shots = 2**7 - - def setUp(self): - super().setUp() - algorithm_globals.random_seed = 10598 - # Quantum Bayesian inference - qc = self._create_bayes_net() - self.qbayesian = QBayesian(qc, sampler=self._sampler) - - def _create_bayes_net(self): - # Probabilities - theta_a = 2 * np.arcsin(np.sqrt(0.25)) - theta_b_na = 2 * np.arcsin(np.sqrt(0.6)) - theta_b_a = 2 * np.arcsin(np.sqrt(0.7)) - theta_c_nbna = 2 * np.arcsin(np.sqrt(0.1)) - theta_c_nba = 2 * np.arcsin(np.sqrt(0.55)) - theta_c_bna = 2 * np.arcsin(np.sqrt(0.7)) - theta_c_ba = 2 * np.arcsin(np.sqrt(0.9)) - # Random variables - qr_a = QuantumRegister(1, name="A") - qr_b = QuantumRegister(1, name="B") - qr_c = QuantumRegister(1, name="C") - # Define a 3-qubit quantum circuit - qc = QuantumCircuit(qr_a, qr_b, qr_c, name="Bayes net") - # P(A) - qc.ry(theta_a, 0) - # P(B|-A) - qc.x(0) - qc.cry(theta_b_na, qr_a, qr_b) - qc.x(0) - # P(B|A) - qc.cry(theta_b_a, qr_a, qr_b) - # P(C|-B,-A) - qc.x(0) - qc.x(1) - qc.mcry(theta_c_nbna, [qr_a[0], qr_b[0]], qr_c[0]) - qc.x(0) - qc.x(1) - # P(C|-B,A) - qc.x(1) - qc.mcry(theta_c_nba, [qr_a[0], qr_b[0]], qr_c[0]) - qc.x(1) - # P(C|B,-A) - qc.x(0) - qc.mcry(theta_c_bna, [qr_a[0], qr_b[0]], qr_c[0]) - qc.x(0) - # P(C|B,A) - qc.mcry(theta_c_ba, [qr_a[0], qr_b[0]], qr_c[0]) - return qc - - def test_rejection_sampling(self): - """Test rejection sampling with different amount of evidence""" - test_cases = [{"A": 0, "B": 0}, {"A": 0}, {}] - true_res = [ - {"000": 0.9, "100": 0.1}, - {"000": 0.36, "100": 0.04, "010": 0.18, "110": 0.42}, - { - "000": 0.27, - "001": 0.03375, - "010": 0.135, - "011": 0.0175, - "100": 0.03, - "101": 0.04125, - "110": 0.315, - "111": 0.1575, - }, - ] - for evd, res in zip(test_cases, true_res): - samples = self.qbayesian.rejection_sampling(evidence=evd) - self.assertTrue( - np.all( - [ - np.isclose(res[sample_key], sample_val, atol=0.08) - for sample_key, sample_val in samples.items() - ] - ) - ) - - def test_rejection_sampling_format_res(self): - """Test rejection sampling with different result format""" - test_cases = [{"A": 0, "C": 1}, {"C": 1}, {}] - true_res = [ - {"P(B=0|A=0,C=1)", "P(B=1|A=0,C=1)"}, - {"P(A=0,B=0|C=1)", "P(A=1,B=0|C=1)", "P(A=0,B=1|C=1)", "P(A=1,B=1|C=1)"}, - { - "P(A=0,B=0,C=0)", - "P(A=1,B=0,C=0)", - "P(A=0,B=1,C=0)", - "P(A=1,B=1,C=0)", - "P(A=0,B=0,C=1)", - "P(A=1,B=0,C=1)", - "P(A=0,B=1,C=1)", - "P(A=1,B=1,C=1)", - }, - ] - for evd, res in zip(test_cases, true_res): - self.assertTrue( - res == set(self.qbayesian.rejection_sampling(evidence=evd, format_res=True).keys()) - ) - - def test_inference(self): - """Test inference with different amount of evidence""" - test_q_1, test_e_1 = ({"B": 1}, {"A": 1, "C": 1}) - test_q_2 = {"B": 0} - test_q_3 = {} - test_q_4, test_e_4 = ({"B": 1}, {"A": 0}) - true_res = [0.79, 0.21, 1, 0.6] - res = [] - samples = [] - # 1. Query basic inference - res.append(self.qbayesian.inference(query=test_q_1, evidence=test_e_1)) - samples.append(self.qbayesian.samples) - # 2. Query basic inference - res.append(self.qbayesian.inference(query=test_q_2)) - samples.append(self.qbayesian.samples) - # 3. Query marginalized inference - res.append(self.qbayesian.inference(query=test_q_3)) - samples.append(self.qbayesian.samples) - # 4. Query marginalized inference - res.append(self.qbayesian.inference(query=test_q_4, evidence=test_e_4)) - # Correct inference - np.testing.assert_allclose(true_res, res, atol=0.04) - # No change in samples - self.assertTrue(samples[0] == samples[1]) - - def test_parameter(self): - """Tests parameter of methods""" - # Test set threshold - self.qbayesian.threshold = 0.9 - self.qbayesian.rejection_sampling(evidence={"A": 1}) - self.assertTrue(self.qbayesian.threshold == 0.9) - # Test set limit - # Not converged - self.qbayesian.limit = 0 - self.qbayesian.rejection_sampling(evidence={"B": 1}) - self.assertFalse(self.qbayesian.converged) - self.assertTrue(self.qbayesian.limit == 0) - # Converged - self.qbayesian.limit = 1 - self.qbayesian.rejection_sampling(evidence={"B": 1}) - self.assertTrue(self.qbayesian.converged) - self.assertTrue(self.qbayesian.limit == 1) - # Test sampler - sampler = copy.deepcopy(self._sampler) - self.qbayesian.sampler = sampler - self.qbayesian.inference(query={"B": 1}, evidence={"A": 0, "C": 0}) - self.assertTrue(self.qbayesian.sampler == sampler) - # Create a quantum circuit with a register that has more than one qubit - with self.assertRaises(ValueError, msg="No ValueError in constructor with invalid input."): - QBayesian(QuantumCircuit(QuantumRegister(2, "qr"))) - # Test invalid inference without evidence or generated samples - with self.assertRaises(ValueError, msg="No ValueError in inference with invalid input."): - QBayesian(QuantumCircuit(QuantumRegister(1, "qr"))).inference({"A": 0}) - - def test_trivial_circuit(self): - """Tests trivial quantum circuit""" - # Define rotation angles - theta_a = 2 * np.arcsin(np.sqrt(0.2)) - theta_b_a = 2 * np.arcsin(np.sqrt(0.9)) - theta_b_na = 2 * np.arcsin(np.sqrt(0.3)) - # Define quantum registers - qr_a = QuantumRegister(1, name="A") - qr_b = QuantumRegister(1, name="B") - # Define a 2-qubit quantum circuit - qc = QuantumCircuit(qr_a, qr_b, name="Bayes net small") - qc.ry(theta_a, 0) - qc.cry(theta_b_a, control_qubit=qr_a, target_qubit=qr_b) - qc.x(0) - qc.cry(theta_b_na, control_qubit=qr_a, target_qubit=qr_b) - qc.x(0) - # Inference - self.assertTrue( - np.all( - np.isclose( - 0.1, - QBayesian(circuit=qc).inference(query={"B": 0}, evidence={"A": 1}), - atol=0.04, - ) - ) - ) - - if __name__ == "__main__": unittest.main() From 45bc6f8d4c356515ed04db0d137f60702f7d4661 Mon Sep 17 00:00:00 2001 From: oscar-wallis Date: Fri, 8 Nov 2024 14:46:16 +0000 Subject: [PATCH 71/85] Bugfixing the test_gradient --- .../gradients/base/base_estimator_gradient.py | 2 +- .../state_fidelities/compute_uncompute.py | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/qiskit_machine_learning/gradients/base/base_estimator_gradient.py b/qiskit_machine_learning/gradients/base/base_estimator_gradient.py index 74dc96ffd..7b8fff691 100644 --- a/qiskit_machine_learning/gradients/base/base_estimator_gradient.py +++ b/qiskit_machine_learning/gradients/base/base_estimator_gradient.py @@ -49,9 +49,9 @@ class BaseEstimatorGradient(ABC): def __init__( self, estimator: BaseEstimator | BaseEstimatorV2, - pass_manager: BasePassManager | None = None, options: Options | None = None, derivative_type: DerivativeType = DerivativeType.REAL, + pass_manager: BasePassManager | None = None, ): r""" Args: diff --git a/qiskit_machine_learning/state_fidelities/compute_uncompute.py b/qiskit_machine_learning/state_fidelities/compute_uncompute.py index 60b981160..19d8873f2 100644 --- a/qiskit_machine_learning/state_fidelities/compute_uncompute.py +++ b/qiskit_machine_learning/state_fidelities/compute_uncompute.py @@ -228,7 +228,14 @@ def _call( if local: raw_fidelities = [ - ComputeUncompute._get_local_fidelity(prob_dist, num_virtual_qubits if isinstance(_sampler, BaseSamplerV2) else circuit.num_qubits) + ComputeUncompute._get_local_fidelity( + prob_dist, + ( + num_virtual_qubits + if isinstance(_sampler, BaseSamplerV2) + else circuit.num_qubits + ), + ) for prob_dist, circuit in zip(quasi_dists, circuits) ] else: From e69c03da5837411f7a7897aefaf0c335b1440e7a Mon Sep 17 00:00:00 2001 From: oscar-wallis Date: Fri, 8 Nov 2024 14:56:09 +0000 Subject: [PATCH 72/85] Fixing an Options error with sampler_gradient --- .../gradients/base/base_sampler_gradient.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit_machine_learning/gradients/base/base_sampler_gradient.py b/qiskit_machine_learning/gradients/base/base_sampler_gradient.py index eaee27945..77f67dedc 100644 --- a/qiskit_machine_learning/gradients/base/base_sampler_gradient.py +++ b/qiskit_machine_learning/gradients/base/base_sampler_gradient.py @@ -45,8 +45,8 @@ class BaseSamplerGradient(ABC): def __init__( self, sampler: BaseSampler, - len_quasi_dist: int | None = None, options: Options | None = None, + len_quasi_dist: int | None = None, pass_manager: BasePassManager | None = None, ): """ @@ -58,9 +58,9 @@ def __init__( Higher priority setting overrides lower priority setting """ self._sampler: BaseSampler = sampler - self._default_options = Options() self._pass_manager = pass_manager self._len_quasi_dist = len_quasi_dist + self._default_options = Options() if options is not None: self._default_options.update_options(**options) self._gradient_circuit_cache: dict[tuple, GradientCircuit] = {} From bd417787c2d790a8f024c3319bc7b832c397e791 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Fri, 8 Nov 2024 16:49:35 +0100 Subject: [PATCH 73/85] Linting and formatting --- qiskit_machine_learning/neural_networks/estimator_qnn.py | 4 +--- test/algorithms/inference/test_qbayesian.py | 2 +- test/neural_networks/test_sampler_qnn.py | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/qiskit_machine_learning/neural_networks/estimator_qnn.py b/qiskit_machine_learning/neural_networks/estimator_qnn.py index 313f8a954..8c4d017d0 100644 --- a/qiskit_machine_learning/neural_networks/estimator_qnn.py +++ b/qiskit_machine_learning/neural_networks/estimator_qnn.py @@ -325,9 +325,7 @@ def _backward( if self._input_gradients: - job = self.gradient.run( - circuits, observables, param_values - ) # type: ignore[arg-type] + job = self.gradient.run(circuits, observables, param_values) elif len(parameter_values[0]) > self._num_inputs: params = [self._circuit.parameters[self._num_inputs :]] * num_circuits diff --git a/test/algorithms/inference/test_qbayesian.py b/test/algorithms/inference/test_qbayesian.py index cae2a1b6d..d0b114b8d 100644 --- a/test/algorithms/inference/test_qbayesian.py +++ b/test/algorithms/inference/test_qbayesian.py @@ -15,7 +15,6 @@ import unittest from test import QiskitMachineLearningTestCase -import copy import numpy as np from qiskit import QuantumCircuit @@ -208,5 +207,6 @@ def test_trivial_circuit(self): ) ) + if __name__ == "__main__": unittest.main() diff --git a/test/neural_networks/test_sampler_qnn.py b/test/neural_networks/test_sampler_qnn.py index 09c0982c0..9651a93d4 100644 --- a/test/neural_networks/test_sampler_qnn.py +++ b/test/neural_networks/test_sampler_qnn.py @@ -106,7 +106,7 @@ def interpret_2d(x): self.backend = GenericBackendV2(num_qubits=8) self.session = Session(backend=self.backend) self.sampler_v2 = SamplerV2(mode=self.session) - + self.pm = None self.array_type = {True: SparseArray, False: np.ndarray} # pylint: disable=too-many-positional-arguments From 3622bc2d944c04d4460fcd80926ba059c2d36e9e Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Fri, 8 Nov 2024 16:58:49 +0100 Subject: [PATCH 74/85] Add reno --- ...v2-primitive-support-2cf30f1701c31d0f.yaml | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 releasenotes/notes/v2-primitive-support-2cf30f1701c31d0f.yaml diff --git a/releasenotes/notes/v2-primitive-support-2cf30f1701c31d0f.yaml b/releasenotes/notes/v2-primitive-support-2cf30f1701c31d0f.yaml new file mode 100644 index 000000000..7468771d4 --- /dev/null +++ b/releasenotes/notes/v2-primitive-support-2cf30f1701c31d0f.yaml @@ -0,0 +1,35 @@ +--- +features: + - | + **Support for V2 Primitives**: + The `EstimatorQNN` and `SamplerQNN` classes now support `V2` primitives + (`EstimatorV2` and `SamplerV2`), allowing direct execution on IBM Quantum backends. + This enhancement ensures compatibility with Qiskit IBM Runtime’s Primitive Unified + Block (PUB) requirements and instruction set architecture (ISA) constraints for + circuits and observables. Users can switch between `V1` primitives + and `V2` primitives from version `0.8`. From version `0.9`, V1 primitives will be + removed. + +upgrade: + - | + Users working with real backends are advised to migrate to `V2` primitives + (`EstimatorV2` and `SamplerV2`) to ensure compatibility with Qiskit IBM Runtime + hardware requirements. These `V2` primitives will become the standard in + the `0.8` release onwards, while `V1` primitives are deprecated. + +deprecations: + - | + **Deprecated V1 Primitives**: + The `V1` primitives (e.g., `EstimatorV1` and `SamplerV1`) are no longer compatible + with real quantum backends via Qiskit IBM Runtime. This update provides initial + transitional support, but `V1` primitives may be fully deprecated and removed in + version `0.9`. Users should adopt `V2` primitives for both local and hardware + executions to ensure long-term compatibility. + +known_issues: + - | + **Optimizer compatibility may be unstable**: + Current implementations of `EstimatorQNN` and `SamplerQNN` using `V2` primitives + may require further testing with optimizers, especially those depending on gradient + calculations. Users are advised to use optimizers with caution and report any + issues related to optimizer compatibility in Qiskit Machine Learning’s issue tracker. From 4844894688c37bdd2314d0cfc53d279f23893606 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Fri, 8 Nov 2024 17:10:52 +0100 Subject: [PATCH 75/85] Fix dict typing definition --- test/neural_networks/test_estimator_qnn_v2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/neural_networks/test_estimator_qnn_v2.py b/test/neural_networks/test_estimator_qnn_v2.py index 773f3fe33..4bb281035 100644 --- a/test/neural_networks/test_estimator_qnn_v2.py +++ b/test/neural_networks/test_estimator_qnn_v2.py @@ -182,7 +182,7 @@ class TestEstimatorQNNV2(QiskitMachineLearningTestCase): """EstimatorQNN Tests for estimator_v2. The correct references is obtained from EstimatorQNN""" - tolerance: dict[str:float] = dict(atol=3 * 1.0e-1, rtol=3 * 1.0e-1) + tolerance: dict[str, float] = dict(atol=3 * 1.0e-1, rtol=3 * 1.0e-1) backend = GenericBackendV2(num_qubits=2, seed=123) session = Session(backend=backend) From e386aaf7e0047adf64cd223f7c73b3fbbaef7be8 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Fri, 8 Nov 2024 17:27:07 +0100 Subject: [PATCH 76/85] Fix mypy --- .../gradients/base/base_estimator_gradient.py | 10 +++++----- .../gradients/lin_comb/lin_comb_estimator_gradient.py | 2 +- .../param_shift/param_shift_estimator_gradient.py | 2 +- .../param_shift/param_shift_sampler_gradient.py | 7 ++++++- .../gradients/spsa/spsa_estimator_gradient.py | 2 +- .../neural_networks/estimator_qnn.py | 6 +++--- .../state_fidelities/compute_uncompute.py | 2 +- 7 files changed, 18 insertions(+), 13 deletions(-) diff --git a/qiskit_machine_learning/gradients/base/base_estimator_gradient.py b/qiskit_machine_learning/gradients/base/base_estimator_gradient.py index ffc53b49e..64f1932d6 100644 --- a/qiskit_machine_learning/gradients/base/base_estimator_gradient.py +++ b/qiskit_machine_learning/gradients/base/base_estimator_gradient.py @@ -97,7 +97,7 @@ def run( self, circuits: Sequence[QuantumCircuit], observables: Sequence[BaseOperator], - parameter_values: Sequence[Sequence[float]], + parameter_values: Sequence[Sequence[float]] | np.ndarray, parameters: Sequence[Sequence[Parameter] | None] | None = None, **options, ) -> AlgorithmJob: @@ -162,7 +162,7 @@ def _run( self, circuits: Sequence[QuantumCircuit], observables: Sequence[BaseOperator], - parameter_values: Sequence[Sequence[float]], + parameter_values: Sequence[Sequence[float]] | np.ndarray, parameters: Sequence[Sequence[Parameter]], **options, ) -> EstimatorGradientResult: @@ -172,7 +172,7 @@ def _run( def _preprocess( self, circuits: Sequence[QuantumCircuit], - parameter_values: Sequence[Sequence[float]], + parameter_values: Sequence[Sequence[float]] | np.ndarray, parameters: Sequence[Sequence[Parameter]], supported_gates: Sequence[str], ) -> tuple[Sequence[QuantumCircuit], Sequence[Sequence[float]], Sequence[Sequence[Parameter]]]: @@ -214,7 +214,7 @@ def _postprocess( self, results: EstimatorGradientResult, circuits: Sequence[QuantumCircuit], - parameter_values: Sequence[Sequence[float]], + parameter_values: Sequence[Sequence[float]] | np.ndarray, parameters: Sequence[Sequence[Parameter]], ) -> EstimatorGradientResult: """Postprocess the gradients. This method computes the gradient of the original circuits @@ -274,7 +274,7 @@ def _postprocess( def _validate_arguments( circuits: Sequence[QuantumCircuit], observables: Sequence[BaseOperator], - parameter_values: Sequence[Sequence[float]], + parameter_values: Sequence[Sequence[float]] | np.ndarray, parameters: Sequence[Sequence[Parameter]], ) -> None: """Validate the arguments of the ``run`` method. diff --git a/qiskit_machine_learning/gradients/lin_comb/lin_comb_estimator_gradient.py b/qiskit_machine_learning/gradients/lin_comb/lin_comb_estimator_gradient.py index f7787f7e3..e70876a26 100644 --- a/qiskit_machine_learning/gradients/lin_comb/lin_comb_estimator_gradient.py +++ b/qiskit_machine_learning/gradients/lin_comb/lin_comb_estimator_gradient.py @@ -98,7 +98,7 @@ def _run( self, circuits: Sequence[QuantumCircuit], observables: Sequence[BaseOperator], - parameter_values: Sequence[Sequence[float]], + parameter_values: Sequence[Sequence[float]] | np.ndarray, parameters: Sequence[Sequence[Parameter]], **options, ) -> EstimatorGradientResult: diff --git a/qiskit_machine_learning/gradients/param_shift/param_shift_estimator_gradient.py b/qiskit_machine_learning/gradients/param_shift/param_shift_estimator_gradient.py index 827a8911c..c01d5d00e 100644 --- a/qiskit_machine_learning/gradients/param_shift/param_shift_estimator_gradient.py +++ b/qiskit_machine_learning/gradients/param_shift/param_shift_estimator_gradient.py @@ -63,7 +63,7 @@ def _run( self, circuits: Sequence[QuantumCircuit], observables: Sequence[BaseOperator], - parameter_values: Sequence[Sequence[float]], + parameter_values: Sequence[Sequence[float]] | np.ndarray, parameters: Sequence[Sequence[Parameter]], **options, ) -> EstimatorGradientResult: diff --git a/qiskit_machine_learning/gradients/param_shift/param_shift_sampler_gradient.py b/qiskit_machine_learning/gradients/param_shift/param_shift_sampler_gradient.py index e376755fa..53476bb3a 100644 --- a/qiskit_machine_learning/gradients/param_shift/param_shift_sampler_gradient.py +++ b/qiskit_machine_learning/gradients/param_shift/param_shift_sampler_gradient.py @@ -129,16 +129,21 @@ def _run_unique( if isinstance(self._sampler, BaseSamplerV1): result = results.quasi_dists[partial_sum_n : partial_sum_n + n] opt = self._get_local_options(options) + elif isinstance(self._sampler, BaseSamplerV2): result = [] for i in range(partial_sum_n, partial_sum_n + n): bitstring_counts = results[i].data.meas.get_counts() + # Normalize the counts to probabilities total_shots = sum(bitstring_counts.values()) probabilities = {k: v / total_shots for k, v in bitstring_counts.items()} + # Convert to quasi-probabilities counts = QuasiDistribution(probabilities) - result.append({k: v for k, v in counts.items() if int(k) < self.len_quasi_dist}) + result.append( + {k: v for k, v in counts.items() if int(k) < self._len_quasi_dist} + ) opt = options for dist_plus, dist_minus in zip(result[: n // 2], result[n // 2 :]): diff --git a/qiskit_machine_learning/gradients/spsa/spsa_estimator_gradient.py b/qiskit_machine_learning/gradients/spsa/spsa_estimator_gradient.py index c0387a201..8f524a0bf 100644 --- a/qiskit_machine_learning/gradients/spsa/spsa_estimator_gradient.py +++ b/qiskit_machine_learning/gradients/spsa/spsa_estimator_gradient.py @@ -75,7 +75,7 @@ def _run( self, circuits: Sequence[QuantumCircuit], observables: Sequence[BaseOperator], - parameter_values: Sequence[Sequence[float]], + parameter_values: Sequence[Sequence[float]] | np.ndarray, parameters: Sequence[Sequence[Parameter]], **options, ) -> EstimatorGradientResult: diff --git a/qiskit_machine_learning/neural_networks/estimator_qnn.py b/qiskit_machine_learning/neural_networks/estimator_qnn.py index 8c4d017d0..223717db7 100644 --- a/qiskit_machine_learning/neural_networks/estimator_qnn.py +++ b/qiskit_machine_learning/neural_networks/estimator_qnn.py @@ -265,10 +265,12 @@ def _forward( results = job.result().values elif isinstance(self.estimator, BaseEstimatorV2): + # Prepare circuit-observable-parameter tuples (PUBs) circuit_observable_params = [] for observable in self._observables: circuit_observable_params.append((self._circuit, observable, parameter_values_)) + # For BaseEstimatorV2, run the estimator using PUBs and specified precision job = self.estimator.run(circuit_observable_params, precision=self._default_precision) results = [result.data.evs for result in job.result()] @@ -329,9 +331,7 @@ def _backward( elif len(parameter_values[0]) > self._num_inputs: params = [self._circuit.parameters[self._num_inputs :]] * num_circuits - job = self.gradient.run( - circuits, observables, param_values, parameters=params # type: ignore[arg-type] - ) + job = self.gradient.run(circuits, observables, param_values, parameters=params) if job is not None: try: diff --git a/qiskit_machine_learning/state_fidelities/compute_uncompute.py b/qiskit_machine_learning/state_fidelities/compute_uncompute.py index e154579c4..04cfffd56 100644 --- a/qiskit_machine_learning/state_fidelities/compute_uncompute.py +++ b/qiskit_machine_learning/state_fidelities/compute_uncompute.py @@ -192,7 +192,7 @@ def _run( else: raise QiskitMachineLearningError( "The accepted estimators are BaseSamplerV1 (deprecated) and BaseSamplerV2; got" - + f" {type(self.sampler)} instead." + + f" {type(self._sampler)} instead." ) return AlgorithmJob( ComputeUncompute._call, From 2527ea758d3bbbb9877af5e662958d9b7ce14e0a Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Fri, 8 Nov 2024 18:17:00 +0100 Subject: [PATCH 77/85] Issue deprecation warnings --- .../algorithms/inference/qbayesian.py | 2 +- .../gradients/base/base_estimator_gradient.py | 11 ++- .../gradients/base/base_sampler_gradient.py | 11 ++- .../param_shift_estimator_gradient.py | 1 - .../param_shift_sampler_gradient.py | 1 - .../neural_networks/estimator_qnn.py | 11 ++- .../neural_networks/sampler_qnn.py | 10 ++- .../state_fidelities/compute_uncompute.py | 9 +- qiskit_machine_learning/utils/deprecation.py | 82 +++++++++++++++++++ ...v2-primitive-support-2cf30f1701c31d0f.yaml | 2 +- test/neural_networks/test_estimator_qnn_v2.py | 2 +- 11 files changed, 130 insertions(+), 12 deletions(-) create mode 100644 qiskit_machine_learning/utils/deprecation.py diff --git a/qiskit_machine_learning/algorithms/inference/qbayesian.py b/qiskit_machine_learning/algorithms/inference/qbayesian.py index 164cbe1ac..9a5bfbd31 100644 --- a/qiskit_machine_learning/algorithms/inference/qbayesian.py +++ b/qiskit_machine_learning/algorithms/inference/qbayesian.py @@ -95,7 +95,7 @@ def __init__( self._limit = limit self._threshold = threshold if sampler is None: - sampler = Sampler() + sampler = BaseSamplerV2() self._sampler = sampler if pass_manager is None: diff --git a/qiskit_machine_learning/gradients/base/base_estimator_gradient.py b/qiskit_machine_learning/gradients/base/base_estimator_gradient.py index 64f1932d6..bb85cd179 100644 --- a/qiskit_machine_learning/gradients/base/base_estimator_gradient.py +++ b/qiskit_machine_learning/gradients/base/base_estimator_gradient.py @@ -23,7 +23,7 @@ import numpy as np from qiskit.circuit import Parameter, ParameterExpression, QuantumCircuit -from qiskit.primitives import BaseEstimator +from qiskit.primitives import BaseEstimator, BaseEstimatorV1 from qiskit.primitives.base import BaseEstimatorV2 from qiskit.primitives.utils import _circuit_key from qiskit.providers import Options @@ -39,7 +39,7 @@ _make_gradient_parameters, _make_gradient_parameter_values, ) - +from ...utils.deprecation import issue_deprecation_msg from ...algorithm_job import AlgorithmJob @@ -72,6 +72,13 @@ def __init__( gradient and this type is the only supported type for function-level schemes like finite difference. """ + if isinstance(estimator, BaseEstimatorV1): + issue_deprecation_msg( + msg="V1 Primitives are deprecated", + version="0.8.0", + remedy="Use V2 primitives for continued compatibility and support.", + period="4 months", + ) self._estimator: BaseEstimator = estimator self._pass_manager = pass_manager self._default_options = Options() diff --git a/qiskit_machine_learning/gradients/base/base_sampler_gradient.py b/qiskit_machine_learning/gradients/base/base_sampler_gradient.py index 77f67dedc..3db0c3e31 100644 --- a/qiskit_machine_learning/gradients/base/base_sampler_gradient.py +++ b/qiskit_machine_learning/gradients/base/base_sampler_gradient.py @@ -22,7 +22,7 @@ from copy import copy from qiskit.circuit import Parameter, ParameterExpression, QuantumCircuit -from qiskit.primitives import BaseSampler +from qiskit.primitives import BaseSampler, BaseSamplerV1 from qiskit.primitives.utils import _circuit_key from qiskit.providers import Options from qiskit.transpiler.passes import TranslateParameterizedGates @@ -35,7 +35,7 @@ _make_gradient_parameters, _make_gradient_parameter_values, ) - +from ...utils.deprecation import issue_deprecation_msg from ...algorithm_job import AlgorithmJob @@ -57,6 +57,13 @@ def __init__( default options > primitive's default setting. Higher priority setting overrides lower priority setting """ + if isinstance(sampler, BaseSamplerV1): + issue_deprecation_msg( + msg="V1 Primitives are deprecated", + version="0.8.0", + remedy="Use V2 primitives for continued compatibility and support.", + period="4 months", + ) self._sampler: BaseSampler = sampler self._pass_manager = pass_manager self._len_quasi_dist = len_quasi_dist diff --git a/qiskit_machine_learning/gradients/param_shift/param_shift_estimator_gradient.py b/qiskit_machine_learning/gradients/param_shift/param_shift_estimator_gradient.py index c01d5d00e..8bbe5f051 100644 --- a/qiskit_machine_learning/gradients/param_shift/param_shift_estimator_gradient.py +++ b/qiskit_machine_learning/gradients/param_shift/param_shift_estimator_gradient.py @@ -28,7 +28,6 @@ from ..base.base_estimator_gradient import BaseEstimatorGradient from ..base.estimator_gradient_result import EstimatorGradientResult from ..utils import _make_param_shift_parameter_values - from ...exceptions import QiskitMachineLearningError diff --git a/qiskit_machine_learning/gradients/param_shift/param_shift_sampler_gradient.py b/qiskit_machine_learning/gradients/param_shift/param_shift_sampler_gradient.py index 53476bb3a..f327b6453 100644 --- a/qiskit_machine_learning/gradients/param_shift/param_shift_sampler_gradient.py +++ b/qiskit_machine_learning/gradients/param_shift/param_shift_sampler_gradient.py @@ -27,7 +27,6 @@ from ..base.base_sampler_gradient import BaseSamplerGradient from ..base.sampler_gradient_result import SamplerGradientResult from ..utils import _make_param_shift_parameter_values - from ...exceptions import AlgorithmError, QiskitMachineLearningError diff --git a/qiskit_machine_learning/neural_networks/estimator_qnn.py b/qiskit_machine_learning/neural_networks/estimator_qnn.py index 223717db7..36b3d92ce 100644 --- a/qiskit_machine_learning/neural_networks/estimator_qnn.py +++ b/qiskit_machine_learning/neural_networks/estimator_qnn.py @@ -26,6 +26,7 @@ from qiskit.quantum_info import SparsePauliOp from qiskit.quantum_info.operators.base_operator import BaseOperator + from ..gradients import ( BaseEstimatorGradient, EstimatorGradientResult, @@ -34,7 +35,7 @@ from ..circuit.library import QNNCircuit from ..exceptions import QiskitMachineLearningError - +from ..utils.deprecation import issue_deprecation_msg from .neural_network import NeuralNetwork logger = logging.getLogger(__name__) @@ -154,6 +155,14 @@ def __init__( """ if estimator is None: estimator = Estimator() + + if isinstance(estimator, BaseEstimatorV1): + issue_deprecation_msg( + msg="V1 Primitives are deprecated", + version="0.8.0", + remedy="Use V2 primitives for continued compatibility and support.", + period="4 months", + ) self.estimator = estimator self._org_circuit = circuit diff --git a/qiskit_machine_learning/neural_networks/sampler_qnn.py b/qiskit_machine_learning/neural_networks/sampler_qnn.py index 6c2e81ea6..bb5ca4023 100644 --- a/qiskit_machine_learning/neural_networks/sampler_qnn.py +++ b/qiskit_machine_learning/neural_networks/sampler_qnn.py @@ -34,7 +34,7 @@ ) from ..circuit.library import QNNCircuit from ..exceptions import QiskitMachineLearningError - +from ..utils.deprecation import issue_deprecation_msg from .neural_network import NeuralNetwork @@ -176,6 +176,14 @@ def __init__( # set primitive, provide default if sampler is None: sampler = Sampler() + + if isinstance(sampler, BaseSamplerV1): + issue_deprecation_msg( + msg="V1 Primitives are deprecated", + version="0.8.0", + remedy="Use V2 primitives for continued compatibility and support.", + period="4 months", + ) self.sampler = sampler if num_virtual_qubits is None: diff --git a/qiskit_machine_learning/state_fidelities/compute_uncompute.py b/qiskit_machine_learning/state_fidelities/compute_uncompute.py index 04cfffd56..03a9d7354 100644 --- a/qiskit_machine_learning/state_fidelities/compute_uncompute.py +++ b/qiskit_machine_learning/state_fidelities/compute_uncompute.py @@ -26,7 +26,7 @@ from qiskit.providers import Options from ..exceptions import AlgorithmError, QiskitMachineLearningError - +from ..utils.deprecation import issue_deprecation_msg from .base_state_fidelity import BaseStateFidelity from .state_fidelity_result import StateFidelityResult from ..algorithm_job import AlgorithmJob @@ -101,6 +101,13 @@ def __init__( raise ValueError( f"Number of virtual qubits should be provided for {type(pass_manager)}." ) + if isinstance(sampler, BaseSamplerV1): + issue_deprecation_msg( + msg="V1 Primitives are deprecated", + version="0.8.0", + remedy="Use V2 primitives for continued compatibility and support.", + period="4 months", + ) self._sampler: BaseSampler = sampler self.num_virtual_qubits = num_virtual_qubits self.pass_manager = pass_manager diff --git a/qiskit_machine_learning/utils/deprecation.py b/qiskit_machine_learning/utils/deprecation.py new file mode 100644 index 000000000..682d71b8c --- /dev/null +++ b/qiskit_machine_learning/utils/deprecation.py @@ -0,0 +1,82 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2024. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Deprecation utilities""" + +from typing import Callable, Any +import functools +import warnings + + +def deprecate_function(deprecated: str, version: str, remedy: str, stacklevel: int = 2) -> Callable: + """Emit a warning prior to calling decorated function. + Args: + deprecated: Function being deprecated. + version: First release the function is deprecated. + remedy: User action to take. + stacklevel: The warning stackevel to use. + + Returns: + The decorated, deprecated callable. + """ + + def decorator(func: Callable) -> Callable: + @functools.wraps(func) + def wrapper(*args: Any, **kwargs: Any) -> Callable: + issue_deprecation_msg( + f"The {deprecated} method is deprecated", + version, + remedy, + stacklevel + 1, + ) + return func(*args, **kwargs) + + return wrapper + + return decorator + + +def deprecate_arguments(deprecated: str, version: str, remedy: str, stacklevel: int = 2) -> None: + """Emit a warning about deprecated keyword arguments. + + Args: + deprecated: Keyword arguments being deprecated. + version: First release the function is deprecated. + remedy: User action to take. + stacklevel: The warning stackevel to use. + """ + issue_deprecation_msg( + f"The '{deprecated}' keyword arguments are deprecated", + version, + remedy, + stacklevel + 1, + ) + + +def issue_deprecation_msg( + msg: str, version: str, remedy: str, stacklevel: int = 2, period: str = "3 months" +) -> None: + """Emit a deprecation warning. + + Args: + msg: Deprecation message. + version: First release the function is deprecated. + remedy: User action to take. + stacklevel: The warning stackevel to use. + period: Deprecation period. + """ + warnings.warn( + f"{msg} as of qiskit-machine-learning {version} " + f"and will be removed no sooner than {period} after the release date. {remedy}", + DeprecationWarning, + stacklevel=stacklevel + 1, # Increment to account for this function. + ) diff --git a/releasenotes/notes/v2-primitive-support-2cf30f1701c31d0f.yaml b/releasenotes/notes/v2-primitive-support-2cf30f1701c31d0f.yaml index 7468771d4..17d00b5aa 100644 --- a/releasenotes/notes/v2-primitive-support-2cf30f1701c31d0f.yaml +++ b/releasenotes/notes/v2-primitive-support-2cf30f1701c31d0f.yaml @@ -15,7 +15,7 @@ upgrade: Users working with real backends are advised to migrate to `V2` primitives (`EstimatorV2` and `SamplerV2`) to ensure compatibility with Qiskit IBM Runtime hardware requirements. These `V2` primitives will become the standard in - the `0.8` release onwards, while `V1` primitives are deprecated. + the `0.8` release going forward, while `V1` primitives are deprecated. deprecations: - | diff --git a/test/neural_networks/test_estimator_qnn_v2.py b/test/neural_networks/test_estimator_qnn_v2.py index 4bb281035..37140ed4f 100644 --- a/test/neural_networks/test_estimator_qnn_v2.py +++ b/test/neural_networks/test_estimator_qnn_v2.py @@ -481,6 +481,7 @@ def test_setters_getters(self): estimator_qnn.input_gradients = True self.assertTrue(estimator_qnn.input_gradients) + @unittest.skip def test_qnn_qc_circuit_construction(self): """Test Estimator QNN properties and forward/backward pass for QNNCircuit construction""" num_qubits = 2 @@ -544,7 +545,6 @@ def test_binding_order(self): for i in range(qc.num_qubits): qc.rx(weight, i) isa_qc = self.pm.run(qc) - print(isa_qc) op = SparsePauliOp.from_list([("Z" * isa_qc.num_qubits, 1)]) op = op.apply_layout(isa_qc.layout) estimator_qnn = EstimatorQNN( From 6c6efc50ebeadc75e7eed952e0f4cada43cd4adb Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Fri, 8 Nov 2024 18:21:50 +0100 Subject: [PATCH 78/85] Update skip test message --- test/neural_networks/test_estimator_qnn_v2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/neural_networks/test_estimator_qnn_v2.py b/test/neural_networks/test_estimator_qnn_v2.py index 37140ed4f..b8fad6557 100644 --- a/test/neural_networks/test_estimator_qnn_v2.py +++ b/test/neural_networks/test_estimator_qnn_v2.py @@ -481,7 +481,7 @@ def test_setters_getters(self): estimator_qnn.input_gradients = True self.assertTrue(estimator_qnn.input_gradients) - @unittest.skip + @unittest.skip("Test unstable, to be checked.") def test_qnn_qc_circuit_construction(self): """Test Estimator QNN properties and forward/backward pass for QNNCircuit construction""" num_qubits = 2 From da04d85e4e2422bbf18afde04a5954bb19e6f8a4 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Fri, 8 Nov 2024 18:30:06 +0100 Subject: [PATCH 79/85] Update deprecation warning for qbayesian.py --- .../algorithms/inference/qbayesian.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/qiskit_machine_learning/algorithms/inference/qbayesian.py b/qiskit_machine_learning/algorithms/inference/qbayesian.py index 9a5bfbd31..964c8249b 100644 --- a/qiskit_machine_learning/algorithms/inference/qbayesian.py +++ b/qiskit_machine_learning/algorithms/inference/qbayesian.py @@ -20,11 +20,12 @@ from qiskit.quantum_info import Statevector from qiskit.circuit import Qubit from qiskit.circuit.library import GroverOperator -from qiskit.primitives import BaseSampler, Sampler, BaseSamplerV2 +from qiskit.primitives import BaseSampler, Sampler, BaseSamplerV2, BaseSamplerV1 from qiskit.transpiler.passmanager import BasePassManager from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager from qiskit.providers.fake_provider import GenericBackendV2 +from ...utils.deprecation import issue_deprecation_msg class QBayesian: r""" @@ -95,7 +96,16 @@ def __init__( self._limit = limit self._threshold = threshold if sampler is None: - sampler = BaseSamplerV2() + sampler = Sampler() + + if isinstance(sampler, BaseSamplerV1): + issue_deprecation_msg( + msg="V1 Primitives are deprecated", + version="0.8.0", + remedy="Use V2 primitives for continued compatibility and support.", + period="4 months", + ) + self._sampler = sampler if pass_manager is None: From 94722611f59bfe7d191e4d98ecd55f2b37cea787 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Fri, 8 Nov 2024 18:32:32 +0100 Subject: [PATCH 80/85] Update deprecation warning for qbayesian.py --- qiskit_machine_learning/algorithms/inference/qbayesian.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qiskit_machine_learning/algorithms/inference/qbayesian.py b/qiskit_machine_learning/algorithms/inference/qbayesian.py index 964c8249b..2d3736eac 100644 --- a/qiskit_machine_learning/algorithms/inference/qbayesian.py +++ b/qiskit_machine_learning/algorithms/inference/qbayesian.py @@ -27,6 +27,7 @@ from ...utils.deprecation import issue_deprecation_msg + class QBayesian: r""" Implements a quantum Bayesian inference (QBI) algorithm that has been developed in [1]. The From fc716fe020a0bc56dfa5f265ceb18788397d02b8 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Fri, 8 Nov 2024 18:38:29 +0100 Subject: [PATCH 81/85] Add headers in deprecation.py --- qiskit_machine_learning/utils/deprecation.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qiskit_machine_learning/utils/deprecation.py b/qiskit_machine_learning/utils/deprecation.py index 682d71b8c..84f82d6a8 100644 --- a/qiskit_machine_learning/utils/deprecation.py +++ b/qiskit_machine_learning/utils/deprecation.py @@ -30,8 +30,10 @@ def deprecate_function(deprecated: str, version: str, remedy: str, stacklevel: i """ def decorator(func: Callable) -> Callable: + """Emit a deprecation warning.""" @functools.wraps(func) def wrapper(*args: Any, **kwargs: Any) -> Callable: + """Emit a deprecation warning.""" issue_deprecation_msg( f"The {deprecated} method is deprecated", version, From e0c6b7d5ca359504ad988236561862fb5cde2396 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Fri, 8 Nov 2024 18:44:42 +0100 Subject: [PATCH 82/85] Add headers in deprecation.py --- qiskit_machine_learning/utils/deprecation.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/qiskit_machine_learning/utils/deprecation.py b/qiskit_machine_learning/utils/deprecation.py index 84f82d6a8..b3f1f0040 100644 --- a/qiskit_machine_learning/utils/deprecation.py +++ b/qiskit_machine_learning/utils/deprecation.py @@ -1,6 +1,6 @@ -# This code is part of Qiskit. +# This code is part of a Qiskit project. # -# (C) Copyright IBM 2024. +# (C) Copyright IBM 2024, 2024. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -9,7 +9,6 @@ # Any modifications or derivative works of this code must retain this # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. - """Deprecation utilities""" from typing import Callable, Any From 0f09f4f1789d0b3404f090695593c4714e1fb9b5 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Fri, 8 Nov 2024 18:45:48 +0100 Subject: [PATCH 83/85] Add headers in deprecation.py --- qiskit_machine_learning/utils/deprecation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qiskit_machine_learning/utils/deprecation.py b/qiskit_machine_learning/utils/deprecation.py index b3f1f0040..5826aade6 100644 --- a/qiskit_machine_learning/utils/deprecation.py +++ b/qiskit_machine_learning/utils/deprecation.py @@ -30,6 +30,7 @@ def deprecate_function(deprecated: str, version: str, remedy: str, stacklevel: i def decorator(func: Callable) -> Callable: """Emit a deprecation warning.""" + @functools.wraps(func) def wrapper(*args: Any, **kwargs: Any) -> Callable: """Emit a deprecation warning.""" From 6a44cbe07ce5072b0b6ef511bf13347593f18710 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Fri, 8 Nov 2024 18:54:53 +0100 Subject: [PATCH 84/85] Correct spelling --- .pylintdict | 1 + qiskit_machine_learning/utils/deprecation.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.pylintdict b/.pylintdict index 93892d47d..a8f524b2f 100644 --- a/.pylintdict +++ b/.pylintdict @@ -501,6 +501,7 @@ sparsearray spedalieri spsa sqrt +stacklevel statefn statevector statevectors diff --git a/qiskit_machine_learning/utils/deprecation.py b/qiskit_machine_learning/utils/deprecation.py index 5826aade6..14fb89ecb 100644 --- a/qiskit_machine_learning/utils/deprecation.py +++ b/qiskit_machine_learning/utils/deprecation.py @@ -22,7 +22,7 @@ def deprecate_function(deprecated: str, version: str, remedy: str, stacklevel: i deprecated: Function being deprecated. version: First release the function is deprecated. remedy: User action to take. - stacklevel: The warning stackevel to use. + stacklevel: The warning stack-level to use. Returns: The decorated, deprecated callable. @@ -54,7 +54,7 @@ def deprecate_arguments(deprecated: str, version: str, remedy: str, stacklevel: deprecated: Keyword arguments being deprecated. version: First release the function is deprecated. remedy: User action to take. - stacklevel: The warning stackevel to use. + stacklevel: The warning stack-level to use. """ issue_deprecation_msg( f"The '{deprecated}' keyword arguments are deprecated", @@ -73,7 +73,7 @@ def issue_deprecation_msg( msg: Deprecation message. version: First release the function is deprecated. remedy: User action to take. - stacklevel: The warning stackevel to use. + stacklevel: The warning stack-level to use. period: Deprecation period. """ warnings.warn( From 5d653b02a89030fb3f52c6f9b07398a1c8d2d9c6 Mon Sep 17 00:00:00 2001 From: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com> Date: Fri, 8 Nov 2024 19:08:31 +0100 Subject: [PATCH 85/85] Add spelling `msg` --- .pylintdict | 1 + 1 file changed, 1 insertion(+) diff --git a/.pylintdict b/.pylintdict index a8f524b2f..70e2bb17f 100644 --- a/.pylintdict +++ b/.pylintdict @@ -312,6 +312,7 @@ monte mosca mpl mprev +msg multiclass multinomial multioutput