From ef0a932d9430d84fb04b9d443b017b7b5d4c2f32 Mon Sep 17 00:00:00 2001 From: Kanguk Lee Date: Mon, 1 Sep 2025 00:28:43 +0900 Subject: [PATCH 1/9] Treat classically controlled ops as non-Clifford in DD Classically controlled operations were passing `_is_clifford_op`, which led `dynamical_decoupling` to synthesize/invert through them and crash. Mark them non-Clifford for DD so they act as boundaries. Add regression test `test_classically_controlled_no_update_succeeds`. Fixes #7617 --- .../cirq/transformers/dynamical_decoupling.py | 4 +++- .../transformers/dynamical_decoupling_test.py | 18 +++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/cirq-core/cirq/transformers/dynamical_decoupling.py b/cirq-core/cirq/transformers/dynamical_decoupling.py index 70ab0f76479..41b6e3c6c54 100644 --- a/cirq-core/cirq/transformers/dynamical_decoupling.py +++ b/cirq-core/cirq/transformers/dynamical_decoupling.py @@ -119,7 +119,9 @@ def _is_single_qubit_gate_moment(moment: Moment) -> bool: def _is_clifford_op(op: ops.Operation) -> bool: - return has_unitary(op) and has_stabilizer_effect(op) + if op.gate: + return has_unitary(op.gate) and has_stabilizer_effect(op.gate) + return False def _calc_busy_moment_range_of_each_qubit(circuit: FrozenCircuit) -> dict[ops.Qid, list[int]]: diff --git a/cirq-core/cirq/transformers/dynamical_decoupling_test.py b/cirq-core/cirq/transformers/dynamical_decoupling_test.py index 410d4beaaa8..7ca85b5a86f 100644 --- a/cirq-core/cirq/transformers/dynamical_decoupling_test.py +++ b/cirq-core/cirq/transformers/dynamical_decoupling_test.py @@ -20,7 +20,7 @@ import pytest import cirq -from cirq import add_dynamical_decoupling, CNOT, CZ, CZPowGate, H, X, Y, Z +from cirq import add_dynamical_decoupling, CNOT, CZ, CZPowGate, H, I, measure, X, Y, Z def assert_sim_eq(circuit1: cirq.AbstractCircuit, circuit2: cirq.AbstractCircuit): @@ -53,6 +53,22 @@ def assert_dd( assert_sim_eq(input_circuit, transformed_circuit) +def test_classically_controlled_no_update_succeeds(): + """Test case diagrams. + Input: + 0: ───M───I─── + ║ ║ + c: ═══@═══^═══ + """ + a = cirq.NamedQubit('a') + + add_dynamical_decoupling( + cirq.Circuit( + cirq.Moment(measure(a, key="a")), cirq.Moment(I(a).with_classical_controls("a")) + ) + ) + + def test_no_insertion(): """Test case diagrams. Input: From b1c4982425fd28dd2b4a4d30df4fd17db926299f Mon Sep 17 00:00:00 2001 From: Kanguk Lee Date: Mon, 1 Sep 2025 02:02:29 +0900 Subject: [PATCH 2/9] Revert `_is_clifford_op` change in `dynamical_decoupling` --- cirq-core/cirq/transformers/dynamical_decoupling.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cirq-core/cirq/transformers/dynamical_decoupling.py b/cirq-core/cirq/transformers/dynamical_decoupling.py index 41b6e3c6c54..70ab0f76479 100644 --- a/cirq-core/cirq/transformers/dynamical_decoupling.py +++ b/cirq-core/cirq/transformers/dynamical_decoupling.py @@ -119,9 +119,7 @@ def _is_single_qubit_gate_moment(moment: Moment) -> bool: def _is_clifford_op(op: ops.Operation) -> bool: - if op.gate: - return has_unitary(op.gate) and has_stabilizer_effect(op.gate) - return False + return has_unitary(op) and has_stabilizer_effect(op) def _calc_busy_moment_range_of_each_qubit(circuit: FrozenCircuit) -> dict[ops.Qid, list[int]]: From 76ad4a674738f30da279a82af46188dad81d32ed Mon Sep 17 00:00:00 2001 From: Kanguk Lee Date: Mon, 1 Sep 2025 02:03:09 +0900 Subject: [PATCH 3/9] Make `ClassicallyControlledOperations` non-unitary --- cirq-core/cirq/ops/classically_controlled_operation.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cirq-core/cirq/ops/classically_controlled_operation.py b/cirq-core/cirq/ops/classically_controlled_operation.py index d69fb75ac91..cb90842bc01 100644 --- a/cirq-core/cirq/ops/classically_controlled_operation.py +++ b/cirq-core/cirq/ops/classically_controlled_operation.py @@ -125,6 +125,9 @@ def with_qubits(self, *new_qubits) -> cirq.Operation: *self._conditions ) + def _has_unitary_(self) -> bool: + return False + def _decompose_with_context_(self, *, context: cirq.DecompositionContext): result = protocols.decompose_once(self._sub_operation, None, flatten=False, context=context) if result is None: From 55e8b49777a87d605ca088c21a125ae6f69f3f16 Mon Sep 17 00:00:00 2001 From: Kanguk Lee Date: Mon, 1 Sep 2025 02:04:20 +0900 Subject: [PATCH 4/9] Fix typos in test docstrings --- cirq-core/cirq/transformers/dynamical_decoupling_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cirq-core/cirq/transformers/dynamical_decoupling_test.py b/cirq-core/cirq/transformers/dynamical_decoupling_test.py index 7ca85b5a86f..ad95d1b5c99 100644 --- a/cirq-core/cirq/transformers/dynamical_decoupling_test.py +++ b/cirq-core/cirq/transformers/dynamical_decoupling_test.py @@ -56,9 +56,9 @@ def assert_dd( def test_classically_controlled_no_update_succeeds(): """Test case diagrams. Input: - 0: ───M───I─── + a: ───M───I─── ║ ║ - c: ═══@═══^═══ + a: ═══@═══^═══ """ a = cirq.NamedQubit('a') From c1a92baa6e3b8032a58616b47fea4ff3579af115 Mon Sep 17 00:00:00 2001 From: Kanguk Lee Date: Mon, 1 Sep 2025 02:58:30 +0900 Subject: [PATCH 5/9] Remove `_has_unitary_` in `ClassicallyControlledOperation` --- cirq-core/cirq/ops/classically_controlled_operation.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/cirq-core/cirq/ops/classically_controlled_operation.py b/cirq-core/cirq/ops/classically_controlled_operation.py index cb90842bc01..d69fb75ac91 100644 --- a/cirq-core/cirq/ops/classically_controlled_operation.py +++ b/cirq-core/cirq/ops/classically_controlled_operation.py @@ -125,9 +125,6 @@ def with_qubits(self, *new_qubits) -> cirq.Operation: *self._conditions ) - def _has_unitary_(self) -> bool: - return False - def _decompose_with_context_(self, *, context: cirq.DecompositionContext): result = protocols.decompose_once(self._sub_operation, None, flatten=False, context=context) if result is None: From f4f5c23bb8cd7ca6bf69e85c658fb200149aa4b1 Mon Sep 17 00:00:00 2001 From: Kanguk Lee Date: Mon, 1 Sep 2025 03:01:07 +0900 Subject: [PATCH 6/9] Check `control_keys(op)` in `_is_clifford_op` --- cirq-core/cirq/transformers/dynamical_decoupling.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cirq-core/cirq/transformers/dynamical_decoupling.py b/cirq-core/cirq/transformers/dynamical_decoupling.py index 70ab0f76479..d499adcbc8b 100644 --- a/cirq-core/cirq/transformers/dynamical_decoupling.py +++ b/cirq-core/cirq/transformers/dynamical_decoupling.py @@ -25,6 +25,7 @@ from cirq import ops, protocols from cirq.circuits import Circuit, FrozenCircuit, Moment from cirq.protocols import unitary_protocol +from cirq.protocols.control_key_protocol import control_keys from cirq.protocols.has_stabilizer_effect_protocol import has_stabilizer_effect from cirq.protocols.has_unitary_protocol import has_unitary from cirq.transformers import transformer_api @@ -119,7 +120,7 @@ def _is_single_qubit_gate_moment(moment: Moment) -> bool: def _is_clifford_op(op: ops.Operation) -> bool: - return has_unitary(op) and has_stabilizer_effect(op) + return has_unitary(op) and has_stabilizer_effect(op) and not control_keys(op) def _calc_busy_moment_range_of_each_qubit(circuit: FrozenCircuit) -> dict[ops.Qid, list[int]]: From 9280f3d571eaddfac80ed205f50ea380fda2f166 Mon Sep 17 00:00:00 2001 From: Kanguk Lee Date: Fri, 19 Sep 2025 11:30:37 +0900 Subject: [PATCH 7/9] Add assert_same_circuits test --- cirq-core/cirq/transformers/dynamical_decoupling_test.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cirq-core/cirq/transformers/dynamical_decoupling_test.py b/cirq-core/cirq/transformers/dynamical_decoupling_test.py index ad95d1b5c99..920b60c0e4a 100644 --- a/cirq-core/cirq/transformers/dynamical_decoupling_test.py +++ b/cirq-core/cirq/transformers/dynamical_decoupling_test.py @@ -62,11 +62,11 @@ def test_classically_controlled_no_update_succeeds(): """ a = cirq.NamedQubit('a') - add_dynamical_decoupling( - cirq.Circuit( - cirq.Moment(measure(a, key="a")), cirq.Moment(I(a).with_classical_controls("a")) - ) + input_circuit = cirq.Circuit( + cirq.Moment(measure(a, key="a")), cirq.Moment(I(a).with_classical_controls("a")) ) + output_circuit = add_dynamical_decoupling(input_circuit) + cirq.testing.assert_same_circuits(input_circuit, output_circuit) def test_no_insertion(): From 91a78a6fec9a35ffad60b47f2c9c91f544cfcdd4 Mon Sep 17 00:00:00 2001 From: Kanguk Lee <68288688+p51lee@users.noreply.github.com> Date: Wed, 29 Oct 2025 16:22:25 +0900 Subject: [PATCH 8/9] Formatting --- cirq-core/cirq/transformers/dynamical_decoupling_test.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cirq-core/cirq/transformers/dynamical_decoupling_test.py b/cirq-core/cirq/transformers/dynamical_decoupling_test.py index 6d72847b852..ad6ec630693 100644 --- a/cirq-core/cirq/transformers/dynamical_decoupling_test.py +++ b/cirq-core/cirq/transformers/dynamical_decoupling_test.py @@ -26,7 +26,6 @@ from cirq.transformers.dynamical_decoupling import _CellType, _Grid - def assert_sim_eq(circuit1: cirq.AbstractCircuit, circuit2: cirq.AbstractCircuit) -> None: # Simulate 2 circuits and compare final states. sampler = cirq.Simulator(dtype=np.complex128) From 5c94b70f53398c6d1cf3afd547bc3392934761e3 Mon Sep 17 00:00:00 2001 From: Kanguk Lee <68288688+p51lee@users.noreply.github.com> Date: Wed, 29 Oct 2025 16:24:33 +0900 Subject: [PATCH 9/9] Format --- cirq-core/cirq/transformers/dynamical_decoupling_test.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cirq-core/cirq/transformers/dynamical_decoupling_test.py b/cirq-core/cirq/transformers/dynamical_decoupling_test.py index ad6ec630693..7cd3e2aa17d 100644 --- a/cirq-core/cirq/transformers/dynamical_decoupling_test.py +++ b/cirq-core/cirq/transformers/dynamical_decoupling_test.py @@ -21,7 +21,6 @@ import pytest import cirq - from cirq import add_dynamical_decoupling, CNOT, CZ, CZPowGate, H, I, measure, X, Y, Z from cirq.transformers.dynamical_decoupling import _CellType, _Grid