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