Coverage for /home/runner/work/tket/tket/pytket/pytket/backends/backendresult.py: 90%

278 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-05-09 15:08 +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"""`BackendResult` class and associated methods.""" 

16 

17import operator 

18import warnings 

19from collections import Counter 

20from collections.abc import Collection, Iterable, Sequence 

21from functools import reduce 

22from typing import Any, NamedTuple, TypeVar, cast 

23 

24import numpy as np 

25 

26from pytket.circuit import ( 

27 _DEBUG_ONE_REG_PREFIX, 

28 _DEBUG_ZERO_REG_PREFIX, 

29 BasisOrder, 

30 Bit, 

31 Circuit, 

32 Qubit, 

33 UnitID, 

34) 

35from pytket.unit_id import UnitType 

36from pytket.utils.distribution import EmpiricalDistribution, ProbabilityDistribution 

37from pytket.utils.outcomearray import OutcomeArray, readout_counts 

38from pytket.utils.results import ( 

39 get_n_qb_from_statevector, 

40 permute_basis_indexing, 

41 permute_rows_cols_in_unitary, 

42 probs_from_state, 

43) 

44 

45from .backend_exceptions import InvalidResultType 

46 

47 

48class StoredResult(NamedTuple): 

49 """NamedTuple with optional fields for all result types.""" 

50 

51 counts: Counter[OutcomeArray] | None = None 

52 shots: OutcomeArray | None = None 

53 state: np.ndarray | None = None 

54 unitary: np.ndarray | None = None 

55 density_matrix: np.ndarray | None = None 

56 

57 

58class BackendResult: 

59 """Encapsulate generic results from pytket Backend instances. 

60 

61 In the case of a real quantum device or a shots-based simulator 

62 a BackendResult will typically be a collection of measurements (shots and counts). 

63 

64 Results can also be the output of ideal simulations of circuits. 

65 These can take the form of statevectors, unitary arrays or density matrices. 

66 

67 :param q_bits: Sequence of qubits. 

68 :param c_bits: Sequence of classical bits. 

69 :param counts: The counts in the result. 

70 :param shots: The shots in the result. 

71 :param state: The resulting statevector (from a statevector simulation). 

72 :param unitary: The resulting unitary operator (from a unitary simulation). 

73 :param density_matrix: The resulting density matrix 

74 (from a density-matrix simulator). 

75 :param ppcirc: If provided, classical postprocessing to be applied to all measured 

76 results (i.e. shots and counts). 

77 """ 

78 

79 def __init__( # noqa: PLR0913 

80 self, 

81 *, 

82 q_bits: Sequence[Qubit] | None = None, 

83 c_bits: Sequence[Bit] | None = None, 

84 counts: Counter[OutcomeArray] | None = None, 

85 shots: OutcomeArray | None = None, 

86 state: Any = None, 

87 unitary: Any = None, 

88 density_matrix: Any = None, 

89 ppcirc: Circuit | None = None, 

90 ): 

91 # deal with mutable defaults 

92 if q_bits is None: 

93 q_bits = [] 

94 if c_bits is None: 

95 c_bits = [] 

96 

97 self._counts = counts 

98 self._shots = shots 

99 

100 self._state = state 

101 self._unitary = unitary 

102 self._density_matrix = density_matrix 

103 

104 self._ppcirc = ppcirc 

105 

106 self.c_bits: dict[Bit, int] = {} 

107 self.q_bits: dict[Qubit, int] = {} 

108 

109 def _process_unitids( 

110 var: Sequence[UnitID], attr: str, lent: int, uid: type[UnitID] 

111 ) -> None: 

112 if var: 

113 setattr(self, attr, {unit: i for i, unit in enumerate(var)}) 

114 if lent != len(var): 

115 raise ValueError( 

116 f"Length of {attr} ({len(var)}) does not" 

117 f" match input data dimensions ({lent})." 

118 ) 

119 else: 

120 setattr(self, attr, {uid(i): i for i in range(lent)}) # type: ignore 

121 

122 if self.contains_measured_results: 

123 _bitlength = 0 

124 if self._counts is not None: 

125 if shots is not None: 

126 raise ValueError( 

127 "Provide either counts or shots, both is not valid." 

128 ) 

129 try: 

130 _bitlength = next(self._counts.elements()).width 

131 except StopIteration: 

