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

Release/0.51.0 #304

Merged
merged 6 commits into from
Apr 2, 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
2 changes: 1 addition & 1 deletion _metadata.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
__extension_version__ = "0.50.0"
__extension_version__ = "0.51.0"
__extension_name__ = "pytket-qiskit"
5 changes: 0 additions & 5 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,6 @@ API documentation
:show-inheritance:
:members:

.. autoclass:: pytket.extensions.qiskit.IBMQLocalEmulatorBackend
:special-members: __init__
:show-inheritance:
:members:

.. autoclass:: pytket.extensions.qiskit.AerBackend
:special-members: __init__
:inherited-members:
Expand Down
9 changes: 9 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
Changelog
~~~~~~~~~

0.51.0 (March 2024)
-------------------

* Update qiskit-ibm-runtime version requirement to 0.22.
* remove all remote simulators
* rename ``IBMQLocalEmulatorBackend`` to ``IBMQEmulatorBackend``
* ``IBMQEmulatorBackend`` will now run locally
* add support for contextual optimisation on local emulator

0.50.0 (March 2024)
-------------------

Expand Down
5 changes: 2 additions & 3 deletions docs/intro.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ Available IBM Backends

IBMQBackend
IBMQEmulatorBackend
IBMQLocalEmulatorBackend
AerBackend
AerStateBackend
AerUnitaryBackend
Expand Down Expand Up @@ -95,7 +94,7 @@ to disk:
IBMProvider.save_account(token=ibm_token)
QiskitRuntimeService.save_account(channel="ibm_quantum", token=ibm_token)

To see which devices you can access you can use the ``available_devices`` method on the :py:class:`IBMQBackend` or :py:class:`IBMQEmulatorBackend`. Note that it is possible to pass optional ``instance`` and ``provider`` arguments to this method. This allows you to see which devices are accessible through your IBM hub.
To see which devices you can access you can use the ``available_devices`` method on the :py:class:`IBMQBackend`. Note that it is possible to pass optional ``instance`` and ``provider`` arguments to this method. This allows you to see which devices are accessible through your IBM hub.

::

Expand Down Expand Up @@ -160,7 +159,7 @@ Default Compilation

Every :py:class:`Backend` in pytket has its own ``default_compilation_pass`` method. This method applies a sequence of optimisations to a circuit depending on the value of an ``optimisation_level`` parameter. This default compilation will ensure that the circuit meets all the constraints required to run on the :py:class:`Backend`. The passes applied by different levels of optimisation are specified in the table below.

.. list-table:: **Default compilation pass for the IBMQBackend, IBMQEmulatorBackend and IBMQLocalEmulatorBackend**
.. list-table:: **Default compilation pass for the IBMQBackend and IBMQEmulatorBackend**
:widths: 25 25 25
:header-rows: 1

Expand Down
1 change: 0 additions & 1 deletion pytket/extensions/qiskit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
AerStateBackend,
AerUnitaryBackend,
IBMQEmulatorBackend,
IBMQLocalEmulatorBackend,
)
from .backends.config import set_ibmq_config
from .qiskit_convert import qiskit_to_tk, tk_to_qiskit, process_characterisation
Expand Down
1 change: 0 additions & 1 deletion pytket/extensions/qiskit/backends/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,3 @@
from .ibm import IBMQBackend, NoIBMQCredentialsError
from .aer import AerBackend, AerStateBackend, AerUnitaryBackend
from .ibmq_emulator import IBMQEmulatorBackend
from .ibmq_local_emulator import IBMQLocalEmulatorBackend
26 changes: 20 additions & 6 deletions pytket/extensions/qiskit/backends/aer.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import itertools
from collections import defaultdict
from dataclasses import dataclass
import json
from logging import warning
from typing import (
Dict,
Expand Down Expand Up @@ -66,6 +67,7 @@
)
from pytket.utils.operators import QubitPauliOperator
from pytket.utils.results import KwargTypes
from pytket.utils import prepare_circuit

