Skip to content

Block system

qadence offers a block-based system to construct quantum circuits in a flexible manner.

AbstractBlock(tag=None, __array_priority__=1000) dataclass

Bases: ABC

Base class for both primitive and composite blocks.

ATTRIBUTE DESCRIPTION
name

A human-readable name attached to the block type. Notice, this is the same for all the class instances so it cannot be used for identifying different blocks

TYPE: str

qubit_support

The qubit support of the block expressed as a tuple of integers

TYPE: tuple[int, ...]

tag

A tag identifying a particular instance of the block which can be used for identification and pretty printing

TYPE: str | None

eigenvalues

The eigenvalues of the matrix representing the block. This is used mainly for primitive blocks and it's needed for generalized parameter shift rule computations. Currently unused.

TYPE: list[float] | None

is_identity: bool property

Identity predicate for blocks.

n_qubits()

The number of qubits in the whole system.

A block acting on qubit N would has at least n_qubits >= N + 1.

Source code in qadence/blocks/abstract.py
@abstractproperty
def n_qubits(self) -> int:
    """The number of qubits in the whole system.

    A block acting on qubit N would has at least n_qubits >= N + 1.
    """
    pass

n_supports()

The number of qubits the block is acting on.

Source code in qadence/blocks/abstract.py
@abstractproperty
def n_supports(self) -> int:
    """The number of qubits the block is acting on."""
    pass

qubit_support()

The indices of the qubit(s) the block is acting on.

Qadence uses the ordering [0..,N-1] for qubits.

Source code in qadence/blocks/abstract.py
@abstractproperty
def qubit_support(self) -> Tuple[int, ...]:
    """The indices of the qubit(s) the block is acting on.

    Qadence uses the ordering [0..,N-1] for qubits.
    """
    pass

Primitive blocks

ControlBlock(control, target_block)

Bases: PrimitiveBlock

The abstract ControlBlock.

Source code in qadence/blocks/primitive.py
def __init__(self, control: tuple[int, ...], target_block: PrimitiveBlock) -> None:
    self.control = control
    self.blocks = (target_block,)
    self.target = target_block.qubit_support

    # using tuple expansion because some control operations could
    # have multiple targets, e.g. CSWAP
    super().__init__((*control, *self.target))  # target_block.qubit_support[0]))

ParametricBlock(qubit_support) dataclass

Bases: PrimitiveBlock

Parameterized primitive blocks.

Source code in qadence/blocks/primitive.py
def __init__(self, qubit_support: tuple[int, ...]):
    self._qubit_support = qubit_support

num_parameters() abstractmethod

The number of parameters required by the block.

This is a class property since the number of parameters is defined automatically before instantiating the operation. Also, this could correspond to a larger number of actual user-facing parameters since any parameter expression is allowed

Examples: - RX operation has 1 parameter - U operation has 3 parameters - HamEvo has 2 parameters (generator and time evolution)

Source code in qadence/blocks/primitive.py
@abstractmethod
def num_parameters(cls) -> int:
    """The number of parameters required by the block.

    This is a class property since the number of parameters is defined
    automatically before instantiating the operation. Also, this could
    correspond to a larger number of actual user-facing parameters
    since any parameter expression is allowed

    Examples:
    - RX operation has 1 parameter
    - U operation has 3 parameters
    - HamEvo has 2 parameters (generator and time evolution)
    """
    pass

ParametricControlBlock(control, target_block)

Bases: ParametricBlock

The abstract parametrized ControlBlock.

Source code in qadence/blocks/primitive.py
def __init__(self, control: tuple[int, ...], target_block: ParametricBlock) -> None:
    self.blocks = (target_block,)
    self.control = control
    self.parameters = target_block.parameters
    super().__init__((*control, *target_block.qubit_support))

PrimitiveBlock(qubit_support)

Bases: AbstractBlock

Primitive blocks represent elementary unitary operations.

Examples are single/multi-qubit gates or Hamiltonian evolution. See qadence.operations for a full list of primitive blocks.

Source code in qadence/blocks/primitive.py
def __init__(self, qubit_support: tuple[int, ...]):
    self._qubit_support = qubit_support

digital_decomposition()

Decomposition into purely digital gates.

This method returns a decomposition of the Block in a combination of purely digital single-qubit and two-qubit 'gates', by manual/custom knowledge of how this can be done efficiently. :return:

