Coverage for /home/runner/work/tket/tket/pytket/pytket/utils/operators.py: 92%
144 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-10 11:51 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-10 11:51 +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, cast
18import numpy
19import numpy as np
20from sympy import Expr, Float, I, Integer, Symbol, im, re
22from pytket.circuit import Circuit, 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:
29 from scipy.sparse import csc_matrix
32def _coeff_convert(coeff: CoeffTypeAccepted) -> Expr:
33 if isinstance(coeff, Expr):
34 return coeff
35 if isinstance(coeff, int):
36 return Integer(coeff)
37 if isinstance(coeff, float):
38 return Float(coeff)
39 if isinstance(coeff, complex): 39 ↛ 41line 39 didn't jump to line 41 because the condition on line 39 was always true
40 return Float(coeff.real) + Float(coeff.imag) * I
41 return Expr(coeff)
44# The functions `_expr_to_str()` and `_str_to_expr()` are a hack to achieve
45# serialization and deserialization of expressions without having to invoke
46# `sympy.sympify()`, which is unsafe. Going via a `Circuit` serialization means we use
47# the C++ conversions instead. This is overkill but works.
50def _expr_to_str(e: Expr) -> str:
51 return cast("str", Circuit().add_phase(e).to_dict()["phase"])
54def _str_to_expr(s: str) -> Expr:
55 return Circuit.from_dict(
56 {
57 "bits": [],
58 "commands": [],
59 "created_qubits": [],
60 "discarded_qubits": [],
61 "implicit_permutation": [],
62 "phase": s,
63 "qubits": [],
64 }
65 ).phase
68class QubitPauliOperator:
69 """
70 Generic data structure for generation of circuits and expectation
71 value calculation. Contains a dictionary from QubitPauliString to
72 sympy Expr. Capacity for symbolic expressions allows the operator
73 to be used to generate ansätze for variational algorithms.
75 Represents a mathematical object :math:`\\sum_j \\alpha_j P_j`,
76 where each :math:`\\alpha_j` is a complex symbolic expression and
77 :math:`P_j` is a Pauli string, i.e. :math:`P_j \\in \\{ I, X, Y,
78 Z\\}^{\\otimes n}`.
80 A prototypical example is a molecular Hamiltonian, for which one
81 may wish to calculate the expectation value :math:`\\langle \\Psi
82 | H | \\Psi \\rangle` by decomposing :math:`H` into individual
83 Pauli measurements. Alternatively, one may wish to evolve a state
84 by the operator :math:`e^{-iHt}` for digital quantum simulation.
85 In this case, the whole operator must be decomposed into native
86 operations.
88 In both cases, :math:`H` may be represented by a
89 QubitPauliOperator.
90 """
92 def __init__(
93 self,
94 dictionary: dict[QubitPauliString, CoeffTypeAccepted] | None = None,
95 ) -> None:
96 self._dict: dict[QubitPauliString, Expr] = {}
97 if dictionary:
98 for key, value in dictionary.items():
99 self._dict[key] = _coeff_convert(value)
100 self._collect_qubits()
102 def __repr__(self) -> str:
103 return self._dict.__repr__()
105 def __getitem__(self, key: QubitPauliString) -> Expr:
106 return self._dict[key]
108 def get(self, key: QubitPauliString, default: CoeffTypeAccepted) -> Expr:
109 """
110 Get the coefficient of a particular string present in the operator.
111 """
112 return self._dict.get(key, _coeff_convert(default))
114 def __setitem__(self, key: QubitPauliString, value: CoeffTypeAccepted) -> None:
115 """Update value in dictionary ([]). Automatically converts value into sympy
116 Expr.
118 :param key: String to use as key
119 :param value: Associated coefficient
120 """
121 self._dict[key] = _coeff_convert(value)
122 self._all_qubits.update(key.map.keys())
124 def __getstate__(self) -> dict[QubitPauliString, Expr]:
125 return self._dict
127 def __setstate__(self, _dict: dict[QubitPauliString, Expr]) -> None:
128 # values assumed to be already sympified
129 self._dict = _dict
130 self._collect_qubits()
132 def __eq__(self, other: object) -> bool:
133 if isinstance(other, QubitPauliOperator): 133 ↛ 135line 133 didn't jump to line 135 because the condition on line 133 was always true
134 return self._dict == other._dict
135 return False
137 def __hash__(self) -> int:
138 return hash(self._dict)
140 def __iadd__(self, addend: "QubitPauliOperator") -> "QubitPauliOperator":
141 """In-place addition (+=) of QubitPauliOperators.
143 :param addend: The operator to add
144 :return: Updated operator (self)
145 """
146 if isinstance(addend, QubitPauliOperator): 146 ↛ 151line 146 didn't jump to line 151 because the condition on line 146 was always true
147 for key, value in addend._dict.items():
148 self[key] = self.get(key, 0.0) + value
149 self._all_qubits.update(addend._all_qubits)
150 else:
151 raise TypeError(f"Cannot add {type(addend)} to QubitPauliOperator.")
153 return self
155 def __add__(self, addend: "QubitPauliOperator") -> "QubitPauliOperator":
156 """Addition (+) of QubitPauliOperators.
158 :param addend: The operator to add
159 :return: Sum operator
160 """
161 summand = copy.deepcopy(self)
162 summand += addend
163 return summand
165 def __imul__(
166 self, multiplier: Union[float, Expr, "QubitPauliOperator"]
167 ) -> "QubitPauliOperator":
168 """In-place multiplication (*=) with QubitPauliOperator or scalar.
169 Multiply coefficients and terms.
171 :param multiplier: The operator or scalar to multiply
172 :return: Updated operator (self)
173 """
175 # Handle operator of the same type
176 if isinstance(multiplier, QubitPauliOperator):
177 result_terms: dict = {}
178 for left_key, left_value in self._dict.items():
179 for right_key, right_value in multiplier._dict.items():
180 new_term, bonus_coeff = pauli_string_mult(left_key, right_key)
181 new_coefficient = bonus_coeff * left_value * right_value
183 # Update result dict.
184 if new_term in result_terms: 184 ↛ 185line 184 didn't jump to line 185 because the condition on line 184 was never true
185 result_terms[new_term] += new_coefficient
186 else:
187 result_terms[new_term] = new_coefficient
188 self._dict = result_terms
189 self._all_qubits.update(multiplier._all_qubits)
190 return self
192 # Handle scalars.
193 if isinstance(multiplier, float | Expr): 193 ↛ 199line 193 didn't jump to line 199 because the condition on line 193 was always true
194 for key in self._dict:
195 self[key] *= multiplier
196 return self
198 # Invalid multiplier type
199 raise TypeError(f"Cannot multiply QubitPauliOperator with {type(multiplier)}")
201 def __mul__(
202 self, multiplier: Union[float, Expr, "QubitPauliOperator"]
203 ) -> "QubitPauliOperator":
204 """Multiplication (*) by QubitPauliOperator or scalar.
206 :param multiplier: The scalar to multiply by
207 :return: Product operator
208 """
209 product = copy.deepcopy(self)
210 product *= multiplier
211 return product
213 def __rmul__(self, multiplier: CoeffTypeAccepted) -> "QubitPauliOperator":
214 """Multiplication (*) by a scalar.
215 We only define __rmul__ for scalars because left multiply is
216 queried as default behaviour, and is used for
217 QubitPauliOperator*QubitPauliOperator.
219 :param multiplier: The scalar to multiply by
220 :return: Product operator
221 """
222 return self.__mul__(_coeff_convert(multiplier))
224 @property
225 def all_qubits(self) -> set[Qubit]:
226 """
227 The set of all qubits the operator ranges over (including qubits
228 that were provided explicitly as identities)
229 """
230 return self._all_qubits
232 def subs(self, symbol_dict: dict[Symbol, complex]) -> None:
233 """Substitutes any matching symbols in the QubitPauliOperator.
235 :param symbol_dict: A dictionary of symbols to fixed values.
236 """
237 for key, value in self._dict.items():
238 self._dict[key] = value.subs(symbol_dict)
240 def get_dict(self) -> dict[QubitPauliString, Expr]:
241 """Generate a dict representation of QubitPauliOperator,
242 mapping each :py:class:`~.QubitPauliString` in the support
243 to its corresponding value.
245 :return: A dict of Pauli strings and their coefficients
246 as key-value pairs
247 """
248 return self._dict
250 def to_list(self) -> list[dict[str, Any]]:
251 """Generate a list serialized representation of QubitPauliOperator,
252 suitable for writing to JSON.
254 :return: JSON serializable list of dictionaries.
255 """
256 ret: list[dict[str, Any]] = []
257 for k, v in self._dict.items():
258 try:
259 coeff = complex_to_list(complex(v))
260 except TypeError:
261 assert isinstance(v, Expr)
262 coeff = _expr_to_str(v)
263 ret.append(
264 {
265 "string": k.to_list(),
266 "coefficient": coeff,
267 }
268 )
269 return ret
271 @classmethod
272 def from_list(cls, pauli_list: list[dict[str, Any]]) -> "QubitPauliOperator":
273 """Construct a QubitPauliOperator from a serializable JSON list format,
274 as returned by QubitPauliOperator.to_list()
276 :return: New QubitPauliOperator instance.
277 """
279 def get_qps(obj: dict[str, Any]) -> QubitPauliString:
280 return QubitPauliString.from_list(obj["string"])
282 def get_coeff(obj: dict[str, Any]) -> Expr:
283 coeff = obj["coefficient"]
284 if type(coeff) is str:
285 return _str_to_expr(coeff)
286 return _coeff_convert(list_to_complex(coeff))
288 return QubitPauliOperator({get_qps(obj): get_coeff(obj) for obj in pauli_list})
290 def to_sparse_matrix(self, qubits: list[Qubit] | int | None = None) -> "csc_matrix":
291 """Represents the sparse operator as a dense operator under the ordering
292 scheme specified by ``qubits``, and generates the corresponding matrix.
294 - When ``qubits`` is an explicit list, the qubits are ordered with
295 ``qubits[0]`` as the most significant qubit for indexing into the matrix.
296 - If ``None``, then no padding qubits are introduced and we use the ILO-BE
297 convention, e.g. ``Qubit("a", 0)`` is more significant than
298 ``Qubit("a", 1)`` or ``Qubit("b")``.
299 - Giving a number specifies the number of qubits to use in the final
300 operator, treated as sequentially indexed from 0 in the default register
301 (padding with identities as necessary) and ordered by ILO-BE so
302 ``Qubit(0)`` is the most significant.
304 :param qubits: Sequencing of qubits in the matrix, either as an explicit
305 list, number of qubits to pad to, or infer from the operator.
306 Defaults to None
307 :return: A sparse matrix representation of the operator.
308 """
309 if qubits is None:
310 qubits_ = sorted(list(self._all_qubits)) # noqa: C414
311 return sum(
312 complex(coeff) * pauli.to_sparse_matrix(qubits_)
313 for pauli, coeff in self._dict.items()
314 )
315 return sum(
316 complex(coeff) * pauli.to_sparse_matrix(qubits)
317 for pauli, coeff in self._dict.items()
318 )
320 def dot_state(
321 self, state: np.ndarray, qubits: list[Qubit] | None = None
322 ) -> np.ndarray:
323 """Applies the operator to the given state, mapping qubits to indexes
324 according to ``qubits``.
326 - When ``qubits`` is an explicit list, the qubits are ordered with
327 ``qubits[0]`` as the most significant qubit for indexing into ``state``.
328 - If ``None``, qubits sequentially indexed from 0 in the default register
329 and ordered by ILO-BE so ``Qubit(0)`` is the most significant.
331 :param state: The initial statevector
332 :param qubits: Sequencing of qubits in ``state``, if not mapped to the
333 default register. Defaults to None
334 :return: The dot product of the operator with the statevector
335 """
336 if qubits:
337 product_sum = sum(
338 complex(coeff) * pauli.dot_state(state, qubits)
339 for pauli, coeff in self._dict.items()
340 )
341 else:
342 product_sum = sum(
343 complex(coeff) * pauli.dot_state(state)
344 for pauli, coeff in self._dict.items()
345 )
346 return product_sum if isinstance(product_sum, numpy.ndarray) else state
348 def state_expectation(
349 self, state: np.ndarray, qubits: list[Qubit] | None = None
350 ) -> complex:
351 """Calculates the expectation value of the given statevector with respect
352 to the operator, mapping qubits to indexes according to ``qubits``.
354 - When ``qubits`` is an explicit list, the qubits are ordered with
355 ``qubits[0]`` as the most significant qubit for indexing into ``state``.
356 - If ``None``, qubits sequentially indexed from 0 in the default register
357 and ordered by ILO-BE so ``Qubit(0)`` is the most significant.
359 :param state: The initial statevector
360 :param qubits: Sequencing of qubits in ``state``, if not mapped to the
361 default register. Defaults to None
362 :return: The expectation value of the statevector and operator
363 """
364 if qubits:
365 return sum(
366 complex(coeff) * pauli.state_expectation(state, qubits)
367 for pauli, coeff in self._dict.items()
368 )
369 return sum(
370 complex(coeff) * pauli.state_expectation(state)
371 for pauli, coeff in self._dict.items()
372 )
374 def compress(self, abs_tol: float = 1e-10) -> None:
375 """Substitutes all free symbols in the QubitPauliOperator with
376 1, and then removes imaginary and real components which have
377 magnitudes below the tolerance. If the resulting expression is
378 0, the term is removed entirely.
380 Warning: This methods assumes significant expression structure
381 is known a priori, and is best suited to operators which have
382 simple product expressions, such as excitation operators for
383 VQE ansätze and digital quantum simulation. Otherwise, it may
384 remove terms relevant to computation. Each expression is of
385 the form :math:`f(a_1,a_2,\\ldots,a_n)` for some symbols
386 :math:`a_i`. :math:`|f(a_1,a_2,\\ldots,a_n)|` is assumed to
387 monotonically increase in both real and imaginary components
388 for all :math:`a_i \\in [0, 1]`.
390 :param abs_tol: The threshold below which to remove values.
391 """
393 to_delete = []
394 for key, value in self._dict.items():
395 placeholder = value.subs(dict.fromkeys(value.free_symbols, 1))
396 if abs(re(placeholder)) <= abs_tol:
397 if abs(im(placeholder)) <= abs_tol:
398 to_delete.append(key)
399 else:
400 self._dict[key] = im(value) * 1j
401 elif abs(im(placeholder)) <= abs_tol: 401 ↛ 394line 401 didn't jump to line 394 because the condition on line 401 was always true
402 self._dict[key] = re(value)
404 for key in to_delete:
405 del self._dict[key]
407 def _collect_qubits(self) -> None:
408 self._all_qubits: set[Qubit] = set()
409 for key in self._dict.keys(): # noqa: SIM118
410 for q in key.map.keys(): # noqa: SIM118
411 self._all_qubits.add(q)