Repository: tqec/tqec

Issue:
False positive checking of different default factories for `Plaquettes`
## Describe the bug

In new layers framework, when connecting two regular cubes spatially (run the following code). 


<details><summary> program </summary>

```python
from tqec.compile.blocks.block import Block
from tqec.compile.blocks.layers.atomic.plaquettes import PlaquetteLayer
from tqec.compile.blocks.layers.composed.repeated import RepeatedLayer
from tqec.compile.graph import TopologicalComputationGraph
from tqec.compile.specs.library.generators.memory import (
    get_memory_horizontal_boundary_plaquettes,
    get_memory_horizontal_boundary_raw_template,
    get_memory_qubit_plaquettes,
    get_memory_qubit_raw_template,
    get_memory_vertical_boundary_plaquettes,
    get_memory_vertical_boundary_raw_template,
)
from tqec.utils.enums import Basis
from tqec.utils.position import BlockPosition3D
from tqec.utils.scale import LinearFunction, PhysicalQubitScalable2D

scalable_qubit_shape = PhysicalQubitScalable2D(
    LinearFunction(4, 5), LinearFunction(4, 5)
)

graph = TopologicalComputationGraph(scalable_qubit_shape)
XZZ = Block(
    [
        PlaquetteLayer(
            get_memory_qubit_raw_template(),
            get_memory_qubit_plaquettes(reset=Basis.Z),
        ),
        RepeatedLayer(
            PlaquetteLayer(
                get_memory_qubit_raw_template(), get_memory_qubit_plaquettes()
            ),
            repetitions=LinearFunction(2, -1),
        ),
        PlaquetteLayer(
            get_memory_qubit_raw_template(),
            get_memory_qubit_plaquettes(measurement=Basis.Z),
        ),
    ]
)
XZZ_1 = Block(
    [
        PlaquetteLayer(
            get_memory_qubit_raw_template(),
            get_memory_qubit_plaquettes(reset=Basis.Z),
        ),
        RepeatedLayer(
            PlaquetteLayer(
                get_memory_qubit_raw_template(), get_memory_qubit_plaquettes()
            ),
            repetitions=LinearFunction(2, -1),
        ),
        PlaquetteLayer(
            get_memory_qubit_raw_template(),
            get_memory_qubit_plaquettes(measurement=Basis.Z),
        ),
    ]
)
XZO = Block(
    [
        PlaquetteLayer(
            get_memory_qubit_raw_template(),
            get_memory_qubit_plaquettes(),
        )
        for _ in range(2)
    ]
)
OZZ = Block(
    [
        PlaquetteLayer(
            get_memory_vertical_boundary_raw_template(),
            get_memory_vertical_boundary_plaquettes(reset=Basis.Z),
        ),
        RepeatedLayer(
            PlaquetteLayer(
                get_memory_vertical_boundary_raw_template(),
                get_memory_vertical_boundary_plaquettes(),
            ),
            repetitions=LinearFunction(2, -1),
        ),
        PlaquetteLayer(
            get_memory_vertical_boundary_raw_template(),
            get_memory_vertical_boundary_plaquettes(measurement=Basis.Z),
        ),
    ]
)

XOZ = Block(
    [
        PlaquetteLayer(
            get_memory_horizontal_boundary_raw_template(),
            get_memory_horizontal_boundary_plaquettes(reset=Basis.Z),
        ),
        RepeatedLayer(
            PlaquetteLayer(
                get_memory_horizontal_boundary_raw_template(),
                get_memory_horizontal_boundary_plaquettes(),
            ),
            repetitions=LinearFunction(2, -1),
        ),
        PlaquetteLayer(
            get_memory_horizontal_boundary_raw_template(),
            get_memory_horizontal_boundary_plaquettes(measurement=Basis.Z),
        ),
    ]
)

graph.add_cube(BlockPosition3D(-1, 0, 0), XZZ)
graph.add_cube(BlockPosition3D(-1, 1, 0), XZZ_1)
graph.add_pipe(BlockPosition3D(-1, 0, 0), BlockPosition3D(-1, 1, 0), XOZ)


layer_tree = graph.to_layer_tree()
circuit = layer_tree.generate_circuit(2)

print(circuit)

```

</details>

It raises error. 


<details><summary> error </summary>

