Coverage for /home/runner/work/tket/tket/pytket/pytket/utils/expectations.py: 84%
115 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-14 11:30 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-14 11:30 +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.
15from typing import TYPE_CHECKING
17import numpy as np
19from pytket.circuit import Circuit, Qubit
20from pytket.partition import (
21 GraphColourMethod,
22 PauliPartitionStrat,
23 measurement_reduction,
24)
25from pytket.pauli import QubitPauliString
27from .measurements import _all_pauli_measurements, append_pauli_measurement
28from .operators import QubitPauliOperator
29from .results import KwargTypes
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
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.
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
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.
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
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])
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
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))
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")
118def get_operator_expectation_value(
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.
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()]
154 except TypeError:
155 raise ValueError("QubitPauliOperator contains unevaluated symbols.")
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 if id_string in operator._dict:
166 energy = complex(operator[id_string])
167 else:
168 energy = 0
169 if not partition_strat:
170 operator_without_id = QubitPauliOperator(
171 {p: c for p, c in operator._dict.items() if (p != id_string)}
172 )
173 coeffs = [complex(c) for c in operator_without_id._dict.values()]
174 pauli_circuits = list(
175 _all_pauli_measurements(operator_without_id, state_circuit)
176 )
178 handles = backend.process_circuits(
179 backend.get_compiled_circuits(pauli_circuits),
180 n_shots,
181 valid_check=True,
182 **kwargs,
183 )
184 results = backend.get_results(handles)
185 if backend.supports_counts:
186 for result, coeff in zip(results, coeffs):
187 counts = result.get_counts()
188 energy += coeff * expectation_from_counts(counts)
189 for handle in handles:
190 backend.pop_result(handle)
191 return energy
192 if backend.supports_shots: 192 ↛ 199line 192 didn't jump to line 199 because the condition on line 192 was always true
193 for result, coeff in zip(results, coeffs):
194 shots = result.get_shots()
195 energy += coeff * expectation_from_shots(shots)
196 for handle in handles:
197 backend.pop_result(handle)
198 return energy
199 raise ValueError("Backend does not support counts or shots")
200 qubit_pauli_string_list = [p for p in operator._dict.keys() if (p != id_string)]
201 measurement_expectation = measurement_reduction(
202 qubit_pauli_string_list, partition_strat, colour_method
203 )
204 # note: this implementation requires storing all the results
205 # in memory simultaneously to filter through them.
206 measure_circs = []
207 for pauli_circ in measurement_expectation.measurement_circs:
208 circ = state_circuit.copy()
209 circ.append(pauli_circ)
210 measure_circs.append(circ)
211 handles = backend.process_circuits(
212 backend.get_compiled_circuits(measure_circs),
213 n_shots=n_shots,
214 valid_check=True,
215 **kwargs,
216 )
217 results = backend.get_results(handles)
218 for pauli_string in measurement_expectation.results:
219 bitmaps = measurement_expectation.results[pauli_string]
220 string_coeff = operator[pauli_string]
221 for bm in bitmaps:
222 index = bm.circ_index
223 aritysum = 0.0
224 if backend.supports_counts:
225 counts = results[index].get_counts()
226 total_shots = 0
227 for row, count in counts.items():
228 aritysum += count * (sum(row[i] for i in bm.bits) % 2)
229 total_shots += count
230 e = (
231 ((-1) ** bm.invert)
232 * string_coeff
233 * (-2 * aritysum / total_shots + 1)
234 )
235 energy += complex(e)
236 elif backend.supports_shots: 236 ↛ 247line 236 didn't jump to line 247 because the condition on line 236 was always true
237 shots = results[index].get_shots()
238 for row in shots:
239 aritysum += sum(row[i] for i in bm.bits) % 2
240 e = (
241 ((-1) ** bm.invert)
242 * string_coeff
243 * (-2 * aritysum / len(shots) + 1)
244 )
245 energy += complex(e)
246 else:
247 raise ValueError("Backend does not support counts or shots")
248 for handle in handles:
249 backend.pop_result(handle)
250 return energy