From 65ac5d948b9deeebd4105d881a6ea0b5b1d324f6 Mon Sep 17 00:00:00 2001 From: Matthieu Moreau Date: Mon, 16 Sep 2024 19:21:32 +0200 Subject: [PATCH 1/9] Introduce new method to return available results --- pulser-core/pulser/backend/remote.py | 7 ++++ pulser-pasqal/pulser_pasqal/pasqal_cloud.py | 37 +++++++++++++-------- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/pulser-core/pulser/backend/remote.py b/pulser-core/pulser/backend/remote.py index 6932e109..151e5a5f 100644 --- a/pulser-core/pulser/backend/remote.py +++ b/pulser-core/pulser/backend/remote.py @@ -103,6 +103,13 @@ def get_status(self) -> SubmissionStatus: """Gets the status of the remote submission.""" return self._connection._get_submission_status(self._submission_id) + def _get_available_result(self, submission_id: str) -> dict[str, Result]: + """Return available results and ignore jobs with no results.""" + return { + k: v + for k, v in self._connection._query_result(submission_id).items() + } + def __getattr__(self, name: str) -> Any: if name == "_results": status = self.get_status() diff --git a/pulser-pasqal/pulser_pasqal/pasqal_cloud.py b/pulser-pasqal/pulser_pasqal/pasqal_cloud.py index e0faa009..fb2a5d18 100644 --- a/pulser-pasqal/pulser_pasqal/pasqal_cloud.py +++ b/pulser-pasqal/pulser_pasqal/pasqal_cloud.py @@ -183,37 +183,46 @@ def _fetch_result( self, submission_id: str, job_ids: list[str] | None ) -> tuple[Result, ...]: # For now, the results are always sampled results + full_results = self._query_result(submission_id) + + if job_ids is None: + job_ids = full_results.keys() + + results = [] + for id in job_ids: + assert ( + full_results[id].result is not None + ), "Failed to fetch the results." + results.append(full_results[id]) + + return tuple(results) + + def _query_result(self, submission_id: str) -> dict[str, Result | None]: get_batch_fn = backoff_decorator(self._sdk_connection.get_batch) batch = get_batch_fn(id=submission_id) + seq_builder = Sequence.from_abstract_repr(batch.sequence_builder) reg = seq_builder.get_register(include_mappable=True) all_qubit_ids = reg.qubit_ids meas_basis = seq_builder.get_measurement_basis() - results = [] + results = {} sdk_jobs = batch.ordered_jobs - if job_ids is not None: - ind_job_pairs = [ - (job_ids.index(job.id), job) - for job in sdk_jobs - if job.id in job_ids - ] - ind_job_pairs.sort() - sdk_jobs = [job for _, job in ind_job_pairs] + for job in sdk_jobs: vars = job.variables size: int | None = None if vars and "qubits" in vars: size = len(vars["qubits"]) - assert job.result is not None, "Failed to fetch the results." - results.append( - SampledResult( + if job.result is None: + results[job.id] = None + else: + results[job.id] = SampledResult( atom_order=all_qubit_ids[slice(size)], meas_basis=meas_basis, bitstring_counts=job.result, ) - ) - return tuple(results) + return results @backoff_decorator def _get_submission_status(self, submission_id: str) -> SubmissionStatus: From 054676dd03fefa256c6cc28b7ce2c804e141871f Mon Sep 17 00:00:00 2001 From: Matthieu Moreau Date: Mon, 16 Sep 2024 19:40:43 +0200 Subject: [PATCH 2/9] mypy --- pulser-core/pulser/backend/remote.py | 7 ++++++- pulser-pasqal/pulser_pasqal/pasqal_cloud.py | 14 ++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/pulser-core/pulser/backend/remote.py b/pulser-core/pulser/backend/remote.py index 151e5a5f..bce8a74e 100644 --- a/pulser-core/pulser/backend/remote.py +++ b/pulser-core/pulser/backend/remote.py @@ -17,7 +17,7 @@ import typing from abc import ABC, abstractmethod from enum import Enum, auto -from typing import Any, TypedDict +from typing import Any, Mapping, TypedDict from pulser.backend.abc import Backend from pulser.devices import Device @@ -108,6 +108,7 @@ def _get_available_result(self, submission_id: str) -> dict[str, Result]: return { k: v for k, v in self._connection._query_result(submission_id).items() + if v is not None } def __getattr__(self, name: str) -> Any: @@ -146,6 +147,10 @@ def _fetch_result( """Fetches the results of a completed submission.""" pass + @abstractmethod + def _query_result(self, submission_id: str) -> Mapping[str, Result | None]: + pass + @abstractmethod def _get_submission_status(self, submission_id: str) -> SubmissionStatus: """Gets the status of a submission from its ID. diff --git a/pulser-pasqal/pulser_pasqal/pasqal_cloud.py b/pulser-pasqal/pulser_pasqal/pasqal_cloud.py index fb2a5d18..d3a1b431 100644 --- a/pulser-pasqal/pulser_pasqal/pasqal_cloud.py +++ b/pulser-pasqal/pulser_pasqal/pasqal_cloud.py @@ -17,7 +17,7 @@ import copy import json from dataclasses import fields -from typing import Any, Type, cast +from typing import Any, Mapping, Type, cast import backoff import numpy as np @@ -186,18 +186,16 @@ def _fetch_result( full_results = self._query_result(submission_id) if job_ids is None: - job_ids = full_results.keys() + job_ids = list(full_results.keys()) results = [] for id in job_ids: - assert ( - full_results[id].result is not None - ), "Failed to fetch the results." - results.append(full_results[id]) + assert full_results[id] is not None, "Failed to fetch the results." + results.append(cast(Result, full_results[id])) return tuple(results) - def _query_result(self, submission_id: str) -> dict[str, Result | None]: + def _query_result(self, submission_id: str) -> Mapping[str, Result | None]: get_batch_fn = backoff_decorator(self._sdk_connection.get_batch) batch = get_batch_fn(id=submission_id) @@ -206,7 +204,7 @@ def _query_result(self, submission_id: str) -> dict[str, Result | None]: all_qubit_ids = reg.qubit_ids meas_basis = seq_builder.get_measurement_basis() - results = {} + results: dict[str, Result | None] = {} sdk_jobs = batch.ordered_jobs for job in sdk_jobs: From 9cd48cb04cc3eb9b13c5cfc539540aa983da539a Mon Sep 17 00:00:00 2001 From: Matthieu Moreau Date: Wed, 18 Sep 2024 10:39:31 +0200 Subject: [PATCH 3/9] review --- pulser-core/pulser/backend/remote.py | 62 ++++++++++++++++----- pulser-pasqal/pulser_pasqal/pasqal_cloud.py | 40 ++++++++----- tests/test_backend.py | 19 ++++--- 3 files changed, 86 insertions(+), 35 deletions(-) diff --git a/pulser-core/pulser/backend/remote.py b/pulser-core/pulser/backend/remote.py index bce8a74e..84971f46 100644 --- a/pulser-core/pulser/backend/remote.py +++ b/pulser-core/pulser/backend/remote.py @@ -44,6 +44,17 @@ class SubmissionStatus(Enum): PAUSED = auto() +class JobStatus(Enum): + """Status of a remote job.""" + + PENDING = auto() + RUNNING = auto() + DONE = auto() + CANCELED = auto() + ERROR = auto() + PAUSED = auto() + + class RemoteResultsError(Exception): """Error raised when fetching remote results fails.""" @@ -103,28 +114,42 @@ def get_status(self) -> SubmissionStatus: """Gets the status of the remote submission.""" return self._connection._get_submission_status(self._submission_id) - def _get_available_result(self, submission_id: str) -> dict[str, Result]: - """Return available results and ignore jobs with no results.""" - return { - k: v - for k, v in self._connection._query_result(submission_id).items() - if v is not None + def get_available_results(self, submission_id: str) -> dict[str, Result]: + """Returns the available results of a submission. + + Unlike the `results` property, this method does not raise an error if + some jobs associated to the submission do not have results. + + It returns a dictionnary mapping the job ID to their results. Jobs with + no result are omitted. + """ + + results = { + k: v[1] + for k, v in self._connection._query_job_progress( + submission_id + ).items() + if v[1] is not None } + if self._job_ids: + return {k: v for k, v in results.items() if k in self._job_ids} + return results + def __getattr__(self, name: str) -> Any: if name == "_results": - status = self.get_status() - if status == SubmissionStatus.DONE: + try: self._results = tuple( self._connection._fetch_result( self._submission_id, self._job_ids ) ) - return self._results - raise RemoteResultsError( - "The results are not available. The submission's status is " - f"{str(status)}." - ) + except RemoteResultsError as e: + raise RemoteResultsError( + "Results are not available for all jobs. Use the " + "`get_available_results` method to retrieve partial " + "results." + ) from e raise AttributeError( f"'RemoteResults' object has no attribute '{name}'." ) @@ -148,7 +173,16 @@ def _fetch_result( pass @abstractmethod - def _query_result(self, submission_id: str) -> Mapping[str, Result | None]: + def _query_job_progress( + self, submission_id: str + ) -> Mapping[str, tuple[JobStatus, Result | None]]: + """Fetches the status and results of a submission. + + Unlike `_fetch_result`, this method does not raise an error if some + jobs associated to the submission do not have results. + + It returns a dictionnary mapping the job ID to their status and results. + """ pass @abstractmethod diff --git a/pulser-pasqal/pulser_pasqal/pasqal_cloud.py b/pulser-pasqal/pulser_pasqal/pasqal_cloud.py index d3a1b431..dbc279c8 100644 --- a/pulser-pasqal/pulser_pasqal/pasqal_cloud.py +++ b/pulser-pasqal/pulser_pasqal/pasqal_cloud.py @@ -33,8 +33,10 @@ from pulser.backend.qpu import QPUBackend from pulser.backend.remote import ( JobParams, + JobStatus, RemoteConnection, RemoteResults, + RemoteResultsError, SubmissionStatus, ) from pulser.devices import Device @@ -183,19 +185,27 @@ def _fetch_result( self, submission_id: str, job_ids: list[str] | None ) -> tuple[Result, ...]: # For now, the results are always sampled results - full_results = self._query_result(submission_id) + jobs = self._query_job_progress(submission_id) if job_ids is None: - job_ids = list(full_results.keys()) + job_ids = list(jobs.keys()) - results = [] + results: list[Result] = [] for id in job_ids: - assert full_results[id] is not None, "Failed to fetch the results." - results.append(cast(Result, full_results[id])) + status, result = jobs[id] + if status != {JobStatus.PENDING, JobStatus.RUNNING}: + raise RemoteResultsError( + f"The results are not yet available, job {id} status is {status}." + ) + if result is None: + raise RemoteResultsError(f"No results found for job {id}.") + results.append(result) return tuple(results) - def _query_result(self, submission_id: str) -> Mapping[str, Result | None]: + def _query_job_progress( + self, submission_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) @@ -204,21 +214,23 @@ def _query_result(self, submission_id: str) -> Mapping[str, Result | None]: all_qubit_ids = reg.qubit_ids meas_basis = seq_builder.get_measurement_basis() - results: dict[str, Result | None] = {} - sdk_jobs = batch.ordered_jobs + results: dict[str, tuple[JobStatus, Result | None]] = {} - for job in sdk_jobs: + for job in batch.ordered_jobs: vars = job.variables size: int | None = None if vars and "qubits" in vars: size = len(vars["qubits"]) if job.result is None: - results[job.id] = None + results[job.id] = (JobStatus[job.status], None) else: - results[job.id] = SampledResult( - atom_order=all_qubit_ids[slice(size)], - meas_basis=meas_basis, - bitstring_counts=job.result, + results[job.id] = ( + JobStatus[job.status], + SampledResult( + atom_order=all_qubit_ids[slice(size)], + meas_basis=meas_basis, + bitstring_counts=job.result, + ), ) return results diff --git a/tests/test_backend.py b/tests/test_backend.py index 98320758..a30b0397 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -23,6 +23,7 @@ from pulser.backend.config import EmulatorConfig from pulser.backend.qpu import QPUBackend from pulser.backend.remote import ( + JobStatus, RemoteConnection, RemoteResults, RemoteResultsError, @@ -89,6 +90,11 @@ def test_emulator_config_type_errors(param, msg): class _MockConnection(RemoteConnection): def __init__(self): self._status_calls = 0 + self.result = SampledResult( + ("q0", "q1"), + meas_basis="ground-rydberg", + bitstring_counts={"00": 100}, + ) def submit(self, sequence, wait: bool = False, **kwargs) -> RemoteResults: return RemoteResults("abcd", self) @@ -96,13 +102,12 @@ def submit(self, sequence, wait: bool = False, **kwargs) -> RemoteResults: def _fetch_result( self, submission_id: str, job_ids: list[str] | None = None ) -> typing.Sequence[Result]: - return ( - SampledResult( - ("q0", "q1"), - meas_basis="ground-rydberg", - bitstring_counts={"00": 100}, - ), - ) + return (self.result,) + + def _query_job_progress( + self, submission_id: str + ) -> typing.Mapping[str, tuple[JobStatus, Result | None]]: + return {"abcd": (JobStatus.DONE, self.result)} def _get_submission_status(self, submission_id: str) -> SubmissionStatus: self._status_calls += 1 From c9552a3994db1fcf708c8b25ecb66d08c8401a35 Mon Sep 17 00:00:00 2001 From: MatthieuMoreau Date: Wed, 18 Sep 2024 11:12:55 +0200 Subject: [PATCH 4/9] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Henrique Silvério <29920212+HGSilveri@users.noreply.github.com> --- pulser-core/pulser/backend/remote.py | 7 ++++--- pulser-pasqal/pulser_pasqal/pasqal_cloud.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pulser-core/pulser/backend/remote.py b/pulser-core/pulser/backend/remote.py index 84971f46..cc1ccdaf 100644 --- a/pulser-core/pulser/backend/remote.py +++ b/pulser-core/pulser/backend/remote.py @@ -120,8 +120,9 @@ def get_available_results(self, submission_id: str) -> dict[str, Result]: Unlike the `results` property, this method does not raise an error if some jobs associated to the submission do not have results. - It returns a dictionnary mapping the job ID to their results. Jobs with - no result are omitted. + Returns: + dict[str, Result]: A dictionary mapping the job ID to their results. + Jobs with no result are omitted. """ results = { @@ -176,7 +177,7 @@ def _fetch_result( def _query_job_progress( self, submission_id: str ) -> Mapping[str, tuple[JobStatus, Result | None]]: - """Fetches the status and results of a submission. + """Fetches the status and results of all the jobs in a submission. Unlike `_fetch_result`, this method does not raise an error if some jobs associated to the submission do not have results. diff --git a/pulser-pasqal/pulser_pasqal/pasqal_cloud.py b/pulser-pasqal/pulser_pasqal/pasqal_cloud.py index dbc279c8..89dca4ee 100644 --- a/pulser-pasqal/pulser_pasqal/pasqal_cloud.py +++ b/pulser-pasqal/pulser_pasqal/pasqal_cloud.py @@ -193,7 +193,7 @@ def _fetch_result( results: list[Result] = [] for id in job_ids: status, result = jobs[id] - if status != {JobStatus.PENDING, JobStatus.RUNNING}: + if status in {JobStatus.PENDING, JobStatus.RUNNING}: raise RemoteResultsError( f"The results are not yet available, job {id} status is {status}." ) From c844186c4160084bc823e526a08bafa78d247564 Mon Sep 17 00:00:00 2001 From: Matthieu Moreau Date: Wed, 18 Sep 2024 12:47:24 +0200 Subject: [PATCH 5/9] [TEST] Add test for get_available_result and update existing tests --- pulser-core/pulser/backend/remote.py | 3 +- tests/test_backend.py | 18 ++-- tests/test_pasqal.py | 127 +++++++++++++++++++++------ 3 files changed, 115 insertions(+), 33 deletions(-) diff --git a/pulser-core/pulser/backend/remote.py b/pulser-core/pulser/backend/remote.py index cc1ccdaf..5eb0a0f2 100644 --- a/pulser-core/pulser/backend/remote.py +++ b/pulser-core/pulser/backend/remote.py @@ -121,7 +121,7 @@ def get_available_results(self, submission_id: str) -> dict[str, Result]: some jobs associated to the submission do not have results. Returns: - dict[str, Result]: A dictionary mapping the job ID to their results. + dict[str, Result]: A dictionary mapping the job ID to their results. Jobs with no result are omitted. """ @@ -145,6 +145,7 @@ def __getattr__(self, name: str) -> Any: self._submission_id, self._job_ids ) ) + return self._results except RemoteResultsError as e: raise RemoteResultsError( "Results are not available for all jobs. Use the " diff --git a/tests/test_backend.py b/tests/test_backend.py index a30b0397..7318908a 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -90,6 +90,7 @@ def test_emulator_config_type_errors(param, msg): class _MockConnection(RemoteConnection): def __init__(self): self._status_calls = 0 + self._progress_calls = 0 self.result = SampledResult( ("q0", "q1"), meas_basis="ground-rydberg", @@ -102,6 +103,10 @@ def submit(self, sequence, wait: bool = False, **kwargs) -> RemoteResults: def _fetch_result( self, submission_id: str, job_ids: list[str] | None = None ) -> typing.Sequence[Result]: + self._progress_calls += 1 + if self._progress_calls == 1: + raise RemoteResultsError("Results not available") + return (self.result,) def _query_job_progress( @@ -110,9 +115,6 @@ def _query_job_progress( return {"abcd": (JobStatus.DONE, self.result)} def _get_submission_status(self, submission_id: str) -> SubmissionStatus: - self._status_calls += 1 - if self._status_calls == 1: - return SubmissionStatus.RUNNING return SubmissionStatus.DONE @@ -181,10 +183,16 @@ def test_qpu_backend(sequence): with pytest.raises( RemoteResultsError, - match="The results are not available. The submission's status is" - " SubmissionStatus.RUNNING", + match=( + "Results are not available for all jobs. " + "Use the `get_available_results` method to retrieve partial " + "results." + ), ): remote_results.results results = remote_results.results assert results[0].sampling_dist == {"00": 1.0} + + available_results = remote_results.get_available_results("id") + assert available_results == {"abcd": connection.result} diff --git a/tests/test_pasqal.py b/tests/test_pasqal.py index 76106194..fd60ae36 100644 --- a/tests/test_pasqal.py +++ b/tests/test_pasqal.py @@ -28,8 +28,10 @@ import pulser_pasqal from pulser.backend.config import EmulatorConfig from pulser.backend.remote import ( + JobStatus, RemoteConnection, RemoteResults, + RemoteResultsError, SubmissionStatus, ) from pulser.devices import DigitalAnalogDevice @@ -64,11 +66,14 @@ class CloudFixture: test_device.to_virtual(), name="test-virtual" ) +_seq = Sequence( + SquareLatticeLayout(5, 5, 5).make_mappable_register(10), test_device +) + @pytest.fixture def seq(): - reg = SquareLatticeLayout(5, 5, 5).make_mappable_register(10) - return Sequence(reg, test_device) + return copy.deepcopy(_seq) class _MockJob: @@ -77,40 +82,38 @@ def __init__( runs=10, variables={"t": 100, "qubits": {"q0": 1, "q1": 2, "q2": 4, "q3": 3}}, result={"00": 5, "11": 5}, + status=JobStatus.DONE.name, ) -> None: self.runs = runs self.variables = variables self.result = result self.id = str(np.random.randint(10000)) + self.status = status -@pytest.fixture -def mock_job(): - return _MockJob() - - -@pytest.fixture -def mock_batch(mock_job, seq): - seq_ = copy.deepcopy(seq) - seq_.declare_channel("rydberg_global", "rydberg_global") - seq_.measure() - - @dataclasses.dataclass - class MockBatch: - id = "abcd" - status = "DONE" - ordered_jobs = [ - mock_job, +@dataclasses.dataclass +class MockBatch: + id = "abcd" + status: str = "DONE" + ordered_jobs: list[_MockJob] = dataclasses.field( + default_factory=lambda: [ + _MockJob(), _MockJob(result={"00": 10}), _MockJob(result={"11": 10}), ] - sequence_builder = seq_.to_abstract_repr() + ) + seq_ = copy.deepcopy(_seq) + seq_.declare_channel("rydberg_global", "rydberg_global") + seq_.measure() + sequence_builder = seq_.to_abstract_repr() + +@pytest.fixture +def mock_batch(): return MockBatch() -@pytest.fixture -def fixt(mock_batch): +def mock_pasqal_cloud_sdk(mock_batch): with patch("pasqal_cloud.SDK", autospec=True) as mock_cloud_sdk_class: pasqal_cloud_kwargs = dict( username="abc", @@ -134,11 +137,14 @@ def fixt(mock_batch): return_value={test_device.name: test_device.to_abstract_repr()} ) - yield CloudFixture( + return CloudFixture( pasqal_cloud=pasqal_cloud, mock_cloud_sdk=mock_cloud_sdk ) - mock_cloud_sdk_class.assert_not_called() + +@pytest.fixture +def fixt(mock_batch): + yield mock_pasqal_cloud_sdk(mock_batch) @pytest.mark.parametrize("with_job_id", [False, True]) @@ -190,15 +196,82 @@ 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") + assert available_results == { + job.id: SampledResult( + atom_order=("q0", "q1", "q2", "q3"), + meas_basis="ground-rydberg", + bitstring_counts=job.result, + ) + for job in select_jobs + } + + +def test_partial_results(): + batch = MockBatch( + status="RUNNING", + ordered_jobs=[ + _MockJob(), + _MockJob(status="RUNNING", result=None), + ], + ) + + fixt = mock_pasqal_cloud_sdk(batch) + + remote_results = RemoteResults( + batch.id, + fixt.pasqal_cloud, + ) + + fixt.mock_cloud_sdk.get_batch.reset_mock() + with pytest.raises(RemoteResultsError): + remote_results.results + available_results = remote_results.get_available_results(batch.id) + assert available_results == { + job.id: SampledResult( + atom_order=("q0", "q1", "q2", "q3"), + meas_basis="ground-rydberg", + bitstring_counts=job.result, + ) + for job in batch.ordered_jobs + if job.result is not None + } + + batch = MockBatch( + status="DONE", + ordered_jobs=[ + _MockJob(), + _MockJob(status="DONE", result=None), + ], + ) + + fixt = mock_pasqal_cloud_sdk(batch) + remote_results = RemoteResults( + batch.id, + fixt.pasqal_cloud, + ) + + with pytest.raises(RemoteResultsError): + remote_results.results + available_results = remote_results.get_available_results(batch.id) + assert available_results == { + job.id: SampledResult( + atom_order=("q0", "q1", "q2", "q3"), + meas_basis="ground-rydberg", + bitstring_counts=job.result, + ) + for job in batch.ordered_jobs + if job.result is not None + } + @pytest.mark.parametrize("mimic_qpu", [False, True]) @pytest.mark.parametrize( "emulator", [None, EmulatorType.EMU_TN, EmulatorType.EMU_FREE] ) @pytest.mark.parametrize("parametrized", [True, False]) -def test_submit( - fixt, parametrized, emulator, mimic_qpu, seq, mock_batch, mock_job -): +def test_submit(fixt, parametrized, emulator, mimic_qpu, seq, mock_batch): with pytest.raises( ValueError, match="The measurement basis can't be implicitly determined for a " From 8a13f0a96b36178d3a2d23cb83bb8d72663875d5 Mon Sep 17 00:00:00 2001 From: Matthieu Moreau Date: Wed, 18 Sep 2024 13:51:34 +0200 Subject: [PATCH 6/9] style --- pulser-core/pulser/backend/remote.py | 5 ++--- pulser-pasqal/pulser_pasqal/pasqal_cloud.py | 3 ++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pulser-core/pulser/backend/remote.py b/pulser-core/pulser/backend/remote.py index 5eb0a0f2..582cc059 100644 --- a/pulser-core/pulser/backend/remote.py +++ b/pulser-core/pulser/backend/remote.py @@ -121,10 +121,9 @@ def get_available_results(self, submission_id: str) -> dict[str, Result]: some jobs associated to the submission do not have results. Returns: - dict[str, Result]: A dictionary mapping the job ID to their results. + dict[str, Result]: A dictionary mapping the job ID to its results. Jobs with no result are omitted. """ - results = { k: v[1] for k, v in self._connection._query_job_progress( @@ -183,7 +182,7 @@ def _query_job_progress( Unlike `_fetch_result`, this method does not raise an error if some jobs associated to the submission do not have results. - It returns a dictionnary mapping the job ID to their status and results. + It returns a dictionnary mapping the job ID to its status and results. """ pass diff --git a/pulser-pasqal/pulser_pasqal/pasqal_cloud.py b/pulser-pasqal/pulser_pasqal/pasqal_cloud.py index 89dca4ee..2aba8651 100644 --- a/pulser-pasqal/pulser_pasqal/pasqal_cloud.py +++ b/pulser-pasqal/pulser_pasqal/pasqal_cloud.py @@ -195,7 +195,8 @@ def _fetch_result( status, result = jobs[id] if status in {JobStatus.PENDING, JobStatus.RUNNING}: raise RemoteResultsError( - f"The results are not yet available, job {id} status is {status}." + f"The results are not yet available, job {id} status is " + f"{status}." ) if result is None: raise RemoteResultsError(f"No results found for job {id}.") From 072a73250c78d9f8291ceae4bd1db240a8be9a39 Mon Sep 17 00:00:00 2001 From: Matthieu Moreau Date: Wed, 18 Sep 2024 17:27:07 +0200 Subject: [PATCH 7/9] Add more assertions to tests --- tests/test_pasqal.py | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/tests/test_pasqal.py b/tests/test_pasqal.py index fd60ae36..039e8620 100644 --- a/tests/test_pasqal.py +++ b/tests/test_pasqal.py @@ -225,8 +225,19 @@ def test_partial_results(): ) fixt.mock_cloud_sdk.get_batch.reset_mock() - with pytest.raises(RemoteResultsError): + with pytest.raises( + RemoteResultsError, + match=( + "Results are not available for all jobs. Use the " + "`get_available_results` method to retrieve partial results." + ), + ): remote_results.results + fixt.mock_cloud_sdk.get_batch.assert_called_once_with( + id=remote_results.batch_id + ) + fixt.mock_cloud_sdk.get_batch.reset_mock() + available_results = remote_results.get_available_results(batch.id) assert available_results == { job.id: SampledResult( @@ -237,6 +248,10 @@ def test_partial_results(): for job in batch.ordered_jobs if job.result is not None } + fixt.mock_cloud_sdk.get_batch.assert_called_once_with( + id=remote_results.batch_id + ) + fixt.mock_cloud_sdk.get_batch.reset_mock() batch = MockBatch( status="DONE", @@ -252,8 +267,19 @@ def test_partial_results(): fixt.pasqal_cloud, ) - with pytest.raises(RemoteResultsError): + with pytest.raises( + RemoteResultsError, + match=( + "Results are not available for all jobs. Use the " + "`get_available_results` method to retrieve partial results." + ), + ): remote_results.results + fixt.mock_cloud_sdk.get_batch.assert_called_once_with( + id=remote_results.batch_id + ) + fixt.mock_cloud_sdk.get_batch.reset_mock() + available_results = remote_results.get_available_results(batch.id) assert available_results == { job.id: SampledResult( @@ -264,6 +290,10 @@ def test_partial_results(): for job in batch.ordered_jobs if job.result is not None } + fixt.mock_cloud_sdk.get_batch.assert_called_once_with( + id=remote_results.batch_id + ) + fixt.mock_cloud_sdk.get_batch.reset_mock() @pytest.mark.parametrize("mimic_qpu", [False, True]) From 32e935c87472c95a540c25ef83c61cd1d8ab1e08 Mon Sep 17 00:00:00 2001 From: Matthieu Moreau Date: Wed, 18 Sep 2024 17:36:38 +0200 Subject: [PATCH 8/9] Move sequence declaration to dedicated function --- tests/test_pasqal.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/tests/test_pasqal.py b/tests/test_pasqal.py index 039e8620..c497e776 100644 --- a/tests/test_pasqal.py +++ b/tests/test_pasqal.py @@ -66,14 +66,21 @@ class CloudFixture: test_device.to_virtual(), name="test-virtual" ) -_seq = Sequence( - SquareLatticeLayout(5, 5, 5).make_mappable_register(10), test_device -) + +def build_test_sequence() -> Sequence: + seq = Sequence( + SquareLatticeLayout(5, 5, 5).make_mappable_register(10), test_device + ) + seq.declare_channel("rydberg_global", "rydberg_global") + seq.measure() + return seq @pytest.fixture def seq(): - return copy.deepcopy(_seq) + return Sequence( + SquareLatticeLayout(5, 5, 5).make_mappable_register(10), test_device + ) class _MockJob: @@ -102,10 +109,7 @@ class MockBatch: _MockJob(result={"11": 10}), ] ) - seq_ = copy.deepcopy(_seq) - seq_.declare_channel("rydberg_global", "rydberg_global") - seq_.measure() - sequence_builder = seq_.to_abstract_repr() + sequence_builder = build_test_sequence().to_abstract_repr() @pytest.fixture From 3166ac1cec7cf7e6af0e5818ab0b032fc10adb12 Mon Sep 17 00:00:00 2001 From: Matthieu Moreau Date: Wed, 18 Sep 2024 17:45:27 +0200 Subject: [PATCH 9/9] style --- tests/test_pasqal.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_pasqal.py b/tests/test_pasqal.py index c497e776..28ded8ed 100644 --- a/tests/test_pasqal.py +++ b/tests/test_pasqal.py @@ -13,7 +13,6 @@ # limitations under the License. from __future__ import annotations -import copy import dataclasses import re from pathlib import Path