```
Traceback (most recent call last):
  File "/home/zhaoyilun/workspace/github/forks/tqec/temp/test_spatial_connection.py", line 121, in <module>
    circuit = layer_tree.generate_circuit(2)
  File "/home/zhaoyilun/workspace/github/forks/tqec/src/tqec/compile/tree/tree.py", line 142, in generate_circuit
    self._annotate_circuits(k)
  File "/home/zhaoyilun/workspace/github/forks/tqec/src/tqec/compile/tree/tree.py", line 80, in _annotate_circuits
    self._root.walk(AnnotateCircuitOnLayerNode(k))
  File "/home/zhaoyilun/workspace/github/forks/tqec/src/tqec/compile/tree/node.py", line 121, in walk
    child.walk(walker)
  File "/home/zhaoyilun/workspace/github/forks/tqec/src/tqec/compile/tree/node.py", line 121, in walk
    child.walk(walker)
  File "/home/zhaoyilun/workspace/github/forks/tqec/src/tqec/compile/tree/node.py", line 119, in walk
    walker.visit_node(self)
  File "/home/zhaoyilun/workspace/github/forks/tqec/src/tqec/compile/tree/annotators/circuit.py", line 16, in visit_node
    node.set_circuit_annotation(self._k, node._layer.to_circuit(self._k))
  File "/home/zhaoyilun/workspace/github/forks/tqec/src/tqec/compile/blocks/layers/atomic/layout.py", line 175, in to_circuit
    template, plaquettes = self.to_template_and_plaquettes()
  File "/home/zhaoyilun/workspace/github/forks/tqec/src/tqec/compile/blocks/layers/atomic/layout.py", line 164, in to_template_and_plaquettes
    return template, template.get_global_plaquettes(plaquettes_dict)
  File "/home/zhaoyilun/workspace/github/forks/tqec/src/tqec/templates/layout.py", line 233, in get_global_plaquettes
    raise TQECException(
tqec.utils.exceptions.TQECException: Found several different default factories: frozenset({<function FrozenDefaultDict.map_values.<locals>.<lambda> at 0x79877
40bf490>, <function FrozenDefaultDict.map_values.<locals>.<lambda> at 0x798774b63d90>}). Cannot pick one for the merged Plaquettes instance.

```

</details>


This commit: https://github.com/tqec/tqec/commit/8b93e17de2c7b084a8dd6bc6b022ab6e23b162f0#diff-d67bc783df08a9d72c5f7e6829851d05036322d22b2a20ac4610dc717cad2823 introduce a method, which has a check: https://github.com/tqec/tqec/blob/f8ebe11d68eda66345568cf48a8e14f8a59686fb/src/tqec/templates/layout.py#L216C6-L225C14

It seems that this checking has some issues.

When there are different instances plaquetts in `individual_plaquettes`. (See the program to reproduce this bug, I created `XZZ` and `XZZ_1`), the `default_factory` will be treated as different elements in a set.

A workaround may be to use the [`__code__`](https://docs.python.org/3/reference/datamodel.html#code-objects) attribute to check if these factories are same functions.

```python

        def _canonical_factory(factory): if factory is None:
                return None
            try:
                return factory.__code__
            except AttributeError:
                return factory

        unique_default_factories = frozenset(
            _canonical_factory(factory) for factory in default_factories
        )


```

This is not elegant to me, but I don't have better solutions for now


## Steps to reproduce the behavior

Run the program as described above

## Expected behavior

no error 


## Python dependency versions

Run `pip freeze` and put the output in the `<details>` element below.

<details>

</details>

## Additional context

*Add any other context about the problem here.*

Hints from issue comments:
@nelimee: I remember now that portion of the code. I think that the best way of dealing with this is to change `FrozenDefaultDict` to accept a default **value** instead of a default **factory**. That should not impact the existing workflows, because the default factory is always `return empty_plaquette`, and that will allow us to correctly check for issues in defaults.

What do you think?@Zhaoyilunnn: > I remember now that portion of the code. I think that the best way of dealing with this is to change `FrozenDefaultDict` to accept a default **value** instead of a default **factory**. That should not impact the existing workflows, because the default factory is always `return empty_plaquette`, and that will allow us to correctly check for issues in defaults.
> 
> What do you think?

That sounds a good solution to me. I prefer to first submit a PR to fix this, rather than merge it into #524 because I still need more time to migrate and pass all unit tests to finish that PR. What do you think? @nelimee: > > I remember now that portion of the code. I think that the best way of dealing with this is to change `FrozenDefaultDict` to accept a default **value** instead of a default **factory**. That should not impact the existing workflows, because the default factory is always `return empty_plaquette`, and that will allow us to correctly check for issues in defaults.
> > 
> > What do you think?
> 
> That sounds a good solution to me. I prefer to first submit a PR to fix this, rather than merge it into #524 because I still need more time to migrate and pass all unit tests to finish that PR. What do you think? 

Seems a good way to solve the issue, that also allows to have clearer git history :) 

