Repository: QCHackers/tqec
Pull request #305 (2024-08-27 12:43:32)

Unified measurement representation
This PR aims at fixing #302 by implementing once and for all a robust and efficient way of handling measurements.

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 | 79% | 0 | ✔
src.tqec.circuit.detectors | 94% | 0 | ✔
src.tqec.circuit.detectors.match_utils | 90% | 0 | ✔
src.tqec.circuit.operations | 68% | 0 | ➖
src.tqec.noise_models | 48% | 0 | ❌
src.tqec.plaquette | 83% | 0 | ✔
src.tqec.plaquette.library | 97% | 0 | ✔
src.tqec.plaquette.library.utils | 92% | 0 | ✔
src.tqec.sketchup | 78% | 0 | ✔
src.tqec.templates | 77% | 0 | ✔
src.tqec.templates.atomic | 86% | 0 | ✔
src.tqec.templates.constructions | 70% | 0 | ➖
**Summary** | **81%** (3042 / 3742) | **0** | ✔

<!-- Sticky Pull Request Comment -->
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 | 79% | 0 | ✔
src.tqec.circuit.detectors | 94% | 0 | ✔
src.tqec.circuit.detectors.match_utils | 90% | 0 | ✔
src.tqec.circuit.operations | 68% | 0 | ➖
src.tqec.noise_models | 48% | 0 | ❌
src.tqec.plaquette | 83% | 0 | ✔
src.tqec.plaquette.library | 97% | 0 | ✔
src.tqec.plaquette.library.utils | 92% | 0 | ✔
src.tqec.sketchup | 79% | 0 | ✔
src.tqec.templates | 77% | 0 | ✔
src.tqec.templates.atomic | 86% | 0 | ✔
src.tqec.templates.constructions | 70% | 0 | ➖
**Summary** | **81%** (3053 / 3754) | **0** | ✔

<!-- Sticky Pull Request Comment -->
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 | 79% | 0 | ✔
src.tqec.circuit.detectors | 94% | 0 | ✔
src.tqec.circuit.detectors.match_utils | 90% | 0 | ✔
src.tqec.circuit.operations | 68% | 0 | ➖
src.tqec.noise_models | 48% | 0 | ❌
src.tqec.plaquette | 83% | 0 | ✔
src.tqec.plaquette.library | 97% | 0 | ✔
src.tqec.plaquette.library.utils | 92% | 0 | ✔
src.tqec.sketchup | 79% | 0 | ✔
src.tqec.templates | 77% | 0 | ✔
src.tqec.templates.atomic | 86% | 0 | ✔
src.tqec.templates.constructions | 70% | 0 | ➖
**Summary** | **81%** (3053 / 3754) | **0** | ✔

<!-- Sticky Pull Request Comment -->
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 | 78% | 0 | ✔
src.tqec.circuit.detectors | 94% | 0 | ✔
src.tqec.circuit.detectors.match_utils | 90% | 0 | ✔
src.tqec.circuit.operations | 66% | 0 | ➖
src.tqec.noise_models | 48% | 0 | ❌
src.tqec.plaquette | 83% | 0 | ✔
src.tqec.plaquette.library | 97% | 0 | ✔
src.tqec.plaquette.library.utils | 92% | 0 | ✔
src.tqec.sketchup | 79% | 0 | ✔
src.tqec.templates | 77% | 0 | ✔
src.tqec.templates.atomic | 86% | 0 | ✔
src.tqec.templates.constructions | 70% | 0 | ➖
**Summary** | **81%** (3038 / 3741) | **0** | ✔

<!-- Sticky Pull Request Comment -->
nelimee: > Actually I didn't see too much difference between the new representation `Measurement` and the old `RelativeMeasurementData`. Maybe I have missed some important points and could you elaborate more on the benefits of the new representation?

I can see 2 benefits to the current representation:
- it is not a semi-private class (`RelativeMeasurementData` was public, but was used in a semi-private way in the code, with opportunities to provide `tuple` instead of instances of the class to avoid a user to have to use it) and so I find `Measurement` easier to use for users.
- there is `RepeatedMeasurement` that allows to represent measurements more efficiently and that interacts nicely with `Measurement`.

but in the end, I very much agree that `Measurement` can be obtained from copy-pasting `RelativeMeasurementData` to another file and renaming it.

> And one thing I still have problem with is that, since the `Measurement` includes the local offset, the same `Measurement` data structure can represent different actual measurements at the different time slices of the circuit. So it's not unique in that sense.

Yes, and this is by design. For a given circuit, `Measurement` is a way to represent a unique measurement. As soon as the circuit changes, the `Measurement` instances should be considered invalid. Basically, instances of the `Measurement` (and `RepeatedMeasurement`) class only make sense when they accompany a circuit and when the "origin" (i.e., the time slice in the circuit that is used for the relative negative offset) is clear (for simplicity, and because we likely do not want to keep track of that "origin", it is always defined as the last time slice of the circuit). 
nelimee: I wanted to have a try at implementing a better measurement map, but I will need some of the code introduced here in a follow-up PR for detector computation, so I guess this is ready to merge.
inmzhang: > I wanted to have a try at implementing a better measurement map, but I will need some of the code introduced here in a follow-up PR for detector computation, so I guess this is ready for review.

Are you going to improve the measurement map in a new PR? If it is, I can approve this one now to avoid blocking.
nelimee: I wanted to, but I think the Best way forward is to make that in another PR. 

Patch:
diff --git a/src/tqec/block/block.py b/src/tqec/block/block.py
index e0f2b7ab..762979b9 100644
--- a/src/tqec/block/block.py
+++ b/src/tqec/block/block.py
@@ -1,6 +1,6 @@
 from __future__ import annotations
 
-import typing
+import typing as ty
 from abc import ABC, abstractmethod
 from collections import defaultdict
 from copy import deepcopy
@@ -12,12 +12,14 @@ from typing_extensions import override
 
 from tqec.block.enums import BlockDimension
 from tqec.circuit.circuit import generate_circuit
+from tqec.circuit.operations.measurement import Measurement, RepeatedMeasurement
 from tqec.circuit.schedule import ScheduledCircuit, merge_scheduled_circuits
 from tqec.exceptions import TQECException
 from tqec.plaquette.library.empty import empty_square_plaquette
 from tqec.plaquette.plaquette import Plaquette, Plaquettes
 from tqec.position import Position3D
 from tqec.templates import Template
+from tqec.templates.interval import Interval
 from tqec.templates.scale import LinearFunction, round_or_fail
 
 _DEFAULT_BLOCK_REPETITIONS = LinearFunction(2, 1)
