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

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 

17 

18import numpy 

19import numpy as np 

20from sympy import Expr, Symbol, im, re, sympify 

21 

22from pytket.circuit import 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: 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 

30 

31 

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 

37 

38 

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. 

45 

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

50 

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. 

58 

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

60 QubitPauliOperator. 

61 """ 

62 

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

72 

73 def __repr__(self) -> str: 

74 return self._dict.__repr__() 

75 

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

77 return self._dict[key] 

78 

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

84 

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

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

87 Expr. 

88 

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

96 

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

98 return self._dict 

99 

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

101 # values assumed to be already sympified 

102 self._dict = _dict 

103 self._collect_qubits() 

104 

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 

109 

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

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

112 

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

124 

125 return self 

126 

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

128 """Addition (+) of QubitPauliOperators. 

129 

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 

138 

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. 

144 

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

150 

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 

158 

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 

167 

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 

173 

174 # Invalid multiplier type 

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

176 

177 def __mul__( 

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

179 ) -> "QubitPauliOperator": 

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

181 

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 

190 

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. 

196 

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

203 

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) 

209 

210 :rtype: Set[Qubit] 

211 """ 

212 return self._all_qubits 

213 

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

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

216 

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) 

222 

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. 

227 

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

229 as key-value pairs 

230 """ 

231 return self._dict 

232 

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

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

235 suitable for writing to JSON. 

236 

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 

254 

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

259 

260 :return: New QubitPauliOperator instance. 

261 :rtype: QubitPauliOperator 

262 """ 

263 

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

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

266 

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

272 

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

274 

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. 

278 

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. 

288 

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 ) 

306 

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

312 

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. 

317 

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 

337 

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

343 

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. 

348 

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 ) 

366 

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. 

372 

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

382 

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

384 :type abs_tol: float 

385 """ 

386 

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) 

397 

398 for key in to_delete: 

399 del self._dict[key] 

400 

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)