Skip to content

Commit

Permalink
[QI2-1081] Implemented job result fetching
Browse files Browse the repository at this point in the history
  • Loading branch information
NischalQuTech committed Sep 12, 2024
1 parent 1059296 commit 1c6424f
Show file tree
Hide file tree
Showing 5 changed files with 218 additions and 32 deletions.
14 changes: 12 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -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}",
}
38 changes: 14 additions & 24 deletions qiskit_quantuminspire/qi_jobs.py
Original file line number Diff line number Diff line change
@@ -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


Expand All @@ -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.
Expand All @@ -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

Expand Down
26 changes: 20 additions & 6 deletions qiskit_quantuminspire/qi_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
65 changes: 65 additions & 0 deletions tests/test_jobs.py
Original file line number Diff line number Diff line change
@@ -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
107 changes: 107 additions & 0 deletions tests/test_results.py
Original file line number Diff line number Diff line change
@@ -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()

0 comments on commit 1c6424f

Please sign in to comment.