Coverage for qml_essentials / utils.py: 87%

155 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-05-16 10:19 +0000

1from typing import List, Tuple, Optional 

2import jax 

3import jax.numpy as jnp 

4from qml_essentials.operations import ( 

5 Operation, 

6 PauliX, 

7 PauliY, 

8 PauliZ, 

9 H, 

10 S, 

11 CX, 

12 CZ, 

13 RX, 

14 RY, 

15 RZ, 

16 PauliRot, 

17 Barrier, 

18 evolve_pauli_with_clifford, 

19 pauli_decompose, 

20 pauli_string_from_operation, 

21) 

22from collections import defaultdict 

23 

24 

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

26 if random_key is None: 

27 return None, None 

28 else: 

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

30 

31 

32class PauliTape: 

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

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

35 Fourier-tree algorithm. 

36 """ 

37 

38 def __init__( 

39 self, 

40 operations: List[Operation], 

41 observables: List[Operation], 

42 ) -> None: 

43 self.operations = operations 

44 self.observables = observables 

45 

46 def get_parameters(self) -> list: 

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

48 params = [] 

49 for op in self.operations: 

50 params.extend(op.parameters) 

51 return params 

52 

53 def get_input_indices(self) -> list: 

54 indices = defaultdict(list) 

55 all_indices = [] 

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

57 for i, op in enumerate(ops_w_params): 

58 if op.input_idx >= 0: 

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

60 all_indices.append(i) 

61 return indices, all_indices 

62 

63 

64class PauliCircuit: 

65 """ 

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

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

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

69 

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

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

72 """ 

73 

74 CLIFFORD_GATES = ( 

75 PauliX, 

76 PauliY, 

77 PauliZ, 

78 H, 

79 S, 

80 CX, 

81 ) 

82 

83 PAULI_ROTATION_GATES = ( 

84 RX, 

85 RY, 

86 RZ, 

87 PauliRot, 

88 ) 

89 

90 SKIPPABLE_OPERATIONS = (Barrier,) 

91 

92 @staticmethod 

93 def from_parameterised_circuit( 

94 tape: List[Operation], 

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

96 ) -> PauliTape: 

97 """ 

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

99 

100 Args: 

101 tape: List of operations recorded from the circuit. 

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

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

104 

105 Returns: 

106 PauliTape: 

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

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

109 """ 

110 if observables is None: 

111 observables = [] 

112 

113 operations = PauliCircuit.get_clifford_pauli_gates(tape) 

114 

115 pauli_gates, final_cliffords = PauliCircuit.commute_all_cliffords_to_the_end( 

116 operations 

117 ) 

118 

119 observables = PauliCircuit.cliffords_in_observable(final_cliffords, observables) 

120 

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

122 

123 @staticmethod 

124 def commute_all_cliffords_to_the_end( 

125 operations: List[Operation], 

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

127 """ 

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

129 accounting for commutation rules. 

130 

131 Args: 

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

133 circuit 

134 

135 Returns: 

136 Tuple[List[Operator], List[Operator]]: 

137 - List of the resulting Pauli-rotations 

138 - List of the resulting Clifford gates 

139 """ 

140 first_clifford = -1 

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

142 j = i 

143 while ( 

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

145 and PauliCircuit._is_clifford(operations[j]) 

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

147 ): 

148 pauli, clifford = PauliCircuit._evolve_clifford_rotation( 

149 operations[j], operations[j + 1] 

150 ) 

151 operations[j] = pauli 

152 operations[j + 1] = clifford 

153 j += 1 

154 first_clifford = j 

155 

156 # No Clifford gates are in the circuit 

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

158 return operations, [] 

159 

160 pauli_rotations = operations[:first_clifford] 

161 clifford_gates = operations[first_clifford:] 

162 

163 return pauli_rotations, clifford_gates 

164 

165 @staticmethod 

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

167 """ 

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

169 pauli-rotation gates. 

170 

171 Args: 

172 tape: List of operations recorded from the circuit. 

173 

174 Returns: 

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

176 and Pauli-rotation gates. 

177 """ 

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

179 

180 operations = [] 

181 for operation in tape: 

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

183 operation 

184 ): 

185 operations.append(operation) 

186 elif PauliCircuit._is_skippable(operation): 

187 continue 

188 elif isinstance(operation, Rot): 

189 w = operation.wires[0] 

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

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

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

193 elif isinstance(operation, CRZ): 

194 c, t = operation.wires 

195 theta = operation.theta 

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

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

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

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

200 elif isinstance(operation, CRX): 

201 c, t = operation.wires 

202 theta = operation.theta 

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

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

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

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

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

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

209 elif isinstance(operation, CRY): 

210 c, t = operation.wires 

211 theta = operation.theta 

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

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

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

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

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

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

218 elif isinstance(operation, CZ): 

219 c, t = operation.wires 

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

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

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

223 else: 

224 raise NotImplementedError( 

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

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

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

228 "Rot, and standard Clifford gates." 

229 ) 

230 

231 return operations 

232 

233 @staticmethod 

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

235 """ 

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