Solution patch:
diff --git a/src/tqec/compile/block.py b/src/tqec/compile/block.py
index 081c729d..b1f591c7 100644
--- a/src/tqec/compile/block.py
+++ b/src/tqec/compile/block.py
@@ -130,7 +130,7 @@ class BlockLayout:
                 )
             plaquettes = Plaquettes(
                 FrozenDefaultDict(
-                    merged_plaquettes, default_factory=empty_square_plaquette
+                    merged_plaquettes, default_value=empty_square_plaquette()
                 )
             )
             if repetitions is not None:
diff --git a/src/tqec/compile/specs/library/_utils.py b/src/tqec/compile/specs/library/_utils.py
index 65cac1bd..e1a8d041 100644
--- a/src/tqec/compile/specs/library/_utils.py
+++ b/src/tqec/compile/specs/library/_utils.py
@@ -106,7 +106,7 @@ def _build_plaquettes_for_rotated_surface_code(
                 12: p2.project_on_boundary(PlaquetteOrientation.RIGHT),
                 13: p1.project_on_boundary(PlaquetteOrientation.DOWN),
             },
-            default_factory=empty_square_plaquette,
+            default_value=empty_square_plaquette(),
         )
     )
     if repetitions is not None:
@@ -208,7 +208,7 @@ def _build_plaquettes_for_space_regular_pipe(
     plaquettes = Plaquettes(
         FrozenDefaultDict(
             mapping,
-            default_factory=empty_square_plaquette,
+            default_value=empty_square_plaquette(),
         )
     )
     if repetitions is not None:
diff --git a/src/tqec/compile/specs/library/generators/hadamard.py b/src/tqec/compile/specs/library/generators/hadamard.py
index d13eb60c..4a1b05c7 100644
--- a/src/tqec/compile/specs/library/generators/hadamard.py
+++ b/src/tqec/compile/specs/library/generators/hadamard.py
@@ -66,7 +66,7 @@ def get_temporal_hadamard_rpng_descriptions(
             # DOWN
             13: RPNGDescription.from_string(f"-{bv}1h -{bv}2h ---- ----"),
         },
-        default_factory=RPNGDescription.empty,
+        default_value=RPNGDescription.empty(),
     )
 
 
@@ -145,7 +145,7 @@ def get_spatial_horizontal_hadamard_rpng_descriptions(
             7: RPNGDescription.from_string(f"{r}{b1}1{m} {r}{b1}3{m} -{b1}2- -{b1}4-"),
             8: RPNGDescription.from_string(f"{r}{b2}1{m} {r}{b2}2{m} -{b2}3- -{b2}4-"),
         },
-        default_factory=RPNGDescription.empty,
+        default_value=RPNGDescription.empty(),
     )
 
 
@@ -224,7 +224,7 @@ def get_spatial_vertical_hadamard_rpng_descriptions(
             7: RPNGDescription.from_string(f"{r}{b1}1{m} -{b1}3- {r}{b1}2{m} -{b1}4-"),
             8: RPNGDescription.from_string(f"{r}{b2}1{m} -{b2}2- {r}{b2}3{m} -{b2}4-"),
         },
-        default_factory=RPNGDescription.empty,
+        default_value=RPNGDescription.empty(),
     )
 
 
diff --git a/src/tqec/compile/specs/library/generators/memory.py b/src/tqec/compile/specs/library/generators/memory.py
index 63ca06f6..a43f3ee4 100644
--- a/src/tqec/compile/specs/library/generators/memory.py
+++ b/src/tqec/compile/specs/library/generators/memory.py
@@ -82,7 +82,7 @@ def get_memory_qubit_rpng_descriptions(
             # DOWN
             13: RPNGDescription.from_string(f"{r}{bv}1{m} {r}{bv}2{m} ---- ----"),
         },
-        default_factory=RPNGDescription.empty,
+        default_value=RPNGDescription.empty(),
     )
 
 
@@ -161,7 +161,7 @@ def get_memory_vertical_boundary_rpng_descriptions(
             7: RPNGDescription.from_string(f"{r}{bh}1{m} -{bh}3- {r}{bh}2{m} -{bh}4-"),
             8: RPNGDescription.from_string(f"{r}{bv}1{m} -{bv}2- {r}{bv}3{m} -{bv}4-"),
         },
-        default_factory=RPNGDescription.empty,
+        default_value=RPNGDescription.empty(),
     )
 
 
@@ -240,7 +240,7 @@ def get_memory_horizontal_boundary_rpng_descriptions(
             7: RPNGDescription.from_string(f"{r}{bh}1{m} {r}{bh}3{m} -{bh}2- -{bh}4-"),
             8: RPNGDescription.from_string(f"{r}{bv}1{m} {r}{bv}2{m} -{bv}3- -{bv}4-"),
         },
