21
21
22
22
from __future__ import annotations
23
23
24
- from typing import Callable , Iterable , TYPE_CHECKING
24
+ from typing import Callable , cast , Iterable , TYPE_CHECKING
25
25
26
26
import numpy as np
27
+ from attr import define
27
28
from scipy .linalg import cossin
28
29
29
30
from cirq import ops
30
31
from cirq .circuits .frozen_circuit import FrozenCircuit
31
32
from cirq .linalg import decompositions , predicates
32
33
from cirq .protocols import unitary_protocol
33
- from cirq .transformers .analytical_decompositions .three_qubit_decomposition import (
34
- three_qubit_matrix_to_operations ,
35
- )
36
34
from cirq .transformers .analytical_decompositions .two_qubit_to_cz import (
37
35
two_qubit_matrix_to_cz_operations ,
36
+ two_qubit_matrix_to_diagonal_and_cz_operations ,
38
37
)
39
38
40
39
if TYPE_CHECKING :
41
40
import cirq
42
41
43
42
43
+ @define
44
+ class _TwoQubitGate :
45
+ location : int
46
+ matrix : np .ndarray
47
+
48
+
44
49
def quantum_shannon_decomposition (
45
50
qubits : list [cirq .Qid ], u : np .ndarray , atol : float = 1e-8
46
51
) -> Iterable [cirq .Operation ]:
@@ -67,14 +72,12 @@ def quantum_shannon_decomposition(
67
72
1. _single_qubit_decomposition
68
73
OR
69
74
(Recursive Case)
70
- 1. _msb_demuxer
71
- 2. _multiplexed_cossin
72
- 3. _msb_demuxer
75
+ 1. _recursive_decomposition
73
76
74
77
Yields:
75
78
A single 2-qubit or 1-qubit operations from OP TREE
76
79
composed from the set
77
- { CNOT, rz, ry, ZPowGate }
80
+ { CNOT, CZ, rz, ry, ZPowGate }
78
81
79
82
Raises:
80
83
ValueError: If the u matrix is non-unitary
@@ -98,30 +101,92 @@ def quantum_shannon_decomposition(
98
101
yield from _single_qubit_decomposition (qubits [0 ], u )
99
102
return
100
103
101
- if n == 4 :
102
- operations = tuple (
103
- two_qubit_matrix_to_cz_operations (
104
- qubits [0 ], qubits [1 ], u , allow_partial_czs = True , clean_operations = True , atol = atol
105
- )
104
+ # Collect all operations from the recursive decomposition
105
+ shannon_decomp : list [cirq .Operation | list [cirq .Operation ]] = [
106
+ * _recursive_decomposition (qubits , u )
107
+ ]
108
+ # Separate all 2-qubit generic gates while keeping track of location
109
+ two_qubit_gates = [
110
+ _TwoQubitGate (location = loc , matrix = unitary_protocol .unitary (o ))
111
+ for loc , o in enumerate (cast (list [ops .Operation ], shannon_decomp ))
112
+ if isinstance (o .gate , ops .MatrixGate )
113
+ ]
114
+ # Apply case A.2 from Shende et al.
115
+ q0 = qubits [- 2 ]
116
+ q1 = qubits [- 1 ]
117
+ for idx in range (len (two_qubit_gates ) - 1 , 0 , - 1 ):
118
+ diagonal , operations = two_qubit_matrix_to_diagonal_and_cz_operations (
119
+ q0 ,
120
+ q1 ,
121
+ two_qubit_gates [idx ].matrix ,
122
+ allow_partial_czs = True ,
123
+ clean_operations = True ,
124
+ atol = atol ,
106
125
)
107
- yield from operations
108
- i , j = np .unravel_index (np .argmax (np .abs (u )), u .shape )
109
- new_unitary = unitary_protocol .unitary (FrozenCircuit .from_moments (* operations ))
110
- global_phase = np .angle (u [i , j ]) - np .angle (new_unitary [i , j ])
111
- if np .abs (global_phase ) > 1e-9 :
112
- yield ops .global_phase_operation (np .exp (1j * global_phase ))
113
- return
126
+ global_phase = _global_phase_difference (
127
+ two_qubit_gates [idx ].matrix , [ops .MatrixGate (diagonal )(q0 , q1 ), * operations ]
128
+ )
129
+ if not np .isclose (global_phase , 0 , atol = atol ):
130
+ operations .append (ops .global_phase_operation (np .exp (1j * global_phase )))
131
+ # Replace the generic gate with ops from OP TREE
132
+ shannon_decomp [two_qubit_gates [idx ].location ] = operations
133
+ # Join the diagonal with the unitary to be decomposed in the next step
134
+ two_qubit_gates [idx - 1 ].matrix = diagonal @ two_qubit_gates [idx - 1 ].matrix
135
+ if len (two_qubit_gates ) > 0 :
136
+ operations = two_qubit_matrix_to_cz_operations (
137
+ q0 ,
138
+ q1 ,
139
+ two_qubit_gates [0 ].matrix ,
140
+ allow_partial_czs = True ,
141
+ clean_operations = True ,
142
+ atol = atol ,
143
+ )
144
+ global_phase = _global_phase_difference (two_qubit_gates [0 ].matrix , operations )
145
+ if not np .isclose (global_phase , 0 , atol = atol ):
146
+ operations .append (ops .global_phase_operation (np .exp (1j * global_phase )))
147
+ shannon_decomp [two_qubit_gates [0 ].location ] = operations
148
+ # Yield the final operations in order
149
+ yield from cast (Iterable [ops .Operation ], ops .flatten_op_tree (shannon_decomp ))
150
+
151
+
152
+ def _recursive_decomposition (qubits : list [cirq .Qid ], u : np .ndarray ) -> Iterable [cirq .Operation ]:
153
+ """Recursive step in the quantum shannon decomposition.
154
+
155
+ Decomposes n-qubit unitary into generic 2-qubit gates, CNOT, CZ and 1-qubit gates.
156
+ All generic 2-qubit gates are applied to the two least significant qubits and
157
+ are not decomposed further here.
158
+
159
+ Args:
160
+ qubits: List of qubits in order of significance
161
+ u: Numpy array for unitary matrix representing gate to be decomposed
162
+
163
+ Calls:
164
+ 1. _msb_demuxer
165
+ 2. _multiplexed_cossin
166
+ 3. _msb_demuxer
167
+
168
+ Yields:
169
+ Generic 2-qubit gates or operations from {ry,rz,CNOT,CZ}.
114
170
115
- if n == 8 :
116
- operations = tuple (
117
- three_qubit_matrix_to_operations (qubits [0 ], qubits [1 ], qubits [2 ], u , atol = atol )
171
+ Raises:
172
+ ValueError: If the u matrix is not of shape (2^n,2^n)
173
+ ValueError: If the u matrix is not of size at least 4
174
+ """
175
+ n = u .shape [0 ]
176
+ if n & (n - 1 ):
177
+ raise ValueError (
178
+ f"Expected input matrix u to be a (2^n x 2^n) shaped numpy array, \
179
+ but instead got shape { u .shape } "
118
180
)
119
- yield from operations
120
- i , j = np .unravel_index (np .argmax (np .abs (u )), u .shape )
121
- new_unitary = unitary_protocol .unitary (FrozenCircuit .from_moments (* operations ))
122
- global_phase = np .angle (u [i , j ]) - np .angle (new_unitary [i , j ])
123
- if np .abs (global_phase ) > 1e-9 :
124
- yield ops .global_phase_operation (np .exp (1j * global_phase ))
181
+
182
+ if n <= 2 :
183
+ raise ValueError (
184
+ f"Expected input matrix u for recursive step to have size at least 4, \
185
+ but it has size { n } "
186
+ )
187
+
188
+ if n == 4 :
189
+ yield ops .MatrixGate (u ).on (* qubits )
125
190
return
126
191
127
192
# Perform a cosine-sine (linalg) decomposition on u
@@ -137,10 +202,30 @@ def quantum_shannon_decomposition(
137
202
# Yield ops from multiplexed Ry part
138
203
yield from _multiplexed_cossin (qubits , theta , ops .ry )
139
204
205
+ # Optimization A.1 in Shende et al. - the last CZ gate in the multiplexed Ry part
206
+ # is merged into the generic multiplexor (u1, u2)
207
+ # This gate is CZ(qubits[1], qubits[0]) = CZ(qubits[0], qubits[1])
208
+ # as CZ is symmetric.
209
+ # For the u1⊕u2 multiplexor operator:
210
+ # as u1 is the operator in case qubits[0] = |0>,
211
+ # and u2 is the operator in case qubits[0] = |1>
212
+ # we can represent the merge by phasing u2 with Z ⊗ I
213
+ cz_diag = np .concatenate ((np .ones (n >> 2 ), np .full (n >> 2 , - 1 )))
214
+ u2 = u2 @ np .diag (cz_diag )
215
+
140
216
# Yield ops from decomposition of multiplexed u1/u2 part
141
217
yield from _msb_demuxer (qubits , u1 , u2 )
142
218
143
219
220
+ def _global_phase_difference (u : np .ndarray , ops : list [cirq .Operation ]) -> float :
221
+ """Returns the difference in global phase between unitary u and
222
+ a list of operations computing u.
223
+ """
224
+ i , j = np .unravel_index (np .argmax (np .abs (u )), u .shape )
225
+ new_unitary = unitary_protocol .unitary (FrozenCircuit .from_moments (* ops ))
226
+ return np .angle (u [i , j ]) - np .angle (new_unitary [i , j ])
227
+
228
+
144
229
def _single_qubit_decomposition (qubit : cirq .Qid , u : np .ndarray ) -> Iterable [cirq .Operation ]:
145
230
"""Decomposes single-qubit gate, and returns list of operations, keeping phase invariant.
146
231
@@ -202,11 +287,14 @@ def _msb_demuxer(
202
287
u2: Lower-right quadrant of total unitary to be decomposed (see diagram)
203
288
204
289
Calls:
205
- 1. quantum_shannon_decomposition
290
+ 1. _recursive_decomposition
206
291
2. _multiplexed_cossin
207
- 3. quantum_shannon_decomposition
292
+ 3. _recursive_decomposition
208
293
209
- Yields: Single operation from OP TREE of 2-qubit and 1-qubit operations
294
+ Yields:
295
+ Generic 2-qubit gates on the two least significant qubits,
296
+ CNOT gates with the target not on the two least significant qubits,
297
+ ry or rz
210
298
"""
211
299
# Perform a diagonalization to find values
212
300
u1 = u1 .astype (np .complex128 )
@@ -231,15 +319,15 @@ def _msb_demuxer(
231
319
# Last term is given by ( I ⊗ W ), demultiplexed
232
320
# Remove most-significant (demuxed) control-qubit
233
321
# Yield operations for QSD on W
234
- yield from quantum_shannon_decomposition (demux_qubits [1 :], W , atol = 1e-6 )
322
+ yield from _recursive_decomposition (demux_qubits [1 :], W )
235
323
236
324
# Use complex phase of d_i to give theta_i (so d_i* gives -theta_i)
237
325
# Observe that middle part looks like Σ_i( Rz(theta_i)⊗|i><i| )
238
326
# Yield ops from multiplexed Rz part
239
327
yield from _multiplexed_cossin (demux_qubits , - np .angle (d ), ops .rz )
240
328
241
329
# Yield operations for QSD on V
242
- yield from quantum_shannon_decomposition (demux_qubits [1 :], V , atol = 1e-6 )
330
+ yield from _recursive_decomposition (demux_qubits [1 :], V )
243
331
244
332
245
333
def _nth_gray (n : int ) -> int :
@@ -263,7 +351,7 @@ def _multiplexed_cossin(
263
351
Calls:
264
352
No major calls
265
353
266
- Yields: Single operation from OP TREE from set 1- and 2-qubit gates: {ry,rz,CNOT}
354
+ Yields: Single operation from OP TREE from set 1- and 2-qubit gates: {ry,rz,CNOT,CZ }
267
355
"""
268
356
# Most significant qubit is main qubit with rotation function applied
269
357
main_qubit = cossin_qubits [0 ]
@@ -304,4 +392,11 @@ def _multiplexed_cossin(
304
392
yield rot_func (rotation ).on (main_qubit )
305
393
306
394
# Add a CNOT from the select qubit to the main qubit
307
- yield ops .CNOT (control_qubits [select_qubit ], main_qubit )
395
+ # Optimization A.1 in Shende et al. - use CZ instead of CNOT for ry rotations
396
+ if rot_func == ops .ry :
397
+ # Don't emit the last gate, as it will be merged into the generic multiplexor
398
+ # in the cosine-sine decomposition
399
+ if j < len (angles ) - 1 :
400
+ yield ops .CZ (control_qubits [select_qubit ], main_qubit )
401
+ else :
402
+ yield ops .CNOT (control_qubits [select_qubit ], main_qubit )
0 commit comments