Skip to content

circuits

circuits ¤

LayerAddressBook ¤

Bases: AddressBook[TorchLayer]

The address book data structure for the circuits. See TorchCircuit. The address book stores a list of AddressBookEntry, where each entry stores the information needed to gather the inputs to each (possibly folded) circuit layer.

Source code in cirkit/backend/torch/circuits.py
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
class LayerAddressBook(AddressBook[TorchLayer]):
    """The address book data structure for the circuits.
    See [TorchCircuit][cirkit.backend.torch.circuits.TorchCircuit].
    The address book stores a list of
    [AddressBookEntry][cirkit.backend.torch.graph.modules.AddressBookEntry],
    where each entry stores the information needed to gather the inputs to each (possibly folded)
    circuit layer.
    """

    def lookup(
        self, module_outputs: list[Tensor], *, in_graph: Tensor | None = None
    ) -> Iterator[tuple[TorchLayer | None, tuple]]:
        # Loop through the entries and yield inputs
        for entry in self:
            layer = entry.module
            in_layer_ids = entry.in_module_ids
            in_fold_idx = entry.in_fold_idx
            # Catch the case there are some inputs coming from other modules
            if in_layer_ids:
                in_fold_idx_h = in_fold_idx[0]
                in_layer_ids_h = in_layer_ids[0]
                if len(in_layer_ids_h) == 1:
                    x = module_outputs[in_layer_ids_h[0]]
                else:
                    x = torch.cat([module_outputs[mid] for mid in in_layer_ids_h], dim=0)
                x = x[in_fold_idx_h]
                yield layer, (x,)
                continue

            # Catch the case there are no inputs coming from other modules
            # That is, we are gathering the inputs of input layers
            assert isinstance(layer, TorchInputLayer)
            if layer.num_variables:
                if in_graph is None:
                    yield layer, ()
                    continue
                # in_graph: An input batch (assignments to variables) of shape (B, D)
                # scope_idx: The scope of the layers in each fold, a tensor of shape (F, D'), D' < D
                # x: (B, D) -> (B, F, D') -> (F, B, D')
                if len(in_graph.shape) != 2:
                    raise ValueError(
                        "The input to the circuit should have shape (B, D), "
                        "where B is the batch size and D is the number of variables "
                        "the circuit is defined on"
                    )
                x = in_graph[..., layer.scope_idx].permute(1, 0, 2)
                yield layer, (x,)
                continue

            # Pass the wanted batch dimension to constant layers
            yield layer, (1 if in_graph is None else in_graph.shape[0],)

    @classmethod
    def from_index_info(
        cls,
        fold_idx_info: FoldIndexInfo[TorchLayer],
        *,
        incomings_fn: Callable[[TorchLayer], Sequence[TorchLayer]],
    ) -> "LayerAddressBook":
        """Constructs the layers address book using fold index information.

        Args:
            fold_idx_info: The fold index information.
            incomings_fn: A function mapping each circuit layer to the sequence of its inputs.

        Returns:
            A layers address book.
        """
        # The address book entries being built
        entries: list[AddressBookEntry[TorchLayer]] = []

        # A useful dictionary mapping module ids to their number of folds
        num_folds: dict[int, int] = {}

        # Build the bookkeeping data structure by following the topological ordering
        for mid, m in enumerate(fold_idx_info.ordering):
            # Retrieve the index information of the input modules
            in_modules_fold_idx = fold_idx_info.in_fold_idx[mid]

            # Catch the case of a folded module having the output of another module as input
            if incomings_fn(m):
                entry = build_address_book_stacked_entry(
                    m, in_modules_fold_idx, num_folds=num_folds
                )
            else:
                # Catch the case of a folded module having the input of the network as input
                # That is, this is the case of an input layer
                entry = AddressBookEntry(m, [], [])

            num_folds[mid] = m.num_folds
            entries.append(entry)

        # Append the last bookkeeping entry with the information to compute the output tensor
        entry = build_address_book_stacked_entry(
            None, [fold_idx_info.out_fold_idx], num_folds=num_folds, output=True
        )
        entries.append(entry)

        return LayerAddressBook(entries)

from_index_info(fold_idx_info, *, incomings_fn) classmethod ¤

Constructs the layers address book using fold index information.

Parameters:

Name Type Description Default
fold_idx_info FoldIndexInfo[TorchLayer]

The fold index information.

required
incomings_fn Callable[[TorchLayer], Sequence[TorchLayer]]

A function mapping each circuit layer to the sequence of its inputs.

required

Returns:

Type Description
LayerAddressBook

A layers address book.

