Skip to content

circuit

circuit ¤

Circuit ¤

Bases: DiAcyclicGraph[Layer]

The symbolic circuit representation.

Source code in cirkit/symbolic/circuit.py
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
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
class Circuit(DiAcyclicGraph[Layer]):
    """The symbolic circuit representation."""

    def __init__(
        self,
        num_channels: int,
        layers: Sequence[Layer],
        in_layers: Mapping[Layer, Sequence[Layer]],
        outputs: Sequence[Layer],
        *,
        operation: CircuitOperation | None = None,
    ) -> None:
        """Initializes a symbolic circuit.

        Args:
            num_channels: The number of channels for each variable.
            layers: The list of symbolic layers.
            in_layers: A dictionary containing the list of inputs to each layer.
            outputs: The output layers of the circuit.
            operation: The optional operation the circuit has been obtained through.
        """
        super().__init__(layers, in_layers, outputs)
        self.num_channels = num_channels
        self.operation = operation

        # Build scopes bottom-up, and check the consistency of the layers, w.r.t.
        # the arity and the number of input and output units
        self._scopes: dict[Layer, Scope] = {}
        for sl in self.topological_ordering():
            sl_ins = self.layer_inputs(sl)
            if isinstance(sl, InputLayer):
                self._scopes[sl] = sl.scope
                if len(sl_ins):
                    raise ValueError(
                        f"{sl}: found an input layer with {len(sl_ins)} layer inputs, "
                        "but expected none"
                    )
                continue
            self._scopes[sl] = Scope.union(*tuple(self._scopes[sli] for sli in sl_ins))
            if sl.arity != len(sl_ins):
                raise ValueError(
                    f"{sl}: expected arity {sl.arity}, " f"but found {len(sl_ins)} input layers"
                )
            sl_ins_units = [sli.num_output_units for sli in sl_ins]
            if any(sl.num_input_units != num_units for num_units in sl_ins_units):
                raise ValueError(
                    f"{sl}: expected number of input units {sl.num_input_units}, "
                    f"but found input layers {sl_ins}"
                )
        self.scope = Scope.union(*tuple(self._scopes[sl] for sl in self.outputs))

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

        Returns:
            int:
        """
        return len(self.scope) * self.num_channels

    def layer_scope(self, sl: Layer) -> Scope:
        """Retrieves the scope of a layer.

        Args:
            sl: The layer.

        Returns:
            The scope of the given layer.
        """
        return self._scopes[sl]

    def layer_inputs(self, sl: Layer) -> Sequence[Layer]:
        """Retrieves the inputs to a layer.

        Args:
            sl: The layer.

        Returns:
            Sequence[Layer]: The list of inputs.
        """
        return self.node_inputs(sl)

    def layer_outputs(self, sl: Layer) -> Sequence[Layer]:
        """Retrieves the outputs of a layer.

        Args:
            sl: The layer.

        Returns:
            Sequence[Layer]: The list of outputs.
        """
        return self.node_outputs(sl)

    @property
    def layers_inputs(self) -> Mapping[Layer, Sequence[Layer]]:
        """Retrieves the dictionary containing the list of inputs to each layer.

        Returns:
            Dict[Layer, Sequence[Layer]]:
        """
        return self.nodes_inputs

    @property
    def layers_outputs(self) -> Mapping[Layer, Sequence[Layer]]:
        """Retrieves the dictionary containing the list of outputs of each layer.

        Returns:
            Dict[Layer, Sequence[Layer]]:
        """
        return self.nodes_outputs

    @property
    def layers(self) -> Sequence[Layer]:
        """Retrieves a sequence of layers.

        Returns:
            Sequence[layer]:
        """
        return self.nodes

    @property
    def inner_layers(self) -> Iterator[SumLayer | ProductLayer]:
        """Retrieves an iterator over inner layers (i.e., non-input layers).

        Returns:
            Iterator[Union[SumLayer, ProductLayer]]:
        """
        return (sl for sl in self.layers if isinstance(sl, (SumLayer, ProductLayer)))

    @property
    def sum_layers(self) -> Iterator[SumLayer]:
        """Retrieves an iterator over sum layers.

        Returns:
            Iterator[SumLayer]:
        """
        return (sl for sl in self.layers if isinstance(sl, SumLayer))

    @property
    def product_layers(self) -> Iterator[ProductLayer]:
        """Retrieves an iterator over product layers.

        Returns:
            Iterator[ProductLayer]:
        """
        return (sl for sl in self.layers if isinstance(sl, ProductLayer))

    def subgraph(self, *outputs: Layer) -> "Circuit":
        """Retrieve the sub-circuit having the given layers as outputs.

        Args:
            *outputs: The output layers.

        Returns:
            The sub-circuit having the given layers as outputs.
        """
        layers, in_layers = subgraph(outputs, self.layer_inputs)
        return Circuit(self.num_channels, layers, in_layers, outputs=outputs)

    ##################################### Structural properties ####################################

    @cached_property
    def is_smooth(self) -> bool:
        """Check if the circuit is smooth.

        Returns:
            bool: True if the circuit is smooth and False otherwise.
        """
        return all(
            self.layer_scope(sum_sl) == self.layer_scope(in_sl)
            for sum_sl in self.sum_layers
            for in_sl in self.layer_inputs(sum_sl)
        )

    @cached_property
    def is_decomposable(self) -> bool:
        """Check if the circuit is decomposable.

        Returns:
            bool: True if the circuit is decomposable and False otherwise.
        """
        return not any(
            self.layer_scope(in_sl1) & self.layer_scope(in_sl2)
            for prod_sl in self.product_layers
            for in_sl1, in_sl2 in itertools.combinations(self.layer_inputs(prod_sl), 2)
        )

    @cached_property
    def is_structured_decomposable(self) -> bool:
        """Check if the circuit is structured-decomposable.

        Returns:
            bool: True if the circuit is structured-decomposable and False otherwise.
        """
        if not self.is_smooth:
            return False
        if not self.is_decomposable:
            return False
        scope_factorizations = _scope_factorizations(self)
        return all(len(fs) == 1 for _, fs in scope_factorizations.items())

    @cached_property
    def is_omni_compatible(self) -> bool:
        """Check if the circuit is omni-compatible.

        Returns:
            bool: True if the circuit is omni-compatible and False otherwise.
        """
        if not self.is_smooth:
            return False
        if not self.is_decomposable:
            return False
        scope_factorizations = _scope_factorizations(self)
        vs = Scope(range(self.num_variables))
        return _are_compatible(scope_factorizations, {vs: {tuple(Scope([vid]) for vid in vs)}})

    @cached_property
    def properties(self) -> StructuralProperties:
        """Retrieves all the structural properties of the circuit: smoothness,
        decomposability, structured-decomposability and omni-compatibility.

        Returns:
            The structural properties.
        """
        return StructuralProperties(
            self.is_smooth,
            self.is_decomposable,
            self.is_structured_decomposable,
            self.is_omni_compatible,
        )

    @classmethod
    def from_operation(
        cls,
        num_channels: int,
        blocks: list[CircuitBlock],
        in_blocks: dict[CircuitBlock, Sequence[CircuitBlock]],
        output_blocks: list[CircuitBlock],
        *,
        operation: CircuitOperation,
    ) -> "Circuit":
        """Constructs a circuit that resulted from an operation over other circuits.

        Args:
            num_channels: The number of channels per variable.
            blocks: The list of circuit blocks.
            in_blocks: A dictionary containing the list of block inputs to each circuit block.
            output_blocks: The outputs blocks of the circuit.
            operation: A circuit operation containing the information of the operation.

        Returns:
            Circuit: A symbolic circuit.Ki

        Raises:
            ValueError: If there is a circuit block having more than one layer with no inputs that
                are not input layers (i.e., they are either sum of product layers).
        """
        # Unwrap blocks into layers (as well as their connections)
        layers = [l for b in blocks for l in b.layers]
        in_layers = defaultdict(list)
        outputs = [b.output for b in output_blocks]

        # Retrieve connections between layers from connections between circuit blocks
        for b in blocks:
            b_layer_inputs = list(b.inputs)
            block_ins = in_blocks.get(b, [])
            if len(b_layer_inputs) == 1:
                (b_input,) = b_layer_inputs
                in_layers[b_input].extend(bi.output for bi in block_ins)
            elif len(block_ins) > 0:
                raise ValueError(
                    "A circuit block having multiple inputs cannot be a non-input block"
                )
            for sl in b.layers:
                in_layers[sl].extend(b.layer_inputs(sl))
        # Build the circuit and set the operation
        return cls(num_channels, layers, in_layers, outputs, operation=operation)

_scopes = {} instance-attribute ¤

inner_layers property ¤

Retrieves an iterator over inner layers (i.e., non-input layers).

Returns:

Type Description
Iterator[SumLayer | ProductLayer]

Iterator[Union[SumLayer, ProductLayer]]:

is_decomposable cached property ¤

Check if the circuit is decomposable.

Returns:

Name Type Description
bool bool

True if the circuit is decomposable and False otherwise.

is_omni_compatible cached property ¤

Check if the circuit is omni-compatible.

Returns:

Name Type Description
bool bool

True if the circuit is omni-compatible and False otherwise.

is_smooth cached property ¤

Check if the circuit is smooth.

Returns:

Name Type Description
bool bool

True if the circuit is smooth and False otherwise.

is_structured_decomposable cached property ¤

Check if the circuit is structured-decomposable.

Returns:

Name Type Description
bool bool

True if the circuit is structured-decomposable and False otherwise.

layers property ¤

Retrieves a sequence of layers.

Returns:

Type Description
Sequence[Layer]

Sequence[layer]:

layers_inputs property ¤

Retrieves the dictionary containing the list of inputs to each layer.

Returns:

Type Description
Mapping[Layer, Sequence[Layer]]

Dict[Layer, Sequence[Layer]]:

layers_outputs property ¤

Retrieves the dictionary containing the list of outputs of each layer.

Returns:

Type Description
Mapping[Layer, Sequence[Layer]]

Dict[Layer, Sequence[Layer]]:

num_channels = num_channels instance-attribute ¤

num_variables property ¤

Retrieves the number of variables the circuit is defined on.

Returns:

Name Type Description
int int

operation = operation instance-attribute ¤

product_layers property ¤

Retrieves an iterator over product layers.

Returns:

Type Description
Iterator[ProductLayer]

Iterator[ProductLayer]:

properties cached property ¤

Retrieves all the structural properties of the circuit: smoothness, decomposability, structured-decomposability and omni-compatibility.

Returns:

Type Description
StructuralProperties

The structural properties.

scope = Scope.union(*tuple(self._scopes[sl] for sl in self.outputs)) instance-attribute ¤

sum_layers property ¤

Retrieves an iterator over sum layers.

Returns:

Type Description
Iterator[SumLayer]

Iterator[SumLayer]:

__init__(num_channels, layers, in_layers, outputs, *, operation=None) ¤

Initializes a symbolic circuit.

Parameters:

Name Type Description Default
num_channels int

The number of channels for each variable.

required
layers Sequence[Layer]

The list of symbolic layers.

required
in_layers Mapping[Layer, Sequence[Layer]]

A dictionary containing the list of inputs to each layer.

required
outputs Sequence[Layer]

The output layers of the circuit.

required
operation CircuitOperation | None

The optional operation the circuit has been obtained through.

None
Source code in cirkit/symbolic/circuit.py
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
def __init__(
    self,
    num_channels: int,
    layers: Sequence[Layer],
    in_layers: Mapping[Layer, Sequence[Layer]],
    outputs: Sequence[Layer],
    *,
    operation: CircuitOperation | None = None,
) -> None:
    """Initializes a symbolic circuit.

    Args:
        num_channels: The number of channels for each variable.
        layers: The list of symbolic layers.
        in_layers: A dictionary containing the list of inputs to each layer.
        outputs: The output layers of the circuit.
        operation: The optional operation the circuit has been obtained through.
    """
    super().__init__(layers, in_layers, outputs)
    self.num_channels = num_channels
    self.operation = operation

    # Build scopes bottom-up, and check the consistency of the layers, w.r.t.
    # the arity and the number of input and output units
    self._scopes: dict[Layer, Scope] = {}
    for sl in self.topological_ordering():
        sl_ins = self.layer_inputs(sl)
        if isinstance(sl, InputLayer):
            self._scopes[sl] = sl.scope
            if len(sl_ins):
                raise ValueError(
                    f"{sl}: found an input layer with {len(sl_ins)} layer inputs, "
                    "but expected none"
                )
            continue
        self._scopes[sl] = Scope.union(*tuple(self._scopes[sli] for sli in sl_ins))
        if sl.arity != len(sl_ins):
            raise ValueError(
                f"{sl}: expected arity {sl.arity}, " f"but found {len(sl_ins)} input layers"
            )
        sl_ins_units = [sli.num_output_units for sli in sl_ins]
        if any(sl.num_input_units != num_units for num_units in sl_ins_units):
            raise ValueError(
                f"{sl}: expected number of input units {sl.num_input_units}, "
                f"but found input layers {sl_ins}"
            )
    self.scope = Scope.union(*tuple(self._scopes[sl] for sl in self.outputs))

from_operation(num_channels, blocks, in_blocks, output_blocks, *, operation) classmethod ¤

Constructs a circuit that resulted from an operation over other circuits.

Parameters:

Name Type Description Default
num_channels int

The number of channels per variable.

required
blocks list[CircuitBlock]

The list of circuit blocks.

required
in_blocks dict[CircuitBlock, Sequence[CircuitBlock]]

A dictionary containing the list of block inputs to each circuit block.

required
output_blocks list[CircuitBlock]

The outputs blocks of the circuit.

required
operation CircuitOperation

A circuit operation containing the information of the operation.

required

Returns:

Name Type Description
Circuit Circuit

A symbolic circuit.Ki

Raises:

Type Description
ValueError

If there is a circuit block having more than one layer with no inputs that are not input layers (i.e., they are either sum of product layers).

Source code in cirkit/symbolic/circuit.py
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
@classmethod
def from_operation(
    cls,
    num_channels: int,
    blocks: list[CircuitBlock],
    in_blocks: dict[CircuitBlock, Sequence[CircuitBlock]],
    output_blocks: list[CircuitBlock],
    *,
    operation: CircuitOperation,
) -> "Circuit":
    """Constructs a circuit that resulted from an operation over other circuits.

    Args:
        num_channels: The number of channels per variable.
        blocks: The list of circuit blocks.
        in_blocks: A dictionary containing the list of block inputs to each circuit block.
        output_blocks: The outputs blocks of the circuit.
        operation: A circuit operation containing the information of the operation.

    Returns:
        Circuit: A symbolic circuit.Ki

    Raises:
        ValueError: If there is a circuit block having more than one layer with no inputs that
            are not input layers (i.e., they are either sum of product layers).
    """
    # Unwrap blocks into layers (as well as their connections)
    layers = [l for b in blocks for l in b.layers]
    in_layers = defaultdict(list)
    outputs = [b.output for b in output_blocks]

    # Retrieve connections between layers from connections between circuit blocks
    for b in blocks:
        b_layer_inputs = list(b.inputs)
        block_ins = in_blocks.get(b, [])
        if len(b_layer_inputs) == 1:
            (b_input,) = b_layer_inputs
            in_layers[b_input].extend(bi.output for bi in block_ins)
        elif len(block_ins) > 0:
            raise ValueError(
                "A circuit block having multiple inputs cannot be a non-input block"
            )
        for sl in b.layers:
            in_layers[sl].extend(b.layer_inputs(sl))
    # Build the circuit and set the operation
    return cls(num_channels, layers, in_layers, outputs, operation=operation)

layer_inputs(sl) ¤

Retrieves the inputs to a layer.

Parameters:

Name Type Description Default
sl Layer

The layer.

required

Returns:

Type Description
Sequence[Layer]

Sequence[Layer]: The list of inputs.

Source code in cirkit/symbolic/circuit.py
295
296
297
298
299
300
301
302
303
304
def layer_inputs(self, sl: Layer) -> Sequence[Layer]:
    """Retrieves the inputs to a layer.

    Args:
        sl: The layer.

    Returns:
        Sequence[Layer]: The list of inputs.
    """
    return self.node_inputs(sl)

layer_outputs(sl) ¤

Retrieves the outputs of a layer.

Parameters:

Name Type Description Default
sl Layer

The layer.

required

Returns:

Type Description
Sequence[Layer]

Sequence[Layer]: The list of outputs.

Source code in cirkit/symbolic/circuit.py
306
307
308
309
310
311
312
313
314
315
def layer_outputs(self, sl: Layer) -> Sequence[Layer]:
    """Retrieves the outputs of a layer.

    Args:
        sl: The layer.

    Returns:
        Sequence[Layer]: The list of outputs.
    """
    return self.node_outputs(sl)

layer_scope(sl) ¤

Retrieves the scope of a layer.

Parameters:

Name Type Description Default
sl Layer

The layer.

required

Returns:

Type Description
Scope

The scope of the given layer.

Source code in cirkit/symbolic/circuit.py
284
285
286
287
288
289
290
291
292
293
def layer_scope(self, sl: Layer) -> Scope:
    """Retrieves the scope of a layer.

    Args:
        sl: The layer.

    Returns:
        The scope of the given layer.
    """
    return self._scopes[sl]

subgraph(*outputs) ¤

Retrieve the sub-circuit having the given layers as outputs.

Parameters:

Name Type Description Default
*outputs Layer

The output layers.

()

Returns:

Type Description
Circuit

The sub-circuit having the given layers as outputs.

Source code in cirkit/symbolic/circuit.py
371
372
373
374
375
376
377
378
379
380
381
def subgraph(self, *outputs: Layer) -> "Circuit":
    """Retrieve the sub-circuit having the given layers as outputs.

    Args:
        *outputs: The output layers.

    Returns:
        The sub-circuit having the given layers as outputs.
    """
    layers, in_layers = subgraph(outputs, self.layer_inputs)
    return Circuit(self.num_channels, layers, in_layers, outputs=outputs)

CircuitBlock ¤

Bases: RootedDiAcyclicGraph[Layer]

The circuit block data structure. A circuit block is a fragment of a symbolic circuit, consisting of a single root (or output) layer. A circuit block can be of only two types: (1) a circuit block whose layers that do not have any inputs must all be input layers, and (2) a circuit block where there is one and only one layer that does not have any other input, which can be either a sum or product layer.

Source code in cirkit/symbolic/circuit.py
 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
120
121
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
class CircuitBlock(RootedDiAcyclicGraph[Layer]):
    """The circuit block data structure. A circuit block is a fragment of a symbolic circuit,
    consisting of a single root (or output) layer. A circuit block can be of only two types:
    (1) a circuit block whose layers that do not have any inputs must all be input layers, and
    (2) a circuit block where there is one and only one layer that does not have any other input,
    which can be either a sum or product layer.
    """

    def __init__(
        self, layers: Sequence[Layer], in_layers: Mapping[Layer, list[Layer]], output: Layer
    ):
        """Initializes a circuit block.

        Args:
            layers: The sequence of layers in the block.
            in_layers: A dictionary containing the list of inputs to each layer.
            output: The root (or output) of the circuit block.
        """
        super().__init__(layers, in_layers, [output])

    def layer_inputs(self, sl: Layer) -> Sequence[Layer]:
        """Retrieves the inputs to a layer.

        Args:
            sl: The layer.

        Returns:
            Sequence[Layer]: The sequence of inputs.
        """
        return self.node_inputs(sl)

    def layer_outputs(self, sl: Layer) -> Sequence[Layer]:
        """Retrieves the outputs of a layer.

        Args:
            sl: The layer.

        Returns:
            Sequence[Layer]: The sequence of outputs.
        """
        return self.node_outputs(sl)

    @property
    def layers_inputs(self) -> Mapping[Layer, Sequence[Layer]]:
        """Retrieves the dictionary containing the sequence of inputs to each layer.

        Returns:
            Mapping[Layer, Sequence[Layer]]:
        """
        return self.nodes_inputs

    @property
    def layers_outputs(self) -> Mapping[Layer, Sequence[Layer]]:
        """Retrieves the dictionary containing the sequence of outputs of each layer.

        Returns:
            Mapping[Layer, Sequence[Layer]]:
        """
        return self.nodes_outputs

    @property
    def layers(self) -> Sequence[Layer]:
        """Retrieves a sequence of layers.

        Returns:
            Sequence[layer]:
        """
        return self.nodes

    @property
    def inner_layers(self) -> Iterator[SumLayer | ProductLayer]:
        """Retrieves an iterator over inner layers (i.e., layers that have at least one input).

        Returns:
            Iterator[Union[SumLayer, ProductLayer]]:
        """
        return (sl for sl in self.layers if isinstance(sl, (SumLayer, ProductLayer)))

    @property
    def sum_layers(self) -> Iterator[SumLayer]:
        """Retrieves an iterator over sum layers.

        Returns:
            Iterator[SumLayer]:
        """
        return (sl for sl in self.layers if isinstance(sl, SumLayer))

    @property
    def product_layers(self) -> Iterator[ProductLayer]:
        """Retrieves an iterator over product layers.

        Returns:
            Iterator[ProductLayer]:
        """
        return (sl for sl in self.layers if isinstance(sl, ProductLayer))

    @staticmethod
    def from_layer(sl: Layer) -> "CircuitBlock":
        """Instantiate a circuit block from a single layer.

        Args:
            sl: The layer.

        Returns:
            CircuitBlock: The circuit block consisting of only one layer.
        """
        return CircuitBlock([sl], {}, sl)

    @staticmethod
    def from_layer_composition(*layers: Layer) -> "CircuitBlock":
        """Instantiate a circuit block from a composition of multiple layers.
         The ordering of the composition is given by the ordering of the layers.

        Args:
            layers: A sequence of layers.

        Returns:
            CircuitBlock: The circuit block consisting of a composition of layers.

        Raises:
            ValueError: If the given sequence of layers consists of less than two layers.
        """
        layers = list(layers)
        in_layers = {}
        if len(layers) <= 1:
            raise ValueError("Expected a composition of at least 2 layers")
        for i, sl in enumerate(layers):
            in_layers[sl] = [layers[i - 1]] if i - 1 >= 0 else []
        return CircuitBlock(layers, in_layers, layers[-1])

    @staticmethod
    def from_nary_layer(lout: Layer, *ls: InputLayer) -> "CircuitBlock":
        """Instantiate a circuit block consisting of an output layer having
        multiple layers as inputs.

        Args:
            lout: The output layer.
            *ls: A sequence of inpput layers.

        Returns:
            CircuitBlock: The circuit block consisting of an output layer with several
                input layers as inputs.
        """
        layers = [lout, *ls]
        in_layers = {lout: list(ls)}
        return CircuitBlock(layers, in_layers, lout)

inner_layers property ¤

Retrieves an iterator over inner layers (i.e., layers that have at least one input).

Returns:

Type Description
Iterator[SumLayer | ProductLayer]

Iterator[Union[SumLayer, ProductLayer]]:

layers property ¤

Retrieves a sequence of layers.

Returns:

Type Description
Sequence[Layer]

Sequence[layer]:

layers_inputs property ¤

Retrieves the dictionary containing the sequence of inputs to each layer.

Returns:

Type Description
Mapping[Layer, Sequence[Layer]]

Mapping[Layer, Sequence[Layer]]:

layers_outputs property ¤

Retrieves the dictionary containing the sequence of outputs of each layer.

Returns:

Type Description
Mapping[Layer, Sequence[Layer]]

Mapping[Layer, Sequence[Layer]]:

product_layers property ¤

Retrieves an iterator over product layers.

Returns:

Type Description
Iterator[ProductLayer]

Iterator[ProductLayer]:

sum_layers property ¤

Retrieves an iterator over sum layers.

Returns:

Type Description
Iterator[SumLayer]

Iterator[SumLayer]:

__init__(layers, in_layers, output) ¤

Initializes a circuit block.

Parameters:

Name Type Description Default
layers Sequence[Layer]

The sequence of layers in the block.

required
in_layers Mapping[Layer, list[Layer]]

A dictionary containing the list of inputs to each layer.

required
output Layer

The root (or output) of the circuit block.

required
Source code in cirkit/symbolic/circuit.py
84
85
86
87
88
89
90
91
92
93
94
def __init__(
    self, layers: Sequence[Layer], in_layers: Mapping[Layer, list[Layer]], output: Layer
):
    """Initializes a circuit block.

    Args:
        layers: The sequence of layers in the block.
        in_layers: A dictionary containing the list of inputs to each layer.
        output: The root (or output) of the circuit block.
    """
    super().__init__(layers, in_layers, [output])

from_layer(sl) staticmethod ¤

Instantiate a circuit block from a single layer.

Parameters:

Name Type Description Default
sl Layer

The layer.

required

Returns:

Name Type Description
CircuitBlock CircuitBlock

The circuit block consisting of only one layer.

Source code in cirkit/symbolic/circuit.py
172
173
174
175
176
177
178
179
180
181
182
@staticmethod
def from_layer(sl: Layer) -> "CircuitBlock":
    """Instantiate a circuit block from a single layer.

    Args:
        sl: The layer.

    Returns:
        CircuitBlock: The circuit block consisting of only one layer.
    """
    return CircuitBlock([sl], {}, sl)

from_layer_composition(*layers) staticmethod ¤

Instantiate a circuit block from a composition of multiple layers. The ordering of the composition is given by the ordering of the layers.

Parameters:

Name Type Description Default
layers Layer

A sequence of layers.

()

Returns:

Name Type Description
CircuitBlock CircuitBlock

The circuit block consisting of a composition of layers.

Raises:

Type Description
ValueError

If the given sequence of layers consists of less than two layers.

Source code in cirkit/symbolic/circuit.py
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
@staticmethod
def from_layer_composition(*layers: Layer) -> "CircuitBlock":
    """Instantiate a circuit block from a composition of multiple layers.
     The ordering of the composition is given by the ordering of the layers.

    Args:
        layers: A sequence of layers.

    Returns:
        CircuitBlock: The circuit block consisting of a composition of layers.

    Raises:
        ValueError: If the given sequence of layers consists of less than two layers.
    """
    layers = list(layers)
    in_layers = {}
    if len(layers) <= 1:
        raise ValueError("Expected a composition of at least 2 layers")
    for i, sl in enumerate(layers):
        in_layers[sl] = [layers[i - 1]] if i - 1 >= 0 else []
    return CircuitBlock(layers, in_layers, layers[-1])

from_nary_layer(lout, *ls) staticmethod ¤

Instantiate a circuit block consisting of an output layer having multiple layers as inputs.

Parameters:

Name Type Description Default
lout Layer

The output layer.

required
*ls InputLayer

A sequence of inpput layers.

()

Returns:

Name Type Description
CircuitBlock CircuitBlock

The circuit block consisting of an output layer with several input layers as inputs.

Source code in cirkit/symbolic/circuit.py
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
@staticmethod
def from_nary_layer(lout: Layer, *ls: InputLayer) -> "CircuitBlock":
    """Instantiate a circuit block consisting of an output layer having
    multiple layers as inputs.

    Args:
        lout: The output layer.
        *ls: A sequence of inpput layers.

    Returns:
        CircuitBlock: The circuit block consisting of an output layer with several
            input layers as inputs.
    """
    layers = [lout, *ls]
    in_layers = {lout: list(ls)}
    return CircuitBlock(layers, in_layers, lout)

layer_inputs(sl) ¤

Retrieves the inputs to a layer.

Parameters:

Name Type Description Default
sl Layer

The layer.

required

Returns:

Type Description
Sequence[Layer]

Sequence[Layer]: The sequence of inputs.

Source code in cirkit/symbolic/circuit.py
 96
 97
 98
 99
100
101
102
103
104
105
def layer_inputs(self, sl: Layer) -> Sequence[Layer]:
    """Retrieves the inputs to a layer.

    Args:
        sl: The layer.

    Returns:
        Sequence[Layer]: The sequence of inputs.
    """
    return self.node_inputs(sl)

layer_outputs(sl) ¤

Retrieves the outputs of a layer.

Parameters:

Name Type Description Default
sl Layer

The layer.

required

Returns:

Type Description
Sequence[Layer]

Sequence[Layer]: The sequence of outputs.

Source code in cirkit/symbolic/circuit.py
107
108
109
110
111
112
113
114
115
116
def layer_outputs(self, sl: Layer) -> Sequence[Layer]:
    """Retrieves the outputs of a layer.

    Args:
        sl: The layer.

    Returns:
        Sequence[Layer]: The sequence of outputs.
    """
    return self.node_outputs(sl)

CircuitOperation dataclass ¤

The symbolic operation that is applied to obtain a symbolic circuit.

Source code in cirkit/symbolic/circuit.py
64
65
66
67
68
69
70
71
72
73
@dataclass(frozen=True)
class CircuitOperation:
    """The symbolic operation that is applied to obtain a symbolic circuit."""

    operator: CircuitOperator
    """The circuit operator of the operation."""
    operands: tuple["Circuit", ...]
    """The circuit operands of the operation."""
    metadata: dict[str, Any] = field(default_factory=dict)
    """Optional metadata of the operation."""

metadata = field(default_factory=dict) class-attribute instance-attribute ¤

Optional metadata of the operation.

operands instance-attribute ¤

The circuit operands of the operation.

operator instance-attribute ¤

The circuit operator of the operation.

__init__(operator, operands, metadata=dict()) ¤

CircuitOperator ¤

Bases: IntEnum

The available symbolic operators defined over circuits.

Source code in cirkit/symbolic/circuit.py
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
class CircuitOperator(IntEnum):
    """The available symbolic operators defined over circuits."""

    CONCATENATE = auto()
    """The concatenation operator defined over many circuits."""
    EVIDENCE = auto()
    """The evidence operator defined over a circuit."""
    INTEGRATION = auto()
    """The integration operator defined over a circuit."""
    DIFFERENTIATION = auto()
    """The differentiation operator defined over a circuit."""
    MULTIPLICATION = auto()
    """The multiplication operator defined over two circuits."""
    CONJUGATION = auto()
    """The conjugatation operator defined over a circuit computing a complex function."""

CONCATENATE = auto() class-attribute instance-attribute ¤

The concatenation operator defined over many circuits.

CONJUGATION = auto() class-attribute instance-attribute ¤

The conjugatation operator defined over a circuit computing a complex function.

DIFFERENTIATION = auto() class-attribute instance-attribute ¤

The differentiation operator defined over a circuit.

EVIDENCE = auto() class-attribute instance-attribute ¤

The evidence operator defined over a circuit.

INTEGRATION = auto() class-attribute instance-attribute ¤

The integration operator defined over a circuit.

MULTIPLICATION = auto() class-attribute instance-attribute ¤

The multiplication operator defined over two circuits.

StructuralProperties dataclass ¤

The available structural properties of a circuit.

Source code in cirkit/symbolic/circuit.py
33
34
35
36
37
38
39
40
41
42
43
44
@dataclass(frozen=True)
class StructuralProperties:
    """The available structural properties of a circuit."""

    smooth: bool
    """Whether the circuit is smooth."""
    decomposable: bool
    """Whether the circuit is decomposable."""
    structured_decomposable: bool
    """Whether the circuit is structured-decomposable, i.e., is compatible with itself."""
    omni_compatible: bool
    """Whether the circuit is omni-compatible, i.e., compatible to a fully-factorized circuit."""

decomposable instance-attribute ¤

Whether the circuit is decomposable.

omni_compatible instance-attribute ¤

Whether the circuit is omni-compatible, i.e., compatible to a fully-factorized circuit.

smooth instance-attribute ¤

Whether the circuit is smooth.

structured_decomposable instance-attribute ¤

Whether the circuit is structured-decomposable, i.e., is compatible with itself.

__init__(smooth, decomposable, structured_decomposable, omni_compatible) ¤

StructuralPropertyError ¤

Bases: Exception

An exception that is raised when an error regarding one circuit structural property occurs.

Source code in cirkit/symbolic/circuit.py
20
21
22
23
24
25
26
27
28
29
30
class StructuralPropertyError(Exception):
    """An exception that is raised when an error regarding one circuit structural property
    occurs."""

    def __init__(self, msg: str):
        """Initializes a structural property error with a message.

        Args:
            msg: The message.
        """
        super().__init__(msg)

__init__(msg) ¤

Initializes a structural property error with a message.

Parameters:

Name Type Description Default
msg str

The message.

required
Source code in cirkit/symbolic/circuit.py
24
25
26
27
28
29
30
def __init__(self, msg: str):
    """Initializes a structural property error with a message.

    Args:
        msg: The message.
    """
    super().__init__(msg)

_are_compatible(sfs1, sfs2) ¤

Source code in cirkit/symbolic/circuit.py
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
def _are_compatible(
    sfs1: dict[Scope, set[tuple[Scope, ...]]], sfs2: dict[Scope, set[tuple[Scope, ...]]]
) -> bool:
    # Check if two scope factorizations are compatible
    # TODO: how to allow for possible product layer rearrangements?
    for scope, fs1 in sfs1.items():
        fs2 = sfs2.get(scope, None)
        if fs2 is None:
            return False
        if len(fs1) != 1 or len(fs2) != 1:
            return False
        f1 = fs1.pop()
        f2 = fs2.pop()
        if f1 != f2:
            return False
    return True

_scope_factorizations(sc) ¤

Source code in cirkit/symbolic/circuit.py
544
545
546
547
548
549
550
551
552
553
554
555
def _scope_factorizations(sc: Circuit) -> dict[Scope, set[tuple[Scope, ...]]]:
    # For each product layer, retrieves how it factorizes its scope
    scope_factorizations: dict[Scope, set[tuple[Scope, ...]]] = defaultdict(set)
    for sl in sc.product_layers:
        sl_scope = sc.layer_scope(sl)
        fs = tuple(sorted(sc.layer_scope(sli) for sli in sc.layer_inputs(sl)))
        # Remove empty scopes that appear in the factorization
        fs = tuple(s for s in fs if s)
        # Add it to the scope factorizations only if it is a factorization
        if len(fs) > 1:
            scope_factorizations[sl_scope].add(fs)
    return scope_factorizations

are_compatible(sc1, sc2) ¤

Check if two symbolic circuits are compatible. Note that compatibility is a commutative property of circuits.

Parameters:

Name Type Description Default
sc1 Circuit

The first symbolic circuit.

required
sc2 Circuit

The second symbolic circuit.

required

Returns:

Name Type Description
bool bool

True if the first symbolic circuit is compatible with the second one.

Source code in cirkit/symbolic/circuit.py
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
def are_compatible(sc1: Circuit, sc2: Circuit) -> bool:
    """Check if two symbolic circuits are compatible.
     Note that compatibility is a commutative property of circuits.

    Args:
        sc1: The first symbolic circuit.
        sc2: The second symbolic circuit.

    Returns:
        bool: True if the first symbolic circuit is compatible with the second one.
    """
    if not sc1.is_smooth:
        return False
    if not sc1.is_decomposable:
        return False
    if not sc2.is_smooth:
        return False
    if not sc2.is_decomposable:
        return False
    sfs1 = _scope_factorizations(sc1)
    sfs2 = _scope_factorizations(sc2)
    return _are_compatible(sfs1, sfs2)

pipeline_topological_ordering(roots) ¤

Retrieves the topological ordering of circuits in a pipeline, given a sequence of root (or output) symbolic circuits in a pipeline.

Parameters:

Name Type Description Default
roots Sequence[Circuit]

The sequence of root (or output) symbolic circuits in a pipeline.

required

Returns:

Type Description
Iterator[Circuit]

Iterator[Circuit]: An iterator of the topological ordering of circuits in a pipeline.

Source code in cirkit/symbolic/circuit.py
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
def pipeline_topological_ordering(roots: Sequence[Circuit]) -> Iterator[Circuit]:
    """Retrieves the topological ordering of circuits in a pipeline, given a sequence of
     root (or output) symbolic circuits in a pipeline.

    Args:
        roots: The sequence of root (or output) symbolic circuits in a pipeline.

    Returns:
        Iterator[Circuit]: An iterator of the topological ordering of circuits in a pipeline.
    """

    def _operands_fn(sc: Circuit) -> tuple[Circuit, ...]:
        return () if sc.operation is None else sc.operation.operands

    return topological_ordering(bfs(roots, incomings_fn=_operands_fn), incomings_fn=_operands_fn)