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
« 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.
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( # 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.
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 )
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