-        default_factory=RPNGDescription.empty,
+        default_value=RPNGDescription.empty(),
     )
 
 
diff --git a/src/tqec/compile/specs/library/generators/spatial.py b/src/tqec/compile/specs/library/generators/spatial.py
index 412f8098..fe0c3db2 100644
--- a/src/tqec/compile/specs/library/generators/spatial.py
+++ b/src/tqec/compile/specs/library/generators/spatial.py
@@ -246,7 +246,7 @@ def get_spatial_cube_qubit_rpng_descriptions(
 
     return FrozenDefaultDict(
         mapping,
-        default_factory=RPNGDescription.empty,
+        default_value=RPNGDescription.empty(),
     )
 
 
@@ -389,7 +389,7 @@ def _get_left_spatial_cube_arm_rpng_descriptions(
             7: RPNGDescription.from_string(f"{r}{bi}1{m} -{bi}3- {r}{bi}2{m} -{bi}4-"),
             8: RPNGDescription.from_string(f"{r}{be}1{m} -{be}2- {r}{be}3{m} -{be}4-"),
         },
-        default_factory=RPNGDescription.empty,
+        default_value=RPNGDescription.empty(),
     )
 
 
@@ -418,7 +418,7 @@ def _get_right_spatial_cube_arm_rpng_descriptions(
             7: RPNGDescription.from_string(f"{r}{bi}1{m} -{bi}3- {r}{bi}2{m} -{bi}4-"),
             8: RPNGDescription.from_string(f"{r}{be}1{m} -{be}2- {r}{be}3{m} -{be}4-"),
         },
-        default_factory=RPNGDescription.empty,
+        default_value=RPNGDescription.empty(),
     )
 
 
@@ -447,7 +447,7 @@ def _get_up_spatial_cube_arm_rpng_descriptions(
             7: RPNGDescription.from_string(f"{r}{bi}1{m} {r}{bi}3{m} -{bi}4- -{bi}5-"),
             8: RPNGDescription.from_string(f"{r}{be}1{m} {r}{be}3{m} -{be}2- -{be}5-"),
         },
-        default_factory=RPNGDescription.empty,
+        default_value=RPNGDescription.empty(),
     )
 
 
@@ -476,7 +476,7 @@ def _get_down_spatial_cube_arm_rpng_descriptions(
             7: RPNGDescription.from_string(f"{r}{be}1{m} {r}{be}3{m} -{be}2- -{be}5-"),
             8: RPNGDescription.from_string(f"{r}{bi}1{m} {r}{bi}3{m} -{bi}4- -{bi}5-"),
         },
-        default_factory=RPNGDescription.empty,
+        default_value=RPNGDescription.empty(),
     )
 
 
diff --git a/src/tqec/plaquette/plaquette.py b/src/tqec/plaquette/plaquette.py
index 7d63e473..31fba4a7 100644
--- a/src/tqec/plaquette/plaquette.py
+++ b/src/tqec/plaquette/plaquette.py
@@ -177,15 +177,15 @@ class Plaquettes:
         d: dict[int | Literal["default"], str] = {
             k: p.name for k, p in self.collection.items()
         }
-        if self.collection.default_factory is not None:
-            d["default"] = self.collection.default_factory().name
+        if self.collection.default_value is not None:
+            d["default"] = self.collection.default_value.name
         return d
 
     def without_plaquettes(self, indices: Collection[int]) -> Plaquettes:
         return Plaquettes(
             FrozenDefaultDict(
                 {k: v for k, v in self.collection.items() if k not in indices},
-                default_factory=self.collection.default_factory,
+                default_value=self.collection.default_value,
             )
         )
 
