Repository: tqec/tqec
Pull request #508: Annotate detectors using the layer approach
Author: login: nelimee
Labels: name: enhancement
description: New feature or request, may not be in the task flow
name: backend
description: Issue pertaining to the Python backend (tqec package)
name: refactor
description: Requires major updates to code base
Created: 2025-03-12T10:14:26Z | Merged: 2025-03-18T14:45:35Z

Should fix #502.

The following code:
```py
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_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),
        ),
    ]
)
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),
        ),
    ]
)
graph.add_cube(BlockPosition3D(0, 0, 0), XZZ)
graph.add_cube(BlockPosition3D(1, 0, 0), XZZ)
graph.add_cube(BlockPosition3D(0, 0, 1), XZZ)
graph.add_cube(BlockPosition3D(1, 0, 1), XZZ)
graph.add_pipe(BlockPosition3D(0, 0, 0), BlockPosition3D(0, 0, 1), XZO)
graph.add_pipe(BlockPosition3D(1, 0, 0), BlockPosition3D(1, 0, 1), XZO)
graph.add_pipe(BlockPosition3D(0, 0, 1), BlockPosition3D(1, 0, 1), OZZ)


layer_tree = graph.to_layout_tree()
layer_tree.annotate_circuits(2)
layer_tree.annotate_qubit_map(2)
layer_tree.annotate_detectors(2)
circuit = layer_tree.generate_circuit(2)

print(circuit)
```

returns 

