Coverage for /home/runner/work/tket/tket/pytket/pytket/utils/operators.py: 90%

137 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-25 16:00 +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 :param value: Associated coefficient 

91 """ 

92 self._dict[key] = _coeff_convert(value) 

93 self._all_qubits.update(key.map.keys()) 

94 

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

96 return self._dict 

97 

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

99 # values assumed to be already sympified 

100 self._dict = _dict 

101 self._collect_qubits() 

102 

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 

107 

108 def __hash__(self) -> int: 

109 return hash(self._dict) 

110 

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

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

113 

114 :param addend: The operator to add 

115 :return: Updated operator (self) 

116 """ 

117 if isinstance(addend, QubitPauliOperator): 117 ↛ 122line 117 didn't jump to line 122 because the condition on line 117 was always true

118 for key, value in addend._dict.items(): 

119 self[key] = self.get(key, 0.0) + value 

120 self._all_qubits.update(addend._all_qubits) 

121 else: 

122 raise TypeError(f"Cannot add {type(addend)} to QubitPauliOperator.") 

123 

124 return self 

125 

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

127 """Addition (+) of QubitPauliOperators. 

128 

129 :param addend: The operator to add 

130 :return: Sum operator 

131 """ 

132 summand = copy.deepcopy(self) 

133 summand += addend 

134 return summand 

135 

136 def __imul__( 

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

138 ) -> "QubitPauliOperator": 

139 """In-place multiplication (*=) with QubitPauliOperator or scalar. 

140 Multiply coefficients and terms. 

141 

142 :param multiplier: The operator or scalar to multiply 

143 :return: Updated operator (self) 

144 """ 

145 

146 # Handle operator of the same type 

147 if isinstance(multiplier, QubitPauliOperator): 

148 result_terms: dict = {} 

149 for left_key, left_value in self._dict.items(): 

150 for right_key, right_value in multiplier._dict.items(): 

151 new_term, bonus_coeff = pauli_string_mult(left_key, right_key) 

152 new_coefficient = bonus_coeff * left_value * right_value 

153 

154 # Update result dict. 

155 if new_term in result_terms: 155 ↛ 156line 155 didn't jump to line 156 because the condition on line 155 was never true

156 result_terms[new_term] += new_coefficient 

157 else: 

158 result_terms[new_term] = new_coefficient 

159 self._dict = result_terms 

160 self._all_qubits.update(multiplier._all_qubits) 

161 return self 

162 

163 # Handle scalars. 

164 if isinstance(multiplier, float | Expr): 164 ↛ 170line 164 didn't jump to line 170 because the condition on line 164 was always true

165 for key in self._dict: 

166 self[key] *= multiplier 

167 return self 

168 

169 # Invalid multiplier type 

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

171 

172 def __mul__( 

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

174 ) -> "QubitPauliOperator": 

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

176 

177 :param multiplier: The scalar to multiply by 

178 :return: Product operator 

179 """ 

180 product = copy.deepcopy(self) 

181 product *= multiplier 

182 return product 

183 

184 def __rmul__(self, multiplier: CoeffTypeAccepted) -> "QubitPauliOperator": 

185 """Multiplication (*) by a scalar. 

186 We only define __rmul__ for scalars because left multiply is 

187 queried as default behaviour, and is used for 

188 QubitPauliOperator*QubitPauliOperator. 

189 

190 :param multiplier: The scalar to multiply by 

191 :return: Product operator 

192 """ 

193 return self.__mul__(_coeff_convert(multiplier)) 

194 

195 @property 

196 def all_qubits(self) -> set[Qubit]: 

197 """ 

198 The set of all qubits the operator ranges over (including qubits 

199 that were provided explicitly as identities) 

200 """ 

201 return self._all_qubits 

202 

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

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

205 

206 :param symbol_dict: A dictionary of symbols to fixed values. 

207 """ 

208 for key, value in self._dict.items(): 

209 self._dict[key] = value.subs(symbol_dict) 

210 

211 def get_dict(self) -> dict[QubitPauliString, Expr]: 

212 """Generate a dict representation of QubitPauliOperator, 

213 mapping each :py:class:`~.QubitPauliString` in the support 

214 to its corresponding value. 

215 

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

217 as key-value pairs 

218 """ 

219 return self._dict 

220 

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

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

223 suitable for writing to JSON. 

224 

225 :return: JSON serializable list of dictionaries. 

226 """ 

227 ret: list[dict[str, Any]] = [] 

228 for k, v in self._dict.items(): 

229 try: 

230 coeff = complex_to_list(complex(v)) 

231 except TypeError: 

232 assert isinstance(Expr(v), Expr) 

233 coeff = str(v) 

234 ret.append( 

235 { 

236 "string": k.to_list(), 

237 "coefficient": coeff, 

238 } 

239 ) 

240 return ret 

241 

242 @classmethod 

243 def from_list(cls, pauli_list: list[dict[str, Any]]) -> "QubitPauliOperator": 

244 """Construct a QubitPauliOperator from a serializable JSON list format, 

245 as returned by QubitPauliOperator.to_list() 

246 

247 :return: New QubitPauliOperator instance. 

248 """ 

249 

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

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

252 

253 def get_coeff(obj: dict[str, Any]) -> Expr: 

254 coeff = obj["coefficient"] 

255 if type(coeff) is str: 

256 return _coeff_convert(coeff) 

257 return _coeff_convert(list_to_complex(coeff)) 

258 

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

260 

261 def to_sparse_matrix(self, qubits: list[Qubit] | int | None = None) -> "csc_matrix": 

262 """Represents the sparse operator as a dense operator under the ordering 

