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

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. 

14 

15"""Abstract base class for all Backend encapsulations.""" 

16 

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 

23 

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 

31 

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 

40 

41 

42class ResultHandleTypeError(Exception): 

43 """Wrong result handle type.""" 

44 

45 

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 """ 

52 

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 

62 

63 def __init__(self) -> None: 

64 self._cache: dict[ResultHandle, dict[str, Any]] = {} 

65 

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) 

77 

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. 

84 

85 :return: Required predicates. 

86 """ 

87 ... 

88 

89 def valid_circuit(self, circuit: Circuit) -> bool: 

90 """ 

91 Checks that the circuit satisfies all of required_predicates. 

92 

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) 

97 

98 @property 

99 def _uses_lightsabre(self) -> bool: 

100 # lightsaber is a routing method used by pytket-qiskit 

101 return False 

102 

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 

127 

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). 

133 

134 :return: Compilation pass that converts gates to primitives supported by 

135 Backend. 

136 """ 

137 ... 

138 

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. 

144 

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. 

148 

149 This is a an abstract method which is implemented in the backend itself, and so 

150 is tailored to the backend's requirements. 

151 

152 :param optimisation_level: The level of optimisation to perform during 

153 compilation. 

154 

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. 

160 

161 :return: Compilation pass guaranteeing required predicates. 

162 """ 

163 ... 

164 

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 

175 

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). 

181 

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. 

192 

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. 

197 

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] 

205 

206 @property 

207 @abstractmethod 

208 def _result_id_type(self) -> _ResultIdTuple: 

209 """Identifier type signature for ResultHandle for this backend. 

210 

211 :return: Type signature (tuple of hashable types) 

212 """ 

213 ... 

214 

215 def _check_handle_type(self, reshandle: ResultHandle) -> None: 

216 """Check a result handle is valid for this backend, raises TypeError if not. 

217 

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 ) 

229 

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 """ 

241 

242 return self.process_circuits( 

243 [circuit], n_shots=n_shots, valid_check=valid_check, **kwargs 

244 )[0] 

245 

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. 

258 

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. 

264 

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: 

268 

269 * `seed`: RNG seed for simulators 

270 * `postprocess`: if True, apply contextual optimisations 

271 

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. 

276 

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 ... 

287 

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 ... 

295 

296 def empty_cache(self) -> None: 

297 """Manually empty the result cache on the backend.""" 

298 self._cache = {} 

299 

300 def pop_result(self, handle: ResultHandle) -> dict[str, Any] | None: 

301 """Remove cache entry corresponding to handle from the cache and return. 

302 

303 :param handle: ResultHandle object 

304 :return: Cache entry corresponding to handle, if it was present 

305 """ 

306 return self._cache.pop(handle, None) 

307 

308 def get_result(self, handle: ResultHandle, **kwargs: KwargTypes) -> BackendResult: 

309 """Return a BackendResult corresponding to the handle. 

310 

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: 

314 

315 * `timeout`: maximum time to wait for remote job to finish 

316 * `wait`: polling interval between remote calls to check job status 

317 

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) 

325 

326 def get_results( 

327 self, handles: Iterable[ResultHandle], **kwargs: KwargTypes 

328 ) -> list[BackendResult]: 

329 """Return results corresponding to handles. 

330 

331 :param handles: Iterable of handles 

332 :return: List of results 

333 

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 

343 

344 raise ResultHandleTypeError( 

345 "Possible use of single ResultHandle" 

346 " where sequence of ResultHandles was expected." 

347 ) from e 

348 

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 

358 

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 

363 

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] 

372 

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 

382 

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 

387 

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 

398 

399 def cancel(self, handle: ResultHandle) -> None: 

400 """ 

401 Cancel a job. 

402 

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.") 

407 

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. 

413 

414 :return: The BackendInfo describing this backend if it exists. 

415 """ 

416 raise NotImplementedError("Backend does not provide any device properties.") 

417 

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. 

423 

424 :return: A list of BackendInfo objects describing available devices. 

425 """ 

426 raise NotImplementedError( 

427 "Backend does not provide information about available devices." 

428 ) 

429 

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 

437 

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 

445 

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 

453 

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 

461 

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 

469 

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 

475 

476 @property 

477 def supports_expectation(self) -> bool: 

478 """Does this backend support expectation value calculation for operators.""" 

479 return self._supports_expectation 

480 

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 

485 

486 @property 

487 def supports_contextual_optimisation(self) -> bool: 

488 """Does this backend support contextual optimisation? 

489 

490 See :py:meth:`process_circuits`.""" 

491 return self._supports_contextual_optimisation 

492 

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. 

496 

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)) 

504 

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. 

509 

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 

517 

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. 

522 

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 

530 

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]: ... 

538 

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]: ... 

547 

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]: ... 

556 

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. 

566 

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. 

571 

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 """ 

578 

579 n_shots_list: list[int | None] = [] 

580 

581 def validate_n_shots(n: int | None) -> bool: 

582 return optional or (n is not None and n > 0) 

583 

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") 

586 

587 if hasattr(n_shots, "__iter__"): 

588 assert not isinstance(n_shots, int) 

589 assert n_shots is not None 

590 

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) 

598 

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 

603 

604 if len(n_shots_list) != n_circuits: 

605 raise ValueError("The length of n_shots and circuits must match") 

606 

607 if set_zero: 

608 # replace None with 0 

609 n_shots_list = [n or 0 for n in n_shots_list] 

610 

611 return n_shots_list 

612 

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. 

619 

620 Raises an exception if the backend does not provide custom expectation 

621 value features. 

622 """ 

623 raise NotImplementedError 

624 

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. 

631 

632 Raises an exception if the backend does not provide custom expectation 

633 value features. 

634 """ 

635 raise NotImplementedError