132 _bitlength = len(c_bits) 

133 

134 if self._shots is not None: 

135 _bitlength = self._shots.width 

136 

137 _process_unitids(c_bits, "c_bits", _bitlength, Bit) 

138 

139 if self.contains_state_results: 

140 _n_qubits = 0 

141 if self._unitary is not None: 

142 _n_qubits = int(np.log2(self._unitary.shape[-1])) 

143 elif self._state is not None: 

144 _n_qubits = get_n_qb_from_statevector(self._state) 

145 elif self._density_matrix is not None: 145 ↛ 148line 145 didn't jump to line 148 because the condition on line 145 was always true

146 _n_qubits = int(np.log2(self._density_matrix.shape[-1])) 

147 

148 _process_unitids(q_bits, "q_bits", _n_qubits, Qubit) 

149 

150 def __repr__(self) -> str: 

151 return ( 

152 f"BackendResult(q_bits={self.q_bits},c_bits={self.c_bits}," 

153 f"counts={self._counts},shots={self._shots},state={self._state}," 

154 f"unitary={self._unitary},density_matrix={self._density_matrix})" 

155 ) 

156 

157 @property 

158 def contains_measured_results(self) -> bool: 

159 """Whether measured type results (shots or counts) are stored""" 

160 return (self._counts is not None) or (self._shots is not None) 

161 

162 @property 

163 def contains_state_results(self) -> bool: 

164 """Whether state type results (state vector or unitary or density_matrix) 

165 are stored""" 

166 return ( 

167 (self._state is not None) 

168 or (self._unitary is not None) 

169 or (self._density_matrix is not None) 

170 ) 

171 

172 def __eq__(self, other: object) -> bool: 

173 if not isinstance(other, BackendResult): 173 ↛ 174line 173 didn't jump to line 174 because the condition on line 173 was never true

174 return NotImplemented 

175 return ( 

176 self.q_bits == other.q_bits 

177 and self.c_bits == other.c_bits 

178 and ( 

179 (self._shots is None and other._shots is None) 

180 or cast("OutcomeArray", self._shots) 

181 == cast("OutcomeArray", other._shots) 

182 ) 

183 and self._counts == other._counts 

184 and np.array_equal(self._state, other._state) 

185 and np.array_equal(self._unitary, other._unitary) 

186 and np.array_equal(self._density_matrix, other._density_matrix) 

187 ) 

188 

189 def get_bitlist(self) -> list[Bit]: 

190 """Return list of Bits in internal storage order. 

191 

192 :raises AttributeError: BackendResult does not include a Bits list. 

193 :return: Sorted list of Bits. 

194 :rtype: List[Bit] 

195 """ 

196 return _sort_keys_by_val(self.c_bits) 

197 

198 def get_qbitlist(self) -> list[Qubit]: 

199 """Return list of Qubits in internal storage order. 

200 

201 :raises AttributeError: BackendResult does not include a Qubits list. 

202 :return: Sorted list of Qubits. 

203 :rtype: List[Qubit] 

204 """ 

205 

206 return _sort_keys_by_val(self.q_bits) 

207 

208 def _get_measured_res( # noqa: PLR0912 

209 self, bits: Sequence[Bit], ppcirc: Circuit | None = None 

210 ) -> StoredResult: 

211 vals: dict[str, Any] = {} 

212 

213 if not self.contains_measured_results: 

214 raise InvalidResultType("shots/counts") 

215 

216 if self._ppcirc is not None: 

217 if ppcirc is None: 217 ↛ 220line 217 didn't jump to line 220 because the condition on line 217 was always true

218 ppcirc = self._ppcirc 

219 else: 

220 raise ValueError("Postprocessing circuit already provided.") 

221 

222 try: 

223 chosen_readouts = [self.c_bits[bit] for bit in bits] 

224 except KeyError: 

225 raise ValueError("Requested Bit not in result.") # noqa: B904 

226 

227 if self._counts is not None: 

228 if ppcirc is not None: 

229 # Modify self._counts: 

230 new_counts: Counter[OutcomeArray] = Counter() 

231 for oa, n in self._counts.items(): 

232 readout = oa.to_readout() 

233 values = {bit: bool(readout[i]) for bit, i in self.c_bits.items()} 

234 new_values = ppcirc._classical_eval(values) # noqa: SLF001 