@@ -72,25 +74,7 @@ def _number_of_moments_needed(plaquettes: Plaquettes) -> int:
         the number of `cirq.Moment` needed to execute the provided plaquettes
         in parallel.
     """
-    all_plaquettes: typing.Iterable[Plaquette]
-    if isinstance(plaquettes, list):
-        all_plaquettes = plaquettes
-    elif isinstance(plaquettes, typing.Mapping):
-        all_plaquettes = plaquettes.values()
-        if (
-            isinstance(plaquettes, defaultdict)
-            and plaquettes.default_factory is not None
-        ):
-            all_plaquettes = list(all_plaquettes) + [plaquettes.default_factory()]
-    return max(max(p.circuit.schedule, default=0) for p in all_plaquettes)
-
-
-def _plaquette_dict(
-    plaquettes: Plaquettes,
-) -> dict[int, Plaquette] | defaultdict[int, Plaquette]:
-    if isinstance(plaquettes, (dict, defaultdict)):
-        return plaquettes
-    return {i: p for i, p in enumerate(plaquettes)}
+    return max(max(p.circuit.schedule, default=0) for p in plaquettes)
 
 
 @dataclass
@@ -110,7 +94,7 @@ class RepeatedPlaquettes:
         self, plaquettes_to_update: dict[int, Plaquette]
     ) -> RepeatedPlaquettes:
         return RepeatedPlaquettes(
-            _plaquette_dict(self.plaquettes) | plaquettes_to_update, self.repetitions
+            self.plaquettes | plaquettes_to_update, self.repetitions
         )
 
 
@@ -125,9 +109,9 @@ class TemporalPlaquetteSequence:
 
     def without_time_boundaries(self) -> TemporalPlaquetteSequence:
         return TemporalPlaquetteSequence(
-            defaultdict(empty_square_plaquette),
+            Plaquettes(defaultdict(empty_square_plaquette)),
             deepcopy(self.repeating_plaquettes),
-            defaultdict(empty_square_plaquette),
+            Plaquettes(defaultdict(empty_square_plaquette)),
         )
 
     def with_updated_plaquettes(
@@ -139,9 +123,9 @@ class TemporalPlaquetteSequence:
                 plaquettes_to_update
             )
         return TemporalPlaquetteSequence(
-            _plaquette_dict(self.initial_plaquettes) | plaquettes_to_update,
+            self.initial_plaquettes | plaquettes_to_update,
             repeating_plaquettes,
-            _plaquette_dict(self.final_plaquettes) | plaquettes_to_update,
+            self.final_plaquettes | plaquettes_to_update,
         )
 
 
@@ -172,7 +156,7 @@ class StandardComputationBlock(ComputationBlock):
     def __post_init__(self) -> None:
         expected_plaquette_number = self.template.expected_plaquettes_number
         if (
-            not isinstance(self.initial_plaquettes, defaultdict)
+            not self.initial_plaquettes.has_default
             and len(self.initial_plaquettes) != expected_plaquette_number
         ):
             raise TQECException(
@@ -181,7 +165,7 @@ class StandardComputationBlock(ComputationBlock):
                 "plaquettes."
             )
         if (
-            not isinstance(self.final_plaquettes, defaultdict)
+            not self.final_plaquettes.has_default
             and len(self.final_plaquettes) != expected_plaquette_number
         ):
             raise TQECException(
@@ -191,7 +175,7 @@ class StandardComputationBlock(ComputationBlock):
             )
         if (
             self.repeating_plaquettes is not None
-            and not isinstance(self.repeating_plaquettes.plaquettes, defaultdict)
+            and not self.repeating_plaquettes.plaquettes.has_default
             and len(self.repeating_plaquettes.plaquettes) != expected_plaquette_number
         ):
             raise TQECException(
@@ -208,10 +192,6 @@ class StandardComputationBlock(ComputationBlock):
     def repeating_plaquettes(self) -> RepeatedPlaquettes | None:
         return self.plaquettes.repeating_plaquettes
 
-    @repeating_plaquettes.setter
-    def repeating_plaquettes(self, value: RepeatedPlaquettes | None) -> None:
-        self.plaquettes.repeating_plaquettes = value
-
     @property
     def final_plaquettes(self) -> Plaquettes:
         return self.plaquettes.final_plaquettes
@@ -259,13 +239,15 @@ class StandardComputationBlock(ComputationBlock):
 
     @override
     def instantiate(self) -> cirq.Circuit:
-        circuit = generate_circuit(self.template, self.initial_plaquettes)
+        circuit = generate_circuit(self.template, self.initial_plaquettes.collection)
         if self.repeating_plaquettes is not None:
             repetitions = self.repeating_plaquettes.number_of_rounds(self.template.k)
             plaquettes = self.repeating_plaquettes.plaquettes
-            inner_circuit = generate_circuit(self.template, plaquettes).freeze()
+            inner_circuit = generate_circuit(
+                self.template, plaquettes.collection
+            ).freeze()
             circuit += cirq.CircuitOperation(inner_circuit, repetitions=repetitions)
-        circuit += generate_circuit(self.template, self.final_plaquettes)
+        circuit += generate_circuit(self.template, self.final_plaquettes.collection)
         return circuit
 
     @override
@@ -276,6 +258,78 @@ class StandardComputationBlock(ComputationBlock):
     def scale_to(self, k: int) -> None:
         self.template.scale_to(k)
 
+    @staticmethod
+    def _get_measurements(
+        template: Template, plaquettes: Plaquettes
+    ) -> ty.Iterator[Measurement]:
+        template_array = template.instantiate()
+        default_increments = template.get_increments()
+
+        for i, row in enumerate(template_array):
+            for j, plaquette_index in enumerate(row):
+                xoffset = j * default_increments.x
+                yoffset = i * default_increments.y
+                yield from (
+                    m.offset_spatially_by(xoffset, yoffset)
+                    for m in plaquettes[plaquette_index].measurements
+                )
+
+    @property
+    def measurements(self) -> frozenset[Measurement | RepeatedMeasurement]:
+        """Returns all the measurements in the block, relative to the end of
+        the block."""
+        measurement_number_by_qubits: dict[cirq.GridQubit, int] = {}
+        all_measurements: list[Measurement | RepeatedMeasurement] = []
+        # Start by the final measurements.
+        for final_measurement in self._get_measurements(
+            self.template, self.final_plaquettes
+        ):
+            all_measurements.append(final_measurement)
+            measurement_number_by_qubits[final_measurement.qubit] = 1
+        # Continue with the repeating measurements
+        if self.repeating_plaquettes is not None:
+            repetitions = self.repeating_plaquettes.number_of_rounds(self.template.k)
+            for repeating_measurement in self._get_measurements(
+                self.template, self.repeating_plaquettes.plaquettes
+            ):
+                qubit = repeating_measurement.qubit
+                past_measurement_number = measurement_number_by_qubits.get(qubit, 0)
+                all_measurements.append(
+                    RepeatedMeasurement(
+                        qubit,
+                        Interval(
+                            -past_measurement_number - repetitions,
+                            -past_measurement_number,
+                            start_excluded=False,
+                            end_excluded=True,
+                        ),
+                    )
+                )
+                measurement_number_by_qubits[repeating_measurement.qubit] = (
+                    past_measurement_number + repetitions
+                )
+        # Finish with the initial measurements
+        for initial_measurement in self._get_measurements(
+            self.template, self.initial_plaquettes
+        ):
+            qubit = repeating_measurement.qubit
+            past_measurement_number = measurement_number_by_qubits.get(qubit, 0)
+            all_measurements.append(
+                initial_measurement.offset_temporally_by(-past_measurement_number)
+            )
+
+        return frozenset(all_measurements)
+
+    @property
+    def all_measurements(self) -> list[Measurement]:
+        measurements: list[Measurement] = []
+        for m in self.measurements:
+            if isinstance(m, Measurement):
+                measurements.append(m)
+            else:  # isinstance(m, RepeatedMeasurement):
+                measurements.extend(m.measurements())
+        return measurements
+
 
 @dataclass
 class Computation:
diff --git a/src/tqec/block/library/closed.py b/src/tqec/block/library/closed.py
index a1b0398c..57cd61bc 100644
--- a/src/tqec/block/library/closed.py
+++ b/src/tqec/block/library/closed.py
@@ -29,58 +29,73 @@ from tqec.plaquette.library.memory import (
     zzzz_memory_plaquette,
 )
 from tqec.plaquette.library.pauli import MeasurementBasis, ResetBasis
+from tqec.plaquette.plaquette import Plaquettes
 from tqec.templates.qubit import QubitTemplate
-from tqec.templates.scale import LinearFunction
 
 
 def _zxB_block(basis: typing.Literal["X", "Z"]) -> StandardComputationBlock:
     reset_basis: ResetBasis = getattr(ResetBasis, basis)
     measurement_basis: MeasurementBasis = getattr(MeasurementBasis, basis)
 
-    initial_plaquettes = defaultdict(empty_square_plaquette) | {
-        6: xx_initialisation_plaquette(
-            PlaquetteOrientation.UP, data_qubit_reset_basis=reset_basis
-        ),
-        7: zz_initialisation_plaquette(
-            PlaquetteOrientation.LEFT, data_qubit_reset_basis=reset_basis
-        ),
-        9: xxxx_initialisation_plaquette(data_qubit_reset_basis=reset_basis),
-        10: zzzz_initialisation_plaquette(data_qubit_reset_basis=reset_basis),
-        12: zz_initialisation_plaquette(
-            PlaquetteOrientation.RIGHT, data_qubit_reset_basis=reset_basis
-        ),
-        13: xx_initialisation_plaquette(
-            PlaquetteOrientation.DOWN, data_qubit_reset_basis=reset_basis
-        ),
-    }
-    repeating_plaquettes = RepeatedPlaquettes(
+    initial_plaquettes = Plaquettes(
         defaultdict(empty_square_plaquette)
         | {
-            6: xx_memory_plaquette(PlaquetteOrientation.UP),
-            7: zz_memory_plaquette(PlaquetteOrientation.LEFT),
-            9: xxxx_memory_plaquette(),
-            10: zzzz_memory_plaquette(),
-            12: zz_memory_plaquette(PlaquetteOrientation.RIGHT),
-            13: xx_memory_plaquette(PlaquetteOrientation.DOWN),
-        },
-        _DEFAULT_BLOCK_REPETITIONS,
+            6: xx_initialisation_plaquette(
+                PlaquetteOrientation.UP, data_qubit_reset_basis=reset_basis
+            ),
+            7: zz_initialisation_plaquette(
+                PlaquetteOrientation.LEFT, data_qubit_reset_basis=reset_basis
+            ),
+            9: xxxx_initialisation_plaquette(data_qubit_reset_basis=reset_basis),
+            10: zzzz_initialisation_plaquette(data_qubit_reset_basis=reset_basis),
+            12: zz_initialisation_plaquette(
+                PlaquetteOrientation.RIGHT, data_qubit_reset_basis=reset_basis
+            ),
+            13: xx_initialisation_plaquette(
+                PlaquetteOrientation.DOWN, data_qubit_reset_basis=reset_basis
+            ),
+        }
     )
-    final_plaquettes = defaultdict(empty_square_plaquette) | {
-        6: xx_measurement_plaquette(
-            PlaquetteOrientation.UP, data_qubit_measurement_basis=measurement_basis
-        ),
-        7: zz_measurement_plaquette(
-            PlaquetteOrientation.LEFT, data_qubit_measurement_basis=measurement_basis
-        ),
-        9: xxxx_measurement_plaquette(data_qubit_measurement_basis=measurement_basis),
-        10: zzzz_measurement_plaquette(data_qubit_measurement_basis=measurement_basis),
-        12: zz_measurement_plaquette(
-            PlaquetteOrientation.RIGHT, data_qubit_measurement_basis=measurement_basis
-        ),
-        13: xx_measurement_plaquette(
-            PlaquetteOrientation.DOWN, data_qubit_measurement_basis=measurement_basis
+    repeating_plaquettes = RepeatedPlaquettes(
+        Plaquettes(
+            defaultdict(empty_square_plaquette)
+            | {
+                6: xx_memory_plaquette(PlaquetteOrientation.UP),
+                7: zz_memory_plaquette(PlaquetteOrientation.LEFT),
+                9: xxxx_memory_plaquette(),
+                10: zzzz_memory_plaquette(),
+                12: zz_memory_plaquette(PlaquetteOrientation.RIGHT),
+                13: xx_memory_plaquette(PlaquetteOrientation.DOWN),
+            }
         ),
-    }
+        _DEFAULT_BLOCK_REPETITIONS,
+    )
+    final_plaquettes = Plaquettes(
+        defaultdict(empty_square_plaquette)
+        | {
+            6: xx_measurement_plaquette(
+                PlaquetteOrientation.UP, data_qubit_measurement_basis=measurement_basis
+            ),
+            7: zz_measurement_plaquette(
+                PlaquetteOrientation.LEFT,
+                data_qubit_measurement_basis=measurement_basis,
+            ),
+            9: xxxx_measurement_plaquette(
+                data_qubit_measurement_basis=measurement_basis
+            ),
+            10: zzzz_measurement_plaquette(
+                data_qubit_measurement_basis=measurement_basis
+            ),
+            12: zz_measurement_plaquette(
+                PlaquetteOrientation.RIGHT,
+                data_qubit_measurement_basis=measurement_basis,
+            ),
+            13: xx_measurement_plaquette(
+                PlaquetteOrientation.DOWN,
+                data_qubit_measurement_basis=measurement_basis,
+            ),
+        }
+    )
     return StandardComputationBlock(
         QubitTemplate(),
         TemporalPlaquetteSequence(
@@ -93,50 +108,65 @@ def _xzB_block(basis: typing.Literal["X", "Z"]) -> StandardComputationBlock:
     reset_basis: ResetBasis = getattr(ResetBasis, basis)
     measurement_basis: MeasurementBasis = getattr(MeasurementBasis, basis)
 
-    initial_plaquettes = defaultdict(empty_square_plaquette) | {
-        6: zz_initialisation_plaquette(
-            PlaquetteOrientation.UP, data_qubit_reset_basis=reset_basis
-        ),
-        7: xx_initialisation_plaquette(
-            PlaquetteOrientation.LEFT, data_qubit_reset_basis=reset_basis
-        ),
-        9: zzzz_initialisation_plaquette(data_qubit_reset_basis=reset_basis),
-        10: xxxx_initialisation_plaquette(data_qubit_reset_basis=reset_basis),
-        12: xx_initialisation_plaquette(
-            PlaquetteOrientation.RIGHT, data_qubit_reset_basis=reset_basis
-        ),
-        13: zz_initialisation_plaquette(
-            PlaquetteOrientation.DOWN, data_qubit_reset_basis=reset_basis
-        ),
-    }
-    repeating_plaquettes = RepeatedPlaquettes(
+    initial_plaquettes = Plaquettes(
         defaultdict(empty_square_plaquette)
         | {
-            6: zz_memory_plaquette(PlaquetteOrientation.UP),
-            7: xx_memory_plaquette(PlaquetteOrientation.LEFT),
-            9: zzzz_memory_plaquette(),
-            10: xxxx_memory_plaquette(),
-            12: xx_memory_plaquette(PlaquetteOrientation.RIGHT),
-            13: zz_memory_plaquette(PlaquetteOrientation.DOWN),
-        },
-        _DEFAULT_BLOCK_REPETITIONS,
+            6: zz_initialisation_plaquette(
+                PlaquetteOrientation.UP, data_qubit_reset_basis=reset_basis
+            ),
+            7: xx_initialisation_plaquette(
+                PlaquetteOrientation.LEFT, data_qubit_reset_basis=reset_basis
+            ),
+            9: zzzz_initialisation_plaquette(data_qubit_reset_basis=reset_basis),
+            10: xxxx_initialisation_plaquette(data_qubit_reset_basis=reset_basis),
+            12: xx_initialisation_plaquette(
+                PlaquetteOrientation.RIGHT, data_qubit_reset_basis=reset_basis
+            ),
+            13: zz_initialisation_plaquette(
+                PlaquetteOrientation.DOWN, data_qubit_reset_basis=reset_basis
+            ),
+        }
     )
-    final_plaquettes = defaultdict(empty_square_plaquette) | {
-        6: zz_measurement_plaquette(
-            PlaquetteOrientation.UP, data_qubit_measurement_basis=measurement_basis
-        ),
-        7: xx_measurement_plaquette(
-            PlaquetteOrientation.LEFT, data_qubit_measurement_basis=measurement_basis
-        ),
-        9: zzzz_measurement_plaquette(data_qubit_measurement_basis=measurement_basis),
-        10: xxxx_measurement_plaquette(data_qubit_measurement_basis=measurement_basis),
-        12: xx_measurement_plaquette(
-            PlaquetteOrientation.RIGHT, data_qubit_measurement_basis=measurement_basis
-        ),
-        13: zz_measurement_plaquette(
-            PlaquetteOrientation.DOWN, data_qubit_measurement_basis=measurement_basis
+    repeating_plaquettes = RepeatedPlaquettes(
+        Plaquettes(
+            defaultdict(empty_square_plaquette)
+            | {
+                6: zz_memory_plaquette(PlaquetteOrientation.UP),
+                7: xx_memory_plaquette(PlaquetteOrientation.LEFT),
+                9: zzzz_memory_plaquette(),
+                10: xxxx_memory_plaquette(),
+                12: xx_memory_plaquette(PlaquetteOrientation.RIGHT),
+                13: zz_memory_plaquette(PlaquetteOrientation.DOWN),
+            }
         ),
-    }
+        _DEFAULT_BLOCK_REPETITIONS,
+    )
+    final_plaquettes = Plaquettes(
+        defaultdict(empty_square_plaquette)
+        | {
+            6: zz_measurement_plaquette(
+                PlaquetteOrientation.UP, data_qubit_measurement_basis=measurement_basis
+            ),
+            7: xx_measurement_plaquette(
+                PlaquetteOrientation.LEFT,
+                data_qubit_measurement_basis=measurement_basis,
+            ),
+            9: zzzz_measurement_plaquette(
+                data_qubit_measurement_basis=measurement_basis
+            ),
+            10: xxxx_measurement_plaquette(
+                data_qubit_measurement_basis=measurement_basis
+            ),
+            12: xx_measurement_plaquette(
+                PlaquetteOrientation.RIGHT,
+                data_qubit_measurement_basis=measurement_basis,
+            ),
+            13: zz_measurement_plaquette(
+                PlaquetteOrientation.DOWN,
+                data_qubit_measurement_basis=measurement_basis,
+            ),
+        }
+    )
     return StandardComputationBlock(
         QubitTemplate(),
         TemporalPlaquetteSequence(
diff --git a/src/tqec/block/library/open.py b/src/tqec/block/library/open.py
index 06a6a99a..fbf3cddf 100644
--- a/src/tqec/block/library/open.py
+++ b/src/tqec/block/library/open.py
@@ -17,7 +17,7 @@ from tqec.plaquette.library.memory import (
     zzzz_memory_plaquette,
 )
 from tqec.plaquette.library.pauli import MeasurementBasis, ResetBasis
-from tqec.plaquette.plaquette import Plaquette
+from tqec.plaquette.plaquette import Plaquette, Plaquettes
 from tqec.templates.qubit import (
     QubitHorizontalBorders,
     QubitTemplate,
@@ -56,48 +56,66 @@ def _with_measurements_on_data_qubits_on_side(
 
 
 def ozx_block() -> StandardComputationBlock:
-    initial_plaquettes = defaultdict(empty_square_plaquette) | {
-        2: _with_resets_on_data_qubits_on_side(
-            zzzz_memory_plaquette(), PlaquetteSide.RIGHT, reset_basis=ResetBasis.X
-        ),
-        3: xxxx_memory_plaquette(),
-        4: _with_resets_on_data_qubits_on_side(
-            zz_memory_plaquette(PlaquetteOrientation.DOWN),
-            PlaquetteSide.RIGHT,
-            reset_basis=ResetBasis.X,
-        ),
-        5: zz_memory_plaquette(PlaquetteOrientation.UP),
-        6: xxxx_memory_plaquette(),
-        7: zzzz_memory_plaquette(),
-    }
-    repeating_plaquettes = RepeatedPlaquettes(
+    initial_plaquettes = Plaquettes(
         defaultdict(empty_square_plaquette)
         | {
-            2: zzzz_memory_plaquette(),
+            2: _with_resets_on_data_qubits_on_side(
+                zzzz_memory_plaquette(),
+                PlaquetteSide.RIGHT,
+                reset_basis=ResetBasis.X,
+            ),
             3: xxxx_memory_plaquette(),
-            4: zz_memory_plaquette(PlaquetteOrientation.DOWN),
+            4: _with_resets_on_data_qubits_on_side(
+                zz_memory_plaquette(PlaquetteOrientation.DOWN),
+                PlaquetteSide.RIGHT,
+                reset_basis=ResetBasis.X,
+            ),
             5: zz_memory_plaquette(PlaquetteOrientation.UP),
             6: xxxx_memory_plaquette(),
             7: zzzz_memory_plaquette(),
-        },
-        _DEFAULT_BLOCK_REPETITIONS,
+        }
     )
-    final_plaquettes = defaultdict(empty_square_plaquette) | {
-        2: _with_measurements_on_data_qubits_on_side(
-            zzzz_memory_plaquette(),
-            PlaquetteSide.RIGHT,
-            measurement_basis=MeasurementBasis.X,
-        ),
-        3: xxxx_memory_plaquette(),
-        4: _with_measurements_on_data_qubits_on_side(
-            zz_memory_plaquette(PlaquetteOrientation.DOWN),
-            PlaquetteSide.RIGHT,
-            measurement_basis=MeasurementBasis.X,
+    repeating_plaquettes = RepeatedPlaquettes(
+        Plaquettes(
+            defaultdict(empty_square_plaquette)
+            | {
+                2: _with_resets_on_data_qubits_on_side(
+                    zzzz_memory_plaquette(),
+                    PlaquetteSide.RIGHT,
+                    reset_basis=ResetBasis.X,
+                ),
+                3: xxxx_memory_plaquette(),
+                4: _with_resets_on_data_qubits_on_side(
+                    zz_memory_plaquette(PlaquetteOrientation.DOWN),
+                    PlaquetteSide.RIGHT,
+                    reset_basis=ResetBasis.X,
+                ),
+                5: zz_memory_plaquette(PlaquetteOrientation.UP),
+                6: xxxx_memory_plaquette(),
+                7: zzzz_memory_plaquette(),
+            }
         ),
-        5: zz_memory_plaquette(PlaquetteOrientation.UP),
-        6: xxxx_memory_plaquette(),
-        7: zzzz_memory_plaquette(),
-    }
+        _DEFAULT_BLOCK_REPETITIONS,
+    )
+    final_plaquettes = Plaquettes(
+        defaultdict(empty_square_plaquette)
+        | {
+            2: _with_measurements_on_data_qubits_on_side(
+                zzzz_memory_plaquette(),
+                PlaquetteSide.RIGHT,
+                measurement_basis=MeasurementBasis.X,
+            ),
+            3: xxxx_memory_plaquette(),
+            4: _with_measurements_on_data_qubits_on_side(
+                zz_memory_plaquette(PlaquetteOrientation.DOWN),
+                PlaquetteSide.RIGHT,
+                measurement_basis=MeasurementBasis.X,
+            ),
+            5: zz_memory_plaquette(PlaquetteOrientation.UP),
+            6: xxxx_memory_plaquette(),
+            7: zzzz_memory_plaquette(),
+        }
+    )
     return StandardComputationBlock(
         QubitVerticalBorders(),
         TemporalPlaquetteSequence(
@@ -107,43 +125,56 @@ def ozx_block() -> StandardComputationBlock:
 
 
 def oxz_block() -> StandardComputationBlock:
-    initial_plaquettes = defaultdict(empty_square_plaquette) | {
-        2: _with_resets_on_data_qubits_on_side(
-            xxxx_memory_plaquette(), PlaquetteSide.RIGHT
-        ),
-        3: zzzz_memory_plaquette(),
-        4: _with_resets_on_data_qubits_on_side(
-            xx_memory_plaquette(PlaquetteOrientation.DOWN),
-            PlaquetteSide.RIGHT,
-        ),
-        5: xx_memory_plaquette(PlaquetteOrientation.UP),
-        6: zzzz_memory_plaquette(),
-        7: xxxx_memory_plaquette(),
-    }
-    repeating_plaquettes = RepeatedPlaquettes(
+    initial_plaquettes = Plaquettes(
         defaultdict(empty_square_plaquette)
         | {
-            2: xxxx_memory_plaquette(),
+            2: _with_resets_on_data_qubits_on_side(
+                xxxx_memory_plaquette(), PlaquetteSide.RIGHT
+            ),
             3: zzzz_memory_plaquette(),
-            4: xx_memory_plaquette(PlaquetteOrientation.DOWN),
+            4: _with_resets_on_data_qubits_on_side(
+                xx_memory_plaquette(PlaquetteOrientation.DOWN),
+                PlaquetteSide.RIGHT,
+            ),
             5: xx_memory_plaquette(PlaquetteOrientation.UP),
             6: zzzz_memory_plaquette(),
             7: xxxx_memory_plaquette(),
-        },
-        _DEFAULT_BLOCK_REPETITIONS,
+        }
     )
-    final_plaquettes = defaultdict(empty_square_plaquette) | {
-        2: _with_measurements_on_data_qubits_on_side(
-            xxxx_memory_plaquette(), PlaquetteSide.RIGHT
-        ),
-        3: zzzz_memory_plaquette(),
-        4: _with_measurements_on_data_qubits_on_side(
-            xx_memory_plaquette(PlaquetteOrientation.DOWN), PlaquetteSide.RIGHT
+    repeating_plaquettes = RepeatedPlaquettes(
+        Plaquettes(
+            defaultdict(empty_square_plaquette)
+            | {
+                2: _with_resets_on_data_qubits_on_side(
+                    xxxx_memory_plaquette(), PlaquetteSide.RIGHT
+                ),
+                3: zzzz_memory_plaquette(),
+                4: _with_resets_on_data_qubits_on_side(
+                    xx_memory_plaquette(PlaquetteOrientation.DOWN),
+                    PlaquetteSide.RIGHT,
+                ),
+                5: xx_memory_plaquette(PlaquetteOrientation.UP),
+                6: zzzz_memory_plaquette(),
+                7: xxxx_memory_plaquette(),
+            }
         ),
-        5: xx_memory_plaquette(PlaquetteOrientation.UP),
-        6: zzzz_memory_plaquette(),
-        7: xxxx_memory_plaquette(),
-    }
+        _DEFAULT_BLOCK_REPETITIONS,
+    )
+    final_plaquettes = Plaquettes(
+        defaultdict(empty_square_plaquette)
+        | {
+            2: _with_measurements_on_data_qubits_on_side(
+                xxxx_memory_plaquette(), PlaquetteSide.RIGHT
+            ),
+            3: zzzz_memory_plaquette(),
+            4: _with_measurements_on_data_qubits_on_side(
+                xx_memory_plaquette(PlaquetteOrientation.DOWN), PlaquetteSide.RIGHT
+            ),
+            5: xx_memory_plaquette(PlaquetteOrientation.UP),
+            6: zzzz_memory_plaquette(),
+            7: xxxx_memory_plaquette(),
+        }
+    )
     return StandardComputationBlock(
         QubitVerticalBorders(),
         TemporalPlaquetteSequence(
@@ -153,46 +184,60 @@ def oxz_block() -> StandardComputationBlock:
 
 
 def xoz_block() -> StandardComputationBlock:
-    initial_plaquettes = defaultdict(empty_square_plaquette) | {
-        1: _with_resets_on_data_qubits_on_side(
-            xx_memory_plaquette(PlaquetteOrientation.LEFT),
-            PlaquetteSide.DOWN,
-        ),
-        2: zzzz_memory_plaquette(),
-        3: _with_resets_on_data_qubits_on_side(
-            xxxx_memory_plaquette(),
-            PlaquetteSide.DOWN,
-        ),
-        6: xxxx_memory_plaquette(),
-        7: zzzz_memory_plaquette(),
-        8: xx_memory_plaquette(PlaquetteOrientation.RIGHT),
-    }
-    repeating_plaquettes = RepeatedPlaquettes(
+    initial_plaquettes = Plaquettes(
         defaultdict(empty_square_plaquette)
         | {
-            1: xx_memory_plaquette(PlaquetteOrientation.LEFT),
+            1: _with_resets_on_data_qubits_on_side(
+                xx_memory_plaquette(PlaquetteOrientation.LEFT),
+                PlaquetteSide.DOWN,
+            ),
             2: zzzz_memory_plaquette(),
-            3: xxxx_memory_plaquette(),
+            3: _with_resets_on_data_qubits_on_side(
+                xxxx_memory_plaquette(),
+                PlaquetteSide.DOWN,
+            ),
             6: xxxx_memory_plaquette(),
             7: zzzz_memory_plaquette(),
             8: xx_memory_plaquette(PlaquetteOrientation.RIGHT),
-        },
-        _DEFAULT_BLOCK_REPETITIONS,
+        }
     )
-    final_plaquettes = defaultdict(empty_square_plaquette) | {
-        1: _with_measurements_on_data_qubits_on_side(
-            xx_memory_plaquette(PlaquetteOrientation.LEFT),
-            PlaquetteSide.DOWN,
-        ),
-        2: zzzz_memory_plaquette(),
-        3: _with_measurements_on_data_qubits_on_side(
-            xxxx_memory_plaquette(),
-            PlaquetteSide.DOWN,
+    repeating_plaquettes = RepeatedPlaquettes(
+        Plaquettes(
+            defaultdict(empty_square_plaquette)
+            | {
+                1: _with_resets_on_data_qubits_on_side(
+                    xx_memory_plaquette(PlaquetteOrientation.LEFT),
+                    PlaquetteSide.DOWN,
+                ),
+                2: zzzz_memory_plaquette(),
+                3: _with_resets_on_data_qubits_on_side(
+                    xxxx_memory_plaquette(),
+                    PlaquetteSide.DOWN,
+                ),
+                6: xxxx_memory_plaquette(),
+                7: zzzz_memory_plaquette(),
+                8: xx_memory_plaquette(PlaquetteOrientation.RIGHT),
+            }
         ),
-        6: xxxx_memory_plaquette(),
-        7: zzzz_memory_plaquette(),
-        8: xx_memory_plaquette(PlaquetteOrientation.RIGHT),
-    }
+        _DEFAULT_BLOCK_REPETITIONS,
+    )
+    final_plaquettes = Plaquettes(
+        defaultdict(empty_square_plaquette)
+        | {
+            1: _with_measurements_on_data_qubits_on_side(
+                xx_memory_plaquette(PlaquetteOrientation.LEFT),
+                PlaquetteSide.DOWN,
+            ),
+            2: zzzz_memory_plaquette(),
+            3: _with_measurements_on_data_qubits_on_side(
+                xxxx_memory_plaquette(),
+                PlaquetteSide.DOWN,
+            ),
+            6: xxxx_memory_plaquette(),
+            7: zzzz_memory_plaquette(),
+            8: xx_memory_plaquette(PlaquetteOrientation.RIGHT),
+        }
+    )
     return StandardComputationBlock(
         QubitHorizontalBorders(),
         TemporalPlaquetteSequence(
@@ -202,48 +247,64 @@ def xoz_block() -> StandardComputationBlock:
 
 
 def zox_block() -> StandardComputationBlock:
-    initial_plaquettes = defaultdict(empty_square_plaquette) | {
-        1: _with_resets_on_data_qubits_on_side(
-            zz_memory_plaquette(PlaquetteOrientation.LEFT),
-            PlaquetteSide.DOWN,
-            reset_basis=ResetBasis.X,
-        ),
-        2: xxxx_memory_plaquette(),
-        3: _with_resets_on_data_qubits_on_side(
-            zzzz_memory_plaquette(), PlaquetteSide.DOWN, reset_basis=ResetBasis.X
-        ),
-        6: zzzz_memory_plaquette(),
-        7: xxxx_memory_plaquette(),
-        8: zz_memory_plaquette(PlaquetteOrientation.RIGHT),
-    }
-    repeating_plaquettes = RepeatedPlaquettes(
+    initial_plaquettes = Plaquettes(
         defaultdict(empty_square_plaquette)
         | {
-            1: zz_memory_plaquette(PlaquetteOrientation.LEFT),
+            1: _with_resets_on_data_qubits_on_side(
+                zz_memory_plaquette(PlaquetteOrientation.LEFT),
+                PlaquetteSide.DOWN,
+                reset_basis=ResetBasis.X,
+            ),
             2: xxxx_memory_plaquette(),
-            3: zzzz_memory_plaquette(),
+            3: _with_resets_on_data_qubits_on_side(
+                zzzz_memory_plaquette(), PlaquetteSide.DOWN, reset_basis=ResetBasis.X
+            ),
             6: zzzz_memory_plaquette(),
             7: xxxx_memory_plaquette(),
             8: zz_memory_plaquette(PlaquetteOrientation.RIGHT),
-        },
-        _DEFAULT_BLOCK_REPETITIONS,
+        }
     )
-    final_plaquettes = defaultdict(empty_square_plaquette) | {
-        1: _with_measurements_on_data_qubits_on_side(
-            zz_memory_plaquette(PlaquetteOrientation.LEFT),
-            PlaquetteSide.DOWN,
-            measurement_basis=MeasurementBasis.X,
-        ),
-        2: xxxx_memory_plaquette(),
-        3: _with_measurements_on_data_qubits_on_side(
-            zzzz_memory_plaquette(),
-            PlaquetteSide.DOWN,
-            measurement_basis=MeasurementBasis.X,
+    repeating_plaquettes = RepeatedPlaquettes(
+        Plaquettes(
+            defaultdict(empty_square_plaquette)
+            | {
+                1: _with_resets_on_data_qubits_on_side(
+                    zz_memory_plaquette(PlaquetteOrientation.LEFT),
+                    PlaquetteSide.DOWN,
+                    reset_basis=ResetBasis.X,
+                ),
+                2: xxxx_memory_plaquette(),
+                3: _with_resets_on_data_qubits_on_side(
+                    zzzz_memory_plaquette(),
+                    PlaquetteSide.DOWN,
+                    reset_basis=ResetBasis.X,
+                ),
+                6: zzzz_memory_plaquette(),
+                7: xxxx_memory_plaquette(),
+                8: zz_memory_plaquette(PlaquetteOrientation.RIGHT),
+            }
         ),
-        6: zzzz_memory_plaquette(),
-        7: xxxx_memory_plaquette(),
-        8: zz_memory_plaquette(PlaquetteOrientation.RIGHT),
-    }
+        _DEFAULT_BLOCK_REPETITIONS,
+    )
+    final_plaquettes = Plaquettes(
+        defaultdict(empty_square_plaquette)
+        | {
+            1: _with_measurements_on_data_qubits_on_side(
+                zz_memory_plaquette(PlaquetteOrientation.LEFT),
+                PlaquetteSide.DOWN,
+                measurement_basis=MeasurementBasis.X,
+            ),
+            2: xxxx_memory_plaquette(),
+            3: _with_measurements_on_data_qubits_on_side(
+                zzzz_memory_plaquette(),
+                PlaquetteSide.DOWN,
+                measurement_basis=MeasurementBasis.X,
+            ),
+            6: zzzz_memory_plaquette(),
+            7: xxxx_memory_plaquette(),
+            8: zz_memory_plaquette(PlaquetteOrientation.RIGHT),
+        }
+    )
     return StandardComputationBlock(
         QubitHorizontalBorders(),
         TemporalPlaquetteSequence(
@@ -253,14 +314,17 @@ def zox_block() -> StandardComputationBlock:
 
 
 def xzo_block() -> StandardComputationBlock:
-    initial_plaquettes = final_plaquettes = defaultdict(empty_square_plaquette) | {
-        6: zz_memory_plaquette(PlaquetteOrientation.UP),
-        7: xx_memory_plaquette(PlaquetteOrientation.LEFT),
-        9: zzzz_memory_plaquette(),
-        10: xxxx_memory_plaquette(),
-        12: xx_memory_plaquette(PlaquetteOrientation.RIGHT),
-        13: zz_memory_plaquette(PlaquetteOrientation.DOWN),
-    }
+    initial_plaquettes = final_plaquettes = Plaquettes(
+        defaultdict(empty_square_plaquette)
+        | {
+            6: zz_memory_plaquette(PlaquetteOrientation.UP),
+            7: xx_memory_plaquette(PlaquetteOrientation.LEFT),
+            9: zzzz_memory_plaquette(),
+            10: xxxx_memory_plaquette(),
+            12: xx_memory_plaquette(PlaquetteOrientation.RIGHT),
+            13: zz_memory_plaquette(PlaquetteOrientation.DOWN),
+        }
+    )
     return StandardComputationBlock(
         QubitTemplate(),
         TemporalPlaquetteSequence(initial_plaquettes, None, final_plaquettes),
@@ -268,14 +332,17 @@ def xzo_block() -> StandardComputationBlock:
 
 
 def zxo_block() -> StandardComputationBlock:
-    initial_plaquettes = final_plaquettes = defaultdict(empty_square_plaquette) | {
-        6: xx_memory_plaquette(PlaquetteOrientation.UP),
-        7: zz_memory_plaquette(PlaquetteOrientation.LEFT),
-        9: xxxx_memory_plaquette(),
-        10: zzzz_memory_plaquette(),
-        12: zz_memory_plaquette(PlaquetteOrientation.RIGHT),
-        13: xx_memory_plaquette(PlaquetteOrientation.DOWN),
-    }
+    initial_plaquettes = final_plaquettes = Plaquettes(
+        defaultdict(empty_square_plaquette)
+        | {
+            6: xx_memory_plaquette(PlaquetteOrientation.UP),
+            7: zz_memory_plaquette(PlaquetteOrientation.LEFT),
+            9: xxxx_memory_plaquette(),
+            10: zzzz_memory_plaquette(),
+            12: zz_memory_plaquette(PlaquetteOrientation.RIGHT),
+            13: xx_memory_plaquette(PlaquetteOrientation.DOWN),
+        }
+    )
     return StandardComputationBlock(
         QubitTemplate(),
         TemporalPlaquetteSequence(initial_plaquettes, None, final_plaquettes),
diff --git a/src/tqec/circuit/__init__.py b/src/tqec/circuit/__init__.py
index 7a29a2d5..cc47dd94 100644
--- a/src/tqec/circuit/__init__.py
+++ b/src/tqec/circuit/__init__.py
@@ -1,6 +1,5 @@
 from .circuit import generate_circuit
 from .operations import (
-    RelativeMeasurementData,
     make_detector,
     make_observable,
     make_shift_coords,
diff --git a/src/tqec/circuit/circuit.py b/src/tqec/circuit/circuit.py
index 9234e127..6f25930c 100644
--- a/src/tqec/circuit/circuit.py
+++ b/src/tqec/circuit/circuit.py
@@ -8,14 +8,14 @@ import cirq
 
 from tqec.circuit.schedule import ScheduledCircuit, merge_scheduled_circuits
 from tqec.exceptions import TQECException
-from tqec.plaquette.plaquette import Plaquette, Plaquettes
+from tqec.plaquette.plaquette import Plaquette
 from tqec.position import Displacement
 from tqec.templates.base import Template
 
 
 def generate_circuit(
     template: Template,
-    plaquettes: Plaquettes,
+    plaquettes: list[Plaquette] | dict[int, Plaquette] | defaultdict[int, Plaquette],
 ) -> cirq.Circuit:
     """Generate a quantum circuit from a template and its plaquettes.
 
