Coverage for /home/runner/work/tket/tket/pytket/pytket/utils/expectations.py: 83%

113 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 

15from typing import TYPE_CHECKING 

16 

17import numpy as np 

18 

19from pytket.circuit import Circuit, Qubit 

20from pytket.partition import ( 

21 GraphColourMethod, 

22 PauliPartitionStrat, 

23 measurement_reduction, 

24) 

25from pytket.pauli import QubitPauliString 

26 

27from .measurements import _all_pauli_measurements, append_pauli_measurement 

28from .operators import QubitPauliOperator 

29from .results import KwargTypes 

30 

31if TYPE_CHECKING: 31 ↛ 32line 31 didn't jump to line 32 because the condition on line 31 was never true

32 from pytket.backends.backend import Backend 

33 

34 

35def expectation_from_shots(shot_table: np.ndarray) -> float: 

36 """Estimates the expectation value of a circuit from its shots. 

37 Computes the parity of '1's across all bits to determine a +1 or -1 contribution 

38 from each row, and returns the average. 

39 

40 :param shot_table: The table of shots to interpret. 

41 :type shot_table: np.ndarray 

42 :return: The expectation value in the range [-1, 1]. 

43 :rtype: float 

44 """ 

45 aritysum = 0.0 

46 for row in shot_table: 

47 aritysum += np.sum(row) % 2 

48 return -2 * aritysum / len(shot_table) + 1 

49 

50 

51def expectation_from_counts(counts: dict[tuple[int, ...], int]) -> float: 

52 """Estimates the expectation value of a circuit from shot counts. 

53 Computes the parity of '1's across all bits to determine a +1 or -1 contribution 

54 from each readout, and returns the weighted average. 

55 

56 :param counts: Counts of each measurement outcome observed. 

57 :type counts: Dict[Tuple[int, ...], int] 

58 :return: The expectation value in the range [-1, 1]. 

59 :rtype: float 

60 """ 

61 aritysum = 0.0 

62 total_shots = 0 

63 for row, count in counts.items(): 

64 aritysum += count * (sum(row) % 2) 

65 total_shots += count 

66 return -2 * aritysum / total_shots + 1 

67 

68 

69def _default_index(q: Qubit) -> int: 

70 if q.reg_name != "q" or len(q.index) != 1: 

71 raise ValueError("Non-default qubit register") 

72 return int(q.index[0]) 

73 

74 

75def get_pauli_expectation_value( 

76 state_circuit: Circuit, 

77 pauli: QubitPauliString, 

78 backend: "Backend", 

79 n_shots: int | None = None, 

80) -> complex: 

81 """Estimates the expectation value of the given circuit with respect to the Pauli 

82 term by preparing measurements in the appropriate basis, running on the backend and 

83 interpreting the counts/statevector 

84 

85 :param state_circuit: Circuit that generates the desired state 

86 :math:`\\left|\\psi\\right>`. 

87 :type state_circuit: Circuit 

88 :param pauli: Pauli operator 

89 :type pauli: QubitPauliString 

90 :param backend: pytket backend to run circuit on. 

91 :type backend: Backend 

92 :param n_shots: Number of shots to run if backend supports shots/counts. Set to None 

93 to calculate using statevector if supported by the backend. Defaults to None 

94 :type n_shots: Optional[int], optional 

95 :return: :math:`\\left<\\psi | P | \\psi \\right>` 

96 :rtype: float 

97 """ 

98 if not n_shots: 

99 if not backend.valid_circuit(state_circuit): 99 ↛ 100line 99 didn't jump to line 100 because the condition on line 99 was never true

100 state_circuit = backend.get_compiled_circuit(state_circuit) 

101 if backend.supports_expectation: 101 ↛ 102line 101 didn't jump to line 102 because the condition on line 101 was never true

102 return backend.get_pauli_expectation_value(state_circuit, pauli) 

103 state = backend.run_circuit(state_circuit).get_state() 

104 return complex(pauli.state_expectation(state)) 

105 

106 measured_circ = state_circuit.copy() 

107 append_pauli_measurement(pauli, measured_circ) 

108 measured_circ = backend.get_compiled_circuit(measured_circ) 

109 if backend.supports_counts: 109 ↛ 112line 109 didn't jump to line 112 because the condition on line 109 was always true

110 counts = backend.run_circuit(measured_circ, n_shots=n_shots).get_counts() 

111 return expectation_from_counts(counts) 

112 if backend.supports_shots: 

113 shot_table = backend.run_circuit(measured_circ, n_shots=n_shots).get_shots() 

114 return expectation_from_shots(shot_table) 

115 raise ValueError("Backend does not support counts or shots") 

116 

117 

118def get_operator_expectation_value( # noqa: PLR0912, PLR0913, PLR0915 

119 state_circuit: Circuit, 

120 operator: QubitPauliOperator, 

121 backend: "Backend", 

122 n_shots: int | None = None, 

123 partition_strat: PauliPartitionStrat | None = None, 

124 colour_method: GraphColourMethod = GraphColourMethod.LargestFirst, 

125 **kwargs: KwargTypes, 

126) -> complex: 