235 new_oa = OutcomeArray.from_readouts( 

236 [[int(new_values[bit]) for bit in bits]] 

237 ) 

238 new_counts[new_oa] += n 

239 else: 

240 new_counts = self._counts 

241 vals["counts"] = reduce( 

242 operator.add, 

243 ( 

244 Counter({outcome.choose_indices(chosen_readouts): count}) 

245 for outcome, count in new_counts.items() 

246 ), 

247 Counter(), 

248 ) 

249 if self._shots is not None: 

250 if ppcirc is not None: 

251 # Modify self._shots: 

252 readouts = self._shots.to_readouts() 

253 new_readouts = [] 

254 for i in range(self._shots.n_outcomes): 

255 readout = readouts[i, :] 

256 values = {bit: bool(readout[i]) for bit, i in self.c_bits.items()} 

257 new_values = ppcirc._classical_eval(values) # noqa: SLF001 

258 new_readout = [0] * self._shots.width 

259 for bit, i in self.c_bits.items(): # noqa: PLW2901 

260 if new_values[bit]: 

261 new_readout[i] = 1 

262 new_readouts.append(new_readout) 

263 new_shots = OutcomeArray.from_readouts(new_readouts) 

264 else: 

265 new_shots = self._shots 

266 vals["shots"] = new_shots.choose_indices(chosen_readouts) 

267 

268 return StoredResult(**vals) 

269 

270 def _permute_statearray_qb_labels( 

271 self, 

272 array: np.ndarray, 

273 relabling_map: dict[Qubit, Qubit], 

274 ) -> np.ndarray: 

275 """Permute statevector/unitary according to a relabelling of Qubits. 

276 

277 :param array: The statevector or unitary 

278 :type array: np.ndarray 

279 :param relabling_map: Map from original Qubits to new. 

280 :type relabling_map: Dict[Qubit, Qubit] 

281 :return: Permuted array. 

282 :rtype: np.ndarray 

283 """ 

284 original_labeling: Sequence[Qubit] = self.get_qbitlist() 

285 n_labels = len(original_labeling) 

286 permutation = [0] * n_labels 

287 for i, orig_qb in enumerate(original_labeling): 

288 permutation[i] = original_labeling.index(relabling_map[orig_qb]) 

289 if permutation == list(range(n_labels)): 

290 # Optimization: nothing to do; return original array. 

291 return array 

292 permuter = ( 

293 permute_basis_indexing 

294 if len(array.shape) == 1 

295 else permute_rows_cols_in_unitary 

296 ) 

297 return permuter(array, tuple(permutation)) 

298 

299 def _get_state_res(self, qubits: Sequence[Qubit]) -> StoredResult: 

300 vals: dict[str, Any] = {} 

301 if not self.contains_state_results: 

302 raise InvalidResultType("state/unitary/density_matrix") 

303 

304 if not _check_permuted_sequence(qubits, self.q_bits): 

305 raise ValueError( 

306 "For state/unitary/density_matrix results only a permutation of" 

307 " all qubits can be requested." 

308 ) 

309 qb_mapping = {selfqb: qubits[index] for selfqb, index in self.q_bits.items()} 

310 if self._state is not None: 

311 vals["state"] = self._permute_statearray_qb_labels(self._state, qb_mapping) 

312 if self._unitary is not None: 

313 vals["unitary"] = self._permute_statearray_qb_labels( 

314 self._unitary, qb_mapping 

315 ) 

316 if self._density_matrix is not None: 

317 vals["density_matrix"] = self._permute_statearray_qb_labels( 

318 self._density_matrix, qb_mapping 

319 ) 

320 return StoredResult(**vals) 

321 

322 def get_result( 

323 self, 

324 request_ids: Sequence[UnitID] | None = None, 

325 basis: BasisOrder = BasisOrder.ilo, 

326 ppcirc: Circuit | None = None, 

327 ) -> StoredResult: 

