Coverage for qml_essentials / utils.py: 87%

156 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-30 11:43 +0000

1from typing import List, Tuple, Optional 

2import jax 

3import jax.numpy as jnp 

4from qml_essentials.operations import ( 

5 _cdtype, 

6 Operation, 

7 PauliX, 

8 PauliY, 

9 PauliZ, 

10 H, 

11 S, 

12 CX, 

13 CZ, 

14 RX, 

15 RY, 

16 RZ, 

17 PauliRot, 

18 Barrier, 

19 evolve_pauli_with_clifford, 

20 pauli_decompose, 

21 pauli_string_from_operation, 

22) 

23from scipy.linalg import logm 

24from collections import defaultdict 

25 

26 

27def safe_random_split(random_key: jax.random.PRNGKey, *args, **kwargs): 

28 if random_key is None: 

29 return None, None 

30 else: 

31 return jax.random.split(random_key, *args, **kwargs) 

32 

33 

34class PauliTape: 

35 """Simple tape wrapper with ``operations``, ``observables``, and 

36 ``get_parameters`` — replacing PennyLane's ``Script`` for the 

37 Fourier-tree algorithm. 

38 """ 

39 

40 def __init__( 

41 self, 

42 operations: List[Operation], 

43 observables: List[Operation], 

44 ) -> None: 

45 self.operations = operations 

46 self.observables = observables 

47 

48 def get_parameters(self) -> list: 

49 """Return the list of all parameter values from the operations.""" 

50 params = [] 

51 for op in self.operations: 

52 params.extend(op.parameters) 

53 return params 

54 

55 def get_input_indices(self) -> list: 

56 indices = defaultdict(list) 

57 all_indices = [] 

58 ops_w_params = [o for o in self.operations if len(o.parameters) > 0] 

59 for i, op in enumerate(ops_w_params): 

60 if op.input_idx >= 0: 

61 indices[op.input_idx].append(i) 

62 all_indices.append(i) 

63 return indices, all_indices 

64 

65 

66class PauliCircuit: 

67 """ 

68 Wrapper for Pauli-Clifford Circuits described by Nemkov et al. 

69 (https://doi.org/10.1103/PhysRevA.108.032406). The code is inspired 

70 by the corresponding implementation: https://github.com/idnm/FourierVQA. 

71 

72 A Pauli Circuit only consists of parameterised Pauli-rotations and Clifford 

73 gates, which is the default for the most common VQCs. 

74 """ 

75 

76 CLIFFORD_GATES = ( 

77 PauliX, 

78 PauliY, 

79 PauliZ, 

80 H, 

81 S, 

82 CX, 

83 ) 

84 

85 PAULI_ROTATION_GATES = ( 

86 RX, 

87 RY, 

88 RZ, 

89 PauliRot, 

90 ) 

91 

92 SKIPPABLE_OPERATIONS = (Barrier,) 

93 

94 @staticmethod 

95 def from_parameterised_circuit( 

96 tape: List[Operation], 

97 observables: Optional[List[Operation]] = None, 

98 ) -> PauliTape: 

99 """ 

100 Transforms a list of operations into a Pauli-Clifford circuit. 

101 

102 Args: 

103 tape: List of operations recorded from the circuit. 

104 observables: List of observable operations. If ``None``, defaults 

105 to ``[PauliZ(0)]``. 

106 

107 Returns: 

108 PauliTape: 

109 A new tape containing the operations of the Pauli-Clifford 

110 circuit and the (possibly Clifford-evolved) observables. 

111 """ 

112 if observables is None: 

113 observables = [] 

114 

115 operations = PauliCircuit.get_clifford_pauli_gates(tape) 

116 

117 pauli_gates, final_cliffords = PauliCircuit.commute_all_cliffords_to_the_end( 

118 operations 

119 ) 

120 

121 observables = PauliCircuit.cliffords_in_observable(final_cliffords, observables) 

122 

123 return PauliTape(operations=pauli_gates, observables=observables) 

124 

125 @staticmethod 

126 def commute_all_cliffords_to_the_end( 

127 operations: List[Operation], 

128 ) -> Tuple[List[Operation], List[Operation]]: 

129 """ 

130 This function moves all clifford gates to the end of the circuit, 

131 accounting for commutation rules. 

132 

133 Args: 

134 operations (List[Operator]): The operations in the tape of the 

135 circuit 

136 

137 Returns: 

138 Tuple[List[Operator], List[Operator]]: 

139 - List of the resulting Pauli-rotations 

140 - List of the resulting Clifford gates 

141 """ 

142 first_clifford = -1 

143 for i in range(len(operations) - 2, -1, -1): 

144 j = i 

145 while ( 

146 j + 1 < len(operations) # Clifford has not alredy reached the end 

147 and PauliCircuit._is_clifford(operations[j]) 

148 and PauliCircuit._is_pauli_rotation(operations[j + 1]) 

149 ): 

150 pauli, clifford = PauliCircuit._evolve_clifford_rotation( 

151 operations[j], operations[j + 1] 

152 ) 

153 operations[j] = pauli 

154 operations[j + 1] = clifford 

