Coverage for /home/runner/work/tket/tket/pytket/pytket/utils/operators.py: 90%
137 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-25 16:00 +0000
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-25 16:00 +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.
15import copy
16from typing import TYPE_CHECKING, Any, Union
18import numpy
19import numpy as np
20from sympy import Expr, Symbol, im, re, sympify
22from pytket.circuit import Qubit
23from pytket.pauli import QubitPauliString, pauli_string_mult
24from pytket.utils.serialization import complex_to_list, list_to_complex
26CoeffTypeAccepted = Union[int, float, complex, Expr] # noqa: UP007
28if TYPE_CHECKING: 28 ↛ 29line 28 didn't jump to line 29 because the condition on line 28 was never true
29 from scipy.sparse import csc_matrix
32def _coeff_convert(coeff: CoeffTypeAccepted | str) -> Expr:
33 sympy_val = sympify(coeff)
34 if not isinstance(sympy_val, Expr): 34 ↛ 35line 34 didn't jump to line 35 because the condition on line 34 was never true
35 raise ValueError("Unsupported value for QubitPauliString coefficient")
36 return sympy_val
39class QubitPauliOperator:
40 """
41 Generic data structure for generation of circuits and expectation
42 value calculation. Contains a dictionary from QubitPauliString to
43 sympy Expr. Capacity for symbolic expressions allows the operator
44 to be used to generate ansätze for variational algorithms.
46 Represents a mathematical object :math:`\\sum_j \\alpha_j P_j`,
47 where each :math:`\\alpha_j` is a complex symbolic expression and
48 :math:`P_j` is a Pauli string, i.e. :math:`P_j \\in \\{ I, X, Y,
49 Z\\}^{\\otimes n}`.
51 A prototypical example is a molecular Hamiltonian, for which one
52 may wish to calculate the expectation value :math:`\\langle \\Psi
53 | H | \\Psi \\rangle` by decomposing :math:`H` into individual
54 Pauli measurements. Alternatively, one may wish to evolve a state
55 by the operator :math:`e^{-iHt}` for digital quantum simulation.
56 In this case, the whole operator must be decomposed into native
57 operations.
59 In both cases, :math:`H` may be represented by a
60 QubitPauliOperator.
61 """
63 def __init__(
64 self,
65 dictionary: dict[QubitPauliString, CoeffTypeAccepted] | None = None,
66 ) -> None:
67 self._dict: dict[QubitPauliString, Expr] = {}
68 if dictionary:
69 for key, value in dictionary.items():
70 self._dict[key] = _coeff_convert(value)
71 self._collect_qubits()
73 def __repr__(self) -> str:
74 return self._dict.__repr__()
76 def __getitem__(self, key: QubitPauliString) -> Expr:
77 return self._dict[key]
79 def get(self, key: QubitPauliString, default: CoeffTypeAccepted) -> Expr:
80 """
81 Get the coefficient of a particular string present in the operator.
82 """
83 return self._dict.get(key, _coeff_convert(default))
85 def __setitem__(self, key: QubitPauliString, value: CoeffTypeAccepted) -> None:
86 """Update value in dictionary ([]). Automatically converts value into sympy
87 Expr.
89 :param key: String to use as key
90 :param value: Associated coefficient
91 """
92 self._dict[key] = _coeff_convert(value)
93 self._all_qubits.update(key.map.keys())
95 def __getstate__(self) -> dict[QubitPauliString, Expr]:
96 return self._dict
98 def __setstate__(self, _dict: dict[QubitPauliString, Expr]) -> None:
99 # values assumed to be already sympified
100 self._dict = _dict
101 self._collect_qubits()
103 def __eq__(self, other: object) -> bool:
104 if isinstance(other, QubitPauliOperator): 104 ↛ 106line 104 didn't jump to line 106 because the condition on line 104 was always true
105 return self._dict == other._dict
106 return False
108 def __hash__(self) -> int:
109 return hash(self._dict)
111 def __iadd__(self, addend: "QubitPauliOperator") -> "QubitPauliOperator":
112 """In-place addition (+=) of QubitPauliOperators.
114 :param addend: The operator to add
115 :return: Updated operator (self)
116 """
117 if isinstance(addend, QubitPauliOperator): 117 ↛ 122line 117 didn't jump to line 122 because the condition on line 117 was always true
118 for key, value in addend._dict.items():
119 self[key] = self.get(key, 0.0) + value
120 self._all_qubits.update(addend._all_qubits)
121 else:
122 raise TypeError(f"Cannot add {type(addend)} to QubitPauliOperator.")
124 return self
126 def __add__(self, addend: "QubitPauliOperator") -> "QubitPauliOperator":
127 """Addition (+) of QubitPauliOperators.
129 :param addend: The operator to add
130 :return: Sum operator
131 """
132 summand = copy.deepcopy(self)
133 summand += addend
134 return summand
136 def __imul__(
137 self, multiplier: Union[float, Expr, "QubitPauliOperator"]
138 ) -> "QubitPauliOperator":
139 """In-place multiplication (*=) with QubitPauliOperator or scalar.
140 Multiply coefficients and terms.
142 :param multiplier: The operator or scalar to multiply
143 :return: Updated operator (self)
144 """
146 # Handle operator of the same type
147 if isinstance(multiplier, QubitPauliOperator):
148 result_terms: dict = {}
149 for left_key, left_value in self._dict.items():
150 for right_key, right_value in multiplier._dict.items():
151 new_term, bonus_coeff = pauli_string_mult(left_key, right_key)
152 new_coefficient = bonus_coeff * left_value * right_value
154 # Update result dict.
155 if new_term in result_terms: 155 ↛ 156line 155 didn't jump to line 156 because the condition on line 155 was never true
156 result_terms[new_term] += new_coefficient
157 else:
158 result_terms[new_term] = new_coefficient
159 self._dict = result_terms
160 self._all_qubits.update(multiplier._all_qubits)
161 return self
163 # Handle scalars.
164 if isinstance(multiplier, float | Expr): 164 ↛ 170line 164 didn't jump to line 170 because the condition on line 164 was always true
165 for key in self._dict:
166 self[key] *= multiplier
167 return self
169 # Invalid multiplier type
170 raise TypeError(f"Cannot multiply QubitPauliOperator with {type(multiplier)}")
172 def __mul__(
173 self, multiplier: Union[float, Expr, "QubitPauliOperator"]
174 ) -> "QubitPauliOperator":
175 """Multiplication (*) by QubitPauliOperator or scalar.
177 :param multiplier: The scalar to multiply by
178 :return: Product operator
179 """
180 product = copy.deepcopy(self)
181 product *= multiplier
182 return product
184 def __rmul__(self, multiplier: CoeffTypeAccepted) -> "QubitPauliOperator":
185 """Multiplication (*) by a scalar.
186 We only define __rmul__ for scalars because left multiply is
187 queried as default behaviour, and is used for
188 QubitPauliOperator*QubitPauliOperator.
190 :param multiplier: The scalar to multiply by
191 :return: Product operator
192 """
193 return self.__mul__(_coeff_convert(multiplier))
195 @property
196 def all_qubits(self) -> set[Qubit]:
197 """
198 The set of all qubits the operator ranges over (including qubits
199 that were provided explicitly as identities)
200 """
201 return self._all_qubits
203 def subs(self, symbol_dict: dict[Symbol, complex]) -> None:
204 """Substitutes any matching symbols in the QubitPauliOperator.
206 :param symbol_dict: A dictionary of symbols to fixed values.
207 """
208 for key, value in self._dict.items():
209 self._dict[key] = value.subs(symbol_dict)
211 def get_dict(self) -> dict[QubitPauliString, Expr]:
212 """Generate a dict representation of QubitPauliOperator,
213 mapping each :py:class:`~.QubitPauliString` in the support
214 to its corresponding value.
216 :return: A dict of Pauli strings and their coefficients
217 as key-value pairs
218 """
219 return self._dict
221 def to_list(self) -> list[dict[str, Any]]:
222 """Generate a list serialized representation of QubitPauliOperator,
223 suitable for writing to JSON.
225 :return: JSON serializable list of dictionaries.
226 """
227 ret: list[dict[str, Any]] = []
228 for k, v in self._dict.items():
229 try:
230 coeff = complex_to_list(complex(v))
231 except TypeError:
232 assert isinstance(Expr(v), Expr)
233 coeff = str(v)
234 ret.append(
235 {
236 "string": k.to_list(),
237 "coefficient": coeff,
238 }
239 )
240 return ret
242 @classmethod
243 def from_list(cls, pauli_list: list[dict[str, Any]]) -> "QubitPauliOperator":
244 """Construct a QubitPauliOperator from a serializable JSON list format,
245 as returned by QubitPauliOperator.to_list()
247 :return: New QubitPauliOperator instance.
248 """
250 def get_qps(obj: dict[str, Any]) -> QubitPauliString:
251 return QubitPauliString.from_list(obj["string"])
253 def get_coeff(obj: dict[str, Any]) -> Expr:
254 coeff = obj["coefficient"]
255 if type(coeff) is str:
256 return _coeff_convert(coeff)
257 return _coeff_convert(list_to_complex(coeff))
259 return QubitPauliOperator({get_qps(obj): get_coeff(obj) for obj in pauli_list})
261 def to_sparse_matrix(self, qubits: list[Qubit] | int | None = None) -> "csc_matrix":
262 """Represents the sparse operator as a dense operator under the ordering
263 scheme specified by ``qubits``, and generates the corresponding matrix.
265 - When ``qubits`` is an explicit list, the qubits are ordered with
266 ``qubits[0]`` as the most significant qubit for indexing into the matrix.
267 - If ``None``, then no padding qubits are introduced and we use the ILO-BE
268 convention, e.g. ``Qubit("a", 0)`` is more significant than
269 ``Qubit("a", 1)`` or ``Qubit("b")``.
270 - Giving a number specifies the number of qubits to use in the final
271 operator, treated as sequentially indexed from 0 in the default register
272 (padding with identities as necessary) and ordered by ILO-BE so
273 ``Qubit(0)`` is the most significant.
275 :param qubits: Sequencing of qubits in the matrix, either as an explicit
276 list, number of qubits to pad to, or infer from the operator.
277 Defaults to None
278 :return: A sparse matrix representation of the operator.
279 """
280 if qubits is None:
281 qubits_ = sorted(list(self._all_qubits)) # noqa: C414
282 return sum(
283 complex(coeff) * pauli.to_sparse_matrix(qubits_)
284 for pauli, coeff in self._dict.items()
285 )
286 return sum(
287 complex(coeff) * pauli.to_sparse_matrix(qubits)
288 for pauli, coeff in self._dict.items()
289 )
291 def dot_state(
292 self, state: np.ndarray, qubits: list[Qubit] | None = None
293 ) -> np.ndarray:
294 """Applies the operator to the given state, mapping qubits to indexes
295 according to ``qubits``.
297 - When ``qubits`` is an explicit list, the qubits are ordered with
298 ``qubits[0]`` as the most significant qubit for indexing into ``state``.
299 - If ``None``, qubits sequentially indexed from 0 in the default register
300 and ordered by ILO-BE so ``Qubit(0)`` is the most significant.
302 :param state: The initial statevector
303 :param qubits: Sequencing of qubits in ``state``, if not mapped to the
304 default register. Defaults to None
305 :return: The dot product of the operator with the statevector
306 """
307 if qubits:
308 product_sum = sum(
309 complex(coeff) * pauli.dot_state(state, qubits)
310 for pauli, coeff in self._dict.items()
311 )
312 else:
313 product_sum = sum(
314 complex(coeff) * pauli.dot_state(state)
315 for pauli, coeff in self._dict.items()
316 )
317 return product_sum if isinstance(product_sum, numpy.ndarray) else state
319 def state_expectation(
320 self, state: np.ndarray, qubits: list[Qubit] | None = None
321 ) -> complex:
322 """Calculates the expectation value of the given statevector with respect
323 to the operator, mapping qubits to indexes according to ``qubits``.
325 - When ``qubits`` is an explicit list, the qubits are ordered with
326 ``qubits[0]`` as the most significant qubit for indexing into ``state``.
327 - If ``None``, qubits sequentially indexed from 0 in the default register
328 and ordered by ILO-BE so ``Qubit(0)`` is the most significant.
330 :param state: The initial statevector
331 :param qubits: Sequencing of qubits in ``state``, if not mapped to the
332 default register. Defaults to None
333 :return: The expectation value of the statevector and operator
334 """
335 if qubits:
336 return sum(
337 complex(coeff) * pauli.state_expectation(state, qubits)
338 for pauli, coeff in self._dict.items()
339 )
340 return sum(
341 complex(coeff) * pauli.state_expectation(state)
342 for pauli, coeff in self._dict.items()
343 )
345 def compress(self, abs_tol: float = 1e-10) -> None:
346 """Substitutes all free symbols in the QubitPauliOperator with
347 1, and then removes imaginary and real components which have
348 magnitudes below the tolerance. If the resulting expression is
349 0, the term is removed entirely.
351 Warning: This methods assumes significant expression structure
352 is known a priori, and is best suited to operators which have
353 simple product expressions, such as excitation operators for
354 VQE ansätze and digital quantum simulation. Otherwise, it may
355 remove terms relevant to computation. Each expression is of
356 the form :math:`f(a_1,a_2,\\ldots,a_n)` for some symbols
357 :math:`a_i`. :math:`|f(a_1,a_2,\\ldots,a_n)|` is assumed to
358 monotonically increase in both real and imaginary components
359 for all :math:`a_i \\in [0, 1]`.
361 :param abs_tol: The threshold below which to remove values.
362 """
364 to_delete = []
365 for key, value in self._dict.items():
366 placeholder = value.subs(dict.fromkeys(value.free_symbols, 1))
367 if abs(re(placeholder)) <= abs_tol:
368 if abs(im(placeholder)) <= abs_tol:
369 to_delete.append(key)
370 else:
371 self._dict[key] = im(value) * 1j
372 elif abs(im(placeholder)) <= abs_tol: 372 ↛ 365line 372 didn't jump to line 365 because the condition on line 372 was always true
373 self._dict[key] = re(value)
375 for key in to_delete:
376 del self._dict[key]
378 def _collect_qubits(self) -> None:
379 self._all_qubits: set[Qubit] = set()
380 for key in self._dict.keys(): # noqa: SIM118
381 for q in key.map.keys(): # noqa: SIM118
382 self._all_qubits.add(q)