Coverage for / home / runner / work / tket / tket / pytket / pytket / backends / backend.py: 83%
182 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-24 15:39 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-24 15:39 +0000
1# Copyright Quantinuum
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
15"""Abstract base class for all Backend encapsulations."""
17import warnings
18from abc import ABC, abstractmethod
19from collections.abc import Iterable, Sequence
20from importlib import import_module
21from types import ModuleType
22from typing import Any, Literal, cast, overload
24from pytket.circuit import Bit, Circuit, OpType
25from pytket.passes import BasePass
26from pytket.pauli import QubitPauliString
27from pytket.predicates import Predicate
28from pytket.utils import QubitPauliOperator
29from pytket.utils.outcomearray import OutcomeArray
30from pytket.utils.results import KwargTypes
32from .backend_exceptions import (
33 CircuitNotRunError,
34 CircuitNotValidError,
35)
36from .backendinfo import BackendInfo
37from .backendresult import BackendResult
38from .resulthandle import ResultHandle, _ResultIdTuple
39from .status import CircuitStatus
42class ResultHandleTypeError(Exception):
43 """Wrong result handle type."""
46class Backend(ABC):
47 """
48 This abstract class defines the structure of a backend as something that
49 can run quantum circuits and produce output as at least one of shots,
50 counts, state, or unitary
51 """
53 _supports_shots = False
54 _supports_counts = False
55 _supports_state = False
56 _supports_unitary = False
57 _supports_density_matrix = False
58 _supports_expectation = False
59 _expectation_allows_nonhermitian = False
60 _supports_contextual_optimisation = False
61 _persistent_handles = False
63 def __init__(self) -> None:
64 self._cache: dict[ResultHandle, dict[str, Any]] = {}
66 @staticmethod
67 def empty_result(circuit: Circuit, n_shots: int) -> BackendResult:
68 """
69 Creates a :py:class:`~pytket.backends.backendresult.BackendResult` mimicking the outcome where every
70 bit is 0 for every shot.
71 """
72 n_bits = len(circuit.bits)
73 empty_readouts = [[0] * n_bits for _ in range(n_shots)]
74 shots = OutcomeArray.from_readouts(empty_readouts)
75 c_bits = [Bit(index) for index in range(n_bits)]
76 return BackendResult(shots=shots, c_bits=c_bits)
78 @property
79 @abstractmethod
80 def required_predicates(self) -> list[Predicate]:
81 """
82 The minimum set of predicates that a circuit must satisfy before it can
83 be successfully run on this backend.
85 :return: Required predicates.
86 """
87 ...
89 def valid_circuit(self, circuit: Circuit) -> bool:
90 """
91 Checks that the circuit satisfies all of required_predicates.
93 :param circuit: The circuit to check.
94 :return: Whether or not all of required_predicates are satisfied.
95 """
96 return all(pred.verify(circuit) for pred in self.required_predicates)
98 @property
99 def _uses_lightsabre(self) -> bool:
100 # lightsaber is a routing method used by pytket-qiskit
101 return False
103 def _check_all_circuits(
104 self, circuits: Iterable[Circuit], nomeasure_warn: bool | None = None
105 ) -> bool:
106 if nomeasure_warn is None: 106 ↛ 113line 106 didn't jump to line 113 because the condition on line 106 was always true
107 nomeasure_warn = not (
108 self._supports_state
109 or self._supports_unitary
110 or self._supports_density_matrix
111 or self._supports_expectation
112 )
113 for i, circ in enumerate(circuits):
114 errors = (
115 CircuitNotValidError(i, repr(pred))
116 for pred in self.required_predicates
117 if not pred.verify(circ)
118 )
119 for error in errors: 119 ↛ 120line 119 didn't jump to line 120 because the loop on line 119 never started
120 raise error
121 if nomeasure_warn and circ.n_gates_of_type(OpType.Measure) < 1: 121 ↛ 122line 121 didn't jump to line 122 because the condition on line 121 was never true
122 warnings.warn( # noqa: B028
123 f"Circuit with index {i} in submitted does not contain a "
124 "measure operation."
125 )
126 return True
128 @abstractmethod
129 def rebase_pass(self) -> BasePass:
130 """
131 A single compilation pass that when run converts all gates in a Circuit to
132 an OpType supported by the Backend (ignoring architecture constraints).
134 :return: Compilation pass that converts gates to primitives supported by
135 Backend.
136 """
137 ...
139 @abstractmethod
140 def default_compilation_pass(self, optimisation_level: int = 2) -> BasePass:
141 """
142 A suggested compilation pass that will will, if possible, produce an equivalent
143 circuit suitable for running on this backend.
145 At a minimum it will ensure that compatible gates are used and that all two-
146 qubit interactions are compatible with the backend's qubit architecture. At
147 higher optimisation levels, further optimisations may be applied.
149 This is a an abstract method which is implemented in the backend itself, and so
150 is tailored to the backend's requirements.
152 :param optimisation_level: The level of optimisation to perform during
153 compilation.
155 - Level 0 does the minimum required to solves the device constraints,
156 without any optimisation.
157 - Level 1 additionally performs some light optimisations.
158 - Level 2 (the default) adds more computationally intensive optimisations
159 that should give the best results from execution.
161 :return: Compilation pass guaranteeing required predicates.
162 """
163 ...
165 def get_compiled_circuit(
166 self, circuit: Circuit, optimisation_level: int = 2
167 ) -> Circuit:
168 """
169 Return a single circuit compiled with :py:meth:`default_compilation_pass`. See
170 :py:meth:`~pytket.backends.backend.Backend.get_compiled_circuits`.
171 """
172 return_circuit = circuit.copy()
173 self.default_compilation_pass(optimisation_level).apply(return_circuit)
174 return return_circuit
176 def get_compiled_circuits(
177 self, circuits: Sequence[Circuit], optimisation_level: int = 2
178 ) -> list[Circuit]:
179 """Compile a sequence of circuits with :py:meth:`default_compilation_pass`
180 and return the list of compiled circuits (does not act in place).
182 As well as applying a degree of optimisation (controlled by the
183 `optimisation_level` parameter), this method tries to ensure that the circuits
184 can be run on the backend (i.e. successfully passed to
185 :py:meth:`process_circuits`), for example by rebasing to the supported gate set,
186 or routing to match the connectivity of the device. However, this is not always
187 possible, for example if the circuit contains classical operations that are not
188 supported by the backend. You may use :py:meth:`valid_circuit` to check whether
189 the circuit meets the backend's requirements after compilation. This validity
190 check is included in :py:meth:`process_circuits` by default, before any circuits
191 are submitted to the backend.
193 If the validity check fails, you can obtain more information about the failure
194 by iterating through the predicates in the `required_predicates` property of the
195 backend, and running the :py:meth:`~pytket.predicates.Predicate.verify` method on each in turn with your
196 circuit.
198 :param circuits: The circuits to compile.
199 :param optimisation_level: The level of optimisation to perform during
200 compilation. See :py:meth:`default_compilation_pass` for a description of
201 the different levels (0, 1 or 2). Defaults to 2.
202 :return: Compiled circuits.
203 """
204 return [self.get_compiled_circuit(c, optimisation_level) for c in circuits]
206 @property
207 @abstractmethod
208 def _result_id_type(self) -> _ResultIdTuple:
209 """Identifier type signature for ResultHandle for this backend.
211 :return: Type signature (tuple of hashable types)
212 """
213 ...
215 def _check_handle_type(self, reshandle: ResultHandle) -> None:
216 """Check a result handle is valid for this backend, raises TypeError if not.
218 :param reshandle: Handle to check
219 :raises TypeError: Types of handle identifiers don't match those of backend.
220 """
221 if (len(reshandle) != len(self._result_id_type)) or not all(
222 isinstance(idval, ty)
223 for idval, ty in zip(reshandle, self._result_id_type, strict=False)
224 ):
225 raise ResultHandleTypeError(
226 f"{reshandle!r} does not match expected "
227 f"identifier types {self._result_id_type}"
228 )
230 def process_circuit(
231 self,
232 circuit: Circuit,
233 n_shots: int | None = None,
234 valid_check: bool = True,
235 **kwargs: KwargTypes,
236 ) -> ResultHandle:
237 """
238 Submit a single circuit to the backend for running. See
239 :py:meth:`~pytket.backends.backend.Backend.process_circuits`.
240 """
242 return self.process_circuits(
243 [circuit], n_shots=n_shots, valid_check=valid_check, **kwargs
244 )[0]
246 @abstractmethod
247 def process_circuits(
248 self,
249 circuits: Sequence[Circuit],
250 n_shots: int | Sequence[int] | None = None,
251 valid_check: bool = True,
252 **kwargs: KwargTypes,
253 ) -> list[ResultHandle]:
254 """
255 Submit circuits to the backend for running. The results will be stored
256 in the backend's result cache to be retrieved by the corresponding
257 get_<data> method.
259 If the `postprocess` keyword argument is set to True, and the backend supports
260 the feature (see :py:attr:`~pytket.backends.backend.Backend.supports_contextual_optimisation`), then contextual
261 optimisatioons are applied before running the circuit and retrieved results will
262 have any necessary classical postprocessing applied. This is not enabled by
263 default.
265 Use keyword arguments to specify parameters to be used in submitting circuits
266 See specific Backend derived class for available parameters, from the following
267 list:
269 * `seed`: RNG seed for simulators
270 * `postprocess`: if True, apply contextual optimisations
272 Note: If a backend is reused many times, the in-memory results cache grows
273 indefinitely. Therefore, when processing many circuits on a statevector or
274 unitary backend (whose results may occupy significant amounts of memory), it is
275 advisable to run :py:meth:`~pytket.backends.backend.Backend.empty_cache` after each result is retrieved.
277 :param circuits: Circuits to process on the backend.
278 :param n_shots: Number of shots to run per circuit. Optionally, this can be
279 a list of shots specifying the number of shots for each circuit separately.
280 None is to be used for state/unitary simulators. Defaults to None.
281 :param valid_check: Explicitly check that all circuits satisfy all required
282 predicates to run on the backend. Defaults to True
283 :return: Handles to results for each input circuit, as an iterable in
284 the same order as the circuits.
285 """
286 ...
288 @abstractmethod
289 def circuit_status(self, handle: ResultHandle) -> CircuitStatus:
290 """
291 Return a CircuitStatus reporting the status of the circuit execution
292 corresponding to the ResultHandle
293 """
294 ...
296 def empty_cache(self) -> None:
297 """Manually empty the result cache on the backend."""
298 self._cache = {}
300 def pop_result(self, handle: ResultHandle) -> dict[str, Any] | None:
301 """Remove cache entry corresponding to handle from the cache and return.
303 :param handle: ResultHandle object
304 :return: Cache entry corresponding to handle, if it was present
305 """
306 return self._cache.pop(handle, None)
308 def get_result(self, handle: ResultHandle, **kwargs: KwargTypes) -> BackendResult:
309 """Return a BackendResult corresponding to the handle.
311 Use keyword arguments to specify parameters to be used in retrieving results.
312 See specific Backend derived class for available parameters, from the following
313 list:
315 * `timeout`: maximum time to wait for remote job to finish
316 * `wait`: polling interval between remote calls to check job status
318 :param handle: handle to results
319 :return: Results corresponding to handle.
320 """
321 self._check_handle_type(handle)
322 if handle in self._cache and "result" in self._cache[handle]:
323 return cast("BackendResult", self._cache[handle]["result"])
324 raise CircuitNotRunError(handle)
326 def get_results(
327 self, handles: Iterable[ResultHandle], **kwargs: KwargTypes
328 ) -> list[BackendResult]:
329 """Return results corresponding to handles.
331 :param handles: Iterable of handles
332 :return: List of results
334 Keyword arguments are as for :py:meth:`~pytket.backends.backend.Backend.get_result`, and apply to all jobs.
335 """
336 try:
337 return [self.get_result(handle, **kwargs) for handle in handles]
338 except ResultHandleTypeError as e:
339 try:
340 self._check_handle_type(cast("ResultHandle", handles))
341 except ResultHandleTypeError:
342 raise e # noqa: B904
344 raise ResultHandleTypeError(
345 "Possible use of single ResultHandle"
346 " where sequence of ResultHandles was expected."
347 ) from e
349 def run_circuit(
350 self,
351 circuit: Circuit,
352 n_shots: int | None = None,
353 valid_check: bool = True,
354 **kwargs: KwargTypes,
355 ) -> BackendResult:
356 """
357 Submits a circuit to the backend and returns results
359 :param circuit: Circuit to be executed
360 :param n_shots: Passed on to :py:meth:`~pytket.backends.backend.Backend.process_circuit`
361 :param valid_check: Passed on to :py:meth:`~pytket.backends.backend.Backend.process_circuit`
362 :return: Result
364 This is a convenience method equivalent to calling
365 :py:meth:`~pytket.backends.backend.Backend.process_circuit` followed by :py:meth:`~pytket.backends.backend.Backend.get_result`.
366 Any additional keyword arguments are passed on to
367 :py:meth:`~pytket.backends.backend.Backend.process_circuit` and :py:meth:`~pytket.backends.backend.Backend.get_result`.
368 """
369 return self.run_circuits(
370 [circuit], n_shots=n_shots, valid_check=valid_check, **kwargs
371 )[0]
373 def run_circuits(
374 self,
375 circuits: Sequence[Circuit],
376 n_shots: int | Sequence[int] | None = None,
377 valid_check: bool = True,
378 **kwargs: KwargTypes,
379 ) -> list[BackendResult]:
380 """
381 Submits circuits to the backend and returns results
383 :param circuits: Sequence of Circuits to be executed
384 :param n_shots: Passed on to :py:meth:`~pytket.backends.backend.Backend.process_circuits`
385 :param valid_check: Passed on to :py:meth:`~pytket.backends.backend.Backend.process_circuits`
386 :return: List of results
388 This is a convenience method equivalent to calling
389 :py:meth:`~pytket.backends.backend.Backend.process_circuits` followed by :py:meth:`~pytket.backends.backend.Backend.get_results`.
390 Any additional keyword arguments are passed on to
391 :py:meth:`~pytket.backends.backend.Backend.process_circuits` and :py:meth:`~pytket.backends.backend.Backend.get_results`.
392 """
393 handles = self.process_circuits(circuits, n_shots, valid_check, **kwargs)
394 results = self.get_results(handles, **kwargs)
395 for h in handles:
396 self.pop_result(h)
397 return results
399 def cancel(self, handle: ResultHandle) -> None:
400 """
401 Cancel a job.
403 :param handle: handle to job
404 :raises NotImplementedError: If backend does not support job cancellation
405 """
406 raise NotImplementedError("Backend does not support job cancellation.")
408 @property
409 def backend_info(self) -> BackendInfo | None:
410 """Retrieve all Backend properties in a BackendInfo object, including
411 device architecture, supported gate set, gate errors and other hardware-specific
412 information.
414 :return: The BackendInfo describing this backend if it exists.
415 """
416 raise NotImplementedError("Backend does not provide any device properties.")
418 @classmethod
419 def available_devices(cls, **kwargs: Any) -> list[BackendInfo]:
420 """Retrieve all available devices as a list of BackendInfo objects, including
421 device name, architecture, supported gate set, gate errors,
422 and other hardware-specific information.
424 :return: A list of BackendInfo objects describing available devices.
425 """
426 raise NotImplementedError(
427 "Backend does not provide information about available devices."
428 )
430 @property
431 def persistent_handles(self) -> bool:
432 """
433 Whether the backend produces `ResultHandle` objects that can be reused with
434 other instances of the backend class.
435 """
436 return self._persistent_handles
438 @property
439 def supports_shots(self) -> bool:
440 """
441 Does this backend support shot result retrieval via
442 :py:meth:`~pytket.backends.backendresult.BackendResult.get_shots`.
443 """
444 return self._supports_shots
446 @property
447 def supports_counts(self) -> bool:
448 """
449 Does this backend support counts result retrieval via
450 :py:meth:`~pytket.backends.backendresult.BackendResult.get_counts`.
451 """
452 return self._supports_counts
454 @property
455 def supports_state(self) -> bool:
456 """
457 Does this backend support statevector retrieval via
458 :py:meth:`~pytket.backends.backendresult.BackendResult.get_state`.
459 """
460 return self._supports_state
462 @property
463 def supports_unitary(self) -> bool:
464 """
465 Does this backend support unitary retrieval via
466 :py:meth:`~pytket.backends.backendresult.BackendResult.get_unitary`.
467 """
468 return self._supports_unitary
470 @property
471 def supports_density_matrix(self) -> bool:
472 """Does this backend support density matrix retrieval via
473 :py:meth:`~pytket.backends.backendresult.BackendResult.get_density_matrix`."""
474 return self._supports_density_matrix
476 @property
477 def supports_expectation(self) -> bool:
478 """Does this backend support expectation value calculation for operators."""
479 return self._supports_expectation
481 @property
482 def expectation_allows_nonhermitian(self) -> bool:
483 """If expectations are supported, is the operator allowed to be non-Hermitan?"""
484 return self._expectation_allows_nonhermitian
486 @property
487 def supports_contextual_optimisation(self) -> bool:
488 """Does this backend support contextual optimisation?
490 See :py:meth:`process_circuits`."""
491 return self._supports_contextual_optimisation
493 def _get_extension_module(self) -> ModuleType | None:
494 """Return the extension module of the backend if it belongs to a
495 pytket-extension package.
497 :return: The extension module of the backend if it belongs to a pytket-extension
498 package.
499 """
500 mod_parts = self.__class__.__module__.split(".")[:3]
501 if not (mod_parts[0] == "pytket" and mod_parts[1] == "extensions"):
502 return None
503 return import_module(".".join(mod_parts))
505 @property
506 def __extension_name__(self) -> str | None:
507 """Retrieve the extension name of the backend if it belongs to a
508 pytket-extension package.
510 :return: The extension name of the backend if it belongs to a pytket-extension
511 package.
512 """
513 try:
514 return self._get_extension_module().__extension_name__ # type: ignore
515 except AttributeError:
516 return None
518 @property
519 def __extension_version__(self) -> str | None:
520 """Retrieve the extension version of the backend if it belongs to a
521 pytket-extension package.
523 :return: The extension version of the backend if it belongs to a
524 pytket-extension package.
525 """
526 try:
527 return self._get_extension_module().__extension_version__ # type: ignore
528 except AttributeError:
529 return None
531 @overload
532 @staticmethod
533 def _get_n_shots_as_list(
534 n_shots: None | int | Sequence[int | None],
535 n_circuits: int,
536 optional: Literal[False],
537 ) -> list[int]: ...
539 @overload
540 @staticmethod
541 def _get_n_shots_as_list(
542 n_shots: None | int | Sequence[int | None],
543 n_circuits: int,
544 optional: Literal[True],
545 set_zero: Literal[True],
546 ) -> list[int]: ...
548 @overload
549 @staticmethod
550 def _get_n_shots_as_list(
551 n_shots: None | int | Sequence[int | None],
552 n_circuits: int,
553 optional: bool = True,
554 set_zero: bool = False,
555 ) -> list[int | None] | list[int]: ...
557 @staticmethod
558 def _get_n_shots_as_list(
559 n_shots: None | int | Sequence[int | None],
560 n_circuits: int,
561 optional: bool = True,
562 set_zero: bool = False,
563 ) -> list[int | None] | list[int]:
564 """
565 Convert any admissible n_shots value into List[Optional[int]] format.
567 This validates the n_shots argument for process_circuits. If a single
568 value is passed, this value is broadcast to the number of circuits.
569 Additional boolean flags control how the argument is validated.
570 Raises an exception if n_shots is in an invalid format.
572 :param n_shots: The argument to be validated.
573 :param n_circuits: Length of the converted argument returned.
574 :param optional: Whether n_shots can be None (default: True).
575 :param set_zero: Whether None values should be set to 0 (default: False).
576 :return: a list of length `n_circuits`, the converted argument
577 """
579 n_shots_list: list[int | None] = []
581 def validate_n_shots(n: int | None) -> bool:
582 return optional or (n is not None and n > 0)
584 if set_zero and not optional: 584 ↛ 585line 584 didn't jump to line 585 because the condition on line 584 was never true
585 raise ValueError("set_zero cannot be true when optional is false")
587 if hasattr(n_shots, "__iter__"):
588 assert not isinstance(n_shots, int)
589 assert n_shots is not None
591 if not all(map(validate_n_shots, n_shots)):
592 raise ValueError(
593 "n_shots values are required for all circuits for this backend"
594 )
595 n_shots_list = list(n_shots)
596 else:
597 assert n_shots is None or isinstance(n_shots, int)
599 if not validate_n_shots(n_shots):
600 raise ValueError("Parameter n_shots is required for this backend")
601 # convert n_shots to a list
602 n_shots_list = [n_shots] * n_circuits
604 if len(n_shots_list) != n_circuits:
605 raise ValueError("The length of n_shots and circuits must match")
607 if set_zero:
608 # replace None with 0
609 n_shots_list = [n or 0 for n in n_shots_list]
611 return n_shots_list
613 def get_pauli_expectation_value(
614 self, state_circuit: Circuit, pauli: QubitPauliString
615 ) -> complex:
616 """
617 Calculates the expectation value of the given circuit using
618 functionality built into the backend.
620 Raises an exception if the backend does not provide custom expectation
621 value features.
622 """
623 raise NotImplementedError
625 def get_operator_expectation_value(
626 self, state_circuit: Circuit, operator: QubitPauliOperator
627 ) -> complex:
628 """
629 Calculates the expectation value of the given circuit with respect to
630 the operator using functionality built into the backend.
632 Raises an exception if the backend does not provide custom expectation
633 value features.
634 """
635 raise NotImplementedError