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

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 __iadd__(self, addend: "QubitPauliOperator") -> "QubitPauliOperator": 

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

110 

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

120 

121 return self 

122 

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

124 """Addition (+) of QubitPauliOperators. 

125 

126 :param addend: The operator to add 

127 :return: Sum operator 

128 """ 

129 summand = copy.deepcopy(self) 

130 summand += addend 

131 return summand 

132 

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. 

138 

139 :param multiplier: The operator or scalar to multiply 

140 :return: Updated operator (self) 

141 """ 

142 

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 

150 

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 

159 

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 

165 

166 # Invalid multiplier type 

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

168 

169 def __mul__( 

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

171 ) -> "QubitPauliOperator": 

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

173 

174 :param multiplier: The scalar to multiply by 

175 :return: Product operator 

176 """ 

177 product = copy.deepcopy(self) 

178 product *= multiplier 

179 return product 

180 

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. 

186 

187 :param multiplier: The scalar to multiply by 

188 :return: Product operator 

189 """ 

190 return self.__mul__(_coeff_convert(multiplier)) 

191 

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 

199 

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

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

202 

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) 

207 

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. 

212 

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

214 as key-value pairs 

215 """ 

216 return self._dict 

217 

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

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

220 suitable for writing to JSON. 

221 

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 

238 

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

243 

244 :return: New QubitPauliOperator instance. 

245 """ 

246 

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

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

249 

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

255 

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

257 

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. 

261 

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. 

271 

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 ) 

287 

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

293 

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. 

298 

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 

315 

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

321 

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. 

326 

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 ) 

341 

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. 

347 

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

357 

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

359 """ 

360 

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) 

371 

372 for key in to_delete: 

373 del self._dict[key] 

374 

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)