263 scheme specified by ``qubits``, and generates the corresponding matrix. 

264 

265 - When ``qubits`` is an explicit list, the qubits are ordered with 

266 ``qubits[0]`` as the most significant qubit for indexing into the matrix. 

267 - If ``None``, then no padding qubits are introduced and we use the ILO-BE 

268 convention, e.g. ``Qubit("a", 0)`` is more significant than 

269 ``Qubit("a", 1)`` or ``Qubit("b")``. 

270 - Giving a number specifies the number of qubits to use in the final 

271 operator, treated as sequentially indexed from 0 in the default register 

272 (padding with identities as necessary) and ordered by ILO-BE so 

273 ``Qubit(0)`` is the most significant. 

274 

275 :param qubits: Sequencing of qubits in the matrix, either as an explicit 

276 list, number of qubits to pad to, or infer from the operator. 

277 Defaults to None 

278 :return: A sparse matrix representation of the operator. 

279 """ 

280 if qubits is None: 

281 qubits_ = sorted(list(self._all_qubits)) # noqa: C414 

282 return sum( 

283 complex(coeff) * pauli.to_sparse_matrix(qubits_) 

284 for pauli, coeff in self._dict.items() 

285 ) 

286 return sum( 

287 complex(coeff) * pauli.to_sparse_matrix(qubits) 

288 for pauli, coeff in self._dict.items() 

289 ) 

290 

291 def dot_state( 

292 self, state: np.ndarray, qubits: list[Qubit] | None = None 

293 ) -> np.ndarray: 

294 """Applies the operator to the given state, mapping qubits to indexes 

295 according to ``qubits``. 

296 

297 - When ``qubits`` is an explicit list, the qubits are ordered with 

298 ``qubits[0]`` as the most significant qubit for indexing into ``state``. 

299 - If ``None``, qubits sequentially indexed from 0 in the default register 

300 and ordered by ILO-BE so ``Qubit(0)`` is the most significant. 

301 

302 :param state: The initial statevector 

303 :param qubits: Sequencing of qubits in ``state``, if not mapped to the 

304 default register. Defaults to None 

305 :return: The dot product of the operator with the statevector 

306 """ 

307 if qubits: 

308 product_sum = sum( 

309 complex(coeff) * pauli.dot_state(state, qubits) 

310 for pauli, coeff in self._dict.items() 

311 ) 

312 else: 

313 product_sum = sum( 

314 complex(coeff) * pauli.dot_state(state) 

315 for pauli, coeff in self._dict.items() 

316 ) 

317 return product_sum if isinstance(product_sum, numpy.ndarray) else state 

318 

319 def state_expectation( 

320 self, state: np.ndarray, qubits: list[Qubit] | None = None 

321 ) -> complex: 

322 """Calculates the expectation value of the given statevector with respect 

323 to the operator, mapping qubits to indexes according to ``qubits``. 

324 

325 - When ``qubits`` is an explicit list, the qubits are ordered with 

326 ``qubits[0]`` as the most significant qubit for indexing into ``state``. 

327 - If ``None``, qubits sequentially indexed from 0 in the default register 

328 and ordered by ILO-BE so ``Qubit(0)`` is the most significant. 

329 

330 :param state: The initial statevector 

331 :param qubits: Sequencing of qubits in ``state``, if not mapped to the 

332 default register. Defaults to None 

333 :return: The expectation value of the statevector and operator 

334 """ 

335 if qubits: 

336 return sum( 

337 complex(coeff) * pauli.state_expectation(state, qubits) 

338 for pauli, coeff in self._dict.items() 

339 ) 

340 return sum( 

341 complex(coeff) * pauli.state_expectation(state) 

342 for pauli, coeff in self._dict.items() 

343 ) 

344 

345 def compress(self, abs_tol: float = 1e-10) -> None: 

346 """Substitutes all free symbols in the QubitPauliOperator with 

347 1, and then removes imaginary and real components which have 

348 magnitudes below the tolerance. If the resulting expression is 

349 0, the term is removed entirely. 

350 

351 Warning: This methods assumes significant expression structure 

352 is known a priori, and is best suited to operators which have 

353 simple product expressions, such as excitation operators for 

354 VQE ansätze and digital quantum simulation. Otherwise, it may 

355 remove terms relevant to computation. Each expression is of 

356 the form :math:`f(a_1,a_2,\\ldots,a_n)` for some symbols 

357 :math:`a_i`. :math:`|f(a_1,a_2,\\ldots,a_n)|` is assumed to 

358 monotonically increase in both real and imaginary components 

359 for all :math:`a_i \\in [0, 1]`. 

360 

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

362 """ 

363 

364 to_delete = [] 

365 for key, value in self._dict.items(): 

366 placeholder = value.subs(dict.fromkeys(value.free_symbols, 1)) 

367 if abs(re(placeholder)) <= abs_tol: 

368 if abs(im(placeholder)) <= abs_tol: 

369 to_delete.append(key) 

370 else: 

371 self._dict[key] = im(value) * 1j 

372 elif abs(im(placeholder)) <= abs_tol: 372 ↛ 365line 372 didn't jump to line 365 because the condition on line 372 was always true

373 self._dict[key] = re(value) 

374 

375 for key in to_delete: 

376 del self._dict[key] 

377 

378 def _collect_qubits(self) -> None: 

379 self._all_qubits: set[Qubit] = set() 

380 for key in self._dict.keys(): # noqa: SIM118 

381 for q in key.map.keys(): # noqa: SIM118 

382 self._all_qubits.add(q)