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
« 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
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)
34class PauliTape:
35 """Simple tape wrapper with ``operations``, ``observables``, and
36 ``get_parameters`` — replacing PennyLane's ``Script`` for the
37 Fourier-tree algorithm.
38 """
40 def __init__(
41 self,
42 operations: List[Operation],
43 observables: List[Operation],
44 ) -> None:
45 self.operations = operations
46 self.observables = observables
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
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
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.
72 A Pauli Circuit only consists of parameterised Pauli-rotations and Clifford
73 gates, which is the default for the most common VQCs.
74 """
76 CLIFFORD_GATES = (
77 PauliX,
78 PauliY,
79 PauliZ,
80 H,
81 S,
82 CX,
83 )
85 PAULI_ROTATION_GATES = (
86 RX,
87 RY,
88 RZ,
89 PauliRot,
90 )
92 SKIPPABLE_OPERATIONS = (Barrier,)
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.
102 Args:
103 tape: List of operations recorded from the circuit.
104 observables: List of observable operations. If ``None``, defaults
105 to ``[PauliZ(0)]``.
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 = []
115 operations = PauliCircuit.get_clifford_pauli_gates(tape)
117 pauli_gates, final_cliffords = PauliCircuit.commute_all_cliffords_to_the_end(
118 operations
119 )
121 observables = PauliCircuit.cliffords_in_observable(final_cliffords, observables)
123 return PauliTape(operations=pauli_gates, observables=observables)
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.
133 Args:
134 operations (List[Operator]): The operations in the tape of the
135 circuit
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
158 # No Clifford gates are in the circuit
159 if not PauliCircuit._is_clifford(operations[-1]):
160 return operations, []
162 pauli_rotations = operations[:first_clifford]
163 clifford_gates = operations[first_clifford:]
165 return pauli_rotations, clifford_gates
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.
173 Args:
174 tape: List of operations recorded from the circuit.
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
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 )
233 return operations
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.
241 Args:
242 operation (Operation): Gate operation
244 Returns:
245 bool: Whether the operation can be skipped.
246 """
247 return isinstance(operation, PauliCircuit.SKIPPABLE_OPERATIONS)
249 @staticmethod
250 def _is_clifford(operation: Operation) -> bool:
251 """
252 Determines is an operator is a Clifford gate.
254 Args:
255 operation (Operation): Gate operation
257 Returns:
258 bool: Whether the operation is Clifford.
259 """
260 return isinstance(operation, PauliCircuit.CLIFFORD_GATES)
262 @staticmethod
263 def _is_pauli_rotation(operation: Operation) -> bool:
264 """
265 Determines is an operator is a Pauli rotation gate.
267 Args:
268 operation (Operation): Gate operation
270 Returns:
271 bool: Whether the operation is a Pauli operation.
272 """
273 return isinstance(operation, PauliCircuit.PAULI_ROTATION_GATES)
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.
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 --- ...
290 Args:
291 clifford (Operation): Clifford gate to move.
292 pauli (Operation): Pauli rotation gate to move the clifford past.
294 Returns:
295 Tuple[Operation, Operation]:
296 - Evolved Pauli rotation operator
297 - Resulting Clifford operator (should be the same as the input)
298 """
300 if not any(p_c in clifford.wires for p_c in pauli.wires):
301 return pauli, clifford
303 gen = pauli.generator()
304 param = pauli.parameters[0]
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 )
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))
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)
322 if pauli.input_idx >= 0:
323 new_pauli.input_idx = pauli.input_idx
325 return new_pauli, clifford
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.
334 Args:
335 pauli_str (str): Pauli string
336 qubits (List[int]): Corresponding qubit indices.
338 Returns:
339 Tuple[str, List[int]]:
340 - Pauli string without identities
341 - Qubits indices without the identities
342 """
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])
351 return reduced_pauli_str, reduced_qubits
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)
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.
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
378 evolved = evolve_pauli_with_clifford(clifford, pauli, adjoint_left=adjoint_left)
379 return evolved, clifford
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.
389 Args:
390 cliffords (List[Operation]): Clifford gates
391 pauli (Operation): Pauli gate
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)
401 return pauli
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.
410 Args:
411 operations (List[Operation]): Clifford gates
412 original_obs (List[Operation]): Original observables from the
413 circuit
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