Source code in cirkit/backend/torch/circuits.py
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
@classmethod
def from_index_info(
    cls,
    fold_idx_info: FoldIndexInfo[TorchLayer],
    *,
    incomings_fn: Callable[[TorchLayer], Sequence[TorchLayer]],
) -> "LayerAddressBook":
    """Constructs the layers address book using fold index information.

    Args:
        fold_idx_info: The fold index information.
        incomings_fn: A function mapping each circuit layer to the sequence of its inputs.

    Returns:
        A layers address book.
    """
    # The address book entries being built
    entries: list[AddressBookEntry[TorchLayer]] = []

    # A useful dictionary mapping module ids to their number of folds
    num_folds: dict[int, int] = {}

    # Build the bookkeeping data structure by following the topological ordering
    for mid, m in enumerate(fold_idx_info.ordering):
        # Retrieve the index information of the input modules
        in_modules_fold_idx = fold_idx_info.in_fold_idx[mid]

        # Catch the case of a folded module having the output of another module as input
        if incomings_fn(m):
            entry = build_address_book_stacked_entry(
                m, in_modules_fold_idx, num_folds=num_folds
            )
        else:
            # Catch the case of a folded module having the input of the network as input
            # That is, this is the case of an input layer
            entry = AddressBookEntry(m, [], [])

        num_folds[mid] = m.num_folds
        entries.append(entry)

    # Append the last bookkeeping entry with the information to compute the output tensor
    entry = build_address_book_stacked_entry(
        None, [fold_idx_info.out_fold_idx], num_folds=num_folds, output=True
    )
    entries.append(entry)

    return LayerAddressBook(entries)

lookup(module_outputs, *, in_graph=None) ¤

Source code in cirkit/backend/torch/circuits.py
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
def lookup(
    self, module_outputs: list[Tensor], *, in_graph: Tensor | None = None
) -> Iterator[tuple[TorchLayer | None, tuple]]:
    # Loop through the entries and yield inputs
    for entry in self:
        layer = entry.module
        in_layer_ids = entry.in_module_ids
        in_fold_idx = entry.in_fold_idx
        # Catch the case there are some inputs coming from other modules
        if in_layer_ids:
            in_fold_idx_h = in_fold_idx[0]
            in_layer_ids_h = in_layer_ids[0]
            if len(in_layer_ids_h) == 1:
                x = module_outputs[in_layer_ids_h[0]]
            else:
                x = torch.cat([module_outputs[mid] for mid in in_layer_ids_h], dim=0)
            x = x[in_fold_idx_h]
            yield layer, (x,)
            continue

        # Catch the case there are no inputs coming from other modules
        # That is, we are gathering the inputs of input layers
        assert isinstance(layer, TorchInputLayer)
        if layer.num_variables:
            if in_graph is None:
                yield layer, ()
                continue
            # in_graph: An input batch (assignments to variables) of shape (B, D)
            # scope_idx: The scope of the layers in each fold, a tensor of shape (F, D'), D' < D
            # x: (B, D) -> (B, F, D') -> (F, B, D')
            if len(in_graph.shape) != 2:
                raise ValueError(
                    "The input to the circuit should have shape (B, D), "
                    "where B is the batch size and D is the number of variables "
                    "the circuit is defined on"
                )
            x = in_graph[..., layer.scope_idx].permute(1, 0, 2)
            yield layer, (x,)
            continue

        # Pass the wanted batch dimension to constant layers
        yield layer, (1 if in_graph is None else in_graph.shape[0],)

TorchCircuit ¤

Bases: TorchDiAcyclicGraph[TorchLayer]

The torch circuit implementation. It is a (possibly folded) computational graph of torch layers implementations.

