From 34c3596004996b0d7d496caf8b5708e7bfbc9db8 Mon Sep 17 00:00:00 2001 From: Omer Date: Wed, 18 Mar 2026 21:21:28 -0500 Subject: [PATCH] Reject multi-qubit noise channels with clear error message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TFQ only supports single-qubit noise channels, but there was no validation to catch multi-qubit channels like cirq.asymmetric_depolarize(error_probabilities={'XX': 0.1}). This led to cryptic errors downstream in the C++ parser, which silently ignored the second qubit. Added an early validation check in the serializer that rejects multi-qubit noise channels with a clear error message explaining the limitation and suggesting .on_each() as a workaround. All 8 channel serializers are now guarded, along with a corresponding test. P.S. I'm a high school student interested in quantum computing — happy to discuss further or iterate on the patch if helpful. Fixes #686 --- .../core/serialize/serializer.py | 27 +++++++++++++------ .../core/serialize/serializer_test.py | 10 +++++++ 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/tensorflow_quantum/core/serialize/serializer.py b/tensorflow_quantum/core/serialize/serializer.py index 323e70025..b38f199b3 100644 --- a/tensorflow_quantum/core/serialize/serializer.py +++ b/tensorflow_quantum/core/serialize/serializer.py @@ -30,6 +30,17 @@ _CONSTANT_TRUE = lambda x: True +def _single_qubit_channel_check(x): + """Check that a noise channel operates on exactly one qubit.""" + if len(x.qubits) != 1: + raise ValueError( + "Multi-qubit noise channels are not supported in TFQ. " + f"Got {x} acting on {len(x.qubits)} qubits. " + "Consider decomposing into single-qubit channels applied " + "via .on_each().") + return True + + def _round(x): return np.round(x, 6) if isinstance(x, float) else x @@ -196,7 +207,7 @@ def _asymmetric_depolarize_serializer(): gate_type=cirq.AsymmetricDepolarizingChannel, serialized_gate_id="ADP", args=args, - can_serialize_predicate=_CONSTANT_TRUE) + can_serialize_predicate=_single_qubit_channel_check) def _asymmetric_depolarize_deserializer(): @@ -234,7 +245,7 @@ def _depolarize_channel_serializer(): gate_type=cirq.DepolarizingChannel, serialized_gate_id="DP", args=args, - can_serialize_predicate=_CONSTANT_TRUE) + can_serialize_predicate=_single_qubit_channel_check) def _depolarize_channel_deserializer(): @@ -272,7 +283,7 @@ def _gad_channel_serializer(): gate_type=cirq.GeneralizedAmplitudeDampingChannel, serialized_gate_id="GAD", args=args, - can_serialize_predicate=_CONSTANT_TRUE) + can_serialize_predicate=_single_qubit_channel_check) def _gad_channel_deserializer(): @@ -309,7 +320,7 @@ def _amplitude_damp_channel_serializer(): gate_type=cirq.AmplitudeDampingChannel, serialized_gate_id="AD", args=args, - can_serialize_predicate=_CONSTANT_TRUE) + can_serialize_predicate=_single_qubit_channel_check) def _amplitude_damp_channel_deserializer(): @@ -341,7 +352,7 @@ def _reset_channel_serializer(): gate_type=cirq.ResetChannel, serialized_gate_id="RST", args=args, - can_serialize_predicate=_CONSTANT_TRUE) + can_serialize_predicate=_single_qubit_channel_check) def _reset_channel_deserializer(): @@ -370,7 +381,7 @@ def _phase_damp_channel_serializer(): gate_type=cirq.PhaseDampingChannel, serialized_gate_id="PD", args=args, - can_serialize_predicate=_CONSTANT_TRUE) + can_serialize_predicate=_single_qubit_channel_check) def _phase_damp_channel_deserializer(): @@ -403,7 +414,7 @@ def _phase_flip_channel_serializer(): gate_type=cirq.PhaseFlipChannel, serialized_gate_id="PF", args=args, - can_serialize_predicate=_CONSTANT_TRUE) + can_serialize_predicate=_single_qubit_channel_check) def _phase_flip_channel_deserializer(): @@ -437,7 +448,7 @@ def _bit_flip_channel_serializer(): gate_type=cirq.BitFlipChannel, serialized_gate_id="BF", args=args, - can_serialize_predicate=_CONSTANT_TRUE) + can_serialize_predicate=_single_qubit_channel_check) def _bit_flip_channel_deserializer(): diff --git a/tensorflow_quantum/core/serialize/serializer_test.py b/tensorflow_quantum/core/serialize/serializer_test.py index 3a89a03ea..e0d2e4a36 100644 --- a/tensorflow_quantum/core/serialize/serializer_test.py +++ b/tensorflow_quantum/core/serialize/serializer_test.py @@ -720,6 +720,16 @@ def test_serialize_noise_channel_unsupported_value(self): with self.assertRaises(ValueError): serializer.serialize_circuit(simple_circuit) + def test_serialize_multi_qubit_noise_channel(self): + """Ensure multi-qubit noise channels are rejected with clear error.""" + q0 = cirq.GridQubit(0, 0) + q1 = cirq.GridQubit(0, 1) + multi_qubit_circuit = cirq.Circuit( + cirq.DepolarizingChannel(p=0.1, n_qubits=2)(q0, q1)) + with self.assertRaisesRegex(ValueError, + "Multi-qubit noise channels"): + serializer.serialize_circuit(multi_qubit_circuit) + @parameterized.parameters([{'inp': v} for v in ['wrong', 1.0, None, []]]) def test_serialize_circuit_wrong_type(self, inp): """Attempt to serialize invalid objects types."""