328 """Retrieve all results, optionally according to a specified UnitID ordering 

329 or subset. 

330 

331 :param request_ids: Ordered set of either Qubits or Bits for which to 

332 retrieve results, defaults to None in which case all results are returned. 

333 For statevector/unitary/density_matrix results some permutation of all 

334 qubits must be requested. 

335 For measured results (shots/counts), some subset of the relevant bits must 

336 be requested. 

337 :type request_ids: Optional[Sequence[UnitID]], optional 

338 :param basis: Toggle between ILO (increasing lexicographic order of bit ids) and 

339 DLO (decreasing lexicographic order) for column ordering if request_ids is 

340 None. Defaults to BasisOrder.ilo. 

341 :param ppcirc: Classical post-processing circuit to apply to measured results 

342 :raises ValueError: Requested UnitIds (request_ids) contain a mixture of qubits 

343 and bits. 

344 :raises RuntimeError: Classical bits not set. 

345 :raises ValueError: Requested (Qu)Bit not in result. 

346 :raises RuntimeError: "Qubits not set." 

347 :raises ValueError: For state/unitary/density_matrix results only a permutation 

348 of all qubits can be requested. 

349 :return: All stored results corresponding to requested IDs. 

350 :rtype: StoredResult 

351 """ 

352 if request_ids is None: 

353 if self.contains_measured_results: 

354 request_ids = sorted( 

355 self.c_bits.keys(), reverse=(basis == BasisOrder.dlo) 

356 ) 

357 elif self.contains_state_results: 357 ↛ 362line 357 didn't jump to line 362 because the condition on line 357 was always true

358 request_ids = sorted( 

359 self.q_bits.keys(), reverse=(basis == BasisOrder.dlo) 

360 ) 

361 else: 

362 raise InvalidResultType("No results stored.") 

363 

364 if all(i.type == UnitType.bit for i in request_ids): 

365 return self._get_measured_res(request_ids, ppcirc) # type: ignore 

366 

367 if all(i.type == UnitType.qubit for i in request_ids): 

368 return self._get_state_res(request_ids) # type: ignore 

369 

370 raise ValueError( 

371 "Requested UnitIds (request_ids) contain a mixture of qubits and bits." 

372 ) 

373 

374 def get_shots( 

375 self, 

376 cbits: Sequence[Bit] | None = None, 

377 basis: BasisOrder = BasisOrder.ilo, 

378 ppcirc: Circuit | None = None, 

379 ) -> np.ndarray: 

380 """Return shots if available. 

381 

382 :param cbits: ordered subset of Bits, returns all results by default, defaults 

383 to None 

384 :type cbits: Optional[Sequence[Bit]], optional 

385 :param basis: Toggle between ILO (increasing lexicographic order of bit ids) and 

386 DLO (decreasing lexicographic order) for column ordering if cbits is None. 

387 Defaults to BasisOrder.ilo. 

388 :param ppcirc: Classical post-processing circuit to apply to measured results 

389 :raises InvalidResultType: Shot results are not available 

390 :return: 2D array of readouts, each row a separate outcome and each column a 

391 bit value. 

392 :rtype: np.ndarray 

393 

394 The order of the columns follows the order of `cbits`, if provided. 

395 """ 

396 if cbits is None: 

397 cbits = sorted(self.c_bits.keys(), reverse=(basis == BasisOrder.dlo)) 

398 res = self.get_result(cbits, ppcirc=ppcirc) 

399 if res.shots is not None: 399 ↛ 401line 399 didn't jump to line 401 because the condition on line 399 was always true

400 return res.shots.to_readouts() 

401 raise InvalidResultType("shots") 

402 

403 def get_counts( 

404 self, 

405 cbits: Sequence[Bit] | None = None, 

406 basis: BasisOrder = BasisOrder.ilo, 

407 ppcirc: Circuit | None = None, 

408 ) -> Counter[tuple[int, ...]]: 

409 """Return counts of outcomes if available. 

410 

411 :param cbits: ordered subset of Bits, returns all results by default, defaults 

412 to None 

413 :type cbits: Optional[Sequence[Bit]], optional 

414 :param basis: Toggle between ILO (increasing lexicographic order of bit ids) and 

415 DLO (decreasing lexicographic order) for column ordering if cbits is None. 

416 Defaults to BasisOrder.ilo. 

417 :param ppcirc: Classical post-processing circuit to apply to measured results 

418 :raises InvalidResultType: Counts are not available 

419 :return: Counts of outcomes 

420 :rtype: Counter[Tuple(int)] 

421 """ 

422 if cbits is None: 

423 cbits = sorted(self.c_bits.keys(), reverse=(basis == BasisOrder.dlo)) 

424 res = self.get_result(cbits, ppcirc=ppcirc) 

