Skip to content

Commit 1ed879d

Browse files
Quantum Shannon Decomposition (#6020)
* Create quantum_shannon_decomposition Quantum Shannon Decomposition for cirq written by Uzair Faruqui, February 2023 Decomposes an arbitrary (2^n x 2^n) unitary matrix into a set of 1-qubit operations and 2-qubit CNOT gates. Implementation loosely based on this paper: https://arxiv.org/pdf/quant-ph/0406176.pdf --------- Co-authored-by: Tanuj Khattar <[email protected]>
1 parent 20b3d93 commit 1ed879d

File tree

5 files changed

+348
-0
lines changed

5 files changed

+348
-0
lines changed

cirq-core/cirq/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,7 @@
371371
parameterized_2q_op_to_sqrt_iswap_operations,
372372
prepare_two_qubit_state_using_cz,
373373
prepare_two_qubit_state_using_sqrt_iswap,
374+
quantum_shannon_decomposition,
374375
RouteCQC,
375376
routed_circuit_with_mapping,
376377
SqrtIswapTargetGateset,

cirq-core/cirq/transformers/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
parameterized_2q_op_to_sqrt_iswap_operations,
2626
prepare_two_qubit_state_using_cz,
2727
prepare_two_qubit_state_using_sqrt_iswap,
28+
quantum_shannon_decomposition,
2829
single_qubit_matrix_to_gates,
2930
single_qubit_matrix_to_pauli_rotations,
3031
single_qubit_matrix_to_phased_x_z,

cirq-core/cirq/transformers/analytical_decompositions/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@
2828
decompose_cphase_into_two_fsim,
2929
)
3030

