diff --git a/.gitignore b/.gitignore index 63d2ad3b..69276097 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ dist/ env* *.egg-info/ __venv__/ +venv \ No newline at end of file diff --git a/pulser-core/pulser/backend/qpu.py b/pulser-core/pulser/backend/qpu.py index f46f07be..477457c5 100644 --- a/pulser-core/pulser/backend/qpu.py +++ b/pulser-core/pulser/backend/qpu.py @@ -65,10 +65,7 @@ def run( self.validate_job_params( job_params or [], self._sequence.device.max_runs ) - results = self._connection.submit( - self._sequence, job_params=job_params, wait=wait - ) - return cast(RemoteResults, results) + return cast(RemoteResults, super().run(job_params, wait)) @staticmethod def validate_job_params( diff --git a/pulser-core/pulser/backend/remote.py b/pulser-core/pulser/backend/remote.py index 582cc059..6abed00e 100644 --- a/pulser-core/pulser/backend/remote.py +++ b/pulser-core/pulser/backend/remote.py @@ -12,12 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. """Base classes for remote backend execution.""" + from __future__ import annotations import typing +import warnings from abc import ABC, abstractmethod +from collections.abc import Callable from enum import Enum, auto -from typing import Any, Mapping, TypedDict +from functools import wraps +from types import TracebackType +from typing import Any, Mapping, Type, TypedDict, TypeVar, cast from pulser.backend.abc import Backend from pulser.devices import Device @@ -44,6 +49,21 @@ class SubmissionStatus(Enum): PAUSED = auto() +class BatchStatus(Enum): + """Status of a batch. + + Same as SubmissionStatus, needed because we renamed Submission -> Batch. + """ + + PENDING = auto() + RUNNING = auto() + DONE = auto() + CANCELED = auto() + TIMED_OUT = auto() + ERROR = auto() + PAUSED = auto() + + class JobStatus(Enum): """Status of a remote job.""" @@ -61,34 +81,63 @@ class RemoteResultsError(Exception): pass +F = TypeVar("F", bound=Callable) + + +def _deprecate_submission_id(func: F) -> F: + @wraps(func) + def wrapper(self: RemoteResults, *args: Any, **kwargs: Any) -> Any: + if "submission_id" in kwargs: + # 'batch_id' is the first positional arg so if len(args) > 0, + # then it is being given + if "batch_id" in kwargs or args: + raise ValueError( + "'submission_id' and 'batch_id' cannot be simultaneously" + " specified. Please provide only the 'batch_id'." + ) + warnings.warn( + "'submission_id' has been deprecated and replaced by " + "'batch_id'.", + category=DeprecationWarning, + stacklevel=3, + ) + kwargs["batch_id"] = kwargs.pop("submission_id") + return func(self, *args, **kwargs) + + return cast(F, wrapper) + + class RemoteResults(Results): """A collection of results obtained through a remote connection. + Warns: + DeprecationWarning: If 'submission_id' is given instead of 'batch_id'. + Args: - submission_id: The ID that identifies the submission linked to - the results. - connection: The remote connection over which to get the submission's + batch_id: The ID that identifies the batch linked to the results. + connection: The remote connection over which to get the batch's status and fetch the results. - job_ids: If given, specifies which jobs within the submission should + job_ids: If given, specifies which jobs within the batch should be included in the results and in what order. If left undefined, all jobs are included. """ + @_deprecate_submission_id def __init__( self, - submission_id: str, + batch_id: str, connection: RemoteConnection, job_ids: list[str] | None = None, ): """Instantiates a new collection of remote results.""" - self._submission_id = submission_id + self._batch_id = batch_id self._connection = connection if job_ids is not None and not set(job_ids).issubset( - all_job_ids := self._connection._get_job_ids(self._submission_id) + all_job_ids := self._connection._get_job_ids(self._batch_id) ): unknown_ids = [id_ for id_ in job_ids if id_ not in all_job_ids] raise RuntimeError( - f"Submission {self._submission_id!r} does not contain jobs " + f"Batch {self._batch_id!r} does not contain jobs " f"{unknown_ids}." ) self._job_ids = job_ids @@ -98,27 +147,53 @@ def results(self) -> tuple[Result, ...]: """The actual results, obtained after execution is done.""" return self._results + @property + def _submission_id(self) -> str: + """The same as the batch ID, kept for backwards compatibility.""" + warnings.warn( + "'RemoteResults._submission_id' has been deprecated, please use" + "'RemoteResults.batch_id' instead.", + category=DeprecationWarning, + stacklevel=2, + ) + return self._batch_id + @property def batch_id(self) -> str: """The ID of the batch containing these results.""" - return self._submission_id + return self._batch_id @property def job_ids(self) -> list[str]: - """The IDs of the jobs within this results submission.""" + """The IDs of the jobs within these results' batch.""" if self._job_ids is None: - return self._connection._get_job_ids(self._submission_id) + return self._connection._get_job_ids(self._batch_id) return self._job_ids def get_status(self) -> SubmissionStatus: - """Gets the status of the remote submission.""" - return self._connection._get_submission_status(self._submission_id) + """Gets the status of the remote submission. + + Warning: + This method has been deprecated, please use + `RemoteResults.get_batch_status()` instead. + """ + warnings.warn( + "'RemoteResults.get_status()' has been deprecated, please use" + "'RemoteResults.get_batch_status()' instead.", + category=DeprecationWarning, + stacklevel=2, + ) + return SubmissionStatus[self.get_batch_status().name] - def get_available_results(self, submission_id: str) -> dict[str, Result]: - """Returns the available results of a submission. + def get_batch_status(self) -> BatchStatus: + """Gets the status of the batch linked to these results.""" + return self._connection._get_batch_status(self._batch_id) + + def get_available_results(self) -> dict[str, Result]: + """Returns the available results. Unlike the `results` property, this method does not raise an error if - some jobs associated to the submission do not have results. + some of the jobs do not have results. Returns: dict[str, Result]: A dictionary mapping the job ID to its results. @@ -127,7 +202,7 @@ def get_available_results(self, submission_id: str) -> dict[str, Result]: results = { k: v[1] for k, v in self._connection._query_job_progress( - submission_id + self.batch_id ).items() if v[1] is not None } @@ -141,7 +216,7 @@ def __getattr__(self, name: str) -> Any: try: self._results = tuple( self._connection._fetch_result( - self._submission_id, self._job_ids + self.batch_id, self._job_ids ) ) return self._results @@ -161,42 +236,43 @@ class RemoteConnection(ABC): @abstractmethod def submit( - self, sequence: Sequence, wait: bool = False, **kwargs: Any + self, + sequence: Sequence, + wait: bool = False, + open: bool = True, + batch_id: str | None = None, + **kwargs: Any, ) -> RemoteResults | tuple[RemoteResults, ...]: """Submit a job for execution.""" pass @abstractmethod def _fetch_result( - self, submission_id: str, job_ids: list[str] | None + self, batch_id: str, job_ids: list[str] | None ) -> typing.Sequence[Result]: - """Fetches the results of a completed submission.""" + """Fetches the results of a completed batch.""" pass @abstractmethod def _query_job_progress( - self, submission_id: str + self, batch_id: str ) -> Mapping[str, tuple[JobStatus, Result | None]]: - """Fetches the status and results of all the jobs in a submission. + """Fetches the status and results of all the jobs in a batch. Unlike `_fetch_result`, this method does not raise an error if some - jobs associated to the submission do not have results. + jobs in the batch do not have results. It returns a dictionnary mapping the job ID to its status and results. """ pass @abstractmethod - def _get_submission_status(self, submission_id: str) -> SubmissionStatus: - """Gets the status of a submission from its ID. - - Not all SubmissionStatus values must be covered, but at least - SubmissionStatus.DONE is expected. - """ + def _get_batch_status(self, batch_id: str) -> BatchStatus: + """Gets the status of a batch from its ID.""" pass - def _get_job_ids(self, submission_id: str) -> list[str]: - """Gets all the job IDs within a submission.""" + def _get_job_ids(self, batch_id: str) -> list[str]: + """Gets all the job IDs within a batch.""" raise NotImplementedError( "Unable to find job IDs through this remote connection." ) @@ -208,6 +284,17 @@ def fetch_available_devices(self) -> dict[str, Device]: "remote connection." ) + def _close_batch(self, batch_id: str) -> None: + """Closes a batch using its ID.""" + raise NotImplementedError( # pragma: no cover + "Unable to close batch through this remote connection" + ) + + @abstractmethod + def supports_open_batch(self) -> bool: + """Flag to confirm this class can support creating an open batch.""" + pass + class RemoteBackend(Backend): """A backend for sequence execution through a remote connection. @@ -234,6 +321,39 @@ def __init__( "'connection' must be a valid RemoteConnection instance." ) self._connection = connection + self._batch_id: str | None = None + + def run( + self, job_params: list[JobParams] | None = None, wait: bool = False + ) -> RemoteResults | tuple[RemoteResults, ...]: + """Runs the sequence on the remote backend and returns the result. + + Args: + job_params: A list of parameters for each job to execute. Each + mapping must contain a defined 'runs' field specifying + the number of times to run the same sequence. If the sequence + is parametrized, the values for all the variables necessary + to build the sequence must be given in it's own mapping, for + each job, under the 'variables' field. + wait: Whether to wait until the results of the jobs become + available. If set to False, the call is non-blocking and the + obtained results' status can be checked using their `status` + property. + + Returns: + The results, which can be accessed once all sequences have been + successfully executed. + """ + return self._connection.submit( + self._sequence, + job_params=job_params, + wait=wait, + **self._submit_kwargs(), + ) + + def _submit_kwargs(self) -> dict[str, Any]: + """Keyword arguments given to any call to RemoteConnection.submit().""" + return dict(batch_id=self._batch_id) @staticmethod def _type_check_job_params(job_params: list[JobParams] | None) -> None: @@ -247,3 +367,38 @@ def _type_check_job_params(job_params: list[JobParams] | None) -> None: "All elements of 'job_params' must be dictionaries; " f"got {type(d)} instead." ) + + def open_batch(self) -> _OpenBatchContextManager: + """Creates an open batch within a context manager object.""" + if not self._connection.supports_open_batch(): + raise NotImplementedError( + "Unable to execute open_batch using this remote connection" + ) + return _OpenBatchContextManager(self) + + +class _OpenBatchContextManager: + def __init__(self, backend: RemoteBackend) -> None: + self.backend = backend + + def __enter__(self) -> _OpenBatchContextManager: + batch = cast( + RemoteResults, + self.backend._connection.submit( + self.backend._sequence, + open=True, + **self.backend._submit_kwargs(), + ), + ) + self.backend._batch_id = batch.batch_id + return self + + def __exit__( + self, + exc_type: Type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + if self.backend._batch_id: + self.backend._connection._close_batch(self.backend._batch_id) + self.backend._batch_id = None diff --git a/pulser-pasqal/pulser_pasqal/backends.py b/pulser-pasqal/pulser_pasqal/backends.py index adb71033..1051178e 100644 --- a/pulser-pasqal/pulser_pasqal/backends.py +++ b/pulser-pasqal/pulser_pasqal/backends.py @@ -15,7 +15,7 @@ from __future__ import annotations from dataclasses import fields -from typing import ClassVar +from typing import Any, ClassVar import pasqal_cloud @@ -88,12 +88,14 @@ def run( "All elements of 'job_params' must specify 'runs'" + suffix ) - return self._connection.submit( - self._sequence, - job_params=job_params, + return super().run(job_params, wait) + + def _submit_kwargs(self) -> dict[str, Any]: + """Keyword arguments given to any call to RemoteConnection.submit().""" + return dict( + batch_id=self._batch_id, emulator=self.emulator, config=self._config, - wait=wait, mimic_qpu=self._mimic_qpu, ) diff --git a/pulser-pasqal/pulser_pasqal/pasqal_cloud.py b/pulser-pasqal/pulser_pasqal/pasqal_cloud.py index c7702da5..5cb8de9c 100644 --- a/pulser-pasqal/pulser_pasqal/pasqal_cloud.py +++ b/pulser-pasqal/pulser_pasqal/pasqal_cloud.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """Allows to connect to PASQAL's cloud platform to run sequences.""" + from __future__ import annotations import json @@ -31,12 +32,12 @@ from pulser.backend.config import EmulatorConfig from pulser.backend.qpu import QPUBackend from pulser.backend.remote import ( + BatchStatus, JobParams, JobStatus, RemoteConnection, RemoteResults, RemoteResultsError, - SubmissionStatus, ) from pulser.devices import Device from pulser.json.abstract_repr.deserializer import deserialize_device @@ -92,16 +93,20 @@ def __init__( **kwargs: Any, ): """Initializes a connection to the Pasqal cloud platform.""" - project_id_ = project_id or kwargs.pop("group_id", "") self._sdk_connection = pasqal_cloud.SDK( username=username, password=password, - project_id=project_id_, + project_id=project_id, **kwargs, ) def submit( - self, sequence: Sequence, wait: bool = False, **kwargs: Any + self, + sequence: Sequence, + wait: bool = False, + open: bool = False, + batch_id: str | None = None, + **kwargs: Any, ) -> RemoteResults: """Submits the sequence for execution on a remote Pasqal backend.""" if not sequence.is_measured(): @@ -164,16 +169,36 @@ def submit( emulator=emulator, strict_validation=mimic_qpu, ) - create_batch_fn = backoff_decorator(self._sdk_connection.create_batch) - batch = create_batch_fn( - serialized_sequence=sequence.to_abstract_repr(), - jobs=job_params or [], # type: ignore[arg-type] - emulator=emulator, - configuration=configuration, - wait=wait, - ) - return RemoteResults(batch.id, self) + # If batch_id is not empty, then we can submit new jobs to a + # batch we just created otherwise, create a new one with + # _sdk_connection.create_batch() + if batch_id: + submit_jobs_fn = backoff_decorator(self._sdk_connection.add_jobs) + old_job_ids = self._get_job_ids(batch_id) + batch = submit_jobs_fn( + batch_id, + jobs=job_params or [], # type: ignore[arg-type] + ) + new_job_ids = [ + job_id + for job_id in self._get_job_ids(batch_id) + if job_id not in old_job_ids + ] + else: + create_batch_fn = backoff_decorator( + self._sdk_connection.create_batch + ) + batch = create_batch_fn( + serialized_sequence=sequence.to_abstract_repr(), + jobs=job_params or [], # type: ignore[arg-type] + emulator=emulator, + configuration=configuration, + wait=wait, + open=open, + ) + new_job_ids = self._get_job_ids(batch.id) + return RemoteResults(batch.id, self, job_ids=new_job_ids) @backoff_decorator def fetch_available_devices(self) -> dict[str, Device]: @@ -185,10 +210,10 @@ def fetch_available_devices(self) -> dict[str, Device]: } def _fetch_result( - self, submission_id: str, job_ids: list[str] | None + self, batch_id: str, job_ids: list[str] | None ) -> tuple[Result, ...]: # For now, the results are always sampled results - jobs = self._query_job_progress(submission_id) + jobs = self._query_job_progress(batch_id) if job_ids is None: job_ids = list(jobs.keys()) @@ -208,10 +233,10 @@ def _fetch_result( return tuple(results) def _query_job_progress( - self, submission_id: str + self, batch_id: str ) -> Mapping[str, tuple[JobStatus, Result | None]]: get_batch_fn = backoff_decorator(self._sdk_connection.get_batch) - batch = get_batch_fn(id=submission_id) + batch = get_batch_fn(id=batch_id) seq_builder = Sequence.from_abstract_repr(batch.sequence_builder) reg = seq_builder.get_register(include_mappable=True) @@ -239,15 +264,15 @@ def _query_job_progress( return results @backoff_decorator - def _get_submission_status(self, submission_id: str) -> SubmissionStatus: - """Gets the status of a submission from its ID.""" - batch = self._sdk_connection.get_batch(id=submission_id) - return SubmissionStatus[batch.status] + def _get_batch_status(self, batch_id: str) -> BatchStatus: + """Gets the status of a batch from its ID.""" + batch = self._sdk_connection.get_batch(id=batch_id) + return BatchStatus[batch.status] @backoff_decorator - def _get_job_ids(self, submission_id: str) -> list[str]: - """Gets all the job IDs within a submission.""" - batch = self._sdk_connection.get_batch(id=submission_id) + def _get_job_ids(self, batch_id: str) -> list[str]: + """Gets all the job IDs within a batch.""" + batch = self._sdk_connection.get_batch(id=batch_id) return [job.id for job in batch.ordered_jobs] def _convert_configuration( @@ -274,3 +299,11 @@ def _convert_configuration( pasqal_config_kwargs["strict_validation"] = strict_validation return emu_cls(**pasqal_config_kwargs) + + def supports_open_batch(self) -> bool: + """Flag to confirm this class can support creating an open batch.""" + return True + + def _close_batch(self, batch_id: str) -> None: + """Closes the batch on pasqal cloud associated with the batch ID.""" + self._sdk_connection.close_batch(batch_id) diff --git a/pulser-pasqal/requirements.txt b/pulser-pasqal/requirements.txt index db2d9520..7b3f97f8 100644 --- a/pulser-pasqal/requirements.txt +++ b/pulser-pasqal/requirements.txt @@ -1,2 +1,2 @@ -pasqal-cloud ~= 0.8.1 +pasqal-cloud ~= 0.12 backoff ~= 2.2 \ No newline at end of file diff --git a/tests/test_backend.py b/tests/test_backend.py index 7318908a..358743a5 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -23,11 +23,12 @@ from pulser.backend.config import EmulatorConfig from pulser.backend.qpu import QPUBackend from pulser.backend.remote import ( + BatchStatus, JobStatus, RemoteConnection, RemoteResults, RemoteResultsError, - SubmissionStatus, + _OpenBatchContextManager, ) from pulser.devices import AnalogDevice, MockDevice from pulser.register import SquareLatticeLayout @@ -90,6 +91,8 @@ def test_emulator_config_type_errors(param, msg): class _MockConnection(RemoteConnection): def __init__(self): self._status_calls = 0 + self._support_open_batch = True + self._got_closed = "" self._progress_calls = 0 self.result = SampledResult( ("q0", "q1"), @@ -97,11 +100,20 @@ def __init__(self): bitstring_counts={"00": 100}, ) - def submit(self, sequence, wait: bool = False, **kwargs) -> RemoteResults: + def submit( + self, + sequence, + wait: bool = False, + open: bool = False, + batch_id: str | None = None, + **kwargs, + ) -> RemoteResults: + if batch_id: + return RemoteResults("dcba", self) return RemoteResults("abcd", self) def _fetch_result( - self, submission_id: str, job_ids: list[str] | None = None + self, batch_id: str, job_ids: list[str] | None = None ) -> typing.Sequence[Result]: self._progress_calls += 1 if self._progress_calls == 1: @@ -110,12 +122,18 @@ def _fetch_result( return (self.result,) def _query_job_progress( - self, submission_id: str + self, batch_id: str ) -> typing.Mapping[str, tuple[JobStatus, Result | None]]: return {"abcd": (JobStatus.DONE, self.result)} - def _get_submission_status(self, submission_id: str) -> SubmissionStatus: - return SubmissionStatus.DONE + def _get_batch_status(self, batch_id: str) -> BatchStatus: + return BatchStatus.DONE + + def _close_batch(self, batch_id: str) -> None: + self._got_closed = batch_id + + def supports_open_batch(self) -> bool: + return bool(self._support_open_batch) def test_remote_connection(): @@ -145,6 +163,7 @@ def test_qpu_backend(sequence): with pytest.raises(ValueError, match="defined from a `RegisterLayout`"): QPUBackend(seq, connection) seq = seq.switch_register(SquareLatticeLayout(5, 5, 5).square_register(2)) + with pytest.raises( ValueError, match="does not accept new register layouts" ): @@ -194,5 +213,29 @@ def test_qpu_backend(sequence): results = remote_results.results assert results[0].sampling_dist == {"00": 1.0} - available_results = remote_results.get_available_results("id") + # Test create a batch and submitting jobs via a context manager + # behaves as expected. + qpu = QPUBackend(seq, connection) + assert connection._got_closed == "" + with qpu.open_batch() as ob: + assert ob.backend is qpu + assert ob.backend._batch_id == "abcd" + assert isinstance(ob, _OpenBatchContextManager) + results = qpu.run(job_params=[{"runs": 200}]) + # batch_id should differ bc of how MockConnection is written + # confirms the batch_id was provided to submit() + assert results.batch_id == "dcba" + assert isinstance(results, RemoteResults) + assert qpu._batch_id is None + assert connection._got_closed == "abcd" + + connection._support_open_batch = False + qpu = QPUBackend(seq, connection) + with pytest.raises( + NotImplementedError, + match="Unable to execute open_batch using this remote connection", + ): + qpu.open_batch() + + available_results = remote_results.get_available_results() assert available_results == {"abcd": connection.result} diff --git a/tests/test_pasqal.py b/tests/test_pasqal.py index dfcc98a6..6382f589 100644 --- a/tests/test_pasqal.py +++ b/tests/test_pasqal.py @@ -27,6 +27,7 @@ import pulser_pasqal from pulser.backend.config import EmulatorConfig from pulser.backend.remote import ( + BatchStatus, JobStatus, RemoteConnection, RemoteResults, @@ -136,6 +137,8 @@ def mock_pasqal_cloud_sdk(mock_batch): mock_cloud_sdk.create_batch = MagicMock(return_value=mock_batch) mock_cloud_sdk.get_batch = MagicMock(return_value=mock_batch) + mock_cloud_sdk.add_jobs = MagicMock(return_value=mock_batch) + mock_cloud_sdk._close_batch = MagicMock(return_value=None) mock_cloud_sdk.get_device_specs_dict = MagicMock( return_value={test_device.name: test_device.to_abstract_repr()} ) @@ -152,6 +155,36 @@ def fixt(mock_batch): @pytest.mark.parametrize("with_job_id", [False, True]) def test_remote_results(fixt, mock_batch, with_job_id): + with pytest.raises( + ValueError, + match="'submission_id' and 'batch_id' cannot be simultaneously", + ): + RemoteResults( + mock_batch.id, + submission_id=mock_batch.id, + connection=fixt.pasqal_cloud, + ) + + with pytest.raises( + ValueError, + match="'submission_id' and 'batch_id' cannot be simultaneously", + ): + RemoteResults( + batch_id=mock_batch.id, + submission_id=mock_batch.id, + connection=fixt.pasqal_cloud, + ) + + with pytest.warns( + DeprecationWarning, + match="'submission_id' has been deprecated and replaced by 'batch_id'", + ): + res_ = RemoteResults( + submission_id=mock_batch.id, + connection=fixt.pasqal_cloud, + ) + assert res_.batch_id == mock_batch.id + with pytest.raises( RuntimeError, match=re.escape("does not contain jobs ['badjobid']") ): @@ -176,9 +209,16 @@ def test_remote_results(fixt, mock_batch, with_job_id): fixt.mock_cloud_sdk.get_batch.assert_called_once_with( id=remote_results.batch_id ) + + with pytest.warns( + DeprecationWarning, + match=re.escape("'RemoteResults.get_status()' has been deprecated,"), + ): + assert remote_results.get_status() == SubmissionStatus.DONE fixt.mock_cloud_sdk.get_batch.reset_mock() - assert remote_results.get_status() == SubmissionStatus.DONE + assert remote_results.get_batch_status() == BatchStatus.DONE + fixt.mock_cloud_sdk.get_batch.assert_called_once_with( id=remote_results.batch_id ) @@ -200,7 +240,7 @@ def test_remote_results(fixt, mock_batch, with_job_id): assert hasattr(remote_results, "_results") fixt.mock_cloud_sdk.get_batch.reset_mock() - available_results = remote_results.get_available_results("id") + available_results = remote_results.get_available_results() assert available_results == { job.id: SampledResult( atom_order=("q0", "q1", "q2", "q3"), @@ -241,7 +281,7 @@ def test_partial_results(): ) fixt.mock_cloud_sdk.get_batch.reset_mock() - available_results = remote_results.get_available_results(batch.id) + available_results = remote_results.get_available_results() assert available_results == { job.id: SampledResult( atom_order=("q0", "q1", "q2", "q3"), @@ -283,7 +323,7 @@ def test_partial_results(): ) fixt.mock_cloud_sdk.get_batch.reset_mock() - available_results = remote_results.get_available_results(batch.id) + available_results = remote_results.get_available_results() assert available_results == { job.id: SampledResult( atom_order=("q0", "q1", "q2", "q3"), @@ -402,6 +442,22 @@ def test_submit(fixt, parametrized, emulator, mimic_qpu, seq, mock_batch): } ] + remote_results = fixt.pasqal_cloud.submit( + seq, job_params=job_params, batch_id="open_batch" + ) + fixt.mock_cloud_sdk.get_batch.assert_any_call(id="open_batch") + fixt.mock_cloud_sdk.add_jobs.assert_called_once_with( + "open_batch", + jobs=job_params, + ) + # The MockBatch returned before and after submission is the same + # so no new job ids are found + assert remote_results.job_ids == [] + + assert fixt.pasqal_cloud.supports_open_batch() is True + fixt.pasqal_cloud._close_batch("open_batch") + fixt.mock_cloud_sdk.close_batch.assert_called_once_with("open_batch") + remote_results = fixt.pasqal_cloud.submit( seq, job_params=job_params, @@ -421,6 +477,7 @@ def test_submit(fixt, parametrized, emulator, mimic_qpu, seq, mock_batch): emulator=emulator, configuration=sdk_config, wait=False, + open=False, ) ) @@ -437,6 +494,37 @@ def test_submit(fixt, parametrized, emulator, mimic_qpu, seq, mock_batch): ) assert isinstance(remote_results, RemoteResults) + with pytest.warns( + DeprecationWarning, + match=re.escape("'RemoteResults.get_status()' has been deprecated,"), + ): + assert remote_results.get_status() == SubmissionStatus.DONE + assert remote_results.get_batch_status() == BatchStatus.DONE + + with pytest.warns( + DeprecationWarning, + match=re.escape("'RemoteResults._submission_id' has been deprecated,"), + ): + assert remote_results._submission_id == remote_results.batch_id + + fixt.mock_cloud_sdk.get_batch.assert_called_with( + id=remote_results.batch_id + ) + + fixt.mock_cloud_sdk.get_batch.reset_mock() + results = remote_results.results + fixt.mock_cloud_sdk.get_batch.assert_called_with( + id=remote_results.batch_id + ) + assert results == tuple( + SampledResult( + atom_order=("q0", "q1", "q2", "q3"), + meas_basis="ground-rydberg", + bitstring_counts=_job.result, + ) + for _job in mock_batch.ordered_jobs + ) + assert hasattr(remote_results, "_results") @pytest.mark.parametrize("emu_cls", [EmuTNBackend, EmuFreeBackend]) @@ -536,4 +624,5 @@ def test_emulators_run(fixt, seq, emu_cls, parametrized: bool, mimic_qpu): emulator=emulator_type, configuration=sdk_config, wait=False, + open=False, ) diff --git a/tutorials/advanced_features/Backends for Sequence Execution.ipynb b/tutorials/advanced_features/Backends for Sequence Execution.ipynb index b85ec320..51854054 100644 --- a/tutorials/advanced_features/Backends for Sequence Execution.ipynb +++ b/tutorials/advanced_features/Backends for Sequence Execution.ipynb @@ -337,7 +337,7 @@ "id": "2618a789", "metadata": {}, "source": [ - "For remote backends, the object returned is a `RemoteResults` instance, which uses the connection to fetch the results once they are ready. To check the status of the submission, we can run:" + "For remote backends, the object returned is a `RemoteResults` instance, which uses the connection to fetch the results once they are ready. To check the status of the batch, we can run:" ] }, { @@ -349,7 +349,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 10, @@ -358,7 +358,7 @@ } ], "source": [ - "free_results.get_status()" + "free_results.get_batch_status()" ] }, { @@ -366,7 +366,7 @@ "id": "763e011c", "metadata": {}, "source": [ - "When the submission states shows as `DONE`, the results can be accessed. In this case, they are a sequence of `SampledResult` objects, one for each entry in `job_params` in the same order. For example, we can retrieve the bitstring counts or even plot an histogram with the results:" + "When the batch states shows as `DONE`, the results can be accessed. In this case, they are a sequence of `SampledResult` objects, one for each entry in `job_params` in the same order. For example, we can retrieve the bitstring counts or even plot an histogram with the results:" ] }, {