Source code in cirkit/backend/torch/circuits.py
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
class TorchCircuit(TorchDiAcyclicGraph[TorchLayer]):
    """The torch circuit implementation. It is a (possibly folded)
    computational graph of torch layers implementations."""

    def __init__(
        self,
        scope: Scope,
        layers: Sequence[TorchLayer],
        in_layers: Mapping[TorchLayer, Sequence[TorchLayer]],
        outputs: Sequence[TorchLayer],
        *,
        properties: StructuralProperties,
        fold_idx_info: FoldIndexInfo[TorchLayer] | None = None,
    ) -> None:
        """Initializes a torch circuit.

        Args:
            scope: The variables scope.
            layers: The sequence of layers.
            in_layers: A dictionary mapping layers to their inputs, if any.
            outputs: A list of output layers.
            properties: The structural properties of the circuit.
            fold_idx_info: The folding index information.
                It can be None if the circuit is not folded.
        """
        super().__init__(
            layers,
            in_layers,
            outputs,
            fold_idx_info=fold_idx_info,
        )
        self._scope = scope
        self._properties = properties

    @property
    def scope(self) -> Scope:
        """Retrieve the variables scope of the circuit.

        Returns:
            The scope.
        """
        return self._scope

    @property
    def num_variables(self) -> int:
        """Retrieve the number of variables the circuit is defined on.

        Returns:
            The number of variables.
        """
        return len(self.scope)

    @property
    def properties(self) -> StructuralProperties:
        """Retrieve the structural properties of the circuit.

        Returns:
            The structural properties.
        """
        return self._properties

    @property
    def layers(self) -> Sequence[TorchLayer]:
        """Retrieve the layers.

        Returns:
            The layers.
        """
        return self.nodes

    def layer_inputs(self, l: TorchLayer) -> Sequence[TorchLayer]:
        """Given a layer, retrieve the layers that are input to it.

        Args:
            l: The layer.

        Returns:
            The inputs to the given layer.
        """
        return self.node_inputs(l)

    def layer_outputs(self, l: TorchLayer) -> Sequence[TorchLayer]:
        """Given a layer, retrieve the layers that receive input from it.

        Args:
            l: The layer.

        Returns:
            The outputs from the given layer.
        """
        return self.node_outputs(l)

    @property
    def layers_inputs(self) -> Mapping[TorchLayer, Sequence[TorchLayer]]:
        """Retrieve the map from layers to their inputs.

        Returns:
            The layers inputs map.
        """
        return self.nodes_inputs

    @property
    def layers_outputs(self) -> Mapping[TorchLayer, Sequence[TorchLayer]]:
        """Retrieve the map from layers to their outputs.

        Returns:
            The layers outputs map.
        """
        return self.nodes_outputs

    def reset_parameters(self) -> None:
        """Reset the parameters of the circuit in-place."""
        # For each layer, initialize its parameters, if any
        for l in self.layers:
            for p in l.params.values():
                p.reset_parameters()

    def __call__(self, x: Tensor | None = None) -> Tensor:
        return super().__call__(x)

    def forward(self, x: Tensor | None = None) -> Tensor:
        """Evaluate the circuit layers in forward mode, i.e., by evaluating each layer by
        following the topological ordering.

        Args:
            x: The tensor input of the circuit, with shape $(B, D)$, where B is the batch size,
                and $D$ is the number of variables. It can be None if the circuit has empty scope,
                i.e., it computes a constant tensor. Defaults to None.

        Returns:
            Tensor: The tensor output of the circuit, with shape $(B, O, K)$,
                where $O$ is the number of vectorized outputs (i.e., the number of output layers),
                and $K$ is the number of scalars in each output (e.g., the number of classes).

        Raises:
            ValueError: If the scope is not empty and the tensor input to the circuit is None.
        """
        if self._scope and x is None:
            raise ValueError(f"Expected some input 'x', as the circuit has scope '{self._scope}'")
        return self._evaluate_layers(x)

    def _build_unfold_index_info(self) -> FoldIndexInfo[TorchLayer]:
        return build_unfold_index_info(
            self.topological_ordering(), outputs=self.outputs, incomings_fn=self.node_inputs
        )

    def _build_address_book(self, fold_idx_info: FoldIndexInfo[TorchLayer]) -> LayerAddressBook:
        return LayerAddressBook.from_index_info(fold_idx_info, incomings_fn=self.layer_inputs)

    def _evaluate_layers(self, x: Tensor | None) -> Tensor:
        # Evaluate layers on the given input
        y = self.evaluate(x)  # (O, B, K)
        y = y.transpose(0, 1)  # (B, O, K)
        # If the circuit has empty scope, we squeeze the batch dimension, as it is 1
        if not self._scope:
            y = y.squeeze(dim=0)  # (O, K)
        return y

layers property ¤

Retrieve the layers.

Returns:

Type Description
Sequence[TorchLayer]

The layers.

layers_inputs property ¤

Retrieve the map from layers to their inputs.

Returns:

Type Description
Mapping[TorchLayer, Sequence[TorchLayer]]

The layers inputs map.

layers_outputs property ¤

Retrieve the map from layers to their outputs.

Returns:

Type Description
Mapping[TorchLayer, Sequence[TorchLayer]]

The layers outputs map.

num_variables property ¤

Retrieve the number of variables the circuit is defined on.

Returns:

Type Description
int

The number of variables.

properties property ¤

Retrieve the structural properties of the circuit.

Returns:

Type Description
StructuralProperties

The structural properties.

scope property ¤

Retrieve the variables scope of the circuit.

Returns:

Type Description
Scope

The scope.

__call__(x=None) ¤

Source code in cirkit/backend/torch/circuits.py
239
240
def __call__(self, x: Tensor | None = None) -> Tensor:
    return super().__call__(x)

__init__(scope, layers, in_layers, outputs, *, properties, fold_idx_info=None) ¤

Initializes a torch circuit.