425 if res.counts is not None: 

426 return readout_counts(res.counts) 

427 if res.shots is not None: 427 ↛ 429line 427 didn't jump to line 429 because the condition on line 427 was always true

428 return readout_counts(res.shots.counts()) 

429 raise InvalidResultType("counts") 

430 

431 def get_state( 

432 self, 

433 qbits: Sequence[Qubit] | None = None, 

434 basis: BasisOrder = BasisOrder.ilo, 

435 ) -> np.ndarray: 

436 """Return statevector if available. 

437 

438 :param qbits: permutation of Qubits, defaults to None 

439 :type qbits: Optional[Sequence[Qubit]], optional 

440 :param basis: Toggle between ILO (increasing lexicographic order of qubit ids) 

441 and DLO (decreasing lexicographic order) for column ordering if qbits is 

442 None. Defaults to BasisOrder.ilo. 

443 :raises InvalidResultType: Statevector not available 

444 :return: Statevector, (complex 1-D numpy array) 

445 :rtype: np.ndarray 

446 """ 

447 if qbits is None: 

448 qbits = sorted(self.q_bits.keys(), reverse=(basis == BasisOrder.dlo)) 

449 res = self.get_result(qbits) 

450 if res.state is not None: 

451 return res.state 

452 if res.unitary is not None: 

453 state: np.ndarray = res.unitary[:, 0] 

454 return state 

455 raise InvalidResultType("state") 

456 

457 def get_unitary( 

458 self, 

459 qbits: Sequence[Qubit] | None = None, 

460 basis: BasisOrder = BasisOrder.ilo, 

461 ) -> np.ndarray: 

462 """Return unitary if available. 

463 

464 :param qbits: permutation of Qubits, defaults to None 

465 :type qbits: Optional[Sequence[Qubit]], optional 

466 :param basis: Toggle between ILO (increasing lexicographic order of qubit ids) 

467 and DLO (decreasing lexicographic order) for column ordering if qbits is 

468 None. Defaults to BasisOrder.ilo. 

469 :raises InvalidResultType: Statevector not available 

470 :return: Unitary, (complex 2-D numpy array) 

471 :rtype: np.ndarray 

472 """ 

473 if qbits is None: 

474 qbits = sorted(self.q_bits.keys(), reverse=(basis == BasisOrder.dlo)) 

475 res = self.get_result(qbits) 

476 if res.unitary is not None: 

477 return res.unitary 

478 raise InvalidResultType("unitary") 

479 

480 def get_density_matrix( 

481 self, 

482 qbits: Sequence[Qubit] | None = None, 

483 basis: BasisOrder = BasisOrder.ilo, 

484 ) -> np.ndarray: 

485 """Return density_matrix if available. 

486 

487 :param qbits: permutation of Qubits, defaults to None 

488 :type qbits: Optional[Sequence[Qubit]], optional 

489 :param basis: Toggle between ILO (increasing lexicographic order of qubit ids) 

490 and DLO (decreasing lexicographic order) for column ordering if qbits is 

491 None. Defaults to BasisOrder.ilo. 

492 :raises InvalidResultType: Statevector not available 

493 :return: density_matrix, (complex 2-D numpy array) 

494 :rtype: np.ndarray 

495 """ 

496 if qbits is None: 

497 qbits = sorted(self.q_bits.keys(), reverse=(basis == BasisOrder.dlo)) 

498 res = self.get_result(qbits) 

499 if res.density_matrix is not None: 

500 return res.density_matrix 

501 raise InvalidResultType("density_matrix") 

502 

503 def get_distribution( 

504 self, units: Sequence[UnitID] | None = None 

505 ) -> dict[tuple[int, ...], float]: 

506 """Calculate an exact or approximate probability distribution over outcomes. 

507 

508 If the exact statevector is known, the exact probability distribution is 

509 returned. Otherwise, if measured results are available the distribution 

510 is estimated from these results. 

511 

512 This method is deprecated. Please use :py:meth:`get_empirical_distribution` or 

513 :py:meth:`get_probability_distribution` instead. 

514 

515 DEPRECATED: will be removed after pytket 1.32. 

516 

517 :param units: Optionally provide the Qubits or Bits 

518 to marginalise the distribution over, defaults to None 

519 :type units: Optional[Sequence[UnitID]], optional 

520 :return: A distribution as a map from bitstring to probability. 

521 :rtype: Dict[Tuple[int, ...], float] 

522 """ 