diff --git a/src/tqec/circuit/detectors/construction.py b/src/tqec/circuit/detectors/construction.py
index aaa72014..fe0df9b1 100644
--- a/src/tqec/circuit/detectors/construction.py
+++ b/src/tqec/circuit/detectors/construction.py
@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 import stim
+
 from tqec.circuit.detectors.flow import build_flows_from_fragments
 from tqec.circuit.detectors.fragment import (
     Fragment,
diff --git a/src/tqec/circuit/operations/__init__.py b/src/tqec/circuit/operations/__init__.py
index c30c87f8..136cd0e6 100644
--- a/src/tqec/circuit/operations/__init__.py
+++ b/src/tqec/circuit/operations/__init__.py
@@ -1,10 +1,2 @@
-from .operation import (
-    RelativeMeasurementData,
-    make_shift_coords,
-    make_detector,
-    make_observable,
-)
-
-from .transformer import (
-    transform_to_stimcirq_compatible,
-)
+from .operation import make_detector, make_observable, make_shift_coords
+from .transformer import transform_to_stimcirq_compatible
diff --git a/src/tqec/circuit/operations/measurement.py b/src/tqec/circuit/operations/measurement.py
new file mode 100644
index 00000000..d734cb7d
--- /dev/null
+++ b/src/tqec/circuit/operations/measurement.py
@@ -0,0 +1,141 @@
+from __future__ import annotations
+
+import typing
+from collections import defaultdict
+from dataclasses import dataclass
+
+import cirq
+
+from tqec.exceptions import TQECException
+from tqec.templates.interval import Interval, Rplus_interval
+
+
+@dataclass(frozen=True)
+class Measurement:
+    """A unique representation for each measurement in a quantum circuit.
+
+    This class aims at being able to represent measurements in a quantum circuit
+    in a unique and easily usable way.
+
+    Note:
+        This is not a global representation as the `offset` is always
+        relative to the end of the quantum circuit considered.
+
+    Attributes:
+        qubit: qubit on which the represented measurement is performed.
+        offset: negative offset representing the number of measurements
+            performed on the provided qubit after the represented measurement.
+            A value of `-1` means that the represented measurement is the
+            last one applied on `qubit`.
+
+    Raises:
+        TQECException: if the provided `offset` is not strictly negative.
+    """
+
+    qubit: cirq.GridQubit
+    offset: int
+
+    def __post_init__(self) -> None:
+        if self.offset >= 0:
+            raise TQECException("Measurement.offset should be negative.")
+
+    def offset_spatially_by(self, x: int, y: int) -> Measurement:
+        return Measurement(self.qubit + (y, x), self.offset)
+
+    def offset_temporally_by(self, t: int) -> Measurement:
+        return Measurement(self.qubit, self.offset + t)
+
+    def __lt__(self, other: object) -> bool:
+        if not isinstance(other, Measurement):
+            raise NotImplementedError(f"Cannot compare {type(self)} < {type(other)}.")
+        if self.offset == other.offset:
+            return self.qubit < other.qubit
+        return self.offset < other.offset
+
+    def __repr__(self) -> str:
+        return f"{self.__class__.__name__}({self.qubit}, {self.offset})"
+
+    def map_qubit(
+        self, qubit_map: typing.Mapping[cirq.GridQubit, cirq.GridQubit]
+    ) -> Measurement:
+        return Measurement(qubit_map[self.qubit], self.offset)
+
+
+@dataclass(frozen=True)
+class RepeatedMeasurement:
+    """A unique representation for a repeated measurement in a quantum circuit.
+
+    This class aims at being able to represent a repeated measurement in a
+    quantum circuit in a unique and easily usable way.
+    Repeated measurements can be found when a :class:`stim.CircuitRepeatBlock`
+    contains measurements.
+
+    Note:
+        This is not a global representation as the `offsets` is always
+        relative to the end of the quantum circuit considered.
+
+    Attributes:
+        qubit: qubit on which the represented repeated measurement is performed.
+        offsets: an interval only containing negative offsets representing the
+            number of measurements performed on the provided qubit after the
+            represented measurement.
+            A value of `-1` means that the represented measurement is the
+            last one applied on `qubit`.
+
+    Raises:
+        TQECException: if the provided `offsetd` contains positive entries.
+    """
+
+    qubit: cirq.GridQubit
+    offsets: Interval
+
+    def __post_init__(self) -> None:
+        if not self.offsets.intersection(Rplus_interval).is_empty():
+            raise TQECException(
+                "Measurement.offsets should be an Interval "
+                "containing only strictly negative values."
+            )
+
+    def offset_spatially_by(self, x: int, y: int) -> RepeatedMeasurement:
+        return RepeatedMeasurement(self.qubit + (y, x), self.offsets)
+
+    def offset_temporally_by(self, t: int) -> RepeatedMeasurement:
+        return RepeatedMeasurement(self.qubit, self.offsets + t)
+
+    def measurements(self) -> list[Measurement]:
+        return [
+            Measurement(self.qubit, offset) for offset in self.offsets.iter_integers()
+        ]
+
+    def __repr__(self) -> str:
+        return f"{self.__class__.__name__}({self.qubit}, {self.offsets})"
+
+
+def get_measurements_from_circuit(
+    circuit: cirq.AbstractCircuit,
+) -> list[Measurement]:
+    ordered_measured_qubits: list[cirq.GridQubit] = []
+    for moment in circuit.moments:
+        for op in moment:
+            if isinstance(op, cirq.CircuitOperation):
+                raise TQECException(
+                    "Found an instance of cirq.CircuitOperation in a function "
+                    "expecting a flat quantum circuit."
+                )
+            if cirq.is_measurement(op):
+                qubits = op.qubits
+                if len(qubits) != 1:
+                    raise TQECException("Found a measurement on multiple qubits.")
+                (qubit,) = qubits
+                if not isinstance(qubit, cirq.GridQubit):
+                    raise TQECException(
+                        "Found a qubit that is not an instance of cirq.GridQubit."
+                    )
+                ordered_measured_qubits.append(qubit)
+    measured_qubit_offset: defaultdict[cirq.GridQubit, int] = defaultdict(lambda: -1)
+    measurements: list[Measurement] = []
+    for mqubit in ordered_measured_qubits:
+        index = measured_qubit_offset[mqubit]
+        measured_qubit_offset[mqubit] = index - 1
+        measurements.append(Measurement(mqubit, index))
+    return measurements
diff --git a/src/tqec/circuit/operations/measurement_map.py b/src/tqec/circuit/operations/measurement_map.py
index 84759813..5104975b 100644
--- a/src/tqec/circuit/operations/measurement_map.py
+++ b/src/tqec/circuit/operations/measurement_map.py
@@ -2,7 +2,7 @@ from __future__ import annotations
 
 import cirq
 
-from tqec.circuit.operations.operation import RelativeMeasurementsRecord
+from tqec.circuit.operations.operation import MeasurementsRecord
 from tqec.exceptions import TQECException
 
 
@@ -171,7 +171,7 @@ class CircuitMeasurementMap:
 
 
 def compute_global_measurements_lookback_offsets(
-    relative_measurements_record: RelativeMeasurementsRecord,
+    measurements_record: MeasurementsRecord,
     measurement_map: CircuitMeasurementMap,
     current_moment_index: int,
 ) -> list[int]:
@@ -182,8 +182,8 @@ def compute_global_measurements_lookback_offsets(
     parameter to compute the measurement record indices for the current gate instance.
 
     Args:
-        relative_measurements_record: the record of relative measurements to
-            compute global measurements offset from.
+        measurements_record: the record of measurements to compute global
+            measurements offset from.
         measurement_map: global measurement data obtained from the complete
             quantum circuit.
         current_moment_index: index of the moment this gate instance is found
@@ -196,21 +196,20 @@ def compute_global_measurements_lookback_offsets(
         TQECException: if the global measurement offset computation fails.
     """
     global_measurements_lookback_offsets = []
-    origin = relative_measurements_record.origin
-    for relative_measurement in relative_measurements_record.relative_measurement_data:
+    for measurement in measurements_record.measurement_data:
         # Coordinate system: adding 2 GridQubit instances together, both are using the GridQubit
         #                    coordinate system, so no issue here.
-        qubit = origin + relative_measurement.relative_qubit_positioning
-        relative_measurement_offset = relative_measurement.relative_measurement_offset
+        qubit = measurement.qubit
+        measurement_offset = measurement.offset
         relative_offset = measurement_map.get_measurement_relative_offset(
             current_moment_index,
             qubit,
-            relative_measurement_offset,
+            measurement_offset,
         )
         if relative_offset is None:
             raise TQECException(
                 "An error happened during the measurement offset lookback computation. "
-                f"You asked for the {relative_measurement_offset} measurement on {qubit} "
+                f"You asked for the {measurement_offset} measurement on {qubit} "
                 f"at the moment {current_moment_index}. The computed measurement map is "
                 f"{measurement_map}."
             )
diff --git a/src/tqec/circuit/operations/operation.py b/src/tqec/circuit/operations/operation.py
index 9469b3ad..d06e1ea7 100644
--- a/src/tqec/circuit/operations/operation.py
+++ b/src/tqec/circuit/operations/operation.py
@@ -1,12 +1,12 @@
 from __future__ import annotations
 
 import typing
-from dataclasses import dataclass
 from typing import Any, Sequence
 
 import cirq
 import stim
 
+from tqec.circuit.operations.measurement import Measurement
 from tqec.exceptions import TQECException
 
 STIM_TAG = "STIM_OPERATION"
@@ -67,102 +67,52 @@ class ShiftCoords(cirq.Operation):
         return f"{self.__class__.__name__}{self._shifts}"
 
 
-@dataclass(frozen=True)
-class RelativeMeasurementData:
-    """The relative_measurement_data of a spatially and temporally relative
-    measurement.
+class MeasurementsRecord(cirq.Operation):
+    def __init__(self, measurement_data: Sequence[Measurement]) -> None:
+        """A group of measurements.
 
-    This class stores two attributes (relative_qubit_positioning and
-    relative_measurement_offset) that respectively represent a spatial
-    offset (a **relative** positioning with respect to some origin) and
-    a temporal offset (with respect to a given Moment index).
-
-    This relative information will be analysed by the fill_in_global_record_indices
-    transformer and replaced by real qubit and global measurement records.
-
-    Parameters:
-        relative_qubit_positioning: The qubit coordinate offset. This will be added to a
-            qubit coordinate system origin to obtain the actual qubit the measurement
-            has been performed on.
-        relative_measurement_offset: The relative measurement offset. The measurements
-            performed on the qubit specified bt the relative_qubit_positioning will be
-            accessed using this offset. For example, a value of -1 means the last
-            measurement performed on the qubit till the time of the current operation.
-    """
-
-    relative_qubit_positioning: cirq.GridQubit
-    relative_measurement_offset: int
-
-    def __post_init__(self) -> None:
-        if self.relative_measurement_offset >= 0:
-            raise TQECException(
-                "The relative_measurement_offset should be a negative integer, "
-                f"but got {self.relative_measurement_offset}."
-            )
-
-    def __repr__(self) -> str:
-        return f"{self.__class__.__name__}({self.relative_qubit_positioning}, {self.relative_measurement_offset})"
-
-
-class RelativeMeasurementsRecord(cirq.Operation):
-    def __init__(
-        self,
-        local_coordinate_system_origin: cirq.GridQubit,
-        relative_measurement_data: list[RelativeMeasurementData],
-    ) -> None:
-        """A group of relative measurement data representing measurements
-        relative to the origin of local coordinate system and current
-        measurement timestamp.
+        This class simply stores a list of :class:`~.Measurement` instances.
+        Its purpose is to be used as a base to build operations that require
+        a list of measurements.
 
         Args:
-            local_coordinate_system_origin: origin of the local coordinate
-                system. The origin along with the local coordinate system will
-                be pinned to the global coordinate system to resolve the actual
-                qubit coordinates the measurements applied to.
-            relative_measurement_data: a list of :class:`RelativeMeasurementData` that composed the
-                relative measurements' operation.
+            measurement_data: a sequence of :class:`~.Measurement` that form
+                a group. The exact meaning of this group (e.g., a detector or
+                an observable) is not explicit and have to be provided by
+                subclasses.
+                There should be no duplicate in the provided sequence.
+
+        Raises:
+            TQECException: if there is any duplicate entry in the provided
+                `measurement_data`.
         """
         # check there is no duplicate relative_measurement_data
-        if len(set(relative_measurement_data)) != len(relative_measurement_data):
+        if len(set(measurement_data)) != len(measurement_data):
             raise TQECException(
                 "The relative_measurement_data should not contain duplicate elements."
             )
 
-        self._local_coordinate_system_origin = local_coordinate_system_origin
-        self._data = relative_measurement_data
+        self._data = list(measurement_data)
 
     @property
     def qubits(self) -> tuple[cirq.Qid, ...]:
         return ()
 
-    def with_qubits(self, *new_qubits: cirq.Qid) -> RelativeMeasurementsRecord:
+    def with_qubits(self, *new_qubits: cirq.Qid) -> MeasurementsRecord:
         return self
 
     @property
-    def relative_measurement_data(self) -> list[RelativeMeasurementData]:
+    def measurement_data(self) -> list[Measurement]:
         """The recorded relative measurement data."""
         return self._data
 
-    @property
-    def origin(self) -> cirq.GridQubit:
-        """The origin of the local coordinate system."""
-        return self._local_coordinate_system_origin
-
-    @origin.setter
-    def origin(self, new_origin: cirq.GridQubit) -> None:
-        """The origin of the local coordinate system."""
-        self._local_coordinate_system_origin = new_origin
-
     def __repr__(self) -> str:
-        return f"{self.__class__.__name__}({self._local_coordinate_system_origin}, {self._data})"
+        return f"{self.__class__.__name__}({self._data})"
 
 
-class Detector(RelativeMeasurementsRecord):
+class Detector(MeasurementsRecord):
     def __init__(
-        self,
-        local_coordinate_system_origin: cirq.GridQubit,
-        relative_measurement_data: list[RelativeMeasurementData],
-        time_coordinate: int = 0,
+        self, measurement_data: Sequence[Measurement], coordinates: tuple[float, ...]
     ) -> None:
         """Operation representing a detector.
 
@@ -181,44 +131,29 @@ class Detector(RelativeMeasurementsRecord):
             you should use `make_detector` to create instances of this class.
             To understand how to use this class directly, please refer to the
             documentation above.
+
         Args:
-            local_coordinate_system_origin: origin of the local coordinate
-                system. The origin along with the local coordinate system will
-                be pinned to the global coordinate system to resolve the actual
-                qubit coordinates the measurements applied to.
-            relative_measurement_data: a list of :class:`RelativeMeasurementData` that composed the
-                relative measurements' operation.
-            time_coordinate: an annotation that will be forwarded to the
-                DETECTOR Stim structure as the last coordinate.
+            measurement_data: a sequence of :class:`~.Measurement` that form
+                a group. There should be no duplicate in the provided sequence.
+            coordinates: an annotation that will be forwarded to the DETECTOR Stim
+                instruction.
         """
-        if time_coordinate < 0:
-            raise TQECException(
-                "The time_coordinate should be a non-negative integer, "
-                f"but got {time_coordinate}."
-            )
 
-        super().__init__(local_coordinate_system_origin, relative_measurement_data)
-        self._time_coordinate = time_coordinate
+        super().__init__(measurement_data)
+        self._coordinates = coordinates
 
     def _circuit_diagram_info_(self, _: Any) -> str:
         row, col, t = self.coordinates
         return f"Detector({row},{col},{t})"
 
     @property
-    def coordinates(self) -> tuple[int, ...]:
-        return (
-            self.origin.row,
-            self.origin.col,
-            self._time_coordinate,
-        )
+    def coordinates(self) -> tuple[float, ...]:
+        return self._coordinates
 
 
-class Observable(RelativeMeasurementsRecord):
+class Observable(MeasurementsRecord):
     def __init__(
-        self,
-        local_coordinate_system_origin: cirq.GridQubit,
-        relative_measurement_data: list[RelativeMeasurementData],
-        observable_index: int = 0,
+        self, measurement_data: Sequence[Measurement], observable_index: int = 0
     ) -> None:
         """Operation representing an observable.
 
@@ -237,14 +172,11 @@ class Observable(RelativeMeasurementsRecord):
             you should use `make_observable` to create instances of this class.
             To understand how to use this class directly, please refer to the
             documentation above.
+
         Args:
-            local_coordinate_system_origin: origin of the local coordinate
-                system. The origin along with the local coordinate system will
-                be pinned to the global coordinate system to resolve the actual
-                qubit coordinates the measurements applied to.
-            relative_measurement_data: a list of :class:`RelativeMeasurementData` that composed the
-                relative measurements' operation.
-            observable_index: the index of the observable.
+            measurement_data: a sequence of :class:`~.Measurement` that form
+                a group. There should be no duplicate in the provided sequence.
+            observable_index: index of the observable.
         """
         if observable_index < 0:
             raise TQECException(
@@ -252,7 +184,7 @@ class Observable(RelativeMeasurementsRecord):
                 f"but got {observable_index}."
             )
 
-        super().__init__(local_coordinate_system_origin, relative_measurement_data)
+        super().__init__(measurement_data)
         self._observable_index = observable_index
 
     def _circuit_diagram_info_(self, _: cirq.CircuitDiagramInfoArgs) -> str:
@@ -278,86 +210,46 @@ def make_shift_coords(*shifts: int) -> cirq.Operation:
 
 
 def make_detector(
-    local_coordinate_system_origin: cirq.GridQubit,
-    relative_measurements: Sequence[
-        tuple[cirq.GridQubit, int] | RelativeMeasurementData
-    ],
-    time_coordinate: int = 0,
+    measurements: Sequence[Measurement], coordinates: tuple[float, ...]
 ) -> cirq.Operation:
     """This is a helper function to make a :class:`Detector` operation with the
     `cirq.VirtualTag` tag.
 
     Args:
-        local_coordinate_system_origin: origin of the local coordinate system.
-            The origin along with the local coordinate system will be pinned to
-            the global coordinate system to resolve the actual qubit coordinates
-            the measurements applied to.
-        relative_measurements: a list of relative measurements that compose the
-            parity check of the detector. Each element of the list is a tuple of
-            (relative_qubit_position, relative_measurement_offset) or a
-            :class:`RelativeMeasurementData` instance. When a tuple is provided,
-            the first element is the position of the qubit relative to the local
-            coordinate system origin and the second element is the relative
-            measurement offset with respect to the most recent measurement
-            performed on the qubit.
-        time_coordinate: an annotation that will be forwarded to the DETECTOR
-            Stim structure as the last coordinate.
+        measurements: a list of measurements that compose the parity check of
+            the detector.
+        coordinates: an annotation that will be forwarded to the DETECTOR Stim
+            instruction.
 
     Returns:
         A :class:`Detector` operation with the `cirq.VirtualTag` tag.
     """
-    relative_measurements_data = [
-        (
-            RelativeMeasurementData(*rm)
-            if not isinstance(rm, RelativeMeasurementData)
-            else rm
+    if coordinates[-1] < 0:
+        raise TQECException(
+            "The last coordinate should represent the time coordinate "
+            f"and cannot be negative. Got {coordinates[-1]}."
         )
-        for rm in relative_measurements
-    ]
-    return Detector(
-        local_coordinate_system_origin, relative_measurements_data, time_coordinate
-    ).with_tags(cirq.VirtualTag(), STIM_TAG)
+    return Detector(measurements, coordinates).with_tags(cirq.VirtualTag(), STIM_TAG)
 
 
 def make_observable(
-    local_coordinate_system_origin: cirq.GridQubit,
-    relative_measurements: Sequence[
-        tuple[cirq.GridQubit, int] | RelativeMeasurementData
-    ],
-    observable_index: int = 0,
+    measurements: Sequence[Measurement], observable_index: int = 0
 ) -> cirq.Operation:
     """This is a helper function to make a :class:`Observable` operation with
     the `cirq.VirtualTag` tag.
 
     Args:
-        local_coordinate_system_origin: origin of the local coordinate system.
-            The origin along with the local coordinate system will be pinned to
-            the global coordinate system to resolve the actual qubit coordinates
-            the measurements applied to.
-        relative_measurements: a list of relative measurements that compose the
-            parity check of the observable. Each element of the list is a tuple
-            of (relative_qubit_position, relative_measurement_offset) or a
-            :class:`RelativeMeasurementData` instance. When a tuple is provided,
-            the first element is the position of the qubit relative to the local
-            coordinate system origin and the second element is the relative
-            measurement offset with respect to the most recent measurement
-            performed on the qubit.
-        observable_index: the index of the observable.
+        relative_measurements: a list of measurements that compose the
+            parity check of the observable.
+        observable_index: index of the observable.
 
     Returns:
         A :class:`Observable` operation with the `cirq.VirtualTag` tag.
     """
-    relative_measurements_data = [
-        (
-            RelativeMeasurementData(*rm)
-            if not isinstance(rm, RelativeMeasurementData)
-            else rm
-        )
-        for rm in relative_measurements
-    ]
-    return Observable(
-        local_coordinate_system_origin, relative_measurements_data, observable_index
-    ).with_tags(cirq.VirtualTag(), STIM_TAG)
+
+    return Observable(measurements, observable_index).with_tags(
+        cirq.VirtualTag(), STIM_TAG
+    )
 
 
 @cirq.value_equality
@@ -462,4 +354,7 @@ class MX(cirq.Operation):
         **kwargs: dict[str, typing.Any],
     ) -> None:
         edit_measurement_key_lengths.append((",".join(map(str, targets)), len(targets)))
-        edit_circuit.append_operation("MX", targets, [])
+        edit_circuit.append_operation("MX", list(self.qubits), None)
+
+    def _is_measurement_(self) -> bool:
+        return True
diff --git a/src/tqec/circuit/schedule.py b/src/tqec/circuit/schedule.py
index 23f6e48c..1e7c8b3d 100644
--- a/src/tqec/circuit/schedule.py
+++ b/src/tqec/circuit/schedule.py
@@ -259,9 +259,7 @@ class ScheduledCircuit:
         """Return the set of qubits involved in the circuit that can be mapped,
         which is the union of the qubits of all the operations performed on and
         the origin of all the detectors."""
-        operation_qubits = self.qubits
-        detector_origins = {detector.origin for detector in self.detectors}
-        return frozenset(operation_qubits.union(detector_origins))
+        return frozenset(self.qubits)
 
     def map_to_qubits(
         self, qubit_map: dict[cirq.GridQubit, cirq.GridQubit], inplace: bool = False
@@ -293,9 +291,8 @@ class ScheduledCircuit:
                 return cirq.measure(*op.qubits).with_tags(*op.tags)
             elif isinstance(untagged, Detector):
                 return make_detector(
-                    qubit_map.get(untagged.origin, untagged.origin),
-                    untagged.relative_measurement_data,
-                    time_coordinate=untagged.coordinates[-1],
+                    [meas.map_qubit(qubit_map) for meas in untagged.measurement_data],
+                    coordinates=untagged.coordinates,
                 )
             else:
                 return op
diff --git a/src/tqec/plaquette/library/pauli.py b/src/tqec/plaquette/library/pauli.py
index 4978fedd..ad2e20be 100644
--- a/src/tqec/plaquette/library/pauli.py
+++ b/src/tqec/plaquette/library/pauli.py
@@ -4,6 +4,7 @@ from enum import Enum, auto
 
 import cirq
 
+from tqec.circuit.operations.measurement import Measurement
 from tqec.circuit.operations.operation import MX, RX
 from tqec.circuit.schedule import ScheduledCircuit
 from tqec.exceptions import TQECException
diff --git a/src/tqec/plaquette/plaquette.py b/src/tqec/plaquette/plaquette.py
index 9bab389b..a4746070 100644
--- a/src/tqec/plaquette/plaquette.py
+++ b/src/tqec/plaquette/plaquette.py
@@ -1,6 +1,14 @@
+from __future__ import annotations
+
+import itertools
 import typing
 from collections import defaultdict
+from dataclasses import dataclass
 
+from tqec.circuit.operations.measurement import (
+    Measurement,
+    get_measurements_from_circuit,
+)
 from tqec.circuit.schedule import ScheduledCircuit
 from tqec.exceptions import TQECException
 from tqec.plaquette.qubit import PlaquetteQubits
@@ -47,6 +55,7 @@ class Plaquette:
             )
         self._qubits = qubits
         self._circuit = circuit
+        self._measurements = get_measurements_from_circuit(circuit.raw_circuit)
 
     @property
     def origin(self) -> Position:
@@ -60,7 +69,66 @@ class Plaquette:
     def circuit(self) -> ScheduledCircuit:
         return self._circuit
 
+    @property
+    def measurements(self) -> list[Measurement]:
+        return self._measurements
+
+
+def _to_plaquette_dict(
+    plaquettes: list[Plaquette] | dict[int, Plaquette] | defaultdict[int, Plaquette],
+) -> dict[int, Plaquette] | defaultdict[int, Plaquette]:
+    if isinstance(plaquettes, (dict, defaultdict)):
+        return plaquettes
+    return {i: p for i, p in enumerate(plaquettes)}
+
+
+@dataclass(frozen=True)
+class Plaquettes:
+    collection: list[Plaquette] | dict[int, Plaquette] | defaultdict[int, Plaquette]
+
+    def __post_init__(self) -> None:
+        if not isinstance(self.collection, (list, dict, defaultdict)):
+            raise TQECException(
+                f"Plaquettes initialized with {type(self.collection)} but only list, "
+                "dict and defaultdict instances are allowed."
+            )
+
+    def __getitem__(self, index: int) -> Plaquette:
+        return self.collection[index]
+
+    def __iter__(self) -> typing.Iterator[Plaquette]:
+        if isinstance(self.collection, list):
+            return iter(self.collection)
+        if isinstance(self.collection, defaultdict):
+            if self.collection.default_factory is not None:
+                default = self.collection.default_factory()
+                return itertools.chain(
+                    self.collection.values(), [default] if default is not None else []
+                )
+            else:
+                return iter(self.collection.values())
+        if isinstance(self.collection, dict):
+            return iter(self.collection.values())
+        else:
+            raise TQECException(
+                f"Plaquette is initialised with the wrong type: {type(self.collection)}."
+            )
+
+    @property
+    def has_default(self) -> bool:
+        return isinstance(self.collection, defaultdict)
+
+    def __len__(self) -> int:
+        if isinstance(self.collection, defaultdict):
+            raise TQECException(
+                "Cannot accurately get the length of a defaultdict instance."
+            )
+        return len(self.collection)
 
-Plaquettes = typing.Union[
-    list[Plaquette], dict[int, Plaquette], defaultdict[int, Plaquette]
-]
+    def __or__(self, other: Plaquettes | dict[int, Plaquette]) -> Plaquettes:
+        other_plaquettes = (
+            _to_plaquette_dict(other.collection)
+            if isinstance(other, Plaquettes)
+            else other
+        )
+        return Plaquettes(_to_plaquette_dict(self.collection) | other_plaquettes)
diff --git a/src/tqec/templates/interval.py b/src/tqec/templates/interval.py
index f88c77f5..3492e894 100644
--- a/src/tqec/templates/interval.py
+++ b/src/tqec/templates/interval.py
@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 import math
+import typing
 from dataclasses import dataclass
 
 from tqec.exceptions import TQECException
@@ -21,8 +22,8 @@ class Interval:
             the provided `end` value.
     """
 
-    start: float
-    end: float
+    start: float | int
+    end: float | int
     start_excluded: bool = False
     end_excluded: bool = True
 
@@ -167,6 +168,25 @@ class Interval:
     def __repr__(self) -> str:
         return f"{'(' if self.start_excluded else '['}{self.start}, {self.end}{')' if self.end_excluded else ']'}"
 
+    def __add__(self, value: int | float) -> Interval:
+        return Interval(
+            self.start + value, self.end + value, self.start_excluded, self.end_excluded
+        )
+
+    def __sub__(self, value: int | float) -> Interval:
+        return Interval(
+            self.start - value, self.end - value, self.start_excluded, self.end_excluded
+        )
+
+    def iter_integers(self) -> typing.Iterable[int]:
+        inclusive_start_int = int(math.ceil(self.start))
+        if self.start_excluded and abs(self.start - inclusive_start_int) < 1e-8:
+            inclusive_start_int += 1
+        exclusive_end_int = int(math.floor(self.end)) + 1
+        if self.end_excluded and abs(self.end - exclusive_end_int) < 1e-8:
+            exclusive_end_int -= 1
+        return range(inclusive_start_int, exclusive_end_int)
+
 
 EMPTY_INTERVAL = Interval(0, 0, start_excluded=True, end_excluded=True)
 


Test patch:
diff --git a/src/tqec/circuit/operations/operation_test.py b/src/tqec/circuit/operations/operation_test.py
index c2ed95ee..1fc1f07d 100644
--- a/src/tqec/circuit/operations/operation_test.py
+++ b/src/tqec/circuit/operations/operation_test.py
@@ -1,11 +1,11 @@
 import cirq
 import pytest
 
+from tqec.circuit.operations.measurement import Measurement
 from tqec.circuit.operations.operation import (
     Detector,
+    MeasurementsRecord,
     Observable,
-    RelativeMeasurementData,
-    RelativeMeasurementsRecord,
     ShiftCoords,
     make_detector,
     make_observable,
@@ -39,86 +39,70 @@ def test_shift_coords() -> None:
 
 @pytest.mark.parametrize("origin", _qubits_examples)
 def test_empty_detector(origin: cirq.GridQubit) -> None:
-    detector_tagged = make_detector(origin, [], time_coordinate=0)
+    detector_tagged = make_detector([], coordinates=(0.0, 0.0, 0.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)
+    assert detector_untagged.coordinates == (0.0, 0.0, 0.0)
 
 
-def test_detector_repeated_relative_measurement() -> None:
-    origin = cirq.GridQubit(0, 0)
-    relative_measurements: list[tuple[cirq.GridQubit, int]] = [
-        (cirq.GridQubit(0, 0), -1),
-        (cirq.GridQubit(0, 0), -1),
+def test_detector_repeated_measurement() -> None:
+    measurements: list[Measurement] = [
+        Measurement(cirq.GridQubit(0, 0), -1),
+        Measurement(cirq.GridQubit(0, 0), -1),
     ]
     with pytest.raises(TQECException):
         # Duplicated relative measurements
-        make_detector(origin, relative_measurements, time_coordinate=0)
+        make_detector(measurements, coordinates=(0.0, 0.0, 0.0))
 
 
-def test_detector_with_relative_measurement() -> None:
-    origin = cirq.GridQubit(0, 0)
-    relative_measurements: list[tuple[cirq.GridQubit, int]] = [
-        (cirq.GridQubit(0, 0), -1),
-        (cirq.GridQubit(0, 0), -2),
+def test_detector_with_measurement() -> None:
+    measurements: list[Measurement] = [
+        Measurement(cirq.GridQubit(0, 0), -1),
+        Measurement(cirq.GridQubit(0, 0), -2),
     ]
-    make_detector(origin, relative_measurements, time_coordinate=0)
+    make_detector(measurements, coordinates=(0.0, 0.0, 0.0))
 
 
 def test_detector_negative_time_coordinate() -> None:
-    origin = cirq.GridQubit(0, 0)
-    relative_measurements: list[tuple[cirq.GridQubit, int]] = [
-        (cirq.GridQubit(0, 0), -1),
-        (cirq.GridQubit(0, 0), -2),
+    measurements: list[Measurement] = [
+        Measurement(cirq.GridQubit(0, 0), -1),
+        Measurement(cirq.GridQubit(0, 0), -2),
     ]
     with pytest.raises(TQECException):
-        make_detector(origin, relative_measurements, time_coordinate=-1)
+        make_detector(measurements, coordinates=(0.0, 0.0, -1.0))
 
 
-@pytest.mark.parametrize("origin", _qubits_examples)
-def test_empty_observable(origin: cirq.GridQubit) -> None:
-    observable_tagged = make_observable(origin, [], observable_index=0)
+def test_empty_observable() -> None:
+    observable_tagged = make_observable([], 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() -> None:
-    origin = cirq.GridQubit(0, 0)
-    relative_measurements: list[tuple[cirq.GridQubit, int]] = [
-        (cirq.GridQubit(0, 0), -1),
-        (cirq.GridQubit(0, 0), -2),
+def test_observable_with_measurement() -> None:
+    measurements: list[Measurement] = [
+        Measurement(cirq.GridQubit(0, 0), -1),
+        Measurement(cirq.GridQubit(0, 0), -2),
     ]
-    make_observable(origin, relative_measurements, observable_index=0)
+    make_observable(measurements, observable_index=0)
 
 
 def test_observable_negative_index() -> None:
-    origin = cirq.GridQubit(0, 0)
-    relative_measurements: list[tuple[cirq.GridQubit, int]] = [
-        (cirq.GridQubit(0, 0), -1),
-        (cirq.GridQubit(0, 0), -2),
+    measurements: list[Measurement] = [
+        Measurement(cirq.GridQubit(0, 0), -1),
+        Measurement(cirq.GridQubit(0, 0), -2),
     ]
     with pytest.raises(TQECException):
-        make_observable(origin, relative_measurements, observable_index=-1)
-
-
-def test_relative_measure_data_negative_index() -> None:
-    with pytest.raises(TQECException):
-        RelativeMeasurementData(cirq.GridQubit(0, 0), 0)
-    with pytest.raises(TQECException):
-        RelativeMeasurementData(cirq.GridQubit(0, 0), 1)
+        make_observable(measurements, observable_index=-1)
 
 
-def test_relative_measurement_record_duplicated_measurement_data() -> None:
-    origin = cirq.GridQubit(0, 0)
+def test_measurement_record_duplicated_measurement_data() -> None:
+    qubit = cirq.GridQubit(0, 0)
     with pytest.raises(TQECException):
-        RelativeMeasurementsRecord(
-            origin,
+        MeasurementsRecord(
             [
-                RelativeMeasurementData(origin, -1),
-                RelativeMeasurementData(origin, -2),
-                RelativeMeasurementData(origin, -1),
+                Measurement(qubit, -1),
+                Measurement(qubit, -2),
+                Measurement(qubit, -1),
             ],
         )