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

278 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-06-02 12:44 +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 """ 

195 return _sort_keys_by_val(self.c_bits) 

196 

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

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

199 

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

201 :return: Sorted list of Qubits. 

202 """ 

203 

204 return _sort_keys_by_val(self.q_bits) 

205 

206 def _get_measured_res( # noqa: PLR0912 

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

208 ) -> StoredResult: 

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

210 

211 if not self.contains_measured_results: 

212 raise InvalidResultType("shots/counts") 

213 

214 if self._ppcirc is not None: 

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

216 ppcirc = self._ppcirc 

217 else: 

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

219 

220 try: 

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

222 except KeyError: 

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

224 

225 if self._counts is not None: 

226 if ppcirc is not None: 

227 # Modify self._counts: 

228 new_counts: Counter[OutcomeArray] = Counter() 

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

230 readout = oa.to_readout() 

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

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

233 new_oa = OutcomeArray.from_readouts( 

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

235 ) 

236 new_counts[new_oa] += n 

237 else: 

238 new_counts = self._counts 

239 vals["counts"] = reduce( 

240 operator.add, 

241 ( 

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

243 for outcome, count in new_counts.items() 

244 ), 

245 Counter(), 

246 ) 

247 if self._shots is not None: 

248 if ppcirc is not None: 

249 # Modify self._shots: 

250 readouts = self._shots.to_readouts() 

251 new_readouts = [] 

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

253 readout = readouts[i, :] 

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

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

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

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

258 if new_values[bit]: 

259 new_readout[i] = 1 

260 new_readouts.append(new_readout) 

261 new_shots = OutcomeArray.from_readouts(new_readouts) 

262 else: 

263 new_shots = self._shots 

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

265 

266 return StoredResult(**vals) 

267 

268 def _permute_statearray_qb_labels( 

269 self, 

270 array: np.ndarray, 

271 relabling_map: dict[Qubit, Qubit], 

272 ) -> np.ndarray: 

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

274 

275 :param array: The statevector or unitary 

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

277 :return: Permuted array. 

278 """ 

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

280 n_labels = len(original_labeling) 

281 permutation = [0] * n_labels 

282 for i, orig_qb in enumerate(original_labeling): 

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

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

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

286 return array 

287 permuter = ( 

288 permute_basis_indexing 

289 if len(array.shape) == 1 

290 else permute_rows_cols_in_unitary 

291 ) 

292 return permuter(array, tuple(permutation)) 

293 

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

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

296 if not self.contains_state_results: 

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

298 

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

300 raise ValueError( 

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

302 " all qubits can be requested." 

303 ) 

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

305 if self._state is not None: 

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

307 if self._unitary is not None: 

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

309 self._unitary, qb_mapping 

310 ) 

311 if self._density_matrix is not None: 

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

313 self._density_matrix, qb_mapping 

314 ) 

315 return StoredResult(**vals) 

316 

317 def get_result( 

318 self, 

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

320 basis: BasisOrder = BasisOrder.ilo, 

321 ppcirc: Circuit | None = None, 

322 ) -> StoredResult: 

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

324 or subset. 

325 

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

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

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

329 qubits must be requested. 

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

331 be requested. 

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

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

334 None. Defaults to BasisOrder.ilo. 

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

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

337 and bits. 

338 :raises RuntimeError: Classical bits not set. 

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

340 :raises RuntimeError: "Qubits not set." 

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

342 of all qubits can be requested. 

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

344 """ 

345 if request_ids is None: 

346 if self.contains_measured_results: 

347 request_ids = sorted( 

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

349 ) 

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

351 request_ids = sorted( 

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

353 ) 

354 else: 

355 raise InvalidResultType("No results stored.") 

356 

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

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

359 

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

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

362 

363 raise ValueError( 

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

365 ) 

366 

367 def get_shots( 

368 self, 

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

370 basis: BasisOrder = BasisOrder.ilo, 

371 ppcirc: Circuit | None = None, 

372 ) -> np.ndarray: 

373 """Return shots if available. 

374 

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

376 to None 

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

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

379 Defaults to BasisOrder.ilo. 

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

381 :raises InvalidResultType: Shot results are not available 

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

383 bit value. 

384 

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

386 """ 

387 if cbits is None: 

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

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

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

391 return res.shots.to_readouts() 

392 raise InvalidResultType("shots") 

393 