127 """Estimates the expectation value of the given circuit with respect to the operator 

128 based on its individual Pauli terms. If the QubitPauliOperator has symbolic values 

129 the expectation value will also be symbolic. The input circuit must belong to the 

130 default qubit register and have contiguous qubit ordering. 

131 

132 :param state_circuit: Circuit that generates the desired state 

133 :math:`\\left|\\psi\\right>` 

134 :type state_circuit: Circuit 

135 :param operator: Operator :math:`H`. Currently does not support free symbols for the 

136 purpose of obtaining expectation values. 

137 :type operator: QubitPauliOperator 

138 :param backend: pytket backend to run circuit on. 

139 :type backend: Backend 

140 :param n_shots: Number of shots to run if backend supports shots/counts. None will 

141 force the backend to give the full state if available. Defaults to None 

142 :type n_shots: Optional[int], optional 

143 :param partition_strat: If retrieving shots, can perform measurement reduction using 

144 a chosen strategy 

145 :type partition_strat: Optional[PauliPartitionStrat], optional 

146 :return: :math:`\\left<\\psi | H | \\psi \\right>` 

147 :rtype: complex 

148 """ 

149 if not n_shots: 

150 if not backend.valid_circuit(state_circuit): 150 ↛ 151line 150 didn't jump to line 151 because the condition on line 150 was never true

151 state_circuit = backend.get_compiled_circuit(state_circuit) 

152 try: 

153 coeffs: list[complex] = [complex(v) for v in operator._dict.values()] # noqa: SLF001 

154 except TypeError: 

155 raise ValueError("QubitPauliOperator contains unevaluated symbols.") # noqa: B904 

156 if backend.supports_expectation and ( 156 ↛ 159line 156 didn't jump to line 159 because the condition on line 156 was never true

157 backend.expectation_allows_nonhermitian or all(z.imag == 0 for z in coeffs) 

158 ): 

159 return backend.get_operator_expectation_value(state_circuit, operator) 

160 result = backend.run_circuit(state_circuit) 

161 state = result.get_state() 

162 return operator.state_expectation(state) 

163 energy: complex 

164 id_string = QubitPauliString() 

165 energy = complex(operator[id_string]) if id_string in operator._dict else 0 # noqa: SLF001 

166 if not partition_strat: 

167 operator_without_id = QubitPauliOperator( 

168 {p: c for p, c in operator._dict.items() if (p != id_string)} # noqa: SLF001 

169 ) 

170 coeffs = [complex(c) for c in operator_without_id._dict.values()] # noqa: SLF001 

171 pauli_circuits = list( 

172 _all_pauli_measurements(operator_without_id, state_circuit) 

173 ) 

174 

175 handles = backend.process_circuits( 

176 backend.get_compiled_circuits(pauli_circuits), 

177 n_shots, 

178 valid_check=True, 

179 **kwargs, 

180 ) 

181 results = backend.get_results(handles) 

182 if backend.supports_counts: 

183 for result, coeff in zip(results, coeffs, strict=False): 

184 counts = result.get_counts() 

185 energy += coeff * expectation_from_counts(counts) 

186 for handle in handles: 

187 backend.pop_result(handle) 

188 return energy 

189 if backend.supports_shots: 189 ↛ 196line 189 didn't jump to line 196 because the condition on line 189 was always true

190 for result, coeff in zip(results, coeffs, strict=False): 

191 shots = result.get_shots() 

192 energy += coeff * expectation_from_shots(shots) 

193 for handle in handles: 

194 backend.pop_result(handle) 

195 return energy 

196 raise ValueError("Backend does not support counts or shots") 

197 qubit_pauli_string_list = [p for p in operator._dict.keys() if (p != id_string)] # noqa: SLF001, SIM118 

198 measurement_expectation = measurement_reduction( 

199 qubit_pauli_string_list, partition_strat, colour_method 

200 ) 

201 # note: this implementation requires storing all the results 

202 # in memory simultaneously to filter through them. 

203 measure_circs = [] 

204 for pauli_circ in measurement_expectation.measurement_circs: 

205 circ = state_circuit.copy() 

206 circ.append(pauli_circ) 

207 measure_circs.append(circ) 

208 handles = backend.process_circuits( 

209 backend.get_compiled_circuits(measure_circs), 

210 n_shots=n_shots, 

211 valid_check=True, 

212 **kwargs, 

213 ) 

214 results = backend.get_results(handles) 

215 for pauli_string in measurement_expectation.results: 

216 bitmaps = measurement_expectation.results[pauli_string] 

217 string_coeff = operator[pauli_string] 

218 for bm in bitmaps: 

219 index = bm.circ_index 

220 aritysum = 0.0 

221 if backend.supports_counts: 

222 counts = results[index].get_counts() 

223 total_shots = 0 

224 for row, count in counts.items(): 

225 aritysum += count * (sum(row[i] for i in bm.bits) % 2) 

226 total_shots += count 

227 e = ( 

228 ((-1) ** bm.invert) 

229 * string_coeff 

230 * (-2 * aritysum / total_shots + 1) 

231 ) 

232 energy += complex(e) 

233 elif backend.supports_shots: 233 ↛ 244line 233 didn't jump to line 244 because the condition on line 233 was always true

234 shots = results[index].get_shots() 

235 for row in shots: 

236 aritysum += sum(row[i] for i in bm.bits) % 2 

237 e = ( 

238 ((-1) ** bm.invert) 

239 * string_coeff 

240 * (-2 * aritysum / len(shots) + 1) 

241 ) 

242 energy += complex(e) 

243 else: 

244 raise ValueError("Backend does not support counts or shots") 

245 for handle in handles: 

246 backend.pop_result(handle) 

247 return energy