diff --git a/perceval/algorithm/abstract_algorithm.py b/perceval/algorithm/abstract_algorithm.py index cf469854f..0011fa7eb 100644 --- a/perceval/algorithm/abstract_algorithm.py +++ b/perceval/algorithm/abstract_algorithm.py @@ -27,7 +27,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from perceval.runtime.abstract_processor import AProcessor +from perceval.runtime import AProcessor class AAlgorithm: diff --git a/perceval/algorithm/sampler.py b/perceval/algorithm/sampler.py index a745d120e..6da3e6217 100644 --- a/perceval/algorithm/sampler.py +++ b/perceval/algorithm/sampler.py @@ -33,9 +33,8 @@ sample_count_to_probs, probs_to_samples, probs_to_sample_count, ProgressCallback from perceval.utils.logging import get_logger, channel from perceval.utils.constants import KEY_MAX_SHOTS, KEY_MAX_SAMPLES, KEY_RESULTS_LIST, KEY_ITERATION -from perceval.runtime.parameter_iterator import ParameterIterator -from perceval.runtime.abstract_processor import AProcessor -from perceval.runtime import Job, RemoteJob, LocalJob +from perceval.runtime.legacy.parameter_iterator import ParameterIterator +from perceval.runtime.legacy import Job, RemoteJob, LocalJob, AProcessor class Sampler(AAlgorithm): diff --git a/perceval/components/_mode_connector.py b/perceval/components/_mode_connector.py index 53762fe01..6e5e394eb 100644 --- a/perceval/components/_mode_connector.py +++ b/perceval/components/_mode_connector.py @@ -61,7 +61,7 @@ def __init__(self, left_experiment, right_obj, mapping): :param right_obj: the component or processor to plug on the right of `left_experiment` :param mapping: the user mapping defining the plugging rules (see resolve method doc for more info) """ - from perceval.runtime.abstract_processor import AProcessor + from perceval.runtime.legacy.abstract_processor import AProcessor # TODO: deprecated 1.3 if isinstance(left_experiment, AProcessor): left_experiment = left_experiment.experiment self._le = left_experiment diff --git a/perceval/components/feed_forward_configurator.py b/perceval/components/feed_forward_configurator.py index 34d5e74d0..c91f344a1 100644 --- a/perceval/components/feed_forward_configurator.py +++ b/perceval/components/feed_forward_configurator.py @@ -126,7 +126,7 @@ class FFCircuitProvider(AFFConfigurator): def __init__(self, m: int, offset: int, default_circuit: ACircuit, name: str = None): assert not isinstance(default_circuit, AFFConfigurator), \ "Can't add directly a Feed-forward configurator to a configurator (use an Experiment)" - from perceval.runtime.abstract_processor import AProcessor + from perceval.runtime.legacy.abstract_processor import AProcessor if isinstance(default_circuit, AProcessor): default_circuit = default_circuit.experiment super().__init__(m, offset, default_circuit, name) @@ -171,7 +171,7 @@ def add_configuration(self, state, circuit: ACircuit) -> FFCircuitProvider: if circuit.m != self._max_circuit_size: raise RuntimeError(f"Circuit size mismatch (got {circuit.m}, expected {self._max_circuit_size} modes)") - from perceval.runtime.abstract_processor import AProcessor + from perceval.runtime.legacy.abstract_processor import AProcessor if isinstance(circuit, AProcessor): circuit = circuit.experiment diff --git a/perceval/providers/__init__.py b/perceval/providers/__init__.py index 151fc37aa..1b6ee262f 100644 --- a/perceval/providers/__init__.py +++ b/perceval/providers/__init__.py @@ -28,11 +28,11 @@ # SOFTWARE. """Provider related imports and classes""" -from perceval.runtime import ISession +from perceval.runtime.legacy import ISession -from .quandela import Session as QuandelaSession -from .scaleway import Session as ScalewaySession -from .kipu import Session as KipuSession +from .quandela import Session as QuandelaSession, QuandelaCommunicationLayer +from .scaleway import Session as ScalewaySession, ScalewayCommunicationLayer +from .kipu import Session as KipuSession, KipuCommunicationLayer PROVIDER_LIST = { "Quandela": QuandelaSession, diff --git a/perceval/providers/kipu/__init__.py b/perceval/providers/kipu/__init__.py index 816087ff0..9a8457251 100644 --- a/perceval/providers/kipu/__init__.py +++ b/perceval/providers/kipu/__init__.py @@ -1,3 +1,32 @@ +# MIT License +# +# Copyright (c) 2022 Quandela +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# As a special exception, the copyright holders of exqalibur library give you +# permission to combine exqalibur with code included in the standard release of +# Perceval under the MIT license (or modified versions of such code). You may +# copy and distribute such a combined system following the terms of the MIT +# license for both exqalibur and Perceval. This exception for the usage of +# exqalibur is limited to the python bindings used by Perceval. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + # MIT License # # Copyright (c) 2026 Kipu Quantum GmbH @@ -29,5 +58,4 @@ from .kipu_session import Session from .kipu_rpc_handler import KipuRPCHandler - -__all__ = ["Session", "KipuRPCHandler"] +from .kipu_communication_layer import KipuCommunicationLayer diff --git a/perceval/providers/kipu/kipu_communication_layer.py b/perceval/providers/kipu/kipu_communication_layer.py new file mode 100644 index 000000000..ef4dfb91c --- /dev/null +++ b/perceval/providers/kipu/kipu_communication_layer.py @@ -0,0 +1,53 @@ +# MIT License +# +# Copyright (c) 2022 Quandela +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# As a special exception, the copyright holders of exqalibur library give you +# permission to combine exqalibur with code included in the standard release of +# Perceval under the MIT license (or modified versions of such code). You may +# copy and distribute such a combined system following the terms of the MIT +# license for both exqalibur and Perceval. This exception for the usage of +# exqalibur is limited to the python bindings used by Perceval. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +from perceval.runtime.communication_layer import RPCBasedCommunicationLayer + +from perceval.utils.logging import get_logger, channel + +from .kipu_rpc_handler import KipuRPCHandler + + +class KipuCommunicationLayer(RPCBasedCommunicationLayer): + + def __init__(self, + platform_name: str, + token: str = None, + organization_id: str = None, + url: str = None, + proxies: dict = None): + + super().__init__(KipuRPCHandler( + platform_name=platform_name, + url=url, + token=token, + organization_id=organization_id, + proxies=proxies, + )) + + get_logger().info(f"Connected to Kipu Cloud platform {platform_name}", channel.general) diff --git a/perceval/providers/kipu/kipu_rpc_handler.py b/perceval/providers/kipu/kipu_rpc_handler.py index b394ccd37..32301b12c 100644 --- a/perceval/providers/kipu/kipu_rpc_handler.py +++ b/perceval/providers/kipu/kipu_rpc_handler.py @@ -1,3 +1,5 @@ +# SKIP LICENSE INSERTION + # MIT License # # Copyright (c) 2026 Kipu Quantum GmbH @@ -29,6 +31,8 @@ from datetime import datetime +from perceval.utils.constants import KEY_VERSION, KEY_PROCESS_ID, KEY_JOB_NAME, KEY_MAX_SHOTS, KEY_MAX_SAMPLES, KEY_PAYLOAD + _MISSING_QHUB_MSG = ( "The Kipu Quantum Hub provider requires the 'qhub-api' package. " "Install it with: pip install perceval[kipu]" @@ -214,8 +218,8 @@ def headers(self) -> dict: def create_job(self, payload: dict) -> str: """Submit a Perceval job payload to the Hub. Returns the Hub job id.""" qhub = _import_qhub() - inner = payload.get("payload") or {} - shots = inner.get("max_shots") or inner.get("max_samples") or 0 + inner = payload.get(KEY_PAYLOAD) or {} + shots = inner.get(KEY_MAX_SHOTS) or inner.get(KEY_MAX_SAMPLES) or 0 input_cls = qhub["input"][self._backend_id] params_cls = qhub["params"][self._backend_id] @@ -226,11 +230,11 @@ def create_job(self, payload: dict) -> str: input=input_cls(value=inner), input_format="PERCEVAL", input_params=params_cls( - pcvl_version=payload.get("pcvl_version"), - process_id=payload.get("process_id"), + pcvl_version=payload.get(KEY_VERSION), + process_id=payload.get(KEY_PROCESS_ID), ), sdk_provider="PERCEVAL", - name=payload.get("job_name"), + name=payload.get(KEY_JOB_NAME), ) return job.id diff --git a/perceval/providers/kipu/kipu_session.py b/perceval/providers/kipu/kipu_session.py index c3828c4a9..91e510297 100644 --- a/perceval/providers/kipu/kipu_session.py +++ b/perceval/providers/kipu/kipu_session.py @@ -1,3 +1,5 @@ +# SKIP LICENSE INSERTION + # MIT License # # Copyright (c) 2026 Kipu Quantum GmbH @@ -27,8 +29,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from perceval.runtime import ISession -from perceval.runtime.remote_processor import RemoteProcessor +from perceval.runtime.legacy import ISession, RemoteProcessor from perceval.utils.logging import get_logger, channel from .kipu_rpc_handler import KipuRPCHandler diff --git a/perceval/providers/quandela/__init__.py b/perceval/providers/quandela/__init__.py index 844b067d7..b8205a4dd 100644 --- a/perceval/providers/quandela/__init__.py +++ b/perceval/providers/quandela/__init__.py @@ -28,3 +28,4 @@ # SOFTWARE. from .quandela_session import Session +from .quandela_communication_layer import QuandelaCommunicationLayer diff --git a/perceval/providers/quandela/quandela_communication_layer.py b/perceval/providers/quandela/quandela_communication_layer.py new file mode 100644 index 000000000..6afd40b70 --- /dev/null +++ b/perceval/providers/quandela/quandela_communication_layer.py @@ -0,0 +1,48 @@ +# MIT License +# +# Copyright (c) 2022 Quandela +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# As a special exception, the copyright holders of exqalibur library give you +# permission to combine exqalibur with code included in the standard release of +# Perceval under the MIT license (or modified versions of such code). You may +# copy and distribute such a combined system following the terms of the MIT +# license for both exqalibur and Perceval. This exception for the usage of +# exqalibur is limited to the python bindings used by Perceval. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +from requests import HTTPError + +from perceval.runtime.communication_layer import RPCBasedCommunicationLayer +from perceval.utils.logging import get_logger, channel + +from .rpc_handler import RPCHandler + +class QuandelaCommunicationLayer(RPCBasedCommunicationLayer): + + def __init__(self, name: str, token: str, url: str, proxies: dict[str, str] = None): + super().__init__(RPCHandler(name, url, token, proxies)) + get_logger().info(f"Connected to Cloud platform {name}", channel.general) + + def get_availability(self) -> int: + try: + availability = self._rpc_handler.get_job_availability() + return availability["max_jobs_in_queue"] - availability["num_jobs_in_queue"] + except HTTPError: + get_logger().warn("Impossible to determine whether there is room for a new job") + return 0 diff --git a/perceval/providers/quandela/quandela_session.py b/perceval/providers/quandela/quandela_session.py index e80be6f38..cbd898316 100644 --- a/perceval/providers/quandela/quandela_session.py +++ b/perceval/providers/quandela/quandela_session.py @@ -27,12 +27,11 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from perceval.runtime import ISession -from perceval.runtime.remote_processor import RemoteProcessor +from perceval.runtime.legacy import ISession, RemoteProcessor from perceval.runtime.remote_config import QUANDELA_CLOUD_URL -from perceval.runtime.rpc_handler import RPCHandler from perceval.utils.logging import get_logger, channel +from .rpc_handler import RPCHandler class Session(ISession): """ diff --git a/perceval/runtime/rpc_handler.py b/perceval/providers/quandela/rpc_handler.py similarity index 98% rename from perceval/runtime/rpc_handler.py rename to perceval/providers/quandela/rpc_handler.py index ced09c814..877b39617 100644 --- a/perceval/runtime/rpc_handler.py +++ b/perceval/providers/quandela/rpc_handler.py @@ -48,6 +48,7 @@ _JOB_ID_KEY = 'job_id' +# TODO: move this class to providers/quandela class RPCHandler: """Remote Call Procedure Handler @@ -127,7 +128,7 @@ def fetch_platform_details(self): try: response = self.get_request(endpoint) # TEMPORARY CODE - # TODO: to be removed in version 1.2 + # TODO: to be removed in version 1.3 if 'architecture' not in response.get('specs', {}): raise requests.HTTPError("Missing required 'architecture' field, falling back to the old endpoint " "containing the 'specific_circuit' field.") diff --git a/perceval/providers/scaleway/__init__.py b/perceval/providers/scaleway/__init__.py index a8b5a0142..9bab1aaec 100644 --- a/perceval/providers/scaleway/__init__.py +++ b/perceval/providers/scaleway/__init__.py @@ -29,5 +29,4 @@ from .scaleway_session import Session from .scaleway_rpc_handler import RPCHandler - -__all__ = ["Session", "RPCHandler"] +from .scaleway_communication_layer import ScalewayCommunicationLayer diff --git a/perceval/providers/scaleway/scaleway_communication_layer.py b/perceval/providers/scaleway/scaleway_communication_layer.py new file mode 100644 index 000000000..cad17ced3 --- /dev/null +++ b/perceval/providers/scaleway/scaleway_communication_layer.py @@ -0,0 +1,77 @@ +# MIT License +# +# Copyright (c) 2022 Quandela +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# As a special exception, the copyright holders of exqalibur library give you +# permission to combine exqalibur with code included in the standard release of +# Perceval under the MIT license (or modified versions of such code). You may +# copy and distribute such a combined system following the terms of the MIT +# license for both exqalibur and Perceval. This exception for the usage of +# exqalibur is limited to the python bindings used by Perceval. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from perceval.runtime.communication_layer import RPCBasedCommunicationLayer +from perceval.utils.logging import get_logger, channel + +from .scaleway_rpc_handler import RPCHandler + +class ScalewayCommunicationLayer(RPCBasedCommunicationLayer): + + def __init__(self, + platform_name: str, + project_id: str, + token: str, + max_idle_duration_s: int = 1200, + max_duration_s: int = 3600, + deduplication_id: str = None, + url: str = None, + proxies: dict[str, str] = None, + provider_name: str = None): + + self._deduplication_id = deduplication_id + self._max_idle_duration_s = max_idle_duration_s + self._max_duration_s = max_duration_s + + super().__init__(RPCHandler( + project_id=project_id, + secret_key=token, + url=url, + proxies=proxies, + platform_name=platform_name, + provider_name=provider_name, + )) + get_logger().info(f"Connected to Scaleway Cloud platform {platform_name}", channel.general) + + def start_session(self) -> None: + self._rpc_handler.create_session( + max_duration_s=self._max_idle_duration_s, + max_idle_duration_s=self._max_idle_duration_s, + deduplication_id=self._deduplication_id, + ) + + def stop_session(self) -> None: + self._rpc_handler.terminate_session() + get_logger().info("Stop Scaleway Session", channel.general) + + def delete_session(self) -> None: + self._rpc_handler.delete_session() + get_logger().info( + "Stop (if not already) and revoke Scaleway Session", channel.general + ) diff --git a/perceval/providers/scaleway/scaleway_rpc_handler.py b/perceval/providers/scaleway/scaleway_rpc_handler.py index 38447742b..4a7e1ca78 100644 --- a/perceval/providers/scaleway/scaleway_rpc_handler.py +++ b/perceval/providers/scaleway/scaleway_rpc_handler.py @@ -35,6 +35,7 @@ from requests import HTTPError from perceval.utils.logging import get_logger, channel +from perceval.utils.constants import KEY_JOB_NAME, KEY_PAYLOAD _ENDPOINT_PLATFORM = "/qaas/v1alpha1/platforms" _ENDPOINT_JOB = "/qaas/v1alpha1/jobs" @@ -51,10 +52,10 @@ def __init__( self, project_id: str, secret_key: str, - url: str, - proxies: dict, + url: str | None, + proxies: dict | None, platform_name: str, - provider_name: str, + provider_name: str | None, ): self._project_id = project_id self._url = url or _DEFAULT_URL @@ -186,8 +187,8 @@ def create_job(self, payload: dict) -> str: raise Exception("Cannot create job because session_id is None") scw_payload = { - "name": payload.get("job_name"), - "circuit": {"percevalCircuit": json.dumps(payload.get("payload", {}))}, + "name": payload.get(KEY_JOB_NAME), + "circuit": {"percevalCircuit": json.dumps(payload.get(KEY_PAYLOAD, {}))}, "project_id": self._project_id, "session_id": self._session_id, } @@ -200,8 +201,8 @@ def create_job(self, payload: dict) -> str: try: request.raise_for_status() job = request.json() + assert "id" in job - self.instance_id = job["id"] except Exception: raise HTTPError(request.json()) diff --git a/perceval/providers/scaleway/scaleway_session.py b/perceval/providers/scaleway/scaleway_session.py index 5808190af..865b3ef7e 100644 --- a/perceval/providers/scaleway/scaleway_session.py +++ b/perceval/providers/scaleway/scaleway_session.py @@ -28,8 +28,7 @@ # SOFTWARE. from typing import Optional -from perceval.runtime import ISession -from perceval.runtime.remote_processor import RemoteProcessor +from perceval.runtime.legacy import ISession, RemoteProcessor from perceval.utils.logging import get_logger, channel from .scaleway_rpc_handler import RPCHandler diff --git a/perceval/runtime/__init__.py b/perceval/runtime/__init__.py index 3b15e2890..cc19f2ad5 100644 --- a/perceval/runtime/__init__.py +++ b/perceval/runtime/__init__.py @@ -28,15 +28,7 @@ # SOFTWARE. from .job_status import JobStatus, RunningStatus -from .job import Job -from .local_job import LocalJob -from .remote_job import RemoteJob -from .abstract_processor import AProcessor -from .processor import Processor -from .remote_processor import RemoteProcessor, perf_dict_to_noise -from .session import ISession from .remote_config import RemoteConfig -from .job_group import JobGroup from .check_cancel import cancel_requested from .payload_generator import PayloadGenerator from .payload_updater import PayloadUpdater @@ -48,5 +40,6 @@ from .local_computer import LocalComputer from .simulated_computer import SimulatedComputer from .remote_computer import RemoteComputer, CommunicationLayer -from .quandela_computer import QuandelaComputer, QuandelaCommunicationLayer from .execution import Execution + +from .legacy import * diff --git a/perceval/runtime/abstract_computer.py b/perceval/runtime/abstract_computer.py index cd9ff4700..fb9306a22 100644 --- a/perceval/runtime/abstract_computer.py +++ b/perceval/runtime/abstract_computer.py @@ -308,13 +308,6 @@ def _execute_command_async(self, computation: Computation) -> AsyncGetter: def is_remote(self) -> bool: pass - def acquire(self) -> ContextManager: - """ - This method can be used to set up the AbstractComputer before the use of the :code:`execute` method. - For example, it can be overloaded to warm up internal tools, or empty some cache at the end of a computation. - """ - return ContextManager() - def _reserve_resource(self) -> ContextManager: """ This method is used internally when computing basic computations (after error mitigation extension). @@ -324,6 +317,10 @@ def _reserve_resource(self) -> ContextManager: """ return ContextManager() + @property + def available_jobs(self) -> int: + return 1 + @property def specs(self) -> PlatformSpecs: specs = PlatformSpecs() @@ -348,6 +345,24 @@ def noise(self, noise: NoiseModel): def performance(self): pass + def start(self) -> None: + """Starts the computer. May do nothing for stateless computers""" + pass + + def stop(self) -> None: + """Stops the computer. May do nothing for stateless computers""" + pass + + def delete(self) -> None: + """Deletes the computer session. May do nothing for stateless computers""" + pass + + def acquire(self) -> ContextManager: + """ + :return: A context manager that starts the computer at enter and stops it at exit + """ + return ContextManager(self.start, self.stop) + @property @abstractmethod def type(self): diff --git a/perceval/runtime/quandela_computer.py b/perceval/runtime/communication_layer.py similarity index 68% rename from perceval/runtime/quandela_computer.py rename to perceval/runtime/communication_layer.py index e5caa422d..141166599 100644 --- a/perceval/runtime/quandela_computer.py +++ b/perceval/runtime/communication_layer.py @@ -26,51 +26,126 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. + import json import time +from abc import ABC, abstractmethod +from typing import TypeVar, Type from requests import HTTPError +from perceval.serialization import deserialize, serialize +from perceval.utils.constants import KEY_JOB_NAME, KEY_JOB_CONTEXT, KEY_RESULT_MAPPING, \ + KEY_MAPPING_PARAMETERS, KEY_RESULTS_LIST, KEY_ITERATION, KEY_RESULTS, KEY_PLATFORM_NAME, KEY_JOB_GROUP_NAME, \ + KEY_COMMAND, KEY_MAX_SHOTS, KEY_MAX_SAMPLES +from perceval.utils.logging import channel, get_logger + +from .job_status import JobStatus, RunningStatus from .command import Command from .platform_specs import PlatformSpecs -from .remote_computer import RemoteComputer, CommunicationLayer, RemoteId, _RemoteGetter -from .remote_config import RemoteConfig -from .job_status import RunningStatus, JobStatus -from .remote_job import _retrieve_from_response -from .remote_processor import PERFS_KEY -from .rpc_handler import RPCHandler -from .computation import Computation -from .payload_generator import PayloadGenerator from .payload_updater import PayloadUpdater +from .payload_generator import PayloadGenerator -from perceval.serialization import deserialize, serialize -from perceval.utils import ContextManager -from perceval.utils.logging import get_logger, channel -from perceval.utils.constants import KEY_JOB_CONTEXT, KEY_RESULT_MAPPING, KEY_RESULTS_LIST, KEY_MAPPING_PARAMETERS, \ - KEY_ITERATION, KEY_RESULTS, KEY_PLATFORM_NAME, KEY_JOB_NAME, KEY_JOB_GROUP_NAME, KEY_COMMAND, KEY_MAX_SHOTS +PERFS_KEY = "perfs" +T = TypeVar('T') + +def _retrieve_from_response(response: dict, field: str, default_value: T = '', value_type: Type[T] = str) -> T: + if field not in response: + get_logger().error(f"Missing field '{field}' from server response. Using default value {default_value}.", channel.general) + return default_value + try: + result = value_type(response[field]) + except (ValueError, TypeError): + get_logger().error(f"The field '{field}' from server response contains the wrong value '{response[field]}'. Using default value {default_value}.", channel.general) + result = default_value + return result + + +RemoteId = TypeVar("RemoteId") + + +class CommunicationLayer(ABC): + """ + This class is responsible for the communication with the distant computer, with . + Implementations of this class must remain const, except for potential non-job-related cache + """ + + @abstractmethod + def get_specs(self) -> PlatformSpecs: + """ + :return: The specs of the target platform + """ + pass + + @abstractmethod + def send(self, payload: dict) -> RemoteId: + pass + + @abstractmethod + def get_results(self, remote_id: RemoteId) -> dict: + pass + + @abstractmethod + def get_job_status(self, remote_id: RemoteId, refresh_errors: int = 0) -> JobStatus | None: + """ + :param remote_id: + :param refresh_errors: The number of times in a row where this method returned None + :return: The Job Status if it was available, None otherwise + """ + pass + + @abstractmethod + def get_remote_status(self) -> str: + pass + + @abstractmethod + def get_performances(self) -> dict: + pass + + @abstractmethod + def get_commands(self) -> list[Command]: + pass + + @abstractmethod + def cancel(self, remote_id: RemoteId) -> None: + pass + + @abstractmethod + def get_availability(self) -> int: + """Returns the number of concurrent jobs currently available to be sent""" + pass -class QuandelaCommunicationLayer(CommunicationLayer): + def start_session(self) -> None: + """May be used to start a non-interrupted session. May do nothing on stateless implementations""" + pass + def stop_session(self) -> None: + """May be used to stop a non-interrupted session. May do nothing on stateless implementations""" + pass + + def delete_session(self) -> None: + """May be used to delete a non-interrupted session. May do nothing on stateless implementations""" + pass + + +class RPCBasedCommunicationLayer(CommunicationLayer): MINIMUM_FETCH_INTERVAL = 5 _MAX_ERROR = 6 - def __init__(self, name: str, token: str, url: str, proxies: dict[str, str] = None): - self.name = name - self.token = token - self.url = url - self.proxies: dict[str, str] = proxies if proxies is not None else {} + # Use duck-typing for RPCHandler. See the quandela RPCHandler for an example + def __init__(self, rpc_handler): + self._rpc_handler = rpc_handler + self._specs = PlatformSpecs() self._status: str = "" self._perfs: dict[str, str] = {} self._last_fetch_time = None - self._rpc_handler = RPCHandler(name, url, token, proxies) self.fetch_data() - get_logger().info(f"Connected to Cloud platform {self.name}", channel.general) def fetch_data(self): - # Quandela specific: the same endpoint gives the specs, perfs and platform status + # RPCHandler specific: the same method gives the specs, perfs and platform status if self._last_fetch_time is None or time.time() - self._last_fetch_time > self.MINIMUM_FETCH_INTERVAL: try: platform_details = self._rpc_handler.fetch_platform_details() @@ -98,13 +173,15 @@ def send(self, payload: dict) -> RemoteId: # Needed for display - Should not be used anywhere else. The cloud expects these so they must be filled payload[KEY_COMMAND] = computation.command.name + assert KEY_MAX_SHOTS in computation.parameters, f"Missing '{KEY_MAX_SHOTS}' parameter" payload[KEY_MAX_SHOTS] = computation.parameters[KEY_MAX_SHOTS] + payload[KEY_MAX_SAMPLES] = computation.parameters.get(KEY_MAX_SAMPLES, 0) if "commands" not in self._specs: # We have a worker that knows only payloads up to version 1 # Using self._specs is a bit of a trick, since internally, # we only needs the argument to have "available_commands" when downgrading to version 1 # This might not be true anymore if we introduce a version 3 someday - payload = PayloadUpdater.update_payload(payload, self._specs, target_payload_version = 1) + payload = PayloadUpdater.update_payload(payload, self._specs, target_payload_version=1) global_data = PayloadGenerator.generate_global_data(payload, {KEY_PLATFORM_NAME: self._rpc_handler.name, @@ -155,7 +232,7 @@ def _handle_status_error(self, error: Exception, remote_id: RemoteId, refresh_er 409, # Conflict in the current state of the resource 421, # Misdirected request 423, # Resource locked - 429 # Too many requests + 429 # Too many requests ]: get_logger().error(f"Got HTTP error {error_code} when updating job {remote_id} status. Ignoring...", channel.general) @@ -171,10 +248,10 @@ def get_job_status(self, remote_id: RemoteId, refresh_errors: int = 0) -> JobSta job_status = JobStatus() job_status.status = RunningStatus.from_server_response(_retrieve_from_response(response, 'status')) - if job_status.running: + if job_status.running or job_status.completed: job_status.update_progress(_retrieve_from_response(response, 'progress', 0., float), _retrieve_from_response(response, 'progress_message')) - elif job_status.failed: + if job_status.failed: job_status._stop_message = _retrieve_from_response(response, 'status_message') self._extract_job_times(job_status, response) @@ -211,80 +288,4 @@ def cancel(self, remote_id: RemoteId) -> None: raise HTTPError(f"Error while trying to cancel job: {e}") from None def get_availability(self) -> int: - """ - :return: The number of jobs available in the queue - """ - try: - availability = self._rpc_handler.get_job_availability() - return availability["max_jobs_in_queue"] - availability["num_jobs_in_queue"] - except HTTPError: - get_logger().warn("Impossible to determine whether there is room for a new job") - return 0 - - -class QuandelaComputer(RemoteComputer): - _communication_layer: QuandelaCommunicationLayer # Used for type hinting only - - WARN_INTERVAL = 1800 - INFO_INTERVAL = 10 - - def __init__(self, - name: str, - token: str = None, - url: str = None, - proxies: dict[str,str] = None): - """ - A Computer meant to access Quandela remote services. - - All parameters but the name of the target platform have to be explicitly named to be set. - - :param name: Platform name. - :param token: Token value to authenticate the user. If not provided, it is taken from the stored RemoteConfig - :param url: Base URL for the Cloud API to connect to - :param proxies: Dictionary mapping protocol to the URL of the proxy - """ - - remote = RemoteConfig() - if token is None: - token = remote.get_token() - if not token: - raise ConnectionError("No token found") - if url is None: - url = remote.get_url() - if proxies is None: - proxies = remote.get_proxies() - self.name = name - communication_layer = QuandelaCommunicationLayer(name, token, url, proxies) - - super().__init__(communication_layer) - self._available_jobs = communication_layer.get_availability() - - def validate_single(self, computation: Computation) -> None: - super().validate_single(computation) - assert KEY_MAX_SHOTS in computation.parameters, f"Missing '{KEY_MAX_SHOTS}' parameter" - - def _take_resource(self): - start = time.time() - start_warn = time.time() - start_info = start_warn - while self._available_jobs <= 0: - self._available_jobs = self._communication_layer.get_availability() - time.sleep(1) - if time.time() - start_warn > self.WARN_INTERVAL: - start_warn = time.time() - get_logger().warn(f"Couldn't find a way to send any job for {int(start_warn - start)} seconds - queue is full") - elif time.time() - start_info > self.INFO_INTERVAL: - start_info = time.time() - get_logger().info(f"Couldn't find a way to send any job for {int(start_info - start)} seconds - queue is full") - - self._available_jobs -= 1 - - def _release_resource(self): - self._available_jobs += 1 - - def _reserve_resource(self) -> ContextManager: - return ContextManager(self._take_resource, self._release_resource) - - def _execute_command_async(self, computation: Computation) -> _RemoteGetter: - self._take_resource() - return super()._execute_command_async(computation) + return 1 diff --git a/perceval/runtime/execution.py b/perceval/runtime/execution.py index c95063087..729aae55d 100644 --- a/perceval/runtime/execution.py +++ b/perceval/runtime/execution.py @@ -202,15 +202,15 @@ def execute_sync(self, *args, allow_partial_results: bool = False, **kwargs) -> return self._results # Problem here if we try to reuse a job with different args and kwargs self._status.start_run() - with self._computer.acquire(): - try: - self._transmit_args(*args, **kwargs) - self._computer.execute(self._computation, self._results, progress_callback=self._progress_callback) - except Exception as e: - if not allow_partial_results: - self._results = {} - self._status.stop_run(RunningStatus.ERROR, f"{type(e).__name__}: {e}") - raise e + try: + self._transmit_args(*args, **kwargs) + self._computer.execute(self._computation, self._results, progress_callback=self._progress_callback) + except Exception as e: + if not allow_partial_results: + self._results = {} + self._status.stop_run(RunningStatus.ERROR, f"{type(e).__name__}: {e}") + raise e + if self._status.canceled: self._status.stop_run(RunningStatus.CANCELED, "Canceled") else: diff --git a/tests/providers/test_kipu_provider_registration.py b/perceval/runtime/legacy/__init__.py similarity index 71% rename from tests/providers/test_kipu_provider_registration.py rename to perceval/runtime/legacy/__init__.py index 3928a22af..02df98d27 100644 --- a/tests/providers/test_kipu_provider_registration.py +++ b/perceval/runtime/legacy/__init__.py @@ -1,6 +1,6 @@ # MIT License # -# Copyright (c) 2026 Kipu Quantum GmbH +# Copyright (c) 2022 Quandela # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -27,25 +27,11 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from perceval.runtime import ISession -from perceval.providers import ProviderFactory - - -def test_kipu_in_provider_list(): - assert "Kipu" in ProviderFactory.list() - - -def test_get_kipu_provider(): - session = ProviderFactory.get_provider( - "Kipu", - platform_name="quandela.sim.belenos", - token="t", - organization_id="org-1", - ) - assert isinstance(session, ISession) - - -def test_kipu_package_exports(): - from perceval.providers.kipu import Session, KipuRPCHandler - assert Session is not None - assert KipuRPCHandler is not None +from .abstract_processor import AProcessor +from .processor import Processor +from .remote_processor import RemoteProcessor +from .session import ISession +from .job import Job +from .local_job import LocalJob +from .remote_job import RemoteJob +from .job_group import JobGroup diff --git a/perceval/runtime/abstract_processor.py b/perceval/runtime/legacy/abstract_processor.py similarity index 99% rename from perceval/runtime/abstract_processor.py rename to perceval/runtime/legacy/abstract_processor.py index 108fe4835..0b57e2c09 100644 --- a/perceval/runtime/abstract_processor.py +++ b/perceval/runtime/legacy/abstract_processor.py @@ -30,7 +30,7 @@ from abc import ABC, abstractmethod -from .platform_specs import PlatformSpecs +from ..platform_specs import PlatformSpecs from perceval.utils import BasicState, FockState, Parameter, PostSelect, LogicalState, NoiseModel, SVDistribution, StateVector, ProcessorType from perceval.components.abstract_component import AComponent from perceval.components.detector import DetectionType diff --git a/perceval/runtime/job.py b/perceval/runtime/legacy/job.py similarity index 99% rename from perceval/runtime/job.py rename to perceval/runtime/legacy/job.py index 097917a97..7b729bf8c 100644 --- a/perceval/runtime/job.py +++ b/perceval/runtime/legacy/job.py @@ -32,7 +32,7 @@ from perceval.utils.logging import get_logger, channel -from .job_status import JobStatus +from ..job_status import JobStatus class Job(ABC): diff --git a/perceval/runtime/job_group.py b/perceval/runtime/legacy/job_group.py similarity index 99% rename from perceval/runtime/job_group.py rename to perceval/runtime/legacy/job_group.py index ad529093e..80f6d6a64 100644 --- a/perceval/runtime/job_group.py +++ b/perceval/runtime/legacy/job_group.py @@ -34,8 +34,9 @@ from requests import HTTPError from tqdm import tqdm -from perceval.runtime import Job, RemoteJob, RunningStatus -from perceval.runtime.rpc_handler import RPCHandler +from .remote_job import RemoteJob +from .local_job import Job, RunningStatus + from perceval.utils import PersistentData, FileFormat from perceval.utils.logging import get_logger, channel @@ -134,6 +135,8 @@ def _build_remote_job(job_entry: dict) -> RemoteJob: """ metadata = job_entry['metadata'] user_token = metadata['headers']['Authorization'].split(' ')[1] + + from perceval.providers.quandela.rpc_handler import RPCHandler # TODO: deprecated since 1.3 rpc_handler = RPCHandler(metadata['platform'], metadata['url'], user_token, metadata.get('proxies')) return RemoteJob._from_dict(job_entry, rpc_handler) diff --git a/perceval/runtime/local_job.py b/perceval/runtime/legacy/local_job.py similarity index 99% rename from perceval/runtime/local_job.py rename to perceval/runtime/legacy/local_job.py index ff9299e0e..9f5499885 100644 --- a/perceval/runtime/local_job.py +++ b/perceval/runtime/legacy/local_job.py @@ -32,9 +32,9 @@ from perceval.utils.logging import get_logger, channel from perceval.utils import ProgressCallback -from .job import Job -from .job_status import JobStatus, RunningStatus +from .job import Job +from ..job_status import JobStatus, RunningStatus class LocalJob(Job): r""" diff --git a/perceval/runtime/parameter_iterator.py b/perceval/runtime/legacy/parameter_iterator.py similarity index 100% rename from perceval/runtime/parameter_iterator.py rename to perceval/runtime/legacy/parameter_iterator.py diff --git a/perceval/runtime/processor.py b/perceval/runtime/legacy/processor.py similarity index 99% rename from perceval/runtime/processor.py rename to perceval/runtime/legacy/processor.py index 12bf9078e..180ec86e8 100644 --- a/perceval/runtime/processor.py +++ b/perceval/runtime/legacy/processor.py @@ -32,12 +32,13 @@ ProcessorType, ProgressCallback from perceval.utils.logging import get_logger, channel -from perceval.runtime.abstract_processor import AProcessor from perceval.components.experiment import Experiment from perceval.components.linear_circuit import ACircuit, Circuit from perceval.components.source import Source from perceval.backends import ExqaliburBackendWrapper, ASamplingBackend +from .abstract_processor import AProcessor + class Processor(AProcessor): """ diff --git a/perceval/runtime/remote_job.py b/perceval/runtime/legacy/remote_job.py similarity index 94% rename from perceval/runtime/remote_job.py rename to perceval/runtime/legacy/remote_job.py index d62b1920a..71e51377e 100644 --- a/perceval/runtime/remote_job.py +++ b/perceval/runtime/legacy/remote_job.py @@ -30,30 +30,16 @@ import json import time -from typing import TypeVar, Type from requests.exceptions import HTTPError, ConnectionError -from .job import Job -from .job_status import JobStatus, RunningStatus from perceval.serialization import deserialize from perceval.serialization._serialized_containers import make_serialized, SerializedDict from perceval.utils.logging import get_logger, channel -from .rpc_handler import RPCHandler - -T = TypeVar('T') - -def _retrieve_from_response(response: dict, field: str, default_value: T = '', value_type: Type[T] = str) -> T: - if field not in response: - get_logger().error(f"Missing field '{field}' from server response. Using default value {default_value}.", channel.general) - return default_value - try: - result = value_type(response[field]) - except (ValueError, TypeError): - get_logger().error(f"The field '{field}' from server response contains the wrong value '{response[field]}'. Using default value {default_value}.", channel.general) - result = default_value - return result +from .job import Job +from ..job_status import JobStatus, RunningStatus +from ..communication_layer import _retrieve_from_response class RemoteJob(Job): r""" @@ -87,7 +73,7 @@ class RemoteJob(Job): STATUS_REFRESH_DELAY = 1 # minimum job status refresh period (in s) _MAX_ERROR = 5 - def __init__(self, request_data: dict, rpc_handler: RPCHandler, job_name: str, + def __init__(self, request_data: dict, rpc_handler, job_name: str, delta_parameters: dict = None, job_context: dict = None, command_param_names: list = None, refresh_progress_delay: int = 3): super().__init__(command_param_names=command_param_names) @@ -124,7 +110,7 @@ def id(self) -> str | None: return self._id @staticmethod - def from_id(job_id: str, rpc_handler: RPCHandler) -> RemoteJob: + def from_id(job_id: str, rpc_handler) -> RemoteJob: """ Recreate an existing RemoteJob from its unique identifier, and a RPCHandler. diff --git a/perceval/runtime/remote_processor.py b/perceval/runtime/legacy/remote_processor.py similarity index 97% rename from perceval/runtime/remote_processor.py rename to perceval/runtime/legacy/remote_processor.py index 9b7991f3c..111e91c64 100644 --- a/perceval/runtime/remote_processor.py +++ b/perceval/runtime/legacy/remote_processor.py @@ -37,15 +37,15 @@ from perceval.serialization import deserialize from perceval.utils.noise_model import TRANSMITTANCE_KEY, perf_dict_to_noise -from .platform_specs import PlatformSpecs from .remote_job import RemoteJob -from .rpc_handler import RPCHandler -from .remote_config import RemoteConfig -from .payload_generator import PayloadGenerator from .abstract_processor import AProcessor from .processor import Processor -PERFS_KEY = "perfs" +from ..communication_layer import PERFS_KEY +from ..payload_generator import PayloadGenerator +from ..remote_config import RemoteConfig +from ..platform_specs import PlatformSpecs + DEFAULT_TRANSMITTANCE = 0.06 @@ -57,7 +57,7 @@ def from_local_processor( token: str = None, url: str = None, proxies: dict[str,str] = None, - rpc_handler: RPCHandler = None): + rpc_handler = None): rp = RemoteProcessor( name=name, token=token, @@ -76,7 +76,7 @@ def __init__(self, token: str = None, url: str = None, proxies: dict[str,str] = None, - rpc_handler: RPCHandler = None, + rpc_handler = None, m: int = None, noise: NoiseModel = None): """ @@ -100,6 +100,8 @@ def __init__(self, f"Initialised a RemoteProcessor with two different platform names ({self.name} vs {name})", channel.user) self.proxies = rpc_handler.proxies else: + from perceval.providers.quandela.rpc_handler import RPCHandler + remote = RemoteConfig() if name is None: raise ValueError("Parameter 'name' must have a value") diff --git a/perceval/runtime/session.py b/perceval/runtime/legacy/session.py similarity index 100% rename from perceval/runtime/session.py rename to perceval/runtime/legacy/session.py diff --git a/perceval/runtime/remote_computer.py b/perceval/runtime/remote_computer.py index 73e87a0d6..819bd210a 100644 --- a/perceval/runtime/remote_computer.py +++ b/perceval/runtime/remote_computer.py @@ -28,73 +28,24 @@ # SOFTWARE. import time -from abc import ABC, abstractmethod from copy import deepcopy -from typing import TypeVar, Callable +from typing import Callable -from .command import Command +from .communication_layer import CommunicationLayer, RemoteId from .computation import Computation from .abstract_computer import AbstractComputer from .computation_iterator import ComputationIterator from .platform_specs import PlatformSpecs from .error_mitigation import AbstractMitigation -from .job_status import JobStatus, RunningStatus +from .job_status import RunningStatus from .simulated_computer import SimulatedComputer from .async_getter import AsyncGetter from .payload_generator import PayloadGenerator -from perceval.utils import perf_dict_to_noise, ProgressCallback, NoiseModel, PostSelect +from perceval.utils import perf_dict_to_noise, ProgressCallback, NoiseModel, PostSelect, ContextManager from perceval.utils.logging import channel, get_logger from perceval.components import PortLocation, Experiment -RemoteId = TypeVar("RemoteId") - - -class CommunicationLayer(ABC): - """ - This class is responsible for the communication with the distant computer. - """ - - @abstractmethod - def get_specs(self) -> PlatformSpecs: - """ - :return: The specs of the target platform - """ - pass - - @abstractmethod - def send(self, payload: dict) -> RemoteId: - pass - - @abstractmethod - def get_results(self, remote_id: RemoteId) -> dict: - pass - - @abstractmethod - def get_job_status(self, remote_id: RemoteId, refresh_errors: int = 0) -> JobStatus | None: - """ - :param remote_id: - :param refresh_errors: The number of times in a row where this method returned None - :return: The Job Status if it was available, None otherwise - """ - pass - - @abstractmethod - def get_remote_status(self) -> str: - pass - - @abstractmethod - def get_performances(self) -> dict: - pass - - @abstractmethod - def get_commands(self) -> list[Command]: - pass - - @abstractmethod - def cancel(self, remote_id: RemoteId) -> None: - pass - class _RemoteGetter(AsyncGetter): STATUS_REFRESH_DELAY = 1 # minimum job status refresh period (in s) @@ -141,6 +92,9 @@ def _get_results(self) -> dict | None: class RemoteComputer(AbstractComputer): + WARN_INTERVAL = 1800 + INFO_INTERVAL = 10 + def __init__(self, communication_layer: CommunicationLayer): super().__init__() self._communication_layer = communication_layer # cloud_access is the communication layer @@ -149,6 +103,7 @@ def __init__(self, communication_layer: CommunicationLayer): self._perfs = communication_layer.get_performances() self._custom_noise: NoiseModel | None = None self.use_mitigations_remotely: bool = True # TODO: detect if the target supports mitigations ? + self._available_jobs = 0 # TODO: how to get default mitigations ? @property @@ -257,10 +212,38 @@ def _execute_command(self, computation: Computation, progress_cb: ProgressCallba return async_getter.get_results() def _execute_command_async(self, computation: Computation) -> _RemoteGetter: - # Subclasses may implement something here to ask for availability before sending to the cloud payload = self.prepare_payload(computation) + self._take_resource() return _RemoteGetter(self._communication_layer, self._communication_layer.send(payload)) + @property + def available_jobs(self) -> int: + self._available_jobs = self._communication_layer.get_availability() + return self._available_jobs + + def _take_resource(self): + start = time.time() + start_warn = time.time() + start_info = start_warn + self._available_jobs = self._communication_layer.get_availability() + while self._available_jobs <= 0: + time.sleep(1) + if time.time() - start_warn > self.WARN_INTERVAL: + start_warn = time.time() + get_logger().warn(f"Couldn't find a way to send any job for {int(start_warn - start)} seconds - queue is full") + elif time.time() - start_info > self.INFO_INTERVAL: + start_info = time.time() + get_logger().info(f"Couldn't find a way to send any job for {int(start_info - start)} seconds - queue is full") + self._available_jobs = self._communication_layer.get_availability() + + self._available_jobs -= 1 + + def _release_resource(self): + self._available_jobs += 1 + + def _reserve_resource(self) -> ContextManager: + return ContextManager(self._take_resource, self._release_resource) + def prepare_payload(self, computation: Computation) -> dict: if self._error_mitigations is not None: if self.use_mitigations_remotely: @@ -275,6 +258,18 @@ def prepare_payload(self, computation: Computation) -> dict: self._parameters, self._custom_noise) + def start(self) -> None: + """May be used to start a non-interrupted session. May do nothing for stateless providers""" + self._communication_layer.start_session() + + def stop(self) -> None: + """May be used to stop a non-interrupted session. May do nothing for stateless providers""" + self._communication_layer.stop_session() + + def delete(self) -> None: + """May be used to delete a non-interrupted session. May do nothing for stateless providers""" + self._communication_layer.delete_session() + @property def is_remote(self) -> bool: return True diff --git a/perceval/runtime/remote_config.py b/perceval/runtime/remote_config.py index c8b703140..466e2e956 100644 --- a/perceval/runtime/remote_config.py +++ b/perceval/runtime/remote_config.py @@ -150,16 +150,6 @@ def get_token_env_var() -> str: """Get the name of the environment variable storing a token.""" return RemoteConfig._token_env_var - @staticmethod - @deprecated(version="v1.1", reason="RemoteConfig maximal job count is no longer used as it is now directly retrieved from the cloud") - def set_cloud_maximal_job_count(count: int) -> None: - pass - - @staticmethod - @deprecated(version="v1.1", reason="RemoteConfig maximal job count is no longer used as it is now directly retrieved from the cloud") - def get_cloud_maximal_job_count() -> int: - return 0 - @staticmethod def clear_cache(): """Delete the RemoteConfig cache.""" diff --git a/perceval/runtime/simulated_computer.py b/perceval/runtime/simulated_computer.py index 864983cd1..d62da1d0c 100644 --- a/perceval/runtime/simulated_computer.py +++ b/perceval/runtime/simulated_computer.py @@ -144,7 +144,7 @@ def probs(self, experiment: Experiment, """ if isinstance(self._backend, AStrongSimulationBackend): experiment = experiment.use_phase_noise(self.noise, compilation_seed) - simulator = SimulatorFactory.build(experiment, self._backend) + simulator = SimulatorFactory.build(experiment, self._backend, self.noise) precision = self._parse_precision(precision, max_shots, max_samples) if precision is not None: diff --git a/perceval/serialization/_serialized_containers.py b/perceval/serialization/_serialized_containers.py index e2d4b00f7..7174645db 100644 --- a/perceval/serialization/_serialized_containers.py +++ b/perceval/serialization/_serialized_containers.py @@ -33,6 +33,7 @@ from multipledispatch import dispatch +# Possibly deprecated since 1.3, no longer needed in the Computer workflow class SerializedDict(dict): """ Class that mimics a python dict, but internally stores serialized versions of the perceval objects given to it, diff --git a/perceval/simulators/feed_forward_simulator.py b/perceval/simulators/feed_forward_simulator.py index aba44bec5..91ab4391e 100644 --- a/perceval/simulators/feed_forward_simulator.py +++ b/perceval/simulators/feed_forward_simulator.py @@ -35,7 +35,6 @@ from perceval.utils.logging import channel from perceval.components.feed_forward_configurator import AFFConfigurator from perceval.backends import AStrongSimulationBackend, IFFBackend -from perceval.runtime import Processor from .simulator_interface import ISimulator @@ -56,7 +55,8 @@ def compute_physical_logical_perf(self, value: bool): if value: get_logger().warn("Only the global performance can be computed for a feed-forward simulator.") - def set_circuit(self, circuit: Processor | Experiment | list[tuple[tuple, AComponent]], m=None): + def set_circuit(self, circuit: Experiment | list[tuple[tuple, AComponent]], m=None): + from perceval.runtime import Processor # TODO: remove (deprecated since 1.3) if isinstance(circuit, (Processor, Experiment)): self._components = circuit.components min_detected_photons = circuit._min_detected_photons_filter @@ -69,10 +69,6 @@ def set_circuit(self, circuit: Processor | Experiment | list[tuple[tuple, ACompo def set_noise(self, nm: NoiseModel): self._noise_model = nm - @deprecated("Version 1.1 - Source is no longer used") - def set_source(self, source: Source): - pass - def _probs_svd(self, input_state: SVDistribution | tuple[Source, BasicState], detectors: list[IDetector] = None, @@ -214,7 +210,7 @@ def _find_next_simulation_layer(self) -> tuple[list[tuple[int, AFFConfigurator]] def _get_sim_params(self, input_state: SVDistribution | tuple[Source, BasicState], - components: list[tuple[tuple, AComponent | Processor | Experiment]], + components: list[tuple[tuple, AComponent | Experiment]], m: int, detectors: list[IDetector] = None, filter_states: bool = False, @@ -232,57 +228,56 @@ def _get_sim_params(self, if detectors is None: detectors = m * [None] - proc = Processor(self._backend, m) - if self._noise_model is not None: - proc.noise = self._noise_model + exp = Experiment(m) for r, c in components: - proc.add(r, c) + exp.add(r, c) - # Now the Processor has only the heralds that were possibly added by adding Processors as input, all at the end + # Now the Experiment has only the heralds that were possibly added by adding Experiment as input, all at the end if isinstance(input_state, SVDistribution): - heralded_dist = proc.generate_noisy_heralds() - if len(heralded_dist): - input_state = input_state * heralded_dist # Must not change the original object + if exp.in_heralds: + heralds_perfect_state = FockState([v for k, v in sorted(exp.in_heralds.items())]) + source = Source.from_noise_model(self._noise_model) + input_state = input_state * source.generate_distribution(heralds_perfect_state) # Must not change the original object else: - proc.with_input(input_state[1]) - input_state = (input_state[0], proc.input_state) + exp.with_input(input_state[1]) + input_state = (input_state[0], exp.input_state) sum_new_heralds = 0 if new_heralds is not None: for r, v in new_heralds.items(): - proc.add_port(r, Herald(v), PortLocation.OUTPUT) + exp.add_port(r, Herald(v), PortLocation.OUTPUT) sum_new_heralds += v if filter_states: if self._heralds is not None: for r, v in self._heralds.items(): - proc.add_port(r, Herald(v), PortLocation.OUTPUT) + exp.add_port(r, Herald(v), PortLocation.OUTPUT) if self._postselect.has_condition: postselect = copy.copy(self._postselect) - postselect.merge(proc.post_select_fn) - proc.set_postselection(postselect) + postselect.merge(exp.post_select_fn) + exp.set_postselection(postselect) # We need to retrieve the new heralds as they are actually counting user photons - proc.min_detected_photons_filter(self._min_detected_photons_filter - sum_new_heralds) + exp.min_detected_photons_filter(self._min_detected_photons_filter - sum_new_heralds) else: # In that case, the new heralds are user-defined heralds that we can safely simulate. # We can't filter more due to potential losses that would depend on the FF parts of the circuit - proc.min_detected_photons_filter(0) + exp.min_detected_photons_filter(0) from .simulator_factory import SimulatorFactory # Avoids a circular import - sim = SimulatorFactory.build(proc) + sim = SimulatorFactory.build(exp, self._backend, self._noise_model) if self._precision is not None: sim.set_precision(self._precision) sim.set_silent(True) - return sim, input_state, detectors + proc.detectors[m:] + return sim, input_state, detectors + exp.detectors[m:] def _simulate(self, input_state: SVDistribution | tuple[Source, BasicState], - components: list[tuple[tuple, AComponent | Processor]], + components: list[tuple[tuple, AComponent | Experiment]], m: int, detectors: list[IDetector], prog_cb: ProgressCallback = None, diff --git a/perceval/simulators/simulator_factory.py b/perceval/simulators/simulator_factory.py index a29060e8e..9f8612437 100644 --- a/perceval/simulators/simulator_factory.py +++ b/perceval/simulators/simulator_factory.py @@ -26,6 +26,7 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +from perceval.utils import NoiseModel from perceval.utils.postselect import PostSelect from .feed_forward_simulator import FFSimulator @@ -37,7 +38,6 @@ from ._simulator_utils import _unitary_components_to_circuit from perceval.components import ACircuit, TD, LC, Experiment, AFFConfigurator from perceval.backends import ABackend, SLOSExqaliburBackend, BACKEND_LIST, ExqaliburBackendWrapper -from perceval.runtime import Processor class SimulatorFactory: @@ -48,8 +48,9 @@ class SimulatorFactory: """ @staticmethod - def build(circuit: ACircuit | Processor | Experiment | list, + def build(circuit: ACircuit | Experiment | list, backend: ABackend | str = None, + noise: NoiseModel = None, **kwargs) -> ISimulator: """ :param circuit: The optical circuit to build the simulation layers around. @@ -58,6 +59,7 @@ def build(circuit: ACircuit | Processor | Experiment | list, :param backend: (Optional) Any probampli capable backend instance or name. If no backend is passed, then the processor backend name is used if the first parameter's type is Processor. Ultimately, the fallback is a SLOS backend instantiated without any configuration (i.e. no mask) + :param noise: The noise to transmit to the Simulator, if relevant :param kwargs: If backend is a string, the kwargs are transmitted to the instantiation of the backend. :return: A simulator object with the input circuit set """ @@ -69,9 +71,11 @@ def build(circuit: ACircuit | Processor | Experiment | list, min_detected_photons = None post_select = PostSelect() heralds = None - noise = None + noise = noise m = None + from perceval.runtime import Processor # TODO: remove (deprecated since 1.3) + if isinstance(circuit, Processor): if backend is None: # If no backend was chosen, the backend type set in the Processor is used @@ -85,7 +89,8 @@ def build(circuit: ACircuit | Processor | Experiment | list, min_detected_photons = circuit.min_photons_filter post_select = circuit.post_select_fn heralds = circuit.heralds - noise = circuit.noise + if circuit.noise is not None: + noise = circuit.noise if circuit.is_unitary: circuit = circuit.unitary_circuit() else: diff --git a/perceval/utils/parameter.py b/perceval/utils/parameter.py index 049288f50..f40e5ee84 100644 --- a/perceval/utils/parameter.py +++ b/perceval/utils/parameter.py @@ -212,12 +212,6 @@ def max(self, m: bool): """ self._max = m - @deprecated(version = "v1.1", reason = "Use id(self) or self.is_identical_to(other) instead") - @property - def pid(self): - r"""Unique identifier for the parameter""" - return id(self) - def __mul__(self, other): if isinstance(other, Parameter): return Expression(f"({self.name}*{other.name})", self._merge_param_sets(other)) diff --git a/tests/providers/kipu/__init__.py b/tests/providers/kipu/__init__.py new file mode 100644 index 000000000..e6cc45e88 --- /dev/null +++ b/tests/providers/kipu/__init__.py @@ -0,0 +1,28 @@ +# MIT License +# +# Copyright (c) 2022 Quandela +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# As a special exception, the copyright holders of exqalibur library give you +# permission to combine exqalibur with code included in the standard release of +# Perceval under the MIT license (or modified versions of such code). You may +# copy and distribute such a combined system following the terms of the MIT +# license for both exqalibur and Perceval. This exception for the usage of +# exqalibur is limited to the python bindings used by Perceval. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. diff --git a/tests/providers/test_kipu_mappings.py b/tests/providers/kipu/test_kipu_mappings.py similarity index 63% rename from tests/providers/test_kipu_mappings.py rename to tests/providers/kipu/test_kipu_mappings.py index 7ad4f0749..da117f341 100644 --- a/tests/providers/test_kipu_mappings.py +++ b/tests/providers/kipu/test_kipu_mappings.py @@ -1,3 +1,32 @@ +# MIT License +# +# Copyright (c) 2022 Quandela +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# As a special exception, the copyright holders of exqalibur library give you +# permission to combine exqalibur with code included in the standard release of +# Perceval under the MIT license (or modified versions of such code). You may +# copy and distribute such a combined system following the terms of the MIT +# license for both exqalibur and Perceval. This exception for the usage of +# exqalibur is limited to the python bindings used by Perceval. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + # MIT License # # Copyright (c) 2026 Kipu Quantum GmbH diff --git a/tests/providers/kipu/test_kipu_provider_registration.py b/tests/providers/kipu/test_kipu_provider_registration.py new file mode 100644 index 000000000..502074640 --- /dev/null +++ b/tests/providers/kipu/test_kipu_provider_registration.py @@ -0,0 +1,80 @@ +# MIT License +# +# Copyright (c) 2022 Quandela +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# As a special exception, the copyright holders of exqalibur library give you +# permission to combine exqalibur with code included in the standard release of +# Perceval under the MIT license (or modified versions of such code). You may +# copy and distribute such a combined system following the terms of the MIT +# license for both exqalibur and Perceval. This exception for the usage of +# exqalibur is limited to the python bindings used by Perceval. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +# MIT License +# +# Copyright (c) 2026 Kipu Quantum GmbH +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# As a special exception, the copyright holders of exqalibur library give you +# permission to combine exqalibur with code included in the standard release of +# Perceval under the MIT license (or modified versions of such code). You may +# copy and distribute such a combined system following the terms of the MIT +# license for both exqalibur and Perceval. This exception for the usage of +# exqalibur is limited to the python bindings used by Perceval. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from perceval.runtime import ISession +from perceval.providers import ProviderFactory + + +def test_kipu_in_provider_list(): + assert "Kipu" in ProviderFactory.list() + + +def test_get_kipu_provider(): + session = ProviderFactory.get_provider( + "Kipu", + platform_name="quandela.sim.belenos", + token="t", + organization_id="org-1", + ) + assert isinstance(session, ISession) + + +def test_kipu_package_exports(): + from perceval.providers.kipu import Session, KipuRPCHandler + assert Session is not None + assert KipuRPCHandler is not None diff --git a/tests/providers/test_kipu_rpc_handler.py b/tests/providers/kipu/test_kipu_rpc_handler.py similarity index 89% rename from tests/providers/test_kipu_rpc_handler.py rename to tests/providers/kipu/test_kipu_rpc_handler.py index 71e3b4057..f3f6649dd 100644 --- a/tests/providers/test_kipu_rpc_handler.py +++ b/tests/providers/kipu/test_kipu_rpc_handler.py @@ -1,3 +1,32 @@ +# MIT License +# +# Copyright (c) 2022 Quandela +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# As a special exception, the copyright holders of exqalibur library give you +# permission to combine exqalibur with code included in the standard release of +# Perceval under the MIT license (or modified versions of such code). You may +# copy and distribute such a combined system following the terms of the MIT +# license for both exqalibur and Perceval. This exception for the usage of +# exqalibur is limited to the python bindings used by Perceval. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + # MIT License # # Copyright (c) 2026 Kipu Quantum GmbH diff --git a/tests/providers/test_kipu_session.py b/tests/providers/kipu/test_kipu_session.py similarity index 67% rename from tests/providers/test_kipu_session.py rename to tests/providers/kipu/test_kipu_session.py index 44c59c5b5..a52655caa 100644 --- a/tests/providers/test_kipu_session.py +++ b/tests/providers/kipu/test_kipu_session.py @@ -1,3 +1,32 @@ +# MIT License +# +# Copyright (c) 2022 Quandela +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# As a special exception, the copyright holders of exqalibur library give you +# permission to combine exqalibur with code included in the standard release of +# Perceval under the MIT license (or modified versions of such code). You may +# copy and distribute such a combined system following the terms of the MIT +# license for both exqalibur and Perceval. This exception for the usage of +# exqalibur is limited to the python bindings used by Perceval. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + # MIT License # # Copyright (c) 2026 Kipu Quantum GmbH @@ -33,7 +62,7 @@ from unittest.mock import patch from perceval.providers.kipu.kipu_session import Session -from perceval.runtime.remote_processor import RemoteProcessor +from perceval.runtime import RemoteProcessor _PLATFORM_DETAILS = { "name": "quandela.sim.belenos", diff --git a/tests/providers/quandela/__init__.py b/tests/providers/quandela/__init__.py new file mode 100644 index 000000000..e6cc45e88 --- /dev/null +++ b/tests/providers/quandela/__init__.py @@ -0,0 +1,28 @@ +# MIT License +# +# Copyright (c) 2022 Quandela +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# As a special exception, the copyright holders of exqalibur library give you +# permission to combine exqalibur with code included in the standard release of +# Perceval under the MIT license (or modified versions of such code). You may +# copy and distribute such a combined system following the terms of the MIT +# license for both exqalibur and Perceval. This exception for the usage of +# exqalibur is limited to the python bindings used by Perceval. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. diff --git a/tests/runtime/_mock_rpc_handler.py b/tests/providers/quandela/_mock_rpc_handler.py similarity index 99% rename from tests/runtime/_mock_rpc_handler.py rename to tests/providers/quandela/_mock_rpc_handler.py index 109d9310f..34a40e8ab 100644 --- a/tests/runtime/_mock_rpc_handler.py +++ b/tests/providers/quandela/_mock_rpc_handler.py @@ -37,7 +37,7 @@ import requests import responses -from perceval.runtime.rpc_handler import ( +from perceval.providers.quandela.rpc_handler import ( RPCHandler, _ENDPOINT_JOB_CANCEL, _ENDPOINT_JOB_CREATE, @@ -77,7 +77,7 @@ ARCHITECTURE_PLATFORM_INFO = { 'id': str(uuid.uuid4()), 'name': None, - 'perfs': {}, + 'perfs': {"g2 (%)": 1.2}, 'specs': { 'available_commands': ['probs'], 'connected_input_modes': [0, 2, 4, 6, 8, 10], diff --git a/tests/providers/quandela/test_communication_layer.py b/tests/providers/quandela/test_communication_layer.py new file mode 100644 index 000000000..8970899db --- /dev/null +++ b/tests/providers/quandela/test_communication_layer.py @@ -0,0 +1,111 @@ +# MIT License +# +# Copyright (c) 2022 Quandela +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# As a special exception, the copyright holders of exqalibur library give you +# permission to combine exqalibur with code included in the standard release of +# Perceval under the MIT license (or modified versions of such code). You may +# copy and distribute such a combined system following the terms of the MIT +# license for both exqalibur and Perceval. This exception for the usage of +# exqalibur is limited to the python bindings used by Perceval. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import pytest + +from perceval import ProcessorType, Computation, CommandFactory, Experiment, PayloadGenerator, RunningStatus, JobStatus +from perceval.providers.quandela.rpc_handler import RPCHandler +from perceval.runtime.communication_layer import RPCBasedCommunicationLayer +from perceval.runtime.platform_specs import PlatformSpecs + +from ._mock_rpc_handler import RPCHandlerResponsesBuilder, ARCHITECTURE_PLATFORM_INFO + +TOKEN = "test_token" +PLATFORM_NAME = "sim:test" +URL = "https://test" + + +# Note: Quandela, Kipu and Scaleway Communication layers share 95% of their code through the RPCBasedCommunicationLayer, +# so testing this one should be enough + +def test_communication_layer_platform_status(): + rpc_handler = RPCHandler(PLATFORM_NAME, URL, TOKEN) + RPCHandlerResponsesBuilder(rpc_handler, ARCHITECTURE_PLATFORM_INFO, use_new_platform_details_url=True) + + comm = RPCBasedCommunicationLayer(rpc_handler) + + specs = comm.get_specs() + assert isinstance(specs, PlatformSpecs) + expected = PlatformSpecs(ARCHITECTURE_PLATFORM_INFO["specs"]) + expected.type = ProcessorType[ARCHITECTURE_PLATFORM_INFO["type"].upper()] + assert specs == expected + + perfs = comm.get_performances() + assert perfs == ARCHITECTURE_PLATFORM_INFO["perfs"] + + platform_status = comm.get_remote_status() + assert platform_status == ARCHITECTURE_PLATFORM_INFO["status"] + + commands = comm.get_commands() + assert commands == specs.commands + + +def test_communication_layer_job(): + expected_running_status = [RunningStatus.RUNNING, RunningStatus.CANCELED, RunningStatus.SUCCESS] + + rpc_handler = RPCHandler(PLATFORM_NAME, URL, TOKEN) + builder = RPCHandlerResponsesBuilder(rpc_handler, + ARCHITECTURE_PLATFORM_INFO, + expected_running_status, + use_new_platform_details_url=True) + builder.set_job_status_sequence(expected_running_status) + + comm = RPCBasedCommunicationLayer(rpc_handler) + + computation = Computation(CommandFactory.probs, Experiment()) + + with pytest.raises(Exception): + comm.send(PayloadGenerator.from_computation(computation)) + + computation.add_params(max_shots = 10000) + + remote_id = comm.send(PayloadGenerator.from_computation(computation)) + status = comm.get_job_status(remote_id) + assert isinstance(status, JobStatus) + assert status.status == expected_running_status[0] + assert status.progress == 0.5 + + comm.cancel(remote_id) # Just check nothrow, the ResponseBuilder doesn't change the status to canceled by itself + + remote_id = comm.send(PayloadGenerator.from_computation(computation)) + status = comm.get_job_status(remote_id) + assert isinstance(status, JobStatus) + assert status.status == expected_running_status[1] + + # Due to the way the ResponseBuilder works, we need to create another job + remote_id = comm.send(PayloadGenerator.from_computation(computation)) + status = comm.get_job_status(remote_id) + assert isinstance(status, JobStatus) + assert status.status == expected_running_status[2] + assert status.progress == 1. + + res = comm.get_results(remote_id) + assert "results" in res # Check this is the correct level of results + assert "physical_perf" in res + assert "logical_perf" in res diff --git a/tests/runtime/test_rpc_handler.py b/tests/providers/quandela/test_rpc_handler.py similarity index 97% rename from tests/runtime/test_rpc_handler.py rename to tests/providers/quandela/test_rpc_handler.py index 94c5570ef..789e44210 100644 --- a/tests/runtime/test_rpc_handler.py +++ b/tests/providers/quandela/test_rpc_handler.py @@ -33,9 +33,9 @@ import responses -from tests.runtime._mock_rpc_handler import RPCHandlerResponsesBuilder, DEFAULT_PLATFORM_INFO, ARCHITECTURE_PLATFORM_INFO +from ._mock_rpc_handler import RPCHandlerResponsesBuilder, DEFAULT_PLATFORM_INFO, ARCHITECTURE_PLATFORM_INFO -from perceval.runtime.rpc_handler import ( +from perceval.providers.quandela.rpc_handler import ( RPCHandler, _ENDPOINT_JOB_CANCEL, _ENDPOINT_JOB_CREATE, diff --git a/tests/runtime/legacy/__init__.py b/tests/runtime/legacy/__init__.py new file mode 100644 index 000000000..e6cc45e88 --- /dev/null +++ b/tests/runtime/legacy/__init__.py @@ -0,0 +1,28 @@ +# MIT License +# +# Copyright (c) 2022 Quandela +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# As a special exception, the copyright holders of exqalibur library give you +# permission to combine exqalibur with code included in the standard release of +# Perceval under the MIT license (or modified versions of such code). You may +# copy and distribute such a combined system following the terms of the MIT +# license for both exqalibur and Perceval. This exception for the usage of +# exqalibur is limited to the python bindings used by Perceval. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. diff --git a/tests/runtime/test_job.py b/tests/runtime/legacy/test_job.py similarity index 99% rename from tests/runtime/test_job.py rename to tests/runtime/legacy/test_job.py index ba5a03838..3bba16933 100644 --- a/tests/runtime/test_job.py +++ b/tests/runtime/legacy/test_job.py @@ -35,7 +35,7 @@ from perceval.runtime.job_status import RunningStatus from perceval.algorithm import Sampler -from .._test_utils import LogChecker +from tests._test_utils import LogChecker PERIOD = 0.1 diff --git a/tests/runtime/test_job_group.py b/tests/runtime/legacy/test_job_group.py similarity index 98% rename from tests/runtime/test_job_group.py rename to tests/runtime/legacy/test_job_group.py index f9d20e64d..1f489231d 100644 --- a/tests/runtime/test_job_group.py +++ b/tests/runtime/legacy/test_job_group.py @@ -35,11 +35,11 @@ import responses from perceval.runtime import JobGroup, RemoteJob, RunningStatus -from perceval.runtime.rpc_handler import RPCHandler +from perceval.providers.quandela.rpc_handler import RPCHandler from perceval.components import Processor, catalog from perceval.algorithm import Sampler from perceval.utils import BasicState -from tests.runtime._mock_rpc_handler import RPCHandlerResponsesBuilder, CloudEndpoint +from tests.providers.quandela._mock_rpc_handler import RPCHandlerResponsesBuilder, CloudEndpoint TEST_JG_NAME = 'UnitTest_Job_Group' diff --git a/tests/runtime/test_processor.py b/tests/runtime/legacy/test_processor.py similarity index 99% rename from tests/runtime/test_processor.py rename to tests/runtime/legacy/test_processor.py index 3f4f662df..0d139b879 100644 --- a/tests/runtime/test_processor.py +++ b/tests/runtime/legacy/test_processor.py @@ -38,8 +38,8 @@ from perceval.backends import Clifford2017Backend from perceval.runtime import Processor -from ._mock_rpc_handler import get_rpc_handler_for_tests -from .._test_utils import LogChecker, assert_svd_close, assert_bsd_close +from tests.providers.quandela._mock_rpc_handler import get_rpc_handler_for_tests +from tests._test_utils import LogChecker, assert_svd_close, assert_bsd_close @patch.object(pcvl.utils.logging.ExqaliburLogger, "warn") diff --git a/tests/runtime/test_remote_job.py b/tests/runtime/legacy/test_remote_job.py similarity index 97% rename from tests/runtime/test_remote_job.py rename to tests/runtime/legacy/test_remote_job.py index 56bcbf253..b40be9814 100644 --- a/tests/runtime/test_remote_job.py +++ b/tests/runtime/legacy/test_remote_job.py @@ -37,13 +37,13 @@ from perceval.algorithm import Sampler from perceval.components import Processor from perceval.runtime import RemoteJob, RunningStatus -from perceval.runtime.rpc_handler import RPCHandler +from perceval.providers.quandela.rpc_handler import RPCHandler from perceval.utils.dist_metrics import tvd_dist from perceval.utils.conversion import sample_count_to_probs from perceval.utils.logging import channel -from .._test_utils import LogChecker -from tests.runtime._mock_rpc_handler import RPCHandlerResponsesBuilder, get_rpc_handler_for_tests, _TIMESTAMP +from tests._test_utils import LogChecker +from tests.providers.quandela._mock_rpc_handler import RPCHandlerResponsesBuilder, get_rpc_handler_for_tests, _TIMESTAMP SIMPLE_PAYLOAD = {"command": "probs", "circuit": ":PCVL:zip:eJyzCnAO87FydM4sSi7NLLFydfTM9K9wdI7MSg52DsyO9AkNCtWu9DANqMj3cg50hAPP9GwvBM+xEKgWwXPxRFNrEegYlu/jDNTj7mzoGhZQnGEWYkF1ewCY7jxM", "input_state": ":PCVL:BasicState:|1,1>", "parameters": {"min_detected_photons": 2}, "max_shots": 10000, "job_context": None} diff --git a/tests/runtime/test_shots_estimate.py b/tests/runtime/legacy/test_shots_estimate.py similarity index 97% rename from tests/runtime/test_shots_estimate.py rename to tests/runtime/legacy/test_shots_estimate.py index 8940cf00d..d7392b10f 100644 --- a/tests/runtime/test_shots_estimate.py +++ b/tests/runtime/legacy/test_shots_estimate.py @@ -27,7 +27,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from perceval.runtime.platform_specs import PlatformSpecs -from perceval.runtime.remote_processor import AProcessor, RemoteProcessor, TRANSMITTANCE_KEY +from perceval.runtime.legacy.remote_processor import AProcessor, RemoteProcessor, TRANSMITTANCE_KEY from perceval.components import Unitary, BS, PS, Experiment, Detector, FFCircuitProvider, Circuit from perceval.utils import Matrix, BasicState, P import random diff --git a/tests/runtime/test_payload_generation.py b/tests/runtime/test_payload_generation.py index f6137c692..cc449fee7 100644 --- a/tests/runtime/test_payload_generation.py +++ b/tests/runtime/test_payload_generation.py @@ -32,7 +32,7 @@ from perceval import RemoteProcessor, BasicState, catalog, PayloadGenerator, SimulatedComputer, NoiseModel from perceval.serialization._constants import ZIP_PREFIX -from tests.runtime._mock_rpc_handler import get_rpc_handler_for_tests +from tests.providers.quandela._mock_rpc_handler import get_rpc_handler_for_tests COMMAND_NAME = 'my_command' diff --git a/tests/runtime/test_remote_computer.py b/tests/runtime/test_remote_computer.py index 475cd9fd2..37ca54fc9 100644 --- a/tests/runtime/test_remote_computer.py +++ b/tests/runtime/test_remote_computer.py @@ -76,6 +76,9 @@ def get_commands(self) -> list[Command]: def cancel(self, remote_id: RemoteId) -> None: remote_id.cancel() + def get_availability(self) -> int: + return self.computer.available_jobs + def test_remote_computer_basic(): # Checks that the communication layer is properly used diff --git a/tests/utils/test_log.py b/tests/utils/test_log.py index 5b9b7e3df..98ce4e4b2 100644 --- a/tests/utils/test_log.py +++ b/tests/utils/test_log.py @@ -34,7 +34,7 @@ from perceval.utils import LoggerConfig from perceval.utils.logging import ExqaliburLogger, PythonLogger, level, channel -from tests.runtime._mock_rpc_handler import get_rpc_handler_for_tests +from tests.providers.quandela._mock_rpc_handler import get_rpc_handler_for_tests DEFAULT_CONFIG = {'use_python_logger': False, 'enable_file': False, 'channels': {'general': {'level': 'off'}, 'resources': {'level': 'off'}, 'user': {'level': 'warn'}}}