Repository: QCHackers/tqec
Pull request #213 (2024-04-22 15:19:01)

Observables
closes: #141

I started to develop this, and I have some open questions:

- I've implemented a test for the `RawRectangleTemplate`. @nelimee, can you clarify if this is the intended behavior?
- What should the scope of this Issue/PR be? Only build this for the available templates, including moves and so on.
- How do we define midlines for uneven-sized templates, e.g., where the number of qubit rows is even?

Todos:
- [ ] Add all templates
- [ ] Write tests, including scaling
- [ ] Figure out edge cases: Moving, uneven ...
- [ ] Documentation

afowler: In my opinion there would be very little benefit and significant additional
complexity allowing even code distances, so I would support not allowing
this at all and consequently always having well defined mid lines.

Best,
Austin.


On Mon, Apr 22, 2024, 8:19 AM Philipp Seitz ***@***.***>
wrote:

> closes: #141 <https://github.com/QCHackers/tqec/issues/141>
>
> I started to develop this, and I have some open questions:
>
>    - I've implemented a test for the RawRectangleTemplate. @nelimee
>    <https://github.com/nelimee>, can you clarify if this is the intended
>    behavior?
>    - What should the scope of this Issue/PR be? Only build this for the
>    available templates, including moves and so on.
>    - How do we define midlines for uneven-sized templates, e.g., where
>    the number of qubit rows is even?
>
> Todos:
>
>    - Add all templates
>    - Write tests, including scaling
>    - Figure out edge cases: Moving, uneven ...
>    - Documentation
>
> ------------------------------
> You can view, comment on, or merge this pull request online at:
>
>   https://github.com/QCHackers/tqec/pull/213
> Commit Summary
>
>    - 4f5cd82
>    <https://github.com/QCHackers/tqec/pull/213/commits/4f5cd823cdcc0e6290e1bfc6a2ef016a71e681b6>
>    Add pytest as explicit dependency
>    - a0ad6d9
>    <https://github.com/QCHackers/tqec/pull/213/commits/a0ad6d9959910af255397b81ad96f1161134d0e9>
>    Add new abstract method to base template
>    - 17db053
>    <https://github.com/QCHackers/tqec/pull/213/commits/17db0537143c503210b33ddf8650b5a98caa10f3>
>    Add initial rectangle implementation
>
> File Changes
>
> (4 files <https://github.com/QCHackers/tqec/pull/213/files>)
>
>    - *M* requirements-dev.txt
>    <https://github.com/QCHackers/tqec/pull/213/files#diff-2b4945591edfeaa4cf4d3f155e66d4b43d1bda7a55d881d5cf3107f1b05abbbc>
>    (3)
>    - *M* src/tqec/templates/atomic/rectangle.py
>    <https://github.com/QCHackers/tqec/pull/213/files#diff-8b5421da61ed89127cacca5e2b190c31922a1c751c8197b0037126c1bde61b26>
>    (43)
>    - *M* src/tqec/templates/atomic/rectangle_test.py
>    <https://github.com/QCHackers/tqec/pull/213/files#diff-9596ec56c970721b10b588d25b854f559278da999c8e57993fb6d4ee752374a9>
>    (26)
>    - *M* src/tqec/templates/base.py
>    <https://github.com/QCHackers/tqec/pull/213/files#diff-3f1946f8f161e910c0c51cfbcae118a9e500fc25c8a93b908ac5ee5715d7f5eb>
>    (17)
>
> Patch Links:
>
>    - https://github.com/QCHackers/tqec/pull/213.patch
>    - https://github.com/QCHackers/tqec/pull/213.diff
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/QCHackers/tqec/pull/213>, or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AAKAXTAR2AFW3WUIWSIA7CLY6US7VAVCNFSM6AAAAABGS75KBKVHI2DSMVQWIX3LMV43ASLTON2WKOZSGI2TMOBSGQYDSMA>
> .
> You are receiving this because you are subscribed to this thread.Message
> ID: ***@***.***>
>

nelimee: > I've implemented a test for the `RawRectangleTemplate`. @nelimee, can you clarify if this is the intended behavior?

The output you fixed is dependent on the actual `Plaquette` implementation. What if the origin (i.e., `(0, 0)` point) was on the top-left of the `Plaquette`? Then both the output coordinates would change. 
I cannot remember if we completely ruled out that case though.

In any case, I think this method needs the `Plaquette` instances to be able to function correctly and generically.

> What should the scope of this Issue/PR be? Only build this for the available templates, including moves and so on.

For this PR, only build this for the available `Template`s. Moving will come after.

> How do we define midlines for uneven-sized templates, e.g., where the number of qubit rows is even?

As Austin said, I would rule out that case. To do that, I would change the `default_observable_qubits` return type to `ty.Sequence[tuple[cirq.GridQubit, int]] | None`, returning `None` being a signal that the midline could not be computed.

Also, I think the implementations of `default_observable_qubits` might be factorisable. But let's have something working for the moment.
Gistbatch: @nelimee, I've updated this; let me know what you think.

If the midline is supposed to depend on the plaquettes, we need this information, so I built `observable_qubits_from_template` similar to the `generate_circuit` function. The template now only returns the row and column indices of the templates that have the observable qubits, by convention, above/left of the midline.
nelimee: > If the midline is supposed to depend on the plaquettes

I do not get how you can generate correct results without any idea of the plaquette used. For the moment, all the plaquettes we use have the support
```
1   2
  5 
3   4
```
with:
1. `cirq.GridQubit(-1, -1)`
2. `cirq.GridQubit( 1, -1)`
3. `cirq.GridQubit(-1,  1)`
4. `cirq.GridQubit( 1,  1)`
5. `cirq.GridQubit( 0,  0)`

but (as far as I remember) we never explicitly said that was the only possible default. This means that a plaquette with the same support but with
1. `cirq.GridQubit(0, 0)`
2. `cirq.GridQubit(2, 0)`
3. `cirq.GridQubit(0, 2)`
4. `cirq.GridQubit(2, 2)`
5. `cirq.GridQubit(1, 1)`

is possible.
If that is the case, then we need to have access to that information, else the midline qubits will be completely wrong. I am missing something here?

I am making a few comments on the code before commenting again on the overall approach. 
afowler: Some reasonable restrictions. We already restrict ourselves to plaquettes
with a rectangular footprint. The midline calculation is associated with a
square 2kx2k template of such plaquettes. It would be reasonable to
restrict ourselves to plaquettes with exactly four qubits in the four
corners of the plaquette, then the midline calculation should depend on the
shape of the plaquette, rather than the precise labeling of the qubits in
the plaquette.

Best,
Austin.

On Fri, Apr 26, 2024, 8:34 AM Adrien Suau ***@***.***> wrote:

> If the midline is supposed to depend on the plaquettes
>
> I do not get how you can generate correct results without any idea of the
> plaquette used. For the moment, all the plaquettes we use have the support
>
> 1   2
>   5
> 3   4
>
> with:
>
>    1. cirq.GridQubit(-1, -1)
>    2. cirq.GridQubit( 1, -1)
>    3. cirq.GridQubit(-1, 1)
>    4. cirq.GridQubit( 1, 1)
>    5. cirq.GridQubit( 0, 0)
>
> but (as far as I remember) we never explicitly said that was the only
> possible default. This means that a plaquette with the same support but with
>
>    1. cirq.GridQubit(0, 0)
>    2. cirq.GridQubit(2, 0)
>    3. cirq.GridQubit(0, 2)
>    4. cirq.GridQubit(2, 2)
>    5. cirq.GridQubit(1, 1)
>
> is possible.
> If that is the case, then we need to have access to that information, else
> the midline qubits will be completely wrong. I am missing something here?
>
> I am making a few comments on the code before commenting again on the
> overall approach.
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/QCHackers/tqec/pull/213#issuecomment-2079626787>, or
> unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AAKAXTCWTHPW2B4VAOCLI2TY7JXYDAVCNFSM6AAAAABGS75KBKVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDANZZGYZDMNZYG4>
> .
> You are receiving this because you commented.Message ID:
> ***@***.***>
>

Gistbatch: Using their coordinates, I take the qubits furthest to the right (bottom). This works only for rectangular shapes but has the advantage that this works independently of the orientation.
As an alternative, we could implement this as the default behavior in the plaquette, which can be overwritten.
I'll clean up the code once we've decided on the mechanism and implement the rest of the templates.
nelimee: > then the midline calculation should depend on the shape of the plaquette, rather than the precise labeling of the qubits in the plaquette.

I agree, the issue here is that, in the current state of the code, the positions of the plaquette qubits can change all the positions. Basically, the positioning algorithm starts by positioning the origin of the top-left-most plaquette on the qubit `(0, 0)`, all the other qubits position being ultimately determined relative to that position. That mean that in the 2 cases I presented above, all the qubits of the template are shifted by `(1, 1)`. And we need exact coordinates for the midline.

This limitation should be quite easy to remove though, using the top-left corner of the plaquette instead of the origin in the first step would solve it on the current use-case. 
Finally, this is very much an implementation detail for the moment, but it has implications on what the user see and experience, so we might want to change the current default anyway.

> Using their coordinates, I take the qubits furthest to the right (bottom). This works only for rectangular shapes but has the advantage that this works independently of the orientation.
> As an alternative, we could implement this as the default behavior in the plaquette, which can be overwritten.

Both approaches seems fine for me. The second one is likely more generic, but we are only interested in rectangular plaquette for the moment so this is not really a problem to implement a simpler, less generic, version. 
Gistbatch: @nelimee, I'm confused about why the test keeps failing. Do you have any idea why this is the case? I can run it locally without this problem, and I'm not sure how to fix this as it seems it works completely fine with `circuit_test` which has a similar import structure.
nelimee: > @nelimee, I'm confused about why the test keeps failing. Do you have any idea why this is the case? I can run it locally without this problem, and I'm not sure how to fix this as it seems it works completely fine with `circuit_test` which has a similar import structure.

This might be due to #198 being merged in main. I am not sure what https://github.com/actions/checkout is checking out, but it might be worth updating this branch with the latest changes from `main` and see if that resolves the issue.

Gistbatch: I've now implemented the midline definition for the existing template instances.
For the `ComposedTemplate` and its children, I'm wondering what the default behavior should be.
It seems impossible to know which qubits are on the midline without prior knowledge.
For example, the `QubitSquareTemplate` has a midline, but none of the associated template midlines coincide.
We could hard code this, but figuring out which templates to look at for qubits seems arbitrary and complex.
@afowler, @nelimee any suggestions?
nelimee: It seems at least hard, maybe impossible (there are likely instantiations of the `CompositeTemplate` that do not represent a valid code, and so do not have observables), to me too.
I will review the code as it is right now, let's wait for @afowler answer.
Gistbatch: I wrote tests for all the templates now.
I don't know what the intended behavior for the stacked template is.
Also, I left the `CornerTemplate` and `StackedTemplate` unimplemented.
I could hardcode the corner template similar to the qubit template if needed.
There isn't a good way of knowing what the midline is supposed to be for the stacked template, so I just left it. 
The only option is to give all possible midlines to the user, and they have to piece them together themselves.

Gistbatch: @nelimee I'll merge this once you approve

Patch:
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 6e200a37..8bc4b2c4 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -6,4 +6,4 @@ myst-parser
 sphinx-rtd-theme
 pytest
 mypy
-pytest-cov
\ No newline at end of file
+pytest-cov
diff --git a/src/tqec/__init__.py b/src/tqec/__init__.py
index 383c7f81..ad064626 100644
--- a/src/tqec/__init__.py
+++ b/src/tqec/__init__.py
@@ -14,6 +14,7 @@ from .circuit import (
 from .enums import (
     CornerPositionEnum,
     PlaquetteOrientation,
+    TemplateOrientation,
     TemplateRelativePositionEnum,
 )
 from .exceptions import TQECException
diff --git a/src/tqec/circuit/observable_qubits.py b/src/tqec/circuit/observable_qubits.py
new file mode 100644
index 00000000..de85aca9
--- /dev/null
+++ b/src/tqec/circuit/observable_qubits.py
@@ -0,0 +1,75 @@
+from __future__ import annotations
+from typing import Mapping, Sequence
+
+import cirq
+
+from tqec.enums import TemplateOrientation
+from tqec.exceptions import TQECException
+from tqec.plaquette.plaquette import Plaquette
+from tqec.plaquette.qubit import PlaquetteQubit
+from tqec.templates.base import Template
+
+
+def observable_qubits_from_template(
+    template: Template,
+    plaquettes: Sequence[Plaquette] | Mapping[int, Plaquette],
+    orientation: TemplateOrientation = TemplateOrientation.HORIZONTAL,
+) -> Sequence[tuple[cirq.GridQubit, int]]:
+    """Return the default observable qubits for the given template and its plaquettes.
+
+    Args:
+        template: The template to get the default observable qubits from.
+        plaquettes: The plaquettes to use to get the acurate positions of the observable qubits.
+        orientation: Whether to get the observable qubits from
+            the horizontal or vertical midline. Defaults to horizontal.
+
+    Raises:
+        TQECException: If the number of plaquettes does not match the expected number.
+        TQECException: If the plaquettes are provided as a dictionary and 0 is in the keys.
+        TQECException: If the template does not have a definable midline.
+
+    Returns:
+        The sequence of qubits and offsets for the observable qubits.
+    """
+    if len(plaquettes) != template.expected_plaquettes_number:
+        raise TQECException(
+            f"{len(plaquettes)} plaquettes have been provided, but "
+            f"{template.expected_plaquettes_number} were expected."
+        )
+    if isinstance(plaquettes, Mapping) and 0 in plaquettes:
+        raise TQECException(
+            "If using a dictionary, the input plaquettes parameter should not "
+            f"contain the entry 0. Found a value ({plaquettes[0]}) at entry 0."
+        )
+
+    if isinstance(plaquettes, list):
+        plaquettes = {i + 1: plaquette for i, plaquette in enumerate(plaquettes)}
+
+    _indices = list(range(1, len(plaquettes) + 1))
+    template_plaquettes = template.instantiate(_indices)
+    increments = template.get_increments()
+
+    try:
+        midline_indices = template.get_midline_plaquettes(orientation)
+    except TQECException as e:
+        raise TQECException(
+            "The template does not have a midline. "
+            "The observable qubits cannot be defined."
+        ) from e
+
+    observable_qubits = []
+    for row_index, column_index in midline_indices:
+        plaquette_index = template_plaquettes[row_index][column_index]
+        if plaquette_index == 0:
+            continue
+        plaquette = plaquettes[plaquette_index]
+        # GridQubits are indexed as (row, col), so (y, x)
+        offset = (
+            row_index * increments.y + plaquette.origin.y,
+            column_index * increments.x + plaquette.origin.x,
+        )
+        observable_qubits += [
+            (qubit.to_grid_qubit() + offset, 0)
+            for qubit in plaquette.qubits.get_edge_qubits(orientation)
+        ]
+    return sorted(set(observable_qubits))
diff --git a/src/tqec/circuit/operations/operation.py b/src/tqec/circuit/operations/operation.py
index d4c10784..9ecd91cb 100644
--- a/src/tqec/circuit/operations/operation.py
+++ b/src/tqec/circuit/operations/operation.py
@@ -7,6 +7,7 @@ import cirq
 
 from tqec.exceptions import TQECException
 
+
 STIM_TAG = "STIM_OPERATION"
 
 
@@ -304,9 +305,11 @@ def make_detector(
         A :class:`Detector` operation with the `cirq.VirtualTag` tag.
     """
     relative_measurements_data = [
-        RelativeMeasurementData(*rm)
-        if not isinstance(rm, RelativeMeasurementData)
-        else rm
+        (
+            RelativeMeasurementData(*rm)
+            if not isinstance(rm, RelativeMeasurementData)
+            else rm
+        )
         for rm in relative_measurements
     ]
     return Detector(
@@ -343,9 +346,11 @@ def make_observable(
         A :class:`Observable` operation with the `cirq.VirtualTag` tag.
     """
     relative_measurements_data = [
-        RelativeMeasurementData(*rm)
-        if not isinstance(rm, RelativeMeasurementData)
-        else rm
+        (
+            RelativeMeasurementData(*rm)
+            if not isinstance(rm, RelativeMeasurementData)
+            else rm
+        )
         for rm in relative_measurements
     ]
     return Observable(
diff --git a/src/tqec/enums.py b/src/tqec/enums.py
index 774f6cf8..efba721c 100644
--- a/src/tqec/enums.py
+++ b/src/tqec/enums.py
@@ -60,3 +60,8 @@ class PlaquetteSide(Enum):
     LEFT = auto()
     DOWN = auto()
     UP = auto()
+
+class TemplateOrientation(Enum):
+    """Indicates the orientation of the midline."""
+    HORIZONTAL = auto()
+    VERTICAL = auto()
diff --git a/src/tqec/plaquette/qubit.py b/src/tqec/plaquette/qubit.py
index 3934f51d..d21b70ad 100644
--- a/src/tqec/plaquette/qubit.py
+++ b/src/tqec/plaquette/qubit.py
@@ -5,7 +5,7 @@ from dataclasses import dataclass
 
 import cirq
 
-from tqec.enums import PlaquetteOrientation, PlaquetteSide
+from tqec.enums import PlaquetteOrientation, PlaquetteSide, TemplateOrientation
 from tqec.position import Position
 
 
@@ -57,6 +57,34 @@ class PlaquetteQubits:
             [self.data_qubits[i] for i in permutation], self.syndrome_qubits
         )
 
+    def get_edge_qubits(
+        self,
+        orientation: TemplateOrientation = TemplateOrientation.HORIZONTAL,
+    ) -> list[PlaquetteQubit]:
+        """Return the data qubits on the edge of the plaquette.
+        By convention, the edge is the one with the highest index in the relevant axis.
+
+        Args:
+            orientation (TemplateOrientation, optional): Wheter to use horizontal or
+                vertical orientation. Defaults to horizontal.
+        Returns:
+            list[PlaquetteQubit]: The qubits on the edge of the plaquette.
+        """
+
+        def _get_relevant_value(qubit: PlaquetteQubit) -> int:
+            return (
+                qubit.position.y
+                if orientation == TemplateOrientation.HORIZONTAL
+                else qubit.position.x
+            )
+
+        max_index = max(_get_relevant_value(q) for q in self.data_qubits)
+        return [
+            qubit
+            for qubit in self.data_qubits
+            if (_get_relevant_value(qubit) == max_index)
+        ]
+
 
 class SquarePlaquetteQubits(PlaquetteQubits):
     def __init__(self):
diff --git a/src/tqec/templates/atomic/rectangle.py b/src/tqec/templates/atomic/rectangle.py
index 299c24bc..a7069e00 100644
--- a/src/tqec/templates/atomic/rectangle.py
+++ b/src/tqec/templates/atomic/rectangle.py
@@ -2,6 +2,7 @@ import typing as ty
 
 import numpy
 from tqec.exceptions import TQECException
+from tqec.enums import TemplateOrientation
 from tqec.position import Shape2D
 from tqec.templates.base import Template
 from tqec.templates.scale import Dimension
@@ -90,6 +91,23 @@ class AlternatingRectangleTemplate(Template):
     def expected_plaquettes_number(self) -> int:
         return 2
 
+    def get_midline_plaquettes(
+        self, orientation: TemplateOrientation = TemplateOrientation.HORIZONTAL
+    ) -> list[tuple[int, int]]:
+        midline_shape, iteration_shape = self.shape.x, self.shape.y
+        if orientation == TemplateOrientation.VERTICAL:
+            midline_shape, iteration_shape = iteration_shape, midline_shape
+
+        if midline_shape % 2 == 1:
+            raise TQECException(
+                "Midline is not defined for odd "
+                + f"{'height' if orientation == TemplateOrientation.HORIZONTAL else 'width'}."
+            )
+        midline = midline_shape // 2 - 1
+        if orientation == TemplateOrientation.VERTICAL:
+            return [(row, midline) for row in range(iteration_shape)]
+        return [(midline, column) for column in range(iteration_shape)]
+
 
 @ty.final
 class RawRectangleTemplate(Template):
@@ -179,15 +197,15 @@ class RawRectangleTemplate(Template):
             plaquette_indices_array = numpy.array(plaquette_indices, dtype=int)
             indices = numpy.array(self._indices, dtype=int)
             return plaquette_indices_array[indices]
-        except IndexError:
+        except IndexError as ex:
             raise TQECException(
                 "RawRectangleTemplate instances should be constructed with 2-dimensional arrays "
                 "that contain indices that will index the plaquette_indices provided to "
-                "this method. The bigest index you provided at this instance creation is "
+                "this method. The biggest index you provided at this instance creation is "
                 f"{max(max(index) for index in self._indices)} "
                 f"but you provided only {len(plaquette_indices)} plaquette indices "
                 "when calling this method."
-            )
+            ) from ex
 
     def scale_to(self, _: int) -> "RawRectangleTemplate":
         return self
@@ -202,3 +220,20 @@ class RawRectangleTemplate(Template):
     @property
     def expected_plaquettes_number(self) -> int:
         return max(max(line) for line in self._indices) + 1
+
+    def get_midline_plaquettes(
+        self, orientation: TemplateOrientation = TemplateOrientation.HORIZONTAL
+    ) -> list[tuple[int, int]]:
+        midline_shape, iteration_shape = self.shape.x, self.shape.y
+        if orientation == TemplateOrientation.VERTICAL:
+            midline_shape, iteration_shape = iteration_shape, midline_shape
+
+        if midline_shape % 2 == 1:
+            raise TQECException(
+                "Midline is not defined for odd "
+                + f"{'height' if orientation == TemplateOrientation.HORIZONTAL else 'width'}."
+            )
+        midline = midline_shape // 2 - 1
+        if orientation == TemplateOrientation.VERTICAL:
+            return [(row, midline) for row in range(iteration_shape)]
+        return [(midline, column) for column in range(iteration_shape)]
diff --git a/src/tqec/templates/atomic/square.py b/src/tqec/templates/atomic/square.py
index 7d10bbf6..90640f74 100644
--- a/src/tqec/templates/atomic/square.py
+++ b/src/tqec/templates/atomic/square.py
@@ -2,7 +2,8 @@ import typing as ty
 
 import numpy
 
-from tqec.enums import CornerPositionEnum
+from tqec.enums import CornerPositionEnum, TemplateOrientation
+from tqec.exceptions import TQECException
 from tqec.position import Shape2D
 from tqec.templates.atomic.rectangle import AlternatingRectangleTemplate
 from tqec.templates.base import Template
@@ -171,3 +172,17 @@ class AlternatingCornerSquareTemplate(Template):
     @property
     def shape(self) -> Shape2D:
         return Shape2D(self._dimension.value, self._dimension.value)
+
+    def get_midline_plaquettes(
+        self, orientation: TemplateOrientation = TemplateOrientation.HORIZONTAL
+    ) -> list[tuple[int, int]]:
+        midline_shape, iteration_shape = self.shape.x, self.shape.y
+        if midline_shape % 2 == 1:
+            raise TQECException(
+                "Midline is not defined for odd "
+                + f"{'height' if orientation == TemplateOrientation.HORIZONTAL else 'width'}."
+            )
+        midline = midline_shape // 2 - 1
+        if orientation == TemplateOrientation.VERTICAL:
+            return [(row, midline) for row in range(iteration_shape)]
+        return [(midline, column) for column in range(iteration_shape)]
diff --git a/src/tqec/templates/base.py b/src/tqec/templates/base.py
index b515af44..cdb921a4 100644
--- a/src/tqec/templates/base.py
+++ b/src/tqec/templates/base.py
@@ -7,7 +7,11 @@ from dataclasses import dataclass
 
 import numpy
 
-from tqec.enums import CornerPositionEnum, TemplateRelativePositionEnum
+from tqec.enums import (
+    CornerPositionEnum,
+    TemplateOrientation,
+    TemplateRelativePositionEnum,
+)
 from tqec.exceptions import TQECException
 from tqec.position import Displacement, Shape2D
 
@@ -161,6 +165,27 @@ class Template(JSONEncodable):
         """
         pass
 
+    @abstractmethod
+    def get_midline_plaquettes(
+        self, orientation: TemplateOrientation = TemplateOrientation.HORIZONTAL
+    ) -> list[tuple[int, int]]:
+        """Returns the default observable qubits for the template.
+
+        If the template has a simple shape, this returns the plaquettes on the ``midline''
+        of the template.
+        By convention, it returns the plaquettes above the midline for the horizontal case
+        and to the left of the midline for the vertical case.
+
+        Args:
+            orientation: Horizontal or vertical qubits. Defaults to horizontal.
+
+        Returns:
+            The sequence of qubits and offsets.
+
+        Raises:
+            TQECException: If the midline is not uniquely defined.
+        """
+
     def to_dict(self) -> dict[str, ty.Any]:
         """Returns a dict-like representation of the instance.
 
diff --git a/src/tqec/templates/composed.py b/src/tqec/templates/composed.py
index c7bb566f..1cf2a032 100644
--- a/src/tqec/templates/composed.py
+++ b/src/tqec/templates/composed.py
@@ -3,7 +3,11 @@ import typing as ty
 import networkx as nx
 import numpy
 
-from tqec.enums import CornerPositionEnum, TemplateRelativePositionEnum
+from tqec.enums import (
+    CornerPositionEnum,
+    TemplateRelativePositionEnum,
+    TemplateOrientation,
+)
 from tqec.exceptions import TQECException
 from tqec.position import Displacement, Position, Shape2D
 from tqec.templates.base import Template, TemplateWithIndices
@@ -274,10 +278,10 @@ class ComposedTemplate(Template):
         dest: int
         # Compute the upper-left (ul) position of all the templates
         for src, dest in nx.bfs_edges(self._relative_position_graph, 0):
-            relative_position: tuple[
-                CornerPositionEnum, CornerPositionEnum
-            ] | None = self._relative_position_graph.get_edge_data(src, dest).get(
-                "relative_position"
+            relative_position: tuple[CornerPositionEnum, CornerPositionEnum] | None = (
+                self._relative_position_graph.get_edge_data(src, dest).get(
+                    "relative_position"
+                )
             )
             assert (
                 relative_position is not None
@@ -486,3 +490,8 @@ class ComposedTemplate(Template):
     @property
     def is_empty(self) -> bool:
         return len(self._templates) == 0
+
+    def get_midline_plaquettes(
+        self, orientation: TemplateOrientation = TemplateOrientation.HORIZONTAL
+    ) -> list[tuple[int, int]]:
+        raise NotImplementedError
diff --git a/src/tqec/templates/constructions/qubit.py b/src/tqec/templates/constructions/qubit.py
index 9ea4d064..b50a0858 100644
--- a/src/tqec/templates/constructions/qubit.py
+++ b/src/tqec/templates/constructions/qubit.py
@@ -1,6 +1,7 @@
 from __future__ import annotations
 
-from tqec.enums import ABOVE_OF, BELOW_OF, LEFT_OF, RIGHT_OF
+from tqec.enums import ABOVE_OF, BELOW_OF, LEFT_OF, RIGHT_OF, TemplateOrientation
+from tqec.exceptions import TQECException
 from tqec.templates.atomic.rectangle import AlternatingRectangleTemplate
 from tqec.templates.atomic.square import AlternatingSquareTemplate
 from tqec.templates.base import TemplateWithIndices
@@ -49,6 +50,20 @@ class QubitSquareTemplate(ComposedTemplate):
         for source, relpos, target in _relations:
             self.add_relation(source, relpos, target)
 
+    def get_midline_plaquettes(
+        self, orientation: TemplateOrientation = TemplateOrientation.HORIZONTAL
+    ) -> list[tuple[int, int]]:
+        midline_shape, iteration_shape = self.shape.x, self.shape.y
+        if midline_shape % 2 == 1:
+            raise TQECException(
+                "Midline is not defined for odd "
+                + f"{'height' if orientation == TemplateOrientation.HORIZONTAL else 'width'}."
+            )
+        midline = midline_shape // 2 - 1
+        if orientation == TemplateOrientation.VERTICAL:
+            return [(row, midline) for row in range(iteration_shape)]
+        return [(midline, column) for column in range(iteration_shape)]
+
 
 class QubitRectangleTemplate(ComposedTemplate):
     def __init__(
@@ -96,3 +111,20 @@ class QubitRectangleTemplate(ComposedTemplate):
         super().__init__(_templates)
         for source, relpos, target in _relations:
             self.add_relation(source, relpos, target)
+
+    def get_midline_plaquettes(
+        self, orientation: TemplateOrientation = TemplateOrientation.HORIZONTAL
+    ) -> list[tuple[int, int]]:
+        midline_shape, iteration_shape = self.shape.x, self.shape.y
+        if orientation == TemplateOrientation.VERTICAL:
+            midline_shape, iteration_shape = iteration_shape, midline_shape
+
+        if midline_shape % 2 == 1:
+            raise TQECException(
+                "Midline is not defined for odd "
+                + f"{'height' if orientation == TemplateOrientation.HORIZONTAL else 'width'}."
+            )
+        midline = midline_shape // 2 - 1
+        if orientation == TemplateOrientation.VERTICAL:
+            return [(row, midline) for row in range(iteration_shape)]
+        return [(midline, column) for column in range(iteration_shape)]
diff --git a/src/tqec/templates/shifted.py b/src/tqec/templates/shifted.py
index dbf1438d..7b059ad9 100644
--- a/src/tqec/templates/shifted.py
+++ b/src/tqec/templates/shifted.py
@@ -3,6 +3,7 @@ from dataclasses import dataclass
 
 import numpy
 
+from tqec.enums import TemplateOrientation
 from tqec.position import Shape2D
 from tqec.templates.base import Template
 from tqec.templates.scale import Dimension
@@ -64,3 +65,11 @@ class ShiftedTemplate(Template):
         tarr = self._shifted_template.instantiate(plaquette_indices)
         arr[yoffset : yoffset + tshape.y, xoffset : xoffset + tshape.x] = tarr
         return arr
+
+    def get_midline_plaquettes(
+        self, orientation: TemplateOrientation = TemplateOrientation.HORIZONTAL
+    ) -> list[tuple[int, int]]:
+        return [
+            (row + self._offset.x.value, column + self._offset.y.value)
+            for row, column in self._shifted_template.get_midline_plaquettes(orientation)
+        ]
diff --git a/src/tqec/templates/stack.py b/src/tqec/templates/stack.py
index 00cec91c..6b65ba5e 100644
--- a/src/tqec/templates/stack.py
+++ b/src/tqec/templates/stack.py
@@ -2,6 +2,8 @@ import typing as ty
 
 import numpy
 
+from tqec.enums import TemplateOrientation
+from tqec.exceptions import TQECException
 from tqec.position import Shape2D
 from tqec.templates.base import Template
 
@@ -105,3 +107,32 @@ class StackedTemplate(Template):
             nonzeros = tarr.nonzero()
             arr[0:yshape, 0:xshape][nonzeros] = tarr[nonzeros]
         return arr
+
+    def get_midline_plaquettes(
+        self, orientation: TemplateOrientation = TemplateOrientation.HORIZONTAL
+    ) -> list[tuple[int, int]]:
+        """We assumme the midline is defined by the template with the largest shape.
+        This also assumes that operators are moved on the biggest template.
+        """
+        midline_shape = self.shape.y
+        if orientation == TemplateOrientation.VERTICAL:
+            midline_shape = self.shape.x
+        if midline_shape % 2 == 1:
+            raise TQECException(
+                "Midline is not defined for odd "
+                + f"{'height' if orientation == TemplateOrientation.HORIZONTAL else 'width'}."
+            )
+        for template in self._stack:
+            if (
+                orientation == TemplateOrientation.HORIZONTAL
+                and template.shape.y == midline_shape
+            ):
+                return template.get_midline_plaquettes(orientation)
+            if (
+                orientation == TemplateOrientation.VERTICAL
+                and template.shape.x == midline_shape
+            ):
+                return template.get_midline_plaquettes(orientation)
+        raise TQECException(
+            "No template with the expected midline shape was found in the stack."
+        )


Test patch:
diff --git a/src/tqec/circuit/circuit_test.py b/src/tqec/circuit/circuit_test.py
index 48ce5259..e4edee21 100644
--- a/src/tqec/circuit/circuit_test.py
+++ b/src/tqec/circuit/circuit_test.py
@@ -21,7 +21,7 @@ def one_by_one_template() -> Template:
 
 def test_generate_initialisation_circuit_list(
     initialisation_plaquette: Plaquette, one_by_one_template
-):
+) -> None:
     circuit = generate_circuit(one_by_one_template, [initialisation_plaquette])
     assert circuit == cirq.Circuit(
         cirq.R(q.to_grid_qubit()) for q in initialisation_plaquette.qubits
diff --git a/src/tqec/circuit/observable_qubits_test.py b/src/tqec/circuit/observable_qubits_test.py
new file mode 100644
index 00000000..6bfae568
--- /dev/null
+++ b/src/tqec/circuit/observable_qubits_test.py
@@ -0,0 +1,36 @@
+import cirq
+import pytest
+
+from tqec.circuit.observable_qubits import observable_qubits_from_template
+from tqec.plaquette.library import xxxx_memory_plaquette, zzzz_memory_plaquette
+from tqec.plaquette.plaquette import Plaquette
+from tqec.templates import RawRectangleTemplate
+
+
+@pytest.fixture
+def plaquettes() -> list[Plaquette]:
+    return [
+        xxxx_memory_plaquette([1, 2, 3, 4, 5, 6, 7, 8]),
+        zzzz_memory_plaquette([1, 3, 4, 5, 6, 8]),
+    ]
+
+
+def test_raw_rectangle_default_obserevable_qubits(plaquettes: list[Plaquette]):
+    template = RawRectangleTemplate(
+        [
+            [0, 1, 0, 1],
+            [1, 0, 1, 0],
+            [0, 1, 0, 1],
+            [1, 0, 1, 0],
+        ]
+    )
+
+    obs = observable_qubits_from_template(template, plaquettes)
+    result = [
+        (cirq.GridQubit(3, -1), 0),
+        (cirq.GridQubit(3, 1), 0),
+        (cirq.GridQubit(3, 3), 0),
+        (cirq.GridQubit(3, 5), 0),
+        (cirq.GridQubit(3, 7), 0),
+    ]
+    assert sorted(obs, key=lambda t: t[0].col) == result
diff --git a/src/tqec/templates/atomic/rectangle_test.py b/src/tqec/templates/atomic/rectangle_test.py
index 15235ce9..40d8eda5 100644
--- a/src/tqec/templates/atomic/rectangle_test.py
+++ b/src/tqec/templates/atomic/rectangle_test.py
@@ -1,5 +1,6 @@
 import numpy
 import pytest
+from tqec.enums import TemplateOrientation
 from tqec.exceptions import TQECException
 from tqec.templates.atomic.rectangle import (
     AlternatingRectangleTemplate,
@@ -28,6 +29,11 @@ def dim2():
     return FixedDimension(2)
 
 
+@pytest.fixture
+def dim2x3():
+    return Dimension(2, LinearFunction(3))
+
+
 def test_rectangle_template_init(dim2x2, dim3x2):
     AlternatingRectangleTemplate(dim2x2, dim3x2)
 
@@ -180,3 +186,54 @@ def test_raw_rectangle_with_noncontiguous_indices():
 def test_raw_rectangle_with_negative_index():
     with pytest.raises(TQECException, match="starting at 0"):
         RawRectangleTemplate([[-1]])
+
+
+def test_raw_rectangle_midline():
+    template = RawRectangleTemplate(
+        [
+            [0, 0, 0, 0],
+            [0, 0, 0, 0],
+            [0, 0, 0, 0],
+            [0, 0, 0, 0],
+            [0, 0, 0, 0],
+            [0, 0, 0, 0],
+        ]
+    )
+    midline = template.get_midline_plaquettes()
+    assert midline == [(1, 0), (1, 1), (1, 2), (1, 3), (1, 4), (1, 5)]
+    midline = template.get_midline_plaquettes(TemplateOrientation.VERTICAL)
+    assert midline == [(0, 2), (1, 2), (2, 2), (3, 2)]
+    template.scale_to(4)
+    midline = template.get_midline_plaquettes()
+    # shoudl be 4*3 elements
+    template = RawRectangleTemplate([[0]])
+    with pytest.raises(TQECException, match="Midline is not defined for odd height."):
+        template.get_midline_plaquettes()
+
+
+def test_rectangle_midline(dim2x2, dim2x3):
+    template = AlternatingRectangleTemplate(dim2x2, dim2x3)
+    midline = template.get_midline_plaquettes()
+    assert midline == [(1, 0), (1, 1), (1, 2), (1, 3), (1, 4), (1, 5)]
+    midline = template.get_midline_plaquettes(TemplateOrientation.VERTICAL)
+    assert midline == [(0, 2), (1, 2), (2, 2), (3, 2)]
+    template.scale_to(4)
+    midline = template.get_midline_plaquettes()
+    # shoudl be 4*3 elements
+    assert midline == [
+        (3, 0),
+        (3, 1),
+        (3, 2),
+        (3, 3),
+        (3, 4),
+        (3, 5),
+        (3, 6),
+        (3, 7),
+        (3, 8),
+        (3, 9),
+        (3, 10),
+        (3, 11),
+    ]
+    template.scale_to(3)
+    with pytest.raises(TQECException, match="Midline is not defined for odd width."):
+        template.get_midline_plaquettes(TemplateOrientation.VERTICAL)
diff --git a/src/tqec/templates/atomic/square_test.py b/src/tqec/templates/atomic/square_test.py
index dc316587..6bf4f810 100644
--- a/src/tqec/templates/atomic/square_test.py
+++ b/src/tqec/templates/atomic/square_test.py
@@ -1,6 +1,7 @@
 import numpy
 import pytest
-from tqec.enums import CornerPositionEnum
+from tqec.enums import CornerPositionEnum, TemplateOrientation
+from tqec.exceptions import TQECException
 from tqec.templates.atomic.square import (
     AlternatingCornerSquareTemplate,
     AlternatingSquareTemplate,
@@ -28,6 +29,11 @@ def dim2():
     return FixedDimension(2)
 
 
+@pytest.fixture
+def dim3():
+    return FixedDimension(3)
+
+
 def test_square_template_init(dim2x2):
     AlternatingSquareTemplate(dim2x2)
 
@@ -98,3 +104,26 @@ def test_corner_square_template_one_fixed_scaling(dim2):
     shape = template.shape
     assert shape.x == 2
     assert shape.y == 2
+
+
+def test_corner_square_template_midline(dim2x2, dim3):
+    template = AlternatingCornerSquareTemplate(dim2x2, CornerPositionEnum.LOWER_LEFT)
+    midline = template.get_midline_plaquettes()
+    assert midline == [(1, 0), (1, 1), (1, 2), (1, 3)]
+    midline = template.get_midline_plaquettes(TemplateOrientation.VERTICAL)
+    assert midline == [(0, 1), (1, 1), (2, 1), (3, 1)]
+    template.scale_to(4)
+    midline = template.get_midline_plaquettes()
+    assert midline == [
+        (3, 0),
+        (3, 1),
+        (3, 2),
+        (3, 3),
+        (3, 4),
+        (3, 5),
+        (3, 6),
+        (3, 7),
+    ]
+    template = AlternatingCornerSquareTemplate(dim3, CornerPositionEnum.LOWER_LEFT)
+    with pytest.raises(TQECException, match="Midline is not defined for odd height."):
+        template.get_midline_plaquettes()
diff --git a/src/tqec/templates/constructions/qubit_test.py b/src/tqec/templates/constructions/qubit_test.py
new file mode 100644
index 00000000..89914ad2
--- /dev/null
+++ b/src/tqec/templates/constructions/qubit_test.py
@@ -0,0 +1,77 @@
+import pytest
+from tqec.enums import TemplateOrientation
+from tqec.exceptions import TQECException
+from tqec.templates.constructions.qubit import (
+    QubitRectangleTemplate,
+    QubitSquareTemplate,
+)
+from tqec.templates.scale import Dimension, FixedDimension, LinearFunction
+
+
+@pytest.fixture
+def dim2x2():
+    return Dimension(2, LinearFunction(2))
+
+
+@pytest.fixture
+def dim2x3():
+    return Dimension(2, LinearFunction(3))
+
+
+@pytest.fixture
+def dim3():
+    return FixedDimension(3)
+
+
+def test_qubit_square_midline(dim2x2, dim3):
+    template = QubitSquareTemplate(dim2x2)
+    midline = template.get_midline_plaquettes()
+    assert midline == [(2, 0), (2, 1), (2, 2), (2, 3), (2, 4), (2, 5)]
+    midline = template.get_midline_plaquettes(TemplateOrientation.VERTICAL)
+    assert midline == [(0, 2), (1, 2), (2, 2), (3, 2), (4, 2), (5, 2)]
+    template.scale_to(4)
+    midline = template.get_midline_plaquettes()
+    assert midline == [
+        (4, 0),
+        (4, 1),
+        (4, 2),
+        (4, 3),
+        (4, 4),
+        (4, 5),
+        (4, 6),
+        (4, 7),
+        (4, 8),
+        (4, 9),
+    ]
+    template = QubitSquareTemplate(dim3)
+    with pytest.raises(TQECException, match="Midline is not defined for odd height."):
+        template.get_midline_plaquettes()
+
+def test_qubit_rectangle_midline(dim2x2, dim2x3):
+    template = QubitRectangleTemplate(dim2x2, dim2x3)
+    midline = template.get_midline_plaquettes()
+    assert midline == [(2, 0), (2, 1), (2, 2), (2, 3), (2, 4), (2, 5), (2, 6), (2, 7)]
+    midline = template.get_midline_plaquettes(TemplateOrientation.VERTICAL)
+    assert midline == [(0, 3), (1, 3), (2, 3), (3, 3), (4, 3), (5, 3)]
+    template.scale_to(4)
+    midline = template.get_midline_plaquettes()
+    # shoudl be 4*3+2 elements
+    assert midline == [
+        (4, 0),
+        (4, 1),
+        (4, 2),
+        (4, 3),
+        (4, 4),
+        (4, 5),
+        (4, 6),
+        (4, 7),
+        (4, 8),
+        (4, 9),
+        (4, 10),
+        (4, 11),
+        (4, 12),
+        (4, 13)
+    ]
+    template.scale_to(3)
+    with pytest.raises(TQECException, match="Midline is not defined for odd width."):
+        template.get_midline_plaquettes(TemplateOrientation.VERTICAL)
diff --git a/src/tqec/templates/shifted_test.py b/src/tqec/templates/shifted_test.py
index d5057145..e26efb85 100644
--- a/src/tqec/templates/shifted_test.py
+++ b/src/tqec/templates/shifted_test.py
@@ -1,6 +1,8 @@
 import numpy
 import pytest
 
+from tqec.enums import TemplateOrientation
+from tqec.exceptions import TQECException
 from tqec.position import Shape2D
 from tqec.templates.atomic.square import AlternatingSquareTemplate
 from tqec.templates.base import Template
@@ -65,3 +67,30 @@ def test_shifted_template(default_template: Template, scalable_offset: ScalableO
         template.instantiate(indices),
         [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 1, 2], [0, 0, 2, 1]],
     )
+
+
+def test_shifted_template_midline(
+    default_template: Template, scalable_offset: ScalableOffset
+):
+    template = ShiftedTemplate(default_template, scalable_offset)
+    midline = template.get_midline_plaquettes()
+    assert midline == [(5, 4), (5, 5), (5, 6), (5, 7)]
+    midline = template.get_midline_plaquettes(TemplateOrientation.VERTICAL)
+    assert midline == [(4, 5), (5, 5), (6, 5), (7, 5)]
+    template.scale_to(4)
+    midline = template.get_midline_plaquettes()
+    assert midline == [
+        (11, 8),
+        (11, 9),
+        (11, 10),
+        (11, 11),
+        (11, 12),
+        (11, 13),
+        (11, 14),
+        (11, 15),
+    ]
+    template = ShiftedTemplate(
+        AlternatingSquareTemplate(FixedDimension(3)), scalable_offset
+    )
+    with pytest.raises(TQECException, match="Midline is not defined for odd height."):
+        template.get_midline_plaquettes()
diff --git a/src/tqec/templates/stack_test.py b/src/tqec/templates/stack_test.py
index 2c7c091e..84fc46d1 100644
--- a/src/tqec/templates/stack_test.py
+++ b/src/tqec/templates/stack_test.py
@@ -1,6 +1,8 @@
 import numpy.testing
 import pytest
 
+from tqec.enums import TemplateOrientation
+from tqec.exceptions import TQECException
 from tqec.position import Shape2D
 from tqec.templates.atomic.square import AlternatingSquareTemplate
 from tqec.templates.base import Template
@@ -151,3 +153,37 @@ def test_stack_scale_to(
             [2, 1, 2, 1, 2, 1, 2, 1],
         ],
     )
+
+
+def test_stack_midline(
+    small_alternating_template: Template, larger_alternating_template: Template
+):
+    template = StackedTemplate()
+    template.push_template_on_top(larger_alternating_template)
+    template.push_template_on_top(small_alternating_template)
+    midline = template.get_midline_plaquettes()
+    assert midline == [(1, 0), (1, 1), (1, 2), (1, 3)]
+    midline = template.get_midline_plaquettes(TemplateOrientation.VERTICAL)
+    assert midline == [(0, 1), (1, 1), (2, 1), (3, 1)]
+    template.pop_template_from_top()
+    template.scale_to(2)
+    midline = template.get_midline_plaquettes()
+    assert midline == [
+        (3, 0),
+        (3, 1),
+        (3, 2),
+        (3, 3),
+        (3, 4),
+        (3, 5),
+        (3, 6),
+        (3, 7),
+    ]
+    template.pop_template_from_top()
+    with pytest.raises(
+        TQECException,
+        match="No template with the expected midline shape was found in the stack.",
+    ):
+        template.get_midline_plaquettes()
+    template.push_template_on_top(AlternatingSquareTemplate(FixedDimension(3)))
+    with pytest.raises(TQECException, match="Midline is not defined for odd height."):
+        template.get_midline_plaquettes()