Repository: quantumlib/Cirq
Pull request #7307: Add a symbolize transformer
Author: login: babacry
Labels: name: size: M
description: 50< lines changed <250
Created: 2025-04-30T05:05:13Z | Merged: 2025-05-17T01:23:07Z

Symbolizes single qubit gates by tags.

Example:
Input:
```
        0: ───X[phxz_0]───@───Y[phxz_1]───X───
                          │
        1: ───────────────@───────────────────
```
`$  cirq.symbolize_single_qubit_gates_by_indexed_tags(c, tag_prefix="phxz")`
Output:
```
        0: ───PhXZ(a=a0,x=x0,z=z0)───@───PhXZ(a=a1,x=x1,z=z1)───X───
                                     │
        1: ──────────────────────────@──────────────────────────────
```
where, a0,x0,z0,a1,x1,z1 are symbols.

Context: to be used in https://github.com/quantumlib/Cirq/issues/6994.




Diff:
diff --git a/cirq-core/cirq/__init__.py b/cirq-core/cirq/__init__.py
index bda16fc332f..76f90129b5e 100644
--- a/cirq-core/cirq/__init__.py
+++ b/cirq-core/cirq/__init__.py
@@ -397,6 +397,7 @@
     single_qubit_matrix_to_phxz as single_qubit_matrix_to_phxz,
     single_qubit_op_to_framed_phase_form as single_qubit_op_to_framed_phase_form,
     stratified_circuit as stratified_circuit,
+    symbolize_single_qubit_gates_by_indexed_tags as symbolize_single_qubit_gates_by_indexed_tags,
     synchronize_terminal_measurements as synchronize_terminal_measurements,
     TRANSFORMER as TRANSFORMER,
     TransformerContext as TransformerContext,
diff --git a/cirq-core/cirq/transformers/__init__.py b/cirq-core/cirq/transformers/__init__.py
index 427ba83e041..4dd744d2b09 100644
--- a/cirq-core/cirq/transformers/__init__.py
+++ b/cirq-core/cirq/transformers/__init__.py
@@ -135,6 +135,11 @@
     unroll_circuit_op_greedy_frontier as unroll_circuit_op_greedy_frontier,
 )
 
