Repository: QCHackers/tqec
Pull request #130 (2024-02-08 20:18:09)

Add tests to the library
This branch has been created from #100 because both #114 and #100 added/changed a lot of things and I did not want to write tests for things that will be removed soon.

There is no need to look at the changes here, the immense majority of the files changed are caused by #100 and #114 and will disappear from this PR once they are merged.

Should fix or at least help fixing #106  

review-notebook-app[bot]: Check out this pull request on&nbsp; <a href="https://app.reviewnb.com/QCHackers/tqec/pull/130"><img align="absmiddle"  alt="ReviewNB" height="28" class="BotMessageButtonImage" src="https://raw.githubusercontent.com/ReviewNB/support/master/images/button_reviewnb.png"/></a> 

 See visual diffs & provide feedback on Jupyter Notebooks. 

---

 <i>Powered by <a href='https://www.reviewnb.com/?utm_source=gh'>ReviewNB</a></i>
nelimee: I will rebase that PR on #137 in order to include tests sooner. #100 is more complex than expected and most of the library-related changes have been implemented in "sub-PRs" that have already been merged.

Waiting for the review process of #137 to finish and for #137 to be merged to adapt this to `main` and open for review.
nelimee: > The current state of this PR is:
> 
>     * `tqec.generation.circuit` is covered (per `pytest-cov` output) but the tests are not good enough.
> 
>     * `tqec.plaquette` is not tested.
> 
>     * `tqec.noise_models` is not tested.
> 
>     * `tqec.template.composed` is not tested.
> 
>     * the other parts of the library should be adequately tested.