523 warnings.warn( 

524 "The `BackendResult.get_distribution()` method is deprecated: " 

525 "please use `get_empirical_distribution()` or " 

526 "`get_probability_distribution()` instead.", 

527 DeprecationWarning, 

528 stacklevel=2, 

529 ) 

530 try: 

531 state = self.get_state(units) # type: ignore 

532 return probs_from_state(state) 

533 except InvalidResultType: 

534 counts = self.get_counts(units) # type: ignore 

535 total = sum(counts.values()) 

536 return {outcome: count / total for outcome, count in counts.items()} 

537 

538 def get_empirical_distribution( 

539 self, bits: Sequence[Bit] | None = None 

540 ) -> EmpiricalDistribution[tuple[int, ...]]: 

541 """Convert to a :py:class:`pytket.utils.distribution.EmpiricalDistribution` 

542 where the observations are sequences of 0s and 1s. 

543 

544 :param bits: Optionally provide the :py:class:`Bit` s over which to 

545 marginalize the distribution. 

546 :return: A distribution where the observations are sequences of 0s and 1s. 

547 """ # noqa: RUF002 

548 if not self.contains_measured_results: 

549 raise InvalidResultType( 

550 "Empirical distribution only available for measured result types." 

551 ) 

552 return EmpiricalDistribution(self.get_counts(bits)) 

553 

554 def get_probability_distribution( 

555 self, qubits: Sequence[Qubit] | None = None, min_p: float = 0.0 

556 ) -> ProbabilityDistribution[tuple[int, ...]]: 

557 """Convert to a :py:class:`pytket.utils.distribution.ProbabilityDistribution` 

558 where the possible outcomes are sequences of 0s and 1s. 

559 

560 :param qubits: Optionally provide the :py:class:`Qubit` s over which to 

561 marginalize the distribution. 

562 :param min_p: Optional probability below which to ignore values (for 

563 example to avoid spurious values due to rounding errors in 

564 statevector computations). Default 0. 

565 :return: A distribution where the possible outcomes are tuples of 0s and 1s. 

566 """ # noqa: RUF002 

567 if not self.contains_state_results: 

568 raise InvalidResultType( 

569 "Probability distribution only available for statevector result types." 

570 ) 

571 state = self.get_state(qubits) 

572 return ProbabilityDistribution(probs_from_state(state), min_p=min_p) 

573 

574 def get_debug_info(self) -> dict[str, float]: 

575 """Calculate the success rate of each assertion averaged across shots. 

576 

577 Each assertion in pytket is decomposed into a sequence of transformations 

578 and measurements. An assertion is successful if and only if all its associated 

579 measurements yield the correct results. 

580 

581 :return: The debug results as a map from assertion to average success rate. 

582 :rtype: Dict[str, float] 

583 """ 

584 _tket_debug_zero_prefix = _DEBUG_ZERO_REG_PREFIX + "_" 

585 _tket_debug_one_prefix = _DEBUG_ONE_REG_PREFIX + "_" 

586 debug_bit_dict: dict[str, dict[str, Any]] = {} 

587 for bit in self.c_bits: 

588 if bit.reg_name.startswith(_tket_debug_zero_prefix): 588 ↛ 591line 588 didn't jump to line 591 because the condition on line 588 was always true

589 expectation = 0 

590 assertion_name = bit.reg_name.split(_tket_debug_zero_prefix, 1)[1] 

591 elif bit.reg_name.startswith(_tket_debug_one_prefix): 

592 expectation = 1 

593 assertion_name = bit.reg_name.split(_tket_debug_one_prefix, 1)[1] 

594 else: 

595 continue 

596 if assertion_name not in debug_bit_dict: 

597 debug_bit_dict[assertion_name] = {"bits": [], "expectations": []} 

598 debug_bit_dict[assertion_name]["bits"].append(bit) 

599 debug_bit_dict[assertion_name]["expectations"].append(expectation) 

600 

601 debug_result_dict: dict[str, float] = {} 

602 for assertion_name, bits_info in debug_bit_dict.items(): 

603 counts = self.get_counts(bits_info["bits"]) 