<details>
<summary>Click he to expand and see the circuit</summary>
<pre>
QUBIT_COORDS(0, 0) 0
QUBIT_COORDS(0, 2) 1
QUBIT_COORDS(0, 4) 2
QUBIT_COORDS(0, 6) 3
QUBIT_COORDS(0, 8) 4
QUBIT_COORDS(0, 10) 5
QUBIT_COORDS(1, 1) 6
QUBIT_COORDS(1, 3) 7
QUBIT_COORDS(1, 5) 8
QUBIT_COORDS(1, 7) 9
QUBIT_COORDS(1, 9) 10
QUBIT_COORDS(2, 0) 11
QUBIT_COORDS(2, 2) 12
QUBIT_COORDS(2, 4) 13
QUBIT_COORDS(2, 6) 14
QUBIT_COORDS(2, 8) 15
QUBIT_COORDS(2, 10) 16
QUBIT_COORDS(3, 1) 17
QUBIT_COORDS(3, 3) 18
QUBIT_COORDS(3, 5) 19
QUBIT_COORDS(3, 7) 20
QUBIT_COORDS(3, 9) 21
QUBIT_COORDS(4, 0) 22
QUBIT_COORDS(4, 2) 23
QUBIT_COORDS(4, 4) 24
QUBIT_COORDS(4, 6) 25
QUBIT_COORDS(4, 8) 26
QUBIT_COORDS(4, 10) 27
QUBIT_COORDS(5, 1) 28
QUBIT_COORDS(5, 3) 29
QUBIT_COORDS(5, 5) 30
QUBIT_COORDS(5, 7) 31
QUBIT_COORDS(5, 9) 32
QUBIT_COORDS(6, 0) 33
QUBIT_COORDS(6, 2) 34
QUBIT_COORDS(6, 4) 35
QUBIT_COORDS(6, 6) 36
QUBIT_COORDS(6, 8) 37
QUBIT_COORDS(6, 10) 38
QUBIT_COORDS(7, 1) 39
QUBIT_COORDS(7, 3) 40
QUBIT_COORDS(7, 5) 41
QUBIT_COORDS(7, 7) 42
QUBIT_COORDS(7, 9) 43
QUBIT_COORDS(8, 0) 44
QUBIT_COORDS(8, 2) 45
QUBIT_COORDS(8, 4) 46
QUBIT_COORDS(8, 6) 47
QUBIT_COORDS(8, 8) 48
QUBIT_COORDS(8, 10) 49
QUBIT_COORDS(9, 1) 50
QUBIT_COORDS(9, 3) 51
QUBIT_COORDS(9, 5) 52
QUBIT_COORDS(9, 7) 53
QUBIT_COORDS(9, 9) 54
QUBIT_COORDS(10, 0) 55
QUBIT_COORDS(10, 2) 56
QUBIT_COORDS(10, 4) 57
QUBIT_COORDS(10, 6) 58
QUBIT_COORDS(10, 8) 59
QUBIT_COORDS(10, 10) 60
QUBIT_COORDS(11, 1) 61
QUBIT_COORDS(11, 3) 62
QUBIT_COORDS(11, 5) 63
QUBIT_COORDS(11, 7) 64
QUBIT_COORDS(11, 9) 65
QUBIT_COORDS(12, 0) 66
QUBIT_COORDS(12, 2) 67
QUBIT_COORDS(12, 4) 68
QUBIT_COORDS(12, 6) 69
QUBIT_COORDS(12, 8) 70
QUBIT_COORDS(12, 10) 71
QUBIT_COORDS(13, 1) 72
QUBIT_COORDS(13, 3) 73
QUBIT_COORDS(13, 5) 74
QUBIT_COORDS(13, 7) 75
QUBIT_COORDS(13, 9) 76
QUBIT_COORDS(14, 0) 77
QUBIT_COORDS(14, 2) 78
QUBIT_COORDS(14, 4) 79
QUBIT_COORDS(14, 6) 80
QUBIT_COORDS(14, 8) 81
QUBIT_COORDS(14, 10) 82
QUBIT_COORDS(15, 1) 83
QUBIT_COORDS(15, 3) 84
QUBIT_COORDS(15, 5) 85
QUBIT_COORDS(15, 7) 86
QUBIT_COORDS(15, 9) 87
QUBIT_COORDS(16, 0) 88
QUBIT_COORDS(16, 2) 89
QUBIT_COORDS(16, 4) 90
QUBIT_COORDS(16, 6) 91
QUBIT_COORDS(16, 8) 92
QUBIT_COORDS(16, 10) 93
QUBIT_COORDS(17, 1) 94
QUBIT_COORDS(17, 3) 95
QUBIT_COORDS(17, 5) 96
QUBIT_COORDS(17, 7) 97
QUBIT_COORDS(17, 9) 98
QUBIT_COORDS(18, 0) 99
QUBIT_COORDS(18, 2) 100
QUBIT_COORDS(18, 4) 101
QUBIT_COORDS(18, 6) 102
QUBIT_COORDS(18, 8) 103
QUBIT_COORDS(18, 10) 104
QUBIT_COORDS(19, 1) 105
QUBIT_COORDS(19, 3) 106
QUBIT_COORDS(19, 5) 107
QUBIT_COORDS(19, 7) 108
QUBIT_COORDS(19, 9) 109
QUBIT_COORDS(20, 0) 110
QUBIT_COORDS(20, 2) 111
QUBIT_COORDS(20, 4) 112
QUBIT_COORDS(20, 6) 113
QUBIT_COORDS(20, 8) 114
QUBIT_COORDS(20, 10) 115
QUBIT_COORDS(21, 1) 116
QUBIT_COORDS(21, 3) 117
QUBIT_COORDS(21, 5) 118
QUBIT_COORDS(21, 7) 119
QUBIT_COORDS(21, 9) 120
QUBIT_COORDS(22, 0) 121
QUBIT_COORDS(22, 2) 122
QUBIT_COORDS(22, 4) 123
QUBIT_COORDS(22, 6) 124
QUBIT_COORDS(22, 8) 125
QUBIT_COORDS(22, 10) 126
RX 1 3 12 13 14 15 16 22 23 24 25 26 34 35 36 37 38 44 45 46 47 48 57 59 67 69 78 79 80 81 82 88 89 90 91 92 100 101 102 103 104 110 111 112 113 114 123 125
R 6 7 8 9 10 17 18 19 20 21 28 29 30 31 32 39 40 41 42 43 50 51 52 53 54 72 73 74 75 76 83 84 85 86 87 94 95 96 97 98 105 106 107 108 109 116 117 118 119 120
TICK
CX 12 6 14 8 16 10 24 18 26 20 34 28 36 30 38 32 46 40 48 42 78 72 80 74 82 76 90 84 92 86 100 94 102 96 104 98 112 106 114 108
CZ 13 7 15 9 23 17 25 19 35 29 37 31 45 39 47 41 57 51 59 53 79 73 81 75 89 83 91 85 101 95 103 97 111 105 113 107 123 117 125 119
TICK
CX 12 17 14 19 16 21 24 29 26 31 34 39 36 41 38 43 46 51 48 53 78 83 80 85 82 87 90 95 92 97 100 105 102 107 104 109 112 117 114 119
CZ 13 8 15 10 23 18 25 20 35 30 37 32 45 40 47 42 57 52 59 54 79 74 81 76 89 84 91 86 101 96 103 98 111 106 113 108 123 118 125 120
TICK
CX 12 7 14 9 22 17 24 19 26 21 34 29 36 31 44 39 46 41 48 43 78 73 80 75 88 83 90 85 92 87 100 95 102 97 110 105 112 107 114 109
CZ 1 6 3 8 13 18 15 20 23 28 25 30 35 40 37 42 45 50 47 52 67 72 69 74 79 84 81 86 89 94 91 96 101 106 103 108 111 116 113 118
TICK
CX 12 18 14 20 22 28 24 30 26 32 34 40 36 42 44 50 46 52 48 54 78 84 80 86 88 94 90 96 92 98 100 106 102 108 110 116 112 118 114 120
CZ 1 7 3 9 13 19 15 21 23 29 25 31 35 41 37 43 45 51 47 53 67 73 69 75 79 85 81 87 89 95 91 97 101 107 103 109 111 117 113 119
TICK
TICK
MX 1 3 12 13 14 15 16 22 23 24 25 26 34 35 36 37 38 44 45 46 47 48 57 59 67 69 78 79 80 81 82 88 89 90 91 92 100 101 102 103 104 110 111 112 113 114 123 125
DETECTOR(0, 2, 0) rec[-48]
DETECTOR(4, 2, 0) rec[-40]
DETECTOR(8, 2, 0) rec[-30]
DETECTOR(12, 2, 0) rec[-24]
DETECTOR(16, 2, 0) rec[-16]
DETECTOR(20, 2, 0) rec[-6]
DETECTOR(2, 4, 0) rec[-45]
DETECTOR(6, 4, 0) rec[-35]
DETECTOR(10, 4, 0) rec[-26]
DETECTOR(14, 4, 0) rec[-21]
DETECTOR(18, 4, 0) rec[-11]
DETECTOR(22, 4, 0) rec[-2]
DETECTOR(0, 6, 0) rec[-47]
DETECTOR(4, 6, 0) rec[-38]
DETECTOR(8, 6, 0) rec[-28]
DETECTOR(12, 6, 0) rec[-23]
DETECTOR(16, 6, 0) rec[-14]
DETECTOR(20, 6, 0) rec[-4]
DETECTOR(2, 8, 0) rec[-43]
DETECTOR(6, 8, 0) rec[-33]
DETECTOR(10, 8, 0) rec[-25]
DETECTOR(14, 8, 0) rec[-19]
DETECTOR(18, 8, 0) rec[-9]
DETECTOR(22, 8, 0) rec[-1]
REPEAT 3 {
    TICK
    RX 1 3 12 13 14 15 16 22 23 24 25 26 34 35 36 37 38 44 45 46 47 48 57 59 67 69 78 79 80 81 82 88 89 90 91 92 100 101 102 103 104 110 111 112 113 114 123 125
    TICK
    CX 12 6 14 8 16 10 24 18 26 20 34 28 36 30 38 32 46 40 48 42 78 72 80 74 82 76 90 84 92 86 100 94 102 96 104 98 112 106 114 108
    CZ 13 7 15 9 23 17 25 19 35 29 37 31 45 39 47 41 57 51 59 53 79 73 81 75 89 83 91 85 101 95 103 97 111 105 113 107 123 117 125 119
    TICK
    CX 12 17 14 19 16 21 24 29 26 31 34 39 36 41 38 43 46 51 48 53 78 83 80 85 82 87 90 95 92 97 100 105 102 107 104 109 112 117 114 119
    CZ 13 8 15 10 23 18 25 20 35 30 37 32 45 40 47 42 57 52 59 54 79 74 81 76 89 84 91 86 101 96 103 98 111 106 113 108 123 118 125 120
    TICK
    CX 12 7 14 9 22 17 24 19 26 21 34 29 36 31 44 39 46 41 48 43 78 73 80 75 88 83 90 85 92 87 100 95 102 97 110 105 112 107 114 109
    CZ 1 6 3 8 13 18 15 20 23 28 25 30 35 40 37 42 45 50 47 52 67 72 69 74 79 84 81 86 89 94 91 96 101 106 103 108 111 116 113 118
    TICK
    CX 12 18 14 20 22 28 24 30 26 32 34 40 36 42 44 50 46 52 48 54 78 84 80 86 88 94 90 96 92 98 100 106 102 108 110 116 112 118 114 120
    CZ 1 7 3 9 13 19 15 21 23 29 25 31 35 41 37 43 45 51 47 53 67 73 69 75 79 85 81 87 89 95 91 97 101 107 103 109 111 117 113 119
    TICK
    TICK
    MX 1 3 12 13 14 15 16 22 23 24 25 26 34 35 36 37 38 44 45 46 47 48 57 59 67 69 78 79 80 81 82 88 89 90 91 92 100 101 102 103 104 110 111 112 113 114 123 125
    SHIFT_COORDS(0, 0, 1)
    DETECTOR(4, 0, 0) rec[-41] rec[-89]
    DETECTOR(8, 0, 0) rec[-31] rec[-79]
    DETECTOR(16, 0, 0) rec[-17] rec[-65]
    DETECTOR(20, 0, 0) rec[-55] rec[-7]
    DETECTOR(0, 2, 0) rec[-48] rec[-96]
    DETECTOR(2, 2, 0) rec[-46] rec[-94]
    DETECTOR(4, 2, 0) rec[-88] rec[-40]
    DETECTOR(6, 2, 0) rec[-36] rec[-84]
    DETECTOR(8, 2, 0) rec[-30] rec[-78]
    DETECTOR(12, 2, 0) rec[-72] rec[-24]
    DETECTOR(14, 2, 0) rec[-70] rec[-22]
    DETECTOR(16, 2, 0) rec[-64] rec[-16]
    DETECTOR(18, 2, 0) rec[-60] rec[-12]
    DETECTOR(20, 2, 0) rec[-54] rec[-6]
    DETECTOR(2, 4, 0) rec[-45] rec[-93]
    DETECTOR(4, 4, 0) rec[-39] rec[-87]
    DETECTOR(6, 4, 0) rec[-35] rec[-83]
    DETECTOR(8, 4, 0) rec[-29] rec[-77]
    DETECTOR(10, 4, 0) rec[-26] rec[-74]
    DETECTOR(14, 4, 0) rec[-21] rec[-69]
    DETECTOR(16, 4, 0) rec[-63] rec[-15]
    DETECTOR(18, 4, 0) rec[-59] rec[-11]
    DETECTOR(20, 4, 0) rec[-53] rec[-5]
    DETECTOR(22, 4, 0) rec[-2] rec[-50]
    DETECTOR(0, 6, 0) rec[-95] rec[-47]
    DETECTOR(2, 6, 0) rec[-44] rec[-92]
    DETECTOR(4, 6, 0) rec[-38] rec[-86]
    DETECTOR(6, 6, 0) rec[-34] rec[-82]
    DETECTOR(8, 6, 0) rec[-76] rec[-28]
    DETECTOR(12, 6, 0) rec[-71] rec[-23]
    DETECTOR(14, 6, 0) rec[-20] rec[-68]
    DETECTOR(16, 6, 0) rec[-62] rec[-14]
    DETECTOR(18, 6, 0) rec[-10] rec[-58]
    DETECTOR(20, 6, 0) rec[-4] rec[-52]
    DETECTOR(2, 8, 0) rec[-91] rec[-43]
    DETECTOR(4, 8, 0) rec[-85] rec[-37]
    DETECTOR(6, 8, 0) rec[-81] rec[-33]
    DETECTOR(8, 8, 0) rec[-75] rec[-27]
    DETECTOR(10, 8, 0) rec[-73] rec[-25]
    DETECTOR(14, 8, 0) rec[-19] rec[-67]
    DETECTOR(16, 8, 0) rec[-13] rec[-61]
    DETECTOR(18, 8, 0) rec[-9] rec[-57]
    DETECTOR(20, 8, 0) rec[-3] rec[-51]
    DETECTOR(22, 8, 0) rec[-1] rec[-49]
    DETECTOR(2, 10, 0) rec[-42] rec[-90]
    DETECTOR(6, 10, 0) rec[-80] rec[-32]
    DETECTOR(14, 10, 0) rec[-66] rec[-18]
    DETECTOR(18, 10, 0) rec[-56] rec[-8]
}
TICK
RX 1 3 12 13 14 15 16 22 23 24 25 26 34 35 36 37 38 44 45 46 47 48 57 59 67 69 78 79 80 81 82 88 89 90 91 92 100 101 102 103 104 110 111 112 113 114 123 125
TICK
CX 12 6 14 8 16 10 24 18 26 20 34 28 36 30 38 32 46 40 48 42 78 72 80 74 82 76 90 84 92 86 100 94 102 96 104 98 112 106 114 108
CZ 13 7 15 9 23 17 25 19 35 29 37 31 45 39 47 41 57 51 59 53 79 73 81 75 89 83 91 85 101 95 103 97 111 105 113 107 123 117 125 119
TICK
CX 12 17 14 19 16 21 24 29 26 31 34 39 36 41 38 43 46 51 48 53 78 83 80 85 82 87 90 95 92 97 100 105 102 107 104 109 112 117 114 119
CZ 13 8 15 10 23 18 25 20 35 30 37 32 45 40 47 42 57 52 59 54 79 74 81 76 89 84 91 86 101 96 103 98 111 106 113 108 123 118 125 120
TICK
CX 12 7 14 9 22 17 24 19 26 21 34 29 36 31 44 39 46 41 48 43 78 73 80 75 88 83 90 85 92 87 100 95 102 97 110 105 112 107 114 109
CZ 1 6 3 8 13 18 15 20 23 28 25 30 35 40 37 42 45 50 47 52 67 72 69 74 79 84 81 86 89 94 91 96 101 106 103 108 111 116 113 118
TICK
CX 12 18 14 20 22 28 24 30 26 32 34 40 36 42 44 50 46 52 48 54 78 84 80 86 88 94 90 96 92 98 100 106 102 108 110 116 112 118 114 120
CZ 1 7 3 9 13 19 15 21 23 29 25 31 35 41 37 43 45 51 47 53 67 73 69 75 79 85 81 87 89 95 91 97 101 107 103 109 111 117 113 119
TICK
TICK
MX 1 3 12 13 14 15 16 22 23 24 25 26 34 35 36 37 38 44 45 46 47 48 57 59 67 69 78 79 80 81 82 88 89 90 91 92 100 101 102 103 104 110 111 112 113 114 123 125
DETECTOR(4, 0, 0) rec[-41] rec[-89]
DETECTOR(8, 0, 0) rec[-31] rec[-79]
DETECTOR(16, 0, 0) rec[-17] rec[-65]
DETECTOR(20, 0, 0) rec[-55] rec[-7]
DETECTOR(0, 2, 0) rec[-48] rec[-96]
DETECTOR(2, 2, 0) rec[-46] rec[-94]
DETECTOR(4, 2, 0) rec[-88] rec[-40]
DETECTOR(6, 2, 0) rec[-36] rec[-84]
DETECTOR(8, 2, 0) rec[-30] rec[-78]
DETECTOR(12, 2, 0) rec[-72] rec[-24]
DETECTOR(14, 2, 0) rec[-70] rec[-22]
DETECTOR(16, 2, 0) rec[-64] rec[-16]
DETECTOR(18, 2, 0) rec[-60] rec[-12]
DETECTOR(20, 2, 0) rec[-54] rec[-6]
DETECTOR(2, 4, 0) rec[-45] rec[-93]
DETECTOR(4, 4, 0) rec[-39] rec[-87]
DETECTOR(6, 4, 0) rec[-35] rec[-83]
DETECTOR(8, 4, 0) rec[-29] rec[-77]
DETECTOR(10, 4, 0) rec[-26] rec[-74]
DETECTOR(14, 4, 0) rec[-21] rec[-69]
DETECTOR(16, 4, 0) rec[-63] rec[-15]
DETECTOR(18, 4, 0) rec[-59] rec[-11]
DETECTOR(20, 4, 0) rec[-53] rec[-5]
DETECTOR(22, 4, 0) rec[-2] rec[-50]
DETECTOR(0, 6, 0) rec[-95] rec[-47]
DETECTOR(2, 6, 0) rec[-44] rec[-92]
DETECTOR(4, 6, 0) rec[-38] rec[-86]
DETECTOR(6, 6, 0) rec[-34] rec[-82]
DETECTOR(8, 6, 0) rec[-76] rec[-28]
DETECTOR(12, 6, 0) rec[-71] rec[-23]
DETECTOR(14, 6, 0) rec[-20] rec[-68]
DETECTOR(16, 6, 0) rec[-62] rec[-14]
DETECTOR(18, 6, 0) rec[-10] rec[-58]
DETECTOR(20, 6, 0) rec[-4] rec[-52]
DETECTOR(2, 8, 0) rec[-91] rec[-43]
DETECTOR(4, 8, 0) rec[-85] rec[-37]
DETECTOR(6, 8, 0) rec[-81] rec[-33]
DETECTOR(8, 8, 0) rec[-75] rec[-27]
DETECTOR(10, 8, 0) rec[-73] rec[-25]
DETECTOR(14, 8, 0) rec[-19] rec[-67]
DETECTOR(16, 8, 0) rec[-13] rec[-61]
DETECTOR(18, 8, 0) rec[-9] rec[-57]
DETECTOR(20, 8, 0) rec[-3] rec[-51]
DETECTOR(22, 8, 0) rec[-1] rec[-49]
DETECTOR(2, 10, 0) rec[-42] rec[-90]
DETECTOR(6, 10, 0) rec[-80] rec[-32]
DETECTOR(14, 10, 0) rec[-66] rec[-18]
DETECTOR(18, 10, 0) rec[-56] rec[-8]
TICK
RX 1 3 12 13 14 15 16 22 23 24 25 26 34 35 36 37 38 44 45 46 47 48 56 57 58 59 60 66 67 68 69 70 78 79 80 81 82 88 89 90 91 92 100 101 102 103 104 110 111 112 113 114 123 125
R 61 62 63 64 65
TICK
CX 12 6 14 8 16 10 24 18 26 20 34 28 36 30 38 32 46 40 48 42 56 50 58 52 60 54 68 62 70 64 78 72 80 74 82 76 90 84 92 86 100 94 102 96 104 98 112 106 114 108
CZ 13 7 15 9 23 17 25 19 35 29 37 31 45 39 47 41 57 51 59 53 67 61 69 63 79 73 81 75 89 83 91 85 101 95 103 97 111 105 113 107 123 117 125 119
TICK
CX 12 17 14 19 16 21 24 29 26 31 34 39 36 41 38 43 46 51 48 53 56 61 58 63 60 65 68 73 70 75 78 83 80 85 82 87 90 95 92 97 100 105 102 107 104 109 112 117 114 119
CZ 13 8 15 10 23 18 25 20 35 30 37 32 45 40 47 42 57 52 59 54 67 62 69 64 79 74 81 76 89 84 91 86 101 96 103 98 111 106 113 108 123 118 125 120
TICK
CX 12 7 14 9 22 17 24 19 26 21 34 29 36 31 44 39 46 41 48 43 56 51 58 53 66 61 68 63 70 65 78 73 80 75 88 83 90 85 92 87 100 95 102 97 110 105 112 107 114 109
CZ 1 6 3 8 13 18 15 20 23 28 25 30 35 40 37 42 45 50 47 52 57 62 59 64 67 72 69 74 79 84 81 86 89 94 91 96 101 106 103 108 111 116 113 118
TICK
CX 12 18 14 20 22 28 24 30 26 32 34 40 36 42 44 50 46 52 48 54 56 62 58 64 66 72 68 74 70 76 78 84 80 86 88 94 90 96 92 98 100 106 102 108 110 116 112 118 114 120
CZ 1 7 3 9 13 19 15 21 23 29 25 31 35 41 37 43 45 51 47 53 57 63 59 65 67 73 69 75 79 85 81 87 89 95 91 97 101 107 103 109 111 117 113 119
TICK
TICK
MX 1 3 12 13 14 15 16 22 23 24 25 26 34 35 36 37 38 44 45 46 47 48 56 57 58 59 60 66 67 68 69 70 78 79 80 81 82 88 89 90 91 92 100 101 102 103 104 110 111 112 113 114 123 125
DETECTOR(4, 0, 0) rec[-47] rec[-95]
DETECTOR(8, 0, 0) rec[-37] rec[-85]
DETECTOR(16, 0, 0) rec[-17] rec[-71]
DETECTOR(20, 0, 0) rec[-61] rec[-7]
DETECTOR(0, 2, 0) rec[-54] rec[-102]
DETECTOR(2, 2, 0) rec[-52] rec[-100]
DETECTOR(4, 2, 0) rec[-94] rec[-46]
DETECTOR(6, 2, 0) rec[-42] rec[-90]
DETECTOR(8, 2, 0) rec[-36] rec[-84]
DETECTOR(12, 2, 0) rec[-78] rec[-26]
DETECTOR(14, 2, 0) rec[-22] rec[-76]
DETECTOR(16, 2, 0) rec[-16] rec[-70]
DETECTOR(18, 2, 0) rec[-66] rec[-12]
DETECTOR(20, 2, 0) rec[-60] rec[-6]
DETECTOR(2, 4, 0) rec[-51] rec[-99]
DETECTOR(4, 4, 0) rec[-45] rec[-93]
DETECTOR(6, 4, 0) rec[-89] rec[-41]
DETECTOR(8, 4, 0) rec[-83] rec[-35]
DETECTOR(10, 4, 0) rec[-80] rec[-31]
DETECTOR(14, 4, 0) rec[-75] rec[-21]
DETECTOR(16, 4, 0) rec[-69] rec[-15]
DETECTOR(18, 4, 0) rec[-65] rec[-11]
DETECTOR(20, 4, 0) rec[-59] rec[-5]
DETECTOR(22, 4, 0) rec[-2] rec[-56]
DETECTOR(0, 6, 0) rec[-101] rec[-53]
DETECTOR(2, 6, 0) rec[-50] rec[-98]
DETECTOR(4, 6, 0) rec[-44] rec[-92]
DETECTOR(6, 6, 0) rec[-88] rec[-40]
DETECTOR(8, 6, 0) rec[-82] rec[-34]
DETECTOR(12, 6, 0) rec[-77] rec[-24]
DETECTOR(14, 6, 0) rec[-20] rec[-74]
DETECTOR(16, 6, 0) rec[-14] rec[-68]
DETECTOR(18, 6, 0) rec[-10] rec[-64]
DETECTOR(20, 6, 0) rec[-4] rec[-58]
DETECTOR(2, 8, 0) rec[-97] rec[-49]
DETECTOR(4, 8, 0) rec[-91] rec[-43]
DETECTOR(6, 8, 0) rec[-87] rec[-39]
DETECTOR(8, 8, 0) rec[-33] rec[-81]
DETECTOR(10, 8, 0) rec[-29] rec[-79]
DETECTOR(14, 8, 0) rec[-73] rec[-19]
DETECTOR(16, 8, 0) rec[-67] rec[-13]
DETECTOR(18, 8, 0) rec[-9] rec[-63]
DETECTOR(20, 8, 0) rec[-3] rec[-57]
DETECTOR(22, 8, 0) rec[-1] rec[-55]
DETECTOR(2, 10, 0) rec[-48] rec[-96]
DETECTOR(6, 10, 0) rec[-38] rec[-86]
DETECTOR(14, 10, 0) rec[-18] rec[-72]
DETECTOR(18, 10, 0) rec[-62] rec[-8]
REPEAT 3 {
    TICK
    RX 1 3 12 13 14 15 16 22 23 24 25 26 34 35 36 37 38 44 45 46 47 48 56 57 58 59 60 66 67 68 69 70 78 79 80 81 82 88 89 90 91 92 100 101 102 103 104 110 111 112 113 114 123 125
    TICK
    CX 12 6 14 8 16 10 24 18 26 20 34 28 36 30 38 32 46 40 48 42 56 50 58 52 60 54 68 62 70 64 78 72 80 74 82 76 90 84 92 86 100 94 102 96 104 98 112 106 114 108
    CZ 13 7 15 9 23 17 25 19 35 29 37 31 45 39 47 41 57 51 59 53 67 61 69 63 79 73 81 75 89 83 91 85 101 95 103 97 111 105 113 107 123 117 125 119
    TICK
    CX 12 17 14 19 16 21 24 29 26 31 34 39 36 41 38 43 46 51 48 53 56 61 58 63 60 65 68 73 70 75 78 83 80 85 82 87 90 95 92 97 100 105 102 107 104 109 112 117 114 119
    CZ 13 8 15 10 23 18 25 20 35 30 37 32 45 40 47 42 57 52 59 54 67 62 69 64 79 74 81 76 89 84 91 86 101 96 103 98 111 106 113 108 123 118 125 120
    TICK
    CX 12 7 14 9 22 17 24 19 26 21 34 29 36 31 44 39 46 41 48 43 56 51 58 53 66 61 68 63 70 65 78 73 80 75 88 83 90 85 92 87 100 95 102 97 110 105 112 107 114 109
    CZ 1 6 3 8 13 18 15 20 23 28 25 30 35 40 37 42 45 50 47 52 57 62 59 64 67 72 69 74 79 84 81 86 89 94 91 96 101 106 103 108 111 116 113 118
    TICK
    CX 12 18 14 20 22 28 24 30 26 32 34 40 36 42 44 50 46 52 48 54 56 62 58 64 66 72 68 74 70 76 78 84 80 86 88 94 90 96 92 98 100 106 102 108 110 116 112 118 114 120
    CZ 1 7 3 9 13 19 15 21 23 29 25 31 35 41 37 43 45 51 47 53 57 63 59 65 67 73 69 75 79 85 81 87 89 95 91 97 101 107 103 109 111 117 113 119
    TICK
    TICK
    MX 1 3 12 13 14 15 16 22 23 24 25 26 34 35 36 37 38 44 45 46 47 48 56 57 58 59 60 66 67 68 69 70 78 79 80 81 82 88 89 90 91 92 100 101 102 103 104 110 111 112 113 114 123 125
    SHIFT_COORDS(0, 0, 1)
    DETECTOR(4, 0, 0) rec[-47] rec[-101]
    DETECTOR(8, 0, 0) rec[-37] rec[-91]
    DETECTOR(12, 0, 0) rec[-27] rec[-81]
    DETECTOR(16, 0, 0) rec[-17] rec[-71]
    DETECTOR(20, 0, 0) rec[-61] rec[-7]
    DETECTOR(0, 2, 0) rec[-54] rec[-108]
    DETECTOR(2, 2, 0) rec[-52] rec[-106]
    DETECTOR(4, 2, 0) rec[-100] rec[-46]
    DETECTOR(6, 2, 0) rec[-42] rec[-96]
    DETECTOR(8, 2, 0) rec[-36] rec[-90]
    DETECTOR(10, 2, 0) rec[-32] rec[-86]
    DETECTOR(12, 2, 0) rec[-80] rec[-26]
    DETECTOR(14, 2, 0) rec[-22] rec[-76]
    DETECTOR(16, 2, 0) rec[-16] rec[-70]
    DETECTOR(18, 2, 0) rec[-66] rec[-12]
    DETECTOR(20, 2, 0) rec[-60] rec[-6]
    DETECTOR(2, 4, 0) rec[-51] rec[-105]
    DETECTOR(4, 4, 0) rec[-45] rec[-99]
    DETECTOR(6, 4, 0) rec[-41] rec[-95]
    DETECTOR(8, 4, 0) rec[-89] rec[-35]
    DETECTOR(10, 4, 0) rec[-31] rec[-85]
    DETECTOR(12, 4, 0) rec[-79] rec[-25]
    DETECTOR(14, 4, 0) rec[-75] rec[-21]
    DETECTOR(16, 4, 0) rec[-69] rec[-15]
    DETECTOR(18, 4, 0) rec[-65] rec[-11]
    DETECTOR(20, 4, 0) rec[-59] rec[-5]
    DETECTOR(22, 4, 0) rec[-2] rec[-56]
    DETECTOR(0, 6, 0) rec[-107] rec[-53]
    DETECTOR(2, 6, 0) rec[-50] rec[-104]
    DETECTOR(4, 6, 0) rec[-44] rec[-98]
    DETECTOR(6, 6, 0) rec[-94] rec[-40]
    DETECTOR(8, 6, 0) rec[-34] rec[-88]
    DETECTOR(10, 6, 0) rec[-30] rec[-84]
    DETECTOR(12, 6, 0) rec[-78] rec[-24]
    DETECTOR(14, 6, 0) rec[-20] rec[-74]
    DETECTOR(16, 6, 0) rec[-14] rec[-68]
    DETECTOR(18, 6, 0) rec[-10] rec[-64]
    DETECTOR(20, 6, 0) rec[-4] rec[-58]
    DETECTOR(2, 8, 0) rec[-103] rec[-49]
    DETECTOR(4, 8, 0) rec[-97] rec[-43]
    DETECTOR(6, 8, 0) rec[-93] rec[-39]
    DETECTOR(8, 8, 0) rec[-33] rec[-87]
    DETECTOR(10, 8, 0) rec[-29] rec[-83]
    DETECTOR(12, 8, 0) rec[-23] rec[-77]
    DETECTOR(14, 8, 0) rec[-73] rec[-19]
    DETECTOR(16, 8, 0) rec[-67] rec[-13]
    DETECTOR(18, 8, 0) rec[-9] rec[-63]
    DETECTOR(20, 8, 0) rec[-3] rec[-57]
    DETECTOR(22, 8, 0) rec[-1] rec[-55]
    DETECTOR(2, 10, 0) rec[-48] rec[-102]
    DETECTOR(6, 10, 0) rec[-38] rec[-92]
    DETECTOR(10, 10, 0) rec[-28] rec[-82]
    DETECTOR(14, 10, 0) rec[-18] rec[-72]
    DETECTOR(18, 10, 0) rec[-62] rec[-8]
}
TICK
RX 1 3 12 13 14 15 16 22 23 24 25 26 34 35 36 37 38 44 45 46 47 48 56 57 58 59 60 66 67 68 69 70 78 79 80 81 82 88 89 90 91 92 100 101 102 103 104 110 111 112 113 114 123 125
TICK
CX 12 6 14 8 16 10 24 18 26 20 34 28 36 30 38 32 46 40 48 42 56 50 58 52 60 54 68 62 70 64 78 72 80 74 82 76 90 84 92 86 100 94 102 96 104 98 112 106 114 108
CZ 13 7 15 9 23 17 25 19 35 29 37 31 45 39 47 41 57 51 59 53 67 61 69 63 79 73 81 75 89 83 91 85 101 95 103 97 111 105 113 107 123 117 125 119
TICK
CX 12 17 14 19 16 21 24 29 26 31 34 39 36 41 38 43 46 51 48 53 56 61 58 63 60 65 68 73 70 75 78 83 80 85 82 87 90 95 92 97 100 105 102 107 104 109 112 117 114 119
CZ 13 8 15 10 23 18 25 20 35 30 37 32 45 40 47 42 57 52 59 54 67 62 69 64 79 74 81 76 89 84 91 86 101 96 103 98 111 106 113 108 123 118 125 120
TICK
CX 12 7 14 9 22 17 24 19 26 21 34 29 36 31 44 39 46 41 48 43 56 51 58 53 66 61 68 63 70 65 78 73 80 75 88 83 90 85 92 87 100 95 102 97 110 105 112 107 114 109
CZ 1 6 3 8 13 18 15 20 23 28 25 30 35 40 37 42 45 50 47 52 57 62 59 64 67 72 69 74 79 84 81 86 89 94 91 96 101 106 103 108 111 116 113 118
TICK
CX 12 18 14 20 22 28 24 30 26 32 34 40 36 42 44 50 46 52 48 54 56 62 58 64 66 72 68 74 70 76 78 84 80 86 88 94 90 96 92 98 100 106 102 108 110 116 112 118 114 120
CZ 1 7 3 9 13 19 15 21 23 29 25 31 35 41 37 43 45 51 47 53 57 63 59 65 67 73 69 75 79 85 81 87 89 95 91 97 101 107 103 109 111 117 113 119
TICK
TICK
MX 1 3 12 13 14 15 16 22 23 24 25 26 34 35 36 37 38 44 45 46 47 48 56 57 58 59 60 66 67 68 69 70 78 79 80 81 82 88 89 90 91 92 100 101 102 103 104 110 111 112 113 114 123 125
M 6 7 8 9 10 17 18 19 20 21 28 29 30 31 32 39 40 41 42 43 50 51 52 53 54 61 62 63 64 65 72 73 74 75 76 83 84 85 86 87 94 95 96 97 98 105 106 107 108 109 116 117 118 119 120
DETECTOR(4, 0, 0) rec[-102] rec[-156]
DETECTOR(8, 0, 0) rec[-146] rec[-92]
DETECTOR(12, 0, 0) rec[-136] rec[-82]
DETECTOR(16, 0, 0) rec[-126] rec[-72]
DETECTOR(20, 0, 0) rec[-62] rec[-116]
DETECTOR(0, 2, 0) rec[-54] rec[-109] rec[-55]
DETECTOR(0, 2, 0) rec[-163] rec[-109]
DETECTOR(2, 2, 0) rec[-107] rec[-161]
DETECTOR(4, 2, 0) rec[-101] rec[-44] rec[-45] rec[-50] rec[-49]
DETECTOR(4, 2, 0) rec[-101] rec[-155]
DETECTOR(6, 2, 0) rec[-151] rec[-97]
DETECTOR(8, 2, 0) rec[-34] rec[-35] rec[-39] rec[-40] rec[-91]
DETECTOR(8, 2, 0) rec[-145] rec[-91]
DETECTOR(10, 2, 0) rec[-141] rec[-87]
DETECTOR(12, 2, 0) rec[-25] rec[-29] rec[-24] rec[-81] rec[-30]
DETECTOR(12, 2, 0) rec[-81] rec[-135]
DETECTOR(14, 2, 0) rec[-77] rec[-131]
DETECTOR(16, 2, 0) rec[-19] rec[-14] rec[-71] rec[-15] rec[-20]
DETECTOR(16, 2, 0) rec[-125] rec[-71]
DETECTOR(18, 2, 0) rec[-67] rec[-121]
DETECTOR(20, 2, 0) rec[-10] rec[-5] rec[-61] rec[-4] rec[-9]
DETECTOR(20, 2, 0) rec[-115] rec[-61]
DETECTOR(2, 4, 0) rec[-48] rec[-106] rec[-53] rec[-54] rec[-49]
DETECTOR(2, 4, 0) rec[-160] rec[-106]
DETECTOR(4, 4, 0) rec[-100] rec[-154]
DETECTOR(6, 4, 0) rec[-38] rec[-43] rec[-96] rec[-44] rec[-39]
DETECTOR(6, 4, 0) rec[-96] rec[-150]
DETECTOR(8, 4, 0) rec[-144] rec[-90]
DETECTOR(10, 4, 0) rec[-33] rec[-28] rec[-29] rec[-86] rec[-34]
DETECTOR(10, 4, 0) rec[-86] rec[-140]
DETECTOR(12, 4, 0) rec[-134] rec[-80]
DETECTOR(14, 4, 0) rec[-19] rec[-23] rec[-18] rec[-76] rec[-24]
DETECTOR(14, 4, 0) rec[-76] rec[-130]
DETECTOR(16, 4, 0) rec[-124] rec[-70]
DETECTOR(18, 4, 0) rec[-14] rec[-13] rec[-8] rec[-66] rec[-9]
DETECTOR(18, 4, 0) rec[-120] rec[-66]
DETECTOR(20, 4, 0) rec[-60] rec[-114]
DETECTOR(22, 4, 0) rec[-57] rec[-4] rec[-3]
DETECTOR(22, 4, 0) rec[-111] rec[-57]
DETECTOR(0, 6, 0) rec[-52] rec[-108] rec[-53]
DETECTOR(0, 6, 0) rec[-162] rec[-108]
DETECTOR(2, 6, 0) rec[-159] rec[-105]
DETECTOR(4, 6, 0) rec[-43] rec[-48] rec[-42] rec[-99] rec[-47]
DETECTOR(4, 6, 0) rec[-99] rec[-153]
DETECTOR(6, 6, 0) rec[-149] rec[-95]
DETECTOR(8, 6, 0) rec[-89] rec[-33] rec[-38] rec[-32] rec[-37]
DETECTOR(8, 6, 0) rec[-89] rec[-143]
DETECTOR(10, 6, 0) rec[-85] rec[-139]
DETECTOR(12, 6, 0) rec[-28] rec[-23] rec[-27] rec[-22] rec[-79]
DETECTOR(12, 6, 0) rec[-79] rec[-133]
DETECTOR(14, 6, 0) rec[-75] rec[-129]
DETECTOR(16, 6, 0) rec[-69] rec[-18] rec[-13] rec[-17] rec[-12]
DETECTOR(16, 6, 0) rec[-123] rec[-69]
DETECTOR(18, 6, 0) rec[-119] rec[-65]
DETECTOR(20, 6, 0) rec[-3] rec[-7] rec[-2] rec[-59] rec[-8]
DETECTOR(20, 6, 0) rec[-113] rec[-59]
DETECTOR(2, 8, 0) rec[-46] rec[-104] rec[-47] rec[-52] rec[-51]
DETECTOR(2, 8, 0) rec[-104] rec[-158]
DETECTOR(4, 8, 0) rec[-98] rec[-152]
DETECTOR(6, 8, 0) rec[-42] rec[-37] rec[-36] rec[-41] rec[-94]
DETECTOR(6, 8, 0) rec[-148] rec[-94]
DETECTOR(8, 8, 0) rec[-88] rec[-142]
DETECTOR(10, 8, 0) rec[-27] rec[-32] rec[-26] rec[-84] rec[-31]
DETECTOR(10, 8, 0) rec[-84] rec[-138]
DETECTOR(12, 8, 0) rec[-78] rec[-132]
DETECTOR(14, 8, 0) rec[-16] rec[-74] rec[-21] rec[-22] rec[-17]
DETECTOR(14, 8, 0) rec[-128] rec[-74]
DETECTOR(16, 8, 0) rec[-122] rec[-68]
DETECTOR(18, 8, 0) rec[-11] rec[-7] rec[-6] rec[-64] rec[-12]
DETECTOR(18, 8, 0) rec[-64] rec[-118]
DETECTOR(20, 8, 0) rec[-58] rec[-112]
DETECTOR(22, 8, 0) rec[-1] rec[-2] rec[-56]
DETECTOR(22, 8, 0) rec[-56] rec[-110]
DETECTOR(2, 10, 0) rec[-103] rec[-157]
DETECTOR(6, 10, 0) rec[-147] rec[-93]
DETECTOR(10, 10, 0) rec[-137] rec[-83]
DETECTOR(14, 10, 0) rec[-73] rec[-127]
DETECTOR(18, 10, 0) rec[-63] rec[-117]
</pre>
</details>

