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
« 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.
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 :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
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.
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
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])
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
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))
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")
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.
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 )
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