155 j += 1 

156 first_clifford = j 

157 

158 # No Clifford gates are in the circuit 

159 if not PauliCircuit._is_clifford(operations[-1]): 

160 return operations, [] 

161 

162 pauli_rotations = operations[:first_clifford] 

163 clifford_gates = operations[first_clifford:] 

164 

165 return pauli_rotations, clifford_gates 

166 

167 @staticmethod 

168 def get_clifford_pauli_gates(tape: List[Operation]) -> List[Operation]: 

169 """ 

170 This function decomposes all gates in the circuit to clifford and 

171 pauli-rotation gates. 

172 

173 Args: 

174 tape: List of operations recorded from the circuit. 

175 

176 Returns: 

177 List[Operation]: A list of operations consisting only of clifford 

178 and Pauli-rotation gates. 

179 """ 

180 from qml_essentials.operations import Rot, CRX, CRY, CRZ 

181 

182 operations = [] 

183 for operation in tape: 

184 if PauliCircuit._is_clifford(operation) or PauliCircuit._is_pauli_rotation( 

185 operation 

186 ): 

187 operations.append(operation) 

188 elif PauliCircuit._is_skippable(operation): 

189 continue 

190 elif isinstance(operation, Rot): 

191 w = operation.wires[0] 

192 operations.append(RZ(operation.phi, wires=w)) 

193 operations.append(RY(operation.theta, wires=w)) 

194 operations.append(RZ(operation.omega, wires=w)) 

195 elif isinstance(operation, CRZ): 

196 c, t = operation.wires 

197 theta = operation.theta 

198 operations.append(RZ(theta / 2, wires=t)) 

199 operations.append(CX(wires=[c, t])) 

200 operations.append(RZ(-theta / 2, wires=t)) 

201 operations.append(CX(wires=[c, t])) 

202 elif isinstance(operation, CRX): 

203 c, t = operation.wires 

204 theta = operation.theta 

205 operations.append(H(wires=t)) 

206 operations.append(RZ(theta / 2, wires=t)) 

207 operations.append(CX(wires=[c, t])) 

208 operations.append(RZ(-theta / 2, wires=t)) 

209 operations.append(CX(wires=[c, t])) 

210 operations.append(H(wires=t)) 

211 elif isinstance(operation, CRY): 

212 c, t = operation.wires 

213 theta = operation.theta 

214 operations.append(RX(-jnp.pi / 2, wires=t)) 

215 operations.append(RZ(theta / 2, wires=t)) 

216 operations.append(CX(wires=[c, t])) 

217 operations.append(RZ(-theta / 2, wires=t)) 

218 operations.append(CX(wires=[c, t])) 

219 operations.append(RX(jnp.pi / 2, wires=t)) 

220 elif isinstance(operation, CZ): 

221 c, t = operation.wires 

222 operations.append(H(wires=c)) 

223 operations.append(CX(wires=[c, t])) 

224 operations.append(H(wires=c)) 

225 else: 

226 raise NotImplementedError( 

227 f"Gate {operation.name} cannot be decomposed into " 

228 "Pauli rotations and Clifford gates. Consider using a " 

229 "circuit ansatz that only uses RX, RY, RZ, PauliRot, " 

230 "Rot, and standard Clifford gates." 

231 ) 

232 

233 return operations 

234 

235 @staticmethod 

236 def _is_skippable(operation: Operation) -> bool: 

237 """ 

238 Determines is an operator can be ignored when building the Pauli 

239 Clifford circuit. Currently this only contains barriers. 

240 

241 Args: 

242 operation (Operation): Gate operation 

243 

244 Returns: 

245 bool: Whether the operation can be skipped. 

246 """ 

247 return isinstance(operation, PauliCircuit.SKIPPABLE_OPERATIONS) 

248 

249 @staticmethod 

250 def _is_clifford(operation: Operation) -> bool: 

251 """ 

252 Determines is an operator is a Clifford gate. 

253 

254 Args: 

255 operation (Operation): Gate operation 

256 

257 Returns: 

258 bool: Whether the operation is Clifford. 

259 """ 

260 return isinstance(operation, PauliCircuit.CLIFFORD_GATES) 

261 

262 @staticmethod 

263 def _is_pauli_rotation(operation: Operation) -> bool: 

264 """ 

265 Determines is an operator is a Pauli rotation gate. 

266 

267 Args: 

268 operation (Operation): Gate operation 

269 

270 Returns: 

271 bool: Whether the operation is a Pauli operation. 

272 """ 

273 return isinstance(operation, PauliCircuit.PAULI_ROTATION_GATES) 

274 

275 @staticmethod 

276 def _evolve_clifford_rotation( 

277 clifford: Operation, pauli: Operation 

278 ) -> Tuple[Operation, Operation]: 

