From a00d797e2f8dabc83d1dab221a092259c7e10711 Mon Sep 17 00:00:00 2001 From: Nischal Sehrawat Date: Thu, 12 Sep 2024 11:19:46 +0200 Subject: [PATCH 1/5] [QI2-1081] Implemented job result fetching --- .vscode/settings.json | 14 +++- qiskit_quantuminspire/qi_jobs.py | 38 ++++------ qiskit_quantuminspire/qi_results.py | 26 +++++-- tests/test_jobs.py | 65 +++++++++++++++++ tests/test_results.py | 107 ++++++++++++++++++++++++++++ 5 files changed, 218 insertions(+), 32 deletions(-) create mode 100644 tests/test_jobs.py create mode 100644 tests/test_results.py diff --git a/.vscode/settings.json b/.vscode/settings.json index c58052a..f351513 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,15 @@ { "python.analysis.extraPaths": [ "qiskit_quantuminspire" - ] -} + ], + "editor.codeActionsOnSave": {}, + "editor.formatOnSave": true, + "black-formatter.path": [ + "${command:python.interpreterPath}" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true, + "python.testing.pytestPath": "${command:python.interpreterPath}", + "python.testing.autoTestDiscoverOnSaveEnabled": true, + "python.testing.cwd": "${workspaceFolder}", +} \ No newline at end of file diff --git a/qiskit_quantuminspire/qi_jobs.py b/qiskit_quantuminspire/qi_jobs.py index 8e1bd3a..0f6ef3f 100644 --- a/qiskit_quantuminspire/qi_jobs.py +++ b/qiskit_quantuminspire/qi_jobs.py @@ -1,13 +1,14 @@ -from datetime import datetime, timezone +import asyncio from typing import Any, List, Union -from compute_api_client import Result as JobResult +from compute_api_client import ApiClient, Result as JobResult, ResultsApi from qiskit.circuit import QuantumCircuit from qiskit.providers import JobV1 as Job from qiskit.providers.backend import Backend from qiskit.providers.jobstatus import JobStatus from qiskit.result.result import Result +from qiskit_quantuminspire.api.client import config from qiskit_quantuminspire.qi_results import QIResult @@ -21,7 +22,7 @@ def __init__( run_input: Union[QuantumCircuit, List[QuantumCircuit]], backend: Union[Backend, None], job_id: str, - **kwargs: Any + **kwargs: Any, ) -> None: """Initialize a QIJob instance. @@ -45,31 +46,20 @@ def submit(self) -> None: self._job_ids.append(str(i)) self.job_id = "999" # ID of the submitted batch-job - def _fetch_job_results(self) -> List[JobResult]: + async def _fetch_job_results(self) -> List[JobResult]: """Fetch results for job_ids from CJM using api client.""" - raw_results = [] - for job_id in self._job_ids: - job_result = JobResult( - id=int(job_id), - metadata_id=1, - created_on=datetime(2022, 10, 25, 15, 37, 54, 269823, tzinfo=timezone.utc), - execution_time_in_seconds=1.23, - shots_requested=100, - shots_done=100, - results={ - "0000000000": 0.270000, - "0000000001": 0.260000, - "0000000010": 0.180000, - "0000000011": 0.290000, - }, - job_id=int(job_id), - ) - raw_results.append(job_result) - return raw_results + results = None + async with ApiClient(config()) as client: + results_api = ResultsApi(client) + result_tasks = [results_api.read_results_by_job_id_results_job_job_id_get(int(id)) for id in self._job_ids] + results = await asyncio.gather(*result_tasks, return_exceptions=True) + return results def result(self) -> Result: """Return the results of the job.""" - raw_results = self._fetch_job_results() + if self.status() is not JobStatus.DONE: + raise RuntimeError(f"Job status is {self.status}.") + raw_results = asyncio.run(self._fetch_job_results()) processed_results = QIResult(raw_results).process(self) return processed_results diff --git a/qiskit_quantuminspire/qi_results.py b/qiskit_quantuminspire/qi_results.py index 7cf2536..4579abf 100644 --- a/qiskit_quantuminspire/qi_results.py +++ b/qiskit_quantuminspire/qi_results.py @@ -23,21 +23,35 @@ def process(self, job: Job) -> Result: """ results = [] + batch_job_success = [False] * len(self._raw_results) - for result in self._raw_results: + for idx, result in enumerate(self._raw_results): + counts = {} + shots = 0 + experiment_success = False + + if isinstance(result, RawJobResult): + shots = result.shots_done + experiment_success = result.shots_done > 0 + counts = {hex(int(key, 2)): value for key, value in result.results.items()} + batch_job_success[idx] = True + + experiment_data = ExperimentResultData( + counts=counts, + ) experiment_result = ExperimentResult( - shots=result.shots_done, - success=True, - data=ExperimentResultData(), + shots=shots, + success=experiment_success, + data=experiment_data, ) results.append(experiment_result) result = Result( backend_name=job.backend().name, backend_version="1.0.0", - qobj_id=1234, + qobj_id="1234", job_id=job.job_id, - success=True, + success=all(batch_job_success), results=results, ) return result diff --git a/tests/test_jobs.py b/tests/test_jobs.py new file mode 100644 index 0000000..9a4d5ee --- /dev/null +++ b/tests/test_jobs.py @@ -0,0 +1,65 @@ +import asyncio +from unittest.mock import AsyncMock, MagicMock + +import pytest +from pytest_mock import MockerFixture +from qiskit.providers.jobstatus import JobStatus + +from qiskit_quantuminspire.qi_jobs import QIJob + + +def test_result(mocker: MockerFixture): + job = QIJob(run_input="", backend=None, job_id="some-id") + + mocker.patch.object(job, "status", return_value=JobStatus.DONE) + + mock_fetch_job_results = AsyncMock(return_value=[MagicMock()]) + mocker.patch.object(job, "_fetch_job_results", mock_fetch_job_results) + + mock_process = mocker.patch( + "qiskit_quantuminspire.qi_jobs.QIResult.process", + return_value=MagicMock(), + ) + + job.result() + + assert mock_fetch_job_results.called + mock_process.assert_called_once() + + +def test_result_raises_error_when_status_not_done(mocker: MockerFixture): + job = QIJob(run_input="", backend=None, job_id="some-id") + + mocker.patch.object(job, "status", return_value=JobStatus.RUNNING) + + with pytest.raises(RuntimeError): + job.result() + + +def test_fetch_job_result(mocker: MockerFixture): + + mocker.patch( + "qiskit_quantuminspire.qi_jobs.config", + return_value=MagicMock(), + ) + mocker.patch( + "qiskit_quantuminspire.qi_jobs.ApiClient", + autospec=True, + ) + n_jobs = 3 + + mock_results_api = MagicMock() + mock_get_result_by_id = AsyncMock() + mock_get_result_by_id.side_effect = [MagicMock() for _ in range(n_jobs)] + mock_results_api.read_results_by_job_id_results_job_job_id_get = mock_get_result_by_id + + mock_results_api = mocker.patch("qiskit_quantuminspire.qi_jobs.ResultsApi", return_value=mock_results_api) + + job = QIJob(run_input="", backend=None, job_id="some-id") + + job._job_ids = [str(i) for i in range(n_jobs)] + + results = asyncio.run(job._fetch_job_results()) + + assert len(results) == n_jobs + assert mock_get_result_by_id.call_count == n_jobs diff --git a/tests/test_results.py b/tests/test_results.py new file mode 100644 index 0000000..73adf15 --- /dev/null +++ b/tests/test_results.py @@ -0,0 +1,107 @@ +from datetime import datetime, timezone + +import pytest +from compute_api_client import BackendStatus, BackendType, Metadata, Result as JobResult +from qiskit.result.models import ExperimentResult, ExperimentResultData +from qiskit.result.result import Result + +from qiskit_quantuminspire.qi_backend import QIBackend +from qiskit_quantuminspire.qi_jobs import QIJob +from qiskit_quantuminspire.qi_results import QIResult + + +@pytest.fixture +def qi_backend() -> QIBackend: + backend_type = BackendType( + id=1, + name="Spin 2", + infrastructure="Hetzner", + description="Silicon spin quantum computer", + image_id="abcd1234", + is_hardware=True, + features=["multiple_measurements"], + default_compiler_config={}, + native_gateset={"single_qubit_gates": ["X"]}, + status=BackendStatus.IDLE, + default_number_of_shots=1024, + max_number_of_shots=2048, + ) + + metadata = Metadata(id=1, backend_id=1, created_on=datetime.now(timezone.utc), data={"nqubits": 6}) + return QIBackend(backend_type=backend_type, metadata=metadata) + + +@pytest.fixture +def qi_job(qi_backend: QIBackend) -> QIJob: + return QIJob(run_input="", backend=qi_backend, job_id="some-id") + + +def test_process(qi_job: QIJob): + qi_job._job_ids = ["1"] # The jobs in the batch job + qi_job.job_id = "100" # The batch job ID + raw_results = [] + for _id in qi_job._job_ids: + raw_results.append( + JobResult( + id=int(_id), + metadata_id=1, + created_on=datetime(2022, 10, 25, 15, 37, 54, 269823, tzinfo=timezone.utc), + execution_time_in_seconds=1.23, + shots_requested=100, + shots_done=100, + results={ + "0000000000": 256, + "0000000001": 256, + "0000000010": 256, + "0000000011": 256, + }, + job_id=int(qi_job.job_id), + ) + ) + processed_results = QIResult(raw_results).process(qi_job) + expected_results = Result( + backend_name="Spin 2", + backend_version="1.0.0", + qobj_id="1234", + job_id="100", + success=True, + results=[ + ExperimentResult( + shots=100, + success=True, + meas_level=2, + data=ExperimentResultData(counts={"0x0": 256, "0x1": 256, "0x2": 256, "0x3": 256}), + ) + ], + date=None, + status=None, + header=None, + ) + assert processed_results.to_dict() == expected_results.to_dict() + + +def test_process_handles_failed_job(qi_job: QIJob): + qi_job._job_ids = ["1"] # The jobs in the batch job + qi_job.job_id = "100" # The batch job ID + raw_results = [Exception("Bad Result")] + + processed_results = QIResult(raw_results).process(qi_job) + expected_results = Result( + backend_name="Spin 2", + backend_version="1.0.0", + qobj_id="1234", + job_id="100", + success=False, + results=[ + ExperimentResult( + shots=0, + success=False, + meas_level=2, + data=ExperimentResultData(counts={}), + ) + ], + date=None, + status=None, + header=None, + ) + assert processed_results.to_dict() == expected_results.to_dict() From 6215a97983672165b9d8ae28eee7b07ab29f6ba5 Mon Sep 17 00:00:00 2001 From: Nischal Sehrawat Date: Thu, 12 Sep 2024 11:24:28 +0200 Subject: [PATCH 2/5] [QI2-1081] Fixed mypy issues --- tests/test_jobs.py | 6 +++--- tests/test_results.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_jobs.py b/tests/test_jobs.py index 9a4d5ee..40c639e 100644 --- a/tests/test_jobs.py +++ b/tests/test_jobs.py @@ -8,7 +8,7 @@ from qiskit_quantuminspire.qi_jobs import QIJob -def test_result(mocker: MockerFixture): +def test_result(mocker: MockerFixture) -> None: job = QIJob(run_input="", backend=None, job_id="some-id") mocker.patch.object(job, "status", return_value=JobStatus.DONE) @@ -27,7 +27,7 @@ def test_result(mocker: MockerFixture): mock_process.assert_called_once() -def test_result_raises_error_when_status_not_done(mocker: MockerFixture): +def test_result_raises_error_when_status_not_done(mocker: MockerFixture) -> None: job = QIJob(run_input="", backend=None, job_id="some-id") mocker.patch.object(job, "status", return_value=JobStatus.RUNNING) @@ -36,7 +36,7 @@ def test_result_raises_error_when_status_not_done(mocker: MockerFixture): job.result() -def test_fetch_job_result(mocker: MockerFixture): +def test_fetch_job_result(mocker: MockerFixture) -> None: mocker.patch( "qiskit_quantuminspire.qi_jobs.config", diff --git a/tests/test_results.py b/tests/test_results.py index 73adf15..6488dfc 100644 --- a/tests/test_results.py +++ b/tests/test_results.py @@ -36,7 +36,7 @@ def qi_job(qi_backend: QIBackend) -> QIJob: return QIJob(run_input="", backend=qi_backend, job_id="some-id") -def test_process(qi_job: QIJob): +def test_process(qi_job: QIJob) -> None: qi_job._job_ids = ["1"] # The jobs in the batch job qi_job.job_id = "100" # The batch job ID raw_results = [] @@ -80,7 +80,7 @@ def test_process(qi_job: QIJob): assert processed_results.to_dict() == expected_results.to_dict() -def test_process_handles_failed_job(qi_job: QIJob): +def test_process_handles_failed_job(qi_job: QIJob) -> None: qi_job._job_ids = ["1"] # The jobs in the batch job qi_job.job_id = "100" # The batch job ID raw_results = [Exception("Bad Result")] From 2dd409b061a0f33664dd7e4b807b3421b1bdda65 Mon Sep 17 00:00:00 2001 From: Nischal Sehrawat Date: Wed, 18 Sep 2024 10:50:45 +0200 Subject: [PATCH 3/5] [QI2-1081] Implemented PR feedback comments --- .vscode/settings.json | 1 - qiskit_quantuminspire/qi_jobs.py | 57 ++++++++---- qiskit_quantuminspire/qi_results.py | 85 ++++++++++-------- tests/test_jobs.py | 56 +++++++----- tests/test_results.py | 129 +++++++++++++--------------- 5 files changed, 186 insertions(+), 142 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index f351513..60df6a8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,7 +3,6 @@ "qiskit_quantuminspire" ], "editor.codeActionsOnSave": {}, - "editor.formatOnSave": true, "black-formatter.path": [ "${command:python.interpreterPath}" ], diff --git a/qiskit_quantuminspire/qi_jobs.py b/qiskit_quantuminspire/qi_jobs.py index 0f6ef3f..f82fd50 100644 --- a/qiskit_quantuminspire/qi_jobs.py +++ b/qiskit_quantuminspire/qi_jobs.py @@ -1,7 +1,7 @@ import asyncio from typing import Any, List, Union -from compute_api_client import ApiClient, Result as JobResult, ResultsApi +from compute_api_client import ApiClient, PageResult, Result as RawJobResult, ResultsApi from qiskit.circuit import QuantumCircuit from qiskit.providers import JobV1 as Job from qiskit.providers.backend import Backend @@ -9,9 +9,19 @@ from qiskit.result.result import Result from qiskit_quantuminspire.api.client import config +from qiskit_quantuminspire.api.pagination import PageReader from qiskit_quantuminspire.qi_results import QIResult +class CircuitExecutionData: + """Class for bookkeping of individual jobs.""" + + def __init__(self, circuit: QuantumCircuit, job_id: int = None, results: List[RawJobResult] = None) -> None: + self.job_id = job_id + self.circuit = circuit + self.results = [] if results is None else results + + # Ignore type checking for QIJob due to missing Qiskit type stubs, # which causes the base class 'Job' to be treated as 'Any'. class QIJob(Job): # type: ignore[misc] @@ -27,41 +37,56 @@ def __init__( """Initialize a QIJob instance. Args: - run_input: A single/list of Qiskit QuantumCircuit objects or hybrid algorithms. + run_input: A single/list of Qiskit QuantumCircuit object(s). backend: The backend on which the job is run. While specified as `Backend` to avoid circular dependency, it is a `QIBackend`. job_id: A unique identifier for the (batch)job. **kwargs: Additional keyword arguments passed to the parent `Job` class. """ super().__init__(backend, job_id, **kwargs) - self._job_ids: List[str] = [] - self._run_input = run_input + self._cached_result: Union[Result, None] = None + self.circuits_run_data: List[CircuitExecutionData] = ( + [CircuitExecutionData(circuit=run_input)] + if isinstance(run_input, QuantumCircuit) + else [CircuitExecutionData(circuit=circuit) for circuit in run_input] + ) def submit(self) -> None: """Submit the (batch)job to the quantum inspire backend. Use compute-api-client to call the cjm endpoints in the correct order, to submit the jobs. """ - for i in range(1, 3): - self._job_ids.append(str(i)) + # Here, we will update the self.circuits_run_data and attach the job ids for each circuit + for _ in range(1, 3): + pass self.job_id = "999" # ID of the submitted batch-job - async def _fetch_job_results(self) -> List[JobResult]: + async def _fetch_job_results(self) -> None: """Fetch results for job_ids from CJM using api client.""" - results = None async with ApiClient(config()) as client: + page_reader = PageReader[PageResult, RawJobResult]() results_api = ResultsApi(client) - result_tasks = [results_api.read_results_by_job_id_results_job_job_id_get(int(id)) for id in self._job_ids] - results = await asyncio.gather(*result_tasks, return_exceptions=True) - return results + pagination_handler = page_reader.get_all + results_handler = results_api.read_results_by_job_id_results_job_job_id_get + + result_tasks = [ + pagination_handler(results_handler, job_id=circuit_data.job_id) + for circuit_data in self.circuits_run_data + ] + result_items = await asyncio.gather(*result_tasks) + for circuit_data, result_item in zip(self.circuits_run_data, result_items): + circuit_data.results = result_item + def result(self) -> Result: """Return the results of the job.""" - if self.status() is not JobStatus.DONE: - raise RuntimeError(f"Job status is {self.status}.") - raw_results = asyncio.run(self._fetch_job_results()) - processed_results = QIResult(raw_results).process(self) - return processed_results + if not self.done(): + raise RuntimeError(f"(Batch)Job status is {self.status()}.") + if self._cached_result: + return self._cached_result + asyncio.run(self._fetch_job_results()) + self._cached_result = QIResult(self).process() + return self._cached_result def status(self) -> JobStatus: """Return the status of the (batch)job, among the values of ``JobStatus``.""" diff --git a/qiskit_quantuminspire/qi_results.py b/qiskit_quantuminspire/qi_results.py index 4579abf..dc359d4 100644 --- a/qiskit_quantuminspire/qi_results.py +++ b/qiskit_quantuminspire/qi_results.py @@ -1,7 +1,5 @@ -from typing import List - -from compute_api_client import Result as RawJobResult from qiskit.providers import JobV1 as Job +from qiskit.qobj import QobjExperimentHeader from qiskit.result.models import ExperimentResult, ExperimentResultData from qiskit.result.result import Result @@ -9,48 +7,65 @@ class QIResult: """Handle QuantumInspire (batch job) results to integrate with Qiskit's Result interface.""" - def __init__(self, qinspire_results: List[RawJobResult]) -> None: - self._raw_results = qinspire_results - - def process(self, job: Job) -> Result: - """Process the raw job results obtained from QuantumInspire. + def __init__(self, job: Job) -> None: + """Initialize the result processor for QuantumInspire job results. Args: job: The (batch) job for which the results were obtained. While specified as `Job` - to avoid circular dependency, it is a `QIJob`. + to avoid circular dependency, it is a `QIJob`. Returns: - The processed results as a Qiskit Result. + None. """ + self._job = job # The batch job + + def process(self) -> Result: + """Process the raw job results obtained from QuantumInspire.""" + results = [] - batch_job_success = [False] * len(self._raw_results) - - for idx, result in enumerate(self._raw_results): - counts = {} - shots = 0 - experiment_success = False - - if isinstance(result, RawJobResult): - shots = result.shots_done - experiment_success = result.shots_done > 0 - counts = {hex(int(key, 2)): value for key, value in result.results.items()} - batch_job_success[idx] = True - - experiment_data = ExperimentResultData( - counts=counts, - ) - experiment_result = ExperimentResult( - shots=shots, - success=experiment_success, - data=experiment_data, - ) - results.append(experiment_result) + batch_job_success = [False] * len(self._job.circuits_run_data) + + for idx, ciruit_data in enumerate(self._job.circuits_run_data): + result_header = QobjExperimentHeader(name=ciruit_data.circuit.name) + circuit_results = ciruit_data.results + + if not circuit_results: + # For failed job, there are no results + experiment_data = ExperimentResultData( + counts={}, + ) + experiment_result = ExperimentResult( + shots=0, + success=False, + data=experiment_data, + header=result_header, + ) + results.append(experiment_result) + else: + results_valid = [False] * len(circuit_results) + for idx_res, result in enumerate(circuit_results): + shots = result.shots_done + experiment_success = result.shots_done > 0 + counts = {hex(int(key, 2)): value for key, value in result.results.items()} + results_valid[idx_res] = experiment_success + + experiment_data = ExperimentResultData( + counts=counts, + ) + experiment_result = ExperimentResult( + shots=shots, + success=experiment_success, + data=experiment_data, + header=result_header, + ) + results.append(experiment_result) + batch_job_success[idx] = all(results_valid) result = Result( - backend_name=job.backend().name, + backend_name=self._job.backend().name, backend_version="1.0.0", - qobj_id="1234", - job_id=job.job_id, + qobj_id="", + job_id=self._job.job_id, success=all(batch_job_success), results=results, ) diff --git a/tests/test_jobs.py b/tests/test_jobs.py index 40c639e..01ba870 100644 --- a/tests/test_jobs.py +++ b/tests/test_jobs.py @@ -1,19 +1,23 @@ import asyncio +from typing import List, Union from unittest.mock import AsyncMock, MagicMock import pytest from pytest_mock import MockerFixture -from qiskit.providers.jobstatus import JobStatus +from qiskit import QuantumCircuit from qiskit_quantuminspire.qi_jobs import QIJob def test_result(mocker: MockerFixture) -> None: - job = QIJob(run_input="", backend=None, job_id="some-id") - mocker.patch.object(job, "status", return_value=JobStatus.DONE) + qc = QuantumCircuit(2, 2) + + job = QIJob(run_input=qc, backend=None, job_id="some-id") + + mocker.patch.object(job, "done", return_value=True) - mock_fetch_job_results = AsyncMock(return_value=[MagicMock()]) + mock_fetch_job_results = AsyncMock(return_value=MagicMock()) mocker.patch.object(job, "_fetch_job_results", mock_fetch_job_results) mock_process = mocker.patch( @@ -21,22 +25,33 @@ def test_result(mocker: MockerFixture) -> None: return_value=MagicMock(), ) - job.result() + for _ in range(4): # Check caching + job.result() - assert mock_fetch_job_results.called mock_process.assert_called_once() + mock_fetch_job_results.assert_called_once() def test_result_raises_error_when_status_not_done(mocker: MockerFixture) -> None: job = QIJob(run_input="", backend=None, job_id="some-id") - - mocker.patch.object(job, "status", return_value=JobStatus.RUNNING) - + mocker.patch.object(job, "done", return_value=False) with pytest.raises(RuntimeError): job.result() -def test_fetch_job_result(mocker: MockerFixture) -> None: +@pytest.mark.parametrize( + "circuits, expected_n_jobs", + [ + (QuantumCircuit(1, 1), 1), # Single circuit + ([QuantumCircuit(1, 1), QuantumCircuit(2, 2)], 2), # List of circuits + ], +) +def test_fetch_job_result( + mocker: MockerFixture, + page_reader_mock: MockerFixture, + circuits: Union[QuantumCircuit, List[QuantumCircuit]], + expected_n_jobs: int, +) -> None: mocker.patch( "qiskit_quantuminspire.qi_jobs.config", @@ -46,20 +61,19 @@ def test_fetch_job_result(mocker: MockerFixture) -> None: "qiskit_quantuminspire.qi_jobs.ApiClient", autospec=True, ) - n_jobs = 3 - mock_results_api = MagicMock() - mock_get_result_by_id = AsyncMock() - mock_get_result_by_id.side_effect = [MagicMock() for _ in range(n_jobs)] - mock_results_api.read_results_by_job_id_results_job_job_id_get = mock_get_result_by_id + mocker.patch( + "qiskit_quantuminspire.qi_jobs.ResultsApi", + autospec=True, + ) - mock_results_api = mocker.patch("qiskit_quantuminspire.qi_jobs.ResultsApi", return_value=mock_results_api) + page_reader_mock.get_all.side_effect = [[MagicMock()] for _ in range(expected_n_jobs)] - job = QIJob(run_input="", backend=None, job_id="some-id") + job = QIJob(run_input=circuits, backend=None, job_id="some-id") + + asyncio.run(job._fetch_job_results()) - job._job_ids = [str(i) for i in range(n_jobs)] + assert len(job.circuits_run_data) == expected_n_jobs - results = asyncio.run(job._fetch_job_results()) + assert all(circuit_data.results for circuit_data in job.circuits_run_data) - assert len(results) == n_jobs - assert mock_get_result_by_id.call_count == n_jobs diff --git a/tests/test_results.py b/tests/test_results.py index 6488dfc..267bb67 100644 --- a/tests/test_results.py +++ b/tests/test_results.py @@ -1,96 +1,86 @@ from datetime import datetime, timezone -import pytest -from compute_api_client import BackendStatus, BackendType, Metadata, Result as JobResult +from compute_api_client import Result as RawJobResult +from qiskit import QuantumCircuit +from qiskit.qobj import QobjExperimentHeader from qiskit.result.models import ExperimentResult, ExperimentResultData from qiskit.result.result import Result -from qiskit_quantuminspire.qi_backend import QIBackend from qiskit_quantuminspire.qi_jobs import QIJob from qiskit_quantuminspire.qi_results import QIResult +from tests.helpers import create_backend_type -@pytest.fixture -def qi_backend() -> QIBackend: - backend_type = BackendType( - id=1, - name="Spin 2", - infrastructure="Hetzner", - description="Silicon spin quantum computer", - image_id="abcd1234", - is_hardware=True, - features=["multiple_measurements"], - default_compiler_config={}, - native_gateset={"single_qubit_gates": ["X"]}, - status=BackendStatus.IDLE, - default_number_of_shots=1024, - max_number_of_shots=2048, - ) - - metadata = Metadata(id=1, backend_id=1, created_on=datetime.now(timezone.utc), data={"nqubits": 6}) - return QIBackend(backend_type=backend_type, metadata=metadata) - - -@pytest.fixture -def qi_job(qi_backend: QIBackend) -> QIJob: - return QIJob(run_input="", backend=qi_backend, job_id="some-id") +def test_process() -> None: + qi_backend = create_backend_type(name="qi_backend_1") + qc = QuantumCircuit(2, 2) - -def test_process(qi_job: QIJob) -> None: - qi_job._job_ids = ["1"] # The jobs in the batch job - qi_job.job_id = "100" # The batch job ID - raw_results = [] - for _id in qi_job._job_ids: - raw_results.append( - JobResult( - id=int(_id), - metadata_id=1, - created_on=datetime(2022, 10, 25, 15, 37, 54, 269823, tzinfo=timezone.utc), - execution_time_in_seconds=1.23, - shots_requested=100, - shots_done=100, - results={ - "0000000000": 256, - "0000000001": 256, - "0000000010": 256, - "0000000011": 256, - }, - job_id=int(qi_job.job_id), - ) + qi_job = QIJob(run_input=qc, backend=qi_backend, job_id="some-id") + batch_job_id = "100" + qi_job.job_id = batch_job_id + individual_job_id = 1 + qi_job.circuits_run_data[0].job_id = individual_job_id + raw_results = [ + RawJobResult( + id=individual_job_id, + metadata_id=1, + created_on=datetime(2022, 10, 25, 15, 37, 54, 269823, tzinfo=timezone.utc), + execution_time_in_seconds=1.23, + shots_requested=100, + shots_done=100, + results={ + "0000000000": 256, + "0000000001": 256, + "0000000010": 256, + "0000000011": 256, + }, + job_id=int(qi_job.job_id), ) - processed_results = QIResult(raw_results).process(qi_job) + ] + qi_job.circuits_run_data[0].results = raw_results + processed_results = QIResult(qi_job).process() + experiment_data = ExperimentResultData(counts={"0x0": 256, "0x1": 256, "0x2": 256, "0x3": 256}) + experiment_result = ExperimentResult( + shots=100, + success=True, + meas_level=2, + data=experiment_data, + header=QobjExperimentHeader(name=qi_job.circuits_run_data[0].circuit.name), + ) expected_results = Result( - backend_name="Spin 2", + backend_name="qi_backend_1", backend_version="1.0.0", - qobj_id="1234", - job_id="100", + qobj_id="", + job_id=batch_job_id, success=True, - results=[ - ExperimentResult( - shots=100, - success=True, - meas_level=2, - data=ExperimentResultData(counts={"0x0": 256, "0x1": 256, "0x2": 256, "0x3": 256}), - ) - ], + results=[experiment_result], date=None, status=None, header=None, ) assert processed_results.to_dict() == expected_results.to_dict() + assert processed_results.data(qc) == experiment_data.to_dict() + + +def test_process_handles_failed_job() -> None: + + qi_backend = create_backend_type(name="qi_backend_1") + qc = QuantumCircuit(2, 2) + qi_job = QIJob(run_input=qc, backend=qi_backend, job_id="some-id") + batch_job_id = "100" + qi_job.job_id = batch_job_id + individual_job_id = 1 + qi_job.circuits_run_data[0].job_id = individual_job_id -def test_process_handles_failed_job(qi_job: QIJob) -> None: - qi_job._job_ids = ["1"] # The jobs in the batch job - qi_job.job_id = "100" # The batch job ID - raw_results = [Exception("Bad Result")] + qi_job.circuits_run_data[0].results = [] - processed_results = QIResult(raw_results).process(qi_job) + processed_results = QIResult(qi_job).process() expected_results = Result( - backend_name="Spin 2", + backend_name="qi_backend_1", backend_version="1.0.0", - qobj_id="1234", - job_id="100", + qobj_id="", + job_id=batch_job_id, success=False, results=[ ExperimentResult( @@ -98,6 +88,7 @@ def test_process_handles_failed_job(qi_job: QIJob) -> None: success=False, meas_level=2, data=ExperimentResultData(counts={}), + header=QobjExperimentHeader(name=qi_job.circuits_run_data[0].circuit.name), ) ], date=None, From 479bfee2d574f2831a1226533b729e9dceff55ea Mon Sep 17 00:00:00 2001 From: Nischal Sehrawat Date: Wed, 18 Sep 2024 11:11:00 +0200 Subject: [PATCH 4/5] [QI2-1081] Fixed linting issues --- qiskit_quantuminspire/qi_jobs.py | 6 ++++-- tests/conftest.py | 15 +++++++++++++++ tests/test_jobs.py | 3 +-- tests/test_qi_provider.py | 10 ---------- 4 files changed, 20 insertions(+), 14 deletions(-) create mode 100644 tests/conftest.py diff --git a/qiskit_quantuminspire/qi_jobs.py b/qiskit_quantuminspire/qi_jobs.py index f82fd50..81555e0 100644 --- a/qiskit_quantuminspire/qi_jobs.py +++ b/qiskit_quantuminspire/qi_jobs.py @@ -16,7 +16,9 @@ class CircuitExecutionData: """Class for bookkeping of individual jobs.""" - def __init__(self, circuit: QuantumCircuit, job_id: int = None, results: List[RawJobResult] = None) -> None: + def __init__( + self, circuit: QuantumCircuit, job_id: Union[int, None] = None, results: Union[List[RawJobResult], None] = None + ) -> None: self.job_id = job_id self.circuit = circuit self.results = [] if results is None else results @@ -77,7 +79,7 @@ async def _fetch_job_results(self) -> None: for circuit_data, result_item in zip(self.circuits_run_data, result_items): circuit_data.results = result_item - + def result(self) -> Result: """Return the results of the job.""" if not self.done(): diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..b6eb384 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,15 @@ +from unittest.mock import AsyncMock + +import pytest +from pytest_mock import MockFixture + +from qiskit_quantuminspire.api.pagination import PageReader + + +@pytest.fixture +def page_reader_mock(mocker: MockFixture) -> AsyncMock: + # Simply calling mocker.patch() doesn't work because PageReader is a generic class + page_reader_mock = AsyncMock() + page_reader_mock.get_all = AsyncMock() + mocker.patch.object(PageReader, "get_all", page_reader_mock.get_all) + return page_reader_mock diff --git a/tests/test_jobs.py b/tests/test_jobs.py index 01ba870..fcef346 100644 --- a/tests/test_jobs.py +++ b/tests/test_jobs.py @@ -48,7 +48,7 @@ def test_result_raises_error_when_status_not_done(mocker: MockerFixture) -> None ) def test_fetch_job_result( mocker: MockerFixture, - page_reader_mock: MockerFixture, + page_reader_mock: AsyncMock, circuits: Union[QuantumCircuit, List[QuantumCircuit]], expected_n_jobs: int, ) -> None: @@ -76,4 +76,3 @@ def test_fetch_job_result( assert len(job.circuits_run_data) == expected_n_jobs assert all(circuit_data.results for circuit_data in job.circuits_run_data) - diff --git a/tests/test_qi_provider.py b/tests/test_qi_provider.py index b0bd36d..0edd954 100644 --- a/tests/test_qi_provider.py +++ b/tests/test_qi_provider.py @@ -4,7 +4,6 @@ import pytest from pytest_mock import MockFixture -from qiskit_quantuminspire.api.pagination import PageReader from qiskit_quantuminspire.qi_backend import QIBackend from qiskit_quantuminspire.qi_provider import QIProvider from tests.helpers import create_backend_type @@ -17,15 +16,6 @@ def mock_api(mocker: MockFixture) -> None: mocker.patch("qiskit_quantuminspire.qi_provider.BackendTypesApi") -@pytest.fixture -def page_reader_mock(mocker: MockFixture) -> AsyncMock: - # Simply calling mocker.patch() doesn't work because PageReader is a generic class - page_reader_mock = AsyncMock() - page_reader_mock.get_all = AsyncMock() - mocker.patch.object(PageReader, "get_all", page_reader_mock.get_all) - return page_reader_mock - - def test_qi_provider_construct(mock_api: Any, page_reader_mock: AsyncMock) -> None: # Arrange page_reader_mock.get_all.return_value = [ From 7cac67dd69eba4dca034c75ac4056bb33c143ab2 Mon Sep 17 00:00:00 2001 From: Nischal Sehrawat Date: Thu, 19 Sep 2024 11:48:25 +0200 Subject: [PATCH 5/5] [QI2-1081] Implemented review 2 feedbacks --- .vscode/settings.json | 4 - qiskit_quantuminspire/qi_jobs.py | 79 ++++++++++++--- qiskit_quantuminspire/qi_results.py | 72 -------------- tests/test_jobs.py | 148 ++++++++++++++++++++++++---- tests/test_results.py | 98 ------------------ 5 files changed, 192 insertions(+), 209 deletions(-) delete mode 100644 qiskit_quantuminspire/qi_results.py delete mode 100644 tests/test_results.py diff --git a/.vscode/settings.json b/.vscode/settings.json index 60df6a8..0181cbd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,10 +2,6 @@ "python.analysis.extraPaths": [ "qiskit_quantuminspire" ], - "editor.codeActionsOnSave": {}, - "black-formatter.path": [ - "${command:python.interpreterPath}" - ], "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true, "python.testing.pytestPath": "${command:python.interpreterPath}", diff --git a/qiskit_quantuminspire/qi_jobs.py b/qiskit_quantuminspire/qi_jobs.py index 81555e0..2d79448 100644 --- a/qiskit_quantuminspire/qi_jobs.py +++ b/qiskit_quantuminspire/qi_jobs.py @@ -1,27 +1,28 @@ import asyncio -from typing import Any, List, Union +from dataclasses import dataclass +from functools import cache +from typing import Any, Dict, List, Optional, Union from compute_api_client import ApiClient, PageResult, Result as RawJobResult, ResultsApi from qiskit.circuit import QuantumCircuit from qiskit.providers import JobV1 as Job from qiskit.providers.backend import Backend from qiskit.providers.jobstatus import JobStatus +from qiskit.qobj import QobjExperimentHeader +from qiskit.result.models import ExperimentResult, ExperimentResultData from qiskit.result.result import Result from qiskit_quantuminspire.api.client import config from qiskit_quantuminspire.api.pagination import PageReader -from qiskit_quantuminspire.qi_results import QIResult +@dataclass class CircuitExecutionData: - """Class for bookkeping of individual jobs.""" + """Class for book-keeping of individual jobs.""" - def __init__( - self, circuit: QuantumCircuit, job_id: Union[int, None] = None, results: Union[List[RawJobResult], None] = None - ) -> None: - self.job_id = job_id - self.circuit = circuit - self.results = [] if results is None else results + circuit: QuantumCircuit + job_id: Optional[int] = None + results: Optional[RawJobResult] = None # Ignore type checking for QIJob due to missing Qiskit type stubs, @@ -46,7 +47,6 @@ def __init__( **kwargs: Additional keyword arguments passed to the parent `Job` class. """ super().__init__(backend, job_id, **kwargs) - self._cached_result: Union[Result, None] = None self.circuits_run_data: List[CircuitExecutionData] = ( [CircuitExecutionData(circuit=run_input)] if isinstance(run_input, QuantumCircuit) @@ -78,18 +78,67 @@ async def _fetch_job_results(self) -> None: result_items = await asyncio.gather(*result_tasks) for circuit_data, result_item in zip(self.circuits_run_data, result_items): - circuit_data.results = result_item + circuit_data.results = None if not result_item else result_item[0] + @cache def result(self) -> Result: """Return the results of the job.""" if not self.done(): raise RuntimeError(f"(Batch)Job status is {self.status()}.") - if self._cached_result: - return self._cached_result asyncio.run(self._fetch_job_results()) - self._cached_result = QIResult(self).process() - return self._cached_result + return self._process_results() def status(self) -> JobStatus: """Return the status of the (batch)job, among the values of ``JobStatus``.""" return JobStatus.DONE + + def _process_results(self) -> Result: + """Process the raw job results obtained from QuantumInspire.""" + + results = [] + batch_job_success = [False] * len(self.circuits_run_data) + + for idx, circuit_data in enumerate(self.circuits_run_data): + qi_result = circuit_data.results + circuit_name = circuit_data.circuit.name + + if qi_result is None: + experiment_result = self._get_experiment_result(circuit_name=circuit_name) + results.append(experiment_result) + continue + experiment_result = self._get_experiment_result( + circuit_name=circuit_name, + shots=qi_result.shots_done, + counts={hex(int(key, 2)): value for key, value in qi_result.results.items()}, + experiment_success=qi_result.shots_done > 0, + ) + results.append(experiment_result) + batch_job_success[idx] = qi_result.shots_done > 0 + + result = Result( + backend_name=self.backend().name, + backend_version="1.0.0", + qobj_id="", + job_id=self.job_id, + success=all(batch_job_success), + results=results, + ) + return result + + @staticmethod + def _get_experiment_result( + circuit_name: str, + shots: int = 0, + counts: Optional[Dict[str, int]] = None, + experiment_success: bool = False, + ) -> ExperimentResult: + """Create an ExperimentResult instance based on RawJobResult parameters.""" + experiment_data = ExperimentResultData( + counts={} if counts is None else counts, + ) + return ExperimentResult( + shots=shots, + success=experiment_success, + data=experiment_data, + header=QobjExperimentHeader(name=circuit_name), + ) diff --git a/qiskit_quantuminspire/qi_results.py b/qiskit_quantuminspire/qi_results.py deleted file mode 100644 index dc359d4..0000000 --- a/qiskit_quantuminspire/qi_results.py +++ /dev/null @@ -1,72 +0,0 @@ -from qiskit.providers import JobV1 as Job -from qiskit.qobj import QobjExperimentHeader -from qiskit.result.models import ExperimentResult, ExperimentResultData -from qiskit.result.result import Result - - -class QIResult: - """Handle QuantumInspire (batch job) results to integrate with Qiskit's Result interface.""" - - def __init__(self, job: Job) -> None: - """Initialize the result processor for QuantumInspire job results. - - Args: - job: The (batch) job for which the results were obtained. While specified as `Job` - to avoid circular dependency, it is a `QIJob`. - Returns: - None. - """ - - self._job = job # The batch job - - def process(self) -> Result: - """Process the raw job results obtained from QuantumInspire.""" - - results = [] - batch_job_success = [False] * len(self._job.circuits_run_data) - - for idx, ciruit_data in enumerate(self._job.circuits_run_data): - result_header = QobjExperimentHeader(name=ciruit_data.circuit.name) - circuit_results = ciruit_data.results - - if not circuit_results: - # For failed job, there are no results - experiment_data = ExperimentResultData( - counts={}, - ) - experiment_result = ExperimentResult( - shots=0, - success=False, - data=experiment_data, - header=result_header, - ) - results.append(experiment_result) - else: - results_valid = [False] * len(circuit_results) - for idx_res, result in enumerate(circuit_results): - shots = result.shots_done - experiment_success = result.shots_done > 0 - counts = {hex(int(key, 2)): value for key, value in result.results.items()} - results_valid[idx_res] = experiment_success - - experiment_data = ExperimentResultData( - counts=counts, - ) - experiment_result = ExperimentResult( - shots=shots, - success=experiment_success, - data=experiment_data, - header=result_header, - ) - results.append(experiment_result) - batch_job_success[idx] = all(results_valid) - - result = Result( - backend_name=self._job.backend().name, - backend_version="1.0.0", - qobj_id="", - job_id=self._job.job_id, - success=all(batch_job_success), - results=results, - ) - return result diff --git a/tests/test_jobs.py b/tests/test_jobs.py index fcef346..b899c32 100644 --- a/tests/test_jobs.py +++ b/tests/test_jobs.py @@ -1,12 +1,35 @@ import asyncio +from datetime import datetime, timezone from typing import List, Union from unittest.mock import AsyncMock, MagicMock import pytest +from compute_api_client import Result as RawJobResult from pytest_mock import MockerFixture from qiskit import QuantumCircuit +from qiskit.qobj import QobjExperimentHeader +from qiskit.result.models import ExperimentResult, ExperimentResultData +from qiskit.result.result import Result from qiskit_quantuminspire.qi_jobs import QIJob +from tests.helpers import create_backend_type + + +@pytest.fixture +def mock_configs_apis(mocker: MockerFixture) -> None: + mocker.patch( + "qiskit_quantuminspire.qi_jobs.config", + return_value=MagicMock(), + ) + mocker.patch( + "qiskit_quantuminspire.qi_jobs.ApiClient", + autospec=True, + ) + + mocker.patch( + "qiskit_quantuminspire.qi_jobs.ResultsApi", + autospec=True, + ) def test_result(mocker: MockerFixture) -> None: @@ -20,15 +43,13 @@ def test_result(mocker: MockerFixture) -> None: mock_fetch_job_results = AsyncMock(return_value=MagicMock()) mocker.patch.object(job, "_fetch_job_results", mock_fetch_job_results) - mock_process = mocker.patch( - "qiskit_quantuminspire.qi_jobs.QIResult.process", - return_value=MagicMock(), - ) + mock_process_results = MagicMock(return_value=MagicMock()) + mocker.patch.object(job, "_process_results", mock_process_results) for _ in range(4): # Check caching job.result() - mock_process.assert_called_once() + mock_process_results.assert_called_once() mock_fetch_job_results.assert_called_once() @@ -47,26 +68,12 @@ def test_result_raises_error_when_status_not_done(mocker: MockerFixture) -> None ], ) def test_fetch_job_result( - mocker: MockerFixture, page_reader_mock: AsyncMock, circuits: Union[QuantumCircuit, List[QuantumCircuit]], expected_n_jobs: int, + mock_configs_apis: None, ) -> None: - mocker.patch( - "qiskit_quantuminspire.qi_jobs.config", - return_value=MagicMock(), - ) - mocker.patch( - "qiskit_quantuminspire.qi_jobs.ApiClient", - autospec=True, - ) - - mocker.patch( - "qiskit_quantuminspire.qi_jobs.ResultsApi", - autospec=True, - ) - page_reader_mock.get_all.side_effect = [[MagicMock()] for _ in range(expected_n_jobs)] job = QIJob(run_input=circuits, backend=None, job_id="some-id") @@ -76,3 +83,104 @@ def test_fetch_job_result( assert len(job.circuits_run_data) == expected_n_jobs assert all(circuit_data.results for circuit_data in job.circuits_run_data) + + +def test_fetch_job_result_handles_invalid_results( + page_reader_mock: AsyncMock, + mock_configs_apis: None, +) -> None: + + circuits = [QuantumCircuit(1, 1), QuantumCircuit(2, 2)] + + page_reader_mock.get_all.side_effect = [[], [None]] + + job = QIJob(run_input=circuits, backend=None, job_id="some-id") + + asyncio.run(job._fetch_job_results()) + + assert all(circuit_data.results is None for circuit_data in job.circuits_run_data) + + assert len(job.circuits_run_data) == len(circuits) + + +def test_process_results() -> None: + qi_backend = create_backend_type(name="qi_backend_1") + qc = QuantumCircuit(2, 2) + + qi_job = QIJob(run_input=qc, backend=qi_backend, job_id="some-id") + batch_job_id = "100" + qi_job.job_id = batch_job_id + individual_job_id = 1 + qi_job.circuits_run_data[0].job_id = 1 # Individual job_id + qi_job.circuits_run_data[0].results = RawJobResult( + id=individual_job_id, + metadata_id=1, + created_on=datetime(2022, 10, 25, 15, 37, 54, 269823, tzinfo=timezone.utc), + execution_time_in_seconds=1.23, + shots_requested=100, + shots_done=100, + results={ + "0000000000": 256, + "0000000001": 256, + "0000000010": 256, + "0000000011": 256, + }, + job_id=int(qi_job.job_id), + ) + processed_results = qi_job._process_results() + experiment_data = ExperimentResultData(counts={"0x0": 256, "0x1": 256, "0x2": 256, "0x3": 256}) + experiment_result = ExperimentResult( + shots=100, + success=True, + meas_level=2, + data=experiment_data, + header=QobjExperimentHeader(name=qi_job.circuits_run_data[0].circuit.name), + ) + expected_results = Result( + backend_name="qi_backend_1", + backend_version="1.0.0", + qobj_id="", + job_id=batch_job_id, + success=True, + results=[experiment_result], + date=None, + status=None, + header=None, + ) + assert processed_results.to_dict() == expected_results.to_dict() + assert processed_results.data(qc) == experiment_data.to_dict() + + +def test_process_results_handles_invalid_results() -> None: + + qi_backend = create_backend_type(name="qi_backend_1") + qc = QuantumCircuit(2, 2) + + qi_job = QIJob(run_input=qc, backend=qi_backend, job_id="some-id") + batch_job_id = "100" + qi_job.job_id = batch_job_id + qi_job.circuits_run_data[0].job_id = 1 # Individual job_id + + qi_job.circuits_run_data[0].results = None + + processed_results = qi_job._process_results() + expected_results = Result( + backend_name="qi_backend_1", + backend_version="1.0.0", + qobj_id="", + job_id=batch_job_id, + success=False, + results=[ + ExperimentResult( + shots=0, + success=False, + meas_level=2, + data=ExperimentResultData(counts={}), + header=QobjExperimentHeader(name=qi_job.circuits_run_data[0].circuit.name), + ) + ], + date=None, + status=None, + header=None, + ) + assert processed_results.to_dict() == expected_results.to_dict() diff --git a/tests/test_results.py b/tests/test_results.py deleted file mode 100644 index 267bb67..0000000 --- a/tests/test_results.py +++ /dev/null @@ -1,98 +0,0 @@ -from datetime import datetime, timezone - -from compute_api_client import Result as RawJobResult -from qiskit import QuantumCircuit -from qiskit.qobj import QobjExperimentHeader -from qiskit.result.models import ExperimentResult, ExperimentResultData -from qiskit.result.result import Result - -from qiskit_quantuminspire.qi_jobs import QIJob -from qiskit_quantuminspire.qi_results import QIResult -from tests.helpers import create_backend_type - - -def test_process() -> None: - qi_backend = create_backend_type(name="qi_backend_1") - qc = QuantumCircuit(2, 2) - - qi_job = QIJob(run_input=qc, backend=qi_backend, job_id="some-id") - batch_job_id = "100" - qi_job.job_id = batch_job_id - individual_job_id = 1 - qi_job.circuits_run_data[0].job_id = individual_job_id - raw_results = [ - RawJobResult( - id=individual_job_id, - metadata_id=1, - created_on=datetime(2022, 10, 25, 15, 37, 54, 269823, tzinfo=timezone.utc), - execution_time_in_seconds=1.23, - shots_requested=100, - shots_done=100, - results={ - "0000000000": 256, - "0000000001": 256, - "0000000010": 256, - "0000000011": 256, - }, - job_id=int(qi_job.job_id), - ) - ] - qi_job.circuits_run_data[0].results = raw_results - processed_results = QIResult(qi_job).process() - experiment_data = ExperimentResultData(counts={"0x0": 256, "0x1": 256, "0x2": 256, "0x3": 256}) - experiment_result = ExperimentResult( - shots=100, - success=True, - meas_level=2, - data=experiment_data, - header=QobjExperimentHeader(name=qi_job.circuits_run_data[0].circuit.name), - ) - expected_results = Result( - backend_name="qi_backend_1", - backend_version="1.0.0", - qobj_id="", - job_id=batch_job_id, - success=True, - results=[experiment_result], - date=None, - status=None, - header=None, - ) - assert processed_results.to_dict() == expected_results.to_dict() - assert processed_results.data(qc) == experiment_data.to_dict() - - -def test_process_handles_failed_job() -> None: - - qi_backend = create_backend_type(name="qi_backend_1") - qc = QuantumCircuit(2, 2) - - qi_job = QIJob(run_input=qc, backend=qi_backend, job_id="some-id") - batch_job_id = "100" - qi_job.job_id = batch_job_id - individual_job_id = 1 - qi_job.circuits_run_data[0].job_id = individual_job_id - - qi_job.circuits_run_data[0].results = [] - - processed_results = QIResult(qi_job).process() - expected_results = Result( - backend_name="qi_backend_1", - backend_version="1.0.0", - qobj_id="", - job_id=batch_job_id, - success=False, - results=[ - ExperimentResult( - shots=0, - success=False, - meas_level=2, - data=ExperimentResultData(counts={}), - header=QobjExperimentHeader(name=qi_job.circuits_run_data[0].circuit.name), - ) - ], - date=None, - status=None, - header=None, - ) - assert processed_results.to_dict() == expected_results.to_dict()