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

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. 

14 

15import copy 

16from typing import TYPE_CHECKING, Any, Union, cast 

17 

18import numpy 

19import numpy as np 

20from sympy import Expr, Float, I, Integer, Symbol, im, re 

21 

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 

25 

26CoeffTypeAccepted = Union[int, float, complex, Expr] # noqa: UP007 

27 

28if TYPE_CHECKING: 

29 from scipy.sparse import csc_matrix 

30 

31 

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) 

42 

43 

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. 

48 

49 

50def _expr_to_str(e: Expr) -> str: 

51 return cast("str", Circuit().add_phase(e).to_dict()["phase"]) 

52 

53 

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 

66 

67 

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. 

74 

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}`. 

79 

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. 

87 

88 In both cases, :math:`H` may be represented by a 

89 QubitPauliOperator. 

90 """ 

91 

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() 

101 

102 def __repr__(self) -> str: 

103 return self._dict.__repr__() 

104 

105 def __getitem__(self, key: QubitPauliString) -> Expr: 

106 return self._dict[key] 

107 

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)) 

113 

114 def __setitem__(self, key: QubitPauliString, value: CoeffTypeAccepted) -> None: 

115 """Update value in dictionary ([]). Automatically converts value into sympy 

116 Expr. 

117 

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()) 

123 

124 def __getstate__(self) -> dict[QubitPauliString, Expr]: 

125 return self._dict 

126 

127 def __setstate__(self, _dict: dict[QubitPauliString, Expr]) -> None: 

128 # values assumed to be already sympified 

129 self._dict = _dict 

130 self._collect_qubits() 

131 

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 

136 

137 def __hash__(self) -> int: 

138 return hash(self._dict) 

139 

140 def __iadd__(self, addend: "QubitPauliOperator") -> "QubitPauliOperator": 

141 """In-place addition (+=) of QubitPauliOperators. 

142 

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.") 

152 

153 return self 

154 

155 def __add__(self, addend: "QubitPauliOperator") -> "QubitPauliOperator": 

156 """Addition (+) of QubitPauliOperators. 

157 

158 :param addend: The operator to add 

159 :return: Sum operator 

160 """ 

161 summand = copy.deepcopy(self) 

162 summand += addend 

163 return summand 

164 

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. 

170 

171 :param multiplier: The operator or scalar to multiply 

172 :return: Updated operator (self) 

173 """ 

174 

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 

182 

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 

191 

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 

197 

198 # Invalid multiplier type 

199 raise TypeError(f"Cannot multiply QubitPauliOperator with {type(multiplier)}") 

200 

201 def __mul__( 

202 self, multiplier: Union[float, Expr, "QubitPauliOperator"] 

203 ) -> "QubitPauliOperator": 

204 """Multiplication (*) by QubitPauliOperator or scalar. 

205 

206 :param multiplier: The scalar to multiply by 

207 :return: Product operator 

208 """ 

209 product = copy.deepcopy(self) 

210 product *= multiplier 

211 return product 

212 

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. 

218 

219 :param multiplier: The scalar to multiply by 

220 :return: Product operator 

221 """ 

222 return self.__mul__(_coeff_convert(multiplier)) 

223 

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 

231 

232 def subs(self, symbol_dict: dict[Symbol, complex]) -> None: 

233 """Substitutes any matching symbols in the QubitPauliOperator. 

234 

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) 

239 

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. 

244 

245 :return: A dict of Pauli strings and their coefficients 

246 as key-value pairs 

247 """ 

248 return self._dict 

249 

250 def to_list(self) -> list[dict[str, Any]]: 

251 """Generate a list serialized representation of QubitPauliOperator, 

252 suitable for writing to JSON. 

253 

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 

270 

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() 

275 

276 :return: New QubitPauliOperator instance. 

277 """ 

278 

279 def get_qps(obj: dict[str, Any]) -> QubitPauliString: 

280 return QubitPauliString.from_list(obj["string"]) 

281 

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)) 

287 

288 return QubitPauliOperator({get_qps(obj): get_coeff(obj) for obj in pauli_list}) 

289 

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. 

293 

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. 

303 

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 ) 

319 

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``. 

325 

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. 

330 

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 

347 

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``. 

353 

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. 

358 

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 ) 

373 

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. 

379 

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]`. 

389 

390 :param abs_tol: The threshold below which to remove values. 

391 """ 

392 

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) 

403 

404 for key in to_delete: 

405 del self._dict[key] 

406 

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)