394 def get_counts( 

395 self, 

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

397 basis: BasisOrder = BasisOrder.ilo, 

398 ppcirc: Circuit | None = None, 

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

400 """Return counts of outcomes if available. 

401 

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

403 to None 

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

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

406 Defaults to BasisOrder.ilo. 

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

408 :raises InvalidResultType: Counts are not available 

409 :return: Counts of outcomes 

410 """ 

411 if cbits is None: 

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

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

414 if res.counts is not None: 

415 return readout_counts(res.counts) 

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

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

418 raise InvalidResultType("counts") 

419 

420 def get_state( 

421 self, 

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

423 basis: BasisOrder = BasisOrder.ilo, 

424 ) -> np.ndarray: 

425 """Return statevector if available. 

426 

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

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

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

430 None. Defaults to BasisOrder.ilo. 

431 :raises InvalidResultType: Statevector not available 

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

433 """ 

434 if qbits is None: 

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

436 res = self.get_result(qbits) 

437 if res.state is not None: 

438 return res.state 

439 if res.unitary is not None: 

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

441 return state 

442 raise InvalidResultType("state") 

443 

444 def get_unitary( 

445 self, 

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

447 basis: BasisOrder = BasisOrder.ilo, 

448 ) -> np.ndarray: 

449 """Return unitary if available. 

450 

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

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

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

454 None. Defaults to BasisOrder.ilo. 

455 :raises InvalidResultType: Statevector not available 

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

457 """ 

458 if qbits is None: 

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

460 res = self.get_result(qbits) 

461 if res.unitary is not None: 

462 return res.unitary 

463 raise InvalidResultType("unitary") 

464 

465 def get_density_matrix( 

466 self, 

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

468 basis: BasisOrder = BasisOrder.ilo, 

469 ) -> np.ndarray: 

470 """Return density_matrix if available. 

471 

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

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

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

475 None. Defaults to BasisOrder.ilo. 

476 :raises InvalidResultType: Statevector not available 

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

478 """ 

479 if qbits is None: 

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

481 res = self.get_result(qbits) 

482 if res.density_matrix is not None: 

483 return res.density_matrix 

484 raise InvalidResultType("density_matrix") 

485 

486 def get_distribution( 

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

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

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

490 

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

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

493 is estimated from these results. 

494 

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

496 :py:meth:`get_probability_distribution` instead. 

497 

498 DEPRECATED: will be removed after pytket 1.32. 

499 

500 :param units: Optionally provide the Qubits or Bits 

501 to marginalise the distribution over, defaults to None 

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

503 """ 

504 warnings.warn( 

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

506 "please use `get_empirical_distribution()` or " 

507 "`get_probability_distribution()` instead.", 

508 DeprecationWarning, 

509 stacklevel=2, 

510 ) 

511 try: 

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

513 return probs_from_state(state) 

514 except InvalidResultType: 

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

516 total = sum(counts.values()) 

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

518 

519 def get_empirical_distribution( 

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

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

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

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

524 

525 :param bits: Optionally provide the :py:class:`~.Bit` s over which to 

526 marginalize the distribution. 

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

528 """ # noqa: RUF002 

529 if not self.contains_measured_results: 

530 raise InvalidResultType( 

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

532 ) 

533 return EmpiricalDistribution(self.get_counts(bits)) 

534 

535 def get_probability_distribution( 

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

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

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

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

540 

541 :param qubits: Optionally provide the :py:class:`~.Qubit` s over which to 

542 marginalize the distribution. 

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

544 example to avoid spurious values due to rounding errors in 

545 statevector computations). Default 0. 

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

547 """ # noqa: RUF002 

548 if not self.contains_state_results: 

549 raise InvalidResultType( 

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

551 ) 

552 state = self.get_state(qubits) 

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

554 

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

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

557 

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

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

560 measurements yield the correct results. 

561 

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

563 """ 

564 _tket_debug_zero_prefix = _DEBUG_ZERO_REG_PREFIX + "_" 

565 _tket_debug_one_prefix = _DEBUG_ONE_REG_PREFIX + "_" 

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

567 for bit in self.c_bits: 

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

569 expectation = 0 

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

571 elif bit.reg_name.startswith(_tket_debug_one_prefix): 

572 expectation = 1 

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

574 else: 

575 continue 

576 if assertion_name not in debug_bit_dict: 

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

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

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

580 

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

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

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

584 debug_result_dict[assertion_name] = counts[ 

585 tuple(bits_info["expectations"]) 

586 ] / sum(counts.values()) 

587 return debug_result_dict 

588 

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

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

591 suitable for writing to JSON. 

592 

593 :return: JSON serializable dictionary. 

594 """ 

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

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

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

598 if self._shots is not None: 

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

600 if self._counts is not None: 

601 outdict["counts"] = [ 

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

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

604 ] 

605 

606 if self._state is not None: 

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

608 if self._unitary is not None: 

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

610 if self._density_matrix is not None: 

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

612 

613 return outdict 

614 

615 @classmethod 

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

617 """Construct BackendResult object from JSON serializable dictionary 

618 representation, as generated by BackendResult.to_dict. 

619 

620 :return: Instance of BackendResult constructed from dictionary. 

621 """ 

622 init_dict = dict.fromkeys( 

623 ( 

624 "q_bits", 

625 "c_bits", 

626 "shots", 

627 "counts", 

628 "state", 

629 "unitary", 

630 "density_matrix", 

631 ) 

632 ) 

633 

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

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

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

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

638 if "shots" in res_dict: 

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

640 if "counts" in res_dict: 

641 init_dict["counts"] = Counter( 

642 { 

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

644 for elem in res_dict["counts"] 

645 } 

646 ) 

647 if "state" in res_dict: 

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

649 if "unitary" in res_dict: 

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

651 if "density_matrix" in res_dict: 

652 init_dict["density_matrix"] = _complex_ar_from_dict( 

653 res_dict["density_matrix"] 

654 ) 

655 

656 return BackendResult(**init_dict) 

657 

658 

659T = TypeVar("T") 

660 

661 

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

663 if not dic: 

664 return [] 

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

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

667 

668 

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

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

671 

672 

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

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

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

676 

677 

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

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

680 

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

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

683 return out