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

113 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 

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 :return: The expectation value in the range [-1, 1]. 

42 """ 

43 aritysum = 0.0 

44 for row in shot_table: 

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

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

47 

48 

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

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

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

52 from each readout, and returns the weighted average. 

53 

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

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

56 """ 

57 aritysum = 0.0 

58 total_shots = 0 

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

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

61 total_shots += count 

62 return -2 * aritysum / total_shots + 1 

63 

64 

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

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

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

68 return int(q.index[0]) 

69 

70 

71def get_pauli_expectation_value( 

72 state_circuit: Circuit, 

73 pauli: QubitPauliString, 

74 backend: "Backend", 

75 n_shots: int | None = None, 

76) -> complex: 

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

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

79 interpreting the counts/statevector 

80 

81 :param state_circuit: Circuit that generates the desired state 

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

83 :param pauli: Pauli operator 

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

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

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

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

88 """ 

89 if not n_shots: 

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

91 state_circuit = backend.get_compiled_circuit(state_circuit) 

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

93 return backend.get_pauli_expectation_value(state_circuit, pauli) 

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

95 return complex(pauli.state_expectation(state)) 

96 

97 measured_circ = state_circuit.copy() 

98 append_pauli_measurement(pauli, measured_circ) 

99 measured_circ = backend.get_compiled_circuit(measured_circ) 

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

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

102 return expectation_from_counts(counts) 

103 if backend.supports_shots: 

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

105 return expectation_from_shots(shot_table) 

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

107 

108 

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

110 state_circuit: Circuit, 

111 operator: QubitPauliOperator, 

112 backend: "Backend", 

113 n_shots: int | None = None, 

114 partition_strat: PauliPartitionStrat | None = None, 

115 colour_method: GraphColourMethod = GraphColourMethod.LargestFirst, 

116 **kwargs: KwargTypes, 

117) -> complex: 

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

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

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

121 default qubit register and have contiguous qubit ordering. 

122 

123 :param state_circuit: Circuit that generates the desired state 

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

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

126 purpose of obtaining expectation values. 

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

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

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

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

131 a chosen strategy 

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

133 """ 

134 if not n_shots: 

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

136 state_circuit = backend.get_compiled_circuit(state_circuit) 

137 try: 

138 coeffs: list[complex] = [complex(v) for v in operator.get_dict().values()] 

139 except TypeError: 

140 raise ValueError( # noqa: B904 

141 "QubitPauliOperator contains unevaluated symbols." 

142 ) 

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

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

145 ): 

146 return backend.get_operator_expectation_value(state_circuit, operator) 

147 result = backend.run_circuit(state_circuit) 

148 state = result.get_state() 

149 return operator.state_expectation(state) 

150 energy: complex 

151 id_string = QubitPauliString() 

152 energy = complex(operator[id_string]) if id_string in operator.get_dict() else 0 

153 if not partition_strat: 

154 operator_without_id = QubitPauliOperator( 

155 {p: c for p, c in operator.get_dict().items() if (p != id_string)} 

156 ) 

157 coeffs = [complex(c) for c in operator_without_id.get_dict().values()] 

158 pauli_circuits = list( 

159 _all_pauli_measurements(operator_without_id, state_circuit) 

160 ) 

161 

162 handles = backend.process_circuits( 

163 backend.get_compiled_circuits(pauli_circuits), 

164 n_shots, 

165 valid_check=True, 

166 **kwargs, 

167 ) 

168 results = backend.get_results(handles) 

169 if backend.supports_counts: 

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

171 counts = result.get_counts() 

172 energy += coeff * expectation_from_counts(counts) 

173 for handle in handles: 

174 backend.pop_result(handle) 

175 return energy 

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

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

178 shots = result.get_shots() 

179 energy += coeff * expectation_from_shots(shots) 

180 for handle in handles: 

181 backend.pop_result(handle) 

182 return energy 

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

184 qubit_pauli_string_list = [p for p in operator.get_dict() if (p != id_string)] 

185 measurement_expectation = measurement_reduction( 

186 qubit_pauli_string_list, partition_strat, colour_method 

187 ) 

188 # note: this implementation requires storing all the results 

189 # in memory simultaneously to filter through them. 

190 measure_circs = [] 

191 for pauli_circ in measurement_expectation.measurement_circs: 

192 circ = state_circuit.copy() 

193 circ.append(pauli_circ) 

194 measure_circs.append(circ) 

195 handles = backend.process_circuits( 

196 backend.get_compiled_circuits(measure_circs), 

197 n_shots=n_shots, 

198 valid_check=True, 

199 **kwargs, 

200 ) 

201 results = backend.get_results(handles) 

202 for pauli_string in measurement_expectation.results: 

203 bitmaps = measurement_expectation.results[pauli_string] 

204 string_coeff = operator[pauli_string] 

205 for bm in bitmaps: 

206 index = bm.circ_index 

207 aritysum = 0.0 

208 if backend.supports_counts: 

209 counts = results[index].get_counts() 

210 total_shots = 0 

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

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

213 total_shots += count 

214 e = ( 

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

216 * string_coeff 

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

218 ) 

219 energy += complex(e) 

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

221 shots = results[index].get_shots() 

222 for row in shots: 

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

224 e = ( 

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

226 * string_coeff 

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

228 ) 

229 energy += complex(e) 

230 else: 

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

232 for handle in handles: 

233 backend.pop_result(handle) 

234 return energy