which seems valid.

Linked issue #502: Add efficient automatic detector computation to the layer approach
https://github.com/tqec/tqec/pull/476 introduces a new approach based on layers to represent topological quantum computations inside `tqec`. That new representation is supposed to replace the existing [CompiledGraph](https://github.com/tqec/tqec/blob/2f7c0a3d01ffb0fedf307c7693a133f73847e9f0/src/tqec/compile/compile.py#L43), but it not currently capable of doing so due to missing capabilities. These missing parts are listed in https://github.com/tqec/tqec/pull/476#issuecomment-2688930874.

This issue is about the **Implementing detector computation** section in the above comment.

### Current architecture

Currently, automatic detector computation is done by calling `CompiledGraph._inplace_add_detectors_to_circuits` ([here](https://github.com/tqec/tqec/blob/2f7c0a3d01ffb0fedf307c7693a133f73847e9f0/src/tqec/compile/compile.py#L137)), that in turns repeatedly calls [`compute_detectors_for_fixed_radius`](https://github.com/tqec/tqec/blob/2f7c0a3d01ffb0fedf307c7693a133f73847e9f0/src/tqec/compile/detectors/compute.py#L473).

This pipeline relies heavily on the fact that we represent the computation as a sequence of `LayoutTemplate` with accompanying `Plaquette` instances, each representing a temporal slice of the topological quantum computation.

### How to change the implementation

As a first goal, I think that the easiest and quickest way of solving this issue will be to re-use `LayoutTemplate`. That means that solving this issue amounts at:

- getting a valid `LayoutTemplate`/`Plaquettes` pair from a `LayoutLayer`,
- use the current pipeline on the resulting `LayoutTemplate`/`Plaquettes` pair.

That approach has a huge limitation: it does not support anything else than layers represented using `Template`/`Plaquettes`. Basically, if the `TopologicalComputationGraph` contains one or more layer(s) that is not a `PlaquetteLayer`, this approach does not work anymore.
That will be the case as soon as we will have the Y-basis reset/measurement half-block implemented, or magic state distillation / cultivation. But this approach will be good enough for the moment.

Linked issue comments: user: login: inmzhang
created_at: 2025-03-10T11:24:34Z
body: > That will be the case as soon as we will have the Y-basis reset/measurement half-block implemented, or magic state distillation / cultivation. But this approach will be good enough for the moment.

Detectors in the raw circuits like Y-basis init/meas can be added directly in the circuit factory, while some tweaks might be needed around the time boundary. And the same for adding observables.
author_association: CONTRIBUTOR
user: login: Zhaoyilunnn
created_at: 2025-03-10T12:23:33Z
body: 
> 
> * getting a valid `LayoutTemplate`/`Plaquettes` pair from a `LayoutLayer`,
> * use the current pipeline on the resulting `LayoutTemplate`/`Plaquettes` pair.
> 

If I understand correctly,  this also works for spatial junctions. For more complicated scenarios, comments from @inmzhang make sense to me, although I don't yet have the details in mind.

I can take this issue if alternative approach is not a must.

Thanks


author_association: CONTRIBUTOR
user: login: Zhaoyilunnn
created_at: 2025-03-11T10:54:52Z
body: Hi @nelimee, sorry I just get some other urgent tasks to to and may not be able to work on this issue till next week. Please feel free to unassign me.
author_association: CONTRIBUTOR
user: login: nelimee
created_at: 2025-03-11T11:01:58Z
body: > Hi [@nelimee](https://github.com/nelimee), sorry I just get some other urgent tasks to to and may not be able to work on this issue till next week. Please feel free to unassign me.

Thanks for telling that. I'll keep you assigned for the moment, but might have a try at it during the week if my schedule allows it.
author_association: CONTRIBUTOR

Diff:
diff --git a/src/tqec/compile/tree/annotations.py b/src/tqec/compile/tree/annotations.py
index f6b1ad4d5..0f9b3387b 100644
--- a/src/tqec/compile/tree/annotations.py
+++ b/src/tqec/compile/tree/annotations.py
@@ -1,10 +1,14 @@
+from __future__ import annotations
+
 from dataclasses import dataclass, field
 from typing import Any
 
 import stim
 
+from tqec.circuit.measurement_map import MeasurementRecordsMap
 from tqec.circuit.qubit_map import QubitMap
 from tqec.circuit.schedule.circuit import ScheduledCircuit
+from tqec.compile.detectors.detector import Detector
 from tqec.utils.coordinates import StimCoordinates
 from tqec.utils.exceptions import TQECException
 
@@ -12,11 +16,7 @@
 @dataclass(frozen=True)
 class DetectorAnnotation:
     """An annotation that should include all the necessary information to build a
-    DETECTOR instruction.
-
-    Todo:
-        Will change according to the needs.
-    """
+    ``DETECTOR`` instruction."""
 
     coordinates: StimCoordinates
     measurement_offsets: list[int]
@@ -32,6 +32,17 @@ def to_instruction(self) -> stim.CircuitInstruction:
             self.coordinates.to_stim_coordinates(),
         )
 
+    @staticmethod
+    def from_detector(
+        detector: Detector, measurement_records: MeasurementRecordsMap
+    ) -> DetectorAnnotation:
+        """Create a :class:`DetectorAnnotation` from a detector and a list of
+        measurement records."""
+        return DetectorAnnotation(
+            detector.coordinates,
+            [measurement_records[m.qubit][m.offset] for m in detector.measurements],
+        )
+
 
 @dataclass(frozen=True)
 class ObservableAnnotation:
diff --git a/src/tqec/compile/tree/annotators/detectors.py b/src/tqec/compile/tree/annotators/detectors.py
new file mode 100644
index 000000000..43fea08a2
--- /dev/null
+++ b/src/tqec/compile/tree/annotators/detectors.py
@@ -0,0 +1,241 @@
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+
+from typing_extensions import override
+
+from tqec.circuit.measurement_map import MeasurementRecordsMap
+from tqec.compile.blocks.layers.atomic.layout import LayoutLayer
+from tqec.compile.detectors.compute import compute_detectors_for_fixed_radius
+from tqec.compile.detectors.database import DetectorDatabase
+from tqec.compile.tree.annotations import DetectorAnnotation
+from tqec.compile.tree.node import LayerNode, NodeWalker
+from tqec.plaquette.plaquette import Plaquettes
+from tqec.templates.base import Template
+from tqec.utils.exceptions import TQECException
+
+
+@dataclass(frozen=True)
+class LookbackInformation:
+    """Stores data on one QEC round.
+
+    This data-structure is used to store information that might be useful to
+    compute detectors. It only represents one QEC round.
+
+    Attributes:
+        template: template representing the QEC round.
+        plaquettes: plaquettes that can be used in conjunction with
+            ``self.template`` to generate the quantum circuit representing the
+            QEC round.
+        measurement_records: all the measurement records of the QEC round
+            represented by ``self``. This could in theory be computed from
+            ``self.template`` and ``self.plaquettes`` by generating the quantum
+            circuit and then extracting the measurement records from it, but it
+            turns out that we already have access to these records when creating
+            such a structure, so we store them to avoid re-computing.
+    """
+
+    template: Template
+    plaquettes: Plaquettes
+    measurement_records: MeasurementRecordsMap
+
+
+@dataclass
+class LookbackInformationList:
+    """A sequence of :class:`LookbackInformation` instances."""
+
+    infos: list[LookbackInformation] = field(default_factory=list)
+
+    def append(
+        self,
+        template: Template,
+        plaquettes: Plaquettes,
+        measurement_records: MeasurementRecordsMap,
+    ) -> None:
+        """Add the provided parameters to the lookback window, potentially removing
+        older items that should not be considered anymore."""
+        self.infos.append(
+            LookbackInformation(template, plaquettes, measurement_records)
+        )
+
+    def extend(self, other: LookbackInformationList, repetitions: int = 1) -> None:
+        self.infos.extend(other.infos * repetitions)
+
+    def __len__(self) -> int:
+        return len(self.infos)
+
+    def __getitem__(
+        self, index: int | slice
+    ) -> LookbackInformation | list[LookbackInformation]:
+        return self.infos[index]
+
+
+class LookbackStack:
+    def __init__(self) -> None:
+        """Initialise the lookback stack that can be used to query the current
+        state for detector computation.
+
+        This data-structure keeps information about the past QEC rounds in order
+        to be able to query them and help in detector computation by only
+        considering the ``N`` last rounds.
+
+        In particular, this data-structure is useful to keep track of previous
+        rounds in the presence of ``REPEAT`` blocks.
+        """
+        self._stack: list[LookbackInformationList] = [LookbackInformationList()]
+
+    def enter_repeat_block(self) -> None:
+        self._stack.append(LookbackInformationList())
+
+    def close_repeat_block(self, repetitions: int) -> None:
+        if len(self._stack) < 2:
+            raise TQECException(
+                f"Only got {len(self._stack)} < 2 entries in the stack. That "
+                "means that we are not in a REPEAT block. Cannot call "
+                "close_repeat_block()."
+            )
+        if repetitions < 1:
+            raise TQECException(
+                "Cannot have a REPEAT block with less than 1 repetitions. Got "
+                f"{repetitions} repetitions."
+            )
+        self._stack[-2].extend(self._stack[-1], repetitions)
+        self._stack.pop(-1)
+
+    def append(
+        self,
+        template: Template,
+        plaquettes: Plaquettes,
+        measurement_records: MeasurementRecordsMap,
+    ) -> None:
+        """Append a new QEC round in the data-structure."""
+        self._stack[-1].append(template, plaquettes, measurement_records)
+
+    def _get_last_n(
+        self, n: int
+    ) -> tuple[list[Template], list[Plaquettes], list[MeasurementRecordsMap]]:
+        if n < 0:
+            raise TQECException(
+                "Cannot look back a negative number of rounds. Got a lookback "
+                f"value of {n}."
+            )
+        if n == 0:
+            return [], [], []
+        templates: list[Template] = []
+        plaquettes: list[Plaquettes] = []
+        measurement_records: list[MeasurementRecordsMap] = []
+        # Filling the lists in reverse order (i.e., from earlier time to oldest
+        # time) and correcting when returning.
+        for element in reversed(self._stack):
+            for info in reversed(element.infos):
+                templates.append(info.template)
+                plaquettes.append(info.plaquettes)
+                measurement_records.append(info.measurement_records)
+                if len(templates) == n:
+                    return templates[::-1], plaquettes[::-1], measurement_records[::-1]
+
+        return templates[::-1], plaquettes[::-1], measurement_records[::-1]
+
+    def lookback(
+        self,
+        n: int,
+    ) -> tuple[list[Template], list[Plaquettes], MeasurementRecordsMap]:
+        """Get the last ``self._lookback`` QEC rounds."""
+        templates, plaquettes, measurement_records = self._get_last_n(n)
+        measurement_record = MeasurementRecordsMap()
+        for mrec in measurement_records:
+            measurement_record = measurement_record.with_added_measurements(mrec)
+        return templates, plaquettes, measurement_record
+
+    def __len__(self) -> int:
+        if len(self._stack) > 1:
+            raise TQECException(
+                "Cannot get a meaningful stack length when a REPEAT block is "
+                "in construction."
+            )
+        return len(self._stack[0])
+
+
+class AnnotateDetectorsOnLayerNode(NodeWalker):
+    def __init__(
+        self,
+        k: int,
+        manhattan_radius: int = 2,
+        detector_database: DetectorDatabase | None = None,
+        lookback: int = 2,
+    ):
+        """Walker computing and annotating detectors on leaf nodes.
+
+        This class keeps track of the ``lookback`` previous leaf nodes seen and
+        uses them to automatically compute the detectors at all the leaf nodes
+        it encounters.
+
+        Args:
+            k: scaling factor.
+            manhattan_radius: Parameter for the automatic computation of detectors.
+                Should be large enough so that flows cancelling each other to
+                form a detector are strictly contained in plaquettes that are at
+                most at a distance of ``manhattan_radius`` from the central
+                plaquette. Detector computation runtime grows with this parameter,
+                so you should try to keep it to its minimum. A value too low might
+                produce invalid detectors.
+            detector_database: existing database of detectors that is used to
+                avoid computing detectors if the database already contains them.
+                Default to `None` which result in not using any kind of database
+                and unconditionally performing the detector computation.
+            lookback_size: number of QEC rounds to consider to try to find
+                detectors. Including more rounds increases computation time.
+        """
+        if lookback < 1:
+            raise TQECException(
+                "Cannot compute detectors without any layer. The `lookback` "
+                f"parameter should be >= 1 but got {lookback}."
+            )
+        self._k = k
+        self._manhattan_radius = manhattan_radius
+        self._database = detector_database or DetectorDatabase()
+        self._lookback_size = lookback
+        self._lookback_stack = LookbackStack()
+
+    @override
+    def visit_node(self, node: LayerNode) -> None:
+        if not isinstance(node._layer, LayoutLayer):
+            return
+        annotations = node.get_annotations(self._k)
+        if annotations.circuit is None:
+            raise TQECException(
+                "Cannot compute detectors without the circuit annotation."
+            )
+        self._lookback_stack.append(
+            *node._layer.to_template_and_plaquettes(),
+            MeasurementRecordsMap.from_scheduled_circuit(annotations.circuit),
+        )
+        templates, plaquettes, measurement_records = self._lookback_stack.lookback(
+            self._lookback_size
+        )
+
+        detectors = compute_detectors_for_fixed_radius(
+            templates, self._k, plaquettes, self._manhattan_radius, self._database
+        )
+        for detector in detectors:
+            annotations.detectors.append(
+                DetectorAnnotation.from_detector(detector, measurement_records)
+            )
+
+    @override
+    def enter_node(self, node: LayerNode) -> None:
+        if node.is_repeated:
+            self._lookback_stack.enter_repeat_block()
+
+    @override
+    def exit_node(self, node: LayerNode) -> None:
+        if not node.is_repeated:
+            return
+        # Note: this is the place to perform checks. In particular, checking that
+        # detectors computed at the first repetition of the REPEAT block are also
+        # valid at any repetitions. This is a requirement for the REPEAT block to
+        # make sense, but that would be nice to include a check to avoid
+        # misleadingly include detectors that are incorrect sometimes.
+        repetitions = node.repetitions
+        assert repetitions is not None
+        self._lookback_stack.close_repeat_block(repetitions.integer_eval(self._k))
diff --git a/src/tqec/compile/tree/annotators/detectors_test.py b/src/tqec/compile/tree/annotators/detectors_test.py
new file mode 100644
index 000000000..7397b5892
--- /dev/null
+++ b/src/tqec/compile/tree/annotators/detectors_test.py
@@ -0,0 +1,163 @@
+import pytest
+
+from tqec.circuit.measurement_map import MeasurementRecordsMap
+from tqec.compile.tree.annotators.detectors import LookbackStack
+from tqec.plaquette.library.empty import empty_square_plaquette
+from tqec.plaquette.plaquette import Plaquettes
+from tqec.templates.base import Template
+from tqec.templates.qubit import QubitTemplate
+from tqec.utils.exceptions import TQECException
+from tqec.utils.frozendefaultdict import FrozenDefaultDict
+
+
+@pytest.fixture(name="template")
+def template_fixture() -> Template:
+    return QubitTemplate()
+
+
+@pytest.fixture(name="plaquettes")
+def plaquettes_fixture() -> Plaquettes:
+    return Plaquettes(FrozenDefaultDict({}, default_factory=empty_square_plaquette))
+
+
+@pytest.fixture(name="measurement_records")
+def measurement_records_fixture() -> MeasurementRecordsMap:
+    return MeasurementRecordsMap()
+
+
+def test_stack_creation() -> None:
+    LookbackStack()
+
+
+def test_stack_append(
+    template: Template,
+    plaquettes: Plaquettes,
+    measurement_records: MeasurementRecordsMap,
+) -> None:
+    stack = LookbackStack()
+    stack.append(template, plaquettes, measurement_records)
+    assert len(stack) == 1
+
+
+def test_stack_len(
+    template: Template,
+    plaquettes: Plaquettes,
+    measurement_records: MeasurementRecordsMap,
+) -> None:
+    stack = LookbackStack()
+    for i in range(12):
+        assert len(stack) == i
+        stack.append(template, plaquettes, measurement_records)
+        assert len(stack) == i + 1
+    stack.enter_repeat_block()
+    with pytest.raises(
+        TQECException,
+        match="^Cannot get a meaningful stack length when a REPEAT block is in construction.$",
+    ):
+        len(stack)
+    stack.enter_repeat_block()
+    with pytest.raises(
+        TQECException,
+        match="^Cannot get a meaningful stack length when a REPEAT block is in construction.$",
+    ):
+        len(stack)
+    stack.close_repeat_block(1)
+    with pytest.raises(
+        TQECException,
+        match="^Cannot get a meaningful stack length when a REPEAT block is in construction.$",
+    ):
+        len(stack)
+    stack.close_repeat_block(1)
+    assert len(stack) == 12
+
+
+def test_stack_repeat_block_append(
+    template: Template,
+    plaquettes: Plaquettes,
+    measurement_records: MeasurementRecordsMap,
+) -> None:
+    stack = LookbackStack()
+    stack.append(template, plaquettes, measurement_records)
+    stack.enter_repeat_block()
+    stack.append(template, plaquettes, measurement_records)
+    stack.append(template, plaquettes, measurement_records)
+    stack.close_repeat_block(2)
+    assert len(stack) == 5
+    stack.enter_repeat_block()
+    stack.close_repeat_block(4)
+    assert len(stack) == 5
+    stack.enter_repeat_block()
+    stack.enter_repeat_block()
+    stack.append(template, plaquettes, measurement_records)
+    stack.close_repeat_block(3)
+    stack.close_repeat_block(3)
+    assert len(stack) == 14
+
+
+def test_stack_erroneous_repeat_block() -> None:
+    stack = LookbackStack()
+    with pytest.raises(TQECException, match="Only got 1 < 2 entries in the stack..*"):
+        stack.close_repeat_block(4)
+    stack.enter_repeat_block()
+    with pytest.raises(
+        TQECException,
+        match="Cannot have a REPEAT block with less than 1 repetitions..*",
+    ):
+        stack.close_repeat_block(0)
+
+
+def test_stack_lookback(
+    template: Template,
+    plaquettes: Plaquettes,
+    measurement_records: MeasurementRecordsMap,
+) -> None:
+    stack = LookbackStack()
+    with pytest.raises(
+        TQECException, match="Cannot look back a negative number of rounds..*"
+    ):
+        stack.lookback(-1)
+    # Valid query on empty stack.
+    ts, ps, _ = stack.lookback(0)
+    assert len(ts) == 0
+    assert len(ps) == 0
+    ts, ps, _ = stack.lookback(3)
+    assert len(ts) == 0
+    assert len(ps) == 0
+
+    stack.append(template, plaquettes, measurement_records)
+    # Valid query on a non-empty stack.
+    ts, ps, _ = stack.lookback(0)
+    assert len(ts) == 0
+    assert len(ps) == 0
+    ts, ps, _ = stack.lookback(3)
+    assert len(ts) == 1
+    assert len(ps) == 1
+
+    stack.enter_repeat_block()
+    # Valid query on a non-empty stack, without any entries in the current repeat block.
+    ts, ps, _ = stack.lookback(3)
+    assert len(ts) == 1
+    assert len(ps) == 1
+
+    stack.append(template, plaquettes, measurement_records)
+    stack.append(template, plaquettes, measurement_records)
+    # Valid query on a non-empty stack, with entries in the current repeat block.
+    ts, ps, _ = stack.lookback(2)
+    assert len(ts) == 2
+    assert len(ps) == 2
+    ts, ps, _ = stack.lookback(5)
+    assert len(ts) == 3
+    assert len(ps) == 3
+
+    stack.close_repeat_block(2)
+    assert len(stack) == 5  # To remember the size of the stack
+    # Valid query on a non-empty stack, with entries in a previous repeat block.
+    ts, ps, _ = stack.lookback(2)
+    assert len(ts) == 2
+    assert len(ps) == 2
+    ts, ps, _ = stack.lookback(5)
+    assert len(ts) == 5
+    assert len(ps) == 5
+    ts, ps, _ = stack.lookback(7)
+    assert len(ts) == 5
+    assert len(ps) == 5
diff --git a/src/tqec/compile/tree/node.py b/src/tqec/compile/tree/node.py
index 1120946b7..734720ca4 100644
--- a/src/tqec/compile/tree/node.py
+++ b/src/tqec/compile/tree/node.py
@@ -16,6 +16,7 @@
 from tqec.compile.tree.annotations import LayerNodeAnnotations
 from tqec.utils.coordinates import StimCoordinates
 from tqec.utils.exceptions import TQECException
+from tqec.utils.scale import LinearFunction
 
 
 def contains_only_layout_or_composed_layers(
@@ -28,6 +29,12 @@ class NodeWalker:
     def visit_node(self, node: LayerNode) -> None:
         pass
 
+    def enter_node(self, node: LayerNode) -> None:
+        pass
+
+    def exit_node(self, node: LayerNode) -> None:
+        pass
+
 
 class LayerNode:
     def __init__(
@@ -84,6 +91,14 @@ def is_repeated(self) -> bool:
         """Returns ``True`` if ``self`` stores a RepeatedLayer."""
         return isinstance(self._layer, RepeatedLayer)
 
+    @property
+    def repetitions(self) -> LinearFunction | None:
+        """Returns the number of repetitions of the repeated block if
+        ``self.is_repeated`` else ``None``."""
+        return (
+            self._layer.repetitions if isinstance(self._layer, RepeatedLayer) else None
+        )
+
     def to_dict(self) -> dict[str, Any]:
         return {
             "layer": type(self._layer).__name__,
@@ -94,14 +109,17 @@ def to_dict(self) -> dict[str, Any]:
         }
 
     def walk(self, walker: NodeWalker) -> None:
-        """Walk the tree using DFS, calling ``walker.visit_node`` on each node.
+        """Walk the tree using DFS, calling the different walker methods on each
+        node.
 
         Args:
             walker: structure that will be called on each explored node.
         """
+        walker.enter_node(self)
         walker.visit_node(self)
         for child in self._children:
             child.walk(walker)
+        walker.exit_node(self)
 
     def get_annotations(self, k: int) -> LayerNodeAnnotations:
         return self._annotations.setdefault(k, LayerNodeAnnotations())
diff --git a/src/tqec/compile/tree/tree.py b/src/tqec/compile/tree/tree.py
index 0772ddc73..9c28fb5be 100644
--- a/src/tqec/compile/tree/tree.py
+++ b/src/tqec/compile/tree/tree.py
@@ -10,6 +10,7 @@
 from tqec.compile.observables.abstract_observable import AbstractObservable
 from tqec.compile.tree.annotations import LayerTreeAnnotations
 from tqec.compile.tree.annotators.circuit import AnnotateCircuitOnLayerNode
+from tqec.compile.tree.annotators.detectors import AnnotateDetectorsOnLayerNode
 from tqec.compile.tree.node import LayerNode, NodeWalker
 from tqec.utils.exceptions import TQECException
 
@@ -87,13 +88,24 @@ def _annotate_observable(self, observable: AbstractObservable) -> None:
 
     def _annotate_detectors(
         self,
+        k: int,
         manhattan_radius: int = 2,
         detector_database: DetectorDatabase | None = None,
+        lookback: int = 2,
     ) -> None:
-        pass
+        self._root.walk(
+            AnnotateDetectorsOnLayerNode(
+                k, manhattan_radius, detector_database, lookback
+            )
+        )
 
     def generate_circuit(
-        self, k: int, include_qubit_coords: bool = True
+        self,
+        k: int,
+        include_qubit_coords: bool = True,
+        manhattan_radius: int = 2,
+        detector_database: DetectorDatabase | None = None,
+        lookback: int = 2,
     ) -> stim.Circuit:
         """Generate the quantum circuit representing ``self``.
 
@@ -104,6 +116,19 @@ def generate_circuit(
             k: scaling factor.
             include_qubit_coords: whether to include ``QUBIT_COORDS`` annotations
                 in the returned quantum circuit or not. Default to ``True``.
+            manhattan_radius: Parameter for the automatic computation of detectors.
+                Should be large enough so that flows cancelling each other to
+                form a detector are strictly contained in plaquettes that are at
+                most at a distance of ``manhattan_radius`` from the central
+                plaquette. Detector computation runtime grows with this parameter,
+                so you should try to keep it to its minimum. A value too low might
+                produce invalid detectors.
+            detector_database: existing database of detectors that is used to
+                avoid computing detectors if the database already contains them.
+                Default to `None` which result in not using any kind of database
+                and unconditionally performing the detector computation.
+            lookback: number of QEC rounds to consider to try to find detectors.
+                Including more rounds increases computation time.
 
         Returns:
             a ``stim.Circuit`` instance implementing the computation described
@@ -111,7 +136,7 @@ def generate_circuit(
         """
         self._annotate_circuits(k)
         self._annotate_qubit_map(k)
-        self._annotate_detectors(k)
+        self._annotate_detectors(k, manhattan_radius, detector_database, lookback)
         annotations = self._get_annotation(k)
         assert annotations.qubit_map is not None
 
diff --git a/src/tqec/plaquette/compilation/base.py b/src/tqec/plaquette/compilation/base.py
index 7bb9506c0..9fdf52678 100644
--- a/src/tqec/plaquette/compilation/base.py
+++ b/src/tqec/plaquette/compilation/base.py
@@ -36,7 +36,7 @@ def compile(self, plaquette: Plaquette) -> Plaquette:
         for compilation_pass in self._passes:
             circuit = compilation_pass.run(circuit)
         return Plaquette(
-            f"{plaquette.name}_{self._name}",
+            f"{self._name}({plaquette.name})",
             plaquette.qubits,
             circuit,
             self._mergeable_instructions_modifier(plaquette.mergeable_instructions),


Reviews:
user: login: inmzhang
body: LGTM. The docstring of `DetectorAnnotation` needs update.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-16T06:55:31Z
user: login: inmzhang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-16T06:59:06Z
user: login: inmzhang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-16T07:15:35Z
user: login: nelimee
body: Again, wanted to submit a comment, it started a review.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T12:51:13Z
user: login: inmzhang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T11:20:34Z
user: login: nelimee
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T11:22:55Z
user: login: nelimee
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T12:12:28Z
user: login: inmzhang
body: LGTM.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T14:25:55Z

Review comments:
diff_hunk: @@ -67,10 +68,16 @@ def _annotate_observable(self, observable: AbstractObservable) -> None:
 
     def _annotate_detectors(
         self,
+        k: int,
         manhattan_radius: int = 2,
         detector_database: DetectorDatabase | None = None,
+        lookback: int = 2,
     ) -> None:
path: src/tqec/compile/tree/tree.py
user: login: inmzhang
body: These parameters should also be added to `generate_circuit`.
created_at: 2025-03-16T06:59:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,85 @@
+from typing import MutableSequence, TypeVar
+
+from typing_extensions import override
+
+from tqec.circuit.measurement_map import MeasurementRecordsMap
+from tqec.compile.blocks.layers.atomic.layout import LayoutLayer
+from tqec.compile.detectors.compute import compute_detectors_for_fixed_radius
+from tqec.compile.detectors.database import DetectorDatabase
+from tqec.compile.tree.annotations import DetectorAnnotation
+from tqec.compile.tree.node import LayerNode, NodeWalkerInterface
+from tqec.plaquette.plaquette import Plaquettes
+from tqec.templates.base import Template
+from tqec.utils.exceptions import TQECException
+
+T = TypeVar("T")
+
+
+def _append_with_capped_size(seq: MutableSequence[T], obj: T, lookback: int) -> None:
+    seq.append(obj)
+    if len(seq) > lookback:
+        seq.pop(0)
+
+
+class AnnotateDetectorsOnLayoutNode(NodeWalkerInterface):
path: src/tqec/compile/tree/annotators/detectors.py
user: login: inmzhang
body: `AnnotateDetectorsOnLayerNode`
created_at: 2025-03-16T07:15:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,117 @@
+from typing import MutableSequence, TypeVar
+
+from typing_extensions import override
+
+from tqec.circuit.measurement_map import MeasurementRecordsMap
+from tqec.compile.blocks.layers.atomic.layout import LayoutLayer
+from tqec.compile.detectors.compute import compute_detectors_for_fixed_radius
+from tqec.compile.detectors.database import DetectorDatabase
+from tqec.compile.tree.annotations import DetectorAnnotation
+from tqec.compile.tree.node import LayerNode, NodeWalker
+from tqec.plaquette.plaquette import Plaquettes
+from tqec.templates.base import Template
+from tqec.utils.exceptions import TQECException
+
+T = TypeVar("T")
+
+
+def _append_with_capped_size(seq: MutableSequence[T], obj: T, lookback: int) -> None:
+    seq.append(obj)
+    if len(seq) > lookback:
+        seq.pop(0)
+
+
+class AnnotateDetectorsOnLayerNode(NodeWalker):
+    def __init__(
+        self,
+        k: int,
+        manhattan_radius: int = 2,
+        detector_database: DetectorDatabase | None = None,
+        lookback: int = 2,
+    ):
+        """Walker computing and annotating detectors on leaf nodes.
+
+        This class keeps track of the ``lookback`` previous leaf nodes seen and
+        uses them to automatically compute the detectors at all the leaf nodes
+        it encounters.
+
+        Args:
+            k: scaling factor.
+            manhattan_radius: Parameter for the automatic computation of detectors.
+                Should be large enough so that flows cancelling each other to
+                form a detector are strictly contained in plaquettes that are at
+                most at a distance of ``manhattan_radius`` from the central
+                plaquette. Detector computation runtime grows with this parameter,
+                so you should try to keep it to its minimum. A value too low might
+                produce invalid detectors.
+            detector_database: existing database of detectors that is used to
+                avoid computing detectors if the database already contains them.
+                Default to `None` which result in not using any kind of database
+                and unconditionally performing the detector computation.
+            lookback: number of QEC rounds to consider to try to find detectors.
+                Including more rounds increases computation time. Cannot be over
+                ``2`` for the moment.
+        """
+        if lookback > 2:
+            raise TQECException(
+                "Cannot annotate detectors by considering more than 2 QEC rounds "
+                "at the moment."
+            )
+        self._k = k
+        self._manhattan_radius = manhattan_radius
+        self._database = detector_database or DetectorDatabase()
+        self._lookback = lookback
+        self._previous_templates: list[Template] = []
+        self._previous_plaquettes: list[Plaquettes] = []
+        self._measurement_records: list[MeasurementRecordsMap] = []
+
+    def append(
+        self,
+        template: Template,
+        plaquettes: Plaquettes,
+        measurement_records: MeasurementRecordsMap,
+    ) -> None:
+        """Add the provided parameters to the lookback window, potentially removing
+        older items that should not be considered anymore."""
+        _append_with_capped_size(self._previous_templates, template, self._lookback)
+        _append_with_capped_size(self._previous_plaquettes, plaquettes, self._lookback)
+        _append_with_capped_size(
+            self._measurement_records, measurement_records, self._lookback
+        )
+
+    @property
+    def full_lookback_measurement_record(self) -> MeasurementRecordsMap:
+        """Returns the measurement records of all the measurements in the stored
+        lookback window."""
+        ret = MeasurementRecordsMap()
+        for mrecords in self._measurement_records:
+            ret = ret.with_added_measurements(mrecords)
+        return ret
+
+    @override
+    def visit_node(self, node: LayerNode) -> None:
+        if not isinstance(node._layer, LayoutLayer):
+            return
path: src/tqec/compile/tree/annotators/detectors.py
user: login: nelimee
body: It turns out that this is wrong as soon as we might consider more than just the previous layer for detector computation. The problem lies with `RepeatedLayer` instances: we need to update the `lookback` window with several instances of the same layer (the repeated one), but this is not what is currently done.

Also, I think that solving this issue might need to add one (or a few) methods to `NodeWalker`. I will have a try later.
created_at: 2025-03-17T12:50:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,117 @@
+from typing import MutableSequence, TypeVar
+
+from typing_extensions import override
+
+from tqec.circuit.measurement_map import MeasurementRecordsMap
+from tqec.compile.blocks.layers.atomic.layout import LayoutLayer
+from tqec.compile.detectors.compute import compute_detectors_for_fixed_radius
+from tqec.compile.detectors.database import DetectorDatabase
+from tqec.compile.tree.annotations import DetectorAnnotation
+from tqec.compile.tree.node import LayerNode, NodeWalker
+from tqec.plaquette.plaquette import Plaquettes
+from tqec.templates.base import Template
+from tqec.utils.exceptions import TQECException
+
+T = TypeVar("T")
+
+
+def _append_with_capped_size(seq: MutableSequence[T], obj: T, lookback: int) -> None:
+    seq.append(obj)
+    if len(seq) > lookback:
+        seq.pop(0)
+
+
+class AnnotateDetectorsOnLayerNode(NodeWalker):
+    def __init__(
+        self,
+        k: int,
+        manhattan_radius: int = 2,
+        detector_database: DetectorDatabase | None = None,
+        lookback: int = 2,
+    ):
+        """Walker computing and annotating detectors on leaf nodes.
+
+        This class keeps track of the ``lookback`` previous leaf nodes seen and
+        uses them to automatically compute the detectors at all the leaf nodes
+        it encounters.
+
+        Args:
+            k: scaling factor.
+            manhattan_radius: Parameter for the automatic computation of detectors.
+                Should be large enough so that flows cancelling each other to
+                form a detector are strictly contained in plaquettes that are at
+                most at a distance of ``manhattan_radius`` from the central
+                plaquette. Detector computation runtime grows with this parameter,
+                so you should try to keep it to its minimum. A value too low might
+                produce invalid detectors.
+            detector_database: existing database of detectors that is used to
+                avoid computing detectors if the database already contains them.
+                Default to `None` which result in not using any kind of database
+                and unconditionally performing the detector computation.
+            lookback: number of QEC rounds to consider to try to find detectors.
+                Including more rounds increases computation time. Cannot be over
+                ``2`` for the moment.
+        """
+        if lookback > 2:
+            raise TQECException(
+                "Cannot annotate detectors by considering more than 2 QEC rounds "
+                "at the moment."
+            )
+        self._k = k
+        self._manhattan_radius = manhattan_radius
+        self._database = detector_database or DetectorDatabase()
+        self._lookback = lookback
+        self._previous_templates: list[Template] = []
+        self._previous_plaquettes: list[Plaquettes] = []
+        self._measurement_records: list[MeasurementRecordsMap] = []
+
+    def append(
+        self,
+        template: Template,
+        plaquettes: Plaquettes,
+        measurement_records: MeasurementRecordsMap,
+    ) -> None:
+        """Add the provided parameters to the lookback window, potentially removing
+        older items that should not be considered anymore."""
+        _append_with_capped_size(self._previous_templates, template, self._lookback)
+        _append_with_capped_size(self._previous_plaquettes, plaquettes, self._lookback)
+        _append_with_capped_size(
+            self._measurement_records, measurement_records, self._lookback
+        )
+
+    @property
+    def full_lookback_measurement_record(self) -> MeasurementRecordsMap:
+        """Returns the measurement records of all the measurements in the stored
+        lookback window."""
+        ret = MeasurementRecordsMap()
+        for mrecords in self._measurement_records:
+            ret = ret.with_added_measurements(mrecords)
+        return ret
+
+    @override
+    def visit_node(self, node: LayerNode) -> None:
+        if not isinstance(node._layer, LayoutLayer):
+            return
path: src/tqec/compile/tree/annotators/detectors.py
user: login: inmzhang
body: Is this planned to be addressed in this PR?
created_at: 2025-03-18T11:20:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,117 @@
+from typing import MutableSequence, TypeVar
+
+from typing_extensions import override
+
+from tqec.circuit.measurement_map import MeasurementRecordsMap
+from tqec.compile.blocks.layers.atomic.layout import LayoutLayer
+from tqec.compile.detectors.compute import compute_detectors_for_fixed_radius
+from tqec.compile.detectors.database import DetectorDatabase
+from tqec.compile.tree.annotations import DetectorAnnotation
+from tqec.compile.tree.node import LayerNode, NodeWalker
+from tqec.plaquette.plaquette import Plaquettes
+from tqec.templates.base import Template
+from tqec.utils.exceptions import TQECException
+
+T = TypeVar("T")
+
+
+def _append_with_capped_size(seq: MutableSequence[T], obj: T, lookback: int) -> None:
+    seq.append(obj)
+    if len(seq) > lookback:
+        seq.pop(0)
+
+
+class AnnotateDetectorsOnLayerNode(NodeWalker):
+    def __init__(
+        self,
+        k: int,
+        manhattan_radius: int = 2,
+        detector_database: DetectorDatabase | None = None,
+        lookback: int = 2,
+    ):
+        """Walker computing and annotating detectors on leaf nodes.
+
+        This class keeps track of the ``lookback`` previous leaf nodes seen and
+        uses them to automatically compute the detectors at all the leaf nodes
+        it encounters.
+
+        Args:
+            k: scaling factor.
+            manhattan_radius: Parameter for the automatic computation of detectors.
+                Should be large enough so that flows cancelling each other to
+                form a detector are strictly contained in plaquettes that are at
+                most at a distance of ``manhattan_radius`` from the central
+                plaquette. Detector computation runtime grows with this parameter,
+                so you should try to keep it to its minimum. A value too low might
+                produce invalid detectors.
+            detector_database: existing database of detectors that is used to
+                avoid computing detectors if the database already contains them.
+                Default to `None` which result in not using any kind of database
+                and unconditionally performing the detector computation.
+            lookback: number of QEC rounds to consider to try to find detectors.
+                Including more rounds increases computation time. Cannot be over
+                ``2`` for the moment.
+        """
+        if lookback > 2:
+            raise TQECException(
+                "Cannot annotate detectors by considering more than 2 QEC rounds "
+                "at the moment."
+            )
+        self._k = k
+        self._manhattan_radius = manhattan_radius
+        self._database = detector_database or DetectorDatabase()
+        self._lookback = lookback
+        self._previous_templates: list[Template] = []
+        self._previous_plaquettes: list[Plaquettes] = []
+        self._measurement_records: list[MeasurementRecordsMap] = []
+
+    def append(
+        self,
+        template: Template,
+        plaquettes: Plaquettes,
+        measurement_records: MeasurementRecordsMap,
+    ) -> None:
+        """Add the provided parameters to the lookback window, potentially removing
+        older items that should not be considered anymore."""
+        _append_with_capped_size(self._previous_templates, template, self._lookback)
+        _append_with_capped_size(self._previous_plaquettes, plaquettes, self._lookback)
+        _append_with_capped_size(
+            self._measurement_records, measurement_records, self._lookback
+        )
+
+    @property
+    def full_lookback_measurement_record(self) -> MeasurementRecordsMap:
+        """Returns the measurement records of all the measurements in the stored
+        lookback window."""
+        ret = MeasurementRecordsMap()
+        for mrecords in self._measurement_records:
+            ret = ret.with_added_measurements(mrecords)
+        return ret
+
+    @override
+    def visit_node(self, node: LayerNode) -> None:
+        if not isinstance(node._layer, LayoutLayer):
+            return
path: src/tqec/compile/tree/annotators/detectors.py
user: login: nelimee
body: Yes, I am trying to address it right now, even though lower parts of the pipeline do not support more than 2 rounds. 
created_at: 2025-03-18T11:22:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,117 @@
+from typing import MutableSequence, TypeVar
+
+from typing_extensions import override
+
+from tqec.circuit.measurement_map import MeasurementRecordsMap
+from tqec.compile.blocks.layers.atomic.layout import LayoutLayer
+from tqec.compile.detectors.compute import compute_detectors_for_fixed_radius
+from tqec.compile.detectors.database import DetectorDatabase
+from tqec.compile.tree.annotations import DetectorAnnotation
+from tqec.compile.tree.node import LayerNode, NodeWalker
+from tqec.plaquette.plaquette import Plaquettes
+from tqec.templates.base import Template
+from tqec.utils.exceptions import TQECException
+
+T = TypeVar("T")
+
+
+def _append_with_capped_size(seq: MutableSequence[T], obj: T, lookback: int) -> None:
+    seq.append(obj)
+    if len(seq) > lookback:
+        seq.pop(0)
+
+
+class AnnotateDetectorsOnLayerNode(NodeWalker):
+    def __init__(
+        self,
+        k: int,
+        manhattan_radius: int = 2,
+        detector_database: DetectorDatabase | None = None,
+        lookback: int = 2,
+    ):
+        """Walker computing and annotating detectors on leaf nodes.
+
+        This class keeps track of the ``lookback`` previous leaf nodes seen and
+        uses them to automatically compute the detectors at all the leaf nodes
+        it encounters.
+
+        Args:
+            k: scaling factor.
+            manhattan_radius: Parameter for the automatic computation of detectors.
+                Should be large enough so that flows cancelling each other to
+                form a detector are strictly contained in plaquettes that are at
+                most at a distance of ``manhattan_radius`` from the central
+                plaquette. Detector computation runtime grows with this parameter,
+                so you should try to keep it to its minimum. A value too low might
+                produce invalid detectors.
+            detector_database: existing database of detectors that is used to
+                avoid computing detectors if the database already contains them.
+                Default to `None` which result in not using any kind of database
+                and unconditionally performing the detector computation.
+            lookback: number of QEC rounds to consider to try to find detectors.
+                Including more rounds increases computation time. Cannot be over
+                ``2`` for the moment.
+        """
+        if lookback > 2:
+            raise TQECException(
+                "Cannot annotate detectors by considering more than 2 QEC rounds "
+                "at the moment."
+            )
+        self._k = k
+        self._manhattan_radius = manhattan_radius
+        self._database = detector_database or DetectorDatabase()
+        self._lookback = lookback
+        self._previous_templates: list[Template] = []
+        self._previous_plaquettes: list[Plaquettes] = []
+        self._measurement_records: list[MeasurementRecordsMap] = []
+
+    def append(
+        self,
+        template: Template,
+        plaquettes: Plaquettes,
+        measurement_records: MeasurementRecordsMap,
+    ) -> None:
+        """Add the provided parameters to the lookback window, potentially removing
+        older items that should not be considered anymore."""
+        _append_with_capped_size(self._previous_templates, template, self._lookback)
+        _append_with_capped_size(self._previous_plaquettes, plaquettes, self._lookback)
+        _append_with_capped_size(
+            self._measurement_records, measurement_records, self._lookback
+        )
+
+    @property
+    def full_lookback_measurement_record(self) -> MeasurementRecordsMap:
+        """Returns the measurement records of all the measurements in the stored
+        lookback window."""
+        ret = MeasurementRecordsMap()
+        for mrecords in self._measurement_records:
+            ret = ret.with_added_measurements(mrecords)
+        return ret
+
+    @override
+    def visit_node(self, node: LayerNode) -> None:
+        if not isinstance(node._layer, LayoutLayer):
+            return
path: src/tqec/compile/tree/annotators/detectors.py
user: login: nelimee
body: Should now be addressed, I'm going to write tests for that as the code became a bit more complex.
created_at: 2025-03-18T12:12:28Z
author_association: CONTRIBUTOR

PR comments:
user: login: Zhaoyilunnn
created_at: 2025-03-15T04:44:07Z
body: >layer_tree = graph.to_layout_tree()
>layer_tree.annotate_circuits(2)
>layer_tree.annotate_qubit_map(2)
>layer_tree.annotate_detectors(2)
>circuit = layer_tree.generate_circuit(2)

With the newest code, these should be

```
layer_tree = graph.to_layer_tree()
circuit = layer_tree.generate_circuit(2)
```

The new `generate_circuit()` method looks very clean, great job.
I have a question. How to test that the generated circuit is correct?
In the old framework's unit tests, I see some assertions to check num detectors and shortest graphlike error. But why is these checking sufficient?

```python
    assert circuit.num_detectors == (d**2 - 1) * d
    assert len(circuit.shortest_graphlike_error()) == d

```



author_association: CONTRIBUTOR
user: login: nelimee
created_at: 2025-03-15T09:04:34Z
body: > > layer_tree = graph.to_layout_tree()
> > layer_tree.annotate_circuits(2)
> > layer_tree.annotate_qubit_map(2)
> > layer_tree.annotate_detectors(2)
> > circuit = layer_tree.generate_circuit(2)
> 
> With the newest code, these should be
> 
> ```
> layer_tree = graph.to_layer_tree()
> circuit = layer_tree.generate_circuit(2)
> ```

Yes, that's simpler to use but less granular for the user. 

> The new `generate_circuit()` method looks very clean, great job. I have a question. How to test that the generated circuit is correct? In the old framework's unit tests, I see some assertions to check num detectors and shortest graphlike error. But why is these checking sufficient?
> 
> ```python
>     assert circuit.num_detectors == (d**2 - 1) * d
>     assert len(circuit.shortest_graphlike_error()) == d
> ```

This is a good question. "Just" checking number of detectors and shortest graph-like error is not sufficient, but it is the best we found to have generic tests that do not directly embed the expected output circuit (tests that then would have to be updated regularly, even if we just change the ordering of gate targets). We could test that using flows, but that has not been done yet.
author_association: CONTRIBUTOR
user: login: nelimee
created_at: 2025-03-18T12:11:30Z
body: Checking that detectors within a `REPEAT` block are valid is a little bit tedious, so it is not done in this PR and I simply left a comment. To explain in a little bit more details the problem, let's say we have the following circuit:

```
OPS1
OPS2
OPS3
REPEAT 5 {
    OPS4
    OPS5
}
```

If, when performing automatic detector computation, we have a lookback window of `3` (i.e., detectors might span 3 QEC rounds) then:

- there is no issue with `OPS1`, `OPS2` and `OPS3`, the computed detectors will be correct,
- detectors computed for `OPS4` should be valid when considering **BOTH** sequences `[OPS2, OPS3, OPS4]` (the first repetition of the `REPEAT` block) **and** `[OPS4, OPS5, OPS4]` (the second, third and subsequent repetitions).
- detectors computed for `OPS5` should be valid when considering **BOTH** sequences `[OPS3, OPS4, OPS5]` (the first repetition of the `REPEAT` block) **and** `[OPS5, OPS4, OPS5]` (the second, third and subsequent repetitions).

Also note that if the lookback window was larger (e.g. 4), we would have 3 different cases to cover (e.g., for `OPS4`: `[OPS1, OPS2, OPS3, OPS4]`, `[OPS3, OPS4, OPS5, OPS4]` and `[OPS5, OPS4, OPS5, OPS4]`).

Currently, the PR only compute the detectors according to the first repetition of the REPEAT loop, and assumes that the provided computation is correct (i.e., that the 2 different cases for a lookback window of `3` listed above both lead to the exact same detectors). It would be nice to check rather than assuming, raising a descriptive error if the provided computation is incorrect, and even listing potential fixes (e.g., unrolling the first few repetitions of the loop).
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-18T12:42:26Z
body: ![Code Coverage](https://img.shields.io/badge/Code%20Coverage-90%25-success?style=flat)

Package | Line Rate | Complexity | Health
-------- | --------- | ---------- | ------
src.tqec | 100% | 0 | ✔
src.tqec.circuit | 97% | 0 | ✔
src.tqec.circuit.schedule | 99% | 0 | ✔
src.tqec.compile | 77% | 0 | ✔
src.tqec.compile.blocks | 87% | 0 | ✔
src.tqec.compile.blocks.layers | 94% | 0 | ✔
src.tqec.compile.blocks.layers.atomic | 88% | 0 | ✔
src.tqec.compile.blocks.layers.composed | 98% | 0 | ✔
src.tqec.compile.detectors | 89% | 0 | ✔
src.tqec.compile.observables | 94% | 0 | ✔
src.tqec.compile.specs | 87% | 0 | ✔
src.tqec.compile.specs.library | 100% | 0 | ✔
src.tqec.compile.specs.library.generators | 94% | 0 | ✔
src.tqec.compile.tree | 61% | 0 | ➖
src.tqec.compile.tree.annotators | 73% | 0 | ➖
src.tqec.computation | 96% | 0 | ✔
src.tqec.gallery | 100% | 0 | ✔
src.tqec.interop | 89% | 0 | ✔
src.tqec.interop.collada | 93% | 0 | ✔
src.tqec.interop.pyzx | 80% | 0 | ✔
src.tqec.interop.pyzx.synthesis | 91% | 0 | ✔
src.tqec.plaquette | 92% | 0 | ✔
src.tqec.plaquette.compilation | 100% | 0 | ✔
src.tqec.plaquette.compilation.passes | 92% | 0 | ✔
src.tqec.plaquette.compilation.passes.transformer | 98% | 0 | ✔
src.tqec.plaquette.library | 98% | 0 | ✔
src.tqec.plaquette.rpng | 73% | 0 | ➖
src.tqec.plaquette.rpng.translators | 97% | 0 | ✔
src.tqec.simulation | 65% | 0 | ➖
src.tqec.simulation.io_utils | 100% | 0 | ✔
src.tqec.simulation.plotting | 56% | 0 | ➖
src.tqec.templates | 95% | 0 | ✔
src.tqec.utils | 93% | 0 | ✔
**Summary** | **90%** (6086 / 6744) | **0** | ✔

<!-- Sticky Pull Request Commentcode-coverage -->
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-18T14:45:49Z
body: 🪓 PR closed, deleted preview at https://tqec.github.io/tqec/pull/508/
<!-- Sticky Pull Request Commentdocumentation-preview -->
author_association: NONE