Skip to content

Commit

Permalink
[QI2-1081] Implemented review 2 feedbacks
Browse files Browse the repository at this point in the history
  • Loading branch information
NischalQuTech committed Sep 19, 2024
1 parent 479bfee commit 16877c0
Show file tree
Hide file tree
Showing 5 changed files with 192 additions and 209 deletions.
4 changes: 0 additions & 4 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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}",
Expand Down
79 changes: 64 additions & 15 deletions qiskit_quantuminspire/qi_jobs.py
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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)
Expand Down Expand Up @@ -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 the provided 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),
)
72 changes: 0 additions & 72 deletions qiskit_quantuminspire/qi_results.py

This file was deleted.

148 changes: 128 additions & 20 deletions tests/test_jobs.py
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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()


Expand All @@ -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")
Expand All @@ -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()
Loading

0 comments on commit 16877c0

Please sign in to comment.