31+
from cirq.transformers.analytical_decompositions.quantum_shannon_decomposition import (
32+
quantum_shannon_decomposition,
33+
)
34+
3135
from cirq.transformers.analytical_decompositions.single_qubit_decompositions import (
3236
is_negligible_turn,
3337
single_qubit_matrix_to_gates,
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
# Copyright 2023 The Cirq Developers
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
16+
"""Utility methods for decomposing arbitrary n-qubit (2^n x 2^n) unitary.
17+
18+
Based on:
19+
Synthesis of Quantum Logic Circuits. Tech. rep. 2006,
20+
https://arxiv.org/abs/quant-ph/0406176
21+
"""
22+
from typing import List, Callable, TYPE_CHECKING
23+
24+
from scipy.linalg import cossin
25+
26+
import numpy as np
27+
28+
from cirq import ops
29+
from cirq.linalg import decompositions, predicates
30+
31+
if TYPE_CHECKING:
32+
import cirq
33+
from cirq.ops import op_tree
34+
35+
36+
def quantum_shannon_decomposition(qubits: 'List[cirq.Qid]', u: np.ndarray) -> 'op_tree.OpTree':
37+
"""Decomposes n-qubit unitary into CX/YPow/ZPow/CNOT gates, preserving global phase.
38+
39+
The algorithm is described in Shende et al.:
40+
Synthesis of Quantum Logic Circuits. Tech. rep. 2006,
41+
https://arxiv.org/abs/quant-ph/0406176
42+
43+
Args:
44+
qubits: List of qubits in order of significance
45+
u: Numpy array for unitary matrix representing gate to be decomposed
46+
47+
Calls:
48+
(Base Case)
49+
1. _single_qubit_decomposition
50+
OR
51+
(Recursive Case)
52+
1. _msb_demuxer
53+
2. _multiplexed_cossin
54+
3. _msb_demuxer
55+
56+
Yields:
57+
A single 2-qubit or 1-qubit operations from OP TREE
58+
composed from the set
59+
{ CNOT, rz, ry, ZPowGate }
60+
61+
Raises:
62+
ValueError: If the u matrix is non-unitary
63+
ValueError: If the u matrix is not of shape (2^n,2^n)
64+
"""
65+
if not predicates.is_unitary(u): # Check that u is unitary
66+
raise ValueError(
67+
"Expected input matrix u to be unitary, \
68+
but it fails cirq.is_unitary check"
69+
)
70+
71+
n = u.shape[0]
72+
if n & (n - 1):
73+
raise ValueError(
74+
f"Expected input matrix u to be a (2^n x 2^n) shaped numpy array, \
75+
but instead got shape {u.shape}"
76+
)
77+
78+
if n == 2:
79+
# Yield a single-qubit decomp if u is 2x2
80+
yield from _single_qubit_decomposition(qubits[0], u)
81+
return
82+
83+
# Perform a cosine-sine (linalg) decomposition on u
84+
# X = [ u1 , 0 ] [ cos(theta) , -sin(theta) ] [ v1 , 0 ]
85+
# [ 0 , u2 ] [ sin(theta) , cos(theta) ] [ 0 , v2 ]
86+
(u1, u2), theta, (v1, v2) = cossin(u, n / 2, n / 2, separate=True)
87+
88+
# Yield ops from decomposition of multiplexed v1/v2 part
89+
yield from _msb_demuxer(qubits, v1, v2)
90+
91+
# Observe that middle part looks like Σ_i( Ry(theta_i)⊗|i><i| )
92+
# Then most significant qubit is Ry multiplexed over all other qubits
93+
# Yield ops from multiplexed Ry part
94+
yield from _multiplexed_cossin(qubits, theta, ops.ry)
95+
96+
# Yield ops from decomposition of multiplexed u1/u2 part
97+
yield from _msb_demuxer(qubits, u1, u2)
98+
99+
100+
def _single_qubit_decomposition(qubit: 'cirq.Qid', u: np.ndarray) -> 'op_tree.OpTree':
101+
"""Decomposes single-qubit gate, and returns list of operations, keeping phase invariant.
102+
103+
Args:
104+
qubit: Qubit on which to apply operations
105+
u: (2 x 2) Numpy array for unitary representing 1-qubit gate to be decomposed
106+
107+
Yields:
108+
A single operation from OP TREE of 3 operations (rz,ry,ZPowGate)
109+
"""
110+
# Perform native ZYZ decomposition
111+
phi_0, phi_1, phi_2 = decompositions.deconstruct_single_qubit_matrix_into_angles(u)
112+
113+
# Determine global phase picked up
114+
phase = np.angle(u[0, 0] / (np.exp(-1j * (phi_0) / 2) * np.cos(phi_1 / 2)))
115+
116+
# Append first two operations operations
117+
yield ops.rz(phi_0).on(qubit)
118+
yield ops.ry(phi_1).on(qubit)
119+
120+
# Append third operation with global phase added
121+
yield ops.ZPowGate(exponent=phi_2 / np.pi, global_shift=phase / phi_2).on(qubit)
122+
123+
124+
def _msb_demuxer(
125+
demux_qubits: 'List[cirq.Qid]', u1: np.ndarray, u2: np.ndarray
126+
) -> 'op_tree.OpTree':
127+
"""Demultiplexes a unitary matrix that is multiplexed in its most-significant-qubit.
128+
129+
Decomposition structure:
130+
[ u1 , 0 ] = [ V , 0 ][ D , 0 ][ W , 0 ]
131+
[ 0 , u2 ] [ 0 , V ][ 0 , D* ][ 0 , W ]
132+
133+
Gives: ( u1 )( u2* ) = ( V )( D^2 )( V* )
134+
and: W = ( D )( V* )( u2 )
135+
136+
Args:
137+
demux_qubits: Subset of total qubits involved in this unitary gate
138+
u1: Upper-left quadrant of total unitary to be decomposed (see diagram)
139+
u2: Lower-right quadrant of total unitary to be decomposed (see diagram)
140+
141+
Calls:
142+
1. quantum_shannon_decomposition
143+
2. _multiplexed_cossin
144+
3. quantum_shannon_decomposition
145+
146+
Yields: Single operation from OP TREE of 2-qubit and 1-qubit operations
147+
"""
148+
# Perform a diagonalization to find values
149+
u = u1 @ u2.T.conjugate()
150+
dsquared, V = np.linalg.eig(u)
151+
d = np.sqrt(dsquared)
152+
D = np.diag(d)
153+
W = D @ V.T.conjugate() @ u2
154+
155+
# Last term is given by ( I ⊗ W ), demultiplexed
156+
# Remove most-significant (demuxed) control-qubit
157+
# Yield operations for QSD on W
158+
yield from quantum_shannon_decomposition(demux_qubits[1:], W)
159+
160+
# Use complex phase of d_i to give theta_i (so d_i* gives -theta_i)
161+
# Observe that middle part looks like Σ_i( Rz(theta_i)⊗|i><i| )
162+
# Yield ops from multiplexed Rz part
163+
yield from _multiplexed_cossin(demux_qubits, -np.angle(d), ops.rz)
164+
165+
# Yield operations for QSD on V
166+
yield from quantum_shannon_decomposition(demux_qubits[1:], V)
167+
168+
169+
def _nth_gray(n: int) -> int:
170+
# Return the nth Gray Code number
171+
return n ^ (n >> 1)
172+
173+
174+
def _multiplexed_cossin(
175+
cossin_qubits: 'List[cirq.Qid]', angles: List[float], rot_func: Callable = ops.ry
176+
) -> 'op_tree.OpTree':
177+
"""Performs a multiplexed rotation over all qubits in this unitary matrix,
178+
179+
Uses ry and rz multiplexing for quantum shannon decomposition
180+
181+
Args:
182+
cossin_qubits: Subset of total qubits involved in this unitary gate
183+
angles: List of angles to be multiplexed over for the given type of rotation
184+
rot_func: Rotation function used for this multiplexing implementation
185+
(cirq.ry or cirq.rz)
186+
187+
Calls:
188+
No major calls
189+
190+
Yields: Single operation from OP TREE from set 1- and 2-qubit gates: {ry,rz,CNOT}
191+
"""
192+
# Most significant qubit is main qubit with rotation function applied
193+
main_qubit = cossin_qubits[0]
194+
195+
# All other qubits are control qubits
196+
control_qubits = cossin_qubits[1:]
197+
198+
for j in range(len(angles)):
199+
# The rotation includes a factor of (-1) for each bit in the Gray Code
200+
# if the position of that bit is also 1
201+
# The number of factors of -1 is counted using the 1s in the
202+
# binary representation of the (gray(j) & i)
203+
# Here, i gives the index for the angle, and
204+
# j is the iteration of the decomposition
205+
rotation = sum(
206+
-angle if bin(_nth_gray(j) & i).count('1') % 2 else angle
207+
for i, angle in enumerate(angles)
208+
)
209+
210+
# Divide by a factor of 2 for each additional select qubit
211+
# This is due to the halving in the decomposition applied recursively
212+
rotation = rotation * 2 / len(angles)
213+
214+
# The XOR of the this gray code with the next will give the 1 for the bit
215+
# corresponding to the CNOT select, else 0
216+
select_string = _nth_gray(j) ^ _nth_gray(j + 1)
217+
218+
# Find the index number where the bit is 1
219+
select_qubit = next(i for i in range(len(angles)) if (select_string >> i & 1))
220+
221+
# Negate the value, since we must index starting at most significant qubit
222+
# Also the final value will overflow, and it should be the MSB,
223+
# so introduce max function
224+
select_qubit = max(-select_qubit - 1, -len(control_qubits))
225+
226+
# Add a rotation on the main qubit
227+
yield rot_func(rotation).on(main_qubit)
228+
229+
# Add a CNOT from the select qubit to the main qubit
230+
yield ops.CNOT(control_qubits[select_qubit], main_qubit)
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# Copyright 2023 The Cirq Developers
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import cirq
16+
from cirq.ops import common_gates
17+
from cirq.transformers.analytical_decompositions.quantum_shannon_decomposition import (
18+
_multiplexed_cossin,
19+
_nth_gray,
20+
_msb_demuxer,
21+
_single_qubit_decomposition,
22+
quantum_shannon_decomposition,
23+
)
24+
25+
import pytest
26+
import numpy as np
27+
from scipy.stats import unitary_group
28+
29+
30+
@pytest.mark.parametrize('n_qubits', list(range(1, 8)))
31+
def test_random_qsd_n_qubit(n_qubits):
32+
U = unitary_group.rvs(2**n_qubits)
33+
qubits = [cirq.NamedQubit(f'q{i}') for i in range(n_qubits)]
34+
circuit = cirq.Circuit(quantum_shannon_decomposition(qubits, U))
35+
# Test return is equal to inital unitary
36+
assert cirq.approx_eq(U, circuit.unitary(), atol=1e-9)
37+
# Test all operations in gate set
38+
gates = (common_gates.Rz, common_gates.Ry, common_gates.ZPowGate, common_gates.CXPowGate)
39+
assert all(isinstance(op.gate, gates) for op in circuit.all_operations())
40+
41+
42+
def test_qsd_n_qubit_errors():
43+
qubits = [cirq.NamedQubit(f'q{i}') for i in range(3)]
44+
with pytest.raises(ValueError, match="shaped numpy array"):
45+
cirq.Circuit(quantum_shannon_decomposition(qubits, np.eye(9)))
46+
with pytest.raises(ValueError, match="is_unitary"):
47+
cirq.Circuit(quantum_shannon_decomposition(qubits, np.ones((8, 8))))
48+
49+
50+
def test_random_single_qubit_decomposition():
51+
U = unitary_group.rvs(2)
52+
qubit = cirq.NamedQubit('q0')
53+
circuit = cirq.Circuit(_single_qubit_decomposition(qubit, U))
54+
# Test return is equal to inital unitary
55+
assert cirq.approx_eq(U, circuit.unitary(), atol=1e-9)
56+
# Test all operations in gate set
57+
gates = (common_gates.Rz, common_gates.Ry, common_gates.ZPowGate, common_gates.CXPowGate)
58+
assert all(isinstance(op.gate, gates) for op in circuit.all_operations())
59+
60+
61+
def test_msb_demuxer():
62+
U1 = unitary_group.rvs(4)
63+
U2 = unitary_group.rvs(4)
64+
U_full = np.kron([[1, 0], [0, 0]], U1) + np.kron([[0, 0], [0, 1]], U2)
65+
qubits = [cirq.NamedQubit(f'q{i}') for i in range(3)]
66+
circuit = cirq.Circuit(_msb_demuxer(qubits, U1, U2))
67+
# Test return is equal to inital unitary
68+
assert cirq.approx_eq(U_full, circuit.unitary(), atol=1e-9)
69+
# Test all operations in gate set
70+
gates = (common_gates.Rz, common_gates.Ry, common_gates.ZPowGate, common_gates.CXPowGate)
71+
assert all(isinstance(op.gate, gates) for op in circuit.all_operations())
72+
73+
74+
def test_multiplexed_cossin():
75+
angle_1 = np.random.random_sample() * 2 * np.pi
76+
angle_2 = np.random.random_sample() * 2 * np.pi
77+
c1, s1 = np.cos(angle_1), np.sin(angle_1)
78+
c2, s2 = np.cos(angle_2), np.sin(angle_2)
79+
multiplexed_ry = [[c1, 0, -s1, 0], [0, c2, 0, -s2], [s1, 0, c1, 0], [0, s2, 0, c2]]
80+
multiplexed_ry = np.array(multiplexed_ry)
81+
qubits = [cirq.NamedQubit(f'q{i}') for i in range(2)]
82+
circuit = cirq.Circuit(_multiplexed_cossin(qubits, [angle_1, angle_2]))
83+
# Test return is equal to inital unitary
84+
assert cirq.approx_eq(multiplexed_ry, circuit.unitary(), atol=1e-9)
85+
# Test all operations in gate set
86+
gates = (common_gates.Rz, common_gates.Ry, common_gates.ZPowGate, common_gates.CXPowGate)
87+
assert all(isinstance(op.gate, gates) for op in circuit.all_operations())
88+
89+
90+
@pytest.mark.parametrize(
91+
'n, gray',
92+
[
93+
(0, 0),
94+
(1, 1),
95+
(2, 3),
96+
(3, 2),
97+
(4, 6),
98+
(5, 7),
99+
(6, 5),
100+
(7, 4),
101+
(8, 12),
102+
(9, 13),
103+
(10, 15),
104+
(11, 14),
105+
(12, 10),
106+
(13, 11),
107+
(14, 9),
108+
(15, 8),
109+
],
110+
)
111+
def test_nth_gray(n, gray):
112+
assert _nth_gray(n) == gray

0 commit comments

Comments
 (0)