Skip to content

Commit 5533d3e

Browse files
RevanthGundalamhuckadaxfohlpavoljuhas
authored andcommitted
Decompose controlled 1x1 unitary gates generically, not just GlobalPhaseGate (quantumlib#7283)
## Summary Fixes quantumlib#7248 This PR improves the decomposition logic for controlled gates in Cirq, ensuring that controlled 1x1 `MatrixGate` and `GlobalPhaseGate` instances decompose correctly. It also adds a targeted unit tests to verify this behavior. ## Details - Updated the [_decompose_with_context_](cci:1://file:///Users/revanthgundala/projects/Cirq/cirq-core/cirq/ops/controlled_gate.py:159:4-241:9) method in [cirq-core/cirq/ops/controlled_gate.py](cci:7://file:///Users/revanthgundala/projects/Cirq/cirq-core/cirq/ops/controlled_gate.py:0:0-0:0) to: - Correctly handle controlled gates with 1x1 unitary (global phase) by decomposing them into a `DiagonalGate` with the correct phase. - Added a parameterized test and additional explicit unit tests in [cirq-core/cirq/ops/controlled_gate_test.py](cci:7://file:///Users/revanthgundala/projects/Cirq/cirq-core/cirq/ops/controlled_gate_test.py:0:0-0:0) to: - Check that controlled global phase gates decompose to the correct diagonal structure for various phases and control configurations. Supersedes quantumlib#7282 --------- Co-authored-by: Michael Hucka <mhucka@caltech.edu> Co-authored-by: Dax Fohl <dax.fohl@gmail.com> Co-authored-by: Pavol Juhas <juhas@google.com>
1 parent cacb494 commit 5533d3e

File tree

2 files changed

+50
-28
lines changed

2 files changed

+50
-28
lines changed

cirq-core/cirq/ops/controlled_gate.py

Lines changed: 21 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -152,34 +152,27 @@ def _decompose_with_context_(
152152
# Prefer the subgate controlled version if available
153153
if self != controlled_sub_gate:
154154
return controlled_sub_gate.on(*qubits)
155-
if (
156-
protocols.has_unitary(self.sub_gate)
157-
and protocols.num_qubits(self.sub_gate) == 1
158-
and self._qid_shape_() == (2,) * len(self._qid_shape_())
159-
and isinstance(self.control_values, cv.ProductOfSums)
160-
):
161-
invert_ops: list[cirq.Operation] = []
162-
for cvals, cqbit in zip(self.control_values, qubits[: self.num_controls()]):
163-
if set(cvals) == {0}:
164-
invert_ops.append(common_gates.X(cqbit))
165-
elif set(cvals) == {0, 1}:
166-
control_qubits.remove(cqbit)
167-
decomposed_ops = controlled_gate_decomposition.decompose_multi_controlled_rotation(
168-
protocols.unitary(self.sub_gate), control_qubits, qubits[-1]
169-
)
170-
return invert_ops + decomposed_ops + invert_ops
171-
if isinstance(self.sub_gate, gp.GlobalPhaseGate):
172-
# A controlled global phase is a diagonal gate, where each active control value index
173-
# is set equal to the phase angle.
174-
shape = self.control_qid_shape
175-
if protocols.is_parameterized(self.sub_gate) or set(shape) != {2}:
176-
# Could work in theory, but DiagonalGate decompose does not support them.
177-
return NotImplemented
178-
angle = np.angle(complex(self.sub_gate.coefficient))
179-
rads = np.zeros(shape=shape)
180-
for hot in self.control_values.expand():
181-
rads[hot] = angle
182-
return dg.DiagonalGate(diag_angles_radians=[*rads.flatten()]).on(*qubits)
155+
if protocols.has_unitary(self.sub_gate) and all(q.dimension == 2 for q in qubits):
156+
n_qubits = protocols.num_qubits(self.sub_gate)
157+
# Case 1: Global Phase (1x1 Matrix)
158+
if n_qubits == 0:
159+
angle = np.angle(protocols.unitary(self.sub_gate)[0, 0])
160+
rads = np.zeros(shape=self.control_qid_shape)
161+
for hot in self.control_values.expand():
162+
rads[hot] = angle
163+
return dg.DiagonalGate(diag_angles_radians=[*rads.flatten()]).on(*qubits)
164+
# Case 2: Multi-controlled single-qubit gate decomposition
165+
if n_qubits == 1 and isinstance(self.control_values, cv.ProductOfSums):
166+
invert_ops: list[cirq.Operation] = []
167+
for cvals, cqbit in zip(self.control_values, qubits[: self.num_controls()]):
168+
if set(cvals) == {0}:
169+
invert_ops.append(common_gates.X(cqbit))
170+
elif set(cvals) == {0, 1}:
171+
control_qubits.remove(cqbit)
172+
decomposed_ops = controlled_gate_decomposition.decompose_multi_controlled_rotation(
173+
protocols.unitary(self.sub_gate), control_qubits, qubits[-1]
174+
)
175+
return invert_ops + decomposed_ops + invert_ops
183176
if isinstance(self.sub_gate, common_gates.CZPowGate):
184177
z_sub_gate = common_gates.ZPowGate(exponent=self.sub_gate.exponent)
185178
num_controls = self.num_controls() + 1

cirq-core/cirq/ops/controlled_gate_test.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -773,3 +773,32 @@ def test_controlled_mixture():
773773
c_yes = cirq.ControlledGate(sub_gate=cirq.phase_flip(0.25), num_controls=1)
774774
assert cirq.has_mixture(c_yes)
775775
assert cirq.approx_eq(cirq.mixture(c_yes), [(0.75, np.eye(4)), (0.25, cirq.unitary(cirq.CZ))])
776+
777+
778+
@pytest.mark.parametrize(
779+
'num_controls, angle, control_values',
780+
[
781+
(1, np.pi / 4, ((1,),)),
782+
(3, -np.pi / 2, ((1,), (1,), (1,))),
783+
(2, 0.0, ((1,), (1,))),
784+
(2, np.pi / 5, ((0,), (0,))),
785+
(3, np.pi, ((1,), (0,), (1,))),
786+
(4, -np.pi / 3, ((0,), (1,), (1,), (0,))),
787+
],
788+
)
789+
def test_controlled_global_phase_matrix_gate_decomposes(num_controls, angle, control_values):
790+
all_qubits = cirq.LineQubit.range(num_controls)
791+
control_values = cirq.ops.control_values.ProductOfSums(control_values)
792+
control_qid_shape = (2,) * num_controls
793+
phase_value = np.exp(1j * angle)
794+
795+
cg_matrix = cirq.ControlledGate(
796+
sub_gate=cirq.MatrixGate(np.array([[phase_value]])),
797+
num_controls=num_controls,
798+
control_values=control_values,
799+
control_qid_shape=control_qid_shape,
800+
)
801+
802+
decomposed = cirq.decompose(cg_matrix(*all_qubits))
803+
assert not any(isinstance(op.gate, cirq.MatrixGate) for op in decomposed)
804+
np.testing.assert_allclose(cirq.unitary(cirq.Circuit(decomposed)), cirq.unitary(cg_matrix))

0 commit comments

Comments
 (0)