237 Clifford circuit. Currently this only contains barriers. 

238 

239 Args: 

240 operation (Operation): Gate operation 

241 

242 Returns: 

243 bool: Whether the operation can be skipped. 

244 """ 

245 return isinstance(operation, PauliCircuit.SKIPPABLE_OPERATIONS) 

246 

247 @staticmethod 

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

249 """ 

250 Determines is an operator is a Clifford gate. 

251 

252 Args: 

253 operation (Operation): Gate operation 

254 

255 Returns: 

256 bool: Whether the operation is Clifford. 

257 """ 

258 return isinstance(operation, PauliCircuit.CLIFFORD_GATES) 

259 

260 @staticmethod 

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

262 """ 

263 Determines is an operator is a Pauli rotation gate. 

264 

265 Args: 

266 operation (Operation): Gate operation 

267 

268 Returns: 

269 bool: Whether the operation is a Pauli operation. 

270 """ 

271 return isinstance(operation, PauliCircuit.PAULI_ROTATION_GATES) 

272 

273 @staticmethod 

274 def _evolve_clifford_rotation( 

275 clifford: Operation, pauli: Operation 

276 ) -> Tuple[Operation, Operation]: 

277 """ 

278 This function computes the resulting operations, when switching a 

279 Clifford gate and a Pauli rotation in the circuit. 

280 

281 Example: 

282 Consider a circuit consisting of the gate sequence 

283 ... --- H --- R_z --- ... 

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

285 clifford (Hadamard) gate to the end: 

286 ... --- R_x --- H --- ... 

287 

288 Args: 

289 clifford (Operation): Clifford gate to move. 

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

291 

292 Returns: 

293 Tuple[Operation, Operation]: 

294 - Evolved Pauli rotation operator 

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

296 """ 

297 

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

299 return pauli, clifford 

300 

301 gen = pauli.generator() 

302 param = pauli.parameters[0] 

303 

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

305 qubits = evolved_gen.wires 

306 _coeff, evolved_pauli_op = pauli_decompose( 

307 evolved_gen.matrix, wire_order=qubits 

308 ) 

309 

310 pauli_str = pauli_string_from_operation(evolved_pauli_op) 

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

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

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

314 

315 pauli_str, qubits = PauliCircuit._remove_identities_from_paulistr( 

316 pauli_str, evolved_pauli_op.wires 

317 ) 

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

319 

320 if pauli.input_idx >= 0: 

321 new_pauli.input_idx = pauli.input_idx 

322 

323 return new_pauli, clifford 

324 

325 @staticmethod 

326 def _remove_identities_from_paulistr( 

327 pauli_str: str, qubits: List[int] 

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

329 """ 

330 Removes identities from Pauli string and its corresponding qubits. 

331 

332 Args: 

333 pauli_str (str): Pauli string 

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

335 

336 Returns: 

337 Tuple[str, List[int]]: 

338 - Pauli string without identities 

339 - Qubits indices without the identities 

340 """ 

341 

342 reduced_qubits = [] 

343 reduced_pauli_str = "" 

344 for i, p in enumerate(pauli_str): 

345 if p != "I": 

346 reduced_pauli_str += p 

347 reduced_qubits.append(qubits[i]) 

348 

349 return reduced_pauli_str, reduced_qubits 

350 

351 @staticmethod 

352 def _evolve_clifford_pauli( 

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

354 ) -> Tuple[Operation, Operation]: 

355 """ 

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

357 Operation with a Clifford operation. 

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

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

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

361 

362 Args: 

363 clifford (Operation): Clifford gate 

364 pauli (Operation): Pauli gate 

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

366 applied to the left. Defaults to True. 

367 

368 Returns: 

369 Tuple[Operation, Operation]: 

370 - Evolved Pauli operator 

371 - Resulting Clifford operator (same as input) 

372 """ 

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

374 return pauli, clifford 

375 

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

377 return evolved, clifford 

378 

379 @staticmethod 

380 def _evolve_cliffords_list( 

381 cliffords: List[Operation], pauli: Operation 

382 ) -> Operation: 

383 """ 

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

385 cliffords. 

386 

387 Args: 

388 cliffords (List[Operation]): Clifford gates 

389 pauli (Operation): Pauli gate 

390 

391 Returns: 

392 Operation: Evolved Pauli operator 

393 """ 

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

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

396 qubits = pauli.wires 

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

398 

399 return pauli 

400 

401 @staticmethod 

402 def cliffords_in_observable( 

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

404 ) -> List[Operation]: 

405 """ 

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

407 

408 Args: 

409 operations (List[Operation]): Clifford gates 

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

411 circuit 

412 

413 Returns: 

414 List[Operation]: Observables with Clifford operations 

415 """ 

416 observables = [] 

417 for ob in original_obs: 

418 clifford_obs = PauliCircuit._evolve_cliffords_list(operations, ob) 

419 observables.append(clifford_obs) 

420 return observables