4.3. Circuit

The lowest layer of representation, used as foundation for QEC, is defined to be a circuit. In Loom, a circuit is defined as a sequence of operations that are applied to a set of data registers over discrete time steps, with the only constraint being that no two operations can be applied to the same data register at the same time step. This is a very flexible definition, which allows us to represent any quantum and/or classical circuit and is used to create more complex representations, such as quantum error correction schemes and high-level quantum algorithms involving QEC.

4.3.1. Channels

The data registers, called channels in Loom, are represented by the Channel class. A channel can hold either classical or quantum data. A Channel is uniquely identified by its id, it can also have a label and a type (either classical or quantum). The ChannelType of a channel is assumed to be constant throughout the lifetime of the channel. The example below shows how to create channels in Loom:

from loom.eka.circuit import Channel, ChannelType

# Create a quantum channel, with a given label. Id is generated automatically.
chan = Channel(label="my_channel", type=ChannelType.QUANTUM)

# Default constructor is supported, it creates a quantum channel with a default label.
chan_default = Channel()

4.3.2. Circuits

The circuits in Loom are represented by the Circuit class, which uses a recursive structure. An instance of Circuit is defined as a sequence of circuits and the set of channels it acts on. At the lowest level, we have operations (often called gates) that are represented as Circuit with an empty sequence and a set of targeted data channels. The following example shows how to create gates and simple circuits in Loom :

# Create a quantum channel
from loom.eka.circuit import ChannelType, Circuit, Channel


channel = Channel(label="my_channel_1", type=ChannelType.QUANTUM)

# Create a gate that applies to the channel
gate = Circuit(name="my_gate", circuit=(), channels=[channel])

# Create a circuit that applies the gate twice in series
# When creating circuits with non-empty circuit sequence, channels are automatically inferred from the sequence.
circuit = Circuit(
    name="my_circuit",
    circuit=(gate, gate),
)

print(circuit)

# Output:
# my_circuit
# 0: my_gate
# 1: my_gate

Loom’s framework is meant to be as flexible as possible; therefore, one can create operations with any names and any number of classical/quantum channels. These are only abstract representations.

In circuits, we typically want to have operations acting on disjoint channels to be run in parallel. This can be done by wrapping the parallel circuit elements in a tuple. Within an instance of Circuit, the sequence of operations has to contain either Circuit instances or tuples of Circuit instances, but not both, so make sure to wrap all the elements in tuples. The example below shows how to create a circuit with parallel operations:

c1 = Channel(label="my_channel_1", type=ChannelType.QUANTUM)
c2 = Channel(label="my_channel_2", type=ChannelType.QUANTUM)

gate_on_1 = Circuit(name="g1", channels=[c1])
gate_on_2 = Circuit(name="g2", channels=[c2])
# Create multiple channel gate
gate_on_1_and_2 = Circuit(name="g12", channels=[c1, c2])

# All gates applied in series
s_circuit = Circuit(
    name="s_circuit",
    circuit=(gate_on_1, gate_on_2, gate_on_1_and_2),
)

print(s_circuit)

# Output:
# s_circuit
# 0: g1
# 1: g2
# 2: g12


# Gate 1 and 2 applied in parallel
p_circuit = Circuit(
    name="p_circuit",
    circuit=(
        (gate_on_1, gate_on_2),  # parallel gates
        (gate_on_1_and_2,),  # has to be wrapped in a tuple
    ),
)

print(p_circuit)

# Output:
# p_circuit
# 0: g1 g2
# 1: g12

Important note: When using parallel execution (i.e., providing a tuple(tuple(Circuit, …), …) as circuit parameter), the elements within the sequence of tuples will be executed on the step corresponding to their index in the sequence, regardless of the duration of the previous elements. This can lead to an unexpected error situation where 2 gates are applied to the same channel at the same time step. Loom leaves the user freedom on how to pad the circuit (you may use empty tuples to do so). The example below illustrates this:

from loom.eka.circuit import Channel, ChannelType, Circuit

chan1 = Channel(label="my_channel_1", type=ChannelType.QUANTUM)
chan2 = Channel(label="my_channel_2", type=ChannelType.QUANTUM)

# Two gates in series, each applied to a different channel.
long_subcircuit = Circuit(
    name="long_subcircuit",
    circuit=(
        Circuit(name="gate_1", channels=[chan1]),
        Circuit(name="gate_2", channels=[chan2]),
    ),
)

print(long_subcircuit)
# Output:
# long_subcircuit
# 0: gate_1
# 1: gate_2


# failing_circuit = Circuit(
#     name="failing_circuit",
#     circuit=[
#         [
#             long_subcircuit, # 2 ticks long
#         ],
#         [
#             Circuit(name="gate_1", channels=[chan1]),
#             Circuit(name="gate_2", channels=[chan2]),
#         ],
#     ],
# )

# This fails because it tries to do:
# tick 0: long_subcircuit tick 0: [gate_1, chan1]
# tick 1: [gate_1, gate_2] AND (long_subcircuit tick 1: [gate_2, chan2]) => 2 gates applied to chan2 at the same time

# We need to pad with empty tick:
valid_circuit = Circuit(
    name="valid_circuit",
    circuit=(
        (long_subcircuit,),  # 2 ticks long
        (),  # empty tick
        (
            Circuit(name="gate_1", channels=[chan1]),
            Circuit(name="gate_2", channels=[chan2]),
        ),
    ),
)

print(valid_circuit)

# Output:
# valid_circuit
# 0: long_subcircuit
# 1:
# 2: gate_1 gate_2

The Circuit class provides a method to automatically pad the circuit by padding with empty tuples after elements of duration more than 1. This may result in a suboptimal circuit :

# This can be done automatically:
valid_circuit = Circuit(
    name="valid_circuit",
    circuit=Circuit.construct_padded_circuit_time_sequence(  # Automatically pads the circuit
        (
            (long_subcircuit,),
            (
                Circuit(name="gate_1", channels=[chan1]),
                Circuit(name="gate_2", channels=[chan2]),
            ),
        ),
    ),
)

print(valid_circuit)
# Output:
# valid_circuit
# 0: long_subcircuit
# 1:
# 2: gate_1 gate_2

4.3.3. Utilities

The Circuit class provides a set of utilities to manipulate circuits:

  • flatten() :

    Returns a flattened copy of the circuit where the sub-sequence of circuit is a list of base operations (i.e., circuits with no sub-circuits). The parallel operations will be flattened and get executed in series.

  • unroll() :

    Unroll the recursive structure and provide a representation of the circuit as a sequence containing only base operations while preserving the time structure.

  • clone() :

    This allows us to clone a circuit structure and have it assigned to other channels.

  • detailed_str() :

    Which provides a string expression for visualization of the circuit in more detail, including which channel the operations are applied to.

Some extra utilities exist for defining circuits independently of channels.

  • from_circuits() :

    Build a Circuit object from a representation of its content with relative qubits indices.

  • as_gate():

    Create a gate without needing to specify channels (they will be automatically generated).