Source code in qadence/blocks/primitive.py
def digital_decomposition(self) -> AbstractBlock:
    """Decomposition into purely digital gates.

    This method returns a decomposition of the Block in a
    combination of purely digital single-qubit and two-qubit
    'gates', by manual/custom knowledge of how this can be done efficiently.
    :return:
    """
    return self

ProjectorBlock(ket, bra, qubit_support)

Bases: PrimitiveBlock

The abstract ProjectorBlock.

Arguments:

ket (str): The ket given as a bitstring.
bra (str): The bra given as a bitstring.
qubit_support (int | tuple[int]): The qubit_support of the block.
Source code in qadence/blocks/primitive.py
def __init__(
    self,
    ket: str,
    bra: str,
    qubit_support: int | tuple[int, ...],
) -> None:
    """
    Arguments:

        ket (str): The ket given as a bitstring.
        bra (str): The bra given as a bitstring.
        qubit_support (int | tuple[int]): The qubit_support of the block.
    """
    if isinstance(qubit_support, int):
        qubit_support = (qubit_support,)
    if len(bra) != len(ket):
        raise ValueError(
            "Bra and ket must be bitstrings of same length in the 'Projector' definition."
        )
    elif len(bra) != len(qubit_support):
        raise ValueError("Bra or ket must be of same length as the 'qubit_support'")
    for wf in [bra, ket]:
        if not all(int(item) == 0 or int(item) == 1 for item in wf):
            raise ValueError(
                "All qubits must be either in the '0' or '1' state"
                " in the 'ProjectorBlock' definition."
            )

    self.ket = ket
    self.bra = bra
    super().__init__(qubit_support)

ScaleBlock(block, parameter)

Bases: ParametricBlock

Scale blocks are created when multiplying a block by a number or parameter.

Example:

from qadence import X

print(X(0) * 2)
[mul: 2] 
└── X(0)

Source code in qadence/blocks/primitive.py
def __init__(self, block: AbstractBlock, parameter: Any):
    self.block = block
    # TODO: more meaningful name like `scale`?
    self.parameters = (
        parameter if isinstance(parameter, ParamMap) else ParamMap(parameter=parameter)
    )
    super().__init__(block.qubit_support)

TimeEvolutionBlock(qubit_support) dataclass

Bases: ParametricBlock

Simple time evolution block with time-independent Hamiltonian.

This class is just a convenience class which is used to label blocks which contains simple time evolution with time-independent Hamiltonian operators

Source code in qadence/blocks/primitive.py
def __init__(self, qubit_support: tuple[int, ...]):
    self._qubit_support = qubit_support

Analog blocks

To learn how to use analog blocks and how to mix digital & analog blocks, check out the digital-analog section of the documentation.

Examples on how to use digital-analog blocks can be found in the *examples folder of the qadence repo:

  • Fit a simple sinus: examples/digital-analog/fit-sin.py
  • Solve a QUBO: examples/digital-analog/qubo.py

AnalogChain(blocks) dataclass

Bases: AnalogComposite

A chain of analog blocks.

Needed because analog blocks require stricter validation than the general ChainBlock.

AnalogChains can only be constructed from AnalogKron blocks or globally supported, primitive, analog blocks (like InteractionBlocks and ConstantAnalogRotations).

Automatically constructed by the chain function if only analog blocks are given.

Example:

from qadence import X, chain, AnalogInteraction

b = chain(AnalogInteraction(200), AnalogInteraction(200))
print(type(b))  # this is an `AnalogChain`

b = chain(X(0), AnalogInteraction(200))
print(type(b))  # this is a general `ChainBlock`
<class 'qadence.blocks.analog.AnalogChain'>
<class 'qadence.blocks.composite.ChainBlock'>

Source code in qadence/blocks/analog.py
def __init__(self, blocks: Tuple[AnalogBlock, ...]):
    """A chain of analog blocks.

    Needed because analog blocks require
    stricter validation than the general `ChainBlock`.

    `AnalogChain`s can only be constructed from `AnalogKron` blocks or
    _**globally supported**_, primitive, analog blocks (like `InteractionBlock`s and
    `ConstantAnalogRotation`s).

    Automatically constructed by the [`chain`][qadence.blocks.utils.chain]
    function if only analog blocks are given.

    Example:
    ```python exec="on" source="material-block" result="json"
    from qadence import X, chain, AnalogInteraction

    b = chain(AnalogInteraction(200), AnalogInteraction(200))
    print(type(b))  # this is an `AnalogChain`

    b = chain(X(0), AnalogInteraction(200))
    print(type(b))  # this is a general `ChainBlock`
    ```
    """
    for b in blocks:
        if not (isinstance(b, AnalogKron) or b.qubit_support.is_global):
            raise ValueError("Only KronBlocks or global blocks can be chain'ed.")
    self.blocks = blocks

