Skip to content

Commit 48378aa

Browse files
authored
Merge pull request #38 from CQCL/release/0.32
Release/0.32
2 parents 2d67c97 + bd333c3 commit 48378aa

11 files changed

+372
-241
lines changed

_metadata.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
__extension_version__ = "0.31.0"
1+
__extension_version__ = "0.32.0"
22
__extension_name__ = "pytket-qiskit"

docs/changelog.rst

+10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
Changelog
22
~~~~~~~~~
33

4+
0.32.0 (December 2022)
5+
----------------------
6+
7+
* Use ``qiskit_ibm_runtime`` services for sampling on ``IBMQBackend`` and
8+
``IBMQEmulatorBackend``. Note that shots tables (ordered lists of results) are
9+
no longer available from these backends. (``BackendResult.get_shots()`` will
10+
fail; use ``get_counts()`` instead.)
11+
12+
* Fix incorrect circuit permutation handling for ``AerUnitaryBackend`` and ``AerStateBackend``.
13+
414
0.31.0 (November 2022)
515
----------------------
616

pytket/extensions/qiskit/backends/ibm.py

+76-66
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import itertools
1616
import logging
1717
from ast import literal_eval
18+
from collections import Counter
1819
import json
1920
from typing import (
2021
cast,
@@ -31,12 +32,16 @@
3132

3233
import qiskit # type: ignore
3334
from qiskit import IBMQ
34-
from qiskit.qobj import QobjExperimentHeader # type: ignore
35-
from qiskit.providers.ibmq.exceptions import IBMQBackendApiError # type: ignore
36-
from qiskit.providers.ibmq.job import IBMQJob # type: ignore
37-
from qiskit.result import Result, models # type: ignore
35+
from qiskit.primitives import SamplerResult # type: ignore
3836
from qiskit.tools.monitor import job_monitor # type: ignore
39-
from qiskit.providers.ibmq.utils.utils import api_status_to_job_status # type: ignore
37+
from qiskit.result.distributions import QuasiDistribution # type: ignore
38+
from qiskit_ibm_runtime import ( # type: ignore
39+
QiskitRuntimeService,
40+
Session,
41+
Options,
42+
Sampler,
43+
RuntimeJob,
44+
)
4045

4146
from pytket.circuit import Circuit, OpType # type: ignore
4247
from pytket.backends import Backend, CircuitNotRunError, CircuitStatus, ResultHandle
@@ -71,12 +76,10 @@
7176
Predicate,
7277
)
7378
from pytket.extensions.qiskit.qiskit_convert import tk_to_qiskit, _tk_gate_set
74-
from pytket.extensions.qiskit.result_convert import (
75-
qiskit_experimentresult_to_backendresult,
76-
)
7779
from pytket.architecture import FullyConnected # type: ignore
7880
from pytket.placement import NoiseAwarePlacement # type: ignore
7981
from pytket.utils import prepare_circuit
82+
from pytket.utils.outcomearray import OutcomeArray
8083
from pytket.utils.results import KwargTypes
8184
from .ibm_utils import _STATUS_MAP, _batch_circuits
8285
from .config import QiskitConfig
@@ -86,35 +89,18 @@
8689
IBMQBackend as _QiskIBMQBackend,
8790
AccountProvider,
8891
)
89-
from qiskit.providers.models import QasmBackendConfiguration # type: ignore
9092

9193
_DEBUG_HANDLE_PREFIX = "_MACHINE_DEBUG_"
9294

9395