279 """ 

280 This function computes the resulting operations, when switching a 

281 Clifford gate and a Pauli rotation in the circuit. 

282 

283 Example: 

284 Consider a circuit consisting of the gate sequence 

285 ... --- H --- R_z --- ... 

286 This function computes the evolved Pauli Rotation, and moves the 

287 clifford (Hadamard) gate to the end: 

288 ... --- R_x --- H --- ... 

289 

290 Args: 

291 clifford (Operation): Clifford gate to move. 

292 pauli (Operation): Pauli rotation gate to move the clifford past. 

293 

294 Returns: 

295 Tuple[Operation, Operation]: 

296 - Evolved Pauli rotation operator 

297 - Resulting Clifford operator (should be the same as the input) 

298 """ 

299 

300 if not any(p_c in clifford.wires for p_c in pauli.wires): 

301 return pauli, clifford 

302 

303 gen = pauli.generator() 

304 param = pauli.parameters[0] 

305 

306 evolved_gen = evolve_pauli_with_clifford(clifford, gen, adjoint_left=False) 

307 qubits = evolved_gen.wires 

308 _coeff, evolved_pauli_op = pauli_decompose( 

309 evolved_gen.matrix, wire_order=qubits 

310 ) 

311 

312 pauli_str = pauli_string_from_operation(evolved_pauli_op) 

313 # The coefficient from the decomposition determines if there's a sign 

314 # flip (param_factor). For Pauli evolution the coefficient is ±1. 

315 param_factor = float(jnp.real(_coeff)) 

316 

317 pauli_str, qubits = PauliCircuit._remove_identities_from_paulistr( 

318 pauli_str, evolved_pauli_op.wires 

319 ) 

320 new_pauli = PauliRot(param * param_factor, pauli_str, qubits) 

321 

322 if pauli.input_idx >= 0: 

323 new_pauli.input_idx = pauli.input_idx 

324 

325 return new_pauli, clifford 

326 

327 @staticmethod 

328 def _remove_identities_from_paulistr( 

329 pauli_str: str, qubits: List[int] 

330 ) -> Tuple[str, List[int]]: 

331 """ 

332 Removes identities from Pauli string and its corresponding qubits. 

333 

334 Args: 

335 pauli_str (str): Pauli string 

336 qubits (List[int]): Corresponding qubit indices. 

337 

338 Returns: 

339 Tuple[str, List[int]]: 

340 - Pauli string without identities 

341 - Qubits indices without the identities 

342 """ 

343 

344 reduced_qubits = [] 

345 reduced_pauli_str = "" 

346 for i, p in enumerate(pauli_str): 

347 if p != "I": 

348 reduced_pauli_str += p 

349 reduced_qubits.append(qubits[i]) 

350 

351 return reduced_pauli_str, reduced_qubits 

352 

353 @staticmethod 

354 def _evolve_clifford_pauli( 

355 clifford: Operation, pauli: Operation, adjoint_left: bool = True 

356 ) -> Tuple[Operation, Operation]: 

357 """ 

358 This function computes the resulting operation, when evolving a Pauli 

359 Operation with a Clifford operation. 

360 For a Clifford operator C and a Pauli operator P, this function computes: 

361 P' = C† P C (adjoint_left=True) 

362 P' = C P C† (adjoint_left=False) 

363 

364 Args: 

365 clifford (Operation): Clifford gate 

366 pauli (Operation): Pauli gate 

367 adjoint_left (bool, optional): If adjoint of the clifford gate is 

368 applied to the left. Defaults to True. 

369 

370 Returns: 

371 Tuple[Operation, Operation]: 

372 - Evolved Pauli operator 

373 - Resulting Clifford operator (same as input) 

374 """ 

375 if not any(p_c in clifford.wires for p_c in pauli.wires): 

376 return pauli, clifford 

377 

378 evolved = evolve_pauli_with_clifford(clifford, pauli, adjoint_left=adjoint_left) 

379 return evolved, clifford 

380 

381 @staticmethod 

382 def _evolve_cliffords_list( 

383 cliffords: List[Operation], pauli: Operation 

384 ) -> Operation: 

385 """ 

386 This function evolves a Pauli operation according to a sequence of 

387 cliffords. 

388 

389 Args: 

390 cliffords (List[Operation]): Clifford gates 

391 pauli (Operation): Pauli gate 

392 

393 Returns: 

394 Operation: Evolved Pauli operator 

395 """ 

396 for clifford in cliffords[::-1]: 

397 pauli, _ = PauliCircuit._evolve_clifford_pauli(clifford, pauli) 

398 qubits = pauli.wires 

399 _coeff, pauli = pauli_decompose(pauli.matrix, wire_order=qubits) 

400 

401 return pauli 

402 

403 @staticmethod 

404 def cliffords_in_observable( 

405 operations: List[Operation], original_obs: List[Operation] 

406 ) -> List[Operation]: 

407 """ 

408 Integrates Clifford gates in the observables of the original ansatz. 

409 

410 Args: 

411 operations (List[Operation]): Clifford gates 

412 original_obs (List[Operation]): Original observables from the 

413 circuit 

414 

415 Returns: 

416 List[Operation]: Observables with Clifford operations 

417 """ 

418 observables = [] 

419 for ob in original_obs: 

420 clifford_obs = PauliCircuit._evolve_cliffords_list(operations, ob) 

421 observables.append(clifford_obs) 

422 return observables