A few updates (note that the modules have been moved around since the last message #163):
- `tqec.circuit.circuit` is not tested enough yet, but some meaningful tests are present.
- testing `tqec.plaquette` would highly benefit from using `stim.Tableau` and `stim.PauliString` to check the effect of plaquettes on a stabilised state. This will likely come in a future PR, so I think we can call it a day on that part and wait for more code.
- `tqec.noise_models` is a pain to implement tests for.
- `tqec.template.composed` has a few tests, better than nothing, but insufficient.

Because I feel like it will take forever to implement the missing tests, I would prefer to merge that PR now and implement the missing tests incrementally.

Patch:
diff --git a/pyproject.toml b/pyproject.toml
index 46dc5403..d52d9177 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -43,7 +43,7 @@ Repository = "https://github.com/QCHackers/tqec"
 Issues = "https://github.com/QCHackers/tqec/issues"
 
 [project.optional-dependencies]
-test = ["pytest", "mypy"]
+test = ["pytest", "mypy", "pytest-cov"]
 dev = ["sinter", "pymatching", "jupyterlab", "tqec[test]"]
 all = ["tqec[test, dev]"]
 
diff --git a/src/tqec/_version.py b/src/tqec/_version.py
index 429a90f3..cc9c9344 100644
--- a/src/tqec/_version.py
+++ b/src/tqec/_version.py
@@ -1,3 +1,3 @@
-import importlib
+import importlib.metadata
 
 __version__ = importlib.metadata.version("tqec")
diff --git a/src/tqec/circuit/circuit.py b/src/tqec/circuit/circuit.py
index 99ed3dc6..b06520f4 100644
--- a/src/tqec/circuit/circuit.py
+++ b/src/tqec/circuit/circuit.py
@@ -1,5 +1,6 @@
 from __future__ import annotations
 
+import typing as ty
 from copy import deepcopy
 
 import cirq
@@ -34,15 +35,16 @@ def generate_circuit(
             we want to implement.
         plaquettes: description of the computation that should happen at
             different time-slices of the quantum error correction experiment (or
-            at least part of it).
+            at least part of it). If provided as a dictionary, plaquettes should be
+            1-indexed (i.e., ``0 not in plaquettes`` should be ``True``).
 
     Returns:
         a cirq.Circuit instance implementing the (part of) quantum error
         correction experiment represented by the provided inputs.
 
     Raises:
-        TQECException: if the provided plaquettes do not match the expected
-            number of plaquettes for the given template.
+        TQECException: if ``len(plaquettes) != template.expected_plaquettes_number`` or
+            if plaquettes is provided as a dicitonary and ``0 in plaquettes``.
     """
     # Check that the user gave enough plaquettes.
     if len(plaquettes) != template.expected_plaquettes_number:
@@ -50,6 +52,11 @@ def generate_circuit(
             f"{len(plaquettes)} plaquettes have been provided, but "
             f"{template.expected_plaquettes_number} were expected."
         )
+    if isinstance(plaquettes, ty.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 plaquettes are given as a list, make that a dict to simplify the following operations
     if isinstance(plaquettes, list):
diff --git a/src/tqec/circuit/operations/operation.py b/src/tqec/circuit/operations/operation.py
index 0102c983..6fa28388 100644
--- a/src/tqec/circuit/operations/operation.py
+++ b/src/tqec/circuit/operations/operation.py
@@ -4,6 +4,7 @@ from dataclasses import dataclass
 from typing import Any, Sequence
 
 import cirq
+
 from tqec.exceptions import TQECException
 
 STIM_TAG = "STIM_OPERATION"
diff --git a/src/tqec/circuit/operations/transformer.py b/src/tqec/circuit/operations/transformer.py
index 36fc3c3b..2a2261ff 100644
--- a/src/tqec/circuit/operations/transformer.py
+++ b/src/tqec/circuit/operations/transformer.py
@@ -38,7 +38,7 @@ def _transform_to_stimcirq_compatible_impl(
                 if isinstance(operation.repetitions, int):
                     operation_repetitions = operation.repetitions
                 elif isinstance(operation.repetitions, sympy.Expr):
-                    operation_repetitions = int(operation.repetitions.evalf())
+                    operation_repetitions = int(operation.repetitions.evalf())  # type: ignore
                 elif isinstance(operation.repetitions, numpy.integer):
                     operation_repetitions = int(operation.repetitions)
                 else:
diff --git a/src/tqec/plaquette/plaquette.py b/src/tqec/plaquette/plaquette.py
index 813a9885..035f61e9 100644
--- a/src/tqec/plaquette/plaquette.py
+++ b/src/tqec/plaquette/plaquette.py
@@ -40,7 +40,7 @@ class Plaquette:
         plaquette_qubits = {qubit.to_grid_qubit() for qubit in qubits}
         circuit_qubits = set(circuit.raw_circuit.all_qubits())
         if not circuit_qubits.issubset(plaquette_qubits):
-            wrong_qubits = plaquette_qubits.difference(circuit_qubits)
+            wrong_qubits = circuit_qubits.difference(plaquette_qubits)
             raise TQECException(
                 f"The following qubits ({wrong_qubits}) are in the provided circuit "
                 "but not in the list of PlaquetteQubit."
@@ -128,7 +128,10 @@ class SquarePlaquette(Plaquette):
 
 class RoundedPlaquette(SquarePlaquette):
     def __init__(
-        self, circuit: ScheduledCircuit, orientation: PlaquetteOrientation
+        self,
+        circuit: ScheduledCircuit,
+        orientation: PlaquetteOrientation,
+        add_unused_qubits: bool = False,
     ) -> None:
         """Represents a rounded QEC plaquette
 
diff --git a/src/tqec/plaquette/qubit.py b/src/tqec/plaquette/qubit.py
index d9d1f6d8..5c228bfa 100644
--- a/src/tqec/plaquette/qubit.py
+++ b/src/tqec/plaquette/qubit.py
@@ -1,11 +1,10 @@
 from dataclasses import dataclass
 
 from cirq import GridQubit
-
 from tqec.position import Position
 
 
-@dataclass
+@dataclass(frozen=True)
 class PlaquetteQubit:
     """Defines a qubit in the plaquette coordinate system
 
diff --git a/src/tqec/position.py b/src/tqec/position.py
index 81a20da0..e092796d 100644
--- a/src/tqec/position.py
+++ b/src/tqec/position.py
@@ -1,7 +1,7 @@
 from dataclasses import dataclass
 
 
-@dataclass
+@dataclass(frozen=True)
 class Position:
     """Simple wrapper around tuple[int, int].
 
@@ -16,6 +16,10 @@ class Position:
     x: int
     y: int
 
+    def to_grid_qubit(self) -> tuple[int, int]:
+        """Returns the position as a tuple following the cirq.GridQubit coordinate system."""
+        return (self.y, self.x)
+
 
 @dataclass
 class Shape2D:
diff --git a/src/tqec/templates/base.py b/src/tqec/templates/base.py
index d09e6d91..206d03b7 100644
--- a/src/tqec/templates/base.py
+++ b/src/tqec/templates/base.py
@@ -6,6 +6,7 @@ from abc import ABC, abstractmethod
 from dataclasses import dataclass
 
 import numpy
+
 from tqec.enums import CornerPositionEnum, TemplateRelativePositionEnum
 from tqec.exceptions import TQECException
 from tqec.position import Displacement, Shape2D
@@ -193,3 +194,17 @@ class TemplateWithIndices:
 
     template: Template
     indices: list[int]
+
+    def __post_init__(self):
+        if self.template.expected_plaquettes_number != len(self.indices):
+            raise TQECException(
+                f"Creating a {self.__class__.__name__} instance with the template "
+                f"{self.template} (that requires {self.template.expected_plaquettes_number} "
+                f"plaquette indices) and a non-matching number of plaquette indices "
+                f"{self.indices}."
+            )
+        if any(i < 0 for i in self.indices):
+            raise TQECException(
+                "Cannot have negative plaquette indices. Found a negative index "
+                f"in {self.indices}."
+            )
diff --git a/src/tqec/templates/composed.py b/src/tqec/templates/composed.py
index b581356d..c7bb566f 100644
--- a/src/tqec/templates/composed.py
+++ b/src/tqec/templates/composed.py
@@ -2,6 +2,7 @@ import typing as ty
 
 import networkx as nx
 import numpy
+
 from tqec.enums import CornerPositionEnum, TemplateRelativePositionEnum
 from tqec.exceptions import TQECException
 from tqec.position import Displacement, Position, Shape2D
@@ -264,8 +265,10 @@ class ComposedTemplate(Template):
 
         Returns:
             a mapping between templates indices and their upper-left corner
-            absolute position.
+            absolute position. This mapping is empty if ``self.is_empty``.
         """
+        if self.is_empty:
+            return {}
         ul_positions: dict[int, Position] = {0: Position(0, 0)}
         src: int
         dest: int
@@ -479,3 +482,7 @@ class ComposedTemplate(Template):
     @property
     def expected_plaquettes_number(self) -> int:
         return self._maximum_plaquette_mapping_index
+
+    @property
+    def is_empty(self) -> bool:
+        return len(self._templates) == 0
diff --git a/src/tqec/templates/shifted.py b/src/tqec/templates/shifted.py
new file mode 100644
index 00000000..dbf1438d
--- /dev/null
+++ b/src/tqec/templates/shifted.py
@@ -0,0 +1,66 @@
+import typing as ty
+from dataclasses import dataclass
+
+import numpy
+
+from tqec.position import Shape2D
+from tqec.templates.base import Template
+from tqec.templates.scale import Dimension
+
+
+@dataclass
+class ScalableOffset:
+    x: Dimension
+    y: Dimension
+
+    def scale_to(self, k: int) -> None:
+        self.x.scale_to(k)
+        self.y.scale_to(k)
+
+    def to_dict(self) -> dict[str, ty.Any]:
+        return {"x": self.x.to_dict(), "y": self.y.to_dict()}
+
+
+class ShiftedTemplate(Template):
+    def __init__(
+        self,
+        template: Template,
+        offset: ScalableOffset,
+        default_x_increment: int = 2,
+        default_y_increment: int = 2,
+    ) -> None:
+        super().__init__(default_x_increment, default_y_increment)
+        self._shifted_template = template
+        self._offset = offset
+
+    def scale_to(self, k: int) -> "ShiftedTemplate":
+        self._shifted_template.scale_to(k)
+        self._offset.scale_to(k)
+        return self
+
+    @property
+    def shape(self) -> Shape2D:
+        tshape = self._shifted_template.shape
+        return Shape2D(self._offset.x.value + tshape.x, self._offset.y.value + tshape.y)
+
+    def to_dict(self) -> dict[str, ty.Any]:
+        return super().to_dict() | {
+            "shifted": {
+                "template": self._shifted_template.to_dict(),
+                "offset": self._offset.to_dict(),
+            }
+        }
+
+    @property
+    def expected_plaquettes_number(self) -> int:
+        return self._shifted_template.expected_plaquettes_number
+
+    def instantiate(self, plaquette_indices: ty.Sequence[int]) -> numpy.ndarray:
+        # Do not explicitely check here, the check is forwarded to the
+        # shifted Template instance.
+        arr = numpy.zeros(self.shape.to_numpy_shape(), dtype=int)
+        tshape = self._shifted_template.shape
+        xoffset, yoffset = self._offset.x.value, self._offset.y.value
+        tarr = self._shifted_template.instantiate(plaquette_indices)
+        arr[yoffset : yoffset + tshape.y, xoffset : xoffset + tshape.x] = tarr
+        return arr
diff --git a/src/tqec/templates/stack.py b/src/tqec/templates/stack.py
new file mode 100644
index 00000000..00cec91c
--- /dev/null
+++ b/src/tqec/templates/stack.py
@@ -0,0 +1,107 @@
+import typing as ty
+
+import numpy
+
+from tqec.position import Shape2D
+from tqec.templates.base import Template
+
+
+class StackedTemplate(Template):
+    def __init__(
+        self, default_x_increment: int = 2, default_y_increment: int = 2
+    ) -> None:
+        """A template composed of templates stacked on top of each others.
+
+        This class implements a naive stack of Template instances. Each Template instance
+        added to this class will be superposed on top of the previously added templates,
+        potentially hiding parts of these.
+
+        ## Warning
+        This class does no effort to simplify the stack of templates. In particular, the
+        plaquette indices that should be provided to the instanciate method are directly
+        forwarded to the stacked templates, from bottom to top. If a stacked Template
+        instance is hidding completly at least one kind of plaquette, this plaquette index
+        should still be provided.
+
+        ### Example
+        Stacking the following template
+        ```text
+        1 2
+        2 1
+        ```
+        on itself will require 4 (FOUR) template indices when calling `instanciate`:
+        - the first 2 indices being forwarded to the bottom-most Template,
+        - the last 2 indices being forwarded to the Template on top of it.
+
+        The instanciation of such a stack using
+        ```py
+        stack.instanciate(1, 2, 3, 4)
+        ```
+        will return
+        ```text
+        3 4
+        4 3
+        ```
+        as the last 2 indices (3 and 4) are forwarded to the top-most Template instance
+        that hides the bottom one.
+        """
+        super().__init__(default_x_increment, default_y_increment)
+        self._stack: list[Template] = []
+
+    def push_template_on_top(
+        self,
+        template: Template,
+    ) -> None:
+        """Place a new template on the top of the stack.
+
+        The new template can be offset by a certain amount, that might be scalable.
+
+        :raises TQECException: if any of the specified offset coordinates is not positive.
+        """
+        self._stack.append(template)
+
+    def pop_template_from_top(self) -> Template:
+        """Removes the top-most template from the stack."""
+        return self._stack.pop()
+
+    def scale_to(self, k: int) -> "StackedTemplate":
+        for t in self._stack:
+            t.scale_to(k)
+        return self
+
+    @property
+    def shape(self) -> Shape2D:
+        shapex, shapey = 0, 0
+        for template in self._stack:
+            tshape = template.shape
+            shapex = max(shapex, tshape.x)
+            shapey = max(shapey, tshape.y)
+        return Shape2D(shapex, shapey)
+
+    def to_dict(self) -> dict[str, ty.Any]:
+        return super().to_dict() | {
+            "stack": {"templates": [t.to_dict() for t in self._stack]}
+        }
+
+    @property
+    def expected_plaquettes_number(self) -> int:
+        return sum(t.expected_plaquettes_number for t in self._stack)
+
+    def instantiate(self, plaquette_indices: ty.Sequence[int]) -> numpy.ndarray:
+        arr = numpy.zeros(self.shape.to_numpy_shape(), dtype=int)
+        first_non_used_plaquette_index: int = 0
+        for template in self._stack:
+            istart = first_non_used_plaquette_index
+            istop = istart + template.expected_plaquettes_number
+            indices = [plaquette_indices[i] for i in range(istart, istop)]
+            first_non_used_plaquette_index = istop
+
+            tarr = template.instantiate(indices)
+            yshape, xshape = tarr.shape
+
+            # We do not want "0" plaquettes (i.e., "no plaquette" with our convention) to
+            # stack over and erase non-zero plaquettes.
+            # To avoid that, we only replace on the non-zeros entries of the stacked over array.
+            nonzeros = tarr.nonzero()
+            arr[0:yshape, 0:xshape][nonzeros] = tarr[nonzeros]
+        return arr


Test patch:
diff --git a/src/tqec/circuit/circuit_test.py b/src/tqec/circuit/circuit_test.py
index 63065652..ee70ed6e 100644
--- a/src/tqec/circuit/circuit_test.py
+++ b/src/tqec/circuit/circuit_test.py
@@ -1,181 +1,55 @@
-"""Example taken from /notebooks/logical_qubit_memory_experiment.ipynb"""
-
 import cirq
+import pytest
 
 from tqec.circuit.circuit import generate_circuit
-from tqec.circuit.operations.operation import make_shift_coords
-from tqec.enums import PlaquetteOrientation
-from tqec.plaquette.library import (
-    MeasurementRoundedPlaquette,
-    MeasurementSquarePlaquette,
-    XXMemoryPlaquette,
-    XXXXMemoryPlaquette,
-    ZRoundedInitialisationPlaquette,
-    ZSquareInitialisationPlaquette,
-    ZZMemoryPlaquette,
-    ZZZZMemoryPlaquette,
-)
+from tqec.exceptions import TQECException
+from tqec.plaquette.library import ZSquareInitialisationPlaquette
 from tqec.plaquette.plaquette import Plaquette
-from tqec.templates.constructions.qubit import QubitSquareTemplate
-from tqec.templates.scale import Dimension, LinearFunction
+from tqec.templates.atomic.rectangle import RawRectangleTemplate
+from tqec.templates.base import Template
 
 
-def _normalise_circuit(norm_circuit: cirq.Circuit) -> cirq.Circuit:
-    ordered_transformers = [
-        cirq.drop_empty_moments,
-    ]
-    for transformer in ordered_transformers:
-        norm_circuit = transformer(norm_circuit)
-    return norm_circuit
+@pytest.fixture
+def initialisation_plaquette() -> Plaquette:
+    return ZSquareInitialisationPlaquette()
 
 
-def _make_repeated_layer(repeat_circuit: cirq.Circuit) -> cirq.Circuit:
-    # Note: we do not care on which qubit it is applied, but we want a SHIFT_COORDS instruction
-    #       to be inserted somewhere in the repetition loop. It is inserted at the beginning.
-    any_qubit = next(iter(repeat_circuit.all_qubits()), None)
-    assert (
-        any_qubit is not None
-    ), "Could not find any qubit in the given Circuit instance."
-    circuit_to_repeat = cirq.Circuit([make_shift_coords(0, 0, 1)]) + repeat_circuit
-    repeated_circuit_operation = cirq.CircuitOperation(
-        circuit_to_repeat.freeze()
-    ).repeat(9)
-    return cirq.Circuit([repeated_circuit_operation])
+@pytest.fixture
+def one_by_one_template() -> Template:
+    return RawRectangleTemplate([[0]])
 
 
-def _generate_circuit() -> cirq.Circuit:
-    template = QubitSquareTemplate(Dimension(2, LinearFunction(2)))
-    plaquettes: list[list[Plaquette]] = [
-        [
-            ZRoundedInitialisationPlaquette(PlaquetteOrientation.UP),
-            XXMemoryPlaquette(
-                PlaquetteOrientation.UP,
-                [1, 2, 5, 6, 7, 8],
-                include_detector=False,
-                is_first_round=True,
-            ),
-            XXMemoryPlaquette(PlaquetteOrientation.UP, [1, 2, 5, 6, 7, 8]),
-            MeasurementRoundedPlaquette(
-                PlaquetteOrientation.UP, include_detector=False
-            ),
-        ],
-        [
-            ZRoundedInitialisationPlaquette(PlaquetteOrientation.LEFT),
-            ZZMemoryPlaquette(
-                PlaquetteOrientation.LEFT, [1, 5, 6, 8], is_first_round=True
-            ),
-            ZZMemoryPlaquette(PlaquetteOrientation.LEFT, [1, 5, 6, 8]),
-            MeasurementRoundedPlaquette(PlaquetteOrientation.LEFT),
-        ],
-        [
-            ZSquareInitialisationPlaquette(),
-            XXXXMemoryPlaquette(
-                [1, 2, 3, 4, 5, 6, 7, 8], include_detector=False, is_first_round=True
-            ),
-            XXXXMemoryPlaquette([1, 2, 3, 4, 5, 6, 7, 8]),
-            MeasurementSquarePlaquette(include_detector=False),
-        ],
-        [
-            ZSquareInitialisationPlaquette(),
-            ZZZZMemoryPlaquette([1, 3, 4, 5, 6, 8], is_first_round=True),
-            ZZZZMemoryPlaquette([1, 3, 4, 5, 6, 8]),
-            MeasurementSquarePlaquette(),
-        ],
-        [
-            ZRoundedInitialisationPlaquette(PlaquetteOrientation.RIGHT),
-            ZZMemoryPlaquette(
-                PlaquetteOrientation.RIGHT, [1, 3, 4, 8], is_first_round=True
-            ),
-            ZZMemoryPlaquette(PlaquetteOrientation.RIGHT, [1, 3, 4, 8]),
-            MeasurementRoundedPlaquette(PlaquetteOrientation.RIGHT),
-        ],
-        [
-            ZRoundedInitialisationPlaquette(PlaquetteOrientation.DOWN),
-            XXMemoryPlaquette(
-                PlaquetteOrientation.DOWN,
-                [1, 2, 3, 4, 7, 8],
-                include_detector=False,
-                is_first_round=True,
-            ),
-            XXMemoryPlaquette(PlaquetteOrientation.DOWN, [1, 2, 3, 4, 7, 8]),
-            MeasurementRoundedPlaquette(
-                PlaquetteOrientation.DOWN, include_detector=False
-            ),
-        ],
-    ]
-    layer_modificators = {1: _make_repeated_layer}
+def test_generate_initialisation_circuit_list(
+    initialisation_plaquette: Plaquette, one_by_one_template
+):
+    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
+    )
 
-    # 4. Actually create the cirq.Circuit instance by concatenating the circuits generated
-    # for each layers and potentially modified by the modifiers defined above.
-    circuit = cirq.Circuit()
-    for layer_index in range(4):
-        layer_circuit = generate_circuit(
-            template,
-            [plaquette_list[layer_index] for plaquette_list in plaquettes],
-        )
-        layer_circuit = _normalise_circuit(layer_circuit)
-        circuit += layer_modificators.get(layer_index, lambda circ: circ)(layer_circuit)
 
-    return circuit
+def test_generate_initialisation_circuit_dict(
+    initialisation_plaquette: Plaquette, one_by_one_template
+):
+    circuit = generate_circuit(one_by_one_template, {1: initialisation_plaquette})
+    assert circuit == cirq.Circuit(
+        cirq.R(q.to_grid_qubit()) for q in initialisation_plaquette.qubits
+    )
+
 
+def test_generate_initialisation_circuit_dict_0_indexed(
+    initialisation_plaquette: Plaquette, one_by_one_template
+):
+    with pytest.raises(TQECException):
+        generate_circuit(one_by_one_template, {0: initialisation_plaquette})
 
-def test_generate_circuit():
-    """Minimal test to check that the circuit is generated correctly
-    The target qubits are taken from the orginal notebook output.
-    """
-    generated_circuit = _generate_circuit()
-    generate_qubits = [(q.row, q.col) for q in generated_circuit.all_qubits()]
-    target_qubits = [
-        (6, 2),
-        (3, 7),
-        (3, 9),
-        (1, 1),
-        (10, 6),
-        (8, 8),
-        (1, 5),
-        (2, 0),
-        (6, 4),
-        (2, 2),
-        (6, 6),
-        (1, 3),
-        (7, 1),
-        (7, 3),
-        (4, 2),
-        (1, 7),
-        (7, 5),
-        (1, 9),
-        (7, 7),
-        (2, 4),
-        (6, 8),
-        (9, 1),
-        (2, 6),
-        (9, 3),
-        (9, 7),
-        (4, 6),
-        (4, 4),
-        (7, 9),
-        (5, 1),
-        (5, 3),
-        (2, 8),
-        (9, 5),
-        (5, 7),
-        (8, 2),
-        (0, 4),
-        (9, 9),
-        (3, 1),
-        (4, 8),
-        (5, 5),
-        (3, 3),
-        (4, 10),
-        (3, 5),
-        (10, 2),
-        (8, 4),
-        (5, 9),
-        (8, 6),
-        (0, 8),
-        (6, 0),
-        (8, 10),
-    ]
-    generate_qubits.sort()
-    target_qubits.sort()
-    assert generate_qubits == target_qubits
+
+def test_generate_circuit_wrong_number_of_plaquettes(
+    initialisation_plaquette: Plaquette, one_by_one_template
+):
+    with pytest.raises(TQECException):
+        generate_circuit(
+            one_by_one_template, [initialisation_plaquette, initialisation_plaquette]
+        )
+    with pytest.raises(TQECException):
+        generate_circuit(one_by_one_template, [])
diff --git a/src/tqec/circuit/operations/measurement_map_test.py b/src/tqec/circuit/operations/measurement_map_test.py
new file mode 100644
index 00000000..5733813a
--- /dev/null
+++ b/src/tqec/circuit/operations/measurement_map_test.py
@@ -0,0 +1,209 @@
+import cirq
+import pytest
+
+from tqec.circuit.operations.measurement_map import CircuitMeasurementMap, flatten
+from tqec.exceptions import TQECException
+
+
+@pytest.fixture
+def empty_circuit():
+    return cirq.Circuit()
+
+
+@pytest.fixture
+def flat_circuit():
+    qubits = cirq.GridQubit.rect(10, 10)
+    return cirq.Circuit(
+        [cirq.H(q) for q in qubits],
+        [cirq.CX(qubits[i], qubits[i + 1]) for i in range(0, 100, 2)],
+        [cirq.M(q) for q in qubits],
+    )
+
+
+@pytest.fixture
+def circuit_with_depth1_circuit_operation():
+    qubits = cirq.GridQubit.rect(10, 10)
+    return cirq.Circuit(
+        [cirq.H(q) for q in qubits],
+        cirq.CircuitOperation(
+            cirq.Circuit(
+                [cirq.CX(qubits[i], qubits[i + 1]) for i in range(0, 100, 2)]
+            ).freeze()
+        ),
+        [cirq.M(q) for q in qubits],
+    )
+
+
+@pytest.fixture
+def circuit_with_depth2_circuit_operation():
+    qubits = cirq.GridQubit.rect(10, 10)
+    return cirq.Circuit(
+        cirq.CircuitOperation(
+            cirq.Circuit(
+                [cirq.H(q) for q in qubits],
+                cirq.CircuitOperation(
+                    cirq.Circuit(
+                        [cirq.CX(qubits[i], qubits[i + 1]) for i in range(0, 100, 2)]
+                    ).freeze()
+                ),
+                [cirq.M(q) for q in qubits],
+            ).freeze()
+        )
+    )
+
+
+@pytest.fixture
+def circuit_with_depth1_repeated_circuit_operation():
+    qubits = cirq.GridQubit.rect(10, 10)
+    return cirq.Circuit(
+        [cirq.H(q) for q in qubits],
+        cirq.CircuitOperation(
+            cirq.Circuit(
+                [cirq.CX(qubits[i], qubits[i + 1]) for i in range(0, 100, 2)]
+            ).freeze()
+        ).repeat(10),
+        [cirq.M(q) for q in qubits],
+    )
+
+
+@pytest.fixture
+def circuit_with_repeated_measurements():
+    qubits = cirq.GridQubit.rect(10, 10)
+    return cirq.Circuit(
+        *[cirq.Moment([cirq.M(q) for q in qubits]) for _ in range(10)],
+    )
+
+
+def test_flatten_empty_circuit(empty_circuit):
+    flattened_circuit = flatten(empty_circuit)
+    assert flattened_circuit == empty_circuit
+
+
+def test_flatten_flat_circuit(flat_circuit):
+    flattened_circuit = flatten(flat_circuit)
+    assert flattened_circuit == flat_circuit
+
+
+def test_flatten_depth1_circuit(circuit_with_depth1_circuit_operation, flat_circuit):
+    flattened_circuit = flatten(circuit_with_depth1_circuit_operation)
+    assert flattened_circuit == flat_circuit
+
+
+def test_flatten_depth2_circuit(circuit_with_depth2_circuit_operation, flat_circuit):
+    flattened_circuit = flatten(circuit_with_depth2_circuit_operation)
+    assert flattened_circuit == flat_circuit
+
+
+def test_flatten_depth1_repeated_circuit(
+    circuit_with_depth1_repeated_circuit_operation,
+):
+    flattened_circuit = flatten(circuit_with_depth1_repeated_circuit_operation)
+    assert all(
+        not isinstance(op, cirq.CircuitOperation)
+        for op in flattened_circuit.all_operations()
+    )
+    # There should be 500 CX gates
+    assert (
+        sum(op._num_qubits_() == 2 for op in flattened_circuit.all_operations()) == 500
+    )
+
+
+def test_measurement_map_empty_initialisation(empty_circuit):
+    CircuitMeasurementMap(empty_circuit)
+
+
+def test_measurement_map_flat_initialisation(flat_circuit):
+    CircuitMeasurementMap(flat_circuit)
+
+
+def test_measurement_map_depth1_initialisation(circuit_with_depth1_circuit_operation):
+    CircuitMeasurementMap(circuit_with_depth1_circuit_operation)
+
+
+def test_measurement_map_depth2_initialisation(circuit_with_depth2_circuit_operation):
+    CircuitMeasurementMap(circuit_with_depth2_circuit_operation)
+
+
+def test_measurement_map_depth1_repeated_initialisation(
+    circuit_with_depth1_repeated_circuit_operation,
+):
+    CircuitMeasurementMap(circuit_with_depth1_repeated_circuit_operation)
+
+
+def test_measurement_map_lot_of_measurements_initialisation(
+    circuit_with_repeated_measurements,
+):
+    CircuitMeasurementMap(circuit_with_repeated_measurements)
+
+
+def test_measurement_map_get_measurement_relative_offset_raises_on_positive_offset(
+    empty_circuit,
+):
+    mmap = CircuitMeasurementMap(empty_circuit)
+    qubit = cirq.GridQubit(0, 0)
+    with pytest.raises(TQECException):
+        mmap.get_measurement_relative_offset(1, qubit, 0)
+    with pytest.raises(TQECException):
+        mmap.get_measurement_relative_offset(1, qubit, 10)
+
+
+def test_measurement_map_get_measurement_relative_offset_raises_on_invalid_moment(
+    flat_circuit,
+):
+    mmap = CircuitMeasurementMap(flat_circuit)
+    qubit = cirq.GridQubit(0, 0)
+    with pytest.raises(TQECException):
+        mmap.get_measurement_relative_offset(-10, qubit, -1)
+    with pytest.raises(TQECException):
+        mmap.get_measurement_relative_offset(-1, qubit, -1)
+    with pytest.raises(TQECException):
+        mmap.get_measurement_relative_offset(3, qubit, -1)
+    with pytest.raises(TQECException):
+        mmap.get_measurement_relative_offset(10, qubit, -1)
+
+
+@pytest.mark.parametrize(
+    "circuit_fixture",
+    [
+        "flat_circuit",
+        "circuit_with_depth1_circuit_operation",
+        "circuit_with_depth2_circuit_operation",
+    ],
+)
+def test_measurement_map_get_measurement_relative_offset(circuit_fixture, request):
+    circuit = request.getfixturevalue(circuit_fixture)
+    mmap = CircuitMeasurementMap(circuit)
+    qubit = cirq.GridQubit(0, 0)
+    # The only measurement on qubit (0, 0) is at moment 2
+    assert mmap.get_measurement_relative_offset(0, qubit, -1) is None
+    assert mmap.get_measurement_relative_offset(1, qubit, -1) is None
+    assert mmap.get_measurement_relative_offset(2, qubit, -1) is not None
+    assert mmap.get_measurement_relative_offset(2, qubit, -2) is None
+
+
+def test_measurement_map_get_measurement_relative_offset_repeat(
+    circuit_with_depth1_repeated_circuit_operation,
+):
+    mmap = CircuitMeasurementMap(circuit_with_depth1_repeated_circuit_operation)
+    qubit = cirq.GridQubit(0, 0)
+    # The only measurement on qubit (0, 0) is at moment 11
+    for moment_index in range(11):
+        assert mmap.get_measurement_relative_offset(moment_index, qubit, -1) is None
+    assert mmap.get_measurement_relative_offset(11, qubit, -1) is not None
+    assert mmap.get_measurement_relative_offset(11, qubit, -2) is None
+
+
+def test_measurement_map_get_measurement_relative_offset_lot_of_measurements(
+    circuit_with_repeated_measurements,
+):
+    mmap = CircuitMeasurementMap(circuit_with_repeated_measurements)
+    qubit = cirq.GridQubit(0, 0)
+    # There are measurements from moment 0 to 9 included
+    for moment_index in range(10):
+        for backward_index in range(1, moment_index + 1):
+            assert (
+                mmap.get_measurement_relative_offset(
+                    moment_index, qubit, -backward_index
+                )
+                is not None
+            )
diff --git a/src/tqec/circuit/operations/operation_test.py b/src/tqec/circuit/operations/operation_test.py
new file mode 100644
index 00000000..02ed254c
--- /dev/null
+++ b/src/tqec/circuit/operations/operation_test.py
@@ -0,0 +1,124 @@
+import cirq
+import pytest
+
+from tqec.circuit.operations.operation import (
+    Detector,
+    Observable,
+    RelativeMeasurementData,
+    RelativeMeasurementsRecord,
+    ShiftCoords,
+    make_detector,
+    make_observable,
+    make_shift_coords,
+)
+from tqec.exceptions import TQECException
+
+_qubits_examples = [
+    cirq.GridQubit(0, 0),
+    cirq.GridQubit(2, 2),
+    cirq.GridQubit(-1, 1),
+    cirq.GridQubit(-10, -3),
+    cirq.GridQubit(-1, 0),
+]
+
+
+def test_empty_shift_coords():
+    with pytest.raises(
+        TQECException,
+        match="The number of shift coordinates should be between 1 and 16, but got 0.",
+    ):
+        make_shift_coords()
+
+
+def test_shift_coords():
+    sc_tagged = make_shift_coords(-1, 0, 18, 2**57)
+    assert isinstance(sc_tagged.untagged, ShiftCoords)
+    sc_untagged: ShiftCoords = sc_tagged.untagged
+    assert sc_untagged.shifts == (-1, 0, 18, 2**57)
+
+
+@pytest.mark.parametrize("origin", _qubits_examples)
+def test_empty_detector(origin):
+    detector_tagged = make_detector(origin, [], time_coordinate=0)
+    assert isinstance(detector_tagged.untagged, Detector)
+    detector_untagged: Detector = detector_tagged.untagged
+    assert detector_untagged.origin == origin
+    assert detector_untagged.coordinates == (origin.row, origin.col, 0)
+
+
+def test_detector_repeated_relative_measurement():
+    origin = cirq.GridQubit(0, 0)
+    relative_measurements: list[tuple[cirq.GridQubit, int]] = [
+        (cirq.GridQubit(0, 0), -1),
+        (cirq.GridQubit(0, 0), -1),
+    ]
+    with pytest.raises(TQECException):
+        # Duplicated relative measurements
+        make_detector(origin, relative_measurements, time_coordinate=0)
+
+
+def test_detector_with_relative_measurement():
+    origin = cirq.GridQubit(0, 0)
+    relative_measurements: list[tuple[cirq.GridQubit, int]] = [
+        (cirq.GridQubit(0, 0), -1),
+        (cirq.GridQubit(0, 0), -2),
+    ]
+    make_detector(origin, relative_measurements, time_coordinate=0)
+
+
+def test_detector_negative_time_coordinate():
+    origin = cirq.GridQubit(0, 0)
+    relative_measurements: list[tuple[cirq.GridQubit, int]] = [
+        (cirq.GridQubit(0, 0), -1),
+        (cirq.GridQubit(0, 0), -2),
+    ]
+    with pytest.raises(TQECException):
+        make_detector(origin, relative_measurements, time_coordinate=-1)
+
+
+@pytest.mark.parametrize("origin", _qubits_examples)
+def test_empty_observable(origin):
+    observable_tagged = make_observable(origin, [], observable_index=0)
+    assert isinstance(observable_tagged.untagged, Observable)
+    observable_untagged: Observable = observable_tagged.untagged
+    assert observable_untagged.origin == origin
+    assert observable_untagged.index == 0
+
+
+def test_observable_with_relative_measurement():
+    origin = cirq.GridQubit(0, 0)
+    relative_measurements: list[tuple[cirq.GridQubit, int]] = [
+        (cirq.GridQubit(0, 0), -1),
+        (cirq.GridQubit(0, 0), -2),
+    ]
+    make_observable(origin, relative_measurements, observable_index=0)
+
+
+def test_observable_negative_index():
+    origin = cirq.GridQubit(0, 0)
+    relative_measurements: list[tuple[cirq.GridQubit, int]] = [
+        (cirq.GridQubit(0, 0), -1),
+        (cirq.GridQubit(0, 0), -2),
+    ]
+    with pytest.raises(TQECException):
+        make_observable(origin, relative_measurements, observable_index=-1)
+
+
+def test_relative_measure_data_negative_index():
+    with pytest.raises(TQECException):
+        RelativeMeasurementData(cirq.GridQubit(0, 0), 0)
+    with pytest.raises(TQECException):
+        RelativeMeasurementData(cirq.GridQubit(0, 0), 1)
+
+
+def test_relative_measurement_record_duplicated_measurement_data():
+    origin = cirq.GridQubit(0, 0)
+    with pytest.raises(TQECException):
+        RelativeMeasurementsRecord(
+            origin,
+            [
+                RelativeMeasurementData(origin, -1),
+                RelativeMeasurementData(origin, -2),
+                RelativeMeasurementData(origin, -1),
+            ],
+        )
diff --git a/src/tqec/plaquette/qubit_test.py b/src/tqec/plaquette/qubit_test.py
new file mode 100644
index 00000000..7380839e
--- /dev/null
+++ b/src/tqec/plaquette/qubit_test.py
@@ -0,0 +1,21 @@
+import cirq
+
+from tqec.plaquette.qubit import PlaquetteQubit
+from tqec.position import Position
+
+
+def test_creation():
+    PlaquetteQubit(Position(0, 0))
+    PlaquetteQubit(Position(100000, -2398467235))
+    PlaquetteQubit(Position(-234897659345, 349578))
+
+
+def test_to_grid_qubit_origin():
+    pq = PlaquetteQubit(Position(0, 0))
+    assert pq.to_grid_qubit() == cirq.GridQubit(0, 0)
+
+
+def test_to_grid_qubit_non_origin():
+    row, col = 238957462345, -945678
+    pq = PlaquetteQubit(Position(col, row))
+    assert pq.to_grid_qubit() == cirq.GridQubit(row, col)
diff --git a/src/tqec/templates/atomic/rectangle_test.py b/src/tqec/templates/atomic/rectangle_test.py
index f4c1acfc..15235ce9 100644
--- a/src/tqec/templates/atomic/rectangle_test.py
+++ b/src/tqec/templates/atomic/rectangle_test.py
@@ -81,6 +81,16 @@ def test_raw_rectangle_init():
     RawRectangleTemplate([[0]])
 
 
+def test_raw_rectangle_empty_init():
+    with pytest.raises(TQECException):
+        RawRectangleTemplate([[]])
+
+
+def test_raw_rectangle_wrongly_sized_init():
+    with pytest.raises(TQECException):
+        RawRectangleTemplate([[0, 1], [1]])
+
+
 def test_raw_rectangle_larger_init():
     RawRectangleTemplate(
         [
diff --git a/src/tqec/templates/base_test.py b/src/tqec/templates/base_test.py
new file mode 100644
index 00000000..35d70f5a
--- /dev/null
+++ b/src/tqec/templates/base_test.py
@@ -0,0 +1,31 @@
+import pytest
+
+from tqec.exceptions import TQECException
+from tqec.templates.atomic.square import AlternatingSquareTemplate
+from tqec.templates.base import TemplateWithIndices
+from tqec.templates.scale import Dimension, LinearFunction
+
+
+@pytest.fixture
+def square_template():
+    return AlternatingSquareTemplate(Dimension(2, LinearFunction(2)))
+
+
+def test_template_with_indices_creation(square_template):
+    twi = TemplateWithIndices(square_template, [1, 2])
+    assert twi.indices == [1, 2]
+    assert twi.template == square_template
+
+
+def test_template_with_indices_creation_wrong_number_of_indices(square_template):
+    with pytest.raises(TQECException):
+        TemplateWithIndices(square_template, [])
+    with pytest.raises(TQECException):
+        TemplateWithIndices(square_template, [1])
+    with pytest.raises(TQECException):
+        TemplateWithIndices(square_template, [2, 4, 1])
+
+
+def test_template_with_negative_indices_creation(square_template):
+    with pytest.raises(TQECException):
+        TemplateWithIndices(square_template, [-1, 0])
diff --git a/src/tqec/templates/composed_test.py b/src/tqec/templates/composed_test.py
new file mode 100644
index 00000000..a8f3f57f
--- /dev/null
+++ b/src/tqec/templates/composed_test.py
@@ -0,0 +1,36 @@
+import pytest
+
+from tqec.templates.atomic import AlternatingSquareTemplate
+from tqec.templates.atomic.rectangle import (
+    AlternatingRectangleTemplate,
+    RawRectangleTemplate,
+)
+from tqec.templates.base import TemplateWithIndices
+from tqec.templates.composed import ComposedTemplate
+from tqec.templates.scale import Dimension, FixedDimension, LinearFunction
+
+_DIMENSIONS = [
+    Dimension(2, LinearFunction(2, 0)),
+    Dimension(2, LinearFunction(4, 0)),
+    FixedDimension(1),
+]
+_TEMPLATES_AND_INDICES = [
+    (AlternatingSquareTemplate(_DIMENSIONS[0]), [1, 2]),
+    (AlternatingRectangleTemplate(_DIMENSIONS[0], _DIMENSIONS[1]), [198345, 21]),
+    (AlternatingRectangleTemplate(_DIMENSIONS[0], _DIMENSIONS[2]), [0, 68]),
+    (RawRectangleTemplate([[0]]), [1]),
+]
+
+
+def test_empty():
+    template = ComposedTemplate([])
+    assert template.expected_plaquettes_number == 0
+    assert template.shape.to_numpy_shape() == (0, 0)
+
+
+@pytest.mark.parametrize("atomic_template_and_indices", _TEMPLATES_AND_INDICES)
+def test_one_template(atomic_template_and_indices):
+    atomic_template, indices = atomic_template_and_indices
+    template = ComposedTemplate([TemplateWithIndices(atomic_template, indices)])
+    assert template.shape == atomic_template.shape
+    assert template.expected_plaquettes_number == max(indices)
diff --git a/src/tqec/templates/shifted_test.py b/src/tqec/templates/shifted_test.py
new file mode 100644
index 00000000..d5057145
--- /dev/null
+++ b/src/tqec/templates/shifted_test.py
@@ -0,0 +1,67 @@
+import numpy
+import pytest
+
+from tqec.position import Shape2D
+from tqec.templates.atomic.square import AlternatingSquareTemplate
+from tqec.templates.base import Template
+from tqec.templates.scale import Dimension, FixedDimension, LinearFunction
+from tqec.templates.shifted import ScalableOffset, ShiftedTemplate
+
+
+@pytest.fixture
+def default_template():
+    return AlternatingSquareTemplate(Dimension(2, LinearFunction(2)))
+
+
+@pytest.fixture
+def zero_offset():
+    return ScalableOffset(FixedDimension(0), FixedDimension(0))
+
+
+@pytest.fixture
+def scalable_offset():
+    return ScalableOffset(
+        Dimension(2, LinearFunction(2)), Dimension(2, LinearFunction(2))
+    )
+
+
+def test_scalable_offset_creation():
+    ScalableOffset(FixedDimension(2), Dimension(2, LinearFunction(2)))
+
+
+def test_scalable_offset_scaling():
+    offset = ScalableOffset(
+        Dimension(2, LinearFunction(3)), Dimension(2, LinearFunction(2))
+    )
+    assert offset.x.value == 6
+    assert offset.y.value == 4
+    offset.scale_to(4)
+    assert offset.x.value == 12
+    assert offset.y.value == 8
+
+
+def test_zero_shifted_template(default_template: Template, zero_offset: ScalableOffset):
+    template = ShiftedTemplate(default_template, zero_offset)
+    assert (
+        template.expected_plaquettes_number
+        == default_template.expected_plaquettes_number
+    )
+    indices = list(range(1, template.expected_plaquettes_number + 1))
+    numpy.testing.assert_equal(
+        template.instantiate(indices), default_template.instantiate(indices)
+    )
+
+
+def test_shifted_template(default_template: Template, scalable_offset: ScalableOffset):
+    template = ShiftedTemplate(default_template, scalable_offset)
+    template.scale_to(1)
+    assert (
+        template.expected_plaquettes_number
+        == default_template.expected_plaquettes_number
+    )
+    indices = list(range(1, template.expected_plaquettes_number + 1))
+    assert template.shape == Shape2D(4, 4)
+    numpy.testing.assert_equal(
+        template.instantiate(indices),
+        [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 1, 2], [0, 0, 2, 1]],
+    )
diff --git a/src/tqec/templates/stack_test.py b/src/tqec/templates/stack_test.py
new file mode 100644
index 00000000..2c7c091e
--- /dev/null
+++ b/src/tqec/templates/stack_test.py
@@ -0,0 +1,153 @@
+import numpy.testing
+import pytest
+
+from tqec.position import Shape2D
+from tqec.templates.atomic.square import AlternatingSquareTemplate
+from tqec.templates.base import Template
+from tqec.templates.scale import Dimension, FixedDimension, LinearFunction
+from tqec.templates.shifted import ScalableOffset, ShiftedTemplate
+from tqec.templates.stack import StackedTemplate
+
+
+@pytest.fixture
+def small_alternating_template():
+    return AlternatingSquareTemplate(Dimension(1, LinearFunction(2)))
+
+
+@pytest.fixture
+def shifted_small_alternating_template():
+    return ShiftedTemplate(
+        AlternatingSquareTemplate(Dimension(1, LinearFunction(2))),
+        ScalableOffset(FixedDimension(2), FixedDimension(2)),
+    )
+
+
+@pytest.fixture
+def larger_alternating_template():
+    return AlternatingSquareTemplate(Dimension(1, LinearFunction(4)))
+
+
+def test_stack_template_over_itself(small_alternating_template: Template):
+    stack = StackedTemplate()
+    stack.push_template_on_top(small_alternating_template)
+    stack.push_template_on_top(small_alternating_template)
+    instantiation = stack.instantiate([1, 2, 3, 4])
+
+    numpy.testing.assert_equal(
+        instantiation, small_alternating_template.instantiate([3, 4])
+    )
+
+
+def test_stack_small_alternating_on_top_of_larger_alternating(
+    small_alternating_template: Template, larger_alternating_template: Template
+):
+    stack = StackedTemplate()
+    stack.push_template_on_top(larger_alternating_template)
+    stack.push_template_on_top(small_alternating_template)
+    instantiation = stack.instantiate([1, 2, 3, 4])
+
+    numpy.testing.assert_equal(
+        instantiation, [[3, 4, 1, 2], [4, 3, 2, 1], [1, 2, 1, 2], [2, 1, 2, 1]]
+    )
+
+
+def test_stack_small_alternating_on_top_of_larger_alternating_expected_plaquette_number(
+    small_alternating_template: Template, larger_alternating_template: Template
+):
+    stack = StackedTemplate()
+    assert stack.expected_plaquettes_number == 0
+    stack.push_template_on_top(larger_alternating_template)
+    assert (
+        stack.expected_plaquettes_number
+        == larger_alternating_template.expected_plaquettes_number
+    )
+    stack.push_template_on_top(small_alternating_template)
+    assert (
+        stack.expected_plaquettes_number
+        == larger_alternating_template.expected_plaquettes_number
+        + small_alternating_template.expected_plaquettes_number
+    )
+    stack.pop_template_from_top()
+    assert (
+        stack.expected_plaquettes_number
+        == larger_alternating_template.expected_plaquettes_number
+    )
+
+
+def test_stack_small_alternating_with_offset_on_top_of_larger_alternating(
+    shifted_small_alternating_template: Template, larger_alternating_template: Template
+):
+    stack = StackedTemplate()
+    stack.push_template_on_top(larger_alternating_template)
+    stack.push_template_on_top(shifted_small_alternating_template)
+    instantiation = stack.instantiate([1, 2, 3, 4])
+
+    numpy.testing.assert_equal(
+        instantiation,
+        [[1, 2, 1, 2], [2, 1, 2, 1], [1, 2, 3, 4], [2, 1, 4, 3]],
+    )
+
+
+def test_stack_small_alternating_below_of_larger_alternating(
+    small_alternating_template: Template, larger_alternating_template: Template
+):
+    stack = StackedTemplate()
+    stack.push_template_on_top(small_alternating_template)
+    stack.push_template_on_top(larger_alternating_template)
+    instantiation = stack.instantiate([1, 2, 3, 4])
+
+    numpy.testing.assert_equal(
+        instantiation,
+        larger_alternating_template.instantiate([3, 4]),
+    )
+
+
+def test_stack_template_pop(
+    small_alternating_template: Template, larger_alternating_template: Template
+):
+    stack = StackedTemplate()
+    stack.push_template_on_top(larger_alternating_template)
+    stack.push_template_on_top(small_alternating_template)
+    stack.pop_template_from_top()
+    instantiation = stack.instantiate([1, 2])
+
+    numpy.testing.assert_equal(
+        instantiation, larger_alternating_template.instantiate([1, 2])
+    )
+
+
+def test_stack_shape(
+    small_alternating_template: Template, larger_alternating_template: Template
+):
+    stack = StackedTemplate()
+    stack.push_template_on_top(small_alternating_template)
+    assert stack.shape == Shape2D(2, 2)
+    stack.push_template_on_top(larger_alternating_template)
+    assert stack.shape == Shape2D(4, 4)
+    stack.scale_to(2)
+    assert stack.shape == Shape2D(8, 8)
+    stack.pop_template_from_top()
+    assert stack.shape == Shape2D(4, 4)
+
+
+def test_stack_scale_to(
+    small_alternating_template: Template, larger_alternating_template: Template
+):
+    stack = StackedTemplate()
+    stack.push_template_on_top(larger_alternating_template)
+    stack.push_template_on_top(small_alternating_template)
+    stack.scale_to(2)
+    instantiation = stack.instantiate([1, 2, 3, 4])
+    numpy.testing.assert_equal(
+        instantiation,
+        [
+            [3, 4, 3, 4, 1, 2, 1, 2],
+            [4, 3, 4, 3, 2, 1, 2, 1],
+            [3, 4, 3, 4, 1, 2, 1, 2],
+            [4, 3, 4, 3, 2, 1, 2, 1],
+            [1, 2, 1, 2, 1, 2, 1, 2],
+            [2, 1, 2, 1, 2, 1, 2, 1],
+            [1, 2, 1, 2, 1, 2, 1, 2],
+            [2, 1, 2, 1, 2, 1, 2, 1],
+        ],
+    )