Repository: QCHackers/tqec
Pull request #318 (2024-09-10 11:49:23)

Extract spatially local sub-templates from `Template` instances
This is based on #317.

This PR introduces several things to the `Template` hierarchy. First, a few details:
- Remove `pass` from abstract methods bodies
- Introduce a `RectangularTemplate` class that inherits from `Template` and provides a default implementation of `get_midline_plaquettes` for `Template` instances with a rectangular form (e.g. all the templates in `tqec.templates.qubit`).

Now the main 2 changes:
- Introduction of a `GridTemplate` that allows to quickly represent a grid of several identical `RectangularTemplate` instances.
- Introduction of a `get_spatially_distinct_subtemplates` function that computes, from a template, all the spatially-local plaquette arrangements found.

A little bit more on `get_spatially_distinct_subtemplates`. It takes an array obtained from calling `Template.instantiate` on any `Template` instance, a `radius` (using the Manhattan distance), and compute all the plaquette arrangements that are found in the instantiation with the provided `radius`.

For example, a radius of `1` would lead to "sub-template" looking like (`c` being the center of the ball of radius `1`):
```
x x x
x c x
x x x
```
whereas a radius of 3 would lead to
```
x x x x x x x
x x x x x x x
x x x x x x x
x x x c x x x
x x x x x x x
x x x x x x x
x x x x x x x
```

Now with a real example, the following code
```py
from tqec.templates.qubit import QubitTemplate

template = QubitTemplate(k=2)
situation, mapping = template.get_spatially_distinct_subtemplates(1)

print("Full map:")
display_template_from_instantiation(situation)
for i, subtemplate in mapping.items():
    print(f"Subtemplate {i}:")
    display_template_from_instantiation(subtemplate)
```
outputs:
```
Full map:
  1  2  4  6  3  5
  7 12 14 16 13 15
  9 18 21 28 24 33
 11 20 28 21 31 26
  8 17 22 29 25 34
 10 19 30 23 32 27
Subtemplate 1:
  .  .  .
  .  1  5
  .  7  9
Subtemplate 2:
  .  .  .
  1  5  6
  7  9 10
Subtemplate 3:
  .  .  .
  5  6  2
  9 10 11
Subtemplate 4:
  .  .  .
  5  6  5
  9 10  9
// Cut for readability
Subtemplate 16:
  6  5  6
 10  9 10
  9 10  9
Subtemplate 17:
  7  9 10
  8 10  9
  3 13 14
// Cut for readability
Subtemplate 33:
 10 11  .
  9 12  .
 10 11  .
Subtemplate 34:
 10 11  .
  9 12  .
 14  4  .
```
In theory, if we ignore `0` entries in the sub-templates (represented by `.` above), we should recover exactly the template instantiation by superimposing on each entry of the "Full map" printed first the corresponding sub-template.


The goal of implementing such a procedure is to be able to list exhaustively the spatially-local sub-templates that we have to consider in order to compute detectors involving measurements in the center plaquette of such sub-template.
What is interesting in this approach is that if we now use a way larger `Template` such as
```py 
from tqec.templates.qubit import QubitTemplate

template = QubitTemplate(k=100)  # << WAY LARGER
situation, mapping = template.get_spatially_distinct_subtemplates(1)

print("Full map:")
display_template_from_instantiation(situation)
for i, subtemplate in mapping.items():
    print(f"Subtemplate {i}:")
    display_template_from_instantiation(subtemplate)
```
the number of distinct spatially local sub-template is still `34`, and we have in the variable `situation` a description of where these sub-templates are.

One of the issue I still have is about naming: I do not like the naming of `situation` to describe a 2-dimensional array filled with sub-template indices, but I cannot find something better. Do anyone have an idea?

inmzhang: Is this output correct? Why is the subtemplate centered at 1 is:
```txt
.  .  .
.  1  5
.  7  9
```
instead of
```txt
.  .  .
.  1  2
.  7  12
```
nelimee: > Is this output correct? Why is the subtemplate centered at 1 is:
> 
> ```
> .  .  .
> .  1  5
> .  7  9
> ```
> 
> instead of
> 
> ```
> .  .  .
> .  1  2
> .  7  12
> ```