diff --git a/src/tqec/templates/layout.py b/src/tqec/templates/layout.py
index 3734bfe6..21a5f3ee 100644
--- a/src/tqec/templates/layout.py
+++ b/src/tqec/templates/layout.py
@@ -65,7 +65,7 @@ which outputs
 """
 
 from copy import deepcopy
-from typing import Callable, Mapping, Sequence
+from typing import Mapping, Sequence
 
 import numpy
 import numpy.typing as npt
@@ -203,7 +203,7 @@ class LayoutTemplate(Template):
                 "The following expected positions were not found in the "
                 f"provided individual_plaquettes: {missing_positions}."
             )
-        default_factories: list[Callable[[], Plaquette] | None] = []
+        default_values: list[Plaquette | None] = []
         index_maps = self.get_indices_map_for_instantiation()
         global_plaquettes: dict[int, Plaquette] = {}
         for position, index_map in index_maps.items():
@@ -213,19 +213,19 @@ class LayoutTemplate(Template):
                     position
                 ].collection.items()
             }
-            default_factories.append(
-                individual_plaquettes[position].collection.default_factory
+            default_values.append(
+                individual_plaquettes[position].collection.default_value
             )
-        unique_default_factories = frozenset(default_factories)
-        if len(unique_default_factories) != 1:
+        unique_default_values = frozenset(default_values)
+        if len(unique_default_values) != 1:
             raise TQECException(
                 "Found several different default factories: "
-                f"{unique_default_factories}. Cannot pick one for the merged "
+                f"{unique_default_values}. Cannot pick one for the merged "
                 f"{Plaquettes.__name__} instance."
             )
         return Plaquettes(
             FrozenDefaultDict(
-                global_plaquettes, default_factory=next(iter(unique_default_factories))
+                global_plaquettes, default_value=next(iter(unique_default_values))
             )
         )
 
diff --git a/src/tqec/utils/frozendefaultdict.py b/src/tqec/utils/frozendefaultdict.py
index 8f757f26..fdf8106f 100644
--- a/src/tqec/utils/frozendefaultdict.py
+++ b/src/tqec/utils/frozendefaultdict.py
@@ -34,16 +34,16 @@ class FrozenDefaultDict(Generic[K, V], Mapping[K, V]):
         self,
         arg: Mapping[K, V] | Iterable[tuple[K, V]] | None = None,
         *,
-        default_factory: Callable[[], V] | None = None,
+        default_value: V | None = None,
     ) -> None:
         super().__init__()
         self._dict: dict[K, V] = dict(arg) if arg is not None else dict()
-        self._default_factory = default_factory
+        self._default_value = default_value
 
     def __missing__(self, key: K) -> V:
-        if self._default_factory is None:
+        if self._default_value is None:
             raise KeyError(key)
-        return self._default_factory()
+        return self._default_value
 
     @override
     def __getitem__(self, key: K) -> V:
@@ -67,7 +67,7 @@ class FrozenDefaultDict(Generic[K, V], Mapping[K, V]):
     def __or__(self, other: Mapping[K, V]) -> FrozenDefaultDict[K, V]:
         mapping = deepcopy(self._dict)
         mapping.update(other)
-        return FrozenDefaultDict(mapping, default_factory=self._default_factory)
+        return FrozenDefaultDict(mapping, default_value=self._default_value)
 
     def __hash__(self) -> int:
         return hash(tuple(sorted(self.items())))
@@ -77,32 +77,26 @@ class FrozenDefaultDict(Generic[K, V], Mapping[K, V]):
             return False
         other = cast(FrozenDefaultDict[K, V], other)
         return (
-            (self._default_factory is None and other._default_factory is None)
-            or (
-                self._default_factory is not None
-                and other._default_factory is not None
-                and (self._default_factory() == other._default_factory())
-            )
+            self._default_value == other._default_value
         ) and self._dict == other._dict
 
-    def has_default_factory(self) -> bool:
-        return self._default_factory is not None
+    def has_default_value(self) -> bool:
+        return self._default_value is not None
 
     @property
-    def default_factory(self) -> Callable[[], V] | None:
-        return self._default_factory
+    def default_value(self) -> V | None:
+        return self._default_value
 
     def map_keys(self, callable: Callable[[K], K]) -> FrozenDefaultDict[K, V]:
         return FrozenDefaultDict(
             {callable(k): v for k, v in self.items()},
-            default_factory=self._default_factory,
+            default_value=self._default_value,
         )
 
     def map_values(self, callable: Callable[[V], Vp]) -> FrozenDefaultDict[K, Vp]:
-        default_factory: Callable[[], Vp] | None = None
-        if self.default_factory is not None:
-            default_value = callable(self.default_factory())
-            default_factory = lambda: default_value  # noqa: E731
+        default_value: Vp | None = None
+        if self.default_value is not None:
+            default_value = callable(self.default_value)
         return FrozenDefaultDict(
-            {k: callable(v) for k, v in self.items()}, default_factory=default_factory
+            {k: callable(v) for k, v in self.items()}, default_value=default_value
         )


Test patch:
diff --git a/src/tqec/compile/blocks/block_test.py b/src/tqec/compile/blocks/block_test.py
index 7c1c400a..65fdf72a 100644
--- a/src/tqec/compile/blocks/block_test.py
+++ b/src/tqec/compile/blocks/block_test.py
@@ -24,7 +24,7 @@ from tqec.utils.scale import LinearFunction, PhysicalQubitScalable2D
 def plaquette_layer_fixture() -> PlaquetteLayer:
     return PlaquetteLayer(
         QubitTemplate(),
-        Plaquettes(FrozenDefaultDict({}, default_factory=empty_square_plaquette)),
+        Plaquettes(FrozenDefaultDict({}, default_value=empty_square_plaquette())),
     )
 
 
@@ -32,7 +32,7 @@ def plaquette_layer_fixture() -> PlaquetteLayer:
 def plaquette_layer2_fixture() -> PlaquetteLayer:
     return PlaquetteLayer(
         QubitSpatialCubeTemplate(),
-        Plaquettes(FrozenDefaultDict({}, default_factory=empty_square_plaquette)),
+        Plaquettes(FrozenDefaultDict({}, default_value=empty_square_plaquette())),
     )
 
 
@@ -57,11 +57,11 @@ def base_layers_fixture() -> list[BaseLayer]:
     return [
         PlaquetteLayer(
             QubitTemplate(),
-            Plaquettes(FrozenDefaultDict({}, default_factory=empty_square_plaquette)),
+            Plaquettes(FrozenDefaultDict({}, default_value=empty_square_plaquette())),
         ),
         PlaquetteLayer(
             QubitSpatialCubeTemplate(),
-            Plaquettes(FrozenDefaultDict({}, default_factory=empty_square_plaquette)),
+            Plaquettes(FrozenDefaultDict({}, default_value=empty_square_plaquette())),
         ),
         RawCircuitLayer(
             lambda k: ScheduledCircuit.from_circuit(stim.Circuit()),
diff --git a/src/tqec/compile/blocks/layers/atomic/layout_test.py b/src/tqec/compile/blocks/layers/atomic/layout_test.py
index c11c6bde..bdc3d0d6 100644
--- a/src/tqec/compile/blocks/layers/atomic/layout_test.py
+++ b/src/tqec/compile/blocks/layers/atomic/layout_test.py
@@ -24,7 +24,7 @@ LOGICAL_QUBIT_SHAPE: Final = PhysicalQubitScalable2D(
 def plaquette_layer_fixture() -> PlaquetteLayer:
     template = QubitTemplate()
     plaquettes = Plaquettes(
-        FrozenDefaultDict({}, default_factory=empty_square_plaquette)
+        FrozenDefaultDict({}, default_value=empty_square_plaquette())
     )
     return PlaquetteLayer(template, plaquettes)
 
@@ -111,6 +111,6 @@ def test_to_template_and_plaquettes_multiple(plaquette_layer: PlaquetteLayer) ->
                 i + plaquette_layer.template.expected_plaquettes_number: plaq
                 for i, plaq in plaquettes.collection.items()
             },
-            default_factory=plaquettes.collection.default_factory,
+            default_value=plaquettes.collection.default_value,
         )
     )
diff --git a/src/tqec/compile/blocks/layers/atomic/plaquettes_test.py b/src/tqec/compile/blocks/layers/atomic/plaquettes_test.py
index cafdd322..8b6be85b 100644
--- a/src/tqec/compile/blocks/layers/atomic/plaquettes_test.py
+++ b/src/tqec/compile/blocks/layers/atomic/plaquettes_test.py
@@ -17,7 +17,7 @@ def test_creation() -> None:
     template = FixedTemplate([[1]])
     large_template = FixedTemplate([[1 for _ in range(10)] for _ in range(10)])
     plaquettes = Plaquettes(
-        FrozenDefaultDict({}, default_factory=empty_square_plaquette)
+        FrozenDefaultDict({}, default_value=empty_square_plaquette())
     )
     PlaquetteLayer(template, plaquettes)
     PlaquetteLayer(
@@ -43,7 +43,7 @@ def test_scalable_shape() -> None:
     template = FixedTemplate([[1]])
     large_template = FixedTemplate([[1 for _ in range(10)] for _ in range(10)])
     plaquettes = Plaquettes(
-        FrozenDefaultDict({}, default_factory=empty_square_plaquette)
+        FrozenDefaultDict({}, default_value=empty_square_plaquette())
     )
     single_plaquette_shape = PhysicalQubitScalable2D(
         LinearFunction(0, 3), LinearFunction(0, 3)
@@ -74,7 +74,7 @@ def test_with_spatial_borders_trimmed(borders: tuple[SpatialBlockBorder, ...]) -
                 i + 1: empty_square_plaquette()
                 for i in range(template.expected_plaquettes_number)
             },
-            default_factory=empty_square_plaquette,
+            default_value=empty_square_plaquette(),
         )
     )
     layer = PlaquetteLayer(template, plaquettes)
@@ -96,7 +96,7 @@ def test_with_spatial_borders_trimmed(borders: tuple[SpatialBlockBorder, ...]) -
 def test_with_temporal_borders_replaced_none() -> None:
     template = FixedTemplate([[1]])
     plaquettes = Plaquettes(
-        FrozenDefaultDict({}, default_factory=empty_square_plaquette)
+        FrozenDefaultDict({}, default_value=empty_square_plaquette())
     )
     layer = PlaquetteLayer(template, plaquettes)
     assert layer.with_temporal_borders_replaced({}) == layer
@@ -119,12 +119,12 @@ def test_with_temporal_borders_replaced_none() -> None:
 def test_with_temporal_borders_replaced() -> None:
     template = FixedTemplate([[1]])
     plaquettes = Plaquettes(
-        FrozenDefaultDict({}, default_factory=empty_square_plaquette)
+        FrozenDefaultDict({}, default_value=empty_square_plaquette())
     )
     layer = PlaquetteLayer(template, plaquettes)
     replacement_template = FixedTemplate([[2]])
     replacement_plaquettes = Plaquettes(
-        FrozenDefaultDict({}, default_factory=empty_square_plaquette)
+        FrozenDefaultDict({}, default_value=empty_square_plaquette())
     )
     replacement_layer = PlaquetteLayer(replacement_template, replacement_plaquettes)
 
diff --git a/src/tqec/compile/blocks/layers/composed/repeated_test.py b/src/tqec/compile/blocks/layers/composed/repeated_test.py
index 916d12d1..9031e62f 100644
--- a/src/tqec/compile/blocks/layers/composed/repeated_test.py
+++ b/src/tqec/compile/blocks/layers/composed/repeated_test.py
@@ -21,7 +21,7 @@ from tqec.utils.scale import LinearFunction, PhysicalQubitScalable2D
 def plaquette_layer_fixture() -> PlaquetteLayer:
     return PlaquetteLayer(
         QubitTemplate(),
-        Plaquettes(FrozenDefaultDict({}, default_factory=empty_square_plaquette)),
+        Plaquettes(FrozenDefaultDict({}, default_value=empty_square_plaquette())),
     )
 
 
@@ -29,7 +29,7 @@ def plaquette_layer_fixture() -> PlaquetteLayer:
 def plaquette_layer2_fixture() -> PlaquetteLayer:
     return PlaquetteLayer(
         QubitSpatialCubeTemplate(),
-        Plaquettes(FrozenDefaultDict({}, default_factory=empty_square_plaquette)),
+        Plaquettes(FrozenDefaultDict({}, default_value=empty_square_plaquette())),
     )
 
 
diff --git a/src/tqec/compile/blocks/layers/composed/sequenced_test.py b/src/tqec/compile/blocks/layers/composed/sequenced_test.py
index 74b8bd28..d58fd4f7 100644
--- a/src/tqec/compile/blocks/layers/composed/sequenced_test.py
+++ b/src/tqec/compile/blocks/layers/composed/sequenced_test.py
@@ -19,7 +19,7 @@ from tqec.utils.scale import LinearFunction, PhysicalQubitScalable2D
 def plaquette_layer_fixture() -> PlaquetteLayer:
     return PlaquetteLayer(
         QubitTemplate(),
-        Plaquettes(FrozenDefaultDict({}, default_factory=empty_square_plaquette)),
+        Plaquettes(FrozenDefaultDict({}, default_value=empty_square_plaquette())),
     )
 
 
@@ -27,7 +27,7 @@ def plaquette_layer_fixture() -> PlaquetteLayer:
 def plaquette_layer2_fixture() -> PlaquetteLayer:
     return PlaquetteLayer(
         QubitSpatialCubeTemplate(),
-        Plaquettes(FrozenDefaultDict({}, default_factory=empty_square_plaquette)),
+        Plaquettes(FrozenDefaultDict({}, default_value=empty_square_plaquette())),
     )
 
 
diff --git a/src/tqec/compile/blocks/layers/merge_test.py b/src/tqec/compile/blocks/layers/merge_test.py
index 8a41b9c6..1d38b054 100644
--- a/src/tqec/compile/blocks/layers/merge_test.py
+++ b/src/tqec/compile/blocks/layers/merge_test.py
@@ -35,11 +35,11 @@ def base_layers_fixture() -> list[BaseLayer]:
     return [
         PlaquetteLayer(
             QubitTemplate(),
-            Plaquettes(FrozenDefaultDict({}, default_factory=empty_square_plaquette)),
+            Plaquettes(FrozenDefaultDict({}, default_value=empty_square_plaquette())),
         ),
         PlaquetteLayer(
             QubitSpatialCubeTemplate(),
-            Plaquettes(FrozenDefaultDict({}, default_factory=empty_square_plaquette)),
+            Plaquettes(FrozenDefaultDict({}, default_value=empty_square_plaquette())),
         ),
         RawCircuitLayer(
             lambda k: ScheduledCircuit.from_circuit(stim.Circuit()),
diff --git a/src/tqec/compile/detectors/compute_test.py b/src/tqec/compile/detectors/compute_test.py
index 07fe917d..4c82a50e 100644
--- a/src/tqec/compile/detectors/compute_test.py
+++ b/src/tqec/compile/detectors/compute_test.py
@@ -142,7 +142,7 @@ def test_center_plaquette_syndrome_qubits_empty(
             empty_center_plaquette_subtemplate,
             Plaquettes(
                 FrozenDefaultDict(
-                    {}, default_factory=lambda: make_css_surface_code_plaquette("X")
+                    {}, default_value=make_css_surface_code_plaquette("X")
                 )
             ),
             Shift2D(2, 2),
@@ -154,7 +154,7 @@ def test_center_plaquette_syndrome_qubits_empty(
             empty_center_plaquette_subtemplate,
             Plaquettes(
                 FrozenDefaultDict(
-                    {}, default_factory=lambda: make_css_surface_code_plaquette("X")
+                    {}, default_value=make_css_surface_code_plaquette("X")
                 )
             ),
             Shift2D(4, 2),
@@ -176,7 +176,7 @@ def test_center_plaquette_syndrome_qubits(
         Plaquettes(
             FrozenDefaultDict(
                 {1: make_css_surface_code_plaquette("X")},
-                default_factory=lambda: empty_square_plaquette(),
+                default_value=empty_square_plaquette(),
             )
         ),
         Shift2D(2, 2),
@@ -186,7 +186,7 @@ def test_center_plaquette_syndrome_qubits(
         Plaquettes(
             FrozenDefaultDict(
                 {1: make_css_surface_code_plaquette("X")},
-                default_factory=lambda: empty_square_plaquette(),
+                default_value=empty_square_plaquette(),
             )
         ),
         Shift2D(4, 2),
diff --git a/src/tqec/compile/generation_test.py b/src/tqec/compile/generation_test.py
index b1be4be6..9552873e 100644
--- a/src/tqec/compile/generation_test.py
+++ b/src/tqec/compile/generation_test.py
@@ -15,7 +15,7 @@ def test_generate_circuit_one_plaquette() -> None:
     circuit = generate_circuit(
         FixedTemplate([[0]]),
         2,
-        Plaquettes(FrozenDefaultDict(default_factory=lambda: plaquette)),
+        Plaquettes(FrozenDefaultDict(default_value=plaquette)),
     )
     assert circuit.get_circuit() == stim.Circuit("""
 QUBIT_COORDS(0, 0) 0