Parameters:

Name Type Description Default
scope Scope

The variables scope.

required
layers Sequence[TorchLayer]

The sequence of layers.

required
in_layers Mapping[TorchLayer, Sequence[TorchLayer]]

A dictionary mapping layers to their inputs, if any.

required
outputs Sequence[TorchLayer]

A list of output layers.

required
properties StructuralProperties

The structural properties of the circuit.

required
fold_idx_info FoldIndexInfo[TorchLayer] | None

The folding index information. It can be None if the circuit is not folded.

None
Source code in cirkit/backend/torch/circuits.py
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
def __init__(
    self,
    scope: Scope,
    layers: Sequence[TorchLayer],
    in_layers: Mapping[TorchLayer, Sequence[TorchLayer]],
    outputs: Sequence[TorchLayer],
    *,
    properties: StructuralProperties,
    fold_idx_info: FoldIndexInfo[TorchLayer] | None = None,
) -> None:
    """Initializes a torch circuit.

    Args:
        scope: The variables scope.
        layers: The sequence of layers.
        in_layers: A dictionary mapping layers to their inputs, if any.
        outputs: A list of output layers.
        properties: The structural properties of the circuit.
        fold_idx_info: The folding index information.
            It can be None if the circuit is not folded.
    """
    super().__init__(
        layers,
        in_layers,
        outputs,
        fold_idx_info=fold_idx_info,
    )
    self._scope = scope
    self._properties = properties

forward(x=None) ¤

Evaluate the circuit layers in forward mode, i.e., by evaluating each layer by following the topological ordering.

Parameters:

Name Type Description Default
x Tensor | None

The tensor input of the circuit, with shape \((B, D)\), where B is the batch size, and \(D\) is the number of variables. It can be None if the circuit has empty scope, i.e., it computes a constant tensor. Defaults to None.

None

Returns:

Name Type Description
Tensor Tensor

The tensor output of the circuit, with shape \((B, O, K)\), where \(O\) is the number of vectorized outputs (i.e., the number of output layers), and \(K\) is the number of scalars in each output (e.g., the number of classes).

Raises:

Type Description
ValueError

If the scope is not empty and the tensor input to the circuit is None.

Source code in cirkit/backend/torch/circuits.py
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
def forward(self, x: Tensor | None = None) -> Tensor:
    """Evaluate the circuit layers in forward mode, i.e., by evaluating each layer by
    following the topological ordering.

    Args:
        x: The tensor input of the circuit, with shape $(B, D)$, where B is the batch size,
            and $D$ is the number of variables. It can be None if the circuit has empty scope,
            i.e., it computes a constant tensor. Defaults to None.

    Returns:
        Tensor: The tensor output of the circuit, with shape $(B, O, K)$,
            where $O$ is the number of vectorized outputs (i.e., the number of output layers),
            and $K$ is the number of scalars in each output (e.g., the number of classes).

    Raises:
        ValueError: If the scope is not empty and the tensor input to the circuit is None.
    """
    if self._scope and x is None:
        raise ValueError(f"Expected some input 'x', as the circuit has scope '{self._scope}'")
    return self._evaluate_layers(x)

layer_inputs(l) ¤

Given a layer, retrieve the layers that are input to it.

Parameters:

Name Type Description Default
l TorchLayer

The layer.

required

Returns:

Type Description
Sequence[TorchLayer]

The inputs to the given layer.

Source code in cirkit/backend/torch/circuits.py
192
193
194
195
196
197
198
199
200
201
def layer_inputs(self, l: TorchLayer) -> Sequence[TorchLayer]:
    """Given a layer, retrieve the layers that are input to it.

    Args:
        l: The layer.

    Returns:
        The inputs to the given layer.
    """
    return self.node_inputs(l)

layer_outputs(l) ¤

Given a layer, retrieve the layers that receive input from it.

Parameters:

Name Type Description Default
l TorchLayer

The layer.

required

Returns:

Type Description
Sequence[TorchLayer]

The outputs from the given layer.

Source code in cirkit/backend/torch/circuits.py
203
204
205
206
207
208
209
210
211
212
def layer_outputs(self, l: TorchLayer) -> Sequence[TorchLayer]:
    """Given a layer, retrieve the layers that receive input from it.

    Args:
        l: The layer.

    Returns:
        The outputs from the given layer.
    """
    return self.node_outputs(l)

reset_parameters() ¤

Reset the parameters of the circuit in-place.

Source code in cirkit/backend/torch/circuits.py
232
233
234
235
236
237
def reset_parameters(self) -> None:
    """Reset the parameters of the circuit in-place."""
    # For each layer, initialize its parameters, if any
    for l in self.layers:
        for p in l.params.values():
            p.reset_parameters()