It is correct because the `QubitTemplate.instantiate` method
```py
from tqec.templates.display import display_template

template = QubitTemplate(k=2)
display_template(template)
```
returns
```
  1  5  6  5  6  2
  7  9 10  9 10 11
  8 10  9 10  9 12
  7  9 10  9 10 11
  8 10  9 10  9 12
  3 13 14 13 14  4
```
github-actions[bot]: ![Code Coverage](https://img.shields.io/badge/Code%20Coverage-82%25-success?style=flat)

Package | Line Rate | Complexity | Health
-------- | --------- | ---------- | ------
src.tqec | 97% | 0 | ✔
src.tqec.circuit | 79% | 0 | ✔
src.tqec.circuit.detectors | 94% | 0 | ✔
src.tqec.circuit.detectors.match_utils | 90% | 0 | ✔
src.tqec.circuit.operations | 68% | 0 | ➖
src.tqec.noise_models | 48% | 0 | ❌
src.tqec.plaquette | 97% | 0 | ✔
src.tqec.plaquette.library | 96% | 0 | ✔
src.tqec.plaquette.library.utils | 92% | 0 | ✔
src.tqec.sketchup | 79% | 0 | ✔
src.tqec.templates | 78% | 0 | ✔
**Summary** | **82%** (2736 / 3326) | **0** | ✔

<!-- Sticky Pull Request Comment -->

Patch:
diff --git a/src/tqec/templates/base.py b/src/tqec/templates/base.py
index 89739961..00b2bea5 100644
--- a/src/tqec/templates/base.py
+++ b/src/tqec/templates/base.py
@@ -3,11 +3,16 @@ from typing import Sequence
 
 import numpy
 import numpy.typing as npt
+from typing_extensions import override
 
 from tqec.exceptions import TQECException
 from tqec.position import Displacement, Shape2D
 from tqec.templates.enums import TemplateOrientation, TemplateSide
 from tqec.templates.scale import Scalable2D, round_or_fail
+from tqec.templates.subtemplates import (
+    UniqueSubTemplates,
+    get_spatially_distinct_subtemplates,
+)
 
 
 class Template(ABC):
@@ -25,8 +30,9 @@ class Template(ABC):
                 to `Displacement(2, 2)` when `None`
         """
         super().__init__()
-        self._k: int
-        self.scale_to(k)
+        if k < 0:
+            raise TQECException(f"Cannot have a negative scaling parameter. Got {k}.")
+        self._k = k
         self._default_increments = default_increments or Displacement(2, 2)
 
     @abstractmethod
@@ -44,7 +50,6 @@ class Template(ABC):
             a numpy array with the given plaquette indices arranged according to
             the underlying shape of the template.
         """
-        pass
 
     def scale_to(self, k: int) -> None:
         """Scales self to the given scale k.
@@ -74,7 +79,6 @@ class Template(ABC):
     @abstractmethod
     def scalable_shape(self) -> Scalable2D:
         """Returns a scalable version of the template shape."""
-        pass
 
     def get_midline_plaquettes(
         self, orientation: TemplateOrientation = TemplateOrientation.HORIZONTAL
@@ -115,7 +119,6 @@ class Template(ABC):
         Returns:
             the number of plaquettes expected from the `instantiate` method.
         """
-        pass
 
     def get_increments(self) -> Displacement:
         """Get the default increments of the template.
@@ -136,4 +139,48 @@ class Template(ABC):
         Returns:
             a non-ordered list of plaquette numbers.
         """
-        pass
+
+    def get_spatially_distinct_subtemplates(
+        self, manhattan_radius: int = 1, avoid_zero_plaquettes: bool = True
+    ) -> UniqueSubTemplates:
+        """Returns a representation of all the distinct sub-templates of the
+        provided manhattan radius.
+
+        Note:
+            This method will likely be inefficient for large templates (i.e., large
+            values of `k`) or for large Manhattan radiuses, both in terms of memory
+            used and computation time.
+            Subclasses are invited to reimplement that method using a specialized
+            algorithm (or hard-coded values) to speed things up.
+
+        Args:
+            manhattan_radius: radius of the considered ball using the Manhattan
+                distance. Only squares with sides of `2*manhattan_radius+1`
+                plaquettes will be considered.
+            avoid_zero_plaquettes: `True` if sub-templates with an empty plaquette
+                (i.e., 0 value in the instantiation of the Template instance) at
+                its center should be ignored. Default to `True`.
+
+        Returns:
+            a representation of all the sub-templates found.
+        """
+        return get_spatially_distinct_subtemplates(
+            self.instantiate(), manhattan_radius, avoid_zero_plaquettes
+        )
+
+
+class RectangularTemplate(Template):
+    @override
+    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/display.py b/src/tqec/templates/display.py
index 243d5a48..09805876 100644
--- a/src/tqec/templates/display.py
+++ b/src/tqec/templates/display.py
@@ -2,6 +2,9 @@ from __future__ import annotations
 
 import typing as ty
 
+import numpy
+import numpy.typing as npt
+
 from tqec.templates.base import Template
 
 
@@ -18,8 +21,18 @@ def display_template(
     """
     if plaquette_indices is None:
         plaquette_indices = tuple(range(1, template.expected_plaquettes_number + 1))
-    arr = template.instantiate(plaquette_indices)
-    for line in arr:
+    display_template_from_instantiation(template.instantiate(plaquette_indices))
+
+
+def display_template_from_instantiation(instantiation: npt.NDArray[numpy.int_]) -> None:
+    """Display an array representing a template instantiation with ASCII
+    output.
+
+    Args:
+        instantiation: the integer array obtained from the `Template.instantiate`
+            method.
+    """
+    for line in instantiation:
         for element in line:
             element = str(element) if element != 0 else "."
             print(f"{element:>3}", end="")
diff --git a/src/tqec/templates/grid.py b/src/tqec/templates/grid.py
new file mode 100644
index 00000000..4290b21b
--- /dev/null
+++ b/src/tqec/templates/grid.py
@@ -0,0 +1,132 @@
+from copy import deepcopy
+from typing import Sequence
+
+import numpy
+import numpy.typing as npt
+from typing_extensions import override
+
+from tqec.exceptions import TQECException
+from tqec.position import Displacement
+from tqec.templates.base import RectangularTemplate
+from tqec.templates.enums import TemplateSide
+from tqec.templates.scale import Scalable2D
+
+
+class GridTemplate(RectangularTemplate):
+    def __init__(
+        self,
+        index_grid: Sequence[Sequence[int]],
+        tiled_template: RectangularTemplate,
+        k: int = 2,
+        default_increments: Displacement | None = None,
+    ) -> None:
+        super().__init__(k, default_increments)
+        if len(index_grid) == 0:
+            raise TQECException("Cannot create a grid with no row.")
+        if len(index_grid[0]) == 0:
+            raise TQECException("Cannot create a grid with no column.")
+        indices_in_grid: npt.NDArray[numpy.int_] = numpy.unique(index_grid)
+        if indices_in_grid[0] not in [0, 1] or not numpy.all(
+            numpy.equal(
+                indices_in_grid,
+                numpy.arange(
+                    indices_in_grid[0], indices_in_grid[0] + len(indices_in_grid)
+                ),
+            )
+        ):
+            raise TQECException(
+                "We require indices in the provided grid to be a contiguous "
+                "sequence of integers starting from either 0 (if some qubits "
+                "are not used) or 1."
+            )
+        self._rows = len(index_grid)
+        self._cols = len(index_grid[0])
+        self._grid = index_grid
+        self._sorted_indices_in_grid = indices_in_grid
+        self._tiled_template = deepcopy(tiled_template)
+        self._tiled_template.scale_to(k)
+
+    def _map_plaquette_indices(
+        self, row: int, col: int, plaquette_indices: Sequence[int]
+    ) -> list[int]:
+        offset = self._tiled_template.expected_plaquettes_number * self._grid[row][col]
+        return [pi + offset for pi in plaquette_indices]
+
+    @override
+    def scale_to(self, k: int) -> None:
+        self._k = k
+        self._tiled_template.scale_to(k)
+
+    @override
+    def instantiate(
+        self, plaquette_indices: Sequence[int] | None = None
+    ) -> npt.NDArray[numpy.int_]:
+        """Generate the numpy array representing the template.
+
+        Args:
+            plaquette_indices: the plaquette indices that will be forwarded to
+                the underlying Shape instance's instantiate method. Defaults
+                to `range(1, self.expected_plaquettes_number + 1)` if `None`.
+
+        Returns:
+            a numpy array with the given plaquette indices arranged according to
+            the underlying shape of the template.
+        """
+        if plaquette_indices is None:
+            plaquette_indices = list(range(1, self.expected_plaquettes_number + 1))
+
+        ret = numpy.zeros(self.shape.to_numpy_shape(), dtype=numpy.int_)
+        ishape = self._tiled_template.shape
+
+        for i, row in enumerate(self._grid):
+            for j, offset in enumerate(row):
+                # Here, an offset of 0 means "no template here", so we do not
+                # have anything to do.
+                if offset == 0:
+                    continue
+                template_expected_plaquettes_num = (
+                    self._tiled_template.expected_plaquettes_number
+                )
+                plaquette_offset = template_expected_plaquettes_num * (offset - 1)
+                plaquette_indices = list(
+                    range(
+                        plaquette_offset + 1,
+                        plaquette_offset + template_expected_plaquettes_num + 1,
+                    )
+                )
+                ret[
+                    i * ishape.y : (i + 1) * ishape.y, j * ishape.x : (j + 1) * ishape.x
+                ] = self._tiled_template.instantiate(plaquette_indices)
+        return ret
+
+    @property
+    @override
+    def scalable_shape(self) -> Scalable2D:
+        """Returns a scalable version of the template shape."""
+        base_shape = self._tiled_template.scalable_shape
+        return Scalable2D(self._cols * base_shape.x, self._rows * base_shape.y)
+
+    @property
+    @override
+    def expected_plaquettes_number(self) -> int:
+        """Returns the number of plaquettes expected from the `instantiate`
+        method.
+
+        Returns:
+            the number of plaquettes expected from the `instantiate` method.
+        """
+        max_index: int = self._sorted_indices_in_grid[-1]
+        return self._tiled_template.expected_plaquettes_number * max_index
+
+    @override
+    def get_plaquette_indices_on_sides(self, _: list[TemplateSide]) -> list[int]:
+        """Get the indices of plaquettes that are located on the provided
+        sides.
+
+        Args:
+            sides: the sides to recover plaquettes from.
+
+        Returns:
+            a non-ordered list of plaquette numbers.
+        """
+        raise NotImplementedError()
diff --git a/src/tqec/templates/qubit.py b/src/tqec/templates/qubit.py
index 51896755..6ca6bae1 100644
--- a/src/tqec/templates/qubit.py
+++ b/src/tqec/templates/qubit.py
@@ -6,12 +6,12 @@ import numpy.typing as npt
 from typing_extensions import override
 
 from tqec.exceptions import TQECWarning
-from tqec.templates.base import Template
+from tqec.templates.base import RectangularTemplate
 from tqec.templates.enums import TemplateSide
 from tqec.templates.scale import LinearFunction, Scalable2D
 
 
-class QubitTemplate(Template):
+class QubitTemplate(RectangularTemplate):
     """An error-corrected qubit.
 
     The below text represents this template for an input ``k == 2`` ::
@@ -92,7 +92,7 @@ class QubitTemplate(Template):
         return indices
 
 
-class QubitVerticalBorders(Template):
+class QubitVerticalBorders(RectangularTemplate):
     """Two vertical sides of neighbouring error-corrected qubits glued
     together.
 
@@ -164,7 +164,7 @@ class QubitVerticalBorders(Template):
         return indices
 
 
-class QubitHorizontalBorders(Template):
+class QubitHorizontalBorders(RectangularTemplate):
     """Two horizontal sides of neighbouring error-corrected qubits glued
     together.
 
@@ -231,7 +231,7 @@ class QubitHorizontalBorders(Template):
         return indices
 
 
-class Qubit4WayJunctionTemplate(Template):
+class Qubit4WayJunctionTemplate(RectangularTemplate):
     """An error-corrected qubit that is making a 4-way junction with other
     logical qubits.
 
diff --git a/src/tqec/templates/subtemplates.py b/src/tqec/templates/subtemplates.py
new file mode 100644
index 00000000..aebaf0ab
--- /dev/null
+++ b/src/tqec/templates/subtemplates.py
@@ -0,0 +1,192 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+
+import numpy
+import numpy.typing as npt
+
+from tqec.exceptions import TQECException
+
+
+@dataclass(frozen=True)
+class UniqueSubTemplates:
+    """Stores information on the sub-templates of a specific radius present on
+    a larger `Template` instance.
+
+    A sub-template is defined here as a portion of a `Template` instantiation.
+    In other words, a sub-template is a sub-array of the array resulting from
+    calling the method `Template.instantiate` on any `Template` instance.
+    The size of this sub-array is defined by its `radius`, which is computed
+    according to the Manhattan distance.
+
+    For example, let's say you have the following array from calling
+    `Template.instantiate` on a `Template` instance:
+
+    ```
+    1  5  6  5  6  2
+    7  9 10  9 10 11
+    8 10  9 10  9 12
+    7  9 10  9 10 11
+    8 10  9 10  9 12
+    3 13 14 13 14  4
+    ```
+
+    Focusing on the top-left `9` (coordinates `(1, 1)` in the array), the
+    following sub-array is a sub-template of radius `1`, centered on the
+    `(1, 1)` coordinate of the above instantiation:
+
+    ```
+    1  5  6
+    7  9 10
+    8 10  9
+    ```
+
+    Still focusing on the top-left `9`, the following sub-array is a
+    sub-template of radius `2`, centered on the `(1, 1)` coordinate of the
+    above instantiation:
+
+    ```
+    .  .  .  .  .
+    .  1  5  6  5
+    .  7  9 10  9
+    .  8 10  9 10
+    .  7  9 10  9
+    ```
+
+    Note the inclusion of `.` that are here to represent `0` (i.e., the
+    absence of plaquette at that particular point) because there is no
+    plaquette in the original `Template` instantiation.
+
+    This dataclass efficiently stores all the sub-templates of a given
+    `Template` instantiation and of a given `radius`.
+
+
+    Attributes:
+        subtemplate_indices: an array that has the same shape as the
+            original `Template` instantiation but stores sub-template
+            indices referencing sub-templates from the `subtemplates`
+            attribute. The integers in this array do NOT represent
+            plaquette indices.
+        subtemplates: a store of sub-template (values) indexed by integers
+            (keys) that link the sub-template center to the original
+            template instantiation thanks to `subtemplate_indices`.
+
+    Raises:
+        TQECException: if any index in `self.subtemplate_indices` is
+            not present in `self.subtemplates.keys()`.
+        TQECException: if any of the sub-template shapes is non-square
+            or of even width or length.
+        TQECException: if not all the sub-template shapes in
+            `self.subtemplates.values()` are equal.
+    """
+
+    subtemplate_indices: npt.NDArray[numpy.int_]
+    subtemplates: dict[int, npt.NDArray[numpy.int_]]
+
+    def __post_init__(self) -> None:
+        indices = frozenset(numpy.unique(self.subtemplate_indices))
+        if not indices.issubset(self.subtemplates.keys()):
+            raise TQECException(
+                "Found an index in subtemplate_indices that does "
+                "not correspond to a valid subtemplate."
+            )
+        shape = next(iter(self.subtemplates.values())).shape
+        if shape[0] != shape[1]:
+            raise TQECException(
+                "Subtemplate shapes are expected to be square. "
+                f"Found the shape {shape} that is not a square."
+            )
+        if shape[0] % 2 == 0:
+            raise TQECException(
+                "Subtemplate shapes are expected to be squares with an "
+                f"odd size length. Found size length {shape[0]}."
+            )
+        if not all(
+            subtemplate.shape == shape for subtemplate in self.subtemplates.values()
+        ):
+            raise TQECException(
+                "All the subtemplates should have the exact same shape. "
+                "Found one with a differing shape."
+            )
+
+    @property
+    def manhattan_radius(self) -> int:
+        return next(iter(self.subtemplates.values())).shape[0] // 2
+
+
+def get_spatially_distinct_subtemplates(
+    instantiation: npt.NDArray[numpy.int_],
+    manhattan_radius: int = 1,
+    avoid_zero_plaquettes: bool = True,
+) -> UniqueSubTemplates:
+    """Returns a representation of all the distinct sub-templates of the
+    provided manhattan radius.
+
+    Note:
+        This method will likely be inefficient for large templates (i.e., large
+        values of `k`) or for large Manhattan radiuses, both in terms of memory
+        used and computation time.
+        Subclasses are invited to reimplement that method using a specialized
+        algorithm (or hard-coded values) to speed things up.
+
+    Args:
+        instantiation: a 2-dimensional array representing the instantiated
+            template on which sub-templates should be computed.
+        manhattan_radius: radius of the considered ball using the Manhattan
+            distance. Only squares with sides of `2*manhattan_radius+1`
+            plaquettes will be considered.
+        avoid_zero_plaquettes: `True` if sub-templates with an empty plaquette
+            (i.e., 0 value in the instantiation of the Template instance) at
+            its center should be ignored. Default to `True`.
+
+    Returns:
+        a representation of all the sub-templates found.
+    """
+    y, x = instantiation.shape
+    extended_instantiation = numpy.pad(
+        instantiation, manhattan_radius, "constant", constant_values=0
+    )
+
+    all_possible_subarrays: list[npt.NDArray[numpy.int_]] = []
+    ignored_flattened_indices: list[int] = []
+    considered_flattened_indices: list[int] = []
+    for i in range(manhattan_radius, manhattan_radius + y):
+        for j in range(manhattan_radius, manhattan_radius + x):
+            # Do not generate anything if the center plaquette is 0 in the
+            # original instantiation.
+            if avoid_zero_plaquettes and extended_instantiation[i, j] == 0:
+                ignored_flattened_indices.append(
+                    (i - manhattan_radius) * x + (j - manhattan_radius)
+                )
+                continue
+            considered_flattened_indices.append(
+                (i - manhattan_radius) * x + (j - manhattan_radius)
+            )
+            all_possible_subarrays.append(
+                extended_instantiation[
+                    i - manhattan_radius : i + manhattan_radius + 1,
+                    j - manhattan_radius : j + manhattan_radius + 1,
+                ]
+            )
+    unique_situations, inverse_indices = numpy.unique(
+        all_possible_subarrays, axis=0, return_inverse=True
+    )
+
+    # Note that the `inverse_indices` DO NOT include the ignored sub-templates because
+    # their center was a 0 plaquette if `avoid_zero_plaquettes` is `True` so we
+    # should reconstruct the full indices from `inverse_indices` and
+    # `ignored_flattened_indices`.
+    # By convention, the index 0 will represent the ignored sub-templates, so we
+    # also have to shift the `inverse_indices` and `unique_situations` keys by 1.
+    # Start by shifting by 1.
+    inverse_indices += 1
+    subtemplates_by_indices = {
+        i + 1: situation for i, situation in enumerate(unique_situations)
+    }
+    if avoid_zero_plaquettes:
+        final_indices = numpy.zeros((y * x,), dtype=numpy.int_)
+        final_indices[ignored_flattened_indices] = 0
+        final_indices[considered_flattened_indices] = inverse_indices
+    else:
+        final_indices = inverse_indices
+    return UniqueSubTemplates(final_indices.reshape((y, x)), subtemplates_by_indices)


Test patch:
diff --git a/src/tqec/circuit/observable_qubits_test.py b/src/tqec/circuit/observable_qubits_test.py
index 439d02f2..176e3123 100644
--- a/src/tqec/circuit/observable_qubits_test.py
+++ b/src/tqec/circuit/observable_qubits_test.py
@@ -15,7 +15,7 @@ def plaquettes() -> list[Plaquette]:
     ]
 
 
-def test_raw_rectangle_default_obserevable_qubits(plaquettes: list[Plaquette]) -> None:
+def test_raw_rectangle_default_observable_qubits(plaquettes: list[Plaquette]) -> None:
     template = FixedTemplate(
         [
             [0, 1, 0, 1],
diff --git a/src/tqec/templates/_testing.py b/src/tqec/templates/_testing.py
index c269421a..eaed006f 100644
--- a/src/tqec/templates/_testing.py
+++ b/src/tqec/templates/_testing.py
@@ -5,12 +5,12 @@ import numpy.typing as npt
 from typing_extensions import override
 
 from tqec.position import Displacement
-from tqec.templates.base import Template
-from tqec.templates.enums import TemplateSide
+from tqec.templates.base import RectangularTemplate
+from tqec.templates.enums import TemplateOrientation, TemplateSide
 from tqec.templates.scale import LinearFunction, Scalable2D
 
 
-class FixedTemplate(Template):
+class FixedTemplate(RectangularTemplate):
     """A fixed template, only used internally for testing."""
 
     def __init__(
@@ -45,7 +45,7 @@ class FixedTemplate(Template):
         return max((max(line, default=0) for line in self._indices), default=0) + 1
 
     @override
-    def get_plaquette_indices_on_sides(self, sides: list[TemplateSide]) -> list[int]:
+    def get_plaquette_indices_on_sides(self, _: list[TemplateSide]) -> list[int]:
         raise NotImplementedError(
             "Cannot call FixedTemplate.get_plaquette_indices_on_sides."
         )