Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[QI2-1208] Raw data #87

Merged
merged 6 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/getting_started/submitting.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ job = simulator_backend.run(qc)
print(job.result().get_counts())
```

On backends that support the `raw data` feature, you can set the `memory` option to get the measurement result of each individual shot:

```python
job = simulator_backend.run(qc, memory=True)
```

## Transpilation

Depending on the chosen backends, certain gates may not be supported. Qiskit is aware of the capabilities of each backend, and can transpile
Expand Down
25 changes: 13 additions & 12 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ classifiers = [
[tool.poetry.dependencies]
python = "^3.9"
qiskit = "^1.2.0"
qi-compute-api-client = "^0.35.0"
qi-compute-api-client = "^0.39.0"
pydantic = "^2.10.3"
requests = "^2.32.3"
opensquirrel = "^0.1.0"
Expand Down
12 changes: 10 additions & 2 deletions qiskit_quantuminspire/qi_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,16 @@ def __init__(self, backend_type: BackendType, **kwargs: Any):
super().__init__(name=backend_type.name, description=backend_type.description, **kwargs)
self._id: int = backend_type.id

self._max_shots: int = backend_type.max_number_of_shots

# Construct options
self._options = self._default_options()
self.set_options(shots=backend_type.default_number_of_shots)

self._max_shots: int = backend_type.max_number_of_shots
if not backend_type.supports_raw_data:
self._options.set_validator("memory", [False])

# Construct coupling map
native_gates = [gate.lower() for gate in backend_type.gateset]
available_gates = [gate for gate in native_gates if gate in _ALL_SUPPORTED_GATES]
unknown_gates = set(native_gates) - set(_ALL_SUPPORTED_GATES) - set(_IGNORED_GATES)
Expand Down Expand Up @@ -113,12 +118,15 @@ def _default_options(cls) -> Options:

shots: int: Number of shots for the job.
"""
options = Options(shots=1024, seed_simulator=None)
options = Options(shots=1024, seed_simulator=None, memory=False)

# Seed_simulator is included in options to enable use of BackendEstimatorV2 in Qiskit,
# but is not actually supported by the backend so any other value than none raises an error.
options.set_validator("seed_simulator", [None])

options.set_validator("shots", int)
options.set_validator("memory", bool)

return options

@property
Expand Down
49 changes: 35 additions & 14 deletions qiskit_quantuminspire/qi_jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from dataclasses import dataclass
from functools import cache
from pathlib import Path
from typing import Any, Dict, List, Optional, Union, cast
from typing import Any, List, Optional, Union, cast

from compute_api_client import (
Algorithm,
Expand Down Expand Up @@ -126,6 +126,7 @@ async def job_run_sequence(
in_api_client,
file.id,
in_batch_job.id,
raw_data_enabled=cast(bool, options.get("memory")),
number_of_shots=options.get("shots"),
)
circuit_data.job_id = job.id
Expand Down Expand Up @@ -183,10 +184,20 @@ async def _create_batch_job(self, api_client: ApiClient, backend_type_id: int) -
return await api_instance.create_batch_job_batch_jobs_post(obj)

async def _create_job(
self, api_client: ApiClient, file_id: int, batch_job_id: int, number_of_shots: Optional[int] = None
self,
api_client: ApiClient,
file_id: int,
batch_job_id: int,
raw_data_enabled: bool,
number_of_shots: Optional[int] = None,
) -> Job:
api_instance = JobsApi(api_client)
obj = JobIn(file_id=file_id, batch_job_id=batch_job_id, number_of_shots=number_of_shots)
obj = JobIn(
file_id=file_id,
batch_job_id=batch_job_id,
number_of_shots=number_of_shots,
raw_data_enabled=raw_data_enabled,
)
return await api_instance.create_job_jobs_post(obj)

async def _enqueue_batch_job(self, api_client: ApiClient, batch_job_id: int) -> BatchJob:
Expand Down Expand Up @@ -322,15 +333,13 @@ def _process_results(self) -> Result:
circuit_name = circuit_data.circuit.name

if qi_result is None:
experiment_result = self._get_experiment_result(circuit_name=circuit_name)
experiment_result = self._create_empty_experiment_result(circuit_name=circuit_name)
results.append(experiment_result)
continue

experiment_result = self._get_experiment_result(
experiment_result = self._create_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,
result=qi_result,
)
results.append(experiment_result)
batch_job_success[idx] = qi_result.shots_done > 0
Expand All @@ -346,19 +355,31 @@ def _process_results(self) -> Result:
return result

@staticmethod
def _get_experiment_result(
def _create_experiment_result(
circuit_name: str,
shots: int = 0,
counts: Optional[Dict[str, int]] = None,
experiment_success: bool = False,
result: RawJobResult,
) -> ExperimentResult:
"""Create an ExperimentResult instance based on RawJobResult parameters."""
counts = {hex(int(key, 2)): value for key, value in result.results.items()}
memory = [hex(int(measurement, 2)) for measurement in result.raw_data] if result.raw_data else None

experiment_data = ExperimentResultData(
counts={} if counts is None else counts,
memory=memory,
)
return ExperimentResult(
shots=shots,
success=experiment_success,
shots=result.shots_done,
success=result.shots_done > 0,
data=experiment_data,
header=QobjExperimentHeader(name=circuit_name),
)

@staticmethod
def _create_empty_experiment_result(circuit_name: str) -> ExperimentResult:
"""Create an empty ExperimentResult instance."""
return ExperimentResult(
shots=0,
success=False,
data=ExperimentResultData(counts={}),
header=QobjExperimentHeader(name=circuit_name),
)
28 changes: 27 additions & 1 deletion tests/helpers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from compute_api_client import BackendStatus, BackendType
from datetime import datetime, timezone
from typing import Optional

from compute_api_client import BackendStatus, BackendType, Result as RawJobResult


def create_backend_type(
Expand Down Expand Up @@ -27,4 +30,27 @@ def create_backend_type(
infrastructure="QCI",
description="A Quantum Inspire backend",
native_gateset="",
supports_raw_data=False,
)


def create_raw_job_result(
results: dict[str, int] = {
"0000000000": 256,
"0000000001": 256,
"0000000010": 256,
"0000000011": 256,
},
raw_data: Optional[list[str]] = None,
) -> RawJobResult:
return RawJobResult(
id=1,
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=results,
raw_data=raw_data,
job_id=10,
)
29 changes: 29 additions & 0 deletions tests/test_qi_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,9 +142,38 @@ def test_qi_backend_run_unsupported_options(mocker: MockerFixture) -> None:
# Act & Assert
qc = QuantumCircuit(2, 2)
with pytest.raises(AttributeError):
qi_backend.run(qc, unsupported_option=True)


def test_qi_backend_run_no_shot_memory_support(mocker: MockerFixture) -> None:
# Arrange
job = MagicMock()
mocker.patch("qiskit_quantuminspire.qi_backend.QIJob", return_value=job)
backend_type = create_backend_type(max_number_of_shots=4096)
qi_backend = QIBackend(backend_type=backend_type)

# Act & Assert
qc = QuantumCircuit(2, 2)
with pytest.raises(ValueError):
qi_backend.run(qc, memory=True)


def test_qi_backend_run_supports_shot_memory(mocker: MockerFixture) -> None:
# Arrange
job = MagicMock()
mocker.patch("qiskit_quantuminspire.qi_backend.QIJob", return_value=job)
backend_type = create_backend_type(max_number_of_shots=4096)
backend_type.supports_raw_data = True
qi_backend = QIBackend(backend_type=backend_type)

# Act
qc = QuantumCircuit(2, 2)
qi_backend.run(qc, memory=True)

# Assert
job.submit.assert_called_once()


def test_qi_backend_run_option_bad_value(mocker: MockerFixture) -> None:
# Arrange
job = MagicMock()
Expand Down
53 changes: 31 additions & 22 deletions tests/test_qi_job.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import asyncio
import tempfile
from datetime import datetime, timezone
from typing import Any, List, Optional, 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, qpy
from qiskit.providers import BackendV2
Expand All @@ -18,7 +16,7 @@
from qiskit_quantuminspire.base_provider import BaseProvider
from qiskit_quantuminspire.qi_backend import QIBackend
from qiskit_quantuminspire.qi_jobs import QIJob
from tests.helpers import create_backend_type
from tests.helpers import create_backend_type, create_raw_job_result


class SingleBackendProvider(BaseProvider):
Expand Down Expand Up @@ -159,25 +157,10 @@ def test_process_results() -> None:
qc = QuantumCircuit(2, 2)

qi_job = QIJob(run_input=qc, backend=qi_backend)
batch_job_id = 100
qi_job.batch_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=10,
)
qi_job.batch_job_id = 100
qi_job.circuits_run_data[0].job_id = 1
qi_job.circuits_run_data[0].results = create_raw_job_result()

processed_results = qi_job._process_results()
experiment_data = ExperimentResultData(counts={"0x0": 256, "0x1": 256, "0x2": 256, "0x3": 256})
experiment_result = ExperimentResult(
Expand All @@ -202,6 +185,32 @@ def test_process_results() -> None:
assert processed_results.data(qc) == experiment_data.to_dict()


def test_process_results_raw_data() -> None:
backend_type = create_backend_type(name="qi_backend_1")
qi_backend = QIBackend(backend_type=backend_type)
qc = QuantumCircuit(2, 2)

qi_job = QIJob(run_input=qc, backend=qi_backend)
qi_job.batch_job_id = 100
qi_job.circuits_run_data[0].job_id = 1
qi_job.circuits_run_data[0].results = create_raw_job_result(
results={
"000": 2,
"001": 1,
"010": 2,
"011": 3,
},
raw_data=["000", "001", "010", "011", "000", "010", "011", "011"],
)

processed_results = qi_job._process_results()
experiment_data = ExperimentResultData(
counts={"0x0": 2, "0x1": 1, "0x2": 2, "0x3": 3}, memory=["0x0", "0x1", "0x2", "0x3", "0x0", "0x2", "0x3", "0x3"]
)

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)
Expand Down
Loading