From a51c5284db4bc834dcfcd3278e6ed030c06be5c8 Mon Sep 17 00:00:00 2001 From: adlantz Date: Fri, 4 Sep 2026 09:43:31 -0700 Subject: [PATCH 1/2] serializer shallow copy --- .../core/serialize/serializer.py | 14 +++++++---- .../core/serialize/serializer_test.py | 23 +++++++++++++++++++ 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/tensorflow_quantum/core/serialize/serializer.py b/tensorflow_quantum/core/serialize/serializer.py index 323e70025..7324eea3a 100644 --- a/tensorflow_quantum/core/serialize/serializer.py +++ b/tensorflow_quantum/core/serialize/serializer.py @@ -815,10 +815,14 @@ def serialize_circuit(circuit_inp): Returns: A `tfq.proto.Program` proto. """ - circuit = copy.deepcopy(circuit_inp) - if not isinstance(circuit, cirq.Circuit): + if not isinstance(circuit_inp, cirq.Circuit): raise TypeError("serialize requires cirq.Circuit objects." - " Given: " + str(type(circuit))) + " Given: " + str(type(circuit_inp))) + + # A shallow copy is enough. The rewrites below replace whole moments + # rather than mutating them in place, and the one operation that does get + # mutated (control demotion, further down) is copied before tagging. + circuit = circuit_inp.copy() # This code is intentionally written to avoid using cirq functions # as this get analyzed by tensorflow-autograph. @@ -870,7 +874,9 @@ def serialize_circuit(circuit_inp): ] new_ops = dict() for op in controlled_ops: - tfq_compatible = op.sub_operation + # Copy before tagging, `sub_operation` is a live reference into + # the caller's circuit and these attributes would leak onto it. + tfq_compatible = copy.copy(op.sub_operation) tfq_compatible._tfq_control_qubits = op.controls tfq_compatible._tfq_control_values = op.control_values new_ops[op.qubits] = tfq_compatible diff --git a/tensorflow_quantum/core/serialize/serializer_test.py b/tensorflow_quantum/core/serialize/serializer_test.py index 3a89a03ea..36165e8fb 100644 --- a/tensorflow_quantum/core/serialize/serializer_test.py +++ b/tensorflow_quantum/core/serialize/serializer_test.py @@ -892,6 +892,29 @@ def test_terminal_measurement_support(self): with self.assertRaisesRegex(ValueError, expected_regex="non-terminal"): serializer.serialize_circuit(invalid_circuit) + def test_serialize_does_not_tag_input_circuit(self): + """Serialization must not leave control tags on the input circuit.""" + q0 = cirq.GridQubit(0, 0) + q1 = cirq.GridQubit(0, 1) + q2 = cirq.GridQubit(0, 2) + circuit = cirq.Circuit( + [cirq.H(q0), + cirq.ControlledOperation([q0], cirq.ISWAP(q1, q2)**0.5)]) + circuit_before_call = copy.deepcopy(circuit) + + serializer.serialize_circuit(circuit) + + # Note that `==` cannot catch this. The demotion tags are plain Python + # attributes and take no part in circuit or operation equality, so they + # have to be checked for directly. + for moment in circuit: + for op in moment: + sub_op = getattr(op, 'sub_operation', op) + self.assertFalse(hasattr(sub_op, '_tfq_control_qubits')) + self.assertFalse(hasattr(sub_op, '_tfq_control_values')) + + self.assertEqual(circuit, circuit_before_call) + def test_serialize_deserialize_identity(self): """Confirm that identity gates can be serialized and deserialized.""" q0 = cirq.GridQubit(0, 0) From 8011ff03735e86bd49c4b35300b7df7768a6c0d6 Mon Sep 17 00:00:00 2001 From: adlantz Date: Fri, 4 Sep 2026 10:01:18 -0700 Subject: [PATCH 2/2] style fixes --- tensorflow_quantum/core/serialize/serializer.py | 5 +++-- tensorflow_quantum/core/serialize/serializer_test.py | 8 +++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/tensorflow_quantum/core/serialize/serializer.py b/tensorflow_quantum/core/serialize/serializer.py index 7324eea3a..3fa350bbe 100644 --- a/tensorflow_quantum/core/serialize/serializer.py +++ b/tensorflow_quantum/core/serialize/serializer.py @@ -808,6 +808,7 @@ def serialize_circuit(circuit_inp): and `cirq.LineQubit` instances during serialization of circuits. Note: once serialized terminal measurements are removed. + Note: the input circuit is not modified. Args: circuit_inp: A `cirq.Circuit`. @@ -874,8 +875,8 @@ def serialize_circuit(circuit_inp): ] new_ops = dict() for op in controlled_ops: - # Copy before tagging, `sub_operation` is a live reference into - # the caller's circuit and these attributes would leak onto it. + # Copy before tagging: `sub_operation` is a live reference into + # the caller's circuit, and these attributes would leak onto it. tfq_compatible = copy.copy(op.sub_operation) tfq_compatible._tfq_control_qubits = op.controls tfq_compatible._tfq_control_values = op.control_values diff --git a/tensorflow_quantum/core/serialize/serializer_test.py b/tensorflow_quantum/core/serialize/serializer_test.py index 36165e8fb..58f273c1f 100644 --- a/tensorflow_quantum/core/serialize/serializer_test.py +++ b/tensorflow_quantum/core/serialize/serializer_test.py @@ -897,9 +897,11 @@ def test_serialize_does_not_tag_input_circuit(self): q0 = cirq.GridQubit(0, 0) q1 = cirq.GridQubit(0, 1) q2 = cirq.GridQubit(0, 2) - circuit = cirq.Circuit( - [cirq.H(q0), - cirq.ControlledOperation([q0], cirq.ISWAP(q1, q2)**0.5)]) + circuit = cirq.Circuit([ + cirq.H(q0), + cirq.ControlledOperation([q0], + cirq.ISWAP(q1, q2)**0.5) + ]) circuit_before_call = copy.deepcopy(circuit) serializer.serialize_circuit(circuit)