Repository: QCHackers/tqec (python)

Issue:
Add a `name` field to `Plaquette`
**Is your feature request related to a problem? Please describe.**
#350 requires to be able to hash `Plaquettes` instances, which in turn requires `Plaquette` instances to be hashable. Right now, the hash is not really robust: it is hashing the string representation of the underlying `stim.Circuit` instance, that **should** never be mutated, but that is not strictly enforced (we just cannot replace the `Plaquette.circuit` field, which does not mean that we cannot mutate the `circuit` field with mutating methods from `ScheduledCircuit`).

Moreover, another issue I had recently is my inability to display nicely `Plaquettes` instances when trying to debug the code.

**Describe the solution you'd like**
Adding a unique and descriptive name to `Plaquette` instances (that should be made immutable in a more robust way, #358 goes in this direction) would solve the 2 issues above:

1. hashing a `Plaquette` instance would just be hashing its name, which is nice because this is a constant (and quick) operation.
2. debugging will be nicer as I will be able to print `Plaquettes` instances and have a readable output (e.g., `{1: "XX_up", 3: "ZZZZ", default: "XXXX"}`).

Pull request description:
Add name to `Plaquette`
This PR adds a descriptive `name` field to each `Plaquette` instance. Because `Plaquette` instances are frozen, it also uses this unique name to implement `__eq__` and `__hash__`. Finally, it uses the name to represent a plaquette with `__str__`.

Fixes #360.

Patch:
diff --git a/src/tqec/plaquette/library/css.py b/src/tqec/plaquette/library/css.py
index 745c58c3..1901b144 100644
--- a/src/tqec/plaquette/library/css.py
+++ b/src/tqec/plaquette/library/css.py
@@ -6,9 +6,9 @@ from typing import Literal
 
 import stim
 
+from tqec.circuit.moment import Moment, iter_stim_circuit_without_repeat_by_moments
 from tqec.circuit.qubit_map import QubitMap
 from tqec.circuit.schedule import ScheduledCircuit
-from tqec.circuit.moment import Moment, iter_stim_circuit_without_repeat_by_moments
 from tqec.plaquette.enums import MeasurementBasis, PlaquetteSide, ResetBasis
 from tqec.plaquette.plaquette import Plaquette
 from tqec.plaquette.qubit import SquarePlaquetteQubits
@@ -43,6 +43,7 @@ def make_css_surface_code_plaquette(
 
 class _CSSPlaquetteBuilder:
     MERGEABLE_INSTRUCTIONS = frozenset(("M", "MZ", "MX", "R", "RZ", "RX"))
+    BASE_NAME = "CSS"
 
     def __init__(
         self,
@@ -57,9 +58,30 @@ class _CSSPlaquetteBuilder:
             {0: self._qubits.syndrome_qubits[0]}
             | {i + 1: q for i, q in enumerate(self._qubits.data_qubits)}
         )
+        self._data_init: tuple[ResetBasis, PlaquetteSide | None] | None = None
+        self._data_meas: tuple[MeasurementBasis, PlaquetteSide | None] | None = None
+
+    def _get_plaquette_name(self) -> str:
+        parts = [
+            _CSSPlaquetteBuilder.BASE_NAME,
+            f"basis({self._basis})",
+            self._x_boundary_orientation,
+        ]
+        if self._data_init is not None:
+            side_part = (
+                f",{self._data_init[1].name}" if self._data_init[1] is not None else ""
+            )
+            parts.append(f"datainit({self._data_init[0].name}{side_part})")
+        if self._data_meas is not None:
+            side_part = (
+                f",{self._data_meas[1].name}" if self._data_meas[1] is not None else ""
+            )
+            parts.append(f"datameas({self._data_meas[0].name}{side_part})")
+        return "_".join(parts)
 
     def build(self) -> Plaquette:
         return Plaquette(
+            self._get_plaquette_name(),
             self._qubits,
             ScheduledCircuit(
                 self._moments,
@@ -74,7 +96,12 @@ class _CSSPlaquetteBuilder:
         basis: ResetBasis | MeasurementBasis,
         only_on_side: PlaquetteSide | None = None,
     ) -> None:
-        moment_index = 0 if isinstance(basis, ResetBasis) else -1
+        if isinstance(basis, ResetBasis):
+            moment_index = 0
+            self._data_init = basis, only_on_side
+        else:
+            moment_index = -1
+            self._data_meas = basis, only_on_side
         self._moments[moment_index].append(
             f"{basis.instruction_name}", self._get_data_qubits(only_on_side), []
         )
diff --git a/src/tqec/plaquette/library/empty.py b/src/tqec/plaquette/library/empty.py
index 0d1e933e..a7f1548b 100644
--- a/src/tqec/plaquette/library/empty.py
+++ b/src/tqec/plaquette/library/empty.py
@@ -10,7 +10,7 @@ from tqec.plaquette.qubit import (
 
 
 def empty_plaquette(qubits: PlaquetteQubits) -> Plaquette:
-    return Plaquette(qubits, ScheduledCircuit.empty())
+    return Plaquette("empty", qubits, ScheduledCircuit.empty())
 
 
 def empty_square_plaquette() -> Plaquette:
diff --git a/src/tqec/plaquette/library/zxxz.py b/src/tqec/plaquette/library/zxxz.py
index 0d99c5d5..789d257b 100644
--- a/src/tqec/plaquette/library/zxxz.py
+++ b/src/tqec/plaquette/library/zxxz.py
@@ -43,6 +43,7 @@ def make_zxxz_surface_code_plaquette(
 
 class _ZXXZPlaquetteBuilder:
     MERGEABLE_INSTRUCTIONS = frozenset(("H", "R", "RZ", "M", "MZ"))
+    BASE_NAME = "ZXXZ"
 
     def __init__(
         self,
@@ -57,9 +58,30 @@ class _ZXXZPlaquetteBuilder:
             {0: self._qubits.syndrome_qubits[0]}
             | {i + 1: q for i, q in enumerate(self._qubits.data_qubits)}
         )
+        self._data_init: tuple[ResetBasis, PlaquetteSide | None] | None = None
+        self._data_meas: tuple[MeasurementBasis, PlaquetteSide | None] | None = None
+
+    def _get_plaquette_name(self) -> str:
+        parts = [
+            _ZXXZPlaquetteBuilder.BASE_NAME,
+            f"basis({self._basis})",
+            self._x_boundary_orientation,
+        ]
+        if self._data_init is not None:
+            side_part = (
+                f",{self._data_init[1].name}" if self._data_init[1] is not None else ""
+            )
+            parts.append(f"datainit({self._data_init[0].name}{side_part})")
+        if self._data_meas is not None:
+            side_part = (
+                f",{self._data_meas[1].name}" if self._data_meas[1] is not None else ""
+            )
+            parts.append(f"datameas({self._data_meas[0].name}{side_part})")
+        return "_".join(parts)
 
     def build(self) -> Plaquette:
         return Plaquette(
+            self._get_plaquette_name(),
             self._qubits,
             ScheduledCircuit(
                 self._moments,
@@ -78,9 +100,11 @@ class _ZXXZPlaquetteBuilder:
         if isinstance(basis, ResetBasis):
             self._moments[0].append("R", dqs_considered, [])
             h_moment_idx = 1
+            self._data_init = basis, only_on_side
         else:
             self._moments[-1].append("M", dqs_considered, [])
             h_moment_idx = -2
+            self._data_meas = basis, only_on_side
 
         basis_change_dqs = self._get_init_meas_basis_change_dqs(
             basis.value, dqs_considered
diff --git a/src/tqec/plaquette/plaquette.py b/src/tqec/plaquette/plaquette.py
index 6026e2a4..c93c4860 100644
--- a/src/tqec/plaquette/plaquette.py
+++ b/src/tqec/plaquette/plaquette.py
@@ -27,6 +27,11 @@ class Plaquette:
     X-axis pointing to the right and a Y-axis pointing down.
 
     Attributes:
+        name: a unique name for the plaquette. This field is used to compare
+            plaquettes efficiently (two :class:`Plaquette` instances are
+            considered equal if and only if their names are the same) as well as
+            computing a hash value for any plaquette instance. Finally, the name
+            is used to represent a :class:`Plaquette` instance as a string.
         qubits: qubits used by the plaquette circuit, given in the local
             plaquette coordinate system.
         circuit: scheduled quantum circuit implementing the computation that
@@ -40,6 +45,7 @@ class Plaquette:
             `qubits`.
     """
 
+    name: str
     qubits: PlaquetteQubits
     circuit: ScheduledCircuit
     mergeable_instructions: frozenset[str] = field(default_factory=frozenset)
@@ -59,14 +65,13 @@ class Plaquette:
         return Position2D(0, 0)
 
     def __eq__(self, rhs: object) -> bool:
-        return (
-            isinstance(rhs, Plaquette)
-            and self.qubits == rhs.qubits
-            and self.circuit == rhs.circuit
-        )
+        return isinstance(rhs, Plaquette) and self.name == rhs.name
 
     def __hash__(self) -> int:
-        return hash((self.qubits, str(self.circuit.get_circuit())))
+        return hash(self.name)
+
+    def __str__(self) -> str:
+        return self.name
 
     def project_on_boundary(
         self, projected_orientation: PlaquetteOrientation
@@ -98,7 +103,10 @@ class Plaquette:
             new_plaquette_qubits.all_qubits
         )
         return Plaquette(
-            new_plaquette_qubits, new_scheduled_circuit, self.mergeable_instructions
+            f"{self.name}_{projected_orientation.name}",
+            new_plaquette_qubits,
+            new_scheduled_circuit,
+            self.mergeable_instructions,
         )
 
 


Test patch:
diff --git a/src/tqec/plaquette/library/css_test.py b/src/tqec/plaquette/library/css_test.py
index f9d1b365..34181557 100644
--- a/src/tqec/plaquette/library/css_test.py
+++ b/src/tqec/plaquette/library/css_test.py
@@ -13,6 +13,7 @@ from tqec.plaquette.qubit import PlaquetteQubits, SquarePlaquetteQubits
 def test_css_surface_code_memory_plaquette() -> None:
     plaquette = make_css_surface_code_plaquette("X")
     assert plaquette.qubits == SquarePlaquetteQubits()
+    assert plaquette.name == "CSS_basis(X)_VERTICAL"
     circuit = plaquette.circuit.get_circuit()
     assert circuit.has_flow(stim.Flow("1 -> _XXXX xor rec[-1]"))
     assert circuit.has_flow(stim.Flow("_XXXX -> 1 xor rec[-1]"))
@@ -35,6 +36,7 @@ TICK
 MX 0
 """)
     plaquette = make_css_surface_code_plaquette("Z")
+    assert plaquette.name == "CSS_basis(Z)_VERTICAL"
     circuit = plaquette.circuit.get_circuit()
     assert circuit.has_flow(stim.Flow("1 -> _ZZZZ xor rec[-1]"))
     assert circuit.has_flow(stim.Flow("_ZZZZ -> 1 xor rec[-1]"))
@@ -59,6 +61,7 @@ M 0
     plaquette = make_css_surface_code_plaquette(
         "X", x_boundary_orientation="HORIZONTAL"
     )
+    assert plaquette.name == "CSS_basis(X)_HORIZONTAL"
     circuit = plaquette.circuit.get_circuit()
     assert circuit.has_flow(stim.Flow("1 -> _XXXX xor rec[-1]"))
     assert circuit.has_flow(stim.Flow("_XXXX -> 1 xor rec[-1]"))
@@ -83,6 +86,7 @@ MX 0
     plaquette = make_css_surface_code_plaquette(
         "Z", x_boundary_orientation="HORIZONTAL"
     )
+    assert plaquette.name == "CSS_basis(Z)_HORIZONTAL"
     circuit = plaquette.circuit.get_circuit()
     assert circuit.has_flow(stim.Flow("1 -> _ZZZZ xor rec[-1]"))
     assert circuit.has_flow(stim.Flow("_ZZZZ -> 1 xor rec[-1]"))
@@ -108,6 +112,7 @@ M 0
 
 def test_css_surface_code_init_meas_plaquette() -> None:
     plaquette = make_css_surface_code_plaquette("Z", ResetBasis.Z, MeasurementBasis.Z)
+    assert plaquette.name == "CSS_basis(Z)_VERTICAL_datainit(Z)_datameas(Z)"
     circuit = plaquette.circuit.get_circuit()
     assert circuit.has_flow(
         stim.Flow("1 -> 1 xor rec[-1] xor rec[-2] xor rec[-3] xor rec[-4] xor rec[-5]")
@@ -131,6 +136,7 @@ TICK
 M 0 1 2 3 4
 """)
     plaquette = make_css_surface_code_plaquette("X", ResetBasis.X, MeasurementBasis.X)
+    assert plaquette.name == "CSS_basis(X)_VERTICAL_datainit(X)_datameas(X)"
     circuit = plaquette.circuit.get_circuit()
     assert circuit.has_flow(
         stim.Flow("1 -> 1 xor rec[-1] xor rec[-2] xor rec[-3] xor rec[-4] xor rec[-5]")
@@ -157,8 +163,10 @@ MX 0 1 2 3 4
 
 def test_css_surface_code_projected_plaquette() -> None:
     plaquette = make_css_surface_code_plaquette("X")
+    assert plaquette.name == "CSS_basis(X)_VERTICAL"
     qubits = plaquette.qubits
     plaquette_up = plaquette.project_on_boundary(PlaquetteOrientation.UP)
+    assert plaquette_up.name == "CSS_basis(X)_VERTICAL_UP"
     assert plaquette_up.qubits == PlaquetteQubits(
         data_qubits=qubits.get_qubits_on_side(PlaquetteSide.DOWN),
         syndrome_qubits=qubits.syndrome_qubits,
@@ -180,6 +188,7 @@ TICK
 MX 0
 """)
     plaquette_down = plaquette.project_on_boundary(PlaquetteOrientation.DOWN)
+    assert plaquette_down.name == "CSS_basis(X)_VERTICAL_DOWN"
     assert plaquette_down.qubits == PlaquetteQubits(
         data_qubits=qubits.get_qubits_on_side(PlaquetteSide.UP),
         syndrome_qubits=qubits.syndrome_qubits,
@@ -201,6 +210,7 @@ TICK
 MX 0
 """)
     plaquette_left = plaquette.project_on_boundary(PlaquetteOrientation.LEFT)
+    assert plaquette_left.name == "CSS_basis(X)_VERTICAL_LEFT"
     assert plaquette_left.qubits == PlaquetteQubits(
         data_qubits=qubits.get_qubits_on_side(PlaquetteSide.RIGHT),
         syndrome_qubits=qubits.syndrome_qubits,
@@ -222,6 +232,7 @@ TICK
 MX 0
 """)
     plaquette_right = plaquette.project_on_boundary(PlaquetteOrientation.RIGHT)
+    assert plaquette_right.name == "CSS_basis(X)_VERTICAL_RIGHT"
     assert plaquette_right.qubits == PlaquetteQubits(
         data_qubits=qubits.get_qubits_on_side(PlaquetteSide.LEFT),
         syndrome_qubits=qubits.syndrome_qubits,
@@ -250,6 +261,7 @@ def test_css_surface_code_init_meas_only() -> None:
         data_initialization=ResetBasis.X,
         init_meas_only_on_side=PlaquetteSide.RIGHT,
     )
+    assert plaquette.name == "CSS_basis(X)_VERTICAL_datainit(X,RIGHT)"
     circuit = plaquette.circuit.get_circuit()
     assert circuit == stim.Circuit("""
 QUBIT_COORDS(0, 0) 0
@@ -274,6 +286,7 @@ MX 0
         data_measurement=MeasurementBasis.Z,
         init_meas_only_on_side=PlaquetteSide.UP,
     )
+    assert plaquette.name == "CSS_basis(Z)_VERTICAL_datameas(Z,UP)"
     circuit = plaquette.circuit.get_circuit()
     assert circuit == stim.Circuit("""
 QUBIT_COORDS(0, 0) 0
diff --git a/src/tqec/plaquette/library/zxxz_test.py b/src/tqec/plaquette/library/zxxz_test.py
index e14cd87b..57cc1f91 100644
--- a/src/tqec/plaquette/library/zxxz_test.py
+++ b/src/tqec/plaquette/library/zxxz_test.py
@@ -13,6 +13,7 @@ from tqec.plaquette.qubit import PlaquetteQubits, SquarePlaquetteQubits
 def test_zxxz_surface_code_memory_plaquette() -> None:
     plaquette = make_zxxz_surface_code_plaquette("X")
     assert plaquette.qubits == SquarePlaquetteQubits()
+    assert plaquette.name == "ZXXZ_basis(X)_VERTICAL"
     assert "H" in plaquette.mergeable_instructions
     circuit = plaquette.circuit.get_circuit()
     assert circuit.has_flow(stim.Flow("_ZXXZ -> Z____"))
@@ -46,6 +47,7 @@ M 0
 
     plaquette = make_zxxz_surface_code_plaquette("Z")
     assert plaquette.qubits == SquarePlaquetteQubits()
+    assert plaquette.name == "ZXXZ_basis(Z)_VERTICAL"
     circuit = plaquette.circuit.get_circuit()
     assert circuit.has_flow(stim.Flow("_ZXXZ -> Z____"))
     assert circuit.has_flow(stim.Flow("1 -> _ZXXZ xor rec[-1]"))
@@ -79,6 +81,7 @@ M 0
     plaquette = make_zxxz_surface_code_plaquette(
         "X", x_boundary_orientation="HORIZONTAL"
     )
+    assert plaquette.name == "ZXXZ_basis(X)_HORIZONTAL"
     circuit = plaquette.circuit.get_circuit()
     assert circuit.has_flow(stim.Flow("_XZZX -> Z____"))
     assert circuit.has_flow(stim.Flow("1 -> _XZZX xor rec[-1]"))
@@ -116,6 +119,7 @@ def test_zxxz_surface_code_init_meas_plaquette() -> None:
         ResetBasis.Z,
         MeasurementBasis.Z,
     )
+    assert plaquette.name == "ZXXZ_basis(Z)_VERTICAL_datainit(Z)_datameas(Z)"
     circuit = plaquette.circuit.get_circuit()
     assert circuit.has_flow(
         stim.Flow("1 -> 1 xor rec[-1] xor rec[-2] xor rec[-3] xor rec[-4] xor rec[-5]")
@@ -151,6 +155,7 @@ M 0 1 2 3 4
         ResetBasis.X,
         MeasurementBasis.X,
     )
+    assert plaquette.name == "ZXXZ_basis(X)_VERTICAL_datainit(X)_datameas(X)"
     circuit = plaquette.circuit.get_circuit()
     assert circuit.has_flow(
         stim.Flow("1 -> 1 xor rec[-1] xor rec[-2] xor rec[-3] xor rec[-4] xor rec[-5]")
@@ -188,6 +193,9 @@ M 0 1 2 3 4
         x_boundary_orientation="HORIZONTAL",
         init_meas_only_on_side=PlaquetteSide.RIGHT,
     )
+    assert (
+        plaquette.name == "ZXXZ_basis(Z)_HORIZONTAL_datainit(Z,RIGHT)_datameas(Z,RIGHT)"
+    )
     circuit = plaquette.circuit.get_circuit()
     assert circuit.has_flow(stim.Flow("1 -> _Z_X_ xor rec[-1] xor rec[-2] xor rec[-3]"))
     assert circuit == stim.Circuit("""
@@ -220,12 +228,14 @@ M 0 2 4
 
 def test_zxxz_surface_code_projected_plaquette() -> None:
     plaquette = make_zxxz_surface_code_plaquette("X", ResetBasis.X, MeasurementBasis.X)
+    assert plaquette.name == "ZXXZ_basis(X)_VERTICAL_datainit(X)_datameas(X)"
     qubits = plaquette.qubits
     plaquette_up = plaquette.project_on_boundary(PlaquetteOrientation.UP)
     assert plaquette_up.qubits == PlaquetteQubits(
         data_qubits=qubits.get_qubits_on_side(PlaquetteSide.DOWN),
         syndrome_qubits=qubits.syndrome_qubits,
     )
+    assert plaquette_up.name == "ZXXZ_basis(X)_VERTICAL_datainit(X)_datameas(X)_UP"
     circuit = plaquette_up.circuit.get_circuit()
     assert circuit.has_flow(stim.Flow("1 -> 1 xor rec[-1] xor rec[-2] xor rec[-3]"))
     assert circuit == stim.Circuit("""
@@ -251,6 +261,7 @@ TICK
 M 0 1 2
 """)
     plaquette_down = plaquette.project_on_boundary(PlaquetteOrientation.DOWN)
+    assert plaquette_down.name == "ZXXZ_basis(X)_VERTICAL_datainit(X)_datameas(X)_DOWN"
     assert plaquette_down.qubits == PlaquetteQubits(
         data_qubits=qubits.get_qubits_on_side(PlaquetteSide.UP),
         syndrome_qubits=qubits.syndrome_qubits,
@@ -280,6 +291,7 @@ TICK
 M 0 1 2
 """)
     plaquette_left = plaquette.project_on_boundary(PlaquetteOrientation.LEFT)
+    assert plaquette_left.name == "ZXXZ_basis(X)_VERTICAL_datainit(X)_datameas(X)_LEFT"
     assert plaquette_left.qubits == PlaquetteQubits(
         data_qubits=qubits.get_qubits_on_side(PlaquetteSide.RIGHT),
         syndrome_qubits=qubits.syndrome_qubits,
@@ -309,6 +321,9 @@ TICK
 M 0 1 2
 """)
     plaquette_right = plaquette.project_on_boundary(PlaquetteOrientation.RIGHT)
+    assert (
+        plaquette_right.name == "ZXXZ_basis(X)_VERTICAL_datainit(X)_datameas(X)_RIGHT"
+    )
     assert plaquette_right.qubits == PlaquetteQubits(
         data_qubits=qubits.get_qubits_on_side(PlaquetteSide.LEFT),
         syndrome_qubits=qubits.syndrome_qubits,
@@ -343,6 +358,7 @@ def test_zxxz_surface_code_init_meas_only_on_side() -> None:
     plaquette = make_zxxz_surface_code_plaquette(
         "X", ResetBasis.X, init_meas_only_on_side=PlaquetteSide.RIGHT
     )
+    assert plaquette.name == "ZXXZ_basis(X)_VERTICAL_datainit(X,RIGHT)"
     circuit = plaquette.circuit.get_circuit()
     assert circuit.has_flow(stim.Flow("1 -> _ZXXZ xor rec[-1]"))
     assert circuit == stim.Circuit("""
@@ -376,6 +392,7 @@ M 0
         data_measurement=MeasurementBasis.Z,
         init_meas_only_on_side=PlaquetteSide.UP,
     )
+    assert plaquette.name == "ZXXZ_basis(Z)_VERTICAL_datameas(Z,UP)"
     circuit = plaquette.circuit.get_circuit()
     assert circuit.has_flow(stim.Flow("1 -> ___XZ xor rec[-1] xor rec[-2] xor rec[-3]"))
     assert circuit == stim.Circuit("""