AnalogKron(blocks, interaction=Interaction.NN) dataclass

Bases: AnalogComposite

Stack analog blocks vertically (i.e. in time).

Needed because analog require stricter validation than the general KronBlock.

AnalogKrons can only be constructed from non-global, analog blocks with the same duration.

Source code in qadence/blocks/analog.py
def __init__(self, blocks: Tuple[AnalogBlock, ...], interaction: Interaction = Interaction.NN):
    """Stack analog blocks vertically (i.e. in time).

    Needed because analog require
    stricter validation than the general `KronBlock`.

    `AnalogKron`s can only be constructed from _**non-global**_, analog blocks
    with the _**same duration**_.
    """
    if len(blocks) == 0:
        raise NotImplementedError("Empty KronBlocks not supported")

    self.blocks = blocks
    self.interaction = interaction

    qubit_support = QubitSupport()
    duration = blocks[0].duration
    for b in blocks:
        if not isinstance(b, AnalogBlock):
            raise ValueError("Can only kron `AnalgoBlock`s with other `AnalgoBlock`s.")

        if b.qubit_support == QubitSupport("global"):
            raise ValueError("Blocks with global support cannot be kron'ed.")

        if not qubit_support.is_disjoint(b.qubit_support):
            raise ValueError("Make sure blocks act on distinct qubits!")

        if not np.isclose(evaluate(duration), evaluate(b.duration)):
            raise ValueError("Kron'ed blocks have to have same duration.")

        qubit_support += b.qubit_support

    self.blocks = blocks

ConstantAnalogRotation(tag=None, __array_priority__=1000, _eigenvalues_generator=None, parameters=ParamMap(alpha=0.0, duration=1000.0, omega=0.0, delta=0.0, phase=0.0), qubit_support=QubitSupport('global'), add_pattern=True) dataclass

Bases: AnalogBlock

Implements a constant analog rotation with interaction dictated by the chosen Hamiltonian.

H/h = ∑ᵢ(Ω/2 cos(φ)*Xᵢ - sin(φ)*Yᵢ - δnᵢ) + Hᵢₙₜ.

To construct this block you can use of the following convenience wrappers: - The general rotation operation AnalogRot - Shorthands for rotatins around an axis: AnalogRX, AnalogRY, AnalogRZ

WARNING: do not use ConstantAnalogRotation with alpha as differentiable parameter - use the convenience wrappers mentioned above.

InteractionBlock(tag=None, __array_priority__=1000, _eigenvalues_generator=None, parameters=ParamMap(duration=1000.0), qubit_support=QubitSupport('global'), add_pattern=True) dataclass

Bases: AnalogBlock

Free-evolution for the Hamiltonian interaction term of a register of qubits.

In real interacting quantum devices, it means letting the system evolve freely according to the time-dependent Schrodinger equation. With emulators, this block is translated to an appropriate interaction Hamiltonian, for example, an Ising interaction

Hᵢₙₜ = ∑ᵢⱼ C₆/rᵢⱼ⁶ nᵢnⱼ

or an XY-interaction

Hᵢₙₜ = ∑ᵢⱼ C₃/rⱼⱼ³ (XᵢXⱼ + ZᵢZⱼ)

with nᵢ = (1-Zᵢ)/2.

To construct, use the AnalogInteraction function.

Composite blocks

chain(*args)

Chain blocks sequentially.

On digital backends this can be interpreted loosely as a matrix mutliplication of blocks. In the analog case it chains blocks in time.

PARAMETER DESCRIPTION
*args

Blocks to chain. Can also be a generator.

TYPE: Union[AbstractBlock, Generator, List[AbstractBlock]] DEFAULT: ()

RETURNS DESCRIPTION
ChainBlock

ChainBlock

Example:

from qadence import X, Y, chain

b = chain(X(0), Y(0))

# or use a generator
b = chain(X(i) for i in range(3))
print(b)
ChainBlock(0,1,2)
├── X(0)
├── X(1)
└── X(2)

