From 9186aa1749472e50eedfaa12b44c64f97965a229 Mon Sep 17 00:00:00 2001 From: Maxence Drutel Date: Wed, 29 Nov 2023 11:55:09 +0100 Subject: [PATCH 1/3] remote: Allow passing of options in get_backend() When we introduced the local provider, we made it so we declare its options (nb_photons, kappa_1...) when using get_backend(). This is contradictory to what is happening in the remote provider, where options must be passed in execute(). This commit harmonizes this behaviour by allowing to set the options in get_backend() for the remote provider. (which actually makes some sense to do it like this when we want to set the nb_photons for example). This allows to quickly swap between the local and remote provider. We aim to stay as permissive as possible, by still letting the user passing options at execute() for the remote provider. --- qiskit_alice_bob_provider/remote/backend.py | 14 +++++--- qiskit_alice_bob_provider/remote/provider.py | 8 +++++ tests/remote/test_against_real_server.py | 6 ++-- tests/remote/test_backend.py | 36 +++++++++++++++++++- 4 files changed, 56 insertions(+), 8 deletions(-) diff --git a/qiskit_alice_bob_provider/remote/backend.py b/qiskit_alice_bob_provider/remote/backend.py index 829510a..7b5b0e3 100644 --- a/qiskit_alice_bob_provider/remote/backend.py +++ b/qiskit_alice_bob_provider/remote/backend.py @@ -67,6 +67,14 @@ def get_translation_stage_plugin(self): in translation_plugin.StatePreparationPlugin""" return self._translation_plugin + def update_options(self, option_updates: Dict[str, Any]) -> Options: + options: Options = self.options + for key, value in option_updates.items(): + if not hasattr(options, key): + raise ValueError(f'Backend does not support option "{key}"') + options.update_options(**{key: value}) + return options + def run(self, run_input: QuantumCircuit, **kwargs) -> AliceBobRemoteJob: """Run the quantum circuit on the Alice & Bob backend by calling the Alice & Bob API. @@ -83,11 +91,7 @@ def run(self, run_input: QuantumCircuit, **kwargs) -> AliceBobRemoteJob: Wait for the results by calling :func:`AliceBobRemoteJob.result`. """ - options: Options = self.options - for key, value in kwargs.items(): - if not hasattr(options, key): - raise ValueError(f'Backend does not support option "{key}"') - options.update_options(**{key: value}) + options = self.update_options(kwargs) input_params = _ab_input_params_from_options(options) job = jobs.create_job(self._api_client, self.name, input_params) run_input = PassManager([EnsurePreparationPass()]).run(run_input) diff --git a/qiskit_alice_bob_provider/remote/provider.py b/qiskit_alice_bob_provider/remote/provider.py index a92e3d1..bca015b 100644 --- a/qiskit_alice_bob_provider/remote/provider.py +++ b/qiskit_alice_bob_provider/remote/provider.py @@ -52,6 +52,14 @@ def __init__( for ab_target in list_targets(client): self._backends.append(AliceBobRemoteBackend(client, ab_target)) + def get_backend(self, name=None, **kwargs) -> AliceBobRemoteBackend: + backend = super().get_backend(name) + # We allow to set the options when getting the backend, + # to align with what we do in the local provider. + if kwargs: + backend.update_options(kwargs) + return backend + def backends( self, name: Optional[str] = None, **kwargs ) -> List[BackendV2]: diff --git a/tests/remote/test_against_real_server.py b/tests/remote/test_against_real_server.py index 94eda73..69447da 100644 --- a/tests/remote/test_against_real_server.py +++ b/tests/remote/test_against_real_server.py @@ -34,7 +34,9 @@ def test_happy_path(base_url: str, api_key: str) -> None: api_key=api_key, url=base_url, ) - backend = provider.get_backend('EMU:1Q:LESCANNE_2020') - job = execute(c, backend) + backend = provider.get_backend( + 'EMU:1Q:LESCANNE_2020', average_nb_photons=6.0 + ) + job = execute(c, backend, shots=100) res = job.result() res.get_counts() diff --git a/tests/remote/test_backend.py b/tests/remote/test_backend.py index 1e634d7..dd274f7 100644 --- a/tests/remote/test_backend.py +++ b/tests/remote/test_backend.py @@ -28,13 +28,47 @@ from qiskit_alice_bob_provider.remote.api.client import AliceBobApiException from qiskit_alice_bob_provider.remote.backend import ( + AliceBobRemoteBackend, _ab_input_params_from_options, _qiskit_to_qir, ) from qiskit_alice_bob_provider.remote.provider import AliceBobRemoteProvider -def test_options_validation(mocked_targets) -> None: +def test_get_backend(mocked_targets) -> None: + provider = AliceBobRemoteProvider(api_key='foo') + backend = provider.get_backend('EMU:1Q:LESCANNE_2020') + assert isinstance(backend, AliceBobRemoteBackend) + assert backend.options['average_nb_photons'] == 4.0 # Default value. + + +def test_get_backend_with_options(mocked_targets) -> None: + provider = AliceBobRemoteProvider(api_key='foo') + backend = provider.get_backend( + 'EMU:1Q:LESCANNE_2020', average_nb_photons=6.0 + ) + assert isinstance(backend, AliceBobRemoteBackend) + assert backend.options['average_nb_photons'] == 6.0 + + +def test_get_backend_options_validation(mocked_targets) -> None: + provider = AliceBobRemoteProvider(api_key='foo') + with pytest.raises(ValueError): + provider.get_backend('EMU:1Q:LESCANNE_2020', average_nb_photons=40) + with pytest.raises(ValueError): + provider.get_backend('EMU:1Q:LESCANNE_2020', average_nb_photons=-1) + with pytest.raises(ValueError): + provider.get_backend('EMU:1Q:LESCANNE_2020', shots=0) + with pytest.raises(ValueError): + provider.get_backend('EMU:1Q:LESCANNE_2020', shots=1e10) + with pytest.raises(ValueError): + provider.get_backend('EMU:1Q:LESCANNE_2020', bad_option=1) + + +def test_execute_options_validation(mocked_targets) -> None: + # We are permissive in our options sytem, allowing the user to both + # define options when creating the backend and executing. + # We therefore need to test both behaviors. c = QuantumCircuit(1, 1) provider = AliceBobRemoteProvider(api_key='foo') backend = provider.get_backend('EMU:1Q:LESCANNE_2020') From 565c434fadabc4619a671422f35f7e603e530496 Mon Sep 17 00:00:00 2001 From: Maxence Drutel Date: Wed, 29 Nov 2023 15:56:03 +0100 Subject: [PATCH 2/3] remote: Use Pydantic's camel & snake case functions This is a fix where we found out the "kappa1" parameter of targets was not converted to "kappa_1" in snake_case when getting a backend as it should. This came from our home made to_snake_case and to_camel_case functions using some obscure regex. Instead, this commit replaces these functions with Pydantic's, that are more tested and proven. --- qiskit_alice_bob_provider/remote/backend.py | 6 ++--- qiskit_alice_bob_provider/remote/utils.py | 28 --------------------- tests/remote/test_utils.py | 14 ----------- 3 files changed, 3 insertions(+), 45 deletions(-) delete mode 100644 qiskit_alice_bob_provider/remote/utils.py delete mode 100644 tests/remote/test_utils.py diff --git a/qiskit_alice_bob_provider/remote/backend.py b/qiskit_alice_bob_provider/remote/backend.py index 7b5b0e3..ce17d47 100644 --- a/qiskit_alice_bob_provider/remote/backend.py +++ b/qiskit_alice_bob_provider/remote/backend.py @@ -16,6 +16,7 @@ from typing import Any, Dict +from pydantic.alias_generators import to_camel, to_snake from qiskit import QuantumCircuit from qiskit.providers import BackendV2, Options from qiskit.transpiler import PassManager, Target @@ -26,7 +27,6 @@ from .api.client import ApiClient from .job import AliceBobRemoteJob from .qir_to_qiskit import ab_target_to_qiskit_target -from .utils import camel_to_snake_case, snake_to_camel_case class AliceBobRemoteBackend(BackendV2): @@ -115,7 +115,7 @@ def _options_from_ab_target(ab_target: Dict) -> Options: """Extract Qiskit options from an Alice & Bob target description""" options = Options() for camel_name, desc in ab_target['inputParams'].items(): - name = camel_to_snake_case(camel_name) + name = to_snake(camel_name) if name == 'nb_shots': # special case name = 'shots' options.update_options(**{name: desc['default']}) @@ -131,7 +131,7 @@ def _ab_input_params_from_options(options: Options) -> Dict: """Extract Qiskit options from an Alice & Bob target description""" input_params: Dict[str, Any] = {} for snake_name, value in options.__dict__.items(): - name = snake_to_camel_case(snake_name) + name = to_camel(snake_name) if name == 'shots': # special case name = 'nbShots' input_params[name] = value diff --git a/qiskit_alice_bob_provider/remote/utils.py b/qiskit_alice_bob_provider/remote/utils.py deleted file mode 100644 index d49bb4b..0000000 --- a/qiskit_alice_bob_provider/remote/utils.py +++ /dev/null @@ -1,28 +0,0 @@ -############################################################################## -# Copyright 2023 Alice & Bob -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -############################################################################## - -import re - -_CAMEL_PATTERN = re.compile(r'(? str: - return _CAMEL_PATTERN.sub('_', name).lower() - - -def snake_to_camel_case(name: str) -> str: - upper_camel = ''.join(x.capitalize() for x in name.lower().split('_')) - return upper_camel[0].lower() + upper_camel[1:] diff --git a/tests/remote/test_utils.py b/tests/remote/test_utils.py deleted file mode 100644 index 6037393..0000000 --- a/tests/remote/test_utils.py +++ /dev/null @@ -1,14 +0,0 @@ -from qiskit_alice_bob_provider.remote.utils import ( - camel_to_snake_case, - snake_to_camel_case, -) - - -def test_camel_to_snake_case() -> None: - assert camel_to_snake_case('nbShots') == 'nb_shots' - assert camel_to_snake_case('averageNbPhotons') == 'average_nb_photons' - - -def test_snake_to_camel_case() -> None: - assert snake_to_camel_case('nb_shots') == 'nbShots' - assert snake_to_camel_case('average_nb_photons') == 'averageNbPhotons' From 66821883a9a793048745a2b10fe9c26847f99dc5 Mon Sep 17 00:00:00 2001 From: Maxence Drutel Date: Wed, 29 Nov 2023 12:01:11 +0100 Subject: [PATCH 3/3] Bump version to 0.5.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b3ffa7a..c9befc2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "qiskit_alice_bob_provider" authors = [ {name = "Alice & Bob Software Team"}, ] -version = "0.4.1" +version = "0.5.0" description = "Provider for running Qiskit circuits on Alice & Bob QPUs and simulators" readme = "README.md" license = {text = "Apache 2.0"}