Coverage for /home/runner/work/tket/tket/pytket/pytket/utils/operators.py: 91%
135 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.
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 :type key: QubitPauliString
91 :param value: Associated coefficient
92 :type value: Union[int, float, complex, Expr]
93 """
94 self._dict[key] = _coeff_convert(value)
95 self._all_qubits.update(key.map.keys())
97 def __getstate__(self) -> dict[QubitPauliString, Expr]:
98 return self._dict
100 def __setstate__(self, _dict: dict[QubitPauliString, Expr]) -> None:
101 # values assumed to be already sympified
102 self._dict = _dict
103 self._collect_qubits()
105 def __eq__(self, other: object) -> bool:
106 if isinstance(other, QubitPauliOperator): 106 ↛ 108line 106 didn't jump to line 108 because the condition on line 106 was always true
107 return self._dict == other._dict
108 return False
110 def __iadd__(self, addend: "QubitPauliOperator") -> "QubitPauliOperator":
111 """In-place addition (+=) of QubitPauliOperators.
113 :param addend: The operator to add
114 :type addend: QubitPauliOperator
115 :return: Updated operator (self)
116 :rtype: QubitPauliOperator
117 """
118 if isinstance(addend, QubitPauliOperator): 118 ↛ 123line 118 didn't jump to line 123 because the condition on line 118 was always true
119 for key, value in addend._dict.items():
120 self[key] = self.get(key, 0.0) + value
121 self._all_qubits.update(addend._all_qubits)
122 else:
123 raise TypeError(f"Cannot add {type(addend)} to QubitPauliOperator.")
125 return self
127 def __add__(self, addend: "QubitPauliOperator") -> "QubitPauliOperator":
128 """Addition (+) of QubitPauliOperators.
130 :param addend: The operator to add
131 :type addend: QubitPauliOperator
132 :return: Sum operator
133 :rtype: QubitPauliOperator
134 """
135 summand = copy.deepcopy(self)
136 summand += addend
137 return summand
139 def __imul__(
140 self, multiplier: Union[float, Expr, "QubitPauliOperator"]
141 ) -> "QubitPauliOperator":
142 """In-place multiplication (*=) with QubitPauliOperator or scalar.
143 Multiply coefficients and terms.
145 :param multiplier: The operator or scalar to multiply
146 :type multiplier: Union[QubitPauliOperator, int, float, complex, Expr]
147 :return: Updated operator (self)
148 :rtype: QubitPauliOperator
149 """
151 # Handle operator of the same type
152 if isinstance(multiplier, QubitPauliOperator):
153 result_terms: dict = {}
154 for left_key, left_value in self._dict.items():
155 for right_key, right_value in multiplier._dict.items():
156 new_term, bonus_coeff = pauli_string_mult(left_key, right_key)
157 new_coefficient = bonus_coeff * left_value * right_value
159 # Update result dict.
160 if new_term in result_terms: 160 ↛ 161line 160 didn't jump to line 161 because the condition on line 160 was never true
161 result_terms[new_term] += new_coefficient
162 else:
163 result_terms[new_term] = new_coefficient
164 self._dict = result_terms
165 self._all_qubits.update(multiplier._all_qubits)
166 return self
168 # Handle scalars.
169 if isinstance(multiplier, float | Expr): 169 ↛ 175line 169 didn't jump to line 175 because the condition on line 169 was always true
170 for key in self._dict:
171 self[key] *= multiplier
172 return self
174 # Invalid multiplier type
175 raise TypeError(f"Cannot multiply QubitPauliOperator with {type(multiplier)}")
177 def __mul__(
178 self, multiplier: Union[float, Expr, "QubitPauliOperator"]
179 ) -> "QubitPauliOperator":
180 """Multiplication (*) by QubitPauliOperator or scalar.
182 :param multiplier: The scalar to multiply by
183 :type multiplier: Union[int, float, complex, Expr, QubitPauliOperator]
184 :return: Product operator
185 :rtype: QubitPauliOperator
186 """
187 product = copy.deepcopy(self)
188 product *= multiplier
189 return product
191 def __rmul__(self, multiplier: CoeffTypeAccepted) -> "QubitPauliOperator":
192 """Multiplication (*) by a scalar.
193 We only define __rmul__ for scalars because left multiply is
194 queried as default behaviour, and is used for
195 QubitPauliOperator*QubitPauliOperator.
197 :param multiplier: The scalar to multiply by
198 :type multiplier: Union[int, float, complex, Expr]
199 :return: Product operator
200 :rtype: QubitPauliOperator
201 """
202 return self.__mul__(_coeff_convert(multiplier))
204 @property
205 def all_qubits(self) -> set[Qubit]:
206 """
207 :return: The set of all qubits the operator ranges over (including qubits
208 that were provided explicitly as identities)
210 :rtype: Set[Qubit]
211 """
212 return self._all_qubits
214 def subs(self, symbol_dict: dict[Symbol, complex]) -> None:
215 """Substitutes any matching symbols in the QubitPauliOperator.
217 :param symbol_dict: A dictionary of symbols to fixed values.
218 :type symbol_dict: Dict[Symbol, complex]
219 """
220 for key, value in self._dict.items():
221 self._dict[key] = value.subs(symbol_dict)
223 def get_dict(self) -> dict[QubitPauliString, Expr]:
224 """Generate a dict representation of QubitPauliOperator,
225 mapping each :py:class:`QubitPauliString` in the support
226 to its corresponding value.
228 :return: A dict of Pauli strings and their coefficients
229 as key-value pairs
230 """
231 return self._dict
233 def to_list(self) -> list[dict[str, Any]]:
234 """Generate a list serialized representation of QubitPauliOperator,
235 suitable for writing to JSON.
237 :return: JSON serializable list of dictionaries.
238 :rtype: List[Dict[str, Any]]
239 """
240 ret: list[dict[str, Any]] = []
241 for k, v in self._dict.items():
242 try:
243 coeff = complex_to_list(complex(v))
244 except TypeError:
245 assert isinstance(Expr(v), Expr)
246 coeff = str(v)
247 ret.append(
248 {
249 "string": k.to_list(),
250 "coefficient": coeff,
251 }
252 )
253 return ret
255 @classmethod
256 def from_list(cls, pauli_list: list[dict[str, Any]]) -> "QubitPauliOperator":
257 """Construct a QubitPauliOperator from a serializable JSON list format,
258 as returned by QubitPauliOperator.to_list()
260 :return: New QubitPauliOperator instance.
261 :rtype: QubitPauliOperator
262 """
264 def get_qps(obj: dict[str, Any]) -> QubitPauliString:
265 return QubitPauliString.from_list(obj["string"])
267 def get_coeff(obj: dict[str, Any]) -> Expr:
268 coeff = obj["coefficient"]
269 if type(coeff) is str:
270 return _coeff_convert(coeff)
271 return _coeff_convert(list_to_complex(coeff))
273 return QubitPauliOperator({get_qps(obj): get_coeff(obj) for obj in pauli_list})
275 def to_sparse_matrix(self, qubits: list[Qubit] | int | None = None) -> "csc_matrix":
276 """Represents the sparse operator as a dense operator under the ordering
277 scheme specified by ``qubits``, and generates the corresponding matrix.
279 - When ``qubits`` is an explicit list, the qubits are ordered with
280 ``qubits[0]`` as the most significant qubit for indexing into the matrix.
281 - If ``None``, then no padding qubits are introduced and we use the ILO-BE
282 convention, e.g. ``Qubit("a", 0)`` is more significant than
283 ``Qubit("a", 1)`` or ``Qubit("b")``.
284 - Giving a number specifies the number of qubits to use in the final
285 operator, treated as sequentially indexed from 0 in the default register
286 (padding with identities as necessary) and ordered by ILO-BE so
287 ``Qubit(0)`` is the most significant.
289 :param qubits: Sequencing of qubits in the matrix, either as an explicit
290 list, number of qubits to pad to, or infer from the operator.
291 Defaults to None
292 :type qubits: Union[List[Qubit], int, None], optional
293 :return: A sparse matrix representation of the operator.
294 :rtype: csc_matrix
295 """
296 if qubits is None:
297 qubits_ = sorted(list(self._all_qubits)) # noqa: C414
298 return sum(
299 complex(coeff) * pauli.to_sparse_matrix(qubits_)
300 for pauli, coeff in self._dict.items()
301 )
302 return sum(
303 complex(coeff) * pauli.to_sparse_matrix(qubits)
304 for pauli, coeff in self._dict.items()
305 )
307 def dot_state(
308 self, state: np.ndarray, qubits: list[Qubit] | None = None
309 ) -> np.ndarray:
310 """Applies the operator to the given state, mapping qubits to indexes
311 according to ``qubits``.
313 - When ``qubits`` is an explicit list, the qubits are ordered with
314 ``qubits[0]`` as the most significant qubit for indexing into ``state``.
315 - If ``None``, qubits sequentially indexed from 0 in the default register
316 and ordered by ILO-BE so ``Qubit(0)`` is the most significant.
318 :param state: The initial statevector
319 :type state: numpy.ndarray
320 :param qubits: Sequencing of qubits in ``state``, if not mapped to the
321 default register. Defaults to None
322 :type qubits: Union[List[Qubit], None], optional
323 :return: The dot product of the operator with the statevector
324 :rtype: numpy.ndarray
325 """
326 if qubits:
327 product_sum = sum(
328 complex(coeff) * pauli.dot_state(state, qubits)
329 for pauli, coeff in self._dict.items()
330 )
331 else:
332 product_sum = sum(
333 complex(coeff) * pauli.dot_state(state)
334 for pauli, coeff in self._dict.items()
335 )
336 return product_sum if isinstance(product_sum, numpy.ndarray) else state
338 def state_expectation(
339 self, state: np.ndarray, qubits: list[Qubit] | None = None
340 ) -> complex:
341 """Calculates the expectation value of the given statevector with respect
342 to the operator, mapping qubits to indexes according to ``qubits``.
344 - When ``qubits`` is an explicit list, the qubits are ordered with
345 ``qubits[0]`` as the most significant qubit for indexing into ``state``.
346 - If ``None``, qubits sequentially indexed from 0 in the default register
347 and ordered by ILO-BE so ``Qubit(0)`` is the most significant.
349 :param state: The initial statevector
350 :type state: numpy.ndarray
351 :param qubits: Sequencing of qubits in ``state``, if not mapped to the
352 default register. Defaults to None
353 :type qubits: Union[List[Qubit], None], optional
354 :return: The expectation value of the statevector and operator
355 :rtype: complex
356 """
357 if qubits:
358 return sum(
359 complex(coeff) * pauli.state_expectation(state, qubits)
360 for pauli, coeff in self._dict.items()
361 )
362 return sum(
363 complex(coeff) * pauli.state_expectation(state)
364 for pauli, coeff in self._dict.items()
365 )
367 def compress(self, abs_tol: float = 1e-10) -> None:
368 """Substitutes all free symbols in the QubitPauliOperator with
369 1, and then removes imaginary and real components which have
370 magnitudes below the tolerance. If the resulting expression is
371 0, the term is removed entirely.
373 Warning: This methods assumes significant expression structure
374 is known a priori, and is best suited to operators which have
375 simple product expressions, such as excitation operators for
376 VQE ansätze and digital quantum simulation. Otherwise, it may
377 remove terms relevant to computation. Each expression is of
378 the form :math:`f(a_1,a_2,\\ldots,a_n)` for some symbols
379 :math:`a_i`. :math:`|f(a_1,a_2,\\ldots,a_n)|` is assumed to
380 monotonically increase in both real and imaginary components
381 for all :math:`a_i \\in [0, 1]`.
383 :param abs_tol: The threshold below which to remove values.
384 :type abs_tol: float
385 """
387 to_delete = []
388 for key, value in self._dict.items():
389 placeholder = value.subs(dict.fromkeys(value.free_symbols, 1))
390 if abs(re(placeholder)) <= abs_tol:
391 if abs(im(placeholder)) <= abs_tol:
392 to_delete.append(key)
393 else:
394 self._dict[key] = im(value) * 1j
395 elif abs(im(placeholder)) <= abs_tol: 395 ↛ 388line 395 didn't jump to line 388 because the condition on line 395 was always true
396 self._dict[key] = re(value)
398 for key in to_delete:
399 del self._dict[key]
401 def _collect_qubits(self) -> None:
402 self._all_qubits: set[Qubit] = set()
403 for key in self._dict.keys(): # noqa: SIM118
404 for q in key.map.keys(): # noqa: SIM118
405 self._all_qubits.add(q)