Source code in qadence/blocks/utils.py
def chain(*args: Union[AbstractBlock, Generator, List[AbstractBlock]]) -> ChainBlock:
    """Chain blocks sequentially.

    On digital backends this can be interpreted
    loosely as a matrix mutliplication of blocks. In the analog case it chains
    blocks in time.

    Arguments:
        *args: Blocks to chain. Can also be a generator.

    Returns:
        ChainBlock

    Example:
    ```python exec="on" source="material-block" result="json"
    from qadence import X, Y, chain

    b = chain(X(0), Y(0))

    # or use a generator
    b = chain(X(i) for i in range(3))
    print(b)
    ```
    """
    # ugly hack to use `AnalogChain` if we are dealing only with analog blocks
    if len(args) and all(
        isinstance(a, AnalogBlock) or isinstance(a, AnalogComposite) for a in args
    ):
        return analog_chain(*args)  # type: ignore[return-value,arg-type]
    return _construct(ChainBlock, args)

kron(*args)

Stack blocks vertically.

On digital backends this can be intepreted loosely as a kronecker product of blocks. In the analog case it executes blocks parallel in time.

PARAMETER DESCRIPTION
*args

Blocks to kron. Can also be a generator.

TYPE: Union[AbstractBlock, Generator] DEFAULT: ()

RETURNS DESCRIPTION
KronBlock

KronBlock

Example:

from qadence import X, Y, kron

b = kron(X(0), Y(1))

# or use a generator
b = kron(X(i) for i in range(3))
print(b)
KronBlock(0,1,2)
├── X(0)
├── X(1)
└── X(2)

Source code in qadence/blocks/utils.py
def kron(*args: Union[AbstractBlock, Generator]) -> KronBlock:
    """Stack blocks vertically.

    On digital backends this can be intepreted
    loosely as a kronecker product of blocks. In the analog case it executes
    blocks parallel in time.

    Arguments:
        *args: Blocks to kron. Can also be a generator.

    Returns:
        KronBlock

    Example:
    ```python exec="on" source="material-block" result="json"
    from qadence import X, Y, kron

    b = kron(X(0), Y(1))

    # or use a generator
    b = kron(X(i) for i in range(3))
    print(b)
    ```
    """
    # ugly hack to use `AnalogKron` if we are dealing only with analog blocks
    if len(args) and all(
        isinstance(a, AnalogBlock) or isinstance(a, AnalogComposite) for a in args
    ):
        return analog_kron(*args)  # type: ignore[return-value,arg-type]
    return _construct(KronBlock, args)

add(*args)

Sums blocks.

PARAMETER DESCRIPTION
*args

Blocks to add. Can also be a generator.

TYPE: Union[AbstractBlock, Generator] DEFAULT: ()

RETURNS DESCRIPTION
AddBlock

AddBlock

Example:

from qadence import X, Y, add

b = add(X(0), Y(0))

# or use a generator
b = add(X(i) for i in range(3))
print(b)
AddBlock(0,1,2)
├── X(0)
├── X(1)
└── X(2)

Source code in qadence/blocks/utils.py
def add(*args: Union[AbstractBlock, Generator]) -> AddBlock:
    """Sums blocks.

    Arguments:
        *args: Blocks to add. Can also be a generator.

    Returns:
        AddBlock

    Example:
    ```python exec="on" source="material-block" result="json"
    from qadence import X, Y, add

    b = add(X(0), Y(0))

    # or use a generator
    b = add(X(i) for i in range(3))
    print(b)
    ```
    """
    return _construct(AddBlock, args)

AddBlock(blocks)

Bases: CompositeBlock

Adds blocks.

Constructed via add.

Source code in qadence/blocks/composite.py
def __init__(self, blocks: Tuple[AbstractBlock, ...]):
    self.blocks = blocks

ChainBlock(blocks)

Bases: CompositeBlock

Chains blocks sequentially.

Constructed via chain

Source code in qadence/blocks/composite.py
def __init__(self, blocks: Tuple[AbstractBlock, ...]):
    self.blocks = blocks

CompositeBlock(tag=None, __array_priority__=1000) dataclass

Bases: AbstractBlock

Block which composes multiple blocks into one larger block (which can again be composed).

Composite blocks are constructed via chain, kron, and add.

KronBlock(blocks)

Bases: CompositeBlock

Stacks blocks horizontally.

Constructed via kron.