94-
def _gen_debug_results(n_qubits: int, shots: int, index: int) -> Result:
95-
raw_counts = {"0x0": shots}
96-
raw_memory = ["0x0"] * shots
97-
base_result_args = dict(
98-
backend_name="test_backend",
99-
backend_version="1.0.0",
100-
qobj_id="id-123",
101-
job_id="job-123",
102-
success=True,
103-
)
104-
data = models.ExperimentResultData(counts=raw_counts, memory=raw_memory)
105-
exp_result_header = QobjExperimentHeader(
106-
creg_sizes=[["c", n_qubits]], memory_slots=n_qubits
107-
)
108-
exp_result = models.ExperimentResult(
109-
shots=shots,
110-
success=True,
111-
meas_level=2,
112-
data=data,
113-
header=exp_result_header,
114-
memory=True,
96+
def _gen_debug_results(n_qubits: int, shots: int, index: int) -> SamplerResult:
97+
debug_dist = {n: 0.0 for n in range(pow(2, n_qubits))}
98+
debug_dist[0] = 1.0
99+
qd = QuasiDistribution(debug_dist)
100+
return SamplerResult(
101+
quasi_dists=[qd] * (index + 1),
102+
metadata=[{"header_metadata": {}, "shots": shots}] * (index + 1),
115103
)
116-
results = [exp_result] * (index + 1)
117-
return Result(results=results, **base_result_args)
118104

119105

120106
class NoIBMQAccountError(Exception):
@@ -128,7 +114,7 @@ def __init__(self) -> None:
128114

129115

130116
class IBMQBackend(Backend):
131-
_supports_shots = True
117+
_supports_shots = False
132118
_supports_counts = True
133119
_supports_contextual_optimisation = True
134120
_persistent_handles = True
@@ -141,6 +127,7 @@ def __init__(
141127
project: Optional[str] = None,
142128
monitor: bool = True,
143129
account_provider: Optional["AccountProvider"] = None,
130+
token: Optional[str] = None,
144131
):
145132
"""A backend for running circuits on remote IBMQ devices.
146133
The provider arguments of `hub`, `group` and `project` can
@@ -166,6 +153,8 @@ def __init__(
166153
Used to pass credentials in if not configured on local machine (as well as hub,
167154
group and project). Defaults to None.
168155
:type account_provider: Optional[AccountProvider]
156+
:param token: Authentication token to use the `QiskitRuntimeService`.
157+
:type token: Optional[str]
169158
"""
170159
super().__init__()
171160
self._pytket_config = QiskitConfig.from_default_config_file()
@@ -175,18 +164,21 @@ def __init__(
175164
else account_provider
176165
)
177166
self._backend: "_QiskIBMQBackend" = self._provider.get_backend(backend_name)
178-
self._config = self._backend.configuration()
179-
self._max_per_job = getattr(self._config, "max_experiments", 1)
167+
config = self._backend.configuration()
168+
self._max_per_job = getattr(config, "max_experiments", 1)
180169

181170
gate_set = _tk_gate_set(self._backend)
182171
self._backend_info = self._get_backend_info(self._backend)
183172

173+
self._service = QiskitRuntimeService(channel="ibm_quantum", token=token)
174+
self._session = Session(service=self._service, backend=backend_name)
175+
184176
self._standard_gateset = gate_set >= {OpType.X, OpType.SX, OpType.Rz, OpType.CX}
185177

186178
self._monitor = monitor
187179

188180
# cache of results keyed by job id and circuit index
189-
self._ibm_res_cache: Dict[Tuple[str, int], models.ExperimentResult] = dict()
181+
self._ibm_res_cache: Dict[Tuple[str, int], Counter] = dict()
190182

191183
self._MACHINE_DEBUG = False
192184

@@ -370,7 +362,8 @@ def default_compilation_pass(self, optimisation_level: int = 2) -> BasePass:
370362

371363
@property
372364
def _result_id_type(self) -> _ResultIdTuple:
373-
return (str, int, str)
365+
# IBMQ job ID, index, number of measurements per shot, post-processing circuit
366+
return (str, int, int, str)
374367

375368
def rebase_pass(self) -> BasePass:
376369
return auto_rebase_pass(
@@ -423,40 +416,47 @@ def process_circuits(
423416
if self._MACHINE_DEBUG:
424417
for i, ind in enumerate(indices_chunk):
425418
handle_list[ind] = ResultHandle(
426-
_DEBUG_HANDLE_PREFIX
427-
+ str((batch_chunk[i].n_qubits, n_shots, batch_id)),
419+
_DEBUG_HANDLE_PREFIX + str((n_shots, batch_id)),
428420
i,
421+
batch_chunk[i].n_qubits,
429422
ppcirc_strs[i],
430423
)
431424
else:
432-
job = self._backend.run(
433-
qcs, shots=n_shots, memory=self._config.memory
434-
)
435-
jobid = job.job_id()
425+
options = Options()
426+
options.optimization_level = 0
427+
options.resilience_level = 0
428+
options.transpilation.skip_transpilation = True
429+
options.execution.shots = n_shots
430+
sampler = Sampler(session=self._session, options=options)
431+
job = sampler.run(circuits=qcs)
432+
job_id = job.job_id
436433
for i, ind in enumerate(indices_chunk):
437-
handle_list[ind] = ResultHandle(jobid, i, ppcirc_strs[i])
434+
handle_list[ind] = ResultHandle(
435+
job_id, i, qcs[i].count_ops()["measure"], ppcirc_strs[i]
436+
)
438437
batch_id += 1
439438
for handle in handle_list:
440439
assert handle is not None
441440
self._cache[handle] = dict()
442441
return cast(List[ResultHandle], handle_list)
443442

444-
def _retrieve_job(self, jobid: str) -> IBMQJob:
445-
return self._backend.retrieve_job(jobid)
443+
def _retrieve_job(self, jobid: str) -> RuntimeJob:
444+
return self._service.job(jobid)
446445

447446
def cancel(self, handle: ResultHandle) -> None:
448447
if not self._MACHINE_DEBUG:
449448
jobid = cast(str, handle[0])
450449
job = self._retrieve_job(jobid)
451-
cancelled = job.cancel()
452-
if not cancelled:
453-
warn(f"Unable to cancel job {jobid}")
450+
try:
451+
job.cancel()
452+
except Exception as e:
453+
warn(f"Unable to cancel job {jobid}: {e}")
454454

455455
def circuit_status(self, handle: ResultHandle) -> CircuitStatus:
456456
self._check_handle_type(handle)
457457
jobid = cast(str, handle[0])
458-
apistatus = self._provider._api_client.job_status(jobid)["status"]
459-
ibmstatus = api_status_to_job_status(apistatus)
458+
job = self._service.job(jobid)
459+
ibmstatus = job.status()
460460
return CircuitStatus(_STATUS_MAP[ibmstatus], ibmstatus.value)
461461

462462
def get_result(self, handle: ResultHandle, **kwargs: KwargTypes) -> BackendResult:
@@ -469,33 +469,43 @@ def get_result(self, handle: ResultHandle, **kwargs: KwargTypes) -> BackendResul
469469
cached_result = self._cache[handle]
470470
if "result" in cached_result:
471471
return cast(BackendResult, cached_result["result"])
472-
jobid, index, ppcirc_str = handle
472+
jobid, index, n_meas, ppcirc_str = handle
473473
ppcirc_rep = json.loads(ppcirc_str)
474474
ppcirc = Circuit.from_dict(ppcirc_rep) if ppcirc_rep is not None else None
475475
cache_key = (jobid, index)
476476
if cache_key not in self._ibm_res_cache:
477477
if self._MACHINE_DEBUG or jobid.startswith(_DEBUG_HANDLE_PREFIX):
478478
shots: int
479-
n_qubits: int
480-
n_qubits, shots, _ = literal_eval(jobid[len(_DEBUG_HANDLE_PREFIX) :])
481-
res = _gen_debug_results(n_qubits, shots, index)
479+
shots, _ = literal_eval(jobid[len(_DEBUG_HANDLE_PREFIX) :])
480+
res = _gen_debug_results(n_meas, shots, index)
482481
else:
483482
try:
484483
job = self._retrieve_job(jobid)
485-
except IBMQBackendApiError:
484+
except Exception as e:
485+
warn(f"Unable to retrieve job {jobid}: {e}")
486486
raise CircuitNotRunError(handle)
487487

488488
if self._monitor and job:
489489
job_monitor(job)
490-
newkwargs = {
491-
key: kwargs[key] for key in ("wait", "timeout") if key in kwargs
492-
}
493-
res = job.result(**newkwargs)
494-
495-
for circ_index, r in enumerate(res.results):
496-
self._ibm_res_cache[(jobid, circ_index)] = r
497-
result = qiskit_experimentresult_to_backendresult(
498-
self._ibm_res_cache[cache_key], ppcirc
499-
)
490+
491+
res = job.result(timeout=kwargs.get("timeout", None))
492+
for circ_index, (r, d) in enumerate(zip(res.quasi_dists, res.metadata)):
493+
self._ibm_res_cache[(jobid, circ_index)] = Counter(
494+
{n: int(0.5 + d["shots"] * p) for n, p in r.items()}
495+
)
496+
497+
counts = self._ibm_res_cache[cache_key] # Counter[int]
498+
# Convert to `OutcomeArray`:
499+
tket_counts: Counter = Counter()
500+
for outcome_key, sample_count in counts.items():
501+
array = OutcomeArray.from_ints(
502+
ints=[outcome_key],
503+
width=n_meas,
504+
big_endian=False,
505+
)
506+
tket_counts[array] = sample_count
507+
# Convert to `BackendResult`:
508+
result = BackendResult(counts=tket_counts, ppcirc=ppcirc)
509+
500510
self._cache[handle] = {"result": result}
501511
return result

0 commit comments

Comments
 (0)