diff --git a/src/tqec/compile/tree/annotators/detectors_test.py b/src/tqec/compile/tree/annotators/detectors_test.py
index 7397b589..56a3f8cd 100644
--- a/src/tqec/compile/tree/annotators/detectors_test.py
+++ b/src/tqec/compile/tree/annotators/detectors_test.py
@@ -17,7 +17,7 @@ def template_fixture() -> Template:
 
 @pytest.fixture(name="plaquettes")
 def plaquettes_fixture() -> Plaquettes:
-    return Plaquettes(FrozenDefaultDict({}, default_factory=empty_square_plaquette))
+    return Plaquettes(FrozenDefaultDict({}, default_value=empty_square_plaquette()))
 
 
 @pytest.fixture(name="measurement_records")
diff --git a/src/tqec/compile/tree/node_test.py b/src/tqec/compile/tree/node_test.py
index 3b4e826f..352a74b9 100644
--- a/src/tqec/compile/tree/node_test.py
+++ b/src/tqec/compile/tree/node_test.py
@@ -26,7 +26,7 @@ LOGICAL_QUBIT_SHAPE: Final = PhysicalQubitScalable2D(
 def plaquette_layer_fixture() -> PlaquetteLayer:
     template = QubitTemplate()
     plaquettes = Plaquettes(
-        FrozenDefaultDict({}, default_factory=empty_square_plaquette)
+        FrozenDefaultDict({}, default_value=empty_square_plaquette())
     )
     return PlaquetteLayer(template, plaquettes)
 
@@ -35,7 +35,7 @@ def plaquette_layer_fixture() -> PlaquetteLayer:
 def layout_layer_fixture() -> LayoutLayer:
     template = QubitTemplate()
     plaquettes = Plaquettes(
-        FrozenDefaultDict({}, default_factory=empty_square_plaquette)
+        FrozenDefaultDict({}, default_value=empty_square_plaquette())
     )
     return LayoutLayer(
         {