Repository: QCHackers/tqec
Pull request #343 (2024-10-08 16:42:15)

Avoid storing `k` in `Template` instances
Managing the scaling parameter `k` in `Template` instances is not needed at all, in fact `k` can just be provided to the functions that need it.

This is less error-prone as we do not have to manage scaling parameters that should be kept in-sync.

github-actions[bot]: A preview of  is uploaded and can be seen here:

✨ https://qchackers.github.io/tqec/pull/343/ ✨

Changes may take a few minutes to propagate.
<!-- Sticky Pull Request Commentdocumentation-preview -->
github-actions[bot]: ![Code Coverage](https://img.shields.io/badge/Code%20Coverage-81%25-success?style=flat)

Package | Line Rate | Complexity | Health
-------- | --------- | ---------- | ------
src.tqec | 97% | 0 | ✔
src.tqec.circuit | 97% | 0 | ✔
src.tqec.circuit.detectors | 94% | 0 | ✔
src.tqec.circuit.detectors.match_utils | 90% | 0 | ✔
src.tqec.compile | 34% | 0 | ❌
src.tqec.computation | 74% | 0 | ➖
src.tqec.computation.block_graph | 81% | 0 | ✔
src.tqec.noise_models | 91% | 0 | ✔
src.tqec.plaquette | 86% | 0 | ✔
src.tqec.plaquette.library | 97% | 0 | ✔
src.tqec.plaquette.library.utils | 92% | 0 | ✔
src.tqec.simulation | 0% | 0 | ❌
src.tqec.simulation.plotting | 0% | 0 | ❌
src.tqec.templates | 78% | 0 | ✔
**Summary** | **81%** (3163 / 3915) | **0** | ✔

<!-- Sticky Pull Request Commentcode-coverage -->

Patch:
diff --git a/src/tqec/circuit/generation.py b/src/tqec/circuit/generation.py
index a8e254c4..0dba3496 100644
--- a/src/tqec/circuit/generation.py
+++ b/src/tqec/circuit/generation.py
@@ -32,7 +32,9 @@ from tqec.position import Displacement
 from tqec.templates.base import Template
 
 
-def generate_circuit(template: Template, plaquettes: Plaquettes) -> ScheduledCircuit:
+def generate_circuit(
+    template: Template, k: int, plaquettes: Plaquettes
+) -> ScheduledCircuit:
     """Generate a quantum circuit from a template and its plaquettes.
 
     This is one of the core methods of the `tqec` package. It generates a
@@ -53,6 +55,7 @@ def generate_circuit(template: Template, plaquettes: Plaquettes) -> ScheduledCir
     Args:
         template: spatial description of the quantum error correction experiment
             we want to implement.
+        k: scaling parameter used to instantiate the provided `template`.
         plaquettes: description of the computation that should happen at
             different time-slices of the quantum error correction experiment (or
             at least part of it).
@@ -77,7 +80,7 @@ def generate_circuit(template: Template, plaquettes: Plaquettes) -> ScheduledCir
     # instantiate the template with the appropriate plaquette indices.
     # Index 0 is "no plaquette" by convention and should not be included here.
     _indices = list(range(1, template.expected_plaquettes_number + 1))
-    template_plaquettes = template.instantiate(_indices)
+    template_plaquettes = template.instantiate(k, _indices)
     increments = template.get_increments()
 
     return generate_circuit_from_instantiation(
diff --git a/src/tqec/circuit/observable_qubits.py b/src/tqec/circuit/observable_qubits.py
index af875067..7a529299 100644
--- a/src/tqec/circuit/observable_qubits.py
+++ b/src/tqec/circuit/observable_qubits.py
@@ -12,6 +12,7 @@ from tqec.templates.enums import TemplateOrientation
 
 def observable_qubits_from_template(
     template: Template,
+    k: int,
     plaquettes: Sequence[Plaquette] | Mapping[int, Plaquette],
     orientation: TemplateOrientation = TemplateOrientation.HORIZONTAL,
 ) -> Sequence[tuple[GridQubit, int]]:
@@ -20,6 +21,7 @@ def observable_qubits_from_template(
 
     Args:
         template: The template to get the default observable qubits from.
+        k: scaling parameter used to instantiate the provided template.
         plaquettes: The plaquettes to use to get the accurate positions of the observable qubits.
         orientation: Whether to get the observable qubits from
             the horizontal or vertical midline. Defaults to horizontal.
@@ -47,11 +49,11 @@ def observable_qubits_from_template(
         plaquettes = {i + 1: plaquette for i, plaquette in enumerate(plaquettes)}
 
     _indices = list(range(1, len(plaquettes) + 1))
-    template_plaquettes = template.instantiate(_indices)
+    template_plaquettes = template.instantiate(k, _indices)
     increments = template.get_increments()
 
     try:
-        midline_indices = template.get_midline_plaquettes(orientation)
+        midline_indices = template.get_midline_plaquettes(k, orientation)
     except TQECException as e:
         raise TQECException(
             "The template does not have a midline. "
diff --git a/src/tqec/compile/block.py b/src/tqec/compile/block.py
index 29ff18bd..a25379e8 100644
--- a/src/tqec/compile/block.py
+++ b/src/tqec/compile/block.py
@@ -118,19 +118,15 @@ class BlockLayout:
             merged_layers.append(plaquettes)
         return merged_layers
 
-    def scale_to(self, k: int) -> None:
-        """Scales all the blocks in the layout to the given factor."""
-        self._template.scale_to(k)
-
     @property
     def num_layers(self) -> int:
         return len(self._layers)
 
-    def instantiate_layer(self, layer_index: int) -> ScheduledCircuit:
+    def instantiate_layer(self, layer_index: int, k: int) -> ScheduledCircuit:
         """Instantiates the specified layer into a `ScheduledCircuit`.
 
         Note that this method does not shift the circuits based on the
         layout template. And the circuits are not repeated based on the
         repetitions in the layer.
         """
-        return generate_circuit(self._template, self._layers[layer_index])
+        return generate_circuit(self._template, k, self._layers[layer_index])
diff --git a/src/tqec/compile/compile.py b/src/tqec/compile/compile.py
index fb99471c..b1808619 100644
--- a/src/tqec/compile/compile.py
+++ b/src/tqec/compile/compile.py
@@ -12,12 +12,12 @@ from tqec.compile.substitute import (
     SubstitutionKey,
     SubstitutionRule,
 )
+from tqec.computation.block_graph import AbstractObservable, BlockGraph
 from tqec.exceptions import TQECException
 from tqec.noise_models import NoiseModel
 from tqec.plaquette.plaquette import RepeatedPlaquettes
 from tqec.position import Direction3D, Displacement, Position3D
 from tqec.scale import round_or_fail
-from tqec.computation.block_graph import AbstractObservable, BlockGraph
 
 
 @dataclass
@@ -45,11 +45,6 @@ class CompiledGraph:
         if len(self.observables) == 0:
             raise TQECException("The compiled graph includes no observable.")
 
-    def scale_to(self, k: int) -> None:
-        """Scale the compiled graph to the given scale `k`."""
-        for layout in self.layout_slices:
-            layout.scale_to(k)
-
     def generate_stim_circuit(
         self,
         k: int,
@@ -63,28 +58,28 @@ class CompiledGraph:
 
             A compiled stim circuit.
         """
-        self.scale_to(k)
         # assemble circuits time slice by time slice, layer by layer.
         circuits: list[list[ScheduledCircuit]] = []
         for layout in self.layout_slices:
             circuits.append(
-                [layout.instantiate_layer(i) for i in range(layout.num_layers)]
+                [layout.instantiate_layer(i, k) for i in range(layout.num_layers)]
             )
         # construct observables
         inplace_add_observables(
             circuits,
             [layout.template for layout in self.layout_slices],
             self.observables,
+            k,
         )
         # shift and merge circuits
-        circuit = self._shift_and_merge_circuits(circuits)
+        circuit = self._shift_and_merge_circuits(circuits, k)
         # apply noise models
         if noise_model is not None:
             circuit = noise_model.noisy_circuit(circuit)
         return circuit
 
     def _shift_and_merge_circuits(
-        self, circuits: list[list[ScheduledCircuit]]
+        self, circuits: list[list[ScheduledCircuit]], k: int
     ) -> stim.Circuit:
         """
 
@@ -99,7 +94,7 @@ class CompiledGraph:
         """
         # First, shift all the circuits to make sure that they are each applied
         # on the correct qubits
-        self._shift_all_circuits_inplace(circuits)
+        self._shift_all_circuits_inplace(circuits, k)
         # Then, get a global qubit index map to remap the circuit indices on the
         # fly.
         needed_qubits = frozenset.union(
@@ -125,7 +120,7 @@ class CompiledGraph:
                 layer = layout.layers[i]
                 if isinstance(layer, RepeatedPlaquettes):
                     final_circuit += circuits[t][i].get_repeated_circuit(
-                        round_or_fail(layer.repetitions(layout.template.k)),
+                        round_or_fail(layer.repetitions(k)),
                         include_qubit_coords=False,
                     )
                 else:
@@ -138,12 +133,12 @@ class CompiledGraph:
         return final_circuit[:-1]
 
     def _shift_all_circuits_inplace(
-        self, circuits: list[list[ScheduledCircuit]]
+        self, circuits: list[list[ScheduledCircuit]], k: int
     ) -> None:
         for t, layout in enumerate(self.layout_slices):
             # We need to shift the circuit based on the shift of the layout template.
             origin_shift = layout.template.origin_shift
-            element_shape = layout.template.element_shape
+            element_shape = layout.template.element_shape(k)
             increment = layout.template.get_increments()
             for i in range(layout.num_layers):
                 qubit_map = {
diff --git a/src/tqec/compile/observables.py b/src/tqec/compile/observables.py
index 4a39725d..10e04fcf 100644
--- a/src/tqec/compile/observables.py
+++ b/src/tqec/compile/observables.py
@@ -2,10 +2,10 @@ import stim
 
 from tqec.circuit.qubit import GridQubit
 from tqec.circuit.schedule import ScheduledCircuit
+from tqec.computation.block_graph import AbstractObservable, Cube, Pipe
 from tqec.exceptions import TQECException
 from tqec.position import Direction3D, Displacement, Shape2D
 from tqec.scale import round_or_fail
-from tqec.computation.block_graph import AbstractObservable, Cube, Pipe
 from tqec.templates.layout import LayoutTemplate
 
 
@@ -131,6 +131,7 @@ def inplace_add_observables(
     circuits: list[list[ScheduledCircuit]],
     template_slices: list[LayoutTemplate],
     abstract_observables: list[AbstractObservable],
+    k: int,
 ) -> None:
     """Inplace add the observable components to the circuits.
 
@@ -143,7 +144,7 @@ def inplace_add_observables(
             z = pipe.u.position.z
             template = template_slices[z]
             stabilizer_qubits = get_stabilizer_region_qubits_for_pipe(
-                pipe, template.element_shape, template.get_increments()
+                pipe, template.element_shape(k), template.get_increments()
             )
             measurement_records = _get_measurement_records_from_scheduled_circuit(
                 circuits[z][0]
@@ -162,14 +163,16 @@ def inplace_add_observables(
                 z = cube_or_pipe.position.z
                 template = template_slices[z]
                 qubits = get_midline_qubits_for_cube(
-                    cube_or_pipe, template.element_shape, template.get_increments()
+                    cube_or_pipe, template.element_shape(k), template.get_increments()
                 )
             else:
                 z = cube_or_pipe.u.position.z
                 template = template_slices[z]
                 qubits = [
                     get_center_qubit_at_horizontal_pipe(
-                        cube_or_pipe, template.element_shape, template.get_increments()
+                        cube_or_pipe,
+                        template.element_shape(k),
+                        template.get_increments(),
                     )
                 ]
             measurement_records = _get_measurement_records_from_scheduled_circuit(
diff --git a/src/tqec/templates/base.py b/src/tqec/templates/base.py
index 1e55f4ee..4d3ba466 100644
--- a/src/tqec/templates/base.py
+++ b/src/tqec/templates/base.py
@@ -16,32 +16,27 @@ from tqec.templates.subtemplates import (
 
 
 class Template(ABC):
-    def __init__(
-        self, k: int = 2, default_increments: Displacement | None = None
-    ) -> None:
+    def __init__(self, default_increments: Displacement | None = None) -> None:
         """Base class for all the templates.
 
         This class is the base of all templates and provide the necessary interface
         that all templates should implement to be usable by the library.
 
         Args:
-            k: initial value for the scaling parameter.
             default_increments: default increments between two plaquettes. Defaults
                 to `Displacement(2, 2)` when `None`
         """
         super().__init__()
-        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
     def instantiate(
-        self, plaquette_indices: Sequence[int] | None = None
+        self, k: int, plaquette_indices: Sequence[int] | None = None
     ) -> npt.NDArray[numpy.int_]:
         """Generate the numpy array representing the template.
 
         Args:
+            k: scaling parameter used to instantiate the template.
             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`.
@@ -51,29 +46,10 @@ class Template(ABC):
             the underlying shape of the template.
         """
 
-    def scale_to(self, k: int) -> None:
-        """Scales self to the given scale k.
-
-        Note that this function scales the template instance INLINE.
-
-        Args:
-            k: the new scale of the template.
-        """
-        if k < 0:
-            raise TQECException(f"Cannot have a negative scaling parameter. Got {k}.")
-        self._k = k
-
-    @property
-    def k(self) -> int:
-        return self._k
-
-    @property
-    def shape(self) -> Shape2D:
+    def shape(self, k: int) -> Shape2D:
         """Returns the current template shape."""
         sshape = self.scalable_shape
-        return Shape2D(
-            round_or_fail(sshape.x(self._k)), round_or_fail(sshape.y(self._k))
-        )
+        return Shape2D(round_or_fail(sshape.x(k)), round_or_fail(sshape.y(k)))
 
     @property
     @abstractmethod
@@ -81,7 +57,7 @@ class Template(ABC):
         """Returns a scalable version of the template shape."""
 
     def get_midline_plaquettes(
-        self, orientation: TemplateOrientation = TemplateOrientation.HORIZONTAL
+        self, k: int, orientation: TemplateOrientation = TemplateOrientation.HORIZONTAL
     ) -> list[tuple[int, int]]:
         """Returns the default observable qubits for the template.
 
@@ -99,7 +75,8 @@ class Template(ABC):
         Raises:
             TQECException: If the midline is not uniquely defined.
         """
-        midline_shape, iteration_shape = self.shape.x, self.shape.y
+        shape = self.shape(k)
+        midline_shape, iteration_shape = shape.x, shape.y
         if midline_shape % 2 == 1:
             raise TQECException(
                 "Midline is not defined for odd "
@@ -141,7 +118,7 @@ class Template(ABC):
         """
 
     def get_spatially_distinct_subtemplates(
-        self, manhattan_radius: int = 1, avoid_zero_plaquettes: bool = True
+        self, k: 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.
@@ -154,6 +131,7 @@ class Template(ABC):
             algorithm (or hard-coded values) to speed things up.
 
         Args:
+            k: scaling parameter used to instantiate the template.
             manhattan_radius: radius of the considered ball using the Manhattan
                 distance. Only squares with sides of `2*manhattan_radius+1`
                 plaquettes will be considered.
@@ -165,16 +143,17 @@ class Template(ABC):
             a representation of all the sub-templates found.
         """
         return get_spatially_distinct_subtemplates(
-            self.instantiate(), manhattan_radius, avoid_zero_plaquettes
+            self.instantiate(k), manhattan_radius, avoid_zero_plaquettes
         )
 
 
 class RectangularTemplate(Template):
     @override
     def get_midline_plaquettes(
-        self, orientation: TemplateOrientation = TemplateOrientation.HORIZONTAL
+        self, k: int, orientation: TemplateOrientation = TemplateOrientation.HORIZONTAL
     ) -> list[tuple[int, int]]:
-        midline_shape, iteration_shape = self.shape.x, self.shape.y
+        shape = self.shape(k)
+        midline_shape, iteration_shape = shape.x, shape.y
         if midline_shape % 2 == 1:
             raise TQECException(
                 "Midline is not defined for odd "
diff --git a/src/tqec/templates/display.py b/src/tqec/templates/display.py
index 09805876..2d5650f1 100644
--- a/src/tqec/templates/display.py
+++ b/src/tqec/templates/display.py
@@ -9,7 +9,7 @@ from tqec.templates.base import Template
 
 
 def display_template(
-    template: Template, plaquette_indices: ty.Sequence[int] | None = None
+    template: Template, k: int, plaquette_indices: ty.Sequence[int] | None = None
 ) -> None:
     """Display a template instance with ASCII output.
 
@@ -21,7 +21,7 @@ def display_template(
     """
     if plaquette_indices is None:
         plaquette_indices = tuple(range(1, template.expected_plaquettes_number + 1))
-    display_template_from_instantiation(template.instantiate(plaquette_indices))
+    display_template_from_instantiation(template.instantiate(k, plaquette_indices))
 
 
 def display_template_from_instantiation(instantiation: npt.NDArray[numpy.int_]) -> None:
diff --git a/src/tqec/templates/layout.py b/src/tqec/templates/layout.py
index 5b8f545d..a575f504 100644
--- a/src/tqec/templates/layout.py
+++ b/src/tqec/templates/layout.py
@@ -16,7 +16,6 @@ class LayoutTemplate(RectangularTemplate):
     def __init__(
         self,
         element_layout: dict[Position2D, RectangularTemplate],
-        k: int = 2,
         default_increments: Displacement | None = None,
     ) -> None:
         """A template representing a layout of other templates.
@@ -34,11 +33,10 @@ class LayoutTemplate(RectangularTemplate):
         Args:
             element_layout: a dictionary with the position of the element
                 templates in the layout as keys and the templates as values.
-            k: initial value for the scaling parameter.
             default_increments: default increments between two plaquettes. Defaults
                 to `Displacement(2, 2)` when `None`.
         """
-        super().__init__(k, default_increments)
+        super().__init__(default_increments)
         if not element_layout:
             raise TQECException("Cannot create a layout with an empty template map.")
 
@@ -62,7 +60,6 @@ class LayoutTemplate(RectangularTemplate):
         self._ny = max_y - min_y + 1
 
         self._layout = deepcopy(element_layout)
-        self.scale_to(k)
 
     def get_indices_map_for_instantiation(
         self,
@@ -84,12 +81,6 @@ class LayoutTemplate(RectangularTemplate):
     def origin_shift(self) -> Displacement:
         return self._origin_shift
 
-    @override
-    def scale_to(self, k: int) -> None:
-        super().scale_to(k)
-        for template in self._layout.values():
-            template.scale_to(k)
-
     @property
     @override
     def scalable_shape(self) -> Scalable2D:
@@ -99,10 +90,9 @@ class LayoutTemplate(RectangularTemplate):
             self._ny * self._element_scalable_shape.y,
         )
 
-    @property
-    def element_shape(self) -> Shape2D:
+    def element_shape(self, k: int) -> Shape2D:
         """Return the uniform shape of the element templates."""
-        return self._element_scalable_shape.to_shape_2d(self._k)
+        return self._element_scalable_shape.to_shape_2d(k)
 
     @property
     @override
@@ -132,11 +122,12 @@ class LayoutTemplate(RectangularTemplate):
 
     @override
     def instantiate(
-        self, plaquette_indices: Sequence[int] | None = None
+        self, k: int, plaquette_indices: Sequence[int] | None = None
     ) -> npt.NDArray[numpy.int_]:
         """Generate the numpy array representing the template.
 
         Args:
+            k: scaling parameter used to instantiate the template.
             plaquette_indices: the plaquette indices that will be forwarded to
                 the underlying Template instance's instantiate method. Defaults
                 to `range(1, self.expected_plaquettes_number + 1)` if `None`.
@@ -147,14 +138,14 @@ class LayoutTemplate(RectangularTemplate):
         """
         indices_map = self.get_indices_map_for_instantiation(plaquette_indices)
 
-        element_shape = self._element_scalable_shape.to_numpy_shape(self.k)
-        ret = numpy.zeros(self.shape.to_numpy_shape(), dtype=numpy.int_)
+        element_shape = self._element_scalable_shape.to_numpy_shape(k)
+        ret = numpy.zeros(self.shape(k).to_numpy_shape(), dtype=numpy.int_)
         for pos, element in self._layout.items():
             imap = indices_map[pos]
             indices = [
                 imap[i] for i in range(1, element.expected_plaquettes_number + 1)
             ]
-            element_instantiation = element.instantiate(indices)
+            element_instantiation = element.instantiate(k, indices)
             shifted_pos = Position2D(
                 pos.x - self.origin_shift.x, pos.y - self.origin_shift.y
             )
diff --git a/src/tqec/templates/qubit.py b/src/tqec/templates/qubit.py
index 30bf551e..a3a6d61c 100644
--- a/src/tqec/templates/qubit.py
+++ b/src/tqec/templates/qubit.py
@@ -26,12 +26,12 @@ class QubitTemplate(RectangularTemplate):
 
     @override
     def instantiate(
-        self, plaquette_indices: Sequence[int] | None = None
+        self, k: int, plaquette_indices: Sequence[int] | None = None
     ) -> npt.NDArray[numpy.int_]:
         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_)
+        ret = numpy.zeros(self.shape(k).to_numpy_shape(), dtype=numpy.int_)
 
         # The four corners
         ret[0, 0] = plaquette_indices[0]
@@ -108,12 +108,12 @@ class QubitVerticalBorders(RectangularTemplate):
 
     @override
     def instantiate(
-        self, plaquette_indices: Sequence[int] | None = None
+        self, k: int, plaquette_indices: Sequence[int] | None = None
     ) -> npt.NDArray[numpy.int_]:
         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_)
+        ret = numpy.zeros(self.shape(k).to_numpy_shape(), dtype=numpy.int_)
 
         # The four corners
         ret[0, 0] = plaquette_indices[0]
@@ -176,12 +176,12 @@ class QubitHorizontalBorders(RectangularTemplate):
 
     @override
     def instantiate(
-        self, plaquette_indices: Sequence[int] | None = None
+        self, k: int, plaquette_indices: Sequence[int] | None = None
     ) -> npt.NDArray[numpy.int_]:
         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_)
+        ret = numpy.zeros(self.shape(k).to_numpy_shape(), dtype=numpy.int_)
 
         # The four corners
         ret[0, 0] = plaquette_indices[0]
@@ -256,12 +256,12 @@ class Qubit4WayJunctionTemplate(RectangularTemplate):
 
     @override
     def instantiate(
-        self, plaquette_indices: Sequence[int] | None = None
+        self, k: int, plaquette_indices: Sequence[int] | None = None
     ) -> npt.NDArray[numpy.int_]:
         if plaquette_indices is None:
             plaquette_indices = list(range(1, self.expected_plaquettes_number + 1))
 
-        if self.k == 1:
+        if k == 1:
             warnings.warn(
                 "Instantiating Qubit4WayJunctionTemplate with k=1. The "
                 "instantiation array returned will not have any plaquette indexed "
@@ -269,8 +269,9 @@ class Qubit4WayJunctionTemplate(RectangularTemplate):
                 TQECWarning,
             )
 
-        ret = numpy.zeros(self.shape.to_numpy_shape(), dtype=numpy.int_)
-        size = self.shape.x
+        shape = self.shape(k)
+        ret = numpy.zeros(shape.to_numpy_shape(), dtype=numpy.int_)
+        size = shape.x
 
         # The four corners
         ret[0, 0] = plaquette_indices[0]


Test patch:
diff --git a/src/tqec/circuit/generation_test.py b/src/tqec/circuit/generation_test.py
index 20546d48..43640b00 100644
--- a/src/tqec/circuit/generation_test.py
+++ b/src/tqec/circuit/generation_test.py
@@ -47,7 +47,7 @@ def _expected_circuit() -> stim.Circuit:
 def test_generate_circuit_dict(
     plaquette: Plaquette, one_by_one_template: Template
 ) -> None:
-    circuit = generate_circuit(one_by_one_template, Plaquettes({1: plaquette}))
+    circuit = generate_circuit(one_by_one_template, 2, Plaquettes({1: plaquette}))
     assert circuit.get_circuit() == _expected_circuit()
 
 
@@ -55,7 +55,7 @@ def test_generate_circuit_defaultdict(
     plaquette: Plaquette, one_by_one_template: Template
 ) -> None:
     circuit = generate_circuit(
-        one_by_one_template, Plaquettes(defaultdict(lambda: plaquette))
+        one_by_one_template, 2, Plaquettes(defaultdict(lambda: plaquette))
     )
     assert circuit.get_circuit() == _expected_circuit()
 
@@ -64,13 +64,15 @@ def test_generate_circuit_dict_0_indexed(
     plaquette: Plaquette, one_by_one_template: Template
 ) -> None:
     with pytest.raises(TQECException):
-        generate_circuit(one_by_one_template, Plaquettes({0: plaquette}))
+        generate_circuit(one_by_one_template, 2, Plaquettes({0: plaquette}))
 
 
 def test_generate_circuit_wrong_number_of_plaquettes(
     plaquette: Plaquette, one_by_one_template: Template
 ) -> None:
     with pytest.raises(TQECException):
-        generate_circuit(one_by_one_template, Plaquettes({1: plaquette, 2: plaquette}))
+        generate_circuit(
+            one_by_one_template, 2, Plaquettes({1: plaquette, 2: plaquette})
+        )
     with pytest.raises(TQECException):
-        generate_circuit(one_by_one_template, Plaquettes({}))
+        generate_circuit(one_by_one_template, 2, Plaquettes({}))
diff --git a/src/tqec/circuit/observable_qubits_test.py b/src/tqec/circuit/observable_qubits_test.py
index 06d88dc8..57712527 100644
--- a/src/tqec/circuit/observable_qubits_test.py
+++ b/src/tqec/circuit/observable_qubits_test.py
@@ -26,7 +26,7 @@ def test_raw_rectangle_default_observable_qubits(plaquettes: list[Plaquette]) ->
         ]
     )
 
-    obs = observable_qubits_from_template(template, plaquettes)
+    obs = observable_qubits_from_template(template, 1, plaquettes)
     result = [
         (GridQubit(-1, 3), 0),
         (GridQubit(1, 3), 0),
diff --git a/src/tqec/templates/_testing.py b/src/tqec/templates/_testing.py
index 6faccf97..604498bb 100644
--- a/src/tqec/templates/_testing.py
+++ b/src/tqec/templates/_testing.py
@@ -16,17 +16,16 @@ class FixedTemplate(RectangularTemplate):
     def __init__(
         self,
         indices: Sequence[Sequence[int]],
-        k: int = 2,
         default_increments: Displacement | None = None,
     ) -> None:
-        super().__init__(k, default_increments)
+        super().__init__(default_increments)
         self._indices: npt.NDArray[numpy.int_] = numpy.array(
             [list(line) for line in indices]
         )
 
     @override
     def instantiate(
-        self, plaquette_indices: Sequence[int] | None = None
+        self, _: int = 0, plaquette_indices: Sequence[int] | None = None
     ) -> npt.NDArray[numpy.int_]:
         if plaquette_indices is None:
             plaquette_indices = list(range(1, self.expected_plaquettes_number + 1))
diff --git a/src/tqec/templates/_testing_test.py b/src/tqec/templates/_testing_test.py
index 99e8d06e..351aab58 100644
--- a/src/tqec/templates/_testing_test.py
+++ b/src/tqec/templates/_testing_test.py
@@ -13,11 +13,16 @@ def test_construction() -> None:
 
 
 def test_instantiation() -> None:
-    numpy.testing.assert_array_equal(FixedTemplate([[0]]).instantiate([1]), [[1]])
+    numpy.testing.assert_array_equal(
+        FixedTemplate([[0]]).instantiate(plaquette_indices=[1]), [[1]]
+    )
     numpy.testing.assert_array_equal(FixedTemplate([[0]]).instantiate(), [[1]])
-    numpy.testing.assert_array_equal(FixedTemplate([[1]]).instantiate([1, 2]), [[2]])
     numpy.testing.assert_array_equal(
-        FixedTemplate([[0, 1], [1, 2]]).instantiate([1, 2, 3]), [[1, 2], [2, 3]]
+        FixedTemplate([[1]]).instantiate(plaquette_indices=[1, 2]), [[2]]
+    )
+    numpy.testing.assert_array_equal(
+        FixedTemplate([[0, 1], [1, 2]]).instantiate(plaquette_indices=[1, 2, 3]),
+        [[1, 2], [2, 3]],
     )
 
 
@@ -25,7 +30,7 @@ def test_shape() -> None:
     assert FixedTemplate([[0]]).scalable_shape == Scalable2D(
         LinearFunction(0, 1), LinearFunction(0, 1)
     )
-    assert FixedTemplate([[0]]).shape == Shape2D(1, 1)
+    assert FixedTemplate([[0]]).shape(1) == Shape2D(1, 1)
 
 
 def test_number_of_expected_plaquettes() -> None:
diff --git a/src/tqec/templates/qubit_test.py b/src/tqec/templates/qubit_test.py
index 7ecafd78..3d2e10b5 100644
--- a/src/tqec/templates/qubit_test.py
+++ b/src/tqec/templates/qubit_test.py
@@ -12,22 +12,11 @@ from tqec.templates.qubit import (
 )
 
 
-@pytest.mark.parametrize(
-    "tested_class",
-    [
-        QubitTemplate,
-        QubitHorizontalBorders,
-        QubitVerticalBorders,
-        Qubit4WayJunctionTemplate,
-    ],
-)
-def test_creation(tested_class: type) -> None:
-    tested_class()
-    tested_class(k=10000)
-    with pytest.raises(
-        TQECException, match=r"^Cannot have a negative scaling parameter. Got -1.$"
-    ):
-        tested_class(k=-1)
+def test_creation() -> None:
+    QubitTemplate()
+    QubitHorizontalBorders()
+    QubitVerticalBorders()
+    Qubit4WayJunctionTemplate()
 
 
 def test_expected_plaquettes_number() -> None:
@@ -53,9 +42,9 @@ def test_scalable_shape() -> None:
 
 
 def test_qubit_template_instantiation() -> None:
-    template = QubitTemplate(k=2)
+    template = QubitTemplate()
     numpy.testing.assert_array_equal(
-        template.instantiate(),
+        template.instantiate(2),
         [
             [1, 5, 6, 5, 6, 2],
             [7, 9, 10, 9, 10, 11],
@@ -65,9 +54,8 @@ def test_qubit_template_instantiation() -> None:
             [3, 13, 14, 13, 14, 4],
         ],
     )
-    template.scale_to(4)
     numpy.testing.assert_array_equal(
-        template.instantiate(),
+        template.instantiate(4),
         [
             [1, 5, 6, 5, 6, 5, 6, 5, 6, 2],
             [7, 9, 10, 9, 10, 9, 10, 9, 10, 11],
@@ -84,9 +72,9 @@ def test_qubit_template_instantiation() -> None:
 
 
 def test_qubit_vertical_borders_template_instantiation() -> None:
-    template = QubitVerticalBorders(k=2)
+    template = QubitVerticalBorders()
     numpy.testing.assert_array_equal(
-        template.instantiate(),
+        template.instantiate(2),
         [
             [1, 2],
             [5, 7],
@@ -96,9 +84,8 @@ def test_qubit_vertical_borders_template_instantiation() -> None:
             [3, 4],
         ],
     )
-    template.scale_to(4)
     numpy.testing.assert_array_equal(
-        template.instantiate(),
+        template.instantiate(4),
         [
             [1, 2],
             [5, 7],
@@ -115,17 +102,16 @@ def test_qubit_vertical_borders_template_instantiation() -> None:
 
 
 def test_qubit_horizontal_borders_template_instantiation() -> None:
-    template = QubitHorizontalBorders(k=2)
+    template = QubitHorizontalBorders()
     numpy.testing.assert_array_equal(
-        template.instantiate(),
+        template.instantiate(2),
         [
             [1, 5, 6, 5, 6, 2],
             [3, 7, 8, 7, 8, 4],
         ],
     )
-    template.scale_to(4)
     numpy.testing.assert_array_equal(
-        template.instantiate(),
+        template.instantiate(4),
         [
             [1, 5, 6, 5, 6, 5, 6, 5, 6, 2],
             [3, 7, 8, 7, 8, 7, 8, 7, 8, 4],
@@ -134,7 +120,7 @@ def test_qubit_horizontal_borders_template_instantiation() -> None:
 
 
 def test_qubit_4_way_junction_template_instantiation() -> None:
-    template = Qubit4WayJunctionTemplate(k=1)
+    template = Qubit4WayJunctionTemplate()
 
     expected_warning_message = (
         "Instantiating Qubit4WayJunctionTemplate with k=1. The "
@@ -143,7 +129,7 @@ def test_qubit_4_way_junction_template_instantiation() -> None:
     )
     with pytest.warns(TQECWarning, match=expected_warning_message):
         numpy.testing.assert_array_equal(
-            template.instantiate(),
+            template.instantiate(1),
             [
                 [1, 5, 6, 2],
                 [7, 10, 11, 12],
@@ -151,9 +137,8 @@ def test_qubit_4_way_junction_template_instantiation() -> None:
                 [3, 14, 15, 4],
             ],
         )
-    template.scale_to(2)
     numpy.testing.assert_array_equal(
-        template.instantiate(),
+        template.instantiate(2),
         [
             [1, 5, 6, 5, 6, 2],
             [7, 10, 11, 10, 11, 12],