+from cirq.transformers.symbolize import (
+    SymbolizeTag as SymbolizeTag,
+    symbolize_single_qubit_gates_by_indexed_tags as symbolize_single_qubit_gates_by_indexed_tags,
+)
+
 from cirq.transformers.gauge_compiling import (
     CZGaugeTransformer as CZGaugeTransformer,
     ConstantGauge as ConstantGauge,
diff --git a/cirq-core/cirq/transformers/symbolize.py b/cirq-core/cirq/transformers/symbolize.py
new file mode 100644
index 00000000000..4dd434b2aba
--- /dev/null
+++ b/cirq-core/cirq/transformers/symbolize.py
@@ -0,0 +1,101 @@
+# Copyright 2025 The Cirq Developers
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Transformers that symbolize operations."""
+
+import re
+from typing import Hashable, Optional, TYPE_CHECKING
+
+import attrs
+import sympy
+from attrs import validators
+
+from cirq import ops
+from cirq.transformers import transformer_api, transformer_primitives
+
+if TYPE_CHECKING:
+    import cirq
+
+
+@attrs.frozen
+class SymbolizeTag:
+    prefix: str = attrs.field(
+        validator=validators.and_(validators.instance_of(str), validators.min_len(1))
+    )
+
+
+@transformer_api.transformer
+def symbolize_single_qubit_gates_by_indexed_tags(
+    circuit: 'cirq.AbstractCircuit',
+    *,
+    context: Optional['cirq.TransformerContext'] = None,
+    symbolize_tag: SymbolizeTag = SymbolizeTag(prefix="TO-PHXZ"),
+) -> 'cirq.Circuit':
+    """Symbolizes single qubit operations by indexed tags prefixed by symbolize_tag.prefix.
+
+    Example:
+        >>> from cirq import transformers
+        >>> q0, q1 = cirq.LineQubit.range(2)
+        >>> c = cirq.Circuit(
+        ...         cirq.X(q0).with_tags("phxz_0"),
+        ...         cirq.CZ(q0,q1),
+        ...         cirq.Y(q0).with_tags("phxz_1"),
+        ...         cirq.X(q0))
+        >>> print(c)
+        0: ───X[phxz_0]───@───Y[phxz_1]───X───
+                          │
+        1: ───────────────@───────────────────
+        >>> new_circuit = cirq.symbolize_single_qubit_gates_by_indexed_tags(
+        ...     c, symbolize_tag=transformers.SymbolizeTag(prefix="phxz"))
+        >>> print(new_circuit)
+        0: ───PhXZ(a=a0,x=x0,z=z0)───@───PhXZ(a=a1,x=x1,z=z1)───X───
+                                     │
+        1: ──────────────────────────@──────────────────────────────
+
+    Args:
+        circuit: Input circuit to apply the transformations on. The input circuit is not mutated.
+        context: `cirq.TransformerContext` storing common configurable options for transformers.
+        symbolize_tag: The tag info used to symbolize the phxz gate. Prefix is required.
+
+    Returns:
+        Copy of the transformed input circuit.
+    """
+
+    def _map_func(op: 'cirq.Operation', _):
+        """Maps an op with tag `{tag_prefix}_i` to a symbolzied `PhasedXZGate(xi,zi,ai)`."""
+        tags: set[Hashable] = set(op.tags)
+        tag_id: None | int = None
+        for tag in tags:
+            if re.fullmatch(f"{symbolize_tag.prefix}_\\d+", str(tag)):
+                if tag_id is None:
+                    tag_id = int(str(tag).rsplit("_", maxsplit=-1)[-1])
+                else:
+                    raise ValueError(f"Multiple tags are prefixed with {symbolize_tag.prefix}.")
+        if tag_id is None:
+            return op
+        tags.remove(f"{symbolize_tag.prefix}_{tag_id}")
+        phxz_params = {
+            "x_exponent": sympy.Symbol(f"x{tag_id}"),
+            "z_exponent": sympy.Symbol(f"z{tag_id}"),
+            "axis_phase_exponent": sympy.Symbol(f"a{tag_id}"),
+        }
+
+        return ops.PhasedXZGate(**phxz_params).on(*op.qubits).with_tags(*tags)
+
+    return transformer_primitives.map_operations(
+        circuit.freeze(),
+        _map_func,
+        deep=context.deep if context else False,
+        tags_to_ignore=context.tags_to_ignore if context else [],
+    ).unfreeze(copy=False)
diff --git a/cirq-core/cirq/transformers/symbolize_test.py b/cirq-core/cirq/transformers/symbolize_test.py
new file mode 100644
index 00000000000..224ea607d53
--- /dev/null
+++ b/cirq-core/cirq/transformers/symbolize_test.py
@@ -0,0 +1,60 @@
+# Copyright 2025 The Cirq Developers
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+import sympy
+
+import cirq
+from cirq.transformers.symbolize import SymbolizeTag
+
+
+def test_symbolize_single_qubit_gates_by_indexed_tags_success():
+    q = cirq.NamedQubit("a")
+    input_circuit = cirq.Circuit(
+        cirq.X(q).with_tags("phxz_1"), cirq.Y(q).with_tags("tag1"), cirq.Z(q).with_tags("phxz_0")
+    )
+    output_circuit = cirq.symbolize_single_qubit_gates_by_indexed_tags(
+        input_circuit, symbolize_tag=SymbolizeTag(prefix="phxz")
+    )
+    cirq.testing.assert_same_circuits(
+        output_circuit,
+        cirq.Circuit(
+            cirq.PhasedXZGate(
+                x_exponent=sympy.Symbol("x1"),
+                z_exponent=sympy.Symbol("z1"),
+                axis_phase_exponent=sympy.Symbol("a1"),
+            ).on(q),
+            cirq.Y(q).with_tags("tag1"),
+            cirq.PhasedXZGate(
+                x_exponent=sympy.Symbol("x0"),
+                z_exponent=sympy.Symbol("z0"),
+                axis_phase_exponent=sympy.Symbol("a0"),
+            ).on(q),
+        ),
+    )
+
+
+def test_symbolize_single_qubit_gates_by_indexed_tags_multiple_tags():
+    q = cirq.NamedQubit("a")
+    input_circuit = cirq.Circuit(cirq.X(q).with_tags("TO-PHXZ_0", "TO-PHXZ_2"))
+
+    with pytest.raises(ValueError, match="Multiple tags are prefixed with TO-PHXZ."):
+        cirq.symbolize_single_qubit_gates_by_indexed_tags(input_circuit)
+
+
+def test_symbolize_tag_invalid_prefix():
+    with pytest.raises(ValueError, match="Length of 'prefix' must be >= 1: 0"):
+        SymbolizeTag(prefix="")
+    with pytest.raises(TypeError, match="'prefix' must be <class 'str'>"):
+        SymbolizeTag(prefix=[1])


Reviews:
user: login: NoureldinYosri
body: overall looks good but lets make the tag a class instead of a string, for example

```py3
@attrs.frozen
class SymbolizeTag:
    prefix: str
```
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-05-05T18:08:00Z
user: login: babacry
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-08T05:09:10Z
user: login: NoureldinYosri
body: LGTM%nit
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-16T17:06:28Z
user: login: babacry
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-17T00:20:11Z

Review comments:
diff_hunk: @@ -0,0 +1,89 @@
+# Copyright 2025 The Cirq Developers
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import re
+from typing import Hashable, Optional, TYPE_CHECKING
+
+import sympy
+
+from cirq import ops
+from cirq.transformers import transformer_api, transformer_primitives
+
+if TYPE_CHECKING:
+    import cirq
+
+
+@transformer_api.transformer
+def symbolize_single_qubit_gates_by_indexed_tags(
+    circuit: 'cirq.AbstractCircuit',
+    *,
+    context: Optional['cirq.TransformerContext'] = None,
+    tag_prefix: Optional[str] = "TO-PHXZ",
path: cirq-core/cirq/transformers/symbolize.py
user: login: NoureldinYosri
body: what does it mean for this parameter to be optional? ... shouldn't it be just `str`
created_at: 2025-05-05T18:04:45Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,89 @@
+# Copyright 2025 The Cirq Developers
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import re
+from typing import Hashable, Optional, TYPE_CHECKING
+
+import sympy
+
+from cirq import ops
+from cirq.transformers import transformer_api, transformer_primitives
+
+if TYPE_CHECKING:
+    import cirq
+
+
+@transformer_api.transformer
+def symbolize_single_qubit_gates_by_indexed_tags(
+    circuit: 'cirq.AbstractCircuit',
+    *,
+    context: Optional['cirq.TransformerContext'] = None,
+    tag_prefix: Optional[str] = "TO-PHXZ",
path: cirq-core/cirq/transformers/symbolize.py
user: login: babacry
body: Yeah, shouldn't have optional here. Good catch! Updated.
created_at: 2025-05-08T05:09:10Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,101 @@
+# Copyright 2025 The Cirq Developers
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Transformers that symbolizes operations."""
+
+import re
+from typing import Hashable, Optional, TYPE_CHECKING
+
+import attr
+import sympy
+
+from cirq import ops
+from cirq.transformers import transformer_api, transformer_primitives
+
+if TYPE_CHECKING:
+    import cirq
+
+
+@attr.frozen
+class SymbolizeTag:
+    prefix: str
+
+
+@transformer_api.transformer
+def symbolize_single_qubit_gates_by_indexed_tags(
+    circuit: 'cirq.AbstractCircuit',
+    *,
+    context: Optional['cirq.TransformerContext'] = None,
+    symbolize_tag: SymbolizeTag = SymbolizeTag(prefix="TO-PHXZ"),
+) -> 'cirq.Circuit':
+    """Symbolizes single qubit operations by indexed tags prefixed by symbolize_tag.prefix.
+
+    Example:
+        >>> from cirq import transformers
+        >>> q0, q1 = cirq.LineQubit.range(2)
+        >>> c = cirq.Circuit(
+        ...         cirq.X(q0).with_tags("phxz_0"),
+        ...         cirq.CZ(q0,q1),
+        ...         cirq.Y(q0).with_tags("phxz_1"),
+        ...         cirq.X(q0))
+        >>> print(c)
+        0: ───X[phxz_0]───@───Y[phxz_1]───X───
+                          │
+        1: ───────────────@───────────────────
+        >>> new_circuit = cirq.symbolize_single_qubit_gates_by_indexed_tags(
+        ...     c, symbolize_tag=transformers.SymbolizeTag(prefix="phxz"))
+        >>> print(new_circuit)
+        0: ───PhXZ(a=a0,x=x0,z=z0)───@───PhXZ(a=a1,x=x1,z=z1)───X───
+                                     │
+        1: ──────────────────────────@──────────────────────────────
+
+    Args:
+        circuit: Input circuit to apply the transformations on. The input circuit is not mutated.
+        context: `cirq.TransformerContext` storing common configurable options for transformers.
+        symbolize_tag: The tag info used to symbolize the phxz gate. Prefix is required.
+
+    Returns:
+        Copy of the transformed input circuit.
+    """
+
+    if not symbolize_tag.prefix:
path: cirq-core/cirq/transformers/symbolize.py
user: login: NoureldinYosri
body: lets move this to the constructor of the tag, see the other comment
created_at: 2025-05-16T17:04:31Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,101 @@
+# Copyright 2025 The Cirq Developers
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Transformers that symbolizes operations."""
+
+import re
+from typing import Hashable, Optional, TYPE_CHECKING
+
+import attr
+import sympy
+
+from cirq import ops
+from cirq.transformers import transformer_api, transformer_primitives
+
+if TYPE_CHECKING:
+    import cirq
+
+
+@attr.frozen
+class SymbolizeTag:
+    prefix: str
path: cirq-core/cirq/transformers/symbolize.py
user: login: NoureldinYosri
body: ```suggestion
    prefix: str = attrs.field(validator=lambda s: len(s) > 0)
```

created_at: 2025-05-16T17:05:27Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,101 @@
+# Copyright 2025 The Cirq Developers
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Transformers that symbolizes operations."""
+
+import re
+from typing import Hashable, Optional, TYPE_CHECKING
+
+import attr
+import sympy
+
+from cirq import ops
+from cirq.transformers import transformer_api, transformer_primitives
+
+if TYPE_CHECKING:
+    import cirq
+
+
+@attr.frozen
+class SymbolizeTag:
+    prefix: str
path: cirq-core/cirq/transformers/symbolize.py
user: login: babacry
body: Done.
created_at: 2025-05-17T00:20:11Z
author_association: COLLABORATOR

PR comments:
user: login: codecov[bot]
created_at: 2025-04-30T05:19:47Z
body: ## [Codecov](https://app.codecov.io/gh/quantumlib/Cirq/pull/7307?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=quantumlib) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 98.67%. Comparing base [(`716549e`)](https://app.codecov.io/gh/quantumlib/Cirq/commit/716549efd5d81da4cf1c3fca4aab7de044390488?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=quantumlib) to head [(`059b5f2`)](https://app.codecov.io/gh/quantumlib/Cirq/commit/059b5f2907b6b1972ed66879f5fc8d05f5b2e997?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=quantumlib).
> Report is 1 commits behind head on main.

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             main    #7307   +/-   ##
=======================================
  Coverage   98.67%   98.67%           
=======================================
  Files        1111     1113    +2     
  Lines       97012    97058   +46     
=======================================
+ Hits        95729    95775   +46     
  Misses       1283     1283           
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/quantumlib/Cirq/pull/7307?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=quantumlib).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=quantumlib).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: babacry
created_at: 2025-05-01T21:55:31Z
body: @NoureldinYosri , phxz symbolizer transformer! Could you help review?
author_association: COLLABORATOR
user: login: babacry
created_at: 2025-05-08T05:09:28Z
body: > overall looks good but lets make the tag a class instead of a string, for example
> 
> ```python
> @attrs.frozen
> class SymbolizeTag:
>     prefix: str
> ```

Good point, done.
author_association: COLLABORATOR