from .ibm_utils import _STATUS_MAP, _batch_circuits
from .._metadata import __extension_version__
Expand Down Expand Up @@ -133,7 +135,7 @@ def required_predicates(self) -> List[Predicate]:

@property
def _result_id_type(self) -> _ResultIdTuple:
return (str, int)
return (str, int, int, str)

@property
def backend_info(self) -> BackendInfo:
Expand Down Expand Up @@ -243,6 +245,8 @@ def process_circuits(
See :py:meth:`pytket.backends.Backend.process_circuits`.
Supported kwargs: `seed`, `postprocess`.
"""
postprocess = kwargs.get("postprocess", False)

circuits = list(circuits)
n_shots_list = Backend._get_n_shots_as_list(
n_shots,
Expand All @@ -268,14 +272,22 @@ def process_circuits(
replace_implicit_swaps = self.supports_state or self.supports_unitary

for (n_shots, batch), indices in zip(circuit_batches, batch_order):
qcs = []
qcs, ppcirc_strs, tkc_qubits_count = [], [], []
for tkc in batch:
qc = tk_to_qiskit(tkc, replace_implicit_swaps)
if postprocess:
c0, ppcirc = prepare_circuit(tkc, allow_classical=False)
ppcirc_rep = ppcirc.to_dict()
else:
c0, ppcirc_rep = tkc, None

qc = tk_to_qiskit(c0, replace_implicit_swaps)
if self.supports_state:
qc.save_state()
elif self.supports_unitary:
qc.save_unitary()
qcs.append(qc)
tkc_qubits_count.append(c0.n_qubits)
ppcirc_strs.append(json.dumps(ppcirc_rep))

if self._needs_transpile:
qcs = transpile(qcs, self._qiskit_backend)
Expand All @@ -291,7 +303,7 @@ def process_circuits(
seed += 1
jobid = job.job_id()
for i, ind in enumerate(indices):
handle = ResultHandle(jobid, i)
handle = ResultHandle(jobid, i, tkc_qubits_count[i], ppcirc_strs[i])
handle_list[ind] = handle
self._cache[handle] = {"job": job}
return cast(List[ResultHandle], handle_list)
Expand All @@ -312,7 +324,7 @@ def get_result(self, handle: ResultHandle, **kwargs: KwargTypes) -> BackendResul
try:
return super().get_result(handle)
except CircuitNotRunError:
jobid, _ = handle
jobid, _, qubit_n, ppc = handle
try:
job: "AerJob" = self._cache[handle]["job"]
except KeyError:
Expand All @@ -321,7 +333,9 @@ def get_result(self, handle: ResultHandle, **kwargs: KwargTypes) -> BackendResul
res = job.result()
backresults = qiskit_result_to_backendresult(res)
for circ_index, backres in enumerate(backresults):
self._cache[ResultHandle(jobid, circ_index)]["result"] = backres
self._cache[ResultHandle(jobid, circ_index, qubit_n, ppc)][
"result"
] = backres

return cast(BackendResult, self._cache[handle]["result"])

Expand Down
3 changes: 1 addition & 2 deletions pytket/extensions/qiskit/backends/ibm.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,8 +355,7 @@ def default_compilation_pass(
This is a an abstract method which is implemented in the backend itself, and so
is tailored to the backend's requirements.

The default compilation passes for the :py:class:`IBMQBackend`,
:py:class:`IBMQEmulatorBackend` and the
The default compilation passes for the :py:class:`IBMQBackend` and the
Aer simulators support an optional ``placement_options`` dictionary containing
arguments to override the default settings in :py:class:`NoiseAwarePlacement`.

Expand Down
160 changes: 23 additions & 137 deletions pytket/extensions/qiskit/backends/ibmq_emulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,52 +12,42 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from ast import literal_eval
from collections import Counter
import itertools
import json
from typing import (
cast,
Dict,
Optional,
List,
Sequence,
Tuple,
Union,
)
from warnings import warn

from qiskit_aer.noise.noise_model import NoiseModel # type: ignore
from qiskit_ibm_runtime import ( # type: ignore
QiskitRuntimeService,
Session,
Options,
Sampler,
RuntimeJob,
)

from qiskit_ibm_provider import IBMProvider # type: ignore

from pytket.backends import Backend, CircuitNotRunError, ResultHandle, CircuitStatus
from pytket.backends import (
Backend,
ResultHandle,
CircuitStatus,
)
from pytket.backends.backendinfo import BackendInfo
from pytket.backends.backendresult import BackendResult
from pytket.backends.resulthandle import _ResultIdTuple
from pytket.circuit import Bit, Circuit, OpType
from pytket.extensions.qiskit.qiskit_convert import tk_to_qiskit
from pytket.circuit import Circuit
from pytket.passes import BasePass
from pytket.predicates import Predicate
from pytket.utils import prepare_circuit
from pytket.utils.outcomearray import OutcomeArray
from pytket.utils.results import KwargTypes

from .aer import AerBackend
from .ibm import IBMQBackend
from .ibm_utils import _STATUS_MAP, _batch_circuits


class IBMQEmulatorBackend(Backend):
"""A Backend which uses the ibmq_qasm_simulator to emulate the behaviour of
"""A backend which uses the AerBackend to loaclly emulate the behaviour of
IBMQBackend. Identical to :py:class:`IBMQBackend` except there is no `monitor`
parameter. Performs the same compilation and predicate checks as IBMQBackend.
Requires a valid IBMQ account.
Requires a valid IBM account.
"""

_supports_shots = False
Expand All @@ -81,14 +71,12 @@ def __init__(
token=token,
)

self._service = QiskitRuntimeService(
channel="ibm_quantum", instance=instance, token=token
)
self._session = Session(service=self._service, backend="ibmq_qasm_simulator")

# Get noise model:
self._noise_model = NoiseModel.from_backend(self._ibmq._backend)

# Construct AerBackend based on noise model:
self._aer = AerBackend(noise_model=self._noise_model)

# cache of results keyed by job id and circuit index
self._ibm_res_cache: Dict[Tuple[str, int], Counter] = dict()

Expand All @@ -112,8 +100,7 @@ def default_compilation_pass(

@property
def _result_id_type(self) -> _ResultIdTuple:
# job ID, index, stringified sequence of measured bits, post-processing circuit
return (str, int, str, str)
return self._aer._result_id_type

def rebase_pass(self) -> BasePass:
return self._ibmq.rebase_pass()
Expand All @@ -129,123 +116,22 @@ def process_circuits(
See :py:meth:`pytket.backends.Backend.process_circuits`.
Supported kwargs: `seed`, `postprocess`.
"""
circuits = list(circuits)
n_shots_list = Backend._get_n_shots_as_list(
n_shots,
len(circuits),
optional=False,
)

handle_list: List[Optional[ResultHandle]] = [None] * len(circuits)
seed = kwargs.get("seed")
circuit_batches, batch_order = _batch_circuits(circuits, n_shots_list)

batch_id = 0 # identify batches for debug purposes only
for (n_shots, batch), indices in zip(circuit_batches, batch_order):
for chunk in itertools.zip_longest(
*([iter(zip(batch, indices))] * self._ibmq._max_per_job)
):
filtchunk = list(filter(lambda x: x is not None, chunk))
batch_chunk, indices_chunk = zip(*filtchunk)

if valid_check:
self._check_all_circuits(batch_chunk)

postprocess = kwargs.get("postprocess", False)

qcs, c_bit_strs, ppcirc_strs = [], [], []
for tkc in batch_chunk:
if postprocess:
c0, ppcirc = prepare_circuit(tkc, allow_classical=False)
ppcirc_rep = ppcirc.to_dict()
else:
c0, ppcirc_rep = tkc, None
qcs.append(tk_to_qiskit(c0))
measured_bits = sorted(
[cmd.args[1] for cmd in tkc if cmd.op.type == OpType.Measure]
)
c_bit_strs.append(
repr([(b.reg_name, b.index) for b in measured_bits])
)
ppcirc_strs.append(json.dumps(ppcirc_rep))
options = Options()
options.resilience_level = 0
options.execution.shots = n_shots
options.simulator.noise_model = self._noise_model
options.seed_simulator = seed
if type(seed) is int:
seed += 1
sampler = Sampler(session=self._session, options=options)
job = sampler.run(circuits=qcs)
job_id = job.job_id()
for i, ind in enumerate(indices_chunk):
handle_list[ind] = ResultHandle(
job_id, i, c_bit_strs[i], ppcirc_strs[i]
)
batch_id += 1
for handle in handle_list:
assert handle is not None
self._cache[handle] = dict()
return cast(List[ResultHandle], handle_list)

def _retrieve_job(self, jobid: str) -> RuntimeJob:
return self._service.job(jobid)
if valid_check:
self._ibmq._check_all_circuits(circuits)
return self._aer.process_circuits(
circuits, n_shots=n_shots, valid_check=False, **kwargs
)

def cancel(self, handle: ResultHandle) -> None:
jobid = cast(str, handle[0])
job = self._retrieve_job(jobid)
try:
job.cancel()
except Exception as e:
warn(f"Unable to cancel job {jobid}: {e}")
self._aer.cancel(handle)

def circuit_status(self, handle: ResultHandle) -> CircuitStatus:
self._check_handle_type(handle)
jobid = cast(str, handle[0])
job = self._service.job(jobid)
ibmstatus = job.status()
return CircuitStatus(_STATUS_MAP[ibmstatus], ibmstatus.value)
return self._aer.circuit_status(handle)

def get_result(self, handle: ResultHandle, **kwargs: KwargTypes) -> BackendResult:
"""
See :py:meth:`pytket.backends.Backend.get_result`.
Supported kwargs: `timeout`, `wait`.
Supported kwargs: none.
"""
self._check_handle_type(handle)
if handle in self._cache:
cached_result = self._cache[handle]
if "result" in cached_result:
return cast(BackendResult, cached_result["result"])
jobid, index, c_bit_str, ppcirc_str = handle
c_bits = [Bit(reg_name, index) for reg_name, index in literal_eval(c_bit_str)]
ppcirc_rep = json.loads(ppcirc_str)
ppcirc = Circuit.from_dict(ppcirc_rep) if ppcirc_rep is not None else None
cache_key = (jobid, index)
if cache_key not in self._ibm_res_cache:
try:
job = self._retrieve_job(jobid)
except Exception as e:
warn(f"Unable to retrieve job {jobid}: {e}")
raise CircuitNotRunError(handle)

res = job.result(timeout=kwargs.get("timeout", None))
for circ_index, (r, d) in enumerate(zip(res.quasi_dists, res.metadata)):
self._ibm_res_cache[(jobid, circ_index)] = Counter(
{n: int(0.5 + d["shots"] * p) for n, p in r.items()}
)

counts = self._ibm_res_cache[cache_key] # Counter[int]
# Convert to `OutcomeArray`:
tket_counts: Counter = Counter()
for outcome_key, sample_count in counts.items():
array = OutcomeArray.from_ints(
ints=[outcome_key],
width=len(c_bits),
big_endian=False,
)
tket_counts[array] = sample_count
# Convert to `BackendResult`:
result = BackendResult(c_bits=c_bits, counts=tket_counts, ppcirc=ppcirc)

self._cache[handle] = {"result": result}
return result
return self._aer.get_result(handle)
Loading