Source code in qadence/blocks/composite.py
def __init__(self, blocks: Tuple[AbstractBlock, ...]):
    if len(blocks) == 0:
        raise NotImplementedError("Empty KronBlocks not supported")

    qubit_support = QubitSupport()
    for b in blocks:
        assert (
            QubitSupportType.GLOBAL,
        ) != b.qubit_support, "Blocks with global support cannot be kron'ed."
        assert qubit_support.is_disjoint(
            b.qubit_support
        ), "Make sure blocks act on distinct qubits!"
        qubit_support += b.qubit_support

    self.blocks = blocks

Converting blocks to matrices

block_to_tensor(block, values={}, qubit_support=None, use_full_support=True, tensor_type=TensorType.DENSE, endianness=Endianness.BIG, device=torch.device('cpu'))

Convert a block into a torch tensor.

PARAMETER DESCRIPTION
block

The block to convert.

TYPE: AbstractBlock

values

A optional dict with values for parameters.

TYPE: dict DEFAULT: {}

qubit_support

The qubit_support of the block.

TYPE: tuple DEFAULT: None

use_full_support

True infers the total number of qubits.

TYPE: bool DEFAULT: True

tensor_type

the target tensor type.

TYPE: TensorType DEFAULT: DENSE

RETURNS DESCRIPTION
Tensor

A torch.Tensor.

Examples:

from qadence import hea, hamiltonian_factory, Z, block_to_tensor

block = hea(2,2)
print(block_to_tensor(block))

# In case you have a diagonal observable, you can use
obs = hamiltonian_factory(2, detuning = Z)
print(block_to_tensor(obs, tensor_type="SparseDiagonal"))
tensor([[[ 0.4573+0.1608j, -0.4272-0.6223j, -0.3603+0.0430j, -0.2423-0.0701j],
         [ 0.0182-0.6188j,  0.4169+0.0030j, -0.4824-0.1323j, -0.4371+0.0416j],
         [-0.2220-0.4905j, -0.1368-0.3666j,  0.4005+0.0552j,  0.0338-0.6265j],
         [-0.0692-0.2954j, -0.2411-0.2126j, -0.0254-0.6738j,  0.4521+0.3816j]]],
       grad_fn=<UnsafeViewBackward0>)
tensor(indices=tensor([[0, 3],
                       [0, 3]]),
       values=tensor([ 2.+0.j, -2.+0.j]),
       size=(4, 4), nnz=2, layout=torch.sparse_coo)

Source code in qadence/blocks/block_to_tensor.py
def block_to_tensor(
    block: AbstractBlock,
    values: dict[str, TNumber | torch.Tensor] = {},
    qubit_support: tuple | None = None,
    use_full_support: bool = True,
    tensor_type: TensorType = TensorType.DENSE,
    endianness: Endianness = Endianness.BIG,
    device: torch.device = torch.device("cpu"),
) -> torch.Tensor:
    """
    Convert a block into a torch tensor.

    Arguments:
        block (AbstractBlock): The block to convert.
        values (dict): A optional dict with values for parameters.
        qubit_support (tuple): The qubit_support of the block.
        use_full_support (bool): True infers the total number of qubits.
        tensor_type (TensorType): the target tensor type.

    Returns:
        A torch.Tensor.

    Examples:
    ```python exec="on" source="material-block" result="json"
    from qadence import hea, hamiltonian_factory, Z, block_to_tensor

    block = hea(2,2)
    print(block_to_tensor(block))

    # In case you have a diagonal observable, you can use
    obs = hamiltonian_factory(2, detuning = Z)
    print(block_to_tensor(obs, tensor_type="SparseDiagonal"))
    ```
    """

    # FIXME: default use_full_support to False. In general, it would
    # be more efficient to do that, and make sure that computations such
    # as observables only do the matmul of the size of the qubit support.

    if tensor_type == TensorType.DENSE:
        from qadence.blocks import embedding

        (ps, embed) = embedding(block)
        return _block_to_tensor_embedded(
            block,
            embed(ps, values),
            qubit_support,
            use_full_support,
            endianness=endianness,
            device=device,
        )

    elif tensor_type == TensorType.SPARSEDIAGONAL:
        t = block_to_diagonal(block, endianness=endianness)
        indices, values, size = torch.nonzero(t), t[t != 0], len(t)
        indices = torch.stack((indices.flatten(), indices.flatten()))
        return torch.sparse_coo_tensor(indices, values, (size, size))