604 debug_result_dict[assertion_name] = counts[ 

605 tuple(bits_info["expectations"]) 

606 ] / sum(counts.values()) 

607 return debug_result_dict 

608 

609 def to_dict(self) -> dict[str, Any]: 

610 """Generate a dictionary serialized representation of BackendResult, 

611 suitable for writing to JSON. 

612 

613 :return: JSON serializable dictionary. 

614 :rtype: Dict[str, Any] 

615 """ 

616 outdict: dict[str, Any] = {} 

617 outdict["qubits"] = [q.to_list() for q in self.get_qbitlist()] 

618 outdict["bits"] = [c.to_list() for c in self.get_bitlist()] 

619 if self._shots is not None: 

620 outdict["shots"] = self._shots.to_dict() 

621 if self._counts is not None: 

622 outdict["counts"] = [ 

623 {"outcome": oc.to_dict(), "count": count} 

624 for oc, count in self._counts.items() 

625 ] 

626 

627 if self._state is not None: 

628 outdict["state"] = _complex_ar_to_dict(self._state) 

629 if self._unitary is not None: 

630 outdict["unitary"] = _complex_ar_to_dict(self._unitary) 

631 if self._density_matrix is not None: 

632 outdict["density_matrix"] = _complex_ar_to_dict(self._density_matrix) 

633 

634 return outdict 

635 

636 @classmethod 

637 def from_dict(cls, res_dict: dict[str, Any]) -> "BackendResult": 

638 """Construct BackendResult object from JSON serializable dictionary 

639 representation, as generated by BackendResult.to_dict. 

640 

641 :return: Instance of BackendResult constructed from dictionary. 

642 :rtype: BackendResult 

643 """ 

644 init_dict = dict.fromkeys( 

645 ( 

646 "q_bits", 

647 "c_bits", 

648 "shots", 

649 "counts", 

650 "state", 

651 "unitary", 

652 "density_matrix", 

653 ) 

654 ) 

655 

656 if "qubits" in res_dict: 656 ↛ 658line 656 didn't jump to line 658 because the condition on line 656 was always true

657 init_dict["q_bits"] = [Qubit.from_list(tup) for tup in res_dict["qubits"]] 

658 if "bits" in res_dict: 658 ↛ 660line 658 didn't jump to line 660 because the condition on line 658 was always true

659 init_dict["c_bits"] = [Bit.from_list(tup) for tup in res_dict["bits"]] 

660 if "shots" in res_dict: 

661 init_dict["shots"] = OutcomeArray.from_dict(res_dict["shots"]) 

662 if "counts" in res_dict: 

663 init_dict["counts"] = Counter( 

664 { 

665 OutcomeArray.from_dict(elem["outcome"]): elem["count"] 

666 for elem in res_dict["counts"] 

667 } 

668 ) 

669 if "state" in res_dict: 

670 init_dict["state"] = _complex_ar_from_dict(res_dict["state"]) 

671 if "unitary" in res_dict: 

672 init_dict["unitary"] = _complex_ar_from_dict(res_dict["unitary"]) 

673 if "density_matrix" in res_dict: 

674 init_dict["density_matrix"] = _complex_ar_from_dict( 

675 res_dict["density_matrix"] 

676 ) 

677 

678 return BackendResult(**init_dict) 

679 

680 

681T = TypeVar("T") 

682 

683 

684def _sort_keys_by_val(dic: dict[T, int]) -> list[T]: 

685 if not dic: 

686 return [] 

687 vals, _ = zip(*sorted(dic.items(), key=lambda x: x[1]), strict=False) 

688 return list(cast("Iterable[T]", vals)) 

689 

690 

691def _check_permuted_sequence(first: Collection[Any], second: Collection[Any]) -> bool: 

692 return len(first) == len(second) and set(first) == set(second) 

693 

694 

695def _complex_ar_to_dict(ar: np.ndarray) -> dict[str, list]: 

696 """Dictionary of real, imaginary parts of complex array, each in list form.""" 

697 return {"real": ar.real.tolist(), "imag": ar.imag.tolist()} 

698 

699 

700def _complex_ar_from_dict(dic: dict[str, list]) -> np.ndarray: 

701 """Construct complex array from dictionary of real and imaginary parts""" 

702 

703 out = np.array(dic["real"], dtype=complex) 

704 out.imag = np.array(dic["imag"], dtype=float) 

705 return out