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
« 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
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)
32class PauliTape:
33 """Simple tape wrapper with ``operations``, ``observables``, and
34 ``get_parameters`` — replacing PennyLane's ``Script`` for the
35 Fourier-tree algorithm.
36 """
38 def __init__(
39 self,
40 operations: List[Operation],
41 observables: List[Operation],
42 ) -> None:
43 self.operations = operations
44 self.observables = observables
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
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
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.
70 A Pauli Circuit only consists of parameterised Pauli-rotations and Clifford
71 gates, which is the default for the most common VQCs.
72 """
74 CLIFFORD_GATES = (
75 PauliX,
76 PauliY,
77 PauliZ,
78 H,
79 S,
80 CX,
81 )
83 PAULI_ROTATION_GATES = (
84 RX,
85 RY,
86 RZ,
87 PauliRot,
88 )
90 SKIPPABLE_OPERATIONS = (Barrier,)
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.
100 Args:
101 tape: List of operations recorded from the circuit.
102 observables: List of observable operations. If ``None``, defaults
103 to ``[PauliZ(0)]``.
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 = []
113 operations = PauliCircuit.get_clifford_pauli_gates(tape)
115 pauli_gates, final_cliffords = PauliCircuit.commute_all_cliffords_to_the_end(
116 operations
117 )
119 observables = PauliCircuit.cliffords_in_observable(final_cliffords, observables)
121 return PauliTape(operations=pauli_gates, observables=observables)
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.
131 Args:
132 operations (List[Operator]): The operations in the tape of the
133 circuit
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
156 # No Clifford gates are in the circuit
157 if not PauliCircuit._is_clifford(operations[-1]):
158 return operations, []
160 pauli_rotations = operations[:first_clifford]
161 clifford_gates = operations[first_clifford:]
163 return pauli_rotations, clifford_gates
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.
171 Args:
172 tape: List of operations recorded from the circuit.
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
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 )
231 return operations
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.
239 Args:
240 operation (Operation): Gate operation
242 Returns:
243 bool: Whether the operation can be skipped.
244 """
245 return isinstance(operation, PauliCircuit.SKIPPABLE_OPERATIONS)
247 @staticmethod
248 def _is_clifford(operation: Operation) -> bool:
249 """
250 Determines is an operator is a Clifford gate.
252 Args:
253 operation (Operation): Gate operation
255 Returns:
256 bool: Whether the operation is Clifford.
257 """
258 return isinstance(operation, PauliCircuit.CLIFFORD_GATES)
260 @staticmethod
261 def _is_pauli_rotation(operation: Operation) -> bool:
262 """
263 Determines is an operator is a Pauli rotation gate.
265 Args:
266 operation (Operation): Gate operation
268 Returns:
269 bool: Whether the operation is a Pauli operation.
270 """
271 return isinstance(operation, PauliCircuit.PAULI_ROTATION_GATES)
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.
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 --- ...
288 Args:
289 clifford (Operation): Clifford gate to move.
290 pauli (Operation): Pauli rotation gate to move the clifford past.
292 Returns:
293 Tuple[Operation, Operation]:
294 - Evolved Pauli rotation operator
295 - Resulting Clifford operator (should be the same as the input)
296 """
298 if not any(p_c in clifford.wires for p_c in pauli.wires):
299 return pauli, clifford
301 gen = pauli.generator()
302 param = pauli.parameters[0]
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 )
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))
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)
320 if pauli.input_idx >= 0:
321 new_pauli.input_idx = pauli.input_idx
323 return new_pauli, clifford
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.
332 Args:
333 pauli_str (str): Pauli string
334 qubits (List[int]): Corresponding qubit indices.
336 Returns:
337 Tuple[str, List[int]]:
338 - Pauli string without identities
339 - Qubits indices without the identities
340 """
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])
349 return reduced_pauli_str, reduced_qubits
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)
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.
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
376 evolved = evolve_pauli_with_clifford(clifford, pauli, adjoint_left=adjoint_left)
377 return evolved, clifford
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.
387 Args:
388 cliffords (List[Operation]): Clifford gates
389 pauli (Operation): Pauli gate
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)
399 return pauli
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.
408 Args:
409 operations (List[Operation]): Clifford gates
410 original_obs (List[Operation]): Original observables from the
411 circuit
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